F: Implementing block UI pt1
This commit is contained in:
parent
2ae9576384
commit
13914a04f9
|
@ -198,7 +198,9 @@ class UpdateOperationSerializer(serializers.Serializer):
|
||||||
'target': msg.operationNotInOSS()
|
'target': msg.operationNotInOSS()
|
||||||
})
|
})
|
||||||
|
|
||||||
if 'parent' in attrs['item_data'] and attrs['item_data']['parent'].oss_id != oss.pk:
|
if 'parent' in attrs['item_data'] and \
|
||||||
|
attrs['item_data']['parent'] is not None and \
|
||||||
|
attrs['item_data']['parent'].oss_id != oss.pk:
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
'parent': msg.parentNotInOSS()
|
'parent': msg.parentNotInOSS()
|
||||||
})
|
})
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { NavigationState } from './navigation/navigation-context';
|
import { NavigationState } from './navigation/navigation-context';
|
||||||
import { Footer } from './footer';
|
import { Footer } from './footer';
|
||||||
import { GlobalDialogs } from './global-dialogs';
|
import { GlobalDialogs } from './global-dialogs';
|
||||||
import { GlobalLoader } from './global-Loader';
|
import { GlobalLoader } from './global-loader1';
|
||||||
import { ToasterThemed } from './global-toaster';
|
import { ToasterThemed } from './global-toaster';
|
||||||
import { GlobalTooltips } from './global-tooltips';
|
import { GlobalTooltips } from './global-tooltips';
|
||||||
import { MutationErrors } from './mutation-errors';
|
import { MutationErrors } from './mutation-errors';
|
||||||
|
|
|
@ -113,6 +113,11 @@ const DlgUploadRSForm = React.lazy(() =>
|
||||||
const DlgGraphParams = React.lazy(() =>
|
const DlgGraphParams = React.lazy(() =>
|
||||||
import('@/features/rsform/dialogs/dlg-graph-params').then(module => ({ default: module.DlgGraphParams }))
|
import('@/features/rsform/dialogs/dlg-graph-params').then(module => ({ default: module.DlgGraphParams }))
|
||||||
);
|
);
|
||||||
|
const DlgCreateBlock = React.lazy(() =>
|
||||||
|
import('@/features/oss/dialogs/dlg-create-block').then(module => ({
|
||||||
|
default: module.DlgCreateBlock
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
export const GlobalDialogs = () => {
|
export const GlobalDialogs = () => {
|
||||||
const active = useDialogsStore(state => state.active);
|
const active = useDialogsStore(state => state.active);
|
||||||
|
@ -127,6 +132,8 @@ export const GlobalDialogs = () => {
|
||||||
return <DlgCreateCst />;
|
return <DlgCreateCst />;
|
||||||
case DialogType.CREATE_OPERATION:
|
case DialogType.CREATE_OPERATION:
|
||||||
return <DlgCreateOperation />;
|
return <DlgCreateOperation />;
|
||||||
|
case DialogType.CREATE_BLOCK:
|
||||||
|
return <DlgCreateBlock />;
|
||||||
case DialogType.DELETE_CONSTITUENTA:
|
case DialogType.DELETE_CONSTITUENTA:
|
||||||
return <DlgDeleteCst />;
|
return <DlgDeleteCst />;
|
||||||
case DialogType.EDIT_EDITORS:
|
case DialogType.EDIT_EDITORS:
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { useMutationErrors } from '@/backend/use-mutation-errors';
|
import { useMutationErrors } from '@/backend/use-mutation-errors';
|
||||||
import { Button } from '@/components/control';
|
import { Button } from '@/components/control';
|
||||||
import { DescribeError } from '@/components/info-error';
|
import { DescribeError } from '@/components/info-error';
|
||||||
|
@ -20,9 +22,23 @@ export function MutationErrors() {
|
||||||
return (
|
return (
|
||||||
<div className='cc-modal-wrapper'>
|
<div className='cc-modal-wrapper'>
|
||||||
<ModalBackdrop onHide={resetErrors} />
|
<ModalBackdrop onHide={resetErrors} />
|
||||||
<div className='z-pop px-10 py-3 flex flex-col items-center border rounded-xl bg-background' role='alertdialog'>
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'z-pop', //
|
||||||
|
'flex flex-col px-10 py-3 items-center',
|
||||||
|
'border rounded-xl bg-background'
|
||||||
|
)}
|
||||||
|
role='alertdialog'
|
||||||
|
>
|
||||||
<h1 className='py-2 select-none'>Ошибка при обработке</h1>
|
<h1 className='py-2 select-none'>Ошибка при обработке</h1>
|
||||||
<div className='px-3 flex flex-col text-destructive text-sm font-semibold select-text'>
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'max-h-[calc(100svh-8rem)] max-w-[calc(100svw-2rem)]',
|
||||||
|
'px-3 flex flex-col',
|
||||||
|
'text-destructive text-sm font-semibold select-text',
|
||||||
|
'overflow-auto'
|
||||||
|
)}
|
||||||
|
>
|
||||||
<DescribeError error={mutationErrors[0]} />
|
<DescribeError error={mutationErrors[0]} />
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={resetErrors} className='w-fit' text='Закрыть' />
|
<Button onClick={resetErrors} className='w-fit' text='Закрыть' />
|
||||||
|
|
|
@ -70,6 +70,7 @@ export { LuGlasses as IconReader } from 'react-icons/lu';
|
||||||
export { TbBriefcase as IconBusiness } from 'react-icons/tb';
|
export { TbBriefcase as IconBusiness } from 'react-icons/tb';
|
||||||
export { VscLibrary as IconLibrary } from 'react-icons/vsc';
|
export { VscLibrary as IconLibrary } from 'react-icons/vsc';
|
||||||
export { BiBot as IconRobot } from 'react-icons/bi';
|
export { BiBot as IconRobot } from 'react-icons/bi';
|
||||||
|
export { TbGps as IconCoordinates } from 'react-icons/tb';
|
||||||
export { IoLibrary as IconLibrary2 } from 'react-icons/io5';
|
export { IoLibrary as IconLibrary2 } from 'react-icons/io5';
|
||||||
export { BiDiamond as IconTemplates } from 'react-icons/bi';
|
export { BiDiamond as IconTemplates } from 'react-icons/bi';
|
||||||
export { TbHexagons as IconOSS } from 'react-icons/tb';
|
export { TbHexagons as IconOSS } from 'react-icons/tb';
|
||||||
|
|
|
@ -20,6 +20,7 @@ export function TabLabel({
|
||||||
titleHtml,
|
titleHtml,
|
||||||
hideTitle,
|
hideTitle,
|
||||||
className,
|
className,
|
||||||
|
disabled,
|
||||||
role = 'tab',
|
role = 'tab',
|
||||||
...otherProps
|
...otherProps
|
||||||
}: TabLabelProps) {
|
}: TabLabelProps) {
|
||||||
|
@ -28,10 +29,12 @@ export function TabLabel({
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'min-w-20 h-full',
|
'min-w-20 h-full',
|
||||||
'px-2 py-1 flex justify-center',
|
'px-2 py-1 flex justify-center',
|
||||||
'cc-hover cc-animate-color duration-select',
|
'cc-animate-color duration-select',
|
||||||
'text-sm whitespace-nowrap font-controls',
|
'text-sm whitespace-nowrap font-controls',
|
||||||
'select-none hover:cursor-pointer',
|
'select-none',
|
||||||
'outline-hidden',
|
'outline-hidden',
|
||||||
|
!disabled && 'hover:cursor-pointer cc-hover',
|
||||||
|
disabled && 'text-muted-foreground',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
tabIndex='-1'
|
tabIndex='-1'
|
||||||
|
@ -40,6 +43,7 @@ export function TabLabel({
|
||||||
data-tooltip-content={title}
|
data-tooltip-content={title}
|
||||||
data-tooltip-hidden={hideTitle}
|
data-tooltip-hidden={hideTitle}
|
||||||
role={role}
|
role={role}
|
||||||
|
disabled={disabled}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import {
|
||||||
type IUpdateBlockDTO,
|
type IUpdateBlockDTO,
|
||||||
type IUpdateInputDTO,
|
type IUpdateInputDTO,
|
||||||
type IUpdateOperationDTO,
|
type IUpdateOperationDTO,
|
||||||
|
schemaBlockCreatedResponse,
|
||||||
schemaConstituentaReference,
|
schemaConstituentaReference,
|
||||||
schemaInputCreatedResponse,
|
schemaInputCreatedResponse,
|
||||||
schemaOperationCreatedResponse,
|
schemaOperationCreatedResponse,
|
||||||
|
@ -55,7 +56,7 @@ export const ossApi = {
|
||||||
|
|
||||||
createBlock: ({ itemID, data }: { itemID: number; data: ICreateBlockDTO }) =>
|
createBlock: ({ itemID, data }: { itemID: number; data: ICreateBlockDTO }) =>
|
||||||
axiosPost<ICreateBlockDTO, IBlockCreatedResponse>({
|
axiosPost<ICreateBlockDTO, IBlockCreatedResponse>({
|
||||||
schema: schemaOperationCreatedResponse,
|
schema: schemaBlockCreatedResponse,
|
||||||
endpoint: `/api/oss/${itemID}/create-block`,
|
endpoint: `/api/oss/${itemID}/create-block`,
|
||||||
request: {
|
request: {
|
||||||
data: data,
|
data: data,
|
||||||
|
@ -74,7 +75,7 @@ export const ossApi = {
|
||||||
deleteBlock: ({ itemID, data }: { itemID: number; data: IDeleteBlockDTO }) =>
|
deleteBlock: ({ itemID, data }: { itemID: number; data: IDeleteBlockDTO }) =>
|
||||||
axiosPatch<IDeleteBlockDTO, IOperationSchemaDTO>({
|
axiosPatch<IDeleteBlockDTO, IOperationSchemaDTO>({
|
||||||
schema: schemaOperationSchema,
|
schema: schemaOperationSchema,
|
||||||
endpoint: `/api/oss/${itemID}/delete-operation`,
|
endpoint: `/api/oss/${itemID}/delete-block`,
|
||||||
request: {
|
request: {
|
||||||
data: data,
|
data: data,
|
||||||
successMessage: infoMsg.operationDestroyed
|
successMessage: infoMsg.operationDestroyed
|
||||||
|
|
|
@ -7,12 +7,10 @@ import { type ILibraryItem } from '@/features/library';
|
||||||
import { Graph } from '@/models/graph';
|
import { Graph } from '@/models/graph';
|
||||||
|
|
||||||
import { type IBlock, type IOperation, type IOperationSchema, type IOperationSchemaStats } from '../models/oss';
|
import { type IBlock, type IOperation, type IOperationSchema, type IOperationSchemaStats } from '../models/oss';
|
||||||
|
import { BLOCK_NODE_MIN_HEIGHT, BLOCK_NODE_MIN_WIDTH } from '../pages/oss-page/editor-oss-graph/graph/block-node';
|
||||||
|
|
||||||
import { type IOperationSchemaDTO, OperationType } from './types';
|
import { type IOperationSchemaDTO, OperationType } from './types';
|
||||||
|
|
||||||
export const DEFAULT_BLOCK_WIDTH = 100;
|
|
||||||
export const DEFAULT_BLOCK_HEIGHT = 100;
|
|
||||||
|
|
||||||
/** Loads data into an {@link IOperationSchema} based on {@link IOperationSchemaDTO}. */
|
/** Loads data into an {@link IOperationSchema} based on {@link IOperationSchemaDTO}. */
|
||||||
export class OssLoader {
|
export class OssLoader {
|
||||||
private oss: IOperationSchema;
|
private oss: IOperationSchema;
|
||||||
|
@ -58,7 +56,7 @@ export class OssLoader {
|
||||||
this.blockByID.set(block.id, block);
|
this.blockByID.set(block.id, block);
|
||||||
this.hierarchy.addNode(-block.id);
|
this.hierarchy.addNode(-block.id);
|
||||||
if (block.parent) {
|
if (block.parent) {
|
||||||
this.graph.addEdge(-block.parent, -block.id);
|
this.hierarchy.addEdge(-block.parent, -block.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -92,8 +90,8 @@ export class OssLoader {
|
||||||
const geometry = this.oss.layout.blocks.find(item => item.id === block.id);
|
const geometry = this.oss.layout.blocks.find(item => item.id === block.id);
|
||||||
block.x = geometry?.x ?? 0;
|
block.x = geometry?.x ?? 0;
|
||||||
block.y = geometry?.y ?? 0;
|
block.y = geometry?.y ?? 0;
|
||||||
block.width = geometry?.width ?? DEFAULT_BLOCK_WIDTH;
|
block.width = geometry?.width ?? BLOCK_NODE_MIN_WIDTH;
|
||||||
block.height = geometry?.height ?? DEFAULT_BLOCK_HEIGHT;
|
block.height = geometry?.height ?? BLOCK_NODE_MIN_HEIGHT;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
156
rsconcept/frontend/src/features/oss/components/pick-contents.tsx
Normal file
156
rsconcept/frontend/src/features/oss/components/pick-contents.tsx
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { MiniButton } from '@/components/control';
|
||||||
|
import { createColumnHelper, DataTable } from '@/components/data-table';
|
||||||
|
import { IconMoveDown, IconMoveUp, IconRemove } from '@/components/icons';
|
||||||
|
import { ComboBox } from '@/components/input/combo-box';
|
||||||
|
import { type Styling } from '@/components/props';
|
||||||
|
import { cn } from '@/components/utils';
|
||||||
|
import { NoData } from '@/components/view';
|
||||||
|
|
||||||
|
import { labelOssItem } from '../labels';
|
||||||
|
import { type IBlock, type IOperation, type IOperationSchema } from '../models/oss';
|
||||||
|
import { isOperation } from '../models/oss-api';
|
||||||
|
|
||||||
|
const SELECTION_CLEAR_TIMEOUT = 1000;
|
||||||
|
|
||||||
|
interface PickMultiOperationProps extends Styling {
|
||||||
|
value: number[];
|
||||||
|
onChange: (newValue: number[]) => void;
|
||||||
|
schema: IOperationSchema;
|
||||||
|
rows?: number;
|
||||||
|
disallowBlocks?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<IOperation | IBlock>();
|
||||||
|
|
||||||
|
export function PickContents({
|
||||||
|
rows,
|
||||||
|
schema,
|
||||||
|
value,
|
||||||
|
disallowBlocks,
|
||||||
|
onChange,
|
||||||
|
className,
|
||||||
|
...restProps
|
||||||
|
}: PickMultiOperationProps) {
|
||||||
|
const selectedItems = value
|
||||||
|
.map(itemID => (itemID > 0 ? schema.operationByID.get(itemID) : schema.blockByID.get(-itemID)))
|
||||||
|
.filter(item => item !== undefined);
|
||||||
|
const [lastSelected, setLastSelected] = useState<IOperation | IBlock | null>(null);
|
||||||
|
const items = [
|
||||||
|
...schema.operations.filter(item => !value.includes(item.id)),
|
||||||
|
...(disallowBlocks ? [] : schema.blocks.filter(item => !value.includes(-item.id)))
|
||||||
|
];
|
||||||
|
|
||||||
|
function handleDelete(operation: number) {
|
||||||
|
onChange(value.filter(item => item !== operation));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelect(target: IOperation | IBlock | null) {
|
||||||
|
if (target) {
|
||||||
|
setLastSelected(target);
|
||||||
|
onChange([...value, isOperation(target) ? target.id : -target.id]);
|
||||||
|
setTimeout(() => setLastSelected(null), SELECTION_CLEAR_TIMEOUT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMoveUp(operation: number) {
|
||||||
|
const index = value.indexOf(operation);
|
||||||
|
if (index > 0) {
|
||||||
|
const newSelected = [...value];
|
||||||
|
newSelected[index] = newSelected[index - 1];
|
||||||
|
newSelected[index - 1] = operation;
|
||||||
|
onChange(newSelected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMoveDown(operation: number) {
|
||||||
|
const index = value.indexOf(operation);
|
||||||
|
if (index < value.length - 1) {
|
||||||
|
const newSelected = [...value];
|
||||||
|
newSelected[index] = newSelected[index + 1];
|
||||||
|
newSelected[index + 1] = operation;
|
||||||
|
onChange(newSelected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
columnHelper.accessor(item => isOperation(item), {
|
||||||
|
id: 'type',
|
||||||
|
header: 'Тип',
|
||||||
|
size: 150,
|
||||||
|
minSize: 150,
|
||||||
|
maxSize: 150,
|
||||||
|
cell: props => <div>{isOperation(props.row.original) ? 'Операция' : 'Блок'}</div>
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('title', {
|
||||||
|
id: 'title',
|
||||||
|
header: 'Название',
|
||||||
|
size: 1200,
|
||||||
|
minSize: 300,
|
||||||
|
maxSize: 1200,
|
||||||
|
cell: props => <div className='text-ellipsis'>{props.getValue()}</div>
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: 'actions',
|
||||||
|
size: 0,
|
||||||
|
cell: props => (
|
||||||
|
<div className='flex w-fit'>
|
||||||
|
<MiniButton
|
||||||
|
title='Удалить'
|
||||||
|
noHover
|
||||||
|
className='px-0'
|
||||||
|
icon={<IconRemove size='1rem' className='icon-red' />}
|
||||||
|
onClick={() => handleDelete(props.row.original.id)}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
title='Переместить выше'
|
||||||
|
noHover
|
||||||
|
className='px-0'
|
||||||
|
icon={<IconMoveUp size='1rem' className='icon-primary' />}
|
||||||
|
onClick={() => handleMoveUp(props.row.original.id)}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
title='Переместить ниже'
|
||||||
|
noHover
|
||||||
|
className='px-0'
|
||||||
|
icon={<IconMoveDown size='1rem' className='icon-primary' />}
|
||||||
|
onClick={() => handleMoveDown(props.row.original.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('flex flex-col gap-1 border-t border-x rounded-md bg-input', className)} {...restProps}>
|
||||||
|
<ComboBox
|
||||||
|
noBorder
|
||||||
|
items={items}
|
||||||
|
value={lastSelected}
|
||||||
|
placeholder='Выберите операцию или блок'
|
||||||
|
idFunc={item => String(item.id)}
|
||||||
|
labelValueFunc={item => labelOssItem(item)}
|
||||||
|
labelOptionFunc={item => labelOssItem(item)}
|
||||||
|
onChange={handleSelect}
|
||||||
|
/>
|
||||||
|
<DataTable
|
||||||
|
dense
|
||||||
|
noFooter
|
||||||
|
rows={rows}
|
||||||
|
contentHeight='1.3rem'
|
||||||
|
className='cc-scroll-y text-sm select-none border-y rounded-b-md'
|
||||||
|
data={selectedItems}
|
||||||
|
columns={columns}
|
||||||
|
headPosition='0rem'
|
||||||
|
noDataComponent={
|
||||||
|
<NoData>
|
||||||
|
<p>Список пуст</p>
|
||||||
|
</NoData>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { ComboBox } from '@/components/input/combo-box';
|
||||||
|
import { type Styling } from '@/components/props';
|
||||||
|
|
||||||
|
import { type IBlock } from '../models/oss';
|
||||||
|
|
||||||
|
interface SelectBlockProps extends Styling {
|
||||||
|
id?: string;
|
||||||
|
value: IBlock | null;
|
||||||
|
onChange: (newValue: IBlock | null) => void;
|
||||||
|
|
||||||
|
items?: IBlock[];
|
||||||
|
placeholder?: string;
|
||||||
|
noBorder?: boolean;
|
||||||
|
popoverClassname?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SelectBlock({ items, placeholder = 'Выберите блок', ...restProps }: SelectBlockProps) {
|
||||||
|
return (
|
||||||
|
<ComboBox
|
||||||
|
items={items}
|
||||||
|
placeholder={placeholder}
|
||||||
|
idFunc={block => String(block.id)}
|
||||||
|
labelValueFunc={block => block.title}
|
||||||
|
labelOptionFunc={block => block.title}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { FormProvider, useForm, useWatch } from 'react-hook-form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|
||||||
|
import { HelpTopic } from '@/features/help';
|
||||||
|
|
||||||
|
import { ModalForm } from '@/components/modal';
|
||||||
|
import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs';
|
||||||
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
|
||||||
|
import { type ICreateBlockDTO, type IOssLayout, schemaCreateBlock } from '../../backend/types';
|
||||||
|
import { useCreateBlock } from '../../backend/use-create-block';
|
||||||
|
import { type IOperationSchema } from '../../models/oss';
|
||||||
|
import { calculateNewBlockPosition } from '../../models/oss-api';
|
||||||
|
import { BLOCK_NODE_MIN_HEIGHT, BLOCK_NODE_MIN_WIDTH } from '../../pages/oss-page/editor-oss-graph/graph/block-node';
|
||||||
|
|
||||||
|
import { TabBlockCard } from './tab-block-card';
|
||||||
|
import { TabBlockChildren } from './tab-block-children';
|
||||||
|
|
||||||
|
export interface DlgCreateBlockProps {
|
||||||
|
oss: IOperationSchema;
|
||||||
|
layout: IOssLayout;
|
||||||
|
initialInputs: number[];
|
||||||
|
defaultX: number;
|
||||||
|
defaultY: number;
|
||||||
|
onCreate?: (newID: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TabID = {
|
||||||
|
CARD: 0,
|
||||||
|
CHILDREN: 1
|
||||||
|
} as const;
|
||||||
|
export type TabID = (typeof TabID)[keyof typeof TabID];
|
||||||
|
|
||||||
|
export function DlgCreateBlock() {
|
||||||
|
const { createBlock } = useCreateBlock();
|
||||||
|
|
||||||
|
const { oss, layout, initialInputs, onCreate, defaultX, defaultY } = useDialogsStore(
|
||||||
|
state => state.props as DlgCreateBlockProps
|
||||||
|
);
|
||||||
|
|
||||||
|
const methods = useForm<ICreateBlockDTO>({
|
||||||
|
resolver: zodResolver(schemaCreateBlock),
|
||||||
|
defaultValues: {
|
||||||
|
item_data: {
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
parent: null
|
||||||
|
},
|
||||||
|
position_x: defaultX,
|
||||||
|
position_y: defaultY,
|
||||||
|
width: BLOCK_NODE_MIN_WIDTH,
|
||||||
|
height: BLOCK_NODE_MIN_HEIGHT,
|
||||||
|
children_blocks: initialInputs.filter(id => id < 0).map(id => -id),
|
||||||
|
children_operations: initialInputs.filter(id => id > 0),
|
||||||
|
layout: layout
|
||||||
|
},
|
||||||
|
mode: 'onChange'
|
||||||
|
});
|
||||||
|
const title = useWatch({ control: methods.control, name: 'item_data.title' });
|
||||||
|
const [activeTab, setActiveTab] = useState<TabID>(TabID.CARD);
|
||||||
|
const isValid = !!title && !oss.blocks.some(block => block.title === title);
|
||||||
|
|
||||||
|
function onSubmit(data: ICreateBlockDTO) {
|
||||||
|
const rectangle = calculateNewBlockPosition(data, layout);
|
||||||
|
data.position_x = rectangle.x;
|
||||||
|
data.position_y = rectangle.y;
|
||||||
|
data.width = rectangle.width;
|
||||||
|
data.height = rectangle.height;
|
||||||
|
void createBlock({ itemID: oss.id, data: data }).then(response => onCreate?.(response.new_block.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalForm
|
||||||
|
header='Создание операции'
|
||||||
|
submitText='Создать'
|
||||||
|
canSubmit={isValid}
|
||||||
|
onSubmit={event => void methods.handleSubmit(onSubmit)(event)}
|
||||||
|
className='w-160 px-6 h-128'
|
||||||
|
helpTopic={HelpTopic.CC_OSS}
|
||||||
|
>
|
||||||
|
<Tabs
|
||||||
|
selectedTabClassName='cc-selected'
|
||||||
|
className='grid'
|
||||||
|
selectedIndex={activeTab}
|
||||||
|
onSelect={index => setActiveTab(index as TabID)}
|
||||||
|
>
|
||||||
|
<TabList className='z-pop mx-auto -mb-5 flex border divide-x rounded-none bg-secondary'>
|
||||||
|
<TabLabel title='Основные атрибуты блока' label='Карточка' />
|
||||||
|
<TabLabel title='Выбор вложенных узлов' label='Содержимое' />
|
||||||
|
</TabList>
|
||||||
|
|
||||||
|
<FormProvider {...methods}>
|
||||||
|
<TabPanel>
|
||||||
|
<TabBlockCard />
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
<TabPanel>
|
||||||
|
<TabBlockChildren />
|
||||||
|
</TabPanel>
|
||||||
|
</FormProvider>
|
||||||
|
</Tabs>
|
||||||
|
</ModalForm>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export { DlgCreateBlock } from './dlg-create-block';
|
|
@ -0,0 +1,52 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { TextArea, TextInput } from '@/components/input';
|
||||||
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
|
||||||
|
import { type ICreateBlockDTO } from '../../backend/types';
|
||||||
|
import { SelectBlock } from '../../components/select-block';
|
||||||
|
|
||||||
|
import { type DlgCreateBlockProps } from './dlg-create-block';
|
||||||
|
|
||||||
|
export function TabBlockCard() {
|
||||||
|
const { oss } = useDialogsStore(state => state.props as DlgCreateBlockProps);
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
control,
|
||||||
|
formState: { errors }
|
||||||
|
} = useFormContext<ICreateBlockDTO>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='cc-fade-in cc-column'>
|
||||||
|
<TextInput
|
||||||
|
id='operation_title' //
|
||||||
|
label='Название'
|
||||||
|
{...register('item_data.title')}
|
||||||
|
error={errors.item_data?.title}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name='item_data.parent'
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<SelectBlock
|
||||||
|
items={oss.blocks}
|
||||||
|
className='w-80'
|
||||||
|
value={field.value ? oss.blockByID.get(field.value) ?? null : null}
|
||||||
|
placeholder='Блок содержания не выбран'
|
||||||
|
onChange={value => field.onChange(value ? value.id : null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
id='operation_comment' //
|
||||||
|
label='Описание'
|
||||||
|
noResize
|
||||||
|
rows={3}
|
||||||
|
{...register('item_data.description')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
'use client';
|
||||||
|
import { useFormContext, useWatch } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { Label } from '@/components/input';
|
||||||
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
|
||||||
|
import { type ICreateBlockDTO } from '../../backend/types';
|
||||||
|
import { PickContents } from '../../components/pick-contents';
|
||||||
|
|
||||||
|
import { type DlgCreateBlockProps } from './dlg-create-block';
|
||||||
|
|
||||||
|
export function TabBlockChildren() {
|
||||||
|
const { setValue, control } = useFormContext<ICreateBlockDTO>();
|
||||||
|
const { oss } = useDialogsStore(state => state.props as DlgCreateBlockProps);
|
||||||
|
const children_blocks = useWatch({ control, name: 'children_blocks' });
|
||||||
|
const children_operations = useWatch({ control, name: 'children_operations' });
|
||||||
|
|
||||||
|
const value = [...children_blocks.map(id => -id), ...children_operations];
|
||||||
|
|
||||||
|
function handleChangeSelected(newValue: number[]) {
|
||||||
|
setValue(
|
||||||
|
'children_blocks',
|
||||||
|
newValue.filter(id => id < 0).map(id => -id),
|
||||||
|
{ shouldValidate: true }
|
||||||
|
);
|
||||||
|
setValue(
|
||||||
|
'children_operations',
|
||||||
|
newValue.filter(id => id > 0),
|
||||||
|
{ shouldValidate: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='cc-fade-in cc-column'>
|
||||||
|
<Label text={`Выбор содержания: [ ${value.length} ]`} />
|
||||||
|
<PickContents schema={oss} value={value} onChange={newValue => handleChangeSelected(newValue)} rows={8} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ import { type ICreateOperationDTO, type IOssLayout, OperationType, schemaCreateO
|
||||||
import { useCreateOperation } from '../../backend/use-create-operation';
|
import { useCreateOperation } from '../../backend/use-create-operation';
|
||||||
import { describeOperationType, labelOperationType } from '../../labels';
|
import { describeOperationType, labelOperationType } from '../../labels';
|
||||||
import { type IOperationSchema } from '../../models/oss';
|
import { type IOperationSchema } from '../../models/oss';
|
||||||
import { calculateInsertPosition } from '../../models/oss-api';
|
import { calculateNewOperationPosition } from '../../models/oss-api';
|
||||||
|
|
||||||
import { TabInputOperation } from './tab-input-operation';
|
import { TabInputOperation } from './tab-input-operation';
|
||||||
import { TabSynthesisOperation } from './tab-synthesis-operation';
|
import { TabSynthesisOperation } from './tab-synthesis-operation';
|
||||||
|
@ -22,6 +22,7 @@ import { TabSynthesisOperation } from './tab-synthesis-operation';
|
||||||
export interface DlgCreateOperationProps {
|
export interface DlgCreateOperationProps {
|
||||||
oss: IOperationSchema;
|
oss: IOperationSchema;
|
||||||
layout: IOssLayout;
|
layout: IOssLayout;
|
||||||
|
initialParent: number | null;
|
||||||
initialInputs: number[];
|
initialInputs: number[];
|
||||||
defaultX: number;
|
defaultX: number;
|
||||||
defaultY: number;
|
defaultY: number;
|
||||||
|
@ -35,9 +36,9 @@ export const TabID = {
|
||||||
export type TabID = (typeof TabID)[keyof typeof TabID];
|
export type TabID = (typeof TabID)[keyof typeof TabID];
|
||||||
|
|
||||||
export function DlgCreateOperation() {
|
export function DlgCreateOperation() {
|
||||||
const { createOperation: operationCreate } = useCreateOperation();
|
const { createOperation } = useCreateOperation();
|
||||||
|
|
||||||
const { oss, layout, initialInputs, onCreate, defaultX, defaultY } = useDialogsStore(
|
const { oss, layout, initialInputs, initialParent, onCreate, defaultX, defaultY } = useDialogsStore(
|
||||||
state => state.props as DlgCreateOperationProps
|
state => state.props as DlgCreateOperationProps
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -50,7 +51,7 @@ export function DlgCreateOperation() {
|
||||||
title: '',
|
title: '',
|
||||||
description: '',
|
description: '',
|
||||||
result: null,
|
result: null,
|
||||||
parent: null
|
parent: initialParent
|
||||||
},
|
},
|
||||||
position_x: defaultX,
|
position_x: defaultX,
|
||||||
position_y: defaultY,
|
position_y: defaultY,
|
||||||
|
@ -65,13 +66,10 @@ export function DlgCreateOperation() {
|
||||||
const isValid = !!alias && !oss.operations.some(operation => operation.alias === alias);
|
const isValid = !!alias && !oss.operations.some(operation => operation.alias === alias);
|
||||||
|
|
||||||
function onSubmit(data: ICreateOperationDTO) {
|
function onSubmit(data: ICreateOperationDTO) {
|
||||||
const target = calculateInsertPosition(oss, data.arguments, layout, {
|
const target = calculateNewOperationPosition(oss, data, layout);
|
||||||
x: defaultX,
|
|
||||||
y: defaultY
|
|
||||||
});
|
|
||||||
data.position_x = target.x;
|
data.position_x = target.x;
|
||||||
data.position_y = target.y;
|
data.position_y = target.y;
|
||||||
void operationCreate({ itemID: oss.id, data: data }).then(response => onCreate?.(response.new_operation.id));
|
void createOperation({ itemID: oss.id, data: data }).then(response => onCreate?.(response.new_operation.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSelectTab(newTab: TabID, last: TabID) {
|
function handleSelectTab(newTab: TabID, last: TabID) {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { Checkbox, Label, TextArea, TextInput } from '@/components/input';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
|
||||||
import { type ICreateOperationDTO } from '../../backend/types';
|
import { type ICreateOperationDTO } from '../../backend/types';
|
||||||
|
import { SelectBlock } from '../../components/select-block';
|
||||||
import { sortItemsForOSS } from '../../models/oss-api';
|
import { sortItemsForOSS } from '../../models/oss-api';
|
||||||
|
|
||||||
import { type DlgCreateOperationProps } from './dlg-create-operation';
|
import { type DlgCreateOperationProps } from './dlg-create-operation';
|
||||||
|
@ -61,14 +62,27 @@ export function TabInputOperation() {
|
||||||
error={errors.item_data?.title}
|
error={errors.item_data?.title}
|
||||||
/>
|
/>
|
||||||
<div className='flex gap-6'>
|
<div className='flex gap-6'>
|
||||||
<TextInput
|
<div className='grid gap-1'>
|
||||||
id='operation_alias' //
|
<TextInput
|
||||||
label='Сокращение'
|
id='operation_alias' //
|
||||||
className='w-64'
|
label='Сокращение'
|
||||||
{...register('item_data.alias')}
|
className='w-64'
|
||||||
error={errors.item_data?.alias}
|
{...register('item_data.alias')}
|
||||||
/>
|
error={errors.item_data?.alias}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name='item_data.parent'
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<SelectBlock
|
||||||
|
items={oss.blocks}
|
||||||
|
value={field.value ? oss.blockByID.get(field.value) ?? null : null}
|
||||||
|
placeholder='Блок содержания'
|
||||||
|
onChange={value => field.onChange(value ? value.id : null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<TextArea
|
<TextArea
|
||||||
id='operation_comment' //
|
id='operation_comment' //
|
||||||
label='Описание'
|
label='Описание'
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
|
||||||
import { type ICreateOperationDTO } from '../../backend/types';
|
import { type ICreateOperationDTO } from '../../backend/types';
|
||||||
import { PickMultiOperation } from '../../components/pick-multi-operation';
|
import { PickMultiOperation } from '../../components/pick-multi-operation';
|
||||||
|
import { SelectBlock } from '../../components/select-block';
|
||||||
|
|
||||||
import { type DlgCreateOperationProps } from './dlg-create-operation';
|
import { type DlgCreateOperationProps } from './dlg-create-operation';
|
||||||
|
|
||||||
|
@ -26,14 +27,27 @@ export function TabSynthesisOperation() {
|
||||||
error={errors.item_data?.title}
|
error={errors.item_data?.title}
|
||||||
/>
|
/>
|
||||||
<div className='flex gap-6'>
|
<div className='flex gap-6'>
|
||||||
<TextInput
|
<div className='grid gap-1'>
|
||||||
id='operation_alias'
|
<TextInput
|
||||||
label='Сокращение'
|
id='operation_alias' //
|
||||||
className='w-64'
|
label='Сокращение'
|
||||||
{...register('item_data.alias')}
|
className='w-64'
|
||||||
error={errors.item_data?.alias}
|
{...register('item_data.alias')}
|
||||||
/>
|
error={errors.item_data?.alias}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name='item_data.parent'
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<SelectBlock
|
||||||
|
items={oss.blocks}
|
||||||
|
value={field.value ? oss.blockByID.get(field.value) ?? null : null}
|
||||||
|
placeholder='Блок содержания'
|
||||||
|
onChange={value => field.onChange(value ? value.id : null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<TextArea
|
<TextArea
|
||||||
id='operation_comment'
|
id='operation_comment'
|
||||||
label='Описание'
|
label='Описание'
|
||||||
|
|
|
@ -21,7 +21,7 @@ export interface DlgDeleteOperationProps {
|
||||||
|
|
||||||
export function DlgDeleteOperation() {
|
export function DlgDeleteOperation() {
|
||||||
const { oss, target, layout } = useDialogsStore(state => state.props as DlgDeleteOperationProps);
|
const { oss, target, layout } = useDialogsStore(state => state.props as DlgDeleteOperationProps);
|
||||||
const { deleteOperation: operationDelete } = useDeleteOperation();
|
const { deleteOperation } = useDeleteOperation();
|
||||||
|
|
||||||
const { handleSubmit, control } = useForm<IDeleteOperationDTO>({
|
const { handleSubmit, control } = useForm<IDeleteOperationDTO>({
|
||||||
resolver: zodResolver(schemaDeleteOperation),
|
resolver: zodResolver(schemaDeleteOperation),
|
||||||
|
@ -34,7 +34,7 @@ export function DlgDeleteOperation() {
|
||||||
});
|
});
|
||||||
|
|
||||||
function onSubmit(data: IDeleteOperationDTO) {
|
function onSubmit(data: IDeleteOperationDTO) {
|
||||||
return operationDelete({ itemID: oss.id, data: data });
|
return deleteOperation({ itemID: oss.id, data: data });
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -34,7 +34,7 @@ export type TabID = (typeof TabID)[keyof typeof TabID];
|
||||||
|
|
||||||
export function DlgEditOperation() {
|
export function DlgEditOperation() {
|
||||||
const { oss, target, layout } = useDialogsStore(state => state.props as DlgEditOperationProps);
|
const { oss, target, layout } = useDialogsStore(state => state.props as DlgEditOperationProps);
|
||||||
const { updateOperation: operationUpdate } = useUpdateOperation();
|
const { updateOperation } = useUpdateOperation();
|
||||||
|
|
||||||
const methods = useForm<IUpdateOperationDTO>({
|
const methods = useForm<IUpdateOperationDTO>({
|
||||||
resolver: zodResolver(schemaUpdateOperation),
|
resolver: zodResolver(schemaUpdateOperation),
|
||||||
|
@ -58,7 +58,7 @@ export function DlgEditOperation() {
|
||||||
const [activeTab, setActiveTab] = useState<TabID>(TabID.CARD);
|
const [activeTab, setActiveTab] = useState<TabID>(TabID.CARD);
|
||||||
|
|
||||||
function onSubmit(data: IUpdateOperationDTO) {
|
function onSubmit(data: IUpdateOperationDTO) {
|
||||||
return operationUpdate({ itemID: oss.id, data });
|
return updateOperation({ itemID: oss.id, data });
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -78,13 +78,23 @@ export function DlgEditOperation() {
|
||||||
onSelect={index => setActiveTab(index as TabID)}
|
onSelect={index => setActiveTab(index as TabID)}
|
||||||
>
|
>
|
||||||
<TabList className='mb-3 mx-auto w-fit flex border divide-x rounded-none bg-secondary'>
|
<TabList className='mb-3 mx-auto w-fit flex border divide-x rounded-none bg-secondary'>
|
||||||
<TabLabel title='Текстовые поля' label='Карточка' className='w-32' />
|
<TabLabel
|
||||||
{target.operation_type === OperationType.SYNTHESIS ? (
|
title='Текстовые поля' //
|
||||||
<TabLabel title='Выбор аргументов операции' label='Аргументы' className='w-32' />
|
label='Карточка'
|
||||||
) : null}
|
className='w-32'
|
||||||
{target.operation_type === OperationType.SYNTHESIS ? (
|
/>
|
||||||
<TabLabel titleHtml='Таблица отождествлений' label='Отождествления' className='w-32' />
|
<TabLabel
|
||||||
) : null}
|
title='Выбор аргументов операции'
|
||||||
|
label='Аргументы'
|
||||||
|
className='w-32'
|
||||||
|
disabled={target.operation_type !== OperationType.SYNTHESIS}
|
||||||
|
/>
|
||||||
|
<TabLabel
|
||||||
|
titleHtml='Таблица отождествлений'
|
||||||
|
label='Отождествления'
|
||||||
|
className='w-32'
|
||||||
|
disabled={target.operation_type !== OperationType.SYNTHESIS}
|
||||||
|
/>
|
||||||
</TabList>
|
</TabList>
|
||||||
|
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
import { TextArea, TextInput } from '@/components/input';
|
import { TextArea, TextInput } from '@/components/input';
|
||||||
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
|
||||||
import { type IUpdateOperationDTO } from '../../backend/types';
|
import { type IUpdateOperationDTO } from '../../backend/types';
|
||||||
|
import { SelectBlock } from '../../components/select-block';
|
||||||
|
|
||||||
|
import { type DlgEditOperationProps } from './dlg-edit-operation';
|
||||||
|
|
||||||
export function TabOperation() {
|
export function TabOperation() {
|
||||||
|
const { oss } = useDialogsStore(state => state.props as DlgEditOperationProps);
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
|
control,
|
||||||
formState: { errors }
|
formState: { errors }
|
||||||
} = useFormContext<IUpdateOperationDTO>();
|
} = useFormContext<IUpdateOperationDTO>();
|
||||||
|
|
||||||
|
@ -14,19 +20,32 @@ export function TabOperation() {
|
||||||
<div className='cc-fade-in cc-column'>
|
<div className='cc-fade-in cc-column'>
|
||||||
<TextInput
|
<TextInput
|
||||||
id='operation_title'
|
id='operation_title'
|
||||||
label='Названиее'
|
label='Название'
|
||||||
{...register('item_data.title')}
|
{...register('item_data.title')}
|
||||||
error={errors.item_data?.title}
|
error={errors.item_data?.title}
|
||||||
/>
|
/>
|
||||||
<div className='flex gap-6'>
|
<div className='flex gap-6'>
|
||||||
<TextInput
|
<div className='grid gap-1'>
|
||||||
id='operation_alias'
|
<TextInput
|
||||||
label='Сокращение'
|
id='operation_alias' //
|
||||||
className='w-64'
|
label='Сокращение'
|
||||||
{...register('item_data.alias')}
|
className='w-64'
|
||||||
error={errors.item_data?.alias}
|
{...register('item_data.alias')}
|
||||||
/>
|
error={errors.item_data?.alias}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name='item_data.parent'
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<SelectBlock
|
||||||
|
items={oss.blocks}
|
||||||
|
value={field.value ? oss.blockByID.get(field.value) ?? null : null}
|
||||||
|
placeholder='Блок содержания'
|
||||||
|
onChange={value => field.onChange(value ? value.id : null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<TextArea
|
<TextArea
|
||||||
id='operation_comment'
|
id='operation_comment'
|
||||||
label='Описание'
|
label='Описание'
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
import { OperationType } from './backend/types';
|
import { OperationType } from './backend/types';
|
||||||
import { type ISubstitutionErrorDescription, SubstitutionErrorType } from './models/oss';
|
import {
|
||||||
|
type IOperation,
|
||||||
|
type IOssItem,
|
||||||
|
type ISubstitutionErrorDescription,
|
||||||
|
SubstitutionErrorType
|
||||||
|
} from './models/oss';
|
||||||
|
import { isOperation } from './models/oss-api';
|
||||||
|
|
||||||
/**
|
/** Retrieves label for {@link OperationType}. */
|
||||||
* Retrieves label for {@link OperationType}.
|
|
||||||
*/
|
|
||||||
export function labelOperationType(itemType: OperationType): string {
|
export function labelOperationType(itemType: OperationType): string {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
switch (itemType) {
|
switch (itemType) {
|
||||||
|
@ -12,9 +16,7 @@ export function labelOperationType(itemType: OperationType): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Retrieves description for {@link OperationType}. */
|
||||||
* Retrieves description for {@link OperationType}.
|
|
||||||
*/
|
|
||||||
export function describeOperationType(itemType: OperationType): string {
|
export function describeOperationType(itemType: OperationType): string {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
switch (itemType) {
|
switch (itemType) {
|
||||||
|
@ -23,9 +25,7 @@ export function describeOperationType(itemType: OperationType): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Generates error description for {@link ISubstitutionErrorDescription}. */
|
||||||
* Generates error description for {@link ISubstitutionErrorDescription}.
|
|
||||||
*/
|
|
||||||
export function describeSubstitutionError(error: ISubstitutionErrorDescription): string {
|
export function describeSubstitutionError(error: ISubstitutionErrorDescription): string {
|
||||||
switch (error.errorType) {
|
switch (error.errorType) {
|
||||||
case SubstitutionErrorType.invalidIDs:
|
case SubstitutionErrorType.invalidIDs:
|
||||||
|
@ -53,3 +53,12 @@ export function describeSubstitutionError(error: ISubstitutionErrorDescription):
|
||||||
}
|
}
|
||||||
return 'UNKNOWN ERROR';
|
return 'UNKNOWN ERROR';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Retrieves label for {@link IOssItem}. */
|
||||||
|
export function labelOssItem(item: IOssItem): string {
|
||||||
|
if (isOperation(item)) {
|
||||||
|
return `${(item as IOperation).alias}: ${item.title}`;
|
||||||
|
} else {
|
||||||
|
return `Блок: ${item.title}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -22,22 +22,26 @@ import {
|
||||||
import { infoMsg } from '@/utils/labels';
|
import { infoMsg } from '@/utils/labels';
|
||||||
|
|
||||||
import { Graph } from '../../../models/graph';
|
import { Graph } from '../../../models/graph';
|
||||||
import { type IOssLayout } from '../backend/types';
|
import { type ICreateBlockDTO, type ICreateOperationDTO, type IOssLayout } from '../backend/types';
|
||||||
import { describeSubstitutionError } from '../labels';
|
import { describeSubstitutionError } from '../labels';
|
||||||
|
import { OPERATION_NODE_HEIGHT, OPERATION_NODE_WIDTH } from '../pages/oss-page/editor-oss-graph/graph/node-core';
|
||||||
|
|
||||||
import { type IOperationSchema, SubstitutionErrorType } from './oss';
|
import { type IOperationSchema, type IOssItem, SubstitutionErrorType } from './oss';
|
||||||
import { type Position2D } from './oss-layout';
|
import { type Position2D, type Rectangle2D } from './oss-layout';
|
||||||
|
|
||||||
export const GRID_SIZE = 10; // pixels - size of OSS grid
|
export const GRID_SIZE = 10; // pixels - size of OSS grid
|
||||||
const MIN_DISTANCE = 20; // pixels - minimum distance between node centers
|
const MIN_DISTANCE = 2 * GRID_SIZE; // pixels - minimum distance between nodes
|
||||||
const DISTANCE_X = 180; // pixels - insert x-distance between node centers
|
const DISTANCE_X = 180; // pixels - insert x-distance between node centers
|
||||||
const DISTANCE_Y = 100; // pixels - insert y-distance between node centers
|
const DISTANCE_Y = 100; // pixels - insert y-distance between node centers
|
||||||
|
|
||||||
const STARTING_SUB_INDEX = 900; // max semantic index for starting substitution
|
const STARTING_SUB_INDEX = 900; // max semantic index for starting substitution
|
||||||
|
|
||||||
/**
|
/** Checks if element is {@link IOperation} or {@link IBlock}. */
|
||||||
* Sorts library items relevant for the specified {@link IOperationSchema}.
|
export function isOperation(item: IOssItem): boolean {
|
||||||
*/
|
return 'arguments' in item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sorts library items relevant for the specified {@link IOperationSchema}. */
|
||||||
export function sortItemsForOSS(oss: IOperationSchema, items: ILibraryItem[]): ILibraryItem[] {
|
export function sortItemsForOSS(oss: IOperationSchema, items: ILibraryItem[]): ILibraryItem[] {
|
||||||
const result = items.filter(item => item.location === oss.location);
|
const result = items.filter(item => item.location === oss.location);
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
|
@ -60,9 +64,7 @@ export function sortItemsForOSS(oss: IOperationSchema, items: ILibraryItem[]): I
|
||||||
|
|
||||||
type CrossMapping = Map<number, AliasMapping>;
|
type CrossMapping = Map<number, AliasMapping>;
|
||||||
|
|
||||||
/**
|
/** Validator for Substitution table. */
|
||||||
* Validator for Substitution table.
|
|
||||||
*/
|
|
||||||
export class SubstitutionValidator {
|
export class SubstitutionValidator {
|
||||||
public msg: string = '';
|
public msg: string = '';
|
||||||
public suggestions: ICstSubstitute[] = [];
|
public suggestions: ICstSubstitute[] = [];
|
||||||
|
@ -476,22 +478,21 @@ export function getRelocateCandidates(
|
||||||
return addedCst.filter(cst => !unreachable.includes(cst.id));
|
return addedCst.filter(cst => !unreachable.includes(cst.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Calculate insert position for a new {@link IOperation} */
|
||||||
* Calculate insert position for a new {@link IOperation}
|
export function calculateNewOperationPosition(
|
||||||
*/
|
|
||||||
export function calculateInsertPosition(
|
|
||||||
oss: IOperationSchema,
|
oss: IOperationSchema,
|
||||||
argumentsOps: number[],
|
data: ICreateOperationDTO,
|
||||||
layout: IOssLayout,
|
layout: IOssLayout
|
||||||
defaultPosition: Position2D
|
|
||||||
): Position2D {
|
): Position2D {
|
||||||
const result = defaultPosition;
|
// TODO: check parent node
|
||||||
|
|
||||||
|
const result = { x: data.position_x, y: data.position_y };
|
||||||
const operations = layout.operations;
|
const operations = layout.operations;
|
||||||
if (operations.length === 0) {
|
if (operations.length === 0) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (argumentsOps.length === 0) {
|
if (data.arguments.length === 0) {
|
||||||
let inputsPositions = operations.filter(pos =>
|
let inputsPositions = operations.filter(pos =>
|
||||||
oss.operations.find(operation => operation.arguments.length === 0 && operation.id === pos.id)
|
oss.operations.find(operation => operation.arguments.length === 0 && operation.id === pos.id)
|
||||||
);
|
);
|
||||||
|
@ -503,7 +504,7 @@ export function calculateInsertPosition(
|
||||||
result.x = maxX + DISTANCE_X;
|
result.x = maxX + DISTANCE_X;
|
||||||
result.y = minY;
|
result.y = minY;
|
||||||
} else {
|
} else {
|
||||||
const argNodes = operations.filter(pos => argumentsOps.includes(pos.id));
|
const argNodes = operations.filter(pos => data.arguments.includes(pos.id));
|
||||||
const maxY = Math.max(...argNodes.map(node => node.y));
|
const maxY = Math.max(...argNodes.map(node => node.y));
|
||||||
const minX = Math.min(...argNodes.map(node => node.x));
|
const minX = Math.min(...argNodes.map(node => node.x));
|
||||||
const maxX = Math.max(...argNodes.map(node => node.x));
|
const maxX = Math.max(...argNodes.map(node => node.x));
|
||||||
|
@ -523,3 +524,51 @@ export function calculateInsertPosition(
|
||||||
} while (flagIntersect);
|
} while (flagIntersect);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Calculate insert position for a new {@link IBlock} */
|
||||||
|
export function calculateNewBlockPosition(data: ICreateBlockDTO, layout: IOssLayout): Rectangle2D {
|
||||||
|
const block_nodes = data.children_blocks
|
||||||
|
.map(id => layout.blocks.find(block => block.id === -id))
|
||||||
|
.filter(node => !!node);
|
||||||
|
const operation_nodes = data.children_operations
|
||||||
|
.map(id => layout.operations.find(operation => operation.id === id))
|
||||||
|
.filter(node => !!node);
|
||||||
|
|
||||||
|
if (block_nodes.length === 0 && operation_nodes.length === 0) {
|
||||||
|
return { x: data.position_x, y: data.position_y, width: data.width, height: data.height };
|
||||||
|
}
|
||||||
|
|
||||||
|
let left = undefined;
|
||||||
|
let top = undefined;
|
||||||
|
let right = undefined;
|
||||||
|
let bottom = undefined;
|
||||||
|
|
||||||
|
for (const node of block_nodes) {
|
||||||
|
left = !left ? node.x - GRID_SIZE : Math.min(left, node.x - GRID_SIZE);
|
||||||
|
top = !top ? node.y - GRID_SIZE : Math.min(top, node.y - GRID_SIZE);
|
||||||
|
right = !right
|
||||||
|
? Math.max(left + data.width, node.x + node.width + GRID_SIZE)
|
||||||
|
: Math.max(right, node.x + node.width + GRID_SIZE);
|
||||||
|
bottom = !bottom
|
||||||
|
? Math.max(top + data.height, node.y + node.height + GRID_SIZE)
|
||||||
|
: Math.max(bottom, node.y + node.height + GRID_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const node of operation_nodes) {
|
||||||
|
left = !left ? node.x - GRID_SIZE : Math.min(left, node.x - GRID_SIZE);
|
||||||
|
top = !top ? node.y - GRID_SIZE : Math.min(top, node.y - GRID_SIZE);
|
||||||
|
right = !right
|
||||||
|
? Math.max(left + data.width, node.x + OPERATION_NODE_WIDTH + GRID_SIZE)
|
||||||
|
: Math.max(right, node.x + OPERATION_NODE_WIDTH + GRID_SIZE);
|
||||||
|
bottom = !bottom
|
||||||
|
? Math.max(top + data.height, node.y + OPERATION_NODE_HEIGHT + GRID_SIZE)
|
||||||
|
: Math.max(bottom, node.y + OPERATION_NODE_HEIGHT + GRID_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: left ?? data.position_x,
|
||||||
|
y: top ?? data.position_y,
|
||||||
|
width: right && left ? right - left : data.width,
|
||||||
|
height: bottom && top ? bottom - top : data.height
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -5,14 +5,18 @@ import { type Node } from 'reactflow';
|
||||||
|
|
||||||
import { type IBlock, type IOperation } from './oss';
|
import { type IBlock, type IOperation } from './oss';
|
||||||
|
|
||||||
/**
|
/** Represents XY Position. */
|
||||||
* Represents XY Position.
|
|
||||||
*/
|
|
||||||
export interface Position2D {
|
export interface Position2D {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Represents XY Position and dimensions. */
|
||||||
|
export interface Rectangle2D extends Position2D {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
/** Represents graph OSS node data. */
|
/** Represents graph OSS node data. */
|
||||||
export interface OssNode extends Node {
|
export interface OssNode extends Node {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
@ -52,6 +52,9 @@ export interface IOperationSchema extends IOperationSchemaDTO {
|
||||||
blockByID: Map<number, IBlock>;
|
blockByID: Map<number, IBlock>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Represents item of OperationSchema. */
|
||||||
|
export type IOssItem = IOperation | IBlock;
|
||||||
|
|
||||||
/** Represents substitution error description. */
|
/** Represents substitution error description. */
|
||||||
export interface ISubstitutionErrorDescription {
|
export interface ISubstitutionErrorDescription {
|
||||||
errorType: SubstitutionErrorType;
|
errorType: SubstitutionErrorType;
|
||||||
|
|
|
@ -23,6 +23,12 @@ export function OssStats({ className, stats }: OssStatsProps) {
|
||||||
<span>Всего</span>
|
<span>Всего</span>
|
||||||
<span>{stats.count_all}</span>
|
<span>{stats.count_all}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<ValueStats
|
||||||
|
id='count_block'
|
||||||
|
title='Блоки'
|
||||||
|
icon={<IconConceptBlock size='1.25rem' className='text-primary' />}
|
||||||
|
value={stats.count_block}
|
||||||
|
/>
|
||||||
<ValueStats
|
<ValueStats
|
||||||
id='count_inputs'
|
id='count_inputs'
|
||||||
title='Загрузка'
|
title='Загрузка'
|
||||||
|
@ -35,12 +41,6 @@ export function OssStats({ className, stats }: OssStatsProps) {
|
||||||
icon={<IconSynthesis size='1.25rem' className='text-primary' />}
|
icon={<IconSynthesis size='1.25rem' className='text-primary' />}
|
||||||
value={stats.count_synthesis}
|
value={stats.count_synthesis}
|
||||||
/>
|
/>
|
||||||
<ValueStats
|
|
||||||
id='count_block'
|
|
||||||
title='Блоки'
|
|
||||||
icon={<IconConceptBlock size='1.25rem' className='text-primary' />}
|
|
||||||
value={stats.count_block}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ValueStats
|
<ValueStats
|
||||||
id='count_schemas'
|
id='count_schemas'
|
||||||
|
|
|
@ -3,20 +3,37 @@
|
||||||
import { NodeResizeControl } from 'reactflow';
|
import { NodeResizeControl } from 'reactflow';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import { useOSSGraphStore } from '@/features/oss/stores/oss-graph';
|
||||||
|
|
||||||
import { IconResize } from '@/components/icons';
|
import { IconResize } from '@/components/icons';
|
||||||
|
|
||||||
import { type BlockInternalNode } from '../../../../models/oss-layout';
|
import { type BlockInternalNode } from '../../../../models/oss-layout';
|
||||||
import { useOssEdit } from '../../oss-edit-context';
|
import { useOssEdit } from '../../oss-edit-context';
|
||||||
|
|
||||||
|
export const BLOCK_NODE_MIN_WIDTH = 160;
|
||||||
|
export const BLOCK_NODE_MIN_HEIGHT = 100;
|
||||||
|
|
||||||
export function BlockNode(node: BlockInternalNode) {
|
export function BlockNode(node: BlockInternalNode) {
|
||||||
|
const showCoordinates = useOSSGraphStore(state => state.showCoordinates);
|
||||||
const { selected, schema } = useOssEdit();
|
const { selected, schema } = useOssEdit();
|
||||||
const singleSelected = selected.length === 1 ? selected[0] : null;
|
const singleSelected = selected.length === 1 ? selected[0] : null;
|
||||||
const isParent = !singleSelected ? false : schema.hierarchy.at(singleSelected)?.inputs.includes(node.data.block.id);
|
const isParent = !singleSelected ? false : schema.hierarchy.at(singleSelected)?.inputs.includes(node.data.block.id);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NodeResizeControl minWidth={160} minHeight={100}>
|
<NodeResizeControl minWidth={BLOCK_NODE_MIN_WIDTH} minHeight={BLOCK_NODE_MIN_HEIGHT}>
|
||||||
<IconResize size={8} className='absolute bottom-[2px] right-[2px]' />
|
<IconResize size={8} className='absolute bottom-[2px] right-[2px]' />
|
||||||
</NodeResizeControl>
|
</NodeResizeControl>
|
||||||
|
{showCoordinates ? (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'absolute top-full mt-1 right-[1px]',
|
||||||
|
'text-[7px]/[8px] font-math',
|
||||||
|
'text-muted-foreground hover:text-foreground'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{`X: ${node.xPos.toFixed(0)} Y: ${node.yPos.toFixed(0)}`}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<div className={clsx('cc-node-block h-full w-full', isParent && 'border-primary')}>
|
<div className={clsx('cc-node-block h-full w-full', isParent && 'border-primary')}>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import { useOSSGraphStore } from '@/features/oss/stores/oss-graph';
|
||||||
|
|
||||||
import { IconConsolidation, IconRSForm } from '@/components/icons';
|
import { IconConsolidation, IconRSForm } from '@/components/icons';
|
||||||
import { Indicator } from '@/components/view';
|
import { Indicator } from '@/components/view';
|
||||||
import { globalIDs } from '@/utils/constants';
|
import { globalIDs } from '@/utils/constants';
|
||||||
|
@ -22,6 +24,7 @@ interface NodeCoreProps {
|
||||||
|
|
||||||
export function NodeCore({ node }: NodeCoreProps) {
|
export function NodeCore({ node }: NodeCoreProps) {
|
||||||
const setHover = useOperationTooltipStore(state => state.setActiveOperation);
|
const setHover = useOperationTooltipStore(state => state.setActiveOperation);
|
||||||
|
const showCoordinates = useOSSGraphStore(state => state.showCoordinates);
|
||||||
|
|
||||||
const hasFile = !!node.data.operation.result;
|
const hasFile = !!node.data.operation.result;
|
||||||
const longLabel = node.data.label.length > LONG_LABEL_CHARS;
|
const longLabel = node.data.label.length > LONG_LABEL_CHARS;
|
||||||
|
@ -47,6 +50,17 @@ export function NodeCore({ node }: NodeCoreProps) {
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
{showCoordinates ? (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'absolute top-full mt-1 right-[1px]',
|
||||||
|
'text-[7px]/[8px] font-math',
|
||||||
|
'text-muted-foreground hover:text-foreground'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{`X: ${node.xPos.toFixed(0)} Y: ${node.yPos.toFixed(0)}`}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{node.data.operation.operation_type === OperationType.INPUT ? (
|
{node.data.operation.operation_type === OperationType.INPUT ? (
|
||||||
<div className='absolute top-[1px] right-1/2 translate-x-1/2 border-t w-[30px]' />
|
<div className='absolute top-[1px] right-1/2 translate-x-1/2 border-t w-[30px]' />
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
useStoreApi
|
useStoreApi
|
||||||
} from 'reactflow';
|
} from 'reactflow';
|
||||||
|
|
||||||
|
import { useDeleteBlock } from '@/features/oss/backend/use-delete-block';
|
||||||
import { type IOperationSchema } from '@/features/oss/models/oss';
|
import { type IOperationSchema } from '@/features/oss/models/oss';
|
||||||
|
|
||||||
import { useMainHeight } from '@/stores/app-layout';
|
import { useMainHeight } from '@/stores/app-layout';
|
||||||
|
@ -60,11 +61,13 @@ export function OssFlow() {
|
||||||
const setHoverOperation = useOperationTooltipStore(state => state.setActiveOperation);
|
const setHoverOperation = useOperationTooltipStore(state => state.setActiveOperation);
|
||||||
|
|
||||||
const showGrid = useOSSGraphStore(state => state.showGrid);
|
const showGrid = useOSSGraphStore(state => state.showGrid);
|
||||||
|
const showCoordinates = useOSSGraphStore(state => state.showCoordinates);
|
||||||
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
|
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
|
||||||
const edgeStraight = useOSSGraphStore(state => state.edgeStraight);
|
const edgeStraight = useOSSGraphStore(state => state.edgeStraight);
|
||||||
|
|
||||||
const getLayout = useGetLayout();
|
const getLayout = useGetLayout();
|
||||||
const { updateLayout: updatePositions } = useUpdateLayout();
|
const { updateLayout } = useUpdateLayout();
|
||||||
|
const { deleteBlock } = useDeleteBlock();
|
||||||
|
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||||
|
@ -72,7 +75,10 @@ export function OssFlow() {
|
||||||
const [menuProps, setMenuProps] = useState<ContextMenuData>({ operation: null, cursorX: 0, cursorY: 0 });
|
const [menuProps, setMenuProps] = useState<ContextMenuData>({ operation: null, cursorX: 0, cursorY: 0 });
|
||||||
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
|
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
const [mouseCoords, setMouseCoords] = useState<Position2D>({ x: 0, y: 0 });
|
||||||
|
|
||||||
const showCreateOperation = useDialogsStore(state => state.showCreateOperation);
|
const showCreateOperation = useDialogsStore(state => state.showCreateOperation);
|
||||||
|
const showCreateBlock = useDialogsStore(state => state.showCreateBlock);
|
||||||
const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation);
|
const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation);
|
||||||
|
|
||||||
function onSelectionChange({ nodes }: { nodes: Node[] }) {
|
function onSelectionChange({ nodes }: { nodes: Node[] }) {
|
||||||
|
@ -99,8 +105,10 @@ export function OssFlow() {
|
||||||
type: 'block',
|
type: 'block',
|
||||||
data: { label: block.title, block: block },
|
data: { label: block.title, block: block },
|
||||||
position: computeRelativePosition(schema, { x: block.x, y: block.y }, block.parent),
|
position: computeRelativePosition(schema, { x: block.x, y: block.y }, block.parent),
|
||||||
width: block.width,
|
style: {
|
||||||
height: block.height,
|
width: block.width,
|
||||||
|
height: block.height
|
||||||
|
},
|
||||||
parentId: block.parent ? `-${block.parent}` : undefined,
|
parentId: block.parent ? `-${block.parent}` : undefined,
|
||||||
expandParent: true,
|
expandParent: true,
|
||||||
extent: 'parent' as const,
|
extent: 'parent' as const,
|
||||||
|
@ -135,7 +143,7 @@ export function OssFlow() {
|
||||||
}, [schema, setNodes, setEdges, toggleReset, edgeStraight, edgeAnimate, fitView]);
|
}, [schema, setNodes, setEdges, toggleReset, edgeStraight, edgeAnimate, fitView]);
|
||||||
|
|
||||||
function handleSavePositions() {
|
function handleSavePositions() {
|
||||||
void updatePositions({ itemID: schema.id, data: getLayout() });
|
void updateLayout({ itemID: schema.id, data: getLayout() });
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCreateOperation() {
|
function handleCreateOperation() {
|
||||||
|
@ -146,6 +154,20 @@ export function OssFlow() {
|
||||||
defaultY: targetPosition.y,
|
defaultY: targetPosition.y,
|
||||||
layout: getLayout(),
|
layout: getLayout(),
|
||||||
initialInputs: selected.filter(id => id > 0),
|
initialInputs: selected.filter(id => id > 0),
|
||||||
|
initialParent: extractSingleBlock(selected),
|
||||||
|
onCreate: () =>
|
||||||
|
setTimeout(() => fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING }), PARAMETER.refreshTimeout)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCreateBlock() {
|
||||||
|
const targetPosition = screenToFlowPosition({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
|
||||||
|
showCreateBlock({
|
||||||
|
oss: schema,
|
||||||
|
defaultX: targetPosition.x,
|
||||||
|
defaultY: targetPosition.y,
|
||||||
|
layout: getLayout(),
|
||||||
|
initialInputs: selected,
|
||||||
onCreate: () =>
|
onCreate: () =>
|
||||||
setTimeout(() => fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING }), PARAMETER.refreshTimeout)
|
setTimeout(() => fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING }), PARAMETER.refreshTimeout)
|
||||||
});
|
});
|
||||||
|
@ -155,15 +177,23 @@ export function OssFlow() {
|
||||||
if (selected.length !== 1) {
|
if (selected.length !== 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const operation = schema.operationByID.get(selected[0]);
|
if (selected[0] > 0) {
|
||||||
if (!operation || !canDelete(operation)) {
|
const operation = schema.operationByID.get(selected[0]);
|
||||||
return;
|
if (!operation || !canDelete(operation)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showDeleteOperation({
|
||||||
|
oss: schema,
|
||||||
|
target: operation,
|
||||||
|
layout: getLayout()
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const block = schema.blockByID.get(-selected[0]);
|
||||||
|
if (!block) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void deleteBlock({ itemID: schema.id, data: { target: block.id, layout: getLayout() } });
|
||||||
}
|
}
|
||||||
showDeleteOperation({
|
|
||||||
oss: schema,
|
|
||||||
target: operation,
|
|
||||||
layout: getLayout()
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleContextMenu(event: React.MouseEvent<Element>, node: OssNode) {
|
function handleContextMenu(event: React.MouseEvent<Element>, node: OssNode) {
|
||||||
|
@ -209,7 +239,11 @@ export function OssFlow() {
|
||||||
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyQ') {
|
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyQ') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
handleCreateOperation();
|
if (event.shiftKey) {
|
||||||
|
handleCreateBlock();
|
||||||
|
} else {
|
||||||
|
handleCreateOperation();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.key === 'Delete') {
|
if (event.key === 'Delete') {
|
||||||
|
@ -220,11 +254,27 @@ export function OssFlow() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleMouseMove(event: React.MouseEvent<HTMLDivElement>) {
|
||||||
|
const targetPosition = screenToFlowPosition({ x: event.clientX, y: event.clientY });
|
||||||
|
setMouseCoords(targetPosition);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div tabIndex={-1} className='relative' onKeyDown={handleKeyDown}>
|
<div
|
||||||
|
tabIndex={-1}
|
||||||
|
className='relative'
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onMouseMove={showCoordinates ? handleMouseMove : undefined}
|
||||||
|
>
|
||||||
|
{showCoordinates ? (
|
||||||
|
<div className='absolute top-1 right-2 hover:bg-background backdrop-blur-xs text-sm font-math'>
|
||||||
|
{`X: ${mouseCoords.x.toFixed(0)} Y: ${mouseCoords.y.toFixed(0)}`}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<ToolbarOssGraph
|
<ToolbarOssGraph
|
||||||
className='absolute z-pop top-8 right-1/2 translate-x-1/2'
|
className='absolute z-pop top-8 right-1/2 translate-x-1/2'
|
||||||
onCreate={handleCreateOperation}
|
onCreateOperation={handleCreateOperation}
|
||||||
|
onCreateBlock={handleCreateBlock}
|
||||||
onDelete={handleDeleteSelected}
|
onDelete={handleDeleteSelected}
|
||||||
onResetPositions={() => setToggleReset(prev => !prev)}
|
onResetPositions={() => setToggleReset(prev => !prev)}
|
||||||
/>
|
/>
|
||||||
|
@ -274,3 +324,8 @@ function computeRelativePosition(schema: IOperationSchema, position: Position2D,
|
||||||
y: position.y - parentBlock.y
|
y: position.y - parentBlock.y
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractSingleBlock(selected: number[]): number | null {
|
||||||
|
const blocks = selected.filter(id => id < 0);
|
||||||
|
return blocks.length === 1 ? -blocks[0] : null;
|
||||||
|
}
|
||||||
|
|
|
@ -11,6 +11,8 @@ import { MiniButton } from '@/components/control';
|
||||||
import {
|
import {
|
||||||
IconAnimation,
|
IconAnimation,
|
||||||
IconAnimationOff,
|
IconAnimationOff,
|
||||||
|
IconConceptBlock,
|
||||||
|
IconCoordinates,
|
||||||
IconDestroy,
|
IconDestroy,
|
||||||
IconEdit2,
|
IconEdit2,
|
||||||
IconExecute,
|
IconExecute,
|
||||||
|
@ -37,13 +39,15 @@ import { VIEW_PADDING } from './oss-flow';
|
||||||
import { useGetLayout } from './use-get-layout';
|
import { useGetLayout } from './use-get-layout';
|
||||||
|
|
||||||
interface ToolbarOssGraphProps extends Styling {
|
interface ToolbarOssGraphProps extends Styling {
|
||||||
onCreate: () => void;
|
onCreateOperation: () => void;
|
||||||
|
onCreateBlock: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onResetPositions: () => void;
|
onResetPositions: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolbarOssGraph({
|
export function ToolbarOssGraph({
|
||||||
onCreate,
|
onCreateOperation,
|
||||||
|
onCreateBlock,
|
||||||
onDelete,
|
onDelete,
|
||||||
onResetPositions,
|
onResetPositions,
|
||||||
className,
|
className,
|
||||||
|
@ -53,12 +57,15 @@ export function ToolbarOssGraph({
|
||||||
const isProcessing = useMutatingOss();
|
const isProcessing = useMutatingOss();
|
||||||
const { fitView } = useReactFlow();
|
const { fitView } = useReactFlow();
|
||||||
const selectedOperation = selected.length !== 1 ? null : schema.operationByID.get(selected[0]) ?? null;
|
const selectedOperation = selected.length !== 1 ? null : schema.operationByID.get(selected[0]) ?? null;
|
||||||
|
const selectedBlock = selected.length !== 1 ? null : schema.blockByID.get(-selected[0]) ?? null;
|
||||||
const getLayout = useGetLayout();
|
const getLayout = useGetLayout();
|
||||||
|
|
||||||
const showGrid = useOSSGraphStore(state => state.showGrid);
|
const showGrid = useOSSGraphStore(state => state.showGrid);
|
||||||
|
const showCoordinates = useOSSGraphStore(state => state.showCoordinates);
|
||||||
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
|
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
|
||||||
const edgeStraight = useOSSGraphStore(state => state.edgeStraight);
|
const edgeStraight = useOSSGraphStore(state => state.edgeStraight);
|
||||||
const toggleShowGrid = useOSSGraphStore(state => state.toggleShowGrid);
|
const toggleShowGrid = useOSSGraphStore(state => state.toggleShowGrid);
|
||||||
|
const toggleShowCoordinates = useOSSGraphStore(state => state.toggleShowCoordinates);
|
||||||
const toggleEdgeAnimate = useOSSGraphStore(state => state.toggleEdgeAnimate);
|
const toggleEdgeAnimate = useOSSGraphStore(state => state.toggleEdgeAnimate);
|
||||||
const toggleEdgeStraight = useOSSGraphStore(state => state.toggleEdgeStraight);
|
const toggleEdgeStraight = useOSSGraphStore(state => state.toggleEdgeStraight);
|
||||||
|
|
||||||
|
@ -174,6 +181,12 @@ export function ToolbarOssGraph({
|
||||||
}
|
}
|
||||||
onClick={toggleEdgeAnimate}
|
onClick={toggleEdgeAnimate}
|
||||||
/>
|
/>
|
||||||
|
<MiniButton
|
||||||
|
title={showCoordinates ? 'Координаты: вкл' : 'Координаты: выкл'}
|
||||||
|
aria-label='Переключатель видимости координат (для отладки)'
|
||||||
|
icon={<IconCoordinates size='1.25rem' className={showCoordinates ? 'icon-green' : 'icon-primary'} />}
|
||||||
|
onClick={toggleShowCoordinates}
|
||||||
|
/>
|
||||||
<BadgeHelp topic={HelpTopic.UI_OSS_GRAPH} contentClass='sm:max-w-160' offset={4} />
|
<BadgeHelp topic={HelpTopic.UI_OSS_GRAPH} contentClass='sm:max-w-160' offset={4} />
|
||||||
</div>
|
</div>
|
||||||
{isMutable ? (
|
{isMutable ? (
|
||||||
|
@ -185,11 +198,18 @@ export function ToolbarOssGraph({
|
||||||
onClick={handleSavePositions}
|
onClick={handleSavePositions}
|
||||||
disabled={isProcessing}
|
disabled={isProcessing}
|
||||||
/>
|
/>
|
||||||
|
<MiniButton
|
||||||
|
titleHtml={prepareTooltip('Новый блок', 'Ctrl + Shift + Q')}
|
||||||
|
aria-label='Новый блок'
|
||||||
|
icon={<IconConceptBlock size='1.25rem' className='icon-green' />}
|
||||||
|
onClick={onCreateBlock}
|
||||||
|
disabled={isProcessing}
|
||||||
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Новая операция', 'Ctrl + Q')}
|
titleHtml={prepareTooltip('Новая операция', 'Ctrl + Q')}
|
||||||
aria-label='Новая операция'
|
aria-label='Новая операция'
|
||||||
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
||||||
onClick={onCreate}
|
onClick={onCreateOperation}
|
||||||
disabled={isProcessing}
|
disabled={isProcessing}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
|
@ -210,7 +230,11 @@ export function ToolbarOssGraph({
|
||||||
aria-label='Удалить выбранную'
|
aria-label='Удалить выбранную'
|
||||||
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
disabled={selected.length !== 1 || isProcessing || !selectedOperation || !canDelete(selectedOperation)}
|
disabled={
|
||||||
|
isProcessing ||
|
||||||
|
(!selectedOperation && !selectedBlock) ||
|
||||||
|
(!!selectedOperation && !canDelete(selectedOperation))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import { type Node, useReactFlow } from 'reactflow';
|
import { type Node, useReactFlow } from 'reactflow';
|
||||||
|
|
||||||
import { DEFAULT_BLOCK_HEIGHT, DEFAULT_BLOCK_WIDTH } from '@/features/oss/backend/oss-loader';
|
|
||||||
import { type IOssLayout } from '@/features/oss/backend/types';
|
import { type IOssLayout } from '@/features/oss/backend/types';
|
||||||
import { type IOperationSchema } from '@/features/oss/models/oss';
|
import { type IOperationSchema } from '@/features/oss/models/oss';
|
||||||
import { type Position2D } from '@/features/oss/models/oss-layout';
|
import { type Position2D } from '@/features/oss/models/oss-layout';
|
||||||
|
|
||||||
import { useOssEdit } from '../oss-edit-context';
|
import { useOssEdit } from '../oss-edit-context';
|
||||||
|
|
||||||
|
import { BLOCK_NODE_MIN_HEIGHT, BLOCK_NODE_MIN_WIDTH } from './graph/block-node';
|
||||||
|
|
||||||
export function useGetLayout() {
|
export function useGetLayout() {
|
||||||
const { getNodes } = useReactFlow();
|
const { getNodes } = useReactFlow();
|
||||||
const { schema } = useOssEdit();
|
const { schema } = useOssEdit();
|
||||||
|
@ -26,8 +27,8 @@ export function useGetLayout() {
|
||||||
.map(node => ({
|
.map(node => ({
|
||||||
id: -Number(node.id),
|
id: -Number(node.id),
|
||||||
...computeAbsolutePosition(node, schema, nodeById),
|
...computeAbsolutePosition(node, schema, nodeById),
|
||||||
width: node.width ?? DEFAULT_BLOCK_WIDTH,
|
width: node.width ?? BLOCK_NODE_MIN_WIDTH,
|
||||||
height: node.height ?? DEFAULT_BLOCK_HEIGHT
|
height: node.height ?? BLOCK_NODE_MIN_HEIGHT
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,6 +5,9 @@ interface OSSGraphStore {
|
||||||
showGrid: boolean;
|
showGrid: boolean;
|
||||||
toggleShowGrid: () => void;
|
toggleShowGrid: () => void;
|
||||||
|
|
||||||
|
showCoordinates: boolean;
|
||||||
|
toggleShowCoordinates: () => void;
|
||||||
|
|
||||||
edgeAnimate: boolean;
|
edgeAnimate: boolean;
|
||||||
toggleEdgeAnimate: () => void;
|
toggleEdgeAnimate: () => void;
|
||||||
|
|
||||||
|
@ -18,6 +21,9 @@ export const useOSSGraphStore = create<OSSGraphStore>()(
|
||||||
showGrid: false,
|
showGrid: false,
|
||||||
toggleShowGrid: () => set(state => ({ showGrid: !state.showGrid })),
|
toggleShowGrid: () => set(state => ({ showGrid: !state.showGrid })),
|
||||||
|
|
||||||
|
showCoordinates: false,
|
||||||
|
toggleShowCoordinates: () => set(state => ({ showCoordinates: !state.showCoordinates })),
|
||||||
|
|
||||||
edgeAnimate: false,
|
edgeAnimate: false,
|
||||||
toggleEdgeAnimate: () => set(state => ({ edgeAnimate: !state.edgeAnimate })),
|
toggleEdgeAnimate: () => set(state => ({ edgeAnimate: !state.edgeAnimate })),
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { type DlgCreateVersionProps } from '@/features/library/dialogs/dlg-creat
|
||||||
import { type DlgEditEditorsProps } from '@/features/library/dialogs/dlg-edit-editors/dlg-edit-editors';
|
import { type DlgEditEditorsProps } from '@/features/library/dialogs/dlg-edit-editors/dlg-edit-editors';
|
||||||
import { type DlgEditVersionsProps } from '@/features/library/dialogs/dlg-edit-versions/dlg-edit-versions';
|
import { type DlgEditVersionsProps } from '@/features/library/dialogs/dlg-edit-versions/dlg-edit-versions';
|
||||||
import { type DlgChangeInputSchemaProps } from '@/features/oss/dialogs/dlg-change-input-schema';
|
import { type DlgChangeInputSchemaProps } from '@/features/oss/dialogs/dlg-change-input-schema';
|
||||||
|
import { type DlgCreateBlockProps } from '@/features/oss/dialogs/dlg-create-block/dlg-create-block';
|
||||||
import { type DlgCreateOperationProps } from '@/features/oss/dialogs/dlg-create-operation/dlg-create-operation';
|
import { type DlgCreateOperationProps } from '@/features/oss/dialogs/dlg-create-operation/dlg-create-operation';
|
||||||
import { type DlgDeleteOperationProps } from '@/features/oss/dialogs/dlg-delete-operation';
|
import { type DlgDeleteOperationProps } from '@/features/oss/dialogs/dlg-delete-operation';
|
||||||
import { type DlgEditOperationProps } from '@/features/oss/dialogs/dlg-edit-operation/dlg-edit-operation';
|
import { type DlgEditOperationProps } from '@/features/oss/dialogs/dlg-edit-operation/dlg-edit-operation';
|
||||||
|
@ -26,28 +27,36 @@ import { type DlgUploadRSFormProps } from '@/features/rsform/dialogs/dlg-upload-
|
||||||
/** Represents global dialog. */
|
/** Represents global dialog. */
|
||||||
export const DialogType = {
|
export const DialogType = {
|
||||||
CONSTITUENTA_TEMPLATE: 1,
|
CONSTITUENTA_TEMPLATE: 1,
|
||||||
CREATE_CONSTITUENTA: 2,
|
SUBSTITUTE_CONSTITUENTS: 2,
|
||||||
CREATE_OPERATION: 3,
|
|
||||||
DELETE_CONSTITUENTA: 4,
|
CREATE_VERSION: 3,
|
||||||
EDIT_EDITORS: 5,
|
|
||||||
EDIT_OPERATION: 6,
|
CREATE_CONSTITUENTA: 4,
|
||||||
EDIT_REFERENCE: 7,
|
DELETE_CONSTITUENTA: 5,
|
||||||
EDIT_VERSIONS: 8,
|
RENAME_CONSTITUENTA: 6,
|
||||||
EDIT_WORD_FORMS: 9,
|
|
||||||
INLINE_SYNTHESIS: 10,
|
CREATE_BLOCK: 7,
|
||||||
SHOW_AST: 11,
|
|
||||||
SHOW_TYPE_GRAPH: 12,
|
CREATE_OPERATION: 8,
|
||||||
CHANGE_INPUT_SCHEMA: 13,
|
EDIT_OPERATION: 9,
|
||||||
CHANGE_LOCATION: 14,
|
DELETE_OPERATION: 10,
|
||||||
CLONE_LIBRARY_ITEM: 15,
|
CHANGE_INPUT_SCHEMA: 11,
|
||||||
CREATE_VERSION: 16,
|
RELOCATE_CONSTITUENTS: 12,
|
||||||
DELETE_OPERATION: 17,
|
|
||||||
GRAPH_PARAMETERS: 18,
|
CLONE_LIBRARY_ITEM: 13,
|
||||||
RELOCATE_CONSTITUENTS: 19,
|
UPLOAD_RSFORM: 14,
|
||||||
RENAME_CONSTITUENTA: 20,
|
EDIT_EDITORS: 15,
|
||||||
|
EDIT_VERSIONS: 16,
|
||||||
|
CHANGE_LOCATION: 17,
|
||||||
|
|
||||||
|
EDIT_REFERENCE: 18,
|
||||||
|
EDIT_WORD_FORMS: 19,
|
||||||
|
INLINE_SYNTHESIS: 20,
|
||||||
|
|
||||||
SHOW_QR_CODE: 21,
|
SHOW_QR_CODE: 21,
|
||||||
SUBSTITUTE_CONSTITUENTS: 22,
|
SHOW_AST: 22,
|
||||||
UPLOAD_RSFORM: 23
|
SHOW_TYPE_GRAPH: 23,
|
||||||
|
GRAPH_PARAMETERS: 24
|
||||||
} as const;
|
} as const;
|
||||||
export type DialogType = (typeof DialogType)[keyof typeof DialogType];
|
export type DialogType = (typeof DialogType)[keyof typeof DialogType];
|
||||||
|
|
||||||
|
@ -62,6 +71,7 @@ interface DialogsStore {
|
||||||
|
|
||||||
showCstTemplate: (props: DlgCstTemplateProps) => void;
|
showCstTemplate: (props: DlgCstTemplateProps) => void;
|
||||||
showCreateCst: (props: DlgCreateCstProps) => void;
|
showCreateCst: (props: DlgCreateCstProps) => void;
|
||||||
|
showCreateBlock: (props: DlgCreateBlockProps) => void;
|
||||||
showCreateOperation: (props: DlgCreateOperationProps) => void;
|
showCreateOperation: (props: DlgCreateOperationProps) => void;
|
||||||
showDeleteCst: (props: DlgDeleteCstProps) => void;
|
showDeleteCst: (props: DlgDeleteCstProps) => void;
|
||||||
showEditEditors: (props: DlgEditEditorsProps) => void;
|
showEditEditors: (props: DlgEditEditorsProps) => void;
|
||||||
|
@ -98,6 +108,7 @@ export const useDialogsStore = create<DialogsStore>()(set => ({
|
||||||
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 }),
|
||||||
showCreateOperation: props => set({ active: DialogType.CREATE_OPERATION, props: props }),
|
showCreateOperation: props => set({ active: DialogType.CREATE_OPERATION, props: props }),
|
||||||
|
showCreateBlock: props => set({ active: DialogType.CREATE_BLOCK, props: props }),
|
||||||
showDeleteCst: props => set({ active: DialogType.DELETE_CONSTITUENTA, props: props }),
|
showDeleteCst: props => set({ active: DialogType.DELETE_CONSTITUENTA, props: props }),
|
||||||
showEditEditors: props => set({ active: DialogType.EDIT_EDITORS, props: props }),
|
showEditEditors: props => set({ active: DialogType.EDIT_EDITORS, props: props }),
|
||||||
showEditOperation: props => set({ active: DialogType.EDIT_OPERATION, props: props }),
|
showEditOperation: props => set({ active: DialogType.EDIT_OPERATION, props: props }),
|
||||||
|
|
Loading…
Reference in New Issue
Block a user