F: Rework reference editor dialog

This commit is contained in:
Ivan 2025-02-15 15:33:37 +03:00
parent cdcf1a9c43
commit 4b450384c4
10 changed files with 230 additions and 163 deletions

View File

@ -43,7 +43,10 @@ interface ModalFormProps extends ModalProps {
beforeSubmit?: () => boolean; beforeSubmit?: () => boolean;
/** Callback to be called after submit. */ /** Callback to be called after submit. */
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void; onSubmit: (event: React.FormEvent<HTMLFormElement>) => void | Promise<void>;
/** Callback to be called when modal is canceled. */
onCancel?: () => void;
} }
/** /**
@ -61,25 +64,30 @@ export function ModalForm({
submitInvalidTooltip, submitInvalidTooltip,
beforeSubmit, beforeSubmit,
onSubmit, onSubmit,
onCancel,
helpTopic, helpTopic,
hideHelpWhen, hideHelpWhen,
...restProps ...restProps
}: React.PropsWithChildren<ModalFormProps>) { }: React.PropsWithChildren<ModalFormProps>) {
const hideDialog = useDialogsStore(state => state.hideDialog); const hideDialog = useDialogsStore(state => state.hideDialog);
useEscapeKey(hideDialog);
function handleCancel() {
onCancel?.();
hideDialog();
}
useEscapeKey(handleCancel);
function handleSubmit(event: React.FormEvent<HTMLFormElement>) { function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
if (beforeSubmit && !beforeSubmit()) { if (beforeSubmit && !beforeSubmit()) {
return; return;
} }
onSubmit(event); void Promise.resolve(onSubmit(event)).then(hideDialog);
hideDialog();
} }
return ( return (
<div className='fixed top-0 left-0 w-full h-full z-modal cursor-default'> <div className='fixed top-0 left-0 w-full h-full z-modal cursor-default'>
<ModalBackdrop onHide={hideDialog} /> <ModalBackdrop onHide={handleCancel} />
<form <form
className={clsx( className={clsx(
'cc-animate-modal', 'cc-animate-modal',
@ -99,7 +107,7 @@ export function ModalForm({
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')} titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
icon={<IconClose size='1.25rem' />} icon={<IconClose size='1.25rem' />}
className='float-right mt-2 mr-2' className='float-right mt-2 mr-2'
onClick={hideDialog} onClick={handleCancel}
/> />
{header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null} {header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null}
@ -126,7 +134,7 @@ export function ModalForm({
className='min-w-[7rem]' className='min-w-[7rem]'
disabled={!canSubmit} disabled={!canSubmit}
/> />
<Button text='Отмена' className='min-w-[7rem]' onClick={hideDialog} /> <Button text='Отмена' className='min-w-[7rem]' onClick={handleCancel} />
</div> </div>
</form> </form>
</div> </div>

View File

@ -9,12 +9,15 @@ import clsx from 'clsx';
import { EditorView } from 'codemirror'; import { EditorView } from 'codemirror';
import { Label } from '@/components/Input'; import { Label } from '@/components/Input';
import { DialogType, useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import { APP_COLORS } from '@/styling/colors'; import { APP_COLORS } from '@/styling/colors';
import { CodeMirrorWrapper } from '@/utils/codemirror'; import { CodeMirrorWrapper } from '@/utils/codemirror';
import { PARAMETER } from '@/utils/constants';
import { IReferenceInputState } from '../../dialogs/DlgEditReference/DlgEditReference';
import { ReferenceType } from '../../models/language'; import { ReferenceType } from '../../models/language';
import { referenceToString } from '../../models/languageAPI';
import { IRSForm } from '../../models/rsform'; import { IRSForm } from '../../models/rsform';
import { RefEntity } from './parse/parser.terms'; import { RefEntity } from './parse/parser.terms';
@ -63,9 +66,9 @@ interface RefsInputInputProps
| 'onBlur' | 'onBlur'
| 'placeholder' | 'placeholder'
> { > {
value?: string; value: string;
resolved?: string; resolved: string;
onChange?: (newValue: string) => void; onChange: (newValue: string) => void;
schema: IRSForm; schema: IRSForm;
onOpenEdit?: (cstID: number) => void; onOpenEdit?: (cstID: number) => void;
@ -75,7 +78,7 @@ interface RefsInputInputProps
initialValue?: string; initialValue?: string;
} }
const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>( export const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
( (
{ {
id, // prettier: split-lines id, // prettier: split-lines
@ -98,14 +101,7 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
const [isFocused, setIsFocused] = useState(false); const [isFocused, setIsFocused] = useState(false);
const showEditReference = useDialogsStore(state => state.showEditReference); const showEditReference = useDialogsStore(state => state.showEditReference);
const activeDialog = useDialogsStore(state => state.active); const [isEditing, setIsEditing] = useState(false);
const isActive = activeDialog === DialogType.EDIT_REFERENCE; // TODO: reconsider this dependency
const [currentType, setCurrentType] = useState<ReferenceType>(ReferenceType.ENTITY);
const [refText, setRefText] = useState('');
const [hintText, setHintText] = useState('');
const [basePosition, setBasePosition] = useState(0);
const [mainRefs, setMainRefs] = useState<string[]>([]);
const internalRef = useRef<ReactCodeMirrorRef>(null); const internalRef = useRef<ReactCodeMirrorRef>(null);
const thisRef = !ref || typeof ref === 'function' ? internalRef : ref; const thisRef = !ref || typeof ref === 'function' ? internalRef : ref;
@ -135,11 +131,8 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
refsHoverTooltip(schema, onOpenEdit !== undefined) refsHoverTooltip(schema, onOpenEdit !== undefined)
]; ];
function handleChange(newValue: string) {
if (onChange) onChange(newValue);
}
function handleFocusIn(event: React.FocusEvent<HTMLDivElement>) { function handleFocusIn(event: React.FocusEvent<HTMLDivElement>) {
setIsEditing(false);
setIsFocused(true); setIsFocused(true);
if (onFocus) onFocus(event); if (onFocus) onFocus(event);
} }
@ -162,45 +155,50 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
const wrap = new CodeMirrorWrapper(thisRef.current as Required<ReactCodeMirrorRef>); const wrap = new CodeMirrorWrapper(thisRef.current as Required<ReactCodeMirrorRef>);
wrap.fixSelection(ReferenceTokens); wrap.fixSelection(ReferenceTokens);
const nodes = wrap.getEnvelopingNodes(ReferenceTokens); const nodes = wrap.getEnvelopingNodes(ReferenceTokens);
const data: IReferenceInputState = {
type: ReferenceType.ENTITY,
refRaw: '',
text: '',
mainRefs: [],
basePosition: 0
};
if (nodes.length !== 1) { if (nodes.length !== 1) {
setCurrentType(ReferenceType.ENTITY); data.text = wrap.getSelectionText();
setRefText('');
setHintText(wrap.getSelectionText());
} else { } else {
setCurrentType(nodes[0].type.id === RefEntity ? ReferenceType.ENTITY : ReferenceType.SYNTACTIC); data.type = nodes[0].type.id === RefEntity ? ReferenceType.ENTITY : ReferenceType.SYNTACTIC;
setRefText(wrap.getSelectionText()); data.refRaw = wrap.getSelectionText();
} }
const selection = wrap.getSelection(); const selection = wrap.getSelection();
const mainNodes = wrap const mainNodes = wrap
.getAllNodes([RefEntity]) .getAllNodes([RefEntity])
.filter(node => node.from >= selection.to || node.to <= selection.from); .filter(node => node.from >= selection.to || node.to <= selection.from);
setMainRefs(mainNodes.map(node => wrap.getText(node.from, node.to))); data.mainRefs = mainNodes.map(node => wrap.getText(node.from, node.to));
setBasePosition(mainNodes.filter(node => node.to <= selection.from).length); data.basePosition = mainNodes.filter(node => node.to <= selection.from).length;
setIsEditing(true);
showEditReference({ showEditReference({
schema: schema, schema: schema,
initial: { initial: data,
type: currentType, onCancel: () => {
refRaw: refText, setIsEditing(false);
text: hintText, setTimeout(() => {
basePosition: basePosition, thisRef.current?.view?.focus();
mainRefs: mainRefs }, 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<ReactCodeMirrorRef>);
wrap.replaceWith(referenceText);
}
return ( return (
<div className={clsx('flex flex-col gap-2', cursor)}> <div className={clsx('flex flex-col gap-2', cursor)}>
<Label text={label} /> <Label text={label} />
@ -210,10 +208,10 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
basicSetup={editorSetup} basicSetup={editorSetup}
theme={customTheme} theme={customTheme}
extensions={editorExtensions} extensions={editorExtensions}
value={isFocused ? value : value !== initialValue || isActive ? value : resolved} value={isFocused ? value : value !== initialValue || isEditing ? value : resolved}
indentWithTab={false} indentWithTab={false}
onChange={handleChange} onChange={onChange}
editable={!disabled && !isActive} editable={!disabled && !isEditing}
onKeyDown={handleInput} onKeyDown={handleInput}
onFocus={handleFocusIn} onFocus={handleFocusIn}
onBlur={handleFocusOut} onBlur={handleFocusOut}
@ -223,5 +221,3 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
); );
} }
); );
export default RefsInput;

View File

@ -1 +1 @@
export { default } from './RefsInput'; export { RefsInput } from './RefsInput';

View File

@ -1,7 +1,10 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import clsx from 'clsx'; import clsx from 'clsx';
import { z } from 'zod';
import { HelpTopic } from '@/features/help'; import { HelpTopic } from '@/features/help';
@ -10,11 +13,17 @@ import { TabLabel, TabList, TabPanel, Tabs } from '@/components/Tabs';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { labelReferenceType } from '../../labels'; import { labelReferenceType } from '../../labels';
import { ReferenceType } from '../../models/language'; import { IReference, ReferenceType } from '../../models/language';
import {
parseEntityReference,
parseGrammemes,
parseSyntacticReference,
supportedGrammeOptions
} from '../../models/languageAPI';
import { IRSForm } from '../../models/rsform'; import { IRSForm } from '../../models/rsform';
import TabEntityReference from './TabEntityReference'; import { TabEntityReference } from './TabEntityReference';
import TabSyntacticReference from './TabSyntacticReference'; import { TabSyntacticReference } from './TabSyntacticReference';
export interface IReferenceInputState { export interface IReferenceInputState {
type: ReferenceType; type: ReferenceType;
@ -24,10 +33,25 @@ export interface IReferenceInputState {
basePosition: number; basePosition: number;
} }
const schemaEditReferenceState = z
.object({
type: z.nativeEnum(ReferenceType),
entity: z.object({ entity: z.string(), grams: z.array(z.object({ value: z.string(), label: z.string() })) }),
syntactic: z.object({ offset: z.coerce.number(), nominal: z.string() })
})
.refine(
data =>
(data.type !== ReferenceType.SYNTACTIC || (data.syntactic.offset !== 0 && data.syntactic.nominal !== '')) &&
(data.type !== ReferenceType.ENTITY || (data.entity.entity !== '' && data.entity.grams.length > 0))
);
export type IEditReferenceState = z.infer<typeof schemaEditReferenceState>;
export interface DlgEditReferenceProps { export interface DlgEditReferenceProps {
schema: IRSForm; schema: IRSForm;
initial: IReferenceInputState; initial: IReferenceInputState;
onSave: (newRef: string) => void; onSave: (newRef: IReference) => void;
onCancel: () => void;
} }
export enum TabID { export enum TabID {
@ -36,22 +60,45 @@ export enum TabID {
} }
function DlgEditReference() { function DlgEditReference() {
const { initial, onSave } = useDialogsStore(state => state.props as DlgEditReferenceProps); const { initial, onSave, onCancel } = useDialogsStore(state => state.props as DlgEditReferenceProps);
const [activeTab, setActiveTab] = useState(initial.type === ReferenceType.ENTITY ? TabID.ENTITY : TabID.SYNTACTIC); const [activeTab, setActiveTab] = useState(initial.type === ReferenceType.ENTITY ? TabID.ENTITY : TabID.SYNTACTIC);
const [reference, setReference] = useState('');
const [isValid, setIsValid] = useState(false);
function handleSubmit() { const methods = useForm<IEditReferenceState>({
onSave(reference); resolver: zodResolver(schemaEditReferenceState),
return true; defaultValues: {
type: initial.type,
entity: initEntityReference(initial),
syntactic: initSyntacticReference(initial)
},
mode: 'onChange'
});
function onSubmit(data: IEditReferenceState) {
if (data.type === ReferenceType.ENTITY) {
onSave({
type: data.type,
data: {
entity: data.entity.entity,
form: data.entity.grams.map(gram => gram.value).join(',')
}
});
} else {
onSave({ type: data.type, data: data.syntactic });
}
}
function handleChangeTab(tab: TabID) {
methods.setValue('type', tab === TabID.ENTITY ? ReferenceType.ENTITY : ReferenceType.SYNTACTIC);
setActiveTab(tab);
} }
return ( return (
<ModalForm <ModalForm
header='Редактирование ссылки' header='Редактирование ссылки'
submitText='Сохранить ссылку' submitText='Сохранить ссылку'
canSubmit={isValid} canSubmit={methods.formState.isValid}
onSubmit={handleSubmit} onCancel={onCancel}
onSubmit={event => void methods.handleSubmit(onSubmit)(event)}
className='w-[40rem] px-6 h-[32rem]' className='w-[40rem] px-6 h-[32rem]'
helpTopic={HelpTopic.TERM_CONTROL} helpTopic={HelpTopic.TERM_CONTROL}
> >
@ -59,7 +106,7 @@ function DlgEditReference() {
selectedTabClassName='clr-selected' selectedTabClassName='clr-selected'
className='flex flex-col' className='flex flex-col'
selectedIndex={activeTab} selectedIndex={activeTab}
onSelect={setActiveTab} onSelect={handleChangeTab}
> >
<TabList className={clsx('mb-3 self-center', 'flex', 'border divide-x rounded-none', 'bg-prim-200')}> <TabList className={clsx('mb-3 self-center', 'flex', 'border divide-x rounded-none', 'bg-prim-200')}>
<TabLabel title='Отсылка на термин в заданной словоформе' label={labelReferenceType(ReferenceType.ENTITY)} /> <TabLabel title='Отсылка на термин в заданной словоформе' label={labelReferenceType(ReferenceType.ENTITY)} />
@ -69,16 +116,47 @@ function DlgEditReference() {
/> />
</TabList> </TabList>
<FormProvider {...methods}>
<TabPanel> <TabPanel>
<TabEntityReference onChangeReference={setReference} onChangeValid={setIsValid} /> <TabEntityReference />
</TabPanel> </TabPanel>
<TabPanel> <TabPanel>
<TabSyntacticReference onChangeReference={setReference} onChangeValid={setIsValid} /> <TabSyntacticReference />
</TabPanel> </TabPanel>
</FormProvider>
</Tabs> </Tabs>
</ModalForm> </ModalForm>
); );
} }
export default DlgEditReference; export default DlgEditReference;
// ====== Internals =========
function initEntityReference(initial: IReferenceInputState) {
if (!initial.refRaw || initial.type === ReferenceType.SYNTACTIC) {
return {
entity: '',
grams: []
};
} else {
const ref = parseEntityReference(initial.refRaw);
const grams = parseGrammemes(ref.form);
const supported = supportedGrammeOptions.filter(data => grams.includes(data.value));
return {
entity: ref.entity,
grams: supported
};
}
}
function initSyntacticReference(initial: IReferenceInputState) {
if (!initial.refRaw || initial.type === ReferenceType.ENTITY) {
return {
offset: 1,
nominal: initial.text ?? ''
};
} else {
return parseSyntacticReference(initial.refRaw);
}
}

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useEffect, useState } from 'react'; import { Controller, useFormContext, useWatch } from 'react-hook-form';
import { Label, TextInput } from '@/components/Input'; import { Label, TextInput } from '@/components/Input';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
@ -8,51 +8,22 @@ import { useDialogsStore } from '@/stores/dialogs';
import { PickConstituenta } from '../../components/PickConstituenta'; import { PickConstituenta } from '../../components/PickConstituenta';
import SelectMultiGrammeme from '../../components/SelectMultiGrammeme'; import SelectMultiGrammeme from '../../components/SelectMultiGrammeme';
import SelectWordForm from '../../components/SelectWordForm'; import SelectWordForm from '../../components/SelectWordForm';
import { IGrammemeOption, ReferenceType } from '../../models/language';
import { parseEntityReference, parseGrammemes, supportedGrammeOptions } from '../../models/languageAPI';
import { IConstituenta } from '../../models/rsform'; import { IConstituenta } from '../../models/rsform';
import { matchConstituenta } from '../../models/rsformAPI'; import { matchConstituenta } from '../../models/rsformAPI';
import { CstMatchMode } from '../../stores/cstSearch'; import { CstMatchMode } from '../../stores/cstSearch';
import { DlgEditReferenceProps } from './DlgEditReference'; import { DlgEditReferenceProps, IEditReferenceState } from './DlgEditReference';
interface TabEntityReferenceProps { export function TabEntityReference() {
onChangeValid: (newValue: boolean) => void;
onChangeReference: (newValue: string) => void;
}
function TabEntityReference({ onChangeValid, onChangeReference }: TabEntityReferenceProps) {
const { schema, initial } = useDialogsStore(state => state.props as DlgEditReferenceProps); const { schema, initial } = useDialogsStore(state => state.props as DlgEditReferenceProps);
const [selectedCst, setSelectedCst] = useState<IConstituenta | undefined>(undefined); const { setValue, control, register } = useFormContext<IEditReferenceState>();
const [alias, setAlias] = useState(''); const alias = useWatch({ control, name: 'entity.entity' });
const [term, setTerm] = useState('');
const [selectedGrams, setSelectedGrams] = useState<IGrammemeOption[]>([]);
// Initialization const selectedCst = schema.cstByAlias.get(alias);
useEffect(() => { const term = selectedCst?.term_resolved ?? '';
if (!!initial.refRaw && initial.type === ReferenceType.ENTITY) {
const ref = parseEntityReference(initial.refRaw);
setAlias(ref.entity);
const grams = parseGrammemes(ref.form);
setSelectedGrams(supportedGrammeOptions.filter(data => grams.includes(data.value)));
}
}, [initial, schema.items]);
// Produce result
useEffect(() => {
onChangeValid(alias !== '' && selectedGrams.length > 0);
onChangeReference(`@{${alias}|${selectedGrams.map(gram => gram.value).join(',')}}`);
}, [alias, selectedGrams, onChangeValid, onChangeReference]);
// Update term when alias changes
useEffect(() => {
const cst = schema.cstByAlias.get(alias);
setTerm(cst?.term_resolved ?? '');
}, [alias, term, schema]);
function handleSelectConstituenta(cst: IConstituenta) { function handleSelectConstituenta(cst: IConstituenta) {
setAlias(cst.alias); setValue('entity.entity', cst.alias);
setSelectedCst(cst);
} }
return ( return (
@ -76,8 +47,7 @@ function TabEntityReference({ onChangeValid, onChangeReference }: TabEntityRefer
label='Конституента' label='Конституента'
placeholder='Имя' placeholder='Имя'
className='w-[11rem]' className='w-[11rem]'
value={alias} {...register('entity.entity')}
onChange={event => setAlias(event.target.value)}
/> />
<TextInput <TextInput
id='dlg_reference_term' id='dlg_reference_term'
@ -91,21 +61,29 @@ function TabEntityReference({ onChangeValid, onChangeReference }: TabEntityRefer
/> />
</div> </div>
<SelectWordForm value={selectedGrams} onChange={setSelectedGrams} /> <Controller
control={control}
name='entity.grams'
render={({ field }) => <SelectWordForm value={field.value} onChange={field.onChange} />}
/>
<div className='flex items-center gap-4'> <div className='flex items-center gap-4'>
<Label text='Словоформа' /> <Label text='Словоформа' />
<Controller
control={control}
name='entity.grams'
render={({ field }) => (
<SelectMultiGrammeme <SelectMultiGrammeme
id='dlg_reference_grammemes' id='dlg_reference_grammemes'
placeholder='Выберите граммемы' placeholder='Выберите граммемы'
className='flex-grow' className='flex-grow'
menuPlacement='top' menuPlacement='top'
value={selectedGrams} value={field.value}
onChange={setSelectedGrams} onChange={field.onChange}
/>
)}
/> />
</div> </div>
</div> </div>
); );
} }
export default TabEntityReference;

View File

@ -1,24 +1,16 @@
'use client'; 'use client';
import { useEffect, useState } from 'react'; import { useFormContext, useWatch } from 'react-hook-form';
import { TextInput } from '@/components/Input'; import { TextInput } from '@/components/Input';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { ReferenceType } from '../../models/language'; import { DlgEditReferenceProps, IEditReferenceState } from './DlgEditReference';
import { parseSyntacticReference } from '../../models/languageAPI';
import { DlgEditReferenceProps } from './DlgEditReference'; export function TabSyntacticReference() {
interface TabSyntacticReferenceProps {
onChangeValid: (newValue: boolean) => void;
onChangeReference: (newValue: string) => void;
}
function TabSyntacticReference({ onChangeValid, onChangeReference }: TabSyntacticReferenceProps) {
const { initial } = useDialogsStore(state => state.props as DlgEditReferenceProps); const { initial } = useDialogsStore(state => state.props as DlgEditReferenceProps);
const [nominal, setNominal] = useState(''); const { control, register } = useFormContext<IEditReferenceState>();
const [offset, setOffset] = useState(1); const offset = useWatch({ control, name: 'syntactic.offset' });
const mainLink = (() => { const mainLink = (() => {
const position = offset > 0 ? initial.basePosition + (offset - 1) : initial.basePosition + offset; 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 ( return (
<div className='cc-fade-in flex flex-col gap-2'> <div className='cc-fade-in flex flex-col gap-2'>
<TextInput <TextInput
@ -52,8 +29,7 @@ function TabSyntacticReference({ onChangeValid, onChangeReference }: TabSyntacti
dense dense
label='Смещение' label='Смещение'
className='max-w-[10rem]' className='max-w-[10rem]'
value={offset} {...register('syntactic.offset')}
onChange={event => setOffset(event.target.valueAsNumber)}
/> />
<TextInput <TextInput
id='dlg_main_ref' id='dlg_main_ref'
@ -68,11 +44,8 @@ function TabSyntacticReference({ onChangeValid, onChangeReference }: TabSyntacti
spellCheck spellCheck
label='Начальная форма' label='Начальная форма'
placeholder='зависимое слово в начальной форме' placeholder='зависимое слово в начальной форме'
value={nominal} {...register('syntactic.nominal')}
onChange={event => setNominal(event.target.value)}
/> />
</div> </div>
); );
} }
export default TabSyntacticReference;

View File

@ -2,6 +2,8 @@
* Module: Natural language model declarations. * Module: Natural language model declarations.
*/ */
import { z } from 'zod';
/** /**
* Represents single unit of language Morphology. * Represents single unit of language Morphology.
*/ */
@ -266,10 +268,15 @@ export interface ITextPosition {
finish: number; 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. * Represents abstract reference data.
*/ */
export interface IReference { export type IReference = z.infer<typeof schemaReference>;
type: ReferenceType;
data: IEntityReference | ISyntacticReference;
}

View File

@ -10,9 +10,11 @@ import {
GrammemeGroups, GrammemeGroups,
IEntityReference, IEntityReference,
IGrammemeOption, IGrammemeOption,
IReference,
ISyntacticReference, ISyntacticReference,
IWordForm, IWordForm,
NounGrams, NounGrams,
ReferenceType,
supportedGrammemes, supportedGrammemes,
VerbGrams VerbGrams
} from './language'; } from './language';
@ -128,3 +130,19 @@ export const supportedGrammeOptions: IGrammemeOption[] = supportedGrammemes.map(
value: gram, value: gram,
label: labelGrammeme(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}}`;
}
}
}

View File

@ -19,7 +19,7 @@ import { errorMsg } from '@/utils/labels';
import { ICstUpdateDTO, schemaCstUpdate } from '../../../backend/types'; import { ICstUpdateDTO, schemaCstUpdate } from '../../../backend/types';
import { useCstUpdate } from '../../../backend/useCstUpdate'; import { useCstUpdate } from '../../../backend/useCstUpdate';
import { useMutatingRSForm } from '../../../backend/useMutatingRSForm'; import { useMutatingRSForm } from '../../../backend/useMutatingRSForm';
import RefsInput from '../../../components/RefsInput'; import { RefsInput } from '../../../components/RefsInput';
import { labelCstTypification, labelTypification } from '../../../labels'; import { labelCstTypification, labelTypification } from '../../../labels';
import { CstType, IConstituenta, IRSForm } from '../../../models/rsform'; import { CstType, IConstituenta, IRSForm } from '../../../models/rsform';
import { isBaseSet, isBasicConcept, isFunctional } from '../../../models/rsformAPI'; import { isBaseSet, isBasicConcept, isFunctional } from '../../../models/rsformAPI';
@ -125,7 +125,7 @@ function FormConstituenta({ disabled, id, toggleReset, schema, activeCst, onOpen
placeholder='Обозначение для текстовых определений' placeholder='Обозначение для текстовых определений'
schema={schema} schema={schema}
onOpenEdit={onOpenEdit} onOpenEdit={onOpenEdit}
value={field.value} value={field.value ?? ''}
initialValue={activeCst.term_raw} initialValue={activeCst.term_raw}
resolved={activeCst.term_resolved} resolved={activeCst.term_resolved}
disabled={disabled} disabled={disabled}
@ -189,7 +189,7 @@ function FormConstituenta({ disabled, id, toggleReset, schema, activeCst, onOpen
maxHeight='8rem' maxHeight='8rem'
schema={schema} schema={schema}
onOpenEdit={onOpenEdit} onOpenEdit={onOpenEdit}
value={field.value} value={field.value ?? ''}
initialValue={activeCst.definition_raw} initialValue={activeCst.definition_raw}
resolved={activeCst.definition_resolved} resolved={activeCst.definition_resolved}
disabled={disabled} disabled={disabled}

View File

@ -52,6 +52,10 @@ export enum DialogType {
UPLOAD_RSFORM UPLOAD_RSFORM
} }
export interface GenericDialogProps {
onHide?: () => void;
}
interface DialogsStore { interface DialogsStore {
active: DialogType | undefined; active: DialogType | undefined;
props: unknown; props: unknown;
@ -85,7 +89,12 @@ interface DialogsStore {
export const useDialogsStore = create<DialogsStore>()(set => ({ export const useDialogsStore = create<DialogsStore>()(set => ({
active: undefined, active: undefined,
props: 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 }), showCstTemplate: props => set({ active: DialogType.CONSTITUENTA_TEMPLATE, props: props }),
showCreateCst: props => set({ active: DialogType.CREATE_CONSTITUENTA, props: props }), showCreateCst: props => set({ active: DialogType.CREATE_CONSTITUENTA, props: props }),