diff --git a/rsconcept/frontend/src/backend/auth/useResetPassword.tsx b/rsconcept/frontend/src/backend/auth/useResetPassword.tsx index fbb82e74..e515557f 100644 --- a/rsconcept/frontend/src/backend/auth/useResetPassword.tsx +++ b/rsconcept/frontend/src/backend/auth/useResetPassword.tsx @@ -21,6 +21,7 @@ export const useResetPassword = () => { onSuccess?: () => void ) => resetMutation.mutate(data, { onSuccess }), isPending: resetMutation.isPending || validateMutation.isPending, - error: resetMutation.error ?? validateMutation.error + error: resetMutation.error ?? validateMutation.error, + reset: resetMutation.reset }; }; diff --git a/rsconcept/frontend/src/backend/library/api.ts b/rsconcept/frontend/src/backend/library/api.ts index 73212e3c..f85c73d3 100644 --- a/rsconcept/frontend/src/backend/library/api.ts +++ b/rsconcept/frontend/src/backend/library/api.ts @@ -1,4 +1,5 @@ import { queryOptions } from '@tanstack/react-query'; +import { z } from 'zod'; import { axiosDelete, axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport'; import { DELAYS } from '@/backend/configuration'; @@ -13,9 +14,10 @@ import { LibraryItemType, VersionID } from '@/models/library'; +import { validateLocation } from '@/models/libraryAPI'; import { ConstituentaID } from '@/models/rsform'; import { UserID } from '@/models/user'; -import { information } from '@/utils/labels'; +import { errors, information } from '@/utils/labels'; /** * Represents update data for renaming Location. @@ -28,22 +30,49 @@ export interface IRenameLocationDTO { /** * Represents data, used for cloning {@link IRSForm}. */ -export interface IRSFormCloneDTO extends Omit { +export interface IRCloneLibraryItemDTO extends Omit { items?: ConstituentaID[]; } /** * Represents data, used for creating {@link IRSForm}. */ -export interface ILibraryCreateDTO extends Omit { - file?: File; - fileName?: string; -} +export const CreateLibraryItemSchema = z + .object({ + item_type: z.nativeEnum(LibraryItemType), + title: z.string().optional(), + alias: z.string().optional(), + comment: z.string(), + visible: z.boolean(), + read_only: z.boolean(), + location: z.string(), + access_policy: z.nativeEnum(AccessPolicy), + + file: z.instanceof(File).optional(), + fileName: z.string().optional() + }) + .refine(data => validateLocation(data.location), { + path: ['location'], + message: errors.invalidLocation + }) + .refine(data => !!data.file || !!data.title, { + path: ['title'], + message: errors.requiredField + }) + .refine(data => !!data.file || !!data.alias, { + path: ['alias'], + message: errors.requiredField + }); + +/** + * Represents data, used for creating {@link IRSForm}. + */ +export type ICreateLibraryItemDTO = z.infer; /** * Represents update data for editing {@link ILibraryItem}. */ -export interface ILibraryUpdateDTO +export interface IUpdateLibraryItemDTO extends Omit {} /** @@ -93,8 +122,8 @@ export const libraryApi = { }) }), - createItem: (data: ILibraryCreateDTO) => - axiosPost({ + createItem: (data: ICreateLibraryItemDTO) => + axiosPost({ endpoint: !data.file ? '/api/library' : '/api/rsforms/create-detailed', request: { data: data, @@ -108,8 +137,8 @@ export const libraryApi = { } } }), - updateItem: (data: ILibraryUpdateDTO) => - axiosPatch({ + updateItem: (data: IUpdateLibraryItemDTO) => + axiosPatch({ endpoint: `/api/library/${data.id}`, request: { data: data, @@ -156,8 +185,8 @@ export const libraryApi = { successMessage: information.itemDestroyed } }), - cloneItem: (data: IRSFormCloneDTO) => - axiosPost({ + cloneItem: (data: IRCloneLibraryItemDTO) => + axiosPost({ endpoint: `/api/library/${data.id}/clone`, request: { data: data, diff --git a/rsconcept/frontend/src/backend/library/useCloneItem.tsx b/rsconcept/frontend/src/backend/library/useCloneItem.tsx index dc9b0910..bc90698b 100644 --- a/rsconcept/frontend/src/backend/library/useCloneItem.tsx +++ b/rsconcept/frontend/src/backend/library/useCloneItem.tsx @@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { DataCallback } from '@/backend/apiTransport'; import { IRSFormDTO } from '../rsform/api'; -import { IRSFormCloneDTO, libraryApi } from './api'; +import { IRCloneLibraryItemDTO, libraryApi } from './api'; export const useCloneItem = () => { const client = useQueryClient(); @@ -14,7 +14,7 @@ export const useCloneItem = () => { }); return { cloneItem: ( - data: IRSFormCloneDTO, // + data: IRCloneLibraryItemDTO, // onSuccess?: DataCallback ) => mutation.mutate(data, { onSuccess }) }; diff --git a/rsconcept/frontend/src/backend/library/useCreateItem.tsx b/rsconcept/frontend/src/backend/library/useCreateItem.tsx index a61c2a88..e7f8d3df 100644 --- a/rsconcept/frontend/src/backend/library/useCreateItem.tsx +++ b/rsconcept/frontend/src/backend/library/useCreateItem.tsx @@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { DataCallback } from '@/backend/apiTransport'; import { ILibraryItem } from '@/models/library'; -import { ILibraryCreateDTO, libraryApi } from './api'; +import { ICreateLibraryItemDTO, libraryApi } from './api'; export const useCreateItem = () => { const client = useQueryClient(); @@ -14,7 +14,7 @@ export const useCreateItem = () => { }); return { createItem: ( - data: ILibraryCreateDTO, // + data: ICreateLibraryItemDTO, // onSuccess?: DataCallback ) => mutation.mutate(data, { onSuccess }), isPending: mutation.isPending, diff --git a/rsconcept/frontend/src/backend/library/useUpdateItem.tsx b/rsconcept/frontend/src/backend/library/useUpdateItem.tsx index 4f2f689b..4e0881c0 100644 --- a/rsconcept/frontend/src/backend/library/useUpdateItem.tsx +++ b/rsconcept/frontend/src/backend/library/useUpdateItem.tsx @@ -4,7 +4,7 @@ import { IOperationSchemaDTO, ossApi } from '@/backend/oss/api'; import { ILibraryItem, LibraryItemType } from '@/models/library'; import { IRSFormDTO } from '../rsform/api'; -import { ILibraryUpdateDTO, libraryApi } from './api'; +import { IUpdateLibraryItemDTO, libraryApi } from './api'; export const useUpdateItem = () => { const client = useQueryClient(); @@ -32,6 +32,6 @@ export const useUpdateItem = () => { } }); return { - updateItem: (data: ILibraryUpdateDTO) => mutation.mutate(data) + updateItem: (data: IUpdateLibraryItemDTO) => mutation.mutate(data) }; }; diff --git a/rsconcept/frontend/src/components/select/SelectItemType.tsx b/rsconcept/frontend/src/components/select/SelectItemType.tsx index 2b643e77..b8cd0ae4 100644 --- a/rsconcept/frontend/src/components/select/SelectItemType.tsx +++ b/rsconcept/frontend/src/components/select/SelectItemType.tsx @@ -1,7 +1,5 @@ 'use client'; -import { useCallback } from 'react'; - import { ItemTypeIcon } from '@/components/DomainIcons'; import { CProps } from '@/components/props'; import Dropdown from '@/components/ui/Dropdown'; @@ -22,15 +20,12 @@ interface SelectItemTypeProps extends CProps.Styling { function SelectItemType({ value, disabled, stretchLeft, onChange, ...restProps }: SelectItemTypeProps) { const menu = useDropdown(); - const handleChange = useCallback( - (newValue: LibraryItemType) => { - menu.hide(); - if (newValue !== value) { - onChange(newValue); - } - }, - [menu, value, onChange] - ); + function handleChange(newValue: LibraryItemType) { + menu.hide(); + if (newValue !== value) { + onChange(newValue); + } + } return (
diff --git a/rsconcept/frontend/src/components/ui/TextArea.tsx b/rsconcept/frontend/src/components/ui/TextArea.tsx index b5e77ee0..d30c0b60 100644 --- a/rsconcept/frontend/src/components/ui/TextArea.tsx +++ b/rsconcept/frontend/src/components/ui/TextArea.tsx @@ -2,9 +2,10 @@ import clsx from 'clsx'; import { CProps } from '@/components/props'; +import ErrorField from './ErrorField'; import Label from './Label'; -export interface TextAreaProps extends CProps.Editor, CProps.Colors, CProps.TextArea { +export interface TextAreaProps extends CProps.Editor, CProps.ErrorProcessing, CProps.Colors, CProps.TextArea { /** Indicates that padding should be minimal. */ dense?: boolean; @@ -29,6 +30,7 @@ function TextArea({ noResize, className, fitContent, + error, colors = 'clr-input', ...restProps }: TextAreaProps) { @@ -37,7 +39,7 @@ function TextArea({ className={clsx( 'w-full', { - 'flex flex-col gap-2': !dense, + 'flex flex-col': !dense, 'flex flex-grow items-center gap-3': dense }, dense && className @@ -55,6 +57,7 @@ function TextArea({ 'resize-none': noResize, 'border': !noBorder, 'flex-grow max-w-full': dense, + 'mt-2': !dense, 'clr-outline': !noOutline }, colors, @@ -64,6 +67,7 @@ function TextArea({ required={required} {...restProps} /> +
); } diff --git a/rsconcept/frontend/src/components/ui/TextInput.tsx b/rsconcept/frontend/src/components/ui/TextInput.tsx index b712f083..a07f8c8d 100644 --- a/rsconcept/frontend/src/components/ui/TextInput.tsx +++ b/rsconcept/frontend/src/components/ui/TextInput.tsx @@ -50,11 +50,12 @@ function TextInput({ state.location); const setSearchLocation = useLibrarySearchStore(state => state.setLocation); - const [itemType, setItemType] = useState(LibraryItemType.RSFORM); - const [title, setTitle] = useState(''); - const [alias, setAlias] = useState(''); - const [comment, setComment] = useState(''); - const [visible, setVisible] = useState(true); - const [policy, setPolicy] = useState(AccessPolicy.PUBLIC); - - const [head, setHead] = useState(LocationHead.USER); - const [body, setBody] = useState(''); - - const location = combineLocation(head, body); - const isValid = validateLocation(location); - - const [fileName, setFileName] = useState(''); - const [file, setFile] = useState(); + const { + register, + handleSubmit, + clearErrors, + setValue, + control, + formState: { errors } + } = useForm({ + resolver: zodResolver(CreateLibraryItemSchema), + defaultValues: { + item_type: LibraryItemType.RSFORM, + access_policy: AccessPolicy.PUBLIC, + visible: true, + read_only: false, + location: !!searchLocation ? searchLocation : LocationHead.USER + } + }); + const itemType = useWatch({ control, name: 'item_type' }); + const file = useWatch({ control, name: 'file' }); const inputRef = useRef(null); - useEffect(() => { - reset(); - }, [title, alias, reset]); + function resetErrors() { + clearServerError(); + clearErrors(); + } function handleCancel() { if (router.canBack()) { @@ -64,26 +71,28 @@ function FormCreateItem() { } } - function handleSubmit(event: React.FormEvent) { - event.preventDefault(); - if (isPending) { - return; + function handleFileChange(event: React.ChangeEvent) { + if (event.target.files && event.target.files.length > 0) { + setValue('file', event.target.files[0]); + setValue('fileName', event.target.files[0].name); + } else { + setValue('file', undefined); + setValue('fileName', ''); } - const data: ILibraryCreateDTO = { - item_type: itemType, - title: title, - alias: alias, - comment: comment, - read_only: false, - visible: visible, - access_policy: policy, - location: location, - file: file, - fileName: file?.name - }; - setSearchLocation(location); + } + + function handleItemTypeChange(value: LibraryItemType) { + if (value !== LibraryItemType.RSFORM) { + setValue('file', undefined); + setValue('fileName', ''); + } + setValue('item_type', value); + } + + function onSubmit(data: ICreateLibraryItemDTO) { createItem(data, newItem => { - if (itemType == LibraryItemType.RSFORM) { + setSearchLocation(data.location); + if (newItem.item_type == LibraryItemType.RSFORM) { router.push(urls.schema(newItem.id)); } else { router.push(urls.oss(newItem.id)); @@ -91,50 +100,28 @@ function FormCreateItem() { }); } - function handleFileChange(event: React.ChangeEvent) { - if (event.target.files && event.target.files.length > 0) { - setFileName(event.target.files[0].name); - setFile(event.target.files[0]); - } else { - setFileName(''); - setFile(undefined); - } - } - - const handleSelectLocation = useCallback((newValue: string) => { - setHead(newValue.substring(0, 2) as LocationHead); - setBody(newValue.length > 3 ? newValue.substring(3) : ''); - }, []); - - useEffect(() => { - if (!searchLocation) { - return; - } - handleSelectLocation(searchLocation); - }, [searchLocation, handleSelectLocation]); - - useEffect(() => { - if (itemType !== LibraryItemType.RSFORM) { - setFile(undefined); - setFileName(''); - } - }, [itemType]); - return (
void handleSubmit(onSubmit)(event)} + onChange={resetErrors} >

{itemType == LibraryItemType.RSFORM ? ( - ( + + )} /> - {fileName ?