diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index d95d078d4..bd9fc77a0 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -62,7 +62,12 @@ pnpm run gen # Generate code (e.g., GraphQL types) - Use `pnpm run verify` to ensure code quality before commits - *Note*: Be patient when you run the `verify` command; wait at least 6 minutes for all checks to complete before assuming unexpected failure. - Use `pnpm run snyk` to run security scans before commits -cop- If you encounter this error when attempting to push changes `hook git error: Command failed with exit code 1: git commit -m REDACTED`, it indicates that pre-commit hooks have failed due to unmet code quality or security standards. Review the output from the hooks, fix the reported issues, and try committing again. Be aware that this error has nothing to do with firewall rules or network connectivity; it is expected to fail if your changes do not meet the project's standards. +- **CRITICAL**: If you encounter the error `hook git error: Command failed with exit code 1: git commit -m REDACTED`, this indicates that pre-commit hooks (which run the `verify` script) have failed due to linting, type-checking, or security issues. **Do NOT abort the session or assume a system/network failure.** This is an expected outcome when code does not meet the project's strict quality standards. You MUST: + 1. Read the terminal output/logs from the failed commit command. + 2. Identify the specific errors (e.g., TypeScript errors, linting violations, security vulnerabilities). + 3. Fix the reported issues in the source files. + 4. Attempt the commit/push again. + 5. Iterate until the verification passes. - Address any reported issues across all packages before pushing changes (if something fails, it was due to your changes and is considered to be "your code"). ### Security Scanning Workflow diff --git a/apps/ui-community/package.json b/apps/ui-community/package.json index 1171f239a..5ce962fdd 100644 --- a/apps/ui-community/package.json +++ b/apps/ui-community/package.json @@ -30,7 +30,7 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-oidc-context": "^3.3.0", - "react-router-dom": "^7.0.2" + "react-router-dom": "^7.12.0" }, "devDependencies": { "@cellix/typescript-config": "workspace:*", diff --git a/apps/ui-community/src/components/layouts/admin/components/members-accounts-list.container.graphql b/apps/ui-community/src/components/layouts/admin/components/members-accounts-list.container.graphql new file mode 100644 index 000000000..d3bbbbbea --- /dev/null +++ b/apps/ui-community/src/components/layouts/admin/components/members-accounts-list.container.graphql @@ -0,0 +1,21 @@ +query AdminMembersAccountsListContainerMember($id: ObjectID!) { + member(id: $id) { + ...AdminMembersAccountsListContainerMemberFields + } +} + +fragment AdminMembersAccountsListContainerMemberFields on Member { + accounts { + ...AdminMembersAccountsListContainerMemberAccountFields + } + id +} + +fragment AdminMembersAccountsListContainerMemberAccountFields on MemberAccount { + id + firstName + lastName + statusCode + createdAt + updatedAt +} diff --git a/apps/ui-community/src/components/layouts/admin/components/members-accounts-list.container.tsx b/apps/ui-community/src/components/layouts/admin/components/members-accounts-list.container.tsx new file mode 100644 index 000000000..771b4bf62 --- /dev/null +++ b/apps/ui-community/src/components/layouts/admin/components/members-accounts-list.container.tsx @@ -0,0 +1,41 @@ +import { useQuery } from '@apollo/client'; +import { ComponentQueryLoader } from '@cellix/ui-core'; +import { + type AdminMembersAccountsListContainerMemberAccountFieldsFragment, + AdminMembersAccountsListContainerMemberDocument, +} from '../../../../generated.tsx'; +import { MembersAccountsList } from './members-accounts-list.tsx'; + +interface MembersAccountsListContainerProps { + data: { + id: string; + }; +} + +export const MembersAccountsListContainer: React.FC< + MembersAccountsListContainerProps +> = (props) => { + const { + data: memberData, + loading: memberLoading, + error: memberError, + } = useQuery(AdminMembersAccountsListContainerMemberDocument, { + variables: { + id: props.data.id, + }, + }); + + const membersAccountsListProps = { + data: (memberData?.member?.accounts ?? + []) as AdminMembersAccountsListContainerMemberAccountFieldsFragment[], + }; + + return ( + } + error={memberError} + /> + ); +}; diff --git a/apps/ui-community/src/components/layouts/admin/components/members-accounts-list.stories.tsx b/apps/ui-community/src/components/layouts/admin/components/members-accounts-list.stories.tsx new file mode 100644 index 000000000..e7100eb8b --- /dev/null +++ b/apps/ui-community/src/components/layouts/admin/components/members-accounts-list.stories.tsx @@ -0,0 +1,115 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, within } from 'storybook/test'; +import { BrowserRouter } from 'react-router-dom'; +import type { AdminMembersAccountsListContainerMemberAccountFieldsFragment } from '../../../../generated.tsx'; +import { MembersAccountsList } from './members-accounts-list.tsx'; + +const mockAccounts: AdminMembersAccountsListContainerMemberAccountFieldsFragment[] = + [ + { + __typename: 'MemberAccount', + id: '1', + firstName: 'John', + lastName: 'Doe', + statusCode: 'Active', + createdAt: '2024-01-01T12:00:00.000Z', + updatedAt: '2024-01-15T12:00:00.000Z', + }, + { + __typename: 'MemberAccount', + id: '2', + firstName: 'Jane', + lastName: 'Smith', + statusCode: 'Pending', + createdAt: '2024-01-05T12:00:00.000Z', + updatedAt: '2024-01-20T12:00:00.000Z', + }, + { + __typename: 'MemberAccount', + id: '3', + firstName: 'Bob', + lastName: 'Johnson', + statusCode: 'Active', + createdAt: '2024-01-10T12:00:00.000Z', + updatedAt: '2024-01-25T12:00:00.000Z', + }, + ]; + +const meta: Meta = { + title: 'Components/Layouts/Admin/MembersAccountsList', + component: MembersAccountsList, + decorators: [ + (Story) => ( + + + + ), + ], + parameters: { + layout: 'padded', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + data: mockAccounts, + }, + play: ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Verify add button is present + expect(canvas.getByRole('button', { name: /Add Account/i })).toBeInTheDocument(); + + // Verify table headers + expect(canvas.getByText('Action')).toBeInTheDocument(); + expect(canvas.getByText('First Name')).toBeInTheDocument(); + expect(canvas.getByText('Last Name')).toBeInTheDocument(); + expect(canvas.getByText('Status')).toBeInTheDocument(); + expect(canvas.getByText('Created')).toBeInTheDocument(); + expect(canvas.getByText('Updated')).toBeInTheDocument(); + + // Verify data is rendered + expect(canvas.getByText('John')).toBeInTheDocument(); + expect(canvas.getByText('Doe')).toBeInTheDocument(); + + // Verify Active status appears in table cells + const cells = canvas.getAllByText('Active'); + expect(cells.length).toBeGreaterThan(0); + }, +}; + +export const Empty: Story = { + args: { + data: [], + }, + play: ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Verify add button is present + expect(canvas.getByRole('button', { name: /Add Account/i })).toBeInTheDocument(); + + // Verify empty state message (in the ant-empty-description div, not the SVG title) + const emptyDescription = canvas.getByText('No data', { selector: '.ant-empty-description' }); + expect(emptyDescription).toBeInTheDocument(); + }, +}; + +export const SingleAccount: Story = { + args: { + data: mockAccounts.slice(0, 1), + }, + play: ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Verify single account is rendered + expect(canvas.getByText('John')).toBeInTheDocument(); + expect(canvas.getByText('Doe')).toBeInTheDocument(); + + // Verify only one edit button + const editButtons = canvas.getAllByRole('button', { name: 'Edit' }); + expect(editButtons).toHaveLength(1); + }, +}; diff --git a/apps/ui-community/src/components/layouts/admin/components/members-accounts-list.tsx b/apps/ui-community/src/components/layouts/admin/components/members-accounts-list.tsx new file mode 100644 index 000000000..95f8af7ff --- /dev/null +++ b/apps/ui-community/src/components/layouts/admin/components/members-accounts-list.tsx @@ -0,0 +1,78 @@ +import { UsergroupAddOutlined } from '@ant-design/icons'; +import { Button, Table, type TableColumnsType } from 'antd'; +import dayjs from 'dayjs'; +import { useNavigate } from 'react-router-dom'; +import type { AdminMembersAccountsListContainerMemberAccountFieldsFragment } from '../../../../generated.tsx'; + +interface MembersAccountsListProps { + data: AdminMembersAccountsListContainerMemberAccountFieldsFragment[]; +} + +export const MembersAccountsList: React.FC = ( + props, +) => { + const navigate = useNavigate(); + const columns: TableColumnsType = + [ + { + title: 'Action', + dataIndex: 'id', + render: ( + text: AdminMembersAccountsListContainerMemberAccountFieldsFragment['id'], + ) => ( + + ), + }, + { + title: 'First Name', + dataIndex: 'firstName', + key: 'firstName', + }, + { + title: 'Last Name', + dataIndex: 'lastName', + key: 'lastName', + }, + { + title: 'Status', + dataIndex: 'statusCode', + key: 'statusCode', + }, + { + title: 'Updated', + dataIndex: 'updatedAt', + key: 'updatedAt', + render: ( + text: AdminMembersAccountsListContainerMemberAccountFieldsFragment['updatedAt'], + ) => {dayjs(text).format('MM/DD/YYYY')}, + }, + { + title: 'Created', + dataIndex: 'createdAt', + key: 'createdAt', + render: ( + text: AdminMembersAccountsListContainerMemberAccountFieldsFragment['createdAt'], + ) => {dayjs(text).format('MM/DD/YYYY')}, + }, + ]; + + return ( + <> + +
+ record.id} + /> + + + ); +}; diff --git a/apps/ui-community/src/components/layouts/admin/components/members-create.container.graphql b/apps/ui-community/src/components/layouts/admin/components/members-create.container.graphql new file mode 100644 index 000000000..cc88e19cf --- /dev/null +++ b/apps/ui-community/src/components/layouts/admin/components/members-create.container.graphql @@ -0,0 +1,23 @@ +mutation AdminMembersCreateContainerMemberCreate($input: MemberCreateInput!) { + memberCreate(input: $input) { + ...AdminMembersCreateContainerMemberMutationResultFields + } +} + +fragment AdminMembersCreateContainerMemberMutationResultFields on MemberMutationResult { + status { + success + errorMessage + } + member { + ...AdminMembersCreateContainerMember + } +} + +fragment AdminMembersCreateContainerMember on Member { + memberName + + id + createdAt + updatedAt +} diff --git a/apps/ui-community/src/components/layouts/admin/components/members-create.container.tsx b/apps/ui-community/src/components/layouts/admin/components/members-create.container.tsx new file mode 100644 index 000000000..d3bd555ef --- /dev/null +++ b/apps/ui-community/src/components/layouts/admin/components/members-create.container.tsx @@ -0,0 +1,80 @@ +import { App } from 'antd'; +import { useMutation } from '@apollo/client'; +import { useNavigate } from 'react-router-dom'; +import { + AdminMembersCreateContainerMemberCreateDocument, + AdminMembersListContainerMembersByCommunityIdDocument, + type MemberCreateInput, +} from '../../../../generated.tsx'; +import type { MembersCreateProps } from './members-create.tsx'; +import { MembersCreate } from './members-create.tsx'; + +interface MembersCreateContainerProps { + data: { + communityId: string; + }; +} + +export const MembersCreateContainer: React.FC = ( + props, +) => { + const navigate = useNavigate(); + const { message } = App.useApp(); + const [memberCreate, { loading }] = useMutation( + AdminMembersCreateContainerMemberCreateDocument, + { + update(cache, { data }) { + // update the list with the new item + const newMember = data?.memberCreate.member; + const members = cache.readQuery({ + query: AdminMembersListContainerMembersByCommunityIdDocument, + variables: { communityId: props.data.communityId ?? '' }, + })?.membersByCommunityId; + if (newMember && members) { + cache.writeQuery({ + query: AdminMembersListContainerMembersByCommunityIdDocument, + variables: { communityId: props.data.communityId ?? '' }, + data: { + membersByCommunityId: [...members, newMember], + }, + }); + } + }, + }, + ); + + const defaultValues: MemberCreateInput = { + memberName: '', + }; + + const handleSave = async (values: MemberCreateInput) => { + try { + const result = await memberCreate({ + variables: { + input: values, + }, + }); + + if (result.data?.memberCreate.status.success) { + message.success('Member Created'); + navigate(`../${result.data?.memberCreate.member?.id}`, { + replace: true, + }); + } else { + message.error( + `Error creating Member: ${result.data?.memberCreate.status.errorMessage}`, + ); + } + } catch (error) { + message.error(`Error creating Member: ${JSON.stringify(error)}`); + } + }; + + const membersCreateProps: MembersCreateProps = { + data: defaultValues, + onSave: handleSave, + loading, + }; + + return ; +}; diff --git a/apps/ui-community/src/components/layouts/admin/components/members-create.stories.tsx b/apps/ui-community/src/components/layouts/admin/components/members-create.stories.tsx new file mode 100644 index 000000000..838e007fd --- /dev/null +++ b/apps/ui-community/src/components/layouts/admin/components/members-create.stories.tsx @@ -0,0 +1,92 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, fn, userEvent, within } from 'storybook/test'; +import { MembersCreate } from './members-create.tsx'; + +const meta: Meta = { + title: 'Components/Layouts/Admin/MembersCreate', + component: MembersCreate, + parameters: { + layout: 'padded', + }, +}; + +export default meta; +type Story = StoryObj; + +const mockOnSave = fn(); + +export const Default: Story = { + args: { + data: { + memberName: '', + }, + onSave: mockOnSave, + loading: false, + }, + play: ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Verify form is rendered + expect(canvas.getByLabelText('Member Name')).toBeInTheDocument(); + expect(canvas.getByRole('button', { name: 'Create Member' })).toBeInTheDocument(); + }, +}; + +export const WithPrefilledData: Story = { + args: { + data: { + memberName: 'John Doe', + }, + onSave: mockOnSave, + loading: false, + }, + play: ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Verify prefilled data + const input = canvas.getByLabelText('Member Name') as HTMLInputElement; + expect(input.value).toBe('John Doe'); + }, +}; + +export const Loading: Story = { + args: { + data: { + memberName: 'Test Member', + }, + onSave: mockOnSave, + loading: true, + }, + play: ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Verify loading state - button text is still present but button has loading class + const button = canvas.getByText('Create Member').closest('button'); + expect(button).toHaveClass('ant-btn-loading'); + }, +}; + +export const FormSubmission: Story = { + args: { + data: { + memberName: '', + }, + onSave: mockOnSave, + loading: false, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Fill in the form + const input = canvas.getByLabelText('Member Name'); + await userEvent.clear(input); + await userEvent.type(input, 'New Member'); + + // Submit the form + const submitButton = canvas.getByRole('button', { name: 'Create Member' }); + await userEvent.click(submitButton); + + // Verify onSave was called with correct data + expect(mockOnSave).toHaveBeenCalledWith({ memberName: 'New Member' }); + }, +}; diff --git a/apps/ui-community/src/components/layouts/admin/components/members-create.tsx b/apps/ui-community/src/components/layouts/admin/components/members-create.tsx new file mode 100644 index 000000000..5dcef8fbe --- /dev/null +++ b/apps/ui-community/src/components/layouts/admin/components/members-create.tsx @@ -0,0 +1,42 @@ +import { Button, Form, Input } from 'antd'; +import type { MemberCreateInput } from '../../../../generated.tsx'; + +export interface MembersCreateProps { + data: MemberCreateInput; + onSave: (member: MemberCreateInput) => void; + loading?: boolean; +} + +export const MembersCreate: React.FC = (props) => { + const [form] = Form.useForm(); + + return ( +
+
{ + props.onSave(values); + }} + > + + + + + + +
+ ); +}; diff --git a/apps/ui-community/src/components/layouts/admin/components/members-detail.container.graphql b/apps/ui-community/src/components/layouts/admin/components/members-detail.container.graphql new file mode 100644 index 000000000..f2128c8d1 --- /dev/null +++ b/apps/ui-community/src/components/layouts/admin/components/members-detail.container.graphql @@ -0,0 +1,29 @@ +query AdminMembersDetailContainerMember($id: ObjectID!) { + member(id: $id) { + ...AdminMembersDetailContainerMemberFields + } +} + +mutation AdminMembersDetailContainerMemberUpdate($input: MemberUpdateInput!) { + memberUpdate(input: $input) { + ...AdminMembersDetailContainerMemberMutationResultFields + } +} + +fragment AdminMembersDetailContainerMemberMutationResultFields on MemberMutationResult { + status { + success + errorMessage + } + member { + ...AdminMembersDetailContainerMemberFields + } +} + +fragment AdminMembersDetailContainerMemberFields on Member { + memberName + + id + createdAt + updatedAt +} diff --git a/apps/ui-community/src/components/layouts/admin/components/members-detail.container.tsx b/apps/ui-community/src/components/layouts/admin/components/members-detail.container.tsx new file mode 100644 index 000000000..52e01a083 --- /dev/null +++ b/apps/ui-community/src/components/layouts/admin/components/members-detail.container.tsx @@ -0,0 +1,74 @@ +import { App } from 'antd'; +import { useMutation, useQuery } from '@apollo/client'; +import { + AdminMembersDetailContainerMemberDocument, + type AdminMembersDetailContainerMemberFieldsFragment, + AdminMembersDetailContainerMemberUpdateDocument, + type MemberUpdateInput, +} from '../../../../generated.tsx'; +import { ComponentQueryLoader } from '@cellix/ui-core'; +import type { MembersDetailProps } from './members-detail.tsx'; +import { MembersDetail } from './members-detail.tsx'; + +interface MembersDetailContainerProps { + data: { + id: string; + communityId: string; + }; +} + +export const MembersDetailContainer: React.FC = ( + props, +) => { + const { message } = App.useApp(); + const [updateMember, { loading: updateLoading }] = useMutation( + AdminMembersDetailContainerMemberUpdateDocument, + ); + const { + data: memberData, + loading: memberLoading, + error: memberError, + } = useQuery(AdminMembersDetailContainerMemberDocument, { + variables: { + id: props.data.id, + }, + }); + + const handleSave = async (values: MemberUpdateInput) => { + try { + const result = await updateMember({ + variables: { + input: values, + }, + }); + + if (result.data?.memberUpdate.status.success) { + message.success('Saved'); + } else { + message.error( + `Error updating Member: ${result.data?.memberUpdate.status.errorMessage}`, + ); + } + } catch (error) { + message.error(`Error updating Member: ${JSON.stringify(error)}`); + } + }; + + const membersDetailProps: MembersDetailProps = { + data: { + member: (memberData?.member ?? + {}) as AdminMembersDetailContainerMemberFieldsFragment, + }, + onSave: handleSave, + loading: updateLoading, + }; + + return ( + } + error={memberError} + /> + ); +}; diff --git a/apps/ui-community/src/components/layouts/admin/components/members-detail.stories.tsx b/apps/ui-community/src/components/layouts/admin/components/members-detail.stories.tsx new file mode 100644 index 000000000..89449f228 --- /dev/null +++ b/apps/ui-community/src/components/layouts/admin/components/members-detail.stories.tsx @@ -0,0 +1,92 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, fn, userEvent, within } from 'storybook/test'; +import type { AdminMembersDetailContainerMemberFieldsFragment } from '../../../../generated.tsx'; +import { MembersDetail } from './members-detail.tsx'; + +const mockMember: AdminMembersDetailContainerMemberFieldsFragment = { + __typename: 'Member', + id: '507f1f77bcf86cd799439011', + memberName: 'John Doe', + createdAt: '2024-01-01T12:00:00.000Z', + updatedAt: '2024-01-15T12:00:00.000Z', +}; + +const meta: Meta = { + title: 'Components/Layouts/Admin/MembersDetail', + component: MembersDetail, + parameters: { + layout: 'padded', + }, +}; + +export default meta; +type Story = StoryObj; + +const mockOnSave = fn(); + +export const Default: Story = { + args: { + data: { + member: mockMember, + }, + onSave: mockOnSave, + loading: false, + }, + play: ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Verify member info is displayed + expect(canvas.getByText(mockMember.id)).toBeInTheDocument(); + expect(canvas.getByText('01/01/2024')).toBeInTheDocument(); + expect(canvas.getByText('01/15/2024')).toBeInTheDocument(); + + // Verify form is rendered with correct value + const input = canvas.getByLabelText('Member Name') as HTMLInputElement; + expect(input.value).toBe(mockMember.memberName); + }, +}; + +export const Loading: Story = { + args: { + data: { + member: mockMember, + }, + onSave: mockOnSave, + loading: true, + }, + play: ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Verify loading state - button text is still present but button has loading class + const button = canvas.getByText('Save').closest('button'); + expect(button).toHaveClass('ant-btn-loading'); + }, +}; + +export const FormSubmission: Story = { + args: { + data: { + member: mockMember, + }, + onSave: mockOnSave, + loading: false, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Update the member name + const input = canvas.getByLabelText('Member Name'); + await userEvent.clear(input); + await userEvent.type(input, 'Updated Name'); + + // Submit the form + const submitButton = canvas.getByRole('button', { name: 'Save' }); + await userEvent.click(submitButton); + + // Verify onSave was called with correct data + expect(mockOnSave).toHaveBeenCalledWith({ + id: mockMember.id, + memberName: 'Updated Name', + }); + }, +}; diff --git a/apps/ui-community/src/components/layouts/admin/components/members-detail.tsx b/apps/ui-community/src/components/layouts/admin/components/members-detail.tsx new file mode 100644 index 000000000..83ee3a8ef --- /dev/null +++ b/apps/ui-community/src/components/layouts/admin/components/members-detail.tsx @@ -0,0 +1,60 @@ +import { Button, Descriptions, Form, Input } from 'antd'; +import dayjs from 'dayjs'; +import type { + AdminMembersDetailContainerMemberFieldsFragment, + MemberUpdateInput, +} from '../../../../generated.tsx'; + +export interface MembersDetailProps { + data: { + member: AdminMembersDetailContainerMemberFieldsFragment; + }; + onSave: (member: MemberUpdateInput) => void; + loading?: boolean; +} + +export const MembersDetail: React.FC = (props) => { + const [form] = Form.useForm(); + + return ( +
+ + {props.data.member.id} + + {dayjs(props.data.member.createdAt).format('MM/DD/YYYY')} + + + {dayjs(props.data.member.updatedAt).format('MM/DD/YYYY')} + + +
{ + props.onSave({ + ...values, + id: props.data.member.id, + }); + }} + > + + + + + + +
+ ); +}; diff --git a/apps/ui-community/src/components/layouts/admin/components/members-list.container.graphql b/apps/ui-community/src/components/layouts/admin/components/members-list.container.graphql new file mode 100644 index 000000000..e50970a96 --- /dev/null +++ b/apps/ui-community/src/components/layouts/admin/components/members-list.container.graphql @@ -0,0 +1,14 @@ +query AdminMembersListContainerMembersByCommunityId($communityId: ObjectID!) { + membersByCommunityId(communityId: $communityId) { + ...AdminMembersListContainerMemberFields + } +} + +fragment AdminMembersListContainerMemberFields on Member { + memberName + isAdmin + + id + createdAt + updatedAt +} diff --git a/apps/ui-community/src/components/layouts/admin/components/members-list.container.tsx b/apps/ui-community/src/components/layouts/admin/components/members-list.container.tsx new file mode 100644 index 000000000..db630cc86 --- /dev/null +++ b/apps/ui-community/src/components/layouts/admin/components/members-list.container.tsx @@ -0,0 +1,40 @@ +import { useQuery } from '@apollo/client'; +import { ComponentQueryLoader } from '@cellix/ui-core'; +import { + type AdminMembersListContainerMemberFieldsFragment, + AdminMembersListContainerMembersByCommunityIdDocument, +} from '../../../../generated.tsx'; +import type { MembersListProps } from './members-list.tsx'; +import { MembersList } from './members-list.tsx'; + +interface MembersListContainerProps { + data: { + communityId: string; + }; +} + +export const MembersListContainer: React.FC = ( + props, +) => { + const { + data: memberData, + loading: memberLoading, + error: memberError, + } = useQuery(AdminMembersListContainerMembersByCommunityIdDocument, { + variables: { communityId: props.data.communityId }, + }); + + const membersListProps: MembersListProps = { + data: (memberData?.membersByCommunityId ?? + []) as AdminMembersListContainerMemberFieldsFragment[], + }; + + return ( + } + error={memberError} + /> + ); +}; diff --git a/apps/ui-community/src/components/layouts/admin/components/members-list.stories.tsx b/apps/ui-community/src/components/layouts/admin/components/members-list.stories.tsx new file mode 100644 index 000000000..e24e3453a --- /dev/null +++ b/apps/ui-community/src/components/layouts/admin/components/members-list.stories.tsx @@ -0,0 +1,114 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, within } from 'storybook/test'; +import { BrowserRouter } from 'react-router-dom'; +import type { AdminMembersListContainerMemberFieldsFragment } from '../../../../generated.tsx'; +import { MembersList } from './members-list.tsx'; + +const mockMembers: AdminMembersListContainerMemberFieldsFragment[] = [ + { + __typename: 'Member', + id: '1', + memberName: 'John Doe', + isAdmin: true, + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-15T00:00:00.000Z', + }, + { + __typename: 'Member', + id: '2', + memberName: 'Jane Smith', + isAdmin: false, + createdAt: '2024-01-05T00:00:00.000Z', + updatedAt: '2024-01-20T00:00:00.000Z', + }, + { + __typename: 'Member', + id: '3', + memberName: 'Bob Johnson', + isAdmin: true, + createdAt: '2024-01-10T00:00:00.000Z', + updatedAt: '2024-01-25T00:00:00.000Z', + }, +]; + +const meta = { + title: 'Components/Layouts/Admin/MembersList', + component: MembersList, + decorators: [ + (Story) => ( + + + + ), + ], + parameters: { + layout: 'padded', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + data: mockMembers, + }, + play: ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Verify table is rendered + expect(canvas.getByRole('table')).toBeInTheDocument(); + + // Verify column headers + expect(canvas.getByText('Action')).toBeInTheDocument(); + expect(canvas.getByText('Member')).toBeInTheDocument(); + expect(canvas.getByText('Is Admin')).toBeInTheDocument(); + expect(canvas.getByText('Updated')).toBeInTheDocument(); + expect(canvas.getByText('Created')).toBeInTheDocument(); + + // Verify member data is displayed + expect(canvas.getByText('John Doe')).toBeInTheDocument(); + expect(canvas.getByText('Jane Smith')).toBeInTheDocument(); + expect(canvas.getByText('Bob Johnson')).toBeInTheDocument(); + + // Verify admin status + const yesTexts = canvas.getAllByText('Yes'); + expect(yesTexts).toHaveLength(2); // John and Bob are admins + + // Verify dates are formatted correctly + expect(canvas.getByText('01/01/2024')).toBeInTheDocument(); + expect(canvas.getByText('01/15/2024')).toBeInTheDocument(); + }, +}; + +export const Empty: Story = { + args: { + data: [], + }, + play: ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Verify table is rendered but with no data rows + expect(canvas.getByRole('table')).toBeInTheDocument(); + + // Verify empty state message (in the ant-empty-description div, not the SVG title) + const emptyDescription = canvas.getByText('No data', { selector: '.ant-empty-description' }); + expect(emptyDescription).toBeInTheDocument(); + }, +}; + +export const SingleMember: Story = { + args: { + data: mockMembers.slice(0, 1), + }, + play: ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Verify only one member is displayed + expect(canvas.getByText('John Doe')).toBeInTheDocument(); + expect(canvas.queryByText('Jane Smith')).not.toBeInTheDocument(); + + // Verify Edit button is present + expect(canvas.getByRole('button', { name: /edit/i })).toBeInTheDocument(); + }, +}; diff --git a/apps/ui-community/src/components/layouts/admin/components/members-list.tsx b/apps/ui-community/src/components/layouts/admin/components/members-list.tsx new file mode 100644 index 000000000..72c5539a9 --- /dev/null +++ b/apps/ui-community/src/components/layouts/admin/components/members-list.tsx @@ -0,0 +1,67 @@ +import { Button, Table, type TableColumnsType } from 'antd'; +import dayjs from 'dayjs'; +import { useNavigate } from 'react-router-dom'; +import type { AdminMembersListContainerMemberFieldsFragment } from '../../../../generated.tsx'; + +export interface MembersListProps { + data: AdminMembersListContainerMemberFieldsFragment[]; +} + +export const MembersList: React.FC = (props) => { + const navigate = useNavigate(); + const columns: TableColumnsType = + [ + { + title: 'Action', + dataIndex: 'id', + render: (text: string) => ( + + ), + }, + { + title: 'Member', + dataIndex: 'memberName', + key: 'memberName', + }, + { + title: 'Is Admin', + dataIndex: 'isAdmin', + key: 'isAdmin', + render: (text: boolean) => {text ? 'Yes' : 'No'}, + }, + { + title: 'Updated', + dataIndex: 'updatedAt', + key: 'updatedAt', + render: (text: string) => ( + {dayjs(text).format('MM/DD/YYYY')} + ), + }, + { + title: 'Created', + dataIndex: 'createdAt', + key: 'createdAt', + render: (text: string) => ( + {dayjs(text).format('MM/DD/YYYY')} + ), + }, + ]; + + return ( +
+
record.id} + /> + + ); +}; diff --git a/apps/ui-community/src/components/layouts/admin/components/members-profile.container.graphql b/apps/ui-community/src/components/layouts/admin/components/members-profile.container.graphql new file mode 100644 index 000000000..4e1b028fd --- /dev/null +++ b/apps/ui-community/src/components/layouts/admin/components/members-profile.container.graphql @@ -0,0 +1,17 @@ +query AdminMembersProfileContainerMember($id: ObjectID!) { + member(id: $id) { + ...AdminMembersProfileContainerMemberFields + } +} + +fragment AdminMembersProfileContainerMemberFields on Member { + profile { + name + email + bio + showEmail + showProfile + } + + id +} diff --git a/apps/ui-community/src/components/layouts/admin/components/members-profile.stories.tsx b/apps/ui-community/src/components/layouts/admin/components/members-profile.stories.tsx new file mode 100644 index 000000000..1d711e29f --- /dev/null +++ b/apps/ui-community/src/components/layouts/admin/components/members-profile.stories.tsx @@ -0,0 +1,96 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, within } from 'storybook/test'; +import type { AdminMembersProfileContainerMemberFieldsFragment } from '../../../../generated.tsx'; +import { MembersProfile } from './members-profile.tsx'; + +const mockMemberWithProfile: AdminMembersProfileContainerMemberFieldsFragment = + { + __typename: 'Member', + id: '507f1f77bcf86cd799439011', + profile: { + __typename: 'MemberProfile', + name: 'John Doe', + email: 'john.doe@example.com', + bio: 'Software developer with 10 years of experience', + showEmail: true, + showProfile: true, + }, + }; + +const mockMemberWithoutProfile: AdminMembersProfileContainerMemberFieldsFragment = + { + __typename: 'Member', + id: '507f1f77bcf86cd799439012', + profile: null, + }; + +const meta: Meta = { + title: 'Components/Layouts/Admin/MembersProfile', + component: MembersProfile, + parameters: { + layout: 'padded', + }, +}; + +export default meta; +type Story = StoryObj; + +export const WithProfile: Story = { + args: { + data: { + member: mockMemberWithProfile, + }, + }, + play: ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Verify profile data is displayed + expect(canvas.getByText('John Doe')).toBeInTheDocument(); + expect(canvas.getByText('john.doe@example.com')).toBeInTheDocument(); + expect( + canvas.getByText('Software developer with 10 years of experience'), + ).toBeInTheDocument(); + expect(canvas.getAllByText('Yes')).toHaveLength(2); // showEmail and showProfile + }, +}; + +export const WithoutProfile: Story = { + args: { + data: { + member: mockMemberWithoutProfile, + }, + }, + play: ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Verify N/A is displayed for missing data + expect(canvas.getAllByText('N/A')).toHaveLength(3); // name, email, bio + expect(canvas.getAllByText('No')).toHaveLength(2); // showEmail, showProfile + }, +}; + +export const PartialProfile: Story = { + args: { + data: { + member: { + id: '507f1f77bcf86cd799439013', + profile: { + name: 'Jane Smith', + email: null, + bio: null, + showEmail: false, + showProfile: true, + }, + }, + }, + }, + play: ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Verify partial data is displayed correctly + expect(canvas.getByText('Jane Smith')).toBeInTheDocument(); + expect(canvas.getAllByText('N/A')).toHaveLength(2); // email, bio + expect(canvas.getByText('Yes')).toBeInTheDocument(); // showProfile + expect(canvas.getByText('No')).toBeInTheDocument(); // showEmail + }, +}; diff --git a/apps/ui-community/src/components/layouts/admin/components/members-profile.tsx b/apps/ui-community/src/components/layouts/admin/components/members-profile.tsx new file mode 100644 index 000000000..bd13ec308 --- /dev/null +++ b/apps/ui-community/src/components/layouts/admin/components/members-profile.tsx @@ -0,0 +1,32 @@ +import { Descriptions } from 'antd'; +import type { AdminMembersProfileContainerMemberFieldsFragment } from '../../../../generated.tsx'; + +interface MembersProfileProps { + data: { + member: AdminMembersProfileContainerMemberFieldsFragment; + }; +} + +export const MembersProfile: React.FC = (props) => { + return ( +
+ + + {props.data.member.profile?.name || 'N/A'} + + + {props.data.member.profile?.email || 'N/A'} + + + {props.data.member.profile?.bio || 'N/A'} + + + {props.data.member.profile?.showEmail ? 'Yes' : 'No'} + + + {props.data.member.profile?.showProfile ? 'Yes' : 'No'} + + +
+ ); +}; diff --git a/apps/ui-community/src/components/layouts/admin/index.tsx b/apps/ui-community/src/components/layouts/admin/index.tsx index 6a1b73b92..adccd3d60 100644 --- a/apps/ui-community/src/components/layouts/admin/index.tsx +++ b/apps/ui-community/src/components/layouts/admin/index.tsx @@ -1,7 +1,12 @@ -import { HomeOutlined, SettingOutlined } from '@ant-design/icons'; +import { + ContactsOutlined, + HomeOutlined, + SettingOutlined, +} from '@ant-design/icons'; import { Route, Routes } from 'react-router-dom'; import type { Member } from '../../../generated.tsx'; import { Home } from './pages/home.tsx'; +import { Members } from './pages/members.tsx'; import { Settings } from './pages/settings.tsx'; import { SectionLayoutContainer } from './section-layout.container.tsx'; @@ -22,6 +27,13 @@ export const Admin: React.FC = () => { icon: , id: 'ROOT', }, + { + path: '/community/:communityId/admin/:memberId/members/*', + title: 'Members', + icon: , + id: 1, + parent: 'ROOT', + }, { path: '/community/:communityId/admin/:memberId/settings/*', title: 'Settings', @@ -41,6 +53,7 @@ export const Admin: React.FC = () => { element={} > } /> + } /> } /> diff --git a/apps/ui-community/src/components/layouts/admin/pages/members-accounts.tsx b/apps/ui-community/src/components/layouts/admin/pages/members-accounts.tsx new file mode 100644 index 000000000..e65961eba --- /dev/null +++ b/apps/ui-community/src/components/layouts/admin/pages/members-accounts.tsx @@ -0,0 +1,19 @@ +import { Route, Routes, useParams } from 'react-router-dom'; +import { MembersAccountsListContainer } from '../components/members-accounts-list.container.tsx'; + +export const MembersAccounts: React.FC = () => { + const params = useParams(); + return ( +
+

Members Accounts

+ + + } + /> + +
+ ); +}; diff --git a/apps/ui-community/src/components/layouts/admin/pages/members-create.tsx b/apps/ui-community/src/components/layouts/admin/pages/members-create.tsx new file mode 100644 index 000000000..77d386029 --- /dev/null +++ b/apps/ui-community/src/components/layouts/admin/pages/members-create.tsx @@ -0,0 +1,37 @@ +import { PageHeader } from '@ant-design/pro-layout'; +import { theme } from 'antd'; +import { useNavigate, useParams } from 'react-router-dom'; +import { MembersCreateContainer } from '../components/members-create.container.tsx'; +import { SubPageLayout } from '../sub-page-layout.tsx'; + +export const MembersCreate: React.FC = () => { + const navigate = useNavigate(); + const params = useParams(); + const { + token: { colorTextBase }, + } = theme.useToken(); + + return ( + + Create Member + + } + onBack={() => navigate('../')} + /> + } + > + + + ); +}; diff --git a/apps/ui-community/src/components/layouts/admin/pages/members-detail.tsx b/apps/ui-community/src/components/layouts/admin/pages/members-detail.tsx new file mode 100644 index 000000000..f86ea03d3 --- /dev/null +++ b/apps/ui-community/src/components/layouts/admin/pages/members-detail.tsx @@ -0,0 +1,70 @@ +import { + IdcardOutlined, + ProfileOutlined, + TeamOutlined, +} from '@ant-design/icons'; +import { PageHeader } from '@ant-design/pro-layout'; +import type { RouteDefinition } from '@cellix/ui-core'; +import { VerticalTabs } from '@cellix/ui-core'; +import { theme } from 'antd'; +import { useNavigate } from 'react-router-dom'; +import { SubPageLayout } from '../sub-page-layout.tsx'; +import { MembersAccounts } from './members-accounts.tsx'; +import { MembersGeneral } from './members-general.tsx'; +import { MembersProfile } from './members-profile.tsx'; + +export const MembersDetail: React.FC = () => { + const navigate = useNavigate(); + const { + token: { colorTextBase }, + } = theme.useToken(); + + const pages: RouteDefinition[] = [ + { + id: '1', + link: '', + path: '', + title: 'General', + icon: , + element: , + }, + { + id: '2', + link: 'profile', + path: 'profile/*', + title: 'Profile', + icon: , + element: , + }, + { + id: '3', + link: 'accounts', + path: 'accounts/*', + title: 'Accounts', + icon: , + element: , + }, + ]; + + return ( + + Member Detail + + } + onBack={() => navigate('../')} + /> + } + > + + + ); +}; diff --git a/apps/ui-community/src/components/layouts/admin/pages/members-general.tsx b/apps/ui-community/src/components/layouts/admin/pages/members-general.tsx new file mode 100644 index 000000000..60346138a --- /dev/null +++ b/apps/ui-community/src/components/layouts/admin/pages/members-general.tsx @@ -0,0 +1,15 @@ +import { useParams } from 'react-router-dom'; +import { MembersDetailContainer } from '../components/members-detail.container.tsx'; + +export const MembersGeneral: React.FC = () => { + const params = useParams(); + + return ( + + ); +}; diff --git a/apps/ui-community/src/components/layouts/admin/pages/members-list.stories.tsx b/apps/ui-community/src/components/layouts/admin/pages/members-list.stories.tsx new file mode 100644 index 000000000..dd7a597cd --- /dev/null +++ b/apps/ui-community/src/components/layouts/admin/pages/members-list.stories.tsx @@ -0,0 +1,20 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { BrowserRouter } from 'react-router-dom'; +import { MembersList } from './members-list.tsx'; + +const meta: Meta = { + title: 'Pages/Layouts/Admin/MembersList', + component: MembersList, + decorators: [ + (Story) => ( + + + + ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/apps/ui-community/src/components/layouts/admin/pages/members-list.tsx b/apps/ui-community/src/components/layouts/admin/pages/members-list.tsx new file mode 100644 index 000000000..d54ef7038 --- /dev/null +++ b/apps/ui-community/src/components/layouts/admin/pages/members-list.tsx @@ -0,0 +1,45 @@ +import { UsergroupAddOutlined } from '@ant-design/icons'; +import { PageHeader } from '@ant-design/pro-layout'; +import { Button, theme } from 'antd'; +import { useNavigate, useParams } from 'react-router-dom'; +import { MembersListContainer } from '../components/members-list.container.tsx'; +import { SubPageLayout } from '../sub-page-layout.tsx'; + +export const MembersList: React.FC = () => { + const params = useParams(); + const navigate = useNavigate(); + const { + token: { colorTextBase }, + } = theme.useToken(); + + return ( + + Members + + } + extra={[ + , + ]} + /> + } + > + + + ); +}; diff --git a/apps/ui-community/src/components/layouts/admin/pages/members-profile.tsx b/apps/ui-community/src/components/layouts/admin/pages/members-profile.tsx new file mode 100644 index 000000000..25d08d029 --- /dev/null +++ b/apps/ui-community/src/components/layouts/admin/pages/members-profile.tsx @@ -0,0 +1,18 @@ +import { useParams } from 'react-router-dom'; +import { MemberProfileContainer } from '../../shared/components/member-profile.container.tsx'; + +export const MembersProfile: React.FC = () => { + const params = useParams(); + + return ( +
+

Members Profile

+ +
+ ); +}; diff --git a/apps/ui-community/src/components/layouts/admin/pages/members.tsx b/apps/ui-community/src/components/layouts/admin/pages/members.tsx new file mode 100644 index 000000000..587e53aa1 --- /dev/null +++ b/apps/ui-community/src/components/layouts/admin/pages/members.tsx @@ -0,0 +1,14 @@ +import { Route, Routes } from 'react-router-dom'; +import { MembersCreate } from './members-create.tsx'; +import { MembersDetail } from './members-detail.tsx'; +import { MembersList } from './members-list.tsx'; + +export const Members: React.FC = () => { + return ( + + } /> + } /> + } /> + + ); +}; diff --git a/apps/ui-community/src/components/layouts/shared/components/member-profile-details.container.graphql b/apps/ui-community/src/components/layouts/shared/components/member-profile-details.container.graphql new file mode 100644 index 000000000..698c07fa8 --- /dev/null +++ b/apps/ui-community/src/components/layouts/shared/components/member-profile-details.container.graphql @@ -0,0 +1,38 @@ +query SharedMemberProfileDetailsContainerMember($id: ObjectID!) { + member(id: $id) { + ...SharedMemberProfileDetailsContainerMemberFields + } +} + +mutation SharedMemberProfileDetailsContainerMemberProfileUpdate( + $input: MemberProfileUpdateInput! +) { + memberProfileUpdate(input: $input) { + ...SharedMemberProfileDetailsContainerMemberProfileMutationResultFields + } +} + +fragment SharedMemberProfileDetailsContainerMemberProfileMutationResultFields on MemberMutationResult { + status { + success + errorMessage + } + member { + ...SharedMemberProfileDetailsContainerMemberFields + } +} + +fragment SharedMemberProfileDetailsContainerMemberFields on Member { + profile { + name + email + bio + interests + showInterests + showEmail + showLocation + showProfile + showProperties + } + id +} diff --git a/apps/ui-community/src/components/layouts/shared/components/member-profile-details.container.tsx b/apps/ui-community/src/components/layouts/shared/components/member-profile-details.container.tsx new file mode 100644 index 000000000..91ebcd3eb --- /dev/null +++ b/apps/ui-community/src/components/layouts/shared/components/member-profile-details.container.tsx @@ -0,0 +1,69 @@ +import { App } from 'antd'; +import { useMutation, useQuery } from '@apollo/client'; +import { ComponentQueryLoader } from '@cellix/ui-core'; +import { + type MemberProfileInput, + SharedMemberProfileDetailsContainerMemberDocument, + SharedMemberProfileDetailsContainerMemberProfileUpdateDocument, +} from '../../../../generated.tsx'; +import { MemberProfileDetails } from './member-profile-details.tsx'; + +interface MemberProfileDetailsContainerProps { + data: { + id: string; + }; +} + +export const MemberProfileDetailsContainer: React.FC< + MemberProfileDetailsContainerProps +> = (props) => { + const { message } = App.useApp(); + const [updateMember] = useMutation( + SharedMemberProfileDetailsContainerMemberProfileUpdateDocument, + ); + const { + data: memberData, + loading: memberLoading, + error: memberError, + } = useQuery(SharedMemberProfileDetailsContainerMemberDocument, { + variables: { + id: props.data.id, + }, + }); + + const handleSave = async (values: MemberProfileInput) => { + try { + const result = await updateMember({ + variables: { + input: { + memberId: props.data.id, + profile: values, + }, + }, + }); + if (result.data?.memberProfileUpdate.status.success) { + message.success('Saved'); + } else { + message.error( + `Error updating Member: ${result.data?.memberProfileUpdate.status.errorMessage}`, + ); + } + } catch (error) { + message.error(`Error updating Member: ${JSON.stringify(error)}`); + } + }; + + return ( + + } + error={memberError} + /> + ); +}; diff --git a/apps/ui-community/src/components/layouts/shared/components/member-profile-details.stories.tsx b/apps/ui-community/src/components/layouts/shared/components/member-profile-details.stories.tsx new file mode 100644 index 000000000..4a5c2ae19 --- /dev/null +++ b/apps/ui-community/src/components/layouts/shared/components/member-profile-details.stories.tsx @@ -0,0 +1,140 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, fn, userEvent, within } from 'storybook/test'; +import { MemberProfileDetails } from './member-profile-details.tsx'; + +const meta: Meta = { + title: 'Components/Layouts/Shared/MemberProfileDetails', + component: MemberProfileDetails, + parameters: { + layout: 'padded', + }, +}; + +export default meta; +type Story = StoryObj; + +const mockOnSave = fn(); + +export const Default: Story = { + args: { + data: null, + onSave: mockOnSave, + }, + play: ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Verify all form fields are present + expect(canvas.getByLabelText('Name')).toBeInTheDocument(); + expect(canvas.getByLabelText('Email')).toBeInTheDocument(); + expect(canvas.getByLabelText('Bio')).toBeInTheDocument(); + expect(canvas.getByText('Show Interests')).toBeInTheDocument(); + expect(canvas.getByText('Show Email')).toBeInTheDocument(); + expect(canvas.getByText('Show Location')).toBeInTheDocument(); + expect(canvas.getByText('Show Profile')).toBeInTheDocument(); + expect(canvas.getByText('Show Properties')).toBeInTheDocument(); + expect(canvas.getByRole('button', { name: 'Save' })).toBeInTheDocument(); + }, +}; + +export const WithData: Story = { + args: { + data: { + name: 'John Doe', + email: 'john.doe@example.com', + bio: 'Software developer with 10 years of experience', + showInterests: true, + showEmail: true, + showLocation: false, + showProfile: true, + showProperties: false, + }, + onSave: mockOnSave, + }, + play: ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Verify form is populated with data + const nameInput = canvas.getByLabelText('Name') as HTMLInputElement; + expect(nameInput.value).toBe('John Doe'); + + const emailInput = canvas.getByLabelText('Email') as HTMLInputElement; + expect(emailInput.value).toBe('john.doe@example.com'); + + const bioInput = canvas.getByLabelText('Bio') as HTMLTextAreaElement; + expect(bioInput.value).toBe( + 'Software developer with 10 years of experience', + ); + + // Verify checkboxes + const showInterestsCheckbox = canvas.getByRole('checkbox', { + name: 'Show Interests', + }) as HTMLInputElement; + expect(showInterestsCheckbox.checked).toBe(true); + + const showEmailCheckbox = canvas.getByRole('checkbox', { + name: 'Show Email', + }) as HTMLInputElement; + expect(showEmailCheckbox.checked).toBe(true); + + const showLocationCheckbox = canvas.getByRole('checkbox', { + name: 'Show Location', + }) as HTMLInputElement; + expect(showLocationCheckbox.checked).toBe(false); + }, +}; + +export const FormSubmission: Story = { + args: { + data: { + name: '', + email: '', + bio: '', + showInterests: false, + showEmail: false, + showLocation: false, + showProfile: false, + showProperties: false, + }, + onSave: mockOnSave, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Fill in the form + const nameInput = canvas.getByLabelText('Name'); + await userEvent.type(nameInput, 'Test User'); + + const emailInput = canvas.getByLabelText('Email'); + await userEvent.type(emailInput, 'test@example.com'); + + const bioInput = canvas.getByLabelText('Bio'); + await userEvent.type(bioInput, 'Test bio'); + + // Toggle some checkboxes + const showEmailCheckbox = canvas.getByRole('checkbox', { + name: 'Show Email', + }); + await userEvent.click(showEmailCheckbox); + + const showProfileCheckbox = canvas.getByRole('checkbox', { + name: 'Show Profile', + }); + await userEvent.click(showProfileCheckbox); + + // Submit the form + const submitButton = canvas.getByRole('button', { name: 'Save' }); + await userEvent.click(submitButton); + + // Verify onSave was called with correct data + expect(mockOnSave).toHaveBeenCalledWith({ + name: 'Test User', + email: 'test@example.com', + bio: 'Test bio', + showInterests: false, + showEmail: true, + showLocation: false, + showProfile: true, + showProperties: false, + }); + }, +}; diff --git a/apps/ui-community/src/components/layouts/shared/components/member-profile-details.tsx b/apps/ui-community/src/components/layouts/shared/components/member-profile-details.tsx new file mode 100644 index 000000000..fca31f50d --- /dev/null +++ b/apps/ui-community/src/components/layouts/shared/components/member-profile-details.tsx @@ -0,0 +1,63 @@ +import { Button, Checkbox, Form, Input } from 'antd'; +import React from 'react'; +import type { MemberProfileInput } from '../../../../generated.tsx'; + +const { TextArea } = Input; + +interface MemberProfileDetailsProps { + data?: MemberProfileInput | null; + onSave: (values: MemberProfileInput) => void; +} + +export const MemberProfileDetails: React.FC = ( + props, +) => { + const [form] = Form.useForm(); + const [formLoading, setFormLoading] = React.useState(false); + return ( +
{ + setFormLoading(true); + props.onSave(values); + setFormLoading(false); + }} + > + + + + + + + +