Compare commits
9 Commits
f86d847d64
...
d8286e6339
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d8286e6339 | ||
![]() |
8bd2c71f50 | ||
![]() |
30ec76cefd | ||
![]() |
584ce59f2d | ||
![]() |
f6b52adacd | ||
![]() |
1efa13b1c5 | ||
![]() |
ce857bb2cb | ||
![]() |
a071f916e0 | ||
![]() |
07974e3760 |
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -155,6 +155,7 @@
|
||||||
"toastify",
|
"toastify",
|
||||||
"tooltipic",
|
"tooltipic",
|
||||||
"tsdoc",
|
"tsdoc",
|
||||||
|
"Typifications",
|
||||||
"unknwn",
|
"unknwn",
|
||||||
"Upvote",
|
"Upvote",
|
||||||
"Viewset",
|
"Viewset",
|
||||||
|
|
|
@ -64,10 +64,12 @@ class VersionInnerSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class VersionCreateSerializer(serializers.ModelSerializer):
|
class VersionCreateSerializer(serializers.ModelSerializer):
|
||||||
''' Serializer: Version create data. '''
|
''' Serializer: Version create data. '''
|
||||||
|
items = PKField(many=True, required=False, default=None, queryset=Constituenta.objects.all().only('pk'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
''' serializer metadata. '''
|
''' serializer metadata. '''
|
||||||
model = Version
|
model = Version
|
||||||
fields = 'version', 'description'
|
fields = 'version', 'description', 'items'
|
||||||
|
|
||||||
|
|
||||||
class LibraryItemDetailsSerializer(serializers.ModelSerializer):
|
class LibraryItemDetailsSerializer(serializers.ModelSerializer):
|
||||||
|
|
|
@ -6,6 +6,7 @@ from zipfile import ZipFile
|
||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
|
from apps.library.models import Version
|
||||||
from apps.rsform.models import Constituenta, RSForm
|
from apps.rsform.models import Constituenta, RSForm
|
||||||
from shared.EndpointTester import EndpointTester, decl_endpoint
|
from shared.EndpointTester import EndpointTester, decl_endpoint
|
||||||
|
|
||||||
|
@ -41,6 +42,20 @@ class TestVersionViews(EndpointTester):
|
||||||
self.assertTrue(response.data['version'] in [v['id'] for v in response.data['schema']['versions']])
|
self.assertTrue(response.data['version'] in [v['id'] for v in response.data['schema']['versions']])
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/library/{schema}/create-version', method='post')
|
||||||
|
def test_create_version_filter(self):
|
||||||
|
x2 = self.owned.insert_new('X2')
|
||||||
|
data = {'version': '1.0.0', 'description': 'test', 'items': [x2.pk]}
|
||||||
|
response = self.executeCreated(data=data, schema=self.owned_id)
|
||||||
|
version = Version.objects.get(pk=response.data['version'])
|
||||||
|
items = version.data['items']
|
||||||
|
self.assertTrue('version' in response.data)
|
||||||
|
self.assertTrue('schema' in response.data)
|
||||||
|
self.assertTrue(response.data['version'] in [v['id'] for v in response.data['schema']['versions']])
|
||||||
|
self.assertEqual(len(items), 1)
|
||||||
|
self.assertEqual(items[0]['id'], x2.pk)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/library/{schema}/versions/{version}', method='get')
|
@decl_endpoint('/api/library/{schema}/versions/{version}', method='get')
|
||||||
def test_retrieve_version(self):
|
def test_retrieve_version(self):
|
||||||
version_id = self._create_version({'version': '1.0.0', 'description': 'test'})
|
version_id = self._create_version({'version': '1.0.0', 'description': 'test'})
|
||||||
|
|
|
@ -103,6 +103,9 @@ def create_version(request: Request, pk_item: int) -> HttpResponse:
|
||||||
version_input = s.VersionCreateSerializer(data=request.data)
|
version_input = s.VersionCreateSerializer(data=request.data)
|
||||||
version_input.is_valid(raise_exception=True)
|
version_input.is_valid(raise_exception=True)
|
||||||
data = RSFormSerializer(item).to_versioned_data()
|
data = RSFormSerializer(item).to_versioned_data()
|
||||||
|
items: list[int] = [] if 'items' not in request.data else request.data['items']
|
||||||
|
if items:
|
||||||
|
data['items'] = [cst for cst in data['items'] if cst['id'] in items]
|
||||||
result = RSForm(item).create_version(
|
result = RSForm(item).create_version(
|
||||||
version=version_input.validated_data['version'],
|
version=version_input.validated_data['version'],
|
||||||
description=version_input.validated_data['description'],
|
description=version_input.validated_data['description'],
|
||||||
|
|
132
rsconcept/frontend/package-lock.json
generated
132
rsconcept/frontend/package-lock.json
generated
|
@ -12,9 +12,9 @@
|
||||||
"@tanstack/react-table": "^8.20.1",
|
"@tanstack/react-table": "^8.20.1",
|
||||||
"@uiw/codemirror-themes": "^4.23.0",
|
"@uiw/codemirror-themes": "^4.23.0",
|
||||||
"@uiw/react-codemirror": "^4.23.0",
|
"@uiw/react-codemirror": "^4.23.0",
|
||||||
"axios": "^1.7.4",
|
"axios": "^1.7.5",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"framer-motion": "^11.3.29",
|
"framer-motion": "^11.3.30",
|
||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
"js-file-download": "^0.4.12",
|
"js-file-download": "^0.4.12",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
@ -43,14 +43,14 @@
|
||||||
"@typescript-eslint/parser": "^8.0.1",
|
"@typescript-eslint/parser": "^8.0.1",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^9.9.0",
|
"eslint": "^9.9.1",
|
||||||
"eslint-plugin-react": "^7.35.0",
|
"eslint-plugin-react": "^7.35.0",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"globals": "^15.9.0",
|
"globals": "^15.9.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"postcss": "^8.4.41",
|
"postcss": "^8.4.41",
|
||||||
"tailwindcss": "^3.4.10",
|
"tailwindcss": "^3.4.10",
|
||||||
"ts-jest": "^29.2.4",
|
"ts-jest": "^29.2.5",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.5.4",
|
||||||
"typescript-eslint": "^8.2.0",
|
"typescript-eslint": "^8.2.0",
|
||||||
"vite": "^5.4.2"
|
"vite": "^5.4.2"
|
||||||
|
@ -97,9 +97,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/compat-data": {
|
"node_modules/@babel/compat-data": {
|
||||||
"version": "7.25.2",
|
"version": "7.25.4",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz",
|
||||||
"integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==",
|
"integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
@ -148,12 +148,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/generator": {
|
"node_modules/@babel/generator": {
|
||||||
"version": "7.25.0",
|
"version": "7.25.5",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.5.tgz",
|
||||||
"integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==",
|
"integrity": "sha512-abd43wyLfbWoxC6ahM8xTkqLpGB2iWBVyuKC9/srhFunCd1SDNrV1s72bBpK4hLj8KLzHBBcOblvLQZBNw9r3w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.25.0",
|
"@babel/types": "^7.25.4",
|
||||||
"@jridgewell/gen-mapping": "^0.3.5",
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
"@jridgewell/trace-mapping": "^0.3.25",
|
"@jridgewell/trace-mapping": "^0.3.25",
|
||||||
"jsesc": "^2.5.1"
|
"jsesc": "^2.5.1"
|
||||||
|
@ -303,12 +303,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/parser": {
|
"node_modules/@babel/parser": {
|
||||||
"version": "7.25.3",
|
"version": "7.25.4",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.4.tgz",
|
||||||
"integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==",
|
"integrity": "sha512-nq+eWrOgdtu3jG5Os4TQP3x3cLA8hR8TvJNjD8vnPa20WGycimcparWnLK4jJhElTK6SDyuJo1weMKO/5LpmLA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.25.2"
|
"@babel/types": "^7.25.4"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"parser": "bin/babel-parser.js"
|
"parser": "bin/babel-parser.js"
|
||||||
|
@ -541,13 +541,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/plugin-syntax-typescript": {
|
"node_modules/@babel/plugin-syntax-typescript": {
|
||||||
"version": "7.24.7",
|
"version": "7.25.4",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.4.tgz",
|
||||||
"integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==",
|
"integrity": "sha512-uMOCoHVU52BsSWxPOMVv5qKRdeSlPuImUCB2dlPuBSU+W2/ROE7/Zg8F2Kepbk+8yBa68LlRKxO+xgEVWorsDg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.24.7"
|
"@babel/helper-plugin-utils": "^7.24.8"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
|
@ -589,9 +589,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/runtime": {
|
"node_modules/@babel/runtime": {
|
||||||
"version": "7.25.0",
|
"version": "7.25.4",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.4.tgz",
|
||||||
"integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==",
|
"integrity": "sha512-DSgLeL/FNcpXuzav5wfYvHCGvynXkJbn3Zvc3823AEe9nPwW9IK4UoCSS5yGymmQzN0pCPvivtgS6/8U2kkm1w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"regenerator-runtime": "^0.14.0"
|
"regenerator-runtime": "^0.14.0"
|
||||||
|
@ -615,16 +615,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/traverse": {
|
"node_modules/@babel/traverse": {
|
||||||
"version": "7.25.3",
|
"version": "7.25.4",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.4.tgz",
|
||||||
"integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==",
|
"integrity": "sha512-VJ4XsrD+nOvlXyLzmLzUs/0qjFS4sK30te5yEFlvbbUNEgKaVb2BHZUpAL+ttLPQAHNrsI3zZisbfha5Cvr8vg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.24.7",
|
"@babel/code-frame": "^7.24.7",
|
||||||
"@babel/generator": "^7.25.0",
|
"@babel/generator": "^7.25.4",
|
||||||
"@babel/parser": "^7.25.3",
|
"@babel/parser": "^7.25.4",
|
||||||
"@babel/template": "^7.25.0",
|
"@babel/template": "^7.25.0",
|
||||||
"@babel/types": "^7.25.2",
|
"@babel/types": "^7.25.4",
|
||||||
"debug": "^4.3.1",
|
"debug": "^4.3.1",
|
||||||
"globals": "^11.1.0"
|
"globals": "^11.1.0"
|
||||||
},
|
},
|
||||||
|
@ -642,9 +642,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/types": {
|
"node_modules/@babel/types": {
|
||||||
"version": "7.25.2",
|
"version": "7.25.4",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.4.tgz",
|
||||||
"integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==",
|
"integrity": "sha512-zQ1ijeeCXVEh+aNL0RlmkPkG8HUiDcU2pzQQFjtbntgAczRASFzj4H+6+bV+dy1ntKR14I/DypeuRG1uma98iQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-string-parser": "^7.24.8",
|
"@babel/helper-string-parser": "^7.24.8",
|
||||||
|
@ -1331,9 +1331,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/config-array": {
|
"node_modules/@eslint/config-array": {
|
||||||
"version": "0.17.1",
|
"version": "0.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz",
|
||||||
"integrity": "sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==",
|
"integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1431,9 +1431,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/js": {
|
"node_modules/@eslint/js": {
|
||||||
"version": "9.9.0",
|
"version": "9.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz",
|
||||||
"integrity": "sha512-hhetes6ZHP3BlXLxmd8K2SNgkhNSi+UcecbnwWKwpP7kyi/uC75DJ1lOOBO3xrC4jyojtGE3YxKZPHfk4yrgug==",
|
"integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
@ -4304,9 +4304,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.7.4",
|
"version": "1.7.5",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz",
|
||||||
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
|
"integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.6",
|
"follow-redirects": "^1.15.6",
|
||||||
|
@ -5897,17 +5897,17 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint": {
|
"node_modules/eslint": {
|
||||||
"version": "9.9.0",
|
"version": "9.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz",
|
||||||
"integrity": "sha512-JfiKJrbx0506OEerjK2Y1QlldtBxkAlLxT5OEcRF8uaQ86noDe2k31Vw9rnSWv+MXZHj7OOUV/dA0AhdLFcyvA==",
|
"integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.11.0",
|
"@eslint-community/regexpp": "^4.11.0",
|
||||||
"@eslint/config-array": "^0.17.1",
|
"@eslint/config-array": "^0.18.0",
|
||||||
"@eslint/eslintrc": "^3.1.0",
|
"@eslint/eslintrc": "^3.1.0",
|
||||||
"@eslint/js": "9.9.0",
|
"@eslint/js": "9.9.1",
|
||||||
"@humanwhocodes/module-importer": "^1.0.1",
|
"@humanwhocodes/module-importer": "^1.0.1",
|
||||||
"@humanwhocodes/retry": "^0.3.0",
|
"@humanwhocodes/retry": "^0.3.0",
|
||||||
"@nodelib/fs.walk": "^1.2.8",
|
"@nodelib/fs.walk": "^1.2.8",
|
||||||
|
@ -6598,9 +6598,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/framer-motion": {
|
"node_modules/framer-motion": {
|
||||||
"version": "11.3.29",
|
"version": "11.3.30",
|
||||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.3.29.tgz",
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.3.30.tgz",
|
||||||
"integrity": "sha512-uyDuUOeOElJEA3kbkbyoTNEf75Jih1EUg0ouLKYMlGDdt/LaJPmO+FyOGAGxM2HwKhHcAoKFNveR5A8peb7yhw==",
|
"integrity": "sha512-9VmqGe9OIjfMoCcs+ZsKXlv6JaG5QagKX2F1uSbkG3Z33wgjnz60Kw+CngC1M49rDYau+Y9aL+8jGagAwrbVyw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
|
@ -7410,9 +7410,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-core-module": {
|
"node_modules/is-core-module": {
|
||||||
"version": "2.15.0",
|
"version": "2.15.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
|
||||||
"integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==",
|
"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hasown": "^2.0.2"
|
"hasown": "^2.0.2"
|
||||||
|
@ -9989,9 +9989,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/micromatch": {
|
"node_modules/micromatch": {
|
||||||
"version": "4.0.7",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||||
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
|
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -12376,21 +12376,21 @@
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/ts-jest": {
|
"node_modules/ts-jest": {
|
||||||
"version": "29.2.4",
|
"version": "29.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz",
|
||||||
"integrity": "sha512-3d6tgDyhCI29HlpwIq87sNuI+3Q6GLTTCeYRHCs7vDz+/3GCMwEtV9jezLyl4ZtnBgx00I7hm8PCP8cTksMGrw==",
|
"integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bs-logger": "0.x",
|
"bs-logger": "^0.2.6",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"fast-json-stable-stringify": "2.x",
|
"fast-json-stable-stringify": "^2.1.0",
|
||||||
"jest-util": "^29.0.0",
|
"jest-util": "^29.0.0",
|
||||||
"json5": "^2.2.3",
|
"json5": "^2.2.3",
|
||||||
"lodash.memoize": "4.x",
|
"lodash.memoize": "^4.1.2",
|
||||||
"make-error": "1.x",
|
"make-error": "^1.3.6",
|
||||||
"semver": "^7.5.3",
|
"semver": "^7.6.3",
|
||||||
"yargs-parser": "^21.0.1"
|
"yargs-parser": "^21.1.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"ts-jest": "cli.js"
|
"ts-jest": "cli.js"
|
||||||
|
@ -12425,9 +12425,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.6.3",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
|
||||||
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==",
|
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/tunnel-rat": {
|
"node_modules/tunnel-rat": {
|
||||||
|
|
|
@ -16,9 +16,9 @@
|
||||||
"@tanstack/react-table": "^8.20.1",
|
"@tanstack/react-table": "^8.20.1",
|
||||||
"@uiw/codemirror-themes": "^4.23.0",
|
"@uiw/codemirror-themes": "^4.23.0",
|
||||||
"@uiw/react-codemirror": "^4.23.0",
|
"@uiw/react-codemirror": "^4.23.0",
|
||||||
"axios": "^1.7.4",
|
"axios": "^1.7.5",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"framer-motion": "^11.3.29",
|
"framer-motion": "^11.3.30",
|
||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
"js-file-download": "^0.4.12",
|
"js-file-download": "^0.4.12",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
@ -47,14 +47,14 @@
|
||||||
"@typescript-eslint/parser": "^8.0.1",
|
"@typescript-eslint/parser": "^8.0.1",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^9.9.0",
|
"eslint": "^9.9.1",
|
||||||
"eslint-plugin-react": "^7.35.0",
|
"eslint-plugin-react": "^7.35.0",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"globals": "^15.9.0",
|
"globals": "^15.9.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"postcss": "^8.4.41",
|
"postcss": "^8.4.41",
|
||||||
"tailwindcss": "^3.4.10",
|
"tailwindcss": "^3.4.10",
|
||||||
"ts-jest": "^29.2.4",
|
"ts-jest": "^29.2.5",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.5.4",
|
||||||
"typescript-eslint": "^8.2.0",
|
"typescript-eslint": "^8.2.0",
|
||||||
"vite": "^5.4.2"
|
"vite": "^5.4.2"
|
||||||
|
|
|
@ -13,7 +13,7 @@ function ApplicationLayout() {
|
||||||
<NavigationState>
|
<NavigationState>
|
||||||
<div className='min-w-[20rem] clr-app antialiased h-full'>
|
<div className='min-w-[20rem] clr-app antialiased h-full'>
|
||||||
<ConceptToaster
|
<ConceptToaster
|
||||||
className='mt-[4rem] text-sm' // prettier: split lines
|
className='mt-[4rem] text-[14px]' // prettier: split lines
|
||||||
autoClose={3000}
|
autoClose={3000}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
pauseOnFocusLoss={false}
|
pauseOnFocusLoss={false}
|
||||||
|
@ -23,7 +23,7 @@ function ApplicationLayout() {
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id={globals.main_scroll}
|
id={globals.main_scroll}
|
||||||
className='flex flex-col items-start overflow-x-auto max-w-[100vw]'
|
className='overflow-x-auto max-w-[100vw]'
|
||||||
style={{
|
style={{
|
||||||
maxHeight: viewportHeight
|
maxHeight: viewportHeight
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -36,7 +36,7 @@ function Navigation() {
|
||||||
<ToggleNavigation />
|
<ToggleNavigation />
|
||||||
<motion.div
|
<motion.div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'pl-2 pr-[0.9rem] h-[3rem] w-full', // prettier: split lines
|
'pl-2 pr-[1.5rem] sm:pr-[0.9rem] h-[3rem] w-full', // prettier: split lines
|
||||||
'flex',
|
'flex',
|
||||||
'cc-shadow-border'
|
'cc-shadow-border'
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -13,10 +13,11 @@ function ToggleNavigation() {
|
||||||
type='button'
|
type='button'
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'absolute top-0 right-0 z-navigation flex items-center justify-center',
|
'absolute top-0 right-0 z-navigation',
|
||||||
|
'min-h-[2rem] min-w-[2rem] sm:min-w-fit',
|
||||||
|
'flex items-center justify-center',
|
||||||
'clr-hover',
|
'clr-hover',
|
||||||
'select-none',
|
'select-none'
|
||||||
'min-h-[2rem]'
|
|
||||||
)}
|
)}
|
||||||
onClick={toggleNoNavigation}
|
onClick={toggleNoNavigation}
|
||||||
initial={false}
|
initial={false}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
IRenameLocationData,
|
IRenameLocationData,
|
||||||
ITargetAccessPolicy,
|
ITargetAccessPolicy,
|
||||||
ITargetLocation,
|
ITargetLocation,
|
||||||
IVersionData
|
IVersionCreateData
|
||||||
} from '@/models/library';
|
} from '@/models/library';
|
||||||
import { IRSFormCloneData, IRSFormData, IVersionCreatedResponse } from '@/models/rsform';
|
import { IRSFormCloneData, IRSFormData, IVersionCreatedResponse } from '@/models/rsform';
|
||||||
import { ITargetUser, ITargetUsers } from '@/models/user';
|
import { ITargetUser, ITargetUsers } from '@/models/user';
|
||||||
|
@ -109,7 +109,7 @@ export function patchSetEditors(target: string, request: FrontPush<ITargetUsers>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function postCreateVersion(target: string, request: FrontExchange<IVersionData, IVersionCreatedResponse>) {
|
export function postCreateVersion(target: string, request: FrontExchange<IVersionCreateData, IVersionCreatedResponse>) {
|
||||||
AxiosPost({
|
AxiosPost({
|
||||||
endpoint: `/api/library/${target}/create-version`,
|
endpoint: `/api/library/${target}/create-version`,
|
||||||
request: request
|
request: request
|
||||||
|
|
|
@ -1,10 +1,18 @@
|
||||||
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
|
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
|
||||||
import { CstMatchMode, DependencyMode } from '@/models/miscellaneous';
|
import { CstMatchMode, DependencyMode } from '@/models/miscellaneous';
|
||||||
import { ExpressionStatus } from '@/models/rsform';
|
import { CstType, ExpressionStatus } from '@/models/rsform';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IconAlias,
|
IconAlias,
|
||||||
IconBusiness,
|
IconBusiness,
|
||||||
|
IconCstAxiom,
|
||||||
|
IconCstBaseSet,
|
||||||
|
IconCstConstSet,
|
||||||
|
IconCstFunction,
|
||||||
|
IconCstPredicate,
|
||||||
|
IconCstStructured,
|
||||||
|
IconCstTerm,
|
||||||
|
IconCstTheorem,
|
||||||
IconFilter,
|
IconFilter,
|
||||||
IconFormula,
|
IconFormula,
|
||||||
IconGraphCollapse,
|
IconGraphCollapse,
|
||||||
|
@ -132,3 +140,24 @@ export function StatusIcon({ value, size = '1.25rem', className }: DomIconProps<
|
||||||
return <IconStatusError size={size} className={className} />;
|
return <IconStatusError size={size} className={className} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function CstTypeIcon({ value, size = '1.25rem', className }: DomIconProps<CstType>) {
|
||||||
|
switch (value) {
|
||||||
|
case CstType.BASE:
|
||||||
|
return <IconCstBaseSet size={size} className={className ?? 'clr-text-green'} />;
|
||||||
|
case CstType.CONSTANT:
|
||||||
|
return <IconCstConstSet size={size} className={className ?? 'clr-text-green'} />;
|
||||||
|
case CstType.STRUCTURED:
|
||||||
|
return <IconCstStructured size={size} className={className ?? 'clr-text-green'} />;
|
||||||
|
case CstType.TERM:
|
||||||
|
return <IconCstTerm size={size} className={className ?? 'clr-text-primary'} />;
|
||||||
|
case CstType.AXIOM:
|
||||||
|
return <IconCstAxiom size={size} className={className ?? 'clr-text-red'} />;
|
||||||
|
case CstType.FUNCTION:
|
||||||
|
return <IconCstFunction size={size} className={className ?? 'clr-text-primary'} />;
|
||||||
|
case CstType.PREDICATE:
|
||||||
|
return <IconCstPredicate size={size} className={className ?? 'clr-text-red'} />;
|
||||||
|
case CstType.THEOREM:
|
||||||
|
return <IconCstTheorem size={size} className={className ?? 'clr-text-red'} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -92,7 +92,7 @@ export { LuView as IconDBStructure } from 'react-icons/lu';
|
||||||
export { LuPlaneTakeoff as IconRESTapi } from 'react-icons/lu';
|
export { LuPlaneTakeoff as IconRESTapi } from 'react-icons/lu';
|
||||||
export { LuImage as IconImage } from 'react-icons/lu';
|
export { LuImage as IconImage } from 'react-icons/lu';
|
||||||
export { TbColumns as IconList } from 'react-icons/tb';
|
export { TbColumns as IconList } from 'react-icons/tb';
|
||||||
export { ImStack as IconVersions } from 'react-icons/im';
|
export { GoVersions as IconVersions } from 'react-icons/go';
|
||||||
export { TbColumnsOff as IconListOff } from 'react-icons/tb';
|
export { TbColumnsOff as IconListOff } from 'react-icons/tb';
|
||||||
export { LuAtSign as IconTerm } from 'react-icons/lu';
|
export { LuAtSign as IconTerm } from 'react-icons/lu';
|
||||||
export { LuSubscript as IconAlias } from 'react-icons/lu';
|
export { LuSubscript as IconAlias } from 'react-icons/lu';
|
||||||
|
@ -122,6 +122,7 @@ export { BiLeftArrow as IconMoveLeft } from 'react-icons/bi';
|
||||||
export { TbHexagonPlus2 as IconNewRSForm } from 'react-icons/tb';
|
export { TbHexagonPlus2 as IconNewRSForm } from 'react-icons/tb';
|
||||||
export { BiPlusCircle as IconNewItem } from 'react-icons/bi';
|
export { BiPlusCircle as IconNewItem } from 'react-icons/bi';
|
||||||
export { FaSquarePlus as IconNewItem2 } from 'react-icons/fa6';
|
export { FaSquarePlus as IconNewItem2 } from 'react-icons/fa6';
|
||||||
|
export { PiStackPlus as IconNewVersion } from 'react-icons/pi';
|
||||||
export { BiDuplicate as IconClone } from 'react-icons/bi';
|
export { BiDuplicate as IconClone } from 'react-icons/bi';
|
||||||
export { LuReplace as IconReplace } from 'react-icons/lu';
|
export { LuReplace as IconReplace } from 'react-icons/lu';
|
||||||
export { FaSortAmountDownAlt as IconSortList } from 'react-icons/fa';
|
export { FaSortAmountDownAlt as IconSortList } from 'react-icons/fa';
|
||||||
|
|
|
@ -5,12 +5,14 @@ import { useLayoutEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import DataTable, { createColumnHelper, RowSelectionState } from '@/components/ui/DataTable';
|
import DataTable, { createColumnHelper, RowSelectionState } from '@/components/ui/DataTable';
|
||||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||||
|
import { CstMatchMode } from '@/models/miscellaneous';
|
||||||
import { ConstituentaID, IConstituenta, IRSForm } from '@/models/rsform';
|
import { ConstituentaID, IConstituenta, IRSForm } from '@/models/rsform';
|
||||||
import { isBasicConcept } from '@/models/rsformAPI';
|
import { isBasicConcept, matchConstituenta } from '@/models/rsformAPI';
|
||||||
import { describeConstituenta } from '@/utils/labels';
|
import { describeConstituenta } from '@/utils/labels';
|
||||||
|
|
||||||
import BadgeConstituenta from '../info/BadgeConstituenta';
|
import BadgeConstituenta from '../info/BadgeConstituenta';
|
||||||
import NoData from '../ui/NoData';
|
import NoData from '../ui/NoData';
|
||||||
|
import SearchBar from '../ui/SearchBar';
|
||||||
import ToolbarGraphSelection from './ToolbarGraphSelection';
|
import ToolbarGraphSelection from './ToolbarGraphSelection';
|
||||||
|
|
||||||
interface PickMultiConstituentaProps {
|
interface PickMultiConstituentaProps {
|
||||||
|
@ -28,18 +30,30 @@ const columnHelper = createColumnHelper<IConstituenta>();
|
||||||
function PickMultiConstituenta({ id, schema, prefixID, rows, selected, setSelected }: PickMultiConstituentaProps) {
|
function PickMultiConstituenta({ id, schema, prefixID, rows, selected, setSelected }: PickMultiConstituentaProps) {
|
||||||
const { colors } = useConceptOptions();
|
const { colors } = useConceptOptions();
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||||
|
const [filtered, setFiltered] = useState<IConstituenta[]>(schema?.items ?? []);
|
||||||
|
const [filterText, setFilterText] = useState('');
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!schema || selected.length === 0) {
|
if (filtered.length === 0) {
|
||||||
setRowSelection({});
|
setRowSelection({});
|
||||||
} else {
|
return;
|
||||||
const newRowSelection: RowSelectionState = {};
|
|
||||||
schema.items.forEach((cst, index) => {
|
|
||||||
newRowSelection[String(index)] = selected.includes(cst.id);
|
|
||||||
});
|
|
||||||
setRowSelection(newRowSelection);
|
|
||||||
}
|
}
|
||||||
}, [selected, schema]);
|
const newRowSelection: RowSelectionState = {};
|
||||||
|
filtered.forEach((cst, index) => {
|
||||||
|
newRowSelection[String(index)] = selected.includes(cst.id);
|
||||||
|
});
|
||||||
|
setRowSelection(newRowSelection);
|
||||||
|
}, [filtered, setRowSelection, selected]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!schema || schema.items.length === 0) {
|
||||||
|
setFiltered([]);
|
||||||
|
} else if (filterText) {
|
||||||
|
setFiltered(schema.items.filter(cst => matchConstituenta(cst, filterText, CstMatchMode.ALL)));
|
||||||
|
} else {
|
||||||
|
setFiltered(schema.items);
|
||||||
|
}
|
||||||
|
}, [filterText, schema?.items, schema]);
|
||||||
|
|
||||||
function handleRowSelection(updater: React.SetStateAction<RowSelectionState>) {
|
function handleRowSelection(updater: React.SetStateAction<RowSelectionState>) {
|
||||||
if (!schema) {
|
if (!schema) {
|
||||||
|
@ -47,12 +61,12 @@ function PickMultiConstituenta({ id, schema, prefixID, rows, selected, setSelect
|
||||||
} else {
|
} else {
|
||||||
const newRowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
|
const newRowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
|
||||||
const newSelection: ConstituentaID[] = [];
|
const newSelection: ConstituentaID[] = [];
|
||||||
schema.items.forEach((cst, index) => {
|
filtered.forEach((cst, index) => {
|
||||||
if (newRowSelection[String(index)] === true) {
|
if (newRowSelection[String(index)] === true) {
|
||||||
newSelection.push(cst.id);
|
newSelection.push(cst.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setSelected(newSelection);
|
setSelected(prev => [...prev.filter(cst_id => !filtered.find(cst => cst.id === cst_id)), ...newSelection]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,10 +89,17 @@ function PickMultiConstituenta({ id, schema, prefixID, rows, selected, setSelect
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className='flex items-end gap-3 mb-3'>
|
<div className='flex justify-between items-center gap-3 clr-input px-3 border-x border-t rounded-t-md'>
|
||||||
<span className='w-[24ch] select-none whitespace-nowrap'>
|
<div className='w-[24ch] select-none whitespace-nowrap'>
|
||||||
Выбраны {selected.length} из {schema?.items.length ?? 0}
|
Выбраны {selected.length} из {schema?.items.length ?? 0}
|
||||||
</span>
|
</div>
|
||||||
|
<SearchBar
|
||||||
|
id='dlg_constituents_search'
|
||||||
|
noBorder
|
||||||
|
className='min-w-[6rem] pr-2 flex-grow'
|
||||||
|
value={filterText}
|
||||||
|
onChange={setFilterText}
|
||||||
|
/>
|
||||||
{schema ? (
|
{schema ? (
|
||||||
<ToolbarGraphSelection
|
<ToolbarGraphSelection
|
||||||
graph={schema.graph}
|
graph={schema.graph}
|
||||||
|
@ -86,7 +107,7 @@ function PickMultiConstituenta({ id, schema, prefixID, rows, selected, setSelect
|
||||||
isOwned={cstID => !schema.cstByID.get(cstID)?.is_inherited}
|
isOwned={cstID => !schema.cstByID.get(cstID)?.is_inherited}
|
||||||
setSelected={setSelected}
|
setSelected={setSelected}
|
||||||
emptySelection={selected.length === 0}
|
emptySelection={selected.length === 0}
|
||||||
className='w-full ml-8'
|
className='w-fit'
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
@ -97,7 +118,7 @@ function PickMultiConstituenta({ id, schema, prefixID, rows, selected, setSelect
|
||||||
rows={rows}
|
rows={rows}
|
||||||
contentHeight='1.3rem'
|
contentHeight='1.3rem'
|
||||||
className={clsx('cc-scroll-y', 'border', 'text-sm', 'select-none')}
|
className={clsx('cc-scroll-y', 'border', 'text-sm', 'select-none')}
|
||||||
data={schema?.items ?? []}
|
data={filtered}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
headPosition='0rem'
|
headPosition='0rem'
|
||||||
enableRowSelection
|
enableRowSelection
|
||||||
|
|
|
@ -96,6 +96,7 @@ function PickSchema({
|
||||||
<div className='border divide-y'>
|
<div className='border divide-y'>
|
||||||
<SearchBar
|
<SearchBar
|
||||||
id={id ? `${id}__search` : undefined}
|
id={id ? `${id}__search` : undefined}
|
||||||
|
className='clr-input'
|
||||||
noBorder
|
noBorder
|
||||||
value={filterText}
|
value={filterText}
|
||||||
onChange={newValue => setFilterText(newValue)}
|
onChange={newValue => setFilterText(newValue)}
|
||||||
|
|
|
@ -18,7 +18,7 @@ function Dropdown({ isOpen, stretchLeft, stretchTop, className, children, ...res
|
||||||
<motion.div
|
<motion.div
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'z-modalTooltip',
|
'z-topmost',
|
||||||
'absolute mt-3',
|
'absolute mt-3',
|
||||||
'flex flex-col',
|
'flex flex-col',
|
||||||
'border rounded-md shadow-lg',
|
'border rounded-md shadow-lg',
|
||||||
|
|
|
@ -27,7 +27,7 @@ function SearchBar({ id, value, noIcon, onChange, noBorder, placeholder = 'По
|
||||||
noOutline
|
noOutline
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
type='search'
|
type='search'
|
||||||
className={clsx('w-full outline-none', !noIcon && 'pl-10')}
|
className={clsx('w-full outline-none bg-transparent', !noIcon && 'pl-10')}
|
||||||
noBorder={noBorder}
|
noBorder={noBorder}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={event => (onChange ? onChange(event.target.value) : undefined)}
|
onChange={event => (onChange ? onChange(event.target.value) : undefined)}
|
||||||
|
|
|
@ -29,7 +29,7 @@ function Tooltip({
|
||||||
}
|
}
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<TooltipImpl
|
<TooltipImpl
|
||||||
delayShow={1000}
|
delayShow={750}
|
||||||
delayHide={100}
|
delayHide={100}
|
||||||
opacity={1}
|
opacity={1}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
|
|
@ -74,7 +74,7 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
|
||||||
spellCheck
|
spellCheck
|
||||||
label='Термин'
|
label='Термин'
|
||||||
placeholder='Обозначение, используемое в текстовых определениях'
|
placeholder='Обозначение, используемое в текстовых определениях'
|
||||||
rows={2}
|
className='cc-fit-content max-h-[3.6rem]'
|
||||||
value={state.term_raw}
|
value={state.term_raw}
|
||||||
onChange={event => partialUpdate({ term_raw: event.target.value })}
|
onChange={event => partialUpdate({ term_raw: event.target.value })}
|
||||||
/>
|
/>
|
||||||
|
@ -104,7 +104,7 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
|
||||||
spellCheck
|
spellCheck
|
||||||
label='Текстовое определение'
|
label='Текстовое определение'
|
||||||
placeholder='Текстовая интерпретация формального выражения'
|
placeholder='Текстовая интерпретация формального выражения'
|
||||||
rows={2}
|
className='cc-fit-content max-h-[3.6rem]'
|
||||||
value={state.definition_raw}
|
value={state.definition_raw}
|
||||||
onChange={event => partialUpdate({ definition_raw: event.target.value })}
|
onChange={event => partialUpdate({ definition_raw: event.target.value })}
|
||||||
/>
|
/>
|
||||||
|
@ -128,7 +128,7 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
|
||||||
spellCheck
|
spellCheck
|
||||||
label={isBasic ? 'Конвенция' : 'Комментарий'}
|
label={isBasic ? 'Конвенция' : 'Комментарий'}
|
||||||
placeholder={isBasic ? 'Договоренность об интерпретации' : 'Пояснение разработчика'}
|
placeholder={isBasic ? 'Договоренность об интерпретации' : 'Пояснение разработчика'}
|
||||||
rows={2}
|
className='cc-fit-content max-h-[5.4rem]'
|
||||||
value={state.convention}
|
value={state.convention}
|
||||||
onChange={event => partialUpdate({ convention: event.target.value })}
|
onChange={event => partialUpdate({ convention: event.target.value })}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -3,30 +3,38 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import Checkbox from '@/components/ui/Checkbox';
|
||||||
import Modal, { ModalProps } from '@/components/ui/Modal';
|
import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||||
import TextArea from '@/components/ui/TextArea';
|
import TextArea from '@/components/ui/TextArea';
|
||||||
import TextInput from '@/components/ui/TextInput';
|
import TextInput from '@/components/ui/TextInput';
|
||||||
import { IVersionData, IVersionInfo } from '@/models/library';
|
import { IVersionCreateData, IVersionInfo } from '@/models/library';
|
||||||
import { nextVersion } from '@/models/libraryAPI';
|
import { nextVersion } from '@/models/libraryAPI';
|
||||||
|
import { ConstituentaID } from '@/models/rsform';
|
||||||
|
|
||||||
interface DlgCreateVersionProps extends Pick<ModalProps, 'hideWindow'> {
|
interface DlgCreateVersionProps extends Pick<ModalProps, 'hideWindow'> {
|
||||||
versions: IVersionInfo[];
|
versions: IVersionInfo[];
|
||||||
onCreate: (data: IVersionData) => void;
|
onCreate: (data: IVersionCreateData) => void;
|
||||||
|
selected: ConstituentaID[];
|
||||||
|
totalCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DlgCreateVersion({ hideWindow, versions, onCreate }: DlgCreateVersionProps) {
|
function DlgCreateVersion({ hideWindow, versions, selected, totalCount, onCreate }: DlgCreateVersionProps) {
|
||||||
const [version, setVersion] = useState(versions.length > 0 ? nextVersion(versions[0].version) : '1.0.0');
|
const [version, setVersion] = useState(versions.length > 0 ? nextVersion(versions[0].version) : '1.0.0');
|
||||||
const [description, setDescription] = useState('');
|
const [description, setDescription] = useState('');
|
||||||
|
const [onlySelected, setOnlySelected] = useState(false);
|
||||||
|
|
||||||
const canSubmit = useMemo(() => {
|
const canSubmit = useMemo(() => {
|
||||||
return !versions.find(ver => ver.version === version);
|
return !versions.find(ver => ver.version === version);
|
||||||
}, [versions, version]);
|
}, [versions, version]);
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
const data: IVersionData = {
|
const data: IVersionCreateData = {
|
||||||
version: version,
|
version: version,
|
||||||
description: description
|
description: description
|
||||||
};
|
};
|
||||||
|
if (onlySelected) {
|
||||||
|
data.items = selected;
|
||||||
|
}
|
||||||
onCreate(data);
|
onCreate(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,6 +63,12 @@ function DlgCreateVersion({ hideWindow, versions, onCreate }: DlgCreateVersionPr
|
||||||
value={description}
|
value={description}
|
||||||
onChange={event => setDescription(event.target.value)}
|
onChange={event => setDescription(event.target.value)}
|
||||||
/>
|
/>
|
||||||
|
<Checkbox
|
||||||
|
id='dlg_only_selected'
|
||||||
|
label={`Только выбранные конституенты [${selected.length} из ${totalCount}]`}
|
||||||
|
value={onlySelected}
|
||||||
|
setValue={value => setOnlySelected(value)}
|
||||||
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { TabList, TabPanel, Tabs } from 'react-tabs';
|
import { TabList, TabPanel, Tabs } from 'react-tabs';
|
||||||
|
|
||||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||||
|
@ -18,6 +18,7 @@ import {
|
||||||
OperationID,
|
OperationID,
|
||||||
OperationType
|
OperationType
|
||||||
} from '@/models/oss';
|
} from '@/models/oss';
|
||||||
|
import { SubstitutionValidator } from '@/models/ossAPI';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
|
||||||
import TabArguments from './TabArguments';
|
import TabArguments from './TabArguments';
|
||||||
|
@ -44,6 +45,9 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
|
||||||
const [title, setTitle] = useState(target.title);
|
const [title, setTitle] = useState(target.title);
|
||||||
const [comment, setComment] = useState(target.comment);
|
const [comment, setComment] = useState(target.comment);
|
||||||
|
|
||||||
|
const [isCorrect, setIsCorrect] = useState(true);
|
||||||
|
const [validationText, setValidationText] = useState('');
|
||||||
|
|
||||||
const [inputs, setInputs] = useState<OperationID[]>(oss.graph.expandInputs([target.id]));
|
const [inputs, setInputs] = useState<OperationID[]>(oss.graph.expandInputs([target.id]));
|
||||||
const inputOperations = useMemo(() => inputs.map(id => oss.operationByID.get(id)!), [inputs, oss.operationByID]);
|
const inputOperations = useMemo(() => inputs.map(id => oss.operationByID.get(id)!), [inputs, oss.operationByID]);
|
||||||
const schemasIDs = useMemo(
|
const schemasIDs = useMemo(
|
||||||
|
@ -54,10 +58,10 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
|
||||||
const cache = useRSFormCache();
|
const cache = useRSFormCache();
|
||||||
const schemas = useMemo(
|
const schemas = useMemo(
|
||||||
() => schemasIDs.map(id => cache.getSchema(id)).filter(item => item !== undefined),
|
() => schemasIDs.map(id => cache.getSchema(id)).filter(item => item !== undefined),
|
||||||
[schemasIDs, cache]
|
[schemasIDs, cache.getSchema]
|
||||||
);
|
);
|
||||||
|
|
||||||
const isValid = useMemo(() => alias !== '', [alias]);
|
const canSubmit = useMemo(() => alias !== '', [alias]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
cache.preload(schemasIDs);
|
cache.preload(schemasIDs);
|
||||||
|
@ -82,7 +86,16 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
|
||||||
);
|
);
|
||||||
}, [schemasIDs, schemas, cache.loading]);
|
}, [schemasIDs, schemas, cache.loading]);
|
||||||
|
|
||||||
const handleSubmit = () => {
|
useEffect(() => {
|
||||||
|
if (cache.loading || schemas.length !== schemasIDs.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const validator = new SubstitutionValidator(schemas, substitutions);
|
||||||
|
setIsCorrect(validator.validate());
|
||||||
|
setValidationText(validator.msg);
|
||||||
|
}, [substitutions, cache.loading, schemas, schemasIDs.length]);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(() => {
|
||||||
const data: IOperationUpdateData = {
|
const data: IOperationUpdateData = {
|
||||||
target: target.id,
|
target: target.id,
|
||||||
item_data: {
|
item_data: {
|
||||||
|
@ -95,7 +108,7 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
|
||||||
substitutions: target.operation_type !== OperationType.SYNTHESIS ? undefined : substitutions
|
substitutions: target.operation_type !== OperationType.SYNTHESIS ? undefined : substitutions
|
||||||
};
|
};
|
||||||
onSubmit(data);
|
onSubmit(data);
|
||||||
};
|
}, [alias, comment, title, inputs, substitutions, target, onSubmit]);
|
||||||
|
|
||||||
const cardPanel = useMemo(
|
const cardPanel = useMemo(
|
||||||
() => (
|
() => (
|
||||||
|
@ -134,12 +147,14 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
|
||||||
schemas={schemas}
|
schemas={schemas}
|
||||||
loading={cache.loading}
|
loading={cache.loading}
|
||||||
error={cache.error}
|
error={cache.error}
|
||||||
|
validationText={validationText}
|
||||||
|
isCorrect={isCorrect}
|
||||||
substitutions={substitutions}
|
substitutions={substitutions}
|
||||||
setSubstitutions={setSubstitutions}
|
setSubstitutions={setSubstitutions}
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
),
|
),
|
||||||
[cache.loading, cache.error, substitutions, schemas]
|
[cache.loading, cache.error, substitutions, schemas, validationText, isCorrect]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -147,7 +162,7 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
|
||||||
header='Редактирование операции'
|
header='Редактирование операции'
|
||||||
submitText='Сохранить'
|
submitText='Сохранить'
|
||||||
hideWindow={hideWindow}
|
hideWindow={hideWindow}
|
||||||
canSubmit={isValid}
|
canSubmit={canSubmit}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
className='w-[40rem] px-6 min-h-[35rem]'
|
className='w-[40rem] px-6 min-h-[35rem]'
|
||||||
>
|
>
|
||||||
|
@ -167,7 +182,11 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
|
||||||
<TabLabel title='Выбор аргументов операции' label='Аргументы' className='w-[8rem]' />
|
<TabLabel title='Выбор аргументов операции' label='Аргументы' className='w-[8rem]' />
|
||||||
) : null}
|
) : null}
|
||||||
{target.operation_type === OperationType.SYNTHESIS ? (
|
{target.operation_type === OperationType.SYNTHESIS ? (
|
||||||
<TabLabel title='Таблица отождествлений' label='Отождествления' className='w-[8rem]' />
|
<TabLabel
|
||||||
|
titleHtml={'Таблица отождествлений' + (isCorrect ? '' : '<br/>(не прошла проверку)')}
|
||||||
|
label={isCorrect ? 'Отождествления' : 'Отождествления*'}
|
||||||
|
className='w-[8rem]'
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</TabList>
|
</TabList>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { ErrorData } from '@/components/info/InfoError';
|
import { ErrorData } from '@/components/info/InfoError';
|
||||||
import PickSubstitutions from '@/components/select/PickSubstitutions';
|
import PickSubstitutions from '@/components/select/PickSubstitutions';
|
||||||
|
import TextArea from '@/components/ui/TextArea';
|
||||||
import DataLoader from '@/components/wrap/DataLoader';
|
import DataLoader from '@/components/wrap/DataLoader';
|
||||||
|
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||||
import { ICstSubstitute } from '@/models/oss';
|
import { ICstSubstitute } from '@/models/oss';
|
||||||
import { IRSForm } from '@/models/rsform';
|
import { IRSForm } from '@/models/rsform';
|
||||||
import { prefixes } from '@/utils/constants';
|
import { prefixes } from '@/utils/constants';
|
||||||
|
@ -8,22 +10,34 @@ import { prefixes } from '@/utils/constants';
|
||||||
interface TabSynthesisProps {
|
interface TabSynthesisProps {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: ErrorData;
|
error: ErrorData;
|
||||||
|
validationText: string;
|
||||||
|
isCorrect: boolean;
|
||||||
|
|
||||||
schemas: IRSForm[];
|
schemas: IRSForm[];
|
||||||
substitutions: ICstSubstitute[];
|
substitutions: ICstSubstitute[];
|
||||||
setSubstitutions: React.Dispatch<React.SetStateAction<ICstSubstitute[]>>;
|
setSubstitutions: React.Dispatch<React.SetStateAction<ICstSubstitute[]>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabSynthesis({ schemas, loading, error, substitutions, setSubstitutions }: TabSynthesisProps) {
|
function TabSynthesis({
|
||||||
|
schemas,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
validationText,
|
||||||
|
isCorrect,
|
||||||
|
substitutions,
|
||||||
|
setSubstitutions
|
||||||
|
}: TabSynthesisProps) {
|
||||||
|
const { colors } = useConceptOptions();
|
||||||
return (
|
return (
|
||||||
<DataLoader id='dlg-synthesis-tab' className='cc-column mt-3' isLoading={loading} error={error}>
|
<DataLoader id='dlg-synthesis-tab' className='cc-column mt-3' isLoading={loading} error={error}>
|
||||||
<PickSubstitutions
|
<PickSubstitutions
|
||||||
schemas={schemas}
|
schemas={schemas}
|
||||||
prefixID={prefixes.dlg_cst_substitutes_list}
|
prefixID={prefixes.dlg_cst_substitutes_list}
|
||||||
rows={8}
|
rows={10}
|
||||||
substitutions={substitutions}
|
substitutions={substitutions}
|
||||||
setSubstitutions={setSubstitutions}
|
setSubstitutions={setSubstitutions}
|
||||||
/>
|
/>
|
||||||
|
<TextArea disabled value={validationText} style={{ borderColor: isCorrect ? undefined : colors.fgRed }} />
|
||||||
</DataLoader>
|
</DataLoader>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,9 @@ function useRSFormCache() {
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (pending.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const ids = pending.filter(id => !processing.includes(id) && !cache.find(schema => schema.id === id));
|
const ids = pending.filter(id => !processing.includes(id) && !cache.find(schema => schema.id === id));
|
||||||
setPending([]);
|
setPending([]);
|
||||||
if (ids.length === 0) {
|
if (ids.length === 0) {
|
||||||
|
|
|
@ -159,4 +159,40 @@ describe('Testing Graph queries', () => {
|
||||||
expect(graph.maximizePart([3, 2])).toStrictEqual([3, 2, 6, 4]);
|
expect(graph.maximizePart([3, 2])).toStrictEqual([3, 2, 6, 4]);
|
||||||
expect(graph.maximizePart([3, 1])).toStrictEqual([3, 1, 7, 5, 6]);
|
expect(graph.maximizePart([3, 1])).toStrictEqual([3, 1, 7, 5, 6]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('find elementary cycle', () => {
|
||||||
|
const graph = new Graph([
|
||||||
|
[1, 1] //
|
||||||
|
]);
|
||||||
|
expect(graph.findCycle()).toStrictEqual([1, 1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('find cycle acyclic', () => {
|
||||||
|
const graph = new Graph([
|
||||||
|
[1, 2], //
|
||||||
|
[2]
|
||||||
|
]);
|
||||||
|
expect(graph.findCycle()).toStrictEqual(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('find cycle typical', () => {
|
||||||
|
const graph = new Graph([
|
||||||
|
[1, 2], //
|
||||||
|
[1, 4],
|
||||||
|
[2, 3],
|
||||||
|
[3, 1],
|
||||||
|
[3, 4],
|
||||||
|
[4]
|
||||||
|
]);
|
||||||
|
expect(graph.findCycle()).toStrictEqual([1, 2, 3, 1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('find cycle acyclic 2 components', () => {
|
||||||
|
const graph = new Graph([
|
||||||
|
[0, 1], //
|
||||||
|
[2, 3],
|
||||||
|
[3, 0]
|
||||||
|
]);
|
||||||
|
expect(graph.findCycle()).toStrictEqual(null);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -293,4 +293,60 @@ export class Graph {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a cycle in the graph.
|
||||||
|
*
|
||||||
|
* @returns {number[] | null} The cycle if found, otherwise `null`.
|
||||||
|
* Uses non-recursive DFS.
|
||||||
|
*/
|
||||||
|
findCycle(): number[] | null {
|
||||||
|
const visited = new Set<number>();
|
||||||
|
const nodeStack = new Set<number>();
|
||||||
|
const parents = new Map<number, number>();
|
||||||
|
|
||||||
|
for (const nodeId of this.nodes.keys()) {
|
||||||
|
if (visited.has(nodeId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const callStack: { nodeId: number; parentId: number | null }[] = [];
|
||||||
|
callStack.push({ nodeId: nodeId, parentId: null });
|
||||||
|
while (callStack.length > 0) {
|
||||||
|
const { nodeId, parentId } = callStack[callStack.length - 1];
|
||||||
|
if (visited.has(nodeId)) {
|
||||||
|
nodeStack.delete(nodeId);
|
||||||
|
callStack.pop();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
visited.add(nodeId);
|
||||||
|
nodeStack.add(nodeId);
|
||||||
|
if (parentId !== null) {
|
||||||
|
parents.set(nodeId, parentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentNode = this.nodes.get(nodeId)!;
|
||||||
|
for (const child of currentNode.outputs) {
|
||||||
|
if (!visited.has(child)) {
|
||||||
|
callStack.push({ nodeId: child, parentId: nodeId });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!nodeStack.has(child)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const cycle: number[] = [];
|
||||||
|
let current = nodeId;
|
||||||
|
cycle.push(child);
|
||||||
|
while (current !== child) {
|
||||||
|
cycle.push(current);
|
||||||
|
current = parents.get(current)!;
|
||||||
|
}
|
||||||
|
cycle.push(child);
|
||||||
|
cycle.reverse();
|
||||||
|
return cycle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
* Module: Models for LibraryItem.
|
* Module: Models for LibraryItem.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { ConstituentaID } from './rsform';
|
||||||
import { UserID } from './user';
|
import { UserID } from './user';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -52,10 +53,17 @@ export interface IVersionInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents user data, intended to create or update version metadata in persistent storage.
|
* Represents version data, intended to update version metadata in persistent storage.
|
||||||
*/
|
*/
|
||||||
export interface IVersionData extends Omit<IVersionInfo, 'id' | 'time_create'> {}
|
export interface IVersionData extends Omit<IVersionInfo, 'id' | 'time_create'> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create version metadata in persistent storage.
|
||||||
|
*/
|
||||||
|
export interface IVersionCreateData extends IVersionData {
|
||||||
|
items?: ConstituentaID[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents library item common data typical for all item types.
|
* Represents library item common data typical for all item types.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -192,3 +192,27 @@ export interface IInputCreatedResponse {
|
||||||
new_schema: ILibraryItem;
|
new_schema: ILibraryItem;
|
||||||
oss: IOperationSchemaData;
|
oss: IOperationSchemaData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents substitution error description.
|
||||||
|
*/
|
||||||
|
export interface ISubstitutionErrorDescription {
|
||||||
|
errorType: SubstitutionErrorType;
|
||||||
|
params: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents Substitution table error types.
|
||||||
|
*/
|
||||||
|
export enum SubstitutionErrorType {
|
||||||
|
invalidIDs,
|
||||||
|
incorrectCst,
|
||||||
|
invalidClasses,
|
||||||
|
invalidBasic,
|
||||||
|
invalidConstant,
|
||||||
|
typificationCycle,
|
||||||
|
baseSubstitutionNotSet,
|
||||||
|
unequalTypification,
|
||||||
|
unequalArgsCount,
|
||||||
|
unequalArgs
|
||||||
|
}
|
||||||
|
|
|
@ -2,10 +2,15 @@
|
||||||
* Module: API for OperationSystem.
|
* Module: API for OperationSystem.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { describeSubstitutionError, information } from '@/utils/labels';
|
||||||
import { TextMatcher } from '@/utils/utils';
|
import { TextMatcher } from '@/utils/utils';
|
||||||
|
|
||||||
import { ILibraryItem } from './library';
|
import { Graph } from './Graph';
|
||||||
import { IOperation, IOperationSchema } from './oss';
|
import { ILibraryItem, LibraryItemID } from './library';
|
||||||
|
import { ICstSubstitute, IOperation, IOperationSchema, SubstitutionErrorType } from './oss';
|
||||||
|
import { ConstituentaID, CstType, IConstituenta, IRSForm } from './rsform';
|
||||||
|
import { AliasMapping, ParsingStatus } from './rslang';
|
||||||
|
import { applyAliasMapping, applyTypificationMapping, extractGlobals, isSetTypification } from './rslangAPI';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a given target {@link IOperation} matches the specified query using.
|
* Checks if a given target {@link IOperation} matches the specified query using.
|
||||||
|
@ -43,3 +48,278 @@ export function sortItemsForOSS(oss: IOperationSchema, items: ILibraryItem[]): I
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CrossMapping = Map<LibraryItemID, AliasMapping>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validator for Substitution table.
|
||||||
|
*/
|
||||||
|
export class SubstitutionValidator {
|
||||||
|
public msg: string = '';
|
||||||
|
|
||||||
|
private schemas: IRSForm[];
|
||||||
|
private substitutions: ICstSubstitute[];
|
||||||
|
private cstByID = new Map<ConstituentaID, IConstituenta>();
|
||||||
|
private schemaByID = new Map<LibraryItemID, IRSForm>();
|
||||||
|
private schemaByCst = new Map<ConstituentaID, IRSForm>();
|
||||||
|
|
||||||
|
constructor(schemas: IRSForm[], substitutions: ICstSubstitute[]) {
|
||||||
|
this.schemas = schemas;
|
||||||
|
this.substitutions = substitutions;
|
||||||
|
schemas.forEach(schema => {
|
||||||
|
this.schemaByID.set(schema.id, schema);
|
||||||
|
schema.items.forEach(item => {
|
||||||
|
this.cstByID.set(item.id, item);
|
||||||
|
this.schemaByCst.set(item.id, schema);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public validate(): boolean {
|
||||||
|
if (this.substitutions.length === 0) {
|
||||||
|
return this.setValid();
|
||||||
|
}
|
||||||
|
if (!this.checkTypes()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!this.checkCycles()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!this.checkTypifications()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.setValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkTypes(): boolean {
|
||||||
|
for (const item of this.substitutions) {
|
||||||
|
const original = this.cstByID.get(item.original);
|
||||||
|
const substitution = this.cstByID.get(item.substitution);
|
||||||
|
if (!original || !substitution) {
|
||||||
|
return this.reportError(SubstitutionErrorType.invalidIDs, []);
|
||||||
|
}
|
||||||
|
if (original.parse.status === ParsingStatus.INCORRECT || substitution.parse.status === ParsingStatus.INCORRECT) {
|
||||||
|
return this.reportError(SubstitutionErrorType.incorrectCst, [substitution.alias, original.alias]);
|
||||||
|
}
|
||||||
|
switch (substitution.cst_type) {
|
||||||
|
case CstType.BASE: {
|
||||||
|
if (original.cst_type !== CstType.BASE && original.cst_type !== CstType.CONSTANT) {
|
||||||
|
return this.reportError(SubstitutionErrorType.invalidBasic, [substitution.alias, original.alias]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case CstType.CONSTANT: {
|
||||||
|
if (original.cst_type !== CstType.CONSTANT) {
|
||||||
|
return this.reportError(SubstitutionErrorType.invalidConstant, [substitution.alias, original.alias]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case CstType.AXIOM:
|
||||||
|
case CstType.THEOREM: {
|
||||||
|
if (original.cst_type !== CstType.AXIOM && original.cst_type !== CstType.THEOREM) {
|
||||||
|
return this.reportError(SubstitutionErrorType.invalidClasses, [substitution.alias, original.alias]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case CstType.FUNCTION: {
|
||||||
|
if (original.cst_type !== CstType.FUNCTION) {
|
||||||
|
return this.reportError(SubstitutionErrorType.invalidClasses, [substitution.alias, original.alias]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CstType.PREDICATE: {
|
||||||
|
if (original.cst_type !== CstType.PREDICATE) {
|
||||||
|
return this.reportError(SubstitutionErrorType.invalidClasses, [substitution.alias, original.alias]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case CstType.TERM:
|
||||||
|
case CstType.STRUCTURED: {
|
||||||
|
if (
|
||||||
|
original.cst_type !== CstType.TERM &&
|
||||||
|
original.cst_type !== CstType.STRUCTURED &&
|
||||||
|
original.cst_type !== CstType.BASE
|
||||||
|
) {
|
||||||
|
return this.reportError(SubstitutionErrorType.invalidClasses, [substitution.alias, original.alias]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkCycles(): boolean {
|
||||||
|
const graph = new Graph();
|
||||||
|
for (const schema of this.schemas) {
|
||||||
|
for (const cst of schema.items) {
|
||||||
|
if (cst.cst_type === CstType.BASE || cst.cst_type === CstType.CONSTANT) {
|
||||||
|
graph.addNode(cst.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const item of this.substitutions) {
|
||||||
|
const original = this.cstByID.get(item.original)!;
|
||||||
|
const substitution = this.cstByID.get(item.substitution)!;
|
||||||
|
for (const cst of [original, substitution]) {
|
||||||
|
if (cst.cst_type === CstType.BASE || cst.cst_type === CstType.CONSTANT) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
graph.addNode(cst.id);
|
||||||
|
const parents = extractGlobals(cst.parse.typification);
|
||||||
|
for (const arg of cst.parse.args) {
|
||||||
|
for (const alias of extractGlobals(arg.typification)) {
|
||||||
|
parents.add(alias);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (parents.size === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const schema = this.schemaByID.get(cst.schema)!;
|
||||||
|
for (const alias of parents) {
|
||||||
|
const parent = schema.cstByAlias.get(alias);
|
||||||
|
if (parent) {
|
||||||
|
graph.addEdge(parent.id, cst.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
graph.addEdge(substitution.id, original.id);
|
||||||
|
}
|
||||||
|
const cycle = graph.findCycle();
|
||||||
|
if (cycle !== null) {
|
||||||
|
const cycleMsg = cycle
|
||||||
|
.map(id => {
|
||||||
|
const cst = this.cstByID.get(id)!;
|
||||||
|
const schema = this.schemaByID.get(cst.schema)!;
|
||||||
|
return `[${schema.alias}]-${cst.alias}`;
|
||||||
|
})
|
||||||
|
.join(', ');
|
||||||
|
return this.reportError(SubstitutionErrorType.typificationCycle, [cycleMsg]);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkTypifications(): boolean {
|
||||||
|
const baseMappings = this.prepareBaseMappings();
|
||||||
|
const typeMappings = this.calculateSubstituteMappings(baseMappings);
|
||||||
|
if (typeMappings === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const item of this.substitutions) {
|
||||||
|
const original = this.cstByID.get(item.original)!;
|
||||||
|
if (original.cst_type === CstType.BASE || original.cst_type === CstType.CONSTANT) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const substitution = this.cstByID.get(item.substitution)!;
|
||||||
|
const originalType = applyTypificationMapping(
|
||||||
|
applyAliasMapping(original.parse.typification, baseMappings.get(original.schema)!),
|
||||||
|
typeMappings
|
||||||
|
);
|
||||||
|
const substitutionType = applyTypificationMapping(
|
||||||
|
applyAliasMapping(substitution.parse.typification, baseMappings.get(substitution.schema)!),
|
||||||
|
typeMappings
|
||||||
|
);
|
||||||
|
if (originalType !== substitutionType) {
|
||||||
|
return this.reportError(SubstitutionErrorType.unequalTypification, [substitution.alias, original.alias]);
|
||||||
|
}
|
||||||
|
if (original.parse.args.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (substitution.parse.args.length !== original.parse.args.length) {
|
||||||
|
return this.reportError(SubstitutionErrorType.unequalArgsCount, [substitution.alias, original.alias]);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < original.parse.args.length; ++i) {
|
||||||
|
const originalArg = applyTypificationMapping(
|
||||||
|
applyAliasMapping(original.parse.args[i].typification, baseMappings.get(original.schema)!),
|
||||||
|
typeMappings
|
||||||
|
);
|
||||||
|
const substitutionArg = applyTypificationMapping(
|
||||||
|
applyAliasMapping(substitution.parse.args[i].typification, baseMappings.get(substitution.schema)!),
|
||||||
|
typeMappings
|
||||||
|
);
|
||||||
|
if (originalArg !== substitutionArg) {
|
||||||
|
return this.reportError(SubstitutionErrorType.unequalArgs, [substitution.alias, original.alias]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private prepareBaseMappings(): CrossMapping {
|
||||||
|
const result: CrossMapping = new Map();
|
||||||
|
let baseCount = 0;
|
||||||
|
let constCount = 0;
|
||||||
|
for (const schema of this.schemas) {
|
||||||
|
const mapping: AliasMapping = {};
|
||||||
|
for (const cst of schema.items) {
|
||||||
|
if (cst.cst_type === CstType.BASE) {
|
||||||
|
baseCount++;
|
||||||
|
mapping[cst.alias] = `X${baseCount}`;
|
||||||
|
} else if (cst.cst_type === CstType.CONSTANT) {
|
||||||
|
constCount++;
|
||||||
|
mapping[cst.alias] = `C${constCount}`;
|
||||||
|
}
|
||||||
|
result.set(schema.id, mapping);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateSubstituteMappings(baseMappings: CrossMapping): AliasMapping | null {
|
||||||
|
const result: AliasMapping = {};
|
||||||
|
const processed = new Set<string>();
|
||||||
|
for (const item of this.substitutions) {
|
||||||
|
const original = this.cstByID.get(item.original)!;
|
||||||
|
if (original.cst_type !== CstType.BASE && original.cst_type !== CstType.CONSTANT) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const originalAlias = baseMappings.get(original.schema)![original.alias];
|
||||||
|
|
||||||
|
const substitution = this.cstByID.get(item.substitution)!;
|
||||||
|
let substitutionText = '';
|
||||||
|
if (substitution.cst_type === original.cst_type) {
|
||||||
|
substitutionText = baseMappings.get(substitution.schema)![substitution.alias];
|
||||||
|
} else {
|
||||||
|
substitutionText = applyAliasMapping(substitution.parse.typification, baseMappings.get(substitution.schema)!);
|
||||||
|
substitutionText = applyTypificationMapping(substitutionText, result);
|
||||||
|
console.log(substitutionText);
|
||||||
|
if (!isSetTypification(substitutionText)) {
|
||||||
|
this.reportError(SubstitutionErrorType.baseSubstitutionNotSet, [
|
||||||
|
substitution.alias,
|
||||||
|
substitution.parse.typification
|
||||||
|
]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (substitutionText.includes('×') || substitutionText.startsWith('ℬℬ')) {
|
||||||
|
substitutionText = substitutionText.slice(1);
|
||||||
|
} else {
|
||||||
|
substitutionText = substitutionText.slice(2, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const prevAlias of processed) {
|
||||||
|
result[prevAlias] = applyTypificationMapping(result[prevAlias], { [originalAlias]: substitutionText });
|
||||||
|
}
|
||||||
|
result[originalAlias] = substitutionText;
|
||||||
|
processed.add(originalAlias);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setValid(): boolean {
|
||||||
|
this.msg = information.substitutionsCorrect;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private reportError(errorType: SubstitutionErrorType, params: string[]): boolean {
|
||||||
|
this.msg = describeSubstitutionError({
|
||||||
|
errorType: errorType,
|
||||||
|
params: params
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,11 @@
|
||||||
* Module: Models for RSLanguage.
|
* Module: Models for RSLanguage.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents alias mapping.
|
||||||
|
*/
|
||||||
|
export type AliasMapping = Record<string, string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents formal expression.
|
* Represents formal expression.
|
||||||
*/
|
*/
|
||||||
|
@ -199,6 +204,9 @@ export enum TokenID {
|
||||||
END
|
END
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents RSLang expression error types.
|
||||||
|
*/
|
||||||
export enum RSErrorType {
|
export enum RSErrorType {
|
||||||
unknownSymbol = 33283,
|
unknownSymbol = 33283,
|
||||||
syntax = 33792,
|
syntax = 33792,
|
||||||
|
|
|
@ -5,12 +5,13 @@
|
||||||
import { applyPattern } from '@/utils/utils';
|
import { applyPattern } from '@/utils/utils';
|
||||||
|
|
||||||
import { CstType } from './rsform';
|
import { CstType } from './rsform';
|
||||||
import { IArgumentValue, IRSErrorDescription, RSErrorClass, RSErrorType } from './rslang';
|
import { AliasMapping, IArgumentValue, IRSErrorDescription, RSErrorClass, RSErrorType } from './rslang';
|
||||||
|
|
||||||
// cspell:disable
|
// cspell:disable
|
||||||
const LOCALS_REGEXP = /[_a-zα-ω][a-zα-ω]*\d*/g;
|
const LOCALS_REGEXP = /[_a-zα-ω][a-zα-ω]*\d*/g;
|
||||||
const GLOBALS_REGEXP = /[XCSADFPT]\d+/g;
|
const GLOBALS_REGEXP = /[XCSADFPT]\d+/g;
|
||||||
const COMPLEX_SYMBOLS_REGEXP = /[∀∃×ℬ;|:]/g;
|
const COMPLEX_SYMBOLS_REGEXP = /[∀∃×ℬ;|:]/g;
|
||||||
|
const TYPIFICATION_SET = /^ℬ+\([ℬ\(X\d+\)×]*\)$/g;
|
||||||
// cspell:enable
|
// cspell:enable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -27,6 +28,13 @@ export function isSimpleExpression(text: string): boolean {
|
||||||
return !text.match(COMPLEX_SYMBOLS_REGEXP);
|
return !text.match(COMPLEX_SYMBOLS_REGEXP);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if expression is set typification.
|
||||||
|
*/
|
||||||
|
export function isSetTypification(text: string): boolean {
|
||||||
|
return !!text.match(TYPIFICATION_SET);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Infers type of constituent for a given template and arguments.
|
* Infers type of constituent for a given template and arguments.
|
||||||
*/
|
*/
|
||||||
|
@ -91,7 +99,7 @@ export function substituteTemplateArgs(expression: string, args: IArgumentValue[
|
||||||
return expression;
|
return expression;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapping: Record<string, string> = {};
|
const mapping: AliasMapping = {};
|
||||||
args
|
args
|
||||||
.filter(arg => !!arg.value)
|
.filter(arg => !!arg.value)
|
||||||
.forEach(arg => {
|
.forEach(arg => {
|
||||||
|
@ -144,3 +152,25 @@ export function getRSErrorPrefix(error: IRSErrorDescription): string {
|
||||||
case RSErrorClass.UNKNOWN: return 'U' + id;
|
case RSErrorClass.UNKNOWN: return 'U' + id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply alias mapping.
|
||||||
|
*/
|
||||||
|
export function applyAliasMapping(target: string, mapping: AliasMapping): string {
|
||||||
|
return applyPattern(target, mapping, GLOBALS_REGEXP);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply alias typification mapping.
|
||||||
|
*/
|
||||||
|
export function applyTypificationMapping(target: string, mapping: AliasMapping): string {
|
||||||
|
const result = applyAliasMapping(target, mapping);
|
||||||
|
if (result === target) {
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove double parentheses
|
||||||
|
// deal with ℬ(ℬ)
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
|
@ -184,8 +184,8 @@ function FormCreateItem() {
|
||||||
onChange={event => setComment(event.target.value)}
|
onChange={event => setComment(event.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='flex justify-between gap-3'>
|
<div className='flex justify-between gap-3 flex-grow'>
|
||||||
<div className='flex flex-col gap-2 w-[7rem] h-min'>
|
<div className='flex flex-col gap-2 min-w-[7rem] h-min'>
|
||||||
<Label text='Корень' />
|
<Label text='Корень' />
|
||||||
<SelectLocationHead
|
<SelectLocationHead
|
||||||
value={head}
|
value={head}
|
||||||
|
@ -197,7 +197,6 @@ function FormCreateItem() {
|
||||||
<TextArea
|
<TextArea
|
||||||
id='dlg_cst_body'
|
id='dlg_cst_body'
|
||||||
label='Путь'
|
label='Путь'
|
||||||
className='w-[18rem]'
|
|
||||||
rows={4}
|
rows={4}
|
||||||
value={body}
|
value={body}
|
||||||
onChange={event => setBody(event.target.value)}
|
onChange={event => setBody(event.target.value)}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { IconEditor, IconNewItem, IconShare, IconUpload, IconVersions } from '@/components/Icons';
|
import { IconEditor, IconNewVersion, IconShare, IconUpload, IconVersions } from '@/components/Icons';
|
||||||
|
|
||||||
function HelpVersions() {
|
function HelpVersions() {
|
||||||
return (
|
return (
|
||||||
|
@ -18,7 +18,7 @@ function HelpVersions() {
|
||||||
<IconUpload size='1.25rem' className='inline-icon icon-red' /> Загрузить версию в актуальную схему
|
<IconUpload size='1.25rem' className='inline-icon icon-red' /> Загрузить версию в актуальную схему
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconNewItem size='1.25rem' className='inline-icon icon-green' /> Создать версию можно только из актуальной
|
<IconNewVersion size='1.25rem' className='inline-icon icon-green' /> Создать версию можно только из актуальной
|
||||||
схемы
|
схемы
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
|
|
@ -52,9 +52,10 @@ function HelpConceptOSS() {
|
||||||
<p>
|
<p>
|
||||||
После задания аргументов и таблицы отождествления необходимо единожды{' '}
|
После задания аргументов и таблицы отождествления необходимо единожды{' '}
|
||||||
<span className='text-nowrap'>
|
<span className='text-nowrap'>
|
||||||
<IconExecute className='inline-icon icon-green' /> выполнить Синтез
|
<IconExecute className='inline-icon icon-green' /> активировать Синтез
|
||||||
</span>
|
</span>
|
||||||
, чтобы активировать <LinkTopic text='сквозные изменения' topic={HelpTopic.CC_PROPAGATION} />.
|
, чтобы выполнить операцию и активировать{' '}
|
||||||
|
<LinkTopic text='сквозные изменения' topic={HelpTopic.CC_PROPAGATION} />.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className='text-nowrap'>
|
<span className='text-nowrap'>
|
||||||
|
|
|
@ -104,7 +104,7 @@ function HelpOssGraph() {
|
||||||
<IconConnect className='inline-icon' /> Выбрать КС для загрузки
|
<IconConnect className='inline-icon' /> Выбрать КС для загрузки
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconExecute className='inline-icon icon-green' /> Выполнить (активировать) операцию
|
<IconExecute className='inline-icon icon-green' /> Активировать операцию
|
||||||
</li>
|
</li>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
IconEdit2,
|
IconEdit2,
|
||||||
IconEditor,
|
IconEditor,
|
||||||
IconMenu,
|
IconMenu,
|
||||||
|
IconNewVersion,
|
||||||
IconOwner,
|
IconOwner,
|
||||||
IconReader,
|
IconReader,
|
||||||
IconShare,
|
IconShare,
|
||||||
|
@ -53,6 +54,9 @@ function HelpRSMenu() {
|
||||||
<li>
|
<li>
|
||||||
<IconClone className='inline-icon icon-green' /> Клонировать – создать копию схемы
|
<IconClone className='inline-icon icon-green' /> Клонировать – создать копию схемы
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconNewVersion size='1.25rem' className='inline-icon icon-green' /> Сохранить версию
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconDownload className='inline-icon' /> Выгрузить – сохранить в файле формата Экстеор
|
<IconDownload className='inline-icon' /> Выгрузить – сохранить в файле формата Экстеор
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -140,10 +140,10 @@ function NodeContextMenu({
|
||||||
) : null}
|
) : null}
|
||||||
{controller.isMutable && !operation.result && operation.operation_type === OperationType.SYNTHESIS ? (
|
{controller.isMutable && !operation.result && operation.operation_type === OperationType.SYNTHESIS ? (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='Выполнить синтез'
|
text='Активировать синтез'
|
||||||
title={
|
titleHtml={
|
||||||
readyForSynthesis
|
readyForSynthesis
|
||||||
? 'Выполнить операцию и получить синтезированную КС'
|
? 'Активировать операцию<br/>и получить синтезированную КС'
|
||||||
: 'Необходимо предоставить все аргументы'
|
: 'Необходимо предоставить все аргументы'
|
||||||
}
|
}
|
||||||
icon={<IconExecute size='1rem' className='icon-green' />}
|
icon={<IconExecute size='1rem' className='icon-green' />}
|
||||||
|
|
|
@ -161,7 +161,7 @@ function ToolbarOssGraph({
|
||||||
onClick={onCreate}
|
onClick={onCreate}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Выполнить операцию'
|
title='Активировать операцию'
|
||||||
icon={<IconExecute size='1.25rem' className='icon-green' />}
|
icon={<IconExecute size='1.25rem' className='icon-green' />}
|
||||||
disabled={controller.isProcessing || controller.selected.length !== 1 || !readyForSynthesis}
|
disabled={controller.isProcessing || controller.selected.length !== 1 || !readyForSynthesis}
|
||||||
onClick={onExecute}
|
onClick={onExecute}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import Loader from '@/components/ui/Loader';
|
||||||
import TabLabel from '@/components/ui/TabLabel';
|
import TabLabel from '@/components/ui/TabLabel';
|
||||||
import TextURL from '@/components/ui/TextURL';
|
import TextURL from '@/components/ui/TextURL';
|
||||||
import AnimateFade from '@/components/wrap/AnimateFade';
|
import AnimateFade from '@/components/wrap/AnimateFade';
|
||||||
|
import { useAuth } from '@/context/AuthContext';
|
||||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||||
import { useLibrary } from '@/context/LibraryContext';
|
import { useLibrary } from '@/context/LibraryContext';
|
||||||
import { useBlockNavigation, useConceptNavigation } from '@/context/NavigationContext';
|
import { useBlockNavigation, useConceptNavigation } from '@/context/NavigationContext';
|
||||||
|
@ -34,6 +35,7 @@ function OssTabs() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const query = useQueryStrings();
|
const query = useQueryStrings();
|
||||||
const activeTab = query.get('tab') ? (Number(query.get('tab')) as OssTabID) : OssTabID.GRAPH;
|
const activeTab = query.get('tab') ? (Number(query.get('tab')) as OssTabID) : OssTabID.GRAPH;
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
const { calculateHeight, setNoFooter } = useConceptOptions();
|
const { calculateHeight, setNoFooter } = useConceptOptions();
|
||||||
const { schema, loading, loadingError: errorLoading } = useOSS();
|
const { schema, loading, loadingError: errorLoading } = useOSS();
|
||||||
|
@ -41,7 +43,12 @@ function OssTabs() {
|
||||||
|
|
||||||
const [isModified, setIsModified] = useState(false);
|
const [isModified, setIsModified] = useState(false);
|
||||||
const [selected, setSelected] = useState<OperationID[]>([]);
|
const [selected, setSelected] = useState<OperationID[]>([]);
|
||||||
useBlockNavigation(isModified);
|
useBlockNavigation(
|
||||||
|
isModified &&
|
||||||
|
schema !== undefined &&
|
||||||
|
user !== undefined &&
|
||||||
|
(user.is_staff || user.id == schema.owner || schema.editors.includes(user.id))
|
||||||
|
);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (schema) {
|
if (schema) {
|
||||||
|
|
|
@ -20,11 +20,6 @@ import { information, labelCstTypification } from '@/utils/labels';
|
||||||
import EditorRSExpression from '../EditorRSExpression';
|
import EditorRSExpression from '../EditorRSExpression';
|
||||||
import ControlsOverlay from './ControlsOverlay';
|
import ControlsOverlay from './ControlsOverlay';
|
||||||
|
|
||||||
/**
|
|
||||||
* Characters limit to start increasing number of rows.
|
|
||||||
*/
|
|
||||||
export const ROW_SIZE_IN_CHARACTERS = 70;
|
|
||||||
|
|
||||||
interface FormConstituentaProps {
|
interface FormConstituentaProps {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
|
||||||
|
@ -166,7 +161,7 @@ function FormConstituenta({
|
||||||
{state ? (
|
{state ? (
|
||||||
<TextArea
|
<TextArea
|
||||||
id='cst_typification'
|
id='cst_typification'
|
||||||
rows={typification.length > ROW_SIZE_IN_CHARACTERS ? 2 : 1}
|
className='cc-fit-content'
|
||||||
dense
|
dense
|
||||||
noResize
|
noResize
|
||||||
noBorder
|
noBorder
|
||||||
|
@ -220,12 +215,12 @@ function FormConstituenta({
|
||||||
<AnimateFade key='cst_convention_fade' hideContent={!showConvention || !state}>
|
<AnimateFade key='cst_convention_fade' hideContent={!showConvention || !state}>
|
||||||
<TextArea
|
<TextArea
|
||||||
id='cst_convention'
|
id='cst_convention'
|
||||||
|
className='cc-fit-content max-h-[8rem]'
|
||||||
spellCheck
|
spellCheck
|
||||||
label={isBasic ? 'Конвенция' : 'Комментарий'}
|
label={isBasic ? 'Конвенция' : 'Комментарий'}
|
||||||
placeholder={isBasic ? 'Договоренность об интерпретации' : 'Пояснение разработчика'}
|
placeholder={isBasic ? 'Договоренность об интерпретации' : 'Пояснение разработчика'}
|
||||||
value={convention}
|
value={convention}
|
||||||
disabled={disabled || (isBasic && state?.is_inherited)}
|
disabled={disabled || (isBasic && state?.is_inherited)}
|
||||||
rows={convention.length > 2 * ROW_SIZE_IN_CHARACTERS || convention.includes('\n') ? 4 : 2}
|
|
||||||
onChange={event => setConvention(event.target.value)}
|
onChange={event => setConvention(event.target.value)}
|
||||||
/>
|
/>
|
||||||
</AnimateFade>
|
</AnimateFade>
|
||||||
|
|
|
@ -49,7 +49,10 @@ function ToolbarConstituenta({
|
||||||
const controller = useRSEdit();
|
const controller = useRSEdit();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Overlay position='top-1 right-4' className='cc-icons sm:right-1/2 sm:translate-x-1/2'>
|
<Overlay
|
||||||
|
position='top-1 right-1/2 translate-x-1/2 sm:right-4 sm:translate-x-0 md:right-1/2 md:translate-x-1/2'
|
||||||
|
className='cc-icons outline-none transition-all duration-500'
|
||||||
|
>
|
||||||
{controller.schema && controller.schema?.oss.length > 0 ? (
|
{controller.schema && controller.schema?.oss.length > 0 ? (
|
||||||
<MiniSelectorOSS
|
<MiniSelectorOSS
|
||||||
items={controller.schema.oss}
|
items={controller.schema.oss}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { IconNewItem, IconUpload, IconVersions } from '@/components/Icons';
|
import { IconNewVersion, IconUpload, IconVersions } from '@/components/Icons';
|
||||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
import Overlay from '@/components/ui/Overlay';
|
import Overlay from '@/components/ui/Overlay';
|
||||||
|
@ -33,7 +33,7 @@ function ToolbarVersioning({ blockReload }: ToolbarVersioningProps) {
|
||||||
titleHtml={controller.isContentEditable ? 'Создать версию' : 'Переключитесь <br/>на актуальную версию'}
|
titleHtml={controller.isContentEditable ? 'Создать версию' : 'Переключитесь <br/>на актуальную версию'}
|
||||||
disabled={!controller.isContentEditable}
|
disabled={!controller.isContentEditable}
|
||||||
onClick={controller.createVersion}
|
onClick={controller.createVersion}
|
||||||
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
icon={<IconNewVersion size='1.25rem' className='icon-green' />}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title={controller.schema?.versions.length === 0 ? 'Список версий пуст' : 'Редактировать версии'}
|
title={controller.schema?.versions.length === 0 ? 'Список версий пуст' : 'Редактировать версии'}
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
|
||||||
import fileDownload from 'js-file-download';
|
import fileDownload from 'js-file-download';
|
||||||
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import { IconCSV } from '@/components/Icons';
|
import { IconCSV } from '@/components/Icons';
|
||||||
import SelectedCounter from '@/components/info/SelectedCounter';
|
|
||||||
import { type RowSelectionState } from '@/components/ui/DataTable';
|
import { type RowSelectionState } from '@/components/ui/DataTable';
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
import Overlay from '@/components/ui/Overlay';
|
import Overlay from '@/components/ui/Overlay';
|
||||||
|
import SearchBar from '@/components/ui/SearchBar';
|
||||||
import AnimateFade from '@/components/wrap/AnimateFade';
|
import AnimateFade from '@/components/wrap/AnimateFade';
|
||||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||||
import { ConstituentaID, CstType } from '@/models/rsform';
|
import { CstMatchMode } from '@/models/miscellaneous';
|
||||||
|
import { ConstituentaID, CstType, IConstituenta } from '@/models/rsform';
|
||||||
|
import { matchConstituenta } from '@/models/rsformAPI';
|
||||||
import { information } from '@/utils/labels';
|
import { information } from '@/utils/labels';
|
||||||
import { convertToCSV } from '@/utils/utils';
|
import { convertToCSV } from '@/utils/utils';
|
||||||
|
|
||||||
|
@ -29,30 +30,43 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||||
const controller = useRSEdit();
|
const controller = useRSEdit();
|
||||||
|
|
||||||
|
const [filtered, setFiltered] = useState<IConstituenta[]>(controller.schema?.items ?? []);
|
||||||
|
const [filterText, setFilterText] = useState('');
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!controller.schema || controller.selected.length === 0) {
|
if (filtered.length === 0) {
|
||||||
setRowSelection({});
|
setRowSelection({});
|
||||||
} else {
|
return;
|
||||||
const newRowSelection: RowSelectionState = {};
|
|
||||||
controller.schema.items.forEach((cst, index) => {
|
|
||||||
newRowSelection[String(index)] = controller.selected.includes(cst.id);
|
|
||||||
});
|
|
||||||
setRowSelection(newRowSelection);
|
|
||||||
}
|
}
|
||||||
}, [controller.selected, controller.schema]);
|
const newRowSelection: RowSelectionState = {};
|
||||||
|
filtered.forEach((cst, index) => {
|
||||||
|
newRowSelection[String(index)] = controller.selected.includes(cst.id);
|
||||||
|
});
|
||||||
|
setRowSelection(newRowSelection);
|
||||||
|
}, [filtered, setRowSelection, controller.selected]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!controller.schema || controller.schema.items.length === 0) {
|
||||||
|
setFiltered([]);
|
||||||
|
} else if (filterText) {
|
||||||
|
setFiltered(controller.schema.items.filter(cst => matchConstituenta(cst, filterText, CstMatchMode.ALL)));
|
||||||
|
} else {
|
||||||
|
setFiltered(controller.schema.items);
|
||||||
|
}
|
||||||
|
}, [filterText, controller.schema?.items, controller.schema]);
|
||||||
|
|
||||||
const handleDownloadCSV = useCallback(() => {
|
const handleDownloadCSV = useCallback(() => {
|
||||||
if (!controller.schema || controller.schema.items.length === 0) {
|
if (!controller.schema || filtered.length === 0) {
|
||||||
toast.error(information.noDataToExport);
|
toast.error(information.noDataToExport);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const blob = convertToCSV(controller.schema.items);
|
const blob = convertToCSV(filtered);
|
||||||
try {
|
try {
|
||||||
fileDownload(blob, `${controller.schema.alias}.csv`, 'text/csv;charset=utf-8;');
|
fileDownload(blob, `${controller.schema.alias}.csv`, 'text/csv;charset=utf-8;');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}, [controller]);
|
}, [filtered, controller]);
|
||||||
|
|
||||||
function handleRowSelection(updater: React.SetStateAction<RowSelectionState>) {
|
function handleRowSelection(updater: React.SetStateAction<RowSelectionState>) {
|
||||||
if (!controller.schema) {
|
if (!controller.schema) {
|
||||||
|
@ -60,12 +74,15 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
|
||||||
} else {
|
} else {
|
||||||
const newRowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
|
const newRowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
|
||||||
const newSelection: ConstituentaID[] = [];
|
const newSelection: ConstituentaID[] = [];
|
||||||
controller.schema.items.forEach((cst, index) => {
|
filtered.forEach((cst, index) => {
|
||||||
if (newRowSelection[String(index)] === true) {
|
if (newRowSelection[String(index)] === true) {
|
||||||
newSelection.push(cst.id);
|
newSelection.push(cst.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
controller.setSelected(newSelection);
|
controller.setSelected(prev => [
|
||||||
|
...prev.filter(cst_id => !filtered.find(cst => cst.id === cst_id)),
|
||||||
|
...newSelection
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,21 +144,21 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
|
||||||
{controller.isContentEditable ? <ToolbarRSList /> : null}
|
{controller.isContentEditable ? <ToolbarRSList /> : null}
|
||||||
<AnimateFade tabIndex={-1} onKeyDown={handleKeyDown}>
|
<AnimateFade tabIndex={-1} onKeyDown={handleKeyDown}>
|
||||||
{controller.isContentEditable ? (
|
{controller.isContentEditable ? (
|
||||||
<SelectedCounter
|
<div className='flex items-center border-b'>
|
||||||
totalCount={controller.schema?.stats?.count_all ?? 0}
|
<div className='px-2'>
|
||||||
selectedCount={controller.selected.length}
|
Выбор {controller.selected.length} из {controller.schema?.stats?.count_all ?? 0}
|
||||||
position='top-[0.3rem] left-2'
|
</div>
|
||||||
/>
|
<SearchBar
|
||||||
|
id='constituents_search'
|
||||||
|
noBorder
|
||||||
|
className='w-[8rem]'
|
||||||
|
value={filterText}
|
||||||
|
onChange={setFilterText}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div
|
<Overlay position='top-[0.25rem] right-[1rem]' layer='z-navigation'>
|
||||||
className={clsx('border-b', {
|
|
||||||
'pt-[2.3rem]': controller.isContentEditable,
|
|
||||||
'relative top-[-1px]': !controller.isContentEditable
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Overlay position='top-[0.25rem] right-[1rem]' layer='z-tooltip'>
|
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Выгрузить в формате CSV'
|
title='Выгрузить в формате CSV'
|
||||||
icon={<IconCSV size='1.25rem' className='icon-green' />}
|
icon={<IconCSV size='1.25rem' className='icon-green' />}
|
||||||
|
@ -150,7 +167,7 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
|
||||||
</Overlay>
|
</Overlay>
|
||||||
|
|
||||||
<TableRSList
|
<TableRSList
|
||||||
items={controller.schema?.items}
|
items={filtered}
|
||||||
maxHeight={tableHeight}
|
maxHeight={tableHeight}
|
||||||
enableSelection={controller.isContentEditable}
|
enableSelection={controller.isContentEditable}
|
||||||
selected={rowSelection}
|
selected={rowSelection}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { CstTypeIcon } from '@/components/DomainIcons';
|
||||||
import {
|
import {
|
||||||
IconClone,
|
IconClone,
|
||||||
IconDestroy,
|
IconDestroy,
|
||||||
|
@ -16,7 +17,6 @@ import Overlay from '@/components/ui/Overlay';
|
||||||
import useDropdown from '@/hooks/useDropdown';
|
import useDropdown from '@/hooks/useDropdown';
|
||||||
import { HelpTopic } from '@/models/miscellaneous';
|
import { HelpTopic } from '@/models/miscellaneous';
|
||||||
import { CstType } from '@/models/rsform';
|
import { CstType } from '@/models/rsform';
|
||||||
import { getCstTypePrefix } from '@/models/rsformAPI';
|
|
||||||
import { prefixes } from '@/utils/constants';
|
import { prefixes } from '@/utils/constants';
|
||||||
import { getCstTypeShortcut, labelCstType, prepareTooltip } from '@/utils/labels';
|
import { getCstTypeShortcut, labelCstType, prepareTooltip } from '@/utils/labels';
|
||||||
|
|
||||||
|
@ -27,7 +27,10 @@ function ToolbarRSList() {
|
||||||
const insertMenu = useDropdown();
|
const insertMenu = useDropdown();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Overlay position='top-1 right-1/2 translate-x-1/2' className='items-start cc-icons'>
|
<Overlay
|
||||||
|
position='top-1 right-4 translate-x-0 md:right-1/2 md:translate-x-1/2'
|
||||||
|
className='cc-icons items-start outline-none transition-all duration-500'
|
||||||
|
>
|
||||||
{controller.schema && controller.schema?.oss.length > 0 ? (
|
{controller.schema && controller.schema?.oss.length > 0 ? (
|
||||||
<MiniSelectorOSS
|
<MiniSelectorOSS
|
||||||
items={controller.schema.oss}
|
items={controller.schema.oss}
|
||||||
|
@ -52,18 +55,6 @@ function ToolbarRSList() {
|
||||||
disabled={controller.isProcessing || controller.nothingSelected}
|
disabled={controller.isProcessing || controller.nothingSelected}
|
||||||
onClick={controller.moveDown}
|
onClick={controller.moveDown}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
|
||||||
titleHtml={prepareTooltip('Клонировать конституенту', 'Alt + V')}
|
|
||||||
icon={<IconClone size='1.25rem' className='icon-green' />}
|
|
||||||
disabled={controller.isProcessing || controller.selected.length !== 1}
|
|
||||||
onClick={controller.cloneCst}
|
|
||||||
/>
|
|
||||||
<MiniButton
|
|
||||||
titleHtml={prepareTooltip('Добавить новую конституенту...', 'Alt + `')}
|
|
||||||
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
|
||||||
disabled={controller.isProcessing}
|
|
||||||
onClick={() => controller.createCst(undefined, false)}
|
|
||||||
/>
|
|
||||||
<div ref={insertMenu.ref}>
|
<div ref={insertMenu.ref}>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Добавить пустую конституенту'
|
title='Добавить пустую конституенту'
|
||||||
|
@ -72,17 +63,30 @@ function ToolbarRSList() {
|
||||||
disabled={controller.isProcessing}
|
disabled={controller.isProcessing}
|
||||||
onClick={insertMenu.toggle}
|
onClick={insertMenu.toggle}
|
||||||
/>
|
/>
|
||||||
<Dropdown isOpen={insertMenu.isOpen}>
|
<Dropdown isOpen={insertMenu.isOpen} className='-translate-x-1/2 md:translate-x-0'>
|
||||||
{Object.values(CstType).map(typeStr => (
|
{Object.values(CstType).map(typeStr => (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
key={`${prefixes.csttype_list}${typeStr}`}
|
key={`${prefixes.csttype_list}${typeStr}`}
|
||||||
text={`${getCstTypePrefix(typeStr as CstType)}1 — ${labelCstType(typeStr as CstType)}`}
|
text={labelCstType(typeStr as CstType)}
|
||||||
|
icon={<CstTypeIcon value={typeStr as CstType} size='1.25rem' />}
|
||||||
onClick={() => controller.createCst(typeStr as CstType, true)}
|
onClick={() => controller.createCst(typeStr as CstType, true)}
|
||||||
titleHtml={getCstTypeShortcut(typeStr as CstType)}
|
titleHtml={getCstTypeShortcut(typeStr as CstType)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
<MiniButton
|
||||||
|
titleHtml={prepareTooltip('Добавить новую конституенту...', 'Alt + `')}
|
||||||
|
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
||||||
|
disabled={controller.isProcessing}
|
||||||
|
onClick={() => controller.createCst(undefined, false)}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
titleHtml={prepareTooltip('Клонировать конституенту', 'Alt + V')}
|
||||||
|
icon={<IconClone size='1.25rem' className='icon-green' />}
|
||||||
|
disabled={controller.isProcessing || controller.selected.length !== 1}
|
||||||
|
onClick={controller.cloneCst}
|
||||||
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Удалить выбранные', 'Delete')}
|
titleHtml={prepareTooltip('Удалить выбранные', 'Delete')}
|
||||||
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
IconLibrary,
|
IconLibrary,
|
||||||
IconMenu,
|
IconMenu,
|
||||||
IconNewItem,
|
IconNewItem,
|
||||||
|
IconNewVersion,
|
||||||
IconOSS,
|
IconOSS,
|
||||||
IconOwner,
|
IconOwner,
|
||||||
IconReader,
|
IconReader,
|
||||||
|
@ -156,6 +157,12 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
||||||
onClick={handleClone}
|
onClick={handleClone}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
<DropdownButton
|
||||||
|
text='Сохранить версию'
|
||||||
|
disabled={!controller.isContentEditable}
|
||||||
|
onClick={controller.createVersion}
|
||||||
|
icon={<IconNewVersion size='1rem' className='icon-green' />}
|
||||||
|
/>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='Выгрузить в Экстеор'
|
text='Выгрузить в Экстеор'
|
||||||
icon={<IconDownload size='1rem' className='icon-primary' />}
|
icon={<IconDownload size='1rem' className='icon-primary' />}
|
||||||
|
|
|
@ -335,9 +335,8 @@ export const RSEditState = ({
|
||||||
if (!model.schema) {
|
if (!model.schema) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
model.versionCreate(data, newVersion => {
|
model.versionCreate(data, () => {
|
||||||
toast.success(information.newVersion(data.version));
|
toast.success(information.newVersion(data.version));
|
||||||
viewVersion(newVersion);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[model, viewVersion]
|
[model, viewVersion]
|
||||||
|
@ -725,6 +724,8 @@ export const RSEditState = ({
|
||||||
versions={model.schema.versions}
|
versions={model.schema.versions}
|
||||||
hideWindow={() => setShowCreateVersion(false)}
|
hideWindow={() => setShowCreateVersion(false)}
|
||||||
onCreate={handleCreateVersion}
|
onCreate={handleCreateVersion}
|
||||||
|
selected={selected}
|
||||||
|
totalCount={model.schema.items.length}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{showEditVersions ? (
|
{showEditVersions ? (
|
||||||
|
|
|
@ -267,7 +267,7 @@ function RSTabs() {
|
||||||
<TabLabel label='Граф термов' />
|
<TabLabel label='Граф термов' />
|
||||||
</TabList>
|
</TabList>
|
||||||
|
|
||||||
<AnimateFade className='overflow-y-auto' style={{ maxHeight: panelHeight }}>
|
<AnimateFade className='overflow-y-auto overflow-x-hidden' style={{ maxHeight: panelHeight }}>
|
||||||
{cardPanel}
|
{cardPanel}
|
||||||
{listPanel}
|
{listPanel}
|
||||||
{editorPanel}
|
{editorPanel}
|
||||||
|
|
|
@ -17,6 +17,7 @@ interface TableSideConstituentsProps {
|
||||||
activeCst?: IConstituenta;
|
activeCst?: IConstituenta;
|
||||||
onOpenEdit: (cstID: ConstituentaID) => void;
|
onOpenEdit: (cstID: ConstituentaID) => void;
|
||||||
denseThreshold?: number;
|
denseThreshold?: number;
|
||||||
|
autoScroll?: boolean;
|
||||||
maxHeight: string;
|
maxHeight: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +26,7 @@ const columnHelper = createColumnHelper<IConstituenta>();
|
||||||
function TableSideConstituents({
|
function TableSideConstituents({
|
||||||
items,
|
items,
|
||||||
activeCst,
|
activeCst,
|
||||||
|
autoScroll = true,
|
||||||
onOpenEdit,
|
onOpenEdit,
|
||||||
maxHeight,
|
maxHeight,
|
||||||
denseThreshold = 9999
|
denseThreshold = 9999
|
||||||
|
@ -38,17 +40,19 @@ function TableSideConstituents({
|
||||||
if (!activeCst) {
|
if (!activeCst) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
if (autoScroll) {
|
||||||
const element = document.getElementById(`${prefixes.cst_side_table}${activeCst.alias}`);
|
setTimeout(() => {
|
||||||
if (element) {
|
const element = document.getElementById(`${prefixes.cst_side_table}${activeCst.alias}`);
|
||||||
element.scrollIntoView({
|
if (element) {
|
||||||
behavior: 'smooth',
|
element.scrollIntoView({
|
||||||
block: 'center',
|
behavior: 'smooth',
|
||||||
inline: 'end'
|
block: 'center',
|
||||||
});
|
inline: 'end'
|
||||||
}
|
});
|
||||||
}, PARAMETER.refreshTimeout);
|
}
|
||||||
}, [activeCst]);
|
}, PARAMETER.refreshTimeout);
|
||||||
|
}
|
||||||
|
}, [activeCst, autoScroll]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
setColumnVisibility(prev => {
|
setColumnVisibility(prev => {
|
||||||
|
|
|
@ -46,6 +46,7 @@ function ViewConstituents({ expression, schema, activeCst, isBottom, onOpenEdit
|
||||||
items={filteredData}
|
items={filteredData}
|
||||||
activeCst={activeCst}
|
activeCst={activeCst}
|
||||||
onOpenEdit={onOpenEdit}
|
onOpenEdit={onOpenEdit}
|
||||||
|
autoScroll={!isBottom}
|
||||||
denseThreshold={COLUMN_EXPRESSION_HIDE_THRESHOLD}
|
denseThreshold={COLUMN_EXPRESSION_HIDE_THRESHOLD}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
@ -55,10 +56,10 @@ function ViewConstituents({ expression, schema, activeCst, isBottom, onOpenEdit
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'border overflow-visible', // prettier: split-lines
|
'border', // prettier: split-lines
|
||||||
{
|
{
|
||||||
'mt-[2.2rem] rounded-l-md rounded-r-none h-fit': !isBottom,
|
'mt-[2.2rem] rounded-l-md rounded-r-none h-fit overflow-visible': !isBottom,
|
||||||
'mt-3 mx-6 rounded-md md:w-[45.8rem]': isBottom
|
'mt-3 mx-6 rounded-md md:max-w-[45.8rem] overflow-hidden': isBottom
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
initial={{ ...animateSideView.initial }}
|
initial={{ ...animateSideView.initial }}
|
||||||
|
|
|
@ -226,6 +226,10 @@
|
||||||
@apply flex gap-1;
|
@apply flex gap-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cc-fit-content {
|
||||||
|
field-sizing: content;
|
||||||
|
}
|
||||||
|
|
||||||
.cc-scroll-row {
|
.cc-scroll-row {
|
||||||
scroll-snap-align: start;
|
scroll-snap-align: start;
|
||||||
scroll-snap-stop: always;
|
scroll-snap-stop: always;
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { GramData, Grammeme, ReferenceType } from '@/models/language';
|
||||||
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
|
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
|
||||||
import { validateLocation } from '@/models/libraryAPI';
|
import { validateLocation } from '@/models/libraryAPI';
|
||||||
import { CstMatchMode, DependencyMode, GraphColoring, GraphSizing, HelpTopic } from '@/models/miscellaneous';
|
import { CstMatchMode, DependencyMode, GraphColoring, GraphSizing, HelpTopic } from '@/models/miscellaneous';
|
||||||
import { OperationType } from '@/models/oss';
|
import { ISubstitutionErrorDescription, OperationType, SubstitutionErrorType } from '@/models/oss';
|
||||||
import { CstClass, CstType, ExpressionStatus, IConstituenta, IRSForm } from '@/models/rsform';
|
import { CstClass, CstType, ExpressionStatus, IConstituenta, IRSForm } from '@/models/rsform';
|
||||||
import {
|
import {
|
||||||
IArgumentInfo,
|
IArgumentInfo,
|
||||||
|
@ -799,6 +799,35 @@ export function describeRSError(error: IRSErrorDescription): string {
|
||||||
return 'UNKNOWN ERROR';
|
return 'UNKNOWN ERROR';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates error description for {@link ISubstitutionErrorDescription}.
|
||||||
|
*/
|
||||||
|
export function describeSubstitutionError(error: ISubstitutionErrorDescription): string {
|
||||||
|
switch (error.errorType) {
|
||||||
|
case SubstitutionErrorType.invalidIDs:
|
||||||
|
return 'Ошибка в идентификаторах схем';
|
||||||
|
case SubstitutionErrorType.incorrectCst:
|
||||||
|
return `Ошибка ${error.params[0]} -> ${error.params[1]}: некорректное выражение конституенты`;
|
||||||
|
case SubstitutionErrorType.invalidBasic:
|
||||||
|
return `Ошибка ${error.params[0]} -> ${error.params[1]}: замена структурного понятия базисным множеством`;
|
||||||
|
case SubstitutionErrorType.invalidConstant:
|
||||||
|
return `Ошибка ${error.params[0]} -> ${error.params[1]}: подстановка константного множества возможна только вместо другого константного`;
|
||||||
|
case SubstitutionErrorType.invalidClasses:
|
||||||
|
return `Ошибка ${error.params[0]} -> ${error.params[1]}: классы конституент не совпадают`;
|
||||||
|
case SubstitutionErrorType.typificationCycle:
|
||||||
|
return `Ошибка: цикл подстановок в типизациях ${error.params[0]}`;
|
||||||
|
case SubstitutionErrorType.baseSubstitutionNotSet:
|
||||||
|
return `Ошибка: типизация не задает множество ${error.params[0]} ∈ ${error.params[1]}`;
|
||||||
|
case SubstitutionErrorType.unequalTypification:
|
||||||
|
return `Ошибка ${error.params[0]} -> ${error.params[1]}: типизация структурных операндов не совпадает`;
|
||||||
|
case SubstitutionErrorType.unequalArgsCount:
|
||||||
|
return `Ошибка ${error.params[0]} -> ${error.params[1]}: количество аргументов не совпадает`;
|
||||||
|
case SubstitutionErrorType.unequalArgs:
|
||||||
|
return `Ошибка ${error.params[0]} -> ${error.params[1]}: типизация аргументов не совпадает`;
|
||||||
|
}
|
||||||
|
return 'UNKNOWN ERROR';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves label for {@link UserLevel}.
|
* Retrieves label for {@link UserLevel}.
|
||||||
*/
|
*/
|
||||||
|
@ -934,6 +963,7 @@ export const information = {
|
||||||
locationRenamed: 'Ваши схемы перемещены',
|
locationRenamed: 'Ваши схемы перемещены',
|
||||||
cloneComplete: (alias: string) => `Копия создана: ${alias}`,
|
cloneComplete: (alias: string) => `Копия создана: ${alias}`,
|
||||||
noDataToExport: 'Нет данных для экспорта',
|
noDataToExport: 'Нет данных для экспорта',
|
||||||
|
substitutionsCorrect: 'Таблица отождествлений прошла проверку',
|
||||||
|
|
||||||
addedConstituents: (count: number) => `Добавлены конституенты: ${count}`,
|
addedConstituents: (count: number) => `Добавлены конституенты: ${count}`,
|
||||||
newLibraryItem: 'Схема успешно создана',
|
newLibraryItem: 'Схема успешно создана',
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
|
|
||||||
import axios, { AxiosError, AxiosHeaderValue, AxiosResponse } from 'axios';
|
import axios, { AxiosError, AxiosHeaderValue, AxiosResponse } from 'axios';
|
||||||
|
|
||||||
|
import { AliasMapping } from '@/models/rslang';
|
||||||
|
|
||||||
import { prompts } from './labels';
|
import { prompts } from './labels';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -46,7 +48,7 @@ export class TextMatcher {
|
||||||
/**
|
/**
|
||||||
* Text substitution guided by mapping and regular expression.
|
* Text substitution guided by mapping and regular expression.
|
||||||
*/
|
*/
|
||||||
export function applyPattern(text: string, mapping: Record<string, string>, pattern: RegExp): string {
|
export function applyPattern(text: string, mapping: AliasMapping, pattern: RegExp): string {
|
||||||
if (text === '' || pattern === null) {
|
if (text === '' || pattern === null) {
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user