F: Rework DeleteCst and InlineSynthesis dialogs

This commit is contained in:
Ivan 2025-02-12 12:21:19 +03:00
parent 2632921dbb
commit c9da647226
17 changed files with 172 additions and 163 deletions

View File

@ -620,6 +620,8 @@ def inline_synthesis(request: Request) -> HttpResponse:
receiver = m.RSForm(serializer.validated_data['receiver'])
items = cast(list[m.Constituenta], serializer.validated_data['items'])
if len(items) == 0:
items = list(m.RSForm(serializer.validated_data['source']).constituents().order_by('order'))
with transaction.atomic():
new_items = receiver.insert_copy(items)

View File

@ -10,7 +10,7 @@ import { Label } from '@/components/Input';
import { ModalForm } from '@/components/Modal';
import { useLibrary } from '@/features/library/backend/useLibrary';
import { ILibraryItem, LibraryItemType } from '@/features/library/models/library';
import PickSchema from '@/features/rsform/components/PickSchema';
import { PickSchema } from '@/features/rsform/components/PickSchema';
import { useDialogsStore } from '@/stores/dialogs';
import { IInputUpdateDTO, IOperationPosition, schemaInputUpdate } from '../backend/api';

View File

@ -8,7 +8,7 @@ import { Checkbox, Label, TextArea, TextInput } from '@/components/Input';
import { useLibrary } from '@/features/library/backend/useLibrary';
import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/features/library/models/library';
import { sortItemsForOSS } from '@/features/oss/models/ossAPI';
import PickSchema from '@/features/rsform/components/PickSchema';
import { PickSchema } from '@/features/rsform/components/PickSchema';
import { useDialogsStore } from '@/stores/dialogs';
import { IOperationCreateDTO } from '../../backend/api';

View File

