From 4b450384c4d761014e6744a7b534be4341eeb68d Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Sat, 15 Feb 2025 15:33:37 +0300 Subject: [PATCH] F: Rework reference editor dialog --- .../src/components/Modal/ModalForm.tsx | 22 ++-- .../rsform/components/RefsInput/RefsInput.tsx | 86 +++++++------ .../rsform/components/RefsInput/index.tsx | 2 +- .../DlgEditReference/DlgEditReference.tsx | 116 +++++++++++++++--- .../DlgEditReference/TabEntityReference.tsx | 76 ++++-------- .../TabSyntacticReference.tsx | 41 ++----- .../src/features/rsform/models/language.ts | 15 ++- .../src/features/rsform/models/languageAPI.ts | 18 +++ .../EditorConstituenta/FormConstituenta.tsx | 6 +- rsconcept/frontend/src/stores/dialogs.ts | 11 +- 10 files changed, 230 insertions(+), 163 deletions(-) diff --git a/rsconcept/frontend/src/components/Modal/ModalForm.tsx b/rsconcept/frontend/src/components/Modal/ModalForm.tsx index 1bdbc393..5c868ecc 100644 --- a/rsconcept/frontend/src/components/Modal/ModalForm.tsx +++ b/rsconcept/frontend/src/components/Modal/ModalForm.tsx @@ -43,7 +43,10 @@ interface ModalFormProps extends ModalProps { beforeSubmit?: () => boolean; /** Callback to be called after submit. */ - onSubmit: (event: React.FormEvent) => void; + onSubmit: (event: React.FormEvent) => void | Promise; + + /** Callback to be called when modal is canceled. */ + onCancel?: () => void; } /** @@ -61,25 +64,30 @@ export function ModalForm({ submitInvalidTooltip, beforeSubmit, onSubmit, + onCancel, helpTopic, hideHelpWhen, ...restProps }: React.PropsWithChildren) { const hideDialog = useDialogsStore(state => state.hideDialog); - useEscapeKey(hideDialog); + + function handleCancel() { + onCancel?.(); + hideDialog(); + } + useEscapeKey(handleCancel); function handleSubmit(event: React.FormEvent) { if (beforeSubmit && !beforeSubmit()) { return; } - onSubmit(event); - hideDialog(); + void Promise.resolve(onSubmit(event)).then(hideDialog); } return (
- +
} className='float-right mt-2 mr-2' - onClick={hideDialog} + onClick={handleCancel} /> {header ?

{header}

: null} @@ -126,7 +134,7 @@ export function ModalForm({ className='min-w-[7rem]' disabled={!canSubmit} /> -
diff --git a/rsconcept/frontend/src/features/rsform/components/RefsInput/RefsInput.tsx b/rsconcept/frontend/src/features/rsform/components/RefsInput/RefsInput.tsx index 50806ab3..be3b422e 100644 --- a/rsconcept/frontend/src/features/rsform/components/RefsInput/RefsInput.tsx +++ b/rsconcept/frontend/src/features/rsform/components/RefsInput/RefsInput.tsx @@ -9,12 +9,15 @@ import clsx from 'clsx'; import { EditorView } from 'codemirror'; import { Label } from '@/components/Input'; -import { DialogType, useDialogsStore } from '@/stores/dialogs'; +import { useDialogsStore } from '@/stores/dialogs'; import { usePreferencesStore } from '@/stores/preferences'; import { APP_COLORS } from '@/styling/colors'; import { CodeMirrorWrapper } from '@/utils/codemirror'; +import { PARAMETER } from '@/utils/constants'; +import { IReferenceInputState } from '../../dialogs/DlgEditReference/DlgEditReference'; import { ReferenceType } from '../../models/language'; +import { referenceToString } from '../../models/languageAPI'; import { IRSForm } from '../../models/rsform'; import { RefEntity } from './parse/parser.terms'; @@ -63,9 +66,9 @@ interface RefsInputInputProps | 'onBlur' | 'placeholder' > { - value?: string; - resolved?: string; - onChange?: (newValue: string) => void; + value: string; + resolved: string; + onChange: (newValue: string) => void; schema: IRSForm; onOpenEdit?: (cstID: number) => void; @@ -75,7 +78,7 @@ interface RefsInputInputProps initialValue?: string; } -const RefsInput = forwardRef( +export const RefsInput = forwardRef( ( { id, // prettier: split-lines @@ -98,14 +101,7 @@ const RefsInput = forwardRef( const [isFocused, setIsFocused] = useState(false); const showEditReference = useDialogsStore(state => state.showEditReference); - const activeDialog = useDialogsStore(state => state.active); - const isActive = activeDialog === DialogType.EDIT_REFERENCE; // TODO: reconsider this dependency - - const [currentType, setCurrentType] = useState(ReferenceType.ENTITY); - const [refText, setRefText] = useState(''); - const [hintText, setHintText] = useState(''); - const [basePosition, setBasePosition] = useState(0); - const [mainRefs, setMainRefs] = useState([]); + const [isEditing, setIsEditing] = useState(false); const internalRef = useRef(null); const thisRef = !ref || typeof ref === 'function' ? internalRef : ref; @@ -135,11 +131,8 @@ const RefsInput = forwardRef( refsHoverTooltip(schema, onOpenEdit !== undefined) ]; - function handleChange(newValue: string) { - if (onChange) onChange(newValue); - } - function handleFocusIn(event: React.FocusEvent) { + setIsEditing(false); setIsFocused(true); if (onFocus) onFocus(event); } @@ -162,45 +155,50 @@ const RefsInput = forwardRef( const wrap = new CodeMirrorWrapper(thisRef.current as Required); wrap.fixSelection(ReferenceTokens); const nodes = wrap.getEnvelopingNodes(ReferenceTokens); + + const data: IReferenceInputState = { + type: ReferenceType.ENTITY, + refRaw: '', + text: '', + mainRefs: [], + basePosition: 0 + }; + if (nodes.length !== 1) { - setCurrentType(ReferenceType.ENTITY); - setRefText(''); - setHintText(wrap.getSelectionText()); + data.text = wrap.getSelectionText(); } else { - setCurrentType(nodes[0].type.id === RefEntity ? ReferenceType.ENTITY : ReferenceType.SYNTACTIC); - setRefText(wrap.getSelectionText()); + data.type = nodes[0].type.id === RefEntity ? ReferenceType.ENTITY : ReferenceType.SYNTACTIC; + data.refRaw = wrap.getSelectionText(); } const selection = wrap.getSelection(); const mainNodes = wrap .getAllNodes([RefEntity]) .filter(node => node.from >= selection.to || node.to <= selection.from); - setMainRefs(mainNodes.map(node => wrap.getText(node.from, node.to))); - setBasePosition(mainNodes.filter(node => node.to <= selection.from).length); + data.mainRefs = mainNodes.map(node => wrap.getText(node.from, node.to)); + data.basePosition = mainNodes.filter(node => node.to <= selection.from).length; + setIsEditing(true); showEditReference({ schema: schema, - initial: { - type: currentType, - refRaw: refText, - text: hintText, - basePosition: basePosition, - mainRefs: mainRefs + initial: data, + onCancel: () => { + setIsEditing(false); + setTimeout(() => { + thisRef.current?.view?.focus(); + }, PARAMETER.minimalTimeout); }, - onSave: handleInputReference + onSave: ref => { + wrap.replaceWith(referenceToString(ref)); + setIsEditing(false); + setTimeout(() => { + thisRef.current?.view?.focus(); + }, PARAMETER.minimalTimeout); + } }); } } - function handleInputReference(referenceText: string) { - if (!thisRef.current?.view) { - return; - } - thisRef.current.view.focus(); - const wrap = new CodeMirrorWrapper(thisRef.current as Required); - wrap.replaceWith(referenceText); - } - return (
- + } + />
); } - -export default TabEntityReference; diff --git a/rsconcept/frontend/src/features/rsform/dialogs/DlgEditReference/TabSyntacticReference.tsx b/rsconcept/frontend/src/features/rsform/dialogs/DlgEditReference/TabSyntacticReference.tsx index b644643e..1099621f 100644 --- a/rsconcept/frontend/src/features/rsform/dialogs/DlgEditReference/TabSyntacticReference.tsx +++ b/rsconcept/frontend/src/features/rsform/dialogs/DlgEditReference/TabSyntacticReference.tsx @@ -1,24 +1,16 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useFormContext, useWatch } from 'react-hook-form'; import { TextInput } from '@/components/Input'; import { useDialogsStore } from '@/stores/dialogs'; -import { ReferenceType } from '../../models/language'; -import { parseSyntacticReference } from '../../models/languageAPI'; +import { DlgEditReferenceProps, IEditReferenceState } from './DlgEditReference'; -import { DlgEditReferenceProps } from './DlgEditReference'; - -interface TabSyntacticReferenceProps { - onChangeValid: (newValue: boolean) => void; - onChangeReference: (newValue: string) => void; -} - -function TabSyntacticReference({ onChangeValid, onChangeReference }: TabSyntacticReferenceProps) { +export function TabSyntacticReference() { const { initial } = useDialogsStore(state => state.props as DlgEditReferenceProps); - const [nominal, setNominal] = useState(''); - const [offset, setOffset] = useState(1); + const { control, register } = useFormContext(); + const offset = useWatch({ control, name: 'syntactic.offset' }); const mainLink = (() => { const position = offset > 0 ? initial.basePosition + (offset - 1) : initial.basePosition + offset; @@ -29,21 +21,6 @@ function TabSyntacticReference({ onChangeValid, onChangeReference }: TabSyntacti } })(); - useEffect(() => { - if (initial.refRaw && initial.type === ReferenceType.SYNTACTIC) { - const ref = parseSyntacticReference(initial.refRaw); - setOffset(ref.offset); - setNominal(ref.nominal); - } else { - setNominal(initial.text ?? ''); - } - }, [initial]); - - useEffect(() => { - onChangeValid(nominal !== '' && offset !== 0); - onChangeReference(`@{${offset}|${nominal}}`); - }, [nominal, offset, onChangeValid, onChangeReference]); - return (
setOffset(event.target.valueAsNumber)} + {...register('syntactic.offset')} /> setNominal(event.target.value)} + {...register('syntactic.nominal')} />
); } - -export default TabSyntacticReference; diff --git a/rsconcept/frontend/src/features/rsform/models/language.ts b/rsconcept/frontend/src/features/rsform/models/language.ts index c56a14dd..d9cb3e6b 100644 --- a/rsconcept/frontend/src/features/rsform/models/language.ts +++ b/rsconcept/frontend/src/features/rsform/models/language.ts @@ -2,6 +2,8 @@ * Module: Natural language model declarations. */ +import { z } from 'zod'; + /** * Represents single unit of language Morphology. */ @@ -266,10 +268,15 @@ export interface ITextPosition { finish: number; } +export const schemaReference = z.object({ + type: z.nativeEnum(ReferenceType), + data: z.union([ + z.object({ entity: z.string(), form: z.string() }), + z.object({ offset: z.number(), nominal: z.string() }) + ]) +}); + /** * Represents abstract reference data. */ -export interface IReference { - type: ReferenceType; - data: IEntityReference | ISyntacticReference; -} +export type IReference = z.infer; diff --git a/rsconcept/frontend/src/features/rsform/models/languageAPI.ts b/rsconcept/frontend/src/features/rsform/models/languageAPI.ts index c78667b4..aaea3b53 100644 --- a/rsconcept/frontend/src/features/rsform/models/languageAPI.ts +++ b/rsconcept/frontend/src/features/rsform/models/languageAPI.ts @@ -10,9 +10,11 @@ import { GrammemeGroups, IEntityReference, IGrammemeOption, + IReference, ISyntacticReference, IWordForm, NounGrams, + ReferenceType, supportedGrammemes, VerbGrams } from './language'; @@ -128,3 +130,19 @@ export const supportedGrammeOptions: IGrammemeOption[] = supportedGrammemes.map( value: gram, label: labelGrammeme(gram) })); + +/** + * Transforms {@link IReference} to string representation. + */ +export function referenceToString(ref: IReference): string { + switch (ref.type) { + case ReferenceType.ENTITY: { + const entity = ref.data as IEntityReference; + return `@{${entity.entity}|${entity.form}}`; + } + case ReferenceType.SYNTACTIC: { + const syntactic = ref.data as ISyntacticReference; + return `@{${syntactic.offset}|${syntactic.nominal}}`; + } + } +} diff --git a/rsconcept/frontend/src/features/rsform/pages/RSFormPage/EditorConstituenta/FormConstituenta.tsx b/rsconcept/frontend/src/features/rsform/pages/RSFormPage/EditorConstituenta/FormConstituenta.tsx index 52c749f2..661027b3 100644 --- a/rsconcept/frontend/src/features/rsform/pages/RSFormPage/EditorConstituenta/FormConstituenta.tsx +++ b/rsconcept/frontend/src/features/rsform/pages/RSFormPage/EditorConstituenta/FormConstituenta.tsx @@ -19,7 +19,7 @@ import { errorMsg } from '@/utils/labels'; import { ICstUpdateDTO, schemaCstUpdate } from '../../../backend/types'; import { useCstUpdate } from '../../../backend/useCstUpdate'; import { useMutatingRSForm } from '../../../backend/useMutatingRSForm'; -import RefsInput from '../../../components/RefsInput'; +import { RefsInput } from '../../../components/RefsInput'; import { labelCstTypification, labelTypification } from '../../../labels'; import { CstType, IConstituenta, IRSForm } from '../../../models/rsform'; import { isBaseSet, isBasicConcept, isFunctional } from '../../../models/rsformAPI'; @@ -125,7 +125,7 @@ function FormConstituenta({ disabled, id, toggleReset, schema, activeCst, onOpen placeholder='Обозначение для текстовых определений' schema={schema} onOpenEdit={onOpenEdit} - value={field.value} + value={field.value ?? ''} initialValue={activeCst.term_raw} resolved={activeCst.term_resolved} disabled={disabled} @@ -189,7 +189,7 @@ function FormConstituenta({ disabled, id, toggleReset, schema, activeCst, onOpen maxHeight='8rem' schema={schema} onOpenEdit={onOpenEdit} - value={field.value} + value={field.value ?? ''} initialValue={activeCst.definition_raw} resolved={activeCst.definition_resolved} disabled={disabled} diff --git a/rsconcept/frontend/src/stores/dialogs.ts b/rsconcept/frontend/src/stores/dialogs.ts index 0d10f007..1b0ffd77 100644 --- a/rsconcept/frontend/src/stores/dialogs.ts +++ b/rsconcept/frontend/src/stores/dialogs.ts @@ -52,6 +52,10 @@ export enum DialogType { UPLOAD_RSFORM } +export interface GenericDialogProps { + onHide?: () => void; +} + interface DialogsStore { active: DialogType | undefined; props: unknown; @@ -85,7 +89,12 @@ interface DialogsStore { export const useDialogsStore = create()(set => ({ active: undefined, props: undefined, - hideDialog: () => set({ active: undefined, props: undefined }), + hideDialog: () => { + set(state => { + (state.props as GenericDialogProps | undefined)?.onHide?.(); + return { active: undefined, props: undefined }; + }); + }, showCstTemplate: props => set({ active: DialogType.CONSTITUENTA_TEMPLATE, props: props }), showCreateCst: props => set({ active: DialogType.CREATE_CONSTITUENTA, props: props }),