Compare commits

..

30 Commits

Author SHA1 Message Date
Ivan
0bdfe67fb1 F: Implement create-block API
Some checks failed
Backend CI / build (3.12) (push) Waiting to run
Backend CI / notify-failure (push) Blocked by required conditions
Frontend CI / build (22.x) (push) Has been cancelled
Frontend CI / notify-failure (push) Has been cancelled
2025-04-14 23:09:10 +03:00
Ivan
1f15a6a53c R: Remove unused dependencies 2025-04-13 23:13:04 +03:00
Ivan
7e0c59709d M: Minor UI fixes and refactoring 2025-04-13 22:28:28 +03:00
Ivan
cc119c73a4 F: More color simplifications 2025-04-13 19:15:54 +03:00
Ivan
873a26483c F: Replace react-select with shadcn based analogue 2025-04-13 17:17:25 +03:00
Ivan
efd143ae94 F: Merge with shadcn styling 2025-04-12 21:47:46 +03:00
Ivan
e7ee9d9667 B: Fix combo-box for release 2025-04-11 21:45:05 +03:00
Ivan
e03f9e747e npm update 2025-04-11 20:06:12 +03:00
Ivan
ad0baf1952 Update settings.json 2025-04-11 20:01:18 +03:00
Ivan
b91ec793e9 F: Replace SelectSingle with combobox 2025-04-11 19:59:08 +03:00
Ivan
bcd54d22b6 F: Implement combo-box 2025-04-10 17:52:36 +03:00
Ivan
91261a745b F: Improve shadcn Select integration 2025-04-10 11:07:04 +03:00
Ivan
5c75b01646 F: Use radix-ui select element for versionSelector 2025-04-09 20:08:36 +03:00
Ivan
c78834aa5b B: Fix search bar width 2025-04-08 22:32:52 +03:00
Ivan
422f1bb49a F: Various UI fixes 2025-04-08 12:03:36 +03:00
Ivan
27dcb3a4cc M: Improve color animations 2025-04-07 22:25:53 +03:00
Ivan
39c3a84c9a M: Add debounce effect to node selection 2025-04-07 22:06:00 +03:00
Ivan
531c44d3c8 R: Split context and state dependencies to improve hot reload 2025-04-07 21:46:19 +03:00
Ivan
e308a52b35 M: Sort users in select user component 2025-04-07 21:43:20 +03:00
Ivan
040601b16b F: Introduce shadcn, implement okclh colors 2025-04-07 15:25:23 +03:00
Ivan
720d78e79c npm update 2025-04-06 16:01:39 +03:00
Ivan
5efce874b2 R: Restructuring layout data pt2 2025-04-06 15:47:40 +03:00
Ivan
3271d9244c R: Restructuring layout data pt1 2025-04-06 13:28:00 +03:00
Ivan
f1faffd063 R: Fix single test runner 2025-04-06 13:21:54 +03:00
Ivan
4dc8673ae4 Update use-escape-key.ts 2025-04-02 18:45:39 +03:00
Ivan
20b56f59b4 B: Prevent default behavior when ESC is intercepted 2025-04-02 17:43:15 +03:00
Ivan
b29a9d603f M: Fix copyright 2025-03-28 18:39:21 +03:00
Ivan
3a99f47998 update dependencies 2025-03-25 23:13:11 +03:00
Ivan
f4ee1fac6c Merge branch 'main' of http://dev.concept.ru:3000/ConceptProd/Portal 2025-03-25 23:03:23 +03:00
Ivan
37ed795ad1 M: Improve fullname label and fix clone defaults 2025-03-25 23:03:09 +03:00
225 changed files with 5128 additions and 3307 deletions

