Compare commits

...

18 Commits

Author SHA1 Message Date
Ivan
c05759901a F: Filter out users without schemas
Some checks failed
Backend CI / build (3.12) (push) Has been cancelled
Frontend CI / build (22.x) (push) Has been cancelled
Backend CI / notify-failure (push) Has been cancelled
Frontend CI / notify-failure (push) Has been cancelled
2025-06-11 11:00:40 +03:00
Ivan
ac50a4fb05 B: Fix token validation issue 2025-06-10 23:20:13 +03:00
Ivan
2b7acfbd27 R: Replace constant with formula 2025-06-10 22:16:44 +03:00
Ivan
313d5de706 B: Fix operation precedences 2025-06-10 21:49:23 +03:00
Ivan
c166a4478a B: Do not change state in render 2025-06-10 21:28:21 +03:00
Ivan
5dcaae4f51 Update text-editing.ts 2025-06-10 21:27:41 +03:00
Ivan
f0f41a4dcb M: Improve double boolean editing 2025-06-10 21:20:22 +03:00
Ivan
2b65b19d08 R: Fix onSuccess typing for react-query 2025-06-05 15:46:01 +03:00
Ivan
53d8dfc3d6 npm update 2025-06-05 15:29:52 +03:00
Ivan
ece60e565d F: Refactor node ID and improve layout for new items 2025-06-05 15:26:06 +03:00
Ivan
841a72af4e update dependencies 2025-05-29 15:46:56 +03:00
Ivan
8452bf4d55 M: Small fixes 2025-05-29 15:38:39 +03:00
Ivan
32eee7ca0b M: Fix ENV format 2025-05-15 12:37:11 +03:00
Ivan
addcd52cb6 B: Fix list HTML structure 2025-05-14 12:29:53 +03:00
Ivan
c442ca8094 M: Minor UI fixes 2025-05-14 11:48:14 +03:00
Ivan
a26dd4bc7b B: Fix context menu position 2025-05-14 11:18:49 +03:00
Ivan
16a74bf4f8 R: Improve layouting commands 2025-05-14 10:56:40 +03:00
Ivan
5f2239dc26 Update text-editing.ts 2025-05-14 10:52:54 +03:00
93 changed files with 2281 additions and 3388 deletions

View File

@ -58,14 +58,6 @@
"depth": 2 "depth": 2
} }
], ],
"colorize.include": [".tsx", ".jsx", ".ts", ".js"],
"colorize.languages": [
"typescript",
"javascript",
"css",
"typescriptreact",
"javascriptreact"
],
"cSpell.words": [ "cSpell.words": [
"ablt", "ablt",
"acconcept", "acconcept",

View File

@ -28,8 +28,8 @@ RUN apt-get update -qq && \
FROM python-base AS builder FROM python-base AS builder
# Set env variables # Set env variables
ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED 1 ENV PYTHONUNBUFFERED=1
COPY ./requirements.txt ./ COPY ./requirements.txt ./
RUN python3.12 -m pip wheel \ RUN python3.12 -m pip wheel \

View File

@ -7410,13 +7410,12 @@
"pk": 1, "pk": 1,
"fields": { "fields": {
"oss": 41, "oss": 41,
"parent": null,
"operation_type": "input", "operation_type": "input",
"result": 38, "result": 38,
"alias": "КС Вещества", "alias": "КС Вещества",
"title": "Вещества и смеси", "title": "Вещества и смеси",
"description": "", "description": ""
"position_x": 530.0,
"position_y": 370.0
} }
}, },
{ {
@ -7424,13 +7423,12 @@
"pk": 2, "pk": 2,
"fields": { "fields": {
"oss": 41, "oss": 41,
"parent": null,
"operation_type": "input", "operation_type": "input",
"result": 39, "result": 39,
"alias": "КС ООО", "alias": "КС ООО",
"title": "Объект-объектные отношения", "title": "Объект-объектные отношения",
"description": "", "description": ""
"position_x": 710.0,
"position_y": 370.0
} }
}, },
{ {
@ -7438,13 +7436,12 @@
"pk": 4, "pk": 4,
"fields": { "fields": {
"oss": 41, "oss": 41,
"parent": null,
"operation_type": "input", "operation_type": "input",
"result": 40, "result": 40,
"alias": "КС Процессы", "alias": "КС Процессы",
"title": "Процессы", "title": "Процессы",
"description": "", "description": ""
"position_x": 890.0,
"position_y": 370.0
} }
}, },
{ {
@ -7452,13 +7449,12 @@
"pk": 9, "pk": 9,
"fields": { "fields": {
"oss": 41, "oss": 41,
"parent": null,
"operation_type": "synthesis", "operation_type": "synthesis",
"result": 43, "result": 43,
"alias": "КС Объект-сред", "alias": "КС Объект-сред",
"title": "Объектная среда", "title": "Объектная среда",
"description": "", "description": ""
"position_x": 620.0,
"position_y": 470.0
} }
}, },
{ {
@ -7466,13 +7462,49 @@
"pk": 10, "pk": 10,
"fields": { "fields": {
"oss": 41, "oss": 41,
"parent": null,
"operation_type": "synthesis", "operation_type": "synthesis",
"result": 44, "result": 44,
"alias": "КС Проц-сред", "alias": "КС Проц-сред",
"title": "Процессные среды", "title": "Процессные среды",
"description": "", "description": ""
"position_x": 760.0, }
"position_y": 570.0 },
{
"model": "oss.layout",
"pk": 1,
"fields": {
"oss": 41,
"data": {
"blocks": [],
"operations": [
{
"x": 530.0,
"y": 370.0,
"id": 1
},
{
"x": 710.0,
"y": 370.0,
"id": 2
},
{
"x": 890.0,
"y": 370.0,
"id": 4
},
{
"x": 620.0,
"y": 470.0,
"id": 9
},
{
"x": 760.0,
"y": 570.0,
"id": 10
}
]
}
} }
} }
] ]

View File

@ -1,10 +1,10 @@
tzdata==2025.2 tzdata==2025.2
Django==5.1.7 Django==5.2.1
djangorestframework==3.15.2 djangorestframework==3.16.0
django-cors-headers==4.7.0 django-cors-headers==4.7.0
django-filter==25.1 django-filter==25.1
drf-spectacular==0.28.0 drf-spectacular==0.28.0
drf-spectacular-sidecar==2025.3.1 drf-spectacular-sidecar==2025.5.1
coreapi==2.3.3 coreapi==2.3.3
django-rest-passwordreset==1.5.0 django-rest-passwordreset==1.5.0
cctext==0.1.4 cctext==0.1.4
@ -13,9 +13,9 @@ pyconcept==0.1.12
psycopg2-binary==2.9.10 psycopg2-binary==2.9.10
gunicorn==23.0.0 gunicorn==23.0.0
djangorestframework-stubs==3.15.3 djangorestframework-stubs==3.16.0
django-extensions==3.2.3 django-extensions==4.1
django-stubs==5.1.3 django-stubs==5.2.0
mypy==1.15.0 mypy==1.15.0
pylint==3.3.6 pylint==3.3.7
coverage==7.7.1 coverage==7.8.2

View File

@ -1,10 +1,10 @@
tzdata==2025.2 tzdata==2025.2
Django==5.1.7 Django==5.2.1
djangorestframework==3.15.2 djangorestframework==3.16.0
django-cors-headers==4.7.0 django-cors-headers==4.7.0
django-filter==25.1 django-filter==25.1
drf-spectacular==0.28.0 drf-spectacular==0.28.0
drf-spectacular-sidecar==2025.3.1 drf-spectacular-sidecar==2025.5.1
coreapi==2.3.3 coreapi==2.3.3
django-rest-passwordreset==1.5.0 django-rest-passwordreset==1.5.0
cctext==0.1.4 cctext==0.1.4

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@
"version": "1.0.0", "version": "1.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"generate": "lezer-generator src/components/rs-input/rslang/rslang-fast.grammar -o src/components/rs-input/rslang/parser.ts && lezer-generator src/components/rs-input/rslang/rslang-ast.grammar -o src/components/rs-input/rslang/parserAST.ts && lezer-generator src/components/refs-input/parse/refs-text.grammar -o src/components/refs-input/parse/parser.ts", "generate": "lezer-generator src/features/rsform/components/rs-input/rslang/rslang-fast.grammar -o src/features/rsform/components/rs-input/rslang/parser.ts && lezer-generator src/features/rsform/components/rs-input/rslang/rslang-ast.grammar -o src/features/rsform/components/rs-input/rslang/parser-ast.ts && lezer-generator src/features/rsform/components/refs-input/parse/refs-text.grammar -o src/features/rsform/components/refs-input/parse/parser.ts",
"test": "jest", "test": "jest",
"test:e2e": "playwright test", "test:e2e": "playwright test",
"dev": "vite --host", "dev": "vite --host",
@ -17,10 +17,10 @@
"@dagrejs/dagre": "^1.1.4", "@dagrejs/dagre": "^1.1.4",
"@hookform/resolvers": "^5.0.1", "@hookform/resolvers": "^5.0.1",
"@lezer/lr": "^1.4.2", "@lezer/lr": "^1.4.2",
"@radix-ui/react-popover": "^1.1.13", "@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-select": "^2.2.4", "@radix-ui/react-select": "^2.2.5",
"@tanstack/react-query": "^5.76.0", "@tanstack/react-query": "^5.80.5",
"@tanstack/react-query-devtools": "^5.76.0", "@tanstack/react-query-devtools": "^5.80.5",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"@uiw/codemirror-themes": "^4.23.12", "@uiw/codemirror-themes": "^4.23.12",
"@uiw/react-codemirror": "^4.23.12", "@uiw/react-codemirror": "^4.23.12",
@ -30,56 +30,56 @@
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
"global": "^4.4.0", "global": "^4.4.0",
"js-file-download": "^0.4.12", "js-file-download": "^0.4.12",
"lucide-react": "^0.510.0", "lucide-react": "^0.511.0",
"qrcode.react": "^4.2.0", "qrcode.react": "^4.2.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-error-boundary": "^6.0.0", "react-error-boundary": "^6.0.0",
"react-hook-form": "^7.56.3", "react-hook-form": "^7.57.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-intl": "^7.1.11", "react-intl": "^7.1.11",
"react-router": "^7.6.0", "react-router": "^7.6.2",
"react-scan": "^0.3.3", "react-scan": "^0.3.4",
"react-tabs": "^6.1.0", "react-tabs": "^6.1.0",
"react-toastify": "^11.0.5", "react-toastify": "^11.0.5",
"react-tooltip": "^5.28.1", "react-tooltip": "^5.28.1",
"react-zoom-pan-pinch": "^3.7.0", "react-zoom-pan-pinch": "^3.7.0",
"reactflow": "^11.11.4", "reactflow": "^11.11.4",
"tailwind-merge": "^3.3.0", "tailwind-merge": "^3.3.0",
"tw-animate-css": "^1.2.9", "tw-animate-css": "^1.3.4",
"use-debounce": "^10.0.4", "use-debounce": "^10.0.4",
"zod": "^3.24.4", "zod": "^3.25.51",
"zustand": "^5.0.4" "zustand": "^5.0.5"
}, },
"devDependencies": { "devDependencies": {
"@lezer/generator": "^1.7.3", "@lezer/generator": "^1.7.3",
"@playwright/test": "^1.52.0", "@playwright/test": "^1.52.0",
"@tailwindcss/vite": "^4.1.6", "@tailwindcss/vite": "^4.1.8",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/node": "^22.15.17", "@types/node": "^22.15.29",
"@types/react": "^19.1.4", "@types/react": "^19.1.6",
"@types/react-dom": "^19.1.5", "@types/react-dom": "^19.1.6",
"@typescript-eslint/eslint-plugin": "^8.0.1", "@typescript-eslint/eslint-plugin": "^8.0.1",
"@typescript-eslint/parser": "^8.0.1", "@typescript-eslint/parser": "^8.0.1",
"@vitejs/plugin-react": "^4.4.1", "@vitejs/plugin-react": "^4.5.1",
"babel-plugin-react-compiler": "^19.1.0-rc.1", "babel-plugin-react-compiler": "^19.1.0-rc.1",
"eslint": "^9.26.0", "eslint": "^9.28.0",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"eslint-plugin-playwright": "^2.2.0", "eslint-plugin-playwright": "^2.2.0",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-compiler": "^19.1.0-rc.1", "eslint-plugin-react-compiler": "^19.1.0-rc.1",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
"globals": "^16.1.0", "globals": "^16.2.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"stylelint": "^16.19.1", "stylelint": "^16.20.0",
"stylelint-config-recommended": "^16.0.0", "stylelint-config-recommended": "^16.0.0",
"stylelint-config-standard": "^38.0.0", "stylelint-config-standard": "^38.0.0",
"stylelint-config-tailwindcss": "^1.0.0", "stylelint-config-tailwindcss": "^1.0.0",
"tailwindcss": "^4.0.7", "tailwindcss": "^4.0.7",
"ts-jest": "^29.3.2", "ts-jest": "^29.3.4",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"typescript-eslint": "^8.32.1", "typescript-eslint": "^8.33.1",
"vite": "^6.3.5" "vite": "^6.3.5"
}, },
"jest": { "jest": {

View File

@ -47,6 +47,7 @@ export function TextArea({
<textarea <textarea
id={id} id={id}
className={cn( className={cn(
'min-h-0',
'px-3 py-2', 'px-3 py-2',
'leading-tight', 'leading-tight',
'overflow-x-hidden overflow-y-auto', 'overflow-x-hidden overflow-y-auto',

View File

@ -19,8 +19,8 @@ function useTokenValidation(token: string, isPending: boolean) {
const validate = async () => { const validate = async () => {
if (!isTokenValidating && !isPending) { if (!isTokenValidating && !isPending) {
await validateToken({ token });
setIsTokenValidating(true); setIsTokenValidating(true);
await validateToken({ token });
} }
}; };
return { isTokenValidating, validate }; return { isTokenValidating, validate };
@ -44,10 +44,7 @@ export function Component() {
void resetPassword({ void resetPassword({
password: newPassword, password: newPassword,
token: token token: token
}).then(() => { }).then(() => router.replace({ path: urls.login }));
router.replace({ path: urls.home });
router.push({ path: urls.login });
});
} }
} }
@ -83,6 +80,9 @@ export function Component() {
setNewPasswordRepeat(event.target.value); setNewPasswordRepeat(event.target.value);
}} }}
/> />
{newPasswordRepeat && newPassword !== newPasswordRepeat ? (
<div className='text-sm text-destructive'>Пароли не совпадают</div>
) : null}
<SubmitButton <SubmitButton
text='Установить пароль' text='Установить пароль'

View File

@ -35,6 +35,7 @@ export function Component() {
<form className='cc-column w-96 mx-auto px-6 mt-3' onSubmit={handleSubmit} onChange={clearServerError}> <form className='cc-column w-96 mx-auto px-6 mt-3' onSubmit={handleSubmit} onChange={clearServerError}>
<TextInput <TextInput
id='email' id='email'
type='email'
autoComplete='email' autoComplete='email'
required required
allowEnter allowEnter

View File

@ -20,6 +20,7 @@ export function HelpConceptSynthesis() {
<p> <p>
Расширение выразительной способности достигается несколькими способами в зависимости от соотношения Расширение выразительной способности достигается несколькими способами в зависимости от соотношения
синтезируемых точек зрения: синтезируемых точек зрения:
<ul>
<li> <li>
<b>аспектный синтез</b> характеризуется отождествлением общих понятий в случае, когда часть неопределяемых <b>аспектный синтез</b> характеризуется отождествлением общих понятий в случае, когда часть неопределяемых
понятий является общей для двух точек зрения; понятий является общей для двух точек зрения;
@ -33,11 +34,12 @@ export function HelpConceptSynthesis() {
интерпретации) схему для соединения понятий из двух операндов путем введения нового неопределяемого понятия, интерпретации) схему для соединения понятий из двух операндов путем введения нового неопределяемого понятия,
моделирующего отношения между синтезируемыми схемами. моделирующего отношения между синтезируемыми схемами.
</li> </li>
</ul>
</p> </p>
<p> <p>
Возможно использование комбинации описанных подходов в рамках одного синтеза. Более подробно про реализацию Поддерживается использование комбинации описанных подходов в рамках одного синтеза. Более подробно про
операций в родоструктурной форме можно прочитать в{' '} реализацию операций в родоструктурной форме можно прочитать в{' '}
<LinkTopic text='разделе Операции' topic={HelpTopic.RSL_OPERATIONS} /> <LinkTopic text='разделе Операции' topic={HelpTopic.RSL_OPERATIONS} />.
</p> </p>
<p> <p>
Для управления совокупностью синтезов используются{' '} Для управления совокупностью синтезов используются{' '}

View File

@ -9,12 +9,13 @@ export function HelpAccess() {
<p> <p>
Доступ к контенту на Портале может быть ограничен владельцем каждой схемы в рамках <b>политики доступа</b>. Доступ к контенту на Портале может быть ограничен владельцем каждой схемы в рамках <b>политики доступа</b>.
</p> </p>
<ul>
<li> <li>
<IconPublic className='inline-icon icon-green' /> публичная политика не ограничивает чтение схемы <IconPublic className='inline-icon icon-green' /> публичная политика не ограничивает чтение схемы
</li> </li>
<li> <li>
<IconProtected className='inline-icon icon-blue' /> защитная политика запрещает доступ для всех кроме редакторов <IconProtected className='inline-icon icon-blue' /> защитная политика запрещает доступ для всех кроме
и владельца схемы редакторов и владельца схемы
</li> </li>
<li> <li>
<IconPrivate className='inline-icon icon-red' /> личная политика оставляет доступ к схеме только владельцу <IconPrivate className='inline-icon icon-red' /> личная политика оставляет доступ к схеме только владельцу
@ -26,6 +27,7 @@ export function HelpAccess() {
<li> <li>
<IconImmutable className='inline-icon' /> режим защиты от редактирования предохраняет от случайных изменений <IconImmutable className='inline-icon' /> режим защиты от редактирования предохраняет от случайных изменений
</li> </li>
</ul>
</div> </div>
); );
} }

View File

@ -24,6 +24,7 @@ export function HelpExteor() {
</p> </p>
<h2>Основные функции</h2> <h2>Основные функции</h2>
<ul>
<li>Работа с РС-формой системы понятий</li> <li>Работа с РС-формой системы понятий</li>
<li>Автоматическое определение типизации выражений</li> <li>Автоматическое определение типизации выражений</li>
<li>Проверка корректности РС-формы</li> <li>Проверка корректности РС-формы</li>
@ -35,6 +36,7 @@ export function HelpExteor() {
<li>Вычисление объектной интерпретации</li> <li>Вычисление объектной интерпретации</li>
<li>Выгрузка концептуальных схем в Word</li> <li>Выгрузка концептуальных схем в Word</li>
<li>Импорт/экспорт интерпретаций через Excel</li> <li>Импорт/экспорт интерпретаций через Excel</li>
</ul>
</div> </div>
); );
} }

View File

