Compare commits

..

No commits in common. "e2ab676ec26ec0932b7f8391277c81dbab19040d" and "dbceac0a6d1600f223241f86a15aa53623f31184" have entirely different histories.

243 changed files with 2908 additions and 6616 deletions

View File

@ -6,11 +6,11 @@
"fractalbrew.backticks",
"streetsidesoftware.code-spell-checker",
"streetsidesoftware.code-spell-checker-russian",
"kamikillerto.vscode-colorize",
"batisteo.vscode-django",
"ms-azuretools.vscode-docker",
"dbaeumer.vscode-eslint",
"seyyedkhandon.firacode",
"nize.oklch-preview",
"ms-python.isort",
"ms-vscode.powershell",
"esbenp.prettier-vscode",

View File

@ -85,7 +85,7 @@ This readme file is used mostly to document project dependencies and conventions
<summary>VS Code plugins</summary>
<pre>
- ESLint
- Oklch Color Preview
- Colorize
- Tailwind CSS IntelliSense
- Code Spell Checker (eng + rus)
- Backticks

View File

@ -15,7 +15,6 @@ User profile:
- Profile pictures
- Custom LibraryItem lists
- Custom user filters and sharing filters
- Personal prompt templates
- Static analyzer for RSForm as a whole: check term duplication and empty conventions
- OSS clone and versioning

View File

@ -92,26 +92,26 @@ class OperationSchema:
)
def update_layout(self, data: dict) -> None:
''' Update graphical layout. '''
''' Update positions. '''
layout = self.layout()
layout.data = data
layout.save()
def create_operation(self, **kwargs) -> Operation:
''' Create Operation. '''
''' Insert new operation. '''
result = Operation.objects.create(oss=self.model, **kwargs)
self.cache.insert_operation(result)
self.save(update_fields=['time_update'])
return result
def create_block(self, **kwargs) -> Block:
''' Create Block. '''
''' Insert new block. '''
result = Block.objects.create(oss=self.model, **kwargs)
self.save(update_fields=['time_update'])
return result
def delete_operation(self, target: int, keep_constituents: bool = False):
''' Delete Operation. '''
''' Delete operation. '''
self.cache.ensure_loaded()
operation = self.cache.operation_by_id[target]
schema = self.cache.get_schema(operation)
@ -139,20 +139,6 @@ class OperationSchema:
operation.delete()
self.save(update_fields=['time_update'])
def delete_block(self, target: Block):
''' Delete Block. '''
new_parent = target.parent
if new_parent is not None:
for block in Block.objects.filter(parent=target):
if block != new_parent:
block.parent = new_parent
block.save(update_fields=['parent'])
for operation in Operation.objects.filter(parent=target):
operation.parent = new_parent
operation.save(update_fields=['parent'])
target.delete()
self.save(update_fields=['time_update'])
def set_input(self, target: int, schema: Optional[LibraryItem]) -> None:
''' Set input schema for operation. '''
operation = self.cache.operation_by_id[target]
@ -179,7 +165,7 @@ class OperationSchema:
self.save(update_fields=['time_update'])
def set_arguments(self, target: int, arguments: list[Operation]) -> None:
''' Set arguments of target Operation. '''
''' Set arguments to operation. '''
self.cache.ensure_loaded()
operation = self.cache.operation_by_id[target]
processed: list[Operation] = []
@ -212,7 +198,7 @@ class OperationSchema:
self.save(update_fields=['time_update'])
def set_substitutions(self, target: int, substitutes: list[dict]) -> None:
''' Clear all arguments for target Operation. '''
''' Clear all arguments for operation. '''
self.cache.ensure_loaded()
operation = self.cache.operation_by_id[target]
schema = self.cache.get_schema(operation)
@ -251,7 +237,7 @@ class OperationSchema:
self.save(update_fields=['time_update'])
def create_input(self, operation: Operation) -> RSForm:
''' Create input RSForm for given Operation. '''
''' Create input RSForm. '''
schema = RSForm.create(
owner=self.model.owner,
alias=operation.alias,
@ -268,7 +254,7 @@ class OperationSchema:
return schema
def execute_operation(self, operation: Operation) -> bool:
''' Execute target Operation. '''
''' Execute target operation. '''
schemas = [
arg.argument.result
for arg in operation.getQ_arguments().order_by('order')
@ -315,7 +301,7 @@ class OperationSchema:
return True
def relocate_down(self, source: RSForm, destination: RSForm, items: list[Constituenta]):
''' Move list of Constituents to destination Schema inheritor. '''
''' Move list of constituents to specific schema inheritor. '''
self.cache.ensure_loaded()
self.cache.insert_schema(source)
self.cache.insert_schema(destination)
@ -329,7 +315,7 @@ class OperationSchema:
Inheritance.objects.filter(operation_id=operation.pk, parent__in=items).delete()
def relocate_up(self, source: RSForm, destination: RSForm, items: list[Constituenta]) -> list[Constituenta]:
''' Move list of Constituents upstream to destination Schema. '''
''' Move list of constituents to specific schema upstream. '''
self.cache.ensure_loaded()
self.cache.insert_schema(source)
self.cache.insert_schema(destination)
@ -359,7 +345,7 @@ class OperationSchema:
cst_list: list[Constituenta],
exclude: Optional[list[int]] = None
) -> None:
''' Trigger cascade resolutions when new Constituenta is created. '''
''' Trigger cascade resolutions when new constituent is created. '''
self.cache.insert_schema(source)
inserted_aliases = [cst.alias for cst in cst_list]
depend_aliases: set[str] = set()
@ -375,13 +361,13 @@ class OperationSchema:
self._cascade_inherit_cst(operation.pk, source, cst_list, alias_mapping, exclude)
def after_change_cst_type(self, source: RSForm, target: Constituenta) -> None:
''' Trigger cascade resolutions when Constituenta type is changed. '''
''' Trigger cascade resolutions when constituenta type is changed. '''
self.cache.insert_schema(source)
operation = self.cache.get_operation(source.model.pk)
self._cascade_change_cst_type(operation.pk, target.pk, cast(CstType, target.cst_type))
def after_update_cst(self, source: RSForm, target: Constituenta, data: dict, old_data: dict) -> None:
''' Trigger cascade resolutions when Constituenta data is changed. '''
''' Trigger cascade resolutions when constituenta data is changed. '''
self.cache.insert_schema(source)
operation = self.cache.get_operation(source.model.pk)
depend_aliases = self._extract_data_references(data, old_data)
@ -399,13 +385,13 @@ class OperationSchema:
)
def before_delete_cst(self, source: RSForm, target: list[Constituenta]) -> None:
''' Trigger cascade resolutions before Constituents are deleted. '''
''' Trigger cascade resolutions before constituents are deleted. '''
self.cache.insert_schema(source)
operation = self.cache.get_operation(source.model.pk)
self._cascade_delete_inherited(operation.pk, target)
def before_substitute(self, source: RSForm, substitutions: CstSubstitution) -> None:
''' Trigger cascade resolutions before Constituents are substituted. '''
''' Trigger cascade resolutions before constituents are substituted. '''
self.cache.insert_schema(source)
operation = self.cache.get_operation(source.model.pk)
self._cascade_before_substitute(substitutions, operation)

View File

@ -17,7 +17,7 @@ class PropagationFacade:
@staticmethod
def after_create_cst(source: RSForm, new_cst: list[Constituenta], exclude: Optional[list[int]] = None) -> None:
''' Trigger cascade resolutions when new constituenta is created. '''
''' Trigger cascade resolutions when new constituent is created. '''
hosts = _get_oss_hosts(source.model)
for host in hosts:
if exclude is None or host.pk not in exclude:

View File

@ -3,23 +3,20 @@
from .basics import LayoutSerializer, SubstitutionExSerializer
from .data_access import (
ArgumentSerializer,
BlockCreateSerializer,
BlockSerializer,
CreateBlockSerializer,
CreateOperationSerializer,
DeleteBlockSerializer,
DeleteOperationSerializer,
MoveItemsSerializer,
OperationCreateSerializer,
OperationDeleteSerializer,
OperationSchemaSerializer,
OperationSerializer,
OperationTargetSerializer,
OperationUpdateSerializer,
RelocateConstituentsSerializer,
SetOperationInputSerializer,
TargetOperationSerializer,
UpdateBlockSerializer,
UpdateOperationSerializer
SetOperationInputSerializer
)
from .responses import (
BlockCreatedResponse,
ConstituentaReferenceResponse,
OperationCreatedResponse,
SchemaCreatedResponse
NewBlockResponse,
NewOperationResponse,
NewSchemaResponse
)

View File

@ -1,5 +1,4 @@
''' Serializers for persistent data manipulation. '''
from collections import deque
from typing import cast
from django.db.models import F
@ -42,7 +41,7 @@ class ArgumentSerializer(serializers.ModelSerializer):
fields = ('operation', 'argument')
class CreateBlockSerializer(serializers.Serializer):
class BlockCreateSerializer(serializers.Serializer):
''' Serializer: Block creation. '''
class BlockCreateData(serializers.ModelSerializer):
''' Serializer: Block creation data. '''
@ -53,6 +52,7 @@ class CreateBlockSerializer(serializers.Serializer):
fields = 'title', 'description', 'parent'
layout = LayoutSerializer()
item_data = BlockCreateData()
width = serializers.FloatField()
height = serializers.FloatField()
@ -63,10 +63,9 @@ class CreateBlockSerializer(serializers.Serializer):
def validate(self, attrs):
oss = cast(LibraryItem, self.context['oss'])
parent = attrs['item_data'].get('parent')
children_blocks = attrs.get('children_blocks', [])
if parent is not None and 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({
'parent': msg.parentNotInOSS()
})
@ -77,114 +76,17 @@ class CreateBlockSerializer(serializers.Serializer):
'children_operations': msg.childNotInOSS()
})
for block in children_blocks:
for block in attrs['children_blocks']:
if block.oss_id != oss.pk:
raise serializers.ValidationError({
'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
class UpdateBlockSerializer(serializers.Serializer):
''' Serializer: Block update. '''
class UpdateBlockData(serializers.ModelSerializer):
''' Serializer: Block update data. '''
class Meta:
''' serializer metadata. '''
model = Block
fields = 'title', 'description', 'parent'
layout = LayoutSerializer(required=False)
target = PKField(many=False, queryset=Block.objects.all())
item_data = UpdateBlockData()
def validate(self, attrs):
oss = cast(LibraryItem, self.context['oss'])
block = cast(Block, attrs['target'])
if block.oss_id != oss.pk:
raise serializers.ValidationError({
'target': msg.blockNotInOSS()
})
parent = attrs['item_data'].get('parent')
if parent is not None:
if parent.oss_id != oss.pk:
raise serializers.ValidationError({
'parent': msg.parentNotInOSS()
})
if parent == attrs['target']:
raise serializers.ValidationError({
'parent': msg.blockCyclicHierarchy()
})
return attrs
class DeleteBlockSerializer(serializers.Serializer):
''' Serializer: Delete block. '''
layout = LayoutSerializer()
target = PKField(many=False, queryset=Block.objects.all().only('oss_id'))
def validate(self, attrs):
oss = cast(LibraryItem, self.context['oss'])
block = cast(Block, attrs['target'])
if block.oss_id != oss.pk:
raise serializers.ValidationError({
'target': msg.blockNotInOSS()
})
return attrs
class MoveItemsSerializer(serializers.Serializer):
''' Serializer: Move items to another parent. '''
layout = LayoutSerializer()
operations = PKField(many=True, queryset=Operation.objects.all().only('oss_id', 'parent'))
blocks = PKField(many=True, queryset=Block.objects.all().only('oss_id', 'parent'))
destination = PKField(many=False, queryset=Block.objects.all().only('oss_id'), allow_null=True)
def validate(self, attrs):
oss = cast(LibraryItem, self.context['oss'])
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:
raise serializers.ValidationError({
'destination': msg.blockNotInOSS()
})
for operation in moved_operations:
if operation.oss_id != oss.pk:
raise serializers.ValidationError({
'operations': msg.operationNotInOSS()
})
for block in moved_blocks:
if parent_block is not None and block.pk == parent_block.pk:
raise serializers.ValidationError({
'destination': msg.blockCyclicHierarchy()
})
if block.oss_id != oss.pk:
raise serializers.ValidationError({
'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
class CreateOperationSerializer(serializers.Serializer):
class OperationCreateSerializer(serializers.Serializer):
''' Serializer: Operation creation. '''
class CreateOperationData(serializers.ModelSerializer):
class OperationCreateData(serializers.ModelSerializer):
''' Serializer: Operation creation data. '''
alias = serializers.CharField()
operation_type = serializers.ChoiceField(OperationType.choices)
@ -197,7 +99,8 @@ class CreateOperationSerializer(serializers.Serializer):
'description', 'result', 'parent'
layout = LayoutSerializer()
item_data = CreateOperationData()
item_data = OperationCreateData()
position_x = serializers.FloatField()
position_y = serializers.FloatField()
create_schema = serializers.BooleanField(default=False, required=False)
@ -205,8 +108,9 @@ class CreateOperationSerializer(serializers.Serializer):
def validate(self, attrs):
oss = cast(LibraryItem, self.context['oss'])
parent = attrs['item_data'].get('parent')
if parent is not None and 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({
'parent': msg.parentNotInOSS()
})
@ -216,23 +120,23 @@ class CreateOperationSerializer(serializers.Serializer):
for operation in attrs['arguments']:
if operation.oss_id != oss.pk:
raise serializers.ValidationError({
'arguments': msg.operationNotInOSS()
'arguments': msg.operationNotInOSS(oss.title)
})
return attrs
class UpdateOperationSerializer(serializers.Serializer):
class OperationUpdateSerializer(serializers.Serializer):
''' Serializer: Operation update. '''
class UpdateOperationData(serializers.ModelSerializer):
class OperationUpdateData(serializers.ModelSerializer):
''' Serializer: Operation update data. '''
class Meta:
''' serializer metadata. '''
model = Operation
fields = 'alias', 'title', 'description', 'parent'
fields = 'alias', 'title', 'description'
layout = LayoutSerializer(required=False)
layout = LayoutSerializer()
target = PKField(many=False, queryset=Operation.objects.all())
item_data = UpdateOperationData()
item_data = OperationUpdateData()
arguments = PKField(many=True, queryset=Operation.objects.all().only('oss_id', 'result_id'), required=False)
substitutions = serializers.ListField(
child=SubstitutionSerializerBase(),
@ -241,14 +145,7 @@ class UpdateOperationSerializer(serializers.Serializer):
def validate(self, attrs):
oss = cast(LibraryItem, self.context['oss'])
parent = attrs['item_data'].get('parent')
target = cast(Block, attrs['target'])
if target.oss_id != oss.pk:
raise serializers.ValidationError({
'target': msg.operationNotInOSS()
})
if parent is not None and parent.oss_id != oss.pk:
if 'parent' in attrs['item_data'] and attrs['item_data']['parent'].oss_id != oss.pk:
raise serializers.ValidationError({
'parent': msg.parentNotInOSS()
})
@ -263,7 +160,7 @@ class UpdateOperationSerializer(serializers.Serializer):
for operation in attrs['arguments']:
if operation.oss_id != oss.pk:
raise serializers.ValidationError({
'arguments': msg.operationNotInOSS()
'arguments': msg.operationNotInOSS(oss.title)
})
if 'substitutions' not in attrs:
@ -295,7 +192,22 @@ class UpdateOperationSerializer(serializers.Serializer):
return attrs
class DeleteOperationSerializer(serializers.Serializer):
class OperationTargetSerializer(serializers.Serializer):
''' Serializer: Target single operation. '''
layout = LayoutSerializer()
target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'result_id'))
def validate(self, attrs):
oss = cast(LibraryItem, self.context['oss'])
operation = cast(Operation, attrs['target'])
if oss and operation.oss_id != oss.pk:
raise serializers.ValidationError({
'target': msg.operationNotInOSS(oss.title)
})
return attrs
class OperationDeleteSerializer(serializers.Serializer):
''' Serializer: Delete operation. '''
layout = LayoutSerializer()
target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'result'))
@ -305,24 +217,9 @@ class DeleteOperationSerializer(serializers.Serializer):
def validate(self, attrs):
oss = cast(LibraryItem, self.context['oss'])
operation = cast(Operation, attrs['target'])
if operation.oss_id != oss.pk:
if oss and operation.oss_id != oss.pk:
raise serializers.ValidationError({
'target': msg.operationNotInOSS()
})
return attrs
class TargetOperationSerializer(serializers.Serializer):
''' Serializer: Target single operation. '''
layout = LayoutSerializer()
target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'result_id'))
def validate(self, attrs):
oss = cast(LibraryItem, self.context['oss'])
operation = cast(Operation, attrs['target'])
if operation.oss_id != oss.pk:
raise serializers.ValidationError({
'target': msg.operationNotInOSS()
'target': msg.operationNotInOSS(oss.title)
})
return attrs
@ -343,7 +240,7 @@ class SetOperationInputSerializer(serializers.Serializer):
operation = cast(Operation, attrs['target'])
if oss and operation.oss_id != oss.pk:
raise serializers.ValidationError({
'target': msg.operationNotInOSS()
'target': msg.operationNotInOSS(oss.title)
})
if operation.operation_type != OperationType.INPUT:
raise serializers.ValidationError({
@ -458,29 +355,3 @@ class RelocateConstituentsSerializer(serializers.Serializer):
})
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

