diff --git a/rsconcept/frontend/src/backend/rsform/api.ts b/rsconcept/frontend/src/backend/rsform/api.ts index 171c466c..001a9929 100644 --- a/rsconcept/frontend/src/backend/rsform/api.ts +++ b/rsconcept/frontend/src/backend/rsform/api.ts @@ -1,4 +1,5 @@ import { queryOptions } from '@tanstack/react-query'; +import { z } from 'zod'; import { axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport'; import { DELAYS } from '@/backend/configuration'; @@ -74,16 +75,21 @@ export interface ICstCreatedResponse { /** * Represents data, used in updating persistent attributes in {@link IConstituenta}. */ -export interface ICstUpdateDTO { - target: ConstituentaID; - item_data: { - convention?: string; - definition_formal?: string; - definition_raw?: string; - term_raw?: string; - term_forms?: TermForm[]; - }; -} +export const CstUpdateSchema = z.object({ + target: z.number(), + item_data: z.object({ + convention: z.string().optional(), + definition_formal: z.string().optional(), + definition_raw: z.string().optional(), + term_raw: z.string().optional(), + term_forms: z.array(z.object({ text: z.string(), tags: z.string() })).optional() + }) +}); + +/** + * Represents data, used in updating persistent attributes in {@link IConstituenta}. + */ +export type ICstUpdateDTO = z.infer; /** * Represents data, used in renaming {@link IConstituenta}. diff --git a/rsconcept/frontend/src/backend/rsform/useCheckConstituenta.tsx b/rsconcept/frontend/src/backend/rsform/useCheckConstituenta.tsx index 2deed079..c8dd1b5d 100644 --- a/rsconcept/frontend/src/backend/rsform/useCheckConstituenta.tsx +++ b/rsconcept/frontend/src/backend/rsform/useCheckConstituenta.tsx @@ -8,7 +8,7 @@ import { ICheckConstituentaDTO, rsformsApi } from './api'; export const useCheckConstituenta = () => { const mutation = useMutation({ - mutationKey: [rsformsApi.baseKey, 'check-constituenta'], + mutationKey: ['actions', 'check-constituenta'], mutationFn: rsformsApi.checkConstituenta }); return { diff --git a/rsconcept/frontend/src/backend/rsform/useCstUpdate.tsx b/rsconcept/frontend/src/backend/rsform/useCstUpdate.tsx index 5bab1566..aa9347ff 100644 --- a/rsconcept/frontend/src/backend/rsform/useCstUpdate.tsx +++ b/rsconcept/frontend/src/backend/rsform/useCstUpdate.tsx @@ -22,6 +22,7 @@ export const useCstUpdate = () => { } }); return { - cstUpdate: (data: { itemID: LibraryItemID; data: ICstUpdateDTO }) => mutation.mutate(data) + cstUpdate: (data: { itemID: LibraryItemID; data: ICstUpdateDTO }, onSuccess?: () => void) => + mutation.mutate(data, { onSuccess }) }; }; diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/EditorConstituenta.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/EditorConstituenta.tsx index 251dba82..75e3a47a 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/EditorConstituenta.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/EditorConstituenta.tsx @@ -15,6 +15,7 @@ import { promptUnsaved } from '@/utils/utils'; import { useRSEdit } from '../RSEditContext'; import ViewConstituents from '../ViewConstituents'; +import EditorControls from './EditorControls'; import FormConstituenta from './FormConstituenta'; import ToolbarConstituenta from './ToolbarConstituenta'; @@ -22,7 +23,7 @@ import ToolbarConstituenta from './ToolbarConstituenta'; const SIDELIST_LAYOUT_THRESHOLD = 1000; // px function EditorConstituenta() { - const controller = useRSEdit(); + const { schema, activeCst, isContentEditable, moveUp, moveDown, cloneCst, navigateCst } = useRSEdit(); const windowSize = useWindowSize(); const mainHeight = useMainHeight(); @@ -34,7 +35,7 @@ function EditorConstituenta() { const [toggleReset, setToggleReset] = useState(false); const isProcessing = useMutatingRSForm(); - const disabled = !controller.activeCst || !controller.isContentEditable || isProcessing; + const disabled = !activeCst || !isContentEditable || isProcessing; const isNarrow = !!windowSize.width && windowSize.width <= SIDELIST_LAYOUT_THRESHOLD; function handleInput(event: React.KeyboardEvent) { @@ -58,19 +59,19 @@ function EditorConstituenta() { } function handleEditTermForms() { - if (!controller.activeCst) { + if (!activeCst) { return; } if (isModified && !promptUnsaved()) { return; } showEditTerm({ - target: controller.activeCst, + target: activeCst, onSave: forms => cstUpdate({ - itemID: controller.schema.id, + itemID: schema.id, data: { - target: controller.activeCst!.id, + target: activeCst.id, item_data: { term_forms: forms } } }) @@ -87,9 +88,9 @@ function EditorConstituenta() { function processAltKey(code: string): boolean { // prettier-ignore switch (code) { - case 'ArrowUp': controller.moveUp(); return true; - case 'ArrowDown': controller.moveDown(); return true; - case 'KeyV': controller.cloneCst(); return true; + case 'ArrowUp': moveUp(); return true; + case 'ArrowDown': moveDown(); return true; + case 'KeyV': cloneCst(); return true; } return false; } @@ -97,7 +98,7 @@ function EditorConstituenta() { return ( <> setToggleReset(prev => !prev)} @@ -114,15 +115,28 @@ function EditorConstituenta() { style={{ maxHeight: mainHeight }} onKeyDown={handleInput} > - +
+ {activeCst ? ( + + ) : null} + {activeCst ? ( + + ) : null} +
diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/FormConstituenta.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/FormConstituenta.tsx index ea166958..5f01f259 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/FormConstituenta.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/FormConstituenta.tsx @@ -1,10 +1,11 @@ 'use client'; -import clsx from 'clsx'; +import { zodResolver } from '@hookform/resolvers/zod'; import { useEffect, useLayoutEffect, useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; import { toast } from 'react-toastify'; -import { ICstUpdateDTO } from '@/backend/rsform/api'; +import { CstUpdateSchema, ICstUpdateDTO } from '@/backend/rsform/api'; import { useCstUpdate } from '@/backend/rsform/useCstUpdate'; import { useMutatingRSForm } from '@/backend/rsform/useMutatingRSForm'; import { IconChild, IconPredecessor, IconSave } from '@/components/Icons'; @@ -14,117 +15,84 @@ import Indicator from '@/components/ui/Indicator'; import Overlay from '@/components/ui/Overlay'; import SubmitButton from '@/components/ui/SubmitButton'; import TextArea from '@/components/ui/TextArea'; -import { CstType } from '@/models/rsform'; +import { ConstituentaID, CstType, IConstituenta, IRSForm } from '@/models/rsform'; import { isBaseSet, isBasicConcept, isFunctional } from '@/models/rsformAPI'; import { IExpressionParse, ParsingStatus } from '@/models/rslang'; import { useDialogsStore } from '@/stores/dialogs'; import { useModificationStore } from '@/stores/modification'; -import { errors, labelCstTypification } from '@/utils/labels'; +import { errors, labelCstTypification, labelTypification } from '@/utils/labels'; import EditorRSExpression from '../EditorRSExpression'; -import { useRSEdit } from '../RSEditContext'; -import EditorControls from './EditorControls'; interface FormConstituentaProps { id?: string; disabled: boolean; toggleReset: boolean; - onEditTerm: () => void; + activeCst: IConstituenta; + schema: IRSForm; + onOpenEdit?: (cstID: ConstituentaID) => void; } -function FormConstituenta({ - disabled, - id, - - toggleReset, - onEditTerm -}: FormConstituentaProps) { +function FormConstituenta({ disabled, id, toggleReset, schema, activeCst, onOpenEdit }: FormConstituentaProps) { const { cstUpdate } = useCstUpdate(); - const { schema, activeCst, navigateCst } = useRSEdit(); + const showTypification = useDialogsStore(state => state.showShowTypeGraph); const { isModified, setIsModified } = useModificationStore(); const isProcessing = useMutatingRSForm(); - const [term, setTerm] = useState(activeCst?.term_raw ?? ''); - const [textDefinition, setTextDefinition] = useState(activeCst?.definition_raw ?? ''); - const [expression, setExpression] = useState(activeCst?.definition_formal ?? ''); - const [convention, setConvention] = useState(activeCst?.convention ?? ''); - const [typification, setTypification] = useState('N/A'); + const { + register, + handleSubmit, + control, + reset, + formState: { isDirty } + } = useForm({ resolver: zodResolver(CstUpdateSchema) }); + const [localParse, setLocalParse] = useState(undefined); - const typeInfo = activeCst - ? { - alias: activeCst.alias, - result: localParse ? localParse.typification : activeCst.parse.typification, - args: localParse ? localParse.args : activeCst.parse.args - } - : undefined; + const typification = localParse + ? labelTypification({ + isValid: localParse.parseResult, + resultType: localParse.typification, + args: localParse.args + }) + : labelCstTypification(activeCst); + + const typeInfo = { + alias: activeCst.alias, + result: localParse ? localParse.typification : activeCst.parse.typification, + args: localParse ? localParse.args : activeCst.parse.args + }; const [forceComment, setForceComment] = useState(false); - - const isBasic = !!activeCst && isBasicConcept(activeCst.cst_type); - const isElementary = !!activeCst && isBaseSet(activeCst.cst_type); - const showConvention = !activeCst || !!activeCst.convention || forceComment || isBasic; - - const showTypification = useDialogsStore(activeCst => activeCst.showShowTypeGraph); + const isBasic = isBasicConcept(activeCst.cst_type); + const isElementary = isBaseSet(activeCst.cst_type); + const showConvention = !!activeCst.convention || forceComment || isBasic; useEffect(() => { - if (activeCst) { - setConvention(activeCst.convention); - setTerm(activeCst.term_raw); - setTextDefinition(activeCst.definition_raw); - setExpression(activeCst.definition_formal); - setTypification(activeCst ? labelCstTypification(activeCst) : 'N/A'); - setForceComment(false); - setLocalParse(undefined); - } - }, [activeCst, schema, toggleReset, setIsModified]); - - useLayoutEffect(() => { - if (!activeCst) { - setIsModified(false); - return; - } - setIsModified( - activeCst.term_raw !== term || - activeCst.definition_raw !== textDefinition || - activeCst.convention !== convention || - activeCst.definition_formal !== expression - ); - return () => setIsModified(false); - }, [ - activeCst, - activeCst?.term_raw, - activeCst?.definition_formal, - activeCst?.definition_raw, - activeCst?.convention, - term, - textDefinition, - expression, - convention, - setIsModified - ]); - - function handleSubmit(event?: React.FormEvent) { - if (event) { - event.preventDefault(); - } - if (!activeCst || isProcessing || !schema) { - return; - } - const data: ICstUpdateDTO = { + reset({ target: activeCst.id, item_data: { - term_raw: activeCst.term_raw !== term ? term : undefined, - definition_formal: activeCst.definition_formal !== expression ? expression : undefined, - definition_raw: activeCst.definition_raw !== textDefinition ? textDefinition : undefined, - convention: activeCst.convention !== convention ? convention : undefined + convention: activeCst.convention, + term_raw: activeCst.term_raw, + definition_raw: activeCst.definition_raw, + definition_formal: activeCst.definition_formal } - }; - cstUpdate({ itemID: schema.id, data }); + }); + setForceComment(false); + setLocalParse(undefined); + }, [activeCst, schema, toggleReset, reset]); + + useLayoutEffect(() => { + setIsModified(isDirty); + return () => setIsModified(false); + }, [isDirty, activeCst, setIsModified]); + + function onSubmit(data: ICstUpdateDTO) { + cstUpdate({ itemID: schema.id, data }, () => reset({ ...data })); } function handleTypeGraph(event: CProps.EventMouse) { - if (!activeCst || (localParse && !localParse.parseResult) || activeCst.parse.status !== ParsingStatus.VERIFIED) { + if ((localParse && !localParse.parseResult) || activeCst.parse.status !== ParsingStatus.VERIFIED) { toast.error(errors.typeStructureFailed); return; } @@ -134,138 +102,145 @@ function FormConstituenta({ } return ( -
- {activeCst ? : null} -
- setTerm(newValue)} - /> - {activeCst ? ( -