-
Notifications
You must be signed in to change notification settings - Fork 0
[Community][Admin] Member Management Page #198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| mutation AdminMemberAddContainerMemberAdd($input: MemberAddInput!) { | ||
| memberAdd(input: $input) { | ||
| status { | ||
| success | ||
| errorMessage | ||
| } | ||
| member { | ||
| ...AdminMemberAddContainerMemberFields | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fragment AdminMemberAddContainerMemberFields on Member { | ||
| id | ||
| memberName | ||
| accounts { | ||
| id | ||
| firstName | ||
| lastName | ||
| statusCode | ||
| } | ||
| createdAt | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import { App } from 'antd'; | ||
| import { useMutation } from '@apollo/client'; | ||
| import { AdminMemberAddContainerMemberAddDocument, AdminMemberListContainerMembersByCommunityIdDocument } from '../../../../generated.tsx'; | ||
| import { MemberAddModal } from './member-add-modal.tsx'; | ||
|
|
||
| export const MemberAddModalContainer: React.FC<{ communityId: string; open: boolean; onClose: () => void }> = ({ communityId, open, onClose }) => { | ||
| const { message } = App.useApp(); | ||
|
|
||
| const [addMember, { loading }] = useMutation(AdminMemberAddContainerMemberAddDocument, { | ||
| refetchQueries: [{ query: AdminMemberListContainerMembersByCommunityIdDocument, variables: { communityId } }], | ||
| }); | ||
|
|
||
| const handleAdd = async (values: { memberName: string; firstName: string; lastName?: string; userExternalId: string }) => { | ||
| try { | ||
| const result = await addMember({ | ||
| variables: { | ||
| input: { | ||
| communityId, | ||
| memberName: values.memberName, | ||
| firstName: values.firstName, | ||
| lastName: values.lastName, | ||
| userExternalId: values.userExternalId, | ||
| }, | ||
| }, | ||
| }); | ||
| if (result.data?.memberAdd?.status?.success) { | ||
| void message.success('Member added successfully'); | ||
| onClose(); | ||
| } else { | ||
| void message.error(result.data?.memberAdd?.status?.errorMessage ?? 'Failed to add member'); | ||
| } | ||
| } catch (error) { | ||
| void message.error((error as Error).message ?? 'Failed to add member'); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <MemberAddModal | ||
| open={open} | ||
| loading={loading} | ||
| onAdd={handleAdd} | ||
| onCancel={onClose} | ||
| /> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import type { Meta, StoryObj } from '@storybook/react'; | ||
| import { fn } from 'storybook/test'; | ||
| import { MemberAddModal } from './member-add-modal.tsx'; | ||
|
|
||
| const meta: Meta<typeof MemberAddModal> = { | ||
| title: 'Admin/MemberAddModal', | ||
| component: MemberAddModal, | ||
| args: { | ||
| open: true, | ||
| loading: false, | ||
| onAdd: fn(), | ||
| onCancel: fn(), | ||
| }, | ||
| }; | ||
|
|
||
| export default meta; | ||
| type Story = StoryObj<typeof MemberAddModal>; | ||
|
|
||
| export const Default: Story = {}; | ||
|
|
||
| export const Loading: Story = { | ||
| args: { | ||
| loading: true, | ||
| }, | ||
| }; | ||
|
|
||
| export const Closed: Story = { | ||
| args: { | ||
| open: false, | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| import { Button, Form, Input, Modal } from 'antd'; | ||
|
|
||
| interface MemberAddFormValues { | ||
| memberName: string; | ||
| firstName: string; | ||
| lastName?: string; | ||
| userExternalId: string; | ||
| } | ||
|
|
||
| export const MemberAddModal: React.FC<{ open: boolean; loading?: boolean; onAdd: (values: MemberAddFormValues) => void; onCancel: () => void }> = ({ open, loading, onAdd, onCancel }) => { | ||
| const [form] = Form.useForm<MemberAddFormValues>(); | ||
|
|
||
| const handleOk = () => { | ||
| form.validateFields().then((values) => { | ||
| onAdd(values); | ||
| form.resetFields(); | ||
| }); | ||
| }; | ||
|
|
||
| const handleCancel = () => { | ||
| form.resetFields(); | ||
| onCancel(); | ||
| }; | ||
|
|
||
| return ( | ||
| <Modal | ||
| title="Add Member" | ||
| open={open} | ||
| onCancel={handleCancel} | ||
| footer={[ | ||
| <Button | ||
| key="cancel" | ||
| onClick={handleCancel} | ||
| > | ||
| Cancel | ||
| </Button>, | ||
| <Button | ||
| key="submit" | ||
| type="primary" | ||
| loading={loading} | ||
| onClick={handleOk} | ||
| > | ||
| Add Member | ||
| </Button>, | ||
| ]} | ||
| > | ||
| <Form | ||
| form={form} | ||
| layout="vertical" | ||
| name="member-add-form" | ||
| > | ||
| <Form.Item | ||
| name="memberName" | ||
| label="Member Name" | ||
| rules={[{ required: true, message: 'Please enter a member name' }]} | ||
| > | ||
| <Input placeholder="e.g. John Doe" /> | ||
| </Form.Item> | ||
| <Form.Item | ||
| name="firstName" | ||
| label="First Name" | ||
| rules={[{ required: true, message: 'Please enter first name' }]} | ||
| > | ||
| <Input placeholder="First name" /> | ||
| </Form.Item> | ||
| <Form.Item | ||
| name="lastName" | ||
| label="Last Name" | ||
| > | ||
| <Input placeholder="Last name (optional)" /> | ||
| </Form.Item> | ||
| <Form.Item | ||
| name="userExternalId" | ||
| label="User External ID" | ||
| rules={[{ required: true, message: 'Please enter the user external ID' }]} | ||
| > | ||
| <Input placeholder="e.g. auth0|123456" /> | ||
| </Form.Item> | ||
| </Form> | ||
| </Modal> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| query AdminMemberListContainerMembersByCommunityId($communityId: ObjectID!) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (review_instructions): This container GraphQL file is under Per the instructions, container GraphQL files should live under a path like This file is currently located at If the convention is still accurate, this file should be moved/renamed so its path matches the Review instructions:Path patterns: Instructions: |
||
| membersByCommunityId(communityId: $communityId) { | ||
| ...AdminMemberListContainerMemberFields | ||
| } | ||
| } | ||
|
|
||
| mutation AdminMemberListContainerMemberRemove($input: MemberRemoveInput!) { | ||
| memberRemove(input: $input) { | ||
| status { | ||
| success | ||
| errorMessage | ||
| } | ||
| member { | ||
| ...AdminMemberListContainerMemberFields | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fragment AdminMemberListContainerMemberFields on Member { | ||
| id | ||
| memberName | ||
| isAdmin | ||
| createdAt | ||
| updatedAt | ||
| profile { | ||
| name | ||
| avatarDocumentId | ||
| } | ||
| accounts { | ||
| id | ||
| firstName | ||
| lastName | ||
| statusCode | ||
| createdAt | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| import { App } from 'antd'; | ||
| import { useMutation, useQuery } from '@apollo/client'; | ||
| import { ComponentQueryLoader } from '@cellix/ui-core'; | ||
| import { useState } from 'react'; | ||
| import { useParams } from 'react-router-dom'; | ||
| import type { AdminMemberListContainerMemberFieldsFragment } from '../../../../generated.tsx'; | ||
| import { AdminMemberListContainerMemberRemoveDocument, AdminMemberListContainerMembersByCommunityIdDocument } from '../../../../generated.tsx'; | ||
| import { MemberList, type MemberListProps } from './member-list.tsx'; | ||
| import { MemberAddModalContainer } from './member-add-modal.container.tsx'; | ||
|
|
||
| export const MemberListContainer: React.FC = () => { | ||
| const { message } = App.useApp(); | ||
| const params = useParams(); | ||
| // biome-ignore lint:useLiteralKeys | ||
| const communityId = params['communityId'] ?? ''; | ||
| const [addModalOpen, setAddModalOpen] = useState(false); | ||
|
|
||
| const { | ||
| data: membersData, | ||
| loading: membersLoading, | ||
| error: membersError, | ||
| } = useQuery(AdminMemberListContainerMembersByCommunityIdDocument, { | ||
| variables: { communityId }, | ||
| skip: !communityId, | ||
| }); | ||
|
|
||
| const [removeMember, { loading: removeLoading }] = useMutation(AdminMemberListContainerMemberRemoveDocument, { | ||
| refetchQueries: [{ query: AdminMemberListContainerMembersByCommunityIdDocument, variables: { communityId } }], | ||
| }); | ||
|
|
||
| const handleRemove = async (memberId: string) => { | ||
| try { | ||
| const result = await removeMember({ variables: { input: { memberId } } }); | ||
| if (result.data?.memberRemove?.status?.success) { | ||
| void message.success('Member removed successfully'); | ||
| } else { | ||
| void message.error(result.data?.memberRemove?.status?.errorMessage ?? 'Failed to remove member'); | ||
| } | ||
| } catch (error) { | ||
| void message.error((error as Error).message ?? 'Failed to remove member'); | ||
| } | ||
| }; | ||
|
|
||
| const memberListProps: MemberListProps = { | ||
| data: (membersData?.membersByCommunityId ?? []) as AdminMemberListContainerMemberFieldsFragment[], | ||
| onAdd: () => setAddModalOpen(true), | ||
| onRemove: handleRemove, | ||
| removeLoading, | ||
| }; | ||
|
|
||
| return ( | ||
| <> | ||
| <ComponentQueryLoader | ||
| loading={membersLoading} | ||
| hasData={membersData} | ||
| hasDataComponent={<MemberList {...memberListProps} />} | ||
| error={membersError} | ||
| noDataComponent={ | ||
| <MemberList | ||
| data={[]} | ||
| onAdd={() => setAddModalOpen(true)} | ||
| /> | ||
| } | ||
| /> | ||
| <MemberAddModalContainer | ||
| communityId={communityId} | ||
| open={addModalOpen} | ||
| onClose={() => setAddModalOpen(false)} | ||
| /> | ||
| </> | ||
| ); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (review_instructions): This container GraphQL file’s path includes an
apps/prefix, so it doesn’t strictly follow the specifiedui-<PortalName>/src/...path pattern.The convention states that container GraphQL files should be at:
ui-<PortalName>/src/components/layouts/<AreaName>/components/<componentName>.container.graphql.This file is at
apps/ui-community/src/components/layouts/admin/components/member-add-modal.container.graphql, which doesn’t strictly adhere to that pattern because of theapps/prefix. If the documented convention is still current, consider relocating/renaming so the path conforms to the expected structure.Review instructions:
Path patterns:
**/*.container.graphqlInstructions:
container graphql files should be found in the following path pattern: ui-/src/components/layouts//components/.container.graphql