Compare commits

..

27 Commits

Author SHA1 Message Date
Ivan
e8509e44b1 M: Improve fullname label and fix clone defaults
Some checks failed
Backend CI / build (3.12) (push) Has been cancelled
Frontend CI / build (22.x) (push) Has been cancelled
Backend CI / notify-failure (push) Has been cancelled
Frontend CI / notify-failure (push) Has been cancelled
2025-03-25 23:00:50 +03:00
Ivan
417583efa5 R: Change fieldname comment to description 2025-03-25 22:30:15 +03:00
Ivan
03fc03a108 F: Replace clickOutside with onBlur trigger for dropdowns 2025-03-20 19:46:50 +03:00
Ivan
419b5e6379 B: Fix pagination change rendering error 2025-03-20 19:30:58 +03:00
Ivan
1cd780504d F: Improve styling and accessibility 2025-03-20 18:24:07 +03:00
Ivan
09b56f49d8 T: Add telegram reporting 2025-03-20 16:17:06 +03:00
Ivan
39783ec74a B: Fix linter error 2025-03-20 15:36:54 +03:00
Ivan
7adbaed116 F: Enable compression for backend responses 2025-03-20 15:34:39 +03:00
Ivan
341db80e68 R: Improve ValueIcon component 2025-03-20 14:41:48 +03:00
Ivan
1365d27af8 Update index.html 2025-03-20 14:13:08 +03:00
Ivan
cda3b70227 Update production.conf 2025-03-20 14:05:35 +03:00
Ivan
ebc6740e35 F: Enable caching for generated assets 2025-03-20 13:57:15 +03:00
Ivan
9bfdc56789 F: Improve buttons accessibility 2025-03-20 11:50:00 +03:00
Ivan
f92e086b13 F: Accessibility improvements 2025-03-19 23:28:32 +03:00
Ivan
9f64282385 B: Fix font loading and small issues 2025-03-19 16:12:18 +03:00
Ivan
f21295061d F: Add StyleLint tooling and fix some CSS issues 2025-03-19 15:11:43 +03:00
Ivan
76902b34ae F: Improve OSS create operations 2025-03-19 13:25:20 +03:00
Ivan
ff744b5367 M: Revert to default selection behavior and fix minor issues 2025-03-19 13:03:26 +03:00
Ivan
6c13a9e774 F: Improve admin panel for User 2025-03-19 12:07:45 +03:00
Ivan
cb6664a606 npm update 2025-03-18 22:47:17 +03:00
Ivan
ec40fe04ac B: Fix search dropdown 2025-03-18 22:27:43 +03:00
Ivan
c341360e90 M: Minor UI improvements 2025-03-14 21:06:31 +03:00
Ivan
eb48014e2f R: Replace enums with const objects 2025-03-14 20:43:30 +03:00
Ivan
0781dad1cb R: Fix tests 2025-03-13 23:56:10 +03:00
Ivan
8bf829513f R: Remove unused symbols 2025-03-13 23:20:52 +03:00
Ivan
5f524e2e6b B: Fix redirect on first load 2025-03-13 20:20:59 +03:00
Ivan
ea8c86119c B: Fix dialog heights for fullscreen dialogs 2025-03-13 20:13:57 +03:00
190 changed files with 2873 additions and 1797 deletions

View File

@ -40,3 +40,16 @@ jobs:
run: |
python manage.py check
python manage.py test
notify-failure:
runs-on: ubuntu-latest
needs: build
if: failure()
defaults:
run:
working-directory: .
steps:
- name: Send Telegram Notification
run: |
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
-d chat_id=${{ secrets.TELEGRAM_CHAT_ID }} \
-d text="❌ Backend build failed! Repository: ${{ github.repository }} Commit: ${{ github.sha }}"

View File

@ -49,3 +49,16 @@ jobs:
name: playwright-report
path: playwright-report/
retention-days: 30
notify-failure:
runs-on: ubuntu-latest
needs: build
if: failure()
defaults:
run:
working-directory: .
steps:
- name: Send Telegram Notification
run: |
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
-d chat_id=${{ secrets.TELEGRAM_CHAT_ID }} \
-d text="❌ Front build failed! Repository: ${{ github.repository }} Commit: ${{ github.sha }}"

View File

@ -21,6 +21,7 @@
"changeProcessCWD": true
}
],
"stylelint.enable": true,
"autopep8.args": [
"--max-line-length",
"120",
@ -151,6 +152,7 @@
"rsconcept",
"rsedit",
"rseditor",
"rsexpression",
"rsform",
"rsforms",
"rsgraph",

View File

@ -71,6 +71,10 @@ This readme file is used mostly to document project dependencies and conventions
- vite
- jest
- ts-jest
- stylelint
- stylelint-config-recommended
- stylelint-config-standard
- stylelint-config-tailwindcss
- @vitejs/plugin-react
- @types/jest
- @lezer/generator

View File

@ -75,4 +75,10 @@ server {
proxy_pass http://innerreact;
proxy_redirect default;
}
location /assets/ {
proxy_pass http://innerreact/assets/;
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
}
}

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-03-25 09:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0005_alter_libraryitem_owner'),
]
operations = [
migrations.AlterField(
model_name='libraryitem',
name='comment',
field=models.TextField(blank=True, verbose_name='Описание'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-03-25 19:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0006_alter_libraryitem_comment'),
]
operations = [
migrations.RenameField(
model_name='libraryitem',
old_name='comment',
new_name='description',
),
]

View File

@ -69,8 +69,8 @@ class LibraryItem(Model):
max_length=255,
blank=True
)
comment = TextField(
verbose_name='Комментарий',
description = TextField(
verbose_name='Описание',
blank=True
)
visible = BooleanField(

View File

@ -46,7 +46,7 @@ class TestLibraryItem(TestCase):
self.assertIsNone(item.owner)
self.assertEqual(item.title, 'Test')
self.assertEqual(item.alias, '')
self.assertEqual(item.comment, '')
self.assertEqual(item.description, '')
self.assertEqual(item.visible, True)
self.assertEqual(item.read_only, False)
self.assertEqual(item.access_policy, AccessPolicy.PUBLIC)
@ -59,13 +59,13 @@ class TestLibraryItem(TestCase):
title='Test',
owner=self.user1,
alias='KS1',
comment='Test comment',
description='Test description',
location=LocationHead.COMMON
)
self.assertEqual(item.owner, self.user1)
self.assertEqual(item.title, 'Test')
self.assertEqual(item.alias, 'KS1')
self.assertEqual(item.comment, 'Test comment')
self.assertEqual(item.description, 'Test description')
self.assertEqual(item.location, LocationHead.COMMON)

View File

@ -55,13 +55,13 @@ class LibraryViewSet(viewsets.ModelViewSet):
if operation.title != instance.title:
operation.title = instance.title
changed = True
if operation.comment != instance.comment:
operation.comment = instance.comment
if operation.description != instance.description:
operation.description = instance.description
changed = True
if changed:
update_list.append(operation)
if update_list:
Operation.objects.bulk_update(update_list, ['alias', 'title', 'comment'])
Operation.objects.bulk_update(update_list, ['alias', 'title', 'description'])
def perform_destroy(self, instance: m.LibraryItem) -> None:
if instance.item_type == m.LibraryItemType.RSFORM:
@ -160,7 +160,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
clone.owner = cast(User, self.request.user)
clone.title = serializer.validated_data['title']
clone.alias = serializer.validated_data.get('alias', '')
clone.comment = serializer.validated_data.get('comment', '')
clone.description = serializer.validated_data.get('description', '')
clone.visible = serializer.validated_data.get('visible', True)
clone.read_only = False
clone.access_policy = serializer.validated_data.get('access_policy', m.AccessPolicy.PUBLIC)
@ -168,7 +168,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
with transaction.atomic():
clone.save()
need_filter = 'items' in request.data
need_filter = 'items' in request.data and len(request.data['items']) > 0
for cst in RSForm(item).constituents():
if not need_filter or cst.pk in request.data['items']:
cst.pk = None

View File

@ -7,7 +7,16 @@ from . import models
class OperationAdmin(admin.ModelAdmin):
''' Admin model: Operation. '''
ordering = ['oss']
list_display = ['id', 'oss', 'operation_type', 'result', 'alias', 'title', 'comment', 'position_x', 'position_y']
list_display = [
'id',
'oss',
'operation_type',
'result',
'alias',
'title',
'description',
'position_x',
'position_y']
search_fields = ['id', 'operation_type', 'title', 'alias']

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-03-25 09:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('oss', '0008_alter_operation_result'),
]
operations = [
migrations.AlterField(
model_name='operation',
name='comment',
field=models.TextField(blank=True, verbose_name='Описание'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-03-25 19:13
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('oss', '0009_alter_operation_comment'),
]
operations = [
migrations.RenameField(
model_name='operation',
old_name='comment',
new_name='description',
),
]

View File

@ -53,8 +53,8 @@ class Operation(Model):
verbose_name='Название',
blank=True
)
comment = TextField(
verbose_name='Комментарий',
description = TextField(
verbose_name='Описание',
blank=True
)

View File

@ -141,8 +141,8 @@ class OperationSchema:
if schema is not None:
operation.alias = schema.alias
operation.title = schema.title
operation.comment = schema.comment
operation.save(update_fields=['result', 'alias', 'title', 'comment'])
operation.description = schema.description
operation.save(update_fields=['result', 'alias', 'title', 'description'])
if schema is not None and has_children:
rsform = RSForm(schema)
@ -227,7 +227,7 @@ class OperationSchema:
owner=self.model.owner,
alias=operation.alias,
title=operation.title,
comment=operation.comment,
description=operation.description,
visible=False,
access_policy=self.model.access_policy,
location=self.model.location

View File

@ -44,7 +44,7 @@ class OperationCreateSerializer(serializers.Serializer):
model = Operation
fields = \
'alias', 'operation_type', 'title', \
'comment', 'result', 'position_x', 'position_y'
'description', 'result', 'position_x', 'position_y'
create_schema = serializers.BooleanField(default=False, required=False)
item_data = OperationCreateData()
@ -63,7 +63,7 @@ class OperationUpdateSerializer(serializers.Serializer):
class Meta:
''' serializer metadata. '''
model = Operation
fields = 'alias', 'title', 'comment'
fields = 'alias', 'title', 'description'
target = PKField(many=False, queryset=Operation.objects.all())
item_data = OperationUpdateData()

View File

@ -28,6 +28,6 @@ class TestOperation(TestCase):
self.assertEqual(self.operation.result, None)
self.assertEqual(self.operation.alias, 'KS1')
self.assertEqual(self.operation.title, '')
self.assertEqual(self.operation.comment, '')
self.assertEqual(self.operation.description, '')
self.assertEqual(self.operation.position_x, 0)
self.assertEqual(self.operation.position_y, 0)

View File

@ -123,7 +123,7 @@ class TestChangeAttributes(EndpointTester):
@decl_endpoint('/api/library/{item}', method='patch')
def test_sync_from_result(self):
data = {'alias': 'KS111', 'title': 'New Title', 'comment': 'New Comment'}
data = {'alias': 'KS111', 'title': 'New Title', 'description': 'New description'}
self.executeOK(data=data, item=self.ks1.model.pk)
self.operation1.refresh_from_db()
@ -131,7 +131,7 @@ class TestChangeAttributes(EndpointTester):
self.assertEqual(self.operation1.result, self.ks1.model)
self.assertEqual(self.operation1.alias, data['alias'])
self.assertEqual(self.operation1.title, data['title'])
self.assertEqual(self.operation1.comment, data['comment'])
self.assertEqual(self.operation1.description, data['description'])
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_sync_from_operation(self):
@ -140,7 +140,7 @@ class TestChangeAttributes(EndpointTester):
'item_data': {
'alias': 'Test3 mod',
'title': 'Test title mod',
'comment': 'Comment mod'
'description': 'Comment mod'
},
'positions': [],
}
@ -149,7 +149,7 @@ class TestChangeAttributes(EndpointTester):
self.ks3.refresh_from_db()
self.assertEqual(self.ks3.model.alias, data['item_data']['alias'])
self.assertEqual(self.ks3.model.title, data['item_data']['title'])
self.assertEqual(self.ks3.model.comment, data['item_data']['comment'])
self.assertEqual(self.ks3.model.description, data['item_data']['description'])
@decl_endpoint('/api/library/{item}', method='delete')
def test_destroy_oss_consequence(self):

View File

@ -281,7 +281,7 @@ class TestChangeOperations(EndpointTester):
'item_data': {
'alias': 'Test4 mod',
'title': 'Test title mod',
'comment': 'Comment mod'
'description': 'Comment mod'
},
'positions': [],
'substitutions': [
@ -315,7 +315,7 @@ class TestChangeOperations(EndpointTester):
'item_data': {
'alias': 'Test4 mod',
'title': 'Test title mod',
'comment': 'Comment mod'
'description': 'Comment mod'
},
'positions': [],
'arguments': [self.operation1.pk],

View File

@ -143,7 +143,7 @@ class TestOssViewset(EndpointTester):
'item_data': {
'alias': 'Test3',
'title': 'Test title',
'comment': 'Тест кириллицы',
'description': 'Тест кириллицы',
'position_x': 1,
'position_y': 1,
},
@ -165,7 +165,7 @@ class TestOssViewset(EndpointTester):
self.assertEqual(new_operation['alias'], data['item_data']['alias'])
self.assertEqual(new_operation['operation_type'], data['item_data']['operation_type'])
self.assertEqual(new_operation['title'], data['item_data']['title'])
self.assertEqual(new_operation['comment'], data['item_data']['comment'])
self.assertEqual(new_operation['description'], data['item_data']['description'])
self.assertEqual(new_operation['position_x'], data['item_data']['position_x'])
self.assertEqual(new_operation['position_y'], data['item_data']['position_y'])
self.assertEqual(new_operation['result'], None)
@ -223,7 +223,7 @@ class TestOssViewset(EndpointTester):
'item_data': {
'alias': 'Test4',
'title': 'Test title',
'comment': 'Comment',
'description': 'Comment',
'operation_type': OperationType.INPUT,
'result': self.ks1.model.pk
},
@ -238,7 +238,7 @@ class TestOssViewset(EndpointTester):
schema = LibraryItem.objects.get(pk=new_operation['result'])
self.assertEqual(schema.alias, data['item_data']['alias'])
self.assertEqual(schema.title, data['item_data']['title'])
self.assertEqual(schema.comment, data['item_data']['comment'])
self.assertEqual(schema.description, data['item_data']['description'])
self.assertEqual(schema.visible, False)
self.assertEqual(schema.access_policy, self.owned.model.access_policy)
self.assertEqual(schema.location, self.owned.model.location)
@ -286,7 +286,7 @@ class TestOssViewset(EndpointTester):
self.executeBadData(data=data, item=self.owned_id)
self.operation1.result = None
self.operation1.comment = 'TestComment'
self.operation1.description = 'TestComment'
self.operation1.title = 'TestTitle'
self.operation1.save()
response = self.executeOK(data=data)
@ -296,7 +296,7 @@ class TestOssViewset(EndpointTester):
self.assertEqual(new_schema['id'], self.operation1.result.pk)
self.assertEqual(new_schema['alias'], self.operation1.alias)
self.assertEqual(new_schema['title'], self.operation1.title)
self.assertEqual(new_schema['comment'], self.operation1.comment)
self.assertEqual(new_schema['description'], self.operation1.description)
data['target'] = self.operation3.pk
self.executeBadData(data=data)
@ -326,14 +326,14 @@ class TestOssViewset(EndpointTester):
data['input'] = self.ks1.model.pk
self.ks1.model.alias = 'Test42'
self.ks1.model.title = 'Test421'
self.ks1.model.comment = 'TestComment42'
self.ks1.model.description = 'TestComment42'
self.ks1.save()
response = self.executeOK(data=data)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.result, self.ks1.model)
self.assertEqual(self.operation1.alias, self.ks1.model.alias)
self.assertEqual(self.operation1.title, self.ks1.model.title)
self.assertEqual(self.operation1.comment, self.ks1.model.comment)
self.assertEqual(self.operation1.description, self.ks1.model.description)
@decl_endpoint('/api/oss/{item}/set-input', method='patch')
def test_set_input_change_schema(self):
@ -382,7 +382,7 @@ class TestOssViewset(EndpointTester):
'item_data': {
'alias': 'Test3 mod',
'title': 'Test title mod',
'comment': 'Comment mod'
'description': 'Comment mod'
},
'positions': [],
'arguments': [self.operation2.pk, self.operation1.pk],
@ -406,7 +406,7 @@ class TestOssViewset(EndpointTester):
self.operation3.refresh_from_db()
self.assertEqual(self.operation3.alias, data['item_data']['alias'])
self.assertEqual(self.operation3.title, data['item_data']['title'])
self.assertEqual(self.operation3.comment, data['item_data']['comment'])
self.assertEqual(self.operation3.description, data['item_data']['description'])
args = self.operation3.getQ_arguments().order_by('order')
self.assertEqual(args[0].argument.pk, data['arguments'][0])
self.assertEqual(args[0].order, 0)
@ -426,7 +426,7 @@ class TestOssViewset(EndpointTester):
'item_data': {
'alias': 'Test3 mod',
'title': 'Test title mod',
'comment': 'Comment mod'
'description': 'Comment mod'
},
'positions': [],
}
@ -435,10 +435,10 @@ class TestOssViewset(EndpointTester):
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.alias, data['item_data']['alias'])
self.assertEqual(self.operation1.title, data['item_data']['title'])
self.assertEqual(self.operation1.comment, data['item_data']['comment'])
self.assertEqual(self.operation1.description, data['item_data']['description'])
self.assertEqual(self.operation1.result.alias, data['item_data']['alias'])
self.assertEqual(self.operation1.result.title, data['item_data']['title'])
self.assertEqual(self.operation1.result.comment, data['item_data']['comment'])
self.assertEqual(self.operation1.result.description, data['item_data']['description'])
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_update_operation_invalid_substitution(self):
@ -451,7 +451,7 @@ class TestOssViewset(EndpointTester):
'item_data': {
'alias': 'Test3 mod',
'title': 'Test title mod',
'comment': 'Comment mod'
'description': 'Comment mod'
},
'positions': [],
'arguments': [self.operation1.pk, self.operation2.pk],
@ -490,7 +490,7 @@ class TestOssViewset(EndpointTester):
self.operation3.refresh_from_db()
schema = self.operation3.result
self.assertEqual(schema.alias, self.operation3.alias)
self.assertEqual(schema.comment, self.operation3.comment)
self.assertEqual(schema.description, self.operation3.description)
self.assertEqual(schema.title, self.operation3.title)
self.assertEqual(schema.visible, False)
items = list(RSForm(schema).constituents())

