B: Prevent cyclic dependencies
This commit is contained in:
parent
34893818fa
commit
7496389c31
|
@ -1,4 +1,5 @@
|
||||||
''' Serializers for persistent data manipulation. '''
|
''' Serializers for persistent data manipulation. '''
|
||||||
|
from collections import deque
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
|
@ -62,9 +63,10 @@ class CreateBlockSerializer(serializers.Serializer):
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
oss = cast(LibraryItem, self.context['oss'])
|
oss = cast(LibraryItem, self.context['oss'])
|
||||||
if 'parent' in attrs['item_data'] and \
|
parent = attrs['item_data'].get('parent')
|
||||||
attrs['item_data']['parent'] is not None and \
|
children_blocks = attrs.get('children_blocks', [])
|
||||||
attrs['item_data']['parent'].oss_id != oss.pk:
|
|
||||||
|
if parent is not None and parent.oss_id != oss.pk:
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
'parent': msg.parentNotInOSS()
|
'parent': msg.parentNotInOSS()
|
||||||
})
|
})
|
||||||
|
@ -75,11 +77,17 @@ class CreateBlockSerializer(serializers.Serializer):
|
||||||
'children_operations': msg.childNotInOSS()
|
'children_operations': msg.childNotInOSS()
|
||||||
})
|
})
|
||||||
|
|
||||||
for block in attrs['children_blocks']:
|
for block in children_blocks:
|
||||||
if block.oss_id != oss.pk:
|
if block.oss_id != oss.pk:
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
'children_blocks': msg.childNotInOSS()
|
'children_blocks': msg.childNotInOSS()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if parent:
|
||||||
|
descendant_ids = _collect_descendants(children_blocks)
|
||||||
|
if parent.pk in descendant_ids:
|
||||||
|
raise serializers.ValidationError({'parent': msg.blockCyclicHierarchy()})
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
@ -104,15 +112,15 @@ class UpdateBlockSerializer(serializers.Serializer):
|
||||||
'target': msg.blockNotInOSS()
|
'target': msg.blockNotInOSS()
|
||||||
})
|
})
|
||||||
|
|
||||||
if 'parent' in attrs['item_data'] and \
|
parent = attrs['item_data'].get('parent')
|
||||||
attrs['item_data']['parent'] is not None:
|
if parent is not None:
|
||||||
if attrs['item_data']['parent'].oss_id != oss.pk:
|
if parent.oss_id != oss.pk:
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
'parent': msg.parentNotInOSS()
|
'parent': msg.parentNotInOSS()
|
||||||
})
|
})
|
||||||
if attrs['item_data']['parent'] == attrs['target']:
|
if parent == attrs['target']:
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
'parent': msg.blockSelfParent()
|
'parent': msg.blockCyclicHierarchy()
|
||||||
})
|
})
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
@ -142,24 +150,35 @@ class MoveItemsSerializer(serializers.Serializer):
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
oss = cast(LibraryItem, self.context['oss'])
|
oss = cast(LibraryItem, self.context['oss'])
|
||||||
parent_block = cast(Block, attrs['destination'])
|
parent_block = cast(Block, attrs['destination'])
|
||||||
|
moved_blocks = attrs.get('blocks', [])
|
||||||
|
moved_operations = attrs.get('operations', [])
|
||||||
|
|
||||||
if parent_block is not None and parent_block.oss_id != oss.pk:
|
if parent_block is not None and parent_block.oss_id != oss.pk:
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
'destination': msg.blockNotInOSS()
|
'destination': msg.blockNotInOSS()
|
||||||
})
|
})
|
||||||
for operation in attrs['operations']:
|
for operation in moved_operations:
|
||||||
if operation.oss_id != oss.pk:
|
if operation.oss_id != oss.pk:
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
'operations': msg.operationNotInOSS()
|
'operations': msg.operationNotInOSS()
|
||||||
})
|
})
|
||||||
for block in attrs['blocks']:
|
for block in moved_blocks:
|
||||||
if parent_block is not None and block.pk == parent_block.pk:
|
if parent_block is not None and block.pk == parent_block.pk:
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
'destination': msg.blockSelfParent()
|
'destination': msg.blockCyclicHierarchy()
|
||||||
})
|
})
|
||||||
if block.oss_id != oss.pk:
|
if block.oss_id != oss.pk:
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
'blocks': msg.blockNotInOSS()
|
'blocks': msg.blockNotInOSS()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if parent_block:
|
||||||
|
ancestor_ids = _collect_ancestors(parent_block)
|
||||||
|
moved_block_ids = {b.pk for b in moved_blocks}
|
||||||
|
if moved_block_ids & ancestor_ids:
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'destination': msg.blockCyclicHierarchy()
|
||||||
|
})
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
@ -186,9 +205,8 @@ class CreateOperationSerializer(serializers.Serializer):
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
oss = cast(LibraryItem, self.context['oss'])
|
oss = cast(LibraryItem, self.context['oss'])
|
||||||
if 'parent' in attrs['item_data'] and \
|
parent = attrs['item_data'].get('parent')
|
||||||
attrs['item_data']['parent'] is not None and \
|
if parent is not None and parent.oss_id != oss.pk:
|
||||||
attrs['item_data']['parent'].oss_id != oss.pk:
|
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
'parent': msg.parentNotInOSS()
|
'parent': msg.parentNotInOSS()
|
||||||
})
|
})
|
||||||
|
@ -223,15 +241,14 @@ class UpdateOperationSerializer(serializers.Serializer):
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
oss = cast(LibraryItem, self.context['oss'])
|
oss = cast(LibraryItem, self.context['oss'])
|
||||||
|
parent = attrs['item_data'].get('parent')
|
||||||
target = cast(Block, attrs['target'])
|
target = cast(Block, attrs['target'])
|
||||||
if target.oss_id != oss.pk:
|
if target.oss_id != oss.pk:
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
'target': msg.operationNotInOSS()
|
'target': msg.operationNotInOSS()
|
||||||
})
|
})
|
||||||
|
|
||||||
if 'parent' in attrs['item_data'] and \
|
if parent is not None and parent.oss_id != oss.pk:
|
||||||
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()
|
||||||
})
|
})
|
||||||
|
@ -441,3 +458,29 @@ class RelocateConstituentsSerializer(serializers.Serializer):
|
||||||
})
|
})
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
# ====== Internals =================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_descendants(start_blocks: list[Block]) -> set[int]:
|
||||||
|
""" Recursively collect all descendant block IDs from a list of blocks. """
|
||||||
|
visited = set()
|
||||||
|
queue = deque(start_blocks)
|
||||||
|
while queue:
|
||||||
|
block = queue.popleft()
|
||||||
|
if block.pk not in visited:
|
||||||
|
visited.add(block.pk)
|
||||||
|
queue.extend(block.as_child_block.all())
|
||||||
|
return visited
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_ancestors(block: Block) -> set[int]:
|
||||||
|
""" Recursively collect all ancestor block IDs of a block. """
|
||||||
|
ancestors = set()
|
||||||
|
current = block.parent
|
||||||
|
while current:
|
||||||
|
if current.pk in ancestors:
|
||||||
|
break # Prevent infinite loop in malformed data
|
||||||
|
ancestors.add(current.pk)
|
||||||
|
current = current.parent
|
||||||
|
return ancestors
|
||||||
|
|
|
@ -168,6 +168,32 @@ class TestOssBlocks(EndpointTester):
|
||||||
self.assertEqual(self.block1.parent.pk, new_block['id'])
|
self.assertEqual(self.block1.parent.pk, new_block['id'])
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/oss/{item}/create-block', method='post')
|
||||||
|
def test_create_block_cyclic(self):
|
||||||
|
self.populateData()
|
||||||
|
data = {
|
||||||
|
'item_data': {
|
||||||
|
'title': 'Test title',
|
||||||
|
'description': 'Тест кириллицы',
|
||||||
|
'parent': self.block2.pk
|
||||||
|
},
|
||||||
|
'layout': self.layout_data,
|
||||||
|
'position_x': 1337,
|
||||||
|
'position_y': 1337,
|
||||||
|
'width': 0.42,
|
||||||
|
'height': 0.42,
|
||||||
|
'children_operations': [],
|
||||||
|
'children_blocks': [self.block1.pk]
|
||||||
|
}
|
||||||
|
self.executeBadData(data=data, item=self.owned_id)
|
||||||
|
|
||||||
|
data['item_data']['parent'] = self.block1.pk
|
||||||
|
self.executeBadData(data=data)
|
||||||
|
|
||||||
|
data['children_blocks'] = [self.block2.pk]
|
||||||
|
self.executeCreated(data=data)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/oss/{item}/delete-block', method='patch')
|
@decl_endpoint('/api/oss/{item}/delete-block', method='patch')
|
||||||
def test_delete_block(self):
|
def test_delete_block(self):
|
||||||
self.populateData()
|
self.populateData()
|
||||||
|
|
|
@ -205,6 +205,24 @@ class TestOssViewset(EndpointTester):
|
||||||
self.assertEqual(self.operation2.parent, None)
|
self.assertEqual(self.operation2.parent, None)
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/oss/{item}/move-items', method='patch')
|
||||||
|
def test_move_items_cyclic(self):
|
||||||
|
self.populateData()
|
||||||
|
self.executeBadData(item=self.owned_id)
|
||||||
|
|
||||||
|
block1 = self.owned.create_block(title='1')
|
||||||
|
block2 = self.owned.create_block(title='2', parent=block1)
|
||||||
|
block3 = self.owned.create_block(title='3', parent=block2)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'layout': self.layout_data,
|
||||||
|
'blocks': [block1.pk],
|
||||||
|
'operations': [],
|
||||||
|
'destination': block3.pk
|
||||||
|
}
|
||||||
|
self.executeBadData(data=data)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/oss/relocate-constituents', method='post')
|
@decl_endpoint('/api/oss/relocate-constituents', method='post')
|
||||||
def test_relocate_constituents(self):
|
def test_relocate_constituents(self):
|
||||||
self.populateData()
|
self.populateData()
|
||||||
|
|
|
@ -22,7 +22,7 @@ def parentNotInOSS():
|
||||||
return 'Родительский блок не принадлежит ОСС'
|
return 'Родительский блок не принадлежит ОСС'
|
||||||
|
|
||||||
|
|
||||||
def blockSelfParent():
|
def blockCyclicHierarchy():
|
||||||
return 'Попытка создания циклического вложения'
|
return 'Попытка создания циклического вложения'
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { NoData } from '@/components/view';
|
||||||
|
|
||||||
import { labelOssItem } from '../labels';
|
import { labelOssItem } from '../labels';
|
||||||
import { type IBlock, type IOperation, type IOperationSchema } from '../models/oss';
|
import { type IBlock, type IOperation, type IOperationSchema } from '../models/oss';
|
||||||
import { isOperation } from '../models/oss-api';
|
import { getItemID, isOperation } from '../models/oss-api';
|
||||||
|
|
||||||
const SELECTION_CLEAR_TIMEOUT = 1000;
|
const SELECTION_CLEAR_TIMEOUT = 1000;
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ interface PickMultiOperationProps extends Styling {
|
||||||
onChange: (newValue: number[]) => void;
|
onChange: (newValue: number[]) => void;
|
||||||
schema: IOperationSchema;
|
schema: IOperationSchema;
|
||||||
rows?: number;
|
rows?: number;
|
||||||
|
exclude?: number[];
|
||||||
disallowBlocks?: boolean;
|
disallowBlocks?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,6 +30,7 @@ const columnHelper = createColumnHelper<IOperation | IBlock>();
|
||||||
export function PickContents({
|
export function PickContents({
|
||||||
rows,
|
rows,
|
||||||
schema,
|
schema,
|
||||||
|
exclude,
|
||||||
value,
|
value,
|
||||||
disallowBlocks,
|
disallowBlocks,
|
||||||
onChange,
|
onChange,
|
||||||
|
@ -40,38 +42,38 @@ export function PickContents({
|
||||||
.filter(item => item !== undefined);
|
.filter(item => item !== undefined);
|
||||||
const [lastSelected, setLastSelected] = useState<IOperation | IBlock | null>(null);
|
const [lastSelected, setLastSelected] = useState<IOperation | IBlock | null>(null);
|
||||||
const items = [
|
const items = [
|
||||||
...schema.operations.filter(item => !value.includes(item.id)),
|
...(disallowBlocks ? [] : schema.blocks.filter(item => !value.includes(-item.id) && !exclude?.includes(-item.id))),
|
||||||
...(disallowBlocks ? [] : schema.blocks.filter(item => !value.includes(-item.id)))
|
...schema.operations.filter(item => !value.includes(item.id) && !exclude?.includes(item.id))
|
||||||
];
|
];
|
||||||
|
|
||||||
function handleDelete(operation: number) {
|
function handleDelete(target: number) {
|
||||||
onChange(value.filter(item => item !== operation));
|
onChange(value.filter(item => item !== target));
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSelect(target: IOperation | IBlock | null) {
|
function handleSelect(target: IOperation | IBlock | null) {
|
||||||
if (target) {
|
if (target) {
|
||||||
setLastSelected(target);
|
setLastSelected(target);
|
||||||
onChange([...value, isOperation(target) ? target.id : -target.id]);
|
onChange([...value, getItemID(target)]);
|
||||||
setTimeout(() => setLastSelected(null), SELECTION_CLEAR_TIMEOUT);
|
setTimeout(() => setLastSelected(null), SELECTION_CLEAR_TIMEOUT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMoveUp(operation: number) {
|
function handleMoveUp(target: number) {
|
||||||
const index = value.indexOf(operation);
|
const index = value.indexOf(target);
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
const newSelected = [...value];
|
const newSelected = [...value];
|
||||||
newSelected[index] = newSelected[index - 1];
|
newSelected[index] = newSelected[index - 1];
|
||||||
newSelected[index - 1] = operation;
|
newSelected[index - 1] = target;
|
||||||
onChange(newSelected);
|
onChange(newSelected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMoveDown(operation: number) {
|
function handleMoveDown(target: number) {
|
||||||
const index = value.indexOf(operation);
|
const index = value.indexOf(target);
|
||||||
if (index < value.length - 1) {
|
if (index < value.length - 1) {
|
||||||
const newSelected = [...value];
|
const newSelected = [...value];
|
||||||
newSelected[index] = newSelected[index + 1];
|
newSelected[index] = newSelected[index + 1];
|
||||||
newSelected[index + 1] = operation;
|
newSelected[index + 1] = target;
|
||||||
onChange(newSelected);
|
onChange(newSelected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -103,21 +105,21 @@ export function PickContents({
|
||||||
noHover
|
noHover
|
||||||
className='px-0'
|
className='px-0'
|
||||||
icon={<IconRemove size='1rem' className='icon-red' />}
|
icon={<IconRemove size='1rem' className='icon-red' />}
|
||||||
onClick={() => handleDelete(props.row.original.id)}
|
onClick={() => handleDelete(getItemID(props.row.original))}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Переместить выше'
|
title='Переместить выше'
|
||||||
noHover
|
noHover
|
||||||
className='px-0'
|
className='px-0'
|
||||||
icon={<IconMoveUp size='1rem' className='icon-primary' />}
|
icon={<IconMoveUp size='1rem' className='icon-primary' />}
|
||||||
onClick={() => handleMoveUp(props.row.original.id)}
|
onClick={() => handleMoveUp(getItemID(props.row.original))}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Переместить ниже'
|
title='Переместить ниже'
|
||||||
noHover
|
noHover
|
||||||
className='px-0'
|
className='px-0'
|
||||||
icon={<IconMoveDown size='1rem' className='icon-primary' />}
|
icon={<IconMoveDown size='1rem' className='icon-primary' />}
|
||||||
onClick={() => handleMoveDown(props.row.original.id)}
|
onClick={() => handleMoveDown(getItemID(props.row.original))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -131,7 +133,7 @@ export function PickContents({
|
||||||
items={items}
|
items={items}
|
||||||
value={lastSelected}
|
value={lastSelected}
|
||||||
placeholder='Выберите операцию или блок'
|
placeholder='Выберите операцию или блок'
|
||||||
idFunc={item => String(item.id)}
|
idFunc={item => String(getItemID(item))}
|
||||||
labelValueFunc={item => labelOssItem(item)}
|
labelValueFunc={item => labelOssItem(item)}
|
||||||
labelOptionFunc={item => labelOssItem(item)}
|
labelOptionFunc={item => labelOssItem(item)}
|
||||||
onChange={handleSelect}
|
onChange={handleSelect}
|
||||||
|
|
|
@ -18,6 +18,7 @@ export function SelectBlock({ items, placeholder = 'Выберите блок',
|
||||||
return (
|
return (
|
||||||
<ComboBox
|
<ComboBox
|
||||||
items={items}
|
items={items}
|
||||||
|
clearable
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
idFunc={block => String(block.id)}
|
idFunc={block => String(block.id)}
|
||||||
labelValueFunc={block => block.title}
|
labelValueFunc={block => block.title}
|
||||||
|
|
|
@ -20,7 +20,8 @@ import { TabBlockChildren } from './tab-block-children';
|
||||||
|
|
||||||
export interface DlgCreateBlockProps {
|
export interface DlgCreateBlockProps {
|
||||||
manager: LayoutManager;
|
manager: LayoutManager;
|
||||||
initialInputs: number[];
|
initialChildren: number[];
|
||||||
|
initialParent: number | null;
|
||||||
defaultX: number;
|
defaultX: number;
|
||||||
defaultY: number;
|
defaultY: number;
|
||||||
onCreate?: (newID: number) => void;
|
onCreate?: (newID: number) => void;
|
||||||
|
@ -35,7 +36,7 @@ export type TabID = (typeof TabID)[keyof typeof TabID];
|
||||||
export function DlgCreateBlock() {
|
export function DlgCreateBlock() {
|
||||||
const { createBlock } = useCreateBlock();
|
const { createBlock } = useCreateBlock();
|
||||||
|
|
||||||
const { manager, initialInputs, onCreate, defaultX, defaultY } = useDialogsStore(
|
const { manager, initialChildren, initialParent, onCreate, defaultX, defaultY } = useDialogsStore(
|
||||||
state => state.props as DlgCreateBlockProps
|
state => state.props as DlgCreateBlockProps
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -45,19 +46,21 @@ export function DlgCreateBlock() {
|
||||||
item_data: {
|
item_data: {
|
||||||
title: '',
|
title: '',
|
||||||
description: '',
|
description: '',
|
||||||
parent: null
|
parent: initialParent
|
||||||
},
|
},
|
||||||
position_x: defaultX,
|
position_x: defaultX,
|
||||||
position_y: defaultY,
|
position_y: defaultY,
|
||||||
width: BLOCK_NODE_MIN_WIDTH,
|
width: BLOCK_NODE_MIN_WIDTH,
|
||||||
height: BLOCK_NODE_MIN_HEIGHT,
|
height: BLOCK_NODE_MIN_HEIGHT,
|
||||||
children_blocks: initialInputs.filter(id => id < 0).map(id => -id),
|
children_blocks: initialChildren.filter(id => id < 0).map(id => -id),
|
||||||
children_operations: initialInputs.filter(id => id > 0),
|
children_operations: initialChildren.filter(id => id > 0),
|
||||||
layout: manager.layout
|
layout: manager.layout
|
||||||
},
|
},
|
||||||
mode: 'onChange'
|
mode: 'onChange'
|
||||||
});
|
});
|
||||||
const title = useWatch({ control: methods.control, name: 'item_data.title' });
|
const title = useWatch({ control: methods.control, name: 'item_data.title' });
|
||||||
|
const children_blocks = useWatch({ control: methods.control, name: 'children_blocks' });
|
||||||
|
const children_operations = useWatch({ control: methods.control, name: 'children_operations' });
|
||||||
const [activeTab, setActiveTab] = useState<TabID>(TabID.CARD);
|
const [activeTab, setActiveTab] = useState<TabID>(TabID.CARD);
|
||||||
const isValid = !!title && !manager.oss.blocks.some(block => block.title === title);
|
const isValid = !!title && !manager.oss.blocks.some(block => block.title === title);
|
||||||
|
|
||||||
|
@ -87,7 +90,10 @@ export function DlgCreateBlock() {
|
||||||
>
|
>
|
||||||
<TabList className='z-pop mx-auto -mb-5 flex border divide-x rounded-none bg-secondary'>
|
<TabList className='z-pop mx-auto -mb-5 flex border divide-x rounded-none bg-secondary'>
|
||||||
<TabLabel title='Основные атрибуты блока' label='Карточка' />
|
<TabLabel title='Основные атрибуты блока' label='Карточка' />
|
||||||
<TabLabel title='Выбор вложенных узлов' label='Содержимое' />
|
<TabLabel
|
||||||
|
title={`Выбор вложенных узлов: [${children_operations.length + children_blocks.length}]`}
|
||||||
|
label={`Содержимое${children_operations.length + children_blocks.length > 0 ? '*' : ''}`}
|
||||||
|
/>
|
||||||
</TabList>
|
</TabList>
|
||||||
|
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Controller, useFormContext } from 'react-hook-form';
|
import { Controller, useFormContext, useWatch } from 'react-hook-form';
|
||||||
|
|
||||||
import { TextArea, TextInput } from '@/components/input';
|
import { TextArea, TextInput } from '@/components/input';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
@ -17,6 +17,11 @@ export function TabBlockCard() {
|
||||||
control,
|
control,
|
||||||
formState: { errors }
|
formState: { errors }
|
||||||
} = useFormContext<ICreateBlockDTO>();
|
} = useFormContext<ICreateBlockDTO>();
|
||||||
|
const children_blocks = useWatch({ control, name: 'children_blocks' });
|
||||||
|
const all_children = [
|
||||||
|
...children_blocks,
|
||||||
|
...manager.oss.hierarchy.expandAllOutputs(children_blocks.filter(id => id < 0).map(id => -id)).map(id => -id)
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='cc-fade-in cc-column'>
|
<div className='cc-fade-in cc-column'>
|
||||||
|
@ -31,7 +36,7 @@ export function TabBlockCard() {
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<SelectParent
|
<SelectParent
|
||||||
items={manager.oss.blocks}
|
items={manager.oss.blocks.filter(block => !all_children.includes(block.id))}
|
||||||
value={field.value ? manager.oss.blockByID.get(field.value) ?? null : null}
|
value={field.value ? manager.oss.blockByID.get(field.value) ?? null : null}
|
||||||
placeholder='Блок содержания не выбран'
|
placeholder='Блок содержания не выбран'
|
||||||
onChange={value => field.onChange(value ? value.id : null)}
|
onChange={value => field.onChange(value ? value.id : null)}
|
||||||
|
|
|
@ -12,8 +12,12 @@ import { type DlgCreateBlockProps } from './dlg-create-block';
|
||||||
export function TabBlockChildren() {
|
export function TabBlockChildren() {
|
||||||
const { setValue, control } = useFormContext<ICreateBlockDTO>();
|
const { setValue, control } = useFormContext<ICreateBlockDTO>();
|
||||||
const { manager } = useDialogsStore(state => state.props as DlgCreateBlockProps);
|
const { manager } = useDialogsStore(state => state.props as DlgCreateBlockProps);
|
||||||
|
const parent = useWatch({ control, name: 'item_data.parent' });
|
||||||
const children_blocks = useWatch({ control, name: 'children_blocks' });
|
const children_blocks = useWatch({ control, name: 'children_blocks' });
|
||||||
const children_operations = useWatch({ control, name: 'children_operations' });
|
const children_operations = useWatch({ control, name: 'children_operations' });
|
||||||
|
const exclude = parent ? [-parent, ...manager.oss.hierarchy.expandAllInputs([-parent]).filter(id => id < 0)] : [];
|
||||||
|
|
||||||
|
console.log(exclude);
|
||||||
|
|
||||||
const value = [...children_blocks.map(id => -id), ...children_operations];
|
const value = [...children_blocks.map(id => -id), ...children_operations];
|
||||||
|
|
||||||
|
@ -35,6 +39,7 @@ export function TabBlockChildren() {
|
||||||
<Label text={`Выбор содержания: [ ${value.length} ]`} />
|
<Label text={`Выбор содержания: [ ${value.length} ]`} />
|
||||||
<PickContents
|
<PickContents
|
||||||
schema={manager.oss}
|
schema={manager.oss}
|
||||||
|
exclude={exclude}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={newValue => handleChangeSelected(newValue)}
|
onChange={newValue => handleChangeSelected(newValue)}
|
||||||
rows={10}
|
rows={10}
|
||||||
|
|
|
@ -33,6 +33,11 @@ export function isOperation(item: IOssItem | null): boolean {
|
||||||
return !!item && 'arguments' in item;
|
return !!item && 'arguments' in item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Extract contiguous ID of {@link IOperation} or {@link IBlock}. */
|
||||||
|
export function getItemID(item: IOssItem): number {
|
||||||
|
return isOperation(item) ? item.id : -item.id;
|
||||||
|
}
|
||||||
|
|
||||||
/** Sorts library items relevant for the specified {@link IOperationSchema}. */
|
/** 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);
|
||||||
|
|
|
@ -74,11 +74,13 @@ export function OssFlow() {
|
||||||
|
|
||||||
function handleCreateBlock() {
|
function handleCreateBlock() {
|
||||||
const targetPosition = screenToFlowPosition({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
|
const targetPosition = screenToFlowPosition({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
|
||||||
|
const parent = extractSingleBlock(selected);
|
||||||
showCreateBlock({
|
showCreateBlock({
|
||||||
manager: new LayoutManager(schema, getLayout()),
|
manager: new LayoutManager(schema, getLayout()),
|
||||||
defaultX: targetPosition.x,
|
defaultX: targetPosition.x,
|
||||||
defaultY: targetPosition.y,
|
defaultY: targetPosition.y,
|
||||||
initialInputs: selected,
|
initialChildren: parent !== null ? [] : selected,
|
||||||
|
initialParent: parent,
|
||||||
onCreate: resetView
|
onCreate: resetView
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,10 +73,13 @@ export function useDragging({ hideContextMenu }: DraggingProps) {
|
||||||
.filter(id => id < 0)
|
.filter(id => id < 0)
|
||||||
.map(id => schema.blockByID.get(-id))
|
.map(id => schema.blockByID.get(-id))
|
||||||
.filter(operation => !!operation);
|
.filter(operation => !!operation);
|
||||||
const parents = [...blocks.map(block => block.parent), ...operations.map(operation => operation.parent)].filter(
|
const parents = new Set(
|
||||||
id => !!id
|
[...blocks.map(block => block.parent), ...operations.map(operation => operation.parent)].filter(id => !!id)
|
||||||
);
|
);
|
||||||
if ((parents.length !== 1 || parents[0] !== new_parent) && (parents.length !== 0 || new_parent !== null)) {
|
if (
|
||||||
|
(parents.size !== 1 || parents.values().next().value !== new_parent) &&
|
||||||
|
(parents.size !== 0 || new_parent !== null)
|
||||||
|
) {
|
||||||
void moveItems({
|
void moveItems({
|
||||||
itemID: schema.id,
|
itemID: schema.id,
|
||||||
data: {
|
data: {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user