Compare commits

..

No commits in common. "0bdfe67fb13a379d16f13e7a8b9376132b2838ac" and "e8509e44b1635d35d16f533d94a041ca22057bdb" have entirely different histories.

225 changed files with 3307 additions and 5128 deletions

4
.vscode/launch.json vendored
View File

@ -42,8 +42,8 @@
"type": "debugpy",
"request": "launch",
"cwd": "${workspaceFolder}/rsconcept/backend",
"program": "${workspaceFolder}/rsconcept/backend/run_testfile.py",
"args": ["${file}"],
"program": "${workspaceFolder}/rsconcept/backend/manage.py",
"args": ["test", "-k", "${fileBasenameNoExtension}"],
"django": true
},
{

View File

@ -21,9 +21,6 @@
"changeProcessCWD": true
}
],
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
],
"stylelint.enable": true,
"autopep8.args": [
"--max-line-length",
@ -82,7 +79,6 @@
"Certbot",
"CIHT",
"clsx",
"cmdk",
"codemirror",
"Constituenta",
"corsheaders",
@ -165,7 +161,6 @@
"rstabs",
"rstemplates",
"setexpr",
"shadcn",
"SIDELIST",
"signup",
"simplebezier",

View File

@ -35,6 +35,7 @@ This readme file is used mostly to document project dependencies and conventions
- react-toastify
- react-tabs
- react-intl
- react-select
- react-error-boundary
- react-tooltip
- react-zoom-pan-pinch
@ -69,7 +70,6 @@ This readme file is used mostly to document project dependencies and conventions
- babel-plugin-react-compiler
- vite
- jest
- shadcn
- ts-jest
- stylelint
- stylelint-config-recommended

View File

@ -31,9 +31,11 @@ User profile:
- Sitemap for better SEO and crawler optimization
[Functionality - CANCELED]
- User notifications on edit - consider spam prevention and change aggregation
- Integrate socials and feedback
- Content based search in Library
- Private projects. Consider cooperative editing
- OSS: synthesis table: auto substitution for diamond synthesis
[Tech]

View File

@ -9,7 +9,6 @@ from apps.library.models import (
LibraryTemplate,
LocationHead
)
from apps.oss.models import OperationSchema
from apps.rsform.models import RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint
from shared.testing_utils import response_contains
@ -59,8 +58,6 @@ class TestLibraryViewset(EndpointTester):
'read_only': True
}
response = self.executeCreated(data=data)
oss = OperationSchema(LibraryItem.objects.get(pk=response.data['id']))
self.assertEqual(oss.model.owner, self.user)
self.assertEqual(response.data['owner'], self.user.pk)
self.assertEqual(response.data['item_type'], data['item_type'])
self.assertEqual(response.data['title'], data['title'])
@ -68,8 +65,6 @@ class TestLibraryViewset(EndpointTester):
self.assertEqual(response.data['access_policy'], data['access_policy'])
self.assertEqual(response.data['visible'], data['visible'])
self.assertEqual(response.data['read_only'], data['read_only'])
self.assertEqual(oss.layout().data['operations'], [])
self.assertEqual(oss.layout().data['blocks'], [])
self.logout()
data = {'title': 'Title2'}
@ -364,7 +359,7 @@ class TestLibraryViewset(EndpointTester):
data = {'title': 'Title1340', 'items': []}
response = self.executeCreated(data=data, item=self.owned.pk)
self.assertEqual(response.data['title'], data['title'])
self.assertEqual(len(response.data['items']), 2)
self.assertEqual(len(response.data['items']), 0)
data = {'title': 'Title1341', 'items': [x12.pk]}
response = self.executeCreated(data=data, item=self.owned.pk)

View File

@ -13,7 +13,7 @@ from rest_framework.decorators import action
from rest_framework.request import Request
from rest_framework.response import Response
from apps.oss.models import Layout, Operation, OperationSchema, PropagationFacade
from apps.oss.models import Operation, OperationSchema, PropagationFacade
from apps.rsform.models import RSForm
from apps.rsform.serializers import RSFormParseSerializer
from apps.users.models import User
@ -40,8 +40,6 @@ class LibraryViewSet(viewsets.ModelViewSet):
serializer.save(owner=self.request.user)
else:
serializer.save()
if serializer.data.get('item_type') == m.LibraryItemType.OPERATION_SCHEMA:
Layout.objects.create(oss=serializer.instance, data={'operations': [], 'blocks': []})
def perform_update(self, serializer) -> None:
instance = serializer.save()

View File

@ -15,24 +15,11 @@ class OperationAdmin(admin.ModelAdmin):
'alias',
'title',
'description',
'parent']
'position_x',
'position_y']
search_fields = ['id', 'operation_type', 'title', 'alias']
class BlockAdmin(admin.ModelAdmin):
''' Admin model: Block. '''
ordering = ['oss']
list_display = ['id', 'oss', 'title', 'description', 'parent']
search_fields = ['oss']
class LayoutAdmin(admin.ModelAdmin):
''' Admin model: Layout. '''
ordering = ['oss']
list_display = ['id', 'oss', 'data']
search_fields = ['oss']
class ArgumentAdmin(admin.ModelAdmin):
''' Admin model: Operation arguments. '''
ordering = ['operation']
@ -55,8 +42,6 @@ class InheritanceAdmin(admin.ModelAdmin):
admin.site.register(models.Operation, OperationAdmin)
admin.site.register(models.Block, BlockAdmin)
admin.site.register(models.Layout, LayoutAdmin)
admin.site.register(models.Argument, ArgumentAdmin)
admin.site.register(models.Substitution, SynthesisSubstitutionAdmin)
admin.site.register(models.Inheritance, InheritanceAdmin)

View File

@ -1,84 +0,0 @@
# Generated by Django 5.1.7 on 2025-03-26 16:04
import django.db.models.deletion
from django.db import migrations, models
def migrate_layout(apps, schema_editor):
LibraryItem = apps.get_model('library', 'LibraryItem')
Operation = apps.get_model('oss', 'Operation')
Layout = apps.get_model('oss', 'Layout')
for library_item in LibraryItem.objects.filter(item_type='oss'):
layout_data = {'operations': [], 'blocks': []}
operations = Operation.objects.filter(oss=library_item)
for operation in operations:
layout_data['operations'].append({
'id': operation.id,
'x': operation.position_x,
'y': operation.position_y
})
Layout.objects.create(oss=library_item, data=layout_data)
class Migration(migrations.Migration):
dependencies = [
('library', '0007_rename_libraryitem_comment_libraryitem_description'),
('oss', '0010_rename_comment_operation_description'),
]
operations = [
migrations.CreateModel(
name='Layout',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('data', models.JSONField(default=dict, verbose_name='Расположение')),
('oss', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
related_name='layout', to='library.libraryitem', verbose_name='Схема синтеза')),
],
options={
'verbose_name': 'Схема расположения',
'verbose_name_plural': 'Схемы расположения',
},
),
migrations.RunPython(migrate_layout),
migrations.RemoveField(
model_name='operation',
name='position_x',
),
migrations.RemoveField(
model_name='operation',
name='position_y',
),
migrations.CreateModel(
name='Block',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.TextField(blank=True, verbose_name='Название')),
('description', models.TextField(blank=True, verbose_name='Описание')),
('oss', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
related_name='blocks', to='library.libraryitem', verbose_name='Схема синтеза')),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL,
related_name='as_child_block', to='oss.block', verbose_name='Содержащий блок')),
],
options={
'verbose_name': 'Блок',
'verbose_name_plural': 'Блоки',
},
),
migrations.AddField(
model_name='operation',
name='parent',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='as_child_operation',
to='oss.block',
verbose_name='Содержащий блок'),
),
]

View File

@ -1,39 +0,0 @@
''' Models: Content Block in OSS. '''
# pylint: disable=duplicate-code
from django.db.models import CASCADE, SET_NULL, ForeignKey, Model, TextField
class Block(Model):
''' Block of content in OSS.'''
oss = ForeignKey(
verbose_name='Схема синтеза',
to='library.LibraryItem',
on_delete=CASCADE,
related_name='blocks'
)
title = TextField(
verbose_name='Название',
blank=True
)
description = TextField(
verbose_name='Описание',
blank=True
)
parent = ForeignKey(
verbose_name='Содержащий блок',
to='oss.Block',
blank=True,
null=True,
on_delete=SET_NULL,
related_name='as_child_block'
)
class Meta:
''' Model metadata. '''
verbose_name = 'Блок'
verbose_name_plural = 'Блоки'
def __str__(self) -> str:
return f'Блок {self.title}'

View File

@ -1,25 +0,0 @@
''' Models: Content Block in OSS. '''
from django.db.models import CASCADE, ForeignKey, JSONField, Model
class Layout(Model):
''' Node layout in OSS.'''
oss = ForeignKey(
verbose_name='Схема синтеза',
to='library.LibraryItem',
on_delete=CASCADE,
related_name='layout'
)
data = JSONField(
verbose_name='Расположение',
default=dict
)
class Meta:
''' Model metadata. '''
verbose_name = 'Схема расположения'
verbose_name_plural = 'Схемы расположения'
def __str__(self) -> str:
return f'Схема расположения {self.oss.alias}'

View File

@ -1,9 +1,9 @@
''' Models: Operation in OSS. '''
# pylint: disable=duplicate-code
from django.db.models import (
CASCADE,
SET_NULL,
CharField,
FloatField,
ForeignKey,
Model,
QuerySet,
@ -44,15 +44,6 @@ class Operation(Model):
related_name='producer'
)
parent = ForeignKey(
verbose_name='Содержащий блок',
to='oss.Block',
blank=True,
null=True,
on_delete=SET_NULL,
related_name='as_child_operation'
)
alias = CharField(
verbose_name='Шифр',
max_length=255,
@ -67,6 +58,15 @@ class Operation(Model):
blank=True
)
position_x = FloatField(
verbose_name='Положение по горизонтали',
default=0
)
position_y = FloatField(
verbose_name='Положение по вертикали',
default=0
)
class Meta:
''' Model metadata. '''
verbose_name = 'Операция'

View File

@ -19,9 +19,7 @@ from apps.rsform.models import (
)
from .Argument import Argument
from .Block import Block
from .Inheritance import Inheritance
from .Layout import Layout
from .Operation import Operation
from .Substitution import Substitution
@ -40,7 +38,6 @@ class OperationSchema:
def create(**kwargs) -> 'OperationSchema':
''' Create LibraryItem via OperationSchema. '''
model = LibraryItem.objects.create(item_type=LibraryItemType.OPERATION_SCHEMA, **kwargs)
Layout.objects.create(oss=model, data={'operations': [], 'blocks': []})
return OperationSchema(model)
@staticmethod
@ -61,20 +58,10 @@ class OperationSchema:
''' Get QuerySet containing all operations of current OSS. '''
return Operation.objects.filter(oss=self.model)
def blocks(self) -> QuerySet[Block]:
''' Get QuerySet containing all blocks of current OSS. '''
return Block.objects.filter(oss=self.model)
def arguments(self) -> QuerySet[Argument]:
''' Operation arguments. '''
return Argument.objects.filter(operation__oss=self.model)
def layout(self) -> Layout:
''' OSS layout. '''
result = Layout.objects.filter(oss=self.model).first()
assert result is not None
return result
def substitutions(self) -> QuerySet[Substitution]:
''' Operation substitutions. '''
return Substitution.objects.filter(operation__oss=self.model)
@ -91,11 +78,15 @@ class OperationSchema:
location=self.model.location
)
def update_layout(self, data: dict) -> None:
def update_positions(self, data: list[dict]) -> None:
''' Update positions. '''
layout = self.layout()
layout.data = data
layout.save()
lookup = {x['id']: x for x in data}
operations = self.operations()
for item in operations:
if item.pk in lookup:
item.position_x = lookup[item.pk]['position_x']
item.position_y = lookup[item.pk]['position_y']
Operation.objects.bulk_update(operations, ['position_x', 'position_y'])
def create_operation(self, **kwargs) -> Operation:
''' Insert new operation. '''
@ -104,12 +95,6 @@ class OperationSchema:
self.save(update_fields=['time_update'])
return result
def create_block(self, **kwargs) -> Block:
''' Insert new block. '''
result = Block.objects.create(oss=self.model, **kwargs)
self.save(update_fields=['time_update'])
return result
def delete_operation(self, target: int, keep_constituents: bool = False):
''' Delete operation. '''
self.cache.ensure_loaded()

View File

@ -1,9 +1,7 @@
''' Django: Models. '''
from .Argument import Argument
from .Block import Block
from .Inheritance import Inheritance
from .Layout import Layout
from .Operation import Operation, OperationType
from .OperationSchema import OperationSchema
from .PropagationFacade import PropagationFacade

View File

