Compare commits
10 Commits
27ba4a5da8
...
513d6a5b71
Author | SHA1 | Date | |
---|---|---|---|
![]() |
513d6a5b71 | ||
![]() |
91642816b1 | ||
![]() |
edc728fe00 | ||
![]() |
be0dfdefd8 | ||
![]() |
92a0453b18 | ||
![]() |
c0d01957ff | ||
![]() |
cc8cb2d53c | ||
![]() |
95d38cea7c | ||
![]() |
e96206b7db | ||
![]() |
044a484607 |
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
|
@ -28,7 +28,9 @@
|
|||
"editor.tabSize": 4,
|
||||
"editor.insertSpaces": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "explicit"
|
||||
"source.organizeImports": "explicit",
|
||||
"source.fixAll.ts": "never",
|
||||
"source.fixAll.eslint": "never"
|
||||
}
|
||||
},
|
||||
"python.testing.unittestArgs": ["-v", "-s", "./tests", "-p", "test*.py"],
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 5.0.7 on 2024-08-06 19:31
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='editor',
|
||||
name='editor',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Редактор'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 5.0.7 on 2024-08-06 19:35
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0002_alter_editor_editor'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='librarytemplate',
|
||||
name='lib_source',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='library.libraryitem', verbose_name='Источник'),
|
||||
),
|
||||
]
|
|
@ -1,14 +1,11 @@
|
|||
''' Models: Editor. '''
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Iterable
|
||||
|
||||
from django.db import transaction
|
||||
from django.db.models import CASCADE, DateTimeField, ForeignKey, Model
|
||||
|
||||
from apps.users.models import User
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .LibraryItem import LibraryItem
|
||||
|
||||
|
||||
class Editor(Model):
|
||||
''' Editor list. '''
|
||||
|
@ -20,8 +17,7 @@ class Editor(Model):
|
|||
editor: ForeignKey = ForeignKey(
|
||||
verbose_name='Редактор',
|
||||
to=User,
|
||||
on_delete=CASCADE,
|
||||
null=True
|
||||
on_delete=CASCADE
|
||||
)
|
||||
time_create: DateTimeField = DateTimeField(
|
||||
verbose_name='Дата добавления',
|
||||
|
@ -38,17 +34,17 @@ class Editor(Model):
|
|||
return f'{self.item}: {self.editor}'
|
||||
|
||||
@staticmethod
|
||||
def add(item: 'LibraryItem', user: User) -> bool:
|
||||
def add(item: int, user: int) -> bool:
|
||||
''' Add Editor for item. '''
|
||||
if Editor.objects.filter(item=item, editor=user).exists():
|
||||
if Editor.objects.filter(item_id=item, editor_id=user).exists():
|
||||
return False
|
||||
Editor.objects.create(item=item, editor=user)
|
||||
Editor.objects.create(item_id=item, editor_id=user)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def remove(item: 'LibraryItem', user: User) -> bool:
|
||||
def remove(item: int, user: int) -> bool:
|
||||
''' Remove Editor. '''
|
||||
editor = Editor.objects.filter(item=item, editor=user)
|
||||
editor = Editor.objects.filter(item_id=item, editor_id=user).only('pk')
|
||||
if not editor.exists():
|
||||
return False
|
||||
editor.delete()
|
||||
|
@ -56,16 +52,40 @@ class Editor(Model):
|
|||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def set(item: 'LibraryItem', users: list[User]):
|
||||
def set(item: int, users: Iterable[int]):
|
||||
''' Set editors for item. '''
|
||||
processed: list[User] = []
|
||||
for editor_item in Editor.objects.filter(item=item):
|
||||
if editor_item.editor not in users:
|
||||
processed: set[int] = set()
|
||||
for editor_item in Editor.objects.filter(item_id=item).only('editor_id'):
|
||||
editor_id = editor_item.editor_id
|
||||
if editor_id not in users:
|
||||
editor_item.delete()
|
||||
else:
|
||||
processed.append(editor_item.editor)
|
||||
processed.add(editor_id)
|
||||
|
||||
for user in users:
|
||||
if user not in processed:
|
||||
processed.add(user)
|
||||
Editor.objects.create(item_id=item, editor_id=user)
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def set_and_return_diff(item: int, users: Iterable[int]) -> tuple[list[int], list[int]]:
|
||||
''' Set editors for item and return diff. '''
|
||||
processed: list[int] = []
|
||||
deleted: list[int] = []
|
||||
added: list[int] = []
|
||||
for editor_item in Editor.objects.filter(item_id=item).only('editor_id'):
|
||||
editor_id = editor_item.editor_id
|
||||
if editor_id not in users:
|
||||
deleted.append(editor_id)
|
||||
editor_item.delete()
|
||||
else:
|
||||
processed.append(editor_id)
|
||||
|
||||
for user in users:
|
||||
if user not in processed:
|
||||
processed.append(user)
|
||||
Editor.objects.create(item=item, editor=user)
|
||||
added.append(user)
|
||||
Editor.objects.create(item_id=item, editor_id=user)
|
||||
|
||||
return (added, deleted)
|
||||
|
|
|
@ -16,7 +16,6 @@ from django.db.models import (
|
|||
|
||||
from apps.users.models import User
|
||||
|
||||
from .Editor import Editor
|
||||
from .Subscription import Subscription
|
||||
from .Version import Version
|
||||
|
||||
|
@ -115,18 +114,19 @@ class LibraryItem(Model):
|
|||
def get_absolute_url(self):
|
||||
return f'/api/library/{self.pk}'
|
||||
|
||||
def subscribers(self) -> list[User]:
|
||||
def subscribers(self) -> QuerySet[User]:
|
||||
''' Get all subscribers for this item. '''
|
||||
return [subscription.user for subscription in Subscription.objects.filter(item=self.pk).only('user')]
|
||||
return User.objects.filter(subscription__item=self.pk)
|
||||
|
||||
def editors(self) -> list[User]:
|
||||
def editors(self) -> QuerySet[User]:
|
||||
''' Get all Editors of this item. '''
|
||||
return [item.editor for item in Editor.objects.filter(item=self.pk).only('editor')]
|
||||
return User.objects.filter(editor__item=self.pk)
|
||||
|
||||
def versions(self) -> QuerySet[Version]:
|
||||
''' Get all Versions of this item. '''
|
||||
return Version.objects.filter(item=self.pk).order_by('-time_create')
|
||||
|
||||
# TODO: move to View layer
|
||||
@transaction.atomic
|
||||
def save(self, *args, **kwargs):
|
||||
''' Save updating subscriptions and connected operations. '''
|
||||
|
@ -135,7 +135,7 @@ class LibraryItem(Model):
|
|||
subscribe = self._state.adding and self.owner
|
||||
super().save(*args, **kwargs)
|
||||
if subscribe:
|
||||
Subscription.subscribe(user=self.owner, item=self)
|
||||
Subscription.subscribe(user=self.owner_id, item=self.pk)
|
||||
|
||||
def _update_connected_operations(self):
|
||||
# using method level import to prevent circular dependency
|
||||
|
|
|
@ -7,8 +7,7 @@ class LibraryTemplate(Model):
|
|||
lib_source: ForeignKey = ForeignKey(
|
||||
verbose_name='Источник',
|
||||
to='library.LibraryItem',
|
||||
on_delete=CASCADE,
|
||||
null=True
|
||||
on_delete=CASCADE
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -1,13 +1,8 @@
|
|||
''' Models: Subscription. '''
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from django.db.models import CASCADE, ForeignKey, Model
|
||||
|
||||
from apps.users.models import User
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .LibraryItem import LibraryItem
|
||||
|
||||
|
||||
class Subscription(Model):
|
||||
''' User subscription to library item. '''
|
||||
|
@ -32,17 +27,17 @@ class Subscription(Model):
|
|||
return f'{self.user} -> {self.item}'
|
||||
|
||||
@staticmethod
|
||||
def subscribe(user: User, item: 'LibraryItem') -> bool:
|
||||
def subscribe(user: int, item: int) -> bool:
|
||||
''' Add subscription. '''
|
||||
if Subscription.objects.filter(user=user, item=item).exists():
|
||||
if Subscription.objects.filter(user_id=user, item_id=item).exists():
|
||||
return False
|
||||
Subscription.objects.create(user=user, item=item)
|
||||
Subscription.objects.create(user_id=user, item_id=item)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def unsubscribe(user: User, item: 'LibraryItem') -> bool:
|
||||
def unsubscribe(user: int, item: int) -> bool:
|
||||
''' Remove subscription. '''
|
||||
sub = Subscription.objects.filter(user=user, item=item)
|
||||
sub = Subscription.objects.filter(user_id=user, item_id=item).only('pk')
|
||||
if not sub.exists():
|
||||
return False
|
||||
sub.delete()
|
||||
|
|
|
@ -83,10 +83,10 @@ class LibraryItemDetailsSerializer(serializers.ModelSerializer):
|
|||
read_only_fields = ('owner', 'id', 'item_type')
|
||||
|
||||
def get_subscribers(self, instance: LibraryItem) -> list[int]:
|
||||
return [item.pk for item in instance.subscribers()]
|
||||
return list(instance.subscribers().values_list('pk', flat=True))
|
||||
|
||||
def get_editors(self, instance: LibraryItem) -> list[int]:
|
||||
return [item.pk for item in instance.editors()]
|
||||
return list(instance.editors().values_list('pk', flat=True))
|
||||
|
||||
def get_versions(self, instance: LibraryItem) -> list:
|
||||
return [VersionInnerSerializer(item).data for item in instance.versions()]
|
||||
|
@ -94,9 +94,9 @@ class LibraryItemDetailsSerializer(serializers.ModelSerializer):
|
|||
|
||||
class UserTargetSerializer(serializers.Serializer):
|
||||
''' Serializer: Target single User. '''
|
||||
user = PKField(many=False, queryset=User.objects.all())
|
||||
user = PKField(many=False, queryset=User.objects.all().only('pk'))
|
||||
|
||||
|
||||
class UsersListSerializer(serializers.Serializer):
|
||||
''' Serializer: List of Users. '''
|
||||
users = PKField(many=True, queryset=User.objects.all())
|
||||
users = PKField(many=True, queryset=User.objects.all().only('pk'))
|
||||
|
|
|
@ -34,44 +34,65 @@ class TestEditor(TestCase):
|
|||
|
||||
|
||||
def test_add_editor(self):
|
||||
self.assertTrue(Editor.add(self.item, self.user1))
|
||||
self.assertEqual(len(self.item.editors()), 1)
|
||||
self.assertTrue(self.user1 in self.item.editors())
|
||||
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.assertFalse(Editor.add(self.item, self.user1))
|
||||
self.assertEqual(len(self.item.editors()), 1)
|
||||
self.assertFalse(Editor.add(self.item.pk, self.user1.pk))
|
||||
self.assertEqual(self.item.editors().count(), 1)
|
||||
|
||||
self.assertTrue(Editor.add(self.item, self.user2))
|
||||
self.assertEqual(len(self.item.editors()), 2)
|
||||
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.user1.delete()
|
||||
self.assertEqual(len(self.item.editors()), 1)
|
||||
self.assertEqual(self.item.editors().count(), 1)
|
||||
|
||||
|
||||
def test_remove_editor(self):
|
||||
self.assertFalse(Editor.remove(self.item, self.user1))
|
||||
Editor.add(self.item, self.user1)
|
||||
Editor.add(self.item, self.user2)
|
||||
self.assertEqual(len(self.item.editors()), 2)
|
||||
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.assertTrue(Editor.remove(self.item, self.user1))
|
||||
self.assertEqual(len(self.item.editors()), 1)
|
||||
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.assertFalse(Editor.remove(self.item, self.user1))
|
||||
self.assertFalse(Editor.remove(self.item.pk, self.user1.pk))
|
||||
|
||||
|
||||
def test_set_editors(self):
|
||||
Editor.set(self.item, [self.user1])
|
||||
self.assertEqual(self.item.editors(), [self.user1])
|
||||
Editor.set(self.item.pk, [self.user1.pk])
|
||||
self.assertEqual(list(self.item.editors()), [self.user1])
|
||||
|
||||
Editor.set(self.item, [self.user1, self.user1])
|
||||
self.assertEqual(self.item.editors(), [self.user1])
|
||||
Editor.set(self.item.pk, [self.user1.pk, self.user1.pk])
|
||||
self.assertEqual(list(self.item.editors()), [self.user1])
|
||||
|
||||
Editor.set(self.item, [])
|
||||
self.assertEqual(self.item.editors(), [])
|
||||
Editor.set(self.item.pk, [])
|
||||
self.assertEqual(list(self.item.editors()), [])
|
||||
|
||||
Editor.set(self.item, [self.user1, self.user2])
|
||||
Editor.set(self.item.pk, [self.user1.pk, self.user2.pk])
|
||||
self.assertEqual(set(self.item.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])
|
||||
|
||||
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])
|
||||
|
||||
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()), [])
|
||||
|
||||
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]))
|
||||
|
|
|
@ -37,33 +37,33 @@ class TestSubscription(TestCase):
|
|||
|
||||
def test_subscribe(self):
|
||||
item = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title='Test')
|
||||
self.assertEqual(len(item.subscribers()), 0)
|
||||
self.assertEqual(item.subscribers().count(), 0)
|
||||
|
||||
self.assertTrue(Subscription.subscribe(self.user1, item))
|
||||
self.assertEqual(len(item.subscribers()), 1)
|
||||
self.assertTrue(Subscription.subscribe(self.user1.pk, item.pk))
|
||||
self.assertEqual(item.subscribers().count(), 1)
|
||||
self.assertTrue(self.user1 in item.subscribers())
|
||||
|
||||
self.assertFalse(Subscription.subscribe(self.user1, item))
|
||||
self.assertEqual(len(item.subscribers()), 1)
|
||||
self.assertFalse(Subscription.subscribe(self.user1.pk, item.pk))
|
||||
self.assertEqual(item.subscribers().count(), 1)
|
||||
|
||||
self.assertTrue(Subscription.subscribe(self.user2, item))
|
||||
self.assertEqual(len(item.subscribers()), 2)
|
||||
self.assertTrue(Subscription.subscribe(self.user2.pk, item.pk))
|
||||
self.assertEqual(item.subscribers().count(), 2)
|
||||
self.assertTrue(self.user1 in item.subscribers())
|
||||
self.assertTrue(self.user2 in item.subscribers())
|
||||
|
||||
self.user1.delete()
|
||||
self.assertEqual(len(item.subscribers()), 1)
|
||||
self.assertEqual(item.subscribers().count(), 1)
|
||||
|
||||
|
||||
def test_unsubscribe(self):
|
||||
item = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title='Test')
|
||||
self.assertFalse(Subscription.unsubscribe(self.user1, item))
|
||||
Subscription.subscribe(self.user1, item)
|
||||
Subscription.subscribe(self.user2, item)
|
||||
self.assertEqual(len(item.subscribers()), 2)
|
||||
self.assertFalse(Subscription.unsubscribe(self.user1.pk, item.pk))
|
||||
Subscription.subscribe(self.user1.pk, item.pk)
|
||||
Subscription.subscribe(self.user2.pk, item.pk)
|
||||
self.assertEqual(item.subscribers().count(), 2)
|
||||
|
||||
self.assertTrue(Subscription.unsubscribe(self.user1, item))
|
||||
self.assertEqual(len(item.subscribers()), 1)
|
||||
self.assertTrue(Subscription.unsubscribe(self.user1.pk, item.pk))
|
||||
self.assertEqual(item.subscribers().count(), 1)
|
||||
self.assertTrue(self.user2 in item.subscribers())
|
||||
|
||||
self.assertFalse(Subscription.unsubscribe(self.user1, item))
|
||||
self.assertFalse(Subscription.unsubscribe(self.user1.pk, item.pk))
|
||||
|
|
|
@ -183,57 +183,6 @@ class TestLibraryViewset(EndpointTester):
|
|||
self.unowned.refresh_from_db()
|
||||
self.assertEqual(self.unowned.location, data['location'])
|
||||
|
||||
@decl_endpoint('/api/library/{item}/add-editor', method='patch')
|
||||
def test_add_editor(self):
|
||||
time_update = self.owned.time_update
|
||||
|
||||
data = {'user': self.invalid_user}
|
||||
self.executeBadData(data=data, item=self.owned.pk)
|
||||
|
||||
data = {'user': self.user.pk}
|
||||
self.executeNotFound(data=data, item=self.invalid_item)
|
||||
self.executeForbidden(data=data, item=self.unowned.pk)
|
||||
|
||||
self.executeOK(data=data, item=self.owned.pk)
|
||||
self.owned.refresh_from_db()
|
||||
self.assertEqual(self.owned.time_update, time_update)
|
||||
self.assertEqual(self.owned.editors(), [self.user])
|
||||
|
||||
self.executeOK(data=data)
|
||||
self.assertEqual(self.owned.editors(), [self.user])
|
||||
|
||||
data = {'user': self.user2.pk}
|
||||
self.executeOK(data=data)
|
||||
self.assertEqual(set(self.owned.editors()), set([self.user, self.user2]))
|
||||
|
||||
|
||||
@decl_endpoint('/api/library/{item}/remove-editor', method='patch')
|
||||
def test_remove_editor(self):
|
||||
time_update = self.owned.time_update
|
||||
|
||||
data = {'user': self.invalid_user}
|
||||
self.executeBadData(data=data, item=self.owned.pk)
|
||||
|
||||
data = {'user': self.user.pk}
|
||||
self.executeNotFound(data=data, item=self.invalid_item)
|
||||
self.executeForbidden(data=data, item=self.unowned.pk)
|
||||
|
||||
self.executeOK(data=data, item=self.owned.pk)
|
||||
self.owned.refresh_from_db()
|
||||
self.assertEqual(self.owned.time_update, time_update)
|
||||
self.assertEqual(self.owned.editors(), [])
|
||||
|
||||
Editor.add(item=self.owned, user=self.user)
|
||||
self.executeOK(data=data)
|
||||
self.assertEqual(self.owned.editors(), [])
|
||||
|
||||
Editor.add(item=self.owned, user=self.user)
|
||||
Editor.add(item=self.owned, user=self.user2)
|
||||
data = {'user': self.user2.pk}
|
||||
self.executeOK(data=data)
|
||||
self.assertEqual(self.owned.editors(), [self.user])
|
||||
|
||||
|
||||
@decl_endpoint('/api/library/{item}/set-editors', method='patch')
|
||||
def test_set_editors(self):
|
||||
time_update = self.owned.time_update
|
||||
|
@ -248,18 +197,18 @@ 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(self.owned.editors(), [self.user])
|
||||
self.assertEqual(list(self.owned.editors()), [self.user])
|
||||
|
||||
self.executeOK(data=data)
|
||||
self.assertEqual(self.owned.editors(), [self.user])
|
||||
self.assertEqual(list(self.owned.editors()), [self.user])
|
||||
|
||||
data = {'users': [self.user2.pk]}
|
||||
self.executeOK(data=data)
|
||||
self.assertEqual(self.owned.editors(), [self.user2])
|
||||
self.assertEqual(list(self.owned.editors()), [self.user2])
|
||||
|
||||
data = {'users': []}
|
||||
self.executeOK(data=data)
|
||||
self.assertEqual(self.owned.editors(), [])
|
||||
self.assertEqual(list(self.owned.editors()), [])
|
||||
|
||||
data = {'users': [self.user2.pk, self.user.pk]}
|
||||
self.executeOK(data=data)
|
||||
|
@ -320,9 +269,9 @@ class TestLibraryViewset(EndpointTester):
|
|||
response = self.executeOK()
|
||||
self.assertFalse(response_contains(response, self.unowned))
|
||||
|
||||
Subscription.subscribe(user=self.user, item=self.unowned)
|
||||
Subscription.subscribe(user=self.user2, item=self.unowned)
|
||||
Subscription.subscribe(user=self.user2, item=self.owned)
|
||||
Subscription.subscribe(user=self.user.pk, item=self.unowned.pk)
|
||||
Subscription.subscribe(user=self.user2.pk, item=self.unowned.pk)
|
||||
Subscription.subscribe(user=self.user2.pk, item=self.owned.pk)
|
||||
|
||||
response = self.executeOK()
|
||||
self.assertTrue(response_contains(response, self.unowned))
|
||||
|
|
|
@ -4,15 +4,15 @@ from typing import cast
|
|||
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||
from rest_framework import filters, generics
|
||||
from rest_framework import generics
|
||||
from rest_framework import status as c
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from apps.oss.models import OperationSchema
|
||||
from apps.rsform.models import RSForm
|
||||
from apps.rsform.serializers import RSFormParseSerializer
|
||||
from apps.users.models import User
|
||||
|
@ -27,10 +27,8 @@ from .. import serializers as s
|
|||
class LibraryViewSet(viewsets.ModelViewSet):
|
||||
''' Endpoint: Library operations. '''
|
||||
queryset = m.LibraryItem.objects.all()
|
||||
# TODO: consider using .only() for performance
|
||||
|
||||
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
|
||||
filterset_fields = ['item_type', 'owner']
|
||||
ordering_fields = ('item_type', 'owner', 'alias', 'title', 'time_update')
|
||||
ordering = '-time_update'
|
||||
|
||||
def get_serializer_class(self):
|
||||
|
@ -52,8 +50,6 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
'set_owner',
|
||||
'set_access_policy',
|
||||
'set_location',
|
||||
'add_editor',
|
||||
'remove_editor',
|
||||
'set_editors'
|
||||
]:
|
||||
access_level = permissions.ItemOwner
|
||||
|
@ -129,7 +125,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
def subscribe(self, request: Request, pk):
|
||||
''' Endpoint: Subscribe current user to item. '''
|
||||
item = self._get_item()
|
||||
m.Subscription.subscribe(user=cast(User, self.request.user), item=item)
|
||||
m.Subscription.subscribe(user=cast(int, self.request.user.pk), item=item.pk)
|
||||
return Response(status=c.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
|
@ -146,7 +142,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
def unsubscribe(self, request: Request, pk):
|
||||
''' Endpoint: Unsubscribe current user from item. '''
|
||||
item = self._get_item()
|
||||
m.Subscription.unsubscribe(user=cast(User, self.request.user), item=item)
|
||||
m.Subscription.unsubscribe(user=cast(int, self.request.user.pk), item=item.pk)
|
||||
return Response(status=c.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
|
@ -165,29 +161,19 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
item = self._get_item()
|
||||
serializer = s.UserTargetSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
new_owner = serializer.validated_data['user']
|
||||
m.LibraryItem.objects.filter(pk=item.pk).update(owner=new_owner)
|
||||
return Response(status=c.HTTP_200_OK)
|
||||
new_owner = serializer.validated_data['user'].pk
|
||||
if new_owner == item.owner_id:
|
||||
return Response(status=c.HTTP_200_OK)
|
||||
|
||||
with transaction.atomic():
|
||||
if item.item_type == m.LibraryItemType.OPERATION_SCHEMA:
|
||||
owned_schemas = OperationSchema(item).owned_schemas().only('owner')
|
||||
for schema in owned_schemas:
|
||||
schema.owner_id = new_owner
|
||||
m.LibraryItem.objects.bulk_update(owned_schemas, ['owner'])
|
||||
item.owner_id = new_owner
|
||||
item.save(update_fields=['owner'])
|
||||
|
||||
@extend_schema(
|
||||
summary='set AccessPolicy for item',
|
||||
tags=['Library'],
|
||||
request=s.AccessPolicySerializer,
|
||||
responses={
|
||||
c.HTTP_200_OK: None,
|
||||
c.HTTP_400_BAD_REQUEST: None,
|
||||
c.HTTP_403_FORBIDDEN: None,
|
||||
c.HTTP_404_NOT_FOUND: None
|
||||
}
|
||||
)
|
||||
@action(detail=True, methods=['patch'], url_path='set-access-policy')
|
||||
def set_access_policy(self, request: Request, pk):
|
||||
''' Endpoint: Set item AccessPolicy. '''
|
||||
item = self._get_item()
|
||||
serializer = s.AccessPolicySerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
new_policy = serializer.validated_data['access_policy']
|
||||
m.LibraryItem.objects.filter(pk=item.pk).update(access_policy=new_policy)
|
||||
return Response(status=c.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
|
@ -208,49 +194,52 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
serializer = s.LocationSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
location: str = serializer.validated_data['location']
|
||||
if location == item.location:
|
||||
return Response(status=c.HTTP_200_OK)
|
||||
if location.startswith(m.LocationHead.LIBRARY) and not self.request.user.is_staff:
|
||||
return Response(status=c.HTTP_403_FORBIDDEN)
|
||||
m.LibraryItem.objects.filter(pk=item.pk).update(location=location)
|
||||
|
||||
with transaction.atomic():
|
||||
if item.item_type == m.LibraryItemType.OPERATION_SCHEMA:
|
||||
owned_schemas = OperationSchema(item).owned_schemas().only('location')
|
||||
for schema in owned_schemas:
|
||||
schema.location = location
|
||||
m.LibraryItem.objects.bulk_update(owned_schemas, ['location'])
|
||||
item.location = location
|
||||
item.save(update_fields=['location'])
|
||||
|
||||
return Response(status=c.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
summary='add editor for item',
|
||||
summary='set AccessPolicy for item',
|
||||
tags=['Library'],
|
||||
request=s.UserTargetSerializer,
|
||||
request=s.AccessPolicySerializer,
|
||||
responses={
|
||||
c.HTTP_200_OK: None,
|
||||
c.HTTP_400_BAD_REQUEST: None,
|
||||
c.HTTP_403_FORBIDDEN: None,
|
||||
c.HTTP_404_NOT_FOUND: None
|
||||
}
|
||||
)
|
||||
@action(detail=True, methods=['patch'], url_path='add-editor')
|
||||
def add_editor(self, request: Request, pk):
|
||||
''' Endpoint: Add editor for item. '''
|
||||
@action(detail=True, methods=['patch'], url_path='set-access-policy')
|
||||
def set_access_policy(self, request: Request, pk):
|
||||
''' Endpoint: Set item AccessPolicy. '''
|
||||
item = self._get_item()
|
||||
serializer = s.UserTargetSerializer(data=request.data)
|
||||
serializer = s.AccessPolicySerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
new_editor = serializer.validated_data['user']
|
||||
m.Editor.add(item=item, user=new_editor)
|
||||
return Response(status=c.HTTP_200_OK)
|
||||
new_policy = serializer.validated_data['access_policy']
|
||||
if new_policy == item.access_policy:
|
||||
return Response(status=c.HTTP_200_OK)
|
||||
|
||||
with transaction.atomic():
|
||||
if item.item_type == m.LibraryItemType.OPERATION_SCHEMA:
|
||||
owned_schemas = OperationSchema(item).owned_schemas().only('access_policy')
|
||||
for schema in owned_schemas:
|
||||
schema.access_policy = new_policy
|
||||
m.LibraryItem.objects.bulk_update(owned_schemas, ['access_policy'])
|
||||
item.access_policy = new_policy
|
||||
item.save(update_fields=['access_policy'])
|
||||
|
||||
@extend_schema(
|
||||
summary='remove editor for item',
|
||||
tags=['Library'],
|
||||
request=s.UserTargetSerializer,
|
||||
responses={
|
||||
c.HTTP_200_OK: None,
|
||||
c.HTTP_403_FORBIDDEN: None,
|
||||
c.HTTP_404_NOT_FOUND: None
|
||||
}
|
||||
)
|
||||
@action(detail=True, methods=['patch'], url_path='remove-editor')
|
||||
def remove_editor(self, request: Request, pk):
|
||||
''' Endpoint: Remove editor for item. '''
|
||||
item = self._get_item()
|
||||
serializer = s.UserTargetSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
editor = serializer.validated_data['user']
|
||||
m.Editor.remove(item=item, user=editor)
|
||||
return Response(status=c.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
|
@ -269,8 +258,32 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
item = self._get_item()
|
||||
serializer = s.UsersListSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
editors = serializer.validated_data['users']
|
||||
m.Editor.set(item=item, users=editors)
|
||||
editors: list[int] = request.data['users']
|
||||
|
||||
with transaction.atomic():
|
||||
added, deleted = m.Editor.set_and_return_diff(item.pk, editors)
|
||||
if len(added) >= 0 or len(deleted) >= 0:
|
||||
owned_schemas = OperationSchema(item).owned_schemas().only('pk')
|
||||
if owned_schemas.exists():
|
||||
m.Editor.objects.filter(
|
||||
item__in=owned_schemas,
|
||||
editor_id__in=deleted
|
||||
).delete()
|
||||
|
||||
existing_editors = m.Editor.objects.filter(
|
||||
item__in=owned_schemas,
|
||||
editor__in=added
|
||||
).values_list('item_id', 'editor_id')
|
||||
existing_editor_set = set(existing_editors)
|
||||
|
||||
new_editors = [
|
||||
m.Editor(item=schema, editor_id=user)
|
||||
for schema in owned_schemas
|
||||
for user in added
|
||||
if (item.id, user) not in existing_editor_set
|
||||
]
|
||||
m.Editor.objects.bulk_create(new_editors)
|
||||
|
||||
return Response(status=c.HTTP_200_OK)
|
||||
|
||||
|
||||
|
|
|
@ -25,6 +25,14 @@ class SynthesisSubstitutionAdmin(admin.ModelAdmin):
|
|||
search_fields = ['id', 'operation', 'original', 'substitution']
|
||||
|
||||
|
||||
class InheritanceAdmin(admin.ModelAdmin):
|
||||
''' Admin model: Inheritance. '''
|
||||
ordering = ['operation']
|
||||
list_display = ['id', 'operation', 'parent', 'child']
|
||||
search_fields = ['id', 'operation', 'parent', 'child']
|
||||
|
||||
|
||||
admin.site.register(models.Operation, OperationAdmin)
|
||||
admin.site.register(models.Argument, ArgumentAdmin)
|
||||
admin.site.register(models.Substitution, SynthesisSubstitutionAdmin)
|
||||
admin.site.register(models.Inheritance, InheritanceAdmin)
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 5.0.7 on 2024-08-02 07:01
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('oss', '0004_remove_substitution_transfer_term'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='inheritance',
|
||||
name='operation',
|
||||
field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, related_name='inheritances', to='oss.operation', verbose_name='Операция'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
|
@ -4,6 +4,12 @@ from django.db.models import CASCADE, ForeignKey, Model
|
|||
|
||||
class Inheritance(Model):
|
||||
''' Inheritance links parent and child constituents in synthesis operation.'''
|
||||
operation: ForeignKey = ForeignKey(
|
||||
verbose_name='Операция',
|
||||
to='oss.Operation',
|
||||
on_delete=CASCADE,
|
||||
related_name='inheritances'
|
||||
)
|
||||
parent: ForeignKey = ForeignKey(
|
||||
verbose_name='Исходная конституента',
|
||||
to='rsform.Constituenta',
|
||||
|
|
|
@ -51,6 +51,14 @@ class OperationSchema:
|
|||
''' Operation substitutions. '''
|
||||
return Substitution.objects.filter(operation__oss=self.model)
|
||||
|
||||
def owned_schemas(self) -> QuerySet[LibraryItem]:
|
||||
''' Get QuerySet containing all result schemas owned by current OSS. '''
|
||||
return LibraryItem.objects.filter(
|
||||
producer__oss=self.model,
|
||||
owner_id=self.model.owner_id,
|
||||
location=self.model.location
|
||||
)
|
||||
|
||||
def update_positions(self, data: list[dict]):
|
||||
''' Update positions. '''
|
||||
lookup = {x['id']: x for x in data}
|
||||
|
@ -161,7 +169,7 @@ class OperationSchema:
|
|||
access_policy=self.model.access_policy,
|
||||
location=self.model.location
|
||||
)
|
||||
Editor.set(schema.model, self.model.editors())
|
||||
Editor.set(schema.model.pk, self.model.editors().values_list('pk', flat=True))
|
||||
operation.result = schema.model
|
||||
operation.save()
|
||||
self.save()
|
||||
|
@ -197,6 +205,7 @@ class OperationSchema:
|
|||
parent = parents.get(cst.pk)
|
||||
assert parent is not None
|
||||
Inheritance.objects.create(
|
||||
operation=operation,
|
||||
child=cst,
|
||||
parent=parent
|
||||
)
|
||||
|
|
|
@ -48,7 +48,7 @@ class OperationCreateSerializer(serializers.Serializer):
|
|||
|
||||
create_schema = serializers.BooleanField(default=False, required=False)
|
||||
item_data = OperationCreateData()
|
||||
arguments = PKField(many=True, queryset=Operation.objects.all(), required=False)
|
||||
arguments = PKField(many=True, queryset=Operation.objects.all().only('pk'), required=False)
|
||||
|
||||
positions = serializers.ListField(
|
||||
child=OperationPositionSerializer(),
|
||||
|
@ -67,7 +67,7 @@ class OperationUpdateSerializer(serializers.Serializer):
|
|||
|
||||
target = PKField(many=False, queryset=Operation.objects.all())
|
||||
item_data = OperationUpdateData()
|
||||
arguments = PKField(many=True, queryset=Operation.objects.all(), required=False)
|
||||
arguments = PKField(many=True, queryset=Operation.objects.all().only('oss_id', 'result_id'), required=False)
|
||||
substitutions = serializers.ListField(
|
||||
child=SubstitutionSerializerBase(),
|
||||
required=False
|
||||
|
@ -121,8 +121,8 @@ class OperationUpdateSerializer(serializers.Serializer):
|
|||
|
||||
|
||||
class OperationTargetSerializer(serializers.Serializer):
|
||||
''' Serializer: Delete operation. '''
|
||||
target = PKField(many=False, queryset=Operation.objects.all())
|
||||
''' Serializer: Target single operation. '''
|
||||
target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'result_id'))
|
||||
positions = serializers.ListField(
|
||||
child=OperationPositionSerializer(),
|
||||
default=[]
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
''' Tests for Django Models. '''
|
||||
from .t_Argument import *
|
||||
from .t_Inheritance import *
|
||||
from .t_Operation import *
|
||||
from .t_Substitution import *
|
||||
|
|
51
rsconcept/backend/apps/oss/tests/s_models/t_Inheritance.py
Normal file
51
rsconcept/backend/apps/oss/tests/s_models/t_Inheritance.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
''' Testing models: Inheritance. '''
|
||||
from django.test import TestCase
|
||||
|
||||
from apps.oss.models import Inheritance, Operation, OperationSchema, OperationType
|
||||
from apps.rsform.models import Constituenta, RSForm
|
||||
|
||||
|
||||
class TestInheritance(TestCase):
|
||||
''' Testing Inheritance model. '''
|
||||
|
||||
def setUp(self):
|
||||
self.oss = OperationSchema.create(alias='T1')
|
||||
|
||||
self.ks1 = RSForm.create(title='Test1', alias='KS1')
|
||||
self.ks2 = RSForm.create(title='Test2', alias='KS2')
|
||||
self.operation = Operation.objects.create(
|
||||
oss=self.oss.model,
|
||||
alias='KS1',
|
||||
operation_type=OperationType.INPUT,
|
||||
result=self.ks1.model
|
||||
)
|
||||
self.ks1_x1 = self.ks1.insert_new('X1')
|
||||
self.ks2_x1 = self.ks2.insert_new('X1')
|
||||
self.inheritance = Inheritance.objects.create(
|
||||
operation=self.operation,
|
||||
parent=self.ks1_x1,
|
||||
child=self.ks2_x1
|
||||
)
|
||||
|
||||
|
||||
def test_str(self):
|
||||
testStr = f'{self.ks1_x1} -> {self.ks2_x1}'
|
||||
self.assertEqual(str(self.inheritance), testStr)
|
||||
|
||||
|
||||
def test_cascade_delete_operation(self):
|
||||
self.assertEqual(Inheritance.objects.count(), 1)
|
||||
self.operation.delete()
|
||||
self.assertEqual(Inheritance.objects.count(), 0)
|
||||
|
||||
|
||||
def test_cascade_delete_parent(self):
|
||||
self.assertEqual(Inheritance.objects.count(), 1)
|
||||
self.ks1_x1.delete()
|
||||
self.assertEqual(Inheritance.objects.count(), 0)
|
||||
|
||||
|
||||
def test_cascade_delete_child(self):
|
||||
self.assertEqual(Inheritance.objects.count(), 1)
|
||||
self.ks2_x1.delete()
|
||||
self.assertEqual(Inheritance.objects.count(), 0)
|
|
@ -1,2 +1,3 @@
|
|||
''' Tests for REST API. '''
|
||||
from .t_change_attributes import *
|
||||
from .t_oss import *
|
||||
|
|
125
rsconcept/backend/apps/oss/tests/s_views/t_change_attributes.py
Normal file
125
rsconcept/backend/apps/oss/tests/s_views/t_change_attributes.py
Normal file
|
@ -0,0 +1,125 @@
|
|||
''' Testing API: Change attributes of OSS and RSForms. '''
|
||||
|
||||
from rest_framework import status
|
||||
|
||||
from apps.library.models import AccessPolicy, Editor, LocationHead
|
||||
from apps.oss.models import Operation, OperationSchema, OperationType
|
||||
from apps.rsform.models import RSForm
|
||||
from apps.users.models import User
|
||||
from shared.EndpointTester import EndpointTester, decl_endpoint
|
||||
|
||||
|
||||
class TestChangeAttributes(EndpointTester):
|
||||
''' Testing LibraryItem view when OSS is associated with RSForms. '''
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user3 = User.objects.create(
|
||||
username='UserTest3',
|
||||
email='anotheranother@test.com',
|
||||
password='password'
|
||||
)
|
||||
|
||||
self.owned = OperationSchema.create(
|
||||
title='Test',
|
||||
alias='T1',
|
||||
owner=self.user,
|
||||
location=LocationHead.LIBRARY
|
||||
)
|
||||
self.owned_id = self.owned.model.pk
|
||||
|
||||
self.ks1 = RSForm.create(
|
||||
alias='KS1',
|
||||
title='Test1',
|
||||
owner=self.user,
|
||||
location=LocationHead.USER
|
||||
)
|
||||
self.ks2 = RSForm.create(
|
||||
alias='KS2',
|
||||
title='Test2',
|
||||
owner=self.user2,
|
||||
location=LocationHead.LIBRARY
|
||||
)
|
||||
|
||||
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.owned.execute_operation(self.operation3)
|
||||
self.operation3.refresh_from_db()
|
||||
self.ks3 = self.operation3.result
|
||||
|
||||
|
||||
@decl_endpoint('/api/library/{item}/set-owner', method='patch')
|
||||
def test_set_owner(self):
|
||||
data = {'user': self.user3.pk}
|
||||
|
||||
self.executeOK(data=data, item=self.owned_id)
|
||||
|
||||
self.owned.refresh_from_db()
|
||||
self.ks1.refresh_from_db()
|
||||
self.ks2.refresh_from_db()
|
||||
self.ks3.refresh_from_db()
|
||||
self.assertEqual(self.owned.model.owner, self.user3)
|
||||
self.assertEqual(self.ks1.model.owner, self.user)
|
||||
self.assertEqual(self.ks2.model.owner, self.user2)
|
||||
self.assertEqual(self.ks3.owner, self.user3)
|
||||
|
||||
@decl_endpoint('/api/library/{item}/set-location', method='patch')
|
||||
def test_set_location(self):
|
||||
data = {'location': '/U/temp'}
|
||||
|
||||
self.executeOK(data=data, item=self.owned_id)
|
||||
|
||||
self.owned.refresh_from_db()
|
||||
self.ks1.refresh_from_db()
|
||||
self.ks2.refresh_from_db()
|
||||
self.ks3.refresh_from_db()
|
||||
self.assertEqual(self.owned.model.location, data['location'])
|
||||
self.assertNotEqual(self.ks1.model.location, data['location'])
|
||||
self.assertNotEqual(self.ks2.model.location, data['location'])
|
||||
self.assertEqual(self.ks3.location, data['location'])
|
||||
|
||||
@decl_endpoint('/api/library/{item}/set-access-policy', method='patch')
|
||||
def test_set_access_policy(self):
|
||||
data = {'access_policy': AccessPolicy.PROTECTED}
|
||||
|
||||
self.executeOK(data=data, item=self.owned_id)
|
||||
|
||||
self.owned.refresh_from_db()
|
||||
self.ks1.refresh_from_db()
|
||||
self.ks2.refresh_from_db()
|
||||
self.ks3.refresh_from_db()
|
||||
self.assertEqual(self.owned.model.access_policy, data['access_policy'])
|
||||
self.assertNotEqual(self.ks1.model.access_policy, data['access_policy'])
|
||||
self.assertNotEqual(self.ks2.model.access_policy, data['access_policy'])
|
||||
self.assertEqual(self.ks3.access_policy, data['access_policy'])
|
||||
|
||||
@decl_endpoint('/api/library/{item}/set-editors', method='patch')
|
||||
def test_set_editors(self):
|
||||
Editor.set(self.owned.model.pk, [self.user2.pk])
|
||||
Editor.set(self.ks1.model.pk, [self.user2.pk, self.user.pk])
|
||||
Editor.set(self.ks3.pk, [self.user2.pk, self.user.pk])
|
||||
data = {'users': [self.user3.pk]}
|
||||
|
||||
self.executeOK(data=data, item=self.owned_id)
|
||||
|
||||
self.owned.refresh_from_db()
|
||||
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.editors()), set([self.user, self.user3]))
|
|
@ -220,7 +220,7 @@ class TestOssViewset(EndpointTester):
|
|||
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
|
||||
def test_create_operation_schema(self):
|
||||
self.populateData()
|
||||
Editor.add(self.owned.model, self.user2)
|
||||
Editor.add(self.owned.model.pk, self.user2.pk)
|
||||
data = {
|
||||
'item_data': {
|
||||
'alias': 'Test4',
|
||||
|
|
|
@ -269,7 +269,7 @@ class CstRenameSerializer(serializers.Serializer):
|
|||
|
||||
class CstListSerializer(serializers.Serializer):
|
||||
''' Serializer: List of constituents from one origin. '''
|
||||
items = PKField(many=True, queryset=Constituenta.objects.all())
|
||||
items = PKField(many=True, queryset=Constituenta.objects.all().only('schema_id'))
|
||||
|
||||
def validate(self, attrs):
|
||||
schema = cast(LibraryItem, self.context['schema'])
|
||||
|
@ -291,8 +291,8 @@ class CstMoveSerializer(CstListSerializer):
|
|||
|
||||
class SubstitutionSerializerBase(serializers.Serializer):
|
||||
''' Serializer: Basic substitution. '''
|
||||
original = PKField(many=False, queryset=Constituenta.objects.only('alias', 'schema'))
|
||||
substitution = PKField(many=False, queryset=Constituenta.objects.only('alias', 'schema'))
|
||||
original = PKField(many=False, queryset=Constituenta.objects.only('alias', 'schema_id'))
|
||||
substitution = PKField(many=False, queryset=Constituenta.objects.only('alias', 'schema_id'))
|
||||
|
||||
|
||||
class CstSubstituteSerializer(serializers.Serializer):
|
||||
|
@ -330,8 +330,8 @@ class CstSubstituteSerializer(serializers.Serializer):
|
|||
|
||||
class InlineSynthesisSerializer(serializers.Serializer):
|
||||
''' Serializer: Inline synthesis operation input. '''
|
||||
receiver = PKField(many=False, queryset=LibraryItem.objects.all())
|
||||
source = PKField(many=False, queryset=LibraryItem.objects.all()) # type: ignore
|
||||
receiver = PKField(many=False, queryset=LibraryItem.objects.all().only('owner_id'))
|
||||
source = PKField(many=False, queryset=LibraryItem.objects.all().only('owner_id')) # type: ignore
|
||||
items = PKField(many=True, queryset=Constituenta.objects.all())
|
||||
substitutions = serializers.ListField(
|
||||
child=SubstitutionSerializerBase()
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -250,9 +250,8 @@ LOGGING = {
|
|||
'root': {
|
||||
'handlers': ['console'],
|
||||
'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO')
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == 'test':
|
||||
logging.disable(logging.CRITICAL)
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
''' Utils: base tester class for endpoints. '''
|
||||
import logging
|
||||
|
||||
from django.db import connection
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient, APIRequestFactory, APITestCase
|
||||
|
||||
|
@ -40,6 +43,9 @@ class EndpointTester(APITestCase):
|
|||
self.client = APIClient()
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
self.logger = logging.getLogger('django.db.backends')
|
||||
self.logger.setLevel(logging.DEBUG)
|
||||
|
||||
def setUpFullUsers(self):
|
||||
self.factory = APIRequestFactory()
|
||||
self.user = User.objects.create_user(
|
||||
|
@ -61,9 +67,9 @@ class EndpointTester(APITestCase):
|
|||
|
||||
def toggle_editor(self, item: LibraryItem, value: bool = True):
|
||||
if value:
|
||||
Editor.add(item, self.user)
|
||||
Editor.add(item.pk, self.user.pk)
|
||||
else:
|
||||
Editor.remove(item, self.user)
|
||||
Editor.remove(item.pk, self.user.pk)
|
||||
|
||||
def login(self):
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
@ -71,6 +77,16 @@ class EndpointTester(APITestCase):
|
|||
def logout(self):
|
||||
self.client.logout()
|
||||
|
||||
def start_db_log(self):
|
||||
''' Warning! Do not use this second time before calling stop_db_log. '''
|
||||
''' Warning! Do not forget to enable global logging in settings. '''
|
||||
logging.disable(logging.NOTSET)
|
||||
connection.force_debug_cursor = True
|
||||
|
||||
def stop_db_log(self):
|
||||
connection.force_debug_cursor = False
|
||||
logging.disable(logging.CRITICAL)
|
||||
|
||||
def set_params(self, **kwargs):
|
||||
''' Given named argument values resolve current endpoint_mask. '''
|
||||
if self.endpoint_mask and len(kwargs) > 0:
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
**/parser.ts
|
||||
**/node_modules/**
|
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended-type-checked",
|
||||
"plugin:react-hooks/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module",
|
||||
"project": ["tsconfig.json", "tsconfig.node.json"]
|
||||
},
|
||||
"plugins": ["react-refresh", "simple-import-sort", "eslint-plugin-tsdoc"],
|
||||
"rules": {
|
||||
"no-console": "off",
|
||||
"require-jsdoc": "off",
|
||||
"react-refresh/only-export-components": ["off", { "allowConstantExport": true }],
|
||||
"simple-import-sort/imports": "warn",
|
||||
"tsdoc/syntax": "warn"
|
||||
}
|
||||
}
|
|
@ -10,4 +10,4 @@
|
|||
"quoteProps": "consistent",
|
||||
"bracketSameLine": false,
|
||||
"bracketSpacing": true
|
||||
}
|
||||
}
|
||||
|
|
61
rsconcept/frontend/eslint.config.js
Normal file
61
rsconcept/frontend/eslint.config.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
import globals from 'globals';
|
||||
import typescriptPlugin from 'typescript-eslint';
|
||||
import typescriptParser from '@typescript-eslint/parser';
|
||||
import reactPlugin from 'eslint-plugin-react';
|
||||
// import { fixupPluginRules } from '@eslint/compat';
|
||||
// import reactHooksPlugin from 'eslint-plugin-react-hooks';
|
||||
|
||||
import simpleImportSort from 'eslint-plugin-simple-import-sort';
|
||||
|
||||
export default [
|
||||
...typescriptPlugin.configs.recommendedTypeChecked,
|
||||
...typescriptPlugin.configs.stylisticTypeChecked,
|
||||
{
|
||||
ignores: ['**/parser.ts', '**/node_modules/**', '**/public/**', 'eslint.config.js']
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
parser: typescriptParser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
globals: { ...globals.browser, ...globals.es2020, ...globals.jest },
|
||||
project: ['./tsconfig.json', './tsconfig.node.json']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
'react': reactPlugin,
|
||||
// 'react-hooks': fixupPluginRules(reactHooksPlugin),
|
||||
'simple-import-sort': simpleImportSort
|
||||
},
|
||||
settings: { react: { version: 'detect' } },
|
||||
rules: {
|
||||
'@typescript-eslint/no-empty-object-type': ['error', { allowInterfaces: 'with-single-extends' }],
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'off',
|
||||
'@typescript-eslint/no-inferrable-types': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
destructuredArrayIgnorePattern: '^_'
|
||||
}
|
||||
],
|
||||
|
||||
'react-refresh/only-export-components': ['off', { allowConstantExport: true }],
|
||||
|
||||
'simple-import-sort/imports': 'warn',
|
||||
'simple-import-sort/exports': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
'require-jsdoc': 'off'
|
||||
}
|
||||
}
|
||||
];
|
2426
rsconcept/frontend/package-lock.json
generated
2426
rsconcept/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -8,17 +8,17 @@
|
|||
"test": "jest",
|
||||
"dev": "vite --host",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lezer/lr": "^1.4.2",
|
||||
"@tanstack/react-table": "^8.19.3",
|
||||
"@tanstack/react-table": "^8.20.1",
|
||||
"@uiw/codemirror-themes": "^4.23.0",
|
||||
"@uiw/react-codemirror": "^4.23.0",
|
||||
"axios": "^1.7.2",
|
||||
"axios": "^1.7.3",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^11.3.19",
|
||||
"framer-motion": "^11.3.21",
|
||||
"html-to-image": "^1.11.11",
|
||||
"js-file-download": "^0.4.12",
|
||||
"react": "^18.3.1",
|
||||
|
@ -28,11 +28,11 @@
|
|||
"react-intl": "^6.6.8",
|
||||
"react-loader-spinner": "^6.1.6",
|
||||
"react-pdf": "^9.1.0",
|
||||
"react-router-dom": "^6.25.1",
|
||||
"react-router-dom": "^6.26.0",
|
||||
"react-select": "^5.8.0",
|
||||
"react-tabs": "^6.0.2",
|
||||
"react-toastify": "^10.0.5",
|
||||
"react-tooltip": "^5.27.1",
|
||||
"react-tooltip": "^5.28.0",
|
||||
"react-zoom-pan-pinch": "^3.6.1",
|
||||
"reactflow": "^11.11.4",
|
||||
"reagraph": "^4.19.2",
|
||||
|
@ -41,23 +41,23 @@
|
|||
"devDependencies": {
|
||||
"@lezer/generator": "^1.7.1",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^20.14.13",
|
||||
"@types/node": "^22.1.0",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
||||
"@typescript-eslint/parser": "^8.0.1",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-react-refresh": "^0.4.9",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.8.0",
|
||||
"eslint-plugin-react": "^7.35.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-tsdoc": "^0.3.0",
|
||||
"globals": "^15.9.0",
|
||||
"jest": "^29.7.0",
|
||||
"postcss": "^8.4.40",
|
||||
"tailwindcss": "^3.4.7",
|
||||
"ts-jest": "^29.2.4",
|
||||
"typescript": "^5.5.4",
|
||||
"typescript-eslint": "^8.0.1",
|
||||
"vite": "^5.3.5"
|
||||
},
|
||||
"jest": {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 122 KiB |
|
@ -1,6 +1,7 @@
|
|||
// Search new icons at https://reactsvgicons.com/
|
||||
// Note: save this file using Ctrl + K, Ctrl + Shift + S to disable autoformat
|
||||
|
||||
/* eslint-disable simple-import-sort/exports */
|
||||
// ==== General actions =======
|
||||
export { BiMenu as IconMenu } from 'react-icons/bi';
|
||||
export { LuLogOut as IconLogout } from 'react-icons/lu';
|
||||
|
@ -61,11 +62,11 @@ export { TbBriefcase as IconBusiness } from 'react-icons/tb';
|
|||
export { VscLibrary as IconLibrary } from 'react-icons/vsc';
|
||||
export { IoLibrary as IconLibrary2 } from 'react-icons/io5';
|
||||
export { BiDiamond as IconTemplates } from 'react-icons/bi';
|
||||
export { GiHoneycomb as IconOSS } from 'react-icons/gi';
|
||||
export { LuBaby as IconChild } from 'react-icons/lu';
|
||||
export { TbHexagons as IconOSS } from 'react-icons/tb';
|
||||
export { TbHexagon as IconRSForm } from 'react-icons/tb';
|
||||
export { GrInherit as IconChild } from 'react-icons/gr';
|
||||
export { RiParentLine as IconParent } from 'react-icons/ri';
|
||||
export { BiSpa as IconPredecessor } from 'react-icons/bi';
|
||||
export { RiHexagonLine as IconRSForm } from 'react-icons/ri';
|
||||
export { LuArchive as IconArchive } from 'react-icons/lu';
|
||||
export { LuDatabase as IconDatabase } from 'react-icons/lu';
|
||||
export { LuView as IconDBStructure } from 'react-icons/lu';
|
||||
|
@ -99,6 +100,7 @@ export { BiDownvote as IconMoveDown } from 'react-icons/bi';
|
|||
export { BiRightArrow as IconMoveRight } from 'react-icons/bi';
|
||||
export { BiLeftArrow as IconMoveLeft } from 'react-icons/bi';
|
||||
export { FiBell as IconFollow } from 'react-icons/fi';
|
||||
export { TbHexagonPlus2 as IconNewRSForm } from 'react-icons/tb';
|
||||
export { FiBellOff as IconFollowOff } from 'react-icons/fi';
|
||||
export { BiPlusCircle as IconNewItem } from 'react-icons/bi';
|
||||
export { FaSquarePlus as IconNewItem2 } from 'react-icons/fa6';
|
||||
|
|
|
@ -14,7 +14,7 @@ function InfoConstituenta({ data, className, ...restProps }: InfoConstituentaPro
|
|||
return (
|
||||
<div className={clsx('dense min-w-[15rem]', className)} {...restProps}>
|
||||
<h2>
|
||||
Конституента {data.alias}
|
||||
{data.alias}
|
||||
{data.is_inherited ? ' (наследуется)' : ''}
|
||||
</h2>
|
||||
{data.term_resolved ? (
|
||||
|
|
12
rsconcept/frontend/src/components/props.d.ts
vendored
12
rsconcept/frontend/src/components/props.d.ts
vendored
|
@ -2,11 +2,11 @@
|
|||
import { HTMLMotionProps } from 'framer-motion';
|
||||
|
||||
export namespace CProps {
|
||||
export type Titled = {
|
||||
export interface Titled {
|
||||
title?: string;
|
||||
titleHtml?: string;
|
||||
hideTitle?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export type Control = Titled & {
|
||||
disabled?: boolean;
|
||||
|
@ -14,18 +14,18 @@ export namespace CProps {
|
|||
noOutline?: boolean;
|
||||
};
|
||||
|
||||
export type Styling = {
|
||||
export interface Styling {
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Editor = Control & {
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export type Colors = {
|
||||
export interface Colors {
|
||||
colors?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Div = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
|
||||
export type Button = Titled &
|
||||
|
|
|
@ -17,25 +17,36 @@ interface MiniSelectorOSSProps {
|
|||
|
||||
function MiniSelectorOSS({ items, onSelect }: MiniSelectorOSSProps) {
|
||||
const ossMenu = useDropdown();
|
||||
|
||||
function onToggle(event: CProps.EventMouse) {
|
||||
if (items.length > 1) {
|
||||
ossMenu.toggle();
|
||||
} else {
|
||||
onSelect(event, items[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={ossMenu.ref} className='flex items-center'>
|
||||
<MiniButton
|
||||
icon={<IconOSS size='1.25rem' className='icon-primary' />}
|
||||
title='Связанные операционные схемы'
|
||||
title='Операционные схемы'
|
||||
hideTitle={ossMenu.isOpen}
|
||||
onClick={() => ossMenu.toggle()}
|
||||
onClick={onToggle}
|
||||
/>
|
||||
<Dropdown isOpen={ossMenu.isOpen}>
|
||||
<Label text='Список ОСС' className='border-b px-3 py-1' />
|
||||
{items.map((reference, index) => (
|
||||
<DropdownButton
|
||||
className='min-w-[5rem]'
|
||||
key={`${prefixes.oss_list}${index}`}
|
||||
text={reference.alias}
|
||||
onClick={event => onSelect(event, reference)}
|
||||
/>
|
||||
))}
|
||||
</Dropdown>
|
||||
{items.length > 1 ? (
|
||||
<Dropdown isOpen={ossMenu.isOpen}>
|
||||
<Label text='Список ОСС' className='border-b px-3 py-1' />
|
||||
{items.map((reference, index) => (
|
||||
<DropdownButton
|
||||
className='min-w-[5rem]'
|
||||
key={`${prefixes.oss_list}${index}`}
|
||||
text={reference.alias}
|
||||
onClick={event => onSelect(event, reference)}
|
||||
/>
|
||||
))}
|
||||
</Dropdown>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -140,13 +140,11 @@ function PickSubstitutions({
|
|||
() => [
|
||||
columnHelper.accessor(item => item.substitution_source?.alias ?? 'N/A', {
|
||||
id: 'left_schema',
|
||||
header: 'Операция',
|
||||
size: 100,
|
||||
cell: props => <div className='min-w-[10.5rem] text-ellipsis text-right'>{props.getValue()}</div>
|
||||
cell: props => <div className='min-w-[10.5rem] text-ellipsis text-left'>{props.getValue()}</div>
|
||||
}),
|
||||
columnHelper.accessor(item => item.substitution?.alias ?? 'N/A', {
|
||||
id: 'left_alias',
|
||||
header: () => <span className='pl-3'>Имя</span>,
|
||||
size: 65,
|
||||
cell: props =>
|
||||
props.row.original.substitution ? (
|
||||
|
@ -157,13 +155,11 @@ function PickSubstitutions({
|
|||
}),
|
||||
columnHelper.display({
|
||||
id: 'status',
|
||||
header: '',
|
||||
size: 40,
|
||||
cell: () => <IconPageRight size='1.2rem' />
|
||||
}),
|
||||
columnHelper.accessor(item => item.original?.alias ?? 'N/A', {
|
||||
id: 'right_alias',
|
||||
header: () => <span className='pl-3'>Имя</span>,
|
||||
size: 65,
|
||||
cell: props =>
|
||||
props.row.original.original ? (
|
||||
|
@ -174,9 +170,8 @@ function PickSubstitutions({
|
|||
}),
|
||||
columnHelper.accessor(item => item.original_source?.alias ?? 'N/A', {
|
||||
id: 'right_schema',
|
||||
header: 'Операция',
|
||||
size: 100,
|
||||
cell: props => <div className='min-w-[8rem] text-ellipsis'>{props.getValue()}</div>
|
||||
cell: props => <div className='min-w-[8rem] text-ellipsis text-right'>{props.getValue()}</div>
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: 'actions',
|
||||
|
|
|
@ -25,7 +25,7 @@ function SelectLocation({ value, folderTree, dense, prefix, onClick, className,
|
|||
const [folded, setFolded] = useState<FolderNode[]>(items);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setFolded(items.filter(item => item !== activeNode && (!activeNode || !activeNode.hasPredecessor(item))));
|
||||
setFolded(items.filter(item => item !== activeNode && !activeNode?.hasPredecessor(item)));
|
||||
}, [items, activeNode]);
|
||||
|
||||
const onFoldItem = useCallback(
|
||||
|
|
|
@ -51,7 +51,7 @@ function SelectUser({
|
|||
options={options}
|
||||
value={value ? { value: value, label: getUserLabel(value) } : null}
|
||||
onChange={data => {
|
||||
if (data !== null && data.value !== undefined) onSelectValue(data.value);
|
||||
if (data?.value !== undefined) onSelectValue(data.value);
|
||||
}}
|
||||
// @ts-expect-error: TODO: use type definitions from react-select in filter object
|
||||
filterOption={filter}
|
||||
|
|
|
@ -23,7 +23,7 @@ import TableBody from './TableBody';
|
|||
import TableFooter from './TableFooter';
|
||||
import TableHeader from './TableHeader';
|
||||
|
||||
export { createColumnHelper, type ColumnSort, type RowSelectionState, type VisibilityState };
|
||||
export { type ColumnSort, createColumnHelper, type RowSelectionState, type VisibilityState };
|
||||
|
||||
export interface IConditionalStyle<TData> {
|
||||
when: (rowData: TData) => boolean;
|
||||
|
|
|
@ -44,7 +44,7 @@ function TableBody<TData>({
|
|||
lastIndex > currentIndex ? currentIndex : lastIndex + 1,
|
||||
lastIndex > currentIndex ? lastIndex : currentIndex + 1
|
||||
);
|
||||
const newSelection: { [key: string]: boolean } = {};
|
||||
const newSelection: Record<string, boolean> = {};
|
||||
toggleRows.forEach(row => {
|
||||
newSelection[row.id] = !target.getIsSelected();
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
export {
|
||||
default,
|
||||
createColumnHelper,
|
||||
default,
|
||||
type IConditionalStyle,
|
||||
type RowSelectionState,
|
||||
type VisibilityState
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
import { GraphCanvas as GraphUI } from 'reagraph';
|
||||
|
||||
export {
|
||||
type CollapseProps,
|
||||
type GraphCanvasRef,
|
||||
type GraphEdge,
|
||||
type GraphNode,
|
||||
type GraphCanvasRef,
|
||||
Sphere,
|
||||
useSelection,
|
||||
type CollapseProps
|
||||
useSelection
|
||||
} from 'reagraph';
|
||||
export { type LayoutTypes as GraphLayout } from 'reagraph';
|
||||
|
||||
|
|
|
@ -274,7 +274,7 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
|
|||
onError: setProcessingError,
|
||||
onSuccess: () =>
|
||||
reloadItems(() => {
|
||||
if (user && user.subscriptions.includes(target)) {
|
||||
if (user?.subscriptions.includes(target)) {
|
||||
user.subscriptions.splice(
|
||||
user.subscriptions.findIndex(item => item === target),
|
||||
1
|
||||
|
|
|
@ -102,7 +102,6 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
|
|||
return false;
|
||||
}
|
||||
return schema.subscribers.includes(user.id);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [user, schema, toggleTracking]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -143,7 +143,6 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
|
|||
return false;
|
||||
}
|
||||
return schema.subscribers.includes(user.id);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [user, schema, toggleTracking]);
|
||||
|
||||
const update = useCallback(
|
||||
|
|
|
@ -118,7 +118,7 @@ function DlgCreateOperation({ hideWindow, oss, onCreate, initialInputs }: DlgCre
|
|||
/>
|
||||
</TabPanel>
|
||||
),
|
||||
[alias, comment, title, attachedID, oss, createSchema]
|
||||
[alias, comment, title, attachedID, oss, createSchema, setAlias]
|
||||
);
|
||||
|
||||
const synthesisPanel = useMemo(
|
||||
|
@ -137,7 +137,7 @@ function DlgCreateOperation({ hideWindow, oss, onCreate, initialInputs }: DlgCre
|
|||
/>
|
||||
</TabPanel>
|
||||
),
|
||||
[oss, alias, comment, title, inputs]
|
||||
[oss, alias, comment, title, inputs, setAlias]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -61,7 +61,6 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
|
|||
|
||||
useEffect(() => {
|
||||
cache.preload(schemasIDs);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [schemasIDs]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
|
@ -92,7 +91,7 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
|
|||
/>
|
||||
</TabPanel>
|
||||
),
|
||||
[alias, comment, title]
|
||||
[alias, comment, title, setAlias]
|
||||
);
|
||||
|
||||
const argumentsPanel = useMemo(
|
||||
|
@ -106,7 +105,7 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
|
|||
/>
|
||||
</TabPanel>
|
||||
),
|
||||
[oss, target, inputs]
|
||||
[oss, target, inputs, setInputs]
|
||||
);
|
||||
|
||||
const synthesisPanel = useMemo(
|
||||
|
|
|
@ -73,7 +73,6 @@ function useRSFormCache() {
|
|||
}
|
||||
})
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pending]);
|
||||
|
||||
return { preload, getSchema, getConstituenta, getSchemaByCst, loading, error, setError };
|
||||
|
|
|
@ -26,7 +26,6 @@ function useWindowSize() {
|
|||
}
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return windowSize;
|
||||
|
|
|
@ -72,7 +72,7 @@ export class FolderNode {
|
|||
*
|
||||
*/
|
||||
export class FolderTree {
|
||||
roots: Map<string, FolderNode> = new Map();
|
||||
roots = new Map<string, FolderNode>();
|
||||
|
||||
constructor(arr?: string[]) {
|
||||
arr?.forEach(path => this.addPath(path));
|
||||
|
|
|
@ -48,7 +48,7 @@ export class GraphNode {
|
|||
* This class is optimized for TermGraph use case and not supposed to be used as generic graph implementation.
|
||||
*/
|
||||
export class Graph {
|
||||
nodes: Map<number, GraphNode> = new Map();
|
||||
nodes = new Map<number, GraphNode>();
|
||||
|
||||
constructor(arr?: number[][]) {
|
||||
if (!arr) {
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
export class OssLoader {
|
||||
private oss: IOperationSchemaData;
|
||||
private graph: Graph = new Graph();
|
||||
private operationByID: Map<OperationID, IOperation> = new Map();
|
||||
private operationByID = new Map<OperationID, IOperation>();
|
||||
private schemas: LibraryItemID[] = [];
|
||||
|
||||
constructor(input: IOperationSchemaData) {
|
||||
|
@ -53,7 +53,7 @@ export class OssLoader {
|
|||
}
|
||||
|
||||
private extractSchemas() {
|
||||
this.schemas = this.oss.items.map(operation => operation.result as LibraryItemID).filter(item => item !== null);
|
||||
this.schemas = this.oss.items.map(operation => operation.result).filter(item => item !== null);
|
||||
}
|
||||
|
||||
private inferOperationAttributes() {
|
||||
|
|
|
@ -18,8 +18,8 @@ import { extractGlobals, isSimpleExpression, splitTemplateDefinition } from './r
|
|||
export class RSFormLoader {
|
||||
private schema: IRSFormData;
|
||||
private graph: Graph = new Graph();
|
||||
private cstByAlias: Map<string, IConstituenta> = new Map();
|
||||
private cstByID: Map<ConstituentaID, IConstituenta> = new Map();
|
||||
private cstByAlias = new Map<string, IConstituenta>();
|
||||
private cstByID = new Map<ConstituentaID, IConstituenta>();
|
||||
|
||||
constructor(input: IRSFormData) {
|
||||
this.schema = input;
|
||||
|
@ -116,7 +116,7 @@ export class RSFormLoader {
|
|||
}
|
||||
|
||||
private extractSources(target: IConstituenta): Set<ConstituentaID> {
|
||||
const sources: Set<ConstituentaID> = new Set();
|
||||
const sources = new Set<ConstituentaID>();
|
||||
if (!isFunctional(target.cst_type)) {
|
||||
const node = this.graph.at(target.id)!;
|
||||
node.inputs.forEach(id => {
|
||||
|
|
|
@ -103,6 +103,7 @@ export interface ILibraryItemEditor {
|
|||
|
||||
isMutable: boolean;
|
||||
isProcessing: boolean;
|
||||
isAttachedToOSS: boolean;
|
||||
|
||||
setOwner: (newOwner: UserID) => void;
|
||||
setAccessPolicy: (newPolicy: AccessPolicy) => void;
|
||||
|
|
|
@ -108,7 +108,7 @@ export enum HelpTopic {
|
|||
/**
|
||||
* Manual topics hierarchy.
|
||||
*/
|
||||
export const topicParent: Map<HelpTopic, HelpTopic> = new Map([
|
||||
export const topicParent = new Map<HelpTopic, HelpTopic>([
|
||||
[HelpTopic.MAIN, HelpTopic.MAIN],
|
||||
|
||||
[HelpTopic.INTERFACE, HelpTopic.INTERFACE],
|
||||
|
|
|
@ -254,7 +254,7 @@ export function isFunctional(type: CstType): boolean {
|
|||
* Validate new alias against {@link CstType} and {@link IRSForm}.
|
||||
*/
|
||||
export function validateNewAlias(alias: string, type: CstType, schema: IRSForm): boolean {
|
||||
return alias.length >= 2 && alias[0] == getCstTypePrefix(type) && !schema.cstByAlias.has(alias);
|
||||
return alias.length >= 2 && alias.startsWith(getCstTypePrefix(type)) && !schema.cstByAlias.has(alias);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -91,7 +91,7 @@ export function substituteTemplateArgs(expression: string, args: IArgumentValue[
|
|||
return expression;
|
||||
}
|
||||
|
||||
const mapping: { [key: string]: string } = {};
|
||||
const mapping: Record<string, string> = {};
|
||||
args
|
||||
.filter(arg => !!arg.value)
|
||||
.forEach(arg => {
|
||||
|
|
|
@ -46,7 +46,7 @@ function FormCreateItem() {
|
|||
|
||||
const location = useMemo(() => combineLocation(head, body), [head, body]);
|
||||
const isValid = useMemo(() => validateLocation(location), [location]);
|
||||
const [initLocation] = useLocalStorage<string>(storage.librarySearchLocation, '');
|
||||
const [initLocation, setInitLocation] = useLocalStorage<string>(storage.librarySearchLocation, '');
|
||||
|
||||
const [fileName, setFileName] = useState('');
|
||||
const [file, setFile] = useState<File | undefined>();
|
||||
|
@ -81,6 +81,7 @@ function FormCreateItem() {
|
|||
file: file,
|
||||
fileName: file?.name
|
||||
};
|
||||
setInitLocation(location);
|
||||
createItem(data, newItem => {
|
||||
toast.success(information.newLibraryItem);
|
||||
if (itemType == LibraryItemType.RSFORM) {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
|
||||
import LinkTopic from '@/components/ui/LinkTopic';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
|
||||
function HelpCstAttributes() {
|
||||
return (
|
||||
|
|
|
@ -1,15 +1,19 @@
|
|||
import {
|
||||
IconChild,
|
||||
IconClone,
|
||||
IconControls,
|
||||
IconDestroy,
|
||||
IconEdit,
|
||||
IconFilter,
|
||||
IconList,
|
||||
IconMoveDown,
|
||||
IconMoveUp,
|
||||
IconNewItem,
|
||||
IconOSS,
|
||||
IconPredecessor,
|
||||
IconReset,
|
||||
IconSave,
|
||||
IconSettings,
|
||||
IconStatusOK,
|
||||
IconText,
|
||||
IconTree
|
||||
|
@ -23,37 +27,64 @@ function HelpCstEditor() {
|
|||
return (
|
||||
<div className='dense'>
|
||||
<h1>Редактор конституенты</h1>
|
||||
<li>
|
||||
<IconOSS className='inline-icon' /> переход к связанной <LinkTopic text='ОСС' topic={HelpTopic.CC_OSS} />
|
||||
</li>
|
||||
<li>
|
||||
<IconSave className='inline-icon' /> сохранить изменения: Ctrl + S
|
||||
</li>
|
||||
<li>
|
||||
<IconReset className='inline-icon' /> сбросить несохраненные изменения
|
||||
</li>
|
||||
<li>
|
||||
<IconClone className='inline-icon icon-green' /> клонировать текущую: Alt + V
|
||||
</li>
|
||||
<li>
|
||||
<IconNewItem className='inline-icon icon-green' /> новая конституента
|
||||
</li>
|
||||
<li>
|
||||
<IconDestroy className='inline-icon icon-red' /> удаление текущей
|
||||
</li>
|
||||
<div className='flex flex-col sm:flex-row sm:gap-3'>
|
||||
<div className='flex flex-col'>
|
||||
<li>
|
||||
<IconOSS className='inline-icon' /> переход к <LinkTopic text='ОСС' topic={HelpTopic.CC_OSS} />
|
||||
</li>
|
||||
<li>
|
||||
<IconPredecessor className='inline-icon' /> переход к исходной
|
||||
</li>
|
||||
<li>
|
||||
<IconList className='inline-icon' /> список конституент
|
||||
</li>
|
||||
<li>
|
||||
<IconSave className='inline-icon' /> сохранить: Ctrl + S
|
||||
</li>
|
||||
<li>
|
||||
<IconReset className='inline-icon' /> сбросить изменения
|
||||
</li>
|
||||
<li>
|
||||
<IconClone className='inline-icon icon-green' /> клонировать: Alt + V
|
||||
</li>
|
||||
<li>
|
||||
<IconNewItem className='inline-icon icon-green' /> новая конституента
|
||||
</li>
|
||||
<li>
|
||||
<IconDestroy className='inline-icon icon-red' /> удалить
|
||||
</li>
|
||||
</div>
|
||||
|
||||
<h2>Термин и Текстовое определение</h2>
|
||||
<li>
|
||||
<IconEdit className='inline-icon' /> кнопка переименования справа от{' '}
|
||||
<LinkTopic text='Имени' topic={HelpTopic.CC_CONSTITUENTA} />
|
||||
</li>
|
||||
<li>
|
||||
<IconEdit className='inline-icon' /> кнопка редактирования словоформ справа от{' '}
|
||||
<LinkTopic text='Термина' topic={HelpTopic.CC_CONSTITUENTA} />
|
||||
</li>
|
||||
<li>Ctrl + Пробел открывает редактирование отсылок</li>
|
||||
<div className='flex flex-col'>
|
||||
<h2>Список конституент</h2>
|
||||
<li>
|
||||
<IconMoveDown className='inline-icon' />
|
||||
<IconMoveUp className='inline-icon' /> Alt + вверх/вниз
|
||||
</li>
|
||||
<li>
|
||||
<IconFilter className='inline-icon' />
|
||||
<IconSettings className='inline-icon' /> фильтрация по графу термов
|
||||
</li>
|
||||
<li>
|
||||
<IconChild className='inline-icon' /> отображение наследованных
|
||||
</li>
|
||||
<li>
|
||||
<span style={{ backgroundColor: colors.bgSelected }}>текущая конституента</span>
|
||||
</li>
|
||||
<li>
|
||||
<span style={{ backgroundColor: colors.bgGreen50 }}>
|
||||
<LinkTopic text='основа' topic={HelpTopic.CC_RELATIONS} /> текущей
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span style={{ backgroundColor: colors.bgOrange50 }}>
|
||||
<LinkTopic text='порожденные' topic={HelpTopic.CC_RELATIONS} /> текущей
|
||||
</span>
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Определение понятия</h2>
|
||||
<h2>Формальное определение</h2>
|
||||
<li>
|
||||
<IconStatusOK className='inline-icon' /> индикатор статуса определения сверху
|
||||
</li>
|
||||
|
@ -69,26 +100,12 @@ function HelpCstEditor() {
|
|||
</li>
|
||||
<li>Ctrl + Пробел дополняет до незанятого имени</li>
|
||||
|
||||
<h2>Список конституент</h2>
|
||||
<h2>Термин и Текстовое определение</h2>
|
||||
<li>
|
||||
<IconList className='inline-icon' /> отображение списка конституент
|
||||
</li>
|
||||
<li>
|
||||
<IconMoveDown className='inline-icon' />
|
||||
<IconMoveUp className='inline-icon' /> Alt + вверх/вниз – перемещение
|
||||
</li>
|
||||
<li>фильтрация в верхней части</li>
|
||||
<li>
|
||||
<span style={{ backgroundColor: colors.bgSelected }}>цветом фона</span> выделена текущая конституента
|
||||
</li>
|
||||
<li>
|
||||
<span style={{ backgroundColor: colors.bgGreen50 }}>цветом фона</span> выделена{' '}
|
||||
<LinkTopic text='основа' topic={HelpTopic.CC_RELATIONS} /> текущей
|
||||
</li>
|
||||
<li>
|
||||
<span style={{ backgroundColor: colors.bgOrange50 }}>цветом фона</span> выделены{' '}
|
||||
<LinkTopic text='порожденные' topic={HelpTopic.CC_RELATIONS} /> текущей
|
||||
<IconEdit className='inline-icon' /> редактирование <LinkTopic text='Имени' topic={HelpTopic.CC_CONSTITUENTA} />{' '}
|
||||
/ <LinkTopic text='Термина' topic={HelpTopic.CC_CONSTITUENTA} />
|
||||
</li>
|
||||
<li>Ctrl + Пробел открывает редактирование отсылок</li>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,101 @@
|
|||
import {
|
||||
IconAnimation,
|
||||
IconAnimationOff,
|
||||
IconConnect,
|
||||
IconDestroy,
|
||||
IconEdit2,
|
||||
IconExecute,
|
||||
IconFitImage,
|
||||
IconGrid,
|
||||
IconImage,
|
||||
IconLineStraight,
|
||||
IconLineWave,
|
||||
IconNewItem,
|
||||
IconNewRSForm,
|
||||
IconReset,
|
||||
IconRSForm,
|
||||
IconSave
|
||||
} from '@/components/Icons';
|
||||
import Divider from '@/components/ui/Divider';
|
||||
import LinkTopic from '@/components/ui/LinkTopic';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
|
||||
function HelpOssGraph() {
|
||||
return (
|
||||
<div>
|
||||
<div className='flex flex-col'>
|
||||
<h1>Граф синтеза</h1>
|
||||
<p>TBD.</p>
|
||||
<div className='flex flex-col sm:flex-row'>
|
||||
<div className='w-full sm:w-[14rem]'>
|
||||
<h1>Настройка графа</h1>
|
||||
<li>
|
||||
<IconFitImage className='inline-icon' /> Вписать в экран
|
||||
</li>
|
||||
<li>
|
||||
<IconGrid className='inline-icon' /> Отображение сетки
|
||||
</li>
|
||||
<li>
|
||||
<IconLineWave className='inline-icon' />
|
||||
<IconLineStraight className='inline-icon' /> Тип линии
|
||||
</li>
|
||||
<li>
|
||||
<IconAnimation className='inline-icon' />
|
||||
<IconAnimationOff className='inline-icon' /> Анимация
|
||||
</li>
|
||||
</div>
|
||||
|
||||
<Divider vertical margins='mx-3 mt-3' className='hidden sm:block' />
|
||||
|
||||
<div className='w-full sm:w-[21rem]'>
|
||||
<h1>Изменение узлов</h1>
|
||||
<li>Клик на операцию – выделение</li>
|
||||
<li>Esc – сбросить выделение</li>
|
||||
<li>
|
||||
<IconEdit2 className='inline-icon' /> Двойной клик – редактирование
|
||||
</li>
|
||||
<li>
|
||||
<IconNewItem className='inline-icon icon-green' /> Новая операция
|
||||
</li>
|
||||
<li>
|
||||
<IconDestroy className='inline-icon icon-red' /> Delete – удалить выбранные
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider margins='my-3' className='hidden sm:block' />
|
||||
|
||||
<div className='flex flex-col-reverse mb-3 sm:flex-row'>
|
||||
<div className='w-full sm:w-[14rem]'>
|
||||
<h1>Общие</h1>
|
||||
<li>
|
||||
<IconReset className='inline-icon' /> Сбросить изменения
|
||||
</li>
|
||||
<li>
|
||||
<IconSave className='inline-icon' /> Сохранить положения
|
||||
</li>
|
||||
<li>
|
||||
<IconImage className='inline-icon' /> Сохранить в формат SVG
|
||||
</li>
|
||||
</div>
|
||||
|
||||
<Divider vertical margins='mx-3' className='hidden sm:block' />
|
||||
|
||||
<div className='dense w-[21rem]'>
|
||||
<h1>Контекстное меню</h1>
|
||||
<li>
|
||||
<IconRSForm className='inline-icon icon-green' /> Переход к связанной{' '}
|
||||
<LinkTopic text='КС' topic={HelpTopic.CC_SYSTEM} />
|
||||
</li>
|
||||
<li>
|
||||
<IconNewRSForm className='inline-icon icon-green' /> Создать пустую КС для загрузки
|
||||
</li>
|
||||
<li>
|
||||
<IconConnect className='inline-icon' /> Выбрать КС для загрузки
|
||||
</li>
|
||||
<li>
|
||||
<IconExecute className='inline-icon icon-green' /> Выполнить (активировать) операцию
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ function HelpTermGraph() {
|
|||
const { colors } = useConceptOptions();
|
||||
return (
|
||||
<div className='flex flex-col'>
|
||||
<h1>Граф термов</h1>
|
||||
<div className='flex flex-col sm:flex-row'>
|
||||
<div className='w-full sm:w-[14rem]'>
|
||||
<h1>Настройка графа</h1>
|
||||
|
@ -78,7 +79,7 @@ function HelpTermGraph() {
|
|||
<IconFilter className='inline-icon' /> Открыть настройки
|
||||
</li>
|
||||
<li>
|
||||
<IconFitImage className='inline-icon' /> Вписать граф в экран
|
||||
<IconFitImage className='inline-icon' /> Вписать в экран
|
||||
</li>
|
||||
<li>
|
||||
<IconImage className='inline-icon' /> Сохранить в формат PNG
|
||||
|
|
|
@ -33,7 +33,7 @@ function InputNode(node: OssNodeInternal) {
|
|||
disabled={!hasFile}
|
||||
/>
|
||||
</Overlay>
|
||||
<div id={`${prefixes.operation_list}${node.id}`} className='flex-grow text-center text-sm'>
|
||||
<div id={`${prefixes.operation_list}${node.id}`} className='flex-grow text-center'>
|
||||
{node.data.label}
|
||||
{controller.showTooltip && !node.dragging ? (
|
||||
<TooltipOperation anchor={`#${prefixes.operation_list}${node.id}`} node={node} />
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { IconConnect, IconDestroy, IconEdit2, IconExecute, IconNewItem, IconRSForm } from '@/components/Icons';
|
||||
import { IconConnect, IconDestroy, IconEdit2, IconExecute, IconNewRSForm, IconRSForm } from '@/components/Icons';
|
||||
import Dropdown from '@/components/ui/Dropdown';
|
||||
import DropdownButton from '@/components/ui/DropdownButton';
|
||||
import useClickedOutside from '@/hooks/useClickedOutside';
|
||||
|
@ -124,7 +124,7 @@ function NodeContextMenu({
|
|||
<DropdownButton
|
||||
text='Создать схему'
|
||||
title='Создать пустую схему для загрузки'
|
||||
icon={<IconNewItem size='1rem' className='icon-green' />}
|
||||
icon={<IconNewRSForm size='1rem' className='icon-green' />}
|
||||
disabled={controller.isProcessing}
|
||||
onClick={handleCreateSchema}
|
||||
/>
|
||||
|
|
|
@ -33,7 +33,7 @@ function OperationNode(node: OssNodeInternal) {
|
|||
/>
|
||||
</Overlay>
|
||||
|
||||
<div id={`${prefixes.operation_list}${node.id}`} className='flex-grow text-center text-sm'>
|
||||
<div id={`${prefixes.operation_list}${node.id}`} className='flex-grow text-center'>
|
||||
{node.data.label}
|
||||
{controller.showTooltip && !node.dragging ? (
|
||||
<TooltipOperation anchor={`#${prefixes.operation_list}${node.id}`} node={node} />
|
||||
|
|
|
@ -24,7 +24,7 @@ import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
|||
import { useOSS } from '@/context/OssContext';
|
||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
||||
import { OssNode } from '@/models/miscellaneous';
|
||||
import { OperationID } from '@/models/oss';
|
||||
import { OperationID, OperationType } from '@/models/oss';
|
||||
import { PARAMETER, storage } from '@/utils/constants';
|
||||
import { errors } from '@/utils/labels';
|
||||
|
||||
|
@ -127,19 +127,32 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
if (!controller.schema) {
|
||||
return;
|
||||
}
|
||||
|
||||
let target = { x: 0, y: 0 };
|
||||
const positions = getPositions();
|
||||
|
||||
if (inputs.length <= 1) {
|
||||
if (positions.length == 0) {
|
||||
target = flow.project({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
|
||||
}
|
||||
if (inputs.length <= 1) {
|
||||
let inputsNodes = positions.filter(pos =>
|
||||
controller.schema!.items.find(
|
||||
operation => operation.operation_type === OperationType.INPUT && operation.id === pos.id
|
||||
)
|
||||
);
|
||||
if (inputsNodes.length > 0) {
|
||||
inputsNodes = positions;
|
||||
}
|
||||
const maxX = Math.max(...inputsNodes.map(node => node.position_x));
|
||||
const minY = Math.min(...inputsNodes.map(node => node.position_y));
|
||||
target.x = maxX + 180;
|
||||
target.y = minY;
|
||||
} else {
|
||||
const inputsNodes = positions.filter(pos => inputs.includes(pos.id));
|
||||
const maxY = Math.max(...inputsNodes.map(node => node.position_y));
|
||||
const minX = Math.min(...inputsNodes.map(node => node.position_x));
|
||||
const maxX = Math.max(...inputsNodes.map(node => node.position_x));
|
||||
|
||||
target.y = maxY + 100;
|
||||
target.x = Math.ceil((maxX + minX) / 2 / PARAMETER.ossGridSize) * PARAMETER.ossGridSize;
|
||||
target.y = maxY + 100;
|
||||
}
|
||||
|
||||
let flagIntersect = false;
|
||||
|
@ -154,8 +167,13 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
target.y += PARAMETER.ossMinDistance;
|
||||
}
|
||||
} while (flagIntersect);
|
||||
|
||||
controller.promptCreateOperation(target.x, target.y, inputs, positions);
|
||||
controller.promptCreateOperation({
|
||||
x: target.x,
|
||||
y: target.y,
|
||||
inputs: inputs,
|
||||
positions: positions,
|
||||
callback: () => flow.fitView({ duration: PARAMETER.zoomDuration })
|
||||
});
|
||||
},
|
||||
[controller, getPositions, flow]
|
||||
);
|
||||
|
|
|
@ -15,7 +15,7 @@ import DlgChangeLocation from '@/dialogs/DlgChangeLocation';
|
|||
import DlgCreateOperation from '@/dialogs/DlgCreateOperation';
|
||||
import DlgEditEditors from '@/dialogs/DlgEditEditors';
|
||||
import DlgEditOperation from '@/dialogs/DlgEditOperation';
|
||||
import { AccessPolicy, LibraryItemID } from '@/models/library';
|
||||
import { AccessPolicy, ILibraryItemEditor, LibraryItemID } from '@/models/library';
|
||||
import { Position2D } from '@/models/miscellaneous';
|
||||
import {
|
||||
IOperationCreateData,
|
||||
|
@ -26,14 +26,24 @@ import {
|
|||
OperationID
|
||||
} from '@/models/oss';
|
||||
import { UserID, UserLevel } from '@/models/user';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { information } from '@/utils/labels';
|
||||
|
||||
export interface IOssEditContext {
|
||||
export interface ICreateOperationPrompt {
|
||||
x: number;
|
||||
y: number;
|
||||
inputs: OperationID[];
|
||||
positions: IOperationPosition[];
|
||||
callback: (newID: OperationID) => void;
|
||||
}
|
||||
|
||||
export interface IOssEditContext extends ILibraryItemEditor {
|
||||
schema?: IOperationSchema;
|
||||
selected: OperationID[];
|
||||
|
||||
isMutable: boolean;
|
||||
isProcessing: boolean;
|
||||
isAttachedToOSS: boolean;
|
||||
|
||||
showTooltip: boolean;
|
||||
setShowTooltip: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
|
@ -51,7 +61,7 @@ export interface IOssEditContext {
|
|||
openOperationSchema: (target: OperationID) => void;
|
||||
|
||||
savePositions: (positions: IOperationPosition[], callback?: () => void) => void;
|
||||
promptCreateOperation: (x: number, y: number, inputs: OperationID[], positions: IOperationPosition[]) => void;
|
||||
promptCreateOperation: (props: ICreateOperationPrompt) => void;
|
||||
deleteOperation: (target: OperationID, positions: IOperationPosition[]) => void;
|
||||
createInput: (target: OperationID, positions: IOperationPosition[]) => void;
|
||||
promptEditInput: (target: OperationID, positions: IOperationPosition[]) => void;
|
||||
|
@ -97,6 +107,8 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
|
|||
const [showCreateOperation, setShowCreateOperation] = useState(false);
|
||||
const [insertPosition, setInsertPosition] = useState<Position2D>({ x: 0, y: 0 });
|
||||
const [initialInputs, setInitialInputs] = useState<OperationID[]>([]);
|
||||
const [createCallback, setCreateCallback] = useState<((newID: OperationID) => void) | undefined>(undefined);
|
||||
|
||||
const [positions, setPositions] = useState<IOperationPosition[]>([]);
|
||||
const [targetOperationID, setTargetOperationID] = useState<OperationID | undefined>(undefined);
|
||||
const targetOperation = useMemo(
|
||||
|
@ -184,7 +196,7 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
|
|||
const openOperationSchema = useCallback(
|
||||
(target: OperationID) => {
|
||||
const node = model.schema?.operationByID.get(target);
|
||||
if (!node || !node.result) {
|
||||
if (!node?.result) {
|
||||
return;
|
||||
}
|
||||
router.push(urls.schema(node.result));
|
||||
|
@ -209,24 +221,27 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
|
|||
[model]
|
||||
);
|
||||
|
||||
const promptCreateOperation = useCallback(
|
||||
(x: number, y: number, inputs: OperationID[], positions: IOperationPosition[]) => {
|
||||
setInsertPosition({ x: x, y: y });
|
||||
setInitialInputs(inputs);
|
||||
setPositions(positions);
|
||||
setShowCreateOperation(true);
|
||||
},
|
||||
[]
|
||||
);
|
||||
const promptCreateOperation = useCallback(({ x, y, inputs, positions, callback }: ICreateOperationPrompt) => {
|
||||
setInsertPosition({ x: x, y: y });
|
||||
setInitialInputs(inputs);
|
||||
setPositions(positions);
|
||||
setCreateCallback(() => callback);
|
||||
setShowCreateOperation(true);
|
||||
}, []);
|
||||
|
||||
const handleCreateOperation = useCallback(
|
||||
(data: IOperationCreateData) => {
|
||||
data.positions = positions;
|
||||
data.item_data.position_x = insertPosition.x;
|
||||
data.item_data.position_y = insertPosition.y;
|
||||
model.createOperation(data, operation => toast.success(information.newOperation(operation.alias)));
|
||||
model.createOperation(data, operation => {
|
||||
toast.success(information.newOperation(operation.alias));
|
||||
if (createCallback) {
|
||||
setTimeout(() => createCallback(operation.id), PARAMETER.refreshTimeout);
|
||||
}
|
||||
});
|
||||
},
|
||||
[model, positions, insertPosition]
|
||||
[model, positions, insertPosition, createCallback]
|
||||
);
|
||||
|
||||
const promptEditOperation = useCallback((target: OperationID, positions: IOperationPosition[]) => {
|
||||
|
@ -305,6 +320,7 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
|
|||
|
||||
isMutable,
|
||||
isProcessing: model.processing,
|
||||
isAttachedToOSS: false,
|
||||
|
||||
toggleSubscribe,
|
||||
setOwner,
|
||||
|
|
|
@ -135,7 +135,7 @@ function OssTabs() {
|
|||
<TabList className={clsx('mx-auto w-fit', 'flex items-stretch', 'border-b-2 border-x-2 divide-x-2')}>
|
||||
<MenuOssTabs onDestroy={onDestroySchema} />
|
||||
|
||||
<TabLabel label='Карточка' titleHtml={`Название: <b>${schema.title ?? ''}</b>`} />
|
||||
<TabLabel label='Карточка' title={schema.title ?? ''} />
|
||||
<TabLabel label='Граф' />
|
||||
</TabList>
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
|
||||
import {
|
||||
IconClone,
|
||||
IconDestroy,
|
||||
|
@ -54,7 +56,7 @@ function ToolbarConstituenta({
|
|||
onSelect={(event, value) => controller.viewOSS(value.id, event.ctrlKey || event.metaKey)}
|
||||
/>
|
||||
) : null}
|
||||
{activeCst && activeCst.is_inherited ? (
|
||||
{activeCst?.is_inherited ? (
|
||||
<MiniButton
|
||||
title='Перейти к исходной конституенте в ОСС'
|
||||
onClick={() => controller.viewPredecessor(activeCst.id)}
|
||||
|
@ -118,7 +120,11 @@ function ToolbarConstituenta({
|
|||
/>
|
||||
</>
|
||||
) : null}
|
||||
<BadgeHelp topic={HelpTopic.UI_RS_EDITOR} offset={4} className={PARAMETER.TOOLTIP_WIDTH} />
|
||||
<BadgeHelp
|
||||
topic={HelpTopic.UI_RS_EDITOR}
|
||||
offset={4}
|
||||
className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')}
|
||||
/>
|
||||
</Overlay>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -117,7 +117,7 @@ function EditorRSExpression({
|
|||
);
|
||||
|
||||
const handleEdit = useCallback((id: TokenID, key?: string) => {
|
||||
if (!rsInput.current || !rsInput.current.editor || !rsInput.current.state || !rsInput.current.view) {
|
||||
if (!rsInput.current?.editor || !rsInput.current.state || !rsInput.current.view) {
|
||||
return;
|
||||
}
|
||||
const text = new RSTextWrapper(rsInput.current as Required<ReactCodeMirrorRef>);
|
||||
|
@ -145,12 +145,12 @@ function EditorRSExpression({
|
|||
const controls = useMemo(
|
||||
() => (
|
||||
<RSEditorControls
|
||||
isOpen={showControls && (!disabled || model.processing)}
|
||||
isOpen={showControls && (!disabled || (model.processing && !activeCst?.is_inherited))}
|
||||
disabled={disabled}
|
||||
onEdit={handleEdit}
|
||||
/>
|
||||
),
|
||||
[showControls, disabled, model.processing, handleEdit]
|
||||
[showControls, disabled, model.processing, handleEdit, activeCst]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -4,6 +4,7 @@ import { useIntl } from 'react-intl';
|
|||
import { IconEdit } from '@/components/Icons';
|
||||
import InfoUsers from '@/components/info/InfoUsers';
|
||||
import SelectUser from '@/components/select/SelectUser';
|
||||
import LabeledValue from '@/components/ui/LabeledValue';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
import Tooltip from '@/components/ui/Tooltip';
|
||||
|
@ -15,8 +16,6 @@ import { UserID, UserLevel } from '@/models/user';
|
|||
import { prefixes } from '@/utils/constants';
|
||||
import { prompts } from '@/utils/labels';
|
||||
|
||||
import LabeledValue from '@/components/ui/LabeledValue';
|
||||
|
||||
interface EditorLibraryItemProps {
|
||||
item?: ILibraryItemData;
|
||||
isModified?: boolean;
|
||||
|
@ -48,11 +47,11 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
|
|||
{accessLevel >= UserLevel.OWNER ? (
|
||||
<Overlay position='top-[-0.5rem] left-[2.3rem] cc-icons'>
|
||||
<MiniButton
|
||||
title='Изменить путь'
|
||||
title={controller.isAttachedToOSS ? 'Путь наследуется от ОСС' : 'Изменить путь'}
|
||||
noHover
|
||||
onClick={() => controller.promptLocation()}
|
||||
icon={<IconEdit size='1rem' className='mt-1 icon-primary' />}
|
||||
disabled={isModified || controller.isProcessing}
|
||||
disabled={isModified || controller.isProcessing || controller.isAttachedToOSS}
|
||||
/>
|
||||
</Overlay>
|
||||
) : null}
|
||||
|
@ -66,11 +65,11 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
|
|||
<Overlay position='top-[-0.5rem] left-[5.5rem] cc-icons'>
|
||||
<div className='flex items-start'>
|
||||
<MiniButton
|
||||
title='Изменить владельца'
|
||||
title={controller.isAttachedToOSS ? 'Владелец наследуется от ОСС' : 'Изменить владельца'}
|
||||
noHover
|
||||
onClick={() => ownerSelector.toggle()}
|
||||
icon={<IconEdit size='1rem' className='mt-1 icon-primary' />}
|
||||
disabled={isModified || controller.isProcessing}
|
||||
disabled={isModified || controller.isProcessing || controller.isAttachedToOSS}
|
||||
/>
|
||||
{ownerSelector.isOpen ? (
|
||||
<SelectUser
|
||||
|
|
|
@ -33,7 +33,7 @@ function ToolbarItemAccess({ visible, toggleVisible, readOnly, toggleReadOnly, c
|
|||
<Label text='Доступ' className='self-center select-none' />
|
||||
<div className='ml-auto cc-icons'>
|
||||
<SelectAccessPolicy
|
||||
disabled={accessLevel <= UserLevel.EDITOR || controller.isProcessing}
|
||||
disabled={accessLevel <= UserLevel.EDITOR || controller.isProcessing || controller.isAttachedToOSS}
|
||||
value={policy}
|
||||
onChange={newPolicy => controller.setAccessPolicy(newPolicy)}
|
||||
/>
|
||||
|
|
|
@ -26,6 +26,7 @@ import DlgSubstituteCst from '@/dialogs/DlgSubstituteCst';
|
|||
import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm';
|
||||
import {
|
||||
AccessPolicy,
|
||||
ILibraryItemEditor,
|
||||
ILibraryUpdateData,
|
||||
IVersionData,
|
||||
LibraryItemID,
|
||||
|
@ -55,13 +56,14 @@ import { promptUnsaved } from '@/utils/utils';
|
|||
|
||||
import { RSTabID } from './RSTabs';
|
||||
|
||||
export interface IRSEditContext {
|
||||
export interface IRSEditContext extends ILibraryItemEditor {
|
||||
schema?: IRSForm;
|
||||
selected: ConstituentaID[];
|
||||
|
||||
isMutable: boolean;
|
||||
isContentEditable: boolean;
|
||||
isProcessing: boolean;
|
||||
isAttachedToOSS: boolean;
|
||||
canProduceStructure: boolean;
|
||||
nothingSelected: boolean;
|
||||
canDeleteSelected: boolean;
|
||||
|
@ -153,6 +155,13 @@ export const RSEditState = ({
|
|||
() => !nothingSelected && selected.every(id => !model.schema?.cstByID.get(id)?.is_inherited),
|
||||
[selected, nothingSelected, model.schema]
|
||||
);
|
||||
const isAttachedToOSS = useMemo(
|
||||
() =>
|
||||
!!model.schema &&
|
||||
model.schema.oss.length > 0 &&
|
||||
(model.schema.stats.count_inherited > 0 || model.schema.items.length === 0),
|
||||
[model.schema]
|
||||
);
|
||||
|
||||
const [showUpload, setShowUpload] = useState(false);
|
||||
const [showClone, setShowClone] = useState(false);
|
||||
|
@ -378,7 +387,7 @@ export const RSEditState = ({
|
|||
const oldCount = model.schema.items.length;
|
||||
model.inlineSynthesis(data, newSchema => {
|
||||
setSelected([]);
|
||||
toast.success(information.addedConstituents(newSchema['items'].length - oldCount));
|
||||
toast.success(information.addedConstituents(newSchema.items.length - oldCount));
|
||||
});
|
||||
},
|
||||
[model, setSelected]
|
||||
|
@ -618,6 +627,7 @@ export const RSEditState = ({
|
|||
isMutable,
|
||||
isContentEditable,
|
||||
isProcessing: model.processing,
|
||||
isAttachedToOSS,
|
||||
canProduceStructure,
|
||||
nothingSelected,
|
||||
canDeleteSelected,
|
||||
|
|
|
@ -75,7 +75,7 @@ function RSTabs() {
|
|||
setIsModified(false);
|
||||
if (activeTab === RSTabID.CST_EDIT) {
|
||||
const cstID = Number(cstQuery);
|
||||
if (cstID && schema && schema.cstByID.has(cstID)) {
|
||||
if (cstID && schema?.cstByID.has(cstID)) {
|
||||
setSelected([cstID]);
|
||||
} else if (schema && schema?.items.length > 0) {
|
||||
setSelected([schema.items[0].id]);
|
||||
|
@ -257,10 +257,7 @@ function RSTabs() {
|
|||
<TabList className={clsx('mx-auto w-fit', 'flex items-stretch', 'border-b-2 border-x-2 divide-x-2')}>
|
||||
<MenuRSTabs onDestroy={onDestroySchema} />
|
||||
|
||||
<TabLabel
|
||||
label='Карточка'
|
||||
titleHtml={`Название: <b>${schema.title ?? ''}</b><br />Версия: ${labelVersion(schema)}`}
|
||||
/>
|
||||
<TabLabel label='Карточка' titleHtml={`${schema.title ?? ''}<br />Версия: ${labelVersion(schema)}`} />
|
||||
<TabLabel
|
||||
label='Содержание'
|
||||
titleHtml={`Конституент: ${schema.stats?.count_all ?? 0}<br />Ошибок: ${schema.stats?.count_errors ?? 0}`}
|
||||
|
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
import { useLayoutEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { IconChild } from '@/components/Icons';
|
||||
import SelectGraphFilter from '@/components/select/SelectGraphFilter';
|
||||
import SelectMatchMode from '@/components/select/SelectMatchMode';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import SearchBar from '@/components/ui/SearchBar';
|
||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
||||
import { CstMatchMode, DependencyMode } from '@/models/miscellaneous';
|
||||
|
@ -25,6 +27,7 @@ function ConstituentsSearch({ schema, activeID, activeExpression, dense, setFilt
|
|||
const [filterMatch, setFilterMatch] = useLocalStorage(storage.cstFilterMatch, CstMatchMode.ALL);
|
||||
const [filterSource, setFilterSource] = useLocalStorage(storage.cstFilterGraph, DependencyMode.ALL);
|
||||
const [filterText, setFilterText] = useState('');
|
||||
const [showInherited, setShowInherited] = useLocalStorage(storage.cstFilterShowInherited, true);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!schema || schema.items.length === 0) {
|
||||
|
@ -48,8 +51,21 @@ function ConstituentsSearch({ schema, activeID, activeExpression, dense, setFilt
|
|||
if (filterText) {
|
||||
result = result.filter(cst => matchConstituenta(cst, filterText, filterMatch));
|
||||
}
|
||||
if (!showInherited) {
|
||||
result = result.filter(cst => !cst.is_inherited);
|
||||
}
|
||||
setFiltered(result);
|
||||
}, [filterText, setFiltered, filterSource, activeExpression, schema?.items, schema, filterMatch, activeID]);
|
||||
}, [
|
||||
filterText,
|
||||
setFiltered,
|
||||
filterSource,
|
||||
activeExpression,
|
||||
schema?.items,
|
||||
schema,
|
||||
filterMatch,
|
||||
activeID,
|
||||
showInherited
|
||||
]);
|
||||
|
||||
const selectGraph = useMemo(
|
||||
() => <SelectGraphFilter value={filterSource} onChange={newValue => setFilterSource(newValue)} dense={dense} />,
|
||||
|
@ -72,6 +88,15 @@ function ConstituentsSearch({ schema, activeID, activeExpression, dense, setFilt
|
|||
/>
|
||||
{selectMatchMode}
|
||||
{selectGraph}
|
||||
{schema && schema?.stats.count_inherited > 0 ? (
|
||||
<MiniButton
|
||||
noHover
|
||||
titleHtml={`Наследованные: <b>${showInherited ? 'отображать' : 'скрывать'}</b>`}
|
||||
icon={<IconChild size='1rem' className={showInherited ? 'icon-primary' : 'clr-text-controls'} />}
|
||||
className='h-fit self-center'
|
||||
onClick={() => setShowInherited(prev => !prev)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ function TableSideConstituents({
|
|||
useLayoutEffect(() => {
|
||||
setColumnVisibility(prev => {
|
||||
const newValue = (windowSize.width ?? 0) >= denseThreshold;
|
||||
if (newValue === prev['expression']) {
|
||||
if (newValue === prev.expression) {
|
||||
return prev;
|
||||
} else {
|
||||
return { expression: newValue };
|
||||
|
|
|
@ -66,6 +66,8 @@
|
|||
border: 1px solid;
|
||||
padding: 2px;
|
||||
width: 150px;
|
||||
height: 30px;
|
||||
font-size: 14px;
|
||||
|
||||
border-radius: 5px;
|
||||
background-color: var(--cl-bg-120);
|
||||
|
|
|
@ -37,11 +37,11 @@ function cursorNode({ type, from, to }: TreeCursor, isLeaf = false): CursorNode
|
|||
return { type, from, to, isLeaf };
|
||||
}
|
||||
|
||||
type TreeTraversalOptions = {
|
||||
interface TreeTraversalOptions {
|
||||
beforeEnter?: (cursor: TreeCursor) => void;
|
||||
onEnter: (node: CursorNode) => false | void;
|
||||
onLeave?: (node: CursorNode) => false | void;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements depth-first traversal.
|
||||
|
|
|
@ -17,6 +17,8 @@ export const PARAMETER = {
|
|||
ossContextMenuWidth: 200, // pixels - width of OSS context menu
|
||||
ossGridSize: 10, // pixels - size of OSS grid
|
||||
ossMinDistance: 20, // pixels - minimum distance between node centers
|
||||
ossDistanceX: 180, // pixels - insert x-distance between node centers
|
||||
ossDistanceY: 100, // pixels - insert y-distance between node centers
|
||||
|
||||
graphHoverXLimit: 0.4, // ratio to clientWidth used to determine which side of screen popup should be
|
||||
graphHoverYLimit: 0.6, // ratio to clientHeight used to determine which side of screen popup should be
|
||||
|
@ -121,7 +123,8 @@ export const storage = {
|
|||
ossEdgeAnimate: 'oss.edge_animate',
|
||||
|
||||
cstFilterMatch: 'cst.filter.match',
|
||||
cstFilterGraph: 'cst.filter.graph'
|
||||
cstFilterGraph: 'cst.filter.graph',
|
||||
cstFilterShowInherited: 'cst.filter.show_inherited'
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -296,7 +296,7 @@ export function describeLocationHead(head: LocationHead): string {
|
|||
/**
|
||||
* Retrieves label for graph layout mode.
|
||||
*/
|
||||
export const mapLabelLayout: Map<GraphLayout, string> = new Map([
|
||||
export const mapLabelLayout = new Map<GraphLayout, string>([
|
||||
['treeTd2d', 'Граф: ДеревоВ 2D'],
|
||||
['treeTd3d', 'Граф: ДеревоВ 3D'],
|
||||
['forceatlas2', 'Граф: Атлас 2D'],
|
||||
|
@ -312,7 +312,7 @@ export const mapLabelLayout: Map<GraphLayout, string> = new Map([
|
|||
/**
|
||||
* Retrieves label for {@link GraphColoring}.
|
||||
*/
|
||||
export const mapLabelColoring: Map<GraphColoring, string> = new Map([
|
||||
export const mapLabelColoring = new Map<GraphColoring, string>([
|
||||
['none', 'Цвет: Моно'],
|
||||
['status', 'Цвет: Статус'],
|
||||
['type', 'Цвет: Класс']
|
||||
|
@ -321,7 +321,7 @@ export const mapLabelColoring: Map<GraphColoring, string> = new Map([
|
|||
/**
|
||||
* Retrieves label for {@link GraphSizing}.
|
||||
*/
|
||||
export const mapLabelSizing: Map<GraphSizing, string> = new Map([
|
||||
export const mapLabelSizing = new Map<GraphSizing, string>([
|
||||
['none', 'Узлы: Моно'],
|
||||
['derived', 'Узлы: Порожденные'],
|
||||
['complex', 'Узлы: Простые']
|
||||
|
|
|
@ -29,14 +29,14 @@ export class TextMatcher {
|
|||
}
|
||||
try {
|
||||
this.query = new RegExp(query, isCaseSensitive ? '' : 'i');
|
||||
} catch (exception: unknown) {
|
||||
} catch (_exception: unknown) {
|
||||
this.query = query;
|
||||
}
|
||||
}
|
||||
|
||||
test(text: string): boolean {
|
||||
if (typeof this.query === 'string') {
|
||||
return text.indexOf(this.query) !== -1;
|
||||
return text.includes(this.query);
|
||||
} else {
|
||||
return !!text.match(this.query);
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ export class TextMatcher {
|
|||
/**
|
||||
* Text substitution guided by mapping and regular expression.
|
||||
*/
|
||||
export function applyPattern(text: string, mapping: { [key: string]: string }, pattern: RegExp): string {
|
||||
export function applyPattern(text: string, mapping: Record<string, string>, pattern: RegExp): string {
|
||||
if (text === '' || pattern === null) {
|
||||
return text;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user