Compare commits
23 Commits
238a22b42f
...
ff18a22b14
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ff18a22b14 | ||
![]() |
1c16c19465 | ||
![]() |
99507a9b8d | ||
![]() |
a85112e265 | ||
![]() |
8d44919303 | ||
![]() |
38149d7168 | ||
![]() |
fce995f27d | ||
![]() |
ad1d1a47d6 | ||
![]() |
28b147ed28 | ||
![]() |
39938ff9dd | ||
![]() |
b9e5331d91 | ||
![]() |
28c84d90ba | ||
![]() |
b63767a401 | ||
![]() |
bfcaeb1ac5 | ||
![]() |
181200fcc5 | ||
![]() |
87b8a8d224 | ||
![]() |
d5386619e5 | ||
![]() |
e293fdc7ee | ||
![]() |
c3cc8f5d89 | ||
![]() |
338cb9543c | ||
![]() |
03578aa0b5 | ||
![]() |
7dc738d87c | ||
![]() |
a47cebf456 |
3
.github/workflows/frontend.yml
vendored
3
.github/workflows/frontend.yml
vendored
|
@ -35,13 +35,14 @@ jobs:
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
npm install -g typescript vite jest playwright
|
npm install -g typescript vite jest playwright
|
||||||
npx playwright install --with-deps
|
|
||||||
npm ci
|
npm ci
|
||||||
|
npx playwright install --with-deps
|
||||||
npm run build --if-present
|
npm run build --if-present
|
||||||
- name: Run CI
|
- name: Run CI
|
||||||
run: |
|
run: |
|
||||||
npm run lint
|
npm run lint
|
||||||
npm test
|
npm test
|
||||||
|
npm run test:e2e
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
with:
|
with:
|
||||||
|
|
29
.vscode/launch.json
vendored
29
.vscode/launch.json
vendored
|
@ -28,6 +28,14 @@
|
||||||
"script": "${workspaceFolder}/scripts/dev/RunTests.ps1",
|
"script": "${workspaceFolder}/scripts/dev/RunTests.ps1",
|
||||||
"args": []
|
"args": []
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Run end-to-end tests
|
||||||
|
"name": "Test E2E",
|
||||||
|
"type": "PowerShell",
|
||||||
|
"request": "launch",
|
||||||
|
"script": "${workspaceFolder}/scripts/dev/RunE2ETests.ps1",
|
||||||
|
"args": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// Run Tests for backend for current file in Debug mode
|
// Run Tests for backend for current file in Debug mode
|
||||||
"name": "BE-DebugTestFile",
|
"name": "BE-DebugTestFile",
|
||||||
|
@ -38,9 +46,28 @@
|
||||||
"args": ["test", "-k", "${fileBasenameNoExtension}"],
|
"args": ["test", "-k", "${fileBasenameNoExtension}"],
|
||||||
"django": true
|
"django": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Run Tests for frontend for current file in Debug mode
|
||||||
|
"name": "FE-DebugTestFile",
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"cwd": "${workspaceFolder}/rsconcept/frontend",
|
||||||
|
"runtimeExecutable": "npx",
|
||||||
|
"runtimeArgs": [
|
||||||
|
"playwright",
|
||||||
|
"test",
|
||||||
|
"${fileBasename}",
|
||||||
|
"--headed",
|
||||||
|
"--project=Desktop Chrome"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"PWDEBUG": "1"
|
||||||
|
},
|
||||||
|
"console": "integratedTerminal"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// Run Tests for frontned in Debug mode
|
// Run Tests for frontned in Debug mode
|
||||||
"name": "FE-DebugTestAll",
|
"name": "Jest DebugAll",
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"runtimeExecutable": "${workspaceFolder}/rsconcept/frontend/node_modules/.bin/jest",
|
"runtimeExecutable": "${workspaceFolder}/rsconcept/frontend/node_modules/.bin/jest",
|
||||||
|
|
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
|
@ -4,7 +4,6 @@
|
||||||
".pytest_cache/": true
|
".pytest_cache/": true
|
||||||
},
|
},
|
||||||
"typescript.tsdk": "rsconcept/frontend/node_modules/typescript/lib",
|
"typescript.tsdk": "rsconcept/frontend/node_modules/typescript/lib",
|
||||||
"eslint.workingDirectories": ["rsconcept/frontend"],
|
|
||||||
"isort.args": [
|
"isort.args": [
|
||||||
"--line-length",
|
"--line-length",
|
||||||
"100",
|
"100",
|
||||||
|
@ -15,6 +14,13 @@
|
||||||
"--project",
|
"--project",
|
||||||
"shared"
|
"shared"
|
||||||
],
|
],
|
||||||
|
"eslint.workingDirectories": [
|
||||||
|
{
|
||||||
|
"directory": "rsconcept/frontend",
|
||||||
|
"overrideConfigFile": "rsconcept/frontend/eslint.config.js",
|
||||||
|
"changeProcessCWD": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"autopep8.args": [
|
"autopep8.args": [
|
||||||
"--max-line-length",
|
"--max-line-length",
|
||||||
"120",
|
"120",
|
||||||
|
|
|
@ -66,6 +66,7 @@ This readme file is used mostly to document project dependencies and conventions
|
||||||
- eslint-plugin-simple-import-sort
|
- eslint-plugin-simple-import-sort
|
||||||
- eslint-plugin-react-hooks
|
- eslint-plugin-react-hooks
|
||||||
- eslint-plugin-tsdoc
|
- eslint-plugin-tsdoc
|
||||||
|
- eslint-plugin-playwright
|
||||||
- babel-plugin-react-compiler
|
- babel-plugin-react-compiler
|
||||||
- vite
|
- vite
|
||||||
- jest
|
- jest
|
||||||
|
|
|
@ -203,6 +203,7 @@ class OperationSchemaSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
def to_representation(self, instance: LibraryItem):
|
def to_representation(self, instance: LibraryItem):
|
||||||
result = LibraryItemDetailsSerializer(instance).data
|
result = LibraryItemDetailsSerializer(instance).data
|
||||||
|
del result['versions']
|
||||||
oss = OperationSchema(instance)
|
oss = OperationSchema(instance)
|
||||||
result['items'] = []
|
result['items'] = []
|
||||||
for operation in oss.operations().order_by('pk'):
|
for operation in oss.operations().order_by('pk'):
|
||||||
|
|
|
@ -13,10 +13,10 @@ from .basics import (
|
||||||
)
|
)
|
||||||
from .data_access import (
|
from .data_access import (
|
||||||
CstCreateSerializer,
|
CstCreateSerializer,
|
||||||
|
CstInfoSerializer,
|
||||||
CstListSerializer,
|
CstListSerializer,
|
||||||
CstMoveSerializer,
|
CstMoveSerializer,
|
||||||
CstRenameSerializer,
|
CstRenameSerializer,
|
||||||
CstSerializer,
|
|
||||||
CstSubstituteSerializer,
|
CstSubstituteSerializer,
|
||||||
CstTargetSerializer,
|
CstTargetSerializer,
|
||||||
CstUpdateSerializer,
|
CstUpdateSerializer,
|
||||||
|
|
|
@ -30,13 +30,12 @@ class CstBaseSerializer(serializers.ModelSerializer):
|
||||||
read_only_fields = ('id',)
|
read_only_fields = ('id',)
|
||||||
|
|
||||||
|
|
||||||
class CstSerializer(serializers.ModelSerializer):
|
class CstInfoSerializer(serializers.ModelSerializer):
|
||||||
''' Serializer: Constituenta data. '''
|
''' Serializer: Constituenta public information. '''
|
||||||
class Meta:
|
class Meta:
|
||||||
''' serializer metadata. '''
|
''' serializer metadata. '''
|
||||||
model = Constituenta
|
model = Constituenta
|
||||||
exclude = ('order',)
|
exclude = ('order', 'schema')
|
||||||
read_only_fields = ('id', 'schema', 'alias', 'cst_type', 'definition_resolved', 'term_resolved')
|
|
||||||
|
|
||||||
|
|
||||||
class CstUpdateSerializer(serializers.Serializer):
|
class CstUpdateSerializer(serializers.Serializer):
|
||||||
|
@ -100,7 +99,7 @@ class RSFormSerializer(serializers.ModelSerializer):
|
||||||
child=serializers.IntegerField()
|
child=serializers.IntegerField()
|
||||||
)
|
)
|
||||||
items = serializers.ListField(
|
items = serializers.ListField(
|
||||||
child=CstSerializer()
|
child=CstInfoSerializer()
|
||||||
)
|
)
|
||||||
inheritance = serializers.ListField(
|
inheritance = serializers.ListField(
|
||||||
child=InheritanceDataSerializer()
|
child=InheritanceDataSerializer()
|
||||||
|
@ -136,8 +135,7 @@ class RSFormSerializer(serializers.ModelSerializer):
|
||||||
result['oss'] = []
|
result['oss'] = []
|
||||||
result['inheritance'] = []
|
result['inheritance'] = []
|
||||||
for cst in RSForm(instance).constituents().defer('order').order_by('order'):
|
for cst in RSForm(instance).constituents().defer('order').order_by('order'):
|
||||||
result['items'].append(CstSerializer(cst).data)
|
result['items'].append(CstInfoSerializer(cst).data)
|
||||||
del result['items'][-1]['schema']
|
|
||||||
for oss in LibraryItem.objects.filter(operations__result=instance).only('alias'):
|
for oss in LibraryItem.objects.filter(operations__result=instance).only('alias'):
|
||||||
result['oss'].append({
|
result['oss'].append({
|
||||||
'id': oss.pk,
|
'id': oss.pk,
|
||||||
|
|
|
@ -92,7 +92,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
return Response(
|
return Response(
|
||||||
status=c.HTTP_201_CREATED,
|
status=c.HTTP_201_CREATED,
|
||||||
data={
|
data={
|
||||||
'new_cst': s.CstSerializer(new_cst).data,
|
'new_cst': s.CstInfoSerializer(new_cst).data,
|
||||||
'schema': s.RSFormParseSerializer(schema.model).data
|
'schema': s.RSFormParseSerializer(schema.model).data
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -102,7 +102,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
tags=['RSForm'],
|
tags=['RSForm'],
|
||||||
request=s.CstUpdateSerializer,
|
request=s.CstUpdateSerializer,
|
||||||
responses={
|
responses={
|
||||||
c.HTTP_200_OK: s.CstSerializer,
|
c.HTTP_200_OK: s.CstInfoSerializer,
|
||||||
c.HTTP_400_BAD_REQUEST: None,
|
c.HTTP_400_BAD_REQUEST: None,
|
||||||
c.HTTP_403_FORBIDDEN: None,
|
c.HTTP_403_FORBIDDEN: None,
|
||||||
c.HTTP_404_NOT_FOUND: None
|
c.HTTP_404_NOT_FOUND: None
|
||||||
|
@ -122,7 +122,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
PropagationFacade.after_update_cst(schema, cst, data, old_data)
|
PropagationFacade.after_update_cst(schema, cst, data, old_data)
|
||||||
return Response(
|
return Response(
|
||||||
status=c.HTTP_200_OK,
|
status=c.HTTP_200_OK,
|
||||||
data=s.CstSerializer(m.Constituenta.objects.get(pk=request.data['target'])).data
|
data=s.CstInfoSerializer(m.Constituenta.objects.get(pk=request.data['target'])).data
|
||||||
)
|
)
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
|
@ -202,7 +202,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
return Response(
|
return Response(
|
||||||
status=c.HTTP_200_OK,
|
status=c.HTTP_200_OK,
|
||||||
data={
|
data={
|
||||||
'new_cst': s.CstSerializer(cst).data,
|
'new_cst': s.CstInfoSerializer(cst).data,
|
||||||
'schema': s.RSFormParseSerializer(schema.model).data
|
'schema': s.RSFormParseSerializer(schema.model).data
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -77,18 +77,6 @@ class AuthSerializer(serializers.Serializer):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class UserInfoSerializer(serializers.ModelSerializer):
|
|
||||||
''' Serializer: User data. '''
|
|
||||||
class Meta:
|
|
||||||
''' serializer metadata. '''
|
|
||||||
model = models.User
|
|
||||||
fields = [
|
|
||||||
'id',
|
|
||||||
'first_name',
|
|
||||||
'last_name',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class UserSerializer(serializers.ModelSerializer):
|
class UserSerializer(serializers.ModelSerializer):
|
||||||
''' Serializer: User data. '''
|
''' Serializer: User data. '''
|
||||||
id = serializers.IntegerField(read_only=True)
|
id = serializers.IntegerField(read_only=True)
|
||||||
|
@ -117,6 +105,20 @@ class UserSerializer(serializers.ModelSerializer):
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
class UserInfoSerializer(serializers.ModelSerializer):
|
||||||
|
''' Serializer: User open information. '''
|
||||||
|
id = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
''' serializer metadata. '''
|
||||||
|
model = models.User
|
||||||
|
fields = [
|
||||||
|
'id',
|
||||||
|
'first_name',
|
||||||
|
'last_name',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class ChangePasswordSerializer(serializers.Serializer):
|
class ChangePasswordSerializer(serializers.Serializer):
|
||||||
''' Serializer: Change password. '''
|
''' Serializer: Change password. '''
|
||||||
old_password = serializers.CharField(required=True)
|
old_password = serializers.CharField(required=True)
|
||||||
|
|
|
@ -73,7 +73,7 @@ class AuthAPIView(generics.RetrieveAPIView):
|
||||||
class ActiveUsersView(generics.ListAPIView):
|
class ActiveUsersView(generics.ListAPIView):
|
||||||
''' Endpoint: Get list of active users. '''
|
''' Endpoint: Get list of active users. '''
|
||||||
permission_classes = (permissions.AllowAny,)
|
permission_classes = (permissions.AllowAny,)
|
||||||
serializer_class = s.UserSerializer
|
serializer_class = s.UserInfoSerializer
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return m.User.objects.filter(is_active=True)
|
return m.User.objects.filter(is_active=True)
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
tzdata==2025.1
|
tzdata==2025.1
|
||||||
Django==5.1.5
|
Django==5.1.7
|
||||||
djangorestframework==3.15.2
|
djangorestframework==3.15.2
|
||||||
django-cors-headers==4.6.0
|
django-cors-headers==4.7.0
|
||||||
django-filter==24.3
|
django-filter==25.1
|
||||||
drf-spectacular==0.28.0
|
drf-spectacular==0.28.0
|
||||||
drf-spectacular-sidecar==2024.12.1
|
drf-spectacular-sidecar==2025.3.1
|
||||||
coreapi==2.3.3
|
coreapi==2.3.3
|
||||||
django-rest-passwordreset==1.5.0
|
django-rest-passwordreset==1.5.0
|
||||||
cctext==0.1.4
|
cctext==0.1.4
|
||||||
|
@ -12,9 +12,10 @@ pyconcept==0.1.12
|
||||||
|
|
||||||
psycopg2-binary==2.9.10
|
psycopg2-binary==2.9.10
|
||||||
gunicorn==23.0.0
|
gunicorn==23.0.0
|
||||||
djangorestframework-stubs==3.15.2
|
|
||||||
|
djangorestframework-stubs==3.15.3
|
||||||
django-extensions==3.2.3
|
django-extensions==3.2.3
|
||||||
django-stubs==5.1.2
|
django-stubs==5.1.3
|
||||||
mypy==1.13.0
|
mypy==1.15.0
|
||||||
pylint==3.3.3
|
pylint==3.3.5
|
||||||
coverage==7.6.10
|
coverage==7.6.12
|
|
@ -1,10 +1,10 @@
|
||||||
tzdata==2025.1
|
tzdata==2025.1
|
||||||
Django==5.1.5
|
Django==5.1.7
|
||||||
djangorestframework==3.15.2
|
djangorestframework==3.15.2
|
||||||
django-cors-headers==4.6.0
|
django-cors-headers==4.7.0
|
||||||
django-filter==24.3
|
django-filter==25.1
|
||||||
drf-spectacular==0.28.0
|
drf-spectacular==0.28.0
|
||||||
drf-spectacular-sidecar==2024.12.1
|
drf-spectacular-sidecar==2025.3.1
|
||||||
coreapi==2.3.3
|
coreapi==2.3.3
|
||||||
django-rest-passwordreset==1.5.0
|
django-rest-passwordreset==1.5.0
|
||||||
cctext==0.1.4
|
cctext==0.1.4
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
# Frontend Developer guidelines
|
|
||||||
|
|
||||||
Styling conventions
|
|
||||||
|
|
||||||
- static > conditional static > props. All dynamic styling should go in styles props
|
|
||||||
- dimensions = rectangle + outer layout
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>clsx className grouping and order</summary>
|
|
||||||
<pre>
|
|
||||||
- layer: z-position
|
|
||||||
- outer layout: fixed bottom-1/2 left-0 -translate-x-1/2
|
|
||||||
- rectangle: mt-3 min-w-fit min-w-10 flex-grow shrink-0
|
|
||||||
- inner layout: px-3 py-2 flex flex-col gap-3 justify-between items-center
|
|
||||||
- overflow behavior: overflow-scroll overscroll-contain
|
|
||||||
- border: borer-2 outline-none shadow-md
|
|
||||||
- text: text-start text-sm font-semibold whitespace-nowrap bg-prim-200 fg-app-100
|
|
||||||
- behavior modifiers: select-none disabled:cursor-auto
|
|
||||||
- transitions:
|
|
||||||
</pre>
|
|
||||||
</details>
|
|
|
@ -6,12 +6,21 @@ import reactCompilerPlugin from 'eslint-plugin-react-compiler';
|
||||||
import reactHooksPlugin from 'eslint-plugin-react-hooks';
|
import reactHooksPlugin from 'eslint-plugin-react-hooks';
|
||||||
import importPlugin from 'eslint-plugin-import';
|
import importPlugin from 'eslint-plugin-import';
|
||||||
import simpleImportSort from 'eslint-plugin-simple-import-sort';
|
import simpleImportSort from 'eslint-plugin-simple-import-sort';
|
||||||
|
import playwright from 'eslint-plugin-playwright';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
...typescriptPlugin.configs.recommendedTypeChecked,
|
...typescriptPlugin.configs.recommendedTypeChecked,
|
||||||
...typescriptPlugin.configs.stylisticTypeChecked,
|
...typescriptPlugin.configs.stylisticTypeChecked,
|
||||||
{
|
{
|
||||||
ignores: ['**/parser.ts', '**/node_modules/**', '**/public/**', '**/dist/**', 'eslint.config.js']
|
ignores: [
|
||||||
|
'**/parser.ts',
|
||||||
|
'**/node_modules/**',
|
||||||
|
'**/public/**',
|
||||||
|
'**/dist/**',
|
||||||
|
'vite.config.ts',
|
||||||
|
'eslint.config.js',
|
||||||
|
'playwright.config.ts'
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
|
@ -20,11 +29,13 @@ export default [
|
||||||
ecmaVersion: 'latest',
|
ecmaVersion: 'latest',
|
||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
globals: { ...globals.browser, ...globals.es2020, ...globals.jest },
|
globals: { ...globals.browser, ...globals.es2020, ...globals.jest },
|
||||||
project: ['./tsconfig.json', './tsconfig.node.json']
|
project: ['./tsconfig.json', './tsconfig.vite.json', './tsconfig.playwright.json']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
files: ['src/**/*.ts', 'src/**/*.tsx'],
|
||||||
|
|
||||||
plugins: {
|
plugins: {
|
||||||
'react': reactPlugin,
|
'react': reactPlugin,
|
||||||
'react-compiler': reactCompilerPlugin,
|
'react-compiler': reactCompilerPlugin,
|
||||||
|
@ -34,7 +45,12 @@ export default [
|
||||||
},
|
},
|
||||||
settings: { react: { version: 'detect' } },
|
settings: { react: { version: 'detect' } },
|
||||||
rules: {
|
rules: {
|
||||||
|
'no-console': 'off',
|
||||||
|
'require-jsdoc': 'off',
|
||||||
|
|
||||||
'react-compiler/react-compiler': 'error',
|
'react-compiler/react-compiler': 'error',
|
||||||
|
'react-refresh/only-export-components': ['off', { allowConstantExport: true }],
|
||||||
|
|
||||||
'@typescript-eslint/consistent-type-imports': [
|
'@typescript-eslint/consistent-type-imports': [
|
||||||
'warn',
|
'warn',
|
||||||
{
|
{
|
||||||
|
@ -54,8 +70,8 @@ export default [
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
'react-refresh/only-export-components': ['off', { allowConstantExport: true }],
|
'simple-import-sort/exports': 'error',
|
||||||
|
'import/no-duplicates': 'warn',
|
||||||
'simple-import-sort/imports': [
|
'simple-import-sort/imports': [
|
||||||
'warn',
|
'warn',
|
||||||
{
|
{
|
||||||
|
@ -81,17 +97,30 @@ export default [
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
'simple-import-sort/exports': 'error',
|
|
||||||
'import/no-duplicates': 'warn',
|
|
||||||
|
|
||||||
...reactHooksPlugin.configs.recommended.rules
|
...reactHooksPlugin.configs.recommended.rules
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['**/*.ts', '**/*.tsx'],
|
...playwright.configs['flat/recommended'],
|
||||||
|
|
||||||
|
files: ['tests/**/*.ts'],
|
||||||
|
|
||||||
|
plugins: {
|
||||||
|
'playwright': playwright,
|
||||||
|
'simple-import-sort': simpleImportSort,
|
||||||
|
'import': importPlugin
|
||||||
|
},
|
||||||
|
|
||||||
rules: {
|
rules: {
|
||||||
|
...playwright.configs['flat/recommended'].rules,
|
||||||
|
|
||||||
'no-console': 'off',
|
'no-console': 'off',
|
||||||
'require-jsdoc': 'off'
|
'require-jsdoc': 'off',
|
||||||
|
|
||||||
|
'simple-import-sort/exports': 'error',
|
||||||
|
'import/no-duplicates': 'warn',
|
||||||
|
'simple-import-sort/imports': 'warn'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
@ -15,8 +15,10 @@
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link
|
<link
|
||||||
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"
|
rel="preload"
|
||||||
rel="stylesheet"
|
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"
|
||||||
|
onload="this.onload=null;this.rel='stylesheet'"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<title>Концепт Портал</title>
|
<title>Концепт Портал</title>
|
||||||
|
|
1247
rsconcept/frontend/package-lock.json
generated
1247
rsconcept/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -5,7 +5,8 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"generate": "lezer-generator src/components/RSInput/rslang/rslangFast.grammar -o src/components/RSInput/rslang/parser.ts && lezer-generator src/components/RSInput/rslang/rslangAST.grammar -o src/components/RSInput/rslang/parserAST.ts && lezer-generator src/components/RefsInput/parse/refsText.grammar -o src/components/RefsInput/parse/parser.ts",
|
"generate": "lezer-generator src/components/RSInput/rslang/rslangFast.grammar -o src/components/RSInput/rslang/parser.ts && lezer-generator src/components/RSInput/rslang/rslangAST.grammar -o src/components/RSInput/rslang/parserAST.ts && lezer-generator src/components/RefsInput/parse/refsText.grammar -o src/components/RefsInput/parse/parser.ts",
|
||||||
"test": "jest && playwright test",
|
"test": "jest",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
"dev": "vite --host",
|
"dev": "vite --host",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
|
||||||
|
@ -14,14 +15,14 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dagrejs/dagre": "^1.1.4",
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
"@hookform/resolvers": "^4.1.2",
|
"@hookform/resolvers": "^4.1.3",
|
||||||
"@lezer/lr": "^1.4.2",
|
"@lezer/lr": "^1.4.2",
|
||||||
"@tanstack/react-query": "^5.66.9",
|
"@tanstack/react-query": "^5.67.2",
|
||||||
"@tanstack/react-query-devtools": "^5.66.9",
|
"@tanstack/react-query-devtools": "^5.67.2",
|
||||||
"@tanstack/react-table": "^8.21.2",
|
"@tanstack/react-table": "^8.21.2",
|
||||||
"@uiw/codemirror-themes": "^4.23.8",
|
"@uiw/codemirror-themes": "^4.23.10",
|
||||||
"@uiw/react-codemirror": "^4.23.8",
|
"@uiw/react-codemirror": "^4.23.10",
|
||||||
"axios": "^1.8.1",
|
"axios": "^1.8.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"global": "^4.4.0",
|
"global": "^4.4.0",
|
||||||
"js-file-download": "^0.4.12",
|
"js-file-download": "^0.4.12",
|
||||||
|
@ -32,9 +33,9 @@
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-intl": "^7.1.6",
|
"react-intl": "^7.1.6",
|
||||||
"react-router": "^7.2.0",
|
"react-router": "^7.3.0",
|
||||||
"react-scan": "^0.1.4",
|
"react-scan": "^0.2.14",
|
||||||
"react-select": "^5.10.0",
|
"react-select": "^5.10.1",
|
||||||
"react-tabs": "^6.1.0",
|
"react-tabs": "^6.1.0",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
"react-tooltip": "^5.28.0",
|
"react-tooltip": "^5.28.0",
|
||||||
|
@ -46,29 +47,30 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lezer/generator": "^1.7.2",
|
"@lezer/generator": "^1.7.2",
|
||||||
"@playwright/test": "^1.50.1",
|
"@playwright/test": "^1.51.0",
|
||||||
"@tailwindcss/vite": "^4.0.9",
|
"@tailwindcss/vite": "^4.0.12",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.13.5",
|
"@types/node": "^22.13.10",
|
||||||
"@types/react": "^19.0.10",
|
"@types/react": "^19.0.10",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
||||||
"@typescript-eslint/parser": "^8.0.1",
|
"@typescript-eslint/parser": "^8.0.1",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"babel-plugin-react-compiler": "^19.0.0-beta-21e868a-20250216",
|
"babel-plugin-react-compiler": "^19.0.0-beta-21e868a-20250216",
|
||||||
"eslint": "^9.21.0",
|
"eslint": "^9.22.0",
|
||||||
"eslint-plugin-import": "^2.31.0",
|
"eslint-plugin-import": "^2.31.0",
|
||||||
|
"eslint-plugin-playwright": "^2.2.0",
|
||||||
"eslint-plugin-react": "^7.37.4",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"eslint-plugin-react-compiler": "^19.0.0-beta-21e868a-20250216",
|
"eslint-plugin-react-compiler": "^19.0.0-beta-21e868a-20250216",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"tailwindcss": "^4.0.7",
|
"tailwindcss": "^4.0.7",
|
||||||
"ts-jest": "^29.2.6",
|
"ts-jest": "^29.2.6",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.8.2",
|
||||||
"typescript-eslint": "^8.25.0",
|
"typescript-eslint": "^8.26.0",
|
||||||
"vite": "^6.2.0"
|
"vite": "^6.2.1"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"react": "^19.0.0"
|
"react": "^19.0.0"
|
||||||
|
|
|
@ -5,6 +5,7 @@ export default defineConfig({
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
retries: process.env.CI ? 2 : 0,
|
retries: process.env.CI ? 2 : 0,
|
||||||
reporter: 'list',
|
reporter: 'list',
|
||||||
|
fullyParallel: true,
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: 'Desktop Chrome',
|
name: 'Desktop Chrome',
|
||||||
|
@ -25,7 +26,7 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
webServer: {
|
webServer: {
|
||||||
command: 'npm run dev',
|
command: 'npm run dev',
|
||||||
url: 'http://localhost:3000',
|
port: 3000,
|
||||||
reuseExistingServer: !process.env.CI
|
reuseExistingServer: !process.env.CI
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -25,7 +25,7 @@ export function ApplicationLayout() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavigationState>
|
<NavigationState>
|
||||||
<div className='min-w-[20rem] antialiased h-full max-w-[120rem] mx-auto'>
|
<div className='min-w-80 antialiased h-full max-w-480 mx-auto'>
|
||||||
<ToasterThemed
|
<ToasterThemed
|
||||||
className='text-[14px]'
|
className='text-[14px]'
|
||||||
style={{ marginTop: noNavigationAnimation ? '1.5rem' : '3.5rem' }}
|
style={{ marginTop: noNavigationAnimation ? '1.5rem' : '3.5rem' }}
|
||||||
|
|
|
@ -13,15 +13,14 @@ export function Footer() {
|
||||||
'text-xs sm:text-sm select-none whitespace-nowrap text-prim-600 bg-prim-100'
|
'text-xs sm:text-sm select-none whitespace-nowrap text-prim-600 bg-prim-100'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className='flex gap-3'>
|
<nav className='flex gap-3' aria-label='Вторичная навигация'>
|
||||||
<TextURL text='Библиотека' href='/library' color='' />
|
<TextURL text='Библиотека' href='/library' color='' />
|
||||||
<TextURL text='Справка' href='/manuals' color='' />
|
<TextURL text='Справка' href='/manuals' color='' />
|
||||||
<TextURL text='Центр Концепт' href={external_urls.concept} color='' />
|
<TextURL text='Центр Концепт' href={external_urls.concept} color='' />
|
||||||
<TextURL text='Экстеор' href='/manuals?topic=exteor' color='' />
|
<TextURL text='Экстеор' href='/manuals?topic=exteor' color='' />
|
||||||
</div>
|
</nav>
|
||||||
<div>
|
|
||||||
<p>© 2024 ЦИВТ КОНЦЕПТ</p>
|
<p>© 2024 ЦИВТ КОНЦЕПТ</p>
|
||||||
</div>
|
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { useNavigation } from 'react-router';
|
import { useNavigation } from 'react-router';
|
||||||
import clsx from 'clsx';
|
|
||||||
import { useDebounce } from 'use-debounce';
|
import { useDebounce } from 'use-debounce';
|
||||||
|
|
||||||
import { Loader } from '@/components/Loader';
|
import { Loader } from '@/components/Loader';
|
||||||
|
import { ModalBackdrop } from '@/components/Modal/ModalBackdrop';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
|
||||||
// TODO: add animation
|
|
||||||
export function GlobalLoader() {
|
export function GlobalLoader() {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
|
|
||||||
|
@ -17,16 +16,9 @@ export function GlobalLoader() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='fixed top-0 left-0 w-full h-full z-modal cursor-default'>
|
<div className='cc-modal-wrapper'>
|
||||||
<div className={clsx('z-navigation', 'fixed top-0 left-0', 'w-full h-full', 'backdrop-blur-[3px] opacity-50')} />
|
<ModalBackdrop />
|
||||||
<div className={clsx('z-navigation', 'fixed top-0 left-0', 'w-full h-full', 'bg-prim-0 opacity-25')} />
|
<div className='cc-fade-in px-10 border rounded-xl bg-prim-100'>
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
'px-10 mb-10',
|
|
||||||
'z-modal absolute bottom-1/2 left-1/2 -translate-x-1/2 translate-y-1/2',
|
|
||||||
'border rounded-xl bg-prim-100'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Loader scale={6} />
|
<Loader scale={6} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,7 +11,7 @@ export const GlobalTooltips = () => {
|
||||||
id={globalIDs.tooltip}
|
id={globalIDs.tooltip}
|
||||||
layer='z-topmost'
|
layer='z-topmost'
|
||||||
place='right-start'
|
place='right-start'
|
||||||
className='mt-8 max-w-[20rem] break-words'
|
className='mt-8 max-w-80 break-words'
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
float
|
float
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
import { useMutationErrors } from '@/backend/useMutationErrors';
|
import { useMutationErrors } from '@/backend/useMutationErrors';
|
||||||
import { Button } from '@/components/Control';
|
import { Button } from '@/components/Control';
|
||||||
import { DescribeError } from '@/components/InfoError';
|
import { DescribeError } from '@/components/InfoError';
|
||||||
|
import { ModalBackdrop } from '@/components/Modal/ModalBackdrop';
|
||||||
import { useEscapeKey } from '@/hooks/useEscapeKey';
|
import { useEscapeKey } from '@/hooks/useEscapeKey';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
|
||||||
|
@ -19,18 +18,11 @@ export function MutationErrors() {
|
||||||
hideDialog();
|
hideDialog();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='fixed top-0 left-0 w-full h-full z-modal cursor-default'>
|
<div className='cc-modal-wrapper'>
|
||||||
<div className={clsx('z-navigation', 'fixed top-0 left-0', 'w-full h-full', 'backdrop-blur-[3px] opacity-50')} />
|
<ModalBackdrop onHide={resetErrors} />
|
||||||
<div className={clsx('z-navigation', 'fixed top-0 left-0', 'w-full h-full', 'bg-prim-0 opacity-25')} />
|
<div className='px-10 py-3 flex flex-col items-center border rounded-xl bg-prim-100' role='alertdialog'>
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
'px-10 mb-10 py-3 flex flex-col items-center',
|
|
||||||
'z-modal absolute bottom-1/2 left-1/2 -translate-x-1/2 translate-y-1/2',
|
|
||||||
'border rounded-xl bg-prim-100'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<h1 className='py-2 select-none'>Ошибка при обработке</h1>
|
<h1 className='py-2 select-none'>Ошибка при обработке</h1>
|
||||||
<div className={clsx('px-3 flex flex-col', 'text-warn-600 text-sm font-semibold', 'select-text')}>
|
<div className='px-3 flex flex-col text-warn-600 text-sm font-semibold select-text'>
|
||||||
<DescribeError error={mutationErrors[0]} />
|
<DescribeError error={mutationErrors[0]} />
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={resetErrors} className='w-fit' text='Закрыть' />
|
<Button onClick={resetErrors} className='w-fit' text='Закрыть' />
|
||||||
|
|
|
@ -8,7 +8,8 @@ export function Logo() {
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
alt=''
|
alt=''
|
||||||
className='max-h-[1.6rem] w-fit max-w-[11.4rem]'
|
aria-hidden
|
||||||
|
className='max-h-7 w-fit max-w-46'
|
||||||
src={size.isSmall ? '/logo_sign.svg' : !darkMode ? '/logo_full.svg' : '/logo_full_dark.svg'}
|
src={size.isSmall ? '/logo_sign.svg' : !darkMode ? '/logo_full.svg' : '/logo_full_dark.svg'}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
import { IconLibrary2, IconManuals, IconNewItem2 } from '@/components/Icons';
|
import { IconLibrary2, IconManuals, IconNewItem2 } from '@/components/Icons';
|
||||||
import { useWindowSize } from '@/hooks/useWindowSize';
|
import { useWindowSize } from '@/hooks/useWindowSize';
|
||||||
import { useAppLayoutStore } from '@/stores/appLayout';
|
import { useAppLayoutStore } from '@/stores/appLayout';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
|
||||||
|
|
||||||
import { urls } from '../urls';
|
import { urls } from '../urls';
|
||||||
|
|
||||||
|
@ -28,41 +25,22 @@ export function Navigation() {
|
||||||
router.push({ path: urls.create_schema, newTab: event.ctrlKey || event.metaKey });
|
router.push({ path: urls.create_schema, newTab: event.ctrlKey || event.metaKey });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav className='z-navigation sticky top-0 left-0 right-0 select-none bg-prim-100'>
|
||||||
className={clsx(
|
|
||||||
'z-navigation', //
|
|
||||||
'sticky top-0 left-0 right-0',
|
|
||||||
'select-none',
|
|
||||||
'bg-prim-100'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<ToggleNavigation />
|
<ToggleNavigation />
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className='pl-2 pr-6 sm:pr-4 h-12 flex cc-shadow-border'
|
||||||
'pl-2 pr-[1.5rem] sm:pr-[0.9rem] h-[3rem]', //
|
|
||||||
'flex',
|
|
||||||
'cc-shadow-border'
|
|
||||||
)}
|
|
||||||
style={{
|
style={{
|
||||||
willChange: 'max-height, translate',
|
|
||||||
transitionProperty: 'max-height, translate',
|
|
||||||
transitionDuration: `${PARAMETER.moveDuration}ms`,
|
|
||||||
maxHeight: noNavigationAnimation ? '0rem' : '3rem',
|
maxHeight: noNavigationAnimation ? '0rem' : '3rem',
|
||||||
translate: noNavigationAnimation ? '0 -1.5rem' : '0'
|
translate: noNavigationAnimation ? '0 -1.5rem' : '0'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div className='flex items-center mr-auto cursor-pointer' onClick={!size.isSmall ? navigateHome : undefined}>
|
||||||
tabIndex={-1}
|
|
||||||
className={clsx('flex items-center mr-auto', !size.isSmall && 'cursor-pointer')}
|
|
||||||
onClick={!size.isSmall ? navigateHome : undefined}
|
|
||||||
>
|
|
||||||
<Logo />
|
<Logo />
|
||||||
</div>
|
</div>
|
||||||
<div className='flex gap-1 py-[0.3rem]'>
|
<div className='flex gap-2 items-center'>
|
||||||
<NavigationButton text='Новая схема' icon={<IconNewItem2 size='1.5rem' />} onClick={navigateCreateNew} />
|
<NavigationButton text='Новая схема' icon={<IconNewItem2 size='1.5rem' />} onClick={navigateCreateNew} />
|
||||||
<NavigationButton text='Библиотека' icon={<IconLibrary2 size='1.5rem' />} onClick={navigateLibrary} />
|
<NavigationButton text='Библиотека' icon={<IconLibrary2 size='1.5rem' />} onClick={navigateLibrary} />
|
||||||
<NavigationButton text='Справка' icon={<IconManuals size='1.5rem' />} onClick={navigateHelp} />
|
<NavigationButton text='Справка' icon={<IconManuals size='1.5rem' />} onClick={navigateHelp} />
|
||||||
|
|
||||||
<UserMenu />
|
<UserMenu />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,49 +1,37 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { type Styling, type Titled } from '@/components/props';
|
import { type Styling } from '@/components/props';
|
||||||
import { globalIDs } from '@/utils/constants';
|
import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
interface NavigationButtonProps extends Titled, Styling {
|
interface NavigationButtonProps extends Styling {
|
||||||
text?: string;
|
text?: string;
|
||||||
|
title?: string;
|
||||||
|
hideTitle?: boolean;
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
onClick?: (event: React.MouseEvent<Element>) => void;
|
onClick?: (event: React.MouseEvent<Element>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NavigationButton({
|
export function NavigationButton({ icon, title, hideTitle, className, style, onClick, text }: NavigationButtonProps) {
|
||||||
icon,
|
|
||||||
title,
|
|
||||||
className,
|
|
||||||
style,
|
|
||||||
titleHtml,
|
|
||||||
hideTitle,
|
|
||||||
onClick,
|
|
||||||
text
|
|
||||||
}: NavigationButtonProps) {
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
aria-label={title}
|
||||||
data-tooltip-html={titleHtml}
|
data-tooltip-id={!!title ? globalIDs.tooltip : undefined}
|
||||||
data-tooltip-content={title}
|
|
||||||
data-tooltip-hidden={hideTitle}
|
data-tooltip-hidden={hideTitle}
|
||||||
|
data-tooltip-content={title}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'mr-1 h-full',
|
'p-2 flex items-center gap-1',
|
||||||
'flex items-center gap-1',
|
|
||||||
'cursor-pointer',
|
'cursor-pointer',
|
||||||
'clr-btn-nav cc-animate-color duration-500',
|
'clr-btn-nav cc-animate-color duration-500',
|
||||||
'rounded-xl',
|
'rounded-xl',
|
||||||
'font-controls whitespace-nowrap',
|
'font-controls whitespace-nowrap',
|
||||||
{
|
|
||||||
'px-2': text,
|
|
||||||
'px-4': !text
|
|
||||||
},
|
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
{icon ? <span>{icon}</span> : null}
|
{icon ? icon : null}
|
||||||
{text ? <span className='hidden sm:inline'>{text}</span> : null}
|
{text ? <span className='hidden sm:inline'>{text}</span> : null}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { createContext, useContext, useEffect, useState } from 'react';
|
import { createContext, use, useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
export interface NavigationProps {
|
export interface NavigationProps {
|
||||||
|
@ -23,9 +23,9 @@ interface INavigationContext {
|
||||||
setIsBlocked: (value: boolean) => void;
|
setIsBlocked: (value: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NavigationContext = createContext<INavigationContext | null>(null);
|
export const NavigationContext = createContext<INavigationContext | null>(null);
|
||||||
export const useConceptNavigation = () => {
|
export const useConceptNavigation = () => {
|
||||||
const context = useContext(NavigationContext);
|
const context = use(NavigationContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('useConceptNavigation has to be used within <NavigationState>');
|
throw new Error('useConceptNavigation has to be used within <NavigationState>');
|
||||||
}
|
}
|
||||||
|
@ -36,13 +36,14 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
|
||||||
const router = useNavigate();
|
const router = useNavigate();
|
||||||
|
|
||||||
const [isBlocked, setIsBlocked] = useState(false);
|
const [isBlocked, setIsBlocked] = useState(false);
|
||||||
|
const [internalNavigation, setInternalNavigation] = useState(false);
|
||||||
|
|
||||||
function validate() {
|
function validate() {
|
||||||
return !isBlocked || confirm('Изменения не сохранены. Вы уверены что хотите совершить переход?');
|
return !isBlocked || confirm('Изменения не сохранены. Вы уверены что хотите совершить переход?');
|
||||||
}
|
}
|
||||||
|
|
||||||
function canBack() {
|
function canBack() {
|
||||||
return !!window.history && window.history?.length !== 0;
|
return internalNavigation && !!window.history && window.history?.length !== 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function push(props: NavigationProps) {
|
function push(props: NavigationProps) {
|
||||||
|
@ -50,6 +51,7 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
|
||||||
window.open(`${props.path}`, '_blank');
|
window.open(`${props.path}`, '_blank');
|
||||||
} else if (props.force || validate()) {
|
} else if (props.force || validate()) {
|
||||||
setIsBlocked(false);
|
setIsBlocked(false);
|
||||||
|
setInternalNavigation(true);
|
||||||
Promise.resolve(router(props.path, { viewTransition: true })).catch(console.error);
|
Promise.resolve(router(props.path, { viewTransition: true })).catch(console.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,6 +61,7 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
|
||||||
window.open(`${props.path}`, '_blank');
|
window.open(`${props.path}`, '_blank');
|
||||||
} else if (props.force || validate()) {
|
} else if (props.force || validate()) {
|
||||||
setIsBlocked(false);
|
setIsBlocked(false);
|
||||||
|
setInternalNavigation(true);
|
||||||
return router(props.path, { viewTransition: true });
|
return router(props.path, { viewTransition: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +1,28 @@
|
||||||
import clsx from 'clsx';
|
'use client';
|
||||||
|
|
||||||
import { IconDarkTheme, IconLightTheme, IconPin, IconUnpin } from '@/components/Icons';
|
import { IconDarkTheme, IconLightTheme, IconPin, IconUnpin } from '@/components/Icons';
|
||||||
import { useAppLayoutStore } from '@/stores/appLayout';
|
import { useAppLayoutStore } from '@/stores/appLayout';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import { globalIDs, PARAMETER } from '@/utils/constants';
|
import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
export function ToggleNavigation() {
|
export function ToggleNavigation() {
|
||||||
const darkMode = usePreferencesStore(state => state.darkMode);
|
const darkMode = usePreferencesStore(state => state.darkMode);
|
||||||
const toggleDarkMode = usePreferencesStore(state => state.toggleDarkMode);
|
const toggleDarkMode = usePreferencesStore(state => state.toggleDarkMode);
|
||||||
const noNavigation = useAppLayoutStore(state => state.noNavigation);
|
|
||||||
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
|
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
|
||||||
const toggleNoNavigation = useAppLayoutStore(state => state.toggleNoNavigation);
|
const toggleNoNavigation = useAppLayoutStore(state => state.toggleNoNavigation);
|
||||||
const iconSize = !noNavigationAnimation ? '0.75rem' : '1rem';
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className='absolute top-0 right-0 z-navigation h-12 grid'>
|
||||||
className={clsx(
|
<button
|
||||||
'absolute top-0 right-0 z-navigation',
|
tabIndex={-1}
|
||||||
'min-h-[2rem] min-w-[2rem]',
|
type='button'
|
||||||
'flex items-end justify-center gap-1',
|
className='p-1 cursor-pointer self-start'
|
||||||
'select-none',
|
onClick={toggleNoNavigation}
|
||||||
!noNavigation && 'flex-col-reverse'
|
data-tooltip-id={globalIDs.tooltip}
|
||||||
)}
|
data-tooltip-content={noNavigationAnimation ? 'Показать навигацию' : 'Скрыть навигацию'}
|
||||||
style={{
|
|
||||||
willChange: 'height, width',
|
|
||||||
transitionProperty: 'height, width',
|
|
||||||
transitionDuration: `${PARAMETER.moveDuration}ms`,
|
|
||||||
height: noNavigationAnimation ? '2rem' : '3rem',
|
|
||||||
width: noNavigationAnimation ? '3rem' : '2rem'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
|
{!noNavigationAnimation ? <IconPin size='0.75rem' /> : null}
|
||||||
|
{noNavigationAnimation ? <IconUnpin size='0.75rem' /> : null}
|
||||||
|
</button>
|
||||||
{!noNavigationAnimation ? (
|
{!noNavigationAnimation ? (
|
||||||
<button
|
<button
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
|
@ -42,17 +36,6 @@ export function ToggleNavigation() {
|
||||||
{!darkMode ? <IconLightTheme size='0.75rem' /> : null}
|
{!darkMode ? <IconLightTheme size='0.75rem' /> : null}
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
<button
|
|
||||||
tabIndex={-1}
|
|
||||||
type='button'
|
|
||||||
className='p-1 cursor-pointer'
|
|
||||||
onClick={toggleNoNavigation}
|
|
||||||
data-tooltip-id={globalIDs.tooltip}
|
|
||||||
data-tooltip-content={noNavigationAnimation ? 'Показать навигацию' : 'Скрыть навигацию'}
|
|
||||||
>
|
|
||||||
{!noNavigationAnimation ? <IconPin size={iconSize} /> : null}
|
|
||||||
{noNavigationAnimation ? <IconUnpin size={iconSize} /> : null}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,15 +2,17 @@ import { useAuthSuspense } from '@/features/auth';
|
||||||
|
|
||||||
import { IconLogin, IconUser2 } from '@/components/Icons';
|
import { IconLogin, IconUser2 } from '@/components/Icons';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
|
import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
import { NavigationButton } from './NavigationButton';
|
import { NavigationButton } from './NavigationButton';
|
||||||
|
|
||||||
interface UserButtonProps {
|
interface UserButtonProps {
|
||||||
onLogin: () => void;
|
onLogin: () => void;
|
||||||
onClickUser: () => void;
|
onClickUser: () => void;
|
||||||
|
isOpen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserButton({ onLogin, onClickUser }: UserButtonProps) {
|
export function UserButton({ onLogin, onClickUser, isOpen }: UserButtonProps) {
|
||||||
const { user, isAnonymous } = useAuthSuspense();
|
const { user, isAnonymous } = useAuthSuspense();
|
||||||
const adminMode = usePreferencesStore(state => state.adminMode);
|
const adminMode = usePreferencesStore(state => state.adminMode);
|
||||||
if (isAnonymous) {
|
if (isAnonymous) {
|
||||||
|
@ -26,6 +28,11 @@ export function UserButton({ onLogin, onClickUser }: UserButtonProps) {
|
||||||
return (
|
return (
|
||||||
<NavigationButton
|
<NavigationButton
|
||||||
className='cc-fade-in'
|
className='cc-fade-in'
|
||||||
|
title='Пользователь'
|
||||||
|
hideTitle={isOpen}
|
||||||
|
aria-haspopup='true'
|
||||||
|
aria-expanded={isOpen}
|
||||||
|
aria-controls={globalIDs.user_dropdown}
|
||||||
icon={<IconUser2 size='1.5rem' className={adminMode && user.is_staff ? 'icon-primary' : ''} />}
|
icon={<IconUser2 size='1.5rem' className={adminMode && user.is_staff ? 'icon-primary' : ''} />}
|
||||||
onClick={onClickUser}
|
onClick={onClickUser}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
IconUser
|
IconUser
|
||||||
} from '@/components/Icons';
|
} from '@/components/Icons';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
|
import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
import { urls } from '../urls';
|
import { urls } from '../urls';
|
||||||
|
|
||||||
|
@ -75,7 +76,7 @@ export function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown className='mt-[1.5rem] min-w-[18ch] max-w-[12rem]' stretchLeft isOpen={isOpen}>
|
<Dropdown id={globalIDs.user_dropdown} className='min-w-[18ch] max-w-48' stretchLeft isOpen={isOpen}>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text={user.username}
|
text={user.username}
|
||||||
title='Профиль пользователя'
|
title='Профиль пользователя'
|
||||||
|
@ -104,7 +105,7 @@ export function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
|
||||||
) : null}
|
) : null}
|
||||||
{user.is_staff ? (
|
{user.is_staff ? (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='REST API' // prettier: split-line
|
text='REST API' //
|
||||||
icon={<IconRESTapi size='1rem' />}
|
icon={<IconRESTapi size='1rem' />}
|
||||||
className='border-t'
|
className='border-t'
|
||||||
onClick={gotoRestApi}
|
onClick={gotoRestApi}
|
||||||
|
@ -112,21 +113,21 @@ export function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
|
||||||
) : null}
|
) : null}
|
||||||
{user.is_staff ? (
|
{user.is_staff ? (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='База данных' // prettier: split-line
|
text='База данных' //
|
||||||
icon={<IconDatabase size='1rem' />}
|
icon={<IconDatabase size='1rem' />}
|
||||||
onClick={gotoAdmin}
|
onClick={gotoAdmin}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{user?.is_staff ? (
|
{user?.is_staff ? (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='Иконки' // prettier: split-line
|
text='Иконки' //
|
||||||
icon={<IconImage size='1rem' />}
|
icon={<IconImage size='1rem' />}
|
||||||
onClick={gotoIcons}
|
onClick={gotoIcons}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{user.is_staff ? (
|
{user.is_staff ? (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='Структура БД' // prettier: split-line
|
text='Структура БД' //
|
||||||
icon={<IconDBStructure size='1rem' />}
|
icon={<IconDBStructure size='1rem' />}
|
||||||
onClick={gotoDatabaseSchema}
|
onClick={gotoDatabaseSchema}
|
||||||
className='border-b'
|
className='border-b'
|
||||||
|
|
|
@ -13,9 +13,13 @@ export function UserMenu() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const menu = useDropdown();
|
const menu = useDropdown();
|
||||||
return (
|
return (
|
||||||
<div ref={menu.ref} className='h-full w-[4rem] flex items-center justify-center'>
|
<div ref={menu.ref} className='flex items-center justify-start relative h-full pr-2'>
|
||||||
<Suspense fallback={<Loader circular scale={1.5} />}>
|
<Suspense fallback={<Loader circular scale={1.5} />}>
|
||||||
<UserButton onLogin={() => router.push({ path: urls.login, force: true })} onClickUser={menu.toggle} />
|
<UserButton
|
||||||
|
onLogin={() => router.push({ path: urls.login, force: true })}
|
||||||
|
onClickUser={menu.toggle}
|
||||||
|
isOpen={menu.isOpen}
|
||||||
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<UserDropdown isOpen={menu.isOpen} hideDropdown={() => menu.hide()} />
|
<UserDropdown isOpen={menu.isOpen} hideDropdown={() => menu.hide()} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -17,7 +17,7 @@ export function Divider({ vertical, margins = 'mx-2', className, ...restProps }:
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
margins, //prettier: split-lines
|
margins, //
|
||||||
className,
|
className,
|
||||||
{
|
{
|
||||||
'border-x': vertical,
|
'border-x': vertical,
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* `flex` column container.
|
|
||||||
* This component is useful for creating vertical layouts with flexbox.
|
|
||||||
*/
|
|
||||||
export function FlexColumn({ className, children, ...restProps }: React.ComponentProps<'div'>) {
|
|
||||||
return (
|
|
||||||
<div className={clsx('cc-column', className)} {...restProps}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
import { type Styling } from '../props';
|
|
||||||
|
|
||||||
interface OverlayProps extends Styling {
|
|
||||||
/** Id of the overlay. */
|
|
||||||
id?: string;
|
|
||||||
|
|
||||||
/** Classnames for position of the overlay. */
|
|
||||||
position?: string;
|
|
||||||
|
|
||||||
/** Classname for z-index of the overlay. */
|
|
||||||
layer?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays a transparent overlay over the main content.
|
|
||||||
*/
|
|
||||||
export function Overlay({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
position = 'top-0 right-0',
|
|
||||||
layer = 'z-pop',
|
|
||||||
...restProps
|
|
||||||
}: React.PropsWithChildren<OverlayProps>) {
|
|
||||||
return (
|
|
||||||
<div className='relative'>
|
|
||||||
<div className={clsx('absolute', className, position, layer)} {...restProps}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -39,6 +39,7 @@ export function Tooltip({
|
||||||
delayHide={100}
|
delayHide={100}
|
||||||
opacity={1}
|
opacity={1}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
'relative',
|
||||||
'max-h-[calc(100svh-6rem)]',
|
'max-h-[calc(100svh-6rem)]',
|
||||||
'overflow-y-auto overflow-x-hidden sm:overflow-hidden overscroll-contain',
|
'overflow-y-auto overflow-x-hidden sm:overflow-hidden overscroll-contain',
|
||||||
'border shadow-md',
|
'border shadow-md',
|
||||||
|
|
|
@ -1,4 +1,2 @@
|
||||||
export { Divider } from './Divider';
|
export { Divider } from './Divider';
|
||||||
export { FlexColumn } from './FlexColumn';
|
|
||||||
export { Overlay } from './Overlay';
|
|
||||||
export { type PlacesType, Tooltip } from './Tooltip';
|
export { type PlacesType, Tooltip } from './Tooltip';
|
||||||
|
|
|
@ -41,7 +41,7 @@ export function Button({
|
||||||
disabled={disabled ?? loading}
|
disabled={disabled ?? loading}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'inline-flex gap-2 items-center justify-center',
|
'inline-flex gap-2 items-center justify-center',
|
||||||
'select-none disabled:cursor-auto',
|
'font-medium select-none disabled:cursor-auto',
|
||||||
'clr-btn-default cc-animate-color',
|
'clr-btn-default cc-animate-color',
|
||||||
{
|
{
|
||||||
'border rounded-sm': !noBorder,
|
'border rounded-sm': !noBorder,
|
||||||
|
@ -61,7 +61,7 @@ export function Button({
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{icon ? icon : null}
|
{icon ? icon : null}
|
||||||
{text ? <span className='font-medium'>{text}</span> : null}
|
{text ? <span>{text}</span> : null}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ export function SubmitButton({ text = 'ОК', icon, disabled, loading, className
|
||||||
disabled={disabled || loading}
|
disabled={disabled || loading}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{icon ? <span>{icon}</span> : null}
|
{icon ? icon : null}
|
||||||
{text ? <span>{text}</span> : null}
|
{text ? <span>{text}</span> : null}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { type Table } from '@tanstack/react-table';
|
import { type Table } from '@tanstack/react-table';
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
import { prefixes } from '@/utils/constants';
|
import { prefixes } from '@/utils/constants';
|
||||||
|
|
||||||
|
@ -34,7 +33,7 @@ export function PaginationTools<TData>({
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx('flex justify-end items-center', 'my-2', 'text-sm', 'clr-text-controls', 'select-none')}>
|
<div className='flex justify-end items-center my-2 text-sm clr-text-controls select-none'>
|
||||||
<span className='mr-3'>
|
<span className='mr-3'>
|
||||||
{`${table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1}
|
{`${table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1}
|
||||||
-
|
-
|
||||||
|
|
|
@ -14,7 +14,7 @@ export function SortingIcon<TData>({ column }: SortingIconProps<TData>) {
|
||||||
{{
|
{{
|
||||||
desc: <IconSortDesc size='1rem' />,
|
desc: <IconSortDesc size='1rem' />,
|
||||||
asc: <IconSortAsc size='1rem' />
|
asc: <IconSortAsc size='1rem' />
|
||||||
}[column.getIsSorted() as string] ?? <IconSortDesc size='1rem' className='opacity-0 hover:opacity-50' />}
|
}[column.getIsSorted() as string] ?? <IconSortDesc size='1rem' className='opacity-0 group-hover:opacity-25' />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,12 +72,12 @@ export function TableBody<TData>({
|
||||||
'cc-scroll-row',
|
'cc-scroll-row',
|
||||||
'clr-hover cc-animate-color',
|
'clr-hover cc-animate-color',
|
||||||
!noHeader && 'scroll-mt-[calc(2px+2rem)]',
|
!noHeader && 'scroll-mt-[calc(2px+2rem)]',
|
||||||
row.getIsSelected() ? 'clr-selected' : index % 2 === 0 ? 'bg-prim-200' : 'bg-prim-100'
|
row.getIsSelected() ? 'clr-selected' : 'odd:bg-prim-200 even:bg-prim-100'
|
||||||
)}
|
)}
|
||||||
style={{ ...(conditionalRowStyles ? getRowStyles(row) : []) }}
|
style={{ ...(conditionalRowStyles ? getRowStyles(row) : []) }}
|
||||||
>
|
>
|
||||||
{enableRowSelection ? (
|
{enableRowSelection ? (
|
||||||
<td key={`select-${row.id}`} className='pl-3 pr-1 align-middle border-y'>
|
<td key={`select-${row.id}`} className='pl-3 pr-1 border-y'>
|
||||||
<SelectRow row={row} onChangeLastSelected={onChangeLastSelected} />
|
<SelectRow row={row} onChangeLastSelected={onChangeLastSelected} />
|
||||||
</td>
|
</td>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -31,7 +31,7 @@ export function TableHeader<TData>({
|
||||||
{table.getHeaderGroups().map((headerGroup: HeaderGroup<TData>) => (
|
{table.getHeaderGroups().map((headerGroup: HeaderGroup<TData>) => (
|
||||||
<tr key={headerGroup.id}>
|
<tr key={headerGroup.id}>
|
||||||
{enableRowSelection ? (
|
{enableRowSelection ? (
|
||||||
<th className='pl-3 pr-1 align-middle'>
|
<th className='pl-3 pr-1' scope='col'>
|
||||||
<SelectAll table={table} resetLastSelected={resetLastSelected} />
|
<SelectAll table={table} resetLastSelected={resetLastSelected} />
|
||||||
</th>
|
</th>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -39,17 +39,16 @@ export function TableHeader<TData>({
|
||||||
<th
|
<th
|
||||||
key={header.id}
|
key={header.id}
|
||||||
colSpan={header.colSpan}
|
colSpan={header.colSpan}
|
||||||
className='pl-2 py-2 text-xs font-medium select-none whitespace-nowrap'
|
scope='col'
|
||||||
|
className='cc-table-header group'
|
||||||
style={{
|
style={{
|
||||||
paddingRight: enableSorting && header.column.getCanSort() ? '0px' : '2px',
|
|
||||||
textAlign: 'start',
|
|
||||||
width: `calc(var(--header-${header?.id}-size) * 1px)`,
|
width: `calc(var(--header-${header?.id}-size) * 1px)`,
|
||||||
cursor: enableSorting && header.column.getCanSort() ? 'pointer' : 'auto'
|
cursor: enableSorting && header.column.getCanSort() ? 'pointer' : 'auto'
|
||||||
}}
|
}}
|
||||||
onClick={enableSorting ? header.column.getToggleSortingHandler() : undefined}
|
onClick={enableSorting ? header.column.getToggleSortingHandler() : undefined}
|
||||||
>
|
>
|
||||||
{!header.isPlaceholder ? (
|
{!header.isPlaceholder ? (
|
||||||
<span className='inline-flex align-middle gap-1'>
|
<span className='inline-flex gap-1'>
|
||||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
{enableSorting && header.column.getCanSort() ? <SortingIcon column={header.column} /> : null}
|
{enableSorting && header.column.getCanSort() ? <SortingIcon column={header.column} /> : null}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import React from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
@ -5,6 +6,15 @@ import { PARAMETER } from '@/utils/constants';
|
||||||
import { type Styling } from '../props';
|
import { type Styling } from '../props';
|
||||||
|
|
||||||
interface DropdownProps extends Styling {
|
interface DropdownProps extends Styling {
|
||||||
|
/** Reference to the dropdown element. */
|
||||||
|
ref?: React.Ref<HTMLDivElement>;
|
||||||
|
|
||||||
|
/** Unique ID for the dropdown. */
|
||||||
|
id?: string;
|
||||||
|
|
||||||
|
/** Margin for the dropdown. */
|
||||||
|
margin?: string;
|
||||||
|
|
||||||
/** Indicates whether the dropdown should stretch to the left. */
|
/** Indicates whether the dropdown should stretch to the left. */
|
||||||
stretchLeft?: boolean;
|
stretchLeft?: boolean;
|
||||||
|
|
||||||
|
@ -17,32 +27,34 @@ interface DropdownProps extends Styling {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Animated list of children with optional positioning and visibility control.
|
* Animated list of children with optional positioning and visibility control.
|
||||||
|
* Note: Dropdown should be inside a relative container.
|
||||||
*/
|
*/
|
||||||
export function Dropdown({
|
export function Dropdown({
|
||||||
isOpen,
|
isOpen,
|
||||||
stretchLeft,
|
stretchLeft,
|
||||||
stretchTop,
|
stretchTop,
|
||||||
|
margin,
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
style,
|
style,
|
||||||
...restProps
|
...restProps
|
||||||
}: React.PropsWithChildren<DropdownProps>) {
|
}: React.PropsWithChildren<DropdownProps>) {
|
||||||
return (
|
return (
|
||||||
<div className='relative'>
|
|
||||||
<div
|
<div
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'z-topmost',
|
'z-topmost absolute',
|
||||||
'absolute mt-3',
|
|
||||||
'flex flex-col',
|
|
||||||
'border rounded-md shadow-lg',
|
|
||||||
'text-sm',
|
|
||||||
'clr-input',
|
|
||||||
{
|
{
|
||||||
'right-0': stretchLeft,
|
'right-0': stretchLeft,
|
||||||
'left-0': !stretchLeft,
|
'left-0': !stretchLeft,
|
||||||
'bottom-[2rem]': stretchTop
|
'bottom-0': stretchTop,
|
||||||
|
'top-full': !stretchTop
|
||||||
},
|
},
|
||||||
|
'grid',
|
||||||
|
'border rounded-md shadow-lg',
|
||||||
|
'clr-input',
|
||||||
|
'text-sm',
|
||||||
|
margin,
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
|
@ -54,10 +66,10 @@ export function Dropdown({
|
||||||
clipPath: isOpen ? 'inset(0% 0% 0% 0%)' : 'inset(10% 0% 90% 0%)',
|
clipPath: isOpen ? 'inset(0% 0% 0% 0%)' : 'inset(10% 0% 90% 0%)',
|
||||||
...style
|
...style
|
||||||
}}
|
}}
|
||||||
|
aria-hidden={!isOpen}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,9 +52,9 @@ export function DropdownButton({
|
||||||
data-tooltip-hidden={hideTitle}
|
data-tooltip-hidden={hideTitle}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
|
{icon ? icon : null}
|
||||||
|
{text ? <span>{text}</span> : null}
|
||||||
{children ? children : null}
|
{children ? children : null}
|
||||||
{!children && icon ? icon : null}
|
|
||||||
{!children && text ? <span>{text}</span> : null}
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -171,16 +171,9 @@ export interface IconProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MetaIconSVG({ viewBox, size = '1.5rem', className, props, children }: React.PropsWithChildren<IconSVGProps>) {
|
function MetaIconSVG({ viewBox, size = '1.5rem', props, children }: React.PropsWithChildren<IconSVGProps>) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg width={size} height={size} fill='currentColor' viewBox={viewBox} {...props}>
|
||||||
width={size}
|
|
||||||
height={size}
|
|
||||||
className={`w-[${size}] h-[${size}] ${className}`}
|
|
||||||
fill='currentColor'
|
|
||||||
viewBox={viewBox}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
@ -205,7 +198,7 @@ export function IconLogin(props: IconProps) {
|
||||||
|
|
||||||
export function CheckboxChecked() {
|
export function CheckboxChecked() {
|
||||||
return (
|
return (
|
||||||
<svg className='w-3 h-3' viewBox='0 0 512 512' fill='#ffffff'>
|
<svg className='w-4 h-4 p-0.75' viewBox='0 0 512 512' fill='#ffffff'>
|
||||||
<path d='M470.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 338.7l233.4-233.3c12.5-12.5 32.8-12.5 45.3 0z' />
|
<path d='M470.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 338.7l233.4-233.3c12.5-12.5 32.8-12.5 45.3 0z' />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
@ -213,7 +206,7 @@ export function CheckboxChecked() {
|
||||||
|
|
||||||
export function CheckboxNull() {
|
export function CheckboxNull() {
|
||||||
return (
|
return (
|
||||||
<svg className='w-3 h-3' viewBox='0 0 16 16' fill='#ffffff'>
|
<svg className='w-4 h-4 p-0.25' viewBox='0 0 16 16' fill='#ffffff'>
|
||||||
<path d='M2 7.75A.75.75 0 012.75 7h10a.75.75 0 010 1.5h-10A.75.75 0 012 7.75z' />
|
<path d='M2 7.75A.75.75 0 012.75 7h10a.75.75 0 010 1.5h-10A.75.75 0 012 7.75z' />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
|
@ -90,7 +90,7 @@ export function InfoError({ error }: InfoErrorProps) {
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'cc-fade-in',
|
'cc-fade-in',
|
||||||
'min-w-[25rem]',
|
'min-w-100',
|
||||||
'px-3 py-2 flex flex-col',
|
'px-3 py-2 flex flex-col',
|
||||||
'text-warn-600 text-sm font-semibold',
|
'text-warn-600 text-sm font-semibold',
|
||||||
'select-text'
|
'select-text'
|
||||||
|
|
|
@ -64,10 +64,8 @@ export function Checkbox({
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'max-w-[1rem] min-w-[1rem] h-4', //
|
'w-4 h-4', //
|
||||||
'pt-[0.05rem] pl-[0.05rem]',
|
|
||||||
'border rounded-xs',
|
'border rounded-xs',
|
||||||
'cc-animate-color',
|
|
||||||
{
|
{
|
||||||
'bg-sec-600 text-sec-0': value !== false,
|
'bg-sec-600 text-sec-0': value !== false,
|
||||||
'bg-prim-100': value === false
|
'bg-prim-100': value === false
|
||||||
|
|
|
@ -66,9 +66,7 @@ export function CheckboxTristate({
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-4 h-4', //
|
'w-4 h-4', //
|
||||||
'pt-[0.05rem] pl-[0.05rem]',
|
|
||||||
'border rounded-xs',
|
'border rounded-xs',
|
||||||
'cc-animate-color',
|
|
||||||
{
|
{
|
||||||
'bg-sec-600 text-sec-0': value !== false,
|
'bg-sec-600 text-sec-0': value !== false,
|
||||||
'bg-prim-100': value === false
|
'bg-prim-100': value === false
|
||||||
|
|
|
@ -41,7 +41,7 @@ export function FileInput({ id, label, acceptType, title, className, style, onCh
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx('py-2', 'flex flex-col gap-2 items-center', className)} style={style}>
|
<div className={clsx('py-2 flex flex-col gap-2 items-center', className)} style={style}>
|
||||||
<input
|
<input
|
||||||
id={id}
|
id={id}
|
||||||
type='file'
|
type='file'
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { Overlay } from '@/components/Container';
|
|
||||||
import { IconSearch } from '@/components/Icons';
|
import { IconSearch } from '@/components/Icons';
|
||||||
import { type Styling } from '@/components/props';
|
import { type Styling } from '@/components/props';
|
||||||
|
|
||||||
|
@ -35,15 +34,17 @@ export function SearchBar({
|
||||||
noIcon,
|
noIcon,
|
||||||
onChangeQuery,
|
onChangeQuery,
|
||||||
noBorder,
|
noBorder,
|
||||||
|
className,
|
||||||
placeholder = 'Поиск',
|
placeholder = 'Поиск',
|
||||||
...restProps
|
...restProps
|
||||||
}: SearchBarProps) {
|
}: SearchBarProps) {
|
||||||
return (
|
return (
|
||||||
<div {...restProps}>
|
<div className={clsx('relative', className)} {...restProps}>
|
||||||
{!noIcon ? (
|
{!noIcon ? (
|
||||||
<Overlay position='top-[-0.125rem] left-3 translate-y-1/2' className='pointer-events-none clr-text-controls'>
|
<IconSearch
|
||||||
<IconSearch size='1.25rem' />
|
className='absolute -top-0.5 left-3 translate-y-1/2 pointer-events-none clr-text-controls'
|
||||||
</Overlay>
|
size='1.25rem'
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<TextInput
|
<TextInput
|
||||||
id={id}
|
id={id}
|
||||||
|
|
|
@ -109,7 +109,7 @@ export function SelectMulti<Option, Group extends GroupBase<Option> = GroupBase<
|
||||||
...theme,
|
...theme,
|
||||||
borderRadius: 0,
|
borderRadius: 0,
|
||||||
spacing: {
|
spacing: {
|
||||||
...theme.spacing, // prettier: split-lines
|
...theme.spacing,
|
||||||
baseUnit: size.isSmall ? 2 : 4,
|
baseUnit: size.isSmall ? 2 : 4,
|
||||||
menuGutter: size.isSmall ? 4 : 8,
|
menuGutter: size.isSmall ? 4 : 8,
|
||||||
controlHeight: size.isSmall ? 28 : 38
|
controlHeight: size.isSmall ? 28 : 38
|
||||||
|
|
|
@ -107,7 +107,7 @@ export function SelectSingle<Option, Group extends GroupBase<Option> = GroupBase
|
||||||
...theme,
|
...theme,
|
||||||
borderRadius: 0,
|
borderRadius: 0,
|
||||||
spacing: {
|
spacing: {
|
||||||
...theme.spacing, // prettier: split-lines
|
...theme.spacing,
|
||||||
baseUnit: size.isSmall ? 2 : 4,
|
baseUnit: size.isSmall ? 2 : 4,
|
||||||
menuGutter: 2,
|
menuGutter: 2,
|
||||||
controlHeight: size.isSmall ? 28 : 38
|
controlHeight: size.isSmall ? 28 : 38
|
||||||
|
|
|
@ -3,7 +3,6 @@ import clsx from 'clsx';
|
||||||
|
|
||||||
import { globalIDs, PARAMETER } from '@/utils/constants';
|
import { globalIDs, PARAMETER } from '@/utils/constants';
|
||||||
|
|
||||||
import { Overlay } from '../Container';
|
|
||||||
import { MiniButton } from '../Control';
|
import { MiniButton } from '../Control';
|
||||||
import { IconDropArrow, IconPageRight } from '../Icons';
|
import { IconDropArrow, IconPageRight } from '../Icons';
|
||||||
import { type Styling } from '../props';
|
import { type Styling } from '../props';
|
||||||
|
@ -51,13 +50,13 @@ export function SelectTree<ItemType>({
|
||||||
setFolded(items.filter(item => getParent(value) !== item && getParent(getParent(value)) !== item));
|
setFolded(items.filter(item => getParent(value) !== item && getParent(getParent(value)) !== item));
|
||||||
}, [value, getParent, items]);
|
}, [value, getParent, items]);
|
||||||
|
|
||||||
function onFoldItem(target: ItemType, showChildren: boolean) {
|
function onFoldItem(target: ItemType) {
|
||||||
setFolded(prev =>
|
setFolded(prev =>
|
||||||
items.filter(item => {
|
items.filter(item => {
|
||||||
if (item === target) {
|
if (item === target) {
|
||||||
return !showChildren;
|
return !prev.includes(target);
|
||||||
}
|
}
|
||||||
if (!showChildren && (getParent(item) === target || getParent(getParent(item)) === target)) {
|
if (!prev.includes(target) && (getParent(item) === target || getParent(getParent(item)) === target)) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return prev.includes(item);
|
return prev.includes(item);
|
||||||
|
@ -66,17 +65,21 @@ export function SelectTree<ItemType>({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClickFold(event: React.MouseEvent<Element>, target: ItemType, showChildren: boolean) {
|
function handleClickFold(event: React.MouseEvent<Element>, target: ItemType) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
onFoldItem(target, showChildren);
|
onFoldItem(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSetValue(event: React.MouseEvent<Element>, target: ItemType) {
|
function handleClickItem(event: React.MouseEvent<Element>, target: ItemType) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
if (event.ctrlKey || event.metaKey) {
|
||||||
|
onFoldItem(target);
|
||||||
|
} else {
|
||||||
onChange(target);
|
onChange(target);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div tabIndex={-1} {...restProps}>
|
<div tabIndex={-1} {...restProps}>
|
||||||
|
@ -86,6 +89,7 @@ export function SelectTree<ItemType>({
|
||||||
<div
|
<div
|
||||||
key={`${prefix}${index}`}
|
key={`${prefix}${index}`}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
'relative',
|
||||||
'pr-3 pl-6 border-b',
|
'pr-3 pl-6 border-b',
|
||||||
'cc-scroll-row',
|
'cc-scroll-row',
|
||||||
'bg-prim-200 clr-hover cc-animate-color',
|
'bg-prim-200 clr-hover cc-animate-color',
|
||||||
|
@ -95,7 +99,7 @@ export function SelectTree<ItemType>({
|
||||||
)}
|
)}
|
||||||
data-tooltip-id={globalIDs.tooltip}
|
data-tooltip-id={globalIDs.tooltip}
|
||||||
data-tooltip-html={getDescription(item)}
|
data-tooltip-html={getDescription(item)}
|
||||||
onClick={event => handleSetValue(event, item)}
|
onClick={event => handleClickItem(event, item)}
|
||||||
style={{
|
style={{
|
||||||
borderBottomWidth: isActive ? '1px' : '0px',
|
borderBottomWidth: isActive ? '1px' : '0px',
|
||||||
willChange: 'max-height, opacity, padding',
|
willChange: 'max-height, opacity, padding',
|
||||||
|
@ -108,14 +112,13 @@ export function SelectTree<ItemType>({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{foldable.has(item) ? (
|
{foldable.has(item) ? (
|
||||||
<Overlay position='left-[-1.3rem]' className={clsx(!folded.includes(item) && 'top-[0.1rem]')}>
|
|
||||||
<MiniButton
|
<MiniButton
|
||||||
|
className={clsx('absolute left-1', !folded.includes(item) ? 'top-1.5' : 'top-1')}
|
||||||
noPadding
|
noPadding
|
||||||
noHover
|
noHover
|
||||||
icon={!folded.includes(item) ? <IconDropArrow size='1rem' /> : <IconPageRight size='1.25rem' />}
|
icon={!folded.includes(item) ? <IconDropArrow size='1rem' /> : <IconPageRight size='1.25rem' />}
|
||||||
onClick={event => handleClickFold(event, item, folded.includes(item))}
|
onClick={event => handleClickFold(event, item)}
|
||||||
/>
|
/>
|
||||||
</Overlay>
|
|
||||||
) : null}
|
) : null}
|
||||||
{getParent(item) === item ? getLabel(item) : `- ${getLabel(item).toLowerCase()}`}
|
{getParent(item) === item ? getLabel(item) : `- ${getLabel(item).toLowerCase()}`}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,19 +1,14 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
interface ModalBackdropProps {
|
interface ModalBackdropProps {
|
||||||
onHide: () => void;
|
onHide?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ModalBackdrop({ onHide }: ModalBackdropProps) {
|
export function ModalBackdrop({ onHide }: ModalBackdropProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={clsx('z-navigation', 'fixed top-0 left-0', 'w-full h-full', 'backdrop-blur-[3px] opacity-50')} />
|
<div className='z-bottom fixed inset-0 backdrop-blur-[3px] opacity-50' />
|
||||||
<div
|
<div className='z-bottom fixed inset-0 bg-prim-0 opacity-25' onClick={onHide} />
|
||||||
className={clsx('z-navigation', 'fixed top-0 left-0', 'w-full h-full', 'bg-prim-0 opacity-25')}
|
|
||||||
onClick={onHide}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,10 +7,8 @@ import { BadgeHelp } from '@/features/help/components';
|
||||||
|
|
||||||
import { useEscapeKey } from '@/hooks/useEscapeKey';
|
import { useEscapeKey } from '@/hooks/useEscapeKey';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
|
||||||
import { prepareTooltip } from '@/utils/utils';
|
import { prepareTooltip } from '@/utils/utils';
|
||||||
|
|
||||||
import { Overlay } from '../Container';
|
|
||||||
import { Button, MiniButton, SubmitButton } from '../Control';
|
import { Button, MiniButton, SubmitButton } from '../Control';
|
||||||
import { IconClose } from '../Icons';
|
import { IconClose } from '../Icons';
|
||||||
import { type Styling } from '../props';
|
import { type Styling } from '../props';
|
||||||
|
@ -89,37 +87,43 @@ export function ModalForm({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='fixed top-0 left-0 w-full h-full z-modal cursor-default'>
|
<div className='cc-modal-wrapper'>
|
||||||
<ModalBackdrop onHide={handleCancel} />
|
<ModalBackdrop onHide={handleCancel} />
|
||||||
<form
|
<form
|
||||||
className={clsx(
|
className='cc-animate-modal grid border rounded-xl bg-prim-100'
|
||||||
'cc-animate-modal',
|
role='dialog'
|
||||||
'z-modal absolute bottom-1/2 left-1/2 -translate-x-1/2 translate-y-1/2',
|
|
||||||
'border rounded-xl bg-prim-100'
|
|
||||||
)}
|
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
|
aria-labelledby='modal-title'
|
||||||
>
|
>
|
||||||
{helpTopic && !hideHelpWhen?.() ? (
|
{helpTopic && !hideHelpWhen?.() ? (
|
||||||
<div className='float-left mt-2 ml-2'>
|
<BadgeHelp
|
||||||
<BadgeHelp topic={helpTopic} className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')} padding='p-0' />
|
topic={helpTopic}
|
||||||
</div>
|
className='absolute z-pop left-0 mt-2 ml-2'
|
||||||
|
padding='p-0'
|
||||||
|
contentClass='sm:max-w-160'
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Overlay className='z-modalOverlay'>
|
|
||||||
<MiniButton
|
<MiniButton
|
||||||
noPadding
|
noPadding
|
||||||
|
aria-label='Закрыть'
|
||||||
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
|
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
|
||||||
icon={<IconClose size='1.25rem' />}
|
icon={<IconClose size='1.25rem' />}
|
||||||
className='float-right mt-2 mr-2'
|
className='absolute z-pop right-0 mt-2 mr-2'
|
||||||
onClick={handleCancel}
|
onClick={hideDialog}
|
||||||
/>
|
/>
|
||||||
</Overlay>
|
|
||||||
|
|
||||||
{header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null}
|
{header ? (
|
||||||
|
<h1 id='modal-title' className='px-12 py-2 select-none'>
|
||||||
|
{header}
|
||||||
|
</h1>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'overscroll-contain max-h-[calc(100svh-8rem)] max-w-[100svw] xs:max-w-[calc(100svw-2rem)] outline-hidden',
|
'@container/modal',
|
||||||
|
'max-h-[calc(100svh-8rem)] max-w-[100svw] xs:max-w-[calc(100svw-2rem)]',
|
||||||
|
'overscroll-contain outline-hidden',
|
||||||
{
|
{
|
||||||
'overflow-auto': !overflowVisible,
|
'overflow-auto': !overflowVisible,
|
||||||
'overflow-visible': overflowVisible
|
'overflow-visible': overflowVisible
|
||||||
|
@ -131,15 +135,15 @@ export function ModalForm({
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='z-modal-controls my-2 flex gap-12 justify-center text-sm'>
|
<div className='z-pop my-2 flex gap-12 justify-center text-sm'>
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
autoFocus
|
autoFocus
|
||||||
text={submitText}
|
text={submitText}
|
||||||
title={!canSubmit ? submitInvalidTooltip : ''}
|
title={!canSubmit ? submitInvalidTooltip : ''}
|
||||||
className='min-w-[7rem]'
|
className='min-w-28'
|
||||||
disabled={!canSubmit}
|
disabled={!canSubmit}
|
||||||
/>
|
/>
|
||||||
<Button text='Отмена' className='min-w-[7rem]' onClick={handleCancel} />
|
<Button text='Отмена' aria-label='Закрыть' className='min-w-28' onClick={handleCancel} />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,19 +1,12 @@
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
import { Loader } from '@/components/Loader';
|
import { Loader } from '@/components/Loader';
|
||||||
|
|
||||||
|
import { ModalBackdrop } from './ModalBackdrop';
|
||||||
|
|
||||||
export function ModalLoader() {
|
export function ModalLoader() {
|
||||||
return (
|
return (
|
||||||
<div className='fixed top-0 left-0 w-full h-full z-modal cursor-default'>
|
<div className='cc-modal-wrapper'>
|
||||||
<div className={clsx('z-navigation', 'fixed top-0 left-0', 'w-full h-full', 'backdrop-blur-[3px] opacity-50')} />
|
<ModalBackdrop />
|
||||||
<div className={clsx('z-navigation', 'fixed top-0 left-0', 'w-full h-full', 'bg-prim-0 opacity-25')} />
|
<div className='cc-animate-modal p-20 border rounded-xl bg-prim-100'>
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
'cc-animate-modal p-20',
|
|
||||||
'z-modal absolute bottom-1/2 left-1/2 -translate-x-1/2 translate-y-1/2',
|
|
||||||
'border rounded-xl bg-prim-100'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Loader circular scale={6} />
|
<Loader circular scale={6} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,10 +6,8 @@ import { BadgeHelp } from '@/features/help/components';
|
||||||
|
|
||||||
import { useEscapeKey } from '@/hooks/useEscapeKey';
|
import { useEscapeKey } from '@/hooks/useEscapeKey';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
|
||||||
import { prepareTooltip } from '@/utils/utils';
|
import { prepareTooltip } from '@/utils/utils';
|
||||||
|
|
||||||
import { Overlay } from '../Container';
|
|
||||||
import { Button, MiniButton } from '../Control';
|
import { Button, MiniButton } from '../Control';
|
||||||
import { IconClose } from '../Icons';
|
import { IconClose } from '../Icons';
|
||||||
|
|
||||||
|
@ -34,36 +32,34 @@ export function ModalView({
|
||||||
useEscapeKey(hideDialog);
|
useEscapeKey(hideDialog);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='fixed top-0 left-0 w-full h-full z-modal cursor-default'>
|
<div className='cc-modal-wrapper'>
|
||||||
<ModalBackdrop onHide={hideDialog} />
|
<ModalBackdrop onHide={hideDialog} />
|
||||||
<div
|
<div className='cc-animate-modal grid border rounded-xl bg-prim-100' role='dialog'>
|
||||||
className={clsx(
|
|
||||||
'cc-animate-modal',
|
|
||||||
'z-modal absolute bottom-1/2 left-1/2 -translate-x-1/2 translate-y-1/2',
|
|
||||||
'border rounded-xl bg-prim-100'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{helpTopic && !hideHelpWhen?.() ? (
|
{helpTopic && !hideHelpWhen?.() ? (
|
||||||
<div className='float-left mt-2 ml-2'>
|
<BadgeHelp
|
||||||
<BadgeHelp topic={helpTopic} className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')} padding='p-0' />
|
topic={helpTopic}
|
||||||
</div>
|
className='absolute z-pop left-0 mt-2 ml-2'
|
||||||
|
padding='p-0'
|
||||||
|
contentClass='sm:max-w-160'
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Overlay className='z-modalOverlay'>
|
|
||||||
<MiniButton
|
<MiniButton
|
||||||
noPadding
|
noPadding
|
||||||
|
aria-label='Закрыть'
|
||||||
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
|
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
|
||||||
icon={<IconClose size='1.25rem' />}
|
icon={<IconClose size='1.25rem' />}
|
||||||
className='float-right mt-2 mr-2'
|
className='absolute z-pop right-0 mt-2 mr-2'
|
||||||
onClick={hideDialog}
|
onClick={hideDialog}
|
||||||
/>
|
/>
|
||||||
</Overlay>
|
|
||||||
|
|
||||||
{header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null}
|
{header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'overscroll-contain max-h-[calc(100svh-8rem)] max-w-[100svw] xs:max-w-[calc(100svw-2rem)] outline-hidden',
|
'@container/modal',
|
||||||
|
'max-h-[calc(100svh-8rem)] max-w-[100svw] xs:max-w-[calc(100svw-2rem)]',
|
||||||
|
'overscroll-contain outline-hidden',
|
||||||
{
|
{
|
||||||
'overflow-auto': !overflowVisible,
|
'overflow-auto': !overflowVisible,
|
||||||
'overflow-visible': overflowVisible
|
'overflow-visible': overflowVisible
|
||||||
|
@ -75,9 +71,12 @@ export function ModalView({
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='z-modal-controls my-2 flex gap-12 justify-center text-sm'>
|
<Button
|
||||||
<Button text='Закрыть' className='min-w-[7rem]' onClick={hideDialog} />
|
text='Закрыть'
|
||||||
</div>
|
aria-label='Закрыть'
|
||||||
|
className='z-pop my-2 mx-auto text-sm min-w-28'
|
||||||
|
onClick={hideDialog}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -18,7 +18,7 @@ export function TabLabel({ label, title, titleHtml, hideTitle, className, ...oth
|
||||||
return (
|
return (
|
||||||
<TabImpl
|
<TabImpl
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'min-w-[5.5rem] h-full',
|
'min-w-20 h-full',
|
||||||
'px-2 py-1 flex justify-center',
|
'px-2 py-1 flex justify-center',
|
||||||
'clr-hover cc-animate-color duration-150',
|
'clr-hover cc-animate-color duration-150',
|
||||||
'text-sm whitespace-nowrap font-controls',
|
'text-sm whitespace-nowrap font-controls',
|
||||||
|
|
|
@ -20,5 +20,5 @@ interface ValueStatsProps extends Styling, Titled {
|
||||||
* Displays statistics value with an icon.
|
* Displays statistics value with an icon.
|
||||||
*/
|
*/
|
||||||
export function ValueStats(props: ValueStatsProps) {
|
export function ValueStats(props: ValueStatsProps) {
|
||||||
return <ValueIcon dense smallThreshold={SMALL_THRESHOLD} textClassName='min-w-[1.4rem]' {...props} />;
|
return <ValueIcon dense smallThreshold={SMALL_THRESHOLD} textClassName='min-w-5' {...props} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ export interface ICurrentUser {
|
||||||
/**
|
/**
|
||||||
* Represents login data, used to authenticate users.
|
* Represents login data, used to authenticate users.
|
||||||
*/
|
*/
|
||||||
export const schemaUserLogin = z.object({
|
export const schemaUserLogin = z.strictObject({
|
||||||
username: z.string().nonempty(errorMsg.requiredField),
|
username: z.string().nonempty(errorMsg.requiredField),
|
||||||
password: z.string().nonempty(errorMsg.requiredField)
|
password: z.string().nonempty(errorMsg.requiredField)
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
import { urls, useConceptNavigation } from '@/app';
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
|
|
||||||
|
@ -56,11 +55,11 @@ export function LoginPage() {
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className={clsx('cc-column cc-fade-in', 'w-[24rem] mx-auto', 'pt-12 pb-6 px-6')}
|
className='cc-column cc-fade-in w-96 mx-auto pt-12 pb-6 px-6'
|
||||||
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
||||||
onChange={resetErrors}
|
onChange={resetErrors}
|
||||||
>
|
>
|
||||||
<img alt='Концепт Портал' src={resources.logo} className='max-h-[2.5rem] min-w-[2.5rem] mb-3' />
|
<img alt='Концепт Портал' src={resources.logo} className='max-h-10 min-w-10 mb-3' />
|
||||||
<TextInput
|
<TextInput
|
||||||
id='username'
|
id='username'
|
||||||
autoComplete='username'
|
autoComplete='username'
|
||||||
|
@ -82,7 +81,7 @@ export function LoginPage() {
|
||||||
error={errors.password}
|
error={errors.password}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SubmitButton text='Войти' className='self-center w-[12rem] mt-3' loading={isPending} />
|
<SubmitButton text='Войти' className='self-center w-48 mt-3' loading={isPending} />
|
||||||
<div className='flex flex-col text-sm'>
|
<div className='flex flex-col text-sm'>
|
||||||
<TextURL text='Восстановить пароль...' href='/restore-password' />
|
<TextURL text='Восстановить пароль...' href='/restore-password' />
|
||||||
<TextURL text='Нет аккаунта? Зарегистрируйтесь...' href='/signup' />
|
<TextURL text='Нет аккаунта? Зарегистрируйтесь...' href='/signup' />
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
import { urls, useConceptNavigation } from '@/app';
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
|
|
||||||
|
@ -51,7 +50,7 @@ export function Component() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className={clsx('cc-fade-in cc-column', 'w-[24rem] mx-auto', 'px-6 mt-3')} onSubmit={handleSubmit}>
|
<form className='cc-fade-in cc-column w-96 mx-auto px-6 mt-3' onSubmit={handleSubmit}>
|
||||||
<TextInput
|
<TextInput
|
||||||
id='new_password'
|
id='new_password'
|
||||||
type='password'
|
type='password'
|
||||||
|
@ -77,7 +76,7 @@ export function Component() {
|
||||||
|
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
text='Установить пароль'
|
text='Установить пароль'
|
||||||
className='self-center w-[12rem] mt-3'
|
className='self-center w-48 mt-3'
|
||||||
loading={isPending}
|
loading={isPending}
|
||||||
disabled={!canSubmit}
|
disabled={!canSubmit}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
import { isAxiosError } from '@/backend/apiTransport';
|
import { isAxiosError } from '@/backend/apiTransport';
|
||||||
import { SubmitButton, TextURL } from '@/components/Control';
|
import { SubmitButton, TextURL } from '@/components/Control';
|
||||||
|
@ -33,11 +32,7 @@ export function Component() {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<form
|
<form className='cc-fade-in cc-column w-96 mx-auto px-6 mt-3' onSubmit={handleSubmit} onChange={clearServerError}>
|
||||||
className={clsx('cc-fade-in cc-column', 'w-[24rem] mx-auto', 'px-6 mt-3')}
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
onChange={clearServerError}
|
|
||||||
>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
id='email'
|
id='email'
|
||||||
autoComplete='email'
|
autoComplete='email'
|
||||||
|
@ -48,12 +43,7 @@ export function Component() {
|
||||||
onChange={event => setEmail(event.target.value)}
|
onChange={event => setEmail(event.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SubmitButton
|
<SubmitButton text='Запросить пароль' className='self-center w-48 mt-3' loading={isPending} disabled={!email} />
|
||||||
text='Запросить пароль'
|
|
||||||
className='self-center w-[12rem] mt-3'
|
|
||||||
loading={isPending}
|
|
||||||
disabled={!email}
|
|
||||||
/>
|
|
||||||
{serverError ? <ServerError error={serverError} /> : null}
|
{serverError ? <ServerError error={serverError} /> : null}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { Suspense } from 'react';
|
import React, { Suspense } from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { type PlacesType, Tooltip } from '@/components/Container';
|
import { type PlacesType, Tooltip } from '@/components/Container';
|
||||||
import { TextURL } from '@/components/Control';
|
import { TextURL } from '@/components/Control';
|
||||||
|
@ -6,6 +7,7 @@ import { IconHelp } from '@/components/Icons';
|
||||||
import { Loader } from '@/components/Loader';
|
import { Loader } from '@/components/Loader';
|
||||||
import { type Styling } from '@/components/props';
|
import { type Styling } from '@/components/props';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
|
||||||
import { type HelpTopic } from '../models/helpTopic';
|
import { type HelpTopic } from '../models/helpTopic';
|
||||||
|
|
||||||
|
@ -25,27 +27,34 @@ interface BadgeHelpProps extends Styling {
|
||||||
|
|
||||||
/** Place of the tooltip in relation to the cursor. */
|
/** Place of the tooltip in relation to the cursor. */
|
||||||
place?: PlacesType;
|
place?: PlacesType;
|
||||||
|
|
||||||
|
/** Classname for content wrapper. */
|
||||||
|
contentClass?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display help icon with a manual page tooltip.
|
* Display help icon with a manual page tooltip.
|
||||||
*/
|
*/
|
||||||
export function BadgeHelp({ topic, padding = 'p-1', ...restProps }: BadgeHelpProps) {
|
export function BadgeHelp({ topic, padding = 'p-1', className, contentClass, style, ...restProps }: BadgeHelpProps) {
|
||||||
const showHelp = usePreferencesStore(state => state.showHelp);
|
const showHelp = usePreferencesStore(state => state.showHelp);
|
||||||
|
|
||||||
if (!showHelp) {
|
if (!showHelp) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div tabIndex={-1} id={`help-${topic}`} className={padding}>
|
<div tabIndex={-1} id={`help-${topic}`} className={clsx(padding, className)} style={style}>
|
||||||
<IconHelp size='1.25rem' className='icon-primary' />
|
<IconHelp size='1.25rem' className='icon-primary' />
|
||||||
<Tooltip clickable anchorSelect={`#help-${topic}`} layer='z-modal-tooltip' {...restProps}>
|
<Tooltip
|
||||||
|
clickable
|
||||||
|
anchorSelect={`#help-${topic}`}
|
||||||
|
layer='z-topmost'
|
||||||
|
className={clsx(PARAMETER.TOOLTIP_WIDTH, contentClass)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
<Suspense fallback={<Loader />}>
|
<Suspense fallback={<Loader />}>
|
||||||
<div className='relative' onClick={event => event.stopPropagation()}>
|
<div className='absolute right-1 text-sm top-2 clr-input' onClick={event => event.stopPropagation()}>
|
||||||
<div className='absolute right-0 text-sm top-[0.4rem] clr-input'>
|
|
||||||
<TextURL text='Справка...' href={`/manuals?topic=${topic}`} />
|
<TextURL text='Справка...' href={`/manuals?topic=${topic}`} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<TopicPage topic={topic} />
|
<TopicPage topic={topic} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
import { CstClass } from '@/features/rsform';
|
import { CstClass } from '@/features/rsform';
|
||||||
import { colorBgCstClass } from '@/features/rsform/colors';
|
import { colorBgCstClass } from '@/features/rsform/colors';
|
||||||
import { describeCstClass, labelCstClass } from '@/features/rsform/labels';
|
import { describeCstClass, labelCstClass } from '@/features/rsform/labels';
|
||||||
|
@ -18,7 +16,7 @@ export function InfoCstClass({ header }: InfoCstClassProps) {
|
||||||
return (
|
return (
|
||||||
<p key={`${prefixes.cst_status_list}${index}`}>
|
<p key={`${prefixes.cst_status_list}${index}`}>
|
||||||
<span
|
<span
|
||||||
className={clsx('inline-block', 'min-w-[7rem]', 'px-1', 'border', 'text-center text-sm font-controls')}
|
className='inline-block min-w-28 px-1 border text-center text-sm font-controls'
|
||||||
style={{ backgroundColor: colorBgCstClass(cstClass) }}
|
style={{ backgroundColor: colorBgCstClass(cstClass) }}
|
||||||
>
|
>
|
||||||
{labelCstClass(cstClass)}
|
{labelCstClass(cstClass)}
|
||||||
|
|
|
@ -21,7 +21,7 @@ export function InfoCstStatus({ title }: InfoCstStatusProps) {
|
||||||
<span
|
<span
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'inline-block', //
|
'inline-block', //
|
||||||
'min-w-[7rem]',
|
'min-w-28',
|
||||||
'px-1',
|
'px-1',
|
||||||
'border',
|
'border',
|
||||||
'text-center text-sm font-controls'
|
'text-center text-sm font-controls'
|
||||||
|
|
|
@ -7,11 +7,12 @@ import { HelpTopic } from '../models/helpTopic';
|
||||||
|
|
||||||
export function HelpRSLang() {
|
export function HelpRSLang() {
|
||||||
const windowSize = useWindowSize();
|
const windowSize = useWindowSize();
|
||||||
|
const isSmall = windowSize.isSmall;
|
||||||
|
|
||||||
const videoHeight = (() => {
|
const videoHeight = (() => {
|
||||||
const viewH = windowSize.height ?? 0;
|
const viewH = windowSize.height ?? 0;
|
||||||
const viewW = windowSize.width ?? 0;
|
const viewW = windowSize.width ?? 0;
|
||||||
const availableWidth = viewW - (windowSize.isSmall ? 35 : 310);
|
const availableWidth = viewW - (isSmall ? 35 : 310);
|
||||||
return Math.min(1080, Math.max(viewH - 450, 300), Math.floor((availableWidth * 9) / 16));
|
return Math.min(1080, Math.max(viewH - 450, 300), Math.floor((availableWidth * 9) / 16));
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
|
@ -25,9 +25,9 @@ import { HelpTopic } from '../../models/helpTopic';
|
||||||
export function HelpOssGraph() {
|
export function HelpOssGraph() {
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
<h1 className='sm:pr-[6rem]'>Граф синтеза</h1>
|
<h1 className='sm:pr-24'>Граф синтеза</h1>
|
||||||
<div className='flex flex-col sm:flex-row'>
|
<div className='flex flex-col sm:flex-row'>
|
||||||
<div className='sm:w-[14rem]'>
|
<div className='sm:w-56'>
|
||||||
<h1>Настройка графа</h1>
|
<h1>Настройка графа</h1>
|
||||||
<li>
|
<li>
|
||||||
<IconFitImage className='inline-icon' /> Вписать в экран
|
<IconFitImage className='inline-icon' /> Вписать в экран
|
||||||
|
@ -51,7 +51,7 @@ export function HelpOssGraph() {
|
||||||
|
|
||||||
<Divider vertical margins='mx-3 mt-3' className='hidden sm:block' />
|
<Divider vertical margins='mx-3 mt-3' className='hidden sm:block' />
|
||||||
|
|
||||||
<div className='sm:w-[21rem]'>
|
<div className='sm:w-84'>
|
||||||
<h1>Изменение узлов</h1>
|
<h1>Изменение узлов</h1>
|
||||||
<li>Клик на операцию – выделение</li>
|
<li>Клик на операцию – выделение</li>
|
||||||
<li>Esc – сбросить выделение</li>
|
<li>Esc – сбросить выделение</li>
|
||||||
|
@ -73,7 +73,7 @@ export function HelpOssGraph() {
|
||||||
<Divider margins='my-3' className='hidden sm:block' />
|
<Divider margins='my-3' className='hidden sm:block' />
|
||||||
|
|
||||||
<div className='flex flex-col-reverse mb-3 sm:flex-row'>
|
<div className='flex flex-col-reverse mb-3 sm:flex-row'>
|
||||||
<div className='sm:w-[14rem]'>
|
<div className='sm:w-56'>
|
||||||
<h1>Общие</h1>
|
<h1>Общие</h1>
|
||||||
<li>
|
<li>
|
||||||
<IconReset className='inline-icon' /> Сбросить изменения
|
<IconReset className='inline-icon' /> Сбросить изменения
|
||||||
|
@ -85,7 +85,7 @@ export function HelpOssGraph() {
|
||||||
|
|
||||||
<Divider vertical margins='mx-3' className='hidden sm:block' />
|
<Divider vertical margins='mx-3' className='hidden sm:block' />
|
||||||
|
|
||||||
<div className='dense w-[21rem]'>
|
<div className='dense w-84'>
|
||||||
<h1>Контекстное меню</h1>
|
<h1>Контекстное меню</h1>
|
||||||
<li>
|
<li>
|
||||||
<IconRSForm className='inline-icon icon-green' /> Статус связанной{' '}
|
<IconRSForm className='inline-icon icon-green' /> Статус связанной{' '}
|
||||||
|
|
|
@ -29,7 +29,7 @@ export function HelpRSGraphTerm() {
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
<h1>Граф термов</h1>
|
<h1>Граф термов</h1>
|
||||||
<div className='flex flex-col sm:flex-row'>
|
<div className='flex flex-col sm:flex-row'>
|
||||||
<div className='sm:w-[14rem]'>
|
<div className='sm:w-56'>
|
||||||
<h1>Настройка графа</h1>
|
<h1>Настройка графа</h1>
|
||||||
<li>Цвет – покраска узлов</li>
|
<li>Цвет – покраска узлов</li>
|
||||||
<li>
|
<li>
|
||||||
|
@ -45,7 +45,7 @@ export function HelpRSGraphTerm() {
|
||||||
|
|
||||||
<Divider vertical margins='mx-3 mt-3' className='hidden sm:block' />
|
<Divider vertical margins='mx-3 mt-3' className='hidden sm:block' />
|
||||||
|
|
||||||
<div className='sm:w-[21rem]'>
|
<div className='sm:w-84'>
|
||||||
<h1>Изменение узлов</h1>
|
<h1>Изменение узлов</h1>
|
||||||
<li>Клик на узел – выделение</li>
|
<li>Клик на узел – выделение</li>
|
||||||
<li>
|
<li>
|
||||||
|
@ -69,7 +69,7 @@ export function HelpRSGraphTerm() {
|
||||||
<Divider margins='my-3' className='hidden sm:block' />
|
<Divider margins='my-3' className='hidden sm:block' />
|
||||||
|
|
||||||
<div className='flex flex-col-reverse mb-3 sm:flex-row'>
|
<div className='flex flex-col-reverse mb-3 sm:flex-row'>
|
||||||
<div className='sm:w-[14rem]'>
|
<div className='sm:w-56'>
|
||||||
<h1>Общие</h1>
|
<h1>Общие</h1>
|
||||||
<li>
|
<li>
|
||||||
<IconOSS className='inline-icon' /> переход к связанной <LinkTopic text='ОСС' topic={HelpTopic.CC_OSS} />
|
<IconOSS className='inline-icon' /> переход к связанной <LinkTopic text='ОСС' topic={HelpTopic.CC_OSS} />
|
||||||
|
@ -88,7 +88,7 @@ export function HelpRSGraphTerm() {
|
||||||
|
|
||||||
<Divider vertical margins='mx-3' className='hidden sm:block' />
|
<Divider vertical margins='mx-3' className='hidden sm:block' />
|
||||||
|
|
||||||
<div className='dense w-[21rem]'>
|
<div className='dense w-84'>
|
||||||
<h1>Выделение</h1>
|
<h1>Выделение</h1>
|
||||||
<li>
|
<li>
|
||||||
<IconGraphCollapse className='inline-icon' /> все влияющие
|
<IconGraphCollapse className='inline-icon' /> все влияющие
|
||||||
|
|
|
@ -67,7 +67,7 @@ export function HelpRSMenu() {
|
||||||
|
|
||||||
<Divider vertical margins='mx-3' />
|
<Divider vertical margins='mx-3' />
|
||||||
|
|
||||||
<div className='w-[18rem]'>
|
<div className='w-72'>
|
||||||
<h2>Режимы работы</h2>
|
<h2>Режимы работы</h2>
|
||||||
<li>
|
<li>
|
||||||
<IconAlert size='1.25rem' className='inline-icon icon-red' /> работа в анонимном режиме. Переход на страницу
|
<IconAlert size='1.25rem' className='inline-icon icon-red' /> работа в анонимном режиме. Переход на страницу
|
||||||
|
|
|
@ -27,7 +27,7 @@ export function ManualsPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex mx-auto max-w-[80rem]' role='manuals' style={{ minHeight: mainHeight }}>
|
<div className='flex mx-auto max-w-320' role='manuals' style={{ minHeight: mainHeight }}>
|
||||||
<TopicsList activeTopic={activeTopic} onChangeTopic={topic => onSelectTopic(topic)} />
|
<TopicsList activeTopic={activeTopic} onChangeTopic={topic => onSelectTopic(topic)} />
|
||||||
<ViewTopic topic={activeTopic} />
|
<ViewTopic topic={activeTopic} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -31,14 +31,14 @@ export function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownPro
|
||||||
<div
|
<div
|
||||||
ref={menu.ref}
|
ref={menu.ref}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'absolute left-0 w-[13.5rem]', // prettier: split-lines
|
'absolute left-0 w-54', //
|
||||||
'flex flex-col',
|
'flex flex-col',
|
||||||
'z-modal-tooltip',
|
'z-topmost',
|
||||||
'text-xs sm:text-sm',
|
'text-xs sm:text-sm',
|
||||||
'select-none',
|
'select-none',
|
||||||
{
|
{
|
||||||
'top-0': noNavigation,
|
'top-0': noNavigation,
|
||||||
'top-[3rem]': !noNavigation
|
'top-12': !noNavigation
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -48,7 +48,7 @@ export function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownPro
|
||||||
title='Список тем'
|
title='Список тем'
|
||||||
hideTitle={menu.isOpen}
|
hideTitle={menu.isOpen}
|
||||||
icon={!menu.isOpen ? <IconMenuUnfold size='1.25rem' /> : <IconMenuFold size='1.25rem' />}
|
icon={!menu.isOpen ? <IconMenuUnfold size='1.25rem' /> : <IconMenuFold size='1.25rem' />}
|
||||||
className={clsx('w-[3rem] h-7 rounded-none border-l-0', menu.isOpen && 'border-b-0')}
|
className={clsx('w-12 h-7 rounded-none border-l-0', menu.isOpen && 'border-b-0')}
|
||||||
onClick={menu.toggle}
|
onClick={menu.toggle}
|
||||||
/>
|
/>
|
||||||
<SelectTree
|
<SelectTree
|
||||||
|
@ -59,11 +59,7 @@ export function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownPro
|
||||||
getParent={item => topicParent.get(item) ?? item}
|
getParent={item => topicParent.get(item) ?? item}
|
||||||
getLabel={labelHelpTopic}
|
getLabel={labelHelpTopic}
|
||||||
getDescription={describeHelpTopic}
|
getDescription={describeHelpTopic}
|
||||||
className={clsx(
|
className='border-r border-t rounded-none cc-scroll-y bg-prim-200'
|
||||||
'border-r border-t rounded-none', // prettier: split-lines
|
|
||||||
'cc-scroll-y',
|
|
||||||
'bg-prim-200'
|
|
||||||
)}
|
|
||||||
style={{
|
style={{
|
||||||
maxHeight: treeHeight,
|
maxHeight: treeHeight,
|
||||||
willChange: 'clip-path',
|
willChange: 'clip-path',
|
||||||
|
|
|
@ -25,7 +25,7 @@ export function TopicsStatic({ activeTopic, onChangeTopic }: TopicsStaticProps)
|
||||||
getDescription={describeHelpTopic}
|
getDescription={describeHelpTopic}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'sticky top-0 left-0',
|
'sticky top-0 left-0',
|
||||||
'min-w-[14.5rem] max-w-[14.5rem] sm:min-w-[12.5rem] sm:max-w-[12.5rem] md:min-w-[14.5rem] md:max-w-[14.5rem]',
|
'min-w-58 max-w-58 sm:min-w-50 sm:max-w-50 md:min-w-58 md:max-w-58',
|
||||||
'cc-scroll-y',
|
'cc-scroll-y',
|
||||||
'self-start',
|
'self-start',
|
||||||
'border-x border-t rounded-none',
|
'border-x border-t rounded-none',
|
||||||
|
|
|
@ -19,7 +19,7 @@ import {
|
||||||
type IRenameLocationDTO,
|
type IRenameLocationDTO,
|
||||||
type IUpdateLibraryItemDTO,
|
type IUpdateLibraryItemDTO,
|
||||||
type IVersionCreateDTO,
|
type IVersionCreateDTO,
|
||||||
type IVersionInfo,
|
type IVersionExInfo,
|
||||||
type IVersionUpdateDTO,
|
type IVersionUpdateDTO,
|
||||||
schemaLibraryItem,
|
schemaLibraryItem,
|
||||||
schemaLibraryItemArray,
|
schemaLibraryItemArray,
|
||||||
|
@ -154,7 +154,7 @@ export const libraryApi = {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
versionUpdate: (data: { itemID: number; version: IVersionUpdateDTO }) =>
|
versionUpdate: (data: { itemID: number; version: IVersionUpdateDTO }) =>
|
||||||
axiosPatch<IVersionUpdateDTO, IVersionInfo>({
|
axiosPatch<IVersionUpdateDTO, IVersionExInfo>({
|
||||||
schema: schemaVersionInfo,
|
schema: schemaVersionInfo,
|
||||||
endpoint: `/api/versions/${data.version.id}`,
|
endpoint: `/api/versions/${data.version.id}`,
|
||||||
request: {
|
request: {
|
||||||
|
|
|
@ -34,6 +34,9 @@ export interface IRenameLocationDTO {
|
||||||
/** Represents library item version information. */
|
/** Represents library item version information. */
|
||||||
export type IVersionInfo = z.infer<typeof schemaVersionInfo>;
|
export type IVersionInfo = z.infer<typeof schemaVersionInfo>;
|
||||||
|
|
||||||
|
/** Represents library item version extended information. */
|
||||||
|
export type IVersionExInfo = z.infer<typeof schemaVersionExInfo>;
|
||||||
|
|
||||||
/** Represents data, used for cloning {@link IRSForm}. */
|
/** Represents data, used for cloning {@link IRSForm}. */
|
||||||
export type ICloneLibraryItemDTO = z.infer<typeof schemaCloneLibraryItem>;
|
export type ICloneLibraryItemDTO = z.infer<typeof schemaCloneLibraryItem>;
|
||||||
|
|
||||||
|
@ -51,7 +54,7 @@ export type IVersionUpdateDTO = z.infer<typeof schemaVersionUpdate>;
|
||||||
|
|
||||||
// ======= SCHEMAS =========
|
// ======= SCHEMAS =========
|
||||||
|
|
||||||
export const schemaLibraryItem = z.object({
|
export const schemaLibraryItem = z.strictObject({
|
||||||
id: z.coerce.number(),
|
id: z.coerce.number(),
|
||||||
item_type: z.nativeEnum(LibraryItemType),
|
item_type: z.nativeEnum(LibraryItemType),
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
|
@ -112,7 +115,7 @@ export const schemaCreateLibraryItem = z
|
||||||
message: errorMsg.requiredField
|
message: errorMsg.requiredField
|
||||||
});
|
});
|
||||||
|
|
||||||
export const schemaUpdateLibraryItem = z.object({
|
export const schemaUpdateLibraryItem = z.strictObject({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
item_type: z.nativeEnum(LibraryItemType),
|
item_type: z.nativeEnum(LibraryItemType),
|
||||||
title: z.string().nonempty(errorMsg.requiredField),
|
title: z.string().nonempty(errorMsg.requiredField),
|
||||||
|
@ -122,20 +125,24 @@ export const schemaUpdateLibraryItem = z.object({
|
||||||
read_only: z.boolean()
|
read_only: z.boolean()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const schemaVersionInfo = z.object({
|
export const schemaVersionInfo = z.strictObject({
|
||||||
id: z.coerce.number(),
|
id: z.coerce.number(),
|
||||||
version: z.string(),
|
version: z.string(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
time_create: z.string().datetime({ offset: true })
|
time_create: z.string().datetime({ offset: true })
|
||||||
});
|
});
|
||||||
|
|
||||||
export const schemaVersionUpdate = z.object({
|
export const schemaVersionExInfo = schemaVersionInfo.extend({
|
||||||
|
item: z.number()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const schemaVersionUpdate = z.strictObject({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
version: z.string().nonempty(errorMsg.requiredField),
|
version: z.string().nonempty(errorMsg.requiredField),
|
||||||
description: z.string()
|
description: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const schemaVersionCreate = z.object({
|
export const schemaVersionCreate = z.strictObject({
|
||||||
version: z.string(),
|
version: z.string(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
items: z.array(z.number())
|
items: z.array(z.number())
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { urls, useConceptNavigation } from '@/app';
|
||||||
import { useLabelUser, useRoleStore, UserRole } from '@/features/users';
|
import { useLabelUser, useRoleStore, UserRole } from '@/features/users';
|
||||||
import { InfoUsers, SelectUser } from '@/features/users/components';
|
import { InfoUsers, SelectUser } from '@/features/users/components';
|
||||||
|
|
||||||
import { Overlay, Tooltip } from '@/components/Container';
|
import { Tooltip } from '@/components/Container';
|
||||||
import { MiniButton } from '@/components/Control';
|
import { MiniButton } from '@/components/Control';
|
||||||
import { useDropdown } from '@/components/Dropdown';
|
import { useDropdown } from '@/components/Dropdown';
|
||||||
import {
|
import {
|
||||||
|
@ -83,7 +83,7 @@ export function EditorLibraryItem({ schema, isAttachedToOSS }: EditorLibraryItem
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
<div className='flex justify-stretch sm:mb-1 max-w-[30rem] gap-3'>
|
<div className='relative flex justify-stretch sm:mb-1 max-w-120 gap-3'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
noHover
|
noHover
|
||||||
noPadding
|
noPadding
|
||||||
|
@ -101,12 +101,11 @@ export function EditorLibraryItem({ schema, isAttachedToOSS }: EditorLibraryItem
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='relative'>
|
||||||
{ownerSelector.isOpen ? (
|
{ownerSelector.isOpen ? (
|
||||||
<Overlay position='top-[-0.5rem] left-[4rem] cc-icons'>
|
<div className='absolute -top-2 right-0'>
|
||||||
{ownerSelector.isOpen ? (
|
<SelectUser className='w-100 text-sm' value={schema.owner} onChange={onSelectUser} />
|
||||||
<SelectUser className='w-[25rem] sm:w-[26rem] text-sm' value={schema.owner} onChange={onSelectUser} />
|
</div>
|
||||||
) : null}
|
|
||||||
</Overlay>
|
|
||||||
) : null}
|
) : null}
|
||||||
<ValueIcon
|
<ValueIcon
|
||||||
className='sm:mb-1'
|
className='sm:mb-1'
|
||||||
|
@ -116,6 +115,7 @@ export function EditorLibraryItem({ schema, isAttachedToOSS }: EditorLibraryItem
|
||||||
onClick={ownerSelector.toggle}
|
onClick={ownerSelector.toggle}
|
||||||
disabled={isModified || isProcessing || isAttachedToOSS || role < UserRole.OWNER}
|
disabled={isModified || isProcessing || isAttachedToOSS || role < UserRole.OWNER}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='sm:mb-1 flex justify-between items-center'>
|
<div className='sm:mb-1 flex justify-between items-center'>
|
||||||
<ValueIcon
|
<ValueIcon
|
||||||
|
@ -126,7 +126,7 @@ export function EditorLibraryItem({ schema, isAttachedToOSS }: EditorLibraryItem
|
||||||
onClick={handleEditEditors}
|
onClick={handleEditEditors}
|
||||||
disabled={isModified || isProcessing || role < UserRole.OWNER}
|
disabled={isModified || isProcessing || role < UserRole.OWNER}
|
||||||
/>
|
/>
|
||||||
<Tooltip anchorSelect='#editor_stats' layer='z-modal-tooltip'>
|
<Tooltip anchorSelect='#editor_stats'>
|
||||||
<Suspense fallback={<Loader scale={2} />}>
|
<Suspense fallback={<Loader scale={2} />}>
|
||||||
<InfoUsers items={schema.editors} prefix={prefixes.user_editors} header='Редакторы' />
|
<InfoUsers items={schema.editors} prefix={prefixes.user_editors} header='Редакторы' />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
|
@ -44,7 +44,7 @@ export function MenuRole({ isOwned, isEditor }: MenuRoleProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={accessMenu.ref}>
|
<div ref={accessMenu.ref} className='relative'>
|
||||||
<Button
|
<Button
|
||||||
dense
|
dense
|
||||||
noBorder
|
noBorder
|
||||||
|
@ -56,7 +56,7 @@ export function MenuRole({ isOwned, isEditor }: MenuRoleProps) {
|
||||||
icon={<IconRole role={role} size='1.25rem' />}
|
icon={<IconRole role={role} size='1.25rem' />}
|
||||||
onClick={accessMenu.toggle}
|
onClick={accessMenu.toggle}
|
||||||
/>
|
/>
|
||||||
<Dropdown isOpen={accessMenu.isOpen}>
|
<Dropdown isOpen={accessMenu.isOpen} margin='mt-3'>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text={labelUserRole(UserRole.READER)}
|
text={labelUserRole(UserRole.READER)}
|
||||||
title={describeUserRole(UserRole.READER)}
|
title={describeUserRole(UserRole.READER)}
|
||||||
|
|
|
@ -28,7 +28,7 @@ export function MiniSelectorOSS({ items, onSelect, className, ...restProps }: Mi
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ossMenu.ref} className={clsx('flex items-center', className)} {...restProps}>
|
<div ref={ossMenu.ref} className={clsx('relative flex items-center', className)} {...restProps}>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
icon={<IconOSS size='1.25rem' className='icon-primary' />}
|
icon={<IconOSS size='1.25rem' className='icon-primary' />}
|
||||||
title='Операционные схемы'
|
title='Операционные схемы'
|
||||||
|
@ -36,11 +36,11 @@ export function MiniSelectorOSS({ items, onSelect, className, ...restProps }: Mi
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
/>
|
/>
|
||||||
{items.length > 1 ? (
|
{items.length > 1 ? (
|
||||||
<Dropdown isOpen={ossMenu.isOpen}>
|
<Dropdown isOpen={ossMenu.isOpen} margin='mt-1'>
|
||||||
<Label text='Список ОСС' className='border-b px-3 py-1' />
|
<Label text='Список ОСС' className='border-b px-3 py-1' />
|
||||||
{items.map((reference, index) => (
|
{items.map((reference, index) => (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
className='min-w-[5rem]'
|
className='min-w-20'
|
||||||
key={`${prefixes.oss_list}${index}`}
|
key={`${prefixes.oss_list}${index}`}
|
||||||
text={reference.alias}
|
text={reference.alias}
|
||||||
onClick={event => onSelect(event, reference)}
|
onClick={event => onSelect(event, reference)}
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { FlexColumn } from '@/components/Container';
|
|
||||||
import { MiniButton } from '@/components/Control';
|
import { MiniButton } from '@/components/Control';
|
||||||
import { createColumnHelper, DataTable, type IConditionalStyle } from '@/components/DataTable';
|
import { createColumnHelper, DataTable, type IConditionalStyle } from '@/components/DataTable';
|
||||||
import { Dropdown, useDropdown } from '@/components/Dropdown';
|
import { Dropdown, useDropdown } from '@/components/Dropdown';
|
||||||
|
@ -113,14 +112,14 @@ export function PickSchema({
|
||||||
query={filterText}
|
query={filterText}
|
||||||
onChangeQuery={newValue => setFilterText(newValue)}
|
onChangeQuery={newValue => setFilterText(newValue)}
|
||||||
/>
|
/>
|
||||||
<div ref={locationMenu.ref}>
|
<div className='relative' ref={locationMenu.ref}>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
icon={<IconFolderTree size='1.25rem' className={!!filterLocation ? 'icon-green' : 'icon-primary'} />}
|
icon={<IconFolderTree size='1.25rem' className={!!filterLocation ? 'icon-green' : 'icon-primary'} />}
|
||||||
title='Фильтр по расположению'
|
title='Фильтр по расположению'
|
||||||
className='mt-1'
|
className='mt-1'
|
||||||
onClick={() => locationMenu.toggle()}
|
onClick={() => locationMenu.toggle()}
|
||||||
/>
|
/>
|
||||||
<Dropdown isOpen={locationMenu.isOpen} stretchLeft className='w-[20rem] h-[12.5rem] z-modal-tooltip mt-0'>
|
<Dropdown isOpen={locationMenu.isOpen} stretchLeft className='w-80 h-50'>
|
||||||
<SelectLocation
|
<SelectLocation
|
||||||
value={filterLocation}
|
value={filterLocation}
|
||||||
prefix={prefixes.folders_list}
|
prefix={prefixes.folders_list}
|
||||||
|
@ -148,10 +147,10 @@ export function PickSchema({
|
||||||
columns={columns}
|
columns={columns}
|
||||||
conditionalRowStyles={conditionalRowStyles}
|
conditionalRowStyles={conditionalRowStyles}
|
||||||
noDataComponent={
|
noDataComponent={
|
||||||
<FlexColumn className='dense p-3 items-center min-h-[6rem]'>
|
<div className='cc-column dense p-3 items-center min-h-24'>
|
||||||
<p>Список схем пуст</p>
|
<p>Список схем пуст</p>
|
||||||
<p>Измените параметры фильтра</p>
|
<p>Измените параметры фильтра</p>
|
||||||
</FlexColumn>
|
</div>
|
||||||
}
|
}
|
||||||
onRowClicked={rowData => onChange(rowData.id)}
|
onRowClicked={rowData => onChange(rowData.id)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { MiniButton } from '@/components/Control';
|
import { MiniButton } from '@/components/Control';
|
||||||
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
|
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
|
||||||
import { type Styling } from '@/components/props';
|
import { type Styling } from '@/components/props';
|
||||||
|
@ -18,7 +20,14 @@ interface SelectAccessPolicyProps extends Styling {
|
||||||
stretchLeft?: boolean;
|
stretchLeft?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SelectAccessPolicy({ value, disabled, stretchLeft, onChange, ...restProps }: SelectAccessPolicyProps) {
|
export function SelectAccessPolicy({
|
||||||
|
value,
|
||||||
|
disabled,
|
||||||
|
className,
|
||||||
|
stretchLeft,
|
||||||
|
onChange,
|
||||||
|
...restProps
|
||||||
|
}: SelectAccessPolicyProps) {
|
||||||
const menu = useDropdown();
|
const menu = useDropdown();
|
||||||
|
|
||||||
function handleChange(newValue: AccessPolicy) {
|
function handleChange(newValue: AccessPolicy) {
|
||||||
|
@ -29,7 +38,7 @@ export function SelectAccessPolicy({ value, disabled, stretchLeft, onChange, ...
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={menu.ref} {...restProps}>
|
<div ref={menu.ref} className={clsx('relative', className)} {...restProps}>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title={`Доступ: ${labelAccessPolicy(value)}`}
|
title={`Доступ: ${labelAccessPolicy(value)}`}
|
||||||
hideTitle={menu.isOpen}
|
hideTitle={menu.isOpen}
|
||||||
|
@ -38,7 +47,7 @@ export function SelectAccessPolicy({ value, disabled, stretchLeft, onChange, ...
|
||||||
onClick={menu.toggle}
|
onClick={menu.toggle}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
<Dropdown isOpen={menu.isOpen} stretchLeft={stretchLeft}>
|
<Dropdown isOpen={menu.isOpen} stretchLeft={stretchLeft} margin='mt-1'>
|
||||||
{Object.values(AccessPolicy).map((item, index) => (
|
{Object.values(AccessPolicy).map((item, index) => (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
key={`${prefixes.policy_list}${index}`}
|
key={`${prefixes.policy_list}${index}`}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { SelectorButton } from '@/components/Control';
|
import { SelectorButton } from '@/components/Control';
|
||||||
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
|
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
|
||||||
import { type Styling } from '@/components/props';
|
import { type Styling } from '@/components/props';
|
||||||
|
@ -17,7 +19,14 @@ interface SelectItemTypeProps extends Styling {
|
||||||
stretchLeft?: boolean;
|
stretchLeft?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SelectItemType({ value, disabled, stretchLeft, onChange, ...restProps }: SelectItemTypeProps) {
|
export function SelectItemType({
|
||||||
|
value,
|
||||||
|
disabled,
|
||||||
|
className,
|
||||||
|
stretchLeft,
|
||||||
|
onChange,
|
||||||
|
...restProps
|
||||||
|
}: SelectItemTypeProps) {
|
||||||
const menu = useDropdown();
|
const menu = useDropdown();
|
||||||
|
|
||||||
function handleChange(newValue: LibraryItemType) {
|
function handleChange(newValue: LibraryItemType) {
|
||||||
|
@ -28,7 +37,7 @@ export function SelectItemType({ value, disabled, stretchLeft, onChange, ...rest
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={menu.ref} {...restProps}>
|
<div ref={menu.ref} className={clsx('relative', className)} {...restProps}>
|
||||||
<SelectorButton
|
<SelectorButton
|
||||||
transparent
|
transparent
|
||||||
title={describeLibraryItemType(value)}
|
title={describeLibraryItemType(value)}
|
||||||
|
@ -39,7 +48,7 @@ export function SelectItemType({ value, disabled, stretchLeft, onChange, ...rest
|
||||||
onClick={menu.toggle}
|
onClick={menu.toggle}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
<Dropdown isOpen={menu.isOpen} stretchLeft={stretchLeft}>
|
<Dropdown isOpen={menu.isOpen} stretchLeft={stretchLeft} margin='mt-1'>
|
||||||
{Object.values(LibraryItemType).map((item, index) => (
|
{Object.values(LibraryItemType).map((item, index) => (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
key={`${prefixes.policy_list}${index}`}
|
key={`${prefixes.policy_list}${index}`}
|
||||||
|
|
|
@ -51,14 +51,14 @@ export function SelectLocation({ value, dense, prefix, onClick, className, style
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx('flex flex-col', 'cc-scroll-y', className)} style={style}>
|
<div className={clsx('flex flex-col cc-scroll-y', className)} style={style}>
|
||||||
{items.map((item, index) =>
|
{items.map((item, index) =>
|
||||||
!item.parent || !folded.includes(item.parent) ? (
|
!item.parent || !folded.includes(item.parent) ? (
|
||||||
<div
|
<div
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
key={`${prefix}${index}`}
|
key={`${prefix}${index}`}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
!dense && 'min-h-[2.0825rem] sm:min-h-[2.3125rem]',
|
!dense && 'h-7 sm:h-8',
|
||||||
'pr-3 py-1 flex items-center gap-2',
|
'pr-3 py-1 flex items-center gap-2',
|
||||||
'cc-scroll-row',
|
'cc-scroll-row',
|
||||||
'clr-hover cc-animate-color',
|
'clr-hover cc-animate-color',
|
||||||
|
|
|
@ -14,7 +14,7 @@ interface SelectLocationContextProps extends Styling {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (newValue: string) => void;
|
onChange: (newValue: string) => void;
|
||||||
title?: string;
|
title?: string;
|
||||||
stretchTop?: boolean;
|
dropdownHeight?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SelectLocationContext({
|
export function SelectLocationContext({
|
||||||
|
@ -22,7 +22,8 @@ export function SelectLocationContext({
|
||||||
title = 'Проводник...',
|
title = 'Проводник...',
|
||||||
onChange,
|
onChange,
|
||||||
className,
|
className,
|
||||||
style
|
dropdownHeight,
|
||||||
|
...restProps
|
||||||
}: SelectLocationContextProps) {
|
}: SelectLocationContextProps) {
|
||||||
const menu = useDropdown();
|
const menu = useDropdown();
|
||||||
|
|
||||||
|
@ -34,18 +35,14 @@ export function SelectLocationContext({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={menu.ref} className='h-full text-right self-start mt-[-0.25rem] ml-[-1.5rem]'>
|
<div ref={menu.ref} className={clsx('relative h-full -mt-1 -ml-6 text-right self-start', className)} {...restProps}>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title={title}
|
title={title}
|
||||||
hideTitle={menu.isOpen}
|
hideTitle={menu.isOpen}
|
||||||
icon={<IconFolderTree size='1.25rem' className='icon-green' />}
|
icon={<IconFolderTree size='1.25rem' className='icon-green' />}
|
||||||
onClick={() => menu.toggle()}
|
onClick={() => menu.toggle()}
|
||||||
/>
|
/>
|
||||||
<Dropdown
|
<Dropdown isOpen={menu.isOpen} className={clsx('w-80 h-50 z-tooltip', dropdownHeight)}>
|
||||||
isOpen={menu.isOpen}
|
|
||||||
className={clsx('w-[20rem] h-[12.5rem] z-modal-tooltip mt-[-0.25rem]', className)}
|
|
||||||
style={style}
|
|
||||||
>
|
|
||||||
<SelectLocation
|
<SelectLocation
|
||||||
value={value}
|
value={value}
|
||||||
prefix={prefixes.folders_list}
|
prefix={prefixes.folders_list}
|
||||||
|
|
|
@ -33,7 +33,7 @@ export function SelectLocationHead({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={menu.ref} className={clsx('h-full text-right', className)} {...restProps}>
|
<div ref={menu.ref} className={clsx('text-right relative', className)} {...restProps}>
|
||||||
<SelectorButton
|
<SelectorButton
|
||||||
transparent
|
transparent
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
|
@ -45,22 +45,18 @@ export function SelectLocationHead({
|
||||||
onClick={menu.toggle}
|
onClick={menu.toggle}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Dropdown isOpen={menu.isOpen} className='z-modal-tooltip'>
|
<Dropdown isOpen={menu.isOpen} margin='mt-2'>
|
||||||
{Object.values(LocationHead)
|
{Object.values(LocationHead)
|
||||||
.filter(head => !excluded.includes(head))
|
.filter(head => !excluded.includes(head))
|
||||||
.map((head, index) => {
|
.map((head, index) => {
|
||||||
return (
|
return (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
className='w-[10rem]'
|
|
||||||
key={`${prefixes.location_head_list}${index}`}
|
key={`${prefixes.location_head_list}${index}`}
|
||||||
onClick={() => handleChange(head)}
|
onClick={() => handleChange(head)}
|
||||||
title={describeLocationHead(head)}
|
title={describeLocationHead(head)}
|
||||||
>
|
icon={<IconLocationHead value={head} size='1rem' />}
|
||||||
<div className='inline-flex items-center gap-3'>
|
text={labelLocationHead(head)}
|
||||||
<IconLocationHead value={head} size='1rem' />
|
/>
|
||||||
{labelLocationHead(head)}
|
|
||||||
</div>
|
|
||||||
</DropdownButton>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
|
@ -34,7 +34,7 @@ export function SelectVersion({ id, className, items, value, onChange, ...restPr
|
||||||
return (
|
return (
|
||||||
<SelectSingle
|
<SelectSingle
|
||||||
id={id}
|
id={id}
|
||||||
className={clsx('min-w-[12rem] text-ellipsis', className)}
|
className={clsx('min-w-48 text-ellipsis', className)}
|
||||||
options={options}
|
options={options}
|
||||||
value={{ value: value, label: labelVersion(value, items) }}
|
value={{ value: value, label: labelVersion(value, items) }}
|
||||||
onChange={data => onChange(data?.value ?? 'latest')}
|
onChange={data => onChange(data?.value ?? 'latest')}
|
||||||
|
|
|
@ -2,11 +2,9 @@ import { HelpTopic } from '@/features/help';
|
||||||
import { BadgeHelp } from '@/features/help/components';
|
import { BadgeHelp } from '@/features/help/components';
|
||||||
import { useRoleStore, UserRole } from '@/features/users';
|
import { useRoleStore, UserRole } from '@/features/users';
|
||||||
|
|
||||||
import { Overlay } from '@/components/Container';
|
|
||||||
import { MiniButton } from '@/components/Control';
|
import { MiniButton } from '@/components/Control';
|
||||||
import { IconImmutable, IconMutable } from '@/components/Icons';
|
import { IconImmutable, IconMutable } from '@/components/Icons';
|
||||||
import { Label } from '@/components/Input';
|
import { Label } from '@/components/Input';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
|
||||||
|
|
||||||
import { type AccessPolicy, type ILibraryItem } from '../backend/types';
|
import { type AccessPolicy, type ILibraryItem } from '../backend/types';
|
||||||
import { useMutatingLibrary } from '../backend/useMutatingLibrary';
|
import { useMutatingLibrary } from '../backend/useMutatingLibrary';
|
||||||
|
@ -42,7 +40,7 @@ export function ToolbarItemAccess({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Overlay position='top-[4.5rem] right-0 w-[12rem] pr-2' className='flex' layer='z-bottom'>
|
<div className='absolute z-bottom top-18 right-0 w-48 flex pr-2'>
|
||||||
<Label text='Доступ' className='self-center select-none' />
|
<Label text='Доступ' className='self-center select-none' />
|
||||||
<div className='ml-auto cc-icons'>
|
<div className='ml-auto cc-icons'>
|
||||||
<SelectAccessPolicy
|
<SelectAccessPolicy
|
||||||
|
@ -70,9 +68,8 @@ export function ToolbarItemAccess({
|
||||||
onClick={toggleReadOnly}
|
onClick={toggleReadOnly}
|
||||||
disabled={role === UserRole.READER || isProcessing}
|
disabled={role === UserRole.READER || isProcessing}
|
||||||
/>
|
/>
|
||||||
|
<BadgeHelp topic={HelpTopic.ACCESS} className='mt-0.5' offset={4} />
|
||||||
<BadgeHelp topic={HelpTopic.ACCESS} className={PARAMETER.TOOLTIP_WIDTH} offset={4} />
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Overlay>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,29 +3,28 @@
|
||||||
import { urls, useConceptNavigation } from '@/app';
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
import { HelpTopic } from '@/features/help';
|
import { HelpTopic } from '@/features/help';
|
||||||
import { BadgeHelp } from '@/features/help/components';
|
import { BadgeHelp } from '@/features/help/components';
|
||||||
import { AccessPolicy, type ILibraryItem, LibraryItemType } from '@/features/library';
|
import { type IRSForm } from '@/features/rsform';
|
||||||
import { useMutatingLibrary } from '@/features/library/backend/useMutatingLibrary';
|
|
||||||
import { MiniSelectorOSS } from '@/features/library/components';
|
|
||||||
import { useRoleStore, UserRole } from '@/features/users';
|
import { useRoleStore, UserRole } from '@/features/users';
|
||||||
|
|
||||||
import { Overlay } from '@/components/Container';
|
|
||||||
import { MiniButton } from '@/components/Control';
|
import { MiniButton } from '@/components/Control';
|
||||||
import { IconDestroy, IconSave, IconShare } from '@/components/Icons';
|
import { IconDestroy, IconSave, IconShare } from '@/components/Icons';
|
||||||
import { useModificationStore } from '@/stores/modification';
|
import { useModificationStore } from '@/stores/modification';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
|
||||||
import { tooltipText } from '@/utils/labels';
|
import { tooltipText } from '@/utils/labels';
|
||||||
import { prepareTooltip, sharePage } from '@/utils/utils';
|
import { prepareTooltip, sharePage } from '@/utils/utils';
|
||||||
|
|
||||||
import { type IRSForm } from '../models/rsform';
|
import { AccessPolicy, type ILibraryItem, LibraryItemType } from '../backend/types';
|
||||||
|
import { useMutatingLibrary } from '../backend/useMutatingLibrary';
|
||||||
|
|
||||||
interface ToolbarRSFormCardProps {
|
import { MiniSelectorOSS } from './MiniSelectorOSS';
|
||||||
|
|
||||||
|
interface ToolbarItemCardProps {
|
||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
isMutable: boolean;
|
isMutable: boolean;
|
||||||
schema: ILibraryItem;
|
schema: ILibraryItem;
|
||||||
deleteSchema: () => void;
|
deleteSchema: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolbarRSFormCard({ schema, onSubmit, isMutable, deleteSchema }: ToolbarRSFormCardProps) {
|
export function ToolbarItemCard({ schema, onSubmit, isMutable, deleteSchema }: ToolbarItemCardProps) {
|
||||||
const role = useRoleStore(state => state.role);
|
const role = useRoleStore(state => state.role);
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const { isModified } = useModificationStore();
|
const { isModified } = useModificationStore();
|
||||||
|
@ -49,7 +48,7 @@ export function ToolbarRSFormCard({ schema, onSubmit, isMutable, deleteSchema }:
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Overlay position='cc-tab-tools' className='cc-icons'>
|
<div className='cc-tab-tools cc-icons'>
|
||||||
{ossSelector}
|
{ossSelector}
|
||||||
{isMutable || isModified ? (
|
{isMutable || isModified ? (
|
||||||
<MiniButton
|
<MiniButton
|
||||||
|
@ -73,7 +72,7 @@ export function ToolbarRSFormCard({ schema, onSubmit, isMutable, deleteSchema }:
|
||||||
onClick={deleteSchema}
|
onClick={deleteSchema}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<BadgeHelp topic={HelpTopic.UI_RS_CARD} offset={4} className={PARAMETER.TOOLTIP_WIDTH} />
|
<BadgeHelp topic={HelpTopic.UI_RS_CARD} offset={4} />
|
||||||
</Overlay>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -5,3 +5,4 @@ export { PickSchema } from './PickSchema';
|
||||||
export { SelectLibraryItem } from './SelectLibraryItem';
|
export { SelectLibraryItem } from './SelectLibraryItem';
|
||||||
export { SelectVersion } from './SelectVersion';
|
export { SelectVersion } from './SelectVersion';
|
||||||
export { ToolbarItemAccess } from './ToolbarItemAccess';
|
export { ToolbarItemAccess } from './ToolbarItemAccess';
|
||||||
|
export { ToolbarItemCard } from './ToolbarItemCard';
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import clsx from 'clsx';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { useAuthSuspense } from '@/features/auth';
|
import { useAuthSuspense } from '@/features/auth';
|
||||||
|
@ -18,7 +17,7 @@ import { SelectLocationHead } from '../components/SelectLocationHead';
|
||||||
import { LocationHead } from '../models/library';
|
import { LocationHead } from '../models/library';
|
||||||
import { combineLocation, validateLocation } from '../models/libraryAPI';
|
import { combineLocation, validateLocation } from '../models/libraryAPI';
|
||||||
|
|
||||||
const schemaLocation = z.object({
|
const schemaLocation = z.strictObject({
|
||||||
location: z.string().refine(data => validateLocation(data), { message: errorMsg.invalidLocation })
|
location: z.string().refine(data => validateLocation(data), { message: errorMsg.invalidLocation })
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -57,9 +56,9 @@ export function DlgChangeLocation() {
|
||||||
submitInvalidTooltip={`Допустимы буквы, цифры, подчерк, пробел и "!". Сегмент пути не может начинаться и заканчиваться пробелом. Общая длина (с корнем) не должна превышать ${limits.location_len}`}
|
submitInvalidTooltip={`Допустимы буквы, цифры, подчерк, пробел и "!". Сегмент пути не может начинаться и заканчиваться пробелом. Общая длина (с корнем) не должна превышать ${limits.location_len}`}
|
||||||
canSubmit={isValid && isDirty}
|
canSubmit={isValid && isDirty}
|
||||||
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
||||||
className={clsx('w-[35rem]', 'pb-3 px-6 flex gap-3 h-[9rem]')}
|
className='w-140 pb-3 px-6 flex gap-3 h-36'
|
||||||
>
|
>
|
||||||
<div className='flex flex-col gap-2 min-w-[7rem] h-min'>
|
<div className='flex flex-col gap-2 min-w-28'>
|
||||||
<Label className='select-none' text='Корень' />
|
<Label className='select-none' text='Корень' />
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
|
@ -77,11 +76,11 @@ export function DlgChangeLocation() {
|
||||||
control={control}
|
control={control}
|
||||||
name='location'
|
name='location'
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<SelectLocationContext className='max-h-[9.2rem]' value={field.value} onChange={field.onChange} />
|
<SelectLocationContext dropdownHeight='max-h-36' value={field.value} onChange={field.onChange} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
control={control} //
|
control={control}
|
||||||
name='location'
|
name='location'
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<TextArea
|
<TextArea
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
import { urls, useConceptNavigation } from '@/app';
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
import { useAuthSuspense } from '@/features/auth';
|
import { useAuthSuspense } from '@/features/auth';
|
||||||
|
@ -69,7 +68,7 @@ export function DlgCloneLibraryItem() {
|
||||||
submitText='Создать'
|
submitText='Создать'
|
||||||
canSubmit={isValid}
|
canSubmit={isValid}
|
||||||
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
||||||
className={clsx('px-6 py-2', 'cc-column', 'max-h-full w-[30rem]')}
|
className='px-6 py-2 cc-column h-fit w-120'
|
||||||
>
|
>
|
||||||
<TextInput
|
<TextInput
|
||||||
id='dlg_full_name' //
|
id='dlg_full_name' //
|
||||||
|
@ -77,14 +76,9 @@ export function DlgCloneLibraryItem() {
|
||||||
{...register('title')}
|
{...register('title')}
|
||||||
error={errors.title}
|
error={errors.title}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='flex justify-between gap-3'>
|
<div className='flex justify-between gap-3'>
|
||||||
<TextInput
|
<TextInput id='dlg_alias' label='Сокращение' className='w-64' {...register('alias')} error={errors.alias} />
|
||||||
id='dlg_alias'
|
|
||||||
label='Сокращение'
|
|
||||||
className='w-[16rem]'
|
|
||||||
{...register('alias')}
|
|
||||||
error={errors.alias}
|
|
||||||
/>
|
|
||||||
<div className='flex flex-col gap-2'>
|
<div className='flex flex-col gap-2'>
|
||||||
<Label text='Доступ' className='self-center select-none' />
|
<Label text='Доступ' className='self-center select-none' />
|
||||||
<div className='ml-auto cc-icons'>
|
<div className='ml-auto cc-icons'>
|
||||||
|
@ -114,8 +108,8 @@ export function DlgCloneLibraryItem() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex justify-between gap-3'>
|
<div className='flex gap-3'>
|
||||||
<div className='flex flex-col gap-2 w-[7rem] h-min'>
|
<div className='flex flex-col gap-2 w-28'>
|
||||||
<Label text='Корень' />
|
<Label text='Корень' />
|
||||||
<Controller
|
<Controller
|
||||||
control={control} //
|
control={control} //
|
||||||
|
@ -155,7 +149,7 @@ export function DlgCloneLibraryItem() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TextArea id='dlg_comment' {...register('comment')} label='Описание' error={errors.comment} />
|
<TextArea id='dlg_comment' {...register('comment')} label='Описание' rows={4} error={errors.comment} />
|
||||||
|
|
||||||
{selected.length > 0 ? (
|
{selected.length > 0 ? (
|
||||||
<Controller
|
<Controller
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
import { Controller, useForm, useWatch } from 'react-hook-form';
|
import { Controller, useForm, useWatch } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
import { Checkbox, TextArea, TextInput } from '@/components/Input';
|
import { Checkbox, TextArea, TextInput } from '@/components/Input';
|
||||||
import { ModalForm } from '@/components/Modal';
|
import { ModalForm } from '@/components/Modal';
|
||||||
|
@ -45,13 +44,13 @@ export function DlgCreateVersion() {
|
||||||
return (
|
return (
|
||||||
<ModalForm
|
<ModalForm
|
||||||
header='Создание версии'
|
header='Создание версии'
|
||||||
className={clsx('cc-column', 'w-[30rem]', 'py-2 px-6')}
|
className='cc-column w-120 py-2 px-6'
|
||||||
canSubmit={canSubmit}
|
canSubmit={canSubmit}
|
||||||
submitInvalidTooltip={errorMsg.versionTaken}
|
submitInvalidTooltip={errorMsg.versionTaken}
|
||||||
submitText='Создать'
|
submitText='Создать'
|
||||||
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
||||||
>
|
>
|
||||||
<TextInput id='dlg_version' {...register('version')} dense label='Версия' className='w-[16rem]' />
|
<TextInput id='dlg_version' {...register('version')} dense label='Версия' className='w-64' />
|
||||||
<TextArea id='dlg_description' {...register('description')} spellCheck label='Описание' rows={3} />
|
<TextArea id='dlg_description' {...register('description')} spellCheck label='Описание' rows={3} />
|
||||||
{selected.length > 0 ? (
|
{selected.length > 0 ? (
|
||||||
<Controller
|
<Controller
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
import { useUsers } from '@/features/users';
|
import { useUsers } from '@/features/users';
|
||||||
import { SelectUser, TableUsers } from '@/features/users/components';
|
import { SelectUser, TableUsers } from '@/features/users/components';
|
||||||
|
@ -42,15 +41,15 @@ export function DlgEditEditors() {
|
||||||
<ModalForm
|
<ModalForm
|
||||||
header='Список редакторов'
|
header='Список редакторов'
|
||||||
submitText='Сохранить список'
|
submitText='Сохранить список'
|
||||||
className='flex flex-col w-[35rem] px-6 gap-3 pb-6'
|
className='flex flex-col w-140 px-6 gap-3 pb-6'
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
<div className={clsx('flex self-center items-center', 'text-sm font-semibold')}>
|
<div className='self-center text-sm font-semibold'>
|
||||||
<span>Всего редакторов [{selected.length}]</span>
|
<span>Всего редакторов [{selected.length}]</span>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
noHover
|
noHover
|
||||||
title='Очистить список'
|
title='Очистить список'
|
||||||
className='py-0'
|
className='py-0 align-middle'
|
||||||
icon={<IconRemove size='1.5rem' className='icon-red' />}
|
icon={<IconRemove size='1.5rem' className='icon-red' />}
|
||||||
disabled={selected.length === 0}
|
disabled={selected.length === 0}
|
||||||
onClick={() => setSelected([])}
|
onClick={() => setSelected([])}
|
||||||
|
@ -65,7 +64,7 @@ export function DlgEditEditors() {
|
||||||
filter={id => !selected.includes(id)} //
|
filter={id => !selected.includes(id)} //
|
||||||
value={null}
|
value={null}
|
||||||
onChange={onAddEditor}
|
onChange={onAddEditor}
|
||||||
className='w-[25rem]'
|
className='w-100'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ModalForm>
|
</ModalForm>
|
||||||
|
|
|
@ -85,7 +85,7 @@ export function DlgEditVersions() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalView header='Редактирование версий' className='flex flex-col w-[40rem] px-6 gap-3 pb-6'>
|
<ModalView header='Редактирование версий' className='flex flex-col w-160 px-6 gap-3 pb-3'>
|
||||||
<TableVersions
|
<TableVersions
|
||||||
processing={isProcessing}
|
processing={isProcessing}
|
||||||
items={schema.versions.reverse()}
|
items={schema.versions.reverse()}
|
||||||
|
@ -100,7 +100,7 @@ export function DlgEditVersions() {
|
||||||
{...register('version')}
|
{...register('version')}
|
||||||
dense
|
dense
|
||||||
label='Версия'
|
label='Версия'
|
||||||
className='w-[16rem] mr-3'
|
className='w-64 mr-3'
|
||||||
error={formErrors.version}
|
error={formErrors.version}
|
||||||
/>
|
/>
|
||||||
<div className='cc-icons h-fit'>
|
<div className='cc-icons h-fit'>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
import { MiniButton } from '@/components/Control';
|
import { MiniButton } from '@/components/Control';
|
||||||
import { createColumnHelper, DataTable, type IConditionalStyle } from '@/components/DataTable';
|
import { createColumnHelper, DataTable, type IConditionalStyle } from '@/components/DataTable';
|
||||||
|
@ -33,7 +32,7 @@ export function TableVersions({ processing, items, onDelete, selected, onSelect
|
||||||
columnHelper.accessor('version', {
|
columnHelper.accessor('version', {
|
||||||
id: 'version',
|
id: 'version',
|
||||||
header: 'Версия',
|
header: 'Версия',
|
||||||
cell: props => <div className='min-w-[6rem] max-w-[6rem] text-ellipsis'>{props.getValue()}</div>
|
cell: props => <div className='w-24 text-ellipsis'>{props.getValue()}</div>
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('description', {
|
columnHelper.accessor('description', {
|
||||||
id: 'description',
|
id: 'description',
|
||||||
|
@ -62,16 +61,15 @@ export function TableVersions({ processing, items, onDelete, selected, onSelect
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
size: 0,
|
size: 0,
|
||||||
cell: props => (
|
cell: props => (
|
||||||
<div className='h-[1.25rem] w-[1.25rem]'>
|
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Удалить версию'
|
title='Удалить версию'
|
||||||
|
className='align-middle'
|
||||||
noHover
|
noHover
|
||||||
noPadding
|
noPadding
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
icon={<IconRemove size='1.25rem' className='icon-red' />}
|
icon={<IconRemove size='1.25rem' className='icon-red' />}
|
||||||
onClick={event => handleDeleteVersion(event, props.row.original.id)}
|
onClick={event => handleDeleteVersion(event, props.row.original.id)}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
@ -90,7 +88,7 @@ export function TableVersions({ processing, items, onDelete, selected, onSelect
|
||||||
dense
|
dense
|
||||||
noFooter
|
noFooter
|
||||||
headPosition='0'
|
headPosition='0'
|
||||||
className={clsx('mb-2', 'max-h-[17.4rem] min-h-[17.4rem]', 'border', 'cc-scroll-y')}
|
className='mb-2 h-70 border cc-scroll-y'
|
||||||
data={items}
|
data={items}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
onRowClicked={rowData => onSelect(rowData.id)}
|
onRowClicked={rowData => onSelect(rowData.id)}
|
||||||
|
|
|
@ -3,12 +3,10 @@
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { Controller, useForm, useWatch } from 'react-hook-form';
|
import { Controller, useForm, useWatch } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
import { urls, useConceptNavigation } from '@/app';
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
import { useAuthSuspense } from '@/features/auth';
|
import { useAuthSuspense } from '@/features/auth';
|
||||||
|
|
||||||
import { Overlay } from '@/components/Container';
|
|
||||||
import { Button, MiniButton, SubmitButton } from '@/components/Control';
|
import { Button, MiniButton, SubmitButton } from '@/components/Control';
|
||||||
import { IconDownload } from '@/components/Icons';
|
import { IconDownload } from '@/components/Icons';
|
||||||
import { InfoError } from '@/components/InfoError';
|
import { InfoError } from '@/components/InfoError';
|
||||||
|
@ -104,13 +102,13 @@ export function FormCreateItem() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className={clsx('cc-fade-in cc-column', 'min-w-[30rem] max-w-[30rem] mx-auto', 'px-6 py-3')}
|
className='cc-fade-in cc-column min-w-120 max-w-120 mx-auto px-6 py-3'
|
||||||
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
||||||
onChange={resetErrors}
|
onChange={resetErrors}
|
||||||
>
|
>
|
||||||
<h1 className='select-none'>
|
<h1 className='select-none relative'>
|
||||||
{itemType == LibraryItemType.RSFORM ? (
|
{itemType == LibraryItemType.RSFORM ? (
|
||||||
<Overlay position='top-0 right-[0.5rem]'>
|
<>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name='file'
|
name='file'
|
||||||
|
@ -127,10 +125,11 @@ export function FormCreateItem() {
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Загрузить из Экстеор'
|
title='Загрузить из Экстеор'
|
||||||
|
className='absolute top-0 right-0'
|
||||||
icon={<IconDownload size='1.25rem' className='icon-primary' />}
|
icon={<IconDownload size='1.25rem' className='icon-primary' />}
|
||||||
onClick={() => inputRef.current?.click()}
|
onClick={() => inputRef.current?.click()}
|
||||||
/>
|
/>
|
||||||
</Overlay>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
Создание схемы
|
Создание схемы
|
||||||
</h1>
|
</h1>
|
||||||
|
@ -151,7 +150,7 @@ export function FormCreateItem() {
|
||||||
{...register('alias')}
|
{...register('alias')}
|
||||||
label='Сокращение'
|
label='Сокращение'
|
||||||
placeholder={file && 'Загрузить из файла'}
|
placeholder={file && 'Загрузить из файла'}
|
||||||
className='w-[16rem]'
|
className='w-64'
|
||||||
error={errors.alias}
|
error={errors.alias}
|
||||||
/>
|
/>
|
||||||
<div className='flex flex-col items-center gap-2'>
|
<div className='flex flex-col items-center gap-2'>
|
||||||
|
@ -206,7 +205,7 @@ export function FormCreateItem() {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='flex justify-between gap-3 grow'>
|
<div className='flex justify-between gap-3 grow'>
|
||||||
<div className='flex flex-col gap-2 min-w-[7rem] h-min'>
|
<div className='flex flex-col gap-2 min-w-28'>
|
||||||
<Label text='Корень' />
|
<Label text='Корень' />
|
||||||
<Controller
|
<Controller
|
||||||
control={control} //
|
control={control} //
|
||||||
|
@ -247,8 +246,8 @@ export function FormCreateItem() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex justify-around gap-6 py-3'>
|
<div className='flex justify-around gap-6 py-3'>
|
||||||
<SubmitButton text='Создать схему' loading={isPending} className='min-w-[10rem]' />
|
<SubmitButton text='Создать схему' loading={isPending} className='min-w-40' />
|
||||||
<Button text='Отмена' className='min-w-[10rem]' onClick={() => handleCancel()} />
|
<Button text='Отмена' className='min-w-40' onClick={() => handleCancel()} />
|
||||||
</div>
|
</div>
|
||||||
{serverError ? <InfoError error={serverError} /> : null}
|
{serverError ? <InfoError error={serverError} /> : null}
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -3,10 +3,8 @@
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import fileDownload from 'js-file-download';
|
import fileDownload from 'js-file-download';
|
||||||
|
|
||||||
import { Overlay } from '@/components/Container';
|
|
||||||
import { MiniButton } from '@/components/Control';
|
import { MiniButton } from '@/components/Control';
|
||||||
import { IconCSV } from '@/components/Icons';
|
import { IconCSV } from '@/components/Icons';
|
||||||
import { useAppLayoutStore } from '@/stores/appLayout';
|
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { infoMsg } from '@/utils/labels';
|
import { infoMsg } from '@/utils/labels';
|
||||||
import { convertToCSV } from '@/utils/utils';
|
import { convertToCSV } from '@/utils/utils';
|
||||||
|
@ -24,8 +22,6 @@ export function LibraryPage() {
|
||||||
const { items: libraryItems } = useLibrarySuspense();
|
const { items: libraryItems } = useLibrarySuspense();
|
||||||
const { renameLocation } = useRenameLocation();
|
const { renameLocation } = useRenameLocation();
|
||||||
|
|
||||||
const noNavigation = useAppLayoutStore(state => state.noNavigation);
|
|
||||||
|
|
||||||
const folderMode = useLibrarySearchStore(state => state.folderMode);
|
const folderMode = useLibrarySearchStore(state => state.folderMode);
|
||||||
const location = useLibrarySearchStore(state => state.location);
|
const location = useLibrarySearchStore(state => state.location);
|
||||||
const setLocation = useLibrarySearchStore(state => state.setLocation);
|
const setLocation = useLibrarySearchStore(state => state.setLocation);
|
||||||
|
@ -57,20 +53,15 @@ export function LibraryPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Overlay
|
<ToolbarSearch total={libraryItems.length} filtered={filtered.length} />
|
||||||
position={noNavigation ? 'top-[0.25rem] right-[3rem]' : 'top-[0.25rem] right-0'}
|
<div className='relative cc-fade-in flex'>
|
||||||
layer='z-tooltip'
|
|
||||||
className='cc-animate-position'
|
|
||||||
>
|
|
||||||
<MiniButton
|
<MiniButton
|
||||||
|
className='absolute z-tooltip top-1 right-0 cc-animate-position'
|
||||||
title='Выгрузить в формате CSV'
|
title='Выгрузить в формате CSV'
|
||||||
icon={<IconCSV size='1.25rem' className='icon-green' />}
|
icon={<IconCSV size='1.25rem' className='icon-green' />}
|
||||||
onClick={handleDownloadCSV}
|
onClick={handleDownloadCSV}
|
||||||
/>
|
/>
|
||||||
</Overlay>
|
|
||||||
<ToolbarSearch total={libraryItems.length} filtered={filtered.length} />
|
|
||||||
|
|
||||||
<div className='cc-fade-in flex'>
|
|
||||||
<ViewSideLocation
|
<ViewSideLocation
|
||||||
isVisible={folderMode}
|
isVisible={folderMode}
|
||||||
onRenameLocation={() => showChangeLocation({ initial: location, onChangeLocation: handleRenameLocation })}
|
onRenameLocation={() => showChangeLocation({ initial: location, onChangeLocation: handleRenameLocation })}
|
||||||
|
|
|
@ -1,43 +1,47 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useLayoutEffect, useState } from 'react';
|
|
||||||
import { useIntl } from 'react-intl';
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { urls, useConceptNavigation } from '@/app';
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
import { useLabelUser } from '@/features/users';
|
|
||||||
|
|
||||||
import { FlexColumn } from '@/components/Container';
|
import { TextURL } from '@/components/Control';
|
||||||
import { MiniButton, TextURL } from '@/components/Control';
|
import { DataTable, type IConditionalStyle, type VisibilityState } from '@/components/DataTable';
|
||||||
import { createColumnHelper, DataTable, type IConditionalStyle, type VisibilityState } from '@/components/DataTable';
|
|
||||||
import { IconFolderTree } from '@/components/Icons';
|
|
||||||
import { useWindowSize } from '@/hooks/useWindowSize';
|
import { useWindowSize } from '@/hooks/useWindowSize';
|
||||||
import { useFitHeight } from '@/stores/appLayout';
|
import { useFitHeight } from '@/stores/appLayout';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import { APP_COLORS } from '@/styling/colors';
|
import { APP_COLORS } from '@/styling/colors';
|
||||||
|
|
||||||
import { type ILibraryItem, LibraryItemType } from '../../backend/types';
|
import { type ILibraryItem, LibraryItemType } from '../../backend/types';
|
||||||
import { BadgeLocation } from '../../components/BadgeLocation';
|
|
||||||
import { useLibrarySearchStore } from '../../stores/librarySearch';
|
import { useLibrarySearchStore } from '../../stores/librarySearch';
|
||||||
|
|
||||||
|
import { useLibraryColumns } from './useLibraryColumns';
|
||||||
|
|
||||||
interface TableLibraryItemsProps {
|
interface TableLibraryItemsProps {
|
||||||
items: ILibraryItem[];
|
items: ILibraryItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<ILibraryItem>();
|
|
||||||
|
|
||||||
export function TableLibraryItems({ items }: TableLibraryItemsProps) {
|
export function TableLibraryItems({ items }: TableLibraryItemsProps) {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const intl = useIntl();
|
const { isSmall } = useWindowSize();
|
||||||
const getUserLabel = useLabelUser();
|
|
||||||
|
|
||||||
const folderMode = useLibrarySearchStore(state => state.folderMode);
|
const folderMode = useLibrarySearchStore(state => state.folderMode);
|
||||||
const toggleFolderMode = useLibrarySearchStore(state => state.toggleFolderMode);
|
|
||||||
const resetFilter = useLibrarySearchStore(state => state.resetFilter);
|
const resetFilter = useLibrarySearchStore(state => state.resetFilter);
|
||||||
|
|
||||||
const itemsPerPage = usePreferencesStore(state => state.libraryPagination);
|
const itemsPerPage = usePreferencesStore(state => state.libraryPagination);
|
||||||
const setItemsPerPage = usePreferencesStore(state => state.setLibraryPagination);
|
const setItemsPerPage = usePreferencesStore(state => state.setLibraryPagination);
|
||||||
|
|
||||||
|
const columns = useLibraryColumns();
|
||||||
|
const columnVisibility: VisibilityState = { owner: !isSmall };
|
||||||
|
const conditionalRowStyles: IConditionalStyle<ILibraryItem>[] = [
|
||||||
|
{
|
||||||
|
when: (item: ILibraryItem) => item.item_type === LibraryItemType.OSS,
|
||||||
|
style: {
|
||||||
|
color: APP_COLORS.fgGreen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const tableHeight = useFitHeight('2.25rem');
|
||||||
|
|
||||||
function handleOpenItem(item: ILibraryItem, event: React.MouseEvent<Element>) {
|
function handleOpenItem(item: ILibraryItem, event: React.MouseEvent<Element>) {
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
if (!!selection && selection.toString().length > 0) {
|
if (!!selection && selection.toString().length > 0) {
|
||||||
|
@ -50,108 +54,6 @@ export function TableLibraryItems({ items }: TableLibraryItemsProps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const windowSize = useWindowSize();
|
|
||||||
|
|
||||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
setColumnVisibility({
|
|
||||||
owner: !windowSize.isSmall
|
|
||||||
});
|
|
||||||
}, [windowSize]);
|
|
||||||
|
|
||||||
function handleToggleFolder(event: React.MouseEvent<Element>) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
toggleFolderMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
...(folderMode
|
|
||||||
? []
|
|
||||||
: [
|
|
||||||
columnHelper.accessor('location', {
|
|
||||||
id: 'location',
|
|
||||||
header: () => (
|
|
||||||
<MiniButton
|
|
||||||
noPadding
|
|
||||||
noHover
|
|
||||||
className='pl-2 max-h-[1rem] translate-y-[-0.125rem]'
|
|
||||||
onClick={handleToggleFolder}
|
|
||||||
titleHtml='Переключение в режим Проводник'
|
|
||||||
icon={<IconFolderTree size='1.25rem' className='clr-text-controls' />}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
size: 50,
|
|
||||||
minSize: 50,
|
|
||||||
maxSize: 50,
|
|
||||||
enableSorting: true,
|
|
||||||
cell: props => <BadgeLocation location={props.getValue()} />,
|
|
||||||
sortingFn: 'text'
|
|
||||||
})
|
|
||||||
]),
|
|
||||||
columnHelper.accessor('alias', {
|
|
||||||
id: 'alias',
|
|
||||||
header: 'Шифр',
|
|
||||||
size: 150,
|
|
||||||
minSize: 80,
|
|
||||||
maxSize: 150,
|
|
||||||
enableSorting: true,
|
|
||||||
cell: props => <div className='min-w-[5rem]'>{props.getValue()}</div>,
|
|
||||||
sortingFn: 'text'
|
|
||||||
}),
|
|
||||||
columnHelper.accessor('title', {
|
|
||||||
id: 'title',
|
|
||||||
header: 'Название',
|
|
||||||
size: 1200,
|
|
||||||
minSize: 200,
|
|
||||||
maxSize: 1200,
|
|
||||||
enableSorting: true,
|
|
||||||
sortingFn: 'text'
|
|
||||||
}),
|
|
||||||
columnHelper.accessor(item => item.owner ?? 0, {
|
|
||||||
id: 'owner',
|
|
||||||
header: 'Владелец',
|
|
||||||
size: 400,
|
|
||||||
minSize: 100,
|
|
||||||
maxSize: 400,
|
|
||||||
cell: props => getUserLabel(props.getValue()),
|
|
||||||
enableSorting: true,
|
|
||||||
sortingFn: 'text'
|
|
||||||
}),
|
|
||||||
columnHelper.accessor('time_update', {
|
|
||||||
id: 'time_update',
|
|
||||||
header: windowSize.isSmall ? 'Дата' : 'Обновлена',
|
|
||||||
cell: props => (
|
|
||||||
<div className='whitespace-nowrap'>
|
|
||||||
{new Date(props.getValue()).toLocaleString(intl.locale, {
|
|
||||||
year: '2-digit',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
...(!windowSize.isSmall && {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
})
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
enableSorting: true,
|
|
||||||
sortingFn: 'datetime',
|
|
||||||
sortDescFirst: true
|
|
||||||
})
|
|
||||||
];
|
|
||||||
|
|
||||||
const tableHeight = useFitHeight('2.2rem');
|
|
||||||
|
|
||||||
const conditionalRowStyles: IConditionalStyle<ILibraryItem>[] = [
|
|
||||||
{
|
|
||||||
when: (item: ILibraryItem) => item.item_type === LibraryItemType.OSS,
|
|
||||||
style: {
|
|
||||||
color: APP_COLORS.fgGreen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTable
|
<DataTable
|
||||||
id='library_data'
|
id='library_data'
|
||||||
|
@ -161,13 +63,13 @@ export function TableLibraryItems({ items }: TableLibraryItemsProps) {
|
||||||
className={clsx('text-xs sm:text-sm cc-scroll-y h-fit border-b', { 'border-l': folderMode })}
|
className={clsx('text-xs sm:text-sm cc-scroll-y h-fit border-b', { 'border-l': folderMode })}
|
||||||
style={{ maxHeight: tableHeight }}
|
style={{ maxHeight: tableHeight }}
|
||||||
noDataComponent={
|
noDataComponent={
|
||||||
<FlexColumn className='dense p-3 items-center min-h-[6rem]'>
|
<div className='cc-column dense p-3 items-center min-h-24'>
|
||||||
<p>Список схем пуст</p>
|
<p>Список схем пуст</p>
|
||||||
<p className='flex gap-6'>
|
<p className='flex gap-6'>
|
||||||
<TextURL text='Создать схему' href='/library/create' />
|
<TextURL text='Создать схему' href='/library/create' />
|
||||||
<TextURL text='Очистить фильтр' onClick={resetFilter} />
|
<TextURL text='Очистить фильтр' onClick={resetFilter} />
|
||||||
</p>
|
</p>
|
||||||
</FlexColumn>
|
</div>
|
||||||
}
|
}
|
||||||
columnVisibility={columnVisibility}
|
columnVisibility={columnVisibility}
|
||||||
onRowClicked={handleOpenItem}
|
onRowClicked={handleOpenItem}
|
||||||
|
|
|
@ -75,35 +75,19 @@ export function ToolbarSearch({ total, filtered }: ToolbarSearchProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className='sticky top-0 h-9 flex gap-3 border-b text-sm clr-input items-center'>
|
||||||
className={clsx(
|
<div className='ml-3 min-w-18 sm:min-w-30 select-none whitespace-nowrap'>
|
||||||
'sticky top-0', //
|
|
||||||
'h-[2.2rem]',
|
|
||||||
'flex items-center gap-3',
|
|
||||||
'border-b',
|
|
||||||
'text-sm',
|
|
||||||
'clr-input'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
'ml-3 pt-1 self-center',
|
|
||||||
'min-w-[4.5rem] sm:min-w-[7.4rem]',
|
|
||||||
'select-none',
|
|
||||||
'whitespace-nowrap'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{filtered} из {total}
|
{filtered} из {total}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='cc-icons'>
|
<div className='cc-icons h-full items-center'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Видимость'
|
title='Видимость'
|
||||||
icon={<IconItemVisibility value={true} className={tripleToggleColor(isVisible)} />}
|
icon={<IconItemVisibility value={true} className={tripleToggleColor(isVisible)} />}
|
||||||
onClick={toggleVisible}
|
onClick={toggleVisible}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div ref={userMenu.ref} className='flex'>
|
<div ref={userMenu.ref} className='relative flex'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Поиск пользователя'
|
title='Поиск пользователя'
|
||||||
hideTitle={userMenu.isOpen}
|
hideTitle={userMenu.isOpen}
|
||||||
|
@ -124,7 +108,7 @@ export function ToolbarSearch({ total, filtered }: ToolbarSearchProps) {
|
||||||
<SelectUser
|
<SelectUser
|
||||||
noBorder
|
noBorder
|
||||||
placeholder='Выберите владельца'
|
placeholder='Выберите владельца'
|
||||||
className='min-w-[15rem] text-sm mx-1 mb-1'
|
className='min-w-60 text-sm mx-1 mb-1'
|
||||||
value={filterUser}
|
value={filterUser}
|
||||||
onChange={setFilterUser}
|
onChange={setFilterUser}
|
||||||
/>
|
/>
|
||||||
|
@ -144,15 +128,15 @@ export function ToolbarSearch({ total, filtered }: ToolbarSearchProps) {
|
||||||
id='library_search'
|
id='library_search'
|
||||||
placeholder='Поиск'
|
placeholder='Поиск'
|
||||||
noBorder
|
noBorder
|
||||||
className={clsx('min-w-[7rem] sm:min-w-[10rem] max-w-[20rem]', folderMode && 'grow')}
|
className={clsx('min-w-28 sm:min-w-40 max-w-80', folderMode && 'grow')}
|
||||||
query={query}
|
query={query}
|
||||||
onChangeQuery={setQuery}
|
onChangeQuery={setQuery}
|
||||||
/>
|
/>
|
||||||
{!folderMode ? (
|
{!folderMode ? (
|
||||||
<div ref={headMenu.ref} className='flex items-center h-full py-1 select-none'>
|
<div ref={headMenu.ref} className='relative flex items-center h-full select-none'>
|
||||||
<SelectorButton
|
<SelectorButton
|
||||||
transparent
|
transparent
|
||||||
className='h-full rounded-lg'
|
className='rounded-lg py-1'
|
||||||
titleHtml={(head ? describeLocationHead(head) : 'Выберите каталог') + '<br/>Ctrl + клик - Проводник'}
|
titleHtml={(head ? describeLocationHead(head) : 'Выберите каталог') + '<br/>Ctrl + клик - Проводник'}
|
||||||
hideTitle={headMenu.isOpen}
|
hideTitle={headMenu.isOpen}
|
||||||
icon={
|
icon={
|
||||||
|
@ -166,32 +150,27 @@ export function ToolbarSearch({ total, filtered }: ToolbarSearchProps) {
|
||||||
text={head ?? '//'}
|
text={head ?? '//'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Dropdown isOpen={headMenu.isOpen} stretchLeft className='z-modal-tooltip'>
|
<Dropdown isOpen={headMenu.isOpen} stretchLeft>
|
||||||
<DropdownButton title='Переключение в режим Проводник' onClick={handleToggleFolder}>
|
<DropdownButton
|
||||||
<div className='inline-flex items-center gap-3'>
|
title='Переключение в режим Проводник'
|
||||||
<IconFolderTree size='1rem' className='clr-text-controls' />
|
text='проводник...'
|
||||||
<span>проводник...</span>
|
icon={<IconFolderTree size='1rem' className='clr-text-controls' />}
|
||||||
</div>
|
onClick={handleToggleFolder}
|
||||||
</DropdownButton>
|
/>
|
||||||
<DropdownButton className='w-[10rem]' onClick={() => handleChange(null)}>
|
<DropdownButton
|
||||||
<div className='inline-flex items-center gap-3'>
|
text='отображать все'
|
||||||
<IconFolder size='1rem' className='clr-text-controls' />
|
icon={<IconFolder size='1rem' className='clr-text-controls' />}
|
||||||
<span>отображать все</span>
|
onClick={() => handleChange(null)}
|
||||||
</div>
|
/>
|
||||||
</DropdownButton>
|
|
||||||
{Object.values(LocationHead).map((head, index) => {
|
{Object.values(LocationHead).map((head, index) => {
|
||||||
return (
|
return (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
className='w-[10rem]'
|
|
||||||
key={`${prefixes.location_head_list}${index}`}
|
key={`${prefixes.location_head_list}${index}`}
|
||||||
onClick={() => handleChange(head)}
|
onClick={() => handleChange(head)}
|
||||||
title={describeLocationHead(head)}
|
title={describeLocationHead(head)}
|
||||||
>
|
text={labelLocationHead(head)}
|
||||||
<div className='inline-flex items-center gap-3'>
|
icon={<IconLocationHead value={head} size='1rem' />}
|
||||||
<IconLocationHead value={head} size='1rem' />
|
/>
|
||||||
{labelLocationHead(head)}
|
|
||||||
</div>
|
|
||||||
</DropdownButton>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
@ -203,7 +182,7 @@ export function ToolbarSearch({ total, filtered }: ToolbarSearchProps) {
|
||||||
placeholder='Путь'
|
placeholder='Путь'
|
||||||
noIcon
|
noIcon
|
||||||
noBorder
|
noBorder
|
||||||
className='w-[4.5rem] sm:w-[5rem] grow'
|
className='w-18 sm:w-20 grow'
|
||||||
query={path}
|
query={path}
|
||||||
onChangeQuery={setPath}
|
onChangeQuery={setPath}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -26,7 +26,7 @@ interface ViewSideLocationProps {
|
||||||
export function ViewSideLocation({ isVisible, onRenameLocation }: ViewSideLocationProps) {
|
export function ViewSideLocation({ isVisible, onRenameLocation }: ViewSideLocationProps) {
|
||||||
const { user, isAnonymous } = useAuthSuspense();
|
const { user, isAnonymous } = useAuthSuspense();
|
||||||
const { items } = useLibrary();
|
const { items } = useLibrary();
|
||||||
const windowSize = useWindowSize();
|
const { isSmall } = useWindowSize();
|
||||||
|
|
||||||
const location = useLibrarySearchStore(state => state.location);
|
const location = useLibrarySearchStore(state => state.location);
|
||||||
const setLocation = useLibrarySearchStore(state => state.setLocation);
|
const setLocation = useLibrarySearchStore(state => state.setLocation);
|
||||||
|
@ -63,23 +63,18 @@ export function ViewSideLocation({ isVisible, onRenameLocation }: ViewSideLocati
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx('max-w-[10rem] sm:max-w-[15rem]', 'flex flex-col', 'text:xs sm:text-sm', 'select-none')}
|
className={clsx('max-w-40 sm:max-w-60', 'flex flex-col', 'text:xs sm:text-sm', 'select-none')}
|
||||||
style={{
|
style={{
|
||||||
transitionProperty: 'width, min-width, opacity',
|
transitionProperty: 'width, min-width, opacity',
|
||||||
transitionDuration: `${PARAMETER.moveDuration}ms`,
|
transitionDuration: `${PARAMETER.moveDuration}ms`,
|
||||||
transitionTimingFunction: 'ease-out',
|
transitionTimingFunction: 'ease-out',
|
||||||
minWidth: isVisible ? (windowSize.isSmall ? '10rem' : '15rem') : '0',
|
minWidth: isVisible ? (isSmall ? '10rem' : '15rem') : '0',
|
||||||
width: isVisible ? '100%' : '0',
|
width: isVisible ? '100%' : '0',
|
||||||
opacity: isVisible ? 1 : 0
|
opacity: isVisible ? 1 : 0
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className='h-[2.08rem] flex justify-between items-center pr-1 pl-[0.125rem]'>
|
<div className='h-8 flex justify-between items-center pr-1 pl-0.5'>
|
||||||
<BadgeHelp
|
<BadgeHelp topic={HelpTopic.UI_LIBRARY} contentClass='text-sm' offset={5} place='right-start' />
|
||||||
topic={HelpTopic.UI_LIBRARY}
|
|
||||||
className={clsx(PARAMETER.TOOLTIP_WIDTH, 'text-sm')}
|
|
||||||
offset={5}
|
|
||||||
place='right-start'
|
|
||||||
/>
|
|
||||||
<div className='cc-icons'>
|
<div className='cc-icons'>
|
||||||
{canRename ? (
|
{canRename ? (
|
||||||
<MiniButton
|
<MiniButton
|
||||||
|
@ -90,7 +85,7 @@ export function ViewSideLocation({ isVisible, onRenameLocation }: ViewSideLocati
|
||||||
) : null}
|
) : null}
|
||||||
{!!location ? (
|
{!!location ? (
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title={subfolders ? 'Вложенные папки: Вкл' : 'Вложенные папки: Выкл'} // prettier: split-lines
|
title={subfolders ? 'Вложенные папки: Вкл' : 'Вложенные папки: Выкл'}
|
||||||
icon={<IconShowSubfolders value={subfolders} />}
|
icon={<IconShowSubfolders value={subfolders} />}
|
||||||
onClick={toggleSubfolders}
|
onClick={toggleSubfolders}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { useLabelUser } from '@/features/users';
|
||||||
|
|
||||||
|
import { MiniButton } from '@/components/Control';
|
||||||
|
import { createColumnHelper } from '@/components/DataTable';
|
||||||
|
import { IconFolderTree } from '@/components/Icons';
|
||||||
|
import { useWindowSize } from '@/hooks/useWindowSize';
|
||||||
|
|
||||||
|
import { type ILibraryItem } from '../../backend/types';
|
||||||
|
import { BadgeLocation } from '../../components/BadgeLocation';
|
||||||
|
import { useLibrarySearchStore } from '../../stores/librarySearch';
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<ILibraryItem>();
|
||||||
|
|
||||||
|
export function useLibraryColumns() {
|
||||||
|
const { isSmall } = useWindowSize();
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const getUserLabel = useLabelUser();
|
||||||
|
const folderMode = useLibrarySearchStore(state => state.folderMode);
|
||||||
|
const toggleFolderMode = useLibrarySearchStore(state => state.toggleFolderMode);
|
||||||
|
|
||||||
|
function handleToggleFolder(event: React.MouseEvent<Element>) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
toggleFolderMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
...(folderMode
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
columnHelper.accessor('location', {
|
||||||
|
id: 'location',
|
||||||
|
header: () => (
|
||||||
|
<MiniButton
|
||||||
|
noPadding
|
||||||
|
noHover
|
||||||
|
className='pl-2 max-h-4 -translate-y-0.5'
|
||||||
|
onClick={handleToggleFolder}
|
||||||
|
titleHtml='Переключение в режим Проводник'
|
||||||
|
icon={<IconFolderTree size='1.25rem' className='clr-text-controls' />}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
size: 50,
|
||||||
|
minSize: 50,
|
||||||
|
maxSize: 50,
|
||||||
|
enableSorting: true,
|
||||||
|
cell: props => <BadgeLocation location={props.getValue()} />,
|
||||||
|
sortingFn: 'text'
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
columnHelper.accessor('alias', {
|
||||||
|
id: 'alias',
|
||||||
|
header: 'Шифр',
|
||||||
|
size: 150,
|
||||||
|
minSize: 80,
|
||||||
|
maxSize: 150,
|
||||||
|
enableSorting: true,
|
||||||
|
cell: props => <span className='min-w-20'>{props.getValue()}</span>,
|
||||||
|
sortingFn: 'text'
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('title', {
|
||||||
|
id: 'title',
|
||||||
|
header: 'Название',
|
||||||
|
size: 1200,
|
||||||
|
minSize: 200,
|
||||||
|
maxSize: 1200,
|
||||||
|
enableSorting: true,
|
||||||
|
sortingFn: 'text'
|
||||||
|
}),
|
||||||
|
columnHelper.accessor(item => item.owner ?? 0, {
|
||||||
|
id: 'owner',
|
||||||
|
header: 'Владелец',
|
||||||
|
size: 400,
|
||||||
|
minSize: 100,
|
||||||
|
maxSize: 400,
|
||||||
|
cell: props => getUserLabel(props.getValue()),
|
||||||
|
enableSorting: true,
|
||||||
|
sortingFn: 'text'
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('time_update', {
|
||||||
|
id: 'time_update',
|
||||||
|
header: isSmall ? 'Дата' : 'Обновлена',
|
||||||
|
cell: props => (
|
||||||
|
<span className='whitespace-nowrap'>
|
||||||
|
{new Date(props.getValue()).toLocaleString(intl.locale, {
|
||||||
|
year: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
...(!isSmall && {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
enableSorting: true,
|
||||||
|
sortingFn: 'datetime',
|
||||||
|
sortDescFirst: true
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user