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