F: Rework create operation dialog

This commit is contained in:
Ivan 2025-02-11 12:34:28 +03:00
parent 7214e4a581
commit 0e62a49fa7
12 changed files with 183 additions and 235 deletions

View File

@ -15,7 +15,7 @@ import { Label, TextArea, TextInput } from '@/components/Input';
import { useAuthSuspense } from '@/features/auth/backend/useAuth';
import { EXTEOR_TRS_FILE } from '@/utils/constants';
import { schemaCreateLibraryItem, ICreateLibraryItemDTO } from '../../backend/api';
import { ICreateLibraryItemDTO, schemaCreateLibraryItem } from '../../backend/api';
import { useCreateItem } from '../../backend/useCreateItem';
import SelectAccessPolicy from '../../components/SelectAccessPolicy';
import SelectItemType from '../../components/SelectItemType';

View File

@ -1,7 +1,7 @@
import { queryOptions } from '@tanstack/react-query';
import { z } from 'zod';
import { axiosDelete, axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport';
import { axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport';
import { DELAYS } from '@/backend/configuration';
import { ILibraryItem, ILibraryItemData, LibraryItemID } from '@/features/library/models/library';
import {
@ -46,20 +46,25 @@ export type IOperationPosition = z.infer<typeof schemaOperationPosition>;
/**
* Represents {@link IOperation} data, used in creation process.
*/
export interface IOperationCreateDTO {
positions: IOperationPosition[];
item_data: {
alias: string;
operation_type: OperationType;
title: string;
comment: string;
position_x: number;
position_y: number;
result: LibraryItemID | null;
};
arguments: OperationID[] | undefined;
create_schema: boolean;
}
export const schemaOperationCreate = z.object({
positions: z.array(schemaOperationPosition),
item_data: z.object({
alias: z.string().nonempty(),
operation_type: z.nativeEnum(OperationType),
title: z.string(),
comment: z.string(),
position_x: z.number(),
position_y: z.number(),
result: z.number().nullable()
}),
arguments: z.array(z.number()),
create_schema: z.boolean()
});
/**
* Represents {@link IOperation} data, used in creation process.
*/
export type IOperationCreateDTO = z.infer<typeof schemaOperationCreate>;
/**
* Represents data response when creating {@link IOperation}.
@ -183,7 +188,7 @@ export const ossApi = {
}
}),
operationDelete: ({ itemID, data }: { itemID: LibraryItemID; data: IOperationDeleteDTO }) =>
axiosDelete<IOperationDeleteDTO, IOperationSchemaDTO>({
axiosPatch<IOperationDeleteDTO, IOperationSchemaDTO>({
endpoint: `/api/oss/${itemID}/delete-operation`,
request: {
data: data,

View File

@ -1,10 +1,9 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { DataCallback } from '@/backend/apiTransport';
import { useUpdateTimestamp } from '@/features/library/backend/useUpdateTimestamp';
import { LibraryItemID } from '@/features/library/models/library';
import { IOperationCreateDTO, IOperationDTO, ossApi } from './api';
import { IOperationCreateDTO, ossApi } from './api';
export const useOperationCreate = () => {
const client = useQueryClient();
@ -12,18 +11,12 @@ export const useOperationCreate = () => {
const mutation = useMutation({
mutationKey: [ossApi.baseKey, 'operation-create'],
mutationFn: ossApi.operationCreate,
onSuccess: data => {
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.oss.id }).queryKey, data.oss);
updateTimestamp(data.oss.id);
onSuccess: response => {
client.setQueryData(ossApi.getOssQueryOptions({ itemID: response.oss.id }).queryKey, response.oss);
updateTimestamp(response.oss.id);
}
});
return {
operationCreate: (
data: {
itemID: LibraryItemID; //
data: IOperationCreateDTO;
},
onSuccess?: DataCallback<IOperationDTO>
) => mutation.mutate(data, { onSuccess: response => onSuccess?.(response.new_operation) })
operationCreate: (data: { itemID: LibraryItemID; data: IOperationCreateDTO }) => mutation.mutateAsync(data)
};
};

View File

@ -14,8 +14,7 @@ import SelectOperation from './SelectOperation';
interface PickMultiOperationProps extends CProps.Styling {
value: OperationID[];
onChange: React.Dispatch<React.SetStateAction<OperationID[]>>;
onChange: (newValue: OperationID[]) => void;
items: IOperation[];
rows?: number;
}
@ -28,13 +27,13 @@ function PickMultiOperation({ rows, items, value, onChange, className, ...restPr
const [lastSelected, setLastSelected] = useState<IOperation | undefined>(undefined);
function handleDelete(operation: OperationID) {
onChange(prev => prev.filter(item => item !== operation));
onChange(value.filter(item => item !== operation));
}
function handleSelect(operation?: IOperation) {
if (operation) {
setLastSelected(operation);
onChange(prev => [...prev, operation.id]);
onChange([...value, operation.id]);
setTimeout(() => setLastSelected(undefined), 1000);
}
}
@ -42,24 +41,20 @@ function PickMultiOperation({ rows, items, value, onChange, className, ...restPr
function handleMoveUp(operation: OperationID) {
const index = value.indexOf(operation);
if (index > 0) {
onChange(prev => {
const newSelected = [...prev];
const newSelected = [...value];
newSelected[index] = newSelected[index - 1];
newSelected[index - 1] = operation;
return newSelected;
});
onChange(newSelected);
}
}
function handleMoveDown(operation: OperationID) {
const index = value.indexOf(operation);
if (index < value.length - 1) {
onChange(prev => {
const newSelected = [...prev];
const newSelected = [...value];
newSelected[index] = newSelected[index + 1];
newSelected[index + 1] = operation;
return newSelected;
});
onChange(newSelected);
}
}

View File

@ -1,25 +1,30 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import clsx from 'clsx';
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { FormProvider, useForm, useWatch } from 'react-hook-form';
import { ModalForm } from '@/components/Modal';
import { TabLabel, TabList, TabPanel, Tabs } from '@/components/Tabs';
import { HelpTopic } from '@/features/help/models/helpTopic';
import { useLibrary } from '@/features/library/backend/useLibrary';
import { LibraryItemID } from '@/features/library/models/library';
import { useDialogsStore } from '@/stores/dialogs';
import { describeOperationType, labelOperationType } from '@/utils/labels';
import { IOperationCreateDTO } from '../../backend/api';
import { IOperationCreateDTO, IOperationPosition, schemaOperationCreate } from '../../backend/api';
import { useOperationCreate } from '../../backend/useOperationCreate';
import { IOperationSchema, OperationID, OperationType } from '../../models/oss';
import { calculateInsertPosition } from '../../models/ossAPI';
import TabInputOperation from './TabInputOperation';
import TabSynthesisOperation from './TabSynthesisOperation';
export interface DlgCreateOperationProps {
oss: IOperationSchema;
onCreate: (data: IOperationCreateDTO) => void;
positions: IOperationPosition[];
initialInputs: OperationID[];
defaultX: number;
defaultY: number;
onCreate?: (newID: OperationID) => void;
}
export enum TabID {
@ -28,70 +33,57 @@ export enum TabID {
}
function DlgCreateOperation() {
const { items: libraryItems } = useLibrary();
const { operationCreate } = useOperationCreate();
const { oss, onCreate, initialInputs } = useDialogsStore(state => state.props as DlgCreateOperationProps);
const [activeTab, setActiveTab] = useState(initialInputs.length > 0 ? TabID.SYNTHESIS : TabID.INPUT);
const { oss, positions, initialInputs, onCreate, defaultX, defaultY } = useDialogsStore(
state => state.props as DlgCreateOperationProps
);
const [alias, setAlias] = useState('');
const [title, setTitle] = useState('');
const [comment, setComment] = useState('');
const [inputs, setInputs] = useState<OperationID[]>(initialInputs);
const [attachedID, setAttachedID] = useState<LibraryItemID | undefined>(undefined);
const [createSchema, setCreateSchema] = useState(false);
const isValid = (() => {
if (alias === '') {
return false;
}
if (activeTab === TabID.SYNTHESIS && inputs.length === 0) {
return false;
}
if (activeTab === TabID.INPUT && !attachedID) {
if (oss.items.some(operation => operation.alias === alias)) {
return false;
}
}
return true;
})();
useEffect(() => {
if (attachedID) {
const schema = libraryItems.find(value => value.id === attachedID);
if (schema) {
setAlias(schema.alias);
setTitle(schema.title);
setComment(schema.comment);
}
}
}, [attachedID, libraryItems]);
const handleSubmit = () => {
onCreate({
const methods = useForm<IOperationCreateDTO>({
resolver: zodResolver(schemaOperationCreate),
defaultValues: {
item_data: {
position_x: 0,
position_y: 0,
alias: alias,
title: title,
comment: comment,
operation_type: activeTab === TabID.INPUT ? OperationType.INPUT : OperationType.SYNTHESIS,
result: activeTab === TabID.INPUT ? attachedID ?? null : null
operation_type: initialInputs.length === 0 ? OperationType.INPUT : OperationType.SYNTHESIS,
result: null,
position_x: defaultX,
position_y: defaultY,
alias: '',
title: '',
comment: ''
},
positions: [],
arguments: activeTab === TabID.INPUT ? undefined : inputs.length > 0 ? inputs : undefined,
create_schema: createSchema
arguments: initialInputs,
create_schema: false,
positions: positions
},
mode: 'onChange'
});
return true;
};
const alias = useWatch({ control: methods.control, name: 'item_data.alias' });
const [activeTab, setActiveTab] = useState(initialInputs.length === 0 ? TabID.INPUT : TabID.SYNTHESIS);
const isValid = !!alias && !oss.items.some(operation => operation.alias === alias);
function onSubmit(data: IOperationCreateDTO) {
const target = calculateInsertPosition(oss, data.item_data.operation_type, data.arguments, positions, {
x: defaultX,
y: defaultY
});
data.item_data.position_x = target.x;
data.item_data.position_y = target.y;
operationCreate({ itemID: oss.id, data: data })
.then(response => onCreate?.(response.new_operation.id))
.catch(console.error);
}
function handleSelectTab(newTab: TabID, last: TabID) {
if (last === newTab) {
return;
}
if (newTab === TabID.INPUT) {
setAttachedID(undefined);
methods.setValue('item_data.operation_type', OperationType.INPUT);
methods.setValue('item_data.result', null);
methods.setValue('arguments', []);
} else {
setInputs(initialInputs);
methods.setValue('item_data.operation_type', OperationType.SYNTHESIS);
methods.setValue('arguments', initialInputs);
}
setActiveTab(newTab);
}
@ -101,7 +93,7 @@ function DlgCreateOperation() {
header='Создание операции'
submitText='Создать'
canSubmit={isValid}
onSubmit={handleSubmit}
onSubmit={event => void methods.handleSubmit(onSubmit)(event)}
className='w-[40rem] px-6 h-[32rem]'
helpTopic={HelpTopic.CC_OSS}
>
@ -123,36 +115,15 @@ function DlgCreateOperation() {
label={labelOperationType(OperationType.SYNTHESIS)}
/>
</TabList>
<FormProvider {...methods}>
<TabPanel>
<TabInputOperation
oss={oss}
alias={alias}
onChangeAlias={setAlias}
comment={comment}
onChangeComment={setComment}
title={title}
onChangeTitle={setTitle}
attachedID={attachedID}
onChangeAttachedID={setAttachedID}
createSchema={createSchema}
onChangeCreateSchema={setCreateSchema}
/>
<TabInputOperation oss={oss} />
</TabPanel>
<TabPanel>
<TabSynthesisOperation
oss={oss}
alias={alias}
onChangeAlias={setAlias}
comment={comment}
onChangeComment={setComment}
title={title}
onChangeTitle={setTitle}
inputs={inputs}
setInputs={setInputs}
/>
<TabSynthesisOperation oss={oss} />
</TabPanel>
</FormProvider>
</Tabs>
</ModalForm>
);

View File

@ -1,6 +1,6 @@
'use client';
import { useEffect } from 'react';
import { Controller, useFormContext, useWatch } from 'react-hook-form';
import { MiniButton } from '@/components/Control';
import { IconReset } from '@/components/Icons';
@ -11,73 +11,70 @@ import { IOperationSchema } from '@/features/oss/models/oss';
import { sortItemsForOSS } from '@/features/oss/models/ossAPI';
import PickSchema from '@/features/rsform/components/PickSchema';
import { IOperationCreateDTO } from '../../backend/api';
interface TabInputOperationProps {
oss: IOperationSchema;
alias: string;
onChangeAlias: (newValue: string) => void;
title: string;
onChangeTitle: (newValue: string) => void;
comment: string;
onChangeComment: (newValue: string) => void;
attachedID: LibraryItemID | undefined;
onChangeAttachedID: (newValue: LibraryItemID | undefined) => void;
createSchema: boolean;
onChangeCreateSchema: (newValue: boolean) => void;
}
function TabInputOperation({
oss,
alias,
onChangeAlias,
title,
onChangeTitle,
comment,
onChangeComment,
attachedID,
onChangeAttachedID,
createSchema,
onChangeCreateSchema
}: TabInputOperationProps) {
function TabInputOperation({ oss }: TabInputOperationProps) {
const { items: libraryItems } = useLibrary();
const sortedItems = sortItemsForOSS(oss, libraryItems);
const {
register,
control,
setValue,
formState: { errors }
} = useFormContext<IOperationCreateDTO>();
const createSchema = useWatch({ control, name: 'create_schema' });
function baseFilter(item: ILibraryItem) {
return !oss.schemas.includes(item.id);
}
useEffect(() => {
if (createSchema) {
onChangeAttachedID(undefined);
function handleChangeCreateSchema(value: boolean) {
if (value) {
setValue('item_data.result', null);
}
setValue('create_schema', value);
}
function handleSetInput(value: LibraryItemID) {
const schema = libraryItems.find(item => item.id === value);
if (!schema) {
return;
}
setValue('item_data.result', value);
setValue('create_schema', false);
setValue('item_data.alias', schema.alias);
setValue('item_data.title', schema.title);
setValue('item_data.comment', schema.comment);
}
}, [createSchema, onChangeAttachedID]);
return (
<div className='cc-fade-in cc-column'>
<TextInput
id='operation_title'
id='operation_title' //
label='Полное название'
value={title}
onChange={event => onChangeTitle(event.target.value)}
disabled={attachedID !== undefined}
{...register('item_data.title')}
error={errors.item_data?.title}
/>
<div className='flex gap-6'>
<TextInput
id='operation_alias'
id='operation_alias' //
label='Сокращение'
className='w-[16rem]'
value={alias}
onChange={event => onChangeAlias(event.target.value)}
disabled={attachedID !== undefined}
{...register('item_data.alias')}
error={errors.item_data?.alias}
/>
<TextArea
id='operation_comment'
id='operation_comment' //
label='Описание'
noResize
rows={3}
value={comment}
onChange={event => onChangeComment(event.target.value)}
disabled={attachedID !== undefined}
{...register('item_data.comment')}
/>
</div>
@ -89,26 +86,30 @@ function TabInputOperation({
noHover
noPadding
icon={<IconReset size='1.25rem' className='icon-primary' />}
onClick={() => onChangeAttachedID(undefined)}
disabled={attachedID == undefined}
onClick={() => setValue('item_data.result', null)}
/>
</div>
<Checkbox
value={createSchema}
onChange={onChangeCreateSchema}
value={createSchema} //
onChange={handleChangeCreateSchema}
label='Создать новую схему'
titleHtml='Создать пустую схему для загрузки'
/>
</div>
{!createSchema ? (
<Controller
control={control}
name='item_data.result'
render={({ field }) => (
<PickSchema
items={sortedItems}
value={attachedID ?? null}
value={field.value}
itemType={LibraryItemType.RSFORM}
onChange={onChangeAttachedID}
onChange={handleSetInput}
rows={8}
baseFilter={baseFilter}
/>
)}
/>
) : null}
</div>
);

View File

@ -1,47 +1,39 @@
import { Controller, useFormContext, useWatch } from 'react-hook-form';
import { FlexColumn } from '@/components/Container';
import { Label, TextArea, TextInput } from '@/components/Input';
import { IOperationCreateDTO } from '../../backend/api';
import PickMultiOperation from '../../components/PickMultiOperation';
import { IOperationSchema, OperationID } from '../../models/oss';
import { IOperationSchema } from '../../models/oss';
interface TabSynthesisOperationProps {
oss: IOperationSchema;
alias: string;
onChangeAlias: (newValue: string) => void;
title: string;
onChangeTitle: (newValue: string) => void;
comment: string;
onChangeComment: (newValue: string) => void;
inputs: OperationID[];
setInputs: React.Dispatch<React.SetStateAction<OperationID[]>>;
}
function TabSynthesisOperation({
oss,
alias,
onChangeAlias,
title,
onChangeTitle,
comment,
onChangeComment,
inputs,
setInputs
}: TabSynthesisOperationProps) {
function TabSynthesisOperation({ oss }: TabSynthesisOperationProps) {
const {
register,
control,
formState: { errors }
} = useFormContext<IOperationCreateDTO>();
const inputs = useWatch({ control, name: 'arguments' });
return (
<div className='cc-fade-in cc-column'>
<TextInput
id='operation_title'
label='Полное название'
value={title}
onChange={event => onChangeTitle(event.target.value)}
{...register('item_data.title')}
error={errors.item_data?.title}
/>
<div className='flex gap-6'>
<TextInput
id='operation_alias'
label='Сокращение'
className='w-[16rem]'
value={alias}
onChange={event => onChangeAlias(event.target.value)}
{...register('item_data.alias')}
error={errors.item_data?.alias}
/>
<TextArea
@ -49,14 +41,21 @@ function TabSynthesisOperation({
label='Описание'
noResize
rows={3}
value={comment}
onChange={event => onChangeComment(event.target.value)}
{...register('item_data.comment')}
error={errors.item_data?.comment}
/>
</div>
<FlexColumn>
<Label text={`Выбор аргументов: [ ${inputs.length} ]`} />
<PickMultiOperation items={oss.items} value={inputs} onChange={setInputs} rows={6} />
<Controller
name='arguments'
control={control}
defaultValue={[]}
render={({ field }) => (
<PickMultiOperation items={oss.items} value={field.value} onChange={field.onChange} rows={6} />
)}
/>
</FlexColumn>
</div>
);

View File

@ -56,8 +56,6 @@ function DlgRelocateConstituents() {
libraryItems.find(item => item.id === initialTarget?.result)
);
console.log(isValid);
const operation = oss.items.find(item => item.result === source?.id);
const sourceSchemas = libraryItems.filter(item => oss.schemas.includes(item.id));
const destinationSchemas = (() => {

View File

@ -488,7 +488,7 @@ export function calculateInsertPosition(
return result;
}
if (operationType === OperationType.INPUT) {
if (operationType === OperationType.INPUT || argumentsOps.length === 0) {
let inputsNodes = positions.filter(pos =>
oss.items.find(operation => operation.operation_type === OperationType.INPUT && operation.id === pos.id)
);

View File

@ -143,7 +143,7 @@ function OssFlow() {
defaultY: target.y,
inputs: inputs,
positions: positions,
callback: () => flow.fitView({ duration: PARAMETER.zoomDuration })
callback: () => setTimeout(() => flow.fitView({ duration: PARAMETER.zoomDuration }), PARAMETER.refreshTimeout)
});
}

View File

@ -12,15 +12,12 @@ import { UserRole } from '@/features/users/models/user';
import { useDialogsStore } from '@/stores/dialogs';
import { usePreferencesStore } from '@/stores/preferences';
import { useRoleStore } from '@/stores/role';
import { PARAMETER } from '@/utils/constants';
import { prompts } from '@/utils/labels';
import { IOperationPosition } from '../../backend/api';
import { useOperationCreate } from '../../backend/useOperationCreate';
import { useOperationUpdate } from '../../backend/useOperationUpdate';
import { useOssSuspense } from '../../backend/useOSS';
import { IOperationSchema, OperationID, OperationType } from '../../models/oss';
import { calculateInsertPosition } from '../../models/ossAPI';
export enum OssTabID {
CARD = 0,
@ -98,7 +95,6 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
const showCreateOperation = useDialogsStore(state => state.showCreateOperation);
const { deleteItem } = useDeleteItem();
const { operationCreate } = useOperationCreate();
const { operationUpdate } = useOperationUpdate();
useEffect(
@ -143,21 +139,11 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
function promptCreateOperation({ defaultX, defaultY, inputs, positions, callback }: ICreateOperationPrompt) {
showCreateOperation({
oss: schema,
onCreate: data => {
const target = calculateInsertPosition(schema, data.item_data.operation_type, data.arguments ?? [], positions, {
x: defaultX,
y: defaultY
});
data.positions = positions;
data.item_data.position_x = target.x;
data.item_data.position_y = target.y;
operationCreate({ itemID: schema.id, data }, operation => {
if (callback) {
setTimeout(() => callback(operation.id), PARAMETER.refreshTimeout);
}
});
},
initialInputs: inputs
defaultX: defaultX,
defaultY: defaultY,
positions: positions,
initialInputs: inputs,
onCreate: callback
});
}

View File

@ -10,7 +10,7 @@ import { VisibilityIcon } from '@/components/DomainIcons';
import { Checkbox, Label, TextArea, TextInput } from '@/components/Input';
import { ModalForm } from '@/components/Modal';
import { useAuthSuspense } from '@/features/auth/backend/useAuth';
import { schemaCloneLibraryItem, ICloneLibraryItemDTO } from '@/features/library/backend/api';
import { ICloneLibraryItemDTO, schemaCloneLibraryItem } from '@/features/library/backend/api';
import { useCloneItem } from '@/features/library/backend/useCloneItem';
import SelectAccessPolicy from '@/features/library/components/SelectAccessPolicy';
import SelectLocationContext from '@/features/library/components/SelectLocationContext';