diff --git a/cypress/e2e/users.cy.ts b/cypress/e2e/users.cy.ts index e3d2298e..a09c8d0c 100644 --- a/cypress/e2e/users.cy.ts +++ b/cypress/e2e/users.cy.ts @@ -16,4 +16,127 @@ describe('Users', () => { cy.findByRole('link', { name: 'home page' }).should('exist'); }); + + describe('add dialog', () => { + beforeEach(() => { + cy.visit('/admin/users'); + cy.findByRole('button', { name: 'Add User' }).click(); + }); + + afterEach(() => { + cy.clearMocks(); + }); + + it('displays required error when username is empty', () => { + cy.findByRole('button', { name: 'Submit' }).click(); + cy.findByText('Username is required.').should('exist'); + }); + + it('displays password field only when auth_type is "local"', () => { + cy.findByLabelText('Username').type('new_user'); + cy.findByLabelText('Password').should('exist'); + }); + + it('dose not displays password field only when auth_type is "FedID"', () => { + cy.findAllByRole('combobox').first().click(); + cy.findByRole('option', { name: 'FedID' }).click(); + + cy.findByLabelText('Username').type('new_user'); + cy.findByLabelText('Password').should('not.exist'); + }); + + it('adds user successfully (local)', () => { + cy.findByLabelText('Username').type('new_user'); + cy.findByLabelText('Password').type('secure_password'); + + cy.findAllByRole('combobox').last().click(); + cy.findByRole('option', { name: '/submit/hdf POST' }).click(); + cy.findAllByRole('combobox').last().click(); + cy.findByRole('option', { name: '/users PATCH' }).click(); + + cy.startSnoopingBrowserMockedRequest(); + + cy.findByRole('button', { name: 'Submit' }).click(); + + cy.findBrowserMockedRequests({ method: 'POST', url: '/users' }).should( + async (postRequests) => { + expect(postRequests.length).equal(1); + const request = postRequests[0]; + expect(JSON.stringify(await request.json())).equal( + JSON.stringify({ + _id: 'new_user', + sha256_password: 'secure_password', + auth_type: 'local', + authorised_routes: ['/submit/hdf POST', '/users PATCH'], + }) + ); + } + ); + }); + + it('adds user successfully (fedId)', () => { + cy.findByLabelText('Username').type('new_user'); + + cy.findAllByRole('combobox').first().click(); + cy.findByRole('option', { name: 'FedID' }).click(); + + cy.startSnoopingBrowserMockedRequest(); + + cy.findByRole('button', { name: 'Submit' }).click(); + + cy.findBrowserMockedRequests({ method: 'POST', url: '/users' }).should( + async (postRequests) => { + expect(postRequests.length).equal(1); + const request = postRequests[0]; + expect(JSON.stringify(await request.json())).equal( + JSON.stringify({ + _id: 'new_user', + auth_type: 'FedID', + }) + ); + } + ); + }); + + it('displays error when adding a user without a password for "local" auth_type', () => { + cy.findByLabelText('Username').type('local_user'); + cy.findByRole('button', { name: 'Submit' }).click(); + cy.findByText( + 'for the auth_type you put (local), a password is required. Please add this field' + ).should('exist'); + }); + + it('displays error for duplicate username', () => { + cy.findByLabelText('Username').type('test_dup'); + cy.findByLabelText('Password').type('secure_password'); + cy.findByRole('button', { name: 'Submit' }).click(); + cy.findByText( + 'username field must not be the same as a pre existing user. You put: test_dup' + ).should('exist'); + }); + + it('displays general error for unknown issues', () => { + cy.findByLabelText('Username').type('error'); + cy.findByLabelText('Password').type('secure_password'); + cy.findByRole('button', { name: 'Submit' }).click(); + cy.findByText( + 'An unexpected error occurred. Please try again later.' + ).should('exist'); + }); + + it('should show and hide password when clicking the visibility toggle', () => { + cy.findByLabelText('Username').type('testuser'); + cy.findByLabelText('Password').type('secure_password'); + + cy.findByLabelText('Password').should('have.attr', 'type', 'password'); + + cy.findByLabelText('Show password').click(); + + cy.findByLabelText('Password').should('have.attr', 'type', 'text'); + + cy.findByLabelText('Hide password').click(); + + cy.findByLabelText('Password').should('have.attr', 'type', 'password'); + }); + }); }); diff --git a/package.json b/package.json index 1c42edd0..191749bf 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "@emotion/react": "11.14.0", "@emotion/styled": "11.14.0", "@hello-pangea/dnd": "17.0.0", + "@hookform/resolvers": "^3.9.1", "@mui/base": "5.0.0-beta.40", "@mui/icons-material": "5.16.2", "@mui/material": "5.16.2", @@ -37,11 +38,13 @@ "react": "18.3.1", "react-colorful": "5.6.1", "react-dom": "18.3.1", + "react-hook-form": "^7.53.2", "react-redux": "9.2.0", "react-router-dom": "7.1.1", "single-spa-react": "6.0.2", "typescript": "5.6.2", - "vite": "5.4.11" + "vite": "5.4.11", + "zod": "^3.23.8" }, "resolutions": { "nwsapi": "2.2.13", diff --git a/src/admin/users/__snapshots__/usersTable.component.test.tsx.snap b/src/admin/users/__snapshots__/usersTable.component.test.tsx.snap index f110a457..bae1510c 100644 --- a/src/admin/users/__snapshots__/usersTable.component.test.tsx.snap +++ b/src/admin/users/__snapshots__/usersTable.component.test.tsx.snap @@ -19,9 +19,60 @@ exports[`UsersTable Snapshot > matches snapshot 1`] = `

- +
+ + +
@@ -217,15 +268,15 @@ exports[`UsersTable Snapshot > matches snapshot 1`] = `
- User ID + Username
matches snapshot 1`] = ` > diff --git a/src/admin/users/userDialogue.component.test.tsx b/src/admin/users/userDialogue.component.test.tsx new file mode 100644 index 00000000..c3e5636d --- /dev/null +++ b/src/admin/users/userDialogue.component.test.tsx @@ -0,0 +1,210 @@ +import { screen } from '@testing-library/react'; +import userEvent, { UserEvent } from '@testing-library/user-event'; +import axios from 'axios'; +import { MockInstance } from 'vitest'; +import { renderComponentWithProviders } from '../../testUtils'; +import UserDialogue, { UserDialogueProps } from './userDialogue.component'; + +describe('userDialogue', () => { + let props: UserDialogueProps; + let user: UserEvent; + + const onClose = vi.fn(); + const createView = () => { + return renderComponentWithProviders(); + }; + + beforeEach(() => { + props = { onClose: onClose, open: true, requestType: 'post' }; + user = userEvent.setup(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + describe('add dialog', () => { + let axiosPostSpy: MockInstance; + + beforeEach(() => { + axiosPostSpy = vi.spyOn(axios, 'post'); + }); + afterEach(() => { + vi.clearAllMocks(); + }); + + it('renders the component correctly', async () => { + createView(); + expect(screen.getByText('Add User')).toBeInTheDocument(); + }); + + it('displays required error when username is empty', async () => { + createView(); + const submitButton = screen.getByText('Submit'); + await user.click(submitButton); + + expect( + await screen.findByText('Username is required.') + ).toBeInTheDocument(); + }); + + it('displays password field only when auth_type is "local"', async () => { + createView(); + + await user.type(screen.getByLabelText('Username'), 'new_user'); + expect(screen.getByLabelText('Password')).toBeInTheDocument(); + }); + + it('does not displays password field only when auth_type is "local"', async () => { + createView(); + await user.type(screen.getByLabelText('Username'), 'new_user'); + const [authType, _routes] = screen.getAllByRole('combobox'); + await user.click(authType); + await user.click(await screen.findByText('FedID')); + expect(screen.queryByLabelText('Password')).not.toBeInTheDocument(); + }); + + it('adds user successfully (local)', async () => { + createView(); + + await user.type(screen.getByLabelText('Username'), 'new_user'); + await user.type(screen.getByLabelText('Password'), 'secure_password'); + + const [_authType, routes] = screen.getAllByRole('combobox'); + + await user.click(routes); + await user.click(await screen.findByText('/submit/hdf POST')); + await user.click(routes); + await user.click(await screen.findByText('/users PATCH')); + + await user.click(screen.getByText('Submit')); + + expect(axiosPostSpy).toHaveBeenCalledWith( + '/users', + { + _id: 'new_user', + auth_type: 'local', + authorised_routes: ['/submit/hdf POST', '/users PATCH'], + sha256_password: 'secure_password', + }, + { + headers: { + Authorization: 'Bearer null', + }, + } + ); + }); + + it('adds user successfully (fedId)', async () => { + createView(); + + await user.type(screen.getByLabelText('Username'), 'new_user'); + + const [authType, _routes] = screen.getAllByRole('combobox'); + + await user.click(authType); + await user.click(await screen.findByText('FedID')); + + await user.click(screen.getByText('Submit')); + + expect(axiosPostSpy).toHaveBeenCalledWith( + '/users', + { + _id: 'new_user', + auth_type: 'FedID', + }, + { + headers: { + Authorization: 'Bearer null', + }, + } + ); + }); + + it('adds user successfully (fedId) switch from local to fedId', async () => { + // This test that the password us removed if you switch from local to fedId + createView(); + + await user.type(screen.getByLabelText('Username'), 'new_user'); + await user.type(screen.getByLabelText('Password'), 'secure_password'); + + const [authType, _routes] = screen.getAllByRole('combobox'); + + await user.click(authType); + await user.click(await screen.findByText('FedID')); + + await user.click(screen.getByText('Submit')); + + expect(axiosPostSpy).toHaveBeenCalledWith( + '/users', + { + _id: 'new_user', + auth_type: 'FedID', + }, + { + headers: { + Authorization: 'Bearer null', + }, + } + ); + }); + + it('displays error when adding a user without a password for "local" auth_type', async () => { + createView(); + await user.type(screen.getByLabelText('Username'), 'local_user'); + await user.click(screen.getByText('Submit')); + expect( + await screen.findByText( + 'for the auth_type you put (local), a password is required. Please add this field' + ) + ).toBeInTheDocument(); + }); + + it('displays error for duplicate username', async () => { + createView(); + + await user.type(screen.getByLabelText('Username'), 'test_dup'); + await user.type(screen.getByLabelText('Password'), 'secure_password'); + await user.click(screen.getByText('Submit')); + + expect( + await screen.findByText( + 'username field must not be the same as a pre existing user. You put: test_dup' + ) + ).toBeInTheDocument(); + }); + + it('displays general error for unknown issues', async () => { + createView(); + + await user.type(screen.getByLabelText('Username'), 'error'); + await user.type(screen.getByLabelText('Password'), 'secure_password'); + await user.click(screen.getByText('Submit')); + + expect( + await screen.findByText( + 'An unexpected error occurred. Please try again later.' + ) + ).toBeInTheDocument(); + }); + + it('should show and hide password when clicking the visibility toggle', async () => { + createView(); + + await user.type(screen.getByLabelText('Username'), 'testuser'); + await user.type(screen.getByLabelText('Password'), 'secure_password'); + + const passwordField = screen.getByLabelText('Password'); + expect(passwordField).toHaveAttribute('type', 'password'); + + const visibilityIcon = screen.getByLabelText('Show password'); + await user.click(visibilityIcon); + + expect(passwordField).toHaveAttribute('type', 'text'); + + const hideVisibilityIcon = screen.getByLabelText('Hide password'); + await user.click(hideVisibilityIcon); + + expect(passwordField).toHaveAttribute('type', 'password'); + }); + }); +}); diff --git a/src/admin/users/userDialogue.component.tsx b/src/admin/users/userDialogue.component.tsx new file mode 100644 index 00000000..05f07cbd --- /dev/null +++ b/src/admin/users/userDialogue.component.tsx @@ -0,0 +1,259 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import VisibilityIcon from '@mui/icons-material/Visibility'; +import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; +import { + Autocomplete, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormHelperText, + IconButton, + TextField, +} from '@mui/material'; +import { AxiosError } from 'axios'; +import React from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { z } from 'zod'; +import { useAddUser } from '../../api/user'; +import { APIError, User, type UserPost } from '../../app.types'; +import { AUTH_TYPE_LIST, AUTHORISED_ROUTE_LIST } from './usersTable.component'; + +export interface UserDialogueProps { + onClose: () => void; + open: boolean; + requestType: 'post' | 'patch'; +} + +interface BaseZodSchemaProps { + errorMessage?: string; +} + +const OptionalStringSchema = z + .string() + .trim() + .transform((val) => (!val ? undefined : val)) + .optional(); + +const MandatoryStringSchema = (props: BaseZodSchemaProps) => + z + .string({ + required_error: props.errorMessage, + }) + .trim() + .min(1, { message: props.errorMessage }); + +// Define Zod schema for the User form +const userSchema = z.object({ + _id: MandatoryStringSchema({ errorMessage: 'Username is required.' }), + sha256_password: OptionalStringSchema, + auth_type: MandatoryStringSchema({}), + authorised_routes: z + .array(z.string()) + .transform((val) => (val.length === 0 || !val ? undefined : val)) + .optional(), +}); + +const UserDialogue = (props: UserDialogueProps) => { + const { open, onClose, requestType } = props; + + const initialUser: UserPost = { + _id: '', + sha256_password: '', + auth_type: 'local', + authorised_routes: [], + }; + + const { + control, + register, + watch, + handleSubmit, + clearErrors, + setError, + reset, + setValue, + formState: { errors }, + } = useForm({ + resolver: zodResolver(userSchema), + defaultValues: initialUser, + }); + const userFormData = watch(); + React.useEffect(() => { + if (userFormData.auth_type === 'FedID') { + setValue('sha256_password', undefined); + } + }, [setValue, userFormData.auth_type]); + + const handleClose = React.useCallback(() => { + reset(); + clearErrors(); + onClose(); + }, [clearErrors, onClose, reset]); + + const { mutateAsync: addUser, isPending: isAddPending } = useAddUser(); + const handleAddUser = React.useCallback( + async (user: User) => { + addUser(user) + .then(() => handleClose()) + .catch((error: AxiosError) => { + const errorDetail = (error.response?.data as APIError).detail; + + if (typeof errorDetail === 'string') { + let field: 'root.formError' | 'sha256_password' | '_id' = + 'root.formError'; + let message: string = errorDetail; + + if (errorDetail.toLowerCase().includes('password')) { + field = 'sha256_password'; + } else if (errorDetail.toLowerCase().includes('username')) { + field = '_id'; + } else { + message = 'An unexpected error occurred. Please try again later.'; + } + + setError(field, { message }); + } + }); + }, + [addUser, handleClose, setError] + ); + + const onSubmit = (data: UserPost) => { + const newData: UserPost = { + ...data, + ...(data.sha256_password && { + sha256_password: data.sha256_password, + }), + }; + + if (requestType === 'post') handleAddUser(newData); + }; + + const [showPassword, setShowPassword] = React.useState(false); + + const togglePasswordVisibility = () => { + setShowPassword(!showPassword); + }; + + return ( + + Add User + + ( + option} + onChange={(_, value) => field.onChange(value)} + renderInput={(params) => ( + + )} + /> + )} + /> + + + {userFormData.auth_type === 'local' && ( + + {showPassword ? : } + + ), + }} + margin="dense" + error={!!errors.sha256_password} + helperText={errors.sha256_password?.message} + /> + )} + ( + option} + onChange={(_, value) => field.onChange(value)} + renderInput={(params) => ( + + )} + /> + )} + /> + + + + + + {errors.root?.formError && ( + + {errors.root?.formError.message} + + )} + + ); +}; + +export default UserDialogue; diff --git a/src/admin/users/usersTable.component.test.tsx b/src/admin/users/usersTable.component.test.tsx index cf523aa8..e4f976d4 100644 --- a/src/admin/users/usersTable.component.test.tsx +++ b/src/admin/users/usersTable.component.test.tsx @@ -1,9 +1,71 @@ +import { screen, waitFor } from '@testing-library/react'; +import userEvent, { UserEvent } from '@testing-library/user-event'; import { renderComponentWithProviders } from '../../testUtils'; import UsersTable from './usersTable.component'; // Update with the correct path describe('UsersTable Snapshot', () => { + let user: UserEvent; + const createView = () => { + return renderComponentWithProviders(); + }; + + beforeEach(() => { + user = userEvent.setup(); + }); it('matches snapshot', () => { - const { asFragment } = renderComponentWithProviders(); + const { asFragment } = createView(); expect(asFragment()).toMatchSnapshot(); }); + + it('opens add dialog and closes it correctly', async () => { + createView(); + await waitFor(() => { + expect(screen.getByText('user1')).toBeInTheDocument(); + }); + + const addButton = screen.getByRole('button', { name: 'Add User' }); + await user.click(addButton); + + await waitFor(() => { + expect( + screen.getByRole('dialog', { name: 'Add User' }) + ).toBeInTheDocument(); + }); + + const closeButton = screen.getByRole('button', { name: 'Cancel' }); + await user.click(closeButton); + + await waitFor(() => { + expect( + screen.queryByRole('dialog', { name: 'Add User' }) + ).not.toBeInTheDocument(); + }); + }); + + it('sets the table filters and clears the table filters', async () => { + createView(); + + await waitFor(() => { + expect(screen.getByText('user1')).toBeInTheDocument(); + }); + const clearFiltersButton = screen.getByRole('button', { + name: 'Clear Filters', + }); + + expect(clearFiltersButton).toBeDisabled(); + + const nameInput = screen.getByLabelText('Filter by Username'); + + await user.type(nameInput, '9'); + + await waitFor(() => { + expect(screen.queryByText('user1')).not.toBeInTheDocument(); + }); + + await user.click(clearFiltersButton); + + await waitFor(() => { + expect(screen.getByText('user1')).toBeInTheDocument(); + }); + }, 10000); }); diff --git a/src/admin/users/usersTable.component.tsx b/src/admin/users/usersTable.component.tsx index 1092fb1a..125e3108 100644 --- a/src/admin/users/usersTable.component.tsx +++ b/src/admin/users/usersTable.component.tsx @@ -1,14 +1,18 @@ -import { Chip, Stack } from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import ClearIcon from '@mui/icons-material/Clear'; +import { Box, Button, Chip, Stack } from '@mui/material'; import { MaterialReactTable, MRT_ColumnDef, useMaterialReactTable, } from 'material-react-table'; import { MRT_Localization_EN } from 'material-react-table/locales/en'; +import React from 'react'; import { useUsers } from '../../api/user'; import { User } from '../../app.types'; +import UserDialogue from './userDialogue.component'; -const AUTHORISED_ROUTE_LIST = [ +export const AUTHORISED_ROUTE_LIST = [ '/submit/hdf POST', '/submit/manifest POST', '/records/{id_} DELETE', @@ -18,15 +22,19 @@ const AUTHORISED_ROUTE_LIST = [ '/users/{id_} DELETE', ]; -const AUTH_TYPE_LIST = ['local', 'FedID']; +export const AUTH_TYPE_LIST = ['local', 'FedID']; function UsersTable() { const { data: userData, isLoading: userDataLoading } = useUsers(); + const [requestType, setRequestType] = React.useState<'patch' | 'post'>( + 'post' + ); + // Define the columns for the table const columns: MRT_ColumnDef[] = [ { accessorKey: '_id', - header: 'User ID', + header: 'Username', }, { @@ -45,7 +53,7 @@ function UsersTable() { return routes && routes.length > 0 ? ( {routes.map((route, index) => ( - + ))} ) : ( @@ -93,6 +101,45 @@ function UsersTable() { shape: 'rounded', variant: 'outlined', }, + renderCreateRowDialogContent: ({ table }) => { + return ( + <> + { + table.setCreatingRow(null); + }} + /> + + ); + }, + renderTopToolbarCustomActions: ({ table }) => ( + + + + + ), }); return ; } diff --git a/src/api/user.test.tsx b/src/api/user.test.tsx index 0763e0b8..006814cf 100644 --- a/src/api/user.test.tsx +++ b/src/api/user.test.tsx @@ -2,7 +2,7 @@ import { renderHook, waitFor } from '@testing-library/react'; import { User } from '../app.types'; import usersJson from '../mocks/users.json'; import { hooksWrapperWithProviders } from '../testUtils'; -import { useUsers } from './user'; +import { useAddUser, useUsers } from './user'; describe('useUsers', () => { it('sends request to fetch users and returns successful response', async () => { @@ -21,3 +21,24 @@ describe('useUsers', () => { 'sends axios request to fetch users and throws an appropriate error on failure' ); }); + +describe('useAddUser', () => { + it('posts a request to add a user and returns successful response', async () => { + const { result } = renderHook(() => useAddUser(), { + wrapper: hooksWrapperWithProviders(), + }); + expect(result.current.isIdle).toBe(true); + + result.current.mutate({ ...usersJson[0], sha256_password: 'test' }); + + await waitFor(() => { + expect(result.current.isSuccess).toBeTruthy(); + }); + + expect(result.current.data).toEqual(usersJson[0]._id); + }); + + it.todo( + 'sends axios request to post user session and throws an appropriate error on failure' + ); +}); diff --git a/src/api/user.tsx b/src/api/user.tsx index feab09a0..ebf4882c 100644 --- a/src/api/user.tsx +++ b/src/api/user.tsx @@ -1,6 +1,12 @@ -import { UseQueryResult, useQuery } from '@tanstack/react-query'; +import { + UseMutationResult, + UseQueryResult, + useMutation, + useQuery, + useQueryClient, +} from '@tanstack/react-query'; import axios, { AxiosError } from 'axios'; -import { User } from '../app.types'; +import { User, type UserPost } from '../app.types'; import { readSciGatewayToken } from '../parseTokens'; import { useAppSelector } from '../state/hooks'; import { selectUrls } from '../state/slices/configSlice'; @@ -27,3 +33,31 @@ export const useUsers = (): UseQueryResult => { }, }); }; + +const addUser = (apiUrl: string, user: UserPost): Promise => { + return axios + .post(`${apiUrl}/users`, user, { + headers: { + Authorization: `Bearer ${readSciGatewayToken()}`, + }, + }) + .then((response) => response.data); +}; + +export const useAddUser = (): UseMutationResult< + string, + AxiosError, + UserPost +> => { + const { apiUrl } = useAppSelector(selectUrls); + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (user: UserPost) => addUser(apiUrl, user), + onError: (error) => { + console.log('Got error ' + error.message); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['Users'] }); + }, + }); +}; diff --git a/src/app.types.tsx b/src/app.types.tsx index 165cda20..f4c47dd7 100644 --- a/src/app.types.tsx +++ b/src/app.types.tsx @@ -318,6 +318,13 @@ export interface User { authorised_routes?: string[] | null; } +export interface UserPost { + _id: string; // Maps the `username` field in Python, which has an alias "_id" + auth_type: string; + sha256_password?: string; + authorised_routes?: string[] | null; +} + export interface FavouriteFilterPost { name: string; filter: string; diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index 0afb4f3d..5c9b6071 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -7,6 +7,7 @@ import { isChannelScalar, Record, ValidateFunctionPost, + type UserPost, } from '../app.types'; import { PREFERRED_COLOUR_MAP_PREFERENCE_NAME } from '../settingsMenuItems.component'; import channelsJson from './channels.json'; @@ -378,6 +379,39 @@ export const handlers = [ return HttpResponse.json(usersJson, { status: 200 }); }), + http.post('/users', async ({ request }) => { + const body = (await request.json()) as UserPost; + + if (body.auth_type === 'local' && !body.sha256_password) { + return HttpResponse.json( + { + detail: + 'for the auth_type you put (local), a password is required. Please add this field', + }, + { status: 400 } + ); + } + + if (body._id === 'test_dup') { + return HttpResponse.json( + { + detail: `username field must not be the same as a pre existing user. You put: ${body._id} `, + }, + { status: 400 } + ); + } + + if (body._id === 'error') { + return HttpResponse.json( + { + detail: 'Unknown error', + }, + { status: 400 } + ); + } + return HttpResponse.json(body._id, { status: 201 }); + }), + http.post('/users/filters', async () => { return HttpResponse.json('1', { status: 201 }); }), diff --git a/yarn.lock b/yarn.lock index 84b66b3e..d3dc6bf3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -814,6 +814,15 @@ __metadata: languageName: node linkType: hard +"@hookform/resolvers@npm:^3.9.1": + version: 3.9.1 + resolution: "@hookform/resolvers@npm:3.9.1" + peerDependencies: + react-hook-form: ^7.0.0 + checksum: 10c0/8a4056db3860b12ee30921ba352996104d6ae75ac45996d4c8b6df429e07ee73f5b87c82a22a15403789213f6f52f5fead1c2637b26ef624068b68d213362cd1 + languageName: node + linkType: hard + "@humanfs/core@npm:^0.19.1": version: 0.19.1 resolution: "@humanfs/core@npm:0.19.1" @@ -1187,8 +1196,8 @@ __metadata: linkType: hard "@mui/utils@npm:^5.14.16, @mui/utils@npm:^5.15.14, @mui/utils@npm:^5.16.2, @mui/utils@npm:^5.16.6": - version: 5.16.8 - resolution: "@mui/utils@npm:5.16.8" + version: 5.16.6 + resolution: "@mui/utils@npm:5.16.6" dependencies: "@babel/runtime": "npm:^7.23.9" "@mui/types": "npm:^7.2.15" @@ -1197,12 +1206,12 @@ __metadata: prop-types: "npm:^15.8.1" react-is: "npm:^18.3.1" peerDependencies: - "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 - react: ^17.0.0 || ^18.0.0 || ^19.0.0 + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 peerDependenciesMeta: "@types/react": optional: true - checksum: 10c0/86a1daf249a1dc766c0babe439c6874e092ce8239bdd67a57e81fc349934bdee0c98ee1833b282286847ffc16966edff88d9e2a47ac98577fb1010245191888a + checksum: 10c0/2db3d11a83d7216fb8ceb459d4b30c795922c04cd8fabc26c721dd7b4f5ed5c4f3f3ace6ea70227bf3b79361bd58f13b723562cfd40255424d979ab238ab2e91 languageName: node linkType: hard @@ -6908,6 +6917,7 @@ __metadata: "@emotion/styled": "npm:11.14.0" "@eslint/compat": "npm:1.2.4" "@hello-pangea/dnd": "npm:17.0.0" + "@hookform/resolvers": "npm:^3.9.1" "@mui/base": "npm:5.0.0-beta.40" "@mui/icons-material": "npm:5.16.2" "@mui/material": "npm:5.16.2" @@ -6963,6 +6973,7 @@ __metadata: react: "npm:18.3.1" react-colorful: "npm:5.6.1" react-dom: "npm:18.3.1" + react-hook-form: "npm:^7.53.2" react-redux: "npm:9.2.0" react-router-dom: "npm:7.1.1" serve: "npm:14.2.0" @@ -6975,6 +6986,7 @@ __metadata: vitest: "npm:2.1.8" vitest-canvas-mock: "npm:0.3.3" vitest-fail-on-console: "npm:0.7.1" + zod: "npm:^3.23.8" languageName: unknown linkType: soft @@ -7515,6 +7527,15 @@ __metadata: languageName: node linkType: hard +"react-hook-form@npm:^7.53.2": + version: 7.53.2 + resolution: "react-hook-form@npm:7.53.2" + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + checksum: 10c0/18336d8e8798a70dcd0af703a0becca2d5dbf82a7b7a3ca334ae0e1f26410490bc3ef2ea51adcf790bb1e7006ed7a763fd00d664e398f71225b23529a7ccf0bf + languageName: node + linkType: hard + "react-is@npm:^16.13.1, react-is@npm:^16.7.0": version: 16.13.1 resolution: "react-is@npm:16.13.1" @@ -9692,3 +9713,10 @@ __metadata: checksum: 10c0/a0e36eb88fea2c7981eab22d1ba45e15d8d268626e6c4143305e2c1628fa17ebfaa40cd306161a8ce04c0a60ee0262058eab12567493d5eb1409780853454c6f languageName: node linkType: hard + +"zod@npm:^3.23.8": + version: 3.23.8 + resolution: "zod@npm:3.23.8" + checksum: 10c0/8f14c87d6b1b53c944c25ce7a28616896319d95bc46a9660fe441adc0ed0a81253b02b5abdaeffedbeb23bdd25a0bf1c29d2c12dd919aef6447652dd295e3e69 + languageName: node + linkType: hard