View File

@ -6,19 +6,19 @@ from apps.library.serializers import LibraryItemSerializer
from .data_access import BlockSerializer, OperationSchemaSerializer, OperationSerializer
class OperationCreatedResponse(serializers.Serializer):
class NewOperationResponse(serializers.Serializer):
''' Serializer: Create operation response. '''
new_operation = OperationSerializer()
oss = OperationSchemaSerializer()
class BlockCreatedResponse(serializers.Serializer):
class NewBlockResponse(serializers.Serializer):
''' Serializer: Create block response. '''
new_block = BlockSerializer()
oss = OperationSchemaSerializer()
class SchemaCreatedResponse(serializers.Serializer):
class NewSchemaResponse(serializers.Serializer):
''' Serializer: Create RSForm for input operation response. '''
new_schema = LibraryItemSerializer()
oss = OperationSchemaSerializer()

View File

@ -27,10 +27,6 @@ class TestOssBlocks(EndpointTester):
self.block1 = self.owned.create_block(
title='1',
)
self.block2 = self.owned.create_block(
title='2',
parent=self.block1
)
self.operation1 = self.owned.create_operation(
alias='1',
operation_type=OperationType.INPUT,
@ -39,12 +35,15 @@ class TestOssBlocks(EndpointTester):
self.operation2 = self.owned.create_operation(
alias='2',
operation_type=OperationType.INPUT,
parent=self.block2,
)
self.operation3 = self.unowned.create_operation(
alias='3',
operation_type=OperationType.INPUT
)
self.block2 = self.owned.create_block(
title='2',
parent=self.block1
)
self.block3 = self.unowned.create_block(
title='3',
parent=self.block1
@ -166,99 +165,3 @@ class TestOssBlocks(EndpointTester):
self.block1.refresh_from_db()
self.assertEqual(self.operation1.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')
def test_delete_block(self):
self.populateData()
self.executeNotFound(item=self.invalid_id)
self.executeBadData(item=self.owned_id)
data = {
'layout': self.layout_data
}
self.executeBadData(data=data)
data['target'] = self.operation1.pk
self.executeBadData(data=data)
data['target'] = self.block3.pk
self.executeBadData(data=data)
data['target'] = self.block2.pk
self.logout()
self.executeForbidden(data=data)
self.login()
response = self.executeOK(data=data)
self.operation2.refresh_from_db()
self.assertEqual(len(response.data['blocks']), 1)
self.assertEqual(self.operation2.parent.pk, self.block1.pk)
data['target'] = self.block1.pk
response = self.executeOK(data=data)
self.operation1.refresh_from_db()
self.operation2.refresh_from_db()
self.assertEqual(len(response.data['blocks']), 0)
self.assertEqual(self.operation1.parent, None)
self.assertEqual(self.operation2.parent, None)
@decl_endpoint('/api/oss/{item}/update-block', method='patch')
def test_update_block(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'target': self.invalid_id,
'item_data': {
'title': 'Test title mod',
'description': 'Comment mod',
'parent': None
},
}
self.executeBadData(data=data)
data['target'] = self.block3.pk
self.toggle_admin(True)
self.executeBadData(data=data)
data['target'] = self.block2.pk
self.logout()
self.executeForbidden(data=data)
self.login()
response = self.executeOK(data=data)
self.block2.refresh_from_db()
self.assertEqual(self.block2.title, data['item_data']['title'])
self.assertEqual(self.block2.description, data['item_data']['description'])
self.assertEqual(self.block2.parent, data['item_data']['parent'])
data['layout'] = self.layout_data
self.executeOK(data=data)

View File

@ -226,8 +226,9 @@ class TestOssOperations(EndpointTester):
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_operation(self):
self.populateData()
self.executeNotFound(item=self.invalid_id)
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
@ -370,6 +371,7 @@ class TestOssOperations(EndpointTester):
'title': 'Test title mod',
'description': 'Comment mod'
},
'layout': self.layout_data,
'arguments': [self.operation2.pk, self.operation1.pk],
'substitutions': [
{
@ -401,10 +403,6 @@ class TestOssOperations(EndpointTester):
self.assertEqual(sub.original.pk, data['substitutions'][0]['original'])
self.assertEqual(sub.substitution.pk, data['substitutions'][0]['substitution'])
data['layout'] = self.layout_data
self.executeOK(data=data)
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_update_operation_sync(self):
self.populateData()

View File

@ -55,13 +55,12 @@ class TestOssViewset(EndpointTester):
alias='3',
operation_type=OperationType.SYNTHESIS
)
self.layout_data = {'operations': [
layout = self.owned.layout()
layout.data = {'operations': [
{'id': self.operation1.pk, 'x': 0, 'y': 0},
{'id': self.operation2.pk, 'x': 0, 'y': 0},
{'id': self.operation3.pk, 'x': 0, 'y': 0},
], 'blocks': []}
layout = self.owned.layout()
layout.data = self.layout_data
layout.save()
self.owned.set_arguments(self.operation3.pk, [self.operation1, self.operation2])
@ -168,61 +167,6 @@ class TestOssViewset(EndpointTester):
self.assertEqual(response.data['id'], self.ks1X2.pk)
self.assertEqual(response.data['schema'], self.ks1.model.pk)
@decl_endpoint('/api/oss/{item}/move-items', method='patch')
def test_move_items(self):
self.populateData()
self.executeBadData(item=self.owned_id)
block1 = self.owned.create_block(title='1')
block2 = self.owned.create_block(title='2')
data = {
'layout': self.layout_data,
'blocks': [block2.pk],
'operations': [self.operation1.pk, self.operation2.pk],
'destination': block2.pk
}
self.executeBadData(data=data)
data['destination'] = block1.pk
self.executeOK(data=data)
self.operation1.refresh_from_db()
self.operation2.refresh_from_db()
block2.refresh_from_db()
self.assertEqual(self.operation1.parent.pk, block1.pk)
self.assertEqual(self.operation2.parent.pk, block1.pk)
self.assertEqual(block2.parent.pk, block1.pk)
data['destination'] = None
self.executeOK(data=data)
self.operation1.refresh_from_db()
self.operation2.refresh_from_db()
block2.refresh_from_db()
self.assertEqual(block2.parent, None)
self.assertEqual(self.operation1.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')
def test_relocate_constituents(self):
self.populateData()

View File

@ -37,15 +37,12 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
''' Determine permission class. '''
if self.action in [
'update_layout',
'create_block',
'update_block',
'delete_block',
'move_items',
'create_operation',
'update_operation',
'create_block',
'delete_operation',
'create_input',
'set_input',
'update_operation',
'execute_operation',
'relocate_constituents'
]:
@ -94,168 +91,12 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
m.OperationSchema(self.get_object()).update_layout(serializer.validated_data)
return Response(status=c.HTTP_200_OK)
@extend_schema(
summary='create block',
tags=['OSS'],
request=s.CreateBlockSerializer(),
responses={
c.HTTP_201_CREATED: s.BlockCreatedResponse,
c.HTTP_400_BAD_REQUEST: None,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['post'], url_path='create-block')
def create_block(self, request: Request, pk) -> HttpResponse:
''' Create Block. '''
serializer = s.CreateBlockSerializer(
data=request.data,
context={'oss': self.get_object()}
)
serializer.is_valid(raise_exception=True)
oss = m.OperationSchema(self.get_object())
layout = serializer.validated_data['layout']
children_blocks: list[m.Block] = serializer.validated_data['children_blocks']
children_operations: list[m.Operation] = serializer.validated_data['children_operations']
with transaction.atomic():
new_block = oss.create_block(**serializer.validated_data['item_data'])
layout['blocks'].append({
'id': new_block.pk,
'x': serializer.validated_data['position_x'],
'y': serializer.validated_data['position_y'],
'width': serializer.validated_data['width'],
'height': serializer.validated_data['height'],
})
oss.update_layout(layout)
if len(children_blocks) > 0:
for block in children_blocks:
block.parent = new_block
m.Block.objects.bulk_update(children_blocks, ['parent'])
if len(children_operations) > 0:
for operation in children_operations:
operation.parent = new_block
m.Operation.objects.bulk_update(children_operations, ['parent'])
return Response(
status=c.HTTP_201_CREATED,
data={
'new_block': s.BlockSerializer(new_block).data,
'oss': s.OperationSchemaSerializer(oss.model).data
}
)
@extend_schema(
summary='update block',
tags=['OSS'],
request=s.UpdateBlockSerializer(),
responses={
c.HTTP_200_OK: s.OperationSchemaSerializer,
c.HTTP_400_BAD_REQUEST: None,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='update-block')
def update_block(self, request: Request, pk) -> HttpResponse:
''' Update Block. '''
serializer = s.UpdateBlockSerializer(
data=request.data,
context={'oss': self.get_object()}
)
serializer.is_valid(raise_exception=True)
block: m.Block = cast(m.Block, serializer.validated_data['target'])
oss = m.OperationSchema(self.get_object())
with transaction.atomic():
if 'layout' in serializer.validated_data:
oss.update_layout(serializer.validated_data['layout'])
if 'title' in serializer.validated_data['item_data']:
block.title = serializer.validated_data['item_data']['title']
if 'description' in serializer.validated_data['item_data']:
block.description = serializer.validated_data['item_data']['description']
if 'parent' in serializer.validated_data['item_data']:
block.parent = serializer.validated_data['item_data']['parent']
block.save(update_fields=['title', 'description', 'parent'])
return Response(
status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data
)
@extend_schema(
summary='delete block',
tags=['OSS'],
request=s.DeleteBlockSerializer,
responses={
c.HTTP_200_OK: s.OperationSchemaSerializer,
c.HTTP_400_BAD_REQUEST: None,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='delete-block')
def delete_block(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Delete Block. '''
serializer = s.DeleteBlockSerializer(
data=request.data,
context={'oss': self.get_object()}
)
serializer.is_valid(raise_exception=True)
oss = m.OperationSchema(self.get_object())
block = cast(m.Block, serializer.validated_data['target'])
layout = serializer.validated_data['layout']
layout['blocks'] = [x for x in layout['blocks'] if x['id'] != block.pk]
with transaction.atomic():
oss.delete_block(block)
oss.update_layout(layout)
return Response(
status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data
)
@extend_schema(
summary='move items',
tags=['OSS'],
request=s.MoveItemsSerializer(),
responses={
c.HTTP_200_OK: s.OperationSchemaSerializer,
c.HTTP_400_BAD_REQUEST: None,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='move-items')
def move_items(self, request: Request, pk) -> HttpResponse:
''' Move items to another parent. '''
serializer = s.MoveItemsSerializer(
data=request.data,
context={'oss': self.get_object()}
)
serializer.is_valid(raise_exception=True)
oss = m.OperationSchema(self.get_object())
with transaction.atomic():
oss.update_layout(serializer.validated_data['layout'])
for operation in serializer.validated_data['operations']:
operation.parent = serializer.validated_data['destination']
operation.save(update_fields=['parent'])
for block in serializer.validated_data['blocks']:
block.parent = serializer.validated_data['destination']
block.save(update_fields=['parent'])
return Response(
status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data
)
@extend_schema(
summary='create operation',
tags=['OSS'],
request=s.CreateOperationSerializer(),
request=s.OperationCreateSerializer(),
responses={
c.HTTP_201_CREATED: s.OperationCreatedResponse,
c.HTTP_201_CREATED: s.NewOperationResponse,
c.HTTP_400_BAD_REQUEST: None,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
@ -263,8 +104,8 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
)
@action(detail=True, methods=['post'], url_path='create-operation')
def create_operation(self, request: Request, pk) -> HttpResponse:
''' Create Operation. '''
serializer = s.CreateOperationSerializer(
''' Create new operation. '''
serializer = s.OperationCreateSerializer(
data=request.data,
context={'oss': self.get_object()}
)
@ -312,58 +153,60 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
)
@extend_schema(
summary='update operation',
summary='create block',
tags=['OSS'],
request=s.UpdateOperationSerializer(),
request=s.BlockCreateSerializer(),
responses={
c.HTTP_200_OK: s.OperationSchemaSerializer,
c.HTTP_201_CREATED: s.NewBlockResponse,
c.HTTP_400_BAD_REQUEST: None,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='update-operation')
def update_operation(self, request: Request, pk) -> HttpResponse:
''' Update Operation arguments and parameters. '''
serializer = s.UpdateOperationSerializer(
@action(detail=True, methods=['post'], url_path='create-block')
def create_block(self, request: Request, pk) -> HttpResponse:
''' Create new block. '''
serializer = s.BlockCreateSerializer(
data=request.data,
context={'oss': self.get_object()}
)
serializer.is_valid(raise_exception=True)
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
oss = m.OperationSchema(self.get_object())
layout = serializer.validated_data['layout']
children_blocks: list[m.Block] = serializer.validated_data['children_blocks']
children_operations: list[m.Operation] = serializer.validated_data['children_operations']
with transaction.atomic():
if 'layout' in serializer.validated_data:
oss.update_layout(serializer.validated_data['layout'])
if 'alias' in serializer.validated_data['item_data']:
operation.alias = serializer.validated_data['item_data']['alias']
if 'title' in serializer.validated_data['item_data']:
operation.title = serializer.validated_data['item_data']['title']
if 'description' in serializer.validated_data['item_data']:
operation.description = serializer.validated_data['item_data']['description']
operation.save(update_fields=['alias', 'title', 'description'])
new_block = oss.create_block(**serializer.validated_data['item_data'])
layout['blocks'].append({
'id': new_block.pk,
'x': serializer.validated_data['position_x'],
'y': serializer.validated_data['position_y'],
'width': serializer.validated_data['width'],
'height': serializer.validated_data['height'],
})
oss.update_layout(layout)
if len(children_blocks) > 0:
for block in children_blocks:
block.parent = new_block
m.Block.objects.bulk_update(children_blocks, ['parent'])
if len(children_operations) > 0:
for operation in children_operations:
operation.parent = new_block
m.Operation.objects.bulk_update(children_operations, ['parent'])
if operation.result is not None:
can_edit = permissions.can_edit_item(request.user, operation.result)
if can_edit or operation.operation_type == m.OperationType.SYNTHESIS:
operation.result.alias = operation.alias
operation.result.title = operation.title
operation.result.description = operation.description
operation.result.save()
if 'arguments' in serializer.validated_data:
oss.set_arguments(operation.pk, serializer.validated_data['arguments'])
if 'substitutions' in serializer.validated_data:
oss.set_substitutions(operation.pk, serializer.validated_data['substitutions'])
return Response(
status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data
status=c.HTTP_201_CREATED,
data={
'new_block': s.BlockSerializer(new_block).data,
'oss': s.OperationSchemaSerializer(oss.model).data
}
)
@extend_schema(
summary='delete operation',
tags=['OSS'],
request=s.DeleteOperationSerializer,
request=s.OperationDeleteSerializer,
responses={
c.HTTP_200_OK: s.OperationSchemaSerializer,
c.HTTP_400_BAD_REQUEST: None,
@ -373,8 +216,8 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
)
@action(detail=True, methods=['patch'], url_path='delete-operation')
def delete_operation(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Delete Operation. '''
serializer = s.DeleteOperationSerializer(
''' Endpoint: Delete operation. '''
serializer = s.OperationDeleteSerializer(
data=request.data,
context={'oss': self.get_object()}
)
@ -403,9 +246,9 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@extend_schema(
summary='create input schema for target operation',
tags=['OSS'],
request=s.TargetOperationSerializer(),
request=s.OperationTargetSerializer(),
responses={
c.HTTP_200_OK: s.SchemaCreatedResponse,
c.HTTP_200_OK: s.NewSchemaResponse,
c.HTTP_400_BAD_REQUEST: None,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
@ -413,8 +256,8 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
)
@action(detail=True, methods=['patch'], url_path='create-input')
def create_input(self, request: Request, pk) -> HttpResponse:
''' Create input RSForm. '''
serializer = s.TargetOperationSerializer(
''' Create new input RSForm. '''
serializer = s.OperationTargetSerializer(
data=request.data,
context={'oss': self.get_object()}
)
@ -490,10 +333,55 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
data=s.OperationSchemaSerializer(oss.model).data
)
@extend_schema(
summary='update operation',
tags=['OSS'],
request=s.OperationUpdateSerializer(),
responses={
c.HTTP_200_OK: s.OperationSchemaSerializer,
c.HTTP_400_BAD_REQUEST: None,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='update-operation')
def update_operation(self, request: Request, pk) -> HttpResponse:
''' Update operation arguments and parameters. '''
serializer = s.OperationUpdateSerializer(
data=request.data,
context={'oss': self.get_object()}
)
serializer.is_valid(raise_exception=True)
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
oss = m.OperationSchema(self.get_object())
with transaction.atomic():
oss.update_layout(serializer.validated_data['layout'])
operation.alias = serializer.validated_data['item_data']['alias']
operation.title = serializer.validated_data['item_data']['title']
operation.description = serializer.validated_data['item_data']['description']
operation.save(update_fields=['alias', 'title', 'description'])
if operation.result is not None:
can_edit = permissions.can_edit_item(request.user, operation.result)
if can_edit or operation.operation_type == m.OperationType.SYNTHESIS:
operation.result.alias = operation.alias
operation.result.title = operation.title
operation.result.description = operation.description
operation.result.save()
if 'arguments' in serializer.validated_data:
oss.set_arguments(operation.pk, serializer.validated_data['arguments'])
if 'substitutions' in serializer.validated_data:
oss.set_substitutions(operation.pk, serializer.validated_data['substitutions'])
return Response(
status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data
)
@extend_schema(
summary='execute operation',
tags=['OSS'],
request=s.TargetOperationSerializer(),
request=s.OperationTargetSerializer(),
responses={
c.HTTP_200_OK: s.OperationSchemaSerializer,
c.HTTP_400_BAD_REQUEST: None,
@ -504,7 +392,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@action(detail=True, methods=['post'], url_path='execute-operation')
def execute_operation(self, request: Request, pk) -> HttpResponse:
''' Execute operation. '''
serializer = s.TargetOperationSerializer(
serializer = s.OperationTargetSerializer(
data=request.data,
context={'oss': self.get_object()}
)

View File

@ -137,7 +137,7 @@ class RSForm:
return result
def create_cst(self, data: dict, insert_after: Optional[Constituenta] = None) -> Constituenta:
''' Create constituenta from data. '''
''' Create new cst from data. '''
if insert_after is None:
position = INSERT_LAST
else:

View File

@ -77,7 +77,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
)
@action(detail=True, methods=['post'], url_path='create-cst')
def create_cst(self, request: Request, pk) -> HttpResponse:
''' Create Constituenta. '''
''' Create new constituenta. '''
serializer = s.CstCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
@ -254,7 +254,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
)
@action(detail=True, methods=['patch'], url_path='delete-multiple-cst')
def delete_multiple_cst(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Delete multiple Constituents. '''
''' Endpoint: Delete multiple constituents. '''
model = self._get_item()
serializer = s.CstListSerializer(
data=request.data,
@ -284,7 +284,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
)
@action(detail=True, methods=['patch'], url_path='move-cst')
def move_cst(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Move multiple Constituents. '''
''' Endpoint: Move multiple constituents. '''
model = self._get_item()
serializer = s.CstMoveSerializer(
data=request.data,
@ -334,7 +334,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
)
@action(detail=True, methods=['patch'], url_path='restore-order')
def restore_order(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Restore order based on types and Term graph. '''
''' Endpoint: Restore order based on types and term graph. '''
model = self._get_item()
m.RSForm(model).restore_order()
return Response(
@ -449,7 +449,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
)
@action(detail=True, methods=['post'], url_path='check-constituenta')
def check_constituenta(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Check RSLang expression against Schema context. '''
''' Endpoint: Check RSLang expression against schema context. '''
serializer = s.ConstituentaCheckSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
expression = serializer.validated_data['definition_formal']
@ -474,7 +474,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
)
@action(detail=True, methods=['post'], url_path='resolve')
def resolve(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Resolve references in text against Schema terms context. '''
''' Endpoint: Resolve references in text against schema terms context. '''
serializer = s.TextSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
text = serializer.validated_data['text']
@ -543,7 +543,7 @@ class TrsImportView(views.APIView):
@extend_schema(
summary='create RSForm empty or from file',
summary='create new RSForm empty or from file',
tags=['RSForm'],
request=LibraryItemSerializer,
responses={

View File

@ -7,23 +7,15 @@ def constituentaNotInRSform(title: str):
def constituentaNotFromOperation():
return 'Конституента не соответствую аргументам операции'
return f'Конституента не соответствую аргументам операции'
def operationNotInOSS():
return 'Операция не принадлежит ОСС'
def blockNotInOSS():
return 'Блок не принадлежит ОСС'
def operationNotInOSS(title: str):
return f'Операция не принадлежит ОСС: {title}'
def parentNotInOSS():
return 'Родительский блок не принадлежит ОСС'
def blockCyclicHierarchy():
return 'Попытка создания циклического вложения'
return f'Родительский блок не принадлежит ОСС'
def childNotInOSS():

File diff suppressed because it is too large Load Diff

View File

@ -17,70 +17,70 @@
"@dagrejs/dagre": "^1.1.4",
"@hookform/resolvers": "^5.0.1",
"@lezer/lr": "^1.4.2",
"@radix-ui/react-popover": "^1.1.13",
"@radix-ui/react-select": "^2.2.4",
"@tanstack/react-query": "^5.76.0",
"@tanstack/react-query-devtools": "^5.76.0",
"@tanstack/react-table": "^8.21.3",
"@uiw/codemirror-themes": "^4.23.12",
"@uiw/react-codemirror": "^4.23.12",
"axios": "^1.9.0",
"@radix-ui/react-popover": "^1.1.7",
"@radix-ui/react-select": "^2.1.7",
"@tanstack/react-query": "^5.72.2",
"@tanstack/react-query-devtools": "^5.72.2",
"@tanstack/react-table": "^8.21.2",
"@uiw/codemirror-themes": "^4.23.10",
"@uiw/react-codemirror": "^4.23.10",
"axios": "^1.8.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"global": "^4.4.0",
"js-file-download": "^0.4.12",
"lucide-react": "^0.510.0",
"lucide-react": "^0.487.0",
"qrcode.react": "^4.2.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-error-boundary": "^6.0.0",
"react-hook-form": "^7.56.3",
"react-error-boundary": "^5.0.0",
"react-hook-form": "^7.55.0",
"react-icons": "^5.5.0",
"react-intl": "^7.1.11",
"react-router": "^7.6.0",
"react-intl": "^7.1.10",
"react-router": "^7.5.0",
"react-scan": "^0.3.3",
"react-tabs": "^6.1.0",
"react-toastify": "^11.0.5",
"react-tooltip": "^5.28.1",
"react-zoom-pan-pinch": "^3.7.0",
"reactflow": "^11.11.4",
"tailwind-merge": "^3.3.0",
"tw-animate-css": "^1.2.9",
"tailwind-merge": "^3.2.0",
"tw-animate-css": "^1.2.5",
"use-debounce": "^10.0.4",
"zod": "^3.24.4",
"zustand": "^5.0.4"
"zod": "^3.24.2",
"zustand": "^5.0.3"
},
"devDependencies": {
"@lezer/generator": "^1.7.3",
"@playwright/test": "^1.52.0",
"@tailwindcss/vite": "^4.1.6",
"@playwright/test": "^1.51.1",
"@tailwindcss/vite": "^4.1.3",
"@types/jest": "^29.5.14",
"@types/node": "^22.15.17",
"@types/react": "^19.1.4",
"@types/react-dom": "^19.1.5",
"@types/node": "^22.14.0",
"@types/react": "^19.1.1",
"@types/react-dom": "^19.1.2",
"@typescript-eslint/eslint-plugin": "^8.0.1",
"@typescript-eslint/parser": "^8.0.1",
"@vitejs/plugin-react": "^4.4.1",
"babel-plugin-react-compiler": "^19.1.0-rc.1",
"eslint": "^9.26.0",
"@vitejs/plugin-react": "^4.3.4",
"babel-plugin-react-compiler": "^19.0.0-beta-21e868a-20250216",
"eslint": "^9.24.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-playwright": "^2.2.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-compiler": "^19.1.0-rc.1",
"eslint-plugin-react-compiler": "^19.0.0-beta-21e868a-20250216",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-simple-import-sort": "^12.1.1",
"globals": "^16.1.0",
"globals": "^16.0.0",
"jest": "^29.7.0",
"stylelint": "^16.19.1",
"stylelint": "^16.18.0",
"stylelint-config-recommended": "^16.0.0",
"stylelint-config-standard": "^38.0.0",
"stylelint-config-tailwindcss": "^1.0.0",
"tailwindcss": "^4.0.7",
"ts-jest": "^29.3.2",
"ts-jest": "^29.3.1",
"typescript": "^5.8.3",
"typescript-eslint": "^8.32.1",
"vite": "^6.3.5"
"typescript-eslint": "^8.29.1",
"vite": "^6.2.6"
},
"jest": {
"preset": "ts-jest",

View File

@ -9,7 +9,7 @@ import { useDialogsStore } from '@/stores/dialogs';
import { NavigationState } from './navigation/navigation-context';
import { Footer } from './footer';
import { GlobalDialogs } from './global-dialogs';
import { GlobalLoader } from './global-loader';
import { GlobalLoader } from './global-Loader';
import { ToasterThemed } from './global-toaster';
import { GlobalTooltips } from './global-tooltips';
import { MutationErrors } from './mutation-errors';
@ -28,7 +28,6 @@ export function ApplicationLayout() {
<div className='min-w-80 antialiased h-full max-w-480 mx-auto'>
<ToasterThemed
className={clsx('sm:text-[14px]/[20px] text-[12px]/[16px]', noNavigationAnimation ? 'mt-6' : 'mt-14')}
aria-label='Оповещения'
autoClose={3000}
draggable={false}
pauseOnFocusLoss={false}
@ -42,7 +41,7 @@ export function ApplicationLayout() {
<Navigation />
<div
className='overflow-x-auto max-w-[100dvw]'
className='overflow-x-auto max-w-[100vw]'
style={{ maxHeight: viewportHeight }}
inert={activeDialog !== null}
>

View File

@ -113,21 +113,6 @@ const DlgUploadRSForm = React.lazy(() =>
const DlgGraphParams = React.lazy(() =>
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
}))
);
const DlgEditBlock = React.lazy(() =>
import('@/features/oss/dialogs/dlg-edit-block').then(module => ({
default: module.DlgEditBlock
}))
);
const DlgOssSettings = React.lazy(() =>
import('@/features/oss/dialogs/dlg-oss-settings').then(module => ({
default: module.DlgOssSettings
}))
);
export const GlobalDialogs = () => {
const active = useDialogsStore(state => state.active);
@ -142,10 +127,6 @@ export const GlobalDialogs = () => {
return <DlgCreateCst />;
case DialogType.CREATE_OPERATION:
return <DlgCreateOperation />;
case DialogType.CREATE_BLOCK:
return <DlgCreateBlock />;
case DialogType.EDIT_BLOCK:
return <DlgEditBlock />;
case DialogType.DELETE_CONSTITUENTA:
return <DlgDeleteCst />;
case DialogType.EDIT_EDITORS:
@ -160,8 +141,6 @@ export const GlobalDialogs = () => {
return <DlgEditWordForms />;
case DialogType.INLINE_SYNTHESIS:
return <DlgInlineSynthesis />;
case DialogType.OSS_SETTINGS:
return <DlgOssSettings />;
case DialogType.SHOW_AST:
return <DlgShowAST />;
case DialogType.SHOW_TYPE_GRAPH:

View File

@ -11,7 +11,7 @@ export const GlobalTooltips = () => {
id={globalIDs.tooltip}
layer='z-topmost'
place='right-start'
className='mt-8 max-w-80 break-words rounded-lg!'
className='mt-8 max-w-80 break-words'
/>
<Tooltip
float

View File

@ -1,5 +1,3 @@
import clsx from 'clsx';
import { useMutationErrors } from '@/backend/use-mutation-errors';
import { Button } from '@/components/control';
import { DescribeError } from '@/components/info-error';
@ -22,23 +20,9 @@ export function MutationErrors() {
return (
<div className='cc-modal-wrapper'>
<ModalBackdrop onHide={resetErrors} />
<div
className={clsx(
'z-pop', //
'flex flex-col px-10 py-3 items-center',
'border rounded-xl bg-background'
)}
role='alertdialog'
>
<div className='z-pop px-10 py-3 flex flex-col items-center border rounded-xl bg-background' role='alertdialog'>
<h1 className='py-2 select-none'>Ошибка при обработке</h1>
<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'
)}
>
<div className='px-3 flex flex-col text-destructive text-sm font-semibold select-text'>
<DescribeError error={mutationErrors[0]} />
</div>
<Button onClick={resetErrors} className='w-fit' text='Закрыть' />

View File

@ -34,7 +34,7 @@ export function Navigation() {
<div
className={clsx(
'pl-2 sm:pr-4 h-12 flex cc-shadow-border',
'transition-[max-height,translate] ease-bezier duration-move',
'transition-[max-height,translate] ease-bezier duration-(--duration-move)',
noNavigationAnimation ? '-translate-y-6 max-h-0' : 'max-h-12'
)}
>

View File

@ -4,7 +4,9 @@
import { buildConstants } from '@/utils/build-constants';
/** Routes. */
/**
* Routes.
*/
export const routes = {
not_found: 'not-found',
login: 'login',
@ -20,9 +22,11 @@ export const routes = {
oss: 'oss',
icons: 'icons',
database_schema: 'database-schema'
} as const;
};
/** Internal navigation URLs. */
/**
* Internal navigation URLs.
*/
export const urls = {
page404: '/not-found',
admin: `${buildConstants.backend}/admin`,
@ -62,4 +66,4 @@ export const urls = {
oss_props: ({ id, tab }: { id: number | string; tab: number }) => {
return `/oss/${id}?tab=${tab}`;
}
} as const;
};

View File

@ -8,7 +8,6 @@ import { type z, ZodError } from 'zod';
import { buildConstants } from '@/utils/build-constants';
import { PARAMETER } from '@/utils/constants';
import { errorMsg } from '@/utils/labels';
import { type RO } from '@/utils/meta';
import { extractErrorMessage } from '@/utils/utils';
export { AxiosError } from 'axios';
@ -59,7 +58,7 @@ export function axiosGet<ResponseData>({ endpoint, options, schema }: IAxiosGetR
.get<ResponseData>(endpoint, options)
.then(response => {
schema?.parse(response.data);
return response.data as RO<ResponseData>;
return response.data;
})
.catch((error: Error | AxiosError) => {
// Note: Ignore cancellation errors
@ -82,7 +81,7 @@ export function axiosPost<RequestData, ResponseData = void>({
.then(response => {
schema?.parse(response.data);
notifySuccess(response.data, request?.successMessage);
return response.data as RO<ResponseData>;
return response.data;
})
.catch((error: Error | AxiosError | ZodError) => {
notifyError(error);
@ -101,7 +100,7 @@ export function axiosDelete<RequestData, ResponseData = void>({
.then(response => {
schema?.parse(response.data);
notifySuccess(response.data, request?.successMessage);
return response.data as RO<ResponseData>;
return response.data;
})
.catch((error: Error | AxiosError | ZodError) => {
notifyError(error);
@ -120,7 +119,7 @@ export function axiosPatch<RequestData, ResponseData = void>({
.then(response => {
schema?.parse(response.data);
notifySuccess(response.data, request?.successMessage);
return response.data as RO<ResponseData>;
return response.data;
})
.catch((error: Error | AxiosError | ZodError) => {
notifyError(error);

View File

@ -6,7 +6,7 @@ export const DELAYS = {
staleShort: 5 * 60 * 1000,
staleMedium: 1 * 60 * 60 * 1000,
staleLong: 24 * 60 * 60 * 1000
} as const;
};
/** API keys for local cache. */
export const KEYS = {
@ -19,8 +19,8 @@ export const KEYS = {
global_mutation: 'global_mutation',
composite: {
libraryList: ['library', 'list'] as const,
libraryList: ['library', 'list'],
ossItem: ({ itemID }: { itemID?: number }) => [KEYS.oss, 'item', itemID],
rsItem: ({ itemID, version }: { itemID?: number; version?: number }) => [KEYS.rsform, 'item', itemID, version ?? '']
}
} as const;
};

View File

@ -19,10 +19,19 @@ import { PaginationTools } from './pagination-tools';
import { TableBody } from './table-body';
import { TableFooter } from './table-footer';
import { TableHeader } from './table-header';
import { type IConditionalStyle, useDataTable } from './use-data-table';
import { useDataTable } from './use-data-table';
export { createColumnHelper, type RowSelectionState, type VisibilityState };
/** Style to conditionally apply to rows. */
export interface IConditionalStyle<TData> {
/** Callback to determine if the style should be applied. */
when: (rowData: TData) => boolean;
/** Style to apply. */
style: React.CSSProperties;
}
export interface DataTableProps<TData extends RowData>
extends Styling,
Pick<TableOptions<TData>, 'data' | 'columns' | 'onRowSelectionChange' | 'onColumnVisibilityChange'> {
@ -78,7 +87,7 @@ export interface DataTableProps<TData extends RowData>
paginationPerPage?: number;
/** List of options to choose from for pagination. */
paginationOptions?: readonly number[];
paginationOptions?: number[];
/** Callback to be called when the pagination option is changed. */
onChangePaginationOption?: (newValue: number) => void;

View File

@ -1,2 +1,7 @@
export { createColumnHelper, DataTable, type RowSelectionState, type VisibilityState } from './data-table';
export { type IConditionalStyle } from './use-data-table';
export {
createColumnHelper,
DataTable,
type IConditionalStyle,
type RowSelectionState,
type VisibilityState
} from './data-table';

View File

@ -1,16 +1,17 @@
'use no memo';
'use client';
import { useCallback } from 'react';
import { type Table } from '@tanstack/react-table';
import { IconPageFirst, IconPageLast, IconPageLeft, IconPageRight } from '../icons';
import { prefixes } from '@/utils/constants';
import { SelectPagination } from './select-pagination';
import { IconPageFirst, IconPageLast, IconPageLeft, IconPageRight } from '../icons';
interface PaginationToolsProps<TData> {
id?: string;
table: Table<TData>;
paginationOptions: readonly number[];
paginationOptions: number[];
onChangePaginationOption?: (newValue: number) => void;
}
@ -20,6 +21,15 @@ export function PaginationTools<TData>({
onChangePaginationOption,
paginationOptions
}: PaginationToolsProps<TData>) {
const handlePaginationOptionsChange = useCallback(
(event: React.ChangeEvent<HTMLSelectElement>) => {
const perPage = Number(event.target.value);
table.setPageSize(perPage);
onChangePaginationOption?.(perPage);
},
[table, onChangePaginationOption]
);
return (
<div className='flex justify-end items-center my-2 text-sm cc-controls select-none'>
<span className='mr-3'>
@ -83,12 +93,19 @@ export function PaginationTools<TData>({
<IconPageLast size='1.5rem' />
</button>
</div>
<SelectPagination
<select
id={id ? `${id}__per_page` : undefined}
table={table}
paginationOptions={paginationOptions}
onChange={onChangePaginationOption}
/>
aria-label='Выбор количества строчек на странице'
value={table.getState().pagination.pageSize}
onChange={handlePaginationOptionsChange}
className='mx-2 cursor-pointer bg-transparent focus-outline'
>
{paginationOptions.map(pageSize => (
<option key={`${prefixes.page_size}${pageSize}`} value={pageSize} aria-label={`${pageSize} на страницу`}>
{pageSize} на стр
</option>
))}
</select>
</div>
);
}

View File

@ -1,46 +0,0 @@
'use no memo';
'use client';
import { useCallback } from 'react';
import { type Table } from '@tanstack/react-table';
import { prefixes } from '@/utils/constants';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../input/select';
interface SelectPaginationProps<TData> {
id?: string;
table: Table<TData>;
paginationOptions: readonly number[];
onChange?: (newValue: number) => void;
}
export function SelectPagination<TData>({ id, table, paginationOptions, onChange }: SelectPaginationProps<TData>) {
const handlePaginationOptionsChange = useCallback(
(newValue: string) => {
const perPage = Number(newValue);
table.setPageSize(perPage);
onChange?.(perPage);
},
[table, onChange]
);
return (
<Select onValueChange={handlePaginationOptionsChange} defaultValue={String(table.getState().pagination.pageSize)}>
<SelectTrigger
id={id}
aria-label='Выбор количества строчек на странице'
className='mx-2 cursor-pointer bg-transparent focus-outline border-0 w-28 max-h-6 px-2 justify-end'
>
<SelectValue />
</SelectTrigger>
<SelectContent>
{paginationOptions?.map(option => (
<SelectItem key={`${prefixes.page_size}${option}`} value={String(option)}>
{option} на стр
</SelectItem>
))}
</SelectContent>
</Select>
);
}

View File

@ -1,11 +1,11 @@
'use no memo';
'use client';
import { useCallback } from 'react';
import { type Row, type Table } from '@tanstack/react-table';
import { type Cell, flexRender, type Row, type Table } from '@tanstack/react-table';
import clsx from 'clsx';
import { TableRow } from './table-row';
import { type IConditionalStyle } from './use-data-table';
import { SelectRow } from './select-row';
import { type IConditionalStyle } from '.';
interface TableBodyProps<TData> {
table: Table<TData>;
@ -30,43 +30,82 @@ export function TableBody<TData>({
onRowClicked,
onRowDoubleClicked
}: TableBodyProps<TData>) {
const getRowStyles = useCallback(
(row: Row<TData>) =>
conditionalRowStyles
?.filter(item => !!item.style && item.when(row.original))
?.reduce((prev, item) => ({ ...prev, ...item.style }), {}),
[conditionalRowStyles]
const handleRowClicked = useCallback(
(target: Row<TData>, event: React.MouseEvent<Element>) => {
onRowClicked?.(target.original, event);
if (table.options.enableRowSelection && target.getCanSelect()) {
if (event.shiftKey && !!lastSelected && lastSelected !== target.id) {
const { rows, rowsById } = table.getRowModel();
const lastIndex = rowsById[lastSelected].index;
const currentIndex = target.index;
const toggleRows = rows.slice(
lastIndex > currentIndex ? currentIndex : lastIndex + 1,
lastIndex > currentIndex ? lastIndex : currentIndex + 1
);
const newSelection: Record<string, boolean> = {};
toggleRows.forEach(row => {
newSelection[row.id] = !target.getIsSelected();
});
table.setRowSelection(prev => ({ ...prev, ...newSelection }));
onChangeLastSelected(null);
} else {
onChangeLastSelected(target.id);
target.toggleSelected(!target.getIsSelected());
}
}
},
[table, lastSelected, onChangeLastSelected, onRowClicked]
);
const getRowClasses = useCallback(
const getRowStyles = useCallback(
(row: Row<TData>) => {
return conditionalRowStyles
?.filter(item => !!item.className && item.when(row.original))
?.reduce((prev, item) => {
prev.push(item.className!);
return prev;
}, [] as string[]);
return {
...conditionalRowStyles!
.filter(item => item.when(row.original))
.reduce((prev, item) => ({ ...prev, ...item.style }), {})
};
},
[conditionalRowStyles]
);
return (
<tbody>
{table.getRowModel().rows.map((row: Row<TData>) => (
<TableRow
{table.getRowModel().rows.map((row: Row<TData>, index) => (
<tr
key={row.id}
table={table}
row={row}
className={getRowClasses(row)?.join(' ')}
style={conditionalRowStyles ? { ...getRowStyles(row) } : undefined}
noHeader={noHeader}
dense={dense}
lastSelected={lastSelected}
onChangeLastSelected={onChangeLastSelected}
onRowClicked={onRowClicked}
onRowDoubleClicked={onRowDoubleClicked}
/>
className={clsx(
'cc-scroll-row',
'cc-hover cc-animate-background duration-(--duration-fade)',
!noHeader && 'scroll-mt-[calc(2px+2rem)]',
table.options.enableRowSelection && row.getIsSelected()
? 'cc-selected'
: 'odd:bg-secondary even:bg-background'
)}
style={{ ...(conditionalRowStyles ? getRowStyles(row) : []) }}
onClick={event => handleRowClicked(row, event)}
onDoubleClick={event => onRowDoubleClicked?.(row.original, event)}
>
{table.options.enableRowSelection ? (
<td key={`select-${row.id}`} className='pl-2 border-y'>
<SelectRow row={row} onChangeLastSelected={onChangeLastSelected} />
</td>
) : null}
{row.getVisibleCells().map((cell: Cell<TData, unknown>) => (
<td
key={cell.id}
className={clsx(
'px-2 align-middle border-y',
dense ? 'py-1' : 'py-2',
onRowClicked || onRowDoubleClicked ? 'cursor-pointer' : 'cursor-auto'
)}
style={{
width: noHeader && index === 0 ? `calc(var(--col-${cell.column.id}-size) * 1px)` : undefined
}}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
);

View File

@ -1,5 +1,4 @@
'use no memo';
'use client';
import { flexRender, type Header, type HeaderGroup, type Table } from '@tanstack/react-table';
import clsx from 'clsx';

View File

@ -1,108 +0,0 @@
'use no memo';
import { useCallback } from 'react';
import { type Cell, flexRender, type Row, type Table } from '@tanstack/react-table';
import clsx from 'clsx';
import { cn } from '../utils';
import { SelectRow } from './select-row';
interface TableRowProps<TData> {
table: Table<TData>;
row: Row<TData>;
className?: string;
style?: React.CSSProperties;
noHeader?: boolean;
dense?: boolean;
lastSelected: string | null;
onChangeLastSelected: (newValue: string | null) => void;
onRowClicked?: (rowData: TData, event: React.MouseEvent<Element>) => void;
onRowDoubleClicked?: (rowData: TData, event: React.MouseEvent<Element>) => void;
}
export function TableRow<TData>({
table,
row,
className,
style,
noHeader,
dense,
lastSelected,
onChangeLastSelected,
onRowClicked,
onRowDoubleClicked
}: TableRowProps<TData>) {
const hasBG = className?.includes('bg-') ?? false;
const handleRowClicked = useCallback(
(target: Row<TData>, event: React.MouseEvent<Element>) => {
onRowClicked?.(target.original, event);
if (table.options.enableRowSelection && target.getCanSelect()) {
if (event.shiftKey && !!lastSelected && lastSelected !== target.id) {
const { rows, rowsById } = table.getRowModel();
const lastIndex = rowsById[lastSelected].index;
const currentIndex = target.index;
const toggleRows = rows.slice(
lastIndex > currentIndex ? currentIndex : lastIndex + 1,
lastIndex > currentIndex ? lastIndex : currentIndex + 1
);
const newSelection: Record<string, boolean> = {};
toggleRows.forEach(row => {
newSelection[row.id] = !target.getIsSelected();
});
table.setRowSelection(prev => ({ ...prev, ...newSelection }));
onChangeLastSelected(null);
} else {
onChangeLastSelected(target.id);
target.toggleSelected(!target.getIsSelected());
}
}
},
[table, lastSelected, onChangeLastSelected, onRowClicked]
);
return (
<tr
className={cn(
'cc-scroll-row',
'cc-hover cc-animate-background duration-fade',
!noHeader && 'scroll-mt-[calc(2px+2rem)]',
table.options.enableRowSelection && row.getIsSelected()
? 'cc-selected'
: !hasBG
? 'odd:bg-secondary even:bg-background'
: '',
className
)}
style={style}
onClick={event => handleRowClicked(row, event)}
onDoubleClick={event => onRowDoubleClicked?.(row.original, event)}
>
{table.options.enableRowSelection ? (
<td key={`select-${row.id}`} className='pl-2 border-y'>
<SelectRow row={row} onChangeLastSelected={onChangeLastSelected} />
</td>
) : null}
{row.getVisibleCells().map((cell: Cell<TData, unknown>) => (
<td
key={cell.id}
className={clsx(
'px-2 align-middle border-y',
dense ? 'py-1' : 'py-2',
onRowClicked || onRowDoubleClicked ? 'cursor-pointer' : 'cursor-auto'
)}
style={{
width: noHeader && row.index === 0 ? `calc(var(--col-${cell.column.id}-size) * 1px)` : undefined
}}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
);
}

View File

@ -21,10 +21,7 @@ export interface IConditionalStyle<TData> {
when: (rowData: TData) => boolean;
/** Style to apply. */
style?: React.CSSProperties;
/** Classname to apply. */
className?: string;
style: React.CSSProperties;
}
interface UseDataTableProps<TData extends RowData>

View File

@ -1,100 +0,0 @@
'use client';
import { type ReactNode, useState } from 'react';
import { Background, ReactFlow, type ReactFlowProps } from 'reactflow';
export { useReactFlow, useStoreApi } from 'reactflow';
import { cn } from '../utils';
type DiagramFlowProps = {
children?: ReactNode;
height?: number | string;
showGrid?: boolean;
gridSize?: number;
onKeyDown?: (event: React.KeyboardEvent<HTMLDivElement>) => void;
onKeyUp?: (event: React.KeyboardEvent<HTMLDivElement>) => void;
} & ReactFlowProps;
export function DiagramFlow({
children,
className,
style,
height,
showGrid = false,
gridSize = 20,
onKeyDown,
onKeyUp,
nodesDraggable = true,
nodesFocusable,
edgesFocusable,
onNodesChange,
onEdgesChange,
onNodeClick,
onNodeDoubleClick,
onNodeContextMenu,
onNodeMouseEnter,
onNodeMouseLeave,
onNodeDragStart,
onNodeDrag,
onNodeDragStop,
onContextMenu,
...restProps
}: DiagramFlowProps) {
const [spaceMode, setSpaceMode] = useState(false);
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
if (event.code === 'Space') {
event.preventDefault();
event.stopPropagation();
setSpaceMode(true);
}
onKeyDown?.(event);
}
function handleKeyUp(event: React.KeyboardEvent<HTMLDivElement>) {
if (event.code === 'Space') {
setSpaceMode(false);
}
onKeyUp?.(event);
}
function handleContextMenu(event: React.MouseEvent<HTMLDivElement>) {
event.preventDefault();
onContextMenu?.(event);
}
return (
<div
tabIndex={-1}
className={cn('relative cc-mask-sides max-w-480 w-[100dvw]', spaceMode && 'space-mode', className)}
style={{ ...style, height: height }}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
>
<ReactFlow
{...restProps}
onNodesChange={spaceMode ? undefined : onNodesChange}
onEdgesChange={spaceMode ? undefined : onEdgesChange}
onNodeClick={spaceMode ? undefined : onNodeClick}
onNodeDoubleClick={spaceMode ? undefined : onNodeDoubleClick}
nodesDraggable={!spaceMode && nodesDraggable}
nodesFocusable={!spaceMode && nodesFocusable}
edgesFocusable={!spaceMode && edgesFocusable}
onNodeDragStart={spaceMode ? undefined : onNodeDragStart}
onNodeDrag={spaceMode ? undefined : onNodeDrag}
onNodeDragStop={spaceMode ? undefined : onNodeDragStop}
onNodeContextMenu={spaceMode ? undefined : onNodeContextMenu}
onNodeMouseEnter={spaceMode ? undefined : onNodeMouseEnter}
onNodeMouseLeave={spaceMode ? undefined : onNodeMouseLeave}
onContextMenu={handleContextMenu}
>
{showGrid ? <Background gap={gridSize} /> : null}
{children}
</ReactFlow>
</div>
);
}

View File

@ -10,13 +10,12 @@ export { BiCheck as IconAccept } from 'react-icons/bi';
export { BiX as IconRemove } from 'react-icons/bi';
export { BiTrash as IconDestroy } from 'react-icons/bi';
export { BiReset as IconReset } from 'react-icons/bi';
export { TbArrowsDiagonal2 as IconResize } from 'react-icons/tb';
export { LiaEdit as IconEdit } from 'react-icons/lia';
export { FiEdit as IconEdit2 } from 'react-icons/fi';
export { BiSearchAlt2 as IconSearch } from 'react-icons/bi';
export { BiDownload as IconDownload } from 'react-icons/bi';
export { BiUpload as IconUpload } from 'react-icons/bi';
export { LuSettings as IconSettings } from 'react-icons/lu';
export { BiCog as IconSettings } from 'react-icons/bi';
export { TbEye as IconShow } from 'react-icons/tb';
export { TbEyeX as IconHide } from 'react-icons/tb';
export { BiShareAlt as IconShare } from 'react-icons/bi';
@ -69,12 +68,9 @@ export { LuGlasses as IconReader } from 'react-icons/lu';
// ===== Domain entities =======
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';
export { BiScreenshot as IconConceptBlock } from 'react-icons/bi';
export { TbHexagon as IconRSForm } from 'react-icons/tb';
export { TbAssembly as IconRSFormOwned } from 'react-icons/tb';
export { TbBallFootball as IconRSFormImported } from 'react-icons/tb';
@ -105,7 +101,7 @@ export { LuSubscript as IconAlias } from 'react-icons/lu';
export { TbMathFunction as IconFormula } from 'react-icons/tb';
export { BiFontFamily as IconText } from 'react-icons/bi';
export { BiFont as IconTextOff } from 'react-icons/bi';
export { TbCircleLetterM as IconTypeGraph } from 'react-icons/tb';
export { TbCar4Wd as IconTypeGraph } from 'react-icons/tb';
export { RiTreeLine as IconTree } from 'react-icons/ri';
export { FaRegKeyboard as IconControls } from 'react-icons/fa6';
export { RiLockLine as IconImmutable } from 'react-icons/ri';
@ -141,7 +137,6 @@ export { GrConnect as IconConnect } from 'react-icons/gr';
export { BiPlayCircle as IconExecute } from 'react-icons/bi';
// ======== Graph UI =======
export { LuLayoutDashboard as IconFixLayout } from 'react-icons/lu';
export { BiCollapse as IconGraphCollapse } from 'react-icons/bi';
export { BiExpand as IconGraphExpand } from 'react-icons/bi';
export { LuMaximize as IconGraphMaximize } from 'react-icons/lu';
@ -205,7 +200,7 @@ export function IconLogin(props: IconProps) {
export function CheckboxChecked() {
return (
<svg className='w-4 h-4 p-0.75 -ml-0.25 -mt-0.25' viewBox='0 0 512 512' fill='#ffffff'>
<svg className='w-4 h-4 p-0.75 -ml-0.25' viewBox='0 0 512 512' fill='#ffffff'>
<path d='M470.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 338.7l233.4-233.3c12.5-12.5 32.8-12.5 45.3 0z' />
</svg>
);
@ -213,7 +208,7 @@ export function CheckboxChecked() {
export function CheckboxNull() {
return (
<svg className='w-4 h-4 px-0.25 -ml-0.25 -mt-0.25' viewBox='0 0 16 16' fill='#ffffff'>
<svg className='w-4 h-4 px-0.25' viewBox='0 0 16 16' fill='#ffffff'>
<path d='M2 7.75A.75.75 0 012.75 7h10a.75.75 0 010 1.5h-10A.75.75 0 012 7.75z' />
</svg>
);

View File

@ -78,7 +78,8 @@ export function InfoError({ error }: InfoErrorProps) {
return (
<div
className={clsx(
'min-w-100', //
'cc-fade-in',
'min-w-100',
'px-3 py-2 flex flex-col',
'text-destructive text-sm font-semibold',
'select-text'

View File

@ -14,13 +14,10 @@ export interface CheckboxProps extends Omit<Button, 'value' | 'onClick' | 'onCha
disabled?: boolean;
/** Current value - `true` or `false`. */
value: boolean;
value?: boolean;
/** Callback to set the `value`. */
onChange?: (newValue: boolean) => void;
/** Custom icon to display next instead of checkbox. */
customIcon?: (checked?: boolean) => React.ReactNode;
}
/**
@ -34,7 +31,6 @@ export function Checkbox({
hideTitle,
className,
value,
customIcon,
onChange,
...restProps
}: CheckboxProps) {
@ -67,19 +63,15 @@ export function Checkbox({
disabled={disabled}
{...restProps}
>
{customIcon ? (
customIcon(value)
) : (
<div
className={clsx(
'w-4 h-4', //
'border rounded-sm',
value === false ? 'bg-background text-foreground' : 'bg-primary text-primary-foreground'
)}
>
{value ? <CheckboxChecked /> : null}
</div>
)}
<div
className={clsx(
'w-4 h-4', //
'border rounded-sm',
value === false ? 'bg-background text-foreground' : 'bg-primary text-primary-foreground'
)}
>
{value ? <CheckboxChecked /> : null}
</div>
{label ? <span className={clsx('text-start text-sm whitespace-nowrap select-text', cursor)}>{label}</span> : null}
</button>
);

View File

@ -1,6 +1,6 @@
'use client';
import { useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { ChevronDownIcon } from 'lucide-react';
import { IconRemove } from '../icons';
@ -11,7 +11,7 @@ import { cn } from '../utils';
interface ComboBoxProps<Option> extends Styling {
id?: string;
items?: readonly Option[];
items?: Option[];
value: Option | null;
onChange: (newValue: Option | null) => void;
@ -49,12 +49,11 @@ export function ComboBox<Option>({
const [popoverWidth, setPopoverWidth] = useState<number | undefined>(undefined);
const triggerRef = useRef<HTMLButtonElement>(null);
function handleOpenChange(isOpen: boolean) {
setOpen(isOpen);
if (isOpen && triggerRef.current) {
useEffect(() => {
if (triggerRef.current) {
setPopoverWidth(triggerRef.current.offsetWidth);
}
}
}, [open]);
function handleChangeValue(newValue: Option | null) {
onChange(newValue);
@ -67,7 +66,7 @@ export function ComboBox<Option>({
}
return (
<Popover open={open} onOpenChange={handleOpenChange}>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
id={id}

View File

@ -1,6 +1,6 @@
'use client';
import { useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { ChevronDownIcon } from 'lucide-react';
import { IconRemove } from '../icons';
@ -43,12 +43,11 @@ export function ComboMulti<Option>({
const [popoverWidth, setPopoverWidth] = useState<number | undefined>(undefined);
const triggerRef = useRef<HTMLButtonElement>(null);
function handleOpenChange(isOpen: boolean) {
setOpen(isOpen);
if (isOpen && triggerRef.current) {
useEffect(() => {
if (triggerRef.current) {
setPopoverWidth(triggerRef.current.offsetWidth);
}
}
}, [open]);
function handleAddValue(newValue: Option) {
if (value.includes(newValue)) {
@ -71,7 +70,7 @@ export function ComboMulti<Option>({
}
return (
<Popover open={open} onOpenChange={handleOpenChange}>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
id={id}

View File

@ -47,7 +47,7 @@ export function SearchBar({
id={id}
type='search'
className={clsx(
'min-w-0 py-2 w-full pr-2',
'min-w-0 py-2',
'leading-tight truncate hover:text-clip',
'bg-transparent',
!noIcon && 'pl-8',

View File

@ -44,12 +44,11 @@ export function SelectTree<ItemType>({
...restProps
}: SelectTreeProps<ItemType>) {
const foldable = new Set(items.filter(item => getParent(item) !== item).map(item => getParent(item)));
const defaultFolded = items.filter(item => getParent(value) !== item && getParent(getParent(value)) !== item);
const [folded, setFolded] = useState<ItemType[]>(defaultFolded);
const [folded, setFolded] = useState<ItemType[]>(items);
useEffect(() => {
setFolded(defaultFolded);
}, [defaultFolded]);
setFolded(items.filter(item => getParent(value) !== item && getParent(getParent(value)) !== item));
}, [value, getParent, items]);
function onFoldItem(target: ItemType) {
setFolded(prev =>

View File

@ -1,5 +1,7 @@
'use client';
import { APP_COLORS } from '@/styling/colors';
interface LoaderProps {
/** Scale of the loader from 1 to 10. */
scale?: number;
@ -55,8 +57,8 @@ const animatePulse = (startBig: boolean, duration: string) => {
export function Loader({ scale = 5, circular }: LoaderProps) {
if (circular) {
return (
<div className='flex justify-center text-primary' aria-busy='true' role='progressbar'>
<svg height={`${scale * 20}`} width={`${scale * 20}`} viewBox='0 0 100 100' fill='currentColor'>
<div className='flex justify-center' aria-label='three-circles-loading' aria-busy='true' role='progressbar'>
<svg height={`${scale * 20}`} width={`${scale * 20}`} viewBox='0 0 100 100' fill={APP_COLORS.bgPrimary}>
<path d='M31.6,3.5C5.9,13.6-6.6,42.7,3.5,68.4c10.1,25.7,39.2,38.3,64.9,28.1l-3.1-7.9c-21.3,8.4-45.4-2-53.8-23.3 c-8.4-21.3,2-45.4,23.3-53.8L31.6,3.5z'>
{animateRotation('2.25s')}
</path>
@ -71,8 +73,8 @@ export function Loader({ scale = 5, circular }: LoaderProps) {
);
} else {
return (
<div className='flex justify-center text-primary' aria-busy='true' role='progressbar'>
<svg height={`${scale * 20}`} width={`${scale * 20}`} viewBox='0 0 120 30' fill='currentColor'>
<div className='flex justify-center' aria-busy='true' role='progressbar'>
<svg height={`${scale * 20}`} width={`${scale * 20}`} viewBox='0 0 120 30' fill={APP_COLORS.bgPrimary}>
<circle cx='15' cy='15' r='16'>
{animatePulse(true, '0.8s')}
</circle>

View File

@ -20,7 +20,6 @@ export function TabLabel({
titleHtml,
hideTitle,
className,
disabled,
role = 'tab',
...otherProps
}: TabLabelProps) {
@ -29,12 +28,10 @@ export function TabLabel({
className={clsx(
'min-w-20 h-full',
'px-2 py-1 flex justify-center',
'cc-animate-color duration-select',
'cc-hover cc-animate-color duration-150',
'text-sm whitespace-nowrap font-controls',
'select-none',
'select-none hover:cursor-pointer',
'outline-hidden',
!disabled && 'hover:cursor-pointer cc-hover',
disabled && 'text-muted-foreground',
className
)}
tabIndex='-1'
@ -43,7 +40,6 @@ export function TabLabel({
data-tooltip-content={title}
data-tooltip-hidden={hideTitle}
role={role}
disabled={disabled}
{...otherProps}
>
{label}

View File

@ -61,4 +61,4 @@ export const authApi = {
endpoint: '/users/api/password-reset/confirm',
request: { data: data }
})
} as const;
};

View File

@ -15,7 +15,7 @@ export function ExpectedAnonymous() {
}
return (
<div className='flex flex-col items-center gap-3 py-6'>
<div className='cc-fade-in flex flex-col items-center gap-3 py-6'>
<p className='font-semibold'>{`Вы вошли в систему как ${user.username}`}</p>
<div className='flex gap-3'>
<TextURL text='Новая схема' href='/library/create' />

View File

@ -55,7 +55,7 @@ export function LoginPage() {
}
return (
<form
className='cc-column w-96 mx-auto pt-12 pb-6 px-6'
className='cc-column cc-fade-in w-96 mx-auto pt-12 pb-6 px-6'
onSubmit={event => void handleSubmit(onSubmit)(event)}
onChange={resetErrors}
>

View File

@ -1,6 +1,6 @@
'use client';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { urls, useConceptNavigation } from '@/app';
@ -13,26 +13,13 @@ import { useQueryStrings } from '@/hooks/use-query-strings';
import { useResetPassword } from '../backend/use-reset-password';
function useTokenValidation(token: string, isPending: boolean) {
const { validateToken } = useResetPassword();
const [isTokenValidating, setIsTokenValidating] = useState(false);
const validate = async () => {
if (!isTokenValidating && !isPending) {
await validateToken({ token });
setIsTokenValidating(true);
}
};
return { isTokenValidating, validate };
}
export function Component() {
const router = useConceptNavigation();
const token = useQueryStrings().get('token') ?? '';
const { resetPassword, isPending, error: serverError } = useResetPassword();
const { isTokenValidating, validate } = useTokenValidation(token, isPending);
const { validateToken, resetPassword, isPending, error: serverError } = useResetPassword();
const [isTokenValidating, setIsTokenValidating] = useState(false);
const [newPassword, setNewPassword] = useState('');
const [newPasswordRepeat, setNewPasswordRepeat] = useState('');
@ -51,9 +38,12 @@ export function Component() {
}
}
if (!isTokenValidating && !isPending) {
void validate();
}
useEffect(() => {
if (!isTokenValidating && !isPending) {
void validateToken({ token: token });
setIsTokenValidating(true);
}
}, [token, validateToken, isTokenValidating, isPending]);
if (isPending) {
return <Loader />;

View File

@ -32,7 +32,7 @@ export function Component() {
);
} else {
return (
<form className='cc-column w-96 mx-auto px-6 mt-3' onSubmit={handleSubmit} onChange={clearServerError}>
<form className='cc-fade-in cc-column w-96 mx-auto px-6 mt-3' onSubmit={handleSubmit} onChange={clearServerError}>
<TextInput
id='email'
autoComplete='email'

View File

@ -51,7 +51,7 @@ export function BadgeHelp({ topic, padding = 'p-1', className, contentClass, sty
{...restProps}
>
<Suspense fallback={<Loader />}>
<div className='absolute right-1 text-sm top-2 bg-' onClick={event => event.stopPropagation()}>
<div className='absolute right-1 text-sm top-2 bg-input' onClick={event => event.stopPropagation()}>
<TextURL text='Справка...' href={`/manuals?topic=${topic}`} />
</div>
<TopicPage topic={topic} />

View File

@ -3,15 +3,12 @@ import {
IconAnimation,
IconAnimationOff,
IconChild,
IconConceptBlock,
IconConnect,
IconConsolidation,
IconCoordinates,
IconDestroy,
IconEdit2,
IconExecute,
IconFitImage,
IconFixLayout,
IconGrid,
IconLineStraight,
IconLineWave,
@ -19,8 +16,7 @@ import {
IconNewRSForm,
IconReset,
IconRSForm,
IconSave,
IconSettings
IconSave
} from '@/components/icons';
import { LinkTopic } from '../../components/link-topic';
@ -33,19 +29,9 @@ export function HelpOssGraph() {
<div className='flex flex-col sm:flex-row'>
<div className='sm:w-56'>
<h1>Настройка графа</h1>
<li>
<IconReset className='inline-icon' /> Сбросить изменения
</li>
<li>
<IconFitImage className='inline-icon' /> Вписать в экран
</li>
<li>
<IconFixLayout className='inline-icon' /> Исправить расположения
</li>
<li>
<IconSettings className='inline-icon' /> Диалог настроек
</li>
<li>
<IconGrid className='inline-icon' /> Отображение сетки
</li>
@ -57,9 +43,6 @@ export function HelpOssGraph() {
<IconAnimation className='inline-icon' />
<IconAnimationOff className='inline-icon' /> Анимация
</li>
<li>
<IconCoordinates className='inline-icon' /> Отображение координат
</li>
<li>черта сверху - Загрузка</li>
<li>
черта слева - КС <LinkTopic text='внешняя' topic={HelpTopic.CC_OSS} />
@ -80,14 +63,11 @@ export function HelpOssGraph() {
<kbd>Двойной клик</kbd> переход к связанной <LinkTopic text='КС' topic={HelpTopic.CC_SYSTEM} />
</li>
<li>
<IconConceptBlock className='inline-icon icon-green' /> Новый блок
<IconEdit2 className='inline-icon' /> Редактирование операции
</li>
<li>
<IconNewItem className='inline-icon icon-green' /> Новая операция
</li>
<li>
<IconEdit2 className='inline-icon' /> Редактирование узла
</li>
<li>
<IconDestroy className='inline-icon icon-red' /> <kbd>Delete</kbd> удалить выбранные
</li>
@ -99,15 +79,12 @@ export function HelpOssGraph() {
<div className='flex flex-col-reverse mb-3 sm:flex-row'>
<div className='sm:w-56'>
<h1>Общие</h1>
<li>
<IconReset className='inline-icon' /> Сбросить изменения
</li>
<li>
<IconSave className='inline-icon' /> Сохранить положения
</li>
<li>
<kbd>Space</kbd> перемещение экрана
</li>
<li>
<kbd>Shift</kbd> перемещение выделенных элементов в границах родителя
</li>
</div>
<Divider vertical margins='mx-3' className='hidden sm:block' />

View File

@ -71,12 +71,12 @@ export function HelpRSEditor() {
<span className='bg-selected'>текущая конституента</span>
</li>
<li>
<span className='bg-accent-green50'>
<span className='bg-(--acc-bg-green50)'>
<LinkTopic text='основа' topic={HelpTopic.CC_RELATIONS} /> текущей
</span>
</li>
<li>
<span className='bg-accent-orange50'>
<span className='bg-(--acc-bg-orange50)'>
<LinkTopic text='порожденные' topic={HelpTopic.CC_RELATIONS} /> текущей
</span>
</li>

View File

@ -10,9 +10,7 @@ import {
IconEditor,
IconMenu,
IconOwner,
IconQR,
IconReader,
IconRobot,
IconShare,
IconUpload
} from '@/components/icons';
@ -53,12 +51,6 @@ export function HelpRSMenu() {
<li>
<IconShare className='inline-icon' /> Поделиться скопировать ссылку на схему
</li>
<li>
<IconQR className='inline-icon' /> Отобразить QR-код схемы
</li>
<li>
<IconRobot className='inline-icon' /> Генерировать запрос для LLM
</li>
<li>
<IconClone className='inline-icon icon-green' /> Клонировать создать копию схемы
</li>

View File

@ -19,13 +19,13 @@ export function HelpTypeGraph() {
<h2>Виды узлов</h2>
<li>
<span className='bg-secondary'>ступень-основание</span>
<span className='bg-prim-200'>ступень-основание</span>
</li>
<li>
<span className='bg-accent-teal'>ступень-булеан</span>
<span className='bg-(--acc-bg-teal)'>ступень-булеан</span>
</li>
<li>
<span className='bg-accent-orange'>ступень декартова произведения</span>
<span className='bg-(--acc-bg-orange)'>ступень декартова произведения</span>
</li>
</div>
);

View File

@ -57,7 +57,7 @@ export function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownPro
getLabel={labelHelpTopic}
getDescription={describeHelpTopic}
className={clsx(
'cc-topic-dropdown border-r border-t rounded-none cc-scroll-y bg-secondary',
'cc-topic-dropdown border-r border-t rounded-none cc-scroll-y bg-prim-200',
menu.isOpen && 'open'
)}
style={{ maxHeight: treeHeight }}

View File

@ -29,7 +29,7 @@ export function TopicsStatic({ activeTopic, onChangeTopic }: TopicsStaticProps)
'cc-scroll-y',
'self-start',
'border-x border-t rounded-none',
'text-xs sm:text-sm bg-secondary',
'text-xs sm:text-sm bg-prim-200',
'select-none'
)}
style={{ maxHeight: topicsHeight }}

View File

@ -13,7 +13,11 @@ interface ViewTopicProps {
export function ViewTopic({ topic }: ViewTopicProps) {
const mainHeight = useMainHeight();
return (
<div key={topic} className='py-2 px-6 mx-auto sm:mx-0 lg:px-12 overflow-y-auto' style={{ maxHeight: mainHeight }}>
<div
key={topic}
className='cc-fade-in py-2 px-6 mx-auto sm:mx-0 lg:px-12 overflow-y-auto'
style={{ maxHeight: mainHeight }}
>
<TopicPage topic={topic} />
</div>
);

View File

@ -16,7 +16,7 @@ export function Component() {
}, [hideFooter]);
return (
<div className='flex justify-center overflow-hidden' style={{ maxHeight: panelHeight }}>
<div className='cc-fade-in flex justify-center overflow-hidden' style={{ maxHeight: panelHeight }}>
<TransformWrapper>
<TransformComponent>
<img alt='Схема базы данных' src={resources.db_schema} className='w-fit h-fit' />

View File

@ -15,12 +15,12 @@ import {
type AccessPolicy,
type ICloneLibraryItemDTO,
type ICreateLibraryItemDTO,
type ICreateVersionDTO,
type ILibraryItem,
type IRenameLocationDTO,
type IUpdateLibraryItemDTO,
type IUpdateVersionDTO,
type IVersionCreateDTO,
type IVersionExInfo,
type IVersionUpdateDTO,
schemaLibraryItem,
schemaLibraryItemArray,
schemaVersionExInfo
@ -136,8 +136,8 @@ export const libraryApi = {
}
}),
createVersion: ({ itemID, data }: { itemID: number; data: ICreateVersionDTO }) =>
axiosPost<ICreateVersionDTO, IVersionCreatedResponse>({
versionCreate: ({ itemID, data }: { itemID: number; data: IVersionCreateDTO }) =>
axiosPost<IVersionCreateDTO, IVersionCreatedResponse>({
schema: schemaVersionCreatedResponse,
endpoint: `/api/library/${itemID}/create-version`,
request: {
@ -145,7 +145,7 @@ export const libraryApi = {
successMessage: infoMsg.newVersion(data.version)
}
}),
restoreVersion: ({ versionID }: { versionID: number }) =>
versionRestore: ({ versionID }: { versionID: number }) =>
axiosPatch<undefined, IRSFormDTO>({
schema: schemaRSForm,
endpoint: `/api/versions/${versionID}/restore`,
@ -153,8 +153,8 @@ export const libraryApi = {
successMessage: infoMsg.versionRestored
}
}),
updateVersion: (data: { itemID: number; version: IUpdateVersionDTO }) =>
axiosPatch<IUpdateVersionDTO, IVersionExInfo>({
versionUpdate: (data: { itemID: number; version: IVersionUpdateDTO }) =>
axiosPatch<IVersionUpdateDTO, IVersionExInfo>({
schema: schemaVersionExInfo,
endpoint: `/api/versions/${data.version.id}`,
request: {
@ -162,11 +162,11 @@ export const libraryApi = {
successMessage: infoMsg.changesSaved
}
}),
deleteVersion: (data: { itemID: number; versionID: number }) =>
versionDelete: (data: { itemID: number; versionID: number }) =>
axiosDelete({
endpoint: `/api/versions/${data.versionID}`,
request: {
successMessage: infoMsg.versionDestroyed
}
})
} as const;
};

View File

@ -49,10 +49,10 @@ export type ICreateLibraryItemDTO = z.infer<typeof schemaCreateLibraryItem>;
export type IUpdateLibraryItemDTO = z.infer<typeof schemaUpdateLibraryItem>;
/** Create version metadata in persistent storage. */
export type ICreateVersionDTO = z.infer<typeof schemaCreateVersion>;
export type IVersionCreateDTO = z.infer<typeof schemaVersionCreate>;
/** Represents version data, intended to update version metadata in persistent storage. */
export type IUpdateVersionDTO = z.infer<typeof schemaUpdateVersion>;
export type IVersionUpdateDTO = z.infer<typeof schemaVersionUpdate>;
// ======= SCHEMAS =========
export const schemaLibraryItemType = z.enum(Object.values(LibraryItemType) as [LibraryItemType, ...LibraryItemType[]]);
@ -140,13 +140,13 @@ export const schemaVersionExInfo = schemaVersionInfo.extend({
item: z.number()
});
export const schemaUpdateVersion = z.strictObject({
export const schemaVersionUpdate = z.strictObject({
id: z.number(),
version: z.string().nonempty(errorMsg.requiredField),
description: z.string()
});
export const schemaCreateVersion = z.strictObject({
export const schemaVersionCreate = z.strictObject({
version: z.string(),
description: z.string(),
items: z.array(z.number())

View File

@ -7,11 +7,6 @@ import { usePreferencesStore } from '@/stores/preferences';
import { libraryApi } from './api';
export function useLibraryListKey() {
const adminMode = usePreferencesStore(state => state.adminMode);
return libraryApi.getLibraryQueryOptions({ isAdmin: adminMode }).queryKey;
}
export function useLibrarySuspense() {
const adminMode = usePreferencesStore(state => state.adminMode);
const { user } = useAuthSuspense();

View File

@ -4,15 +4,12 @@ import { type IOperationSchemaDTO } from '@/features/oss';
import { type IRSFormDTO } from '@/features/rsform';
import { KEYS } from '@/backend/configuration';
import { type RO } from '@/utils/meta';
import { libraryApi } from './api';
import { type AccessPolicy, type ILibraryItem } from './types';
import { useLibraryListKey } from './use-library';
export const useSetAccessPolicy = () => {
const client = useQueryClient();
const libraryKey = useLibraryListKey();
const mutation = useMutation({
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'set-location'],
mutationFn: libraryApi.setAccessPolicy,
@ -39,7 +36,7 @@ export const useSetAccessPolicy = () => {
client.setQueryData(rsKey, (prev: IRSFormDTO | undefined) =>
!prev ? undefined : { ...prev, access_policy: variables.policy }
);
client.setQueryData(libraryKey, (prev: RO<ILibraryItem[]> | undefined) =>
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
prev?.map(item => (item.id === variables.itemID ? { ...item, access_policy: variables.policy } : item))
);
},

View File

@ -4,15 +4,12 @@ import { type IOperationSchemaDTO } from '@/features/oss';
import { type IRSFormDTO } from '@/features/rsform';
import { KEYS } from '@/backend/configuration';
import { type RO } from '@/utils/meta';
import { libraryApi } from './api';
import { type ILibraryItem } from './types';
import { useLibraryListKey } from './use-library';
export const useSetLocation = () => {
const client = useQueryClient();
const libraryKey = useLibraryListKey();
const mutation = useMutation({
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'set-location'],
mutationFn: libraryApi.setLocation,
@ -39,7 +36,7 @@ export const useSetLocation = () => {
client.setQueryData(rsKey, (prev: IRSFormDTO | undefined) =>
!prev ? undefined : { ...prev, location: variables.location }
);
client.setQueryData(libraryKey, (prev: RO<ILibraryItem[]> | undefined) =>
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
prev?.map(item => (item.id === variables.itemID ? { ...item, location: variables.location } : item))
);
},

View File

@ -4,15 +4,12 @@ import { type IOperationSchemaDTO } from '@/features/oss';
import { type IRSFormDTO } from '@/features/rsform';
import { KEYS } from '@/backend/configuration';
import { type RO } from '@/utils/meta';
import { libraryApi } from './api';
import { type ILibraryItem } from './types';
import { useLibraryListKey } from './use-library';
export const useSetOwner = () => {
const client = useQueryClient();
const libraryKey = useLibraryListKey();
const mutation = useMutation({
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'set-owner'],
mutationFn: libraryApi.setOwner,
@ -39,7 +36,7 @@ export const useSetOwner = () => {
client.setQueryData(rsKey, (prev: IRSFormDTO | undefined) =>
!prev ? undefined : { ...prev, owner: variables.owner }
);
client.setQueryData(libraryKey, (prev: RO<ILibraryItem[]> | undefined) =>
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
prev?.map(item => (item.id === variables.itemID ? { ...item, owner: variables.owner } : item))
);
},

View File

@ -4,15 +4,12 @@ import { type IOperationSchemaDTO } from '@/features/oss';
import { type IRSFormDTO } from '@/features/rsform';
import { KEYS } from '@/backend/configuration';
import { type RO } from '@/utils/meta';
import { libraryApi } from './api';
import { type ILibraryItem, type IUpdateLibraryItemDTO, LibraryItemType } from './types';
import { useLibraryListKey } from './use-library';
export const useUpdateItem = () => {
const client = useQueryClient();
const libraryKey = useLibraryListKey();
const mutation = useMutation({
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'update-item'],
mutationFn: libraryApi.updateItem,
@ -21,7 +18,7 @@ export const useUpdateItem = () => {
data.item_type === LibraryItemType.RSFORM
? KEYS.composite.rsItem({ itemID: data.id })
: KEYS.composite.ossItem({ itemID: data.id });
client.setQueryData(libraryKey, (prev: RO<ILibraryItem[]> | undefined) =>
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
prev?.map(item => (item.id === data.id ? data : item))
);
client.setQueryData(itemKey, (prev: IRSFormDTO | IOperationSchemaDTO | undefined) =>

View File

@ -1,18 +1,15 @@
import { useQueryClient } from '@tanstack/react-query';
import { type RO } from '@/utils/meta';
import { libraryApi } from './api';
import { type ILibraryItem } from './types';
import { useLibraryListKey } from './use-library';
export function useUpdateTimestamp() {
const client = useQueryClient();
const libraryKey = useLibraryListKey();
return {
updateTimestamp: (target: number) =>
client.setQueryData(
libraryKey, //
(prev: RO<ILibraryItem[]> | undefined) =>
libraryApi.libraryListKey, //
(prev: ILibraryItem[] | undefined) =>
prev?.map(item => (item.id === target ? { ...item, time_update: Date() } : item))
)
};

View File

@ -3,15 +3,15 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { KEYS } from '@/backend/configuration';
import { libraryApi } from './api';
import { type ICreateVersionDTO } from './types';
import { type IVersionCreateDTO } from './types';
import { useUpdateTimestamp } from './use-update-timestamp';
export const useCreateVersion = () => {
export const useVersionCreate = () => {
const client = useQueryClient();
const { updateTimestamp } = useUpdateTimestamp();
const mutation = useMutation({
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'create-version'],
mutationFn: libraryApi.createVersion,
mutationFn: libraryApi.versionCreate,
onSuccess: data => {
client.setQueryData(KEYS.composite.rsItem({ itemID: data.schema.id }), data.schema);
updateTimestamp(data.schema.id);
@ -19,7 +19,7 @@ export const useCreateVersion = () => {
onError: () => client.invalidateQueries()
});
return {
createVersion: (data: { itemID: number; data: ICreateVersionDTO }) =>
versionCreate: (data: { itemID: number; data: IVersionCreateDTO }) =>
mutation.mutateAsync(data).then(response => response.version)
};
};

View File

@ -6,11 +6,11 @@ import { KEYS } from '@/backend/configuration';
import { libraryApi } from './api';
export const useDeleteVersion = () => {
export const useVersionDelete = () => {
const client = useQueryClient();
const mutation = useMutation({
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'delete-version'],
mutationFn: libraryApi.deleteVersion,
mutationFn: libraryApi.versionDelete,
onSuccess: (_, variables) => {
client.setQueryData(KEYS.composite.rsItem({ itemID: variables.itemID }), (prev: IRSFormDTO | undefined) =>
!prev
@ -24,6 +24,6 @@ export const useDeleteVersion = () => {
onError: () => client.invalidateQueries()
});
return {
deleteVersion: (data: { itemID: number; versionID: number }) => mutation.mutateAsync(data)
versionDelete: (data: { itemID: number; versionID: number }) => mutation.mutateAsync(data)
};
};

View File

@ -4,11 +4,11 @@ import { KEYS } from '@/backend/configuration';
import { libraryApi } from './api';
export const useRestoreVersion = () => {
export const useVersionRestore = () => {
const client = useQueryClient();
const mutation = useMutation({
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'restore-version'],
mutationFn: libraryApi.restoreVersion,
mutationFn: libraryApi.versionRestore,
onSuccess: data => {
client.setQueryData(KEYS.composite.rsItem({ itemID: data.id }), data);
return client.invalidateQueries({ queryKey: [libraryApi.baseKey] });
@ -16,6 +16,6 @@ export const useRestoreVersion = () => {
onError: () => client.invalidateQueries()
});
return {
restoreVersion: (data: { versionID: number }) => mutation.mutateAsync(data)
versionRestore: (data: { versionID: number }) => mutation.mutateAsync(data)
};
};

View File

@ -5,13 +5,13 @@ import { type IRSFormDTO } from '@/features/rsform';
import { KEYS } from '@/backend/configuration';
import { libraryApi } from './api';
import { type IUpdateVersionDTO } from './types';
import { type IVersionUpdateDTO } from './types';
export const useUpdateVersion = () => {
export const useVersionUpdate = () => {
const client = useQueryClient();
const mutation = useMutation({
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'update-version'],
mutationFn: libraryApi.updateVersion,
mutationFn: libraryApi.versionUpdate,
onSuccess: (data, variables) => {
client.setQueryData(KEYS.composite.rsItem({ itemID: variables.itemID }), (prev: IRSFormDTO | undefined) =>
!prev
@ -41,6 +41,6 @@ export const useUpdateVersion = () => {
onError: () => client.invalidateQueries()
});
return {
updateVersion: (data: { itemID: number; version: IUpdateVersionDTO }) => mutation.mutateAsync(data)
versionUpdate: (data: { itemID: number; version: IVersionUpdateDTO }) => mutation.mutateAsync(data)
};
};

View File

@ -8,6 +8,7 @@ import { IconClose, IconFolderTree } from '@/components/icons';
import { SearchBar } from '@/components/input';
import { type Styling } from '@/components/props';
import { cn } from '@/components/utils';
import { APP_COLORS } from '@/styling/colors';
import { prefixes } from '@/utils/constants';
import { type ILibraryItem, type LibraryItemType } from '../backend/types';
@ -90,7 +91,7 @@ export function PickSchema({
const conditionalRowStyles: IConditionalStyle<ILibraryItem>[] = [
{
when: (item: ILibraryItem) => item.id === value,
className: 'bg-selected'
style: { backgroundColor: APP_COLORS.bgSelected }
}
];

View File

@ -62,7 +62,7 @@ export function SelectLocation({ value, dense, prefix, onClick, className, style
!dense && 'h-7 sm:h-8',
'pr-3 py-1 flex items-center gap-2',
'cc-scroll-row',
'cc-hover cc-animate-color duration-fade',
'cc-hover cc-animate-color',
'cursor-pointer',
'leading-3 sm:leading-4',
activeNode === item && 'cc-selected'

View File

@ -8,8 +8,8 @@ import { ModalForm } from '@/components/modal';
import { useDialogsStore } from '@/stores/dialogs';
import { errorMsg } from '@/utils/labels';
import { type ICreateVersionDTO, type IVersionInfo, schemaCreateVersion } from '../backend/types';
import { useCreateVersion } from '../backend/use-create-version';
import { type IVersionCreateDTO, type IVersionInfo, schemaVersionCreate } from '../backend/types';
import { useVersionCreate } from '../backend/use-version-create';
import { nextVersion } from '../models/library-api';
export interface DlgCreateVersionProps {
@ -24,10 +24,10 @@ export function DlgCreateVersion() {
const { itemID, versions, selected, totalCount, onCreate } = useDialogsStore(
state => state.props as DlgCreateVersionProps
);
const { createVersion: versionCreate } = useCreateVersion();
const { versionCreate } = useVersionCreate();
const { register, handleSubmit, control } = useForm<ICreateVersionDTO>({
resolver: zodResolver(schemaCreateVersion),
const { register, handleSubmit, control } = useForm<IVersionCreateDTO>({
resolver: zodResolver(schemaVersionCreate),
defaultValues: {
version: versions.length > 0 ? nextVersion(versions[versions.length - 1].version) : '1.0.0',
description: '',
@ -37,7 +37,7 @@ export function DlgCreateVersion() {
const version = useWatch({ control, name: 'version' });
const canSubmit = !versions.find(ver => ver.version === version);
function onSubmit(data: ICreateVersionDTO) {
function onSubmit(data: IVersionCreateDTO) {
return versionCreate({ itemID, data }).then(onCreate);
}

View File

@ -14,10 +14,10 @@ import { ModalView } from '@/components/modal';
import { useDialogsStore } from '@/stores/dialogs';
import { errorMsg } from '@/utils/labels';
import { type IUpdateVersionDTO, schemaUpdateVersion } from '../../backend/types';
import { useDeleteVersion } from '../../backend/use-delete-version';
import { type IVersionUpdateDTO, schemaVersionUpdate } from '../../backend/types';
import { useMutatingLibrary } from '../../backend/use-mutating-library';
import { useUpdateVersion } from '../../backend/use-update-version';
import { useVersionDelete } from '../../backend/use-version-delete';
import { useVersionUpdate } from '../../backend/use-version-update';
import { TableVersions } from './table-versions';
@ -31,8 +31,8 @@ export function DlgEditVersions() {
const hideDialog = useDialogsStore(state => state.hideDialog);
const { schema } = useRSFormSuspense({ itemID });
const isProcessing = useMutatingLibrary();
const { deleteVersion: versionDelete } = useDeleteVersion();
const { updateVersion: versionUpdate } = useUpdateVersion();
const { versionDelete } = useVersionDelete();
const { versionUpdate } = useVersionUpdate();
const {
register,
@ -40,8 +40,8 @@ export function DlgEditVersions() {
control,
reset,
formState: { isDirty, errors: formErrors }
} = useForm<IUpdateVersionDTO>({
resolver: zodResolver(schemaUpdateVersion),
} = useForm<IVersionUpdateDTO>({
resolver: zodResolver(schemaVersionUpdate),
defaultValues: {
id: schema.versions[schema.versions.length - 1].id,
version: schema.versions[schema.versions.length - 1].version,
@ -77,7 +77,7 @@ export function DlgEditVersions() {
});
}
function onUpdate(data: IUpdateVersionDTO) {
function onUpdate(data: IVersionUpdateDTO) {
if (!isDirty || isProcessing || !isValid) {
return;
}

View File

@ -5,6 +5,7 @@ import { useIntl } from 'react-intl';
import { MiniButton } from '@/components/control';
import { createColumnHelper, DataTable, type IConditionalStyle } from '@/components/data-table';
import { IconRemove } from '@/components/icons';
import { APP_COLORS } from '@/styling/colors';
import { type IVersionInfo } from '../../backend/types';
@ -76,7 +77,9 @@ export function TableVersions({ processing, items, onDelete, selected, onSelect
const conditionalRowStyles: IConditionalStyle<IVersionInfo>[] = [
{
when: (version: IVersionInfo) => version.id === selected,
className: 'bg-selected'
style: {
backgroundColor: APP_COLORS.bgSelected
}
}
];

View File

@ -99,7 +99,7 @@ export function FormCreateItem() {
return (
<form
className='cc-column w-120 mx-auto px-6 py-3'
className='cc-fade-in cc-column w-120 mx-auto px-6 py-3'
onSubmit={event => void handleSubmit(onSubmit)(event)}
onChange={resetErrors}
>

View File

@ -54,7 +54,7 @@ export function LibraryPage() {
return (
<>
<ToolbarSearch className='top-0 h-9' total={libraryItems.length} filtered={filtered.length} />
<div className='relative flex'>
<div className='relative cc-fade-in flex'>
<MiniButton
title='Выгрузить в формате CSV'
className='absolute z-tooltip -top-8 right-6 hidden sm:block'

View File

@ -9,7 +9,7 @@ import { DataTable, type IConditionalStyle, type VisibilityState } from '@/compo
import { useWindowSize } from '@/hooks/use-window-size';
import { useFitHeight } from '@/stores/app-layout';
import { usePreferencesStore } from '@/stores/preferences';
import { type RO } from '@/utils/meta';
import { APP_COLORS } from '@/styling/colors';
import { type ILibraryItem, LibraryItemType } from '../../backend/types';
import { useLibrarySearchStore } from '../../stores/library-search';
@ -17,7 +17,7 @@ import { useLibrarySearchStore } from '../../stores/library-search';
import { useLibraryColumns } from './use-library-columns';
interface TableLibraryItemsProps {
items: RO<ILibraryItem[]>;
items: ILibraryItem[];
}
export function TableLibraryItems({ items }: TableLibraryItemsProps) {
@ -35,7 +35,9 @@ export function TableLibraryItems({ items }: TableLibraryItemsProps) {
const conditionalRowStyles: IConditionalStyle<ILibraryItem>[] = [
{
when: (item: ILibraryItem) => item.item_type === LibraryItemType.OSS,
className: 'text-accent-green-foreground'
style: {
color: APP_COLORS.fgGreen
}
}
];
const tableHeight = useFitHeight('2.25rem');
@ -56,7 +58,7 @@ export function TableLibraryItems({ items }: TableLibraryItemsProps) {
<DataTable
id='library_data'
columns={columns}
data={items as ILibraryItem[]}
data={items}
headPosition='0'
className={clsx('cc-scroll-y h-fit text-xs sm:text-sm border-b', folderMode && 'border-l')}
style={{ maxHeight: tableHeight }}

View File

@ -128,7 +128,7 @@ export function ToolbarSearch({ className, total, filtered }: ToolbarSearchProps
/>
</div>
<div className='flex h-full grow pr-4 sm:pr-12'>
<div className='flex h-full grow pr-4'>
<SearchBar
id='library_search'
placeholder='Поиск'

View File

@ -6,13 +6,12 @@ import { MiniButton } from '@/components/control';
import { createColumnHelper } from '@/components/data-table';
import { IconFolderTree } from '@/components/icons';
import { useWindowSize } from '@/hooks/use-window-size';
import { type RO } from '@/utils/meta';
import { type ILibraryItem } from '../../backend/types';
import { BadgeLocation } from '../../components/badge-location';
import { useLibrarySearchStore } from '../../stores/library-search';
const columnHelper = createColumnHelper<RO<ILibraryItem>>();
const columnHelper = createColumnHelper<ILibraryItem>();
export function useLibraryColumns() {
const { isSmall } = useWindowSize();

View File

@ -5,23 +5,17 @@ import { DELAYS, KEYS } from '@/backend/configuration';
import { infoMsg } from '@/utils/labels';
import {
type IBlockCreatedResponse,
type IConstituentaReference,
type ICreateBlockDTO,
type ICreateOperationDTO,
type IDeleteBlockDTO,
type IDeleteOperationDTO,
type ICstRelocateDTO,
type IInputCreatedResponse,
type IMoveItemsDTO,
type IInputUpdateDTO,
type IOperationCreatedResponse,
type IOperationCreateDTO,
type IOperationDeleteDTO,
type IOperationSchemaDTO,
type IOperationUpdateDTO,
type IOssLayout,
type IRelocateConstituentsDTO,
type ITargetOperation,
type IUpdateBlockDTO,
type IUpdateInputDTO,
type IUpdateOperationDTO,
schemaBlockCreatedResponse,
schemaConstituentaReference,
schemaInputCreatedResponse,
schemaOperationCreatedResponse,
@ -55,36 +49,8 @@ export const ossApi = {
}
}),
createBlock: ({ itemID, data }: { itemID: number; data: ICreateBlockDTO }) =>
axiosPost<ICreateBlockDTO, IBlockCreatedResponse>({
schema: schemaBlockCreatedResponse,
endpoint: `/api/oss/${itemID}/create-block`,
request: {
data: data,
successMessage: infoMsg.changesSaved
}
}),
updateBlock: ({ itemID, data }: { itemID: number; data: IUpdateBlockDTO }) =>
axiosPatch<IUpdateBlockDTO, IOperationSchemaDTO>({
schema: schemaOperationSchema,
endpoint: `/api/oss/${itemID}/update-block`,
request: {
data: data,
successMessage: infoMsg.changesSaved
}
}),
deleteBlock: ({ itemID, data }: { itemID: number; data: IDeleteBlockDTO }) =>
axiosPatch<IDeleteBlockDTO, IOperationSchemaDTO>({
schema: schemaOperationSchema,
endpoint: `/api/oss/${itemID}/delete-block`,
request: {
data: data,
successMessage: infoMsg.blockDestroyed
}
}),
createOperation: ({ itemID, data }: { itemID: number; data: ICreateOperationDTO }) =>
axiosPost<ICreateOperationDTO, IOperationCreatedResponse>({
operationCreate: ({ itemID, data }: { itemID: number; data: IOperationCreateDTO }) =>
axiosPost<IOperationCreateDTO, IOperationCreatedResponse>({
schema: schemaOperationCreatedResponse,
endpoint: `/api/oss/${itemID}/create-operation`,
request: {
@ -92,17 +58,8 @@ export const ossApi = {
successMessage: response => infoMsg.newOperation(response.new_operation.alias)
}
}),
updateOperation: ({ itemID, data }: { itemID: number; data: IUpdateOperationDTO }) =>
axiosPatch<IUpdateOperationDTO, IOperationSchemaDTO>({
schema: schemaOperationSchema,
endpoint: `/api/oss/${itemID}/update-operation`,
request: {
data: data,
successMessage: infoMsg.changesSaved
}
}),
deleteOperation: ({ itemID, data }: { itemID: number; data: IDeleteOperationDTO }) =>
axiosPatch<IDeleteOperationDTO, IOperationSchemaDTO>({
operationDelete: ({ itemID, data }: { itemID: number; data: IOperationDeleteDTO }) =>
axiosPatch<IOperationDeleteDTO, IOperationSchemaDTO>({
schema: schemaOperationSchema,
endpoint: `/api/oss/${itemID}/delete-operation`,
request: {
@ -110,8 +67,7 @@ export const ossApi = {
successMessage: infoMsg.operationDestroyed
}
}),
createInput: ({ itemID, data }: { itemID: number; data: ITargetOperation }) =>
inputCreate: ({ itemID, data }: { itemID: number; data: ITargetOperation }) =>
axiosPatch<ITargetOperation, IInputCreatedResponse>({
schema: schemaInputCreatedResponse,
endpoint: `/api/oss/${itemID}/create-input`,
@ -120,8 +76,8 @@ export const ossApi = {
successMessage: infoMsg.newLibraryItem
}
}),
updateInput: ({ itemID, data }: { itemID: number; data: IUpdateInputDTO }) =>
axiosPatch<IUpdateInputDTO, IOperationSchemaDTO>({
inputUpdate: ({ itemID, data }: { itemID: number; data: IInputUpdateDTO }) =>
axiosPatch<IInputUpdateDTO, IOperationSchemaDTO>({
schema: schemaOperationSchema,
endpoint: `/api/oss/${itemID}/set-input`,
request: {
@ -129,7 +85,16 @@ export const ossApi = {
successMessage: infoMsg.changesSaved
}
}),
executeOperation: ({ itemID, data }: { itemID: number; data: ITargetOperation }) =>
operationUpdate: ({ itemID, data }: { itemID: number; data: IOperationUpdateDTO }) =>
axiosPatch<IOperationUpdateDTO, IOperationSchemaDTO>({
schema: schemaOperationSchema,
endpoint: `/api/oss/${itemID}/update-operation`,
request: {
data: data,
successMessage: infoMsg.changesSaved
}
}),
operationExecute: ({ itemID, data }: { itemID: number; data: ITargetOperation }) =>
axiosPost<ITargetOperation, IOperationSchemaDTO>({
schema: schemaOperationSchema,
endpoint: `/api/oss/${itemID}/execute-operation`,
@ -139,18 +104,8 @@ export const ossApi = {
}
}),
moveItems: ({ itemID, data }: { itemID: number; data: IMoveItemsDTO }) =>
axiosPatch<IMoveItemsDTO, IOperationSchemaDTO>({
schema: schemaOperationSchema,
endpoint: `/api/oss/${itemID}/move-items`,
request: {
data: data,
successMessage: infoMsg.moveSuccess
}
}),
relocateConstituents: (data: IRelocateConstituentsDTO) =>
axiosPost<IRelocateConstituentsDTO, IOperationSchemaDTO>({
relocateConstituents: (data: ICstRelocateDTO) =>
axiosPost<ICstRelocateDTO, IOperationSchemaDTO>({
schema: schemaOperationSchema,
endpoint: `/api/oss/relocate-constituents`,
request: {
@ -164,4 +119,4 @@ export const ossApi = {
endpoint: '/api/oss/get-predecessor',
request: { data: { target: cstID } }
})
} as const;
};

View File

@ -5,25 +5,24 @@
import { type ILibraryItem } from '@/features/library';
import { Graph } from '@/models/graph';
import { type RO } from '@/utils/meta';
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 IOperation, type IOperationSchema, type IOperationSchemaStats } from '../models/oss';
import { type IOperationSchemaDTO, OperationType } from './types';
/** Loads data into an {@link IOperationSchema} based on {@link IOperationSchemaDTO}. */
/**
* Loads data into an {@link IOperationSchema} based on {@link IOperationSchemaDTO}.
*
*/
export class OssLoader {
private oss: IOperationSchema;
private graph: Graph = new Graph();
private hierarchy: Graph = new Graph();
private operationByID = new Map<number, IOperation>();
private blockByID = new Map<number, IBlock>();
private schemaIDs: number[] = [];
private items: RO<ILibraryItem[]>;
private items: ILibraryItem[];
constructor(input: RO<IOperationSchemaDTO>, items: RO<ILibraryItem[]>) {
this.oss = structuredClone(input) as IOperationSchema;
constructor(input: IOperationSchemaDTO, items: ILibraryItem[]) {
this.oss = input as unknown as IOperationSchema;
this.items = items;
}
@ -33,12 +32,9 @@ export class OssLoader {
this.createGraph();
this.extractSchemas();
this.inferOperationAttributes();
this.inferBlockAttributes();
result.operationByID = this.operationByID;
result.blockByID = this.blockByID;
result.graph = this.graph;
result.hierarchy = this.hierarchy;
result.schemas = this.schemaIDs;
result.stats = this.calculateStats();
return result;
@ -48,17 +44,6 @@ export class OssLoader {
this.oss.operations.forEach(operation => {
this.operationByID.set(operation.id, operation);
this.graph.addNode(operation.id);
this.hierarchy.addNode(operation.id);
if (operation.parent) {
this.hierarchy.addEdge(-operation.parent, operation.id);
}
});
this.oss.blocks.forEach(block => {
this.blockByID.set(block.id, block);
this.hierarchy.addNode(-block.id);
if (block.parent) {
this.hierarchy.addEdge(-block.parent, -block.id);
}
});
}
@ -86,16 +71,6 @@ export class OssLoader {
});
}
private inferBlockAttributes() {
this.oss.blocks.forEach(block => {
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 ?? BLOCK_NODE_MIN_WIDTH;
block.height = geometry?.height ?? BLOCK_NODE_MIN_HEIGHT;
});
}
private inferConsolidation(operationID: number): boolean {
const inputs = this.graph.expandInputs([operationID]);
if (inputs.length === 0) {
@ -110,14 +85,13 @@ export class OssLoader {
}
private calculateStats(): IOperationSchemaStats {
const operations = this.oss.operations;
const items = this.oss.operations;
return {
count_all: this.oss.operations.length + this.oss.blocks.length,
count_inputs: operations.filter(item => item.operation_type === OperationType.INPUT).length,
count_synthesis: operations.filter(item => item.operation_type === OperationType.SYNTHESIS).length,
count_operations: items.length,
count_inputs: items.filter(item => item.operation_type === OperationType.INPUT).length,
count_synthesis: items.filter(item => item.operation_type === OperationType.SYNTHESIS).length,
count_schemas: this.schemaIDs.length,
count_owned: operations.filter(item => !!item.result && item.is_owned).length,
count_block: this.oss.blocks.length
count_owned: items.filter(item => !!item.result && item.is_owned).length
};
}
}

View File

@ -1,7 +1,7 @@
import { z } from 'zod';
import { schemaLibraryItem } from '@/features/library/backend/types';
import { schemaSubstituteConstituents } from '@/features/rsform/backend/types';
import { schemaCstSubstitute } from '@/features/rsform/backend/types';
import { errorMsg } from '@/utils/labels';
@ -18,7 +18,7 @@ export type ICstSubstituteInfo = z.infer<typeof schemaCstSubstituteInfo>;
/** Represents {@link IOperation} data from server. */
export type IOperationDTO = z.infer<typeof schemaOperation>;
/** Represents {@link IBlock} data from server. */
/** Represents {@link IOperation} data from server. */
export type IBlockDTO = z.infer<typeof schemaBlock>;
/** Represents backend data for {@link IOperationSchema}. */
@ -27,47 +27,33 @@ export type IOperationSchemaDTO = z.infer<typeof schemaOperationSchema>;
/** Represents {@link IOperationSchema} layout. */
export type IOssLayout = z.infer<typeof schemaOssLayout>;
/** Represents {@link IBlock} data, used in Create action. */
export type ICreateBlockDTO = z.infer<typeof schemaCreateBlock>;
/** Represents data response when creating {@link IBlock}. */
export type IBlockCreatedResponse = z.infer<typeof schemaBlockCreatedResponse>;
/** Represents {@link IBlock} data, used in Update action. */
export type IUpdateBlockDTO = z.infer<typeof schemaUpdateBlock>;
/** Represents {@link IBlock} data, used in Delete action. */
export type IDeleteBlockDTO = z.infer<typeof schemaDeleteBlock>;
/** Represents data, used to move {@link IOssItem} to another parent. */
export type IMoveItemsDTO = z.infer<typeof schemaMoveItems>;
/** Represents {@link IOperation} data, used in Create action. */
export type ICreateOperationDTO = z.infer<typeof schemaCreateOperation>;
/** Represents {@link IOperation} data, used in creation process. */
export type IOperationCreateDTO = z.infer<typeof schemaOperationCreate>;
/** Represents data response when creating {@link IOperation}. */
export type IOperationCreatedResponse = z.infer<typeof schemaOperationCreatedResponse>;
/** Represents {@link IOperation} data, used in Update action. */
export type IUpdateOperationDTO = z.infer<typeof schemaUpdateOperation>;
/** Represents {@link IOperation} data, used in Delete action. */
export type IDeleteOperationDTO = z.infer<typeof schemaDeleteOperation>;
/** Represents target {@link IOperation}. */
/**
* Represents target {@link IOperation}.
*/
export interface ITargetOperation {
layout: IOssLayout;
target: number;
}
/** Represents {@link IOperation} data, used in destruction process. */
export type IOperationDeleteDTO = z.infer<typeof schemaOperationDelete>;
/** Represents data response when creating {@link IRSForm} for Input {@link IOperation}. */
export type IInputCreatedResponse = z.infer<typeof schemaInputCreatedResponse>;
/** Represents {@link IOperation} data, used in setInput action. */
export type IUpdateInputDTO = z.infer<typeof schemaUpdateInput>;
/** Represents {@link IOperation} data, used in setInput process. */
export type IInputUpdateDTO = z.infer<typeof schemaInputUpdate>;
/** Represents {@link IOperation} data, used in update process. */
export type IOperationUpdateDTO = z.infer<typeof schemaOperationUpdate>;
/** Represents data, used relocating {@link IConstituenta}s between {@link ILibraryItem}s. */
export type IRelocateConstituentsDTO = z.infer<typeof schemaRelocateConstituents>;
export type ICstRelocateDTO = z.infer<typeof schemaCstRelocate>;
/** Represents {@link IConstituenta} reference. */
export type IConstituentaReference = z.infer<typeof schemaConstituentaReference>;
@ -94,7 +80,7 @@ export const schemaBlock = z.strictObject({
parent: z.number().nullable()
});
export const schemaCstSubstituteInfo = schemaSubstituteConstituents.extend({
export const schemaCstSubstituteInfo = schemaCstSubstitute.extend({
operation: z.number(),
original_alias: z.string(),
original_term: z.string(),
@ -135,42 +121,7 @@ export const schemaOperationSchema = schemaLibraryItem.extend({
substitutions: z.array(schemaCstSubstituteInfo)
});
export const schemaCreateBlock = z.strictObject({
layout: schemaOssLayout,
item_data: z.strictObject({
title: z.string(),
description: z.string(),
parent: z.number().nullable()
}),
position_x: z.number(),
position_y: z.number(),
width: z.number(),
height: z.number(),
children_operations: z.array(z.number()),
children_blocks: z.array(z.number())
});
export const schemaBlockCreatedResponse = z.strictObject({
new_block: schemaBlock,
oss: schemaOperationSchema
});
export const schemaUpdateBlock = z.strictObject({
target: z.number(),
layout: schemaOssLayout,
item_data: z.strictObject({
title: z.string(),
description: z.string(),
parent: z.number().nullable()
})
});
export const schemaDeleteBlock = z.strictObject({
target: z.number(),
layout: schemaOssLayout
});
export const schemaCreateOperation = z.strictObject({
export const schemaOperationCreate = z.strictObject({
layout: schemaOssLayout,
item_data: z.strictObject({
alias: z.string().nonempty(),
@ -191,34 +142,14 @@ export const schemaOperationCreatedResponse = z.strictObject({
oss: schemaOperationSchema
});
export const schemaUpdateOperation = z.strictObject({
target: z.number(),
layout: schemaOssLayout,
item_data: z.strictObject({
alias: z.string().nonempty(errorMsg.requiredField),
title: z.string(),
description: z.string(),
parent: z.number().nullable()
}),
arguments: z.array(z.number()),
substitutions: z.array(schemaSubstituteConstituents)
});
export const schemaDeleteOperation = z.strictObject({
export const schemaOperationDelete = z.strictObject({
target: z.number(),
layout: schemaOssLayout,
keep_constituents: z.boolean(),
delete_schema: z.boolean()
});
export const schemaMoveItems = z.strictObject({
layout: schemaOssLayout,
operations: z.array(z.number()),
blocks: z.array(z.number()),
destination: z.number().nullable()
});
export const schemaUpdateInput = z.strictObject({
export const schemaInputUpdate = z.strictObject({
target: z.number(),
layout: schemaOssLayout,
input: z.number().nullable()
@ -229,7 +160,19 @@ export const schemaInputCreatedResponse = z.strictObject({
oss: schemaOperationSchema
});
export const schemaRelocateConstituents = z.strictObject({
export const schemaOperationUpdate = z.strictObject({
target: z.number(),
layout: schemaOssLayout,
item_data: z.strictObject({
alias: z.string().nonempty(errorMsg.requiredField),
title: z.string(),
description: z.string()
}),
arguments: z.array(z.number()),
substitutions: z.array(schemaCstSubstitute)
});
export const schemaCstRelocate = z.strictObject({
destination: z.number().nullable(),
items: z.array(z.number()).refine(data => data.length > 0)
});

View File

@ -1,25 +0,0 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/features/library/backend/use-update-timestamp';
import { KEYS } from '@/backend/configuration';
import { ossApi } from './api';
import { type ICreateOperationDTO } from './types';
export const useCreateOperation = () => {
const client = useQueryClient();
const { updateTimestamp } = useUpdateTimestamp();
const mutation = useMutation({
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'create-operation'],
mutationFn: ossApi.createOperation,
onSuccess: response => {
client.setQueryData(ossApi.getOssQueryOptions({ itemID: response.oss.id }).queryKey, response.oss);
updateTimestamp(response.oss.id);
},
onError: () => client.invalidateQueries()
});
return {
createOperation: (data: { itemID: number; data: ICreateOperationDTO }) => mutation.mutateAsync(data)
};
};

View File

@ -1,25 +0,0 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { KEYS } from '@/backend/configuration';
import { ossApi } from './api';
import { type IDeleteBlockDTO } from './types';
export const useDeleteBlock = () => {
const client = useQueryClient();
const mutation = useMutation({
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'delete-block'],
mutationFn: ossApi.deleteBlock,
onSuccess: data => {
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
return Promise.allSettled([
client.invalidateQueries({ queryKey: KEYS.composite.libraryList }),
client.invalidateQueries({ queryKey: [KEYS.rsform] })
]);
},
onError: () => client.invalidateQueries()
});
return {
deleteBlock: (data: { itemID: number; data: IDeleteBlockDTO }) => mutation.mutateAsync(data)
};
};

View File

@ -1,25 +0,0 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { KEYS } from '@/backend/configuration';
import { ossApi } from './api';
import { type IDeleteOperationDTO } from './types';
export const useDeleteOperation = () => {
const client = useQueryClient();
const mutation = useMutation({
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'delete-operation'],
mutationFn: ossApi.deleteOperation,
onSuccess: data => {
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
return Promise.allSettled([
client.invalidateQueries({ queryKey: KEYS.composite.libraryList }),
client.invalidateQueries({ queryKey: [KEYS.rsform] })
]);
},
onError: () => client.invalidateQueries()
});
return {
deleteOperation: (data: { itemID: number; data: IDeleteOperationDTO }) => mutation.mutateAsync(data)
};
};

View File

@ -5,11 +5,11 @@ import { KEYS } from '@/backend/configuration';
import { ossApi } from './api';
import { type ITargetOperation } from './types';
export const useCreateInput = () => {
export const useInputCreate = () => {
const client = useQueryClient();
const mutation = useMutation({
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'create-input'],
mutationFn: ossApi.createInput,
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'input-create'],
mutationFn: ossApi.inputCreate,
onSuccess: data => {
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.oss.id }).queryKey, data.oss);
return Promise.allSettled([
@ -20,7 +20,7 @@ export const useCreateInput = () => {
onError: () => client.invalidateQueries()
});
return {
createInput: (data: { itemID: number; data: ITargetOperation }) =>
inputCreate: (data: { itemID: number; data: ITargetOperation }) =>
mutation.mutateAsync(data).then(response => response.new_schema)
};
};

View File

@ -3,13 +3,13 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { KEYS } from '@/backend/configuration';
import { ossApi } from './api';
import { type IUpdateInputDTO } from './types';
import { type IInputUpdateDTO } from './types';
export const useUpdateInput = () => {
export const useInputUpdate = () => {
const client = useQueryClient();
const mutation = useMutation({
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'update-input'],
mutationFn: ossApi.updateInput,
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'input-update'],
mutationFn: ossApi.inputUpdate,
onSuccess: data => {
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
return Promise.allSettled([
@ -20,6 +20,6 @@ export const useUpdateInput = () => {
onError: () => client.invalidateQueries()
});
return {
updateInput: (data: { itemID: number; data: IUpdateInputDTO }) => mutation.mutateAsync(data)
inputUpdate: (data: { itemID: number; data: IInputUpdateDTO }) => mutation.mutateAsync(data)
};
};

View File

@ -5,14 +5,14 @@ import { useUpdateTimestamp } from '@/features/library/backend/use-update-timest
import { KEYS } from '@/backend/configuration';
import { ossApi } from './api';
import { type ICreateBlockDTO } from './types';
import { type IOperationCreateDTO } from './types';
export const useCreateBlock = () => {
export const useOperationCreate = () => {
const client = useQueryClient();
const { updateTimestamp } = useUpdateTimestamp();
const mutation = useMutation({
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'create-block'],
mutationFn: ossApi.createBlock,
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'operation-create'],
mutationFn: ossApi.operationCreate,
onSuccess: response => {
client.setQueryData(ossApi.getOssQueryOptions({ itemID: response.oss.id }).queryKey, response.oss);
updateTimestamp(response.oss.id);
@ -20,6 +20,6 @@ export const useCreateBlock = () => {
onError: () => client.invalidateQueries()
});
return {
createBlock: (data: { itemID: number; data: ICreateBlockDTO }) => mutation.mutateAsync(data)
operationCreate: (data: { itemID: number; data: IOperationCreateDTO }) => mutation.mutateAsync(data)
};
};

View File

@ -3,13 +3,13 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { KEYS } from '@/backend/configuration';
import { ossApi } from './api';
import { type IMoveItemsDTO } from './types';
import { type IOperationDeleteDTO } from './types';
export const useMoveItems = () => {
export const useOperationDelete = () => {
const client = useQueryClient();
const mutation = useMutation({
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'move-items'],
mutationFn: ossApi.moveItems,
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'operation-delete'],
mutationFn: ossApi.operationDelete,
onSuccess: data => {
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
return Promise.allSettled([
@ -20,6 +20,6 @@ export const useMoveItems = () => {
onError: () => client.invalidateQueries()
});
return {
moveItems: (data: { itemID: number; data: IMoveItemsDTO }) => mutation.mutateAsync(data)
operationDelete: (data: { itemID: number; data: IOperationDeleteDTO }) => mutation.mutateAsync(data)
};
};

View File

@ -5,11 +5,11 @@ import { KEYS } from '@/backend/configuration';
import { ossApi } from './api';
import { type ITargetOperation } from './types';
export const useExecuteOperation = () => {
export const useOperationExecute = () => {
const client = useQueryClient();
const mutation = useMutation({
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'execute-operation'],
mutationFn: ossApi.executeOperation,
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'operation-execute'],
mutationFn: ossApi.operationExecute,
onSuccess: data => {
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
return Promise.allSettled([
@ -20,6 +20,6 @@ export const useExecuteOperation = () => {
onError: () => client.invalidateQueries()
});
return {
executeOperation: (data: { itemID: number; data: ITargetOperation }) => mutation.mutateAsync(data)
operationExecute: (data: { itemID: number; data: ITargetOperation }) => mutation.mutateAsync(data)
};
};

View File

@ -5,13 +5,13 @@ import { type ILibraryItem } from '@/features/library';
import { KEYS } from '@/backend/configuration';
import { ossApi } from './api';
import { type IUpdateOperationDTO } from './types';
import { type IOperationUpdateDTO } from './types';
export const useUpdateOperation = () => {
export const useOperationUpdate = () => {
const client = useQueryClient();
const mutation = useMutation({
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'update-operation'],
mutationFn: ossApi.updateOperation,
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'operation-update'],
mutationFn: ossApi.operationUpdate,
onSuccess: (data, variables) => {
client.setQueryData(KEYS.composite.ossItem({ itemID: data.id }), data);
const schemaID = data.operations.find(item => item.id === variables.data.target)?.result;
@ -32,6 +32,6 @@ export const useUpdateOperation = () => {
onError: () => client.invalidateQueries()
});
return {
updateOperation: (data: { itemID: number; data: IUpdateOperationDTO }) => mutation.mutateAsync(data)
operationUpdate: (data: { itemID: number; data: IOperationUpdateDTO }) => mutation.mutateAsync(data)
};
};

View File

@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { KEYS } from '@/backend/configuration';
import { ossApi } from './api';
import { type IRelocateConstituentsDTO } from './types';
import { type ICstRelocateDTO } from './types';
export const useRelocateConstituents = () => {
const client = useQueryClient();
@ -20,6 +20,6 @@ export const useRelocateConstituents = () => {
onError: () => client.invalidateQueries()
});
return {
relocateConstituents: (data: IRelocateConstituentsDTO) => mutation.mutateAsync(data)
relocateConstituents: (data: ICstRelocateDTO) => mutation.mutateAsync(data)
};
};

View File

@ -1,37 +0,0 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { type ILibraryItem } from '@/features/library';
import { KEYS } from '@/backend/configuration';
import { ossApi } from './api';
import { type IUpdateBlockDTO } from './types';
export const useUpdateBlock = () => {
const client = useQueryClient();
const mutation = useMutation({
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'update-block'],
mutationFn: ossApi.updateBlock,
onSuccess: (data, variables) => {
client.setQueryData(KEYS.composite.ossItem({ itemID: data.id }), data);
const schemaID = data.operations.find(item => item.id === variables.data.target)?.result;
if (!schemaID) {
return;
}
client.setQueryData(KEYS.composite.libraryList, (prev: ILibraryItem[] | undefined) =>
!prev
? undefined
: prev.map(item =>
item.id === schemaID ? { ...item, ...variables.data.item_data, time_update: Date() } : item
)
);
return client.invalidateQueries({
queryKey: KEYS.composite.rsItem({ itemID: schemaID })
});
},
onError: () => client.invalidateQueries()
});
return {
updateBlock: (data: { itemID: number; data: IUpdateBlockDTO }) => mutation.mutateAsync(data)
};
};

View File

@ -3,7 +3,6 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/features/library/backend/use-update-timestamp';
import { KEYS } from '@/backend/configuration';
import { type RO } from '@/utils/meta';
import { ossApi } from './api';
import { type IOperationSchemaDTO, type IOssLayout } from './types';
@ -18,7 +17,7 @@ export const useUpdateLayout = () => {
updateTimestamp(variables.itemID);
client.setQueryData(
ossApi.getOssQueryOptions({ itemID: variables.itemID }).queryKey,
(prev: RO<IOperationSchemaDTO> | undefined) =>
(prev: IOperationSchemaDTO | undefined) =>
!prev
? prev
: {

View File

@ -1,26 +0,0 @@
'use client';
import { type IBlock } from '../models/oss';
interface InfoOperationProps {
block: IBlock;
}
export function InfoBlock({ block }: InfoOperationProps) {
return (
<>
{block.title ? (
<p>
<b>Название: </b>
{block.title}
</p>
) : null}
{block.description ? (
<p>
<b>Описание: </b>
{block.description}
</p>
) : null}
</>
);
}

View File

@ -0,0 +1,21 @@
import { Tooltip } from '@/components/container';
import { globalIDs } from '@/utils/constants';
import { useOperationTooltipStore } from '../stores/operation-tooltip';
import { InfoOperation } from './info-operation';
export function OperationTooltip() {
const hoverOperation = useOperationTooltipStore(state => state.activeOperation);
return (
<Tooltip
clickable
id={globalIDs.operation_tooltip}
layer='z-topmost'
className='max-w-140 dense max-h-120! overflow-y-auto!'
hidden={!hoverOperation}
>
{hoverOperation ? <InfoOperation operation={hoverOperation} /> : null}
</Tooltip>
);
}

View File

@ -1,159 +0,0 @@
'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 { type RO } from '@/utils/meta';
import { labelOssItem } from '../labels';
import { type IOperationSchema, type IOssItem } from '../models/oss';
import { getItemID, isOperation } from '../models/oss-api';
const SELECTION_CLEAR_TIMEOUT = 1000;
interface PickMultiOperationProps extends Styling {
value: number[];
onChange: (newValue: number[]) => void;
schema: IOperationSchema;
rows?: number;
exclude?: number[];
disallowBlocks?: boolean;
}
const columnHelper = createColumnHelper<RO<IOssItem>>();
export function PickContents({
rows,
schema,
exclude,
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<RO<IOssItem> | null>(null);
const items = [
...(disallowBlocks ? [] : schema.blocks.filter(item => !value.includes(-item.id) && !exclude?.includes(-item.id))),
...schema.operations.filter(item => !value.includes(item.id) && !exclude?.includes(item.id))
];
function handleDelete(target: number) {
onChange(value.filter(item => item !== target));
}
function handleSelect(target: RO<IOssItem> | null) {
if (target) {
setLastSelected(target);
onChange([...value, getItemID(target)]);
setTimeout(() => setLastSelected(null), SELECTION_CLEAR_TIMEOUT);
}
}
function handleMoveUp(target: number) {
const index = value.indexOf(target);
if (index > 0) {
const newSelected = [...value];
newSelected[index] = newSelected[index - 1];
newSelected[index - 1] = target;
onChange(newSelected);
}
}
function handleMoveDown(target: number) {
const index = value.indexOf(target);
if (index < value.length - 1) {
const newSelected = [...value];
newSelected[index] = newSelected[index + 1];
newSelected[index + 1] = target;
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(getItemID(props.row.original))}
/>
<MiniButton
title='Переместить выше'
noHover
className='px-0'
icon={<IconMoveUp size='1rem' className='icon-primary' />}
onClick={() => handleMoveUp(getItemID(props.row.original))}
/>
<MiniButton
title='Переместить ниже'
noHover
className='px-0'
icon={<IconMoveDown size='1rem' className='icon-primary' />}
onClick={() => handleMoveDown(getItemID(props.row.original))}
/>
</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(getItemID(item))}
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>
);
}

View File

@ -1,29 +0,0 @@
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}
clearable
placeholder={placeholder}
idFunc={block => String(block.id)}
labelValueFunc={block => block.title}
labelOptionFunc={block => block.title}
{...restProps}
/>
);
}

View File

@ -1,35 +0,0 @@
import clsx from 'clsx';
import { IconConceptBlock } from '@/components/icons';
import { globalIDs } from '@/utils/constants';
import { type IBlock } from '../models/oss';
import { SelectBlock } from './select-block';
interface SelectParentProps {
id?: string;
value: IBlock | null;
onChange: (newValue: IBlock | null) => void;
fullWidth?: boolean;
items?: IBlock[];
placeholder?: string;
noBorder?: boolean;
popoverClassname?: string;
}
export function SelectParent({ fullWidth, ...restProps }: SelectParentProps) {
return (
<div className={clsx('flex gap-2 items-center', !fullWidth ? 'w-80' : 'w-full')}>
<IconConceptBlock
tabIndex={-1}
size='2rem'
className='text-primary min-w-8'
data-tooltip-id={globalIDs.tooltip}
data-tooltip-content='Родительский блок содержания'
/>
<SelectBlock className={fullWidth ? 'grow' : 'w-70'} {...restProps} />
</div>
);
}

Some files were not shown because too many files have changed in this diff Show More