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