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