Compare commits
14 Commits
f919bfe4cf
...
76aee5bea7
Author | SHA1 | Date | |
---|---|---|---|
![]() |
76aee5bea7 | ||
![]() |
d899e17fcd | ||
![]() |
bfae07c7b6 | ||
![]() |
6f23ec5354 | ||
![]() |
539ed87ddf | ||
![]() |
55fa09c6fb | ||
![]() |
f526efd0ee | ||
![]() |
08fd26e687 | ||
![]() |
26bd0ce16b | ||
![]() |
2e03756646 | ||
![]() |
50d29f5fee | ||
![]() |
a47ea5d32d | ||
![]() |
68dd1aa3c4 | ||
![]() |
12a4b740bc |
10
.github/workflows/frontend.yml
vendored
10
.github/workflows/frontend.yml
vendored
|
@ -23,7 +23,6 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
node-version: [22.x]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
@ -35,10 +34,17 @@ jobs:
|
|||
cache: "npm"
|
||||
- name: Build
|
||||
run: |
|
||||
npm install -g typescript vite jest
|
||||
npm install -g typescript vite jest playwright
|
||||
npx playwright install --with-deps
|
||||
npm ci
|
||||
npm run build --if-present
|
||||
- name: Run CI
|
||||
run: |
|
||||
npm run lint
|
||||
npm test
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -67,3 +67,5 @@ bower_components
|
|||
venv/
|
||||
/GitExtensions.settings
|
||||
rsconcept/frontend/public/privacy.pdf
|
||||
/rsconcept/frontend/playwright-report
|
||||
/rsconcept/frontend/test-results
|
||||
|
|
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
|
@ -19,7 +19,8 @@
|
|||
"ms-python.pylint",
|
||||
"ms-python.autopep8",
|
||||
"ms-python.vscode-pylance",
|
||||
"vscode-icons-team.vscode-icons"
|
||||
"vscode-icons-team.vscode-icons",
|
||||
"ms-playwright.playwright"
|
||||
],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
|
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -119,6 +119,7 @@
|
|||
"NUMR",
|
||||
"Opencorpora",
|
||||
"overscroll",
|
||||
"partialize",
|
||||
"passwordreset",
|
||||
"perfectivity",
|
||||
"PNCT",
|
||||
|
|
|
@ -44,7 +44,10 @@ This readme file is used mostly to document project dependencies and conventions
|
|||
- use-debounce
|
||||
- qrcode.react
|
||||
- html-to-image
|
||||
- zustand
|
||||
- @tanstack/react-table
|
||||
- @tanstack/react-query
|
||||
- @tanstack/react-query-devtools
|
||||
- @uiw/react-codemirror
|
||||
- @uiw/codemirror-themes
|
||||
- @lezer/lr
|
||||
|
@ -67,6 +70,7 @@ This readme file is used mostly to document project dependencies and conventions
|
|||
- ts-jest
|
||||
- @types/jest
|
||||
- @lezer/generator
|
||||
- @playwright/test
|
||||
</pre>
|
||||
</details>
|
||||
<details>
|
||||
|
@ -132,6 +136,7 @@ This readme file is used mostly to document project dependencies and conventions
|
|||
- isort
|
||||
- Django
|
||||
- SQLite
|
||||
- Playwright
|
||||
</pre>
|
||||
</details>
|
||||
|
||||
|
|
|
@ -20,22 +20,6 @@
|
|||
/>
|
||||
|
||||
<title>Концепт Портал</title>
|
||||
|
||||
<!-- <script src="https://unpkg.com/react-scan/dist/auto.global.js"></script> -->
|
||||
<script>
|
||||
let isDark = false;
|
||||
if ('portal.theme.dark' in localStorage) {
|
||||
isDark = localStorage.getItem('portal.theme.dark') === 'true';
|
||||
} else if (window.matchMedia) {
|
||||
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
localStorage.setItem('portal.theme.dark', isDark ? 'true' : 'false');
|
||||
}
|
||||
if (isDark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
1592
rsconcept/frontend/package-lock.json
generated
1592
rsconcept/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -5,7 +5,7 @@
|
|||
"type": "module",
|
||||
"scripts": {
|
||||
"generate": "lezer-generator src/components/RSInput/rslang/rslangFast.grammar -o src/components/RSInput/rslang/parser.ts && lezer-generator src/components/RSInput/rslang/rslangAST.grammar -o src/components/RSInput/rslang/parserAST.ts && lezer-generator src/components/RefsInput/parse/refsText.grammar -o src/components/RefsInput/parse/parser.ts",
|
||||
"test": "jest",
|
||||
"test": "jest && playwright test",
|
||||
"dev": "vite --host",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
|
||||
|
@ -14,6 +14,8 @@
|
|||
"dependencies": {
|
||||
"@dagrejs/dagre": "^1.1.4",
|
||||
"@lezer/lr": "^1.4.2",
|
||||
"@tanstack/react-query": "^5.64.1",
|
||||
"@tanstack/react-query-devtools": "^5.64.1",
|
||||
"@tanstack/react-table": "^8.20.6",
|
||||
"@uiw/codemirror-themes": "^4.23.7",
|
||||
"@uiw/react-codemirror": "^4.23.7",
|
||||
|
@ -24,42 +26,44 @@
|
|||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-error-boundary": "^4.1.2",
|
||||
"react-error-boundary": "^5.0.0",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-intl": "^7.0.4",
|
||||
"react-router": "^7.0.2",
|
||||
"react-intl": "7.0.4",
|
||||
"react-router": "^7.1.2",
|
||||
"react-select": "^5.9.0",
|
||||
"react-tabs": "^6.0.2",
|
||||
"react-toastify": "^11.0.0",
|
||||
"react-tabs": "^6.1.0",
|
||||
"react-toastify": "^11.0.3",
|
||||
"react-tooltip": "^5.28.0",
|
||||
"react-zoom-pan-pinch": "^3.6.1",
|
||||
"reactflow": "^11.11.4",
|
||||
"use-debounce": "^10.0.4"
|
||||
"use-debounce": "^10.0.4",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lezer/generator": "^1.7.2",
|
||||
"@playwright/test": "^1.49.1",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/react": "^19.0.1",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/react": "^19.0.7",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
||||
"@typescript-eslint/parser": "^8.0.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"babel-plugin-react-compiler": "^19.0.0-beta-37ed2a7-20241206",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-react-compiler": "^19.0.0-beta-37ed2a7-20241206",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"globals": "^15.13.0",
|
||||
"globals": "^15.14.0",
|
||||
"jest": "^29.7.0",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss": "^8.5.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"ts-jest": "^29.2.5",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript-eslint": "^8.18.1",
|
||||
"vite": "^6.0.3"
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.20.0",
|
||||
"vite": "^6.0.7"
|
||||
},
|
||||
"overrides": {
|
||||
"react": "^19.0.0"
|
||||
|
@ -67,6 +71,9 @@
|
|||
"jest": {
|
||||
"preset": "ts-jest",
|
||||
"testEnvironment": "node",
|
||||
"testPathIgnorePatterns": [
|
||||
"<rootDir>/tests/"
|
||||
],
|
||||
"transform": {
|
||||
"node_modules/variables/.+\\.(j|t)sx?$": "ts-jest"
|
||||
},
|
||||
|
|
31
rsconcept/frontend/playwright.config.ts
Normal file
31
rsconcept/frontend/playwright.config.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: 'tests',
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
reporter: 'html',
|
||||
projects: [
|
||||
{
|
||||
name: 'Desktop Chrome',
|
||||
use: { ...devices['Desktop Chrome'] }
|
||||
},
|
||||
{
|
||||
name: 'Desktop Firefox',
|
||||
use: { ...devices['Desktop Firefox'] }
|
||||
},
|
||||
{
|
||||
name: 'Desktop Safari',
|
||||
use: { ...devices['Desktop Safari'] }
|
||||
}
|
||||
],
|
||||
use: {
|
||||
baseURL: 'http://localhost:3000',
|
||||
trace: 'on-first-retry'
|
||||
},
|
||||
webServer: {
|
||||
command: 'npm run dev',
|
||||
url: 'http://localhost:3000',
|
||||
reuseExistingServer: !process.env.CI
|
||||
}
|
||||
});
|
|
@ -5,12 +5,23 @@ import ConceptToaster from '@/app/ConceptToaster';
|
|||
import Footer from '@/app/Footer';
|
||||
import Navigation from '@/app/Navigation';
|
||||
import Loader from '@/components/ui/Loader';
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { NavigationState } from '@/context/NavigationContext';
|
||||
import { useAppLayoutStore, useMainHeight, useViewportHeight } from '@/stores/appLayout';
|
||||
import { globals } from '@/utils/constants';
|
||||
|
||||
import { GlobalDialogs } from './GlobalDialogs';
|
||||
import { GlobalTooltips } from './GlobalTooltips';
|
||||
|
||||
function ApplicationLayout() {
|
||||
const { viewportHeight, mainHeight, showScroll, noNavigationAnimation } = useConceptOptions();
|
||||
const mainHeight = useMainHeight();
|
||||
const viewportHeight = useViewportHeight();
|
||||
const showScroll = useAppLayoutStore(state => !state.noScroll);
|
||||
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
|
||||
const noNavigation = useAppLayoutStore(state => state.noNavigation);
|
||||
const noFooter = useAppLayoutStore(state => state.noFooter);
|
||||
|
||||
// TODO: prefetch data
|
||||
|
||||
return (
|
||||
<NavigationState>
|
||||
<div className='min-w-[20rem] antialiased h-full max-w-[120rem] mx-auto'>
|
||||
|
@ -22,6 +33,9 @@ function ApplicationLayout() {
|
|||
pauseOnFocusLoss={false}
|
||||
/>
|
||||
|
||||
<GlobalDialogs />
|
||||
<GlobalTooltips />
|
||||
|
||||
<Navigation />
|
||||
|
||||
<div
|
||||
|
@ -36,7 +50,7 @@ function ApplicationLayout() {
|
|||
<Outlet />
|
||||
</Suspense>
|
||||
</main>
|
||||
<Footer />
|
||||
{!noNavigation && !noFooter ? <Footer /> : null}
|
||||
</div>
|
||||
</div>
|
||||
</NavigationState>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { ToastContainer, type ToastContainerProps } from 'react-toastify';
|
||||
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
|
||||
interface ToasterThemedProps extends Omit<ToastContainerProps, 'theme'> {}
|
||||
|
||||
function ToasterThemed(props: ToasterThemedProps) {
|
||||
const { darkMode } = useConceptOptions();
|
||||
const darkMode = usePreferencesStore(state => state.darkMode);
|
||||
|
||||
return <ToastContainer theme={darkMode ? 'dark' : 'light'} {...props} />;
|
||||
}
|
||||
|
|
|
@ -1,15 +1,10 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { external_urls } from '@/utils/constants';
|
||||
|
||||
import TextURL from '../components/ui/TextURL';
|
||||
|
||||
function Footer() {
|
||||
const { noNavigation, noFooter } = useConceptOptions();
|
||||
if (noNavigation || noFooter) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<footer
|
||||
className={clsx(
|
||||
|
|
83
rsconcept/frontend/src/app/GlobalDialogs.tsx
Normal file
83
rsconcept/frontend/src/app/GlobalDialogs.tsx
Normal file
|
@ -0,0 +1,83 @@
|
|||
'use client';
|
||||
|
||||
import DlgChangeInputSchema from '@/dialogs/DlgChangeInputSchema';
|
||||
import DlgChangeLocation from '@/dialogs/DlgChangeLocation';
|
||||
import DlgCloneLibraryItem from '@/dialogs/DlgCloneLibraryItem';
|
||||
import DlgCreateCst from '@/dialogs/DlgCreateCst';
|
||||
import DlgCreateOperation from '@/dialogs/DlgCreateOperation';
|
||||
import DlgCreateVersion from '@/dialogs/DlgCreateVersion';
|
||||
import DlgCstTemplate from '@/dialogs/DlgCstTemplate';
|
||||
import DlgDeleteCst from '@/dialogs/DlgDeleteCst';
|
||||
import DlgDeleteOperation from '@/dialogs/DlgDeleteOperation';
|
||||
import DlgEditEditors from '@/dialogs/DlgEditEditors';
|
||||
import DlgEditOperation from '@/dialogs/DlgEditOperation';
|
||||
import DlgEditReference from '@/dialogs/DlgEditReference';
|
||||
import DlgEditVersions from '@/dialogs/DlgEditVersions';
|
||||
import DlgEditWordForms from '@/dialogs/DlgEditWordForms';
|
||||
import DlgGraphParams from '@/dialogs/DlgGraphParams';
|
||||
import DlgInlineSynthesis from '@/dialogs/DlgInlineSynthesis';
|
||||
import DlgRelocateConstituents from '@/dialogs/DlgRelocateConstituents';
|
||||
import DlgRenameCst from '@/dialogs/DlgRenameCst';
|
||||
import DlgShowAST from '@/dialogs/DlgShowAST';
|
||||
import DlgShowQR from '@/dialogs/DlgShowQR';
|
||||
import DlgShowTypeGraph from '@/dialogs/DlgShowTypeGraph';
|
||||
import DlgSubstituteCst from '@/dialogs/DlgSubstituteCst';
|
||||
import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm';
|
||||
import { DialogType } from '@/models/miscellaneous';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
|
||||
export const GlobalDialogs = () => {
|
||||
const active = useDialogsStore(state => state.active);
|
||||
|
||||
if (active === undefined) {
|
||||
return null;
|
||||
}
|
||||
switch (active) {
|
||||
case DialogType.CONSTITUENTA_TEMPLATE:
|
||||
return <DlgCstTemplate />;
|
||||
case DialogType.CREATE_CONSTITUENTA:
|
||||
return <DlgCreateCst />;
|
||||
case DialogType.CREATE_OPERATION:
|
||||
return <DlgCreateOperation />;
|
||||
case DialogType.DELETE_CONSTITUENTA:
|
||||
return <DlgDeleteCst />;
|
||||
case DialogType.EDIT_EDITORS:
|
||||
return <DlgEditEditors />;
|
||||
case DialogType.EDIT_OPERATION:
|
||||
return <DlgEditOperation />;
|
||||
case DialogType.EDIT_REFERENCE:
|
||||
return <DlgEditReference />;
|
||||
case DialogType.EDIT_VERSIONS:
|
||||
return <DlgEditVersions />;
|
||||
case DialogType.EDIT_WORD_FORMS:
|
||||
return <DlgEditWordForms />;
|
||||
case DialogType.INLINE_SYNTHESIS:
|
||||
return <DlgInlineSynthesis />;
|
||||
case DialogType.SHOW_AST:
|
||||
return <DlgShowAST />;
|
||||
case DialogType.SHOW_TYPE_GRAPH:
|
||||
return <DlgShowTypeGraph />;
|
||||
case DialogType.CHANGE_INPUT_SCHEMA:
|
||||
return <DlgChangeInputSchema />;
|
||||
case DialogType.CHANGE_LOCATION:
|
||||
return <DlgChangeLocation />;
|
||||
case DialogType.CLONE_LIBRARY_ITEM:
|
||||
return <DlgCloneLibraryItem />;
|
||||
case DialogType.CREATE_VERSION:
|
||||
return <DlgCreateVersion />;
|
||||
case DialogType.DELETE_OPERATION:
|
||||
return <DlgDeleteOperation />;
|
||||
case DialogType.GRAPH_PARAMETERS:
|
||||
return <DlgGraphParams />;
|
||||
case DialogType.RELOCATE_CONSTITUENTS:
|
||||
return <DlgRelocateConstituents />;
|
||||
case DialogType.RENAME_CONSTITUENTA:
|
||||
return <DlgRenameCst />;
|
||||
case DialogType.SHOW_QR_CODE:
|
||||
return <DlgShowQR />;
|
||||
case DialogType.SUBSTITUTE_CONSTITUENTS:
|
||||
return <DlgSubstituteCst />;
|
||||
case DialogType.UPLOAD_RSFORM:
|
||||
return <DlgUploadRSForm />;
|
||||
}
|
||||
};
|
|
@ -1,13 +1,13 @@
|
|||
'use client';
|
||||
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import { AuthState } from '@/context/AuthContext';
|
||||
import { OptionsState } from '@/context/ConceptOptionsContext';
|
||||
import { queryClient } from '@/backend/queryClient';
|
||||
import { GlobalOssState } from '@/context/GlobalOssContext';
|
||||
import { LibraryState } from '@/context/LibraryContext';
|
||||
import { UsersState } from '@/context/UsersContext';
|
||||
|
||||
import ErrorFallback from './ErrorFallback';
|
||||
|
||||
|
@ -31,19 +31,16 @@ function GlobalProviders({ children }: React.PropsWithChildren) {
|
|||
onError={logError}
|
||||
>
|
||||
<IntlProvider locale='ru' defaultLocale='ru'>
|
||||
<OptionsState>
|
||||
<UsersState>
|
||||
<AuthState>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<LibraryState>
|
||||
<GlobalOssState>
|
||||
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
{children}
|
||||
|
||||
</GlobalOssState>
|
||||
</LibraryState>
|
||||
</AuthState>
|
||||
</UsersState>
|
||||
</OptionsState>
|
||||
</QueryClientProvider>
|
||||
</IntlProvider>
|
||||
</ErrorBoundary>);
|
||||
}
|
||||
|
|
32
rsconcept/frontend/src/app/GlobalTooltips.tsx
Normal file
32
rsconcept/frontend/src/app/GlobalTooltips.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
'use client';
|
||||
|
||||
import InfoConstituenta from '@/components/info/InfoConstituenta';
|
||||
import Loader from '@/components/ui/Loader';
|
||||
import Tooltip from '@/components/ui/Tooltip';
|
||||
import { useTooltipsStore } from '@/stores/tooltips';
|
||||
import { globals } from '@/utils/constants';
|
||||
|
||||
export const GlobalTooltips = () => {
|
||||
const hoverCst = useTooltipsStore(state => state.activeCst);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
float
|
||||
id={globals.tooltip}
|
||||
layer='z-topmost'
|
||||
place='right-start'
|
||||
className='mt-8 max-w-[20rem] break-words'
|
||||
/>
|
||||
<Tooltip
|
||||
float
|
||||
id={globals.value_tooltip}
|
||||
layer='z-topmost'
|
||||
className='max-w-[calc(min(40rem,100dvw-2rem))] text-justify'
|
||||
/>
|
||||
<Tooltip clickable id={globals.constituenta_tooltip} layer='z-modalTooltip' className='max-w-[30rem]'>
|
||||
{hoverCst ? <InfoConstituenta data={hoverCst} onClick={event => event.stopPropagation()} /> : <Loader />}
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,8 +1,8 @@
|
|||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import useWindowSize from '@/hooks/useWindowSize';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
|
||||
function Logo() {
|
||||
const { darkMode } = useConceptOptions();
|
||||
const darkMode = usePreferencesStore(state => state.darkMode);
|
||||
const size = useWindowSize();
|
||||
|
||||
return (
|
||||
|
|
|
@ -2,9 +2,9 @@ import clsx from 'clsx';
|
|||
|
||||
import { IconLibrary2, IconManuals, IconNewItem2 } from '@/components/Icons';
|
||||
import { CProps } from '@/components/props';
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||
import useWindowSize from '@/hooks/useWindowSize';
|
||||
import { useAppLayoutStore } from '@/stores/appLayout';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
||||
import { urls } from '../urls';
|
||||
|
@ -16,7 +16,7 @@ import UserMenu from './UserMenu';
|
|||
function Navigation() {
|
||||
const router = useConceptNavigation();
|
||||
const size = useWindowSize();
|
||||
const { noNavigationAnimation } = useConceptOptions();
|
||||
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
|
||||
|
||||
const navigateHome = (event: CProps.EventMouse) => router.push(urls.home, event.ctrlKey || event.metaKey);
|
||||
const navigateLibrary = (event: CProps.EventMouse) => router.push(urls.library, event.ctrlKey || event.metaKey);
|
||||
|
@ -29,7 +29,8 @@ function Navigation() {
|
|||
className={clsx(
|
||||
'z-navigation', // prettier: split lines
|
||||
'sticky top-0 left-0 right-0',
|
||||
'select-none'
|
||||
'select-none',
|
||||
'bg-prim-100'
|
||||
)}
|
||||
>
|
||||
<ToggleNavigation />
|
||||
|
@ -57,6 +58,7 @@ function Navigation() {
|
|||
<NavigationButton text='Новая схема' icon={<IconNewItem2 size='1.5rem' />} onClick={navigateCreateNew} />
|
||||
<NavigationButton text='Библиотека' icon={<IconLibrary2 size='1.5rem' />} onClick={navigateLibrary} />
|
||||
<NavigationButton text='Справка' icon={<IconManuals size='1.5rem' />} onClick={navigateHelp} />
|
||||
|
||||
<UserMenu />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { IconDarkTheme, IconLightTheme, IconPin, IconUnpin } from '@/components/Icons';
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { useAppLayoutStore } from '@/stores/appLayout';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { globals, PARAMETER } from '@/utils/constants';
|
||||
|
||||
function ToggleNavigation() {
|
||||
const { noNavigationAnimation, noNavigation, toggleNoNavigation, toggleDarkMode, darkMode } = useConceptOptions();
|
||||
const darkMode = usePreferencesStore(state => state.darkMode);
|
||||
const toggleDarkMode = usePreferencesStore(state => state.toggleDarkMode);
|
||||
const noNavigation = useAppLayoutStore(state => state.noNavigation);
|
||||
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
|
||||
const toggleNoNavigation = useAppLayoutStore(state => state.toggleNoNavigation);
|
||||
const iconSize = !noNavigationAnimation ? '0.75rem' : '1rem';
|
||||
return (
|
||||
<div
|
||||
|
|
35
rsconcept/frontend/src/app/Navigation/UserButton.tsx
Normal file
35
rsconcept/frontend/src/app/Navigation/UserButton.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { useAuthSuspense } from '@/backend/auth/useAuth';
|
||||
import { IconLogin, IconUser2 } from '@/components/Icons';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
|
||||
import NavigationButton from './NavigationButton';
|
||||
|
||||
interface UserButtonProps {
|
||||
onLogin: () => void;
|
||||
onClickUser: () => void;
|
||||
}
|
||||
|
||||
function UserButton({ onLogin, onClickUser }: UserButtonProps) {
|
||||
const { user } = useAuthSuspense();
|
||||
const adminMode = usePreferencesStore(state => state.adminMode);
|
||||
if (!user) {
|
||||
return (
|
||||
<NavigationButton
|
||||
className='cc-fade-in'
|
||||
title='Перейти на страницу логина'
|
||||
icon={<IconLogin size='1.5rem' className='icon-primary' />}
|
||||
onClick={onLogin}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<NavigationButton
|
||||
className='cc-fade-in'
|
||||
icon={<IconUser2 size='1.5rem' className={adminMode && user.is_staff ? 'icon-primary' : ''} />}
|
||||
onClick={onClickUser}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default UserButton;
|
|
@ -1,3 +1,5 @@
|
|||
import { useAuth } from '@/backend/auth/useAuth';
|
||||
import { useLogout } from '@/backend/auth/useLogout';
|
||||
import {
|
||||
IconAdmin,
|
||||
IconAdminOff,
|
||||
|
@ -15,9 +17,8 @@ import {
|
|||
import { CProps } from '@/components/props';
|
||||
import Dropdown from '@/components/ui/Dropdown';
|
||||
import DropdownButton from '@/components/ui/DropdownButton';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
|
||||
import { urls } from '../urls';
|
||||
|
||||
|
@ -27,9 +28,16 @@ interface UserDropdownProps {
|
|||
}
|
||||
|
||||
function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
|
||||
const { darkMode, adminMode, toggleAdminMode, toggleDarkMode, showHelp, toggleShowHelp } = useConceptOptions();
|
||||
const router = useConceptNavigation();
|
||||
const { user, logout } = useAuth();
|
||||
const { user } = useAuth();
|
||||
const { logout } = useLogout();
|
||||
|
||||
const darkMode = usePreferencesStore(state => state.darkMode);
|
||||
const toggleDarkMode = usePreferencesStore(state => state.toggleDarkMode);
|
||||
const showHelp = usePreferencesStore(state => state.showHelp);
|
||||
const toggleShowHelp = usePreferencesStore(state => state.toggleShowHelp);
|
||||
const adminMode = usePreferencesStore(state => state.adminMode);
|
||||
const toggleAdminMode = usePreferencesStore(state => state.toggleAdminMode);
|
||||
|
||||
function navigateProfile(event: CProps.EventMouse) {
|
||||
hideDropdown();
|
||||
|
|
|
@ -1,41 +1,22 @@
|
|||
import { IconLogin, IconUser2 } from '@/components/Icons';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import Loader from '@/components/ui/Loader';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||
import useDropdown from '@/hooks/useDropdown';
|
||||
|
||||
import { urls } from '../urls';
|
||||
import NavigationButton from './NavigationButton';
|
||||
import UserButton from './UserButton';
|
||||
import UserDropdown from './UserDropdown';
|
||||
|
||||
function UserMenu() {
|
||||
const router = useConceptNavigation();
|
||||
const { user, loading } = useAuth();
|
||||
const { adminMode } = useConceptOptions();
|
||||
const menu = useDropdown();
|
||||
|
||||
const navigateLogin = () => router.push(urls.login);
|
||||
|
||||
return (
|
||||
<div ref={menu.ref} className='h-full w-[4rem] flex items-center justify-center'>
|
||||
{loading ? <Loader circular scale={1.5} /> : null}
|
||||
{!user && !loading ? (
|
||||
<NavigationButton
|
||||
className='cc-fade-in'
|
||||
title='Перейти на страницу логина'
|
||||
icon={<IconLogin size='1.5rem' className='icon-primary' />}
|
||||
onClick={navigateLogin}
|
||||
/>
|
||||
) : null}
|
||||
{user && !loading ? (
|
||||
<NavigationButton
|
||||
className='cc-fade-in'
|
||||
icon={<IconUser2 size='1.5rem' className={adminMode && user.is_staff ? 'icon-primary' : ''} />}
|
||||
onClick={menu.toggle}
|
||||
/>
|
||||
) : null}
|
||||
<UserDropdown isOpen={!!user && menu.isOpen} hideDropdown={() => menu.hide()} />
|
||||
<Suspense fallback={<Loader circular scale={1.5} />}>
|
||||
<UserButton onLogin={() => router.push(urls.login)} onClickUser={menu.toggle} />
|
||||
</Suspense>
|
||||
<UserDropdown isOpen={menu.isOpen} hideDropdown={() => menu.hide()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -42,17 +42,17 @@ export interface IAxiosRequest<RequestData, ResponseData> {
|
|||
|
||||
// ================ Transport API calls ================
|
||||
export function AxiosGet<ResponseData>({ endpoint, request, options }: IAxiosRequest<undefined, ResponseData>) {
|
||||
if (request.setLoading) request.setLoading(true);
|
||||
request.setLoading?.(true);
|
||||
axiosInstance
|
||||
.get<ResponseData>(endpoint, options)
|
||||
.then(response => {
|
||||
if (request.setLoading) request.setLoading(false);
|
||||
if (request.onSuccess) request.onSuccess(response.data);
|
||||
request.setLoading?.(false);
|
||||
request.onSuccess?.(response.data);
|
||||
})
|
||||
.catch((error: Error | AxiosError) => {
|
||||
if (request.setLoading) request.setLoading(false);
|
||||
request.setLoading?.(false);
|
||||
if (request.showError) toast.error(extractErrorMessage(error));
|
||||
if (request.onError) request.onError(error);
|
||||
request.onError?.(error);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -61,17 +61,17 @@ export function AxiosPost<RequestData, ResponseData>({
|
|||
request,
|
||||
options
|
||||
}: IAxiosRequest<RequestData, ResponseData>) {
|
||||
if (request.setLoading) request.setLoading(true);
|
||||
request.setLoading?.(true);
|
||||
axiosInstance
|
||||
.post<ResponseData>(endpoint, request.data, options)
|
||||
.then(response => {
|
||||
if (request.setLoading) request.setLoading(false);
|
||||
if (request.onSuccess) request.onSuccess(response.data);
|
||||
request.setLoading?.(false);
|
||||
request.onSuccess?.(response.data);
|
||||
})
|
||||
.catch((error: Error | AxiosError) => {
|
||||
if (request.setLoading) request.setLoading(false);
|
||||
request.setLoading?.(false);
|
||||
if (request.showError) toast.error(extractErrorMessage(error));
|
||||
if (request.onError) request.onError(error);
|
||||
request.onError?.(error);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -80,17 +80,17 @@ export function AxiosDelete<RequestData, ResponseData>({
|
|||
request,
|
||||
options
|
||||
}: IAxiosRequest<RequestData, ResponseData>) {
|
||||
if (request.setLoading) request.setLoading(true);
|
||||
request.setLoading?.(true);
|
||||
axiosInstance
|
||||
.delete<ResponseData>(endpoint, options)
|
||||
.then(response => {
|
||||
if (request.setLoading) request.setLoading(false);
|
||||
if (request.onSuccess) request.onSuccess(response.data);
|
||||
request.setLoading?.(false);
|
||||
request.onSuccess?.(response.data);
|
||||
})
|
||||
.catch((error: Error | AxiosError) => {
|
||||
if (request.setLoading) request.setLoading(false);
|
||||
request.setLoading?.(false);
|
||||
if (request.showError) toast.error(extractErrorMessage(error));
|
||||
if (request.onError) request.onError(error);
|
||||
request.onError?.(error);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -99,17 +99,17 @@ export function AxiosPatch<RequestData, ResponseData>({
|
|||
request,
|
||||
options
|
||||
}: IAxiosRequest<RequestData, ResponseData>) {
|
||||
if (request.setLoading) request.setLoading(true);
|
||||
request.setLoading?.(true);
|
||||
axiosInstance
|
||||
.patch<ResponseData>(endpoint, request.data, options)
|
||||
.then(response => {
|
||||
if (request.setLoading) request.setLoading(false);
|
||||
if (request.onSuccess) request.onSuccess(response.data);
|
||||
request.setLoading?.(false);
|
||||
request.onSuccess?.(response.data);
|
||||
return response.data;
|
||||
})
|
||||
.catch((error: Error | AxiosError) => {
|
||||
if (request.setLoading) request.setLoading(false);
|
||||
request.setLoading?.(false);
|
||||
if (request.showError) toast.error(extractErrorMessage(error));
|
||||
if (request.onError) request.onError(error);
|
||||
request.onError?.(error);
|
||||
});
|
||||
}
|
||||
|
|
67
rsconcept/frontend/src/backend/auth/api.ts
Normal file
67
rsconcept/frontend/src/backend/auth/api.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { queryOptions } from '@tanstack/react-query';
|
||||
|
||||
import { ICurrentUser, IUser } from '@/models/user';
|
||||
|
||||
import { axiosInstance } from '../apiConfiguration';
|
||||
|
||||
/**
|
||||
* Represents login data, used to authenticate users.
|
||||
*/
|
||||
export interface IUserLoginDTO {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents data needed to update password for current user.
|
||||
*/
|
||||
export interface IChangePasswordDTO {
|
||||
old_password: string;
|
||||
new_password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents password reset request data.
|
||||
*/
|
||||
export interface IRequestPasswordDTO extends Pick<IUser, 'email'> {}
|
||||
|
||||
/**
|
||||
* Represents password reset data.
|
||||
*/
|
||||
export interface IResetPasswordDTO {
|
||||
password: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents password token data.
|
||||
*/
|
||||
export interface IPasswordTokenDTO extends Pick<IResetPasswordDTO, 'token'> {}
|
||||
|
||||
/**
|
||||
* Authentication API.
|
||||
*/
|
||||
export const authApi = {
|
||||
baseKey: 'auth',
|
||||
|
||||
getAuthQueryOptions: () => {
|
||||
return queryOptions({
|
||||
queryKey: [authApi.baseKey, 'user'],
|
||||
queryFn: meta =>
|
||||
axiosInstance
|
||||
.get<ICurrentUser>('/users/api/auth', {
|
||||
signal: meta.signal
|
||||
})
|
||||
.then(response => (response.data.id === null ? null : response.data)),
|
||||
placeholderData: null,
|
||||
staleTime: 24 * 60 * 60 * 1000
|
||||
});
|
||||
},
|
||||
|
||||
logout: () => axiosInstance.post('/users/api/logout'),
|
||||
login: (data: IUserLoginDTO) => axiosInstance.post('/users/api/login', data),
|
||||
changePassword: (data: IChangePasswordDTO) => axiosInstance.post('/users/api/change-password', data),
|
||||
requestPasswordReset: (data: IRequestPasswordDTO) => axiosInstance.post('/users/api/password-reset', data),
|
||||
validatePasswordToken: (data: IPasswordTokenDTO) => axiosInstance.post('/users/api/password-reset/validate', data),
|
||||
resetPassword: (data: IResetPasswordDTO) => axiosInstance.post('/users/api/password-reset/confirm', data)
|
||||
};
|
21
rsconcept/frontend/src/backend/auth/useAuth.tsx
Normal file
21
rsconcept/frontend/src/backend/auth/useAuth.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
||||
|
||||
import { authApi } from './api';
|
||||
|
||||
export function useAuth() {
|
||||
const {
|
||||
data: user,
|
||||
isLoading,
|
||||
error
|
||||
} = useQuery({
|
||||
...authApi.getAuthQueryOptions()
|
||||
});
|
||||
return { user, isLoading, error };
|
||||
}
|
||||
|
||||
export function useAuthSuspense() {
|
||||
const { data: user } = useSuspenseQuery({
|
||||
...authApi.getAuthQueryOptions()
|
||||
});
|
||||
return { user };
|
||||
}
|
18
rsconcept/frontend/src/backend/auth/useChangePassword.tsx
Normal file
18
rsconcept/frontend/src/backend/auth/useChangePassword.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { authApi, IChangePasswordDTO } from './api';
|
||||
|
||||
export const useChangePassword = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const mutation = useMutation({
|
||||
mutationKey: ['change-password'],
|
||||
mutationFn: authApi.changePassword,
|
||||
onSettled: () => queryClient.invalidateQueries({ queryKey: [authApi.baseKey] })
|
||||
});
|
||||
return {
|
||||
changePassword: (data: IChangePasswordDTO, onSuccess?: () => void) => mutation.mutate(data, { onSuccess }),
|
||||
isPending: mutation.isPending,
|
||||
error: mutation.error,
|
||||
reset: mutation.reset
|
||||
};
|
||||
};
|
19
rsconcept/frontend/src/backend/auth/useLogin.tsx
Normal file
19
rsconcept/frontend/src/backend/auth/useLogin.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { authApi } from './api';
|
||||
|
||||
export const useLogin = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const mutation = useMutation({
|
||||
mutationKey: ['login'],
|
||||
mutationFn: authApi.login,
|
||||
onSettled: () => queryClient.invalidateQueries({ queryKey: [authApi.baseKey] })
|
||||
});
|
||||
return {
|
||||
login: (username: string, password: string, onSuccess?: () => void) =>
|
||||
mutation.mutate({ username, password }, { onSuccess }),
|
||||
isPending: mutation.isPending,
|
||||
error: mutation.error,
|
||||
reset: mutation.reset
|
||||
};
|
||||
};
|
13
rsconcept/frontend/src/backend/auth/useLogout.tsx
Normal file
13
rsconcept/frontend/src/backend/auth/useLogout.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { authApi } from './api';
|
||||
|
||||
export const useLogout = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const mutation = useMutation({
|
||||
mutationKey: ['logout'],
|
||||
mutationFn: authApi.logout,
|
||||
onSettled: () => queryClient.invalidateQueries({ queryKey: [authApi.baseKey] })
|
||||
});
|
||||
return { logout: (onSuccess?: () => void) => mutation.mutate(undefined, { onSuccess }) };
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { authApi, IRequestPasswordDTO } from './api';
|
||||
|
||||
export const useRequestPasswordReset = () => {
|
||||
const mutation = useMutation({
|
||||
mutationKey: ['request-password-reset'],
|
||||
mutationFn: authApi.requestPasswordReset
|
||||
});
|
||||
return {
|
||||
requestPasswordReset: (data: IRequestPasswordDTO, onSuccess?: () => void) => mutation.mutate(data, { onSuccess }),
|
||||
isPending: mutation.isPending,
|
||||
error: mutation.error,
|
||||
reset: mutation.reset
|
||||
};
|
||||
};
|
24
rsconcept/frontend/src/backend/auth/useResetPassword.tsx
Normal file
24
rsconcept/frontend/src/backend/auth/useResetPassword.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { authApi, IPasswordTokenDTO, IResetPasswordDTO } from './api';
|
||||
|
||||
export const useResetPassword = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const validateMutation = useMutation({
|
||||
mutationKey: ['reset-password'],
|
||||
mutationFn: authApi.validatePasswordToken,
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: [authApi.baseKey] })
|
||||
});
|
||||
const resetMutation = useMutation({
|
||||
mutationKey: ['reset-password'],
|
||||
mutationFn: authApi.resetPassword,
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: [authApi.baseKey] })
|
||||
});
|
||||
return {
|
||||
validateToken: (data: IPasswordTokenDTO, onSuccess?: () => void) => validateMutation.mutate(data, { onSuccess }),
|
||||
resetPassword: (data: IResetPasswordDTO, onSuccess?: () => void) => resetMutation.mutate(data, { onSuccess }),
|
||||
isPending: resetMutation.isPending,
|
||||
error: resetMutation.error,
|
||||
reset: resetMutation.reset
|
||||
};
|
||||
};
|
21
rsconcept/frontend/src/backend/queryClient.ts
Normal file
21
rsconcept/frontend/src/backend/queryClient.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { QueryClient } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
|
||||
declare module '@tanstack/react-query' {
|
||||
interface Register {
|
||||
defaultError: AxiosError;
|
||||
}
|
||||
}
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 5 * 60 * 1000,
|
||||
gcTime: 24 * 60 * 60 * 1000,
|
||||
retry: 3,
|
||||
refetchOnWindowFocus: true,
|
||||
refetchOnMount: true,
|
||||
refetchOnReconnect: true
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,99 +0,0 @@
|
|||
/**
|
||||
* Endpoints: users.
|
||||
*/
|
||||
|
||||
import {
|
||||
ICurrentUser,
|
||||
IPasswordTokenData,
|
||||
IRequestPasswordData,
|
||||
IResetPasswordData,
|
||||
IUserInfo,
|
||||
IUserLoginData,
|
||||
IUserProfile,
|
||||
IUserSignupData,
|
||||
IUserUpdateData,
|
||||
IUserUpdatePassword
|
||||
} from '@/models/user';
|
||||
|
||||
import { AxiosGet, AxiosPatch, AxiosPost, FrontAction, FrontExchange, FrontPull, FrontPush } from './apiTransport';
|
||||
|
||||
export function getAuth(request: FrontPull<ICurrentUser>) {
|
||||
AxiosGet({
|
||||
endpoint: `/users/api/auth`,
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
||||
export function postLogin(request: FrontPush<IUserLoginData>) {
|
||||
AxiosPost({
|
||||
endpoint: '/users/api/login',
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
||||
export function postLogout(request: FrontAction) {
|
||||
AxiosPost({
|
||||
endpoint: '/users/api/logout',
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
||||
export function postSignup(request: FrontExchange<IUserSignupData, IUserProfile>) {
|
||||
AxiosPost({
|
||||
endpoint: '/users/api/signup',
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
||||
export function getProfile(request: FrontPull<IUserProfile>) {
|
||||
AxiosGet({
|
||||
endpoint: '/users/api/profile',
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
||||
export function patchProfile(request: FrontExchange<IUserUpdateData, IUserProfile>) {
|
||||
AxiosPatch({
|
||||
endpoint: '/users/api/profile',
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
||||
export function patchPassword(request: FrontPush<IUserUpdatePassword>) {
|
||||
AxiosPatch({
|
||||
endpoint: '/users/api/change-password',
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
||||
export function postRequestPasswordReset(request: FrontPush<IRequestPasswordData>) {
|
||||
// title: 'Request password reset',
|
||||
AxiosPost({
|
||||
endpoint: '/users/api/password-reset',
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
||||
export function postValidatePasswordToken(request: FrontPush<IPasswordTokenData>) {
|
||||
// title: 'Validate password token',
|
||||
AxiosPost({
|
||||
endpoint: '/users/api/password-reset/validate',
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
||||
export function postResetPassword(request: FrontPush<IResetPasswordData>) {
|
||||
// title: 'Reset password',
|
||||
AxiosPost({
|
||||
endpoint: '/users/api/password-reset/confirm',
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
||||
export function getActiveUsers(request: FrontPull<IUserInfo[]>) {
|
||||
// title: 'Active users list',
|
||||
AxiosGet({
|
||||
endpoint: '/users/api/active-users',
|
||||
request: request
|
||||
});
|
||||
}
|
40
rsconcept/frontend/src/backend/users/api.ts
Normal file
40
rsconcept/frontend/src/backend/users/api.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { queryOptions } from '@tanstack/react-query';
|
||||
|
||||
import { IUser, IUserInfo, IUserProfile, IUserSignupData } from '@/models/user';
|
||||
|
||||
import { axiosInstance } from '../apiConfiguration';
|
||||
|
||||
/**
|
||||
* Represents user data, intended to update user profile in persistent storage.
|
||||
*/
|
||||
export interface IUpdateProfileDTO extends Omit<IUser, 'is_staff' | 'id'> {}
|
||||
|
||||
export const usersApi = {
|
||||
baseKey: 'users',
|
||||
getUsersQueryOptions: () =>
|
||||
queryOptions({
|
||||
queryKey: [usersApi.baseKey, 'list'],
|
||||
queryFn: meta =>
|
||||
axiosInstance
|
||||
.get<IUserInfo[]>('/users/api/active-users', {
|
||||
signal: meta.signal
|
||||
})
|
||||
.then(response => response.data),
|
||||
placeholderData: []
|
||||
}),
|
||||
getProfileQueryOptions: () =>
|
||||
queryOptions({
|
||||
queryKey: [usersApi.baseKey, 'profile'],
|
||||
queryFn: meta =>
|
||||
axiosInstance
|
||||
.get<IUserProfile>('/users/api/profile', {
|
||||
signal: meta.signal
|
||||
})
|
||||
.then(response => response.data)
|
||||
}),
|
||||
|
||||
signup: (data: IUserSignupData) => axiosInstance.post('/users/api/signup', data),
|
||||
updateProfile: (data: IUpdateProfileDTO) => axiosInstance.patch('/users/api/profile', data)
|
||||
};
|
||||
|
||||
//DataCallback<IUserProfile>
|
25
rsconcept/frontend/src/backend/users/useLabelUser.tsx
Normal file
25
rsconcept/frontend/src/backend/users/useLabelUser.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { useUsers } from './useUsers';
|
||||
|
||||
export function useLabelUser() {
|
||||
const { users } = useUsers();
|
||||
|
||||
function getUserLabel(userID: number | null): string {
|
||||
const user = users.find(({ id }) => id === userID);
|
||||
if (!user || userID === null) {
|
||||
return userID ? `Аноним ${userID.toString()}` : 'Отсутствует';
|
||||
}
|
||||
const hasFirstName = user.first_name !== '';
|
||||
const hasLastName = user.last_name !== '';
|
||||
if (hasFirstName || hasLastName) {
|
||||
if (!hasLastName) {
|
||||
return user.first_name;
|
||||
}
|
||||
if (!hasFirstName) {
|
||||
return user.last_name;
|
||||
}
|
||||
return user.last_name + ' ' + user.first_name;
|
||||
}
|
||||
return `Аноним ${userID.toString()}`;
|
||||
}
|
||||
return getUserLabel;
|
||||
}
|
21
rsconcept/frontend/src/backend/users/useProfile.tsx
Normal file
21
rsconcept/frontend/src/backend/users/useProfile.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
||||
|
||||
import { usersApi } from './api';
|
||||
|
||||
export function useProfile() {
|
||||
const {
|
||||
data: profile,
|
||||
isLoading,
|
||||
error
|
||||
} = useQuery({
|
||||
...usersApi.getProfileQueryOptions()
|
||||
});
|
||||
return { profile, isLoading, error };
|
||||
}
|
||||
|
||||
export function useProfileSuspense() {
|
||||
const { data: profile } = useSuspenseQuery({
|
||||
...usersApi.getProfileQueryOptions()
|
||||
});
|
||||
return { profile };
|
||||
}
|
22
rsconcept/frontend/src/backend/users/useSignup.tsx
Normal file
22
rsconcept/frontend/src/backend/users/useSignup.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { usersApi } from '@/backend/users/api';
|
||||
import { IUserProfile, IUserSignupData } from '@/models/user';
|
||||
|
||||
import { DataCallback } from '../apiTransport';
|
||||
|
||||
export const useSignup = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const mutation = useMutation({
|
||||
mutationKey: ['signup'],
|
||||
mutationFn: usersApi.signup,
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: [usersApi.baseKey] })
|
||||
});
|
||||
return {
|
||||
signup: (data: IUserSignupData, onSuccess?: DataCallback<IUserProfile>) =>
|
||||
mutation.mutate(data, { onSuccess: response => onSuccess?.(response.data as IUserProfile) }),
|
||||
isPending: mutation.isPending,
|
||||
error: mutation.error,
|
||||
reset: mutation.reset
|
||||
};
|
||||
};
|
23
rsconcept/frontend/src/backend/users/useUpdateProfile.tsx
Normal file
23
rsconcept/frontend/src/backend/users/useUpdateProfile.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { IUserProfile } from '@/models/user';
|
||||
|
||||
import { IUpdateProfileDTO, usersApi } from './api';
|
||||
|
||||
// TODO: reload users / optimistic update
|
||||
|
||||
export const useUpdateProfile = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const mutation = useMutation({
|
||||
mutationKey: ['update-profile'],
|
||||
mutationFn: usersApi.updateProfile,
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: [usersApi.baseKey] })
|
||||
});
|
||||
return {
|
||||
updateProfile: (data: IUpdateProfileDTO, onSuccess?: (newUser: IUserProfile) => void) =>
|
||||
mutation.mutate(data, { onSuccess: response => onSuccess?.(response.data as IUserProfile) }),
|
||||
isPending: mutation.isPending,
|
||||
error: mutation.error,
|
||||
reset: mutation.reset
|
||||
};
|
||||
};
|
18
rsconcept/frontend/src/backend/users/useUsers.tsx
Normal file
18
rsconcept/frontend/src/backend/users/useUsers.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
||||
|
||||
import { usersApi } from './api';
|
||||
|
||||
export function useUsersSuspense() {
|
||||
const { data: users } = useSuspenseQuery({
|
||||
...usersApi.getUsersQueryOptions(),
|
||||
initialData: []
|
||||
});
|
||||
return { users };
|
||||
}
|
||||
|
||||
export function useUsers() {
|
||||
const { data: users } = useQuery({
|
||||
...usersApi.getUsersQueryOptions()
|
||||
});
|
||||
return { users: users ?? [] };
|
||||
}
|
|
@ -9,10 +9,10 @@ import { EditorView } from 'codemirror';
|
|||
import { forwardRef, useRef } from 'react';
|
||||
|
||||
import Label from '@/components/ui/Label';
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { ConstituentaID, IRSForm } from '@/models/rsform';
|
||||
import { generateAlias, getCstTypePrefix, guessCstType } from '@/models/rsformAPI';
|
||||
import { extractGlobals } from '@/models/rslangAPI';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { APP_COLORS } from '@/styling/color';
|
||||
|
||||
import { ccBracketMatching } from './bracketMatching';
|
||||
|
@ -64,7 +64,7 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
|
|||
},
|
||||
ref
|
||||
) => {
|
||||
const { darkMode } = useConceptOptions();
|
||||
const darkMode = usePreferencesStore(state => state.darkMode);
|
||||
|
||||
const internalRef = useRef<ReactCodeMirrorRef>(null);
|
||||
const thisRef = !ref || typeof ref === 'function' ? internalRef : ref;
|
||||
|
|
|
@ -9,13 +9,13 @@ import { EditorView } from 'codemirror';
|
|||
import { forwardRef, useRef, useState } from 'react';
|
||||
|
||||
import Label from '@/components/ui/Label';
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import DlgEditReference from '@/dialogs/DlgEditReference';
|
||||
import { ReferenceType } from '@/models/language';
|
||||
import { DialogType } from '@/models/miscellaneous';
|
||||
import { ConstituentaID, IRSForm } from '@/models/rsform';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { APP_COLORS } from '@/styling/color';
|
||||
import { CodeMirrorWrapper } from '@/utils/codemirror';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
||||
import { refsNavigation } from './clickNavigation';
|
||||
import { NaturalLanguage, ReferenceTokens } from './parse';
|
||||
|
@ -92,11 +92,14 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
|
|||
},
|
||||
ref
|
||||
) => {
|
||||
const { darkMode } = useConceptOptions();
|
||||
const darkMode = usePreferencesStore(state => state.darkMode);
|
||||
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
const [showEditor, setShowEditor] = useState(false);
|
||||
const showEditReference = useDialogsStore(state => state.showEditReference);
|
||||
const activeDialog = useDialogsStore(state => state.active);
|
||||
const isActive = activeDialog === DialogType.EDIT_REFERENCE;
|
||||
|
||||
const [currentType, setCurrentType] = useState<ReferenceType>(ReferenceType.ENTITY);
|
||||
const [refText, setRefText] = useState('');
|
||||
const [hintText, setHintText] = useState('');
|
||||
|
@ -146,7 +149,7 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
|
|||
}
|
||||
|
||||
function handleInput(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
if (!thisRef.current?.view) {
|
||||
if (!thisRef.current?.view || !schema) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
|
@ -174,7 +177,17 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
|
|||
setMainRefs(mainNodes.map(node => wrap.getText(node.from, node.to)));
|
||||
setBasePosition(mainNodes.filter(node => node.to <= selection.from).length);
|
||||
|
||||
setShowEditor(true);
|
||||
showEditReference({
|
||||
schema: schema,
|
||||
initial: {
|
||||
type: currentType,
|
||||
refRaw: refText,
|
||||
text: hintText,
|
||||
basePosition: basePosition,
|
||||
mainRefs: mainRefs
|
||||
},
|
||||
onSave: handleInputReference
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -187,27 +200,8 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
|
|||
wrap.replaceWith(referenceText);
|
||||
}
|
||||
|
||||
function hideEditReference() {
|
||||
setShowEditor(false);
|
||||
setTimeout(() => thisRef.current?.view?.focus(), PARAMETER.refreshTimeout);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx('flex flex-col gap-2', cursor)}>
|
||||
{showEditor && schema ? (
|
||||
<DlgEditReference
|
||||
hideWindow={hideEditReference}
|
||||
schema={schema}
|
||||
initial={{
|
||||
type: currentType,
|
||||
refRaw: refText,
|
||||
text: hintText,
|
||||
basePosition: basePosition,
|
||||
mainRefs: mainRefs
|
||||
}}
|
||||
onSave={handleInputReference}
|
||||
/>
|
||||
) : null}
|
||||
<Label text={label} />
|
||||
<CodeMirror
|
||||
id={id}
|
||||
|
@ -215,10 +209,10 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
|
|||
basicSetup={editorSetup}
|
||||
theme={customTheme}
|
||||
extensions={editorExtensions}
|
||||
value={isFocused ? value : value !== initialValue || showEditor ? value : resolved}
|
||||
value={isFocused ? value : value !== initialValue || isActive ? value : resolved}
|
||||
indentWithTab={false}
|
||||
onChange={handleChange}
|
||||
editable={!disabled && !showEditor}
|
||||
editable={!disabled && !isActive}
|
||||
onKeyDown={handleInput}
|
||||
onFocus={handleFocusIn}
|
||||
onBlur={handleFocusOut}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { CstClass, IConstituenta } from '@/models/rsform';
|
||||
import { useTooltipsStore } from '@/stores/tooltips';
|
||||
import { APP_COLORS, colorFgCstStatus } from '@/styling/color';
|
||||
import { globals } from '@/utils/constants';
|
||||
|
||||
|
@ -19,7 +19,7 @@ interface BadgeConstituentaProps extends CProps.Styling {
|
|||
* Displays a badge with a constituenta alias and information tooltip.
|
||||
*/
|
||||
function BadgeConstituenta({ value, prefixID, className, style }: BadgeConstituentaProps) {
|
||||
const { setHoverCst } = useConceptOptions();
|
||||
const setActiveCst = useTooltipsStore(state => state.setActiveCst);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -39,7 +39,7 @@ function BadgeConstituenta({ value, prefixID, className, style }: BadgeConstitue
|
|||
...style
|
||||
}}
|
||||
data-tooltip-id={globals.constituenta_tooltip}
|
||||
onMouseEnter={() => setHoverCst(value)}
|
||||
onMouseEnter={() => setActiveCst(value)}
|
||||
>
|
||||
{value.alias}
|
||||
</div>
|
||||
|
|
|
@ -2,8 +2,8 @@ import React, { Suspense } from 'react';
|
|||
|
||||
import TextURL from '@/components/ui/TextURL';
|
||||
import Tooltip, { PlacesType } from '@/components/ui/Tooltip';
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
|
||||
import { IconHelp } from '../Icons';
|
||||
import { CProps } from '../props';
|
||||
|
@ -29,7 +29,7 @@ interface BadgeHelpProps extends CProps.Styling {
|
|||
* Display help icon with a manual page tooltip.
|
||||
*/
|
||||
function BadgeHelp({ topic, padding = 'p-1', ...restProps }: BadgeHelpProps) {
|
||||
const { showHelp } = useConceptOptions();
|
||||
const showHelp = usePreferencesStore(state => state.showHelp);
|
||||
|
||||
if (!showHelp) {
|
||||
return null;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { useUsers } from '@/context/UsersContext';
|
||||
import { useLabelUser } from '@/backend/users/useLabelUser';
|
||||
import { UserID } from '@/models/user';
|
||||
|
||||
import { CProps } from '../props';
|
||||
|
@ -12,8 +12,7 @@ interface InfoUsersProps extends CProps.Styling {
|
|||
}
|
||||
|
||||
function InfoUsers({ items, className, prefix, header, ...restProps }: InfoUsersProps) {
|
||||
const { getUserLabel } = useUsers();
|
||||
|
||||
const getUserLabel = useLabelUser();
|
||||
return (
|
||||
<div className={clsx('flex flex-col dense', className)} {...restProps}>
|
||||
{header ? <h2>{header}</h2> : null}
|
||||
|
|
|
@ -2,17 +2,18 @@
|
|||
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { useUsers } from '@/context/UsersContext';
|
||||
import { IUserInfo, UserID } from '@/models/user';
|
||||
import { useLabelUser } from '@/backend/users/useLabelUser';
|
||||
import { useUsers } from '@/backend/users/useUsers';
|
||||
import { UserID } from '@/models/user';
|
||||
import { matchUser } from '@/models/userAPI';
|
||||
|
||||
import { CProps } from '../props';
|
||||
import SelectSingle from '../ui/SelectSingle';
|
||||
|
||||
interface SelectUserProps extends CProps.Styling {
|
||||
items?: IUserInfo[];
|
||||
value?: UserID;
|
||||
onSelectValue: (newValue: UserID) => void;
|
||||
filter?: (userID: UserID) => boolean;
|
||||
|
||||
placeholder?: string;
|
||||
noBorder?: boolean;
|
||||
|
@ -20,20 +21,23 @@ interface SelectUserProps extends CProps.Styling {
|
|||
|
||||
function SelectUser({
|
||||
className,
|
||||
items,
|
||||
filter,
|
||||
value,
|
||||
onSelectValue,
|
||||
placeholder = 'Выберите пользователя',
|
||||
...restProps
|
||||
}: SelectUserProps) {
|
||||
const { getUserLabel } = useUsers();
|
||||
const { users } = useUsers();
|
||||
const getUserLabel = useLabelUser();
|
||||
|
||||
const items = filter ? users.filter(user => filter(user.id)) : users;
|
||||
const options =
|
||||
items?.map(user => ({
|
||||
value: user.id,
|
||||
label: getUserLabel(user.id)
|
||||
})) ?? [];
|
||||
|
||||
function filter(option: { value: UserID | undefined; label: string }, inputValue: string) {
|
||||
function filterLabel(option: { value: UserID | undefined; label: string }, inputValue: string) {
|
||||
const user = items?.find(item => item.id === option.value);
|
||||
return !user ? false : matchUser(user, inputValue);
|
||||
}
|
||||
|
@ -47,7 +51,7 @@ function SelectUser({
|
|||
if (data?.value !== undefined) onSelectValue(data.value);
|
||||
}}
|
||||
// @ts-expect-error: TODO: use type definitions from react-select in filter object
|
||||
filterOption={filter}
|
||||
filterOption={filterLabel}
|
||||
placeholder={placeholder}
|
||||
{...restProps}
|
||||
/>
|
||||
|
|
|
@ -4,6 +4,7 @@ import clsx from 'clsx';
|
|||
|
||||
import useEscapeKey from '@/hooks/useEscapeKey';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { prepareTooltip } from '@/utils/labels';
|
||||
|
||||
|
@ -32,9 +33,6 @@ export interface ModalProps extends CProps.Styling {
|
|||
/** Indicates that the modal window should be scrollable. */
|
||||
overflowVisible?: boolean;
|
||||
|
||||
/** Callback to be called when the modal window is closed. */
|
||||
hideWindow: () => void;
|
||||
|
||||
/** Callback to be called before submit. */
|
||||
beforeSubmit?: () => boolean;
|
||||
|
||||
|
@ -65,7 +63,6 @@ function Modal({
|
|||
canSubmit,
|
||||
overflowVisible,
|
||||
|
||||
hideWindow,
|
||||
beforeSubmit,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
|
@ -75,10 +72,11 @@ function Modal({
|
|||
hideHelpWhen,
|
||||
...restProps
|
||||
}: React.PropsWithChildren<ModalProps>) {
|
||||
useEscapeKey(hideWindow);
|
||||
const hideDialog = useDialogsStore(state => state.hideDialog);
|
||||
useEscapeKey(hideDialog);
|
||||
|
||||
const handleCancel = () => {
|
||||
hideWindow();
|
||||
hideDialog();
|
||||
onCancel?.();
|
||||
};
|
||||
|
||||
|
@ -87,7 +85,7 @@ function Modal({
|
|||
return;
|
||||
}
|
||||
onSubmit?.();
|
||||
hideWindow();
|
||||
hideDialog();
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -95,7 +93,7 @@ function Modal({
|
|||
<div className={clsx('z-navigation', 'fixed top-0 left-0', 'w-full h-full', 'backdrop-blur-[3px] opacity-50')} />
|
||||
<div
|
||||
className={clsx('z-navigation', 'fixed top-0 left-0', 'w-full h-full', 'bg-prim-0 opacity-25')}
|
||||
onClick={hideWindow}
|
||||
onClick={hideDialog}
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import useWindowSize from '@/hooks/useWindowSize';
|
||||
import { useFitHeight } from '@/stores/appLayout';
|
||||
|
||||
/** Maximum width of the viewer. */
|
||||
const MAXIMUM_WIDTH = 1600;
|
||||
|
@ -25,10 +25,9 @@ interface PDFViewerProps {
|
|||
*/
|
||||
function PDFViewer({ file, offsetXpx, minWidth = MINIMUM_WIDTH }: PDFViewerProps) {
|
||||
const windowSize = useWindowSize();
|
||||
const { calculateHeight } = useConceptOptions();
|
||||
|
||||
const pageWidth = Math.max(minWidth, Math.min((windowSize?.width ?? 0) - (offsetXpx ?? 0) - 10, MAXIMUM_WIDTH));
|
||||
const pageHeight = calculateHeight('1rem');
|
||||
const pageHeight = useFitHeight('1rem');
|
||||
|
||||
return <embed src={`${file}#toolbar=0`} className='p-3' style={{ width: pageWidth, height: pageHeight }} />;
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ function SubmitButton({ text = 'ОК', icon, disabled, loading, className, ...re
|
|||
loading && 'cursor-progress',
|
||||
className
|
||||
)}
|
||||
disabled={disabled ?? loading}
|
||||
disabled={disabled || loading}
|
||||
{...restProps}
|
||||
>
|
||||
{icon ? <span>{icon}</span> : null}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { ReactNode } from 'react';
|
|||
import { createPortal } from 'react-dom';
|
||||
import { ITooltip, Tooltip as TooltipImpl } from 'react-tooltip';
|
||||
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
|
||||
export type { PlacesType } from 'react-tooltip';
|
||||
|
||||
|
@ -29,7 +29,7 @@ function Tooltip({
|
|||
style,
|
||||
...restProps
|
||||
}: TooltipProps) {
|
||||
const { darkMode } = useConceptOptions();
|
||||
const darkMode = usePreferencesStore(state => state.darkMode);
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { urls } from '@/app/urls';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useAuth } from '@/backend/auth/useAuth';
|
||||
import { useLogout } from '@/backend/auth/useLogout';
|
||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||
|
||||
import TextURL from '../ui/TextURL';
|
||||
|
||||
function ExpectedAnonymous() {
|
||||
const { user, logout } = useAuth();
|
||||
const { user } = useAuth();
|
||||
const { logout } = useLogout();
|
||||
const router = useConceptNavigation();
|
||||
|
||||
function logoutAndRedirect() {
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
'use client';
|
||||
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useAuth } from '@/backend/auth/useAuth';
|
||||
|
||||
import Loader from '../ui/Loader';
|
||||
import TextURL from '../ui/TextURL';
|
||||
|
||||
function RequireAuth({ children }: React.PropsWithChildren) {
|
||||
const { user, loading } = useAuth();
|
||||
const { user, isLoading } = useAuth();
|
||||
|
||||
if (loading) {
|
||||
if (isLoading) {
|
||||
return <Loader key='auth-loader' />;
|
||||
}
|
||||
if (user) {
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { createContext, useContext, useState } from 'react';
|
||||
|
||||
import { UserLevel } from '@/models/user';
|
||||
import { contextOutsideScope } from '@/utils/labels';
|
||||
|
||||
interface IAccessModeContext {
|
||||
accessLevel: UserLevel;
|
||||
setAccessLevel: React.Dispatch<React.SetStateAction<UserLevel>>;
|
||||
}
|
||||
|
||||
const AccessContext = createContext<IAccessModeContext | null>(null);
|
||||
export const useAccessMode = () => {
|
||||
const context = useContext(AccessContext);
|
||||
if (!context) {
|
||||
throw new Error(contextOutsideScope('useAccessMode', 'AccessModeState'));
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const AccessModeState = ({ children }: React.PropsWithChildren) => {
|
||||
const [accessLevel, setAccessLevel] = useState<UserLevel>(UserLevel.READER);
|
||||
return <AccessContext value={{ accessLevel, setAccessLevel }}>{children}</AccessContext>;
|
||||
};
|
|
@ -1,207 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { DataCallback } from '@/backend/apiTransport';
|
||||
import {
|
||||
getAuth,
|
||||
patchPassword,
|
||||
postLogin,
|
||||
postLogout,
|
||||
postRequestPasswordReset,
|
||||
postResetPassword,
|
||||
postSignup,
|
||||
postValidatePasswordToken
|
||||
} from '@/backend/users';
|
||||
import { type ErrorData } from '@/components/info/InfoError';
|
||||
import {
|
||||
ICurrentUser,
|
||||
IPasswordTokenData,
|
||||
IRequestPasswordData,
|
||||
IResetPasswordData,
|
||||
IUserInfo,
|
||||
IUserLoginData,
|
||||
IUserProfile,
|
||||
IUserSignupData,
|
||||
IUserUpdatePassword
|
||||
} from '@/models/user';
|
||||
import { contextOutsideScope } from '@/utils/labels';
|
||||
|
||||
import { useUsers } from './UsersContext';
|
||||
|
||||
interface IAuthContext {
|
||||
user: ICurrentUser | undefined;
|
||||
login: (data: IUserLoginData, callback?: DataCallback) => void;
|
||||
logout: (callback?: DataCallback) => void;
|
||||
signup: (data: IUserSignupData, callback?: DataCallback<IUserProfile>) => void;
|
||||
updatePassword: (data: IUserUpdatePassword, callback?: () => void) => void;
|
||||
requestPasswordReset: (data: IRequestPasswordData, callback?: () => void) => void;
|
||||
validateToken: (data: IPasswordTokenData, callback?: () => void) => void;
|
||||
resetPassword: (data: IResetPasswordData, callback?: () => void) => void;
|
||||
loading: boolean;
|
||||
error: ErrorData;
|
||||
setError: (error: ErrorData) => void;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<IAuthContext | null>(null);
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error(contextOutsideScope('useAuth', 'AuthState'));
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const AuthState = ({ children }: React.PropsWithChildren) => {
|
||||
const { users } = useUsers();
|
||||
const [user, setUser] = useState<ICurrentUser | undefined>(undefined);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<ErrorData>(undefined);
|
||||
|
||||
const reload = useCallback(
|
||||
(callback?: () => void) => {
|
||||
getAuth({
|
||||
onError: () => setUser(undefined),
|
||||
setLoading: setLoading,
|
||||
onSuccess: currentUser => {
|
||||
if (currentUser.id) {
|
||||
setUser(currentUser);
|
||||
} else {
|
||||
setUser(undefined);
|
||||
}
|
||||
callback?.();
|
||||
}
|
||||
});
|
||||
},
|
||||
[setUser]
|
||||
);
|
||||
|
||||
function login(data: IUserLoginData, callback?: DataCallback) {
|
||||
setError(undefined);
|
||||
postLogin({
|
||||
data: data,
|
||||
showError: false,
|
||||
setLoading: setLoading,
|
||||
onError: setError,
|
||||
onSuccess: newData =>
|
||||
reload(() => {
|
||||
callback?.(newData);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function logout(callback?: DataCallback) {
|
||||
setError(undefined);
|
||||
postLogout({
|
||||
showError: true,
|
||||
onSuccess: newData =>
|
||||
reload(() => {
|
||||
callback?.(newData);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function signup(data: IUserSignupData, callback?: DataCallback<IUserProfile>) {
|
||||
setError(undefined);
|
||||
postSignup({
|
||||
data: data,
|
||||
showError: true,
|
||||
setLoading: setLoading,
|
||||
onError: setError,
|
||||
onSuccess: newData =>
|
||||
reload(() => {
|
||||
users.push(newData as IUserInfo);
|
||||
callback?.(newData);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
const updatePassword = useCallback(
|
||||
(data: IUserUpdatePassword, callback?: () => void) => {
|
||||
setError(undefined);
|
||||
patchPassword({
|
||||
data: data,
|
||||
showError: true,
|
||||
setLoading: setLoading,
|
||||
onError: setError,
|
||||
onSuccess: () => reload(callback)
|
||||
});
|
||||
},
|
||||
[reload]
|
||||
);
|
||||
|
||||
const requestPasswordReset = useCallback(
|
||||
(data: IRequestPasswordData, callback?: () => void) => {
|
||||
setError(undefined);
|
||||
postRequestPasswordReset({
|
||||
data: data,
|
||||
showError: false,
|
||||
setLoading: setLoading,
|
||||
onError: setError,
|
||||
onSuccess: () =>
|
||||
reload(() => {
|
||||
callback?.();
|
||||
})
|
||||
});
|
||||
},
|
||||
[reload]
|
||||
);
|
||||
|
||||
const validateToken = useCallback(
|
||||
(data: IPasswordTokenData, callback?: () => void) => {
|
||||
setError(undefined);
|
||||
postValidatePasswordToken({
|
||||
data: data,
|
||||
showError: false,
|
||||
setLoading: setLoading,
|
||||
onError: setError,
|
||||
onSuccess: () =>
|
||||
reload(() => {
|
||||
callback?.();
|
||||
})
|
||||
});
|
||||
},
|
||||
[reload]
|
||||
);
|
||||
|
||||
const resetPassword = useCallback(
|
||||
(data: IResetPasswordData, callback?: () => void) => {
|
||||
setError(undefined);
|
||||
postResetPassword({
|
||||
data: data,
|
||||
showError: false,
|
||||
setLoading: setLoading,
|
||||
onError: setError,
|
||||
onSuccess: () =>
|
||||
reload(() => {
|
||||
callback?.();
|
||||
})
|
||||
});
|
||||
},
|
||||
[reload]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
reload();
|
||||
}, [reload]);
|
||||
|
||||
return (
|
||||
<AuthContext
|
||||
value={{
|
||||
user,
|
||||
login,
|
||||
logout,
|
||||
signup,
|
||||
loading,
|
||||
error,
|
||||
setError,
|
||||
updatePassword,
|
||||
requestPasswordReset,
|
||||
validateToken,
|
||||
resetPassword
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext>
|
||||
);
|
||||
};
|
|
@ -1,194 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
|
||||
import InfoConstituenta from '@/components/info/InfoConstituenta';
|
||||
import Loader from '@/components/ui/Loader';
|
||||
import Tooltip from '@/components/ui/Tooltip';
|
||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
||||
import { IConstituenta } from '@/models/rsform';
|
||||
import { globals, PARAMETER, storage } from '@/utils/constants';
|
||||
import { contextOutsideScope } from '@/utils/labels';
|
||||
|
||||
interface IOptionsContext {
|
||||
viewportHeight: string;
|
||||
mainHeight: string;
|
||||
|
||||
darkMode: boolean;
|
||||
toggleDarkMode: () => void;
|
||||
|
||||
adminMode: boolean;
|
||||
toggleAdminMode: () => void;
|
||||
|
||||
noNavigationAnimation: boolean;
|
||||
noNavigation: boolean;
|
||||
toggleNoNavigation: () => void;
|
||||
|
||||
noFooter: boolean;
|
||||
setNoFooter: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
|
||||
showScroll: boolean;
|
||||
setShowScroll: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
|
||||
showHelp: boolean;
|
||||
toggleShowHelp: () => void;
|
||||
|
||||
folderMode: boolean;
|
||||
setFolderMode: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
|
||||
location: string;
|
||||
setLocation: React.Dispatch<React.SetStateAction<string>>;
|
||||
|
||||
setHoverCst: (newValue: IConstituenta | undefined) => void;
|
||||
|
||||
calculateHeight: (offset: string, minimum?: string) => string;
|
||||
}
|
||||
|
||||
const OptionsContext = createContext<IOptionsContext | null>(null);
|
||||
export const useConceptOptions = () => {
|
||||
const context = useContext(OptionsContext);
|
||||
if (!context) {
|
||||
throw new Error(contextOutsideScope('useConceptTheme', 'ThemeState'));
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const OptionsState = ({ children }: React.PropsWithChildren) => {
|
||||
const [darkMode, setDarkMode] = useLocalStorage(storage.themeDark, false);
|
||||
const [adminMode, setAdminMode] = useLocalStorage(storage.optionsAdmin, false);
|
||||
const [showHelp, setShowHelp] = useLocalStorage(storage.optionsHelp, true);
|
||||
const [noNavigation, setNoNavigation] = useState(false);
|
||||
|
||||
const [folderMode, setFolderMode] = useLocalStorage<boolean>(storage.librarySearchFolderMode, true);
|
||||
const [location, setLocation] = useLocalStorage<string>(storage.librarySearchLocation, '');
|
||||
|
||||
const [noNavigationAnimation, setNoNavigationAnimation] = useState(false);
|
||||
const [noFooter, setNoFooter] = useState(false);
|
||||
const [showScroll, setShowScroll] = useState(false);
|
||||
|
||||
const [hoverCst, setHoverCst] = useState<IConstituenta | undefined>(undefined);
|
||||
|
||||
function setDarkClass(isDark: boolean) {
|
||||
const root = window.document.documentElement;
|
||||
if (isDark) {
|
||||
root.classList.add('dark');
|
||||
} else {
|
||||
root.classList.remove('dark');
|
||||
}
|
||||
root.setAttribute('data-color-scheme', !isDark ? 'light' : 'dark');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setDarkClass(darkMode);
|
||||
}, [darkMode]);
|
||||
|
||||
const toggleNoNavigation = useCallback(() => {
|
||||
if (noNavigation) {
|
||||
setNoNavigationAnimation(false);
|
||||
setNoNavigation(false);
|
||||
} else {
|
||||
setNoNavigationAnimation(true);
|
||||
setTimeout(() => setNoNavigation(true), PARAMETER.moveDuration);
|
||||
}
|
||||
}, [noNavigation]);
|
||||
|
||||
const calculateHeight = useCallback(
|
||||
(offset: string, minimum: string = '0px') => {
|
||||
if (noNavigation) {
|
||||
return `max(calc(100dvh - (${offset})), ${minimum})`;
|
||||
} else if (noFooter) {
|
||||
return `max(calc(100dvh - 3rem - (${offset})), ${minimum})`;
|
||||
} else {
|
||||
return `max(calc(100dvh - 6.75rem - (${offset})), ${minimum})`;
|
||||
}
|
||||
},
|
||||
[noNavigation, noFooter]
|
||||
);
|
||||
|
||||
const toggleDarkMode = useCallback(() => {
|
||||
if (!document.startViewTransition) {
|
||||
setDarkMode(prev => !prev);
|
||||
} else {
|
||||
const style = document.createElement('style');
|
||||
style.innerHTML = `
|
||||
* {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
document.startViewTransition(() => {
|
||||
flushSync(() => {
|
||||
setDarkMode(prev => !prev);
|
||||
});
|
||||
});
|
||||
|
||||
setTimeout(() => document.head.removeChild(style), PARAMETER.moveDuration);
|
||||
}
|
||||
}, [setDarkMode]);
|
||||
|
||||
const mainHeight = useMemo(() => {
|
||||
if (noNavigation) {
|
||||
return '100dvh';
|
||||
} else if (noFooter) {
|
||||
return 'calc(100dvh - 3rem)';
|
||||
} else {
|
||||
return 'calc(100dvh - 6.75rem)';
|
||||
}
|
||||
}, [noNavigation, noFooter]);
|
||||
|
||||
const viewportHeight = useMemo(() => {
|
||||
return !noNavigation ? 'calc(100dvh - 3rem)' : '100dvh';
|
||||
}, [noNavigation]);
|
||||
|
||||
return (
|
||||
<OptionsContext
|
||||
value={{
|
||||
darkMode,
|
||||
adminMode,
|
||||
noNavigationAnimation,
|
||||
noNavigation,
|
||||
noFooter,
|
||||
folderMode,
|
||||
setFolderMode,
|
||||
location,
|
||||
setLocation,
|
||||
showScroll,
|
||||
showHelp,
|
||||
toggleDarkMode: toggleDarkMode,
|
||||
toggleAdminMode: () => setAdminMode(prev => !prev),
|
||||
toggleNoNavigation: toggleNoNavigation,
|
||||
setNoFooter,
|
||||
setShowScroll,
|
||||
toggleShowHelp: () => setShowHelp(prev => !prev),
|
||||
viewportHeight,
|
||||
mainHeight,
|
||||
calculateHeight,
|
||||
setHoverCst
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<Tooltip
|
||||
float
|
||||
id={globals.tooltip}
|
||||
layer='z-topmost'
|
||||
place='right-start'
|
||||
className='mt-8 max-w-[20rem] break-words'
|
||||
/>
|
||||
<Tooltip
|
||||
float
|
||||
id={globals.value_tooltip}
|
||||
layer='z-topmost'
|
||||
className='max-w-[calc(min(40rem,100dvw-2rem))] text-justify'
|
||||
/>
|
||||
<Tooltip clickable id={globals.constituenta_tooltip} layer='z-modalTooltip' className='max-w-[30rem]'>
|
||||
{hoverCst ? <InfoConstituenta data={hoverCst} onClick={event => event.stopPropagation()} /> : <Loader />}
|
||||
</Tooltip>
|
||||
|
||||
{children}
|
||||
</>
|
||||
</OptionsContext>
|
||||
);
|
||||
};
|
|
@ -3,6 +3,7 @@
|
|||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { DataCallback } from '@/backend/apiTransport';
|
||||
import { useAuth } from '@/backend/auth/useAuth';
|
||||
import {
|
||||
deleteLibraryItem,
|
||||
getAdminLibrary,
|
||||
|
@ -21,11 +22,9 @@ import { matchLibraryItem, matchLibraryItemLocation } from '@/models/libraryAPI'
|
|||
import { ILibraryFilter } from '@/models/miscellaneous';
|
||||
import { IRSForm, IRSFormCloneData, IRSFormData } from '@/models/rsform';
|
||||
import { RSFormLoader } from '@/models/RSFormLoader';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { contextOutsideScope } from '@/utils/labels';
|
||||
|
||||
import { useAuth } from './AuthContext';
|
||||
import { useConceptOptions } from './ConceptOptionsContext';
|
||||
|
||||
interface ILibraryContext {
|
||||
items: ILibraryItem[];
|
||||
templates: ILibraryItem[];
|
||||
|
@ -62,8 +61,8 @@ export const useLibrary = (): ILibraryContext => {
|
|||
};
|
||||
|
||||
export const LibraryState = ({ children }: React.PropsWithChildren) => {
|
||||
const { user, loading: userLoading } = useAuth();
|
||||
const { adminMode } = useConceptOptions();
|
||||
const { user, isLoading: userLoading } = useAuth();
|
||||
const adminMode = usePreferencesStore(state => state.adminMode);
|
||||
|
||||
const [items, setItems] = useState<ILibraryItem[]>([]);
|
||||
const [templates, setTemplates] = useState<ILibraryItem[]>([]);
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { DataCallback } from '@/backend/apiTransport';
|
||||
import { useAuth } from '@/backend/auth/useAuth';
|
||||
import {
|
||||
patchLibraryItem,
|
||||
patchSetAccessPolicy,
|
||||
|
@ -38,7 +39,6 @@ import {
|
|||
import { UserID } from '@/models/user';
|
||||
import { contextOutsideScope } from '@/utils/labels';
|
||||
|
||||
import { useAuth } from './AuthContext';
|
||||
import { useGlobalOss } from './GlobalOssContext';
|
||||
import { useLibrary } from './LibraryContext';
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import { createContext, useCallback, useContext, useMemo, useState } from 'react';
|
||||
|
||||
import { DataCallback } from '@/backend/apiTransport';
|
||||
import { useAuth } from '@/backend/auth/useAuth';
|
||||
import {
|
||||
patchLibraryItem,
|
||||
patchSetAccessPolicy,
|
||||
|
@ -50,7 +51,6 @@ import {
|
|||
import { UserID } from '@/models/user';
|
||||
import { contextOutsideScope } from '@/utils/labels';
|
||||
|
||||
import { useAuth } from './AuthContext';
|
||||
import { useGlobalOss } from './GlobalOssContext';
|
||||
import { useLibrary } from './LibraryContext';
|
||||
|
||||
|
|
|
@ -1,83 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { DataCallback } from '@/backend/apiTransport';
|
||||
import { getProfile, patchProfile } from '@/backend/users';
|
||||
import { ErrorData } from '@/components/info/InfoError';
|
||||
import { IUserProfile, IUserUpdateData } from '@/models/user';
|
||||
import { contextOutsideScope } from '@/utils/labels';
|
||||
|
||||
import { useUsers } from './UsersContext';
|
||||
|
||||
interface IUserProfileContext {
|
||||
user: IUserProfile | undefined;
|
||||
loading: boolean;
|
||||
processing: boolean;
|
||||
error: ErrorData;
|
||||
errorProcessing: ErrorData;
|
||||
setError: (error: ErrorData) => void;
|
||||
updateUser: (data: IUserUpdateData, callback?: DataCallback<IUserProfile>) => void;
|
||||
}
|
||||
|
||||
const ProfileContext = createContext<IUserProfileContext | null>(null);
|
||||
|
||||
export const useUserProfile = () => {
|
||||
const context = useContext(ProfileContext);
|
||||
if (!context) {
|
||||
throw new Error(contextOutsideScope('useUserProfile', 'UserProfileState'));
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const UserProfileState = ({ children }: React.PropsWithChildren) => {
|
||||
const { users } = useUsers();
|
||||
const [user, setUser] = useState<IUserProfile | undefined>(undefined);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [error, setError] = useState<ErrorData>(undefined);
|
||||
const [errorProcessing, setErrorProcessing] = useState<ErrorData>(undefined);
|
||||
|
||||
const reload = useCallback(() => {
|
||||
setError(undefined);
|
||||
setUser(undefined);
|
||||
getProfile({
|
||||
showError: true,
|
||||
setLoading: setLoading,
|
||||
onError: setError,
|
||||
onSuccess: newData => setUser(newData)
|
||||
});
|
||||
}, [setUser]);
|
||||
|
||||
const updateUser = useCallback(
|
||||
(data: IUserUpdateData, callback?: DataCallback<IUserProfile>) => {
|
||||
setErrorProcessing(undefined);
|
||||
patchProfile({
|
||||
data: data,
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
onError: setErrorProcessing,
|
||||
onSuccess: newData => {
|
||||
setUser(newData);
|
||||
const libraryUser = users.find(item => item.id === user?.id);
|
||||
if (libraryUser) {
|
||||
libraryUser.first_name = newData.first_name;
|
||||
libraryUser.last_name = newData.last_name;
|
||||
}
|
||||
callback?.(newData);
|
||||
}
|
||||
});
|
||||
},
|
||||
[setUser, users, user?.id]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
reload();
|
||||
}, [reload]);
|
||||
|
||||
return (
|
||||
<ProfileContext value={{ user, updateUser, error, loading, setError, processing, errorProcessing }}>
|
||||
{children}
|
||||
</ProfileContext>
|
||||
);
|
||||
};
|
|
@ -1,94 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { getActiveUsers } from '@/backend/users';
|
||||
import { IUserInfo } from '@/models/user';
|
||||
import { contextOutsideScope } from '@/utils/labels';
|
||||
|
||||
interface IUsersContext {
|
||||
users: IUserInfo[];
|
||||
reload: (callback?: () => void) => void;
|
||||
getUserLabel: (userID: number | null) => string;
|
||||
}
|
||||
|
||||
const UsersContext = createContext<IUsersContext | null>(null);
|
||||
export const useUsers = (): IUsersContext => {
|
||||
const context = useContext(UsersContext);
|
||||
if (context === null) {
|
||||
throw new Error(contextOutsideScope('useUsers', 'UsersState'));
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const UsersState = ({ children }: React.PropsWithChildren) => {
|
||||
const [users, setUsers] = useState<IUserInfo[]>([]);
|
||||
|
||||
function getUserLabel(userID: number | null) {
|
||||
const user = users.find(({ id }) => id === userID);
|
||||
if (!user) {
|
||||
return userID ? userID.toString() : 'Отсутствует';
|
||||
}
|
||||
const hasFirstName = user.first_name !== '';
|
||||
const hasLastName = user.last_name !== '';
|
||||
if (hasFirstName || hasLastName) {
|
||||
if (!hasLastName) {
|
||||
return user.first_name;
|
||||
}
|
||||
if (!hasFirstName) {
|
||||
return user.last_name;
|
||||
}
|
||||
return user.last_name + ' ' + user.first_name;
|
||||
}
|
||||
return `Аноним ${userID}`;
|
||||
}
|
||||
|
||||
const reload = useCallback(
|
||||
(callback?: () => void) => {
|
||||
getActiveUsers({
|
||||
showError: true,
|
||||
onError: () => setUsers([]),
|
||||
onSuccess: newData => {
|
||||
newData.sort((a, b) => {
|
||||
if (a.last_name === '') {
|
||||
if (b.last_name === '') {
|
||||
return a.id - b.id;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
} else if (b.last_name === '') {
|
||||
if (a.last_name === '') {
|
||||
return a.id - b.id;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
} else if (a.last_name !== b.last_name) {
|
||||
return a.last_name.localeCompare(b.last_name);
|
||||
} else {
|
||||
return a.first_name.localeCompare(b.first_name);
|
||||
}
|
||||
});
|
||||
setUsers(newData);
|
||||
callback?.();
|
||||
}
|
||||
});
|
||||
},
|
||||
[setUsers]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
reload();
|
||||
}, [reload]);
|
||||
|
||||
return (
|
||||
<UsersContext
|
||||
value={{
|
||||
users,
|
||||
reload,
|
||||
getUserLabel
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</UsersContext>
|
||||
);
|
||||
};
|
|
@ -7,19 +7,21 @@ import { IconReset } from '@/components/Icons';
|
|||
import PickSchema from '@/components/select/PickSchema';
|
||||
import Label from '@/components/ui/Label';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import { useLibrary } from '@/context/LibraryContext';
|
||||
import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library';
|
||||
import { IOperation, IOperationSchema } from '@/models/oss';
|
||||
import { IOperation, IOperationSchema, OperationID } from '@/models/oss';
|
||||
import { sortItemsForOSS } from '@/models/ossAPI';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
|
||||
interface DlgChangeInputSchemaProps extends Pick<ModalProps, 'hideWindow'> {
|
||||
export interface DlgChangeInputSchemaProps {
|
||||
oss: IOperationSchema;
|
||||
target: IOperation;
|
||||
onSubmit: (newSchema: LibraryItemID | undefined) => void;
|
||||
onSubmit: (target: OperationID, newSchema: LibraryItemID | undefined) => void;
|
||||
}
|
||||
|
||||
function DlgChangeInputSchema({ oss, hideWindow, target, onSubmit }: DlgChangeInputSchemaProps) {
|
||||
function DlgChangeInputSchema() {
|
||||
const { oss, target, onSubmit } = useDialogsStore(state => state.props as DlgChangeInputSchemaProps);
|
||||
const [selected, setSelected] = useState<LibraryItemID | undefined>(target.result ?? undefined);
|
||||
const library = useLibrary();
|
||||
const sortedItems = sortItemsForOSS(oss, library.items);
|
||||
|
@ -38,9 +40,8 @@ function DlgChangeInputSchema({ oss, hideWindow, target, onSubmit }: DlgChangeIn
|
|||
overflowVisible
|
||||
header='Выбор концептуальной схемы'
|
||||
submitText='Подтвердить выбор'
|
||||
hideWindow={hideWindow}
|
||||
canSubmit={isValid}
|
||||
onSubmit={() => onSubmit(selected)}
|
||||
onSubmit={() => onSubmit(target.id, selected)}
|
||||
className={clsx('w-[35rem]', 'pb-3 px-6 cc-column')}
|
||||
>
|
||||
<div className='flex justify-between gap-3 items-center'>
|
||||
|
|
|
@ -3,23 +3,25 @@
|
|||
import clsx from 'clsx';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useAuth } from '@/backend/auth/useAuth';
|
||||
import SelectLocationContext from '@/components/select/SelectLocationContext';
|
||||
import SelectLocationHead from '@/components/select/SelectLocationHead';
|
||||
import Label from '@/components/ui/Label';
|
||||
import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import TextArea from '@/components/ui/TextArea';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useLibrary } from '@/context/LibraryContext';
|
||||
import { LocationHead } from '@/models/library';
|
||||
import { combineLocation, validateLocation } from '@/models/libraryAPI';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { limits } from '@/utils/constants';
|
||||
|
||||
interface DlgChangeLocationProps extends Pick<ModalProps, 'hideWindow'> {
|
||||
export interface DlgChangeLocationProps {
|
||||
initial: string;
|
||||
onChangeLocation: (newLocation: string) => void;
|
||||
}
|
||||
|
||||
function DlgChangeLocation({ hideWindow, initial, onChangeLocation }: DlgChangeLocationProps) {
|
||||
function DlgChangeLocation() {
|
||||
const { initial, onChangeLocation } = useDialogsStore(state => state.props as DlgChangeLocationProps);
|
||||
const { user } = useAuth();
|
||||
const [head, setHead] = useState<LocationHead>(initial.substring(0, 2) as LocationHead);
|
||||
const [body, setBody] = useState<string>(initial.substring(3));
|
||||
|
@ -40,7 +42,6 @@ function DlgChangeLocation({ hideWindow, initial, onChangeLocation }: DlgChangeL
|
|||
header='Изменение расположения'
|
||||
submitText='Переместить'
|
||||
submitInvalidTooltip={`Допустимы буквы, цифры, подчерк, пробел и "!". Сегмент пути не может начинаться и заканчиваться пробелом. Общая длина (с корнем) не должна превышать ${limits.location_len}`}
|
||||
hideWindow={hideWindow}
|
||||
canSubmit={isValid}
|
||||
onSubmit={() => onChangeLocation(location)}
|
||||
className={clsx('w-[35rem]', 'pb-3 px-6 flex gap-3 h-[9rem]')}
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useState } from 'react';
|
|||
import { toast } from 'react-toastify';
|
||||
|
||||
import { urls } from '@/app/urls';
|
||||
import { useAuth } from '@/backend/auth/useAuth';
|
||||
import { VisibilityIcon } from '@/components/DomainIcons';
|
||||
import SelectAccessPolicy from '@/components/select/SelectAccessPolicy';
|
||||
import SelectLocationContext from '@/components/select/SelectLocationContext';
|
||||
|
@ -12,27 +13,31 @@ import SelectLocationHead from '@/components/select/SelectLocationHead';
|
|||
import Checkbox from '@/components/ui/Checkbox';
|
||||
import Label from '@/components/ui/Label';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import TextArea from '@/components/ui/TextArea';
|
||||
import TextInput from '@/components/ui/TextInput';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useLibrary } from '@/context/LibraryContext';
|
||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||
import { AccessPolicy, ILibraryItem, LocationHead } from '@/models/library';
|
||||
import { cloneTitle, combineLocation, validateLocation } from '@/models/libraryAPI';
|
||||
import { ConstituentaID, IRSFormCloneData } from '@/models/rsform';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { information } from '@/utils/labels';
|
||||
|
||||
interface DlgCloneLibraryItemProps extends Pick<ModalProps, 'hideWindow'> {
|
||||
export interface DlgCloneLibraryItemProps {
|
||||
base: ILibraryItem;
|
||||
initialLocation: string;
|
||||
selected: ConstituentaID[];
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
function DlgCloneLibraryItem({ hideWindow, base, initialLocation, selected, totalCount }: DlgCloneLibraryItemProps) {
|
||||
function DlgCloneLibraryItem() {
|
||||
const { base, initialLocation, selected, totalCount } = useDialogsStore(
|
||||
state => state.props as DlgCloneLibraryItemProps
|
||||
);
|
||||
const router = useConceptNavigation();
|
||||
const { user } = useAuth();
|
||||
|
||||
const [title, setTitle] = useState(cloneTitle(base));
|
||||
const [alias, setAlias] = useState(base.alias);
|
||||
const [comment, setComment] = useState(base.comment);
|
||||
|
@ -77,7 +82,6 @@ function DlgCloneLibraryItem({ hideWindow, base, initialLocation, selected, tota
|
|||
return (
|
||||
<Modal
|
||||
header='Создание копии концептуальной схемы'
|
||||
hideWindow={hideWindow}
|
||||
canSubmit={canSubmit}
|
||||
submitText='Создать'
|
||||
onSubmit={handleSubmit}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export { default } from './DlgConstituentaTemplate';
|
|
@ -2,20 +2,22 @@
|
|||
|
||||
import { useState } from 'react';
|
||||
|
||||
import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import usePartialUpdate from '@/hooks/usePartialUpdate';
|
||||
import { CstType, ICstCreateData, IRSForm } from '@/models/rsform';
|
||||
import { generateAlias } from '@/models/rsformAPI';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
|
||||
import FormCreateCst from './FormCreateCst';
|
||||
|
||||
interface DlgCreateCstProps extends Pick<ModalProps, 'hideWindow'> {
|
||||
export interface DlgCreateCstProps {
|
||||
initial?: ICstCreateData;
|
||||
schema: IRSForm;
|
||||
onCreate: (data: ICstCreateData) => void;
|
||||
}
|
||||
|
||||
function DlgCreateCst({ hideWindow, initial, schema, onCreate }: DlgCreateCstProps) {
|
||||
function DlgCreateCst() {
|
||||
const { initial, schema, onCreate } = useDialogsStore(state => state.props as DlgCreateCstProps);
|
||||
const [validated, setValidated] = useState(false);
|
||||
const [cstData, updateCstData] = usePartialUpdate(
|
||||
initial || {
|
||||
|
@ -35,7 +37,6 @@ function DlgCreateCst({ hideWindow, initial, schema, onCreate }: DlgCreateCstPro
|
|||
return (
|
||||
<Modal
|
||||
header='Создание конституенты'
|
||||
hideWindow={hideWindow}
|
||||
canSubmit={validated}
|
||||
onSubmit={handleSubmit}
|
||||
submitText='Создать'
|
||||
|
|
|
@ -10,13 +10,13 @@ import { useLibrary } from '@/context/LibraryContext';
|
|||
import { LibraryItemID } from '@/models/library';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
import { IOperationCreateData, IOperationSchema, OperationID, OperationType } from '@/models/oss';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { describeOperationType, labelOperationType } from '@/utils/labels';
|
||||
|
||||
import TabInputOperation from './TabInputOperation';
|
||||
import TabSynthesisOperation from './TabSynthesisOperation';
|
||||
|
||||
interface DlgCreateOperationProps {
|
||||
hideWindow: () => void;
|
||||
export interface DlgCreateOperationProps {
|
||||
oss: IOperationSchema;
|
||||
onCreate: (data: IOperationCreateData) => void;
|
||||
initialInputs: OperationID[];
|
||||
|
@ -27,7 +27,8 @@ export enum TabID {
|
|||
SYNTHESIS = 1
|
||||
}
|
||||
|
||||
function DlgCreateOperation({ hideWindow, oss, onCreate, initialInputs }: DlgCreateOperationProps) {
|
||||
function DlgCreateOperation() {
|
||||
const { oss, onCreate, initialInputs } = useDialogsStore(state => state.props as DlgCreateOperationProps);
|
||||
const library = useLibrary();
|
||||
const [activeTab, setActiveTab] = useState(initialInputs.length > 0 ? TabID.SYNTHESIS : TabID.INPUT);
|
||||
|
||||
|
@ -98,7 +99,6 @@ function DlgCreateOperation({ hideWindow, oss, onCreate, initialInputs }: DlgCre
|
|||
<Modal
|
||||
header='Создание операции'
|
||||
submitText='Создать'
|
||||
hideWindow={hideWindow}
|
||||
canSubmit={isValid}
|
||||
onSubmit={handleSubmit}
|
||||
className='w-[40rem] px-6 h-[32rem]'
|
||||
|
|
|
@ -4,21 +4,23 @@ import clsx from 'clsx';
|
|||
import { useState } from 'react';
|
||||
|
||||
import Checkbox from '@/components/ui/Checkbox';
|
||||
import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import TextArea from '@/components/ui/TextArea';
|
||||
import TextInput from '@/components/ui/TextInput';
|
||||
import { IVersionCreateData, IVersionInfo } from '@/models/library';
|
||||
import { nextVersion } from '@/models/libraryAPI';
|
||||
import { ConstituentaID } from '@/models/rsform';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
|
||||
interface DlgCreateVersionProps extends Pick<ModalProps, 'hideWindow'> {
|
||||
export interface DlgCreateVersionProps {
|
||||
versions: IVersionInfo[];
|
||||
onCreate: (data: IVersionCreateData) => void;
|
||||
selected: ConstituentaID[];
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
function DlgCreateVersion({ hideWindow, versions, selected, totalCount, onCreate }: DlgCreateVersionProps) {
|
||||
function DlgCreateVersion() {
|
||||
const { versions, selected, totalCount, onCreate } = useDialogsStore(state => state.props as DlgCreateVersionProps);
|
||||
const [version, setVersion] = useState(versions.length > 0 ? nextVersion(versions[0].version) : '1.0.0');
|
||||
const [description, setDescription] = useState('');
|
||||
const [onlySelected, setOnlySelected] = useState(false);
|
||||
|
@ -39,7 +41,6 @@ function DlgCreateVersion({ hideWindow, versions, selected, totalCount, onCreate
|
|||
return (
|
||||
<Modal
|
||||
header='Создание версии'
|
||||
hideWindow={hideWindow}
|
||||
canSubmit={canSubmit}
|
||||
onSubmit={handleSubmit}
|
||||
submitText='Создать'
|
||||
|
|
|
@ -4,7 +4,7 @@ import clsx from 'clsx';
|
|||
import { useEffect, useState } from 'react';
|
||||
import { TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
|
||||
import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import TabLabel from '@/components/ui/TabLabel';
|
||||
import { useLibrary } from '@/context/LibraryContext';
|
||||
import usePartialUpdate from '@/hooks/usePartialUpdate';
|
||||
|
@ -12,13 +12,14 @@ import { HelpTopic } from '@/models/miscellaneous';
|
|||
import { CstType, ICstCreateData, IRSForm } from '@/models/rsform';
|
||||
import { generateAlias, validateNewAlias } from '@/models/rsformAPI';
|
||||
import { inferTemplatedType, substituteTemplateArgs } from '@/models/rslangAPI';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { prompts } from '@/utils/labels';
|
||||
|
||||
import FormCreateCst from '../DlgCreateCst/FormCreateCst';
|
||||
import TabArguments, { IArgumentsState } from './TabArguments';
|
||||
import TabTemplate, { ITemplateState } from './TabTemplate';
|
||||
|
||||
interface DlgConstituentaTemplateProps extends Pick<ModalProps, 'hideWindow'> {
|
||||
export interface DlgCstTemplateProps {
|
||||
schema: IRSForm;
|
||||
onCreate: (data: ICstCreateData) => void;
|
||||
insertAfter?: number;
|
||||
|
@ -30,7 +31,8 @@ export enum TabID {
|
|||
CONSTITUENTA = 2
|
||||
}
|
||||
|
||||
function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }: DlgConstituentaTemplateProps) {
|
||||
function DlgCstTemplate() {
|
||||
const { schema, onCreate, insertAfter } = useDialogsStore(state => state.props as DlgCstTemplateProps);
|
||||
const { retrieveTemplate } = useLibrary();
|
||||
const [activeTab, setActiveTab] = useState(TabID.TEMPLATE);
|
||||
|
||||
|
@ -128,7 +130,6 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
|
|||
header='Создание конституенты из шаблона'
|
||||
submitText='Создать'
|
||||
className='w-[43rem] h-[35rem] px-6'
|
||||
hideWindow={hideWindow}
|
||||
canSubmit={validated}
|
||||
beforeSubmit={handlePrompt}
|
||||
onSubmit={handleSubmit}
|
||||
|
@ -164,4 +165,4 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
|
|||
);
|
||||
}
|
||||
|
||||
export default DlgConstituentaTemplate;
|
||||
export default DlgCstTemplate;
|
1
rsconcept/frontend/src/dialogs/DlgCstTemplate/index.tsx
Normal file
1
rsconcept/frontend/src/dialogs/DlgCstTemplate/index.tsx
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from './DlgCstTemplate';
|
|
@ -4,19 +4,22 @@ import clsx from 'clsx';
|
|||
import { useState } from 'react';
|
||||
|
||||
import Checkbox from '@/components/ui/Checkbox';
|
||||
import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import { ConstituentaID, IRSForm } from '@/models/rsform';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { prefixes } from '@/utils/constants';
|
||||
|
||||
import ListConstituents from './ListConstituents';
|
||||
|
||||
interface DlgDeleteCstProps extends Pick<ModalProps, 'hideWindow'> {
|
||||
export interface DlgDeleteCstProps {
|
||||
schema: IRSForm;
|
||||
selected: ConstituentaID[];
|
||||
onDelete: (items: ConstituentaID[]) => void;
|
||||
}
|
||||
|
||||
function DlgDeleteCst({ hideWindow, selected, schema, onDelete }: DlgDeleteCstProps) {
|
||||
function DlgDeleteCst() {
|
||||
const { selected, schema, onDelete } = useDialogsStore(state => state.props as DlgDeleteCstProps);
|
||||
const hideDialog = useDialogsStore(state => state.hideDialog);
|
||||
const [expandOut, setExpandOut] = useState(false);
|
||||
const expansion: ConstituentaID[] = schema.graph.expandAllOutputs(selected);
|
||||
const hasInherited = selected.some(
|
||||
|
@ -25,7 +28,7 @@ function DlgDeleteCst({ hideWindow, selected, schema, onDelete }: DlgDeleteCstPr
|
|||
);
|
||||
|
||||
function handleSubmit() {
|
||||
hideWindow();
|
||||
hideDialog();
|
||||
if (expandOut) {
|
||||
onDelete(selected.concat(expansion));
|
||||
} else {
|
||||
|
@ -38,7 +41,6 @@ function DlgDeleteCst({ hideWindow, selected, schema, onDelete }: DlgDeleteCstPr
|
|||
canSubmit
|
||||
header='Удаление конституент'
|
||||
submitText={expandOut ? 'Удалить с зависимыми' : 'Удалить'}
|
||||
hideWindow={hideWindow}
|
||||
onSubmit={handleSubmit}
|
||||
className={clsx('cc-column', 'max-w-[60vw] min-w-[30rem]', 'px-6')}
|
||||
>
|
||||
|
|
|
@ -4,22 +4,24 @@ import clsx from 'clsx';
|
|||
import { useState } from 'react';
|
||||
|
||||
import Checkbox from '@/components/ui/Checkbox';
|
||||
import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import TextInput from '@/components/ui/TextInput';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
import { IOperation } from '@/models/oss';
|
||||
import { IOperation, OperationID } from '@/models/oss';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
|
||||
interface DlgDeleteOperationProps extends Pick<ModalProps, 'hideWindow'> {
|
||||
export interface DlgDeleteOperationProps {
|
||||
target: IOperation;
|
||||
onSubmit: (keepConstituents: boolean, deleteSchema: boolean) => void;
|
||||
onSubmit: (targetID: OperationID, keepConstituents: boolean, deleteSchema: boolean) => void;
|
||||
}
|
||||
|
||||
function DlgDeleteOperation({ hideWindow, target, onSubmit }: DlgDeleteOperationProps) {
|
||||
function DlgDeleteOperation() {
|
||||
const { target, onSubmit } = useDialogsStore(state => state.props as DlgDeleteOperationProps);
|
||||
const [keepConstituents, setKeepConstituents] = useState(false);
|
||||
const [deleteSchema, setDeleteSchema] = useState(false);
|
||||
|
||||
function handleSubmit() {
|
||||
onSubmit(keepConstituents, deleteSchema);
|
||||
onSubmit(target.id, keepConstituents, deleteSchema);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -27,7 +29,6 @@ function DlgDeleteOperation({ hideWindow, target, onSubmit }: DlgDeleteOperation
|
|||
overflowVisible
|
||||
header='Удаление операции'
|
||||
submitText='Подтвердить удаление'
|
||||
hideWindow={hideWindow}
|
||||
canSubmit={true}
|
||||
onSubmit={handleSubmit}
|
||||
className={clsx('w-[35rem]', 'pb-3 px-6 cc-column', 'select-none')}
|
||||
|
|
|
@ -3,26 +3,26 @@
|
|||
import clsx from 'clsx';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useUsers } from '@/backend/users/useUsers';
|
||||
import { IconRemove } from '@/components/Icons';
|
||||
import SelectUser from '@/components/select/SelectUser';
|
||||
import Label from '@/components/ui/Label';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import { useUsers } from '@/context/UsersContext';
|
||||
import { UserID } from '@/models/user';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
|
||||
import TableUsers from './TableUsers';
|
||||
|
||||
interface DlgEditEditorsProps {
|
||||
export interface DlgEditEditorsProps {
|
||||
editors: UserID[];
|
||||
setEditors: (newValue: UserID[]) => void;
|
||||
hideWindow: () => void;
|
||||
}
|
||||
|
||||
function DlgEditEditors({ hideWindow, editors, setEditors }: DlgEditEditorsProps) {
|
||||
function DlgEditEditors() {
|
||||
const { editors, setEditors } = useDialogsStore(state => state.props as DlgEditEditorsProps);
|
||||
const [selected, setSelected] = useState<UserID[]>(editors);
|
||||
const { users } = useUsers();
|
||||
const filtered = users.filter(user => !selected.includes(user.id));
|
||||
|
||||
function handleSubmit() {
|
||||
setEditors(selected);
|
||||
|
@ -41,7 +41,6 @@ function DlgEditEditors({ hideWindow, editors, setEditors }: DlgEditEditorsProps
|
|||
canSubmit
|
||||
header='Список редакторов'
|
||||
submitText='Сохранить список'
|
||||
hideWindow={hideWindow}
|
||||
className='flex flex-col w-[35rem] px-6 gap-3 pb-6'
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
|
@ -61,7 +60,12 @@ function DlgEditEditors({ hideWindow, editors, setEditors }: DlgEditEditorsProps
|
|||
|
||||
<div className='flex items-center gap-3'>
|
||||
<Label text='Добавить' />
|
||||
<SelectUser items={filtered} value={undefined} onSelectValue={onAddEditor} className='w-[25rem]' />
|
||||
<SelectUser
|
||||
filter={id => !selected.includes(id)}
|
||||
value={undefined}
|
||||
onSelectValue={onAddEditor}
|
||||
className='w-[25rem]'
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
@ -19,13 +19,13 @@ import {
|
|||
} from '@/models/oss';
|
||||
import { SubstitutionValidator } from '@/models/ossAPI';
|
||||
import { ConstituentaID } from '@/models/rsform';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
|
||||
import TabArguments from './TabArguments';
|
||||
import TabOperation from './TabOperation';
|
||||
import TabSynthesis from './TabSynthesis';
|
||||
|
||||
interface DlgEditOperationProps {
|
||||
hideWindow: () => void;
|
||||
export interface DlgEditOperationProps {
|
||||
oss: IOperationSchema;
|
||||
target: IOperation;
|
||||
onSubmit: (data: IOperationUpdateData) => void;
|
||||
|
@ -37,7 +37,8 @@ export enum TabID {
|
|||
SUBSTITUTION = 2
|
||||
}
|
||||
|
||||
function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperationProps) {
|
||||
function DlgEditOperation() {
|
||||
const { oss, target, onSubmit } = useDialogsStore(state => state.props as DlgEditOperationProps);
|
||||
const [activeTab, setActiveTab] = useState(TabID.CARD);
|
||||
|
||||
const [alias, setAlias] = useState(target.alias);
|
||||
|
@ -142,7 +143,6 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
|
|||
<Modal
|
||||
header='Редактирование операции'
|
||||
submitText='Сохранить'
|
||||
hideWindow={hideWindow}
|
||||
canSubmit={canSubmit}
|
||||
onSubmit={handleSubmit}
|
||||
className='w-[40rem] px-6 h-[32rem]'
|
||||
|
|
|
@ -9,6 +9,7 @@ import TabLabel from '@/components/ui/TabLabel';
|
|||
import { ReferenceType } from '@/models/language';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
import { IRSForm } from '@/models/rsform';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { labelReferenceType } from '@/utils/labels';
|
||||
|
||||
import TabEntityReference from './TabEntityReference';
|
||||
|
@ -22,8 +23,7 @@ export interface IReferenceInputState {
|
|||
basePosition: number;
|
||||
}
|
||||
|
||||
interface DlgEditReferenceProps {
|
||||
hideWindow: () => void;
|
||||
export interface DlgEditReferenceProps {
|
||||
schema: IRSForm;
|
||||
initial: IReferenceInputState;
|
||||
onSave: (newRef: string) => void;
|
||||
|
@ -34,7 +34,8 @@ export enum TabID {
|
|||
SYNTACTIC = 1
|
||||
}
|
||||
|
||||
function DlgEditReference({ hideWindow, schema, initial, onSave }: DlgEditReferenceProps) {
|
||||
function DlgEditReference() {
|
||||
const { schema, initial, onSave } = useDialogsStore(state => state.props as DlgEditReferenceProps);
|
||||
const [activeTab, setActiveTab] = useState(initial.type === ReferenceType.ENTITY ? TabID.ENTITY : TabID.SYNTACTIC);
|
||||
const [reference, setReference] = useState('');
|
||||
const [isValid, setIsValid] = useState(false);
|
||||
|
@ -43,7 +44,6 @@ function DlgEditReference({ hideWindow, schema, initial, onSave }: DlgEditRefere
|
|||
<Modal
|
||||
header='Редактирование ссылки'
|
||||
submitText='Сохранить ссылку'
|
||||
hideWindow={hideWindow}
|
||||
canSubmit={isValid}
|
||||
onSubmit={() => onSave(reference)}
|
||||
className='w-[40rem] px-6 h-[32rem]'
|
||||
|
|
|
@ -7,21 +7,21 @@ import MiniButton from '@/components/ui/MiniButton';
|
|||
import Modal from '@/components/ui/Modal';
|
||||
import TextArea from '@/components/ui/TextArea';
|
||||
import TextInput from '@/components/ui/TextInput';
|
||||
import { useRSForm } from '@/context/RSFormContext';
|
||||
import { IVersionData, IVersionInfo, VersionID } from '@/models/library';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
|
||||
import TableVersions from './TableVersions';
|
||||
|
||||
interface DlgEditVersionsProps {
|
||||
hideWindow: () => void;
|
||||
export interface DlgEditVersionsProps {
|
||||
versions: IVersionInfo[];
|
||||
onDelete: (versionID: VersionID) => void;
|
||||
onUpdate: (versionID: VersionID, data: IVersionData) => void;
|
||||
}
|
||||
|
||||
function DlgEditVersions({ hideWindow, versions, onDelete, onUpdate }: DlgEditVersionsProps) {
|
||||
const { processing } = useRSForm();
|
||||
function DlgEditVersions() {
|
||||
const { versions, onDelete, onUpdate } = useDialogsStore(state => state.props as DlgEditVersionsProps);
|
||||
const [selected, setSelected] = useState<IVersionInfo | undefined>(undefined);
|
||||
const processing = false; // TODO: fix processing hook and versions update
|
||||
|
||||
const [version, setVersion] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
|
@ -54,12 +54,7 @@ function DlgEditVersions({ hideWindow, versions, onDelete, onUpdate }: DlgEditVe
|
|||
}, [selected]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
readonly
|
||||
header='Редактирование версий'
|
||||
hideWindow={hideWindow}
|
||||
className='flex flex-col w-[40rem] px-6 gap-3 pb-6'
|
||||
>
|
||||
<Modal readonly header='Редактирование версий' className='flex flex-col w-[40rem] px-6 gap-3 pb-6'>
|
||||
<TableVersions
|
||||
processing={processing}
|
||||
items={versions}
|
||||
|
|
|
@ -14,18 +14,19 @@ import { Grammeme, ITextRequest, IWordForm, IWordFormPlain } from '@/models/lang
|
|||
import { parseGrammemes, wordFormEquals } from '@/models/languageAPI';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
import { IConstituenta, TermForm } from '@/models/rsform';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { prompts } from '@/utils/labels';
|
||||
import { IGrammemeOption, SelectorGrammemes, SelectorGrammemesList } from '@/utils/selectors';
|
||||
|
||||
import TableWordForms from './TableWordForms';
|
||||
|
||||
interface DlgEditWordFormsProps {
|
||||
hideWindow: () => void;
|
||||
export interface DlgEditWordFormsProps {
|
||||
target: IConstituenta;
|
||||
onSave: (data: TermForm[]) => void;
|
||||
}
|
||||
|
||||
function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps) {
|
||||
function DlgEditWordForms() {
|
||||
const { target, onSave } = useDialogsStore(state => state.props as DlgEditWordFormsProps);
|
||||
const textProcessor = useConceptText();
|
||||
|
||||
const [term, setTerm] = useState('');
|
||||
|
@ -123,7 +124,6 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
|
|||
<Modal
|
||||
canSubmit
|
||||
header='Редактирование словоформ'
|
||||
hideWindow={hideWindow}
|
||||
submitText='Сохранить'
|
||||
onSubmit={handleSubmit}
|
||||
className='flex flex-col w-[40rem] px-6'
|
||||
|
|
|
@ -1,24 +1,25 @@
|
|||
'use client';
|
||||
|
||||
import Checkbox from '@/components/ui/Checkbox';
|
||||
import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import usePartialUpdate from '@/hooks/usePartialUpdate';
|
||||
import { GraphFilterParams } from '@/models/miscellaneous';
|
||||
import { CstType } from '@/models/rsform';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { labelCstType } from '@/utils/labels';
|
||||
|
||||
interface DlgGraphParamsProps extends Pick<ModalProps, 'hideWindow'> {
|
||||
export interface DlgGraphParamsProps {
|
||||
initial: GraphFilterParams;
|
||||
onConfirm: (params: GraphFilterParams) => void;
|
||||
}
|
||||
|
||||
function DlgGraphParams({ hideWindow, initial, onConfirm }: DlgGraphParamsProps) {
|
||||
function DlgGraphParams() {
|
||||
const { initial, onConfirm } = useDialogsStore(state => state.props as DlgGraphParamsProps);
|
||||
const [params, updateParams] = usePartialUpdate(initial);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
canSubmit
|
||||
hideWindow={hideWindow}
|
||||
header='Настройки графа термов'
|
||||
onSubmit={() => onConfirm(params)}
|
||||
submitText='Применить'
|
||||
|
|
|
@ -4,18 +4,19 @@ import clsx from 'clsx';
|
|||
import { useEffect, useState } from 'react';
|
||||
import { TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
|
||||
import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import TabLabel from '@/components/ui/TabLabel';
|
||||
import useRSFormDetails from '@/hooks/useRSFormDetails';
|
||||
import { LibraryItemID } from '@/models/library';
|
||||
import { ICstSubstitute } from '@/models/oss';
|
||||
import { ConstituentaID, IInlineSynthesisData, IRSForm } from '@/models/rsform';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
|
||||
import TabConstituents from './TabConstituents';
|
||||
import TabSchema from './TabSchema';
|
||||
import TabSubstitutions from './TabSubstitutions';
|
||||
|
||||
interface DlgInlineSynthesisProps extends Pick<ModalProps, 'hideWindow'> {
|
||||
export interface DlgInlineSynthesisProps {
|
||||
receiver: IRSForm;
|
||||
onInlineSynthesis: (data: IInlineSynthesisData) => void;
|
||||
}
|
||||
|
@ -26,7 +27,8 @@ export enum TabID {
|
|||
SUBSTITUTIONS = 2
|
||||
}
|
||||
|
||||
function DlgInlineSynthesis({ hideWindow, receiver, onInlineSynthesis }: DlgInlineSynthesisProps) {
|
||||
function DlgInlineSynthesis() {
|
||||
const { receiver, onInlineSynthesis } = useDialogsStore(state => state.props as DlgInlineSynthesisProps);
|
||||
const [activeTab, setActiveTab] = useState(TabID.SCHEMA);
|
||||
|
||||
const [donorID, setDonorID] = useState<LibraryItemID | undefined>(undefined);
|
||||
|
@ -60,7 +62,6 @@ function DlgInlineSynthesis({ hideWindow, receiver, onInlineSynthesis }: DlgInli
|
|||
header='Импорт концептуальной схем'
|
||||
submitText='Добавить конституенты'
|
||||
className='w-[40rem] h-[33rem] px-6'
|
||||
hideWindow={hideWindow}
|
||||
canSubmit={validated}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
|
|
|
@ -7,7 +7,7 @@ import { RelocateUpIcon } from '@/components/DomainIcons';
|
|||
import PickMultiConstituenta from '@/components/select/PickMultiConstituenta';
|
||||
import SelectLibraryItem from '@/components/select/SelectLibraryItem';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import DataLoader from '@/components/wrap/DataLoader';
|
||||
import { useLibrary } from '@/context/LibraryContext';
|
||||
import useRSFormDetails from '@/hooks/useRSFormDetails';
|
||||
|
@ -16,15 +16,17 @@ import { HelpTopic } from '@/models/miscellaneous';
|
|||
import { ICstRelocateData, IOperation, IOperationSchema } from '@/models/oss';
|
||||
import { getRelocateCandidates } from '@/models/ossAPI';
|
||||
import { ConstituentaID } from '@/models/rsform';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { prefixes } from '@/utils/constants';
|
||||
|
||||
interface DlgRelocateConstituentsProps extends Pick<ModalProps, 'hideWindow'> {
|
||||
export interface DlgRelocateConstituentsProps {
|
||||
oss: IOperationSchema;
|
||||
initialTarget?: IOperation;
|
||||
onSubmit: (data: ICstRelocateData) => void;
|
||||
}
|
||||
|
||||
function DlgRelocateConstituents({ oss, hideWindow, initialTarget, onSubmit }: DlgRelocateConstituentsProps) {
|
||||
function DlgRelocateConstituents() {
|
||||
const { oss, initialTarget, onSubmit } = useDialogsStore(state => state.props as DlgRelocateConstituentsProps);
|
||||
const library = useLibrary();
|
||||
|
||||
const [directionUp, setDirectionUp] = useState(true);
|
||||
|
@ -88,7 +90,6 @@ function DlgRelocateConstituents({ oss, hideWindow, initialTarget, onSubmit }: D
|
|||
<Modal
|
||||
header='Перенос конституент'
|
||||
submitText='Переместить'
|
||||
hideWindow={hideWindow}
|
||||
canSubmit={isValid}
|
||||
onSubmit={handleSubmit}
|
||||
className={clsx('w-[40rem] h-[33rem]', 'py-3 px-6')}
|
||||
|
|
|
@ -3,25 +3,26 @@
|
|||
import clsx from 'clsx';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import SelectSingle from '@/components/ui/SelectSingle';
|
||||
import TextInput from '@/components/ui/TextInput';
|
||||
import { useRSForm } from '@/context/RSFormContext';
|
||||
import usePartialUpdate from '@/hooks/usePartialUpdate';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
import { CstType, ICstRenameData } from '@/models/rsform';
|
||||
import { CstType, ICstRenameData, IRSForm } from '@/models/rsform';
|
||||
import { generateAlias, validateNewAlias } from '@/models/rsformAPI';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { labelCstType } from '@/utils/labels';
|
||||
import { SelectorCstType } from '@/utils/selectors';
|
||||
|
||||
interface DlgRenameCstProps extends Pick<ModalProps, 'hideWindow'> {
|
||||
export interface DlgRenameCstProps {
|
||||
schema: IRSForm;
|
||||
initial: ICstRenameData;
|
||||
allowChangeType: boolean;
|
||||
onRename: (data: ICstRenameData) => void;
|
||||
}
|
||||
|
||||
function DlgRenameCst({ hideWindow, initial, allowChangeType, onRename }: DlgRenameCstProps) {
|
||||
const { schema } = useRSForm();
|
||||
function DlgRenameCst() {
|
||||
const { schema, initial, allowChangeType, onRename } = useDialogsStore(state => state.props as DlgRenameCstProps);
|
||||
const [validated, setValidated] = useState(false);
|
||||
const [cstData, updateData] = usePartialUpdate(initial);
|
||||
|
||||
|
@ -42,7 +43,6 @@ function DlgRenameCst({ hideWindow, initial, allowChangeType, onRename }: DlgRen
|
|||
header='Переименование конституенты'
|
||||
submitText='Переименовать'
|
||||
submitInvalidTooltip='Введите незанятое имя, соответствующее типу'
|
||||
hideWindow={hideWindow}
|
||||
canSubmit={validated}
|
||||
onSubmit={() => onRename(cstData)}
|
||||
className={clsx('w-[30rem]', 'py-6 pr-3 pl-6 flex gap-3 justify-center items-center ')}
|
||||
|
|
|
@ -3,19 +3,21 @@
|
|||
import { useState } from 'react';
|
||||
import { ReactFlowProvider } from 'reactflow';
|
||||
|
||||
import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
import { SyntaxTree } from '@/models/rslang';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
|
||||
import ASTFlow from './ASTFlow';
|
||||
|
||||
interface DlgShowASTProps extends Pick<ModalProps, 'hideWindow'> {
|
||||
export interface DlgShowASTProps {
|
||||
syntaxTree: SyntaxTree;
|
||||
expression: string;
|
||||
}
|
||||
|
||||
function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
|
||||
function DlgShowAST() {
|
||||
const { syntaxTree, expression } = useDialogsStore(state => state.props as DlgShowASTProps);
|
||||
const [hoverID, setHoverID] = useState<number | undefined>(undefined);
|
||||
const hoverNode = syntaxTree.find(node => node.uid === hoverID);
|
||||
|
||||
|
@ -24,7 +26,6 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
|
|||
return (
|
||||
<Modal
|
||||
readonly
|
||||
hideWindow={hideWindow}
|
||||
className='flex flex-col justify-stretch w-[calc(100dvw-3rem)] h-[calc(100dvh-6rem)]'
|
||||
helpTopic={HelpTopic.UI_FORMULA_TREE}
|
||||
>
|
||||
|
|
|
@ -3,19 +3,17 @@
|
|||
import clsx from 'clsx';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
|
||||
import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
|
||||
interface DlgShowQRProps extends Pick<ModalProps, 'hideWindow'> {
|
||||
export interface DlgShowQRProps {
|
||||
target: string;
|
||||
}
|
||||
|
||||
function DlgShowQR({ hideWindow, target }: DlgShowQRProps) {
|
||||
function DlgShowQR() {
|
||||
const { target } = useDialogsStore(state => state.props as DlgShowQRProps);
|
||||
return (
|
||||
<Modal
|
||||
readonly
|
||||
hideWindow={hideWindow}
|
||||
className={clsx('w-[30rem]', 'py-12 pr-3 pl-6 flex gap-3 justify-center items-center')}
|
||||
>
|
||||
<Modal readonly className={clsx('w-[30rem]', 'py-12 pr-3 pl-6 flex gap-3 justify-center items-center')}>
|
||||
<div className='bg-[#ffffff] p-4 border'>
|
||||
<QRCodeSVG value={target} size={256} />
|
||||
</div>
|
||||
|
|
|
@ -3,19 +3,22 @@
|
|||
import { toast } from 'react-toastify';
|
||||
import { ReactFlowProvider } from 'reactflow';
|
||||
|
||||
import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
import { ITypeInfo } from '@/models/rslang';
|
||||
import { TMGraph } from '@/models/TMGraph';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { errors } from '@/utils/labels';
|
||||
|
||||
import MGraphFlow from './MGraphFlow';
|
||||
|
||||
interface DlgShowTypeGraphProps extends Pick<ModalProps, 'hideWindow'> {
|
||||
export interface DlgShowTypeGraphProps {
|
||||
items: ITypeInfo[];
|
||||
}
|
||||
|
||||
function DlgShowTypeGraph({ hideWindow, items }: DlgShowTypeGraphProps) {
|
||||
function DlgShowTypeGraph() {
|
||||
const { items } = useDialogsStore(state => state.props as DlgShowTypeGraphProps);
|
||||
const hideDialog = useDialogsStore(state => state.hideDialog);
|
||||
const graph = (() => {
|
||||
const result = new TMGraph();
|
||||
items.forEach(item => result.addConstituenta(item.alias, item.result, item.args));
|
||||
|
@ -24,7 +27,7 @@ function DlgShowTypeGraph({ hideWindow, items }: DlgShowTypeGraphProps) {
|
|||
|
||||
if (graph.nodes.length === 0) {
|
||||
toast.error(errors.typeStructureFailed);
|
||||
hideWindow();
|
||||
hideDialog();
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -32,7 +35,6 @@ function DlgShowTypeGraph({ hideWindow, items }: DlgShowTypeGraphProps) {
|
|||
<Modal
|
||||
header='Граф ступеней'
|
||||
readonly
|
||||
hideWindow={hideWindow}
|
||||
className='flex flex-col justify-stretch w-[calc(100dvw-3rem)] h-[calc(100dvh-6rem)]'
|
||||
helpTopic={HelpTopic.UI_TYPE_GRAPH}
|
||||
>
|
||||
|
|
|
@ -4,18 +4,20 @@ import clsx from 'clsx';
|
|||
import { useState } from 'react';
|
||||
|
||||
import PickSubstitutions from '@/components/select/PickSubstitutions';
|
||||
import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
import { ICstSubstitute, ICstSubstituteData } from '@/models/oss';
|
||||
import { IRSForm } from '@/models/rsform';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { prefixes } from '@/utils/constants';
|
||||
|
||||
interface DlgSubstituteCstProps extends Pick<ModalProps, 'hideWindow'> {
|
||||
export interface DlgSubstituteCstProps {
|
||||
schema: IRSForm;
|
||||
onSubstitute: (data: ICstSubstituteData) => void;
|
||||
}
|
||||
|
||||
function DlgSubstituteCst({ hideWindow, onSubstitute, schema }: DlgSubstituteCstProps) {
|
||||
function DlgSubstituteCst() {
|
||||
const { onSubstitute, schema } = useDialogsStore(state => state.props as DlgSubstituteCstProps);
|
||||
const [substitutions, setSubstitutions] = useState<ICstSubstitute[]>([]);
|
||||
const canSubmit = substitutions.length > 0;
|
||||
|
||||
|
@ -31,7 +33,6 @@ function DlgSubstituteCst({ hideWindow, onSubstitute, schema }: DlgSubstituteCst
|
|||
header='Отождествление'
|
||||
submitText='Отождествить'
|
||||
submitInvalidTooltip='Выберите две различные конституенты'
|
||||
hideWindow={hideWindow}
|
||||
canSubmit={canSubmit}
|
||||
onSubmit={handleSubmit}
|
||||
className={clsx('w-[40rem]', 'px-6 pb-3')}
|
||||
|
|
|
@ -6,16 +6,16 @@ import { toast } from 'react-toastify';
|
|||
import Checkbox from '@/components/ui/Checkbox';
|
||||
import FileInput from '@/components/ui/FileInput';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import { useRSForm } from '@/context/RSFormContext';
|
||||
import { IRSFormUploadData } from '@/models/rsform';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { EXTEOR_TRS_FILE } from '@/utils/constants';
|
||||
|
||||
interface DlgUploadRSFormProps {
|
||||
hideWindow: () => void;
|
||||
export interface DlgUploadRSFormProps {
|
||||
upload: (data: IRSFormUploadData, callback: () => void) => void;
|
||||
}
|
||||
|
||||
function DlgUploadRSForm({ hideWindow }: DlgUploadRSFormProps) {
|
||||
const { upload } = useRSForm();
|
||||
function DlgUploadRSForm() {
|
||||
const { upload } = useDialogsStore(state => state.props as DlgUploadRSFormProps);
|
||||
const [loadMetadata, setLoadMetadata] = useState(false);
|
||||
const [file, setFile] = useState<File | undefined>();
|
||||
|
||||
|
@ -42,7 +42,6 @@ function DlgUploadRSForm({ hideWindow }: DlgUploadRSFormProps) {
|
|||
return (
|
||||
<Modal
|
||||
header='Импорт схемы из Экстеора'
|
||||
hideWindow={hideWindow}
|
||||
canSubmit={!!file}
|
||||
onSubmit={handleSubmit}
|
||||
submitText='Загрузить'
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { storage } from '@/utils/constants';
|
||||
|
||||
function useLocalStorage<ValueType>(key: string, defaultValue: ValueType | (() => ValueType)) {
|
||||
const prefixedKey = `${storage.PREFIX}${key}`;
|
||||
const [value, setValue] = useState<ValueType>(() => {
|
||||
const loadedJson = localStorage.getItem(prefixedKey);
|
||||
if (loadedJson != null) {
|
||||
return JSON.parse(loadedJson) as ValueType;
|
||||
} else if (typeof defaultValue === 'function') {
|
||||
return (defaultValue as () => ValueType)();
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (value === undefined) {
|
||||
localStorage.removeItem(prefixedKey);
|
||||
} else {
|
||||
localStorage.setItem(prefixedKey, JSON.stringify(value));
|
||||
}
|
||||
}, [prefixedKey, value]);
|
||||
|
||||
return [value, setValue] as [ValueType, typeof setValue];
|
||||
}
|
||||
|
||||
export default useLocalStorage;
|
|
@ -2,15 +2,15 @@
|
|||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { useAuth } from '@/backend/auth/useAuth';
|
||||
import { getOssDetails } from '@/backend/oss';
|
||||
import { type ErrorData } from '@/components/info/InfoError';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useLibrary } from '@/context/LibraryContext';
|
||||
import { IOperationSchema, IOperationSchemaData } from '@/models/oss';
|
||||
import { OssLoader } from '@/models/OssLoader';
|
||||
|
||||
function useOssDetails({ target }: { target?: string }) {
|
||||
const { loading: userLoading } = useAuth();
|
||||
const { isLoading: userLoading } = useAuth();
|
||||
const library = useLibrary();
|
||||
const [schema, setInner] = useState<IOperationSchema | undefined>(undefined);
|
||||
const [loading, setLoading] = useState(target != undefined);
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { useAuth } from '@/backend/auth/useAuth';
|
||||
import { getRSFormDetails } from '@/backend/rsforms';
|
||||
import { type ErrorData } from '@/components/info/InfoError';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { IRSForm, IRSFormData } from '@/models/rsform';
|
||||
import { RSFormLoader } from '@/models/RSFormLoader';
|
||||
|
||||
function useRSFormDetails({ target, version }: { target?: string; version?: string }) {
|
||||
const { loading: userLoading } = useAuth();
|
||||
const { isLoading: userLoading } = useAuth();
|
||||
const [schema, setInnerSchema] = useState<IRSForm | undefined>(undefined);
|
||||
const [loading, setLoading] = useState(target != undefined);
|
||||
const [error, setError] = useState<ErrorData>(undefined);
|
||||
|
|
|
@ -224,3 +224,32 @@ export interface Position2D {
|
|||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents global dialog.
|
||||
*/
|
||||
export enum DialogType {
|
||||
CONSTITUENTA_TEMPLATE = 1,
|
||||
CREATE_CONSTITUENTA,
|
||||
CREATE_OPERATION,
|
||||
DELETE_CONSTITUENTA,
|
||||
EDIT_EDITORS,
|
||||
EDIT_OPERATION,
|
||||
EDIT_REFERENCE,
|
||||
EDIT_VERSIONS,
|
||||
EDIT_WORD_FORMS,
|
||||
INLINE_SYNTHESIS,
|
||||
SHOW_AST,
|
||||
SHOW_TYPE_GRAPH,
|
||||
CHANGE_INPUT_SCHEMA,
|
||||
CHANGE_LOCATION,
|
||||
CLONE_LIBRARY_ITEM,
|
||||
CREATE_VERSION,
|
||||
DELETE_OPERATION,
|
||||
GRAPH_PARAMETERS,
|
||||
RELOCATE_CONSTITUENTS,
|
||||
RENAME_CONSTITUENTA,
|
||||
SHOW_QR_CODE,
|
||||
SUBSTITUTE_CONSTITUENTS,
|
||||
UPLOAD_RSFORM
|
||||
}
|
||||
|
|
|
@ -26,35 +26,9 @@ export interface IUser {
|
|||
* Represents CurrentUser information.
|
||||
*/
|
||||
export interface ICurrentUser extends Pick<IUser, 'id' | 'username' | 'is_staff'> {
|
||||
subscriptions: LibraryItemID[];
|
||||
editor: LibraryItemID[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents login data, used to authenticate users.
|
||||
*/
|
||||
export interface IUserLoginData extends Pick<IUser, 'username'> {
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents password reset data.
|
||||
*/
|
||||
export interface IResetPasswordData {
|
||||
password: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents password token data.
|
||||
*/
|
||||
export interface IPasswordTokenData extends Pick<IResetPasswordData, 'token'> {}
|
||||
|
||||
/**
|
||||
* Represents password reset request data.
|
||||
*/
|
||||
export interface IRequestPasswordData extends Pick<IUser, 'email'> {}
|
||||
|
||||
/**
|
||||
* Represents signup data, used to create new users.
|
||||
*/
|
||||
|
@ -63,11 +37,6 @@ export interface IUserSignupData extends Omit<IUser, 'is_staff' | 'id'> {
|
|||
password2: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents user data, intended to update user profile in persistent storage.
|
||||
*/
|
||||
export interface IUserUpdateData extends Omit<IUser, 'is_staff' | 'id'> {}
|
||||
|
||||
/**
|
||||
* Represents user profile for viewing and editing {@link IUser}.
|
||||
*/
|
||||
|
@ -78,14 +47,6 @@ export interface IUserProfile extends Omit<IUser, 'is_staff'> {}
|
|||
*/
|
||||
export interface IUserInfo extends Omit<IUserProfile, 'email' | 'username'> {}
|
||||
|
||||
/**
|
||||
* Represents data needed to update password for current user.
|
||||
*/
|
||||
export interface IUserUpdatePassword {
|
||||
old_password: string;
|
||||
new_password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents target {@link User}.
|
||||
*/
|
||||
|
@ -103,7 +64,7 @@ export interface ITargetUsers {
|
|||
/**
|
||||
* Represents user access mode.
|
||||
*/
|
||||
export enum UserLevel {
|
||||
export enum UserRole {
|
||||
READER = 0,
|
||||
EDITOR,
|
||||
OWNER,
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
|||
import { toast } from 'react-toastify';
|
||||
|
||||
import { urls } from '@/app/urls';
|
||||
import { useAuth } from '@/backend/auth/useAuth';
|
||||
import { VisibilityIcon } from '@/components/DomainIcons';
|
||||
import { IconDownload } from '@/components/Icons';
|
||||
import InfoError from '@/components/info/InfoError';
|
||||
|
@ -19,22 +20,23 @@ import Overlay from '@/components/ui/Overlay';
|
|||
import SubmitButton from '@/components/ui/SubmitButton';
|
||||
import TextArea from '@/components/ui/TextArea';
|
||||
import TextInput from '@/components/ui/TextInput';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { useLibrary } from '@/context/LibraryContext';
|
||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
|
||||
import { ILibraryCreateData } from '@/models/library';
|
||||
import { combineLocation, validateLocation } from '@/models/libraryAPI';
|
||||
import { useLibrarySearchStore } from '@/stores/librarySearch';
|
||||
import { EXTEOR_TRS_FILE } from '@/utils/constants';
|
||||
import { information } from '@/utils/labels';
|
||||
|
||||
function FormCreateItem() {
|
||||
const router = useConceptNavigation();
|
||||
const options = useConceptOptions();
|
||||
const { user } = useAuth();
|
||||
const { createItem, processingError, setProcessingError, processing, folders } = useLibrary();
|
||||
|
||||
const searchLocation = useLibrarySearchStore(state => state.location);
|
||||
const setSearchLocation = useLibrarySearchStore(state => state.setLocation);
|
||||
|
||||
const [itemType, setItemType] = useState(LibraryItemType.RSFORM);
|
||||
const [title, setTitle] = useState('');
|
||||
const [alias, setAlias] = useState('');
|
||||
|
@ -81,7 +83,7 @@ function FormCreateItem() {
|
|||
file: file,
|
||||
fileName: file?.name
|
||||
};
|
||||
options.setLocation(location);
|
||||
setSearchLocation(location);
|
||||
createItem(data, newItem => {
|
||||
toast.success(information.newLibraryItem);
|
||||
if (itemType == LibraryItemType.RSFORM) {
|
||||
|
@ -108,11 +110,11 @@ function FormCreateItem() {
|
|||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!options.location) {
|
||||
if (!searchLocation) {
|
||||
return;
|
||||
}
|
||||
handleSelectLocation(options.location);
|
||||
}, [options.location, handleSelectLocation]);
|
||||
handleSelectLocation(searchLocation);
|
||||
}, [searchLocation, handleSelectLocation]);
|
||||
|
||||
useEffect(() => {
|
||||
if (itemType !== LibraryItemType.RSFORM) {
|
||||
|
|
|
@ -3,18 +3,18 @@
|
|||
import { useEffect } from 'react';
|
||||
import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch';
|
||||
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { useAppLayoutStore, useFitHeight } from '@/stores/appLayout';
|
||||
import { resources } from '@/utils/constants';
|
||||
|
||||
function DatabaseSchemaPage() {
|
||||
const { calculateHeight, setNoFooter } = useConceptOptions();
|
||||
const hideFooter = useAppLayoutStore(state => state.hideFooter);
|
||||
|
||||
const panelHeight = calculateHeight('0px');
|
||||
const panelHeight = useFitHeight('0px');
|
||||
|
||||
useEffect(() => {
|
||||
setNoFooter(true);
|
||||
return () => setNoFooter(false);
|
||||
}, [setNoFooter]);
|
||||
hideFooter(true);
|
||||
return () => hideFooter(false);
|
||||
}, [hideFooter]);
|
||||
|
||||
return (
|
||||
<div className='cc-fade-in flex justify-center overflow-hidden' style={{ maxHeight: panelHeight }}>
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import { useEffect } from 'react';
|
||||
|
||||
import { urls } from '@/app/urls';
|
||||
import { useAuth } from '@/backend/auth/useAuth';
|
||||
import Loader from '@/components/ui/Loader';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
||||
function HomePage() {
|
||||
const router = useConceptNavigation();
|
||||
const { user, loading } = useAuth();
|
||||
const { user, isLoading } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
if (!isLoading) {
|
||||
if (!user) {
|
||||
setTimeout(() => {
|
||||
router.replace(urls.manuals);
|
||||
|
@ -22,7 +22,7 @@ function HomePage() {
|
|||
}, PARAMETER.refreshTimeout);
|
||||
}
|
||||
}
|
||||
}, [router, user, loading]);
|
||||
}, [router, user, isLoading]);
|
||||
|
||||
return <Loader />;
|
||||
}
|
||||
|
|
|
@ -1,24 +1,19 @@
|
|||
'use client';
|
||||
|
||||
import fileDownload from 'js-file-download';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { IconCSV } from '@/components/Icons';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
import DataLoader from '@/components/wrap/DataLoader';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { useLibrary } from '@/context/LibraryContext';
|
||||
import DlgChangeLocation from '@/dialogs/DlgChangeLocation';
|
||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
||||
import { ILibraryItem, IRenameLocationData, LocationHead } from '@/models/library';
|
||||
import { ILibraryFilter } from '@/models/miscellaneous';
|
||||
import { UserID } from '@/models/user';
|
||||
import { storage } from '@/utils/constants';
|
||||
import { IRenameLocationData } from '@/models/library';
|
||||
import { useAppLayoutStore } from '@/stores/appLayout';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { useLibraryFilter, useLibrarySearchStore } from '@/stores/librarySearch';
|
||||
import { information } from '@/utils/labels';
|
||||
import { convertToCSV, toggleTristateFlag } from '@/utils/utils';
|
||||
import { convertToCSV } from '@/utils/utils';
|
||||
|
||||
import TableLibraryItems from './TableLibraryItems';
|
||||
import ToolbarSearch from './ToolbarSearch';
|
||||
|
@ -26,91 +21,29 @@ import ViewSideLocation from './ViewSideLocation';
|
|||
|
||||
function LibraryPage() {
|
||||
const library = useLibrary();
|
||||
const { user } = useAuth();
|
||||
const [items, setItems] = useState<ILibraryItem[]>([]);
|
||||
const options = useConceptOptions();
|
||||
const noNavigation = useAppLayoutStore(state => state.noNavigation);
|
||||
|
||||
const [query, setQuery] = useState('');
|
||||
const [path, setPath] = useState('');
|
||||
const folderMode = useLibrarySearchStore(state => state.folderMode);
|
||||
const location = useLibrarySearchStore(state => state.location);
|
||||
const setLocation = useLibrarySearchStore(state => state.setLocation);
|
||||
|
||||
const [head, setHead] = useLocalStorage<LocationHead | undefined>(storage.librarySearchHead, undefined);
|
||||
const [subfolders, setSubfolders] = useLocalStorage<boolean>(storage.librarySearchSubfolders, false);
|
||||
const [isVisible, setIsVisible] = useLocalStorage<boolean | undefined>(storage.librarySearchVisible, true);
|
||||
const [isOwned, setIsOwned] = useLocalStorage<boolean | undefined>(storage.librarySearchOwned, undefined);
|
||||
const [isEditor, setIsEditor] = useLocalStorage<boolean | undefined>(storage.librarySearchEditor, undefined);
|
||||
const [filterUser, setFilterUser] = useLocalStorage<UserID | undefined>(storage.librarySearchUser, undefined);
|
||||
const [showRenameLocation, setShowRenameLocation] = useState(false);
|
||||
const filter = useLibraryFilter();
|
||||
const items = library.applyFilter(filter);
|
||||
|
||||
const filter: ILibraryFilter = useMemo(
|
||||
() => ({
|
||||
head: head,
|
||||
path: path,
|
||||
query: query,
|
||||
isEditor: user ? isEditor : undefined,
|
||||
isOwned: user ? isOwned : undefined,
|
||||
isVisible: user ? isVisible : true,
|
||||
folderMode: options.folderMode,
|
||||
subfolders: subfolders,
|
||||
location: options.location,
|
||||
filterUser: filterUser
|
||||
}),
|
||||
[
|
||||
head,
|
||||
path,
|
||||
query,
|
||||
isEditor,
|
||||
isOwned,
|
||||
isVisible,
|
||||
user,
|
||||
options.folderMode,
|
||||
options.location,
|
||||
subfolders,
|
||||
filterUser
|
||||
]
|
||||
);
|
||||
const showChangeLocation = useDialogsStore(state => state.showChangeLocation);
|
||||
|
||||
const hasCustomFilter =
|
||||
!!filter.path ||
|
||||
!!filter.query ||
|
||||
filter.head !== undefined ||
|
||||
filter.isEditor !== undefined ||
|
||||
filter.isOwned !== undefined ||
|
||||
filter.isVisible !== true ||
|
||||
filter.filterUser !== undefined ||
|
||||
!!filter.location;
|
||||
function handleRenameLocation(newLocation: string) {
|
||||
const data: IRenameLocationData = {
|
||||
target: location,
|
||||
new_location: newLocation
|
||||
};
|
||||
library.renameLocation(data, () => {
|
||||
setLocation(newLocation);
|
||||
toast.success(information.locationRenamed);
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setItems(library.applyFilter(filter));
|
||||
}, [library, library.items.length, filter]);
|
||||
|
||||
const toggleFolderMode = () => options.setFolderMode(prev => !prev);
|
||||
|
||||
const resetFilter = useCallback(() => {
|
||||
setQuery('');
|
||||
setPath('');
|
||||
setHead(undefined);
|
||||
setIsVisible(true);
|
||||
setIsOwned(undefined);
|
||||
setIsEditor(undefined);
|
||||
setFilterUser(undefined);
|
||||
options.setLocation('');
|
||||
}, [setHead, setIsVisible, setIsOwned, setIsEditor, setFilterUser, options]);
|
||||
|
||||
const handleRenameLocation = useCallback(
|
||||
(newLocation: string) => {
|
||||
const data: IRenameLocationData = {
|
||||
target: options.location,
|
||||
new_location: newLocation
|
||||
};
|
||||
library.renameLocation(data, () => {
|
||||
options.setLocation(newLocation);
|
||||
toast.success(information.locationRenamed);
|
||||
});
|
||||
},
|
||||
[options, library]
|
||||
);
|
||||
|
||||
const handleDownloadCSV = useCallback(() => {
|
||||
function handleDownloadCSV() {
|
||||
if (items.length === 0) {
|
||||
toast.error(information.noDataToExport);
|
||||
return;
|
||||
|
@ -121,19 +54,12 @@ function LibraryPage() {
|
|||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, [items]);
|
||||
}
|
||||
|
||||
return (
|
||||
<DataLoader isLoading={library.loading} error={library.loadingError} hasNoData={library.items.length === 0}>
|
||||
{showRenameLocation ? (
|
||||
<DlgChangeLocation
|
||||
initial={options.location}
|
||||
onChangeLocation={handleRenameLocation}
|
||||
hideWindow={() => setShowRenameLocation(false)}
|
||||
/>
|
||||
) : null}
|
||||
<Overlay
|
||||
position={options.noNavigation ? 'top-[0.25rem] right-[3rem]' : 'top-[0.25rem] right-0'}
|
||||
position={noNavigation ? 'top-[0.25rem] right-[3rem]' : 'top-[0.25rem] right-0'}
|
||||
layer='z-tooltip'
|
||||
className='cc-animate-position'
|
||||
>
|
||||
|
@ -143,47 +69,16 @@ function LibraryPage() {
|
|||
onClick={handleDownloadCSV}
|
||||
/>
|
||||
</Overlay>
|
||||
<ToolbarSearch
|
||||
total={library.items.length ?? 0}
|
||||
filtered={items.length}
|
||||
hasCustomFilter={hasCustomFilter}
|
||||
query={query}
|
||||
onChangeQuery={setQuery}
|
||||
path={path}
|
||||
onChangePath={setPath}
|
||||
head={head}
|
||||
onChangeHead={setHead}
|
||||
isVisible={isVisible}
|
||||
isOwned={isOwned}
|
||||
toggleOwned={() => setIsOwned(prev => toggleTristateFlag(prev))}
|
||||
toggleVisible={() => setIsVisible(prev => toggleTristateFlag(prev))}
|
||||
isEditor={isEditor}
|
||||
toggleEditor={() => setIsEditor(prev => toggleTristateFlag(prev))}
|
||||
filterUser={filterUser}
|
||||
onChangeFilterUser={setFilterUser}
|
||||
resetFilter={resetFilter}
|
||||
folderMode={options.folderMode}
|
||||
toggleFolderMode={toggleFolderMode}
|
||||
/>
|
||||
<ToolbarSearch total={library.items.length ?? 0} filtered={items.length} />
|
||||
|
||||
<div className='cc-fade-in flex'>
|
||||
<ViewSideLocation
|
||||
isVisible={options.folderMode}
|
||||
activeLocation={options.location}
|
||||
onChangeActiveLocation={options.setLocation}
|
||||
subfolders={subfolders}
|
||||
isVisible={folderMode}
|
||||
folderTree={library.folders}
|
||||
toggleFolderMode={toggleFolderMode}
|
||||
toggleSubfolders={() => setSubfolders(prev => !prev)}
|
||||
onRenameLocation={() => setShowRenameLocation(true)}
|
||||
onRenameLocation={() => showChangeLocation({ initial: location, onChangeLocation: handleRenameLocation })}
|
||||
/>
|
||||
|
||||
<TableLibraryItems
|
||||
resetQuery={resetFilter}
|
||||
items={items}
|
||||
folderMode={options.folderMode}
|
||||
toggleFolderMode={toggleFolderMode}
|
||||
/>
|
||||
<TableLibraryItems items={items} />
|
||||
</div>
|
||||
</DataLoader>
|
||||
);
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useLayoutEffect, useState } from 'react';
|
|||
import { useIntl } from 'react-intl';
|
||||
|
||||
import { urls } from '@/app/urls';
|
||||
import { useLabelUser } from '@/backend/users/useLabelUser';
|
||||
import { IconFolderTree } from '@/components/Icons';
|
||||
import BadgeLocation from '@/components/info/BadgeLocation';
|
||||
import { CProps } from '@/components/props';
|
||||
|
@ -12,30 +13,31 @@ import DataTable, { createColumnHelper, IConditionalStyle, VisibilityState } fro
|
|||
import FlexColumn from '@/components/ui/FlexColumn';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import TextURL from '@/components/ui/TextURL';
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||
import { useUsers } from '@/context/UsersContext';
|
||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
||||
import useWindowSize from '@/hooks/useWindowSize';
|
||||
import { ILibraryItem, LibraryItemType } from '@/models/library';
|
||||
import { useFitHeight } from '@/stores/appLayout';
|
||||
import { useLibrarySearchStore } from '@/stores/librarySearch';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { APP_COLORS } from '@/styling/color';
|
||||
import { storage } from '@/utils/constants';
|
||||
|
||||
interface TableLibraryItemsProps {
|
||||
items: ILibraryItem[];
|
||||
resetQuery: () => void;
|
||||
folderMode: boolean;
|
||||
toggleFolderMode: () => void;
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<ILibraryItem>();
|
||||
|
||||
function TableLibraryItems({ items, resetQuery, folderMode, toggleFolderMode }: TableLibraryItemsProps) {
|
||||
function TableLibraryItems({ items }: TableLibraryItemsProps) {
|
||||
const router = useConceptNavigation();
|
||||
const intl = useIntl();
|
||||
const { getUserLabel } = useUsers();
|
||||
const { calculateHeight } = useConceptOptions();
|
||||
const [itemsPerPage, setItemsPerPage] = useLocalStorage<number>(storage.libraryPagination, 50);
|
||||
const getUserLabel = useLabelUser();
|
||||
|
||||
const folderMode = useLibrarySearchStore(state => state.folderMode);
|
||||
const toggleFolderMode = useLibrarySearchStore(state => state.toggleFolderMode);
|
||||
const resetFilter = useLibrarySearchStore(state => state.resetFilter);
|
||||
|
||||
const itemsPerPage = usePreferencesStore(state => state.libraryPagination);
|
||||
const setItemsPerPage = usePreferencesStore(state => state.setLibraryPagination);
|
||||
|
||||
function handleOpenItem(item: ILibraryItem, event: CProps.EventMouse) {
|
||||
const selection = window.getSelection();
|
||||
|
@ -140,7 +142,7 @@ function TableLibraryItems({ items, resetQuery, folderMode, toggleFolderMode }:
|
|||
})
|
||||
];
|
||||
|
||||
const tableHeight = calculateHeight('2.2rem');
|
||||
const tableHeight = useFitHeight('2.2rem');
|
||||
|
||||
const conditionalRowStyles: IConditionalStyle<ILibraryItem>[] = [
|
||||
{
|
||||
|
@ -164,7 +166,7 @@ function TableLibraryItems({ items, resetQuery, folderMode, toggleFolderMode }:
|
|||
<p>Список схем пуст</p>
|
||||
<p className='flex gap-6'>
|
||||
<TextURL text='Создать схему' href='/library/create' />
|
||||
<TextURL text='Очистить фильтр' onClick={resetQuery} />
|
||||
<TextURL text='Очистить фильтр' onClick={resetFilter} />
|
||||
</p>
|
||||
</FlexColumn>
|
||||
}
|
||||
|
|
|
@ -19,10 +19,9 @@ import DropdownButton from '@/components/ui/DropdownButton';
|
|||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import SearchBar from '@/components/ui/SearchBar';
|
||||
import SelectorButton from '@/components/ui/SelectorButton';
|
||||
import { useUsers } from '@/context/UsersContext';
|
||||
import useDropdown from '@/hooks/useDropdown';
|
||||
import { LocationHead } from '@/models/library';
|
||||
import { UserID } from '@/models/user';
|
||||
import { useHasCustomFilter, useLibrarySearchStore } from '@/stores/librarySearch';
|
||||
import { prefixes } from '@/utils/constants';
|
||||
import { describeLocationHead, labelLocationHead } from '@/utils/labels';
|
||||
import { tripleToggleColor } from '@/utils/utils';
|
||||
|
@ -30,65 +29,37 @@ import { tripleToggleColor } from '@/utils/utils';
|
|||
interface ToolbarSearchProps {
|
||||
total: number;
|
||||
filtered: number;
|
||||
hasCustomFilter: boolean;
|
||||
|
||||
query: string;
|
||||
onChangeQuery: (newValue: string) => void;
|
||||
path: string;
|
||||
onChangePath: (newValue: string) => void;
|
||||
head: LocationHead | undefined;
|
||||
onChangeHead: (newValue: LocationHead | undefined) => void;
|
||||
|
||||
folderMode: boolean;
|
||||
toggleFolderMode: () => void;
|
||||
|
||||
isVisible: boolean | undefined;
|
||||
toggleVisible: () => void;
|
||||
isOwned: boolean | undefined;
|
||||
toggleOwned: () => void;
|
||||
isEditor: boolean | undefined;
|
||||
toggleEditor: () => void;
|
||||
filterUser: UserID | undefined;
|
||||
onChangeFilterUser: (newValue: UserID | undefined) => void;
|
||||
|
||||
resetFilter: () => void;
|
||||
}
|
||||
|
||||
function ToolbarSearch({
|
||||
total,
|
||||
filtered,
|
||||
hasCustomFilter,
|
||||
|
||||
query,
|
||||
onChangeQuery,
|
||||
path,
|
||||
onChangePath,
|
||||
head,
|
||||
onChangeHead,
|
||||
|
||||
folderMode,
|
||||
toggleFolderMode,
|
||||
|
||||
isVisible,
|
||||
toggleVisible,
|
||||
isOwned,
|
||||
toggleOwned,
|
||||
isEditor,
|
||||
toggleEditor,
|
||||
filterUser,
|
||||
onChangeFilterUser,
|
||||
|
||||
resetFilter
|
||||
}: ToolbarSearchProps) {
|
||||
function ToolbarSearch({ total, filtered }: ToolbarSearchProps) {
|
||||
const headMenu = useDropdown();
|
||||
const userMenu = useDropdown();
|
||||
const { users } = useUsers();
|
||||
|
||||
const query = useLibrarySearchStore(state => state.query);
|
||||
const setQuery = useLibrarySearchStore(state => state.setQuery);
|
||||
const path = useLibrarySearchStore(state => state.path);
|
||||
const setPath = useLibrarySearchStore(state => state.setPath);
|
||||
const head = useLibrarySearchStore(state => state.head);
|
||||
const setHead = useLibrarySearchStore(state => state.setHead);
|
||||
const folderMode = useLibrarySearchStore(state => state.folderMode);
|
||||
const toggleFolderMode = useLibrarySearchStore(state => state.toggleFolderMode);
|
||||
const isOwned = useLibrarySearchStore(state => state.isOwned);
|
||||
const toggleOwned = useLibrarySearchStore(state => state.toggleOwned);
|
||||
const isEditor = useLibrarySearchStore(state => state.isEditor);
|
||||
const toggleEditor = useLibrarySearchStore(state => state.toggleEditor);
|
||||
const isVisible = useLibrarySearchStore(state => state.isVisible);
|
||||
const toggleVisible = useLibrarySearchStore(state => state.toggleVisible);
|
||||
const filterUser = useLibrarySearchStore(state => state.filterUser);
|
||||
const setFilterUser = useLibrarySearchStore(state => state.setFilterUser);
|
||||
|
||||
const resetFilter = useLibrarySearchStore(state => state.resetFilter);
|
||||
const hasCustomFilter = useHasCustomFilter();
|
||||
|
||||
const userActive = isOwned !== undefined || isEditor !== undefined || filterUser !== undefined;
|
||||
|
||||
function handleChange(newValue: LocationHead | undefined) {
|
||||
headMenu.hide();
|
||||
onChangeHead(newValue);
|
||||
setHead(newValue);
|
||||
}
|
||||
|
||||
function handleToggleFolder() {
|
||||
|
@ -155,9 +126,8 @@ function ToolbarSearch({
|
|||
noBorder
|
||||
placeholder='Выберите владельца'
|
||||
className='min-w-[15rem] text-sm mx-1 mb-1'
|
||||
items={users}
|
||||
value={filterUser}
|
||||
onSelectValue={onChangeFilterUser}
|
||||
onSelectValue={setFilterUser}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
@ -177,7 +147,7 @@ function ToolbarSearch({
|
|||
noBorder
|
||||
className={clsx('min-w-[7rem] sm:min-w-[10rem] max-w-[20rem]', folderMode && 'flex-grow')}
|
||||
query={query}
|
||||
onChangeQuery={onChangeQuery}
|
||||
onChangeQuery={setQuery}
|
||||
/>
|
||||
{!folderMode ? (
|
||||
<div ref={headMenu.ref} className='flex items-center h-full py-1 select-none'>
|
||||
|
@ -236,7 +206,7 @@ function ToolbarSearch({
|
|||
noBorder
|
||||
className='w-[4.5rem] sm:w-[5rem] flex-grow'
|
||||
query={path}
|
||||
onChangeQuery={onChangePath}
|
||||
onChangeQuery={setPath}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
|
@ -1,62 +1,52 @@
|
|||
import clsx from 'clsx';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { useAuth } from '@/backend/auth/useAuth';
|
||||
import { SubfoldersIcon } from '@/components/DomainIcons';
|
||||
import { IconFolderEdit, IconFolderTree } from '@/components/Icons';
|
||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||
import { CProps } from '@/components/props';
|
||||
import SelectLocation from '@/components/select/SelectLocation';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { useLibrary } from '@/context/LibraryContext';
|
||||
import useWindowSize from '@/hooks/useWindowSize';
|
||||
import { FolderNode, FolderTree } from '@/models/FolderTree';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
import { useFitHeight } from '@/stores/appLayout';
|
||||
import { useLibrarySearchStore } from '@/stores/librarySearch';
|
||||
import { PARAMETER, prefixes } from '@/utils/constants';
|
||||
import { information } from '@/utils/labels';
|
||||
|
||||
interface ViewSideLocationProps {
|
||||
folderTree: FolderTree;
|
||||
isVisible: boolean;
|
||||
subfolders: boolean;
|
||||
activeLocation: string;
|
||||
onChangeActiveLocation: (newValue: string) => void;
|
||||
toggleFolderMode: () => void;
|
||||
toggleSubfolders: () => void;
|
||||
onRenameLocation: () => void;
|
||||
}
|
||||
|
||||
function ViewSideLocation({
|
||||
folderTree,
|
||||
activeLocation,
|
||||
subfolders,
|
||||
isVisible,
|
||||
onChangeActiveLocation,
|
||||
toggleFolderMode,
|
||||
toggleSubfolders,
|
||||
onRenameLocation
|
||||
}: ViewSideLocationProps) {
|
||||
function ViewSideLocation({ folderTree, isVisible, onRenameLocation }: ViewSideLocationProps) {
|
||||
const { user } = useAuth();
|
||||
const { items } = useLibrary();
|
||||
const { calculateHeight } = useConceptOptions();
|
||||
const windowSize = useWindowSize();
|
||||
|
||||
const location = useLibrarySearchStore(state => state.location);
|
||||
const setLocation = useLibrarySearchStore(state => state.setLocation);
|
||||
const toggleFolderMode = useLibrarySearchStore(state => state.toggleFolderMode);
|
||||
const subfolders = useLibrarySearchStore(state => state.subfolders);
|
||||
const toggleSubfolders = useLibrarySearchStore(state => state.toggleSubfolders);
|
||||
|
||||
const canRename = (() => {
|
||||
if (activeLocation.length <= 3 || !user) {
|
||||
if (location.length <= 3 || !user) {
|
||||
return false;
|
||||
}
|
||||
if (user.is_staff) {
|
||||
return true;
|
||||
}
|
||||
const owned = items.filter(item => item.owner == user.id);
|
||||
const located = owned.filter(
|
||||
item => item.location == activeLocation || item.location.startsWith(`${activeLocation}/`)
|
||||
);
|
||||
const located = owned.filter(item => item.location == location || item.location.startsWith(`${location}/`));
|
||||
return located.length !== 0;
|
||||
})();
|
||||
|
||||
const maxHeight = calculateHeight('4.5rem');
|
||||
const maxHeight = useFitHeight('4.5rem');
|
||||
|
||||
function handleClickFolder(event: CProps.EventMouse, target: FolderNode) {
|
||||
event.preventDefault();
|
||||
|
@ -67,7 +57,7 @@ function ViewSideLocation({
|
|||
.then(() => toast.success(information.pathReady))
|
||||
.catch(console.error);
|
||||
} else {
|
||||
onChangeActiveLocation(target.getPath());
|
||||
setLocation(target.getPath());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,7 +88,7 @@ function ViewSideLocation({
|
|||
onClick={onRenameLocation}
|
||||
/>
|
||||
) : null}
|
||||
{!!activeLocation ? (
|
||||
{!!location ? (
|
||||
<MiniButton
|
||||
title={subfolders ? 'Вложенные папки: Вкл' : 'Вложенные папки: Выкл'} // prettier: split-lines
|
||||
icon={<SubfoldersIcon value={subfolders} />}
|
||||
|
@ -113,7 +103,7 @@ function ViewSideLocation({
|
|||
</div>
|
||||
</div>
|
||||
<SelectLocation
|
||||
value={activeLocation}
|
||||
value={location}
|
||||
folderTree={folderTree}
|
||||
prefix={prefixes.folders_list}
|
||||
onClick={handleClickFolder}
|
||||
|
|
|
@ -5,15 +5,15 @@ import clsx from 'clsx';
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { urls } from '@/app/urls';
|
||||
import { useAuth } from '@/backend/auth/useAuth';
|
||||
import { useLogin } from '@/backend/auth/useLogin';
|
||||
import InfoError, { ErrorData } from '@/components/info/InfoError';
|
||||
import SubmitButton from '@/components/ui/SubmitButton';
|
||||
import TextInput from '@/components/ui/TextInput';
|
||||
import TextURL from '@/components/ui/TextURL';
|
||||
import ExpectedAnonymous from '@/components/wrap/ExpectedAnonymous';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||
import useQueryStrings from '@/hooks/useQueryStrings';
|
||||
import { IUserLoginData } from '@/models/user';
|
||||
import { resources } from '@/utils/constants';
|
||||
|
||||
function LoginPage() {
|
||||
|
@ -21,23 +21,20 @@ function LoginPage() {
|
|||
const query = useQueryStrings();
|
||||
const userQuery = query.get('username');
|
||||
|
||||
const { user, login, loading, error, setError } = useAuth();
|
||||
const { user } = useAuth();
|
||||
const { login, isPending, error, reset } = useLogin();
|
||||
|
||||
const [username, setUsername] = useState(userQuery || '');
|
||||
const [password, setPassword] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setError(undefined);
|
||||
}, [username, password, setError]);
|
||||
reset();
|
||||
}, [username, password, reset]);
|
||||
|
||||
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (!loading) {
|
||||
const data: IUserLoginData = {
|
||||
username: username,
|
||||
password: password
|
||||
};
|
||||
login(data, () => {
|
||||
if (!isPending) {
|
||||
login(username, password, () => {
|
||||
if (router.canBack()) {
|
||||
router.back();
|
||||
} else {
|
||||
|
@ -78,7 +75,7 @@ function LoginPage() {
|
|||
<SubmitButton
|
||||
text='Войти'
|
||||
className='self-center w-[12rem] mt-3'
|
||||
loading={loading}
|
||||
loading={isPending}
|
||||
disabled={!username || !password}
|
||||
/>
|
||||
<div className='flex flex-col text-sm'>
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
import { useCallback } from 'react';
|
||||
|
||||
import { urls } from '@/app/urls';
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||
import useQueryStrings from '@/hooks/useQueryStrings';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
import { useMainHeight } from '@/stores/appLayout';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
||||
import TopicsList from './TopicsList';
|
||||
|
@ -17,7 +17,7 @@ function ManualsPage() {
|
|||
const query = useQueryStrings();
|
||||
const activeTopic = (query.get('topic') || HelpTopic.MAIN) as HelpTopic;
|
||||
|
||||
const { mainHeight } = useConceptOptions();
|
||||
const mainHeight = useMainHeight();
|
||||
|
||||
const onSelectTopic = useCallback(
|
||||
(newTopic: HelpTopic) => {
|
||||
|
|
|
@ -6,9 +6,9 @@ import { useCallback } from 'react';
|
|||
import { IconMenuFold, IconMenuUnfold } from '@/components/Icons';
|
||||
import Button from '@/components/ui/Button';
|
||||
import SelectTree from '@/components/ui/SelectTree';
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import useDropdown from '@/hooks/useDropdown';
|
||||
import { HelpTopic, topicParent } from '@/models/miscellaneous';
|
||||
import { useAppLayoutStore, useFitHeight } from '@/stores/appLayout';
|
||||
import { PARAMETER, prefixes } from '@/utils/constants';
|
||||
import { describeHelpTopic, labelHelpTopic } from '@/utils/labels';
|
||||
|
||||
|
@ -19,7 +19,8 @@ interface TopicsDropdownProps {
|
|||
|
||||
function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownProps) {
|
||||
const menu = useDropdown();
|
||||
const { noNavigation, calculateHeight } = useConceptOptions();
|
||||
const noNavigation = useAppLayoutStore(state => state.noNavigation);
|
||||
const treeHeight = useFitHeight('4rem + 2px');
|
||||
|
||||
const handleSelectTopic = useCallback(
|
||||
(topic: HelpTopic) => {
|
||||
|
@ -67,7 +68,7 @@ function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownProps) {
|
|||
'bg-prim-200'
|
||||
)}
|
||||
style={{
|
||||
maxHeight: calculateHeight('4rem + 2px'),
|
||||
maxHeight: treeHeight,
|
||||
transitionProperty: 'clip-path',
|
||||
transitionDuration: `${PARAMETER.moveDuration}ms`,
|
||||
clipPath: menu.isOpen ? 'inset(0% 0% 0% 0%)' : 'inset(0% 100% 0% 0%)'
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import SelectTree from '@/components/ui/SelectTree';
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { HelpTopic, topicParent } from '@/models/miscellaneous';
|
||||
import { useFitHeight } from '@/stores/appLayout';
|
||||
import { prefixes } from '@/utils/constants';
|
||||
import { describeHelpTopic, labelHelpTopic } from '@/utils/labels';
|
||||
|
||||
|
@ -12,7 +12,7 @@ interface TopicsStaticProps {
|
|||
}
|
||||
|
||||
function TopicsStatic({ activeTopic, onChangeTopic }: TopicsStaticProps) {
|
||||
const { calculateHeight } = useConceptOptions();
|
||||
const topicsHeight = useFitHeight('1rem + 2px');
|
||||
return (
|
||||
<SelectTree
|
||||
items={Object.values(HelpTopic).map(item => item as HelpTopic)}
|
||||
|
@ -31,7 +31,7 @@ function TopicsStatic({ activeTopic, onChangeTopic }: TopicsStaticProps) {
|
|||
'text-xs sm:text-sm bg-prim-200',
|
||||
'select-none'
|
||||
)}
|
||||
style={{ maxHeight: calculateHeight('1rem + 2px') }}
|
||||
style={{ maxHeight: topicsHeight }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user