4
.vscode/launch.json vendored
View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@ from apps.library.models import (
LibraryTemplate, LibraryTemplate,
LocationHead LocationHead
) )
from apps.oss.models import OperationSchema
from apps.rsform.models import RSForm from apps.rsform.models import RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint from shared.EndpointTester import EndpointTester, decl_endpoint
from shared.testing_utils import response_contains from shared.testing_utils import response_contains
@ -58,6 +59,8 @@ class TestLibraryViewset(EndpointTester):
'read_only': True 'read_only': True
} }
response = self.executeCreated(data=data) 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['owner'], self.user.pk)
self.assertEqual(response.data['item_type'], data['item_type']) self.assertEqual(response.data['item_type'], data['item_type'])
self.assertEqual(response.data['title'], data['title']) self.assertEqual(response.data['title'], data['title'])
@ -65,6 +68,8 @@ class TestLibraryViewset(EndpointTester):
self.assertEqual(response.data['access_policy'], data['access_policy']) self.assertEqual(response.data['access_policy'], data['access_policy'])
self.assertEqual(response.data['visible'], data['visible']) self.assertEqual(response.data['visible'], data['visible'])
self.assertEqual(response.data['read_only'], data['read_only']) self.assertEqual(response.data['read_only'], data['read_only'])
self.assertEqual(oss.layout().data['operations'], [])
self.assertEqual(oss.layout().data['blocks'], [])
self.logout() self.logout()
data = {'title': 'Title2'} data = {'title': 'Title2'}
@ -359,7 +364,7 @@ class TestLibraryViewset(EndpointTester):
data = {'title': 'Title1340', 'items': []} data = {'title': 'Title1340', 'items': []}
response = self.executeCreated(data=data, item=self.owned.pk) response = self.executeCreated(data=data, item=self.owned.pk)
self.assertEqual(response.data['title'], data['title']) self.assertEqual(response.data['title'], data['title'])
self.assertEqual(len(response.data['items']), 0) self.assertEqual(len(response.data['items']), 2)
data = {'title': 'Title1341', 'items': [x12.pk]} data = {'title': 'Title1341', 'items': [x12.pk]}
response = self.executeCreated(data=data, item=self.owned.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.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from apps.oss.models import Operation, OperationSchema, PropagationFacade from apps.oss.models import Layout, Operation, OperationSchema, PropagationFacade
from apps.rsform.models import RSForm from apps.rsform.models import RSForm
from apps.rsform.serializers import RSFormParseSerializer from apps.rsform.serializers import RSFormParseSerializer
from apps.users.models import User from apps.users.models import User
@ -40,6 +40,8 @@ class LibraryViewSet(viewsets.ModelViewSet):
serializer.save(owner=self.request.user) serializer.save(owner=self.request.user)
else: else:
serializer.save() 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: def perform_update(self, serializer) -> None:
instance = serializer.save() instance = serializer.save()

View File

@ -15,11 +15,24 @@ class OperationAdmin(admin.ModelAdmin):
'alias', 'alias',
'title', 'title',
'description', 'description',
'position_x', 'parent']
'position_y']
search_fields = ['id', 'operation_type', 'title', 'alias'] 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): class ArgumentAdmin(admin.ModelAdmin):
''' Admin model: Operation arguments. ''' ''' Admin model: Operation arguments. '''
ordering = ['operation'] ordering = ['operation']
@ -42,6 +55,8 @@ class InheritanceAdmin(admin.ModelAdmin):
admin.site.register(models.Operation, OperationAdmin) 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.Argument, ArgumentAdmin)
admin.site.register(models.Substitution, SynthesisSubstitutionAdmin) admin.site.register(models.Substitution, SynthesisSubstitutionAdmin)
admin.site.register(models.Inheritance, InheritanceAdmin) admin.site.register(models.Inheritance, InheritanceAdmin)

View File

@ -0,0 +1,84 @@
# 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

