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