View File

@ -200,9 +200,9 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
serializer.is_valid(raise_exception=True)
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
if operation.operation_type != m.OperationType.INPUT:
if len(operation.getQ_arguments()) > 0:
raise serializers.ValidationError({
'target': msg.operationNotInput(operation.alias)
'target': msg.operationHasArguments(operation.alias)
})
if operation.result is not None:
raise serializers.ValidationError({
@ -295,15 +295,15 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
oss.update_positions(serializer.validated_data['positions'])
operation.alias = serializer.validated_data['item_data']['alias']
operation.title = serializer.validated_data['item_data']['title']
operation.comment = serializer.validated_data['item_data']['comment']
operation.save(update_fields=['alias', 'title', 'comment'])
operation.description = serializer.validated_data['item_data']['description']
operation.save(update_fields=['alias', 'title', 'description'])
if operation.result is not None:
can_edit = permissions.can_edit_item(request.user, operation.result)
if can_edit or operation.operation_type == m.OperationType.SYNTHESIS:
operation.result.alias = operation.alias
operation.result.title = operation.title
operation.result.comment = operation.comment
operation.result.description = operation.description
operation.result.save()
if 'arguments' in serializer.validated_data:
oss.set_arguments(operation.pk, serializer.validated_data['arguments'])

View File

@ -42,7 +42,7 @@ class RSFormTRSSerializer(serializers.Serializer):
'type': _TRS_TYPE,
'title': schema.title,
'alias': schema.alias,
'comment': schema.comment,
'comment': schema.description,
'items': [],
'claimed': False,
'selection': [],
@ -78,7 +78,7 @@ class RSFormTRSSerializer(serializers.Serializer):
'type': _TRS_TYPE,
'title': data['title'],
'alias': data['alias'],
'comment': data['comment'],
'comment': data['description'],
'items': [],
'claimed': False,
'selection': [],
@ -123,7 +123,7 @@ class RSFormTRSSerializer(serializers.Serializer):
if self.context['load_meta']:
result['title'] = data.get('title', 'Без названия')
result['alias'] = data.get('alias', '')
result['comment'] = data.get('comment', '')
result['description'] = data.get('description', '')
if 'id' in data:
result['id'] = data['id']
self.instance = RSForm.from_id(result['id'])
@ -144,7 +144,7 @@ class RSFormTRSSerializer(serializers.Serializer):
owner=validated_data.get('owner', None),
alias=validated_data['alias'],
title=validated_data['title'],
comment=validated_data['comment'],
description=validated_data['description'],
visible=validated_data['visible'],
read_only=validated_data['read_only'],
access_policy=validated_data['access_policy'],
@ -171,8 +171,8 @@ class RSFormTRSSerializer(serializers.Serializer):
instance.model.alias = validated_data['alias']
if 'title' in validated_data:
instance.model.title = validated_data['title']
if 'comment' in validated_data:
instance.model.comment = validated_data['comment']
if 'description' in validated_data:
instance.model.description = validated_data['description']
order = 0
prev_constituents = instance.constituents()

View File

@ -30,7 +30,7 @@ class TestRSFormViewset(EndpointTester):
work_dir = os.path.dirname(os.path.abspath(__file__))
data = {
'title': 'Test123',
'comment': '123',
'description': '123',
'alias': 'ks1',
'location': LocationHead.PROJECTS,
'access_policy': AccessPolicy.PROTECTED,
@ -45,7 +45,7 @@ class TestRSFormViewset(EndpointTester):
self.assertEqual(response.data['owner'], self.user.pk)
self.assertEqual(response.data['title'], data['title'])
self.assertEqual(response.data['alias'], data['alias'])
self.assertEqual(response.data['comment'], data['comment'])
self.assertEqual(response.data['description'], data['description'])
@decl_endpoint('/api/rsforms', method='get')

View File

@ -586,8 +586,8 @@ def _prepare_rsform_data(data: dict, request: Request, owner: Union[User, None])
data['title'] = 'Без названия ' + request.FILES['file'].fileName
if 'alias' in request.data and request.data['alias'] != '':
data['alias'] = request.data['alias']
if 'comment' in request.data and request.data['comment'] != '':
data['comment'] = request.data['comment']
if 'description' in request.data and request.data['description'] != '':
data['description'] = request.data['description']
visible = True
if 'visible' in request.data:

View File

@ -1 +1,27 @@
''' Admin: User profile and Authorization. '''
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.contrib.auth.admin import UserAdmin
User = get_user_model()
class CustomUserAdmin(UserAdmin):
''' Admin model: User. '''
fieldsets = UserAdmin.fieldsets
list_display = (
'username',
'email',
'first_name',
'last_name',
'is_staff',
'is_active',
'date_joined',
'last_login')
ordering = ['date_joined', 'username']
search_fields = ['email', 'first_name', 'last_name', 'username']
list_filter = ['is_staff', 'is_superuser', 'is_active']
admin.site.unregister(User)
admin.site.register(User, CustomUserAdmin)

View File

@ -88,7 +88,7 @@
"owner": 1,
"title": "Банк выражений",
"alias": "БВ",
"comment": "Банк шаблонов для генерации выражений",
"description": "Банк шаблонов для генерации выражений",
"visible": true,
"read_only": false,
"access_policy": "public",
@ -105,7 +105,7 @@
"owner": 5,
"title": "Групповая операция",
"alias": К09",
"comment": "",
"description": "",
"visible": true,
"read_only": false,
"access_policy": "public",
@ -122,7 +122,7 @@
"owner": 3,
"title": "Булева алгебра",
"alias": К12",
"comment": "",
"description": "",
"visible": true,
"read_only": false,
"access_policy": "public",
@ -139,7 +139,7 @@
"owner": 3,
"title": "Генеалогия",
"alias": "D0001",
"comment": "построено на основе понятия \"родство\" из Википедии",
"description": "построено на основе понятия \"родство\" из Википедии",
"visible": true,
"read_only": false,
"access_policy": "public",
@ -156,7 +156,7 @@
"owner": 1,
"title": "Вещества и смеси",
"alias": "КС Вещества",
"comment": "",
"description": "",
"visible": false,
"read_only": false,
"access_policy": "public",
@ -173,7 +173,7 @@
"owner": 1,
"title": "Объект-объектные отношения",
"alias": "КС ООО",
"comment": "",
"description": "",
"visible": false,
"read_only": false,
"access_policy": "public",
@ -190,7 +190,7 @@
"owner": 1,
"title": "Процессы",
"alias": "КС Процессы",
"comment": "",
"description": "",
"visible": false,
"read_only": false,
"access_policy": "public",
@ -207,7 +207,7 @@
"owner": 1,
"title": "Экологические правоотношения",
"alias": "ЭКОС",
"comment": "",
"description": "",
"visible": true,
"read_only": false,
"access_policy": "public",
@ -224,7 +224,7 @@
"owner": 1,
"title": "Объектная среда",
"alias": "КС Объект-сред",
"comment": "",
"description": "",
"visible": false,
"read_only": false,
"access_policy": "public",
@ -241,7 +241,7 @@
"owner": 1,
"title": "Процессные среды",
"alias": "КС Проц-сред",
"comment": "",
"description": "",
"visible": false,
"read_only": false,
"access_policy": "public",
@ -6763,7 +6763,7 @@
"fields": {
"operation": 9,
"argument": 1,
"order": 0
"order": 0
}
},
{
@ -6772,7 +6772,7 @@
"fields": {
"operation": 9,
"argument": 2,
"order": 1
"order": 1
}
},
{
@ -6781,7 +6781,7 @@
"fields": {
"operation": 10,
"argument": 4,
"order": 0
"order": 0
}
},
{
@ -6790,7 +6790,7 @@
"fields": {
"operation": 10,
"argument": 9,
"order": 1
"order": 1
}
},
{
@ -7414,7 +7414,7 @@
"result": 38,
"alias": "КС Вещества",
"title": "Вещества и смеси",
"comment": "",
"description": "",
"position_x": 530.0,
"position_y": 370.0
}
@ -7428,7 +7428,7 @@
"result": 39,
"alias": "КС ООО",
"title": "Объект-объектные отношения",
"comment": "",
"description": "",
"position_x": 710.0,
"position_y": 370.0
}
@ -7442,7 +7442,7 @@
"result": 40,
"alias": "КС Процессы",
"title": "Процессы",
"comment": "",
"description": "",
"position_x": 890.0,
"position_y": 370.0
}
@ -7456,7 +7456,7 @@
"result": 43,
"alias": "КС Объект-сред",
"title": "Объектная среда",
"comment": "",
"description": "",
"position_x": 620.0,
"position_y": 470.0
}
@ -7470,7 +7470,7 @@
"result": 44,
"alias": "КС Проц-сред",
"title": "Процессные среды",
"comment": "",
"description": "",
"position_x": 760.0,
"position_y": 570.0
}

