Setup ESLint and fix issues

This commit is contained in:
IRBorisov 2023-07-25 20:27:29 +03:00
parent 747ce126ae
commit e737628cea
83 changed files with 1737 additions and 1470 deletions

View File

@ -1,4 +1,5 @@
{ {
"python.linting.flake8Enabled": true, "python.linting.flake8Enabled": true,
"python.linting.enabled": true "python.linting.enabled": true,
"eslint.workingDirectories": [{ "mode": "auto" }]
} }

View File

@ -0,0 +1,28 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"standard-with-typescript",
"plugin:react/recommended",
"plugin:react/jsx-runtime"
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"project": ["tsconfig.json"]
},
"plugins": [
"react", "simple-import-sort"
],
"rules": {
"simple-import-sort/imports": "error",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/semi": "off",
"@typescript-eslint/strict-boolean-expressions": "off",
"@typescript-eslint/space-before-function-paren": "off",
"@typescript-eslint/indent": "off",
"object-shorthand": "off"
}
}

View File

@ -29,12 +29,20 @@
"react-tabs": "^6.0.1", "react-tabs": "^6.0.1",
"react-toastify": "^9.1.3", "react-toastify": "^9.1.3",
"styled-components": "^6.0.4", "styled-components": "^6.0.4",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.0", "@babel/plugin-proposal-private-property-in-object": "^7.21.0",
"tailwindcss": "^3.3.2" "@typescript-eslint/eslint-plugin": "^5.62.0",
"eslint": "^8.45.0",
"eslint-config-standard-with-typescript": "^37.0.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-n": "^16.0.1",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-react": "^7.33.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"tailwindcss": "^3.3.2",
"typescript": "^5.1.6"
} }
}, },
"node_modules/@aashutoshrathi/word-wrap": { "node_modules/@aashutoshrathi/word-wrap": {
@ -2530,9 +2538,9 @@
} }
}, },
"node_modules/@eslint-community/regexpp": { "node_modules/@eslint-community/regexpp": {
"version": "4.5.1", "version": "4.6.1",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.1.tgz",
"integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", "integrity": "sha512-O7x6dMstWLn2ktjcoiNLDkAGG2EjveHL+Vvc+n0fXumkJYAcSqcVYKtwDU+hDZ0uDUsnUagSYaZrOLAYE8un1A==",
"engines": { "engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0" "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
} }
@ -5956,6 +5964,48 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/builtins": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz",
"integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==",
"dev": true,
"dependencies": {
"semver": "^7.0.0"
}
},
"node_modules/builtins/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/builtins/node_modules/semver": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/builtins/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/bytes": { "node_modules/bytes": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
@ -7515,9 +7565,9 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "8.44.0", "version": "8.45.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.44.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.45.0.tgz",
"integrity": "sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A==", "integrity": "sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.4.0", "@eslint-community/regexpp": "^4.4.0",
@ -7544,7 +7594,6 @@
"globals": "^13.19.0", "globals": "^13.19.0",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^5.2.0", "ignore": "^5.2.0",
"import-fresh": "^3.0.0",
"imurmurhash": "^0.1.4", "imurmurhash": "^0.1.4",
"is-glob": "^4.0.0", "is-glob": "^4.0.0",
"is-path-inside": "^3.0.3", "is-path-inside": "^3.0.3",
@ -7556,7 +7605,6 @@
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
"optionator": "^0.9.3", "optionator": "^0.9.3",
"strip-ansi": "^6.0.1", "strip-ansi": "^6.0.1",
"strip-json-comments": "^3.1.0",
"text-table": "^0.2.0" "text-table": "^0.2.0"
}, },
"bin": { "bin": {
@ -7596,6 +7644,53 @@
"eslint": "^8.0.0" "eslint": "^8.0.0"
} }
}, },
"node_modules/eslint-config-standard": {
"version": "17.1.0",
"resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz",
"integrity": "sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"eslint": "^8.0.1",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-n": "^15.0.0 || ^16.0.0 ",
"eslint-plugin-promise": "^6.0.0"
}
},
"node_modules/eslint-config-standard-with-typescript": {
"version": "37.0.0",
"resolved": "https://registry.npmjs.org/eslint-config-standard-with-typescript/-/eslint-config-standard-with-typescript-37.0.0.tgz",
"integrity": "sha512-V8I/Q1eFf9tiOuFHkbksUdWO3p1crFmewecfBtRxXdnvb71BCJx+1xAknlIRZMwZioMX3/bPtMVCZsf1+AjjOw==",
"dev": true,
"dependencies": {
"@typescript-eslint/parser": "^5.52.0",
"eslint-config-standard": "17.1.0"
},
"peerDependencies": {
"@typescript-eslint/eslint-plugin": "^5.52.0",
"eslint": "^8.0.1",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-n": "^15.0.0 || ^16.0.0 ",
"eslint-plugin-promise": "^6.0.0",
"typescript": "*"
}
},
"node_modules/eslint-import-resolver-node": { "node_modules/eslint-import-resolver-node": {
"version": "0.3.7", "version": "0.3.7",
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz",
@ -7638,6 +7733,25 @@
"ms": "^2.1.1" "ms": "^2.1.1"
} }
}, },
"node_modules/eslint-plugin-es-x": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.2.0.tgz",
"integrity": "sha512-9dvv5CcvNjSJPqnS5uZkqb3xmbeqRLnvXKK7iI5+oK/yTusyc46zbBZKENGsOfojm/mKfszyZb+wNqNPAPeGXA==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.1.2",
"@eslint-community/regexpp": "^4.6.0"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ota-meshi"
},
"peerDependencies": {
"eslint": ">=8"
}
},
"node_modules/eslint-plugin-flowtype": { "node_modules/eslint-plugin-flowtype": {
"version": "8.0.3", "version": "8.0.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz",
@ -7754,10 +7868,80 @@
"eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
} }
}, },
"node_modules/eslint-plugin-n": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-16.0.1.tgz",
"integrity": "sha512-CDmHegJN0OF3L5cz5tATH84RPQm9kG+Yx39wIqIwPR2C0uhBGMWfbbOtetR83PQjjidA5aXMu+LEFw1jaSwvTA==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"builtins": "^5.0.1",
"eslint-plugin-es-x": "^7.1.0",
"ignore": "^5.2.4",
"is-core-module": "^2.12.1",
"minimatch": "^3.1.2",
"resolve": "^1.22.2",
"semver": "^7.5.3"
},
"engines": {
"node": ">=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/mysticatea"
},
"peerDependencies": {
"eslint": ">=7.0.0"
}
},
"node_modules/eslint-plugin-n/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/eslint-plugin-n/node_modules/semver": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/eslint-plugin-n/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/eslint-plugin-promise": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz",
"integrity": "sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"peerDependencies": {
"eslint": "^7.0.0 || ^8.0.0"
}
},
"node_modules/eslint-plugin-react": { "node_modules/eslint-plugin-react": {
"version": "7.32.2", "version": "7.33.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.0.tgz",
"integrity": "sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==", "integrity": "sha512-qewL/8P34WkY8jAqdQxsiL82pDUeT7nhs8IsuXgfgnsEloKCT4miAV9N9kGtx7/KM9NH/NCGUE7Edt9iGxLXFw==",
"dependencies": { "dependencies": {
"array-includes": "^3.1.6", "array-includes": "^3.1.6",
"array.prototype.flatmap": "^1.3.1", "array.prototype.flatmap": "^1.3.1",
@ -7772,7 +7956,7 @@
"object.values": "^1.1.6", "object.values": "^1.1.6",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"resolve": "^2.0.0-next.4", "resolve": "^2.0.0-next.4",
"semver": "^6.3.0", "semver": "^6.3.1",
"string.prototype.matchall": "^4.0.8" "string.prototype.matchall": "^4.0.8"
}, },
"engines": { "engines": {
@ -7820,6 +8004,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/eslint-plugin-simple-import-sort": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-10.0.0.tgz",
"integrity": "sha512-AeTvO9UCMSNzIHRkg8S6c3RPy5YEwKWSQPx3DYghLedo2ZQxowPFLGDN1AZ2evfg6r6mjBSZSLxLFsWSu3acsw==",
"dev": true,
"peerDependencies": {
"eslint": ">=5.0.0"
}
},
"node_modules/eslint-plugin-testing-library": { "node_modules/eslint-plugin-testing-library": {
"version": "5.11.0", "version": "5.11.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.11.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.11.0.tgz",
@ -16973,15 +17166,15 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "4.9.5", "version": "5.1.6",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
}, },
"engines": { "engines": {
"node": ">=4.2.0" "node": ">=14.17"
} }
}, },
"node_modules/unbox-primitive": { "node_modules/unbox-primitive": {

View File

@ -24,7 +24,6 @@
"react-tabs": "^6.0.1", "react-tabs": "^6.0.1",
"react-toastify": "^9.1.3", "react-toastify": "^9.1.3",
"styled-components": "^6.0.4", "styled-components": "^6.0.4",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"scripts": { "scripts": {
@ -33,12 +32,6 @@
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject" "eject": "react-scripts eject"
}, },
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": { "browserslist": {
"production": [ "production": [
">0.2%", ">0.2%",
@ -53,6 +46,15 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.0", "@babel/plugin-proposal-private-property-in-object": "^7.21.0",
"tailwindcss": "^3.3.2" "@typescript-eslint/eslint-plugin": "^5.62.0",
"eslint": "^8.45.0",
"eslint-config-standard-with-typescript": "^37.0.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-n": "^16.0.1",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-react": "^7.33.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"tailwindcss": "^3.3.2",
"typescript": "^5.1.6"
} }
} }

View File

@ -1,18 +1,18 @@
import { Route, Routes } from 'react-router-dom'; import { Route, Routes } from 'react-router-dom';
import Footer from './components/Footer';
import Navigation from './components/Navigation/Navigation'; import Navigation from './components/Navigation/Navigation';
import RSFormsPage from './pages/RSFormsPage'; import ToasterThemed from './components/ToasterThemed';
import RSFormPage from './pages/RSFormPage';
import NotFoundPage from './pages/NotFoundPage';
import HomePage from './pages/HomePage'; import HomePage from './pages/HomePage';
import LoginPage from './pages/LoginPage'; import LoginPage from './pages/LoginPage';
import RestorePasswordPage from './pages/RestorePasswordPage';
import UserProfilePage from './pages/UserProfilePage';
import RegisterPage from './pages/RegisterPage';
import ManualsPage from './pages/ManualsPage'; import ManualsPage from './pages/ManualsPage';
import Footer from './components/Footer'; import NotFoundPage from './pages/NotFoundPage';
import RegisterPage from './pages/RegisterPage';
import RestorePasswordPage from './pages/RestorePasswordPage';
import RSFormCreatePage from './pages/RSFormCreatePage'; import RSFormCreatePage from './pages/RSFormCreatePage';
import ToasterThemed from './components/ToasterThemed'; import RSFormPage from './pages/RSFormPage';
import RSFormsPage from './pages/RSFormsPage';
import UserProfilePage from './pages/UserProfilePage';
function App () { function App () {
return ( return (

View File

@ -1,4 +1,5 @@
import axios, { AxiosError } from 'axios'; import axios, { type AxiosError } from 'axios';
import PrettyJson from './Common/PrettyJSON'; import PrettyJson from './Common/PrettyJSON';
export type ErrorInfo = string | Error | AxiosError | undefined; export type ErrorInfo = string | Error | AxiosError | undefined;
@ -22,7 +23,7 @@ function DescribeError(error: ErrorInfo) {
if (error.response.status === 404) { if (error.response.status === 404) {
return ( return (
<div className='flex flex-col justify-start'> <div className='flex flex-col justify-start'>
<p>{`Обращение к несуществующему API`}</p> <p>{'Обращение к несуществующему API'}</p>
<PrettyJson data={error} /> <PrettyJson data={error} />
</div> </div>
); );

View File

@ -1,4 +1,4 @@
import { MouseEventHandler } from 'react' import { type MouseEventHandler } from 'react';
interface ButtonProps { interface ButtonProps {
id?: string id?: string
@ -14,14 +14,15 @@ interface ButtonProps {
onClick?: MouseEventHandler<HTMLButtonElement> | undefined onClick?: MouseEventHandler<HTMLButtonElement> | undefined
} }
function Button({id, text, icon, tooltip, function Button({
id, text, icon, tooltip,
dense, disabled, dense, disabled,
borderClass = 'border rounded', colorClass = 'clr-btn-default', widthClass = 'w-fit h-fit', borderClass = 'border rounded', colorClass = 'clr-btn-default', widthClass = 'w-fit h-fit',
loading, onClick, loading, onClick,
...props ...props
}: ButtonProps) { }: ButtonProps) {
const padding = dense ? 'px-1' : 'px-3 py-2' const padding = dense ? 'px-1' : 'px-3 py-2';
const cursor = 'disabled:cursor-not-allowed ' + (loading ? 'cursor-progress ': 'cursor-pointer ') const cursor = 'disabled:cursor-not-allowed ' + (loading ? 'cursor-progress ' : 'cursor-pointer ');
return ( return (
<button id={id} <button id={id}
type='button' type='button'
@ -34,7 +35,7 @@ function Button({id, text, icon, tooltip,
{icon && <span>{icon}</span>} {icon && <span>{icon}</span>}
{text && <span className={'font-semibold'}>{text}</span>} {text && <span className={'font-semibold'}>{text}</span>}
</button> </button>
) );
} }
export default Button; export default Button;

View File

@ -1,10 +1,10 @@
import { Tab} from 'react-tabs';
import type { TabProps } from 'react-tabs'; import type { TabProps } from 'react-tabs';
import { Tab } from 'react-tabs';
function ConceptTab({ children, className, ...otherProps }: TabProps) { function ConceptTab({ children, className, ...otherProps }: TabProps) {
return ( return (
<Tab <Tab
className={`px-2 py-1 text-sm hover:cursor-pointer clr-tab ${className} whitespace-nowrap`} className={`px-2 py-1 text-sm hover:cursor-pointer clr-tab ${className?.toString() ?? ''} whitespace-nowrap`}
{...otherProps} {...otherProps}
> >
{children} {children}

View File

@ -1,40 +1,41 @@
import DataTable, { createTheme, TableProps } from 'react-data-table-component'; import DataTable, { createTheme, type TableProps } from 'react-data-table-component';
import { useConceptTheme } from '../../context/ThemeContext'; import { useConceptTheme } from '../../context/ThemeContext';
export interface SelectionInfo<T> { export interface SelectionInfo<T> {
allSelected: boolean; allSelected: boolean
selectedCount: number; selectedCount: number
selectedRows: T[]; selectedRows: T[]
} }
createTheme('customDark', { createTheme('customDark', {
text: { text: {
primary: 'rgba(228, 228, 231, 1)', primary: 'rgba(228, 228, 231, 1)',
secondary: 'rgba(228, 228, 231, 0.87)', secondary: 'rgba(228, 228, 231, 0.87)',
disabled: 'rgba(228, 228, 231, 0.54)', disabled: 'rgba(228, 228, 231, 0.54)'
}, },
background: { background: {
default: '#002b36', default: '#002b36'
}, },
context: { context: {
background: '#3e014d', background: '#3e014d',
text: 'rgba(228, 228, 231, 0.87)', text: 'rgba(228, 228, 231, 0.87)'
}, },
highlightOnHover: { highlightOnHover: {
default: '#3e014d', default: '#3e014d',
text: 'rgba(228, 228, 231, 1)', text: 'rgba(228, 228, 231, 1)'
}, },
divider: { divider: {
default: '#6b6b6b', default: '#6b6b6b'
}, },
striped: { striped: {
default: '#004859', default: '#004859',
text: 'rgba(228, 228, 231, 1)', text: 'rgba(228, 228, 231, 1)'
}, },
selected: { selected: {
default: '#4b015c', default: '#4b015c',
text: 'rgba(228, 228, 231, 1)', text: 'rgba(228, 228, 231, 1)'
}, }
}, 'dark'); }, 'dark');
function DataTableThemed<T>({ theme, ...props }: TableProps<T>) { function DataTableThemed<T>({ theme, ...props }: TableProps<T>) {
@ -42,7 +43,7 @@ function DataTableThemed<T>({theme, ...props}: TableProps<T>) {
return ( return (
<DataTable<T> <DataTable<T>
theme={ theme ? theme : darkMode ? 'customDark' : ''} theme={ theme ?? (darkMode ? 'customDark' : '')}
{...props} {...props}
/> />
); );

View File

@ -1,4 +1,5 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { UploadIcon } from '../Icons'; import { UploadIcon } from '../Icons';
import Button from './Button'; import Button from './Button';
import Label from './Label'; import Label from './Label';

View File

@ -1,4 +1,4 @@
import { ThreeDots } from 'react-loader-spinner' import { ThreeDots } from 'react-loader-spinner';
export function Loader() { export function Loader() {
return ( return (

View File

@ -1,6 +1,7 @@
import { useRef } from 'react' import { useRef } from 'react';
import Button from './Button'
import useClickedOutside from '../../hooks/useClickedOutside' import useClickedOutside from '../../hooks/useClickedOutside';
import Button from './Button';
interface ModalProps { interface ModalProps {
title?: string title?: string
@ -15,7 +16,7 @@ interface ModalProps {
function Modal({ title, show, toggle, onSubmit, onCancel, canSubmit, children, submitText = 'Продолжить' }: ModalProps) { function Modal({ title, show, toggle, onSubmit, onCancel, canSubmit, children, submitText = 'Продолжить' }: ModalProps) {
const ref = useRef(null); const ref = useRef(null);
useClickedOutside({ref: ref, callback: toggle}) useClickedOutside({ ref, callback: toggle })
if (!show) { if (!show) {
return null; return null;
@ -33,14 +34,14 @@ function Modal({title, show, toggle, onSubmit, onCancel, canSubmit, children, su
return ( return (
<> <>
<div className='fixed top-0 left-0 w-full h-full clr-modal opacity-50 z-50'> <div className='fixed top-0 left-0 z-50 w-full h-full opacity-50 clr-modal'>
</div> </div>
<div ref={ref} className='fixed bottom-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2 px-6 py-4 flex flex-col w-fit z-[60] clr-card border shadow-md'> <div ref={ref} className='fixed bottom-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2 px-6 py-4 flex flex-col w-fit z-[60] clr-card border shadow-md'>
{ title && <h1 className='mb-4 text-xl font-bold'>{title}</h1> } { title && <h1 className='mb-4 text-xl font-bold'>{title}</h1> }
<div className='py-2'> <div className='py-2'>
{children} {children}
</div> </div>
<div className='pt-4 mt-2 border-t-4 flex justify-between w-full'> <div className='flex justify-between w-full pt-4 mt-2 border-t-4'>
<Button <Button
text={submitText} text={submitText}
widthClass='min-w-[6rem] w-fit h-fit' widthClass='min-w-[6rem] w-fit h-fit'

View File

@ -1,5 +1,5 @@
interface PrettyJsonProps { interface PrettyJsonProps {
data: Object data: any
} }
function PrettyJson({ data }: PrettyJsonProps) { function PrettyJson({ data }: PrettyJsonProps) {

View File

@ -1,4 +1,5 @@
import { InputHTMLAttributes } from 'react'; import { type InputHTMLAttributes } from 'react';
import Label from './Label'; import Label from './Label';
interface TextInputProps interface TextInputProps

View File

@ -1,4 +1,5 @@
import { FallbackProps } from 'react-error-boundary'; import { type FallbackProps } from 'react-error-boundary';
import Button from './Common/Button'; import Button from './Common/Button';
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) { function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {

View File

@ -1,9 +1,10 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { urls } from '../utils/constants'; import { urls } from '../utils/constants';
function Footer() { function Footer() {
return ( return (
<footer className='z-50 px-4 pt-2 pb-4 t border-t-2 clr-footer'> <footer className='z-50 px-4 pt-2 pb-4 border-t-2 t clr-footer'>
<div className='flex items-stretch justify-center w-full mx-auto'> <div className='flex items-stretch justify-center w-full mx-auto'>
<div className='px-4 underline'> <div className='px-4 underline'>
<Link to='manuals' tabIndex={-1}>Справка</Link> <br/> <Link to='manuals' tabIndex={-1}>Справка</Link> <br/>

View File

@ -19,7 +19,7 @@ function IconSVG({viewbox, size=6, color, props, children} : IconSVGProps) {
<svg <svg
width={width} width={width}
height={width} height={width}
className={`w-[${width}] h-[${width}] ${color}`} className={`w-[${width}] h-[${width}] ${color ?? ''}`}
fill='currentColor' fill='currentColor'
viewBox={viewbox} viewBox={viewbox}
{...props} {...props}
@ -31,7 +31,7 @@ function IconSVG({viewbox, size=6, color, props, children} : IconSVGProps) {
export function MagnifyingGlassIcon({ size, ...props }: IconProps) { export function MagnifyingGlassIcon({ size, ...props }: IconProps) {
return ( return (
<IconSVG viewbox='0 0 20 20' size={size || 5} {...props} > <IconSVG viewbox='0 0 20 20' size={size ?? 5} {...props} >
<path d='M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z'/> <path d='M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z'/>
</IconSVG> </IconSVG>
); );

View File

@ -1,20 +1,21 @@
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import TopSearch from './TopSearch';
import { EducationIcon, LibraryIcon } from '../Icons';
import NavigationButton from './NavigationButton';
import UserMenu from './UserMenu';
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
import UserTools from './UserTools';
import Logo from './Logo';
import { useConceptTheme } from '../../context/ThemeContext'; import { useConceptTheme } from '../../context/ThemeContext';
import { EducationIcon, LibraryIcon } from '../Icons';
import Logo from './Logo'
import NavigationButton from './NavigationButton';
import TopSearch from './TopSearch';
import UserMenu from './UserMenu';
import UserTools from './UserTools';
function Navigation () { function Navigation () {
const { user } = useAuth(); const { user } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const { noNavigation, toggleNoNavigation } = useConceptTheme(); const { noNavigation, toggleNoNavigation } = useConceptTheme();
const navigateCommon = () => navigate('/rsforms?filter=common'); const navigateCommon = () => { navigate('/rsforms?filter=common') };
const navigateHelp = () => navigate('/manuals'); const navigateHelp = () => { navigate('/manuals') };
return ( return (
<nav className='sticky top-0 left-0 right-0 z-50'> <nav className='sticky top-0 left-0 right-0 z-50'>

View File

@ -1,11 +1,12 @@
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
import DropdownButton from '../Common/DropdownButton';
import { useConceptTheme } from '../../context/ThemeContext'; import { useConceptTheme } from '../../context/ThemeContext';
import Dropdown from '../Common/Dropdown'; import Dropdown from '../Common/Dropdown';
import DropdownButton from '../Common/DropdownButton';
interface UserDropdownProps { interface UserDropdownProps {
hideDropdown: Function hideDropdown: () => void
} }
function UserDropdown({ hideDropdown }: UserDropdownProps) { function UserDropdown({ hideDropdown }: UserDropdownProps) {
@ -18,13 +19,14 @@ function UserDropdown({hideDropdown}: UserDropdownProps) {
navigate('/profile'); navigate('/profile');
}; };
const logoutAndRedirect = () => { const logoutAndRedirect =
hideDropdown() () => {
hideDropdown();
logout(() => { navigate('/login/'); }) logout(() => { navigate('/login/'); })
}; };
const navigateMyWork = () => { const navigateMyWork = () => {
hideDropdown() hideDropdown();
navigate('/rsforms?filter=personal'); navigate('/rsforms?filter=personal');
}; };

View File

@ -1,9 +1,10 @@
import { UserIcon } from '../Icons';
import { useAuth } from '../../context/AuthContext';
import UserDropdown from './UserDropdown';
import NavigationButton from './NavigationButton';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useAuth } from '../../context/AuthContext';
import useDropdown from '../../hooks/useDropdown'; import useDropdown from '../../hooks/useDropdown';
import { UserIcon } from '../Icons';
import NavigationButton from './NavigationButton';
import UserDropdown from './UserDropdown';
function LoginRef() { function LoginRef() {
return ( return (
@ -27,7 +28,7 @@ function UserMenu() {
/>} />}
{ user && menu.isActive && { user && menu.isActive &&
<UserDropdown <UserDropdown
hideDropdown={() => menu.hide()} hideDropdown={() => { menu.hide(); }}
/>} />}
</div> </div>
); );

View File

@ -1,13 +1,14 @@
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import NavigationButton from './NavigationButton';
import { BellIcon, PlusIcon, SquaresIcon } from '../Icons';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { BellIcon, PlusIcon, SquaresIcon } from '../Icons';
import NavigationButton from './NavigationButton';
function UserTools() { function UserTools() {
const navigate = useNavigate(); const navigate = useNavigate();
const navigateCreateRSForm = () => navigate('/rsform-create'); const navigateCreateRSForm = () => { navigate('/rsform-create'); };
const navigateMyWork = () => navigate('/rsforms?filter=personal'); const navigateMyWork = () => { navigate('/rsforms?filter=personal'); };
const handleNotifications = () => { const handleNotifications = () => {
toast.info('Уведомления в разработке'); toast.info('Уведомления в разработке');

View File

@ -1,4 +1,5 @@
import { ToastContainer, ToastContainerProps } from 'react-toastify'; import { ToastContainer, type ToastContainerProps } from 'react-toastify';
import { useConceptTheme } from '../context/ThemeContext'; import { useConceptTheme } from '../context/ThemeContext';
function ToasterThemed({ theme, ...props }: ToastContainerProps) { function ToasterThemed({ theme, ...props }: ToastContainerProps) {

View File

@ -1,15 +1,15 @@
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useState } from 'react'; import { createContext, useCallback, useContext, useLayoutEffect, useState } from 'react';
import { ICurrentUser, IUserSignupData } from '../utils/models';
import { ErrorInfo } from '../components/BackendError';
import useLocalStorage from '../hooks/useLocalStorage';
import { BackendCallback, getAuth, postLogin, postLogout, postSignup } from '../utils/backendAPI';
import { type ErrorInfo } from '../components/BackendError';
import useLocalStorage from '../hooks/useLocalStorage';
import { type BackendCallback, getAuth, postLogin, postLogout, postSignup } from '../utils/backendAPI';
import { type ICurrentUser, type IUserSignupData } from '../utils/models';
interface IAuthContext { interface IAuthContext {
user: ICurrentUser | undefined user: ICurrentUser | undefined
login: (username: string, password: string, callback?: BackendCallback) => Promise<void> login: (username: string, password: string, callback?: BackendCallback) => void
logout: (callback?: BackendCallback) => Promise<void> logout: (callback?: BackendCallback) => void
signup: (data: IUserSignupData, callback?: BackendCallback) => Promise<void> signup: (data: IUserSignupData, callback?: BackendCallback) => void
loading: boolean loading: boolean
error: ErrorInfo error: ErrorInfo
setError: (error: ErrorInfo) => void setError: (error: ErrorInfo) => void
@ -37,8 +37,8 @@ export const AuthState = ({ children }: AuthStateProps) => {
const loadCurrentUser = useCallback( const loadCurrentUser = useCallback(
async () => { async () => {
getAuth({ await getAuth({
onError: error => setUser(undefined), onError: () => { setUser(undefined); },
onSucccess: response => { onSucccess: response => {
if (response.data.id) { if (response.data.id) {
setUser(response.data); setUser(response.data);
@ -50,50 +50,53 @@ export const AuthState = ({ children }: AuthStateProps) => {
}, [setUser] }, [setUser]
); );
async function login(uname: string, pw: string, callback?: BackendCallback) { function login(uname: string, pw: string, callback?: BackendCallback) {
setError(undefined); setError(undefined);
postLogin({ postLogin({
data: { username: uname, password: pw }, data: { username: uname, password: pw },
showError: true, showError: true,
setLoading: setLoading, setLoading,
onError: error => setError(error), onError: error => { setError(error); },
onSucccess: onSucccess:
async (response) => { (response) => {
await loadCurrentUser(); loadCurrentUser()
if(callback) callback(response); .then(() => { if (callback) callback(response); })
.catch(console.error);
} }
}); }).catch(console.error);
} }
async function logout(callback?: BackendCallback) { function logout(callback?: BackendCallback) {
setError(undefined); setError(undefined);
postLogout({ postLogout({
showError: true, showError: true,
onSucccess: onSucccess:
async (response) => { (response) => {
await loadCurrentUser(); loadCurrentUser()
if (callback) callback(response); .then(() => { if (callback) callback(response); })
.catch(console.error);
} }
}); }).catch(console.error);
} }
async function signup(data: IUserSignupData, callback?: BackendCallback) { function signup(data: IUserSignupData, callback?: BackendCallback) {
setError(undefined); setError(undefined);
postSignup({ postSignup({
data: data, data,
showError: true, showError: true,
setLoading: setLoading, setLoading,
onError: error => setError(error), onError: error => { setError(error); },
onSucccess: onSucccess:
async (response) => { (response) => {
await loadCurrentUser(); loadCurrentUser()
if (callback) callback(response); .then(() => { if (callback) callback(response); })
.catch(console.error);
} }
}); }).catch(console.error);
} }
useLayoutEffect(() => { useLayoutEffect(() => {
loadCurrentUser(); loadCurrentUser().catch(console.error);
}, [loadCurrentUser]) }, [loadCurrentUser])
return ( return (

View File

@ -1,14 +1,15 @@
import { createContext, useState, useContext, useMemo, useCallback } from 'react'; import { createContext, useCallback, useContext, useMemo, useState } from 'react'
import { IConstituenta, IRSForm } from '../utils/models'; import { toast } from 'react-toastify'
import { useRSFormDetails } from '../hooks/useRSFormDetails';
import { ErrorInfo } from '../components/BackendError'; import { type ErrorInfo } from '../components/BackendError'
import { useAuth } from './AuthContext'; import { useRSFormDetails } from '../hooks/useRSFormDetails'
import { import {
BackendCallback, deleteRSForm, getTRSFile, type BackendCallback, deleteRSForm, getTRSFile,
patchConstituenta, patchMoveConstituenta, patchRSForm, patchConstituenta, patchDeleteConstituenta, patchMoveConstituenta, patchRSForm,
postClaimRSForm, patchDeleteConstituenta, postNewConstituenta postClaimRSForm, postNewConstituenta
} from '../utils/backendAPI'; } from '../utils/backendAPI'
import { toast } from 'react-toastify'; import { type IConstituenta, type IRSForm } from '../utils/models'
import { useAuth } from './AuthContext'
interface IRSFormContext { interface IRSFormContext {
schema?: IRSForm schema?: IRSForm
@ -31,26 +32,26 @@ interface IRSFormContext {
toggleReadonly: () => void toggleReadonly: () => void
toggleTracking: () => void toggleTracking: () => void
update: (data: any, callback?: BackendCallback) => Promise<void> update: (data: any, callback?: BackendCallback) => void
destroy: (callback?: BackendCallback) => Promise<void> destroy: (callback?: BackendCallback) => void
claim: (callback?: BackendCallback) => Promise<void> claim: (callback?: BackendCallback) => void
download: (callback: BackendCallback) => Promise<void> download: (callback: BackendCallback) => void
cstUpdate: (data: any, callback?: BackendCallback) => Promise<void> cstUpdate: (data: any, callback?: BackendCallback) => void
cstCreate: (data: any, callback?: BackendCallback) => Promise<void> cstCreate: (data: any, callback?: BackendCallback) => void
cstDelete: (data: any, callback?: BackendCallback) => Promise<void> cstDelete: (data: any, callback?: BackendCallback) => void
cstMoveTo: (data: any, callback?: BackendCallback) => Promise<void> cstMoveTo: (data: any, callback?: BackendCallback) => void
} }
const RSFormContext = createContext<IRSFormContext | null>(null); const RSFormContext = createContext<IRSFormContext | null>(null)
export const useRSForm = () => { export const useRSForm = () => {
const context = useContext(RSFormContext); const context = useContext(RSFormContext)
if (!context) { if (context == null) {
throw new Error( throw new Error(
'useRSForm has to be used within <RSFormState.Provider>' 'useRSForm has to be used within <RSFormState.Provider>'
); )
} }
return context; return context
} }
interface RSFormStateProps { interface RSFormStateProps {
@ -59,163 +60,181 @@ interface RSFormStateProps {
} }
export const RSFormState = ({ schemaID, children }: RSFormStateProps) => { export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
const { user } = useAuth(); const { user } = useAuth()
const { schema, reload, error, setError, setSchema, loading } = useRSFormDetails({target: schemaID}); const { schema, reload, error, setError, setSchema, loading } = useRSFormDetails({ target: schemaID })
const [processing, setProcessing] = useState(false) const [processing, setProcessing] = useState(false)
const [activeID, setActiveID] = useState<number | undefined>(undefined); const [activeID, setActiveID] = useState<number | undefined>(undefined)
const [isForceAdmin, setIsForceAdmin] = useState(false); const [isForceAdmin, setIsForceAdmin] = useState(false)
const [isReadonly, setIsReadonly] = useState(false); const [isReadonly, setIsReadonly] = useState(false)
const isOwned = useMemo(() => user?.id === schema?.owner || false, [user, schema?.owner]); const isOwned = useMemo(() => user?.id === schema?.owner || false, [user, schema?.owner])
const isClaimable = useMemo(() => user?.id !== schema?.owner || false, [user, schema?.owner]); const isClaimable = useMemo(() => user?.id !== schema?.owner || false, [user, schema?.owner])
const isEditable = useMemo( const isEditable = useMemo(
() => { () => {
return ( return (
!loading && !isReadonly && !loading && !isReadonly &&
(isOwned || (isForceAdmin && user?.is_staff) || false) ((isOwned || (isForceAdmin && user?.is_staff)) ?? false)
) )
}, [user, isReadonly, isForceAdmin, isOwned, loading]); }, [user, isReadonly, isForceAdmin, isOwned, loading])
const activeCst = useMemo( const activeCst = useMemo(
() => { () => {
return schema?.items && schema?.items.find((cst) => cst.id === activeID); return schema?.items?.find((cst) => cst.id === activeID)
}, [schema?.items, activeID]); }, [schema?.items, activeID])
const isTracking = useMemo( const isTracking = useMemo(
() => { () => {
return true; return true
}, []); }, [])
const toggleTracking = useCallback( const toggleTracking = useCallback(
() => { () => {
toast('not implemented yet'); toast('not implemented yet')
}, []); }, [])
const update = useCallback( const update = useCallback(
async (data: any, callback?: BackendCallback) => { (data: any, callback?: BackendCallback) => {
setError(undefined); setError(undefined)
patchRSForm(schemaID, { patchRSForm(schemaID, {
data: data, data,
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
onError: error => setError(error), onError: error => { setError(error) },
onSucccess: async (response) => { onSucccess: (response) => {
await reload(); reload()
if (callback) callback(response); .then(() => { if (callback != null) callback(response); })
.catch(console.error);
} }
}); }).catch(console.error);
}, [schemaID, setError, reload]); }, [schemaID, setError, reload])
const destroy = useCallback( const destroy = useCallback(
async (callback?: BackendCallback) => { (callback?: BackendCallback) => {
setError(undefined); setError(undefined)
deleteRSForm(schemaID, { deleteRSForm(schemaID, {
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
onError: error => setError(error), onError: error => { setError(error) },
onSucccess: callback onSucccess: callback
}); }).catch(console.error);
}, [schemaID, setError]); }, [schemaID, setError])
const claim = useCallback( const claim = useCallback(
async (callback?: BackendCallback) => { (callback?: BackendCallback) => {
setError(undefined); if (!schema || !user) {
return;
}
setError(undefined)
postClaimRSForm(schemaID, { postClaimRSForm(schemaID, {
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
onError: error => setError(error), onError: error => { setError(error) },
onSucccess: async (response) => { onSucccess: (response) => {
schema!.owner = user!.id schema.owner = user.id
schema!.time_update = response.data['time_update'] schema.time_update = response.data.time_update
setSchema(schema) setSchema(schema)
if (callback) callback(response); if (callback != null) callback(response)
} }
}); }).catch(console.error);
}, [schemaID, setError, schema, user, setSchema]); }, [schemaID, setError, schema, user, setSchema])
const download = useCallback( const download = useCallback(
async (callback: BackendCallback) => { (callback: BackendCallback) => {
setError(undefined); setError(undefined)
getTRSFile(schemaID, { getTRSFile(schemaID, {
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
onError: error => setError(error), onError: error => { setError(error) },
onSucccess: callback onSucccess: callback
}); }).catch(console.error);
}, [schemaID, setError]); }, [schemaID, setError])
const cstUpdate = useCallback( const cstUpdate = useCallback(
async (data: any, callback?: BackendCallback) => { (data: any, callback?: BackendCallback) => {
setError(undefined); setError(undefined)
patchConstituenta(String(activeID), { patchConstituenta(String(activeID), {
data: data, data,
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
onError: error => setError(error), onError: error => { setError(error) },
onSucccess: callback onSucccess: callback
}); }).catch(console.error);
}, [activeID, setError]); }, [activeID, setError])
const cstCreate = useCallback( const cstCreate = useCallback(
async (data: any, callback?: BackendCallback) => { (data: any, callback?: BackendCallback) => {
setError(undefined); setError(undefined)
postNewConstituenta(schemaID, { postNewConstituenta(schemaID, {
data: data, data,
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
onError: error => setError(error), onError: error => { setError(error) },
onSucccess: async (response) => { onSucccess: (response) => {
setSchema(response.data['schema']); setSchema(response.data.schema)
if (callback) callback(response); if (callback != null) callback(response)
} }
}); }).catch(console.error);
}, [schemaID, setError, setSchema]); }, [schemaID, setError, setSchema])
const cstDelete = useCallback( const cstDelete = useCallback(
async (data: any, callback?: BackendCallback) => { (data: any, callback?: BackendCallback) => {
setError(undefined); setError(undefined)
patchDeleteConstituenta(schemaID, { patchDeleteConstituenta(schemaID, {
data: data, data,
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
onError: error => setError(error), onError: error => { setError(error) },
onSucccess: async (response) => { onSucccess: (response) => {
setSchema(response.data); setSchema(response.data)
if (callback) callback(response); if (callback != null) callback(response)
} }
}); }).catch(console.error);
}, [schemaID, setError, setSchema]); }, [schemaID, setError, setSchema])
const cstMoveTo = useCallback( const cstMoveTo = useCallback(
async (data: any, callback?: BackendCallback) => { (data: any, callback?: BackendCallback) => {
setError(undefined); setError(undefined)
patchMoveConstituenta(schemaID, { patchMoveConstituenta(schemaID, {
data: data, data,
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
onError: error => setError(error), onError: error => { setError(error) },
onSucccess: (response) => { onSucccess: (response) => {
setSchema(response.data); setSchema(response.data)
if (callback) callback(response); if (callback != null) callback(response)
} }
}); }).catch(console.error);
}, [schemaID, setError, setSchema]); }, [schemaID, setError, setSchema])
return ( return (
<RSFormContext.Provider value={{ <RSFormContext.Provider value={{
schema, error, loading, processing, schema,
activeID, activeCst, error,
loading,
processing,
activeID,
activeCst,
setActiveID, setActiveID,
isForceAdmin, isReadonly, isForceAdmin,
toggleForceAdmin: () => setIsForceAdmin(prev => !prev), isReadonly,
toggleReadonly: () => setIsReadonly(prev => !prev), toggleForceAdmin: () => { setIsForceAdmin(prev => !prev) },
isOwned, isEditable, isClaimable, toggleReadonly: () => { setIsReadonly(prev => !prev) },
isTracking, toggleTracking, isOwned,
update, download, destroy, claim, isEditable,
cstUpdate, cstCreate, cstDelete, cstMoveTo, isClaimable,
isTracking,
toggleTracking,
update,
download,
destroy,
claim,
cstUpdate,
cstCreate,
cstDelete,
cstMoveTo
}}> }}>
{ children } { children }
</RSFormContext.Provider> </RSFormContext.Provider>
); )
} }

View File

@ -1,6 +1,6 @@
import { createContext, useContext, useEffect, useState } from 'react'; import { createContext, useContext, useEffect, useState } from 'react';
import useLocalStorage from '../hooks/useLocalStorage';
import useLocalStorage from '../hooks/useLocalStorage';
interface IThemeContext { interface IThemeContext {
darkMode: boolean darkMode: boolean
@ -44,8 +44,10 @@ export const ThemeState = ({ children }: ThemeStateProps) => {
return ( return (
<ThemeContext.Provider value={{ <ThemeContext.Provider value={{
darkMode, toggleDarkMode: () => setDarkMode(prev => !prev), darkMode,
noNavigation, toggleNoNavigation: () => setNoNavigation(prev => !prev), toggleDarkMode: () => { setDarkMode(prev => !prev); },
noNavigation,
toggleNoNavigation: () => { setNoNavigation(prev => !prev); }
}}> }}>
{children} {children}
</ThemeContext.Provider> </ThemeContext.Provider>

View File

@ -1,7 +1,7 @@
import { createContext, useCallback, useContext, useEffect, useState } from 'react' import { createContext, useCallback, useContext, useEffect, useState } from 'react';
import { IUserInfo } from '../utils/models'
import { getActiveUsers } from '../utils/backendAPI'
import { getActiveUsers } from '../utils/backendAPI';
import { type IUserInfo } from '../utils/models';
interface IUsersContext { interface IUsersContext {
users: IUserInfo[] users: IUserInfo[]
@ -9,10 +9,10 @@ interface IUsersContext {
getUserLabel: (userID?: number) => string getUserLabel: (userID?: number) => string
} }
const UsersContext = createContext<IUsersContext | null>(null); const UsersContext = createContext<IUsersContext | null>(null)
export const useUsers = () => { export const useUsers = (): IUsersContext => {
const context = useContext(UsersContext); const context = useContext(UsersContext);
if (!context) { if (context == null) {
throw new Error( throw new Error(
'useUsers has to be used within <UsersState.Provider>' 'useUsers has to be used within <UsersState.Provider>'
); );
@ -25,45 +25,46 @@ interface UsersStateProps {
} }
export const UsersState = ({ children }: UsersStateProps) => { export const UsersState = ({ children }: UsersStateProps) => {
const [users, setUsers] = useState<IUserInfo[]>([]); const [users, setUsers] = useState<IUserInfo[]>([])
const getUserLabel = (userID?: number) => { const getUserLabel = (userID?: number) => {
const user = users.find(({id}) => id === userID); const user = users.find(({ id }) => id === userID)
if (!user) { if (user == null) {
return (userID ? userID.toString() : 'Отсутствует'); return (userID !== undefined ? userID.toString() : 'Отсутствует');
} }
if (user.first_name || user.last_name) { const hasFirstName = user.first_name != null && user.first_name !== '';
if (!user.last_name) { const hasLastName = user.last_name != null && user.last_name !== '';
if (hasFirstName || hasLastName) {
if (!hasLastName) {
return user.first_name; return user.first_name;
} }
if (!user.first_name) { if (!hasFirstName) {
return user.last_name; return user.last_name;
} }
return user.first_name + ' ' + user.last_name return user.first_name + ' ' + user.last_name;
} }
return user.username; return user.username;
} }
const reload = useCallback( const reload = useCallback(
async () => { async () => {
getActiveUsers({ await getActiveUsers({
showError: true, showError: true,
onError: error => setUsers([]), onError: () => { setUsers([]); },
onSucccess: response => { onSucccess: response => { setUsers(response.data); }
setUsers(response ? response.data : []);
}
}); });
}, [setUsers] }, [setUsers]
); )
useEffect(() => { useEffect(() => {
reload(); reload().catch(console.error);
}, [reload]); }, [reload])
return ( return (
<UsersContext.Provider value={{ <UsersContext.Provider value={{
users, users,
reload, getUserLabel reload,
getUserLabel
}}> }}>
{ children } { children }
</UsersContext.Provider> </UsersContext.Provider>

View File

@ -1,24 +1,25 @@
import { type AxiosResponse } from 'axios';
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { ErrorInfo } from '../components/BackendError';
import { type ErrorInfo } from '../components/BackendError';
import { postCheckExpression } from '../utils/backendAPI'; import { postCheckExpression } from '../utils/backendAPI';
import { IRSForm } from '../utils/models'; import { type IRSForm } from '../utils/models';
import { AxiosResponse } from 'axios';
function useCheckExpression({ schema }: { schema?: IRSForm }) { function useCheckExpression({ schema }: { schema?: IRSForm }) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<ErrorInfo>(undefined); const [error, setError] = useState<ErrorInfo>(undefined);
const [parseData, setParseData] = useState<any | undefined>(undefined); const [parseData, setParseData] = useState<any | undefined>(undefined);
const resetParse = useCallback(() => setParseData(undefined), []); const resetParse = useCallback(() => { setParseData(undefined); }, []);
async function checkExpression(expression: string, onSuccess?: (response: AxiosResponse) => void) { async function checkExpression(expression: string, onSuccess?: (response: AxiosResponse) => void) {
setError(undefined); setError(undefined);
setParseData(undefined); setParseData(undefined);
postCheckExpression(String(schema!.id), { await postCheckExpression(String(schema?.id), {
data: {'expression': expression}, data: { expression },
showError: true, showError: true,
setLoading: setLoading, setLoading,
onError: error => setError(error), onError: error => { setError(error); },
onSucccess: (response) => { onSucccess: (response) => {
setParseData(response.data); setParseData(response.data);
if (onSuccess) onSuccess(response); if (onSuccess) onSuccess(response);

View File

@ -1,12 +1,13 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { assertIsNode } from '../utils/utils'; import { assertIsNode } from '../utils/utils';
function useClickedOutside({ref, callback}: {ref: React.RefObject<HTMLElement>, callback: Function}) { function useClickedOutside({ ref, callback }: { ref: React.RefObject<HTMLElement>, callback?: () => void }) {
useEffect(() => { useEffect(() => {
function handleClickOutside(event: MouseEvent) { function handleClickOutside(event: MouseEvent) {
assertIsNode(event.target); assertIsNode(event.target);
if (ref.current && !ref.current.contains(event.target)) { if (ref.current && !ref.current.contains(event.target)) {
callback() if (callback) callback();
} }
} }
document.addEventListener('mouseup', handleClickOutside); document.addEventListener('mouseup', handleClickOutside);

View File

@ -1,18 +1,19 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import useClickedOutside from './useClickedOutside'; import useClickedOutside from './useClickedOutside';
function useDropdown() { function useDropdown() {
const [isActive, setIsActive] = useState(false); const [isActive, setIsActive] = useState(false);
const ref = useRef(null); const ref = useRef(null);
useClickedOutside({ref: ref, callback: () => setIsActive(false)}) useClickedOutside({ ref, callback: () => { setIsActive(false); } })
return { return {
ref: ref, ref,
isActive: isActive, isActive,
setIsActive: setIsActive, setIsActive,
toggle: () => setIsActive(!isActive), toggle: () => { setIsActive(!isActive); },
hide: () => setIsActive(false) hide: () => { setIsActive(false); }
}; };
}; };

View File

@ -1,8 +1,8 @@
import { useState, useEffect } from 'react'; import { useEffect, useState } from 'react';
function getStorageValue<ValueType>(key: string, defaultValue: ValueType) { function getStorageValue<ValueType>(key: string, defaultValue: ValueType) {
const saved = localStorage.getItem(key); const saved = localStorage.getItem(key);
const initial = saved ? JSON.parse(saved!) : undefined; const initial = saved ? JSON.parse(saved) : undefined;
return initial || defaultValue; return initial || defaultValue;
} }

View File

@ -1,5 +1,6 @@
import { useState } from 'react' import { useState } from 'react'
import { ErrorInfo } from '../components/BackendError';
import { type ErrorInfo } from '../components/BackendError';
import { postNewRSForm } from '../utils/backendAPI'; import { postNewRSForm } from '../utils/backendAPI';
function useNewRSForm() { function useNewRSForm() {
@ -7,20 +8,21 @@ function useNewRSForm() {
const [error, setError] = useState<ErrorInfo>(undefined); const [error, setError] = useState<ErrorInfo>(undefined);
async function createSchema({ data, file, onSuccess }: { async function createSchema({ data, file, onSuccess }: {
data: any, file?: File, data: any
file?: File
onSuccess: (newID: string) => void onSuccess: (newID: string) => void
}) { }) {
setError(undefined); setError(undefined);
if (file) { if (file) {
data['file'] = file; data.file = file;
data['fileName'] = file.name; data.fileName = file.name;
} }
postNewRSForm({ await postNewRSForm({
data: data, data,
showError: true, showError: true,
setLoading: setLoading, setLoading,
onError: error => setError(error), onError: error => { setError(error); },
onSucccess: response => onSuccess(response.data.id) onSucccess: response => { onSuccess(response.data.id); }
}); });
} }

View File

@ -1,7 +1,8 @@
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { CalculateStats, IRSForm } from '../utils/models'
import { ErrorInfo } from '../components/BackendError'; import { type ErrorInfo } from '../components/BackendError';
import { getRSFormDetails } from '../utils/backendAPI'; import { getRSFormDetails } from '../utils/backendAPI';
import { CalculateStats, type IRSForm } from '../utils/models'
export function useRSFormDetails({ target }: { target?: string }) { export function useRSFormDetails({ target }: { target?: string }) {
const [schema, setInnerSchema] = useState<IRSForm | undefined>(undefined); const [schema, setInnerSchema] = useState<IRSForm | undefined>(undefined);
@ -21,20 +22,20 @@ export function useRSFormDetails({target}: {target?: string}) {
if (!target) { if (!target) {
return; return;
} }
getRSFormDetails(target, { await getRSFormDetails(target, {
showError: true, showError: true,
setLoading: setLoading, setLoading,
onError: error => setError(error), onError: error => { setError(error); },
onSucccess: (response) => setSchema(response.data) onSucccess: (response) => { setSchema(response.data); }
}); });
}, [target]); }, [target]);
async function reload() { async function reload() {
fetchData(); await fetchData();
} }
useEffect(() => { useEffect(() => {
fetchData(); fetchData().catch((error) => { setError(error); });
}, [fetchData]) }, [fetchData])
return { schema, setSchema, reload, error, setError, loading }; return { schema, setSchema, reload, error, setError, loading };

View File

@ -1,7 +1,8 @@
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { IRSForm } from '../utils/models'
import { ErrorInfo } from '../components/BackendError'; import { type ErrorInfo } from '../components/BackendError';
import { getRSForms } from '../utils/backendAPI'; import { getRSForms } from '../utils/backendAPI';
import { type IRSForm } from '../utils/models'
export enum FilterType { export enum FilterType {
PERSONAL = 'personal', PERSONAL = 'personal',
@ -19,11 +20,11 @@ export function useRSForms() {
const [error, setError] = useState<ErrorInfo>(undefined); const [error, setError] = useState<ErrorInfo>(undefined);
const loadList = useCallback(async (filter: RSFormsFilter) => { const loadList = useCallback(async (filter: RSFormsFilter) => {
getRSForms(filter, { await getRSForms(filter, {
showError: true, showError: true,
setLoading: setLoading, setLoading,
onError: error => setError(error), onError: error => { setError(error); },
onSucccess: response => setRSForms(response.data) onSucccess: response => { setRSForms(response.data); }
}); });
}, []); }, []);

View File

@ -1,7 +1,8 @@
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { IUserProfile } from '../utils/models'
import { ErrorInfo } from '../components/BackendError' import { type ErrorInfo } from '../components/BackendError'
import { getProfile } from '../utils/backendAPI' import { getProfile } from '../utils/backendAPI'
import { type IUserProfile } from '../utils/models'
export function useUserProfile() { export function useUserProfile() {
const [user, setUser] = useState<IUserProfile | undefined>(undefined); const [user, setUser] = useState<IUserProfile | undefined>(undefined);
@ -12,17 +13,17 @@ export function useUserProfile() {
async () => { async () => {
setError(undefined); setError(undefined);
setUser(undefined); setUser(undefined);
getProfile({ await getProfile({
showError: true, showError: true,
setLoading: setLoading, setLoading,
onError: error => setError(error), onError: error => { setError(error); },
onSucccess: response => setUser(response.data) onSucccess: response => { setUser(response.data); }
}); });
}, [setUser] }, [setUser]
) )
useEffect(() => { useEffect(() => {
fetchUser(); fetchUser().catch((error) => { setError(error); });
}, [fetchUser]) }, [fetchUser])
return { user, fetchUser, error, loading }; return { user, fetchUser, error, loading };

View File

@ -1,17 +1,19 @@
'use client'; 'use client';
import React from 'react';
import axios from 'axios';
import './index.css'; import './index.css';
import 'react-toastify/dist/ReactToastify.css'; import 'react-toastify/dist/ReactToastify.css';
import axios from 'axios';
import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import { ErrorBoundary } from 'react-error-boundary';
import { IntlProvider } from 'react-intl';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import App from './App'; import App from './App';
import ErrorFallback from './components/ErrorFallback';
import { AuthState } from './context/AuthContext'; import { AuthState } from './context/AuthContext';
import { ThemeState } from './context/ThemeContext'; import { ThemeState } from './context/ThemeContext';
import { IntlProvider } from 'react-intl';
import { ErrorBoundary } from 'react-error-boundary';
import ErrorFallback from './components/ErrorFallback';
import { UsersState } from './context/UsersContext'; import { UsersState } from './context/UsersContext';
axios.defaults.withCredentials = true axios.defaults.withCredentials = true
@ -28,7 +30,7 @@ const resetState = () => {
const logError = (error: Error, info: { componentStack: string }) => { const logError = (error: Error, info: { componentStack: string }) => {
console.log('Error fallback: ' + error.message) console.log('Error fallback: ' + error.message)
console.log('Component stack: ' + info) console.log('Component stack: ' + info.componentStack)
}; };
root.render( root.render(

View File

@ -1,4 +1,5 @@
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
function HomePage() { function HomePage() {

View File

@ -1,13 +1,13 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import TextInput from '../components/Common/TextInput';
import Form from '../components/Common/Form';
import { useAuth } from '../context/AuthContext';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import SubmitButton from '../components/Common/SubmitButton';
import BackendError from '../components/BackendError'; import BackendError from '../components/BackendError';
import InfoMessage from '../components/InfoMessage'; import Form from '../components/Common/Form';
import SubmitButton from '../components/Common/SubmitButton';
import TextInput from '../components/Common/TextInput';
import TextURL from '../components/Common/TextURL'; import TextURL from '../components/Common/TextURL';
import InfoMessage from '../components/InfoMessage';
import { useAuth } from '../context/AuthContext';
function LoginPage() { function LoginPage() {
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
@ -19,7 +19,7 @@ function LoginPage() {
useEffect(() => { useEffect(() => {
const name = new URLSearchParams(search).get('username'); const name = new URLSearchParams(search).get('username');
setUsername(name || ''); setUsername(name ?? '');
setPassword(''); setPassword('');
}, [search]); }, [search]);
@ -30,29 +30,28 @@ function LoginPage() {
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
if (!loading) { if (!loading) {
login(username, password, () => navigate('/rsforms?filter=personal')); login(username, password, () => { navigate('/rsforms?filter=personal'); });
} }
}; };
return ( return (
<div className='w-full py-2'> { user ? <div className='w-full py-2'> { user
<InfoMessage message={`Вы вошли в систему как ${user.username}`} /> ? <InfoMessage message={`Вы вошли в систему как ${user.username}`} />
: : <Form title='Ввод данных пользователя' onSubmit={handleSubmit} widthClass='w-[20rem]'>
<Form title='Ввод данных пользователя' onSubmit={handleSubmit} widthClass='w-[20rem]'>
<TextInput id='username' <TextInput id='username'
label='Имя пользователя' label='Имя пользователя'
required required
type='text' type='text'
value={username} value={username}
autoFocus autoFocus
onChange={event => setUsername(event.target.value)} onChange={event => { setUsername(event.target.value); }}
/> />
<TextInput id='password' <TextInput id='password'
label='Пароль' label='Пароль'
required required
type='password' type='password'
value={password} value={password}
onChange={event => setPassword(event.target.value)} onChange={event => { setPassword(event.target.value); }}
/> />
<div className='flex items-center justify-between mt-4'> <div className='flex items-center justify-between mt-4'>

View File

@ -1,17 +1,17 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import TextInput from '../components/Common/TextInput';
import Form from '../components/Common/Form';
import SubmitButton from '../components/Common/SubmitButton';
import BackendError from '../components/BackendError';
import { IRSFormCreateData } from '../utils/models';
import RequireAuth from '../components/RequireAuth';
import useNewRSForm from '../hooks/useNewRSForm';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import TextArea from '../components/Common/TextArea'; import { toast } from 'react-toastify';
import BackendError from '../components/BackendError';
import Checkbox from '../components/Common/Checkbox'; import Checkbox from '../components/Common/Checkbox';
import FileInput from '../components/Common/FileInput'; import FileInput from '../components/Common/FileInput';
import { toast } from 'react-toastify'; import Form from '../components/Common/Form';
import SubmitButton from '../components/Common/SubmitButton';
import TextArea from '../components/Common/TextArea';
import TextInput from '../components/Common/TextInput';
import RequireAuth from '../components/RequireAuth';
import useNewRSForm from '../hooks/useNewRSForm';
import { type IRSFormCreateData } from '../utils/models';
function RSFormCreatePage() { function RSFormCreatePage() {
const navigate = useNavigate(); const navigate = useNavigate();
@ -42,19 +42,20 @@ function RSFormCreatePage() {
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
if (!loading) { if (loading) {
const data: IRSFormCreateData = { return;
'title': title,
'alias': alias,
'comment': comment,
'is_common': common,
};
createSchema({
data: data,
file: file,
onSuccess: onSuccess
});
} }
const data: IRSFormCreateData = {
title,
alias,
comment,
is_common: common
};
void createSchema({
data,
file,
onSuccess
});
}; };
return ( return (
@ -64,23 +65,23 @@ function RSFormCreatePage() {
required={!file} required={!file}
placeholder={file && 'Загрузить из файла'} placeholder={file && 'Загрузить из файла'}
value={title} value={title}
onChange={event => setTitle(event.target.value)} onChange={event => { setTitle(event.target.value); }}
/> />
<TextInput id='alias' label='Сокращение' type='text' <TextInput id='alias' label='Сокращение' type='text'
required={!file} required={!file}
value={alias} value={alias}
placeholder={file && 'Загрузить из файла'} placeholder={file && 'Загрузить из файла'}
widthClass='max-w-sm' widthClass='max-w-sm'
onChange={event => setAlias(event.target.value)} onChange={event => { setAlias(event.target.value); }}
/> />
<TextArea id='comment' label='Комментарий' <TextArea id='comment' label='Комментарий'
value={comment} value={comment}
placeholder={file && 'Загрузить из файла'} placeholder={file && 'Загрузить из файла'}
onChange={event => setComment(event.target.value)} onChange={event => { setComment(event.target.value); }}
/> />
<Checkbox id='common' label='Общедоступная схема' <Checkbox id='common' label='Общедоступная схема'
value={common} value={common}
onChange={event => setCommon(event.target.checked)} onChange={event => { setCommon(event.target.checked); }}
/> />
<FileInput id='trs' label='Загрузить *.trs' <FileInput id='trs' label='Загрузить *.trs'
acceptType='.trs' acceptType='.trs'

View File

@ -1,15 +1,16 @@
import { type AxiosResponse } from 'axios';
import { useCallback, useLayoutEffect, useState } from 'react'; import { useCallback, useLayoutEffect, useState } from 'react';
import { useRSForm } from '../../context/RSFormContext';
import { CstType, EditMode, INewCstData } from '../../utils/models';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import TextArea from '../../components/Common/TextArea';
import ExpressionEditor from './ExpressionEditor';
import SubmitButton from '../../components/Common/SubmitButton'; import SubmitButton from '../../components/Common/SubmitButton';
import TextArea from '../../components/Common/TextArea';
import { DumpBinIcon, SaveIcon, SmallPlusIcon } from '../../components/Icons';
import { useRSForm } from '../../context/RSFormContext';
import { type CstType, EditMode, type INewCstData } from '../../utils/models';
import { createAliasFor, getCstTypeLabel } from '../../utils/staticUI'; import { createAliasFor, getCstTypeLabel } from '../../utils/staticUI';
import ConstituentsSideList from './ConstituentsSideList'; import ConstituentsSideList from './ConstituentsSideList';
import { DumpBinIcon, SaveIcon, SmallPlusIcon } from '../../components/Icons';
import CreateCstModal from './CreateCstModal'; import CreateCstModal from './CreateCstModal';
import { AxiosResponse } from 'axios'; import ExpressionEditor from './ExpressionEditor';
function ConstituentEditor() { function ConstituentEditor() {
const { const {
@ -30,7 +31,9 @@ function ConstituentEditor() {
useLayoutEffect(() => { useLayoutEffect(() => {
if (schema?.items && schema?.items.length > 0) { if (schema?.items && schema?.items.length > 0) {
setActiveID((prev) => (prev || schema?.items![0].id)); // TODO: figure out why schema.items could be undef?
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
setActiveID((prev) => (prev ?? schema?.items![0].id ?? undefined));
} }
}, [schema, setActiveID]) }, [schema, setActiveID])
@ -38,30 +41,30 @@ function ConstituentEditor() {
if (activeCst) { if (activeCst) {
setAlias(activeCst.alias); setAlias(activeCst.alias);
setType(getCstTypeLabel(activeCst.cstType)); setType(getCstTypeLabel(activeCst.cstType));
setConvention(activeCst.convention || ''); setConvention(activeCst.convention ?? '');
setTerm(activeCst.term?.raw || ''); setTerm(activeCst.term?.raw ?? '');
setTextDefinition(activeCst.definition?.text?.raw || ''); setTextDefinition(activeCst.definition?.text?.raw ?? '');
setExpression(activeCst.definition?.formal || ''); setExpression(activeCst.definition?.formal ?? '');
setTypification(activeCst?.parse?.typification || 'N/A'); setTypification(activeCst?.parse?.typification ?? 'N/A');
} }
}, [activeCst]); }, [activeCst]);
const handleSubmit = const handleSubmit =
async (event: React.FormEvent<HTMLFormElement>) => { (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
if (!processing) { if (!processing) {
const data = { const data = {
'alias': alias, alias: alias,
'convention': convention, convention: convention,
'definition_formal': expression, definition_formal: expression,
'definition_text': { definition_text: {
'raw': textDefinition, raw: textDefinition,
'resolved': '', resolved: ''
}, },
'term': { term: {
'raw': term, raw: term,
'resolved': '', resolved: '',
'forms': activeCst?.term?.forms || [], forms: activeCst?.term?.forms ?? []
} }
}; };
cstUpdate(data, () => toast.success('Изменения сохранены')); cstUpdate(data, () => toast.success('Изменения сохранены'));
@ -69,12 +72,12 @@ function ConstituentEditor() {
}; };
const handleDelete = useCallback( const handleDelete = useCallback(
async () => { () => {
if (!activeID || !schema?.items || !window.confirm('Вы уверены, что хотите удалить конституенту?')) { if (!activeID || !schema?.items || !window.confirm('Вы уверены, что хотите удалить конституенту?')) {
return; return;
} }
const data = { const data = {
'items': [{'id': activeID}] items: [{ id: activeID }]
} }
const index = schema.items.findIndex((cst) => cst.id === activeID); const index = schema.items.findIndex((cst) => cst.id === activeID);
if (index !== -1 && index + 1 < schema.items.length) { if (index !== -1 && index + 1 < schema.items.length) {
@ -84,7 +87,7 @@ function ConstituentEditor() {
}, [activeID, schema, setActiveID, cstDelete]); }, [activeID, schema, setActiveID, cstDelete]);
const handleAddNew = useCallback( const handleAddNew = useCallback(
async (csttype?: CstType) => { (csttype?: CstType) => {
if (!activeID || !schema?.items) { if (!activeID || !schema?.items) {
return; return;
} }
@ -92,15 +95,14 @@ function ConstituentEditor() {
setShowCstModal(true); setShowCstModal(true);
} else { } else {
const data: INewCstData = { const data: INewCstData = {
'csttype': csttype, csttype: csttype,
'alias': createAliasFor(csttype, schema!), alias: createAliasFor(csttype, schema),
'insert_after': activeID insert_after: activeID
} }
cstCreate(data, cstCreate(data,
async (response: AxiosResponse) => { (response: AxiosResponse) => {
// navigate(`/rsforms/${schema.id}?tab=${RSFormTabsList.CST_EDIT}&active=${response.data['new_cst']['id']}`); setActiveID(response.data.new_cst.id);
setActiveID(response.data['new_cst']['id']); toast.success(`Конституента добавлена: ${response.data.new_cst.alias as string}`);
toast.success(`Конституента добавлена: ${response.data['new_cst']['alias']}`);
}); });
} }
}, [activeID, schema, cstCreate, setActiveID]); }, [activeID, schema, cstCreate, setActiveID]);
@ -113,12 +115,11 @@ function ConstituentEditor() {
toast.info('Изменение типа в разработке'); toast.info('Изменение типа в разработке');
}, []); }, []);
return ( return (
<div className='flex items-start w-full gap-2'> <div className='flex items-start w-full gap-2'>
<CreateCstModal <CreateCstModal
show={showCstModal} show={showCstModal}
toggle={() => setShowCstModal(!showCstModal)} toggle={() => { setShowCstModal(!showCstModal); }}
onCreate={handleAddNew} onCreate={handleAddNew}
defaultType={activeCst?.cstType as CstType} defaultType={activeCst?.cstType as CstType}
/> />
@ -158,7 +159,7 @@ function ConstituentEditor() {
title='Создать конституенты после данной' title='Создать конституенты после данной'
className='px-1 py-1 font-bold rounded-full whitespace-nowrap disabled:cursor-not-allowed clr-btn-clear' className='px-1 py-1 font-bold rounded-full whitespace-nowrap disabled:cursor-not-allowed clr-btn-clear'
disabled={!isEditable} disabled={!isEditable}
onClick={() => handleAddNew()} onClick={() => { handleAddNew(); }}
> >
<SmallPlusIcon size={5} color={isEditable ? 'text-green' : ''} /> <SmallPlusIcon size={5} color={isEditable ? 'text-green' : ''} />
</button> </button>
@ -178,8 +179,8 @@ function ConstituentEditor() {
value={term} value={term}
disabled={!isEditable} disabled={!isEditable}
spellCheck spellCheck
onChange={event => setTerm(event.target.value)} onChange={event => { setTerm(event.target.value); }}
onFocus={() => setEditMode(EditMode.TEXT)} onFocus={() => { setEditMode(EditMode.TEXT); }}
/> />
<TextArea id='typification' label='Типизация' <TextArea id='typification' label='Типизация'
rows={1} rows={1}
@ -191,8 +192,8 @@ function ConstituentEditor() {
value={expression} value={expression}
disabled={!isEditable} disabled={!isEditable}
isActive={editMode === 'rslang'} isActive={editMode === 'rslang'}
toggleEditMode={() => setEditMode(EditMode.RSLANG)} toggleEditMode={() => { setEditMode(EditMode.RSLANG); }}
onChange={event => setExpression(event.target.value)} onChange={event => { setExpression(event.target.value); }}
setValue={setExpression} setValue={setExpression}
setTypification={setTypification} setTypification={setTypification}
/> />
@ -202,8 +203,8 @@ function ConstituentEditor() {
value={textDefinition} value={textDefinition}
disabled={!isEditable} disabled={!isEditable}
spellCheck spellCheck
onChange={event => setTextDefinition(event.target.value)} onChange={event => { setTextDefinition(event.target.value); }}
onFocus={() => setEditMode(EditMode.TEXT)} onFocus={() => { setEditMode(EditMode.TEXT); }}
/> />
<TextArea id='convention' label='Конвенция / Комментарий' <TextArea id='convention' label='Конвенция / Комментарий'
placeholder='Договоренность об интерпретации неопределяемого понятия&#x000D;&#x000A;Комментарий к производному понятию' placeholder='Договоренность об интерпретации неопределяемого понятия&#x000D;&#x000A;Комментарий к производному понятию'
@ -211,8 +212,8 @@ function ConstituentEditor() {
value={convention} value={convention}
disabled={!isEditable} disabled={!isEditable}
spellCheck spellCheck
onChange={event => setConvention(event.target.value)} onChange={event => { setConvention(event.target.value); }}
onFocus={() => setEditMode(EditMode.TEXT)} onFocus={() => { setEditMode(EditMode.TEXT); }}
/> />
<div className='flex justify-center w-full mt-2'> <div className='flex justify-center w-full mt-2'>
<SubmitButton <SubmitButton

View File

@ -1,9 +1,10 @@
import { useCallback, useState, useMemo, useEffect } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useRSForm } from '../../context/RSFormContext';
import { CstType, IConstituenta, matchConstituenta } from '../../utils/models';
import Checkbox from '../../components/Common/Checkbox'; import Checkbox from '../../components/Common/Checkbox';
import DataTableThemed from '../../components/Common/DataTableThemed'; import DataTableThemed from '../../components/Common/DataTableThemed';
import { useRSForm } from '../../context/RSFormContext';
import useLocalStorage from '../../hooks/useLocalStorage'; import useLocalStorage from '../../hooks/useLocalStorage';
import { CstType, type IConstituenta, matchConstituenta } from '../../utils/models';
import { extractGlobals } from '../../utils/staticUI'; import { extractGlobals } from '../../utils/staticUI';
interface ConstituentsSideListProps { interface ConstituentsSideListProps {
@ -12,7 +13,7 @@ interface ConstituentsSideListProps {
function ConstituentsSideList({ expression }: ConstituentsSideListProps) { function ConstituentsSideList({ expression }: ConstituentsSideListProps) {
const { schema, setActiveID } = useRSForm(); const { schema, setActiveID } = useRSForm();
const [filteredData, setFilteredData] = useState<IConstituenta[]>(schema?.items || []); const [filteredData, setFilteredData] = useState<IConstituenta[]>(schema?.items ?? []);
const [filterText, setFilterText] = useLocalStorage('side-filter-text', '') const [filterText, setFilterText] = useLocalStorage('side-filter-text', '')
const [onlyExpression, setOnlyExpression] = useLocalStorage('side-filter-flag', false); const [onlyExpression, setOnlyExpression] = useLocalStorage('side-filter-flag', false);
@ -21,9 +22,9 @@ function ConstituentsSideList({expression}: ConstituentsSideListProps) {
setFilteredData([]); setFilteredData([]);
} else if (onlyExpression) { } else if (onlyExpression) {
const aliases = extractGlobals(expression); const aliases = extractGlobals(expression);
let filtered = schema?.items.filter((cst) => aliases.has(cst.alias)); const filtered = schema?.items.filter((cst) => aliases.has(cst.alias));
const names = filtered.map(cst => cst.alias) const names = filtered.map(cst => cst.alias)
const diff = Array.from(aliases).filter(name => names.indexOf(name) < 0); const diff = Array.from(aliases).filter(name => !names.includes(name));
if (diff.length > 0) { if (diff.length > 0) {
diff.forEach( diff.forEach(
(alias, i) => filtered.push({ (alias, i) => filtered.push({
@ -58,7 +59,7 @@ function ConstituentsSideList({expression}: ConstituentsSideListProps) {
{ {
id: 'id', id: 'id',
selector: (cst: IConstituenta) => cst.id, selector: (cst: IConstituenta) => cst.id,
omit: true, omit: true
}, },
{ {
name: 'ID', name: 'ID',
@ -70,26 +71,27 @@ function ConstituentsSideList({expression}: ConstituentsSideListProps) {
{ {
when: (cst: IConstituenta) => cst.id <= 0, when: (cst: IConstituenta) => cst.id <= 0,
classNames: ['bg-[#ffc9c9]', 'dark:bg-[#592b2b]'] classNames: ['bg-[#ffc9c9]', 'dark:bg-[#592b2b]']
}, }
], ]
}, },
{ {
name: 'Описание', name: 'Описание',
id: 'description', id: 'description',
selector: (cst: IConstituenta) => cst.term?.resolved || cst.definition?.text.resolved || cst.definition?.formal || cst.convention || '', selector: (cst: IConstituenta) =>
cst.term?.resolved ?? cst.definition?.text.resolved ?? cst.definition?.formal ?? cst.convention ?? '',
minWidth: '350px', minWidth: '350px',
wrap: true, wrap: true,
conditionalCellStyles: [ conditionalCellStyles: [
{ {
when: (cst: IConstituenta) => cst.id <= 0, when: (cst: IConstituenta) => cst.id <= 0,
classNames: ['bg-[#ffc9c9]', 'dark:bg-[#592b2b]'] classNames: ['bg-[#ffc9c9]', 'dark:bg-[#592b2b]']
}, }
], ]
}, },
{ {
name: 'Выражение', name: 'Выражение',
id: 'expression', id: 'expression',
selector: (cst: IConstituenta) => cst.definition?.formal || '', selector: (cst: IConstituenta) => cst.definition?.formal ?? '',
minWidth: '200px', minWidth: '200px',
hide: 1600, hide: 1600,
grow: 2, grow: 2,
@ -98,8 +100,8 @@ function ConstituentsSideList({expression}: ConstituentsSideListProps) {
{ {
when: (cst: IConstituenta) => cst.id <= 0, when: (cst: IConstituenta) => cst.id <= 0,
classNames: ['bg-[#ffc9c9]', 'dark:bg-[#592b2b]'] classNames: ['bg-[#ffc9c9]', 'dark:bg-[#592b2b]']
}, }
], ]
} }
], [] ], []
); );
@ -112,7 +114,7 @@ function ConstituentsSideList({expression}: ConstituentsSideListProps) {
className='w-full px-2 outline-none dark:bg-gray-700 hover:text-clip' className='w-full px-2 outline-none dark:bg-gray-700 hover:text-clip'
placeholder='текст для фильтрации списка' placeholder='текст для фильтрации списка'
value={filterText} value={filterText}
onChange={event => setFilterText(event.target.value)} onChange={event => { setFilterText(event.target.value); }}
disabled={onlyExpression} disabled={onlyExpression}
/> />
</div> </div>
@ -120,7 +122,7 @@ function ConstituentsSideList({expression}: ConstituentsSideListProps) {
<Checkbox <Checkbox
label='из выражения' label='из выражения'
value={onlyExpression} value={onlyExpression}
onChange={event => setOnlyExpression(event.target.checked)} onChange={event => { setOnlyExpression(event.target.checked); }}
/> />
</div> </div>
</div> </div>

View File

@ -1,15 +1,16 @@
import { CstType, IConstituenta, INewCstData, ParsingStatus, ValueClass, inferStatus } from '../../utils/models' import { type AxiosResponse } from 'axios';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import DataTableThemed from '../../components/Common/DataTableThemed';
import { useRSForm } from '../../context/RSFormContext';
import Button from '../../components/Common/Button';
import { ArrowDownIcon, ArrowUpIcon, ArrowsRotateIcon, DumpBinIcon, SmallPlusIcon } from '../../components/Icons';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import Button from '../../components/Common/Button';
import DataTableThemed from '../../components/Common/DataTableThemed';
import Divider from '../../components/Common/Divider'; import Divider from '../../components/Common/Divider';
import { ArrowDownIcon, ArrowsRotateIcon, ArrowUpIcon, DumpBinIcon, SmallPlusIcon } from '../../components/Icons';
import { useRSForm } from '../../context/RSFormContext';
import { useConceptTheme } from '../../context/ThemeContext';
import { CstType, type IConstituenta, type INewCstData, inferStatus, ParsingStatus, ValueClass } from '../../utils/models'
import { createAliasFor, getCstTypeLabel, getCstTypePrefix, getStatusInfo, getTypeLabel } from '../../utils/staticUI'; import { createAliasFor, getCstTypeLabel, getCstTypePrefix, getStatusInfo, getTypeLabel } from '../../utils/staticUI';
import CreateCstModal from './CreateCstModal'; import CreateCstModal from './CreateCstModal';
import { AxiosResponse } from 'axios';
import { useConceptTheme } from '../../context/ThemeContext';
interface ConstituentsTableProps { interface ConstituentsTableProps {
onOpenEdit: (cst: IConstituenta) => void onOpenEdit: (cst: IConstituenta) => void
@ -35,9 +36,9 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
const handleSelectionChange = useCallback( const handleSelectionChange = useCallback(
({ selectedRows }: { ({ selectedRows }: {
allSelected: boolean; allSelected: boolean
selectedCount: number; selectedCount: number
selectedRows: IConstituenta[]; selectedRows: IConstituenta[]
}) => { }) => {
setSelected(selectedRows.map((cst) => cst.id)); setSelected(selectedRows.map((cst) => cst.id));
}, [setSelected]); }, [setSelected]);
@ -48,10 +49,10 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
return; return;
} }
const data = { const data = {
'items': selected.map(id => { return {'id': id }; }), items: selected.map(id => { return { id }; })
} }
const deletedNamed = selected.map(id => schema.items?.find((cst) => cst.id === id)?.alias); const deletedNames = selected.map(id => schema.items?.find((cst) => cst.id === id)?.alias);
cstDelete(data, () => toast.success(`Конституенты удалены: ${deletedNamed}`)); cstDelete(data, () => toast.success(`Конституенты удалены: ${deletedNames.toString()}`));
}, [selected, schema?.items, cstDelete]); }, [selected, schema?.items, cstDelete]);
// Move selected cst up // Move selected cst up
@ -61,7 +62,7 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
return; return;
} }
const currentIndex = schema.items.reduce((prev, cst, index) => { const currentIndex = schema.items.reduce((prev, cst, index) => {
if (selected.indexOf(cst.id) < 0) { if (!selected.includes(cst.id)) {
return prev; return prev;
} else if (prev === -1) { } else if (prev === -1) {
return index; return index;
@ -70,22 +71,21 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
}, -1); }, -1);
const insertIndex = Math.max(0, currentIndex - 1) + 1 const insertIndex = Math.max(0, currentIndex - 1) + 1
const data = { const data = {
'items': selected.map(id => { return {'id': id }; }), items: selected.map(id => { return { id }; }),
'move_to': insertIndex move_to: insertIndex
} }
cstMoveTo(data); cstMoveTo(data);
}, [selected, schema?.items, cstMoveTo]); }, [selected, schema?.items, cstMoveTo]);
// Move selected cst down // Move selected cst down
const handleMoveDown = useCallback( const handleMoveDown = useCallback(
async () => { () => {
if (!schema?.items || selected.length === 0) { if (!schema?.items || selected.length === 0) {
return; return;
} }
let count = 0; let count = 0;
const currentIndex = schema.items.reduce((prev, cst, index) => { const currentIndex = schema.items.reduce((prev, cst, index) => {
if (selected.indexOf(cst.id) < 0) { if (!selected.includes(cst.id)) {
return prev; return prev;
} else { } else {
count += 1; count += 1;
@ -97,8 +97,8 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
}, -1); }, -1);
const insertIndex = Math.min(schema.items.length - 1, currentIndex - count + 2) + 1 const insertIndex = Math.min(schema.items.length - 1, currentIndex - count + 2) + 1
const data = { const data = {
'items': selected.map(id => { return {'id': id }; }), items: selected.map(id => { return { id }; }),
'move_to': insertIndex move_to: insertIndex
} }
cstMoveTo(data); cstMoveTo(data);
}, [selected, schema?.items, cstMoveTo]); }, [selected, schema?.items, cstMoveTo]);
@ -110,18 +110,21 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
// Add new constituent // Add new constituent
const handleAddNew = useCallback((csttype?: CstType) => { const handleAddNew = useCallback((csttype?: CstType) => {
if (!schema) {
return;
}
if (!csttype) { if (!csttype) {
setShowCstModal(true); setShowCstModal(true);
} else { } else {
let data: INewCstData = { const data: INewCstData = {
'csttype': csttype, csttype,
'alias': createAliasFor(csttype, schema!) alias: createAliasFor(csttype, schema)
} }
if (selected.length > 0) { if (selected.length > 0) {
data['insert_after'] = selected[selected.length - 1] data.insert_after = selected[selected.length - 1]
} }
cstCreate(data, (response: AxiosResponse) => cstCreate(data, (response: AxiosResponse) =>
toast.success(`Добавлена конституента ${response.data['new_cst']['alias']}`)); toast.success(`Добавлена конституента ${response.data.new_cst.alias as string}`));
} }
}, [schema, selected, cstCreate]); }, [schema, selected, cstCreate]);
@ -135,7 +138,7 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
} }
switch (event.key) { switch (event.key) {
case 'ArrowUp': handleMoveUp(); return; case 'ArrowUp': handleMoveUp(); return;
case 'ArrowDown': handleMoveDown(); return; case 'ArrowDown': handleMoveDown();
} }
}, [isEditable, selected, handleMoveUp, handleMoveDown]); }, [isEditable, selected, handleMoveUp, handleMoveDown]);
@ -145,7 +148,7 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
name: 'ID', name: 'ID',
id: 'id', id: 'id',
selector: (cst: IConstituenta) => cst.id, selector: (cst: IConstituenta) => cst.id,
omit: true, omit: true
}, },
{ {
name: 'Статус', name: 'Статус',
@ -170,8 +173,8 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
{ {
when: (cst: IConstituenta) => cst.parse?.status === ParsingStatus.VERIFIED && cst.parse?.valueClass === ValueClass.PROPERTY, when: (cst: IConstituenta) => cst.parse?.status === ParsingStatus.VERIFIED && cst.parse?.valueClass === ValueClass.PROPERTY,
classNames: ['bg-[#a5e9fa]', 'dark:bg-[#36899e]'] classNames: ['bg-[#a5e9fa]', 'dark:bg-[#36899e]']
}, }
], ]
}, },
{ {
name: 'Имя', name: 'Имя',
@ -192,8 +195,8 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
{ {
when: (cst: IConstituenta) => cst.parse?.status === ParsingStatus.VERIFIED && cst.parse?.valueClass === ValueClass.PROPERTY, when: (cst: IConstituenta) => cst.parse?.status === ParsingStatus.VERIFIED && cst.parse?.valueClass === ValueClass.PROPERTY,
classNames: ['bg-[#a5e9fa]', 'dark:bg-[#36899e]'] classNames: ['bg-[#a5e9fa]', 'dark:bg-[#36899e]']
}, }
], ]
}, },
{ {
name: 'Тип', name: 'Тип',
@ -204,65 +207,70 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
maxWidth: '140px', maxWidth: '140px',
wrap: true, wrap: true,
reorder: true, reorder: true,
hide: 1600, hide: 1600
}, },
{ {
name: 'Термин', name: 'Термин',
id: 'term', id: 'term',
selector: (cst: IConstituenta) => cst.term?.resolved || cst.term?.raw || '', selector: (cst: IConstituenta) => cst.term?.resolved ?? cst.term?.raw ?? '',
width: '350px', width: '350px',
minWidth: '150px', minWidth: '150px',
maxWidth: '350px', maxWidth: '350px',
wrap: true, wrap: true,
reorder: true, reorder: true
}, },
{ {
name: 'Формальное определение', name: 'Формальное определение',
id: 'expression', id: 'expression',
selector: (cst: IConstituenta) => cst.definition?.formal || '', selector: (cst: IConstituenta) => cst.definition?.formal ?? '',
minWidth: '300px', minWidth: '300px',
maxWidth: '500px', maxWidth: '500px',
grow: 2, grow: 2,
wrap: true, wrap: true,
reorder: true, reorder: true
}, },
{ {
name: 'Текстовое определение', name: 'Текстовое определение',
id: 'definition', id: 'definition',
cell: (cst: IConstituenta) => ( cell: (cst: IConstituenta) => (
<div style={{ fontSize: 12 }}> <div style={{ fontSize: 12 }}>
{cst.definition?.text.resolved || cst.definition?.text.raw || ''} {cst.definition?.text.resolved ?? cst.definition?.text.raw ?? ''}
</div> </div>
), ),
minWidth: '200px', minWidth: '200px',
grow: 2, grow: 2,
wrap: true, wrap: true,
reorder: true, reorder: true
}, },
{ {
name: 'Конвенция / Комментарий', name: 'Конвенция / Комментарий',
id: 'convention', id: 'convention',
cell: (cst: IConstituenta) => <div style={{fontSize: 12}}>{cst.convention || ''}</div>, cell: (cst: IConstituenta) => <div style={{ fontSize: 12 }}>{cst.convention ?? ''}</div>,
minWidth: '100px', minWidth: '100px',
wrap: true, wrap: true,
reorder: true, reorder: true,
hide: 1800, hide: 1800
}, }
], [] ], []
); );
return (<> return (<>
<CreateCstModal <CreateCstModal
show={showCstModal} show={showCstModal}
toggle={() => setShowCstModal(!showCstModal)} toggle={() => { setShowCstModal(!showCstModal); }}
onCreate={handleAddNew} onCreate={handleAddNew}
/> />
<div className='w-full'> <div className='w-full'>
<div <div
className={'flex justify-start w-full gap-1 px-2 py-1 border-y items-center h-[2.2rem] clr-app' className={'flex justify-start w-full gap-1 px-2 py-1 border-y items-center h-[2.2rem] clr-app' +
+ (!noNavigation ? ' sticky z-10 top-[4rem]' : ' sticky z-10 top-[0rem]')} (!noNavigation ? ' sticky z-10 top-[4rem]' : ' sticky z-10 top-[0rem]')}
> >
<div className='mr-3 whitespace-nowrap'>Выбраны <span className='ml-2'><b>{selected.length}</b> из {schema?.stats?.count_all || 0}</span></div> <div className='mr-3 whitespace-nowrap'>
Выбраны
<span className='ml-2'>
<b>{selected.length}</b> из {schema?.stats?.count_all ?? 0}
</span>
</div>
{isEditable && <div className='flex justify-start w-full gap-1'> {isEditable && <div className='flex justify-start w-full gap-1'>
<Button <Button
tooltip='Переместить вверх' tooltip='Переместить вверх'
@ -296,23 +304,23 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
tooltip='Новая конституента' tooltip='Новая конституента'
icon={<SmallPlusIcon color='text-green' size={6}/>} icon={<SmallPlusIcon color='text-green' size={6}/>}
dense dense
onClick={() => handleAddNew()} onClick={() => { handleAddNew(); }}
/> />
{(Object.values(CstType)).map( {(Object.values(CstType)).map(
(typeStr) => { (typeStr) => {
const type = typeStr as CstType; const type = typeStr as CstType;
return <Button return <Button key={type}
text={`${getCstTypePrefix(type)}`} text={`${getCstTypePrefix(type)}`}
tooltip={getCstTypeLabel(type)} tooltip={getCstTypeLabel(type)}
dense dense
onClick={() =>handleAddNew(type)} onClick={() => { handleAddNew(type); }}
/>; />;
})} })}
</div>} </div>}
</div> </div>
<div className='w-full h-full' onKeyDown={handleTableKey} tabIndex={0}> <div className='w-full h-full' onKeyDown={handleTableKey} tabIndex={0}>
<DataTableThemed <DataTableThemed
data={schema!.items!} data={schema?.items ?? []}
columns={columns} columns={columns}
keyField='id' keyField='id'
noDataComponent={ noDataComponent={

View File

@ -1,8 +1,9 @@
import Modal from '../../components/Common/Modal';
import { CstType } from '../../utils/models';
import Select from 'react-select';
import { CstTypeSelector, getCstTypeLabel } from '../../utils/staticUI';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import Select from 'react-select';
import Modal from '../../components/Common/Modal';
import { type CstType } from '../../utils/models';
import { CstTypeSelector, getCstTypeLabel } from '../../utils/staticUI';
interface CreateCstModalProps { interface CreateCstModalProps {
show: boolean show: boolean
@ -41,7 +42,7 @@ function CreateCstModal({show, toggle, defaultType, onCreate}: CreateCstModalPro
placeholder='Выберите тип' placeholder='Выберите тип'
filterOption={null} filterOption={null}
value={selectedType && { value: selectedType, label: getCstTypeLabel(selectedType) }} value={selectedType && { value: selectedType, label: getCstTypeLabel(selectedType) }}
onChange={(data) => setSelectedType(data?.value)} onChange={(data) => { setSelectedType(data?.value); }}
/> />
</Modal> </Modal>
) )

View File

@ -1,17 +1,18 @@
import { type AxiosResponse } from 'axios';
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { toast } from 'react-toastify';
import Button from '../../components/Common/Button'; import Button from '../../components/Common/Button';
import Label from '../../components/Common/Label'; import Label from '../../components/Common/Label';
import { useRSForm } from '../../context/RSFormContext';
import { toast } from 'react-toastify';
import RSTokenButton from './RSTokenButton';
import { CstType, TokenID } from '../../utils/models';
import useCheckExpression from '../../hooks/useCheckExpression';
import ParsingResult from './ParsingResult';
import { Loader } from '../../components/Common/Loader'; import { Loader } from '../../components/Common/Loader';
import StatusBar from './StatusBar'; import { useRSForm } from '../../context/RSFormContext';
import { AxiosResponse } from 'axios'; import useCheckExpression from '../../hooks/useCheckExpression';
import { TextWrapper, getSymbolSubstitute } from './textEditing'; import { CstType, TokenID } from '../../utils/models';
import ParsingResult from './ParsingResult';
import RSLocalButton from './RSLocalButton'; import RSLocalButton from './RSLocalButton';
import RSTokenButton from './RSTokenButton';
import StatusBar from './StatusBar';
import { getSymbolSubstitute, TextWrapper } from './textEditing';
interface ExpressionEditorProps { interface ExpressionEditorProps {
id: string id: string
@ -19,7 +20,7 @@ interface ExpressionEditorProps {
isActive: boolean isActive: boolean
disabled?: boolean disabled?: boolean
placeholder?: string placeholder?: string
value: any value: string
onChange: (event: React.ChangeEvent<HTMLTextAreaElement>) => void onChange: (event: React.ChangeEvent<HTMLTextAreaElement>) => void
toggleEditMode: () => void toggleEditMode: () => void
setTypification: (typificaiton: string) => void setTypification: (typificaiton: string) => void
@ -32,7 +33,7 @@ function ExpressionEditor({
}: ExpressionEditorProps) { }: ExpressionEditorProps) {
const { schema, activeCst } = useRSForm(); const { schema, activeCst } = useRSForm();
const [isModified, setIsModified] = useState(false); const [isModified, setIsModified] = useState(false);
const { parseData, checkExpression, resetParse, loading } = useCheckExpression({schema: schema}); const { parseData, checkExpression, resetParse, loading } = useCheckExpression({ schema });
const expressionCtrl = useRef<HTMLTextAreaElement>(null); const expressionCtrl = useRef<HTMLTextAreaElement>(null);
useLayoutEffect(() => { useLayoutEffect(() => {
@ -41,14 +42,17 @@ function ExpressionEditor({
}, [activeCst, resetParse]); }, [activeCst, resetParse]);
const handleCheckExpression = useCallback(() => { const handleCheckExpression = useCallback(() => {
if (!activeCst) {
return;
}
const prefix = activeCst?.alias + (activeCst?.cstType === CstType.STRUCTURED ? '::=' : ':=='); const prefix = activeCst?.alias + (activeCst?.cstType === CstType.STRUCTURED ? '::=' : ':==');
const expression = prefix + value; const expression = prefix + value;
checkExpression(expression, (response: AxiosResponse) => { checkExpression(expression, (response: AxiosResponse) => {
// TODO: update cursor position // TODO: update cursor position
setIsModified(false); setIsModified(false);
setTypification(response.data['typification']); setTypification(response.data.typification);
toast.success('проверка завершена'); toast.success('проверка завершена');
}); }).catch(console.error);
}, [value, checkExpression, activeCst, setTypification]); }, [value, checkExpression, activeCst, setTypification]);
const handleEdit = useCallback((id: TokenID, key?: string) => { const handleEdit = useCallback((id: TokenID, key?: string) => {
@ -56,9 +60,9 @@ function ExpressionEditor({
toast.error('Нет доступа к полю редактирования формального выражения'); toast.error('Нет доступа к полю редактирования формального выражения');
return; return;
} }
let text = new TextWrapper(expressionCtrl.current); const text = new TextWrapper(expressionCtrl.current);
if (id === TokenID.ID_LOCAL) { if (id === TokenID.ID_LOCAL) {
text.insertChar(key!); text.insertChar(key ?? 'unknown_local');
} else { } else {
text.insertToken(id); text.insertToken(id);
} }
@ -74,8 +78,11 @@ function ExpressionEditor({
}, [setIsModified, onChange]); }, [setIsModified, onChange]);
const handleInput = useCallback((event: React.KeyboardEvent<HTMLTextAreaElement>) => { const handleInput = useCallback((event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (!expressionCtrl.current) {
return;
}
if (event.altKey) { if (event.altKey) {
let text = new TextWrapper(expressionCtrl.current!); const text = new TextWrapper(expressionCtrl.current);
if (text.processAltKey(event.key)) { if (text.processAltKey(event.key)) {
event.preventDefault(); event.preventDefault();
text.finalize(); text.finalize();
@ -86,7 +93,7 @@ function ExpressionEditor({
const newSymbol = getSymbolSubstitute(event.key); const newSymbol = getSymbolSubstitute(event.key);
if (newSymbol) { if (newSymbol) {
event.preventDefault(); event.preventDefault();
let text = new TextWrapper(expressionCtrl.current!); const text = new TextWrapper(expressionCtrl.current);
text.replaceWith(newSymbol); text.replaceWith(newSymbol);
text.finalize(); text.finalize();
setValue(text.value); setValue(text.value);

View File

@ -1,16 +1,17 @@
import { useCallback, useEffect, useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';
import Button from '../../components/Common/Button';
import Checkbox from '../../components/Common/Checkbox'; import Checkbox from '../../components/Common/Checkbox';
import SubmitButton from '../../components/Common/SubmitButton'; import SubmitButton from '../../components/Common/SubmitButton';
import TextArea from '../../components/Common/TextArea'; import TextArea from '../../components/Common/TextArea';
import TextInput from '../../components/Common/TextInput'; import TextInput from '../../components/Common/TextInput';
import { useRSForm } from '../../context/RSFormContext';
import { useCallback, useEffect, useState } from 'react';
import Button from '../../components/Common/Button';
import { CrownIcon, DownloadIcon, DumpBinIcon, SaveIcon, ShareIcon } from '../../components/Icons'; import { CrownIcon, DownloadIcon, DumpBinIcon, SaveIcon, ShareIcon } from '../../components/Icons';
import { useUsers } from '../../context/UsersContext';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
import { useRSForm } from '../../context/RSFormContext';
import { useUsers } from '../../context/UsersContext';
import { claimOwnershipProc, deleteRSFormProc, downloadRSFormProc, shareCurrentURLProc } from '../../utils/procedures'; import { claimOwnershipProc, deleteRSFormProc, downloadRSFormProc, shareCurrentURLProc } from '../../utils/procedures';
function RSFormCard() { function RSFormCard() {
@ -29,28 +30,30 @@ function RSFormCard() {
const [common, setCommon] = useState(false); const [common, setCommon] = useState(false);
useEffect(() => { useEffect(() => {
setTitle(schema!.title) setTitle(schema?.title ?? '');
setAlias(schema!.alias) setAlias(schema?.alias ?? '');
setComment(schema!.comment) setComment(schema?.comment ?? '');
setCommon(schema!.is_common) setCommon(schema?.is_common ?? false);
}, [schema]); }, [schema]);
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
const data = { const data = {
'title': title, title,
'alias': alias, alias,
'comment': comment, comment,
'is_common': common, is_common: common
}; };
// eslint-disable-next-line @typescript-eslint/no-floating-promises
update(data, () => toast.success('Изменения сохранены')); update(data, () => toast.success('Изменения сохранены'));
}; };
const handleDelete = const handleDelete =
useCallback(() => deleteRSFormProc(destroy, navigate), [destroy, navigate]); useCallback(() => { deleteRSFormProc(destroy, navigate); }, [destroy, navigate]);
const handleDownload = useCallback(() => { const handleDownload = useCallback(() => {
const fileName = (schema?.alias || 'Schema') + '.trs'; const fileName = (schema?.alias ?? 'Schema') + '.trs';
// eslint-disable-next-line @typescript-eslint/no-floating-promises
downloadRSFormProc(download, fileName); downloadRSFormProc(download, fileName);
}, [download, schema?.alias]); }, [download, schema?.alias]);
@ -60,24 +63,24 @@ function RSFormCard() {
required required
value={title} value={title}
disabled={!isEditable} disabled={!isEditable}
onChange={event => setTitle(event.target.value)} onChange={event => { setTitle(event.target.value); }}
/> />
<TextInput id='alias' label='Сокращение' type='text' <TextInput id='alias' label='Сокращение' type='text'
required required
value={alias} value={alias}
disabled={!isEditable} disabled={!isEditable}
widthClass='max-w-sm' widthClass='max-w-sm'
onChange={event => setAlias(event.target.value)} onChange={event => { setAlias(event.target.value); }}
/> />
<TextArea id='comment' label='Комментарий' <TextArea id='comment' label='Комментарий'
value={comment} value={comment}
disabled={!isEditable} disabled={!isEditable}
onChange={event => setComment(event.target.value)} onChange={event => { setComment(event.target.value); }}
/> />
<Checkbox id='common' label='Общедоступная схема' <Checkbox id='common' label='Общедоступная схема'
value={common} value={common}
disabled={!isEditable} disabled={!isEditable}
onChange={event => setCommon(event.target.checked)} onChange={event => { setCommon(event.target.checked); }}
/> />
<div className='flex items-center justify-between gap-1 py-2 mt-2'> <div className='flex items-center justify-between gap-1 py-2 mt-2'>
@ -104,7 +107,7 @@ function RSFormCard() {
tooltip={isClaimable ? 'Стать владельцем' : 'Вы уже являетесь владельцем' } tooltip={isClaimable ? 'Стать владельцем' : 'Вы уже являетесь владельцем' }
disabled={!isClaimable || processing || !user} disabled={!isClaimable || processing || !user}
icon={<CrownIcon color={isOwned ? '' : 'text-green'}/>} icon={<CrownIcon color={isOwned ? '' : 'text-green'}/>}
onClick={() => claimOwnershipProc(claim)} onClick={() => { claimOwnershipProc(claim); }}
/> />
<Button <Button
tooltip={ isEditable ? 'Удалить схему' : 'Вы не можете редактировать данную схему'} tooltip={ isEditable ? 'Удалить схему' : 'Вы не можете редактировать данную схему'}
@ -124,11 +127,11 @@ function RSFormCard() {
</div> </div>
<div className='flex justify-start mt-2'> <div className='flex justify-start mt-2'>
<label className='font-semibold'>Дата обновления:</label> <label className='font-semibold'>Дата обновления:</label>
<span className='ml-2'>{new Date(schema!.time_update).toLocaleString(intl.locale)}</span> <span className='ml-2'>{schema && new Date(schema?.time_update).toLocaleString(intl.locale)}</span>
</div> </div>
<div className='flex justify-start mt-2'> <div className='flex justify-start mt-2'>
<label className='font-semibold'>Дата создания:</label> <label className='font-semibold'>Дата создания:</label>
<span className='ml-8'>{new Date(schema!.time_create).toLocaleString(intl.locale)}</span> <span className='ml-8'>{schema && new Date(schema?.time_create).toLocaleString(intl.locale)}</span>
</div> </div>
</form> </form>
); );

View File

@ -1,7 +1,7 @@
import Card from '../../components/Common/Card'; import Card from '../../components/Common/Card';
import Divider from '../../components/Common/Divider'; import Divider from '../../components/Common/Divider';
import LabeledText from '../../components/Common/LabeledText'; import LabeledText from '../../components/Common/LabeledText';
import { IRSFormStats } from '../../utils/models'; import { type IRSFormStats } from '../../utils/models';
interface RSFormStatsProps { interface RSFormStatsProps {
stats: IRSFormStats stats: IRSFormStats

View File

@ -1,15 +1,16 @@
import { Tabs, TabList, TabPanel } from 'react-tabs';
import ConstituentsTable from './ConstituentsTable';
import { IConstituenta } from '../../utils/models';
import { useRSForm } from '../../context/RSFormContext';
import { useEffect, useLayoutEffect, useState } from 'react'; import { useEffect, useLayoutEffect, useState } from 'react';
import ConceptTab from '../../components/Common/ConceptTab'; import { TabList, TabPanel, Tabs } from 'react-tabs';
import RSFormCard from './RSFormCard';
import { Loader } from '../../components/Common/Loader';
import BackendError from '../../components/BackendError'; import BackendError from '../../components/BackendError';
import ConstituentEditor from './ConstituentEditor'; import ConceptTab from '../../components/Common/ConceptTab';
import RSFormStats from './RSFormStats'; import { Loader } from '../../components/Common/Loader';
import { useRSForm } from '../../context/RSFormContext';
import useLocalStorage from '../../hooks/useLocalStorage'; import useLocalStorage from '../../hooks/useLocalStorage';
import { type IConstituenta } from '../../utils/models';
import ConstituentEditor from './ConstituentEditor';
import ConstituentsTable from './ConstituentsTable';
import RSFormCard from './RSFormCard';
import RSFormStats from './RSFormStats';
import TablistTools from './TablistTools'; import TablistTools from './TablistTools';
export enum RSFormTabsList { export enum RSFormTabsList {
@ -19,7 +20,7 @@ export enum RSFormTabsList {
} }
function RSFormTabs() { function RSFormTabs() {
const { setActiveID, activeCst, activeID, error, schema, loading } = useRSForm(); const { setActiveID, activeID, error, schema, loading } = useRSForm();
const [tabIndex, setTabIndex] = useLocalStorage('rsform_edit_tab', RSFormTabsList.CARD); const [tabIndex, setTabIndex] = useLocalStorage('rsform_edit_tab', RSFormTabsList.CARD);
const [init, setInit] = useState(false); const [init, setInit] = useState(false);
@ -37,7 +38,7 @@ function RSFormTabs() {
if (schema) { if (schema) {
const url = new URL(window.location.href); const url = new URL(window.location.href);
const activeQuery = url.searchParams.get('active'); const activeQuery = url.searchParams.get('active');
const activeCst = schema?.items?.find((cst) => cst.id === Number(activeQuery)) || undefined; const activeCst = schema?.items?.find((cst) => cst.id === Number(activeQuery));
setActiveID(activeCst?.id); setActiveID(activeCst?.id);
setInit(true); setInit(true);
} }
@ -52,7 +53,7 @@ function RSFormTabs() {
useEffect(() => { useEffect(() => {
if (init) { if (init) {
const url = new URL(window.location.href); const url = new URL(window.location.href);
let currentActive = url.searchParams.get('active'); const currentActive = url.searchParams.get('active');
const currentTab = url.searchParams.get('tab'); const currentTab = url.searchParams.get('tab');
const saveHistory = tabIndex === RSFormTabsList.CST_EDIT && currentActive !== String(activeID); const saveHistory = tabIndex === RSFormTabsList.CST_EDIT && currentActive !== String(activeID);
if (currentTab !== String(tabIndex)) { if (currentTab !== String(tabIndex)) {
@ -89,7 +90,7 @@ function RSFormTabs() {
<ConceptTab>Паспорт схемы</ConceptTab> <ConceptTab>Паспорт схемы</ConceptTab>
<ConceptTab className='border-x-2 clr-border min-w-[10rem] flex justify-between gap-2'> <ConceptTab className='border-x-2 clr-border min-w-[10rem] flex justify-between gap-2'>
<span>Конституенты</span> <span>Конституенты</span>
<span>{`${schema.stats?.count_errors} | ${schema.stats?.count_all}`}</span> <span>{`${schema.stats?.count_errors ?? 0} | ${schema.stats?.count_all ?? 0}`}</span>
</ConceptTab> </ConceptTab>
<ConceptTab>Редактор</ConceptTab> <ConceptTab>Редактор</ConceptTab>
</TabList> </TabList>

View File

@ -12,7 +12,7 @@ function RSLocalButton({text, tooltip, disabled, onInsert}: RSLocalButtonProps)
<button <button
type='button' type='button'
disabled={disabled} disabled={disabled}
onClick={() => onInsert(TokenID.ID_LOCAL, text)} onClick={() => { onInsert(TokenID.ID_LOCAL, text); }}
title={tooltip} title={tooltip}
tabIndex={-1} tabIndex={-1}
className='w-[1.5rem] h-7 cursor-pointer border rounded-none clr-btn-clear' className='w-[1.5rem] h-7 cursor-pointer border rounded-none clr-btn-clear'

View File

@ -1,4 +1,4 @@
import { TokenID } from '../../utils/models' import { type TokenID } from '../../utils/models'
import { getRSButtonData } from '../../utils/staticUI' import { getRSButtonData } from '../../utils/staticUI'
interface RSTokenButtonProps { interface RSTokenButtonProps {
@ -14,7 +14,7 @@ function RSTokenButton({id, disabled, onInsert}: RSTokenButtonProps) {
<button <button
type='button' type='button'
disabled={disabled} disabled={disabled}
onClick={() => onInsert(id)} onClick={() => { onInsert(id); }}
title={data.tooltip} title={data.tooltip}
tabIndex={-1} tabIndex={-1}
className={`px-1 cursor-pointer border rounded-none h-7 ${width} clr-btn-clear`} className={`px-1 cursor-pointer border rounded-none h-7 ${width} clr-btn-clear`}

View File

@ -1,5 +1,6 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { ExpressionStatus, IConstituenta, ParsingStatus, inferStatus } from '../../utils/models';
import { ExpressionStatus, type IConstituenta, inferStatus, ParsingStatus } from '../../utils/models';
import { getStatusInfo } from '../../utils/staticUI'; import { getStatusInfo } from '../../utils/staticUI';
interface StatusBarProps { interface StatusBarProps {
@ -14,8 +15,8 @@ function StatusBar({isModified, constituenta, parseData}: StatusBarProps) {
return ExpressionStatus.UNKNOWN; return ExpressionStatus.UNKNOWN;
} }
if (parseData) { if (parseData) {
const parse = parseData['parseResult'] ? ParsingStatus.VERIFIED : ParsingStatus.INCORRECT; const parse = parseData.parseResult ? ParsingStatus.VERIFIED : ParsingStatus.INCORRECT;
return inferStatus(parse, parseData['valueClass']); return inferStatus(parse, parseData.valueClass);
} }
return inferStatus(constituenta?.parse?.status, constituenta?.parse?.valueClass); return inferStatus(constituenta?.parse?.status, constituenta?.parse?.valueClass);
}, [isModified, constituenta, parseData]); }, [isModified, constituenta, parseData]);

View File

@ -1,20 +1,22 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import Button from '../../components/Common/Button';
import Dropdown from '../../components/Common/Dropdown';
import { CloneIcon, CrownIcon, DownloadIcon, DumpBinIcon, EyeIcon, EyeOffIcon, MenuIcon, PenIcon, ShareIcon, UploadIcon } from '../../components/Icons';
import { useRSForm } from '../../context/RSFormContext';
import useDropdown from '../../hooks/useDropdown';
import DropdownButton from '../../components/Common/DropdownButton';
import Checkbox from '../../components/Common/Checkbox';
import { useAuth } from '../../context/AuthContext';
import { claimOwnershipProc, deleteRSFormProc, downloadRSFormProc, shareCurrentURLProc } from '../../utils/procedures';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import Button from '../../components/Common/Button';
import Checkbox from '../../components/Common/Checkbox';
import Dropdown from '../../components/Common/Dropdown';
import DropdownButton from '../../components/Common/DropdownButton';
import { CloneIcon, CrownIcon, DownloadIcon, DumpBinIcon, EyeIcon, EyeOffIcon, MenuIcon, PenIcon, ShareIcon, UploadIcon } from '../../components/Icons';
import { useAuth } from '../../context/AuthContext';
import { useRSForm } from '../../context/RSFormContext';
import useDropdown from '../../hooks/useDropdown';
import { claimOwnershipProc, deleteRSFormProc, downloadRSFormProc, shareCurrentURLProc } from '../../utils/procedures';
function TablistTools() { function TablistTools() {
const navigate = useNavigate(); const navigate = useNavigate();
const { user } = useAuth(); const { user } = useAuth();
const { schema, const {
schema,
isOwned, isEditable, isTracking, isReadonly: readonly, isForceAdmin: forceAdmin, isOwned, isEditable, isTracking, isReadonly: readonly, isForceAdmin: forceAdmin,
toggleTracking, toggleForceAdmin, toggleReadonly, toggleTracking, toggleForceAdmin, toggleReadonly,
claim, destroy, download claim, destroy, download
@ -24,7 +26,7 @@ function TablistTools() {
const handleClaimOwner = useCallback(() => { const handleClaimOwner = useCallback(() => {
editMenu.hide(); editMenu.hide();
claimOwnershipProc(claim); claimOwnershipProc(claim)
}, [claim, editMenu]); }, [claim, editMenu]);
const handleDelete = useCallback(() => { const handleDelete = useCallback(() => {
@ -34,7 +36,7 @@ function TablistTools() {
const handleDownload = useCallback(() => { const handleDownload = useCallback(() => {
schemaMenu.hide(); schemaMenu.hide();
const fileName = (schema?.alias || 'Schema') + '.trs'; const fileName = (schema?.alias ?? 'Schema') + '.trs';
downloadRSFormProc(download, fileName); downloadRSFormProc(download, fileName);
}, [schemaMenu, download, schema?.alias]); }, [schemaMenu, download, schema?.alias]);
@ -44,7 +46,6 @@ function TablistTools() {
toast.info('Замена содержимого на файл Экстеора'); toast.info('Замена содержимого на файл Экстеора');
}, [schemaMenu]); }, [schemaMenu]);
const handleClone = useCallback(() => { const handleClone = useCallback(() => {
// TODO: implement // TODO: implement
schemaMenu.hide(); schemaMenu.hide();
@ -131,8 +132,8 @@ function TablistTools() {
<div> <div>
<Button <Button
tooltip={'отслеживание: ' + (isTracking ? '[включено]' : '[выключено]')} tooltip={'отслеживание: ' + (isTracking ? '[включено]' : '[выключено]')}
icon={isTracking ? icon={isTracking
<EyeIcon color='text-primary' size={5}/> ? <EyeIcon color='text-primary' size={5}/>
: <EyeOffIcon size={5}/> : <EyeOffIcon size={5}/>
} }
borderClass='' borderClass=''

View File

@ -1,11 +1,12 @@
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { RSFormState } from '../../context/RSFormContext'; import { RSFormState } from '../../context/RSFormContext';
import RSFormTabs from './RSFormTabs'; import RSFormTabs from './RSFormTabs';
function RSFormPage() { function RSFormPage() {
const { id } = useParams(); const { id } = useParams();
return ( return (
<RSFormState schemaID={id || ''}> <RSFormState schemaID={id ?? ''}>
<RSFormTabs /> <RSFormTabs />
</RSFormState> </RSFormState>
); );

View File

@ -1,10 +1,11 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import BackendError from '../../components/BackendError' import BackendError from '../../components/BackendError'
import { Loader } from '../../components/Common/Loader' import { Loader } from '../../components/Common/Loader'
import { FilterType, RSFormsFilter, useRSForms } from '../../hooks/useRSForms'
import RSFormsTable from './RSFormsTable';
import { useEffect } from 'react';
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
import { FilterType, type RSFormsFilter, useRSForms } from '../../hooks/useRSForms'
import RSFormsTable from './RSFormsTable';
function RSFormsPage() { function RSFormsPage() {
const search = useLocation().search; const search = useLocation().search;
@ -14,11 +15,11 @@ function RSFormsPage() {
useEffect(() => { useEffect(() => {
const filterQuery = new URLSearchParams(search).get('filter'); const filterQuery = new URLSearchParams(search).get('filter');
const type = (!user || !filterQuery ? FilterType.COMMON : filterQuery as FilterType); const type = (!user || !filterQuery ? FilterType.COMMON : filterQuery as FilterType);
let filter: RSFormsFilter = {type: type}; const filter: RSFormsFilter = { type };
if (type === FilterType.PERSONAL) { if (type === FilterType.PERSONAL) {
filter.data = user?.id; filter.data = user?.id;
} }
loadList(filter); loadList(filter).catch(console.error);
}, [search, user, loadList]); }, [search, user, loadList]);
return ( return (

View File

@ -1,13 +1,13 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import TextInput from '../components/Common/TextInput';
import Form from '../components/Common/Form';
import { useAuth } from '../context/AuthContext';
import SubmitButton from '../components/Common/SubmitButton';
import BackendError from '../components/BackendError'; import BackendError from '../components/BackendError';
import { IUserSignupData } from '../utils/models'; import Form from '../components/Common/Form';
import InfoMessage from '../components/InfoMessage'; import SubmitButton from '../components/Common/SubmitButton';
import TextInput from '../components/Common/TextInput';
import TextURL from '../components/Common/TextURL'; import TextURL from '../components/Common/TextURL';
import InfoMessage from '../components/InfoMessage';
import { useAuth } from '../context/AuthContext';
import { type IUserSignupData } from '../utils/models';
function RegisterPage() { function RegisterPage() {
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
@ -28,14 +28,14 @@ function RegisterPage() {
event.preventDefault(); event.preventDefault();
if (!loading) { if (!loading) {
const data: IUserSignupData = { const data: IUserSignupData = {
'username': username, username,
'email': email, email,
'password': password, password,
'password2': password2, password2,
'first_name': firstName, first_name: firstName,
'last_name': lastName, last_name: lastName
}; };
signup(data, () => setSuccess(true)); signup(data, () => { setSuccess(true); });
} }
}; };
@ -53,17 +53,17 @@ function RegisterPage() {
<TextInput id='username' label='Имя пользователя' type='text' <TextInput id='username' label='Имя пользователя' type='text'
required required
value={username} value={username}
onChange={event => setUsername(event.target.value)} onChange={event => { setUsername(event.target.value); }}
/> />
<TextInput id='password' label='Пароль' type='password' <TextInput id='password' label='Пароль' type='password'
required required
value={password} value={password}
onChange={event => setPassword(event.target.value)} onChange={event => { setPassword(event.target.value); }}
/> />
<TextInput id='password2' label='Повторите пароль' type='password' <TextInput id='password2' label='Повторите пароль' type='password'
required required
value={password2} value={password2}
onChange={event => setPassword2(event.target.value)} onChange={event => { setPassword2(event.target.value); }}
/> />
<div className='text-sm'> <div className='text-sm'>
<p>- минимум 8 символов</p> <p>- минимум 8 символов</p>
@ -73,15 +73,15 @@ function RegisterPage() {
<TextInput id='email' label='email' type='text' <TextInput id='email' label='email' type='text'
required required
value={email} value={email}
onChange={event => setEmail(event.target.value)} onChange={event => { setEmail(event.target.value); }}
/> />
<TextInput id='first_name' label='Имя' type='text' <TextInput id='first_name' label='Имя' type='text'
value={firstName} value={firstName}
onChange={event => setFirstName(event.target.value)} onChange={event => { setFirstName(event.target.value); }}
/> />
<TextInput id='last_name' label='Фамилия' type='text' <TextInput id='last_name' label='Фамилия' type='text'
value={lastName} value={lastName}
onChange={event => setLastName(event.target.value)} onChange={event => { setLastName(event.target.value); }}
/> />
<div className='flex items-center justify-between my-4'> <div className='flex items-center justify-between my-4'>

View File

@ -1,4 +1,4 @@
import { IUserProfile } from '../../utils/models'; import { type IUserProfile } from '../../utils/models';
interface UserProfileProps { interface UserProfileProps {
profile: IUserProfile profile: IUserProfile

View File

@ -1,9 +1,10 @@
import axios, { AxiosResponse } from 'axios' import axios, { type AxiosResponse } from 'axios'
import { config } from './constants'
import { ErrorInfo } from '../components/BackendError'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { ICurrentUser, IRSForm, IUserInfo, IUserProfile } from './models'
import { FilterType, RSFormsFilter } from '../hooks/useRSForms' import { type ErrorInfo } from '../components/BackendError'
import { FilterType, type RSFormsFilter } from '../hooks/useRSForms'
import { config } from './constants'
import { type ICurrentUser, type IRSForm, type IUserInfo, type IUserProfile } from './models'
export type BackendCallback = (response: AxiosResponse) => void; export type BackendCallback = (response: AxiosResponse) => void;
@ -23,158 +24,157 @@ interface IAxiosRequest {
// ================= Export API ============== // ================= Export API ==============
export async function postLogin(request?: IFrontRequest) { export async function postLogin(request?: IFrontRequest) {
AxiosPost({ await AxiosPost({
title: 'Login', title: 'Login',
endpoint: `${config.url.AUTH}login`, endpoint: `${config.url.AUTH}login`,
request: request request
}); });
} }
export async function getAuth(request?: IFrontRequest) { export async function getAuth(request?: IFrontRequest) {
AxiosGet<ICurrentUser>({ await AxiosGet<ICurrentUser>({
title: 'Current user', title: 'Current user',
endpoint: `${config.url.AUTH}auth`, endpoint: `${config.url.AUTH}auth`,
request: request request
}); });
} }
export async function getProfile(request?: IFrontRequest) { export async function getProfile(request?: IFrontRequest) {
AxiosGet<IUserProfile>({ await AxiosGet<IUserProfile>({
title: 'Current user profile', title: 'Current user profile',
endpoint: `${config.url.AUTH}profile`, endpoint: `${config.url.AUTH}profile`,
request: request request
}); });
} }
export async function postLogout(request?: IFrontRequest) { export async function postLogout(request?: IFrontRequest) {
AxiosPost({ await AxiosPost({
title: 'Logout', title: 'Logout',
endpoint: `${config.url.AUTH}logout`, endpoint: `${config.url.AUTH}logout`,
request: request request
}); });
}; };
export async function postSignup(request?: IFrontRequest) { export async function postSignup(request?: IFrontRequest) {
AxiosPost({ await AxiosPost({
title: 'Register user', title: 'Register user',
endpoint: `${config.url.AUTH}signup`, endpoint: `${config.url.AUTH}signup`,
request: request request
}); });
} }
export async function getActiveUsers(request?: IFrontRequest) { export async function getActiveUsers(request?: IFrontRequest) {
AxiosGet<IUserInfo>({ await AxiosGet<IUserInfo>({
title: 'Active users list', title: 'Active users list',
endpoint: `${config.url.AUTH}active-users`, endpoint: `${config.url.AUTH}active-users`,
request: request request
}); });
} }
export async function getRSForms(filter: RSFormsFilter, request?: IFrontRequest) { export async function getRSForms(filter: RSFormsFilter, request?: IFrontRequest) {
let endpoint: string = '' let endpoint: string = ''
if (filter.type === FilterType.PERSONAL) { if (filter.type === FilterType.PERSONAL) {
endpoint = `${config.url.BASE}rsforms?owner=${filter.data!}` endpoint = `${config.url.BASE}rsforms?owner=${filter.data as number}`
} else { } else {
endpoint = `${config.url.BASE}rsforms?is_common=true` endpoint = `${config.url.BASE}rsforms?is_common=true`
} }
AxiosGet<IRSForm[]>({ await AxiosGet<IRSForm[]>({
title: `RSForms list`, title: 'RSForms list',
endpoint: endpoint, endpoint,
request: request request
}); });
} }
export async function postNewRSForm(request?: IFrontRequest) { export async function postNewRSForm(request?: IFrontRequest) {
AxiosPost({ await AxiosPost({
title: `New RSForm`, title: 'New RSForm',
endpoint: `${config.url.BASE}rsforms/create-detailed/`, endpoint: `${config.url.BASE}rsforms/create-detailed/`,
request: request request
}); });
} }
export async function getRSFormDetails(target: string, request?: IFrontRequest) { export async function getRSFormDetails(target: string, request?: IFrontRequest) {
AxiosGet<IRSForm>({ await AxiosGet<IRSForm>({
title: `RSForm details for id=${target}`, title: `RSForm details for id=${target}`,
endpoint: `${config.url.BASE}rsforms/${target}/details/`, endpoint: `${config.url.BASE}rsforms/${target}/details/`,
request: request request
}); });
} }
export async function patchRSForm(target: string, request?: IFrontRequest) { export async function patchRSForm(target: string, request?: IFrontRequest) {
AxiosPatch({ await AxiosPatch({
title: `RSForm id=${target}`, title: `RSForm id=${target}`,
endpoint: `${config.url.BASE}rsforms/${target}/`, endpoint: `${config.url.BASE}rsforms/${target}/`,
request: request request
}); });
} }
export async function patchConstituenta(target: string, request?: IFrontRequest) { export async function patchConstituenta(target: string, request?: IFrontRequest) {
AxiosPatch({ await AxiosPatch({
title: `Constituenta id=${target}`, title: `Constituenta id=${target}`,
endpoint: `${config.url.BASE}constituents/${target}/`, endpoint: `${config.url.BASE}constituents/${target}/`,
request: request request
}); });
} }
export async function deleteRSForm(target: string, request?: IFrontRequest) { export async function deleteRSForm(target: string, request?: IFrontRequest) {
AxiosDelete({ await AxiosDelete({
title: `RSForm id=${target}`, title: `RSForm id=${target}`,
endpoint: `${config.url.BASE}rsforms/${target}/`, endpoint: `${config.url.BASE}rsforms/${target}/`,
request: request request
}); });
} }
export async function getTRSFile(target: string, request?: IFrontRequest) { export async function getTRSFile(target: string, request?: IFrontRequest) {
AxiosGetBlob({ await AxiosGetBlob({
title: `RSForm TRS file for id=${target}`, title: `RSForm TRS file for id=${target}`,
endpoint: `${config.url.BASE}rsforms/${target}/export-trs/`, endpoint: `${config.url.BASE}rsforms/${target}/export-trs/`,
request: request request
}); });
} }
export async function postClaimRSForm(target: string, request?: IFrontRequest) { export async function postClaimRSForm(target: string, request?: IFrontRequest) {
AxiosPost({ await AxiosPost({
title: `Claim on RSForm id=${target}`, title: `Claim on RSForm id=${target}`,
endpoint: `${config.url.BASE}rsforms/${target}/claim/`, endpoint: `${config.url.BASE}rsforms/${target}/claim/`,
request: request request
}); });
} }
export async function postCheckExpression(schema: string, request?: IFrontRequest) { export async function postCheckExpression(schema: string, request?: IFrontRequest) {
AxiosPost({ await AxiosPost({
title: `Check expression for RSForm id=${schema}: ${request?.data['expression']}`, title: `Check expression for RSForm id=${schema}: ${request?.data.expression as string}`,
endpoint: `${config.url.BASE}rsforms/${schema}/check/`, endpoint: `${config.url.BASE}rsforms/${schema}/check/`,
request: request request
}); });
} }
export async function postNewConstituenta(schema: string, request?: IFrontRequest) { export async function postNewConstituenta(schema: string, request?: IFrontRequest) {
AxiosPost({ await AxiosPost({
title: `New Constituenta for RSForm id=${schema}: ${request?.data['alias']}`, title: `New Constituenta for RSForm id=${schema}: ${request?.data.alias as string}`,
endpoint: `${config.url.BASE}rsforms/${schema}/cst-create/`, endpoint: `${config.url.BASE}rsforms/${schema}/cst-create/`,
request: request request
}); });
} }
export async function patchDeleteConstituenta(schema: string, request?: IFrontRequest) { export async function patchDeleteConstituenta(schema: string, request?: IFrontRequest) {
AxiosPatch<IRSForm>({ await AxiosPatch<IRSForm>({
title: `Delete Constituents for RSForm id=${schema}: ${request?.data['items'].toString()}`, // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
title: `Delete Constituents for RSForm id=${schema}: ${request?.data.items.toString()}`,
endpoint: `${config.url.BASE}rsforms/${schema}/cst-multidelete/`, endpoint: `${config.url.BASE}rsforms/${schema}/cst-multidelete/`,
request: request request
}); });
} }
export async function patchMoveConstituenta(schema: string, request?: IFrontRequest) { export async function patchMoveConstituenta(schema: string, request?: IFrontRequest) {
AxiosPatch<IRSForm>({ await AxiosPatch<IRSForm>({
title: `Moving Constituents for RSForm id=${schema}: ${JSON.stringify(request?.data['items'])} to ${request?.data['move_to']}`, title: `Moving Constituents for RSForm id=${schema}: ${JSON.stringify(request?.data.items)} to ${request?.data.move_to as number}`,
endpoint: `${config.url.BASE}rsforms/${schema}/cst-moveto/`, endpoint: `${config.url.BASE}rsforms/${schema}/cst-moveto/`,
request: request request
}); });
} }
// ====== Helper functions =========== // ====== Helper functions ===========
async function AxiosGet<ReturnType>({ endpoint, request, title }: IAxiosRequest) { async function AxiosGet<ReturnType>({ endpoint, request, title }: IAxiosRequest) {
if (title) console.log(`[[${title}]] requested`); if (title) console.log(`[[${title}]] requested`);

View File

@ -2,15 +2,15 @@
const prod = { const prod = {
url: { url: {
BASE: 'http://rs.acconcept.ru:8000/api/', BASE: 'http://rs.acconcept.ru:8000/api/',
AUTH: 'http://rs.acconcept.ru:8000/users/api/', AUTH: 'http://rs.acconcept.ru:8000/users/api/'
}, }
}; };
const dev = { const dev = {
url: { url: {
BASE: 'http://localhost:8000/api/', BASE: 'http://localhost:8000/api/',
AUTH: 'http://localhost:8000/users/api/', AUTH: 'http://localhost:8000/users/api/'
}, }
}; };
export const urls = { export const urls = {
@ -19,7 +19,7 @@ export const urls = {
exteor64: 'https://drive.google.com/open?id=1IJt25ZRQ-ZMA6t7hOqmo5cv05WJCQKMv&usp=drive_fs', exteor64: 'https://drive.google.com/open?id=1IJt25ZRQ-ZMA6t7hOqmo5cv05WJCQKMv&usp=drive_fs',
ponomarev: 'https://inponomarev.ru/textbook', ponomarev: 'https://inponomarev.ru/textbook',
intro_video: 'https://www.youtube.com/watch?v=0Ty9mu9sOJo', intro_video: 'https://www.youtube.com/watch?v=0Ty9mu9sOJo',
full_course: 'https://www.youtube.com/playlist?list=PLGe_JiAwpqu1C70ruQmCm_OWTWU3KJwDo', full_course: 'https://www.youtube.com/playlist?list=PLGe_JiAwpqu1C70ruQmCm_OWTWU3KJwDo'
}; };
export const config = process.env.NODE_ENV === 'production' ? prod : dev; export const config = process.env.NODE_ENV === 'production' ? prod : dev;

View File

@ -297,75 +297,39 @@ export function CalculateStats(schema: IRSForm) {
count_term: 0, count_term: 0,
count_function: 0, count_function: 0,
count_predicate: 0, count_predicate: 0,
count_theorem: 0, count_theorem: 0
} }
return; return;
} }
schema.stats = { schema.stats = {
count_all: schema.items?.length || 0, count_all: schema.items?.length || 0,
count_errors: schema.items?.reduce( count_errors: schema.items?.reduce(
(sum, cst) => sum + (sum, cst) => sum + (cst.parse?.status === ParsingStatus.INCORRECT ? 1 : 0) || 0, 0),
(cst.parse?.status === ParsingStatus.INCORRECT ? 1 : 0) || 0,
0
),
count_property: schema.items?.reduce( count_property: schema.items?.reduce(
(sum, cst) => sum + (sum, cst) => sum + (cst.parse?.valueClass === ValueClass.PROPERTY ? 1 : 0) || 0, 0),
(cst.parse?.valueClass === ValueClass.PROPERTY ? 1 : 0) || 0,
0
),
count_incalc: schema.items?.reduce( count_incalc: schema.items?.reduce(
(sum, cst) => sum + (sum, cst) => sum +
((cst.parse?.status === ParsingStatus.VERIFIED && ((cst.parse?.status === ParsingStatus.VERIFIED && cst.parse?.valueClass === ValueClass.INVALID) ? 1 : 0) || 0, 0),
cst.parse?.valueClass === ValueClass.INVALID) ? 1 : 0) || 0,
0
),
count_termin: schema.items?.reduce( count_termin: schema.items?.reduce(
(sum, cst) => (sum + (sum, cst) => (sum + (cst.term?.raw ? 1 : 0) || 0), 0),
(cst.term?.raw ? 1 : 0) || 0),
0
),
count_base: schema.items?.reduce( count_base: schema.items?.reduce(
(sum, cst) => sum + (sum, cst) => sum + (cst.cstType === CstType.BASE ? 1 : 0), 0),
(cst.cstType === CstType.BASE ? 1 : 0),
0
),
count_constant: schema.items?.reduce( count_constant: schema.items?.reduce(
(sum, cst) => sum + (sum, cst) => sum + (cst.cstType === CstType.CONSTANT ? 1 : 0), 0),
(cst.cstType === CstType.CONSTANT ? 1 : 0),
0
),
count_structured: schema.items?.reduce( count_structured: schema.items?.reduce(
(sum, cst) => sum + (sum, cst) => sum + (cst.cstType === CstType.STRUCTURED ? 1 : 0), 0),
(cst.cstType === CstType.STRUCTURED ? 1 : 0),
0
),
count_axiom: schema.items?.reduce( count_axiom: schema.items?.reduce(
(sum, cst) => sum + (sum, cst) => sum + (cst.cstType === CstType.AXIOM ? 1 : 0), 0),
(cst.cstType === CstType.AXIOM ? 1 : 0),
0
),
count_term: schema.items?.reduce( count_term: schema.items?.reduce(
(sum, cst) => sum + (sum, cst) => sum + (cst.cstType === CstType.TERM ? 1 : 0), 0),
(cst.cstType === CstType.TERM ? 1 : 0),
0
),
count_function: schema.items?.reduce( count_function: schema.items?.reduce(
(sum, cst) => sum + (sum, cst) => sum + (cst.cstType === CstType.FUNCTION ? 1 : 0), 0),
(cst.cstType === CstType.FUNCTION ? 1 : 0),
0
),
count_predicate: schema.items?.reduce( count_predicate: schema.items?.reduce(
(sum, cst) => sum + (sum, cst) => sum + (cst.cstType === CstType.PREDICATE ? 1 : 0), 0),
(cst.cstType === CstType.PREDICATE ? 1 : 0),
0
),
count_theorem: schema.items?.reduce( count_theorem: schema.items?.reduce(
(sum, cst) => sum + (sum, cst) => sum + (cst.cstType === CstType.THEOREM ? 1 : 0), 0)
(cst.cstType === CstType.THEOREM ? 1 : 0),
0
),
} }
} }

View File

@ -1,15 +1,17 @@
import { toast } from 'react-toastify';
import { BackendCallback } from './backendAPI';
import fileDownload from 'js-file-download'; import fileDownload from 'js-file-download';
import { toast } from 'react-toastify';
import { type BackendCallback } from './backendAPI';
export function shareCurrentURLProc() { export function shareCurrentURLProc() {
const url = window.location.href + '&share'; const url = window.location.href + '&share';
navigator.clipboard.writeText(url); navigator.clipboard.writeText(url)
toast.success(`Ссылка скопирована: ${url}`); .then(() => toast.success(`Ссылка скопирована: ${url}`))
.catch(console.error);
} }
export async function claimOwnershipProc( export function claimOwnershipProc(
claim: (callback: BackendCallback) => Promise<void>, claim: (callback: BackendCallback) => void
) { ) {
if (!window.confirm('Вы уверены, что хотите стать владельцем данной схемы?')) { if (!window.confirm('Вы уверены, что хотите стать владельцем данной схемы?')) {
return; return;
@ -17,9 +19,9 @@ export async function claimOwnershipProc(
claim(() => toast.success('Вы стали владельцем схемы')); claim(() => toast.success('Вы стали владельцем схемы'));
} }
export async function deleteRSFormProc( export function deleteRSFormProc(
destroy: (callback: BackendCallback) => Promise<void>, destroy: (callback: BackendCallback) => void,
navigate: Function navigate: (path: string) => void
) { ) {
if (!window.confirm('Вы уверены, что хотите удалить данную схему?')) { if (!window.confirm('Вы уверены, что хотите удалить данную схему?')) {
return; return;
@ -30,8 +32,8 @@ export async function deleteRSFormProc(
}); });
} }
export async function downloadRSFormProc( export function downloadRSFormProc(
download: (callback: BackendCallback) => Promise<void>, download: (callback: BackendCallback) => void,
fileName: string fileName: string
) { ) {
download((response) => { download((response) => {

View File

@ -1,4 +1,4 @@
import { CstType, ExpressionStatus, IConstituenta, IRSForm, ParsingStatus, TokenID } from './models'; import { CstType, ExpressionStatus, type IConstituenta, type IRSForm, ParsingStatus, TokenID } from './models';
export interface IRSButtonData { export interface IRSButtonData {
text: string text: string
@ -25,160 +25,160 @@ export function getRSButtonData(id: TokenID): IRSButtonData {
switch (id) { switch (id) {
case TokenID.BOOLEAN: return { case TokenID.BOOLEAN: return {
text: '()', text: '()',
tooltip: 'Булеан [Alt + E]', tooltip: 'Булеан [Alt + E]'
}; };
case TokenID.DECART: return { case TokenID.DECART: return {
text: '×', text: '×',
tooltip: 'Декартово произведение [Shift + 8]', tooltip: 'Декартово произведение [Shift + 8]'
}; };
case TokenID.PUNC_PL: return { case TokenID.PUNC_PL: return {
text: '( )', text: '( )',
tooltip: 'Скобки вокруг выражения [ Alt + Shift + 9 ]', tooltip: 'Скобки вокруг выражения [ Alt + Shift + 9 ]'
}; };
case TokenID.PUNC_SL: return { case TokenID.PUNC_SL: return {
text: '[ ]', text: '[ ]',
tooltip: 'Скобки вокруг выражения [ Alt + [ ]', tooltip: 'Скобки вокруг выражения [ Alt + [ ]'
}; };
case TokenID.FORALL: return { case TokenID.FORALL: return {
text: '∀', text: '∀',
tooltip: 'Квантор всеобщности [`]', tooltip: 'Квантор всеобщности [`]'
}; };
case TokenID.EXISTS: return { case TokenID.EXISTS: return {
text: '∃', text: '∃',
tooltip: 'Квантор существования [Shift + `]', tooltip: 'Квантор существования [Shift + `]'
}; };
case TokenID.NOT: return { case TokenID.NOT: return {
text: '¬', text: '¬',
tooltip: 'Отрицание [Alt + `]', tooltip: 'Отрицание [Alt + `]'
}; };
case TokenID.AND: return { case TokenID.AND: return {
text: '&', text: '&',
tooltip: 'Конъюнкция [Alt + 3 ~ Shift + 7]', tooltip: 'Конъюнкция [Alt + 3 ~ Shift + 7]'
}; };
case TokenID.OR: return { case TokenID.OR: return {
text: '', text: '',
tooltip: 'дизъюнкция [Alt + Shift + 3]', tooltip: 'дизъюнкция [Alt + Shift + 3]'
}; };
case TokenID.IMPLICATION: return { case TokenID.IMPLICATION: return {
text: '⇒', text: '⇒',
tooltip: 'импликация [Alt + 4]', tooltip: 'импликация [Alt + 4]'
}; };
case TokenID.EQUIVALENT: return { case TokenID.EQUIVALENT: return {
text: '⇔', text: '⇔',
tooltip: 'эквивалентность [Alt + Shift + 4]', tooltip: 'эквивалентность [Alt + Shift + 4]'
}; };
case TokenID.LIT_EMPTYSET: return { case TokenID.LIT_EMPTYSET: return {
text: '∅', text: '∅',
tooltip: 'пустое множество [Alt + X]', tooltip: 'пустое множество [Alt + X]'
}; };
case TokenID.LIT_INTSET: return { case TokenID.LIT_INTSET: return {
text: 'Z', text: 'Z',
tooltip: 'целые числа [Alt + Z]', tooltip: 'целые числа [Alt + Z]'
}; };
case TokenID.EQUAL: return { case TokenID.EQUAL: return {
text: '=', text: '=',
tooltip: 'равенство', tooltip: 'равенство'
}; };
case TokenID.NOTEQUAL: return { case TokenID.NOTEQUAL: return {
text: '≠', text: '≠',
tooltip: 'неравенство [Alt + Shift + `]', tooltip: 'неравенство [Alt + Shift + `]'
}; };
case TokenID.GREATER_OR_EQ: return { case TokenID.GREATER_OR_EQ: return {
text: '≥', text: '≥',
tooltip: 'больше или равно', tooltip: 'больше или равно'
}; };
case TokenID.LESSER_OR_EQ: return { case TokenID.LESSER_OR_EQ: return {
text: '≤', text: '≤',
tooltip: 'меньше или равно', tooltip: 'меньше или равно'
}; };
case TokenID.IN: return { case TokenID.IN: return {
text: '∈', text: '∈',
tooltip: 'быть элементом (принадлежит) [Alt + \']', tooltip: 'быть элементом (принадлежит) [Alt + \']'
}; };
case TokenID.NOTIN: return { case TokenID.NOTIN: return {
text: '∉', text: '∉',
tooltip: 'не принадлежит [Alt + Shift + \']', tooltip: 'не принадлежит [Alt + Shift + \']'
}; };
case TokenID.SUBSET_OR_EQ: return { case TokenID.SUBSET_OR_EQ: return {
text: '⊆', text: '⊆',
tooltip: 'быть частью (нестрогое подмножество) [Alt + 2]', tooltip: 'быть частью (нестрогое подмножество) [Alt + 2]'
}; };
case TokenID.SUBSET: return { case TokenID.SUBSET: return {
text: '⊂', text: '⊂',
tooltip: 'строгое подмножество [Alt + ;]', tooltip: 'строгое подмножество [Alt + ;]'
}; };
case TokenID.NOTSUBSET: return { case TokenID.NOTSUBSET: return {
text: '⊄', text: '⊄',
tooltip: 'не подмножество [Alt + Shift + 2]', tooltip: 'не подмножество [Alt + Shift + 2]'
}; };
case TokenID.INTERSECTION: return { case TokenID.INTERSECTION: return {
text: '∩', text: '∩',
tooltip: 'пересечение [Alt + Y]', tooltip: 'пересечение [Alt + Y]'
}; };
case TokenID.UNION: return { case TokenID.UNION: return {
text: '', text: '',
tooltip: 'объединение [Alt + U]', tooltip: 'объединение [Alt + U]'
}; };
case TokenID.SET_MINUS: return { case TokenID.SET_MINUS: return {
text: '\\', text: '\\',
tooltip: 'Разность множеств [Alt + 5]', tooltip: 'Разность множеств [Alt + 5]'
}; };
case TokenID.SYMMINUS: return { case TokenID.SYMMINUS: return {
text: '∆', text: '∆',
tooltip: 'Симметрическая разность [Alt + Shift + 5]', tooltip: 'Симметрическая разность [Alt + Shift + 5]'
}; };
case TokenID.NT_DECLARATIVE_EXPR: return { case TokenID.NT_DECLARATIVE_EXPR: return {
text: 'D{}', text: 'D{}',
tooltip: 'Декларативная форма определения терма [Alt + D]', tooltip: 'Декларативная форма определения терма [Alt + D]'
}; };
case TokenID.NT_IMPERATIVE_EXPR: return { case TokenID.NT_IMPERATIVE_EXPR: return {
text: 'I{}', text: 'I{}',
tooltip: 'императивная форма определения терма [Alt + G]', tooltip: 'императивная форма определения терма [Alt + G]'
}; };
case TokenID.NT_RECURSIVE_FULL: return { case TokenID.NT_RECURSIVE_FULL: return {
text: 'R{}', text: 'R{}',
tooltip: 'рекурсивная (цикличная) форма определения терма [Alt + T]', tooltip: 'рекурсивная (цикличная) форма определения терма [Alt + T]'
}; };
case TokenID.BIGPR: return { case TokenID.BIGPR: return {
text: 'Pr1()', text: 'Pr1()',
tooltip: 'большая проекция [Alt + Q]', tooltip: 'большая проекция [Alt + Q]'
}; };
case TokenID.SMALLPR: return { case TokenID.SMALLPR: return {
text: 'pr1()', text: 'pr1()',
tooltip: 'малая проекция [Alt + W]', tooltip: 'малая проекция [Alt + W]'
}; };
case TokenID.FILTER: return { case TokenID.FILTER: return {
text: 'Fi1[]()', text: 'Fi1[]()',
tooltip: 'фильтр [Alt + F]', tooltip: 'фильтр [Alt + F]'
}; };
case TokenID.REDUCE: return { case TokenID.REDUCE: return {
text: 'red()', text: 'red()',
tooltip: 'множество-сумма [Alt + R]', tooltip: 'множество-сумма [Alt + R]'
}; };
case TokenID.CARD: return { case TokenID.CARD: return {
text: 'card()', text: 'card()',
tooltip: 'мощность [Alt + C]', tooltip: 'мощность [Alt + C]'
}; };
case TokenID.BOOL: return { case TokenID.BOOL: return {
text: 'bool()', text: 'bool()',
tooltip: 'синглетон [Alt + B]', tooltip: 'синглетон [Alt + B]'
}; };
case TokenID.DEBOOL: return { case TokenID.DEBOOL: return {
text: 'debool()', text: 'debool()',
tooltip: 'десинглетон [Alt + V]', tooltip: 'десинглетон [Alt + V]'
}; };
case TokenID.PUNC_ASSIGN: return { case TokenID.PUNC_ASSIGN: return {
text: ':=', text: ':=',
tooltip: 'присвоение (императивный синтаксис)', tooltip: 'присвоение (императивный синтаксис)'
}; };
case TokenID.PUNC_ITERATE: return { case TokenID.PUNC_ITERATE: return {
text: ':∈', text: ':∈',
tooltip: 'перебор элементов множества (императивный синтаксис)', tooltip: 'перебор элементов множества (императивный синтаксис)'
}; };
} }
return { return {
text: 'undefined', text: 'undefined',
tooltip: 'undefined', tooltip: 'undefined'
} }
} }
@ -255,11 +255,11 @@ export function getStatusInfo(status?: ExpressionStatus): IStatusInfo {
} }
export function extractGlobals(expression: string): Set<string> { export function extractGlobals(expression: string): Set<string> {
return new Set(expression.match(/[XCSADFPT]\d+/g) || []); return new Set(expression.match(/[XCSADFPT]\d+/g) ?? []);
} }
export function createAliasFor(type: CstType, schema: IRSForm): string { export function createAliasFor(type: CstType, schema: IRSForm): string {
let prefix = getCstTypePrefix(type); const prefix = getCstTypePrefix(type);
if (!schema.items || schema.items.length <= 0) { if (!schema.items || schema.items.length <= 0) {
return `${prefix}1`; return `${prefix}1`;
} }

View File

@ -1,9 +1,9 @@
export function assertIsNode(e: EventTarget | null): asserts e is Node { export function assertIsNode(e: EventTarget | null): asserts e is Node {
if (!e || !('nodeType' in e)) { if (!e || !('nodeType' in e)) {
throw new Error(`Node expected`); throw new Error('Node expected');
} }
} }
export function delay(ms: number) { export async function delay(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms)); return await new Promise(resolve => setTimeout(resolve, ms));
} }