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",
|
||||
"tooltipic",
|
||||
"tsdoc",
|
||||
"Typifications",
|
||||
"unknwn",
|
||||
"Upvote",
|
||||
"Viewset",
|
||||
|
|
|
@ -64,10 +64,12 @@ class VersionInnerSerializer(serializers.ModelSerializer):
|
|||
|
||||
class VersionCreateSerializer(serializers.ModelSerializer):
|
||||
''' Serializer: Version create data. '''
|
||||
items = PKField(many=True, required=False, default=None, queryset=Constituenta.objects.all().only('pk'))
|
||||
|
||||
class Meta:
|
||||
''' serializer metadata. '''
|
||||
model = Version
|
||||
fields = 'version', 'description'
|
||||
fields = 'version', 'description', 'items'
|
||||
|
||||
|
||||
class LibraryItemDetailsSerializer(serializers.ModelSerializer):
|
||||
|
|
|
@ -6,6 +6,7 @@ from zipfile import ZipFile
|
|||
|
||||
from rest_framework import status
|
||||
|
||||
from apps.library.models import Version
|
||||
from apps.rsform.models import Constituenta, RSForm
|
||||
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']])
|
||||
|
||||
|
||||
@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')
|
||||
def test_retrieve_version(self):
|
||||
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.is_valid(raise_exception=True)
|
||||
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(
|
||||
version=version_input.validated_data['version'],
|
||||
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",
|
||||
"@uiw/codemirror-themes": "^4.23.0",
|
||||
"@uiw/react-codemirror": "^4.23.0",
|
||||
"axios": "^1.7.4",
|
||||
"axios": "^1.7.5",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^11.3.29",
|
||||
"framer-motion": "^11.3.30",
|
||||
"html-to-image": "^1.11.11",
|
||||
"js-file-download": "^0.4.12",
|
||||
"react": "^18.3.1",
|
||||
|
@ -43,14 +43,14 @@
|
|||
"@typescript-eslint/parser": "^8.0.1",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.9.0",
|
||||
"eslint": "^9.9.1",
|
||||
"eslint-plugin-react": "^7.35.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"globals": "^15.9.0",
|
||||
"jest": "^29.7.0",
|
||||
"postcss": "^8.4.41",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"ts-jest": "^29.2.4",
|
||||
"ts-jest": "^29.2.5",
|
||||
"typescript": "^5.5.4",
|
||||
"typescript-eslint": "^8.2.0",
|
||||
"vite": "^5.4.2"
|
||||
|
@ -97,9 +97,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/compat-data": {
|
||||
"version": "7.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz",
|
||||
"integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==",
|
||||
"version": "7.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz",
|
||||
"integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
@ -148,12 +148,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz",
|
||||
"integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==",
|
||||
"version": "7.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.5.tgz",
|
||||
"integrity": "sha512-abd43wyLfbWoxC6ahM8xTkqLpGB2iWBVyuKC9/srhFunCd1SDNrV1s72bBpK4hLj8KLzHBBcOblvLQZBNw9r3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.25.0",
|
||||
"@babel/types": "^7.25.4",
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"jsesc": "^2.5.1"
|
||||
|
@ -303,12 +303,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz",
|
||||
"integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==",
|
||||
"version": "7.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.4.tgz",
|
||||
"integrity": "sha512-nq+eWrOgdtu3jG5Os4TQP3x3cLA8hR8TvJNjD8vnPa20WGycimcparWnLK4jJhElTK6SDyuJo1weMKO/5LpmLA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.25.2"
|
||||
"@babel/types": "^7.25.4"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
|
@ -541,13 +541,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-typescript": {
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz",
|
||||
"integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==",
|
||||
"version": "7.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.4.tgz",
|
||||
"integrity": "sha512-uMOCoHVU52BsSWxPOMVv5qKRdeSlPuImUCB2dlPuBSU+W2/ROE7/Zg8F2Kepbk+8yBa68LlRKxO+xgEVWorsDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.24.7"
|
||||
"@babel/helper-plugin-utils": "^7.24.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
|
@ -589,9 +589,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz",
|
||||
"integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==",
|
||||
"version": "7.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.4.tgz",
|
||||
"integrity": "sha512-DSgLeL/FNcpXuzav5wfYvHCGvynXkJbn3Zvc3823AEe9nPwW9IK4UoCSS5yGymmQzN0pCPvivtgS6/8U2kkm1w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
|
@ -615,16 +615,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz",
|
||||
"integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==",
|
||||
"version": "7.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.4.tgz",
|
||||
"integrity": "sha512-VJ4XsrD+nOvlXyLzmLzUs/0qjFS4sK30te5yEFlvbbUNEgKaVb2BHZUpAL+ttLPQAHNrsI3zZisbfha5Cvr8vg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.24.7",
|
||||
"@babel/generator": "^7.25.0",
|
||||
"@babel/parser": "^7.25.3",
|
||||
"@babel/generator": "^7.25.4",
|
||||
"@babel/parser": "^7.25.4",
|
||||
"@babel/template": "^7.25.0",
|
||||
"@babel/types": "^7.25.2",
|
||||
"@babel/types": "^7.25.4",
|
||||
"debug": "^4.3.1",
|
||||
"globals": "^11.1.0"
|
||||
},
|
||||
|
@ -642,9 +642,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz",
|
||||
"integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==",
|
||||
"version": "7.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.4.tgz",
|
||||
"integrity": "sha512-zQ1ijeeCXVEh+aNL0RlmkPkG8HUiDcU2pzQQFjtbntgAczRASFzj4H+6+bV+dy1ntKR14I/DypeuRG1uma98iQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.24.8",
|
||||
|
@ -1331,9 +1331,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array": {
|
||||
"version": "0.17.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.1.tgz",
|
||||
"integrity": "sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==",
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz",
|
||||
"integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
|
@ -1431,9 +1431,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.0.tgz",
|
||||
"integrity": "sha512-hhetes6ZHP3BlXLxmd8K2SNgkhNSi+UcecbnwWKwpP7kyi/uC75DJ1lOOBO3xrC4jyojtGE3YxKZPHfk4yrgug==",
|
||||
"version": "9.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz",
|
||||
"integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
@ -4304,9 +4304,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
|
||||
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
|
||||
"version": "1.7.5",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz",
|
||||
"integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
|
@ -5897,17 +5897,17 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.9.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.0.tgz",
|
||||
"integrity": "sha512-JfiKJrbx0506OEerjK2Y1QlldtBxkAlLxT5OEcRF8uaQ86noDe2k31Vw9rnSWv+MXZHj7OOUV/dA0AhdLFcyvA==",
|
||||
"version": "9.9.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz",
|
||||
"integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.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/js": "9.9.0",
|
||||
"@eslint/js": "9.9.1",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@humanwhocodes/retry": "^0.3.0",
|
||||
"@nodelib/fs.walk": "^1.2.8",
|
||||
|
@ -6598,9 +6598,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "11.3.29",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.3.29.tgz",
|
||||
"integrity": "sha512-uyDuUOeOElJEA3kbkbyoTNEf75Jih1EUg0ouLKYMlGDdt/LaJPmO+FyOGAGxM2HwKhHcAoKFNveR5A8peb7yhw==",
|
||||
"version": "11.3.30",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.3.30.tgz",
|
||||
"integrity": "sha512-9VmqGe9OIjfMoCcs+ZsKXlv6JaG5QagKX2F1uSbkG3Z33wgjnz60Kw+CngC1M49rDYau+Y9aL+8jGagAwrbVyw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
|
@ -7410,9 +7410,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/is-core-module": {
|
||||
"version": "2.15.0",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz",
|
||||
"integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==",
|
||||
"version": "2.15.1",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
|
||||
"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hasown": "^2.0.2"
|
||||
|
@ -9989,9 +9989,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
|
||||
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
@ -12376,21 +12376,21 @@
|
|||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/ts-jest": {
|
||||
"version": "29.2.4",
|
||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.4.tgz",
|
||||
"integrity": "sha512-3d6tgDyhCI29HlpwIq87sNuI+3Q6GLTTCeYRHCs7vDz+/3GCMwEtV9jezLyl4ZtnBgx00I7hm8PCP8cTksMGrw==",
|
||||
"version": "29.2.5",
|
||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz",
|
||||
"integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bs-logger": "0.x",
|
||||
"bs-logger": "^0.2.6",
|
||||
"ejs": "^3.1.10",
|
||||
"fast-json-stable-stringify": "2.x",
|
||||
"fast-json-stable-stringify": "^2.1.0",
|
||||
"jest-util": "^29.0.0",
|
||||
"json5": "^2.2.3",
|
||||
"lodash.memoize": "4.x",
|
||||
"make-error": "1.x",
|
||||
"semver": "^7.5.3",
|
||||
"yargs-parser": "^21.0.1"
|
||||
"lodash.memoize": "^4.1.2",
|
||||
"make-error": "^1.3.6",
|
||||
"semver": "^7.6.3",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"ts-jest": "cli.js"
|
||||
|
@ -12425,9 +12425,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
|
||||
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==",
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
|
||||
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tunnel-rat": {
|
||||
|
|
|
@ -16,9 +16,9 @@
|
|||
"@tanstack/react-table": "^8.20.1",
|
||||
"@uiw/codemirror-themes": "^4.23.0",
|
||||
"@uiw/react-codemirror": "^4.23.0",
|
||||
"axios": "^1.7.4",
|
||||
"axios": "^1.7.5",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^11.3.29",
|
||||
"framer-motion": "^11.3.30",
|
||||
"html-to-image": "^1.11.11",
|
||||
"js-file-download": "^0.4.12",
|
||||
"react": "^18.3.1",
|
||||
|
@ -47,14 +47,14 @@
|
|||
"@typescript-eslint/parser": "^8.0.1",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.9.0",
|
||||
"eslint": "^9.9.1",
|
||||
"eslint-plugin-react": "^7.35.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"globals": "^15.9.0",
|
||||
"jest": "^29.7.0",
|
||||
"postcss": "^8.4.41",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"ts-jest": "^29.2.4",
|
||||
"ts-jest": "^29.2.5",
|
||||
"typescript": "^5.5.4",
|
||||
"typescript-eslint": "^8.2.0",
|
||||
"vite": "^5.4.2"
|
||||
|
|
|
@ -13,7 +13,7 @@ function ApplicationLayout() {
|
|||
<NavigationState>
|
||||
<div className='min-w-[20rem] clr-app antialiased h-full'>
|
||||
<ConceptToaster
|
||||
className='mt-[4rem] text-sm' // prettier: split lines
|
||||
className='mt-[4rem] text-[14px]' // prettier: split lines
|
||||
autoClose={3000}
|
||||
draggable={false}
|
||||
pauseOnFocusLoss={false}
|
||||
|
@ -23,7 +23,7 @@ function ApplicationLayout() {
|
|||
|
||||
<div
|
||||
id={globals.main_scroll}
|
||||
className='flex flex-col items-start overflow-x-auto max-w-[100vw]'
|
||||
className='overflow-x-auto max-w-[100vw]'
|
||||
style={{
|
||||
maxHeight: viewportHeight
|
||||
}}
|
||||
|
|
|
@ -36,7 +36,7 @@ function Navigation() {
|
|||
<ToggleNavigation />
|
||||
<motion.div
|
||||
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',
|
||||
'cc-shadow-border'
|
||||
)}
|
||||
|
|
|
@ -13,10 +13,11 @@ function ToggleNavigation() {
|
|||
type='button'
|
||||
tabIndex={-1}
|
||||
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',
|
||||
'select-none',
|
||||
'min-h-[2rem]'
|
||||
'select-none'
|
||||
)}
|
||||
onClick={toggleNoNavigation}
|
||||
initial={false}
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
IRenameLocationData,
|
||||
ITargetAccessPolicy,
|
||||
ITargetLocation,
|
||||
IVersionData
|
||||
IVersionCreateData
|
||||
} from '@/models/library';
|
||||
import { IRSFormCloneData, IRSFormData, IVersionCreatedResponse } from '@/models/rsform';
|
||||
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({
|
||||
endpoint: `/api/library/${target}/create-version`,
|
||||
request: request
|
||||
|
|
|
@ -1,10 +1,18 @@
|
|||
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
|
||||
import { CstMatchMode, DependencyMode } from '@/models/miscellaneous';
|
||||
import { ExpressionStatus } from '@/models/rsform';
|
||||
import { CstType, ExpressionStatus } from '@/models/rsform';
|
||||
|
||||
import {
|
||||
IconAlias,
|
||||
IconBusiness,
|
||||
IconCstAxiom,
|
||||
IconCstBaseSet,
|
||||
IconCstConstSet,
|
||||
IconCstFunction,
|
||||
IconCstPredicate,
|
||||
IconCstStructured,
|
||||
IconCstTerm,
|
||||
IconCstTheorem,
|
||||
IconFilter,
|
||||
IconFormula,
|
||||
IconGraphCollapse,
|
||||
|
@ -132,3 +140,24 @@ export function StatusIcon({ value, size = '1.25rem', className }: DomIconProps<
|
|||
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 { LuImage as IconImage } from 'react-icons/lu';
|
||||
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 { LuAtSign as IconTerm } 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 { BiPlusCircle as IconNewItem } from 'react-icons/bi';
|
||||
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 { LuReplace as IconReplace } from 'react-icons/lu';
|
||||
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 { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { CstMatchMode } from '@/models/miscellaneous';
|
||||
import { ConstituentaID, IConstituenta, IRSForm } from '@/models/rsform';
|
||||
import { isBasicConcept } from '@/models/rsformAPI';
|
||||
import { isBasicConcept, matchConstituenta } from '@/models/rsformAPI';
|
||||
import { describeConstituenta } from '@/utils/labels';
|
||||
|
||||
import BadgeConstituenta from '../info/BadgeConstituenta';
|
||||
import NoData from '../ui/NoData';
|
||||
import SearchBar from '../ui/SearchBar';
|
||||
import ToolbarGraphSelection from './ToolbarGraphSelection';
|
||||
|
||||
interface PickMultiConstituentaProps {
|
||||
|
@ -28,18 +30,30 @@ const columnHelper = createColumnHelper<IConstituenta>();
|
|||
function PickMultiConstituenta({ id, schema, prefixID, rows, selected, setSelected }: PickMultiConstituentaProps) {
|
||||
const { colors } = useConceptOptions();
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [filtered, setFiltered] = useState<IConstituenta[]>(schema?.items ?? []);
|
||||
const [filterText, setFilterText] = useState('');
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!schema || selected.length === 0) {
|
||||
if (filtered.length === 0) {
|
||||
setRowSelection({});
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
const newRowSelection: RowSelectionState = {};
|
||||
schema.items.forEach((cst, index) => {
|
||||
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);
|
||||
}
|
||||
}, [selected, schema]);
|
||||
}, [filterText, schema?.items, schema]);
|
||||
|
||||
function handleRowSelection(updater: React.SetStateAction<RowSelectionState>) {
|
||||
if (!schema) {
|
||||
|
@ -47,12 +61,12 @@ function PickMultiConstituenta({ id, schema, prefixID, rows, selected, setSelect
|
|||
} else {
|
||||
const newRowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
|
||||
const newSelection: ConstituentaID[] = [];
|
||||
schema.items.forEach((cst, index) => {
|
||||
filtered.forEach((cst, index) => {
|
||||
if (newRowSelection[String(index)] === true) {
|
||||
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 (
|
||||
<div>
|
||||
<div className='flex items-end gap-3 mb-3'>
|
||||
<span className='w-[24ch] select-none whitespace-nowrap'>
|
||||
<div className='flex justify-between items-center gap-3 clr-input px-3 border-x border-t rounded-t-md'>
|
||||
<div className='w-[24ch] select-none whitespace-nowrap'>
|
||||
Выбраны {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 ? (
|
||||
<ToolbarGraphSelection
|
||||
graph={schema.graph}
|
||||
|
@ -86,7 +107,7 @@ function PickMultiConstituenta({ id, schema, prefixID, rows, selected, setSelect
|
|||
isOwned={cstID => !schema.cstByID.get(cstID)?.is_inherited}
|
||||
setSelected={setSelected}
|
||||
emptySelection={selected.length === 0}
|
||||
className='w-full ml-8'
|
||||
className='w-fit'
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
@ -97,7 +118,7 @@ function PickMultiConstituenta({ id, schema, prefixID, rows, selected, setSelect
|
|||
rows={rows}
|
||||
contentHeight='1.3rem'
|
||||
className={clsx('cc-scroll-y', 'border', 'text-sm', 'select-none')}
|
||||
data={schema?.items ?? []}
|
||||
data={filtered}
|
||||
columns={columns}
|
||||
headPosition='0rem'
|
||||
enableRowSelection
|
||||
|
|
|
@ -96,6 +96,7 @@ function PickSchema({
|
|||
<div className='border divide-y'>
|
||||
<SearchBar
|
||||
id={id ? `${id}__search` : undefined}
|
||||
className='clr-input'
|
||||
noBorder
|
||||
value={filterText}
|
||||
onChange={newValue => setFilterText(newValue)}
|
||||
|
|
|
@ -18,7 +18,7 @@ function Dropdown({ isOpen, stretchLeft, stretchTop, className, children, ...res
|
|||
<motion.div
|
||||
tabIndex={-1}
|
||||
className={clsx(
|
||||
'z-modalTooltip',
|
||||
'z-topmost',
|
||||
'absolute mt-3',
|
||||
'flex flex-col',
|
||||
'border rounded-md shadow-lg',
|
||||
|
|
|
@ -27,7 +27,7 @@ function SearchBar({ id, value, noIcon, onChange, noBorder, placeholder = 'По
|
|||
noOutline
|
||||
placeholder={placeholder}
|
||||
type='search'
|
||||
className={clsx('w-full outline-none', !noIcon && 'pl-10')}
|
||||
className={clsx('w-full outline-none bg-transparent', !noIcon && 'pl-10')}
|
||||
noBorder={noBorder}
|
||||
value={value}
|
||||
onChange={event => (onChange ? onChange(event.target.value) : undefined)}
|
||||
|
|
|
@ -29,7 +29,7 @@ function Tooltip({
|
|||
}
|
||||
return createPortal(
|
||||
<TooltipImpl
|
||||
delayShow={1000}
|
||||
delayShow={750}
|
||||
delayHide={100}
|
||||
opacity={1}
|
||||
className={clsx(
|
||||
|
|
|
@ -74,7 +74,7 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
|
|||
spellCheck
|
||||
label='Термин'
|
||||
placeholder='Обозначение, используемое в текстовых определениях'
|
||||
rows={2}
|
||||
className='cc-fit-content max-h-[3.6rem]'
|
||||
value={state.term_raw}
|
||||
onChange={event => partialUpdate({ term_raw: event.target.value })}
|
||||
/>
|
||||
|
@ -104,7 +104,7 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
|
|||
spellCheck
|
||||
label='Текстовое определение'
|
||||
placeholder='Текстовая интерпретация формального выражения'
|
||||
rows={2}
|
||||
className='cc-fit-content max-h-[3.6rem]'
|
||||
value={state.definition_raw}
|
||||
onChange={event => partialUpdate({ definition_raw: event.target.value })}
|
||||
/>
|
||||
|
@ -128,7 +128,7 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
|
|||
spellCheck
|
||||
label={isBasic ? 'Конвенция' : 'Комментарий'}
|
||||
placeholder={isBasic ? 'Договоренность об интерпретации' : 'Пояснение разработчика'}
|
||||
rows={2}
|
||||
className='cc-fit-content max-h-[5.4rem]'
|
||||
value={state.convention}
|
||||
onChange={event => partialUpdate({ convention: event.target.value })}
|
||||
/>
|
||||
|
|
|
@ -3,30 +3,38 @@
|
|||
import clsx from 'clsx';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import Checkbox from '@/components/ui/Checkbox';
|
||||
import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||
import TextArea from '@/components/ui/TextArea';
|
||||
import TextInput from '@/components/ui/TextInput';
|
||||
import { IVersionData, IVersionInfo } from '@/models/library';
|
||||
import { IVersionCreateData, IVersionInfo } from '@/models/library';
|
||||
import { nextVersion } from '@/models/libraryAPI';
|
||||
import { ConstituentaID } from '@/models/rsform';
|
||||
|
||||
interface DlgCreateVersionProps extends Pick<ModalProps, 'hideWindow'> {
|
||||
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 [description, setDescription] = useState('');
|
||||
const [onlySelected, setOnlySelected] = useState(false);
|
||||
|
||||
const canSubmit = useMemo(() => {
|
||||
return !versions.find(ver => ver.version === version);
|
||||
}, [versions, version]);
|
||||
|
||||
function handleSubmit() {
|
||||
const data: IVersionData = {
|
||||
const data: IVersionCreateData = {
|
||||
version: version,
|
||||
description: description
|
||||
};
|
||||
if (onlySelected) {
|
||||
data.items = selected;
|
||||
}
|
||||
onCreate(data);
|
||||
}
|
||||
|
||||
|
@ -55,6 +63,12 @@ function DlgCreateVersion({ hideWindow, versions, onCreate }: DlgCreateVersionPr
|
|||
value={description}
|
||||
onChange={event => setDescription(event.target.value)}
|
||||
/>
|
||||
<Checkbox
|
||||
id='dlg_only_selected'
|
||||
label={`Только выбранные конституенты [${selected.length} из ${totalCount}]`}
|
||||
value={onlySelected}
|
||||
setValue={value => setOnlySelected(value)}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
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 BadgeHelp from '@/components/info/BadgeHelp';
|
||||
|
@ -18,6 +18,7 @@ import {
|
|||
OperationID,
|
||||
OperationType
|
||||
} from '@/models/oss';
|
||||
import { SubstitutionValidator } from '@/models/ossAPI';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
||||
import TabArguments from './TabArguments';
|
||||
|
@ -44,6 +45,9 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
|
|||
const [title, setTitle] = useState(target.title);
|
||||
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 inputOperations = useMemo(() => inputs.map(id => oss.operationByID.get(id)!), [inputs, oss.operationByID]);
|
||||
const schemasIDs = useMemo(
|
||||
|
@ -54,10 +58,10 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
|
|||
const cache = useRSFormCache();
|
||||
const schemas = useMemo(
|
||||
() => 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(() => {
|
||||
cache.preload(schemasIDs);
|
||||
|
@ -82,7 +86,16 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
|
|||
);
|
||||
}, [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 = {
|
||||
target: target.id,
|
||||
item_data: {
|
||||
|
@ -95,7 +108,7 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
|
|||
substitutions: target.operation_type !== OperationType.SYNTHESIS ? undefined : substitutions
|
||||
};
|
||||
onSubmit(data);
|
||||
};
|
||||
}, [alias, comment, title, inputs, substitutions, target, onSubmit]);
|
||||
|
||||
const cardPanel = useMemo(
|
||||
() => (
|
||||
|
@ -134,12 +147,14 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
|
|||
schemas={schemas}
|
||||
loading={cache.loading}
|
||||
error={cache.error}
|
||||
validationText={validationText}
|
||||
isCorrect={isCorrect}
|
||||
substitutions={substitutions}
|
||||
setSubstitutions={setSubstitutions}
|
||||
/>
|
||||
</TabPanel>
|
||||
),
|
||||
[cache.loading, cache.error, substitutions, schemas]
|
||||
[cache.loading, cache.error, substitutions, schemas, validationText, isCorrect]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -147,7 +162,7 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
|
|||
header='Редактирование операции'
|
||||
submitText='Сохранить'
|
||||
hideWindow={hideWindow}
|
||||
canSubmit={isValid}
|
||||
canSubmit={canSubmit}
|
||||
onSubmit={handleSubmit}
|
||||
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]' />
|
||||
) : null}
|
||||
{target.operation_type === OperationType.SYNTHESIS ? (
|
||||
<TabLabel title='Таблица отождествлений' label='Отождествления' className='w-[8rem]' />
|
||||
<TabLabel
|
||||
titleHtml={'Таблица отождествлений' + (isCorrect ? '' : '<br/>(не прошла проверку)')}
|
||||
label={isCorrect ? 'Отождествления' : 'Отождествления*'}
|
||||
className='w-[8rem]'
|
||||
/>
|
||||
) : null}
|
||||
</TabList>
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { ErrorData } from '@/components/info/InfoError';
|
||||
import PickSubstitutions from '@/components/select/PickSubstitutions';
|
||||
import TextArea from '@/components/ui/TextArea';
|
||||
import DataLoader from '@/components/wrap/DataLoader';
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { ICstSubstitute } from '@/models/oss';
|
||||
import { IRSForm } from '@/models/rsform';
|
||||
import { prefixes } from '@/utils/constants';
|
||||
|
@ -8,22 +10,34 @@ import { prefixes } from '@/utils/constants';
|
|||
interface TabSynthesisProps {
|
||||
loading: boolean;
|
||||
error: ErrorData;
|
||||
validationText: string;
|
||||
isCorrect: boolean;
|
||||
|
||||
schemas: IRSForm[];
|
||||
substitutions: 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 (
|
||||
<DataLoader id='dlg-synthesis-tab' className='cc-column mt-3' isLoading={loading} error={error}>
|
||||
<PickSubstitutions
|
||||
schemas={schemas}
|
||||
prefixID={prefixes.dlg_cst_substitutes_list}
|
||||
rows={8}
|
||||
rows={10}
|
||||
substitutions={substitutions}
|
||||
setSubstitutions={setSubstitutions}
|
||||
/>
|
||||
<TextArea disabled value={validationText} style={{ borderColor: isCorrect ? undefined : colors.fgRed }} />
|
||||
</DataLoader>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -54,6 +54,9 @@ function useRSFormCache() {
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (pending.length === 0) {
|
||||
return;
|
||||
}
|
||||
const ids = pending.filter(id => !processing.includes(id) && !cache.find(schema => schema.id === id));
|
||||
setPending([]);
|
||||
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, 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.
|
||||
*/
|
||||
|
||||
import { ConstituentaID } from './rsform';
|
||||
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'> {}
|
||||
|
||||
/**
|
||||
* Create version metadata in persistent storage.
|
||||
*/
|
||||
export interface IVersionCreateData extends IVersionData {
|
||||
items?: ConstituentaID[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents library item common data typical for all item types.
|
||||
*/
|
||||
|
|
|
@ -192,3 +192,27 @@ export interface IInputCreatedResponse {
|
|||
new_schema: ILibraryItem;
|
||||
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.
|
||||
*/
|
||||
|
||||
import { describeSubstitutionError, information } from '@/utils/labels';
|
||||
import { TextMatcher } from '@/utils/utils';
|
||||
|
||||
import { ILibraryItem } from './library';
|
||||
import { IOperation, IOperationSchema } from './oss';
|
||||
import { Graph } from './Graph';
|
||||
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.
|
||||
|
@ -43,3 +48,278 @@ export function sortItemsForOSS(oss: IOperationSchema, items: ILibraryItem[]): I
|
|||
}
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents alias mapping.
|
||||
*/
|
||||
export type AliasMapping = Record<string, string>;
|
||||
|
||||
/**
|
||||
* Represents formal expression.
|
||||
*/
|
||||
|
@ -199,6 +204,9 @@ export enum TokenID {
|
|||
END
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents RSLang expression error types.
|
||||
*/
|
||||
export enum RSErrorType {
|
||||
unknownSymbol = 33283,
|
||||
syntax = 33792,
|
||||
|
|
|
@ -5,12 +5,13 @@
|
|||
import { applyPattern } from '@/utils/utils';
|
||||
|
||||
import { CstType } from './rsform';
|
||||
import { IArgumentValue, IRSErrorDescription, RSErrorClass, RSErrorType } from './rslang';
|
||||
import { AliasMapping, IArgumentValue, IRSErrorDescription, RSErrorClass, RSErrorType } from './rslang';
|
||||
|
||||
// cspell:disable
|
||||
const LOCALS_REGEXP = /[_a-zα-ω][a-zα-ω]*\d*/g;
|
||||
const GLOBALS_REGEXP = /[XCSADFPT]\d+/g;
|
||||
const COMPLEX_SYMBOLS_REGEXP = /[∀∃×ℬ;|:]/g;
|
||||
const TYPIFICATION_SET = /^ℬ+\([ℬ\(X\d+\)×]*\)$/g;
|
||||
// cspell:enable
|
||||
|
||||
/**
|
||||
|
@ -27,6 +28,13 @@ export function isSimpleExpression(text: string): boolean {
|
|||
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.
|
||||
*/
|
||||
|
@ -91,7 +99,7 @@ export function substituteTemplateArgs(expression: string, args: IArgumentValue[
|
|||
return expression;
|
||||
}
|
||||
|
||||
const mapping: Record<string, string> = {};
|
||||
const mapping: AliasMapping = {};
|
||||
args
|
||||
.filter(arg => !!arg.value)
|
||||
.forEach(arg => {
|
||||
|
@ -144,3 +152,25 @@ export function getRSErrorPrefix(error: IRSErrorDescription): string {
|
|||
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)}
|
||||
/>
|
||||
|
||||
<div className='flex justify-between gap-3'>
|
||||
<div className='flex flex-col gap-2 w-[7rem] h-min'>
|
||||
<div className='flex justify-between gap-3 flex-grow'>
|
||||
<div className='flex flex-col gap-2 min-w-[7rem] h-min'>
|
||||
<Label text='Корень' />
|
||||
<SelectLocationHead
|
||||
value={head}
|
||||
|
@ -197,7 +197,6 @@ function FormCreateItem() {
|
|||
<TextArea
|
||||
id='dlg_cst_body'
|
||||
label='Путь'
|
||||
className='w-[18rem]'
|
||||
rows={4}
|
||||
value={body}
|
||||
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() {
|
||||
return (
|
||||
|
@ -18,7 +18,7 @@ function HelpVersions() {
|
|||
<IconUpload size='1.25rem' className='inline-icon icon-red' /> Загрузить версию в актуальную схему
|
||||
</li>
|
||||
<li>
|
||||
<IconNewItem size='1.25rem' className='inline-icon icon-green' /> Создать версию можно только из актуальной
|
||||
<IconNewVersion size='1.25rem' className='inline-icon icon-green' /> Создать версию можно только из актуальной
|
||||
схемы
|
||||
</li>
|
||||
|
||||
|
|
|
@ -52,9 +52,10 @@ function HelpConceptOSS() {
|
|||
<p>
|
||||
После задания аргументов и таблицы отождествления необходимо единожды{' '}
|
||||
<span className='text-nowrap'>
|
||||
<IconExecute className='inline-icon icon-green' /> выполнить Синтез
|
||||
<IconExecute className='inline-icon icon-green' /> активировать Синтез
|
||||
</span>
|
||||
, чтобы активировать <LinkTopic text='сквозные изменения' topic={HelpTopic.CC_PROPAGATION} />.
|
||||
, чтобы выполнить операцию и активировать{' '}
|
||||
<LinkTopic text='сквозные изменения' topic={HelpTopic.CC_PROPAGATION} />.
|
||||
</p>
|
||||
<p>
|
||||
<span className='text-nowrap'>
|
||||
|
|
|
@ -104,7 +104,7 @@ function HelpOssGraph() {
|
|||
<IconConnect className='inline-icon' /> Выбрать КС для загрузки
|
||||
</li>
|
||||
<li>
|
||||
<IconExecute className='inline-icon icon-green' /> Выполнить (активировать) операцию
|
||||
<IconExecute className='inline-icon icon-green' /> Активировать операцию
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
IconEdit2,
|
||||
IconEditor,
|
||||
IconMenu,
|
||||
IconNewVersion,
|
||||
IconOwner,
|
||||
IconReader,
|
||||
IconShare,
|
||||
|
@ -53,6 +54,9 @@ function HelpRSMenu() {
|
|||
<li>
|
||||
<IconClone className='inline-icon icon-green' /> Клонировать – создать копию схемы
|
||||
</li>
|
||||
<li>
|
||||
<IconNewVersion size='1.25rem' className='inline-icon icon-green' /> Сохранить версию
|
||||
</li>
|
||||
<li>
|
||||
<IconDownload className='inline-icon' /> Выгрузить – сохранить в файле формата Экстеор
|
||||
</li>
|
||||
|
|
|
@ -140,10 +140,10 @@ function NodeContextMenu({
|
|||
) : null}
|
||||
{controller.isMutable && !operation.result && operation.operation_type === OperationType.SYNTHESIS ? (
|
||||
<DropdownButton
|
||||
text='Выполнить синтез'
|
||||
title={
|
||||
text='Активировать синтез'
|
||||
titleHtml={
|
||||
readyForSynthesis
|
||||
? 'Выполнить операцию и получить синтезированную КС'
|
||||
? 'Активировать операцию<br/>и получить синтезированную КС'
|
||||
: 'Необходимо предоставить все аргументы'
|
||||
}
|
||||
icon={<IconExecute size='1rem' className='icon-green' />}
|
||||
|
|
|
@ -161,7 +161,7 @@ function ToolbarOssGraph({
|
|||
onClick={onCreate}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Выполнить операцию'
|
||||
title='Активировать операцию'
|
||||
icon={<IconExecute size='1.25rem' className='icon-green' />}
|
||||
disabled={controller.isProcessing || controller.selected.length !== 1 || !readyForSynthesis}
|
||||
onClick={onExecute}
|
||||
|
|
|
@ -12,6 +12,7 @@ import Loader from '@/components/ui/Loader';
|
|||
import TabLabel from '@/components/ui/TabLabel';
|
||||
import TextURL from '@/components/ui/TextURL';
|
||||
import AnimateFade from '@/components/wrap/AnimateFade';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { useLibrary } from '@/context/LibraryContext';
|
||||
import { useBlockNavigation, useConceptNavigation } from '@/context/NavigationContext';
|
||||
|
@ -34,6 +35,7 @@ function OssTabs() {
|
|||
const router = useConceptNavigation();
|
||||
const query = useQueryStrings();
|
||||
const activeTab = query.get('tab') ? (Number(query.get('tab')) as OssTabID) : OssTabID.GRAPH;
|
||||
const { user } = useAuth();
|
||||
|
||||
const { calculateHeight, setNoFooter } = useConceptOptions();
|
||||
const { schema, loading, loadingError: errorLoading } = useOSS();
|
||||
|
@ -41,7 +43,12 @@ function OssTabs() {
|
|||
|
||||
const [isModified, setIsModified] = useState(false);
|
||||
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(() => {
|
||||
if (schema) {
|
||||
|
|
|
@ -20,11 +20,6 @@ import { information, labelCstTypification } from '@/utils/labels';
|
|||
import EditorRSExpression from '../EditorRSExpression';
|
||||
import ControlsOverlay from './ControlsOverlay';
|
||||
|
||||
/**
|
||||
* Characters limit to start increasing number of rows.
|
||||
*/
|
||||
export const ROW_SIZE_IN_CHARACTERS = 70;
|
||||
|
||||
interface FormConstituentaProps {
|
||||
disabled: boolean;
|
||||
|
||||
|
@ -166,7 +161,7 @@ function FormConstituenta({
|
|||
{state ? (
|
||||
<TextArea
|
||||
id='cst_typification'
|
||||
rows={typification.length > ROW_SIZE_IN_CHARACTERS ? 2 : 1}
|
||||
className='cc-fit-content'
|
||||
dense
|
||||
noResize
|
||||
noBorder
|
||||
|
@ -220,12 +215,12 @@ function FormConstituenta({
|
|||
<AnimateFade key='cst_convention_fade' hideContent={!showConvention || !state}>
|
||||
<TextArea
|
||||
id='cst_convention'
|
||||
className='cc-fit-content max-h-[8rem]'
|
||||
spellCheck
|
||||
label={isBasic ? 'Конвенция' : 'Комментарий'}
|
||||
placeholder={isBasic ? 'Договоренность об интерпретации' : 'Пояснение разработчика'}
|
||||
value={convention}
|
||||
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)}
|
||||
/>
|
||||
</AnimateFade>
|
||||
|
|
|
@ -49,7 +49,10 @@ function ToolbarConstituenta({
|
|||
const controller = useRSEdit();
|
||||
|
||||
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 ? (
|
||||
<MiniSelectorOSS
|
||||
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 MiniButton from '@/components/ui/MiniButton';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
|
@ -33,7 +33,7 @@ function ToolbarVersioning({ blockReload }: ToolbarVersioningProps) {
|
|||
titleHtml={controller.isContentEditable ? 'Создать версию' : 'Переключитесь <br/>на актуальную версию'}
|
||||
disabled={!controller.isContentEditable}
|
||||
onClick={controller.createVersion}
|
||||
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
||||
icon={<IconNewVersion size='1.25rem' className='icon-green' />}
|
||||
/>
|
||||
<MiniButton
|
||||
title={controller.schema?.versions.length === 0 ? 'Список версий пуст' : 'Редактировать версии'}
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import fileDownload from 'js-file-download';
|
||||
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { IconCSV } from '@/components/Icons';
|
||||
import SelectedCounter from '@/components/info/SelectedCounter';
|
||||
import { type RowSelectionState } from '@/components/ui/DataTable';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
import SearchBar from '@/components/ui/SearchBar';
|
||||
import AnimateFade from '@/components/wrap/AnimateFade';
|
||||
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 { convertToCSV } from '@/utils/utils';
|
||||
|
||||
|
@ -29,30 +30,43 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
|
|||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const controller = useRSEdit();
|
||||
|
||||
const [filtered, setFiltered] = useState<IConstituenta[]>(controller.schema?.items ?? []);
|
||||
const [filterText, setFilterText] = useState('');
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!controller.schema || controller.selected.length === 0) {
|
||||
if (filtered.length === 0) {
|
||||
setRowSelection({});
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
const newRowSelection: RowSelectionState = {};
|
||||
controller.schema.items.forEach((cst, index) => {
|
||||
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);
|
||||
}
|
||||
}, [controller.selected, controller.schema]);
|
||||
}, [filterText, controller.schema?.items, controller.schema]);
|
||||
|
||||
const handleDownloadCSV = useCallback(() => {
|
||||
if (!controller.schema || controller.schema.items.length === 0) {
|
||||
if (!controller.schema || filtered.length === 0) {
|
||||
toast.error(information.noDataToExport);
|
||||
return;
|
||||
}
|
||||
const blob = convertToCSV(controller.schema.items);
|
||||
const blob = convertToCSV(filtered);
|
||||
try {
|
||||
fileDownload(blob, `${controller.schema.alias}.csv`, 'text/csv;charset=utf-8;');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, [controller]);
|
||||
}, [filtered, controller]);
|
||||
|
||||
function handleRowSelection(updater: React.SetStateAction<RowSelectionState>) {
|
||||
if (!controller.schema) {
|
||||
|
@ -60,12 +74,15 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
|
|||
} else {
|
||||
const newRowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
|
||||
const newSelection: ConstituentaID[] = [];
|
||||
controller.schema.items.forEach((cst, index) => {
|
||||
filtered.forEach((cst, index) => {
|
||||
if (newRowSelection[String(index)] === true) {
|
||||
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}
|
||||
<AnimateFade tabIndex={-1} onKeyDown={handleKeyDown}>
|
||||
{controller.isContentEditable ? (
|
||||
<SelectedCounter
|
||||
totalCount={controller.schema?.stats?.count_all ?? 0}
|
||||
selectedCount={controller.selected.length}
|
||||
position='top-[0.3rem] left-2'
|
||||
<div className='flex items-center border-b'>
|
||||
<div className='px-2'>
|
||||
Выбор {controller.selected.length} из {controller.schema?.stats?.count_all ?? 0}
|
||||
</div>
|
||||
<SearchBar
|
||||
id='constituents_search'
|
||||
noBorder
|
||||
className='w-[8rem]'
|
||||
value={filterText}
|
||||
onChange={setFilterText}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className={clsx('border-b', {
|
||||
'pt-[2.3rem]': controller.isContentEditable,
|
||||
'relative top-[-1px]': !controller.isContentEditable
|
||||
})}
|
||||
/>
|
||||
|
||||
<Overlay position='top-[0.25rem] right-[1rem]' layer='z-tooltip'>
|
||||
<Overlay position='top-[0.25rem] right-[1rem]' layer='z-navigation'>
|
||||
<MiniButton
|
||||
title='Выгрузить в формате CSV'
|
||||
icon={<IconCSV size='1.25rem' className='icon-green' />}
|
||||
|
@ -150,7 +167,7 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
|
|||
</Overlay>
|
||||
|
||||
<TableRSList
|
||||
items={controller.schema?.items}
|
||||
items={filtered}
|
||||
maxHeight={tableHeight}
|
||||
enableSelection={controller.isContentEditable}
|
||||
selected={rowSelection}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { CstTypeIcon } from '@/components/DomainIcons';
|
||||
import {
|
||||
IconClone,
|
||||
IconDestroy,
|
||||
|
@ -16,7 +17,6 @@ import Overlay from '@/components/ui/Overlay';
|
|||
import useDropdown from '@/hooks/useDropdown';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
import { CstType } from '@/models/rsform';
|
||||
import { getCstTypePrefix } from '@/models/rsformAPI';
|
||||
import { prefixes } from '@/utils/constants';
|
||||
import { getCstTypeShortcut, labelCstType, prepareTooltip } from '@/utils/labels';
|
||||
|
||||
|
@ -27,7 +27,10 @@ function ToolbarRSList() {
|
|||
const insertMenu = useDropdown();
|
||||
|
||||
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 ? (
|
||||
<MiniSelectorOSS
|
||||
items={controller.schema.oss}
|
||||
|
@ -52,18 +55,6 @@ function ToolbarRSList() {
|
|||
disabled={controller.isProcessing || controller.nothingSelected}
|
||||
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}>
|
||||
<MiniButton
|
||||
title='Добавить пустую конституенту'
|
||||
|
@ -72,17 +63,30 @@ function ToolbarRSList() {
|
|||
disabled={controller.isProcessing}
|
||||
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 => (
|
||||
<DropdownButton
|
||||
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)}
|
||||
titleHtml={getCstTypeShortcut(typeStr as CstType)}
|
||||
/>
|
||||
))}
|
||||
</Dropdown>
|
||||
</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
|
||||
titleHtml={prepareTooltip('Удалить выбранные', 'Delete')}
|
||||
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
IconLibrary,
|
||||
IconMenu,
|
||||
IconNewItem,
|
||||
IconNewVersion,
|
||||
IconOSS,
|
||||
IconOwner,
|
||||
IconReader,
|
||||
|
@ -156,6 +157,12 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
|||
onClick={handleClone}
|
||||
/>
|
||||
) : null}
|
||||
<DropdownButton
|
||||
text='Сохранить версию'
|
||||
disabled={!controller.isContentEditable}
|
||||
onClick={controller.createVersion}
|
||||
icon={<IconNewVersion size='1rem' className='icon-green' />}
|
||||
/>
|
||||
<DropdownButton
|
||||
text='Выгрузить в Экстеор'
|
||||
icon={<IconDownload size='1rem' className='icon-primary' />}
|
||||
|
|
|
@ -335,9 +335,8 @@ export const RSEditState = ({
|
|||
if (!model.schema) {
|
||||
return;
|
||||
}
|
||||
model.versionCreate(data, newVersion => {
|
||||
model.versionCreate(data, () => {
|
||||
toast.success(information.newVersion(data.version));
|
||||
viewVersion(newVersion);
|
||||
});
|
||||
},
|
||||
[model, viewVersion]
|
||||
|
@ -725,6 +724,8 @@ export const RSEditState = ({
|
|||
versions={model.schema.versions}
|
||||
hideWindow={() => setShowCreateVersion(false)}
|
||||
onCreate={handleCreateVersion}
|
||||
selected={selected}
|
||||
totalCount={model.schema.items.length}
|
||||
/>
|
||||
) : null}
|
||||
{showEditVersions ? (
|
||||
|
|
|
@ -267,7 +267,7 @@ function RSTabs() {
|
|||
<TabLabel label='Граф термов' />
|
||||
</TabList>
|
||||
|
||||
<AnimateFade className='overflow-y-auto' style={{ maxHeight: panelHeight }}>
|
||||
<AnimateFade className='overflow-y-auto overflow-x-hidden' style={{ maxHeight: panelHeight }}>
|
||||
{cardPanel}
|
||||
{listPanel}
|
||||
{editorPanel}
|
||||
|
|
|
@ -17,6 +17,7 @@ interface TableSideConstituentsProps {
|
|||
activeCst?: IConstituenta;
|
||||
onOpenEdit: (cstID: ConstituentaID) => void;
|
||||
denseThreshold?: number;
|
||||
autoScroll?: boolean;
|
||||
maxHeight: string;
|
||||
}
|
||||
|
||||
|
@ -25,6 +26,7 @@ const columnHelper = createColumnHelper<IConstituenta>();
|
|||
function TableSideConstituents({
|
||||
items,
|
||||
activeCst,
|
||||
autoScroll = true,
|
||||
onOpenEdit,
|
||||
maxHeight,
|
||||
denseThreshold = 9999
|
||||
|
@ -38,6 +40,7 @@ function TableSideConstituents({
|
|||
if (!activeCst) {
|
||||
return;
|
||||
}
|
||||
if (autoScroll) {
|
||||
setTimeout(() => {
|
||||
const element = document.getElementById(`${prefixes.cst_side_table}${activeCst.alias}`);
|
||||
if (element) {
|
||||
|
@ -48,7 +51,8 @@ function TableSideConstituents({
|
|||
});
|
||||
}
|
||||
}, PARAMETER.refreshTimeout);
|
||||
}, [activeCst]);
|
||||
}
|
||||
}, [activeCst, autoScroll]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setColumnVisibility(prev => {
|
||||
|
|
|
@ -46,6 +46,7 @@ function ViewConstituents({ expression, schema, activeCst, isBottom, onOpenEdit
|
|||
items={filteredData}
|
||||
activeCst={activeCst}
|
||||
onOpenEdit={onOpenEdit}
|
||||
autoScroll={!isBottom}
|
||||
denseThreshold={COLUMN_EXPRESSION_HIDE_THRESHOLD}
|
||||
/>
|
||||
),
|
||||
|
@ -55,10 +56,10 @@ function ViewConstituents({ expression, schema, activeCst, isBottom, onOpenEdit
|
|||
return (
|
||||
<motion.div
|
||||
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-3 mx-6 rounded-md md:w-[45.8rem]': isBottom
|
||||
'mt-[2.2rem] rounded-l-md rounded-r-none h-fit overflow-visible': !isBottom,
|
||||
'mt-3 mx-6 rounded-md md:max-w-[45.8rem] overflow-hidden': isBottom
|
||||
}
|
||||
)}
|
||||
initial={{ ...animateSideView.initial }}
|
||||
|
|
|
@ -226,6 +226,10 @@
|
|||
@apply flex gap-1;
|
||||
}
|
||||
|
||||
.cc-fit-content {
|
||||
field-sizing: content;
|
||||
}
|
||||
|
||||
.cc-scroll-row {
|
||||
scroll-snap-align: start;
|
||||
scroll-snap-stop: always;
|
||||
|
|
|
@ -10,7 +10,7 @@ import { GramData, Grammeme, ReferenceType } from '@/models/language';
|
|||
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
|
||||
import { validateLocation } from '@/models/libraryAPI';
|
||||
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 {
|
||||
IArgumentInfo,
|
||||
|
@ -799,6 +799,35 @@ export function describeRSError(error: IRSErrorDescription): string {
|
|||
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}.
|
||||
*/
|
||||
|
@ -934,6 +963,7 @@ export const information = {
|
|||
locationRenamed: 'Ваши схемы перемещены',
|
||||
cloneComplete: (alias: string) => `Копия создана: ${alias}`,
|
||||
noDataToExport: 'Нет данных для экспорта',
|
||||
substitutionsCorrect: 'Таблица отождествлений прошла проверку',
|
||||
|
||||
addedConstituents: (count: number) => `Добавлены конституенты: ${count}`,
|
||||
newLibraryItem: 'Схема успешно создана',
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
import axios, { AxiosError, AxiosHeaderValue, AxiosResponse } from 'axios';
|
||||
|
||||
import { AliasMapping } from '@/models/rslang';
|
||||
|
||||
import { prompts } from './labels';
|
||||
|
||||
/**
|
||||
|
@ -46,7 +48,7 @@ export class TextMatcher {
|
|||
/**
|
||||
* 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) {
|
||||
return text;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user