@ -1,10 +1,8 @@
''' REST API: Serializers. '''
from .basics import LayoutSerializer, SubstitutionExSerializer
from .basics import OperationPositionSerializer, PositionsSerializer, SubstitutionExSerializer
from .data_access import (
ArgumentSerializer,
BlockCreateSerializer,
BlockSerializer,
OperationCreateSerializer,
OperationDeleteSerializer,
OperationSchemaSerializer,
@ -14,9 +12,4 @@ from .data_access import (
RelocateConstituentsSerializer,
SetOperationInputSerializer
)
from .responses import (
ConstituentaReferenceResponse,
NewBlockResponse,
NewOperationResponse,
NewSchemaResponse
)
from .responses import ConstituentaReferenceResponse, NewOperationResponse, NewSchemaResponse

View File

@ -2,29 +2,17 @@
from rest_framework import serializers
class OperationNodeSerializer(serializers.Serializer):
class OperationPositionSerializer(serializers.Serializer):
''' Operation position. '''
id = serializers.IntegerField()
x = serializers.FloatField()
y = serializers.FloatField()
position_x = serializers.FloatField()
position_y = serializers.FloatField()
class BlockNodeSerializer(serializers.Serializer):
''' Block position. '''
id = serializers.IntegerField()
x = serializers.FloatField()
y = serializers.FloatField()
width = serializers.FloatField()
height = serializers.FloatField()
class LayoutSerializer(serializers.Serializer):
''' Layout for OperationSchema. '''
blocks = serializers.ListField(
child=BlockNodeSerializer()
)
operations = serializers.ListField(
child=OperationNodeSerializer()
class PositionsSerializer(serializers.Serializer):
''' Operations position for OperationSchema. '''
positions = serializers.ListField(
child=OperationPositionSerializer()
)

View File

@ -11,8 +11,8 @@ from apps.rsform.models import Constituenta
from apps.rsform.serializers import SubstitutionSerializerBase
from shared import messages as msg
from ..models import Argument, Block, Inheritance, Operation, OperationSchema, OperationType
from .basics import LayoutSerializer, SubstitutionExSerializer
from ..models import Argument, Inheritance, Operation, OperationSchema, OperationType
from .basics import OperationPositionSerializer, SubstitutionExSerializer
class OperationSerializer(serializers.ModelSerializer):
@ -24,15 +24,6 @@ class OperationSerializer(serializers.ModelSerializer):
read_only_fields = ('id', 'oss')
class BlockSerializer(serializers.ModelSerializer):
''' Serializer: Block data. '''
class Meta:
''' serializer metadata. '''
model = Block
fields = '__all__'
read_only_fields = ('id', 'oss')
class ArgumentSerializer(serializers.ModelSerializer):
''' Serializer: Operation data. '''
class Meta:
@ -41,49 +32,6 @@ class ArgumentSerializer(serializers.ModelSerializer):
fields = ('operation', 'argument')
class BlockCreateSerializer(serializers.Serializer):
''' Serializer: Block creation. '''
class BlockCreateData(serializers.ModelSerializer):
''' Serializer: Block creation data. '''
class Meta:
''' serializer metadata. '''
model = Block
fields = 'title', 'description', 'parent'
layout = LayoutSerializer()
item_data = BlockCreateData()
width = serializers.FloatField()
height = serializers.FloatField()
position_x = serializers.FloatField()
position_y = serializers.FloatField()
children_operations = PKField(many=True, queryset=Operation.objects.all().only('oss_id'))
children_blocks = PKField(many=True, queryset=Block.objects.all().only('oss_id'))
def validate(self, attrs):
oss = cast(LibraryItem, self.context['oss'])
if 'parent' in attrs['item_data'] and \
attrs['item_data']['parent'] is not None and \
attrs['item_data']['parent'].oss_id != oss.pk:
raise serializers.ValidationError({
'parent': msg.parentNotInOSS()
})
for operation in attrs['children_operations']:
if operation.oss_id != oss.pk:
raise serializers.ValidationError({
'children_operations': msg.childNotInOSS()
})
for block in attrs['children_blocks']:
if block.oss_id != oss.pk:
raise serializers.ValidationError({
'children_blocks': msg.childNotInOSS()
})
return attrs
class OperationCreateSerializer(serializers.Serializer):
''' Serializer: Operation creation. '''
class OperationCreateData(serializers.ModelSerializer):
@ -96,33 +44,16 @@ class OperationCreateSerializer(serializers.Serializer):
model = Operation
fields = \
'alias', 'operation_type', 'title', \
'description', 'result', 'parent'
'description', 'result', 'position_x', 'position_y'
layout = LayoutSerializer()
item_data = OperationCreateData()
position_x = serializers.FloatField()
position_y = serializers.FloatField()
create_schema = serializers.BooleanField(default=False, required=False)
item_data = OperationCreateData()
arguments = PKField(many=True, queryset=Operation.objects.all().only('pk'), required=False)
def validate(self, attrs):
oss = cast(LibraryItem, self.context['oss'])
if 'parent' in attrs['item_data'] and \
attrs['item_data']['parent'] is not None and \
attrs['item_data']['parent'].oss_id != oss.pk:
raise serializers.ValidationError({
'parent': msg.parentNotInOSS()
})
if 'arguments' not in attrs:
return attrs
for operation in attrs['arguments']:
if operation.oss_id != oss.pk:
raise serializers.ValidationError({
'arguments': msg.operationNotInOSS(oss.title)
})
return attrs
positions = serializers.ListField(
child=OperationPositionSerializer(),
default=[]
)
class OperationUpdateSerializer(serializers.Serializer):
@ -134,7 +65,6 @@ class OperationUpdateSerializer(serializers.Serializer):
model = Operation
fields = 'alias', 'title', 'description'
layout = LayoutSerializer()
target = PKField(many=False, queryset=Operation.objects.all())
item_data = OperationUpdateData()
arguments = PKField(many=True, queryset=Operation.objects.all().only('oss_id', 'result_id'), required=False)
@ -143,20 +73,16 @@ class OperationUpdateSerializer(serializers.Serializer):
required=False
)
def validate(self, attrs):
oss = cast(LibraryItem, self.context['oss'])
if 'parent' in attrs['item_data'] and attrs['item_data']['parent'].oss_id != oss.pk:
raise serializers.ValidationError({
'parent': msg.parentNotInOSS()
})
positions = serializers.ListField(
child=OperationPositionSerializer(),
default=[]
)
def validate(self, attrs):
if 'arguments' not in attrs:
if 'substitutions' in attrs:
raise serializers.ValidationError({
'arguments': msg.missingArguments()
})
return attrs
oss = cast(LibraryItem, self.context['oss'])
for operation in attrs['arguments']:
if operation.oss_id != oss.pk:
raise serializers.ValidationError({
@ -194,8 +120,11 @@ class OperationUpdateSerializer(serializers.Serializer):
class OperationTargetSerializer(serializers.Serializer):
''' Serializer: Target single operation. '''
layout = LayoutSerializer()
target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'result_id'))
positions = serializers.ListField(
child=OperationPositionSerializer(),
default=[]
)
def validate(self, attrs):
oss = cast(LibraryItem, self.context['oss'])
@ -209,8 +138,11 @@ class OperationTargetSerializer(serializers.Serializer):
class OperationDeleteSerializer(serializers.Serializer):
''' Serializer: Delete operation. '''
layout = LayoutSerializer()
target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'result'))
positions = serializers.ListField(
child=OperationPositionSerializer(),
default=[]
)
keep_constituents = serializers.BooleanField(default=False, required=False)
delete_schema = serializers.BooleanField(default=False, required=False)
@ -226,7 +158,6 @@ class OperationDeleteSerializer(serializers.Serializer):
class SetOperationInputSerializer(serializers.Serializer):
''' Serializer: Set input schema for operation. '''
layout = LayoutSerializer()
target = PKField(many=False, queryset=Operation.objects.all())
input = PKField(
many=False,
@ -234,6 +165,10 @@ class SetOperationInputSerializer(serializers.Serializer):
allow_null=True,
default=None
)
positions = serializers.ListField(
child=OperationPositionSerializer(),
default=[]
)
def validate(self, attrs):
oss = cast(LibraryItem, self.context['oss'])
@ -251,19 +186,15 @@ class SetOperationInputSerializer(serializers.Serializer):
class OperationSchemaSerializer(serializers.ModelSerializer):
''' Serializer: Detailed data for OSS. '''
operations = serializers.ListField(
items = serializers.ListField(
child=OperationSerializer()
)
blocks = serializers.ListField(
child=BlockSerializer()
)
arguments = serializers.ListField(
child=ArgumentSerializer()
)
substitutions = serializers.ListField(
child=SubstitutionExSerializer()
)
layout = LayoutSerializer()
class Meta:
''' serializer metadata. '''
@ -274,17 +205,13 @@ class OperationSchemaSerializer(serializers.ModelSerializer):
result = LibraryItemDetailsSerializer(instance).data
del result['versions']
oss = OperationSchema(instance)
result['layout'] = oss.layout().data
result['operations'] = []
result['blocks'] = []
result['arguments'] = []
result['substitutions'] = []
result['items'] = []
for operation in oss.operations().order_by('pk'):
result['operations'].append(OperationSerializer(operation).data)
for block in oss.blocks().order_by('pk'):
result['blocks'].append(BlockSerializer(block).data)
result['items'].append(OperationSerializer(operation).data)
result['arguments'] = []
for argument in oss.arguments().order_by('order'):
result['arguments'].append(ArgumentSerializer(argument).data)
result['substitutions'] = []
for substitution in oss.substitutions().values(
'operation',
'original',

View File

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

View File

@ -8,7 +8,6 @@ from apps.oss.models import Argument, Operation, OperationSchema, OperationType
class TestArgument(TestCase):
''' Testing Argument model. '''
def setUp(self):
self.oss = OperationSchema.create(alias='T1')

View File

@ -8,7 +8,6 @@ from apps.rsform.models import Constituenta, RSForm
class TestInheritance(TestCase):
''' Testing Inheritance model. '''
def setUp(self):
self.oss = OperationSchema.create(alias='T1')

View File

@ -9,7 +9,6 @@ from apps.rsform.models import RSForm
class TestOperation(TestCase):
''' Testing Operation model. '''
def setUp(self):
self.oss = OperationSchema.create(alias='T1')
self.operation = Operation.objects.create(
@ -26,8 +25,9 @@ class TestOperation(TestCase):
def test_create_default(self):
self.assertEqual(self.operation.oss, self.oss.model)
self.assertEqual(self.operation.operation_type, OperationType.INPUT)
self.assertEqual(self.operation.parent, None)
self.assertEqual(self.operation.result, None)
self.assertEqual(self.operation.alias, 'KS1')
self.assertEqual(self.operation.title, '')
self.assertEqual(self.operation.description, '')
self.assertEqual(self.operation.position_x, 0)
self.assertEqual(self.operation.position_y, 0)

View File

@ -10,7 +10,6 @@ from apps.rsform.models import RSForm
class TestSynthesisSubstitution(TestCase):
''' Testing Synthesis Substitution model. '''
def setUp(self):
self.oss = OperationSchema.create(alias='T1')

View File

@ -9,7 +9,6 @@ from shared.EndpointTester import EndpointTester, decl_endpoint
class TestChangeAttributes(EndpointTester):
''' Testing LibraryItem view when OSS is associated with RSForms. '''
def setUp(self):
super().setUp()
self.user3 = User.objects.create(
@ -59,19 +58,6 @@ class TestChangeAttributes(EndpointTester):
self.operation3.refresh_from_db()
self.ks3 = RSForm(self.operation3.result)
self.layout_data = {
'operations': [
{'id': self.operation1.pk, 'x': 0, 'y': 0},
{'id': self.operation2.pk, 'x': 0, 'y': 0},
{'id': self.operation3.pk, 'x': 0, 'y': 0},
],
'blocks': []
}
layout = self.owned.layout()
layout.data = self.layout_data
layout.save()
@decl_endpoint('/api/library/{item}/set-owner', method='patch')
def test_set_owner(self):
data = {'user': self.user3.pk}
@ -87,7 +73,6 @@ class TestChangeAttributes(EndpointTester):
self.assertEqual(self.ks2.model.owner, self.user2)
self.assertEqual(self.ks3.model.owner, self.user3)
@decl_endpoint('/api/library/{item}/set-location', method='patch')
def test_set_location(self):
data = {'location': '/U/temp'}
@ -103,7 +88,6 @@ class TestChangeAttributes(EndpointTester):
self.assertNotEqual(self.ks2.model.location, data['location'])
self.assertEqual(self.ks3.model.location, data['location'])
@decl_endpoint('/api/library/{item}/set-access-policy', method='patch')
def test_set_access_policy(self):
data = {'access_policy': AccessPolicy.PROTECTED}
@ -119,7 +103,6 @@ class TestChangeAttributes(EndpointTester):
self.assertNotEqual(self.ks2.model.access_policy, data['access_policy'])
self.assertEqual(self.ks3.model.access_policy, data['access_policy'])
@decl_endpoint('/api/library/{item}/set-editors', method='patch')
def test_set_editors(self):
Editor.set(self.owned.model.pk, [self.user2.pk])
@ -138,7 +121,6 @@ class TestChangeAttributes(EndpointTester):
self.assertEqual(list(self.ks2.model.getQ_editors()), [])
self.assertEqual(set(self.ks3.model.getQ_editors()), set([self.user, self.user3]))
@decl_endpoint('/api/library/{item}', method='patch')
def test_sync_from_result(self):
data = {'alias': 'KS111', 'title': 'New Title', 'description': 'New description'}
@ -151,7 +133,6 @@ class TestChangeAttributes(EndpointTester):
self.assertEqual(self.operation1.title, data['title'])
self.assertEqual(self.operation1.description, data['description'])
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_sync_from_operation(self):
data = {
@ -161,7 +142,7 @@ class TestChangeAttributes(EndpointTester):
'title': 'Test title mod',
'description': 'Comment mod'
},
'layout': self.layout_data
'positions': [],
}
response = self.executeOK(data=data, item=self.owned_id)
@ -170,7 +151,6 @@ class TestChangeAttributes(EndpointTester):
self.assertEqual(self.ks3.model.title, data['item_data']['title'])
self.assertEqual(self.ks3.model.description, data['item_data']['description'])
@decl_endpoint('/api/library/{item}', method='delete')
def test_destroy_oss_consequence(self):
response = self.executeNoContent(item=self.owned_id)

View File

@ -57,19 +57,6 @@ class TestChangeConstituents(EndpointTester):
self.ks3 = RSForm(self.operation3.result)
self.assertEqual(self.ks3.constituents().count(), 4)
self.layout_data = {
'operations': [
{'id': self.operation1.pk, 'x': 0, 'y': 0},
{'id': self.operation2.pk, 'x': 0, 'y': 0},
{'id': self.operation3.pk, 'x': 0, 'y': 0},
],
'blocks': []
}
layout = self.owned.layout()
layout.data = self.layout_data
layout.save()
@decl_endpoint('/api/rsforms/{item}/details', method='get')
def test_retrieve_inheritance(self):
response = self.executeOK(item=self.ks3.model.pk)
@ -97,7 +84,6 @@ class TestChangeConstituents(EndpointTester):
},
])
@decl_endpoint('/api/rsforms/{schema}/create-cst', method='post')
def test_create_constituenta(self):
data = {
@ -114,7 +100,6 @@ class TestChangeConstituents(EndpointTester):
self.assertEqual(inherited_cst.order, 2)
self.assertEqual(inherited_cst.definition_formal, 'X1 = X2')
@decl_endpoint('/api/rsforms/{schema}/rename-cst', method='patch')
def test_rename_constituenta(self):
data = {'target': self.ks1X1.pk, 'alias': 'D21', 'cst_type': CstType.TERM}
@ -126,7 +111,6 @@ class TestChangeConstituents(EndpointTester):
self.assertEqual(inherited_cst.alias, 'D2')
self.assertEqual(inherited_cst.cst_type, data['cst_type'])
@decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch')
def test_update_constituenta(self):
d2 = self.ks3.insert_new('D2', cst_type=CstType.TERM, definition_raw='@{X1|sing,nomn}')
@ -153,7 +137,6 @@ class TestChangeConstituents(EndpointTester):
self.assertEqual(inherited_cst.definition_formal, r'X1\X1')
self.assertEqual(inherited_cst.definition_raw, r'@{X2|sing,datv}')
@decl_endpoint('/api/rsforms/{schema}/delete-multiple-cst', method='patch')
def test_delete_constituenta(self):
data = {'items': [self.ks2X1.pk]}
@ -165,7 +148,6 @@ class TestChangeConstituents(EndpointTester):
self.assertEqual(self.ks2D1.definition_formal, r'DEL\DEL')
self.assertEqual(inherited_cst.definition_formal, r'DEL\DEL')
@decl_endpoint('/api/rsforms/{schema}/substitute', method='patch')
def test_substitute(self):
d2 = self.ks3.insert_new('D2', cst_type=CstType.TERM, definition_formal=r'X1\X2\X3')

View File

@ -8,7 +8,6 @@ from shared.EndpointTester import EndpointTester, decl_endpoint
class TestChangeOperations(EndpointTester):
''' Testing Operations change propagation in OSS. '''
def setUp(self):
super().setUp()
self.owned = OperationSchema.create(
@ -107,21 +106,6 @@ class TestChangeOperations(EndpointTester):
convention='KS5D4'
)
self.layout_data = {
'operations': [
{'id': self.operation1.pk, 'x': 0, 'y': 0},
{'id': self.operation2.pk, 'x': 0, 'y': 0},
{'id': self.operation3.pk, 'x': 0, 'y': 0},
{'id': self.operation4.pk, 'x': 0, 'y': 0},
{'id': self.operation5.pk, 'x': 0, 'y': 0},
],
'blocks': []
}
layout = self.owned.layout()
layout.data = self.layout_data
layout.save()
def test_oss_setup(self):
self.assertEqual(self.ks1.constituents().count(), 3)
self.assertEqual(self.ks2.constituents().count(), 3)
@ -130,11 +114,10 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(self.ks5.constituents().count(), 8)
self.assertEqual(self.ks4D1.definition_formal, 'S1 X1')
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_input_operation(self):
data = {
'layout': self.layout_data,
'positions': [],
'target': self.operation2.pk
}
self.executeOK(data=data, item=self.owned_id)
@ -151,11 +134,10 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(self.ks4D2.definition_formal, r'X1 DEL DEL DEL D1')
self.assertEqual(self.ks5D4.definition_formal, r'DEL DEL X3 DEL D1 D2 D3')
@decl_endpoint('/api/oss/{item}/set-input', method='patch')
def test_set_input_null(self):
data = {
'layout': self.layout_data,
'positions': [],
'target': self.operation2.pk,
'input': None
}
@ -175,7 +157,6 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(self.ks4D2.definition_formal, r'X1 DEL DEL DEL D1')
self.assertEqual(self.ks5D4.definition_formal, r'DEL DEL X3 DEL D1 D2 D3')
@decl_endpoint('/api/oss/{item}/set-input', method='patch')
def test_set_input_change_schema(self):
ks6 = RSForm.create(
@ -188,7 +169,7 @@ class TestChangeOperations(EndpointTester):
ks6D1 = ks6.insert_new('D1', definition_formal='X1 X2', convention='KS6D1')
data = {
'layout': self.layout_data,
'positions': [],
'target': self.operation2.pk,
'input': ks6.model.pk
}
@ -211,7 +192,6 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(self.ks4D2.definition_formal, r'X1 DEL DEL DEL D1')
self.assertEqual(self.ks5D4.definition_formal, r'DEL DEL X3 DEL D1 D2 D3')
@decl_endpoint('/api/library/{item}', method='delete')
def test_delete_schema(self):
self.executeNoContent(item=self.ks1.model.pk)
@ -228,11 +208,10 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(self.ks4D2.definition_formal, r'DEL X2 X3 S1 DEL')
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 DEL D2 D3')
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_operation_and_constituents(self):
data = {
'layout': self.layout_data,
'positions': [],
'target': self.operation1.pk,
'keep_constituents': False,
'delete_schema': True
@ -250,11 +229,10 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(self.ks4D2.definition_formal, r'DEL X2 X3 S1 DEL')
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 DEL D2 D3')
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_operation_keep_constituents(self):
data = {
'layout': self.layout_data,
'positions': [],
'target': self.operation1.pk,
'keep_constituents': True,
'delete_schema': True
@ -272,11 +250,10 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(self.ks4D2.definition_formal, r'X1 X2 X3 S1 D1')
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 D1 D2 D3')
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_operation_keep_schema(self):
data = {
'layout': self.layout_data,
'positions': [],
'target': self.operation1.pk,
'keep_constituents': True,
'delete_schema': False
@ -297,7 +274,6 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 D1 D2 D3')
self.assertFalse(Constituenta.objects.filter(as_child__parent_id=self.ks1D1.pk).exists())
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_change_substitutions(self):
data = {
@ -307,8 +283,7 @@ class TestChangeOperations(EndpointTester):
'title': 'Test title mod',
'description': 'Comment mod'
},
'layout': self.layout_data,
'arguments': [self.operation1.pk, self.operation2.pk],
'positions': [],
'substitutions': [
{
'original': self.ks1X1.pk,
@ -333,7 +308,6 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(self.ks4D2.definition_formal, r'X1 D1 X3 S1 D1')
self.assertEqual(self.ks5D4.definition_formal, r'D1 X2 X3 S1 D1 D2 D3')
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_change_arguments(self):
data = {
@ -343,7 +317,7 @@ class TestChangeOperations(EndpointTester):
'title': 'Test title mod',
'description': 'Comment mod'
},
'layout': self.layout_data,
'positions': [],
'arguments': [self.operation1.pk],
}
@ -372,7 +346,6 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(self.ks4D2.definition_formal, r'X1 DEL DEL DEL D1')
self.assertEqual(self.ks5D4.definition_formal, r'DEL DEL X3 DEL D1 D2 D3')
@decl_endpoint('/api/oss/{item}/execute-operation', method='post')
def test_execute_middle_operation(self):
self.client.delete(f'/api/library/{self.ks4.model.pk}')
@ -383,7 +356,7 @@ class TestChangeOperations(EndpointTester):
data = {
'target': self.operation4.pk,
'layout': self.layout_data
'positions': []
}
self.executeOK(data=data, item=self.owned_id)
self.operation4.refresh_from_db()
@ -391,7 +364,6 @@ class TestChangeOperations(EndpointTester):
self.assertNotEqual(self.operation4.result, None)
self.assertEqual(self.ks5.constituents().count(), 8)
@decl_endpoint('/api/oss/relocate-constituents', method='post')
def test_relocate_constituents_up(self):
ks1_old_count = self.ks1.constituents().count()
@ -421,7 +393,6 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(self.ks1.constituents().count(), ks1_old_count + 1)
self.assertEqual(self.ks4.constituents().count(), ks4_old_count + 1)
@decl_endpoint('/api/oss/relocate-constituents', method='post')
def test_relocate_constituents_down(self):
ks1_old_count = self.ks1.constituents().count()

View File

@ -8,7 +8,6 @@ from shared.EndpointTester import EndpointTester, decl_endpoint
class TestChangeSubstitutions(EndpointTester):
''' Testing Substitutions change propagation in OSS. '''
def setUp(self):
super().setUp()
self.owned = OperationSchema.create(
@ -107,20 +106,6 @@ class TestChangeSubstitutions(EndpointTester):
convention='KS5D4'
)
self.layout_data = {
'operations': [
{'id': self.operation1.pk, 'x': 0, 'y': 0},
{'id': self.operation2.pk, 'x': 0, 'y': 0},
{'id': self.operation3.pk, 'x': 0, 'y': 0},
{'id': self.operation4.pk, 'x': 0, 'y': 0},
{'id': self.operation5.pk, 'x': 0, 'y': 0},
],
'blocks': []
}
layout = self.owned.layout()
layout.data = self.layout_data
layout.save()
def test_oss_setup(self):
self.assertEqual(self.ks1.constituents().count(), 3)
@ -130,7 +115,6 @@ class TestChangeSubstitutions(EndpointTester):
self.assertEqual(self.ks5.constituents().count(), 8)
self.assertEqual(self.ks4D1.definition_formal, 'S1 X1')
@decl_endpoint('/api/rsforms/{schema}/substitute', method='patch')
def test_substitute_original(self):
data = {'substitutions': [{
@ -153,15 +137,12 @@ class TestChangeSubstitutions(EndpointTester):
self.assertEqual(self.ks4D2.definition_formal, r'S1 X2 X3 S1 D1')
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 X3 D1 D2 D3')
@decl_endpoint('/api/rsforms/{schema}/substitute', method='patch')
def test_substitute_substitution(self):
data = {
'substitutions': [{
'original': self.ks2S1.pk,
'substitution': self.ks2X1.pk
}]
}
data = {'substitutions': [{
'original': self.ks2S1.pk,
'substitution': self.ks2X1.pk
}]}
self.executeOK(data=data, schema=self.ks2.model.pk)
self.ks4D1.refresh_from_db()
self.ks4D2.refresh_from_db()
@ -178,7 +159,6 @@ class TestChangeSubstitutions(EndpointTester):
self.assertEqual(self.ks4D2.definition_formal, r'X1 X2 X3 X2 D1')
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 X1 D1 D2 D3')
@decl_endpoint('/api/rsforms/{schema}/delete-multiple-cst', method='patch')
def test_delete_original(self):
data = {'items': [self.ks1X1.pk, self.ks1D1.pk]}
@ -193,7 +173,6 @@ class TestChangeSubstitutions(EndpointTester):
self.assertEqual(self.ks4D2.definition_formal, r'X1 X2 X3 S1 DEL')
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 DEL D2 D3')
@decl_endpoint('/api/rsforms/{schema}/delete-multiple-cst', method='patch')
def test_delete_substitution(self):
data = {'items': [self.ks2S1.pk, self.ks2X2.pk]}

View File

@ -1,4 +1,2 @@
''' Tests for REST API. '''
from .t_blocks import *
from .t_operations import *
from .t_oss import *

View File

@ -1,167 +0,0 @@
''' Testing API: Operation Schema. '''
from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType
from apps.oss.models import Operation, OperationSchema, OperationType
from apps.rsform.models import Constituenta, RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint
class TestOssBlocks(EndpointTester):
''' Testing OSS view - operations. '''
def setUp(self):
super().setUp()
self.owned = OperationSchema.create(title='Test', alias='T1', owner=self.user)
self.owned_id = self.owned.model.pk
self.unowned = OperationSchema.create(title='Test2', alias='T2')
self.unowned_id = self.unowned.model.pk
self.invalid_id = self.unowned_id + 1337
def populateData(self):
self.unowned.create_block()
self.unowned.create_block()
self.unowned.create_block()
self.unowned.create_block()
self.block1 = self.owned.create_block(
title='1',
)
self.operation1 = self.owned.create_operation(
alias='1',
operation_type=OperationType.INPUT,
parent=self.block1,
)
self.operation2 = self.owned.create_operation(
alias='2',
operation_type=OperationType.INPUT,
)
self.operation3 = self.unowned.create_operation(
alias='3',
operation_type=OperationType.INPUT
)
self.block2 = self.owned.create_block(
title='2',
parent=self.block1
)
self.block3 = self.unowned.create_block(
title='3',
parent=self.block1
)
self.layout_data = {
'operations': [
{'id': self.operation1.pk, 'x': 0, 'y': 0},
{'id': self.operation2.pk, 'x': 0, 'y': 0},
],
'blocks': [
{'id': self.block1.pk, 'x': 0, 'y': 0, 'width': 0.5, 'height': 0.5},
{'id': self.block2.pk, 'x': 0, 'y': 0, 'width': 0.5, 'height': 0.5},
]
}
layout = self.owned.layout()
layout.data = self.layout_data
layout.save()
@decl_endpoint('/api/oss/{item}/create-block', method='post')
def test_create_block(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'item_data': {
'title': 'Test title',
'description': 'Тест кириллицы',
},
'layout': self.layout_data,
'position_x': 1337,
'position_y': 1337,
'width': 0.42,
'height': 0.42,
'children_operations': [],
'children_blocks': []
}
self.executeNotFound(data=data, item=self.invalid_id)
response = self.executeCreated(data=data, item=self.owned_id)
self.assertEqual(len(response.data['oss']['blocks']), 3)
new_block = response.data['new_block']
layout = response.data['oss']['layout']
item = [item for item in layout['blocks'] if item['id'] == new_block['id']][0]
self.assertEqual(new_block['title'], data['item_data']['title'])
self.assertEqual(new_block['description'], data['item_data']['description'])
self.assertEqual(new_block['parent'], None)
self.assertEqual(item['x'], data['position_x'])
self.assertEqual(item['y'], data['position_y'])
self.assertEqual(item['width'], data['width'])
self.assertEqual(item['height'], data['height'])
self.operation1.refresh_from_db()
self.executeForbidden(data=data, item=self.unowned_id)
self.toggle_admin(True)
self.executeCreated(data=data, item=self.unowned_id)
@decl_endpoint('/api/oss/{item}/create-block', method='post')
def test_create_block_parent(self):
self.populateData()
data = {
'item_data': {
'title': 'Test title',
'description': 'Тест кириллицы',
'parent': self.invalid_id
},
'layout': self.layout_data,
'position_x': 1337,
'position_y': 1337,
'width': 0.42,
'height': 0.42,
'children_operations': [],
'children_blocks': []
}
self.executeBadData(data=data, item=self.owned_id)
data['item_data']['parent'] = self.block3.pk
self.executeBadData(data=data)
data['item_data']['parent'] = self.block1.pk
response = self.executeCreated(data=data)
new_block = response.data['new_block']
self.assertEqual(new_block['parent'], self.block1.pk)
@decl_endpoint('/api/oss/{item}/create-block', method='post')
def test_create_block_children(self):
self.populateData()
data = {
'item_data': {
'title': 'Test title',
'description': 'Тест кириллицы',
},
'layout': self.layout_data,
'position_x': 1337,
'position_y': 1337,
'width': 0.42,
'height': 0.42,
'children_operations': [self.invalid_id],
'children_blocks': []
}
self.executeBadData(data=data, item=self.owned_id)
data['children_operations'] = [self.operation3.pk]
self.executeBadData(data=data)
data['children_operations'] = [self.block1.pk]
self.executeBadData(data=data)
data['children_operations'] = [self.operation1.pk]
data['children_blocks'] = [self.operation1.pk]
self.executeBadData(data=data)
data['children_blocks'] = [self.block1.pk]
response = self.executeCreated(data=data)
new_block = response.data['new_block']
self.operation1.refresh_from_db()
self.block1.refresh_from_db()
self.assertEqual(self.operation1.parent.pk, new_block['id'])
self.assertEqual(self.block1.parent.pk, new_block['id'])

View File

@ -1,488 +0,0 @@
''' Testing API: Operation Schema. '''
from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType
from apps.oss.models import Operation, OperationSchema, OperationType
from apps.rsform.models import Constituenta, RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint
class TestOssOperations(EndpointTester):
''' Testing OSS view - operations. '''
def setUp(self):
super().setUp()
self.owned = OperationSchema.create(title='Test', alias='T1', owner=self.user)
self.owned_id = self.owned.model.pk
self.unowned = OperationSchema.create(title='Test2', alias='T2')
self.unowned_id = self.unowned.model.pk
self.invalid_id = self.unowned_id + 1337
def populateData(self):
self.ks1 = RSForm.create(
alias='KS1',
title='Test1',
owner=self.user
)
self.ks1X1 = self.ks1.insert_new(
'X1',
term_raw='X1_1',
term_resolved='X1_1'
)
self.ks2 = RSForm.create(
alias='KS2',
title='Test2',
owner=self.user
)
self.ks2X1 = self.ks2.insert_new(
'X2',
term_raw='X1_2',
term_resolved='X1_2'
)
self.operation1 = self.owned.create_operation(
alias='1',
operation_type=OperationType.INPUT,
result=self.ks1.model
)
self.operation2 = self.owned.create_operation(
alias='2',
operation_type=OperationType.INPUT,
result=self.ks2.model
)
self.operation3 = self.owned.create_operation(
alias='3',
operation_type=OperationType.SYNTHESIS
)
self.layout_data = {
'operations': [
{'id': self.operation1.pk, 'x': 0, 'y': 0},
{'id': self.operation2.pk, 'x': 0, 'y': 0},
{'id': self.operation3.pk, 'x': 0, 'y': 0},
],
'blocks': []
}
layout = self.owned.layout()
layout.data = self.layout_data
layout.save()
self.owned.set_arguments(self.operation3.pk, [self.operation1, self.operation2])
self.owned.set_substitutions(self.operation3.pk, [{
'original': self.ks1X1,
'substitution': self.ks2X1
}])
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
def test_create_operation(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'item_data': {
'alias': 'Test3',
'title': 'Test title',
'description': 'Тест кириллицы',
},
'layout': self.layout_data,
'position_x': 1,
'position_y': 1
}
self.executeBadData(data=data)
data['item_data']['operation_type'] = 'invalid'
self.executeBadData(data=data)
data['item_data']['operation_type'] = OperationType.INPUT
self.executeNotFound(data=data, item=self.invalid_id)
response = self.executeCreated(data=data, item=self.owned_id)
self.assertEqual(len(response.data['oss']['operations']), 4)
new_operation = response.data['new_operation']
layout = response.data['oss']['layout']
item = [item for item in layout['operations'] if item['id'] == new_operation['id']][0]
self.assertEqual(new_operation['alias'], data['item_data']['alias'])
self.assertEqual(new_operation['operation_type'], data['item_data']['operation_type'])
self.assertEqual(new_operation['title'], data['item_data']['title'])
self.assertEqual(new_operation['description'], data['item_data']['description'])
self.assertEqual(new_operation['result'], None)
self.assertEqual(new_operation['parent'], None)
self.assertEqual(item['x'], data['position_x'])
self.assertEqual(item['y'], data['position_y'])
self.operation1.refresh_from_db()
self.executeForbidden(data=data, item=self.unowned_id)
self.toggle_admin(True)
self.executeCreated(data=data, item=self.unowned_id)
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
def test_create_operation_parent(self):
self.populateData()
data = {
'item_data': {
'parent': self.invalid_id,
'alias': 'Test3',
'title': 'Test title',
'description': '',
'operation_type': OperationType.INPUT
},
'layout': self.layout_data,
'position_x': 1,
'position_y': 1
}
self.executeBadData(data=data, item=self.owned_id)
block_unowned = self.unowned.create_block(title='TestBlock1')
data['item_data']['parent'] = block_unowned.id
self.executeBadData(data=data, item=self.owned_id)
block_owned = self.owned.create_block(title='TestBlock2')
data['item_data']['parent'] = block_owned.id
response = self.executeCreated(data=data, item=self.owned_id)
self.assertEqual(len(response.data['oss']['operations']), 4)
new_operation = response.data['new_operation']
self.assertEqual(new_operation['parent'], block_owned.id)
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
def test_create_operation_arguments(self):
self.populateData()
data = {
'item_data': {
'alias': 'Test4',
'operation_type': OperationType.SYNTHESIS
},
'layout': self.layout_data,
'position_x': 1,
'position_y': 1,
'arguments': [self.operation1.pk, self.operation3.pk]
}
response = self.executeCreated(data=data, item=self.owned_id)
self.owned.refresh_from_db()
new_operation = response.data['new_operation']
arguments = self.owned.arguments()
self.assertTrue(arguments.filter(operation__id=new_operation['id'], argument=self.operation1))
self.assertTrue(arguments.filter(operation__id=new_operation['id'], argument=self.operation3))
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
def test_create_operation_result(self):
self.populateData()
self.operation1.result = None
self.operation1.save()
data = {
'item_data': {
'alias': 'Test4',
'operation_type': OperationType.INPUT,
'result': self.ks1.model.pk
},
'layout': self.layout_data,
'position_x': 1,
'position_y': 1
}
response = self.executeCreated(data=data, item=self.owned_id)
new_operation = response.data['new_operation']
self.assertEqual(new_operation['result'], self.ks1.model.pk)
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
def test_create_operation_schema(self):
self.populateData()
Editor.add(self.owned.model.pk, self.user2.pk)
data = {
'item_data': {
'alias': 'Test4',
'title': 'Test title',
'description': 'Comment',
'operation_type': OperationType.INPUT,
'result': self.ks1.model.pk
},
'create_schema': True,
'layout': self.layout_data,
'position_x': 1,
'position_y': 1
}
self.executeBadData(data=data, item=self.owned_id)
data['item_data']['result'] = None
response = self.executeCreated(data=data, item=self.owned_id)
self.owned.refresh_from_db()
new_operation = response.data['new_operation']
schema = LibraryItem.objects.get(pk=new_operation['result'])
self.assertEqual(schema.alias, data['item_data']['alias'])
self.assertEqual(schema.title, data['item_data']['title'])
self.assertEqual(schema.description, data['item_data']['description'])
self.assertEqual(schema.visible, False)
self.assertEqual(schema.access_policy, self.owned.model.access_policy)
self.assertEqual(schema.location, self.owned.model.location)
self.assertIn(self.user2, schema.getQ_editors())
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_operation(self):
self.executeNotFound(item=self.invalid_id)
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'layout': self.layout_data
}
self.executeBadData(data=data)
data['target'] = self.operation1.pk
self.toggle_admin(True)
self.executeBadData(data=data, item=self.unowned_id)
self.logout()
self.executeForbidden(data=data, item=self.owned_id)
self.login()
response = self.executeOK(data=data)
layout = response.data['layout']
deleted_items = [item for item in layout['operations'] if item['id'] == data['target']]
self.assertEqual(len(response.data['operations']), 2)
self.assertEqual(len(deleted_items), 0)
@decl_endpoint('/api/oss/{item}/create-input', method='patch')
def test_create_input(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'layout': self.layout_data
}
self.executeBadData(data=data)
data['target'] = self.operation1.pk
self.toggle_admin(True)
self.executeBadData(data=data, item=self.unowned_id)
self.logout()
self.executeForbidden(data=data, item=self.owned_id)
self.login()
self.executeBadData(data=data, item=self.owned_id)
self.operation1.result = None
self.operation1.description = 'TestComment'
self.operation1.title = 'TestTitle'
self.operation1.save()
response = self.executeOK(data=data)
self.operation1.refresh_from_db()
new_schema = response.data['new_schema']
self.assertEqual(new_schema['id'], self.operation1.result.pk)
self.assertEqual(new_schema['alias'], self.operation1.alias)
self.assertEqual(new_schema['title'], self.operation1.title)
self.assertEqual(new_schema['description'], self.operation1.description)
data['target'] = self.operation3.pk
self.executeBadData(data=data)
@decl_endpoint('/api/oss/{item}/set-input', method='patch')
def test_set_input_null(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'layout': self.layout_data
}
self.executeBadData(data=data)
data['target'] = self.operation1.pk
data['input'] = None
self.toggle_admin(True)
self.executeBadData(data=data, item=self.unowned_id)
self.logout()
self.executeForbidden(data=data, item=self.owned_id)
self.login()
response = self.executeOK(data=data)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.result, None)
data['input'] = self.ks1.model.pk
self.ks1.model.alias = 'Test42'
self.ks1.model.title = 'Test421'
self.ks1.model.description = 'TestComment42'
self.ks1.save()
response = self.executeOK(data=data)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.result, self.ks1.model)
self.assertEqual(self.operation1.alias, self.ks1.model.alias)
self.assertEqual(self.operation1.title, self.ks1.model.title)
self.assertEqual(self.operation1.description, self.ks1.model.description)
@decl_endpoint('/api/oss/{item}/set-input', method='patch')
def test_set_input_change_schema(self):
self.populateData()
self.operation2.result = None
data = {
'layout': self.layout_data,
'target': self.operation1.pk,
'input': self.ks2.model.pk
}
self.executeBadData(data=data, item=self.owned_id)
self.ks2.model.visible = False
self.ks2.model.save(update_fields=['visible'])
data = {
'layout': self.layout_data,
'target': self.operation2.pk,
'input': None
}
self.executeOK(data=data, item=self.owned_id)
self.operation2.refresh_from_db()
self.ks2.model.refresh_from_db()
self.assertEqual(self.operation2.result, None)
self.assertEqual(self.ks2.model.visible, True)
data = {
'layout': self.layout_data,
'target': self.operation1.pk,
'input': self.ks2.model.pk
}
self.executeOK(data=data, item=self.owned_id)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.result, self.ks2.model)
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_update_operation(self):
self.populateData()
self.executeBadData(item=self.owned_id)
ks3 = RSForm.create(alias='KS3', title='Test3', owner=self.user)
ks3x1 = ks3.insert_new('X1', term_resolved='X1_1')
data = {
'target': self.operation3.pk,
'item_data': {
'alias': 'Test3 mod',
'title': 'Test title mod',
'description': 'Comment mod'
},
'layout': self.layout_data,
'arguments': [self.operation2.pk, self.operation1.pk],
'substitutions': [
{
'original': self.ks1X1.pk,
'substitution': ks3x1.pk
}
]
}
self.executeBadData(data=data)
data['substitutions'][0]['substitution'] = self.ks2X1.pk
self.toggle_admin(True)
self.executeBadData(data=data, item=self.unowned_id)
self.logout()
self.executeForbidden(data=data, item=self.owned_id)
self.login()
response = self.executeOK(data=data)
self.operation3.refresh_from_db()
self.assertEqual(self.operation3.alias, data['item_data']['alias'])
self.assertEqual(self.operation3.title, data['item_data']['title'])
self.assertEqual(self.operation3.description, data['item_data']['description'])
args = self.operation3.getQ_arguments().order_by('order')
self.assertEqual(args[0].argument.pk, data['arguments'][0])
self.assertEqual(args[0].order, 0)
self.assertEqual(args[1].argument.pk, data['arguments'][1])
self.assertEqual(args[1].order, 1)
sub = self.operation3.getQ_substitutions()[0]
self.assertEqual(sub.original.pk, data['substitutions'][0]['original'])
self.assertEqual(sub.substitution.pk, data['substitutions'][0]['substitution'])
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_update_operation_sync(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'target': self.operation1.pk,
'item_data': {
'alias': 'Test3 mod',
'title': 'Test title mod',
'description': 'Comment mod'
},
'layout': self.layout_data
}
response = self.executeOK(data=data)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.alias, data['item_data']['alias'])
self.assertEqual(self.operation1.title, data['item_data']['title'])
self.assertEqual(self.operation1.description, data['item_data']['description'])
self.assertEqual(self.operation1.result.alias, data['item_data']['alias'])
self.assertEqual(self.operation1.result.title, data['item_data']['title'])
self.assertEqual(self.operation1.result.description, data['item_data']['description'])
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_update_operation_invalid_substitution(self):
self.populateData()
self.ks1X2 = self.ks1.insert_new('X2')
data = {
'target': self.operation3.pk,
'item_data': {
'alias': 'Test3 mod',
'title': 'Test title mod',
'description': 'Comment mod'
},
'layout': self.layout_data,
'arguments': [self.operation1.pk, self.operation2.pk],
'substitutions': [
{
'original': self.ks1X1.pk,
'substitution': self.ks2X1.pk
},
{
'original': self.ks2X1.pk,
'substitution': self.ks1X2.pk
}
]
}
self.executeBadData(data=data, item=self.owned_id)
@decl_endpoint('/api/oss/{item}/execute-operation', method='post')
def test_execute_operation(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'layout': self.layout_data,
'target': self.operation1.pk
}
self.executeBadData(data=data)
data['target'] = self.operation3.pk
self.toggle_admin(True)
self.executeBadData(data=data, item=self.unowned_id)
self.logout()
self.executeForbidden(data=data, item=self.owned_id)
self.login()
self.executeOK(data=data)
self.operation3.refresh_from_db()
schema = self.operation3.result
self.assertEqual(schema.alias, self.operation3.alias)
self.assertEqual(schema.description, self.operation3.description)
self.assertEqual(schema.title, self.operation3.title)
self.assertEqual(schema.visible, False)
items = list(RSForm(schema).constituents())
self.assertEqual(len(items), 1)
self.assertEqual(items[0].alias, 'X1')
self.assertEqual(items[0].term_resolved, self.ks2X1.term_resolved)

View File

@ -1,6 +1,6 @@
''' Testing API: Operation Schema. '''
from apps.library.models import AccessPolicy, LibraryItemType
from apps.oss.models import OperationSchema, OperationType
from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType
from apps.oss.models import Operation, OperationSchema, OperationType
from apps.rsform.models import Constituenta, RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint
@ -16,8 +16,7 @@ class TestOssViewset(EndpointTester):
self.unowned_id = self.unowned.model.pk
self.private = OperationSchema.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE)
self.private_id = self.private.model.pk
self.invalid_id = self.private_id + 1337
self.invalid_id = self.private.model.pk + 1337
def populateData(self):
self.ks1 = RSForm.create(
@ -55,21 +54,12 @@ class TestOssViewset(EndpointTester):
alias='3',
operation_type=OperationType.SYNTHESIS
)
layout = self.owned.layout()
layout.data = {'operations': [
{'id': self.operation1.pk, 'x': 0, 'y': 0},
{'id': self.operation2.pk, 'x': 0, 'y': 0},
{'id': self.operation3.pk, 'x': 0, 'y': 0},
], 'blocks': []}
layout.save()
self.owned.set_arguments(self.operation3.pk, [self.operation1, self.operation2])
self.owned.set_substitutions(self.operation3.pk, [{
'original': self.ks1X1,
'substitution': self.ks2X1
}])
@decl_endpoint('/api/oss/{item}/details', method='get')
def test_details(self):
self.populateData()
@ -84,9 +74,9 @@ class TestOssViewset(EndpointTester):
self.assertEqual(response.data['item_type'], LibraryItemType.OPERATION_SCHEMA)
self.assertEqual(len(response.data['operations']), 3)
self.assertEqual(response.data['operations'][0]['id'], self.operation1.pk)
self.assertEqual(response.data['operations'][0]['operation_type'], self.operation1.operation_type)
self.assertEqual(len(response.data['items']), 3)
self.assertEqual(response.data['items'][0]['id'], self.operation1.pk)
self.assertEqual(response.data['items'][0]['operation_type'], self.operation1.operation_type)
self.assertEqual(len(response.data['substitutions']), 1)
sub = response.data['substitutions'][0]
@ -105,12 +95,6 @@ class TestOssViewset(EndpointTester):
self.assertEqual(arguments[1]['operation'], self.operation3.pk)
self.assertEqual(arguments[1]['argument'], self.operation2.pk)
layout = response.data['layout']
self.assertEqual(layout['blocks'], [])
self.assertEqual(layout['operations'][0], {'id': self.operation1.pk, 'x': 0, 'y': 0})
self.assertEqual(layout['operations'][1], {'id': self.operation2.pk, 'x': 0, 'y': 0})
self.assertEqual(layout['operations'][2], {'id': self.operation3.pk, 'x': 0, 'y': 0})
self.executeOK(item=self.unowned_id)
self.executeForbidden(item=self.private_id)
@ -119,33 +103,400 @@ class TestOssViewset(EndpointTester):
self.executeOK(item=self.unowned_id)
self.executeForbidden(item=self.private_id)
@decl_endpoint('/api/oss/{item}/update-layout', method='patch')
def test_update_layout(self):
@decl_endpoint('/api/oss/{item}/update-positions', method='patch')
def test_update_positions(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {'operations': [], 'blocks': []}
data = {'positions': []}
self.executeOK(data=data)
data = {
'operations': [
{'id': self.operation1.pk, 'x': 42.1, 'y': 1337},
{'id': self.operation2.pk, 'x': 36.1, 'y': 1437},
{'id': self.operation3.pk, 'x': 36.1, 'y': 1435}
], 'blocks': []
}
data = {'positions': [
{'id': self.operation1.pk, 'position_x': 42.1, 'position_y': 1337},
{'id': self.operation2.pk, 'position_x': 36.1, 'position_y': 1437},
{'id': self.invalid_id, 'position_x': 31, 'position_y': 12},
]}
self.toggle_admin(True)
self.executeOK(data=data, item=self.unowned_id)
self.operation1.refresh_from_db()
self.assertNotEqual(self.operation1.position_x, data['positions'][0]['position_x'])
self.assertNotEqual(self.operation1.position_y, data['positions'][0]['position_y'])
self.toggle_admin(False)
self.executeOK(data=data, item=self.owned_id)
self.owned.refresh_from_db()
self.assertEqual(self.owned.layout().data, data)
self.operation1.refresh_from_db()
self.operation2.refresh_from_db()
self.assertEqual(self.operation1.position_x, data['positions'][0]['position_x'])
self.assertEqual(self.operation1.position_y, data['positions'][0]['position_y'])
self.assertEqual(self.operation2.position_x, data['positions'][1]['position_x'])
self.assertEqual(self.operation2.position_y, data['positions'][1]['position_y'])
self.executeForbidden(data=data, item=self.unowned_id)
self.executeForbidden(data=data, item=self.private_id)
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
def test_create_operation(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'item_data': {
'alias': 'Test3',
'title': 'Test title',
'description': 'Тест кириллицы',
'position_x': 1,
'position_y': 1,
},
'positions': [
{'id': self.operation1.pk, 'position_x': 42.1, 'position_y': 1337}
]
}
self.executeBadData(data=data)
data['item_data']['operation_type'] = 'invalid'
self.executeBadData(data=data)
data['item_data']['operation_type'] = OperationType.INPUT
self.executeNotFound(data=data, item=self.invalid_id)
response = self.executeCreated(data=data, item=self.owned_id)
self.assertEqual(len(response.data['oss']['items']), 4)
new_operation = response.data['new_operation']
self.assertEqual(new_operation['alias'], data['item_data']['alias'])
self.assertEqual(new_operation['operation_type'], data['item_data']['operation_type'])
self.assertEqual(new_operation['title'], data['item_data']['title'])
self.assertEqual(new_operation['description'], data['item_data']['description'])
self.assertEqual(new_operation['position_x'], data['item_data']['position_x'])
self.assertEqual(new_operation['position_y'], data['item_data']['position_y'])
self.assertEqual(new_operation['result'], None)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.position_x, data['positions'][0]['position_x'])
self.assertEqual(self.operation1.position_y, data['positions'][0]['position_y'])
self.executeForbidden(data=data, item=self.unowned_id)
self.toggle_admin(True)
self.executeCreated(data=data, item=self.unowned_id)
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
def test_create_operation_arguments(self):
self.populateData()
data = {
'item_data': {
'alias': 'Test4',
'operation_type': OperationType.SYNTHESIS
},
'positions': [],
'arguments': [self.operation1.pk, self.operation3.pk]
}
response = self.executeCreated(data=data, item=self.owned_id)
self.owned.refresh_from_db()
new_operation = response.data['new_operation']
arguments = self.owned.arguments()
self.assertTrue(arguments.filter(operation__id=new_operation['id'], argument=self.operation1))
self.assertTrue(arguments.filter(operation__id=new_operation['id'], argument=self.operation3))
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
def test_create_operation_result(self):
self.populateData()
self.operation1.result = None
self.operation1.save()
data = {
'item_data': {
'alias': 'Test4',
'operation_type': OperationType.INPUT,
'result': self.ks1.model.pk
},
'positions': [],
}
response = self.executeCreated(data=data, item=self.owned_id)
self.owned.refresh_from_db()
new_operation = response.data['new_operation']
self.assertEqual(new_operation['result'], self.ks1.model.pk)
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
def test_create_operation_schema(self):
self.populateData()
Editor.add(self.owned.model.pk, self.user2.pk)
data = {
'item_data': {
'alias': 'Test4',
'title': 'Test title',
'description': 'Comment',
'operation_type': OperationType.INPUT,
'result': self.ks1.model.pk
},
'create_schema': True,
'positions': [],
}
self.executeBadData(data=data, item=self.owned_id)
data['item_data']['result'] = None
response = self.executeCreated(data=data, item=self.owned_id)
self.owned.refresh_from_db()
new_operation = response.data['new_operation']
schema = LibraryItem.objects.get(pk=new_operation['result'])
self.assertEqual(schema.alias, data['item_data']['alias'])
self.assertEqual(schema.title, data['item_data']['title'])
self.assertEqual(schema.description, data['item_data']['description'])
self.assertEqual(schema.visible, False)
self.assertEqual(schema.access_policy, self.owned.model.access_policy)
self.assertEqual(schema.location, self.owned.model.location)
self.assertIn(self.user2, schema.getQ_editors())
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_operation(self):
self.executeNotFound(item=self.invalid_id)
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'positions': []
}
self.executeBadData(data=data)
data['target'] = self.operation1.pk
self.toggle_admin(True)
self.executeBadData(data=data, item=self.unowned_id)
self.logout()
self.executeForbidden(data=data, item=self.owned_id)
self.login()
response = self.executeOK(data=data)
self.assertEqual(len(response.data['items']), 2)
@decl_endpoint('/api/oss/{item}/create-input', method='patch')
def test_create_input(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'positions': []
}
self.executeBadData(data=data)
data['target'] = self.operation1.pk
self.toggle_admin(True)
self.executeBadData(data=data, item=self.unowned_id)
self.logout()
self.executeForbidden(data=data, item=self.owned_id)
self.login()
self.executeBadData(data=data, item=self.owned_id)
self.operation1.result = None
self.operation1.description = 'TestComment'
self.operation1.title = 'TestTitle'
self.operation1.save()
response = self.executeOK(data=data)
self.operation1.refresh_from_db()
new_schema = response.data['new_schema']
self.assertEqual(new_schema['id'], self.operation1.result.pk)
self.assertEqual(new_schema['alias'], self.operation1.alias)
self.assertEqual(new_schema['title'], self.operation1.title)
self.assertEqual(new_schema['description'], self.operation1.description)
data['target'] = self.operation3.pk
self.executeBadData(data=data)
@decl_endpoint('/api/oss/{item}/set-input', method='patch')
def test_set_input_null(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'positions': []
}
self.executeBadData(data=data)
data['target'] = self.operation1.pk
data['input'] = None
self.toggle_admin(True)
self.executeBadData(data=data, item=self.unowned_id)
self.logout()
self.executeForbidden(data=data, item=self.owned_id)
self.login()
response = self.executeOK(data=data)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.result, None)
data['input'] = self.ks1.model.pk
self.ks1.model.alias = 'Test42'
self.ks1.model.title = 'Test421'
self.ks1.model.description = 'TestComment42'
self.ks1.save()
response = self.executeOK(data=data)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.result, self.ks1.model)
self.assertEqual(self.operation1.alias, self.ks1.model.alias)
self.assertEqual(self.operation1.title, self.ks1.model.title)
self.assertEqual(self.operation1.description, self.ks1.model.description)
@decl_endpoint('/api/oss/{item}/set-input', method='patch')
def test_set_input_change_schema(self):
self.populateData()
self.operation2.result = None
data = {
'positions': [],
'target': self.operation1.pk,
'input': self.ks2.model.pk
}
self.executeBadData(data=data, item=self.owned_id)
self.ks2.model.visible = False
self.ks2.model.save(update_fields=['visible'])
data = {
'positions': [],
'target': self.operation2.pk,
'input': None
}
self.executeOK(data=data, item=self.owned_id)
self.operation2.refresh_from_db()
self.ks2.model.refresh_from_db()
self.assertEqual(self.operation2.result, None)
self.assertEqual(self.ks2.model.visible, True)
data = {
'positions': [],
'target': self.operation1.pk,
'input': self.ks2.model.pk
}
self.executeOK(data=data, item=self.owned_id)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.result, self.ks2.model)
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_update_operation(self):
self.populateData()
self.executeBadData(item=self.owned_id)
ks3 = RSForm.create(alias='KS3', title='Test3', owner=self.user)
ks3x1 = ks3.insert_new('X1', term_resolved='X1_1')
data = {
'target': self.operation3.pk,
'item_data': {
'alias': 'Test3 mod',
'title': 'Test title mod',
'description': 'Comment mod'
},
'positions': [],
'arguments': [self.operation2.pk, self.operation1.pk],
'substitutions': [
{
'original': self.ks1X1.pk,
'substitution': ks3x1.pk
}
]
}
self.executeBadData(data=data)
data['substitutions'][0]['substitution'] = self.ks2X1.pk
self.toggle_admin(True)
self.executeBadData(data=data, item=self.unowned_id)
self.logout()
self.executeForbidden(data=data, item=self.owned_id)
self.login()
response = self.executeOK(data=data)
self.operation3.refresh_from_db()
self.assertEqual(self.operation3.alias, data['item_data']['alias'])
self.assertEqual(self.operation3.title, data['item_data']['title'])
self.assertEqual(self.operation3.description, data['item_data']['description'])
args = self.operation3.getQ_arguments().order_by('order')
self.assertEqual(args[0].argument.pk, data['arguments'][0])
self.assertEqual(args[0].order, 0)
self.assertEqual(args[1].argument.pk, data['arguments'][1])
self.assertEqual(args[1].order, 1)
sub = self.operation3.getQ_substitutions()[0]
self.assertEqual(sub.original.pk, data['substitutions'][0]['original'])
self.assertEqual(sub.substitution.pk, data['substitutions'][0]['substitution'])
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_update_operation_sync(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'target': self.operation1.pk,
'item_data': {
'alias': 'Test3 mod',
'title': 'Test title mod',
'description': 'Comment mod'
},
'positions': [],
}
response = self.executeOK(data=data)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.alias, data['item_data']['alias'])
self.assertEqual(self.operation1.title, data['item_data']['title'])
self.assertEqual(self.operation1.description, data['item_data']['description'])
self.assertEqual(self.operation1.result.alias, data['item_data']['alias'])
self.assertEqual(self.operation1.result.title, data['item_data']['title'])
self.assertEqual(self.operation1.result.description, data['item_data']['description'])
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_update_operation_invalid_substitution(self):
self.populateData()
self.ks1X2 = self.ks1.insert_new('X2')
data = {
'target': self.operation3.pk,
'item_data': {
'alias': 'Test3 mod',
'title': 'Test title mod',
'description': 'Comment mod'
},
'positions': [],
'arguments': [self.operation1.pk, self.operation2.pk],
'substitutions': [
{
'original': self.ks1X1.pk,
'substitution': self.ks2X1.pk
},
{
'original': self.ks2X1.pk,
'substitution': self.ks1X2.pk
}
]
}
self.executeBadData(data=data, item=self.owned_id)
@decl_endpoint('/api/oss/{item}/execute-operation', method='post')
def test_execute_operation(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'positions': [],
'target': self.operation1.pk
}
self.executeBadData(data=data)
data['target'] = self.operation3.pk
self.toggle_admin(True)
self.executeBadData(data=data, item=self.unowned_id)
self.logout()
self.executeForbidden(data=data, item=self.owned_id)
self.login()
self.executeOK(data=data)
self.operation3.refresh_from_db()
schema = self.operation3.result
self.assertEqual(schema.alias, self.operation3.alias)
self.assertEqual(schema.description, self.operation3.description)
self.assertEqual(schema.title, self.operation3.title)
self.assertEqual(schema.visible, False)
items = list(RSForm(schema).constituents())
self.assertEqual(len(items), 1)
self.assertEqual(items[0].alias, 'X1')
self.assertEqual(items[0].term_resolved, self.ks2X1.term_resolved)
@decl_endpoint('/api/oss/get-predecessor', method='post')
def test_get_predecessor(self):

