F: Rework cst create and template dialogs

This commit is contained in:
Ivan 2025-02-14 02:41:31 +03:00
parent 3c92e07d0f
commit d5854366a9
9 changed files with 351 additions and 320 deletions

View File

@ -4,7 +4,7 @@ import { ILibraryItemReference, ILibraryItemVersioned } from '@/features/library
import { errorMsg } from '@/utils/labels'; import { errorMsg } from '@/utils/labels';
import { CstType, IConstituentaMeta, IInheritanceInfo, TermForm } from '../models/rsform'; import { CstType, IConstituentaMeta, IInheritanceInfo } from '../models/rsform';
import { IArgumentInfo, ParsingStatus, ValueClass } from '../models/rslang'; import { IArgumentInfo, ParsingStatus, ValueClass } from '../models/rslang';
/** /**
@ -42,17 +42,21 @@ export interface IRSFormUploadDTO {
/** /**
* Represents {@link IConstituenta} data, used in creation process. * Represents {@link IConstituenta} data, used in creation process.
*/ */
export interface ICstCreateDTO { export const schemaCstCreate = z.object({
alias: string; cst_type: z.nativeEnum(CstType),
cst_type: CstType; alias: z.string().nonempty(errorMsg.requiredField),
definition_raw: string; convention: z.string(),
term_raw: string; definition_formal: z.string(),
convention: string; definition_raw: z.string(),
definition_formal: string; term_raw: z.string(),
term_forms: TermForm[]; term_forms: z.array(z.object({ text: z.string(), tags: z.string() })),
insert_after: z.number().nullable()
});
insert_after: number | null; /**
} * Represents {@link IConstituenta} data, used in creation process.
*/
export type ICstCreateDTO = z.infer<typeof schemaCstCreate>;
/** /**
* Represents data response when creating {@link IConstituenta}. * Represents data response when creating {@link IConstituenta}.

View File

@ -1,54 +1,53 @@
'use client'; 'use client';
import { useState } from 'react'; import { FormProvider, useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { ModalForm } from '@/components/Modal'; import { ModalForm } from '@/components/Modal';
import usePartialUpdate from '@/hooks/usePartialUpdate';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { errorMsg } from '@/utils/labels';
import { ICstCreateDTO } from '../../backend/types'; import { ICstCreateDTO, schemaCstCreate } from '../../backend/types';
import { CstType, IRSForm } from '../../models/rsform'; import { useCstCreate } from '../../backend/useCstCreate';
import { generateAlias } from '../../models/rsformAPI'; import { IConstituentaMeta, IRSForm } from '../../models/rsform';
import { validateNewAlias } from '../../models/rsformAPI';
import FormCreateCst from './FormCreateCst'; import FormCreateCst from './FormCreateCst';
export interface DlgCreateCstProps { export interface DlgCreateCstProps {
initial?: ICstCreateDTO; initial: ICstCreateDTO;
schema: IRSForm; schema: IRSForm;
onCreate: (data: ICstCreateDTO) => void; onCreate: (data: IConstituentaMeta) => void;
} }
function DlgCreateCst() { function DlgCreateCst() {
const { initial, schema, onCreate } = useDialogsStore(state => state.props as DlgCreateCstProps); const { initial, schema, onCreate } = useDialogsStore(state => state.props as DlgCreateCstProps);
const { cstCreate } = useCstCreate();
const [validated, setValidated] = useState(false); const methods = useForm<ICstCreateDTO>({
const [cstData, updateCstData] = usePartialUpdate( resolver: zodResolver(schemaCstCreate),
initial || { defaultValues: { ...initial }
cst_type: CstType.BASE, });
insert_after: null, const alias = useWatch({ control: methods.control, name: 'alias' });
alias: generateAlias(CstType.BASE, schema), const cst_type = useWatch({ control: methods.control, name: 'cst_type' });
convention: '', const isValid = alias !== initial.alias && validateNewAlias(alias, cst_type, schema);
definition_formal: '',
definition_raw: '',
term_raw: '',
term_forms: []
}
);
const handleSubmit = () => { function onSubmit(data: ICstCreateDTO) {
onCreate(cstData); return cstCreate({ itemID: schema.id, data }).then(onCreate);
return true; }
};
return ( return (
<ModalForm <ModalForm
header='Создание конституенты' header='Создание конституенты'
canSubmit={validated} canSubmit={isValid}
onSubmit={handleSubmit} onSubmit={event => void methods.handleSubmit(onSubmit)(event)}
submitInvalidTooltip={errorMsg.aliasInvalid}
submitText='Создать' submitText='Создать'
className='cc-column w-[35rem] max-h-[30rem] py-2 px-6' className='cc-column w-[35rem] max-h-[30rem] py-2 px-6'
> >
<FormCreateCst schema={schema} state={cstData} partialUpdate={updateCstData} setValidated={setValidated} /> <FormProvider {...methods}>
<FormCreateCst schema={schema} />
</FormProvider>
</ModalForm> </ModalForm>
); );
} }

View File

@ -1,6 +1,7 @@
'use client'; 'use client';
import { useEffect, useState } from 'react'; import { useState } from 'react';
import { Controller, useFormContext, useWatch } from 'react-hook-form';
import clsx from 'clsx'; import clsx from 'clsx';
import { BadgeHelp, HelpTopic } from '@/features/help'; import { BadgeHelp, HelpTopic } from '@/features/help';
@ -12,48 +13,45 @@ import { ICstCreateDTO } from '../../backend/types';
import RSInput from '../../components/RSInput'; import RSInput from '../../components/RSInput';
import { SelectCstType } from '../../components/SelectCstType'; import { SelectCstType } from '../../components/SelectCstType';
import { CstType, IRSForm } from '../../models/rsform'; import { CstType, IRSForm } from '../../models/rsform';
import { generateAlias, isBaseSet, isBasicConcept, isFunctional, validateNewAlias } from '../../models/rsformAPI'; import { generateAlias, isBaseSet, isBasicConcept, isFunctional } from '../../models/rsformAPI';
interface FormCreateCstProps { interface FormCreateCstProps {
schema: IRSForm; schema: IRSForm;
state: ICstCreateDTO;
partialUpdate: React.Dispatch<Partial<ICstCreateDTO>>;
setValidated?: React.Dispatch<React.SetStateAction<boolean>>;
} }
function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreateCstProps) { function FormCreateCst({ schema }: FormCreateCstProps) {
const {
setValue,
register,
control,
formState: { errors }
} = useFormContext<ICstCreateDTO>();
const [forceComment, setForceComment] = useState(false); const [forceComment, setForceComment] = useState(false);
const isBasic = isBasicConcept(state.cst_type); const cst_type = useWatch({ control, name: 'cst_type' });
const isElementary = isBaseSet(state.cst_type); const convention = useWatch({ control, name: 'convention' });
const showConvention = !!state.convention || forceComment || isBasic; const isBasic = isBasicConcept(cst_type);
const isElementary = isBaseSet(cst_type);
useEffect(() => { const isFunction = isFunctional(cst_type);
setForceComment(false); const showConvention = !!convention || forceComment || isBasic;
}, [state.cst_type, partialUpdate, schema]);
useEffect(() => {
if (setValidated) {
setValidated(validateNewAlias(state.alias, state.cst_type, schema));
}
}, [state.alias, state.cst_type, schema, setValidated]);
function handleTypeChange(target: CstType) { function handleTypeChange(target: CstType) {
return partialUpdate({ cst_type: target, alias: generateAlias(target, schema) }); setValue('cst_type', target);
setValue('alias', generateAlias(target, schema));
setForceComment(false);
} }
return ( return (
<> <>
<div className='flex items-center self-center gap-3'> <div className='flex items-center self-center gap-3'>
<SelectCstType id='dlg_cst_type' className='w-[16rem]' value={state.cst_type} onChange={handleTypeChange} /> <SelectCstType id='dlg_cst_type' className='w-[16rem]' value={cst_type} onChange={handleTypeChange} />
<TextInput <TextInput
id='dlg_cst_alias' id='dlg_cst_alias'
dense dense
label='Имя' label='Имя'
className='w-[7rem]' className='w-[7rem]'
value={state.alias} {...register('alias')}
onChange={event => partialUpdate({ alias: event.target.value })} error={errors.alias}
/> />
<BadgeHelp <BadgeHelp
topic={HelpTopic.CC_CONSTITUENTA} topic={HelpTopic.CC_CONSTITUENTA}
@ -69,42 +67,58 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
label='Термин' label='Термин'
placeholder='Обозначение для текстовых определений' placeholder='Обозначение для текстовых определений'
className='max-h-[3.6rem]' className='max-h-[3.6rem]'
value={state.term_raw} {...register('term_raw')}
onChange={event => partialUpdate({ term_raw: event.target.value })} error={errors.term_raw}
/> />
{!!state.definition_formal || !isElementary ? ( <Controller
<RSInput control={control}
id='dlg_cst_expression' name='definition_formal'
noTooltip render={({ field }) =>
label={ !!field.value || !isElementary ? (
state.cst_type === CstType.STRUCTURED <RSInput
? 'Область определения' id='dlg_cst_expression'
: isFunctional(state.cst_type) noTooltip
? 'Определение функции' label={
: 'Формальное определение' cst_type === CstType.STRUCTURED
} ? 'Область определения'
placeholder={ : isFunction
state.cst_type !== CstType.STRUCTURED ? 'Родоструктурное выражение' : 'Типизация родовой структуры' ? 'Определение функции'
} : 'Формальное определение'
value={state.definition_formal} }
onChange={value => partialUpdate({ definition_formal: value })} placeholder={
schema={schema} cst_type !== CstType.STRUCTURED ? 'Родоструктурное выражение' : 'Типизация родовой структуры'
/> }
) : null} value={field.value}
onChange={field.onChange}
schema={schema}
/>
) : (
<></>
)
}
/>
{!!state.definition_raw || !isElementary ? ( <Controller
<TextArea control={control}
id='dlg_cst_definition' name='definition_raw'
spellCheck render={({ field }) =>
fitContent !!field.value || !isElementary ? (
label='Текстовое определение' <TextArea
placeholder='Текстовая интерпретация формального выражения' id='dlg_cst_definition'
className='max-h-[3.6rem]' spellCheck
value={state.definition_raw} fitContent
onChange={event => partialUpdate({ definition_raw: event.target.value })} label='Текстовое определение'
/> placeholder='Текстовая интерпретация формального выражения'
) : null} className='max-h-[3.6rem]'
value={field.value}
onChange={field.onChange}
/>
) : (
<></>
)
}
/>
{!showConvention ? ( {!showConvention ? (
<button <button
@ -124,8 +138,7 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
label={isBasic ? 'Конвенция' : 'Комментарий'} label={isBasic ? 'Конвенция' : 'Комментарий'}
placeholder={isBasic ? 'Договоренность об интерпретации' : 'Пояснение разработчика'} placeholder={isBasic ? 'Договоренность об интерпретации' : 'Пояснение разработчика'}
className='max-h-[5.4rem]' className='max-h-[5.4rem]'
value={state.convention} {...register('convention')}
onChange={event => partialUpdate({ convention: event.target.value })}
/> />
)} )}
</> </>

View File

@ -1,6 +1,8 @@
'use client'; 'use client';
import { Suspense, useEffect, useState } from 'react'; import { Suspense, useState } from 'react';
import { FormProvider, useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import clsx from 'clsx'; import clsx from 'clsx';
import { HelpTopic } from '@/features/help'; import { HelpTopic } from '@/features/help';
@ -8,23 +10,21 @@ import { HelpTopic } from '@/features/help';
import { Loader } from '@/components/Loader'; import { Loader } from '@/components/Loader';
import { ModalForm } from '@/components/Modal'; import { ModalForm } from '@/components/Modal';
import { TabLabel, TabList, TabPanel, Tabs } from '@/components/Tabs'; import { TabLabel, TabList, TabPanel, Tabs } from '@/components/Tabs';
import usePartialUpdate from '@/hooks/usePartialUpdate';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { promptText } from '@/utils/labels';
import { ICstCreateDTO } from '../../backend/types'; import { ICstCreateDTO, schemaCstCreate } from '../../backend/types';
import { useRSForm } from '../../backend/useRSForm'; import { useCstCreate } from '../../backend/useCstCreate';
import { CstType, IRSForm } from '../../models/rsform'; import { CstType, IConstituentaMeta, IRSForm } from '../../models/rsform';
import { generateAlias, validateNewAlias } from '../../models/rsformAPI'; import { generateAlias, validateNewAlias } from '../../models/rsformAPI';
import { inferTemplatedType, substituteTemplateArgs } from '../../models/rslangAPI';
import FormCreateCst from '../DlgCreateCst/FormCreateCst'; import FormCreateCst from '../DlgCreateCst/FormCreateCst';
import TabArguments, { IArgumentsState } from './TabArguments'; import TabArguments from './TabArguments';
import TabTemplate, { ITemplateState } from './TabTemplate'; import TabTemplate from './TabTemplate';
import { TemplateState } from './TemplateContext';
export interface DlgCstTemplateProps { export interface DlgCstTemplateProps {
schema: IRSForm; schema: IRSForm;
onCreate: (data: ICstCreateDTO) => void; onCreate: (data: IConstituentaMeta) => void;
insertAfter?: number; insertAfter?: number;
} }
@ -36,98 +36,38 @@ export enum TabID {
function DlgCstTemplate() { function DlgCstTemplate() {
const { schema, onCreate, insertAfter } = useDialogsStore(state => state.props as DlgCstTemplateProps); const { schema, onCreate, insertAfter } = useDialogsStore(state => state.props as DlgCstTemplateProps);
const { cstCreate } = useCstCreate();
const methods = useForm<ICstCreateDTO>({
resolver: zodResolver(schemaCstCreate),
defaultValues: {
cst_type: CstType.TERM,
insert_after: insertAfter ?? null,
alias: generateAlias(CstType.TERM, schema),
convention: '',
definition_formal: '',
definition_raw: '',
term_raw: '',
term_forms: []
}
});
const alias = useWatch({ control: methods.control, name: 'alias' });
const cst_type = useWatch({ control: methods.control, name: 'cst_type' });
const isValid = validateNewAlias(alias, cst_type, schema);
const [activeTab, setActiveTab] = useState(TabID.TEMPLATE); const [activeTab, setActiveTab] = useState(TabID.TEMPLATE);
const [template, updateTemplate] = usePartialUpdate<ITemplateState>({}); function onSubmit(data: ICstCreateDTO) {
const { schema: templateSchema } = useRSForm({ itemID: template.templateID }); return cstCreate({ itemID: schema.id, data }).then(onCreate);
const [substitutes, updateSubstitutes] = usePartialUpdate<IArgumentsState>({
definition: '',
arguments: []
});
const [constituenta, updateConstituenta] = usePartialUpdate<ICstCreateDTO>({
cst_type: CstType.TERM,
insert_after: insertAfter ?? null,
alias: generateAlias(CstType.TERM, schema),
convention: '',
definition_formal: '',
definition_raw: '',
term_raw: '',
term_forms: []
});
const [validated, setValidated] = useState(false);
function handleSubmit() {
onCreate(constituenta);
return true;
} }
function handlePrompt(): boolean {
const definedSomeArgs = substitutes.arguments.some(arg => !!arg.value);
if (!definedSomeArgs && !window.confirm(promptText.templateUndefined)) {
return false;
}
return true;
}
useEffect(() => {
if (!template.prototype) {
updateConstituenta({
definition_raw: '',
definition_formal: '',
term_raw: ''
});
updateSubstitutes({
definition: '',
arguments: []
});
} else {
updateConstituenta({
cst_type: template.prototype.cst_type,
alias: generateAlias(template.prototype.cst_type, schema),
definition_raw: template.prototype.definition_raw,
definition_formal: template.prototype.definition_formal,
term_raw: template.prototype.term_raw
});
updateSubstitutes({
definition: template.prototype.definition_formal,
arguments: template.prototype.parse.args.map(arg => ({
alias: arg.alias,
typification: arg.typification,
value: ''
}))
});
}
}, [template.prototype, updateConstituenta, updateSubstitutes, schema]);
useEffect(() => {
if (substitutes.arguments.length === 0 || !template.prototype) {
return;
}
const definition = substituteTemplateArgs(template.prototype.definition_formal, substitutes.arguments);
const type = inferTemplatedType(template.prototype.cst_type, substitutes.arguments);
updateConstituenta({
cst_type: type,
alias: generateAlias(type, schema),
definition_formal: definition
});
updateSubstitutes({
definition: definition
});
}, [substitutes.arguments, template.prototype, updateConstituenta, updateSubstitutes, schema]);
useEffect(() => {
setValidated(!!template.prototype && validateNewAlias(constituenta.alias, constituenta.cst_type, schema));
}, [constituenta.alias, constituenta.cst_type, schema, template.prototype]);
return ( return (
<ModalForm <ModalForm
header='Создание конституенты из шаблона' header='Создание конституенты из шаблона'
submitText='Создать' submitText='Создать'
className='w-[43rem] h-[35rem] px-6' className='w-[43rem] h-[35rem] px-6'
canSubmit={validated} canSubmit={isValid}
beforeSubmit={handlePrompt} onSubmit={event => void methods.handleSubmit(onSubmit)(event)}
onSubmit={handleSubmit}
helpTopic={HelpTopic.RSL_TEMPLATES} helpTopic={HelpTopic.RSL_TEMPLATES}
> >
<Tabs <Tabs
@ -142,21 +82,25 @@ function DlgCstTemplate() {
<TabLabel label='Конституента' title='Редактирование конституенты' className='w-[8rem]' /> <TabLabel label='Конституента' title='Редактирование конституенты' className='w-[8rem]' />
</TabList> </TabList>
<TabPanel> <FormProvider {...methods}>
<Suspense fallback={<Loader />}> <TemplateState>
<TabTemplate state={template} partialUpdate={updateTemplate} templateSchema={templateSchema} /> <TabPanel>
</Suspense> <Suspense fallback={<Loader />}>
</TabPanel> <TabTemplate />
</Suspense>
</TabPanel>
<TabPanel> <TabPanel>
<TabArguments schema={schema} state={substitutes} partialUpdate={updateSubstitutes} /> <TabArguments />
</TabPanel> </TabPanel>
<TabPanel> <TabPanel>
<div className='cc-fade-in cc-column'> <div className='cc-fade-in cc-column'>
<FormCreateCst state={constituenta} partialUpdate={updateConstituenta} schema={schema} /> <FormCreateCst schema={schema} />
</div> </div>
</TabPanel> </TabPanel>
</TemplateState>
</FormProvider>
</Tabs> </Tabs>
</ModalForm> </ModalForm>
); );

View File

@ -1,6 +1,7 @@
'use client'; 'use client';
import { useEffect, useState } from 'react'; import { useState } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
import { createColumnHelper } from '@tanstack/react-table'; import { createColumnHelper } from '@tanstack/react-table';
import clsx from 'clsx'; import clsx from 'clsx';
@ -8,37 +9,32 @@ import { MiniButton } from '@/components/Control';
import DataTable, { IConditionalStyle } from '@/components/DataTable'; import DataTable, { IConditionalStyle } from '@/components/DataTable';
import { IconAccept, IconRemove, IconReset } from '@/components/Icons'; import { IconAccept, IconRemove, IconReset } from '@/components/Icons';
import { NoData } from '@/components/View'; import { NoData } from '@/components/View';
import { useDialogsStore } from '@/stores/dialogs';
import { APP_COLORS } from '@/styling/colors'; import { APP_COLORS } from '@/styling/colors';
import { ICstCreateDTO } from '../../backend/types';
import { PickConstituenta } from '../../components/PickConstituenta'; import { PickConstituenta } from '../../components/PickConstituenta';
import RSInput from '../../components/RSInput'; import RSInput from '../../components/RSInput';
import { IConstituenta, IRSForm } from '../../models/rsform'; import { IConstituenta } from '../../models/rsform';
import { IArgumentValue } from '../../models/rslang'; import { IArgumentValue } from '../../models/rslang';
interface TabArgumentsProps { import { DlgCstTemplateProps } from './DlgCstTemplate';
state: IArgumentsState; import { useTemplateContext } from './TemplateContext';
schema: IRSForm;
partialUpdate: React.Dispatch<Partial<IArgumentsState>>;
}
export interface IArgumentsState {
arguments: IArgumentValue[];
definition: string;
}
const argumentsHelper = createColumnHelper<IArgumentValue>(); const argumentsHelper = createColumnHelper<IArgumentValue>();
function TabArguments({ state, schema, partialUpdate }: TabArgumentsProps) { function TabArguments() {
const [selectedCst, setSelectedCst] = useState<IConstituenta | undefined>(undefined); const { schema } = useDialogsStore(state => state.props as DlgCstTemplateProps);
const [selectedArgument, setSelectedArgument] = useState<IArgumentValue | undefined>(undefined); const { control } = useFormContext<ICstCreateDTO>();
const [argumentValue, setArgumentValue] = useState(''); const { args, onChangeArguments } = useTemplateContext();
const isModified = selectedArgument && argumentValue !== selectedArgument.value; const definition = useWatch({ control, name: 'definition_formal' });
useEffect(() => { const [selectedCst, setSelectedCst] = useState<IConstituenta | undefined>(undefined);
if (!selectedArgument && state.arguments.length > 0) { const [selectedArgument, setSelectedArgument] = useState<IArgumentValue | undefined>(
setSelectedArgument(state.arguments[0]); args.length > 0 ? args[0] : undefined
} );
}, [state.arguments, selectedArgument]);
const [argumentValue, setArgumentValue] = useState('');
function handleSelectArgument(arg: IArgumentValue) { function handleSelectArgument(arg: IArgumentValue) {
setSelectedArgument(arg); setSelectedArgument(arg);
@ -54,9 +50,7 @@ function TabArguments({ state, schema, partialUpdate }: TabArgumentsProps) {
function handleClearArgument(target: IArgumentValue) { function handleClearArgument(target: IArgumentValue) {
const newArg = { ...target, value: '' }; const newArg = { ...target, value: '' };
partialUpdate({ onChangeArguments(args.map(arg => (arg.alias !== target.alias ? arg : newArg)));
arguments: state.arguments.map(arg => (arg.alias !== target.alias ? arg : newArg))
});
setSelectedArgument(newArg); setSelectedArgument(newArg);
} }
@ -64,11 +58,9 @@ function TabArguments({ state, schema, partialUpdate }: TabArgumentsProps) {
setArgumentValue(selectedArgument?.value ?? ''); setArgumentValue(selectedArgument?.value ?? '');
} }
function handleAssignArgument(target: IArgumentValue, value: string) { function handleAssignArgument(target: IArgumentValue, argValue: string) {
const newArg = { ...target, value: value }; const newArg = { ...target, value: argValue };
partialUpdate({ onChangeArguments(args.map(arg => (arg.alias !== target.alias ? arg : newArg)));
arguments: state.arguments.map(arg => (arg.alias !== target.alias ? arg : newArg))
});
setSelectedArgument(newArg); setSelectedArgument(newArg);
} }
@ -139,7 +131,7 @@ function TabArguments({ state, schema, partialUpdate }: TabArgumentsProps) {
'border', 'border',
'select-none' 'select-none'
)} )}
data={state.arguments} data={args}
columns={columns} columns={columns}
conditionalRowStyles={conditionalRowStyles} conditionalRowStyles={conditionalRowStyles}
noDataComponent={<NoData className='min-h-[3.6rem]'>Аргументы отсутствуют</NoData>} noDataComponent={<NoData className='min-h-[3.6rem]'>Аргументы отсутствуют</NoData>}
@ -179,7 +171,6 @@ function TabArguments({ state, schema, partialUpdate }: TabArgumentsProps) {
title='Очистить поле' title='Очистить поле'
noHover noHover
className='py-0' className='py-0'
disabled={!isModified}
onClick={handleReset} onClick={handleReset}
icon={<IconReset size='1.5rem' className='icon-primary' />} icon={<IconReset size='1.5rem' className='icon-primary' />}
/> />
@ -201,7 +192,7 @@ function TabArguments({ state, schema, partialUpdate }: TabArgumentsProps) {
placeholder='Итоговое определение' placeholder='Итоговое определение'
className='mt-[1.2rem]' className='mt-[1.2rem]'
height='5.1rem' height='5.1rem'
value={state.definition} value={definition}
/> />
</div> </div>
); );

View File

@ -1,36 +1,44 @@
'use client'; 'use client';
import { Dispatch, useEffect, useState } from 'react';
import { useTemplatesSuspense } from '@/features/library'; import { useTemplatesSuspense } from '@/features/library';
import { SelectSingle, TextArea } from '@/components/Input'; import { SelectSingle, TextArea } from '@/components/Input';
import { useRSForm } from '../../backend/useRSForm';
import { PickConstituenta } from '../../components/PickConstituenta'; import { PickConstituenta } from '../../components/PickConstituenta';
import RSInput from '../../components/RSInput'; import RSInput from '../../components/RSInput';
import { CATEGORY_CST_TYPE, IConstituenta, IRSForm } from '../../models/rsform'; import { CATEGORY_CST_TYPE } from '../../models/rsform';
import { applyFilterCategory } from '../../models/rsformAPI'; import { applyFilterCategory } from '../../models/rsformAPI';
export interface ITemplateState { import { useTemplateContext } from './TemplateContext';
templateID?: number;
prototype?: IConstituenta;
filterCategory?: IConstituenta;
}
interface TabTemplateProps { function TabTemplate() {
state: ITemplateState; const {
partialUpdate: Dispatch<Partial<ITemplateState>>; templateID, //
templateSchema?: IRSForm; filterCategory,
} prototype,
onChangePrototype,
onChangeTemplateID,
onChangeFilterCategory
} = useTemplateContext();
function TabTemplate({ state, partialUpdate, templateSchema }: TabTemplateProps) {
const { templates } = useTemplatesSuspense(); const { templates } = useTemplatesSuspense();
const { schema: templateSchema } = useRSForm({ itemID: templateID });
const [filteredData, setFilteredData] = useState<IConstituenta[]>([]); if (!templateID) {
onChangeTemplateID(templates[0].id);
return null;
}
const prototypeInfo = !state.prototype const filteredData = !templateSchema
? []
: !filterCategory
? templateSchema.items
: applyFilterCategory(filterCategory, templateSchema);
const prototypeInfo = !prototype
? '' ? ''
: `${state.prototype?.term_raw}${state.prototype?.definition_raw ? `${state.prototype?.definition_raw}` : ''}`; : `${prototype?.term_raw}${prototype?.definition_raw ? `${prototype?.definition_raw}` : ''}`;
const templateSelector = templates.map(template => ({ const templateSelector = templates.map(template => ({
value: template.id, value: template.id,
@ -46,23 +54,6 @@ function TabTemplate({ state, partialUpdate, templateSchema }: TabTemplateProps)
label: cst.term_raw label: cst.term_raw
})); }));
useEffect(() => {
if (templates.length > 0 && !state.templateID) {
partialUpdate({ templateID: templates[0].id });
}
}, [templates, state.templateID, partialUpdate]);
useEffect(() => {
if (!templateSchema) {
return;
}
let data = templateSchema.items;
if (state.filterCategory) {
data = applyFilterCategory(state.filterCategory, templateSchema);
}
setFilteredData(data);
}, [state.filterCategory, templateSchema]);
return ( return (
<div className='cc-fade-in'> <div className='cc-fade-in'>
<div className='flex border-t border-x rounded-t-md clr-input'> <div className='flex border-t border-x rounded-t-md clr-input'>
@ -71,12 +62,8 @@ function TabTemplate({ state, partialUpdate, templateSchema }: TabTemplateProps)
placeholder='Источник' placeholder='Источник'
className='w-[12rem]' className='w-[12rem]'
options={templateSelector} options={templateSelector}
value={ value={templateID ? { value: templateID, label: templates.find(item => item.id == templateID)!.title } : null}
state.templateID onChange={data => onChangeTemplateID(data ? data.value : undefined)}
? { value: state.templateID, label: templates.find(item => item.id == state.templateID)!.title }
: null
}
onChange={data => partialUpdate({ templateID: data ? data.value : undefined })}
/> />
<SelectSingle <SelectSingle
noBorder noBorder
@ -85,24 +72,22 @@ function TabTemplate({ state, partialUpdate, templateSchema }: TabTemplateProps)
className='flex-grow ml-1 border-none' className='flex-grow ml-1 border-none'
options={categorySelector} options={categorySelector}
value={ value={
state.filterCategory && templateSchema filterCategory && templateSchema
? { ? {
value: state.filterCategory.id, value: filterCategory.id,
label: state.filterCategory.term_raw label: filterCategory.term_raw
} }
: null : null
} }
onChange={data => onChange={data => onChangeFilterCategory(data ? templateSchema?.cstByID.get(data?.value) : undefined)}
partialUpdate({ filterCategory: data ? templateSchema?.cstByID.get(data?.value) : undefined })
}
isClearable isClearable
/> />
</div> </div>
<PickConstituenta <PickConstituenta
id='dlg_template_picker' id='dlg_template_picker'
value={state.prototype} value={prototype}
items={filteredData} items={filteredData}
onChange={cst => partialUpdate({ prototype: cst })} onChange={onChangePrototype}
className='rounded-t-none' className='rounded-t-none'
rows={8} rows={8}
/> />
@ -121,7 +106,7 @@ function TabTemplate({ state, partialUpdate, templateSchema }: TabTemplateProps)
disabled disabled
placeholder='Выберите шаблон из списка' placeholder='Выберите шаблон из списка'
height='5.1rem' height='5.1rem'
value={state.prototype?.definition_formal} value={prototype?.definition_formal}
/> />
</div> </div>
); );

View File

@ -0,0 +1,95 @@
'use client';
import { createContext, useContext, useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { useDialogsStore } from '@/stores/dialogs';
import { ICstCreateDTO } from '../../backend/types';
import { IConstituenta } from '../../models/rsform';
import { generateAlias } from '../../models/rsformAPI';
import { IArgumentValue } from '../../models/rslang';
import { inferTemplatedType, substituteTemplateArgs } from '../../models/rslangAPI';
import { DlgCstTemplateProps } from './DlgCstTemplate';
export interface ITemplateContext {
args: IArgumentValue[];
prototype?: IConstituenta;
templateID?: number;
filterCategory?: IConstituenta;
onChangeArguments: (newArgs: IArgumentValue[]) => void;
onChangePrototype: (newPrototype: IConstituenta) => void;
onChangeTemplateID: (newTemplateID: number | undefined) => void;
onChangeFilterCategory: (newFilterCategory: IConstituenta | undefined) => void;
}
const TemplateContext = createContext<ITemplateContext | null>(null);
export const useTemplateContext = () => {
const context = useContext(TemplateContext);
if (context === null) {
throw new Error('useTemplateContext has to be used within <TemplateState>');
}
return context;
};
export const TemplateState = ({ children }: React.PropsWithChildren) => {
const { schema } = useDialogsStore(state => state.props as DlgCstTemplateProps);
const { setValue } = useFormContext<ICstCreateDTO>();
const [templateID, setTemplateID] = useState<number | undefined>(undefined);
const [args, setArguments] = useState<IArgumentValue[]>([]);
const [prototype, setPrototype] = useState<IConstituenta | undefined>(undefined);
const [filterCategory, setFilterCategory] = useState<IConstituenta | undefined>(undefined);
function onChangeArguments(newArgs: IArgumentValue[]) {
setArguments(newArgs);
if (newArgs.length === 0 || !prototype) {
return;
}
const newType = inferTemplatedType(prototype.cst_type, newArgs);
setValue('definition_formal', substituteTemplateArgs(prototype.definition_formal, newArgs));
setValue('cst_type', newType);
setValue('alias', generateAlias(newType, schema));
}
function onChangePrototype(newPrototype: IConstituenta) {
setPrototype(newPrototype);
setArguments(
newPrototype.parse.args.map(arg => ({
alias: arg.alias,
typification: arg.typification,
value: ''
}))
);
setValue('cst_type', newPrototype.cst_type);
setValue('alias', generateAlias(newPrototype.cst_type, schema));
setValue('definition_formal', newPrototype.definition_formal);
setValue('term_raw', newPrototype.term_raw);
setValue('definition_raw', newPrototype.definition_raw);
}
function onChangeTemplateID(newTemplateID: number | undefined) {
setTemplateID(newTemplateID);
setPrototype(undefined);
setArguments([]);
}
return (
<TemplateContext
value={{
templateID,
prototype,
filterCategory,
args,
onChangeArguments,
onChangePrototype,
onChangeFilterCategory: setFilterCategory,
onChangeTemplateID
}}
>
{children}
</TemplateContext>
);
};

View File

@ -18,7 +18,7 @@ import { ICstCreateDTO } from '../../backend/types';
import { useCstCreate } from '../../backend/useCstCreate'; import { useCstCreate } from '../../backend/useCstCreate';
import { useCstMove } from '../../backend/useCstMove'; import { useCstMove } from '../../backend/useCstMove';
import { useRSFormSuspense } from '../../backend/useRSForm'; import { useRSFormSuspense } from '../../backend/useRSForm';
import { CstType, IConstituenta, IRSForm } from '../../models/rsform'; import { CstType, IConstituenta, IConstituentaMeta, IRSForm } from '../../models/rsform';
import { generateAlias } from '../../models/rsformAPI'; import { generateAlias } from '../../models/rsformAPI';
export enum RSTabID { export enum RSTabID {
@ -177,24 +177,21 @@ export const RSEditState = ({
}); });
} }
function handleCreateCst(data: ICstCreateDTO) { function onCreateCst(newCst: IConstituentaMeta) {
data.alias = data.alias || generateAlias(data.cst_type, schema); setSelected([newCst.id]);
void cstCreate({ itemID: itemID, data }).then(newCst => { navigateRSForm({ tab: activeTab, activeID: newCst.id });
setSelected([newCst.id]); if (activeTab === RSTabID.CST_LIST) {
navigateRSForm({ tab: activeTab, activeID: newCst.id }); setTimeout(() => {
if (activeTab === RSTabID.CST_LIST) { const element = document.getElementById(`${prefixes.cst_list}${newCst.id}`);
setTimeout(() => { if (element) {
const element = document.getElementById(`${prefixes.cst_list}${newCst.id}`); element.scrollIntoView({
if (element) { behavior: 'smooth',
element.scrollIntoView({ block: 'nearest',
behavior: 'smooth', inline: 'end'
block: 'nearest', });
inline: 'end' }
}); }, PARAMETER.refreshTimeout);
} }
}, PARAMETER.refreshTimeout);
}
});
} }
function moveUp() { function moveUp() {
@ -258,9 +255,9 @@ export const RSEditState = ({
term_forms: [] term_forms: []
}; };
if (skipDialog) { if (skipDialog) {
handleCreateCst(data); void cstCreate({ itemID: schema.id, data }).then(onCreateCst);
} else { } else {
showCreateCst({ schema: schema, onCreate: handleCreateCst, initial: data }); showCreateCst({ schema: schema, onCreate: onCreateCst, initial: data });
} }
} }
@ -268,17 +265,19 @@ export const RSEditState = ({
if (!activeCst) { if (!activeCst) {
return; return;
} }
const data: ICstCreateDTO = { void cstCreate({
insert_after: activeCst.id, itemID: schema.id,
cst_type: activeCst.cst_type, data: {
alias: generateAlias(activeCst.cst_type, schema), insert_after: activeCst.id,
term_raw: activeCst.term_raw, cst_type: activeCst.cst_type,
definition_formal: activeCst.definition_formal, alias: generateAlias(activeCst.cst_type, schema),
definition_raw: activeCst.definition_raw, term_raw: activeCst.term_raw,
convention: activeCst.convention, definition_formal: activeCst.definition_formal,
term_forms: activeCst.term_forms definition_raw: activeCst.definition_raw,
}; convention: activeCst.convention,
handleCreateCst(data); term_forms: activeCst.term_forms
}
}).then(onCreateCst);
} }
function promptDeleteCst() { function promptDeleteCst() {
@ -299,11 +298,12 @@ export const RSEditState = ({
} }
}); });
} }
function promptTemplate() { function promptTemplate() {
if (isModified && !promptUnsaved()) { if (isModified && !promptUnsaved()) {
return; return;
} }
showCstTemplate({ schema: schema, onCreate: handleCreateCst, insertAfter: activeCst?.id }); showCstTemplate({ schema: schema, onCreate: onCreateCst, insertAfter: activeCst?.id });
} }
return ( return (

View File

@ -140,7 +140,8 @@ export const errorMsg = {
loginFormat: 'Имя пользователя должно содержать только буквы и цифры', loginFormat: 'Имя пользователя должно содержать только буквы и цифры',
invalidLocation: 'Некорректный формат пути', invalidLocation: 'Некорректный формат пути',
versionTaken: 'Версия с таким шифром уже существует', versionTaken: 'Версия с таким шифром уже существует',
emptySubstitutions: 'Выберите хотя бы одно отождествление' emptySubstitutions: 'Выберите хотя бы одно отождествление',
aliasInvalid: 'Введите незанятое имя, соответствующее типу'
}; };
/** /**
@ -162,7 +163,6 @@ export const promptText = {
'Внимание!!\nУдаление операционной схемы приведет к удалению всех операций и собственных концептуальных схем.\nДанное действие нельзя отменить.\nВы уверены, что хотите удалить данную ОСС?', 'Внимание!!\nУдаление операционной схемы приведет к удалению всех операций и собственных концептуальных схем.\nДанное действие нельзя отменить.\nВы уверены, что хотите удалить данную ОСС?',
generateWordforms: 'Данное действие приведет к перезаписи словоформ при совпадении граммем. Продолжить?', generateWordforms: 'Данное действие приведет к перезаписи словоформ при совпадении граммем. Продолжить?',
restoreArchive: 'При восстановлении архивной версии актуальная схему будет заменена. Продолжить?', restoreArchive: 'При восстановлении архивной версии актуальная схему будет заменена. Продолжить?',
templateUndefined: 'Вы уверены, что хотите создать шаблонную конституенту не фиксируя аргументы?',
ownerChange: ownerChange:
'Вы уверены, что хотите изменить владельца? Вы потеряете право управления данной схемой. Данное действие отменить нельзя' 'Вы уверены, что хотите изменить владельца? Вы потеряете право управления данной схемой. Данное действие отменить нельзя'
}; };