F: Rework Constituenta editor form

This commit is contained in:
Ivan 2025-02-05 15:55:56 +03:00
parent cc6e592149
commit 77479fb6fe
6 changed files with 244 additions and 256 deletions

View File

@ -1,4 +1,5 @@
import { queryOptions } from '@tanstack/react-query'; import { queryOptions } from '@tanstack/react-query';
import { z } from 'zod';
import { axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport'; import { axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport';
import { DELAYS } from '@/backend/configuration'; import { DELAYS } from '@/backend/configuration';
@ -74,16 +75,21 @@ export interface ICstCreatedResponse {
/** /**
* Represents data, used in updating persistent attributes in {@link IConstituenta}. * Represents data, used in updating persistent attributes in {@link IConstituenta}.
*/ */
export interface ICstUpdateDTO { export const CstUpdateSchema = z.object({
target: ConstituentaID; target: z.number(),
item_data: { item_data: z.object({
convention?: string; convention: z.string().optional(),
definition_formal?: string; definition_formal: z.string().optional(),
definition_raw?: string; definition_raw: z.string().optional(),
term_raw?: string; term_raw: z.string().optional(),
term_forms?: TermForm[]; 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<typeof CstUpdateSchema>;
/** /**
* Represents data, used in renaming {@link IConstituenta}. * Represents data, used in renaming {@link IConstituenta}.

View File

@ -8,7 +8,7 @@ import { ICheckConstituentaDTO, rsformsApi } from './api';
export const useCheckConstituenta = () => { export const useCheckConstituenta = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [rsformsApi.baseKey, 'check-constituenta'], mutationKey: ['actions', 'check-constituenta'],
mutationFn: rsformsApi.checkConstituenta mutationFn: rsformsApi.checkConstituenta
}); });
return { return {

View File

@ -22,6 +22,7 @@ export const useCstUpdate = () => {
} }
}); });
return { return {
cstUpdate: (data: { itemID: LibraryItemID; data: ICstUpdateDTO }) => mutation.mutate(data) cstUpdate: (data: { itemID: LibraryItemID; data: ICstUpdateDTO }, onSuccess?: () => void) =>
mutation.mutate(data, { onSuccess })
}; };
}; };

View File

@ -15,6 +15,7 @@ import { promptUnsaved } from '@/utils/utils';
import { useRSEdit } from '../RSEditContext'; import { useRSEdit } from '../RSEditContext';
import ViewConstituents from '../ViewConstituents'; import ViewConstituents from '../ViewConstituents';
import EditorControls from './EditorControls';
import FormConstituenta from './FormConstituenta'; import FormConstituenta from './FormConstituenta';
import ToolbarConstituenta from './ToolbarConstituenta'; import ToolbarConstituenta from './ToolbarConstituenta';
@ -22,7 +23,7 @@ import ToolbarConstituenta from './ToolbarConstituenta';
const SIDELIST_LAYOUT_THRESHOLD = 1000; // px const SIDELIST_LAYOUT_THRESHOLD = 1000; // px
function EditorConstituenta() { function EditorConstituenta() {
const controller = useRSEdit(); const { schema, activeCst, isContentEditable, moveUp, moveDown, cloneCst, navigateCst } = useRSEdit();
const windowSize = useWindowSize(); const windowSize = useWindowSize();
const mainHeight = useMainHeight(); const mainHeight = useMainHeight();
@ -34,7 +35,7 @@ function EditorConstituenta() {
const [toggleReset, setToggleReset] = useState(false); const [toggleReset, setToggleReset] = useState(false);
const isProcessing = useMutatingRSForm(); const isProcessing = useMutatingRSForm();
const disabled = !controller.activeCst || !controller.isContentEditable || isProcessing; const disabled = !activeCst || !isContentEditable || isProcessing;
const isNarrow = !!windowSize.width && windowSize.width <= SIDELIST_LAYOUT_THRESHOLD; const isNarrow = !!windowSize.width && windowSize.width <= SIDELIST_LAYOUT_THRESHOLD;
function handleInput(event: React.KeyboardEvent<HTMLDivElement>) { function handleInput(event: React.KeyboardEvent<HTMLDivElement>) {
@ -58,19 +59,19 @@ function EditorConstituenta() {
} }
function handleEditTermForms() { function handleEditTermForms() {
if (!controller.activeCst) { if (!activeCst) {
return; return;
} }
if (isModified && !promptUnsaved()) { if (isModified && !promptUnsaved()) {
return; return;
} }
showEditTerm({ showEditTerm({
target: controller.activeCst, target: activeCst,
onSave: forms => onSave: forms =>
cstUpdate({ cstUpdate({
itemID: controller.schema.id, itemID: schema.id,
data: { data: {
target: controller.activeCst!.id, target: activeCst.id,
item_data: { term_forms: forms } item_data: { term_forms: forms }
} }
}) })
@ -87,9 +88,9 @@ function EditorConstituenta() {
function processAltKey(code: string): boolean { function processAltKey(code: string): boolean {
// prettier-ignore // prettier-ignore
switch (code) { switch (code) {
case 'ArrowUp': controller.moveUp(); return true; case 'ArrowUp': moveUp(); return true;
case 'ArrowDown': controller.moveDown(); return true; case 'ArrowDown': moveDown(); return true;
case 'KeyV': controller.cloneCst(); return true; case 'KeyV': cloneCst(); return true;
} }
return false; return false;
} }
@ -97,7 +98,7 @@ function EditorConstituenta() {
return ( return (
<> <>
<ToolbarConstituenta <ToolbarConstituenta
activeCst={controller.activeCst} activeCst={activeCst}
disabled={disabled} disabled={disabled}
onSubmit={initiateSubmit} onSubmit={initiateSubmit}
onReset={() => setToggleReset(prev => !prev)} onReset={() => setToggleReset(prev => !prev)}
@ -114,15 +115,28 @@ function EditorConstituenta() {
style={{ maxHeight: mainHeight }} style={{ maxHeight: mainHeight }}
onKeyDown={handleInput} onKeyDown={handleInput}
> >
<FormConstituenta <div className='mx-0 md:mx-auto pt-[2rem] md:w-[48.8rem] shrink-0 xs:pt-0'>
id={globals.constituenta_editor} {activeCst ? (
disabled={disabled} <EditorControls
toggleReset={toggleReset} disabled={disabled} //
constituenta={activeCst}
onEditTerm={handleEditTermForms} onEditTerm={handleEditTermForms}
/> />
) : null}
{activeCst ? (
<FormConstituenta
id={globals.constituenta_editor} //
disabled={disabled}
toggleReset={toggleReset}
activeCst={activeCst}
schema={schema}
onOpenEdit={navigateCst}
/>
) : null}
</div>
<ViewConstituents <ViewConstituents
isMounted={showList} isMounted={showList} //
expression={controller.activeCst?.definition_formal ?? ''} expression={activeCst?.definition_formal ?? ''}
isBottom={isNarrow} isBottom={isNarrow}
/> />
</div> </div>

View File

@ -1,10 +1,11 @@
'use client'; 'use client';
import clsx from 'clsx'; import { zodResolver } from '@hookform/resolvers/zod';
import { useEffect, useLayoutEffect, useState } from 'react'; import { useEffect, useLayoutEffect, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { toast } from 'react-toastify'; 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 { useCstUpdate } from '@/backend/rsform/useCstUpdate';
import { useMutatingRSForm } from '@/backend/rsform/useMutatingRSForm'; import { useMutatingRSForm } from '@/backend/rsform/useMutatingRSForm';
import { IconChild, IconPredecessor, IconSave } from '@/components/Icons'; import { IconChild, IconPredecessor, IconSave } from '@/components/Icons';
@ -14,117 +15,84 @@ import Indicator from '@/components/ui/Indicator';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import SubmitButton from '@/components/ui/SubmitButton'; import SubmitButton from '@/components/ui/SubmitButton';
import TextArea from '@/components/ui/TextArea'; 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 { isBaseSet, isBasicConcept, isFunctional } from '@/models/rsformAPI';
import { IExpressionParse, ParsingStatus } from '@/models/rslang'; import { IExpressionParse, ParsingStatus } from '@/models/rslang';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { useModificationStore } from '@/stores/modification'; import { useModificationStore } from '@/stores/modification';
import { errors, labelCstTypification } from '@/utils/labels'; import { errors, labelCstTypification, labelTypification } from '@/utils/labels';
import EditorRSExpression from '../EditorRSExpression'; import EditorRSExpression from '../EditorRSExpression';
import { useRSEdit } from '../RSEditContext';
import EditorControls from './EditorControls';
interface FormConstituentaProps { interface FormConstituentaProps {
id?: string; id?: string;
disabled: boolean; disabled: boolean;
toggleReset: boolean; toggleReset: boolean;
onEditTerm: () => void; activeCst: IConstituenta;
schema: IRSForm;
onOpenEdit?: (cstID: ConstituentaID) => void;
} }
function FormConstituenta({ function FormConstituenta({ disabled, id, toggleReset, schema, activeCst, onOpenEdit }: FormConstituentaProps) {
disabled,
id,
toggleReset,
onEditTerm
}: FormConstituentaProps) {
const { cstUpdate } = useCstUpdate(); const { cstUpdate } = useCstUpdate();
const { schema, activeCst, navigateCst } = useRSEdit(); const showTypification = useDialogsStore(state => state.showShowTypeGraph);
const { isModified, setIsModified } = useModificationStore(); const { isModified, setIsModified } = useModificationStore();
const isProcessing = useMutatingRSForm(); const isProcessing = useMutatingRSForm();
const [term, setTerm] = useState(activeCst?.term_raw ?? ''); const {
const [textDefinition, setTextDefinition] = useState(activeCst?.definition_raw ?? ''); register,
const [expression, setExpression] = useState(activeCst?.definition_formal ?? ''); handleSubmit,
const [convention, setConvention] = useState(activeCst?.convention ?? ''); control,
const [typification, setTypification] = useState('N/A'); reset,
formState: { isDirty }
} = useForm<ICstUpdateDTO>({ resolver: zodResolver(CstUpdateSchema) });
const [localParse, setLocalParse] = useState<IExpressionParse | undefined>(undefined); const [localParse, setLocalParse] = useState<IExpressionParse | undefined>(undefined);
const typeInfo = activeCst const typification = localParse
? { ? labelTypification({
isValid: localParse.parseResult,
resultType: localParse.typification,
args: localParse.args
})
: labelCstTypification(activeCst);
const typeInfo = {
alias: activeCst.alias, alias: activeCst.alias,
result: localParse ? localParse.typification : activeCst.parse.typification, result: localParse ? localParse.typification : activeCst.parse.typification,
args: localParse ? localParse.args : activeCst.parse.args args: localParse ? localParse.args : activeCst.parse.args
} };
: undefined;
const [forceComment, setForceComment] = useState(false); const [forceComment, setForceComment] = useState(false);
const isBasic = isBasicConcept(activeCst.cst_type);
const isBasic = !!activeCst && isBasicConcept(activeCst.cst_type); const isElementary = isBaseSet(activeCst.cst_type);
const isElementary = !!activeCst && isBaseSet(activeCst.cst_type); const showConvention = !!activeCst.convention || forceComment || isBasic;
const showConvention = !activeCst || !!activeCst.convention || forceComment || isBasic;
const showTypification = useDialogsStore(activeCst => activeCst.showShowTypeGraph);
useEffect(() => { useEffect(() => {
if (activeCst) { reset({
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<HTMLFormElement>) {
if (event) {
event.preventDefault();
}
if (!activeCst || isProcessing || !schema) {
return;
}
const data: ICstUpdateDTO = {
target: activeCst.id, target: activeCst.id,
item_data: { item_data: {
term_raw: activeCst.term_raw !== term ? term : undefined, convention: activeCst.convention,
definition_formal: activeCst.definition_formal !== expression ? expression : undefined, term_raw: activeCst.term_raw,
definition_raw: activeCst.definition_raw !== textDefinition ? textDefinition : undefined, definition_raw: activeCst.definition_raw,
convention: activeCst.convention !== convention ? convention : undefined 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) { 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); toast.error(errors.typeStructureFailed);
return; return;
} }
@ -134,9 +102,11 @@ function FormConstituenta({
} }
return ( return (
<div className='mx-0 md:mx-auto pt-[2rem] xs:pt-0'> <form id={id} className='cc-column mt-1 px-6 py-1' onSubmit={event => void handleSubmit(onSubmit)(event)}>
{activeCst ? <EditorControls disabled={disabled} constituenta={activeCst} onEditTerm={onEditTerm} /> : null} <Controller
<form id={id} className={clsx('cc-column', 'mt-1 md:w-[48.8rem] shrink-0', 'px-6 py-1')} onSubmit={handleSubmit}> control={control}
name='item_data.term_raw'
render={({ field }) => (
<RefsInput <RefsInput
key='cst_term' key='cst_term'
id='cst_term' id='cst_term'
@ -144,14 +114,16 @@ function FormConstituenta({
maxHeight='8rem' maxHeight='8rem'
placeholder='Обозначение для текстовых определений' placeholder='Обозначение для текстовых определений'
schema={schema} schema={schema}
onOpenEdit={navigateCst} onOpenEdit={onOpenEdit}
value={term} value={field.value}
initialValue={activeCst?.term_raw ?? ''} initialValue={activeCst.term_raw}
resolved={activeCst?.term_resolved ?? 'Конституента не выбрана'} resolved={activeCst.term_resolved}
disabled={disabled} disabled={disabled}
onChange={newValue => setTerm(newValue)} onChange={newValue => field.onChange(newValue)}
/> />
{activeCst ? ( )}
/>
<TextArea <TextArea
id='cst_typification' id='cst_typification'
fitContent fitContent
@ -164,10 +136,12 @@ function FormConstituenta({
value={typification} value={typification}
colors='bg-transparent clr-text-default cursor-default' colors='bg-transparent clr-text-default cursor-default'
/> />
) : null}
{activeCst ? (
<>
{!!activeCst.definition_formal || !isElementary ? ( {!!activeCst.definition_formal || !isElementary ? (
<Controller
control={control}
name='item_data.definition_formal'
render={({ field }) => (
<EditorRSExpression <EditorRSExpression
id='cst_expression' id='cst_expression'
label={ label={
@ -178,22 +152,25 @@ function FormConstituenta({
: 'Формальное определение' : 'Формальное определение'
} }
placeholder={ placeholder={
activeCst.cst_type !== CstType.STRUCTURED activeCst.cst_type !== CstType.STRUCTURED ? 'Родоструктурное выражение' : 'Типизация родовой структуры'
? 'Родоструктурное выражение'
: 'Типизация родовой структуры'
} }
value={expression} value={field.value ?? ''}
activeCst={activeCst} activeCst={activeCst}
disabled={disabled || activeCst.is_inherited} disabled={disabled || activeCst.is_inherited}
toggleReset={toggleReset} toggleReset={toggleReset}
onChangeExpression={newValue => setExpression(newValue)} onChange={newValue => field.onChange(newValue)}
onChangeTypification={setTypification}
onChangeLocalParse={setLocalParse} onChangeLocalParse={setLocalParse}
onOpenEdit={navigateCst} onOpenEdit={onOpenEdit}
onShowTypeGraph={handleTypeGraph} onShowTypeGraph={handleTypeGraph}
/> />
)}
/>
) : null} ) : null}
{!!activeCst.definition_raw || !isElementary ? ( {!!activeCst.definition_raw || !isElementary ? (
<Controller
control={control}
name='item_data.definition_raw'
render={({ field }) => (
<RefsInput <RefsInput
id='cst_definition' id='cst_definition'
label='Текстовое определение' label='Текстовое определение'
@ -201,26 +178,27 @@ function FormConstituenta({
minHeight='3.75rem' minHeight='3.75rem'
maxHeight='8rem' maxHeight='8rem'
schema={schema} schema={schema}
onOpenEdit={navigateCst} onOpenEdit={onOpenEdit}
value={textDefinition} value={field.value}
initialValue={activeCst.definition_raw} initialValue={activeCst.definition_raw}
resolved={activeCst.definition_resolved} resolved={activeCst.definition_resolved}
disabled={disabled} disabled={disabled}
onChange={newValue => setTextDefinition(newValue)} onChange={newValue => field.onChange(newValue)}
/>
)}
/> />
) : null} ) : null}
{showConvention ? ( {showConvention ? (
<TextArea <TextArea
id='cst_convention' id='cst_convention'
{...register('item_data.convention')}
fitContent fitContent
className='max-h-[8rem]' className='max-h-[8rem]'
spellCheck spellCheck
label={isBasic ? 'Конвенция' : 'Комментарий'} label={isBasic ? 'Конвенция' : 'Комментарий'}
placeholder={isBasic ? 'Договоренность об интерпретации' : 'Пояснение разработчика'} placeholder={isBasic ? 'Договоренность об интерпретации' : 'Пояснение разработчика'}
value={convention}
disabled={disabled || (isBasic && activeCst.is_inherited)} disabled={disabled || (isBasic && activeCst.is_inherited)}
onChange={event => setConvention(event.target.value)}
/> />
) : null} ) : null}
@ -262,10 +240,7 @@ function FormConstituenta({
</Overlay> </Overlay>
</div> </div>
) : null} ) : null}
</>
) : null}
</form> </form>
</div>
); );
} }

View File

@ -22,7 +22,7 @@ import { TokenID } from '@/models/rslang';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import { transformAST } from '@/utils/codemirror'; import { transformAST } from '@/utils/codemirror';
import { errors, labelTypification } from '@/utils/labels'; import { errors } from '@/utils/labels';
import { useRSEdit } from '../RSEditContext'; import { useRSEdit } from '../RSEditContext';
import ParsingResult from './ParsingResult'; import ParsingResult from './ParsingResult';
@ -32,17 +32,17 @@ import ToolbarRSExpression from './ToolbarRSExpression';
interface EditorRSExpressionProps { interface EditorRSExpressionProps {
id?: string; id?: string;
activeCst: IConstituenta;
value: string; value: string;
onChange: (newValue: string) => void;
activeCst: IConstituenta;
label: string; label: string;
placeholder?: string; placeholder?: string;
disabled?: boolean; disabled?: boolean;
toggleReset?: boolean; toggleReset?: boolean;
onChangeTypification: (typification: string) => void;
onChangeLocalParse: (typification: IExpressionParse | undefined) => void; onChangeLocalParse: (typification: IExpressionParse | undefined) => void;
onChangeExpression: (newValue: string) => void;
onOpenEdit?: (cstID: ConstituentaID) => void; onOpenEdit?: (cstID: ConstituentaID) => void;
onShowTypeGraph: (event: CProps.EventMouse) => void; onShowTypeGraph: (event: CProps.EventMouse) => void;
} }
@ -52,9 +52,8 @@ function EditorRSExpression({
disabled, disabled,
value, value,
toggleReset, toggleReset,
onChangeTypification, onChange,
onChangeLocalParse, onChangeLocalParse,
onChangeExpression,
onOpenEdit, onOpenEdit,
onShowTypeGraph, onShowTypeGraph,
...restProps ...restProps
@ -89,7 +88,7 @@ function EditorRSExpression({
}, [activeCst, toggleReset]); }, [activeCst, toggleReset]);
function handleChange(newValue: string) { function handleChange(newValue: string) {
onChangeExpression(newValue); onChange(newValue);
setIsModified(newValue !== activeCst.definition_formal); setIsModified(newValue !== activeCst.definition_formal);
} }
@ -102,13 +101,6 @@ function EditorRSExpression({
rsInput.current?.view?.focus(); rsInput.current?.view?.focus();
} }
setIsModified(false); setIsModified(false);
onChangeTypification(
labelTypification({
isValid: parse.parseResult,
resultType: parse.typification,
args: parse.args
})
);
callback?.(parse); callback?.(parse);
}); });
} }