Compare commits

...

9 Commits

Author SHA1 Message Date
Ivan
d8286e6339 M: Small UI fixes
Some checks failed
Backend CI / build (3.12) (push) Has been cancelled
Frontend CI / build (22.x) (push) Has been cancelled
2024-08-27 11:33:56 +03:00
Ivan
8bd2c71f50 F: Implement typification checks on substitutions 2024-08-26 22:53:02 +03:00
Ivan
30ec76cefd F: Implement basic substitution checks 2024-08-26 17:24:46 +03:00
Ivan
584ce59f2d F: Add search feature to multiple cst selector 2024-08-25 13:52:14 +03:00
Ivan
f6b52adacd M: Improve mobile UI 2024-08-24 19:40:54 +03:00
Ivan
1efa13b1c5 F: Improve versioning UI 2024-08-24 12:29:09 +03:00
Ivan
ce857bb2cb M: Small UI layout fixes 2024-08-24 11:20:49 +03:00
Ivan
a071f916e0 npm update 2024-08-24 11:19:13 +03:00
Ivan
07974e3760 B: Fix RSForm editor layout 2024-08-24 08:24:05 +03:00
50 changed files with 855 additions and 210 deletions

View File

@ -155,6 +155,7 @@
"toastify",
"tooltipic",
"tsdoc",
"Typifications",
"unknwn",
"Upvote",
"Viewset",

View File

@ -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):

View File

@ -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'})

View File

@ -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'],

View File

@ -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": {

View File

@ -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"

View File

@ -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
}}

View File

@ -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'
)}

View File

@ -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}

View File

@ -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

View File

@ -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'} />;
}
}

View File

@ -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';

View File

@ -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 {
const newRowSelection: RowSelectionState = {};
schema.items.forEach((cst, index) => {
newRowSelection[String(index)] = selected.includes(cst.id);
});
setRowSelection(newRowSelection);
return;
}
}, [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>) {
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

View File

@ -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)}

View File

@ -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',

View File

@ -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)}

View File

@ -29,7 +29,7 @@ function Tooltip({
}
return createPortal(
<TooltipImpl
delayShow={1000}
delayShow={750}
delayHide={100}
opacity={1}
className={clsx(

View File

@ -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 })}
/>

View File

@ -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>
);
}

View File

@ -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>

View File

@ -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>
);
}

View File

@ -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) {

View File

@ -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);
});
});

View File

@ -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;
}
}

View File

@ -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.
*/

View File

@ -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
}

View File

@ -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;
}
}

View File

@ -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,

View File

@ -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;
}

View File

@ -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)}

View File

@ -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>

View File

@ -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'>

View File

@ -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>

View File

@ -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>

View File

@ -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' />}

View File

@ -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}

View File

@ -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) {

View File

@ -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>

View File

@ -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}

View File

@ -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 ? 'Список версий пуст' : 'Редактировать версии'}

View File

@ -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 {
const newRowSelection: RowSelectionState = {};
controller.schema.items.forEach((cst, index) => {
newRowSelection[String(index)] = controller.selected.includes(cst.id);
});
setRowSelection(newRowSelection);
return;
}
}, [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(() => {
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}

View File

@ -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' />}

View File

@ -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' />}

View File

@ -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 ? (

View File

@ -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}

View File

@ -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,17 +40,19 @@ function TableSideConstituents({
if (!activeCst) {
return;
}
setTimeout(() => {
const element = document.getElementById(`${prefixes.cst_side_table}${activeCst.alias}`);
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'end'
});
}
}, PARAMETER.refreshTimeout);
}, [activeCst]);
if (autoScroll) {
setTimeout(() => {
const element = document.getElementById(`${prefixes.cst_side_table}${activeCst.alias}`);
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'end'
});
}
}, PARAMETER.refreshTimeout);
}
}, [activeCst, autoScroll]);
useLayoutEffect(() => {
setColumnVisibility(prev => {

View File

@ -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 }}

View File

@ -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;

View File

@ -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: 'Схема успешно создана',

View File

@ -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;
}