Compare commits
9 Commits
6b397003b3
...
31195cdd60
Author | SHA1 | Date | |
---|---|---|---|
![]() |
31195cdd60 | ||
![]() |
453118ea66 | ||
![]() |
17d1eac522 | ||
![]() |
16479d6299 | ||
![]() |
ca077cb2f3 | ||
![]() |
9804df4bae | ||
![]() |
dc9609d347 | ||
![]() |
9199518b25 | ||
![]() |
c8c5055f72 |
41
.github/workflows/backend.yml
vendored
41
.github/workflows/backend.yml
vendored
|
@ -6,16 +6,15 @@ defaults:
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- rsconcept/backend/**
|
||||
- .github/workflows/backend.yml
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
branches: ["main"]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
max-parallel: 4
|
||||
|
@ -23,21 +22,21 @@ jobs:
|
|||
python-version: [3.12]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements-dev.txt
|
||||
- name: Lint
|
||||
run: |
|
||||
pylint project apps
|
||||
mypy project apps
|
||||
- name: Run Tests
|
||||
if: '!cancelled()'
|
||||
run: |
|
||||
python manage.py check
|
||||
python manage.py test
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements-dev-lock.txt
|
||||
- name: Lint
|
||||
run: |
|
||||
pylint project apps
|
||||
mypy project apps
|
||||
- name: Run Tests
|
||||
if: "!cancelled()"
|
||||
run: |
|
||||
python manage.py check
|
||||
python manage.py test
|
||||
|
|
|
@ -143,11 +143,10 @@ This readme file is used mostly to document project dependencies and conventions
|
|||
## 📝 Commit conventions
|
||||
|
||||
- 🚀 F: major feature implementation
|
||||
- 💄 D: UI design
|
||||
- 🔥 B: bug fix
|
||||
- 🚑 M: Minor fixes
|
||||
- 🔧 R: refactoring and code improvement
|
||||
- 📝 I: documentation
|
||||
- 📝 D: documentation
|
||||
|
||||
## 🖥️ Local build (Windows 10+)
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
''' Admin view: Library. '''
|
||||
from typing import cast
|
||||
from django.contrib import admin
|
||||
|
||||
from . import models
|
||||
|
@ -23,7 +24,7 @@ class LibraryTemplateAdmin(admin.ModelAdmin):
|
|||
|
||||
def alias(self, template: models.LibraryTemplate):
|
||||
if template.lib_source:
|
||||
return template.lib_source.alias
|
||||
return cast(models.LibraryItem, template.lib_source).alias
|
||||
else:
|
||||
return 'N/A'
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 5.1.1 on 2024-09-12 16:48
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0004_delete_subscription'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='libraryitem',
|
||||
name='owner',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Владелец'),
|
||||
),
|
||||
]
|
|
@ -9,17 +9,17 @@ from apps.users.models import User
|
|||
|
||||
class Editor(Model):
|
||||
''' Editor list. '''
|
||||
item: ForeignKey = ForeignKey(
|
||||
item = ForeignKey(
|
||||
verbose_name='Схема',
|
||||
to='library.LibraryItem',
|
||||
on_delete=CASCADE
|
||||
)
|
||||
editor: ForeignKey = ForeignKey(
|
||||
editor = ForeignKey(
|
||||
verbose_name='Редактор',
|
||||
to=User,
|
||||
on_delete=CASCADE
|
||||
)
|
||||
time_create: DateTimeField = DateTimeField(
|
||||
time_create = DateTimeField(
|
||||
verbose_name='Дата добавления',
|
||||
auto_now_add=True
|
||||
)
|
||||
|
|
|
@ -48,55 +48,56 @@ def validate_location(target: str) -> bool:
|
|||
|
||||
class LibraryItem(Model):
|
||||
''' Abstract library item.'''
|
||||
item_type: CharField = CharField(
|
||||
item_type = CharField(
|
||||
verbose_name='Тип',
|
||||
max_length=50,
|
||||
choices=LibraryItemType.choices,
|
||||
default=LibraryItemType.RSFORM
|
||||
)
|
||||
owner: ForeignKey = ForeignKey(
|
||||
owner = ForeignKey(
|
||||
verbose_name='Владелец',
|
||||
to=User,
|
||||
on_delete=SET_NULL,
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
title: TextField = TextField(
|
||||
title = TextField(
|
||||
verbose_name='Название'
|
||||
)
|
||||
alias: CharField = CharField(
|
||||
alias = CharField(
|
||||
verbose_name='Шифр',
|
||||
max_length=255,
|
||||
blank=True
|
||||
)
|
||||
comment: TextField = TextField(
|
||||
comment = TextField(
|
||||
verbose_name='Комментарий',
|
||||
blank=True
|
||||
)
|
||||
visible: BooleanField = BooleanField(
|
||||
visible = BooleanField(
|
||||
verbose_name='Отображаемая',
|
||||
default=True
|
||||
)
|
||||
read_only: BooleanField = BooleanField(
|
||||
read_only = BooleanField(
|
||||
verbose_name='Запретить редактирование',
|
||||
default=False
|
||||
)
|
||||
access_policy: CharField = CharField(
|
||||
access_policy = CharField(
|
||||
verbose_name='Политика доступа',
|
||||
max_length=500,
|
||||
choices=AccessPolicy.choices,
|
||||
default=AccessPolicy.PUBLIC
|
||||
)
|
||||
location: TextField = TextField(
|
||||
location = TextField(
|
||||
verbose_name='Расположение',
|
||||
max_length=500,
|
||||
default=LocationHead.USER
|
||||
)
|
||||
|
||||
time_create: DateTimeField = DateTimeField(
|
||||
time_create = DateTimeField(
|
||||
verbose_name='Дата создания',
|
||||
auto_now_add=True
|
||||
)
|
||||
time_update: DateTimeField = DateTimeField(
|
||||
time_update = DateTimeField(
|
||||
verbose_name='Дата изменения',
|
||||
auto_now=True
|
||||
)
|
||||
|
@ -112,11 +113,11 @@ class LibraryItem(Model):
|
|||
def get_absolute_url(self):
|
||||
return f'/api/library/{self.pk}'
|
||||
|
||||
def editors(self) -> QuerySet[User]:
|
||||
def getQ_editors(self) -> QuerySet[User]:
|
||||
''' Get all Editors of this item. '''
|
||||
return User.objects.filter(editor__item=self.pk)
|
||||
|
||||
def versions(self) -> QuerySet[Version]:
|
||||
def getQ_versions(self) -> QuerySet[Version]:
|
||||
''' Get all Versions of this item. '''
|
||||
return Version.objects.filter(item=self.pk).order_by('-time_create')
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ from django.db.models import CASCADE, ForeignKey, Model
|
|||
|
||||
class LibraryTemplate(Model):
|
||||
''' Template for library items and constituents. '''
|
||||
lib_source: ForeignKey = ForeignKey(
|
||||
lib_source = ForeignKey(
|
||||
verbose_name='Источник',
|
||||
to='library.LibraryItem',
|
||||
on_delete=CASCADE
|
||||
|
|
|
@ -12,7 +12,7 @@ from django.db.models import (
|
|||
|
||||
class Version(Model):
|
||||
''' Library item version archive. '''
|
||||
item: ForeignKey = ForeignKey(
|
||||
item = ForeignKey(
|
||||
verbose_name='Схема',
|
||||
to='library.LibraryItem',
|
||||
on_delete=CASCADE
|
||||
|
@ -22,14 +22,14 @@ class Version(Model):
|
|||
max_length=20,
|
||||
blank=False
|
||||
)
|
||||
description: TextField = TextField(
|
||||
description = TextField(
|
||||
verbose_name='Описание',
|
||||
blank=True
|
||||
)
|
||||
data: JSONField = JSONField(
|
||||
data = JSONField(
|
||||
verbose_name='Содержание'
|
||||
)
|
||||
time_create: DateTimeField = DateTimeField(
|
||||
time_create = DateTimeField(
|
||||
verbose_name='Дата создания',
|
||||
auto_now_add=True
|
||||
)
|
||||
|
|
|
@ -84,10 +84,10 @@ class LibraryItemDetailsSerializer(serializers.ModelSerializer):
|
|||
read_only_fields = ('owner', 'id', 'item_type')
|
||||
|
||||
def get_editors(self, instance: LibraryItem) -> list[int]:
|
||||
return list(instance.editors().order_by('pk').values_list('pk', flat=True))
|
||||
return list(instance.getQ_editors().order_by('pk').values_list('pk', flat=True))
|
||||
|
||||
def get_versions(self, instance: LibraryItem) -> list:
|
||||
return [VersionInnerSerializer(item).data for item in instance.versions().order_by('pk')]
|
||||
return [VersionInnerSerializer(item).data for item in instance.getQ_versions().order_by('pk')]
|
||||
|
||||
|
||||
class UserTargetSerializer(serializers.Serializer):
|
||||
|
|
|
@ -35,64 +35,64 @@ class TestEditor(TestCase):
|
|||
|
||||
def test_add_editor(self):
|
||||
self.assertTrue(Editor.add(self.item.pk, self.user1.pk))
|
||||
self.assertEqual(self.item.editors().count(), 1)
|
||||
self.assertTrue(self.user1 in list(self.item.editors()))
|
||||
self.assertEqual(self.item.getQ_editors().count(), 1)
|
||||
self.assertTrue(self.user1 in list(self.item.getQ_editors()))
|
||||
|
||||
self.assertFalse(Editor.add(self.item.pk, self.user1.pk))
|
||||
self.assertEqual(self.item.editors().count(), 1)
|
||||
self.assertEqual(self.item.getQ_editors().count(), 1)
|
||||
|
||||
self.assertTrue(Editor.add(self.item.pk, self.user2.pk))
|
||||
self.assertEqual(self.item.editors().count(), 2)
|
||||
self.assertTrue(self.user1 in self.item.editors())
|
||||
self.assertTrue(self.user2 in self.item.editors())
|
||||
self.assertEqual(self.item.getQ_editors().count(), 2)
|
||||
self.assertTrue(self.user1 in self.item.getQ_editors())
|
||||
self.assertTrue(self.user2 in self.item.getQ_editors())
|
||||
|
||||
self.user1.delete()
|
||||
self.assertEqual(self.item.editors().count(), 1)
|
||||
self.assertEqual(self.item.getQ_editors().count(), 1)
|
||||
|
||||
|
||||
def test_remove_editor(self):
|
||||
self.assertFalse(Editor.remove(self.item.pk, self.user1.pk))
|
||||
Editor.add(self.item.pk, self.user1.pk)
|
||||
Editor.add(self.item.pk, self.user2.pk)
|
||||
self.assertEqual(self.item.editors().count(), 2)
|
||||
self.assertEqual(self.item.getQ_editors().count(), 2)
|
||||
|
||||
self.assertTrue(Editor.remove(self.item.pk, self.user1.pk))
|
||||
self.assertEqual(self.item.editors().count(), 1)
|
||||
self.assertTrue(self.user2 in self.item.editors())
|
||||
self.assertEqual(self.item.getQ_editors().count(), 1)
|
||||
self.assertTrue(self.user2 in self.item.getQ_editors())
|
||||
|
||||
self.assertFalse(Editor.remove(self.item.pk, self.user1.pk))
|
||||
|
||||
|
||||
def test_set_editors(self):
|
||||
Editor.set(self.item.pk, [self.user1.pk])
|
||||
self.assertEqual(list(self.item.editors()), [self.user1])
|
||||
self.assertEqual(list(self.item.getQ_editors()), [self.user1])
|
||||
|
||||
Editor.set(self.item.pk, [self.user1.pk, self.user1.pk])
|
||||
self.assertEqual(list(self.item.editors()), [self.user1])
|
||||
self.assertEqual(list(self.item.getQ_editors()), [self.user1])
|
||||
|
||||
Editor.set(self.item.pk, [])
|
||||
self.assertEqual(list(self.item.editors()), [])
|
||||
self.assertEqual(list(self.item.getQ_editors()), [])
|
||||
|
||||
Editor.set(self.item.pk, [self.user1.pk, self.user2.pk])
|
||||
self.assertEqual(set(self.item.editors()), set([self.user1, self.user2]))
|
||||
self.assertEqual(set(self.item.getQ_editors()), set([self.user1, self.user2]))
|
||||
|
||||
def test_set_editors_return_diff(self):
|
||||
added, deleted = Editor.set_and_return_diff(self.item.pk, [self.user1.pk])
|
||||
self.assertEqual(added, [self.user1.pk])
|
||||
self.assertEqual(deleted, [])
|
||||
self.assertEqual(list(self.item.editors()), [self.user1])
|
||||
self.assertEqual(list(self.item.getQ_editors()), [self.user1])
|
||||
|
||||
added, deleted = Editor.set_and_return_diff(self.item.pk, [self.user1.pk, self.user1.pk])
|
||||
self.assertEqual(added, [])
|
||||
self.assertEqual(deleted, [])
|
||||
self.assertEqual(list(self.item.editors()), [self.user1])
|
||||
self.assertEqual(list(self.item.getQ_editors()), [self.user1])
|
||||
|
||||
added, deleted = Editor.set_and_return_diff(self.item.pk, [])
|
||||
self.assertEqual(added, [])
|
||||
self.assertEqual(deleted, [self.user1.pk])
|
||||
self.assertEqual(list(self.item.editors()), [])
|
||||
self.assertEqual(list(self.item.getQ_editors()), [])
|
||||
|
||||
added, deleted = Editor.set_and_return_diff(self.item.pk, [self.user1.pk, self.user2.pk])
|
||||
self.assertEqual(added, [self.user1.pk, self.user2.pk])
|
||||
self.assertEqual(deleted, [])
|
||||
self.assertEqual(set(self.item.editors()), set([self.user1, self.user2]))
|
||||
self.assertEqual(set(self.item.getQ_editors()), set([self.user1, self.user2]))
|
||||
|
|
|
@ -250,22 +250,22 @@ class TestLibraryViewset(EndpointTester):
|
|||
self.executeOK(data=data, item=self.owned.pk)
|
||||
self.owned.refresh_from_db()
|
||||
self.assertEqual(self.owned.time_update, time_update)
|
||||
self.assertEqual(list(self.owned.editors()), [self.user])
|
||||
self.assertEqual(list(self.owned.getQ_editors()), [self.user])
|
||||
|
||||
self.executeOK(data=data)
|
||||
self.assertEqual(list(self.owned.editors()), [self.user])
|
||||
self.assertEqual(list(self.owned.getQ_editors()), [self.user])
|
||||
|
||||
data = {'users': [self.user2.pk]}
|
||||
self.executeOK(data=data)
|
||||
self.assertEqual(list(self.owned.editors()), [self.user2])
|
||||
self.assertEqual(list(self.owned.getQ_editors()), [self.user2])
|
||||
|
||||
data = {'users': []}
|
||||
self.executeOK(data=data)
|
||||
self.assertEqual(list(self.owned.editors()), [])
|
||||
self.assertEqual(list(self.owned.getQ_editors()), [])
|
||||
|
||||
data = {'users': [self.user2.pk, self.user.pk]}
|
||||
self.executeOK(data=data)
|
||||
self.assertEqual(set(self.owned.editors()), set([self.user2, self.user]))
|
||||
self.assertEqual(set(self.owned.getQ_editors()), set([self.user2, self.user]))
|
||||
|
||||
|
||||
@decl_endpoint('/api/library/{item}', method='delete')
|
||||
|
|
|
@ -157,7 +157,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
|
||||
clone = deepcopy(item)
|
||||
clone.pk = None
|
||||
clone.owner = self.request.user
|
||||
clone.owner = cast(User, self.request.user)
|
||||
clone.title = serializer.validated_data['title']
|
||||
clone.alias = serializer.validated_data.get('alias', '')
|
||||
clone.comment = serializer.validated_data.get('comment', '')
|
||||
|
|
|
@ -44,7 +44,7 @@ class VersionViewset(
|
|||
def restore(self, request: Request, pk) -> HttpResponse:
|
||||
''' Restore version data into current item. '''
|
||||
version = cast(m.Version, self.get_object())
|
||||
item = cast(m.LibraryItem, version.item)
|
||||
item = version.item
|
||||
with transaction.atomic():
|
||||
RSFormSerializer(item).restore_from_version(version.data)
|
||||
return Response(
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 5.1.1 on 2024-09-12 16:48
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0005_alter_libraryitem_owner'),
|
||||
('oss', '0007_argument_order'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='operation',
|
||||
name='result',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='producer', to='library.libraryitem', verbose_name='Связанная КС'),
|
||||
),
|
||||
]
|
|
@ -4,19 +4,19 @@ from django.db.models import CASCADE, ForeignKey, Model, PositiveIntegerField
|
|||
|
||||
class Argument(Model):
|
||||
''' Operation Argument.'''
|
||||
operation: ForeignKey = ForeignKey(
|
||||
operation = ForeignKey(
|
||||
verbose_name='Операция',
|
||||
to='oss.Operation',
|
||||
on_delete=CASCADE,
|
||||
related_name='arguments'
|
||||
)
|
||||
argument: ForeignKey = ForeignKey(
|
||||
argument = ForeignKey(
|
||||
verbose_name='Аргумент',
|
||||
to='oss.Operation',
|
||||
on_delete=CASCADE,
|
||||
related_name='descendants'
|
||||
)
|
||||
order: PositiveIntegerField = PositiveIntegerField(
|
||||
order = PositiveIntegerField(
|
||||
verbose_name='Позиция',
|
||||
default=0,
|
||||
)
|
||||
|
|
|
@ -4,19 +4,19 @@ from django.db.models import CASCADE, ForeignKey, Model
|
|||
|
||||
class Inheritance(Model):
|
||||
''' Inheritance links parent and child constituents in synthesis operation.'''
|
||||
operation: ForeignKey = ForeignKey(
|
||||
operation = ForeignKey(
|
||||
verbose_name='Операция',
|
||||
to='oss.Operation',
|
||||
on_delete=CASCADE,
|
||||
related_name='inheritances'
|
||||
)
|
||||
parent: ForeignKey = ForeignKey(
|
||||
parent = ForeignKey(
|
||||
verbose_name='Исходная конституента',
|
||||
to='rsform.Constituenta',
|
||||
on_delete=CASCADE,
|
||||
related_name='as_parent'
|
||||
)
|
||||
child: ForeignKey = ForeignKey(
|
||||
child = ForeignKey(
|
||||
verbose_name='Наследованная конституента',
|
||||
to='rsform.Constituenta',
|
||||
on_delete=CASCADE,
|
||||
|
|
|
@ -23,45 +23,46 @@ class OperationType(TextChoices):
|
|||
|
||||
class Operation(Model):
|
||||
''' Operational schema Unit.'''
|
||||
oss: ForeignKey = ForeignKey(
|
||||
oss = ForeignKey(
|
||||
verbose_name='Схема синтеза',
|
||||
to='library.LibraryItem',
|
||||
on_delete=CASCADE,
|
||||
related_name='operations'
|
||||
)
|
||||
operation_type: CharField = CharField(
|
||||
operation_type = CharField(
|
||||
verbose_name='Тип',
|
||||
max_length=10,
|
||||
choices=OperationType.choices,
|
||||
default=OperationType.INPUT
|
||||
)
|
||||
result: ForeignKey = ForeignKey(
|
||||
result = ForeignKey(
|
||||
verbose_name='Связанная КС',
|
||||
to='library.LibraryItem',
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=SET_NULL,
|
||||
related_name='producer'
|
||||
)
|
||||
|
||||
alias: CharField = CharField(
|
||||
alias = CharField(
|
||||
verbose_name='Шифр',
|
||||
max_length=255,
|
||||
blank=True
|
||||
)
|
||||
title: TextField = TextField(
|
||||
title = TextField(
|
||||
verbose_name='Название',
|
||||
blank=True
|
||||
)
|
||||
comment: TextField = TextField(
|
||||
comment = TextField(
|
||||
verbose_name='Комментарий',
|
||||
blank=True
|
||||
)
|
||||
|
||||
position_x: FloatField = FloatField(
|
||||
position_x = FloatField(
|
||||
verbose_name='Положение по горизонтали',
|
||||
default=0
|
||||
)
|
||||
position_y: FloatField = FloatField(
|
||||
position_y = FloatField(
|
||||
verbose_name='Положение по вертикали',
|
||||
default=0
|
||||
)
|
||||
|
@ -74,10 +75,10 @@ class Operation(Model):
|
|||
def __str__(self) -> str:
|
||||
return f'Операция {self.alias}'
|
||||
|
||||
def getArguments(self) -> QuerySet[Argument]:
|
||||
def getQ_arguments(self) -> QuerySet[Argument]:
|
||||
''' Operation arguments. '''
|
||||
return Argument.objects.filter(operation=self)
|
||||
|
||||
def getSubstitutions(self) -> QuerySet[Substitution]:
|
||||
def getQ_substitutions(self) -> QuerySet[Substitution]:
|
||||
''' Operation substitutions. '''
|
||||
return Substitution.objects.filter(operation=self)
|
||||
|
|
|
@ -121,7 +121,6 @@ class OperationSchema:
|
|||
|
||||
operation.result = schema
|
||||
if schema is not None:
|
||||
operation.result = schema
|
||||
operation.alias = schema.alias
|
||||
operation.title = schema.title
|
||||
operation.comment = schema.comment
|
||||
|
@ -139,7 +138,7 @@ class OperationSchema:
|
|||
processed: list[Operation] = []
|
||||
updated: list[Argument] = []
|
||||
deleted: list[Argument] = []
|
||||
for current in operation.getArguments():
|
||||
for current in operation.getQ_arguments():
|
||||
if current.argument not in arguments:
|
||||
deleted.append(current)
|
||||
else:
|
||||
|
@ -172,7 +171,7 @@ class OperationSchema:
|
|||
schema = self.cache.get_schema(operation)
|
||||
processed: list[dict] = []
|
||||
deleted: list[Substitution] = []
|
||||
for current in operation.getSubstitutions():
|
||||
for current in operation.getQ_substitutions():
|
||||
subs = [
|
||||
x for x in substitutes
|
||||
if x['original'] == current.original and x['substitution'] == current.substitution
|
||||
|
@ -215,7 +214,7 @@ class OperationSchema:
|
|||
access_policy=self.model.access_policy,
|
||||
location=self.model.location
|
||||
)
|
||||
Editor.set(schema.model.pk, self.model.editors().values_list('pk', flat=True))
|
||||
Editor.set(schema.model.pk, self.model.getQ_editors().values_list('pk', flat=True))
|
||||
operation.result = schema.model
|
||||
operation.save()
|
||||
self.save(update_fields=['time_update'])
|
||||
|
@ -223,10 +222,14 @@ class OperationSchema:
|
|||
|
||||
def execute_operation(self, operation: Operation) -> bool:
|
||||
''' Execute target operation. '''
|
||||
schemas: list[LibraryItem] = [arg.argument.result for arg in operation.getArguments().order_by('order')]
|
||||
if None in schemas:
|
||||
schemas = [
|
||||
arg.argument.result
|
||||
for arg in operation.getQ_arguments().order_by('order')
|
||||
if arg.argument.result is not None
|
||||
]
|
||||
if len(schemas) == 0:
|
||||
return False
|
||||
substitutions = operation.getSubstitutions()
|
||||
substitutions = operation.getQ_substitutions()
|
||||
receiver = self.create_input(self.cache.operation_by_id[operation.pk])
|
||||
|
||||
parents: dict = {}
|
||||
|
@ -284,7 +287,7 @@ class OperationSchema:
|
|||
''' Trigger cascade resolutions when constituenta type is changed. '''
|
||||
self.cache.insert_schema(source)
|
||||
operation = self.cache.get_operation(source.model.pk)
|
||||
self._cascade_change_cst_type(operation.pk, target.pk, target.cst_type)
|
||||
self._cascade_change_cst_type(operation.pk, target.pk, cast(CstType, target.cst_type))
|
||||
|
||||
def after_update_cst(self, source: RSForm, target: Constituenta, data: dict, old_data: dict) -> None:
|
||||
''' Trigger cascade resolutions when constituenta data is changed. '''
|
||||
|
@ -659,7 +662,7 @@ class OperationSchema:
|
|||
substitution_id = self.cache.get_inheritor(substitution_cst.pk, operation_id)
|
||||
assert substitution_id is not None
|
||||
substitution_inheritor = schema.cache.by_id[substitution_id]
|
||||
mapping = {cast(str, substitution_inheritor.alias): new_original}
|
||||
mapping = {substitution_inheritor.alias: new_original}
|
||||
self._cascade_partial_mapping(mapping, dependant, operation_id, schema)
|
||||
|
||||
def _process_added_substitutions(self, schema: Optional[RSForm], added: list[Substitution]) -> None:
|
||||
|
|
|
@ -4,19 +4,19 @@ from django.db.models import CASCADE, ForeignKey, Model
|
|||
|
||||
class Substitution(Model):
|
||||
''' Substitutions as part of Synthesis operation in OSS.'''
|
||||
operation: ForeignKey = ForeignKey(
|
||||
operation = ForeignKey(
|
||||
verbose_name='Операция',
|
||||
to='oss.Operation',
|
||||
on_delete=CASCADE
|
||||
)
|
||||
|
||||
original: ForeignKey = ForeignKey(
|
||||
original = ForeignKey(
|
||||
verbose_name='Удаляемая конституента',
|
||||
to='rsform.Constituenta',
|
||||
on_delete=CASCADE,
|
||||
related_name='as_original'
|
||||
)
|
||||
substitution: ForeignKey = ForeignKey(
|
||||
substitution = ForeignKey(
|
||||
verbose_name='Замещающая конституента',
|
||||
to='rsform.Constituenta',
|
||||
on_delete=CASCADE,
|
||||
|
|
|
@ -53,6 +53,7 @@ class TestChangeAttributes(EndpointTester):
|
|||
alias='3',
|
||||
operation_type=OperationType.SYNTHESIS
|
||||
)
|
||||
self.owned.set_arguments(self.operation3.pk, [self.operation1, self.operation2])
|
||||
self.owned.execute_operation(self.operation3)
|
||||
self.operation3.refresh_from_db()
|
||||
self.ks3 = RSForm(self.operation3.result)
|
||||
|
@ -116,10 +117,10 @@ class TestChangeAttributes(EndpointTester):
|
|||
self.ks1.refresh_from_db()
|
||||
self.ks2.refresh_from_db()
|
||||
self.ks3.refresh_from_db()
|
||||
self.assertEqual(list(self.owned.model.editors()), [self.user3])
|
||||
self.assertEqual(list(self.ks1.model.editors()), [self.user, self.user2])
|
||||
self.assertEqual(list(self.ks2.model.editors()), [])
|
||||
self.assertEqual(set(self.ks3.model.editors()), set([self.user, self.user3]))
|
||||
self.assertEqual(list(self.owned.model.getQ_editors()), [self.user3])
|
||||
self.assertEqual(list(self.ks1.model.getQ_editors()), [self.user, self.user2])
|
||||
self.assertEqual(list(self.ks2.model.getQ_editors()), [])
|
||||
self.assertEqual(set(self.ks3.model.getQ_editors()), set([self.user, self.user3]))
|
||||
|
||||
@decl_endpoint('/api/library/{item}', method='patch')
|
||||
def test_sync_from_result(self):
|
||||
|
|
|
@ -124,9 +124,9 @@ class TestChangeOperations(EndpointTester):
|
|||
self.ks4D1.refresh_from_db()
|
||||
self.ks4D2.refresh_from_db()
|
||||
self.ks5D4.refresh_from_db()
|
||||
subs1_2 = self.operation4.getSubstitutions()
|
||||
subs1_2 = self.operation4.getQ_substitutions()
|
||||
self.assertEqual(subs1_2.count(), 0)
|
||||
subs3_4 = self.operation5.getSubstitutions()
|
||||
subs3_4 = self.operation5.getQ_substitutions()
|
||||
self.assertEqual(subs3_4.count(), 1)
|
||||
self.assertEqual(self.ks4.constituents().count(), 4)
|
||||
self.assertEqual(self.ks5.constituents().count(), 6)
|
||||
|
@ -147,9 +147,9 @@ class TestChangeOperations(EndpointTester):
|
|||
self.ks5D4.refresh_from_db()
|
||||
self.operation2.refresh_from_db()
|
||||
self.assertEqual(self.operation2.result, None)
|
||||
subs1_2 = self.operation4.getSubstitutions()
|
||||
subs1_2 = self.operation4.getQ_substitutions()
|
||||
self.assertEqual(subs1_2.count(), 0)
|
||||
subs3_4 = self.operation5.getSubstitutions()
|
||||
subs3_4 = self.operation5.getQ_substitutions()
|
||||
self.assertEqual(subs3_4.count(), 1)
|
||||
self.assertEqual(self.ks4.constituents().count(), 4)
|
||||
self.assertEqual(self.ks5.constituents().count(), 6)
|
||||
|
@ -181,9 +181,9 @@ class TestChangeOperations(EndpointTester):
|
|||
self.operation2.refresh_from_db()
|
||||
self.assertEqual(self.operation2.result, ks6.model)
|
||||
self.assertEqual(self.operation2.alias, ks6.model.alias)
|
||||
subs1_2 = self.operation4.getSubstitutions()
|
||||
subs1_2 = self.operation4.getQ_substitutions()
|
||||
self.assertEqual(subs1_2.count(), 0)
|
||||
subs3_4 = self.operation5.getSubstitutions()
|
||||
subs3_4 = self.operation5.getQ_substitutions()
|
||||
self.assertEqual(subs3_4.count(), 1)
|
||||
self.assertEqual(self.ks4.constituents().count(), 7)
|
||||
self.assertEqual(self.ks5.constituents().count(), 9)
|
||||
|
@ -199,9 +199,9 @@ class TestChangeOperations(EndpointTester):
|
|||
self.ks5D4.refresh_from_db()
|
||||
self.operation1.refresh_from_db()
|
||||
self.assertEqual(self.operation1.result, None)
|
||||
subs1_2 = self.operation4.getSubstitutions()
|
||||
subs1_2 = self.operation4.getQ_substitutions()
|
||||
self.assertEqual(subs1_2.count(), 0)
|
||||
subs3_4 = self.operation5.getSubstitutions()
|
||||
subs3_4 = self.operation5.getQ_substitutions()
|
||||
self.assertEqual(subs3_4.count(), 0)
|
||||
self.assertEqual(self.ks4.constituents().count(), 4)
|
||||
self.assertEqual(self.ks5.constituents().count(), 7)
|
||||
|
@ -220,9 +220,9 @@ class TestChangeOperations(EndpointTester):
|
|||
self.executeOK(data=data, item=self.owned_id)
|
||||
self.ks4D2.refresh_from_db()
|
||||
self.ks5D4.refresh_from_db()
|
||||
subs1_2 = self.operation4.getSubstitutions()
|
||||
subs1_2 = self.operation4.getQ_substitutions()
|
||||
self.assertEqual(subs1_2.count(), 0)
|
||||
subs3_4 = self.operation5.getSubstitutions()
|
||||
subs3_4 = self.operation5.getQ_substitutions()
|
||||
self.assertEqual(subs3_4.count(), 0)
|
||||
self.assertEqual(self.ks4.constituents().count(), 4)
|
||||
self.assertEqual(self.ks5.constituents().count(), 7)
|
||||
|
@ -241,9 +241,9 @@ class TestChangeOperations(EndpointTester):
|
|||
self.executeOK(data=data, item=self.owned_id)
|
||||
self.ks4D2.refresh_from_db()
|
||||
self.ks5D4.refresh_from_db()
|
||||
subs1_2 = self.operation4.getSubstitutions()
|
||||
subs1_2 = self.operation4.getQ_substitutions()
|
||||
self.assertEqual(subs1_2.count(), 0)
|
||||
subs3_4 = self.operation5.getSubstitutions()
|
||||
subs3_4 = self.operation5.getQ_substitutions()
|
||||
self.assertEqual(subs3_4.count(), 1)
|
||||
self.assertEqual(self.ks4.constituents().count(), 6)
|
||||
self.assertEqual(self.ks5.constituents().count(), 8)
|
||||
|
@ -275,9 +275,9 @@ class TestChangeOperations(EndpointTester):
|
|||
self.executeOK(data=data, item=self.owned_id)
|
||||
self.ks4D2.refresh_from_db()
|
||||
self.ks5D4.refresh_from_db()
|
||||
subs1_2 = self.operation4.getSubstitutions()
|
||||
subs1_2 = self.operation4.getQ_substitutions()
|
||||
self.assertEqual(subs1_2.count(), 2)
|
||||
subs3_4 = self.operation5.getSubstitutions()
|
||||
subs3_4 = self.operation5.getQ_substitutions()
|
||||
self.assertEqual(subs3_4.count(), 1)
|
||||
self.assertEqual(self.ks4.constituents().count(), 5)
|
||||
self.assertEqual(self.ks5.constituents().count(), 7)
|
||||
|
@ -300,9 +300,9 @@ class TestChangeOperations(EndpointTester):
|
|||
self.executeOK(data=data, item=self.owned_id)
|
||||
self.ks4D2.refresh_from_db()
|
||||
self.ks5D4.refresh_from_db()
|
||||
subs1_2 = self.operation4.getSubstitutions()
|
||||
subs1_2 = self.operation4.getQ_substitutions()
|
||||
self.assertEqual(subs1_2.count(), 0)
|
||||
subs3_4 = self.operation5.getSubstitutions()
|
||||
subs3_4 = self.operation5.getQ_substitutions()
|
||||
self.assertEqual(subs3_4.count(), 1)
|
||||
self.assertEqual(self.ks4.constituents().count(), 4)
|
||||
self.assertEqual(self.ks5.constituents().count(), 6)
|
||||
|
@ -313,9 +313,9 @@ class TestChangeOperations(EndpointTester):
|
|||
self.executeOK(data=data, item=self.owned_id)
|
||||
self.ks4D2.refresh_from_db()
|
||||
self.ks5D4.refresh_from_db()
|
||||
subs1_2 = self.operation4.getSubstitutions()
|
||||
subs1_2 = self.operation4.getQ_substitutions()
|
||||
self.assertEqual(subs1_2.count(), 0)
|
||||
subs3_4 = self.operation5.getSubstitutions()
|
||||
subs3_4 = self.operation5.getQ_substitutions()
|
||||
self.assertEqual(subs3_4.count(), 1)
|
||||
self.assertEqual(self.ks4.constituents().count(), 7)
|
||||
self.assertEqual(self.ks5.constituents().count(), 9)
|
||||
|
|
|
@ -125,11 +125,11 @@ class TestChangeSubstitutions(EndpointTester):
|
|||
self.ks4D1.refresh_from_db()
|
||||
self.ks4D2.refresh_from_db()
|
||||
self.ks5D4.refresh_from_db()
|
||||
subs1_2 = self.operation4.getSubstitutions()
|
||||
subs1_2 = self.operation4.getQ_substitutions()
|
||||
self.assertEqual(subs1_2.count(), 1)
|
||||
self.assertEqual(subs1_2.first().original, self.ks1X2)
|
||||
self.assertEqual(subs1_2.first().substitution, self.ks2S1)
|
||||
subs3_4 = self.operation5.getSubstitutions()
|
||||
subs3_4 = self.operation5.getQ_substitutions()
|
||||
self.assertEqual(subs3_4.count(), 1)
|
||||
self.assertEqual(subs3_4.first().original, self.ks4S1)
|
||||
self.assertEqual(subs3_4.first().substitution, self.ks3X1)
|
||||
|
@ -147,11 +147,11 @@ class TestChangeSubstitutions(EndpointTester):
|
|||
self.ks4D1.refresh_from_db()
|
||||
self.ks4D2.refresh_from_db()
|
||||
self.ks5D4.refresh_from_db()
|
||||
subs1_2 = self.operation4.getSubstitutions()
|
||||
subs1_2 = self.operation4.getQ_substitutions()
|
||||
self.assertEqual(subs1_2.count(), 1)
|
||||
self.assertEqual(subs1_2.first().original, self.ks1X1)
|
||||
self.assertEqual(subs1_2.first().substitution, self.ks2X1)
|
||||
subs3_4 = self.operation5.getSubstitutions()
|
||||
subs3_4 = self.operation5.getQ_substitutions()
|
||||
self.assertEqual(subs3_4.count(), 1)
|
||||
self.assertEqual(subs3_4.first().original, self.ks4X1)
|
||||
self.assertEqual(subs3_4.first().substitution, self.ks3X1)
|
||||
|
@ -165,9 +165,9 @@ class TestChangeSubstitutions(EndpointTester):
|
|||
self.executeOK(data=data, schema=self.ks1.model.pk)
|
||||
self.ks4D2.refresh_from_db()
|
||||
self.ks5D4.refresh_from_db()
|
||||
subs1_2 = self.operation4.getSubstitutions()
|
||||
subs1_2 = self.operation4.getQ_substitutions()
|
||||
self.assertEqual(subs1_2.count(), 0)
|
||||
subs3_4 = self.operation5.getSubstitutions()
|
||||
subs3_4 = self.operation5.getQ_substitutions()
|
||||
self.assertEqual(subs3_4.count(), 1)
|
||||
self.assertEqual(self.ks5.constituents().count(), 7)
|
||||
self.assertEqual(self.ks4D2.definition_formal, r'X1 X2 X3 S1 DEL')
|
||||
|
@ -180,9 +180,9 @@ class TestChangeSubstitutions(EndpointTester):
|
|||
self.ks4D1.refresh_from_db()
|
||||
self.ks4D2.refresh_from_db()
|
||||
self.ks5D4.refresh_from_db()
|
||||
subs1_2 = self.operation4.getSubstitutions()
|
||||
subs1_2 = self.operation4.getQ_substitutions()
|
||||
self.assertEqual(subs1_2.count(), 0)
|
||||
subs3_4 = self.operation5.getSubstitutions()
|
||||
subs3_4 = self.operation5.getQ_substitutions()
|
||||
self.assertEqual(subs3_4.count(), 1)
|
||||
self.assertEqual(self.ks5.constituents().count(), 7)
|
||||
self.assertEqual(self.ks4D1.definition_formal, r'X4 X1')
|
||||
|
|
|
@ -244,7 +244,7 @@ class TestOssViewset(EndpointTester):
|
|||
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.editors())
|
||||
self.assertIn(self.user2, schema.getQ_editors())
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
|
||||
def test_delete_operation(self):
|
||||
|
@ -409,12 +409,12 @@ class TestOssViewset(EndpointTester):
|
|||
self.assertEqual(self.operation3.alias, data['item_data']['alias'])
|
||||
self.assertEqual(self.operation3.title, data['item_data']['title'])
|
||||
self.assertEqual(self.operation3.comment, data['item_data']['comment'])
|
||||
args = self.operation3.getArguments().order_by('order')
|
||||
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.getSubstitutions()[0]
|
||||
sub = self.operation3.getQ_substitutions()[0]
|
||||
self.assertEqual(sub.original.pk, data['substitutions'][0]['original'])
|
||||
self.assertEqual(sub.substitution.pk, data['substitutions'][0]['substitution'])
|
||||
|
||||
|
|
|
@ -162,7 +162,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
|
||||
oss = m.OperationSchema(self.get_object())
|
||||
operation = cast(m.Operation, serializer.validated_data['target'])
|
||||
old_schema: Optional[LibraryItem] = operation.result
|
||||
old_schema = operation.result
|
||||
with transaction.atomic():
|
||||
oss.update_positions(serializer.validated_data['positions'])
|
||||
oss.delete_operation(operation.pk, serializer.validated_data['keep_constituents'])
|
||||
|
@ -255,7 +255,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
'input': msg.operationInputAlreadyConnected()
|
||||
})
|
||||
oss = m.OperationSchema(self.get_object())
|
||||
old_schema: Optional[LibraryItem] = target_operation.result
|
||||
old_schema = target_operation.result
|
||||
with transaction.atomic():
|
||||
if old_schema is not None:
|
||||
if old_schema.is_synced(oss.model):
|
||||
|
@ -370,10 +370,13 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
serializer = CstTargetSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
cst = cast(Constituenta, serializer.validated_data['target'])
|
||||
inheritance = m.Inheritance.objects.filter(child=cst)
|
||||
while inheritance.exists():
|
||||
cst = cast(m.Inheritance, inheritance.first()).parent
|
||||
inheritance = m.Inheritance.objects.filter(child=cst)
|
||||
inheritance_query = m.Inheritance.objects.filter(child=cst)
|
||||
while inheritance_query.exists():
|
||||
inheritance = inheritance_query.first()
|
||||
if inheritance is None:
|
||||
break
|
||||
cst = inheritance.parent
|
||||
inheritance_query = m.Inheritance.objects.filter(child=cst)
|
||||
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
|
|
|
@ -49,56 +49,56 @@ class CstType(TextChoices):
|
|||
|
||||
class Constituenta(Model):
|
||||
''' Constituenta is the base unit for every conceptual schema. '''
|
||||
schema: ForeignKey = ForeignKey(
|
||||
schema = ForeignKey(
|
||||
verbose_name='Концептуальная схема',
|
||||
to='library.LibraryItem',
|
||||
on_delete=CASCADE
|
||||
)
|
||||
order: PositiveIntegerField = PositiveIntegerField(
|
||||
order = PositiveIntegerField(
|
||||
verbose_name='Позиция',
|
||||
default=0,
|
||||
)
|
||||
alias: CharField = CharField(
|
||||
alias = CharField(
|
||||
verbose_name='Имя',
|
||||
max_length=8,
|
||||
default='undefined'
|
||||
)
|
||||
cst_type: CharField = CharField(
|
||||
cst_type = CharField(
|
||||
verbose_name='Тип',
|
||||
max_length=10,
|
||||
choices=CstType.choices,
|
||||
default=CstType.BASE
|
||||
)
|
||||
convention: TextField = TextField(
|
||||
convention = TextField(
|
||||
verbose_name='Комментарий/Конвенция',
|
||||
default='',
|
||||
blank=True
|
||||
)
|
||||
term_raw: TextField = TextField(
|
||||
term_raw = TextField(
|
||||
verbose_name='Термин (с отсылками)',
|
||||
default='',
|
||||
blank=True
|
||||
)
|
||||
term_resolved: TextField = TextField(
|
||||
term_resolved = TextField(
|
||||
verbose_name='Термин',
|
||||
default='',
|
||||
blank=True
|
||||
)
|
||||
term_forms: JSONField = JSONField(
|
||||
term_forms = JSONField(
|
||||
verbose_name='Словоформы',
|
||||
default=list
|
||||
)
|
||||
definition_formal: TextField = TextField(
|
||||
definition_formal = TextField(
|
||||
verbose_name='Родоструктурное определение',
|
||||
default='',
|
||||
blank=True
|
||||
)
|
||||
definition_raw: TextField = TextField(
|
||||
definition_raw = TextField(
|
||||
verbose_name='Текстовое определение (с отсылками)',
|
||||
default='',
|
||||
blank=True
|
||||
)
|
||||
definition_resolved: TextField = TextField(
|
||||
definition_resolved = TextField(
|
||||
verbose_name='Текстовое определение',
|
||||
default='',
|
||||
blank=True
|
||||
|
|
|
@ -121,7 +121,7 @@ class RSForm:
|
|||
update_list.append(cst)
|
||||
Constituenta.objects.bulk_update(update_list, ['definition_resolved'])
|
||||
|
||||
def get_max_index(self, cst_type: CstType) -> int:
|
||||
def get_max_index(self, cst_type: str) -> int:
|
||||
''' Get maximum alias index for specific CstType. '''
|
||||
result: int = 0
|
||||
cst_list: Iterable[Constituenta] = []
|
||||
|
@ -363,6 +363,8 @@ class RSForm:
|
|||
for cst in self.cache.constituents:
|
||||
if cst.apply_mapping(mapping, change_aliases):
|
||||
update_list.append(cst)
|
||||
if change_aliases:
|
||||
self.cache.reset_aliases()
|
||||
Constituenta.objects.bulk_update(update_list, ['alias', 'definition_formal', 'term_raw', 'definition_raw'])
|
||||
self.save(update_fields=['time_update'])
|
||||
|
||||
|
@ -555,6 +557,9 @@ class RSFormCache:
|
|||
if not self.is_loaded:
|
||||
self.reload()
|
||||
|
||||
def reset_aliases(self) -> None:
|
||||
self.by_alias = {cst.alias: cst for cst in self.constituents}
|
||||
|
||||
def clear(self) -> None:
|
||||
self.constituents = []
|
||||
self.by_id = {}
|
||||
|
|
|
@ -26,7 +26,7 @@ class TokenType(IntEnum):
|
|||
REDUCE = 299
|
||||
|
||||
|
||||
def get_type_prefix(cst_type: CstType) -> str:
|
||||
def get_type_prefix(cst_type: str) -> str:
|
||||
''' Get alias prefix. '''
|
||||
match cst_type:
|
||||
case CstType.BASE: return 'X'
|
||||
|
@ -40,7 +40,7 @@ def get_type_prefix(cst_type: CstType) -> str:
|
|||
return 'X'
|
||||
|
||||
|
||||
def is_basic_concept(cst_type: CstType) -> bool:
|
||||
def is_basic_concept(cst_type: str) -> bool:
|
||||
''' Evaluate if CstType is basic concept.'''
|
||||
return cst_type in [
|
||||
CstType.BASE,
|
||||
|
@ -50,7 +50,7 @@ def is_basic_concept(cst_type: CstType) -> bool:
|
|||
]
|
||||
|
||||
|
||||
def is_base_set(cst_type: CstType) -> bool:
|
||||
def is_base_set(cst_type: str) -> bool:
|
||||
''' Evaluate if CstType is base set or constant set.'''
|
||||
return cst_type in [
|
||||
CstType.BASE,
|
||||
|
@ -58,7 +58,7 @@ def is_base_set(cst_type: CstType) -> bool:
|
|||
]
|
||||
|
||||
|
||||
def is_functional(cst_type: CstType) -> bool:
|
||||
def is_functional(cst_type: str) -> bool:
|
||||
''' Evaluate if CstType is function.'''
|
||||
return cst_type in [
|
||||
CstType.FUNCTION,
|
||||
|
@ -70,7 +70,7 @@ def guess_type(alias: str) -> CstType:
|
|||
''' Get CstType for alias. '''
|
||||
prefix = alias[0]
|
||||
for (value, _) in CstType.choices:
|
||||
if prefix == get_type_prefix(cast(CstType, value)):
|
||||
if prefix == get_type_prefix(value):
|
||||
return cast(CstType, value)
|
||||
return CstType.BASE
|
||||
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
warn_return_any = True
|
||||
warn_unused_configs = True
|
||||
|
||||
plugins = mypy_django_plugin.main
|
||||
plugins =
|
||||
mypy_django_plugin.main
|
||||
|
||||
# Per-module options:
|
||||
[mypy.plugins.django-stubs]
|
||||
|
|
20
rsconcept/backend/requirements-dev-lock.txt
Normal file
20
rsconcept/backend/requirements-dev-lock.txt
Normal file
|
@ -0,0 +1,20 @@
|
|||
tzdata==2024.1
|
||||
Django==5.1.1
|
||||
djangorestframework==3.15.2
|
||||
django-cors-headers==4.4.0
|
||||
django-filter==24.3
|
||||
drf-spectacular==0.27.2
|
||||
drf-spectacular-sidecar==2024.7.1
|
||||
coreapi==2.3.3
|
||||
django-rest-passwordreset==1.4.1
|
||||
cctext==0.1.4
|
||||
pyconcept==0.1.6
|
||||
|
||||
psycopg2-binary==2.9.9
|
||||
gunicorn==23.0.0
|
||||
|
||||
djangorestframework-stubs==3.15.1
|
||||
django-extensions==3.2.3
|
||||
mypy==1.11.2
|
||||
pylint==3.2.7
|
||||
coverage==7.6.1
|
|
@ -1,5 +1,5 @@
|
|||
tzdata==2024.1
|
||||
Django==5.1
|
||||
Django==5.1.1
|
||||
djangorestframework==3.15.2
|
||||
django-cors-headers==4.4.0
|
||||
django-filter==24.3
|
||||
|
|
97
rsconcept/frontend/src/components/props.d.ts
vendored
97
rsconcept/frontend/src/components/props.d.ts
vendored
|
@ -2,47 +2,112 @@
|
|||
import { HTMLMotionProps } from 'framer-motion';
|
||||
|
||||
export namespace CProps {
|
||||
export interface Titled {
|
||||
title?: string;
|
||||
titleHtml?: string;
|
||||
hideTitle?: boolean;
|
||||
}
|
||||
|
||||
export type Control = Titled & {
|
||||
disabled?: boolean;
|
||||
noBorder?: boolean;
|
||||
noOutline?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents an object that can have inline styles and CSS class names for styling.
|
||||
*/
|
||||
export interface Styling {
|
||||
/** Optional inline styles for the component. */
|
||||
style?: React.CSSProperties;
|
||||
|
||||
/** Optional CSS class name(s) for the component. */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export type Editor = Control & {
|
||||
label?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents an object that can have a color or set of colors.
|
||||
*/
|
||||
export interface Colors {
|
||||
/** Optional color or set of colors applied via classNames. */
|
||||
colors?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an object that can have a title with optional HTML rendering and a flag to hide the title.
|
||||
*/
|
||||
export interface Titled {
|
||||
/** Tooltip: `plain text`. */
|
||||
title?: string;
|
||||
|
||||
/** Tooltip: `HTML formatted`. */
|
||||
titleHtml?: string;
|
||||
|
||||
/** Indicates whether the `title` should be hidden. */
|
||||
hideTitle?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents `control` component with optional title and configuration options.
|
||||
*
|
||||
* @remarks
|
||||
* This type extends the {@link Titled} interface, adding properties to control the visual and interactive behavior of a component.
|
||||
*/
|
||||
export type Control = Titled & {
|
||||
/** Indicates whether the control is disabled. */
|
||||
disabled?: boolean;
|
||||
|
||||
/** Indicates whether the control should render without a border. */
|
||||
noBorder?: boolean;
|
||||
|
||||
/** Indicates whether the control should render without an outline. */
|
||||
noOutline?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents `editor` component that includes a label, control features, and optional title properties.
|
||||
*
|
||||
* @remarks
|
||||
* This type extends the {@link Control} type, inheriting title-related properties and additional configuration options, while also adding an optional label.
|
||||
*/
|
||||
export type Editor = Control & {
|
||||
/** Text label. */
|
||||
label?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents `div` component with all standard HTML attributes and React-specific properties.
|
||||
*/
|
||||
export type Div = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
|
||||
|
||||
/**
|
||||
* Represents `button` component with optional title and HTML attributes.
|
||||
*/
|
||||
export type Button = Titled &
|
||||
Omit<
|
||||
React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>,
|
||||
'children' | 'type'
|
||||
>;
|
||||
|
||||
/**
|
||||
* Represents `label` component with HTML attributes.
|
||||
*/
|
||||
export type Label = Omit<
|
||||
React.DetailedHTMLProps<React.LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>,
|
||||
'children'
|
||||
>;
|
||||
|
||||
/**
|
||||
* Represents `textarea` component with optional title and HTML attributes.
|
||||
*/
|
||||
export type TextArea = Titled &
|
||||
React.DetailedHTMLProps<React.TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>;
|
||||
|
||||
/**
|
||||
* Represents `input` component with optional title and HTML attributes.
|
||||
*/
|
||||
export type Input = Titled & React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
|
||||
|
||||
/**
|
||||
* Represents `button` component with optional title and animation properties.
|
||||
*/
|
||||
export type AnimatedButton = Titled & Omit<HTMLMotionProps<'button'>, 'type'>;
|
||||
|
||||
/**
|
||||
* Represents `div` component with animation properties.
|
||||
*/
|
||||
export type AnimatedDiv = HTMLMotionProps<'div'>;
|
||||
|
||||
/**
|
||||
* Represents `mouse event` in React.
|
||||
*/
|
||||
export type EventMouse = React.MouseEvent<Element, MouseEvent>;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,21 @@
|
|||
import { useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/ui/DataTable';
|
||||
import SearchBar from '@/components/ui/SearchBar';
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { useLibrary } from '@/context/LibraryContext';
|
||||
import useDropdown from '@/hooks/useDropdown';
|
||||
import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library';
|
||||
import { matchLibraryItem } from '@/models/libraryAPI';
|
||||
import { prefixes } from '@/utils/constants';
|
||||
|
||||
import { IconClose, IconFolderTree } from '../Icons';
|
||||
import { CProps } from '../props';
|
||||
import Dropdown from '../ui/Dropdown';
|
||||
import FlexColumn from '../ui/FlexColumn';
|
||||
import MiniButton from '../ui/MiniButton';
|
||||
import SelectLocation from './SelectLocation';
|
||||
|
||||
interface PickSchemaProps {
|
||||
id?: string;
|
||||
|
@ -35,18 +43,27 @@ function PickSchema({
|
|||
}: PickSchemaProps) {
|
||||
const intl = useIntl();
|
||||
const { colors } = useConceptOptions();
|
||||
const { folders } = useLibrary();
|
||||
|
||||
const [filterText, setFilterText] = useState(initialFilter);
|
||||
const [filterLocation, setFilterLocation] = useState('');
|
||||
const [filtered, setFiltered] = useState<ILibraryItem[]>([]);
|
||||
const baseFiltered = useMemo(
|
||||
() => items.filter(item => item.item_type === itemType && (!baseFilter || baseFilter(item))),
|
||||
[items, itemType, baseFilter]
|
||||
);
|
||||
|
||||
const locationMenu = useDropdown();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const newFiltered = baseFiltered.filter(item => matchLibraryItem(item, filterText));
|
||||
let newFiltered = baseFiltered.filter(item => matchLibraryItem(item, filterText));
|
||||
if (filterLocation.length > 0) {
|
||||
newFiltered = newFiltered.filter(
|
||||
item => item.location === filterLocation || item.location.startsWith(`${filterLocation}/`)
|
||||
);
|
||||
}
|
||||
setFiltered(newFiltered);
|
||||
}, [filterText]);
|
||||
}, [filterText, filterLocation]);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
|
@ -92,15 +109,51 @@ function PickSchema({
|
|||
[value, colors]
|
||||
);
|
||||
|
||||
const handleLocationClick = useCallback(
|
||||
(event: CProps.EventMouse, newValue: string) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
locationMenu.hide();
|
||||
setFilterLocation(newValue);
|
||||
},
|
||||
[locationMenu]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='border divide-y'>
|
||||
<SearchBar
|
||||
id={id ? `${id}__search` : undefined}
|
||||
className='clr-input'
|
||||
noBorder
|
||||
value={filterText}
|
||||
onChange={newValue => setFilterText(newValue)}
|
||||
/>
|
||||
<div className='flex justify-between clr-input items-center pr-1'>
|
||||
<SearchBar
|
||||
id={id ? `${id}__search` : undefined}
|
||||
className='clr-input w-full'
|
||||
noBorder
|
||||
value={filterText}
|
||||
onChange={newValue => setFilterText(newValue)}
|
||||
/>
|
||||
<div ref={locationMenu.ref}>
|
||||
<MiniButton
|
||||
icon={<IconFolderTree size='1.25rem' className={!!filterLocation ? 'icon-green' : 'icon-primary'} />}
|
||||
title='Фильтр по расположению'
|
||||
className='mt-1'
|
||||
onClick={() => locationMenu.toggle()}
|
||||
/>
|
||||
<Dropdown isOpen={locationMenu.isOpen} stretchLeft className='w-[20rem] h-[12.5rem] z-modalTooltip mt-0'>
|
||||
<SelectLocation
|
||||
folderTree={folders}
|
||||
value={filterLocation}
|
||||
prefix={prefixes.folders_list}
|
||||
dense
|
||||
onClick={(event, target) => handleLocationClick(event, target.getPath())}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
{filterLocation.length > 0 ? (
|
||||
<MiniButton
|
||||
icon={<IconClose size='1.25rem' className='icon-red' />}
|
||||
title='Сбросить фильтр'
|
||||
onClick={() => setFilterLocation('')}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<DataTable
|
||||
id={id}
|
||||
rows={rows}
|
||||
|
|
|
@ -15,13 +15,21 @@ import SelectLocation from './SelectLocation';
|
|||
|
||||
interface SelectLocationContextProps extends CProps.Styling {
|
||||
value: string;
|
||||
title?: string;
|
||||
folderTree: FolderTree;
|
||||
stretchTop?: boolean;
|
||||
|
||||
onChange: (newValue: string) => void;
|
||||
}
|
||||
|
||||
function SelectLocationContext({ value, folderTree, onChange, className, style }: SelectLocationContextProps) {
|
||||
function SelectLocationContext({
|
||||
value,
|
||||
title = 'Проводник...',
|
||||
folderTree,
|
||||
onChange,
|
||||
className,
|
||||
style
|
||||
}: SelectLocationContextProps) {
|
||||
const menu = useDropdown();
|
||||
|
||||
const handleClick = useCallback(
|
||||
|
@ -37,7 +45,7 @@ function SelectLocationContext({ value, folderTree, onChange, className, style }
|
|||
return (
|
||||
<div ref={menu.ref} className='h-full text-right self-start mt-[-0.25rem] ml-[-1.5rem]'>
|
||||
<MiniButton
|
||||
title='Проводник...'
|
||||
title={title}
|
||||
hideTitle={menu.isOpen}
|
||||
icon={<IconFolderTree size='1.25rem' className='icon-green' />}
|
||||
onClick={() => menu.toggle()}
|
||||
|
|
|
@ -5,16 +5,25 @@ import { globals } from '@/utils/constants';
|
|||
import { CProps } from '../props';
|
||||
|
||||
interface ButtonProps extends CProps.Control, CProps.Colors, CProps.Button {
|
||||
text?: string;
|
||||
/** Icon to display first. */
|
||||
icon?: React.ReactNode;
|
||||
|
||||
/** Text to display second. */
|
||||
text?: string;
|
||||
|
||||
/** Indicates whether to render the button in a dense style. */
|
||||
dense?: boolean;
|
||||
|
||||
/** Indicates loading state to prevent interactions and change visual style. */
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Button component that provides a customizable `button` with text, icon, tooltips and various styles.
|
||||
*/
|
||||
function Button({
|
||||
text,
|
||||
icon,
|
||||
text,
|
||||
title,
|
||||
titleHtml,
|
||||
hideTitle,
|
||||
|
|
|
@ -7,13 +7,22 @@ import { CheckboxChecked } from '../Icons';
|
|||
import { CProps } from '../props';
|
||||
|
||||
export interface CheckboxProps extends Omit<CProps.Button, 'value' | 'onClick'> {
|
||||
/** Label to display next to the checkbox. */
|
||||
label?: string;
|
||||
|
||||
/** Indicates whether the checkbox is disabled. */
|
||||
disabled?: boolean;
|
||||
|
||||
/** Current value - `true` or `false`. */
|
||||
value: boolean;
|
||||
|
||||
/** Callback to set the `value`. */
|
||||
setValue?: (newValue: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkbox component that allows users to toggle a boolean value.
|
||||
*/
|
||||
function Checkbox({
|
||||
disabled,
|
||||
label,
|
||||
|
|
|
@ -8,10 +8,16 @@ import { CProps } from '../props';
|
|||
import { CheckboxProps } from './Checkbox';
|
||||
|
||||
export interface CheckboxTristateProps extends Omit<CheckboxProps, 'value' | 'setValue'> {
|
||||
/** Current value - `null`, `true` or `false`. */
|
||||
value: boolean | null;
|
||||
|
||||
/** Callback to set the `value`. */
|
||||
setValue?: (newValue: boolean | null) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* CheckboxTristate component that allows toggling among three states: `true`, `false`, and `null`.
|
||||
*/
|
||||
function CheckboxTristate({
|
||||
disabled,
|
||||
label,
|
||||
|
|
|
@ -2,11 +2,17 @@ import clsx from 'clsx';
|
|||
|
||||
import { CProps } from '@/components/props';
|
||||
|
||||
interface DividerProps extends CProps.Styling {
|
||||
export interface DividerProps extends CProps.Styling {
|
||||
/** Indicates whether the divider is vertical. */
|
||||
vertical?: boolean;
|
||||
|
||||
/** Margins to apply to the divider `classNames`. */
|
||||
margins?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Divider component that renders a horizontal or vertical divider with customizable margins and styling.
|
||||
*/
|
||||
function Divider({ vertical, margins = 'mx-2', className, ...restProps }: DividerProps) {
|
||||
return (
|
||||
<div
|
||||
|
|
|
@ -6,12 +6,22 @@ import { animateDropdown } from '@/styling/animations';
|
|||
import { CProps } from '../props';
|
||||
|
||||
interface DropdownProps extends CProps.Styling {
|
||||
/** Indicates whether the dropdown should stretch to the left. */
|
||||
stretchLeft?: boolean;
|
||||
|
||||
/** Indicates whether the dropdown should stretch to the top. */
|
||||
stretchTop?: boolean;
|
||||
|
||||
/** Indicates whether the dropdown is open. */
|
||||
isOpen: boolean;
|
||||
|
||||
/** Children to render inside the component. */
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dropdown animated component that displays a list of children with optional positioning and visibility control.
|
||||
*/
|
||||
function Dropdown({ isOpen, stretchLeft, stretchTop, className, children, ...restProps }: DropdownProps) {
|
||||
return (
|
||||
<div className='relative'>
|
||||
|
|
|
@ -7,15 +7,23 @@ import { globals } from '@/utils/constants';
|
|||
import { CProps } from '../props';
|
||||
|
||||
interface DropdownButtonProps extends CProps.AnimatedButton {
|
||||
text?: string;
|
||||
/** Icon to display first (not used if children are provided). */
|
||||
icon?: React.ReactNode;
|
||||
|
||||
/** Text to display second (not used if children are provided). */
|
||||
text?: string;
|
||||
|
||||
/** Custom children to display. */
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* DropdownButton animated component that renders a `button` with optional text, icon, and click functionality.
|
||||
* It supports optional children for custom content or the default text/icon display.
|
||||
*/
|
||||
function DropdownButton({
|
||||
text,
|
||||
icon,
|
||||
text,
|
||||
className,
|
||||
title,
|
||||
titleHtml,
|
||||
|
|
|
@ -3,21 +3,13 @@ import { motion } from 'framer-motion';
|
|||
|
||||
import { animateDropdownItem } from '@/styling/animations';
|
||||
|
||||
import Checkbox from './Checkbox';
|
||||
import Checkbox, { CheckboxProps } from './Checkbox';
|
||||
|
||||
interface DropdownCheckboxProps {
|
||||
value: boolean;
|
||||
label?: string;
|
||||
title?: string;
|
||||
disabled?: boolean;
|
||||
setValue?: (newValue: boolean) => void;
|
||||
}
|
||||
|
||||
function DropdownCheckbox({ title, setValue, disabled, ...restProps }: DropdownCheckboxProps) {
|
||||
/** DropdownCheckbox animated component that renders a {@link Checkbox} inside a {@link Dropdown} item. */
|
||||
function DropdownCheckbox({ setValue, disabled, ...restProps }: CheckboxProps) {
|
||||
return (
|
||||
<motion.div
|
||||
variants={animateDropdownItem}
|
||||
title={title}
|
||||
className={clsx(
|
||||
'px-3 py-1',
|
||||
'text-left overflow-ellipsis whitespace-nowrap',
|
||||
|
|
28
rsconcept/frontend/src/components/ui/DropdownDivider.tsx
Normal file
28
rsconcept/frontend/src/components/ui/DropdownDivider.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import clsx from 'clsx';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
import { animateDropdownItem } from '@/styling/animations';
|
||||
|
||||
import { DividerProps } from './Divider';
|
||||
|
||||
/**
|
||||
* DropdownDivider component that renders {@link Divider} with animation inside {@link Dropdown}.
|
||||
*/
|
||||
function DropdownDivider({ vertical, margins = 'mx-2', className, ...restProps }: DividerProps) {
|
||||
return (
|
||||
<motion.div
|
||||
variants={animateDropdownItem}
|
||||
className={clsx(
|
||||
margins, //prettier: split-lines
|
||||
className,
|
||||
{
|
||||
'border-x': vertical,
|
||||
'border-y': !vertical
|
||||
}
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default DropdownDivider;
|
|
@ -1,9 +1,17 @@
|
|||
interface EmbedYoutubeProps {
|
||||
/** Video ID to embed. */
|
||||
videoID: string;
|
||||
|
||||
/** Display height in pixels. */
|
||||
pxHeight: number;
|
||||
|
||||
/** Display width in pixels. */
|
||||
pxWidth?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* EmbedYoutube component that embeds a YouTube video into the page using the given video ID and dimensions.
|
||||
*/
|
||||
function EmbedYoutube({ videoID, pxHeight, pxWidth }: EmbedYoutubeProps) {
|
||||
if (!pxWidth) {
|
||||
pxWidth = (pxHeight * 16) / 9;
|
||||
|
|
|
@ -9,12 +9,19 @@ import Button from './Button';
|
|||
import Label from './Label';
|
||||
|
||||
interface FileInputProps extends Omit<CProps.Input, 'accept' | 'type'> {
|
||||
/** Label to display in file upload button. */
|
||||
label: string;
|
||||
|
||||
/** Filter: file types. */
|
||||
acceptType?: string;
|
||||
|
||||
/** Callback to set the `value`. Value is transmitted as `event.target.files[0]`. */
|
||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* FileInput component for selecting a `file`, displaying the selected file name.
|
||||
*/
|
||||
function FileInput({ id, label, acceptType, title, className, style, onChange, ...restProps }: FileInputProps) {
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [fileName, setFileName] = useState('');
|
||||
|
|
|
@ -2,6 +2,10 @@ import clsx from 'clsx';
|
|||
|
||||
import { CProps } from '../props';
|
||||
|
||||
/**
|
||||
* FlexColumn component that renders a `flex` column container.
|
||||
* This component is useful for creating vertical layouts with flexbox.
|
||||
*/
|
||||
function FlexColumn({ className, children, ...restProps }: CProps.Div) {
|
||||
return (
|
||||
<div className={clsx('cc-column', className)} {...restProps}>
|
||||
|
|
40
rsconcept/frontend/src/components/ui/Indicator.tsx
Normal file
40
rsconcept/frontend/src/components/ui/Indicator.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { globals } from '@/utils/constants';
|
||||
|
||||
import { CProps } from '../props';
|
||||
|
||||
interface IndicatorProps extends CProps.Titled, CProps.Styling {
|
||||
/** Icon to display. */
|
||||
icon: React.ReactNode;
|
||||
|
||||
/** Indicates whether the indicator should have no padding. */
|
||||
noPadding?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicator component that displays a status `icon` with a tooltip.
|
||||
*/
|
||||
function Indicator({ icon, title, titleHtml, hideTitle, noPadding, className, ...restProps }: IndicatorProps) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'clr-btn-clear',
|
||||
'outline-none',
|
||||
{
|
||||
'px-1 py-1': !noPadding
|
||||
},
|
||||
className
|
||||
)}
|
||||
data-tooltip-id={!!title || !!titleHtml ? globals.tooltip : undefined}
|
||||
data-tooltip-html={titleHtml}
|
||||
data-tooltip-content={title}
|
||||
data-tooltip-hidden={hideTitle}
|
||||
{...restProps}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Indicator;
|
|
@ -36,12 +36,12 @@ function Tooltip({
|
|||
'max-h-[calc(100svh-6rem)]',
|
||||
'overflow-y-auto overflow-x-hidden sm:overflow-hidden overscroll-contain',
|
||||
'border shadow-md',
|
||||
'text-balance',
|
||||
'text-pretty',
|
||||
layer,
|
||||
className
|
||||
)}
|
||||
classNameArrow={layer}
|
||||
style={{ ...{ paddingTop: '2px', paddingBottom: '2px' }, ...style }}
|
||||
style={{ ...{ paddingTop: '2px', paddingBottom: '2px', paddingLeft: '8px', paddingRight: '8px' }, ...style }}
|
||||
variant={darkMode ? 'dark' : 'light'}
|
||||
place={place}
|
||||
{...restProps}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { createContext, useCallback, useContext, useLayoutEffect, useMemo, useState } from 'react';
|
||||
|
||||
import Tooltip from '@/components/ui/Tooltip';
|
||||
|
@ -147,17 +146,17 @@ export const OptionsState = ({ children }: OptionsStateProps) => {
|
|||
>
|
||||
<>
|
||||
<Tooltip
|
||||
float
|
||||
float // prettier: split-lines
|
||||
id={`${globals.tooltip}`}
|
||||
layer='z-topmost'
|
||||
place='right-start'
|
||||
className={clsx('mt-8', 'max-w-[20rem]')}
|
||||
className='mt-8 max-w-[20rem]'
|
||||
/>
|
||||
<Tooltip
|
||||
float
|
||||
id={`${globals.value_tooltip}`}
|
||||
layer='z-topmost'
|
||||
className={clsx('max-w-[calc(min(40rem,100dvw-2rem))]')}
|
||||
className='max-w-[calc(min(40rem,100dvw-2rem))] text-justify'
|
||||
/>
|
||||
|
||||
{children}
|
||||
|
|
|
@ -63,7 +63,7 @@ function DlgChangeInputSchema({ oss, hideWindow, target, onSubmit }: DlgChangeIn
|
|||
itemType={LibraryItemType.RSFORM}
|
||||
value={selected} // prettier: split-line
|
||||
onSelectValue={handleSelectLocation}
|
||||
rows={8}
|
||||
rows={14}
|
||||
baseFilter={baseFilter}
|
||||
/>
|
||||
</Modal>
|
||||
|
|
|
@ -58,7 +58,7 @@ function DlgInlineSynthesis({ hideWindow, receiver, onInlineSynthesis }: DlgInli
|
|||
const schemaPanel = useMemo(
|
||||
() => (
|
||||
<TabPanel>
|
||||
<TabSchema selected={donorID} setSelected={setDonorID} />
|
||||
<TabSchema selected={donorID} setSelected={setDonorID} receiver={receiver} />
|
||||
</TabPanel>
|
||||
),
|
||||
[donorID]
|
||||
|
|
|
@ -7,15 +7,19 @@ import TextInput from '@/components/ui/TextInput';
|
|||
import AnimateFade from '@/components/wrap/AnimateFade';
|
||||
import { useLibrary } from '@/context/LibraryContext';
|
||||
import { LibraryItemID, LibraryItemType } from '@/models/library';
|
||||
import { IRSForm } from '@/models/rsform';
|
||||
import { sortItemsForInlineSynthesis } from '@/models/rsformAPI';
|
||||
|
||||
interface TabSchemaProps {
|
||||
selected?: LibraryItemID;
|
||||
setSelected: (newValue: LibraryItemID) => void;
|
||||
receiver: IRSForm;
|
||||
}
|
||||
|
||||
function TabSchema({ selected, setSelected }: TabSchemaProps) {
|
||||
function TabSchema({ selected, receiver, setSelected }: TabSchemaProps) {
|
||||
const library = useLibrary();
|
||||
const selectedInfo = useMemo(() => library.items.find(item => item.id === selected), [selected, library.items]);
|
||||
const sortedItems = useMemo(() => sortItemsForInlineSynthesis(receiver, library.items), [receiver, library.items]);
|
||||
|
||||
return (
|
||||
<AnimateFade className='flex flex-col'>
|
||||
|
@ -33,7 +37,7 @@ function TabSchema({ selected, setSelected }: TabSchemaProps) {
|
|||
</div>
|
||||
<PickSchema
|
||||
id='dlg_schema_picker' // prettier: split lines
|
||||
items={library.items}
|
||||
items={sortedItems}
|
||||
itemType={LibraryItemType.RSFORM}
|
||||
rows={14}
|
||||
value={selected}
|
||||
|
|
|
@ -32,6 +32,8 @@ export enum LocationHead {
|
|||
LIBRARY = '/L'
|
||||
}
|
||||
|
||||
export const BASIC_SCHEMAS = '/L/Базовые';
|
||||
|
||||
/**
|
||||
* Represents {@link LibraryItem} identifier type.
|
||||
*/
|
||||
|
|
|
@ -44,7 +44,7 @@ describe('Testing matching LibraryItem', () => {
|
|||
expect(matchLibraryItem(item1, item1.title + '@invalid')).toEqual(false);
|
||||
expect(matchLibraryItem(item1, item1.alias + '@invalid')).toEqual(false);
|
||||
expect(matchLibraryItem(item1, item1.time_create)).toEqual(false);
|
||||
expect(matchLibraryItem(item1, item1.comment)).toEqual(false);
|
||||
expect(matchLibraryItem(item1, item1.comment)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ const LOCATION_REGEXP = /^\/[PLUS]((\/[!\d\p{L}]([!\d\p{L}\- ]*[!\d\p{L}])?)*)?$
|
|||
*/
|
||||
export function matchLibraryItem(target: ILibraryItem, query: string): boolean {
|
||||
const matcher = new TextMatcher(query);
|
||||
return matcher.test(target.alias) || matcher.test(target.title);
|
||||
return matcher.test(target.alias) || matcher.test(target.title) || matcher.test(target.comment);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -26,9 +26,6 @@ export function matchOperation(target: IOperation, query: string): boolean {
|
|||
|
||||
/**
|
||||
* Sorts library items relevant for the specified {@link IOperationSchema}.
|
||||
*
|
||||
* @param oss - The {@link IOperationSchema} to be sorted.
|
||||
* @param items - The items to be sorted.
|
||||
*/
|
||||
export function sortItemsForOSS(oss: IOperationSchema, items: ILibraryItem[]): ILibraryItem[] {
|
||||
const result = items.filter(item => item.location === oss.location);
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
import { TextMatcher } from '@/utils/utils';
|
||||
|
||||
import { BASIC_SCHEMAS, ILibraryItem } from './library';
|
||||
import { CstMatchMode } from './miscellaneous';
|
||||
import {
|
||||
CATEGORY_CST_TYPE,
|
||||
|
@ -308,3 +309,31 @@ export function generateAlias(type: CstType, schema: IRSForm, takenAliases: stri
|
|||
}
|
||||
return alias;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts library items relevant for InlineSynthesis with specified {@link IRSForm}.
|
||||
*/
|
||||
export function sortItemsForInlineSynthesis(receiver: IRSForm, items: ILibraryItem[]): ILibraryItem[] {
|
||||
const result = items.filter(item => item.location === receiver.location);
|
||||
for (const item of items) {
|
||||
if (item.visible && item.owner === item.owner && !result.includes(item)) {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
for (const item of items) {
|
||||
if (!result.includes(item) && item.location.startsWith(BASIC_SCHEMAS)) {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
for (const item of items) {
|
||||
if (item.visible && !result.includes(item)) {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
for (const item of items) {
|
||||
if (!result.includes(item)) {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -40,6 +40,21 @@ function ViewSideLocation({
|
|||
const { user } = useAuth();
|
||||
const { items } = useLibrary();
|
||||
const windowSize = useWindowSize();
|
||||
|
||||
const canRename = useMemo(() => {
|
||||
if (active.length <= 3 || !user) {
|
||||
return false;
|
||||
}
|
||||
if (user.is_staff) {
|
||||
return true;
|
||||
}
|
||||
const owned = items.filter(item => item.owner == user.id);
|
||||
const located = owned.filter(item => item.location == active || item.location.startsWith(`${active}/`));
|
||||
return located.length !== 0;
|
||||
}, [active, user, items]);
|
||||
|
||||
const animations = useMemo(() => animateSideMinWidth(windowSize.isSmall ? '10rem' : '15rem'), [windowSize]);
|
||||
|
||||
const handleClickFolder = useCallback(
|
||||
(event: CProps.EventMouse, target: FolderNode) => {
|
||||
event.preventDefault();
|
||||
|
@ -56,20 +71,6 @@ function ViewSideLocation({
|
|||
[setActive]
|
||||
);
|
||||
|
||||
const canRename = useMemo(() => {
|
||||
if (active.length <= 3 || !user) {
|
||||
return false;
|
||||
}
|
||||
if (user.is_staff) {
|
||||
return true;
|
||||
}
|
||||
const owned = items.filter(item => item.owner == user.id);
|
||||
const located = owned.filter(item => item.location == active || item.location.startsWith(`${active}/`));
|
||||
return located.length !== 0;
|
||||
}, [active, user, items]);
|
||||
|
||||
const animations = useMemo(() => animateSideMinWidth(windowSize.isSmall ? '10rem' : '15rem'), [windowSize]);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={clsx('max-w-[10rem] sm:max-w-[15rem]', 'flex flex-col', 'text:xs sm:text-sm', 'select-none')}
|
||||
|
|
|
@ -111,7 +111,7 @@ function NodeContextMenu({
|
|||
text='Редактировать'
|
||||
title='Редактировать операцию'
|
||||
icon={<IconEdit2 size='1rem' className='icon-primary' />}
|
||||
disabled={controller.isProcessing}
|
||||
disabled={!controller.isMutable || controller.isProcessing}
|
||||
onClick={handleEditOperation}
|
||||
/>
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { IconConsolidation, IconRSForm } from '@/components/Icons';
|
||||
import TooltipOperation from '@/components/info/TooltipOperation';
|
||||
import MiniButton from '@/components/ui/MiniButton.tsx';
|
||||
import Indicator from '@/components/ui/Indicator';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
import { OssNodeInternal } from '@/models/miscellaneous';
|
||||
import { OperationType } from '@/models/oss';
|
||||
|
@ -25,19 +25,15 @@ function NodeCore({ node }: NodeCoreProps) {
|
|||
return (
|
||||
<>
|
||||
<Overlay position='top-0 right-0' className='flex flex-col gap-1 p-[2px]'>
|
||||
<MiniButton
|
||||
disabled
|
||||
noHover
|
||||
<Indicator
|
||||
noPadding
|
||||
title={hasFile ? 'Связанная КС' : 'Нет связанной КС'}
|
||||
icon={<IconRSForm className={hasFile ? 'clr-text-green' : 'clr-text-red'} size='12px' />}
|
||||
hideTitle={!controller.showTooltip}
|
||||
/>
|
||||
{node.data.operation.is_consolidation ? (
|
||||
<MiniButton
|
||||
disabled
|
||||
<Indicator
|
||||
noPadding
|
||||
noHover
|
||||
titleHtml='<b>Внимание!</b><br />Ромбовидный синтез</br/>Возможны дубликаты конституент'
|
||||
icon={<IconConsolidation className='clr-text-primary' size='12px' />}
|
||||
hideTitle={!controller.showTooltip}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { toSvg } from 'html-to-image';
|
||||
import { toPng } from 'html-to-image';
|
||||
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import {
|
||||
|
@ -256,7 +256,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
const imageHeight = PARAMETER.ossImageHeight;
|
||||
const nodesBounds = getNodesBounds(nodes);
|
||||
const viewport = getViewportForBounds(nodesBounds, imageWidth, imageHeight, 0.5, 2);
|
||||
toSvg(canvas, {
|
||||
toPng(canvas, {
|
||||
backgroundColor: colors.bgDefault,
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
|
@ -268,7 +268,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
})
|
||||
.then(dataURL => {
|
||||
const a = document.createElement('a');
|
||||
a.setAttribute('download', `${model.schema?.alias ?? 'oss'}.svg`);
|
||||
a.setAttribute('download', `${model.schema?.alias ?? 'oss'}.png`);
|
||||
a.setAttribute('href', dataURL);
|
||||
a.click();
|
||||
})
|
||||
|
@ -416,7 +416,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
{...menuProps}
|
||||
/>
|
||||
) : null}
|
||||
<div className='relative w-[100vw]' style={{ height: mainHeight }}>
|
||||
<div className='relative w-[100vw]' style={{ height: mainHeight, fontFamily: 'Rubik' }}>
|
||||
{graph}
|
||||
</div>
|
||||
</AnimateFade>
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
IconShare
|
||||
} from '@/components/Icons';
|
||||
import Button from '@/components/ui/Button';
|
||||
import DropdownDivider from '@/components/ui/DropdownDivider';
|
||||
import Dropdown from '@/components/ui/Dropdown';
|
||||
import DropdownButton from '@/components/ui/DropdownButton';
|
||||
import { useAccessMode } from '@/context/AccessModeContext';
|
||||
|
@ -94,9 +95,11 @@ function MenuOssTabs({ onDestroy }: MenuOssTabsProps) {
|
|||
onClick={handleDelete}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<DropdownDivider margins='mx-3 my-1' />
|
||||
|
||||
{user ? (
|
||||
<DropdownButton
|
||||
className='border-t-2'
|
||||
text='Создать новую схему'
|
||||
icon={<IconNewItem size='1rem' className='icon-primary' />}
|
||||
onClick={handleCreateNew}
|
||||
|
|
|
@ -7,7 +7,7 @@ import { toast } from 'react-toastify';
|
|||
|
||||
import { IconChild, IconPredecessor, IconSave } from '@/components/Icons';
|
||||
import RefsInput from '@/components/RefsInput';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import Indicator from '@/components/ui/Indicator';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
import SubmitButton from '@/components/ui/SubmitButton';
|
||||
import TextArea from '@/components/ui/TextArea';
|
||||
|
@ -165,7 +165,7 @@ function FormConstituenta({
|
|||
dense
|
||||
noResize
|
||||
noBorder
|
||||
disabled={true}
|
||||
disabled
|
||||
label='Типизация'
|
||||
value={typification}
|
||||
colors='clr-app clr-text-default'
|
||||
|
@ -250,16 +250,14 @@ function FormConstituenta({
|
|||
/>
|
||||
<Overlay position='top-[0.1rem] left-[0.4rem]' className='cc-icons'>
|
||||
{state.is_inherited_parent && !state.is_inherited ? (
|
||||
<MiniButton
|
||||
<Indicator
|
||||
icon={<IconPredecessor size='1.25rem' className='clr-text-primary' />}
|
||||
disabled
|
||||
titleHtml='Внимание!</br> Конституента имеет потомков<br/> в операционной схеме синтеза'
|
||||
/>
|
||||
) : null}
|
||||
{state.is_inherited ? (
|
||||
<MiniButton
|
||||
<Indicator
|
||||
icon={<IconChild size='1.25rem' className='clr-text-primary' />}
|
||||
disabled
|
||||
titleHtml='Внимание!</br> Конституента является наследником<br/>'
|
||||
/>
|
||||
) : null}
|
||||
|
|
|
@ -20,7 +20,7 @@ function ToolbarVersioning({ blockReload }: ToolbarVersioningProps) {
|
|||
<MiniButton
|
||||
titleHtml={
|
||||
blockReload
|
||||
? 'Невозможно откатить КС, прикрепленную к операционной схеме'
|
||||
? 'Невозможно откатить КС, <br>прикрепленную к операционной схеме'
|
||||
: !controller.isContentEditable
|
||||
? 'Откатить к версии'
|
||||
: 'Переключитесь на <br/>неактуальную версию'
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
IconUpload
|
||||
} from '@/components/Icons';
|
||||
import Button from '@/components/ui/Button';
|
||||
import DropdownDivider from '@/components/ui/DropdownDivider';
|
||||
import Dropdown from '@/components/ui/Dropdown';
|
||||
import DropdownButton from '@/components/ui/DropdownButton';
|
||||
import { useAccessMode } from '@/context/AccessModeContext';
|
||||
|
@ -184,9 +185,11 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
|||
onClick={handleDelete}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<DropdownDivider margins='mx-3 my-1' />
|
||||
|
||||
{user ? (
|
||||
<DropdownButton
|
||||
className='border-t-2'
|
||||
text='Создать новую схему'
|
||||
icon={<IconNewItem size='1rem' className='icon-primary' />}
|
||||
onClick={handleCreateNew}
|
||||
|
@ -235,8 +238,10 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
|||
disabled={!controller.isContentEditable || controller.isProcessing}
|
||||
onClick={handleInlineSynthesis}
|
||||
/>
|
||||
|
||||
<DropdownDivider margins='mx-3 my-1' />
|
||||
|
||||
<DropdownButton
|
||||
className='border-t-2'
|
||||
text='Упорядочить список'
|
||||
titleHtml='Упорядочить список, исходя из <br/>логики типов и связей конституент'
|
||||
icon={<IconSortList size='1rem' className='icon-primary' />}
|
||||
|
|
|
@ -51,7 +51,7 @@ export interface IColorTheme {
|
|||
*/
|
||||
// prettier-ignore
|
||||
export const lightT: IColorTheme = {
|
||||
bgDefault: 'var(--cl-bg-100)',
|
||||
bgDefault: 'hsl(000, 000%, 098%)', //var(--cl-bg-100)',
|
||||
bgInput: 'var(--cl-bg-120)',
|
||||
bgControls: 'var(--cl-bg-80)',
|
||||
bgDisabled: 'var(--cl-bg-60)',
|
||||
|
@ -91,7 +91,7 @@ export const lightT: IColorTheme = {
|
|||
*/
|
||||
// prettier-ignore
|
||||
export const darkT: IColorTheme = {
|
||||
bgDefault: 'var(--cd-bg-100)',
|
||||
bgDefault: 'hsl(000, 000%, 005%)', //'var(--cd-bg-100)',
|
||||
bgInput: 'var(--cd-bg-120)',
|
||||
bgControls: 'var(--cd-bg-80)',
|
||||
bgDisabled: 'var(--cd-bg-60)',
|
||||
|
|
|
@ -141,7 +141,32 @@ div:not(.dense) > p {
|
|||
@apply rounded;
|
||||
}
|
||||
|
||||
:is(.sticky) {
|
||||
.border,
|
||||
.border-x,
|
||||
.border-y,
|
||||
.border-b,
|
||||
.border-t,
|
||||
.border-l,
|
||||
.border-r,
|
||||
.border-2,
|
||||
.border-x-2,
|
||||
.border-y-2,
|
||||
.border-b-2,
|
||||
.border-t-2,
|
||||
.border-l-2,
|
||||
.border-r-2,
|
||||
.divide-x,
|
||||
.divide-y,
|
||||
.divide-x-2,
|
||||
.divide-y-2 {
|
||||
border-color: var(--cl-bg-40);
|
||||
@apply divide-inherit;
|
||||
.dark & {
|
||||
border-color: var(--cd-bg-40);
|
||||
}
|
||||
}
|
||||
|
||||
.sticky {
|
||||
z-index: 20;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,35 +21,8 @@
|
|||
}
|
||||
|
||||
@layer components {
|
||||
:is(
|
||||
.clr-border,
|
||||
.border,
|
||||
.border-x,
|
||||
.border-y,
|
||||
.border-b,
|
||||
.border-t,
|
||||
.border-l,
|
||||
.border-r,
|
||||
.border-2,
|
||||
.border-x-2,
|
||||
.border-y-2,
|
||||
.border-b-2,
|
||||
.border-t-2,
|
||||
.border-l-2,
|
||||
.border-r-2,
|
||||
.divide-x,
|
||||
.divide-y,
|
||||
.divide-x-2,
|
||||
.divide-y-2
|
||||
) {
|
||||
border-color: var(--cl-bg-40);
|
||||
@apply divide-inherit;
|
||||
.dark & {
|
||||
border-color: var(--cd-bg-40);
|
||||
}
|
||||
}
|
||||
|
||||
:is(.clr-app, .clr-btn-nav) {
|
||||
.clr-app,
|
||||
.clr-btn-nav {
|
||||
background-color: var(--cl-bg-100);
|
||||
.dark & {
|
||||
background-color: var(--cd-bg-100);
|
||||
|
@ -86,14 +59,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
:is(.clr-controls, .clr-tab, .clr-btn-default) {
|
||||
.clr-controls,
|
||||
.clr-tab,
|
||||
.clr-btn-default {
|
||||
background-color: var(--cl-bg-80);
|
||||
.dark & {
|
||||
background-color: var(--cd-bg-80);
|
||||
}
|
||||
}
|
||||
|
||||
:is(.clr-primary, .clr-btn-primary:hover, .clr-btn-primary:focus-visible) {
|
||||
.clr-primary,
|
||||
.clr-btn-primary:hover,
|
||||
.clr-btn-primary:focus-visible {
|
||||
@apply transition;
|
||||
color: var(--cl-prim-fg-100);
|
||||
background-color: var(--cl-prim-bg-100);
|
||||
|
@ -103,7 +80,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
:is(.clr-selected, .clr-btn-primary) {
|
||||
.clr-selected,
|
||||
.clr-btn-primary {
|
||||
color: var(--cl-fg-100);
|
||||
background-color: var(--cl-prim-bg-80);
|
||||
.dark & {
|
||||
|
@ -142,15 +120,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
:is(.clr-text-primary, .clr-text-url) {
|
||||
.clr-text-primary,
|
||||
.clr-text-url {
|
||||
color: var(--cl-prim-fg-80);
|
||||
.dark & {
|
||||
color: var(--cd-prim-fg-80);
|
||||
}
|
||||
}
|
||||
|
||||
:is(.clr-text-controls, .clr-btn-nav, .clr-btn-clear) {
|
||||
.clr-text-controls,
|
||||
.clr-btn-nav,
|
||||
.clr-btn-clear {
|
||||
color: var(--cl-fg-80);
|
||||
background-color: transparent;
|
||||
&:disabled {
|
||||
color: var(--cl-fg-60);
|
||||
}
|
||||
|
@ -169,7 +151,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
:is(.clr-text-default, input:disabled:not(::placeholder), textarea:disabled:not(::placeholder)) {
|
||||
.clr-text-default,
|
||||
input:disabled:not(::placeholder),
|
||||
textarea:disabled:not(::placeholder) {
|
||||
opacity: 1;
|
||||
-webkit-text-fill-color: var(--cl-fg-100);
|
||||
color: var(--cl-fg-100);
|
||||
|
@ -225,11 +209,6 @@
|
|||
@apply top-[1.9rem] pt-1 right-1/2 translate-x-1/2;
|
||||
}
|
||||
|
||||
.cc-modal-blur {
|
||||
opacity: 0.3;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.cc-label {
|
||||
@apply text-sm font-medium cursor-default select-text whitespace-nowrap;
|
||||
}
|
||||
|
@ -261,6 +240,11 @@
|
|||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
.cc-modal-blur {
|
||||
opacity: 0.3;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.cc-shadow-border {
|
||||
@apply shadow-sm shadow-[var(--cl-bg-40)] dark:shadow-[var(--cd-bg-40)];
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user