diff --git a/README.md b/README.md index f8576165..db432eba 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ This readme file is used mostly to document project dependencies and conventions - react-error-boundary - react-tooltip - react-zoom-pan-pinch + - react-hook-form - reactflow - js-file-download - use-debounce @@ -46,6 +47,7 @@ This readme file is used mostly to document project dependencies and conventions - html-to-image - zustand - zod + - @hookform/resolvers - @tanstack/react-table - @tanstack/react-query - @tanstack/react-query-devtools diff --git a/rsconcept/frontend/package-lock.json b/rsconcept/frontend/package-lock.json index 774100ff..a899f331 100644 --- a/rsconcept/frontend/package-lock.json +++ b/rsconcept/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "@dagrejs/dagre": "^1.1.4", + "@hookform/resolvers": "^3.10.0", "@lezer/lr": "^1.4.2", "@tanstack/react-query": "^5.64.2", "@tanstack/react-query-devtools": "^5.64.2", @@ -23,6 +24,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-error-boundary": "^5.0.0", + "react-hook-form": "^7.54.2", "react-icons": "^5.4.0", "react-intl": "^7.1.5", "react-router": "^7.1.3", @@ -1699,6 +1701,15 @@ "tslib": "2" } }, + "node_modules/@hookform/resolvers": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", + "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==", + "license": "MIT", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -9102,6 +9113,22 @@ "react": ">=16.13.1" } }, + "node_modules/react-hook-form": { + "version": "7.54.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.2.tgz", + "integrity": "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-icons": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz", diff --git a/rsconcept/frontend/package.json b/rsconcept/frontend/package.json index 1b604886..bb02564d 100644 --- a/rsconcept/frontend/package.json +++ b/rsconcept/frontend/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@dagrejs/dagre": "^1.1.4", + "@hookform/resolvers": "^3.10.0", "@lezer/lr": "^1.4.2", "@tanstack/react-query": "^5.64.2", "@tanstack/react-query-devtools": "^5.64.2", @@ -27,6 +28,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-error-boundary": "^5.0.0", + "react-hook-form": "^7.54.2", "react-icons": "^5.4.0", "react-intl": "^7.1.5", "react-router": "^7.1.3", diff --git a/rsconcept/frontend/src/app/Navigation/UserButton.tsx b/rsconcept/frontend/src/app/Navigation/UserButton.tsx index bd00f10e..c4f69e5d 100644 --- a/rsconcept/frontend/src/app/Navigation/UserButton.tsx +++ b/rsconcept/frontend/src/app/Navigation/UserButton.tsx @@ -11,6 +11,7 @@ interface UserButtonProps { function UserButton({ onLogin, onClickUser }: UserButtonProps) { const { user, isAnonymous } = useAuthSuspense(); + console.log(user); const adminMode = usePreferencesStore(state => state.adminMode); if (isAnonymous) { return ( diff --git a/rsconcept/frontend/src/backend/auth/api.ts b/rsconcept/frontend/src/backend/auth/api.ts index 55ffe30b..a31f4c72 100644 --- a/rsconcept/frontend/src/backend/auth/api.ts +++ b/rsconcept/frontend/src/backend/auth/api.ts @@ -10,8 +10,8 @@ import { information } from '@/utils/labels'; * Represents login data, used to authenticate users. */ export const UserLoginSchema = z.object({ - username: z.string(), - password: z.string() + username: z.string().nonempty('Поле логина обязательно для заполнения'), + password: z.string().nonempty('Поле пароля обязательно для заполнения') }); /** diff --git a/rsconcept/frontend/src/backend/auth/useLogin.tsx b/rsconcept/frontend/src/backend/auth/useLogin.tsx index 6837e626..97a42704 100644 --- a/rsconcept/frontend/src/backend/auth/useLogin.tsx +++ b/rsconcept/frontend/src/backend/auth/useLogin.tsx @@ -1,6 +1,5 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; - -import { libraryApi } from '@/backend/library/api'; +import { AxiosError } from 'axios'; import { authApi, IUserLoginDTO } from './api'; @@ -10,10 +9,11 @@ export const useLogin = () => { mutationKey: ['login'], mutationFn: authApi.login, onSettled: () => client.invalidateQueries({ queryKey: [authApi.baseKey] }), - onSuccess: () => client.removeQueries({ queryKey: [libraryApi.baseKey] }) + onSuccess: () => client.resetQueries() }); return { - login: (data: IUserLoginDTO, onSuccess?: () => void) => mutation.mutate(data, { onSuccess }), + login: (data: IUserLoginDTO, onSuccess?: () => void, onError?: (error: AxiosError) => void) => + mutation.mutate(data, { onSuccess, onError }), isPending: mutation.isPending, error: mutation.error, reset: mutation.reset diff --git a/rsconcept/frontend/src/backend/auth/useLogout.tsx b/rsconcept/frontend/src/backend/auth/useLogout.tsx index 1562e180..dab06168 100644 --- a/rsconcept/frontend/src/backend/auth/useLogout.tsx +++ b/rsconcept/frontend/src/backend/auth/useLogout.tsx @@ -7,8 +7,7 @@ export const useLogout = () => { const mutation = useMutation({ mutationKey: ['logout'], mutationFn: authApi.logout, - onSettled: () => client.invalidateQueries({ queryKey: [authApi.baseKey] }), - onSuccess: () => client.removeQueries() + onSuccess: () => client.resetQueries() }); return { logout: (onSuccess?: () => void) => mutation.mutate(undefined, { onSuccess }) }; }; diff --git a/rsconcept/frontend/src/backend/users/api.ts b/rsconcept/frontend/src/backend/users/api.ts index 41df5ec9..8ceefbad 100644 --- a/rsconcept/frontend/src/backend/users/api.ts +++ b/rsconcept/frontend/src/backend/users/api.ts @@ -2,9 +2,17 @@ import { queryOptions } from '@tanstack/react-query'; import { axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport'; import { DELAYS } from '@/backend/configuration'; -import { IUser, IUserInfo, IUserProfile, IUserSignupData } from '@/models/user'; +import { IUser, IUserInfo, IUserProfile } from '@/models/user'; import { information } from '@/utils/labels'; +/** + * Represents signup data, used to create new users. + */ +export interface IUserSignupData extends Omit { + password: string; + password2: string; +} + /** * Represents user data, intended to update user profile in persistent storage. */ diff --git a/rsconcept/frontend/src/backend/users/useSignup.tsx b/rsconcept/frontend/src/backend/users/useSignup.tsx index 319a736f..32a108f6 100644 --- a/rsconcept/frontend/src/backend/users/useSignup.tsx +++ b/rsconcept/frontend/src/backend/users/useSignup.tsx @@ -1,8 +1,8 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { DataCallback } from '@/backend/apiTransport'; -import { usersApi } from '@/backend/users/api'; -import { IUserProfile, IUserSignupData } from '@/models/user'; +import { IUserSignupData, usersApi } from '@/backend/users/api'; +import { IUserProfile } from '@/models/user'; export const useSignup = () => { const client = useQueryClient(); diff --git a/rsconcept/frontend/src/components/props.d.ts b/rsconcept/frontend/src/components/props.d.ts index 2070fdc1..c6b4e5d8 100644 --- a/rsconcept/frontend/src/components/props.d.ts +++ b/rsconcept/frontend/src/components/props.d.ts @@ -1,5 +1,6 @@ // =========== Module contains interfaces for common UI elements. ========== import React from 'react'; +import { FieldError } from 'react-hook-form'; export namespace CProps { /** @@ -35,6 +36,13 @@ export namespace CProps { hideTitle?: boolean; } + /** + * Represents an object that can have an error message. + */ + export interface ErrorProcessing { + error?: FieldError; + } + /** * Represents `control` component with optional title and configuration options. * diff --git a/rsconcept/frontend/src/components/ui/ErrorField.tsx b/rsconcept/frontend/src/components/ui/ErrorField.tsx new file mode 100644 index 00000000..ed937b2c --- /dev/null +++ b/rsconcept/frontend/src/components/ui/ErrorField.tsx @@ -0,0 +1,17 @@ +import { FieldError, GlobalError } from 'react-hook-form'; + +interface ErrorFieldProps { + error?: FieldError | GlobalError; +} + +/** + * Displays an error message for input field. + */ +function ErrorField({ error }: ErrorFieldProps) { + if (!error) { + return null; + } + return
{error.message}
; +} + +export default ErrorField; diff --git a/rsconcept/frontend/src/components/ui/TextInput.tsx b/rsconcept/frontend/src/components/ui/TextInput.tsx index cfbc0bdb..baf40ccd 100644 --- a/rsconcept/frontend/src/components/ui/TextInput.tsx +++ b/rsconcept/frontend/src/components/ui/TextInput.tsx @@ -2,9 +2,10 @@ import clsx from 'clsx'; import { CProps } from '@/components/props'; +import ErrorField from './ErrorField'; import Label from './Label'; -interface TextInputProps extends CProps.Editor, CProps.Colors, CProps.Input { +interface TextInputProps extends CProps.Editor, CProps.ErrorProcessing, CProps.Colors, CProps.Input { /** Indicates that padding should be minimal. */ dense?: boolean; @@ -32,6 +33,7 @@ function TextInput({ className, colors = 'clr-input', onKeyDown, + error, ...restProps }: TextInputProps) { return ( @@ -63,6 +65,7 @@ function TextInput({ disabled={disabled} {...restProps} /> + ); } diff --git a/rsconcept/frontend/src/dialogs/DlgChangeLocation.tsx b/rsconcept/frontend/src/dialogs/DlgChangeLocation.tsx index 404528fd..5393be49 100644 --- a/rsconcept/frontend/src/dialogs/DlgChangeLocation.tsx +++ b/rsconcept/frontend/src/dialogs/DlgChangeLocation.tsx @@ -48,7 +48,7 @@ function DlgChangeLocation() { diff --git a/rsconcept/frontend/src/dialogs/DlgCloneLibraryItem.tsx b/rsconcept/frontend/src/dialogs/DlgCloneLibraryItem.tsx index 4688119f..6568abd7 100644 --- a/rsconcept/frontend/src/dialogs/DlgCloneLibraryItem.tsx +++ b/rsconcept/frontend/src/dialogs/DlgCloneLibraryItem.tsx @@ -119,11 +119,7 @@ function DlgCloneLibraryItem() {