+
diff --git a/rsconcept/frontend/src/components/icons.tsx b/rsconcept/frontend/src/components/icons.tsx
index 307e6c21..7839a4f7 100644
--- a/rsconcept/frontend/src/components/icons.tsx
+++ b/rsconcept/frontend/src/components/icons.tsx
@@ -70,6 +70,7 @@ export { LuGlasses as IconReader } from 'react-icons/lu';
export { TbBriefcase as IconBusiness } from 'react-icons/tb';
export { VscLibrary as IconLibrary } from 'react-icons/vsc';
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 { BiDiamond as IconTemplates } from 'react-icons/bi';
export { TbHexagons as IconOSS } from 'react-icons/tb';
diff --git a/rsconcept/frontend/src/components/tabs/tab-label.tsx b/rsconcept/frontend/src/components/tabs/tab-label.tsx
index e10a46b3..f1b93d21 100644
--- a/rsconcept/frontend/src/components/tabs/tab-label.tsx
+++ b/rsconcept/frontend/src/components/tabs/tab-label.tsx
@@ -20,6 +20,7 @@ export function TabLabel({
titleHtml,
hideTitle,
className,
+ disabled,
role = 'tab',
...otherProps
}: TabLabelProps) {
@@ -28,10 +29,12 @@ export function TabLabel({
className={clsx(
'min-w-20 h-full',
'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',
- 'select-none hover:cursor-pointer',
+ 'select-none',
'outline-hidden',
+ !disabled && 'hover:cursor-pointer cc-hover',
+ disabled && 'text-muted-foreground',
className
)}
tabIndex='-1'
@@ -40,6 +43,7 @@ export function TabLabel({
data-tooltip-content={title}
data-tooltip-hidden={hideTitle}
role={role}
+ disabled={disabled}
{...otherProps}
>
{label}
diff --git a/rsconcept/frontend/src/features/oss/backend/api.ts b/rsconcept/frontend/src/features/oss/backend/api.ts
index 2dfde8a2..2a160a52 100644
--- a/rsconcept/frontend/src/features/oss/backend/api.ts
+++ b/rsconcept/frontend/src/features/oss/backend/api.ts
@@ -20,6 +20,7 @@ import {
type IUpdateBlockDTO,
type IUpdateInputDTO,
type IUpdateOperationDTO,
+ schemaBlockCreatedResponse,
schemaConstituentaReference,
schemaInputCreatedResponse,
schemaOperationCreatedResponse,
@@ -55,7 +56,7 @@ export const ossApi = {
createBlock: ({ itemID, data }: { itemID: number; data: ICreateBlockDTO }) =>
axiosPost
({
- schema: schemaOperationCreatedResponse,
+ schema: schemaBlockCreatedResponse,
endpoint: `/api/oss/${itemID}/create-block`,
request: {
data: data,
@@ -74,7 +75,7 @@ export const ossApi = {
deleteBlock: ({ itemID, data }: { itemID: number; data: IDeleteBlockDTO }) =>
axiosPatch({
schema: schemaOperationSchema,
- endpoint: `/api/oss/${itemID}/delete-operation`,
+ endpoint: `/api/oss/${itemID}/delete-block`,
request: {
data: data,
successMessage: infoMsg.operationDestroyed
diff --git a/rsconcept/frontend/src/features/oss/backend/oss-loader.ts b/rsconcept/frontend/src/features/oss/backend/oss-loader.ts
index 913c3b37..a4107ecc 100644
--- a/rsconcept/frontend/src/features/oss/backend/oss-loader.ts
+++ b/rsconcept/frontend/src/features/oss/backend/oss-loader.ts
@@ -7,12 +7,10 @@ import { type ILibraryItem } from '@/features/library';
import { Graph } from '@/models/graph';
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';
-export const DEFAULT_BLOCK_WIDTH = 100;
-export const DEFAULT_BLOCK_HEIGHT = 100;
-
/** Loads data into an {@link IOperationSchema} based on {@link IOperationSchemaDTO}. */
export class OssLoader {
private oss: IOperationSchema;
@@ -58,7 +56,7 @@ export class OssLoader {
this.blockByID.set(block.id, block);
this.hierarchy.addNode(-block.id);
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);
block.x = geometry?.x ?? 0;
block.y = geometry?.y ?? 0;
- block.width = geometry?.width ?? DEFAULT_BLOCK_WIDTH;
- block.height = geometry?.height ?? DEFAULT_BLOCK_HEIGHT;
+ block.width = geometry?.width ?? BLOCK_NODE_MIN_WIDTH;
+ block.height = geometry?.height ?? BLOCK_NODE_MIN_HEIGHT;
});
}
diff --git a/rsconcept/frontend/src/features/oss/components/pick-contents.tsx b/rsconcept/frontend/src/features/oss/components/pick-contents.tsx
new file mode 100644
index 00000000..4c5f3a74
--- /dev/null
+++ b/rsconcept/frontend/src/features/oss/components/pick-contents.tsx
@@ -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();
+
+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(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 => {isOperation(props.row.original) ? 'Операция' : 'Блок'}
+ }),
+ columnHelper.accessor('title', {
+ id: 'title',
+ header: 'Название',
+ size: 1200,
+ minSize: 300,
+ maxSize: 1200,
+ cell: props => {props.getValue()}
+ }),
+ columnHelper.display({
+ id: 'actions',
+ size: 0,
+ cell: props => (
+
+ }
+ onClick={() => handleDelete(props.row.original.id)}
+ />
+ }
+ onClick={() => handleMoveUp(props.row.original.id)}
+ />
+ }
+ onClick={() => handleMoveDown(props.row.original.id)}
+ />
+
+ )
+ })
+ ];
+
+ return (
+
+
String(item.id)}
+ labelValueFunc={item => labelOssItem(item)}
+ labelOptionFunc={item => labelOssItem(item)}
+ onChange={handleSelect}
+ />
+
+ Список пуст
+
+ }
+ />
+
+ );
+}
diff --git a/rsconcept/frontend/src/features/oss/components/select-block.tsx b/rsconcept/frontend/src/features/oss/components/select-block.tsx
new file mode 100644
index 00000000..a858010e
--- /dev/null
+++ b/rsconcept/frontend/src/features/oss/components/select-block.tsx
@@ -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 (
+ String(block.id)}
+ labelValueFunc={block => block.title}
+ labelOptionFunc={block => block.title}
+ {...restProps}
+ />
+ );
+}
diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-block/dlg-create-block.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-block/dlg-create-block.tsx
new file mode 100644
index 00000000..48e48ec9
--- /dev/null
+++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-block/dlg-create-block.tsx
@@ -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({
+ 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.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 (
+ void methods.handleSubmit(onSubmit)(event)}
+ className='w-160 px-6 h-128'
+ helpTopic={HelpTopic.CC_OSS}
+ >
+ setActiveTab(index as TabID)}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-block/index.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-block/index.tsx
new file mode 100644
index 00000000..8baeacee
--- /dev/null
+++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-block/index.tsx
@@ -0,0 +1 @@
+export { DlgCreateBlock } from './dlg-create-block';
diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-block/tab-block-card.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-block/tab-block-card.tsx
new file mode 100644
index 00000000..baf791b0
--- /dev/null
+++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-block/tab-block-card.tsx
@@ -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();
+
+ return (
+
+
+ (
+ field.onChange(value ? value.id : null)}
+ />
+ )}
+ />
+
+
+
+ );
+}
diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-block/tab-block-children.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-block/tab-block-children.tsx
new file mode 100644
index 00000000..8dd413b3
--- /dev/null
+++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-block/tab-block-children.tsx
@@ -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();
+ 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 (
+
+
+
handleChangeSelected(newValue)} rows={8} />
+
+ );
+}
diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/dlg-create-operation.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/dlg-create-operation.tsx
index 84ef8bf0..985d7d08 100644
--- a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/dlg-create-operation.tsx
+++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/dlg-create-operation.tsx
@@ -14,7 +14,7 @@ import { type ICreateOperationDTO, type IOssLayout, OperationType, schemaCreateO
import { useCreateOperation } from '../../backend/use-create-operation';
import { describeOperationType, labelOperationType } from '../../labels';
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 { TabSynthesisOperation } from './tab-synthesis-operation';
@@ -22,6 +22,7 @@ import { TabSynthesisOperation } from './tab-synthesis-operation';
export interface DlgCreateOperationProps {
oss: IOperationSchema;
layout: IOssLayout;
+ initialParent: number | null;
initialInputs: number[];
defaultX: number;
defaultY: number;
@@ -35,9 +36,9 @@ export const TabID = {
export type TabID = (typeof TabID)[keyof typeof TabID];
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
);
@@ -50,7 +51,7 @@ export function DlgCreateOperation() {
title: '',
description: '',
result: null,
- parent: null
+ parent: initialParent
},
position_x: defaultX,
position_y: defaultY,
@@ -65,13 +66,10 @@ export function DlgCreateOperation() {
const isValid = !!alias && !oss.operations.some(operation => operation.alias === alias);
function onSubmit(data: ICreateOperationDTO) {
- const target = calculateInsertPosition(oss, data.arguments, layout, {
- x: defaultX,
- y: defaultY
- });
+ const target = calculateNewOperationPosition(oss, data, layout);
data.position_x = target.x;
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) {
diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/tab-input-operation.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/tab-input-operation.tsx
index 58587266..4c86e530 100644
--- a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/tab-input-operation.tsx
+++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/tab-input-operation.tsx
@@ -12,6 +12,7 @@ import { Checkbox, Label, TextArea, TextInput } from '@/components/input';
import { useDialogsStore } from '@/stores/dialogs';
import { type ICreateOperationDTO } from '../../backend/types';
+import { SelectBlock } from '../../components/select-block';
import { sortItemsForOSS } from '../../models/oss-api';
import { type DlgCreateOperationProps } from './dlg-create-operation';
@@ -61,14 +62,27 @@ export function TabInputOperation() {
error={errors.item_data?.title}
/>
-
-
+
+
+ (
+ field.onChange(value ? value.id : null)}
+ />
+ )}
+ />
+
-
-
+
+
+ (
+ field.onChange(value ? value.id : null)}
+ />
+ )}
+ />
+