View File

@ -36,10 +36,9 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
def get_permissions(self):
''' Determine permission class. '''
if self.action in [
'update_layout',
'create_operation',
'create_block',
'delete_operation',
'update_positions',
'create_input',
'set_input',
'update_operation',
@ -74,21 +73,21 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
)
@extend_schema(
summary='update layout',
summary='update positions',
tags=['OSS'],
request=s.LayoutSerializer,
request=s.PositionsSerializer,
responses={
c.HTTP_200_OK: None,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='update-layout')
def update_layout(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Update schema layout. '''
serializer = s.LayoutSerializer(data=request.data)
@action(detail=True, methods=['patch'], url_path='update-positions')
def update_positions(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Update operations positions. '''
serializer = s.PositionsSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
m.OperationSchema(self.get_object()).update_layout(serializer.validated_data)
m.OperationSchema(self.get_object()).update_positions(serializer.validated_data['positions'])
return Response(status=c.HTTP_200_OK)
@extend_schema(
@ -105,23 +104,13 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@action(detail=True, methods=['post'], url_path='create-operation')
def create_operation(self, request: Request, pk) -> HttpResponse:
''' Create new operation. '''
serializer = s.OperationCreateSerializer(
data=request.data,
context={'oss': self.get_object()}
)
serializer = s.OperationCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
oss = m.OperationSchema(self.get_object())
layout = serializer.validated_data['layout']
with transaction.atomic():
oss.update_positions(serializer.validated_data['positions'])
new_operation = oss.create_operation(**serializer.validated_data['item_data'])
layout['operations'].append({
'id': new_operation.pk,
'x': serializer.validated_data['position_x'],
'y': serializer.validated_data['position_y']
})
oss.update_layout(layout)
schema = new_operation.result
if schema is not None:
connected_operations = \
@ -152,57 +141,6 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
}
)
@extend_schema(
summary='create block',
tags=['OSS'],
request=s.BlockCreateSerializer(),
responses={
c.HTTP_201_CREATED: s.NewBlockResponse,
c.HTTP_400_BAD_REQUEST: None,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['post'], url_path='create-block')
def create_block(self, request: Request, pk) -> HttpResponse:
''' Create new block. '''
serializer = s.BlockCreateSerializer(
data=request.data,
context={'oss': self.get_object()}
)
serializer.is_valid(raise_exception=True)
oss = m.OperationSchema(self.get_object())
layout = serializer.validated_data['layout']
children_blocks: list[m.Block] = serializer.validated_data['children_blocks']
children_operations: list[m.Operation] = serializer.validated_data['children_operations']
with transaction.atomic():
new_block = oss.create_block(**serializer.validated_data['item_data'])
layout['blocks'].append({
'id': new_block.pk,
'x': serializer.validated_data['position_x'],
'y': serializer.validated_data['position_y'],
'width': serializer.validated_data['width'],
'height': serializer.validated_data['height'],
})
oss.update_layout(layout)
if len(children_blocks) > 0:
for block in children_blocks:
block.parent = new_block
m.Block.objects.bulk_update(children_blocks, ['parent'])
if len(children_operations) > 0:
for operation in children_operations:
operation.parent = new_block
m.Operation.objects.bulk_update(children_operations, ['parent'])
return Response(
status=c.HTTP_201_CREATED,
data={
'new_block': s.BlockSerializer(new_block).data,
'oss': s.OperationSchemaSerializer(oss.model).data
}
)
@extend_schema(
summary='delete operation',
tags=['OSS'],
@ -226,11 +164,9 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
oss = m.OperationSchema(self.get_object())
operation = cast(m.Operation, serializer.validated_data['target'])
old_schema = operation.result
layout = serializer.validated_data['layout']
layout['operations'] = [x for x in layout['operations'] if x['id'] != operation.pk]
with transaction.atomic():
oss.update_positions(serializer.validated_data['positions'])
oss.delete_operation(operation.pk, serializer.validated_data['keep_constituents'])
oss.update_layout(layout)
if old_schema is not None:
if serializer.validated_data['delete_schema']:
m.PropagationFacade.before_delete_schema(old_schema)
@ -275,7 +211,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
oss = m.OperationSchema(self.get_object())
with transaction.atomic():
oss.update_layout(serializer.validated_data['layout'])
oss.update_positions(serializer.validated_data['positions'])
schema = oss.create_input(operation)
return Response(
@ -326,7 +262,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
if old_schema.is_synced(oss.model):
old_schema.visible = True
old_schema.save(update_fields=['visible'])
oss.update_layout(serializer.validated_data['layout'])
oss.update_positions(serializer.validated_data['positions'])
oss.set_input(target_operation.pk, schema)
return Response(
status=c.HTTP_200_OK,
@ -356,7 +292,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
oss = m.OperationSchema(self.get_object())
with transaction.atomic():
oss.update_layout(serializer.validated_data['layout'])
oss.update_positions(serializer.validated_data['positions'])
operation.alias = serializer.validated_data['item_data']['alias']
operation.title = serializer.validated_data['item_data']['title']
operation.description = serializer.validated_data['item_data']['description']
@ -410,7 +346,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
oss = m.OperationSchema(self.get_object())
with transaction.atomic():
oss.update_layout(serializer.validated_data['layout'])
oss.update_positions(serializer.validated_data['positions'])
oss.execute_operation(operation)
return Response(

View File

@ -9,7 +9,6 @@ from apps.rsform.models import Constituenta, CstType, RSForm
class TestConstituenta(TestCase):
''' Testing Constituenta model. '''
def setUp(self):
self.schema1 = RSForm.create(title='Test1')
self.schema2 = RSForm.create(title='Test2')
@ -48,7 +47,6 @@ class TestConstituenta(TestCase):
self.assertEqual(cst.definition_resolved, '')
self.assertEqual(cst.definition_raw, '')
def test_extract_references(self):
cst = Constituenta.objects.create(
alias='X1',
@ -59,7 +57,6 @@ class TestConstituenta(TestCase):
)
self.assertEqual(cst.extract_references(), set(['X1', 'X2', 'X3', 'X4', 'X5']))
def text_apply_mapping(self):
cst = Constituenta.objects.create(
alias='X1',

View File

@ -9,7 +9,6 @@ from shared.DBTester import DBTester
class TestRSForm(DBTester):
''' Testing RSForm wrapper. '''
def setUp(self):
super().setUp()
self.user1 = User.objects.create(username='User1')
@ -104,7 +103,6 @@ class TestRSForm(DBTester):
self.assertEqual(x2.schema, self.schema.model)
self.assertEqual(x1.order, 0)
def test_create_cst(self):
data = {
'alias': 'X3',
@ -196,7 +194,6 @@ class TestRSForm(DBTester):
self.assertEqual(d1.definition_raw, '@{DEL|sing}')
self.assertEqual(d1.term_raw, '@{X2|plur}')
def test_apply_mapping(self):
x1 = self.schema.insert_new('X1')
x2 = self.schema.insert_new('X11')

View File

@ -138,7 +138,6 @@ class TestRSFormViewset(EndpointTester):
self.assertEqual(response.data['typification'], 'LOGIC')
self.assertEqual(response.data['valueClass'], 'value')
@decl_endpoint('/api/rsforms/{item}/check-constituenta', method='post')
def test_check_constituenta_error(self):
self.owned.insert_new('X1')
@ -146,7 +145,6 @@ class TestRSFormViewset(EndpointTester):
response = self.executeOK(data=data, item=self.owned_id)
self.assertEqual(response.data['parseResult'], False)
@decl_endpoint('/api/rsforms/{item}/resolve', method='post')
def test_resolve(self):
x1 = self.owned.insert_new(

View File

@ -7,7 +7,6 @@ from apps.rsform.graph import Graph
class TestGraph(unittest.TestCase):
''' Test class for graph. '''
def test_construction(self):
graph = Graph()
self.assertFalse(graph.contains(1))
@ -27,7 +26,6 @@ class TestGraph(unittest.TestCase):
self.assertTrue(graph.has_edge(1, 3))
self.assertTrue(graph.has_edge(2, 1))
def test_remove_node(self):
graph = Graph({
1: [2],
@ -41,7 +39,6 @@ class TestGraph(unittest.TestCase):
self.assertEqual(graph.outputs[1], [])
self.assertEqual(len(graph.outputs), 3)
def test_remove_edge(self):
graph = Graph({
1: [2],
@ -56,7 +53,6 @@ class TestGraph(unittest.TestCase):
self.assertEqual(graph.outputs[1], [])
graph.remove_edge(1, 2)
def test_expand_outputs(self):
graph = Graph({
1: [2],
@ -71,7 +67,6 @@ class TestGraph(unittest.TestCase):
self.assertEqual(graph.expand_outputs([7]), [])
self.assertEqual(graph.expand_outputs([2, 5]), [3, 6, 1])
def test_expand_inputs(self):
graph = Graph({
1: [2],
@ -106,7 +101,6 @@ class TestGraph(unittest.TestCase):
7: [6]
})
def test_topological_order(self):
self.assertEqual(Graph().topological_order(), [])
graph = Graph({
@ -128,7 +122,6 @@ class TestGraph(unittest.TestCase):
})
self.assertEqual(graph.topological_order(), [5, 3, 2, 4, 1])
def test_sort_stable(self):
graph = Graph({
1: [2],

View File

@ -8,7 +8,6 @@ from apps.rsform.utils import apply_pattern, fix_old_references
class TestUtils(unittest.TestCase):
''' Test various utility functions. '''
def test_apply_mapping_patter(self):
mapping = {'X101': 'X20'}
pattern = re.compile(r'(X[0-9]+)')

View File

@ -1,4 +1,4 @@
tzdata==2025.2
tzdata==2025.1
Django==5.1.7
djangorestframework==3.15.2
django-cors-headers==4.7.0
@ -17,5 +17,5 @@ djangorestframework-stubs==3.15.3
django-extensions==3.2.3
django-stubs==5.1.3
mypy==1.15.0
pylint==3.3.6
coverage==7.7.1
pylint==3.3.5
coverage==7.6.12

View File

@ -1,4 +1,4 @@
tzdata==2025.2
tzdata==2025.1
Django==5.1.7
djangorestframework==3.15.2
django-cors-headers==4.7.0

View File

@ -1,13 +0,0 @@
import os
import runpy
import sys
# Build the module path from the test file
filepath = sys.argv[1]
project_root = os.path.dirname(__file__)
relpath = os.path.relpath(filepath, project_root)
module_path = relpath.replace('/', '.').replace('\\', '.').removesuffix('.py')
# Run manage.py in-process so breakpoints work
sys.argv = ["manage.py", "test", module_path]
runpy.run_path("manage.py", run_name="__main__")

View File

@ -14,18 +14,6 @@ def operationNotInOSS(title: str):
return f'Операция не принадлежит ОСС: {title}'
def parentNotInOSS():
return f'Родительский блок не принадлежит ОСС'
def childNotInOSS():
return f'Дочерний элемент блок не принадлежит ОСС'
def missingArguments():
return 'Операция не содержит аргументов, при этом содержит отождествления'
def exteorFileCorrupted():
return 'Файл Экстеор не соответствует ожидаемому формату. Попробуйте сохранить файл в новой версии'

View File

@ -1,21 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

File diff suppressed because it is too large Load Diff

View File

@ -15,72 +15,69 @@
},
"dependencies": {
"@dagrejs/dagre": "^1.1.4",
"@hookform/resolvers": "^5.0.1",
"@hookform/resolvers": "^4.1.3",
"@lezer/lr": "^1.4.2",
"@radix-ui/react-popover": "^1.1.7",
"@radix-ui/react-select": "^2.1.7",
"@tanstack/react-query": "^5.72.2",
"@tanstack/react-query-devtools": "^5.72.2",
"@tanstack/react-query": "^5.69.0",
"@tanstack/react-query-devtools": "^5.69.0",
"@tanstack/react-table": "^8.21.2",
"@uiw/codemirror-themes": "^4.23.10",
"@uiw/react-codemirror": "^4.23.10",
"axios": "^1.8.4",
"class-variance-authority": "^0.7.1",
"axios": "^1.8.3",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"global": "^4.4.0",
"js-file-download": "^0.4.12",
"lucide-react": "^0.487.0",
"qrcode.react": "^4.2.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-error-boundary": "^5.0.0",
"react-hook-form": "^7.55.0",
"react-hook-form": "^7.54.2",
"react-icons": "^5.5.0",
"react-intl": "^7.1.10",
"react-router": "^7.5.0",
"react-scan": "^0.3.3",
"react-intl": "^7.1.6",
"react-router": "^7.3.0",
"react-scan": "^0.3.2",
"react-select": "^5.10.1",
"react-tabs": "^6.1.0",
"react-toastify": "^11.0.5",
"react-tooltip": "^5.28.1",
"react-tooltip": "^5.28.0",
"react-zoom-pan-pinch": "^3.7.0",
"reactflow": "^11.11.4",
"tailwind-merge": "^3.2.0",
"tw-animate-css": "^1.2.5",
"use-debounce": "^10.0.4",
"zod": "^3.24.2",
"zustand": "^5.0.3"
},
"devDependencies": {
"@lezer/generator": "^1.7.3",
"@lezer/generator": "^1.7.2",
"@playwright/test": "^1.51.1",
"@tailwindcss/vite": "^4.1.3",
"@tailwindcss/vite": "^4.0.14",
"@types/jest": "^29.5.14",
"@types/node": "^22.14.0",
"@types/react": "^19.1.1",
"@types/react-dom": "^19.1.2",
"@types/node": "^22.13.10",
"@types/react": "^19.0.11",
"@types/react-dom": "^19.0.4",
"@typescript-eslint/eslint-plugin": "^8.0.1",
"@typescript-eslint/parser": "^8.0.1",
"@vitejs/plugin-react": "^4.3.4",
"babel-plugin-react-compiler": "^19.0.0-beta-21e868a-20250216",
"eslint": "^9.24.0",
"eslint": "^9.22.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-playwright": "^2.2.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-compiler": "^19.0.0-beta-21e868a-20250216",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-simple-import-sort": "^12.1.1",
"globals": "^16.0.0",
"jest": "^29.7.0",
"stylelint": "^16.18.0",
"stylelint-config-recommended": "^16.0.0",
"stylelint-config-standard": "^38.0.0",
"stylelint": "^16.16.0",
"stylelint-config-recommended": "^15.0.0",
"stylelint-config-standard": "^37.0.0",
"stylelint-config-tailwindcss": "^1.0.0",
"tailwindcss": "^4.0.7",
"ts-jest": "^29.3.1",
"typescript": "^5.8.3",
"typescript-eslint": "^8.29.1",
"vite": "^6.2.6"
"ts-jest": "^29.2.6",
"typescript": "^5.8.2",
"typescript-eslint": "^8.26.1",
"vite": "^6.2.2"
},
"overrides": {
"react": "^19.0.0"
},
"jest": {
"preset": "ts-jest",

View File

@ -10,7 +10,7 @@ export function Footer() {
'z-navigation',
'mx-auto',
'px-3 py-2 flex flex-col items-center gap-1',
'text-xs sm:text-sm select-none whitespace-nowrap text-muted-foreground bg-background'
'text-xs sm:text-sm select-none whitespace-nowrap text-prim-600 bg-prim-100'
)}
>
<nav className='flex gap-3' aria-label='Вторичная навигация'>
@ -20,7 +20,7 @@ export function Footer() {
<TextURL text='Экстеор' href='/manuals?topic=exteor' color='' />
</nav>
<p>© 2025 ЦИВТ КОНЦЕПТ</p>
<p>© 2024 ЦИВТ КОНЦЕПТ</p>
</footer>
);
}

View File

@ -18,7 +18,7 @@ export function GlobalLoader() {
return (
<div className='cc-modal-wrapper'>
<ModalBackdrop />
<div className='z-pop cc-fade-in px-10 border rounded-xl bg-background'>
<div className='z-pop cc-fade-in px-10 border rounded-xl bg-prim-100'>
<Loader scale={6} />
</div>
</div>

View File

@ -20,9 +20,9 @@ export function MutationErrors() {
return (
<div className='cc-modal-wrapper'>
<ModalBackdrop onHide={resetErrors} />
<div className='z-pop px-10 py-3 flex flex-col items-center border rounded-xl bg-background' role='alertdialog'>
<div className='z-pop px-10 py-3 flex flex-col items-center border rounded-xl bg-prim-100' role='alertdialog'>
<h1 className='py-2 select-none'>Ошибка при обработке</h1>
<div className='px-3 flex flex-col text-destructive text-sm font-semibold select-text'>
<div className='px-3 flex flex-col text-warn-600 text-sm font-semibold select-text'>
<DescribeError error={mutationErrors[0]} />
</div>
<Button onClick={resetErrors} className='w-fit' text='Закрыть' />

View File

@ -1,5 +1,6 @@
import clsx from 'clsx';
import { type Styling } from '@/components/props';
import { cn } from '@/components/utils';
import { globalIDs } from '@/utils/constants';
interface NavigationButtonProps extends Styling {
@ -20,7 +21,7 @@ export function NavigationButton({ icon, title, hideTitle, className, style, onC
data-tooltip-hidden={hideTitle}
data-tooltip-content={title}
onClick={onClick}
className={cn('p-2 flex items-center gap-1', 'cc-btn-nav', 'font-controls focus-outline', className)}
className={clsx('p-2 flex items-center gap-1', 'cc-btn-nav', 'font-controls focus-outline', className)}
style={style}
>
{icon ? icon : null}

View File

@ -3,7 +3,6 @@ import clsx from 'clsx';
import { IconLibrary2, IconManuals, IconNewItem2 } from '@/components/icons';
import { useWindowSize } from '@/hooks/use-window-size';
import { useAppLayoutStore } from '@/stores/app-layout';
import { useDialogsStore } from '@/stores/dialogs';
import { urls } from '../urls';
@ -17,7 +16,6 @@ export function Navigation() {
const { push } = useConceptNavigation();
const size = useWindowSize();
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
const activeDialog = useDialogsStore(state => state.active);
const navigateHome = (event: React.MouseEvent<Element>) =>
push({ path: urls.home, newTab: event.ctrlKey || event.metaKey });
@ -29,7 +27,7 @@ export function Navigation() {
push({ path: urls.create_schema, newTab: event.ctrlKey || event.metaKey });
return (
<nav className='z-navigation sticky top-0 left-0 right-0 select-none bg-background' inert={activeDialog !== null}>
<nav className='z-navigation sticky top-0 left-0 right-0 select-none bg-prim-100'>
<ToggleNavigation />
<div
className={clsx(

View File

@ -13,7 +13,7 @@ export function UserMenu() {
const router = useConceptNavigation();
const menu = useDropdown();
return (
<div ref={menu.ref} onBlur={menu.handleBlur} className='flex items-center justify-start relative h-full pr-2'>
<div ref={menu.ref} className='flex items-center justify-start relative h-full pr-2'>
<Suspense fallback={<Loader circular scale={1.5} />}>
<UserButton
onLogin={() => router.push({ path: urls.login, force: true })}

View File

@ -1,5 +1,6 @@
import clsx from 'clsx';
import { type Styling } from '../props';
import { cn } from '../utils';
interface DividerProps extends Styling {
/** Indicates whether the divider is vertical. */
@ -15,7 +16,7 @@ interface DividerProps extends Styling {
export function Divider({ vertical, margins = 'mx-2', className, ...restProps }: DividerProps) {
return (
<div
className={cn(
className={clsx(
vertical ? 'border-x' : 'border-y', //
margins,
className

View File

@ -3,11 +3,10 @@
import { type ReactNode } from 'react';
import { createPortal } from 'react-dom';
import { type ITooltip, Tooltip as TooltipImpl } from 'react-tooltip';
import clsx from 'clsx';
import { usePreferencesStore } from '@/stores/preferences';
import { cn } from '../utils';
export type { PlacesType } from 'react-tooltip';
interface TooltipProps extends Omit<ITooltip, 'variant'> {
@ -38,7 +37,7 @@ export function Tooltip({
delayShow={750}
delayHide={100}
opacity={1}
className={cn(
className={clsx(
'relative',
'py-0.5! px-2!',
'max-h-[calc(100svh-6rem)]',

View File

@ -1,7 +1,8 @@
import clsx from 'clsx';
import { globalIDs } from '@/utils/constants';
import { type Button as ButtonStyle, type Control } from '../props';
import { cn } from '../utils';
interface ButtonProps extends Control, ButtonStyle {
/** Icon to display first. */
@ -37,13 +38,13 @@ export function Button({
return (
<button
type='button'
className={cn(
className={clsx(
'inline-flex gap-2 items-center justify-center',
'font-medium select-none disabled:cursor-auto disabled:opacity-75',
'bg-secondary text-secondary-foreground cc-hover cc-animate-color',
'font-medium select-none disabled:cursor-auto',
'clr-btn-default cc-animate-color',
dense ? 'px-1' : 'px-3 py-1',
loading ? 'cursor-progress' : 'cursor-pointer',
noOutline ? 'outline-hidden focus-visible:bg-selected' : 'focus-outline',
noOutline ? 'outline-hidden' : 'focus-outline',
!noBorder && 'border rounded-sm',
className
)}

View File

@ -39,9 +39,9 @@ export function MiniButton({
tabIndex={tabIndex ?? -1}
className={clsx(
'rounded-lg',
'cc-controls cc-animate-background',
'clr-text-controls cc-animate-color',
'cursor-pointer disabled:cursor-auto',
noHover ? 'outline-hidden' : 'cc-hover',
noHover ? 'outline-hidden' : 'clr-hover',
!noPadding && 'px-1 py-1',
className
)}

View File

@ -1,7 +1,8 @@
import clsx from 'clsx';
import { globalIDs } from '@/utils/constants';
import { type Button } from '../props';
import { cn } from '../utils';
interface SelectorButtonProps extends Button {
/** Text to display in the button. */
@ -9,6 +10,9 @@ interface SelectorButtonProps extends Button {
/** Icon to display in the button. */
icon?: React.ReactNode;
/** Indicates if button background should be transparent. */
transparent?: boolean;
}
/**
@ -20,6 +24,7 @@ export function SelectorButton({
title,
titleHtml,
className,
transparent,
hideTitle,
...restProps
}: SelectorButtonProps) {
@ -27,12 +32,13 @@ export function SelectorButton({
<button
type='button'
tabIndex={-1}
className={cn(
className={clsx(
'px-1 flex flex-start items-center gap-1',
'text-sm font-controls select-none',
'text-btn cc-controls',
'text-btn clr-text-controls',
'disabled:cursor-auto cursor-pointer',
'cc-hover cc-animate-color',
'cc-animate-color',
transparent ? 'clr-hover' : 'clr-btn-default border',
className
)}
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}

View File

@ -1,5 +1,6 @@
import clsx from 'clsx';
import { type Button } from '../props';
import { cn } from '../utils';
interface SubmitButtonProps extends Button {
/** Text to display in the button. */
@ -19,12 +20,12 @@ export function SubmitButton({ text = 'ОК', icon, disabled, loading, className
return (
<button
type='submit'
className={cn(
className={clsx(
'px-3 py-1 flex gap-2 items-center justify-center',
'border',
'font-medium',
'cc-btn-primary disabled:opacity-50 cc-animate-color',
'select-none cursor-pointer disabled:cursor-auto',
'clr-btn-primary cc-animate-color',
'select-none disabled:cursor-auto',
loading && 'cursor-progress',
className
)}

View File

@ -20,7 +20,7 @@ interface TextURLProps {
/**
* Displays a text with a clickable link.
*/
export function TextURL({ text, href, title, color = 'text-primary', onClick }: TextURLProps) {
export function TextURL({ text, href, title, color = 'text-sec-600', onClick }: TextURLProps) {
const design = `cursor-pointer hover:underline ${color}`;
if (href) {
return (
@ -38,3 +38,4 @@ export function TextURL({ text, href, title, color = 'text-primary', onClick }:
return null;
}
}

View File

@ -10,9 +10,9 @@ import {
type TableOptions,
type VisibilityState
} from '@tanstack/react-table';
import clsx from 'clsx';
import { type Styling } from '../props';
import { cn } from '../utils';
import { DefaultNoData } from './default-no-data';
import { PaginationTools } from './pagination-tools';
@ -157,7 +157,7 @@ export function DataTable<TData extends RowData>({
<div
tabIndex={-1}
id={id}
className={cn('table-auto', className)}
className={clsx('table-auto', className)}
style={{ minHeight: fixedSize, maxHeight: fixedSize, ...style }}
>
<table className='w-full' style={{ ...columnSizeVars }}>

View File

@ -31,7 +31,7 @@ export function PaginationTools<TData>({
);
return (
<div className='flex justify-end items-center my-2 text-sm cc-controls select-none'>
<div className='flex justify-end items-center my-2 text-sm clr-text-controls select-none'>
<span className='mr-3'>
{`${table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1}
-
@ -46,7 +46,7 @@ export function PaginationTools<TData>({
<button
type='button'
aria-label='Первая страница'
className='cc-hover cc-controls cc-animate-color focus-outline'
className='clr-hover clr-text-controls cc-animate-color focus-outline'
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
@ -55,7 +55,7 @@ export function PaginationTools<TData>({
<button
type='button'
aria-label='Предыдущая страница'
className='cc-hover cc-controls cc-animate-color focus-outline'
className='clr-hover clr-text-controls cc-animate-color focus-outline'
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
@ -65,7 +65,7 @@ export function PaginationTools<TData>({
id={id ? `${id}__page` : undefined}
title='Номер страницы. Выделите для ручного ввода'
aria-label='Номер страницы'
className='w-6 text-center bg-transparent focus-outline'
className='w-6 text-center bg-prim-100 focus-outline'
value={table.getState().pagination.pageIndex + 1}
onChange={event => {
const page = event.target.value ? Number(event.target.value) - 1 : 0;
@ -77,7 +77,7 @@ export function PaginationTools<TData>({
<button
type='button'
aria-label='Следующая страница'
className='cc-hover cc-controls cc-animate-color focus-outline'
className='clr-hover clr-text-controls cc-animate-color focus-outline'
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
@ -86,7 +86,7 @@ export function PaginationTools<TData>({
<button
type='button'
aria-label='Последняя страница'
className='cc-hover cc-controls cc-animate-color focus-outline'
className='clr-hover clr-text-controls cc-animate-color focus-outline'
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
@ -98,7 +98,7 @@ export function PaginationTools<TData>({
aria-label='Выбор количества строчек на странице'
value={table.getState().pagination.pageSize}
onChange={handlePaginationOptionsChange}
className='mx-2 cursor-pointer bg-transparent focus-outline'
className='mx-2 cursor-pointer bg-prim-100 focus-outline'
>
{paginationOptions.map(pageSize => (
<option key={`${prefixes.page_size}${pageSize}`} value={pageSize} aria-label={`${pageSize} на страницу`}>

View File

@ -75,11 +75,11 @@ export function TableBody<TData>({
key={row.id}
className={clsx(
'cc-scroll-row',
'cc-hover cc-animate-background duration-(--duration-fade)',
'clr-hover cc-animate-color',
!noHeader && 'scroll-mt-[calc(2px+2rem)]',
table.options.enableRowSelection && row.getIsSelected()
? 'cc-selected'
: 'odd:bg-secondary even:bg-background'
? 'clr-selected'
: 'odd:bg-prim-200 even:bg-prim-100'
)}
style={{ ...(conditionalRowStyles ? getRowStyles(row) : []) }}
onClick={event => handleRowClicked(row, event)}

View File

@ -14,7 +14,7 @@ interface TableHeaderProps<TData> {
export function TableHeader<TData>({ table, headPosition, resetLastSelected }: TableHeaderProps<TData>) {
return (
<thead className='sticky bg-background cc-shadow-border' style={{ top: headPosition }}>
<thead className='sticky bg-prim-100 cc-shadow-border' style={{ top: headPosition }}>
{table.getHeaderGroups().map((headerGroup: HeaderGroup<TData>) => (
<tr key={headerGroup.id}>
{table.options.enableRowSelection ? (

View File

@ -1,8 +1,8 @@
import clsx from 'clsx';
import { type Button } from '@/components/props';
import { globalIDs } from '@/utils/constants';
import { cn } from '../utils';
interface DropdownButtonProps extends Button {
/** Icon to display first (not used if children are provided). */
icon?: React.ReactNode;
@ -33,12 +33,12 @@ export function DropdownButton({
<button
type='button'
onClick={onClick}
className={cn(
className={clsx(
'px-3 py-1 inline-flex items-center gap-2',
'text-left text-sm text-ellipsis whitespace-nowrap',
'disabled:cc-controls disabled:opacity-75',
'focus-outline cc-animate-background',
!!onClick ? 'cc-hover cursor-pointer disabled:cursor-auto' : 'bg-secondary text-secondary-foreground',
'disabled:clr-text-controls',
'cc-animate-color',
!!onClick ? 'clr-hover cursor-pointer disabled:cursor-auto' : 'clr-btn-default',
className
)}
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}

View File

@ -1,5 +1,7 @@
import React from 'react';
import clsx from 'clsx';
import { type Styling } from '../props';
import { cn } from '../utils';
interface DropdownProps extends Styling {
/** Reference to the dropdown element. */
@ -37,8 +39,8 @@ export function Dropdown({
return (
<div
tabIndex={-1}
className={cn(
'cc-dropdown isolate z-topmost absolute grid bg-popover border rounded-md shadow-lg text-sm',
className={clsx(
'cc-dropdown isolate z-topmost absolute grid bg-prim-0 border rounded-md shadow-lg text-sm',
stretchLeft ? 'right-0' : 'left-0',
stretchTop ? 'bottom-0' : 'top-full',
isOpen && 'open',

View File

@ -7,10 +7,9 @@ export function useDropdown() {
const ref = useRef<HTMLDivElement>(null);
function handleBlur(event: React.FocusEvent<HTMLDivElement>) {
if (ref.current?.contains(event.relatedTarget as Node)) {
return;
if (!ref.current?.contains(event.relatedTarget as Node)) {
setIsOpen(false);
}
setIsOpen(false);
}
return {

View File

@ -146,7 +146,6 @@ export { BiGitMerge as IconGraphOutputs } from 'react-icons/bi';
export { LuAtom as IconGraphCore } from 'react-icons/lu';
export { LuRotate3D as IconRotate3D } from 'react-icons/lu';
export { MdOutlineFitScreen as IconFitImage } from 'react-icons/md';
export { RiFocus3Line as IconFocus } from 'react-icons/ri';
export { LuSparkles as IconClustering } from 'react-icons/lu';
export { LuSparkle as IconClusteringOff } from 'react-icons/lu';
export { TbGridDots as IconGrid } from 'react-icons/tb';

View File

@ -81,11 +81,11 @@ export function InfoError({ error }: InfoErrorProps) {
'cc-fade-in',
'min-w-100',
'px-3 py-2 flex flex-col',
'text-destructive text-sm font-semibold',
'text-warn-600 text-sm font-semibold',
'select-text'
)}
>
<div className='font-normal text-foreground mb-6'>
<div className='font-normal clr-text-default mb-6'>
<p>Пожалуйста сделайте скриншот и отправьте вместе с описанием ситуации на почту portal@acconcept.ru</p>
<br />
<p>Для продолжения работы перезагрузите страницу</p>

View File

@ -3,7 +3,6 @@ import clsx from 'clsx';
import { globalIDs } from '@/utils/constants';
import { CheckboxChecked, CheckboxNull } from '../icons';
import { cn } from '../utils';
import { type CheckboxProps } from './checkbox';
@ -49,7 +48,7 @@ export function CheckboxTristate({
return (
<button
type='button'
className={cn(
className={clsx(
'flex items-center gap-2', //
'outline-hidden',
'focus-frame',
@ -68,7 +67,7 @@ export function CheckboxTristate({
className={clsx(
'w-4 h-4', //
'border rounded-sm',
value === false ? 'bg-background text-foreground' : 'bg-primary text-primary-foreground'
value === false ? 'bg-prim-100' : 'bg-sec-600 text-sec-0'
)}
>
{value ? <CheckboxChecked /> : null}

View File

@ -4,7 +4,6 @@ import { globalIDs } from '@/utils/constants';
import { CheckboxChecked } from '../icons';
import { type Button } from '../props';
import { cn } from '../utils';
export interface CheckboxProps extends Omit<Button, 'value' | 'onClick' | 'onChange'> {
/** Label to display next to the checkbox. */
@ -48,7 +47,7 @@ export function Checkbox({
return (
<button
type='button'
className={cn(
className={clsx(
'flex items-center gap-2', //
'outline-hidden',
'focus-frame',
@ -67,7 +66,7 @@ export function Checkbox({
className={clsx(
'w-4 h-4', //
'border rounded-sm',
value === false ? 'bg-background text-foreground' : 'bg-primary text-primary-foreground'
value === false ? 'bg-prim-100' : 'bg-sec-600 text-sec-0'
)}
>
{value ? <CheckboxChecked /> : null}

View File

@ -1,127 +0,0 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import { ChevronDownIcon } from 'lucide-react';
import { IconRemove } from '../icons';
import { type Styling } from '../props';
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '../ui/command';
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
import { cn } from '../utils';
interface ComboBoxProps<Option> extends Styling {
id?: string;
items?: Option[];
value: Option | null;
onChange: (newValue: Option | null) => void;
idFunc: (item: Option) => string;
labelValueFunc: (item: Option) => string;
labelOptionFunc: (item: Option) => string;
placeholder?: string;
hidden?: boolean;
noBorder?: boolean;
clearable?: boolean;
noSearch?: boolean;
}
/**
* Displays a combo-select component.
*/
export function ComboBox<Option>({
id,
items,
value,
onChange,
labelValueFunc,
labelOptionFunc,
idFunc,
noBorder,
placeholder,
className,
style,
hidden,
clearable,
noSearch
}: ComboBoxProps<Option>) {
const [open, setOpen] = useState(false);
const [popoverWidth, setPopoverWidth] = useState<number | undefined>(undefined);
const triggerRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (triggerRef.current) {
setPopoverWidth(triggerRef.current.offsetWidth);
}
}, [open]);
function handleChangeValue(newValue: Option | null) {
onChange(newValue);
setOpen(false);
}
function handleClear(event: React.MouseEvent<SVGElement>) {
event.stopPropagation();
handleChangeValue(null);
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
id={id}
ref={triggerRef}
role='combobox'
aria-expanded={open}
className={cn(
'relative h-9',
'flex gap-2 px-3 py-2 items-center justify-between',
'bg-input disabled:opacity-50',
'cursor-pointer disabled:cursor-auto',
'whitespace-nowrap',
'focus-outline',
"[&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0",
open && 'cursor-auto',
!noBorder && 'border',
noBorder && 'rounded-md',
!value && 'text-muted-foreground',
className
)}
style={style}
hidden={hidden && !open}
>
<span className='truncate'>{value ? labelValueFunc(value) : placeholder}</span>
<ChevronDownIcon className={cn('text-muted-foreground', clearable && !!value && 'opacity-0')} />
{clearable && !!value ? (
<IconRemove
tabIndex={-1}
size='1rem'
className='cc-remove absolute pointer-events-auto right-3'
onClick={handleClear}
/>
) : null}
</button>
</PopoverTrigger>
<PopoverContent sideOffset={-1} className='p-0' style={{ width: popoverWidth }}>
<Command>
{!noSearch ? <CommandInput placeholder='Поиск...' className='h-9' /> : null}
<CommandList>
<CommandEmpty>Список пуст</CommandEmpty>
<CommandGroup>
{items?.map(item => (
<CommandItem
key={idFunc(item)}
value={labelOptionFunc(item)}
onSelect={() => handleChangeValue(item)}
className={cn(value === item && 'bg-selected text-selected-foreground')}
>
{labelOptionFunc(item)}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}

View File

@ -1,145 +0,0 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import { ChevronDownIcon } from 'lucide-react';
import { IconRemove } from '../icons';
import { type Styling } from '../props';
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '../ui/command';
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
import { cn } from '../utils';
interface ComboMultiProps<Option> extends Styling {
id?: string;
items?: Option[];
value: Option[];
onChange: (newValue: Option[]) => void;
idFunc: (item: Option) => string;
labelValueFunc: (item: Option) => string;
labelOptionFunc: (item: Option) => string;
placeholder?: string;
noSearch?: boolean;
}
/**
* Displays a combo-box component with multiple selection.
*/
export function ComboMulti<Option>({
id,
items,
value,
onChange,
labelValueFunc,
labelOptionFunc,
idFunc,
placeholder,
className,
style,
noSearch
}: ComboMultiProps<Option>) {
const [open, setOpen] = useState(false);
const [popoverWidth, setPopoverWidth] = useState<number | undefined>(undefined);
const triggerRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (triggerRef.current) {
setPopoverWidth(triggerRef.current.offsetWidth);
}
}, [open]);
function handleAddValue(newValue: Option) {
if (value.includes(newValue)) {
handleRemoveValue(newValue);
} else {
onChange([...value, newValue]);
setOpen(false);
}
}
function handleRemoveValue(delValue: Option) {
onChange(value.filter(v => v !== delValue));
setOpen(false);
}
function handleClear(event: React.MouseEvent<SVGElement>) {
event.stopPropagation();
onChange([]);
setOpen(false);
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
id={id}
ref={triggerRef}
role='combobox'
aria-expanded={open}
className={cn(
'relative h-9',
'flex gap-2 px-3 py-2 items-center justify-between',
'bg-input disabled:opacity-50',
'cursor-pointer disabled:cursor-auto',
'whitespace-nowrap',
'focus-outline border',
"[&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0",
open && 'cursor-auto',
!value && 'text-muted-foreground',
className
)}
style={style}
>
<div className='flex flex-wrap gap-1 items-center'>
{value.length === 0 ? <div className='text-muted-foreground'>{placeholder}</div> : null}
{value.map(item => (
<div key={idFunc(item)} className='flex px-1 items-center border rounded-lg bg-accent text-sm'>
{labelValueFunc(item)}
<IconRemove
tabIndex={-1}
size='1rem'
className='cc-remove'
onClick={event => {
event.stopPropagation();
handleRemoveValue(item);
}}
/>
</div>
))}
</div>
<ChevronDownIcon className={cn('text-muted-foreground', !!value && 'opacity-0')} />
{!!value ? (
<IconRemove
tabIndex={-1}
size='1rem'
className='cc-remove absolute pointer-events-auto right-3'
onClick={handleClear}
/>
) : null}
</button>
</PopoverTrigger>
<PopoverContent sideOffset={-1} className='p-0' style={{ width: popoverWidth }}>
<Command>
{!noSearch ? <CommandInput placeholder='Поиск...' className='h-9' /> : null}
<CommandList>
<CommandEmpty>Список пуст</CommandEmpty>
<CommandGroup>
{items?.map(item => (
<CommandItem
key={idFunc(item)}
value={labelOptionFunc(item)}
onSelect={() => handleAddValue(item)}
className={cn(value === item && 'bg-selected text-selected-foreground')}
>
{labelOptionFunc(item)}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}

View File

@ -15,7 +15,7 @@ export function ErrorField({ error, className, ...restProps }: ErrorFieldProps):
return null;
}
return (
<div className={clsx('text-sm text-destructive select-none', className)} {...restProps}>
<div className={clsx('text-sm text-warn-600 select-none', className)} {...restProps}>
{error.message}
</div>
);

View File

@ -1,9 +1,11 @@
export { Checkbox } from './checkbox';
export { Checkbox, type CheckboxProps } from './checkbox';
export { CheckboxTristate } from './checkbox-tristate';
export { ErrorField } from './error-field';
export { FileInput } from './file-input';
export { Label } from './label';
export { SearchBar } from './search-bar';
export { SelectMulti, type SelectMultiProps } from './select-multi';
export { SelectSingle } from './select-single';
export { SelectTree } from './select-tree';
export { TextArea } from './text-area';
export { TextInput } from './text-input';

View File

@ -3,7 +3,7 @@ import clsx from 'clsx';
import { IconSearch } from '@/components/icons';
import { type Styling } from '@/components/props';
import { cn } from '../utils';
import { TextInput } from './text-input';
interface SearchBarProps extends Styling {
/** Id of the search bar. */
@ -39,23 +39,20 @@ export function SearchBar({
...restProps
}: SearchBarProps) {
return (
<div className={cn('relative flex items-center grow', className)} {...restProps}>
<div className={clsx('relative flex items-center', className)} {...restProps}>
{!noIcon ? (
<IconSearch className='absolute -top-0.5 left-2 translate-y-1/2 cc-search-icon' size='1.25rem' />
) : null}
<input
<TextInput
id={id}
noOutline
transparent
placeholder={placeholder}
type='search'
className={clsx(
'min-w-0 py-2',
'leading-tight truncate hover:text-clip',
'bg-transparent',
!noIcon && 'pl-8',
!noBorder && 'border px-3'
)}
className={clsx('bg-transparent', !noIcon && 'pl-8')}
noBorder={noBorder}
value={query}
onChange={event => onChangeQuery?.(event.target.value)}
placeholder={placeholder}
/>
</div>
);

View File

@ -0,0 +1,128 @@
'use client';
import Select, {
type ClearIndicatorProps,
components,
type DropdownIndicatorProps,
type GroupBase,
type Props,
type StylesConfig
} from 'react-select';
import { useWindowSize } from '@/hooks/use-window-size';
import { APP_COLORS, SELECT_THEME } from '@/styling/colors';
import { IconClose, IconDropArrow, IconDropArrowUp } from '../icons';
function DropdownIndicator<Option, Group extends GroupBase<Option> = GroupBase<Option>>(
props: DropdownIndicatorProps<Option, true, Group>
) {
return (
components.DropdownIndicator && (
<components.DropdownIndicator {...props}>
{props.selectProps.menuIsOpen ? <IconDropArrowUp size='1.25rem' /> : <IconDropArrow size='1.25rem' />}
</components.DropdownIndicator>
)
);
}
function ClearIndicator<Option, Group extends GroupBase<Option> = GroupBase<Option>>(
props: ClearIndicatorProps<Option, true, Group>
) {
return (
components.ClearIndicator && (
<components.ClearIndicator {...props}>
<IconClose size='1.25rem' />
</components.ClearIndicator>
)
);
}
export interface SelectMultiProps<Option, Group extends GroupBase<Option> = GroupBase<Option>>
extends Omit<Props<Option, true, Group>, 'theme' | 'menuPortalTarget'> {
noPortal?: boolean;
}
/**
* Displays a multi-select component.
*/
export function SelectMulti<Option, Group extends GroupBase<Option> = GroupBase<Option>>({
noPortal,
...restProps
}: SelectMultiProps<Option, Group>) {
const size = useWindowSize();
const adjustedStyles: StylesConfig<Option, true, Group> = {
container: defaultStyles => ({
...defaultStyles,
borderRadius: '0.25rem'
}),
control: (styles, { isDisabled }) => ({
...styles,
borderRadius: '0.25rem',
cursor: isDisabled ? 'not-allowed' : 'pointer',
boxShadow: 'none'
}),
option: (styles, { isSelected }) => ({
...styles,
padding: '0.25rem 0.75rem',
fontSize: '0.875rem',
lineHeight: '1.25rem',
backgroundColor: isSelected ? APP_COLORS.bgSelected : styles.backgroundColor,
color: isSelected ? APP_COLORS.fgSelected : styles.color,
borderWidth: '1px',
borderColor: APP_COLORS.border
}),
menuPortal: styles => ({
...styles,
zIndex: 9999
}),
menuList: styles => ({
...styles,
padding: 0
}),
input: styles => ({ ...styles }),
placeholder: styles => ({ ...styles }),
multiValue: styles => ({
...styles,
borderRadius: '0.5rem',
backgroundColor: APP_COLORS.bgSelected
}),
dropdownIndicator: base => ({
...base,
paddingTop: 0,
paddingBottom: 0
}),
clearIndicator: base => ({
...base,
paddingTop: 0,
paddingBottom: 0
})
};
return (
<Select
isMulti
noOptionsMessage={() => 'Список пуст'}
components={{ DropdownIndicator, ClearIndicator }}
theme={theme => ({
...theme,
borderRadius: 0,
spacing: {
...theme.spacing,
baseUnit: size.isSmall ? 2 : 4,
menuGutter: size.isSmall ? 4 : 8,
controlHeight: size.isSmall ? 28 : 38
},
colors: {
...theme.colors,
...SELECT_THEME
}
})}
menuPortalTarget={!noPortal ? document.body : null}
styles={adjustedStyles}
classNames={{ container: () => 'focus-frame' }}
{...restProps}
/>
);
}

View File

@ -0,0 +1,126 @@
'use client';
import Select, {
type ClearIndicatorProps,
components,
type DropdownIndicatorProps,
type GroupBase,
type Props,
type StylesConfig
} from 'react-select';
import { useWindowSize } from '@/hooks/use-window-size';
import { APP_COLORS, SELECT_THEME } from '@/styling/colors';
import { IconClose, IconDropArrow, IconDropArrowUp } from '../icons';
function DropdownIndicator<Option, Group extends GroupBase<Option> = GroupBase<Option>>(
props: DropdownIndicatorProps<Option, false, Group>
) {
return (
components.DropdownIndicator && (
<components.DropdownIndicator {...props}>
{props.selectProps.menuIsOpen ? <IconDropArrowUp size='1.25rem' /> : <IconDropArrow size='1.25rem' />}
</components.DropdownIndicator>
)
);
}
function ClearIndicator<Option, Group extends GroupBase<Option> = GroupBase<Option>>(
props: ClearIndicatorProps<Option, false, Group>
) {
return (
components.ClearIndicator && (
<components.ClearIndicator {...props}>
<IconClose size='1.25rem' />
</components.ClearIndicator>
)
);
}
interface SelectSingleProps<Option, Group extends GroupBase<Option> = GroupBase<Option>>
extends Omit<Props<Option, false, Group>, 'theme' | 'menuPortalTarget'> {
noPortal?: boolean;
noBorder?: boolean;
}
/**
* Displays a single-select component.
*/
export function SelectSingle<Option, Group extends GroupBase<Option> = GroupBase<Option>>({
noPortal,
noBorder,
...restProps
}: SelectSingleProps<Option, Group>) {
const size = useWindowSize();
const adjustedStyles: StylesConfig<Option, false, Group> = {
container: defaultStyles => ({
...defaultStyles,
borderRadius: '0.25rem'
}),
control: (defaultStyles, { isDisabled }) => ({
...defaultStyles,
borderRadius: '0.25rem',
...(noBorder ? { borderWidth: 0 } : {}),
cursor: isDisabled ? 'not-allowed' : 'pointer',
boxShadow: 'none'
}),
menuPortal: defaultStyles => ({
...defaultStyles,
zIndex: 9999
}),
menuList: defaultStyles => ({
...defaultStyles,
padding: 0
}),
option: (defaultStyles, { isSelected }) => ({
...defaultStyles,
padding: '0.25rem 0.75rem',
fontSize: '0.875rem',
lineHeight: '1.25rem',
backgroundColor: isSelected ? APP_COLORS.bgSelected : defaultStyles.backgroundColor,
color: isSelected ? APP_COLORS.fgSelected : defaultStyles.color,
borderWidth: '1px',
borderColor: APP_COLORS.border
}),
input: defaultStyles => ({ ...defaultStyles }),
placeholder: defaultStyles => ({ ...defaultStyles }),
singleValue: defaultStyles => ({ ...defaultStyles }),
dropdownIndicator: base => ({
...base,
paddingTop: 0,
paddingBottom: 0
}),
clearIndicator: base => ({
...base,
paddingTop: 0,
paddingBottom: 0
})
};
return (
<Select
noOptionsMessage={() => 'Список пуст'}
components={{ DropdownIndicator, ClearIndicator }}
theme={theme => ({
...theme,
borderRadius: 0,
spacing: {
...theme.spacing,
baseUnit: size.isSmall ? 2 : 4,
menuGutter: 2,
controlHeight: size.isSmall ? 28 : 38
},
colors: {
...theme.colors,
...SELECT_THEME
}
})}
menuPortalTarget={!noPortal ? document.body : null}
styles={adjustedStyles}
classNames={{ container: () => 'focus-frame' }}
{...restProps}
/>
);
}

View File

@ -89,9 +89,9 @@ export function SelectTree<ItemType>({
<div
key={`${prefix}${index}`}
className={clsx(
'cc-tree-item relative cc-scroll-row cc-hover',
'cc-tree-item relative cc-scroll-row clr-hover',
isActive ? 'max-h-7 py-1 border-b' : 'max-h-0 opacity-0 pointer-events-none',
value === item && 'cc-selected'
value === item && 'clr-selected'
)}
data-tooltip-id={globalIDs.tooltip}
data-tooltip-html={getDescription(item)}

View File

@ -1,173 +0,0 @@
'use client';
import * as SelectPrimitive from '@radix-ui/react-select';
import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react';
import { cn } from '../utils';
function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot='select' {...props} />;
}
function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot='select-group' {...props} />;
}
function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot='select-value' {...props} />;
}
function SelectTrigger({
className,
children,
noBorder,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
noBorder?: boolean;
}) {
return (
<SelectPrimitive.Trigger
data-slot='select-trigger'
className={cn(
'h-9',
'flex gap-2 px-3 py-2 items-center justify-between',
'bg-input disabled:opacity-50',
'cursor-pointer disabled:cursor-auto',
'whitespace-nowrap',
'focus-outline',
'data-[placeholder]:text-muted-foreground',
'*:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2',
"[&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
!noBorder && 'border',
noBorder && 'rounded-md',
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className='size-4' />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
}
function SelectContent({
className,
children,
position = 'popper',
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot='select-content'
className={cn(
'z-topmost relative max-h-(--radix-select-content-available-height) min-w-32',
'bg-popover text-sm text-popover-foreground',
'border shadow-md',
'overflow-x-hidden overflow-y-auto',
'cc-animate-popover',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
'p-1',
position === 'popper' &&
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1'
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
);
}
function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot='select-label'
className={cn('text-muted-foreground px-2 py-1.5', className)}
{...props}
/>
);
}
function SelectItem({ className, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot='select-item'
className={cn(
'relative',
'flex py-1 pr-8 pl-2 items-center gap-2',
'cursor-default rounded-sm select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'outline-none focus:bg-accent focus:text-accent-foreground',
'*:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2',
"[&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"data-[state='checked']:not-[:hover]:bg-selected data-[state='checked']:not-[:hover]:text-selected-foreground",
className
)}
{...props}
>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
}
function SelectSeparator({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot='select-separator'
className={cn('bg-border pointer-events-none -mx-1 my-1 h-px', className)}
{...props}
/>
);
}
function SelectScrollUpButton({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot='select-scroll-up-button'
className={cn('flex cursor-default items-center justify-center py-1', className)}
{...props}
>
<ChevronUpIcon className='size-4' />
</SelectPrimitive.ScrollUpButton>
);
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot='select-scroll-down-button'
className={cn('flex cursor-default items-center justify-center py-1', className)}
{...props}
>
<ChevronDownIcon className='size-4' />
</SelectPrimitive.ScrollDownButton>
);
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue
};

View File

@ -1,5 +1,6 @@
import clsx from 'clsx';
import { type Editor, type ErrorProcessing, type Titled } from '../props';
import { cn } from '../utils';
import { ErrorField } from './error-field';
import { Label } from './label';
@ -24,20 +25,21 @@ interface TextAreaProps extends Editor, ErrorProcessing, Titled, React.Component
export function TextArea({
id,
label,
required,
transparent,
rows,
dense,
noBorder,
noOutline,
noResize,
className,
fitContent,
disabled,
error,
...restProps
}: TextAreaProps) {
return (
<div
className={cn(
className={clsx(
'w-full', //
dense ? 'flex grow items-center gap-3' : 'flex flex-col',
dense && className
@ -46,20 +48,21 @@ export function TextArea({
<Label text={label} htmlFor={id} />
<textarea
id={id}
className={cn(
className={clsx(
'px-3 py-2',
'leading-tight',
'overflow-x-hidden overflow-y-auto',
!noBorder && 'border',
fitContent && 'field-sizing-content',
noResize && 'resize-none',
transparent || disabled ? 'bg-transparent' : 'bg-input',
transparent ? 'bg-transparent' : 'clr-input',
!noOutline && 'focus-outline',
dense && 'grow max-w-full',
!dense && !!label && 'mt-2',
!dense && className
)}
disabled={disabled}
rows={rows}
required={required}
{...restProps}
/>
<ErrorField className='mt-1' error={error} />

View File

@ -1,5 +1,6 @@
import clsx from 'clsx';
import { type Editor, type ErrorProcessing, type Titled } from '../props';
import { cn } from '../utils';
import { ErrorField } from './error-field';
import { Label } from './label';
@ -40,7 +41,7 @@ export function TextInput({
}: TextInputProps) {
return (
<div
className={cn(
className={clsx(
dense ? 'flex items-center gap-3' : 'flex flex-col', //
dense && className
)}
@ -48,10 +49,10 @@ export function TextInput({
<Label text={label} htmlFor={id} />
<input
id={id}
className={cn(
className={clsx(
'min-w-0 py-2',
'leading-tight truncate hover:text-clip',
transparent || disabled ? 'bg-transparent' : 'bg-input',
transparent ? 'bg-transparent' : 'clr-input',
!noBorder && 'border',
!noOutline && 'focus-outline',
(!noBorder || !disabled) && 'px-3',

View File

@ -8,7 +8,7 @@ export function ModalBackdrop({ onHide }: ModalBackdropProps) {
return (
<>
<div className='z-bottom fixed inset-0 backdrop-blur-[3px] opacity-50' />
<div className='z-bottom fixed inset-0 bg-popover opacity-25' onClick={onHide} />
<div className='z-bottom fixed inset-0 bg-prim-0 opacity-25' onClick={onHide} />
</>
);
}

View File

@ -1,5 +1,7 @@
'use client';
import clsx from 'clsx';
import { type HelpTopic } from '@/features/help';
import { BadgeHelp } from '@/features/help/components';
@ -10,7 +12,6 @@ import { prepareTooltip } from '@/utils/utils';
import { Button, MiniButton, SubmitButton } from '../control';
import { IconClose } from '../icons';
import { type Styling } from '../props';
import { cn } from '../utils';
import { ModalBackdrop } from './modal-backdrop';
@ -89,7 +90,7 @@ export function ModalForm({
<div className='cc-modal-wrapper'>
<ModalBackdrop onHide={handleCancel} />
<form
className='cc-animate-modal relative grid border rounded-xl bg-background'
className='cc-animate-modal relative grid border rounded-xl bg-prim-100'
role='dialog'
onSubmit={handleSubmit}
aria-labelledby='modal-title'
@ -119,7 +120,7 @@ export function ModalForm({
) : null}
<div
className={cn(
className={clsx(
'@container/modal',
'max-h-[calc(100svh-8rem)] max-w-[100svw] xs:max-w-[calc(100svw-2rem)]',
'overscroll-contain outline-hidden',

View File

@ -6,7 +6,7 @@ export function ModalLoader() {
return (
<div className='cc-modal-wrapper'>
<ModalBackdrop />
<div className='cc-animate-modal p-20 border rounded-xl bg-background'>
<div className='cc-animate-modal p-20 border rounded-xl bg-prim-100'>
<Loader circular scale={6} />
</div>
</div>

View File

@ -10,7 +10,6 @@ import { prepareTooltip } from '@/utils/utils';
import { Button, MiniButton } from '../control';
import { IconClose } from '../icons';
import { cn } from '../utils';
import { ModalBackdrop } from './modal-backdrop';
import { type ModalProps } from './modal-form';
@ -39,7 +38,7 @@ export function ModalView({
return (
<div className='cc-modal-wrapper'>
<ModalBackdrop onHide={hideDialog} />
<div className='cc-animate-modal relative grid border rounded-xl bg-background' role='dialog'>
<div className='cc-animate-modal relative grid border rounded-xl bg-prim-100' role='dialog'>
{helpTopic && !hideHelpWhen?.() ? (
<BadgeHelp
topic={helpTopic}
@ -62,8 +61,7 @@ export function ModalView({
<h1
className={clsx(
'px-12 py-2 select-none',
fullScreen &&
'z-pop absolute top-0 right-1/2 translate-x-1/2 backdrop-blur-xs bg-background/90 rounded-2xl'
fullScreen && 'z-pop absolute top-0 right-1/2 translate-x-1/2 backdrop-blur-xs bg-prim-100/90 rounded-2xl'
)}
>
{header}
@ -71,7 +69,7 @@ export function ModalView({
) : null}
<div
className={cn(
className={clsx(
'@container/modal',
'max-w-[100svw] xs:max-w-[calc(100svw-2rem)]',
'overscroll-contain outline-hidden',
@ -95,7 +93,7 @@ export function ModalView({
onClick={hideDialog}
/>
) : (
<div className='z-pop absolute bottom-0 right-1/2 translate-x-1/2 p-3 rounded-xl bg-background/90 backdrop-blur-xs'>
<div className='z-pop absolute bottom-0 right-1/2 translate-x-1/2 p-3 rounded-xl bg-prim-100/90 backdrop-blur-xs'>
{' '}
<Button text='Закрыть' aria-label='Закрыть' className='text-sm min-w-28' onClick={hideDialog} />
</div>

View File

@ -28,7 +28,7 @@ export function TabLabel({
className={clsx(
'min-w-20 h-full',
'px-2 py-1 flex justify-center',
'cc-hover cc-animate-color duration-150',
'clr-hover cc-animate-color duration-150',
'text-sm whitespace-nowrap font-controls',
'select-none hover:cursor-pointer',
'outline-hidden',

View File

@ -1,131 +0,0 @@
import { Command as CommandPrimitive } from 'cmdk';
import { SearchIcon } from 'lucide-react';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { cn } from '../utils';
function Command({ className, ...props }: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot='command'
className={cn(
'bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md',
className
)}
{...props}
/>
);
}
function CommandDialog({
title = 'Command Palette',
description = 'Search for a command to run...',
children,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string;
description?: string;
}) {
return (
<Dialog {...props}>
<DialogHeader className='sr-only'>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent className='overflow-hidden p-0'>
<Command className='[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5'>
{children}
</Command>
</DialogContent>
</Dialog>
);
}
function CommandInput({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div data-slot='command-input-wrapper' className='flex h-9 items-center gap-2 border-b px-3'>
<SearchIcon className='size-4 shrink-0 opacity-50' />
<CommandPrimitive.Input
data-slot='command-input'
className={cn(
'placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...props}
/>
</div>
);
}
function CommandList({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot='command-list'
className={cn('max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto', className)}
{...props}
/>
);
}
function CommandEmpty({ ...props }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return <CommandPrimitive.Empty data-slot='command-empty' className='py-6 text-center text-sm' {...props} />;
}
function CommandGroup({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot='command-group'
className={cn(
'text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium',
className
)}
{...props}
/>
);
}
function CommandSeparator({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot='command-separator'
className={cn('bg-border -mx-1 h-px', className)}
{...props}
/>
);
}
function CommandItem({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot='command-item'
className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
);
}
function CommandShortcut({ className, ...props }: React.ComponentProps<'span'>) {
return (
<span
data-slot='command-shortcut'
className={cn('text-muted-foreground ml-auto text-xs tracking-widest', className)}
{...props}
/>
);
}
export {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut
};

View File

@ -1,110 +0,0 @@
'use client';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { XIcon } from 'lucide-react';
import { cn } from '../utils';
function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot='dialog' {...props} />;
}
function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot='dialog-trigger' {...props} />;
}
function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot='dialog-portal' {...props} />;
}
function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot='dialog-close' {...props} />;
}
function DialogOverlay({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot='dialog-overlay'
className={cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-topmost bg-black/50',
className
)}
{...props}
/>
);
}
function DialogContent({ className, children, ...props }: React.ComponentProps<typeof DialogPrimitive.Content>) {
return (
<DialogPortal data-slot='dialog-portal'>
<DialogOverlay />
<DialogPrimitive.Content
data-slot='dialog-content'
className={cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
<XIcon />
<span className='sr-only'>Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
);
}
function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='dialog-header'
className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
{...props}
/>
);
}
function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='dialog-footer'
className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
{...props}
/>
);
}
function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot='dialog-title'
className={cn('text-lg leading-none font-semibold', className)}
{...props}
/>
);
}
function DialogDescription({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot='dialog-description'
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
);
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger
};

View File

@ -1,39 +0,0 @@
import * as PopoverPrimitive from '@radix-ui/react-popover';
import { cn } from '../utils';
function Popover({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot='popover' {...props} />;
}
function PopoverTrigger({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot='popover-trigger' {...props} />;
}
function PopoverContent({
className,
align = 'center',
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot='popover-content'
align={align}
sideOffset={sideOffset}
className={cn(
'z-topmost bg-popover text-popover-foreground cc-animate-popover w-72 border p-4 shadow-md outline-hidden',
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
);
}
function PopoverAnchor({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot='popover-anchor' {...props} />;
}
export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger };

View File

@ -1,6 +0,0 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@ -18,7 +18,7 @@ export function Indicator({ icon, title, titleHtml, hideTitle, noPadding, classN
return (
<div
className={clsx(
'cc-controls', //
'clr-text-controls', //
'outline-hidden',
!noPadding && 'px-1 py-1',
className

View File

@ -24,7 +24,7 @@ export function ExpectedAnonymous() {
<span> | </span>
<TextURL text='Справка' href='/manuals' />
<span> | </span>
<span className='cursor-pointer hover:underline text-primary' aria-label='Выйти' onClick={logoutAndRedirect}>
<span className='cursor-pointer hover:underline text-sec-600' onClick={logoutAndRedirect}>
Выйти
</span>
</div>

View File

@ -95,7 +95,7 @@ export function LoginPage() {
function ServerError({ error }: { error: ErrorData }): React.ReactElement | null {
if (isAxiosError(error) && error.response && error.response.status === 400) {
return (
<div className='text-sm select-text text-destructive'>
<div className='text-sm select-text text-warn-600'>
На Портале отсутствует такое сочетание имени пользователя и пароля
</div>
);

View File

@ -88,7 +88,7 @@ export function Component() {
// ====== Internals =========
function ServerError({ error }: { error: ErrorData }): React.ReactElement {
if (isAxiosError(error) && error.response && error.response.status === 404) {
return <div className='mx-auto mt-6 text-sm select-text text-destructive'>Данная ссылка не действительна</div>;
return <div className='mx-auto mt-6 text-sm select-text text-warn-600'>Данная ссылка не действительна</div>;
}
return <InfoError error={error} />;
}

View File

@ -54,7 +54,7 @@ export function Component() {
function ServerError({ error }: { error: ErrorData }): React.ReactElement {
if (isAxiosError(error) && error.response && error.response.status === 400) {
return (
<div className='mx-auto mt-6 text-sm select-text text-destructive'>Данный email не используется на Портале.</div>
<div className='mx-auto mt-6 text-sm select-text text-warn-600'>Данный email не используется на Портале.</div>
);
}
throw error as Error;

View File

@ -1,11 +1,11 @@
import React, { Suspense } from 'react';
import clsx from 'clsx';
import { type PlacesType, Tooltip } from '@/components/container';
import { TextURL } from '@/components/control';
import { IconHelp } from '@/components/icons';
import { Loader } from '@/components/loader';
import { type Styling } from '@/components/props';
import { cn } from '@/components/utils';
import { usePreferencesStore } from '@/stores/preferences';
import { type HelpTopic } from '../models/help-topic';
@ -41,17 +41,17 @@ export function BadgeHelp({ topic, padding = 'p-1', className, contentClass, sty
return null;
}
return (
<div tabIndex={-1} id={`help-${topic}`} className={cn(padding, className)} style={style}>
<div tabIndex={-1} id={`help-${topic}`} className={clsx(padding, className)} style={style}>
<IconHelp size='1.25rem' className='icon-primary' />
<Tooltip
clickable
anchorSelect={`#help-${topic}`}
layer='z-topmost'
className={cn('max-w-120', contentClass)}
className={clsx('max-w-120', contentClass)}
{...restProps}
>
<Suspense fallback={<Loader />}>
<div className='absolute right-1 text-sm top-2 bg-input' onClick={event => event.stopPropagation()}>
<div className='absolute right-1 text-sm top-2 clr-input' onClick={event => event.stopPropagation()}>
<TextURL text='Справка...' href={`/manuals?topic=${topic}`} />
</div>
<TopicPage topic={topic} />

View File

@ -25,7 +25,7 @@ export function HelpFormulaTree() {
<span className='bg-(--acc-bg-red)'>присвоение и итерация</span>
</li>
<li>
<span className='bg-secondary'>составные выражения</span>
<span className='bg-prim-300'>составные выражения</span>
</li>
</div>
);

View File

@ -81,7 +81,7 @@ export function HelpLibrary() {
<kbd>клик</kbd> по иконке сворачивает/разворачивает вложенные
</li>
<li>
<IconFolderEmpty size='1rem' className='inline-icon text-foreground' /> папка без схем
<IconFolderEmpty size='1rem' className='inline-icon clr-text-default' /> папка без схем
</li>
<li>
<IconFolderEmpty size='1rem' className='inline-icon' /> папка с вложенными без схем

View File

@ -68,7 +68,7 @@ export function HelpRSEditor() {
<IconChild className='inline-icon' /> отображение наследованных
</li>
<li>
<span className='bg-selected'>текущая конституента</span>
<span className='bg-sec-200'>текущая конституента</span>
</li>
<li>
<span className='bg-(--acc-bg-green50)'>

View File

@ -5,7 +5,6 @@ import {
IconEdit,
IconFilter,
IconFitImage,
IconFocus,
IconGraphCollapse,
IconGraphCore,
IconGraphExpand,
@ -48,7 +47,9 @@ export function HelpRSGraphTerm() {
<div className='sm:w-84'>
<h1>Изменение узлов</h1>
<li>Клик на узел выделение</li>
<li>Левый клик выбор фокус-конституенты</li>
<li>
Левый клик выбор <span className='text-(--acc-fg-purple)'>фокус-конституенты</span>
</li>
<li>
<IconReset className='inline-icon' /> Esc сбросить выделение
</li>
@ -75,9 +76,6 @@ export function HelpRSGraphTerm() {
<li>
<IconFilter className='inline-icon' /> Открыть настройки
</li>
<li>
<IconFocus className='inline-icon' /> Задать фокус
</li>
<li>
<IconFitImage className='inline-icon' /> Вписать в экран
</li>

View File

@ -42,6 +42,7 @@ export function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownPro
>
<Button
noOutline
tabIndex={-1}
title='Список тем'
hideTitle={menu.isOpen}
icon={!menu.isOpen ? <IconMenuUnfold size='1.25rem' /> : <IconMenuFold size='1.25rem' />}

View File

@ -20,7 +20,7 @@ export const useSetAccessPolicy = () => {
client.setQueryData(ossKey, { ...ossData, access_policy: variables.policy });
return Promise.allSettled([
client.invalidateQueries({ queryKey: KEYS.composite.libraryList }),
...ossData.operations
...ossData.items
.map(item => {
if (!item.result) {
return;

View File

@ -18,7 +18,7 @@ export const useSetEditors = () => {
if (ossData) {
client.setQueryData(ossKey, { ...ossData, editors: variables.editors });
return Promise.allSettled(
ossData.operations
ossData.items
.map(item => {
if (!item.result) {
return;

View File

@ -20,7 +20,7 @@ export const useSetLocation = () => {
client.setQueryData(ossKey, { ...ossData, location: variables.location });
return Promise.allSettled([
client.invalidateQueries({ queryKey: libraryApi.libraryListKey }),
...ossData.operations
...ossData.items
.map(item => {
if (!item.result) {
return;

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