Compare commits
30 Commits
e8509e44b1
...
0bdfe67fb1
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0bdfe67fb1 | ||
![]() |
1f15a6a53c | ||
![]() |
7e0c59709d | ||
![]() |
cc119c73a4 | ||
![]() |
873a26483c | ||
![]() |
efd143ae94 | ||
![]() |
e7ee9d9667 | ||
![]() |
e03f9e747e | ||
![]() |
ad0baf1952 | ||
![]() |
b91ec793e9 | ||
![]() |
bcd54d22b6 | ||
![]() |
91261a745b | ||
![]() |
5c75b01646 | ||
![]() |
c78834aa5b | ||
![]() |
422f1bb49a | ||
![]() |
27dcb3a4cc | ||
![]() |
39c3a84c9a | ||
![]() |
531c44d3c8 | ||
![]() |
e308a52b35 | ||
![]() |
040601b16b | ||
![]() |
720d78e79c | ||
![]() |
5efce874b2 | ||
![]() |
3271d9244c | ||
![]() |
f1faffd063 | ||
![]() |
4dc8673ae4 | ||
![]() |
20b56f59b4 | ||
![]() |
b29a9d603f | ||
![]() |
3a99f47998 | ||
![]() |
f4ee1fac6c | ||
![]() |
37ed795ad1 |
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
|
@ -42,8 +42,8 @@
|
|||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceFolder}/rsconcept/backend",
|
||||
"program": "${workspaceFolder}/rsconcept/backend/manage.py",
|
||||
"args": ["test", "-k", "${fileBasenameNoExtension}"],
|
||||
"program": "${workspaceFolder}/rsconcept/backend/run_testfile.py",
|
||||
"args": ["${file}"],
|
||||
"django": true
|
||||
},
|
||||
{
|
||||
|
|
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
|
@ -21,6 +21,9 @@
|
|||
"changeProcessCWD": true
|
||||
}
|
||||
],
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
|
||||
],
|
||||
"stylelint.enable": true,
|
||||
"autopep8.args": [
|
||||
"--max-line-length",
|
||||
|
@ -79,6 +82,7 @@
|
|||
"Certbot",
|
||||
"CIHT",
|
||||
"clsx",
|
||||
"cmdk",
|
||||
"codemirror",
|
||||
"Constituenta",
|
||||
"corsheaders",
|
||||
|
@ -161,6 +165,7 @@
|
|||
"rstabs",
|
||||
"rstemplates",
|
||||
"setexpr",
|
||||
"shadcn",
|
||||
"SIDELIST",
|
||||
"signup",
|
||||
"simplebezier",
|
||||
|
|
|
@ -35,7 +35,6 @@ This readme file is used mostly to document project dependencies and conventions
|
|||
- react-toastify
|
||||
- react-tabs
|
||||
- react-intl
|
||||
- react-select
|
||||
- react-error-boundary
|
||||
- react-tooltip
|
||||
- react-zoom-pan-pinch
|
||||
|
@ -70,6 +69,7 @@ This readme file is used mostly to document project dependencies and conventions
|
|||
- babel-plugin-react-compiler
|
||||
- vite
|
||||
- jest
|
||||
- shadcn
|
||||
- ts-jest
|
||||
- stylelint
|
||||
- stylelint-config-recommended
|
||||
|
|
2
TODO.txt
2
TODO.txt
|
@ -31,11 +31,9 @@ User profile:
|
|||
- Sitemap for better SEO and crawler optimization
|
||||
|
||||
[Functionality - CANCELED]
|
||||
- User notifications on edit - consider spam prevention and change aggregation
|
||||
- Integrate socials and feedback
|
||||
- Content based search in Library
|
||||
- Private projects. Consider cooperative editing
|
||||
- OSS: synthesis table: auto substitution for diamond synthesis
|
||||
|
||||
|
||||
[Tech]
|
||||
|
|
|
@ -9,6 +9,7 @@ from apps.library.models import (
|
|||
LibraryTemplate,
|
||||
LocationHead
|
||||
)
|
||||
from apps.oss.models import OperationSchema
|
||||
from apps.rsform.models import RSForm
|
||||
from shared.EndpointTester import EndpointTester, decl_endpoint
|
||||
from shared.testing_utils import response_contains
|
||||
|
@ -58,6 +59,8 @@ class TestLibraryViewset(EndpointTester):
|
|||
'read_only': True
|
||||
}
|
||||
response = self.executeCreated(data=data)
|
||||
oss = OperationSchema(LibraryItem.objects.get(pk=response.data['id']))
|
||||
self.assertEqual(oss.model.owner, self.user)
|
||||
self.assertEqual(response.data['owner'], self.user.pk)
|
||||
self.assertEqual(response.data['item_type'], data['item_type'])
|
||||
self.assertEqual(response.data['title'], data['title'])
|
||||
|
@ -65,6 +68,8 @@ class TestLibraryViewset(EndpointTester):
|
|||
self.assertEqual(response.data['access_policy'], data['access_policy'])
|
||||
self.assertEqual(response.data['visible'], data['visible'])
|
||||
self.assertEqual(response.data['read_only'], data['read_only'])
|
||||
self.assertEqual(oss.layout().data['operations'], [])
|
||||
self.assertEqual(oss.layout().data['blocks'], [])
|
||||
|
||||
self.logout()
|
||||
data = {'title': 'Title2'}
|
||||
|
@ -359,7 +364,7 @@ class TestLibraryViewset(EndpointTester):
|
|||
data = {'title': 'Title1340', 'items': []}
|
||||
response = self.executeCreated(data=data, item=self.owned.pk)
|
||||
self.assertEqual(response.data['title'], data['title'])
|
||||
self.assertEqual(len(response.data['items']), 0)
|
||||
self.assertEqual(len(response.data['items']), 2)
|
||||
|
||||
data = {'title': 'Title1341', 'items': [x12.pk]}
|
||||
response = self.executeCreated(data=data, item=self.owned.pk)
|
||||
|
|
|
@ -13,7 +13,7 @@ from rest_framework.decorators import action
|
|||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from apps.oss.models import Operation, OperationSchema, PropagationFacade
|
||||
from apps.oss.models import Layout, Operation, OperationSchema, PropagationFacade
|
||||
from apps.rsform.models import RSForm
|
||||
from apps.rsform.serializers import RSFormParseSerializer
|
||||
from apps.users.models import User
|
||||
|
@ -40,6 +40,8 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
serializer.save(owner=self.request.user)
|
||||
else:
|
||||
serializer.save()
|
||||
if serializer.data.get('item_type') == m.LibraryItemType.OPERATION_SCHEMA:
|
||||
Layout.objects.create(oss=serializer.instance, data={'operations': [], 'blocks': []})
|
||||
|
||||
def perform_update(self, serializer) -> None:
|
||||
instance = serializer.save()
|
||||
|
|
|
@ -15,11 +15,24 @@ class OperationAdmin(admin.ModelAdmin):
|
|||
'alias',
|
||||
'title',
|
||||
'description',
|
||||
'position_x',
|
||||
'position_y']
|
||||
'parent']
|
||||
search_fields = ['id', 'operation_type', 'title', 'alias']
|
||||
|
||||
|
||||
class BlockAdmin(admin.ModelAdmin):
|
||||
''' Admin model: Block. '''
|
||||
ordering = ['oss']
|
||||
list_display = ['id', 'oss', 'title', 'description', 'parent']
|
||||
search_fields = ['oss']
|
||||
|
||||
|
||||
class LayoutAdmin(admin.ModelAdmin):
|
||||
''' Admin model: Layout. '''
|
||||
ordering = ['oss']
|
||||
list_display = ['id', 'oss', 'data']
|
||||
search_fields = ['oss']
|
||||
|
||||
|
||||
class ArgumentAdmin(admin.ModelAdmin):
|
||||
''' Admin model: Operation arguments. '''
|
||||
ordering = ['operation']
|
||||
|
@ -42,6 +55,8 @@ class InheritanceAdmin(admin.ModelAdmin):
|
|||
|
||||
|
||||
admin.site.register(models.Operation, OperationAdmin)
|
||||
admin.site.register(models.Block, BlockAdmin)
|
||||
admin.site.register(models.Layout, LayoutAdmin)
|
||||
admin.site.register(models.Argument, ArgumentAdmin)
|
||||
admin.site.register(models.Substitution, SynthesisSubstitutionAdmin)
|
||||
admin.site.register(models.Inheritance, InheritanceAdmin)
|
||||
|
|
|
@ -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='Содержащий блок'),
|
||||
),
|
||||
]
|
39
rsconcept/backend/apps/oss/models/Block.py
Normal file
39
rsconcept/backend/apps/oss/models/Block.py
Normal 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}'
|
25
rsconcept/backend/apps/oss/models/Layout.py
Normal file
25
rsconcept/backend/apps/oss/models/Layout.py
Normal 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}'
|
|
@ -1,9 +1,9 @@
|
|||
''' Models: Operation in OSS. '''
|
||||
# pylint: disable=duplicate-code
|
||||
from django.db.models import (
|
||||
CASCADE,
|
||||
SET_NULL,
|
||||
CharField,
|
||||
FloatField,
|
||||
ForeignKey,
|
||||
Model,
|
||||
QuerySet,
|
||||
|
@ -44,6 +44,15 @@ class Operation(Model):
|
|||
related_name='producer'
|
||||
)
|
||||
|
||||
parent = ForeignKey(
|
||||
verbose_name='Содержащий блок',
|
||||
to='oss.Block',
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=SET_NULL,
|
||||
related_name='as_child_operation'
|
||||
)
|
||||
|
||||
alias = CharField(
|
||||
verbose_name='Шифр',
|
||||
max_length=255,
|
||||
|
@ -58,15 +67,6 @@ class Operation(Model):
|
|||
blank=True
|
||||
)
|
||||
|
||||
position_x = FloatField(
|
||||
verbose_name='Положение по горизонтали',
|
||||
default=0
|
||||
)
|
||||
position_y = FloatField(
|
||||
verbose_name='Положение по вертикали',
|
||||
default=0
|
||||
)
|
||||
|
||||
class Meta:
|
||||
''' Model metadata. '''
|
||||
verbose_name = 'Операция'
|
||||
|
|
|
@ -19,7 +19,9 @@ from apps.rsform.models import (
|
|||
)
|
||||
|
||||
from .Argument import Argument
|
||||
from .Block import Block
|
||||
from .Inheritance import Inheritance
|
||||
from .Layout import Layout
|
||||
from .Operation import Operation
|
||||
from .Substitution import Substitution
|
||||
|
||||
|
@ -38,6 +40,7 @@ class OperationSchema:
|
|||
def create(**kwargs) -> 'OperationSchema':
|
||||
''' Create LibraryItem via OperationSchema. '''
|
||||
model = LibraryItem.objects.create(item_type=LibraryItemType.OPERATION_SCHEMA, **kwargs)
|
||||
Layout.objects.create(oss=model, data={'operations': [], 'blocks': []})
|
||||
return OperationSchema(model)
|
||||
|
||||
@staticmethod
|
||||
|
@ -58,10 +61,20 @@ class OperationSchema:
|
|||
''' Get QuerySet containing all operations of current OSS. '''
|
||||
return Operation.objects.filter(oss=self.model)
|
||||
|
||||
def blocks(self) -> QuerySet[Block]:
|
||||
''' Get QuerySet containing all blocks of current OSS. '''
|
||||
return Block.objects.filter(oss=self.model)
|
||||
|
||||
def arguments(self) -> QuerySet[Argument]:
|
||||
''' Operation arguments. '''
|
||||
return Argument.objects.filter(operation__oss=self.model)
|
||||
|
||||
def layout(self) -> Layout:
|
||||
''' OSS layout. '''
|
||||
result = Layout.objects.filter(oss=self.model).first()
|
||||
assert result is not None
|
||||
return result
|
||||
|
||||
def substitutions(self) -> QuerySet[Substitution]:
|
||||
''' Operation substitutions. '''
|
||||
return Substitution.objects.filter(operation__oss=self.model)
|
||||
|
@ -78,15 +91,11 @@ class OperationSchema:
|
|||
location=self.model.location
|
||||
)
|
||||
|
||||
def update_positions(self, data: list[dict]) -> None:
|
||||
def update_layout(self, data: dict) -> None:
|
||||
''' Update positions. '''
|
||||
lookup = {x['id']: x for x in data}
|
||||
operations = self.operations()
|
||||
for item in operations:
|
||||
if item.pk in lookup:
|
||||
item.position_x = lookup[item.pk]['position_x']
|
||||
item.position_y = lookup[item.pk]['position_y']
|
||||
Operation.objects.bulk_update(operations, ['position_x', 'position_y'])
|
||||
layout = self.layout()
|
||||
layout.data = data
|
||||
layout.save()
|
||||
|
||||
def create_operation(self, **kwargs) -> Operation:
|
||||
''' Insert new operation. '''
|
||||
|
@ -95,6 +104,12 @@ class OperationSchema:
|
|||
self.save(update_fields=['time_update'])
|
||||
return result
|
||||
|
||||
def create_block(self, **kwargs) -> Block:
|
||||
''' Insert new block. '''
|
||||
result = Block.objects.create(oss=self.model, **kwargs)
|
||||
self.save(update_fields=['time_update'])
|
||||
return result
|
||||
|
||||
def delete_operation(self, target: int, keep_constituents: bool = False):
|
||||
''' Delete operation. '''
|
||||
self.cache.ensure_loaded()
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
''' Django: Models. '''
|
||||
|
||||
from .Argument import Argument
|
||||
from .Block import Block
|
||||
from .Inheritance import Inheritance
|
||||
from .Layout import Layout
|
||||
from .Operation import Operation, OperationType
|
||||
from .OperationSchema import OperationSchema
|
||||
from .PropagationFacade import PropagationFacade
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
''' REST API: Serializers. '''
|
||||
|
||||
from .basics import OperationPositionSerializer, PositionsSerializer, SubstitutionExSerializer
|
||||
from .basics import LayoutSerializer, SubstitutionExSerializer
|
||||
from .data_access import (
|
||||
ArgumentSerializer,
|
||||
BlockCreateSerializer,
|
||||
BlockSerializer,
|
||||
OperationCreateSerializer,
|
||||
OperationDeleteSerializer,
|
||||
OperationSchemaSerializer,
|
||||
|
@ -12,4 +14,9 @@ from .data_access import (
|
|||
RelocateConstituentsSerializer,
|
||||
SetOperationInputSerializer
|
||||
)
|
||||
from .responses import ConstituentaReferenceResponse, NewOperationResponse, NewSchemaResponse
|
||||
from .responses import (
|
||||
ConstituentaReferenceResponse,
|
||||
NewBlockResponse,
|
||||
NewOperationResponse,
|
||||
NewSchemaResponse
|
||||
)
|
||||
|
|
|
@ -2,17 +2,29 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
|
||||
class OperationPositionSerializer(serializers.Serializer):
|
||||
class OperationNodeSerializer(serializers.Serializer):
|
||||
''' Operation position. '''
|
||||
id = serializers.IntegerField()
|
||||
position_x = serializers.FloatField()
|
||||
position_y = serializers.FloatField()
|
||||
x = serializers.FloatField()
|
||||
y = serializers.FloatField()
|
||||
|
||||
|
||||
class PositionsSerializer(serializers.Serializer):
|
||||
''' Operations position for OperationSchema. '''
|
||||
positions = serializers.ListField(
|
||||
child=OperationPositionSerializer()
|
||||
class BlockNodeSerializer(serializers.Serializer):
|
||||
''' Block position. '''
|
||||
id = serializers.IntegerField()
|
||||
x = serializers.FloatField()
|
||||
y = serializers.FloatField()
|
||||
width = serializers.FloatField()
|
||||
height = serializers.FloatField()
|
||||
|
||||
|
||||
class LayoutSerializer(serializers.Serializer):
|
||||
''' Layout for OperationSchema. '''
|
||||
blocks = serializers.ListField(
|
||||
child=BlockNodeSerializer()
|
||||
)
|
||||
operations = serializers.ListField(
|
||||
child=OperationNodeSerializer()
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -11,8 +11,8 @@ from apps.rsform.models import Constituenta
|
|||
from apps.rsform.serializers import SubstitutionSerializerBase
|
||||
from shared import messages as msg
|
||||
|
||||
from ..models import Argument, Inheritance, Operation, OperationSchema, OperationType
|
||||
from .basics import OperationPositionSerializer, SubstitutionExSerializer
|
||||
from ..models import Argument, Block, Inheritance, Operation, OperationSchema, OperationType
|
||||
from .basics import LayoutSerializer, SubstitutionExSerializer
|
||||
|
||||
|
||||
class OperationSerializer(serializers.ModelSerializer):
|
||||
|
@ -24,6 +24,15 @@ class OperationSerializer(serializers.ModelSerializer):
|
|||
read_only_fields = ('id', 'oss')
|
||||
|
||||
|
||||
class BlockSerializer(serializers.ModelSerializer):
|
||||
''' Serializer: Block data. '''
|
||||
class Meta:
|
||||
''' serializer metadata. '''
|
||||
model = Block
|
||||
fields = '__all__'
|
||||
read_only_fields = ('id', 'oss')
|
||||
|
||||
|
||||
class ArgumentSerializer(serializers.ModelSerializer):
|
||||
''' Serializer: Operation data. '''
|
||||
class Meta:
|
||||
|
@ -32,6 +41,49 @@ class ArgumentSerializer(serializers.ModelSerializer):
|
|||
fields = ('operation', 'argument')
|
||||
|
||||
|
||||
class BlockCreateSerializer(serializers.Serializer):
|
||||
''' Serializer: Block creation. '''
|
||||
class BlockCreateData(serializers.ModelSerializer):
|
||||
''' Serializer: Block creation data. '''
|
||||
|
||||
class Meta:
|
||||
''' serializer metadata. '''
|
||||
model = Block
|
||||
fields = 'title', 'description', 'parent'
|
||||
|
||||
layout = LayoutSerializer()
|
||||
|
||||
item_data = BlockCreateData()
|
||||
width = serializers.FloatField()
|
||||
height = serializers.FloatField()
|
||||
position_x = serializers.FloatField()
|
||||
position_y = serializers.FloatField()
|
||||
children_operations = PKField(many=True, queryset=Operation.objects.all().only('oss_id'))
|
||||
children_blocks = PKField(many=True, queryset=Block.objects.all().only('oss_id'))
|
||||
|
||||
def validate(self, attrs):
|
||||
oss = cast(LibraryItem, self.context['oss'])
|
||||
if 'parent' in attrs['item_data'] and \
|
||||
attrs['item_data']['parent'] is not None and \
|
||||
attrs['item_data']['parent'].oss_id != oss.pk:
|
||||
raise serializers.ValidationError({
|
||||
'parent': msg.parentNotInOSS()
|
||||
})
|
||||
|
||||
for operation in attrs['children_operations']:
|
||||
if operation.oss_id != oss.pk:
|
||||
raise serializers.ValidationError({
|
||||
'children_operations': msg.childNotInOSS()
|
||||
})
|
||||
|
||||
for block in attrs['children_blocks']:
|
||||
if block.oss_id != oss.pk:
|
||||
raise serializers.ValidationError({
|
||||
'children_blocks': msg.childNotInOSS()
|
||||
})
|
||||
return attrs
|
||||
|
||||
|
||||
class OperationCreateSerializer(serializers.Serializer):
|
||||
''' Serializer: Operation creation. '''
|
||||
class OperationCreateData(serializers.ModelSerializer):
|
||||
|
@ -44,16 +96,33 @@ class OperationCreateSerializer(serializers.Serializer):
|
|||
model = Operation
|
||||
fields = \
|
||||
'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()
|
||||
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)
|
||||
|
||||
positions = serializers.ListField(
|
||||
child=OperationPositionSerializer(),
|
||||
default=[]
|
||||
)
|
||||
def validate(self, attrs):
|
||||
oss = cast(LibraryItem, self.context['oss'])
|
||||
if 'parent' in attrs['item_data'] and \
|
||||
attrs['item_data']['parent'] is not None and \
|
||||
attrs['item_data']['parent'].oss_id != oss.pk:
|
||||
raise serializers.ValidationError({
|
||||
'parent': msg.parentNotInOSS()
|
||||
})
|
||||
|
||||
if 'arguments' not in attrs:
|
||||
return attrs
|
||||
for operation in attrs['arguments']:
|
||||
if operation.oss_id != oss.pk:
|
||||
raise serializers.ValidationError({
|
||||
'arguments': msg.operationNotInOSS(oss.title)
|
||||
})
|
||||
return attrs
|
||||
|
||||
|
||||
class OperationUpdateSerializer(serializers.Serializer):
|
||||
|
@ -65,6 +134,7 @@ class OperationUpdateSerializer(serializers.Serializer):
|
|||
model = Operation
|
||||
fields = 'alias', 'title', 'description'
|
||||
|
||||
layout = LayoutSerializer()
|
||||
target = PKField(many=False, queryset=Operation.objects.all())
|
||||
item_data = OperationUpdateData()
|
||||
arguments = PKField(many=True, queryset=Operation.objects.all().only('oss_id', 'result_id'), required=False)
|
||||
|
@ -73,16 +143,20 @@ class OperationUpdateSerializer(serializers.Serializer):
|
|||
required=False
|
||||
)
|
||||
|
||||
positions = serializers.ListField(
|
||||
child=OperationPositionSerializer(),
|
||||
default=[]
|
||||
)
|
||||
|
||||
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 'substitutions' in attrs:
|
||||
raise serializers.ValidationError({
|
||||
'arguments': msg.missingArguments()
|
||||
})
|
||||
return attrs
|
||||
|
||||
oss = cast(LibraryItem, self.context['oss'])
|
||||
for operation in attrs['arguments']:
|
||||
if operation.oss_id != oss.pk:
|
||||
raise serializers.ValidationError({
|
||||
|
@ -120,11 +194,8 @@ class OperationUpdateSerializer(serializers.Serializer):
|
|||
|
||||
class OperationTargetSerializer(serializers.Serializer):
|
||||
''' Serializer: Target single operation. '''
|
||||
layout = LayoutSerializer()
|
||||
target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'result_id'))
|
||||
positions = serializers.ListField(
|
||||
child=OperationPositionSerializer(),
|
||||
default=[]
|
||||
)
|
||||
|
||||
def validate(self, attrs):
|
||||
oss = cast(LibraryItem, self.context['oss'])
|
||||
|
@ -138,11 +209,8 @@ class OperationTargetSerializer(serializers.Serializer):
|
|||
|
||||
class OperationDeleteSerializer(serializers.Serializer):
|
||||
''' Serializer: Delete operation. '''
|
||||
layout = LayoutSerializer()
|
||||
target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'result'))
|
||||
positions = serializers.ListField(
|
||||
child=OperationPositionSerializer(),
|
||||
default=[]
|
||||
)
|
||||
keep_constituents = serializers.BooleanField(default=False, required=False)
|
||||
delete_schema = serializers.BooleanField(default=False, required=False)
|
||||
|
||||
|
@ -158,6 +226,7 @@ class OperationDeleteSerializer(serializers.Serializer):
|
|||
|
||||
class SetOperationInputSerializer(serializers.Serializer):
|
||||
''' Serializer: Set input schema for operation. '''
|
||||
layout = LayoutSerializer()
|
||||
target = PKField(many=False, queryset=Operation.objects.all())
|
||||
input = PKField(
|
||||
many=False,
|
||||
|
@ -165,10 +234,6 @@ class SetOperationInputSerializer(serializers.Serializer):
|
|||
allow_null=True,
|
||||
default=None
|
||||
)
|
||||
positions = serializers.ListField(
|
||||
child=OperationPositionSerializer(),
|
||||
default=[]
|
||||
)
|
||||
|
||||
def validate(self, attrs):
|
||||
oss = cast(LibraryItem, self.context['oss'])
|
||||
|
@ -186,15 +251,19 @@ class SetOperationInputSerializer(serializers.Serializer):
|
|||
|
||||
class OperationSchemaSerializer(serializers.ModelSerializer):
|
||||
''' Serializer: Detailed data for OSS. '''
|
||||
items = serializers.ListField(
|
||||
operations = serializers.ListField(
|
||||
child=OperationSerializer()
|
||||
)
|
||||
blocks = serializers.ListField(
|
||||
child=BlockSerializer()
|
||||
)
|
||||
arguments = serializers.ListField(
|
||||
child=ArgumentSerializer()
|
||||
)
|
||||
substitutions = serializers.ListField(
|
||||
child=SubstitutionExSerializer()
|
||||
)
|
||||
layout = LayoutSerializer()
|
||||
|
||||
class Meta:
|
||||
''' serializer metadata. '''
|
||||
|
@ -205,13 +274,17 @@ class OperationSchemaSerializer(serializers.ModelSerializer):
|
|||
result = LibraryItemDetailsSerializer(instance).data
|
||||
del result['versions']
|
||||
oss = OperationSchema(instance)
|
||||
result['items'] = []
|
||||
for operation in oss.operations().order_by('pk'):
|
||||
result['items'].append(OperationSerializer(operation).data)
|
||||
result['layout'] = oss.layout().data
|
||||
result['operations'] = []
|
||||
result['blocks'] = []
|
||||
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'):
|
||||
result['arguments'].append(ArgumentSerializer(argument).data)
|
||||
result['substitutions'] = []
|
||||
for substitution in oss.substitutions().values(
|
||||
'operation',
|
||||
'original',
|
||||
|
|
|
@ -3,7 +3,7 @@ from rest_framework import serializers
|
|||
|
||||
from apps.library.serializers import LibraryItemSerializer
|
||||
|
||||
from .data_access import OperationSchemaSerializer, OperationSerializer
|
||||
from .data_access import BlockSerializer, OperationSchemaSerializer, OperationSerializer
|
||||
|
||||
|
||||
class NewOperationResponse(serializers.Serializer):
|
||||
|
@ -12,6 +12,12 @@ class NewOperationResponse(serializers.Serializer):
|
|||
oss = OperationSchemaSerializer()
|
||||
|
||||
|
||||
class NewBlockResponse(serializers.Serializer):
|
||||
''' Serializer: Create block response. '''
|
||||
new_block = BlockSerializer()
|
||||
oss = OperationSchemaSerializer()
|
||||
|
||||
|
||||
class NewSchemaResponse(serializers.Serializer):
|
||||
''' Serializer: Create RSForm for input operation response. '''
|
||||
new_schema = LibraryItemSerializer()
|
||||
|
|
|
@ -8,6 +8,7 @@ from apps.oss.models import Argument, Operation, OperationSchema, OperationType
|
|||
class TestArgument(TestCase):
|
||||
''' Testing Argument model. '''
|
||||
|
||||
|
||||
def setUp(self):
|
||||
self.oss = OperationSchema.create(alias='T1')
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ from apps.rsform.models import Constituenta, RSForm
|
|||
class TestInheritance(TestCase):
|
||||
''' Testing Inheritance model. '''
|
||||
|
||||
|
||||
def setUp(self):
|
||||
self.oss = OperationSchema.create(alias='T1')
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ from apps.rsform.models import RSForm
|
|||
class TestOperation(TestCase):
|
||||
''' Testing Operation model. '''
|
||||
|
||||
|
||||
def setUp(self):
|
||||
self.oss = OperationSchema.create(alias='T1')
|
||||
self.operation = Operation.objects.create(
|
||||
|
@ -25,9 +26,8 @@ class TestOperation(TestCase):
|
|||
def test_create_default(self):
|
||||
self.assertEqual(self.operation.oss, self.oss.model)
|
||||
self.assertEqual(self.operation.operation_type, OperationType.INPUT)
|
||||
self.assertEqual(self.operation.parent, None)
|
||||
self.assertEqual(self.operation.result, None)
|
||||
self.assertEqual(self.operation.alias, 'KS1')
|
||||
self.assertEqual(self.operation.title, '')
|
||||
self.assertEqual(self.operation.description, '')
|
||||
self.assertEqual(self.operation.position_x, 0)
|
||||
self.assertEqual(self.operation.position_y, 0)
|
||||
|
|
|
@ -10,6 +10,7 @@ from apps.rsform.models import RSForm
|
|||
class TestSynthesisSubstitution(TestCase):
|
||||
''' Testing Synthesis Substitution model. '''
|
||||
|
||||
|
||||
def setUp(self):
|
||||
self.oss = OperationSchema.create(alias='T1')
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ from shared.EndpointTester import EndpointTester, decl_endpoint
|
|||
class TestChangeAttributes(EndpointTester):
|
||||
''' Testing LibraryItem view when OSS is associated with RSForms. '''
|
||||
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user3 = User.objects.create(
|
||||
|
@ -58,6 +59,19 @@ class TestChangeAttributes(EndpointTester):
|
|||
self.operation3.refresh_from_db()
|
||||
self.ks3 = RSForm(self.operation3.result)
|
||||
|
||||
self.layout_data = {
|
||||
'operations': [
|
||||
{'id': self.operation1.pk, 'x': 0, 'y': 0},
|
||||
{'id': self.operation2.pk, 'x': 0, 'y': 0},
|
||||
{'id': self.operation3.pk, 'x': 0, 'y': 0},
|
||||
],
|
||||
'blocks': []
|
||||
}
|
||||
layout = self.owned.layout()
|
||||
layout.data = self.layout_data
|
||||
layout.save()
|
||||
|
||||
|
||||
@decl_endpoint('/api/library/{item}/set-owner', method='patch')
|
||||
def test_set_owner(self):
|
||||
data = {'user': self.user3.pk}
|
||||
|
@ -73,6 +87,7 @@ class TestChangeAttributes(EndpointTester):
|
|||
self.assertEqual(self.ks2.model.owner, self.user2)
|
||||
self.assertEqual(self.ks3.model.owner, self.user3)
|
||||
|
||||
|
||||
@decl_endpoint('/api/library/{item}/set-location', method='patch')
|
||||
def test_set_location(self):
|
||||
data = {'location': '/U/temp'}
|
||||
|
@ -88,6 +103,7 @@ class TestChangeAttributes(EndpointTester):
|
|||
self.assertNotEqual(self.ks2.model.location, data['location'])
|
||||
self.assertEqual(self.ks3.model.location, data['location'])
|
||||
|
||||
|
||||
@decl_endpoint('/api/library/{item}/set-access-policy', method='patch')
|
||||
def test_set_access_policy(self):
|
||||
data = {'access_policy': AccessPolicy.PROTECTED}
|
||||
|
@ -103,6 +119,7 @@ class TestChangeAttributes(EndpointTester):
|
|||
self.assertNotEqual(self.ks2.model.access_policy, data['access_policy'])
|
||||
self.assertEqual(self.ks3.model.access_policy, data['access_policy'])
|
||||
|
||||
|
||||
@decl_endpoint('/api/library/{item}/set-editors', method='patch')
|
||||
def test_set_editors(self):
|
||||
Editor.set(self.owned.model.pk, [self.user2.pk])
|
||||
|
@ -121,6 +138,7 @@ class TestChangeAttributes(EndpointTester):
|
|||
self.assertEqual(list(self.ks2.model.getQ_editors()), [])
|
||||
self.assertEqual(set(self.ks3.model.getQ_editors()), set([self.user, self.user3]))
|
||||
|
||||
|
||||
@decl_endpoint('/api/library/{item}', method='patch')
|
||||
def test_sync_from_result(self):
|
||||
data = {'alias': 'KS111', 'title': 'New Title', 'description': 'New description'}
|
||||
|
@ -133,6 +151,7 @@ class TestChangeAttributes(EndpointTester):
|
|||
self.assertEqual(self.operation1.title, data['title'])
|
||||
self.assertEqual(self.operation1.description, data['description'])
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
|
||||
def test_sync_from_operation(self):
|
||||
data = {
|
||||
|
@ -142,7 +161,7 @@ class TestChangeAttributes(EndpointTester):
|
|||
'title': 'Test title mod',
|
||||
'description': 'Comment mod'
|
||||
},
|
||||
'positions': [],
|
||||
'layout': self.layout_data
|
||||
}
|
||||
|
||||
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.description, data['item_data']['description'])
|
||||
|
||||
|
||||
@decl_endpoint('/api/library/{item}', method='delete')
|
||||
def test_destroy_oss_consequence(self):
|
||||
response = self.executeNoContent(item=self.owned_id)
|
||||
|
|
|
@ -57,6 +57,19 @@ class TestChangeConstituents(EndpointTester):
|
|||
self.ks3 = RSForm(self.operation3.result)
|
||||
self.assertEqual(self.ks3.constituents().count(), 4)
|
||||
|
||||
self.layout_data = {
|
||||
'operations': [
|
||||
{'id': self.operation1.pk, 'x': 0, 'y': 0},
|
||||
{'id': self.operation2.pk, 'x': 0, 'y': 0},
|
||||
{'id': self.operation3.pk, 'x': 0, 'y': 0},
|
||||
],
|
||||
'blocks': []
|
||||
}
|
||||
layout = self.owned.layout()
|
||||
layout.data = self.layout_data
|
||||
layout.save()
|
||||
|
||||
|
||||
@decl_endpoint('/api/rsforms/{item}/details', method='get')
|
||||
def test_retrieve_inheritance(self):
|
||||
response = self.executeOK(item=self.ks3.model.pk)
|
||||
|
@ -84,6 +97,7 @@ class TestChangeConstituents(EndpointTester):
|
|||
},
|
||||
])
|
||||
|
||||
|
||||
@decl_endpoint('/api/rsforms/{schema}/create-cst', method='post')
|
||||
def test_create_constituenta(self):
|
||||
data = {
|
||||
|
@ -100,6 +114,7 @@ class TestChangeConstituents(EndpointTester):
|
|||
self.assertEqual(inherited_cst.order, 2)
|
||||
self.assertEqual(inherited_cst.definition_formal, 'X1 = X2')
|
||||
|
||||
|
||||
@decl_endpoint('/api/rsforms/{schema}/rename-cst', method='patch')
|
||||
def test_rename_constituenta(self):
|
||||
data = {'target': self.ks1X1.pk, 'alias': 'D21', 'cst_type': CstType.TERM}
|
||||
|
@ -111,6 +126,7 @@ class TestChangeConstituents(EndpointTester):
|
|||
self.assertEqual(inherited_cst.alias, 'D2')
|
||||
self.assertEqual(inherited_cst.cst_type, data['cst_type'])
|
||||
|
||||
|
||||
@decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch')
|
||||
def test_update_constituenta(self):
|
||||
d2 = self.ks3.insert_new('D2', cst_type=CstType.TERM, definition_raw='@{X1|sing,nomn}')
|
||||
|
@ -137,6 +153,7 @@ class TestChangeConstituents(EndpointTester):
|
|||
self.assertEqual(inherited_cst.definition_formal, r'X1\X1')
|
||||
self.assertEqual(inherited_cst.definition_raw, r'@{X2|sing,datv}')
|
||||
|
||||
|
||||
@decl_endpoint('/api/rsforms/{schema}/delete-multiple-cst', method='patch')
|
||||
def test_delete_constituenta(self):
|
||||
data = {'items': [self.ks2X1.pk]}
|
||||
|
@ -148,6 +165,7 @@ class TestChangeConstituents(EndpointTester):
|
|||
self.assertEqual(self.ks2D1.definition_formal, r'DEL\DEL')
|
||||
self.assertEqual(inherited_cst.definition_formal, r'DEL\DEL')
|
||||
|
||||
|
||||
@decl_endpoint('/api/rsforms/{schema}/substitute', method='patch')
|
||||
def test_substitute(self):
|
||||
d2 = self.ks3.insert_new('D2', cst_type=CstType.TERM, definition_formal=r'X1\X2\X3')
|
||||
|
|
|
@ -8,6 +8,7 @@ from shared.EndpointTester import EndpointTester, decl_endpoint
|
|||
class TestChangeOperations(EndpointTester):
|
||||
''' Testing Operations change propagation in OSS. '''
|
||||
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.owned = OperationSchema.create(
|
||||
|
@ -106,6 +107,21 @@ class TestChangeOperations(EndpointTester):
|
|||
convention='KS5D4'
|
||||
)
|
||||
|
||||
self.layout_data = {
|
||||
'operations': [
|
||||
{'id': self.operation1.pk, 'x': 0, 'y': 0},
|
||||
{'id': self.operation2.pk, 'x': 0, 'y': 0},
|
||||
{'id': self.operation3.pk, 'x': 0, 'y': 0},
|
||||
{'id': self.operation4.pk, 'x': 0, 'y': 0},
|
||||
{'id': self.operation5.pk, 'x': 0, 'y': 0},
|
||||
],
|
||||
'blocks': []
|
||||
}
|
||||
layout = self.owned.layout()
|
||||
layout.data = self.layout_data
|
||||
layout.save()
|
||||
|
||||
|
||||
def test_oss_setup(self):
|
||||
self.assertEqual(self.ks1.constituents().count(), 3)
|
||||
self.assertEqual(self.ks2.constituents().count(), 3)
|
||||
|
@ -114,10 +130,11 @@ class TestChangeOperations(EndpointTester):
|
|||
self.assertEqual(self.ks5.constituents().count(), 8)
|
||||
self.assertEqual(self.ks4D1.definition_formal, 'S1 X1')
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
|
||||
def test_delete_input_operation(self):
|
||||
data = {
|
||||
'positions': [],
|
||||
'layout': self.layout_data,
|
||||
'target': self.operation2.pk
|
||||
}
|
||||
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.ks5D4.definition_formal, r'DEL DEL X3 DEL D1 D2 D3')
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/set-input', method='patch')
|
||||
def test_set_input_null(self):
|
||||
data = {
|
||||
'positions': [],
|
||||
'layout': self.layout_data,
|
||||
'target': self.operation2.pk,
|
||||
'input': None
|
||||
}
|
||||
|
@ -157,6 +175,7 @@ class TestChangeOperations(EndpointTester):
|
|||
self.assertEqual(self.ks4D2.definition_formal, r'X1 DEL DEL DEL D1')
|
||||
self.assertEqual(self.ks5D4.definition_formal, r'DEL DEL X3 DEL D1 D2 D3')
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/set-input', method='patch')
|
||||
def test_set_input_change_schema(self):
|
||||
ks6 = RSForm.create(
|
||||
|
@ -169,7 +188,7 @@ class TestChangeOperations(EndpointTester):
|
|||
ks6D1 = ks6.insert_new('D1', definition_formal='X1 X2', convention='KS6D1')
|
||||
|
||||
data = {
|
||||
'positions': [],
|
||||
'layout': self.layout_data,
|
||||
'target': self.operation2.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.ks5D4.definition_formal, r'DEL DEL X3 DEL D1 D2 D3')
|
||||
|
||||
|
||||
@decl_endpoint('/api/library/{item}', method='delete')
|
||||
def test_delete_schema(self):
|
||||
self.executeNoContent(item=self.ks1.model.pk)
|
||||
|
@ -208,10 +228,11 @@ class TestChangeOperations(EndpointTester):
|
|||
self.assertEqual(self.ks4D2.definition_formal, r'DEL X2 X3 S1 DEL')
|
||||
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 DEL D2 D3')
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
|
||||
def test_delete_operation_and_constituents(self):
|
||||
data = {
|
||||
'positions': [],
|
||||
'layout': self.layout_data,
|
||||
'target': self.operation1.pk,
|
||||
'keep_constituents': False,
|
||||
'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.ks5D4.definition_formal, r'X1 X2 X3 S1 DEL D2 D3')
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
|
||||
def test_delete_operation_keep_constituents(self):
|
||||
data = {
|
||||
'positions': [],
|
||||
'layout': self.layout_data,
|
||||
'target': self.operation1.pk,
|
||||
'keep_constituents': 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.ks5D4.definition_formal, r'X1 X2 X3 S1 D1 D2 D3')
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
|
||||
def test_delete_operation_keep_schema(self):
|
||||
data = {
|
||||
'positions': [],
|
||||
'layout': self.layout_data,
|
||||
'target': self.operation1.pk,
|
||||
'keep_constituents': True,
|
||||
'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.assertFalse(Constituenta.objects.filter(as_child__parent_id=self.ks1D1.pk).exists())
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
|
||||
def test_change_substitutions(self):
|
||||
data = {
|
||||
|
@ -283,7 +307,8 @@ class TestChangeOperations(EndpointTester):
|
|||
'title': 'Test title mod',
|
||||
'description': 'Comment mod'
|
||||
},
|
||||
'positions': [],
|
||||
'layout': self.layout_data,
|
||||
'arguments': [self.operation1.pk, self.operation2.pk],
|
||||
'substitutions': [
|
||||
{
|
||||
'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.ks5D4.definition_formal, r'D1 X2 X3 S1 D1 D2 D3')
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
|
||||
def test_change_arguments(self):
|
||||
data = {
|
||||
|
@ -317,7 +343,7 @@ class TestChangeOperations(EndpointTester):
|
|||
'title': 'Test title mod',
|
||||
'description': 'Comment mod'
|
||||
},
|
||||
'positions': [],
|
||||
'layout': self.layout_data,
|
||||
'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.ks5D4.definition_formal, r'DEL DEL X3 DEL D1 D2 D3')
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/execute-operation', method='post')
|
||||
def test_execute_middle_operation(self):
|
||||
self.client.delete(f'/api/library/{self.ks4.model.pk}')
|
||||
|
@ -356,7 +383,7 @@ class TestChangeOperations(EndpointTester):
|
|||
|
||||
data = {
|
||||
'target': self.operation4.pk,
|
||||
'positions': []
|
||||
'layout': self.layout_data
|
||||
}
|
||||
self.executeOK(data=data, item=self.owned_id)
|
||||
self.operation4.refresh_from_db()
|
||||
|
@ -364,6 +391,7 @@ class TestChangeOperations(EndpointTester):
|
|||
self.assertNotEqual(self.operation4.result, None)
|
||||
self.assertEqual(self.ks5.constituents().count(), 8)
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/relocate-constituents', method='post')
|
||||
def test_relocate_constituents_up(self):
|
||||
ks1_old_count = self.ks1.constituents().count()
|
||||
|
@ -393,6 +421,7 @@ class TestChangeOperations(EndpointTester):
|
|||
self.assertEqual(self.ks1.constituents().count(), ks1_old_count + 1)
|
||||
self.assertEqual(self.ks4.constituents().count(), ks4_old_count + 1)
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/relocate-constituents', method='post')
|
||||
def test_relocate_constituents_down(self):
|
||||
ks1_old_count = self.ks1.constituents().count()
|
||||
|
|
|
@ -8,6 +8,7 @@ from shared.EndpointTester import EndpointTester, decl_endpoint
|
|||
class TestChangeSubstitutions(EndpointTester):
|
||||
''' Testing Substitutions change propagation in OSS. '''
|
||||
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.owned = OperationSchema.create(
|
||||
|
@ -106,6 +107,20 @@ class TestChangeSubstitutions(EndpointTester):
|
|||
convention='KS5D4'
|
||||
)
|
||||
|
||||
self.layout_data = {
|
||||
'operations': [
|
||||
{'id': self.operation1.pk, 'x': 0, 'y': 0},
|
||||
{'id': self.operation2.pk, 'x': 0, 'y': 0},
|
||||
{'id': self.operation3.pk, 'x': 0, 'y': 0},
|
||||
{'id': self.operation4.pk, 'x': 0, 'y': 0},
|
||||
{'id': self.operation5.pk, 'x': 0, 'y': 0},
|
||||
],
|
||||
'blocks': []
|
||||
}
|
||||
layout = self.owned.layout()
|
||||
layout.data = self.layout_data
|
||||
layout.save()
|
||||
|
||||
|
||||
def test_oss_setup(self):
|
||||
self.assertEqual(self.ks1.constituents().count(), 3)
|
||||
|
@ -115,6 +130,7 @@ class TestChangeSubstitutions(EndpointTester):
|
|||
self.assertEqual(self.ks5.constituents().count(), 8)
|
||||
self.assertEqual(self.ks4D1.definition_formal, 'S1 X1')
|
||||
|
||||
|
||||
@decl_endpoint('/api/rsforms/{schema}/substitute', method='patch')
|
||||
def test_substitute_original(self):
|
||||
data = {'substitutions': [{
|
||||
|
@ -137,12 +153,15 @@ class TestChangeSubstitutions(EndpointTester):
|
|||
self.assertEqual(self.ks4D2.definition_formal, r'S1 X2 X3 S1 D1')
|
||||
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 X3 D1 D2 D3')
|
||||
|
||||
|
||||
@decl_endpoint('/api/rsforms/{schema}/substitute', method='patch')
|
||||
def test_substitute_substitution(self):
|
||||
data = {'substitutions': [{
|
||||
'original': self.ks2S1.pk,
|
||||
'substitution': self.ks2X1.pk
|
||||
}]}
|
||||
data = {
|
||||
'substitutions': [{
|
||||
'original': self.ks2S1.pk,
|
||||
'substitution': self.ks2X1.pk
|
||||
}]
|
||||
}
|
||||
self.executeOK(data=data, schema=self.ks2.model.pk)
|
||||
self.ks4D1.refresh_from_db()
|
||||
self.ks4D2.refresh_from_db()
|
||||
|
@ -159,6 +178,7 @@ class TestChangeSubstitutions(EndpointTester):
|
|||
self.assertEqual(self.ks4D2.definition_formal, r'X1 X2 X3 X2 D1')
|
||||
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 X1 D1 D2 D3')
|
||||
|
||||
|
||||
@decl_endpoint('/api/rsforms/{schema}/delete-multiple-cst', method='patch')
|
||||
def test_delete_original(self):
|
||||
data = {'items': [self.ks1X1.pk, self.ks1D1.pk]}
|
||||
|
@ -173,6 +193,7 @@ class TestChangeSubstitutions(EndpointTester):
|
|||
self.assertEqual(self.ks4D2.definition_formal, r'X1 X2 X3 S1 DEL')
|
||||
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 DEL D2 D3')
|
||||
|
||||
|
||||
@decl_endpoint('/api/rsforms/{schema}/delete-multiple-cst', method='patch')
|
||||
def test_delete_substitution(self):
|
||||
data = {'items': [self.ks2S1.pk, self.ks2X2.pk]}
|
||||
|
|
|
@ -1,2 +1,4 @@
|
|||
''' Tests for REST API. '''
|
||||
from .t_blocks import *
|
||||
from .t_operations import *
|
||||
from .t_oss import *
|
||||
|
|
167
rsconcept/backend/apps/oss/tests/s_views/t_blocks.py
Normal file
167
rsconcept/backend/apps/oss/tests/s_views/t_blocks.py
Normal 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'])
|
488
rsconcept/backend/apps/oss/tests/s_views/t_operations.py
Normal file
488
rsconcept/backend/apps/oss/tests/s_views/t_operations.py
Normal 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)
|
|
@ -1,6 +1,6 @@
|
|||
''' Testing API: Operation Schema. '''
|
||||
from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType
|
||||
from apps.oss.models import Operation, OperationSchema, OperationType
|
||||
from apps.library.models import AccessPolicy, LibraryItemType
|
||||
from apps.oss.models import OperationSchema, OperationType
|
||||
from apps.rsform.models import Constituenta, RSForm
|
||||
from shared.EndpointTester import EndpointTester, decl_endpoint
|
||||
|
||||
|
@ -16,7 +16,8 @@ class TestOssViewset(EndpointTester):
|
|||
self.unowned_id = self.unowned.model.pk
|
||||
self.private = OperationSchema.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE)
|
||||
self.private_id = self.private.model.pk
|
||||
self.invalid_id = self.private.model.pk + 1337
|
||||
self.invalid_id = self.private_id + 1337
|
||||
|
||||
|
||||
def populateData(self):
|
||||
self.ks1 = RSForm.create(
|
||||
|
@ -54,12 +55,21 @@ class TestOssViewset(EndpointTester):
|
|||
alias='3',
|
||||
operation_type=OperationType.SYNTHESIS
|
||||
)
|
||||
layout = self.owned.layout()
|
||||
layout.data = {'operations': [
|
||||
{'id': self.operation1.pk, 'x': 0, 'y': 0},
|
||||
{'id': self.operation2.pk, 'x': 0, 'y': 0},
|
||||
{'id': self.operation3.pk, 'x': 0, 'y': 0},
|
||||
], 'blocks': []}
|
||||
layout.save()
|
||||
|
||||
self.owned.set_arguments(self.operation3.pk, [self.operation1, self.operation2])
|
||||
self.owned.set_substitutions(self.operation3.pk, [{
|
||||
'original': self.ks1X1,
|
||||
'substitution': self.ks2X1
|
||||
}])
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/details', method='get')
|
||||
def test_details(self):
|
||||
self.populateData()
|
||||
|
@ -74,9 +84,9 @@ class TestOssViewset(EndpointTester):
|
|||
|
||||
self.assertEqual(response.data['item_type'], LibraryItemType.OPERATION_SCHEMA)
|
||||
|
||||
self.assertEqual(len(response.data['items']), 3)
|
||||
self.assertEqual(response.data['items'][0]['id'], self.operation1.pk)
|
||||
self.assertEqual(response.data['items'][0]['operation_type'], self.operation1.operation_type)
|
||||
self.assertEqual(len(response.data['operations']), 3)
|
||||
self.assertEqual(response.data['operations'][0]['id'], self.operation1.pk)
|
||||
self.assertEqual(response.data['operations'][0]['operation_type'], self.operation1.operation_type)
|
||||
|
||||
self.assertEqual(len(response.data['substitutions']), 1)
|
||||
sub = response.data['substitutions'][0]
|
||||
|
@ -95,6 +105,12 @@ class TestOssViewset(EndpointTester):
|
|||
self.assertEqual(arguments[1]['operation'], self.operation3.pk)
|
||||
self.assertEqual(arguments[1]['argument'], self.operation2.pk)
|
||||
|
||||
layout = response.data['layout']
|
||||
self.assertEqual(layout['blocks'], [])
|
||||
self.assertEqual(layout['operations'][0], {'id': self.operation1.pk, 'x': 0, 'y': 0})
|
||||
self.assertEqual(layout['operations'][1], {'id': self.operation2.pk, 'x': 0, 'y': 0})
|
||||
self.assertEqual(layout['operations'][2], {'id': self.operation3.pk, 'x': 0, 'y': 0})
|
||||
|
||||
self.executeOK(item=self.unowned_id)
|
||||
self.executeForbidden(item=self.private_id)
|
||||
|
||||
|
@ -103,400 +119,33 @@ class TestOssViewset(EndpointTester):
|
|||
self.executeOK(item=self.unowned_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.executeBadData(item=self.owned_id)
|
||||
|
||||
data = {'positions': []}
|
||||
data = {'operations': [], 'blocks': []}
|
||||
self.executeOK(data=data)
|
||||
|
||||
data = {'positions': [
|
||||
{'id': self.operation1.pk, 'position_x': 42.1, 'position_y': 1337},
|
||||
{'id': self.operation2.pk, 'position_x': 36.1, 'position_y': 1437},
|
||||
{'id': self.invalid_id, 'position_x': 31, 'position_y': 12},
|
||||
]}
|
||||
data = {
|
||||
'operations': [
|
||||
{'id': self.operation1.pk, 'x': 42.1, 'y': 1337},
|
||||
{'id': self.operation2.pk, 'x': 36.1, 'y': 1437},
|
||||
{'id': self.operation3.pk, 'x': 36.1, 'y': 1435}
|
||||
], 'blocks': []
|
||||
}
|
||||
self.toggle_admin(True)
|
||||
self.executeOK(data=data, item=self.unowned_id)
|
||||
self.operation1.refresh_from_db()
|
||||
self.assertNotEqual(self.operation1.position_x, data['positions'][0]['position_x'])
|
||||
self.assertNotEqual(self.operation1.position_y, data['positions'][0]['position_y'])
|
||||
|
||||
self.toggle_admin(False)
|
||||
self.executeOK(data=data, item=self.owned_id)
|
||||
self.operation1.refresh_from_db()
|
||||
self.operation2.refresh_from_db()
|
||||
self.assertEqual(self.operation1.position_x, data['positions'][0]['position_x'])
|
||||
self.assertEqual(self.operation1.position_y, data['positions'][0]['position_y'])
|
||||
self.assertEqual(self.operation2.position_x, data['positions'][1]['position_x'])
|
||||
self.assertEqual(self.operation2.position_y, data['positions'][1]['position_y'])
|
||||
self.owned.refresh_from_db()
|
||||
self.assertEqual(self.owned.layout().data, data)
|
||||
|
||||
self.executeForbidden(data=data, item=self.unowned_id)
|
||||
self.executeForbidden(data=data, item=self.private_id)
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
|
||||
def test_create_operation(self):
|
||||
self.populateData()
|
||||
self.executeBadData(item=self.owned_id)
|
||||
|
||||
data = {
|
||||
'item_data': {
|
||||
'alias': 'Test3',
|
||||
'title': 'Test title',
|
||||
'description': 'Тест кириллицы',
|
||||
'position_x': 1,
|
||||
'position_y': 1,
|
||||
},
|
||||
'positions': [
|
||||
{'id': self.operation1.pk, 'position_x': 42.1, 'position_y': 1337}
|
||||
]
|
||||
}
|
||||
self.executeBadData(data=data)
|
||||
|
||||
data['item_data']['operation_type'] = 'invalid'
|
||||
self.executeBadData(data=data)
|
||||
|
||||
data['item_data']['operation_type'] = OperationType.INPUT
|
||||
self.executeNotFound(data=data, item=self.invalid_id)
|
||||
|
||||
response = self.executeCreated(data=data, item=self.owned_id)
|
||||
self.assertEqual(len(response.data['oss']['items']), 4)
|
||||
new_operation = response.data['new_operation']
|
||||
self.assertEqual(new_operation['alias'], data['item_data']['alias'])
|
||||
self.assertEqual(new_operation['operation_type'], data['item_data']['operation_type'])
|
||||
self.assertEqual(new_operation['title'], data['item_data']['title'])
|
||||
self.assertEqual(new_operation['description'], data['item_data']['description'])
|
||||
self.assertEqual(new_operation['position_x'], data['item_data']['position_x'])
|
||||
self.assertEqual(new_operation['position_y'], data['item_data']['position_y'])
|
||||
self.assertEqual(new_operation['result'], None)
|
||||
self.operation1.refresh_from_db()
|
||||
self.assertEqual(self.operation1.position_x, data['positions'][0]['position_x'])
|
||||
self.assertEqual(self.operation1.position_y, data['positions'][0]['position_y'])
|
||||
|
||||
self.executeForbidden(data=data, item=self.unowned_id)
|
||||
self.toggle_admin(True)
|
||||
self.executeCreated(data=data, item=self.unowned_id)
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
|
||||
def test_create_operation_arguments(self):
|
||||
self.populateData()
|
||||
data = {
|
||||
'item_data': {
|
||||
'alias': 'Test4',
|
||||
'operation_type': OperationType.SYNTHESIS
|
||||
},
|
||||
'positions': [],
|
||||
'arguments': [self.operation1.pk, self.operation3.pk]
|
||||
}
|
||||
response = self.executeCreated(data=data, item=self.owned_id)
|
||||
self.owned.refresh_from_db()
|
||||
new_operation = response.data['new_operation']
|
||||
arguments = self.owned.arguments()
|
||||
self.assertTrue(arguments.filter(operation__id=new_operation['id'], argument=self.operation1))
|
||||
self.assertTrue(arguments.filter(operation__id=new_operation['id'], argument=self.operation3))
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
|
||||
def test_create_operation_result(self):
|
||||
self.populateData()
|
||||
|
||||
self.operation1.result = None
|
||||
self.operation1.save()
|
||||
|
||||
data = {
|
||||
'item_data': {
|
||||
'alias': 'Test4',
|
||||
'operation_type': OperationType.INPUT,
|
||||
'result': self.ks1.model.pk
|
||||
},
|
||||
'positions': [],
|
||||
}
|
||||
response = self.executeCreated(data=data, item=self.owned_id)
|
||||
self.owned.refresh_from_db()
|
||||
new_operation = response.data['new_operation']
|
||||
self.assertEqual(new_operation['result'], self.ks1.model.pk)
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
|
||||
def test_create_operation_schema(self):
|
||||
self.populateData()
|
||||
Editor.add(self.owned.model.pk, self.user2.pk)
|
||||
data = {
|
||||
'item_data': {
|
||||
'alias': 'Test4',
|
||||
'title': 'Test title',
|
||||
'description': 'Comment',
|
||||
'operation_type': OperationType.INPUT,
|
||||
'result': self.ks1.model.pk
|
||||
},
|
||||
'create_schema': True,
|
||||
'positions': [],
|
||||
}
|
||||
self.executeBadData(data=data, item=self.owned_id)
|
||||
data['item_data']['result'] = None
|
||||
response = self.executeCreated(data=data, item=self.owned_id)
|
||||
self.owned.refresh_from_db()
|
||||
new_operation = response.data['new_operation']
|
||||
schema = LibraryItem.objects.get(pk=new_operation['result'])
|
||||
self.assertEqual(schema.alias, data['item_data']['alias'])
|
||||
self.assertEqual(schema.title, data['item_data']['title'])
|
||||
self.assertEqual(schema.description, data['item_data']['description'])
|
||||
self.assertEqual(schema.visible, False)
|
||||
self.assertEqual(schema.access_policy, self.owned.model.access_policy)
|
||||
self.assertEqual(schema.location, self.owned.model.location)
|
||||
self.assertIn(self.user2, schema.getQ_editors())
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
|
||||
def test_delete_operation(self):
|
||||
self.executeNotFound(item=self.invalid_id)
|
||||
|
||||
self.populateData()
|
||||
self.executeBadData(item=self.owned_id)
|
||||
|
||||
data = {
|
||||
'positions': []
|
||||
}
|
||||
self.executeBadData(data=data)
|
||||
|
||||
data['target'] = self.operation1.pk
|
||||
self.toggle_admin(True)
|
||||
self.executeBadData(data=data, item=self.unowned_id)
|
||||
self.logout()
|
||||
self.executeForbidden(data=data, item=self.owned_id)
|
||||
|
||||
self.login()
|
||||
response = self.executeOK(data=data)
|
||||
self.assertEqual(len(response.data['items']), 2)
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/create-input', method='patch')
|
||||
def test_create_input(self):
|
||||
self.populateData()
|
||||
self.executeBadData(item=self.owned_id)
|
||||
|
||||
data = {
|
||||
'positions': []
|
||||
}
|
||||
self.executeBadData(data=data)
|
||||
|
||||
data['target'] = self.operation1.pk
|
||||
self.toggle_admin(True)
|
||||
self.executeBadData(data=data, item=self.unowned_id)
|
||||
self.logout()
|
||||
self.executeForbidden(data=data, item=self.owned_id)
|
||||
|
||||
self.login()
|
||||
self.executeBadData(data=data, item=self.owned_id)
|
||||
|
||||
self.operation1.result = None
|
||||
self.operation1.description = 'TestComment'
|
||||
self.operation1.title = 'TestTitle'
|
||||
self.operation1.save()
|
||||
response = self.executeOK(data=data)
|
||||
self.operation1.refresh_from_db()
|
||||
|
||||
new_schema = response.data['new_schema']
|
||||
self.assertEqual(new_schema['id'], self.operation1.result.pk)
|
||||
self.assertEqual(new_schema['alias'], self.operation1.alias)
|
||||
self.assertEqual(new_schema['title'], self.operation1.title)
|
||||
self.assertEqual(new_schema['description'], self.operation1.description)
|
||||
|
||||
data['target'] = self.operation3.pk
|
||||
self.executeBadData(data=data)
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/set-input', method='patch')
|
||||
def test_set_input_null(self):
|
||||
self.populateData()
|
||||
self.executeBadData(item=self.owned_id)
|
||||
|
||||
data = {
|
||||
'positions': []
|
||||
}
|
||||
self.executeBadData(data=data)
|
||||
|
||||
data['target'] = self.operation1.pk
|
||||
data['input'] = None
|
||||
self.toggle_admin(True)
|
||||
self.executeBadData(data=data, item=self.unowned_id)
|
||||
self.logout()
|
||||
self.executeForbidden(data=data, item=self.owned_id)
|
||||
|
||||
self.login()
|
||||
response = self.executeOK(data=data)
|
||||
self.operation1.refresh_from_db()
|
||||
self.assertEqual(self.operation1.result, None)
|
||||
|
||||
data['input'] = self.ks1.model.pk
|
||||
self.ks1.model.alias = 'Test42'
|
||||
self.ks1.model.title = 'Test421'
|
||||
self.ks1.model.description = 'TestComment42'
|
||||
self.ks1.save()
|
||||
response = self.executeOK(data=data)
|
||||
self.operation1.refresh_from_db()
|
||||
self.assertEqual(self.operation1.result, self.ks1.model)
|
||||
self.assertEqual(self.operation1.alias, self.ks1.model.alias)
|
||||
self.assertEqual(self.operation1.title, self.ks1.model.title)
|
||||
self.assertEqual(self.operation1.description, self.ks1.model.description)
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/set-input', method='patch')
|
||||
def test_set_input_change_schema(self):
|
||||
self.populateData()
|
||||
self.operation2.result = None
|
||||
|
||||
data = {
|
||||
'positions': [],
|
||||
'target': self.operation1.pk,
|
||||
'input': self.ks2.model.pk
|
||||
}
|
||||
self.executeBadData(data=data, item=self.owned_id)
|
||||
|
||||
self.ks2.model.visible = False
|
||||
self.ks2.model.save(update_fields=['visible'])
|
||||
data = {
|
||||
'positions': [],
|
||||
'target': self.operation2.pk,
|
||||
'input': None
|
||||
}
|
||||
self.executeOK(data=data, item=self.owned_id)
|
||||
self.operation2.refresh_from_db()
|
||||
self.ks2.model.refresh_from_db()
|
||||
self.assertEqual(self.operation2.result, None)
|
||||
self.assertEqual(self.ks2.model.visible, True)
|
||||
|
||||
data = {
|
||||
'positions': [],
|
||||
'target': self.operation1.pk,
|
||||
'input': self.ks2.model.pk
|
||||
}
|
||||
self.executeOK(data=data, item=self.owned_id)
|
||||
self.operation1.refresh_from_db()
|
||||
self.assertEqual(self.operation1.result, self.ks2.model)
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
|
||||
def test_update_operation(self):
|
||||
self.populateData()
|
||||
self.executeBadData(item=self.owned_id)
|
||||
|
||||
ks3 = RSForm.create(alias='KS3', title='Test3', owner=self.user)
|
||||
ks3x1 = ks3.insert_new('X1', term_resolved='X1_1')
|
||||
|
||||
data = {
|
||||
'target': self.operation3.pk,
|
||||
'item_data': {
|
||||
'alias': 'Test3 mod',
|
||||
'title': 'Test title mod',
|
||||
'description': 'Comment mod'
|
||||
},
|
||||
'positions': [],
|
||||
'arguments': [self.operation2.pk, self.operation1.pk],
|
||||
'substitutions': [
|
||||
{
|
||||
'original': self.ks1X1.pk,
|
||||
'substitution': ks3x1.pk
|
||||
}
|
||||
]
|
||||
}
|
||||
self.executeBadData(data=data)
|
||||
|
||||
data['substitutions'][0]['substitution'] = self.ks2X1.pk
|
||||
self.toggle_admin(True)
|
||||
self.executeBadData(data=data, item=self.unowned_id)
|
||||
self.logout()
|
||||
self.executeForbidden(data=data, item=self.owned_id)
|
||||
|
||||
self.login()
|
||||
response = self.executeOK(data=data)
|
||||
self.operation3.refresh_from_db()
|
||||
self.assertEqual(self.operation3.alias, data['item_data']['alias'])
|
||||
self.assertEqual(self.operation3.title, data['item_data']['title'])
|
||||
self.assertEqual(self.operation3.description, data['item_data']['description'])
|
||||
args = self.operation3.getQ_arguments().order_by('order')
|
||||
self.assertEqual(args[0].argument.pk, data['arguments'][0])
|
||||
self.assertEqual(args[0].order, 0)
|
||||
self.assertEqual(args[1].argument.pk, data['arguments'][1])
|
||||
self.assertEqual(args[1].order, 1)
|
||||
sub = self.operation3.getQ_substitutions()[0]
|
||||
self.assertEqual(sub.original.pk, data['substitutions'][0]['original'])
|
||||
self.assertEqual(sub.substitution.pk, data['substitutions'][0]['substitution'])
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
|
||||
def test_update_operation_sync(self):
|
||||
self.populateData()
|
||||
self.executeBadData(item=self.owned_id)
|
||||
|
||||
data = {
|
||||
'target': self.operation1.pk,
|
||||
'item_data': {
|
||||
'alias': 'Test3 mod',
|
||||
'title': 'Test title mod',
|
||||
'description': 'Comment mod'
|
||||
},
|
||||
'positions': [],
|
||||
}
|
||||
|
||||
response = self.executeOK(data=data)
|
||||
self.operation1.refresh_from_db()
|
||||
self.assertEqual(self.operation1.alias, data['item_data']['alias'])
|
||||
self.assertEqual(self.operation1.title, data['item_data']['title'])
|
||||
self.assertEqual(self.operation1.description, data['item_data']['description'])
|
||||
self.assertEqual(self.operation1.result.alias, data['item_data']['alias'])
|
||||
self.assertEqual(self.operation1.result.title, data['item_data']['title'])
|
||||
self.assertEqual(self.operation1.result.description, data['item_data']['description'])
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
|
||||
def test_update_operation_invalid_substitution(self):
|
||||
self.populateData()
|
||||
|
||||
self.ks1X2 = self.ks1.insert_new('X2')
|
||||
|
||||
data = {
|
||||
'target': self.operation3.pk,
|
||||
'item_data': {
|
||||
'alias': 'Test3 mod',
|
||||
'title': 'Test title mod',
|
||||
'description': 'Comment mod'
|
||||
},
|
||||
'positions': [],
|
||||
'arguments': [self.operation1.pk, self.operation2.pk],
|
||||
'substitutions': [
|
||||
{
|
||||
'original': self.ks1X1.pk,
|
||||
'substitution': self.ks2X1.pk
|
||||
},
|
||||
{
|
||||
'original': self.ks2X1.pk,
|
||||
'substitution': self.ks1X2.pk
|
||||
}
|
||||
]
|
||||
}
|
||||
self.executeBadData(data=data, item=self.owned_id)
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/execute-operation', method='post')
|
||||
def test_execute_operation(self):
|
||||
self.populateData()
|
||||
self.executeBadData(item=self.owned_id)
|
||||
|
||||
data = {
|
||||
'positions': [],
|
||||
'target': self.operation1.pk
|
||||
}
|
||||
self.executeBadData(data=data)
|
||||
|
||||
data['target'] = self.operation3.pk
|
||||
self.toggle_admin(True)
|
||||
self.executeBadData(data=data, item=self.unowned_id)
|
||||
self.logout()
|
||||
self.executeForbidden(data=data, item=self.owned_id)
|
||||
|
||||
self.login()
|
||||
self.executeOK(data=data)
|
||||
self.operation3.refresh_from_db()
|
||||
schema = self.operation3.result
|
||||
self.assertEqual(schema.alias, self.operation3.alias)
|
||||
self.assertEqual(schema.description, self.operation3.description)
|
||||
self.assertEqual(schema.title, self.operation3.title)
|
||||
self.assertEqual(schema.visible, False)
|
||||
items = list(RSForm(schema).constituents())
|
||||
self.assertEqual(len(items), 1)
|
||||
self.assertEqual(items[0].alias, 'X1')
|
||||
self.assertEqual(items[0].term_resolved, self.ks2X1.term_resolved)
|
||||
|
||||
@decl_endpoint('/api/oss/get-predecessor', method='post')
|
||||
def test_get_predecessor(self):
|
||||
|
|
|
@ -36,9 +36,10 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
def get_permissions(self):
|
||||
''' Determine permission class. '''
|
||||
if self.action in [
|
||||
'update_layout',
|
||||
'create_operation',
|
||||
'create_block',
|
||||
'delete_operation',
|
||||
'update_positions',
|
||||
'create_input',
|
||||
'set_input',
|
||||
'update_operation',
|
||||
|
@ -73,21 +74,21 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
)
|
||||
|
||||
@extend_schema(
|
||||
summary='update positions',
|
||||
summary='update layout',
|
||||
tags=['OSS'],
|
||||
request=s.PositionsSerializer,
|
||||
request=s.LayoutSerializer,
|
||||
responses={
|
||||
c.HTTP_200_OK: None,
|
||||
c.HTTP_403_FORBIDDEN: None,
|
||||
c.HTTP_404_NOT_FOUND: None
|
||||
}
|
||||
)
|
||||
@action(detail=True, methods=['patch'], url_path='update-positions')
|
||||
def update_positions(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Update operations positions. '''
|
||||
serializer = s.PositionsSerializer(data=request.data)
|
||||
@action(detail=True, methods=['patch'], url_path='update-layout')
|
||||
def update_layout(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Update schema layout. '''
|
||||
serializer = s.LayoutSerializer(data=request.data)
|
||||
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)
|
||||
|
||||
@extend_schema(
|
||||
|
@ -104,13 +105,23 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
@action(detail=True, methods=['post'], url_path='create-operation')
|
||||
def create_operation(self, request: Request, pk) -> HttpResponse:
|
||||
''' Create new operation. '''
|
||||
serializer = s.OperationCreateSerializer(data=request.data)
|
||||
serializer = s.OperationCreateSerializer(
|
||||
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']
|
||||
with transaction.atomic():
|
||||
oss.update_positions(serializer.validated_data['positions'])
|
||||
new_operation = oss.create_operation(**serializer.validated_data['item_data'])
|
||||
layout['operations'].append({
|
||||
'id': new_operation.pk,
|
||||
'x': serializer.validated_data['position_x'],
|
||||
'y': serializer.validated_data['position_y']
|
||||
})
|
||||
oss.update_layout(layout)
|
||||
|
||||
schema = new_operation.result
|
||||
if schema is not None:
|
||||
connected_operations = \
|
||||
|
@ -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(
|
||||
summary='delete operation',
|
||||
tags=['OSS'],
|
||||
|
@ -164,9 +226,11 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
oss = m.OperationSchema(self.get_object())
|
||||
operation = cast(m.Operation, serializer.validated_data['target'])
|
||||
old_schema = operation.result
|
||||
layout = serializer.validated_data['layout']
|
||||
layout['operations'] = [x for x in layout['operations'] if x['id'] != operation.pk]
|
||||
with transaction.atomic():
|
||||
oss.update_positions(serializer.validated_data['positions'])
|
||||
oss.delete_operation(operation.pk, serializer.validated_data['keep_constituents'])
|
||||
oss.update_layout(layout)
|
||||
if old_schema is not None:
|
||||
if serializer.validated_data['delete_schema']:
|
||||
m.PropagationFacade.before_delete_schema(old_schema)
|
||||
|
@ -211,7 +275,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
|
||||
oss = m.OperationSchema(self.get_object())
|
||||
with transaction.atomic():
|
||||
oss.update_positions(serializer.validated_data['positions'])
|
||||
oss.update_layout(serializer.validated_data['layout'])
|
||||
schema = oss.create_input(operation)
|
||||
|
||||
return Response(
|
||||
|
@ -262,7 +326,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
if old_schema.is_synced(oss.model):
|
||||
old_schema.visible = True
|
||||
old_schema.save(update_fields=['visible'])
|
||||
oss.update_positions(serializer.validated_data['positions'])
|
||||
oss.update_layout(serializer.validated_data['layout'])
|
||||
oss.set_input(target_operation.pk, schema)
|
||||
return Response(
|
||||
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'])
|
||||
oss = m.OperationSchema(self.get_object())
|
||||
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.title = serializer.validated_data['item_data']['title']
|
||||
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())
|
||||
with transaction.atomic():
|
||||
oss.update_positions(serializer.validated_data['positions'])
|
||||
oss.update_layout(serializer.validated_data['layout'])
|
||||
oss.execute_operation(operation)
|
||||
|
||||
return Response(
|
||||
|
|
|
@ -9,6 +9,7 @@ from apps.rsform.models import Constituenta, CstType, RSForm
|
|||
class TestConstituenta(TestCase):
|
||||
''' Testing Constituenta model. '''
|
||||
|
||||
|
||||
def setUp(self):
|
||||
self.schema1 = RSForm.create(title='Test1')
|
||||
self.schema2 = RSForm.create(title='Test2')
|
||||
|
@ -47,6 +48,7 @@ class TestConstituenta(TestCase):
|
|||
self.assertEqual(cst.definition_resolved, '')
|
||||
self.assertEqual(cst.definition_raw, '')
|
||||
|
||||
|
||||
def test_extract_references(self):
|
||||
cst = Constituenta.objects.create(
|
||||
alias='X1',
|
||||
|
@ -57,6 +59,7 @@ class TestConstituenta(TestCase):
|
|||
)
|
||||
self.assertEqual(cst.extract_references(), set(['X1', 'X2', 'X3', 'X4', 'X5']))
|
||||
|
||||
|
||||
def text_apply_mapping(self):
|
||||
cst = Constituenta.objects.create(
|
||||
alias='X1',
|
||||
|
|
|
@ -9,6 +9,7 @@ from shared.DBTester import DBTester
|
|||
class TestRSForm(DBTester):
|
||||
''' Testing RSForm wrapper. '''
|
||||
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user1 = User.objects.create(username='User1')
|
||||
|
@ -103,6 +104,7 @@ class TestRSForm(DBTester):
|
|||
self.assertEqual(x2.schema, self.schema.model)
|
||||
self.assertEqual(x1.order, 0)
|
||||
|
||||
|
||||
def test_create_cst(self):
|
||||
data = {
|
||||
'alias': 'X3',
|
||||
|
@ -194,6 +196,7 @@ class TestRSForm(DBTester):
|
|||
self.assertEqual(d1.definition_raw, '@{DEL|sing}')
|
||||
self.assertEqual(d1.term_raw, '@{X2|plur}')
|
||||
|
||||
|
||||
def test_apply_mapping(self):
|
||||
x1 = self.schema.insert_new('X1')
|
||||
x2 = self.schema.insert_new('X11')
|
||||
|
|
|
@ -138,6 +138,7 @@ class TestRSFormViewset(EndpointTester):
|
|||
self.assertEqual(response.data['typification'], 'LOGIC')
|
||||
self.assertEqual(response.data['valueClass'], 'value')
|
||||
|
||||
|
||||
@decl_endpoint('/api/rsforms/{item}/check-constituenta', method='post')
|
||||
def test_check_constituenta_error(self):
|
||||
self.owned.insert_new('X1')
|
||||
|
@ -145,6 +146,7 @@ class TestRSFormViewset(EndpointTester):
|
|||
response = self.executeOK(data=data, item=self.owned_id)
|
||||
self.assertEqual(response.data['parseResult'], False)
|
||||
|
||||
|
||||
@decl_endpoint('/api/rsforms/{item}/resolve', method='post')
|
||||
def test_resolve(self):
|
||||
x1 = self.owned.insert_new(
|
||||
|
|
|
@ -7,6 +7,7 @@ from apps.rsform.graph import Graph
|
|||
class TestGraph(unittest.TestCase):
|
||||
''' Test class for graph. '''
|
||||
|
||||
|
||||
def test_construction(self):
|
||||
graph = Graph()
|
||||
self.assertFalse(graph.contains(1))
|
||||
|
@ -26,6 +27,7 @@ class TestGraph(unittest.TestCase):
|
|||
self.assertTrue(graph.has_edge(1, 3))
|
||||
self.assertTrue(graph.has_edge(2, 1))
|
||||
|
||||
|
||||
def test_remove_node(self):
|
||||
graph = Graph({
|
||||
1: [2],
|
||||
|
@ -39,6 +41,7 @@ class TestGraph(unittest.TestCase):
|
|||
self.assertEqual(graph.outputs[1], [])
|
||||
self.assertEqual(len(graph.outputs), 3)
|
||||
|
||||
|
||||
def test_remove_edge(self):
|
||||
graph = Graph({
|
||||
1: [2],
|
||||
|
@ -53,6 +56,7 @@ class TestGraph(unittest.TestCase):
|
|||
self.assertEqual(graph.outputs[1], [])
|
||||
graph.remove_edge(1, 2)
|
||||
|
||||
|
||||
def test_expand_outputs(self):
|
||||
graph = Graph({
|
||||
1: [2],
|
||||
|
@ -67,6 +71,7 @@ class TestGraph(unittest.TestCase):
|
|||
self.assertEqual(graph.expand_outputs([7]), [])
|
||||
self.assertEqual(graph.expand_outputs([2, 5]), [3, 6, 1])
|
||||
|
||||
|
||||
def test_expand_inputs(self):
|
||||
graph = Graph({
|
||||
1: [2],
|
||||
|
@ -101,6 +106,7 @@ class TestGraph(unittest.TestCase):
|
|||
7: [6]
|
||||
})
|
||||
|
||||
|
||||
def test_topological_order(self):
|
||||
self.assertEqual(Graph().topological_order(), [])
|
||||
graph = Graph({
|
||||
|
@ -122,6 +128,7 @@ class TestGraph(unittest.TestCase):
|
|||
})
|
||||
self.assertEqual(graph.topological_order(), [5, 3, 2, 4, 1])
|
||||
|
||||
|
||||
def test_sort_stable(self):
|
||||
graph = Graph({
|
||||
1: [2],
|
||||
|
|
|
@ -8,6 +8,7 @@ from apps.rsform.utils import apply_pattern, fix_old_references
|
|||
class TestUtils(unittest.TestCase):
|
||||
''' Test various utility functions. '''
|
||||
|
||||
|
||||
def test_apply_mapping_patter(self):
|
||||
mapping = {'X101': 'X20'}
|
||||
pattern = re.compile(r'(X[0-9]+)')
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
tzdata==2025.1
|
||||
tzdata==2025.2
|
||||
Django==5.1.7
|
||||
djangorestframework==3.15.2
|
||||
django-cors-headers==4.7.0
|
||||
|
@ -17,5 +17,5 @@ djangorestframework-stubs==3.15.3
|
|||
django-extensions==3.2.3
|
||||
django-stubs==5.1.3
|
||||
mypy==1.15.0
|
||||
pylint==3.3.5
|
||||
coverage==7.6.12
|
||||
pylint==3.3.6
|
||||
coverage==7.7.1
|
|
@ -1,4 +1,4 @@
|
|||
tzdata==2025.1
|
||||
tzdata==2025.2
|
||||
Django==5.1.7
|
||||
djangorestframework==3.15.2
|
||||
django-cors-headers==4.7.0
|
||||
|
|
13
rsconcept/backend/run_testfile.py
Normal file
13
rsconcept/backend/run_testfile.py
Normal 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__")
|
|
@ -14,6 +14,18 @@ def operationNotInOSS(title: str):
|
|||
return f'Операция не принадлежит ОСС: {title}'
|
||||
|
||||
|
||||
def parentNotInOSS():
|
||||
return f'Родительский блок не принадлежит ОСС'
|
||||
|
||||
|
||||
def childNotInOSS():
|
||||
return f'Дочерний элемент блок не принадлежит ОСС'
|
||||
|
||||
|
||||
def missingArguments():
|
||||
return 'Операция не содержит аргументов, при этом содержит отождествления'
|
||||
|
||||
|
||||
def exteorFileCorrupted():
|
||||
return 'Файл Экстеор не соответствует ожидаемому формату. Попробуйте сохранить файл в новой версии'
|
||||
|
||||
|
|
21
rsconcept/frontend/components.json
Normal file
21
rsconcept/frontend/components.json
Normal 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"
|
||||
}
|
2430
rsconcept/frontend/package-lock.json
generated
2430
rsconcept/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -15,69 +15,72 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@dagrejs/dagre": "^1.1.4",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@lezer/lr": "^1.4.2",
|
||||
"@tanstack/react-query": "^5.69.0",
|
||||
"@tanstack/react-query-devtools": "^5.69.0",
|
||||
"@radix-ui/react-popover": "^1.1.7",
|
||||
"@radix-ui/react-select": "^2.1.7",
|
||||
"@tanstack/react-query": "^5.72.2",
|
||||
"@tanstack/react-query-devtools": "^5.72.2",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"@uiw/codemirror-themes": "^4.23.10",
|
||||
"@uiw/react-codemirror": "^4.23.10",
|
||||
"axios": "^1.8.3",
|
||||
"axios": "^1.8.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"global": "^4.4.0",
|
||||
"js-file-download": "^0.4.12",
|
||||
"lucide-react": "^0.487.0",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.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-intl": "^7.1.6",
|
||||
"react-router": "^7.3.0",
|
||||
"react-scan": "^0.3.2",
|
||||
"react-select": "^5.10.1",
|
||||
"react-intl": "^7.1.10",
|
||||
"react-router": "^7.5.0",
|
||||
"react-scan": "^0.3.3",
|
||||
"react-tabs": "^6.1.0",
|
||||
"react-toastify": "^11.0.5",
|
||||
"react-tooltip": "^5.28.0",
|
||||
"react-tooltip": "^5.28.1",
|
||||
"react-zoom-pan-pinch": "^3.7.0",
|
||||
"reactflow": "^11.11.4",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tw-animate-css": "^1.2.5",
|
||||
"use-debounce": "^10.0.4",
|
||||
"zod": "^3.24.2",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lezer/generator": "^1.7.2",
|
||||
"@lezer/generator": "^1.7.3",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@tailwindcss/vite": "^4.0.14",
|
||||
"@tailwindcss/vite": "^4.1.3",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.13.10",
|
||||
"@types/react": "^19.0.11",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/react": "^19.1.1",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
||||
"@typescript-eslint/parser": "^8.0.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"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-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-hooks": "^5.2.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"globals": "^16.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"stylelint": "^16.16.0",
|
||||
"stylelint-config-recommended": "^15.0.0",
|
||||
"stylelint-config-standard": "^37.0.0",
|
||||
"stylelint": "^16.18.0",
|
||||
"stylelint-config-recommended": "^16.0.0",
|
||||
"stylelint-config-standard": "^38.0.0",
|
||||
"stylelint-config-tailwindcss": "^1.0.0",
|
||||
"tailwindcss": "^4.0.7",
|
||||
"ts-jest": "^29.2.6",
|
||||
"typescript": "^5.8.2",
|
||||
"typescript-eslint": "^8.26.1",
|
||||
"vite": "^6.2.2"
|
||||
},
|
||||
"overrides": {
|
||||
"react": "^19.0.0"
|
||||
"ts-jest": "^29.3.1",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.29.1",
|
||||
"vite": "^6.2.6"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "ts-jest",
|
||||
|
|
|
@ -10,7 +10,7 @@ export function Footer() {
|
|||
'z-navigation',
|
||||
'mx-auto',
|
||||
'px-3 py-2 flex flex-col items-center gap-1',
|
||||
'text-xs sm:text-sm select-none whitespace-nowrap text-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='Вторичная навигация'>
|
||||
|
@ -20,7 +20,7 @@ export function Footer() {
|
|||
<TextURL text='Экстеор' href='/manuals?topic=exteor' color='' />
|
||||
</nav>
|
||||
|
||||
<p>© 2024 ЦИВТ КОНЦЕПТ</p>
|
||||
<p>© 2025 ЦИВТ КОНЦЕПТ</p>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ export function GlobalLoader() {
|
|||
return (
|
||||
<div className='cc-modal-wrapper'>
|
||||
<ModalBackdrop />
|
||||
<div className='z-pop cc-fade-in px-10 border rounded-xl bg-prim-100'>
|
||||
<div className='z-pop cc-fade-in px-10 border rounded-xl bg-background'>
|
||||
<Loader scale={6} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -20,9 +20,9 @@ export function MutationErrors() {
|
|||
return (
|
||||
<div className='cc-modal-wrapper'>
|
||||
<ModalBackdrop onHide={resetErrors} />
|
||||
<div className='z-pop px-10 py-3 flex flex-col items-center border rounded-xl bg-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>
|
||||
<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]} />
|
||||
</div>
|
||||
<Button onClick={resetErrors} className='w-fit' text='Закрыть' />
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { type Styling } from '@/components/props';
|
||||
import { cn } from '@/components/utils';
|
||||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
interface NavigationButtonProps extends Styling {
|
||||
|
@ -21,7 +20,7 @@ export function NavigationButton({ icon, title, hideTitle, className, style, onC
|
|||
data-tooltip-hidden={hideTitle}
|
||||
data-tooltip-content={title}
|
||||
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}
|
||||
>
|
||||
{icon ? icon : null}
|
||||
|
|
|
@ -3,6 +3,7 @@ import clsx from 'clsx';
|
|||
import { IconLibrary2, IconManuals, IconNewItem2 } from '@/components/icons';
|
||||
import { useWindowSize } from '@/hooks/use-window-size';
|
||||
import { useAppLayoutStore } from '@/stores/app-layout';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
|
||||
import { urls } from '../urls';
|
||||
|
||||
|
@ -16,6 +17,7 @@ export function Navigation() {
|
|||
const { push } = useConceptNavigation();
|
||||
const size = useWindowSize();
|
||||
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
|
||||
const activeDialog = useDialogsStore(state => state.active);
|
||||
|
||||
const navigateHome = (event: React.MouseEvent<Element>) =>
|
||||
push({ path: urls.home, newTab: event.ctrlKey || event.metaKey });
|
||||
|
@ -27,7 +29,7 @@ export function Navigation() {
|
|||
push({ path: urls.create_schema, newTab: event.ctrlKey || event.metaKey });
|
||||
|
||||
return (
|
||||
<nav className='z-navigation sticky top-0 left-0 right-0 select-none bg-prim-100'>
|
||||
<nav className='z-navigation sticky top-0 left-0 right-0 select-none bg-background' inert={activeDialog !== null}>
|
||||
<ToggleNavigation />
|
||||
<div
|
||||
className={clsx(
|
||||
|
|
|
@ -13,7 +13,7 @@ export function UserMenu() {
|
|||
const router = useConceptNavigation();
|
||||
const menu = useDropdown();
|
||||
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} />}>
|
||||
<UserButton
|
||||
onLogin={() => router.push({ path: urls.login, force: true })}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { type Styling } from '../props';
|
||||
import { cn } from '../utils';
|
||||
|
||||
interface DividerProps extends Styling {
|
||||
/** Indicates whether the divider is vertical. */
|
||||
|
@ -16,7 +15,7 @@ interface DividerProps extends Styling {
|
|||
export function Divider({ vertical, margins = 'mx-2', className, ...restProps }: DividerProps) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
className={cn(
|
||||
vertical ? 'border-x' : 'border-y', //
|
||||
margins,
|
||||
className
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
import { type ReactNode } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { type ITooltip, Tooltip as TooltipImpl } from 'react-tooltip';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
|
||||
import { cn } from '../utils';
|
||||
|
||||
export type { PlacesType } from 'react-tooltip';
|
||||
|
||||
interface TooltipProps extends Omit<ITooltip, 'variant'> {
|
||||
|
@ -37,7 +38,7 @@ export function Tooltip({
|
|||
delayShow={750}
|
||||
delayHide={100}
|
||||
opacity={1}
|
||||
className={clsx(
|
||||
className={cn(
|
||||
'relative',
|
||||
'py-0.5! px-2!',
|
||||
'max-h-[calc(100svh-6rem)]',
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
import { type Button as ButtonStyle, type Control } from '../props';
|
||||
import { cn } from '../utils';
|
||||
|
||||
interface ButtonProps extends Control, ButtonStyle {
|
||||
/** Icon to display first. */
|
||||
|
@ -38,13 +37,13 @@ export function Button({
|
|||
return (
|
||||
<button
|
||||
type='button'
|
||||
className={clsx(
|
||||
className={cn(
|
||||
'inline-flex gap-2 items-center justify-center',
|
||||
'font-medium select-none disabled:cursor-auto',
|
||||
'clr-btn-default cc-animate-color',
|
||||
'font-medium select-none disabled:cursor-auto disabled:opacity-75',
|
||||
'bg-secondary text-secondary-foreground cc-hover cc-animate-color',
|
||||
dense ? 'px-1' : 'px-3 py-1',
|
||||
loading ? 'cursor-progress' : 'cursor-pointer',
|
||||
noOutline ? 'outline-hidden' : 'focus-outline',
|
||||
noOutline ? 'outline-hidden focus-visible:bg-selected' : 'focus-outline',
|
||||
!noBorder && 'border rounded-sm',
|
||||
className
|
||||
)}
|
||||
|
|
|
@ -39,9 +39,9 @@ export function MiniButton({
|
|||
tabIndex={tabIndex ?? -1}
|
||||
className={clsx(
|
||||
'rounded-lg',
|
||||
'clr-text-controls cc-animate-color',
|
||||
'cc-controls cc-animate-background',
|
||||
'cursor-pointer disabled:cursor-auto',
|
||||
noHover ? 'outline-hidden' : 'clr-hover',
|
||||
noHover ? 'outline-hidden' : 'cc-hover',
|
||||
!noPadding && 'px-1 py-1',
|
||||
className
|
||||
)}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
import { type Button } from '../props';
|
||||
import { cn } from '../utils';
|
||||
|
||||
interface SelectorButtonProps extends Button {
|
||||
/** Text to display in the button. */
|
||||
|
@ -10,9 +9,6 @@ interface SelectorButtonProps extends Button {
|
|||
|
||||
/** Icon to display in the button. */
|
||||
icon?: React.ReactNode;
|
||||
|
||||
/** Indicates if button background should be transparent. */
|
||||
transparent?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -24,7 +20,6 @@ export function SelectorButton({
|
|||
title,
|
||||
titleHtml,
|
||||
className,
|
||||
transparent,
|
||||
hideTitle,
|
||||
...restProps
|
||||
}: SelectorButtonProps) {
|
||||
|
@ -32,13 +27,12 @@ export function SelectorButton({
|
|||
<button
|
||||
type='button'
|
||||
tabIndex={-1}
|
||||
className={clsx(
|
||||
className={cn(
|
||||
'px-1 flex flex-start items-center gap-1',
|
||||
'text-sm font-controls select-none',
|
||||
'text-btn clr-text-controls',
|
||||
'text-btn cc-controls',
|
||||
'disabled:cursor-auto cursor-pointer',
|
||||
'cc-animate-color',
|
||||
transparent ? 'clr-hover' : 'clr-btn-default border',
|
||||
'cc-hover cc-animate-color',
|
||||
className
|
||||
)}
|
||||
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { type Button } from '../props';
|
||||
import { cn } from '../utils';
|
||||
|
||||
interface SubmitButtonProps extends Button {
|
||||
/** Text to display in the button. */
|
||||
|
@ -20,12 +19,12 @@ export function SubmitButton({ text = 'ОК', icon, disabled, loading, className
|
|||
return (
|
||||
<button
|
||||
type='submit'
|
||||
className={clsx(
|
||||
className={cn(
|
||||
'px-3 py-1 flex gap-2 items-center justify-center',
|
||||
'border',
|
||||
'font-medium',
|
||||
'clr-btn-primary cc-animate-color',
|
||||
'select-none disabled:cursor-auto',
|
||||
'cc-btn-primary disabled:opacity-50 cc-animate-color',
|
||||
'select-none cursor-pointer disabled:cursor-auto',
|
||||
loading && 'cursor-progress',
|
||||
className
|
||||
)}
|
||||
|
|
|
@ -20,7 +20,7 @@ interface TextURLProps {
|
|||
/**
|
||||
* 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}`;
|
||||
if (href) {
|
||||
return (
|
||||
|
@ -38,4 +38,3 @@ export function TextURL({ text, href, title, color = 'text-sec-600', onClick }:
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,9 +10,9 @@ import {
|
|||
type TableOptions,
|
||||
type VisibilityState
|
||||
} from '@tanstack/react-table';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { type Styling } from '../props';
|
||||
import { cn } from '../utils';
|
||||
|
||||
import { DefaultNoData } from './default-no-data';
|
||||
import { PaginationTools } from './pagination-tools';
|
||||
|
@ -157,7 +157,7 @@ export function DataTable<TData extends RowData>({
|
|||
<div
|
||||
tabIndex={-1}
|
||||
id={id}
|
||||
className={clsx('table-auto', className)}
|
||||
className={cn('table-auto', className)}
|
||||
style={{ minHeight: fixedSize, maxHeight: fixedSize, ...style }}
|
||||
>
|
||||
<table className='w-full' style={{ ...columnSizeVars }}>
|
||||
|
|
|
@ -31,7 +31,7 @@ export function PaginationTools<TData>({
|
|||
);
|
||||
|
||||
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'>
|
||||
{`${table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1}
|
||||
-
|
||||
|
@ -46,7 +46,7 @@ export function PaginationTools<TData>({
|
|||
<button
|
||||
type='button'
|
||||
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)}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
|
@ -55,7 +55,7 @@ export function PaginationTools<TData>({
|
|||
<button
|
||||
type='button'
|
||||
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()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
|
@ -65,7 +65,7 @@ export function PaginationTools<TData>({
|
|||
id={id ? `${id}__page` : undefined}
|
||||
title='Номер страницы. Выделите для ручного ввода'
|
||||
aria-label='Номер страницы'
|
||||
className='w-6 text-center bg-prim-100 focus-outline'
|
||||
className='w-6 text-center bg-transparent focus-outline'
|
||||
value={table.getState().pagination.pageIndex + 1}
|
||||
onChange={event => {
|
||||
const page = event.target.value ? Number(event.target.value) - 1 : 0;
|
||||
|
@ -77,7 +77,7 @@ export function PaginationTools<TData>({
|
|||
<button
|
||||
type='button'
|
||||
aria-label='Следующая страница'
|
||||
className='clr-hover clr-text-controls cc-animate-color focus-outline'
|
||||
className='cc-hover cc-controls cc-animate-color focus-outline'
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
|
@ -86,7 +86,7 @@ export function PaginationTools<TData>({
|
|||
<button
|
||||
type='button'
|
||||
aria-label='Последняя страница'
|
||||
className='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)}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
|
@ -98,7 +98,7 @@ export function PaginationTools<TData>({
|
|||
aria-label='Выбор количества строчек на странице'
|
||||
value={table.getState().pagination.pageSize}
|
||||
onChange={handlePaginationOptionsChange}
|
||||
className='mx-2 cursor-pointer bg-prim-100 focus-outline'
|
||||
className='mx-2 cursor-pointer bg-transparent focus-outline'
|
||||
>
|
||||
{paginationOptions.map(pageSize => (
|
||||
<option key={`${prefixes.page_size}${pageSize}`} value={pageSize} aria-label={`${pageSize} на страницу`}>
|
||||
|
|
|
@ -75,11 +75,11 @@ export function TableBody<TData>({
|
|||
key={row.id}
|
||||
className={clsx(
|
||||
'cc-scroll-row',
|
||||
'clr-hover cc-animate-color',
|
||||
'cc-hover cc-animate-background duration-(--duration-fade)',
|
||||
!noHeader && 'scroll-mt-[calc(2px+2rem)]',
|
||||
table.options.enableRowSelection && row.getIsSelected()
|
||||
? 'clr-selected'
|
||||
: 'odd:bg-prim-200 even:bg-prim-100'
|
||||
? 'cc-selected'
|
||||
: 'odd:bg-secondary even:bg-background'
|
||||
)}
|
||||
style={{ ...(conditionalRowStyles ? getRowStyles(row) : []) }}
|
||||
onClick={event => handleRowClicked(row, event)}
|
||||
|
|
|
@ -14,7 +14,7 @@ interface TableHeaderProps<TData> {
|
|||
|
||||
export function TableHeader<TData>({ table, headPosition, resetLastSelected }: TableHeaderProps<TData>) {
|
||||
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>) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{table.options.enableRowSelection ? (
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { type Button } from '@/components/props';
|
||||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
import { cn } from '../utils';
|
||||
|
||||
interface DropdownButtonProps extends Button {
|
||||
/** Icon to display first (not used if children are provided). */
|
||||
icon?: React.ReactNode;
|
||||
|
@ -33,12 +33,12 @@ export function DropdownButton({
|
|||
<button
|
||||
type='button'
|
||||
onClick={onClick}
|
||||
className={clsx(
|
||||
className={cn(
|
||||
'px-3 py-1 inline-flex items-center gap-2',
|
||||
'text-left text-sm text-ellipsis whitespace-nowrap',
|
||||
'disabled:clr-text-controls',
|
||||
'cc-animate-color',
|
||||
!!onClick ? 'clr-hover cursor-pointer disabled:cursor-auto' : 'clr-btn-default',
|
||||
'disabled:cc-controls disabled:opacity-75',
|
||||
'focus-outline cc-animate-background',
|
||||
!!onClick ? 'cc-hover cursor-pointer disabled:cursor-auto' : 'bg-secondary text-secondary-foreground',
|
||||
className
|
||||
)}
|
||||
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { type Styling } from '../props';
|
||||
import { cn } from '../utils';
|
||||
|
||||
interface DropdownProps extends Styling {
|
||||
/** Reference to the dropdown element. */
|
||||
|
@ -39,8 +37,8 @@ export function Dropdown({
|
|||
return (
|
||||
<div
|
||||
tabIndex={-1}
|
||||
className={clsx(
|
||||
'cc-dropdown isolate z-topmost absolute grid bg-prim-0 border rounded-md shadow-lg text-sm',
|
||||
className={cn(
|
||||
'cc-dropdown isolate z-topmost absolute grid bg-popover border rounded-md shadow-lg text-sm',
|
||||
stretchLeft ? 'right-0' : 'left-0',
|
||||
stretchTop ? 'bottom-0' : 'top-full',
|
||||
isOpen && 'open',
|
||||
|
|
|
@ -7,9 +7,10 @@ export function useDropdown() {
|
|||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
function handleBlur(event: React.FocusEvent<HTMLDivElement>) {
|
||||
if (!ref.current?.contains(event.relatedTarget as Node)) {
|
||||
setIsOpen(false);
|
||||
if (ref.current?.contains(event.relatedTarget as Node)) {
|
||||
return;
|
||||
}
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -146,6 +146,7 @@ export { BiGitMerge as IconGraphOutputs } from 'react-icons/bi';
|
|||
export { LuAtom as IconGraphCore } from 'react-icons/lu';
|
||||
export { LuRotate3D as IconRotate3D } from 'react-icons/lu';
|
||||
export { MdOutlineFitScreen as IconFitImage } from 'react-icons/md';
|
||||
export { RiFocus3Line as IconFocus } from 'react-icons/ri';
|
||||
export { LuSparkles as IconClustering } from 'react-icons/lu';
|
||||
export { LuSparkle as IconClusteringOff } from 'react-icons/lu';
|
||||
export { TbGridDots as IconGrid } from 'react-icons/tb';
|
||||
|
|
|
@ -81,11 +81,11 @@ export function InfoError({ error }: InfoErrorProps) {
|
|||
'cc-fade-in',
|
||||
'min-w-100',
|
||||
'px-3 py-2 flex flex-col',
|
||||
'text-warn-600 text-sm font-semibold',
|
||||
'text-destructive text-sm font-semibold',
|
||||
'select-text'
|
||||
)}
|
||||
>
|
||||
<div className='font-normal clr-text-default mb-6'>
|
||||
<div className='font-normal text-foreground mb-6'>
|
||||
<p>Пожалуйста сделайте скриншот и отправьте вместе с описанием ситуации на почту portal@acconcept.ru</p>
|
||||
<br />
|
||||
<p>Для продолжения работы перезагрузите страницу</p>
|
||||
|
|
|
@ -3,6 +3,7 @@ import clsx from 'clsx';
|
|||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
import { CheckboxChecked, CheckboxNull } from '../icons';
|
||||
import { cn } from '../utils';
|
||||
|
||||
import { type CheckboxProps } from './checkbox';
|
||||
|
||||
|
@ -48,7 +49,7 @@ export function CheckboxTristate({
|
|||
return (
|
||||
<button
|
||||
type='button'
|
||||
className={clsx(
|
||||
className={cn(
|
||||
'flex items-center gap-2', //
|
||||
'outline-hidden',
|
||||
'focus-frame',
|
||||
|
@ -67,7 +68,7 @@ export function CheckboxTristate({
|
|||
className={clsx(
|
||||
'w-4 h-4', //
|
||||
'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}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { globalIDs } from '@/utils/constants';
|
|||
|
||||
import { CheckboxChecked } from '../icons';
|
||||
import { type Button } from '../props';
|
||||
import { cn } from '../utils';
|
||||
|
||||
export interface CheckboxProps extends Omit<Button, 'value' | 'onClick' | 'onChange'> {
|
||||
/** Label to display next to the checkbox. */
|
||||
|
@ -47,7 +48,7 @@ export function Checkbox({
|
|||
return (
|
||||
<button
|
||||
type='button'
|
||||
className={clsx(
|
||||
className={cn(
|
||||
'flex items-center gap-2', //
|
||||
'outline-hidden',
|
||||
'focus-frame',
|
||||
|
@ -66,7 +67,7 @@ export function Checkbox({
|
|||
className={clsx(
|
||||
'w-4 h-4', //
|
||||
'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}
|
||||
|
|
127
rsconcept/frontend/src/components/input/combo-box.tsx
Normal file
127
rsconcept/frontend/src/components/input/combo-box.tsx
Normal 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>
|
||||
);
|
||||
}
|
145
rsconcept/frontend/src/components/input/combo-multi.tsx
Normal file
145
rsconcept/frontend/src/components/input/combo-multi.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -15,7 +15,7 @@ export function ErrorField({ error, className, ...restProps }: ErrorFieldProps):
|
|||
return null;
|
||||
}
|
||||
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}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
export { Checkbox, type CheckboxProps } from './checkbox';
|
||||
export { Checkbox } from './checkbox';
|
||||
export { CheckboxTristate } from './checkbox-tristate';
|
||||
export { ErrorField } from './error-field';
|
||||
export { FileInput } from './file-input';
|
||||
export { Label } from './label';
|
||||
export { SearchBar } from './search-bar';
|
||||
export { SelectMulti, type SelectMultiProps } from './select-multi';
|
||||
export { SelectSingle } from './select-single';
|
||||
export { SelectTree } from './select-tree';
|
||||
export { TextArea } from './text-area';
|
||||
export { TextInput } from './text-input';
|
||||
|
|
|
@ -3,7 +3,7 @@ import clsx from 'clsx';
|
|||
import { IconSearch } from '@/components/icons';
|
||||
import { type Styling } from '@/components/props';
|
||||
|
||||
import { TextInput } from './text-input';
|
||||
import { cn } from '../utils';
|
||||
|
||||
interface SearchBarProps extends Styling {
|
||||
/** Id of the search bar. */
|
||||
|
@ -39,20 +39,23 @@ export function SearchBar({
|
|||
...restProps
|
||||
}: SearchBarProps) {
|
||||
return (
|
||||
<div className={clsx('relative flex items-center', className)} {...restProps}>
|
||||
<div className={cn('relative flex items-center grow', className)} {...restProps}>
|
||||
{!noIcon ? (
|
||||
<IconSearch className='absolute -top-0.5 left-2 translate-y-1/2 cc-search-icon' size='1.25rem' />
|
||||
) : null}
|
||||
<TextInput
|
||||
<input
|
||||
id={id}
|
||||
noOutline
|
||||
transparent
|
||||
placeholder={placeholder}
|
||||
type='search'
|
||||
className={clsx('bg-transparent', !noIcon && 'pl-8')}
|
||||
noBorder={noBorder}
|
||||
className={clsx(
|
||||
'min-w-0 py-2',
|
||||
'leading-tight truncate hover:text-clip',
|
||||
'bg-transparent',
|
||||
!noIcon && 'pl-8',
|
||||
!noBorder && 'border px-3'
|
||||
)}
|
||||
value={query}
|
||||
onChange={event => onChangeQuery?.(event.target.value)}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -89,9 +89,9 @@ export function SelectTree<ItemType>({
|
|||
<div
|
||||
key={`${prefix}${index}`}
|
||||
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',
|
||||
value === item && 'clr-selected'
|
||||
value === item && 'cc-selected'
|
||||
)}
|
||||
data-tooltip-id={globalIDs.tooltip}
|
||||
data-tooltip-html={getDescription(item)}
|
||||
|
|
173
rsconcept/frontend/src/components/input/select.tsx
Normal file
173
rsconcept/frontend/src/components/input/select.tsx
Normal 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
|
||||
};
|
|
@ -1,6 +1,5 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { type Editor, type ErrorProcessing, type Titled } from '../props';
|
||||
import { cn } from '../utils';
|
||||
|
||||
import { ErrorField } from './error-field';
|
||||
import { Label } from './label';
|
||||
|
@ -25,21 +24,20 @@ interface TextAreaProps extends Editor, ErrorProcessing, Titled, React.Component
|
|||
export function TextArea({
|
||||
id,
|
||||
label,
|
||||
required,
|
||||
transparent,
|
||||
rows,
|
||||
dense,
|
||||
noBorder,
|
||||
noOutline,
|
||||
noResize,
|
||||
className,
|
||||
fitContent,
|
||||
disabled,
|
||||
error,
|
||||
...restProps
|
||||
}: TextAreaProps) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
className={cn(
|
||||
'w-full', //
|
||||
dense ? 'flex grow items-center gap-3' : 'flex flex-col',
|
||||
dense && className
|
||||
|
@ -48,21 +46,20 @@ export function TextArea({
|
|||
<Label text={label} htmlFor={id} />
|
||||
<textarea
|
||||
id={id}
|
||||
className={clsx(
|
||||
className={cn(
|
||||
'px-3 py-2',
|
||||
'leading-tight',
|
||||
'overflow-x-hidden overflow-y-auto',
|
||||
!noBorder && 'border',
|
||||
fitContent && 'field-sizing-content',
|
||||
noResize && 'resize-none',
|
||||
transparent ? 'bg-transparent' : 'clr-input',
|
||||
transparent || disabled ? 'bg-transparent' : 'bg-input',
|
||||
!noOutline && 'focus-outline',
|
||||
dense && 'grow max-w-full',
|
||||
!dense && !!label && 'mt-2',
|
||||
!dense && className
|
||||
)}
|
||||
rows={rows}
|
||||
required={required}
|
||||
disabled={disabled}
|
||||
{...restProps}
|
||||
/>
|
||||
<ErrorField className='mt-1' error={error} />
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { type Editor, type ErrorProcessing, type Titled } from '../props';
|
||||
import { cn } from '../utils';
|
||||
|
||||
import { ErrorField } from './error-field';
|
||||
import { Label } from './label';
|
||||
|
@ -41,7 +40,7 @@ export function TextInput({
|
|||
}: TextInputProps) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
className={cn(
|
||||
dense ? 'flex items-center gap-3' : 'flex flex-col', //
|
||||
dense && className
|
||||
)}
|
||||
|
@ -49,10 +48,10 @@ export function TextInput({
|
|||
<Label text={label} htmlFor={id} />
|
||||
<input
|
||||
id={id}
|
||||
className={clsx(
|
||||
className={cn(
|
||||
'min-w-0 py-2',
|
||||
'leading-tight truncate hover:text-clip',
|
||||
transparent ? 'bg-transparent' : 'clr-input',
|
||||
transparent || disabled ? 'bg-transparent' : 'bg-input',
|
||||
!noBorder && 'border',
|
||||
!noOutline && 'focus-outline',
|
||||
(!noBorder || !disabled) && 'px-3',
|
||||
|
|
|
@ -8,7 +8,7 @@ export function ModalBackdrop({ onHide }: ModalBackdropProps) {
|
|||
return (
|
||||
<>
|
||||
<div className='z-bottom fixed inset-0 backdrop-blur-[3px] opacity-50' />
|
||||
<div className='z-bottom fixed inset-0 bg-prim-0 opacity-25' onClick={onHide} />
|
||||
<div className='z-bottom fixed inset-0 bg-popover opacity-25' onClick={onHide} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { type HelpTopic } from '@/features/help';
|
||||
import { BadgeHelp } from '@/features/help/components';
|
||||
|
||||
|
@ -12,6 +10,7 @@ import { prepareTooltip } from '@/utils/utils';
|
|||
import { Button, MiniButton, SubmitButton } from '../control';
|
||||
import { IconClose } from '../icons';
|
||||
import { type Styling } from '../props';
|
||||
import { cn } from '../utils';
|
||||
|
||||
import { ModalBackdrop } from './modal-backdrop';
|
||||
|
||||
|
@ -90,7 +89,7 @@ export function ModalForm({
|
|||
<div className='cc-modal-wrapper'>
|
||||
<ModalBackdrop onHide={handleCancel} />
|
||||
<form
|
||||
className='cc-animate-modal relative grid border rounded-xl bg-prim-100'
|
||||
className='cc-animate-modal relative grid border rounded-xl bg-background'
|
||||
role='dialog'
|
||||
onSubmit={handleSubmit}
|
||||
aria-labelledby='modal-title'
|
||||
|
@ -120,7 +119,7 @@ export function ModalForm({
|
|||
) : null}
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
className={cn(
|
||||
'@container/modal',
|
||||
'max-h-[calc(100svh-8rem)] max-w-[100svw] xs:max-w-[calc(100svw-2rem)]',
|
||||
'overscroll-contain outline-hidden',
|
||||
|
|
|
@ -6,7 +6,7 @@ export function ModalLoader() {
|
|||
return (
|
||||
<div className='cc-modal-wrapper'>
|
||||
<ModalBackdrop />
|
||||
<div className='cc-animate-modal p-20 border rounded-xl bg-prim-100'>
|
||||
<div className='cc-animate-modal p-20 border rounded-xl bg-background'>
|
||||
<Loader circular scale={6} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -10,6 +10,7 @@ import { prepareTooltip } from '@/utils/utils';
|
|||
|
||||
import { Button, MiniButton } from '../control';
|
||||
import { IconClose } from '../icons';
|
||||
import { cn } from '../utils';
|
||||
|
||||
import { ModalBackdrop } from './modal-backdrop';
|
||||
import { type ModalProps } from './modal-form';
|
||||
|
@ -38,7 +39,7 @@ export function ModalView({
|
|||
return (
|
||||
<div className='cc-modal-wrapper'>
|
||||
<ModalBackdrop onHide={hideDialog} />
|
||||
<div className='cc-animate-modal relative grid border rounded-xl bg-prim-100' role='dialog'>
|
||||
<div className='cc-animate-modal relative grid border rounded-xl bg-background' role='dialog'>
|
||||
{helpTopic && !hideHelpWhen?.() ? (
|
||||
<BadgeHelp
|
||||
topic={helpTopic}
|
||||
|
@ -61,7 +62,8 @@ export function ModalView({
|
|||
<h1
|
||||
className={clsx(
|
||||
'px-12 py-2 select-none',
|
||||
fullScreen && 'z-pop absolute top-0 right-1/2 translate-x-1/2 backdrop-blur-xs bg-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}
|
||||
|
@ -69,7 +71,7 @@ export function ModalView({
|
|||
) : null}
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
className={cn(
|
||||
'@container/modal',
|
||||
'max-w-[100svw] xs:max-w-[calc(100svw-2rem)]',
|
||||
'overscroll-contain outline-hidden',
|
||||
|
@ -93,7 +95,7 @@ export function ModalView({
|
|||
onClick={hideDialog}
|
||||
/>
|
||||
) : (
|
||||
<div className='z-pop absolute bottom-0 right-1/2 translate-x-1/2 p-3 rounded-xl bg-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} />
|
||||
</div>
|
||||
|
|
|
@ -28,7 +28,7 @@ export function TabLabel({
|
|||
className={clsx(
|
||||
'min-w-20 h-full',
|
||||
'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',
|
||||
'select-none hover:cursor-pointer',
|
||||
'outline-hidden',
|
||||
|
|
131
rsconcept/frontend/src/components/ui/command.tsx
Normal file
131
rsconcept/frontend/src/components/ui/command.tsx
Normal 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
|
||||
};
|
110
rsconcept/frontend/src/components/ui/dialog.tsx
Normal file
110
rsconcept/frontend/src/components/ui/dialog.tsx
Normal 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
|
||||
};
|
39
rsconcept/frontend/src/components/ui/popover.tsx
Normal file
39
rsconcept/frontend/src/components/ui/popover.tsx
Normal 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 };
|
6
rsconcept/frontend/src/components/utils.ts
Normal file
6
rsconcept/frontend/src/components/utils.ts
Normal 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));
|
||||
}
|
|
@ -18,7 +18,7 @@ export function Indicator({ icon, title, titleHtml, hideTitle, noPadding, classN
|
|||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'clr-text-controls', //
|
||||
'cc-controls', //
|
||||
'outline-hidden',
|
||||
!noPadding && 'px-1 py-1',
|
||||
className
|
||||
|
|
|
@ -24,7 +24,7 @@ export function ExpectedAnonymous() {
|
|||
<span> | </span>
|
||||
<TextURL text='Справка' href='/manuals' />
|
||||
<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>
|
||||
</div>
|
||||
|
|
|
@ -95,7 +95,7 @@ export function LoginPage() {
|
|||
function ServerError({ error }: { error: ErrorData }): React.ReactElement | null {
|
||||
if (isAxiosError(error) && error.response && error.response.status === 400) {
|
||||
return (
|
||||
<div className='text-sm select-text text-warn-600'>
|
||||
<div className='text-sm select-text text-destructive'>
|
||||
На Портале отсутствует такое сочетание имени пользователя и пароля
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -88,7 +88,7 @@ export function Component() {
|
|||
// ====== Internals =========
|
||||
function ServerError({ error }: { error: ErrorData }): React.ReactElement {
|
||||
if (isAxiosError(error) && error.response && error.response.status === 404) {
|
||||
return <div className='mx-auto mt-6 text-sm select-text text-warn-600'>Данная ссылка не действительна</div>;
|
||||
return <div className='mx-auto mt-6 text-sm select-text text-destructive'>Данная ссылка не действительна</div>;
|
||||
}
|
||||
return <InfoError error={error} />;
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ export function Component() {
|
|||
function ServerError({ error }: { error: ErrorData }): React.ReactElement {
|
||||
if (isAxiosError(error) && error.response && error.response.status === 400) {
|
||||
return (
|
||||
<div className='mx-auto mt-6 text-sm select-text text-warn-600'>Данный email не используется на Портале.</div>
|
||||
<div className='mx-auto mt-6 text-sm select-text text-destructive'>Данный email не используется на Портале.</div>
|
||||
);
|
||||
}
|
||||
throw error as Error;
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import React, { Suspense } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { type PlacesType, Tooltip } from '@/components/container';
|
||||
import { TextURL } from '@/components/control';
|
||||
import { IconHelp } from '@/components/icons';
|
||||
import { Loader } from '@/components/loader';
|
||||
import { type Styling } from '@/components/props';
|
||||
import { cn } from '@/components/utils';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
|
||||
import { type HelpTopic } from '../models/help-topic';
|
||||
|
@ -41,17 +41,17 @@ export function BadgeHelp({ topic, padding = 'p-1', className, contentClass, sty
|
|||
return null;
|
||||
}
|
||||
return (
|
||||
<div tabIndex={-1} id={`help-${topic}`} className={clsx(padding, className)} style={style}>
|
||||
<div tabIndex={-1} id={`help-${topic}`} className={cn(padding, className)} style={style}>
|
||||
<IconHelp size='1.25rem' className='icon-primary' />
|
||||
<Tooltip
|
||||
clickable
|
||||
anchorSelect={`#help-${topic}`}
|
||||
layer='z-topmost'
|
||||
className={clsx('max-w-120', contentClass)}
|
||||
className={cn('max-w-120', contentClass)}
|
||||
{...restProps}
|
||||
>
|
||||
<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}`} />
|
||||
</div>
|
||||
<TopicPage topic={topic} />
|
||||
|
|
|
@ -25,7 +25,7 @@ export function HelpFormulaTree() {
|
|||
<span className='bg-(--acc-bg-red)'>присвоение и итерация</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className='bg-prim-300'>составные выражения</span>
|
||||
<span className='bg-secondary'>составные выражения</span>
|
||||
</li>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -81,7 +81,7 @@ export function HelpLibrary() {
|
|||
<kbd>клик</kbd> по иконке сворачивает/разворачивает вложенные
|
||||
</li>
|
||||
<li>
|
||||
<IconFolderEmpty size='1rem' className='inline-icon clr-text-default' /> папка без схем
|
||||
<IconFolderEmpty size='1rem' className='inline-icon text-foreground' /> папка без схем
|
||||
</li>
|
||||
<li>
|
||||
<IconFolderEmpty size='1rem' className='inline-icon' /> папка с вложенными без схем
|
||||
|
|
|
@ -68,7 +68,7 @@ export function HelpRSEditor() {
|
|||
<IconChild className='inline-icon' /> отображение наследованных
|
||||
</li>
|
||||
<li>
|
||||
<span className='bg-sec-200'>текущая конституента</span>
|
||||
<span className='bg-selected'>текущая конституента</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className='bg-(--acc-bg-green50)'>
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
IconEdit,
|
||||
IconFilter,
|
||||
IconFitImage,
|
||||
IconFocus,
|
||||
IconGraphCollapse,
|
||||
IconGraphCore,
|
||||
IconGraphExpand,
|
||||
|
@ -47,9 +48,7 @@ export function HelpRSGraphTerm() {
|
|||
<div className='sm:w-84'>
|
||||
<h1>Изменение узлов</h1>
|
||||
<li>Клик на узел – выделение</li>
|
||||
<li>
|
||||
Левый клик – выбор <span className='text-(--acc-fg-purple)'>фокус-конституенты</span>
|
||||
</li>
|
||||
<li>Левый клик – выбор фокус-конституенты</li>
|
||||
<li>
|
||||
<IconReset className='inline-icon' /> Esc – сбросить выделение
|
||||
</li>
|
||||
|
@ -76,6 +75,9 @@ export function HelpRSGraphTerm() {
|
|||
<li>
|
||||
<IconFilter className='inline-icon' /> Открыть настройки
|
||||
</li>
|
||||
<li>
|
||||
<IconFocus className='inline-icon' /> Задать фокус
|
||||
</li>
|
||||
<li>
|
||||
<IconFitImage className='inline-icon' /> Вписать в экран
|
||||
</li>
|
||||
|
|
|
@ -42,7 +42,6 @@ export function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownPro
|
|||
>
|
||||
<Button
|
||||
noOutline
|
||||
tabIndex={-1}
|
||||
title='Список тем'
|
||||
hideTitle={menu.isOpen}
|
||||
icon={!menu.isOpen ? <IconMenuUnfold size='1.25rem' /> : <IconMenuFold size='1.25rem' />}
|
||||
|
|
|
@ -20,7 +20,7 @@ export const useSetAccessPolicy = () => {
|
|||
client.setQueryData(ossKey, { ...ossData, access_policy: variables.policy });
|
||||
return Promise.allSettled([
|
||||
client.invalidateQueries({ queryKey: KEYS.composite.libraryList }),
|
||||
...ossData.items
|
||||
...ossData.operations
|
||||
.map(item => {
|
||||
if (!item.result) {
|
||||
return;
|
||||
|
|
|
@ -18,7 +18,7 @@ export const useSetEditors = () => {
|
|||
if (ossData) {
|
||||
client.setQueryData(ossKey, { ...ossData, editors: variables.editors });
|
||||
return Promise.allSettled(
|
||||
ossData.items
|
||||
ossData.operations
|
||||
.map(item => {
|
||||
if (!item.result) {
|
||||
return;
|
||||
|
|
|
@ -20,7 +20,7 @@ export const useSetLocation = () => {
|
|||
client.setQueryData(ossKey, { ...ossData, location: variables.location });
|
||||
return Promise.allSettled([
|
||||
client.invalidateQueries({ queryKey: libraryApi.libraryListKey }),
|
||||
...ossData.items
|
||||
...ossData.operations
|
||||
.map(item => {
|
||||
if (!item.result) {
|
||||
return;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user