mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
F: Rework reference editor dialog
This commit is contained in:
parent
5afe8ac86e
commit
00fd2beea3
|
@ -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>
|
||||||
|
|
|
@ -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;
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export { default } from './RefsInput';
|
export { RefsInput } from './RefsInput';
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
|
|
|
@ -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;
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -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}}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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 }),
|
||||||
|
|
Loading…
Reference in New Issue
Block a user