View File

@ -112,6 +112,7 @@ if _domain != '':
MIDDLEWARE = [
'django.middleware.gzip.GZipMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',

View File

@ -34,6 +34,10 @@ def operationNotInput(title: str):
return f'Операция не является Загрузкой: {title}'
def operationHasArguments(title: str):
return f'Операция имеет аргументы: {title}'
def operationResultFromAnotherOSS():
return 'Схема является результатом другой ОСС'

View File

@ -0,0 +1,22 @@
{
"extends": ["stylelint-config-recommended", "stylelint-config-standard", "stylelint-config-tailwindcss"],
"rules": {
"color-no-invalid-hex": true,
"font-family-no-missing-generic-family-keyword": true,
"unit-no-unknown": true,
"block-no-empty": true,
"selector-pseudo-element-no-unknown": true,
"property-no-unknown": true,
"declaration-block-no-duplicate-properties": true,
"no-duplicate-selectors": true,
"no-empty-source": true,
"import-notation": null,
"at-rule-empty-line-before": null,
"declaration-empty-line-before": null,
"at-rule-no-unknown": null,
"comment-no-empty": null,
"comment-empty-line-before": null,
"custom-property-empty-line-before": null
}
}

View File

@ -8,6 +8,33 @@ import importPlugin from 'eslint-plugin-import';
import simpleImportSort from 'eslint-plugin-simple-import-sort';
import playwright from 'eslint-plugin-playwright';
const basicRules = {
'no-console': 'off',
'require-jsdoc': 'off',
'@typescript-eslint/consistent-type-imports': [
'warn',
{
fixStyle: 'inline-type-imports'
}
],
'@typescript-eslint/no-empty-object-type': ['error', { allowInterfaces: 'with-single-extends' }],
'@typescript-eslint/prefer-nullish-coalescing': 'off',
'@typescript-eslint/no-inferrable-types': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_'
}
],
'simple-import-sort/exports': 'error',
'import/no-duplicates': 'warn'
};
export default [
...typescriptPlugin.configs.recommendedTypeChecked,
...typescriptPlugin.configs.stylisticTypeChecked,
@ -45,33 +72,9 @@ export default [
},
settings: { react: { version: 'detect' } },
rules: {
'no-console': 'off',
'require-jsdoc': 'off',
...basicRules,
'react-compiler/react-compiler': 'error',
'react-refresh/only-export-components': ['off', { allowConstantExport: true }],
'@typescript-eslint/consistent-type-imports': [
'warn',
{
fixStyle: 'inline-type-imports'
}
],
'@typescript-eslint/no-empty-object-type': ['error', { allowInterfaces: 'with-single-extends' }],
'@typescript-eslint/prefer-nullish-coalescing': 'off',
'@typescript-eslint/no-inferrable-types': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_'
}
],
'simple-import-sort/exports': 'error',
'import/no-duplicates': 'warn',
'simple-import-sort/imports': [
'warn',
{
@ -113,13 +116,8 @@ export default [
},
rules: {
...basicRules,
...playwright.configs['flat/recommended'].rules,
'no-console': 'off',
'require-jsdoc': 'off',
'simple-import-sort/exports': 'error',
'import/no-duplicates': 'warn',
'simple-import-sort/imports': 'warn'
}
}

View File

@ -12,12 +12,16 @@
<meta name="google-site-verification" content="bodB0xvBD_xM-VHg7EgfTf87jEMBF1DriZKdrZjwW1k" />
<meta name="yandex-verification" content="2b1f1f721cd6b66a" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="preload"
as="style"
href="https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Fira+Code:wght@300..700&family=Noto+Sans+Math&family=Noto+Sans+Symbols+2&family=Alegreya+Sans+SC:wght@100;300;400;500;700;800;900&family=Noto+Color+Emoji&display=block"
href="https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,400;0,500;0,600;1,400;1,600&family=Fira+Code:wght@400;500;600&family=Noto+Sans+Math&family=Noto+Sans+Symbols+2&family=Alegreya+Sans+SC:wght@400;700&family=Noto+Color+Emoji&display=swap"
onload="this.onload=null;this.rel='stylesheet'"
/>

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,7 @@
"test:e2e": "playwright test",
"dev": "vite --host",
"build": "tsc && vite build",
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
"lint": "stylelint \"src/**/*.css\" && eslint . --report-unused-disable-directives --max-warnings 0",
"lintFix": "eslint . --report-unused-disable-directives --max-warnings 0 --fix",
"preview": "vite preview --port 3000"
},
@ -17,12 +17,12 @@
"@dagrejs/dagre": "^1.1.4",
"@hookform/resolvers": "^4.1.3",
"@lezer/lr": "^1.4.2",
"@tanstack/react-query": "^5.67.2",
"@tanstack/react-query-devtools": "^5.67.2",
"@tanstack/react-query": "^5.69.0",
"@tanstack/react-query-devtools": "^5.69.0",
"@tanstack/react-table": "^8.21.2",
"@uiw/codemirror-themes": "^4.23.10",
"@uiw/react-codemirror": "^4.23.10",
"axios": "^1.8.2",
"axios": "^1.8.3",
"clsx": "^2.1.1",
"global": "^4.4.0",
"js-file-download": "^0.4.12",
@ -34,7 +34,7 @@
"react-icons": "^5.5.0",
"react-intl": "^7.1.6",
"react-router": "^7.3.0",
"react-scan": "^0.2.14",
"react-scan": "^0.3.2",
"react-select": "^5.10.1",
"react-tabs": "^6.1.0",
"react-toastify": "^11.0.5",
@ -47,11 +47,11 @@
},
"devDependencies": {
"@lezer/generator": "^1.7.2",
"@playwright/test": "^1.51.0",
"@tailwindcss/vite": "^4.0.12",
"@playwright/test": "^1.51.1",
"@tailwindcss/vite": "^4.0.14",
"@types/jest": "^29.5.14",
"@types/node": "^22.13.10",
"@types/react": "^19.0.10",
"@types/react": "^19.0.11",
"@types/react-dom": "^19.0.4",
"@typescript-eslint/eslint-plugin": "^8.0.1",
"@typescript-eslint/parser": "^8.0.1",
@ -66,11 +66,15 @@
"eslint-plugin-simple-import-sort": "^12.1.1",
"globals": "^16.0.0",
"jest": "^29.7.0",
"stylelint": "^16.16.0",
"stylelint-config-recommended": "^15.0.0",
"stylelint-config-standard": "^37.0.0",
"stylelint-config-tailwindcss": "^1.0.0",
"tailwindcss": "^4.0.7",
"ts-jest": "^29.2.6",
"typescript": "^5.8.2",
"typescript-eslint": "^8.26.0",
"vite": "^6.2.1"
"typescript-eslint": "^8.26.1",
"vite": "^6.2.2"
},
"overrides": {
"react": "^19.0.0"

View File

@ -15,20 +15,13 @@ export function NavigationButton({ icon, title, hideTitle, className, style, onC
return (
<button
type='button'
tabIndex={-1}
tabIndex={0}
aria-label={title}
data-tooltip-id={!!title ? globalIDs.tooltip : undefined}
data-tooltip-hidden={hideTitle}
data-tooltip-content={title}
onClick={onClick}
className={clsx(
'p-2 flex items-center gap-1',
'cursor-pointer',
'clr-btn-nav cc-animate-color duration-500',
'rounded-xl',
'font-controls whitespace-nowrap',
className
)}
className={clsx('p-2 flex items-center gap-1', 'cc-btn-nav', 'font-controls focus-outline', className)}
style={style}
>
{icon ? icon : null}

View File

@ -22,7 +22,7 @@ interface INavigationContext {
setRequireConfirmation: (value: boolean) => void;
}
export const NavigationContext = createContext<INavigationContext | null>(null);
const NavigationContext = createContext<INavigationContext | null>(null);
export const useConceptNavigation = () => {
const context = use(NavigationContext);
if (!context) {

View File

@ -13,25 +13,25 @@ import { ToggleNavigation } from './toggle-navigation';
import { UserMenu } from './user-menu';
export function Navigation() {
const router = useConceptNavigation();
const { push } = useConceptNavigation();
const size = useWindowSize();
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
const navigateHome = (event: React.MouseEvent<Element>) =>
router.push({ path: urls.home, newTab: event.ctrlKey || event.metaKey });
push({ path: urls.home, newTab: event.ctrlKey || event.metaKey });
const navigateLibrary = (event: React.MouseEvent<Element>) =>
router.push({ path: urls.library, newTab: event.ctrlKey || event.metaKey });
push({ path: urls.library, newTab: event.ctrlKey || event.metaKey });
const navigateHelp = (event: React.MouseEvent<Element>) =>
router.push({ path: urls.manuals, newTab: event.ctrlKey || event.metaKey });
push({ path: urls.manuals, newTab: event.ctrlKey || event.metaKey });
const navigateCreateNew = (event: React.MouseEvent<Element>) =>
router.push({ path: urls.create_schema, newTab: event.ctrlKey || event.metaKey });
push({ path: urls.create_schema, newTab: event.ctrlKey || event.metaKey });
return (
<nav className='z-navigation sticky top-0 left-0 right-0 select-none bg-prim-100'>
<ToggleNavigation />
<div
className={clsx(
'pl-2 pr-6 sm:pr-4 h-12 flex cc-shadow-border',
'pl-2 sm:pr-4 h-12 flex cc-shadow-border',
'transition-[max-height,translate] ease-bezier duration-(--duration-move)',
noNavigationAnimation ? '-translate-y-6 max-h-0' : 'max-h-12'
)}

View File

@ -1,5 +1,7 @@
'use client';
import clsx from 'clsx';
import { IconDarkTheme, IconLightTheme, IconPin, IconUnpin } from '@/components/icons';
import { useAppLayoutStore } from '@/stores/app-layout';
import { usePreferencesStore } from '@/stores/preferences';
@ -11,7 +13,9 @@ export function ToggleNavigation() {
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
const toggleNoNavigation = useAppLayoutStore(state => state.toggleNoNavigation);
return (
<div className='absolute top-0 right-0 z-navigation h-12 grid'>
<div
className={clsx('absolute top-0 right-0 z-navigation h-12', noNavigationAnimation ? 'grid' : 'hidden sm:grid')}
>
<button
tabIndex={-1}
type='button'
@ -19,6 +23,7 @@ export function ToggleNavigation() {
onClick={toggleNoNavigation}
data-tooltip-id={globalIDs.tooltip}
data-tooltip-content={noNavigationAnimation ? 'Показать навигацию' : 'Скрыть навигацию'}
aria-label={noNavigationAnimation ? 'Показать навигацию' : 'Скрыть навигацию'}
>
{!noNavigationAnimation ? <IconPin size='0.75rem' /> : null}
{noNavigationAnimation ? <IconUnpin size='0.75rem' /> : null}
@ -31,6 +36,7 @@ export function ToggleNavigation() {
onClick={toggleDarkMode}
data-tooltip-id={globalIDs.tooltip}
data-tooltip-content={darkMode ? 'Тема: Темная' : 'Тема: Светлая'}
aria-label={darkMode ? 'Тема: Темная' : 'Тема: Светлая'}
>
{darkMode ? <IconDarkTheme size='0.75rem' /> : null}
{!darkMode ? <IconLightTheme size='0.75rem' /> : null}

View File

@ -85,27 +85,28 @@ export function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
/>
<DropdownButton
text={darkMode ? 'Тема: Темная' : 'Тема: Светлая'}
icon={darkMode ? <IconDarkTheme size='1rem' /> : <IconLightTheme size='1rem' />}
title='Переключение темы оформления'
icon={darkMode ? <IconDarkTheme size='1rem' /> : <IconLightTheme size='1rem' />}
onClick={handleToggleDarkMode}
/>
<DropdownButton
text={showHelp ? 'Помощь: Вкл' : 'Помощь: Выкл'}
icon={showHelp ? <IconHelp size='1rem' /> : <IconHelpOff size='1rem' />}
title='Отображение иконок подсказок'
icon={showHelp ? <IconHelp size='1rem' /> : <IconHelpOff size='1rem' />}
onClick={toggleShowHelp}
/>
{user.is_staff ? (
<DropdownButton
text={adminMode ? 'Админ: Вкл' : 'Админ: Выкл'}
icon={adminMode ? <IconAdmin size='1rem' /> : <IconAdminOff size='1rem' />}
title='Работа в режиме администратора'
icon={adminMode ? <IconAdmin size='1rem' /> : <IconAdminOff size='1rem' />}
onClick={toggleAdminMode}
/>
) : null}
{user.is_staff ? (
<DropdownButton
text='REST API' //
title='Переход к backend API'
icon={<IconRESTapi size='1rem' />}
className='border-t'
onClick={gotoRestApi}
@ -114,6 +115,7 @@ export function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
{user.is_staff ? (
<DropdownButton
text='База данных' //
title='Переход к администрированию базы данных'
icon={<IconDatabase size='1rem' />}
onClick={gotoAdmin}
/>
@ -121,6 +123,7 @@ export function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
{user?.is_staff ? (
<DropdownButton
text='Иконки' //
title='Переход к странице иконок'
icon={<IconImage size='1rem' />}
onClick={gotoIcons}
/>
@ -128,6 +131,7 @@ export function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
{user.is_staff ? (
<DropdownButton
text='Структура БД' //
title='Переход к странице структуры БД'
icon={<IconDBStructure size='1rem' />}
onClick={gotoDatabaseSchema}
className='border-b'
@ -135,6 +139,7 @@ export function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
) : null}
<DropdownButton
text='Выйти...'
title='Выход из приложения'
className='font-semibold'
icon={<IconLogout size='1rem' />}
onClick={logoutAndRedirect}

View File

@ -33,19 +33,19 @@ axiosInstance.interceptors.request.use(config => {
});
// ================ Data transfer types ================
export interface IFrontRequest<RequestData, ResponseData> {
interface IFrontRequest<RequestData, ResponseData> {
data?: RequestData;
successMessage?: string | ((data: ResponseData) => string);
}
export interface IAxiosRequest<RequestData, ResponseData> {
interface IAxiosRequest<RequestData, ResponseData> {
endpoint: string;
request?: IFrontRequest<RequestData, ResponseData>;
options?: AxiosRequestConfig;
schema?: z.ZodType;
}
export interface IAxiosGetRequest {
interface IAxiosGetRequest {
endpoint: string;
options?: AxiosRequestConfig;
signal?: AbortSignal;

View File

@ -38,14 +38,13 @@ export function Button({
return (
<button
type='button'
disabled={disabled ?? loading}
className={clsx(
'inline-flex gap-2 items-center justify-center',
'font-medium select-none disabled:cursor-auto',
'clr-btn-default cc-animate-color',
dense ? 'px-1' : 'px-3 py-1',
loading ? 'cursor-progress' : 'cursor-pointer',
noOutline ? 'outline-hidden' : 'clr-outline',
noOutline ? 'outline-hidden' : 'focus-outline',
!noBorder && 'border rounded-sm',
className
)}
@ -53,6 +52,8 @@ export function Button({
data-tooltip-html={titleHtml}
data-tooltip-content={title}
data-tooltip-hidden={hideTitle}
disabled={disabled ?? loading}
aria-label={!text ? title : undefined}
{...restProps}
>
{icon ? icon : null}

View File

@ -49,6 +49,7 @@ export function MiniButton({
data-tooltip-html={titleHtml}
data-tooltip-content={title}
data-tooltip-hidden={hideTitle}
aria-label={title}
{...restProps}
>
{icon}

View File

@ -21,7 +21,7 @@ import { TableFooter } from './table-footer';
import { TableHeader } from './table-header';
import { useDataTable } from './use-data-table';
export { type ColumnSort, createColumnHelper, type RowSelectionState, type VisibilityState };
export { createColumnHelper, type RowSelectionState, type VisibilityState };
/** Style to conditionally apply to rows. */
export interface IConditionalStyle<TData> {
@ -120,6 +120,7 @@ export function DataTable<TData extends RowData>({
onRowDoubleClicked,
noDataComponent,
onChangePaginationOption,
paginationPerPage,
paginationOptions = [10, 20, 30, 40, 50],
@ -182,6 +183,7 @@ export function DataTable<TData extends RowData>({
<PaginationTools
id={id ? `${id}__pagination` : undefined}
table={table}
onChangePaginationOption={onChangePaginationOption}
paginationOptions={paginationOptions}
/>
) : null}

View File

@ -12,15 +12,22 @@ interface PaginationToolsProps<TData> {
id?: string;
table: Table<TData>;
paginationOptions: number[];
onChangePaginationOption?: (newValue: number) => void;
}
export function PaginationTools<TData>({ id, table, paginationOptions }: PaginationToolsProps<TData>) {
export function PaginationTools<TData>({
id,
table,
onChangePaginationOption,
paginationOptions
}: PaginationToolsProps<TData>) {
const handlePaginationOptionsChange = useCallback(
(event: React.ChangeEvent<HTMLSelectElement>) => {
const perPage = Number(event.target.value);
table.setPageSize(perPage);
onChangePaginationOption?.(perPage);
},
[table]
[table, onChangePaginationOption]
);
return (
@ -38,7 +45,8 @@ export function PaginationTools<TData>({ id, table, paginationOptions }: Paginat
<div className='flex'>
<button
type='button'
className='clr-hover clr-text-controls cc-animate-color'
aria-label='Первая страница'
className='clr-hover clr-text-controls cc-animate-color focus-outline'
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
@ -46,7 +54,8 @@ export function PaginationTools<TData>({ id, table, paginationOptions }: Paginat
</button>
<button
type='button'
className='clr-hover clr-text-controls cc-animate-color'
aria-label='Предыдущая страница'
className='clr-hover clr-text-controls cc-animate-color focus-outline'
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
@ -55,7 +64,8 @@ export function PaginationTools<TData>({ id, table, paginationOptions }: Paginat
<input
id={id ? `${id}__page` : undefined}
title='Номер страницы. Выделите для ручного ввода'
className='w-6 text-center bg-prim-100'
aria-label='Номер страницы'
className='w-6 text-center bg-prim-100 focus-outline'
value={table.getState().pagination.pageIndex + 1}
onChange={event => {
const page = event.target.value ? Number(event.target.value) - 1 : 0;
@ -66,7 +76,8 @@ export function PaginationTools<TData>({ id, table, paginationOptions }: Paginat
/>
<button
type='button'
className='clr-hover clr-text-controls cc-animate-color'
aria-label='Следующая страница'
className='clr-hover clr-text-controls cc-animate-color focus-outline'
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
@ -74,7 +85,8 @@ export function PaginationTools<TData>({ id, table, paginationOptions }: Paginat
</button>
<button
type='button'
className='clr-hover clr-text-controls cc-animate-color'
aria-label='Последняя страница'
className='clr-hover clr-text-controls cc-animate-color focus-outline'
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
@ -83,12 +95,13 @@ export function PaginationTools<TData>({ id, table, paginationOptions }: Paginat
</div>
<select
id={id ? `${id}__per_page` : undefined}
aria-label='Выбор количества строчек на странице'
value={table.getState().pagination.pageSize}
onChange={handlePaginationOptionsChange}
className='mx-2 cursor-pointer bg-prim-100'
className='mx-2 cursor-pointer bg-prim-100 focus-outline'
>
{paginationOptions.map(pageSize => (
<option key={`${prefixes.page_size}${pageSize}`} value={pageSize}>
<option key={`${prefixes.page_size}${pageSize}`} value={pageSize} aria-label={`${pageSize} на страницу`}>
{pageSize} на стр
</option>
))}

View File

@ -33,7 +33,7 @@ export function TableBody<TData>({
const handleRowClicked = useCallback(
(target: Row<TData>, event: React.MouseEvent<Element>) => {
onRowClicked?.(target.original, event);
if (target.getCanSelect()) {
if (table.options.enableRowSelection && target.getCanSelect()) {
if (event.shiftKey && !!lastSelected && lastSelected !== target.id) {
const { rows, rowsById } = table.getRowModel();
const lastIndex = rowsById[lastSelected].index;

View File

@ -1,9 +1,8 @@
'use client';
import { useCallback, useState } from 'react';
import { useState } from 'react';
import {
type ColumnSort,
createColumnHelper,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
@ -12,13 +11,10 @@ import {
type RowSelectionState,
type SortingState,
type TableOptions,
type Updater,
useReactTable,
type VisibilityState
} from '@tanstack/react-table';
export { type ColumnSort, createColumnHelper, type RowSelectionState, type VisibilityState };
/** Style to conditionally apply to rows. */
export interface IConditionalStyle<TData> {
/** Callback to determine if the style should be applied. */
@ -28,7 +24,7 @@ export interface IConditionalStyle<TData> {
style: React.CSSProperties;
}
export interface UseDataTableProps<TData extends RowData>
interface UseDataTableProps<TData extends RowData>
extends Pick<TableOptions<TData>, 'data' | 'columns' | 'onRowSelectionChange' | 'onColumnVisibilityChange'> {
/** Enable row selection. */
enableRowSelection?: boolean;
@ -48,9 +44,6 @@ export interface UseDataTableProps<TData extends RowData>
/** Number of rows per page. */
paginationPerPage?: number;
/** Callback to be called when the pagination option is changed. */
onChangePaginationOption?: (newValue: number) => void;
/** Enable sorting. */
enableSorting?: boolean;
@ -76,7 +69,6 @@ export function useDataTable<TData extends RowData>({
enablePagination,
paginationPerPage = 10,
onChangePaginationOption,
...restProps
}: UseDataTableProps<TData>) {
@ -86,19 +78,6 @@ export function useDataTable<TData extends RowData>({
pageSize: paginationPerPage
});
const handleChangePagination = useCallback(
(updater: Updater<PaginationState>) => {
setPagination(prev => {
const resolvedValue = typeof updater === 'function' ? updater(prev) : updater;
if (onChangePaginationOption && prev.pageSize !== resolvedValue.pageSize) {
onChangePaginationOption(resolvedValue.pageSize);
}
return resolvedValue;
});
},
[onChangePaginationOption]
);
const table = useReactTable({
state: {
pagination: pagination,
@ -114,7 +93,7 @@ export function useDataTable<TData extends RowData>({
onSortingChange: enableSorting ? setSorting : undefined,
getPaginationRowModel: enablePagination ? getPaginationRowModel() : undefined,
onPaginationChange: enablePagination ? handleChangePagination : undefined,
onPaginationChange: enablePagination ? setPagination : undefined,
enableHiding: enableHiding,
enableMultiRowSelection: enableRowSelection,

View File

@ -31,7 +31,6 @@ export function DropdownButton({
}: DropdownButtonProps) {
return (
<button
tabIndex={-1}
type='button'
onClick={onClick}
className={clsx(
@ -46,6 +45,7 @@ export function DropdownButton({
data-tooltip-html={titleHtml}
data-tooltip-content={title}
data-tooltip-hidden={hideTitle}
aria-label={title}
{...restProps}
>
{icon ? icon : null}

View File

@ -1,19 +0,0 @@
import clsx from 'clsx';
import { Checkbox, type CheckboxProps } from '../input';
/** Animated {@link Checkbox} inside a {@link Dropdown} item. */
export function DropdownCheckbox({ onChange: setValue, disabled, ...restProps }: CheckboxProps) {
return (
<div
className={clsx(
'px-3 py-1',
'text-left text-ellipsis whitespace-nowrap',
'disabled:clr-text-controls cc-animate-color',
!!setValue && !disabled && 'clr-hover'
)}
>
<Checkbox tabIndex={-1} disabled={disabled} onChange={setValue} {...restProps} />
</div>
);
}

View File

@ -48,6 +48,7 @@ export function Dropdown({
className
)}
aria-hidden={!isOpen}
inert={!isOpen}
{...restProps}
>
{children}

View File

@ -1,4 +1,3 @@
export { Dropdown } from './dropdown';
export { DropdownButton } from './dropdown-button';
export { DropdownCheckbox } from './dropdown-checkbox';
export { useDropdown } from './use-dropdown';

View File

@ -2,18 +2,21 @@
import { useRef, useState } from 'react';
import { useClickedOutside } from '@/hooks/use-clicked-outside';
export function useDropdown() {
const [isOpen, setIsOpen] = useState(false);
const ref = useRef(null);
const ref = useRef<HTMLDivElement>(null);
useClickedOutside(isOpen, ref, () => setIsOpen(false));
function handleBlur(event: React.FocusEvent<HTMLDivElement>) {
if (!ref.current?.contains(event.relatedTarget as Node)) {
setIsOpen(false);
}
}
return {
ref,
isOpen,
setIsOpen,
handleBlur,
toggle: () => setIsOpen(!isOpen),
hide: () => setIsOpen(false)
};

View File

@ -171,9 +171,9 @@ export interface IconProps {
className?: string;
}
function MetaIconSVG({ viewBox, size = '1.5rem', props, children }: React.PropsWithChildren<IconSVGProps>) {
function MetaIconSVG({ viewBox, size = '1.5rem', props, className, children }: React.PropsWithChildren<IconSVGProps>) {
return (
<svg width={size} height={size} fill='currentColor' viewBox={viewBox} {...props}>
<svg width={size} height={size} fill='currentColor' className={className} viewBox={viewBox} {...props}>
{children}
</svg>
);

View File

@ -6,7 +6,7 @@ import { CheckboxChecked, CheckboxNull } from '../icons';
import { type CheckboxProps } from './checkbox';
export interface CheckboxTristateProps extends Omit<CheckboxProps, 'value' | 'onChange'> {
interface CheckboxTristateProps extends Omit<CheckboxProps, 'value' | 'onChange'> {
/** Current value - `null`, `true` or `false`. */
value: boolean | null;
@ -55,12 +55,12 @@ export function CheckboxTristate({
cursor,
className
)}
disabled={disabled}
onClick={handleClick}
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
data-tooltip-html={titleHtml}
data-tooltip-content={title}
data-tooltip-hidden={hideTitle}
disabled={disabled}
{...restProps}
>
<div

View File

@ -54,12 +54,12 @@ export function Checkbox({
cursor,
className
)}
disabled={disabled}
onClick={handleClick}
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
data-tooltip-html={titleHtml}
data-tooltip-content={title}
data-tooltip-hidden={hideTitle}
disabled={disabled}
{...restProps}
>
<div

View File

@ -5,7 +5,7 @@ export { FileInput } from './file-input';
export { Label } from './label';
export { SearchBar } from './search-bar';
export { SelectMulti, type SelectMultiProps } from './select-multi';
export { SelectSingle, type SelectSingleProps } from './select-single';
export { SelectSingle } from './select-single';
export { SelectTree } from './select-tree';
export { TextArea } from './text-area';
export { TextInput } from './text-input';

View File

@ -41,10 +41,7 @@ export function SearchBar({
return (
<div className={clsx('relative flex items-center', className)} {...restProps}>
{!noIcon ? (
<IconSearch
className='absolute -top-0.5 left-2 translate-y-1/2 pointer-events-none clr-text-controls'
size='1.25rem'
/>
<IconSearch className='absolute -top-0.5 left-2 translate-y-1/2 cc-search-icon' size='1.25rem' />
) : null}
<TextInput
id={id}

View File

@ -38,7 +38,7 @@ function ClearIndicator<Option, Group extends GroupBase<Option> = GroupBase<Opti
);
}
export interface SelectSingleProps<Option, Group extends GroupBase<Option> = GroupBase<Option>>
interface SelectSingleProps<Option, Group extends GroupBase<Option> = GroupBase<Option>>
extends Omit<Props<Option, false, Group>, 'theme' | 'menuPortalTarget'> {
noPortal?: boolean;
noBorder?: boolean;

View File

@ -99,6 +99,7 @@ export function SelectTree<ItemType>({
>
{foldable.has(item) ? (
<MiniButton
aria-label={!folded.includes(item) ? 'Свернуть' : 'Развернуть'}
className={clsx('absolute left-1', !folded.includes(item) ? 'top-1.5' : 'top-1')}
noPadding
noHover

View File

@ -5,7 +5,7 @@ import { type Editor, type ErrorProcessing, type Titled } from '../props';
import { ErrorField } from './error-field';
import { Label } from './label';
export interface TextAreaProps extends Editor, ErrorProcessing, Titled, React.ComponentProps<'textarea'> {
interface TextAreaProps extends Editor, ErrorProcessing, Titled, React.ComponentProps<'textarea'> {
/** Indicates that the input should be transparent. */
transparent?: boolean;
@ -56,7 +56,7 @@ export function TextArea({
fitContent && 'field-sizing-content',
noResize && 'resize-none',
transparent ? 'bg-transparent' : 'clr-input',
!noOutline && 'clr-outline',
!noOutline && 'focus-outline',
dense && 'grow max-w-full',
!dense && !!label && 'mt-2',
!dense && className

View File

@ -54,7 +54,7 @@ export function TextInput({
'leading-tight truncate hover:text-clip',
transparent ? 'bg-transparent' : 'clr-input',
!noBorder && 'border',
!noOutline && 'clr-outline',
!noOutline && 'focus-outline',
(!noBorder || !disabled) && 'px-3',
dense && 'grow max-w-full',
!dense && !!label && 'mt-2',

View File

@ -105,9 +105,9 @@ export function ModalForm({
) : null}
<MiniButton
noPadding
aria-label='Закрыть'
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
aria-label='Закрыть'
noPadding
icon={<IconClose size='1.25rem' />}
className='absolute z-pop top-2 right-2'
onClick={hideDialog}

View File

@ -49,9 +49,9 @@ export function ModalView({
) : null}
<MiniButton
noPadding
aria-label='Закрыть'
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
aria-label='Закрыть'
noPadding
icon={<IconClose size='1.25rem' />}
className='absolute z-pop top-2 right-2'
onClick={hideDialog}
@ -71,9 +71,10 @@ export function ModalView({
<div
className={clsx(
'@container/modal',
'max-h-[calc(100svh-8rem)] max-w-[100svw] xs:max-w-[calc(100svw-2rem)]',
'max-w-[100svw] xs:max-w-[calc(100svw-2rem)]',
'overscroll-contain outline-hidden',
overflowVisible ? 'overflow-visible' : 'overflow-auto',
fullScreen ? 'max-h-[calc(100svh-2rem)]' : 'max-h-[calc(100svh-8rem)]',
className
)}
{...restProps}

View File

@ -14,7 +14,15 @@ interface TabLabelProps extends Omit<TabPropsImpl, 'children'>, Titled {
/**
* Displays a tab header with a label.
*/
export function TabLabel({ label, title, titleHtml, hideTitle, className, ...otherProps }: TabLabelProps) {
export function TabLabel({
label,
title,
titleHtml,
hideTitle,
className,
role = 'tab',
...otherProps
}: TabLabelProps) {
return (
<TabImpl
className={clsx(
@ -31,6 +39,7 @@ export function TabLabel({ label, title, titleHtml, hideTitle, className, ...oth
data-tooltip-html={titleHtml}
data-tooltip-content={title}
data-tooltip-hidden={hideTitle}
role={role}
{...otherProps}
>
{label}

View File

@ -5,5 +5,4 @@ export { PDFViewer } from './pdf-viewer';
export { PrettyJson } from './pretty-json';
export { TextContent } from './text-content';
export { ValueIcon } from './value-icon';
export { ValueLabeled } from './value-labeled';
export { ValueStats } from './value-stats';

View File

@ -5,7 +5,7 @@ import { truncateToLastWord } from '@/utils/utils';
import { type Styling } from '../props';
export interface TextContentProps extends Styling {
interface TextContentProps extends Styling {
/** Text to display. */
text: string;

View File

@ -15,15 +15,9 @@ interface ValueIconProps extends Styling, Titled {
/** Icon to display. */
icon: React.ReactNode;
/** Classname for the text. */
textClassName?: string;
/** Callback to be called when the component is clicked. */
onClick?: (event: React.MouseEvent<Element>) => void;
/** Number of symbols to display in a small size. */
smallThreshold?: number;
/** Indicates that padding should be minimal. */
dense?: boolean;
@ -39,18 +33,14 @@ export function ValueIcon({
dense,
icon,
value,
textClassName,
disabled = true,
title,
titleHtml,
hideTitle,
className,
smallThreshold,
onClick,
...restProps
}: ValueIconProps) {
// TODO: use CSS instead of threshold
const isSmall = !smallThreshold || String(value).length < smallThreshold;
return (
<div
className={clsx(
@ -65,11 +55,10 @@ export function ValueIcon({
data-tooltip-html={titleHtml}
data-tooltip-content={title}
data-tooltip-hidden={hideTitle}
aria-label={title}
>
<MiniButton noHover noPadding icon={icon} disabled={disabled} onClick={onClick} />
<span id={id} className={clsx({ 'text-xs': !isSmall }, textClassName)}>
{value}
</span>
{onClick ? <MiniButton noHover noPadding icon={icon} onClick={onClick} disabled={disabled} /> : icon}
<span id={id}>{value}</span>
</div>
);
}

View File

@ -1,29 +0,0 @@
import clsx from 'clsx';
import { type Styling } from '@/components/props';
interface ValueLabeledProps extends Styling {
/** Id of the component. */
id?: string;
/** Label to display. */
label: string;
/** Value to display. */
text: string | number;
/** Tooltip for the component. */
title?: string;
}
/**
* Displays a labeled value.
*/
export function ValueLabeled({ id, label, text, title, className, ...restProps }: ValueLabeledProps) {
return (
<div className={clsx('flex justify-between gap-6', className)} {...restProps}>
<span title={title}>{label}</span>
<span id={id}>{text}</span>
</div>
);
}

View File

@ -1,6 +1,7 @@
import { type Styling, type Titled } from '@/components/props';
import clsx from 'clsx';
import { ValueIcon } from './value-icon';
import { type Styling, type Titled } from '@/components/props';
import { globalIDs } from '@/utils/constants';
// characters - threshold for small labels - small font
const SMALL_THRESHOLD = 3;
@ -16,9 +17,23 @@ interface ValueStatsProps extends Styling, Titled {
value: string | number;
}
/**
* Displays statistics value with an icon.
*/
export function ValueStats(props: ValueStatsProps) {
return <ValueIcon dense smallThreshold={SMALL_THRESHOLD} textClassName='min-w-5' {...props} />;
/** Displays statistics value with an icon. */
export function ValueStats({ id, icon, value, className, title, titleHtml, hideTitle, ...restProps }: ValueStatsProps) {
const isSmall = String(value).length < SMALL_THRESHOLD;
return (
<div
className={clsx('flex items-center gap-1', 'text-right', 'hover:cursor-default', className)}
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
data-tooltip-html={titleHtml}
data-tooltip-content={title}
data-tooltip-hidden={hideTitle}
aria-label={title}
{...restProps}
>
{icon}
<span id={id} className={clsx(!isSmall && 'text-xs', 'min-w-5')}>
{value}
</span>
</div>
);
}

View File

@ -64,20 +64,20 @@ export function LoginPage() {
id='username'
autoComplete='username'
label='Логин или email'
{...register('username')}
autoFocus
allowEnter
spellCheck={false}
defaultValue={initialName}
{...register('username')}
error={errors.username}
/>
<TextInput
id='password'
{...register('password')}
type='password'
autoComplete='current-password'
label='Пароль'
allowEnter
{...register('password')}
error={errors.password}
/>

View File

@ -10,13 +10,13 @@ interface SubtopicsProps {
export function Subtopics({ headTopic }: SubtopicsProps) {
return (
<>
<h2>Содержание раздела</h2>
<details>
<summary className='text-center font-semibold'>Содержание раздела</summary>
{Object.values(HelpTopic)
.filter(topic => topic !== headTopic && topicParent.get(topic) === headTopic)
.map(topic => (
<TopicItem key={`${prefixes.topic_item}${topic}`} topic={topic} />
))}
</>
</details>
);
}

View File

@ -26,17 +26,22 @@ export function HelpInterface() {
интерфейса изменяются (цвет, иконка) в зависимости от доступности соответствующего функционала.
</p>
<p>
<IconHelp className='inline-icon' />
Помимо данного раздела справка предоставляется контекстно через специальную иконку{' '}
<IconHelp className='inline-icon' />
<IconHelp className='inline-icon' /> Помимо данного раздела справка предоставляется контекстно через специальную
иконку <IconHelp className='inline-icon' />
</p>
<h2>Навигация и настройки</h2>
<li>Ctrl + клик на объект навигации откроет новую вкладку</li>
<li>
<kbd>Ctrl + клик</kbd> на объект навигации откроет новую вкладку
</li>
<li>
<IconPin size='1.25rem' className='inline-icon' /> навигационную панель можно скрыть с помощью кнопки в правом
верхнем углу
</li>
<li>
<IconLightTheme className='inline-icon' />
<IconDarkTheme className='inline-icon' /> переключатели темы
</li>
<li>
<IconLogin size='1.25rem' className='inline-icon' /> вход в систему / регистрация нового пользователя
</li>
@ -44,10 +49,7 @@ export function HelpInterface() {
<IconUser2 size='1.25rem' className='inline-icon' /> меню пользователя содержит ряд настроек и переход к профилю
пользователя
</li>
<li>
<IconLightTheme className='inline-icon' />
<IconDarkTheme className='inline-icon' /> переключатели темы
</li>
<li>
<IconHelp className='inline-icon' />
<IconHelpOff className='inline-icon' /> отключение иконок контекстной справки

View File

@ -20,20 +20,22 @@ export function HelpMain() {
<LinkTopic text='Операционной схеме синтеза' topic={HelpTopic.CC_OSS} />.
</p>
<h2>Разделы Справки</h2>
{[
HelpTopic.THESAURUS,
HelpTopic.INTERFACE,
HelpTopic.CONCEPTUAL,
HelpTopic.RSLANG,
HelpTopic.TERM_CONTROL,
HelpTopic.ACCESS,
HelpTopic.VERSIONS,
HelpTopic.INFO,
HelpTopic.EXTEOR
].map(topic => (
<TopicItem key={`${prefixes.topic_item}${topic}`} topic={topic} />
))}
<details>
<summary className='text-center font-semibold'>Разделы Справки</summary>
{[
HelpTopic.THESAURUS,
HelpTopic.INTERFACE,
HelpTopic.CONCEPTUAL,
HelpTopic.RSLANG,
HelpTopic.TERM_CONTROL,
HelpTopic.ACCESS,
HelpTopic.VERSIONS,
HelpTopic.INFO,
HelpTopic.EXTEOR
].map(topic => (
<TopicItem key={`${prefixes.topic_item}${topic}`} topic={topic} />
))}
</details>
<h2>Лицензирование и раскрытие информации</h2>
<li>Пользователи Портала сохраняют авторские права на создаваемый ими контент</li>

View File

@ -259,6 +259,10 @@ export function HelpThesaurus() {
<h2>Операция</h2>
<p>Операция выделенная часть ОСС, определяющая способ получения КС в рамках ОСС.</p>
<p>
<IconConsolidation className='inline-icon' />
{'\u2009'}Ромбовидный синтез операция, где используются КС, имеющие общих предков.
</p>
<ul>
По <b>способу получения КС выделены</b>:
@ -271,13 +275,6 @@ export function HelpThesaurus() {
{'\u2009'}синтез концептуальных схем.
</li>
</ul>
<br />
<p>
<IconConsolidation className='inline-icon' />
{'\u2009'}Ромбовидный синтез операция, где используются КС, имеющие общих предков.
</p>
</div>
);
}

View File

@ -34,8 +34,12 @@ export function HelpLibrary() {
<li>
<span className='text-(--acc-fg-green)'>зеленым текстом</span> выделены ОСС
</li>
<li>клик по строке - переход к редактированию схемы</li>
<li>Ctrl + клик по строке откроет схему в новой вкладке</li>
<li>
<kbd>клик</kbd> по строке - переход к редактированию схемы
</li>
<li>
<kbd>Ctrl + клик</kbd> по строке откроет схему в новой вкладке
</li>
<li>Фильтры атрибутов три позиции: да/нет/не применять</li>
<li>
<IconShow size='1rem' className='inline-icon' /> фильтры атрибутов применяются по клику
@ -67,9 +71,15 @@ export function HelpLibrary() {
<li>
<IconSubfolders size='1rem' className='inline-icon icon-green' /> схемы во вложенных папках
</li>
<li>клик по папке отображает справа схемы в ней</li>
<li>Ctrl + клик по папке копирует путь в буфер обмена</li>
<li>клик по иконке сворачивает/разворачивает вложенные</li>
<li>
<kbd>клик</kbd> по папке отображает справа схемы в ней
</li>
<li>
<kbd>Ctrl + клик по папке копирует путь в буфер обмена</kbd>
</li>
<li>
<kbd>клик</kbd> по иконке сворачивает/разворачивает вложенные
</li>
<li>
<IconFolderEmpty size='1rem' className='inline-icon clr-text-default' /> папка без схем
</li>

View File

@ -53,10 +53,14 @@ export function HelpOssGraph() {
<div className='sm:w-84'>
<h1>Изменение узлов</h1>
<li>Клик на операцию выделение</li>
<li>Esc сбросить выделение</li>
<li>
Двойной клик переход к связанной <LinkTopic text='КС' topic={HelpTopic.CC_SYSTEM} />
<kbd>Клик</kbd> на операцию выделение
</li>
<li>
<kbd>Esc</kbd> сбросить выделение
</li>
<li>
<kbd>Двойной клик</kbd> переход к связанной <LinkTopic text='КС' topic={HelpTopic.CC_SYSTEM} />
</li>
<li>
<IconEdit2 className='inline-icon' /> Редактирование операции
@ -65,7 +69,7 @@ export function HelpOssGraph() {
<IconNewItem className='inline-icon icon-green' /> Новая операция
</li>
<li>
<IconDestroy className='inline-icon icon-red' /> Delete удалить выбранные
<IconDestroy className='inline-icon icon-red' /> <kbd>Delete</kbd> удалить выбранные
</li>
</div>
</div>

View File

@ -34,7 +34,7 @@ export function HelpRSCard() {
<IconOSS className='inline-icon' /> переход к связанной <LinkTopic text='ОСС' topic={HelpTopic.CC_OSS} />
</li>
<li>
<IconSave className='inline-icon' /> сохранить изменения: Ctrl + S
<IconSave className='inline-icon' /> сохранить изменения: <kbd>Ctrl + S</kbd>
</li>
<li>
<IconEditor className='inline-icon' /> Редактор обладает правом редактирования

View File

@ -38,13 +38,13 @@ export function HelpRSEditor() {
<IconList className='inline-icon' /> список конституент
</li>
<li>
<IconSave className='inline-icon' /> сохранить: Ctrl + S
<IconSave className='inline-icon' /> сохранить: <kbd>Ctrl + S</kbd>
</li>
<li>
<IconReset className='inline-icon' /> сбросить изменения
</li>
<li>
<IconClone className='inline-icon icon-green' /> клонировать: Alt + V
<IconClone className='inline-icon icon-green' /> клонировать: <kbd>Alt + V</kbd>
</li>
<li>
<IconNewItem className='inline-icon icon-green' /> новая конституента
@ -58,7 +58,7 @@ export function HelpRSEditor() {
<h2>Список конституент</h2>
<li>
<IconMoveDown className='inline-icon' />
<IconMoveUp className='inline-icon' /> Alt + вверх/вниз
<IconMoveUp className='inline-icon' /> <kbd>Alt + вверх/вниз</kbd>
</li>
<li>
<IconFilter className='inline-icon' />
@ -98,14 +98,18 @@ export function HelpRSEditor() {
<IconTree className='inline-icon' /> отображение{' '}
<LinkTopic text='дерева разбора' topic={HelpTopic.UI_FORMULA_TREE} />
</li>
<li>Ctrl + Пробел вставка незанятого имени / замена проекции</li>
<li>
<kbd>Ctrl + Пробел</kbd> вставка незанятого имени / замена проекции
</li>
<h2>Термин и Текстовое определение</h2>
<li>
<IconEdit className='inline-icon' /> редактирование <LinkTopic text='Имени' topic={HelpTopic.CC_CONSTITUENTA} />{' '}
/ <LinkTopic text='Термина' topic={HelpTopic.CC_CONSTITUENTA} />
</li>
<li>Ctrl + Пробел открывает редактирование отсылок</li>
<li>
<kbd>Ctrl + Пробел</kbd> открывает редактирование отсылок
</li>
</div>
);
}

View File

@ -33,28 +33,34 @@ export function HelpRSList() {
<IconOSS className='inline-icon' /> переход к связанной <LinkTopic text='ОСС' topic={HelpTopic.CC_OSS} />
</li>
<li>
<IconReset className='inline-icon' /> сбросить выделение: ESC
<IconReset className='inline-icon' /> сбросить выделение: <kbd>ESC</kbd>
</li>
<li>Клик на строку выделение</li>
<li>Shift + клик выделение нескольких</li>
<li>Alt + клик Редактор</li>
<li>Двойной клик Редактор</li>
<li>
<kbd>Shift + клик</kbd> выделение нескольких
</li>
<li>
<kbd>Alt + клик</kbd> Редактор
</li>
<li>
<kbd>Двойной клик</kbd> Редактор
</li>
<li>
<IconMoveUp className='inline-icon' />
<IconMoveDown className='inline-icon' /> Alt + вверх/вниз перемещение
<IconMoveDown className='inline-icon' /> <kbd>Alt + вверх/вниз</kbd> перемещение
</li>
<li>
<IconClone className='inline-icon icon-green' /> клонировать выделенную: Alt + V
<IconClone className='inline-icon icon-green' /> клонировать выделенную: <kbd>Alt + V</kbd>
</li>
<li>
<IconNewItem className='inline-icon icon-green' /> новая конституента: Alt + `
<IconNewItem className='inline-icon icon-green' /> новая конституента: <kbd>Alt + `</kbd>
</li>
<li>
<IconOpenList className='inline-icon icon-green' /> быстрое добавление: Alt + 1-6,Q,W
<IconOpenList className='inline-icon icon-green' /> быстрое добавление: <kbd>Alt + 1-6,Q,W</kbd>
</li>
<li>
<IconDestroy className='inline-icon icon-red' /> удаление выделенных: Delete
<IconDestroy className='inline-icon icon-red' /> удаление выделенных: <kbd>Delete</kbd>
</li>
<Divider margins='my-2' />

View File

@ -1,53 +1,55 @@
/**
* Represents manuals topic.
*/
export enum HelpTopic {
MAIN = 'main',
export const HelpTopic = {
MAIN: 'main',
THESAURUS = 'thesaurus',
THESAURUS: 'thesaurus',
INTERFACE = 'user-interface',
UI_LIBRARY = 'ui-library',
UI_RS_MENU = 'ui-rsform-menu',
UI_RS_CARD = 'ui-rsform-card',
UI_RS_LIST = 'ui-rsform-list',
UI_RS_EDITOR = 'ui-rsform-editor',
UI_GRAPH_TERM = 'ui-graph-term',
UI_FORMULA_TREE = 'ui-formula-tree',
UI_TYPE_GRAPH = 'ui-type-graph',
UI_CST_STATUS = 'ui-rsform-cst-status',
UI_CST_CLASS = 'ui-rsform-cst-class',
UI_OSS_GRAPH = 'ui-oss-graph',
UI_SUBSTITUTIONS = 'ui-substitutions',
UI_RELOCATE_CST = 'ui-relocate-cst',
INTERFACE: 'user-interface',
UI_LIBRARY: 'ui-library',
UI_RS_MENU: 'ui-rsform-menu',
UI_RS_CARD: 'ui-rsform-card',
UI_RS_LIST: 'ui-rsform-list',
UI_RS_EDITOR: 'ui-rsform-editor',
UI_GRAPH_TERM: 'ui-graph-term',
UI_FORMULA_TREE: 'ui-formula-tree',
UI_TYPE_GRAPH: 'ui-type-graph',
UI_CST_STATUS: 'ui-rsform-cst-status',
UI_CST_CLASS: 'ui-rsform-cst-class',
UI_OSS_GRAPH: 'ui-oss-graph',
UI_SUBSTITUTIONS: 'ui-substitutions',
UI_RELOCATE_CST: 'ui-relocate-cst',
CONCEPTUAL = 'concept',
CC_SYSTEM = 'concept-rsform',
CC_CONSTITUENTA = 'concept-constituenta',
CC_RELATIONS = 'concept-relations',
CC_SYNTHESIS = 'concept-synthesis',
CC_OSS = 'concept-operations-schema',
CC_PROPAGATION = 'concept-change-propagation',
CONCEPTUAL: 'concept',
CC_SYSTEM: 'concept-rsform',
CC_CONSTITUENTA: 'concept-constituenta',
CC_RELATIONS: 'concept-relations',
CC_SYNTHESIS: 'concept-synthesis',
CC_OSS: 'concept-operations-schema',
CC_PROPAGATION: 'concept-change-propagation',
RSLANG = 'rslang',
RSL_TYPES = 'rslang-types',
RSL_CORRECT = 'rslang-correctness',
RSL_INTERPRET = 'rslang-interpretation',
RSL_OPERATIONS = 'rslang-operations',
RSL_TEMPLATES = 'rslang-templates',
RSLANG: 'rslang',
RSL_TYPES: 'rslang-types',
RSL_CORRECT: 'rslang-correctness',
RSL_INTERPRET: 'rslang-interpretation',
RSL_OPERATIONS: 'rslang-operations',
RSL_TEMPLATES: 'rslang-templates',
TERM_CONTROL = 'terminology-control',
ACCESS = 'access',
VERSIONS = 'versions',
TERM_CONTROL: 'terminology-control',
ACCESS: 'access',
VERSIONS: 'versions',
INFO = 'documentation',
INFO_RULES = 'rules',
INFO_CONTRIB = 'contributors',
INFO_PRIVACY = 'privacy',
INFO_API = 'api',
INFO: 'documentation',
INFO_RULES: 'rules',
INFO_CONTRIB: 'contributors',
INFO_PRIVACY: 'privacy',
INFO_API: 'api',
EXTEOR = 'exteor'
}
EXTEOR: 'exteor'
} as const;
export type HelpTopic = (typeof HelpTopic)[keyof typeof HelpTopic];
/**
* Manual topics hierarchy.
@ -99,8 +101,3 @@ export const topicParent = new Map<HelpTopic, HelpTopic>([
[HelpTopic.EXTEOR, HelpTopic.EXTEOR]
]);
/**
* Topics that can be folded.
*/
export const foldableTopics = [HelpTopic.INTERFACE, HelpTopic.RSLANG, HelpTopic.CONCEPTUAL, HelpTopic.INFO];

View File

@ -30,6 +30,7 @@ export function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownPro
return (
<div
ref={menu.ref}
onBlur={menu.handleBlur}
className={clsx(
'absolute left-0 w-54', //
noNavigation ? 'top-0' : 'top-12',

View File

@ -1,14 +1,17 @@
import { urls, useConceptNavigation } from '@/app';
import { useAuthSuspense } from '@/features/auth';
import { PARAMETER } from '@/utils/constants';
export function HomePage() {
const router = useConceptNavigation();
const { isAnonymous } = useAuthSuspense();
if (isAnonymous) {
router.replace({ path: urls.manuals });
// Note: Timeout is needed to let router initialize
setTimeout(() => router.replace({ path: urls.login }), PARAMETER.minimalTimeout);
} else {
router.replace({ path: urls.library });
setTimeout(() => router.replace({ path: urls.library }), PARAMETER.minimalTimeout);
}
return null;

View File

@ -5,17 +5,19 @@ import { errorMsg } from '@/utils/labels';
import { validateLocation } from '../models/library-api';
/** Represents type of library items. */
export enum LibraryItemType {
RSFORM = 'rsform',
OSS = 'oss'
}
export const LibraryItemType = {
RSFORM: 'rsform',
OSS: 'oss'
} as const;
export type LibraryItemType = (typeof LibraryItemType)[keyof typeof LibraryItemType];
/** Represents Access policy for library items.*/
export enum AccessPolicy {
PUBLIC = 'public',
PROTECTED = 'protected',
PRIVATE = 'private'
}
export const AccessPolicy = {
PUBLIC: 'public',
PROTECTED: 'protected',
PRIVATE: 'private'
} as const;
export type AccessPolicy = (typeof AccessPolicy)[keyof typeof AccessPolicy];
/** Represents library item common data typical for all item types. */
export type ILibraryItem = z.infer<typeof schemaLibraryItem>;
@ -53,17 +55,19 @@ export type IVersionCreateDTO = z.infer<typeof schemaVersionCreate>;
export type IVersionUpdateDTO = z.infer<typeof schemaVersionUpdate>;
// ======= SCHEMAS =========
export const schemaLibraryItemType = z.enum(Object.values(LibraryItemType) as [LibraryItemType, ...LibraryItemType[]]);
export const schemaAccessPolicy = z.enum(Object.values(AccessPolicy) as [AccessPolicy, ...AccessPolicy[]]);
export const schemaLibraryItem = z.strictObject({
id: z.coerce.number(),
item_type: z.nativeEnum(LibraryItemType),
item_type: schemaLibraryItemType,
title: z.string(),
alias: z.string().nonempty(),
comment: z.string(),
description: z.string(),
visible: z.boolean(),
read_only: z.boolean(),
location: z.string(),
access_policy: z.nativeEnum(AccessPolicy),
access_policy: schemaAccessPolicy,
time_create: z.string().datetime({ offset: true }),
time_update: z.string().datetime({ offset: true }),
@ -78,7 +82,7 @@ export const schemaCloneLibraryItem = schemaLibraryItem
item_type: true,
title: true,
alias: true,
comment: true,
description: true,
visible: true,
read_only: true,
location: true,
@ -94,14 +98,14 @@ export const schemaCloneLibraryItem = schemaLibraryItem
export const schemaCreateLibraryItem = z
.object({
item_type: z.nativeEnum(LibraryItemType),
item_type: schemaLibraryItemType,
title: z.string().optional(),
alias: z.string().optional(),
comment: z.string(),
description: z.string(),
visible: z.boolean(),
read_only: z.boolean(),
location: z.string().refine(data => validateLocation(data), { message: errorMsg.invalidLocation }),
access_policy: z.nativeEnum(AccessPolicy),
access_policy: schemaAccessPolicy,
file: z.instanceof(File).optional(),
fileName: z.string().optional()
@ -117,10 +121,10 @@ export const schemaCreateLibraryItem = z
export const schemaUpdateLibraryItem = z.strictObject({
id: z.number(),
item_type: z.nativeEnum(LibraryItemType),
item_type: schemaLibraryItemType,
title: z.string().nonempty(errorMsg.requiredField),
alias: z.string().nonempty(errorMsg.requiredField),
comment: z.string(),
description: z.string(),
visible: z.boolean(),
read_only: z.boolean()
});

View File

@ -85,9 +85,9 @@ export function EditorLibraryItem({ schema, isAttachedToOSS }: EditorLibraryItem
<div className='flex flex-col'>
<div className='relative flex justify-stretch sm:mb-1 max-w-120 gap-3'>
<MiniButton
title='Открыть в библиотеке'
noHover
noPadding
title='Открыть в библиотеке'
icon={<IconFolderOpened size='1.25rem' className='icon-primary' />}
onClick={handleOpenLibrary}
/>
@ -101,7 +101,7 @@ export function EditorLibraryItem({ schema, isAttachedToOSS }: EditorLibraryItem
/>
</div>
<div className='relative'>
<div className='relative' ref={ownerSelector.ref} onBlur={ownerSelector.handleBlur}>
{ownerSelector.isOpen ? (
<div className='absolute -top-2 right-0'>
<SelectUser className='w-100 text-sm' value={schema.owner} onChange={onSelectUser} />
@ -133,23 +133,21 @@ export function EditorLibraryItem({ schema, isAttachedToOSS }: EditorLibraryItem
</Tooltip>
<ValueIcon
title='Дата обновления'
dense
disabled
icon={<IconDateUpdate size='1.25rem' className='text-ok-600' />}
value={new Date(schema.time_update).toLocaleString(intl.locale)}
title='Дата обновления'
/>
<ValueIcon
title='Дата создания'
dense
disabled
icon={<IconDateCreate size='1.25rem' className='text-ok-600' />}
value={new Date(schema.time_create).toLocaleString(intl.locale, {
year: '2-digit',
month: '2-digit',
day: '2-digit'
})}
title='Дата создания'
/>
</div>
</div>

View File

@ -44,7 +44,7 @@ export function MenuRole({ isOwned, isEditor }: MenuRoleProps) {
}
return (
<div ref={accessMenu.ref} className='relative'>
<div ref={accessMenu.ref} onBlur={accessMenu.handleBlur} className='relative'>
<Button
dense
noBorder
@ -67,22 +67,22 @@ export function MenuRole({ isOwned, isEditor }: MenuRoleProps) {
text={labelUserRole(UserRole.EDITOR)}
title={describeUserRole(UserRole.EDITOR)}
icon={<IconRole role={UserRole.EDITOR} size='1rem' />}
disabled={!isOwned && !isEditor}
onClick={() => handleChangeMode(UserRole.EDITOR)}
disabled={!isOwned && !isEditor}
/>
<DropdownButton
text={labelUserRole(UserRole.OWNER)}
title={describeUserRole(UserRole.OWNER)}
icon={<IconRole role={UserRole.OWNER} size='1rem' />}
disabled={!isOwned}
onClick={() => handleChangeMode(UserRole.OWNER)}
disabled={!isOwned}
/>
<DropdownButton
text={labelUserRole(UserRole.ADMIN)}
title={describeUserRole(UserRole.ADMIN)}
icon={<IconRole role={UserRole.ADMIN} size='1rem' />}
disabled={!user.is_staff}
onClick={() => handleChangeMode(UserRole.ADMIN)}
disabled={!user.is_staff}
/>
</Dropdown>
</div>

View File

@ -28,10 +28,15 @@ export function MiniSelectorOSS({ items, onSelect, className, ...restProps }: Mi
}
return (
<div ref={ossMenu.ref} className={clsx('relative flex items-center', className)} {...restProps}>
<div
ref={ossMenu.ref}
onBlur={ossMenu.handleBlur}
className={clsx('relative flex items-center', className)}
{...restProps}
>
<MiniButton
icon={<IconOSS size='1.25rem' className='icon-primary' />}
title='Операционные схемы'
icon={<IconOSS size='1.25rem' className='icon-primary' />}
hideTitle={ossMenu.isOpen}
onClick={onToggle}
/>
@ -40,9 +45,9 @@ export function MiniSelectorOSS({ items, onSelect, className, ...restProps }: Mi
<Label text='Список ОСС' className='border-b px-3 py-1' />
{items.map((reference, index) => (
<DropdownButton
className='min-w-20'
key={`${prefixes.oss_list}${index}`}
text={reference.alias}
className='min-w-20'
onClick={event => onSelect(event, reference)}
/>
))}

View File

@ -35,7 +35,12 @@ export function SelectLocationContext({
}
return (
<div ref={menu.ref} className={clsx('relative text-right self-start', className)} {...restProps}>
<div
ref={menu.ref} //
onBlur={menu.handleBlur}
className={clsx('relative text-right self-start', className)}
{...restProps}
>
<MiniButton
title={title}
hideTitle={menu.isOpen}

View File

@ -112,10 +112,10 @@ export function PickSchema({
query={filterText}
onChangeQuery={newValue => setFilterText(newValue)}
/>
<div className='relative' ref={locationMenu.ref}>
<div className='relative' ref={locationMenu.ref} onBlur={locationMenu.handleBlur}>
<MiniButton
icon={<IconFolderTree size='1.25rem' className={!!filterLocation ? 'icon-green' : 'icon-primary'} />}
title='Фильтр по расположению'
icon={<IconFolderTree size='1.25rem' className={!!filterLocation ? 'icon-green' : 'icon-primary'} />}
className='mt-1'
onClick={() => locationMenu.toggle()}
/>

View File

@ -38,7 +38,7 @@ export function SelectAccessPolicy({
}
return (
<div ref={menu.ref} className={clsx('relative', className)} {...restProps}>
<div ref={menu.ref} onBlur={menu.handleBlur} className={clsx('relative', className)} {...restProps}>
<MiniButton
title={`Доступ: ${labelAccessPolicy(value)}`}
hideTitle={menu.isOpen}

View File

@ -37,7 +37,7 @@ export function SelectItemType({
}
return (
<div ref={menu.ref} className={clsx('relative', className)} {...restProps}>
<div ref={menu.ref} onBlur={menu.handleBlur} className={clsx('relative', className)} {...restProps}>
<SelectorButton
transparent
title={describeLibraryItemType(value)}

View File

@ -33,7 +33,12 @@ export function SelectLocationHead({
}
return (
<div ref={menu.ref} className={clsx('text-right relative', className)} {...restProps}>
<div
ref={menu.ref} //
onBlur={menu.handleBlur}
className={clsx('text-right relative', className)}
{...restProps}
>
<SelectorButton
transparent
tabIndex={-1}
@ -52,10 +57,10 @@ export function SelectLocationHead({
return (
<DropdownButton
key={`${prefixes.location_head_list}${index}`}
onClick={() => handleChange(head)}
title={describeLocationHead(head)}
icon={<IconLocationHead value={head} size='1rem' />}
text={labelLocationHead(head)}
title={describeLocationHead(head)}
onClick={() => handleChange(head)}
icon={<IconLocationHead value={head} size='1rem' />}
/>
);
})}

View File

@ -84,6 +84,7 @@ export function SelectLocation({ value, dense, prefix, onClick, className, style
<IconFolderOpened size='1rem' className='icon-green' />
)
}
aria-label='Отображение вложенных папок'
onClick={event => handleClickFold(event, item, folded.includes(item))}
/>
) : (

View File

@ -48,13 +48,14 @@ export function ToolbarItemAccess({
<Label text='Доступ' className='self-center select-none' />
<div className='ml-auto cc-icons'>
<SelectAccessPolicy
disabled={role <= UserRole.EDITOR || isProcessing || isAttachedToOSS}
value={policy}
onChange={handleSetAccessPolicy}
disabled={role <= UserRole.EDITOR || isProcessing || isAttachedToOSS}
/>
<MiniButton
title={visible ? 'Библиотека: отображать' : 'Библиотека: скрывать'}
aria-label='Переключатель отображения библиотеки'
icon={<IconItemVisibility value={visible} />}
onClick={toggleVisible}
disabled={role === UserRole.READER || isProcessing}
@ -62,6 +63,7 @@ export function ToolbarItemAccess({
<MiniButton
title={readOnly ? 'Изменение: запрещено' : 'Изменение: разрешено'}
aria-label='Переключатель режима изменения'
icon={
readOnly ? (
<IconImmutable size='1.25rem' className='text-sec-600' />

View File

@ -56,13 +56,15 @@ export function ToolbarItemCard({ className, schema, onSubmit, isMutable, delete
{isMutable || isModified ? (
<MiniButton
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
disabled={!canSave}
aria-label='Сохранить изменения'
icon={<IconSave size='1.25rem' className='icon-primary' />}
onClick={onSubmit}
disabled={!canSave}
/>
) : null}
<MiniButton
titleHtml={tooltipText.shareItem(schema.access_policy === AccessPolicy.PUBLIC)}
aria-label='Поделиться схемой'
icon={<IconShare size='1.25rem' className='icon-primary' />}
onClick={sharePage}
disabled={schema.access_policy !== AccessPolicy.PUBLIC}
@ -71,8 +73,8 @@ export function ToolbarItemCard({ className, schema, onSubmit, isMutable, delete
<MiniButton
title='Удалить схему'
icon={<IconDestroy size='1.25rem' className='icon-red' />}
disabled={!isMutable || isProcessing || role < UserRole.OWNER}
onClick={deleteSchema}
disabled={!isMutable || isProcessing || role < UserRole.OWNER}
/>
) : null}
<BadgeHelp topic={HelpTopic.UI_RS_CARD} offset={4} />

View File

@ -43,7 +43,7 @@ export function DlgCloneLibraryItem() {
item_type: base.item_type,
title: cloneTitle(base),
alias: base.alias,
comment: base.comment,
description: base.description,
visible: true,
read_only: false,
access_policy: AccessPolicy.PUBLIC,
@ -68,7 +68,7 @@ export function DlgCloneLibraryItem() {
>
<TextInput
id='dlg_full_name' //
label='Полное название'
label='Название'
{...register('title')}
error={errors.title}
/>
@ -95,6 +95,7 @@ export function DlgCloneLibraryItem() {
render={({ field }) => (
<MiniButton
title={field.value ? 'Библиотека: отображать' : 'Библиотека: скрывать'}
aria-label='Переключатель отображения библиотеки'
icon={<IconItemVisibility value={field.value} />}
onClick={() => field.onChange(!field.value)}
/>
@ -108,11 +109,17 @@ export function DlgCloneLibraryItem() {
control={control}
name='location'
render={({ field }) => (
<PickLocation value={field.value} rows={2} onChange={field.onChange} error={errors.location} />
<PickLocation
value={field.value} //
rows={2}
onChange={field.onChange}
className={!!errors.location ? '-mb-6' : undefined}
error={errors.location}
/>
)}
/>
<TextArea id='dlg_comment' {...register('comment')} label='Описание' rows={4} error={errors.comment} />
<TextArea id='dlg_comment' {...register('description')} label='Описание' rows={4} error={errors.description} />
{selected.length > 0 ? (
<Controller

View File

@ -47,12 +47,12 @@ export function DlgEditEditors() {
<div className='self-center text-sm font-semibold'>
<span>Всего редакторов [{selected.length}]</span>
<MiniButton
noHover
title='Очистить список'
noHover
className='py-0 align-middle'
icon={<IconRemove size='1.5rem' className='icon-red' />}
disabled={selected.length === 0}
onClick={() => setSelected([])}
disabled={selected.length === 0}
/>
</div>

View File

@ -107,14 +107,15 @@ export function DlgEditVersions() {
<MiniButton
type='submit'
title={isValid ? 'Сохранить изменения' : errorMsg.versionTaken}
disabled={!isDirty || !isValid || isProcessing}
aria-label='Сохранить изменения'
icon={<IconSave size='1.25rem' className='icon-primary' />}
disabled={!isDirty || !isValid || isProcessing}
/>
<MiniButton
title='Сбросить несохраненные изменения'
disabled={!isDirty}
onClick={() => reset()}
icon={<IconReset size='1.25rem' className='icon-primary' />}
disabled={!isDirty}
/>
</div>
</form>

View File

@ -66,9 +66,9 @@ export function TableVersions({ processing, items, onDelete, selected, onSelect
className='align-middle'
noHover
noPadding
disabled={processing}
icon={<IconRemove size='1.25rem' className='icon-red' />}
onClick={event => handleDeleteVersion(event, props.row.original.id)}
disabled={processing}
/>
)
})

View File

@ -40,13 +40,6 @@ export function labelFolderNode(node: FolderNode): string {
}
}
/**
* Retrieves description for {@link FolderNode}.
*/
export function describeFolderNode(node: FolderNode): string {
return `${node.filesInside} | ${node.filesTotal}`;
}
/**
* Retrieves label for {@link AccessPolicy}.
*/

View File

@ -9,7 +9,7 @@ describe('Testing matching LibraryItem', () => {
item_type: LibraryItemType.RSFORM,
title: 'Item1',
alias: 'I1',
comment: 'comment',
description: 'description',
time_create: 'I2',
time_update: '',
owner: null,
@ -24,7 +24,7 @@ describe('Testing matching LibraryItem', () => {
item_type: LibraryItemType.RSFORM,
title: '',
alias: '',
comment: '',
description: '',
time_create: '',
time_update: '',
owner: null,
@ -46,7 +46,7 @@ describe('Testing matching LibraryItem', () => {
expect(matchLibraryItem(item1, item1.title + '@invalid')).toEqual(false);
expect(matchLibraryItem(item1, item1.alias + '@invalid')).toEqual(false);
expect(matchLibraryItem(item1, item1.time_create)).toEqual(false);
expect(matchLibraryItem(item1, item1.comment)).toEqual(true);
expect(matchLibraryItem(item1, item1.description)).toEqual(true);
});
});

View File

@ -17,7 +17,7 @@ const LOCATION_REGEXP = /^\/[PLUS]((\/[!\d\p{L}]([!\d\p{L}\- ]*[!\d\p{L}])?)*)?$
*/
export function matchLibraryItem(target: ILibraryItem, query: string): boolean {
const matcher = new TextMatcher(query);
return matcher.test(target.alias) || matcher.test(target.title) || matcher.test(target.comment);
return matcher.test(target.alias) || matcher.test(target.title) || matcher.test(target.description);
}
/**

View File

@ -5,12 +5,13 @@
/**
* Represents valid location headers.
*/
export enum LocationHead {
USER = '/U',
COMMON = '/S',
PROJECTS = '/P',
LIBRARY = '/L'
}
export const LocationHead = {
USER: '/U',
COMMON: '/S',
PROJECTS: '/P',
LIBRARY: '/L'
} as const;
export type LocationHead = (typeof LocationHead)[keyof typeof LocationHead];
export const BASIC_SCHEMAS = '/L/Базовые';

Some files were not shown because too many files have changed in this diff Show More