diff --git a/rsconcept/frontend/src/features/auth/backend/api.ts b/rsconcept/frontend/src/features/auth/backend/api.ts index 5ed613be..319f474d 100644 --- a/rsconcept/frontend/src/features/auth/backend/api.ts +++ b/rsconcept/frontend/src/features/auth/backend/api.ts @@ -5,7 +5,7 @@ import { axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport'; import { DELAYS } from '@/backend/configuration'; import { LibraryItemID } from '@/features/library/models/library'; import { UserID } from '@/features/users/models/user'; -import { errors, information } from '@/utils/labels'; +import { errorMsg, infoMsg } from '@/utils/labels'; /** * Represents CurrentUser information. @@ -21,8 +21,8 @@ export interface ICurrentUser { * Represents login data, used to authenticate users. */ export const schemaUserLogin = z.object({ - username: z.string().nonempty(errors.requiredField), - password: z.string().nonempty(errors.requiredField) + username: z.string().nonempty(errorMsg.requiredField), + password: z.string().nonempty(errorMsg.requiredField) }); /** @@ -35,17 +35,17 @@ export type IUserLoginDTO = z.infer; */ export const schemaChangePassword = z .object({ - old_password: z.string().nonempty(errors.requiredField), - new_password: z.string().nonempty(errors.requiredField), - new_password2: z.string().nonempty(errors.requiredField) + old_password: z.string().nonempty(errorMsg.requiredField), + new_password: z.string().nonempty(errorMsg.requiredField), + new_password2: z.string().nonempty(errorMsg.requiredField) }) .refine(schema => schema.new_password === schema.new_password2, { path: ['new_password2'], - message: errors.passwordsMismatch + message: errorMsg.passwordsMismatch }) .refine(schema => schema.old_password !== schema.new_password, { path: ['new_password'], - message: errors.passwordsSame + message: errorMsg.passwordsSame }); /** @@ -105,7 +105,7 @@ export const authApi = { endpoint: '/users/api/change-password', request: { data: data, - successMessage: information.changesSaved + successMessage: infoMsg.changesSaved } }), requestPasswordReset: (data: IRequestPasswordDTO) => diff --git a/rsconcept/frontend/src/features/library/backend/api.ts b/rsconcept/frontend/src/features/library/backend/api.ts index eb73156e..9dbf9508 100644 --- a/rsconcept/frontend/src/features/library/backend/api.ts +++ b/rsconcept/frontend/src/features/library/backend/api.ts @@ -6,7 +6,7 @@ import { DELAYS } from '@/backend/configuration'; import { ossApi } from '@/features/oss/backend/api'; import { IRSFormDTO, rsformsApi } from '@/features/rsform/backend/api'; import { UserID } from '@/features/users/models/user'; -import { errors, information } from '@/utils/labels'; +import { errorMsg, infoMsg } from '@/utils/labels'; import { AccessPolicy, ILibraryItem, IVersionInfo, LibraryItemID, LibraryItemType, VersionID } from '../models/library'; import { validateLocation } from '../models/libraryAPI'; @@ -25,12 +25,12 @@ export interface IRenameLocationDTO { export const schemaCloneLibraryItem = z.object({ id: z.number(), item_type: z.nativeEnum(LibraryItemType), - title: z.string().nonempty(errors.requiredField), - alias: z.string().nonempty(errors.requiredField), + title: z.string().nonempty(errorMsg.requiredField), + alias: z.string().nonempty(errorMsg.requiredField), comment: z.string(), visible: z.boolean(), read_only: z.boolean(), - location: z.string().refine(data => validateLocation(data), { message: errors.invalidLocation }), + location: z.string().refine(data => validateLocation(data), { message: errorMsg.invalidLocation }), access_policy: z.nativeEnum(AccessPolicy), items: z.array(z.number()).optional() @@ -52,7 +52,7 @@ export const schemaCreateLibraryItem = z comment: z.string(), visible: z.boolean(), read_only: z.boolean(), - location: z.string().refine(data => validateLocation(data), { message: errors.invalidLocation }), + location: z.string().refine(data => validateLocation(data), { message: errorMsg.invalidLocation }), access_policy: z.nativeEnum(AccessPolicy), file: z.instanceof(File).optional(), @@ -60,11 +60,11 @@ export const schemaCreateLibraryItem = z }) .refine(data => !!data.file || !!data.title, { path: ['title'], - message: errors.requiredField + message: errorMsg.requiredField }) .refine(data => !!data.file || !!data.alias, { path: ['alias'], - message: errors.requiredField + message: errorMsg.requiredField }); /** @@ -78,8 +78,8 @@ export type ICreateLibraryItemDTO = z.infer; export const schemaUpdateLibraryItem = z.object({ id: z.number(), item_type: z.nativeEnum(LibraryItemType), - title: z.string().nonempty(errors.requiredField), - alias: z.string().nonempty(errors.requiredField), + title: z.string().nonempty(errorMsg.requiredField), + alias: z.string().nonempty(errorMsg.requiredField), comment: z.string(), visible: z.boolean(), read_only: z.boolean() @@ -117,7 +117,7 @@ export interface IVersionCreatedResponse { */ export const schemaVersionUpdate = z.object({ id: z.number(), - version: z.string().nonempty(errors.requiredField), + version: z.string().nonempty(errorMsg.requiredField), description: z.string() }); @@ -161,7 +161,7 @@ export const libraryApi = { endpoint: !data.file ? '/api/library' : '/api/rsforms/create-detailed', request: { data: data, - successMessage: information.newLibraryItem + successMessage: infoMsg.newLibraryItem }, options: !data.file ? undefined @@ -176,7 +176,7 @@ export const libraryApi = { endpoint: `/api/library/${data.id}`, request: { data: data, - successMessage: information.changesSaved + successMessage: infoMsg.changesSaved } }), setOwner: ({ itemID, owner }: { itemID: LibraryItemID; owner: UserID }) => @@ -184,7 +184,7 @@ export const libraryApi = { endpoint: `/api/library/${itemID}/set-owner`, request: { data: { user: owner }, - successMessage: information.changesSaved + successMessage: infoMsg.changesSaved } }), setLocation: ({ itemID, location }: { itemID: LibraryItemID; location: string }) => @@ -192,7 +192,7 @@ export const libraryApi = { endpoint: `/api/library/${itemID}/set-location`, request: { data: { location: location }, - successMessage: information.moveComplete + successMessage: infoMsg.moveComplete } }), setAccessPolicy: ({ itemID, policy }: { itemID: LibraryItemID; policy: AccessPolicy }) => @@ -200,7 +200,7 @@ export const libraryApi = { endpoint: `/api/library/${itemID}/set-access-policy`, request: { data: { access_policy: policy }, - successMessage: information.changesSaved + successMessage: infoMsg.changesSaved } }), setEditors: ({ itemID, editors }: { itemID: LibraryItemID; editors: UserID[] }) => @@ -208,7 +208,7 @@ export const libraryApi = { endpoint: `/api/library/${itemID}/set-editors`, request: { data: { users: editors }, - successMessage: information.changesSaved + successMessage: infoMsg.changesSaved } }), @@ -216,7 +216,7 @@ export const libraryApi = { axiosDelete({ endpoint: `/api/library/${target}`, request: { - successMessage: information.itemDestroyed + successMessage: infoMsg.itemDestroyed } }), cloneItem: (data: ICloneLibraryItemDTO) => @@ -224,7 +224,7 @@ export const libraryApi = { endpoint: `/api/library/${data.id}/clone`, request: { data: data, - successMessage: newSchema => information.cloneComplete(newSchema.alias) + successMessage: newSchema => infoMsg.cloneComplete(newSchema.alias) } }), renameLocation: (data: IRenameLocationDTO) => @@ -232,7 +232,7 @@ export const libraryApi = { endpoint: '/api/library/rename-location', request: { data: data, - successMessage: information.locationRenamed + successMessage: infoMsg.locationRenamed } }), @@ -241,14 +241,14 @@ export const libraryApi = { endpoint: `/api/library/${itemID}/create-version`, request: { data: data, - successMessage: information.newVersion(data.version) + successMessage: infoMsg.newVersion(data.version) } }), versionRestore: ({ versionID }: { versionID: VersionID }) => axiosPatch({ endpoint: `/api/versions/${versionID}/restore`, request: { - successMessage: information.versionRestored + successMessage: infoMsg.versionRestored } }), versionUpdate: (data: IVersionUpdateDTO) => @@ -256,14 +256,14 @@ export const libraryApi = { endpoint: `/api/versions/${data.id}`, request: { data: data, - successMessage: information.changesSaved + successMessage: infoMsg.changesSaved } }), versionDelete: (data: { itemID: LibraryItemID; versionID: VersionID }) => axiosDelete({ endpoint: `/api/versions/${data.versionID}`, request: { - successMessage: information.versionDestroyed + successMessage: infoMsg.versionDestroyed } }) }; diff --git a/rsconcept/frontend/src/features/library/dialogs/DlgChangeLocation.tsx b/rsconcept/frontend/src/features/library/dialogs/DlgChangeLocation.tsx index 1b0c69cb..0365c3c8 100644 --- a/rsconcept/frontend/src/features/library/dialogs/DlgChangeLocation.tsx +++ b/rsconcept/frontend/src/features/library/dialogs/DlgChangeLocation.tsx @@ -1,19 +1,28 @@ 'use client'; +import { zodResolver } from '@hookform/resolvers/zod'; import clsx from 'clsx'; -import { useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { z } from 'zod'; import { Label, TextArea } from '@/components/Input'; import { ModalForm } from '@/components/Modal'; import { useAuthSuspense } from '@/features/auth/backend/useAuth'; import { useDialogsStore } from '@/stores/dialogs'; import { limits } from '@/utils/constants'; +import { errorMsg } from '@/utils/labels'; import SelectLocationContext from '../components/SelectLocationContext'; import SelectLocationHead from '../components/SelectLocationHead'; import { LocationHead } from '../models/library'; import { combineLocation, validateLocation } from '../models/libraryAPI'; +const schemaLocation = z.object({ + location: z.string().refine(data => validateLocation(data), { message: errorMsg.invalidLocation }) +}); + +type ILocationType = z.infer; + export interface DlgChangeLocationProps { initial: string; onChangeLocation: (newLocation: string) => void; @@ -22,20 +31,21 @@ export interface DlgChangeLocationProps { function DlgChangeLocation() { const { initial, onChangeLocation } = useDialogsStore(state => state.props as DlgChangeLocationProps); const { user } = useAuthSuspense(); - const [head, setHead] = useState(initial.substring(0, 2) as LocationHead); - const [body, setBody] = useState(initial.substring(3)); - const location = combineLocation(head, body); - const isValid = initial !== location && validateLocation(location); + const { + handleSubmit, + control, + formState: { errors, isValid, isDirty } + } = useForm({ + resolver: zodResolver(schemaLocation), + defaultValues: { + location: initial + }, + mode: 'onChange' + }); - function handleSelectLocation(newValue: string) { - setHead(newValue.substring(0, 2) as LocationHead); - setBody(newValue.length > 3 ? newValue.substring(3) : ''); - } - - function handleSubmit() { - onChangeLocation(location); - return true; + function onSubmit(data: ILocationType) { + onChangeLocation(data.location); } return ( @@ -44,20 +54,45 @@ function DlgChangeLocation() { header='Изменение расположения' submitText='Переместить' submitInvalidTooltip={`Допустимы буквы, цифры, подчерк, пробел и "!". Сегмент пути не может начинаться и заканчиваться пробелом. Общая длина (с корнем) не должна превышать ${limits.location_len}`} - canSubmit={isValid} - onSubmit={handleSubmit} + canSubmit={isValid && isDirty} + onSubmit={event => void handleSubmit(onSubmit)(event)} className={clsx('w-[35rem]', 'pb-3 px-6 flex gap-3 h-[9rem]')} >
- -