Compare commits
37 Commits
c05759901a
...
5c4149337b
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5c4149337b | ||
![]() |
ac38e9f4b5 | ||
![]() |
1ee3e9ef4f | ||
![]() |
9b6a014fb3 | ||
![]() |
f365b20814 | ||
![]() |
cb0f8783e5 | ||
![]() |
44c21dd7fd | ||
![]() |
65a65aba41 | ||
![]() |
6b36a3fd41 | ||
![]() |
976ab669e7 | ||
![]() |
52b039259a | ||
![]() |
893875c0cf | ||
![]() |
ef8355dc57 | ||
![]() |
8eedd70ffd | ||
![]() |
d2b37f9cad | ||
![]() |
4a65e2a0c4 | ||
![]() |
ffebe27d48 | ||
![]() |
807c22a3c8 | ||
![]() |
a7f6f994fe | ||
![]() |
fd6d448efd | ||
![]() |
59fa2c36de | ||
![]() |
e6d0b50833 | ||
![]() |
096efef357 | ||
![]() |
8a3748bc2d | ||
![]() |
9b00125996 | ||
![]() |
4278505f86 | ||
![]() |
983874af7e | ||
![]() |
d4e45a4c73 | ||
![]() |
d75d834865 | ||
![]() |
cd26b0a10a | ||
![]() |
f78dbc6065 | ||
![]() |
8cbb1faa50 | ||
![]() |
507a7c32db | ||
![]() |
be11b4a59a | ||
![]() |
af80099292 | ||
![]() |
3e32233764 | ||
![]() |
3d9930c44f |
73
.cursorignore
Normal file
73
.cursorignore
Normal file
|
@ -0,0 +1,73 @@
|
|||
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
|
||||
|
||||
# SECURITY SENSITIVE FILES
|
||||
secrets/
|
||||
nginx/cert/*.pem
|
||||
|
||||
# External distributions
|
||||
rsconcept/backend/import/*.whl
|
||||
rsconcept/backend/static
|
||||
rsconcept/backend/media
|
||||
rsconcept/frontend/dist
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
eggs/
|
||||
.eggs/
|
||||
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
.mypy_cache/
|
||||
|
||||
|
||||
# Django
|
||||
*.log
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
visualizeDB.dot
|
||||
|
||||
|
||||
# React
|
||||
.DS_*
|
||||
*.log
|
||||
*.tsbuildinfo
|
||||
logs
|
||||
**/*.backup.*
|
||||
**/*.back.*
|
||||
|
||||
node_modules
|
||||
bower_components
|
||||
|
||||
*.sublime*
|
||||
|
||||
# NextJS
|
||||
**/.next/
|
||||
**/out/
|
||||
|
||||
|
||||
# Environments
|
||||
venv/
|
||||
/GitExtensions.settings
|
||||
rsconcept/frontend/public/privacy.pdf
|
||||
/rsconcept/frontend/playwright-report
|
||||
/rsconcept/frontend/test-results
|
|
@ -68,8 +68,6 @@ class TestLibraryViewset(EndpointTester):
|
|||
self.assertEqual(response.data['access_policy'], data['access_policy'])
|
||||
self.assertEqual(response.data['visible'], data['visible'])
|
||||
self.assertEqual(response.data['read_only'], data['read_only'])
|
||||
self.assertEqual(oss.layout().data['operations'], [])
|
||||
self.assertEqual(oss.layout().data['blocks'], [])
|
||||
|
||||
self.logout()
|
||||
data = {'title': 'Title2'}
|
||||
|
|
|
@ -41,7 +41,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
else:
|
||||
serializer.save()
|
||||
if serializer.data.get('item_type') == m.LibraryItemType.OPERATION_SCHEMA:
|
||||
Layout.objects.create(oss=serializer.instance, data={'operations': [], 'blocks': []})
|
||||
Layout.objects.create(oss=serializer.instance, data=[])
|
||||
|
||||
def perform_update(self, serializer) -> None:
|
||||
instance = serializer.save()
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
from django.db import migrations
|
||||
|
||||
|
||||
def migrate_layout(apps, schema_editor):
|
||||
Layout = apps.get_model('oss', 'Layout')
|
||||
|
||||
for layout in Layout.objects.all():
|
||||
previous_data = layout.data
|
||||
new_layout = []
|
||||
|
||||
for operation in previous_data['operations']:
|
||||
new_layout.append({
|
||||
'nodeID': 'o' + str(operation['id']),
|
||||
'x': operation['x'],
|
||||
'y': operation['y'],
|
||||
'width': 150,
|
||||
'height': 40
|
||||
})
|
||||
|
||||
for block in previous_data['blocks']:
|
||||
new_layout.append({
|
||||
'nodeID': 'b' + str(block['id']),
|
||||
'x': block['x'],
|
||||
'y': block['y'],
|
||||
'width': block['width'],
|
||||
'height': block['height']
|
||||
})
|
||||
|
||||
layout.data = new_layout
|
||||
layout.save(update_fields=['data'])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('oss', '0011_remove_operation_position_x_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_layout),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 5.2.1 on 2025-06-11 10:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('oss', '0012 restructure_layout'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='layout',
|
||||
name='data',
|
||||
field=models.JSONField(default=list, verbose_name='Расположение'),
|
||||
),
|
||||
]
|
|
@ -13,7 +13,7 @@ class Layout(Model):
|
|||
|
||||
data = JSONField(
|
||||
verbose_name='Расположение',
|
||||
default=dict
|
||||
default=list
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -40,7 +40,7 @@ class OperationSchema:
|
|||
def create(**kwargs) -> 'OperationSchema':
|
||||
''' Create LibraryItem via OperationSchema. '''
|
||||
model = LibraryItem.objects.create(item_type=LibraryItemType.OPERATION_SCHEMA, **kwargs)
|
||||
Layout.objects.create(oss=model, data={'operations': [], 'blocks': []})
|
||||
Layout.objects.create(oss=model, data=[])
|
||||
return OperationSchema(model)
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -2,16 +2,9 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
|
||||
class OperationNodeSerializer(serializers.Serializer):
|
||||
''' Operation position. '''
|
||||
id = serializers.IntegerField()
|
||||
x = serializers.FloatField()
|
||||
y = serializers.FloatField()
|
||||
|
||||
|
||||
class BlockNodeSerializer(serializers.Serializer):
|
||||
class NodeSerializer(serializers.Serializer):
|
||||
''' Block position. '''
|
||||
id = serializers.IntegerField()
|
||||
nodeID = serializers.CharField()
|
||||
x = serializers.FloatField()
|
||||
y = serializers.FloatField()
|
||||
width = serializers.FloatField()
|
||||
|
@ -19,13 +12,8 @@ class BlockNodeSerializer(serializers.Serializer):
|
|||
|
||||
|
||||
class LayoutSerializer(serializers.Serializer):
|
||||
''' Layout for OperationSchema. '''
|
||||
blocks = serializers.ListField(
|
||||
child=BlockNodeSerializer()
|
||||
)
|
||||
operations = serializers.ListField(
|
||||
child=OperationNodeSerializer()
|
||||
)
|
||||
''' Serializer: Layout data. '''
|
||||
data = serializers.ListField(child=NodeSerializer()) # type: ignore
|
||||
|
||||
|
||||
class SubstitutionExSerializer(serializers.Serializer):
|
||||
|
|
|
@ -13,7 +13,7 @@ from apps.rsform.serializers import SubstitutionSerializerBase
|
|||
from shared import messages as msg
|
||||
|
||||
from ..models import Argument, Block, Inheritance, Operation, OperationSchema, OperationType
|
||||
from .basics import LayoutSerializer, SubstitutionExSerializer
|
||||
from .basics import NodeSerializer, SubstitutionExSerializer
|
||||
|
||||
|
||||
class OperationSerializer(serializers.ModelSerializer):
|
||||
|
@ -52,7 +52,9 @@ class CreateBlockSerializer(serializers.Serializer):
|
|||
model = Block
|
||||
fields = 'title', 'description', 'parent'
|
||||
|
||||
layout = LayoutSerializer()
|
||||
layout = serializers.ListField(
|
||||
child=NodeSerializer()
|
||||
)
|
||||
item_data = BlockCreateData()
|
||||
width = serializers.FloatField()
|
||||
height = serializers.FloatField()
|
||||
|
@ -100,7 +102,10 @@ class UpdateBlockSerializer(serializers.Serializer):
|
|||
model = Block
|
||||
fields = 'title', 'description', 'parent'
|
||||
|
||||
layout = LayoutSerializer(required=False)
|
||||
layout = serializers.ListField(
|
||||
child=NodeSerializer(),
|
||||
required=False
|
||||
)
|
||||
target = PKField(many=False, queryset=Block.objects.all())
|
||||
item_data = UpdateBlockData()
|
||||
|
||||
|
@ -127,7 +132,9 @@ class UpdateBlockSerializer(serializers.Serializer):
|
|||
|
||||
class DeleteBlockSerializer(serializers.Serializer):
|
||||
''' Serializer: Delete block. '''
|
||||
layout = LayoutSerializer()
|
||||
layout = serializers.ListField(
|
||||
child=NodeSerializer()
|
||||
)
|
||||
target = PKField(many=False, queryset=Block.objects.all().only('oss_id'))
|
||||
|
||||
def validate(self, attrs):
|
||||
|
@ -142,7 +149,9 @@ class DeleteBlockSerializer(serializers.Serializer):
|
|||
|
||||
class MoveItemsSerializer(serializers.Serializer):
|
||||
''' Serializer: Move items to another parent. '''
|
||||
layout = LayoutSerializer()
|
||||
layout = serializers.ListField(
|
||||
child=NodeSerializer()
|
||||
)
|
||||
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)
|
||||
|
@ -196,8 +205,12 @@ class CreateOperationSerializer(serializers.Serializer):
|
|||
'alias', 'operation_type', 'title', \
|
||||
'description', 'result', 'parent'
|
||||
|
||||
layout = LayoutSerializer()
|
||||
layout = serializers.ListField(
|
||||
child=NodeSerializer()
|
||||
)
|
||||
item_data = CreateOperationData()
|
||||
width = serializers.FloatField()
|
||||
height = serializers.FloatField()
|
||||
position_x = serializers.FloatField()
|
||||
position_y = serializers.FloatField()
|
||||
create_schema = serializers.BooleanField(default=False, required=False)
|
||||
|
@ -230,7 +243,10 @@ class UpdateOperationSerializer(serializers.Serializer):
|
|||
model = Operation
|
||||
fields = 'alias', 'title', 'description', 'parent'
|
||||
|
||||
layout = LayoutSerializer(required=False)
|
||||
layout = serializers.ListField(
|
||||
child=NodeSerializer(),
|
||||
required=False
|
||||
)
|
||||
target = PKField(many=False, queryset=Operation.objects.all())
|
||||
item_data = UpdateOperationData()
|
||||
arguments = PKField(many=True, queryset=Operation.objects.all().only('oss_id', 'result_id'), required=False)
|
||||
|
@ -297,7 +313,9 @@ class UpdateOperationSerializer(serializers.Serializer):
|
|||
|
||||
class DeleteOperationSerializer(serializers.Serializer):
|
||||
''' Serializer: Delete operation. '''
|
||||
layout = LayoutSerializer()
|
||||
layout = serializers.ListField(
|
||||
child=NodeSerializer()
|
||||
)
|
||||
target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'result'))
|
||||
keep_constituents = serializers.BooleanField(default=False, required=False)
|
||||
delete_schema = serializers.BooleanField(default=False, required=False)
|
||||
|
@ -314,7 +332,9 @@ class DeleteOperationSerializer(serializers.Serializer):
|
|||
|
||||
class TargetOperationSerializer(serializers.Serializer):
|
||||
''' Serializer: Target single operation. '''
|
||||
layout = LayoutSerializer()
|
||||
layout = serializers.ListField(
|
||||
child=NodeSerializer()
|
||||
)
|
||||
target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'result_id'))
|
||||
|
||||
def validate(self, attrs):
|
||||
|
@ -329,7 +349,9 @@ class TargetOperationSerializer(serializers.Serializer):
|
|||
|
||||
class SetOperationInputSerializer(serializers.Serializer):
|
||||
''' Serializer: Set input schema for operation. '''
|
||||
layout = LayoutSerializer()
|
||||
layout = serializers.ListField(
|
||||
child=NodeSerializer()
|
||||
)
|
||||
target = PKField(many=False, queryset=Operation.objects.all())
|
||||
input = PKField(
|
||||
many=False,
|
||||
|
@ -366,7 +388,9 @@ class OperationSchemaSerializer(serializers.ModelSerializer):
|
|||
substitutions = serializers.ListField(
|
||||
child=SubstitutionExSerializer()
|
||||
)
|
||||
layout = LayoutSerializer()
|
||||
layout = serializers.ListField(
|
||||
child=NodeSerializer()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
''' serializer metadata. '''
|
||||
|
@ -459,7 +483,7 @@ class RelocateConstituentsSerializer(serializers.Serializer):
|
|||
|
||||
return attrs
|
||||
|
||||
# ====== Internals =================================================================================
|
||||
# ====== Internals ============
|
||||
|
||||
|
||||
def _collect_descendants(start_blocks: list[Block]) -> set[int]:
|
||||
|
|
|
@ -59,14 +59,11 @@ class TestChangeAttributes(EndpointTester):
|
|||
self.operation3.refresh_from_db()
|
||||
self.ks3 = RSForm(self.operation3.result)
|
||||
|
||||
self.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': []
|
||||
}
|
||||
self.layout_data = [
|
||||
{'nodeID': 'o' + str(self.operation1.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation2.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation3.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
]
|
||||
layout = self.owned.layout()
|
||||
layout.data = self.layout_data
|
||||
layout.save()
|
||||
|
|
|
@ -57,14 +57,11 @@ class TestChangeConstituents(EndpointTester):
|
|||
self.ks3 = RSForm(self.operation3.result)
|
||||
self.assertEqual(self.ks3.constituents().count(), 4)
|
||||
|
||||
self.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': []
|
||||
}
|
||||
self.layout_data = [
|
||||
{'nodeID': 'o' + str(self.operation1.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation2.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation3.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
]
|
||||
layout = self.owned.layout()
|
||||
layout.data = self.layout_data
|
||||
layout.save()
|
||||
|
|
|
@ -107,16 +107,13 @@ class TestChangeOperations(EndpointTester):
|
|||
convention='KS5D4'
|
||||
)
|
||||
|
||||
self.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},
|
||||
{'id': self.operation4.pk, 'x': 0, 'y': 0},
|
||||
{'id': self.operation5.pk, 'x': 0, 'y': 0},
|
||||
],
|
||||
'blocks': []
|
||||
}
|
||||
self.layout_data = [
|
||||
{'nodeID': 'o' + str(self.operation1.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation2.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation3.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation4.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation5.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40}
|
||||
]
|
||||
layout = self.owned.layout()
|
||||
layout.data = self.layout_data
|
||||
layout.save()
|
||||
|
|
|
@ -107,16 +107,13 @@ class TestChangeSubstitutions(EndpointTester):
|
|||
convention='KS5D4'
|
||||
)
|
||||
|
||||
self.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},
|
||||
{'id': self.operation4.pk, 'x': 0, 'y': 0},
|
||||
{'id': self.operation5.pk, 'x': 0, 'y': 0},
|
||||
],
|
||||
'blocks': []
|
||||
}
|
||||
self.layout_data = [
|
||||
{'nodeID': 'o' + str(self.operation1.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation2.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation3.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation4.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation5.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
]
|
||||
layout = self.owned.layout()
|
||||
layout.data = self.layout_data
|
||||
layout.save()
|
||||
|
|
|
@ -49,16 +49,14 @@ class TestOssBlocks(EndpointTester):
|
|||
title='3',
|
||||
parent=self.block1
|
||||
)
|
||||
self.layout_data = {
|
||||
'operations': [
|
||||
{'id': self.operation1.pk, 'x': 0, 'y': 0},
|
||||
{'id': self.operation2.pk, 'x': 0, 'y': 0},
|
||||
],
|
||||
'blocks': [
|
||||
{'id': self.block1.pk, 'x': 0, 'y': 0, 'width': 0.5, 'height': 0.5},
|
||||
{'id': self.block2.pk, 'x': 0, 'y': 0, 'width': 0.5, 'height': 0.5},
|
||||
self.layout_data = [
|
||||
{'nodeID': 'o' + str(self.operation1.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation2.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
|
||||
{'nodeID': 'b' + str(self.block1.pk), 'x': 0, 'y': 0, 'width': 0.5, 'height': 0.5},
|
||||
{'nodeID': 'b' + str(self.block2.pk), 'x': 0, 'y': 0, 'width': 0.5, 'height': 0.5},
|
||||
]
|
||||
}
|
||||
|
||||
layout = self.owned.layout()
|
||||
layout.data = self.layout_data
|
||||
layout.save()
|
||||
|
@ -88,7 +86,7 @@ class TestOssBlocks(EndpointTester):
|
|||
self.assertEqual(len(response.data['oss']['blocks']), 3)
|
||||
new_block = response.data['new_block']
|
||||
layout = response.data['oss']['layout']
|
||||
item = [item for item in layout['blocks'] if item['id'] == new_block['id']][0]
|
||||
item = [item for item in layout if item['nodeID'] == 'b' + str(new_block['id'])][0]
|
||||
self.assertEqual(new_block['title'], data['item_data']['title'])
|
||||
self.assertEqual(new_block['description'], data['item_data']['description'])
|
||||
self.assertEqual(new_block['parent'], None)
|
||||
|
|
|
@ -54,14 +54,11 @@ class TestOssOperations(EndpointTester):
|
|||
alias='3',
|
||||
operation_type=OperationType.SYNTHESIS
|
||||
)
|
||||
self.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': []
|
||||
}
|
||||
self.layout_data = [
|
||||
{'nodeID': 'o' + str(self.operation1.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation2.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation3.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
]
|
||||
layout = self.owned.layout()
|
||||
layout.data = self.layout_data
|
||||
layout.save()
|
||||
|
@ -87,7 +84,9 @@ class TestOssOperations(EndpointTester):
|
|||
},
|
||||
'layout': self.layout_data,
|
||||
'position_x': 1,
|
||||
'position_y': 1
|
||||
'position_y': 1,
|
||||
'width': 500,
|
||||
'height': 50
|
||||
|
||||
}
|
||||
self.executeBadData(data=data)
|
||||
|
@ -102,7 +101,7 @@ class TestOssOperations(EndpointTester):
|
|||
self.assertEqual(len(response.data['oss']['operations']), 4)
|
||||
new_operation = response.data['new_operation']
|
||||
layout = response.data['oss']['layout']
|
||||
item = [item for item in layout['operations'] if item['id'] == new_operation['id']][0]
|
||||
item = [item for item in layout if item['nodeID'] == 'o' + str(new_operation['id'])][0]
|
||||
self.assertEqual(new_operation['alias'], data['item_data']['alias'])
|
||||
self.assertEqual(new_operation['operation_type'], data['item_data']['operation_type'])
|
||||
self.assertEqual(new_operation['title'], data['item_data']['title'])
|
||||
|
@ -111,6 +110,8 @@ class TestOssOperations(EndpointTester):
|
|||
self.assertEqual(new_operation['parent'], None)
|
||||
self.assertEqual(item['x'], data['position_x'])
|
||||
self.assertEqual(item['y'], data['position_y'])
|
||||
self.assertEqual(item['width'], data['width'])
|
||||
self.assertEqual(item['height'], data['height'])
|
||||
self.operation1.refresh_from_db()
|
||||
|
||||
self.executeForbidden(data=data, item=self.unowned_id)
|
||||
|
@ -132,7 +133,9 @@ class TestOssOperations(EndpointTester):
|
|||
},
|
||||
'layout': self.layout_data,
|
||||
'position_x': 1,
|
||||
'position_y': 1
|
||||
'position_y': 1,
|
||||
'width': 500,
|
||||
'height': 50
|
||||
|
||||
}
|
||||
self.executeBadData(data=data, item=self.owned_id)
|
||||
|
@ -160,6 +163,8 @@ class TestOssOperations(EndpointTester):
|
|||
'layout': self.layout_data,
|
||||
'position_x': 1,
|
||||
'position_y': 1,
|
||||
'width': 500,
|
||||
'height': 50,
|
||||
'arguments': [self.operation1.pk, self.operation3.pk]
|
||||
}
|
||||
response = self.executeCreated(data=data, item=self.owned_id)
|
||||
|
@ -185,7 +190,9 @@ class TestOssOperations(EndpointTester):
|
|||
},
|
||||
'layout': self.layout_data,
|
||||
'position_x': 1,
|
||||
'position_y': 1
|
||||
'position_y': 1,
|
||||
'width': 500,
|
||||
'height': 50
|
||||
}
|
||||
response = self.executeCreated(data=data, item=self.owned_id)
|
||||
new_operation = response.data['new_operation']
|
||||
|
@ -207,7 +214,9 @@ class TestOssOperations(EndpointTester):
|
|||
'create_schema': True,
|
||||
'layout': self.layout_data,
|
||||
'position_x': 1,
|
||||
'position_y': 1
|
||||
'position_y': 1,
|
||||
'width': 500,
|
||||
'height': 50
|
||||
}
|
||||
self.executeBadData(data=data, item=self.owned_id)
|
||||
data['item_data']['result'] = None
|
||||
|
@ -244,7 +253,7 @@ class TestOssOperations(EndpointTester):
|
|||
self.login()
|
||||
response = self.executeOK(data=data)
|
||||
layout = response.data['layout']
|
||||
deleted_items = [item for item in layout['operations'] if item['id'] == data['target']]
|
||||
deleted_items = [item for item in layout if item['nodeID'] == 'o' + str(data['target'])]
|
||||
self.assertEqual(len(response.data['operations']), 2)
|
||||
self.assertEqual(len(deleted_items), 0)
|
||||
|
||||
|
|
|
@ -55,11 +55,11 @@ class TestOssViewset(EndpointTester):
|
|||
alias='3',
|
||||
operation_type=OperationType.SYNTHESIS
|
||||
)
|
||||
self.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': []}
|
||||
self.layout_data = [
|
||||
{'nodeID': 'o' + str(self.operation1.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation2.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation3.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40}
|
||||
]
|
||||
layout = self.owned.layout()
|
||||
layout.data = self.layout_data
|
||||
layout.save()
|
||||
|
@ -107,10 +107,9 @@ class TestOssViewset(EndpointTester):
|
|||
self.assertEqual(arguments[1]['argument'], self.operation2.pk)
|
||||
|
||||
layout = response.data['layout']
|
||||
self.assertEqual(layout['blocks'], [])
|
||||
self.assertEqual(layout['operations'][0], {'id': self.operation1.pk, 'x': 0, 'y': 0})
|
||||
self.assertEqual(layout['operations'][1], {'id': self.operation2.pk, 'x': 0, 'y': 0})
|
||||
self.assertEqual(layout['operations'][2], {'id': self.operation3.pk, 'x': 0, 'y': 0})
|
||||
self.assertEqual(layout[0], self.layout_data[0])
|
||||
self.assertEqual(layout[1], self.layout_data[1])
|
||||
self.assertEqual(layout[2], self.layout_data[2])
|
||||
|
||||
self.executeOK(item=self.unowned_id)
|
||||
self.executeForbidden(item=self.private_id)
|
||||
|
@ -126,23 +125,21 @@ class TestOssViewset(EndpointTester):
|
|||
self.populateData()
|
||||
self.executeBadData(item=self.owned_id)
|
||||
|
||||
data = {'operations': [], 'blocks': []}
|
||||
data = {'data': []}
|
||||
self.executeOK(data=data)
|
||||
|
||||
data = {
|
||||
'operations': [
|
||||
{'id': self.operation1.pk, 'x': 42.1, 'y': 1337},
|
||||
{'id': self.operation2.pk, 'x': 36.1, 'y': 1437},
|
||||
{'id': self.operation3.pk, 'x': 36.1, 'y': 1435}
|
||||
], 'blocks': []
|
||||
}
|
||||
data = {'data': [
|
||||
{'nodeID': 'o' + str(self.operation1.pk), 'x': 42.1, 'y': 1337, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation2.pk), 'x': 36.1, 'y': 1437, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation3.pk), 'x': 36.1, 'y': 1435, 'width': 150, 'height': 40}
|
||||
]}
|
||||
self.toggle_admin(True)
|
||||
self.executeOK(data=data, item=self.unowned_id)
|
||||
|
||||
self.toggle_admin(False)
|
||||
self.executeOK(data=data, item=self.owned_id)
|
||||
self.owned.refresh_from_db()
|
||||
self.assertEqual(self.owned.layout().data, data)
|
||||
self.assertEqual(self.owned.layout().data, data['data'])
|
||||
|
||||
self.executeForbidden(data=data, item=self.unowned_id)
|
||||
self.executeForbidden(data=data, item=self.private_id)
|
||||
|
|
|
@ -91,7 +91,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
''' Endpoint: Update schema layout. '''
|
||||
serializer = s.LayoutSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
m.OperationSchema(self.get_object()).update_layout(serializer.validated_data)
|
||||
m.OperationSchema(self.get_object()).update_layout(serializer.validated_data['data'])
|
||||
return Response(status=c.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
|
@ -120,8 +120,8 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
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,
|
||||
layout.append({
|
||||
'nodeID': 'b' + str(new_block.pk),
|
||||
'x': serializer.validated_data['position_x'],
|
||||
'y': serializer.validated_data['position_y'],
|
||||
'width': serializer.validated_data['width'],
|
||||
|
@ -205,7 +205,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
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]
|
||||
layout = [x for x in layout if x['nodeID'] != 'b' + str(block.pk)]
|
||||
with transaction.atomic():
|
||||
oss.delete_block(block)
|
||||
oss.update_layout(layout)
|
||||
|
@ -274,10 +274,12 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
layout = serializer.validated_data['layout']
|
||||
with transaction.atomic():
|
||||
new_operation = oss.create_operation(**serializer.validated_data['item_data'])
|
||||
layout['operations'].append({
|
||||
'id': new_operation.pk,
|
||||
layout.append({
|
||||
'nodeID': 'o' + str(new_operation.pk),
|
||||
'x': serializer.validated_data['position_x'],
|
||||
'y': serializer.validated_data['position_y']
|
||||
'y': serializer.validated_data['position_y'],
|
||||
'width': serializer.validated_data['width'],
|
||||
'height': serializer.validated_data['height']
|
||||
})
|
||||
oss.update_layout(layout)
|
||||
|
||||
|
@ -342,7 +344,9 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
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'])
|
||||
if 'parent' in serializer.validated_data['item_data']:
|
||||
operation.parent = serializer.validated_data['item_data']['parent']
|
||||
operation.save(update_fields=['alias', 'title', 'description', 'parent'])
|
||||
|
||||
if operation.result is not None:
|
||||
can_edit = permissions.can_edit_item(request.user, operation.result)
|
||||
|
@ -384,7 +388,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
operation = cast(m.Operation, serializer.validated_data['target'])
|
||||
old_schema = operation.result
|
||||
layout = serializer.validated_data['layout']
|
||||
layout['operations'] = [x for x in layout['operations'] if x['id'] != operation.pk]
|
||||
layout = [x for x in layout if x['nodeID'] != 'o' + str(operation.pk)]
|
||||
with transaction.atomic():
|
||||
oss.delete_operation(operation.pk, serializer.validated_data['keep_constituents'])
|
||||
oss.update_layout(layout)
|
||||
|
|
|
@ -7475,36 +7475,44 @@
|
|||
"pk": 1,
|
||||
"fields": {
|
||||
"oss": 41,
|
||||
"data": {
|
||||
"blocks": [],
|
||||
"operations": [
|
||||
"data": [
|
||||
{
|
||||
"x": 530.0,
|
||||
"y": 370.0,
|
||||
"id": 1
|
||||
"nodeID": "o1",
|
||||
"width": 150.0,
|
||||
"height": 40.0
|
||||
},
|
||||
|
||||
{
|
||||
"x": 710.0,
|
||||
"y": 370.0,
|
||||
"id": 2
|
||||
"nodeID": "o2",
|
||||
"width": 150.0,
|
||||
"height": 40.0
|
||||
},
|
||||
{
|
||||
"x": 890.0,
|
||||
"y": 370.0,
|
||||
"id": 4
|
||||
"nodeID": "o4",
|
||||
"width": 150.0,
|
||||
"height": 40.0
|
||||
},
|
||||
{
|
||||
"x": 620.0,
|
||||
"y": 470.0,
|
||||
"id": 9
|
||||
"nodeID": "o9",
|
||||
"width": 150.0,
|
||||
"height": 40.0
|
||||
},
|
||||
{
|
||||
"x": 760.0,
|
||||
"y": 570.0,
|
||||
"id": 10
|
||||
"nodeID": "o10",
|
||||
"width": 150.0,
|
||||
"height": 40.0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -29,6 +29,18 @@
|
|||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script>
|
||||
try {
|
||||
const preferences = JSON.parse(localStorage.getItem('portal.preferences') || '{}').state;
|
||||
const isDark = preferences ? preferences.darkMode : window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
if (isDark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
document.documentElement.setAttribute('data-color-scheme', isDark ? 'dark' : 'light');
|
||||
} catch (e) {}
|
||||
</script>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
3412
rsconcept/frontend/package-lock.json
generated
3412
rsconcept/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -14,73 +14,73 @@
|
|||
"preview": "vite preview --port 3000"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dagrejs/dagre": "^1.1.4",
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@dagrejs/dagre": "^1.1.5",
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
"@lezer/lr": "^1.4.2",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@tanstack/react-query": "^5.80.5",
|
||||
"@tanstack/react-query-devtools": "^5.80.5",
|
||||
"@tanstack/react-query": "^5.81.5",
|
||||
"@tanstack/react-query-devtools": "^5.81.5",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@uiw/codemirror-themes": "^4.23.12",
|
||||
"@uiw/react-codemirror": "^4.23.12",
|
||||
"axios": "^1.9.0",
|
||||
"@uiw/codemirror-themes": "^4.23.14",
|
||||
"@uiw/react-codemirror": "^4.23.14",
|
||||
"axios": "^1.10.0",
|
||||
"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.511.0",
|
||||
"lucide-react": "^0.525.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.57.0",
|
||||
"react-hook-form": "^7.59.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-intl": "^7.1.11",
|
||||
"react-router": "^7.6.2",
|
||||
"react-scan": "^0.3.4",
|
||||
"react-router": "^7.6.3",
|
||||
"react-scan": "^0.3.6",
|
||||
"react-tabs": "^6.1.0",
|
||||
"react-toastify": "^11.0.5",
|
||||
"react-tooltip": "^5.28.1",
|
||||
"react-tooltip": "^5.29.1",
|
||||
"react-zoom-pan-pinch": "^3.7.0",
|
||||
"reactflow": "^11.11.4",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tw-animate-css": "^1.3.4",
|
||||
"use-debounce": "^10.0.4",
|
||||
"zod": "^3.25.51",
|
||||
"zustand": "^5.0.5"
|
||||
"use-debounce": "^10.0.5",
|
||||
"zod": "^3.25.67",
|
||||
"zustand": "^5.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lezer/generator": "^1.7.3",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@tailwindcss/vite": "^4.1.8",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.15.29",
|
||||
"@types/react": "^19.1.6",
|
||||
"@lezer/generator": "^1.8.0",
|
||||
"@playwright/test": "^1.53.2",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^24.0.8",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
||||
"@typescript-eslint/parser": "^8.0.1",
|
||||
"@vitejs/plugin-react": "^4.5.1",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"babel-plugin-react-compiler": "^19.1.0-rc.1",
|
||||
"eslint": "^9.28.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint": "^9.30.0",
|
||||
"eslint-plugin-import": "^2.32.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-hooks": "^5.2.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"globals": "^16.2.0",
|
||||
"jest": "^29.7.0",
|
||||
"stylelint": "^16.20.0",
|
||||
"globals": "^16.3.0",
|
||||
"jest": "^30.0.3",
|
||||
"stylelint": "^16.21.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.4",
|
||||
"ts-jest": "^29.4.0",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.33.1",
|
||||
"vite": "^6.3.5"
|
||||
"typescript-eslint": "^8.35.1",
|
||||
"vite": "^7.0.0"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "ts-jest",
|
||||
|
|
|
@ -14,10 +14,10 @@ export function Footer() {
|
|||
)}
|
||||
>
|
||||
<nav className='flex gap-3' aria-label='Вторичная навигация'>
|
||||
<TextURL text='Библиотека' href='/library' color='' />
|
||||
<TextURL text='Справка' href='/manuals' color='' />
|
||||
<TextURL text='Центр Концепт' href={external_urls.concept} color='' />
|
||||
<TextURL text='Экстеор' href='/manuals?topic=exteor' color='' />
|
||||
<TextURL text='Библиотека' href='/library' color='hover:text-foreground' />
|
||||
<TextURL text='Справка' href='/manuals' color='hover:text-foreground' />
|
||||
<TextURL text='Центр Концепт' href={external_urls.concept} color='hover:text-foreground' />
|
||||
<TextURL text='Экстеор' href='/manuals?topic=exteor' color='hover:text-foreground' />
|
||||
</nav>
|
||||
|
||||
<p>© 2025 ЦИВТ КОНЦЕПТ</p>
|
||||
|
|
|
@ -10,8 +10,9 @@ export const GlobalTooltips = () => {
|
|||
float
|
||||
id={globalIDs.tooltip}
|
||||
layer='z-topmost'
|
||||
place='right-start'
|
||||
className='mt-8 max-w-80 break-words rounded-lg!'
|
||||
place='bottom-start'
|
||||
offset={24}
|
||||
className='max-w-80 break-words rounded-lg! select-none'
|
||||
/>
|
||||
<Tooltip
|
||||
float
|
||||
|
|
|
@ -25,8 +25,12 @@ export function ToggleNavigation() {
|
|||
data-tooltip-content={noNavigationAnimation ? 'Показать навигацию' : 'Скрыть навигацию'}
|
||||
aria-label={noNavigationAnimation ? 'Показать навигацию' : 'Скрыть навигацию'}
|
||||
>
|
||||
{!noNavigationAnimation ? <IconPin size='0.75rem' /> : null}
|
||||
{noNavigationAnimation ? <IconUnpin size='0.75rem' /> : null}
|
||||
{!noNavigationAnimation ? (
|
||||
<IconPin size='0.75rem' className='hover:text-primary cc-animate-color cc-hover-pulse' />
|
||||
) : null}
|
||||
{noNavigationAnimation ? (
|
||||
<IconUnpin size='0.75rem' className='hover:text-primary cc-animate-color cc-hover-pulse' />
|
||||
) : null}
|
||||
</button>
|
||||
{!noNavigationAnimation ? (
|
||||
<button
|
||||
|
@ -38,8 +42,12 @@ export function ToggleNavigation() {
|
|||
data-tooltip-content={darkMode ? 'Тема: Темная' : 'Тема: Светлая'}
|
||||
aria-label={darkMode ? 'Тема: Темная' : 'Тема: Светлая'}
|
||||
>
|
||||
{darkMode ? <IconDarkTheme size='0.75rem' /> : null}
|
||||
{!darkMode ? <IconLightTheme size='0.75rem' /> : null}
|
||||
{darkMode ? (
|
||||
<IconDarkTheme size='0.75rem' className='hover:text-primary cc-animate-color cc-hover-pulse' />
|
||||
) : null}
|
||||
{!darkMode ? (
|
||||
<IconLightTheme size='0.75rem' className='hover:text-primary cc-animate-color cc-hover-pulse' />
|
||||
) : null}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
|
@ -18,7 +18,6 @@ export function UserButton({ onLogin, onClickUser, isOpen }: UserButtonProps) {
|
|||
if (isAnonymous) {
|
||||
return (
|
||||
<NavigationButton
|
||||
className='cc-fade-in'
|
||||
title='Перейти на страницу логина'
|
||||
icon={<IconLogin size='1.5rem' className='icon-primary' />}
|
||||
onClick={onLogin}
|
||||
|
@ -27,7 +26,6 @@ export function UserButton({ onLogin, onClickUser, isOpen }: UserButtonProps) {
|
|||
} else {
|
||||
return (
|
||||
<NavigationButton
|
||||
className='cc-fade-in'
|
||||
title='Пользователь'
|
||||
hideTitle={isOpen}
|
||||
aria-haspopup='true'
|
||||
|
|
|
@ -40,7 +40,7 @@ export function Button({
|
|||
className={cn(
|
||||
'inline-flex gap-2 items-center justify-center',
|
||||
'font-medium select-none disabled:cursor-auto disabled:opacity-75',
|
||||
'bg-secondary text-secondary-foreground cc-hover cc-animate-color',
|
||||
'bg-secondary text-secondary-foreground cc-hover-bg cc-animate-color',
|
||||
dense ? 'px-1' : 'px-3 py-1',
|
||||
loading ? 'cursor-progress' : 'cursor-pointer',
|
||||
noOutline ? 'outline-hidden focus-visible:bg-selected' : 'focus-outline',
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
import { type Button } from '../props';
|
||||
import { cn } from '../utils';
|
||||
|
||||
interface MiniButtonProps extends Button {
|
||||
/** Button type. */
|
||||
|
@ -37,11 +36,12 @@ export function MiniButton({
|
|||
<button
|
||||
type={type}
|
||||
tabIndex={tabIndex ?? -1}
|
||||
className={clsx(
|
||||
className={cn(
|
||||
'rounded-lg',
|
||||
'cc-controls cc-animate-background',
|
||||
'cursor-pointer disabled:cursor-auto',
|
||||
noHover ? 'outline-hidden' : 'cc-hover',
|
||||
'text-muted-foreground cc-animate-color',
|
||||
'cursor-pointer disabled:cursor-auto disabled:opacity-75',
|
||||
(!tabIndex || tabIndex === -1) && 'outline-hidden',
|
||||
!noHover && 'cc-hover-pulse',
|
||||
!noPadding && 'px-1 py-1',
|
||||
className
|
||||
)}
|
||||
|
|
|
@ -30,9 +30,9 @@ export function SelectorButton({
|
|||
className={cn(
|
||||
'px-1 flex flex-start items-center gap-1',
|
||||
'text-sm font-controls select-none',
|
||||
'text-btn cc-controls',
|
||||
'disabled:cursor-auto cursor-pointer',
|
||||
'cc-hover cc-animate-color',
|
||||
'disabled:cursor-auto cursor-pointer outline-hidden',
|
||||
'text-muted-foreground cc-hover-text cc-animate-color disabled:opacity-75',
|
||||
!text && 'cc-hover-pulse',
|
||||
className
|
||||
)}
|
||||
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
||||
|
|
|
@ -20,8 +20,10 @@ export function PaginationTools<TData>({
|
|||
onChangePaginationOption,
|
||||
paginationOptions
|
||||
}: PaginationToolsProps<TData>) {
|
||||
const buttonClass =
|
||||
'cc-hover-text cc-animate-color focus-outline rounded-md disabled:opacity-75 not-[:disabled]:cursor-pointer';
|
||||
return (
|
||||
<div className='flex justify-end items-center my-2 text-sm cc-controls select-none'>
|
||||
<div className='flex justify-end items-center my-2 text-muted-foreground text-sm select-none'>
|
||||
<span className='mr-3'>
|
||||
{`${table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1}
|
||||
-
|
||||
|
@ -36,7 +38,7 @@ export function PaginationTools<TData>({
|
|||
<button
|
||||
type='button'
|
||||
aria-label='Первая страница'
|
||||
className='cc-hover cc-controls cc-animate-color focus-outline'
|
||||
className={buttonClass}
|
||||
onClick={() => table.setPageIndex(0)}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
|
@ -45,7 +47,7 @@ export function PaginationTools<TData>({
|
|||
<button
|
||||
type='button'
|
||||
aria-label='Предыдущая страница'
|
||||
className='cc-hover cc-controls cc-animate-color focus-outline'
|
||||
className={buttonClass}
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
|
@ -55,7 +57,7 @@ export function PaginationTools<TData>({
|
|||
id={id ? `${id}__page` : undefined}
|
||||
title='Номер страницы. Выделите для ручного ввода'
|
||||
aria-label='Номер страницы'
|
||||
className='w-6 text-center bg-transparent focus-outline'
|
||||
className='w-6 text-center bg-transparent focus-outline rounded-md'
|
||||
value={table.getState().pagination.pageIndex + 1}
|
||||
onChange={event => {
|
||||
const page = event.target.value ? Number(event.target.value) - 1 : 0;
|
||||
|
@ -67,7 +69,7 @@ export function PaginationTools<TData>({
|
|||
<button
|
||||
type='button'
|
||||
aria-label='Следующая страница'
|
||||
className='cc-hover cc-controls cc-animate-color focus-outline'
|
||||
className={buttonClass}
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
|
@ -76,7 +78,7 @@ export function PaginationTools<TData>({
|
|||
<button
|
||||
type='button'
|
||||
aria-label='Последняя страница'
|
||||
className='cc-hover cc-controls cc-animate-color focus-outline'
|
||||
className={buttonClass}
|
||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import { useCallback } from 'react';
|
||||
import { type Table } from '@tanstack/react-table';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { prefixes } from '@/utils/constants';
|
||||
|
||||
|
@ -30,7 +31,12 @@ export function SelectPagination<TData>({ id, table, paginationOptions, onChange
|
|||
<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'
|
||||
className={clsx(
|
||||
'w-28 max-h-6 mx-2',
|
||||
'px-2 justify-end',
|
||||
'bg-transparent cc-hover-text cc-animate-color focus-outline border-0 rounded-md',
|
||||
'cursor-pointer'
|
||||
)}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
|
|
@ -70,7 +70,7 @@ export function TableRow<TData>({
|
|||
<tr
|
||||
className={cn(
|
||||
'cc-scroll-row',
|
||||
'cc-hover cc-animate-background duration-fade',
|
||||
'cc-hover-bg cc-animate-background duration-fade',
|
||||
!noHeader && 'scroll-mt-[calc(2px+2rem)]',
|
||||
table.options.enableRowSelection && row.getIsSelected()
|
||||
? 'cc-selected'
|
||||
|
|
|
@ -36,9 +36,9 @@ export function DropdownButton({
|
|||
className={cn(
|
||||
'px-3 py-1 inline-flex items-center gap-2',
|
||||
'text-left text-sm text-ellipsis whitespace-nowrap',
|
||||
'disabled:cc-controls disabled:opacity-75',
|
||||
'disabled:text-muted-foreground disabled:opacity-75',
|
||||
'focus-outline cc-animate-background',
|
||||
!!onClick ? 'cc-hover cursor-pointer disabled:cursor-auto' : 'bg-secondary text-secondary-foreground',
|
||||
!!onClick ? 'cc-hover-bg cursor-pointer disabled:cursor-auto' : 'bg-secondary text-secondary-foreground',
|
||||
className
|
||||
)}
|
||||
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
||||
|
|
|
@ -25,6 +25,10 @@ export { LuQrCode as IconQR } from 'react-icons/lu';
|
|||
export { LuFilterX as IconFilterReset } from 'react-icons/lu';
|
||||
export {BiDownArrowCircle as IconOpenList } from 'react-icons/bi';
|
||||
export { LuTriangleAlert as IconAlert } from 'react-icons/lu';
|
||||
export { LuPanelLeftOpen as IconLeftOpen } from 'react-icons/lu';
|
||||
export { LuPanelLeftClose as IconLeftClose } from 'react-icons/lu';
|
||||
export { LuPanelBottomOpen as IconBottomOpen } from 'react-icons/lu';
|
||||
export { LuPanelBottomClose as IconBottomClose } from 'react-icons/lu';
|
||||
|
||||
// ===== UI elements =======
|
||||
export { BiX as IconClose } from 'react-icons/bi';
|
||||
|
@ -97,9 +101,7 @@ export { LuDatabase as IconDatabase } from 'react-icons/lu';
|
|||
export { LuView as IconDBStructure } from 'react-icons/lu';
|
||||
export { LuPlaneTakeoff as IconRESTapi } from 'react-icons/lu';
|
||||
export { LuImage as IconImage } from 'react-icons/lu';
|
||||
export { TbColumns as IconList } from 'react-icons/tb';
|
||||
export { GoVersions as IconVersions } from 'react-icons/go';
|
||||
export { TbColumnsOff as IconListOff } from 'react-icons/tb';
|
||||
export { LuAtSign as IconTerm } from 'react-icons/lu';
|
||||
export { LuSubscript as IconAlias } from 'react-icons/lu';
|
||||
export { TbMathFunction as IconFormula } from 'react-icons/tb';
|
||||
|
@ -107,7 +109,8 @@ 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 { RiTreeLine as IconTree } from 'react-icons/ri';
|
||||
export { FaRegKeyboard as IconControls } from 'react-icons/fa6';
|
||||
export { LuKeyboard as IconKeyboard } from 'react-icons/lu';
|
||||
export { LuKeyboardOff as IconKeyboardOff } from 'react-icons/lu';
|
||||
export { RiLockLine as IconImmutable } from 'react-icons/ri';
|
||||
export { RiLockUnlockLine as IconMutable } from 'react-icons/ri';
|
||||
export { RiOpenSourceLine as IconPublic } from 'react-icons/ri';
|
||||
|
@ -141,7 +144,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';
|
||||
|
|
|
@ -92,7 +92,12 @@ export function ComboBox<Option>({
|
|||
hidden={hidden && !open}
|
||||
>
|
||||
<span className='truncate'>{value ? labelValueFunc(value) : placeholder}</span>
|
||||
<ChevronDownIcon className={cn('text-muted-foreground', clearable && !!value && 'opacity-0')} />
|
||||
<ChevronDownIcon
|
||||
className={cn(
|
||||
'text-muted-foreground cc-hover-pulse hover:text-primary',
|
||||
clearable && !!value && 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
{clearable && !!value ? (
|
||||
<IconRemove
|
||||
tabIndex={-1}
|
||||
|
|
|
@ -115,7 +115,7 @@ export function ComboMulti<Option>({
|
|||
<IconRemove
|
||||
tabIndex={-1}
|
||||
size='1rem'
|
||||
className='cc-remove absolute pointer-events-auto right-3'
|
||||
className='cc-remove absolute pointer-events-auto right-3 cc-hover-pulse hover:text-primary'
|
||||
onClick={handleClear}
|
||||
/>
|
||||
) : null}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { type FieldError, type GlobalError } from 'react-hook-form';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { type Styling } from '../props';
|
||||
import { cn } from '../utils';
|
||||
|
||||
interface ErrorFieldProps extends Styling {
|
||||
error?: FieldError | GlobalError;
|
||||
|
@ -15,7 +15,7 @@ export function ErrorField({ error, className, ...restProps }: ErrorFieldProps):
|
|||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={clsx('text-sm text-destructive select-none', className)} {...restProps}>
|
||||
<div className={cn('text-sm text-destructive select-none', className)} {...restProps}>
|
||||
{error.message}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -90,7 +90,7 @@ export function SelectTree<ItemType>({
|
|||
<div
|
||||
key={`${prefix}${index}`}
|
||||
className={clsx(
|
||||
'cc-tree-item relative cc-scroll-row cc-hover',
|
||||
'cc-tree-item relative cc-scroll-row cc-hover-bg',
|
||||
isActive ? 'max-h-7 py-1 border-b' : 'max-h-0 opacity-0 pointer-events-none',
|
||||
value === item && 'cc-selected'
|
||||
)}
|
||||
|
@ -101,9 +101,8 @@ export function SelectTree<ItemType>({
|
|||
{foldable.has(item) ? (
|
||||
<MiniButton
|
||||
aria-label={!folded.includes(item) ? 'Свернуть' : 'Развернуть'}
|
||||
className={clsx('absolute left-1', !folded.includes(item) ? 'top-1.5' : 'top-1')}
|
||||
className={clsx('absolute left-1 hover:text-primary', !folded.includes(item) ? 'top-1.5' : 'top-1')}
|
||||
noPadding
|
||||
noHover
|
||||
icon={!folded.includes(item) ? <IconDropArrow size='1rem' /> : <IconPageRight size='1.25rem' />}
|
||||
onClick={event => handleClickFold(event, item)}
|
||||
/>
|
||||
|
|
|
@ -46,7 +46,7 @@ function SelectTrigger({
|
|||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className='size-4' />
|
||||
<ChevronDownIcon className='size-4 cc-hover-pulse hover:text-primary' />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
);
|
||||
|
@ -154,7 +154,7 @@ function SelectScrollDownButton({
|
|||
className={cn('flex cursor-default items-center justify-center py-1', className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className='size-4' />
|
||||
<ChevronDownIcon className='size-4 cc-hover-pulse hover:text-primary' />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ export function ModalBackdrop({ onHide }: ModalBackdropProps) {
|
|||
return (
|
||||
<>
|
||||
<div className='z-bottom fixed inset-0 backdrop-blur-[3px] opacity-50' />
|
||||
<div className='z-bottom fixed inset-0 bg-popover opacity-25' onClick={onHide} />
|
||||
<div className='z-bottom fixed inset-0 bg-foreground opacity-5' onClick={onHide} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { type HelpTopic } from '@/features/help';
|
||||
import { BadgeHelp } from '@/features/help/components';
|
||||
import { BadgeHelp } from '@/features/help/components/badge-help';
|
||||
|
||||
import { useEscapeKey } from '@/hooks/use-escape-key';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
|
@ -89,7 +89,7 @@ export function ModalForm({
|
|||
<div className='cc-modal-wrapper'>
|
||||
<ModalBackdrop onHide={handleCancel} />
|
||||
<form
|
||||
className='cc-animate-modal relative grid border rounded-xl bg-background'
|
||||
className='cc-animate-modal relative grid border-2 px-1 pb-1 rounded-xl bg-background'
|
||||
role='dialog'
|
||||
onSubmit={handleSubmit}
|
||||
aria-labelledby='modal-title'
|
||||
|
|
|
@ -6,7 +6,7 @@ export function ModalLoader() {
|
|||
return (
|
||||
<div className='cc-modal-wrapper'>
|
||||
<ModalBackdrop />
|
||||
<div className='cc-animate-modal p-20 border rounded-xl bg-background'>
|
||||
<div className='cc-animate-modal p-20 border-2 rounded-xl bg-background'>
|
||||
<Loader circular scale={6} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { BadgeHelp } from '@/features/help/components';
|
||||
import { BadgeHelp } from '@/features/help/components/badge-help';
|
||||
|
||||
import { useEscapeKey } from '@/hooks/use-escape-key';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
|
@ -39,7 +39,7 @@ export function ModalView({
|
|||
return (
|
||||
<div className='cc-modal-wrapper'>
|
||||
<ModalBackdrop onHide={hideDialog} />
|
||||
<div className='cc-animate-modal relative grid border rounded-xl bg-background' role='dialog'>
|
||||
<div className='cc-animate-modal relative grid border-2 px-1 pb-1 rounded-xl bg-background' role='dialog'>
|
||||
{helpTopic && !hideHelpWhen?.() ? (
|
||||
<BadgeHelp
|
||||
topic={helpTopic}
|
||||
|
|
|
@ -22,6 +22,7 @@ export function TabLabel({
|
|||
className,
|
||||
disabled,
|
||||
role = 'tab',
|
||||
selectedClassName = 'text-foreground! bg-secondary',
|
||||
...otherProps
|
||||
}: TabLabelProps) {
|
||||
return (
|
||||
|
@ -29,12 +30,12 @@ export function TabLabel({
|
|||
className={clsx(
|
||||
'min-w-20 h-full',
|
||||
'px-2 py-1 flex justify-center',
|
||||
'cc-animate-color duration-select',
|
||||
'cc-animate-color duration-select text-muted-foreground',
|
||||
'text-sm whitespace-nowrap font-controls',
|
||||
'select-none',
|
||||
'outline-hidden',
|
||||
!disabled && 'hover:cursor-pointer cc-hover',
|
||||
disabled && 'text-muted-foreground',
|
||||
!disabled && 'hover:cursor-pointer cc-hover-text',
|
||||
disabled && 'bg-secondary',
|
||||
className
|
||||
)}
|
||||
tabIndex='-1'
|
||||
|
@ -44,6 +45,7 @@ export function TabLabel({
|
|||
data-tooltip-hidden={hideTitle}
|
||||
role={role}
|
||||
disabled={disabled}
|
||||
selectedClassName={selectedClassName}
|
||||
{...otherProps}
|
||||
>
|
||||
{label}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { type Styling, type Titled } from '@/components/props';
|
||||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
import { cn } from '../utils';
|
||||
|
||||
interface IndicatorProps extends Titled, Styling {
|
||||
/** Icon to display. */
|
||||
icon: React.ReactNode;
|
||||
|
@ -17,8 +17,8 @@ interface IndicatorProps extends Titled, Styling {
|
|||
export function Indicator({ icon, title, titleHtml, hideTitle, noPadding, className, ...restProps }: IndicatorProps) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'cc-controls', //
|
||||
className={cn(
|
||||
'text-muted-foreground', //
|
||||
'outline-hidden',
|
||||
!noPadding && 'px-1 py-1',
|
||||
className
|
||||
|
|
|
@ -57,7 +57,7 @@ export function ValueIcon({
|
|||
data-tooltip-hidden={hideTitle}
|
||||
aria-label={title}
|
||||
>
|
||||
{onClick ? <MiniButton noHover noPadding icon={icon} onClick={onClick} disabled={disabled} /> : icon}
|
||||
{onClick ? <MiniButton noPadding icon={icon} onClick={onClick} disabled={disabled} /> : icon}
|
||||
<span id={id}>{value}</span>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
export { ExpectedAnonymous } from './expected-anonymous';
|
||||
export { RequireAuth } from './require-auth';
|
|
@ -42,7 +42,7 @@ export function BadgeHelp({ topic, padding = 'p-1', className, contentClass, sty
|
|||
}
|
||||
return (
|
||||
<div tabIndex={-1} id={`help-${topic}`} className={cn(padding, className)} style={style}>
|
||||
<IconHelp size='1.25rem' className='icon-primary' />
|
||||
<IconHelp size='1.25rem' className='text-muted-foreground hover:text-primary cc-animate-color' />
|
||||
<Tooltip
|
||||
clickable
|
||||
anchorSelect={`#help-${topic}`}
|
||||
|
|
|
@ -1 +1 @@
|
|||
export { BadgeHelp } from './badge-help';
|
||||
|
|
@ -12,26 +12,30 @@ export function HelpFormulaTree() {
|
|||
</ul>
|
||||
|
||||
<h2>Виды узлов</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<span className='bg-(--acc-bg-green)'>объявление идентификатора</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className='bg-(--acc-bg-teal)'>глобальный идентификатор</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className='bg-(--acc-bg-orange)'>логическое выражение</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className='bg-(--acc-bg-blue)'>типизированное выражение</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className='bg-(--acc-bg-red)'>присвоение и итерация</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className='bg-secondary'>составные выражения</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p className='m-0'>
|
||||
<span className='cc-sample-color bg-(--acc-bg-green)' />
|
||||
объявление идентификатора
|
||||
</p>
|
||||
<p className='m-0'>
|
||||
<span className='cc-sample-color bg-(--acc-bg-teal)' />
|
||||
глобальный идентификатор
|
||||
</p>
|
||||
<p className='m-0'>
|
||||
<span className='cc-sample-color bg-(--acc-bg-orange)' />
|
||||
логическое выражение
|
||||
</p>
|
||||
<p className='m-0'>
|
||||
<span className='cc-sample-color bg-(--acc-bg-blue)' />
|
||||
типизированное выражение
|
||||
</p>
|
||||
<p className='m-0'>
|
||||
<span className='cc-sample-color bg-(--acc-bg-red)' />
|
||||
присвоение и итерация
|
||||
</p>
|
||||
<p className='m-0'>
|
||||
<span className='cc-sample-color bg-secondary' />
|
||||
составные выражения
|
||||
</p>
|
||||
|
||||
<h2>Команды</h2>
|
||||
<ul>
|
||||
|
|
|
@ -62,7 +62,7 @@ export function HelpLibrary() {
|
|||
<IconFilterReset size='1rem' className='inline-icon' /> сбросить фильтры
|
||||
</li>
|
||||
<li>
|
||||
<IconFolderTree size='1rem' className='inline-icon' /> переключение между Проводник и Поиск
|
||||
<IconFolderTree size='1rem' className='inline-icon' /> переключение между Проводник и Таблица
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
|
|
@ -11,8 +11,8 @@ import {
|
|||
IconEdit2,
|
||||
IconExecute,
|
||||
IconFitImage,
|
||||
IconFixLayout,
|
||||
IconGrid,
|
||||
IconLeftOpen,
|
||||
IconLineStraight,
|
||||
IconLineWave,
|
||||
IconNewItem,
|
||||
|
@ -31,7 +31,7 @@ export function HelpOssGraph() {
|
|||
<div className='flex flex-col'>
|
||||
<h1 className='sm:pr-24'>Граф синтеза</h1>
|
||||
<div className='flex flex-col sm:flex-row'>
|
||||
<div className='sm:w-56'>
|
||||
<div className='sm:w-64'>
|
||||
<h2>Настройка графа</h2>
|
||||
<ul>
|
||||
<li>
|
||||
|
@ -41,7 +41,7 @@ export function HelpOssGraph() {
|
|||
<IconFitImage className='inline-icon' /> Вписать в экран
|
||||
</li>
|
||||
<li>
|
||||
<IconFixLayout className='inline-icon' /> Исправить расположения
|
||||
<IconLeftOpen className='inline-icon' /> Панель связанной КС
|
||||
</li>
|
||||
<li>
|
||||
<IconSettings className='inline-icon' /> Диалог настроек
|
||||
|
@ -70,7 +70,7 @@ export function HelpOssGraph() {
|
|||
|
||||
<Divider vertical margins='mx-3 mt-3' className='hidden sm:block' />
|
||||
|
||||
<div className='sm:w-84'>
|
||||
<div className='sm:w-76'>
|
||||
<h2>Изменение узлов</h2>
|
||||
<ul>
|
||||
<li>
|
||||
|
@ -101,7 +101,7 @@ export function HelpOssGraph() {
|
|||
<Divider margins='my-2' className='hidden sm:block' />
|
||||
|
||||
<div className='flex flex-col-reverse mb-3 sm:flex-row'>
|
||||
<div className='sm:w-56'>
|
||||
<div className='sm:w-64'>
|
||||
<h2>Общие</h2>
|
||||
<ul>
|
||||
<li>
|
||||
|
@ -111,14 +111,14 @@ export function HelpOssGraph() {
|
|||
<kbd>Space</kbd> – перемещение экрана
|
||||
</li>
|
||||
<li>
|
||||
<kbd>Shift</kbd> – перемещение выделенных элементов в границах родителя
|
||||
<kbd>Shift</kbd> – перемещение в границах блока
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<Divider vertical margins='mx-3' className='hidden sm:block' />
|
||||
|
||||
<div className='dense w-84'>
|
||||
<div className='dense w-76'>
|
||||
<h2>Контекстное меню</h2>
|
||||
<ul>
|
||||
<li>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import {
|
||||
IconClone,
|
||||
IconDestroy,
|
||||
IconDownload,
|
||||
IconEditor,
|
||||
IconImmutable,
|
||||
IconLeftOpen,
|
||||
IconOSS,
|
||||
IconOwner,
|
||||
IconPublic,
|
||||
|
@ -16,17 +15,17 @@ import { HelpTopic } from '../../models/help-topic';
|
|||
export function HelpRSCard() {
|
||||
return (
|
||||
<div className='dense'>
|
||||
<h1>Карточка схемы</h1>
|
||||
<h1>Паспорт схемы</h1>
|
||||
|
||||
<p>Карточка содержит общую информацию и статистику</p>
|
||||
<p>Паспорт содержит общую информацию и статистику</p>
|
||||
<p>
|
||||
Карточка позволяет управлять атрибутами и <LinkTopic text='версиями' topic={HelpTopic.VERSIONS} />
|
||||
Паспорт позволяет управлять атрибутами и <LinkTopic text='версиями' topic={HelpTopic.VERSIONS} />
|
||||
</p>
|
||||
<p>
|
||||
Карточка позволяет назначать <IconEditor className='inline-icon' /> Редакторов
|
||||
Паспорт позволяет назначать <IconEditor className='inline-icon' /> Редакторов
|
||||
</p>
|
||||
<p>
|
||||
Карточка позволяет изменить <IconOwner className='inline-icon icon-green' /> Владельца
|
||||
Паспорт позволяет изменить <IconOwner className='inline-icon icon-green' /> Владельца
|
||||
</p>
|
||||
|
||||
<h2>Управление</h2>
|
||||
|
@ -49,15 +48,12 @@ export function HelpRSCard() {
|
|||
<li>
|
||||
<IconImmutable className='inline-icon' /> Неизменные схемы
|
||||
</li>
|
||||
<li>
|
||||
<IconClone className='inline-icon icon-green' /> Клонировать – создать копию схемы
|
||||
</li>
|
||||
<li>
|
||||
<IconDownload className='inline-icon' /> Загрузить/Выгрузить – взаимодействие с Экстеор
|
||||
</li>
|
||||
<li>
|
||||
<IconDestroy className='inline-icon icon-red' /> Удалить – полностью удаляет схему из базы Портала
|
||||
</li>
|
||||
<li>
|
||||
<IconLeftOpen className='inline-icon' /> Отображение статистики
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import {
|
||||
IconChild,
|
||||
IconClone,
|
||||
IconControls,
|
||||
IconDestroy,
|
||||
IconEdit,
|
||||
IconFilter,
|
||||
IconList,
|
||||
IconKeyboard,
|
||||
IconLeftOpen,
|
||||
IconMoveDown,
|
||||
IconMoveUp,
|
||||
IconNewItem,
|
||||
|
@ -37,7 +37,7 @@ export function HelpRSEditor() {
|
|||
<IconPredecessor className='inline-icon' /> переход к исходной
|
||||
</li>
|
||||
<li>
|
||||
<IconList className='inline-icon' /> список конституент
|
||||
<IconLeftOpen className='inline-icon' /> список конституент
|
||||
</li>
|
||||
<li>
|
||||
<IconSave className='inline-icon' /> сохранить: <kbd>Ctrl + S</kbd>
|
||||
|
@ -72,17 +72,16 @@ export function HelpRSEditor() {
|
|||
<IconChild className='inline-icon' /> отображение наследованных
|
||||
</li>
|
||||
<li>
|
||||
<span className='bg-selected'>текущая конституента</span>
|
||||
<span className='cc-sample-color bg-selected' />
|
||||
выбранная конституента
|
||||
</li>
|
||||
<li>
|
||||
<span className='bg-accent-green50'>
|
||||
<LinkTopic text='основа' topic={HelpTopic.CC_RELATIONS} /> текущей
|
||||
</span>
|
||||
<span className='cc-sample-color bg-accent-green50' />
|
||||
<LinkTopic text='основа' topic={HelpTopic.CC_RELATIONS} /> выбранной
|
||||
</li>
|
||||
<li>
|
||||
<span className='bg-accent-orange50'>
|
||||
<LinkTopic text='порожденные' topic={HelpTopic.CC_RELATIONS} /> текущей
|
||||
</span>
|
||||
<span className='cc-sample-color bg-accent-orange50' />
|
||||
<LinkTopic text='порожденные' topic={HelpTopic.CC_RELATIONS} /> выбранной
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -94,7 +93,7 @@ export function HelpRSEditor() {
|
|||
<IconStatusOK className='inline-icon' /> индикатор статуса определения сверху
|
||||
</li>
|
||||
<li>
|
||||
<IconControls className='inline-icon' /> специальная клавиатура и горячие клавиши
|
||||
<IconKeyboard className='inline-icon' /> специальная клавиатура и горячие клавиши
|
||||
</li>
|
||||
<li>
|
||||
<IconTypeGraph className='inline-icon' /> отображение{' '}
|
||||
|
|
|
@ -32,18 +32,17 @@ export function HelpRSMenu() {
|
|||
<h2>Вкладки</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<LinkTopic text='Карточка' topic={HelpTopic.UI_RS_CARD} /> – редактирование атрибутов схемы и версии
|
||||
<LinkTopic text='Паспорт' topic={HelpTopic.UI_RS_CARD} /> – редактирование атрибутов схемы и версии
|
||||
</li>
|
||||
<li>
|
||||
<LinkTopic text='Содержание' topic={HelpTopic.UI_RS_LIST} /> – работа со списком конституент в табличной форме
|
||||
<LinkTopic text='Список' topic={HelpTopic.UI_RS_LIST} /> – работа со списком конституент в табличной форме
|
||||
</li>
|
||||
<li>
|
||||
<LinkTopic text='Редактор' topic={HelpTopic.UI_RS_EDITOR} /> – редактирование отдельной{' '}
|
||||
<LinkTopic text='Понятие' topic={HelpTopic.UI_RS_EDITOR} /> – редактирование отдельной{' '}
|
||||
<LinkTopic text='Конституенты' topic={HelpTopic.CC_CONSTITUENTA} />
|
||||
</li>
|
||||
<li>
|
||||
<LinkTopic text='Граф термов' topic={HelpTopic.UI_GRAPH_TERM} /> – графическое представление связей
|
||||
конституент
|
||||
<LinkTopic text='Граф' topic={HelpTopic.UI_GRAPH_TERM} /> – графическое представление связей конституент
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
|
|
@ -23,17 +23,18 @@ export function HelpTypeGraph() {
|
|||
|
||||
<h2>Цвета узлов</h2>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<span className='bg-secondary'>ступень-основание</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className='bg-accent-teal'>ступень-булеан</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className='bg-accent-orange'>ступень декартова произведения</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p className='m-0'>
|
||||
<span className='cc-sample-color bg-secondary' />
|
||||
ступень-основание
|
||||
</p>
|
||||
<p className='m-0'>
|
||||
<span className='cc-sample-color bg-accent-teal' />
|
||||
ступень-булеан
|
||||
</p>
|
||||
<p className='m-0'>
|
||||
<span className='cc-sample-color bg-accent-orange' />
|
||||
ступень декартова произведения
|
||||
</p>
|
||||
|
||||
<h2>Команды</h2>
|
||||
<ul>
|
||||
|
|
|
@ -13,7 +13,7 @@ export function labelHelpTopic(topic: HelpTopic): string {
|
|||
case HelpTopic.INTERFACE: return '🌀 Интерфейс';
|
||||
case HelpTopic.UI_LIBRARY: return 'Библиотека';
|
||||
case HelpTopic.UI_RS_MENU: return 'Меню схемы';
|
||||
case HelpTopic.UI_RS_CARD: return 'Карточка схемы';
|
||||
case HelpTopic.UI_RS_CARD: return 'Паспорт схемы';
|
||||
case HelpTopic.UI_RS_LIST: return 'Список конституент';
|
||||
case HelpTopic.UI_RS_EDITOR: return 'Редактор конституенты';
|
||||
case HelpTopic.UI_GRAPH_TERM: return 'Граф термов';
|
||||
|
|
|
@ -3,7 +3,8 @@ import { useIntl } from 'react-intl';
|
|||
|
||||
import { urls, useConceptNavigation } from '@/app';
|
||||
import { useLabelUser, useRoleStore, UserRole } from '@/features/users';
|
||||
import { InfoUsers, SelectUser } from '@/features/users/components';
|
||||
import { InfoUsers } from '@/features/users/components/info-users';
|
||||
import { SelectUser } from '@/features/users/components/select-user';
|
||||
|
||||
import { Tooltip } from '@/components/container';
|
||||
import { MiniButton } from '@/components/control';
|
||||
|
@ -86,7 +87,6 @@ export function EditorLibraryItem({ schema, isAttachedToOSS }: EditorLibraryItem
|
|||
<div className='relative flex justify-stretch sm:mb-1 max-w-120 gap-3'>
|
||||
<MiniButton
|
||||
title='Открыть в библиотеке'
|
||||
noHover
|
||||
noPadding
|
||||
icon={<IconFolderOpened size='1.25rem' className='icon-primary' />}
|
||||
onClick={handleOpenLibrary}
|
||||
|
@ -137,14 +137,14 @@ export function EditorLibraryItem({ schema, isAttachedToOSS }: EditorLibraryItem
|
|||
<ValueIcon
|
||||
title='Дата обновления'
|
||||
dense
|
||||
icon={<IconDateUpdate size='1.25rem' className='text-constructive' />}
|
||||
icon={<IconDateUpdate size='1.25rem' />}
|
||||
value={new Date(schema.time_update).toLocaleString(intl.locale)}
|
||||
/>
|
||||
|
||||
<ValueIcon
|
||||
title='Дата создания'
|
||||
dense
|
||||
icon={<IconDateCreate size='1.25rem' className='text-constructive' />}
|
||||
icon={<IconDateCreate size='1.25rem' />}
|
||||
value={new Date(schema.time_create).toLocaleString(intl.locale, {
|
||||
year: '2-digit',
|
||||
month: '2-digit',
|
||||
|
|
|
@ -1,21 +1,16 @@
|
|||
import { UserRole } from '@/features/users';
|
||||
|
||||
import { IconAdmin, IconEditor, IconOwner, IconReader } from '@/components/icons';
|
||||
import { type DomIconProps, IconAdmin, IconEditor, IconOwner, IconReader } from '@/components/icons';
|
||||
|
||||
interface IconRoleProps {
|
||||
role: UserRole;
|
||||
size?: string;
|
||||
}
|
||||
|
||||
export function IconRole({ role, size = '1.25rem' }: IconRoleProps) {
|
||||
switch (role) {
|
||||
export function IconRole({ value, size = '1.25rem', className }: DomIconProps<UserRole>) {
|
||||
switch (value) {
|
||||
case UserRole.ADMIN:
|
||||
return <IconAdmin size={size} className='icon-primary' />;
|
||||
return <IconAdmin size={size} className={className ?? 'icon-primary'} />;
|
||||
case UserRole.OWNER:
|
||||
return <IconOwner size={size} className='icon-primary' />;
|
||||
return <IconOwner size={size} className={className ?? 'icon-primary'} />;
|
||||
case UserRole.EDITOR:
|
||||
return <IconEditor size={size} className='icon-primary' />;
|
||||
return <IconEditor size={size} className={className ?? 'icon-primary'} />;
|
||||
case UserRole.READER:
|
||||
return <IconReader size={size} className='icon-primary' />;
|
||||
return <IconReader size={size} className={className ?? 'icon-primary'} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import { type DomIconProps, IconBottomClose, IconBottomOpen, IconLeftClose, IconLeftOpen } from '@/components/icons';
|
||||
|
||||
/** Icon for sidebar visibility. */
|
||||
export function IconShowSidebar({
|
||||
value,
|
||||
size = '1.25rem',
|
||||
className,
|
||||
isBottom
|
||||
}: DomIconProps<boolean> & { isBottom: boolean }) {
|
||||
if (isBottom) {
|
||||
if (value) {
|
||||
return <IconBottomClose size={size} className={className ?? 'icon-primary'} />;
|
||||
} else {
|
||||
return <IconBottomOpen size={size} className={className ?? 'icon-primary'} />;
|
||||
}
|
||||
} else {
|
||||
if (value) {
|
||||
return <IconLeftOpen size={size} className={className ?? 'icon-primary'} />;
|
||||
} else {
|
||||
return <IconLeftClose size={size} className={className ?? 'icon-primary'} />;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
export { EditorLibraryItem } from './editor-library-item';
|
||||
export { MenuRole } from './menu-role';
|
||||
export { MiniSelectorOSS } from './mini-selector-oss';
|
||||
export { PickSchema } from './pick-schema';
|
||||
export { SelectLibraryItem } from './select-library-item';
|
||||
export { SelectVersion } from './select-version';
|
||||
export { ToolbarItemAccess } from './toolbar-item-access';
|
||||
export { ToolbarItemCard } from './toolbar-item-card';
|
|
@ -3,7 +3,7 @@ import { useAuthSuspense } from '@/features/auth';
|
|||
import { useRoleStore, UserRole } from '@/features/users';
|
||||
import { describeUserRole, labelUserRole } from '@/features/users/labels';
|
||||
|
||||
import { Button } from '@/components/control';
|
||||
import { MiniButton } from '@/components/control';
|
||||
import { Dropdown, DropdownButton, useDropdown } from '@/components/dropdown';
|
||||
import { IconAlert } from '@/components/icons';
|
||||
|
||||
|
@ -29,13 +29,11 @@ export function MenuRole({ isOwned, isEditor }: MenuRoleProps) {
|
|||
|
||||
if (isAnonymous) {
|
||||
return (
|
||||
<Button
|
||||
dense
|
||||
noBorder
|
||||
noOutline
|
||||
<MiniButton
|
||||
noPadding
|
||||
titleHtml='<b>Анонимный режим</b><br />Войти в Портал'
|
||||
hideTitle={accessMenu.isOpen}
|
||||
className='h-full pr-2'
|
||||
className='h-full pr-2 pl-3 bg-transparent'
|
||||
icon={<IconAlert size='1.25rem' className='icon-red' />}
|
||||
onClick={() => router.push({ path: urls.login })}
|
||||
/>
|
||||
|
@ -44,44 +42,45 @@ export function MenuRole({ isOwned, isEditor }: MenuRoleProps) {
|
|||
|
||||
return (
|
||||
<div ref={accessMenu.ref} onBlur={accessMenu.handleBlur} className='relative'>
|
||||
<Button
|
||||
dense
|
||||
noBorder
|
||||
noOutline
|
||||
<MiniButton
|
||||
noHover
|
||||
noPadding
|
||||
title={`Режим ${labelUserRole(role)}`}
|
||||
hideTitle={accessMenu.isOpen}
|
||||
className='h-full pr-2'
|
||||
icon={<IconRole role={role} size='1.25rem' />}
|
||||
className='h-full pr-2 text-muted-foreground hover:text-primary cc-animate-color'
|
||||
icon={<IconRole value={role} size='1.25rem' className='' />}
|
||||
onClick={accessMenu.toggle}
|
||||
/>
|
||||
<Dropdown isOpen={accessMenu.isOpen} margin='mt-3'>
|
||||
<DropdownButton
|
||||
text={labelUserRole(UserRole.READER)}
|
||||
title={describeUserRole(UserRole.READER)}
|
||||
icon={<IconRole role={UserRole.READER} size='1rem' />}
|
||||
icon={<IconRole value={UserRole.READER} size='1rem' />}
|
||||
onClick={() => handleChangeMode(UserRole.READER)}
|
||||
/>
|
||||
<DropdownButton
|
||||
text={labelUserRole(UserRole.EDITOR)}
|
||||
title={describeUserRole(UserRole.EDITOR)}
|
||||
icon={<IconRole role={UserRole.EDITOR} size='1rem' />}
|
||||
icon={<IconRole value={UserRole.EDITOR} size='1rem' />}
|
||||
onClick={() => handleChangeMode(UserRole.EDITOR)}
|
||||
disabled={!isOwned && !isEditor}
|
||||
/>
|
||||
<DropdownButton
|
||||
text={labelUserRole(UserRole.OWNER)}
|
||||
title={describeUserRole(UserRole.OWNER)}
|
||||
icon={<IconRole role={UserRole.OWNER} size='1rem' />}
|
||||
icon={<IconRole value={UserRole.OWNER} size='1rem' />}
|
||||
onClick={() => handleChangeMode(UserRole.OWNER)}
|
||||
disabled={!isOwned}
|
||||
/>
|
||||
{user.is_staff ? (
|
||||
<DropdownButton
|
||||
text={labelUserRole(UserRole.ADMIN)}
|
||||
title={describeUserRole(UserRole.ADMIN)}
|
||||
icon={<IconRole role={UserRole.ADMIN} size='1rem' />}
|
||||
icon={<IconRole value={UserRole.ADMIN} size='1rem' />}
|
||||
onClick={() => handleChangeMode(UserRole.ADMIN)}
|
||||
disabled={!user.is_staff}
|
||||
/>
|
||||
) : null}
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -5,7 +5,7 @@ import clsx from 'clsx';
|
|||
|
||||
import { useAuthSuspense } from '@/features/auth';
|
||||
|
||||
import { Label, TextArea } from '@/components/input';
|
||||
import { TextArea } from '@/components/input';
|
||||
import { type Styling } from '@/components/props';
|
||||
|
||||
import { LocationHead } from '../../models/library';
|
||||
|
@ -35,18 +35,16 @@ export function PickLocation({
|
|||
const { user } = useAuthSuspense();
|
||||
|
||||
return (
|
||||
<div className={clsx('flex', className)} {...restProps}>
|
||||
<div className='flex flex-col gap-2 min-w-28'>
|
||||
<Label className='select-none' text='Корень' />
|
||||
<div className={clsx('flex relative', className)} {...restProps}>
|
||||
<SelectLocationHead
|
||||
className='absolute right-0 top-0'
|
||||
value={value.substring(0, 2) as LocationHead}
|
||||
onChange={newValue => onChange(combineLocation(newValue, value.substring(3)))}
|
||||
excluded={!user.is_staff ? [LocationHead.LIBRARY] : []}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SelectLocationContext
|
||||
className='-mt-1 -ml-8'
|
||||
className='absolute left-28 -top-1'
|
||||
dropdownHeight={dropdownHeight} //
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
|
@ -54,7 +52,7 @@ export function PickLocation({
|
|||
|
||||
<TextArea
|
||||
id='dlg_location'
|
||||
label='Путь'
|
||||
label='Расположение'
|
||||
rows={rows}
|
||||
value={value.substring(3)}
|
||||
onChange={event => onChange(combineLocation(value.substring(0, 2), event.target.value))}
|
||||
|
|
|
@ -38,13 +38,13 @@ export function SelectLocationContext({
|
|||
<div
|
||||
ref={menu.ref} //
|
||||
onBlur={menu.handleBlur}
|
||||
className={clsx('relative text-right self-start', className)}
|
||||
className={clsx('text-right self-start', className)}
|
||||
{...restProps}
|
||||
>
|
||||
<MiniButton
|
||||
title={title}
|
||||
hideTitle={menu.isOpen}
|
||||
icon={<IconFolderTree size='1.25rem' className='icon-green' />}
|
||||
icon={<IconFolderTree size='1.25rem' className='icon-primary' />}
|
||||
onClick={() => menu.toggle()}
|
||||
/>
|
||||
<Dropdown isOpen={menu.isOpen} className={clsx('w-80 z-tooltip', dropdownHeight)}>
|
||||
|
|
|
@ -48,7 +48,7 @@ export function SelectLocationHead({
|
|||
onClick={menu.toggle}
|
||||
/>
|
||||
|
||||
<Dropdown isOpen={menu.isOpen} margin='mt-2'>
|
||||
<Dropdown isOpen={menu.isOpen} stretchLeft margin='mt-2'>
|
||||
{Object.values(LocationHead)
|
||||
.filter(head => !excluded.includes(head))
|
||||
.map((head, index) => {
|
||||
|
|
|
@ -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-bg cc-animate-color duration-fade',
|
||||
'cursor-pointer',
|
||||
'leading-3 sm:leading-4',
|
||||
activeNode === item && 'cc-selected'
|
||||
|
@ -73,7 +73,6 @@ export function SelectLocation({ value, dense, prefix, onClick, className, style
|
|||
{item.children.size > 0 ? (
|
||||
<MiniButton
|
||||
noPadding
|
||||
noHover
|
||||
icon={
|
||||
folded.includes(item) ? (
|
||||
item.filesInside ? (
|
||||
|
@ -93,7 +92,7 @@ export function SelectLocation({ value, dense, prefix, onClick, className, style
|
|||
{item.filesInside ? (
|
||||
<IconFolder size='1rem' className='text-foreground' />
|
||||
) : (
|
||||
<IconFolderEmpty size='1rem' className='cc-controls' />
|
||||
<IconFolderEmpty size='1rem' className='text-foreground-muted' />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { HelpTopic } from '@/features/help';
|
||||
import { BadgeHelp } from '@/features/help/components';
|
||||
import { BadgeHelp } from '@/features/help/components/badge-help';
|
||||
import { useRoleStore, UserRole } from '@/features/users';
|
||||
|
||||
import { MiniButton } from '@/components/control';
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { urls, useConceptNavigation } from '@/app';
|
||||
import { HelpTopic } from '@/features/help';
|
||||
import { BadgeHelp } from '@/features/help/components';
|
||||
import { BadgeHelp } from '@/features/help/components/badge-help';
|
||||
import { type IRSForm } from '@/features/rsform';
|
||||
import { useRoleStore, UserRole } from '@/features/users';
|
||||
|
||||
|
@ -10,29 +10,46 @@ import { MiniButton } from '@/components/control';
|
|||
import { IconDestroy, IconSave, IconShare } from '@/components/icons';
|
||||
import { cn } from '@/components/utils';
|
||||
import { useModificationStore } from '@/stores/modification';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { tooltipText } from '@/utils/labels';
|
||||
import { prepareTooltip, sharePage } from '@/utils/utils';
|
||||
|
||||
import { AccessPolicy, type ILibraryItem, LibraryItemType } from '../backend/types';
|
||||
import { useMutatingLibrary } from '../backend/use-mutating-library';
|
||||
|
||||
import { IconShowSidebar } from './icon-show-sidebar';
|
||||
import { MiniSelectorOSS } from './mini-selector-oss';
|
||||
|
||||
interface ToolbarItemCardProps {
|
||||
className?: string;
|
||||
isNarrow: boolean;
|
||||
onSubmit: () => void;
|
||||
isMutable: boolean;
|
||||
schema: ILibraryItem;
|
||||
deleteSchema: () => void;
|
||||
}
|
||||
|
||||
export function ToolbarItemCard({ className, schema, onSubmit, isMutable, deleteSchema }: ToolbarItemCardProps) {
|
||||
export function ToolbarItemCard({
|
||||
className,
|
||||
isNarrow,
|
||||
schema,
|
||||
onSubmit,
|
||||
isMutable,
|
||||
deleteSchema
|
||||
}: ToolbarItemCardProps) {
|
||||
const role = useRoleStore(state => state.role);
|
||||
const router = useConceptNavigation();
|
||||
const { isModified } = useModificationStore();
|
||||
const isProcessing = useMutatingLibrary();
|
||||
const canSave = isModified && !isProcessing;
|
||||
|
||||
const showRSFormStats = usePreferencesStore(state => state.showRSFormStats);
|
||||
const toggleShowRSFormStats = usePreferencesStore(state => state.toggleShowRSFormStats);
|
||||
const showOSSStats = usePreferencesStore(state => state.showOSSStats);
|
||||
const toggleShowOSSStats = usePreferencesStore(state => state.toggleShowOSSStats);
|
||||
const isRSForm = schema.item_type === LibraryItemType.RSFORM;
|
||||
const isOSS = schema.item_type === LibraryItemType.OSS;
|
||||
|
||||
const ossSelector = (() => {
|
||||
if (schema.item_type !== LibraryItemType.RSFORM) {
|
||||
return null;
|
||||
|
@ -76,6 +93,15 @@ export function ToolbarItemCard({ className, schema, onSubmit, isMutable, delete
|
|||
disabled={!isMutable || isProcessing || role < UserRole.OWNER}
|
||||
/>
|
||||
) : null}
|
||||
{(isRSForm || isOSS) && (
|
||||
<MiniButton
|
||||
title='Отображение статистики'
|
||||
icon={
|
||||
<IconShowSidebar value={isRSForm ? showRSFormStats : showOSSStats} isBottom={isNarrow} size='1.25rem' />
|
||||
}
|
||||
onClick={isRSForm ? toggleShowRSFormStats : toggleShowOSSStats}
|
||||
/>
|
||||
)}
|
||||
<BadgeHelp topic={HelpTopic.UI_RS_CARD} offset={4} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
import { useUsers } from '@/features/users';
|
||||
import { SelectUser, TableUsers } from '@/features/users/components';
|
||||
import { SelectUser } from '@/features/users/components/select-user';
|
||||
import { TableUsers } from '@/features/users/components/table-users';
|
||||
|
||||
import { MiniButton } from '@/components/control';
|
||||
import { IconRemove } from '@/components/icons';
|
||||
|
@ -48,9 +49,8 @@ export function DlgEditEditors() {
|
|||
<span>Всего редакторов [{selected.length}]</span>
|
||||
<MiniButton
|
||||
title='Очистить список'
|
||||
noHover
|
||||
className='py-0 align-middle'
|
||||
icon={<IconRemove size='1.5rem' className='cc-remove' />}
|
||||
icon={<IconRemove size='1.25rem' className='cc-remove' />}
|
||||
onClick={() => setSelected([])}
|
||||
disabled={selected.length === 0}
|
||||
/>
|
||||
|
|
|
@ -63,7 +63,6 @@ export function TableVersions({ processing, items, onDelete, selected, onSelect
|
|||
<MiniButton
|
||||
title='Удалить версию'
|
||||
className='align-middle'
|
||||
noHover
|
||||
noPadding
|
||||
icon={<IconRemove size='1.25rem' className='cc-remove' />}
|
||||
onClick={event => handleDeleteVersion(event, props.row.original.id)}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { RequireAuth } from '@/features/auth/components';
|
||||
import { RequireAuth } from '@/features/auth/components/require-auth';
|
||||
|
||||
import { FormCreateItem } from './form-create-item';
|
||||
|
||||
|
|
|
@ -58,7 +58,7 @@ export function LibraryPage() {
|
|||
<MiniButton
|
||||
title='Выгрузить в формате CSV'
|
||||
className='absolute z-tooltip -top-8 right-6 hidden sm:block'
|
||||
icon={<IconCSV size='1.25rem' className='icon-green' />}
|
||||
icon={<IconCSV size='1.25rem' className='text-muted-foreground hover:text-constructive' />}
|
||||
onClick={handleDownloadCSV}
|
||||
/>
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { SelectUser } from '@/features/users/components';
|
||||
import { SelectUser } from '@/features/users/components/select-user';
|
||||
|
||||
import { MiniButton, SelectorButton } from '@/components/control';
|
||||
import { Dropdown, DropdownButton, useDropdown } from '@/components/dropdown';
|
||||
|
@ -156,28 +156,21 @@ export function ToolbarSearch({ className, total, filtered }: ToolbarSearchProps
|
|||
(head ? describeLocationHead(head) : 'Выберите каталог') + '<br/><kbd>Ctrl + клик</kbd> - Проводник'
|
||||
}
|
||||
hideTitle={headMenu.isOpen}
|
||||
icon={
|
||||
head ? (
|
||||
<IconLocationHead value={head} size='1.25rem' />
|
||||
) : (
|
||||
<IconFolderSearch size='1.25rem' className='cc-controls' />
|
||||
)
|
||||
}
|
||||
icon={head ? <IconLocationHead value={head} size='1.25rem' /> : <IconFolderSearch size='1.25rem' />}
|
||||
onClick={handleFolderClick}
|
||||
text={head ?? '//'}
|
||||
/>
|
||||
|
||||
<Dropdown isOpen={headMenu.isOpen} stretchLeft>
|
||||
<DropdownButton
|
||||
text='проводник...'
|
||||
title='Переключение в режим Проводник'
|
||||
icon={<IconFolderTree size='1rem' className='cc-controls' />}
|
||||
icon={<IconFolderTree size='1rem' className='icon-primary' />}
|
||||
onClick={handleToggleFolder}
|
||||
/>
|
||||
<DropdownButton
|
||||
text='отображать все'
|
||||
title='Очистить фильтр по расположению'
|
||||
icon={<IconFolder size='1rem' className='cc-controls' />}
|
||||
icon={<IconFolder size='1rem' className='icon-primary' />}
|
||||
onClick={() => handleChange(null)}
|
||||
/>
|
||||
{Object.values(LocationHead).map((head, index) => {
|
||||
|
@ -200,7 +193,7 @@ export function ToolbarSearch({ className, total, filtered }: ToolbarSearchProps
|
|||
placeholder='Путь'
|
||||
noIcon
|
||||
noBorder
|
||||
className='w-18 sm:w-20 grow'
|
||||
className='w-18 sm:w-20 grow ml-1'
|
||||
query={path}
|
||||
onChangeQuery={setPath}
|
||||
/>
|
||||
|
|
|
@ -39,10 +39,9 @@ export function useLibraryColumns() {
|
|||
titleHtml='Переключение в режим Проводник'
|
||||
aria-label='Переключатель режима Проводник'
|
||||
noPadding
|
||||
noHover
|
||||
className='pl-2 max-h-4 -translate-y-0.5'
|
||||
className='ml-2 max-h-4 -translate-y-0.5'
|
||||
onClick={handleToggleFolder}
|
||||
icon={<IconFolderTree size='1.25rem' className='cc-controls' />}
|
||||
icon={<IconFolderTree size='1.25rem' className='text-primary' />}
|
||||
/>
|
||||
),
|
||||
size: 50,
|
||||
|
|
|
@ -3,7 +3,7 @@ import clsx from 'clsx';
|
|||
|
||||
import { useAuthSuspense } from '@/features/auth';
|
||||
import { HelpTopic } from '@/features/help';
|
||||
import { BadgeHelp } from '@/features/help/components';
|
||||
import { BadgeHelp } from '@/features/help/components/badge-help';
|
||||
|
||||
import { MiniButton } from '@/components/control';
|
||||
import { IconFolderEdit, IconFolderTree } from '@/components/icons';
|
||||
|
@ -86,8 +86,8 @@ export function ViewSideLocation({ isVisible, onRenameLocation }: ViewSideLocati
|
|||
/>
|
||||
) : null}
|
||||
<MiniButton
|
||||
title='Переключение в режим Поиск'
|
||||
icon={<IconFolderTree size='1.25rem' className='icon-green' />}
|
||||
title='Переключение в режим Таблица'
|
||||
icon={<IconFolderTree size='1.25rem' className='text-primary' />}
|
||||
onClick={toggleFolderMode}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -50,7 +50,7 @@ export const ossApi = {
|
|||
axiosPatch({
|
||||
endpoint: `/api/oss/${itemID}/update-layout`,
|
||||
request: {
|
||||
data: data,
|
||||
data: { data: data },
|
||||
successMessage: isSilent ? undefined : infoMsg.changesSaved
|
||||
}
|
||||
}),
|
||||
|
|
|
@ -90,7 +90,7 @@ export class OssLoader {
|
|||
this.graph.topologicalOrder().forEach(operationID => {
|
||||
const operation = this.operationByID.get(operationID)!;
|
||||
const schema = this.items.find(item => item.id === operation.result);
|
||||
const position = this.oss.layout.operations.find(item => item.id === operationID);
|
||||
const position = this.oss.layout.find(item => item.nodeID === operation.nodeID);
|
||||
operation.x = position?.x ?? 0;
|
||||
operation.y = position?.y ?? 0;
|
||||
operation.is_consolidation = this.inferConsolidation(operationID);
|
||||
|
@ -104,7 +104,7 @@ export class OssLoader {
|
|||
|
||||
private inferBlockAttributes() {
|
||||
this.oss.blocks.forEach(block => {
|
||||
const geometry = this.oss.layout.blocks.find(item => item.id === block.id);
|
||||
const geometry = this.oss.layout.find(item => item.nodeID === block.nodeID);
|
||||
block.x = geometry?.x ?? 0;
|
||||
block.y = geometry?.y ?? 0;
|
||||
block.width = geometry?.width ?? BLOCK_NODE_MIN_WIDTH;
|
||||
|
|
|
@ -72,11 +72,8 @@ export type IRelocateConstituentsDTO = z.infer<typeof schemaRelocateConstituents
|
|||
/** Represents {@link IConstituenta} reference. */
|
||||
export type IConstituentaReference = z.infer<typeof schemaConstituentaReference>;
|
||||
|
||||
/** Represents {@link IOperation} position. */
|
||||
export type IOperationPosition = z.infer<typeof schemaOperationPosition>;
|
||||
|
||||
/** Represents {@link IBlock} position. */
|
||||
export type IBlockPosition = z.infer<typeof schemaBlockPosition>;
|
||||
/** Represents {@link IOperationSchema} node position. */
|
||||
export type INodePosition = z.infer<typeof schemaNodePosition>;
|
||||
|
||||
// ====== Schemas ======
|
||||
export const schemaOperationType = z.enum(Object.values(OperationType) as [OperationType, ...OperationType[]]);
|
||||
|
@ -108,24 +105,15 @@ export const schemaCstSubstituteInfo = schemaSubstituteConstituents.extend({
|
|||
substitution_term: z.string()
|
||||
});
|
||||
|
||||
export const schemaOperationPosition = z.strictObject({
|
||||
id: z.number(),
|
||||
x: z.number(),
|
||||
y: z.number()
|
||||
});
|
||||
|
||||
export const schemaBlockPosition = z.strictObject({
|
||||
id: z.number(),
|
||||
export const schemaNodePosition = z.strictObject({
|
||||
nodeID: z.string(),
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
width: z.number(),
|
||||
height: z.number()
|
||||
});
|
||||
|
||||
export const schemaOssLayout = z.strictObject({
|
||||
operations: z.array(schemaOperationPosition),
|
||||
blocks: z.array(schemaBlockPosition)
|
||||
});
|
||||
export const schemaOssLayout = z.array(schemaNodePosition);
|
||||
|
||||
export const schemaOperationSchema = schemaLibraryItem.extend({
|
||||
editors: z.number().array(),
|
||||
|
@ -188,6 +176,8 @@ export const schemaCreateOperation = z.strictObject({
|
|||
}),
|
||||
position_x: z.number(),
|
||||
position_y: z.number(),
|
||||
width: z.number(),
|
||||
height: z.number(),
|
||||
arguments: z.array(z.number()),
|
||||
create_schema: z.boolean()
|
||||
});
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
import { cn } from '@/components/utils';
|
||||
import { ValueStats } from '@/components/view';
|
||||
|
||||
import { type IOperationSchemaStats } from '../../../models/oss';
|
||||
import { type IOperationSchemaStats } from '../models/oss';
|
||||
|
||||
interface OssStatsProps {
|
||||
className?: string;
|
||||
|
@ -18,48 +18,43 @@ interface OssStatsProps {
|
|||
|
||||
export function OssStats({ className, stats }: OssStatsProps) {
|
||||
return (
|
||||
<div className={cn('grid grid-cols-4 gap-1 justify-items-end', className)}>
|
||||
<aside className={cn('grid grid-cols-4 gap-1 justify-items-end h-min', className)}>
|
||||
<div id='count_operations' className='w-fit flex gap-3 hover:cursor-default '>
|
||||
<span>Всего</span>
|
||||
<span>{stats.count_all}</span>
|
||||
</div>
|
||||
<ValueStats
|
||||
id='count_block'
|
||||
title='Блоки'
|
||||
icon={<IconConceptBlock size='1.25rem' className='text-primary' />}
|
||||
value={stats.count_block}
|
||||
/>
|
||||
<ValueStats id='count_block' title='Блоки' icon={<IconConceptBlock size='1.25rem' />} value={stats.count_block} />
|
||||
<ValueStats
|
||||
id='count_inputs'
|
||||
title='Загрузка'
|
||||
icon={<IconDownload size='1.25rem' className='text-primary' />}
|
||||
icon={<IconDownload size='1.25rem' />}
|
||||
value={stats.count_inputs}
|
||||
/>
|
||||
<ValueStats
|
||||
id='count_synthesis'
|
||||
title='Синтез'
|
||||
icon={<IconSynthesis size='1.25rem' className='text-primary' />}
|
||||
icon={<IconSynthesis size='1.25rem' />}
|
||||
value={stats.count_synthesis}
|
||||
/>
|
||||
|
||||
<ValueStats
|
||||
id='count_schemas'
|
||||
title='Прикрепленные схемы'
|
||||
icon={<IconRSForm size='1.25rem' className='text-primary' />}
|
||||
icon={<IconRSForm size='1.25rem' />}
|
||||
value={stats.count_schemas}
|
||||
/>
|
||||
<ValueStats
|
||||
id='count_owned'
|
||||
title='Собственные'
|
||||
icon={<IconRSFormOwned size='1.25rem' className='text-primary' />}
|
||||
icon={<IconRSFormOwned size='1.25rem' />}
|
||||
value={stats.count_owned}
|
||||
/>
|
||||
<ValueStats
|
||||
id='count_imported'
|
||||
title='Внешние'
|
||||
icon={<IconRSFormImported size='1.25rem' className='text-primary' />}
|
||||
icon={<IconRSFormImported size='1.25rem' />}
|
||||
value={stats.count_schemas - stats.count_owned}
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
|
@ -98,21 +98,18 @@ export function PickContents({
|
|||
<div className='flex w-fit'>
|
||||
<MiniButton
|
||||
title='Удалить'
|
||||
noHover
|
||||
className='px-0'
|
||||
icon={<IconRemove size='1rem' className='icon-red' />}
|
||||
onClick={() => handleDelete(props.row.original)}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Переместить выше'
|
||||
noHover
|
||||
className='px-0'
|
||||
icon={<IconMoveUp size='1rem' className='icon-primary' />}
|
||||
onClick={() => handleMoveUp(props.row.original)}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Переместить ниже'
|
||||
noHover
|
||||
className='px-0'
|
||||
icon={<IconMoveDown size='1rem' className='icon-primary' />}
|
||||
onClick={() => handleMoveDown(props.row.original)}
|
||||
|
|
|
@ -82,21 +82,18 @@ export function PickMultiOperation({ rows, items, value, onChange, className, ..
|
|||
<div className='flex w-fit'>
|
||||
<MiniButton
|
||||
title='Удалить'
|
||||
noHover
|
||||
className='px-0'
|
||||
icon={<IconRemove size='1rem' className='icon-red' />}
|
||||
onClick={() => handleDelete(props.row.original.id)}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Переместить выше'
|
||||
noHover
|
||||
className='px-0'
|
||||
icon={<IconMoveUp size='1rem' className='icon-primary' />}
|
||||
onClick={() => handleMoveUp(props.row.original.id)}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Переместить ниже'
|
||||
noHover
|
||||
className='px-0'
|
||||
icon={<IconMoveDown size='1rem' className='icon-primary' />}
|
||||
onClick={() => handleMoveDown(props.row.original.id)}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
|||
|
||||
import { type ILibraryItem, LibraryItemType } from '@/features/library';
|
||||
import { useLibrary } from '@/features/library/backend/use-library';
|
||||
import { PickSchema } from '@/features/library/components';
|
||||
import { PickSchema } from '@/features/library/components/pick-schema';
|
||||
|
||||
import { MiniButton } from '@/components/control';
|
||||
import { IconReset } from '@/components/icons';
|
||||
|
@ -61,7 +61,6 @@ export function DlgChangeInputSchema() {
|
|||
<Label text='Загружаемая концептуальная схема' />
|
||||
<MiniButton
|
||||
title='Сбросить выбор схемы'
|
||||
noHover
|
||||
noPadding
|
||||
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
||||
onClick={() => setValue('input', null)}
|
||||
|
|
|
@ -84,14 +84,9 @@ export function DlgCreateBlock() {
|
|||
className='w-160 px-6 h-110'
|
||||
helpTopic={HelpTopic.CC_OSS}
|
||||
>
|
||||
<Tabs
|
||||
selectedTabClassName='cc-selected'
|
||||
className='grid'
|
||||
selectedIndex={activeTab}
|
||||
onSelect={index => setActiveTab(index as TabID)}
|
||||
>
|
||||
<TabList className='z-pop mx-auto -mb-5 flex border divide-x rounded-none bg-secondary'>
|
||||
<TabLabel title='Основные атрибуты блока' label='Карточка' />
|
||||
<Tabs className='grid' selectedIndex={activeTab} onSelect={index => setActiveTab(index as TabID)}>
|
||||
<TabList className='z-pop mx-auto -mb-5 flex border divide-x rounded-none'>
|
||||
<TabLabel title='Основные атрибуты блока' label='Паспорт' />
|
||||
<TabLabel
|
||||
title={`Выбор вложенных узлов: [${children_operations.length + children_blocks.length}]`}
|
||||
label={`Содержимое${children_operations.length + children_blocks.length > 0 ? '*' : ''}`}
|
||||
|
|
|
@ -13,7 +13,7 @@ import { useDialogsStore } from '@/stores/dialogs';
|
|||
import { type ICreateOperationDTO, OperationType, schemaCreateOperation } from '../../backend/types';
|
||||
import { useCreateOperation } from '../../backend/use-create-operation';
|
||||
import { describeOperationType, labelOperationType } from '../../labels';
|
||||
import { type LayoutManager } from '../../models/oss-layout-api';
|
||||
import { type LayoutManager, OPERATION_NODE_HEIGHT, OPERATION_NODE_WIDTH } from '../../models/oss-layout-api';
|
||||
|
||||
import { TabInputOperation } from './tab-input-operation';
|
||||
import { TabSynthesisOperation } from './tab-synthesis-operation';
|
||||
|
@ -54,6 +54,8 @@ export function DlgCreateOperation() {
|
|||
position_x: defaultX,
|
||||
position_y: defaultY,
|
||||
arguments: initialInputs,
|
||||
width: OPERATION_NODE_WIDTH,
|
||||
height: OPERATION_NODE_HEIGHT,
|
||||
create_schema: false,
|
||||
layout: manager.layout
|
||||
},
|
||||
|
@ -98,12 +100,11 @@ export function DlgCreateOperation() {
|
|||
helpTopic={HelpTopic.CC_OSS}
|
||||
>
|
||||
<Tabs
|
||||
selectedTabClassName='cc-selected'
|
||||
className='grid'
|
||||
selectedIndex={activeTab}
|
||||
onSelect={(index, last) => handleSelectTab(index as TabID, last as TabID)}
|
||||
>
|
||||
<TabList className='z-pop mx-auto -mb-5 flex border divide-x rounded-none bg-secondary'>
|
||||
<TabList className='z-pop mx-auto -mb-5 flex border divide-x rounded-none'>
|
||||
<TabLabel
|
||||
title={describeOperationType(OperationType.INPUT)}
|
||||
label={labelOperationType(OperationType.INPUT)}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Controller, useFormContext, useWatch } from 'react-hook-form';
|
|||
|
||||
import { type ILibraryItem, LibraryItemType } from '@/features/library';
|
||||
import { useLibrary } from '@/features/library/backend/use-library';
|
||||
import { PickSchema } from '@/features/library/components';
|
||||
import { PickSchema } from '@/features/library/components/pick-schema';
|
||||
|
||||
import { MiniButton } from '@/components/control';
|
||||
import { IconReset } from '@/components/icons';
|
||||
|
@ -97,7 +97,6 @@ export function TabInputOperation() {
|
|||
<Label text='Загружаемая концептуальная схема' />
|
||||
<MiniButton
|
||||
title='Сбросить выбор схемы'
|
||||
noHover
|
||||
noPadding
|
||||
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
||||
onClick={() => setValue('item_data.result', null)}
|
||||
|
|
|
@ -43,7 +43,7 @@ export function DlgEditBlock() {
|
|||
|
||||
function onSubmit(data: IUpdateBlockDTO) {
|
||||
if (data.item_data.parent !== target.parent) {
|
||||
manager.onBlockChangeParent(data.target, data.item_data.parent);
|
||||
manager.onChangeParent(target.nodeID, data.item_data.parent === null ? null : `b${data.item_data.parent}`);
|
||||
data.layout = manager.layout;
|
||||
}
|
||||
return updateBlock({ itemID: manager.oss.id, data });
|
||||
|
|
|
@ -59,7 +59,7 @@ export function DlgEditOperation() {
|
|||
|
||||
function onSubmit(data: IUpdateOperationDTO) {
|
||||
if (data.item_data.parent !== target.parent) {
|
||||
manager.onOperationChangeParent(data.target, data.item_data.parent);
|
||||
manager.onChangeParent(target.nodeID, data.item_data.parent === null ? null : `b${data.item_data.parent}`);
|
||||
data.layout = manager.layout;
|
||||
}
|
||||
return updateOperation({ itemID: manager.oss.id, data });
|
||||
|
@ -75,16 +75,11 @@ export function DlgEditOperation() {
|
|||
helpTopic={HelpTopic.UI_SUBSTITUTIONS}
|
||||
hideHelpWhen={() => activeTab !== TabID.SUBSTITUTION}
|
||||
>
|
||||
<Tabs
|
||||
selectedTabClassName='cc-selected'
|
||||
className='grid'
|
||||
selectedIndex={activeTab}
|
||||
onSelect={index => setActiveTab(index as TabID)}
|
||||
>
|
||||
<TabList className='mb-3 mx-auto w-fit flex border divide-x rounded-none bg-secondary'>
|
||||
<Tabs className='grid' selectedIndex={activeTab} onSelect={index => setActiveTab(index as TabID)}>
|
||||
<TabList className='mb-3 mx-auto w-fit flex border divide-x rounded-none'>
|
||||
<TabLabel
|
||||
title='Текстовые поля' //
|
||||
label='Карточка'
|
||||
label='Паспорт'
|
||||
className='w-32'
|
||||
/>
|
||||
<TabLabel
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import { Controller, useFormContext, useWatch } from 'react-hook-form';
|
||||
|
||||
import { useRSForms } from '@/features/rsform/backend/use-rsforms';
|
||||
import { PickSubstitutions } from '@/features/rsform/components';
|
||||
import { PickSubstitutions } from '@/features/rsform/components/pick-substitutions';
|
||||
|
||||
import { TextArea } from '@/components/input';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
|
|
|
@ -7,9 +7,9 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
|||
import { HelpTopic } from '@/features/help';
|
||||
import { type ILibraryItem } from '@/features/library';
|
||||
import { useLibrary } from '@/features/library/backend/use-library';
|
||||
import { SelectLibraryItem } from '@/features/library/components';
|
||||
import { SelectLibraryItem } from '@/features/library/components/select-library-item';
|
||||
import { useRSForm } from '@/features/rsform/backend/use-rsform';
|
||||
import { PickMultiConstituenta } from '@/features/rsform/components';
|
||||
import { PickMultiConstituenta } from '@/features/rsform/components/pick-multi-constituenta';
|
||||
|
||||
import { MiniButton } from '@/components/control';
|
||||
import { Loader } from '@/components/loader';
|
||||
|
|
|
@ -1,10 +1,4 @@
|
|||
import {
|
||||
type IBlockPosition,
|
||||
type ICreateBlockDTO,
|
||||
type ICreateOperationDTO,
|
||||
type IOperationPosition,
|
||||
type IOssLayout
|
||||
} from '../backend/types';
|
||||
import { type ICreateBlockDTO, type ICreateOperationDTO, type INodePosition, type IOssLayout } from '../backend/types';
|
||||
|
||||
import { type IOperationSchema } from './oss';
|
||||
import { type Position2D, type Rectangle2D } from './oss-layout';
|
||||
|
@ -12,8 +6,8 @@ import { type Position2D, type Rectangle2D } from './oss-layout';
|
|||
export const GRID_SIZE = 10; // pixels - size of OSS grid
|
||||
const MIN_DISTANCE = 2 * GRID_SIZE; // pixels - minimum distance between nodes
|
||||
|
||||
const OPERATION_NODE_WIDTH = 150;
|
||||
const OPERATION_NODE_HEIGHT = 40;
|
||||
export const OPERATION_NODE_WIDTH = 150;
|
||||
export const OPERATION_NODE_HEIGHT = 40;
|
||||
|
||||
/** Layout manipulations for {@link IOperationSchema}. */
|
||||
export class LayoutManager {
|
||||
|
@ -30,52 +24,40 @@ export class LayoutManager {
|
|||
}
|
||||
|
||||
/** Calculate insert position for a new {@link IOperation} */
|
||||
newOperationPosition(data: ICreateOperationDTO): Position2D {
|
||||
let result = { x: data.position_x, y: data.position_y };
|
||||
const operations = this.layout.operations;
|
||||
const parentNode = this.layout.blocks.find(pos => pos.id === data.item_data.parent);
|
||||
if (operations.length === 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
newOperationPosition(data: ICreateOperationDTO): Rectangle2D {
|
||||
const result = { x: data.position_x, y: data.position_y, width: data.width, height: data.height };
|
||||
const parentNode = this.layout.find(pos => pos.nodeID === `b${data.item_data.parent}`) ?? null;
|
||||
const operations = this.layout.filter(pos => pos.nodeID.startsWith('o'));
|
||||
if (data.arguments.length !== 0) {
|
||||
result = calculatePositionFromArgs(data.arguments, operations);
|
||||
const pos = calculatePositionFromArgs(
|
||||
operations.filter(node => data.arguments.includes(Number(node.nodeID.slice(1))))
|
||||
);
|
||||
result.x = pos.x;
|
||||
result.y = pos.y;
|
||||
} else if (parentNode) {
|
||||
result.x = parentNode.x + MIN_DISTANCE;
|
||||
result.y = parentNode.y + MIN_DISTANCE;
|
||||
} else {
|
||||
result = this.calculatePositionForFreeOperation(result);
|
||||
const pos = this.calculatePositionForFreeOperation(result);
|
||||
result.x = pos.x;
|
||||
result.y = pos.y;
|
||||
}
|
||||
|
||||
result = preventOverlap(
|
||||
{ ...result, width: OPERATION_NODE_WIDTH, height: OPERATION_NODE_HEIGHT },
|
||||
operations.map(node => ({ ...node, width: OPERATION_NODE_WIDTH, height: OPERATION_NODE_HEIGHT }))
|
||||
);
|
||||
preventOverlap(result, operations);
|
||||
this.extendParentBounds(parentNode, result);
|
||||
|
||||
if (parentNode) {
|
||||
const borderX = result.x + OPERATION_NODE_WIDTH + MIN_DISTANCE;
|
||||
const borderY = result.y + OPERATION_NODE_HEIGHT + MIN_DISTANCE;
|
||||
if (borderX > parentNode.x + parentNode.width) {
|
||||
parentNode.width = borderX - parentNode.x;
|
||||
}
|
||||
if (borderY > parentNode.y + parentNode.height) {
|
||||
parentNode.height = borderY - parentNode.y;
|
||||
}
|
||||
// TODO: trigger cascading updates
|
||||
}
|
||||
|
||||
return { x: result.x, y: result.y };
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Calculate insert position for a new {@link IBlock} */
|
||||
newBlockPosition(data: ICreateBlockDTO): Rectangle2D {
|
||||
const block_nodes = data.children_blocks
|
||||
.map(id => this.layout.blocks.find(block => block.id === id))
|
||||
.map(id => this.layout.find(block => block.nodeID === `b${id}`))
|
||||
.filter(node => !!node);
|
||||
const operation_nodes = data.children_operations
|
||||
.map(id => this.layout.operations.find(operation => operation.id === id))
|
||||
.map(id => this.layout.find(operation => operation.nodeID === `o${id}`))
|
||||
.filter(node => !!node);
|
||||
const parentNode = this.layout.blocks.find(pos => pos.id === data.item_data.parent);
|
||||
const parentNode = this.layout.find(pos => pos.nodeID === `b${data.item_data.parent}`) ?? null;
|
||||
|
||||
let result: Rectangle2D = { x: data.position_x, y: data.position_y, width: data.width, height: data.height };
|
||||
|
||||
|
@ -98,61 +80,78 @@ export class LayoutManager {
|
|||
|
||||
if (block_nodes.length === 0 && operation_nodes.length === 0) {
|
||||
if (parentNode) {
|
||||
const siblings = this.oss.blocks.filter(block => block.parent === parentNode.id).map(block => block.id);
|
||||
const siblings = this.oss.blocks
|
||||
.filter(block => block.parent === data.item_data.parent)
|
||||
.map(block => block.nodeID);
|
||||
if (siblings.length > 0) {
|
||||
result = preventOverlap(
|
||||
preventOverlap(
|
||||
result,
|
||||
this.layout.blocks.filter(block => siblings.includes(block.id))
|
||||
this.layout.filter(node => siblings.includes(node.nodeID))
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const rootBlocks = this.oss.blocks.filter(block => block.parent === null).map(block => block.id);
|
||||
const rootBlocks = this.oss.blocks.filter(block => block.parent === null).map(block => block.nodeID);
|
||||
if (rootBlocks.length > 0) {
|
||||
result = preventOverlap(
|
||||
preventOverlap(
|
||||
result,
|
||||
this.layout.blocks.filter(block => rootBlocks.includes(block.id))
|
||||
this.layout.filter(node => rootBlocks.includes(node.nodeID))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (parentNode) {
|
||||
const borderX = result.x + result.width + MIN_DISTANCE;
|
||||
const borderY = result.y + result.height + MIN_DISTANCE;
|
||||
if (borderX > parentNode.x + parentNode.width) {
|
||||
parentNode.width = borderX - parentNode.x;
|
||||
}
|
||||
if (borderY > parentNode.y + parentNode.height) {
|
||||
parentNode.height = borderY - parentNode.y;
|
||||
}
|
||||
// TODO: trigger cascading updates
|
||||
}
|
||||
|
||||
this.extendParentBounds(parentNode, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Update layout when parent changes */
|
||||
onOperationChangeParent(targetID: number, newParent: number | null) {
|
||||
console.error('not implemented', targetID, newParent);
|
||||
onChangeParent(targetID: string, newParent: string | null) {
|
||||
const targetNode = this.layout.find(pos => pos.nodeID === targetID);
|
||||
if (!targetNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** Update layout when parent changes */
|
||||
onBlockChangeParent(targetID: number, newParent: number | null) {
|
||||
console.error('not implemented', targetID, newParent);
|
||||
const parentNode = this.layout.find(pos => pos.nodeID === newParent) ?? null;
|
||||
const offset = this.calculateOffsetForParentChange(targetNode, parentNode);
|
||||
if (offset.x === 0 && offset.y === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
targetNode.x += offset.x;
|
||||
targetNode.y += offset.y;
|
||||
|
||||
const children = this.oss.hierarchy.expandAllOutputs([targetID]);
|
||||
const childrenPositions = this.layout.filter(pos => children.includes(pos.nodeID));
|
||||
for (const child of childrenPositions) {
|
||||
child.x += offset.x;
|
||||
child.y += offset.y;
|
||||
}
|
||||
|
||||
this.extendParentBounds(parentNode, targetNode);
|
||||
}
|
||||
|
||||
private extendParentBounds(parent: INodePosition | null, child: Rectangle2D) {
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
const borderX = child.x + child.width + MIN_DISTANCE;
|
||||
const borderY = child.y + child.height + MIN_DISTANCE;
|
||||
parent.width = Math.max(parent.width, borderX - parent.x);
|
||||
parent.height = Math.max(parent.height, borderY - parent.y);
|
||||
// TODO: cascade update
|
||||
}
|
||||
|
||||
private calculatePositionForFreeOperation(initial: Position2D): Position2D {
|
||||
const operations = this.layout.operations;
|
||||
if (operations.length === 0) {
|
||||
if (this.oss.operations.length === 0) {
|
||||
return initial;
|
||||
}
|
||||
|
||||
const freeInputs = this.oss.operations
|
||||
.filter(operation => operation.arguments.length === 0 && operation.parent === null)
|
||||
.map(operation => operation.id);
|
||||
let inputsPositions = operations.filter(pos => freeInputs.includes(pos.id));
|
||||
.map(operation => operation.nodeID);
|
||||
let inputsPositions = this.layout.filter(pos => freeInputs.includes(pos.nodeID));
|
||||
if (inputsPositions.length === 0) {
|
||||
inputsPositions = operations;
|
||||
inputsPositions = this.layout.filter(pos => pos.nodeID.startsWith('o'));
|
||||
}
|
||||
const maxX = Math.max(...inputsPositions.map(node => node.x));
|
||||
const minY = Math.min(...inputsPositions.map(node => node.y));
|
||||
|
@ -163,8 +162,8 @@ export class LayoutManager {
|
|||
}
|
||||
|
||||
private calculatePositionForFreeBlock(initial: Rectangle2D): Rectangle2D {
|
||||
const rootBlocks = this.oss.blocks.filter(block => block.parent === null).map(block => block.id);
|
||||
const blocksPositions = this.layout.blocks.filter(pos => rootBlocks.includes(pos.id));
|
||||
const rootBlocks = this.oss.blocks.filter(block => block.parent === null).map(block => block.nodeID);
|
||||
const blocksPositions = this.layout.filter(pos => rootBlocks.includes(pos.nodeID));
|
||||
if (blocksPositions.length === 0) {
|
||||
return initial;
|
||||
}
|
||||
|
@ -172,6 +171,23 @@ export class LayoutManager {
|
|||
const minY = Math.min(...blocksPositions.map(node => node.y));
|
||||
return { ...initial, x: maxX + MIN_DISTANCE, y: minY };
|
||||
}
|
||||
|
||||
private calculateOffsetForParentChange(target: INodePosition, parent: INodePosition | null): Position2D {
|
||||
const newPosition = { ...target };
|
||||
if (parent === null) {
|
||||
const rootElements = this.oss.hierarchy.rootNodes();
|
||||
const positions = this.layout.filter(pos => rootElements.includes(pos.nodeID));
|
||||
preventOverlap(newPosition, positions);
|
||||
} else if (!rectanglesOverlap(target, parent)) {
|
||||
newPosition.x = parent.x + MIN_DISTANCE;
|
||||
newPosition.y = parent.y + MIN_DISTANCE;
|
||||
|
||||
const siblings = this.oss.hierarchy.at(parent.nodeID)?.outputs ?? [];
|
||||
const siblingsPositions = this.layout.filter(pos => siblings.includes(pos.nodeID));
|
||||
preventOverlap(newPosition, siblingsPositions);
|
||||
}
|
||||
return { x: newPosition.x - target.x, y: newPosition.y - target.y };
|
||||
}
|
||||
}
|
||||
|
||||
// ======= Internals =======
|
||||
|
@ -184,38 +200,25 @@ function rectanglesOverlap(a: Rectangle2D, b: Rectangle2D): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
function getOverlapAmount(a: Rectangle2D, b: Rectangle2D): Position2D {
|
||||
const xOverlap = Math.max(0, Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x));
|
||||
const yOverlap = Math.max(0, Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y));
|
||||
return { x: xOverlap, y: yOverlap };
|
||||
}
|
||||
|
||||
function preventOverlap(target: Rectangle2D, fixedRectangles: Rectangle2D[]): Rectangle2D {
|
||||
function preventOverlap(target: Rectangle2D, fixedRectangles: Rectangle2D[]) {
|
||||
let hasOverlap: boolean;
|
||||
do {
|
||||
hasOverlap = false;
|
||||
for (const fixed of fixedRectangles) {
|
||||
if (rectanglesOverlap(target, fixed)) {
|
||||
hasOverlap = true;
|
||||
const overlap = getOverlapAmount(target, fixed);
|
||||
if (overlap.x >= overlap.y) {
|
||||
target.x += overlap.x + MIN_DISTANCE;
|
||||
} else {
|
||||
target.y += overlap.y + MIN_DISTANCE;
|
||||
}
|
||||
target.x += MIN_DISTANCE;
|
||||
target.y += MIN_DISTANCE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} while (hasOverlap);
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
function calculatePositionFromArgs(args: number[], operations: IOperationPosition[]): Position2D {
|
||||
const argNodes = operations.filter(pos => args.includes(pos.id));
|
||||
const maxY = Math.max(...argNodes.map(node => node.y));
|
||||
const minX = Math.min(...argNodes.map(node => node.x));
|
||||
const maxX = Math.max(...argNodes.map(node => node.x));
|
||||
function calculatePositionFromArgs(args: INodePosition[]): Position2D {
|
||||
const maxY = Math.max(...args.map(node => node.y));
|
||||
const minX = Math.min(...args.map(node => node.x));
|
||||
const maxX = Math.max(...args.map(node => node.x));
|
||||
return {
|
||||
x: Math.ceil((maxX + minX) / 2 / GRID_SIZE) * GRID_SIZE,
|
||||
y: maxY + 2 * OPERATION_NODE_HEIGHT + MIN_DISTANCE
|
||||
|
@ -224,42 +227,23 @@ function calculatePositionFromArgs(args: number[], operations: IOperationPositio
|
|||
|
||||
function calculatePositionFromChildren(
|
||||
initial: Rectangle2D,
|
||||
operations: IOperationPosition[],
|
||||
blocks: IBlockPosition[]
|
||||
operations: INodePosition[],
|
||||
blocks: INodePosition[]
|
||||
): Rectangle2D {
|
||||
let left = undefined;
|
||||
let top = undefined;
|
||||
let right = undefined;
|
||||
let bottom = undefined;
|
||||
|
||||
for (const block of blocks) {
|
||||
left = left === undefined ? block.x - MIN_DISTANCE : Math.min(left, block.x - MIN_DISTANCE);
|
||||
top = top === undefined ? block.y - MIN_DISTANCE : Math.min(top, block.y - MIN_DISTANCE);
|
||||
right =
|
||||
right === undefined
|
||||
? Math.max(left + initial.width, block.x + block.width + MIN_DISTANCE)
|
||||
: Math.max(right, block.x + block.width + MIN_DISTANCE);
|
||||
bottom = !bottom
|
||||
? Math.max(top + initial.height, block.y + block.height + MIN_DISTANCE)
|
||||
: Math.max(bottom, block.y + block.height + MIN_DISTANCE);
|
||||
const allNodes = [...blocks, ...operations];
|
||||
if (allNodes.length === 0) {
|
||||
return initial;
|
||||
}
|
||||
|
||||
for (const operation of operations) {
|
||||
left = left === undefined ? operation.x - MIN_DISTANCE : Math.min(left, operation.x - MIN_DISTANCE);
|
||||
top = top === undefined ? operation.y - MIN_DISTANCE : Math.min(top, operation.y - MIN_DISTANCE);
|
||||
right =
|
||||
right === undefined
|
||||
? Math.max(left + initial.width, operation.x + OPERATION_NODE_WIDTH + MIN_DISTANCE)
|
||||
: Math.max(right, operation.x + OPERATION_NODE_WIDTH + MIN_DISTANCE);
|
||||
bottom = !bottom
|
||||
? Math.max(top + initial.height, operation.y + OPERATION_NODE_HEIGHT + MIN_DISTANCE)
|
||||
: Math.max(bottom, operation.y + OPERATION_NODE_HEIGHT + MIN_DISTANCE);
|
||||
}
|
||||
const left = Math.min(...allNodes.map(n => n.x)) - MIN_DISTANCE;
|
||||
const top = Math.min(...allNodes.map(n => n.y)) - MIN_DISTANCE;
|
||||
const right = Math.max(...allNodes.map(n => n.x + n.width)) + MIN_DISTANCE;
|
||||
const bottom = Math.max(...allNodes.map(n => n.y + n.height)) + MIN_DISTANCE;
|
||||
|
||||
return {
|
||||
x: left ?? initial.x,
|
||||
y: top ?? initial.y,
|
||||
width: right !== undefined && left !== undefined ? right - left : initial.width,
|
||||
height: bottom !== undefined && top !== undefined ? bottom - top : initial.height
|
||||
x: left,
|
||||
y: top,
|
||||
width: right - left,
|
||||
height: bottom - top
|
||||
};
|
||||
}
|
||||
|
|
|
@ -2,19 +2,27 @@
|
|||
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { EditorLibraryItem, ToolbarItemCard } from '@/features/library/components';
|
||||
import { EditorLibraryItem } from '@/features/library/components/editor-library-item';
|
||||
import { ToolbarItemCard } from '@/features/library/components/toolbar-item-card';
|
||||
|
||||
import { useWindowSize } from '@/hooks/use-window-size';
|
||||
import { useModificationStore } from '@/stores/modification';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
import { OssStats } from '../../../components/oss-stats';
|
||||
import { useOssEdit } from '../oss-edit-context';
|
||||
|
||||
import { FormOSS } from './form-oss';
|
||||
import { OssStats } from './oss-stats';
|
||||
|
||||
const SIDELIST_LAYOUT_THRESHOLD = 768; // px
|
||||
|
||||
export function EditorOssCard() {
|
||||
const { schema, isMutable, deleteSchema } = useOssEdit();
|
||||
const { isModified } = useModificationStore();
|
||||
const showOSSStats = usePreferencesStore(state => state.showOSSStats);
|
||||
const windowSize = useWindowSize();
|
||||
const isNarrow = !!windowSize.width && windowSize.width <= SIDELIST_LAYOUT_THRESHOLD;
|
||||
|
||||
function initiateSubmit() {
|
||||
const element = document.getElementById(globalIDs.library_item_editor) as HTMLFormElement;
|
||||
|
@ -33,25 +41,36 @@ export function EditorOssCard() {
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
onKeyDown={handleInput}
|
||||
className={clsx(
|
||||
'relative md:w-fit md:max-w-fit max-w-128',
|
||||
'flex px-6 pt-8',
|
||||
isNarrow && 'flex-col md:items-center'
|
||||
)}
|
||||
>
|
||||
<ToolbarItemCard
|
||||
className='cc-tab-tools'
|
||||
onSubmit={initiateSubmit}
|
||||
schema={schema}
|
||||
isMutable={isMutable}
|
||||
deleteSchema={deleteSchema}
|
||||
isNarrow={isNarrow}
|
||||
/>
|
||||
<div
|
||||
onKeyDown={handleInput}
|
||||
className={clsx('md:max-w-fit max-w-128 min-w-fit', 'flex flex-row flex-wrap pt-8 px-6 justify-center')}
|
||||
>
|
||||
<div className='cc-column px-3'>
|
||||
|
||||
<div className='cc-column px-3 mx-0 md:mx-auto'>
|
||||
<FormOSS key={schema.id} />
|
||||
<EditorLibraryItem schema={schema} isAttachedToOSS={false} />
|
||||
</div>
|
||||
|
||||
<OssStats className='mt-3 md:mt-8 md:ml-5 w-80 md:w-56 mx-auto h-min' stats={schema.stats} />
|
||||
<OssStats
|
||||
className={clsx(
|
||||
'w-80 md:w-56 mt-3 md:mt-8 mx-auto md:ml-5 md:mr-0',
|
||||
'cc-animate-sidebar',
|
||||
showOSSStats ? 'max-w-full' : 'opacity-0 max-w-0'
|
||||
)}
|
||||
stats={schema.stats}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
|||
|
||||
import { type IUpdateLibraryItemDTO, LibraryItemType, schemaUpdateLibraryItem } from '@/features/library';
|
||||
import { useUpdateItem } from '@/features/library/backend/use-update-item';
|
||||
import { ToolbarItemAccess } from '@/features/library/components';
|
||||
import { ToolbarItemAccess } from '@/features/library/components/toolbar-item-access';
|
||||
|
||||
import { SubmitButton } from '@/components/control';
|
||||
import { IconSave } from '@/components/icons';
|
||||
|
|
|
@ -19,18 +19,13 @@ export function useContextMenu() {
|
|||
const setHoverOperation = useOperationTooltipStore(state => state.setHoverItem);
|
||||
const { addSelectedNodes } = useStoreApi().getState();
|
||||
|
||||
function handleContextMenu(event: React.MouseEvent<Element>, node: OssNode) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
function openContextMenu(node: OssNode, clientX: number, clientY: number) {
|
||||
addSelectedNodes([node.id]);
|
||||
|
||||
setMenuProps({
|
||||
item: node.type === 'block' ? node.data.block ?? null : node.data.operation ?? null,
|
||||
cursorX: event.clientX,
|
||||
cursorY: event.clientY
|
||||
cursorX: clientX,
|
||||
cursorY: clientY
|
||||
});
|
||||
|
||||
setIsOpen(true);
|
||||
setHoverOperation(null);
|
||||
}
|
||||
|
@ -42,7 +37,7 @@ export function useContextMenu() {
|
|||
return {
|
||||
isOpen,
|
||||
menuProps,
|
||||
handleContextMenu,
|
||||
openContextMenu,
|
||||
hideContextMenu
|
||||
};
|
||||
}
|
||||
|
|
|
@ -38,9 +38,6 @@ export function NodeCore({ node }: NodeCoreProps) {
|
|||
'relative flex items-center justify-center p-[2px]',
|
||||
isChild && 'border-accent-orange'
|
||||
)}
|
||||
data-tooltip-id={globalIDs.operation_tooltip}
|
||||
data-tooltip-hidden={node.dragging}
|
||||
onMouseEnter={() => setHover(node.data.operation)}
|
||||
>
|
||||
<div className='absolute z-pop top-0 right-0 flex flex-col gap-[4px] p-[2px]'>
|
||||
<Indicator
|
||||
|
@ -79,9 +76,12 @@ export function NodeCore({ node }: NodeCoreProps) {
|
|||
|
||||
<div
|
||||
className={clsx(
|
||||
'text-center line-clamp-2 pl-[4px]',
|
||||
longLabel ? 'text-[12px]/[16px] pr-[10px]' : 'text-[14px]/[20px] pr-[4px]'
|
||||
'text-center line-clamp-2 px-[4px] mr-[12px]',
|
||||
longLabel ? 'text-[12px]/[16px]' : 'text-[14px]/[20px]'
|
||||
)}
|
||||
data-tooltip-id={globalIDs.operation_tooltip}
|
||||
data-tooltip-hidden={node.dragging}
|
||||
onMouseEnter={() => setHover(node.data.operation)}
|
||||
>
|
||||
{node.data.label}
|
||||
</div>
|
||||
|
|
|
@ -6,6 +6,7 @@ import clsx from 'clsx';
|
|||
import { DiagramFlow, useReactFlow, useStoreApi } from '@/components/flow/diagram-flow';
|
||||
import { useMainHeight } from '@/stores/app-layout';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { promptText } from '@/utils/labels';
|
||||
|
||||
|
@ -23,6 +24,7 @@ import { useContextMenu } from './context-menu/use-context-menu';
|
|||
import { OssNodeTypes } from './graph/oss-node-types';
|
||||
import { CoordinateDisplay } from './coordinate-display';
|
||||
import { useOssFlow } from './oss-flow-context';
|
||||
import { SidePanel } from './side-panel';
|
||||
import { ToolbarOssGraph } from './toolbar-oss-graph';
|
||||
import { useDragging } from './use-dragging';
|
||||
import { useGetLayout } from './use-get-layout';
|
||||
|
@ -52,6 +54,7 @@ export function OssFlow() {
|
|||
|
||||
const showGrid = useOSSGraphStore(state => state.showGrid);
|
||||
const showCoordinates = useOSSGraphStore(state => state.showCoordinates);
|
||||
const showPanel = usePreferencesStore(state => state.showOssSidePanel);
|
||||
|
||||
const getLayout = useGetLayout();
|
||||
const { updateLayout } = useUpdateLayout();
|
||||
|
@ -64,7 +67,7 @@ export function OssFlow() {
|
|||
const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation);
|
||||
const showEditBlock = useDialogsStore(state => state.showEditBlock);
|
||||
|
||||
const { isOpen: isContextMenuOpen, menuProps, handleContextMenu, hideContextMenu } = useContextMenu();
|
||||
const { isOpen: isContextMenuOpen, menuProps, openContextMenu, hideContextMenu } = useContextMenu();
|
||||
const { handleDragStart, handleDrag, handleDragStop } = useDragging({ hideContextMenu });
|
||||
|
||||
function handleSavePositions() {
|
||||
|
@ -135,8 +138,8 @@ export function OssFlow() {
|
|||
});
|
||||
}
|
||||
} else {
|
||||
if (node.data.operation?.result) {
|
||||
navigateOperationSchema(Number(node.id));
|
||||
if (node.data.operation) {
|
||||
navigateOperationSchema(node.data.operation.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -183,19 +186,29 @@ export function OssFlow() {
|
|||
setMouseCoords(targetPosition);
|
||||
}
|
||||
|
||||
function handleNodeContextMenu(event: React.MouseEvent<Element>, node: OssNode) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
openContextMenu(node, event.clientX, event.clientY);
|
||||
}
|
||||
|
||||
return (
|
||||
<div tabIndex={-1} className='relative' onMouseMove={showCoordinates ? handleMouseMove : undefined}>
|
||||
{showCoordinates ? <CoordinateDisplay mouseCoords={mouseCoords} className='absolute top-1 right-2' /> : null}
|
||||
|
||||
<ContextMenu isOpen={isContextMenuOpen} onHide={hideContextMenu} {...menuProps} />
|
||||
|
||||
<ToolbarOssGraph
|
||||
className='absolute z-pop top-8 right-1/2 translate-x-1/2'
|
||||
onCreateOperation={handleCreateOperation}
|
||||
onCreateBlock={handleCreateBlock}
|
||||
onDelete={handleDeleteSelected}
|
||||
onResetPositions={resetGraph}
|
||||
openContextMenu={openContextMenu}
|
||||
isContextMenuOpen={isContextMenuOpen}
|
||||
hideContextMenu={hideContextMenu}
|
||||
/>
|
||||
|
||||
<ContextMenu isOpen={isContextMenuOpen} onHide={hideContextMenu} {...menuProps} />
|
||||
|
||||
<DiagramFlow
|
||||
{...flowOptions}
|
||||
className={clsx(!containMovement && 'cursor-relocate')}
|
||||
|
@ -209,12 +222,22 @@ export function OssFlow() {
|
|||
showGrid={showGrid}
|
||||
onClick={hideContextMenu}
|
||||
onNodeDoubleClick={handleNodeDoubleClick}
|
||||
onNodeContextMenu={handleContextMenu}
|
||||
onNodeContextMenu={handleNodeContextMenu}
|
||||
onContextMenu={hideContextMenu}
|
||||
onNodeDragStart={handleDragStart}
|
||||
onNodeDrag={handleDrag}
|
||||
onNodeDragStop={handleDragStop}
|
||||
/>
|
||||
|
||||
<SidePanel
|
||||
className={clsx(
|
||||
'absolute right-0 top-0 z-sticky w-84 min-h-80',
|
||||
'cc-animate-panel cc-shadow-border',
|
||||
showPanel ? 'translate-x-0' : 'opacity-0 translate-x-full pointer-events-none'
|
||||
)}
|
||||
isMounted={showPanel}
|
||||
selectedItems={selectedItems}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export { SidePanel } from './side-panel';
|
|
@ -0,0 +1,72 @@
|
|||
import { Suspense } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
|
||||
import { MiniButton } from '@/components/control';
|
||||
import { IconClose } from '@/components/icons';
|
||||
import { Loader } from '@/components/loader';
|
||||
import { cn } from '@/components/utils';
|
||||
import { useMainHeight } from '@/stores/app-layout';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
||||
import { type IOssItem, NodeType } from '../../../../models/oss';
|
||||
|
||||
import { ViewSchema } from './view-schema';
|
||||
|
||||
interface SidePanelProps {
|
||||
selectedItems: IOssItem[];
|
||||
className?: string;
|
||||
isMounted: boolean;
|
||||
}
|
||||
|
||||
export function SidePanel({ selectedItems, isMounted, className }: SidePanelProps) {
|
||||
const selectedOperation =
|
||||
selectedItems.length === 1 && selectedItems[0].nodeType === NodeType.OPERATION ? selectedItems[0] : null;
|
||||
const selectedSchema = selectedOperation?.result ?? null;
|
||||
|
||||
const debouncedMounted = useDebounce(isMounted, PARAMETER.moveDuration);
|
||||
const closePanel = usePreferencesStore(state => state.toggleShowOssSidePanel);
|
||||
const sidePanelHeight = useMainHeight();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex flex-col py-2 h-full overflow-hidden',
|
||||
'border-l rounded-none rounded-l-sm bg-background',
|
||||
className
|
||||
)}
|
||||
style={{ height: sidePanelHeight }}
|
||||
>
|
||||
<MiniButton
|
||||
titleHtml='Закрыть панель'
|
||||
aria-label='Закрыть'
|
||||
noPadding
|
||||
icon={<IconClose size='1.25rem' />}
|
||||
className='absolute z-pop top-2 right-1'
|
||||
onClick={closePanel}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
'mt-0 mb-1',
|
||||
'font-medium text-sm select-none self-center',
|
||||
'transition-transform',
|
||||
selectedSchema && 'translate-x-16'
|
||||
)}
|
||||
>
|
||||
Содержание КС
|
||||
</div>
|
||||
|
||||
{!selectedOperation ? (
|
||||
<div className='text-center text-sm cc-fade-in'>Выделите операцию для просмотра</div>
|
||||
) : !selectedSchema ? (
|
||||
<div className='text-center text-sm cc-fade-in'>Отсутствует концептуальная схема для выбранной операции</div>
|
||||
) : debouncedMounted ? (
|
||||
<Suspense fallback={<Loader />}>
|
||||
<ViewSchema schemaID={selectedSchema} />
|
||||
</Suspense>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,195 @@
|
|||
import { urls, useConceptNavigation } from '@/app';
|
||||
import { type IConstituenta, type IRSForm } from '@/features/rsform';
|
||||
import { CstType, type IConstituentaBasicsDTO, type ICreateConstituentaDTO } from '@/features/rsform/backend/types';
|
||||
import { useCreateConstituenta } from '@/features/rsform/backend/use-create-constituenta';
|
||||
import { useMoveConstituents } from '@/features/rsform/backend/use-move-constituents';
|
||||
import { useMutatingRSForm } from '@/features/rsform/backend/use-mutating-rsform';
|
||||
import { generateAlias } from '@/features/rsform/models/rsform-api';
|
||||
import { useCstSearchStore } from '@/features/rsform/stores/cst-search';
|
||||
|
||||
import { MiniButton } from '@/components/control';
|
||||
import { IconClone, IconDestroy, IconMoveDown, IconMoveUp, IconNewItem, IconRSForm } from '@/components/icons';
|
||||
import { cn } from '@/components/utils';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { PARAMETER, prefixes } from '@/utils/constants';
|
||||
import { type RO } from '@/utils/meta';
|
||||
|
||||
interface ToolbarConstituentsProps {
|
||||
schema: IRSForm;
|
||||
activeCst: IConstituenta | null;
|
||||
setActive: (cstID: number) => void;
|
||||
resetActive: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ToolbarConstituents({
|
||||
schema,
|
||||
activeCst,
|
||||
setActive,
|
||||
resetActive,
|
||||
className
|
||||
}: ToolbarConstituentsProps) {
|
||||
const router = useConceptNavigation();
|
||||
const isProcessing = useMutatingRSForm();
|
||||
const searchText = useCstSearchStore(state => state.query);
|
||||
const hasSearch = searchText.length > 0;
|
||||
|
||||
const showCreateCst = useDialogsStore(state => state.showCreateCst);
|
||||
const showDeleteCst = useDialogsStore(state => state.showDeleteCst);
|
||||
const { moveConstituents } = useMoveConstituents();
|
||||
const { createConstituenta } = useCreateConstituenta();
|
||||
|
||||
function navigateRSForm() {
|
||||
router.push({ path: urls.schema(schema.id) });
|
||||
}
|
||||
|
||||
function onCreateCst(newCst: RO<IConstituentaBasicsDTO>) {
|
||||
setActive(newCst.id);
|
||||
setTimeout(() => {
|
||||
const element = document.getElementById(`${prefixes.cst_list}${newCst.id}`);
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
inline: 'end'
|
||||
});
|
||||
}
|
||||
}, PARAMETER.refreshTimeout);
|
||||
}
|
||||
|
||||
function createCst() {
|
||||
const targetType = activeCst?.cst_type ?? CstType.BASE;
|
||||
const data: ICreateConstituentaDTO = {
|
||||
insert_after: activeCst?.id ?? null,
|
||||
cst_type: targetType,
|
||||
alias: generateAlias(targetType, schema),
|
||||
term_raw: '',
|
||||
definition_formal: '',
|
||||
definition_raw: '',
|
||||
convention: '',
|
||||
term_forms: []
|
||||
};
|
||||
showCreateCst({ schema: schema, onCreate: onCreateCst, initial: data });
|
||||
}
|
||||
|
||||
function cloneCst() {
|
||||
if (!activeCst) {
|
||||
return;
|
||||
}
|
||||
void createConstituenta({
|
||||
itemID: schema.id,
|
||||
data: {
|
||||
insert_after: activeCst.id,
|
||||
cst_type: activeCst.cst_type,
|
||||
alias: generateAlias(activeCst.cst_type, schema),
|
||||
term_raw: activeCst.term_raw,
|
||||
definition_formal: activeCst.definition_formal,
|
||||
definition_raw: activeCst.definition_raw,
|
||||
convention: activeCst.convention,
|
||||
term_forms: activeCst.term_forms
|
||||
}
|
||||
}).then(onCreateCst);
|
||||
}
|
||||
|
||||
function promptDeleteCst() {
|
||||
if (!activeCst) {
|
||||
return;
|
||||
}
|
||||
showDeleteCst({
|
||||
schema: schema,
|
||||
selected: [activeCst.id],
|
||||
afterDelete: resetActive
|
||||
});
|
||||
}
|
||||
|
||||
function moveUp() {
|
||||
if (!activeCst) {
|
||||
return;
|
||||
}
|
||||
const currentIndex = schema.items.reduce((prev, cst, index) => {
|
||||
if (activeCst.id !== cst.id) {
|
||||
return prev;
|
||||
} else if (prev === -1) {
|
||||
return index;
|
||||
}
|
||||
return Math.min(prev, index);
|
||||
}, -1);
|
||||
const target = Math.max(0, currentIndex - 1);
|
||||
void moveConstituents({
|
||||
itemID: schema.id,
|
||||
data: {
|
||||
items: [activeCst.id],
|
||||
move_to: target
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function moveDown() {
|
||||
if (!activeCst) {
|
||||
return;
|
||||
}
|
||||
let count = 0;
|
||||
const currentIndex = schema.items.reduce((prev, cst, index) => {
|
||||
if (activeCst.id !== cst.id) {
|
||||
return prev;
|
||||
} else {
|
||||
count += 1;
|
||||
if (prev === -1) {
|
||||
return index;
|
||||
}
|
||||
return Math.max(prev, index);
|
||||
}
|
||||
}, -1);
|
||||
const target = Math.min(schema.items.length - 1, currentIndex - count + 2);
|
||||
void moveConstituents({
|
||||
itemID: schema.id,
|
||||
data: {
|
||||
items: [activeCst.id],
|
||||
move_to: target
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex gap-0.5', className)}>
|
||||
<MiniButton
|
||||
title='Перейти к концептуальной схеме'
|
||||
icon={<IconRSForm size='1rem' className='icon-primary' />}
|
||||
onClick={navigateRSForm}
|
||||
/>
|
||||
|
||||
<MiniButton
|
||||
title='Создать конституенту'
|
||||
icon={<IconNewItem size='1rem' className='icon-green' />}
|
||||
onClick={createCst}
|
||||
disabled={isProcessing}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Клонировать конституенту'
|
||||
icon={<IconClone size='1rem' className='icon-green' />}
|
||||
onClick={cloneCst}
|
||||
disabled={!activeCst || isProcessing}
|
||||
/>
|
||||
|
||||
<MiniButton
|
||||
title='Удалить выделенную конституенту'
|
||||
onClick={promptDeleteCst}
|
||||
icon={<IconDestroy size='1rem' className='icon-red' />}
|
||||
disabled={!activeCst || isProcessing || activeCst?.is_inherited}
|
||||
/>
|
||||
|
||||
<MiniButton
|
||||
title='Переместить вверх'
|
||||
icon={<IconMoveUp size='1rem' className='icon-primary' />}
|
||||
onClick={moveUp}
|
||||
disabled={!activeCst || isProcessing || schema.items.length < 2 || hasSearch}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Переместить вниз'
|
||||
icon={<IconMoveDown size='1rem' className='icon-primary' />}
|
||||
onClick={moveDown}
|
||||
disabled={!activeCst || isProcessing || schema.items.length < 2 || hasSearch}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
import { useRSFormSuspense } from '@/features/rsform/backend/use-rsform';
|
||||
import { RSFormStats } from '@/features/rsform/components/rsform-stats';
|
||||
import { ViewConstituents } from '@/features/rsform/components/view-constituents';
|
||||
|
||||
import { useFitHeight } from '@/stores/app-layout';
|
||||
|
||||
import { ToolbarConstituents } from './toolbar-constituents';
|
||||
|
||||
interface ViewSchemaProps {
|
||||
schemaID: number;
|
||||
}
|
||||
|
||||
export function ViewSchema({ schemaID }: ViewSchemaProps) {
|
||||
const { schema } = useRSFormSuspense({ itemID: schemaID });
|
||||
const [activeID, setActiveID] = useState<number | null>(null);
|
||||
const activeCst = activeID ? schema.cstByID.get(activeID) ?? null : null;
|
||||
|
||||
const listHeight = useFitHeight('19rem', '10rem');
|
||||
|
||||
return (
|
||||
<div className='grid h-full relative cc-fade-in' style={{ gridTemplateRows: '1fr auto' }}>
|
||||
<ToolbarConstituents
|
||||
className='absolute -top-7 left-1'
|
||||
schema={schema}
|
||||
activeCst={activeCst}
|
||||
setActive={setActiveID}
|
||||
resetActive={() => setActiveID(null)}
|
||||
/>
|
||||
|
||||
<ViewConstituents
|
||||
dense
|
||||
noBorder
|
||||
className='border-y rounded-none'
|
||||
schema={schema}
|
||||
activeCst={activeCst}
|
||||
onActivate={cst => setActiveID(cst.id)}
|
||||
maxListHeight={listHeight}
|
||||
/>
|
||||
|
||||
<RSFormStats className='pr-4 py-2 ml-[-1rem]' stats={schema.stats} />
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,18 +1,18 @@
|
|||
'use client';
|
||||
|
||||
import { toast } from 'react-toastify';
|
||||
import React from 'react';
|
||||
|
||||
import { HelpTopic } from '@/features/help';
|
||||
import { BadgeHelp } from '@/features/help/components';
|
||||
import { BadgeHelp } from '@/features/help/components/badge-help';
|
||||
import { IconShowSidebar } from '@/features/library/components/icon-show-sidebar';
|
||||
import { type OssNode } from '@/features/oss/models/oss-layout';
|
||||
|
||||
import { MiniButton } from '@/components/control';
|
||||
import {
|
||||
IconConceptBlock,
|
||||
IconDestroy,
|
||||
IconEdit2,
|
||||
IconExecute,
|
||||
IconFitImage,
|
||||
IconFixLayout,
|
||||
IconNewItem,
|
||||
IconReset,
|
||||
IconSave,
|
||||
|
@ -21,14 +21,12 @@ import {
|
|||
import { type Styling } from '@/components/props';
|
||||
import { cn } from '@/components/utils';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { prepareTooltip } from '@/utils/utils';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { isIOS, prepareTooltip } from '@/utils/utils';
|
||||
|
||||
import { OperationType } from '../../../backend/types';
|
||||
import { useExecuteOperation } from '../../../backend/use-execute-operation';
|
||||
import { useMutatingOss } from '../../../backend/use-mutating-oss';
|
||||
import { useUpdateLayout } from '../../../backend/use-update-layout';
|
||||
import { NodeType } from '../../../models/oss';
|
||||
import { LayoutManager } from '../../../models/oss-layout-api';
|
||||
import { useOssEdit } from '../oss-edit-context';
|
||||
|
||||
import { useOssFlow } from './oss-flow-context';
|
||||
|
@ -39,6 +37,10 @@ interface ToolbarOssGraphProps extends Styling {
|
|||
onCreateBlock: () => void;
|
||||
onDelete: () => void;
|
||||
onResetPositions: () => void;
|
||||
|
||||
isContextMenuOpen: boolean;
|
||||
openContextMenu: (node: OssNode, clientX: number, clientY: number) => void;
|
||||
hideContextMenu: () => void;
|
||||
}
|
||||
|
||||
export function ToolbarOssGraph({
|
||||
|
@ -46,12 +48,16 @@ export function ToolbarOssGraph({
|
|||
onCreateBlock,
|
||||
onDelete,
|
||||
onResetPositions,
|
||||
|
||||
isContextMenuOpen,
|
||||
openContextMenu,
|
||||
hideContextMenu,
|
||||
className,
|
||||
...restProps
|
||||
}: ToolbarOssGraphProps) {
|
||||
const { schema, selectedItems, isMutable, canDeleteOperation: canDelete } = useOssEdit();
|
||||
const isProcessing = useMutatingOss();
|
||||
const { resetView } = useOssFlow();
|
||||
const { resetView, nodes } = useOssFlow();
|
||||
const selectedOperation =
|
||||
selectedItems.length === 1 && selectedItems[0].nodeType === NodeType.OPERATION ? selectedItems[0] : null;
|
||||
const selectedBlock =
|
||||
|
@ -59,67 +65,31 @@ export function ToolbarOssGraph({
|
|||
const getLayout = useGetLayout();
|
||||
|
||||
const { updateLayout } = useUpdateLayout();
|
||||
const { executeOperation } = useExecuteOperation();
|
||||
|
||||
const showEditOperation = useDialogsStore(state => state.showEditOperation);
|
||||
const showEditBlock = useDialogsStore(state => state.showEditBlock);
|
||||
const showOssOptions = useDialogsStore(state => state.showOssOptions);
|
||||
|
||||
const readyForSynthesis = (() => {
|
||||
if (!selectedOperation || selectedOperation.operation_type !== OperationType.SYNTHESIS) {
|
||||
return false;
|
||||
}
|
||||
if (selectedOperation.result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const argumentIDs = schema.graph.expandInputs([selectedOperation.id]);
|
||||
if (!argumentIDs || argumentIDs.length < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const argumentOperations = argumentIDs.map(id => schema.operationByID.get(id)!);
|
||||
if (argumentOperations.some(item => item.result === null)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})();
|
||||
|
||||
function handleFixLayout() {
|
||||
// TODO: implement layout algorithm
|
||||
toast.info('Еще не реализовано');
|
||||
}
|
||||
const showOptions = useDialogsStore(state => state.showOssOptions);
|
||||
const showSidePanel = usePreferencesStore(state => state.showOssSidePanel);
|
||||
const toggleShowSidePanel = usePreferencesStore(state => state.toggleShowOssSidePanel);
|
||||
|
||||
function handleShowOptions() {
|
||||
showOssOptions();
|
||||
showOptions();
|
||||
}
|
||||
|
||||
function handleSavePositions() {
|
||||
void updateLayout({ itemID: schema.id, data: getLayout() });
|
||||
}
|
||||
|
||||
function handleOperationExecute() {
|
||||
if (!readyForSynthesis || !selectedOperation) {
|
||||
function handleEditItem(event: React.MouseEvent<HTMLButtonElement>) {
|
||||
if (isContextMenuOpen) {
|
||||
hideContextMenu();
|
||||
return;
|
||||
}
|
||||
void executeOperation({
|
||||
itemID: schema.id, //
|
||||
data: { target: selectedOperation.id, layout: getLayout() }
|
||||
});
|
||||
const nodeID = selectedOperation?.nodeID ?? selectedBlock?.nodeID;
|
||||
if (!nodeID) {
|
||||
return;
|
||||
}
|
||||
|
||||
function handleEditItem() {
|
||||
if (selectedOperation) {
|
||||
showEditOperation({
|
||||
manager: new LayoutManager(schema, getLayout()),
|
||||
target: selectedOperation
|
||||
});
|
||||
} else if (selectedBlock) {
|
||||
showEditBlock({
|
||||
manager: new LayoutManager(schema, getLayout()),
|
||||
target: selectedBlock
|
||||
});
|
||||
const node = nodes.find(node => node.id === nodeID);
|
||||
if (node) {
|
||||
openContextMenu(node, event.clientX, event.clientY);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -145,12 +115,9 @@ export function ToolbarOssGraph({
|
|||
onClick={resetView}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Исправить позиции узлов'
|
||||
icon={<IconFixLayout size='1.25rem' className='icon-primary' />}
|
||||
onClick={handleFixLayout}
|
||||
disabled={
|
||||
selectedItems.length > 1 || (selectedItems.length > 0 && selectedItems[0].nodeType === NodeType.OPERATION)
|
||||
}
|
||||
title='Панель содержания КС'
|
||||
icon={<IconShowSidebar value={showSidePanel} isBottom={false} size='1.25rem' />}
|
||||
onClick={toggleShowSidePanel}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Настройки отображения'
|
||||
|
@ -169,11 +136,12 @@ export function ToolbarOssGraph({
|
|||
disabled={isProcessing}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Новый блок', 'Ctrl + Shift + Q')}
|
||||
aria-label='Новый блок'
|
||||
icon={<IconConceptBlock size='1.25rem' className='icon-green' />}
|
||||
onClick={onCreateBlock}
|
||||
disabled={isProcessing}
|
||||
titleHtml={prepareTooltip('Редактировать выбранную', isIOS() ? '' : 'Правый клик')}
|
||||
hideTitle={isContextMenuOpen}
|
||||
aria-label='Редактировать выбранную'
|
||||
icon={<IconEdit2 size='1.25rem' className='icon-primary' />}
|
||||
onClick={handleEditItem}
|
||||
disabled={selectedItems.length !== 1 || isProcessing}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Новая операция', 'Ctrl + Q')}
|
||||
|
@ -183,18 +151,13 @@ export function ToolbarOssGraph({
|
|||
disabled={isProcessing}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Активировать операцию'
|
||||
icon={<IconExecute size='1.25rem' className='icon-green' />}
|
||||
onClick={handleOperationExecute}
|
||||
disabled={isProcessing || selectedItems.length !== 1 || !readyForSynthesis}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Редактировать выбранную', 'Двойной клик')}
|
||||
aria-label='Редактировать выбранную'
|
||||
icon={<IconEdit2 size='1.25rem' className='icon-primary' />}
|
||||
onClick={handleEditItem}
|
||||
disabled={selectedItems.length !== 1 || isProcessing}
|
||||
titleHtml={prepareTooltip('Новый блок', 'Ctrl + Shift + Q')}
|
||||
aria-label='Новый блок'
|
||||
icon={<IconConceptBlock size='1.25rem' className='icon-green' />}
|
||||
onClick={onCreateBlock}
|
||||
disabled={isProcessing}
|
||||
/>
|
||||
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Удалить выбранную', 'Delete')}
|
||||
aria-label='Удалить выбранную'
|
||||
|
|
|
@ -3,6 +3,7 @@ import { type Node, useReactFlow } from 'reactflow';
|
|||
import { type IOssLayout } from '../../../backend/types';
|
||||
import { type IOperationSchema } from '../../../models/oss';
|
||||
import { type Position2D } from '../../../models/oss-layout';
|
||||
import { OPERATION_NODE_HEIGHT, OPERATION_NODE_WIDTH } from '../../../models/oss-layout-api';
|
||||
import { useOssEdit } from '../oss-edit-context';
|
||||
|
||||
import { BLOCK_NODE_MIN_HEIGHT, BLOCK_NODE_MIN_WIDTH } from './graph/block-node';
|
||||
|
@ -14,22 +15,24 @@ export function useGetLayout() {
|
|||
return function getLayout(): IOssLayout {
|
||||
const nodes = getNodes();
|
||||
const nodeById = new Map(nodes.map(node => [node.id, node]));
|
||||
return {
|
||||
operations: nodes
|
||||
return [
|
||||
...nodes
|
||||
.filter(node => node.type !== 'block')
|
||||
.map(node => ({
|
||||
id: schema.itemByNodeID.get(node.id)!.id,
|
||||
...computeAbsolutePosition(node, schema, nodeById)
|
||||
nodeID: node.id,
|
||||
...computeAbsolutePosition(node, schema, nodeById),
|
||||
width: OPERATION_NODE_WIDTH,
|
||||
height: OPERATION_NODE_HEIGHT
|
||||
})),
|
||||
blocks: nodes
|
||||
...nodes
|
||||
.filter(node => node.type === 'block')
|
||||
.map(node => ({
|
||||
id: schema.itemByNodeID.get(node.id)!.id,
|
||||
nodeID: node.id,
|
||||
...computeAbsolutePosition(node, schema, nodeById),
|
||||
width: node.width ?? BLOCK_NODE_MIN_WIDTH,
|
||||
height: node.height ?? BLOCK_NODE_MIN_HEIGHT
|
||||
}))
|
||||
};
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user