Compare commits
27 Commits
65c210b047
...
e8509e44b1
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e8509e44b1 | ||
![]() |
417583efa5 | ||
![]() |
03fc03a108 | ||
![]() |
419b5e6379 | ||
![]() |
1cd780504d | ||
![]() |
09b56f49d8 | ||
![]() |
39783ec74a | ||
![]() |
7adbaed116 | ||
![]() |
341db80e68 | ||
![]() |
1365d27af8 | ||
![]() |
cda3b70227 | ||
![]() |
ebc6740e35 | ||
![]() |
9bfdc56789 | ||
![]() |
f92e086b13 | ||
![]() |
9f64282385 | ||
![]() |
f21295061d | ||
![]() |
76902b34ae | ||
![]() |
ff744b5367 | ||
![]() |
6c13a9e774 | ||
![]() |
cb6664a606 | ||
![]() |
ec40fe04ac | ||
![]() |
c341360e90 | ||
![]() |
eb48014e2f | ||
![]() |
0781dad1cb | ||
![]() |
8bf829513f | ||
![]() |
5f524e2e6b | ||
![]() |
ea8c86119c |
13
.github/workflows/backend.yml
vendored
13
.github/workflows/backend.yml
vendored
|
@ -40,3 +40,16 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
python manage.py check
|
python manage.py check
|
||||||
python manage.py test
|
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 }}"
|
||||||
|
|
13
.github/workflows/frontend.yml
vendored
13
.github/workflows/frontend.yml
vendored
|
@ -49,3 +49,16 @@ jobs:
|
||||||
name: playwright-report
|
name: playwright-report
|
||||||
path: playwright-report/
|
path: playwright-report/
|
||||||
retention-days: 30
|
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 }}"
|
||||||
|
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -21,6 +21,7 @@
|
||||||
"changeProcessCWD": true
|
"changeProcessCWD": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"stylelint.enable": true,
|
||||||
"autopep8.args": [
|
"autopep8.args": [
|
||||||
"--max-line-length",
|
"--max-line-length",
|
||||||
"120",
|
"120",
|
||||||
|
@ -151,6 +152,7 @@
|
||||||
"rsconcept",
|
"rsconcept",
|
||||||
"rsedit",
|
"rsedit",
|
||||||
"rseditor",
|
"rseditor",
|
||||||
|
"rsexpression",
|
||||||
"rsform",
|
"rsform",
|
||||||
"rsforms",
|
"rsforms",
|
||||||
"rsgraph",
|
"rsgraph",
|
||||||
|
|
|
@ -71,6 +71,10 @@ This readme file is used mostly to document project dependencies and conventions
|
||||||
- vite
|
- vite
|
||||||
- jest
|
- jest
|
||||||
- ts-jest
|
- ts-jest
|
||||||
|
- stylelint
|
||||||
|
- stylelint-config-recommended
|
||||||
|
- stylelint-config-standard
|
||||||
|
- stylelint-config-tailwindcss
|
||||||
- @vitejs/plugin-react
|
- @vitejs/plugin-react
|
||||||
- @types/jest
|
- @types/jest
|
||||||
- @lezer/generator
|
- @lezer/generator
|
||||||
|
|
|
@ -75,4 +75,10 @@ server {
|
||||||
proxy_pass http://innerreact;
|
proxy_pass http://innerreact;
|
||||||
proxy_redirect default;
|
proxy_redirect default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /assets/ {
|
||||||
|
proxy_pass http://innerreact/assets/;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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='Описание'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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',
|
||||||
|
),
|
||||||
|
]
|
|
@ -69,8 +69,8 @@ class LibraryItem(Model):
|
||||||
max_length=255,
|
max_length=255,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
comment = TextField(
|
description = TextField(
|
||||||
verbose_name='Комментарий',
|
verbose_name='Описание',
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
visible = BooleanField(
|
visible = BooleanField(
|
||||||
|
|
|
@ -46,7 +46,7 @@ class TestLibraryItem(TestCase):
|
||||||
self.assertIsNone(item.owner)
|
self.assertIsNone(item.owner)
|
||||||
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.description, '')
|
||||||
self.assertEqual(item.visible, True)
|
self.assertEqual(item.visible, True)
|
||||||
self.assertEqual(item.read_only, False)
|
self.assertEqual(item.read_only, False)
|
||||||
self.assertEqual(item.access_policy, AccessPolicy.PUBLIC)
|
self.assertEqual(item.access_policy, AccessPolicy.PUBLIC)
|
||||||
|
@ -59,13 +59,13 @@ class TestLibraryItem(TestCase):
|
||||||
title='Test',
|
title='Test',
|
||||||
owner=self.user1,
|
owner=self.user1,
|
||||||
alias='KS1',
|
alias='KS1',
|
||||||
comment='Test comment',
|
description='Test description',
|
||||||
location=LocationHead.COMMON
|
location=LocationHead.COMMON
|
||||||
)
|
)
|
||||||
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.description, 'Test description')
|
||||||
self.assertEqual(item.location, LocationHead.COMMON)
|
self.assertEqual(item.location, LocationHead.COMMON)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -55,13 +55,13 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
||||||
if operation.title != instance.title:
|
if operation.title != instance.title:
|
||||||
operation.title = instance.title
|
operation.title = instance.title
|
||||||
changed = True
|
changed = True
|
||||||
if operation.comment != instance.comment:
|
if operation.description != instance.description:
|
||||||
operation.comment = instance.comment
|
operation.description = instance.description
|
||||||
changed = True
|
changed = True
|
||||||
if changed:
|
if changed:
|
||||||
update_list.append(operation)
|
update_list.append(operation)
|
||||||
if update_list:
|
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:
|
def perform_destroy(self, instance: m.LibraryItem) -> None:
|
||||||
if instance.item_type == m.LibraryItemType.RSFORM:
|
if instance.item_type == m.LibraryItemType.RSFORM:
|
||||||
|
@ -160,7 +160,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
||||||
clone.owner = cast(User, self.request.user)
|
clone.owner = cast(User, self.request.user)
|
||||||
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.description = serializer.validated_data.get('description', '')
|
||||||
clone.visible = serializer.validated_data.get('visible', True)
|
clone.visible = serializer.validated_data.get('visible', True)
|
||||||
clone.read_only = False
|
clone.read_only = False
|
||||||
clone.access_policy = serializer.validated_data.get('access_policy', m.AccessPolicy.PUBLIC)
|
clone.access_policy = serializer.validated_data.get('access_policy', m.AccessPolicy.PUBLIC)
|
||||||
|
@ -168,7 +168,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
clone.save()
|
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():
|
for cst in 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']:
|
||||||
cst.pk = None
|
cst.pk = None
|
||||||
|
|
|
@ -7,7 +7,16 @@ from . import models
|
||||||
class OperationAdmin(admin.ModelAdmin):
|
class OperationAdmin(admin.ModelAdmin):
|
||||||
''' Admin model: Operation. '''
|
''' Admin model: Operation. '''
|
||||||
ordering = ['oss']
|
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']
|
search_fields = ['id', 'operation_type', 'title', 'alias']
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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='Описание'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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',
|
||||||
|
),
|
||||||
|
]
|
|
@ -53,8 +53,8 @@ class Operation(Model):
|
||||||
verbose_name='Название',
|
verbose_name='Название',
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
comment = TextField(
|
description = TextField(
|
||||||
verbose_name='Комментарий',
|
verbose_name='Описание',
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -141,8 +141,8 @@ class OperationSchema:
|
||||||
if schema is not None:
|
if schema is not None:
|
||||||
operation.alias = schema.alias
|
operation.alias = schema.alias
|
||||||
operation.title = schema.title
|
operation.title = schema.title
|
||||||
operation.comment = schema.comment
|
operation.description = schema.description
|
||||||
operation.save(update_fields=['result', 'alias', 'title', 'comment'])
|
operation.save(update_fields=['result', 'alias', 'title', 'description'])
|
||||||
|
|
||||||
if schema is not None and has_children:
|
if schema is not None and has_children:
|
||||||
rsform = RSForm(schema)
|
rsform = RSForm(schema)
|
||||||
|
@ -227,7 +227,7 @@ class OperationSchema:
|
||||||
owner=self.model.owner,
|
owner=self.model.owner,
|
||||||
alias=operation.alias,
|
alias=operation.alias,
|
||||||
title=operation.title,
|
title=operation.title,
|
||||||
comment=operation.comment,
|
description=operation.description,
|
||||||
visible=False,
|
visible=False,
|
||||||
access_policy=self.model.access_policy,
|
access_policy=self.model.access_policy,
|
||||||
location=self.model.location
|
location=self.model.location
|
||||||
|
|
|
@ -44,7 +44,7 @@ class OperationCreateSerializer(serializers.Serializer):
|
||||||
model = Operation
|
model = Operation
|
||||||
fields = \
|
fields = \
|
||||||
'alias', 'operation_type', 'title', \
|
'alias', 'operation_type', 'title', \
|
||||||
'comment', 'result', 'position_x', 'position_y'
|
'description', 'result', 'position_x', 'position_y'
|
||||||
|
|
||||||
create_schema = serializers.BooleanField(default=False, required=False)
|
create_schema = serializers.BooleanField(default=False, required=False)
|
||||||
item_data = OperationCreateData()
|
item_data = OperationCreateData()
|
||||||
|
@ -63,7 +63,7 @@ class OperationUpdateSerializer(serializers.Serializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
''' serializer metadata. '''
|
''' serializer metadata. '''
|
||||||
model = Operation
|
model = Operation
|
||||||
fields = 'alias', 'title', 'comment'
|
fields = 'alias', 'title', 'description'
|
||||||
|
|
||||||
target = PKField(many=False, queryset=Operation.objects.all())
|
target = PKField(many=False, queryset=Operation.objects.all())
|
||||||
item_data = OperationUpdateData()
|
item_data = OperationUpdateData()
|
||||||
|
|
|
@ -28,6 +28,6 @@ class TestOperation(TestCase):
|
||||||
self.assertEqual(self.operation.result, None)
|
self.assertEqual(self.operation.result, None)
|
||||||
self.assertEqual(self.operation.alias, 'KS1')
|
self.assertEqual(self.operation.alias, 'KS1')
|
||||||
self.assertEqual(self.operation.title, '')
|
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_x, 0)
|
||||||
self.assertEqual(self.operation.position_y, 0)
|
self.assertEqual(self.operation.position_y, 0)
|
||||||
|
|
|
@ -123,7 +123,7 @@ class TestChangeAttributes(EndpointTester):
|
||||||
|
|
||||||
@decl_endpoint('/api/library/{item}', method='patch')
|
@decl_endpoint('/api/library/{item}', method='patch')
|
||||||
def test_sync_from_result(self):
|
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.executeOK(data=data, item=self.ks1.model.pk)
|
||||||
self.operation1.refresh_from_db()
|
self.operation1.refresh_from_db()
|
||||||
|
@ -131,7 +131,7 @@ class TestChangeAttributes(EndpointTester):
|
||||||
self.assertEqual(self.operation1.result, self.ks1.model)
|
self.assertEqual(self.operation1.result, self.ks1.model)
|
||||||
self.assertEqual(self.operation1.alias, data['alias'])
|
self.assertEqual(self.operation1.alias, data['alias'])
|
||||||
self.assertEqual(self.operation1.title, data['title'])
|
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')
|
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
|
||||||
def test_sync_from_operation(self):
|
def test_sync_from_operation(self):
|
||||||
|
@ -140,7 +140,7 @@ class TestChangeAttributes(EndpointTester):
|
||||||
'item_data': {
|
'item_data': {
|
||||||
'alias': 'Test3 mod',
|
'alias': 'Test3 mod',
|
||||||
'title': 'Test title mod',
|
'title': 'Test title mod',
|
||||||
'comment': 'Comment mod'
|
'description': 'Comment mod'
|
||||||
},
|
},
|
||||||
'positions': [],
|
'positions': [],
|
||||||
}
|
}
|
||||||
|
@ -149,7 +149,7 @@ class TestChangeAttributes(EndpointTester):
|
||||||
self.ks3.refresh_from_db()
|
self.ks3.refresh_from_db()
|
||||||
self.assertEqual(self.ks3.model.alias, data['item_data']['alias'])
|
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.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')
|
@decl_endpoint('/api/library/{item}', method='delete')
|
||||||
def test_destroy_oss_consequence(self):
|
def test_destroy_oss_consequence(self):
|
||||||
|
|
|
@ -281,7 +281,7 @@ class TestChangeOperations(EndpointTester):
|
||||||
'item_data': {
|
'item_data': {
|
||||||
'alias': 'Test4 mod',
|
'alias': 'Test4 mod',
|
||||||
'title': 'Test title mod',
|
'title': 'Test title mod',
|
||||||
'comment': 'Comment mod'
|
'description': 'Comment mod'
|
||||||
},
|
},
|
||||||
'positions': [],
|
'positions': [],
|
||||||
'substitutions': [
|
'substitutions': [
|
||||||
|
@ -315,7 +315,7 @@ class TestChangeOperations(EndpointTester):
|
||||||
'item_data': {
|
'item_data': {
|
||||||
'alias': 'Test4 mod',
|
'alias': 'Test4 mod',
|
||||||
'title': 'Test title mod',
|
'title': 'Test title mod',
|
||||||
'comment': 'Comment mod'
|
'description': 'Comment mod'
|
||||||
},
|
},
|
||||||
'positions': [],
|
'positions': [],
|
||||||
'arguments': [self.operation1.pk],
|
'arguments': [self.operation1.pk],
|
||||||
|
|
|
@ -143,7 +143,7 @@ class TestOssViewset(EndpointTester):
|
||||||
'item_data': {
|
'item_data': {
|
||||||
'alias': 'Test3',
|
'alias': 'Test3',
|
||||||
'title': 'Test title',
|
'title': 'Test title',
|
||||||
'comment': 'Тест кириллицы',
|
'description': 'Тест кириллицы',
|
||||||
'position_x': 1,
|
'position_x': 1,
|
||||||
'position_y': 1,
|
'position_y': 1,
|
||||||
},
|
},
|
||||||
|
@ -165,7 +165,7 @@ class TestOssViewset(EndpointTester):
|
||||||
self.assertEqual(new_operation['alias'], data['item_data']['alias'])
|
self.assertEqual(new_operation['alias'], data['item_data']['alias'])
|
||||||
self.assertEqual(new_operation['operation_type'], data['item_data']['operation_type'])
|
self.assertEqual(new_operation['operation_type'], data['item_data']['operation_type'])
|
||||||
self.assertEqual(new_operation['title'], data['item_data']['title'])
|
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_x'], data['item_data']['position_x'])
|
||||||
self.assertEqual(new_operation['position_y'], data['item_data']['position_y'])
|
self.assertEqual(new_operation['position_y'], data['item_data']['position_y'])
|
||||||
self.assertEqual(new_operation['result'], None)
|
self.assertEqual(new_operation['result'], None)
|
||||||
|
@ -223,7 +223,7 @@ class TestOssViewset(EndpointTester):
|
||||||
'item_data': {
|
'item_data': {
|
||||||
'alias': 'Test4',
|
'alias': 'Test4',
|
||||||
'title': 'Test title',
|
'title': 'Test title',
|
||||||
'comment': 'Comment',
|
'description': 'Comment',
|
||||||
'operation_type': OperationType.INPUT,
|
'operation_type': OperationType.INPUT,
|
||||||
'result': self.ks1.model.pk
|
'result': self.ks1.model.pk
|
||||||
},
|
},
|
||||||
|
@ -238,7 +238,7 @@ class TestOssViewset(EndpointTester):
|
||||||
schema = LibraryItem.objects.get(pk=new_operation['result'])
|
schema = LibraryItem.objects.get(pk=new_operation['result'])
|
||||||
self.assertEqual(schema.alias, data['item_data']['alias'])
|
self.assertEqual(schema.alias, data['item_data']['alias'])
|
||||||
self.assertEqual(schema.title, data['item_data']['title'])
|
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.visible, False)
|
||||||
self.assertEqual(schema.access_policy, self.owned.model.access_policy)
|
self.assertEqual(schema.access_policy, self.owned.model.access_policy)
|
||||||
self.assertEqual(schema.location, self.owned.model.location)
|
self.assertEqual(schema.location, self.owned.model.location)
|
||||||
|
@ -286,7 +286,7 @@ class TestOssViewset(EndpointTester):
|
||||||
self.executeBadData(data=data, item=self.owned_id)
|
self.executeBadData(data=data, item=self.owned_id)
|
||||||
|
|
||||||
self.operation1.result = None
|
self.operation1.result = None
|
||||||
self.operation1.comment = 'TestComment'
|
self.operation1.description = 'TestComment'
|
||||||
self.operation1.title = 'TestTitle'
|
self.operation1.title = 'TestTitle'
|
||||||
self.operation1.save()
|
self.operation1.save()
|
||||||
response = self.executeOK(data=data)
|
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['id'], self.operation1.result.pk)
|
||||||
self.assertEqual(new_schema['alias'], self.operation1.alias)
|
self.assertEqual(new_schema['alias'], self.operation1.alias)
|
||||||
self.assertEqual(new_schema['title'], self.operation1.title)
|
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
|
data['target'] = self.operation3.pk
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data=data)
|
||||||
|
@ -326,14 +326,14 @@ class TestOssViewset(EndpointTester):
|
||||||
data['input'] = self.ks1.model.pk
|
data['input'] = self.ks1.model.pk
|
||||||
self.ks1.model.alias = 'Test42'
|
self.ks1.model.alias = 'Test42'
|
||||||
self.ks1.model.title = 'Test421'
|
self.ks1.model.title = 'Test421'
|
||||||
self.ks1.model.comment = 'TestComment42'
|
self.ks1.model.description = 'TestComment42'
|
||||||
self.ks1.save()
|
self.ks1.save()
|
||||||
response = self.executeOK(data=data)
|
response = self.executeOK(data=data)
|
||||||
self.operation1.refresh_from_db()
|
self.operation1.refresh_from_db()
|
||||||
self.assertEqual(self.operation1.result, self.ks1.model)
|
self.assertEqual(self.operation1.result, self.ks1.model)
|
||||||
self.assertEqual(self.operation1.alias, self.ks1.model.alias)
|
self.assertEqual(self.operation1.alias, self.ks1.model.alias)
|
||||||
self.assertEqual(self.operation1.title, self.ks1.model.title)
|
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')
|
@decl_endpoint('/api/oss/{item}/set-input', method='patch')
|
||||||
def test_set_input_change_schema(self):
|
def test_set_input_change_schema(self):
|
||||||
|
@ -382,7 +382,7 @@ class TestOssViewset(EndpointTester):
|
||||||
'item_data': {
|
'item_data': {
|
||||||
'alias': 'Test3 mod',
|
'alias': 'Test3 mod',
|
||||||
'title': 'Test title mod',
|
'title': 'Test title mod',
|
||||||
'comment': 'Comment mod'
|
'description': 'Comment mod'
|
||||||
},
|
},
|
||||||
'positions': [],
|
'positions': [],
|
||||||
'arguments': [self.operation2.pk, self.operation1.pk],
|
'arguments': [self.operation2.pk, self.operation1.pk],
|
||||||
|
@ -406,7 +406,7 @@ class TestOssViewset(EndpointTester):
|
||||||
self.operation3.refresh_from_db()
|
self.operation3.refresh_from_db()
|
||||||
self.assertEqual(self.operation3.alias, data['item_data']['alias'])
|
self.assertEqual(self.operation3.alias, data['item_data']['alias'])
|
||||||
self.assertEqual(self.operation3.title, data['item_data']['title'])
|
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')
|
args = self.operation3.getQ_arguments().order_by('order')
|
||||||
self.assertEqual(args[0].argument.pk, data['arguments'][0])
|
self.assertEqual(args[0].argument.pk, data['arguments'][0])
|
||||||
self.assertEqual(args[0].order, 0)
|
self.assertEqual(args[0].order, 0)
|
||||||
|
@ -426,7 +426,7 @@ class TestOssViewset(EndpointTester):
|
||||||
'item_data': {
|
'item_data': {
|
||||||
'alias': 'Test3 mod',
|
'alias': 'Test3 mod',
|
||||||
'title': 'Test title mod',
|
'title': 'Test title mod',
|
||||||
'comment': 'Comment mod'
|
'description': 'Comment mod'
|
||||||
},
|
},
|
||||||
'positions': [],
|
'positions': [],
|
||||||
}
|
}
|
||||||
|
@ -435,10 +435,10 @@ class TestOssViewset(EndpointTester):
|
||||||
self.operation1.refresh_from_db()
|
self.operation1.refresh_from_db()
|
||||||
self.assertEqual(self.operation1.alias, data['item_data']['alias'])
|
self.assertEqual(self.operation1.alias, data['item_data']['alias'])
|
||||||
self.assertEqual(self.operation1.title, data['item_data']['title'])
|
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.alias, data['item_data']['alias'])
|
||||||
self.assertEqual(self.operation1.result.title, data['item_data']['title'])
|
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')
|
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
|
||||||
def test_update_operation_invalid_substitution(self):
|
def test_update_operation_invalid_substitution(self):
|
||||||
|
@ -451,7 +451,7 @@ class TestOssViewset(EndpointTester):
|
||||||
'item_data': {
|
'item_data': {
|
||||||
'alias': 'Test3 mod',
|
'alias': 'Test3 mod',
|
||||||
'title': 'Test title mod',
|
'title': 'Test title mod',
|
||||||
'comment': 'Comment mod'
|
'description': 'Comment mod'
|
||||||
},
|
},
|
||||||
'positions': [],
|
'positions': [],
|
||||||
'arguments': [self.operation1.pk, self.operation2.pk],
|
'arguments': [self.operation1.pk, self.operation2.pk],
|
||||||
|
@ -490,7 +490,7 @@ class TestOssViewset(EndpointTester):
|
||||||
self.operation3.refresh_from_db()
|
self.operation3.refresh_from_db()
|
||||||
schema = self.operation3.result
|
schema = self.operation3.result
|
||||||
self.assertEqual(schema.alias, self.operation3.alias)
|
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.title, self.operation3.title)
|
||||||
self.assertEqual(schema.visible, False)
|
self.assertEqual(schema.visible, False)
|
||||||
items = list(RSForm(schema).constituents())
|
items = list(RSForm(schema).constituents())
|
||||||
|
|
|
@ -200,9 +200,9 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
|
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({
|
raise serializers.ValidationError({
|
||||||
'target': msg.operationNotInput(operation.alias)
|
'target': msg.operationHasArguments(operation.alias)
|
||||||
})
|
})
|
||||||
if operation.result is not None:
|
if operation.result is not None:
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
|
@ -295,15 +295,15 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
oss.update_positions(serializer.validated_data['positions'])
|
oss.update_positions(serializer.validated_data['positions'])
|
||||||
operation.alias = serializer.validated_data['item_data']['alias']
|
operation.alias = serializer.validated_data['item_data']['alias']
|
||||||
operation.title = serializer.validated_data['item_data']['title']
|
operation.title = serializer.validated_data['item_data']['title']
|
||||||
operation.comment = serializer.validated_data['item_data']['comment']
|
operation.description = serializer.validated_data['item_data']['description']
|
||||||
operation.save(update_fields=['alias', 'title', 'comment'])
|
operation.save(update_fields=['alias', 'title', 'description'])
|
||||||
|
|
||||||
if operation.result is not None:
|
if operation.result is not None:
|
||||||
can_edit = permissions.can_edit_item(request.user, operation.result)
|
can_edit = permissions.can_edit_item(request.user, operation.result)
|
||||||
if can_edit or operation.operation_type == m.OperationType.SYNTHESIS:
|
if can_edit or operation.operation_type == m.OperationType.SYNTHESIS:
|
||||||
operation.result.alias = operation.alias
|
operation.result.alias = operation.alias
|
||||||
operation.result.title = operation.title
|
operation.result.title = operation.title
|
||||||
operation.result.comment = operation.comment
|
operation.result.description = operation.description
|
||||||
operation.result.save()
|
operation.result.save()
|
||||||
if 'arguments' in serializer.validated_data:
|
if 'arguments' in serializer.validated_data:
|
||||||
oss.set_arguments(operation.pk, serializer.validated_data['arguments'])
|
oss.set_arguments(operation.pk, serializer.validated_data['arguments'])
|
||||||
|
|
|
@ -42,7 +42,7 @@ class RSFormTRSSerializer(serializers.Serializer):
|
||||||
'type': _TRS_TYPE,
|
'type': _TRS_TYPE,
|
||||||
'title': schema.title,
|
'title': schema.title,
|
||||||
'alias': schema.alias,
|
'alias': schema.alias,
|
||||||
'comment': schema.comment,
|
'comment': schema.description,
|
||||||
'items': [],
|
'items': [],
|
||||||
'claimed': False,
|
'claimed': False,
|
||||||
'selection': [],
|
'selection': [],
|
||||||
|
@ -78,7 +78,7 @@ class RSFormTRSSerializer(serializers.Serializer):
|
||||||
'type': _TRS_TYPE,
|
'type': _TRS_TYPE,
|
||||||
'title': data['title'],
|
'title': data['title'],
|
||||||
'alias': data['alias'],
|
'alias': data['alias'],
|
||||||
'comment': data['comment'],
|
'comment': data['description'],
|
||||||
'items': [],
|
'items': [],
|
||||||
'claimed': False,
|
'claimed': False,
|
||||||
'selection': [],
|
'selection': [],
|
||||||
|
@ -123,7 +123,7 @@ class RSFormTRSSerializer(serializers.Serializer):
|
||||||
if self.context['load_meta']:
|
if self.context['load_meta']:
|
||||||
result['title'] = data.get('title', 'Без названия')
|
result['title'] = data.get('title', 'Без названия')
|
||||||
result['alias'] = data.get('alias', '')
|
result['alias'] = data.get('alias', '')
|
||||||
result['comment'] = data.get('comment', '')
|
result['description'] = data.get('description', '')
|
||||||
if 'id' in data:
|
if 'id' in data:
|
||||||
result['id'] = data['id']
|
result['id'] = data['id']
|
||||||
self.instance = RSForm.from_id(result['id'])
|
self.instance = RSForm.from_id(result['id'])
|
||||||
|
@ -144,7 +144,7 @@ class RSFormTRSSerializer(serializers.Serializer):
|
||||||
owner=validated_data.get('owner', None),
|
owner=validated_data.get('owner', None),
|
||||||
alias=validated_data['alias'],
|
alias=validated_data['alias'],
|
||||||
title=validated_data['title'],
|
title=validated_data['title'],
|
||||||
comment=validated_data['comment'],
|
description=validated_data['description'],
|
||||||
visible=validated_data['visible'],
|
visible=validated_data['visible'],
|
||||||
read_only=validated_data['read_only'],
|
read_only=validated_data['read_only'],
|
||||||
access_policy=validated_data['access_policy'],
|
access_policy=validated_data['access_policy'],
|
||||||
|
@ -171,8 +171,8 @@ class RSFormTRSSerializer(serializers.Serializer):
|
||||||
instance.model.alias = validated_data['alias']
|
instance.model.alias = validated_data['alias']
|
||||||
if 'title' in validated_data:
|
if 'title' in validated_data:
|
||||||
instance.model.title = validated_data['title']
|
instance.model.title = validated_data['title']
|
||||||
if 'comment' in validated_data:
|
if 'description' in validated_data:
|
||||||
instance.model.comment = validated_data['comment']
|
instance.model.description = validated_data['description']
|
||||||
|
|
||||||
order = 0
|
order = 0
|
||||||
prev_constituents = instance.constituents()
|
prev_constituents = instance.constituents()
|
||||||
|
|
|
@ -30,7 +30,7 @@ class TestRSFormViewset(EndpointTester):
|
||||||
work_dir = os.path.dirname(os.path.abspath(__file__))
|
work_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
data = {
|
data = {
|
||||||
'title': 'Test123',
|
'title': 'Test123',
|
||||||
'comment': '123',
|
'description': '123',
|
||||||
'alias': 'ks1',
|
'alias': 'ks1',
|
||||||
'location': LocationHead.PROJECTS,
|
'location': LocationHead.PROJECTS,
|
||||||
'access_policy': AccessPolicy.PROTECTED,
|
'access_policy': AccessPolicy.PROTECTED,
|
||||||
|
@ -45,7 +45,7 @@ class TestRSFormViewset(EndpointTester):
|
||||||
self.assertEqual(response.data['owner'], self.user.pk)
|
self.assertEqual(response.data['owner'], self.user.pk)
|
||||||
self.assertEqual(response.data['title'], data['title'])
|
self.assertEqual(response.data['title'], data['title'])
|
||||||
self.assertEqual(response.data['alias'], data['alias'])
|
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')
|
@decl_endpoint('/api/rsforms', method='get')
|
||||||
|
|
|
@ -586,8 +586,8 @@ def _prepare_rsform_data(data: dict, request: Request, owner: Union[User, None])
|
||||||
data['title'] = 'Без названия ' + request.FILES['file'].fileName
|
data['title'] = 'Без названия ' + request.FILES['file'].fileName
|
||||||
if 'alias' in request.data and request.data['alias'] != '':
|
if 'alias' in request.data and request.data['alias'] != '':
|
||||||
data['alias'] = request.data['alias']
|
data['alias'] = request.data['alias']
|
||||||
if 'comment' in request.data and request.data['comment'] != '':
|
if 'description' in request.data and request.data['description'] != '':
|
||||||
data['comment'] = request.data['comment']
|
data['description'] = request.data['description']
|
||||||
|
|
||||||
visible = True
|
visible = True
|
||||||
if 'visible' in request.data:
|
if 'visible' in request.data:
|
||||||
|
|
|
@ -1 +1,27 @@
|
||||||
''' Admin: User profile and Authorization. '''
|
''' 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)
|
||||||
|
|
|
@ -88,7 +88,7 @@
|
||||||
"owner": 1,
|
"owner": 1,
|
||||||
"title": "Банк выражений",
|
"title": "Банк выражений",
|
||||||
"alias": "БВ",
|
"alias": "БВ",
|
||||||
"comment": "Банк шаблонов для генерации выражений",
|
"description": "Банк шаблонов для генерации выражений",
|
||||||
"visible": true,
|
"visible": true,
|
||||||
"read_only": false,
|
"read_only": false,
|
||||||
"access_policy": "public",
|
"access_policy": "public",
|
||||||
|
@ -105,7 +105,7 @@
|
||||||
"owner": 5,
|
"owner": 5,
|
||||||
"title": "Групповая операция",
|
"title": "Групповая операция",
|
||||||
"alias": "БК09",
|
"alias": "БК09",
|
||||||
"comment": "",
|
"description": "",
|
||||||
"visible": true,
|
"visible": true,
|
||||||
"read_only": false,
|
"read_only": false,
|
||||||
"access_policy": "public",
|
"access_policy": "public",
|
||||||
|
@ -122,7 +122,7 @@
|
||||||
"owner": 3,
|
"owner": 3,
|
||||||
"title": "Булева алгебра",
|
"title": "Булева алгебра",
|
||||||
"alias": "БК12",
|
"alias": "БК12",
|
||||||
"comment": "",
|
"description": "",
|
||||||
"visible": true,
|
"visible": true,
|
||||||
"read_only": false,
|
"read_only": false,
|
||||||
"access_policy": "public",
|
"access_policy": "public",
|
||||||
|
@ -139,7 +139,7 @@
|
||||||
"owner": 3,
|
"owner": 3,
|
||||||
"title": "Генеалогия",
|
"title": "Генеалогия",
|
||||||
"alias": "D0001",
|
"alias": "D0001",
|
||||||
"comment": "построено на основе понятия \"родство\" из Википедии",
|
"description": "построено на основе понятия \"родство\" из Википедии",
|
||||||
"visible": true,
|
"visible": true,
|
||||||
"read_only": false,
|
"read_only": false,
|
||||||
"access_policy": "public",
|
"access_policy": "public",
|
||||||
|
@ -156,7 +156,7 @@
|
||||||
"owner": 1,
|
"owner": 1,
|
||||||
"title": "Вещества и смеси",
|
"title": "Вещества и смеси",
|
||||||
"alias": "КС Вещества",
|
"alias": "КС Вещества",
|
||||||
"comment": "",
|
"description": "",
|
||||||
"visible": false,
|
"visible": false,
|
||||||
"read_only": false,
|
"read_only": false,
|
||||||
"access_policy": "public",
|
"access_policy": "public",
|
||||||
|
@ -173,7 +173,7 @@
|
||||||
"owner": 1,
|
"owner": 1,
|
||||||
"title": "Объект-объектные отношения",
|
"title": "Объект-объектные отношения",
|
||||||
"alias": "КС ООО",
|
"alias": "КС ООО",
|
||||||
"comment": "",
|
"description": "",
|
||||||
"visible": false,
|
"visible": false,
|
||||||
"read_only": false,
|
"read_only": false,
|
||||||
"access_policy": "public",
|
"access_policy": "public",
|
||||||
|
@ -190,7 +190,7 @@
|
||||||
"owner": 1,
|
"owner": 1,
|
||||||
"title": "Процессы",
|
"title": "Процессы",
|
||||||
"alias": "КС Процессы",
|
"alias": "КС Процессы",
|
||||||
"comment": "",
|
"description": "",
|
||||||
"visible": false,
|
"visible": false,
|
||||||
"read_only": false,
|
"read_only": false,
|
||||||
"access_policy": "public",
|
"access_policy": "public",
|
||||||
|
@ -207,7 +207,7 @@
|
||||||
"owner": 1,
|
"owner": 1,
|
||||||
"title": "Экологические правоотношения",
|
"title": "Экологические правоотношения",
|
||||||
"alias": "ЭКОС",
|
"alias": "ЭКОС",
|
||||||
"comment": "",
|
"description": "",
|
||||||
"visible": true,
|
"visible": true,
|
||||||
"read_only": false,
|
"read_only": false,
|
||||||
"access_policy": "public",
|
"access_policy": "public",
|
||||||
|
@ -224,7 +224,7 @@
|
||||||
"owner": 1,
|
"owner": 1,
|
||||||
"title": "Объектная среда",
|
"title": "Объектная среда",
|
||||||
"alias": "КС Объект-сред",
|
"alias": "КС Объект-сред",
|
||||||
"comment": "",
|
"description": "",
|
||||||
"visible": false,
|
"visible": false,
|
||||||
"read_only": false,
|
"read_only": false,
|
||||||
"access_policy": "public",
|
"access_policy": "public",
|
||||||
|
@ -241,7 +241,7 @@
|
||||||
"owner": 1,
|
"owner": 1,
|
||||||
"title": "Процессные среды",
|
"title": "Процессные среды",
|
||||||
"alias": "КС Проц-сред",
|
"alias": "КС Проц-сред",
|
||||||
"comment": "",
|
"description": "",
|
||||||
"visible": false,
|
"visible": false,
|
||||||
"read_only": false,
|
"read_only": false,
|
||||||
"access_policy": "public",
|
"access_policy": "public",
|
||||||
|
@ -7414,7 +7414,7 @@
|
||||||
"result": 38,
|
"result": 38,
|
||||||
"alias": "КС Вещества",
|
"alias": "КС Вещества",
|
||||||
"title": "Вещества и смеси",
|
"title": "Вещества и смеси",
|
||||||
"comment": "",
|
"description": "",
|
||||||
"position_x": 530.0,
|
"position_x": 530.0,
|
||||||
"position_y": 370.0
|
"position_y": 370.0
|
||||||
}
|
}
|
||||||
|
@ -7428,7 +7428,7 @@
|
||||||
"result": 39,
|
"result": 39,
|
||||||
"alias": "КС ООО",
|
"alias": "КС ООО",
|
||||||
"title": "Объект-объектные отношения",
|
"title": "Объект-объектные отношения",
|
||||||
"comment": "",
|
"description": "",
|
||||||
"position_x": 710.0,
|
"position_x": 710.0,
|
||||||
"position_y": 370.0
|
"position_y": 370.0
|
||||||
}
|
}
|
||||||
|
@ -7442,7 +7442,7 @@
|
||||||
"result": 40,
|
"result": 40,
|
||||||
"alias": "КС Процессы",
|
"alias": "КС Процессы",
|
||||||
"title": "Процессы",
|
"title": "Процессы",
|
||||||
"comment": "",
|
"description": "",
|
||||||
"position_x": 890.0,
|
"position_x": 890.0,
|
||||||
"position_y": 370.0
|
"position_y": 370.0
|
||||||
}
|
}
|
||||||
|
@ -7456,7 +7456,7 @@
|
||||||
"result": 43,
|
"result": 43,
|
||||||
"alias": "КС Объект-сред",
|
"alias": "КС Объект-сред",
|
||||||
"title": "Объектная среда",
|
"title": "Объектная среда",
|
||||||
"comment": "",
|
"description": "",
|
||||||
"position_x": 620.0,
|
"position_x": 620.0,
|
||||||
"position_y": 470.0
|
"position_y": 470.0
|
||||||
}
|
}
|
||||||
|
@ -7470,7 +7470,7 @@
|
||||||
"result": 44,
|
"result": 44,
|
||||||
"alias": "КС Проц-сред",
|
"alias": "КС Проц-сред",
|
||||||
"title": "Процессные среды",
|
"title": "Процессные среды",
|
||||||
"comment": "",
|
"description": "",
|
||||||
"position_x": 760.0,
|
"position_x": 760.0,
|
||||||
"position_y": 570.0
|
"position_y": 570.0
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,6 +112,7 @@ if _domain != '':
|
||||||
|
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
'django.middleware.gzip.GZipMiddleware',
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'corsheaders.middleware.CorsMiddleware',
|
'corsheaders.middleware.CorsMiddleware',
|
||||||
|
|
|
@ -34,6 +34,10 @@ def operationNotInput(title: str):
|
||||||
return f'Операция не является Загрузкой: {title}'
|
return f'Операция не является Загрузкой: {title}'
|
||||||
|
|
||||||
|
|
||||||
|
def operationHasArguments(title: str):
|
||||||
|
return f'Операция имеет аргументы: {title}'
|
||||||
|
|
||||||
|
|
||||||
def operationResultFromAnotherOSS():
|
def operationResultFromAnotherOSS():
|
||||||
return 'Схема является результатом другой ОСС'
|
return 'Схема является результатом другой ОСС'
|
||||||
|
|
||||||
|
|
22
rsconcept/frontend/.stylelintrc.json
Normal file
22
rsconcept/frontend/.stylelintrc.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,33 @@ import importPlugin from 'eslint-plugin-import';
|
||||||
import simpleImportSort from 'eslint-plugin-simple-import-sort';
|
import simpleImportSort from 'eslint-plugin-simple-import-sort';
|
||||||
import playwright from 'eslint-plugin-playwright';
|
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 [
|
export default [
|
||||||
...typescriptPlugin.configs.recommendedTypeChecked,
|
...typescriptPlugin.configs.recommendedTypeChecked,
|
||||||
...typescriptPlugin.configs.stylisticTypeChecked,
|
...typescriptPlugin.configs.stylisticTypeChecked,
|
||||||
|
@ -45,33 +72,9 @@ export default [
|
||||||
},
|
},
|
||||||
settings: { react: { version: 'detect' } },
|
settings: { react: { version: 'detect' } },
|
||||||
rules: {
|
rules: {
|
||||||
'no-console': 'off',
|
...basicRules,
|
||||||
'require-jsdoc': 'off',
|
|
||||||
|
|
||||||
'react-compiler/react-compiler': 'error',
|
'react-compiler/react-compiler': 'error',
|
||||||
'react-refresh/only-export-components': ['off', { allowConstantExport: true }],
|
'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': [
|
'simple-import-sort/imports': [
|
||||||
'warn',
|
'warn',
|
||||||
{
|
{
|
||||||
|
@ -113,13 +116,8 @@ export default [
|
||||||
},
|
},
|
||||||
|
|
||||||
rules: {
|
rules: {
|
||||||
|
...basicRules,
|
||||||
...playwright.configs['flat/recommended'].rules,
|
...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'
|
'simple-import-sort/imports': 'warn'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,12 +12,16 @@
|
||||||
<meta name="google-site-verification" content="bodB0xvBD_xM-VHg7EgfTf87jEMBF1DriZKdrZjwW1k" />
|
<meta name="google-site-verification" content="bodB0xvBD_xM-VHg7EgfTf87jEMBF1DriZKdrZjwW1k" />
|
||||||
<meta name="yandex-verification" content="2b1f1f721cd6b66a" />
|
<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.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link
|
<link
|
||||||
rel="preload"
|
rel="preload"
|
||||||
as="style"
|
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'"
|
onload="this.onload=null;this.rel='stylesheet'"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
1456
rsconcept/frontend/package-lock.json
generated
1456
rsconcept/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -9,7 +9,7 @@
|
||||||
"test:e2e": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
"dev": "vite --host",
|
"dev": "vite --host",
|
||||||
"build": "tsc && vite build",
|
"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",
|
"lintFix": "eslint . --report-unused-disable-directives --max-warnings 0 --fix",
|
||||||
"preview": "vite preview --port 3000"
|
"preview": "vite preview --port 3000"
|
||||||
},
|
},
|
||||||
|
@ -17,12 +17,12 @@
|
||||||
"@dagrejs/dagre": "^1.1.4",
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
"@hookform/resolvers": "^4.1.3",
|
"@hookform/resolvers": "^4.1.3",
|
||||||
"@lezer/lr": "^1.4.2",
|
"@lezer/lr": "^1.4.2",
|
||||||
"@tanstack/react-query": "^5.67.2",
|
"@tanstack/react-query": "^5.69.0",
|
||||||
"@tanstack/react-query-devtools": "^5.67.2",
|
"@tanstack/react-query-devtools": "^5.69.0",
|
||||||
"@tanstack/react-table": "^8.21.2",
|
"@tanstack/react-table": "^8.21.2",
|
||||||
"@uiw/codemirror-themes": "^4.23.10",
|
"@uiw/codemirror-themes": "^4.23.10",
|
||||||
"@uiw/react-codemirror": "^4.23.10",
|
"@uiw/react-codemirror": "^4.23.10",
|
||||||
"axios": "^1.8.2",
|
"axios": "^1.8.3",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"global": "^4.4.0",
|
"global": "^4.4.0",
|
||||||
"js-file-download": "^0.4.12",
|
"js-file-download": "^0.4.12",
|
||||||
|
@ -34,7 +34,7 @@
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-intl": "^7.1.6",
|
"react-intl": "^7.1.6",
|
||||||
"react-router": "^7.3.0",
|
"react-router": "^7.3.0",
|
||||||
"react-scan": "^0.2.14",
|
"react-scan": "^0.3.2",
|
||||||
"react-select": "^5.10.1",
|
"react-select": "^5.10.1",
|
||||||
"react-tabs": "^6.1.0",
|
"react-tabs": "^6.1.0",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
|
@ -47,11 +47,11 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lezer/generator": "^1.7.2",
|
"@lezer/generator": "^1.7.2",
|
||||||
"@playwright/test": "^1.51.0",
|
"@playwright/test": "^1.51.1",
|
||||||
"@tailwindcss/vite": "^4.0.12",
|
"@tailwindcss/vite": "^4.0.14",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.13.10",
|
"@types/node": "^22.13.10",
|
||||||
"@types/react": "^19.0.10",
|
"@types/react": "^19.0.11",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
||||||
"@typescript-eslint/parser": "^8.0.1",
|
"@typescript-eslint/parser": "^8.0.1",
|
||||||
|
@ -66,11 +66,15 @@
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"jest": "^29.7.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",
|
"tailwindcss": "^4.0.7",
|
||||||
"ts-jest": "^29.2.6",
|
"ts-jest": "^29.2.6",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.8.2",
|
||||||
"typescript-eslint": "^8.26.0",
|
"typescript-eslint": "^8.26.1",
|
||||||
"vite": "^6.2.1"
|
"vite": "^6.2.2"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"react": "^19.0.0"
|
"react": "^19.0.0"
|
||||||
|
|
|
@ -15,20 +15,13 @@ export function NavigationButton({ icon, title, hideTitle, className, style, onC
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
tabIndex={-1}
|
tabIndex={0}
|
||||||
aria-label={title}
|
aria-label={title}
|
||||||
data-tooltip-id={!!title ? globalIDs.tooltip : undefined}
|
data-tooltip-id={!!title ? globalIDs.tooltip : undefined}
|
||||||
data-tooltip-hidden={hideTitle}
|
data-tooltip-hidden={hideTitle}
|
||||||
data-tooltip-content={title}
|
data-tooltip-content={title}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={clsx(
|
className={clsx('p-2 flex items-center gap-1', 'cc-btn-nav', 'font-controls focus-outline', className)}
|
||||||
'p-2 flex items-center gap-1',
|
|
||||||
'cursor-pointer',
|
|
||||||
'clr-btn-nav cc-animate-color duration-500',
|
|
||||||
'rounded-xl',
|
|
||||||
'font-controls whitespace-nowrap',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
{icon ? icon : null}
|
{icon ? icon : null}
|
||||||
|
|
|
@ -22,7 +22,7 @@ interface INavigationContext {
|
||||||
setRequireConfirmation: (value: boolean) => void;
|
setRequireConfirmation: (value: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NavigationContext = createContext<INavigationContext | null>(null);
|
const NavigationContext = createContext<INavigationContext | null>(null);
|
||||||
export const useConceptNavigation = () => {
|
export const useConceptNavigation = () => {
|
||||||
const context = use(NavigationContext);
|
const context = use(NavigationContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
|
|
|
@ -13,25 +13,25 @@ import { ToggleNavigation } from './toggle-navigation';
|
||||||
import { UserMenu } from './user-menu';
|
import { UserMenu } from './user-menu';
|
||||||
|
|
||||||
export function Navigation() {
|
export function Navigation() {
|
||||||
const router = useConceptNavigation();
|
const { push } = useConceptNavigation();
|
||||||
const size = useWindowSize();
|
const size = useWindowSize();
|
||||||
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
|
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
|
||||||
|
|
||||||
const navigateHome = (event: React.MouseEvent<Element>) =>
|
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>) =>
|
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>) =>
|
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>) =>
|
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 (
|
return (
|
||||||
<nav className='z-navigation sticky top-0 left-0 right-0 select-none bg-prim-100'>
|
<nav className='z-navigation sticky top-0 left-0 right-0 select-none bg-prim-100'>
|
||||||
<ToggleNavigation />
|
<ToggleNavigation />
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
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)',
|
'transition-[max-height,translate] ease-bezier duration-(--duration-move)',
|
||||||
noNavigationAnimation ? '-translate-y-6 max-h-0' : 'max-h-12'
|
noNavigationAnimation ? '-translate-y-6 max-h-0' : 'max-h-12'
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { IconDarkTheme, IconLightTheme, IconPin, IconUnpin } from '@/components/icons';
|
import { IconDarkTheme, IconLightTheme, IconPin, IconUnpin } from '@/components/icons';
|
||||||
import { useAppLayoutStore } from '@/stores/app-layout';
|
import { useAppLayoutStore } from '@/stores/app-layout';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
|
@ -11,7 +13,9 @@ export function ToggleNavigation() {
|
||||||
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
|
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
|
||||||
const toggleNoNavigation = useAppLayoutStore(state => state.toggleNoNavigation);
|
const toggleNoNavigation = useAppLayoutStore(state => state.toggleNoNavigation);
|
||||||
return (
|
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
|
<button
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
type='button'
|
type='button'
|
||||||
|
@ -19,6 +23,7 @@ export function ToggleNavigation() {
|
||||||
onClick={toggleNoNavigation}
|
onClick={toggleNoNavigation}
|
||||||
data-tooltip-id={globalIDs.tooltip}
|
data-tooltip-id={globalIDs.tooltip}
|
||||||
data-tooltip-content={noNavigationAnimation ? 'Показать навигацию' : 'Скрыть навигацию'}
|
data-tooltip-content={noNavigationAnimation ? 'Показать навигацию' : 'Скрыть навигацию'}
|
||||||
|
aria-label={noNavigationAnimation ? 'Показать навигацию' : 'Скрыть навигацию'}
|
||||||
>
|
>
|
||||||
{!noNavigationAnimation ? <IconPin size='0.75rem' /> : null}
|
{!noNavigationAnimation ? <IconPin size='0.75rem' /> : null}
|
||||||
{noNavigationAnimation ? <IconUnpin size='0.75rem' /> : null}
|
{noNavigationAnimation ? <IconUnpin size='0.75rem' /> : null}
|
||||||
|
@ -31,6 +36,7 @@ export function ToggleNavigation() {
|
||||||
onClick={toggleDarkMode}
|
onClick={toggleDarkMode}
|
||||||
data-tooltip-id={globalIDs.tooltip}
|
data-tooltip-id={globalIDs.tooltip}
|
||||||
data-tooltip-content={darkMode ? 'Тема: Темная' : 'Тема: Светлая'}
|
data-tooltip-content={darkMode ? 'Тема: Темная' : 'Тема: Светлая'}
|
||||||
|
aria-label={darkMode ? 'Тема: Темная' : 'Тема: Светлая'}
|
||||||
>
|
>
|
||||||
{darkMode ? <IconDarkTheme size='0.75rem' /> : null}
|
{darkMode ? <IconDarkTheme size='0.75rem' /> : null}
|
||||||
{!darkMode ? <IconLightTheme size='0.75rem' /> : null}
|
{!darkMode ? <IconLightTheme size='0.75rem' /> : null}
|
||||||
|
|
|
@ -85,27 +85,28 @@ export function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
|
||||||
/>
|
/>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text={darkMode ? 'Тема: Темная' : 'Тема: Светлая'}
|
text={darkMode ? 'Тема: Темная' : 'Тема: Светлая'}
|
||||||
icon={darkMode ? <IconDarkTheme size='1rem' /> : <IconLightTheme size='1rem' />}
|
|
||||||
title='Переключение темы оформления'
|
title='Переключение темы оформления'
|
||||||
|
icon={darkMode ? <IconDarkTheme size='1rem' /> : <IconLightTheme size='1rem' />}
|
||||||
onClick={handleToggleDarkMode}
|
onClick={handleToggleDarkMode}
|
||||||
/>
|
/>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text={showHelp ? 'Помощь: Вкл' : 'Помощь: Выкл'}
|
text={showHelp ? 'Помощь: Вкл' : 'Помощь: Выкл'}
|
||||||
icon={showHelp ? <IconHelp size='1rem' /> : <IconHelpOff size='1rem' />}
|
|
||||||
title='Отображение иконок подсказок'
|
title='Отображение иконок подсказок'
|
||||||
|
icon={showHelp ? <IconHelp size='1rem' /> : <IconHelpOff size='1rem' />}
|
||||||
onClick={toggleShowHelp}
|
onClick={toggleShowHelp}
|
||||||
/>
|
/>
|
||||||
{user.is_staff ? (
|
{user.is_staff ? (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text={adminMode ? 'Админ: Вкл' : 'Админ: Выкл'}
|
text={adminMode ? 'Админ: Вкл' : 'Админ: Выкл'}
|
||||||
icon={adminMode ? <IconAdmin size='1rem' /> : <IconAdminOff size='1rem' />}
|
|
||||||
title='Работа в режиме администратора'
|
title='Работа в режиме администратора'
|
||||||
|
icon={adminMode ? <IconAdmin size='1rem' /> : <IconAdminOff size='1rem' />}
|
||||||
onClick={toggleAdminMode}
|
onClick={toggleAdminMode}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{user.is_staff ? (
|
{user.is_staff ? (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='REST API' //
|
text='REST API' //
|
||||||
|
title='Переход к backend API'
|
||||||
icon={<IconRESTapi size='1rem' />}
|
icon={<IconRESTapi size='1rem' />}
|
||||||
className='border-t'
|
className='border-t'
|
||||||
onClick={gotoRestApi}
|
onClick={gotoRestApi}
|
||||||
|
@ -114,6 +115,7 @@ export function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
|
||||||
{user.is_staff ? (
|
{user.is_staff ? (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='База данных' //
|
text='База данных' //
|
||||||
|
title='Переход к администрированию базы данных'
|
||||||
icon={<IconDatabase size='1rem' />}
|
icon={<IconDatabase size='1rem' />}
|
||||||
onClick={gotoAdmin}
|
onClick={gotoAdmin}
|
||||||
/>
|
/>
|
||||||
|
@ -121,6 +123,7 @@ export function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
|
||||||
{user?.is_staff ? (
|
{user?.is_staff ? (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='Иконки' //
|
text='Иконки' //
|
||||||
|
title='Переход к странице иконок'
|
||||||
icon={<IconImage size='1rem' />}
|
icon={<IconImage size='1rem' />}
|
||||||
onClick={gotoIcons}
|
onClick={gotoIcons}
|
||||||
/>
|
/>
|
||||||
|
@ -128,6 +131,7 @@ export function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
|
||||||
{user.is_staff ? (
|
{user.is_staff ? (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='Структура БД' //
|
text='Структура БД' //
|
||||||
|
title='Переход к странице структуры БД'
|
||||||
icon={<IconDBStructure size='1rem' />}
|
icon={<IconDBStructure size='1rem' />}
|
||||||
onClick={gotoDatabaseSchema}
|
onClick={gotoDatabaseSchema}
|
||||||
className='border-b'
|
className='border-b'
|
||||||
|
@ -135,6 +139,7 @@ export function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
|
||||||
) : null}
|
) : null}
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='Выйти...'
|
text='Выйти...'
|
||||||
|
title='Выход из приложения'
|
||||||
className='font-semibold'
|
className='font-semibold'
|
||||||
icon={<IconLogout size='1rem' />}
|
icon={<IconLogout size='1rem' />}
|
||||||
onClick={logoutAndRedirect}
|
onClick={logoutAndRedirect}
|
||||||
|
|
|
@ -33,19 +33,19 @@ axiosInstance.interceptors.request.use(config => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// ================ Data transfer types ================
|
// ================ Data transfer types ================
|
||||||
export interface IFrontRequest<RequestData, ResponseData> {
|
interface IFrontRequest<RequestData, ResponseData> {
|
||||||
data?: RequestData;
|
data?: RequestData;
|
||||||
successMessage?: string | ((data: ResponseData) => string);
|
successMessage?: string | ((data: ResponseData) => string);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAxiosRequest<RequestData, ResponseData> {
|
interface IAxiosRequest<RequestData, ResponseData> {
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
request?: IFrontRequest<RequestData, ResponseData>;
|
request?: IFrontRequest<RequestData, ResponseData>;
|
||||||
options?: AxiosRequestConfig;
|
options?: AxiosRequestConfig;
|
||||||
schema?: z.ZodType;
|
schema?: z.ZodType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAxiosGetRequest {
|
interface IAxiosGetRequest {
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
options?: AxiosRequestConfig;
|
options?: AxiosRequestConfig;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
|
|
|
@ -38,14 +38,13 @@ export function Button({
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
disabled={disabled ?? loading}
|
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'inline-flex gap-2 items-center justify-center',
|
'inline-flex gap-2 items-center justify-center',
|
||||||
'font-medium select-none disabled:cursor-auto',
|
'font-medium select-none disabled:cursor-auto',
|
||||||
'clr-btn-default cc-animate-color',
|
'clr-btn-default cc-animate-color',
|
||||||
dense ? 'px-1' : 'px-3 py-1',
|
dense ? 'px-1' : 'px-3 py-1',
|
||||||
loading ? 'cursor-progress' : 'cursor-pointer',
|
loading ? 'cursor-progress' : 'cursor-pointer',
|
||||||
noOutline ? 'outline-hidden' : 'clr-outline',
|
noOutline ? 'outline-hidden' : 'focus-outline',
|
||||||
!noBorder && 'border rounded-sm',
|
!noBorder && 'border rounded-sm',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
@ -53,6 +52,8 @@ export function Button({
|
||||||
data-tooltip-html={titleHtml}
|
data-tooltip-html={titleHtml}
|
||||||
data-tooltip-content={title}
|
data-tooltip-content={title}
|
||||||
data-tooltip-hidden={hideTitle}
|
data-tooltip-hidden={hideTitle}
|
||||||
|
disabled={disabled ?? loading}
|
||||||
|
aria-label={!text ? title : undefined}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{icon ? icon : null}
|
{icon ? icon : null}
|
||||||
|
|
|
@ -49,6 +49,7 @@ export function MiniButton({
|
||||||
data-tooltip-html={titleHtml}
|
data-tooltip-html={titleHtml}
|
||||||
data-tooltip-content={title}
|
data-tooltip-content={title}
|
||||||
data-tooltip-hidden={hideTitle}
|
data-tooltip-hidden={hideTitle}
|
||||||
|
aria-label={title}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { TableFooter } from './table-footer';
|
||||||
import { TableHeader } from './table-header';
|
import { TableHeader } from './table-header';
|
||||||
import { useDataTable } from './use-data-table';
|
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. */
|
/** Style to conditionally apply to rows. */
|
||||||
export interface IConditionalStyle<TData> {
|
export interface IConditionalStyle<TData> {
|
||||||
|
@ -120,6 +120,7 @@ export function DataTable<TData extends RowData>({
|
||||||
onRowDoubleClicked,
|
onRowDoubleClicked,
|
||||||
noDataComponent,
|
noDataComponent,
|
||||||
|
|
||||||
|
onChangePaginationOption,
|
||||||
paginationPerPage,
|
paginationPerPage,
|
||||||
paginationOptions = [10, 20, 30, 40, 50],
|
paginationOptions = [10, 20, 30, 40, 50],
|
||||||
|
|
||||||
|
@ -182,6 +183,7 @@ export function DataTable<TData extends RowData>({
|
||||||
<PaginationTools
|
<PaginationTools
|
||||||
id={id ? `${id}__pagination` : undefined}
|
id={id ? `${id}__pagination` : undefined}
|
||||||
table={table}
|
table={table}
|
||||||
|
onChangePaginationOption={onChangePaginationOption}
|
||||||
paginationOptions={paginationOptions}
|
paginationOptions={paginationOptions}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -12,15 +12,22 @@ interface PaginationToolsProps<TData> {
|
||||||
id?: string;
|
id?: string;
|
||||||
table: Table<TData>;
|
table: Table<TData>;
|
||||||
paginationOptions: number[];
|
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(
|
const handlePaginationOptionsChange = useCallback(
|
||||||
(event: React.ChangeEvent<HTMLSelectElement>) => {
|
(event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
const perPage = Number(event.target.value);
|
const perPage = Number(event.target.value);
|
||||||
table.setPageSize(perPage);
|
table.setPageSize(perPage);
|
||||||
|
onChangePaginationOption?.(perPage);
|
||||||
},
|
},
|
||||||
[table]
|
[table, onChangePaginationOption]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -38,7 +45,8 @@ export function PaginationTools<TData>({ id, table, paginationOptions }: Paginat
|
||||||
<div className='flex'>
|
<div className='flex'>
|
||||||
<button
|
<button
|
||||||
type='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)}
|
onClick={() => table.setPageIndex(0)}
|
||||||
disabled={!table.getCanPreviousPage()}
|
disabled={!table.getCanPreviousPage()}
|
||||||
>
|
>
|
||||||
|
@ -46,7 +54,8 @@ export function PaginationTools<TData>({ id, table, paginationOptions }: Paginat
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type='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()}
|
onClick={() => table.previousPage()}
|
||||||
disabled={!table.getCanPreviousPage()}
|
disabled={!table.getCanPreviousPage()}
|
||||||
>
|
>
|
||||||
|
@ -55,7 +64,8 @@ export function PaginationTools<TData>({ id, table, paginationOptions }: Paginat
|
||||||
<input
|
<input
|
||||||
id={id ? `${id}__page` : undefined}
|
id={id ? `${id}__page` : undefined}
|
||||||
title='Номер страницы. Выделите для ручного ввода'
|
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}
|
value={table.getState().pagination.pageIndex + 1}
|
||||||
onChange={event => {
|
onChange={event => {
|
||||||
const page = event.target.value ? Number(event.target.value) - 1 : 0;
|
const page = event.target.value ? Number(event.target.value) - 1 : 0;
|
||||||
|
@ -66,7 +76,8 @@ export function PaginationTools<TData>({ id, table, paginationOptions }: Paginat
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type='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()}
|
onClick={() => table.nextPage()}
|
||||||
disabled={!table.getCanNextPage()}
|
disabled={!table.getCanNextPage()}
|
||||||
>
|
>
|
||||||
|
@ -74,7 +85,8 @@ export function PaginationTools<TData>({ id, table, paginationOptions }: Paginat
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type='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)}
|
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||||
disabled={!table.getCanNextPage()}
|
disabled={!table.getCanNextPage()}
|
||||||
>
|
>
|
||||||
|
@ -83,12 +95,13 @@ export function PaginationTools<TData>({ id, table, paginationOptions }: Paginat
|
||||||
</div>
|
</div>
|
||||||
<select
|
<select
|
||||||
id={id ? `${id}__per_page` : undefined}
|
id={id ? `${id}__per_page` : undefined}
|
||||||
|
aria-label='Выбор количества строчек на странице'
|
||||||
value={table.getState().pagination.pageSize}
|
value={table.getState().pagination.pageSize}
|
||||||
onChange={handlePaginationOptionsChange}
|
onChange={handlePaginationOptionsChange}
|
||||||
className='mx-2 cursor-pointer bg-prim-100'
|
className='mx-2 cursor-pointer bg-prim-100 focus-outline'
|
||||||
>
|
>
|
||||||
{paginationOptions.map(pageSize => (
|
{paginationOptions.map(pageSize => (
|
||||||
<option key={`${prefixes.page_size}${pageSize}`} value={pageSize}>
|
<option key={`${prefixes.page_size}${pageSize}`} value={pageSize} aria-label={`${pageSize} на страницу`}>
|
||||||
{pageSize} на стр
|
{pageSize} на стр
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -33,7 +33,7 @@ export function TableBody<TData>({
|
||||||
const handleRowClicked = useCallback(
|
const handleRowClicked = useCallback(
|
||||||
(target: Row<TData>, event: React.MouseEvent<Element>) => {
|
(target: Row<TData>, event: React.MouseEvent<Element>) => {
|
||||||
onRowClicked?.(target.original, event);
|
onRowClicked?.(target.original, event);
|
||||||
if (target.getCanSelect()) {
|
if (table.options.enableRowSelection && target.getCanSelect()) {
|
||||||
if (event.shiftKey && !!lastSelected && lastSelected !== target.id) {
|
if (event.shiftKey && !!lastSelected && lastSelected !== target.id) {
|
||||||
const { rows, rowsById } = table.getRowModel();
|
const { rows, rowsById } = table.getRowModel();
|
||||||
const lastIndex = rowsById[lastSelected].index;
|
const lastIndex = rowsById[lastSelected].index;
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
type ColumnSort,
|
type ColumnSort,
|
||||||
createColumnHelper,
|
|
||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
getPaginationRowModel,
|
getPaginationRowModel,
|
||||||
getSortedRowModel,
|
getSortedRowModel,
|
||||||
|
@ -12,13 +11,10 @@ import {
|
||||||
type RowSelectionState,
|
type RowSelectionState,
|
||||||
type SortingState,
|
type SortingState,
|
||||||
type TableOptions,
|
type TableOptions,
|
||||||
type Updater,
|
|
||||||
useReactTable,
|
useReactTable,
|
||||||
type VisibilityState
|
type VisibilityState
|
||||||
} from '@tanstack/react-table';
|
} from '@tanstack/react-table';
|
||||||
|
|
||||||
export { type ColumnSort, createColumnHelper, type RowSelectionState, type VisibilityState };
|
|
||||||
|
|
||||||
/** Style to conditionally apply to rows. */
|
/** Style to conditionally apply to rows. */
|
||||||
export interface IConditionalStyle<TData> {
|
export interface IConditionalStyle<TData> {
|
||||||
/** Callback to determine if the style should be applied. */
|
/** Callback to determine if the style should be applied. */
|
||||||
|
@ -28,7 +24,7 @@ export interface IConditionalStyle<TData> {
|
||||||
style: React.CSSProperties;
|
style: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UseDataTableProps<TData extends RowData>
|
interface UseDataTableProps<TData extends RowData>
|
||||||
extends Pick<TableOptions<TData>, 'data' | 'columns' | 'onRowSelectionChange' | 'onColumnVisibilityChange'> {
|
extends Pick<TableOptions<TData>, 'data' | 'columns' | 'onRowSelectionChange' | 'onColumnVisibilityChange'> {
|
||||||
/** Enable row selection. */
|
/** Enable row selection. */
|
||||||
enableRowSelection?: boolean;
|
enableRowSelection?: boolean;
|
||||||
|
@ -48,9 +44,6 @@ export interface UseDataTableProps<TData extends RowData>
|
||||||
/** Number of rows per page. */
|
/** Number of rows per page. */
|
||||||
paginationPerPage?: number;
|
paginationPerPage?: number;
|
||||||
|
|
||||||
/** Callback to be called when the pagination option is changed. */
|
|
||||||
onChangePaginationOption?: (newValue: number) => void;
|
|
||||||
|
|
||||||
/** Enable sorting. */
|
/** Enable sorting. */
|
||||||
enableSorting?: boolean;
|
enableSorting?: boolean;
|
||||||
|
|
||||||
|
@ -76,7 +69,6 @@ export function useDataTable<TData extends RowData>({
|
||||||
|
|
||||||
enablePagination,
|
enablePagination,
|
||||||
paginationPerPage = 10,
|
paginationPerPage = 10,
|
||||||
onChangePaginationOption,
|
|
||||||
|
|
||||||
...restProps
|
...restProps
|
||||||
}: UseDataTableProps<TData>) {
|
}: UseDataTableProps<TData>) {
|
||||||
|
@ -86,19 +78,6 @@ export function useDataTable<TData extends RowData>({
|
||||||
pageSize: paginationPerPage
|
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({
|
const table = useReactTable({
|
||||||
state: {
|
state: {
|
||||||
pagination: pagination,
|
pagination: pagination,
|
||||||
|
@ -114,7 +93,7 @@ export function useDataTable<TData extends RowData>({
|
||||||
onSortingChange: enableSorting ? setSorting : undefined,
|
onSortingChange: enableSorting ? setSorting : undefined,
|
||||||
|
|
||||||
getPaginationRowModel: enablePagination ? getPaginationRowModel() : undefined,
|
getPaginationRowModel: enablePagination ? getPaginationRowModel() : undefined,
|
||||||
onPaginationChange: enablePagination ? handleChangePagination : undefined,
|
onPaginationChange: enablePagination ? setPagination : undefined,
|
||||||
|
|
||||||
enableHiding: enableHiding,
|
enableHiding: enableHiding,
|
||||||
enableMultiRowSelection: enableRowSelection,
|
enableMultiRowSelection: enableRowSelection,
|
||||||
|
|
|
@ -31,7 +31,6 @@ export function DropdownButton({
|
||||||
}: DropdownButtonProps) {
|
}: DropdownButtonProps) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
tabIndex={-1}
|
|
||||||
type='button'
|
type='button'
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
@ -46,6 +45,7 @@ export function DropdownButton({
|
||||||
data-tooltip-html={titleHtml}
|
data-tooltip-html={titleHtml}
|
||||||
data-tooltip-content={title}
|
data-tooltip-content={title}
|
||||||
data-tooltip-hidden={hideTitle}
|
data-tooltip-hidden={hideTitle}
|
||||||
|
aria-label={title}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{icon ? icon : null}
|
{icon ? icon : null}
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -48,6 +48,7 @@ export function Dropdown({
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
aria-hidden={!isOpen}
|
aria-hidden={!isOpen}
|
||||||
|
inert={!isOpen}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
export { Dropdown } from './dropdown';
|
export { Dropdown } from './dropdown';
|
||||||
export { DropdownButton } from './dropdown-button';
|
export { DropdownButton } from './dropdown-button';
|
||||||
export { DropdownCheckbox } from './dropdown-checkbox';
|
|
||||||
export { useDropdown } from './use-dropdown';
|
export { useDropdown } from './use-dropdown';
|
||||||
|
|
|
@ -2,18 +2,21 @@
|
||||||
|
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
|
|
||||||
import { useClickedOutside } from '@/hooks/use-clicked-outside';
|
|
||||||
|
|
||||||
export function useDropdown() {
|
export function useDropdown() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
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 {
|
return {
|
||||||
ref,
|
ref,
|
||||||
isOpen,
|
isOpen,
|
||||||
setIsOpen,
|
setIsOpen,
|
||||||
|
handleBlur,
|
||||||
toggle: () => setIsOpen(!isOpen),
|
toggle: () => setIsOpen(!isOpen),
|
||||||
hide: () => setIsOpen(false)
|
hide: () => setIsOpen(false)
|
||||||
};
|
};
|
||||||
|
|
|
@ -171,9 +171,9 @@ export interface IconProps {
|
||||||
className?: string;
|
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 (
|
return (
|
||||||
<svg width={size} height={size} fill='currentColor' viewBox={viewBox} {...props}>
|
<svg width={size} height={size} fill='currentColor' className={className} viewBox={viewBox} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { CheckboxChecked, CheckboxNull } from '../icons';
|
||||||
|
|
||||||
import { type CheckboxProps } from './checkbox';
|
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`. */
|
/** Current value - `null`, `true` or `false`. */
|
||||||
value: boolean | null;
|
value: boolean | null;
|
||||||
|
|
||||||
|
@ -55,12 +55,12 @@ export function CheckboxTristate({
|
||||||
cursor,
|
cursor,
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
disabled={disabled}
|
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
||||||
data-tooltip-html={titleHtml}
|
data-tooltip-html={titleHtml}
|
||||||
data-tooltip-content={title}
|
data-tooltip-content={title}
|
||||||
data-tooltip-hidden={hideTitle}
|
data-tooltip-hidden={hideTitle}
|
||||||
|
disabled={disabled}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -54,12 +54,12 @@ export function Checkbox({
|
||||||
cursor,
|
cursor,
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
disabled={disabled}
|
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
||||||
data-tooltip-html={titleHtml}
|
data-tooltip-html={titleHtml}
|
||||||
data-tooltip-content={title}
|
data-tooltip-content={title}
|
||||||
data-tooltip-hidden={hideTitle}
|
data-tooltip-hidden={hideTitle}
|
||||||
|
disabled={disabled}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -5,7 +5,7 @@ export { FileInput } from './file-input';
|
||||||
export { Label } from './label';
|
export { Label } from './label';
|
||||||
export { SearchBar } from './search-bar';
|
export { SearchBar } from './search-bar';
|
||||||
export { SelectMulti, type SelectMultiProps } from './select-multi';
|
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 { SelectTree } from './select-tree';
|
||||||
export { TextArea } from './text-area';
|
export { TextArea } from './text-area';
|
||||||
export { TextInput } from './text-input';
|
export { TextInput } from './text-input';
|
||||||
|
|
|
@ -41,10 +41,7 @@ export function SearchBar({
|
||||||
return (
|
return (
|
||||||
<div className={clsx('relative flex items-center', className)} {...restProps}>
|
<div className={clsx('relative flex items-center', className)} {...restProps}>
|
||||||
{!noIcon ? (
|
{!noIcon ? (
|
||||||
<IconSearch
|
<IconSearch className='absolute -top-0.5 left-2 translate-y-1/2 cc-search-icon' size='1.25rem' />
|
||||||
className='absolute -top-0.5 left-2 translate-y-1/2 pointer-events-none clr-text-controls'
|
|
||||||
size='1.25rem'
|
|
||||||
/>
|
|
||||||
) : null}
|
) : null}
|
||||||
<TextInput
|
<TextInput
|
||||||
id={id}
|
id={id}
|
||||||
|
|
|
@ -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'> {
|
extends Omit<Props<Option, false, Group>, 'theme' | 'menuPortalTarget'> {
|
||||||
noPortal?: boolean;
|
noPortal?: boolean;
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
|
|
|
@ -99,6 +99,7 @@ export function SelectTree<ItemType>({
|
||||||
>
|
>
|
||||||
{foldable.has(item) ? (
|
{foldable.has(item) ? (
|
||||||
<MiniButton
|
<MiniButton
|
||||||
|
aria-label={!folded.includes(item) ? 'Свернуть' : 'Развернуть'}
|
||||||
className={clsx('absolute left-1', !folded.includes(item) ? 'top-1.5' : 'top-1')}
|
className={clsx('absolute left-1', !folded.includes(item) ? 'top-1.5' : 'top-1')}
|
||||||
noPadding
|
noPadding
|
||||||
noHover
|
noHover
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { type Editor, type ErrorProcessing, type Titled } from '../props';
|
||||||
import { ErrorField } from './error-field';
|
import { ErrorField } from './error-field';
|
||||||
import { Label } from './label';
|
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. */
|
/** Indicates that the input should be transparent. */
|
||||||
transparent?: boolean;
|
transparent?: boolean;
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ export function TextArea({
|
||||||
fitContent && 'field-sizing-content',
|
fitContent && 'field-sizing-content',
|
||||||
noResize && 'resize-none',
|
noResize && 'resize-none',
|
||||||
transparent ? 'bg-transparent' : 'clr-input',
|
transparent ? 'bg-transparent' : 'clr-input',
|
||||||
!noOutline && 'clr-outline',
|
!noOutline && 'focus-outline',
|
||||||
dense && 'grow max-w-full',
|
dense && 'grow max-w-full',
|
||||||
!dense && !!label && 'mt-2',
|
!dense && !!label && 'mt-2',
|
||||||
!dense && className
|
!dense && className
|
||||||
|
|
|
@ -54,7 +54,7 @@ export function TextInput({
|
||||||
'leading-tight truncate hover:text-clip',
|
'leading-tight truncate hover:text-clip',
|
||||||
transparent ? 'bg-transparent' : 'clr-input',
|
transparent ? 'bg-transparent' : 'clr-input',
|
||||||
!noBorder && 'border',
|
!noBorder && 'border',
|
||||||
!noOutline && 'clr-outline',
|
!noOutline && 'focus-outline',
|
||||||
(!noBorder || !disabled) && 'px-3',
|
(!noBorder || !disabled) && 'px-3',
|
||||||
dense && 'grow max-w-full',
|
dense && 'grow max-w-full',
|
||||||
!dense && !!label && 'mt-2',
|
!dense && !!label && 'mt-2',
|
||||||
|
|
|
@ -105,9 +105,9 @@ export function ModalForm({
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<MiniButton
|
<MiniButton
|
||||||
noPadding
|
|
||||||
aria-label='Закрыть'
|
|
||||||
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
|
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
|
||||||
|
aria-label='Закрыть'
|
||||||
|
noPadding
|
||||||
icon={<IconClose size='1.25rem' />}
|
icon={<IconClose size='1.25rem' />}
|
||||||
className='absolute z-pop top-2 right-2'
|
className='absolute z-pop top-2 right-2'
|
||||||
onClick={hideDialog}
|
onClick={hideDialog}
|
||||||
|
|
|
@ -49,9 +49,9 @@ export function ModalView({
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<MiniButton
|
<MiniButton
|
||||||
noPadding
|
|
||||||
aria-label='Закрыть'
|
|
||||||
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
|
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
|
||||||
|
aria-label='Закрыть'
|
||||||
|
noPadding
|
||||||
icon={<IconClose size='1.25rem' />}
|
icon={<IconClose size='1.25rem' />}
|
||||||
className='absolute z-pop top-2 right-2'
|
className='absolute z-pop top-2 right-2'
|
||||||
onClick={hideDialog}
|
onClick={hideDialog}
|
||||||
|
@ -71,9 +71,10 @@ export function ModalView({
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'@container/modal',
|
'@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',
|
'overscroll-contain outline-hidden',
|
||||||
overflowVisible ? 'overflow-visible' : 'overflow-auto',
|
overflowVisible ? 'overflow-visible' : 'overflow-auto',
|
||||||
|
fullScreen ? 'max-h-[calc(100svh-2rem)]' : 'max-h-[calc(100svh-8rem)]',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
|
|
|
@ -14,7 +14,15 @@ interface TabLabelProps extends Omit<TabPropsImpl, 'children'>, Titled {
|
||||||
/**
|
/**
|
||||||
* Displays a tab header with a label.
|
* 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 (
|
return (
|
||||||
<TabImpl
|
<TabImpl
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
@ -31,6 +39,7 @@ export function TabLabel({ label, title, titleHtml, hideTitle, className, ...oth
|
||||||
data-tooltip-html={titleHtml}
|
data-tooltip-html={titleHtml}
|
||||||
data-tooltip-content={title}
|
data-tooltip-content={title}
|
||||||
data-tooltip-hidden={hideTitle}
|
data-tooltip-hidden={hideTitle}
|
||||||
|
role={role}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
|
|
|
@ -5,5 +5,4 @@ export { PDFViewer } from './pdf-viewer';
|
||||||
export { PrettyJson } from './pretty-json';
|
export { PrettyJson } from './pretty-json';
|
||||||
export { TextContent } from './text-content';
|
export { TextContent } from './text-content';
|
||||||
export { ValueIcon } from './value-icon';
|
export { ValueIcon } from './value-icon';
|
||||||
export { ValueLabeled } from './value-labeled';
|
|
||||||
export { ValueStats } from './value-stats';
|
export { ValueStats } from './value-stats';
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { truncateToLastWord } from '@/utils/utils';
|
||||||
|
|
||||||
import { type Styling } from '../props';
|
import { type Styling } from '../props';
|
||||||
|
|
||||||
export interface TextContentProps extends Styling {
|
interface TextContentProps extends Styling {
|
||||||
/** Text to display. */
|
/** Text to display. */
|
||||||
text: string;
|
text: string;
|
||||||
|
|
||||||
|
|
|
@ -15,15 +15,9 @@ interface ValueIconProps extends Styling, Titled {
|
||||||
/** Icon to display. */
|
/** Icon to display. */
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
|
|
||||||
/** Classname for the text. */
|
|
||||||
textClassName?: string;
|
|
||||||
|
|
||||||
/** Callback to be called when the component is clicked. */
|
/** Callback to be called when the component is clicked. */
|
||||||
onClick?: (event: React.MouseEvent<Element>) => void;
|
onClick?: (event: React.MouseEvent<Element>) => void;
|
||||||
|
|
||||||
/** Number of symbols to display in a small size. */
|
|
||||||
smallThreshold?: number;
|
|
||||||
|
|
||||||
/** Indicates that padding should be minimal. */
|
/** Indicates that padding should be minimal. */
|
||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
|
|
||||||
|
@ -39,18 +33,14 @@ export function ValueIcon({
|
||||||
dense,
|
dense,
|
||||||
icon,
|
icon,
|
||||||
value,
|
value,
|
||||||
textClassName,
|
|
||||||
disabled = true,
|
disabled = true,
|
||||||
title,
|
title,
|
||||||
titleHtml,
|
titleHtml,
|
||||||
hideTitle,
|
hideTitle,
|
||||||
className,
|
className,
|
||||||
smallThreshold,
|
|
||||||
onClick,
|
onClick,
|
||||||
...restProps
|
...restProps
|
||||||
}: ValueIconProps) {
|
}: ValueIconProps) {
|
||||||
// TODO: use CSS instead of threshold
|
|
||||||
const isSmall = !smallThreshold || String(value).length < smallThreshold;
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
@ -65,11 +55,10 @@ export function ValueIcon({
|
||||||
data-tooltip-html={titleHtml}
|
data-tooltip-html={titleHtml}
|
||||||
data-tooltip-content={title}
|
data-tooltip-content={title}
|
||||||
data-tooltip-hidden={hideTitle}
|
data-tooltip-hidden={hideTitle}
|
||||||
|
aria-label={title}
|
||||||
>
|
>
|
||||||
<MiniButton noHover noPadding icon={icon} disabled={disabled} onClick={onClick} />
|
{onClick ? <MiniButton noHover noPadding icon={icon} onClick={onClick} disabled={disabled} /> : icon}
|
||||||
<span id={id} className={clsx({ 'text-xs': !isSmall }, textClassName)}>
|
<span id={id}>{value}</span>
|
||||||
{value}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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
|
// characters - threshold for small labels - small font
|
||||||
const SMALL_THRESHOLD = 3;
|
const SMALL_THRESHOLD = 3;
|
||||||
|
@ -16,9 +17,23 @@ interface ValueStatsProps extends Styling, Titled {
|
||||||
value: string | number;
|
value: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Displays statistics value with an icon. */
|
||||||
* 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;
|
||||||
export function ValueStats(props: ValueStatsProps) {
|
return (
|
||||||
return <ValueIcon dense smallThreshold={SMALL_THRESHOLD} textClassName='min-w-5' {...props} />;
|
<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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,20 +64,20 @@ export function LoginPage() {
|
||||||
id='username'
|
id='username'
|
||||||
autoComplete='username'
|
autoComplete='username'
|
||||||
label='Логин или email'
|
label='Логин или email'
|
||||||
{...register('username')}
|
|
||||||
autoFocus
|
autoFocus
|
||||||
allowEnter
|
allowEnter
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
defaultValue={initialName}
|
defaultValue={initialName}
|
||||||
|
{...register('username')}
|
||||||
error={errors.username}
|
error={errors.username}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
id='password'
|
id='password'
|
||||||
{...register('password')}
|
|
||||||
type='password'
|
type='password'
|
||||||
autoComplete='current-password'
|
autoComplete='current-password'
|
||||||
label='Пароль'
|
label='Пароль'
|
||||||
allowEnter
|
allowEnter
|
||||||
|
{...register('password')}
|
||||||
error={errors.password}
|
error={errors.password}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -10,13 +10,13 @@ interface SubtopicsProps {
|
||||||
|
|
||||||
export function Subtopics({ headTopic }: SubtopicsProps) {
|
export function Subtopics({ headTopic }: SubtopicsProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<details>
|
||||||
<h2>Содержание раздела</h2>
|
<summary className='text-center font-semibold'>Содержание раздела</summary>
|
||||||
{Object.values(HelpTopic)
|
{Object.values(HelpTopic)
|
||||||
.filter(topic => topic !== headTopic && topicParent.get(topic) === headTopic)
|
.filter(topic => topic !== headTopic && topicParent.get(topic) === headTopic)
|
||||||
.map(topic => (
|
.map(topic => (
|
||||||
<TopicItem key={`${prefixes.topic_item}${topic}`} topic={topic} />
|
<TopicItem key={`${prefixes.topic_item}${topic}`} topic={topic} />
|
||||||
))}
|
))}
|
||||||
</>
|
</details>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,17 +26,22 @@ export function HelpInterface() {
|
||||||
интерфейса изменяются (цвет, иконка) в зависимости от доступности соответствующего функционала.
|
интерфейса изменяются (цвет, иконка) в зависимости от доступности соответствующего функционала.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<IconHelp className='inline-icon' />
|
<IconHelp className='inline-icon' /> Помимо данного раздела справка предоставляется контекстно через специальную
|
||||||
Помимо данного раздела справка предоставляется контекстно через специальную иконку{' '}
|
иконку <IconHelp className='inline-icon' />
|
||||||
<IconHelp className='inline-icon' />
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Навигация и настройки</h2>
|
<h2>Навигация и настройки</h2>
|
||||||
<li>Ctrl + клик на объект навигации откроет новую вкладку</li>
|
<li>
|
||||||
|
<kbd>Ctrl + клик</kbd> на объект навигации откроет новую вкладку
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconPin size='1.25rem' className='inline-icon' /> навигационную панель можно скрыть с помощью кнопки в правом
|
<IconPin size='1.25rem' className='inline-icon' /> навигационную панель можно скрыть с помощью кнопки в правом
|
||||||
верхнем углу
|
верхнем углу
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconLightTheme className='inline-icon' />
|
||||||
|
<IconDarkTheme className='inline-icon' /> переключатели темы
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconLogin size='1.25rem' className='inline-icon' /> вход в систему / регистрация нового пользователя
|
<IconLogin size='1.25rem' className='inline-icon' /> вход в систему / регистрация нового пользователя
|
||||||
</li>
|
</li>
|
||||||
|
@ -44,10 +49,7 @@ export function HelpInterface() {
|
||||||
<IconUser2 size='1.25rem' className='inline-icon' /> меню пользователя содержит ряд настроек и переход к профилю
|
<IconUser2 size='1.25rem' className='inline-icon' /> меню пользователя содержит ряд настроек и переход к профилю
|
||||||
пользователя
|
пользователя
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<IconLightTheme className='inline-icon' />
|
|
||||||
<IconDarkTheme className='inline-icon' /> переключатели темы
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<IconHelp className='inline-icon' />
|
<IconHelp className='inline-icon' />
|
||||||
<IconHelpOff className='inline-icon' /> отключение иконок контекстной справки
|
<IconHelpOff className='inline-icon' /> отключение иконок контекстной справки
|
||||||
|
|
|
@ -20,7 +20,8 @@ export function HelpMain() {
|
||||||
<LinkTopic text='Операционной схеме синтеза' topic={HelpTopic.CC_OSS} />.
|
<LinkTopic text='Операционной схеме синтеза' topic={HelpTopic.CC_OSS} />.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Разделы Справки</h2>
|
<details>
|
||||||
|
<summary className='text-center font-semibold'>Разделы Справки</summary>
|
||||||
{[
|
{[
|
||||||
HelpTopic.THESAURUS,
|
HelpTopic.THESAURUS,
|
||||||
HelpTopic.INTERFACE,
|
HelpTopic.INTERFACE,
|
||||||
|
@ -34,6 +35,7 @@ export function HelpMain() {
|
||||||
].map(topic => (
|
].map(topic => (
|
||||||
<TopicItem key={`${prefixes.topic_item}${topic}`} topic={topic} />
|
<TopicItem key={`${prefixes.topic_item}${topic}`} topic={topic} />
|
||||||
))}
|
))}
|
||||||
|
</details>
|
||||||
|
|
||||||
<h2>Лицензирование и раскрытие информации</h2>
|
<h2>Лицензирование и раскрытие информации</h2>
|
||||||
<li>Пользователи Портала сохраняют авторские права на создаваемый ими контент</li>
|
<li>Пользователи Портала сохраняют авторские права на создаваемый ими контент</li>
|
||||||
|
|
|
@ -259,6 +259,10 @@ export function HelpThesaurus() {
|
||||||
|
|
||||||
<h2>Операция</h2>
|
<h2>Операция</h2>
|
||||||
<p>Операция – выделенная часть ОСС, определяющая способ получения КС в рамках ОСС.</p>
|
<p>Операция – выделенная часть ОСС, определяющая способ получения КС в рамках ОСС.</p>
|
||||||
|
<p>
|
||||||
|
<IconConsolidation className='inline-icon' />
|
||||||
|
{'\u2009'}Ромбовидный синтез – операция, где используются КС, имеющие общих предков.
|
||||||
|
</p>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
По <b>способу получения КС выделены</b>:
|
По <b>способу получения КС выделены</b>:
|
||||||
|
@ -271,13 +275,6 @@ export function HelpThesaurus() {
|
||||||
{'\u2009'}синтез концептуальных схем.
|
{'\u2009'}синтез концептуальных схем.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<IconConsolidation className='inline-icon' />
|
|
||||||
{'\u2009'}Ромбовидный синтез – операция, где используются КС, имеющие общих предков.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,8 +34,12 @@ export function HelpLibrary() {
|
||||||
<li>
|
<li>
|
||||||
<span className='text-(--acc-fg-green)'>зеленым текстом</span> выделены ОСС
|
<span className='text-(--acc-fg-green)'>зеленым текстом</span> выделены ОСС
|
||||||
</li>
|
</li>
|
||||||
<li>клик по строке - переход к редактированию схемы</li>
|
<li>
|
||||||
<li>Ctrl + клик по строке откроет схему в новой вкладке</li>
|
<kbd>клик</kbd> по строке - переход к редактированию схемы
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<kbd>Ctrl + клик</kbd> по строке откроет схему в новой вкладке
|
||||||
|
</li>
|
||||||
<li>Фильтры атрибутов три позиции: да/нет/не применять</li>
|
<li>Фильтры атрибутов три позиции: да/нет/не применять</li>
|
||||||
<li>
|
<li>
|
||||||
<IconShow size='1rem' className='inline-icon' /> фильтры атрибутов применяются по клику
|
<IconShow size='1rem' className='inline-icon' /> фильтры атрибутов применяются по клику
|
||||||
|
@ -67,9 +71,15 @@ export function HelpLibrary() {
|
||||||
<li>
|
<li>
|
||||||
<IconSubfolders size='1rem' className='inline-icon icon-green' /> схемы во вложенных папках
|
<IconSubfolders size='1rem' className='inline-icon icon-green' /> схемы во вложенных папках
|
||||||
</li>
|
</li>
|
||||||
<li>клик по папке отображает справа схемы в ней</li>
|
<li>
|
||||||
<li>Ctrl + клик по папке копирует путь в буфер обмена</li>
|
<kbd>клик</kbd> по папке отображает справа схемы в ней
|
||||||
<li>клик по иконке сворачивает/разворачивает вложенные</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<kbd>Ctrl + клик по папке копирует путь в буфер обмена</kbd>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<kbd>клик</kbd> по иконке сворачивает/разворачивает вложенные
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconFolderEmpty size='1rem' className='inline-icon clr-text-default' /> папка без схем
|
<IconFolderEmpty size='1rem' className='inline-icon clr-text-default' /> папка без схем
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -53,10 +53,14 @@ export function HelpOssGraph() {
|
||||||
|
|
||||||
<div className='sm:w-84'>
|
<div className='sm:w-84'>
|
||||||
<h1>Изменение узлов</h1>
|
<h1>Изменение узлов</h1>
|
||||||
<li>Клик на операцию – выделение</li>
|
|
||||||
<li>Esc – сбросить выделение</li>
|
|
||||||
<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>
|
||||||
<li>
|
<li>
|
||||||
<IconEdit2 className='inline-icon' /> Редактирование операции
|
<IconEdit2 className='inline-icon' /> Редактирование операции
|
||||||
|
@ -65,7 +69,7 @@ export function HelpOssGraph() {
|
||||||
<IconNewItem className='inline-icon icon-green' /> Новая операция
|
<IconNewItem className='inline-icon icon-green' /> Новая операция
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconDestroy className='inline-icon icon-red' /> Delete – удалить выбранные
|
<IconDestroy className='inline-icon icon-red' /> <kbd>Delete</kbd> – удалить выбранные
|
||||||
</li>
|
</li>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -34,7 +34,7 @@ export function HelpRSCard() {
|
||||||
<IconOSS className='inline-icon' /> переход к связанной <LinkTopic text='ОСС' topic={HelpTopic.CC_OSS} />
|
<IconOSS className='inline-icon' /> переход к связанной <LinkTopic text='ОСС' topic={HelpTopic.CC_OSS} />
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconSave className='inline-icon' /> сохранить изменения: Ctrl + S
|
<IconSave className='inline-icon' /> сохранить изменения: <kbd>Ctrl + S</kbd>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconEditor className='inline-icon' /> Редактор обладает правом редактирования
|
<IconEditor className='inline-icon' /> Редактор обладает правом редактирования
|
||||||
|
|
|
@ -38,13 +38,13 @@ export function HelpRSEditor() {
|
||||||
<IconList className='inline-icon' /> список конституент
|
<IconList className='inline-icon' /> список конституент
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconSave className='inline-icon' /> сохранить: Ctrl + S
|
<IconSave className='inline-icon' /> сохранить: <kbd>Ctrl + S</kbd>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconReset className='inline-icon' /> сбросить изменения
|
<IconReset className='inline-icon' /> сбросить изменения
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconClone className='inline-icon icon-green' /> клонировать: Alt + V
|
<IconClone className='inline-icon icon-green' /> клонировать: <kbd>Alt + V</kbd>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconNewItem className='inline-icon icon-green' /> новая конституента
|
<IconNewItem className='inline-icon icon-green' /> новая конституента
|
||||||
|
@ -58,7 +58,7 @@ export function HelpRSEditor() {
|
||||||
<h2>Список конституент</h2>
|
<h2>Список конституент</h2>
|
||||||
<li>
|
<li>
|
||||||
<IconMoveDown className='inline-icon' />
|
<IconMoveDown className='inline-icon' />
|
||||||
<IconMoveUp className='inline-icon' /> Alt + вверх/вниз
|
<IconMoveUp className='inline-icon' /> <kbd>Alt + вверх/вниз</kbd>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconFilter className='inline-icon' />
|
<IconFilter className='inline-icon' />
|
||||||
|
@ -98,14 +98,18 @@ export function HelpRSEditor() {
|
||||||
<IconTree className='inline-icon' /> отображение{' '}
|
<IconTree className='inline-icon' /> отображение{' '}
|
||||||
<LinkTopic text='дерева разбора' topic={HelpTopic.UI_FORMULA_TREE} />
|
<LinkTopic text='дерева разбора' topic={HelpTopic.UI_FORMULA_TREE} />
|
||||||
</li>
|
</li>
|
||||||
<li>Ctrl + Пробел вставка незанятого имени / замена проекции</li>
|
<li>
|
||||||
|
<kbd>Ctrl + Пробел</kbd> вставка незанятого имени / замена проекции
|
||||||
|
</li>
|
||||||
|
|
||||||
<h2>Термин и Текстовое определение</h2>
|
<h2>Термин и Текстовое определение</h2>
|
||||||
<li>
|
<li>
|
||||||
<IconEdit className='inline-icon' /> редактирование <LinkTopic text='Имени' topic={HelpTopic.CC_CONSTITUENTA} />{' '}
|
<IconEdit className='inline-icon' /> редактирование <LinkTopic text='Имени' topic={HelpTopic.CC_CONSTITUENTA} />{' '}
|
||||||
/ <LinkTopic text='Термина' topic={HelpTopic.CC_CONSTITUENTA} />
|
/ <LinkTopic text='Термина' topic={HelpTopic.CC_CONSTITUENTA} />
|
||||||
</li>
|
</li>
|
||||||
<li>Ctrl + Пробел открывает редактирование отсылок</li>
|
<li>
|
||||||
|
<kbd>Ctrl + Пробел</kbd> открывает редактирование отсылок
|
||||||
|
</li>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,28 +33,34 @@ export function HelpRSList() {
|
||||||
<IconOSS className='inline-icon' /> переход к связанной <LinkTopic text='ОСС' topic={HelpTopic.CC_OSS} />
|
<IconOSS className='inline-icon' /> переход к связанной <LinkTopic text='ОСС' topic={HelpTopic.CC_OSS} />
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconReset className='inline-icon' /> сбросить выделение: ESC
|
<IconReset className='inline-icon' /> сбросить выделение: <kbd>ESC</kbd>
|
||||||
</li>
|
</li>
|
||||||
<li>Клик на строку – выделение</li>
|
<li>Клик на строку – выделение</li>
|
||||||
<li>Shift + клик – выделение нескольких</li>
|
<li>
|
||||||
<li>Alt + клик – Редактор</li>
|
<kbd>Shift + клик</kbd> – выделение нескольких
|
||||||
<li>Двойной клик – Редактор</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<kbd>Alt + клик</kbd> – Редактор
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<kbd>Двойной клик</kbd> – Редактор
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconMoveUp className='inline-icon' />
|
<IconMoveUp className='inline-icon' />
|
||||||
<IconMoveDown className='inline-icon' /> Alt + вверх/вниз – перемещение
|
<IconMoveDown className='inline-icon' /> <kbd>Alt + вверх/вниз</kbd> – перемещение
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<IconClone className='inline-icon icon-green' /> клонировать выделенную: Alt + V
|
<IconClone className='inline-icon icon-green' /> клонировать выделенную: <kbd>Alt + V</kbd>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconNewItem className='inline-icon icon-green' /> новая конституента: Alt + `
|
<IconNewItem className='inline-icon icon-green' /> новая конституента: <kbd>Alt + `</kbd>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
||||||
<li>
|
<li>
|
||||||
<IconDestroy className='inline-icon icon-red' /> удаление выделенных: Delete
|
<IconDestroy className='inline-icon icon-red' /> удаление выделенных: <kbd>Delete</kbd>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<Divider margins='my-2' />
|
<Divider margins='my-2' />
|
||||||
|
|
|
@ -1,53 +1,55 @@
|
||||||
/**
|
/**
|
||||||
* Represents manuals topic.
|
* Represents manuals topic.
|
||||||
*/
|
*/
|
||||||
export enum HelpTopic {
|
export const HelpTopic = {
|
||||||
MAIN = 'main',
|
MAIN: 'main',
|
||||||
|
|
||||||
THESAURUS = 'thesaurus',
|
THESAURUS: 'thesaurus',
|
||||||
|
|
||||||
INTERFACE = 'user-interface',
|
INTERFACE: 'user-interface',
|
||||||
UI_LIBRARY = 'ui-library',
|
UI_LIBRARY: 'ui-library',
|
||||||
UI_RS_MENU = 'ui-rsform-menu',
|
UI_RS_MENU: 'ui-rsform-menu',
|
||||||
UI_RS_CARD = 'ui-rsform-card',
|
UI_RS_CARD: 'ui-rsform-card',
|
||||||
UI_RS_LIST = 'ui-rsform-list',
|
UI_RS_LIST: 'ui-rsform-list',
|
||||||
UI_RS_EDITOR = 'ui-rsform-editor',
|
UI_RS_EDITOR: 'ui-rsform-editor',
|
||||||
UI_GRAPH_TERM = 'ui-graph-term',
|
UI_GRAPH_TERM: 'ui-graph-term',
|
||||||
UI_FORMULA_TREE = 'ui-formula-tree',
|
UI_FORMULA_TREE: 'ui-formula-tree',
|
||||||
UI_TYPE_GRAPH = 'ui-type-graph',
|
UI_TYPE_GRAPH: 'ui-type-graph',
|
||||||
UI_CST_STATUS = 'ui-rsform-cst-status',
|
UI_CST_STATUS: 'ui-rsform-cst-status',
|
||||||
UI_CST_CLASS = 'ui-rsform-cst-class',
|
UI_CST_CLASS: 'ui-rsform-cst-class',
|
||||||
UI_OSS_GRAPH = 'ui-oss-graph',
|
UI_OSS_GRAPH: 'ui-oss-graph',
|
||||||
UI_SUBSTITUTIONS = 'ui-substitutions',
|
UI_SUBSTITUTIONS: 'ui-substitutions',
|
||||||
UI_RELOCATE_CST = 'ui-relocate-cst',
|
UI_RELOCATE_CST: 'ui-relocate-cst',
|
||||||
|
|
||||||
CONCEPTUAL = 'concept',
|
CONCEPTUAL: 'concept',
|
||||||
CC_SYSTEM = 'concept-rsform',
|
CC_SYSTEM: 'concept-rsform',
|
||||||
CC_CONSTITUENTA = 'concept-constituenta',
|
CC_CONSTITUENTA: 'concept-constituenta',
|
||||||
CC_RELATIONS = 'concept-relations',
|
CC_RELATIONS: 'concept-relations',
|
||||||
CC_SYNTHESIS = 'concept-synthesis',
|
CC_SYNTHESIS: 'concept-synthesis',
|
||||||
CC_OSS = 'concept-operations-schema',
|
CC_OSS: 'concept-operations-schema',
|
||||||
CC_PROPAGATION = 'concept-change-propagation',
|
CC_PROPAGATION: 'concept-change-propagation',
|
||||||
|
|
||||||
RSLANG = 'rslang',
|
RSLANG: 'rslang',
|
||||||
RSL_TYPES = 'rslang-types',
|
RSL_TYPES: 'rslang-types',
|
||||||
RSL_CORRECT = 'rslang-correctness',
|
RSL_CORRECT: 'rslang-correctness',
|
||||||
RSL_INTERPRET = 'rslang-interpretation',
|
RSL_INTERPRET: 'rslang-interpretation',
|
||||||
RSL_OPERATIONS = 'rslang-operations',
|
RSL_OPERATIONS: 'rslang-operations',
|
||||||
RSL_TEMPLATES = 'rslang-templates',
|
RSL_TEMPLATES: 'rslang-templates',
|
||||||
|
|
||||||
TERM_CONTROL = 'terminology-control',
|
TERM_CONTROL: 'terminology-control',
|
||||||
ACCESS = 'access',
|
ACCESS: 'access',
|
||||||
VERSIONS = 'versions',
|
VERSIONS: 'versions',
|
||||||
|
|
||||||
INFO = 'documentation',
|
INFO: 'documentation',
|
||||||
INFO_RULES = 'rules',
|
INFO_RULES: 'rules',
|
||||||
INFO_CONTRIB = 'contributors',
|
INFO_CONTRIB: 'contributors',
|
||||||
INFO_PRIVACY = 'privacy',
|
INFO_PRIVACY: 'privacy',
|
||||||
INFO_API = 'api',
|
INFO_API: 'api',
|
||||||
|
|
||||||
EXTEOR = 'exteor'
|
EXTEOR: 'exteor'
|
||||||
}
|
} as const;
|
||||||
|
|
||||||
|
export type HelpTopic = (typeof HelpTopic)[keyof typeof HelpTopic];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manual topics hierarchy.
|
* Manual topics hierarchy.
|
||||||
|
@ -99,8 +101,3 @@ export const topicParent = new Map<HelpTopic, HelpTopic>([
|
||||||
|
|
||||||
[HelpTopic.EXTEOR, HelpTopic.EXTEOR]
|
[HelpTopic.EXTEOR, HelpTopic.EXTEOR]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
|
||||||
* Topics that can be folded.
|
|
||||||
*/
|
|
||||||
export const foldableTopics = [HelpTopic.INTERFACE, HelpTopic.RSLANG, HelpTopic.CONCEPTUAL, HelpTopic.INFO];
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ export function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownPro
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={menu.ref}
|
ref={menu.ref}
|
||||||
|
onBlur={menu.handleBlur}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'absolute left-0 w-54', //
|
'absolute left-0 w-54', //
|
||||||
noNavigation ? 'top-0' : 'top-12',
|
noNavigation ? 'top-0' : 'top-12',
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
import { urls, useConceptNavigation } from '@/app';
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
import { useAuthSuspense } from '@/features/auth';
|
import { useAuthSuspense } from '@/features/auth';
|
||||||
|
|
||||||
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
|
||||||
export function HomePage() {
|
export function HomePage() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const { isAnonymous } = useAuthSuspense();
|
const { isAnonymous } = useAuthSuspense();
|
||||||
|
|
||||||
if (isAnonymous) {
|
if (isAnonymous) {
|
||||||
router.replace({ path: urls.manuals });
|
// Note: Timeout is needed to let router initialize
|
||||||
|
setTimeout(() => router.replace({ path: urls.login }), PARAMETER.minimalTimeout);
|
||||||
} else {
|
} else {
|
||||||
router.replace({ path: urls.library });
|
setTimeout(() => router.replace({ path: urls.library }), PARAMETER.minimalTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -5,17 +5,19 @@ import { errorMsg } from '@/utils/labels';
|
||||||
import { validateLocation } from '../models/library-api';
|
import { validateLocation } from '../models/library-api';
|
||||||
|
|
||||||
/** Represents type of library items. */
|
/** Represents type of library items. */
|
||||||
export enum LibraryItemType {
|
export const LibraryItemType = {
|
||||||
RSFORM = 'rsform',
|
RSFORM: 'rsform',
|
||||||
OSS = 'oss'
|
OSS: 'oss'
|
||||||
}
|
} as const;
|
||||||
|
export type LibraryItemType = (typeof LibraryItemType)[keyof typeof LibraryItemType];
|
||||||
|
|
||||||
/** Represents Access policy for library items.*/
|
/** Represents Access policy for library items.*/
|
||||||
export enum AccessPolicy {
|
export const AccessPolicy = {
|
||||||
PUBLIC = 'public',
|
PUBLIC: 'public',
|
||||||
PROTECTED = 'protected',
|
PROTECTED: 'protected',
|
||||||
PRIVATE = 'private'
|
PRIVATE: 'private'
|
||||||
}
|
} as const;
|
||||||
|
export type AccessPolicy = (typeof AccessPolicy)[keyof typeof AccessPolicy];
|
||||||
|
|
||||||
/** Represents library item common data typical for all item types. */
|
/** Represents library item common data typical for all item types. */
|
||||||
export type ILibraryItem = z.infer<typeof schemaLibraryItem>;
|
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>;
|
export type IVersionUpdateDTO = z.infer<typeof schemaVersionUpdate>;
|
||||||
|
|
||||||
// ======= SCHEMAS =========
|
// ======= 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({
|
export const schemaLibraryItem = z.strictObject({
|
||||||
id: z.coerce.number(),
|
id: z.coerce.number(),
|
||||||
item_type: z.nativeEnum(LibraryItemType),
|
item_type: schemaLibraryItemType,
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
alias: z.string().nonempty(),
|
alias: z.string().nonempty(),
|
||||||
comment: z.string(),
|
description: z.string(),
|
||||||
visible: z.boolean(),
|
visible: z.boolean(),
|
||||||
read_only: z.boolean(),
|
read_only: z.boolean(),
|
||||||
location: z.string(),
|
location: z.string(),
|
||||||
access_policy: z.nativeEnum(AccessPolicy),
|
access_policy: schemaAccessPolicy,
|
||||||
|
|
||||||
time_create: z.string().datetime({ offset: true }),
|
time_create: z.string().datetime({ offset: true }),
|
||||||
time_update: z.string().datetime({ offset: true }),
|
time_update: z.string().datetime({ offset: true }),
|
||||||
|
@ -78,7 +82,7 @@ export const schemaCloneLibraryItem = schemaLibraryItem
|
||||||
item_type: true,
|
item_type: true,
|
||||||
title: true,
|
title: true,
|
||||||
alias: true,
|
alias: true,
|
||||||
comment: true,
|
description: true,
|
||||||
visible: true,
|
visible: true,
|
||||||
read_only: true,
|
read_only: true,
|
||||||
location: true,
|
location: true,
|
||||||
|
@ -94,14 +98,14 @@ export const schemaCloneLibraryItem = schemaLibraryItem
|
||||||
|
|
||||||
export const schemaCreateLibraryItem = z
|
export const schemaCreateLibraryItem = z
|
||||||
.object({
|
.object({
|
||||||
item_type: z.nativeEnum(LibraryItemType),
|
item_type: schemaLibraryItemType,
|
||||||
title: z.string().optional(),
|
title: z.string().optional(),
|
||||||
alias: z.string().optional(),
|
alias: z.string().optional(),
|
||||||
comment: z.string(),
|
description: z.string(),
|
||||||
visible: z.boolean(),
|
visible: z.boolean(),
|
||||||
read_only: z.boolean(),
|
read_only: z.boolean(),
|
||||||
location: z.string().refine(data => validateLocation(data), { message: errorMsg.invalidLocation }),
|
location: z.string().refine(data => validateLocation(data), { message: errorMsg.invalidLocation }),
|
||||||
access_policy: z.nativeEnum(AccessPolicy),
|
access_policy: schemaAccessPolicy,
|
||||||
|
|
||||||
file: z.instanceof(File).optional(),
|
file: z.instanceof(File).optional(),
|
||||||
fileName: z.string().optional()
|
fileName: z.string().optional()
|
||||||
|
@ -117,10 +121,10 @@ export const schemaCreateLibraryItem = z
|
||||||
|
|
||||||
export const schemaUpdateLibraryItem = z.strictObject({
|
export const schemaUpdateLibraryItem = z.strictObject({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
item_type: z.nativeEnum(LibraryItemType),
|
item_type: schemaLibraryItemType,
|
||||||
title: z.string().nonempty(errorMsg.requiredField),
|
title: z.string().nonempty(errorMsg.requiredField),
|
||||||
alias: z.string().nonempty(errorMsg.requiredField),
|
alias: z.string().nonempty(errorMsg.requiredField),
|
||||||
comment: z.string(),
|
description: z.string(),
|
||||||
visible: z.boolean(),
|
visible: z.boolean(),
|
||||||
read_only: z.boolean()
|
read_only: z.boolean()
|
||||||
});
|
});
|
||||||
|
|
|
@ -85,9 +85,9 @@ export function EditorLibraryItem({ schema, isAttachedToOSS }: EditorLibraryItem
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
<div className='relative flex justify-stretch sm:mb-1 max-w-120 gap-3'>
|
<div className='relative flex justify-stretch sm:mb-1 max-w-120 gap-3'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
|
title='Открыть в библиотеке'
|
||||||
noHover
|
noHover
|
||||||
noPadding
|
noPadding
|
||||||
title='Открыть в библиотеке'
|
|
||||||
icon={<IconFolderOpened size='1.25rem' className='icon-primary' />}
|
icon={<IconFolderOpened size='1.25rem' className='icon-primary' />}
|
||||||
onClick={handleOpenLibrary}
|
onClick={handleOpenLibrary}
|
||||||
/>
|
/>
|
||||||
|
@ -101,7 +101,7 @@ export function EditorLibraryItem({ schema, isAttachedToOSS }: EditorLibraryItem
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='relative'>
|
<div className='relative' ref={ownerSelector.ref} onBlur={ownerSelector.handleBlur}>
|
||||||
{ownerSelector.isOpen ? (
|
{ownerSelector.isOpen ? (
|
||||||
<div className='absolute -top-2 right-0'>
|
<div className='absolute -top-2 right-0'>
|
||||||
<SelectUser className='w-100 text-sm' value={schema.owner} onChange={onSelectUser} />
|
<SelectUser className='w-100 text-sm' value={schema.owner} onChange={onSelectUser} />
|
||||||
|
@ -133,23 +133,21 @@ export function EditorLibraryItem({ schema, isAttachedToOSS }: EditorLibraryItem
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<ValueIcon
|
<ValueIcon
|
||||||
|
title='Дата обновления'
|
||||||
dense
|
dense
|
||||||
disabled
|
|
||||||
icon={<IconDateUpdate size='1.25rem' className='text-ok-600' />}
|
icon={<IconDateUpdate size='1.25rem' className='text-ok-600' />}
|
||||||
value={new Date(schema.time_update).toLocaleString(intl.locale)}
|
value={new Date(schema.time_update).toLocaleString(intl.locale)}
|
||||||
title='Дата обновления'
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ValueIcon
|
<ValueIcon
|
||||||
|
title='Дата создания'
|
||||||
dense
|
dense
|
||||||
disabled
|
|
||||||
icon={<IconDateCreate size='1.25rem' className='text-ok-600' />}
|
icon={<IconDateCreate size='1.25rem' className='text-ok-600' />}
|
||||||
value={new Date(schema.time_create).toLocaleString(intl.locale, {
|
value={new Date(schema.time_create).toLocaleString(intl.locale, {
|
||||||
year: '2-digit',
|
year: '2-digit',
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
day: '2-digit'
|
day: '2-digit'
|
||||||
})}
|
})}
|
||||||
title='Дата создания'
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -44,7 +44,7 @@ export function MenuRole({ isOwned, isEditor }: MenuRoleProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={accessMenu.ref} className='relative'>
|
<div ref={accessMenu.ref} onBlur={accessMenu.handleBlur} className='relative'>
|
||||||
<Button
|
<Button
|
||||||
dense
|
dense
|
||||||
noBorder
|
noBorder
|
||||||
|
@ -67,22 +67,22 @@ export function MenuRole({ isOwned, isEditor }: MenuRoleProps) {
|
||||||
text={labelUserRole(UserRole.EDITOR)}
|
text={labelUserRole(UserRole.EDITOR)}
|
||||||
title={describeUserRole(UserRole.EDITOR)}
|
title={describeUserRole(UserRole.EDITOR)}
|
||||||
icon={<IconRole role={UserRole.EDITOR} size='1rem' />}
|
icon={<IconRole role={UserRole.EDITOR} size='1rem' />}
|
||||||
disabled={!isOwned && !isEditor}
|
|
||||||
onClick={() => handleChangeMode(UserRole.EDITOR)}
|
onClick={() => handleChangeMode(UserRole.EDITOR)}
|
||||||
|
disabled={!isOwned && !isEditor}
|
||||||
/>
|
/>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text={labelUserRole(UserRole.OWNER)}
|
text={labelUserRole(UserRole.OWNER)}
|
||||||
title={describeUserRole(UserRole.OWNER)}
|
title={describeUserRole(UserRole.OWNER)}
|
||||||
icon={<IconRole role={UserRole.OWNER} size='1rem' />}
|
icon={<IconRole role={UserRole.OWNER} size='1rem' />}
|
||||||
disabled={!isOwned}
|
|
||||||
onClick={() => handleChangeMode(UserRole.OWNER)}
|
onClick={() => handleChangeMode(UserRole.OWNER)}
|
||||||
|
disabled={!isOwned}
|
||||||
/>
|
/>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text={labelUserRole(UserRole.ADMIN)}
|
text={labelUserRole(UserRole.ADMIN)}
|
||||||
title={describeUserRole(UserRole.ADMIN)}
|
title={describeUserRole(UserRole.ADMIN)}
|
||||||
icon={<IconRole role={UserRole.ADMIN} size='1rem' />}
|
icon={<IconRole role={UserRole.ADMIN} size='1rem' />}
|
||||||
disabled={!user.is_staff}
|
|
||||||
onClick={() => handleChangeMode(UserRole.ADMIN)}
|
onClick={() => handleChangeMode(UserRole.ADMIN)}
|
||||||
|
disabled={!user.is_staff}
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -28,10 +28,15 @@ export function MiniSelectorOSS({ items, onSelect, className, ...restProps }: Mi
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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
|
<MiniButton
|
||||||
icon={<IconOSS size='1.25rem' className='icon-primary' />}
|
|
||||||
title='Операционные схемы'
|
title='Операционные схемы'
|
||||||
|
icon={<IconOSS size='1.25rem' className='icon-primary' />}
|
||||||
hideTitle={ossMenu.isOpen}
|
hideTitle={ossMenu.isOpen}
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
/>
|
/>
|
||||||
|
@ -40,9 +45,9 @@ export function MiniSelectorOSS({ items, onSelect, className, ...restProps }: Mi
|
||||||
<Label text='Список ОСС' className='border-b px-3 py-1' />
|
<Label text='Список ОСС' className='border-b px-3 py-1' />
|
||||||
{items.map((reference, index) => (
|
{items.map((reference, index) => (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
className='min-w-20'
|
|
||||||
key={`${prefixes.oss_list}${index}`}
|
key={`${prefixes.oss_list}${index}`}
|
||||||
text={reference.alias}
|
text={reference.alias}
|
||||||
|
className='min-w-20'
|
||||||
onClick={event => onSelect(event, reference)}
|
onClick={event => onSelect(event, reference)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -35,7 +35,12 @@ export function SelectLocationContext({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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
|
<MiniButton
|
||||||
title={title}
|
title={title}
|
||||||
hideTitle={menu.isOpen}
|
hideTitle={menu.isOpen}
|
||||||
|
|
|
@ -112,10 +112,10 @@ export function PickSchema({
|
||||||
query={filterText}
|
query={filterText}
|
||||||
onChangeQuery={newValue => setFilterText(newValue)}
|
onChangeQuery={newValue => setFilterText(newValue)}
|
||||||
/>
|
/>
|
||||||
<div className='relative' ref={locationMenu.ref}>
|
<div className='relative' ref={locationMenu.ref} onBlur={locationMenu.handleBlur}>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
icon={<IconFolderTree size='1.25rem' className={!!filterLocation ? 'icon-green' : 'icon-primary'} />}
|
|
||||||
title='Фильтр по расположению'
|
title='Фильтр по расположению'
|
||||||
|
icon={<IconFolderTree size='1.25rem' className={!!filterLocation ? 'icon-green' : 'icon-primary'} />}
|
||||||
className='mt-1'
|
className='mt-1'
|
||||||
onClick={() => locationMenu.toggle()}
|
onClick={() => locationMenu.toggle()}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -38,7 +38,7 @@ export function SelectAccessPolicy({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={menu.ref} className={clsx('relative', className)} {...restProps}>
|
<div ref={menu.ref} onBlur={menu.handleBlur} className={clsx('relative', className)} {...restProps}>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title={`Доступ: ${labelAccessPolicy(value)}`}
|
title={`Доступ: ${labelAccessPolicy(value)}`}
|
||||||
hideTitle={menu.isOpen}
|
hideTitle={menu.isOpen}
|
||||||
|
|
|
@ -37,7 +37,7 @@ export function SelectItemType({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={menu.ref} className={clsx('relative', className)} {...restProps}>
|
<div ref={menu.ref} onBlur={menu.handleBlur} className={clsx('relative', className)} {...restProps}>
|
||||||
<SelectorButton
|
<SelectorButton
|
||||||
transparent
|
transparent
|
||||||
title={describeLibraryItemType(value)}
|
title={describeLibraryItemType(value)}
|
||||||
|
|
|
@ -33,7 +33,12 @@ export function SelectLocationHead({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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
|
<SelectorButton
|
||||||
transparent
|
transparent
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
|
@ -52,10 +57,10 @@ export function SelectLocationHead({
|
||||||
return (
|
return (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
key={`${prefixes.location_head_list}${index}`}
|
key={`${prefixes.location_head_list}${index}`}
|
||||||
onClick={() => handleChange(head)}
|
|
||||||
title={describeLocationHead(head)}
|
|
||||||
icon={<IconLocationHead value={head} size='1rem' />}
|
|
||||||
text={labelLocationHead(head)}
|
text={labelLocationHead(head)}
|
||||||
|
title={describeLocationHead(head)}
|
||||||
|
onClick={() => handleChange(head)}
|
||||||
|
icon={<IconLocationHead value={head} size='1rem' />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -84,6 +84,7 @@ export function SelectLocation({ value, dense, prefix, onClick, className, style
|
||||||
<IconFolderOpened size='1rem' className='icon-green' />
|
<IconFolderOpened size='1rem' className='icon-green' />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
aria-label='Отображение вложенных папок'
|
||||||
onClick={event => handleClickFold(event, item, folded.includes(item))}
|
onClick={event => handleClickFold(event, item, folded.includes(item))}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -48,13 +48,14 @@ export function ToolbarItemAccess({
|
||||||
<Label text='Доступ' className='self-center select-none' />
|
<Label text='Доступ' className='self-center select-none' />
|
||||||
<div className='ml-auto cc-icons'>
|
<div className='ml-auto cc-icons'>
|
||||||
<SelectAccessPolicy
|
<SelectAccessPolicy
|
||||||
disabled={role <= UserRole.EDITOR || isProcessing || isAttachedToOSS}
|
|
||||||
value={policy}
|
value={policy}
|
||||||
onChange={handleSetAccessPolicy}
|
onChange={handleSetAccessPolicy}
|
||||||
|
disabled={role <= UserRole.EDITOR || isProcessing || isAttachedToOSS}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title={visible ? 'Библиотека: отображать' : 'Библиотека: скрывать'}
|
title={visible ? 'Библиотека: отображать' : 'Библиотека: скрывать'}
|
||||||
|
aria-label='Переключатель отображения библиотеки'
|
||||||
icon={<IconItemVisibility value={visible} />}
|
icon={<IconItemVisibility value={visible} />}
|
||||||
onClick={toggleVisible}
|
onClick={toggleVisible}
|
||||||
disabled={role === UserRole.READER || isProcessing}
|
disabled={role === UserRole.READER || isProcessing}
|
||||||
|
@ -62,6 +63,7 @@ export function ToolbarItemAccess({
|
||||||
|
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title={readOnly ? 'Изменение: запрещено' : 'Изменение: разрешено'}
|
title={readOnly ? 'Изменение: запрещено' : 'Изменение: разрешено'}
|
||||||
|
aria-label='Переключатель режима изменения'
|
||||||
icon={
|
icon={
|
||||||
readOnly ? (
|
readOnly ? (
|
||||||
<IconImmutable size='1.25rem' className='text-sec-600' />
|
<IconImmutable size='1.25rem' className='text-sec-600' />
|
||||||
|
|
|
@ -56,13 +56,15 @@ export function ToolbarItemCard({ className, schema, onSubmit, isMutable, delete
|
||||||
{isMutable || isModified ? (
|
{isMutable || isModified ? (
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
|
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
|
||||||
disabled={!canSave}
|
aria-label='Сохранить изменения'
|
||||||
icon={<IconSave size='1.25rem' className='icon-primary' />}
|
icon={<IconSave size='1.25rem' className='icon-primary' />}
|
||||||
onClick={onSubmit}
|
onClick={onSubmit}
|
||||||
|
disabled={!canSave}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={tooltipText.shareItem(schema.access_policy === AccessPolicy.PUBLIC)}
|
titleHtml={tooltipText.shareItem(schema.access_policy === AccessPolicy.PUBLIC)}
|
||||||
|
aria-label='Поделиться схемой'
|
||||||
icon={<IconShare size='1.25rem' className='icon-primary' />}
|
icon={<IconShare size='1.25rem' className='icon-primary' />}
|
||||||
onClick={sharePage}
|
onClick={sharePage}
|
||||||
disabled={schema.access_policy !== AccessPolicy.PUBLIC}
|
disabled={schema.access_policy !== AccessPolicy.PUBLIC}
|
||||||
|
@ -71,8 +73,8 @@ export function ToolbarItemCard({ className, schema, onSubmit, isMutable, delete
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Удалить схему'
|
title='Удалить схему'
|
||||||
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
||||||
disabled={!isMutable || isProcessing || role < UserRole.OWNER}
|
|
||||||
onClick={deleteSchema}
|
onClick={deleteSchema}
|
||||||
|
disabled={!isMutable || isProcessing || role < UserRole.OWNER}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<BadgeHelp topic={HelpTopic.UI_RS_CARD} offset={4} />
|
<BadgeHelp topic={HelpTopic.UI_RS_CARD} offset={4} />
|
||||||
|
|
|
@ -43,7 +43,7 @@ export function DlgCloneLibraryItem() {
|
||||||
item_type: base.item_type,
|
item_type: base.item_type,
|
||||||
title: cloneTitle(base),
|
title: cloneTitle(base),
|
||||||
alias: base.alias,
|
alias: base.alias,
|
||||||
comment: base.comment,
|
description: base.description,
|
||||||
visible: true,
|
visible: true,
|
||||||
read_only: false,
|
read_only: false,
|
||||||
access_policy: AccessPolicy.PUBLIC,
|
access_policy: AccessPolicy.PUBLIC,
|
||||||
|
@ -68,7 +68,7 @@ export function DlgCloneLibraryItem() {
|
||||||
>
|
>
|
||||||
<TextInput
|
<TextInput
|
||||||
id='dlg_full_name' //
|
id='dlg_full_name' //
|
||||||
label='Полное название'
|
label='Название'
|
||||||
{...register('title')}
|
{...register('title')}
|
||||||
error={errors.title}
|
error={errors.title}
|
||||||
/>
|
/>
|
||||||
|
@ -95,6 +95,7 @@ export function DlgCloneLibraryItem() {
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title={field.value ? 'Библиотека: отображать' : 'Библиотека: скрывать'}
|
title={field.value ? 'Библиотека: отображать' : 'Библиотека: скрывать'}
|
||||||
|
aria-label='Переключатель отображения библиотеки'
|
||||||
icon={<IconItemVisibility value={field.value} />}
|
icon={<IconItemVisibility value={field.value} />}
|
||||||
onClick={() => field.onChange(!field.value)}
|
onClick={() => field.onChange(!field.value)}
|
||||||
/>
|
/>
|
||||||
|
@ -108,11 +109,17 @@ export function DlgCloneLibraryItem() {
|
||||||
control={control}
|
control={control}
|
||||||
name='location'
|
name='location'
|
||||||
render={({ field }) => (
|
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 ? (
|
{selected.length > 0 ? (
|
||||||
<Controller
|
<Controller
|
||||||
|
|
|
@ -47,12 +47,12 @@ export function DlgEditEditors() {
|
||||||
<div className='self-center text-sm font-semibold'>
|
<div className='self-center text-sm font-semibold'>
|
||||||
<span>Всего редакторов [{selected.length}]</span>
|
<span>Всего редакторов [{selected.length}]</span>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
noHover
|
|
||||||
title='Очистить список'
|
title='Очистить список'
|
||||||
|
noHover
|
||||||
className='py-0 align-middle'
|
className='py-0 align-middle'
|
||||||
icon={<IconRemove size='1.5rem' className='icon-red' />}
|
icon={<IconRemove size='1.5rem' className='icon-red' />}
|
||||||
disabled={selected.length === 0}
|
|
||||||
onClick={() => setSelected([])}
|
onClick={() => setSelected([])}
|
||||||
|
disabled={selected.length === 0}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -107,14 +107,15 @@ export function DlgEditVersions() {
|
||||||
<MiniButton
|
<MiniButton
|
||||||
type='submit'
|
type='submit'
|
||||||
title={isValid ? 'Сохранить изменения' : errorMsg.versionTaken}
|
title={isValid ? 'Сохранить изменения' : errorMsg.versionTaken}
|
||||||
disabled={!isDirty || !isValid || isProcessing}
|
aria-label='Сохранить изменения'
|
||||||
icon={<IconSave size='1.25rem' className='icon-primary' />}
|
icon={<IconSave size='1.25rem' className='icon-primary' />}
|
||||||
|
disabled={!isDirty || !isValid || isProcessing}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Сбросить несохраненные изменения'
|
title='Сбросить несохраненные изменения'
|
||||||
disabled={!isDirty}
|
|
||||||
onClick={() => reset()}
|
onClick={() => reset()}
|
||||||
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
||||||
|
disabled={!isDirty}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -66,9 +66,9 @@ export function TableVersions({ processing, items, onDelete, selected, onSelect
|
||||||
className='align-middle'
|
className='align-middle'
|
||||||
noHover
|
noHover
|
||||||
noPadding
|
noPadding
|
||||||
disabled={processing}
|
|
||||||
icon={<IconRemove size='1.25rem' className='icon-red' />}
|
icon={<IconRemove size='1.25rem' className='icon-red' />}
|
||||||
onClick={event => handleDeleteVersion(event, props.row.original.id)}
|
onClick={event => handleDeleteVersion(event, props.row.original.id)}
|
||||||
|
disabled={processing}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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}.
|
* Retrieves label for {@link AccessPolicy}.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -9,7 +9,7 @@ describe('Testing matching LibraryItem', () => {
|
||||||
item_type: LibraryItemType.RSFORM,
|
item_type: LibraryItemType.RSFORM,
|
||||||
title: 'Item1',
|
title: 'Item1',
|
||||||
alias: 'I1',
|
alias: 'I1',
|
||||||
comment: 'comment',
|
description: 'description',
|
||||||
time_create: 'I2',
|
time_create: 'I2',
|
||||||
time_update: '',
|
time_update: '',
|
||||||
owner: null,
|
owner: null,
|
||||||
|
@ -24,7 +24,7 @@ describe('Testing matching LibraryItem', () => {
|
||||||
item_type: LibraryItemType.RSFORM,
|
item_type: LibraryItemType.RSFORM,
|
||||||
title: '',
|
title: '',
|
||||||
alias: '',
|
alias: '',
|
||||||
comment: '',
|
description: '',
|
||||||
time_create: '',
|
time_create: '',
|
||||||
time_update: '',
|
time_update: '',
|
||||||
owner: null,
|
owner: null,
|
||||||
|
@ -46,7 +46,7 @@ describe('Testing matching LibraryItem', () => {
|
||||||
expect(matchLibraryItem(item1, item1.title + '@invalid')).toEqual(false);
|
expect(matchLibraryItem(item1, item1.title + '@invalid')).toEqual(false);
|
||||||
expect(matchLibraryItem(item1, item1.alias + '@invalid')).toEqual(false);
|
expect(matchLibraryItem(item1, item1.alias + '@invalid')).toEqual(false);
|
||||||
expect(matchLibraryItem(item1, item1.time_create)).toEqual(false);
|
expect(matchLibraryItem(item1, item1.time_create)).toEqual(false);
|
||||||
expect(matchLibraryItem(item1, item1.comment)).toEqual(true);
|
expect(matchLibraryItem(item1, item1.description)).toEqual(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ const LOCATION_REGEXP = /^\/[PLUS]((\/[!\d\p{L}]([!\d\p{L}\- ]*[!\d\p{L}])?)*)?$
|
||||||
*/
|
*/
|
||||||
export function matchLibraryItem(target: ILibraryItem, query: string): boolean {
|
export function matchLibraryItem(target: ILibraryItem, query: string): boolean {
|
||||||
const matcher = new TextMatcher(query);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -5,12 +5,13 @@
|
||||||
/**
|
/**
|
||||||
* Represents valid location headers.
|
* Represents valid location headers.
|
||||||
*/
|
*/
|
||||||
export enum LocationHead {
|
export const LocationHead = {
|
||||||
USER = '/U',
|
USER: '/U',
|
||||||
COMMON = '/S',
|
COMMON: '/S',
|
||||||
PROJECTS = '/P',
|
PROJECTS: '/P',
|
||||||
LIBRARY = '/L'
|
LIBRARY: '/L'
|
||||||
}
|
} as const;
|
||||||
|
export type LocationHead = (typeof LocationHead)[keyof typeof LocationHead];
|
||||||
|
|
||||||
export const BASIC_SCHEMAS = '/L/Базовые';
|
export const BASIC_SCHEMAS = '/L/Базовые';
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user