mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Implementing locations and permissions pt1
This commit is contained in:
parent
64e1b028bc
commit
dc0555076b
|
@ -16,11 +16,11 @@ class LibraryItemAdmin(admin.ModelAdmin):
|
||||||
date_hierarchy = 'time_update'
|
date_hierarchy = 'time_update'
|
||||||
list_display = [
|
list_display = [
|
||||||
'alias', 'title', 'owner',
|
'alias', 'title', 'owner',
|
||||||
'is_common', 'is_canonical',
|
'visible', 'read_only', 'access_policy', 'location',
|
||||||
'time_update'
|
'time_update'
|
||||||
]
|
]
|
||||||
list_filter = ['is_common', 'is_canonical', 'time_update']
|
list_filter = ['visible', 'read_only', 'access_policy', 'location', 'time_update']
|
||||||
search_fields = ['alias', 'title']
|
search_fields = ['alias', 'title', 'location']
|
||||||
|
|
||||||
|
|
||||||
class LibraryTemplateAdmin(admin.ModelAdmin):
|
class LibraryTemplateAdmin(admin.ModelAdmin):
|
||||||
|
@ -44,6 +44,15 @@ class SubscriptionAdmin(admin.ModelAdmin):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class EditorAdmin(admin.ModelAdmin):
|
||||||
|
''' Admin model: Editors. '''
|
||||||
|
list_display = ['id', 'item', 'editor']
|
||||||
|
search_fields = [
|
||||||
|
'item__title', 'item__alias',
|
||||||
|
'editor__username', 'editor__first_name', 'editor__last_name'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class VersionAdmin(admin.ModelAdmin):
|
class VersionAdmin(admin.ModelAdmin):
|
||||||
''' Admin model: Versions. '''
|
''' Admin model: Versions. '''
|
||||||
list_display = ['id', 'item', 'version', 'description', 'time_create']
|
list_display = ['id', 'item', 'version', 'description', 'time_create']
|
||||||
|
@ -57,3 +66,4 @@ admin.site.register(models.LibraryItem, LibraryItemAdmin)
|
||||||
admin.site.register(models.LibraryTemplate, LibraryTemplateAdmin)
|
admin.site.register(models.LibraryTemplate, LibraryTemplateAdmin)
|
||||||
admin.site.register(models.Subscription, SubscriptionAdmin)
|
admin.site.register(models.Subscription, SubscriptionAdmin)
|
||||||
admin.site.register(models.Version, VersionAdmin)
|
admin.site.register(models.Version, VersionAdmin)
|
||||||
|
admin.site.register(models.Editor, EditorAdmin)
|
||||||
|
|
|
@ -30,6 +30,14 @@ def aliasTaken(name: str):
|
||||||
return f'Имя уже используется: {name}'
|
return f'Имя уже используется: {name}'
|
||||||
|
|
||||||
|
|
||||||
|
def invalidLocation():
|
||||||
|
return f'Некорректная строка расположения'
|
||||||
|
|
||||||
|
|
||||||
|
def invalidEnum(value: str):
|
||||||
|
return f'Неподдерживаемое значение параметра: {value}'
|
||||||
|
|
||||||
|
|
||||||
def pyconceptFailure():
|
def pyconceptFailure():
|
||||||
return 'Invalid data response from pyconcept'
|
return 'Invalid data response from pyconcept'
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
# Hand written migration 20240531
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
from .. import models as m
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('rsform', '0006_editor'),
|
||||||
|
]
|
||||||
|
|
||||||
|
def calculate_location(apps, schema_editor):
|
||||||
|
LibraryItem = apps.get_model('rsform', 'LibraryItem')
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
for item in LibraryItem.objects.using(db_alias).all():
|
||||||
|
if item.is_canonical:
|
||||||
|
location = m.LocationHead.LIBRARY
|
||||||
|
elif item.is_common:
|
||||||
|
location = m.LocationHead.COMMON
|
||||||
|
else:
|
||||||
|
location = m.LocationHead.USER
|
||||||
|
item.location = location
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='libraryitem',
|
||||||
|
name='access_policy',
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
('public', 'Public'),
|
||||||
|
('protected', 'Protected'),
|
||||||
|
('private', 'Private')
|
||||||
|
],
|
||||||
|
default='public',
|
||||||
|
max_length=500,
|
||||||
|
verbose_name='Политика доступа'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='libraryitem',
|
||||||
|
name='location',
|
||||||
|
field=models.TextField(default='/U', max_length=500, verbose_name='Расположение'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='libraryitem',
|
||||||
|
name='read_only',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Запретить редактирование'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='libraryitem',
|
||||||
|
name='visible',
|
||||||
|
field=models.BooleanField(default=True, verbose_name='Отображаемая'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(calculate_location, migrations.RunPython.noop), # type: ignore
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='libraryitem',
|
||||||
|
name='is_canonical',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='libraryitem',
|
||||||
|
name='is_common',
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,4 +1,6 @@
|
||||||
''' Models: LibraryItem. '''
|
''' Models: LibraryItem. '''
|
||||||
|
import re
|
||||||
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
SET_NULL,
|
SET_NULL,
|
||||||
|
@ -24,6 +26,28 @@ class LibraryItemType(TextChoices):
|
||||||
OPERATIONS_SCHEMA = 'oss'
|
OPERATIONS_SCHEMA = 'oss'
|
||||||
|
|
||||||
|
|
||||||
|
class AccessPolicy(TextChoices):
|
||||||
|
''' Type of item access policy. '''
|
||||||
|
PUBLIC = 'public'
|
||||||
|
PROTECTED = 'protected'
|
||||||
|
PRIVATE = 'private'
|
||||||
|
|
||||||
|
|
||||||
|
class LocationHead(TextChoices):
|
||||||
|
''' Location prefixes. '''
|
||||||
|
PROJECTS = '/P'
|
||||||
|
LIBRARY = '/L'
|
||||||
|
USER = '/U'
|
||||||
|
COMMON = '/S'
|
||||||
|
|
||||||
|
|
||||||
|
_RE_LOCATION = r'^/[PLUS]((/[!\d\w]([!\d\w ]*[!\d\w])?)*)?$' # cspell:disable-line
|
||||||
|
|
||||||
|
|
||||||
|
def validate_location(target: str) -> bool:
|
||||||
|
return bool(re.search(_RE_LOCATION, target))
|
||||||
|
|
||||||
|
|
||||||
class LibraryItem(Model):
|
class LibraryItem(Model):
|
||||||
''' Abstract library item.'''
|
''' Abstract library item.'''
|
||||||
item_type: CharField = CharField(
|
item_type: CharField = CharField(
|
||||||
|
@ -49,14 +73,26 @@ class LibraryItem(Model):
|
||||||
verbose_name='Комментарий',
|
verbose_name='Комментарий',
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
is_common: BooleanField = BooleanField(
|
visible: BooleanField = BooleanField(
|
||||||
verbose_name='Общая',
|
verbose_name='Отображаемая',
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
read_only: BooleanField = BooleanField(
|
||||||
|
verbose_name='Запретить редактирование',
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
is_canonical: BooleanField = BooleanField(
|
access_policy: CharField = CharField(
|
||||||
verbose_name='Каноничная',
|
verbose_name='Политика доступа',
|
||||||
default=False
|
max_length=500,
|
||||||
|
choices=AccessPolicy.choices,
|
||||||
|
default=AccessPolicy.PUBLIC
|
||||||
)
|
)
|
||||||
|
location: TextField = TextField(
|
||||||
|
verbose_name='Расположение',
|
||||||
|
max_length=500,
|
||||||
|
default=LocationHead.USER
|
||||||
|
)
|
||||||
|
|
||||||
time_create: DateTimeField = DateTimeField(
|
time_create: DateTimeField = DateTimeField(
|
||||||
verbose_name='Дата создания',
|
verbose_name='Дата создания',
|
||||||
auto_now_add=True
|
auto_now_add=True
|
||||||
|
|
|
@ -3,7 +3,14 @@
|
||||||
from .api_RSForm import RSForm
|
from .api_RSForm import RSForm
|
||||||
from .Constituenta import Constituenta, CstType, _empty_forms
|
from .Constituenta import Constituenta, CstType, _empty_forms
|
||||||
from .Editor import Editor
|
from .Editor import Editor
|
||||||
from .LibraryItem import LibraryItem, LibraryItemType, User
|
from .LibraryItem import (
|
||||||
|
AccessPolicy,
|
||||||
|
LibraryItem,
|
||||||
|
LibraryItemType,
|
||||||
|
LocationHead,
|
||||||
|
User,
|
||||||
|
validate_location
|
||||||
|
)
|
||||||
from .LibraryTemplate import LibraryTemplate
|
from .LibraryTemplate import LibraryTemplate
|
||||||
from .Subscription import Subscription
|
from .Subscription import Subscription
|
||||||
from .Version import Version
|
from .Version import Version
|
||||||
|
|
|
@ -57,10 +57,11 @@ class ItemEditor(ItemOwner):
|
||||||
''' Item permission: Editor or higher. '''
|
''' Item permission: Editor or higher. '''
|
||||||
|
|
||||||
def has_object_permission(self, request: Request, view: APIView, obj: Any) -> bool:
|
def has_object_permission(self, request: Request, view: APIView, obj: Any) -> bool:
|
||||||
|
item = _extract_item(obj)
|
||||||
if m.Editor.objects.filter(
|
if m.Editor.objects.filter(
|
||||||
item=_extract_item(obj),
|
item=item,
|
||||||
editor=cast(m.User, request.user)
|
editor=cast(m.User, request.user)
|
||||||
).exists():
|
).exists() and item.access_policy != m.AccessPolicy.PRIVATE:
|
||||||
return True
|
return True
|
||||||
return super().has_object_permission(request, view, obj)
|
return super().has_object_permission(request, view, obj)
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
''' REST API: Serializers. '''
|
''' REST API: Serializers. '''
|
||||||
|
|
||||||
from .basics import (
|
from .basics import (
|
||||||
|
AccessPolicySerializer,
|
||||||
ASTNodeSerializer,
|
ASTNodeSerializer,
|
||||||
ExpressionParseSerializer,
|
ExpressionParseSerializer,
|
||||||
ExpressionSerializer,
|
ExpressionSerializer,
|
||||||
|
LocationSerializer,
|
||||||
MultiFormSerializer,
|
MultiFormSerializer,
|
||||||
ResolverSerializer,
|
ResolverSerializer,
|
||||||
TextSerializer,
|
TextSerializer,
|
||||||
|
@ -18,6 +20,7 @@ from .data_access import (
|
||||||
CstSubstituteSerializer,
|
CstSubstituteSerializer,
|
||||||
CstTargetSerializer,
|
CstTargetSerializer,
|
||||||
InlineSynthesisSerializer,
|
InlineSynthesisSerializer,
|
||||||
|
LibraryItemBase,
|
||||||
LibraryItemCloneSerializer,
|
LibraryItemCloneSerializer,
|
||||||
LibraryItemSerializer,
|
LibraryItemSerializer,
|
||||||
RSFormParseSerializer,
|
RSFormParseSerializer,
|
||||||
|
|
|
@ -4,6 +4,9 @@ from typing import cast
|
||||||
from cctext import EntityReference, Reference, ReferenceType, Resolver, SyntacticReference
|
from cctext import EntityReference, Reference, ReferenceType, Resolver, SyntacticReference
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from .. import messages as msg
|
||||||
|
from ..models import AccessPolicy, validate_location
|
||||||
|
|
||||||
|
|
||||||
class ExpressionSerializer(serializers.Serializer):
|
class ExpressionSerializer(serializers.Serializer):
|
||||||
''' Serializer: RSLang expression. '''
|
''' Serializer: RSLang expression. '''
|
||||||
|
@ -16,6 +19,32 @@ class WordFormSerializer(serializers.Serializer):
|
||||||
grams = serializers.CharField()
|
grams = serializers.CharField()
|
||||||
|
|
||||||
|
|
||||||
|
class LocationSerializer(serializers.Serializer):
|
||||||
|
''' Serializer: Item location. '''
|
||||||
|
location = serializers.CharField(max_length=500)
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
attrs = super().validate(attrs)
|
||||||
|
if not validate_location(attrs['location']):
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'location': msg.invalidLocation()
|
||||||
|
})
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
class AccessPolicySerializer(serializers.Serializer):
|
||||||
|
''' Serializer: Constituenta renaming. '''
|
||||||
|
access_policy = serializers.CharField(max_length=500)
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
attrs = super().validate(attrs)
|
||||||
|
if not attrs['access_policy'] in AccessPolicy.values:
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'access_policy': msg.invalidEnum(attrs['access_policy'])
|
||||||
|
})
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class MultiFormSerializer(serializers.Serializer):
|
class MultiFormSerializer(serializers.Serializer):
|
||||||
''' Serializer: inflect request. '''
|
''' Serializer: inflect request. '''
|
||||||
items = serializers.ListField(
|
items = serializers.ListField(
|
||||||
|
|
|
@ -13,8 +13,8 @@ from .basics import CstParseSerializer
|
||||||
from .io_pyconcept import PyConceptAdapter
|
from .io_pyconcept import PyConceptAdapter
|
||||||
|
|
||||||
|
|
||||||
class LibraryItemSerializer(serializers.ModelSerializer):
|
class LibraryItemBase(serializers.ModelSerializer):
|
||||||
''' Serializer: LibraryItem entry. '''
|
''' Serializer: LibraryItem entry full access. '''
|
||||||
class Meta:
|
class Meta:
|
||||||
''' serializer metadata. '''
|
''' serializer metadata. '''
|
||||||
model = LibraryItem
|
model = LibraryItem
|
||||||
|
@ -22,6 +22,15 @@ class LibraryItemSerializer(serializers.ModelSerializer):
|
||||||
read_only_fields = ('id', 'item_type')
|
read_only_fields = ('id', 'item_type')
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryItemSerializer(serializers.ModelSerializer):
|
||||||
|
''' Serializer: LibraryItem entry limited access. '''
|
||||||
|
class Meta:
|
||||||
|
''' serializer metadata. '''
|
||||||
|
model = LibraryItem
|
||||||
|
fields = '__all__'
|
||||||
|
read_only_fields = ('id', 'item_type', 'owner', 'location', 'access_policy')
|
||||||
|
|
||||||
|
|
||||||
class VersionSerializer(serializers.ModelSerializer):
|
class VersionSerializer(serializers.ModelSerializer):
|
||||||
''' Serializer: Version data. '''
|
''' Serializer: Version data. '''
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -164,8 +173,11 @@ class RSFormSerializer(serializers.ModelSerializer):
|
||||||
del result['editors']
|
del result['editors']
|
||||||
|
|
||||||
del result['owner']
|
del result['owner']
|
||||||
del result['is_common']
|
del result['visible']
|
||||||
del result['is_canonical']
|
del result['read_only']
|
||||||
|
del result['access_policy']
|
||||||
|
del result['location']
|
||||||
|
|
||||||
del result['time_create']
|
del result['time_create']
|
||||||
del result['time_update']
|
del result['time_update']
|
||||||
return result
|
return result
|
||||||
|
@ -208,7 +220,7 @@ class RSFormSerializer(serializers.ModelSerializer):
|
||||||
validated_data=new_cst.validated_data
|
validated_data=new_cst.validated_data
|
||||||
)
|
)
|
||||||
|
|
||||||
loaded_item = LibraryItemSerializer(data=data)
|
loaded_item = LibraryItemBase(data=data)
|
||||||
loaded_item.is_valid(raise_exception=True)
|
loaded_item.is_valid(raise_exception=True)
|
||||||
loaded_item.update(
|
loaded_item.update(
|
||||||
instance=cast(LibraryItem, self.instance),
|
instance=cast(LibraryItem, self.instance),
|
||||||
|
@ -325,7 +337,7 @@ class CstListSerializer(serializers.Serializer):
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class LibraryItemCloneSerializer(LibraryItemSerializer):
|
class LibraryItemCloneSerializer(LibraryItemBase):
|
||||||
''' Serializer: LibraryItem cloning. '''
|
''' Serializer: LibraryItem cloning. '''
|
||||||
items = PKField(many=True, required=False, queryset=Constituenta.objects.all())
|
items = PKField(many=True, required=False, queryset=Constituenta.objects.all())
|
||||||
|
|
||||||
|
|
|
@ -109,10 +109,14 @@ class RSFormTRSSerializer(serializers.Serializer):
|
||||||
result = super().to_internal_value(data)
|
result = super().to_internal_value(data)
|
||||||
if 'owner' in data:
|
if 'owner' in data:
|
||||||
result['owner'] = data['owner']
|
result['owner'] = data['owner']
|
||||||
if 'is_common' in data:
|
if 'visible' in data:
|
||||||
result['is_common'] = data['is_common']
|
result['visible'] = data['visible']
|
||||||
if 'is_canonical' in data:
|
if 'read_only' in data:
|
||||||
result['is_canonical'] = data['is_canonical']
|
result['read_only'] = data['read_only']
|
||||||
|
if 'access_policy' in data:
|
||||||
|
result['access_policy'] = data['access_policy']
|
||||||
|
if 'location' in data:
|
||||||
|
result['location'] = data['location']
|
||||||
result['items'] = data.get('items', [])
|
result['items'] = data.get('items', [])
|
||||||
if self.context['load_meta']:
|
if self.context['load_meta']:
|
||||||
result['title'] = data.get('title', 'Без названия')
|
result['title'] = data.get('title', 'Без названия')
|
||||||
|
@ -139,8 +143,10 @@ class RSFormTRSSerializer(serializers.Serializer):
|
||||||
alias=validated_data['alias'],
|
alias=validated_data['alias'],
|
||||||
title=validated_data['title'],
|
title=validated_data['title'],
|
||||||
comment=validated_data['comment'],
|
comment=validated_data['comment'],
|
||||||
is_common=validated_data['is_common'],
|
visible=validated_data['visible'],
|
||||||
is_canonical=validated_data['is_canonical']
|
read_only=validated_data['read_only'],
|
||||||
|
access_policy=validated_data['access_policy'],
|
||||||
|
location=validated_data['location']
|
||||||
)
|
)
|
||||||
self.instance.item.save()
|
self.instance.item.save()
|
||||||
order = 1
|
order = 1
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
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
|
||||||
|
|
||||||
|
from apps.rsform.models import Editor, LibraryItem
|
||||||
from apps.users.models import User
|
from apps.users.models import User
|
||||||
|
|
||||||
|
|
||||||
|
@ -43,6 +44,12 @@ class EndpointTester(APITestCase):
|
||||||
self.user.is_staff = value
|
self.user.is_staff = value
|
||||||
self.user.save()
|
self.user.save()
|
||||||
|
|
||||||
|
def toggle_editor(self, item: LibraryItem, value: bool = True):
|
||||||
|
if value:
|
||||||
|
Editor.add(item, self.user)
|
||||||
|
else:
|
||||||
|
Editor.remove(item, self.user)
|
||||||
|
|
||||||
def login(self):
|
def login(self):
|
||||||
self.client.force_authenticate(user=self.user)
|
self.client.force_authenticate(user=self.user)
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,15 @@
|
||||||
''' Testing models: LibraryItem. '''
|
''' Testing models: LibraryItem. '''
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from apps.rsform.models import LibraryItem, LibraryItemType, Subscription, User
|
from apps.rsform.models import (
|
||||||
|
AccessPolicy,
|
||||||
|
LibraryItem,
|
||||||
|
LibraryItemType,
|
||||||
|
LocationHead,
|
||||||
|
Subscription,
|
||||||
|
User,
|
||||||
|
validate_location
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestLibraryItem(TestCase):
|
class TestLibraryItem(TestCase):
|
||||||
|
@ -40,8 +48,10 @@ class TestLibraryItem(TestCase):
|
||||||
self.assertEqual(item.title, 'Test')
|
self.assertEqual(item.title, 'Test')
|
||||||
self.assertEqual(item.alias, '')
|
self.assertEqual(item.alias, '')
|
||||||
self.assertEqual(item.comment, '')
|
self.assertEqual(item.comment, '')
|
||||||
self.assertEqual(item.is_common, False)
|
self.assertEqual(item.visible, True)
|
||||||
self.assertEqual(item.is_canonical, False)
|
self.assertEqual(item.read_only, False)
|
||||||
|
self.assertEqual(item.access_policy, AccessPolicy.PUBLIC)
|
||||||
|
self.assertEqual(item.location, LocationHead.USER)
|
||||||
|
|
||||||
|
|
||||||
def test_create(self):
|
def test_create(self):
|
||||||
|
@ -51,13 +61,39 @@ class TestLibraryItem(TestCase):
|
||||||
owner=self.user1,
|
owner=self.user1,
|
||||||
alias='KS1',
|
alias='KS1',
|
||||||
comment='Test comment',
|
comment='Test comment',
|
||||||
is_common=True,
|
location=LocationHead.COMMON
|
||||||
is_canonical=True
|
|
||||||
)
|
)
|
||||||
self.assertEqual(item.owner, self.user1)
|
self.assertEqual(item.owner, self.user1)
|
||||||
self.assertEqual(item.title, 'Test')
|
self.assertEqual(item.title, 'Test')
|
||||||
self.assertEqual(item.alias, 'KS1')
|
self.assertEqual(item.alias, 'KS1')
|
||||||
self.assertEqual(item.comment, 'Test comment')
|
self.assertEqual(item.comment, 'Test comment')
|
||||||
self.assertEqual(item.is_common, True)
|
self.assertEqual(item.location, LocationHead.COMMON)
|
||||||
self.assertEqual(item.is_canonical, True)
|
|
||||||
self.assertTrue(Subscription.objects.filter(user=item.owner, item=item).exists())
|
self.assertTrue(Subscription.objects.filter(user=item.owner, item=item).exists())
|
||||||
|
|
||||||
|
|
||||||
|
class TestLocation(TestCase):
|
||||||
|
''' Testing Location model. '''
|
||||||
|
|
||||||
|
def test_validate_location(self):
|
||||||
|
self.assertFalse(validate_location(''))
|
||||||
|
self.assertFalse(validate_location('/A'))
|
||||||
|
self.assertFalse(validate_location('U/U'))
|
||||||
|
self.assertFalse(validate_location('/U/'))
|
||||||
|
self.assertFalse(validate_location('/U/user@mail'))
|
||||||
|
self.assertFalse(validate_location('/U/u\\asdf'))
|
||||||
|
self.assertFalse(validate_location('/U/ asdf'))
|
||||||
|
self.assertFalse(validate_location('/User'))
|
||||||
|
self.assertFalse(validate_location('//'))
|
||||||
|
self.assertFalse(validate_location('/S/1/'))
|
||||||
|
self.assertFalse(validate_location('/S/1 '))
|
||||||
|
self.assertFalse(validate_location('/S/1/2 /3'))
|
||||||
|
|
||||||
|
self.assertTrue(validate_location('/P'))
|
||||||
|
self.assertTrue(validate_location('/L'))
|
||||||
|
self.assertTrue(validate_location('/U'))
|
||||||
|
self.assertTrue(validate_location('/S'))
|
||||||
|
self.assertTrue(validate_location('/S/1'))
|
||||||
|
self.assertTrue(validate_location('/S/12'))
|
||||||
|
self.assertTrue(validate_location('/S/12/3'))
|
||||||
|
self.assertTrue(validate_location('/S/Вася пупки'))
|
||||||
|
self.assertTrue(validate_location('/S/1/!asdf/тест тест'))
|
||||||
|
|
|
@ -2,10 +2,12 @@
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from apps.rsform.models import (
|
from apps.rsform.models import (
|
||||||
|
AccessPolicy,
|
||||||
Editor,
|
Editor,
|
||||||
LibraryItem,
|
LibraryItem,
|
||||||
LibraryItemType,
|
LibraryItemType,
|
||||||
LibraryTemplate,
|
LibraryTemplate,
|
||||||
|
LocationHead,
|
||||||
RSForm,
|
RSForm,
|
||||||
Subscription
|
Subscription
|
||||||
)
|
)
|
||||||
|
@ -35,7 +37,7 @@ class TestLibraryViewset(EndpointTester):
|
||||||
item_type=LibraryItemType.RSFORM,
|
item_type=LibraryItemType.RSFORM,
|
||||||
title='Test3',
|
title='Test3',
|
||||||
alias='T3',
|
alias='T3',
|
||||||
is_common=True
|
location=LocationHead.COMMON
|
||||||
)
|
)
|
||||||
self.invalid_user = 1337 + self.user2.pk
|
self.invalid_user = 1337 + self.user2.pk
|
||||||
self.invalid_item = 1337 + self.common.pk
|
self.invalid_item = 1337 + self.common.pk
|
||||||
|
@ -55,15 +57,37 @@ class TestLibraryViewset(EndpointTester):
|
||||||
|
|
||||||
@decl_endpoint('/api/library/{item}', method='patch')
|
@decl_endpoint('/api/library/{item}', method='patch')
|
||||||
def test_update(self):
|
def test_update(self):
|
||||||
data = {'id': self.unowned.pk, 'title': 'New title'}
|
data = {'id': self.unowned.pk, 'title': 'New Title'}
|
||||||
self.executeNotFound(data, item=self.invalid_item)
|
self.executeNotFound(data, item=self.invalid_item)
|
||||||
self.executeForbidden(data, item=self.unowned.pk)
|
self.executeForbidden(data, item=self.unowned.pk)
|
||||||
|
|
||||||
data = {'id': self.owned.pk, 'title': 'New title'}
|
self.toggle_editor(self.unowned, True)
|
||||||
|
response = self.executeOK(data, item=self.unowned.pk)
|
||||||
|
self.assertEqual(response.data['title'], data['title'])
|
||||||
|
|
||||||
|
self.unowned.access_policy = AccessPolicy.PRIVATE
|
||||||
|
self.unowned.save()
|
||||||
|
self.executeForbidden(data, item=self.unowned.pk)
|
||||||
|
|
||||||
|
data = {'id': self.owned.pk, 'title': 'New Title'}
|
||||||
response = self.executeOK(data, item=self.owned.pk)
|
response = self.executeOK(data, item=self.owned.pk)
|
||||||
self.assertEqual(response.data['title'], 'New title')
|
self.assertEqual(response.data['title'], data['title'])
|
||||||
self.assertEqual(response.data['alias'], self.owned.alias)
|
self.assertEqual(response.data['alias'], self.owned.alias)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'id': self.owned.pk,
|
||||||
|
'title': 'Another Title',
|
||||||
|
'owner': self.user2.pk,
|
||||||
|
'access_policy': AccessPolicy.PROTECTED,
|
||||||
|
'location': LocationHead.LIBRARY
|
||||||
|
}
|
||||||
|
response = self.executeOK(data, item=self.owned.pk)
|
||||||
|
self.assertEqual(response.data['title'], data['title'])
|
||||||
|
self.assertEqual(response.data['owner'], self.owned.owner.pk)
|
||||||
|
self.assertEqual(response.data['access_policy'], self.owned.access_policy)
|
||||||
|
self.assertEqual(response.data['location'], self.owned.location)
|
||||||
|
self.assertNotEqual(response.data['location'], LocationHead.LIBRARY)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/library/{item}/set-owner', method='patch')
|
@decl_endpoint('/api/library/{item}/set-owner', method='patch')
|
||||||
def test_set_owner(self):
|
def test_set_owner(self):
|
||||||
|
@ -89,6 +113,59 @@ class TestLibraryViewset(EndpointTester):
|
||||||
self.owned.refresh_from_db()
|
self.owned.refresh_from_db()
|
||||||
self.assertEqual(self.owned.owner, self.user)
|
self.assertEqual(self.owned.owner, self.user)
|
||||||
|
|
||||||
|
@decl_endpoint('/api/library/{item}/set-access-policy', method='patch')
|
||||||
|
def test_set_access_policy(self):
|
||||||
|
time_update = self.owned.time_update
|
||||||
|
|
||||||
|
data = {'access_policy': 'invalid'}
|
||||||
|
self.executeBadData(data, item=self.owned.pk)
|
||||||
|
|
||||||
|
data = {'access_policy': AccessPolicy.PRIVATE}
|
||||||
|
self.executeNotFound(data, item=self.invalid_item)
|
||||||
|
self.executeForbidden(data, item=self.unowned.pk)
|
||||||
|
self.executeOK(data, item=self.owned.pk)
|
||||||
|
self.owned.refresh_from_db()
|
||||||
|
self.assertEqual(self.owned.access_policy, data['access_policy'])
|
||||||
|
|
||||||
|
self.toggle_editor(self.unowned, True)
|
||||||
|
self.executeForbidden(data, item=self.unowned.pk)
|
||||||
|
|
||||||
|
self.toggle_admin(True)
|
||||||
|
self.executeOK(data, item=self.unowned.pk)
|
||||||
|
self.unowned.refresh_from_db()
|
||||||
|
self.assertEqual(self.unowned.access_policy, data['access_policy'])
|
||||||
|
|
||||||
|
@decl_endpoint('/api/library/{item}/set-location', method='patch')
|
||||||
|
def test_set_location(self):
|
||||||
|
time_update = self.owned.time_update
|
||||||
|
|
||||||
|
data = {'location': 'invalid'}
|
||||||
|
self.executeBadData(data, item=self.owned.pk)
|
||||||
|
|
||||||
|
data = {'location': '/U/temp'}
|
||||||
|
self.executeNotFound(data, item=self.invalid_item)
|
||||||
|
self.executeForbidden(data, item=self.unowned.pk)
|
||||||
|
self.executeOK(data, item=self.owned.pk)
|
||||||
|
self.owned.refresh_from_db()
|
||||||
|
self.assertEqual(self.owned.location, data['location'])
|
||||||
|
|
||||||
|
data = {'location': LocationHead.LIBRARY}
|
||||||
|
self.executeForbidden(data, item=self.owned.pk)
|
||||||
|
|
||||||
|
data = {'location': '/U/temp'}
|
||||||
|
self.toggle_editor(self.unowned, True)
|
||||||
|
self.executeForbidden(data, item=self.unowned.pk)
|
||||||
|
|
||||||
|
self.toggle_admin(True)
|
||||||
|
data = {'location': LocationHead.LIBRARY}
|
||||||
|
self.executeOK(data, item=self.owned.pk)
|
||||||
|
self.owned.refresh_from_db()
|
||||||
|
self.assertEqual(self.owned.location, data['location'])
|
||||||
|
|
||||||
|
self.executeOK(data, item=self.unowned.pk)
|
||||||
|
self.unowned.refresh_from_db()
|
||||||
|
self.assertEqual(self.unowned.location, data['location'])
|
||||||
|
|
||||||
@decl_endpoint('/api/library/{item}/editors-add', method='patch')
|
@decl_endpoint('/api/library/{item}/editors-add', method='patch')
|
||||||
def test_add_editor(self):
|
def test_add_editor(self):
|
||||||
time_update = self.owned.time_update
|
time_update = self.owned.time_update
|
||||||
|
|
|
@ -6,7 +6,15 @@ from zipfile import ZipFile
|
||||||
from cctext import ReferenceType
|
from cctext import ReferenceType
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from apps.rsform.models import Constituenta, CstType, LibraryItem, LibraryItemType, RSForm
|
from apps.rsform.models import (
|
||||||
|
AccessPolicy,
|
||||||
|
Constituenta,
|
||||||
|
CstType,
|
||||||
|
LibraryItem,
|
||||||
|
LibraryItemType,
|
||||||
|
LocationHead,
|
||||||
|
RSForm
|
||||||
|
)
|
||||||
|
|
||||||
from ..EndpointTester import EndpointTester, decl_endpoint
|
from ..EndpointTester import EndpointTester, decl_endpoint
|
||||||
from ..testing_utils import response_contains
|
from ..testing_utils import response_contains
|
||||||
|
@ -38,12 +46,21 @@ class TestRSFormViewset(EndpointTester):
|
||||||
|
|
||||||
@decl_endpoint('/api/rsforms/create-detailed', method='post')
|
@decl_endpoint('/api/rsforms/create-detailed', method='post')
|
||||||
def test_create_rsform_json(self):
|
def test_create_rsform_json(self):
|
||||||
data = {'title': 'Test123', 'comment': '123', 'alias': 'ks1'}
|
data = {
|
||||||
|
'title': 'Test123',
|
||||||
|
'comment': '123',
|
||||||
|
'alias': 'ks1',
|
||||||
|
'location': LocationHead.PROJECTS,
|
||||||
|
'access_policy': AccessPolicy.PROTECTED,
|
||||||
|
'visible': False
|
||||||
|
}
|
||||||
response = self.executeCreated(data)
|
response = self.executeCreated(data)
|
||||||
self.assertEqual(response.data['owner'], self.user.pk)
|
self.assertEqual(response.data['owner'], self.user.pk)
|
||||||
self.assertEqual(response.data['title'], 'Test123')
|
self.assertEqual(response.data['title'], data['title'])
|
||||||
self.assertEqual(response.data['alias'], 'ks1')
|
self.assertEqual(response.data['alias'], data['alias'])
|
||||||
self.assertEqual(response.data['comment'], '123')
|
self.assertEqual(response.data['location'], data['location'])
|
||||||
|
self.assertEqual(response.data['access_policy'], data['access_policy'])
|
||||||
|
self.assertEqual(response.data['visible'], data['visible'])
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/rsforms', method='get')
|
@decl_endpoint('/api/rsforms', method='get')
|
||||||
|
|
|
@ -27,12 +27,26 @@ class LibraryActiveView(generics.ListAPIView):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
if self.request.user.is_anonymous:
|
if self.request.user.is_anonymous:
|
||||||
return m.LibraryItem.objects.filter(is_common=True).order_by('-time_update')
|
return m.LibraryItem.objects.filter(
|
||||||
|
Q(access_policy=m.AccessPolicy.PUBLIC),
|
||||||
|
).filter(
|
||||||
|
Q(location__startswith=m.LocationHead.COMMON) |
|
||||||
|
Q(location__startswith=m.LocationHead.LIBRARY)
|
||||||
|
).order_by('-time_update')
|
||||||
else:
|
else:
|
||||||
user = cast(m.User, self.request.user)
|
user = cast(m.User, self.request.user)
|
||||||
# pylint: disable=unsupported-binary-operation
|
# pylint: disable=unsupported-binary-operation
|
||||||
return m.LibraryItem.objects.filter(
|
return m.LibraryItem.objects.filter(
|
||||||
Q(is_common=True) | Q(owner=user) | Q(subscription__user=user)
|
(
|
||||||
|
Q(access_policy=m.AccessPolicy.PUBLIC) &
|
||||||
|
(
|
||||||
|
Q(location__startswith=m.LocationHead.COMMON) |
|
||||||
|
Q(location__startswith=m.LocationHead.LIBRARY)
|
||||||
|
)
|
||||||
|
) |
|
||||||
|
Q(owner=user) |
|
||||||
|
Q(editor__editor=user) |
|
||||||
|
Q(subscription__user=user)
|
||||||
).distinct().order_by('-time_update')
|
).distinct().order_by('-time_update')
|
||||||
|
|
||||||
|
|
||||||
|
@ -68,8 +82,8 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = s.LibraryItemSerializer
|
serializer_class = s.LibraryItemSerializer
|
||||||
|
|
||||||
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
|
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
|
||||||
filterset_fields = ['item_type', 'owner', 'is_common', 'is_canonical']
|
filterset_fields = ['item_type', 'owner']
|
||||||
ordering_fields = ('item_type', 'owner', 'title', 'time_update')
|
ordering_fields = ('item_type', 'owner', 'alias', 'title', 'time_update')
|
||||||
ordering = '-time_update'
|
ordering = '-time_update'
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
|
@ -82,7 +96,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
||||||
if self.action in ['update', 'partial_update']:
|
if self.action in ['update', 'partial_update']:
|
||||||
permission_list = [permissions.ItemEditor]
|
permission_list = [permissions.ItemEditor]
|
||||||
elif self.action in [
|
elif self.action in [
|
||||||
'destroy', 'set_owner',
|
'destroy', 'set_owner', 'set_access_policy', 'set_location',
|
||||||
'editors_add', 'editors_remove', 'editors_set'
|
'editors_add', 'editors_remove', 'editors_set'
|
||||||
]:
|
]:
|
||||||
permission_list = [permissions.ItemOwner]
|
permission_list = [permissions.ItemOwner]
|
||||||
|
@ -119,12 +133,13 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
||||||
clone.title = serializer.validated_data['title']
|
clone.title = serializer.validated_data['title']
|
||||||
clone.alias = serializer.validated_data.get('alias', '')
|
clone.alias = serializer.validated_data.get('alias', '')
|
||||||
clone.comment = serializer.validated_data.get('comment', '')
|
clone.comment = serializer.validated_data.get('comment', '')
|
||||||
clone.is_common = serializer.validated_data.get('is_common', False)
|
clone.visible = serializer.validated_data.get('visible', True)
|
||||||
clone.is_canonical = False
|
clone.read_only = False
|
||||||
|
clone.access_policy = serializer.validated_data.get('access_policy', m.AccessPolicy.PUBLIC)
|
||||||
if clone.item_type == m.LibraryItemType.RSFORM:
|
clone.location = serializer.validated_data.get('location', m.LocationHead.USER)
|
||||||
clone.save()
|
clone.save()
|
||||||
|
|
||||||
|
if clone.item_type == m.LibraryItemType.RSFORM:
|
||||||
need_filter = 'items' in request.data
|
need_filter = 'items' in request.data
|
||||||
for cst in m.RSForm(item).constituents():
|
for cst in m.RSForm(item).constituents():
|
||||||
if not need_filter or cst.pk in request.data['items']:
|
if not need_filter or cst.pk in request.data['items']:
|
||||||
|
@ -191,6 +206,49 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
||||||
m.LibraryItem.objects.filter(pk=item.pk).update(owner=new_owner)
|
m.LibraryItem.objects.filter(pk=item.pk).update(owner=new_owner)
|
||||||
return Response(status=c.HTTP_200_OK)
|
return Response(status=c.HTTP_200_OK)
|
||||||
|
|
||||||
|
@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)
|
||||||
|
m.LibraryItem.objects.filter(pk=item.pk).update(access_policy=serializer.validated_data['access_policy'])
|
||||||
|
return Response(status=c.HTTP_200_OK)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
summary='set location for item',
|
||||||
|
tags=['Library'],
|
||||||
|
request=s.LocationSerializer,
|
||||||
|
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-location')
|
||||||
|
def set_location(self, request: Request, pk):
|
||||||
|
''' Endpoint: Set item location. '''
|
||||||
|
item = self._get_item()
|
||||||
|
serializer = s.LocationSerializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
location: str = serializer.validated_data['location']
|
||||||
|
if location.startswith(m.LocationHead.LIBRARY) and not self.request.user.is_staff:
|
||||||
|
return Response(status=c.HTTP_403_FORBIDDEN)
|
||||||
|
m.LibraryItem.objects.filter(pk=item.pk).update(location=location)
|
||||||
|
return Response(status=c.HTTP_200_OK)
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
summary='add editor for item',
|
summary='add editor for item',
|
||||||
tags=['Library'],
|
tags=['Library'],
|
||||||
|
|
|
@ -447,15 +447,17 @@ def create_rsform(request: Request):
|
||||||
''' Endpoint: Create RSForm from user input and/or trs file. '''
|
''' Endpoint: Create RSForm from user input and/or trs file. '''
|
||||||
owner = cast(m.User, request.user) if not request.user.is_anonymous else None
|
owner = cast(m.User, request.user) if not request.user.is_anonymous else None
|
||||||
if 'file' not in request.FILES:
|
if 'file' not in request.FILES:
|
||||||
serializer = s.LibraryItemSerializer(data=request.data)
|
serializer = s.LibraryItemBase(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
schema = m.RSForm.create(
|
schema = m.RSForm.create(
|
||||||
title=serializer.validated_data['title'],
|
title=serializer.validated_data['title'],
|
||||||
owner=owner,
|
owner=owner,
|
||||||
alias=serializer.validated_data.get('alias', ''),
|
alias=serializer.validated_data.get('alias', ''),
|
||||||
comment=serializer.validated_data.get('comment', ''),
|
comment=serializer.validated_data.get('comment', ''),
|
||||||
is_common=serializer.validated_data.get('is_common', False),
|
visible=serializer.validated_data.get('visible', True),
|
||||||
is_canonical=serializer.validated_data.get('is_canonical', False),
|
read_only=serializer.validated_data.get('read_only', False),
|
||||||
|
access_policy=serializer.validated_data.get('access_policy', m.AccessPolicy.PUBLIC),
|
||||||
|
location=serializer.validated_data.get('location', m.LocationHead.USER),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
data = utils.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME)
|
data = utils.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME)
|
||||||
|
@ -481,12 +483,15 @@ def _prepare_rsform_data(data: dict, request: Request, owner: Union[m.User, None
|
||||||
if 'comment' in request.data and request.data['comment'] != '':
|
if 'comment' in request.data and request.data['comment'] != '':
|
||||||
data['comment'] = request.data['comment']
|
data['comment'] = request.data['comment']
|
||||||
|
|
||||||
is_common = True
|
visible = True
|
||||||
if 'is_common' in request.data:
|
if 'visible' in request.data:
|
||||||
is_common = request.data['is_common'] == 'true'
|
visible = request.data['visible'] == 'true'
|
||||||
data['is_common'] = is_common
|
data['visible'] = visible
|
||||||
|
|
||||||
is_canonical = False
|
read_only = False
|
||||||
if 'is_canonical' in request.data:
|
if 'read_only' in request.data:
|
||||||
is_canonical = request.data['is_canonical'] == 'true'
|
read_only = request.data['read_only'] == 'true'
|
||||||
data['is_canonical'] = is_canonical
|
data['read_only'] = read_only
|
||||||
|
|
||||||
|
data['access_policy'] = request.data.get('access_policy', m.AccessPolicy.PUBLIC)
|
||||||
|
data['location'] = request.data.get('location', m.LocationHead.USER)
|
||||||
|
|
|
@ -11,7 +11,7 @@ function ApplicationLayout() {
|
||||||
const { viewportHeight, mainHeight, showScroll } = useConceptOptions();
|
const { viewportHeight, mainHeight, showScroll } = useConceptOptions();
|
||||||
return (
|
return (
|
||||||
<NavigationState>
|
<NavigationState>
|
||||||
<div className='min-w-[20rem] overflow-x-auto max-w-[100vw] clr-app antialiased'>
|
<div className='min-w-[20rem] clr-app antialiased'>
|
||||||
<ConceptToaster
|
<ConceptToaster
|
||||||
className='mt-[4rem] text-sm' // prettier: split lines
|
className='mt-[4rem] text-sm' // prettier: split lines
|
||||||
autoClose={3000}
|
autoClose={3000}
|
||||||
|
@ -23,13 +23,13 @@ function ApplicationLayout() {
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id={globals.main_scroll}
|
id={globals.main_scroll}
|
||||||
className='cc-scroll-y min-w-fit'
|
className='cc-scroll-y flex flex-col items-start overflow-x-auto max-w-[100vw]'
|
||||||
style={{
|
style={{
|
||||||
maxHeight: viewportHeight,
|
maxHeight: viewportHeight,
|
||||||
overflowY: showScroll ? 'scroll' : 'auto'
|
overflowY: showScroll ? 'scroll' : 'auto'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<main className='flex flex-col items-center' style={{ minHeight: mainHeight }}>
|
<main className='flex flex-col items-center w-full' style={{ minHeight: mainHeight }}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|
|
@ -15,6 +15,7 @@ function Footer() {
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'z-navigation',
|
'z-navigation',
|
||||||
|
'w-full',
|
||||||
'sm:px-4 sm:py-2 flex flex-col items-center gap-1',
|
'sm:px-4 sm:py-2 flex flex-col items-center gap-1',
|
||||||
'text-xs sm:text-sm select-none whitespace-nowrap'
|
'text-xs sm:text-sm select-none whitespace-nowrap'
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { createBrowserRouter } from 'react-router-dom';
|
import { createBrowserRouter } from 'react-router-dom';
|
||||||
|
|
||||||
import CreateRSFormPage from '@/pages/CreateRSFormPage';
|
import CreateItemPage from '@/pages/CreateRSFormPage';
|
||||||
import HomePage from '@/pages/HomePage';
|
import HomePage from '@/pages/HomePage';
|
||||||
import LibraryPage from '@/pages/LibraryPage';
|
import LibraryPage from '@/pages/LibraryPage';
|
||||||
import LoginPage from '@/pages/LoginPage';
|
import LoginPage from '@/pages/LoginPage';
|
||||||
|
@ -51,7 +51,7 @@ export const Router = createBrowserRouter([
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: routes.create_schema,
|
path: routes.create_schema,
|
||||||
element: <CreateRSFormPage />
|
element: <CreateItemPage />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${routes.rsforms}/:id`,
|
path: `${routes.rsforms}/:id`,
|
||||||
|
|
|
@ -7,7 +7,8 @@ import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import { type ErrorData } from '@/components/info/InfoError';
|
import { type ErrorData } from '@/components/info/InfoError';
|
||||||
import { ILexemeData, IResolutionData, ITextRequest, ITextResult, IWordFormPlain } from '@/models/language';
|
import { ILexemeData, IResolutionData, ITextRequest, ITextResult, IWordFormPlain } from '@/models/language';
|
||||||
import { ILibraryItem, ILibraryUpdateData, IVersionData } from '@/models/library';
|
import { ILibraryItem, ILibraryUpdateData, ITargetAccessPolicy, ITargetLocation, IVersionData } from '@/models/library';
|
||||||
|
import { ILibraryCreateData } from '@/models/library';
|
||||||
import {
|
import {
|
||||||
IConstituentaList,
|
IConstituentaList,
|
||||||
IConstituentaMeta,
|
IConstituentaMeta,
|
||||||
|
@ -20,7 +21,6 @@ import {
|
||||||
IInlineSynthesisData,
|
IInlineSynthesisData,
|
||||||
IProduceStructureResponse,
|
IProduceStructureResponse,
|
||||||
IRSFormCloneData,
|
IRSFormCloneData,
|
||||||
IRSFormCreateData,
|
|
||||||
IRSFormData,
|
IRSFormData,
|
||||||
IRSFormUploadData,
|
IRSFormUploadData,
|
||||||
ITargetCst,
|
ITargetCst,
|
||||||
|
@ -199,7 +199,7 @@ export function getTemplates(request: FrontPull<ILibraryItem[]>) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function postNewRSForm(request: FrontExchange<IRSFormCreateData, ILibraryItem>) {
|
export function postNewRSForm(request: FrontExchange<ILibraryCreateData, ILibraryItem>) {
|
||||||
AxiosPost({
|
AxiosPost({
|
||||||
endpoint: '/api/rsforms/create-detailed',
|
endpoint: '/api/rsforms/create-detailed',
|
||||||
request: request,
|
request: request,
|
||||||
|
@ -253,6 +253,20 @@ export function patchSetOwner(target: string, request: FrontPush<ITargetUser>) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function patchSetAccessPolicy(target: string, request: FrontPush<ITargetAccessPolicy>) {
|
||||||
|
AxiosPatch({
|
||||||
|
endpoint: `/api/library/${target}/set-access-policy`,
|
||||||
|
request: request
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function patchSetLocation(target: string, request: FrontPush<ITargetLocation>) {
|
||||||
|
AxiosPatch({
|
||||||
|
endpoint: `/api/library/${target}/set-location`,
|
||||||
|
request: request
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function patchEditorsAdd(target: string, request: FrontPush<ITargetUser>) {
|
export function patchEditorsAdd(target: string, request: FrontPush<ITargetUser>) {
|
||||||
AxiosPatch({
|
AxiosPatch({
|
||||||
endpoint: `/api/library/${target}/editors-add`,
|
endpoint: `/api/library/${target}/editors-add`,
|
||||||
|
|
104
rsconcept/frontend/src/components/DomainIcons.tsx
Normal file
104
rsconcept/frontend/src/components/DomainIcons.tsx
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
import { AccessPolicy, LocationHead } from '@/models/library';
|
||||||
|
import { CstMatchMode, DependencyMode } from '@/models/miscellaneous';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IconAlias,
|
||||||
|
IconBusiness,
|
||||||
|
IconFilter,
|
||||||
|
IconFollow,
|
||||||
|
IconFollowOff,
|
||||||
|
IconFormula,
|
||||||
|
IconGraphCollapse,
|
||||||
|
IconGraphExpand,
|
||||||
|
IconGraphInputs,
|
||||||
|
IconGraphOutputs,
|
||||||
|
IconHide,
|
||||||
|
IconPrivate,
|
||||||
|
IconProps,
|
||||||
|
IconProtected,
|
||||||
|
IconPublic,
|
||||||
|
IconSettings,
|
||||||
|
IconShow,
|
||||||
|
IconTemplates,
|
||||||
|
IconTerm,
|
||||||
|
IconText,
|
||||||
|
IconUser
|
||||||
|
} from './Icons';
|
||||||
|
|
||||||
|
export interface DomIconProps<RequestData> extends IconProps {
|
||||||
|
value: RequestData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PolicyIcon({ value, size = '1.25rem', className }: DomIconProps<AccessPolicy>) {
|
||||||
|
switch (value) {
|
||||||
|
case AccessPolicy.PRIVATE:
|
||||||
|
return <IconPrivate size={size} className={className ?? 'clr-text-red'} />;
|
||||||
|
case AccessPolicy.PROTECTED:
|
||||||
|
return <IconProtected size={size} className={className ?? 'clr-text-primary'} />;
|
||||||
|
case AccessPolicy.PUBLIC:
|
||||||
|
return <IconPublic size={size} className={className ?? 'clr-text-green'} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VisibilityIcon({ value, size = '1.25rem', className }: DomIconProps<boolean>) {
|
||||||
|
if (value) {
|
||||||
|
return <IconShow size={size} className={className ?? 'clr-text-green'} />;
|
||||||
|
} else {
|
||||||
|
return <IconHide size={size} className={className ?? 'clr-text-red'} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SubscribeIcon({ value, size = '1.25rem', className }: DomIconProps<boolean>) {
|
||||||
|
if (value) {
|
||||||
|
return <IconFollow size={size} className={className ?? 'clr-text-green'} />;
|
||||||
|
} else {
|
||||||
|
return <IconFollowOff size={size} className={className ?? 'clr-text-red'} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LocationHeadIcon({ value: value, size = '1.25rem', className }: DomIconProps<LocationHead>) {
|
||||||
|
switch (value) {
|
||||||
|
case undefined:
|
||||||
|
return <IconFilter size={size} className={className ?? 'clr-text-primary'} />;
|
||||||
|
case LocationHead.COMMON:
|
||||||
|
return <IconPublic size={size} className={className ?? 'clr-text-primary'} />;
|
||||||
|
case LocationHead.LIBRARY:
|
||||||
|
return <IconTemplates size={size} className={className ?? 'clr-text-primary'} />;
|
||||||
|
case LocationHead.PROJECTS:
|
||||||
|
return <IconBusiness size={size} className={className ?? 'clr-text-primary'} />;
|
||||||
|
case LocationHead.USER:
|
||||||
|
return <IconUser size={size} className={className ?? 'clr-text-primary'} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DependencyIcon(mode: DependencyMode, size: string, color?: string) {
|
||||||
|
switch (mode) {
|
||||||
|
case DependencyMode.ALL:
|
||||||
|
return <IconSettings size={size} className={color} />;
|
||||||
|
case DependencyMode.EXPRESSION:
|
||||||
|
return <IconText size={size} className={color} />;
|
||||||
|
case DependencyMode.OUTPUTS:
|
||||||
|
return <IconGraphOutputs size={size} className={color} />;
|
||||||
|
case DependencyMode.INPUTS:
|
||||||
|
return <IconGraphInputs size={size} className={color} />;
|
||||||
|
case DependencyMode.EXPAND_OUTPUTS:
|
||||||
|
return <IconGraphExpand size={size} className={color} />;
|
||||||
|
case DependencyMode.EXPAND_INPUTS:
|
||||||
|
return <IconGraphCollapse size={size} className={color} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MatchModeIcon(mode: CstMatchMode, size: string, color?: string) {
|
||||||
|
switch (mode) {
|
||||||
|
case CstMatchMode.ALL:
|
||||||
|
return <IconFilter size={size} className={color} />;
|
||||||
|
case CstMatchMode.TEXT:
|
||||||
|
return <IconText size={size} className={color} />;
|
||||||
|
case CstMatchMode.EXPR:
|
||||||
|
return <IconFormula size={size} className={color} />;
|
||||||
|
case CstMatchMode.TERM:
|
||||||
|
return <IconTerm size={size} className={color} />;
|
||||||
|
case CstMatchMode.NAME:
|
||||||
|
return <IconAlias size={size} className={color} />;
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,8 @@ export { BiSearchAlt2 as IconSearch } from 'react-icons/bi';
|
||||||
export { BiDownload as IconDownload } from 'react-icons/bi';
|
export { BiDownload as IconDownload } from 'react-icons/bi';
|
||||||
export { BiUpload as IconUpload } from 'react-icons/bi';
|
export { BiUpload as IconUpload } from 'react-icons/bi';
|
||||||
export { BiCog as IconSettings } from 'react-icons/bi';
|
export { BiCog as IconSettings } from 'react-icons/bi';
|
||||||
|
export { TbEye as IconShow } from 'react-icons/tb';
|
||||||
|
export { TbEyeX as IconHide } from 'react-icons/tb';
|
||||||
export { BiShareAlt as IconShare } from 'react-icons/bi';
|
export { BiShareAlt as IconShare } from 'react-icons/bi';
|
||||||
export { BiFilterAlt as IconFilter } from 'react-icons/bi';
|
export { BiFilterAlt as IconFilter } from 'react-icons/bi';
|
||||||
export {BiDownArrowCircle as IconOpenList } from 'react-icons/bi';
|
export {BiDownArrowCircle as IconOpenList } from 'react-icons/bi';
|
||||||
|
@ -28,6 +30,7 @@ export { RiMenuFoldFill as IconMenuFold } from 'react-icons/ri';
|
||||||
export { RiMenuUnfoldFill as IconMenuUnfold } from 'react-icons/ri';
|
export { RiMenuUnfoldFill as IconMenuUnfold } from 'react-icons/ri';
|
||||||
export { LuMoon as IconDarkTheme } from 'react-icons/lu';
|
export { LuMoon as IconDarkTheme } from 'react-icons/lu';
|
||||||
export { LuSun as IconLightTheme } from 'react-icons/lu';
|
export { LuSun as IconLightTheme } from 'react-icons/lu';
|
||||||
|
export { FaRegFolder as IconFolder } from 'react-icons/fa';
|
||||||
export { LuLightbulb as IconHelp } from 'react-icons/lu';
|
export { LuLightbulb as IconHelp } from 'react-icons/lu';
|
||||||
export { LuLightbulbOff as IconHelpOff } from 'react-icons/lu';
|
export { LuLightbulbOff as IconHelpOff } from 'react-icons/lu';
|
||||||
export { RiPushpinFill as IconPin } from 'react-icons/ri';
|
export { RiPushpinFill as IconPin } from 'react-icons/ri';
|
||||||
|
@ -42,13 +45,14 @@ export { BiLastPage as IconPageLast } from 'react-icons/bi';
|
||||||
// ==== User status =======
|
// ==== User status =======
|
||||||
export { LuUserCircle2 as IconUser } from 'react-icons/lu';
|
export { LuUserCircle2 as IconUser } from 'react-icons/lu';
|
||||||
export { FaCircleUser as IconUser2 } from 'react-icons/fa6';
|
export { FaCircleUser as IconUser2 } from 'react-icons/fa6';
|
||||||
export { LuShovel as IconEditor } from 'react-icons/lu';
|
export { TbUserEdit as IconEditor } from 'react-icons/tb';
|
||||||
export { LuCrown as IconOwner } from 'react-icons/lu';
|
export { LuCrown as IconOwner } from 'react-icons/lu';
|
||||||
export { TbMeteor as IconAdmin } from 'react-icons/tb';
|
export { TbMeteor as IconAdmin } from 'react-icons/tb';
|
||||||
export { TbMeteorOff as IconAdminOff } from 'react-icons/tb';
|
export { TbMeteorOff as IconAdminOff } from 'react-icons/tb';
|
||||||
export { LuGlasses as IconReader } from 'react-icons/lu';
|
export { LuGlasses as IconReader } from 'react-icons/lu';
|
||||||
|
|
||||||
// ===== Domain entities =======
|
// ===== Domain entities =======
|
||||||
|
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';
|
||||||
|
@ -56,6 +60,7 @@ 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 { LuImage as IconImage } from 'react-icons/lu';
|
export { LuImage as IconImage } from 'react-icons/lu';
|
||||||
export { TbColumns as IconList } from 'react-icons/tb';
|
export { TbColumns as IconList } from 'react-icons/tb';
|
||||||
|
export { ImStack as IconVersions } from 'react-icons/im';
|
||||||
export { TbColumnsOff as IconListOff } from 'react-icons/tb';
|
export { TbColumnsOff as IconListOff } from 'react-icons/tb';
|
||||||
export { LuAtSign as IconTerm } from 'react-icons/lu';
|
export { LuAtSign as IconTerm } from 'react-icons/lu';
|
||||||
export { LuSubscript as IconAlias } from 'react-icons/lu';
|
export { LuSubscript as IconAlias } from 'react-icons/lu';
|
||||||
|
@ -64,8 +69,11 @@ export { BiFontFamily as IconText } from 'react-icons/bi';
|
||||||
export { BiFont as IconTextOff } from 'react-icons/bi';
|
export { BiFont as IconTextOff } from 'react-icons/bi';
|
||||||
export { RiTreeLine as IconTree } from 'react-icons/ri';
|
export { RiTreeLine as IconTree } from 'react-icons/ri';
|
||||||
export { FaRegKeyboard as IconControls } from 'react-icons/fa6';
|
export { FaRegKeyboard as IconControls } from 'react-icons/fa6';
|
||||||
export { BiCheckShield as IconImmutable } from 'react-icons/bi';
|
export { RiLockLine as IconImmutable } from 'react-icons/ri';
|
||||||
|
export { RiLockUnlockLine as IconMutable } from 'react-icons/ri';
|
||||||
export { RiOpenSourceLine as IconPublic } from 'react-icons/ri';
|
export { RiOpenSourceLine as IconPublic } from 'react-icons/ri';
|
||||||
|
export { RiShieldLine as IconProtected } from 'react-icons/ri';
|
||||||
|
export { RiShieldKeyholeLine as IconPrivate } from 'react-icons/ri';
|
||||||
export { BiBug as IconStatusError } from 'react-icons/bi';
|
export { BiBug as IconStatusError } from 'react-icons/bi';
|
||||||
export { BiCheckCircle as IconStatusOK } from 'react-icons/bi';
|
export { BiCheckCircle as IconStatusOK } from 'react-icons/bi';
|
||||||
export { BiHelpCircle as IconStatusUnknown } from 'react-icons/bi';
|
export { BiHelpCircle as IconStatusUnknown } from 'react-icons/bi';
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import TextURL from '@/components/ui/TextURL';
|
import TextURL from '@/components/ui/TextURL';
|
||||||
import Tooltip, { PlacesType } from '@/components/ui/Tooltip';
|
import Tooltip, { PlacesType } from '@/components/ui/Tooltip';
|
||||||
import { useConceptOptions } from '@/context/OptionsContext';
|
import { useConceptOptions } from '@/context/OptionsContext';
|
||||||
|
@ -10,19 +12,20 @@ import { CProps } from '../props';
|
||||||
interface BadgeHelpProps extends CProps.Styling {
|
interface BadgeHelpProps extends CProps.Styling {
|
||||||
topic: HelpTopic;
|
topic: HelpTopic;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
|
padding?: string;
|
||||||
place?: PlacesType;
|
place?: PlacesType;
|
||||||
}
|
}
|
||||||
|
|
||||||
function BadgeHelp({ topic, ...restProps }: BadgeHelpProps) {
|
function BadgeHelp({ topic, padding, ...restProps }: BadgeHelpProps) {
|
||||||
const { showHelp } = useConceptOptions();
|
const { showHelp } = useConceptOptions();
|
||||||
|
|
||||||
if (!showHelp) {
|
if (!showHelp) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div tabIndex={-1} id={`help-${topic}`} className='p-1'>
|
<div tabIndex={-1} id={`help-${topic}`} className={clsx('p-1', padding)}>
|
||||||
<IconHelp size='1.25rem' className='icon-primary' />
|
<IconHelp size='1.25rem' className='icon-primary' />
|
||||||
<Tooltip clickable anchorSelect={`#help-${topic}`} layer='z-modal-tooltip' {...restProps}>
|
<Tooltip clickable anchorSelect={`#help-${topic}`} layer='z-modalTooltip' {...restProps}>
|
||||||
<div className='relative' onClick={event => event.stopPropagation()}>
|
<div className='relative' onClick={event => event.stopPropagation()}>
|
||||||
<div className='absolute right-0 text-sm top-[0.4rem] clr-input'>
|
<div className='absolute right-0 text-sm top-[0.4rem] clr-input'>
|
||||||
<TextURL text='Справка...' href={`/manuals?topic=${topic}`} />
|
<TextURL text='Справка...' href={`/manuals?topic=${topic}`} />
|
||||||
|
|
|
@ -9,7 +9,7 @@ interface ConstituentaTooltipProps {
|
||||||
|
|
||||||
function ConstituentaTooltip({ data, anchor }: ConstituentaTooltipProps) {
|
function ConstituentaTooltip({ data, anchor }: ConstituentaTooltipProps) {
|
||||||
return (
|
return (
|
||||||
<Tooltip clickable layer='z-modal-tooltip' anchorSelect={anchor} className='max-w-[30rem]'>
|
<Tooltip clickable layer='z-modalTooltip' anchorSelect={anchor} className='max-w-[30rem]'>
|
||||||
<InfoConstituenta data={data} onClick={event => event.stopPropagation()} />
|
<InfoConstituenta data={data} onClick={event => event.stopPropagation()} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import Dropdown from '@/components/ui/Dropdown';
|
||||||
|
import useDropdown from '@/hooks/useDropdown';
|
||||||
|
import { AccessPolicy } from '@/models/library';
|
||||||
|
import { prefixes } from '@/utils/constants';
|
||||||
|
import { describeAccessPolicy, labelAccessPolicy } from '@/utils/labels';
|
||||||
|
|
||||||
|
import { PolicyIcon } from '../DomainIcons';
|
||||||
|
import DropdownButton from '../ui/DropdownButton';
|
||||||
|
import MiniButton from '../ui/MiniButton';
|
||||||
|
|
||||||
|
interface SelectAccessPolicyProps {
|
||||||
|
value: AccessPolicy;
|
||||||
|
onChange: (value: AccessPolicy) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
stretchLeft?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectAccessPolicy({ value, disabled, stretchLeft, onChange }: SelectAccessPolicyProps) {
|
||||||
|
const menu = useDropdown();
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(newValue: AccessPolicy) => {
|
||||||
|
menu.hide();
|
||||||
|
if (newValue !== value) {
|
||||||
|
onChange(newValue);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[menu, value, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={menu.ref}>
|
||||||
|
<MiniButton
|
||||||
|
title={`Доступ: ${labelAccessPolicy(value)}`}
|
||||||
|
hideTitle={menu.isOpen}
|
||||||
|
className='h-full disabled:cursor-auto'
|
||||||
|
icon={<PolicyIcon value={value} size='1.25rem' />}
|
||||||
|
onClick={menu.toggle}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<Dropdown isOpen={menu.isOpen} stretchLeft={stretchLeft}>
|
||||||
|
{Object.values(AccessPolicy).map((item, index) => (
|
||||||
|
<DropdownButton
|
||||||
|
key={`${prefixes.policy_list}${index}`}
|
||||||
|
text={labelAccessPolicy(item)}
|
||||||
|
title={describeAccessPolicy(item)}
|
||||||
|
icon={<PolicyIcon value={item} size='1rem' />}
|
||||||
|
onClick={() => handleChange(item)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SelectAccessPolicy;
|
|
@ -1,93 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import { IconFilter, IconFollow, IconImmutable, IconOwner, IconPublic } from '@/components/Icons';
|
|
||||||
import Dropdown from '@/components/ui/Dropdown';
|
|
||||||
import SelectorButton from '@/components/ui/SelectorButton';
|
|
||||||
import { useAuth } from '@/context/AuthContext';
|
|
||||||
import useDropdown from '@/hooks/useDropdown';
|
|
||||||
import useWindowSize from '@/hooks/useWindowSize';
|
|
||||||
import { LibraryFilterStrategy } from '@/models/miscellaneous';
|
|
||||||
import { prefixes } from '@/utils/constants';
|
|
||||||
import { describeLibraryFilter, labelLibraryFilter } from '@/utils/labels';
|
|
||||||
|
|
||||||
import DropdownButton from '../ui/DropdownButton';
|
|
||||||
|
|
||||||
function StrategyIcon(strategy: LibraryFilterStrategy, size: string, color?: string) {
|
|
||||||
switch (strategy) {
|
|
||||||
case LibraryFilterStrategy.MANUAL:
|
|
||||||
return <IconFilter size={size} className={color} />;
|
|
||||||
case LibraryFilterStrategy.CANONICAL:
|
|
||||||
return <IconImmutable size={size} className={color} />;
|
|
||||||
case LibraryFilterStrategy.COMMON:
|
|
||||||
return <IconPublic size={size} className={color} />;
|
|
||||||
case LibraryFilterStrategy.OWNED:
|
|
||||||
return <IconOwner size={size} className={color} />;
|
|
||||||
case LibraryFilterStrategy.SUBSCRIBE:
|
|
||||||
return <IconFollow size={size} className={color} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SelectFilterStrategyProps {
|
|
||||||
value: LibraryFilterStrategy;
|
|
||||||
onChange: (value: LibraryFilterStrategy) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectFilterStrategy({ value, onChange }: SelectFilterStrategyProps) {
|
|
||||||
const menu = useDropdown();
|
|
||||||
const { user } = useAuth();
|
|
||||||
const size = useWindowSize();
|
|
||||||
|
|
||||||
const handleChange = useCallback(
|
|
||||||
(newValue: LibraryFilterStrategy) => {
|
|
||||||
menu.hide();
|
|
||||||
onChange(newValue);
|
|
||||||
},
|
|
||||||
[menu, onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
function isStrategyDisabled(strategy: LibraryFilterStrategy): boolean {
|
|
||||||
if (strategy === LibraryFilterStrategy.SUBSCRIBE || strategy === LibraryFilterStrategy.OWNED) {
|
|
||||||
return !user;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={menu.ref} className='h-full text-right'>
|
|
||||||
<SelectorButton
|
|
||||||
transparent
|
|
||||||
tabIndex={-1}
|
|
||||||
title={describeLibraryFilter(value)}
|
|
||||||
hideTitle={menu.isOpen}
|
|
||||||
className='h-full'
|
|
||||||
icon={StrategyIcon(value, '1rem', value !== LibraryFilterStrategy.MANUAL ? 'icon-primary' : '')}
|
|
||||||
text={size.isSmall ? undefined : labelLibraryFilter(value)}
|
|
||||||
onClick={menu.toggle}
|
|
||||||
/>
|
|
||||||
<Dropdown isOpen={menu.isOpen}>
|
|
||||||
{Object.values(LibraryFilterStrategy).map((enumValue, index) => {
|
|
||||||
const strategy = enumValue as LibraryFilterStrategy;
|
|
||||||
return (
|
|
||||||
<DropdownButton
|
|
||||||
className='w-[10rem]'
|
|
||||||
key={`${prefixes.library_filters_list}${index}`}
|
|
||||||
onClick={() => handleChange(strategy)}
|
|
||||||
title={describeLibraryFilter(strategy)}
|
|
||||||
disabled={isStrategyDisabled(strategy)}
|
|
||||||
>
|
|
||||||
<div className='inline-flex items-center gap-3'>
|
|
||||||
{StrategyIcon(strategy, '1rem')}
|
|
||||||
{labelLibraryFilter(strategy)}
|
|
||||||
</div>
|
|
||||||
</DropdownButton>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SelectFilterStrategy;
|
|
|
@ -10,32 +10,9 @@ import { DependencyMode } from '@/models/miscellaneous';
|
||||||
import { prefixes } from '@/utils/constants';
|
import { prefixes } from '@/utils/constants';
|
||||||
import { describeCstSource, labelCstSource } from '@/utils/labels';
|
import { describeCstSource, labelCstSource } from '@/utils/labels';
|
||||||
|
|
||||||
import {
|
import { DependencyIcon } from '../DomainIcons';
|
||||||
IconGraphCollapse,
|
|
||||||
IconGraphExpand,
|
|
||||||
IconGraphInputs,
|
|
||||||
IconGraphOutputs,
|
|
||||||
IconSettings,
|
|
||||||
IconText
|
|
||||||
} from '../Icons';
|
|
||||||
import DropdownButton from '../ui/DropdownButton';
|
import DropdownButton from '../ui/DropdownButton';
|
||||||
|
|
||||||
function DependencyIcon(mode: DependencyMode, size: string, color?: string) {
|
|
||||||
switch (mode) {
|
|
||||||
case DependencyMode.ALL:
|
|
||||||
return <IconSettings size={size} className={color} />;
|
|
||||||
case DependencyMode.EXPRESSION:
|
|
||||||
return <IconText size={size} className={color} />;
|
|
||||||
case DependencyMode.OUTPUTS:
|
|
||||||
return <IconGraphOutputs size={size} className={color} />;
|
|
||||||
case DependencyMode.INPUTS:
|
|
||||||
return <IconGraphInputs size={size} className={color} />;
|
|
||||||
case DependencyMode.EXPAND_OUTPUTS:
|
|
||||||
return <IconGraphExpand size={size} className={color} />;
|
|
||||||
case DependencyMode.EXPAND_INPUTS:
|
|
||||||
return <IconGraphCollapse size={size} className={color} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
interface SelectGraphFilterProps {
|
interface SelectGraphFilterProps {
|
||||||
value: DependencyMode;
|
value: DependencyMode;
|
||||||
onChange: (value: DependencyMode) => void;
|
onChange: (value: DependencyMode) => void;
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import Dropdown from '@/components/ui/Dropdown';
|
||||||
|
import SelectorButton from '@/components/ui/SelectorButton';
|
||||||
|
import useDropdown from '@/hooks/useDropdown';
|
||||||
|
import { LocationHead } from '@/models/library';
|
||||||
|
import { prefixes } from '@/utils/constants';
|
||||||
|
import { describeLocationHead, labelLocationHead } from '@/utils/labels';
|
||||||
|
|
||||||
|
import { LocationHeadIcon } from '../DomainIcons';
|
||||||
|
import DropdownButton from '../ui/DropdownButton';
|
||||||
|
|
||||||
|
interface SelectLocationHeadProps {
|
||||||
|
value: LocationHead;
|
||||||
|
onChange: (value: LocationHead) => void;
|
||||||
|
excluded?: LocationHead[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectLocationHead({ value, excluded = [], onChange }: SelectLocationHeadProps) {
|
||||||
|
const menu = useDropdown();
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(newValue: LocationHead) => {
|
||||||
|
menu.hide();
|
||||||
|
onChange(newValue);
|
||||||
|
},
|
||||||
|
[menu, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={menu.ref} className='h-full text-right'>
|
||||||
|
<SelectorButton
|
||||||
|
transparent
|
||||||
|
tabIndex={-1}
|
||||||
|
title={describeLocationHead(value)}
|
||||||
|
hideTitle={menu.isOpen}
|
||||||
|
className='h-full'
|
||||||
|
icon={<LocationHeadIcon value={value} size='1rem' />}
|
||||||
|
text={labelLocationHead(value)}
|
||||||
|
onClick={menu.toggle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Dropdown isOpen={menu.isOpen} className='z-modalTooltip'>
|
||||||
|
{Object.values(LocationHead)
|
||||||
|
.filter(head => !excluded.includes(head))
|
||||||
|
.map((head, index) => {
|
||||||
|
return (
|
||||||
|
<DropdownButton
|
||||||
|
className='w-[10rem]'
|
||||||
|
key={`${prefixes.location_head_list}${index}`}
|
||||||
|
onClick={() => handleChange(head)}
|
||||||
|
title={describeLocationHead(head)}
|
||||||
|
>
|
||||||
|
<div className='inline-flex items-center gap-3'>
|
||||||
|
<LocationHeadIcon value={head} size='1rem' />
|
||||||
|
{labelLocationHead(head)}
|
||||||
|
</div>
|
||||||
|
</DropdownButton>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SelectLocationHead;
|
|
@ -10,23 +10,9 @@ import { CstMatchMode } from '@/models/miscellaneous';
|
||||||
import { prefixes } from '@/utils/constants';
|
import { prefixes } from '@/utils/constants';
|
||||||
import { describeCstMatchMode, labelCstMatchMode } from '@/utils/labels';
|
import { describeCstMatchMode, labelCstMatchMode } from '@/utils/labels';
|
||||||
|
|
||||||
import { IconAlias, IconFilter, IconFormula, IconTerm, IconText } from '../Icons';
|
import { MatchModeIcon } from '../DomainIcons';
|
||||||
import DropdownButton from '../ui/DropdownButton';
|
import DropdownButton from '../ui/DropdownButton';
|
||||||
|
|
||||||
function MatchModeIcon(mode: CstMatchMode, size: string, color?: string) {
|
|
||||||
switch (mode) {
|
|
||||||
case CstMatchMode.ALL:
|
|
||||||
return <IconFilter size={size} className={color} />;
|
|
||||||
case CstMatchMode.TEXT:
|
|
||||||
return <IconText size={size} className={color} />;
|
|
||||||
case CstMatchMode.EXPR:
|
|
||||||
return <IconFormula size={size} className={color} />;
|
|
||||||
case CstMatchMode.TERM:
|
|
||||||
return <IconTerm size={size} className={color} />;
|
|
||||||
case CstMatchMode.NAME:
|
|
||||||
return <IconAlias size={size} className={color} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
interface SelectMatchModeProps {
|
interface SelectMatchModeProps {
|
||||||
value: CstMatchMode;
|
value: CstMatchMode;
|
||||||
onChange: (value: CstMatchMode) => void;
|
onChange: (value: CstMatchMode) => void;
|
||||||
|
|
|
@ -17,7 +17,7 @@ function Dropdown({ isOpen, stretchLeft, className, children, ...restProps }: Dr
|
||||||
<motion.div
|
<motion.div
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'z-modal-tooltip',
|
'z-modalTooltip',
|
||||||
'absolute mt-3',
|
'absolute mt-3',
|
||||||
'flex flex-col',
|
'flex flex-col',
|
||||||
'border rounded-md shadow-lg',
|
'border rounded-md shadow-lg',
|
||||||
|
|
|
@ -11,7 +11,7 @@ interface LabeledValueProps extends CProps.Styling {
|
||||||
|
|
||||||
function LabeledValue({ id, label, text, title, className, ...restProps }: LabeledValueProps) {
|
function LabeledValue({ id, label, text, title, className, ...restProps }: LabeledValueProps) {
|
||||||
return (
|
return (
|
||||||
<div className={clsx('flex justify-between gap-3', className)} {...restProps}>
|
<div className={clsx('flex justify-between gap-6', className)} {...restProps}>
|
||||||
<span title={title}>{label}</span>
|
<span title={title}>{label}</span>
|
||||||
<span id={id}>{text}</span>
|
<span id={id}>{text}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -93,7 +93,7 @@ function Modal({
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={clsx('z-modal-controls', 'px-6 py-3 flex gap-12 justify-center')}>
|
<div className={clsx('z-modalControls', 'px-6 py-3 flex gap-12 justify-center')}>
|
||||||
{!readonly ? (
|
{!readonly ? (
|
||||||
<Button
|
<Button
|
||||||
autoFocus
|
autoFocus
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { IconSearch } from '../Icons';
|
import { IconSearch } from '../Icons';
|
||||||
import { CProps } from '../props';
|
import { CProps } from '../props';
|
||||||
import Overlay from './Overlay';
|
import Overlay from './Overlay';
|
||||||
|
@ -5,23 +7,27 @@ import TextInput from './TextInput';
|
||||||
|
|
||||||
interface SearchBarProps extends CProps.Styling {
|
interface SearchBarProps extends CProps.Styling {
|
||||||
value: string;
|
value: string;
|
||||||
|
noIcon?: boolean;
|
||||||
id?: string;
|
id?: string;
|
||||||
|
placeholder?: string;
|
||||||
onChange?: (newValue: string) => void;
|
onChange?: (newValue: string) => void;
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SearchBar({ id, value, onChange, noBorder, ...restProps }: SearchBarProps) {
|
function SearchBar({ id, value, noIcon, onChange, noBorder, placeholder = 'Поиск', ...restProps }: SearchBarProps) {
|
||||||
return (
|
return (
|
||||||
<div {...restProps}>
|
<div {...restProps}>
|
||||||
|
{!noIcon ? (
|
||||||
<Overlay position='top-[-0.125rem] left-3 translate-y-1/2' className='pointer-events-none clr-text-controls'>
|
<Overlay position='top-[-0.125rem] left-3 translate-y-1/2' className='pointer-events-none clr-text-controls'>
|
||||||
<IconSearch size='1.25rem' />
|
<IconSearch size='1.25rem' />
|
||||||
</Overlay>
|
</Overlay>
|
||||||
|
) : null}
|
||||||
<TextInput
|
<TextInput
|
||||||
id={id}
|
id={id}
|
||||||
noOutline
|
noOutline
|
||||||
placeholder='Поиск'
|
placeholder={placeholder}
|
||||||
type='search'
|
type='search'
|
||||||
className='w-full pl-10 outline-none'
|
className={clsx('w-full outline-none', !noIcon && 'pl-10')}
|
||||||
noBorder={noBorder}
|
noBorder={noBorder}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={event => (onChange ? onChange(event.target.value) : undefined)}
|
onChange={event => (onChange ? onChange(event.target.value) : undefined)}
|
||||||
|
|
|
@ -101,7 +101,6 @@ function SelectTree<ItemType>({
|
||||||
{foldable.has(item) ? (
|
{foldable.has(item) ? (
|
||||||
<Overlay position='left-[-1.3rem]' className={clsx(!folded.includes(item) && 'top-[0.1rem]')}>
|
<Overlay position='left-[-1.3rem]' className={clsx(!folded.includes(item) && 'top-[0.1rem]')}>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
tabIndex={-1}
|
|
||||||
noPadding
|
noPadding
|
||||||
noHover
|
noHover
|
||||||
icon={!folded.includes(item) ? <IconDropArrow size='1rem' /> : <IconPageRight size='1.25rem' />}
|
icon={!folded.includes(item) ? <IconDropArrow size='1rem' /> : <IconPageRight size='1.25rem' />}
|
||||||
|
|
|
@ -14,9 +14,10 @@ import {
|
||||||
} from '@/app/backendAPI';
|
} from '@/app/backendAPI';
|
||||||
import { ErrorData } from '@/components/info/InfoError';
|
import { ErrorData } from '@/components/info/InfoError';
|
||||||
import { ILibraryItem, LibraryItemID } from '@/models/library';
|
import { ILibraryItem, LibraryItemID } from '@/models/library';
|
||||||
import { matchLibraryItem } from '@/models/libraryAPI';
|
import { ILibraryCreateData } from '@/models/library';
|
||||||
|
import { matchLibraryItem, matchLibraryItemLocation } from '@/models/libraryAPI';
|
||||||
import { ILibraryFilter } from '@/models/miscellaneous';
|
import { ILibraryFilter } from '@/models/miscellaneous';
|
||||||
import { IRSForm, IRSFormCloneData, IRSFormCreateData, IRSFormData } from '@/models/rsform';
|
import { IRSForm, IRSFormCloneData, IRSFormData } from '@/models/rsform';
|
||||||
import { RSFormLoader } from '@/models/RSFormLoader';
|
import { RSFormLoader } from '@/models/RSFormLoader';
|
||||||
|
|
||||||
import { useAuth } from './AuthContext';
|
import { useAuth } from './AuthContext';
|
||||||
|
@ -32,7 +33,7 @@ interface ILibraryContext {
|
||||||
|
|
||||||
applyFilter: (params: ILibraryFilter) => ILibraryItem[];
|
applyFilter: (params: ILibraryFilter) => ILibraryItem[];
|
||||||
retrieveTemplate: (templateID: LibraryItemID, callback: (schema: IRSForm) => void) => void;
|
retrieveTemplate: (templateID: LibraryItemID, callback: (schema: IRSForm) => void) => void;
|
||||||
createItem: (data: IRSFormCreateData, callback?: DataCallback<ILibraryItem>) => void;
|
createItem: (data: ILibraryCreateData, callback?: DataCallback<ILibraryItem>) => void;
|
||||||
cloneItem: (target: LibraryItemID, data: IRSFormCloneData, callback: DataCallback<IRSFormData>) => void;
|
cloneItem: (target: LibraryItemID, data: IRSFormCloneData, callback: DataCallback<IRSFormData>) => void;
|
||||||
destroyItem: (target: LibraryItemID, callback?: () => void) => void;
|
destroyItem: (target: LibraryItemID, callback?: () => void) => void;
|
||||||
|
|
||||||
|
@ -65,22 +66,28 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
|
||||||
const [cachedTemplates, setCachedTemplates] = useState<IRSForm[]>([]);
|
const [cachedTemplates, setCachedTemplates] = useState<IRSForm[]>([]);
|
||||||
|
|
||||||
const applyFilter = useCallback(
|
const applyFilter = useCallback(
|
||||||
(params: ILibraryFilter) => {
|
(filter: ILibraryFilter) => {
|
||||||
let result = items;
|
let result = items;
|
||||||
if (params.is_owned) {
|
if (filter.head) {
|
||||||
result = result.filter(item => item.owner === user?.id);
|
result = result.filter(item => item.location.startsWith(filter.head!));
|
||||||
}
|
}
|
||||||
if (params.is_common !== undefined) {
|
if (filter.isVisible !== undefined) {
|
||||||
result = result.filter(item => item.is_common === params.is_common);
|
result = result.filter(item => filter.isVisible === item.visible);
|
||||||
}
|
}
|
||||||
if (params.is_canonical !== undefined) {
|
if (filter.isOwned !== undefined) {
|
||||||
result = result.filter(item => item.is_canonical === params.is_canonical);
|
result = result.filter(item => filter.isOwned === (item.owner === user?.id));
|
||||||
}
|
}
|
||||||
if (params.is_subscribed !== undefined) {
|
if (filter.isSubscribed !== undefined) {
|
||||||
result = result.filter(item => user?.subscriptions.includes(item.id));
|
result = result.filter(item => filter.isSubscribed == user?.subscriptions.includes(item.id));
|
||||||
}
|
}
|
||||||
if (params.query) {
|
if (filter.isEditor !== undefined) {
|
||||||
result = result.filter(item => matchLibraryItem(item, params.query!));
|
// TODO: load editors from backend
|
||||||
|
}
|
||||||
|
if (filter.query) {
|
||||||
|
result = result.filter(item => matchLibraryItem(item, filter.query!));
|
||||||
|
}
|
||||||
|
if (filter.path) {
|
||||||
|
result = result.filter(item => matchLibraryItemLocation(item, filter.path!));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
@ -173,7 +180,7 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const createItem = useCallback(
|
const createItem = useCallback(
|
||||||
(data: IRSFormCreateData, callback?: DataCallback<ILibraryItem>) => {
|
(data: ILibraryCreateData, callback?: DataCallback<ILibraryItem>) => {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
postNewRSForm({
|
postNewRSForm({
|
||||||
data: data,
|
data: data,
|
||||||
|
|
|
@ -18,6 +18,8 @@ import {
|
||||||
patchResetAliases,
|
patchResetAliases,
|
||||||
patchRestoreOrder,
|
patchRestoreOrder,
|
||||||
patchRestoreVersion,
|
patchRestoreVersion,
|
||||||
|
patchSetAccessPolicy,
|
||||||
|
patchSetLocation,
|
||||||
patchSetOwner,
|
patchSetOwner,
|
||||||
patchSubstituteConstituents,
|
patchSubstituteConstituents,
|
||||||
patchUploadTRS,
|
patchUploadTRS,
|
||||||
|
@ -28,7 +30,7 @@ import {
|
||||||
} from '@/app/backendAPI';
|
} from '@/app/backendAPI';
|
||||||
import { type ErrorData } from '@/components/info/InfoError';
|
import { type ErrorData } from '@/components/info/InfoError';
|
||||||
import useRSFormDetails from '@/hooks/useRSFormDetails';
|
import useRSFormDetails from '@/hooks/useRSFormDetails';
|
||||||
import { ILibraryItem, IVersionData, VersionID } from '@/models/library';
|
import { AccessPolicy, ILibraryItem, IVersionData, VersionID } from '@/models/library';
|
||||||
import { ILibraryUpdateData } from '@/models/library';
|
import { ILibraryUpdateData } from '@/models/library';
|
||||||
import {
|
import {
|
||||||
ConstituentaID,
|
ConstituentaID,
|
||||||
|
@ -55,13 +57,13 @@ interface IRSFormContext {
|
||||||
schemaID: string;
|
schemaID: string;
|
||||||
versionID?: string;
|
versionID?: string;
|
||||||
|
|
||||||
error: ErrorData;
|
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
errorLoading: ErrorData;
|
||||||
processing: boolean;
|
processing: boolean;
|
||||||
|
processingError: ErrorData;
|
||||||
|
|
||||||
isArchive: boolean;
|
isArchive: boolean;
|
||||||
isOwned: boolean;
|
isOwned: boolean;
|
||||||
isClaimable: boolean;
|
|
||||||
isSubscribed: boolean;
|
isSubscribed: boolean;
|
||||||
|
|
||||||
update: (data: ILibraryUpdateData, callback?: DataCallback<ILibraryItem>) => void;
|
update: (data: ILibraryUpdateData, callback?: DataCallback<ILibraryItem>) => void;
|
||||||
|
@ -71,6 +73,8 @@ interface IRSFormContext {
|
||||||
subscribe: (callback?: () => void) => void;
|
subscribe: (callback?: () => void) => void;
|
||||||
unsubscribe: (callback?: () => void) => void;
|
unsubscribe: (callback?: () => void) => void;
|
||||||
setOwner: (newOwner: UserID, callback?: () => void) => void;
|
setOwner: (newOwner: UserID, callback?: () => void) => void;
|
||||||
|
setAccessPolicy: (newPolicy: AccessPolicy, callback?: () => void) => void;
|
||||||
|
setLocation: (newLocation: string, callback?: () => void) => void;
|
||||||
setEditors: (newEditors: UserID[], callback?: () => void) => void;
|
setEditors: (newEditors: UserID[], callback?: () => void) => void;
|
||||||
|
|
||||||
resetAliases: (callback: () => void) => void;
|
resetAliases: (callback: () => void) => void;
|
||||||
|
@ -112,8 +116,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
const {
|
const {
|
||||||
schema, // prettier: split lines
|
schema, // prettier: split lines
|
||||||
reload,
|
reload,
|
||||||
error,
|
error: errorLoading,
|
||||||
setError,
|
|
||||||
setSchema,
|
setSchema,
|
||||||
loading
|
loading
|
||||||
} = useRSFormDetails({
|
} = useRSFormDetails({
|
||||||
|
@ -121,6 +124,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
version: versionID
|
version: versionID
|
||||||
});
|
});
|
||||||
const [processing, setProcessing] = useState(false);
|
const [processing, setProcessing] = useState(false);
|
||||||
|
const [processingError, setProcessingError] = useState<ErrorData>(undefined);
|
||||||
|
|
||||||
const [toggleTracking, setToggleTracking] = useState(false);
|
const [toggleTracking, setToggleTracking] = useState(false);
|
||||||
|
|
||||||
|
@ -130,10 +134,6 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
|
|
||||||
const isArchive = useMemo(() => !!versionID, [versionID]);
|
const isArchive = useMemo(() => !!versionID, [versionID]);
|
||||||
|
|
||||||
const isClaimable = useMemo(() => {
|
|
||||||
return !isArchive && ((user?.id !== schema?.owner && schema?.is_common && !schema?.is_canonical) ?? false);
|
|
||||||
}, [user, schema?.owner, schema?.is_common, schema?.is_canonical, isArchive]);
|
|
||||||
|
|
||||||
const isSubscribed = useMemo(() => {
|
const isSubscribed = useMemo(() => {
|
||||||
if (!user || !schema || !user.id) {
|
if (!user || !schema || !user.id) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -147,12 +147,12 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
if (!schema) {
|
if (!schema) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setError(undefined);
|
setProcessingError(undefined);
|
||||||
patchLibraryItem(schemaID, {
|
patchLibraryItem(schemaID, {
|
||||||
data: data,
|
data: data,
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: setError,
|
onError: setProcessingError,
|
||||||
onSuccess: newData => {
|
onSuccess: newData => {
|
||||||
setSchema(Object.assign(schema, newData));
|
setSchema(Object.assign(schema, newData));
|
||||||
library.localUpdateItem(newData);
|
library.localUpdateItem(newData);
|
||||||
|
@ -160,7 +160,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[schemaID, setError, setSchema, schema, library]
|
[schemaID, setSchema, schema, library]
|
||||||
);
|
);
|
||||||
|
|
||||||
const upload = useCallback(
|
const upload = useCallback(
|
||||||
|
@ -168,12 +168,12 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
if (!schema) {
|
if (!schema) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setError(undefined);
|
setProcessingError(undefined);
|
||||||
patchUploadTRS(schemaID, {
|
patchUploadTRS(schemaID, {
|
||||||
data: data,
|
data: data,
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: setError,
|
onError: setProcessingError,
|
||||||
onSuccess: newData => {
|
onSuccess: newData => {
|
||||||
setSchema(newData);
|
setSchema(newData);
|
||||||
library.localUpdateItem(newData);
|
library.localUpdateItem(newData);
|
||||||
|
@ -181,7 +181,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[schemaID, setError, setSchema, schema, library]
|
[schemaID, setSchema, schema, library]
|
||||||
);
|
);
|
||||||
|
|
||||||
const subscribe = useCallback(
|
const subscribe = useCallback(
|
||||||
|
@ -189,11 +189,11 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
if (!schema || !user) {
|
if (!schema || !user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setError(undefined);
|
setProcessingError(undefined);
|
||||||
postSubscribe(schemaID, {
|
postSubscribe(schemaID, {
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: setError,
|
onError: setProcessingError,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
if (user.id && !schema.subscribers.includes(user.id)) {
|
if (user.id && !schema.subscribers.includes(user.id)) {
|
||||||
schema.subscribers.push(user.id);
|
schema.subscribers.push(user.id);
|
||||||
|
@ -206,7 +206,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[schemaID, setError, schema, user]
|
[schemaID, schema, user]
|
||||||
);
|
);
|
||||||
|
|
||||||
const unsubscribe = useCallback(
|
const unsubscribe = useCallback(
|
||||||
|
@ -214,11 +214,11 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
if (!schema || !user) {
|
if (!schema || !user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setError(undefined);
|
setProcessingError(undefined);
|
||||||
deleteUnsubscribe(schemaID, {
|
deleteUnsubscribe(schemaID, {
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: setError,
|
onError: setProcessingError,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
if (user.id && schema.subscribers.includes(user.id)) {
|
if (user.id && schema.subscribers.includes(user.id)) {
|
||||||
schema.subscribers.splice(schema.subscribers.indexOf(user.id), 1);
|
schema.subscribers.splice(schema.subscribers.indexOf(user.id), 1);
|
||||||
|
@ -231,7 +231,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[schemaID, setError, schema, user]
|
[schemaID, schema, user]
|
||||||
);
|
);
|
||||||
|
|
||||||
const setOwner = useCallback(
|
const setOwner = useCallback(
|
||||||
|
@ -239,21 +239,65 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
if (!schema) {
|
if (!schema) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setError(undefined);
|
setProcessingError(undefined);
|
||||||
patchSetOwner(schemaID, {
|
patchSetOwner(schemaID, {
|
||||||
data: {
|
data: {
|
||||||
user: newOwner
|
user: newOwner
|
||||||
},
|
},
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: setError,
|
onError: setProcessingError,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
schema.owner = newOwner;
|
schema.owner = newOwner;
|
||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[schemaID, setError, schema]
|
[schemaID, schema]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setAccessPolicy = useCallback(
|
||||||
|
(newPolicy: AccessPolicy, callback?: () => void) => {
|
||||||
|
if (!schema) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setProcessingError(undefined);
|
||||||
|
patchSetAccessPolicy(schemaID, {
|
||||||
|
data: {
|
||||||
|
access_policy: newPolicy
|
||||||
|
},
|
||||||
|
showError: true,
|
||||||
|
setLoading: setProcessing,
|
||||||
|
onError: setProcessingError,
|
||||||
|
onSuccess: () => {
|
||||||
|
schema.access_policy = newPolicy;
|
||||||
|
if (callback) callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[schemaID, schema]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setLocation = useCallback(
|
||||||
|
(newLocation: string, callback?: () => void) => {
|
||||||
|
if (!schema) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setProcessingError(undefined);
|
||||||
|
patchSetLocation(schemaID, {
|
||||||
|
data: {
|
||||||
|
location: newLocation
|
||||||
|
},
|
||||||
|
showError: true,
|
||||||
|
setLoading: setProcessing,
|
||||||
|
onError: setProcessingError,
|
||||||
|
onSuccess: () => {
|
||||||
|
schema.location = newLocation;
|
||||||
|
if (callback) callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[schemaID, schema]
|
||||||
);
|
);
|
||||||
|
|
||||||
const setEditors = useCallback(
|
const setEditors = useCallback(
|
||||||
|
@ -261,21 +305,21 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
if (!schema) {
|
if (!schema) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setError(undefined);
|
setProcessingError(undefined);
|
||||||
patchSetEditors(schemaID, {
|
patchSetEditors(schemaID, {
|
||||||
data: {
|
data: {
|
||||||
users: newEditors
|
users: newEditors
|
||||||
},
|
},
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: setError,
|
onError: setProcessingError,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
schema.editors = newEditors;
|
schema.editors = newEditors;
|
||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[schemaID, setError, schema]
|
[schemaID, schema]
|
||||||
);
|
);
|
||||||
|
|
||||||
const resetAliases = useCallback(
|
const resetAliases = useCallback(
|
||||||
|
@ -283,11 +327,11 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
if (!schema || !user) {
|
if (!schema || !user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setError(undefined);
|
setProcessingError(undefined);
|
||||||
patchResetAliases(schemaID, {
|
patchResetAliases(schemaID, {
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: setError,
|
onError: setProcessingError,
|
||||||
onSuccess: newData => {
|
onSuccess: newData => {
|
||||||
setSchema(Object.assign(schema, newData));
|
setSchema(Object.assign(schema, newData));
|
||||||
library.localUpdateTimestamp(newData.id);
|
library.localUpdateTimestamp(newData.id);
|
||||||
|
@ -295,7 +339,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[schemaID, setError, schema, library, user, setSchema]
|
[schemaID, schema, library, user, setSchema]
|
||||||
);
|
);
|
||||||
|
|
||||||
const restoreOrder = useCallback(
|
const restoreOrder = useCallback(
|
||||||
|
@ -303,11 +347,11 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
if (!schema || !user) {
|
if (!schema || !user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setError(undefined);
|
setProcessingError(undefined);
|
||||||
patchRestoreOrder(schemaID, {
|
patchRestoreOrder(schemaID, {
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: setError,
|
onError: setProcessingError,
|
||||||
onSuccess: newData => {
|
onSuccess: newData => {
|
||||||
setSchema(Object.assign(schema, newData));
|
setSchema(Object.assign(schema, newData));
|
||||||
library.localUpdateTimestamp(newData.id);
|
library.localUpdateTimestamp(newData.id);
|
||||||
|
@ -315,17 +359,17 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[schemaID, setError, schema, library, user, setSchema]
|
[schemaID, schema, library, user, setSchema]
|
||||||
);
|
);
|
||||||
|
|
||||||
const produceStructure = useCallback(
|
const produceStructure = useCallback(
|
||||||
(data: ITargetCst, callback?: DataCallback<ConstituentaID[]>) => {
|
(data: ITargetCst, callback?: DataCallback<ConstituentaID[]>) => {
|
||||||
setError(undefined);
|
setProcessingError(undefined);
|
||||||
patchProduceStructure(schemaID, {
|
patchProduceStructure(schemaID, {
|
||||||
data: data,
|
data: data,
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: setError,
|
onError: setProcessingError,
|
||||||
onSuccess: newData => {
|
onSuccess: newData => {
|
||||||
setSchema(newData.schema);
|
setSchema(newData.schema);
|
||||||
library.localUpdateTimestamp(newData.schema.id);
|
library.localUpdateTimestamp(newData.schema.id);
|
||||||
|
@ -333,30 +377,30 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setError, setSchema, library, schemaID]
|
[setSchema, library, schemaID]
|
||||||
);
|
);
|
||||||
|
|
||||||
const download = useCallback(
|
const download = useCallback(
|
||||||
(callback: DataCallback<Blob>) => {
|
(callback: DataCallback<Blob>) => {
|
||||||
setError(undefined);
|
setProcessingError(undefined);
|
||||||
getTRSFile(schemaID, String(schema?.version ?? ''), {
|
getTRSFile(schemaID, String(schema?.version ?? ''), {
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: setError,
|
onError: setProcessingError,
|
||||||
onSuccess: callback
|
onSuccess: callback
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[schemaID, setError, schema]
|
[schemaID, schema]
|
||||||
);
|
);
|
||||||
|
|
||||||
const cstCreate = useCallback(
|
const cstCreate = useCallback(
|
||||||
(data: ICstCreateData, callback?: DataCallback<IConstituentaMeta>) => {
|
(data: ICstCreateData, callback?: DataCallback<IConstituentaMeta>) => {
|
||||||
setError(undefined);
|
setProcessingError(undefined);
|
||||||
postNewConstituenta(schemaID, {
|
postNewConstituenta(schemaID, {
|
||||||
data: data,
|
data: data,
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: setError,
|
onError: setProcessingError,
|
||||||
onSuccess: newData => {
|
onSuccess: newData => {
|
||||||
setSchema(newData.schema);
|
setSchema(newData.schema);
|
||||||
library.localUpdateTimestamp(newData.schema.id);
|
library.localUpdateTimestamp(newData.schema.id);
|
||||||
|
@ -364,17 +408,17 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[schemaID, setError, library, setSchema]
|
[schemaID, library, setSchema]
|
||||||
);
|
);
|
||||||
|
|
||||||
const cstDelete = useCallback(
|
const cstDelete = useCallback(
|
||||||
(data: IConstituentaList, callback?: () => void) => {
|
(data: IConstituentaList, callback?: () => void) => {
|
||||||
setError(undefined);
|
setProcessingError(undefined);
|
||||||
patchDeleteConstituenta(schemaID, {
|
patchDeleteConstituenta(schemaID, {
|
||||||
data: data,
|
data: data,
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: setError,
|
onError: setProcessingError,
|
||||||
onSuccess: newData => {
|
onSuccess: newData => {
|
||||||
setSchema(newData);
|
setSchema(newData);
|
||||||
library.localUpdateTimestamp(newData.id);
|
library.localUpdateTimestamp(newData.id);
|
||||||
|
@ -382,17 +426,17 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[schemaID, setError, library, setSchema]
|
[schemaID, library, setSchema]
|
||||||
);
|
);
|
||||||
|
|
||||||
const cstUpdate = useCallback(
|
const cstUpdate = useCallback(
|
||||||
(data: ICstUpdateData, callback?: DataCallback<IConstituentaMeta>) => {
|
(data: ICstUpdateData, callback?: DataCallback<IConstituentaMeta>) => {
|
||||||
setError(undefined);
|
setProcessingError(undefined);
|
||||||
patchConstituenta(String(data.id), {
|
patchConstituenta(String(data.id), {
|
||||||
data: data,
|
data: data,
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: setError,
|
onError: setProcessingError,
|
||||||
onSuccess: newData =>
|
onSuccess: newData =>
|
||||||
reload(setProcessing, () => {
|
reload(setProcessing, () => {
|
||||||
library.localUpdateTimestamp(Number(schemaID));
|
library.localUpdateTimestamp(Number(schemaID));
|
||||||
|
@ -400,17 +444,17 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setError, schemaID, library, reload]
|
[schemaID, library, reload]
|
||||||
);
|
);
|
||||||
|
|
||||||
const cstRename = useCallback(
|
const cstRename = useCallback(
|
||||||
(data: ICstRenameData, callback?: DataCallback<IConstituentaMeta>) => {
|
(data: ICstRenameData, callback?: DataCallback<IConstituentaMeta>) => {
|
||||||
setError(undefined);
|
setProcessingError(undefined);
|
||||||
patchRenameConstituenta(schemaID, {
|
patchRenameConstituenta(schemaID, {
|
||||||
data: data,
|
data: data,
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: setError,
|
onError: setProcessingError,
|
||||||
onSuccess: newData => {
|
onSuccess: newData => {
|
||||||
setSchema(newData.schema);
|
setSchema(newData.schema);
|
||||||
library.localUpdateTimestamp(newData.schema.id);
|
library.localUpdateTimestamp(newData.schema.id);
|
||||||
|
@ -418,17 +462,17 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setError, setSchema, library, schemaID]
|
[setSchema, library, schemaID]
|
||||||
);
|
);
|
||||||
|
|
||||||
const cstSubstitute = useCallback(
|
const cstSubstitute = useCallback(
|
||||||
(data: ICstSubstituteData, callback?: () => void) => {
|
(data: ICstSubstituteData, callback?: () => void) => {
|
||||||
setError(undefined);
|
setProcessingError(undefined);
|
||||||
patchSubstituteConstituents(schemaID, {
|
patchSubstituteConstituents(schemaID, {
|
||||||
data: data,
|
data: data,
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: setError,
|
onError: setProcessingError,
|
||||||
onSuccess: newData => {
|
onSuccess: newData => {
|
||||||
setSchema(newData);
|
setSchema(newData);
|
||||||
library.localUpdateTimestamp(newData.id);
|
library.localUpdateTimestamp(newData.id);
|
||||||
|
@ -436,17 +480,17 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setError, setSchema, library, schemaID]
|
[setSchema, library, schemaID]
|
||||||
);
|
);
|
||||||
|
|
||||||
const cstMoveTo = useCallback(
|
const cstMoveTo = useCallback(
|
||||||
(data: ICstMovetoData, callback?: () => void) => {
|
(data: ICstMovetoData, callback?: () => void) => {
|
||||||
setError(undefined);
|
setProcessingError(undefined);
|
||||||
patchMoveConstituenta(schemaID, {
|
patchMoveConstituenta(schemaID, {
|
||||||
data: data,
|
data: data,
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: setError,
|
onError: setProcessingError,
|
||||||
onSuccess: newData => {
|
onSuccess: newData => {
|
||||||
setSchema(newData);
|
setSchema(newData);
|
||||||
library.localUpdateTimestamp(Number(schemaID));
|
library.localUpdateTimestamp(Number(schemaID));
|
||||||
|
@ -454,17 +498,17 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[schemaID, setError, library, setSchema]
|
[schemaID, library, setSchema]
|
||||||
);
|
);
|
||||||
|
|
||||||
const versionCreate = useCallback(
|
const versionCreate = useCallback(
|
||||||
(data: IVersionData, callback?: (version: number) => void) => {
|
(data: IVersionData, callback?: (version: number) => void) => {
|
||||||
setError(undefined);
|
setProcessingError(undefined);
|
||||||
postCreateVersion(schemaID, {
|
postCreateVersion(schemaID, {
|
||||||
data: data,
|
data: data,
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: setError,
|
onError: setProcessingError,
|
||||||
onSuccess: newData => {
|
onSuccess: newData => {
|
||||||
setSchema(newData.schema);
|
setSchema(newData.schema);
|
||||||
library.localUpdateTimestamp(Number(schemaID));
|
library.localUpdateTimestamp(Number(schemaID));
|
||||||
|
@ -472,17 +516,17 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[schemaID, setError, library, setSchema]
|
[schemaID, library, setSchema]
|
||||||
);
|
);
|
||||||
|
|
||||||
const versionUpdate = useCallback(
|
const versionUpdate = useCallback(
|
||||||
(target: number, data: IVersionData, callback?: () => void) => {
|
(target: number, data: IVersionData, callback?: () => void) => {
|
||||||
setError(undefined);
|
setProcessingError(undefined);
|
||||||
patchVersion(String(target), {
|
patchVersion(String(target), {
|
||||||
data: data,
|
data: data,
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: setError,
|
onError: setProcessingError,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
schema!.versions = schema!.versions.map(prev => {
|
schema!.versions = schema!.versions.map(prev => {
|
||||||
if (prev.id === target) {
|
if (prev.id === target) {
|
||||||
|
@ -498,16 +542,16 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setError, schema, setSchema]
|
[schema, setSchema]
|
||||||
);
|
);
|
||||||
|
|
||||||
const versionDelete = useCallback(
|
const versionDelete = useCallback(
|
||||||
(target: number, callback?: () => void) => {
|
(target: number, callback?: () => void) => {
|
||||||
setError(undefined);
|
setProcessingError(undefined);
|
||||||
deleteVersion(String(target), {
|
deleteVersion(String(target), {
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: setError,
|
onError: setProcessingError,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
schema!.versions = schema!.versions.filter(prev => prev.id !== target);
|
schema!.versions = schema!.versions.filter(prev => prev.id !== target);
|
||||||
setSchema(schema);
|
setSchema(schema);
|
||||||
|
@ -515,33 +559,33 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setError, schema, setSchema]
|
[schema, setSchema]
|
||||||
);
|
);
|
||||||
|
|
||||||
const versionRestore = useCallback(
|
const versionRestore = useCallback(
|
||||||
(target: string, callback?: () => void) => {
|
(target: string, callback?: () => void) => {
|
||||||
setError(undefined);
|
setProcessingError(undefined);
|
||||||
patchRestoreVersion(target, {
|
patchRestoreVersion(target, {
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: setError,
|
onError: setProcessingError,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setSchema(schema);
|
setSchema(schema);
|
||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setError, schema, setSchema]
|
[schema, setSchema]
|
||||||
);
|
);
|
||||||
|
|
||||||
const inlineSynthesis = useCallback(
|
const inlineSynthesis = useCallback(
|
||||||
(data: IInlineSynthesisData, callback?: DataCallback<IRSFormData>) => {
|
(data: IInlineSynthesisData, callback?: DataCallback<IRSFormData>) => {
|
||||||
setError(undefined);
|
setProcessingError(undefined);
|
||||||
patchInlineSynthesis({
|
patchInlineSynthesis({
|
||||||
data: data,
|
data: data,
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: setError,
|
onError: setProcessingError,
|
||||||
onSuccess: newData => {
|
onSuccess: newData => {
|
||||||
setSchema(newData);
|
setSchema(newData);
|
||||||
library.localUpdateTimestamp(Number(schemaID));
|
library.localUpdateTimestamp(Number(schemaID));
|
||||||
|
@ -549,7 +593,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setError, library, schemaID, setSchema]
|
[library, schemaID, setSchema]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -558,11 +602,11 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
schema,
|
schema,
|
||||||
schemaID,
|
schemaID,
|
||||||
versionID,
|
versionID,
|
||||||
error,
|
|
||||||
loading,
|
loading,
|
||||||
|
errorLoading,
|
||||||
processing,
|
processing,
|
||||||
|
processingError,
|
||||||
isOwned,
|
isOwned,
|
||||||
isClaimable,
|
|
||||||
isSubscribed,
|
isSubscribed,
|
||||||
isArchive,
|
isArchive,
|
||||||
update,
|
update,
|
||||||
|
@ -577,6 +621,8 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
||||||
unsubscribe,
|
unsubscribe,
|
||||||
setOwner,
|
setOwner,
|
||||||
setEditors,
|
setEditors,
|
||||||
|
setAccessPolicy,
|
||||||
|
setLocation,
|
||||||
|
|
||||||
cstUpdate,
|
cstUpdate,
|
||||||
cstCreate,
|
cstCreate,
|
||||||
|
|
62
rsconcept/frontend/src/dialogs/DlgChangeLocation.tsx
Normal file
62
rsconcept/frontend/src/dialogs/DlgChangeLocation.tsx
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import SelectLocationHead from '@/components/select/SelectLocationHead';
|
||||||
|
import Label from '@/components/ui/Label';
|
||||||
|
import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||||
|
import TextArea from '@/components/ui/TextArea';
|
||||||
|
import { useAuth } from '@/context/AuthContext';
|
||||||
|
import { LocationHead } from '@/models/library';
|
||||||
|
import { combineLocation, validateLocation } from '@/models/libraryAPI';
|
||||||
|
import { limits } from '@/utils/constants';
|
||||||
|
|
||||||
|
interface DlgChangeLocationProps extends Pick<ModalProps, 'hideWindow'> {
|
||||||
|
initial: string;
|
||||||
|
onChangeLocation: (newLocation: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DlgChangeLocation({ hideWindow, initial, onChangeLocation }: DlgChangeLocationProps) {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [head, setHead] = useState<LocationHead>(initial.substring(0, 2) as LocationHead);
|
||||||
|
const [body, setBody] = useState<string>(initial.substring(3));
|
||||||
|
|
||||||
|
const location = useMemo(() => combineLocation(head, body), [head, body]);
|
||||||
|
const isValid = useMemo(() => initial !== location && validateLocation(location), [initial, location]);
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
onChangeLocation(location);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
header='Изменение расположения'
|
||||||
|
submitText='Переместить'
|
||||||
|
submitInvalidTooltip={`Допустимы буквы, цифры, подчерк, пробел и "!". Сегмент пути не может начинаться и заканчиваться пробелом. Общая длина (с корнем) не должна превышать ${limits.location_len}`}
|
||||||
|
hideWindow={hideWindow}
|
||||||
|
canSubmit={isValid}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className={clsx('w-[35rem]', 'pb-12 px-6 flex gap-3')}
|
||||||
|
>
|
||||||
|
<div className='flex flex-col gap-2 w-[7rem] h-min'>
|
||||||
|
<Label text='Корень' />
|
||||||
|
<SelectLocationHead
|
||||||
|
value={head} // prettier: split-lines
|
||||||
|
onChange={setHead}
|
||||||
|
excluded={!user?.is_staff ? [LocationHead.LIBRARY] : []}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<TextArea
|
||||||
|
id='dlg_cst_body'
|
||||||
|
label='Путь'
|
||||||
|
className='w-[23rem]'
|
||||||
|
rows={5}
|
||||||
|
value={body}
|
||||||
|
onChange={event => setBody(event.target.value)}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DlgChangeLocation;
|
|
@ -5,33 +5,47 @@ import { useMemo, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import { urls } from '@/app/urls';
|
import { urls } from '@/app/urls';
|
||||||
|
import { VisibilityIcon } from '@/components/DomainIcons';
|
||||||
|
import SelectAccessPolicy from '@/components/select/SelectAccessPolicy';
|
||||||
|
import SelectLocationHead from '@/components/select/SelectLocationHead';
|
||||||
import Checkbox from '@/components/ui/Checkbox';
|
import Checkbox from '@/components/ui/Checkbox';
|
||||||
|
import Label from '@/components/ui/Label';
|
||||||
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
import Modal, { ModalProps } from '@/components/ui/Modal';
|
import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||||
import TextArea from '@/components/ui/TextArea';
|
import TextArea from '@/components/ui/TextArea';
|
||||||
import TextInput from '@/components/ui/TextInput';
|
import TextInput from '@/components/ui/TextInput';
|
||||||
|
import { useAuth } from '@/context/AuthContext';
|
||||||
import { useLibrary } from '@/context/LibraryContext';
|
import { useLibrary } from '@/context/LibraryContext';
|
||||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||||
import { ILibraryItem } from '@/models/library';
|
import { AccessPolicy, ILibraryItem, LocationHead } from '@/models/library';
|
||||||
import { cloneTitle } from '@/models/libraryAPI';
|
import { cloneTitle, combineLocation, validateLocation } from '@/models/libraryAPI';
|
||||||
import { ConstituentaID, IRSFormCloneData } from '@/models/rsform';
|
import { ConstituentaID, IRSFormCloneData } from '@/models/rsform';
|
||||||
|
|
||||||
interface DlgCloneLibraryItemProps extends Pick<ModalProps, 'hideWindow'> {
|
interface DlgCloneLibraryItemProps extends Pick<ModalProps, 'hideWindow'> {
|
||||||
base: ILibraryItem;
|
base: ILibraryItem;
|
||||||
|
initialLocation: string;
|
||||||
selected: ConstituentaID[];
|
selected: ConstituentaID[];
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DlgCloneLibraryItem({ hideWindow, base, selected, totalCount }: DlgCloneLibraryItemProps) {
|
function DlgCloneLibraryItem({ hideWindow, base, initialLocation, selected, totalCount }: DlgCloneLibraryItemProps) {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
|
const { user } = useAuth();
|
||||||
const [title, setTitle] = useState(cloneTitle(base));
|
const [title, setTitle] = useState(cloneTitle(base));
|
||||||
const [alias, setAlias] = useState(base.alias);
|
const [alias, setAlias] = useState(base.alias);
|
||||||
const [comment, setComment] = useState(base.comment);
|
const [comment, setComment] = useState(base.comment);
|
||||||
const [common, setCommon] = useState(base.is_common);
|
const [visible, setVisible] = useState(true);
|
||||||
|
const [policy, setPolicy] = useState(AccessPolicy.PUBLIC);
|
||||||
|
|
||||||
const [onlySelected, setOnlySelected] = useState(false);
|
const [onlySelected, setOnlySelected] = useState(false);
|
||||||
|
|
||||||
|
const [head, setHead] = useState(initialLocation.substring(0, 2) as LocationHead);
|
||||||
|
const [body, setBody] = useState(initialLocation.substring(3));
|
||||||
|
const location = useMemo(() => combineLocation(head, body), [head, body]);
|
||||||
|
|
||||||
const { cloneItem } = useLibrary();
|
const { cloneItem } = useLibrary();
|
||||||
|
|
||||||
const canSubmit = useMemo(() => title !== '' && alias !== '', [title, alias]);
|
const canSubmit = useMemo(() => title !== '' && alias !== '' && validateLocation(location), [title, alias, location]);
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
const data: IRSFormCloneData = {
|
const data: IRSFormCloneData = {
|
||||||
|
@ -39,8 +53,10 @@ function DlgCloneLibraryItem({ hideWindow, base, selected, totalCount }: DlgClon
|
||||||
title: title,
|
title: title,
|
||||||
alias: alias,
|
alias: alias,
|
||||||
comment: comment,
|
comment: comment,
|
||||||
is_common: common,
|
read_only: false,
|
||||||
is_canonical: false
|
visible: visible,
|
||||||
|
access_policy: policy,
|
||||||
|
location: location
|
||||||
};
|
};
|
||||||
if (onlySelected) {
|
if (onlySelected) {
|
||||||
data.items = selected;
|
data.items = selected;
|
||||||
|
@ -58,7 +74,7 @@ function DlgCloneLibraryItem({ hideWindow, base, selected, totalCount }: DlgClon
|
||||||
canSubmit={canSubmit}
|
canSubmit={canSubmit}
|
||||||
submitText='Создать'
|
submitText='Создать'
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
className={clsx('px-6 py-2', 'cc-column')}
|
className={clsx('px-6 py-2', 'cc-column', 'max-h-full w-[30rem]')}
|
||||||
>
|
>
|
||||||
<TextInput
|
<TextInput
|
||||||
id='dlg_full_name'
|
id='dlg_full_name'
|
||||||
|
@ -66,21 +82,60 @@ function DlgCloneLibraryItem({ hideWindow, base, selected, totalCount }: DlgClon
|
||||||
value={title}
|
value={title}
|
||||||
onChange={event => setTitle(event.target.value)}
|
onChange={event => setTitle(event.target.value)}
|
||||||
/>
|
/>
|
||||||
|
<div className='flex justify-between gap-3'>
|
||||||
<TextInput
|
<TextInput
|
||||||
id='dlg_alias'
|
id='dlg_alias'
|
||||||
label='Сокращение'
|
label='Сокращение'
|
||||||
value={alias}
|
value={alias}
|
||||||
className='max-w-sm'
|
className='w-[15rem]'
|
||||||
onChange={event => setAlias(event.target.value)}
|
onChange={event => setAlias(event.target.value)}
|
||||||
/>
|
/>
|
||||||
|
<div className='flex flex-col gap-2'>
|
||||||
|
<Label text='Доступ' className='self-center select-none' />
|
||||||
|
<div className='ml-auto cc-icons'>
|
||||||
|
<SelectAccessPolicy
|
||||||
|
stretchLeft // prettier: split-lines
|
||||||
|
value={policy}
|
||||||
|
onChange={newPolicy => setPolicy(newPolicy)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MiniButton
|
||||||
|
className='disabled:cursor-auto'
|
||||||
|
title={visible ? 'Библиотека: отображать' : 'Библиотека: скрывать'}
|
||||||
|
icon={<VisibilityIcon value={visible} />}
|
||||||
|
onClick={() => setVisible(prev => !prev)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex justify-between gap-3'>
|
||||||
|
<div className='flex flex-col gap-2 w-[7rem] h-min'>
|
||||||
|
<Label text='Корень' />
|
||||||
|
<SelectLocationHead
|
||||||
|
value={head}
|
||||||
|
onChange={setHead}
|
||||||
|
excluded={!user?.is_staff ? [LocationHead.LIBRARY] : []}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<TextArea
|
||||||
|
id='dlg_cst_body'
|
||||||
|
label='Путь'
|
||||||
|
className='w-[18rem]'
|
||||||
|
rows={3}
|
||||||
|
value={body}
|
||||||
|
onChange={event => setBody(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<TextArea id='dlg_comment' label='Описание' value={comment} onChange={event => setComment(event.target.value)} />
|
<TextArea id='dlg_comment' label='Описание' value={comment} onChange={event => setComment(event.target.value)} />
|
||||||
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id='dlg_only_selected'
|
id='dlg_only_selected'
|
||||||
label={`Только выбранные конституенты [${selected.length} из ${totalCount}]`}
|
label={`Только выбранные конституенты [${selected.length} из ${totalCount}]`}
|
||||||
value={onlySelected}
|
value={onlySelected}
|
||||||
setValue={value => setOnlySelected(value)}
|
setValue={value => setOnlySelected(value)}
|
||||||
/>
|
/>
|
||||||
<Checkbox id='dlg_is_common' label='Общедоступная схема' value={common} setValue={value => setCommon(value)} />
|
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,25 @@ export enum LibraryItemType {
|
||||||
OPERATIONS_SCHEMA = 'oss'
|
OPERATIONS_SCHEMA = 'oss'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents Access policy for library items.
|
||||||
|
*/
|
||||||
|
export enum AccessPolicy {
|
||||||
|
PUBLIC = 'public',
|
||||||
|
PROTECTED = 'protected',
|
||||||
|
PRIVATE = 'private'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents valid location headers.
|
||||||
|
*/
|
||||||
|
export enum LocationHead {
|
||||||
|
USER = '/U',
|
||||||
|
COMMON = '/S',
|
||||||
|
LIBRARY = '/L',
|
||||||
|
PROJECTS = '/P'
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents {@link LibraryItem} identifier type.
|
* Represents {@link LibraryItem} identifier type.
|
||||||
*/
|
*/
|
||||||
|
@ -46,8 +65,10 @@ export interface ILibraryItem {
|
||||||
title: string;
|
title: string;
|
||||||
alias: string;
|
alias: string;
|
||||||
comment: string;
|
comment: string;
|
||||||
is_common: boolean;
|
visible: boolean;
|
||||||
is_canonical: boolean;
|
read_only: boolean;
|
||||||
|
location: string;
|
||||||
|
access_policy: AccessPolicy;
|
||||||
time_create: string;
|
time_create: string;
|
||||||
time_update: string;
|
time_update: string;
|
||||||
owner: UserID | null;
|
owner: UserID | null;
|
||||||
|
@ -66,4 +87,27 @@ export interface ILibraryItemEx extends ILibraryItem {
|
||||||
/**
|
/**
|
||||||
* Represents update data for editing {@link ILibraryItem}.
|
* Represents update data for editing {@link ILibraryItem}.
|
||||||
*/
|
*/
|
||||||
export interface ILibraryUpdateData extends Omit<ILibraryItem, 'time_create' | 'time_update' | 'id' | 'owner'> {}
|
export interface ILibraryUpdateData
|
||||||
|
extends Omit<ILibraryItem, 'time_create' | 'time_update' | 'access_policy' | 'location' | 'id' | 'owner'> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents update data for editing {@link AccessPolicy} of a {@link ILibraryItem}.
|
||||||
|
*/
|
||||||
|
export interface ITargetAccessPolicy {
|
||||||
|
access_policy: AccessPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents update data for editing Location of a {@link ILibraryItem}.
|
||||||
|
*/
|
||||||
|
export interface ITargetLocation {
|
||||||
|
location: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents data, used for creating {@link IRSForm}.
|
||||||
|
*/
|
||||||
|
export interface ILibraryCreateData extends Omit<ILibraryItem, 'time_create' | 'time_update' | 'id' | 'owner'> {
|
||||||
|
file?: File;
|
||||||
|
fileName?: string;
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { ILibraryItem, LibraryItemType } from './library';
|
import { AccessPolicy, ILibraryItem, LibraryItemType, LocationHead } from './library';
|
||||||
import { matchLibraryItem } from './libraryAPI';
|
import { matchLibraryItem, validateLocation } from './libraryAPI';
|
||||||
|
|
||||||
describe('Testing matching LibraryItem', () => {
|
describe('Testing matching LibraryItem', () => {
|
||||||
const item1: ILibraryItem = {
|
const item1: ILibraryItem = {
|
||||||
|
@ -8,11 +8,13 @@ describe('Testing matching LibraryItem', () => {
|
||||||
title: 'Item1',
|
title: 'Item1',
|
||||||
alias: 'I1',
|
alias: 'I1',
|
||||||
comment: 'comment',
|
comment: 'comment',
|
||||||
is_common: true,
|
|
||||||
is_canonical: true,
|
|
||||||
time_create: 'I2',
|
time_create: 'I2',
|
||||||
time_update: '',
|
time_update: '',
|
||||||
owner: null
|
owner: null,
|
||||||
|
access_policy: AccessPolicy.PUBLIC,
|
||||||
|
location: LocationHead.COMMON,
|
||||||
|
read_only: false,
|
||||||
|
visible: true
|
||||||
};
|
};
|
||||||
|
|
||||||
const itemEmpty: ILibraryItem = {
|
const itemEmpty: ILibraryItem = {
|
||||||
|
@ -21,11 +23,13 @@ describe('Testing matching LibraryItem', () => {
|
||||||
title: '',
|
title: '',
|
||||||
alias: '',
|
alias: '',
|
||||||
comment: '',
|
comment: '',
|
||||||
is_common: true,
|
|
||||||
is_canonical: true,
|
|
||||||
time_create: '',
|
time_create: '',
|
||||||
time_update: '',
|
time_update: '',
|
||||||
owner: null
|
owner: null,
|
||||||
|
access_policy: AccessPolicy.PUBLIC,
|
||||||
|
location: LocationHead.COMMON,
|
||||||
|
read_only: false,
|
||||||
|
visible: true
|
||||||
};
|
};
|
||||||
|
|
||||||
test('empty input', () => {
|
test('empty input', () => {
|
||||||
|
@ -43,3 +47,31 @@ describe('Testing matching LibraryItem', () => {
|
||||||
expect(matchLibraryItem(item1, item1.comment)).toEqual(false);
|
expect(matchLibraryItem(item1, item1.comment)).toEqual(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const validateLocationData = [
|
||||||
|
['', 'false'],
|
||||||
|
['U/U', 'false'],
|
||||||
|
['/A', 'false'],
|
||||||
|
['/U/user@mail', 'false'],
|
||||||
|
['U/u\\asdf', 'false'],
|
||||||
|
['/U/ asdf', 'false'],
|
||||||
|
['/User', 'false'],
|
||||||
|
['//', 'false'],
|
||||||
|
['/S/1 ', 'false'],
|
||||||
|
['/S/1/2 /3', 'false'],
|
||||||
|
|
||||||
|
['/P', 'true'],
|
||||||
|
['/L', 'true'],
|
||||||
|
['/U', 'true'],
|
||||||
|
['/S', 'true'],
|
||||||
|
['/S/Вася пупки', 'true'],
|
||||||
|
['/S/123', 'true'],
|
||||||
|
['/S/1234', 'true'],
|
||||||
|
['/S/1/!asdf/тест тест', 'true']
|
||||||
|
];
|
||||||
|
describe('Testing location validation', () => {
|
||||||
|
it.each(validateLocationData)('isValid %p', (input: string, expected: string) => {
|
||||||
|
const result = validateLocation(input);
|
||||||
|
expect(String(result)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -2,10 +2,13 @@
|
||||||
* Module: API for Library entities and Users.
|
* Module: API for Library entities and Users.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { limits } from '@/utils/constants';
|
||||||
import { TextMatcher } from '@/utils/utils';
|
import { TextMatcher } from '@/utils/utils';
|
||||||
|
|
||||||
import { ILibraryItem } from './library';
|
import { ILibraryItem } from './library';
|
||||||
|
|
||||||
|
const LOCATION_REGEXP = /^\/[PLUS]((\/[!\d\p{L}]([!\d\p{L} ]*[!\d\p{L}])?)*)?$/u; // cspell:disable-line
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a given target {@link ILibraryItem} matches the specified query.
|
* Checks if a given target {@link ILibraryItem} matches the specified query.
|
||||||
*
|
*
|
||||||
|
@ -17,6 +20,17 @@ export function matchLibraryItem(target: ILibraryItem, query: string): boolean {
|
||||||
return matcher.test(target.alias) || matcher.test(target.title);
|
return matcher.test(target.alias) || matcher.test(target.title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a given target {@link ILibraryItem} location matches the specified query.
|
||||||
|
*
|
||||||
|
* @param target - item to be matched
|
||||||
|
* @param query - text to be found
|
||||||
|
*/
|
||||||
|
export function matchLibraryItemLocation(target: ILibraryItem, path: string): boolean {
|
||||||
|
const matcher = new TextMatcher(path);
|
||||||
|
return matcher.test(target.location);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate title for clone {@link ILibraryItem}.
|
* Generate title for clone {@link ILibraryItem}.
|
||||||
*/
|
*/
|
||||||
|
@ -42,3 +56,17 @@ export function nextVersion(version: string): string {
|
||||||
}
|
}
|
||||||
return `${version.substring(0, dot)}.${lastNumber + 1}`;
|
return `${version.substring(0, dot)}.${lastNumber + 1}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validation location against regexp.
|
||||||
|
*/
|
||||||
|
export function validateLocation(location: string): boolean {
|
||||||
|
return location.length <= limits.location_len && LOCATION_REGEXP.test(location);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combining head and body into location.
|
||||||
|
*/
|
||||||
|
export function combineLocation(head: string, body?: string): string {
|
||||||
|
return body ? `${head}/${body}` : head;
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
* Module: Miscellaneous frontend model types. Future targets for refactoring aimed at extracting modules.
|
* Module: Miscellaneous frontend model types. Future targets for refactoring aimed at extracting modules.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { LocationHead } from './library';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents graph dependency mode.
|
* Represents graph dependency mode.
|
||||||
*/
|
*/
|
||||||
|
@ -124,21 +126,13 @@ export enum CstMatchMode {
|
||||||
*/
|
*/
|
||||||
export interface ILibraryFilter {
|
export interface ILibraryFilter {
|
||||||
query?: string;
|
query?: string;
|
||||||
is_owned?: boolean;
|
path?: string;
|
||||||
is_common?: boolean;
|
head?: LocationHead;
|
||||||
is_canonical?: boolean;
|
|
||||||
is_subscribed?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
isVisible?: boolean;
|
||||||
* Represents filtering strategy for Library.
|
isOwned?: boolean;
|
||||||
*/
|
isSubscribed?: boolean;
|
||||||
export enum LibraryFilterStrategy {
|
isEditor?: boolean;
|
||||||
MANUAL = 'manual',
|
|
||||||
COMMON = 'common',
|
|
||||||
SUBSCRIBE = 'subscribe',
|
|
||||||
CANONICAL = 'canonical',
|
|
||||||
OWNED = 'owned'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Module: API for miscellaneous frontend model types. Future targets for refactoring aimed at extracting modules.
|
* Module: API for miscellaneous frontend model types. Future targets for refactoring aimed at extracting modules.
|
||||||
*/
|
*/
|
||||||
import { DependencyMode, FontStyle, GraphSizing, ILibraryFilter, LibraryFilterStrategy } from './miscellaneous';
|
import { DependencyMode, FontStyle, GraphSizing } from './miscellaneous';
|
||||||
import { IConstituenta, IRSForm } from './rsform';
|
import { IConstituenta, IRSForm } from './rsform';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -42,20 +42,6 @@ export function applyGraphFilter(target: IRSForm, start: number, mode: Dependenc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter list of {@link ILibraryItem} to a given text query.
|
|
||||||
*/
|
|
||||||
export function filterFromStrategy(strategy: LibraryFilterStrategy): ILibraryFilter {
|
|
||||||
// prettier-ignore
|
|
||||||
switch (strategy) {
|
|
||||||
case LibraryFilterStrategy.MANUAL: return {};
|
|
||||||
case LibraryFilterStrategy.COMMON: return { is_common: true };
|
|
||||||
case LibraryFilterStrategy.CANONICAL: return { is_canonical: true };
|
|
||||||
case LibraryFilterStrategy.SUBSCRIBE: return { is_subscribed: true };
|
|
||||||
case LibraryFilterStrategy.OWNED: return { is_owned: true };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply {@link GraphSizing} to a given {@link IConstituenta}.
|
* Apply {@link GraphSizing} to a given {@link IConstituenta}.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
import { Graph } from '@/models/Graph';
|
import { Graph } from '@/models/Graph';
|
||||||
|
|
||||||
import { ILibraryItemEx, ILibraryUpdateData, LibraryItemID } from './library';
|
import { ILibraryItem, ILibraryItemEx, LibraryItemID } from './library';
|
||||||
import { IArgumentInfo, ParsingStatus, ValueClass } from './rslang';
|
import { IArgumentInfo, ParsingStatus, ValueClass } from './rslang';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -241,18 +241,10 @@ export interface IRSFormData extends ILibraryItemEx {
|
||||||
items: IConstituentaData[];
|
items: IConstituentaData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents data, used for creating {@link IRSForm}.
|
|
||||||
*/
|
|
||||||
export interface IRSFormCreateData extends ILibraryUpdateData {
|
|
||||||
file?: File;
|
|
||||||
fileName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents data, used for cloning {@link IRSForm}.
|
* Represents data, used for cloning {@link IRSForm}.
|
||||||
*/
|
*/
|
||||||
export interface IRSFormCloneData extends ILibraryUpdateData {
|
export interface IRSFormCloneData extends Omit<ILibraryItem, 'time_create' | 'time_update' | 'id' | 'owner'> {
|
||||||
items?: ConstituentaID[];
|
items?: ConstituentaID[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
* Module: Models for Users.
|
* Module: Models for Users.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { LibraryItemID } from './library';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents {@link User} identifier type.
|
* Represents {@link User} identifier type.
|
||||||
*/
|
*/
|
||||||
|
@ -24,7 +26,7 @@ export interface IUser {
|
||||||
* Represents CurrentUser information.
|
* Represents CurrentUser information.
|
||||||
*/
|
*/
|
||||||
export interface ICurrentUser extends Pick<IUser, 'id' | 'username' | 'is_staff'> {
|
export interface ICurrentUser extends Pick<IUser, 'id' | 'username' | 'is_staff'> {
|
||||||
subscriptions: UserID[];
|
subscriptions: LibraryItemID[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,147 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import clsx from 'clsx';
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
|
|
||||||
import { urls } from '@/app/urls';
|
|
||||||
import { IconDownload } from '@/components/Icons';
|
|
||||||
import InfoError from '@/components/info/InfoError';
|
|
||||||
import Button from '@/components/ui/Button';
|
|
||||||
import Checkbox from '@/components/ui/Checkbox';
|
|
||||||
import Label from '@/components/ui/Label';
|
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
|
||||||
import Overlay from '@/components/ui/Overlay';
|
|
||||||
import SubmitButton from '@/components/ui/SubmitButton';
|
|
||||||
import TextArea from '@/components/ui/TextArea';
|
|
||||||
import TextInput from '@/components/ui/TextInput';
|
|
||||||
import AnimateFade from '@/components/wrap/AnimateFade';
|
|
||||||
import RequireAuth from '@/components/wrap/RequireAuth';
|
|
||||||
import { useLibrary } from '@/context/LibraryContext';
|
|
||||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
|
||||||
import { LibraryItemType } from '@/models/library';
|
|
||||||
import { IRSFormCreateData } from '@/models/rsform';
|
|
||||||
import { EXTEOR_TRS_FILE, limits, patterns } from '@/utils/constants';
|
|
||||||
|
|
||||||
function CreateRSFormPage() {
|
|
||||||
const router = useConceptNavigation();
|
|
||||||
const { createItem, error, setError, processing } = useLibrary();
|
|
||||||
|
|
||||||
const [title, setTitle] = useState('');
|
|
||||||
const [alias, setAlias] = useState('');
|
|
||||||
const [comment, setComment] = useState('');
|
|
||||||
const [common, setCommon] = useState(false);
|
|
||||||
|
|
||||||
const [fileName, setFileName] = useState('');
|
|
||||||
const [file, setFile] = useState<File | undefined>();
|
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setError(undefined);
|
|
||||||
}, [title, alias, setError]);
|
|
||||||
|
|
||||||
function handleCancel() {
|
|
||||||
if (router.canBack()) {
|
|
||||||
router.back();
|
|
||||||
} else {
|
|
||||||
router.push(urls.library);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
|
||||||
event.preventDefault();
|
|
||||||
if (processing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data: IRSFormCreateData = {
|
|
||||||
item_type: LibraryItemType.RSFORM,
|
|
||||||
title: title,
|
|
||||||
alias: alias,
|
|
||||||
comment: comment,
|
|
||||||
is_common: common,
|
|
||||||
is_canonical: false,
|
|
||||||
file: file,
|
|
||||||
fileName: file?.name
|
|
||||||
};
|
|
||||||
createItem(data, newSchema => {
|
|
||||||
toast.success('Схема успешно создана');
|
|
||||||
router.push(urls.schema(newSchema.id));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleFileChange(event: React.ChangeEvent<HTMLInputElement>) {
|
|
||||||
if (event.target.files && event.target.files.length > 0) {
|
|
||||||
setFileName(event.target.files[0].name);
|
|
||||||
setFile(event.target.files[0]);
|
|
||||||
} else {
|
|
||||||
setFileName('');
|
|
||||||
setFile(undefined);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AnimateFade>
|
|
||||||
<RequireAuth>
|
|
||||||
<form className={clsx('cc-column', 'px-6 py-3')} onSubmit={handleSubmit}>
|
|
||||||
<h1>Создание концептуальной схемы</h1>
|
|
||||||
<Overlay position='top-[-2.4rem] right-[-1rem]'>
|
|
||||||
<input
|
|
||||||
id='schema_file'
|
|
||||||
ref={inputRef}
|
|
||||||
type='file'
|
|
||||||
style={{ display: 'none' }}
|
|
||||||
accept={EXTEOR_TRS_FILE}
|
|
||||||
onChange={handleFileChange}
|
|
||||||
/>
|
|
||||||
<MiniButton
|
|
||||||
title='Загрузить из Экстеор'
|
|
||||||
icon={<IconDownload size='1.25rem' className='icon-primary' />}
|
|
||||||
onClick={() => inputRef.current?.click()}
|
|
||||||
/>
|
|
||||||
</Overlay>
|
|
||||||
{fileName ? <Label text={`Загружен файл: ${fileName}`} /> : null}
|
|
||||||
|
|
||||||
<TextInput
|
|
||||||
id='schema_title'
|
|
||||||
required={!file}
|
|
||||||
label='Полное название'
|
|
||||||
placeholder={file && 'Загрузить из файла'}
|
|
||||||
value={title}
|
|
||||||
onChange={event => setTitle(event.target.value)}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
id='schema_alias'
|
|
||||||
required={!file}
|
|
||||||
label='Сокращение'
|
|
||||||
placeholder={file && 'Загрузить из файла'}
|
|
||||||
className='w-[14rem]'
|
|
||||||
pattern={patterns.library_alias}
|
|
||||||
title={`не более ${limits.library_alias_len} символов`}
|
|
||||||
value={alias}
|
|
||||||
onChange={event => setAlias(event.target.value)}
|
|
||||||
/>
|
|
||||||
<TextArea
|
|
||||||
id='schema_comment'
|
|
||||||
label='Описание'
|
|
||||||
placeholder={file && 'Загрузить из файла'}
|
|
||||||
value={comment}
|
|
||||||
onChange={event => setComment(event.target.value)}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
id='schema_common'
|
|
||||||
label='Общедоступная схема'
|
|
||||||
value={common}
|
|
||||||
setValue={value => setCommon(value ?? false)}
|
|
||||||
/>
|
|
||||||
<div className='flex justify-around gap-6 py-3'>
|
|
||||||
<SubmitButton text='Создать схему' loading={processing} className='min-w-[10rem]' />
|
|
||||||
<Button text='Отмена' className='min-w-[10rem]' onClick={() => handleCancel()} />
|
|
||||||
</div>
|
|
||||||
{error ? <InfoError error={error} /> : null}
|
|
||||||
</form>
|
|
||||||
</RequireAuth>
|
|
||||||
</AnimateFade>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CreateRSFormPage;
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import AnimateFade from '@/components/wrap/AnimateFade';
|
||||||
|
import RequireAuth from '@/components/wrap/RequireAuth';
|
||||||
|
|
||||||
|
import FormCreateItem from './FormCreateItem';
|
||||||
|
|
||||||
|
function CreateItemPage() {
|
||||||
|
return (
|
||||||
|
<AnimateFade>
|
||||||
|
<RequireAuth>
|
||||||
|
<FormCreateItem />
|
||||||
|
</RequireAuth>
|
||||||
|
</AnimateFade>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateItemPage;
|
190
rsconcept/frontend/src/pages/CreateRSFormPage/FormCreateItem.tsx
Normal file
190
rsconcept/frontend/src/pages/CreateRSFormPage/FormCreateItem.tsx
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
import { urls } from '@/app/urls';
|
||||||
|
import { VisibilityIcon } from '@/components/DomainIcons';
|
||||||
|
import { IconDownload } from '@/components/Icons';
|
||||||
|
import InfoError from '@/components/info/InfoError';
|
||||||
|
import SelectAccessPolicy from '@/components/select/SelectAccessPolicy';
|
||||||
|
import SelectLocationHead from '@/components/select/SelectLocationHead';
|
||||||
|
import Button from '@/components/ui/Button';
|
||||||
|
import Label from '@/components/ui/Label';
|
||||||
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
|
import Overlay from '@/components/ui/Overlay';
|
||||||
|
import SubmitButton from '@/components/ui/SubmitButton';
|
||||||
|
import TextArea from '@/components/ui/TextArea';
|
||||||
|
import TextInput from '@/components/ui/TextInput';
|
||||||
|
import { useAuth } from '@/context/AuthContext';
|
||||||
|
import { useLibrary } from '@/context/LibraryContext';
|
||||||
|
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||||
|
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
|
||||||
|
import { ILibraryCreateData } from '@/models/library';
|
||||||
|
import { combineLocation, validateLocation } from '@/models/libraryAPI';
|
||||||
|
import { EXTEOR_TRS_FILE, limits, patterns } from '@/utils/constants';
|
||||||
|
|
||||||
|
function FormCreateItem() {
|
||||||
|
const router = useConceptNavigation();
|
||||||
|
const { user } = useAuth();
|
||||||
|
const { createItem, error, setError, processing } = useLibrary();
|
||||||
|
|
||||||
|
const [title, setTitle] = useState('');
|
||||||
|
const [alias, setAlias] = useState('');
|
||||||
|
const [comment, setComment] = useState('');
|
||||||
|
const [visible, setVisible] = useState(true);
|
||||||
|
const [policy, setPolicy] = useState(AccessPolicy.PUBLIC);
|
||||||
|
|
||||||
|
const [head, setHead] = useState(LocationHead.USER);
|
||||||
|
const [body, setBody] = useState('');
|
||||||
|
|
||||||
|
const location = useMemo(() => combineLocation(head, body), [head, body]);
|
||||||
|
const isValid = useMemo(() => validateLocation(location), [location]);
|
||||||
|
|
||||||
|
const [fileName, setFileName] = useState('');
|
||||||
|
const [file, setFile] = useState<File | undefined>();
|
||||||
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setError(undefined);
|
||||||
|
}, [title, alias, setError]);
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
if (router.canBack()) {
|
||||||
|
router.back();
|
||||||
|
} else {
|
||||||
|
router.push(urls.library);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (processing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data: ILibraryCreateData = {
|
||||||
|
item_type: LibraryItemType.RSFORM,
|
||||||
|
title: title,
|
||||||
|
alias: alias,
|
||||||
|
comment: comment,
|
||||||
|
read_only: false,
|
||||||
|
visible: visible,
|
||||||
|
access_policy: policy,
|
||||||
|
location: location,
|
||||||
|
file: file,
|
||||||
|
fileName: file?.name
|
||||||
|
};
|
||||||
|
createItem(data, newSchema => {
|
||||||
|
toast.success('Схема успешно создана');
|
||||||
|
router.push(urls.schema(newSchema.id));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
if (event.target.files && event.target.files.length > 0) {
|
||||||
|
setFileName(event.target.files[0].name);
|
||||||
|
setFile(event.target.files[0]);
|
||||||
|
} else {
|
||||||
|
setFileName('');
|
||||||
|
setFile(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Overlay position='top-[0.5rem] right-[0.5rem]'>
|
||||||
|
<input
|
||||||
|
id='schema_file'
|
||||||
|
ref={inputRef}
|
||||||
|
type='file'
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
accept={EXTEOR_TRS_FILE}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
title='Загрузить из Экстеор'
|
||||||
|
icon={<IconDownload size='1.25rem' className='icon-primary' />}
|
||||||
|
onClick={() => inputRef.current?.click()}
|
||||||
|
/>
|
||||||
|
</Overlay>
|
||||||
|
|
||||||
|
<form className={clsx('cc-column', 'min-w-[30rem]', 'px-6 py-3')} onSubmit={handleSubmit}>
|
||||||
|
<h1>Создание концептуальной схемы</h1>
|
||||||
|
|
||||||
|
{fileName ? <Label text={`Загружен файл: ${fileName}`} /> : null}
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
id='schema_title'
|
||||||
|
required={!file}
|
||||||
|
label='Полное название'
|
||||||
|
placeholder={file && 'Загрузить из файла'}
|
||||||
|
value={title}
|
||||||
|
onChange={event => setTitle(event.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='flex justify-between gap-3'>
|
||||||
|
<TextInput
|
||||||
|
id='schema_alias'
|
||||||
|
required={!file}
|
||||||
|
label='Сокращение'
|
||||||
|
placeholder={file && 'Загрузить из файла'}
|
||||||
|
className='w-[14rem]'
|
||||||
|
pattern={patterns.library_alias}
|
||||||
|
title={`не более ${limits.library_alias_len} символов`}
|
||||||
|
value={alias}
|
||||||
|
onChange={event => setAlias(event.target.value)}
|
||||||
|
/>
|
||||||
|
<div className='flex flex-col gap-2'>
|
||||||
|
<Label text='Доступ' className='self-center select-none' />
|
||||||
|
<div className='ml-auto cc-icons'>
|
||||||
|
<SelectAccessPolicy value={policy} onChange={newPolicy => setPolicy(newPolicy)} />
|
||||||
|
|
||||||
|
<MiniButton
|
||||||
|
className='disabled:cursor-auto'
|
||||||
|
title={visible ? 'Библиотека: отображать' : 'Библиотека: скрывать'}
|
||||||
|
icon={<VisibilityIcon value={visible} />}
|
||||||
|
onClick={() => setVisible(prev => !prev)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
id='schema_comment'
|
||||||
|
label='Описание'
|
||||||
|
placeholder={file && 'Загрузить из файла'}
|
||||||
|
value={comment}
|
||||||
|
onChange={event => setComment(event.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='flex justify-between gap-3'>
|
||||||
|
<div className='flex flex-col gap-2 w-[7rem] h-min'>
|
||||||
|
<Label text='Корень' />
|
||||||
|
<SelectLocationHead
|
||||||
|
value={head}
|
||||||
|
onChange={setHead}
|
||||||
|
excluded={!user?.is_staff ? [LocationHead.LIBRARY] : []}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<TextArea
|
||||||
|
id='dlg_cst_body'
|
||||||
|
label='Путь'
|
||||||
|
className='w-[18rem]'
|
||||||
|
rows={4}
|
||||||
|
value={body}
|
||||||
|
onChange={event => setBody(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex justify-around gap-6 py-3'>
|
||||||
|
<SubmitButton text='Создать схему' loading={processing} className='min-w-[10rem]' disabled={!isValid} />
|
||||||
|
<Button text='Отмена' className='min-w-[10rem]' onClick={() => handleCancel()} />
|
||||||
|
</div>
|
||||||
|
{error ? <InfoError error={error} /> : null}
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FormCreateItem;
|
1
rsconcept/frontend/src/pages/CreateRSFormPage/index.tsx
Normal file
1
rsconcept/frontend/src/pages/CreateRSFormPage/index.tsx
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './CreateItemPage';
|
|
@ -1,30 +0,0 @@
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
import { IconImmutable, IconPublic } from '@/components/Icons';
|
|
||||||
import { ILibraryItem } from '@/models/library';
|
|
||||||
import { ICurrentUser } from '@/models/user';
|
|
||||||
import { prefixes } from '@/utils/constants';
|
|
||||||
|
|
||||||
interface ItemIconsProps {
|
|
||||||
user?: ICurrentUser;
|
|
||||||
item: ILibraryItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemIcons({ item }: ItemIconsProps) {
|
|
||||||
return (
|
|
||||||
<div className={clsx('min-w-[2.2rem]', 'inline-flex gap-1 align-middle')} id={`${prefixes.library_list}${item.id}`}>
|
|
||||||
{item.is_common ? (
|
|
||||||
<span title='Общедоступная'>
|
|
||||||
<IconPublic size='1rem' />
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
{item.is_canonical ? (
|
|
||||||
<span title='Неизменная'>
|
|
||||||
<IconImmutable size='1rem' />
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ItemIcons;
|
|
|
@ -2,71 +2,78 @@
|
||||||
|
|
||||||
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { urls } from '@/app/urls';
|
|
||||||
import DataLoader from '@/components/wrap/DataLoader';
|
import DataLoader from '@/components/wrap/DataLoader';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
|
||||||
import { useLibrary } from '@/context/LibraryContext';
|
import { useLibrary } from '@/context/LibraryContext';
|
||||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
|
||||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
import useLocalStorage from '@/hooks/useLocalStorage';
|
||||||
import useQueryStrings from '@/hooks/useQueryStrings';
|
import { ILibraryItem, LocationHead } from '@/models/library';
|
||||||
import { ILibraryItem } from '@/models/library';
|
import { ILibraryFilter } from '@/models/miscellaneous';
|
||||||
import { ILibraryFilter, LibraryFilterStrategy } from '@/models/miscellaneous';
|
|
||||||
import { filterFromStrategy } from '@/models/miscellaneousAPI';
|
|
||||||
import { storage } from '@/utils/constants';
|
import { storage } from '@/utils/constants';
|
||||||
|
import { toggleTristateFlag } from '@/utils/utils';
|
||||||
|
|
||||||
import SearchPanel from './SearchPanel';
|
import SearchPanel from './SearchPanel';
|
||||||
import ViewLibrary from './ViewLibrary';
|
import ViewLibrary from './ViewLibrary';
|
||||||
|
|
||||||
function LibraryPage() {
|
function LibraryPage() {
|
||||||
const router = useConceptNavigation();
|
|
||||||
const urlParams = useQueryStrings();
|
|
||||||
const queryFilter = (urlParams.get('filter') || null) as LibraryFilterStrategy | null;
|
|
||||||
|
|
||||||
const { user } = useAuth();
|
|
||||||
|
|
||||||
const library = useLibrary();
|
const library = useLibrary();
|
||||||
|
|
||||||
const [filter, setFilter] = useState<ILibraryFilter>({});
|
|
||||||
const [items, setItems] = useState<ILibraryItem[]>([]);
|
const [items, setItems] = useState<ILibraryItem[]>([]);
|
||||||
|
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
const [strategy, setStrategy] = useLocalStorage<LibraryFilterStrategy>(
|
const [path, setPath] = useState('');
|
||||||
storage.librarySearchStrategy,
|
|
||||||
LibraryFilterStrategy.MANUAL
|
|
||||||
);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
const [head, setHead] = useLocalStorage<LocationHead | undefined>(storage.librarySearchHead, undefined);
|
||||||
if (!queryFilter || !Object.values(LibraryFilterStrategy).includes(queryFilter)) {
|
const [isVisible, setIsVisible] = useLocalStorage<boolean | undefined>(storage.librarySearchVisible, true);
|
||||||
router.replace(urls.library_filter(strategy));
|
const [isSubscribed, setIsSubscribed] = useLocalStorage<boolean | undefined>(
|
||||||
return;
|
storage.librarySearchSubscribed,
|
||||||
}
|
undefined
|
||||||
setQuery('');
|
);
|
||||||
setStrategy(queryFilter);
|
const [isOwned, setIsOwned] = useLocalStorage<boolean | undefined>(storage.librarySearchEditor, undefined);
|
||||||
setFilter(filterFromStrategy(queryFilter));
|
const [isEditor, setIsEditor] = useLocalStorage<boolean | undefined>(storage.librarySearchEditor, undefined);
|
||||||
}, [user, router, setQuery, setFilter, setStrategy, strategy, queryFilter]);
|
|
||||||
|
const filter: ILibraryFilter = useMemo(
|
||||||
|
() => ({
|
||||||
|
head: head,
|
||||||
|
path: path,
|
||||||
|
query: query,
|
||||||
|
isEditor: isEditor,
|
||||||
|
isOwned: isOwned,
|
||||||
|
isSubscribed: isSubscribed,
|
||||||
|
isVisible: isVisible
|
||||||
|
}),
|
||||||
|
[head, path, query, isEditor, isOwned, isSubscribed, isVisible]
|
||||||
|
);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
setItems(library.applyFilter(filter));
|
setItems(library.applyFilter(filter));
|
||||||
}, [library, filter, filter.query]);
|
}, [library, filter, filter.query]);
|
||||||
|
|
||||||
const resetQuery = useCallback(() => {
|
const toggleVisible = useCallback(() => setIsVisible(prev => toggleTristateFlag(prev)), [setIsVisible]);
|
||||||
|
const toggleOwned = useCallback(() => setIsOwned(prev => toggleTristateFlag(prev)), [setIsOwned]);
|
||||||
|
const toggleSubscribed = useCallback(() => setIsSubscribed(prev => toggleTristateFlag(prev)), [setIsSubscribed]);
|
||||||
|
const toggleEditor = useCallback(() => setIsEditor(prev => toggleTristateFlag(prev)), [setIsEditor]);
|
||||||
|
|
||||||
|
const resetFilter = useCallback(() => {
|
||||||
setQuery('');
|
setQuery('');
|
||||||
setFilter({});
|
setPath('');
|
||||||
}, []);
|
setHead(undefined);
|
||||||
|
setIsVisible(true);
|
||||||
|
setIsSubscribed(undefined);
|
||||||
|
setIsOwned(undefined);
|
||||||
|
setIsEditor(undefined);
|
||||||
|
}, [setHead, setIsVisible, setIsSubscribed, setIsOwned, setIsEditor]);
|
||||||
|
|
||||||
const view = useMemo(
|
const view = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<ViewLibrary
|
<ViewLibrary
|
||||||
resetQuery={resetQuery} //
|
resetQuery={resetFilter} // prettier: split lines
|
||||||
items={items}
|
items={items}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[resetQuery, items]
|
[resetFilter, items]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataLoader
|
<DataLoader
|
||||||
id='library-page' //
|
id='library-page' // prettier: split lines
|
||||||
isLoading={library.loading}
|
isLoading={library.loading}
|
||||||
error={library.error}
|
error={library.error}
|
||||||
hasNoData={library.items.length === 0}
|
hasNoData={library.items.length === 0}
|
||||||
|
@ -74,10 +81,20 @@ function LibraryPage() {
|
||||||
<SearchPanel
|
<SearchPanel
|
||||||
query={query}
|
query={query}
|
||||||
setQuery={setQuery}
|
setQuery={setQuery}
|
||||||
strategy={strategy}
|
path={path}
|
||||||
|
setPath={setPath}
|
||||||
|
head={head}
|
||||||
|
setHead={setHead}
|
||||||
total={library.items.length ?? 0}
|
total={library.items.length ?? 0}
|
||||||
filtered={items.length}
|
filtered={items.length}
|
||||||
setFilter={setFilter}
|
isVisible={isVisible}
|
||||||
|
isOwned={isOwned}
|
||||||
|
toggleOwned={toggleOwned}
|
||||||
|
toggleVisible={toggleVisible}
|
||||||
|
isSubscribed={isSubscribed}
|
||||||
|
toggleSubscribed={toggleSubscribed}
|
||||||
|
isEditor={isEditor}
|
||||||
|
toggleEditor={toggleEditor}
|
||||||
/>
|
/>
|
||||||
{view}
|
{view}
|
||||||
</DataLoader>
|
</DataLoader>
|
||||||
|
|
|
@ -1,51 +1,71 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { urls } from '@/app/urls';
|
import { LocationHeadIcon, SubscribeIcon, VisibilityIcon } from '@/components/DomainIcons';
|
||||||
|
import { IconEditor, IconFolder, IconOwner } from '@/components/Icons';
|
||||||
|
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||||
|
import Dropdown from '@/components/ui/Dropdown';
|
||||||
|
import DropdownButton from '@/components/ui/DropdownButton';
|
||||||
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
import SearchBar from '@/components/ui/SearchBar';
|
import SearchBar from '@/components/ui/SearchBar';
|
||||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
import SelectorButton from '@/components/ui/SelectorButton';
|
||||||
import { ILibraryFilter } from '@/models/miscellaneous';
|
import useDropdown from '@/hooks/useDropdown';
|
||||||
import { LibraryFilterStrategy } from '@/models/miscellaneous';
|
import { LocationHead } from '@/models/library';
|
||||||
|
import { HelpTopic } from '@/models/miscellaneous';
|
||||||
import SelectFilterStrategy from '../../components/select/SelectFilterStrategy';
|
import { prefixes } from '@/utils/constants';
|
||||||
|
import { describeLocationHead, labelLocationHead } from '@/utils/labels';
|
||||||
|
import { tripleToggleColor } from '@/utils/utils';
|
||||||
|
|
||||||
interface SearchPanelProps {
|
interface SearchPanelProps {
|
||||||
total: number;
|
total: number;
|
||||||
filtered: number;
|
filtered: number;
|
||||||
setFilter: React.Dispatch<React.SetStateAction<ILibraryFilter>>;
|
|
||||||
query: string;
|
query: string;
|
||||||
setQuery: React.Dispatch<React.SetStateAction<string>>;
|
setQuery: React.Dispatch<React.SetStateAction<string>>;
|
||||||
strategy: LibraryFilterStrategy;
|
path: string;
|
||||||
|
setPath: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
head: LocationHead | undefined;
|
||||||
|
setHead: React.Dispatch<React.SetStateAction<LocationHead | undefined>>;
|
||||||
|
|
||||||
|
isVisible: boolean | undefined;
|
||||||
|
toggleVisible: () => void;
|
||||||
|
isOwned: boolean | undefined;
|
||||||
|
toggleOwned: () => void;
|
||||||
|
isSubscribed: boolean | undefined;
|
||||||
|
toggleSubscribed: () => void;
|
||||||
|
isEditor: boolean | undefined;
|
||||||
|
toggleEditor: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SearchPanel({ total, filtered, query, setQuery, strategy, setFilter }: SearchPanelProps) {
|
function SearchPanel({
|
||||||
const router = useConceptNavigation();
|
total,
|
||||||
|
filtered,
|
||||||
|
query,
|
||||||
|
setQuery,
|
||||||
|
path,
|
||||||
|
setPath,
|
||||||
|
head,
|
||||||
|
setHead,
|
||||||
|
|
||||||
function handleChangeQuery(newQuery: string) {
|
isVisible,
|
||||||
setQuery(newQuery);
|
toggleVisible,
|
||||||
setFilter(prev => ({
|
isOwned,
|
||||||
query: newQuery,
|
toggleOwned,
|
||||||
is_owned: prev.is_owned,
|
isSubscribed,
|
||||||
is_common: prev.is_common,
|
toggleSubscribed,
|
||||||
is_canonical: prev.is_canonical,
|
isEditor,
|
||||||
is_subscribed: prev.is_subscribed
|
toggleEditor
|
||||||
}));
|
}: SearchPanelProps) {
|
||||||
}
|
const headMenu = useDropdown();
|
||||||
|
|
||||||
const handleChangeStrategy = useCallback(
|
const handleChange = useCallback(
|
||||||
(value: LibraryFilterStrategy) => {
|
(newValue: LocationHead | undefined) => {
|
||||||
if (value !== strategy) {
|
headMenu.hide();
|
||||||
router.push(urls.library_filter(value));
|
setHead(newValue);
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[strategy, router]
|
[headMenu, setHead]
|
||||||
);
|
|
||||||
|
|
||||||
const selectStrategy = useMemo(
|
|
||||||
() => <SelectFilterStrategy value={strategy} onChange={handleChangeStrategy} />,
|
|
||||||
[strategy, handleChangeStrategy]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -53,33 +73,107 @@ function SearchPanel({ total, filtered, query, setQuery, strategy, setFilter }:
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'sticky top-0', // prettier: split lines
|
'sticky top-0', // prettier: split lines
|
||||||
'w-full h-[2.2rem]',
|
'w-full h-[2.2rem]',
|
||||||
'sm:pr-[12rem] flex',
|
'pr-3 flex items-center',
|
||||||
'border-b',
|
'border-b',
|
||||||
'text-sm',
|
'text-sm',
|
||||||
'clr-input'
|
'clr-input'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div className={clsx('px-2 self-center', 'min-w-[9rem]', 'select-none', 'whitespace-nowrap')}>
|
||||||
className={clsx(
|
|
||||||
'min-w-[10rem]', // prettier: split lines
|
|
||||||
'px-2 self-center',
|
|
||||||
'select-none',
|
|
||||||
'whitespace-nowrap'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Фильтр
|
Фильтр
|
||||||
<span className='ml-2'>
|
<span className='ml-2'>
|
||||||
{filtered} из {total}
|
{filtered} из {total}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{selectStrategy}
|
|
||||||
|
<div className='cc-icons'>
|
||||||
|
<MiniButton
|
||||||
|
title='Видимость'
|
||||||
|
icon={<VisibilityIcon value={true} className={tripleToggleColor(isVisible)} />}
|
||||||
|
onClick={toggleVisible}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
title='Я - Подписчик'
|
||||||
|
icon={<SubscribeIcon value={true} className={tripleToggleColor(isSubscribed)} />}
|
||||||
|
onClick={toggleSubscribed}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
title='Я - Владелец'
|
||||||
|
icon={<IconOwner size='1.25rem' className={tripleToggleColor(isOwned)} />}
|
||||||
|
onClick={toggleOwned}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MiniButton
|
||||||
|
title='Я - Редактор'
|
||||||
|
icon={<IconEditor size='1.25rem' className={tripleToggleColor(isEditor)} />}
|
||||||
|
onClick={toggleEditor}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex items-center h-full mx-auto'>
|
||||||
<SearchBar
|
<SearchBar
|
||||||
id='library_search'
|
id='library_search'
|
||||||
|
placeholder='Аттрибуты'
|
||||||
noBorder
|
noBorder
|
||||||
className='mx-auto min-w-[10rem]'
|
className='min-w-[10rem]'
|
||||||
value={query}
|
value={query}
|
||||||
onChange={handleChangeQuery}
|
onChange={setQuery}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div ref={headMenu.ref} className='flex items-center h-full py-1 select-none'>
|
||||||
|
<SelectorButton
|
||||||
|
transparent
|
||||||
|
className='h-full rounded-lg'
|
||||||
|
title={head ? describeLocationHead(head) : 'Выберите каталог'}
|
||||||
|
hideTitle={headMenu.isOpen}
|
||||||
|
icon={
|
||||||
|
head ? (
|
||||||
|
<LocationHeadIcon value={head} size='1.25rem' />
|
||||||
|
) : (
|
||||||
|
<IconFolder size='1.25rem' className='clr-text-controls' />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onClick={headMenu.toggle}
|
||||||
|
text={head ?? '//'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Dropdown isOpen={headMenu.isOpen} className='z-modalTooltip'>
|
||||||
|
<DropdownButton className='w-[10rem]' onClick={() => handleChange(undefined)}>
|
||||||
|
<div className='inline-flex items-center gap-3'>
|
||||||
|
<IconFolder size='1rem' className='clr-text-controls' />
|
||||||
|
<span>отображать все</span>
|
||||||
|
</div>
|
||||||
|
</DropdownButton>
|
||||||
|
{Object.values(LocationHead).map((head, index) => {
|
||||||
|
return (
|
||||||
|
<DropdownButton
|
||||||
|
className='w-[10rem]'
|
||||||
|
key={`${prefixes.location_head_list}${index}`}
|
||||||
|
onClick={() => handleChange(head)}
|
||||||
|
title={describeLocationHead(head)}
|
||||||
|
>
|
||||||
|
<div className='inline-flex items-center gap-3'>
|
||||||
|
<LocationHeadIcon value={head} size='1rem' />
|
||||||
|
{labelLocationHead(head)}
|
||||||
|
</div>
|
||||||
|
</DropdownButton>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SearchBar
|
||||||
|
id='path_search'
|
||||||
|
placeholder='Путь'
|
||||||
|
noIcon
|
||||||
|
noBorder
|
||||||
|
className='min-w-[10rem]'
|
||||||
|
value={path}
|
||||||
|
onChange={setPath}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BadgeHelp topic={HelpTopic.UI_LIBRARY} className='max-w-[30rem] text-sm' offset={5} place='right-start' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
|
||||||
import { useLayoutEffect, useMemo, useState } from 'react';
|
import { useLayoutEffect, useMemo, useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { urls } from '@/app/urls';
|
import { urls } from '@/app/urls';
|
||||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
|
||||||
import { CProps } from '@/components/props';
|
import { CProps } from '@/components/props';
|
||||||
import DataTable, { createColumnHelper, VisibilityState } from '@/components/ui/DataTable';
|
import DataTable, { createColumnHelper, VisibilityState } from '@/components/ui/DataTable';
|
||||||
import FlexColumn from '@/components/ui/FlexColumn';
|
import FlexColumn from '@/components/ui/FlexColumn';
|
||||||
|
@ -16,11 +14,8 @@ import { useUsers } from '@/context/UsersContext';
|
||||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
import useLocalStorage from '@/hooks/useLocalStorage';
|
||||||
import useWindowSize from '@/hooks/useWindowSize';
|
import useWindowSize from '@/hooks/useWindowSize';
|
||||||
import { ILibraryItem } from '@/models/library';
|
import { ILibraryItem } from '@/models/library';
|
||||||
import { HelpTopic } from '@/models/miscellaneous';
|
|
||||||
import { storage } from '@/utils/constants';
|
import { storage } from '@/utils/constants';
|
||||||
|
|
||||||
import ItemIcons from './ItemIcons';
|
|
||||||
|
|
||||||
interface ViewLibraryProps {
|
interface ViewLibraryProps {
|
||||||
items: ILibraryItem[];
|
items: ILibraryItem[];
|
||||||
resetQuery: () => void;
|
resetQuery: () => void;
|
||||||
|
@ -51,14 +46,6 @@ function ViewLibrary({ items, resetQuery }: ViewLibraryProps) {
|
||||||
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() => [
|
() => [
|
||||||
columnHelper.display({
|
|
||||||
id: 'status',
|
|
||||||
header: '',
|
|
||||||
size: 60,
|
|
||||||
minSize: 60,
|
|
||||||
maxSize: 60,
|
|
||||||
cell: props => <ItemIcons item={props.row.original} />
|
|
||||||
}),
|
|
||||||
columnHelper.accessor('alias', {
|
columnHelper.accessor('alias', {
|
||||||
id: 'alias',
|
id: 'alias',
|
||||||
header: 'Шифр',
|
header: 'Шифр',
|
||||||
|
@ -66,6 +53,7 @@ function ViewLibrary({ items, resetQuery }: ViewLibraryProps) {
|
||||||
minSize: 80,
|
minSize: 80,
|
||||||
maxSize: 150,
|
maxSize: 150,
|
||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
|
cell: props => <div className='pl-2'>{props.getValue()}</div>,
|
||||||
sortingFn: 'text'
|
sortingFn: 'text'
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('title', {
|
columnHelper.accessor('title', {
|
||||||
|
@ -114,19 +102,6 @@ function ViewLibrary({ items, resetQuery }: ViewLibraryProps) {
|
||||||
const tableHeight = useMemo(() => calculateHeight('2.2rem'), [calculateHeight]);
|
const tableHeight = useMemo(() => calculateHeight('2.2rem'), [calculateHeight]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<div className='sticky top-[2.3rem]'>
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
'z-pop', // prettier: split lines
|
|
||||||
'absolute top-[0.125rem] left-[0.25rem]',
|
|
||||||
'ml-3',
|
|
||||||
'flex gap-1'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<BadgeHelp topic={HelpTopic.UI_LIBRARY} className='max-w-[30rem] text-sm' offset={5} place='right-start' />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DataTable
|
<DataTable
|
||||||
id='library_data'
|
id='library_data'
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
@ -155,7 +130,6 @@ function ViewLibrary({ items, resetQuery }: ViewLibraryProps) {
|
||||||
onChangePaginationOption={setItemsPerPage}
|
onChangePaginationOption={setItemsPerPage}
|
||||||
paginationOptions={[10, 20, 30, 50, 100]}
|
paginationOptions={[10, 20, 30, 50, 100]}
|
||||||
/>
|
/>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownProps) {
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'absolute left-0 w-[13.5rem]', // prettier: split-lines
|
'absolute left-0 w-[13.5rem]', // prettier: split-lines
|
||||||
'flex flex-col',
|
'flex flex-col',
|
||||||
'z-modal-tooltip',
|
'z-modalTooltip',
|
||||||
'text-xs sm:text-sm',
|
'text-xs sm:text-sm',
|
||||||
'select-none',
|
'select-none',
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { IconEditor, IconList, IconNewItem, IconShare, IconUpload } from '@/components/Icons';
|
import { IconEditor, IconNewItem, IconShare, IconUpload, IconVersions } from '@/components/Icons';
|
||||||
|
|
||||||
function HelpVersions() {
|
function HelpVersions() {
|
||||||
return (
|
return (
|
||||||
|
@ -23,7 +23,7 @@ function HelpVersions() {
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<IconList size='1.25rem' className='inline-icon' /> Редактировать атрибуты версий
|
<IconVersions size='1.25rem' className='inline-icon' /> Редактировать атрибуты версий
|
||||||
</li>
|
</li>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { VisibilityIcon } from '@/components/DomainIcons';
|
||||||
|
import { IconImmutable, IconMutable } from '@/components/Icons';
|
||||||
|
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||||
|
import SelectAccessPolicy from '@/components/select/SelectAccessPolicy';
|
||||||
|
import Label from '@/components/ui/Label';
|
||||||
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
|
import Overlay from '@/components/ui/Overlay';
|
||||||
|
import { useAccessMode } from '@/context/AccessModeContext';
|
||||||
|
import { AccessPolicy } from '@/models/library';
|
||||||
|
import { HelpTopic } from '@/models/miscellaneous';
|
||||||
|
import { UserLevel } from '@/models/user';
|
||||||
|
|
||||||
|
import { useRSEdit } from '../RSEditContext';
|
||||||
|
|
||||||
|
interface AccessToolbarProps {
|
||||||
|
visible: boolean;
|
||||||
|
toggleVisible: () => void;
|
||||||
|
readOnly: boolean;
|
||||||
|
toggleReadOnly: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccessToolbar({ visible, toggleVisible, readOnly, toggleReadOnly }: AccessToolbarProps) {
|
||||||
|
const controller = useRSEdit();
|
||||||
|
const { accessLevel } = useAccessMode();
|
||||||
|
const policy = useMemo(
|
||||||
|
() => controller.schema?.access_policy ?? AccessPolicy.PRIVATE,
|
||||||
|
[controller.schema?.access_policy]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Overlay position='top-[4.5rem] right-0 w-[12rem] pr-2' className='flex'>
|
||||||
|
<Label text='Доступ' className='self-center select-none' />
|
||||||
|
<div className='ml-auto cc-icons'>
|
||||||
|
<SelectAccessPolicy
|
||||||
|
disabled={accessLevel <= UserLevel.EDITOR || controller.isProcessing}
|
||||||
|
value={policy}
|
||||||
|
onChange={newPolicy => controller.setAccessPolicy(newPolicy)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MiniButton
|
||||||
|
className='disabled:cursor-auto'
|
||||||
|
title={visible ? 'Библиотека: отображать' : 'Библиотека: скрывать'}
|
||||||
|
icon={<VisibilityIcon value={visible} />}
|
||||||
|
onClick={toggleVisible}
|
||||||
|
disabled={accessLevel === UserLevel.READER || controller.isProcessing}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MiniButton
|
||||||
|
className='disabled:cursor-auto'
|
||||||
|
title={readOnly ? 'Изменение: запрещено' : 'Изменение: разрешено'}
|
||||||
|
icon={
|
||||||
|
readOnly ? (
|
||||||
|
<IconImmutable size='1.25rem' className='clr-text-primary' />
|
||||||
|
) : (
|
||||||
|
<IconMutable size='1.25rem' className='clr-text-green' />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onClick={toggleReadOnly}
|
||||||
|
disabled={accessLevel === UserLevel.READER || controller.isProcessing}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<BadgeHelp topic={HelpTopic.VERSIONS} className='max-w-[30rem]' offset={4} />
|
||||||
|
</div>
|
||||||
|
</Overlay>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AccessToolbar;
|
|
@ -50,7 +50,20 @@ function EditorLibraryItem({ item, isModified }: EditorLibraryItemProps) {
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
{accessLevel >= UserLevel.OWNER ? (
|
{accessLevel >= UserLevel.OWNER ? (
|
||||||
<Overlay position='top-[-0.5rem] left-[6rem] cc-icons'>
|
<Overlay position='top-[-0.5rem] left-[2.3rem] cc-icons'>
|
||||||
|
<MiniButton
|
||||||
|
title='Изменить путь'
|
||||||
|
noHover
|
||||||
|
onClick={() => controller.promptLocation()}
|
||||||
|
icon={<IconEdit size='1rem' className='mt-1 icon-primary' />}
|
||||||
|
disabled={isModified || controller.isProcessing}
|
||||||
|
/>
|
||||||
|
</Overlay>
|
||||||
|
) : null}
|
||||||
|
<LabeledValue className='sm:mb-1 text-ellipsis max-w-[30rem]' label='Путь' text={item?.location ?? ''} />
|
||||||
|
|
||||||
|
{accessLevel >= UserLevel.OWNER ? (
|
||||||
|
<Overlay position='top-[-0.5rem] left-[5.5rem] cc-icons'>
|
||||||
<div className='flex items-start'>
|
<div className='flex items-start'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Изменить владельца'
|
title='Изменить владельца'
|
||||||
|
@ -61,7 +74,7 @@ function EditorLibraryItem({ item, isModified }: EditorLibraryItemProps) {
|
||||||
/>
|
/>
|
||||||
{ownerSelector.isOpen ? (
|
{ownerSelector.isOpen ? (
|
||||||
<SelectUser
|
<SelectUser
|
||||||
className='w-[20rem] sm:w-[22.5rem] text-sm'
|
className='w-[21rem] sm:w-[23rem] text-sm'
|
||||||
items={users}
|
items={users}
|
||||||
value={item?.owner ?? undefined}
|
value={item?.owner ?? undefined}
|
||||||
onSelectValue={onSelectUser}
|
onSelectValue={onSelectUser}
|
||||||
|
@ -73,7 +86,7 @@ function EditorLibraryItem({ item, isModified }: EditorLibraryItemProps) {
|
||||||
<LabeledValue className='sm:mb-1' label='Владелец' text={getUserLabel(item?.owner ?? null)} />
|
<LabeledValue className='sm:mb-1' label='Владелец' text={getUserLabel(item?.owner ?? null)} />
|
||||||
|
|
||||||
{accessLevel >= UserLevel.OWNER ? (
|
{accessLevel >= UserLevel.OWNER ? (
|
||||||
<Overlay position='top-[-0.5rem] left-[6rem] 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='Изменить редакторов'
|
||||||
|
@ -86,12 +99,12 @@ function EditorLibraryItem({ item, isModified }: EditorLibraryItemProps) {
|
||||||
</Overlay>
|
</Overlay>
|
||||||
) : null}
|
) : null}
|
||||||
<LabeledValue id='editor_stats' className='sm:mb-1' label='Редакторы' text={item?.editors.length ?? 0} />
|
<LabeledValue id='editor_stats' className='sm:mb-1' label='Редакторы' text={item?.editors.length ?? 0} />
|
||||||
<Tooltip anchorSelect='#editor_stats' layer='z-modal-tooltip'>
|
<Tooltip anchorSelect='#editor_stats' layer='z-modalTooltip'>
|
||||||
<InfoUsers items={item?.editors ?? []} prefix={prefixes.user_editors} />
|
<InfoUsers items={item?.editors ?? []} prefix={prefixes.user_editors} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<LabeledValue id='sub_stats' className='sm:mb-1' label='Отслеживают' text={item?.subscribers.length ?? 0} />
|
<LabeledValue id='sub_stats' className='sm:mb-1' label='Отслеживают' text={item?.subscribers.length ?? 0} />
|
||||||
<Tooltip anchorSelect='#sub_stats' layer='z-modal-tooltip'>
|
<Tooltip anchorSelect='#sub_stats' layer='z-modalTooltip'>
|
||||||
<InfoUsers items={item?.subscribers ?? []} prefix={prefixes.user_subs} />
|
<InfoUsers items={item?.subscribers ?? []} prefix={prefixes.user_subs} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ interface EditorRSFormProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditorRSForm({ isModified, onDestroy, setIsModified }: EditorRSFormProps) {
|
function EditorRSForm({ isModified, onDestroy, setIsModified }: EditorRSFormProps) {
|
||||||
const { schema, isClaimable, isSubscribed } = useRSForm();
|
const { schema, isSubscribed } = useRSForm();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
function initiateSubmit() {
|
function initiateSubmit() {
|
||||||
|
@ -45,13 +45,12 @@ function EditorRSForm({ isModified, onDestroy, setIsModified }: EditorRSFormProp
|
||||||
<RSFormToolbar
|
<RSFormToolbar
|
||||||
subscribed={isSubscribed}
|
subscribed={isSubscribed}
|
||||||
modified={isModified}
|
modified={isModified}
|
||||||
claimable={isClaimable}
|
|
||||||
anonymous={!user}
|
anonymous={!user}
|
||||||
onSubmit={initiateSubmit}
|
onSubmit={initiateSubmit}
|
||||||
onDestroy={onDestroy}
|
onDestroy={onDestroy}
|
||||||
/>
|
/>
|
||||||
<AnimateFade onKeyDown={handleInput} className={clsx('sm:w-fit w-full', 'flex flex-col sm:flex-row')}>
|
<AnimateFade onKeyDown={handleInput} className={clsx('sm:w-fit mx-auto', 'flex flex-col sm:flex-row')}>
|
||||||
<FlexColumn className='px-4 pb-2'>
|
<FlexColumn className='px-3'>
|
||||||
<FormRSForm id={globals.library_item_editor} isModified={isModified} setIsModified={setIsModified} />
|
<FormRSForm id={globals.library_item_editor} isModified={isModified} setIsModified={setIsModified} />
|
||||||
|
|
||||||
<Divider margins='my-1' />
|
<Divider margins='my-1' />
|
||||||
|
|
|
@ -4,24 +4,19 @@ import clsx from 'clsx';
|
||||||
import { useEffect, useLayoutEffect, useState } from 'react';
|
import { useEffect, useLayoutEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import { IconList, IconNewItem, IconSave, IconUpload } from '@/components/Icons';
|
import { IconSave } from '@/components/Icons';
|
||||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
|
||||||
import SelectVersion from '@/components/select/SelectVersion';
|
import SelectVersion from '@/components/select/SelectVersion';
|
||||||
import Checkbox from '@/components/ui/Checkbox';
|
|
||||||
import Label from '@/components/ui/Label';
|
import Label from '@/components/ui/Label';
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
|
||||||
import Overlay from '@/components/ui/Overlay';
|
|
||||||
import SubmitButton from '@/components/ui/SubmitButton';
|
import SubmitButton from '@/components/ui/SubmitButton';
|
||||||
import TextArea from '@/components/ui/TextArea';
|
import TextArea from '@/components/ui/TextArea';
|
||||||
import TextInput from '@/components/ui/TextInput';
|
import TextInput from '@/components/ui/TextInput';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
|
||||||
import { useRSForm } from '@/context/RSFormContext';
|
import { useRSForm } from '@/context/RSFormContext';
|
||||||
import { LibraryItemType } from '@/models/library';
|
import { ILibraryUpdateData, LibraryItemType } from '@/models/library';
|
||||||
import { HelpTopic } from '@/models/miscellaneous';
|
|
||||||
import { IRSFormCreateData } from '@/models/rsform';
|
|
||||||
import { limits, patterns } from '@/utils/constants';
|
import { limits, patterns } from '@/utils/constants';
|
||||||
|
|
||||||
import { useRSEdit } from '../RSEditContext';
|
import { useRSEdit } from '../RSEditContext';
|
||||||
|
import AccessToolbar from './AccessToolbar';
|
||||||
|
import VersionsToolbar from './VersionsToolbar';
|
||||||
|
|
||||||
interface FormRSFormProps {
|
interface FormRSFormProps {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
@ -31,14 +26,13 @@ interface FormRSFormProps {
|
||||||
|
|
||||||
function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
|
function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
|
||||||
const { schema, update, processing } = useRSForm();
|
const { schema, update, processing } = useRSForm();
|
||||||
const { user } = useAuth();
|
|
||||||
const controller = useRSEdit();
|
const controller = useRSEdit();
|
||||||
|
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
const [alias, setAlias] = useState('');
|
const [alias, setAlias] = useState('');
|
||||||
const [comment, setComment] = useState('');
|
const [comment, setComment] = useState('');
|
||||||
const [common, setCommon] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const [canonical, setCanonical] = useState(false);
|
const [readOnly, setReadOnly] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!schema) {
|
if (!schema) {
|
||||||
|
@ -49,8 +43,8 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
|
||||||
schema.title !== title ||
|
schema.title !== title ||
|
||||||
schema.alias !== alias ||
|
schema.alias !== alias ||
|
||||||
schema.comment !== comment ||
|
schema.comment !== comment ||
|
||||||
schema.is_common !== common ||
|
schema.visible !== visible ||
|
||||||
schema.is_canonical !== canonical
|
schema.read_only !== readOnly
|
||||||
);
|
);
|
||||||
return () => setIsModified(false);
|
return () => setIsModified(false);
|
||||||
}, [
|
}, [
|
||||||
|
@ -58,13 +52,13 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
|
||||||
schema?.title,
|
schema?.title,
|
||||||
schema?.alias,
|
schema?.alias,
|
||||||
schema?.comment,
|
schema?.comment,
|
||||||
schema?.is_common,
|
schema?.visible,
|
||||||
schema?.is_canonical,
|
schema?.read_only,
|
||||||
title,
|
title,
|
||||||
alias,
|
alias,
|
||||||
comment,
|
comment,
|
||||||
common,
|
visible,
|
||||||
canonical,
|
readOnly,
|
||||||
setIsModified
|
setIsModified
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -73,8 +67,8 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
|
||||||
setTitle(schema.title);
|
setTitle(schema.title);
|
||||||
setAlias(schema.alias);
|
setAlias(schema.alias);
|
||||||
setComment(schema.comment);
|
setComment(schema.comment);
|
||||||
setCommon(schema.is_common);
|
setVisible(schema.visible);
|
||||||
setCanonical(schema.is_canonical);
|
setReadOnly(schema.read_only);
|
||||||
}
|
}
|
||||||
}, [schema]);
|
}, [schema]);
|
||||||
|
|
||||||
|
@ -82,28 +76,29 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
|
||||||
if (event) {
|
if (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
const data: IRSFormCreateData = {
|
const data: ILibraryUpdateData = {
|
||||||
item_type: LibraryItemType.RSFORM,
|
item_type: LibraryItemType.RSFORM,
|
||||||
title: title,
|
title: title,
|
||||||
alias: alias,
|
alias: alias,
|
||||||
comment: comment,
|
comment: comment,
|
||||||
is_common: common,
|
visible: visible,
|
||||||
is_canonical: canonical
|
read_only: readOnly
|
||||||
};
|
};
|
||||||
update(data, () => toast.success('Изменения сохранены'));
|
update(data, () => toast.success('Изменения сохранены'));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form id={id} className={clsx('cc-column', 'mt-1 min-w-[22rem] sm:w-[30rem]', 'py-1')} onSubmit={handleSubmit}>
|
<form id={id} className={clsx('mt-1 min-w-[22rem] sm:w-[30rem]', 'flex flex-col pt-1')} onSubmit={handleSubmit}>
|
||||||
<TextInput
|
<TextInput
|
||||||
id='schema_title'
|
id='schema_title'
|
||||||
required
|
required
|
||||||
label='Полное название'
|
label='Полное название'
|
||||||
|
className='mb-3'
|
||||||
value={title}
|
value={title}
|
||||||
disabled={!controller.isContentEditable}
|
disabled={!controller.isContentEditable}
|
||||||
onChange={event => setTitle(event.target.value)}
|
onChange={event => setTitle(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<div className='flex justify-between w-full gap-3'>
|
<div className='flex justify-between w-full gap-3 mb-3'>
|
||||||
<TextInput
|
<TextInput
|
||||||
id='schema_alias'
|
id='schema_alias'
|
||||||
required
|
required
|
||||||
|
@ -116,40 +111,24 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
|
||||||
onChange={event => setAlias(event.target.value)}
|
onChange={event => setAlias(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
<Overlay position='top-[-0.25rem] right-[-0.25rem] cc-icons'>
|
<VersionsToolbar />
|
||||||
{controller.isMutable ? (
|
<AccessToolbar
|
||||||
<>
|
visible={visible}
|
||||||
<MiniButton
|
toggleVisible={() => setVisible(prev => !prev)}
|
||||||
title={!controller.isContentEditable ? 'Откатить к версии' : 'Переключитесь на неактуальную версию'}
|
readOnly={readOnly}
|
||||||
disabled={controller.isContentEditable}
|
toggleReadOnly={() => setReadOnly(prev => !prev)}
|
||||||
onClick={() => controller.restoreVersion()}
|
|
||||||
icon={<IconUpload size='1.25rem' className='icon-red' />}
|
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
|
||||||
title={controller.isContentEditable ? 'Создать версию' : 'Переключитесь на актуальную версию'}
|
|
||||||
disabled={!controller.isContentEditable}
|
|
||||||
onClick={controller.createVersion}
|
|
||||||
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
|
||||||
/>
|
|
||||||
<MiniButton
|
|
||||||
title={schema?.versions.length === 0 ? 'Список версий пуст' : 'Редактировать версии'}
|
|
||||||
disabled={!schema || schema?.versions.length === 0}
|
|
||||||
onClick={controller.editVersions}
|
|
||||||
icon={<IconList size='1.25rem' className='icon-primary' />}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
<BadgeHelp topic={HelpTopic.VERSIONS} className='max-w-[30rem]' offset={4} />
|
|
||||||
</Overlay>
|
|
||||||
<Label text='Версия' className='mb-2' />
|
<Label text='Версия' className='mb-2' />
|
||||||
<SelectVersion
|
<SelectVersion
|
||||||
id='schema_version'
|
id='schema_version'
|
||||||
|
className='select-none'
|
||||||
value={schema?.version} // prettier: split lines
|
value={schema?.version} // prettier: split lines
|
||||||
items={schema?.versions}
|
items={schema?.versions}
|
||||||
onSelectValue={controller.viewVersion}
|
onSelectValue={controller.viewVersion}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TextArea
|
<TextArea
|
||||||
id='schema_comment'
|
id='schema_comment'
|
||||||
label='Описание'
|
label='Описание'
|
||||||
|
@ -158,28 +137,10 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
|
||||||
disabled={!controller.isContentEditable || controller.isProcessing}
|
disabled={!controller.isContentEditable || controller.isProcessing}
|
||||||
onChange={event => setComment(event.target.value)}
|
onChange={event => setComment(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<div className='flex justify-between whitespace-nowrap'>
|
{controller.isContentEditable || isModified ? (
|
||||||
<Checkbox
|
|
||||||
id='schema_common'
|
|
||||||
label='Общедоступная схема'
|
|
||||||
title='Общедоступные схемы видны всем пользователям и могут быть изменены'
|
|
||||||
disabled={!controller.isContentEditable || controller.isProcessing}
|
|
||||||
value={common}
|
|
||||||
setValue={value => setCommon(value)}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
id='schema_immutable'
|
|
||||||
label='Неизменная схема'
|
|
||||||
title='Только администраторы могут присваивать схемам неизменный статус'
|
|
||||||
disabled={!controller.isContentEditable || !user?.is_staff || controller.isProcessing}
|
|
||||||
value={canonical}
|
|
||||||
setValue={value => setCanonical(value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{controller.isContentEditable ? (
|
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
text='Сохранить изменения'
|
text='Сохранить изменения'
|
||||||
className='self-center'
|
className='self-center mt-4'
|
||||||
loading={processing}
|
loading={processing}
|
||||||
disabled={!isModified}
|
disabled={!isModified}
|
||||||
icon={<IconSave size='1.25rem' />}
|
icon={<IconSave size='1.25rem' />}
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { IconDestroy, IconDownload, IconFollow, IconFollowOff, IconSave, IconShare } from '@/components/Icons';
|
import { SubscribeIcon } from '@/components/DomainIcons';
|
||||||
|
import { IconDestroy, IconDownload, IconSave, IconShare } from '@/components/Icons';
|
||||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
import Overlay from '@/components/ui/Overlay';
|
import Overlay from '@/components/ui/Overlay';
|
||||||
|
@ -17,7 +18,6 @@ interface RSFormToolbarProps {
|
||||||
modified: boolean;
|
modified: boolean;
|
||||||
subscribed: boolean;
|
subscribed: boolean;
|
||||||
anonymous: boolean;
|
anonymous: boolean;
|
||||||
claimable: boolean;
|
|
||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
onDestroy: () => void;
|
onDestroy: () => void;
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ function RSFormToolbar({ modified, anonymous, subscribed, onSubmit, onDestroy }:
|
||||||
const canSave = useMemo(() => modified && !controller.isProcessing, [modified, controller.isProcessing]);
|
const canSave = useMemo(() => modified && !controller.isProcessing, [modified, controller.isProcessing]);
|
||||||
return (
|
return (
|
||||||
<Overlay position='top-1 right-1/2 translate-x-1/2' className='cc-icons'>
|
<Overlay position='top-1 right-1/2 translate-x-1/2' className='cc-icons'>
|
||||||
{controller.isContentEditable ? (
|
{controller.isContentEditable || modified ? (
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
|
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
|
||||||
disabled={!canSave}
|
disabled={!canSave}
|
||||||
|
@ -49,13 +49,7 @@ function RSFormToolbar({ modified, anonymous, subscribed, onSubmit, onDestroy }:
|
||||||
{!anonymous ? (
|
{!anonymous ? (
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={`Отслеживание <b>${subscribed ? 'включено' : 'выключено'}</b>`}
|
titleHtml={`Отслеживание <b>${subscribed ? 'включено' : 'выключено'}</b>`}
|
||||||
icon={
|
icon={<SubscribeIcon value={subscribed} className={subscribed ? 'icon-primary' : 'clr-text-controls'} />}
|
||||||
subscribed ? (
|
|
||||||
<IconFollow size='1.25rem' className='icon-primary' />
|
|
||||||
) : (
|
|
||||||
<IconFollowOff size='1.25rem' className='clr-text-controls' />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
disabled={controller.isProcessing}
|
disabled={controller.isProcessing}
|
||||||
onClick={controller.toggleSubscribe}
|
onClick={controller.toggleSubscribe}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { IconNewItem, IconUpload, IconVersions } from '@/components/Icons';
|
||||||
|
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||||
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
|
import Overlay from '@/components/ui/Overlay';
|
||||||
|
import { HelpTopic } from '@/models/miscellaneous';
|
||||||
|
|
||||||
|
import { useRSEdit } from '../RSEditContext';
|
||||||
|
|
||||||
|
function VersionsToolbar() {
|
||||||
|
const controller = useRSEdit();
|
||||||
|
return (
|
||||||
|
<Overlay position='top-[-0.4rem] right-[0rem]' className='cc-icons pr-2'>
|
||||||
|
{controller.isMutable ? (
|
||||||
|
<>
|
||||||
|
<MiniButton
|
||||||
|
title={!controller.isContentEditable ? 'Откатить к версии' : 'Переключитесь на неактуальную версию'}
|
||||||
|
disabled={controller.isContentEditable}
|
||||||
|
onClick={() => controller.restoreVersion()}
|
||||||
|
icon={<IconUpload size='1.25rem' className='icon-red' />}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
title={controller.isContentEditable ? 'Создать версию' : 'Переключитесь на актуальную версию'}
|
||||||
|
disabled={!controller.isContentEditable}
|
||||||
|
onClick={controller.createVersion}
|
||||||
|
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
title={controller.schema?.versions.length === 0 ? 'Список версий пуст' : 'Редактировать версии'}
|
||||||
|
disabled={!controller.schema || controller.schema?.versions.length === 0}
|
||||||
|
onClick={controller.editVersions}
|
||||||
|
icon={<IconVersions size='1.25rem' className='icon-primary' />}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
<BadgeHelp topic={HelpTopic.VERSIONS} className='max-w-[30rem]' offset={4} />
|
||||||
|
</Overlay>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VersionsToolbar;
|
|
@ -105,7 +105,7 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{controller.isContentEditable ? <RSListToolbar /> : null}
|
{controller.isContentEditable ? <RSListToolbar /> : null}
|
||||||
<AnimateFade tabIndex={-1} className='outline-none' onKeyDown={handleKeyDown}>
|
<AnimateFade tabIndex={-1} onKeyDown={handleKeyDown}>
|
||||||
{controller.isContentEditable ? (
|
{controller.isContentEditable ? (
|
||||||
<SelectedCounter
|
<SelectedCounter
|
||||||
totalCount={controller.schema?.stats?.count_all ?? 0}
|
totalCount={controller.schema?.stats?.count_all ?? 0}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { useAuth } from '@/context/AuthContext';
|
||||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||||
import { useConceptOptions } from '@/context/OptionsContext';
|
import { useConceptOptions } from '@/context/OptionsContext';
|
||||||
import { useRSForm } from '@/context/RSFormContext';
|
import { useRSForm } from '@/context/RSFormContext';
|
||||||
|
import DlgChangeLocation from '@/dialogs/DlgChangeLocation';
|
||||||
import DlgCloneLibraryItem from '@/dialogs/DlgCloneLibraryItem';
|
import DlgCloneLibraryItem from '@/dialogs/DlgCloneLibraryItem';
|
||||||
import DlgConstituentaTemplate from '@/dialogs/DlgConstituentaTemplate';
|
import DlgConstituentaTemplate from '@/dialogs/DlgConstituentaTemplate';
|
||||||
import DlgCreateCst from '@/dialogs/DlgCreateCst';
|
import DlgCreateCst from '@/dialogs/DlgCreateCst';
|
||||||
|
@ -28,7 +29,7 @@ import DlgInlineSynthesis from '@/dialogs/DlgInlineSynthesis';
|
||||||
import DlgRenameCst from '@/dialogs/DlgRenameCst';
|
import DlgRenameCst from '@/dialogs/DlgRenameCst';
|
||||||
import DlgSubstituteCst from '@/dialogs/DlgSubstituteCst';
|
import DlgSubstituteCst from '@/dialogs/DlgSubstituteCst';
|
||||||
import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm';
|
import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm';
|
||||||
import { IVersionData, VersionID } from '@/models/library';
|
import { AccessPolicy, IVersionData, LocationHead, VersionID } from '@/models/library';
|
||||||
import {
|
import {
|
||||||
ConstituentaID,
|
ConstituentaID,
|
||||||
CstType,
|
CstType,
|
||||||
|
@ -60,7 +61,9 @@ interface IRSEditContext {
|
||||||
nothingSelected: boolean;
|
nothingSelected: boolean;
|
||||||
|
|
||||||
setOwner: (newOwner: UserID) => void;
|
setOwner: (newOwner: UserID) => void;
|
||||||
|
setAccessPolicy: (newPolicy: AccessPolicy) => void;
|
||||||
promptEditors: () => void;
|
promptEditors: () => void;
|
||||||
|
promptLocation: () => void;
|
||||||
toggleSubscribe: () => void;
|
toggleSubscribe: () => void;
|
||||||
|
|
||||||
setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
|
setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
|
||||||
|
@ -130,7 +133,10 @@ export const RSEditState = ({
|
||||||
const { accessLevel, setAccessLevel } = useAccessMode();
|
const { accessLevel, setAccessLevel } = useAccessMode();
|
||||||
const model = useRSForm();
|
const model = useRSForm();
|
||||||
|
|
||||||
const isMutable = useMemo(() => accessLevel > UserLevel.READER, [accessLevel]);
|
const isMutable = useMemo(
|
||||||
|
() => accessLevel > UserLevel.READER && !model.schema?.read_only,
|
||||||
|
[accessLevel, model.schema?.read_only]
|
||||||
|
);
|
||||||
const isContentEditable = useMemo(() => isMutable && !model.isArchive, [isMutable, model.isArchive]);
|
const isContentEditable = useMemo(() => isMutable && !model.isArchive, [isMutable, model.isArchive]);
|
||||||
const nothingSelected = useMemo(() => selected.length === 0, [selected]);
|
const nothingSelected = useMemo(() => selected.length === 0, [selected]);
|
||||||
|
|
||||||
|
@ -138,6 +144,7 @@ export const RSEditState = ({
|
||||||
const [showClone, setShowClone] = useState(false);
|
const [showClone, setShowClone] = useState(false);
|
||||||
const [showDeleteCst, setShowDeleteCst] = useState(false);
|
const [showDeleteCst, setShowDeleteCst] = useState(false);
|
||||||
const [showEditEditors, setShowEditEditors] = useState(false);
|
const [showEditEditors, setShowEditEditors] = useState(false);
|
||||||
|
const [showEditLocation, setShowEditLocation] = useState(false);
|
||||||
const [showEditTerm, setShowEditTerm] = useState(false);
|
const [showEditTerm, setShowEditTerm] = useState(false);
|
||||||
const [showSubstitute, setShowSubstitute] = useState(false);
|
const [showSubstitute, setShowSubstitute] = useState(false);
|
||||||
const [showCreateVersion, setShowCreateVersion] = useState(false);
|
const [showCreateVersion, setShowCreateVersion] = useState(false);
|
||||||
|
@ -199,6 +206,21 @@ export const RSEditState = ({
|
||||||
});
|
});
|
||||||
}, [model, viewVersion]);
|
}, [model, viewVersion]);
|
||||||
|
|
||||||
|
function calculateCloneLocation(): string {
|
||||||
|
if (!model.schema) {
|
||||||
|
return LocationHead.USER;
|
||||||
|
}
|
||||||
|
const location = model.schema.location;
|
||||||
|
const head = model.schema.location.substring(0, 2) as LocationHead;
|
||||||
|
if (head === LocationHead.LIBRARY) {
|
||||||
|
return user?.is_staff ? location : LocationHead.USER;
|
||||||
|
}
|
||||||
|
if (model.schema.owner === user?.id) {
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
return head === LocationHead.USER ? LocationHead.USER : location;
|
||||||
|
}
|
||||||
|
|
||||||
const handleCreateCst = useCallback(
|
const handleCreateCst = useCallback(
|
||||||
(data: ICstCreateData) => {
|
(data: ICstCreateData) => {
|
||||||
if (!model.schema) {
|
if (!model.schema) {
|
||||||
|
@ -302,6 +324,16 @@ export const RSEditState = ({
|
||||||
[model]
|
[model]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleSetLocation = useCallback(
|
||||||
|
(newLocation: string) => {
|
||||||
|
if (!model.schema) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
model.setLocation(newLocation, () => toast.success('Схема перемещена'));
|
||||||
|
},
|
||||||
|
[model]
|
||||||
|
);
|
||||||
|
|
||||||
const handleInlineSynthesis = useCallback(
|
const handleInlineSynthesis = useCallback(
|
||||||
(data: IInlineSynthesisData) => {
|
(data: IInlineSynthesisData) => {
|
||||||
if (!model.schema) {
|
if (!model.schema) {
|
||||||
|
@ -485,6 +517,10 @@ export const RSEditState = ({
|
||||||
setShowEditEditors(true);
|
setShowEditEditors(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const promptLocation = useCallback(() => {
|
||||||
|
setShowEditLocation(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const download = useCallback(() => {
|
const download = useCallback(() => {
|
||||||
if (isModified && !promptUnsaved()) {
|
if (isModified && !promptUnsaved()) {
|
||||||
return;
|
return;
|
||||||
|
@ -523,6 +559,13 @@ export const RSEditState = ({
|
||||||
[model]
|
[model]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const setAccessPolicy = useCallback(
|
||||||
|
(newPolicy: AccessPolicy) => {
|
||||||
|
model.setAccessPolicy(newPolicy, () => toast.success('Политика доступа изменена'));
|
||||||
|
},
|
||||||
|
[model]
|
||||||
|
);
|
||||||
|
|
||||||
const setEditors = useCallback(
|
const setEditors = useCallback(
|
||||||
(newEditors: UserID[]) => {
|
(newEditors: UserID[]) => {
|
||||||
model.setEditors(newEditors, () => toast.success('Редакторы обновлены'));
|
model.setEditors(newEditors, () => toast.success('Редакторы обновлены'));
|
||||||
|
@ -543,7 +586,9 @@ export const RSEditState = ({
|
||||||
|
|
||||||
toggleSubscribe,
|
toggleSubscribe,
|
||||||
setOwner,
|
setOwner,
|
||||||
|
setAccessPolicy,
|
||||||
promptEditors,
|
promptEditors,
|
||||||
|
promptLocation,
|
||||||
|
|
||||||
setSelected: setSelected,
|
setSelected: setSelected,
|
||||||
select: (target: ConstituentaID) => setSelected(prev => [...prev, target]),
|
select: (target: ConstituentaID) => setSelected(prev => [...prev, target]),
|
||||||
|
@ -584,6 +629,7 @@ export const RSEditState = ({
|
||||||
{showClone ? (
|
{showClone ? (
|
||||||
<DlgCloneLibraryItem
|
<DlgCloneLibraryItem
|
||||||
base={model.schema}
|
base={model.schema}
|
||||||
|
initialLocation={calculateCloneLocation()}
|
||||||
hideWindow={() => setShowClone(false)}
|
hideWindow={() => setShowClone(false)}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
totalCount={model.schema.items.length}
|
totalCount={model.schema.items.length}
|
||||||
|
@ -655,6 +701,14 @@ export const RSEditState = ({
|
||||||
setEditors={setEditors}
|
setEditors={setEditors}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
{showEditLocation ? (
|
||||||
|
<DlgChangeLocation
|
||||||
|
hideWindow={() => setShowEditLocation(false)}
|
||||||
|
initial={model.schema.location}
|
||||||
|
onChangeLocation={handleSetLocation}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{showInlineSynthesis ? (
|
{showInlineSynthesis ? (
|
||||||
<DlgInlineSynthesis
|
<DlgInlineSynthesis
|
||||||
receiver={model.schema}
|
receiver={model.schema}
|
||||||
|
@ -666,7 +720,9 @@ export const RSEditState = ({
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{model.loading ? <Loader /> : null}
|
{model.loading ? <Loader /> : null}
|
||||||
{model.error ? <ProcessError error={model.error} isArchive={model.isArchive} schemaID={model.schemaID} /> : null}
|
{model.errorLoading ? (
|
||||||
|
<ProcessError error={model.errorLoading} isArchive={model.isArchive} schemaID={model.schemaID} />
|
||||||
|
) : null}
|
||||||
{model.schema && !model.loading ? children : null}
|
{model.schema && !model.loading ? children : null}
|
||||||
</RSEditContext.Provider>
|
</RSEditContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -239,7 +239,7 @@ function RSTabs() {
|
||||||
onSelect={onSelectTab}
|
onSelect={onSelectTab}
|
||||||
defaultFocus
|
defaultFocus
|
||||||
selectedTabClassName='clr-selected'
|
selectedTabClassName='clr-selected'
|
||||||
className='flex flex-col min-w-fit'
|
className='flex flex-col min-w-fit mx-auto'
|
||||||
>
|
>
|
||||||
<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')}>
|
||||||
<RSTabsMenu onDestroy={onDestroySchema} />
|
<RSTabsMenu onDestroy={onDestroySchema} />
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
/* Depth layers */
|
|
||||||
.z-bottom {
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.z-pop {
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
:is(.z-sticky, .sticky) {
|
|
||||||
z-index: 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
.z-tooltip {
|
|
||||||
z-index: 30;
|
|
||||||
}
|
|
||||||
|
|
||||||
.z-navigation {
|
|
||||||
z-index: 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.z-modal {
|
|
||||||
z-index: 60;
|
|
||||||
}
|
|
||||||
|
|
||||||
.z-modal-controls {
|
|
||||||
z-index: 70;
|
|
||||||
}
|
|
||||||
|
|
||||||
.z-modal-top {
|
|
||||||
z-index: 80;
|
|
||||||
}
|
|
||||||
|
|
||||||
.z-modal-tooltip {
|
|
||||||
z-index: 90;
|
|
||||||
}
|
|
||||||
|
|
||||||
.z-topmost {
|
|
||||||
z-index: 99;
|
|
||||||
}
|
|
|
@ -3,7 +3,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@import './constants.css';
|
@import './constants.css';
|
||||||
@import './layers.css';
|
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
|
@ -139,4 +138,8 @@ div:not(.dense) > p {
|
||||||
.border {
|
.border {
|
||||||
@apply rounded;
|
@apply rounded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:is(.sticky) {
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,7 +128,7 @@ export function findContainedNodes(start: number, finish: number, tree: Tree, fi
|
||||||
export function domTooltipConstituenta(cst?: IConstituenta) {
|
export function domTooltipConstituenta(cst?: IConstituenta) {
|
||||||
const dom = document.createElement('div');
|
const dom = document.createElement('div');
|
||||||
dom.className = clsx(
|
dom.className = clsx(
|
||||||
'z-modal-tooltip',
|
'z-modalTooltip',
|
||||||
'max-h-[25rem] max-w-[25rem] min-w-[10rem]',
|
'max-h-[25rem] max-w-[25rem] min-w-[10rem]',
|
||||||
'dense',
|
'dense',
|
||||||
'p-2',
|
'p-2',
|
||||||
|
|
|
@ -30,7 +30,8 @@ export const PARAMETER = {
|
||||||
* Numeric limitations.
|
* Numeric limitations.
|
||||||
*/
|
*/
|
||||||
export const limits = {
|
export const limits = {
|
||||||
library_alias_len: 12
|
library_alias_len: 12,
|
||||||
|
location_len: 500
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -94,7 +95,11 @@ export const storage = {
|
||||||
rseditShowList: 'rsedit.show_list',
|
rseditShowList: 'rsedit.show_list',
|
||||||
rseditShowControls: 'rsedit.show_controls',
|
rseditShowControls: 'rsedit.show_controls',
|
||||||
|
|
||||||
librarySearchStrategy: 'library.search.strategy',
|
librarySearchHead: 'library.search.head',
|
||||||
|
librarySearchVisible: 'library.search.visible',
|
||||||
|
librarySearchOwned: 'library.search.owned',
|
||||||
|
librarySearchSubscribed: 'library.search.subscribed',
|
||||||
|
librarySearchEditor: 'library.search.editor',
|
||||||
libraryPagination: 'library.pagination',
|
libraryPagination: 'library.pagination',
|
||||||
|
|
||||||
rsgraphFilter: 'rsgraph.filter2',
|
rsgraphFilter: 'rsgraph.filter2',
|
||||||
|
@ -138,7 +143,9 @@ export const prefixes = {
|
||||||
cst_delete_list: 'cst_delete_list_',
|
cst_delete_list: 'cst_delete_list_',
|
||||||
cst_dependant_list: 'cst_dependant_list_',
|
cst_dependant_list: 'cst_dependant_list_',
|
||||||
csttype_list: 'csttype_',
|
csttype_list: 'csttype_',
|
||||||
|
policy_list: 'policy_list_',
|
||||||
library_filters_list: 'library_filters_list_',
|
library_filters_list: 'library_filters_list_',
|
||||||
|
location_head_list: 'location_head_list_',
|
||||||
topic_list: 'topic_list_',
|
topic_list: 'topic_list_',
|
||||||
topic_item: 'topic_item_',
|
topic_item: 'topic_item_',
|
||||||
library_list: 'library_list_',
|
library_list: 'library_list_',
|
||||||
|
|
|
@ -6,14 +6,8 @@
|
||||||
*/
|
*/
|
||||||
import { GraphLayout } from '@/components/ui/GraphUI';
|
import { GraphLayout } from '@/components/ui/GraphUI';
|
||||||
import { GramData, Grammeme, ReferenceType } from '@/models/language';
|
import { GramData, Grammeme, ReferenceType } from '@/models/language';
|
||||||
import {
|
import { AccessPolicy, LocationHead } from '@/models/library';
|
||||||
CstMatchMode,
|
import { CstMatchMode, DependencyMode, GraphColoring, GraphSizing, HelpTopic } from '@/models/miscellaneous';
|
||||||
DependencyMode,
|
|
||||||
GraphColoring,
|
|
||||||
GraphSizing,
|
|
||||||
HelpTopic,
|
|
||||||
LibraryFilterStrategy
|
|
||||||
} from '@/models/miscellaneous';
|
|
||||||
import { CstClass, CstType, ExpressionStatus, IConstituenta, IRSForm } from '@/models/rsform';
|
import { CstClass, CstType, ExpressionStatus, IConstituenta, IRSForm } from '@/models/rsform';
|
||||||
import {
|
import {
|
||||||
IArgumentInfo,
|
IArgumentInfo,
|
||||||
|
@ -261,30 +255,28 @@ export function describeCstSource(mode: DependencyMode): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves label for {@link LibraryFilterStrategy}.
|
* Retrieves label for {@link LocationHead}.
|
||||||
*/
|
*/
|
||||||
export function labelLibraryFilter(strategy: LibraryFilterStrategy): string {
|
export function labelLocationHead(head: LocationHead): string {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
switch (strategy) {
|
switch (head) {
|
||||||
case LibraryFilterStrategy.MANUAL: return 'отображать все';
|
case LocationHead.USER: return 'личные (/U)';
|
||||||
case LibraryFilterStrategy.COMMON: return 'общедоступные';
|
case LocationHead.COMMON: return 'общие (/S)';
|
||||||
case LibraryFilterStrategy.CANONICAL: return 'неизменные';
|
case LocationHead.LIBRARY: return 'неизменные (/L)';
|
||||||
case LibraryFilterStrategy.SUBSCRIBE: return 'подписки';
|
case LocationHead.PROJECTS: return 'проекты (/P)';
|
||||||
case LibraryFilterStrategy.OWNED: return 'владелец';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves description for {@link LibraryFilterStrategy}.
|
* Retrieves description for {@link LocationHead}.
|
||||||
*/
|
*/
|
||||||
export function describeLibraryFilter(strategy: LibraryFilterStrategy): string {
|
export function describeLocationHead(head: LocationHead): string {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
switch (strategy) {
|
switch (head) {
|
||||||
case LibraryFilterStrategy.MANUAL: return 'Отображать все схемы';
|
case LocationHead.USER: return 'Личные схемы пользователя';
|
||||||
case LibraryFilterStrategy.COMMON: return 'Отображать общедоступные схемы';
|
case LocationHead.COMMON: return 'Рабочий каталог публичных схем';
|
||||||
case LibraryFilterStrategy.CANONICAL: return 'Отображать стандартные схемы';
|
case LocationHead.LIBRARY: return 'Каталог неизменных схем';
|
||||||
case LibraryFilterStrategy.SUBSCRIBE: return 'Отображать подписки';
|
case LocationHead.PROJECTS: return 'Рабочий каталог проектных схем';
|
||||||
case LibraryFilterStrategy.OWNED: return 'Отображать собственные схемы';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -802,9 +794,37 @@ export function describeAccessMode(mode: UserLevel): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves label for {@link AccessPolicy}.
|
||||||
|
*/
|
||||||
|
export function labelAccessPolicy(policy: AccessPolicy): string {
|
||||||
|
// prettier-ignore
|
||||||
|
switch (policy) {
|
||||||
|
case AccessPolicy.PRIVATE: return 'Личный';
|
||||||
|
case AccessPolicy.PROTECTED: return 'Защищенный';
|
||||||
|
case AccessPolicy.PUBLIC: return 'Открытый';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves description for {@link AccessPolicy}.
|
||||||
|
*/
|
||||||
|
export function describeAccessPolicy(policy: AccessPolicy): string {
|
||||||
|
// prettier-ignore
|
||||||
|
switch (policy) {
|
||||||
|
case AccessPolicy.PRIVATE:
|
||||||
|
return 'Доступ только для владельца';
|
||||||
|
case AccessPolicy.PROTECTED:
|
||||||
|
return 'Доступ для владельца и редакторов';
|
||||||
|
case AccessPolicy.PUBLIC:
|
||||||
|
return 'Открытый доступ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UI shared messages.
|
* UI shared messages.
|
||||||
*/
|
*/
|
||||||
export const messages = {
|
export const messages = {
|
||||||
unsaved: 'Сохраните или отмените изменения'
|
unsaved: 'Сохраните или отмените изменения',
|
||||||
|
promptUnsaved: 'Присутствуют несохраненные изменения. Продолжить без их учета?'
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
|
|
||||||
import { AxiosHeaderValue, AxiosResponse } from 'axios';
|
import { AxiosHeaderValue, AxiosResponse } from 'axios';
|
||||||
|
|
||||||
|
import { messages } from './labels';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if arguments is Node.
|
* Checks if arguments is Node.
|
||||||
*/
|
*/
|
||||||
|
@ -100,5 +102,25 @@ export function convertBase64ToBlob(base64String: string): Uint8Array {
|
||||||
* Prompt user of confirming discarding changes before continue.
|
* Prompt user of confirming discarding changes before continue.
|
||||||
*/
|
*/
|
||||||
export function promptUnsaved(): boolean {
|
export function promptUnsaved(): boolean {
|
||||||
return window.confirm('Присутствуют несохраненные изменения. Продолжить без их учета?');
|
return window.confirm(messages.promptUnsaved);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle tristate flag: undefined - true - false.
|
||||||
|
*/
|
||||||
|
export function toggleTristateFlag(prev: boolean | undefined): boolean | undefined {
|
||||||
|
if (prev === undefined) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return prev ? false : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle tristate color: gray - green - red .
|
||||||
|
*/
|
||||||
|
export function tripleToggleColor(value: boolean | undefined): string {
|
||||||
|
if (value === undefined) {
|
||||||
|
return 'clr-text-controls';
|
||||||
|
}
|
||||||
|
return value ? 'clr-text-green' : 'clr-text-red';
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,17 @@ export default {
|
||||||
darkMode: 'class',
|
darkMode: 'class',
|
||||||
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
||||||
theme: {
|
theme: {
|
||||||
|
zIndex: {
|
||||||
|
bottom: '0',
|
||||||
|
topmost: '99',
|
||||||
|
pop: '10',
|
||||||
|
sticky: '20',
|
||||||
|
tooltip: '30',
|
||||||
|
navigation: '50',
|
||||||
|
modal: '60',
|
||||||
|
modalControls: '70',
|
||||||
|
modalTooltip: '90'
|
||||||
|
},
|
||||||
extend: {}
|
extend: {}
|
||||||
},
|
},
|
||||||
plugins: []
|
plugins: []
|
||||||
|
|
Loading…
Reference in New Issue
Block a user