@ -0,0 +1,39 @@
''' 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

@ -0,0 +1,25 @@
''' 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. ''' ''' Models: Operation in OSS. '''
# pylint: disable=duplicate-code
from django.db.models import ( from django.db.models import (
CASCADE, CASCADE,
SET_NULL, SET_NULL,
CharField, CharField,
FloatField,
ForeignKey, ForeignKey,
Model, Model,
QuerySet, QuerySet,
@ -44,6 +44,15 @@ class Operation(Model):
related_name='producer' 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( alias = CharField(
verbose_name='Шифр', verbose_name='Шифр',
max_length=255, max_length=255,
@ -58,15 +67,6 @@ class Operation(Model):
blank=True blank=True
) )
position_x = FloatField(
verbose_name='Положение по горизонтали',
default=0
)
position_y = FloatField(
verbose_name='Положение по вертикали',
default=0
)
class Meta: class Meta:
''' Model metadata. ''' ''' Model metadata. '''
verbose_name = 'Операция' verbose_name = 'Операция'

View File

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

View File

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

View File

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

View File

@ -2,17 +2,29 @@
from rest_framework import serializers from rest_framework import serializers
class OperationPositionSerializer(serializers.Serializer): class OperationNodeSerializer(serializers.Serializer):
''' Operation position. ''' ''' Operation position. '''
id = serializers.IntegerField() id = serializers.IntegerField()
position_x = serializers.FloatField() x = serializers.FloatField()
position_y = serializers.FloatField() y = serializers.FloatField()
class PositionsSerializer(serializers.Serializer): class BlockNodeSerializer(serializers.Serializer):
''' Operations position for OperationSchema. ''' ''' Block position. '''
positions = serializers.ListField( id = serializers.IntegerField()
child=OperationPositionSerializer() 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()
) )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,167 @@
''' 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

