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
1,381 changes: 5 additions & 1,376 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
"@typescript-eslint/parser": "^8.24.0",
"@vitejs/plugin-react": "^4.3.4",
"@vitest/coverage-v8": "^3.2.1",
"cypress": "^14.1.0",
"dotenv": "^16.4.7",
"eslint": "^9.19.0",
"eslint-plugin-react-hooks": "^5.0.0",
Expand Down
1 change: 1 addition & 0 deletions src/api/groups/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default as getAllGroups } from './getAllGroups';
export { default as useAllGroupsQuery } from './useAllGroupsQuery';
export { default as useCreateGroupQuery } from './useCreateGroupQuery';
36 changes: 36 additions & 0 deletions src/api/groups/useCreateGroupQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { SERVER_ROUTES } from '../../routes';
import { CreateGroupAPIRequest } from '../../types/api';
import { formatAPIPath } from '../../utils';
import APICore from '../core';
import { queryKeys } from '../query-keys';

function useCreateGroupQuery(client: APICore) {
const queryClient = useQueryClient();

return useMutation({
mutationKey: queryKeys.groups.create,
mutationFn: async ({
nickname,
}: {
nickname: string;
}) => {
const body: CreateGroupAPIRequest = {
type: 'group',
nickname: nickname,
};

const response = await client.post({
suffix: formatAPIPath([SERVER_ROUTES.GROUPS]),
body,
});

return response;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.groups.all });
},
});
}

export default useCreateGroupQuery;
1 change: 1 addition & 0 deletions src/api/query-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ export const queryKeys = {
groups: {
all: ['groups'] as const,
detail: (id: string) => ['groups', id] as const,
create: ['groups', 'create'] as const,
},
};
90 changes: 90 additions & 0 deletions src/components/Modal/CreateGroupModal.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import CreateGroupModal from './CreateGroupModal';

// Mock dependencies
vi.mock('../../api/groups', () => ({
useCreateGroupQuery: vi.fn().mockReturnValue({
mutateAsync: vi.fn().mockResolvedValue({ success: true }),
isPending: false,
}),
}));

vi.mock('../../hooks', () => ({
useAxios: vi.fn().mockReturnValue({}),
useFormValidation: vi.fn().mockReturnValue({
formState: { nickname: '' },
errors: {},
handleInputChange: vi.fn(),
isFormValid: vi.fn().mockReturnValue(true),
}),
}));

describe('CreateGroupModal', () => {
let queryClient: QueryClient;

beforeAll(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
});

const renderWithProviders = (component: React.ReactElement) => {
return render(
<QueryClientProvider client={queryClient}>
{component}
</QueryClientProvider>
);
};

it('renders modal when open', () => {
const mockOnClose = vi.fn();

renderWithProviders(
<CreateGroupModal isOpen={true} onClose={mockOnClose} />
);

expect(screen.getByRole('dialog')).toBeTruthy();
expect(screen.getByText('Enter in details below to create a new group')).toBeTruthy();
expect(screen.getByPlaceholderText('Admin Team')).toBeTruthy();
expect(screen.getByRole('button', { name: 'Create Group' })).toBeTruthy();
});

it('does not render when closed', () => {
const mockOnClose = vi.fn();

renderWithProviders(
<CreateGroupModal isOpen={false} onClose={mockOnClose} />
);

expect(screen.queryByRole('dialog')).toBeFalsy();
});

it('calls onClose when cancel button is clicked', () => {
const mockOnClose = vi.fn();

renderWithProviders(
<CreateGroupModal isOpen={true} onClose={mockOnClose} />
);

const cancelButton = screen.getByRole('button', { name: 'Cancel' });
fireEvent.click(cancelButton);

expect(mockOnClose).toHaveBeenCalled();
});

it('shows the group name field with proper label', () => {
const mockOnClose = vi.fn();

renderWithProviders(
<CreateGroupModal isOpen={true} onClose={mockOnClose} />
);

expect(screen.getByText('Group Name')).toBeTruthy();
expect(screen.getByDisplayValue('')).toBeTruthy(); // Input field
});
});
93 changes: 93 additions & 0 deletions src/components/Modal/CreateGroupModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Box, Button, Flex, Text } from '@radix-ui/themes';
import { InputField } from '..';
import { useCreateGroupQuery } from '../../api/groups';
import { useAxios, useFormValidation } from '../../hooks';
import { CreateGroupModalSchema } from '../../types/forms';
import Modal from './Modal';

