Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion apps/ui-community/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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 (
<ComponentQueryLoader
loading={memberLoading}
hasData={memberData?.member}
hasDataComponent={<MembersAccountsList {...membersAccountsListProps} />}
error={memberError}
/>
);
};
Original file line number Diff line number Diff line change
@@ -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<typeof MembersAccountsList> = {
title: 'Components/Layouts/Admin/MembersAccountsList',
component: MembersAccountsList,
decorators: [
(Story) => (
<BrowserRouter>
<Story />
</BrowserRouter>
),
],
parameters: {
layout: 'padded',
},
};

export default meta;
type Story = StoryObj<typeof MembersAccountsList>;

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);
},
};
Original file line number Diff line number Diff line change
@@ -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<MembersAccountsListProps> = (
props,
) => {
const navigate = useNavigate();
const columns: TableColumnsType<AdminMembersAccountsListContainerMemberAccountFieldsFragment> =
[
{
title: 'Action',
dataIndex: 'id',
render: (
text: AdminMembersAccountsListContainerMemberAccountFieldsFragment['id'],
) => (
<Button type="primary" size="small" onClick={() => navigate(text)}>
Edit
</Button>
),
},
{
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'],
) => <span>{dayjs(text).format('MM/DD/YYYY')}</span>,
},
{
title: 'Created',
dataIndex: 'createdAt',
key: 'createdAt',
render: (
text: AdminMembersAccountsListContainerMemberAccountFieldsFragment['createdAt'],
) => <span>{dayjs(text).format('MM/DD/YYYY')}</span>,
},
];

return (
<>
<Button
onClick={() => navigate('./add')}
icon={<UsergroupAddOutlined />}
>
Add Account
</Button>
<div>
<Table
columns={columns}
dataSource={props.data}
rowKey={(record) => record.id}
/>
</div>
</>
);
};
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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<MembersCreateContainerProps> = (
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 <MembersCreate {...membersCreateProps} />;
};
Loading
Loading