Compare commits
No commits in common. "5c4149337b6486b888cc88ed9b7700f24163905d" and "c05759901ae1d867571581db9483c32d2fdde7a6" have entirely different histories.
5c4149337b
...
c05759901a
|
@ -1,73 +0,0 @@
|
||||||
# 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,6 +68,8 @@ class TestLibraryViewset(EndpointTester):
|
||||||
self.assertEqual(response.data['access_policy'], data['access_policy'])
|
self.assertEqual(response.data['access_policy'], data['access_policy'])
|
||||||
self.assertEqual(response.data['visible'], data['visible'])
|
self.assertEqual(response.data['visible'], data['visible'])
|
||||||
self.assertEqual(response.data['read_only'], data['read_only'])
|
self.assertEqual(response.data['read_only'], data['read_only'])
|
||||||
|
self.assertEqual(oss.layout().data['operations'], [])
|
||||||
|
self.assertEqual(oss.layout().data['blocks'], [])
|
||||||
|
|
||||||
self.logout()
|
self.logout()
|
||||||
data = {'title': 'Title2'}
|
data = {'title': 'Title2'}
|
||||||
|
|
|
@ -41,7 +41,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
||||||
else:
|
else:
|
||||||
serializer.save()
|
serializer.save()
|
||||||
if serializer.data.get('item_type') == m.LibraryItemType.OPERATION_SCHEMA:
|
if serializer.data.get('item_type') == m.LibraryItemType.OPERATION_SCHEMA:
|
||||||
Layout.objects.create(oss=serializer.instance, data=[])
|
Layout.objects.create(oss=serializer.instance, data={'operations': [], 'blocks': []})
|
||||||
|
|
||||||
def perform_update(self, serializer) -> None:
|
def perform_update(self, serializer) -> None:
|
||||||
instance = serializer.save()
|
instance = serializer.save()
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
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),
|
|
||||||
]
|
|
|
@ -1,18 +0,0 @@
|
||||||
# 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(
|
data = JSONField(
|
||||||
verbose_name='Расположение',
|
verbose_name='Расположение',
|
||||||
default=list
|
default=dict
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -40,7 +40,7 @@ class OperationSchema:
|
||||||
def create(**kwargs) -> 'OperationSchema':
|
def create(**kwargs) -> 'OperationSchema':
|
||||||
''' Create LibraryItem via OperationSchema. '''
|
''' Create LibraryItem via OperationSchema. '''
|
||||||
model = LibraryItem.objects.create(item_type=LibraryItemType.OPERATION_SCHEMA, **kwargs)
|
model = LibraryItem.objects.create(item_type=LibraryItemType.OPERATION_SCHEMA, **kwargs)
|
||||||
Layout.objects.create(oss=model, data=[])
|
Layout.objects.create(oss=model, data={'operations': [], 'blocks': []})
|
||||||
return OperationSchema(model)
|
return OperationSchema(model)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
@ -2,9 +2,16 @@
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
class NodeSerializer(serializers.Serializer):
|
class OperationNodeSerializer(serializers.Serializer):
|
||||||
|
''' Operation position. '''
|
||||||
|
id = serializers.IntegerField()
|
||||||
|
x = serializers.FloatField()
|
||||||
|
y = serializers.FloatField()
|
||||||
|
|
||||||
|
|
||||||
|
class BlockNodeSerializer(serializers.Serializer):
|
||||||
''' Block position. '''
|
''' Block position. '''
|
||||||
nodeID = serializers.CharField()
|
id = serializers.IntegerField()
|
||||||
x = serializers.FloatField()
|
x = serializers.FloatField()
|
||||||
y = serializers.FloatField()
|
y = serializers.FloatField()
|
||||||
width = serializers.FloatField()
|
width = serializers.FloatField()
|
||||||
|
@ -12,8 +19,13 @@ class NodeSerializer(serializers.Serializer):
|
||||||
|
|
||||||
|
|
||||||
class LayoutSerializer(serializers.Serializer):
|
class LayoutSerializer(serializers.Serializer):
|
||||||
''' Serializer: Layout data. '''
|
''' Layout for OperationSchema. '''
|
||||||
data = serializers.ListField(child=NodeSerializer()) # type: ignore
|
blocks = serializers.ListField(
|
||||||
|
child=BlockNodeSerializer()
|
||||||
|
)
|
||||||
|
operations = serializers.ListField(
|
||||||
|
child=OperationNodeSerializer()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SubstitutionExSerializer(serializers.Serializer):
|
class SubstitutionExSerializer(serializers.Serializer):
|
||||||
|
|
|
@ -13,7 +13,7 @@ from apps.rsform.serializers import SubstitutionSerializerBase
|
||||||
from shared import messages as msg
|
from shared import messages as msg
|
||||||
|
|
||||||
from ..models import Argument, Block, Inheritance, Operation, OperationSchema, OperationType
|
from ..models import Argument, Block, Inheritance, Operation, OperationSchema, OperationType
|
||||||
from .basics import NodeSerializer, SubstitutionExSerializer
|
from .basics import LayoutSerializer, SubstitutionExSerializer
|
||||||
|
|
||||||
|
|
||||||
class OperationSerializer(serializers.ModelSerializer):
|
class OperationSerializer(serializers.ModelSerializer):
|
||||||
|
@ -52,9 +52,7 @@ class CreateBlockSerializer(serializers.Serializer):
|
||||||
model = Block
|
model = Block
|
||||||
fields = 'title', 'description', 'parent'
|
fields = 'title', 'description', 'parent'
|
||||||
|
|
||||||
layout = serializers.ListField(
|
layout = LayoutSerializer()
|
||||||
child=NodeSerializer()
|
|
||||||
)
|
|
||||||
item_data = BlockCreateData()
|
item_data = BlockCreateData()
|
||||||
width = serializers.FloatField()
|
width = serializers.FloatField()
|
||||||
height = serializers.FloatField()
|
height = serializers.FloatField()
|
||||||
|
@ -102,10 +100,7 @@ class UpdateBlockSerializer(serializers.Serializer):
|
||||||
model = Block
|
model = Block
|
||||||
fields = 'title', 'description', 'parent'
|
fields = 'title', 'description', 'parent'
|
||||||
|
|
||||||
layout = serializers.ListField(
|
layout = LayoutSerializer(required=False)
|
||||||
child=NodeSerializer(),
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
target = PKField(many=False, queryset=Block.objects.all())
|
target = PKField(many=False, queryset=Block.objects.all())
|
||||||
item_data = UpdateBlockData()
|
item_data = UpdateBlockData()
|
||||||
|
|
||||||
|
@ -132,9 +127,7 @@ class UpdateBlockSerializer(serializers.Serializer):
|
||||||
|
|
||||||
class DeleteBlockSerializer(serializers.Serializer):
|
class DeleteBlockSerializer(serializers.Serializer):
|
||||||
''' Serializer: Delete block. '''
|
''' Serializer: Delete block. '''
|
||||||
layout = serializers.ListField(
|
layout = LayoutSerializer()
|
||||||
child=NodeSerializer()
|
|
||||||
)
|
|
||||||
target = PKField(many=False, queryset=Block.objects.all().only('oss_id'))
|
target = PKField(many=False, queryset=Block.objects.all().only('oss_id'))
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
|
@ -149,9 +142,7 @@ class DeleteBlockSerializer(serializers.Serializer):
|
||||||
|
|
||||||
class MoveItemsSerializer(serializers.Serializer):
|
class MoveItemsSerializer(serializers.Serializer):
|
||||||
''' Serializer: Move items to another parent. '''
|
''' Serializer: Move items to another parent. '''
|
||||||
layout = serializers.ListField(
|
layout = LayoutSerializer()
|
||||||
child=NodeSerializer()
|
|
||||||
)
|
|
||||||
operations = PKField(many=True, queryset=Operation.objects.all().only('oss_id', 'parent'))
|
operations = PKField(many=True, queryset=Operation.objects.all().only('oss_id', 'parent'))
|
||||||
blocks = PKField(many=True, queryset=Block.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)
|
destination = PKField(many=False, queryset=Block.objects.all().only('oss_id'), allow_null=True)
|
||||||
|
@ -205,12 +196,8 @@ class CreateOperationSerializer(serializers.Serializer):
|
||||||
'alias', 'operation_type', 'title', \
|
'alias', 'operation_type', 'title', \
|
||||||
'description', 'result', 'parent'
|
'description', 'result', 'parent'
|
||||||
|
|
||||||
layout = serializers.ListField(
|
layout = LayoutSerializer()
|
||||||
child=NodeSerializer()
|
|
||||||
)
|
|
||||||
item_data = CreateOperationData()
|
item_data = CreateOperationData()
|
||||||
width = serializers.FloatField()
|
|
||||||
height = serializers.FloatField()
|
|
||||||
position_x = serializers.FloatField()
|
position_x = serializers.FloatField()
|
||||||
position_y = serializers.FloatField()
|
position_y = serializers.FloatField()
|
||||||
create_schema = serializers.BooleanField(default=False, required=False)
|
create_schema = serializers.BooleanField(default=False, required=False)
|
||||||
|
@ -243,10 +230,7 @@ class UpdateOperationSerializer(serializers.Serializer):
|
||||||
model = Operation
|
model = Operation
|
||||||
fields = 'alias', 'title', 'description', 'parent'
|
fields = 'alias', 'title', 'description', 'parent'
|
||||||
|
|
||||||
layout = serializers.ListField(
|
layout = LayoutSerializer(required=False)
|
||||||
child=NodeSerializer(),
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
target = PKField(many=False, queryset=Operation.objects.all())
|
target = PKField(many=False, queryset=Operation.objects.all())
|
||||||
item_data = UpdateOperationData()
|
item_data = UpdateOperationData()
|
||||||
arguments = PKField(many=True, queryset=Operation.objects.all().only('oss_id', 'result_id'), required=False)
|
arguments = PKField(many=True, queryset=Operation.objects.all().only('oss_id', 'result_id'), required=False)
|
||||||
|
@ -313,9 +297,7 @@ class UpdateOperationSerializer(serializers.Serializer):
|
||||||
|
|
||||||
class DeleteOperationSerializer(serializers.Serializer):
|
class DeleteOperationSerializer(serializers.Serializer):
|
||||||
''' Serializer: Delete operation. '''
|
''' Serializer: Delete operation. '''
|
||||||
layout = serializers.ListField(
|
layout = LayoutSerializer()
|
||||||
child=NodeSerializer()
|
|
||||||
)
|
|
||||||
target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'result'))
|
target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'result'))
|
||||||
keep_constituents = serializers.BooleanField(default=False, required=False)
|
keep_constituents = serializers.BooleanField(default=False, required=False)
|
||||||
delete_schema = serializers.BooleanField(default=False, required=False)
|
delete_schema = serializers.BooleanField(default=False, required=False)
|
||||||
|
@ -332,9 +314,7 @@ class DeleteOperationSerializer(serializers.Serializer):
|
||||||
|
|
||||||
class TargetOperationSerializer(serializers.Serializer):
|
class TargetOperationSerializer(serializers.Serializer):
|
||||||
''' Serializer: Target single operation. '''
|
''' Serializer: Target single operation. '''
|
||||||
layout = serializers.ListField(
|
layout = LayoutSerializer()
|
||||||
child=NodeSerializer()
|
|
||||||
)
|
|
||||||
target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'result_id'))
|
target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'result_id'))
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
|
@ -349,9 +329,7 @@ class TargetOperationSerializer(serializers.Serializer):
|
||||||
|
|
||||||
class SetOperationInputSerializer(serializers.Serializer):
|
class SetOperationInputSerializer(serializers.Serializer):
|
||||||
''' Serializer: Set input schema for operation. '''
|
''' Serializer: Set input schema for operation. '''
|
||||||
layout = serializers.ListField(
|
layout = LayoutSerializer()
|
||||||
child=NodeSerializer()
|
|
||||||
)
|
|
||||||
target = PKField(many=False, queryset=Operation.objects.all())
|
target = PKField(many=False, queryset=Operation.objects.all())
|
||||||
input = PKField(
|
input = PKField(
|
||||||
many=False,
|
many=False,
|
||||||
|
@ -388,9 +366,7 @@ class OperationSchemaSerializer(serializers.ModelSerializer):
|
||||||
substitutions = serializers.ListField(
|
substitutions = serializers.ListField(
|
||||||
child=SubstitutionExSerializer()
|
child=SubstitutionExSerializer()
|
||||||
)
|
)
|
||||||
layout = serializers.ListField(
|
layout = LayoutSerializer()
|
||||||
child=NodeSerializer()
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
''' serializer metadata. '''
|
''' serializer metadata. '''
|
||||||
|
@ -483,7 +459,7 @@ class RelocateConstituentsSerializer(serializers.Serializer):
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
# ====== Internals ============
|
# ====== Internals =================================================================================
|
||||||
|
|
||||||
|
|
||||||
def _collect_descendants(start_blocks: list[Block]) -> set[int]:
|
def _collect_descendants(start_blocks: list[Block]) -> set[int]:
|
||||||
|
|
|
@ -59,11 +59,14 @@ class TestChangeAttributes(EndpointTester):
|
||||||
self.operation3.refresh_from_db()
|
self.operation3.refresh_from_db()
|
||||||
self.ks3 = RSForm(self.operation3.result)
|
self.ks3 = RSForm(self.operation3.result)
|
||||||
|
|
||||||
self.layout_data = [
|
self.layout_data = {
|
||||||
{'nodeID': 'o' + str(self.operation1.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
'operations': [
|
||||||
{'nodeID': 'o' + str(self.operation2.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
{'id': self.operation1.pk, 'x': 0, 'y': 0},
|
||||||
{'nodeID': 'o' + str(self.operation3.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
{'id': self.operation2.pk, 'x': 0, 'y': 0},
|
||||||
]
|
{'id': self.operation3.pk, 'x': 0, 'y': 0},
|
||||||
|
],
|
||||||
|
'blocks': []
|
||||||
|
}
|
||||||
layout = self.owned.layout()
|
layout = self.owned.layout()
|
||||||
layout.data = self.layout_data
|
layout.data = self.layout_data
|
||||||
layout.save()
|
layout.save()
|
||||||
|
|
|
@ -57,11 +57,14 @@ class TestChangeConstituents(EndpointTester):
|
||||||
self.ks3 = RSForm(self.operation3.result)
|
self.ks3 = RSForm(self.operation3.result)
|
||||||
self.assertEqual(self.ks3.constituents().count(), 4)
|
self.assertEqual(self.ks3.constituents().count(), 4)
|
||||||
|
|
||||||
self.layout_data = [
|
self.layout_data = {
|
||||||
{'nodeID': 'o' + str(self.operation1.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
'operations': [
|
||||||
{'nodeID': 'o' + str(self.operation2.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
{'id': self.operation1.pk, 'x': 0, 'y': 0},
|
||||||
{'nodeID': 'o' + str(self.operation3.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
{'id': self.operation2.pk, 'x': 0, 'y': 0},
|
||||||
]
|
{'id': self.operation3.pk, 'x': 0, 'y': 0},
|
||||||
|
],
|
||||||
|
'blocks': []
|
||||||
|
}
|
||||||
layout = self.owned.layout()
|
layout = self.owned.layout()
|
||||||
layout.data = self.layout_data
|
layout.data = self.layout_data
|
||||||
layout.save()
|
layout.save()
|
||||||
|
|
|
@ -107,13 +107,16 @@ class TestChangeOperations(EndpointTester):
|
||||||
convention='KS5D4'
|
convention='KS5D4'
|
||||||
)
|
)
|
||||||
|
|
||||||
self.layout_data = [
|
self.layout_data = {
|
||||||
{'nodeID': 'o' + str(self.operation1.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
'operations': [
|
||||||
{'nodeID': 'o' + str(self.operation2.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
{'id': self.operation1.pk, 'x': 0, 'y': 0},
|
||||||
{'nodeID': 'o' + str(self.operation3.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
{'id': self.operation2.pk, 'x': 0, 'y': 0},
|
||||||
{'nodeID': 'o' + str(self.operation4.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
{'id': self.operation3.pk, 'x': 0, 'y': 0},
|
||||||
{'nodeID': 'o' + str(self.operation5.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40}
|
{'id': self.operation4.pk, 'x': 0, 'y': 0},
|
||||||
]
|
{'id': self.operation5.pk, 'x': 0, 'y': 0},
|
||||||
|
],
|
||||||
|
'blocks': []
|
||||||
|
}
|
||||||
layout = self.owned.layout()
|
layout = self.owned.layout()
|
||||||
layout.data = self.layout_data
|
layout.data = self.layout_data
|
||||||
layout.save()
|
layout.save()
|
||||||
|
|
|
@ -107,13 +107,16 @@ class TestChangeSubstitutions(EndpointTester):
|
||||||
convention='KS5D4'
|
convention='KS5D4'
|
||||||
)
|
)
|
||||||
|
|
||||||
self.layout_data = [
|
self.layout_data = {
|
||||||
{'nodeID': 'o' + str(self.operation1.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
'operations': [
|
||||||
{'nodeID': 'o' + str(self.operation2.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
{'id': self.operation1.pk, 'x': 0, 'y': 0},
|
||||||
{'nodeID': 'o' + str(self.operation3.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
{'id': self.operation2.pk, 'x': 0, 'y': 0},
|
||||||
{'nodeID': 'o' + str(self.operation4.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
{'id': self.operation3.pk, 'x': 0, 'y': 0},
|
||||||
{'nodeID': 'o' + str(self.operation5.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
{'id': self.operation4.pk, 'x': 0, 'y': 0},
|
||||||
]
|
{'id': self.operation5.pk, 'x': 0, 'y': 0},
|
||||||
|
],
|
||||||
|
'blocks': []
|
||||||
|
}
|
||||||
layout = self.owned.layout()
|
layout = self.owned.layout()
|
||||||
layout.data = self.layout_data
|
layout.data = self.layout_data
|
||||||
layout.save()
|
layout.save()
|
||||||
|
|
|
@ -49,14 +49,16 @@ class TestOssBlocks(EndpointTester):
|
||||||
title='3',
|
title='3',
|
||||||
parent=self.block1
|
parent=self.block1
|
||||||
)
|
)
|
||||||
self.layout_data = [
|
self.layout_data = {
|
||||||
{'nodeID': 'o' + str(self.operation1.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
'operations': [
|
||||||
{'nodeID': 'o' + str(self.operation2.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
{'id': self.operation1.pk, 'x': 0, 'y': 0},
|
||||||
|
{'id': self.operation2.pk, 'x': 0, 'y': 0},
|
||||||
{'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},
|
'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},
|
||||||
|
]
|
||||||
|
}
|
||||||
layout = self.owned.layout()
|
layout = self.owned.layout()
|
||||||
layout.data = self.layout_data
|
layout.data = self.layout_data
|
||||||
layout.save()
|
layout.save()
|
||||||
|
@ -86,7 +88,7 @@ class TestOssBlocks(EndpointTester):
|
||||||
self.assertEqual(len(response.data['oss']['blocks']), 3)
|
self.assertEqual(len(response.data['oss']['blocks']), 3)
|
||||||
new_block = response.data['new_block']
|
new_block = response.data['new_block']
|
||||||
layout = response.data['oss']['layout']
|
layout = response.data['oss']['layout']
|
||||||
item = [item for item in layout if item['nodeID'] == 'b' + str(new_block['id'])][0]
|
item = [item for item in layout['blocks'] if item['id'] == new_block['id']][0]
|
||||||
self.assertEqual(new_block['title'], data['item_data']['title'])
|
self.assertEqual(new_block['title'], data['item_data']['title'])
|
||||||
self.assertEqual(new_block['description'], data['item_data']['description'])
|
self.assertEqual(new_block['description'], data['item_data']['description'])
|
||||||
self.assertEqual(new_block['parent'], None)
|
self.assertEqual(new_block['parent'], None)
|
||||||
|
|
|
@ -54,11 +54,14 @@ class TestOssOperations(EndpointTester):
|
||||||
alias='3',
|
alias='3',
|
||||||
operation_type=OperationType.SYNTHESIS
|
operation_type=OperationType.SYNTHESIS
|
||||||
)
|
)
|
||||||
self.layout_data = [
|
self.layout_data = {
|
||||||
{'nodeID': 'o' + str(self.operation1.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
'operations': [
|
||||||
{'nodeID': 'o' + str(self.operation2.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
{'id': self.operation1.pk, 'x': 0, 'y': 0},
|
||||||
{'nodeID': 'o' + str(self.operation3.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
{'id': self.operation2.pk, 'x': 0, 'y': 0},
|
||||||
]
|
{'id': self.operation3.pk, 'x': 0, 'y': 0},
|
||||||
|
],
|
||||||
|
'blocks': []
|
||||||
|
}
|
||||||
layout = self.owned.layout()
|
layout = self.owned.layout()
|
||||||
layout.data = self.layout_data
|
layout.data = self.layout_data
|
||||||
layout.save()
|
layout.save()
|
||||||
|
@ -84,9 +87,7 @@ class TestOssOperations(EndpointTester):
|
||||||
},
|
},
|
||||||
'layout': self.layout_data,
|
'layout': self.layout_data,
|
||||||
'position_x': 1,
|
'position_x': 1,
|
||||||
'position_y': 1,
|
'position_y': 1
|
||||||
'width': 500,
|
|
||||||
'height': 50
|
|
||||||
|
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data=data)
|
||||||
|
@ -101,7 +102,7 @@ class TestOssOperations(EndpointTester):
|
||||||
self.assertEqual(len(response.data['oss']['operations']), 4)
|
self.assertEqual(len(response.data['oss']['operations']), 4)
|
||||||
new_operation = response.data['new_operation']
|
new_operation = response.data['new_operation']
|
||||||
layout = response.data['oss']['layout']
|
layout = response.data['oss']['layout']
|
||||||
item = [item for item in layout if item['nodeID'] == 'o' + str(new_operation['id'])][0]
|
item = [item for item in layout['operations'] if item['id'] == new_operation['id']][0]
|
||||||
self.assertEqual(new_operation['alias'], data['item_data']['alias'])
|
self.assertEqual(new_operation['alias'], data['item_data']['alias'])
|
||||||
self.assertEqual(new_operation['operation_type'], data['item_data']['operation_type'])
|
self.assertEqual(new_operation['operation_type'], data['item_data']['operation_type'])
|
||||||
self.assertEqual(new_operation['title'], data['item_data']['title'])
|
self.assertEqual(new_operation['title'], data['item_data']['title'])
|
||||||
|
@ -110,8 +111,6 @@ class TestOssOperations(EndpointTester):
|
||||||
self.assertEqual(new_operation['parent'], None)
|
self.assertEqual(new_operation['parent'], None)
|
||||||
self.assertEqual(item['x'], data['position_x'])
|
self.assertEqual(item['x'], data['position_x'])
|
||||||
self.assertEqual(item['y'], data['position_y'])
|
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.operation1.refresh_from_db()
|
||||||
|
|
||||||
self.executeForbidden(data=data, item=self.unowned_id)
|
self.executeForbidden(data=data, item=self.unowned_id)
|
||||||
|
@ -133,9 +132,7 @@ class TestOssOperations(EndpointTester):
|
||||||
},
|
},
|
||||||
'layout': self.layout_data,
|
'layout': self.layout_data,
|
||||||
'position_x': 1,
|
'position_x': 1,
|
||||||
'position_y': 1,
|
'position_y': 1
|
||||||
'width': 500,
|
|
||||||
'height': 50
|
|
||||||
|
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data, item=self.owned_id)
|
self.executeBadData(data=data, item=self.owned_id)
|
||||||
|
@ -163,8 +160,6 @@ class TestOssOperations(EndpointTester):
|
||||||
'layout': self.layout_data,
|
'layout': self.layout_data,
|
||||||
'position_x': 1,
|
'position_x': 1,
|
||||||
'position_y': 1,
|
'position_y': 1,
|
||||||
'width': 500,
|
|
||||||
'height': 50,
|
|
||||||
'arguments': [self.operation1.pk, self.operation3.pk]
|
'arguments': [self.operation1.pk, self.operation3.pk]
|
||||||
}
|
}
|
||||||
response = self.executeCreated(data=data, item=self.owned_id)
|
response = self.executeCreated(data=data, item=self.owned_id)
|
||||||
|
@ -190,9 +185,7 @@ class TestOssOperations(EndpointTester):
|
||||||
},
|
},
|
||||||
'layout': self.layout_data,
|
'layout': self.layout_data,
|
||||||
'position_x': 1,
|
'position_x': 1,
|
||||||
'position_y': 1,
|
'position_y': 1
|
||||||
'width': 500,
|
|
||||||
'height': 50
|
|
||||||
}
|
}
|
||||||
response = self.executeCreated(data=data, item=self.owned_id)
|
response = self.executeCreated(data=data, item=self.owned_id)
|
||||||
new_operation = response.data['new_operation']
|
new_operation = response.data['new_operation']
|
||||||
|
@ -214,9 +207,7 @@ class TestOssOperations(EndpointTester):
|
||||||
'create_schema': True,
|
'create_schema': True,
|
||||||
'layout': self.layout_data,
|
'layout': self.layout_data,
|
||||||
'position_x': 1,
|
'position_x': 1,
|
||||||
'position_y': 1,
|
'position_y': 1
|
||||||
'width': 500,
|
|
||||||
'height': 50
|
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data, item=self.owned_id)
|
self.executeBadData(data=data, item=self.owned_id)
|
||||||
data['item_data']['result'] = None
|
data['item_data']['result'] = None
|
||||||
|
@ -253,7 +244,7 @@ class TestOssOperations(EndpointTester):
|
||||||
self.login()
|
self.login()
|
||||||
response = self.executeOK(data=data)
|
response = self.executeOK(data=data)
|
||||||
layout = response.data['layout']
|
layout = response.data['layout']
|
||||||
deleted_items = [item for item in layout if item['nodeID'] == 'o' + str(data['target'])]
|
deleted_items = [item for item in layout['operations'] if item['id'] == data['target']]
|
||||||
self.assertEqual(len(response.data['operations']), 2)
|
self.assertEqual(len(response.data['operations']), 2)
|
||||||
self.assertEqual(len(deleted_items), 0)
|
self.assertEqual(len(deleted_items), 0)
|
||||||
|
|
||||||
|
|
|
@ -55,11 +55,11 @@ class TestOssViewset(EndpointTester):
|
||||||
alias='3',
|
alias='3',
|
||||||
operation_type=OperationType.SYNTHESIS
|
operation_type=OperationType.SYNTHESIS
|
||||||
)
|
)
|
||||||
self.layout_data = [
|
self.layout_data = {'operations': [
|
||||||
{'nodeID': 'o' + str(self.operation1.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
{'id': self.operation1.pk, 'x': 0, 'y': 0},
|
||||||
{'nodeID': 'o' + str(self.operation2.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
{'id': self.operation2.pk, 'x': 0, 'y': 0},
|
||||||
{'nodeID': 'o' + str(self.operation3.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40}
|
{'id': self.operation3.pk, 'x': 0, 'y': 0},
|
||||||
]
|
], 'blocks': []}
|
||||||
layout = self.owned.layout()
|
layout = self.owned.layout()
|
||||||
layout.data = self.layout_data
|
layout.data = self.layout_data
|
||||||
layout.save()
|
layout.save()
|
||||||
|
@ -107,9 +107,10 @@ class TestOssViewset(EndpointTester):
|
||||||
self.assertEqual(arguments[1]['argument'], self.operation2.pk)
|
self.assertEqual(arguments[1]['argument'], self.operation2.pk)
|
||||||
|
|
||||||
layout = response.data['layout']
|
layout = response.data['layout']
|
||||||
self.assertEqual(layout[0], self.layout_data[0])
|
self.assertEqual(layout['blocks'], [])
|
||||||
self.assertEqual(layout[1], self.layout_data[1])
|
self.assertEqual(layout['operations'][0], {'id': self.operation1.pk, 'x': 0, 'y': 0})
|
||||||
self.assertEqual(layout[2], self.layout_data[2])
|
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.executeOK(item=self.unowned_id)
|
self.executeOK(item=self.unowned_id)
|
||||||
self.executeForbidden(item=self.private_id)
|
self.executeForbidden(item=self.private_id)
|
||||||
|
@ -125,21 +126,23 @@ class TestOssViewset(EndpointTester):
|
||||||
self.populateData()
|
self.populateData()
|
||||||
self.executeBadData(item=self.owned_id)
|
self.executeBadData(item=self.owned_id)
|
||||||
|
|
||||||
data = {'data': []}
|
data = {'operations': [], 'blocks': []}
|
||||||
self.executeOK(data=data)
|
self.executeOK(data=data)
|
||||||
|
|
||||||
data = {'data': [
|
data = {
|
||||||
{'nodeID': 'o' + str(self.operation1.pk), 'x': 42.1, 'y': 1337, 'width': 150, 'height': 40},
|
'operations': [
|
||||||
{'nodeID': 'o' + str(self.operation2.pk), 'x': 36.1, 'y': 1437, 'width': 150, 'height': 40},
|
{'id': self.operation1.pk, 'x': 42.1, 'y': 1337},
|
||||||
{'nodeID': 'o' + str(self.operation3.pk), 'x': 36.1, 'y': 1435, 'width': 150, 'height': 40}
|
{'id': self.operation2.pk, 'x': 36.1, 'y': 1437},
|
||||||
]}
|
{'id': self.operation3.pk, 'x': 36.1, 'y': 1435}
|
||||||
|
], 'blocks': []
|
||||||
|
}
|
||||||
self.toggle_admin(True)
|
self.toggle_admin(True)
|
||||||
self.executeOK(data=data, item=self.unowned_id)
|
self.executeOK(data=data, item=self.unowned_id)
|
||||||
|
|
||||||
self.toggle_admin(False)
|
self.toggle_admin(False)
|
||||||
self.executeOK(data=data, item=self.owned_id)
|
self.executeOK(data=data, item=self.owned_id)
|
||||||
self.owned.refresh_from_db()
|
self.owned.refresh_from_db()
|
||||||
self.assertEqual(self.owned.layout().data, data['data'])
|
self.assertEqual(self.owned.layout().data, data)
|
||||||
|
|
||||||
self.executeForbidden(data=data, item=self.unowned_id)
|
self.executeForbidden(data=data, item=self.unowned_id)
|
||||||
self.executeForbidden(data=data, item=self.private_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. '''
|
''' Endpoint: Update schema layout. '''
|
||||||
serializer = s.LayoutSerializer(data=request.data)
|
serializer = s.LayoutSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
m.OperationSchema(self.get_object()).update_layout(serializer.validated_data['data'])
|
m.OperationSchema(self.get_object()).update_layout(serializer.validated_data)
|
||||||
return Response(status=c.HTTP_200_OK)
|
return Response(status=c.HTTP_200_OK)
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
|
@ -120,8 +120,8 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
children_operations: list[m.Operation] = serializer.validated_data['children_operations']
|
children_operations: list[m.Operation] = serializer.validated_data['children_operations']
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
new_block = oss.create_block(**serializer.validated_data['item_data'])
|
new_block = oss.create_block(**serializer.validated_data['item_data'])
|
||||||
layout.append({
|
layout['blocks'].append({
|
||||||
'nodeID': 'b' + str(new_block.pk),
|
'id': new_block.pk,
|
||||||
'x': serializer.validated_data['position_x'],
|
'x': serializer.validated_data['position_x'],
|
||||||
'y': serializer.validated_data['position_y'],
|
'y': serializer.validated_data['position_y'],
|
||||||
'width': serializer.validated_data['width'],
|
'width': serializer.validated_data['width'],
|
||||||
|
@ -205,7 +205,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
oss = m.OperationSchema(self.get_object())
|
oss = m.OperationSchema(self.get_object())
|
||||||
block = cast(m.Block, serializer.validated_data['target'])
|
block = cast(m.Block, serializer.validated_data['target'])
|
||||||
layout = serializer.validated_data['layout']
|
layout = serializer.validated_data['layout']
|
||||||
layout = [x for x in layout if x['nodeID'] != 'b' + str(block.pk)]
|
layout['blocks'] = [x for x in layout['blocks'] if x['id'] != block.pk]
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
oss.delete_block(block)
|
oss.delete_block(block)
|
||||||
oss.update_layout(layout)
|
oss.update_layout(layout)
|
||||||
|
@ -274,12 +274,10 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
layout = serializer.validated_data['layout']
|
layout = serializer.validated_data['layout']
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
new_operation = oss.create_operation(**serializer.validated_data['item_data'])
|
new_operation = oss.create_operation(**serializer.validated_data['item_data'])
|
||||||
layout.append({
|
layout['operations'].append({
|
||||||
'nodeID': 'o' + str(new_operation.pk),
|
'id': new_operation.pk,
|
||||||
'x': serializer.validated_data['position_x'],
|
'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)
|
oss.update_layout(layout)
|
||||||
|
|
||||||
|
@ -344,9 +342,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
operation.title = serializer.validated_data['item_data']['title']
|
operation.title = serializer.validated_data['item_data']['title']
|
||||||
if 'description' in serializer.validated_data['item_data']:
|
if 'description' in serializer.validated_data['item_data']:
|
||||||
operation.description = serializer.validated_data['item_data']['description']
|
operation.description = serializer.validated_data['item_data']['description']
|
||||||
if 'parent' in serializer.validated_data['item_data']:
|
operation.save(update_fields=['alias', 'title', 'description'])
|
||||||
operation.parent = serializer.validated_data['item_data']['parent']
|
|
||||||
operation.save(update_fields=['alias', 'title', 'description', 'parent'])
|
|
||||||
|
|
||||||
if operation.result is not None:
|
if operation.result is not None:
|
||||||
can_edit = permissions.can_edit_item(request.user, operation.result)
|
can_edit = permissions.can_edit_item(request.user, operation.result)
|
||||||
|
@ -388,7 +384,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
operation = cast(m.Operation, serializer.validated_data['target'])
|
operation = cast(m.Operation, serializer.validated_data['target'])
|
||||||
old_schema = operation.result
|
old_schema = operation.result
|
||||||
layout = serializer.validated_data['layout']
|
layout = serializer.validated_data['layout']
|
||||||
layout = [x for x in layout if x['nodeID'] != 'o' + str(operation.pk)]
|
layout['operations'] = [x for x in layout['operations'] if x['id'] != operation.pk]
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
oss.delete_operation(operation.pk, serializer.validated_data['keep_constituents'])
|
oss.delete_operation(operation.pk, serializer.validated_data['keep_constituents'])
|
||||||
oss.update_layout(layout)
|
oss.update_layout(layout)
|
||||||
|
|
|
@ -7475,44 +7475,36 @@
|
||||||
"pk": 1,
|
"pk": 1,
|
||||||
"fields": {
|
"fields": {
|
||||||
"oss": 41,
|
"oss": 41,
|
||||||
"data": [
|
"data": {
|
||||||
{
|
"blocks": [],
|
||||||
"x": 530.0,
|
"operations": [
|
||||||
"y": 370.0,
|
{
|
||||||
"nodeID": "o1",
|
"x": 530.0,
|
||||||
"width": 150.0,
|
"y": 370.0,
|
||||||
"height": 40.0
|
"id": 1
|
||||||
},
|
},
|
||||||
|
{
|
||||||
{
|
"x": 710.0,
|
||||||
"x": 710.0,
|
"y": 370.0,
|
||||||
"y": 370.0,
|
"id": 2
|
||||||
"nodeID": "o2",
|
},
|
||||||
"width": 150.0,
|
{
|
||||||
"height": 40.0
|
"x": 890.0,
|
||||||
},
|
"y": 370.0,
|
||||||
{
|
"id": 4
|
||||||
"x": 890.0,
|
},
|
||||||
"y": 370.0,
|
{
|
||||||
"nodeID": "o4",
|
"x": 620.0,
|
||||||
"width": 150.0,
|
"y": 470.0,
|
||||||
"height": 40.0
|
"id": 9
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 620.0,
|
"x": 760.0,
|
||||||
"y": 470.0,
|
"y": 570.0,
|
||||||
"nodeID": "o9",
|
"id": 10
|
||||||
"width": 150.0,
|
}
|
||||||
"height": 40.0
|
]
|
||||||
},
|
}
|
||||||
{
|
|
||||||
"x": 760.0,
|
|
||||||
"y": 570.0,
|
|
||||||
"nodeID": "o10",
|
|
||||||
"width": 150.0,
|
|
||||||
"height": 40.0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -29,18 +29,6 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<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>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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"
|
"preview": "vite preview --port 3000"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dagrejs/dagre": "^1.1.5",
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
"@hookform/resolvers": "^5.1.1",
|
"@hookform/resolvers": "^5.0.1",
|
||||||
"@lezer/lr": "^1.4.2",
|
"@lezer/lr": "^1.4.2",
|
||||||
"@radix-ui/react-popover": "^1.1.14",
|
"@radix-ui/react-popover": "^1.1.14",
|
||||||
"@radix-ui/react-select": "^2.2.5",
|
"@radix-ui/react-select": "^2.2.5",
|
||||||
"@tanstack/react-query": "^5.81.5",
|
"@tanstack/react-query": "^5.80.5",
|
||||||
"@tanstack/react-query-devtools": "^5.81.5",
|
"@tanstack/react-query-devtools": "^5.80.5",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@uiw/codemirror-themes": "^4.23.14",
|
"@uiw/codemirror-themes": "^4.23.12",
|
||||||
"@uiw/react-codemirror": "^4.23.14",
|
"@uiw/react-codemirror": "^4.23.12",
|
||||||
"axios": "^1.10.0",
|
"axios": "^1.9.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"global": "^4.4.0",
|
"global": "^4.4.0",
|
||||||
"js-file-download": "^0.4.12",
|
"js-file-download": "^0.4.12",
|
||||||
"lucide-react": "^0.525.0",
|
"lucide-react": "^0.511.0",
|
||||||
"qrcode.react": "^4.2.0",
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-error-boundary": "^6.0.0",
|
"react-error-boundary": "^6.0.0",
|
||||||
"react-hook-form": "^7.59.0",
|
"react-hook-form": "^7.57.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-intl": "^7.1.11",
|
"react-intl": "^7.1.11",
|
||||||
"react-router": "^7.6.3",
|
"react-router": "^7.6.2",
|
||||||
"react-scan": "^0.3.6",
|
"react-scan": "^0.3.4",
|
||||||
"react-tabs": "^6.1.0",
|
"react-tabs": "^6.1.0",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
"react-tooltip": "^5.29.1",
|
"react-tooltip": "^5.28.1",
|
||||||
"react-zoom-pan-pinch": "^3.7.0",
|
"react-zoom-pan-pinch": "^3.7.0",
|
||||||
"reactflow": "^11.11.4",
|
"reactflow": "^11.11.4",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.0",
|
||||||
"tw-animate-css": "^1.3.4",
|
"tw-animate-css": "^1.3.4",
|
||||||
"use-debounce": "^10.0.5",
|
"use-debounce": "^10.0.4",
|
||||||
"zod": "^3.25.67",
|
"zod": "^3.25.51",
|
||||||
"zustand": "^5.0.6"
|
"zustand": "^5.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lezer/generator": "^1.8.0",
|
"@lezer/generator": "^1.7.3",
|
||||||
"@playwright/test": "^1.53.2",
|
"@playwright/test": "^1.52.0",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.8",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^24.0.8",
|
"@types/node": "^22.15.29",
|
||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.6",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
||||||
"@typescript-eslint/parser": "^8.0.1",
|
"@typescript-eslint/parser": "^8.0.1",
|
||||||
"@vitejs/plugin-react": "^4.6.0",
|
"@vitejs/plugin-react": "^4.5.1",
|
||||||
"babel-plugin-react-compiler": "^19.1.0-rc.1",
|
"babel-plugin-react-compiler": "^19.1.0-rc.1",
|
||||||
"eslint": "^9.30.0",
|
"eslint": "^9.28.0",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-import": "^2.31.0",
|
||||||
"eslint-plugin-playwright": "^2.2.0",
|
"eslint-plugin-playwright": "^2.2.0",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-compiler": "^19.1.0-rc.1",
|
"eslint-plugin-react-compiler": "^19.1.0-rc.1",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"globals": "^16.3.0",
|
"globals": "^16.2.0",
|
||||||
"jest": "^30.0.3",
|
"jest": "^29.7.0",
|
||||||
"stylelint": "^16.21.0",
|
"stylelint": "^16.20.0",
|
||||||
"stylelint-config-recommended": "^16.0.0",
|
"stylelint-config-recommended": "^16.0.0",
|
||||||
"stylelint-config-standard": "^38.0.0",
|
"stylelint-config-standard": "^38.0.0",
|
||||||
"stylelint-config-tailwindcss": "^1.0.0",
|
"stylelint-config-tailwindcss": "^1.0.0",
|
||||||
"tailwindcss": "^4.0.7",
|
"tailwindcss": "^4.0.7",
|
||||||
"ts-jest": "^29.4.0",
|
"ts-jest": "^29.3.4",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"typescript-eslint": "^8.35.1",
|
"typescript-eslint": "^8.33.1",
|
||||||
"vite": "^7.0.0"
|
"vite": "^6.3.5"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"preset": "ts-jest",
|
"preset": "ts-jest",
|
||||||
|
|
|
@ -14,10 +14,10 @@ export function Footer() {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<nav className='flex gap-3' aria-label='Вторичная навигация'>
|
<nav className='flex gap-3' aria-label='Вторичная навигация'>
|
||||||
<TextURL text='Библиотека' href='/library' color='hover:text-foreground' />
|
<TextURL text='Библиотека' href='/library' color='' />
|
||||||
<TextURL text='Справка' href='/manuals' color='hover:text-foreground' />
|
<TextURL text='Справка' href='/manuals' color='' />
|
||||||
<TextURL text='Центр Концепт' href={external_urls.concept} color='hover:text-foreground' />
|
<TextURL text='Центр Концепт' href={external_urls.concept} color='' />
|
||||||
<TextURL text='Экстеор' href='/manuals?topic=exteor' color='hover:text-foreground' />
|
<TextURL text='Экстеор' href='/manuals?topic=exteor' color='' />
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<p>© 2025 ЦИВТ КОНЦЕПТ</p>
|
<p>© 2025 ЦИВТ КОНЦЕПТ</p>
|
||||||
|
|
|
@ -10,9 +10,8 @@ export const GlobalTooltips = () => {
|
||||||
float
|
float
|
||||||
id={globalIDs.tooltip}
|
id={globalIDs.tooltip}
|
||||||
layer='z-topmost'
|
layer='z-topmost'
|
||||||
place='bottom-start'
|
place='right-start'
|
||||||
offset={24}
|
className='mt-8 max-w-80 break-words rounded-lg!'
|
||||||
className='max-w-80 break-words rounded-lg! select-none'
|
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
float
|
float
|
||||||
|
|
|
@ -25,12 +25,8 @@ export function ToggleNavigation() {
|
||||||
data-tooltip-content={noNavigationAnimation ? 'Показать навигацию' : 'Скрыть навигацию'}
|
data-tooltip-content={noNavigationAnimation ? 'Показать навигацию' : 'Скрыть навигацию'}
|
||||||
aria-label={noNavigationAnimation ? 'Показать навигацию' : 'Скрыть навигацию'}
|
aria-label={noNavigationAnimation ? 'Показать навигацию' : 'Скрыть навигацию'}
|
||||||
>
|
>
|
||||||
{!noNavigationAnimation ? (
|
{!noNavigationAnimation ? <IconPin size='0.75rem' /> : null}
|
||||||
<IconPin size='0.75rem' className='hover:text-primary cc-animate-color cc-hover-pulse' />
|
{noNavigationAnimation ? <IconUnpin size='0.75rem' /> : null}
|
||||||
) : null}
|
|
||||||
{noNavigationAnimation ? (
|
|
||||||
<IconUnpin size='0.75rem' className='hover:text-primary cc-animate-color cc-hover-pulse' />
|
|
||||||
) : null}
|
|
||||||
</button>
|
</button>
|
||||||
{!noNavigationAnimation ? (
|
{!noNavigationAnimation ? (
|
||||||
<button
|
<button
|
||||||
|
@ -42,12 +38,8 @@ export function ToggleNavigation() {
|
||||||
data-tooltip-content={darkMode ? 'Тема: Темная' : 'Тема: Светлая'}
|
data-tooltip-content={darkMode ? 'Тема: Темная' : 'Тема: Светлая'}
|
||||||
aria-label={darkMode ? 'Тема: Темная' : 'Тема: Светлая'}
|
aria-label={darkMode ? 'Тема: Темная' : 'Тема: Светлая'}
|
||||||
>
|
>
|
||||||
{darkMode ? (
|
{darkMode ? <IconDarkTheme size='0.75rem' /> : null}
|
||||||
<IconDarkTheme size='0.75rem' className='hover:text-primary cc-animate-color cc-hover-pulse' />
|
{!darkMode ? <IconLightTheme size='0.75rem' /> : null}
|
||||||
) : null}
|
|
||||||
{!darkMode ? (
|
|
||||||
<IconLightTheme size='0.75rem' className='hover:text-primary cc-animate-color cc-hover-pulse' />
|
|
||||||
) : null}
|
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -18,6 +18,7 @@ export function UserButton({ onLogin, onClickUser, isOpen }: UserButtonProps) {
|
||||||
if (isAnonymous) {
|
if (isAnonymous) {
|
||||||
return (
|
return (
|
||||||
<NavigationButton
|
<NavigationButton
|
||||||
|
className='cc-fade-in'
|
||||||
title='Перейти на страницу логина'
|
title='Перейти на страницу логина'
|
||||||
icon={<IconLogin size='1.5rem' className='icon-primary' />}
|
icon={<IconLogin size='1.5rem' className='icon-primary' />}
|
||||||
onClick={onLogin}
|
onClick={onLogin}
|
||||||
|
@ -26,6 +27,7 @@ export function UserButton({ onLogin, onClickUser, isOpen }: UserButtonProps) {
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<NavigationButton
|
<NavigationButton
|
||||||
|
className='cc-fade-in'
|
||||||
title='Пользователь'
|
title='Пользователь'
|
||||||
hideTitle={isOpen}
|
hideTitle={isOpen}
|
||||||
aria-haspopup='true'
|
aria-haspopup='true'
|
||||||
|
|
|
@ -40,7 +40,7 @@ export function Button({
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex gap-2 items-center justify-center',
|
'inline-flex gap-2 items-center justify-center',
|
||||||
'font-medium select-none disabled:cursor-auto disabled:opacity-75',
|
'font-medium select-none disabled:cursor-auto disabled:opacity-75',
|
||||||
'bg-secondary text-secondary-foreground cc-hover-bg cc-animate-color',
|
'bg-secondary text-secondary-foreground cc-hover cc-animate-color',
|
||||||
dense ? 'px-1' : 'px-3 py-1',
|
dense ? 'px-1' : 'px-3 py-1',
|
||||||
loading ? 'cursor-progress' : 'cursor-pointer',
|
loading ? 'cursor-progress' : 'cursor-pointer',
|
||||||
noOutline ? 'outline-hidden focus-visible:bg-selected' : 'focus-outline',
|
noOutline ? 'outline-hidden focus-visible:bg-selected' : 'focus-outline',
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { globalIDs } from '@/utils/constants';
|
import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
import { type Button } from '../props';
|
import { type Button } from '../props';
|
||||||
import { cn } from '../utils';
|
|
||||||
|
|
||||||
interface MiniButtonProps extends Button {
|
interface MiniButtonProps extends Button {
|
||||||
/** Button type. */
|
/** Button type. */
|
||||||
|
@ -36,12 +37,11 @@ export function MiniButton({
|
||||||
<button
|
<button
|
||||||
type={type}
|
type={type}
|
||||||
tabIndex={tabIndex ?? -1}
|
tabIndex={tabIndex ?? -1}
|
||||||
className={cn(
|
className={clsx(
|
||||||
'rounded-lg',
|
'rounded-lg',
|
||||||
'text-muted-foreground cc-animate-color',
|
'cc-controls cc-animate-background',
|
||||||
'cursor-pointer disabled:cursor-auto disabled:opacity-75',
|
'cursor-pointer disabled:cursor-auto',
|
||||||
(!tabIndex || tabIndex === -1) && 'outline-hidden',
|
noHover ? 'outline-hidden' : 'cc-hover',
|
||||||
!noHover && 'cc-hover-pulse',
|
|
||||||
!noPadding && 'px-1 py-1',
|
!noPadding && 'px-1 py-1',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -30,9 +30,9 @@ export function SelectorButton({
|
||||||
className={cn(
|
className={cn(
|
||||||
'px-1 flex flex-start items-center gap-1',
|
'px-1 flex flex-start items-center gap-1',
|
||||||
'text-sm font-controls select-none',
|
'text-sm font-controls select-none',
|
||||||
'disabled:cursor-auto cursor-pointer outline-hidden',
|
'text-btn cc-controls',
|
||||||
'text-muted-foreground cc-hover-text cc-animate-color disabled:opacity-75',
|
'disabled:cursor-auto cursor-pointer',
|
||||||
!text && 'cc-hover-pulse',
|
'cc-hover cc-animate-color',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
||||||
|
|
|
@ -20,10 +20,8 @@ export function PaginationTools<TData>({
|
||||||
onChangePaginationOption,
|
onChangePaginationOption,
|
||||||
paginationOptions
|
paginationOptions
|
||||||
}: PaginationToolsProps<TData>) {
|
}: PaginationToolsProps<TData>) {
|
||||||
const buttonClass =
|
|
||||||
'cc-hover-text cc-animate-color focus-outline rounded-md disabled:opacity-75 not-[:disabled]:cursor-pointer';
|
|
||||||
return (
|
return (
|
||||||
<div className='flex justify-end items-center my-2 text-muted-foreground text-sm select-none'>
|
<div className='flex justify-end items-center my-2 text-sm cc-controls select-none'>
|
||||||
<span className='mr-3'>
|
<span className='mr-3'>
|
||||||
{`${table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1}
|
{`${table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1}
|
||||||
-
|
-
|
||||||
|
@ -38,7 +36,7 @@ export function PaginationTools<TData>({
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
aria-label='Первая страница'
|
aria-label='Первая страница'
|
||||||
className={buttonClass}
|
className='cc-hover cc-controls cc-animate-color focus-outline'
|
||||||
onClick={() => table.setPageIndex(0)}
|
onClick={() => table.setPageIndex(0)}
|
||||||
disabled={!table.getCanPreviousPage()}
|
disabled={!table.getCanPreviousPage()}
|
||||||
>
|
>
|
||||||
|
@ -47,7 +45,7 @@ export function PaginationTools<TData>({
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
aria-label='Предыдущая страница'
|
aria-label='Предыдущая страница'
|
||||||
className={buttonClass}
|
className='cc-hover cc-controls cc-animate-color focus-outline'
|
||||||
onClick={() => table.previousPage()}
|
onClick={() => table.previousPage()}
|
||||||
disabled={!table.getCanPreviousPage()}
|
disabled={!table.getCanPreviousPage()}
|
||||||
>
|
>
|
||||||
|
@ -57,7 +55,7 @@ export function PaginationTools<TData>({
|
||||||
id={id ? `${id}__page` : undefined}
|
id={id ? `${id}__page` : undefined}
|
||||||
title='Номер страницы. Выделите для ручного ввода'
|
title='Номер страницы. Выделите для ручного ввода'
|
||||||
aria-label='Номер страницы'
|
aria-label='Номер страницы'
|
||||||
className='w-6 text-center bg-transparent focus-outline rounded-md'
|
className='w-6 text-center bg-transparent focus-outline'
|
||||||
value={table.getState().pagination.pageIndex + 1}
|
value={table.getState().pagination.pageIndex + 1}
|
||||||
onChange={event => {
|
onChange={event => {
|
||||||
const page = event.target.value ? Number(event.target.value) - 1 : 0;
|
const page = event.target.value ? Number(event.target.value) - 1 : 0;
|
||||||
|
@ -69,7 +67,7 @@ export function PaginationTools<TData>({
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
aria-label='Следующая страница'
|
aria-label='Следующая страница'
|
||||||
className={buttonClass}
|
className='cc-hover cc-controls cc-animate-color focus-outline'
|
||||||
onClick={() => table.nextPage()}
|
onClick={() => table.nextPage()}
|
||||||
disabled={!table.getCanNextPage()}
|
disabled={!table.getCanNextPage()}
|
||||||
>
|
>
|
||||||
|
@ -78,7 +76,7 @@ export function PaginationTools<TData>({
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
aria-label='Последняя страница'
|
aria-label='Последняя страница'
|
||||||
className={buttonClass}
|
className='cc-hover cc-controls cc-animate-color focus-outline'
|
||||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||||
disabled={!table.getCanNextPage()}
|
disabled={!table.getCanNextPage()}
|
||||||
>
|
>
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { type Table } from '@tanstack/react-table';
|
import { type Table } from '@tanstack/react-table';
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
import { prefixes } from '@/utils/constants';
|
import { prefixes } from '@/utils/constants';
|
||||||
|
|
||||||
|
@ -31,12 +30,7 @@ export function SelectPagination<TData>({ id, table, paginationOptions, onChange
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
id={id}
|
id={id}
|
||||||
aria-label='Выбор количества строчек на странице'
|
aria-label='Выбор количества строчек на странице'
|
||||||
className={clsx(
|
className='mx-2 cursor-pointer bg-transparent focus-outline border-0 w-28 max-h-6 px-2 justify-end'
|
||||||
'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 />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
|
@ -70,7 +70,7 @@ export function TableRow<TData>({
|
||||||
<tr
|
<tr
|
||||||
className={cn(
|
className={cn(
|
||||||
'cc-scroll-row',
|
'cc-scroll-row',
|
||||||
'cc-hover-bg cc-animate-background duration-fade',
|
'cc-hover cc-animate-background duration-fade',
|
||||||
!noHeader && 'scroll-mt-[calc(2px+2rem)]',
|
!noHeader && 'scroll-mt-[calc(2px+2rem)]',
|
||||||
table.options.enableRowSelection && row.getIsSelected()
|
table.options.enableRowSelection && row.getIsSelected()
|
||||||
? 'cc-selected'
|
? 'cc-selected'
|
||||||
|
|
|
@ -36,9 +36,9 @@ export function DropdownButton({
|
||||||
className={cn(
|
className={cn(
|
||||||
'px-3 py-1 inline-flex items-center gap-2',
|
'px-3 py-1 inline-flex items-center gap-2',
|
||||||
'text-left text-sm text-ellipsis whitespace-nowrap',
|
'text-left text-sm text-ellipsis whitespace-nowrap',
|
||||||
'disabled:text-muted-foreground disabled:opacity-75',
|
'disabled:cc-controls disabled:opacity-75',
|
||||||
'focus-outline cc-animate-background',
|
'focus-outline cc-animate-background',
|
||||||
!!onClick ? 'cc-hover-bg cursor-pointer disabled:cursor-auto' : 'bg-secondary text-secondary-foreground',
|
!!onClick ? 'cc-hover cursor-pointer disabled:cursor-auto' : 'bg-secondary text-secondary-foreground',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
||||||
|
|
|
@ -25,10 +25,6 @@ export { LuQrCode as IconQR } from 'react-icons/lu';
|
||||||
export { LuFilterX as IconFilterReset } from 'react-icons/lu';
|
export { LuFilterX as IconFilterReset } from 'react-icons/lu';
|
||||||
export {BiDownArrowCircle as IconOpenList } from 'react-icons/bi';
|
export {BiDownArrowCircle as IconOpenList } from 'react-icons/bi';
|
||||||
export { LuTriangleAlert as IconAlert } from 'react-icons/lu';
|
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 =======
|
// ===== UI elements =======
|
||||||
export { BiX as IconClose } from 'react-icons/bi';
|
export { BiX as IconClose } from 'react-icons/bi';
|
||||||
|
@ -101,7 +97,9 @@ export { LuDatabase as IconDatabase } from 'react-icons/lu';
|
||||||
export { LuView as IconDBStructure } from 'react-icons/lu';
|
export { LuView as IconDBStructure } from 'react-icons/lu';
|
||||||
export { LuPlaneTakeoff as IconRESTapi } from 'react-icons/lu';
|
export { LuPlaneTakeoff as IconRESTapi } from 'react-icons/lu';
|
||||||
export { LuImage as IconImage } 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 { GoVersions as IconVersions } from 'react-icons/go';
|
||||||
|
export { TbColumnsOff as IconListOff } from 'react-icons/tb';
|
||||||
export { LuAtSign as IconTerm } from 'react-icons/lu';
|
export { LuAtSign as IconTerm } from 'react-icons/lu';
|
||||||
export { LuSubscript as IconAlias } from 'react-icons/lu';
|
export { LuSubscript as IconAlias } from 'react-icons/lu';
|
||||||
export { TbMathFunction as IconFormula } from 'react-icons/tb';
|
export { TbMathFunction as IconFormula } from 'react-icons/tb';
|
||||||
|
@ -109,8 +107,7 @@ export { BiFontFamily as IconText } from 'react-icons/bi';
|
||||||
export { BiFont as IconTextOff } from 'react-icons/bi';
|
export { BiFont as IconTextOff } from 'react-icons/bi';
|
||||||
export { TbCircleLetterM as IconTypeGraph } from 'react-icons/tb';
|
export { TbCircleLetterM as IconTypeGraph } from 'react-icons/tb';
|
||||||
export { RiTreeLine as IconTree } from 'react-icons/ri';
|
export { RiTreeLine as IconTree } from 'react-icons/ri';
|
||||||
export { LuKeyboard as IconKeyboard } from 'react-icons/lu';
|
export { FaRegKeyboard as IconControls } from 'react-icons/fa6';
|
||||||
export { LuKeyboardOff as IconKeyboardOff } from 'react-icons/lu';
|
|
||||||
export { RiLockLine as IconImmutable } from 'react-icons/ri';
|
export { RiLockLine as IconImmutable } from 'react-icons/ri';
|
||||||
export { RiLockUnlockLine as IconMutable } from 'react-icons/ri';
|
export { RiLockUnlockLine as IconMutable } from 'react-icons/ri';
|
||||||
export { RiOpenSourceLine as IconPublic } from 'react-icons/ri';
|
export { RiOpenSourceLine as IconPublic } from 'react-icons/ri';
|
||||||
|
@ -144,6 +141,7 @@ export { GrConnect as IconConnect } from 'react-icons/gr';
|
||||||
export { BiPlayCircle as IconExecute } from 'react-icons/bi';
|
export { BiPlayCircle as IconExecute } from 'react-icons/bi';
|
||||||
|
|
||||||
// ======== Graph UI =======
|
// ======== Graph UI =======
|
||||||
|
export { LuLayoutDashboard as IconFixLayout } from 'react-icons/lu';
|
||||||
export { BiCollapse as IconGraphCollapse } from 'react-icons/bi';
|
export { BiCollapse as IconGraphCollapse } from 'react-icons/bi';
|
||||||
export { BiExpand as IconGraphExpand } from 'react-icons/bi';
|
export { BiExpand as IconGraphExpand } from 'react-icons/bi';
|
||||||
export { LuMaximize as IconGraphMaximize } from 'react-icons/lu';
|
export { LuMaximize as IconGraphMaximize } from 'react-icons/lu';
|
||||||
|
|
|
@ -92,12 +92,7 @@ export function ComboBox<Option>({
|
||||||
hidden={hidden && !open}
|
hidden={hidden && !open}
|
||||||
>
|
>
|
||||||
<span className='truncate'>{value ? labelValueFunc(value) : placeholder}</span>
|
<span className='truncate'>{value ? labelValueFunc(value) : placeholder}</span>
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon className={cn('text-muted-foreground', clearable && !!value && 'opacity-0')} />
|
||||||
className={cn(
|
|
||||||
'text-muted-foreground cc-hover-pulse hover:text-primary',
|
|
||||||
clearable && !!value && 'opacity-0'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{clearable && !!value ? (
|
{clearable && !!value ? (
|
||||||
<IconRemove
|
<IconRemove
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
|
|
|
@ -115,7 +115,7 @@ export function ComboMulti<Option>({
|
||||||
<IconRemove
|
<IconRemove
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
size='1rem'
|
size='1rem'
|
||||||
className='cc-remove absolute pointer-events-auto right-3 cc-hover-pulse hover:text-primary'
|
className='cc-remove absolute pointer-events-auto right-3'
|
||||||
onClick={handleClear}
|
onClick={handleClear}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { type FieldError, type GlobalError } from 'react-hook-form';
|
import { type FieldError, type GlobalError } from 'react-hook-form';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { type Styling } from '../props';
|
import { type Styling } from '../props';
|
||||||
import { cn } from '../utils';
|
|
||||||
|
|
||||||
interface ErrorFieldProps extends Styling {
|
interface ErrorFieldProps extends Styling {
|
||||||
error?: FieldError | GlobalError;
|
error?: FieldError | GlobalError;
|
||||||
|
@ -15,7 +15,7 @@ export function ErrorField({ error, className, ...restProps }: ErrorFieldProps):
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className={cn('text-sm text-destructive select-none', className)} {...restProps}>
|
<div className={clsx('text-sm text-destructive select-none', className)} {...restProps}>
|
||||||
{error.message}
|
{error.message}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -90,7 +90,7 @@ export function SelectTree<ItemType>({
|
||||||
<div
|
<div
|
||||||
key={`${prefix}${index}`}
|
key={`${prefix}${index}`}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'cc-tree-item relative cc-scroll-row cc-hover-bg',
|
'cc-tree-item relative cc-scroll-row cc-hover',
|
||||||
isActive ? 'max-h-7 py-1 border-b' : 'max-h-0 opacity-0 pointer-events-none',
|
isActive ? 'max-h-7 py-1 border-b' : 'max-h-0 opacity-0 pointer-events-none',
|
||||||
value === item && 'cc-selected'
|
value === item && 'cc-selected'
|
||||||
)}
|
)}
|
||||||
|
@ -101,8 +101,9 @@ export function SelectTree<ItemType>({
|
||||||
{foldable.has(item) ? (
|
{foldable.has(item) ? (
|
||||||
<MiniButton
|
<MiniButton
|
||||||
aria-label={!folded.includes(item) ? 'Свернуть' : 'Развернуть'}
|
aria-label={!folded.includes(item) ? 'Свернуть' : 'Развернуть'}
|
||||||
className={clsx('absolute left-1 hover:text-primary', !folded.includes(item) ? 'top-1.5' : 'top-1')}
|
className={clsx('absolute left-1', !folded.includes(item) ? 'top-1.5' : 'top-1')}
|
||||||
noPadding
|
noPadding
|
||||||
|
noHover
|
||||||
icon={!folded.includes(item) ? <IconDropArrow size='1rem' /> : <IconPageRight size='1.25rem' />}
|
icon={!folded.includes(item) ? <IconDropArrow size='1rem' /> : <IconPageRight size='1.25rem' />}
|
||||||
onClick={event => handleClickFold(event, item)}
|
onClick={event => handleClickFold(event, item)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -46,7 +46,7 @@ function SelectTrigger({
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<SelectPrimitive.Icon asChild>
|
<SelectPrimitive.Icon asChild>
|
||||||
<ChevronDownIcon className='size-4 cc-hover-pulse hover:text-primary' />
|
<ChevronDownIcon className='size-4' />
|
||||||
</SelectPrimitive.Icon>
|
</SelectPrimitive.Icon>
|
||||||
</SelectPrimitive.Trigger>
|
</SelectPrimitive.Trigger>
|
||||||
);
|
);
|
||||||
|
@ -154,7 +154,7 @@ function SelectScrollDownButton({
|
||||||
className={cn('flex cursor-default items-center justify-center py-1', className)}
|
className={cn('flex cursor-default items-center justify-center py-1', className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChevronDownIcon className='size-4 cc-hover-pulse hover:text-primary' />
|
<ChevronDownIcon className='size-4' />
|
||||||
</SelectPrimitive.ScrollDownButton>
|
</SelectPrimitive.ScrollDownButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ export function ModalBackdrop({ onHide }: ModalBackdropProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='z-bottom fixed inset-0 backdrop-blur-[3px] opacity-50' />
|
<div className='z-bottom fixed inset-0 backdrop-blur-[3px] opacity-50' />
|
||||||
<div className='z-bottom fixed inset-0 bg-foreground opacity-5' onClick={onHide} />
|
<div className='z-bottom fixed inset-0 bg-popover opacity-25' onClick={onHide} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { type HelpTopic } from '@/features/help';
|
import { type HelpTopic } from '@/features/help';
|
||||||
import { BadgeHelp } from '@/features/help/components/badge-help';
|
import { BadgeHelp } from '@/features/help/components';
|
||||||
|
|
||||||
import { useEscapeKey } from '@/hooks/use-escape-key';
|
import { useEscapeKey } from '@/hooks/use-escape-key';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
@ -89,7 +89,7 @@ export function ModalForm({
|
||||||
<div className='cc-modal-wrapper'>
|
<div className='cc-modal-wrapper'>
|
||||||
<ModalBackdrop onHide={handleCancel} />
|
<ModalBackdrop onHide={handleCancel} />
|
||||||
<form
|
<form
|
||||||
className='cc-animate-modal relative grid border-2 px-1 pb-1 rounded-xl bg-background'
|
className='cc-animate-modal relative grid border rounded-xl bg-background'
|
||||||
role='dialog'
|
role='dialog'
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
aria-labelledby='modal-title'
|
aria-labelledby='modal-title'
|
||||||
|
|
|
@ -6,7 +6,7 @@ export function ModalLoader() {
|
||||||
return (
|
return (
|
||||||
<div className='cc-modal-wrapper'>
|
<div className='cc-modal-wrapper'>
|
||||||
<ModalBackdrop />
|
<ModalBackdrop />
|
||||||
<div className='cc-animate-modal p-20 border-2 rounded-xl bg-background'>
|
<div className='cc-animate-modal p-20 border rounded-xl bg-background'>
|
||||||
<Loader circular scale={6} />
|
<Loader circular scale={6} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { BadgeHelp } from '@/features/help/components/badge-help';
|
import { BadgeHelp } from '@/features/help/components';
|
||||||
|
|
||||||
import { useEscapeKey } from '@/hooks/use-escape-key';
|
import { useEscapeKey } from '@/hooks/use-escape-key';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
@ -39,7 +39,7 @@ export function ModalView({
|
||||||
return (
|
return (
|
||||||
<div className='cc-modal-wrapper'>
|
<div className='cc-modal-wrapper'>
|
||||||
<ModalBackdrop onHide={hideDialog} />
|
<ModalBackdrop onHide={hideDialog} />
|
||||||
<div className='cc-animate-modal relative grid border-2 px-1 pb-1 rounded-xl bg-background' role='dialog'>
|
<div className='cc-animate-modal relative grid border rounded-xl bg-background' role='dialog'>
|
||||||
{helpTopic && !hideHelpWhen?.() ? (
|
{helpTopic && !hideHelpWhen?.() ? (
|
||||||
<BadgeHelp
|
<BadgeHelp
|
||||||
topic={helpTopic}
|
topic={helpTopic}
|
||||||
|
|
|
@ -22,7 +22,6 @@ export function TabLabel({
|
||||||
className,
|
className,
|
||||||
disabled,
|
disabled,
|
||||||
role = 'tab',
|
role = 'tab',
|
||||||
selectedClassName = 'text-foreground! bg-secondary',
|
|
||||||
...otherProps
|
...otherProps
|
||||||
}: TabLabelProps) {
|
}: TabLabelProps) {
|
||||||
return (
|
return (
|
||||||
|
@ -30,12 +29,12 @@ export function TabLabel({
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'min-w-20 h-full',
|
'min-w-20 h-full',
|
||||||
'px-2 py-1 flex justify-center',
|
'px-2 py-1 flex justify-center',
|
||||||
'cc-animate-color duration-select text-muted-foreground',
|
'cc-animate-color duration-select',
|
||||||
'text-sm whitespace-nowrap font-controls',
|
'text-sm whitespace-nowrap font-controls',
|
||||||
'select-none',
|
'select-none',
|
||||||
'outline-hidden',
|
'outline-hidden',
|
||||||
!disabled && 'hover:cursor-pointer cc-hover-text',
|
!disabled && 'hover:cursor-pointer cc-hover',
|
||||||
disabled && 'bg-secondary',
|
disabled && 'text-muted-foreground',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
tabIndex='-1'
|
tabIndex='-1'
|
||||||
|
@ -45,7 +44,6 @@ export function TabLabel({
|
||||||
data-tooltip-hidden={hideTitle}
|
data-tooltip-hidden={hideTitle}
|
||||||
role={role}
|
role={role}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
selectedClassName={selectedClassName}
|
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { type Styling, type Titled } from '@/components/props';
|
import { type Styling, type Titled } from '@/components/props';
|
||||||
import { globalIDs } from '@/utils/constants';
|
import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
import { cn } from '../utils';
|
|
||||||
|
|
||||||
interface IndicatorProps extends Titled, Styling {
|
interface IndicatorProps extends Titled, Styling {
|
||||||
/** Icon to display. */
|
/** Icon to display. */
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
|
@ -17,8 +17,8 @@ interface IndicatorProps extends Titled, Styling {
|
||||||
export function Indicator({ icon, title, titleHtml, hideTitle, noPadding, className, ...restProps }: IndicatorProps) {
|
export function Indicator({ icon, title, titleHtml, hideTitle, noPadding, className, ...restProps }: IndicatorProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={clsx(
|
||||||
'text-muted-foreground', //
|
'cc-controls', //
|
||||||
'outline-hidden',
|
'outline-hidden',
|
||||||
!noPadding && 'px-1 py-1',
|
!noPadding && 'px-1 py-1',
|
||||||
className
|
className
|
||||||
|
|
|
@ -57,7 +57,7 @@ export function ValueIcon({
|
||||||
data-tooltip-hidden={hideTitle}
|
data-tooltip-hidden={hideTitle}
|
||||||
aria-label={title}
|
aria-label={title}
|
||||||
>
|
>
|
||||||
{onClick ? <MiniButton noPadding icon={icon} onClick={onClick} disabled={disabled} /> : icon}
|
{onClick ? <MiniButton noHover noPadding icon={icon} onClick={onClick} disabled={disabled} /> : icon}
|
||||||
<span id={id}>{value}</span>
|
<span id={id}>{value}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
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 (
|
return (
|
||||||
<div tabIndex={-1} id={`help-${topic}`} className={cn(padding, className)} style={style}>
|
<div tabIndex={-1} id={`help-${topic}`} className={cn(padding, className)} style={style}>
|
||||||
<IconHelp size='1.25rem' className='text-muted-foreground hover:text-primary cc-animate-color' />
|
<IconHelp size='1.25rem' className='icon-primary' />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
clickable
|
clickable
|
||||||
anchorSelect={`#help-${topic}`}
|
anchorSelect={`#help-${topic}`}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
|
export { BadgeHelp } from './badge-help';
|
||||||
|
|
|
@ -12,30 +12,26 @@ export function HelpFormulaTree() {
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2>Виды узлов</h2>
|
<h2>Виды узлов</h2>
|
||||||
<p className='m-0'>
|
<ul>
|
||||||
<span className='cc-sample-color bg-(--acc-bg-green)' />
|
<li>
|
||||||
объявление идентификатора
|
<span className='bg-(--acc-bg-green)'>объявление идентификатора</span>
|
||||||
</p>
|
</li>
|
||||||
<p className='m-0'>
|
<li>
|
||||||
<span className='cc-sample-color bg-(--acc-bg-teal)' />
|
<span className='bg-(--acc-bg-teal)'>глобальный идентификатор</span>
|
||||||
глобальный идентификатор
|
</li>
|
||||||
</p>
|
<li>
|
||||||
<p className='m-0'>
|
<span className='bg-(--acc-bg-orange)'>логическое выражение</span>
|
||||||
<span className='cc-sample-color bg-(--acc-bg-orange)' />
|
</li>
|
||||||
логическое выражение
|
<li>
|
||||||
</p>
|
<span className='bg-(--acc-bg-blue)'>типизированное выражение</span>
|
||||||
<p className='m-0'>
|
</li>
|
||||||
<span className='cc-sample-color bg-(--acc-bg-blue)' />
|
<li>
|
||||||
типизированное выражение
|
<span className='bg-(--acc-bg-red)'>присвоение и итерация</span>
|
||||||
</p>
|
</li>
|
||||||
<p className='m-0'>
|
<li>
|
||||||
<span className='cc-sample-color bg-(--acc-bg-red)' />
|
<span className='bg-secondary'>составные выражения</span>
|
||||||
присвоение и итерация
|
</li>
|
||||||
</p>
|
</ul>
|
||||||
<p className='m-0'>
|
|
||||||
<span className='cc-sample-color bg-secondary' />
|
|
||||||
составные выражения
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Команды</h2>
|
<h2>Команды</h2>
|
||||||
<ul>
|
<ul>
|
||||||
|
|
|
@ -62,7 +62,7 @@ export function HelpLibrary() {
|
||||||
<IconFilterReset size='1rem' className='inline-icon' /> сбросить фильтры
|
<IconFilterReset size='1rem' className='inline-icon' /> сбросить фильтры
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconFolderTree size='1rem' className='inline-icon' /> переключение между Проводник и Таблица
|
<IconFolderTree size='1rem' className='inline-icon' /> переключение между Проводник и Поиск
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
|
@ -11,8 +11,8 @@ import {
|
||||||
IconEdit2,
|
IconEdit2,
|
||||||
IconExecute,
|
IconExecute,
|
||||||
IconFitImage,
|
IconFitImage,
|
||||||
|
IconFixLayout,
|
||||||
IconGrid,
|
IconGrid,
|
||||||
IconLeftOpen,
|
|
||||||
IconLineStraight,
|
IconLineStraight,
|
||||||
IconLineWave,
|
IconLineWave,
|
||||||
IconNewItem,
|
IconNewItem,
|
||||||
|
@ -31,7 +31,7 @@ export function HelpOssGraph() {
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
<h1 className='sm:pr-24'>Граф синтеза</h1>
|
<h1 className='sm:pr-24'>Граф синтеза</h1>
|
||||||
<div className='flex flex-col sm:flex-row'>
|
<div className='flex flex-col sm:flex-row'>
|
||||||
<div className='sm:w-64'>
|
<div className='sm:w-56'>
|
||||||
<h2>Настройка графа</h2>
|
<h2>Настройка графа</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
|
@ -41,7 +41,7 @@ export function HelpOssGraph() {
|
||||||
<IconFitImage className='inline-icon' /> Вписать в экран
|
<IconFitImage className='inline-icon' /> Вписать в экран
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconLeftOpen className='inline-icon' /> Панель связанной КС
|
<IconFixLayout className='inline-icon' /> Исправить расположения
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconSettings className='inline-icon' /> Диалог настроек
|
<IconSettings className='inline-icon' /> Диалог настроек
|
||||||
|
@ -70,7 +70,7 @@ export function HelpOssGraph() {
|
||||||
|
|
||||||
<Divider vertical margins='mx-3 mt-3' className='hidden sm:block' />
|
<Divider vertical margins='mx-3 mt-3' className='hidden sm:block' />
|
||||||
|
|
||||||
<div className='sm:w-76'>
|
<div className='sm:w-84'>
|
||||||
<h2>Изменение узлов</h2>
|
<h2>Изменение узлов</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
|
@ -101,7 +101,7 @@ export function HelpOssGraph() {
|
||||||
<Divider margins='my-2' className='hidden sm:block' />
|
<Divider margins='my-2' className='hidden sm:block' />
|
||||||
|
|
||||||
<div className='flex flex-col-reverse mb-3 sm:flex-row'>
|
<div className='flex flex-col-reverse mb-3 sm:flex-row'>
|
||||||
<div className='sm:w-64'>
|
<div className='sm:w-56'>
|
||||||
<h2>Общие</h2>
|
<h2>Общие</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
|
@ -111,14 +111,14 @@ export function HelpOssGraph() {
|
||||||
<kbd>Space</kbd> – перемещение экрана
|
<kbd>Space</kbd> – перемещение экрана
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<kbd>Shift</kbd> – перемещение в границах блока
|
<kbd>Shift</kbd> – перемещение выделенных элементов в границах родителя
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Divider vertical margins='mx-3' className='hidden sm:block' />
|
<Divider vertical margins='mx-3' className='hidden sm:block' />
|
||||||
|
|
||||||
<div className='dense w-76'>
|
<div className='dense w-84'>
|
||||||
<h2>Контекстное меню</h2>
|
<h2>Контекстное меню</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import {
|
import {
|
||||||
|
IconClone,
|
||||||
IconDestroy,
|
IconDestroy,
|
||||||
|
IconDownload,
|
||||||
IconEditor,
|
IconEditor,
|
||||||
IconImmutable,
|
IconImmutable,
|
||||||
IconLeftOpen,
|
|
||||||
IconOSS,
|
IconOSS,
|
||||||
IconOwner,
|
IconOwner,
|
||||||
IconPublic,
|
IconPublic,
|
||||||
|
@ -15,17 +16,17 @@ import { HelpTopic } from '../../models/help-topic';
|
||||||
export function HelpRSCard() {
|
export function HelpRSCard() {
|
||||||
return (
|
return (
|
||||||
<div className='dense'>
|
<div className='dense'>
|
||||||
<h1>Паспорт схемы</h1>
|
<h1>Карточка схемы</h1>
|
||||||
|
|
||||||
<p>Паспорт содержит общую информацию и статистику</p>
|
<p>Карточка содержит общую информацию и статистику</p>
|
||||||
<p>
|
<p>
|
||||||
Паспорт позволяет управлять атрибутами и <LinkTopic text='версиями' topic={HelpTopic.VERSIONS} />
|
Карточка позволяет управлять атрибутами и <LinkTopic text='версиями' topic={HelpTopic.VERSIONS} />
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Паспорт позволяет назначать <IconEditor className='inline-icon' /> Редакторов
|
Карточка позволяет назначать <IconEditor className='inline-icon' /> Редакторов
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Паспорт позволяет изменить <IconOwner className='inline-icon icon-green' /> Владельца
|
Карточка позволяет изменить <IconOwner className='inline-icon icon-green' /> Владельца
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Управление</h2>
|
<h2>Управление</h2>
|
||||||
|
@ -49,10 +50,13 @@ export function HelpRSCard() {
|
||||||
<IconImmutable className='inline-icon' /> Неизменные схемы
|
<IconImmutable className='inline-icon' /> Неизменные схемы
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconDestroy className='inline-icon icon-red' /> Удалить – полностью удаляет схему из базы Портала
|
<IconClone className='inline-icon icon-green' /> Клонировать – создать копию схемы
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconLeftOpen className='inline-icon' /> Отображение статистики
|
<IconDownload className='inline-icon' /> Загрузить/Выгрузить – взаимодействие с Экстеор
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconDestroy className='inline-icon icon-red' /> Удалить – полностью удаляет схему из базы Портала
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import {
|
import {
|
||||||
IconChild,
|
IconChild,
|
||||||
IconClone,
|
IconClone,
|
||||||
|
IconControls,
|
||||||
IconDestroy,
|
IconDestroy,
|
||||||
IconEdit,
|
IconEdit,
|
||||||
IconFilter,
|
IconFilter,
|
||||||
IconKeyboard,
|
IconList,
|
||||||
IconLeftOpen,
|
|
||||||
IconMoveDown,
|
IconMoveDown,
|
||||||
IconMoveUp,
|
IconMoveUp,
|
||||||
IconNewItem,
|
IconNewItem,
|
||||||
|
@ -37,7 +37,7 @@ export function HelpRSEditor() {
|
||||||
<IconPredecessor className='inline-icon' /> переход к исходной
|
<IconPredecessor className='inline-icon' /> переход к исходной
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconLeftOpen className='inline-icon' /> список конституент
|
<IconList className='inline-icon' /> список конституент
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconSave className='inline-icon' /> сохранить: <kbd>Ctrl + S</kbd>
|
<IconSave className='inline-icon' /> сохранить: <kbd>Ctrl + S</kbd>
|
||||||
|
@ -72,16 +72,17 @@ export function HelpRSEditor() {
|
||||||
<IconChild className='inline-icon' /> отображение наследованных
|
<IconChild className='inline-icon' /> отображение наследованных
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span className='cc-sample-color bg-selected' />
|
<span className='bg-selected'>текущая конституента</span>
|
||||||
выбранная конституента
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span className='cc-sample-color bg-accent-green50' />
|
<span className='bg-accent-green50'>
|
||||||
<LinkTopic text='основа' topic={HelpTopic.CC_RELATIONS} /> выбранной
|
<LinkTopic text='основа' topic={HelpTopic.CC_RELATIONS} /> текущей
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span className='cc-sample-color bg-accent-orange50' />
|
<span className='bg-accent-orange50'>
|
||||||
<LinkTopic text='порожденные' topic={HelpTopic.CC_RELATIONS} /> выбранной
|
<LinkTopic text='порожденные' topic={HelpTopic.CC_RELATIONS} /> текущей
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -93,7 +94,7 @@ export function HelpRSEditor() {
|
||||||
<IconStatusOK className='inline-icon' /> индикатор статуса определения сверху
|
<IconStatusOK className='inline-icon' /> индикатор статуса определения сверху
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconKeyboard className='inline-icon' /> специальная клавиатура и горячие клавиши
|
<IconControls className='inline-icon' /> специальная клавиатура и горячие клавиши
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconTypeGraph className='inline-icon' /> отображение{' '}
|
<IconTypeGraph className='inline-icon' /> отображение{' '}
|
||||||
|
|
|
@ -32,17 +32,18 @@ export function HelpRSMenu() {
|
||||||
<h2>Вкладки</h2>
|
<h2>Вкладки</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<LinkTopic text='Паспорт' topic={HelpTopic.UI_RS_CARD} /> – редактирование атрибутов схемы и версии
|
<LinkTopic text='Карточка' topic={HelpTopic.UI_RS_CARD} /> – редактирование атрибутов схемы и версии
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<LinkTopic text='Список' topic={HelpTopic.UI_RS_LIST} /> – работа со списком конституент в табличной форме
|
<LinkTopic text='Содержание' topic={HelpTopic.UI_RS_LIST} /> – работа со списком конституент в табличной форме
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<LinkTopic text='Понятие' topic={HelpTopic.UI_RS_EDITOR} /> – редактирование отдельной{' '}
|
<LinkTopic text='Редактор' topic={HelpTopic.UI_RS_EDITOR} /> – редактирование отдельной{' '}
|
||||||
<LinkTopic text='Конституенты' topic={HelpTopic.CC_CONSTITUENTA} />
|
<LinkTopic text='Конституенты' topic={HelpTopic.CC_CONSTITUENTA} />
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<LinkTopic text='Граф' topic={HelpTopic.UI_GRAPH_TERM} /> – графическое представление связей конституент
|
<LinkTopic text='Граф термов' topic={HelpTopic.UI_GRAPH_TERM} /> – графическое представление связей
|
||||||
|
конституент
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
|
@ -23,18 +23,17 @@ export function HelpTypeGraph() {
|
||||||
|
|
||||||
<h2>Цвета узлов</h2>
|
<h2>Цвета узлов</h2>
|
||||||
|
|
||||||
<p className='m-0'>
|
<ul>
|
||||||
<span className='cc-sample-color bg-secondary' />
|
<li>
|
||||||
ступень-основание
|
<span className='bg-secondary'>ступень-основание</span>
|
||||||
</p>
|
</li>
|
||||||
<p className='m-0'>
|
<li>
|
||||||
<span className='cc-sample-color bg-accent-teal' />
|
<span className='bg-accent-teal'>ступень-булеан</span>
|
||||||
ступень-булеан
|
</li>
|
||||||
</p>
|
<li>
|
||||||
<p className='m-0'>
|
<span className='bg-accent-orange'>ступень декартова произведения</span>
|
||||||
<span className='cc-sample-color bg-accent-orange' />
|
</li>
|
||||||
ступень декартова произведения
|
</ul>
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Команды</h2>
|
<h2>Команды</h2>
|
||||||
<ul>
|
<ul>
|
||||||
|
|
|
@ -13,7 +13,7 @@ export function labelHelpTopic(topic: HelpTopic): string {
|
||||||
case HelpTopic.INTERFACE: return '🌀 Интерфейс';
|
case HelpTopic.INTERFACE: return '🌀 Интерфейс';
|
||||||
case HelpTopic.UI_LIBRARY: return 'Библиотека';
|
case HelpTopic.UI_LIBRARY: return 'Библиотека';
|
||||||
case HelpTopic.UI_RS_MENU: 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_LIST: return 'Список конституент';
|
||||||
case HelpTopic.UI_RS_EDITOR: return 'Редактор конституенты';
|
case HelpTopic.UI_RS_EDITOR: return 'Редактор конституенты';
|
||||||
case HelpTopic.UI_GRAPH_TERM: return 'Граф термов';
|
case HelpTopic.UI_GRAPH_TERM: return 'Граф термов';
|
||||||
|
|
|
@ -3,8 +3,7 @@ import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { urls, useConceptNavigation } from '@/app';
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
import { useLabelUser, useRoleStore, UserRole } from '@/features/users';
|
import { useLabelUser, useRoleStore, UserRole } from '@/features/users';
|
||||||
import { InfoUsers } from '@/features/users/components/info-users';
|
import { InfoUsers, SelectUser } from '@/features/users/components';
|
||||||
import { SelectUser } from '@/features/users/components/select-user';
|
|
||||||
|
|
||||||
import { Tooltip } from '@/components/container';
|
import { Tooltip } from '@/components/container';
|
||||||
import { MiniButton } from '@/components/control';
|
import { MiniButton } from '@/components/control';
|
||||||
|
@ -87,6 +86,7 @@ export function EditorLibraryItem({ schema, isAttachedToOSS }: EditorLibraryItem
|
||||||
<div className='relative flex justify-stretch sm:mb-1 max-w-120 gap-3'>
|
<div className='relative flex justify-stretch sm:mb-1 max-w-120 gap-3'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Открыть в библиотеке'
|
title='Открыть в библиотеке'
|
||||||
|
noHover
|
||||||
noPadding
|
noPadding
|
||||||
icon={<IconFolderOpened size='1.25rem' className='icon-primary' />}
|
icon={<IconFolderOpened size='1.25rem' className='icon-primary' />}
|
||||||
onClick={handleOpenLibrary}
|
onClick={handleOpenLibrary}
|
||||||
|
@ -137,14 +137,14 @@ export function EditorLibraryItem({ schema, isAttachedToOSS }: EditorLibraryItem
|
||||||
<ValueIcon
|
<ValueIcon
|
||||||
title='Дата обновления'
|
title='Дата обновления'
|
||||||
dense
|
dense
|
||||||
icon={<IconDateUpdate size='1.25rem' />}
|
icon={<IconDateUpdate size='1.25rem' className='text-constructive' />}
|
||||||
value={new Date(schema.time_update).toLocaleString(intl.locale)}
|
value={new Date(schema.time_update).toLocaleString(intl.locale)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ValueIcon
|
<ValueIcon
|
||||||
title='Дата создания'
|
title='Дата создания'
|
||||||
dense
|
dense
|
||||||
icon={<IconDateCreate size='1.25rem' />}
|
icon={<IconDateCreate size='1.25rem' className='text-constructive' />}
|
||||||
value={new Date(schema.time_create).toLocaleString(intl.locale, {
|
value={new Date(schema.time_create).toLocaleString(intl.locale, {
|
||||||
year: '2-digit',
|
year: '2-digit',
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
import { UserRole } from '@/features/users';
|
import { UserRole } from '@/features/users';
|
||||||
|
|
||||||
import { type DomIconProps, IconAdmin, IconEditor, IconOwner, IconReader } from '@/components/icons';
|
import { IconAdmin, IconEditor, IconOwner, IconReader } from '@/components/icons';
|
||||||
|
|
||||||
export function IconRole({ value, size = '1.25rem', className }: DomIconProps<UserRole>) {
|
interface IconRoleProps {
|
||||||
switch (value) {
|
role: UserRole;
|
||||||
|
size?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconRole({ role, size = '1.25rem' }: IconRoleProps) {
|
||||||
|
switch (role) {
|
||||||
case UserRole.ADMIN:
|
case UserRole.ADMIN:
|
||||||
return <IconAdmin size={size} className={className ?? 'icon-primary'} />;
|
return <IconAdmin size={size} className='icon-primary' />;
|
||||||
case UserRole.OWNER:
|
case UserRole.OWNER:
|
||||||
return <IconOwner size={size} className={className ?? 'icon-primary'} />;
|
return <IconOwner size={size} className='icon-primary' />;
|
||||||
case UserRole.EDITOR:
|
case UserRole.EDITOR:
|
||||||
return <IconEditor size={size} className={className ?? 'icon-primary'} />;
|
return <IconEditor size={size} className='icon-primary' />;
|
||||||
case UserRole.READER:
|
case UserRole.READER:
|
||||||
return <IconReader size={size} className={className ?? 'icon-primary'} />;
|
return <IconReader size={size} className='icon-primary' />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
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'} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
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 { useRoleStore, UserRole } from '@/features/users';
|
||||||
import { describeUserRole, labelUserRole } from '@/features/users/labels';
|
import { describeUserRole, labelUserRole } from '@/features/users/labels';
|
||||||
|
|
||||||
import { MiniButton } from '@/components/control';
|
import { Button } from '@/components/control';
|
||||||
import { Dropdown, DropdownButton, useDropdown } from '@/components/dropdown';
|
import { Dropdown, DropdownButton, useDropdown } from '@/components/dropdown';
|
||||||
import { IconAlert } from '@/components/icons';
|
import { IconAlert } from '@/components/icons';
|
||||||
|
|
||||||
|
@ -29,11 +29,13 @@ export function MenuRole({ isOwned, isEditor }: MenuRoleProps) {
|
||||||
|
|
||||||
if (isAnonymous) {
|
if (isAnonymous) {
|
||||||
return (
|
return (
|
||||||
<MiniButton
|
<Button
|
||||||
noPadding
|
dense
|
||||||
|
noBorder
|
||||||
|
noOutline
|
||||||
titleHtml='<b>Анонимный режим</b><br />Войти в Портал'
|
titleHtml='<b>Анонимный режим</b><br />Войти в Портал'
|
||||||
hideTitle={accessMenu.isOpen}
|
hideTitle={accessMenu.isOpen}
|
||||||
className='h-full pr-2 pl-3 bg-transparent'
|
className='h-full pr-2'
|
||||||
icon={<IconAlert size='1.25rem' className='icon-red' />}
|
icon={<IconAlert size='1.25rem' className='icon-red' />}
|
||||||
onClick={() => router.push({ path: urls.login })}
|
onClick={() => router.push({ path: urls.login })}
|
||||||
/>
|
/>
|
||||||
|
@ -42,45 +44,44 @@ export function MenuRole({ isOwned, isEditor }: MenuRoleProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={accessMenu.ref} onBlur={accessMenu.handleBlur} className='relative'>
|
<div ref={accessMenu.ref} onBlur={accessMenu.handleBlur} className='relative'>
|
||||||
<MiniButton
|
<Button
|
||||||
noHover
|
dense
|
||||||
noPadding
|
noBorder
|
||||||
|
noOutline
|
||||||
title={`Режим ${labelUserRole(role)}`}
|
title={`Режим ${labelUserRole(role)}`}
|
||||||
hideTitle={accessMenu.isOpen}
|
hideTitle={accessMenu.isOpen}
|
||||||
className='h-full pr-2 text-muted-foreground hover:text-primary cc-animate-color'
|
className='h-full pr-2'
|
||||||
icon={<IconRole value={role} size='1.25rem' className='' />}
|
icon={<IconRole role={role} size='1.25rem' />}
|
||||||
onClick={accessMenu.toggle}
|
onClick={accessMenu.toggle}
|
||||||
/>
|
/>
|
||||||
<Dropdown isOpen={accessMenu.isOpen} margin='mt-3'>
|
<Dropdown isOpen={accessMenu.isOpen} margin='mt-3'>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text={labelUserRole(UserRole.READER)}
|
text={labelUserRole(UserRole.READER)}
|
||||||
title={describeUserRole(UserRole.READER)}
|
title={describeUserRole(UserRole.READER)}
|
||||||
icon={<IconRole value={UserRole.READER} size='1rem' />}
|
icon={<IconRole role={UserRole.READER} size='1rem' />}
|
||||||
onClick={() => handleChangeMode(UserRole.READER)}
|
onClick={() => handleChangeMode(UserRole.READER)}
|
||||||
/>
|
/>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text={labelUserRole(UserRole.EDITOR)}
|
text={labelUserRole(UserRole.EDITOR)}
|
||||||
title={describeUserRole(UserRole.EDITOR)}
|
title={describeUserRole(UserRole.EDITOR)}
|
||||||
icon={<IconRole value={UserRole.EDITOR} size='1rem' />}
|
icon={<IconRole role={UserRole.EDITOR} size='1rem' />}
|
||||||
onClick={() => handleChangeMode(UserRole.EDITOR)}
|
onClick={() => handleChangeMode(UserRole.EDITOR)}
|
||||||
disabled={!isOwned && !isEditor}
|
disabled={!isOwned && !isEditor}
|
||||||
/>
|
/>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text={labelUserRole(UserRole.OWNER)}
|
text={labelUserRole(UserRole.OWNER)}
|
||||||
title={describeUserRole(UserRole.OWNER)}
|
title={describeUserRole(UserRole.OWNER)}
|
||||||
icon={<IconRole value={UserRole.OWNER} size='1rem' />}
|
icon={<IconRole role={UserRole.OWNER} size='1rem' />}
|
||||||
onClick={() => handleChangeMode(UserRole.OWNER)}
|
onClick={() => handleChangeMode(UserRole.OWNER)}
|
||||||
disabled={!isOwned}
|
disabled={!isOwned}
|
||||||
/>
|
/>
|
||||||
{user.is_staff ? (
|
<DropdownButton
|
||||||
<DropdownButton
|
text={labelUserRole(UserRole.ADMIN)}
|
||||||
text={labelUserRole(UserRole.ADMIN)}
|
title={describeUserRole(UserRole.ADMIN)}
|
||||||
title={describeUserRole(UserRole.ADMIN)}
|
icon={<IconRole role={UserRole.ADMIN} size='1rem' />}
|
||||||
icon={<IconRole value={UserRole.ADMIN} size='1rem' />}
|
onClick={() => handleChangeMode(UserRole.ADMIN)}
|
||||||
onClick={() => handleChangeMode(UserRole.ADMIN)}
|
disabled={!user.is_staff}
|
||||||
disabled={!user.is_staff}
|
/>
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,7 +5,7 @@ import clsx from 'clsx';
|
||||||
|
|
||||||
import { useAuthSuspense } from '@/features/auth';
|
import { useAuthSuspense } from '@/features/auth';
|
||||||
|
|
||||||
import { TextArea } from '@/components/input';
|
import { Label, TextArea } from '@/components/input';
|
||||||
import { type Styling } from '@/components/props';
|
import { type Styling } from '@/components/props';
|
||||||
|
|
||||||
import { LocationHead } from '../../models/library';
|
import { LocationHead } from '../../models/library';
|
||||||
|
@ -35,16 +35,18 @@ export function PickLocation({
|
||||||
const { user } = useAuthSuspense();
|
const { user } = useAuthSuspense();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx('flex relative', className)} {...restProps}>
|
<div className={clsx('flex', className)} {...restProps}>
|
||||||
<SelectLocationHead
|
<div className='flex flex-col gap-2 min-w-28'>
|
||||||
className='absolute right-0 top-0'
|
<Label className='select-none' text='Корень' />
|
||||||
value={value.substring(0, 2) as LocationHead}
|
<SelectLocationHead
|
||||||
onChange={newValue => onChange(combineLocation(newValue, value.substring(3)))}
|
value={value.substring(0, 2) as LocationHead}
|
||||||
excluded={!user.is_staff ? [LocationHead.LIBRARY] : []}
|
onChange={newValue => onChange(combineLocation(newValue, value.substring(3)))}
|
||||||
/>
|
excluded={!user.is_staff ? [LocationHead.LIBRARY] : []}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<SelectLocationContext
|
<SelectLocationContext
|
||||||
className='absolute left-28 -top-1'
|
className='-mt-1 -ml-8'
|
||||||
dropdownHeight={dropdownHeight} //
|
dropdownHeight={dropdownHeight} //
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
@ -52,7 +54,7 @@ export function PickLocation({
|
||||||
|
|
||||||
<TextArea
|
<TextArea
|
||||||
id='dlg_location'
|
id='dlg_location'
|
||||||
label='Расположение'
|
label='Путь'
|
||||||
rows={rows}
|
rows={rows}
|
||||||
value={value.substring(3)}
|
value={value.substring(3)}
|
||||||
onChange={event => onChange(combineLocation(value.substring(0, 2), event.target.value))}
|
onChange={event => onChange(combineLocation(value.substring(0, 2), event.target.value))}
|
||||||
|
|
|
@ -38,13 +38,13 @@ export function SelectLocationContext({
|
||||||
<div
|
<div
|
||||||
ref={menu.ref} //
|
ref={menu.ref} //
|
||||||
onBlur={menu.handleBlur}
|
onBlur={menu.handleBlur}
|
||||||
className={clsx('text-right self-start', className)}
|
className={clsx('relative text-right self-start', className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title={title}
|
title={title}
|
||||||
hideTitle={menu.isOpen}
|
hideTitle={menu.isOpen}
|
||||||
icon={<IconFolderTree size='1.25rem' className='icon-primary' />}
|
icon={<IconFolderTree size='1.25rem' className='icon-green' />}
|
||||||
onClick={() => menu.toggle()}
|
onClick={() => menu.toggle()}
|
||||||
/>
|
/>
|
||||||
<Dropdown isOpen={menu.isOpen} className={clsx('w-80 z-tooltip', dropdownHeight)}>
|
<Dropdown isOpen={menu.isOpen} className={clsx('w-80 z-tooltip', dropdownHeight)}>
|
||||||
|
|
|
@ -48,7 +48,7 @@ export function SelectLocationHead({
|
||||||
onClick={menu.toggle}
|
onClick={menu.toggle}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Dropdown isOpen={menu.isOpen} stretchLeft margin='mt-2'>
|
<Dropdown isOpen={menu.isOpen} margin='mt-2'>
|
||||||
{Object.values(LocationHead)
|
{Object.values(LocationHead)
|
||||||
.filter(head => !excluded.includes(head))
|
.filter(head => !excluded.includes(head))
|
||||||
.map((head, index) => {
|
.map((head, index) => {
|
||||||
|
|
|
@ -62,7 +62,7 @@ export function SelectLocation({ value, dense, prefix, onClick, className, style
|
||||||
!dense && 'h-7 sm:h-8',
|
!dense && 'h-7 sm:h-8',
|
||||||
'pr-3 py-1 flex items-center gap-2',
|
'pr-3 py-1 flex items-center gap-2',
|
||||||
'cc-scroll-row',
|
'cc-scroll-row',
|
||||||
'cc-hover-bg cc-animate-color duration-fade',
|
'cc-hover cc-animate-color duration-fade',
|
||||||
'cursor-pointer',
|
'cursor-pointer',
|
||||||
'leading-3 sm:leading-4',
|
'leading-3 sm:leading-4',
|
||||||
activeNode === item && 'cc-selected'
|
activeNode === item && 'cc-selected'
|
||||||
|
@ -73,6 +73,7 @@ export function SelectLocation({ value, dense, prefix, onClick, className, style
|
||||||
{item.children.size > 0 ? (
|
{item.children.size > 0 ? (
|
||||||
<MiniButton
|
<MiniButton
|
||||||
noPadding
|
noPadding
|
||||||
|
noHover
|
||||||
icon={
|
icon={
|
||||||
folded.includes(item) ? (
|
folded.includes(item) ? (
|
||||||
item.filesInside ? (
|
item.filesInside ? (
|
||||||
|
@ -92,7 +93,7 @@ export function SelectLocation({ value, dense, prefix, onClick, className, style
|
||||||
{item.filesInside ? (
|
{item.filesInside ? (
|
||||||
<IconFolder size='1rem' className='text-foreground' />
|
<IconFolder size='1rem' className='text-foreground' />
|
||||||
) : (
|
) : (
|
||||||
<IconFolderEmpty size='1rem' className='text-foreground-muted' />
|
<IconFolderEmpty size='1rem' className='cc-controls' />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { HelpTopic } from '@/features/help';
|
import { HelpTopic } from '@/features/help';
|
||||||
import { BadgeHelp } from '@/features/help/components/badge-help';
|
import { BadgeHelp } from '@/features/help/components';
|
||||||
import { useRoleStore, UserRole } from '@/features/users';
|
import { useRoleStore, UserRole } from '@/features/users';
|
||||||
|
|
||||||
import { MiniButton } from '@/components/control';
|
import { MiniButton } from '@/components/control';
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { urls, useConceptNavigation } from '@/app';
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
import { HelpTopic } from '@/features/help';
|
import { HelpTopic } from '@/features/help';
|
||||||
import { BadgeHelp } from '@/features/help/components/badge-help';
|
import { BadgeHelp } from '@/features/help/components';
|
||||||
import { type IRSForm } from '@/features/rsform';
|
import { type IRSForm } from '@/features/rsform';
|
||||||
import { useRoleStore, UserRole } from '@/features/users';
|
import { useRoleStore, UserRole } from '@/features/users';
|
||||||
|
|
||||||
|
@ -10,46 +10,29 @@ import { MiniButton } from '@/components/control';
|
||||||
import { IconDestroy, IconSave, IconShare } from '@/components/icons';
|
import { IconDestroy, IconSave, IconShare } from '@/components/icons';
|
||||||
import { cn } from '@/components/utils';
|
import { cn } from '@/components/utils';
|
||||||
import { useModificationStore } from '@/stores/modification';
|
import { useModificationStore } from '@/stores/modification';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
|
||||||
import { tooltipText } from '@/utils/labels';
|
import { tooltipText } from '@/utils/labels';
|
||||||
import { prepareTooltip, sharePage } from '@/utils/utils';
|
import { prepareTooltip, sharePage } from '@/utils/utils';
|
||||||
|
|
||||||
import { AccessPolicy, type ILibraryItem, LibraryItemType } from '../backend/types';
|
import { AccessPolicy, type ILibraryItem, LibraryItemType } from '../backend/types';
|
||||||
import { useMutatingLibrary } from '../backend/use-mutating-library';
|
import { useMutatingLibrary } from '../backend/use-mutating-library';
|
||||||
|
|
||||||
import { IconShowSidebar } from './icon-show-sidebar';
|
|
||||||
import { MiniSelectorOSS } from './mini-selector-oss';
|
import { MiniSelectorOSS } from './mini-selector-oss';
|
||||||
|
|
||||||
interface ToolbarItemCardProps {
|
interface ToolbarItemCardProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
isNarrow: boolean;
|
|
||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
isMutable: boolean;
|
isMutable: boolean;
|
||||||
schema: ILibraryItem;
|
schema: ILibraryItem;
|
||||||
deleteSchema: () => void;
|
deleteSchema: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolbarItemCard({
|
export function ToolbarItemCard({ className, schema, onSubmit, isMutable, deleteSchema }: ToolbarItemCardProps) {
|
||||||
className,
|
|
||||||
isNarrow,
|
|
||||||
schema,
|
|
||||||
onSubmit,
|
|
||||||
isMutable,
|
|
||||||
deleteSchema
|
|
||||||
}: ToolbarItemCardProps) {
|
|
||||||
const role = useRoleStore(state => state.role);
|
const role = useRoleStore(state => state.role);
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const { isModified } = useModificationStore();
|
const { isModified } = useModificationStore();
|
||||||
const isProcessing = useMutatingLibrary();
|
const isProcessing = useMutatingLibrary();
|
||||||
const canSave = isModified && !isProcessing;
|
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 = (() => {
|
const ossSelector = (() => {
|
||||||
if (schema.item_type !== LibraryItemType.RSFORM) {
|
if (schema.item_type !== LibraryItemType.RSFORM) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -93,15 +76,6 @@ export function ToolbarItemCard({
|
||||||
disabled={!isMutable || isProcessing || role < UserRole.OWNER}
|
disabled={!isMutable || isProcessing || role < UserRole.OWNER}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : 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} />
|
<BadgeHelp topic={HelpTopic.UI_RS_CARD} offset={4} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,8 +3,7 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useUsers } from '@/features/users';
|
import { useUsers } from '@/features/users';
|
||||||
import { SelectUser } from '@/features/users/components/select-user';
|
import { SelectUser, TableUsers } from '@/features/users/components';
|
||||||
import { TableUsers } from '@/features/users/components/table-users';
|
|
||||||
|
|
||||||
import { MiniButton } from '@/components/control';
|
import { MiniButton } from '@/components/control';
|
||||||
import { IconRemove } from '@/components/icons';
|
import { IconRemove } from '@/components/icons';
|
||||||
|
@ -49,8 +48,9 @@ export function DlgEditEditors() {
|
||||||
<span>Всего редакторов [{selected.length}]</span>
|
<span>Всего редакторов [{selected.length}]</span>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Очистить список'
|
title='Очистить список'
|
||||||
|
noHover
|
||||||
className='py-0 align-middle'
|
className='py-0 align-middle'
|
||||||
icon={<IconRemove size='1.25rem' className='cc-remove' />}
|
icon={<IconRemove size='1.5rem' className='cc-remove' />}
|
||||||
onClick={() => setSelected([])}
|
onClick={() => setSelected([])}
|
||||||
disabled={selected.length === 0}
|
disabled={selected.length === 0}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -63,6 +63,7 @@ export function TableVersions({ processing, items, onDelete, selected, onSelect
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Удалить версию'
|
title='Удалить версию'
|
||||||
className='align-middle'
|
className='align-middle'
|
||||||
|
noHover
|
||||||
noPadding
|
noPadding
|
||||||
icon={<IconRemove size='1.25rem' className='cc-remove' />}
|
icon={<IconRemove size='1.25rem' className='cc-remove' />}
|
||||||
onClick={event => handleDeleteVersion(event, props.row.original.id)}
|
onClick={event => handleDeleteVersion(event, props.row.original.id)}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { RequireAuth } from '@/features/auth/components/require-auth';
|
import { RequireAuth } from '@/features/auth/components';
|
||||||
|
|
||||||
import { FormCreateItem } from './form-create-item';
|
import { FormCreateItem } from './form-create-item';
|
||||||
|
|
||||||
|
|
|
@ -58,7 +58,7 @@ export function LibraryPage() {
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Выгрузить в формате CSV'
|
title='Выгрузить в формате CSV'
|
||||||
className='absolute z-tooltip -top-8 right-6 hidden sm:block'
|
className='absolute z-tooltip -top-8 right-6 hidden sm:block'
|
||||||
icon={<IconCSV size='1.25rem' className='text-muted-foreground hover:text-constructive' />}
|
icon={<IconCSV size='1.25rem' className='icon-green' />}
|
||||||
onClick={handleDownloadCSV}
|
onClick={handleDownloadCSV}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { SelectUser } from '@/features/users/components/select-user';
|
import { SelectUser } from '@/features/users/components';
|
||||||
|
|
||||||
import { MiniButton, SelectorButton } from '@/components/control';
|
import { MiniButton, SelectorButton } from '@/components/control';
|
||||||
import { Dropdown, DropdownButton, useDropdown } from '@/components/dropdown';
|
import { Dropdown, DropdownButton, useDropdown } from '@/components/dropdown';
|
||||||
|
@ -156,21 +156,28 @@ export function ToolbarSearch({ className, total, filtered }: ToolbarSearchProps
|
||||||
(head ? describeLocationHead(head) : 'Выберите каталог') + '<br/><kbd>Ctrl + клик</kbd> - Проводник'
|
(head ? describeLocationHead(head) : 'Выберите каталог') + '<br/><kbd>Ctrl + клик</kbd> - Проводник'
|
||||||
}
|
}
|
||||||
hideTitle={headMenu.isOpen}
|
hideTitle={headMenu.isOpen}
|
||||||
icon={head ? <IconLocationHead value={head} size='1.25rem' /> : <IconFolderSearch size='1.25rem' />}
|
icon={
|
||||||
|
head ? (
|
||||||
|
<IconLocationHead value={head} size='1.25rem' />
|
||||||
|
) : (
|
||||||
|
<IconFolderSearch size='1.25rem' className='cc-controls' />
|
||||||
|
)
|
||||||
|
}
|
||||||
onClick={handleFolderClick}
|
onClick={handleFolderClick}
|
||||||
|
text={head ?? '//'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Dropdown isOpen={headMenu.isOpen} stretchLeft>
|
<Dropdown isOpen={headMenu.isOpen} stretchLeft>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='проводник...'
|
text='проводник...'
|
||||||
title='Переключение в режим Проводник'
|
title='Переключение в режим Проводник'
|
||||||
icon={<IconFolderTree size='1rem' className='icon-primary' />}
|
icon={<IconFolderTree size='1rem' className='cc-controls' />}
|
||||||
onClick={handleToggleFolder}
|
onClick={handleToggleFolder}
|
||||||
/>
|
/>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='отображать все'
|
text='отображать все'
|
||||||
title='Очистить фильтр по расположению'
|
title='Очистить фильтр по расположению'
|
||||||
icon={<IconFolder size='1rem' className='icon-primary' />}
|
icon={<IconFolder size='1rem' className='cc-controls' />}
|
||||||
onClick={() => handleChange(null)}
|
onClick={() => handleChange(null)}
|
||||||
/>
|
/>
|
||||||
{Object.values(LocationHead).map((head, index) => {
|
{Object.values(LocationHead).map((head, index) => {
|
||||||
|
@ -193,7 +200,7 @@ export function ToolbarSearch({ className, total, filtered }: ToolbarSearchProps
|
||||||
placeholder='Путь'
|
placeholder='Путь'
|
||||||
noIcon
|
noIcon
|
||||||
noBorder
|
noBorder
|
||||||
className='w-18 sm:w-20 grow ml-1'
|
className='w-18 sm:w-20 grow'
|
||||||
query={path}
|
query={path}
|
||||||
onChangeQuery={setPath}
|
onChangeQuery={setPath}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -39,9 +39,10 @@ export function useLibraryColumns() {
|
||||||
titleHtml='Переключение в режим Проводник'
|
titleHtml='Переключение в режим Проводник'
|
||||||
aria-label='Переключатель режима Проводник'
|
aria-label='Переключатель режима Проводник'
|
||||||
noPadding
|
noPadding
|
||||||
className='ml-2 max-h-4 -translate-y-0.5'
|
noHover
|
||||||
|
className='pl-2 max-h-4 -translate-y-0.5'
|
||||||
onClick={handleToggleFolder}
|
onClick={handleToggleFolder}
|
||||||
icon={<IconFolderTree size='1.25rem' className='text-primary' />}
|
icon={<IconFolderTree size='1.25rem' className='cc-controls' />}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
size: 50,
|
size: 50,
|
||||||
|
|
|
@ -3,7 +3,7 @@ import clsx from 'clsx';
|
||||||
|
|
||||||
import { useAuthSuspense } from '@/features/auth';
|
import { useAuthSuspense } from '@/features/auth';
|
||||||
import { HelpTopic } from '@/features/help';
|
import { HelpTopic } from '@/features/help';
|
||||||
import { BadgeHelp } from '@/features/help/components/badge-help';
|
import { BadgeHelp } from '@/features/help/components';
|
||||||
|
|
||||||
import { MiniButton } from '@/components/control';
|
import { MiniButton } from '@/components/control';
|
||||||
import { IconFolderEdit, IconFolderTree } from '@/components/icons';
|
import { IconFolderEdit, IconFolderTree } from '@/components/icons';
|
||||||
|
@ -86,8 +86,8 @@ export function ViewSideLocation({ isVisible, onRenameLocation }: ViewSideLocati
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Переключение в режим Таблица'
|
title='Переключение в режим Поиск'
|
||||||
icon={<IconFolderTree size='1.25rem' className='text-primary' />}
|
icon={<IconFolderTree size='1.25rem' className='icon-green' />}
|
||||||
onClick={toggleFolderMode}
|
onClick={toggleFolderMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -50,7 +50,7 @@ export const ossApi = {
|
||||||
axiosPatch({
|
axiosPatch({
|
||||||
endpoint: `/api/oss/${itemID}/update-layout`,
|
endpoint: `/api/oss/${itemID}/update-layout`,
|
||||||
request: {
|
request: {
|
||||||
data: { data: data },
|
data: data,
|
||||||
successMessage: isSilent ? undefined : infoMsg.changesSaved
|
successMessage: isSilent ? undefined : infoMsg.changesSaved
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -90,7 +90,7 @@ export class OssLoader {
|
||||||
this.graph.topologicalOrder().forEach(operationID => {
|
this.graph.topologicalOrder().forEach(operationID => {
|
||||||
const operation = this.operationByID.get(operationID)!;
|
const operation = this.operationByID.get(operationID)!;
|
||||||
const schema = this.items.find(item => item.id === operation.result);
|
const schema = this.items.find(item => item.id === operation.result);
|
||||||
const position = this.oss.layout.find(item => item.nodeID === operation.nodeID);
|
const position = this.oss.layout.operations.find(item => item.id === operationID);
|
||||||
operation.x = position?.x ?? 0;
|
operation.x = position?.x ?? 0;
|
||||||
operation.y = position?.y ?? 0;
|
operation.y = position?.y ?? 0;
|
||||||
operation.is_consolidation = this.inferConsolidation(operationID);
|
operation.is_consolidation = this.inferConsolidation(operationID);
|
||||||
|
@ -104,7 +104,7 @@ export class OssLoader {
|
||||||
|
|
||||||
private inferBlockAttributes() {
|
private inferBlockAttributes() {
|
||||||
this.oss.blocks.forEach(block => {
|
this.oss.blocks.forEach(block => {
|
||||||
const geometry = this.oss.layout.find(item => item.nodeID === block.nodeID);
|
const geometry = this.oss.layout.blocks.find(item => item.id === block.id);
|
||||||
block.x = geometry?.x ?? 0;
|
block.x = geometry?.x ?? 0;
|
||||||
block.y = geometry?.y ?? 0;
|
block.y = geometry?.y ?? 0;
|
||||||
block.width = geometry?.width ?? BLOCK_NODE_MIN_WIDTH;
|
block.width = geometry?.width ?? BLOCK_NODE_MIN_WIDTH;
|
||||||
|
|
|
@ -72,8 +72,11 @@ export type IRelocateConstituentsDTO = z.infer<typeof schemaRelocateConstituents
|
||||||
/** Represents {@link IConstituenta} reference. */
|
/** Represents {@link IConstituenta} reference. */
|
||||||
export type IConstituentaReference = z.infer<typeof schemaConstituentaReference>;
|
export type IConstituentaReference = z.infer<typeof schemaConstituentaReference>;
|
||||||
|
|
||||||
/** Represents {@link IOperationSchema} node position. */
|
/** Represents {@link IOperation} position. */
|
||||||
export type INodePosition = z.infer<typeof schemaNodePosition>;
|
export type IOperationPosition = z.infer<typeof schemaOperationPosition>;
|
||||||
|
|
||||||
|
/** Represents {@link IBlock} position. */
|
||||||
|
export type IBlockPosition = z.infer<typeof schemaBlockPosition>;
|
||||||
|
|
||||||
// ====== Schemas ======
|
// ====== Schemas ======
|
||||||
export const schemaOperationType = z.enum(Object.values(OperationType) as [OperationType, ...OperationType[]]);
|
export const schemaOperationType = z.enum(Object.values(OperationType) as [OperationType, ...OperationType[]]);
|
||||||
|
@ -105,15 +108,24 @@ export const schemaCstSubstituteInfo = schemaSubstituteConstituents.extend({
|
||||||
substitution_term: z.string()
|
substitution_term: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const schemaNodePosition = z.strictObject({
|
export const schemaOperationPosition = z.strictObject({
|
||||||
nodeID: z.string(),
|
id: z.number(),
|
||||||
|
x: z.number(),
|
||||||
|
y: z.number()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const schemaBlockPosition = z.strictObject({
|
||||||
|
id: z.number(),
|
||||||
x: z.number(),
|
x: z.number(),
|
||||||
y: z.number(),
|
y: z.number(),
|
||||||
width: z.number(),
|
width: z.number(),
|
||||||
height: z.number()
|
height: z.number()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const schemaOssLayout = z.array(schemaNodePosition);
|
export const schemaOssLayout = z.strictObject({
|
||||||
|
operations: z.array(schemaOperationPosition),
|
||||||
|
blocks: z.array(schemaBlockPosition)
|
||||||
|
});
|
||||||
|
|
||||||
export const schemaOperationSchema = schemaLibraryItem.extend({
|
export const schemaOperationSchema = schemaLibraryItem.extend({
|
||||||
editors: z.number().array(),
|
editors: z.number().array(),
|
||||||
|
@ -176,8 +188,6 @@ export const schemaCreateOperation = z.strictObject({
|
||||||
}),
|
}),
|
||||||
position_x: z.number(),
|
position_x: z.number(),
|
||||||
position_y: z.number(),
|
position_y: z.number(),
|
||||||
width: z.number(),
|
|
||||||
height: z.number(),
|
|
||||||
arguments: z.array(z.number()),
|
arguments: z.array(z.number()),
|
||||||
create_schema: z.boolean()
|
create_schema: z.boolean()
|
||||||
});
|
});
|
||||||
|
|
|
@ -98,18 +98,21 @@ export function PickContents({
|
||||||
<div className='flex w-fit'>
|
<div className='flex w-fit'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Удалить'
|
title='Удалить'
|
||||||
|
noHover
|
||||||
className='px-0'
|
className='px-0'
|
||||||
icon={<IconRemove size='1rem' className='icon-red' />}
|
icon={<IconRemove size='1rem' className='icon-red' />}
|
||||||
onClick={() => handleDelete(props.row.original)}
|
onClick={() => handleDelete(props.row.original)}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Переместить выше'
|
title='Переместить выше'
|
||||||
|
noHover
|
||||||
className='px-0'
|
className='px-0'
|
||||||
icon={<IconMoveUp size='1rem' className='icon-primary' />}
|
icon={<IconMoveUp size='1rem' className='icon-primary' />}
|
||||||
onClick={() => handleMoveUp(props.row.original)}
|
onClick={() => handleMoveUp(props.row.original)}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Переместить ниже'
|
title='Переместить ниже'
|
||||||
|
noHover
|
||||||
className='px-0'
|
className='px-0'
|
||||||
icon={<IconMoveDown size='1rem' className='icon-primary' />}
|
icon={<IconMoveDown size='1rem' className='icon-primary' />}
|
||||||
onClick={() => handleMoveDown(props.row.original)}
|
onClick={() => handleMoveDown(props.row.original)}
|
||||||
|
|
|
@ -82,18 +82,21 @@ export function PickMultiOperation({ rows, items, value, onChange, className, ..
|
||||||
<div className='flex w-fit'>
|
<div className='flex w-fit'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Удалить'
|
title='Удалить'
|
||||||
|
noHover
|
||||||
className='px-0'
|
className='px-0'
|
||||||
icon={<IconRemove size='1rem' className='icon-red' />}
|
icon={<IconRemove size='1rem' className='icon-red' />}
|
||||||
onClick={() => handleDelete(props.row.original.id)}
|
onClick={() => handleDelete(props.row.original.id)}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Переместить выше'
|
title='Переместить выше'
|
||||||
|
noHover
|
||||||
className='px-0'
|
className='px-0'
|
||||||
icon={<IconMoveUp size='1rem' className='icon-primary' />}
|
icon={<IconMoveUp size='1rem' className='icon-primary' />}
|
||||||
onClick={() => handleMoveUp(props.row.original.id)}
|
onClick={() => handleMoveUp(props.row.original.id)}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Переместить ниже'
|
title='Переместить ниже'
|
||||||
|
noHover
|
||||||
className='px-0'
|
className='px-0'
|
||||||
icon={<IconMoveDown size='1rem' className='icon-primary' />}
|
icon={<IconMoveDown size='1rem' className='icon-primary' />}
|
||||||
onClick={() => handleMoveDown(props.row.original.id)}
|
onClick={() => handleMoveDown(props.row.original.id)}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|
||||||
import { type ILibraryItem, LibraryItemType } from '@/features/library';
|
import { type ILibraryItem, LibraryItemType } from '@/features/library';
|
||||||
import { useLibrary } from '@/features/library/backend/use-library';
|
import { useLibrary } from '@/features/library/backend/use-library';
|
||||||
import { PickSchema } from '@/features/library/components/pick-schema';
|
import { PickSchema } from '@/features/library/components';
|
||||||
|
|
||||||
import { MiniButton } from '@/components/control';
|
import { MiniButton } from '@/components/control';
|
||||||
import { IconReset } from '@/components/icons';
|
import { IconReset } from '@/components/icons';
|
||||||
|
@ -61,6 +61,7 @@ export function DlgChangeInputSchema() {
|
||||||
<Label text='Загружаемая концептуальная схема' />
|
<Label text='Загружаемая концептуальная схема' />
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Сбросить выбор схемы'
|
title='Сбросить выбор схемы'
|
||||||
|
noHover
|
||||||
noPadding
|
noPadding
|
||||||
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
||||||
onClick={() => setValue('input', null)}
|
onClick={() => setValue('input', null)}
|
||||||
|
|
|
@ -84,9 +84,14 @@ export function DlgCreateBlock() {
|
||||||
className='w-160 px-6 h-110'
|
className='w-160 px-6 h-110'
|
||||||
helpTopic={HelpTopic.CC_OSS}
|
helpTopic={HelpTopic.CC_OSS}
|
||||||
>
|
>
|
||||||
<Tabs className='grid' selectedIndex={activeTab} onSelect={index => setActiveTab(index as TabID)}>
|
<Tabs
|
||||||
<TabList className='z-pop mx-auto -mb-5 flex border divide-x rounded-none'>
|
selectedTabClassName='cc-selected'
|
||||||
<TabLabel title='Основные атрибуты блока' label='Паспорт' />
|
className='grid'
|
||||||
|
selectedIndex={activeTab}
|
||||||
|
onSelect={index => setActiveTab(index as TabID)}
|
||||||
|
>
|
||||||
|
<TabList className='z-pop mx-auto -mb-5 flex border divide-x rounded-none bg-secondary'>
|
||||||
|
<TabLabel title='Основные атрибуты блока' label='Карточка' />
|
||||||
<TabLabel
|
<TabLabel
|
||||||
title={`Выбор вложенных узлов: [${children_operations.length + children_blocks.length}]`}
|
title={`Выбор вложенных узлов: [${children_operations.length + children_blocks.length}]`}
|
||||||
label={`Содержимое${children_operations.length + children_blocks.length > 0 ? '*' : ''}`}
|
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 { type ICreateOperationDTO, OperationType, schemaCreateOperation } from '../../backend/types';
|
||||||
import { useCreateOperation } from '../../backend/use-create-operation';
|
import { useCreateOperation } from '../../backend/use-create-operation';
|
||||||
import { describeOperationType, labelOperationType } from '../../labels';
|
import { describeOperationType, labelOperationType } from '../../labels';
|
||||||
import { type LayoutManager, OPERATION_NODE_HEIGHT, OPERATION_NODE_WIDTH } from '../../models/oss-layout-api';
|
import { type LayoutManager } from '../../models/oss-layout-api';
|
||||||
|
|
||||||
import { TabInputOperation } from './tab-input-operation';
|
import { TabInputOperation } from './tab-input-operation';
|
||||||
import { TabSynthesisOperation } from './tab-synthesis-operation';
|
import { TabSynthesisOperation } from './tab-synthesis-operation';
|
||||||
|
@ -54,8 +54,6 @@ export function DlgCreateOperation() {
|
||||||
position_x: defaultX,
|
position_x: defaultX,
|
||||||
position_y: defaultY,
|
position_y: defaultY,
|
||||||
arguments: initialInputs,
|
arguments: initialInputs,
|
||||||
width: OPERATION_NODE_WIDTH,
|
|
||||||
height: OPERATION_NODE_HEIGHT,
|
|
||||||
create_schema: false,
|
create_schema: false,
|
||||||
layout: manager.layout
|
layout: manager.layout
|
||||||
},
|
},
|
||||||
|
@ -100,11 +98,12 @@ export function DlgCreateOperation() {
|
||||||
helpTopic={HelpTopic.CC_OSS}
|
helpTopic={HelpTopic.CC_OSS}
|
||||||
>
|
>
|
||||||
<Tabs
|
<Tabs
|
||||||
|
selectedTabClassName='cc-selected'
|
||||||
className='grid'
|
className='grid'
|
||||||
selectedIndex={activeTab}
|
selectedIndex={activeTab}
|
||||||
onSelect={(index, last) => handleSelectTab(index as TabID, last as TabID)}
|
onSelect={(index, last) => handleSelectTab(index as TabID, last as TabID)}
|
||||||
>
|
>
|
||||||
<TabList className='z-pop mx-auto -mb-5 flex border divide-x rounded-none'>
|
<TabList className='z-pop mx-auto -mb-5 flex border divide-x rounded-none bg-secondary'>
|
||||||
<TabLabel
|
<TabLabel
|
||||||
title={describeOperationType(OperationType.INPUT)}
|
title={describeOperationType(OperationType.INPUT)}
|
||||||
label={labelOperationType(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 { type ILibraryItem, LibraryItemType } from '@/features/library';
|
||||||
import { useLibrary } from '@/features/library/backend/use-library';
|
import { useLibrary } from '@/features/library/backend/use-library';
|
||||||
import { PickSchema } from '@/features/library/components/pick-schema';
|
import { PickSchema } from '@/features/library/components';
|
||||||
|
|
||||||
import { MiniButton } from '@/components/control';
|
import { MiniButton } from '@/components/control';
|
||||||
import { IconReset } from '@/components/icons';
|
import { IconReset } from '@/components/icons';
|
||||||
|
@ -97,6 +97,7 @@ export function TabInputOperation() {
|
||||||
<Label text='Загружаемая концептуальная схема' />
|
<Label text='Загружаемая концептуальная схема' />
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Сбросить выбор схемы'
|
title='Сбросить выбор схемы'
|
||||||
|
noHover
|
||||||
noPadding
|
noPadding
|
||||||
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
||||||
onClick={() => setValue('item_data.result', null)}
|
onClick={() => setValue('item_data.result', null)}
|
||||||
|
|
|
@ -43,7 +43,7 @@ export function DlgEditBlock() {
|
||||||
|
|
||||||
function onSubmit(data: IUpdateBlockDTO) {
|
function onSubmit(data: IUpdateBlockDTO) {
|
||||||
if (data.item_data.parent !== target.parent) {
|
if (data.item_data.parent !== target.parent) {
|
||||||
manager.onChangeParent(target.nodeID, data.item_data.parent === null ? null : `b${data.item_data.parent}`);
|
manager.onBlockChangeParent(data.target, data.item_data.parent);
|
||||||
data.layout = manager.layout;
|
data.layout = manager.layout;
|
||||||
}
|
}
|
||||||
return updateBlock({ itemID: manager.oss.id, data });
|
return updateBlock({ itemID: manager.oss.id, data });
|
||||||
|
|
|
@ -59,7 +59,7 @@ export function DlgEditOperation() {
|
||||||
|
|
||||||
function onSubmit(data: IUpdateOperationDTO) {
|
function onSubmit(data: IUpdateOperationDTO) {
|
||||||
if (data.item_data.parent !== target.parent) {
|
if (data.item_data.parent !== target.parent) {
|
||||||
manager.onChangeParent(target.nodeID, data.item_data.parent === null ? null : `b${data.item_data.parent}`);
|
manager.onOperationChangeParent(data.target, data.item_data.parent);
|
||||||
data.layout = manager.layout;
|
data.layout = manager.layout;
|
||||||
}
|
}
|
||||||
return updateOperation({ itemID: manager.oss.id, data });
|
return updateOperation({ itemID: manager.oss.id, data });
|
||||||
|
@ -75,11 +75,16 @@ export function DlgEditOperation() {
|
||||||
helpTopic={HelpTopic.UI_SUBSTITUTIONS}
|
helpTopic={HelpTopic.UI_SUBSTITUTIONS}
|
||||||
hideHelpWhen={() => activeTab !== TabID.SUBSTITUTION}
|
hideHelpWhen={() => activeTab !== TabID.SUBSTITUTION}
|
||||||
>
|
>
|
||||||
<Tabs className='grid' selectedIndex={activeTab} onSelect={index => setActiveTab(index as TabID)}>
|
<Tabs
|
||||||
<TabList className='mb-3 mx-auto w-fit flex border divide-x rounded-none'>
|
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'>
|
||||||
<TabLabel
|
<TabLabel
|
||||||
title='Текстовые поля' //
|
title='Текстовые поля' //
|
||||||
label='Паспорт'
|
label='Карточка'
|
||||||
className='w-32'
|
className='w-32'
|
||||||
/>
|
/>
|
||||||
<TabLabel
|
<TabLabel
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import { Controller, useFormContext, useWatch } from 'react-hook-form';
|
import { Controller, useFormContext, useWatch } from 'react-hook-form';
|
||||||
|
|
||||||
import { useRSForms } from '@/features/rsform/backend/use-rsforms';
|
import { useRSForms } from '@/features/rsform/backend/use-rsforms';
|
||||||
import { PickSubstitutions } from '@/features/rsform/components/pick-substitutions';
|
import { PickSubstitutions } from '@/features/rsform/components';
|
||||||
|
|
||||||
import { TextArea } from '@/components/input';
|
import { TextArea } from '@/components/input';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
|
|
@ -7,9 +7,9 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { HelpTopic } from '@/features/help';
|
import { HelpTopic } from '@/features/help';
|
||||||
import { type ILibraryItem } from '@/features/library';
|
import { type ILibraryItem } from '@/features/library';
|
||||||
import { useLibrary } from '@/features/library/backend/use-library';
|
import { useLibrary } from '@/features/library/backend/use-library';
|
||||||
import { SelectLibraryItem } from '@/features/library/components/select-library-item';
|
import { SelectLibraryItem } from '@/features/library/components';
|
||||||
import { useRSForm } from '@/features/rsform/backend/use-rsform';
|
import { useRSForm } from '@/features/rsform/backend/use-rsform';
|
||||||
import { PickMultiConstituenta } from '@/features/rsform/components/pick-multi-constituenta';
|
import { PickMultiConstituenta } from '@/features/rsform/components';
|
||||||
|
|
||||||
import { MiniButton } from '@/components/control';
|
import { MiniButton } from '@/components/control';
|
||||||
import { Loader } from '@/components/loader';
|
import { Loader } from '@/components/loader';
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
import { type ICreateBlockDTO, type ICreateOperationDTO, type INodePosition, type IOssLayout } from '../backend/types';
|
import {
|
||||||
|
type IBlockPosition,
|
||||||
|
type ICreateBlockDTO,
|
||||||
|
type ICreateOperationDTO,
|
||||||
|
type IOperationPosition,
|
||||||
|
type IOssLayout
|
||||||
|
} from '../backend/types';
|
||||||
|
|
||||||
import { type IOperationSchema } from './oss';
|
import { type IOperationSchema } from './oss';
|
||||||
import { type Position2D, type Rectangle2D } from './oss-layout';
|
import { type Position2D, type Rectangle2D } from './oss-layout';
|
||||||
|
@ -6,8 +12,8 @@ import { type Position2D, type Rectangle2D } from './oss-layout';
|
||||||
export const GRID_SIZE = 10; // pixels - size of OSS grid
|
export const GRID_SIZE = 10; // pixels - size of OSS grid
|
||||||
const MIN_DISTANCE = 2 * GRID_SIZE; // pixels - minimum distance between nodes
|
const MIN_DISTANCE = 2 * GRID_SIZE; // pixels - minimum distance between nodes
|
||||||
|
|
||||||
export const OPERATION_NODE_WIDTH = 150;
|
const OPERATION_NODE_WIDTH = 150;
|
||||||
export const OPERATION_NODE_HEIGHT = 40;
|
const OPERATION_NODE_HEIGHT = 40;
|
||||||
|
|
||||||
/** Layout manipulations for {@link IOperationSchema}. */
|
/** Layout manipulations for {@link IOperationSchema}. */
|
||||||
export class LayoutManager {
|
export class LayoutManager {
|
||||||
|
@ -24,40 +30,52 @@ export class LayoutManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Calculate insert position for a new {@link IOperation} */
|
/** Calculate insert position for a new {@link IOperation} */
|
||||||
newOperationPosition(data: ICreateOperationDTO): Rectangle2D {
|
newOperationPosition(data: ICreateOperationDTO): Position2D {
|
||||||
const result = { x: data.position_x, y: data.position_y, width: data.width, height: data.height };
|
let result = { x: data.position_x, y: data.position_y };
|
||||||
const parentNode = this.layout.find(pos => pos.nodeID === `b${data.item_data.parent}`) ?? null;
|
const operations = this.layout.operations;
|
||||||
const operations = this.layout.filter(pos => pos.nodeID.startsWith('o'));
|
const parentNode = this.layout.blocks.find(pos => pos.id === data.item_data.parent);
|
||||||
|
if (operations.length === 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
if (data.arguments.length !== 0) {
|
if (data.arguments.length !== 0) {
|
||||||
const pos = calculatePositionFromArgs(
|
result = calculatePositionFromArgs(data.arguments, operations);
|
||||||
operations.filter(node => data.arguments.includes(Number(node.nodeID.slice(1))))
|
|
||||||
);
|
|
||||||
result.x = pos.x;
|
|
||||||
result.y = pos.y;
|
|
||||||
} else if (parentNode) {
|
} else if (parentNode) {
|
||||||
result.x = parentNode.x + MIN_DISTANCE;
|
result.x = parentNode.x + MIN_DISTANCE;
|
||||||
result.y = parentNode.y + MIN_DISTANCE;
|
result.y = parentNode.y + MIN_DISTANCE;
|
||||||
} else {
|
} else {
|
||||||
const pos = this.calculatePositionForFreeOperation(result);
|
result = this.calculatePositionForFreeOperation(result);
|
||||||
result.x = pos.x;
|
|
||||||
result.y = pos.y;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
preventOverlap(result, operations);
|
result = preventOverlap(
|
||||||
this.extendParentBounds(parentNode, result);
|
{ ...result, width: OPERATION_NODE_WIDTH, height: OPERATION_NODE_HEIGHT },
|
||||||
|
operations.map(node => ({ ...node, width: OPERATION_NODE_WIDTH, height: OPERATION_NODE_HEIGHT }))
|
||||||
|
);
|
||||||
|
|
||||||
return 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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Calculate insert position for a new {@link IBlock} */
|
/** Calculate insert position for a new {@link IBlock} */
|
||||||
newBlockPosition(data: ICreateBlockDTO): Rectangle2D {
|
newBlockPosition(data: ICreateBlockDTO): Rectangle2D {
|
||||||
const block_nodes = data.children_blocks
|
const block_nodes = data.children_blocks
|
||||||
.map(id => this.layout.find(block => block.nodeID === `b${id}`))
|
.map(id => this.layout.blocks.find(block => block.id === id))
|
||||||
.filter(node => !!node);
|
.filter(node => !!node);
|
||||||
const operation_nodes = data.children_operations
|
const operation_nodes = data.children_operations
|
||||||
.map(id => this.layout.find(operation => operation.nodeID === `o${id}`))
|
.map(id => this.layout.operations.find(operation => operation.id === id))
|
||||||
.filter(node => !!node);
|
.filter(node => !!node);
|
||||||
const parentNode = this.layout.find(pos => pos.nodeID === `b${data.item_data.parent}`) ?? null;
|
const parentNode = this.layout.blocks.find(pos => pos.id === data.item_data.parent);
|
||||||
|
|
||||||
let result: Rectangle2D = { x: data.position_x, y: data.position_y, width: data.width, height: data.height };
|
let result: Rectangle2D = { x: data.position_x, y: data.position_y, width: data.width, height: data.height };
|
||||||
|
|
||||||
|
@ -80,78 +98,61 @@ export class LayoutManager {
|
||||||
|
|
||||||
if (block_nodes.length === 0 && operation_nodes.length === 0) {
|
if (block_nodes.length === 0 && operation_nodes.length === 0) {
|
||||||
if (parentNode) {
|
if (parentNode) {
|
||||||
const siblings = this.oss.blocks
|
const siblings = this.oss.blocks.filter(block => block.parent === parentNode.id).map(block => block.id);
|
||||||
.filter(block => block.parent === data.item_data.parent)
|
|
||||||
.map(block => block.nodeID);
|
|
||||||
if (siblings.length > 0) {
|
if (siblings.length > 0) {
|
||||||
preventOverlap(
|
result = preventOverlap(
|
||||||
result,
|
result,
|
||||||
this.layout.filter(node => siblings.includes(node.nodeID))
|
this.layout.blocks.filter(block => siblings.includes(block.id))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const rootBlocks = this.oss.blocks.filter(block => block.parent === null).map(block => block.nodeID);
|
const rootBlocks = this.oss.blocks.filter(block => block.parent === null).map(block => block.id);
|
||||||
if (rootBlocks.length > 0) {
|
if (rootBlocks.length > 0) {
|
||||||
preventOverlap(
|
result = preventOverlap(
|
||||||
result,
|
result,
|
||||||
this.layout.filter(node => rootBlocks.includes(node.nodeID))
|
this.layout.blocks.filter(block => rootBlocks.includes(block.id))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.extendParentBounds(parentNode, result);
|
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
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Update layout when parent changes */
|
/** Update layout when parent changes */
|
||||||
onChangeParent(targetID: string, newParent: string | null) {
|
onOperationChangeParent(targetID: number, newParent: number | null) {
|
||||||
const targetNode = this.layout.find(pos => pos.nodeID === targetID);
|
console.error('not implemented', targetID, newParent);
|
||||||
if (!targetNode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
/** Update layout when parent changes */
|
||||||
if (!parent) {
|
onBlockChangeParent(targetID: number, newParent: number | null) {
|
||||||
return;
|
console.error('not implemented', targetID, newParent);
|
||||||
}
|
|
||||||
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 {
|
private calculatePositionForFreeOperation(initial: Position2D): Position2D {
|
||||||
if (this.oss.operations.length === 0) {
|
const operations = this.layout.operations;
|
||||||
|
if (operations.length === 0) {
|
||||||
return initial;
|
return initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
const freeInputs = this.oss.operations
|
const freeInputs = this.oss.operations
|
||||||
.filter(operation => operation.arguments.length === 0 && operation.parent === null)
|
.filter(operation => operation.arguments.length === 0 && operation.parent === null)
|
||||||
.map(operation => operation.nodeID);
|
.map(operation => operation.id);
|
||||||
let inputsPositions = this.layout.filter(pos => freeInputs.includes(pos.nodeID));
|
let inputsPositions = operations.filter(pos => freeInputs.includes(pos.id));
|
||||||
if (inputsPositions.length === 0) {
|
if (inputsPositions.length === 0) {
|
||||||
inputsPositions = this.layout.filter(pos => pos.nodeID.startsWith('o'));
|
inputsPositions = operations;
|
||||||
}
|
}
|
||||||
const maxX = Math.max(...inputsPositions.map(node => node.x));
|
const maxX = Math.max(...inputsPositions.map(node => node.x));
|
||||||
const minY = Math.min(...inputsPositions.map(node => node.y));
|
const minY = Math.min(...inputsPositions.map(node => node.y));
|
||||||
|
@ -162,8 +163,8 @@ export class LayoutManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculatePositionForFreeBlock(initial: Rectangle2D): Rectangle2D {
|
private calculatePositionForFreeBlock(initial: Rectangle2D): Rectangle2D {
|
||||||
const rootBlocks = this.oss.blocks.filter(block => block.parent === null).map(block => block.nodeID);
|
const rootBlocks = this.oss.blocks.filter(block => block.parent === null).map(block => block.id);
|
||||||
const blocksPositions = this.layout.filter(pos => rootBlocks.includes(pos.nodeID));
|
const blocksPositions = this.layout.blocks.filter(pos => rootBlocks.includes(pos.id));
|
||||||
if (blocksPositions.length === 0) {
|
if (blocksPositions.length === 0) {
|
||||||
return initial;
|
return initial;
|
||||||
}
|
}
|
||||||
|
@ -171,23 +172,6 @@ export class LayoutManager {
|
||||||
const minY = Math.min(...blocksPositions.map(node => node.y));
|
const minY = Math.min(...blocksPositions.map(node => node.y));
|
||||||
return { ...initial, x: maxX + MIN_DISTANCE, y: minY };
|
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 =======
|
// ======= Internals =======
|
||||||
|
@ -200,25 +184,38 @@ function rectanglesOverlap(a: Rectangle2D, b: Rectangle2D): boolean {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function preventOverlap(target: Rectangle2D, fixedRectangles: Rectangle2D[]) {
|
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 {
|
||||||
let hasOverlap: boolean;
|
let hasOverlap: boolean;
|
||||||
do {
|
do {
|
||||||
hasOverlap = false;
|
hasOverlap = false;
|
||||||
for (const fixed of fixedRectangles) {
|
for (const fixed of fixedRectangles) {
|
||||||
if (rectanglesOverlap(target, fixed)) {
|
if (rectanglesOverlap(target, fixed)) {
|
||||||
hasOverlap = true;
|
hasOverlap = true;
|
||||||
target.x += MIN_DISTANCE;
|
const overlap = getOverlapAmount(target, fixed);
|
||||||
target.y += MIN_DISTANCE;
|
if (overlap.x >= overlap.y) {
|
||||||
|
target.x += overlap.x + MIN_DISTANCE;
|
||||||
|
} else {
|
||||||
|
target.y += overlap.y + MIN_DISTANCE;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} while (hasOverlap);
|
} while (hasOverlap);
|
||||||
|
|
||||||
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculatePositionFromArgs(args: INodePosition[]): Position2D {
|
function calculatePositionFromArgs(args: number[], operations: IOperationPosition[]): Position2D {
|
||||||
const maxY = Math.max(...args.map(node => node.y));
|
const argNodes = operations.filter(pos => args.includes(pos.id));
|
||||||
const minX = Math.min(...args.map(node => node.x));
|
const maxY = Math.max(...argNodes.map(node => node.y));
|
||||||
const maxX = Math.max(...args.map(node => node.x));
|
const minX = Math.min(...argNodes.map(node => node.x));
|
||||||
|
const maxX = Math.max(...argNodes.map(node => node.x));
|
||||||
return {
|
return {
|
||||||
x: Math.ceil((maxX + minX) / 2 / GRID_SIZE) * GRID_SIZE,
|
x: Math.ceil((maxX + minX) / 2 / GRID_SIZE) * GRID_SIZE,
|
||||||
y: maxY + 2 * OPERATION_NODE_HEIGHT + MIN_DISTANCE
|
y: maxY + 2 * OPERATION_NODE_HEIGHT + MIN_DISTANCE
|
||||||
|
@ -227,23 +224,42 @@ function calculatePositionFromArgs(args: INodePosition[]): Position2D {
|
||||||
|
|
||||||
function calculatePositionFromChildren(
|
function calculatePositionFromChildren(
|
||||||
initial: Rectangle2D,
|
initial: Rectangle2D,
|
||||||
operations: INodePosition[],
|
operations: IOperationPosition[],
|
||||||
blocks: INodePosition[]
|
blocks: IBlockPosition[]
|
||||||
): Rectangle2D {
|
): Rectangle2D {
|
||||||
const allNodes = [...blocks, ...operations];
|
let left = undefined;
|
||||||
if (allNodes.length === 0) {
|
let top = undefined;
|
||||||
return initial;
|
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 left = Math.min(...allNodes.map(n => n.x)) - MIN_DISTANCE;
|
for (const operation of operations) {
|
||||||
const top = Math.min(...allNodes.map(n => n.y)) - MIN_DISTANCE;
|
left = left === undefined ? operation.x - MIN_DISTANCE : Math.min(left, operation.x - MIN_DISTANCE);
|
||||||
const right = Math.max(...allNodes.map(n => n.x + n.width)) + MIN_DISTANCE;
|
top = top === undefined ? operation.y - MIN_DISTANCE : Math.min(top, operation.y - MIN_DISTANCE);
|
||||||
const bottom = Math.max(...allNodes.map(n => n.y + n.height)) + 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);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x: left,
|
x: left ?? initial.x,
|
||||||
y: top,
|
y: top ?? initial.y,
|
||||||
width: right - left,
|
width: right !== undefined && left !== undefined ? right - left : initial.width,
|
||||||
height: bottom - top
|
height: bottom !== undefined && top !== undefined ? bottom - top : initial.height
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,27 +2,19 @@
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { EditorLibraryItem } from '@/features/library/components/editor-library-item';
|
import { EditorLibraryItem, ToolbarItemCard } from '@/features/library/components';
|
||||||
import { ToolbarItemCard } from '@/features/library/components/toolbar-item-card';
|
|
||||||
|
|
||||||
import { useWindowSize } from '@/hooks/use-window-size';
|
|
||||||
import { useModificationStore } from '@/stores/modification';
|
import { useModificationStore } from '@/stores/modification';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
|
||||||
import { globalIDs } from '@/utils/constants';
|
import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
import { OssStats } from '../../../components/oss-stats';
|
|
||||||
import { useOssEdit } from '../oss-edit-context';
|
import { useOssEdit } from '../oss-edit-context';
|
||||||
|
|
||||||
import { FormOSS } from './form-oss';
|
import { FormOSS } from './form-oss';
|
||||||
|
import { OssStats } from './oss-stats';
|
||||||
const SIDELIST_LAYOUT_THRESHOLD = 768; // px
|
|
||||||
|
|
||||||
export function EditorOssCard() {
|
export function EditorOssCard() {
|
||||||
const { schema, isMutable, deleteSchema } = useOssEdit();
|
const { schema, isMutable, deleteSchema } = useOssEdit();
|
||||||
const { isModified } = useModificationStore();
|
const { isModified } = useModificationStore();
|
||||||
const showOSSStats = usePreferencesStore(state => state.showOSSStats);
|
|
||||||
const windowSize = useWindowSize();
|
|
||||||
const isNarrow = !!windowSize.width && windowSize.width <= SIDELIST_LAYOUT_THRESHOLD;
|
|
||||||
|
|
||||||
function initiateSubmit() {
|
function initiateSubmit() {
|
||||||
const element = document.getElementById(globalIDs.library_item_editor) as HTMLFormElement;
|
const element = document.getElementById(globalIDs.library_item_editor) as HTMLFormElement;
|
||||||
|
@ -41,36 +33,25 @@ export function EditorOssCard() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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
|
<ToolbarItemCard
|
||||||
className='cc-tab-tools'
|
className='cc-tab-tools'
|
||||||
onSubmit={initiateSubmit}
|
onSubmit={initiateSubmit}
|
||||||
schema={schema}
|
schema={schema}
|
||||||
isMutable={isMutable}
|
isMutable={isMutable}
|
||||||
deleteSchema={deleteSchema}
|
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'>
|
||||||
|
<FormOSS key={schema.id} />
|
||||||
|
<EditorLibraryItem schema={schema} isAttachedToOSS={false} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='cc-column px-3 mx-0 md:mx-auto'>
|
<OssStats className='mt-3 md:mt-8 md:ml-5 w-80 md:w-56 mx-auto h-min' stats={schema.stats} />
|
||||||
<FormOSS key={schema.id} />
|
|
||||||
<EditorLibraryItem schema={schema} isAttachedToOSS={false} />
|
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
<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 { type IUpdateLibraryItemDTO, LibraryItemType, schemaUpdateLibraryItem } from '@/features/library';
|
||||||
import { useUpdateItem } from '@/features/library/backend/use-update-item';
|
import { useUpdateItem } from '@/features/library/backend/use-update-item';
|
||||||
import { ToolbarItemAccess } from '@/features/library/components/toolbar-item-access';
|
import { ToolbarItemAccess } from '@/features/library/components';
|
||||||
|
|
||||||
import { SubmitButton } from '@/components/control';
|
import { SubmitButton } from '@/components/control';
|
||||||
import { IconSave } from '@/components/icons';
|
import { IconSave } from '@/components/icons';
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
import { cn } from '@/components/utils';
|
import { cn } from '@/components/utils';
|
||||||
import { ValueStats } from '@/components/view';
|
import { ValueStats } from '@/components/view';
|
||||||
|
|
||||||
import { type IOperationSchemaStats } from '../models/oss';
|
import { type IOperationSchemaStats } from '../../../models/oss';
|
||||||
|
|
||||||
interface OssStatsProps {
|
interface OssStatsProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
@ -18,43 +18,48 @@ interface OssStatsProps {
|
||||||
|
|
||||||
export function OssStats({ className, stats }: OssStatsProps) {
|
export function OssStats({ className, stats }: OssStatsProps) {
|
||||||
return (
|
return (
|
||||||
<aside className={cn('grid grid-cols-4 gap-1 justify-items-end h-min', className)}>
|
<div className={cn('grid grid-cols-4 gap-1 justify-items-end', className)}>
|
||||||
<div id='count_operations' className='w-fit flex gap-3 hover:cursor-default '>
|
<div id='count_operations' className='w-fit flex gap-3 hover:cursor-default '>
|
||||||
<span>Всего</span>
|
<span>Всего</span>
|
||||||
<span>{stats.count_all}</span>
|
<span>{stats.count_all}</span>
|
||||||
</div>
|
</div>
|
||||||
<ValueStats id='count_block' title='Блоки' icon={<IconConceptBlock size='1.25rem' />} value={stats.count_block} />
|
<ValueStats
|
||||||
|
id='count_block'
|
||||||
|
title='Блоки'
|
||||||
|
icon={<IconConceptBlock size='1.25rem' className='text-primary' />}
|
||||||
|
value={stats.count_block}
|
||||||
|
/>
|
||||||
<ValueStats
|
<ValueStats
|
||||||
id='count_inputs'
|
id='count_inputs'
|
||||||
title='Загрузка'
|
title='Загрузка'
|
||||||
icon={<IconDownload size='1.25rem' />}
|
icon={<IconDownload size='1.25rem' className='text-primary' />}
|
||||||
value={stats.count_inputs}
|
value={stats.count_inputs}
|
||||||
/>
|
/>
|
||||||
<ValueStats
|
<ValueStats
|
||||||
id='count_synthesis'
|
id='count_synthesis'
|
||||||
title='Синтез'
|
title='Синтез'
|
||||||
icon={<IconSynthesis size='1.25rem' />}
|
icon={<IconSynthesis size='1.25rem' className='text-primary' />}
|
||||||
value={stats.count_synthesis}
|
value={stats.count_synthesis}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ValueStats
|
<ValueStats
|
||||||
id='count_schemas'
|
id='count_schemas'
|
||||||
title='Прикрепленные схемы'
|
title='Прикрепленные схемы'
|
||||||
icon={<IconRSForm size='1.25rem' />}
|
icon={<IconRSForm size='1.25rem' className='text-primary' />}
|
||||||
value={stats.count_schemas}
|
value={stats.count_schemas}
|
||||||
/>
|
/>
|
||||||
<ValueStats
|
<ValueStats
|
||||||
id='count_owned'
|
id='count_owned'
|
||||||
title='Собственные'
|
title='Собственные'
|
||||||
icon={<IconRSFormOwned size='1.25rem' />}
|
icon={<IconRSFormOwned size='1.25rem' className='text-primary' />}
|
||||||
value={stats.count_owned}
|
value={stats.count_owned}
|
||||||
/>
|
/>
|
||||||
<ValueStats
|
<ValueStats
|
||||||
id='count_imported'
|
id='count_imported'
|
||||||
title='Внешние'
|
title='Внешние'
|
||||||
icon={<IconRSFormImported size='1.25rem' />}
|
icon={<IconRSFormImported size='1.25rem' className='text-primary' />}
|
||||||
value={stats.count_schemas - stats.count_owned}
|
value={stats.count_schemas - stats.count_owned}
|
||||||
/>
|
/>
|
||||||
</aside>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -19,13 +19,18 @@ export function useContextMenu() {
|
||||||
const setHoverOperation = useOperationTooltipStore(state => state.setHoverItem);
|
const setHoverOperation = useOperationTooltipStore(state => state.setHoverItem);
|
||||||
const { addSelectedNodes } = useStoreApi().getState();
|
const { addSelectedNodes } = useStoreApi().getState();
|
||||||
|
|
||||||
function openContextMenu(node: OssNode, clientX: number, clientY: number) {
|
function handleContextMenu(event: React.MouseEvent<Element>, node: OssNode) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
addSelectedNodes([node.id]);
|
addSelectedNodes([node.id]);
|
||||||
|
|
||||||
setMenuProps({
|
setMenuProps({
|
||||||
item: node.type === 'block' ? node.data.block ?? null : node.data.operation ?? null,
|
item: node.type === 'block' ? node.data.block ?? null : node.data.operation ?? null,
|
||||||
cursorX: clientX,
|
cursorX: event.clientX,
|
||||||
cursorY: clientY
|
cursorY: event.clientY
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
setHoverOperation(null);
|
setHoverOperation(null);
|
||||||
}
|
}
|
||||||
|
@ -37,7 +42,7 @@ export function useContextMenu() {
|
||||||
return {
|
return {
|
||||||
isOpen,
|
isOpen,
|
||||||
menuProps,
|
menuProps,
|
||||||
openContextMenu,
|
handleContextMenu,
|
||||||
hideContextMenu
|
hideContextMenu
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,9 @@ export function NodeCore({ node }: NodeCoreProps) {
|
||||||
'relative flex items-center justify-center p-[2px]',
|
'relative flex items-center justify-center p-[2px]',
|
||||||
isChild && 'border-accent-orange'
|
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]'>
|
<div className='absolute z-pop top-0 right-0 flex flex-col gap-[4px] p-[2px]'>
|
||||||
<Indicator
|
<Indicator
|
||||||
|
@ -76,12 +79,9 @@ export function NodeCore({ node }: NodeCoreProps) {
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'text-center line-clamp-2 px-[4px] mr-[12px]',
|
'text-center line-clamp-2 pl-[4px]',
|
||||||
longLabel ? 'text-[12px]/[16px]' : 'text-[14px]/[20px]'
|
longLabel ? 'text-[12px]/[16px] pr-[10px]' : 'text-[14px]/[20px] pr-[4px]'
|
||||||
)}
|
)}
|
||||||
data-tooltip-id={globalIDs.operation_tooltip}
|
|
||||||
data-tooltip-hidden={node.dragging}
|
|
||||||
onMouseEnter={() => setHover(node.data.operation)}
|
|
||||||
>
|
>
|
||||||
{node.data.label}
|
{node.data.label}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,7 +6,6 @@ import clsx from 'clsx';
|
||||||
import { DiagramFlow, useReactFlow, useStoreApi } from '@/components/flow/diagram-flow';
|
import { DiagramFlow, useReactFlow, useStoreApi } from '@/components/flow/diagram-flow';
|
||||||
import { useMainHeight } from '@/stores/app-layout';
|
import { useMainHeight } from '@/stores/app-layout';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
import { promptText } from '@/utils/labels';
|
import { promptText } from '@/utils/labels';
|
||||||
|
|
||||||
|
@ -24,7 +23,6 @@ import { useContextMenu } from './context-menu/use-context-menu';
|
||||||
import { OssNodeTypes } from './graph/oss-node-types';
|
import { OssNodeTypes } from './graph/oss-node-types';
|
||||||
import { CoordinateDisplay } from './coordinate-display';
|
import { CoordinateDisplay } from './coordinate-display';
|
||||||
import { useOssFlow } from './oss-flow-context';
|
import { useOssFlow } from './oss-flow-context';
|
||||||
import { SidePanel } from './side-panel';
|
|
||||||
import { ToolbarOssGraph } from './toolbar-oss-graph';
|
import { ToolbarOssGraph } from './toolbar-oss-graph';
|
||||||
import { useDragging } from './use-dragging';
|
import { useDragging } from './use-dragging';
|
||||||
import { useGetLayout } from './use-get-layout';
|
import { useGetLayout } from './use-get-layout';
|
||||||
|
@ -54,7 +52,6 @@ export function OssFlow() {
|
||||||
|
|
||||||
const showGrid = useOSSGraphStore(state => state.showGrid);
|
const showGrid = useOSSGraphStore(state => state.showGrid);
|
||||||
const showCoordinates = useOSSGraphStore(state => state.showCoordinates);
|
const showCoordinates = useOSSGraphStore(state => state.showCoordinates);
|
||||||
const showPanel = usePreferencesStore(state => state.showOssSidePanel);
|
|
||||||
|
|
||||||
const getLayout = useGetLayout();
|
const getLayout = useGetLayout();
|
||||||
const { updateLayout } = useUpdateLayout();
|
const { updateLayout } = useUpdateLayout();
|
||||||
|
@ -67,7 +64,7 @@ export function OssFlow() {
|
||||||
const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation);
|
const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation);
|
||||||
const showEditBlock = useDialogsStore(state => state.showEditBlock);
|
const showEditBlock = useDialogsStore(state => state.showEditBlock);
|
||||||
|
|
||||||
const { isOpen: isContextMenuOpen, menuProps, openContextMenu, hideContextMenu } = useContextMenu();
|
const { isOpen: isContextMenuOpen, menuProps, handleContextMenu, hideContextMenu } = useContextMenu();
|
||||||
const { handleDragStart, handleDrag, handleDragStop } = useDragging({ hideContextMenu });
|
const { handleDragStart, handleDrag, handleDragStop } = useDragging({ hideContextMenu });
|
||||||
|
|
||||||
function handleSavePositions() {
|
function handleSavePositions() {
|
||||||
|
@ -138,8 +135,8 @@ export function OssFlow() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (node.data.operation) {
|
if (node.data.operation?.result) {
|
||||||
navigateOperationSchema(node.data.operation.id);
|
navigateOperationSchema(Number(node.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -186,29 +183,19 @@ export function OssFlow() {
|
||||||
setMouseCoords(targetPosition);
|
setMouseCoords(targetPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleNodeContextMenu(event: React.MouseEvent<Element>, node: OssNode) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
openContextMenu(node, event.clientX, event.clientY);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div tabIndex={-1} className='relative' onMouseMove={showCoordinates ? handleMouseMove : undefined}>
|
<div tabIndex={-1} className='relative' onMouseMove={showCoordinates ? handleMouseMove : undefined}>
|
||||||
{showCoordinates ? <CoordinateDisplay mouseCoords={mouseCoords} className='absolute top-1 right-2' /> : null}
|
{showCoordinates ? <CoordinateDisplay mouseCoords={mouseCoords} className='absolute top-1 right-2' /> : null}
|
||||||
|
|
||||||
<ContextMenu isOpen={isContextMenuOpen} onHide={hideContextMenu} {...menuProps} />
|
|
||||||
|
|
||||||
<ToolbarOssGraph
|
<ToolbarOssGraph
|
||||||
className='absolute z-pop top-8 right-1/2 translate-x-1/2'
|
className='absolute z-pop top-8 right-1/2 translate-x-1/2'
|
||||||
onCreateOperation={handleCreateOperation}
|
onCreateOperation={handleCreateOperation}
|
||||||
onCreateBlock={handleCreateBlock}
|
onCreateBlock={handleCreateBlock}
|
||||||
onDelete={handleDeleteSelected}
|
onDelete={handleDeleteSelected}
|
||||||
onResetPositions={resetGraph}
|
onResetPositions={resetGraph}
|
||||||
openContextMenu={openContextMenu}
|
|
||||||
isContextMenuOpen={isContextMenuOpen}
|
|
||||||
hideContextMenu={hideContextMenu}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ContextMenu isOpen={isContextMenuOpen} onHide={hideContextMenu} {...menuProps} />
|
||||||
|
|
||||||
<DiagramFlow
|
<DiagramFlow
|
||||||
{...flowOptions}
|
{...flowOptions}
|
||||||
className={clsx(!containMovement && 'cursor-relocate')}
|
className={clsx(!containMovement && 'cursor-relocate')}
|
||||||
|
@ -222,22 +209,12 @@ export function OssFlow() {
|
||||||
showGrid={showGrid}
|
showGrid={showGrid}
|
||||||
onClick={hideContextMenu}
|
onClick={hideContextMenu}
|
||||||
onNodeDoubleClick={handleNodeDoubleClick}
|
onNodeDoubleClick={handleNodeDoubleClick}
|
||||||
onNodeContextMenu={handleNodeContextMenu}
|
onNodeContextMenu={handleContextMenu}
|
||||||
onContextMenu={hideContextMenu}
|
onContextMenu={hideContextMenu}
|
||||||
onNodeDragStart={handleDragStart}
|
onNodeDragStart={handleDragStart}
|
||||||
onNodeDrag={handleDrag}
|
onNodeDrag={handleDrag}
|
||||||
onNodeDragStop={handleDragStop}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
export { SidePanel } from './side-panel';
|
|
|
@ -1,72 +0,0 @@
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,195 +0,0 @@
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
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';
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import { HelpTopic } from '@/features/help';
|
import { HelpTopic } from '@/features/help';
|
||||||
import { BadgeHelp } from '@/features/help/components/badge-help';
|
import { BadgeHelp } from '@/features/help/components';
|
||||||
import { IconShowSidebar } from '@/features/library/components/icon-show-sidebar';
|
|
||||||
import { type OssNode } from '@/features/oss/models/oss-layout';
|
|
||||||
|
|
||||||
import { MiniButton } from '@/components/control';
|
import { MiniButton } from '@/components/control';
|
||||||
import {
|
import {
|
||||||
IconConceptBlock,
|
IconConceptBlock,
|
||||||
IconDestroy,
|
IconDestroy,
|
||||||
IconEdit2,
|
IconEdit2,
|
||||||
|
IconExecute,
|
||||||
IconFitImage,
|
IconFitImage,
|
||||||
|
IconFixLayout,
|
||||||
IconNewItem,
|
IconNewItem,
|
||||||
IconReset,
|
IconReset,
|
||||||
IconSave,
|
IconSave,
|
||||||
|
@ -21,12 +21,14 @@ import {
|
||||||
import { type Styling } from '@/components/props';
|
import { type Styling } from '@/components/props';
|
||||||
import { cn } from '@/components/utils';
|
import { cn } from '@/components/utils';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { prepareTooltip } from '@/utils/utils';
|
||||||
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 { useMutatingOss } from '../../../backend/use-mutating-oss';
|
||||||
import { useUpdateLayout } from '../../../backend/use-update-layout';
|
import { useUpdateLayout } from '../../../backend/use-update-layout';
|
||||||
import { NodeType } from '../../../models/oss';
|
import { NodeType } from '../../../models/oss';
|
||||||
|
import { LayoutManager } from '../../../models/oss-layout-api';
|
||||||
import { useOssEdit } from '../oss-edit-context';
|
import { useOssEdit } from '../oss-edit-context';
|
||||||
|
|
||||||
import { useOssFlow } from './oss-flow-context';
|
import { useOssFlow } from './oss-flow-context';
|
||||||
|
@ -37,10 +39,6 @@ interface ToolbarOssGraphProps extends Styling {
|
||||||
onCreateBlock: () => void;
|
onCreateBlock: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onResetPositions: () => void;
|
onResetPositions: () => void;
|
||||||
|
|
||||||
isContextMenuOpen: boolean;
|
|
||||||
openContextMenu: (node: OssNode, clientX: number, clientY: number) => void;
|
|
||||||
hideContextMenu: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolbarOssGraph({
|
export function ToolbarOssGraph({
|
||||||
|
@ -48,16 +46,12 @@ export function ToolbarOssGraph({
|
||||||
onCreateBlock,
|
onCreateBlock,
|
||||||
onDelete,
|
onDelete,
|
||||||
onResetPositions,
|
onResetPositions,
|
||||||
|
|
||||||
isContextMenuOpen,
|
|
||||||
openContextMenu,
|
|
||||||
hideContextMenu,
|
|
||||||
className,
|
className,
|
||||||
...restProps
|
...restProps
|
||||||
}: ToolbarOssGraphProps) {
|
}: ToolbarOssGraphProps) {
|
||||||
const { schema, selectedItems, isMutable, canDeleteOperation: canDelete } = useOssEdit();
|
const { schema, selectedItems, isMutable, canDeleteOperation: canDelete } = useOssEdit();
|
||||||
const isProcessing = useMutatingOss();
|
const isProcessing = useMutatingOss();
|
||||||
const { resetView, nodes } = useOssFlow();
|
const { resetView } = useOssFlow();
|
||||||
const selectedOperation =
|
const selectedOperation =
|
||||||
selectedItems.length === 1 && selectedItems[0].nodeType === NodeType.OPERATION ? selectedItems[0] : null;
|
selectedItems.length === 1 && selectedItems[0].nodeType === NodeType.OPERATION ? selectedItems[0] : null;
|
||||||
const selectedBlock =
|
const selectedBlock =
|
||||||
|
@ -65,31 +59,67 @@ export function ToolbarOssGraph({
|
||||||
const getLayout = useGetLayout();
|
const getLayout = useGetLayout();
|
||||||
|
|
||||||
const { updateLayout } = useUpdateLayout();
|
const { updateLayout } = useUpdateLayout();
|
||||||
|
const { executeOperation } = useExecuteOperation();
|
||||||
|
|
||||||
const showOptions = useDialogsStore(state => state.showOssOptions);
|
const showEditOperation = useDialogsStore(state => state.showEditOperation);
|
||||||
const showSidePanel = usePreferencesStore(state => state.showOssSidePanel);
|
const showEditBlock = useDialogsStore(state => state.showEditBlock);
|
||||||
const toggleShowSidePanel = usePreferencesStore(state => state.toggleShowOssSidePanel);
|
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('Еще не реализовано');
|
||||||
|
}
|
||||||
|
|
||||||
function handleShowOptions() {
|
function handleShowOptions() {
|
||||||
showOptions();
|
showOssOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSavePositions() {
|
function handleSavePositions() {
|
||||||
void updateLayout({ itemID: schema.id, data: getLayout() });
|
void updateLayout({ itemID: schema.id, data: getLayout() });
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEditItem(event: React.MouseEvent<HTMLButtonElement>) {
|
function handleOperationExecute() {
|
||||||
if (isContextMenuOpen) {
|
if (!readyForSynthesis || !selectedOperation) {
|
||||||
hideContextMenu();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const nodeID = selectedOperation?.nodeID ?? selectedBlock?.nodeID;
|
void executeOperation({
|
||||||
if (!nodeID) {
|
itemID: schema.id, //
|
||||||
return;
|
data: { target: selectedOperation.id, layout: getLayout() }
|
||||||
}
|
});
|
||||||
const node = nodes.find(node => node.id === nodeID);
|
}
|
||||||
if (node) {
|
|
||||||
openContextMenu(node, event.clientX, event.clientY);
|
function handleEditItem() {
|
||||||
|
if (selectedOperation) {
|
||||||
|
showEditOperation({
|
||||||
|
manager: new LayoutManager(schema, getLayout()),
|
||||||
|
target: selectedOperation
|
||||||
|
});
|
||||||
|
} else if (selectedBlock) {
|
||||||
|
showEditBlock({
|
||||||
|
manager: new LayoutManager(schema, getLayout()),
|
||||||
|
target: selectedBlock
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,9 +145,12 @@ export function ToolbarOssGraph({
|
||||||
onClick={resetView}
|
onClick={resetView}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Панель содержания КС'
|
title='Исправить позиции узлов'
|
||||||
icon={<IconShowSidebar value={showSidePanel} isBottom={false} size='1.25rem' />}
|
icon={<IconFixLayout size='1.25rem' className='icon-primary' />}
|
||||||
onClick={toggleShowSidePanel}
|
onClick={handleFixLayout}
|
||||||
|
disabled={
|
||||||
|
selectedItems.length > 1 || (selectedItems.length > 0 && selectedItems[0].nodeType === NodeType.OPERATION)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Настройки отображения'
|
title='Настройки отображения'
|
||||||
|
@ -136,12 +169,11 @@ export function ToolbarOssGraph({
|
||||||
disabled={isProcessing}
|
disabled={isProcessing}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Редактировать выбранную', isIOS() ? '' : 'Правый клик')}
|
titleHtml={prepareTooltip('Новый блок', 'Ctrl + Shift + Q')}
|
||||||
hideTitle={isContextMenuOpen}
|
aria-label='Новый блок'
|
||||||
aria-label='Редактировать выбранную'
|
icon={<IconConceptBlock size='1.25rem' className='icon-green' />}
|
||||||
icon={<IconEdit2 size='1.25rem' className='icon-primary' />}
|
onClick={onCreateBlock}
|
||||||
onClick={handleEditItem}
|
disabled={isProcessing}
|
||||||
disabled={selectedItems.length !== 1 || isProcessing}
|
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Новая операция', 'Ctrl + Q')}
|
titleHtml={prepareTooltip('Новая операция', 'Ctrl + Q')}
|
||||||
|
@ -151,13 +183,18 @@ export function ToolbarOssGraph({
|
||||||
disabled={isProcessing}
|
disabled={isProcessing}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Новый блок', 'Ctrl + Shift + Q')}
|
title='Активировать операцию'
|
||||||
aria-label='Новый блок'
|
icon={<IconExecute size='1.25rem' className='icon-green' />}
|
||||||
icon={<IconConceptBlock size='1.25rem' className='icon-green' />}
|
onClick={handleOperationExecute}
|
||||||
onClick={onCreateBlock}
|
disabled={isProcessing || selectedItems.length !== 1 || !readyForSynthesis}
|
||||||
disabled={isProcessing}
|
/>
|
||||||
|
<MiniButton
|
||||||
|
titleHtml={prepareTooltip('Редактировать выбранную', 'Двойной клик')}
|
||||||
|
aria-label='Редактировать выбранную'
|
||||||
|
icon={<IconEdit2 size='1.25rem' className='icon-primary' />}
|
||||||
|
onClick={handleEditItem}
|
||||||
|
disabled={selectedItems.length !== 1 || isProcessing}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Удалить выбранную', 'Delete')}
|
titleHtml={prepareTooltip('Удалить выбранную', 'Delete')}
|
||||||
aria-label='Удалить выбранную'
|
aria-label='Удалить выбранную'
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { type Node, useReactFlow } from 'reactflow';
|
||||||
import { type IOssLayout } from '../../../backend/types';
|
import { type IOssLayout } from '../../../backend/types';
|
||||||
import { type IOperationSchema } from '../../../models/oss';
|
import { type IOperationSchema } from '../../../models/oss';
|
||||||
import { type Position2D } from '../../../models/oss-layout';
|
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 { useOssEdit } from '../oss-edit-context';
|
||||||
|
|
||||||
import { BLOCK_NODE_MIN_HEIGHT, BLOCK_NODE_MIN_WIDTH } from './graph/block-node';
|
import { BLOCK_NODE_MIN_HEIGHT, BLOCK_NODE_MIN_WIDTH } from './graph/block-node';
|
||||||
|
@ -15,24 +14,22 @@ export function useGetLayout() {
|
||||||
return function getLayout(): IOssLayout {
|
return function getLayout(): IOssLayout {
|
||||||
const nodes = getNodes();
|
const nodes = getNodes();
|
||||||
const nodeById = new Map(nodes.map(node => [node.id, node]));
|
const nodeById = new Map(nodes.map(node => [node.id, node]));
|
||||||
return [
|
return {
|
||||||
...nodes
|
operations: nodes
|
||||||
.filter(node => node.type !== 'block')
|
.filter(node => node.type !== 'block')
|
||||||
.map(node => ({
|
.map(node => ({
|
||||||
nodeID: node.id,
|
id: schema.itemByNodeID.get(node.id)!.id,
|
||||||
...computeAbsolutePosition(node, schema, nodeById),
|
...computeAbsolutePosition(node, schema, nodeById)
|
||||||
width: OPERATION_NODE_WIDTH,
|
|
||||||
height: OPERATION_NODE_HEIGHT
|
|
||||||
})),
|
})),
|
||||||
...nodes
|
blocks: nodes
|
||||||
.filter(node => node.type === 'block')
|
.filter(node => node.type === 'block')
|
||||||
.map(node => ({
|
.map(node => ({
|
||||||
nodeID: node.id,
|
id: schema.itemByNodeID.get(node.id)!.id,
|
||||||
...computeAbsolutePosition(node, schema, nodeById),
|
...computeAbsolutePosition(node, schema, nodeById),
|
||||||
width: node.width ?? BLOCK_NODE_MIN_WIDTH,
|
width: node.width ?? BLOCK_NODE_MIN_WIDTH,
|
||||||
height: node.height ?? BLOCK_NODE_MIN_HEIGHT
|
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