export default function CreateGroupModal({
isOpen,
onClose,
}: {
isOpen: boolean;
onClose: () => void;
}) {
const title = 'Create Group';
const description = 'Enter in details below to create a new group';

const { formState, errors, handleInputChange, isFormValid } = useFormValidation({
schema: CreateGroupModalSchema,
});

const api = useAxios();
const mutation = useCreateGroupQuery(api);

const handleCreateGroup = async (e: React.FormEvent) => {
e.preventDefault();
if (!isFormValid()) return false;

const { nickname } = formState;

try {
const response = await mutation.mutateAsync({ nickname });
if (response) {
console.log('Group Created Successfully', response);
console.info('Group Created:', { formState });
onClose();
return true;
}
return false;
} catch (error) {
console.error('Error creating group:', error);
return false;
}
};

return (
<Modal
data-testid='create-group-modal'
title={title}
description={description}
onClose={onClose}
isOpen={isOpen}
>
<Flex direction='column' gap='4' style={{ paddingTop: '20px', paddingBottom: '15px' }} justify='start'>
<form>
<Box>
<Text
as='label'
size='2'
style={{
display: 'block',
marginBottom: '8px',
fontWeight: '600',
}}
>
Group Name<span style={{ color: 'red' }}>*</span>
</Text>

<InputField
name='nickname'
size='3'
placeholder='Admin Team'
radius='large'
value={formState.nickname}
onChange={handleInputChange}
style={{ width: '100%' }}
/>
{errors.nickname && <Text style={{ color: 'red', fontSize: '12px' }}>{errors.nickname}</Text>}
</Box>

<Flex gap='3' mt='4' justify='end'>
<Button variant='soft' color='gray' onClick={onClose}>
Cancel
</Button>
<Button type='submit' onClick={handleCreateGroup} loading={mutation.isPending}>
Create Group
</Button>
</Flex>
</form>
</Flex>
</Modal>
);
}
1 change: 1 addition & 0 deletions src/components/Modal/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { default as CreateAppModal } from './CreateAppModal';
export { default as CreateGroupModal } from './CreateGroupModal';
export { default as CreateUserModal } from './CreateUserModal';
export { default as Modal } from './Modal';
export { default as PasswordGeneratedModal } from './PasswordGeneratedModal';
Expand Down
3 changes: 3 additions & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ export { default as Protected } from './Protected/Protected';
export { default as Sidebar } from './Sidebar/Sidebar';
export { default as Table } from './Table';
export { default as TableList } from './TableList/TableList';

// Export modals
export * from './Modal';
35 changes: 23 additions & 12 deletions src/pages/GroupList.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import { useAllGroupsQuery } from '../api';
import { Table } from '../components';
import { CreateGroupModal, Table } from '../components';
import { Groups } from '../utils/helpers/models';

const GroupList = () => {
Expand Down Expand Up @@ -57,27 +57,38 @@ const GroupList = () => {
);

const { data, isLoading, error } = useAllGroupsQuery();
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);

if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;

const handleAddGroup = () => {
console.log('Add group');
setIsCreateModalOpen(true);
};

const handleCloseModal = () => {
setIsCreateModalOpen(false);
};

const handleDeleteGroup = () => {
console.log('Delete group');
};
return (
<Table
testId='group-list'
columnDefs={groupColumnHeadings}
data={data}
itemName='group'
onDelete={handleDeleteGroup}
onAdd={handleAddGroup}
initialValues={{}}
/>
<>
<Table
testId='group-list'
columnDefs={groupColumnHeadings}
data={data}
itemName='group'
onDelete={handleDeleteGroup}
onAdd={handleAddGroup}
initialValues={{}}
/>
<CreateGroupModal
isOpen={isCreateModalOpen}
onClose={handleCloseModal}
/>
</>
);
};

Expand Down
7 changes: 6 additions & 1 deletion src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export type MutationRequestParams = {
onError?: (error: AxiosError) => void;
};

export type APIRequestBody = CreateUserAPIRequest | UpdateUserAPIRequest | Record<string, never>;
export type APIRequestBody = CreateUserAPIRequest | UpdateUserAPIRequest | CreateGroupAPIRequest | Record<string, never>;

export type APIResponseToastMessages = {
[key: number]: {
Expand All @@ -44,3 +44,8 @@ export type UpdateUserAPIRequest = {
type: string;
active: boolean;
};

export type CreateGroupAPIRequest = {
type: 'group';
nickname: string;
};
4 changes: 4 additions & 0 deletions src/types/forms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export const UpdateAppModalSchema = z.object({
appURL: z.string().url({ message: 'Invalid URL format' }).optional().or(z.literal('')),
});

export const CreateGroupModalSchema = z.object({
nickname: z.string().min(1, { message: 'Group Name is required' }),
});

export type UserAppInitialValues = {
appName: string;
appURL: string;
Expand Down