@ -49,7 +49,6 @@ function TabSynthesisOperation() {
<Controller
name='arguments'
control={control}
defaultValue={[]}
render={({ field }) => (
<PickMultiOperation items={oss.items} value={field.value} onChange={field.onChange} rows={6} />
)}

View File

@ -29,7 +29,6 @@ function TabArguments() {
<Controller
name='arguments'
control={control}
defaultValue={[]}
render={({ field }) => (
<>
<Label text={`Выбор аргументов: [ ${field.value.length} ]`} />

View File

@ -32,7 +32,6 @@ function TabSynthesis() {
<Controller
name='substitutions'
control={control}
defaultValue={[]}
render={({ field }) => (
<PickSubstitutions
schemas={schemas}

View File

@ -142,12 +142,17 @@ export type ICstSubstitute = z.infer<typeof schemaCstSubstitute>;
/**
* Represents input data for inline synthesis.
*/
export interface IInlineSynthesisDTO {
receiver: LibraryItemID;
source: LibraryItemID;
items: ConstituentaID[];
substitutions: ICstSubstitute[];
}
export const schemaInlineSynthesis = z.object({
receiver: z.number(),
source: z.number().nullable(),
items: z.array(z.number()),
substitutions: z.array(schemaCstSubstitute)
});
/**
* Represents input data for inline synthesis.
*/
export type IInlineSynthesisDTO = z.infer<typeof schemaInlineSynthesis>;
/**
* Represents {@link IConstituenta} data, used for checking expression.
@ -260,9 +265,9 @@ export const rsformsApi = {
successMessage: response => infoMsg.addedConstituents(response.cst_list.length)
}
}),
inlineSynthesis: ({ itemID, data }: { itemID: LibraryItemID; data: IInlineSynthesisDTO }) =>
axiosPost<IInlineSynthesisDTO, IRSFormDTO>({
endpoint: `/api/rsforms/${itemID}/inline-synthesis`,
inlineSynthesis: (data: IInlineSynthesisDTO) =>
axiosPatch<IInlineSynthesisDTO, IRSFormDTO>({
endpoint: `/api/rsforms/inline-synthesis`,
request: {
data: data,
successMessage: infoMsg.inlineSynthesisComplete

View File

@ -1,7 +1,6 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/features/library/backend/useUpdateTimestamp';
import { LibraryItemID } from '@/features/library/models/library';
import { ossApi } from '@/features/oss/backend/api';
import { IInlineSynthesisDTO, rsformsApi } from './api';
@ -26,6 +25,6 @@ export const useInlineSynthesis = () => {
}
});
return {
inlineSynthesis: (data: { itemID: LibraryItemID; data: IInlineSynthesisDTO }) => mutation.mutateAsync(data)
inlineSynthesis: (data: IInlineSynthesisDTO) => mutation.mutateAsync(data)
};
};

View File

@ -31,7 +31,7 @@ interface PickSchemaProps extends CProps.Styling {
const columnHelper = createColumnHelper<ILibraryItem>();
function PickSchema({
export function PickSchema({
id,
initialFilter = '',
rows = 4,
@ -157,5 +157,3 @@ function PickSchema({
</div>
);
}
export default PickSchema;

View File

@ -30,7 +30,7 @@ interface PickSubstitutionsProps extends CProps.Styling {
allowSelfSubstitution?: boolean;
schemas: IRSForm[];
filter?: (cst: IConstituenta) => boolean;
filterCst?: (cst: IConstituenta) => boolean;
}
const columnHelper = createColumnHelper<IMultiSubstitution>();
@ -41,7 +41,7 @@ function PickSubstitutions({
suggestions,
rows,
schemas,
filter,
filterCst,
allowSelfSubstitution,
className,
...restProps
@ -225,7 +225,7 @@ function PickSubstitutions({
<SelectConstituenta
noBorder
items={(leftArgument as IRSForm)?.items.filter(
cst => !value.find(item => item.original === cst.id) && (!filter || filter(cst))
cst => !value.find(item => item.original === cst.id) && (!filterCst || filterCst(cst))
)}
value={leftCst}
onChange={setLeftCst}
@ -247,7 +247,7 @@ function PickSubstitutions({
title='Добавить в таблицу отождествлений'
className='mb-[0.375rem] grow-0'
icon={<IconReplace size='1.5rem' className='icon-primary' />}
disabled={!leftCst || !rightCst || leftCst === rightCst}
disabled={!leftCst || !rightCst || (leftCst === rightCst && !allowSelfSubstitution)}
onClick={addSubstitution}
/>
</div>
@ -263,7 +263,7 @@ function PickSubstitutions({
<SelectConstituenta
noBorder
items={(rightArgument as IRSForm)?.items.filter(
cst => !value.find(item => item.original === cst.id) && (!filter || filter(cst))
cst => !value.find(item => item.original === cst.id) && (!filterCst || filterCst(cst))
)}
value={rightCst}
onChange={setRightCst}

View File

@ -8,17 +8,20 @@ import { ModalForm } from '@/components/Modal';
import { useDialogsStore } from '@/stores/dialogs';
import { prefixes } from '@/utils/constants';
import { useCstDelete } from '../../backend/useCstDelete';
import { ConstituentaID, IRSForm } from '../../models/rsform';
import ListConstituents from './ListConstituents';
export interface DlgDeleteCstProps {
schema: IRSForm;
selected: ConstituentaID[];
onDelete: (items: ConstituentaID[]) => void;
afterDelete: (initialSchema: IRSForm, deleted: ConstituentaID[]) => void;
}
function DlgDeleteCst() {
const { selected, schema, onDelete } = useDialogsStore(state => state.props as DlgDeleteCstProps);
const { selected, schema, afterDelete } = useDialogsStore(state => state.props as DlgDeleteCstProps);
const { cstDelete } = useCstDelete();
const [expandOut, setExpandOut] = useState(false);
const expansion: ConstituentaID[] = schema.graph.expandAllOutputs(selected);
const hasInherited = selected.some(
@ -27,12 +30,8 @@ function DlgDeleteCst() {
);
function handleSubmit() {
if (expandOut) {
onDelete(selected.concat(expansion));
} else {
onDelete(selected);
}
return true;
const deleted = expandOut ? selected.concat(expansion) : selected;
void cstDelete({ itemID: schema.id, data: { items: deleted } }).then(() => afterDelete(schema, deleted));
}
return (

View File

@ -1,24 +1,25 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import clsx from 'clsx';
import { Suspense, useEffect, useState } from 'react';
import { Suspense, useState } from 'react';
import { FormProvider, useForm, useWatch } from 'react-hook-form';
import { Loader } from '@/components/Loader';
import { ModalForm } from '@/components/Modal';
import { TabLabel, TabList, TabPanel, Tabs } from '@/components/Tabs';
import { LibraryItemID } from '@/features/library/models/library';
import { useRSForm } from '@/features/rsform/backend/useRSForm';
import { useDialogsStore } from '@/stores/dialogs';
import { ICstSubstitute, IInlineSynthesisDTO } from '../../backend/api';
import { ConstituentaID, IRSForm } from '../../models/rsform';
import { IInlineSynthesisDTO, schemaInlineSynthesis } from '../../backend/api';
import { useInlineSynthesis } from '../../backend/useInlineSynthesis';
import { IRSForm } from '../../models/rsform';
import TabConstituents from './TabConstituents';
import TabSource from './TabSource';
import TabSubstitutions from './TabSubstitutions';
export interface DlgInlineSynthesisProps {
receiver: IRSForm;
onInlineSynthesis: (data: IInlineSynthesisDTO) => void;
onSynthesis: () => void;
}
export enum TabID {
@ -28,40 +29,24 @@ export enum TabID {
}
function DlgInlineSynthesis() {
const { receiver, onInlineSynthesis } = useDialogsStore(state => state.props as DlgInlineSynthesisProps);
const { receiver, onSynthesis } = useDialogsStore(state => state.props as DlgInlineSynthesisProps);
const [activeTab, setActiveTab] = useState(TabID.SCHEMA);
const { inlineSynthesis } = useInlineSynthesis();
const [sourceID, setSourceID] = useState<LibraryItemID | undefined>(undefined);
const [selected, setSelected] = useState<ConstituentaID[]>([]);
const [substitutions, setSubstitutions] = useState<ICstSubstitute[]>([]);
const { schema } = useRSForm({ itemID: sourceID });
const validated = selected.length > 0;
function handleSubmit() {
if (!sourceID || selected.length === 0) {
return true;
}
onInlineSynthesis({
source: sourceID,
const methods = useForm<IInlineSynthesisDTO>({
resolver: zodResolver(schemaInlineSynthesis),
defaultValues: {
receiver: receiver.id,
items: selected,
substitutions: substitutions
source: null,
items: [],
substitutions: []
},
mode: 'onChange'
});
return true;
}
const sourceID = useWatch({ control: methods.control, name: 'source' });
useEffect(() => {
if (schema) {
setSelected(schema.items.map(cst => cst.id));
}
}, [schema, setSelected]);
function handleSetSource(schemaID: LibraryItemID) {
setSourceID(schemaID);
setSelected([]);
setSubstitutions([]);
function onSubmit(data: IInlineSynthesisDTO) {
return inlineSynthesis(data).then(onSynthesis);
}
return (
@ -69,8 +54,8 @@ function DlgInlineSynthesis() {
header='Импорт концептуальной схем'
submitText='Добавить конституенты'
className='w-[40rem] h-[33rem] px-6'
canSubmit={validated}
onSubmit={handleSubmit}
canSubmit={methods.formState.isValid && sourceID !== null}
onSubmit={event => void methods.handleSubmit(onSubmit)(event)}
>
<Tabs
selectedTabClassName='clr-selected'
@ -94,14 +79,15 @@ function DlgInlineSynthesis() {
/>
</TabList>
<FormProvider {...methods}>
<TabPanel>
<TabSource selected={sourceID} setSelected={handleSetSource} receiver={receiver} />
<TabSource />
</TabPanel>
<TabPanel>
{!!sourceID ? (
<Suspense fallback={<Loader />}>
<TabConstituents itemID={sourceID} selected={selected} setSelected={setSelected} />
<TabConstituents />
</Suspense>
) : null}
</TabPanel>
@ -109,16 +95,11 @@ function DlgInlineSynthesis() {
<TabPanel>
{!!sourceID ? (
<Suspense fallback={<Loader />}>
<TabSubstitutions
sourceID={sourceID}
receiver={receiver}
selected={selected}
substitutions={substitutions}
setSubstitutions={setSubstitutions}
/>
<TabSubstitutions />
</Suspense>
) : null}
</TabPanel>
</FormProvider>
</Tabs>
</ModalForm>
);

View File

@ -1,22 +1,43 @@
'use client';
import { LibraryItemID } from '@/features/library/models/library';
import { Controller, useFormContext, useWatch } from 'react-hook-form';
import { IInlineSynthesisDTO } from '../../backend/api';
import { useRSFormSuspense } from '../../backend/useRSForm';
import PickMultiConstituenta from '../../components/PickMultiConstituenta';
import { ConstituentaID } from '../../models/rsform';
interface TabConstituentsProps {
itemID: LibraryItemID;
selected: ConstituentaID[];
setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
}
function TabConstituents() {
const { setValue, control } = useFormContext<IInlineSynthesisDTO>();
const sourceID = useWatch({ control, name: 'source' });
const substitutions = useWatch({ control, name: 'substitutions' });
function TabConstituents({ itemID, selected, setSelected }: TabConstituentsProps) {
const { schema } = useRSFormSuspense({ itemID });
const { schema } = useRSFormSuspense({ itemID: sourceID! });
function handleSelectItems(newValue: ConstituentaID[]) {
setValue('items', newValue);
const newSubstitutions = substitutions.filter(
sub => newValue.includes(sub.original) || newValue.includes(sub.substitution)
);
if (newSubstitutions.length !== substitutions.length) {
setValue('substitutions', newSubstitutions);
}
}
return (
<PickMultiConstituenta schema={schema} items={schema.items} rows={13} value={selected} onChange={setSelected} />
<Controller
name='items'
control={control}
render={({ field }) => (
<PickMultiConstituenta
schema={schema}
items={schema.items}
rows={13}
value={field.value}
onChange={handleSelectItems}
/>
)}
/>
);
}

View File

@ -1,33 +1,46 @@
'use client';
import { useFormContext, useWatch } from 'react-hook-form';
import { TextInput } from '@/components/Input';
import { useLibrary } from '@/features/library/backend/useLibrary';
import { LibraryItemID, LibraryItemType } from '@/features/library/models/library';
import { useDialogsStore } from '@/stores/dialogs';
import PickSchema from '../../components/PickSchema';
import { IRSForm } from '../../models/rsform';
import { IInlineSynthesisDTO } from '../../backend/api';
import { PickSchema } from '../../components/PickSchema';
import { sortItemsForInlineSynthesis } from '../../models/rsformAPI';
import { DlgInlineSynthesisProps } from './DlgInlineSynthesis';
interface TabSourceProps {
selected?: LibraryItemID;
setSelected: (newValue: LibraryItemID) => void;
receiver: IRSForm;
}
function TabSource({ selected, receiver, setSelected }: TabSourceProps) {
function TabSource() {
const { items: libraryItems } = useLibrary();
const selectedInfo = libraryItems.find(item => item.id === selected);
const { receiver } = useDialogsStore(state => state.props as DlgInlineSynthesisProps);
const { setValue, control } = useFormContext<IInlineSynthesisDTO>();
const sourceID = useWatch({ control, name: 'source' });
const selectedInfo = libraryItems.find(item => item.id === sourceID);
const sortedItems = sortItemsForInlineSynthesis(receiver, libraryItems);
function handleSelectSource(newValue: LibraryItemID) {
if (newValue === sourceID) {
return;
}
setValue('source', newValue);
setValue('items', []);
setValue('substitutions', []);
}
return (
<div className='cc-fade-in flex flex-col'>
<PickSchema
id='dlg_schema_picker' //
id='dlg_schema_picker'
items={sortedItems}
itemType={LibraryItemType.RSFORM}
rows={14}
value={selected ?? null}
onChange={setSelected}
value={sourceID}
onChange={handleSelectSource}
/>
<div className='flex items-center gap-6 '>
<span className='select-none'>Выбрана</span>
<TextInput

View File

@ -1,32 +1,37 @@
'use client';
import { LibraryItemID } from '@/features/library/models/library';
import { Controller, useFormContext, useWatch } from 'react-hook-form';
import { ICstSubstitute } from '../../backend/api';
import { useDialogsStore } from '@/stores/dialogs';
import { IInlineSynthesisDTO } from '../../backend/api';
import { useRSFormSuspense } from '../../backend/useRSForm';
import PickSubstitutions from '../../components/PickSubstitutions';
import { ConstituentaID, IRSForm } from '../../models/rsform';
import { DlgInlineSynthesisProps } from './DlgInlineSynthesis';
interface TabSubstitutionsProps {
receiver: IRSForm;
sourceID: LibraryItemID;
selected: ConstituentaID[];
function TabSubstitutions() {
const { receiver } = useDialogsStore(state => state.props as DlgInlineSynthesisProps);
const { control } = useFormContext<IInlineSynthesisDTO>();
const sourceID = useWatch({ control, name: 'source' });
const selected = useWatch({ control, name: 'items' });
substitutions: ICstSubstitute[];
setSubstitutions: React.Dispatch<React.SetStateAction<ICstSubstitute[]>>;
}
function TabSubstitutions({ sourceID, receiver, selected, substitutions, setSubstitutions }: TabSubstitutionsProps) {
const { schema: source } = useRSFormSuspense({ itemID: sourceID });
const schemas = [...(source ? [source] : []), ...(receiver ? [receiver] : [])];
const { schema: source } = useRSFormSuspense({ itemID: sourceID! });
const selfSubstitution = receiver.id === source.id;
return (
<Controller
name='substitutions'
control={control}
render={({ field }) => (
<PickSubstitutions
value={substitutions}
onChange={setSubstitutions}
value={field.value}
onChange={field.onChange}
allowSelfSubstitution={selfSubstitution}
rows={10}
schemas={schemas}
filter={cst => cst.id !== source?.id || selected.includes(cst.id)}
schemas={selfSubstitution ? [source] : [source, receiver]}
filterCst={selected.length === 0 ? undefined : cst => selected.includes(cst.id)}
/>
)}
/>
);
}

View File

@ -43,7 +43,6 @@ import { describeAccessMode, labelAccessMode, tooltipText } from '@/utils/labels
import { generatePageQR, promptUnsaved, sharePage } from '@/utils/utils';
import { useDownloadRSForm } from '../../backend/useDownloadRSForm';
import { useInlineSynthesis } from '../../backend/useInlineSynthesis';
import { useMutatingRSForm } from '../../backend/useMutatingRSForm';
import { useProduceStructure } from '../../backend/useProduceStructure';
import { useResetAliases } from '../../backend/useResetAliases';
@ -64,7 +63,6 @@ function MenuRSTabs() {
const { resetAliases } = useResetAliases();
const { restoreOrder } = useRestoreOrder();
const { produceStructure } = useProduceStructure();
const { inlineSynthesis } = useInlineSynthesis();
const { download } = useDownloadRSForm();
const showInlineSynthesis = useDialogsStore(state => state.showInlineSynthesis);
@ -199,9 +197,7 @@ function MenuRSTabs() {
}
showInlineSynthesis({
receiver: controller.schema,
onInlineSynthesis: data => {
void inlineSynthesis({ itemID: controller.schema.id, data }).then(() => controller.deselectAll());
}
onSynthesis: () => controller.deselectAll()
});
}

View File

@ -19,7 +19,6 @@ import { promptUnsaved } from '@/utils/utils';
import { ICstCreateDTO } from '../../backend/api';
import { useCstCreate } from '../../backend/useCstCreate';
import { useCstDelete } from '../../backend/useCstDelete';
import { useCstMove } from '../../backend/useCstMove';
import { useRSFormSuspense } from '../../backend/useRSForm';
import { ConstituentaID, CstType, IConstituenta, IRSForm } from '../../models/rsform';
@ -111,7 +110,6 @@ export const RSEditState = ({
const { cstCreate } = useCstCreate();
const { cstMove } = useCstMove();
const { cstDelete } = useCstDelete();
const { deleteItem } = useDeleteItem();
const showCreateCst = useDialogsStore(state => state.showCreateCst);
@ -202,26 +200,6 @@ export const RSEditState = ({
});
}
function handleDeleteCst(deleted: ConstituentaID[]) {
const data = {
items: deleted
};
const isEmpty = deleted.length === schema.items.length;
const nextActive = isEmpty ? undefined : getNextActiveOnDelete(activeCst?.id, schema.items, deleted);
void cstDelete({ itemID: itemID, data }).then(() => {
setSelected(nextActive ? [nextActive] : []);
if (!nextActive) {
navigateRSForm({ tab: RSTabID.CST_LIST });
} else if (activeTab === RSTabID.CST_EDIT) {
navigateRSForm({ tab: activeTab, activeID: nextActive });
} else {
navigateRSForm({ tab: activeTab });
}
});
}
function moveUp() {
if (selected.length === 0) {
return;
@ -307,7 +285,22 @@ export const RSEditState = ({
}
function promptDeleteCst() {
showDeleteCst({ schema: schema, selected: selected, onDelete: handleDeleteCst });
showDeleteCst({
schema: schema,
selected: selected,
afterDelete: (schema, deleted) => {
const isEmpty = deleted.length === schema.items.length;
const nextActive = isEmpty ? undefined : getNextActiveOnDelete(activeCst?.id, schema.items, deleted);
setSelected(nextActive ? [nextActive] : []);
if (!nextActive) {
navigateRSForm({ tab: RSTabID.CST_LIST });
} else if (activeTab === RSTabID.CST_EDIT) {
navigateRSForm({ tab: activeTab, activeID: nextActive });
} else {
navigateRSForm({ tab: activeTab });
}
}
});
}
function promptTemplate() {
if (isModified && !promptUnsaved()) {