F: Implement Constituenta editing from OSS
Some checks are pending
Frontend CI / build (22.x) (push) Waiting to run
Frontend CI / notify-failure (push) Blocked by required conditions

This commit is contained in:
Ivan 2025-07-08 20:49:50 +03:00
parent e876478e55
commit acdd74001e
11 changed files with 298 additions and 9 deletions

View File

@ -27,7 +27,7 @@ export function ApplicationLayout() {
<NavigationState>
<div className='min-w-80 antialiased h-full max-w-480 mx-auto'>
<ToasterThemed
className={clsx('sm:text-[14px]/[20px] text-[12px]/[16px]', noNavigationAnimation ? 'mt-10' : 'mt-18')}
className={clsx('sm:text-[14px]/[20px] text-[12px]/[16px]', noNavigationAnimation ? 'mt-9' : 'mt-17')}
aria-label='Оповещения'
autoClose={3000}
draggable={false}

View File

@ -128,6 +128,9 @@ const DlgOssSettings = React.lazy(() =>
default: module.DlgOssSettings
}))
);
const DlgEditCst = React.lazy(() =>
import('@/features/rsform/dialogs/dlg-edit-cst').then(module => ({ default: module.DlgEditCst }))
);
export const GlobalDialogs = () => {
const active = useDialogsStore(state => state.active);
@ -188,5 +191,7 @@ export const GlobalDialogs = () => {
return <DlgSubstituteCst />;
case DialogType.UPLOAD_RSFORM:
return <DlgUploadRSForm />;
case DialogType.EDIT_CONSTITUENTA:
return <DlgEditCst />;
}
};

View File