@ -0,0 +1,488 @@
''' 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. ''' ''' Testing API: Operation Schema. '''
from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType from apps.library.models import AccessPolicy, LibraryItemType
from apps.oss.models import Operation, OperationSchema, OperationType from apps.oss.models import OperationSchema, OperationType
from apps.rsform.models import Constituenta, RSForm from apps.rsform.models import Constituenta, RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint from shared.EndpointTester import EndpointTester, decl_endpoint
@ -16,7 +16,8 @@ class TestOssViewset(EndpointTester):
self.unowned_id = self.unowned.model.pk self.unowned_id = self.unowned.model.pk
self.private = OperationSchema.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE) self.private = OperationSchema.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE)
self.private_id = self.private.model.pk self.private_id = self.private.model.pk
self.invalid_id = self.private.model.pk + 1337 self.invalid_id = self.private_id + 1337
def populateData(self): def populateData(self):
self.ks1 = RSForm.create( self.ks1 = RSForm.create(
@ -54,12 +55,21 @@ class TestOssViewset(EndpointTester):
alias='3', alias='3',
operation_type=OperationType.SYNTHESIS 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_arguments(self.operation3.pk, [self.operation1, self.operation2])
self.owned.set_substitutions(self.operation3.pk, [{ self.owned.set_substitutions(self.operation3.pk, [{
'original': self.ks1X1, 'original': self.ks1X1,
'substitution': self.ks2X1 'substitution': self.ks2X1
}]) }])
@decl_endpoint('/api/oss/{item}/details', method='get') @decl_endpoint('/api/oss/{item}/details', method='get')
def test_details(self): def test_details(self):
self.populateData() self.populateData()
@ -74,9 +84,9 @@ class TestOssViewset(EndpointTester):
self.assertEqual(response.data['item_type'], LibraryItemType.OPERATION_SCHEMA) self.assertEqual(response.data['item_type'], LibraryItemType.OPERATION_SCHEMA)
self.assertEqual(len(response.data['items']), 3) self.assertEqual(len(response.data['operations']), 3)
self.assertEqual(response.data['items'][0]['id'], self.operation1.pk) self.assertEqual(response.data['operations'][0]['id'], self.operation1.pk)
self.assertEqual(response.data['items'][0]['operation_type'], self.operation1.operation_type) self.assertEqual(response.data['operations'][0]['operation_type'], self.operation1.operation_type)
self.assertEqual(len(response.data['substitutions']), 1) self.assertEqual(len(response.data['substitutions']), 1)
sub = response.data['substitutions'][0] sub = response.data['substitutions'][0]
@ -95,6 +105,12 @@ class TestOssViewset(EndpointTester):
self.assertEqual(arguments[1]['operation'], self.operation3.pk) self.assertEqual(arguments[1]['operation'], self.operation3.pk)
self.assertEqual(arguments[1]['argument'], self.operation2.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.executeOK(item=self.unowned_id)
self.executeForbidden(item=self.private_id) self.executeForbidden(item=self.private_id)
@ -103,400 +119,33 @@ class TestOssViewset(EndpointTester):
self.executeOK(item=self.unowned_id) self.executeOK(item=self.unowned_id)
self.executeForbidden(item=self.private_id) self.executeForbidden(item=self.private_id)
@decl_endpoint('/api/oss/{item}/update-positions', method='patch')
def test_update_positions(self): @decl_endpoint('/api/oss/{item}/update-layout', method='patch')
def test_update_layout(self):
self.populateData() self.populateData()
self.executeBadData(item=self.owned_id) self.executeBadData(item=self.owned_id)
data = {'positions': []} data = {'operations': [], 'blocks': []}
self.executeOK(data=data) self.executeOK(data=data)
data = {'positions': [ data = {
{'id': self.operation1.pk, 'position_x': 42.1, 'position_y': 1337}, 'operations': [
{'id': self.operation2.pk, 'position_x': 36.1, 'position_y': 1437}, {'id': self.operation1.pk, 'x': 42.1, 'y': 1337},
{'id': self.invalid_id, 'position_x': 31, 'position_y': 12}, {'id': self.operation2.pk, 'x': 36.1, 'y': 1437},
]} {'id': self.operation3.pk, 'x': 36.1, 'y': 1435}
], 'blocks': []
}
self.toggle_admin(True) self.toggle_admin(True)
self.executeOK(data=data, item=self.unowned_id) self.executeOK(data=data, item=self.unowned_id)
self.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.toggle_admin(False)
self.executeOK(data=data, item=self.owned_id) self.executeOK(data=data, item=self.owned_id)
self.operation1.refresh_from_db() self.owned.refresh_from_db()
self.operation2.refresh_from_db() self.assertEqual(self.owned.layout().data, data)
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.unowned_id)
self.executeForbidden(data=data, item=self.private_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') @decl_endpoint('/api/oss/get-predecessor', method='post')
def test_get_predecessor(self): def test_get_predecessor(self):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -146,6 +146,7 @@ export { BiGitMerge as IconGraphOutputs } from 'react-icons/bi';
export { LuAtom as IconGraphCore } from 'react-icons/lu'; export { LuAtom as IconGraphCore } from 'react-icons/lu';
export { LuRotate3D as IconRotate3D } from 'react-icons/lu'; export { LuRotate3D as IconRotate3D } from 'react-icons/lu';
export { MdOutlineFitScreen as IconFitImage } from 'react-icons/md'; 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 { LuSparkles as IconClustering } from 'react-icons/lu';
export { LuSparkle as IconClusteringOff } from 'react-icons/lu'; export { LuSparkle as IconClusteringOff } from 'react-icons/lu';
export { TbGridDots as IconGrid } from 'react-icons/tb'; export { TbGridDots as IconGrid } from 'react-icons/tb';

View File

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

View File

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

View File

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

View File

@ -0,0 +1,127 @@
'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

@ -0,0 +1,145 @@
'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 null;
} }
return ( return (
<div className={clsx('text-sm text-warn-600 select-none', className)} {...restProps}> <div className={clsx('text-sm text-destructive select-none', className)} {...restProps}>
{error.message} {error.message}
</div> </div>
); );

View File

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

View File

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

View File

@ -1,128 +0,0 @@
'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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,131 @@
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

@ -0,0 +1,110 @@
'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

@ -0,0 +1,39 @@
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

@ -0,0 +1,6 @@
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 ( return (
<div <div
className={clsx( className={clsx(
'clr-text-controls', // 'cc-controls', //
'outline-hidden', 'outline-hidden',
!noPadding && 'px-1 py-1', !noPadding && 'px-1 py-1',
className className

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -42,7 +42,6 @@ export function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownPro
> >
<Button <Button
noOutline noOutline
tabIndex={-1}
title='Список тем' title='Список тем'
hideTitle={menu.isOpen} hideTitle={menu.isOpen}
icon={!menu.isOpen ? <IconMenuUnfold size='1.25rem' /> : <IconMenuFold size='1.25rem' />} 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 }); client.setQueryData(ossKey, { ...ossData, access_policy: variables.policy });
return Promise.allSettled([ return Promise.allSettled([
client.invalidateQueries({ queryKey: KEYS.composite.libraryList }), client.invalidateQueries({ queryKey: KEYS.composite.libraryList }),
...ossData.items ...ossData.operations
.map(item => { .map(item => {
if (!item.result) { if (!item.result) {
return; return;

View File

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

View File

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

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