diff --git a/rsconcept/frontend/src/features/library/pages/CreateItemPage/FormCreateItem.tsx b/rsconcept/frontend/src/features/library/pages/CreateItemPage/FormCreateItem.tsx index 263fef4e..5dbc07d6 100644 --- a/rsconcept/frontend/src/features/library/pages/CreateItemPage/FormCreateItem.tsx +++ b/rsconcept/frontend/src/features/library/pages/CreateItemPage/FormCreateItem.tsx @@ -15,7 +15,7 @@ import { Label, TextArea, TextInput } from '@/components/Input'; import { useAuthSuspense } from '@/features/auth/backend/useAuth'; import { EXTEOR_TRS_FILE } from '@/utils/constants'; -import { schemaCreateLibraryItem, ICreateLibraryItemDTO } from '../../backend/api'; +import { ICreateLibraryItemDTO, schemaCreateLibraryItem } from '../../backend/api'; import { useCreateItem } from '../../backend/useCreateItem'; import SelectAccessPolicy from '../../components/SelectAccessPolicy'; import SelectItemType from '../../components/SelectItemType'; diff --git a/rsconcept/frontend/src/features/oss/backend/api.ts b/rsconcept/frontend/src/features/oss/backend/api.ts index ddde17ac..421bb074 100644 --- a/rsconcept/frontend/src/features/oss/backend/api.ts +++ b/rsconcept/frontend/src/features/oss/backend/api.ts @@ -1,7 +1,7 @@ import { queryOptions } from '@tanstack/react-query'; import { z } from 'zod'; -import { axiosDelete, axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport'; +import { axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport'; import { DELAYS } from '@/backend/configuration'; import { ILibraryItem, ILibraryItemData, LibraryItemID } from '@/features/library/models/library'; import { @@ -46,20 +46,25 @@ export type IOperationPosition = z.infer; /** * Represents {@link IOperation} data, used in creation process. */ -export interface IOperationCreateDTO { - positions: IOperationPosition[]; - item_data: { - alias: string; - operation_type: OperationType; - title: string; - comment: string; - position_x: number; - position_y: number; - result: LibraryItemID | null; - }; - arguments: OperationID[] | undefined; - create_schema: boolean; -} +export const schemaOperationCreate = z.object({ + positions: z.array(schemaOperationPosition), + item_data: z.object({ + alias: z.string().nonempty(), + operation_type: z.nativeEnum(OperationType), + title: z.string(), + comment: z.string(), + position_x: z.number(), + position_y: z.number(), + result: z.number().nullable() + }), + arguments: z.array(z.number()), + create_schema: z.boolean() +}); + +/** + * Represents {@link IOperation} data, used in creation process. + */ +export type IOperationCreateDTO = z.infer; /** * Represents data response when creating {@link IOperation}. @@ -183,7 +188,7 @@ export const ossApi = { } }), operationDelete: ({ itemID, data }: { itemID: LibraryItemID; data: IOperationDeleteDTO }) => - axiosDelete({ + axiosPatch({ endpoint: `/api/oss/${itemID}/delete-operation`, request: { data: data, diff --git a/rsconcept/frontend/src/features/oss/backend/useOperationCreate.tsx b/rsconcept/frontend/src/features/oss/backend/useOperationCreate.tsx index 514f08b9..88458072 100644 --- a/rsconcept/frontend/src/features/oss/backend/useOperationCreate.tsx +++ b/rsconcept/frontend/src/features/oss/backend/useOperationCreate.tsx @@ -1,10 +1,9 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { DataCallback } from '@/backend/apiTransport'; import { useUpdateTimestamp } from '@/features/library/backend/useUpdateTimestamp'; import { LibraryItemID } from '@/features/library/models/library'; -import { IOperationCreateDTO, IOperationDTO, ossApi } from './api'; +import { IOperationCreateDTO, ossApi } from './api'; export const useOperationCreate = () => { const client = useQueryClient(); @@ -12,18 +11,12 @@ export const useOperationCreate = () => { const mutation = useMutation({ mutationKey: [ossApi.baseKey, 'operation-create'], mutationFn: ossApi.operationCreate, - onSuccess: data => { - client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.oss.id }).queryKey, data.oss); - updateTimestamp(data.oss.id); + onSuccess: response => { + client.setQueryData(ossApi.getOssQueryOptions({ itemID: response.oss.id }).queryKey, response.oss); + updateTimestamp(response.oss.id); } }); return { - operationCreate: ( - data: { - itemID: LibraryItemID; // - data: IOperationCreateDTO; - }, - onSuccess?: DataCallback - ) => mutation.mutate(data, { onSuccess: response => onSuccess?.(response.new_operation) }) + operationCreate: (data: { itemID: LibraryItemID; data: IOperationCreateDTO }) => mutation.mutateAsync(data) }; }; diff --git a/rsconcept/frontend/src/features/oss/components/PickMultiOperation.tsx b/rsconcept/frontend/src/features/oss/components/PickMultiOperation.tsx index 7ee32c75..8c44b4c8 100644 --- a/rsconcept/frontend/src/features/oss/components/PickMultiOperation.tsx +++ b/rsconcept/frontend/src/features/oss/components/PickMultiOperation.tsx @@ -14,8 +14,7 @@ import SelectOperation from './SelectOperation'; interface PickMultiOperationProps extends CProps.Styling { value: OperationID[]; - onChange: React.Dispatch>; - + onChange: (newValue: OperationID[]) => void; items: IOperation[]; rows?: number; } @@ -28,13 +27,13 @@ function PickMultiOperation({ rows, items, value, onChange, className, ...restPr const [lastSelected, setLastSelected] = useState(undefined); function handleDelete(operation: OperationID) { - onChange(prev => prev.filter(item => item !== operation)); + onChange(value.filter(item => item !== operation)); } function handleSelect(operation?: IOperation) { if (operation) { setLastSelected(operation); - onChange(prev => [...prev, operation.id]); + onChange([...value, operation.id]); setTimeout(() => setLastSelected(undefined), 1000); } } @@ -42,24 +41,20 @@ function PickMultiOperation({ rows, items, value, onChange, className, ...restPr function handleMoveUp(operation: OperationID) { const index = value.indexOf(operation); if (index > 0) { - onChange(prev => { - const newSelected = [...prev]; - newSelected[index] = newSelected[index - 1]; - newSelected[index - 1] = operation; - return newSelected; - }); + const newSelected = [...value]; + newSelected[index] = newSelected[index - 1]; + newSelected[index - 1] = operation; + onChange(newSelected); } } function handleMoveDown(operation: OperationID) { const index = value.indexOf(operation); if (index < value.length - 1) { - onChange(prev => { - const newSelected = [...prev]; - newSelected[index] = newSelected[index + 1]; - newSelected[index + 1] = operation; - return newSelected; - }); + const newSelected = [...value]; + newSelected[index] = newSelected[index + 1]; + newSelected[index + 1] = operation; + onChange(newSelected); } } diff --git a/rsconcept/frontend/src/features/oss/dialogs/DlgCreateOperation/DlgCreateOperation.tsx b/rsconcept/frontend/src/features/oss/dialogs/DlgCreateOperation/DlgCreateOperation.tsx index 7cf6ef97..2fa1eaf2 100644 --- a/rsconcept/frontend/src/features/oss/dialogs/DlgCreateOperation/DlgCreateOperation.tsx +++ b/rsconcept/frontend/src/features/oss/dialogs/DlgCreateOperation/DlgCreateOperation.tsx @@ -1,25 +1,30 @@ 'use client'; +import { zodResolver } from '@hookform/resolvers/zod'; import clsx from 'clsx'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; +import { FormProvider, useForm, useWatch } from 'react-hook-form'; import { ModalForm } from '@/components/Modal'; import { TabLabel, TabList, TabPanel, Tabs } from '@/components/Tabs'; import { HelpTopic } from '@/features/help/models/helpTopic'; -import { useLibrary } from '@/features/library/backend/useLibrary'; -import { LibraryItemID } from '@/features/library/models/library'; import { useDialogsStore } from '@/stores/dialogs'; import { describeOperationType, labelOperationType } from '@/utils/labels'; -import { IOperationCreateDTO } from '../../backend/api'; +import { IOperationCreateDTO, IOperationPosition, schemaOperationCreate } from '../../backend/api'; +import { useOperationCreate } from '../../backend/useOperationCreate'; import { IOperationSchema, OperationID, OperationType } from '../../models/oss'; +import { calculateInsertPosition } from '../../models/ossAPI'; import TabInputOperation from './TabInputOperation'; import TabSynthesisOperation from './TabSynthesisOperation'; export interface DlgCreateOperationProps { oss: IOperationSchema; - onCreate: (data: IOperationCreateDTO) => void; + positions: IOperationPosition[]; initialInputs: OperationID[]; + defaultX: number; + defaultY: number; + onCreate?: (newID: OperationID) => void; } export enum TabID { @@ -28,70 +33,57 @@ export enum TabID { } function DlgCreateOperation() { - const { items: libraryItems } = useLibrary(); + const { operationCreate } = useOperationCreate(); - const { oss, onCreate, initialInputs } = useDialogsStore(state => state.props as DlgCreateOperationProps); - const [activeTab, setActiveTab] = useState(initialInputs.length > 0 ? TabID.SYNTHESIS : TabID.INPUT); + const { oss, positions, initialInputs, onCreate, defaultX, defaultY } = useDialogsStore( + state => state.props as DlgCreateOperationProps + ); - const [alias, setAlias] = useState(''); - const [title, setTitle] = useState(''); - const [comment, setComment] = useState(''); - const [inputs, setInputs] = useState(initialInputs); - const [attachedID, setAttachedID] = useState(undefined); - const [createSchema, setCreateSchema] = useState(false); - - const isValid = (() => { - if (alias === '') { - return false; - } - if (activeTab === TabID.SYNTHESIS && inputs.length === 0) { - return false; - } - if (activeTab === TabID.INPUT && !attachedID) { - if (oss.items.some(operation => operation.alias === alias)) { - return false; - } - } - return true; - })(); - - useEffect(() => { - if (attachedID) { - const schema = libraryItems.find(value => value.id === attachedID); - if (schema) { - setAlias(schema.alias); - setTitle(schema.title); - setComment(schema.comment); - } - } - }, [attachedID, libraryItems]); - - const handleSubmit = () => { - onCreate({ + const methods = useForm({ + resolver: zodResolver(schemaOperationCreate), + defaultValues: { item_data: { - position_x: 0, - position_y: 0, - alias: alias, - title: title, - comment: comment, - operation_type: activeTab === TabID.INPUT ? OperationType.INPUT : OperationType.SYNTHESIS, - result: activeTab === TabID.INPUT ? attachedID ?? null : null + operation_type: initialInputs.length === 0 ? OperationType.INPUT : OperationType.SYNTHESIS, + result: null, + position_x: defaultX, + position_y: defaultY, + alias: '', + title: '', + comment: '' }, - positions: [], - arguments: activeTab === TabID.INPUT ? undefined : inputs.length > 0 ? inputs : undefined, - create_schema: createSchema + arguments: initialInputs, + create_schema: false, + positions: positions + }, + mode: 'onChange' + }); + const alias = useWatch({ control: methods.control, name: 'item_data.alias' }); + const [activeTab, setActiveTab] = useState(initialInputs.length === 0 ? TabID.INPUT : TabID.SYNTHESIS); + const isValid = !!alias && !oss.items.some(operation => operation.alias === alias); + + function onSubmit(data: IOperationCreateDTO) { + const target = calculateInsertPosition(oss, data.item_data.operation_type, data.arguments, positions, { + x: defaultX, + y: defaultY }); - return true; - }; + data.item_data.position_x = target.x; + data.item_data.position_y = target.y; + operationCreate({ itemID: oss.id, data: data }) + .then(response => onCreate?.(response.new_operation.id)) + .catch(console.error); + } function handleSelectTab(newTab: TabID, last: TabID) { if (last === newTab) { return; } if (newTab === TabID.INPUT) { - setAttachedID(undefined); + methods.setValue('item_data.operation_type', OperationType.INPUT); + methods.setValue('item_data.result', null); + methods.setValue('arguments', []); } else { - setInputs(initialInputs); + methods.setValue('item_data.operation_type', OperationType.SYNTHESIS); + methods.setValue('arguments', initialInputs); } setActiveTab(newTab); } @@ -101,7 +93,7 @@ function DlgCreateOperation() { header='Создание операции' submitText='Создать' canSubmit={isValid} - onSubmit={handleSubmit} + onSubmit={event => void methods.handleSubmit(onSubmit)(event)} className='w-[40rem] px-6 h-[32rem]' helpTopic={HelpTopic.CC_OSS} > @@ -123,36 +115,15 @@ function DlgCreateOperation() { label={labelOperationType(OperationType.SYNTHESIS)} /> + + + + - - - - - - - + + + + ); diff --git a/rsconcept/frontend/src/features/oss/dialogs/DlgCreateOperation/TabInputOperation.tsx b/rsconcept/frontend/src/features/oss/dialogs/DlgCreateOperation/TabInputOperation.tsx index deec9d41..7e6acde4 100644 --- a/rsconcept/frontend/src/features/oss/dialogs/DlgCreateOperation/TabInputOperation.tsx +++ b/rsconcept/frontend/src/features/oss/dialogs/DlgCreateOperation/TabInputOperation.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect } from 'react'; +import { Controller, useFormContext, useWatch } from 'react-hook-form'; import { MiniButton } from '@/components/Control'; import { IconReset } from '@/components/Icons'; @@ -11,73 +11,70 @@ import { IOperationSchema } from '@/features/oss/models/oss'; import { sortItemsForOSS } from '@/features/oss/models/ossAPI'; import PickSchema from '@/features/rsform/components/PickSchema'; +import { IOperationCreateDTO } from '../../backend/api'; + interface TabInputOperationProps { oss: IOperationSchema; - alias: string; - onChangeAlias: (newValue: string) => void; - title: string; - onChangeTitle: (newValue: string) => void; - comment: string; - onChangeComment: (newValue: string) => void; - attachedID: LibraryItemID | undefined; - onChangeAttachedID: (newValue: LibraryItemID | undefined) => void; - createSchema: boolean; - onChangeCreateSchema: (newValue: boolean) => void; } -function TabInputOperation({ - oss, - alias, - onChangeAlias, - title, - onChangeTitle, - comment, - onChangeComment, - attachedID, - onChangeAttachedID, - createSchema, - onChangeCreateSchema -}: TabInputOperationProps) { +function TabInputOperation({ oss }: TabInputOperationProps) { const { items: libraryItems } = useLibrary(); const sortedItems = sortItemsForOSS(oss, libraryItems); + const { + register, + control, + setValue, + formState: { errors } + } = useFormContext(); + const createSchema = useWatch({ control, name: 'create_schema' }); + function baseFilter(item: ILibraryItem) { return !oss.schemas.includes(item.id); } - useEffect(() => { - if (createSchema) { - onChangeAttachedID(undefined); + function handleChangeCreateSchema(value: boolean) { + if (value) { + setValue('item_data.result', null); } - }, [createSchema, onChangeAttachedID]); + setValue('create_schema', value); + } + + function handleSetInput(value: LibraryItemID) { + const schema = libraryItems.find(item => item.id === value); + if (!schema) { + return; + } + setValue('item_data.result', value); + setValue('create_schema', false); + setValue('item_data.alias', schema.alias); + setValue('item_data.title', schema.title); + setValue('item_data.comment', schema.comment); + } return (
onChangeTitle(event.target.value)} - disabled={attachedID !== undefined} + {...register('item_data.title')} + error={errors.item_data?.title} />
onChangeAlias(event.target.value)} - disabled={attachedID !== undefined} + {...register('item_data.alias')} + error={errors.item_data?.alias} />