@ -44,8 +44,7 @@ export function SidePanel({ isMounted, className }: SidePanelProps) {
style={{ height: sidePanelHeight }}
>
<MiniButton
titleHtml='Закрыть панель'
aria-label='Закрыть'
title='Закрыть панель'
noPadding
icon={<IconClose size='1.25rem' />}
className={clsx(

View File

@ -1,11 +1,12 @@
import { useState } from 'react';
import { type IConstituenta } from '@/features/rsform';
import { useRSFormSuspense } from '@/features/rsform/backend/use-rsform';
import { RSFormStats } from '@/features/rsform/components/rsform-stats';
import { ViewConstituents } from '@/features/rsform/components/view-constituents';
import { useFitHeight } from '@/stores/app-layout';
import { notImplemented } from '@/utils/utils';
import { useDialogsStore } from '@/stores/dialogs';
import { ToolbarConstituents } from './toolbar-constituents';
@ -18,9 +19,14 @@ export function ViewSchema({ schemaID, isMutable }: ViewSchemaProps) {
const { schema } = useRSFormSuspense({ itemID: schemaID });
const [activeID, setActiveID] = useState<number | null>(null);
const activeCst = activeID ? schema.cstByID.get(activeID) ?? null : null;
const showEditCst = useDialogsStore(state => state.showEditCst);
const listHeight = useFitHeight('14.5rem', '10rem');
function handleEditCst(cst: IConstituenta) {
showEditCst({ schema: schema, target: cst });
}
return (
<div className='grid h-full relative cc-fade-in mt-5' style={{ gridTemplateRows: '1fr auto' }}>
<ToolbarConstituents
@ -28,7 +34,7 @@ export function ViewSchema({ schemaID, isMutable }: ViewSchemaProps) {
schema={schema}
activeCst={activeCst}
isMutable={isMutable}
onEditActive={notImplemented}
onEditActive={() => handleEditCst(activeCst!)}
setActive={setActiveID}
resetActive={() => setActiveID(null)}
/>
@ -41,6 +47,7 @@ export function ViewSchema({ schemaID, isMutable }: ViewSchemaProps) {
activeCst={activeCst}
onActivate={cst => setActiveID(cst.id)}
maxListHeight={listHeight}
onDoubleClick={isMutable ? handleEditCst : undefined}
/>
<RSFormStats className='pr-4 py-2 ml-[-1rem]' stats={schema.stats} />

View File

@ -18,6 +18,7 @@ interface TableSideConstituentsProps {
schema: IRSForm;
activeCst?: IConstituenta | null;
onActivate?: (cst: IConstituenta) => void;
onDoubleClick?: (cst: IConstituenta) => void;
maxHeight?: string;
autoScroll?: boolean;
@ -29,6 +30,7 @@ export function TableSideConstituents({
schema,
activeCst,
onActivate,
onDoubleClick,
maxHeight,
autoScroll = true
}: TableSideConstituentsProps) {
@ -102,6 +104,7 @@ export function TableSideConstituents({
</NoData>
}
onRowClicked={onActivate ? cst => onActivate(cst) : undefined}
onRowDoubleClicked={onDoubleClick}
/>
);
}

View File

@ -11,6 +11,7 @@ interface ViewConstituentsProps {
schema: IRSForm;
activeCst?: IConstituenta | null;
onActivate?: (cst: IConstituenta) => void;
onDoubleClick?: (cst: IConstituenta) => void;
className?: string;
maxListHeight?: string;
@ -23,6 +24,7 @@ export function ViewConstituents({
schema,
activeCst,
onActivate,
onDoubleClick,
className,
maxListHeight,
@ -43,6 +45,7 @@ export function ViewConstituents({
onActivate={onActivate}
maxHeight={maxListHeight}
autoScroll={autoScroll}
onDoubleClick={onDoubleClick}
/>
</aside>
);

View File

@ -11,6 +11,7 @@ import { TextArea, TextInput } from '@/components/input';
import { CstType, type ICreateConstituentaDTO } from '../../backend/types';
import { RSInput } from '../../components/rs-input';
import { SelectCstType } from '../../components/select-cst-type';
import { getRSDefinitionPlaceholder } from '../../labels';
import { type IRSForm } from '../../models/rsform';
import { generateAlias, isBaseSet, isBasicConcept, isFunctional } from '../../models/rsform-api';
@ -85,9 +86,7 @@ export function FormCreateCst({ schema }: FormCreateCstProps) {
? 'Определение функции'
: 'Формальное определение'
}
placeholder={
cst_type !== CstType.STRUCTURED ? 'Родоструктурное выражение' : 'Типизация родовой структуры'
}
placeholder={getRSDefinitionPlaceholder(cst_type)}
value={field.value}
onChange={field.onChange}
schema={schema}

View File

@ -0,0 +1,112 @@
import { FormProvider, useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { urls, useConceptNavigation } from '@/app';
import { useFindPredecessor } from '@/features/oss/backend/use-find-predecessor';
import { MiniButton } from '@/components/control';
import { IconChild, IconRSForm } from '@/components/icons';
import { ModalForm } from '@/components/modal';
import { useDialogsStore } from '@/stores/dialogs';
import { errorMsg } from '@/utils/labels';
import { type IUpdateConstituentaDTO, schemaUpdateConstituenta } from '../../backend/types';
import { useUpdateConstituenta } from '../../backend/use-update-constituenta';
import { type IConstituenta, type IRSForm } from '../../models/rsform';
import { validateNewAlias } from '../../models/rsform-api';
import { RSTabID } from '../../pages/rsform-page/rsedit-context';
import { FormEditCst } from './form-edit-cst';
export interface DlgEditCstProps {
schema: IRSForm;
target: IConstituenta;
}
export function DlgEditCst() {
const { schema, target } = useDialogsStore(state => state.props as DlgEditCstProps);
const hideDialog = useDialogsStore(state => state.hideDialog);
const { updateConstituenta } = useUpdateConstituenta();
const router = useConceptNavigation();
const { findPredecessor } = useFindPredecessor();
const methods = useForm<IUpdateConstituentaDTO>({
resolver: zodResolver(schemaUpdateConstituenta),
defaultValues: {
target: target.id,
item_data: {
alias: target.alias,
cst_type: target.cst_type,
convention: target.convention,
definition_formal: target.definition_formal,
definition_raw: target.definition_raw,
term_raw: target.term_raw,
term_forms: target.term_forms
}
}
});
const alias = useWatch({ control: methods.control, name: 'item_data.alias' })!;
const cst_type = useWatch({ control: methods.control, name: 'item_data.cst_type' })!;
const isValid = (alias === target.alias && cst_type == target.cst_type) || validateNewAlias(alias, cst_type, schema);
function onSubmit(data: IUpdateConstituentaDTO) {
return updateConstituenta({ itemID: schema.id, data });
}
function navigateToTarget() {
hideDialog();
router.push({
path: urls.schema_props({
id: schema.id,
tab: RSTabID.CST_EDIT,
active: target.id
})
});
}
function editSource() {
hideDialog();
void findPredecessor(target.id).then(reference =>
router.push({
path: urls.schema_props({
id: reference.schema,
active: reference.id,
tab: RSTabID.CST_EDIT
})
})
);
}
return (
<ModalForm
header='Редактирование конституенты'
canSubmit={isValid}
onSubmit={event => void methods.handleSubmit(onSubmit)(event)}
submitInvalidTooltip={errorMsg.aliasInvalid}
submitText='Сохранить'
className='cc-column w-140 max-h-120 py-2 px-6'
>
<div className='cc-icons absolute z-pop left-2 top-2'>
<MiniButton
title='Редактировать в КС'
noPadding
icon={<IconRSForm size='1.25rem' className='text-primary' />}
className=''
onClick={navigateToTarget}
/>
<MiniButton
title='Перейти к предку'
noPadding
icon={<IconChild size='1.25rem' className={target.is_inherited ? 'text-primary' : 'text-foreground-muted'} />}
disabled={!target.is_inherited}
onClick={editSource}
/>
</div>
<FormProvider {...methods}>
<FormEditCst target={target} schema={schema} />
</FormProvider>
</ModalForm>
);
}

View File

@ -0,0 +1,156 @@
import { useState } from 'react';
import { Controller, useFormContext, useWatch } from 'react-hook-form';
import { TextArea, TextInput } from '@/components/input';
import { CstType, type IUpdateConstituentaDTO } from '../../backend/types';
import { SelectCstType } from '../../components/select-cst-type';
import { getRSDefinitionPlaceholder, labelCstTypification } from '../../labels';
import { type IConstituenta, type IRSForm } from '../../models/rsform';
import { generateAlias, isBaseSet, isBasicConcept, isFunctional } from '../../models/rsform-api';
interface FormEditCstProps {
schema: IRSForm;
target: IConstituenta;
}
export function FormEditCst({ target, schema }: FormEditCstProps) {
const {
setValue,
control,
register,
formState: { errors }
} = useFormContext<IUpdateConstituentaDTO>();
const [forceComment, setForceComment] = useState(false);
const cst_type = useWatch({ control, name: 'item_data.cst_type' }) ?? CstType.BASE;
const convention = useWatch({ control, name: 'item_data.convention' });
const isBasic = isBasicConcept(cst_type);
const isElementary = isBaseSet(cst_type);
const isFunction = isFunctional(cst_type);
const showConvention = !!convention || forceComment || isBasic;
function handleTypeChange(newValue: CstType) {
setValue('item_data.cst_type', newValue);
setValue('item_data.alias', generateAlias(newValue, schema), { shouldValidate: true });
setForceComment(false);
}
return (
<>
<div className='flex items-center self-center gap-3'>
<SelectCstType
id='dlg_cst_type' //
value={cst_type}
onChange={handleTypeChange}
disabled={target.is_inherited}
/>
<TextInput
id='dlg_cst_alias'
dense
label='Имя'
className='w-28'
{...register('item_data.alias')}
error={errors.item_data?.alias}
/>
</div>
<TextArea
id='dlg_cst_term'
fitContent
spellCheck
label='Термин'
className='max-h-15'
{...register('item_data.term_raw')}
error={errors.item_data?.term_raw}
/>
<TextArea
id='cst_typification'
fitContent
dense
noResize
noBorder
noOutline
transparent
readOnly
label='Типизация'
value={labelCstTypification(target)}
className='cursor-default'
/>
<Controller
control={control}
name='item_data.definition_formal'
render={({ field }) =>
!!field.value || (!isElementary && !target.is_inherited) ? (
<TextArea
id='dlg_cst_expression'
fitContent
label={
cst_type === CstType.STRUCTURED
? 'Область определения'
: isFunction
? 'Определение функции'
: 'Формальное определение'
}
placeholder={getRSDefinitionPlaceholder(cst_type)}
className='max-h-15'
value={field.value}
onChange={field.onChange}
error={errors.item_data?.definition_formal}
disabled={target.is_inherited}
/>
) : (
<></>
)
}
/>
<Controller
control={control}
name='item_data.definition_raw'
render={({ field }) =>
!!field.value || !isElementary ? (
<TextArea
id='dlg_edit_cst_definition_raw'
fitContent
spellCheck
label='Текстовое определение'
className='max-h-15'
value={field.value}
onChange={field.onChange}
error={errors.item_data?.definition_raw}
/>
) : (
<></>
)
}
/>
{!showConvention ? (
<button
id='dlg_cst_show_comment'
tabIndex={-1}
type='button'
className='self-start cc-label text-primary hover:underline select-none'
onClick={() => setForceComment(true)}
>
Добавить комментарий
</button>
) : (
<TextArea
id='dlg_edit_cst_convention'
fitContent
spellCheck
label={isBasic ? 'Конвенция' : 'Комментарий'}
className='max-h-20'
{...register('item_data.convention')}
error={errors.item_data?.convention}
disabled={isBasic && target.is_inherited}
/>
)}
</>
);
}

View File

@ -0,0 +1 @@
export { DlgEditCst } from './dlg-edit-cst';

View File

@ -15,6 +15,7 @@ import { type DlgRelocateConstituentsProps } from '@/features/oss/dialogs/dlg-re
import { type DlgCreateCstProps } from '@/features/rsform/dialogs/dlg-create-cst/dlg-create-cst';
import { type DlgCstTemplateProps } from '@/features/rsform/dialogs/dlg-cst-template/dlg-cst-template';
import { type DlgDeleteCstProps } from '@/features/rsform/dialogs/dlg-delete-cst/dlg-delete-cst';
import { type DlgEditCstProps } from '@/features/rsform/dialogs/dlg-edit-cst/dlg-edit-cst';
import { type DlgEditReferenceProps } from '@/features/rsform/dialogs/dlg-edit-reference/dlg-edit-reference';
import { type DlgEditWordFormsProps } from '@/features/rsform/dialogs/dlg-edit-word-forms/dlg-edit-word-forms';
import { type DlgInlineSynthesisProps } from '@/features/rsform/dialogs/dlg-inline-synthesis/dlg-inline-synthesis';
@ -45,6 +46,7 @@ export const DialogType = {
CHANGE_INPUT_SCHEMA: 11,
RELOCATE_CONSTITUENTS: 12,
OSS_SETTINGS: 26,
EDIT_CONSTITUENTA: 27,
CLONE_LIBRARY_ITEM: 13,
UPLOAD_RSFORM: 14,
@ -98,6 +100,7 @@ interface DialogsStore {
showQR: (props: DlgShowQRProps) => void;
showSubstituteCst: (props: DlgSubstituteCstProps) => void;
showUploadRSForm: (props: DlgUploadRSFormProps) => void;
showEditCst: (props: DlgEditCstProps) => void;
}
export const useDialogsStore = create<DialogsStore>()(set => ({
@ -135,5 +138,6 @@ export const useDialogsStore = create<DialogsStore>()(set => ({
showRenameCst: props => set({ active: DialogType.RENAME_CONSTITUENTA, props: props }),
showQR: props => set({ active: DialogType.SHOW_QR_CODE, props: props }),
showSubstituteCst: props => set({ active: DialogType.SUBSTITUTE_CONSTITUENTS, props: props }),
showUploadRSForm: props => set({ active: DialogType.UPLOAD_RSFORM, props: props })
showUploadRSForm: props => set({ active: DialogType.UPLOAD_RSFORM, props: props }),
showEditCst: props => set({ active: DialogType.EDIT_CONSTITUENTA, props: props })
}));