@ -31,6 +31,7 @@ export function HelpInterface() {
</p> </p>
<h2>Навигация и настройки</h2> <h2>Навигация и настройки</h2>
<ul>
<li> <li>
<kbd>Ctrl + клик</kbd> на объект навигации откроет новую вкладку <kbd>Ctrl + клик</kbd> на объект навигации откроет новую вкладку
</li> </li>
@ -46,8 +47,8 @@ export function HelpInterface() {
<IconLogin size='1.25rem' className='inline-icon' /> вход в систему / регистрация нового пользователя <IconLogin size='1.25rem' className='inline-icon' /> вход в систему / регистрация нового пользователя
</li> </li>
<li> <li>
<IconUser2 size='1.25rem' className='inline-icon' /> меню пользователя содержит ряд настроек и переход к профилю <IconUser2 size='1.25rem' className='inline-icon' /> меню пользователя содержит ряд настроек и переход к
пользователя профилю пользователя
</li> </li>
<li> <li>
@ -57,6 +58,7 @@ export function HelpInterface() {
<li> <li>
<IconLogout className='inline-icon' /> выход из системы <IconLogout className='inline-icon' /> выход из системы
</li> </li>
</ul>
<Subtopics headTopic={HelpTopic.INTERFACE} /> <Subtopics headTopic={HelpTopic.INTERFACE} />
</div> </div>

View File

@ -22,6 +22,7 @@ export function HelpMain() {
<details> <details>
<summary className='text-center font-semibold'>Разделы Справки</summary> <summary className='text-center font-semibold'>Разделы Справки</summary>
<ul>
{[ {[
HelpTopic.THESAURUS, HelpTopic.THESAURUS,
HelpTopic.INTERFACE, HelpTopic.INTERFACE,
@ -35,9 +36,11 @@ export function HelpMain() {
].map(topic => ( ].map(topic => (
<TopicItem key={`${prefixes.topic_item}${topic}`} topic={topic} /> <TopicItem key={`${prefixes.topic_item}${topic}`} topic={topic} />
))} ))}
</ul>
</details> </details>
<h2>Лицензирование и раскрытие информации</h2> <h2>Лицензирование и раскрытие информации</h2>
<ul>
<li>Пользователи Портала сохраняют авторские права на создаваемый ими контент</li> <li>Пользователи Портала сохраняют авторские права на создаваемый ими контент</li>
<li> <li>
Политика обработки данных доступна по <LinkTopic text='ссылке' topic={HelpTopic.INFO_PRIVACY} /> Политика обработки данных доступна по <LinkTopic text='ссылке' topic={HelpTopic.INFO_PRIVACY} />
@ -50,6 +53,7 @@ export function HelpMain() {
Данный сайт использует доменное имя и серверные мощности{' '} Данный сайт использует доменное имя и серверные мощности{' '}
<TextURL text='Центра Концепт' href={external_urls.concept} /> <TextURL text='Центра Концепт' href={external_urls.concept} />
</li> </li>
</ul>
<h2>Поддержка</h2> <h2>Поддержка</h2>
<p> <p>

View File

@ -11,6 +11,7 @@ export function HelpVersions() {
<p>После создания версии ее содержание изменить нельзя.</p> <p>После создания версии ее содержание изменить нельзя.</p>
<h2>Действия</h2> <h2>Действия</h2>
<ul>
<li> <li>
<IconShare size='1.25rem' className='inline-icon' /> Поделиться включает версию в ссылку <IconShare size='1.25rem' className='inline-icon' /> Поделиться включает версию в ссылку
</li> </li>
@ -25,6 +26,7 @@ export function HelpVersions() {
<li> <li>
<IconVersions size='1.25rem' className='inline-icon' /> Редактировать атрибуты версий <IconVersions size='1.25rem' className='inline-icon' /> Редактировать атрибуты версий
</li> </li>
</ul>
</div> </div>
); );
} }

View File

@ -17,8 +17,8 @@ export function HelpContributors() {
В списке указан год окончания работ над соответствующим результатом или год публикации соответствующей статьи. В списке указан год окончания работ над соответствующим результатом или год публикации соответствующей статьи.
Курсивом выделены комментарии к значимости указанного результата. Курсивом выделены комментарии к значимости указанного результата.
</p> </p>
<p>Любые добавления и поправки приветствуются.</p> <p>Добавления и корректировки приветствуются.</p>
<div className='flex flex-col gap-3'> <ul className='flex flex-col gap-3'>
<li>1973 Никаноров С.П., Персиц Д.Б. Формальное проектирование целостных СОУ.</li> <li>1973 Никаноров С.П., Персиц Д.Б. Формальное проектирование целостных СОУ.</li>
<li> <li>
19751981 Никаноров С.П., Персиц Д.Б., Айзенштат А.В., Закс Б.А. Экспериментальная система пакетов прикладных 19751981 Никаноров С.П., Персиц Д.Б., Айзенштат А.В., Закс Б.А. Экспериментальная система пакетов прикладных
@ -48,18 +48,18 @@ export function HelpContributors() {
решений. решений.
</li> </li>
<li> <li>
1989 Кучкаров З.А., Остапов А.В. Методические вопросы концептуализации предметных областей,{' '} 1989 Остапов А.В., Кучкаров З.А. Методические вопросы концептуализации предметных областей,{' '}
<i> <i>
как пример одной из работ Остапова, значительно расширившего технику экспликации и практику применения как пример одной из работ Остапова, значительно расширившего технику экспликации и практику применения
"бескванторных" выражений. "бескванторных" выражений.
</i> </i>
</li> </li>
<li> <li>
1990 Никитина Н.К., Постников В.В. Синтаксический анализатор текста рода структуры для МАКС,{' '} 1990 Постников В.В., Никитина Н.К. Синтаксический анализатор текста рода структуры для МАКС,{' '}
<i>являющийся первой попыткой реализовать автоматизированную проверку синтаксиса родов структур.</i> <i>являющийся первой попыткой реализовать автоматизированную проверку синтаксиса родов структур.</i>
</li> </li>
<li> <li>
1993 Костюк А.В., Никитина Н.К., Юдкин Ю.Ю. Программа визуализации М-графов, представляющих родовую структуру. 1993 Юдкин Ю.Ю., Костюк А.В., Никитина Н.К. Программа визуализации М-графов, представляющих родовую структуру.
</li> </li>
<li>1993 Никитина Н.К., Чувашов Е.В. Система проектирования баз данных по их концептуальной модели.</li> <li>1993 Никитина Н.К., Чувашов Е.В. Система проектирования баз данных по их концептуальной модели.</li>
<li> <li>
@ -72,11 +72,11 @@ export function HelpContributors() {
PROLOG-программ, формирующих предметные интерпретации родоструктурных экспликаций Инттеор. PROLOG-программ, формирующих предметные интерпретации родоструктурных экспликаций Инттеор.
</li> </li>
<li> <li>
1994 Кучкаров З.А., Ким В.Л. Разработка родоструктурных конструктов для библиотеки моделей и исследование 1994 Ким В.Л., Кучкаров З.А. Разработка родоструктурных конструктов для библиотеки моделей и исследование
возможностей их развития. возможностей их развития.
</li> </li>
<li> <li>
1994 Коваль А.Г., Воробей П.Н. Редактор Программного комплекса Экстеор 1.5,{' '} 1994 Воробей П.Н., Коваль А.Г. Редактор Программного комплекса Экстеор 1.5,{' '}
<i>упростивший механизм печати экспликаций и улучшивший синтаксический анализ формального выражения.</i> <i>упростивший механизм печати экспликаций и улучшивший синтаксический анализ формального выражения.</i>
</li> </li>
<li> <li>
@ -84,16 +84,17 @@ export function HelpContributors() {
родоструктурного синтеза операционализированных терминальных концептуальных моделей Экстеор 2,{' '} родоструктурного синтеза операционализированных терминальных концептуальных моделей Экстеор 2,{' '}
<i>ставшая первой версией реализации родоструктурного аппарата на C++ под Windows.</i> <i>ставшая первой версией реализации родоструктурного аппарата на C++ под Windows.</i>
</li> </li>
<li> <li>
1996 Никаноров С.П., Никитина Н.К., Климишин В.В. Автоматизированная система "Библиотека концептуальных схем",{' '} 1996 Климишин В.В., Никаноров С.П., Никитина Н.К. Автоматизированная система "Библиотека концептуальных схем",{' '}
<i>впервые определившая паспорт концептуальной схемы.</i> <i>впервые определившая паспорт концептуальной схемы.</i>
</li> </li>
<li> <li>
1997 Никитина Н.К., Юрьев О.И. Система поддержки процессов концептуального анализа и проектирования ПРОКСИМА 1997 Юрьев О.И., Никитина Н.К. Система поддержки процессов концептуального анализа и проектирования ПРОКСИМА
1. 1.
</li> </li>
<li> <li>
1998 Никитина Н.К., Гараева Ю.Р. Синтаксический анализатор выражений на языке родоструктурной экспликации для 1998 Гараева Ю.Р., Никитина Н.К. Синтаксический анализатор выражений на языке родоструктурной экспликации для
ПРОКСИМА 1. ПРОКСИМА 1.
</li> </li>
<li> <li>
@ -101,12 +102,12 @@ export function HelpContributors() {
концептуального проектирования. концептуального проектирования.
</li> </li>
<li> <li>
1999 Кучкаров З.А., Кононенко А.А. Программа преобразования родоструктурного синтеза операционализированных 1999 Кононенко А.А., Кучкаров З.А. Программа преобразования родоструктурного синтеза операционализированных
терминальных концептуальных моделей Экстеор 3,{' '} терминальных концептуальных моделей Экстеор 3,{' '}
<i>впервые включившая операционную схему синтеза (дерево синтеза).</i> <i>впервые включившая операционную схему синтеза (дерево синтеза).</i>
</li> </li>
<li> <li>
1999 Никитина Н.К., Ландин Н.А. Разработка автоматизированной подсистемы, реализующей операции отслоения и 1999 Ландин Н.А., Никитина Н.К. Разработка автоматизированной подсистемы, реализующей операции отслоения и
рассечения над концептуальными схемами. рассечения над концептуальными схемами.
</li> </li>
<li> <li>
@ -122,7 +123,7 @@ export function HelpContributors() {
</i> </i>
</li> </li>
<li> <li>
2000 Кононенко А.А., Майоров В.А. Программа автоматизированной генерации структуры данных и их визуализации по 2000 Майоров В.А., Кононенко А.А. Программа автоматизированной генерации структуры данных и их визуализации по
концептуальной модели БДтеор,{' '} концептуальной модели БДтеор,{' '}
<i> <i>
определившая проблемы интерфейса наполнения концептуальной модели в сложных ступенях и предложившая определившая проблемы интерфейса наполнения концептуальной модели в сложных ступенях и предложившая
@ -136,11 +137,11 @@ export function HelpContributors() {
</li> </li>
<li>2000 Ключников А.В. Эквивалентность теорий родов структур.</li> <li>2000 Ключников А.В. Эквивалентность теорий родов структур.</li>
<li> <li>
2001 Кучкаров З.А., Никитин А.В. Исследование и построение типологии изменений теоретико-множественных 2001 Никитин А.В., Кучкаров З.А. Исследование и построение типологии изменений теоретико-множественных
интерпретаций класса декартового произведения. интерпретаций класса декартового произведения.
</li> </li>
<li> <li>
2001 Кононенко А.А., Майоров В.А. Программа преобразования сети процедур из формата Оргтеор в формат BPWin 2001 Майоров В.А., Кононенко А.А. Программа преобразования сети процедур из формата Оргтеор в формат BPWin
(IDEF0). (IDEF0).
</li> </li>
<li> <li>
@ -158,6 +159,7 @@ export function HelpContributors() {
2003 Юдкин Ю.Ю., Кудюкин Д.А. Разработка и испытание компьютерной программы, формирующей 2003 Юдкин Ю.Ю., Кудюкин Д.А. Разработка и испытание компьютерной программы, формирующей
теоретико-множественную интерпретацию терма частной родоструктурной теории. теоретико-множественную интерпретацию терма частной родоструктурной теории.
</li> </li>
<li> <li>
2004 Кононенко А.А. Генерация кода на языке программирования C++ по тексту концептуальной схемы, 2004 Кононенко А.А. Генерация кода на языке программирования C++ по тексту концептуальной схемы,
эксплицированной в родах структур. эксплицированной в родах структур.
@ -182,25 +184,25 @@ export function HelpContributors() {
2008 Пономарев И.Н. Об эквивалентной представимости рода структуры с помощью заданной типовой характеристики. 2008 Пономарев И.Н. Об эквивалентной представимости рода структуры с помощью заданной типовой характеристики.
</li> </li>
<li> <li>
2010 Кононенко А.А., Грязнов А.Д. Исследование и построение транслятора концептуальной схемы в концептуальную 2010 Грязнов А.Д., Кононенко А.А. Исследование и построение транслятора концептуальной схемы в концептуальную
модель. модель.
</li> </li>
<li>2010 Никаноров С.П. Введение в аппарат ступеней.</li> <li>2010 Никаноров С.П. Введение в аппарат ступеней.</li>
<li> <li>
2012 Кононенко А.А., Елисов Д.Н. Использование механизма XSD-схем для хранения и операционализации 2012 Елисов Д.Н., Кононенко А.А. Использование механизма XSD-схем для хранения и операционализации
концептуальных схем и концептуальных моделей с помощью XML. концептуальных схем и концептуальных моделей с помощью XML.
</li> </li>
<li> <li>
2013 Кононенко А.А., Борисов И.Р. Исследование, разработка и экспериментальная программная реализация операций 2013 Борисов И.Р., Кононенко А.А. Исследование, разработка и экспериментальная программная реализация операций
над концептуальными моделями,{' '} над концептуальными моделями,{' '}
<i> <i>
впервые реализовавшая модуль прямого вычисления интерпретации формального выражения, встроенный в Экстеор впервые реализовавшая модуль прямого вычисления интерпретации формального выражения, встроенный в Экстеор
3.5. 3.5.
</i> </i>
</li> </li>
<li>2013 Пономарев И.Н., Липатов А.А. Операции над родами структур и пример автоматизации их выполнения.</li> <li>2013 Липатов А.А., Пономарев И.Н. Операции над родами структур и пример автоматизации их выполнения.</li>
<li> <li>
2014 Борисов И.Р., Баширов Р.М. Исследования и программная реализации оптимальной структуры данных для 2014 Баширов Р.М., Борисов И.Р. Исследования и программная реализации оптимальной структуры данных для
вычисления интерпретации концептуальных схем. вычисления интерпретации концептуальных схем.
</li> </li>
<li> <li>
@ -220,7 +222,7 @@ export function HelpContributors() {
</li> </li>
<li>2015 Иванов А.Ю. Аппарат ступеней С.П. Никанорова и возможное развитие идей по его использованию.</li> <li>2015 Иванов А.Ю. Аппарат ступеней С.П. Никанорова и возможное развитие идей по его использованию.</li>
<li> <li>
2016 Борисов И.Р., Баширов Р.М. Исследование области компьютерной лингвистики и разработка модулей 2016 Баширов Р.М., Борисов И.Р. Исследование области компьютерной лингвистики и разработка модулей
терминологического контроля в Экстеор 4 и Microsoft Office Word,{' '} терминологического контроля в Экстеор 4 и Microsoft Office Word,{' '}
<i> <i>
являющееся основой библиотеки <TextURL text='cctext' href={external_urls.git_cctext} />. являющееся основой библиотеки <TextURL text='cctext' href={external_urls.git_cctext} />.
@ -239,25 +241,25 @@ export function HelpContributors() {
теории (на примере родственных отношений). теории (на примере родственных отношений).
</li> </li>
<li> <li>
2017 Борисов И.Р., Мурадов А.К. Организация операций над системами понятий посредством графических 2017 Мурадов А.К., Борисов И.Р. Организация операций над системами понятий посредством графических
интерфейсов, <i>заложивший основу для технологии Концепт.Блоки и блока графического синтеза.</i> интерфейсов, <i>заложивший основу для технологии Концепт.Блоки и блока графического синтеза.</i>
</li> </li>
<li> <li>
2018 Борисов И.Р., Князев А.В. Изучение методов концептуальной расчистки, разметки текстов и разработка 2018 Князев А.В., Борисов И.Р. Изучение методов концептуальной расчистки, разметки текстов и разработка
программных средств их автоматизации,{' '} программных средств их автоматизации,{' '}
<i> &mdash; диплом, сформировавший основу для технологий Концепт.Разметка и Концепт.Майнинг.</i> <i> &mdash; диплом, сформировавший основу для технологий Концепт.Разметка и Концепт.Майнинг.</i>
</li> </li>
<li> <li>
2018 Никитин А.В., Болотин П.В. Исследование типологии изменения теоретико-множественной интерпретации класса 2018 Болотин П.В., Никитин А.В. Исследование типологии изменения теоретико-множественной интерпретации класса
множества подмножеств. множества подмножеств.
</li> </li>
<li> <li>
2019 Борисов И.Р., Широкова Л.Р. Исследование возможностей применения методов машинного обучения для решения 2019 Широкова Л.Р., Борисов И.Р. Исследование возможностей применения методов машинного обучения для решения
задач расчистки текстов. Разработка прототипа программного модуля, &mdash;{' '} задач расчистки текстов. Разработка прототипа программного модуля, &mdash;{' '}
<i>первая попытка внедрения технологий ИИ в текстовый модуль.</i> <i>первая попытка внедрения технологий ИИ в текстовый модуль.</i>
</li> </li>
<li> <li>
2020 Борисов И.Р., Пакулина Т.А. Исследование применения методов машинного обучения для выделения именованных 2020 Пакулина Т.А., Борисов И.Р. Исследование применения методов машинного обучения для выделения именованных
сущностей в текстах интервью. Экспериментальная разработка программного модуля расчистки текстов,{' '} сущностей в текстах интервью. Экспериментальная разработка программного модуля расчистки текстов,{' '}
<i>ставшего расширением технологии Концепт.Расчистка.</i> <i>ставшего расширением технологии Концепт.Расчистка.</i>
</li> </li>
@ -266,12 +268,12 @@ export function HelpContributors() {
структур (рекурсивные и императивные выражения, фильтры, ASCII синтаксис). структур (рекурсивные и императивные выражения, фильтры, ASCII синтаксис).
</li> </li>
<li> <li>
2021 Борисов И.Р., Демешко А.Б. Исследование и разработка программного модуля формирования текстов функций на 2021 Демешко А.Б., Борисов И.Р. Исследование и разработка программного модуля формирования текстов функций на
основе концепта функциональная структура,{' '} основе концепта функциональная структура,{' '}
<i>дополнившего текстовый модуль возможностью работы с глагольными формами.</i> <i>дополнившего текстовый модуль возможностью работы с глагольными формами.</i>
</li> </li>
<li> <li>
2023 Борисов И.Р., Тулисов А.В. Разработка инструмента экспликации концептуальных схем в родоструктурной форме 2023 Тулисов А.В., Борисов И.Р. Разработка инструмента экспликации концептуальных схем в родоструктурной форме
через веб-интерфейс, &mdash; <i>разработка прототипа интерфейса КонцептПортал.</i> через веб-интерфейс, &mdash; <i>разработка прототипа интерфейса КонцептПортал.</i>
</li> </li>
<li> <li>
@ -283,11 +285,16 @@ export function HelpContributors() {
</i> </i>
</li> </li>
<li> <li>
2024 Борисов И.Р., Хаданович Б.А. Исследование механизмов проведения сквозных изменений в операционной схеме 2024 Хаданович Б.А., Борисов И.Р. Исследование механизмов проведения сквозных изменений в операционной схеме
синтеза. Разработка прототипа веб-интерфейса синтеза концептуальных схем. синтеза. Разработка прототипа веб-интерфейса синтеза концептуальных схем.
<i> Прототип графического интерфейса для синтеза концептуальных схем.</i> <i> Прототип графического интерфейса для синтеза концептуальных схем.</i>
</li> </li>
</div> <li>
2024 Викентьев М.И., Борисов И.Р. Исследование использования современных web-интерфейсов для визуализации
отношений для применения в рамках концептуального синтеза.{' '}
<i> Визуализации смешанных представлений концептуальной схемы.</i>
</li>
</ul>
</div> </div>
); );
} }

View File

@ -28,6 +28,7 @@ export function HelpRules() {
</p> </p>
<h2>Ожидаемое поведение</h2> <h2>Ожидаемое поведение</h2>
<ul>
<li>взаимное уважением, поддержка в отношениях с участниками Портала.</li> <li>взаимное уважением, поддержка в отношениях с участниками Портала.</li>
<li> <li>
пожелания по доработке, найденные ошибки и иные предложения следует направлять по адресу email:{' '} пожелания по доработке, найденные ошибки и иные предложения следует направлять по адресу email:{' '}
@ -50,6 +51,7 @@ export function HelpRules() {
осложнение создания (и/или поддержания) контента, созданного другими участниками. осложнение создания (и/или поддержания) контента, созданного другими участниками.
</li> </li>
<li>нарушение работоспособности Портала, в том числе путем использования уязвимостей и ошибок в коде.</li> <li>нарушение работоспособности Портала, в том числе путем использования уязвимостей и ошибок в коде.</li>
</ul>
</div> </div>
); );
} }

View File

@ -19,7 +19,7 @@ export function HelpRSLangOperations() {
<h2> <h2>
<IconSortList size='1.25rem' className='inline-icon' /> Упорядочение <IconSortList size='1.25rem' className='inline-icon' /> Упорядочение
</h2> </h2>
<p> <ul>
Упорядочение списка конституент по следующим правилам Упорядочение списка конституент по следующим правилам
<li>базисные и константные множества объявляются первыми</li> <li>базисные и константные множества объявляются первыми</li>
<li> <li>
@ -32,7 +32,7 @@ export function HelpRSLangOperations() {
<LinkTopic text='порожденные' topic={HelpTopic.CC_RELATIONS} /> конституенты следуют сразу за исходной <LinkTopic text='порожденные' topic={HelpTopic.CC_RELATIONS} /> конституенты следуют сразу за исходной
</li> </li>
<li>максимальное сохранение исходного порядка при выполнении предыдущих правил</li> <li>максимальное сохранение исходного порядка при выполнении предыдущих правил</li>
</p> </ul>
<h2> <h2>
<IconGenerateNames size='1.25rem' className='inline-icon' /> Порядковые имена <IconGenerateNames size='1.25rem' className='inline-icon' /> Порядковые имена

View File

@ -10,6 +10,7 @@ export function HelpRSLangTemplates() {
Источником шаблонов является <b>Банк выражений</b>, содержащий параметризованные понятия и утверждения, Источником шаблонов является <b>Банк выражений</b>, содержащий параметризованные понятия и утверждения,
сгруппированные по разделам сгруппированные по разделам
</p> </p>
<ul>
<li>Сначала выбирается шаблон выражения (вкладка Шаблон)</li> <li>Сначала выбирается шаблон выражения (вкладка Шаблон)</li>
<li> <li>
Далее для аргументов можно зафиксировать значения, выбрав из конституент текущей схемы или указав выражения Далее для аргументов можно зафиксировать значения, выбрав из конституент текущей схемы или указав выражения
@ -21,6 +22,7 @@ export function HelpRSLangTemplates() {
<li> <li>
Кнопка <b>Создать</b> инициирует добавление выбранной конституенты в схему Кнопка <b>Создать</b> инициирует добавление выбранной конституенты в схему
</li> </li>
</ul>
</div> </div>
); );
} }

View File

@ -2,7 +2,7 @@ export function HelpRSLangTypes() {
return ( return (
<div> <div>
<h1>Типизация</h1> <h1>Типизация</h1>
<p> <ul>
Родоструктурное выражение <code>ξ</code> обладает типизацией (структурой), если выполнено ξH, Родоструктурное выражение <code>ξ</code> обладает типизацией (структурой), если выполнено ξH,
<br /> <br />
где <code>H</code> корректное выражение <b>ступени</b>, задаваемой следующими правилами: где <code>H</code> корректное выражение <b>ступени</b>, задаваемой следующими правилами:
@ -18,7 +18,7 @@ export function HelpRSLangTypes() {
<li> <li>
<code>(H)</code> ступень, называемая <b>множеством</b>. <code>(H)</code> ступень, называемая <b>множеством</b>.
</li> </li>
</p> </ul>
<p>Пустое множество имеет типизацию (R0) множество с произвольной структурой элемента</p> <p>Пустое множество имеет типизацию (R0) множество с произвольной структурой элемента</p>
<p> <p>
Для обобщения понятия типизация на логические и параметризованные выражения вводится ряд дополнительных Для обобщения понятия типизация на логические и параметризованные выражения вводится ряд дополнительных

View File

@ -4,11 +4,15 @@ export function HelpFormulaTree() {
<h1>Дерево разбора выражения</h1> <h1>Дерево разбора выражения</h1>
<p>Дерево получено путем семантических преобразований дерева синтаксического разбора.</p> <p>Дерево получено путем семантических преобразований дерева синтаксического разбора.</p>
<p>Оно отражает структуру грамматически корректного выражения языка родов структур.</p> <p>Оно отражает структуру грамматически корректного выражения языка родов структур.</p>
<ul>
<li>Порядок узлов в рамках одного уровня может отличаться от их порядка в выражении</li> <li>Порядок узлов в рамках одного уровня может отличаться от их порядка в выражении</li>
<li>При наведении курсора на узел в тексте выделяется соответствующий ему фрагмент</li> <li>При наведении курсора на узел в тексте выделяется соответствующий ему фрагмент</li>
<li>Текст в узле дерева соответствует элементу языка</li> <li>Текст в узле дерева соответствует элементу языка</li>
</ul>
<h2>Виды узлов</h2> <h2>Виды узлов</h2>
<ul>
<li> <li>
<span className='bg-(--acc-bg-green)'>объявление идентификатора</span> <span className='bg-(--acc-bg-green)'>объявление идентификатора</span>
</li> </li>
@ -27,6 +31,14 @@ export function HelpFormulaTree() {
<li> <li>
<span className='bg-secondary'>составные выражения</span> <span className='bg-secondary'>составные выражения</span>
</li> </li>
</ul>
<h2>Команды</h2>
<ul>
<li>
<kbd>Space</kbd> перемещение экрана
</li>
</ul>
</div> </div>
); );
} }

View File

@ -31,6 +31,7 @@ export function HelpLibrary() {
<LinkTopic text='операционные схемы синтеза' topic={HelpTopic.CC_OSS} /> (ОСС). <LinkTopic text='операционные схемы синтеза' topic={HelpTopic.CC_OSS} /> (ОСС).
</p> </p>
<ul>
<li> <li>
<span className='text-(--acc-fg-green)'>зеленым текстом</span> выделены ОСС <span className='text-(--acc-fg-green)'>зеленым текстом</span> выделены ОСС
</li> </li>
@ -63,8 +64,10 @@ export function HelpLibrary() {
<li> <li>
<IconFolderTree size='1rem' className='inline-icon' /> переключение между Проводник и Поиск <IconFolderTree size='1rem' className='inline-icon' /> переключение между Проводник и Поиск
</li> </li>
</ul>
<h2>Режим: Проводник</h2> <h2>Режим: Проводник</h2>
<ul>
<li> <li>
<IconFolderEdit size='1rem' className='inline-icon' /> переименовать выбранную <IconFolderEdit size='1rem' className='inline-icon' /> переименовать выбранную
</li> </li>
@ -95,6 +98,7 @@ export function HelpLibrary() {
<li> <li>
<IconFolderOpened size='1rem' className='inline-icon icon-green' /> развернутая папка <IconFolderOpened size='1rem' className='inline-icon icon-green' /> развернутая папка
</li> </li>
</ul>
</div> </div>
); );
} }

View File

@ -32,7 +32,8 @@ export function HelpOssGraph() {
<h1 className='sm:pr-24'>Граф синтеза</h1> <h1 className='sm:pr-24'>Граф синтеза</h1>
<div className='flex flex-col sm:flex-row'> <div className='flex flex-col sm:flex-row'>
<div className='sm:w-56'> <div className='sm:w-56'>
<h1>Настройка графа</h1> <h2>Настройка графа</h2>
<ul>
<li> <li>
<IconReset className='inline-icon' /> Сбросить изменения <IconReset className='inline-icon' /> Сбросить изменения
</li> </li>
@ -64,12 +65,14 @@ export function HelpOssGraph() {
<li> <li>
черта слева - КС <LinkTopic text='внешняя' topic={HelpTopic.CC_OSS} /> черта слева - КС <LinkTopic text='внешняя' topic={HelpTopic.CC_OSS} />
</li> </li>
</ul>
</div> </div>
<Divider vertical margins='mx-3 mt-3' className='hidden sm:block' /> <Divider vertical margins='mx-3 mt-3' className='hidden sm:block' />
<div className='sm:w-84'> <div className='sm:w-84'>
<h1>Изменение узлов</h1> <h2>Изменение узлов</h2>
<ul>
<li> <li>
<kbd>Клик</kbd> на операцию выделение <kbd>Клик</kbd> на операцию выделение
</li> </li>
@ -91,14 +94,16 @@ export function HelpOssGraph() {
<li> <li>
<IconDestroy className='inline-icon icon-red' /> <kbd>Delete</kbd> удалить выбранные <IconDestroy className='inline-icon icon-red' /> <kbd>Delete</kbd> удалить выбранные
</li> </li>
</ul>
</div> </div>
</div> </div>
<Divider margins='my-3' className='hidden sm:block' /> <Divider margins='my-2' className='hidden sm:block' />
<div className='flex flex-col-reverse mb-3 sm:flex-row'> <div className='flex flex-col-reverse mb-3 sm:flex-row'>
<div className='sm:w-56'> <div className='sm:w-56'>
<h1>Общие</h1> <h2>Общие</h2>
<ul>
<li> <li>
<IconSave className='inline-icon' /> Сохранить положения <IconSave className='inline-icon' /> Сохранить положения
</li> </li>
@ -108,12 +113,14 @@ export function HelpOssGraph() {
<li> <li>
<kbd>Shift</kbd> перемещение выделенных элементов в границах родителя <kbd>Shift</kbd> перемещение выделенных элементов в границах родителя
</li> </li>
</ul>
</div> </div>
<Divider vertical margins='mx-3' className='hidden sm:block' /> <Divider vertical margins='mx-3' className='hidden sm:block' />
<div className='dense w-84'> <div className='dense w-84'>
<h1>Контекстное меню</h1> <h2>Контекстное меню</h2>
<ul>
<li> <li>
<IconRSForm className='inline-icon icon-green' /> Статус связанной{' '} <IconRSForm className='inline-icon icon-green' /> Статус связанной{' '}
<LinkTopic text='КС' topic={HelpTopic.CC_SYSTEM} /> <LinkTopic text='КС' topic={HelpTopic.CC_SYSTEM} />
@ -135,6 +142,7 @@ export function HelpOssGraph() {
<li> <li>
<IconExecute className='inline-icon icon-green' /> Активировать операцию <IconExecute className='inline-icon icon-green' /> Активировать операцию
</li> </li>
</ul>
</div> </div>
</div> </div>
</div> </div>

View File

@ -12,6 +12,7 @@ export function HelpRelocateCst() {
другую КС (целевую) в рамках одной <IconOSS size='1rem' className='inline-icon' />{' '} другую КС (целевую) в рамках одной <IconOSS size='1rem' className='inline-icon' />{' '}
<LinkTopic text='операционной схемы синтеза' topic={HelpTopic.CC_OSS} />. <LinkTopic text='операционной схемы синтеза' topic={HelpTopic.CC_OSS} />.
</p> </p>
<ul>
<li> <li>
только для <IconPredecessor size='1rem' className='inline-icon' /> собственных конституент источника только для <IconPredecessor size='1rem' className='inline-icon' /> собственных конституент источника
</li> </li>
@ -19,16 +20,21 @@ export function HelpRelocateCst() {
<IconMoveUp size='1rem' className='inline-icon' /> <IconMoveUp size='1rem' className='inline-icon' />
<IconMoveDown size='1rem' className='inline-icon' /> направление переноса - вверх или вниз по дереву синтеза <IconMoveDown size='1rem' className='inline-icon' /> направление переноса - вверх или вниз по дереву синтеза
</li> </li>
</ul>
<h2>Перенос вверх</h2> <h2>Перенос вверх</h2>
<ul>
<li>выбранные конституенты становятся наследованными, а их копии добавляются в целевую КС</li> <li>выбранные конституенты становятся наследованными, а их копии добавляются в целевую КС</li>
<li>нельзя выбирать конституенты, зависящие от конституент других концептуальных схем</li> <li>нельзя выбирать конституенты, зависящие от конституент других концептуальных схем</li>
</ul>
<h2>Перенос вниз</h2> <h2>Перенос вниз</h2>
<ul>
<li> <li>
выбранные конституенты становятся собственными конституентами целевой КС, удаляются из исходной КС и ее выбранные конституенты становятся собственными конституентами целевой КС, удаляются из исходной КС и ее
наследников наследников
</li> </li>
</ul>
</div> </div>
); );
} }

View File

@ -30,6 +30,7 @@ export function HelpRSCard() {
</p> </p>
<h2>Управление</h2> <h2>Управление</h2>
<ul>
<li> <li>
<IconOSS className='inline-icon' /> переход к связанной <LinkTopic text='ОСС' topic={HelpTopic.CC_OSS} /> <IconOSS className='inline-icon' /> переход к связанной <LinkTopic text='ОСС' topic={HelpTopic.CC_OSS} />
</li> </li>
@ -57,6 +58,7 @@ export function HelpRSCard() {
<li> <li>
<IconDestroy className='inline-icon icon-red' /> Удалить полностью удаляет схему из базы Портала <IconDestroy className='inline-icon icon-red' /> Удалить полностью удаляет схему из базы Портала
</li> </li>
</ul>
</div> </div>
); );
} }

View File

@ -27,7 +27,9 @@ export function HelpRSEditor() {
<div className='dense'> <div className='dense'>
<h1>Редактор конституенты</h1> <h1>Редактор конституенты</h1>
<div className='flex flex-col sm:flex-row sm:gap-3'> <div className='flex flex-col sm:flex-row sm:gap-3'>
<div className='flex flex-col'> <div>
<h2>Команды</h2>
<ul>
<li> <li>
<IconOSS className='inline-icon' /> переход к <LinkTopic text='ОСС' topic={HelpTopic.CC_OSS} /> <IconOSS className='inline-icon' /> переход к <LinkTopic text='ОСС' topic={HelpTopic.CC_OSS} />
</li> </li>
@ -52,10 +54,12 @@ export function HelpRSEditor() {
<li> <li>
<IconDestroy className='inline-icon icon-red' /> удалить <IconDestroy className='inline-icon icon-red' /> удалить
</li> </li>
</ul>
</div> </div>
<div className='flex flex-col'> <div>
<h2>Список конституент</h2> <h2>Список конституент</h2>
<ul>
<li> <li>
<IconMoveDown className='inline-icon' /> <IconMoveDown className='inline-icon' />
<IconMoveUp className='inline-icon' /> <kbd>Alt + вверх/вниз</kbd> <IconMoveUp className='inline-icon' /> <kbd>Alt + вверх/вниз</kbd>
@ -80,10 +84,12 @@ export function HelpRSEditor() {
<LinkTopic text='порожденные' topic={HelpTopic.CC_RELATIONS} /> текущей <LinkTopic text='порожденные' topic={HelpTopic.CC_RELATIONS} /> текущей
</span> </span>
</li> </li>
</ul>
</div> </div>
</div> </div>
<h2>Формальное определение</h2> <h2>Формальное определение</h2>
<ul>
<li> <li>
<IconStatusOK className='inline-icon' /> индикатор статуса определения сверху <IconStatusOK className='inline-icon' /> индикатор статуса определения сверху
</li> </li>
@ -101,15 +107,19 @@ export function HelpRSEditor() {
<li> <li>
<kbd>Ctrl + Пробел</kbd> вставка незанятого имени / замена проекции <kbd>Ctrl + Пробел</kbd> вставка незанятого имени / замена проекции
</li> </li>
</ul>
<h2>Термин и Текстовое определение</h2> <h2>Термин и Текстовое определение</h2>
<ul>
<li> <li>
<IconEdit className='inline-icon' /> редактирование <LinkTopic text='Имени' topic={HelpTopic.CC_CONSTITUENTA} />{' '} <IconEdit className='inline-icon' /> редактирование{' '}
/ <LinkTopic text='Термина' topic={HelpTopic.CC_CONSTITUENTA} /> <LinkTopic text='Имени' topic={HelpTopic.CC_CONSTITUENTA} /> /{' '}
<LinkTopic text='Термина' topic={HelpTopic.CC_CONSTITUENTA} />
</li> </li>
<li> <li>
<kbd>Ctrl + Пробел</kbd> открывает редактирование отсылок <kbd>Ctrl + Пробел</kbd> открывает редактирование отсылок
</li> </li>
</ul>
</div> </div>
); );
} }

View File

@ -30,7 +30,8 @@ export function HelpRSGraphTerm() {
<h1>Граф термов</h1> <h1>Граф термов</h1>
<div className='flex flex-col sm:flex-row'> <div className='flex flex-col sm:flex-row'>
<div className='sm:w-56'> <div className='sm:w-56'>
<h1>Настройка графа</h1> <h2>Настройка графа</h2>
<ul>
<li>Цвет покраска узлов</li> <li>Цвет покраска узлов</li>
<li> <li>
<IconText className='inline-icon' /> Отображение текста <IconText className='inline-icon' /> Отображение текста
@ -41,12 +42,14 @@ export function HelpRSGraphTerm() {
<li> <li>
<IconRotate3D className='inline-icon' /> Вращение 3D <IconRotate3D className='inline-icon' /> Вращение 3D
</li> </li>
</ul>
</div> </div>
<Divider vertical margins='mx-3 mt-3' className='hidden sm:block' /> <Divider vertical margins='mx-3 mt-3' className='hidden sm:block' />
<div className='sm:w-84'> <div className='sm:w-84'>
<h1>Изменение узлов</h1> <h2>Изменение узлов</h2>
<ul>
<li>Клик на узел выделение</li> <li>Клик на узел выделение</li>
<li>Левый клик выбор фокус-конституенты</li> <li>Левый клик выбор фокус-конституенты</li>
<li> <li>
@ -61,6 +64,7 @@ export function HelpRSGraphTerm() {
<li> <li>
<IconNewItem className='inline-icon icon-green' /> Новая со ссылками на выделенные <IconNewItem className='inline-icon icon-green' /> Новая со ссылками на выделенные
</li> </li>
</ul>
</div> </div>
</div> </div>
@ -68,7 +72,11 @@ export function HelpRSGraphTerm() {
<div className='flex flex-col-reverse mb-3 sm:flex-row'> <div className='flex flex-col-reverse mb-3 sm:flex-row'>
<div className='sm:w-56'> <div className='sm:w-56'>
<h1>Общие</h1> <h2>Общие</h2>
<ul>
<li>
<kbd>Space</kbd> перемещение экрана
</li>
<li> <li>
<IconOSS className='inline-icon' /> переход к связанной <LinkTopic text='ОСС' topic={HelpTopic.CC_OSS} /> <IconOSS className='inline-icon' /> переход к связанной <LinkTopic text='ОСС' topic={HelpTopic.CC_OSS} />
</li> </li>
@ -85,12 +93,14 @@ export function HelpRSGraphTerm() {
<IconTypeGraph className='inline-icon' /> Открыть{' '} <IconTypeGraph className='inline-icon' /> Открыть{' '}
<LinkTopic text='граф ступеней' topic={HelpTopic.UI_TYPE_GRAPH} /> <LinkTopic text='граф ступеней' topic={HelpTopic.UI_TYPE_GRAPH} />
</li> </li>
</ul>
</div> </div>
<Divider vertical margins='mx-3' className='hidden sm:block' /> <Divider vertical margins='mx-3' className='hidden sm:block' />
<div className='dense w-84'> <div className='dense w-84'>
<h1>Выделение</h1> <h2>Выделение</h2>
<ul>
<li> <li>
<IconGraphCollapse className='inline-icon' /> все влияющие <IconGraphCollapse className='inline-icon' /> все влияющие
</li> </li>
@ -113,6 +123,7 @@ export function HelpRSGraphTerm() {
<IconPredecessor className='inline-icon' /> выделить{' '} <IconPredecessor className='inline-icon' /> выделить{' '}
<LinkTopic text='собственные' topic={HelpTopic.CC_PROPAGATION} /> <LinkTopic text='собственные' topic={HelpTopic.CC_PROPAGATION} />
</li> </li>
</ul>
</div> </div>
</div> </div>
</div> </div>

View File

@ -19,6 +19,7 @@ export function HelpRSList() {
return ( return (
<div className='dense'> <div className='dense'>
<h1>Список конституент</h1> <h1>Список конституент</h1>
<ul>
<li> <li>
<IconAlias className='inline-icon' /> <IconAlias className='inline-icon' />
Конституенты обладают уникальным <LinkTopic text='Именем' topic={HelpTopic.CC_CONSTITUENTA} /> Конституенты обладают уникальным <LinkTopic text='Именем' topic={HelpTopic.CC_CONSTITUENTA} />
@ -27,8 +28,10 @@ export function HelpRSList() {
<li> <li>
пунктиром отображаются <LinkTopic text='наследованные' topic={HelpTopic.CC_OSS} /> конституенты пунктиром отображаются <LinkTopic text='наследованные' topic={HelpTopic.CC_OSS} /> конституенты
</li> </li>
</ul>
<h2>Управление списком</h2> <h2>Управление списком</h2>
<ul>
<li> <li>
<IconOSS className='inline-icon' /> переход к связанной <LinkTopic text='ОСС' topic={HelpTopic.CC_OSS} /> <IconOSS className='inline-icon' /> переход к связанной <LinkTopic text='ОСС' topic={HelpTopic.CC_OSS} />
</li> </li>
@ -62,6 +65,7 @@ export function HelpRSList() {
<li> <li>
<IconDestroy className='inline-icon icon-red' /> удаление выделенных: <kbd>Delete</kbd> <IconDestroy className='inline-icon icon-red' /> удаление выделенных: <kbd>Delete</kbd>
</li> </li>
</ul>
<Divider margins='my-2' /> <Divider margins='my-2' />

View File

@ -30,6 +30,7 @@ export function HelpRSMenu() {
</p> </p>
<h2>Вкладки</h2> <h2>Вкладки</h2>
<ul>
<li> <li>
<LinkTopic text='Карточка' topic={HelpTopic.UI_RS_CARD} /> редактирование атрибутов схемы и версии <LinkTopic text='Карточка' topic={HelpTopic.UI_RS_CARD} /> редактирование атрибутов схемы и версии
</li> </li>
@ -41,12 +42,15 @@ export function HelpRSMenu() {
<LinkTopic text='Конституенты' topic={HelpTopic.CC_CONSTITUENTA} /> <LinkTopic text='Конституенты' topic={HelpTopic.CC_CONSTITUENTA} />
</li> </li>
<li> <li>
<LinkTopic text='Граф термов' topic={HelpTopic.UI_GRAPH_TERM} /> графическое представление связей конституент <LinkTopic text='Граф термов' topic={HelpTopic.UI_GRAPH_TERM} /> графическое представление связей
конституент
</li> </li>
</ul>
<div className='flex my-3'> <div className='flex my-3'>
<div> <div>
<h2>Меню схемы</h2> <h2>Меню схемы</h2>
<ul>
<li> <li>
<IconMenu size='1.25rem' className='inline-icon' /> Меню схемы выпадающее меню с общими функциями <IconMenu size='1.25rem' className='inline-icon' /> Меню схемы выпадающее меню с общими функциями
</li> </li>
@ -71,18 +75,21 @@ export function HelpRSMenu() {
<li> <li>
<IconDestroy className='inline-icon icon-red' /> Удалить полностью удаляет схему из базы Портала <IconDestroy className='inline-icon icon-red' /> Удалить полностью удаляет схему из базы Портала
</li> </li>
</ul>
</div> </div>
<Divider vertical margins='mx-3' /> <Divider vertical margins='mx-3' />
<div className='w-72'> <div className='w-72'>
<h2>Режимы работы</h2> <h2>Режимы работы</h2>
<ul>
<li> <li>
<IconAlert size='1.25rem' className='inline-icon icon-red' /> работа в анонимном режиме. Переход на страницу <IconAlert size='1.25rem' className='inline-icon icon-red' /> работа в анонимном режиме. Переход на
логина страницу логина
</li> </li>
<li> <li>
<IconArchive size='1.25rem' className='inline-icon' /> просмотр архивной версии. Переход к актуальной версии <IconArchive size='1.25rem' className='inline-icon' /> просмотр архивной версии. Переход к актуальной
версии
</li> </li>
<li> <li>
<IconReader size='1.25rem' className='inline-icon' /> режим Читатель <IconReader size='1.25rem' className='inline-icon' /> режим Читатель
@ -96,6 +103,7 @@ export function HelpRSMenu() {
<li> <li>
<IconAdmin size='1.25rem' className='inline-icon' /> режим Администратор <IconAdmin size='1.25rem' className='inline-icon' /> режим Администратор
</li> </li>
</ul>
</div> </div>
</div> </div>

View File

@ -5,6 +5,7 @@ export function HelpSubstitutions() {
<p>Пара отождествлений, обозначает замену вхождений одной конституенты на другую.</p> <p>Пара отождествлений, обозначает замену вхождений одной конституенты на другую.</p>
<p> <p>
Таблица отождествлений накладывает следующие ограничения: Таблица отождествлений накладывает следующие ограничения:
<ul>
<li>конституента может быть удаляемой только в одном отождествлении</li> <li>конституента может быть удаляемой только в одном отождествлении</li>
<li>удаляемые конституенты не могут быть замещающими в отождествлениях</li> <li>удаляемые конституенты не могут быть замещающими в отождествлениях</li>
<li>базисные множества могут замещать только другие базисные множества</li> <li>базисные множества могут замещать только другие базисные множества</li>
@ -15,6 +16,7 @@ export function HelpSubstitutions() {
</li> </li>
<li>логические выражения могут замещать только другие логические выражения</li> <li>логические выражения могут замещать только другие логические выражения</li>
<li>при отождествлении параметризованных конституент количество и типизации аргументов должно совпадать</li> <li>при отождествлении параметризованных конституент количество и типизации аргументов должно совпадать</li>
</ul>
</p> </p>
</div> </div>
); );

View File

@ -7,17 +7,23 @@ export function HelpTypeGraph() {
<h1>Граф ступеней</h1> <h1>Граф ступеней</h1>
<p> <p>
Граф связей между ступенями, используемыми в данном выражении или{' '} Граф связей между ступенями, используемыми в данном выражении или{' '}
<LinkTopic text='КС' topic={HelpTopic.CC_OSS} />. Исторически отображался в форме мультиграфа (М-граф). В <LinkTopic text='КС' topic={HelpTopic.CC_OSS} />.<br />
Портале кратные ребра представлены перечислением индексов компонент произведения. Исторически отображался в форме мультиграфа (М-граф).
<br />
Кратные ребра представлены перечислением индексов компонент произведения.
</p> </p>
<ul>
<li>ребра без надписей означают взятие булеана</li> <li>ребра без надписей означают взятие булеана</li>
<li>цифры на ребрах означают номера компонент декартова произведения</li> <li>цифры на ребрах означают номера компонент декартова произведения</li>
<li>цифры на узлах означают количество конституент в данной ступени</li> <li>цифры на узлах означают количество конституент в данной ступени</li>
<li>основаниями дерева являются ступени базисных, константных множеств</li> <li>основаниями дерева являются ступени базисных, константных множеств</li>
<li>ступень терм-функции - произведение ступеней результата и аргументов</li> <li>ступень терм-функции - произведение ступеней результата и аргументов</li>
<li>ступень предикат-функции - произведение ступеней аргументов</li> <li>ступень предикат-функции - произведение ступеней аргументов</li>
</ul>
<h2>Виды узлов</h2> <h2>Цвета узлов</h2>
<ul>
<li> <li>
<span className='bg-secondary'>ступень-основание</span> <span className='bg-secondary'>ступень-основание</span>
</li> </li>
@ -27,6 +33,14 @@ export function HelpTypeGraph() {
<li> <li>
<span className='bg-accent-orange'>ступень декартова произведения</span> <span className='bg-accent-orange'>ступень декартова произведения</span>
</li> </li>
</ul>
<h2>Команды</h2>
<ul>
<li>
<kbd>Space</kbd> перемещение экрана
</li>
</ul>
</div> </div>
); );
} }

View File

@ -10,12 +10,13 @@ export const useRenameLocation = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [libraryApi.baseKey, 'rename-location'], mutationKey: [libraryApi.baseKey, 'rename-location'],
mutationFn: libraryApi.renameLocation, mutationFn: libraryApi.renameLocation,
onSuccess: () => onSuccess: async () => {
Promise.allSettled([ await Promise.allSettled([
client.invalidateQueries({ queryKey: [KEYS.library] }), client.invalidateQueries({ queryKey: [KEYS.library] }),
client.invalidateQueries({ queryKey: [KEYS.rsform] }), client.invalidateQueries({ queryKey: [KEYS.rsform] }),
client.invalidateQueries({ queryKey: [KEYS.oss] }) client.invalidateQueries({ queryKey: [KEYS.oss] })
]), ]);
},
onError: () => client.invalidateQueries() onError: () => client.invalidateQueries()
}); });
return { return {

View File

@ -16,12 +16,12 @@ export const useSetAccessPolicy = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'set-location'], mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'set-location'],
mutationFn: libraryApi.setAccessPolicy, mutationFn: libraryApi.setAccessPolicy,
onSuccess: (_, variables) => { onSuccess: async (_, variables) => {
const ossKey = KEYS.composite.ossItem({ itemID: variables.itemID }); const ossKey = KEYS.composite.ossItem({ itemID: variables.itemID });
const ossData: IOperationSchemaDTO | undefined = client.getQueryData(ossKey); const ossData: IOperationSchemaDTO | undefined = client.getQueryData(ossKey);
if (ossData) { if (ossData) {
client.setQueryData(ossKey, { ...ossData, access_policy: variables.policy }); client.setQueryData(ossKey, { ...ossData, access_policy: variables.policy });
return Promise.allSettled([ await Promise.allSettled([
client.invalidateQueries({ queryKey: KEYS.composite.libraryList }), client.invalidateQueries({ queryKey: KEYS.composite.libraryList }),
...ossData.operations ...ossData.operations
.map(item => { .map(item => {
@ -33,6 +33,7 @@ export const useSetAccessPolicy = () => {
}) })
.filter(item => !!item) .filter(item => !!item)
]); ]);
return;
} }
const rsKey = KEYS.composite.rsItem({ itemID: variables.itemID }); const rsKey = KEYS.composite.rsItem({ itemID: variables.itemID });

View File

@ -12,12 +12,12 @@ export const useSetEditors = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'set-location'], mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'set-location'],
mutationFn: libraryApi.setEditors, mutationFn: libraryApi.setEditors,
onSuccess: (_, variables) => { onSuccess: async (_, variables) => {
const ossKey = KEYS.composite.ossItem({ itemID: variables.itemID }); const ossKey = KEYS.composite.ossItem({ itemID: variables.itemID });
const ossData: IOperationSchemaDTO | undefined = client.getQueryData(ossKey); const ossData: IOperationSchemaDTO | undefined = client.getQueryData(ossKey);
if (ossData) { if (ossData) {
client.setQueryData(ossKey, { ...ossData, editors: variables.editors }); client.setQueryData(ossKey, { ...ossData, editors: variables.editors });
return Promise.allSettled( await Promise.allSettled(
ossData.operations ossData.operations
.map(item => { .map(item => {
if (!item.result) { if (!item.result) {
@ -28,6 +28,7 @@ export const useSetEditors = () => {
}) })
.filter(item => !!item) .filter(item => !!item)
); );
return;
} }
const rsKey = KEYS.composite.rsItem({ itemID: variables.itemID }); const rsKey = KEYS.composite.rsItem({ itemID: variables.itemID });

View File

@ -16,12 +16,12 @@ export const useSetLocation = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'set-location'], mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'set-location'],
mutationFn: libraryApi.setLocation, mutationFn: libraryApi.setLocation,
onSuccess: (_, variables) => { onSuccess: async (_, variables) => {
const ossKey = KEYS.composite.ossItem({ itemID: variables.itemID }); const ossKey = KEYS.composite.ossItem({ itemID: variables.itemID });
const ossData: IOperationSchemaDTO | undefined = client.getQueryData(ossKey); const ossData: IOperationSchemaDTO | undefined = client.getQueryData(ossKey);
if (ossData) { if (ossData) {
client.setQueryData(ossKey, { ...ossData, location: variables.location }); client.setQueryData(ossKey, { ...ossData, location: variables.location });
return Promise.allSettled([ await Promise.allSettled([
client.invalidateQueries({ queryKey: libraryApi.libraryListKey }), client.invalidateQueries({ queryKey: libraryApi.libraryListKey }),
...ossData.operations ...ossData.operations
.map(item => { .map(item => {
@ -33,6 +33,7 @@ export const useSetLocation = () => {
}) })
.filter(item => !!item) .filter(item => !!item)
]); ]);
return;
} }
const rsKey = KEYS.composite.rsItem({ itemID: variables.itemID }); const rsKey = KEYS.composite.rsItem({ itemID: variables.itemID });

View File

@ -16,12 +16,12 @@ export const useSetOwner = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'set-owner'], mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'set-owner'],
mutationFn: libraryApi.setOwner, mutationFn: libraryApi.setOwner,
onSuccess: (_, variables) => { onSuccess: async (_, variables) => {
const ossKey = KEYS.composite.ossItem({ itemID: variables.itemID }); const ossKey = KEYS.composite.ossItem({ itemID: variables.itemID });
const ossData: IOperationSchemaDTO | undefined = client.getQueryData(ossKey); const ossData: IOperationSchemaDTO | undefined = client.getQueryData(ossKey);
if (ossData) { if (ossData) {
client.setQueryData(ossKey, { ...ossData, owner: variables.owner }); client.setQueryData(ossKey, { ...ossData, owner: variables.owner });
return Promise.allSettled([ await Promise.allSettled([
client.invalidateQueries({ queryKey: libraryApi.libraryListKey }), client.invalidateQueries({ queryKey: libraryApi.libraryListKey }),
...ossData.operations ...ossData.operations
.map(item => { .map(item => {

View File

@ -16,7 +16,7 @@ export const useUpdateItem = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'update-item'], mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'update-item'],
mutationFn: libraryApi.updateItem, mutationFn: libraryApi.updateItem,
onSuccess: (data: ILibraryItem) => { onSuccess: async (data: ILibraryItem) => {
const itemKey = const itemKey =
data.item_type === LibraryItemType.RSFORM data.item_type === LibraryItemType.RSFORM
? KEYS.composite.rsItem({ itemID: data.id }) ? KEYS.composite.rsItem({ itemID: data.id })
@ -30,7 +30,7 @@ export const useUpdateItem = () => {
if (data.item_type === LibraryItemType.RSFORM) { if (data.item_type === LibraryItemType.RSFORM) {
const schema: IRSFormDTO | undefined = client.getQueryData(itemKey); const schema: IRSFormDTO | undefined = client.getQueryData(itemKey);
if (schema) { if (schema) {
return Promise.allSettled( await Promise.allSettled(
schema.oss.map(item => client.invalidateQueries({ queryKey: KEYS.composite.ossItem({ itemID: item.id }) })) schema.oss.map(item => client.invalidateQueries({ queryKey: KEYS.composite.ossItem({ itemID: item.id }) }))
); );
} }

View File

@ -20,6 +20,7 @@ import { cn } from '@/components/utils';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
import { tripleToggleColor } from '@/utils/utils'; import { tripleToggleColor } from '@/utils/utils';
import { useLibrarySuspense } from '../../backend/use-library';
import { IconItemVisibility } from '../../components/icon-item-visibility'; import { IconItemVisibility } from '../../components/icon-item-visibility';
import { IconLocationHead } from '../../components/icon-location-head'; import { IconLocationHead } from '../../components/icon-location-head';
import { describeLocationHead, labelLocationHead } from '../../labels'; import { describeLocationHead, labelLocationHead } from '../../labels';
@ -33,6 +34,7 @@ interface ToolbarSearchProps {
} }
export function ToolbarSearch({ className, total, filtered }: ToolbarSearchProps) { export function ToolbarSearch({ className, total, filtered }: ToolbarSearchProps) {
const { items } = useLibrarySuspense();
const userMenu = useDropdown(); const userMenu = useDropdown();
const headMenu = useDropdown(); const headMenu = useDropdown();
@ -58,6 +60,10 @@ export function ToolbarSearch({ className, total, filtered }: ToolbarSearchProps
const userActive = isOwned !== null || isEditor !== null || filterUser !== null; const userActive = isOwned !== null || isEditor !== null || filterUser !== null;
function filterNonEmptyUsers(userID: number): boolean {
return items.some(item => item.owner === userID);
}
function handleChange(newValue: LocationHead | null) { function handleChange(newValue: LocationHead | null) {
headMenu.hide(); headMenu.hide();
setHead(newValue); setHead(newValue);
@ -114,6 +120,7 @@ export function ToolbarSearch({ className, total, filtered }: ToolbarSearchProps
placeholder='Выберите владельца' placeholder='Выберите владельца'
noBorder noBorder
className='min-w-60 mx-1 mb-1' className='min-w-60 mx-1 mb-1'
filter={filterNonEmptyUsers}
value={filterUser} value={filterUser}
onChange={setFilterUser} onChange={setFilterUser}
/> />

View File

@ -7,7 +7,15 @@ import { type ILibraryItem } from '@/features/library';
import { Graph } from '@/models/graph'; import { Graph } from '@/models/graph';
import { type RO } from '@/utils/meta'; import { type RO } from '@/utils/meta';
import { type IBlock, type IOperation, type IOperationSchema, type IOperationSchemaStats } from '../models/oss'; import {
type IBlock,
type IOperation,
type IOperationSchema,
type IOperationSchemaStats,
type IOssItem,
NodeType
} from '../models/oss';
import { constructNodeID } from '../models/oss-api';
import { BLOCK_NODE_MIN_HEIGHT, BLOCK_NODE_MIN_WIDTH } from '../pages/oss-page/editor-oss-graph/graph/block-node'; import { BLOCK_NODE_MIN_HEIGHT, BLOCK_NODE_MIN_WIDTH } from '../pages/oss-page/editor-oss-graph/graph/block-node';
import { type IOperationSchemaDTO, OperationType } from './types'; import { type IOperationSchemaDTO, OperationType } from './types';
@ -16,8 +24,9 @@ import { type IOperationSchemaDTO, OperationType } from './types';
export class OssLoader { export class OssLoader {
private oss: IOperationSchema; private oss: IOperationSchema;
private graph: Graph = new Graph(); private graph: Graph = new Graph();
private hierarchy: Graph = new Graph(); private hierarchy: Graph<string> = new Graph<string>();
private operationByID = new Map<number, IOperation>(); private operationByID = new Map<number, IOperation>();
private itemByNodeID = new Map<string, IOssItem>();
private blockByID = new Map<number, IBlock>(); private blockByID = new Map<number, IBlock>();
private schemaIDs: number[] = []; private schemaIDs: number[] = [];
private items: RO<ILibraryItem[]>; private items: RO<ILibraryItem[]>;
@ -37,6 +46,7 @@ export class OssLoader {
result.operationByID = this.operationByID; result.operationByID = this.operationByID;
result.blockByID = this.blockByID; result.blockByID = this.blockByID;
result.itemByNodeID = this.itemByNodeID;
result.graph = this.graph; result.graph = this.graph;
result.hierarchy = this.hierarchy; result.hierarchy = this.hierarchy;
result.schemas = this.schemaIDs; result.schemas = this.schemaIDs;
@ -46,18 +56,24 @@ export class OssLoader {
private prepareLookups() { private prepareLookups() {
this.oss.operations.forEach(operation => { this.oss.operations.forEach(operation => {
operation.nodeID = constructNodeID(NodeType.OPERATION, operation.id);
operation.nodeType = NodeType.OPERATION;
this.itemByNodeID.set(operation.nodeID, operation);
this.operationByID.set(operation.id, operation); this.operationByID.set(operation.id, operation);
this.graph.addNode(operation.id); this.graph.addNode(operation.id);
this.hierarchy.addNode(operation.id); this.hierarchy.addNode(operation.nodeID);
if (operation.parent) { if (operation.parent) {
this.hierarchy.addEdge(-operation.parent, operation.id); this.hierarchy.addEdge(constructNodeID(NodeType.BLOCK, operation.parent), operation.nodeID);
} }
}); });
this.oss.blocks.forEach(block => { this.oss.blocks.forEach(block => {
block.nodeID = constructNodeID(NodeType.BLOCK, block.id);
block.nodeType = NodeType.BLOCK;
this.itemByNodeID.set(block.nodeID, block);
this.blockByID.set(block.id, block); this.blockByID.set(block.id, block);
this.hierarchy.addNode(-block.id); this.hierarchy.addNode(block.nodeID);
if (block.parent) { if (block.parent) {
this.hierarchy.addEdge(-block.parent, -block.id); this.hierarchy.addEdge(constructNodeID(NodeType.BLOCK, block.parent), block.nodeID);
} }
}); });
} }

View File

@ -72,6 +72,12 @@ export type IRelocateConstituentsDTO = z.infer<typeof schemaRelocateConstituents
/** Represents {@link IConstituenta} reference. */ /** Represents {@link IConstituenta} reference. */
export type IConstituentaReference = z.infer<typeof schemaConstituentaReference>; export type IConstituentaReference = z.infer<typeof schemaConstituentaReference>;
/** Represents {@link IOperation} position. */
export type IOperationPosition = z.infer<typeof schemaOperationPosition>;
/** Represents {@link IBlock} position. */
export type IBlockPosition = z.infer<typeof schemaBlockPosition>;
// ====== Schemas ====== // ====== Schemas ======
export const schemaOperationType = z.enum(Object.values(OperationType) as [OperationType, ...OperationType[]]); export const schemaOperationType = z.enum(Object.values(OperationType) as [OperationType, ...OperationType[]]);

View File

@ -10,9 +10,9 @@ export const useCreateInput = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'create-input'], mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'create-input'],
mutationFn: ossApi.createInput, mutationFn: ossApi.createInput,
onSuccess: data => { onSuccess: async data => {
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.oss.id }).queryKey, data.oss); client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.oss.id }).queryKey, data.oss);
return Promise.allSettled([ await Promise.allSettled([
client.invalidateQueries({ queryKey: KEYS.composite.libraryList }), client.invalidateQueries({ queryKey: KEYS.composite.libraryList }),
client.invalidateQueries({ queryKey: [KEYS.rsform] }) client.invalidateQueries({ queryKey: [KEYS.rsform] })
]); ]);

View File

@ -10,9 +10,9 @@ export const useDeleteBlock = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'delete-block'], mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'delete-block'],
mutationFn: ossApi.deleteBlock, mutationFn: ossApi.deleteBlock,
onSuccess: data => { onSuccess: async data => {
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data); client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
return Promise.allSettled([ await Promise.allSettled([
client.invalidateQueries({ queryKey: KEYS.composite.libraryList }), client.invalidateQueries({ queryKey: KEYS.composite.libraryList }),
client.invalidateQueries({ queryKey: [KEYS.rsform] }) client.invalidateQueries({ queryKey: [KEYS.rsform] })
]); ]);

View File

@ -10,9 +10,9 @@ export const useDeleteOperation = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'delete-operation'], mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'delete-operation'],
mutationFn: ossApi.deleteOperation, mutationFn: ossApi.deleteOperation,
onSuccess: data => { onSuccess: async data => {
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data); client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
return Promise.allSettled([ await Promise.allSettled([
client.invalidateQueries({ queryKey: KEYS.composite.libraryList }), client.invalidateQueries({ queryKey: KEYS.composite.libraryList }),
client.invalidateQueries({ queryKey: [KEYS.rsform] }) client.invalidateQueries({ queryKey: [KEYS.rsform] })
]); ]);

View File

@ -10,9 +10,9 @@ export const useExecuteOperation = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'execute-operation'], mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'execute-operation'],
mutationFn: ossApi.executeOperation, mutationFn: ossApi.executeOperation,
onSuccess: data => { onSuccess: async data => {
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data); client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
return Promise.allSettled([ await Promise.allSettled([
client.invalidateQueries({ queryKey: KEYS.composite.libraryList }), client.invalidateQueries({ queryKey: KEYS.composite.libraryList }),
client.invalidateQueries({ queryKey: [KEYS.rsform] }) client.invalidateQueries({ queryKey: [KEYS.rsform] })
]); ]);

View File

@ -10,9 +10,9 @@ export const useMoveItems = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'move-items'], mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'move-items'],
mutationFn: ossApi.moveItems, mutationFn: ossApi.moveItems,
onSuccess: data => { onSuccess: async data => {
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data); client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
return Promise.allSettled([ await Promise.allSettled([
client.invalidateQueries({ queryKey: KEYS.composite.libraryList }), client.invalidateQueries({ queryKey: KEYS.composite.libraryList }),
client.invalidateQueries({ queryKey: [KEYS.rsform] }) client.invalidateQueries({ queryKey: [KEYS.rsform] })
]); ]);

View File

@ -10,9 +10,9 @@ export const useRelocateConstituents = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'relocate-constituents'], mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'relocate-constituents'],
mutationFn: ossApi.relocateConstituents, mutationFn: ossApi.relocateConstituents,
onSuccess: data => { onSuccess: async data => {
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data); client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
return Promise.allSettled([ await Promise.allSettled([
client.invalidateQueries({ queryKey: KEYS.composite.libraryList }), client.invalidateQueries({ queryKey: KEYS.composite.libraryList }),
client.invalidateQueries({ queryKey: [KEYS.rsform] }) client.invalidateQueries({ queryKey: [KEYS.rsform] })
]); ]);

View File

@ -10,9 +10,9 @@ export const useUpdateInput = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'update-input'], mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'update-input'],
mutationFn: ossApi.updateInput, mutationFn: ossApi.updateInput,
onSuccess: data => { onSuccess: async data => {
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data); client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
return Promise.allSettled([ await Promise.allSettled([
client.invalidateQueries({ queryKey: KEYS.composite.libraryList }), client.invalidateQueries({ queryKey: KEYS.composite.libraryList }),
client.invalidateQueries({ queryKey: [KEYS.rsform] }) client.invalidateQueries({ queryKey: [KEYS.rsform] })
]); ]);

View File

@ -9,24 +9,22 @@ import { ComboBox } from '@/components/input/combo-box';
import { type Styling } from '@/components/props'; import { type Styling } from '@/components/props';
import { cn } from '@/components/utils'; import { cn } from '@/components/utils';
import { NoData } from '@/components/view'; import { NoData } from '@/components/view';
import { type RO } from '@/utils/meta';
import { labelOssItem } from '../labels'; import { labelOssItem } from '../labels';
import { type IOperationSchema, type IOssItem } from '../models/oss'; import { type IOperationSchema, type IOssItem, NodeType } from '../models/oss';
import { getItemID, isOperation } from '../models/oss-api';
const SELECTION_CLEAR_TIMEOUT = 1000; const SELECTION_CLEAR_TIMEOUT = 1000;
interface PickMultiOperationProps extends Styling { interface PickContentsProps extends Styling {
value: number[]; value: IOssItem[];
onChange: (newValue: number[]) => void; onChange: (newValue: IOssItem[]) => void;
schema: IOperationSchema; schema: IOperationSchema;
rows?: number; rows?: number;
exclude?: number[]; exclude?: IOssItem[];
disallowBlocks?: boolean; disallowBlocks?: boolean;
} }
const columnHelper = createColumnHelper<RO<IOssItem>>(); const columnHelper = createColumnHelper<IOssItem>();
export function PickContents({ export function PickContents({
rows, rows,
@ -37,29 +35,26 @@ export function PickContents({
onChange, onChange,
className, className,
...restProps ...restProps
}: PickMultiOperationProps) { }: PickContentsProps) {
const selectedItems = value const [lastSelected, setLastSelected] = useState<IOssItem | null>(null);
.map(itemID => (itemID > 0 ? schema.operationByID.get(itemID) : schema.blockByID.get(-itemID))) const items: IOssItem[] = [
.filter(item => item !== undefined); ...(disallowBlocks ? [] : schema.blocks.filter(item => !value.includes(item) && !exclude?.includes(item))),
const [lastSelected, setLastSelected] = useState<RO<IOssItem> | null>(null); ...schema.operations.filter(item => !value.includes(item) && !exclude?.includes(item))
const items = [
...(disallowBlocks ? [] : schema.blocks.filter(item => !value.includes(-item.id) && !exclude?.includes(-item.id))),
...schema.operations.filter(item => !value.includes(item.id) && !exclude?.includes(item.id))
]; ];
function handleDelete(target: number) { function handleDelete(target: IOssItem) {
onChange(value.filter(item => item !== target)); onChange(value.filter(item => item !== target));
} }
function handleSelect(target: RO<IOssItem> | null) { function handleSelect(target: IOssItem | null) {
if (target) { if (target) {
setLastSelected(target); setLastSelected(target);
onChange([...value, getItemID(target)]); onChange([...value, target]);
setTimeout(() => setLastSelected(null), SELECTION_CLEAR_TIMEOUT); setTimeout(() => setLastSelected(null), SELECTION_CLEAR_TIMEOUT);
} }
} }
function handleMoveUp(target: number) { function handleMoveUp(target: IOssItem) {
const index = value.indexOf(target); const index = value.indexOf(target);
if (index > 0) { if (index > 0) {
const newSelected = [...value]; const newSelected = [...value];
@ -69,7 +64,7 @@ export function PickContents({
} }
} }
function handleMoveDown(target: number) { function handleMoveDown(target: IOssItem) {
const index = value.indexOf(target); const index = value.indexOf(target);
if (index < value.length - 1) { if (index < value.length - 1) {
const newSelected = [...value]; const newSelected = [...value];
@ -80,13 +75,13 @@ export function PickContents({
} }
const columns = [ const columns = [
columnHelper.accessor(item => isOperation(item), { columnHelper.accessor(item => item.nodeType === NodeType.OPERATION, {
id: 'type', id: 'type',
header: 'Тип', header: 'Тип',
size: 150, size: 150,
minSize: 150, minSize: 150,
maxSize: 150, maxSize: 150,
cell: props => <div>{isOperation(props.row.original) ? 'Операция' : 'Блок'}</div> cell: props => <div>{props.getValue() ? 'Операция' : 'Блок'}</div>
}), }),
columnHelper.accessor('title', { columnHelper.accessor('title', {
id: 'title', id: 'title',
@ -106,21 +101,21 @@ export function PickContents({
noHover noHover
className='px-0' className='px-0'
icon={<IconRemove size='1rem' className='icon-red' />} icon={<IconRemove size='1rem' className='icon-red' />}
onClick={() => handleDelete(getItemID(props.row.original))} onClick={() => handleDelete(props.row.original)}
/> />
<MiniButton <MiniButton
title='Переместить выше' title='Переместить выше'
noHover noHover
className='px-0' className='px-0'
icon={<IconMoveUp size='1rem' className='icon-primary' />} icon={<IconMoveUp size='1rem' className='icon-primary' />}
onClick={() => handleMoveUp(getItemID(props.row.original))} onClick={() => handleMoveUp(props.row.original)}
/> />
<MiniButton <MiniButton
title='Переместить ниже' title='Переместить ниже'
noHover noHover
className='px-0' className='px-0'
icon={<IconMoveDown size='1rem' className='icon-primary' />} icon={<IconMoveDown size='1rem' className='icon-primary' />}
onClick={() => handleMoveDown(getItemID(props.row.original))} onClick={() => handleMoveDown(props.row.original)}
/> />
</div> </div>
) )
@ -134,7 +129,7 @@ export function PickContents({
items={items} items={items}
value={lastSelected} value={lastSelected}
placeholder='Выберите операцию или блок' placeholder='Выберите операцию или блок'
idFunc={item => String(getItemID(item))} idFunc={item => item.nodeID}
labelValueFunc={item => labelOssItem(item)} labelValueFunc={item => labelOssItem(item)}
labelOptionFunc={item => labelOssItem(item)} labelOptionFunc={item => labelOssItem(item)}
onChange={handleSelect} onChange={handleSelect}
@ -145,7 +140,7 @@ export function PickContents({
rows={rows} rows={rows}
contentHeight='1.3rem' contentHeight='1.3rem'
className='cc-scroll-y text-sm select-none border-y rounded-b-md' className='cc-scroll-y text-sm select-none border-y rounded-b-md'
data={selectedItems} data={value}
columns={columns} columns={columns}
headPosition='0rem' headPosition='0rem'
noDataComponent={ noDataComponent={

View File

@ -1,8 +1,7 @@
import { Tooltip } from '@/components/container'; import { Tooltip } from '@/components/container';
import { globalIDs } from '@/utils/constants'; import { globalIDs } from '@/utils/constants';
import { type IBlock, type IOperation } from '../models/oss'; import { NodeType } from '../models/oss';
import { isOperation } from '../models/oss-api';
import { useOperationTooltipStore } from '../stores/operation-tooltip'; import { useOperationTooltipStore } from '../stores/operation-tooltip';
import { InfoBlock } from './info-block'; import { InfoBlock } from './info-block';
@ -10,7 +9,7 @@ import { InfoOperation } from './info-operation';
export function OperationTooltip() { export function OperationTooltip() {
const hoverItem = useOperationTooltipStore(state => state.hoverItem); const hoverItem = useOperationTooltipStore(state => state.hoverItem);
const isOperationNode = isOperation(hoverItem); const isOperationNode = hoverItem?.nodeType === NodeType.OPERATION;
return ( return (
<Tooltip <Tooltip
@ -20,8 +19,8 @@ export function OperationTooltip() {
className='max-w-140 dense max-h-120! overflow-y-auto!' className='max-w-140 dense max-h-120! overflow-y-auto!'
hidden={!hoverItem} hidden={!hoverItem}
> >
{hoverItem && isOperationNode ? <InfoOperation operation={hoverItem as IOperation} /> : null} {hoverItem && isOperationNode ? <InfoOperation operation={hoverItem} /> : null}
{hoverItem && !isOperationNode ? <InfoBlock block={hoverItem as IBlock} /> : null} {hoverItem && !isOperationNode ? <InfoBlock block={hoverItem} /> : null}
</Tooltip> </Tooltip>
); );
} }

View File

@ -12,6 +12,7 @@ import { useDialogsStore } from '@/stores/dialogs';
import { type ICreateBlockDTO, schemaCreateBlock } from '../../backend/types'; import { type ICreateBlockDTO, schemaCreateBlock } from '../../backend/types';
import { useCreateBlock } from '../../backend/use-create-block'; import { useCreateBlock } from '../../backend/use-create-block';
import { type IOssItem, NodeType } from '../../models/oss';
import { type LayoutManager } from '../../models/oss-layout-api'; import { type LayoutManager } from '../../models/oss-layout-api';
import { BLOCK_NODE_MIN_HEIGHT, BLOCK_NODE_MIN_WIDTH } from '../../pages/oss-page/editor-oss-graph/graph/block-node'; import { BLOCK_NODE_MIN_HEIGHT, BLOCK_NODE_MIN_WIDTH } from '../../pages/oss-page/editor-oss-graph/graph/block-node';
@ -20,7 +21,7 @@ import { TabBlockChildren } from './tab-block-children';
export interface DlgCreateBlockProps { export interface DlgCreateBlockProps {
manager: LayoutManager; manager: LayoutManager;
initialChildren: number[]; initialChildren: IOssItem[];
initialParent: number | null; initialParent: number | null;
defaultX: number; defaultX: number;
defaultY: number; defaultY: number;
@ -52,8 +53,8 @@ export function DlgCreateBlock() {
position_y: defaultY, position_y: defaultY,
width: BLOCK_NODE_MIN_WIDTH, width: BLOCK_NODE_MIN_WIDTH,
height: BLOCK_NODE_MIN_HEIGHT, height: BLOCK_NODE_MIN_HEIGHT,
children_blocks: initialChildren.filter(id => id < 0).map(id => -id), children_blocks: initialChildren.filter(item => item.nodeType === NodeType.BLOCK).map(item => item.id),
children_operations: initialChildren.filter(id => id > 0), children_operations: initialChildren.filter(item => item.nodeType === NodeType.OPERATION).map(item => item.id),
layout: manager.layout layout: manager.layout
}, },
mode: 'onChange' mode: 'onChange'
@ -65,11 +66,12 @@ export function DlgCreateBlock() {
const isValid = !!title && !manager.oss.blocks.some(block => block.title === title); const isValid = !!title && !manager.oss.blocks.some(block => block.title === title);
function onSubmit(data: ICreateBlockDTO) { function onSubmit(data: ICreateBlockDTO) {
const rectangle = manager.calculateNewBlockPosition(data); const rectangle = manager.newBlockPosition(data);
data.position_x = rectangle.x; data.position_x = rectangle.x;
data.position_y = rectangle.y; data.position_y = rectangle.y;
data.width = rectangle.width; data.width = rectangle.width;
data.height = rectangle.height; data.height = rectangle.height;
data.layout = manager.layout;
void createBlock({ itemID: manager.oss.id, data: data }).then(response => onCreate?.(response.new_block.id)); void createBlock({ itemID: manager.oss.id, data: data }).then(response => onCreate?.(response.new_block.id));
} }

View File

@ -7,6 +7,8 @@ import { useDialogsStore } from '@/stores/dialogs';
import { type ICreateBlockDTO } from '../../backend/types'; import { type ICreateBlockDTO } from '../../backend/types';
import { SelectParent } from '../../components/select-parent'; import { SelectParent } from '../../components/select-parent';
import { NodeType } from '../../models/oss';
import { constructNodeID } from '../../models/oss-api';
import { type DlgCreateBlockProps } from './dlg-create-block'; import { type DlgCreateBlockProps } from './dlg-create-block';
@ -18,10 +20,8 @@ export function TabBlockCard() {
formState: { errors } formState: { errors }
} = useFormContext<ICreateBlockDTO>(); } = useFormContext<ICreateBlockDTO>();
const children_blocks = useWatch({ control, name: 'children_blocks' }); const children_blocks = useWatch({ control, name: 'children_blocks' });
const all_children = [ const block_ids = children_blocks.map(id => constructNodeID(NodeType.BLOCK, id));
...children_blocks, const all_children = [...block_ids, ...manager.oss.hierarchy.expandAllOutputs(block_ids)];
...manager.oss.hierarchy.expandAllOutputs(children_blocks.filter(id => id < 0).map(id => -id)).map(id => -id)
];
return ( return (
<div className='cc-fade-in cc-column'> <div className='cc-fade-in cc-column'>
@ -36,7 +36,7 @@ export function TabBlockCard() {
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<SelectParent <SelectParent
items={manager.oss.blocks.filter(block => !all_children.includes(block.id))} items={manager.oss.blocks.filter(block => !all_children.includes(block.nodeID))}
value={field.value ? manager.oss.blockByID.get(field.value) ?? null : null} value={field.value ? manager.oss.blockByID.get(field.value) ?? null : null}
placeholder='Блок содержания не выбран' placeholder='Блок содержания не выбран'
onChange={value => field.onChange(value ? value.id : null)} onChange={value => field.onChange(value ? value.id : null)}

View File

@ -6,6 +6,7 @@ import { useDialogsStore } from '@/stores/dialogs';
import { type ICreateBlockDTO } from '../../backend/types'; import { type ICreateBlockDTO } from '../../backend/types';
import { PickContents } from '../../components/pick-contents'; import { PickContents } from '../../components/pick-contents';
import { type IOssItem, NodeType } from '../../models/oss';
import { type DlgCreateBlockProps } from './dlg-create-block'; import { type DlgCreateBlockProps } from './dlg-create-block';
@ -15,19 +16,31 @@ export function TabBlockChildren() {
const parent = useWatch({ control, name: 'item_data.parent' }); const parent = useWatch({ control, name: 'item_data.parent' });
const children_blocks = useWatch({ control, name: 'children_blocks' }); const children_blocks = useWatch({ control, name: 'children_blocks' });
const children_operations = useWatch({ control, name: 'children_operations' }); const children_operations = useWatch({ control, name: 'children_operations' });
const exclude = parent ? [-parent, ...manager.oss.hierarchy.expandAllInputs([-parent]).filter(id => id < 0)] : [];
const value = [...children_blocks.map(id => -id), ...children_operations]; const parentItem = parent ? manager.oss.blockByID.get(parent) : null;
const internalBlocks = parentItem
? manager.oss.hierarchy
.expandAllInputs([parentItem.nodeID])
.map(id => manager.oss.itemByNodeID.get(id))
.filter(item => item !== null && item?.nodeType === NodeType.BLOCK)
: [];
function handleChangeSelected(newValue: number[]) { const exclude = parentItem ? [parentItem, ...internalBlocks] : [];
const value = [
...children_blocks.map(id => manager.oss.blockByID.get(id)!),
...children_operations.map(id => manager.oss.operationByID.get(id)!)
];
function handleChangeSelected(newValue: IOssItem[]) {
setValue( setValue(
'children_blocks', 'children_blocks',
newValue.filter(id => id < 0).map(id => -id), newValue.filter(item => item.nodeType === NodeType.BLOCK).map(item => item.id),
{ shouldValidate: true } { shouldValidate: true }
); );
setValue( setValue(
'children_operations', 'children_operations',
newValue.filter(id => id > 0), newValue.filter(item => item.nodeType === NodeType.OPERATION).map(item => item.id),
{ shouldValidate: true } { shouldValidate: true }
); );
} }

View File

@ -64,9 +64,10 @@ export function DlgCreateOperation() {
const isValid = !!alias && !manager.oss.operations.some(operation => operation.alias === alias); const isValid = !!alias && !manager.oss.operations.some(operation => operation.alias === alias);
function onSubmit(data: ICreateOperationDTO) { function onSubmit(data: ICreateOperationDTO) {
const target = manager.calculateNewOperationPosition(data); const target = manager.newOperationPosition(data);
data.position_x = target.x; data.position_x = target.x;
data.position_y = target.y; data.position_y = target.y;
data.layout = manager.layout;
void createOperation({ itemID: manager.oss.id, data: data }).then(response => void createOperation({ itemID: manager.oss.id, data: data }).then(response =>
onCreate?.(response.new_operation.id) onCreate?.(response.new_operation.id)
); );

View File

@ -42,6 +42,10 @@ export function DlgEditBlock() {
}); });
function onSubmit(data: IUpdateBlockDTO) { function onSubmit(data: IUpdateBlockDTO) {
if (data.item_data.parent !== target.parent) {
manager.onBlockChangeParent(data.target, data.item_data.parent);
data.layout = manager.layout;
}
return updateBlock({ itemID: manager.oss.id, data }); return updateBlock({ itemID: manager.oss.id, data });
} }

View File

@ -58,9 +58,10 @@ export function DlgEditOperation() {
const [activeTab, setActiveTab] = useState<TabID>(TabID.CARD); const [activeTab, setActiveTab] = useState<TabID>(TabID.CARD);
function onSubmit(data: IUpdateOperationDTO) { function onSubmit(data: IUpdateOperationDTO) {
// if (data.item_data.parent !== target.parent) { if (data.item_data.parent !== target.parent) {
// data.layout = updateLayoutOnOperationChange(data.target, data.item_data.parent, data.layout); manager.onOperationChangeParent(data.target, data.item_data.parent);
// } data.layout = manager.layout;
}
return updateOperation({ itemID: manager.oss.id, data }); return updateOperation({ itemID: manager.oss.id, data });
} }

View File

@ -5,9 +5,9 @@ import {
type IOperation, type IOperation,
type IOssItem, type IOssItem,
type ISubstitutionErrorDescription, type ISubstitutionErrorDescription,
NodeType,
SubstitutionErrorType SubstitutionErrorType
} from './models/oss'; } from './models/oss';
import { isOperation } from './models/oss-api';
/** Retrieves label for {@link OperationType}. */ /** Retrieves label for {@link OperationType}. */
export function labelOperationType(itemType: OperationType): string { export function labelOperationType(itemType: OperationType): string {
@ -58,7 +58,7 @@ export function describeSubstitutionError(error: RO<ISubstitutionErrorDescriptio
/** Retrieves label for {@link IOssItem}. */ /** Retrieves label for {@link IOssItem}. */
export function labelOssItem(item: RO<IOssItem>): string { export function labelOssItem(item: RO<IOssItem>): string {
if (isOperation(item)) { if (item.nodeType === NodeType.OPERATION) {
return `${(item as IOperation).alias}: ${item.title}`; return `${(item as IOperation).alias}: ${item.title}`;
} else { } else {
return `Блок: ${item.title}`; return `Блок: ${item.title}`;

View File

@ -20,23 +20,16 @@ import {
} from '@/features/rsform/models/rslang-api'; } from '@/features/rsform/models/rslang-api';
import { infoMsg } from '@/utils/labels'; import { infoMsg } from '@/utils/labels';
import { type RO } from '@/utils/meta';
import { Graph } from '../../../models/graph'; import { Graph } from '../../../models/graph';
import { describeSubstitutionError } from '../labels'; import { describeSubstitutionError } from '../labels';
import { type IOperationSchema, type IOssItem, SubstitutionErrorType } from './oss'; import { type IOperationSchema, NodeType, SubstitutionErrorType } from './oss';
const STARTING_SUB_INDEX = 900; // max semantic index for starting substitution const STARTING_SUB_INDEX = 900; // max semantic index for starting substitution
/** Checks if element is {@link IOperation} or {@link IBlock}. */ export function constructNodeID(type: NodeType, itemID: number): string {
export function isOperation(item: RO<IOssItem> | null): boolean { return type === NodeType.OPERATION ? 'o' + String(itemID) : 'b' + String(itemID);
return !!item && 'arguments' in item;
}
/** Extract contiguous ID of {@link IOperation} or {@link IBlock}. */
export function getItemID(item: RO<IOssItem>): number {
return isOperation(item) ? item.id : -item.id;
} }
/** Sorts library items relevant for the specified {@link IOperationSchema}. */ /** Sorts library items relevant for the specified {@link IOperationSchema}. */

View File

@ -1,12 +1,16 @@
import { type ICreateBlockDTO, type ICreateOperationDTO, type IOssLayout } from '../backend/types'; import {
type IBlockPosition,
type ICreateBlockDTO,
type ICreateOperationDTO,
type IOperationPosition,
type IOssLayout
} from '../backend/types';
import { type IOperationSchema } from './oss'; import { type IOperationSchema } from './oss';
import { type Position2D, type Rectangle2D } from './oss-layout'; import { type Position2D, type Rectangle2D } from './oss-layout';
export const GRID_SIZE = 10; // pixels - size of OSS grid export const GRID_SIZE = 10; // pixels - size of OSS grid
const MIN_DISTANCE = 2 * GRID_SIZE; // pixels - minimum distance between nodes const MIN_DISTANCE = 2 * GRID_SIZE; // pixels - minimum distance between nodes
const DISTANCE_X = 180; // pixels - insert x-distance between node centers
const DISTANCE_Y = 100; // pixels - insert y-distance between node centers
const OPERATION_NODE_WIDTH = 150; const OPERATION_NODE_WIDTH = 150;
const OPERATION_NODE_HEIGHT = 40; const OPERATION_NODE_HEIGHT = 40;
@ -26,93 +30,236 @@ export class LayoutManager {
} }
/** Calculate insert position for a new {@link IOperation} */ /** Calculate insert position for a new {@link IOperation} */
calculateNewOperationPosition(data: ICreateOperationDTO): Position2D { newOperationPosition(data: ICreateOperationDTO): Position2D {
// TODO: check parent node let result = { x: data.position_x, y: data.position_y };
const result = { x: data.position_x, y: data.position_y };
const operations = this.layout.operations; const operations = this.layout.operations;
const parentNode = this.layout.blocks.find(pos => pos.id === data.item_data.parent);
if (operations.length === 0) { if (operations.length === 0) {
return result; return result;
} }
if (data.arguments.length === 0) { if (data.arguments.length !== 0) {
let inputsPositions = operations.filter(pos => result = calculatePositionFromArgs(data.arguments, operations);
this.oss.operations.find(operation => operation.arguments.length === 0 && operation.id === pos.id) } else if (parentNode) {
); result.x = parentNode.x + MIN_DISTANCE;
if (inputsPositions.length === 0) { result.y = parentNode.y + MIN_DISTANCE;
inputsPositions = operations;
}
const maxX = Math.max(...inputsPositions.map(node => node.x));
const minY = Math.min(...inputsPositions.map(node => node.y));
result.x = maxX + DISTANCE_X;
result.y = minY;
} else { } else {
const argNodes = operations.filter(pos => data.arguments.includes(pos.id)); result = this.calculatePositionForFreeOperation(result);
const maxY = Math.max(...argNodes.map(node => node.y));
const minX = Math.min(...argNodes.map(node => node.x));
const maxX = Math.max(...argNodes.map(node => node.x));
result.x = Math.ceil((maxX + minX) / 2 / GRID_SIZE) * GRID_SIZE;
result.y = maxY + DISTANCE_Y;
} }
let flagIntersect = false; result = preventOverlap(
do { { ...result, width: OPERATION_NODE_WIDTH, height: OPERATION_NODE_HEIGHT },
flagIntersect = operations.some( operations.map(node => ({ ...node, width: OPERATION_NODE_WIDTH, height: OPERATION_NODE_HEIGHT }))
position => Math.abs(position.x - result.x) < MIN_DISTANCE && Math.abs(position.y - result.y) < MIN_DISTANCE
); );
if (flagIntersect) {
result.x += MIN_DISTANCE; if (parentNode) {
result.y += MIN_DISTANCE; const borderX = result.x + OPERATION_NODE_WIDTH + MIN_DISTANCE;
const borderY = result.y + OPERATION_NODE_HEIGHT + MIN_DISTANCE;
if (borderX > parentNode.x + parentNode.width) {
parentNode.width = borderX - parentNode.x;
} }
} while (flagIntersect); if (borderY > parentNode.y + parentNode.height) {
return result; parentNode.height = borderY - parentNode.y;
}
// TODO: trigger cascading updates
}
return { x: result.x, y: result.y };
} }
/** Calculate insert position for a new {@link IBlock} */ /** Calculate insert position for a new {@link IBlock} */
calculateNewBlockPosition(data: ICreateBlockDTO): Rectangle2D { newBlockPosition(data: ICreateBlockDTO): Rectangle2D {
const block_nodes = data.children_blocks const block_nodes = data.children_blocks
.map(id => this.layout.blocks.find(block => block.id === id)) .map(id => this.layout.blocks.find(block => block.id === id))
.filter(node => !!node); .filter(node => !!node);
const operation_nodes = data.children_operations const operation_nodes = data.children_operations
.map(id => this.layout.operations.find(operation => operation.id === id)) .map(id => this.layout.operations.find(operation => operation.id === id))
.filter(node => !!node); .filter(node => !!node);
const parentNode = this.layout.blocks.find(pos => pos.id === data.item_data.parent);
if (block_nodes.length === 0 && operation_nodes.length === 0) { let result: Rectangle2D = { x: data.position_x, y: data.position_y, width: data.width, height: data.height };
return { x: data.position_x, y: data.position_y, width: data.width, height: data.height };
if (block_nodes.length !== 0 || operation_nodes.length !== 0) {
result = calculatePositionFromChildren(
{ x: data.position_x, y: data.position_y, width: data.width, height: data.height },
operation_nodes,
block_nodes
);
} else if (parentNode) {
result = {
x: parentNode.x + MIN_DISTANCE,
y: parentNode.y + MIN_DISTANCE,
width: data.width,
height: data.height
};
} else {
result = this.calculatePositionForFreeBlock(result);
} }
if (block_nodes.length === 0 && operation_nodes.length === 0) {
if (parentNode) {
const siblings = this.oss.blocks.filter(block => block.parent === parentNode.id).map(block => block.id);
if (siblings.length > 0) {
result = preventOverlap(
result,
this.layout.blocks.filter(block => siblings.includes(block.id))
);
}
} else {
const rootBlocks = this.oss.blocks.filter(block => block.parent === null).map(block => block.id);
if (rootBlocks.length > 0) {
result = preventOverlap(
result,
this.layout.blocks.filter(block => rootBlocks.includes(block.id))
);
}
}
}
if (parentNode) {
const borderX = result.x + result.width + MIN_DISTANCE;
const borderY = result.y + result.height + MIN_DISTANCE;
if (borderX > parentNode.x + parentNode.width) {
parentNode.width = borderX - parentNode.x;
}
if (borderY > parentNode.y + parentNode.height) {
parentNode.height = borderY - parentNode.y;
}
// TODO: trigger cascading updates
}
return result;
}
/** Update layout when parent changes */
onOperationChangeParent(targetID: number, newParent: number | null) {
console.error('not implemented', targetID, newParent);
}
/** Update layout when parent changes */
onBlockChangeParent(targetID: number, newParent: number | null) {
console.error('not implemented', targetID, newParent);
}
private calculatePositionForFreeOperation(initial: Position2D): Position2D {
const operations = this.layout.operations;
if (operations.length === 0) {
return initial;
}
const freeInputs = this.oss.operations
.filter(operation => operation.arguments.length === 0 && operation.parent === null)
.map(operation => operation.id);
let inputsPositions = operations.filter(pos => freeInputs.includes(pos.id));
if (inputsPositions.length === 0) {
inputsPositions = operations;
}
const maxX = Math.max(...inputsPositions.map(node => node.x));
const minY = Math.min(...inputsPositions.map(node => node.y));
return {
x: maxX + OPERATION_NODE_WIDTH + MIN_DISTANCE + GRID_SIZE,
y: minY
};
}
private calculatePositionForFreeBlock(initial: Rectangle2D): Rectangle2D {
const rootBlocks = this.oss.blocks.filter(block => block.parent === null).map(block => block.id);
const blocksPositions = this.layout.blocks.filter(pos => rootBlocks.includes(pos.id));
if (blocksPositions.length === 0) {
return initial;
}
const maxX = Math.max(...blocksPositions.map(node => node.x + node.width));
const minY = Math.min(...blocksPositions.map(node => node.y));
return { ...initial, x: maxX + MIN_DISTANCE, y: minY };
}
}
// ======= Internals =======
function rectanglesOverlap(a: Rectangle2D, b: Rectangle2D): boolean {
return !(
a.x + a.width + MIN_DISTANCE <= b.x ||
b.x + b.width + MIN_DISTANCE <= a.x ||
a.y + a.height + MIN_DISTANCE <= b.y ||
b.y + b.height + MIN_DISTANCE <= a.y
);
}
function getOverlapAmount(a: Rectangle2D, b: Rectangle2D): Position2D {
const xOverlap = Math.max(0, Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x));
const yOverlap = Math.max(0, Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y));
return { x: xOverlap, y: yOverlap };
}
function preventOverlap(target: Rectangle2D, fixedRectangles: Rectangle2D[]): Rectangle2D {
let hasOverlap: boolean;
do {
hasOverlap = false;
for (const fixed of fixedRectangles) {
if (rectanglesOverlap(target, fixed)) {
hasOverlap = true;
const overlap = getOverlapAmount(target, fixed);
if (overlap.x >= overlap.y) {
target.x += overlap.x + MIN_DISTANCE;
} else {
target.y += overlap.y + MIN_DISTANCE;
}
break;
}
}
} while (hasOverlap);
return target;
}
function calculatePositionFromArgs(args: number[], operations: IOperationPosition[]): Position2D {
const argNodes = operations.filter(pos => args.includes(pos.id));
const maxY = Math.max(...argNodes.map(node => node.y));
const minX = Math.min(...argNodes.map(node => node.x));
const maxX = Math.max(...argNodes.map(node => node.x));
return {
x: Math.ceil((maxX + minX) / 2 / GRID_SIZE) * GRID_SIZE,
y: maxY + 2 * OPERATION_NODE_HEIGHT + MIN_DISTANCE
};
}
function calculatePositionFromChildren(
initial: Rectangle2D,
operations: IOperationPosition[],
blocks: IBlockPosition[]
): Rectangle2D {
let left = undefined; let left = undefined;
let top = undefined; let top = undefined;
let right = undefined; let right = undefined;
let bottom = undefined; let bottom = undefined;
for (const block of block_nodes) { for (const block of blocks) {
left = !left ? block.x - MIN_DISTANCE : Math.min(left, block.x - MIN_DISTANCE); left = left === undefined ? block.x - MIN_DISTANCE : Math.min(left, block.x - MIN_DISTANCE);
top = !top ? block.y - MIN_DISTANCE : Math.min(top, block.y - MIN_DISTANCE); top = top === undefined ? block.y - MIN_DISTANCE : Math.min(top, block.y - MIN_DISTANCE);
right = !right right =
? Math.max(left + data.width, block.x + block.width + MIN_DISTANCE) right === undefined
? Math.max(left + initial.width, block.x + block.width + MIN_DISTANCE)
: Math.max(right, block.x + block.width + MIN_DISTANCE); : Math.max(right, block.x + block.width + MIN_DISTANCE);
bottom = !bottom bottom = !bottom
? Math.max(top + data.height, block.y + block.height + MIN_DISTANCE) ? Math.max(top + initial.height, block.y + block.height + MIN_DISTANCE)
: Math.max(bottom, block.y + block.height + MIN_DISTANCE); : Math.max(bottom, block.y + block.height + MIN_DISTANCE);
} }
for (const operation of operation_nodes) { for (const operation of operations) {
left = !left ? operation.x - MIN_DISTANCE : Math.min(left, operation.x - MIN_DISTANCE); left = left === undefined ? operation.x - MIN_DISTANCE : Math.min(left, operation.x - MIN_DISTANCE);
top = !top ? operation.y - MIN_DISTANCE : Math.min(top, operation.y - MIN_DISTANCE); top = top === undefined ? operation.y - MIN_DISTANCE : Math.min(top, operation.y - MIN_DISTANCE);
right = !right right =
? Math.max(left + data.width, operation.x + OPERATION_NODE_WIDTH + MIN_DISTANCE) right === undefined
? Math.max(left + initial.width, operation.x + OPERATION_NODE_WIDTH + MIN_DISTANCE)
: Math.max(right, operation.x + OPERATION_NODE_WIDTH + MIN_DISTANCE); : Math.max(right, operation.x + OPERATION_NODE_WIDTH + MIN_DISTANCE);
bottom = !bottom bottom = !bottom
? Math.max(top + data.height, operation.y + OPERATION_NODE_HEIGHT + MIN_DISTANCE) ? Math.max(top + initial.height, operation.y + OPERATION_NODE_HEIGHT + MIN_DISTANCE)
: Math.max(bottom, operation.y + OPERATION_NODE_HEIGHT + MIN_DISTANCE); : Math.max(bottom, operation.y + OPERATION_NODE_HEIGHT + MIN_DISTANCE);
} }
return { return {
x: left ?? data.position_x, x: left ?? initial.x,
y: top ?? data.position_y, y: top ?? initial.y,
width: right && left ? right - left : data.width, width: right !== undefined && left !== undefined ? right - left : initial.width,
height: bottom && top ? bottom - top : data.height height: bottom !== undefined && top !== undefined ? bottom - top : initial.height
}; };
}
} }

View File

@ -11,8 +11,17 @@ import {
type IOperationSchemaDTO type IOperationSchemaDTO
} from '../backend/types'; } from '../backend/types';
/** Represents OSS node type. */
export const NodeType = {
OPERATION: 1,
BLOCK: 2
} as const;
export type NodeType = (typeof NodeType)[keyof typeof NodeType];
/** Represents Operation. */ /** Represents Operation. */
export interface IOperation extends IOperationDTO { export interface IOperation extends IOperationDTO {
nodeID: string;
nodeType: typeof NodeType.OPERATION;
x: number; x: number;
y: number; y: number;
is_owned: boolean; is_owned: boolean;
@ -23,12 +32,17 @@ export interface IOperation extends IOperationDTO {
/** Represents Block. */ /** Represents Block. */
export interface IBlock extends IBlockDTO { export interface IBlock extends IBlockDTO {
nodeID: string;
nodeType: typeof NodeType.BLOCK;
x: number; x: number;
y: number; y: number;
width: number; width: number;
height: number; height: number;
} }
/** Represents item of OperationSchema. */
export type IOssItem = IOperation | IBlock;
/** Represents {@link IOperationSchema} statistics. */ /** Represents {@link IOperationSchema} statistics. */
export interface IOperationSchemaStats { export interface IOperationSchemaStats {
count_all: number; count_all: number;
@ -45,16 +59,14 @@ export interface IOperationSchema extends IOperationSchemaDTO {
blocks: IBlock[]; blocks: IBlock[];
graph: Graph; graph: Graph;
hierarchy: Graph; hierarchy: Graph<string>;
schemas: number[]; schemas: number[];
stats: IOperationSchemaStats; stats: IOperationSchemaStats;
operationByID: Map<number, IOperation>; operationByID: Map<number, IOperation>;
blockByID: Map<number, IBlock>; blockByID: Map<number, IBlock>;
itemByNodeID: Map<string, IOssItem>;
} }
/** Represents item of OperationSchema. */
export type IOssItem = IOperation | IBlock;
/** Represents substitution error description. */ /** Represents substitution error description. */
export interface ISubstitutionErrorDescription { export interface ISubstitutionErrorDescription {
errorType: SubstitutionErrorType; errorType: SubstitutionErrorType;

View File

@ -1,7 +1,7 @@
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler 'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
'use client'; 'use client';
import { useRef } from 'react'; import { useEffect, useRef } from 'react';
import { useForm, useWatch } from 'react-hook-form'; import { useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
@ -47,10 +47,12 @@ export function FormOSS() {
const readOnly = useWatch({ control, name: 'read_only' }); const readOnly = useWatch({ control, name: 'read_only' });
const prevDirty = useRef(isDirty); const prevDirty = useRef(isDirty);
useEffect(() => {
if (prevDirty.current !== isDirty) { if (prevDirty.current !== isDirty) {
prevDirty.current = isDirty; prevDirty.current = isDirty;
setIsModified(isDirty); setIsModified(isDirty);
} }
}, [isDirty, setIsModified]);
function onSubmit(data: IUpdateLibraryItemDTO) { function onSubmit(data: IUpdateLibraryItemDTO) {
return updateOss(data).then(() => reset({ ...data })); return updateOss(data).then(() => reset({ ...data }));

View File

@ -4,8 +4,7 @@ import { useRef } from 'react';
import { Dropdown } from '@/components/dropdown'; import { Dropdown } from '@/components/dropdown';
import { type IBlock, type IOperation, type IOssItem } from '../../../../models/oss'; import { type IOssItem, NodeType } from '../../../../models/oss';
import { isOperation } from '../../../../models/oss-api';
import { MenuBlock } from './menu-block'; import { MenuBlock } from './menu-block';
import { MenuOperation } from './menu-operation'; import { MenuOperation } from './menu-operation';
@ -27,7 +26,6 @@ interface ContextMenuProps extends ContextMenuData {
export function ContextMenu({ isOpen, item, cursorX, cursorY, onHide }: ContextMenuProps) { export function ContextMenu({ isOpen, item, cursorX, cursorY, onHide }: ContextMenuProps) {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const isOperationNode = isOperation(item);
function handleBlur(event: React.FocusEvent<HTMLDivElement>) { function handleBlur(event: React.FocusEvent<HTMLDivElement>) {
if (!ref.current?.contains(event.relatedTarget as Node)) { if (!ref.current?.contains(event.relatedTarget as Node)) {
@ -39,21 +37,20 @@ export function ContextMenu({ isOpen, item, cursorX, cursorY, onHide }: ContextM
<div <div
ref={ref} ref={ref}
onBlur={handleBlur} onBlur={handleBlur}
className='relative' className='fixed z-tooltip'
style={{ top: `calc(${cursorY}px - 2.5rem)`, left: cursorX }} style={{ top: `calc(${cursorY}px + 0.5rem)`, left: cursorX }}
> >
<Dropdown <Dropdown
className='z-navigation!'
isOpen={isOpen} isOpen={isOpen}
stretchLeft={cursorX >= window.innerWidth - MENU_WIDTH} stretchLeft={cursorX >= window.innerWidth - MENU_WIDTH}
stretchTop={cursorY >= window.innerHeight - MENU_HEIGHT} stretchTop={cursorY >= window.innerHeight - MENU_HEIGHT}
margin={cursorY >= window.innerHeight - MENU_HEIGHT ? 'mb-3' : 'mt-3'} margin={cursorY >= window.innerHeight - MENU_HEIGHT ? 'mb-3' : 'mt-3'}
> >
{!!item ? ( {!!item ? (
isOperationNode ? ( item.nodeType === NodeType.OPERATION ? (
<MenuOperation operation={item as IOperation} onHide={onHide} /> <MenuOperation operation={item} onHide={onHide} />
) : ( ) : (
<MenuBlock block={item as IBlock} onHide={onHide} /> <MenuBlock block={item} onHide={onHide} />
) )
) : null} ) : null}
</Dropdown> </Dropdown>

View File

@ -16,15 +16,15 @@ export const BLOCK_NODE_MIN_WIDTH = 160;
export const BLOCK_NODE_MIN_HEIGHT = 100; export const BLOCK_NODE_MIN_HEIGHT = 100;
export function BlockNode(node: BlockInternalNode) { export function BlockNode(node: BlockInternalNode) {
const { selected, schema } = useOssEdit(); const { selectedItems, schema } = useOssEdit();
const dropTarget = useDraggingStore(state => state.dropTarget); const dropTarget = useDraggingStore(state => state.dropTarget);
const isDragging = useDraggingStore(state => state.isDragging); const isDragging = useDraggingStore(state => state.isDragging);
const showCoordinates = useOSSGraphStore(state => state.showCoordinates); const showCoordinates = useOSSGraphStore(state => state.showCoordinates);
const setHover = useOperationTooltipStore(state => state.setHoverItem); const setHover = useOperationTooltipStore(state => state.setHoverItem);
const focus = selected.length === 1 ? selected[0] : null; const focus = selectedItems.length === 1 ? selectedItems[0] : null;
const isParent = (!!focus && schema.hierarchy.at(focus)?.inputs.includes(-node.data.block.id)) ?? false; const isParent = (!!focus && schema.hierarchy.at(focus.nodeID)?.inputs.includes(node.data.block.nodeID)) ?? false;
const isChild = (!!focus && schema.hierarchy.at(focus)?.outputs.includes(-node.data.block.id)) ?? false; const isChild = (!!focus && schema.hierarchy.at(focus.nodeID)?.outputs.includes(node.data.block.nodeID)) ?? false;
return ( return (
<> <>
<NodeResizeControl minWidth={BLOCK_NODE_MIN_WIDTH} minHeight={BLOCK_NODE_MIN_HEIGHT}> <NodeResizeControl minWidth={BLOCK_NODE_MIN_WIDTH} minHeight={BLOCK_NODE_MIN_HEIGHT}>

View File

@ -21,9 +21,9 @@ interface NodeCoreProps {
} }
export function NodeCore({ node }: NodeCoreProps) { export function NodeCore({ node }: NodeCoreProps) {
const { selected, schema } = useOssEdit(); const { selectedItems, schema } = useOssEdit();
const focus = selected.length === 1 ? selected[0] : null; const focus = selectedItems.length === 1 ? selectedItems[0] : null;
const isChild = (!!focus && schema.hierarchy.at(focus)?.outputs.includes(node.data.operation.id)) ?? false; const isChild = (!!focus && schema.hierarchy.at(focus.nodeID)?.outputs.includes(node.data.operation.nodeID)) ?? false;
const setHover = useOperationTooltipStore(state => state.setHoverItem); const setHover = useOperationTooltipStore(state => state.setHoverItem);
const showCoordinates = useOSSGraphStore(state => state.showCoordinates); const showCoordinates = useOSSGraphStore(state => state.showCoordinates);

View File

@ -3,12 +3,12 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { type Edge, type Node, useEdgesState, useNodesState, useOnSelectionChange, useReactFlow } from 'reactflow'; import { type Edge, type Node, useEdgesState, useNodesState, useOnSelectionChange, useReactFlow } from 'reactflow';
import { type IOperationSchema } from '@/features/oss/models/oss';
import { type Position2D } from '@/features/oss/models/oss-layout';
import { useOSSGraphStore } from '@/features/oss/stores/oss-graph';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { type IOperationSchema, NodeType } from '../../../models/oss';
import { constructNodeID } from '../../../models/oss-api';
import { type Position2D } from '../../../models/oss-layout';
import { useOSSGraphStore } from '../../../stores/oss-graph';
import { useOssEdit } from '../oss-edit-context'; import { useOssEdit } from '../oss-edit-context';
import { flowOptions } from './oss-flow'; import { flowOptions } from './oss-flow';
@ -29,10 +29,10 @@ export const OssFlowState = ({ children }: React.PropsWithChildren) => {
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]); const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
function onSelectionChange({ nodes }: { nodes: Node[] }) { function onSelectionChange({ nodes }: { nodes: Node[] }) {
const ids = nodes.map(node => Number(node.id)); const ids = nodes.map(node => node.id);
setSelected(prev => [ setSelected(prev => [
...prev.filter(nodeID => ids.includes(nodeID)), ...prev.filter(nodeID => ids.includes(nodeID)),
...ids.filter(nodeID => !prev.includes(Number(nodeID))) ...ids.filter(nodeID => !prev.includes(nodeID))
]); ]);
} }
@ -41,46 +41,45 @@ export const OssFlowState = ({ children }: React.PropsWithChildren) => {
}); });
const resetGraph = useCallback(() => { const resetGraph = useCallback(() => {
const newNodes: Node[] = [ const newNodes: Node[] = schema.hierarchy.topologicalOrder().map(nodeID => {
...schema.hierarchy const item = schema.itemByNodeID.get(nodeID)!;
.topologicalOrder() if (item.nodeType === NodeType.BLOCK) {
.filter(id => id < 0)
.map(id => {
const block = schema.blockByID.get(-id)!;
return { return {
id: String(id), id: nodeID,
type: 'block', type: 'block',
data: { label: block.title, block: block }, data: { label: item.title, block: item },
position: computeRelativePosition(schema, { x: block.x, y: block.y }, block.parent), position: computeRelativePosition(schema, { x: item.x, y: item.y }, item.parent),
style: { style: {
width: block.width, width: item.width,
height: block.height height: item.height
}, },
parentId: block.parent ? `-${block.parent}` : undefined, parentId: item.parent ? constructNodeID(NodeType.BLOCK, item.parent) : undefined,
zIndex: Z_BLOCK zIndex: Z_BLOCK
}; };
}), } else {
...schema.operations.map(operation => ({ return {
id: String(operation.id), id: item.nodeID,
type: operation.operation_type.toString(), type: item.operation_type.toString(),
data: { label: operation.alias, operation: operation }, data: { label: item.alias, operation: item },
position: computeRelativePosition(schema, { x: operation.x, y: operation.y }, operation.parent), position: computeRelativePosition(schema, { x: item.x, y: item.y }, item.parent),
parentId: operation.parent ? `-${operation.parent}` : undefined, parentId: item.parent ? constructNodeID(NodeType.BLOCK, item.parent) : undefined,
zIndex: Z_SCHEMA zIndex: Z_SCHEMA
})) };
]; }
});
const newEdges: Edge[] = schema.arguments.map((argument, index) => ({ const newEdges: Edge[] = schema.arguments.map((argument, index) => {
const source = schema.operationByID.get(argument.argument)!;
const target = schema.operationByID.get(argument.operation)!;
return {
id: String(index), id: String(index),
source: String(argument.argument), source: source.nodeID,
target: String(argument.operation), target: target.nodeID,
type: edgeStraight ? 'straight' : 'simplebezier', type: edgeStraight ? 'straight' : 'simplebezier',
animated: edgeAnimate, animated: edgeAnimate,
targetHandle: targetHandle: source.x > target.x ? 'right' : 'left'
schema.operationByID.get(argument.argument)!.x > schema.operationByID.get(argument.operation)!.x };
? 'right' });
: 'left'
}));
setNodes(newNodes); setNodes(newNodes);
setEdges(newEdges); setEdges(newEdges);

View File

@ -12,6 +12,7 @@ import { promptText } from '@/utils/labels';
import { useDeleteBlock } from '../../../backend/use-delete-block'; import { useDeleteBlock } from '../../../backend/use-delete-block';
import { useMutatingOss } from '../../../backend/use-mutating-oss'; import { useMutatingOss } from '../../../backend/use-mutating-oss';
import { useUpdateLayout } from '../../../backend/use-update-layout'; import { useUpdateLayout } from '../../../backend/use-update-layout';
import { type IOssItem, NodeType } from '../../../models/oss';
import { type OssNode, type Position2D } from '../../../models/oss-layout'; import { type OssNode, type Position2D } from '../../../models/oss-layout';
import { GRID_SIZE, LayoutManager } from '../../../models/oss-layout-api'; import { GRID_SIZE, LayoutManager } from '../../../models/oss-layout-api';
import { useOSSGraphStore } from '../../../stores/oss-graph'; import { useOSSGraphStore } from '../../../stores/oss-graph';
@ -41,7 +42,7 @@ export const flowOptions = {
export function OssFlow() { export function OssFlow() {
const mainHeight = useMainHeight(); const mainHeight = useMainHeight();
const { navigateOperationSchema, schema, selected, isMutable, canDeleteOperation } = useOssEdit(); const { navigateOperationSchema, schema, selected, selectedItems, isMutable, canDeleteOperation } = useOssEdit();
const { screenToFlowPosition } = useReactFlow(); const { screenToFlowPosition } = useReactFlow();
const { containMovement, nodes, onNodesChange, edges, onEdgesChange, resetGraph, resetView } = useOssFlow(); const { containMovement, nodes, onNodesChange, edges, onEdgesChange, resetGraph, resetView } = useOssFlow();
const store = useStoreApi(); const store = useStoreApi();
@ -76,20 +77,21 @@ export function OssFlow() {
manager: new LayoutManager(schema, getLayout()), manager: new LayoutManager(schema, getLayout()),
defaultX: targetPosition.x, defaultX: targetPosition.x,
defaultY: targetPosition.y, defaultY: targetPosition.y,
initialInputs: selected.filter(id => id > 0), initialInputs: selectedItems.filter(item => item?.nodeType === NodeType.OPERATION).map(item => item.id),
initialParent: extractSingleBlock(selected), initialParent: extractBlockParent(selectedItems),
onCreate: resetView onCreate: resetView
}); });
} }
function handleCreateBlock() { function handleCreateBlock() {
const targetPosition = screenToFlowPosition({ x: window.innerWidth / 2, y: window.innerHeight / 2 }); const targetPosition = screenToFlowPosition({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
const parent = extractSingleBlock(selected); const parent = extractBlockParent(selectedItems);
showCreateBlock({ showCreateBlock({
manager: new LayoutManager(schema, getLayout()), manager: new LayoutManager(schema, getLayout()),
defaultX: targetPosition.x, defaultX: targetPosition.x,
defaultY: targetPosition.y, defaultY: targetPosition.y,
initialChildren: parent !== null ? [] : selected, initialChildren:
parent !== null && selectedItems.length === 1 && parent === selectedItems[0].id ? [] : selectedItems,
initialParent: parent, initialParent: parent,
onCreate: resetView onCreate: resetView
}); });
@ -99,25 +101,24 @@ export function OssFlow() {
if (selected.length !== 1) { if (selected.length !== 1) {
return; return;
} }
if (selected[0] > 0) { const item = schema.itemByNodeID.get(selected[0]);
const operation = schema.operationByID.get(selected[0]); if (!item) {
if (!operation || !canDeleteOperation(operation)) { return;
}
if (item.nodeType === NodeType.OPERATION) {
if (!canDeleteOperation(item)) {
return; return;
} }
showDeleteOperation({ showDeleteOperation({
oss: schema, oss: schema,
target: operation, target: item,
layout: getLayout() layout: getLayout()
}); });
} else { } else {
const block = schema.blockByID.get(-selected[0]);
if (!block) {
return;
}
if (!window.confirm(promptText.deleteBlock)) { if (!window.confirm(promptText.deleteBlock)) {
return; return;
} }
void deleteBlock({ itemID: schema.id, data: { target: block.id, layout: getLayout() } }); void deleteBlock({ itemID: schema.id, data: { target: item.id, layout: getLayout() } });
} }
} }
@ -219,7 +220,10 @@ export function OssFlow() {
} }
// -------- Internals -------- // -------- Internals --------
function extractSingleBlock(selected: number[]): number | null { function extractBlockParent(selectedItems: IOssItem[]): number | null {
const blocks = selected.filter(id => id < 0); if (selectedItems.length === 1 && selectedItems[0].nodeType === NodeType.BLOCK) {
return blocks.length === 1 ? -blocks[0] : null; return selectedItems[0].id;
}
const parents = selectedItems.map(item => item.parent).filter(id => id !== null);
return parents.length === 0 ? null : parents[0];
} }

View File

@ -27,6 +27,7 @@ import { OperationType } from '../../../backend/types';
import { useExecuteOperation } from '../../../backend/use-execute-operation'; import { useExecuteOperation } from '../../../backend/use-execute-operation';
import { useMutatingOss } from '../../../backend/use-mutating-oss'; import { useMutatingOss } from '../../../backend/use-mutating-oss';
import { useUpdateLayout } from '../../../backend/use-update-layout'; import { useUpdateLayout } from '../../../backend/use-update-layout';
import { NodeType } from '../../../models/oss';
import { LayoutManager } from '../../../models/oss-layout-api'; import { LayoutManager } from '../../../models/oss-layout-api';
import { useOssEdit } from '../oss-edit-context'; import { useOssEdit } from '../oss-edit-context';
@ -48,11 +49,13 @@ export function ToolbarOssGraph({
className, className,
...restProps ...restProps
}: ToolbarOssGraphProps) { }: ToolbarOssGraphProps) {
const { schema, selected, isMutable, canDeleteOperation: canDelete } = useOssEdit(); const { schema, selectedItems, isMutable, canDeleteOperation: canDelete } = useOssEdit();
const isProcessing = useMutatingOss(); const isProcessing = useMutatingOss();
const { resetView } = useOssFlow(); const { resetView } = useOssFlow();
const selectedOperation = selected.length !== 1 ? null : schema.operationByID.get(selected[0]) ?? null; const selectedOperation =
const selectedBlock = selected.length !== 1 ? null : schema.blockByID.get(-selected[0]) ?? null; selectedItems.length === 1 && selectedItems[0].nodeType === NodeType.OPERATION ? selectedItems[0] : null;
const selectedBlock =
selectedItems.length === 1 && selectedItems[0].nodeType === NodeType.BLOCK ? selectedItems[0] : null;
const getLayout = useGetLayout(); const getLayout = useGetLayout();
const { updateLayout } = useUpdateLayout(); const { updateLayout } = useUpdateLayout();
@ -145,7 +148,9 @@ export function ToolbarOssGraph({
title='Исправить позиции узлов' title='Исправить позиции узлов'
icon={<IconFixLayout size='1.25rem' className='icon-primary' />} icon={<IconFixLayout size='1.25rem' className='icon-primary' />}
onClick={handleFixLayout} onClick={handleFixLayout}
disabled={selected.length > 1 || selected[0] > 0} disabled={
selectedItems.length > 1 || (selectedItems.length > 0 && selectedItems[0].nodeType === NodeType.OPERATION)
}
/> />
<MiniButton <MiniButton
title='Настройки отображения' title='Настройки отображения'
@ -181,14 +186,14 @@ export function ToolbarOssGraph({
title='Активировать операцию' title='Активировать операцию'
icon={<IconExecute size='1.25rem' className='icon-green' />} icon={<IconExecute size='1.25rem' className='icon-green' />}
onClick={handleOperationExecute} onClick={handleOperationExecute}
disabled={isProcessing || selected.length !== 1 || !readyForSynthesis} disabled={isProcessing || selectedItems.length !== 1 || !readyForSynthesis}
/> />
<MiniButton <MiniButton
titleHtml={prepareTooltip('Редактировать выбранную', 'Двойной клик')} titleHtml={prepareTooltip('Редактировать выбранную', 'Двойной клик')}
aria-label='Редактировать выбранную' aria-label='Редактировать выбранную'
icon={<IconEdit2 size='1.25rem' className='icon-primary' />} icon={<IconEdit2 size='1.25rem' className='icon-primary' />}
onClick={handleEditItem} onClick={handleEditItem}
disabled={selected.length !== 1 || isProcessing} disabled={selectedItems.length !== 1 || isProcessing}
/> />
<MiniButton <MiniButton
titleHtml={prepareTooltip('Удалить выбранную', 'Delete')} titleHtml={prepareTooltip('Удалить выбранную', 'Delete')}

View File

@ -1,10 +1,10 @@
import { type Node } from 'reactflow'; import { type Node } from 'reactflow';
import { useMoveItems } from '@/features/oss/backend/use-move-items';
import { useThrottleCallback } from '@/hooks/use-throttle-callback'; import { useThrottleCallback } from '@/hooks/use-throttle-callback';
import { useDraggingStore } from '@/stores/dragging'; import { useDraggingStore } from '@/stores/dragging';
import { useMoveItems } from '../../../backend/use-move-items';
import { NodeType } from '../../../models/oss';
import { useOssEdit } from '../oss-edit-context'; import { useOssEdit } from '../oss-edit-context';
import { useOssFlow } from './oss-flow-context'; import { useOssFlow } from './oss-flow-context';
@ -21,6 +21,7 @@ interface DraggingProps {
export function useDragging({ hideContextMenu }: DraggingProps) { export function useDragging({ hideContextMenu }: DraggingProps) {
const { setContainMovement, containMovement, setNodes } = useOssFlow(); const { setContainMovement, containMovement, setNodes } = useOssFlow();
const setIsDragging = useDraggingStore(state => state.setIsDragging); const setIsDragging = useDraggingStore(state => state.setIsDragging);
const isDragging = useDraggingStore(state => state.isDragging);
const getLayout = useGetLayout(); const getLayout = useGetLayout();
const { selected, schema } = useOssEdit(); const { selected, schema } = useOssEdit();
const dropTarget = useDropTarget(); const dropTarget = useDropTarget();
@ -43,7 +44,7 @@ export function useDragging({ hideContextMenu }: DraggingProps) {
function handleDragStart(event: React.MouseEvent, target: Node) { function handleDragStart(event: React.MouseEvent, target: Node) {
if (event.shiftKey) { if (event.shiftKey) {
setContainMovement(true); setContainMovement(true);
applyContainMovement([target.id, ...selected.map(id => String(id))], true); applyContainMovement([target.id, ...selected], true);
} else { } else {
setContainMovement(false); setContainMovement(false);
dropTarget.update(event); dropTarget.update(event);
@ -61,24 +62,20 @@ export function useDragging({ hideContextMenu }: DraggingProps) {
function handleDragStop(event: React.MouseEvent, target: Node) { function handleDragStop(event: React.MouseEvent, target: Node) {
if (containMovement) { if (containMovement) {
applyContainMovement([target.id, ...selected.map(id => String(id))], false); applyContainMovement([target.id, ...selected], false);
} else { } else {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
if (isDragging) {
setIsDragging(false);
const new_parent = dropTarget.evaluate(event); const new_parent = dropTarget.evaluate(event);
const allSelected = [...selected.filter(id => id != Number(target.id)), Number(target.id)]; const allSelected = [...selected.filter(id => id != target.id), target.id].map(id =>
const operations = allSelected schema.itemByNodeID.get(id)
.filter(id => id > 0)
.map(id => schema.operationByID.get(id))
.filter(operation => !!operation);
const blocks = allSelected
.filter(id => id < 0)
.map(id => schema.blockByID.get(-id))
.filter(operation => !!operation);
const parents = new Set(
[...blocks.map(block => block.parent), ...operations.map(operation => operation.parent)].filter(id => !!id)
); );
const parents = new Set(allSelected.map(item => item?.parent).filter(id => !!id));
const operations = allSelected.filter(item => item?.nodeType === NodeType.OPERATION);
const blocks = allSelected.filter(item => item?.nodeType === NodeType.BLOCK);
if ( if (
(parents.size !== 1 || parents.values().next().value !== new_parent) && (parents.size !== 1 || parents.values().next().value !== new_parent) &&
(parents.size !== 0 || new_parent !== null) (parents.size !== 0 || new_parent !== null)
@ -94,8 +91,8 @@ export function useDragging({ hideContextMenu }: DraggingProps) {
}); });
} }
} }
}
setIsDragging(false);
setContainMovement(false); setContainMovement(false);
dropTarget.reset(); dropTarget.reset();
} }

View File

@ -1,5 +1,7 @@
import { useReactFlow } from 'reactflow'; import { useReactFlow } from 'reactflow';
import { NodeType } from '@/features/oss/models/oss';
import { useDraggingStore } from '@/stores/dragging'; import { useDraggingStore } from '@/stores/dragging';
import { useOssEdit } from '../oss-edit-context'; import { useOssEdit } from '../oss-edit-context';
@ -7,7 +9,7 @@ import { useOssEdit } from '../oss-edit-context';
/** Hook to encapsulate drop target logic. */ /** Hook to encapsulate drop target logic. */
export function useDropTarget() { export function useDropTarget() {
const { getIntersectingNodes, screenToFlowPosition } = useReactFlow(); const { getIntersectingNodes, screenToFlowPosition } = useReactFlow();
const { selected, schema } = useOssEdit(); const { selectedItems, selected, schema } = useOssEdit();
const dropTarget = useDraggingStore(state => state.dropTarget); const dropTarget = useDraggingStore(state => state.dropTarget);
const setDropTarget = useDraggingStore(state => state.setDropTarget); const setDropTarget = useDraggingStore(state => state.setDropTarget);
@ -19,17 +21,16 @@ export function useDropTarget() {
width: 1, width: 1,
height: 1 height: 1
}) })
.map(node => Number(node.id)) .filter(node => !selected.includes(node.id))
.filter(id => id < 0 && !selected.includes(id)) .map(node => schema.itemByNodeID.get(node.id))
.map(id => schema.blockByID.get(-id)) .filter(item => item?.nodeType === NodeType.BLOCK);
.filter(block => !!block);
if (blocks.length === 0) { if (blocks.length === 0) {
return null; return null;
} }
const successors = schema.hierarchy.expandAllOutputs([...selected]).filter(id => id < 0); const successors = schema.hierarchy.expandAllOutputs(selectedItems.map(item => item.nodeID));
blocks = blocks.filter(block => !successors.includes(-block.id)); blocks = blocks.filter(block => !successors.includes(block.nodeID));
if (blocks.length === 0) { if (blocks.length === 0) {
return null; return null;
} }

View File

@ -18,13 +18,13 @@ export function useGetLayout() {
operations: nodes operations: nodes
.filter(node => node.type !== 'block') .filter(node => node.type !== 'block')
.map(node => ({ .map(node => ({
id: Number(node.id), id: schema.itemByNodeID.get(node.id)!.id,
...computeAbsolutePosition(node, schema, nodeById) ...computeAbsolutePosition(node, schema, nodeById)
})), })),
blocks: nodes blocks: nodes
.filter(node => node.type === 'block') .filter(node => node.type === 'block')
.map(node => ({ .map(node => ({
id: -Number(node.id), id: schema.itemByNodeID.get(node.id)!.id,
...computeAbsolutePosition(node, schema, nodeById), ...computeAbsolutePosition(node, schema, nodeById),
width: node.width ?? BLOCK_NODE_MIN_WIDTH, width: node.width ?? BLOCK_NODE_MIN_WIDTH,
height: node.height ?? BLOCK_NODE_MIN_HEIGHT height: node.height ?? BLOCK_NODE_MIN_HEIGHT
@ -35,7 +35,7 @@ export function useGetLayout() {
// ------- Internals ------- // ------- Internals -------
function computeAbsolutePosition(target: Node, schema: IOperationSchema, nodeById: Map<string, Node>): Position2D { function computeAbsolutePosition(target: Node, schema: IOperationSchema, nodeById: Map<string, Node>): Position2D {
const nodes = schema.hierarchy.expandAllInputs([Number(target.id)]); const nodes = schema.hierarchy.expandAllInputs([target.id]);
let x = target.position.x; let x = target.position.x;
let y = target.position.y; let y = target.position.y;
for (const nodeID of nodes) { for (const nodeID of nodes) {

View File

@ -2,7 +2,7 @@
import { createContext, use } from 'react'; import { createContext, use } from 'react';
import { type IOperation, type IOperationSchema } from '../../models/oss'; import { type IOperation, type IOperationSchema, type IOssItem } from '../../models/oss';
export const OssTabID = { export const OssTabID = {
CARD: 0, CARD: 0,
@ -12,7 +12,8 @@ export type OssTabID = (typeof OssTabID)[keyof typeof OssTabID];
interface IOssEditContext { interface IOssEditContext {
schema: IOperationSchema; schema: IOperationSchema;
selected: number[]; selected: string[];
selectedItems: IOssItem[];
isOwned: boolean; isOwned: boolean;
isMutable: boolean; isMutable: boolean;
@ -22,7 +23,7 @@ interface IOssEditContext {
canDeleteOperation: (target: IOperation) => boolean; canDeleteOperation: (target: IOperation) => boolean;
deleteSchema: () => void; deleteSchema: () => void;
setSelected: React.Dispatch<React.SetStateAction<number[]>>; setSelected: React.Dispatch<React.SetStateAction<string[]>>;
} }
export const OssEditContext = createContext<IOssEditContext | null>(null); export const OssEditContext = createContext<IOssEditContext | null>(null);

View File

@ -38,7 +38,8 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
const isMutable = role > UserRole.READER && !schema.read_only; const isMutable = role > UserRole.READER && !schema.read_only;
const isEditor = !!user.id && schema.editors.includes(user.id); const isEditor = !!user.id && schema.editors.includes(user.id);
const [selected, setSelected] = useState<number[]>([]); const [selected, setSelected] = useState<string[]>([]);
const selectedItems = selected.map(id => schema.itemByNodeID.get(id)).filter(item => !!item);
const { deleteItem } = useDeleteItem(); const { deleteItem } = useDeleteItem();
@ -92,6 +93,7 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
value={{ value={{
schema, schema,
selected, selected,
selectedItems,
isOwned, isOwned,
isMutable, isMutable,

View File

@ -13,11 +13,11 @@ export const useCreateConstituenta = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [KEYS.global_mutation, rsformsApi.baseKey, 'create-constituenta'], mutationKey: [KEYS.global_mutation, rsformsApi.baseKey, 'create-constituenta'],
mutationFn: rsformsApi.createConstituenta, mutationFn: rsformsApi.createConstituenta,
onSuccess: data => { onSuccess: async data => {
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.schema.id }).queryKey, data.schema); client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.schema.id }).queryKey, data.schema);
updateTimestamp(data.schema.id); updateTimestamp(data.schema.id);
return Promise.allSettled([ await Promise.allSettled([
client.invalidateQueries({ queryKey: [KEYS.oss] }), client.invalidateQueries({ queryKey: [KEYS.oss] }),
client.invalidateQueries({ client.invalidateQueries({
queryKey: [rsformsApi.baseKey], queryKey: [rsformsApi.baseKey],

View File

@ -13,11 +13,11 @@ export const useDeleteConstituents = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [KEYS.global_mutation, rsformsApi.baseKey, 'delete-constituents'], mutationKey: [KEYS.global_mutation, rsformsApi.baseKey, 'delete-constituents'],
mutationFn: rsformsApi.deleteConstituents, mutationFn: rsformsApi.deleteConstituents,
onSuccess: data => { onSuccess: async data => {
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data); client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data);
updateTimestamp(data.id); updateTimestamp(data.id);
return Promise.allSettled([ await Promise.allSettled([
client.invalidateQueries({ queryKey: [KEYS.oss] }), client.invalidateQueries({ queryKey: [KEYS.oss] }),
client.invalidateQueries({ client.invalidateQueries({
queryKey: [rsformsApi.baseKey], queryKey: [rsformsApi.baseKey],

View File

@ -13,11 +13,11 @@ export const useInlineSynthesis = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [KEYS.global_mutation, rsformsApi.baseKey, 'inline-synthesis'], mutationKey: [KEYS.global_mutation, rsformsApi.baseKey, 'inline-synthesis'],
mutationFn: rsformsApi.inlineSynthesis, mutationFn: rsformsApi.inlineSynthesis,
onSuccess: data => { onSuccess: async data => {
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data); client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data);
updateTimestamp(data.id); updateTimestamp(data.id);
return Promise.allSettled([ await Promise.allSettled([
client.invalidateQueries({ queryKey: [KEYS.oss] }), client.invalidateQueries({ queryKey: [KEYS.oss] }),
client.invalidateQueries({ client.invalidateQueries({
queryKey: [rsformsApi.baseKey], queryKey: [rsformsApi.baseKey],

View File

@ -12,11 +12,11 @@ export const useProduceStructure = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [KEYS.global_mutation, rsformsApi.baseKey, 'produce-structure'], mutationKey: [KEYS.global_mutation, rsformsApi.baseKey, 'produce-structure'],
mutationFn: rsformsApi.produceStructure, mutationFn: rsformsApi.produceStructure,
onSuccess: data => { onSuccess: async data => {
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.schema.id }).queryKey, data.schema); client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.schema.id }).queryKey, data.schema);
updateTimestamp(data.schema.id); updateTimestamp(data.schema.id);
return Promise.allSettled([ await Promise.allSettled([
client.invalidateQueries({ queryKey: [KEYS.oss] }), client.invalidateQueries({ queryKey: [KEYS.oss] }),
client.invalidateQueries({ client.invalidateQueries({
queryKey: [rsformsApi.baseKey], queryKey: [rsformsApi.baseKey],

View File

@ -13,11 +13,11 @@ export const useRenameConstituenta = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [KEYS.global_mutation, rsformsApi.baseKey, 'rename-constituenta'], mutationKey: [KEYS.global_mutation, rsformsApi.baseKey, 'rename-constituenta'],
mutationFn: rsformsApi.renameConstituenta, mutationFn: rsformsApi.renameConstituenta,
onSuccess: data => { onSuccess: async data => {
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.schema.id }).queryKey, data.schema); client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.schema.id }).queryKey, data.schema);
updateTimestamp(data.schema.id); updateTimestamp(data.schema.id);
return Promise.allSettled([ await Promise.allSettled([
client.invalidateQueries({ queryKey: [KEYS.oss] }), client.invalidateQueries({ queryKey: [KEYS.oss] }),
client.invalidateQueries({ client.invalidateQueries({
queryKey: [rsformsApi.baseKey], queryKey: [rsformsApi.baseKey],

View File

@ -12,11 +12,11 @@ export const useResetAliases = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [KEYS.global_mutation, rsformsApi.baseKey, 'reset-aliases'], mutationKey: [KEYS.global_mutation, rsformsApi.baseKey, 'reset-aliases'],
mutationFn: rsformsApi.resetAliases, mutationFn: rsformsApi.resetAliases,
onSuccess: data => { onSuccess: async data => {
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data); client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data);
updateTimestamp(data.id); updateTimestamp(data.id);
return Promise.allSettled([ await Promise.allSettled([
client.invalidateQueries({ queryKey: [KEYS.oss] }), client.invalidateQueries({ queryKey: [KEYS.oss] }),
client.invalidateQueries({ client.invalidateQueries({
queryKey: [rsformsApi.baseKey], queryKey: [rsformsApi.baseKey],

View File

@ -13,11 +13,11 @@ export const useSubstituteConstituents = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [KEYS.global_mutation, rsformsApi.baseKey, 'substitute-constituents'], mutationKey: [KEYS.global_mutation, rsformsApi.baseKey, 'substitute-constituents'],
mutationFn: rsformsApi.substituteConstituents, mutationFn: rsformsApi.substituteConstituents,
onSuccess: data => { onSuccess: async data => {
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data); client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data);
updateTimestamp(data.id); updateTimestamp(data.id);
return Promise.allSettled([ await Promise.allSettled([
client.invalidateQueries({ queryKey: [KEYS.oss] }), client.invalidateQueries({ queryKey: [KEYS.oss] }),
client.invalidateQueries({ client.invalidateQueries({
queryKey: [rsformsApi.baseKey], queryKey: [rsformsApi.baseKey],

View File

@ -13,10 +13,10 @@ export const useUpdateConstituenta = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [KEYS.global_mutation, rsformsApi.baseKey, 'update-constituenta'], mutationKey: [KEYS.global_mutation, rsformsApi.baseKey, 'update-constituenta'],
mutationFn: rsformsApi.updateConstituenta, mutationFn: rsformsApi.updateConstituenta,
onSuccess: (_, variables) => { onSuccess: async (_, variables) => {
updateTimestamp(variables.itemID); updateTimestamp(variables.itemID);
return Promise.allSettled([ await Promise.allSettled([
client.invalidateQueries({ queryKey: [KEYS.oss] }), client.invalidateQueries({ queryKey: [KEYS.oss] }),
client.invalidateQueries({ queryKey: [rsformsApi.baseKey] }) client.invalidateQueries({ queryKey: [rsformsApi.baseKey] })
]); ]);

View File

@ -12,13 +12,13 @@ export const useUploadTRS = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [KEYS.global_mutation, rsformsApi.baseKey, 'load-trs'], mutationKey: [KEYS.global_mutation, rsformsApi.baseKey, 'load-trs'],
mutationFn: rsformsApi.upload, mutationFn: rsformsApi.upload,
onSuccess: data => { onSuccess: async data => {
client.setQueryData(KEYS.composite.rsItem({ itemID: data.id }), data); client.setQueryData(KEYS.composite.rsItem({ itemID: data.id }), data);
client.setQueryData(KEYS.composite.libraryList, (prev: ILibraryItem[] | undefined) => client.setQueryData(KEYS.composite.libraryList, (prev: ILibraryItem[] | undefined) =>
prev?.map(item => (item.id === data.id ? data : item)) prev?.map(item => (item.id === data.id ? data : item))
); );
return Promise.allSettled([ await Promise.allSettled([
client.invalidateQueries({ queryKey: [KEYS.oss] }), client.invalidateQueries({ queryKey: [KEYS.oss] }),
client.invalidateQueries({ client.invalidateQueries({
queryKey: [rsformsApi.baseKey], queryKey: [rsformsApi.baseKey],

View File

@ -1,20 +1,18 @@
// This file was generated by lezer-generator. You probably shouldn't edit it. // This file was generated by lezer-generator. You probably shouldn't edit it.
import { LRParser } from '@lezer/lr'; import {LRParser} from "@lezer/lr"
import { highlighting } from './highlight'; import {highlighting} from "./highlight"
export const parser = LRParser.deserialize({ export const parser = LRParser.deserialize({
version: 14, version: 14,
states: states: "$nQVQPOOObQQO'#C^OOQO'#Cl'#ClOjQPO'#CuOxQPO'#CdOOQO'#Ce'#CeOOQO'#Ck'#CkOOQO'#Cf'#CfQVQPOOO}QPO,58xO!SQPO,58{OOQO,59a,59aO!XQPO,59OOOQO-E6d-E6dO!^QSO1G.dO!cQPO1G.gOOQO1G.j1G.jO!hQQO'#CpOOQO'#C`'#C`O!pQPO7+$OOOQO'#Cg'#CgO!uQPO'#CcO!}QPO7+$RO!^QSO,59[OOQO<<Gj<<GjOOQO-E6e-E6eOOQO<<Gm<<GmOOQO1G.v1G.v",
"$nQVQPOOObQQO'#C^OOQO'#Cl'#ClOjQPO'#CuOxQPO'#CdOOQO'#Ce'#CeOOQO'#Ck'#CkOOQO'#Cf'#CfQVQPOOO}QPO,58xO!SQPO,58{OOQO,59a,59aO!XQPO,59OOOQO-E6d-E6dO!^QSO1G.dO!cQPO1G.gOOQO1G.j1G.jO!hQQO'#CpOOQO'#C`'#C`O!pQPO7+$OOOQO'#Cg'#CgO!uQPO'#CcO!}QPO7+$RO!^QSO,59[OOQO<<Gj<<GjOOQO-E6e-E6eOOQO<<Gm<<GmOOQO1G.v1G.v", stateData: "#V~O^OS~ObPOgROhSO~ORXOUYO~OgRO[iXbiXhiX~OgRO~Oc^O~Oc_O~Oh`O~OeaO~OgdO~OfgOadX~OahO~OgdOaVX~OajO~Og^~",
stateData: '#V~O^OS~ObPOgROhSO~ORXOUYO~OgRO[iXbiXhiX~OgRO~Oc^O~Oc_O~Oh`O~OeaO~OgdO~OfgOadX~OahO~OgdOaVX~OajO~Og^~', goto: "!kjPPkPokPruuy!PPPP!VuPPP!ZPPPP!aTQOWRc^Rf_TUOWQWOR]WQe_RieTVOWQb^RkgSTOWQZRR[S",
goto: '!kjPPkPokPruuy!PPPP!VuPPP!ZPPPP!aTQOWRc^Rf_TUOWQWOR]WQe_RieTVOWQb^RkgSTOWQZRR[S', nodeNames: "⚠ Text RefEntity Global Grams RefSyntactic Offset Nominal Error Filler",
nodeNames: '⚠ Text RefEntity Global Grams RefSyntactic Offset Nominal Error Filler',
maxTerm: 25, maxTerm: 25,
propSources: [highlighting], propSources: [highlighting],
skippedNodes: [0], skippedNodes: [0],
repeatNodeCount: 2, repeatNodeCount: 2,
tokenData: tokenData: ".V~R!UOX$eXZ%VZ^%z^p$epq%Vqr'sr|$e|}'x}!O(f!O!Q$e!Q!R)i!R![*i![!b$e!b!c+k!c!d+v!d!e)i!e!f+v!f!g+v!g!h)i!h!i+v!i!r)i!r!s+v!s!t)i!t!u+v!u!v+v!v!w+v!w!z)i!z!{+v!{!})i!}#T$e#T#o)i#p#q-{#q#r.Q#r#y$e#y#z%z#z$f$e$f$g%z$g#BY$e#BY#BZ%z#BZ$IS$e$IS$I_%z$I_$I|$e$I|$JO%z$JO$JT$e$JT$JU%z$JU$KV$e$KV$KW%z$KW&FU$e&FU&FV%z&FV;'S$e;'S;=`%P<%lO$eP$jVgPOX$eZp$er!b$e!c#o$e#r;'S$e;'S;=`%P<%lO$eP%SP;=`<%l$e~%[Y^~X^%Vpq%V#y#z%V$f$g%V#BY#BZ%V$IS$I_%V$I|$JO%V$JT$JU%V$KV$KW%V&FU&FV%V~&RjgP^~OX$eXZ%VZ^%z^p$epq%Vr!b$e!c#o$e#r#y$e#y#z%z#z$f$e$f$g%z$g#BY$e#BY#BZ%z#BZ$IS$e$IS$I_%z$I_$I|$e$I|$JO%z$JO$JT$e$JT$JU%z$JU$KV$e$KV$KW%z$KW&FU$e&FU&FV%z&FV;'S$e;'S;=`%P<%lO$e~'xOh~R(PVfQgPOX$eZp$er!b$e!c#o$e#r;'S$e;'S;=`%P<%lO$eV(m^eSgPOX$eZp$er}$e}!O)i!O!Q$e!Q!R)i!R![*i![!b$e!c!})i!}#T$e#T#o)i#r;'S$e;'S;=`%P<%lO$eT)p]eSgPOX$eZp$er}$e}!O)i!O!Q$e!Q![)i![!b$e!c!})i!}#T$e#T#o)i#r;'S$e;'S;=`%P<%lO$eV*r]UQeSgPOX$eZp$er}$e}!O)i!O!Q$e!Q![*i![!b$e!c!})i!}#T$e#T#o)i#r;'S$e;'S;=`%P<%lO$e~+nP#o#p+q~+vOb~V+}^eSgPOX$eZp$er}$e}!O)i!O!Q$e!Q!R)i!R![,y![!b$e!c!})i!}#T$e#T#o)i#r;'S$e;'S;=`%P<%lO$eV-S]RQeSgPOX$eZp$er}$e}!O)i!O!Q$e!Q![,y![!b$e!c!})i!}#T$e#T#o)i#r;'S$e;'S;=`%P<%lO$e~.QOc~~.VOa~",
".V~R!UOX$eXZ%VZ^%z^p$epq%Vqr'sr|$e|}'x}!O(f!O!Q$e!Q!R)i!R![*i![!b$e!b!c+k!c!d+v!d!e)i!e!f+v!f!g+v!g!h)i!h!i+v!i!r)i!r!s+v!s!t)i!t!u+v!u!v+v!v!w+v!w!z)i!z!{+v!{!})i!}#T$e#T#o)i#p#q-{#q#r.Q#r#y$e#y#z%z#z$f$e$f$g%z$g#BY$e#BY#BZ%z#BZ$IS$e$IS$I_%z$I_$I|$e$I|$JO%z$JO$JT$e$JT$JU%z$JU$KV$e$KV$KW%z$KW&FU$e&FU&FV%z&FV;'S$e;'S;=`%P<%lO$eP$jVgPOX$eZp$er!b$e!c#o$e#r;'S$e;'S;=`%P<%lO$eP%SP;=`<%l$e~%[Y^~X^%Vpq%V#y#z%V$f$g%V#BY#BZ%V$IS$I_%V$I|$JO%V$JT$JU%V$KV$KW%V&FU&FV%V~&RjgP^~OX$eXZ%VZ^%z^p$epq%Vr!b$e!c#o$e#r#y$e#y#z%z#z$f$e$f$g%z$g#BY$e#BY#BZ%z#BZ$IS$e$IS$I_%z$I_$I|$e$I|$JO%z$JO$JT$e$JT$JU%z$JU$KV$e$KV$KW%z$KW&FU$e&FU&FV%z&FV;'S$e;'S;=`%P<%lO$e~'xOh~R(PVfQgPOX$eZp$er!b$e!c#o$e#r;'S$e;'S;=`%P<%lO$eV(m^eSgPOX$eZp$er}$e}!O)i!O!Q$e!Q!R)i!R![*i![!b$e!c!})i!}#T$e#T#o)i#r;'S$e;'S;=`%P<%lO$eT)p]eSgPOX$eZp$er}$e}!O)i!O!Q$e!Q![)i![!b$e!c!})i!}#T$e#T#o)i#r;'S$e;'S;=`%P<%lO$eV*r]UQeSgPOX$eZp$er}$e}!O)i!O!Q$e!Q![*i![!b$e!c!})i!}#T$e#T#o)i#r;'S$e;'S;=`%P<%lO$e~+nP#o#p+q~+vOb~V+}^eSgPOX$eZp$er}$e}!O)i!O!Q$e!Q!R)i!R![,y![!b$e!c!})i!}#T$e#T#o)i#r;'S$e;'S;=`%P<%lO$eV-S]RQeSgPOX$eZp$er}$e}!O)i!O!Q$e!Q![,y![!b$e!c!})i!}#T$e#T#o)i#r;'S$e;'S;=`%P<%lO$e~.QOc~~.VOa~",
tokenizers: [0, 1, 2], tokenizers: [0, 1, 2],
topRules: { Text: [0, 1] }, topRules: {"Text":[0,1]},
tokenPrec: 96 tokenPrec: 96
}); })

View File

@ -1,6 +1,5 @@
// This file was generated by lezer-generator. You probably shouldn't edit it. // This file was generated by lezer-generator. You probably shouldn't edit it.
export const export const Expression = 1,
Expression = 1,
Logic = 2, Logic = 2,
Logic_predicates = 3, Logic_predicates = 3,
Variable = 4, Variable = 4,
@ -43,4 +42,4 @@ export const
Logic_binary = 76, Logic_binary = 76,
Function_decl = 81, Function_decl = 81,
Arguments = 82, Arguments = 82,
Declaration = 83 Declaration = 83;

View File

@ -1,20 +1,24 @@
// This file was generated by lezer-generator. You probably shouldn't edit it. // This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser} from "@lezer/lr" import { LRParser } from '@lezer/lr';
export const parser = LRParser.deserialize({ export const parser = LRParser.deserialize({
version: 14, version: 14,
states: "2xO!sQPOOOOQO'#Ch'#ChO#lQPO'#EWOOQO'#EW'#EWO%yQPO'#EVOVQPO'#CnO'_QPO'#CzO'fQPO'#C{O'nQPO'#C}O'sQPO'#DRO'xQPO'#DWO'}QPO'#D[OOQO'#Cw'#CwO(SQPO'#CwOOQO'#D_'#D_OOQO'#Cg'#CgO)pQPO'#CgO)uQPO'#CgO)zQPO'#C_OVQPO'#DsO*SQPO'#DvOOQO'#Dr'#DrO*[QPO'#DrO*aQPO'#EVOOQO'#C^'#C^O*rQPO'#EPQOQPOOO*wQPO,59YO*wQPO,59YO*wQPO,59YO*wQPO,59YO*wQPO,59YO*wQPO,59YO*wQPO,59YO*wQPO,59YO*wQPO,58yO+OQPO'#CfO+VQPO,59YO,jQPO,59OO,rQPO'#CeO,wQPO,58xO-YQPO,59mO-{QPO'#CfO*wQPO'#CnO.YQPO,59fOOQO'#Cf'#CfO*wQPO,59gOOQO,59g,59gO*wQPO,59iO*SQPO,59mO*wQPO,59rO*SQPO,59vO*wQPO,59RO*wQPO,59RO$lQPO'#C_OOQO,5:_,5:_O*wQPO'#CdOOQO'#C`'#C`OOQO'#Dx'#DxO.bQPO,5:bO*wQPO,5:^OVQPO,5:fOVQPO,5:fOVQPO,5:fOVQPO,5:fO.jQPO'#EROOQO'#EQ'#EQO.oQPO,5:kOOQO1G.t1G.tO2`QPO1G.tO2gQPO1G.tO6VQPO1G.tO9uQPO1G.tO9|QPO1G.tO:TQPO1G.tO:[QPO1G.tO=vQPO1G.eOOQO1G.j1G.jO*wQPO,59POOQO1G.d1G.dO*wQPO1G/XOOQO1G/Q1G/QO>gQPO1G/RO>nQPO1G/TO>vQPO1G/XO>{QPO1G/^O?SQPO1G/bO?XQPO1G.mO?aQPO1G.mO*SQPO,5:dO*wQPO1G/|O?hQPO1G/xOOQO1G0Q1G0QO@[QPO1G0QO@cQPO1G0QO@jQPO1G0QO*wQPO,5:mO*rQPO,5:lOVQPO1G0VOAXQPO1G.kOAiQPO7+$sOOQO7+$m7+$mOApQPO7+$oO*wQPO7+$sOVQPO7+$xO*wQPO7+$|OOQO7+$X7+$XOOQO1G0O1G0OOAuQPO7+%hOOQO7+%d7+%dOBcQPO1G0XOOQO1G0W1G0WOOQO7+%q7+%qOVQPO<<H_O*wQPO<<HZOBmQPO<<H_OBtQPO'#DXOCYQPO<<HdOCbQPO<<HhOCiQPO<<ISODWQPOAN=yODiQPOAN=uOVQPOAN=yOVQPO,59sOOQOAN>OAN>OOVQPOAN>SOOQOG23eG23eOOQOG23aG23aODpQPOG23eOERQPO1G/_OEgQPOG23nOEnQPOG23nOOQOLD)PLD)POOQOLD)YLD)YO*wQPOLD)YOFPQPO!$'LtOOQO!)9B`!)9B`", states:
stateData: "Fi~O!xOS~OTQOVTO]PO^PO_PO`ROaROmUOpVOrWOxXOyYO}ZO!Q`O!S^O!T^O!U^O!V^O!W^O!X^O!hcO!ifO!kdO!mdO~OsiO~PVOc!zXd!zXe!zXf!zXg!zXh!zXi!zXj!zX!Y!zXl!zX~Ov!zX!PSX!ZSX![!zX!]!zX!^!zX!_!zX!`!zX!a!zX!b!zX!c!zX!d!zX!e!zX!v!zX~P!zOckOdlOemOfnOgoOhpOiqOjrOvsO![sO!]sO!^sO!_sO!`sO!asO!bsO!csO!dsO!esO~O!v!yX~P$lOV{O]PO^PO_PO`ROaROmUOpVOrWOxXOyYO}ZO!Q`O!S^O!T^O!U^O!V^O!W^O!X^O~OTyO~P&QOV!OOpVO~Os!QO~Om!RO~Om!SO~Om!TO~OckXdkXekXfkXgkXhkXikXjkXvkX!PSX!ZSX![kX!]kX!^kX!_kX!`kX!akX!bkX!ckX!dkX!ekX!vkX!YkXlkX~Os!UO~OV!VO~O!PsO!ZsO~OT!ZOV!YO~Os!^O~O!o!_O!p!`O!q!aO!r!bO!v!yX~OT!cO~OTRO~P&QO!YYX~P$lOU!fOcZXdZXeZXfZXgZXhZXiZXjZXvZX!YZX![ZX!]ZX!^ZX!_ZX!`ZX!aZX!bZX!cZX!dZX!eZX~OU!oO!YYX~O!Y!pO~OU!qO!o!_O!p!`O!q!aO!r!bO~Ov!rO~P!zOckOdlOemOfnOgoOhpOiqOjrO~OlYX!YYXtYX~P-aOl!sO!Y!pO~Ov!|O!Y!{O~Ov#SO~Ot#UO!Y#TO~OckOebifbigbihbiibijbivbi![bi!]bi!^bi!_bi!`bi!abi!bbi!cbi!dbi!ebi!vbiUbi!Ybilbi!obi!pbi!qbi!rbitbiwbiTbiVbi]bi^bi_bi`biabimbipbirbixbiybi}bi!Qbi!Sbi!Tbi!Ubi!Vbi!Wbi!Xbi!hbi!ibi!kbi!mbi|bi~Odbi~P.wOdlO~P.wOckOdlOemOjrOgbihbiibivbi![bi!]bi!^bi!_bi!`bi!abi!bbi!cbi!dbi!ebi!vbiUbi!Ybilbi!obi!pbi!qbi!rbitbiwbiTbiVbi]bi^bi_bi`biabimbipbirbixbiybi}bi!Qbi!Sbi!Tbi!Ubi!Vbi!Wbi!Xbi!hbi!ibi!kbi!mbi|bi~Ofbi~P2nOckOdlOemOfnOiqOjrOhbivbi![bi!]bi!^bi!_bi!`bi!abi!bbi!cbi!dbi!ebi!vbiUbi!Ybilbi!obi!pbi!qbi!rbitbiwbiTbiVbi]bi^bi_bi`biabimbipbirbixbiybi}bi!Qbi!Sbi!Tbi!Ubi!Vbi!Wbi!Xbi!hbi!ibi!kbi!mbi|bi~Ogbi~P6^OgoO~P6^OfnO~P2nOckOdlOemOfbigbihbiibijbivbi![bi!]bi!^bi!_bi!`bi!abi!bbi!cbi!dbi!ebi!vbiUbi!Ybilbi!obi!pbi!qbi!rbitbiwbiTbiVbi]bi^bi_bi`biabimbipbirbixbiybi}bi!Qbi!Sbi!Tbi!Ubi!Vbi!Wbi!Xbi!hbi!ibi!kbi!mbi|bi~O!oRi!pRi!qRi!rRi!vRiURilRi|RiwRi~P-aOU#XO~P-aOt#YO!Y!pO~Ov#ZO~Ow#[O~P-aO!P#]O~Ot#^O!Y!pO~OU#^O~P-aOt#aO!Y!pO~O!o!_O!q!ni!r!ni!v!niU!nil!ni|!niw!ni~O!p!ni~P?pO!p!`O~P?pO!o!_O!p!`O!q!aO!r!ni!v!niU!nil!ni|!niw!ni~OUXi!YXilXitXi~P-aOw#eO~P-aOV#fO~OckOdlOemOfnOgoOhpOiqOjrO~PVOt!ui!Y!ui~P-aOw#nO~P-aO!o!_O!p!`O!q!aO!r!bOl{X|{X~Ol#pO|#oO~Ow#qO~P-aO!o!_O!p!`O!q!aO!r!bO!v!jyU!jyl!jy|!jyw!jy~Ol#rO!o!_O!p!`O!q!aO!r!bO~OU#sO~P-aOl#xO!o!_O!p!`O!q!aO!r!bO~O!o!_O!p!`O!q!aO!r!bOl{i|{i~Ol#yO~P$lOw#zO!o!_O!p!`O!q!aO!r!bO~Ol#|O~P-aOr!S!i!Q`a!U!V!W!X!TT}yxy~", "2xO!sQPOOOOQO'#Ch'#ChO#lQPO'#EWOOQO'#EW'#EWO%yQPO'#EVOVQPO'#CnO'_QPO'#CzO'fQPO'#C{O'nQPO'#C}O'sQPO'#DRO'xQPO'#DWO'}QPO'#D[OOQO'#Cw'#CwO(SQPO'#CwOOQO'#D_'#D_OOQO'#Cg'#CgO)pQPO'#CgO)uQPO'#CgO)zQPO'#C_OVQPO'#DsO*SQPO'#DvOOQO'#Dr'#DrO*[QPO'#DrO*aQPO'#EVOOQO'#C^'#C^O*rQPO'#EPQOQPOOO*wQPO,59YO*wQPO,59YO*wQPO,59YO*wQPO,59YO*wQPO,59YO*wQPO,59YO*wQPO,59YO*wQPO,59YO*wQPO,58yO+OQPO'#CfO+VQPO,59YO,jQPO,59OO,rQPO'#CeO,wQPO,58xO-YQPO,59mO-{QPO'#CfO*wQPO'#CnO.YQPO,59fOOQO'#Cf'#CfO*wQPO,59gOOQO,59g,59gO*wQPO,59iO*SQPO,59mO*wQPO,59rO*SQPO,59vO*wQPO,59RO*wQPO,59RO$lQPO'#C_O.bQPO,5:_O*wQPO'#CdOOQO'#C`'#C`OOQO'#Dx'#DxO/PQPO,5:bO*wQPO,5:^OVQPO,5:fOVQPO,5:fOVQPO,5:fOVQPO,5:fO/XQPO'#EROOQO'#EQ'#EQO/^QPO,5:kOOQO1G.t1G.tO2}QPO1G.tO3UQPO1G.tO6tQPO1G.tO:dQPO1G.tO:kQPO1G.tO:rQPO1G.tO:yQPO1G.tO>eQPO1G.eOOQO1G.j1G.jO*wQPO,59POOQO1G.d1G.dO*wQPO1G/XOOQO1G/Q1G/QO?UQPO1G/RO?]QPO1G/TO?eQPO1G/XO?jQPO1G/^O?qQPO1G/bO?vQPO1G.mO@OQPO1G.mO*SQPO,5:dO*wQPO1G/|O@VQPO1G/xO@yQPO1G0QOAQQPO1G0QOAXQPO1G0QOOQO1G0Q1G0QO*wQPO,5:mO*rQPO,5:lOVQPO1G0VOAvQPO1G.kOBWQPO7+$sOOQO7+$m7+$mOB_QPO7+$oO*wQPO7+$sOVQPO7+$xO*wQPO7+$|OOQO7+$X7+$XOOQO1G0O1G0OOBdQPO7+%hOOQO7+%d7+%dOCQQPO1G0XOOQO1G0W1G0WOOQO7+%q7+%qOVQPO<<H_O*wQPO<<HZOC[QPO<<H_OCcQPO'#DXOCwQPO<<HdODPQPO<<HhODWQPO<<ISODuQPOAN=yOEWQPOAN=uOVQPOAN=yOVQPO,59sOOQOAN>OAN>OOVQPOAN>SOOQOG23eG23eOOQOG23aG23aOE_QPOG23eOEpQPO1G/_OFUQPOG23nOF]QPOG23nOOQOLD)PLD)POOQOLD)YLD)YO*wQPOLD)YOFnQPO!$'LtOOQO!)9B`!)9B`",
goto: "-}!{PP!|#v$WPPP$t%x&T&f(dPPPPP)`PPPPPPPP(dPP*^+YP*^PPP*^PPPP*^,XPP*^PP,[PPPPPPPPPPPPPPPPPP#v-WPP-WP-hP#vPPPP-k-n-qPPP-w(dSgO#UQxTQ!XcQ#O!_Q#P!`Q#Q!aQ#R!bQ#h#[Q#k#`Q#l#eQ#t#nQ#u#oR#w#qmhOTc!_!`!a!b#U#[#`#e#n#o#qlbOTc!_!`!a!b#U#[#`#e#n#o#qQ![dQ!v!RQ!x!TR#_!{!U[Uklmnopqrs{!O!Q!S!U!V!Y!^!p!r!|#S#Z#]#f#zl]OTc!_!`!a!b#U#[#`#e#n#o#qX!Zd!R!T!{UvT{!YX}U!Q!U!^UwT{!YQ|UQ!u!QQ!y!UR!}!^SSO#UQtT[zU{!Q!U!Y!^d!Wc!_!`!a!b#[#`#e#n#oQ!fkQ!glQ!hmQ!inQ!joQ!kpQ!lqQ!mrQ!nsQ!t!OQ!w!SQ!z!VQ#V!pQ#W!rQ#`!|Q#b#SQ#g#ZQ#j#]Q#m#fQ#v#qR#{#z!s_OTUcklmnopqrs{!O!Q!S!U!V!Y!^!_!`!a!b!p!r!|#S#U#Z#[#]#`#e#f#n#o#q#z!n_OUcklmnopqrs!O!Q!S!U!V!Y!^!_!`!a!b!p!r!|#S#U#Z#[#]#`#e#f#n#o#q#zTuT{!s[OTUcklmnopqrs{!O!Q!S!U!V!Y!^!_!`!a!b!p!r!|#S#U#Z#[#]#`#e#f#n#o#q#z!r[OTUcklmnopqrs{!O!Q!S!U!V!Y!^!_!`!a!b!p!r!|#S#U#Z#[#]#`#e#f#n#o#q#zR!PVR#i#[!saOTUcklmnopqrs{!O!Q!S!U!V!Y!^!_!`!a!b!p!r!|#S#U#Z#[#]#`#e#f#n#o#q#zmeOTc!_!`!a!b#U#[#`#e#n#o#qR!]dRjOR!eiQ!diR#c#TQjOR#d#U", stateData:
nodeNames: "⚠ Expression Logic Logic_predicates Variable Local ) ( Tuple Setexpr_enum_min2 Setexpr_enum Setexpr Literal Integer EmptySet IntegerSet Global Radical Setexpr_binary + - * \\ ∆ ∩ × Setexpr_generators } { Enumeration Boolean Filter_expression Filter [ ] Declarative ∈ | PrefixD PrefixI Imperative Imp_blocks ; PrefixR Recursion := Function TextFunction BigPr SmallPr Card Bool Debool Red , :∈ ∉ ⊆ ⊄ ⊂ > ≥ < ≤ ≠ = Logic_unary Negation ¬ Predicate Logic_quantor ∀ Variable_pack ∃ Logic_binary ⇔ ⇒ & Function_decl Arguments Declaration", 'GW~O!xOS~OTQOVTO]PO^PO_PO`ROaROmUOpVOrWOxXOyYO}ZO!Q`O!S^O!T^O!U^O!V^O!W^O!X^O!hcO!ifO!kdO!mdO~OsiO~PVOc!zXd!zXe!zXf!zXg!zXh!zXi!zXj!zX!Y!zXl!zX~Ov!zX!PSX!ZSX![!zX!]!zX!^!zX!_!zX!`!zX!a!zX!b!zX!c!zX!d!zX!e!zX!v!zX~P!zOckOdlOemOfnOgoOhpOiqOjrOvsO![sO!]sO!^sO!_sO!`sO!asO!bsO!csO!dsO!esO~O!v!yX~P$lOV{O]PO^PO_PO`ROaROmUOpVOrWOxXOyYO}ZO!Q`O!S^O!T^O!U^O!V^O!W^O!X^O~OTyO~P&QOV!OOpVO~Os!QO~Om!RO~Om!SO~Om!TO~OckXdkXekXfkXgkXhkXikXjkXvkX!PSX!ZSX![kX!]kX!^kX!_kX!`kX!akX!bkX!ckX!dkX!ekX!vkX!YkXlkX~Os!UO~OV!VO~O!PsO!ZsO~OT!ZOV!YO~Os!^O~O!o!_O!p!`O!q!aO!r!bO!v!yX~OT!cO~OTRO~P&QO!YYX~P$lOU!fOcZXdZXeZXfZXgZXhZXiZXjZXvZX!YZX![ZX!]ZX!^ZX!_ZX!`ZX!aZX!bZX!cZX!dZX!eZX~OU!oO!YYX~O!Y!pO~OU!qO!o!_O!p!`O!q!aO!r!bO~Ov!rO~P!zOckOdlOemOfnOgoOhpOiqOjrO~OlYX!YYXtYX~P-aOl!sO!Y!pO~O!o!_O!p!`O!q!aO!r!bO!v!gaU!gal!ga|!gaw!ga~Ov!|O!Y!{O~Ov#SO~Ot#UO!Y#TO~OckOebifbigbihbiibijbivbi![bi!]bi!^bi!_bi!`bi!abi!bbi!cbi!dbi!ebi!vbiUbi!Ybilbi!obi!pbi!qbi!rbitbiwbiTbiVbi]bi^bi_bi`biabimbipbirbixbiybi}bi!Qbi!Sbi!Tbi!Ubi!Vbi!Wbi!Xbi!hbi!ibi!kbi!mbi|bi~Odbi~P/fOdlO~P/fOckOdlOemOjrOgbihbiibivbi![bi!]bi!^bi!_bi!`bi!abi!bbi!cbi!dbi!ebi!vbiUbi!Ybilbi!obi!pbi!qbi!rbitbiwbiTbiVbi]bi^bi_bi`biabimbipbirbixbiybi}bi!Qbi!Sbi!Tbi!Ubi!Vbi!Wbi!Xbi!hbi!ibi!kbi!mbi|bi~Ofbi~P3]OckOdlOemOfnOiqOjrOhbivbi![bi!]bi!^bi!_bi!`bi!abi!bbi!cbi!dbi!ebi!vbiUbi!Ybilbi!obi!pbi!qbi!rbitbiwbiTbiVbi]bi^bi_bi`biabimbipbirbixbiybi}bi!Qbi!Sbi!Tbi!Ubi!Vbi!Wbi!Xbi!hbi!ibi!kbi!mbi|bi~Ogbi~P6{OgoO~P6{OfnO~P3]OckOdlOemOfbigbihbiibijbivbi![bi!]bi!^bi!_bi!`bi!abi!bbi!cbi!dbi!ebi!vbiUbi!Ybilbi!obi!pbi!qbi!rbitbiwbiTbiVbi]bi^bi_bi`biabimbipbirbixbiybi}bi!Qbi!Sbi!Tbi!Ubi!Vbi!Wbi!Xbi!hbi!ibi!kbi!mbi|bi~O!oRi!pRi!qRi!rRi!vRiURilRi|RiwRi~P-aOU#XO~P-aOt#YO!Y!pO~Ov#ZO~Ow#[O~P-aO!P#]O~Ot#^O!Y!pO~OU#^O~P-aOt#aO!Y!pO~O!q!aO!r!bO!o!ni!v!niU!nil!ni|!niw!ni~O!p!`O~P@_O!p!ni~P@_O!r!bO!o!ni!p!ni!q!ni!v!niU!nil!ni|!niw!ni~OUXi!YXilXitXi~P-aOw#eO~P-aOV#fO~OckOdlOemOfnOgoOhpOiqOjrO~PVOt!ui!Y!ui~P-aOw#nO~P-aO!o!_O!p!`O!q!aO!r!bOl{X|{X~Ol#pO|#oO~Ow#qO~P-aO!o!_O!p!`O!q!aO!r!bO!v!jyU!jyl!jy|!jyw!jy~Ol#rO!o!_O!p!`O!q!aO!r!bO~OU#sO~P-aOl#xO!o!_O!p!`O!q!aO!r!bO~O!o!_O!p!`O!q!aO!r!bOl{i|{i~Ol#yO~P$lOw#zO!o!_O!p!`O!q!aO!r!bO~Ol#|O~P-aOr!S!i!Q`a!U!V!W!X!TT}yxy~',
goto: '-}!{PP!|#v$WPPP$t%x&T&f(dPPPPP)`PPPPPPPP(dPP*^+YP*^PPP*^PPPP*^,XPP*^PP,[PPPPPPPPPPPPPPPPPP#v-WPP-WP-hP#vPPPP-k-n-qPPP-w(dSgO#UQxTQ!XcQ#O!_Q#P!`Q#Q!aQ#R!bQ#h#[Q#k#`Q#l#eQ#t#nQ#u#oR#w#qmhOTc!_!`!a!b#U#[#`#e#n#o#qlbOTc!_!`!a!b#U#[#`#e#n#o#qQ![dQ!v!RQ!x!TR#_!{!U[Uklmnopqrs{!O!Q!S!U!V!Y!^!p!r!|#S#Z#]#f#zl]OTc!_!`!a!b#U#[#`#e#n#o#qX!Zd!R!T!{UvT{!YX}U!Q!U!^UwT{!YQ|UQ!u!QQ!y!UR!}!^SSO#UQtT[zU{!Q!U!Y!^d!Wc!_!`!a!b#[#`#e#n#oQ!fkQ!glQ!hmQ!inQ!joQ!kpQ!lqQ!mrQ!nsQ!t!OQ!w!SQ!z!VQ#V!pQ#W!rQ#`!|Q#b#SQ#g#ZQ#j#]Q#m#fQ#v#qR#{#z!s_OTUcklmnopqrs{!O!Q!S!U!V!Y!^!_!`!a!b!p!r!|#S#U#Z#[#]#`#e#f#n#o#q#z!n_OUcklmnopqrs!O!Q!S!U!V!Y!^!_!`!a!b!p!r!|#S#U#Z#[#]#`#e#f#n#o#q#zTuT{!s[OTUcklmnopqrs{!O!Q!S!U!V!Y!^!_!`!a!b!p!r!|#S#U#Z#[#]#`#e#f#n#o#q#z!r[OTUcklmnopqrs{!O!Q!S!U!V!Y!^!_!`!a!b!p!r!|#S#U#Z#[#]#`#e#f#n#o#q#zR!PVR#i#[!saOTUcklmnopqrs{!O!Q!S!U!V!Y!^!_!`!a!b!p!r!|#S#U#Z#[#]#`#e#f#n#o#q#zmeOTc!_!`!a!b#U#[#`#e#n#o#qR!]dRjOR!eiQ!diR#c#TQjOR#d#U',
nodeNames:
'⚠ Expression Logic Logic_predicates Variable Local ) ( Tuple Setexpr_enum_min2 Setexpr_enum Setexpr Literal Integer EmptySet IntegerSet Global Radical Setexpr_binary * + - \\ ∆ ∩ × Setexpr_generators } { Enumeration Boolean Filter_expression Filter [ ] Declarative ∈ | PrefixD PrefixI Imperative Imp_blocks ; PrefixR Recursion := Function TextFunction BigPr SmallPr Card Bool Debool Red , :∈ ∉ ⊆ ⊄ ⊂ > ≥ < ≤ ≠ = Logic_unary Negation ¬ Predicate Logic_quantor ∀ Variable_pack ∃ Logic_binary ⇔ ⇒ & Function_decl Arguments Declaration',
maxTerm: 88, maxTerm: 88,
nodeProps: [ nodeProps: [
["openedBy", 6,"(",28,"{"], ['openedBy', 6, '(', 28, '{'],
["closedBy", 7,")",29,"}"] ['closedBy', 7, ')', 29, '}']
], ],
skippedNodes: [0], skippedNodes: [0],
repeatNodeCount: 0, repeatNodeCount: 0,
tokenData: "6g~R!iX^%ppq%pvw&exy&jyz&oz{&t{|&y|}'O}!O'T!Q!['Y![!]'b!]!^'u!^!_'z!_!`(P!`!a(U!c!d(Z!e!f(Z!f!g(i!h!i(q!k!l)e!r!s)j!t!u*^!u!v(Z!v!w(Z!z!{(Z!|!}*n!}#O*s#O#P*x#P#Q*}#R#S+S#T#U+S#U#V+j#V#W-Y#W#X.u#X#d+S#d#e1_#e#f+S#f#g2t#g#o+S#o#p4O#p#q4T#q#r4Y#y#z%p$f$g%p$r$s4_%o%p4d5i6S+S#BY#BZ%p$IS$I_%p$I|$JO%p$JT$JU%p$KV$KW%p% l% m4i%%Y%%Z4n%%[%%]4s%&Y%&Z4x%&]%&^4}%&_%&`5S%&`%&a5X%&b%&c5^%&c%&d5c%'S%'T5h%'T%'U5m%'U%'V5r%(^%(_5w%(b%(c5|%(c%(d6R%)Q%)R6W%)S%)T6]%)U%)V6b&FU&FV%p~%uY!x~X^%ppq%p#y#z%p$f$g%p#BY#BZ%p$IS$I_%p$I|$JO%p$JT$JU%p$KV$KW%p&FU&FV%p~&jO!r~~&oOV~~&tOU~~&yOe~~'OOc~~'TO!Y~~'YOd~~'_P]~!Q!['Y~'eQ!_!`'k%&b%&c'p~'pO!P~~'uO!Z~~'zO|~~(PO!b~~(UO!e~~(ZO!`~~(^P!Q![(a~(fP`~!Q![(a~(nPx~!Q![(a~(tQ!Q![(z#]#^)S~)PP!Q~!Q![(z~)VP!R![)Y~)_Qr~|})S!Q![)Y~)jOy~~)mQ!Q![)s#f#g){~)xP!i~!Q![)s~*OP!R![*R~*WQ!S~|}){!Q![*R~*cP}~!Q![*f~*kPa~!Q![*f~*sO_~~*xOs~~*}Og~~+SOt~~+XRT~!Q![+b#T#o+S5i6S+S~+gPT~!Q![+b~+oTT~!Q![+b#T#c+S#c#d,O#d#o+S5i6S+S~,TTT~!Q![+b#T#c+S#c#d,d#d#o+S5i6S+S~,iTT~!Q![+b#T#`+S#`#a,x#a#o+S5i6S+S~-PR!V~T~!Q![+b#T#o+S5i6S+S~-_ST~!Q![+b#T#U-k#U#o+S5i6S+S~-pTT~!Q![+b#T#f+S#f#g.P#g#o+S5i6S+S~.UTT~!Q![+b#T#W+S#W#X.e#X#o+S5i6S+S~.lR!U~T~!Q![+b#T#o+S5i6S+S~.zTT~!Q![+b#T#X+S#X#Y/Z#Y#o+S5i6S+S~/`TT~!Q![+b#T#U+S#U#V/o#V#o+S5i6S+S~/tTT~!Q![+b#T#c+S#c#d0T#d#o+S5i6S+S~0YTT~!Q![+b#T#c+S#c#d0i#d#o+S5i6S+S~0nTT~!Q![+b#T#`+S#`#a0}#a#o+S5i6S+S~1UR!W~T~!Q![+b#T#o+S5i6S+S~1dTT~!Q![+b#T#f+S#f#g1s#g#o+S5i6S+S~1xST~!Q!R+b!R![2U#T#o+S5i6S+S~2]Q!T~T~|}2c!Q![2U~2fP!R![2i~2nQ!T~|}2c!Q![2i~2yTT~!Q![+b#T#X+S#X#Y3Y#Y#o+S5i6S+S~3_TT~!Q![+b#T#W+S#W#X3n#X#o+S5i6S+S~3uR!X~T~!Q![+b#T#o+S5i6S+S~4TOm~~4YOw~~4_Ol~~4dO!h~~4iOj~~4nOp~~4sO!p~~4xO!o~~4}O!k~~5SO!m~~5XO^~~5^Oh~~5cOv~~5hO![~~5mO!q~~5rOi~~5wOf~~5|O!d~~6RO!c~~6WO!a~~6]O!_~~6bO!^~~6gO!]~", tokenData:
"6g~R!iX^%ppq%pvw&exy&jyz&oz{&t{|&y|}'O}!O'T!Q!['Y![!]'b!]!^'u!^!_'z!_!`(P!`!a(U!c!d(Z!e!f(Z!f!g(i!h!i(q!k!l)e!r!s)j!t!u*^!u!v(Z!v!w(Z!z!{(Z!|!}*n!}#O*s#O#P*x#P#Q*}#R#S+S#T#U+S#U#V+j#V#W-Y#W#X.u#X#d+S#d#e1_#e#f+S#f#g2t#g#o+S#o#p4O#p#q4T#q#r4Y#y#z%p$f$g%p$r$s4_%o%p4d5i6S+S#BY#BZ%p$IS$I_%p$I|$JO%p$JT$JU%p$KV$KW%p% l% m4i%%Y%%Z4n%%[%%]4s%&Y%&Z4x%&]%&^4}%&_%&`5S%&`%&a5X%&b%&c5^%&c%&d5c%'S%'T5h%'T%'U5m%'U%'V5r%(^%(_5w%(b%(c5|%(c%(d6R%)Q%)R6W%)S%)T6]%)U%)V6b&FU&FV%p~%uY!x~X^%ppq%p#y#z%p$f$g%p#BY#BZ%p$IS$I_%p$I|$JO%p$JT$JU%p$KV$KW%p&FU&FV%p~&jO!r~~&oOV~~&tOU~~&yOc~~'OOd~~'TO!Y~~'YOe~~'_P]~!Q!['Y~'eQ!_!`'k%&b%&c'p~'pO!P~~'uO!Z~~'zO|~~(PO!b~~(UO!e~~(ZO!`~~(^P!Q![(a~(fP`~!Q![(a~(nPx~!Q![(a~(tQ!Q![(z#]#^)S~)PP!Q~!Q![(z~)VP!R![)Y~)_Qr~|})S!Q![)Y~)jOy~~)mQ!Q![)s#f#g){~)xP!i~!Q![)s~*OP!R![*R~*WQ!S~|}){!Q![*R~*cP}~!Q![*f~*kPa~!Q![*f~*sO_~~*xOs~~*}Og~~+SOt~~+XRT~!Q![+b#T#o+S5i6S+S~+gPT~!Q![+b~+oTT~!Q![+b#T#c+S#c#d,O#d#o+S5i6S+S~,TTT~!Q![+b#T#c+S#c#d,d#d#o+S5i6S+S~,iTT~!Q![+b#T#`+S#`#a,x#a#o+S5i6S+S~-PR!V~T~!Q![+b#T#o+S5i6S+S~-_ST~!Q![+b#T#U-k#U#o+S5i6S+S~-pTT~!Q![+b#T#f+S#f#g.P#g#o+S5i6S+S~.UTT~!Q![+b#T#W+S#W#X.e#X#o+S5i6S+S~.lR!U~T~!Q![+b#T#o+S5i6S+S~.zTT~!Q![+b#T#X+S#X#Y/Z#Y#o+S5i6S+S~/`TT~!Q![+b#T#U+S#U#V/o#V#o+S5i6S+S~/tTT~!Q![+b#T#c+S#c#d0T#d#o+S5i6S+S~0YTT~!Q![+b#T#c+S#c#d0i#d#o+S5i6S+S~0nTT~!Q![+b#T#`+S#`#a0}#a#o+S5i6S+S~1UR!W~T~!Q![+b#T#o+S5i6S+S~1dTT~!Q![+b#T#f+S#f#g1s#g#o+S5i6S+S~1xST~!Q!R+b!R![2U#T#o+S5i6S+S~2]Q!T~T~|}2c!Q![2U~2fP!R![2i~2nQ!T~|}2c!Q![2i~2yTT~!Q![+b#T#X+S#X#Y3Y#Y#o+S5i6S+S~3_TT~!Q![+b#T#W+S#W#X3n#X#o+S5i6S+S~3uR!X~T~!Q![+b#T#o+S5i6S+S~4TOm~~4YOw~~4_Ol~~4dO!h~~4iOj~~4nOp~~4sO!p~~4xO!o~~4}O!k~~5SO!m~~5XO^~~5^Oh~~5cOv~~5hO![~~5mO!q~~5rOi~~5wOf~~5|O!d~~6RO!c~~6WO!a~~6]O!_~~6bO!^~~6gO!]~",
tokenizers: [0], tokenizers: [0],
topRules: {"Expression":[0,1]}, topRules: { Expression: [0, 1] },
tokenPrec: 1710 tokenPrec: 1739
}) });

View File

@ -1,6 +1,5 @@
// This file was generated by lezer-generator. You probably shouldn't edit it. // This file was generated by lezer-generator. You probably shouldn't edit it.
export const export const Expression = 1,
Expression = 1,
Local = 2, Local = 2,
Literal = 4, Literal = 4,
Global = 5, Global = 5,
@ -12,4 +11,4 @@ export const
PrefixR = 28, PrefixR = 28,
Function = 30, Function = 30,
TextFunction = 31, TextFunction = 31,
Predicate = 44 Predicate = 44;

View File

@ -4,19 +4,19 @@ import { highlighting } from './highlight';
export const parser = LRParser.deserialize({ export const parser = LRParser.deserialize({
version: 14, version: 14,
states: states:
"3UO!sQPOOOOQO'#C`'#C`O#lQPO'#DoOOQO'#Do'#DoO%yQPO'#DdOVQPO'#DpO'_QPO'#DrO'fQPO'#DsO'nQQO'#DtO'sQPO'#DuO'xQPO'#DvO'}QPO'#DxOOQO'#Dq'#DqO(SQPO'#DqO)pQQO'#C{OOQO'#C{'#C{OOQO'#Dk'#DkO)uQPO'#DkO)zQPO'#DkO*PQPO'#DfOVQPO'#EQO*XQPO'#EQO*^QPO'#EQO*fQPO'#DdOOQO'#De'#DeO*wQPO'#ETQOQPOOO*|QPO,5:[O*|QPO,5:[O*|QPO,5:[O*|QPO,5:[O*|QPO,5:[O*|QPO,5:[O*|QPO,5:[O*|QPO,5:[O*|QPO,5:QO+TQPO'#DjO+[QPO,5:[O,oQPO,5:SO,wQPO'#DiO,|QPO,5:PO-_QPO,5:aO.QQPO'#DjO*|QPO'#DpO._QPO,5:^OOQO'#Dj'#DjO*|QPO,5:_OOQO,5:_,5:_O.gQPO,5:`O*^QPO,5:aO*|QPO,5:bO*^QPO,5:dOOQO,59g,59gO*|QPO,5:VO*|QPO,5:VO$lQPO'#DfOOQO,5:l,5:lO*|QPO,5:lO*|QPO'#DhOOQO'#Dg'#DgOOQO'#ER'#ERO.lQPO,5:lOVQPO,5:nOVQPO,5:nOVQPO,5:nOVQPO,5:nO.tQPO'#EVOOQO'#EU'#EUO.yQPO,5:oOOQO1G/v1G/vO2jQPO1G/vO2qQPO1G/vO6aQPO1G/vO:PQPO1G/vO:WQPO1G/vO:_QPO1G/vO:fQPO1G/vO>QQPO1G/lOOQO1G/n1G/nO*|QPO,5:TOOQO1G/k1G/kO*|QPO1G/{OOQO1G/x1G/xO>qQPO1G/yO*|QPO1G/zO>xQPO1G/{O>}QPO1G/|O?UQPO1G0OO?ZQPO1G/qO?cQPO1G/qO?jQPO1G0WO*^QPO,5:mO*|QPO1G0WOOQO1G0Y1G0YO@^QPO1G0YO@eQPO1G0YO@lQPO1G0YO*|QPO,5:qO*wQPO,5:pOVQPO1G0ZOAZQPO1G/oOAkQPO7+%gOOQO7+%e7+%eOArQPO7+%fO*|QPO7+%gOVQPO7+%hO*|QPO7+%jOOQO7+%]7+%]OOQO7+%r7+%rOOQO1G0X1G0XOAzQPO7+%rOBhQPO1G0]OOQO1G0[1G0[OOQO7+%u7+%uOVQPO<<IROBrQPO<<IQOBwQPO<<IROCOQPO'#DwOCdQPO<<ISOClQPO<<IUOCsQPO<<I^ODbQPOAN>mO*|QPOAN>lOVQPOAN>mOVQPO,5:cOOQOAN>nAN>nOVQPOAN>pOOQOG24XG24XODsQPOG24WODzQPOG24XOE]QPO1G/}OEqQPOG24[OExQPOG24[OOQOLD)rLD)rOOQOLD)sLD)sOOQOLD)vLD)vO*|QPOLD)vOFZQPO!$'MbOOQO!)9B|!)9B|", "3UO!sQPOOOOQO'#C`'#C`O#lQPO'#DoOOQO'#Do'#DoO%yQPO'#DdOVQPO'#DpO'_QPO'#DrO'fQPO'#DsO'nQQO'#DtO'sQPO'#DuO'xQPO'#DvO'}QPO'#DxOOQO'#Dq'#DqO(SQPO'#DqO)pQQO'#C{OOQO'#C{'#C{OOQO'#Dk'#DkO)uQPO'#DkO)zQPO'#DkO*PQPO'#DfOVQPO'#EQO*XQPO'#EQO*^QPO'#EQO*fQPO'#DdOOQO'#De'#DeO*wQPO'#ETQOQPOOO*|QPO,5:[O*|QPO,5:[O*|QPO,5:[O*|QPO,5:[O*|QPO,5:[O*|QPO,5:[O*|QPO,5:[O*|QPO,5:[O*|QPO,5:QO+TQPO'#DjO+[QPO,5:[O,oQPO,5:SO,wQPO'#DiO,|QPO,5:PO-_QPO,5:aO.QQPO'#DjO*|QPO'#DpO._QPO,5:^OOQO'#Dj'#DjO*|QPO,5:_OOQO,5:_,5:_O.gQPO,5:`O*^QPO,5:aO*|QPO,5:bO*^QPO,5:dOOQO,59g,59gO*|QPO,5:VO*|QPO,5:VO$lQPO'#DfO.lQPO,5:lO*|QPO,5:lO*|QPO'#DhOOQO'#Dg'#DgOOQO'#ER'#ERO/ZQPO,5:lOVQPO,5:nOVQPO,5:nOVQPO,5:nOVQPO,5:nO/cQPO'#EVOOQO'#EU'#EUO/hQPO,5:oOOQO1G/v1G/vO3XQPO1G/vO3`QPO1G/vO7OQPO1G/vO:nQPO1G/vO:uQPO1G/vO:|QPO1G/vO;TQPO1G/vO>oQPO1G/lOOQO1G/n1G/nO*|QPO,5:TOOQO1G/k1G/kO*|QPO1G/{OOQO1G/x1G/xO?`QPO1G/yO*|QPO1G/zO?gQPO1G/{O?lQPO1G/|O?sQPO1G0OO?xQPO1G/qO@QQPO1G/qO@XQPO1G0WO*^QPO,5:mO*|QPO1G0WO@{QPO1G0YOASQPO1G0YOAZQPO1G0YOOQO1G0Y1G0YO*|QPO,5:qO*wQPO,5:pOVQPO1G0ZOAxQPO1G/oOBYQPO7+%gOOQO7+%e7+%eOBaQPO7+%fO*|QPO7+%gOVQPO7+%hO*|QPO7+%jOOQO7+%]7+%]OOQO7+%r7+%rOOQO1G0X1G0XOBiQPO7+%rOCVQPO1G0]OOQO1G0[1G0[OOQO7+%u7+%uOVQPO<<IROCaQPO<<IQOCfQPO<<IROCmQPO'#DwODRQPO<<ISODZQPO<<IUODbQPO<<I^OEPQPOAN>mO*|QPOAN>lOVQPOAN>mOVQPO,5:cOOQOAN>nAN>nOVQPOAN>pOOQOG24XG24XOEbQPOG24WOEiQPOG24XOEzQPO1G/}OF`QPOG24[OFgQPOG24[OOQOLD)rLD)rOOQOLD)sLD)sOOQOLD)vLD)vO*|QPOLD)vOFxQPO!$'MbOOQO!)9B|!)9B|",
stateData: stateData:
'Fs~O!VOS~OQQORTOTROURO`UObVOcWOiXOjYOlZOnaO{dO|eO}fO!OfO!`PO!aPO!bPO!m^O!n^O!o_O!p_O!q_O!r_O~OeiO~PVOV!cXW!cXX!cXY!cXZ!cX[!cX]!cX^!cX!s!cXa!cX~Og!cXm!ZXp!ZXq!cXr!cXs!cXt!cXu!cXv!cXw!cXx!cXy!cXz!cX!T!cX~P!zOVkOWlOXmOYnOZoO[pO]qO^rOgsOqsOrsOssOtsOusOvsOwsOxsOysOzsO~O!T!WX~P$lOR{OTROURO`UObVOcWOiXOjYOlZOnaO!`PO!aPO!bPO!m^O!n^O!o_O!p_O!q_O!r_O~OQyO~P&QOR!OObVO~Od!QO~O`!RO~O`!SO~O`!TO~OV!eXW!eXX!eXY!eXZ!eX[!eX]!eX^!eXg!eXm!ZXp!ZXq!eXr!eXs!eXt!eXu!eXv!eXw!eXx!eXy!eXz!eX!T!eX!s!eXa!eX~Od!UO~Oe!VO~OR!WO~OmsOpsO~Oe!ZO~OQ!]OR![O~O!P!`O!Q!aO!R!bO!S!cO!T!WX~OQ!dO~OQRO~P&QO!s!^X~P$lO_!gOV!_XW!_XX!_XY!_XZ!_X[!_X]!_X^!_Xg!_Xq!_Xr!_Xs!_Xt!_Xu!_Xv!_Xw!_Xx!_Xy!_Xz!_X!s!_X~O_!pO!s!^X~O!s!qO~O_!rO!P!`O!Q!aO!R!bO!S!cO~Og!sO~P!zOVkOWlOXmOYnOZoO[pO]qO^rO~Oa!^X!s!^Xf!^X~P-fOa!tO!s!qO~Oe!vO~Og#OO!s!}O~Og#TO~Of#VO!s#UO~OVkOX!diY!diZ!di[!di]!di^!dig!diq!dir!dis!dit!diu!div!diw!dix!diy!diz!di!T!di_!di!s!dia!di!P!di!Q!di!R!di!S!dih!dif!diQ!diR!diT!diU!di`!dib!dic!dii!dij!dil!din!di{!di|!di}!di!O!di!`!di!a!di!b!di!m!di!n!di!o!di!p!di!q!di!r!dik!di~OW!di~P/ROWlO~P/ROVkOWlOXmO^rOZ!di[!di]!dig!diq!dir!dis!dit!diu!div!diw!dix!diy!diz!di!T!di_!di!s!dia!di!P!di!Q!di!R!di!S!dih!dif!diQ!diR!diT!diU!di`!dib!dic!dii!dij!dil!din!di{!di|!di}!di!O!di!`!di!a!di!b!di!m!di!n!di!o!di!p!di!q!di!r!dik!di~OY!di~P2xOVkOWlOXmOYnO]qO^rO[!dig!diq!dir!dis!dit!diu!div!diw!dix!diy!diz!di!T!di_!di!s!dia!di!P!di!Q!di!R!di!S!dih!dif!diQ!diR!diT!diU!di`!dib!dic!dii!dij!dil!din!di{!di|!di}!di!O!di!`!di!a!di!b!di!m!di!n!di!o!di!p!di!q!di!r!dik!di~OZ!di~P6hOZoO~P6hOYnO~P2xOVkOWlOXmOY!diZ!di[!di]!di^!dig!diq!dir!dis!dit!diu!div!diw!dix!diy!diz!di!T!di_!di!s!dia!di!P!di!Q!di!R!di!S!dih!dif!diQ!diR!diT!diU!di`!dib!dic!dii!dij!dil!din!di{!di|!di}!di!O!di!`!di!a!di!b!di!m!di!n!di!o!di!p!di!q!di!r!dik!di~O!P!Yi!Q!Yi!R!Yi!S!Yi!T!Yi_!Yia!Yik!Yih!Yi~P-fO_#YO~P-fOg#[O~Oh#]O~P-fOm#^O~Of#_O!s!qO~O_#_O~P-fOf#`O!s!qO~O!P!`O!R!vi!S!vi!T!vi_!via!vik!vih!vi~O!Q!vi~P?rO!Q!aO~P?rO!P!`O!Q!aO!R!bO!S!vi!T!vi_!via!vik!vih!vi~O_!]i!s!]ia!]if!]i~P-fOh#fO~P-fOf#gO!s!qO~OVkOWlOXmOYnOZoO[pO]qO^rO~PVOf!yi!s!yi~P-fOR#nO~Oh#oO~P-fO!P!`O!Q!aO!R!bO!S!cOa!kXk!kX~Oa#qOk#pO~Oh#rO~P-fO!P!`O!Q!aO!R!bO!S!cO!T!ty_!tya!tyk!tyh!ty~Oa#sO!P!`O!Q!aO!R!bO!S!cO~O_#yO~P-fOa#zO!P!`O!Q!aO!R!bO!S!cO~O!P!`O!Q!aO!R!bO!S!cOa!kik!ki~Oa#{O~P$lOh#|O!P!`O!Q!aO!R!bO!S!cO~Oa$OO~P-fOc!m|nTU!o!p!q!r!nQljij~', 'Gb~O!VOS~OQQORTOTROURO`UObVOcWOiXOjYOlZOnaO{dO|eO}fO!OfO!`PO!aPO!bPO!m^O!n^O!o_O!p_O!q_O!r_O~OeiO~PVOV!cXW!cXX!cXY!cXZ!cX[!cX]!cX^!cX!s!cXa!cX~Og!cXm!ZXp!ZXq!cXr!cXs!cXt!cXu!cXv!cXw!cXx!cXy!cXz!cX!T!cX~P!zOVkOWlOXmOYnOZoO[pO]qO^rOgsOqsOrsOssOtsOusOvsOwsOxsOysOzsO~O!T!WX~P$lOR{OTROURO`UObVOcWOiXOjYOlZOnaO!`PO!aPO!bPO!m^O!n^O!o_O!p_O!q_O!r_O~OQyO~P&QOR!OObVO~Od!QO~O`!RO~O`!SO~O`!TO~OV!eXW!eXX!eXY!eXZ!eX[!eX]!eX^!eXg!eXm!ZXp!ZXq!eXr!eXs!eXt!eXu!eXv!eXw!eXx!eXy!eXz!eX!T!eX!s!eXa!eX~Od!UO~Oe!VO~OR!WO~OmsOpsO~Oe!ZO~OQ!]OR![O~O!P!`O!Q!aO!R!bO!S!cO!T!WX~OQ!dO~OQRO~P&QO!s!^X~P$lO_!gOV!_XW!_XX!_XY!_XZ!_X[!_X]!_X^!_Xg!_Xq!_Xr!_Xs!_Xt!_Xu!_Xv!_Xw!_Xx!_Xy!_Xz!_X!s!_X~O_!pO!s!^X~O!s!qO~O_!rO!P!`O!Q!aO!R!bO!S!cO~Og!sO~P!zOVkOWlOXmOYnOZoO[pO]qO^rO~Oa!^X!s!^Xf!^X~P-fOa!tO!s!qO~Oe!vO~O!P!`O!Q!aO!R!bO!S!cO!T!ta_!taa!tak!tah!ta~Og#OO!s!}O~Og#TO~Of#VO!s#UO~OVkOX!diY!diZ!di[!di]!di^!dig!diq!dir!dis!dit!diu!div!diw!dix!diy!diz!di!T!di_!di!s!dia!di!P!di!Q!di!R!di!S!dih!dif!diQ!diR!diT!diU!di`!dib!dic!dii!dij!dil!din!di{!di|!di}!di!O!di!`!di!a!di!b!di!m!di!n!di!o!di!p!di!q!di!r!dik!di~OW!di~P/pOWlO~P/pOVkOWlOXmO^rOZ!di[!di]!dig!diq!dir!dis!dit!diu!div!diw!dix!diy!diz!di!T!di_!di!s!dia!di!P!di!Q!di!R!di!S!dih!dif!diQ!diR!diT!diU!di`!dib!dic!dii!dij!dil!din!di{!di|!di}!di!O!di!`!di!a!di!b!di!m!di!n!di!o!di!p!di!q!di!r!dik!di~OY!di~P3gOVkOWlOXmOYnO]qO^rO[!dig!diq!dir!dis!dit!diu!div!diw!dix!diy!diz!di!T!di_!di!s!dia!di!P!di!Q!di!R!di!S!dih!dif!diQ!diR!diT!diU!di`!dib!dic!dii!dij!dil!din!di{!di|!di}!di!O!di!`!di!a!di!b!di!m!di!n!di!o!di!p!di!q!di!r!dik!di~OZ!di~P7VOZoO~P7VOYnO~P3gOVkOWlOXmOY!diZ!di[!di]!di^!dig!diq!dir!dis!dit!diu!div!diw!dix!diy!diz!di!T!di_!di!s!dia!di!P!di!Q!di!R!di!S!dih!dif!diQ!diR!diT!diU!di`!dib!dic!dii!dij!dil!din!di{!di|!di}!di!O!di!`!di!a!di!b!di!m!di!n!di!o!di!p!di!q!di!r!dik!di~O!P!Yi!Q!Yi!R!Yi!S!Yi!T!Yi_!Yia!Yik!Yih!Yi~P-fO_#YO~P-fOg#[O~Oh#]O~P-fOm#^O~Of#_O!s!qO~O_#_O~P-fOf#`O!s!qO~O!R!bO!S!cO!P!vi!T!vi_!via!vik!vih!vi~O!Q!aO~P@aO!Q!vi~P@aO!S!cO!P!vi!Q!vi!R!vi!T!vi_!via!vik!vih!vi~O_!]i!s!]ia!]if!]i~P-fOh#fO~P-fOf#gO!s!qO~OVkOWlOXmOYnOZoO[pO]qO^rO~PVOf!yi!s!yi~P-fOR#nO~Oh#oO~P-fO!P!`O!Q!aO!R!bO!S!cOa!kXk!kX~Oa#qOk#pO~Oh#rO~P-fO!P!`O!Q!aO!R!bO!S!cO!T!ty_!tya!tyk!tyh!ty~Oa#sO!P!`O!Q!aO!R!bO!S!cO~O_#yO~P-fOa#zO!P!`O!Q!aO!R!bO!S!cO~O!P!`O!Q!aO!R!bO!S!cOa!kik!ki~Oa#{O~P$lOh#|O!P!`O!Q!aO!R!bO!S!cO~Oa$OO~P-fOc!m|nTU!o!p!q!r!nQljij~',
goto: "-l!zPPPP!{PPPPPPPPPPPPPPPPPPPPPPPPPP#wPPPPPPPPPPPPPPPPPPPPPP$s$y%s&T&q'u(Q(cPPP!{*a!{+_,Z+_+_+_-Y+_PPPPPPP%s-]%s-`-c-f!s`OTUdklmnopqrs{!O!S!V!W!Z![!`!a!b!c!q!s!v#O#T#V#[#]#^#b#f#n#o#p#r#|!sbOTUdklmnopqrs{!O!S!V!W!Z![!`!a!b!c!q!s!v#O#T#V#[#]#^#b#f#n#o#p#r#|QjOR#e#VSgO#VQxTQ!YdQ#P!`Q#Q!aQ#R!bQ#S!cQ#i#]Q#l#bQ#m#fQ#u#oQ#v#pR#x#rmhOTd!`!a!b!c#V#]#b#f#o#p#rlcOTd!`!a!b!c#V#]#b#f#o#p#rQ!^fQ!w!RQ!y!TR#a!}!U[Uklmnopqrs{!O!S!V!W!Z![!q!s!v#O#T#[#^#n#|l]OTd!`!a!b!c#V#]#b#f#o#p#rX!]f!R!T!}UvT{![X}U!V!Z!vUwT{![Q|UQ!z!VQ!|!ZR#Z!vSSO#VQtT[zU{!V!Z![!vd!Xd!`!a!b!c#]#b#f#o#pQ!gkQ!hlQ!imQ!jnQ!koQ!lpQ!mqQ!nrQ!osQ!u!OQ!x!SQ!{!WQ#W!qQ#X!sQ#b#OQ#c#TQ#h#[Q#k#^Q#t#nQ#w#rR#}#|!n`OUdklmnopqrs!O!S!V!W!Z![!`!a!b!c!q!s!v#O#T#V#[#]#^#b#f#n#o#p#r#|TuT{!s[OTUdklmnopqrs{!O!S!V!W!Z![!`!a!b!c!q!s!v#O#T#V#[#]#^#b#f#n#o#p#r#|!r[OTUdklmnopqrs{!O!S!V!W!Z![!`!a!b!c!q!s!v#O#T#V#[#]#^#b#f#n#o#p#r#|R!PVR#j#]R!_fRjOR!fiQ!eiR#d#U", goto: "-l!zPPPP!{PPPPPPPPPPPPPPPPPPPPPPPPPP#wPPPPPPPPPPPPPPPPPPPPPP$s$y%s&T&q'u(Q(cPPP!{*a!{+_,Z+_+_+_-Y+_PPPPPPP%s-]%s-`-c-f!s`OTUdklmnopqrs{!O!S!V!W!Z![!`!a!b!c!q!s!v#O#T#V#[#]#^#b#f#n#o#p#r#|!sbOTUdklmnopqrs{!O!S!V!W!Z![!`!a!b!c!q!s!v#O#T#V#[#]#^#b#f#n#o#p#r#|QjOR#e#VSgO#VQxTQ!YdQ#P!`Q#Q!aQ#R!bQ#S!cQ#i#]Q#l#bQ#m#fQ#u#oQ#v#pR#x#rmhOTd!`!a!b!c#V#]#b#f#o#p#rlcOTd!`!a!b!c#V#]#b#f#o#p#rQ!^fQ!w!RQ!y!TR#a!}!U[Uklmnopqrs{!O!S!V!W!Z![!q!s!v#O#T#[#^#n#|l]OTd!`!a!b!c#V#]#b#f#o#p#rX!]f!R!T!}UvT{![X}U!V!Z!vUwT{![Q|UQ!z!VQ!|!ZR#Z!vSSO#VQtT[zU{!V!Z![!vd!Xd!`!a!b!c#]#b#f#o#pQ!gkQ!hlQ!imQ!jnQ!koQ!lpQ!mqQ!nrQ!osQ!u!OQ!x!SQ!{!WQ#W!qQ#X!sQ#b#OQ#c#TQ#h#[Q#k#^Q#t#nQ#w#rR#}#|!n`OUdklmnopqrs!O!S!V!W!Z![!`!a!b!c!q!s!v#O#T#V#[#]#^#b#f#n#o#p#r#|TuT{!s[OTUdklmnopqrs{!O!S!V!W!Z![!`!a!b!c!q!s!v#O#T#V#[#]#^#b#f#n#o#p#r#|!r[OTUdklmnopqrs{!O!S!V!W!Z![!`!a!b!c!q!s!v#O#T#V#[#]#^#b#f#n#o#p#r#|R!PVR#j#]R!_fRjOR!fiQ!eiR#d#U",
nodeNames: nodeNames:
'⚠ Expression Local ( Literal Global Radical + - * \\ ∆ ∩ × ) { } Filter ComplexIndex [ ] ∈ | PrefixD PrefixI ; PrefixR := Function TextFunction :∈ ∉ ⊆ ⊄ ⊂ > ≥ < ≤ ≠ = ¬ Predicate ∀ ∃ ⇔ ⇒ &', '⚠ Expression Local ( Literal Global Radical * + - \\ ∆ ∩ × ) { } Filter ComplexIndex [ ] ∈ | PrefixD PrefixI ; PrefixR := Function TextFunction :∈ ∉ ⊆ ⊄ ⊂ > ≥ < ≤ ≠ = ¬ Predicate ∀ ∃ ⇔ ⇒ &',
maxTerm: 87, maxTerm: 87,
propSources: [highlighting], propSources: [highlighting],
skippedNodes: [0], skippedNodes: [0],
repeatNodeCount: 0, repeatNodeCount: 0,
tokenData: tokenData:
"6P~R!jX^%spq%svw&hxy&myz&rz{&w{|&||}'R}!O'W!Q!R']!R!['e![!](T!]!^(h!^!_(m!_!`(r!`!a(w!c!d(|!e!f(|!f!g)[!h!i)d!k!l)z!r!s*P!t!u*g!u!v(|!v!w(|!z!{(|!|!}*w!}#O*|#O#P+R#P#Q+W#R#S+]#T#U+]#U#V+s#V#W-c#W#X/O#X#d+]#d#e1h#e#f+]#f#g2^#g#o+]#o#p3h#p#q3m#q#r3r#y#z%s$f$g%s$r$s3w%o%p3|5i6S+]#BY#BZ%s$IS$I_%s$I|$JO%s$JT$JU%s$KV$KW%s% l% m4R%%Y%%Z4W%%[%%]4]%&Y%&Z4b%&]%&^4g%&_%&`4l%&`%&a4q%&b%&c4v%&c%&d4{%'S%'T5Q%'T%'U5V%'U%'V5[%(^%(_5a%(b%(c5f%(c%(d5k%)Q%)R5p%)S%)T5u%)U%)V5z&FU&FV%s~%xY!V~X^%spq%s#y#z%s$f$g%s#BY#BZ%s$IS$I_%s$I|$JO%s$JT$JU%s$KV$KW%s&FU&FV%s~&mO!S~~&rOR~~&wO_~~&|OX~~'ROV~~'WO!s~~']OW~P'bP!`P!Q![']R'lQdQ!`P|}'r!Q!['eQ'uP!R!['xQ'}QdQ|}'r!Q!['x~(WQ!_!`(^%&b%&c(c~(cOm~~(hOp~~(mOk~~(rOw~~(wOz~~(|Ou~~)PP!Q![)S~)XPT~!Q![)S~)aPi~!Q![)S~)gQ!Q![)m#]#^)u~)rPn~!Q![)m~)zOc~~*POj~~*SQ!Q![*Y#f#g*b~*_P|~!Q![*Y~*gO!m~~*lPl~!Q![*o~*tPU~!Q![*o~*|O!b~~+ROe~~+WOZ~~+]Of~~+bRQ~!Q![+k#T#o+]5i6S+]~+pPQ~!Q![+k~+xTQ~!Q![+k#T#c+]#c#d,X#d#o+]5i6S+]~,^TQ~!Q![+k#T#c+]#c#d,m#d#o+]5i6S+]~,rTQ~!Q![+k#T#`+]#`#a-R#a#o+]5i6S+]~-YR!p~Q~!Q![+k#T#o+]5i6S+]~-hSQ~!Q![+k#T#U-t#U#o+]5i6S+]~-yTQ~!Q![+k#T#f+]#f#g.Y#g#o+]5i6S+]~._TQ~!Q![+k#T#W+]#W#X.n#X#o+]5i6S+]~.uR!o~Q~!Q![+k#T#o+]5i6S+]~/TTQ~!Q![+k#T#X+]#X#Y/d#Y#o+]5i6S+]~/iTQ~!Q![+k#T#U+]#U#V/x#V#o+]5i6S+]~/}TQ~!Q![+k#T#c+]#c#d0^#d#o+]5i6S+]~0cTQ~!Q![+k#T#c+]#c#d0r#d#o+]5i6S+]~0wTQ~!Q![+k#T#`+]#`#a1W#a#o+]5i6S+]~1_R!q~Q~!Q![+k#T#o+]5i6S+]~1mTQ~!Q![+k#T#f+]#f#g1|#g#o+]5i6S+]~2TR!n~Q~!Q![+k#T#o+]5i6S+]~2cTQ~!Q![+k#T#X+]#X#Y2r#Y#o+]5i6S+]~2wTQ~!Q![+k#T#W+]#W#X3W#X#o+]5i6S+]~3_R!r~Q~!Q![+k#T#o+]5i6S+]~3mO`~~3rOh~~3wOa~~3|O{~~4RO^~~4WOb~~4]O!Q~~4bO!P~~4gO}~~4lO!O~~4qO!a~~4vO[~~4{Og~~5QOq~~5VO!R~~5[O]~~5aOY~~5fOy~~5kOx~~5pOv~~5uOt~~5zOs~~6POr~", "6P~R!jX^%spq%svw&hxy&myz&rz{&w{|&||}'R}!O'W!Q!R']!R!['e![!](T!]!^(h!^!_(m!_!`(r!`!a(w!c!d(|!e!f(|!f!g)[!h!i)d!k!l)z!r!s*P!t!u*g!u!v(|!v!w(|!z!{(|!|!}*w!}#O*|#O#P+R#P#Q+W#R#S+]#T#U+]#U#V+s#V#W-c#W#X/O#X#d+]#d#e1h#e#f+]#f#g2^#g#o+]#o#p3h#p#q3m#q#r3r#y#z%s$f$g%s$r$s3w%o%p3|5i6S+]#BY#BZ%s$IS$I_%s$I|$JO%s$JT$JU%s$KV$KW%s% l% m4R%%Y%%Z4W%%[%%]4]%&Y%&Z4b%&]%&^4g%&_%&`4l%&`%&a4q%&b%&c4v%&c%&d4{%'S%'T5Q%'T%'U5V%'U%'V5[%(^%(_5a%(b%(c5f%(c%(d5k%)Q%)R5p%)S%)T5u%)U%)V5z&FU&FV%s~%xY!V~X^%spq%s#y#z%s$f$g%s#BY#BZ%s$IS$I_%s$I|$JO%s$JT$JU%s$KV$KW%s&FU&FV%s~&mO!S~~&rOR~~&wO_~~&|OV~~'ROW~~'WO!s~~']OX~P'bP!`P!Q![']R'lQdQ!`P|}'r!Q!['eQ'uP!R!['xQ'}QdQ|}'r!Q!['x~(WQ!_!`(^%&b%&c(c~(cOm~~(hOp~~(mOk~~(rOw~~(wOz~~(|Ou~~)PP!Q![)S~)XPT~!Q![)S~)aPi~!Q![)S~)gQ!Q![)m#]#^)u~)rPn~!Q![)m~)zOc~~*POj~~*SQ!Q![*Y#f#g*b~*_P|~!Q![*Y~*gO!m~~*lPl~!Q![*o~*tPU~!Q![*o~*|O!b~~+ROe~~+WOZ~~+]Of~~+bRQ~!Q![+k#T#o+]5i6S+]~+pPQ~!Q![+k~+xTQ~!Q![+k#T#c+]#c#d,X#d#o+]5i6S+]~,^TQ~!Q![+k#T#c+]#c#d,m#d#o+]5i6S+]~,rTQ~!Q![+k#T#`+]#`#a-R#a#o+]5i6S+]~-YR!p~Q~!Q![+k#T#o+]5i6S+]~-hSQ~!Q![+k#T#U-t#U#o+]5i6S+]~-yTQ~!Q![+k#T#f+]#f#g.Y#g#o+]5i6S+]~._TQ~!Q![+k#T#W+]#W#X.n#X#o+]5i6S+]~.uR!o~Q~!Q![+k#T#o+]5i6S+]~/TTQ~!Q![+k#T#X+]#X#Y/d#Y#o+]5i6S+]~/iTQ~!Q![+k#T#U+]#U#V/x#V#o+]5i6S+]~/}TQ~!Q![+k#T#c+]#c#d0^#d#o+]5i6S+]~0cTQ~!Q![+k#T#c+]#c#d0r#d#o+]5i6S+]~0wTQ~!Q![+k#T#`+]#`#a1W#a#o+]5i6S+]~1_R!q~Q~!Q![+k#T#o+]5i6S+]~1mTQ~!Q![+k#T#f+]#f#g1|#g#o+]5i6S+]~2TR!n~Q~!Q![+k#T#o+]5i6S+]~2cTQ~!Q![+k#T#X+]#X#Y2r#Y#o+]5i6S+]~2wTQ~!Q![+k#T#W+]#W#X3W#X#o+]5i6S+]~3_R!r~Q~!Q![+k#T#o+]5i6S+]~3mO`~~3rOh~~3wOa~~3|O{~~4RO^~~4WOb~~4]O!Q~~4bO!P~~4gO}~~4lO!O~~4qO!a~~4vO[~~4{Og~~5QOq~~5VO!R~~5[O]~~5aOY~~5fOy~~5kOx~~5pOv~~5uOt~~5zOs~~6POr~",
tokenizers: [0, 1], tokenizers: [0, 1],
topRules: { Expression: [0, 1] }, topRules: { Expression: [0, 1] },
tokenPrec: 1720 tokenPrec: 1749
}); });

View File

@ -7,16 +7,20 @@
// ------------- Precedence Definitions -------------------- // ------------- Precedence Definitions --------------------
/////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////
@precedence { @precedence {
plus @left minus @left,
times @left,
not @right,
log_equiv @left,
log_impl @left,
log_or @left,
log_and @left, log_and @left,
log_or @left,
log_impl @left,
log_equiv @left,
times @left,
plus @left minus @left,
set_decart @left set_union @left set_intersect @left set_minus @left set_symminus @left, set_decart @left set_union @left set_intersect @left set_minus @left set_symminus @left,
set_bool @right, set_bool @right,
quant @right
quant @right,
not @right
} }
/////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////
@ -197,9 +201,9 @@ identifier {
} }
Setexpr_binary { Setexpr_binary {
Setexpr !times "*" Setexpr |
Setexpr !plus "+" Setexpr | Setexpr !plus "+" Setexpr |
Setexpr !minus "-" Setexpr | Setexpr !minus "-" Setexpr |
Setexpr !times "*" Setexpr |
Setexpr !set_union "" Setexpr | Setexpr !set_union "" Setexpr |
Setexpr !set_minus "\\" Setexpr | Setexpr !set_minus "\\" Setexpr |
Setexpr !set_symminus "∆" Setexpr | Setexpr !set_symminus "∆" Setexpr |

View File

@ -8,16 +8,20 @@
// ------------- Precedence Definitions -------------------- // ------------- Precedence Definitions --------------------
/////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////
@precedence { @precedence {
plus @left minus @left,
times @left,
not @right,
log_equiv @left,
log_impl @left,
log_or @left,
log_and @left, log_and @left,
log_or @left,
log_impl @left,
log_equiv @left,
times @left,
plus @left minus @left,
set_decart @left set_union @left set_intersect @left set_minus @left set_symminus @left, set_decart @left set_union @left set_intersect @left set_minus @left set_symminus @left,
set_bool @right, set_bool @right,
quant @right
quant @right,
not @right
} }
/////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////
@ -195,9 +199,9 @@ identifier {
} }
setexpr_binary { setexpr_binary {
setexpr !times "*" setexpr |
setexpr !plus "+" setexpr | setexpr !plus "+" setexpr |
setexpr !minus "-" setexpr | setexpr !minus "-" setexpr |
setexpr !times "*" setexpr |
setexpr !set_union "" setexpr | setexpr !set_union "" setexpr |
setexpr !set_minus "\\" setexpr | setexpr !set_minus "\\" setexpr |
setexpr !set_symminus "∆" setexpr | setexpr !set_symminus "∆" setexpr |

View File

@ -145,6 +145,15 @@ export class RSTextWrapper extends CodeMirrorWrapper {
return true; return true;
} }
case TokenID.BOOLEAN: { case TokenID.BOOLEAN: {
if (!hasSelection && selection.from >= 2) {
const enclosingText = this.ref.view.state.sliceDoc(selection.from - 2, selection.from + 1);
if (enclosingText === '()') {
this.ref.view.dispatch({
changes: [{ from: selection.from - 2, insert: '' }]
});
return true;
}
}
if (hasSelection && this.startsWithBoolean(selection)) { if (hasSelection && this.startsWithBoolean(selection)) {
this.envelopeWith('', ''); this.envelopeWith('', '');
} else { } else {
@ -290,7 +299,7 @@ export class RSTextWrapper extends CodeMirrorWrapper {
if (!text.startsWith('') || !text.endsWith(')')) { if (!text.startsWith('') || !text.endsWith(')')) {
return false; return false;
} }
const openParenIndex = text.indexOf('(', 1); // start after '' const openParenIndex = text.indexOf('(', 1);
if (openParenIndex === -1) { if (openParenIndex === -1) {
return false; return false;
} }

View File

@ -1,7 +1,7 @@
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler 'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
'use client'; 'use client';
import { useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
@ -121,10 +121,12 @@ export function FormConstituenta({ disabled, id, toggleReset, schema, activeCst,
} }
const prevDirty = useRef(isDirty); const prevDirty = useRef(isDirty);
useEffect(() => {
if (prevDirty.current !== isDirty) { if (prevDirty.current !== isDirty) {
prevDirty.current = isDirty; prevDirty.current = isDirty;
setIsModified(isDirty); setIsModified(isDirty);
} }
}, [isDirty, setIsModified]);
function onSubmit(data: IUpdateConstituentaDTO) { function onSubmit(data: IUpdateConstituentaDTO) {
return cstUpdate({ itemID: schema.id, data }).then(() => { return cstUpdate({ itemID: schema.id, data }).then(() => {

View File

@ -1,7 +1,7 @@
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler 'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
'use client'; 'use client';
import { useRef } from 'react'; import { useEffect, useRef } from 'react';
import { useForm, useWatch } from 'react-hook-form'; import { useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
@ -70,10 +70,12 @@ export function FormRSForm() {
} }
const prevDirty = useRef(isDirty); const prevDirty = useRef(isDirty);
useEffect(() => {
if (prevDirty.current !== isDirty) { if (prevDirty.current !== isDirty) {
prevDirty.current = isDirty; prevDirty.current = isDirty;
setIsModified(isDirty); setIsModified(isDirty);
} }
}, [isDirty, setIsModified]);
function handleSelectVersion(version: CurrentVersion) { function handleSelectVersion(version: CurrentVersion) {
router.push({ path: urls.schema(schema.id, version === 'latest' ? undefined : version) }); router.push({ path: urls.schema(schema.id, version === 'latest' ? undefined : version) });

View File

@ -2,6 +2,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import clsx from 'clsx';
import fileDownload from 'js-file-download'; import fileDownload from 'js-file-download';
import { MiniButton } from '@/components/control'; import { MiniButton } from '@/components/control';
@ -123,7 +124,7 @@ export function EditorRSList() {
return false; return false;
} }
const tableHeight = useFitHeight('4rem + 5px'); const tableHeight = useFitHeight(isContentEditable ? '4rem + 5px' : '2rem');
return ( return (
<div tabIndex={-1} onKeyDown={handleKeyDown} className='relative pt-8'> <div tabIndex={-1} onKeyDown={handleKeyDown} className='relative pt-8'>
@ -146,7 +147,7 @@ export function EditorRSList() {
) : null} ) : null}
<MiniButton <MiniButton
className='absolute z-pop top-18 right-4 hidden sm:block' className={clsx('absolute z-pop right-4 hidden sm:block', isContentEditable ? 'top-18' : 'top-8')}
title='Выгрузить в формате CSV' title='Выгрузить в формате CSV'
icon={<IconCSV size='1.25rem' className='icon-green' />} icon={<IconCSV size='1.25rem' className='icon-green' />}
onClick={handleDownloadCSV} onClick={handleDownloadCSV}

View File

@ -5,41 +5,41 @@
/** /**
* Represents single node of a {@link Graph}, as implemented by storing outgoing and incoming connections. * Represents single node of a {@link Graph}, as implemented by storing outgoing and incoming connections.
*/ */
export class GraphNode { export class GraphNode<NodeID> {
/** Unique identifier of the node. */ /** Unique identifier of the node. */
id: number; id: NodeID;
/** List of outgoing nodes. */ /** List of outgoing nodes. */
outputs: number[]; outputs: NodeID[];
/** List of incoming nodes. */ /** List of incoming nodes. */
inputs: number[]; inputs: NodeID[];
constructor(id: number) { constructor(id: NodeID) {
this.id = id; this.id = id;
this.outputs = []; this.outputs = [];
this.inputs = []; this.inputs = [];
} }
clone(): GraphNode { clone(): GraphNode<NodeID> {
const result = new GraphNode(this.id); const result = new GraphNode(this.id);
result.outputs = [...this.outputs]; result.outputs = [...this.outputs];
result.inputs = [...this.inputs]; result.inputs = [...this.inputs];
return result; return result;
} }
addOutput(node: number): void { addOutput(node: NodeID): void {
this.outputs.push(node); this.outputs.push(node);
} }
addInput(node: number): void { addInput(node: NodeID): void {
this.inputs.push(node); this.inputs.push(node);
} }
removeInput(target: number): number | null { removeInput(target: NodeID): NodeID | null {
const index = this.inputs.findIndex(node => node === target); const index = this.inputs.findIndex(node => node === target);
return index > -1 ? this.inputs.splice(index, 1)[0] : null; return index > -1 ? this.inputs.splice(index, 1)[0] : null;
} }
removeOutput(target: number): number | null { removeOutput(target: NodeID): NodeID | null {
const index = this.outputs.findIndex(node => node === target); const index = this.outputs.findIndex(node => node === target);
return index > -1 ? this.outputs.splice(index, 1)[0] : null; return index > -1 ? this.outputs.splice(index, 1)[0] : null;
} }
@ -50,11 +50,11 @@ export class GraphNode {
* *
* This class is optimized for TermGraph use case and not supposed to be used as generic graph implementation. * This class is optimized for TermGraph use case and not supposed to be used as generic graph implementation.
*/ */
export class Graph { export class Graph<NodeID = number> {
/** Map of nodes. */ /** Map of nodes. */
nodes = new Map<number, GraphNode>(); nodes = new Map<NodeID, GraphNode<NodeID>>();
constructor(arr?: number[][]) { constructor(arr?: NodeID[][]) {
if (!arr) { if (!arr) {
return; return;
} }
@ -67,17 +67,17 @@ export class Graph {
}); });
} }
clone(): Graph { clone(): Graph<NodeID> {
const result = new Graph(); const result = new Graph<NodeID>();
this.nodes.forEach(node => result.nodes.set(node.id, node.clone())); this.nodes.forEach(node => result.nodes.set(node.id, node.clone()));
return result; return result;
} }
at(target: number): GraphNode | undefined { at(target: NodeID): GraphNode<NodeID> | undefined {
return this.nodes.get(target); return this.nodes.get(target);
} }
addNode(target: number): GraphNode { addNode(target: NodeID): GraphNode<NodeID> {
let node = this.nodes.get(target); let node = this.nodes.get(target);
if (!node) { if (!node) {
node = new GraphNode(target); node = new GraphNode(target);
@ -86,11 +86,11 @@ export class Graph {
return node; return node;
} }
hasNode(target: number): boolean { hasNode(target: NodeID): boolean {
return !!this.nodes.get(target); return !!this.nodes.get(target);
} }
removeNode(target: number): void { removeNode(target: NodeID): void {
this.nodes.forEach(node => { this.nodes.forEach(node => {
node.removeInput(target); node.removeInput(target);
node.removeOutput(target); node.removeOutput(target);
@ -98,7 +98,7 @@ export class Graph {
this.nodes.delete(target); this.nodes.delete(target);
} }
foldNode(target: number): void { foldNode(target: NodeID): void {
const nodeToRemove = this.nodes.get(target); const nodeToRemove = this.nodes.get(target);
if (!nodeToRemove) { if (!nodeToRemove) {
return; return;
@ -111,8 +111,8 @@ export class Graph {
this.removeNode(target); this.removeNode(target);
} }
removeIsolated(): GraphNode[] { removeIsolated(): GraphNode<NodeID>[] {
const result: GraphNode[] = []; const result: GraphNode<NodeID>[] = [];
this.nodes.forEach(node => { this.nodes.forEach(node => {
if (node.outputs.length === 0 && node.inputs.length === 0) { if (node.outputs.length === 0 && node.inputs.length === 0) {
result.push(node); result.push(node);
@ -122,7 +122,7 @@ export class Graph {
return result; return result;
} }
addEdge(source: number, destination: number): void { addEdge(source: NodeID, destination: NodeID): void {
if (this.hasEdge(source, destination)) { if (this.hasEdge(source, destination)) {
return; return;
} }
@ -132,7 +132,7 @@ export class Graph {
destinationNode.addInput(sourceNode.id); destinationNode.addInput(sourceNode.id);
} }
removeEdge(source: number, destination: number): void { removeEdge(source: NodeID, destination: NodeID): void {
const sourceNode = this.nodes.get(source); const sourceNode = this.nodes.get(source);
const destinationNode = this.nodes.get(destination); const destinationNode = this.nodes.get(destination);
if (sourceNode && destinationNode) { if (sourceNode && destinationNode) {
@ -141,7 +141,7 @@ export class Graph {
} }
} }
hasEdge(source: number, destination: number): boolean { hasEdge(source: NodeID, destination: NodeID): boolean {
const sourceNode = this.nodes.get(source); const sourceNode = this.nodes.get(source);
if (!sourceNode) { if (!sourceNode) {
return false; return false;
@ -149,8 +149,8 @@ export class Graph {
return !!sourceNode.outputs.find(id => id === destination); return !!sourceNode.outputs.find(id => id === destination);
} }
expandOutputs(origin: number[]): number[] { expandOutputs(origin: NodeID[]): NodeID[] {
const result: number[] = []; const result: NodeID[] = [];
origin.forEach(id => { origin.forEach(id => {
const node = this.nodes.get(id); const node = this.nodes.get(id);
if (node) { if (node) {
@ -164,8 +164,8 @@ export class Graph {
return result; return result;
} }
expandInputs(origin: number[]): number[] { expandInputs(origin: NodeID[]): NodeID[] {
const result: number[] = []; const result: NodeID[] = [];
origin.forEach(id => { origin.forEach(id => {
const node = this.nodes.get(id); const node = this.nodes.get(id);
if (node) { if (node) {
@ -179,13 +179,13 @@ export class Graph {
return result; return result;
} }
expandAllOutputs(origin: number[]): number[] { expandAllOutputs(origin: NodeID[]): NodeID[] {
const result: number[] = this.expandOutputs(origin); const result: NodeID[] = this.expandOutputs(origin);
if (result.length === 0) { if (result.length === 0) {
return []; return [];
} }
const marked = new Map<number, boolean>(); const marked = new Map<NodeID, boolean>();
origin.forEach(id => marked.set(id, true)); origin.forEach(id => marked.set(id, true));
let position = 0; let position = 0;
while (position < result.length) { while (position < result.length) {
@ -203,13 +203,13 @@ export class Graph {
return result; return result;
} }
expandAllInputs(origin: number[]): number[] { expandAllInputs(origin: NodeID[]): NodeID[] {
const result: number[] = this.expandInputs(origin); const result: NodeID[] = this.expandInputs(origin);
if (result.length === 0) { if (result.length === 0) {
return []; return [];
} }
const marked = new Map<number, boolean>(); const marked = new Map<NodeID, boolean>();
origin.forEach(id => marked.set(id, true)); origin.forEach(id => marked.set(id, true));
let position = 0; let position = 0;
while (position < result.length) { while (position < result.length) {
@ -227,8 +227,8 @@ export class Graph {
return result; return result;
} }
maximizePart(origin: number[]): number[] { maximizePart(origin: NodeID[]): NodeID[] {
const outputs: number[] = this.expandAllOutputs(origin); const outputs: NodeID[] = this.expandAllOutputs(origin);
const result = [...origin]; const result = [...origin];
this.topologicalOrder() this.topologicalOrder()
.filter(id => outputs.includes(id)) .filter(id => outputs.includes(id))
@ -241,10 +241,10 @@ export class Graph {
return result; return result;
} }
topologicalOrder(): number[] { topologicalOrder(): NodeID[] {
const result: number[] = []; const result: NodeID[] = [];
const marked = new Set<number>(); const marked = new Set<NodeID>();
const nodeStack: number[] = []; const nodeStack: NodeID[] = [];
this.nodes.forEach(node => { this.nodes.forEach(node => {
if (marked.has(node.id)) { if (marked.has(node.id)) {
return; return;
@ -275,12 +275,12 @@ export class Graph {
transitiveReduction() { transitiveReduction() {
const order = this.topologicalOrder(); const order = this.topologicalOrder();
const marked = new Map<number, boolean>(); const marked = new Map<NodeID, boolean>();
order.forEach(nodeID => { order.forEach(nodeID => {
if (marked.get(nodeID)) { if (marked.get(nodeID)) {
return; return;
} }
const stack: { id: number; parents: number[] }[] = []; const stack: { id: NodeID; parents: NodeID[] }[] = [];
stack.push({ id: nodeID, parents: [] }); stack.push({ id: nodeID, parents: [] });
while (stack.length > 0) { while (stack.length > 0) {
const item = stack.splice(0, 1)[0]; const item = stack.splice(0, 1)[0];
@ -299,20 +299,20 @@ export class Graph {
/** /**
* Finds a cycle in the graph. * Finds a cycle in the graph.
* *
* @returns {number[] | null} The cycle if found, otherwise `null`. * @returns {NodeID[] | null} The cycle if found, otherwise `null`.
* Uses non-recursive DFS. * Uses non-recursive DFS.
*/ */
findCycle(): number[] | null { findCycle(): NodeID[] | null {
const visited = new Set<number>(); const visited = new Set<NodeID>();
const nodeStack = new Set<number>(); const nodeStack = new Set<NodeID>();
const parents = new Map<number, number>(); const parents = new Map<NodeID, NodeID>();
for (const nodeId of this.nodes.keys()) { for (const nodeId of this.nodes.keys()) {
if (visited.has(nodeId)) { if (visited.has(nodeId)) {
continue; continue;
} }
const callStack: { nodeId: number; parentId: number | null }[] = []; const callStack: { nodeId: NodeID; parentId: NodeID | null }[] = [];
callStack.push({ nodeId: nodeId, parentId: null }); callStack.push({ nodeId: nodeId, parentId: null });
while (callStack.length > 0) { while (callStack.length > 0) {
const { nodeId, parentId } = callStack[callStack.length - 1]; const { nodeId, parentId } = callStack[callStack.length - 1];
@ -336,7 +336,7 @@ export class Graph {
if (!nodeStack.has(child)) { if (!nodeStack.has(child)) {
continue; continue;
} }
const cycle: number[] = []; const cycle: NodeID[] = [];
let current = nodeId; let current = nodeId;
cycle.push(child); cycle.push(child);
while (current !== child) { while (current !== child) {