Skip to content

Commit

Permalink
Merge pull request #522 from jmarx/rsvp-form-token-field
Browse files Browse the repository at this point in the history
WIP: Rsvp form token field
  • Loading branch information
mauteri authored Feb 7, 2024
2 parents ec7b4c1 + 5862100 commit 6e5022b
Show file tree
Hide file tree
Showing 8 changed files with 182 additions and 9 deletions.
2 changes: 1 addition & 1 deletion build/blocks/events-list/style-index.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion build/blocks/rsvp-response/index.asset.php
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<?php return array('dependencies' => array('react', 'wp-block-editor', 'wp-blocks', 'wp-data', 'wp-element', 'wp-i18n'), 'version' => '9f907f9207968e2c6424');
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-core-data', 'wp-data', 'wp-element', 'wp-i18n'), 'version' => '180202f982b9bd07b444');
2 changes: 1 addition & 1 deletion build/blocks/rsvp-response/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/blocks/rsvp-response/style-index.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion includes/core/classes/class-rsvp.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ public function save( int $user_id, string $status, int $anonymous = 0, int $gue
);

// If not attending and anonymous, just remove record.
if ( 'not_attending' === $status && $anonymous ) {
if ( ( 'not_attending' === $status && $anonymous ) || 'no_status' === $status ) {
$save = $wpdb->delete( $table, $where ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$status = 'no_status'; // Set default status for UI.
} else {
Expand Down
39 changes: 35 additions & 4 deletions src/blocks/rsvp-response/edit.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
/**
* WordPress dependencies.
*/
import { useBlockProps } from '@wordpress/block-editor';
import { useBlockProps, BlockControls } from '@wordpress/block-editor';
import { ToolbarGroup, ToolbarButton } from '@wordpress/components';
import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies.
*/
import RsvpResponse from '../../components/RsvpResponse';
import RsvpResponseEdit from '../../components/RsvpResponseEdit';
import EditCover from '../../components/EditCover';

/**
Expand All @@ -19,14 +23,41 @@ import EditCover from '../../components/EditCover';
*
* @return {JSX.Element} The rendered React component.
*/

const Edit = () => {
const isAdmin = wp.data.select('core').canUser('create', 'posts');
const blockProps = useBlockProps();

const [editMode, setEditMode] = useState(false);

const onEditClick = (e) => {
e.preventDefault();
setEditMode(!editMode);
};

return (
<div {...blockProps}>
<EditCover>
<RsvpResponse />
</EditCover>
{editMode && <RsvpResponseEdit />}
{!editMode && (
<EditCover>
<RsvpResponse />
</EditCover>
)}
{isAdmin && (
<BlockControls>
<ToolbarGroup>
<ToolbarButton
label={__('Edit', 'gatherpress')}
text={
editMode
? __('Preview', 'gatherpress')
: __('Edit', 'gatherpress')
}
onClick={onEditClick}
/>
</ToolbarGroup>
</BlockControls>
)}
</div>
);
};
Expand Down
8 changes: 8 additions & 0 deletions src/blocks/rsvp-response/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,12 @@
font-weight: 600;
margin-bottom: 1rem;
}

// UI fix in editor.
.edit-post-visual-editor & {
.components-button.has-icon {
padding: 0 2px;
}
}
}

134 changes: 134 additions & 0 deletions src/components/RsvpResponseEdit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
* WordPress dependencies.
*/
import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { FormTokenField } from '@wordpress/components';
import apiFetch from '@wordpress/api-fetch';
import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';

/**
* Internal dependencies.
*/
import { getFromGlobal, setToGlobal } from '../helpers/globals';

/**
* Component for displaying and managing RSVP responses.
*
* This component renders a user interface for managing RSVP responses to an event.
* It includes options for attending, being on the waiting list, or not attending,
* and updates the status based on user interactions. The component also listens for
* changes in RSVP status and updates the state accordingly.
*
* @since 1.0.0
*
* @return {JSX.Element} The rendered RSVP response component.
*/
const RsvpResponseEdit = () => {
const responses = getFromGlobal('eventDetails.responses');
const postId = getFromGlobal('eventDetails.postId');
const [rsvpResponse, setRsvpResponse] = useState(responses);
const attendees = rsvpResponse.attending.responses;

/**
* Fetches user records from the core store via getEntityRecords.
* Returns userList containing the list of user records.
*/
const { userList } = useSelect((select) => {
const { getEntityRecords } = select(coreStore);

const users = getEntityRecords('root', 'user', {
per_page: -1,
});

return {
userList: users,
};
}, []);

/**
* Reduces the userList to an object mapping usernames to user objects.
* This provides convenient lookup from username to full user data.
*/
const userSuggestions =
userList?.reduce(
(accumulator, user) => ({
...accumulator,
[user.username]: user,
}),
{}
) ?? {};

/**
* Updates the RSVP status for a user attending the given event.
*
* @param {number} userId - The ID of the user to update.
* @param {string} [status='attending'] - The RSVP status to set (attending or remove).
*/
const updateUserStatus = (userId, status = 'attending') => {
apiFetch({
path: getFromGlobal('urls.eventRestApi') + '/rsvp',
method: 'POST',
data: {
post_id: postId,
status,
user_id: userId,
_wpnonce: getFromGlobal('misc.nonce'),
},
}).then((res) => {
setRsvpResponse(res.responses);
setToGlobal('eventDetails.responses', res.responses);
});
};

/**
* Updates the attendee list for the RSVP based on the provided tokens.
* If new tokens are added, new attendees will be added.
* If existing tokens are removed, the associated attendees will be removed.
*
* @param {Object[]} tokens - Array of token objects representing attendees
*/
const changeAttendees = async (tokens) => {
// Adding some new attendees
if (tokens.length > attendees.length) {
tokens.forEach((token) => {
if (!userSuggestions[token]) {
return;
}

// We have a new user to add to the attendees list.
updateUserStatus(userSuggestions[token].id, 'attending');
});
} else {
// Removing attendees
attendees.forEach((attendee) => {
if (false === tokens.some((item) => item.id === attendee.id)) {
updateUserStatus(attendee.id, 'no_status');
}
});
}
};

return (
<div className="gp-rsvp-response">
<FormTokenField
key="query-controls-topics-select"
label={__('Attendees', 'gatherpress')}
value={
attendees &&
attendees.map((item) => ({
id: item.id,
value: item.name,
}))
}
tokenizeOnSpace={true}
onChange={changeAttendees}
suggestions={Object.keys(userSuggestions)}
maxSuggestions={20}
/>
</div>
);
};

export default RsvpResponseEdit;

0 comments on commit 6e5022b

Please sign in to comment.