Compare commits

...

14 Commits

Author SHA1 Message Date
Ivan
76aee5bea7 F: Implement react-query pt2
Some checks failed
Frontend CI / build (22.x) (push) Has been cancelled
2025-01-21 20:33:05 +03:00
Ivan
d899e17fcd F: Implement react-query pt1 2025-01-21 12:00:09 +03:00
Ivan
bfae07c7b6 npm update 2025-01-17 12:19:21 +03:00
Ivan
6f23ec5354 R: Refactor dialogs using zustand store 2025-01-16 16:31:03 +03:00
Ivan
539ed87ddf R: Replace AccessMode with RoleStore 2025-01-15 23:03:23 +03:00
Ivan
55fa09c6fb R: Migrating to zustand for local state management pt3 2025-01-15 22:16:06 +03:00
Ivan
f526efd0ee B: Fix nodes not being selectable through hidden div 2025-01-15 19:15:52 +03:00
Ivan
08fd26e687 R: Migrating to zustand for local state management pt2 2025-01-15 16:06:18 +03:00
Ivan
26bd0ce16b R: Migrating to zustand for local state management pt1 2025-01-14 21:57:32 +03:00
Ivan
2e03756646 R: Integrate Playwrght as end-to-end testing 2025-01-14 16:24:46 +03:00
Ivan
50d29f5fee npm update 2025-01-13 15:49:15 +03:00
Ivan
a47ea5d32d M: Fix layout for small screens 2025-01-09 20:35:51 +03:00
Ivan
68dd1aa3c4 B: Fix intl version 2025-01-09 16:15:49 +03:00
Ivan
12a4b740bc npm update 2025-01-03 11:05:33 +03:00
146 changed files with 3144 additions and 2778 deletions

View File

@ -23,7 +23,6 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [22.x] node-version: [22.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -35,10 +34,17 @@ jobs:
cache: "npm" cache: "npm"
- name: Build - name: Build
run: | run: |
npm install -g typescript vite jest npm install -g typescript vite jest playwright
npx playwright install --with-deps
npm ci npm ci
npm run build --if-present npm run build --if-present
- name: Run CI - name: Run CI
run: | run: |
npm run lint npm run lint
npm test npm test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30

2
.gitignore vendored
View File

@ -67,3 +67,5 @@ bower_components
venv/ venv/
/GitExtensions.settings /GitExtensions.settings
rsconcept/frontend/public/privacy.pdf rsconcept/frontend/public/privacy.pdf
/rsconcept/frontend/playwright-report
/rsconcept/frontend/test-results

View File

@ -19,7 +19,8 @@
"ms-python.pylint", "ms-python.pylint",
"ms-python.autopep8", "ms-python.autopep8",
"ms-python.vscode-pylance", "ms-python.vscode-pylance",
"vscode-icons-team.vscode-icons" "vscode-icons-team.vscode-icons",
"ms-playwright.playwright"
], ],
"unwantedRecommendations": [] "unwantedRecommendations": []
} }

View File

@ -119,6 +119,7 @@
"NUMR", "NUMR",
"Opencorpora", "Opencorpora",
"overscroll", "overscroll",
"partialize",
"passwordreset", "passwordreset",
"perfectivity", "perfectivity",
"PNCT", "PNCT",

View File

@ -44,7 +44,10 @@ This readme file is used mostly to document project dependencies and conventions
- use-debounce - use-debounce
- qrcode.react - qrcode.react
- html-to-image - html-to-image
- zustand
- @tanstack/react-table - @tanstack/react-table
- @tanstack/react-query
- @tanstack/react-query-devtools
- @uiw/react-codemirror - @uiw/react-codemirror
- @uiw/codemirror-themes - @uiw/codemirror-themes
- @lezer/lr - @lezer/lr
@ -67,6 +70,7 @@ This readme file is used mostly to document project dependencies and conventions
- ts-jest - ts-jest
- @types/jest - @types/jest
- @lezer/generator - @lezer/generator
- @playwright/test
</pre> </pre>
</details> </details>
<details> <details>
@ -132,6 +136,7 @@ This readme file is used mostly to document project dependencies and conventions
- isort - isort
- Django - Django
- SQLite - SQLite
- Playwright
</pre> </pre>
</details> </details>

View File

@ -20,22 +20,6 @@
/> />
<title>Концепт Портал</title> <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> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@
"type": "module", "type": "module",
"scripts": { "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", "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", "dev": "vite --host",
"build": "tsc && vite build", "build": "tsc && vite build",
"lint": "eslint . --report-unused-disable-directives --max-warnings 0", "lint": "eslint . --report-unused-disable-directives --max-warnings 0",
@ -14,6 +14,8 @@
"dependencies": { "dependencies": {
"@dagrejs/dagre": "^1.1.4", "@dagrejs/dagre": "^1.1.4",
"@lezer/lr": "^1.4.2", "@lezer/lr": "^1.4.2",
"@tanstack/react-query": "^5.64.1",
"@tanstack/react-query-devtools": "^5.64.1",
"@tanstack/react-table": "^8.20.6", "@tanstack/react-table": "^8.20.6",
"@uiw/codemirror-themes": "^4.23.7", "@uiw/codemirror-themes": "^4.23.7",
"@uiw/react-codemirror": "^4.23.7", "@uiw/react-codemirror": "^4.23.7",
@ -24,42 +26,44 @@
"qrcode.react": "^4.2.0", "qrcode.react": "^4.2.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^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-icons": "^5.4.0",
"react-intl": "^7.0.4", "react-intl": "7.0.4",
"react-router": "^7.0.2", "react-router": "^7.1.2",
"react-select": "^5.9.0", "react-select": "^5.9.0",
"react-tabs": "^6.0.2", "react-tabs": "^6.1.0",
"react-toastify": "^11.0.0", "react-toastify": "^11.0.3",
"react-tooltip": "^5.28.0", "react-tooltip": "^5.28.0",
"react-zoom-pan-pinch": "^3.6.1", "react-zoom-pan-pinch": "^3.6.1",
"reactflow": "^11.11.4", "reactflow": "^11.11.4",
"use-debounce": "^10.0.4" "use-debounce": "^10.0.4",
"zustand": "^5.0.3"
}, },
"devDependencies": { "devDependencies": {
"@lezer/generator": "^1.7.2", "@lezer/generator": "^1.7.2",
"@playwright/test": "^1.49.1",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/node": "^22.10.2", "@types/node": "^22.10.7",
"@types/react": "^19.0.1", "@types/react": "^19.0.7",
"@types/react-dom": "^19.0.2", "@types/react-dom": "^19.0.3",
"@typescript-eslint/eslint-plugin": "^8.0.1", "@typescript-eslint/eslint-plugin": "^8.0.1",
"@typescript-eslint/parser": "^8.0.1", "@typescript-eslint/parser": "^8.0.1",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"babel-plugin-react-compiler": "^19.0.0-beta-37ed2a7-20241206", "babel-plugin-react-compiler": "^19.0.0-beta-37ed2a7-20241206",
"eslint": "^9.17.0", "eslint": "^9.18.0",
"eslint-plugin-react": "^7.37.2", "eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-compiler": "^19.0.0-beta-37ed2a7-20241206", "eslint-plugin-react-compiler": "^19.0.0-beta-37ed2a7-20241206",
"eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
"globals": "^15.13.0", "globals": "^15.14.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"postcss": "^8.4.49", "postcss": "^8.5.1",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"ts-jest": "^29.2.5", "ts-jest": "^29.2.5",
"typescript": "^5.7.2", "typescript": "^5.7.3",
"typescript-eslint": "^8.18.1", "typescript-eslint": "^8.20.0",
"vite": "^6.0.3" "vite": "^6.0.7"
}, },
"overrides": { "overrides": {
"react": "^19.0.0" "react": "^19.0.0"
@ -67,6 +71,9 @@
"jest": { "jest": {
"preset": "ts-jest", "preset": "ts-jest",
"testEnvironment": "node", "testEnvironment": "node",
"testPathIgnorePatterns": [
"<rootDir>/tests/"
],
"transform": { "transform": {
"node_modules/variables/.+\\.(j|t)sx?$": "ts-jest" "node_modules/variables/.+\\.(j|t)sx?$": "ts-jest"
}, },

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

View File

@ -5,12 +5,23 @@ import ConceptToaster from '@/app/ConceptToaster';
import Footer from '@/app/Footer'; import Footer from '@/app/Footer';
import Navigation from '@/app/Navigation'; import Navigation from '@/app/Navigation';
import Loader from '@/components/ui/Loader'; import Loader from '@/components/ui/Loader';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { NavigationState } from '@/context/NavigationContext'; import { NavigationState } from '@/context/NavigationContext';
import { useAppLayoutStore, useMainHeight, useViewportHeight } from '@/stores/appLayout';
import { globals } from '@/utils/constants'; import { globals } from '@/utils/constants';
import { GlobalDialogs } from './GlobalDialogs';
import { GlobalTooltips } from './GlobalTooltips';
function ApplicationLayout() { 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 ( return (
<NavigationState> <NavigationState>
<div className='min-w-[20rem] antialiased h-full max-w-[120rem] mx-auto'> <div className='min-w-[20rem] antialiased h-full max-w-[120rem] mx-auto'>
@ -22,6 +33,9 @@ function ApplicationLayout() {
pauseOnFocusLoss={false} pauseOnFocusLoss={false}
/> />
<GlobalDialogs />
<GlobalTooltips />
<Navigation /> <Navigation />
<div <div
@ -36,7 +50,7 @@ function ApplicationLayout() {
<Outlet /> <Outlet />
</Suspense> </Suspense>
</main> </main>
<Footer /> {!noNavigation && !noFooter ? <Footer /> : null}
</div> </div>
</div> </div>
</NavigationState> </NavigationState>

View File

@ -1,11 +1,11 @@
import { ToastContainer, type ToastContainerProps } from 'react-toastify'; import { ToastContainer, type ToastContainerProps } from 'react-toastify';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { usePreferencesStore } from '@/stores/preferences';
interface ToasterThemedProps extends Omit<ToastContainerProps, 'theme'> {} interface ToasterThemedProps extends Omit<ToastContainerProps, 'theme'> {}
function ToasterThemed(props: ToasterThemedProps) { function ToasterThemed(props: ToasterThemedProps) {
const { darkMode } = useConceptOptions(); const darkMode = usePreferencesStore(state => state.darkMode);
return <ToastContainer theme={darkMode ? 'dark' : 'light'} {...props} />; return <ToastContainer theme={darkMode ? 'dark' : 'light'} {...props} />;
} }

View File

@ -1,15 +1,10 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { external_urls } from '@/utils/constants'; import { external_urls } from '@/utils/constants';
import TextURL from '../components/ui/TextURL'; import TextURL from '../components/ui/TextURL';
function Footer() { function Footer() {
const { noNavigation, noFooter } = useConceptOptions();
if (noNavigation || noFooter) {
return null;
}
return ( return (
<footer <footer
className={clsx( className={clsx(

View 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 />;
}
};

View File

@ -1,13 +1,13 @@
'use client'; 'use client';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
import { IntlProvider } from 'react-intl'; import { IntlProvider } from 'react-intl';
import { AuthState } from '@/context/AuthContext'; import { queryClient } from '@/backend/queryClient';
import { OptionsState } from '@/context/ConceptOptionsContext';
import { GlobalOssState } from '@/context/GlobalOssContext'; import { GlobalOssState } from '@/context/GlobalOssContext';
import { LibraryState } from '@/context/LibraryContext'; import { LibraryState } from '@/context/LibraryContext';
import { UsersState } from '@/context/UsersContext';
import ErrorFallback from './ErrorFallback'; import ErrorFallback from './ErrorFallback';
@ -31,19 +31,16 @@ function GlobalProviders({ children }: React.PropsWithChildren) {
onError={logError} onError={logError}
> >
<IntlProvider locale='ru' defaultLocale='ru'> <IntlProvider locale='ru' defaultLocale='ru'>
<OptionsState> <QueryClientProvider client={queryClient}>
<UsersState>
<AuthState>
<LibraryState> <LibraryState>
<GlobalOssState> <GlobalOssState>
<ReactQueryDevtools initialIsOpen={false} />
{children} {children}
</GlobalOssState> </GlobalOssState>
</LibraryState> </LibraryState>
</AuthState> </QueryClientProvider>
</UsersState>
</OptionsState>
</IntlProvider> </IntlProvider>
</ErrorBoundary>); </ErrorBoundary>);
} }

View 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>
</>
);
};

View File

@ -1,8 +1,8 @@
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import useWindowSize from '@/hooks/useWindowSize'; import useWindowSize from '@/hooks/useWindowSize';
import { usePreferencesStore } from '@/stores/preferences';
function Logo() { function Logo() {
const { darkMode } = useConceptOptions(); const darkMode = usePreferencesStore(state => state.darkMode);
const size = useWindowSize(); const size = useWindowSize();
return ( return (

View File

@ -2,9 +2,9 @@ import clsx from 'clsx';
import { IconLibrary2, IconManuals, IconNewItem2 } from '@/components/Icons'; import { IconLibrary2, IconManuals, IconNewItem2 } from '@/components/Icons';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
import useWindowSize from '@/hooks/useWindowSize'; import useWindowSize from '@/hooks/useWindowSize';
import { useAppLayoutStore } from '@/stores/appLayout';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { urls } from '../urls'; import { urls } from '../urls';
@ -16,7 +16,7 @@ import UserMenu from './UserMenu';
function Navigation() { function Navigation() {
const router = useConceptNavigation(); const router = useConceptNavigation();
const size = useWindowSize(); 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 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); const navigateLibrary = (event: CProps.EventMouse) => router.push(urls.library, event.ctrlKey || event.metaKey);
@ -29,7 +29,8 @@ function Navigation() {
className={clsx( className={clsx(
'z-navigation', // prettier: split lines 'z-navigation', // prettier: split lines
'sticky top-0 left-0 right-0', 'sticky top-0 left-0 right-0',
'select-none' 'select-none',
'bg-prim-100'
)} )}
> >
<ToggleNavigation /> <ToggleNavigation />
@ -57,6 +58,7 @@ function Navigation() {
<NavigationButton text='Новая схема' icon={<IconNewItem2 size='1.5rem' />} onClick={navigateCreateNew} /> <NavigationButton text='Новая схема' icon={<IconNewItem2 size='1.5rem' />} onClick={navigateCreateNew} />
<NavigationButton text='Библиотека' icon={<IconLibrary2 size='1.5rem' />} onClick={navigateLibrary} /> <NavigationButton text='Библиотека' icon={<IconLibrary2 size='1.5rem' />} onClick={navigateLibrary} />
<NavigationButton text='Справка' icon={<IconManuals size='1.5rem' />} onClick={navigateHelp} /> <NavigationButton text='Справка' icon={<IconManuals size='1.5rem' />} onClick={navigateHelp} />
<UserMenu /> <UserMenu />
</div> </div>
</div> </div>

View File

@ -1,11 +1,16 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { IconDarkTheme, IconLightTheme, IconPin, IconUnpin } from '@/components/Icons'; 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'; import { globals, PARAMETER } from '@/utils/constants';
function ToggleNavigation() { 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'; const iconSize = !noNavigationAnimation ? '0.75rem' : '1rem';
return ( return (
<div <div

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

View File

@ -1,3 +1,5 @@
import { useAuth } from '@/backend/auth/useAuth';
import { useLogout } from '@/backend/auth/useLogout';
import { import {
IconAdmin, IconAdmin,
IconAdminOff, IconAdminOff,
@ -15,9 +17,8 @@ import {
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import Dropdown from '@/components/ui/Dropdown'; import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton'; import DropdownButton from '@/components/ui/DropdownButton';
import { useAuth } from '@/context/AuthContext';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
import { usePreferencesStore } from '@/stores/preferences';
import { urls } from '../urls'; import { urls } from '../urls';
@ -27,9 +28,16 @@ interface UserDropdownProps {
} }
function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) { function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
const { darkMode, adminMode, toggleAdminMode, toggleDarkMode, showHelp, toggleShowHelp } = useConceptOptions();
const router = useConceptNavigation(); 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) { function navigateProfile(event: CProps.EventMouse) {
hideDropdown(); hideDropdown();

View File

@ -1,41 +1,22 @@
import { IconLogin, IconUser2 } from '@/components/Icons'; import { Suspense } from 'react';
import Loader from '@/components/ui/Loader'; import Loader from '@/components/ui/Loader';
import { useAuth } from '@/context/AuthContext';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
import useDropdown from '@/hooks/useDropdown'; import useDropdown from '@/hooks/useDropdown';
import { urls } from '../urls'; import { urls } from '../urls';
import NavigationButton from './NavigationButton'; import UserButton from './UserButton';
import UserDropdown from './UserDropdown'; import UserDropdown from './UserDropdown';
function UserMenu() { function UserMenu() {
const router = useConceptNavigation(); const router = useConceptNavigation();
const { user, loading } = useAuth();
const { adminMode } = useConceptOptions();
const menu = useDropdown(); const menu = useDropdown();
const navigateLogin = () => router.push(urls.login);
return ( return (
<div ref={menu.ref} className='h-full w-[4rem] flex items-center justify-center'> <div ref={menu.ref} className='h-full w-[4rem] flex items-center justify-center'>
{loading ? <Loader circular scale={1.5} /> : null} <Suspense fallback={<Loader circular scale={1.5} />}>
{!user && !loading ? ( <UserButton onLogin={() => router.push(urls.login)} onClickUser={menu.toggle} />
<NavigationButton </Suspense>
className='cc-fade-in' <UserDropdown isOpen={menu.isOpen} hideDropdown={() => menu.hide()} />
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()} />
</div> </div>
); );
} }

View File

@ -42,17 +42,17 @@ export interface IAxiosRequest<RequestData, ResponseData> {
// ================ Transport API calls ================ // ================ Transport API calls ================
export function AxiosGet<ResponseData>({ endpoint, request, options }: IAxiosRequest<undefined, ResponseData>) { export function AxiosGet<ResponseData>({ endpoint, request, options }: IAxiosRequest<undefined, ResponseData>) {
if (request.setLoading) request.setLoading(true); request.setLoading?.(true);
axiosInstance axiosInstance
.get<ResponseData>(endpoint, options) .get<ResponseData>(endpoint, options)
.then(response => { .then(response => {
if (request.setLoading) request.setLoading(false); request.setLoading?.(false);
if (request.onSuccess) request.onSuccess(response.data); request.onSuccess?.(response.data);
}) })
.catch((error: Error | AxiosError) => { .catch((error: Error | AxiosError) => {
if (request.setLoading) request.setLoading(false); request.setLoading?.(false);
if (request.showError) toast.error(extractErrorMessage(error)); 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, request,
options options
}: IAxiosRequest<RequestData, ResponseData>) { }: IAxiosRequest<RequestData, ResponseData>) {
if (request.setLoading) request.setLoading(true); request.setLoading?.(true);
axiosInstance axiosInstance
.post<ResponseData>(endpoint, request.data, options) .post<ResponseData>(endpoint, request.data, options)
.then(response => { .then(response => {
if (request.setLoading) request.setLoading(false); request.setLoading?.(false);
if (request.onSuccess) request.onSuccess(response.data); request.onSuccess?.(response.data);
}) })
.catch((error: Error | AxiosError) => { .catch((error: Error | AxiosError) => {
if (request.setLoading) request.setLoading(false); request.setLoading?.(false);
if (request.showError) toast.error(extractErrorMessage(error)); 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, request,
options options
}: IAxiosRequest<RequestData, ResponseData>) { }: IAxiosRequest<RequestData, ResponseData>) {
if (request.setLoading) request.setLoading(true); request.setLoading?.(true);
axiosInstance axiosInstance
.delete<ResponseData>(endpoint, options) .delete<ResponseData>(endpoint, options)
.then(response => { .then(response => {
if (request.setLoading) request.setLoading(false); request.setLoading?.(false);
if (request.onSuccess) request.onSuccess(response.data); request.onSuccess?.(response.data);
}) })
.catch((error: Error | AxiosError) => { .catch((error: Error | AxiosError) => {
if (request.setLoading) request.setLoading(false); request.setLoading?.(false);
if (request.showError) toast.error(extractErrorMessage(error)); 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, request,
options options
}: IAxiosRequest<RequestData, ResponseData>) { }: IAxiosRequest<RequestData, ResponseData>) {
if (request.setLoading) request.setLoading(true); request.setLoading?.(true);
axiosInstance axiosInstance
.patch<ResponseData>(endpoint, request.data, options) .patch<ResponseData>(endpoint, request.data, options)
.then(response => { .then(response => {
if (request.setLoading) request.setLoading(false); request.setLoading?.(false);
if (request.onSuccess) request.onSuccess(response.data); request.onSuccess?.(response.data);
return response.data; return response.data;
}) })
.catch((error: Error | AxiosError) => { .catch((error: Error | AxiosError) => {
if (request.setLoading) request.setLoading(false); request.setLoading?.(false);
if (request.showError) toast.error(extractErrorMessage(error)); if (request.showError) toast.error(extractErrorMessage(error));
if (request.onError) request.onError(error); request.onError?.(error);
}); });
} }

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

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

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

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

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

View File

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

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

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

View File

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

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

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

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

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

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

View 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 ?? [] };
}

View File

@ -9,10 +9,10 @@ import { EditorView } from 'codemirror';
import { forwardRef, useRef } from 'react'; import { forwardRef, useRef } from 'react';
import Label from '@/components/ui/Label'; import Label from '@/components/ui/Label';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { ConstituentaID, IRSForm } from '@/models/rsform'; import { ConstituentaID, IRSForm } from '@/models/rsform';
import { generateAlias, getCstTypePrefix, guessCstType } from '@/models/rsformAPI'; import { generateAlias, getCstTypePrefix, guessCstType } from '@/models/rsformAPI';
import { extractGlobals } from '@/models/rslangAPI'; import { extractGlobals } from '@/models/rslangAPI';
import { usePreferencesStore } from '@/stores/preferences';
import { APP_COLORS } from '@/styling/color'; import { APP_COLORS } from '@/styling/color';
import { ccBracketMatching } from './bracketMatching'; import { ccBracketMatching } from './bracketMatching';
@ -64,7 +64,7 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
}, },
ref ref
) => { ) => {
const { darkMode } = useConceptOptions(); const darkMode = usePreferencesStore(state => state.darkMode);
const internalRef = useRef<ReactCodeMirrorRef>(null); const internalRef = useRef<ReactCodeMirrorRef>(null);
const thisRef = !ref || typeof ref === 'function' ? internalRef : ref; const thisRef = !ref || typeof ref === 'function' ? internalRef : ref;

View File

@ -9,13 +9,13 @@ import { EditorView } from 'codemirror';
import { forwardRef, useRef, useState } from 'react'; import { forwardRef, useRef, useState } from 'react';
import Label from '@/components/ui/Label'; import Label from '@/components/ui/Label';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import DlgEditReference from '@/dialogs/DlgEditReference';
import { ReferenceType } from '@/models/language'; import { ReferenceType } from '@/models/language';
import { DialogType } from '@/models/miscellaneous';
import { ConstituentaID, IRSForm } from '@/models/rsform'; import { ConstituentaID, IRSForm } from '@/models/rsform';
import { useDialogsStore } from '@/stores/dialogs';
import { usePreferencesStore } from '@/stores/preferences';
import { APP_COLORS } from '@/styling/color'; import { APP_COLORS } from '@/styling/color';
import { CodeMirrorWrapper } from '@/utils/codemirror'; import { CodeMirrorWrapper } from '@/utils/codemirror';
import { PARAMETER } from '@/utils/constants';
import { refsNavigation } from './clickNavigation'; import { refsNavigation } from './clickNavigation';
import { NaturalLanguage, ReferenceTokens } from './parse'; import { NaturalLanguage, ReferenceTokens } from './parse';
@ -92,11 +92,14 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
}, },
ref ref
) => { ) => {
const { darkMode } = useConceptOptions(); const darkMode = usePreferencesStore(state => state.darkMode);
const [isFocused, setIsFocused] = useState(false); 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 [currentType, setCurrentType] = useState<ReferenceType>(ReferenceType.ENTITY);
const [refText, setRefText] = useState(''); const [refText, setRefText] = useState('');
const [hintText, setHintText] = useState(''); const [hintText, setHintText] = useState('');
@ -146,7 +149,7 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
} }
function handleInput(event: React.KeyboardEvent<HTMLDivElement>) { function handleInput(event: React.KeyboardEvent<HTMLDivElement>) {
if (!thisRef.current?.view) { if (!thisRef.current?.view || !schema) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
return; return;
@ -174,7 +177,17 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
setMainRefs(mainNodes.map(node => wrap.getText(node.from, node.to))); setMainRefs(mainNodes.map(node => wrap.getText(node.from, node.to)));
setBasePosition(mainNodes.filter(node => node.to <= selection.from).length); 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); wrap.replaceWith(referenceText);
} }
function hideEditReference() {
setShowEditor(false);
setTimeout(() => thisRef.current?.view?.focus(), PARAMETER.refreshTimeout);
}
return ( return (
<div className={clsx('flex flex-col gap-2', cursor)}> <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} /> <Label text={label} />
<CodeMirror <CodeMirror
id={id} id={id}
@ -215,10 +209,10 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
basicSetup={editorSetup} basicSetup={editorSetup}
theme={customTheme} theme={customTheme}
extensions={editorExtensions} extensions={editorExtensions}
value={isFocused ? value : value !== initialValue || showEditor ? value : resolved} value={isFocused ? value : value !== initialValue || isActive ? value : resolved}
indentWithTab={false} indentWithTab={false}
onChange={handleChange} onChange={handleChange}
editable={!disabled && !showEditor} editable={!disabled && !isActive}
onKeyDown={handleInput} onKeyDown={handleInput}
onFocus={handleFocusIn} onFocus={handleFocusIn}
onBlur={handleFocusOut} onBlur={handleFocusOut}

View File

@ -1,7 +1,7 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { CstClass, IConstituenta } from '@/models/rsform'; import { CstClass, IConstituenta } from '@/models/rsform';
import { useTooltipsStore } from '@/stores/tooltips';
import { APP_COLORS, colorFgCstStatus } from '@/styling/color'; import { APP_COLORS, colorFgCstStatus } from '@/styling/color';
import { globals } from '@/utils/constants'; import { globals } from '@/utils/constants';
@ -19,7 +19,7 @@ interface BadgeConstituentaProps extends CProps.Styling {
* Displays a badge with a constituenta alias and information tooltip. * Displays a badge with a constituenta alias and information tooltip.
*/ */
function BadgeConstituenta({ value, prefixID, className, style }: BadgeConstituentaProps) { function BadgeConstituenta({ value, prefixID, className, style }: BadgeConstituentaProps) {
const { setHoverCst } = useConceptOptions(); const setActiveCst = useTooltipsStore(state => state.setActiveCst);
return ( return (
<div <div
@ -39,7 +39,7 @@ function BadgeConstituenta({ value, prefixID, className, style }: BadgeConstitue
...style ...style
}} }}
data-tooltip-id={globals.constituenta_tooltip} data-tooltip-id={globals.constituenta_tooltip}
onMouseEnter={() => setHoverCst(value)} onMouseEnter={() => setActiveCst(value)}
> >
{value.alias} {value.alias}
</div> </div>

View File

@ -2,8 +2,8 @@ import React, { Suspense } from 'react';
import TextURL from '@/components/ui/TextURL'; import TextURL from '@/components/ui/TextURL';
import Tooltip, { PlacesType } from '@/components/ui/Tooltip'; import Tooltip, { PlacesType } from '@/components/ui/Tooltip';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
import { usePreferencesStore } from '@/stores/preferences';
import { IconHelp } from '../Icons'; import { IconHelp } from '../Icons';
import { CProps } from '../props'; import { CProps } from '../props';
@ -29,7 +29,7 @@ interface BadgeHelpProps extends CProps.Styling {
* Display help icon with a manual page tooltip. * Display help icon with a manual page tooltip.
*/ */
function BadgeHelp({ topic, padding = 'p-1', ...restProps }: BadgeHelpProps) { function BadgeHelp({ topic, padding = 'p-1', ...restProps }: BadgeHelpProps) {
const { showHelp } = useConceptOptions(); const showHelp = usePreferencesStore(state => state.showHelp);
if (!showHelp) { if (!showHelp) {
return null; return null;

View File

@ -1,6 +1,6 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useUsers } from '@/context/UsersContext'; import { useLabelUser } from '@/backend/users/useLabelUser';
import { UserID } from '@/models/user'; import { UserID } from '@/models/user';
import { CProps } from '../props'; import { CProps } from '../props';
@ -12,8 +12,7 @@ interface InfoUsersProps extends CProps.Styling {
} }
function InfoUsers({ items, className, prefix, header, ...restProps }: InfoUsersProps) { function InfoUsers({ items, className, prefix, header, ...restProps }: InfoUsersProps) {
const { getUserLabel } = useUsers(); const getUserLabel = useLabelUser();
return ( return (
<div className={clsx('flex flex-col dense', className)} {...restProps}> <div className={clsx('flex flex-col dense', className)} {...restProps}>
{header ? <h2>{header}</h2> : null} {header ? <h2>{header}</h2> : null}

View File

@ -2,17 +2,18 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useUsers } from '@/context/UsersContext'; import { useLabelUser } from '@/backend/users/useLabelUser';
import { IUserInfo, UserID } from '@/models/user'; import { useUsers } from '@/backend/users/useUsers';
import { UserID } from '@/models/user';
import { matchUser } from '@/models/userAPI'; import { matchUser } from '@/models/userAPI';
import { CProps } from '../props'; import { CProps } from '../props';
import SelectSingle from '../ui/SelectSingle'; import SelectSingle from '../ui/SelectSingle';
interface SelectUserProps extends CProps.Styling { interface SelectUserProps extends CProps.Styling {
items?: IUserInfo[];
value?: UserID; value?: UserID;
onSelectValue: (newValue: UserID) => void; onSelectValue: (newValue: UserID) => void;
filter?: (userID: UserID) => boolean;
placeholder?: string; placeholder?: string;
noBorder?: boolean; noBorder?: boolean;
@ -20,20 +21,23 @@ interface SelectUserProps extends CProps.Styling {
function SelectUser({ function SelectUser({
className, className,
items, filter,
value, value,
onSelectValue, onSelectValue,
placeholder = 'Выберите пользователя', placeholder = 'Выберите пользователя',
...restProps ...restProps
}: SelectUserProps) { }: SelectUserProps) {
const { getUserLabel } = useUsers(); const { users } = useUsers();
const getUserLabel = useLabelUser();
const items = filter ? users.filter(user => filter(user.id)) : users;
const options = const options =
items?.map(user => ({ items?.map(user => ({
value: user.id, value: user.id,
label: getUserLabel(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); const user = items?.find(item => item.id === option.value);
return !user ? false : matchUser(user, inputValue); return !user ? false : matchUser(user, inputValue);
} }
@ -47,7 +51,7 @@ function SelectUser({
if (data?.value !== undefined) onSelectValue(data.value); if (data?.value !== undefined) onSelectValue(data.value);
}} }}
// @ts-expect-error: TODO: use type definitions from react-select in filter object // @ts-expect-error: TODO: use type definitions from react-select in filter object
filterOption={filter} filterOption={filterLabel}
placeholder={placeholder} placeholder={placeholder}
{...restProps} {...restProps}
/> />

View File

@ -4,6 +4,7 @@ import clsx from 'clsx';
import useEscapeKey from '@/hooks/useEscapeKey'; import useEscapeKey from '@/hooks/useEscapeKey';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
import { useDialogsStore } from '@/stores/dialogs';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { prepareTooltip } from '@/utils/labels'; import { prepareTooltip } from '@/utils/labels';
@ -32,9 +33,6 @@ export interface ModalProps extends CProps.Styling {
/** Indicates that the modal window should be scrollable. */ /** Indicates that the modal window should be scrollable. */
overflowVisible?: boolean; overflowVisible?: boolean;
/** Callback to be called when the modal window is closed. */
hideWindow: () => void;
/** Callback to be called before submit. */ /** Callback to be called before submit. */
beforeSubmit?: () => boolean; beforeSubmit?: () => boolean;
@ -65,7 +63,6 @@ function Modal({
canSubmit, canSubmit,
overflowVisible, overflowVisible,
hideWindow,
beforeSubmit, beforeSubmit,
onSubmit, onSubmit,
onCancel, onCancel,
@ -75,10 +72,11 @@ function Modal({
hideHelpWhen, hideHelpWhen,
...restProps ...restProps
}: React.PropsWithChildren<ModalProps>) { }: React.PropsWithChildren<ModalProps>) {
useEscapeKey(hideWindow); const hideDialog = useDialogsStore(state => state.hideDialog);
useEscapeKey(hideDialog);
const handleCancel = () => { const handleCancel = () => {
hideWindow(); hideDialog();
onCancel?.(); onCancel?.();
}; };
@ -87,7 +85,7 @@ function Modal({
return; return;
} }
onSubmit?.(); onSubmit?.();
hideWindow(); hideDialog();
}; };
return ( 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', 'backdrop-blur-[3px] opacity-50')} />
<div <div
className={clsx('z-navigation', 'fixed top-0 left-0', 'w-full h-full', 'bg-prim-0 opacity-25')} className={clsx('z-navigation', 'fixed top-0 left-0', 'w-full h-full', 'bg-prim-0 opacity-25')}
onClick={hideWindow} onClick={hideDialog}
/> />
<div <div
className={clsx( className={clsx(

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import useWindowSize from '@/hooks/useWindowSize'; import useWindowSize from '@/hooks/useWindowSize';
import { useFitHeight } from '@/stores/appLayout';
/** Maximum width of the viewer. */ /** Maximum width of the viewer. */
const MAXIMUM_WIDTH = 1600; const MAXIMUM_WIDTH = 1600;
@ -25,10 +25,9 @@ interface PDFViewerProps {
*/ */
function PDFViewer({ file, offsetXpx, minWidth = MINIMUM_WIDTH }: PDFViewerProps) { function PDFViewer({ file, offsetXpx, minWidth = MINIMUM_WIDTH }: PDFViewerProps) {
const windowSize = useWindowSize(); const windowSize = useWindowSize();
const { calculateHeight } = useConceptOptions();
const pageWidth = Math.max(minWidth, Math.min((windowSize?.width ?? 0) - (offsetXpx ?? 0) - 10, MAXIMUM_WIDTH)); 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 }} />; return <embed src={`${file}#toolbar=0`} className='p-3' style={{ width: pageWidth, height: pageHeight }} />;
} }

View File

@ -29,7 +29,7 @@ function SubmitButton({ text = 'ОК', icon, disabled, loading, className, ...re
loading && 'cursor-progress', loading && 'cursor-progress',
className className
)} )}
disabled={disabled ?? loading} disabled={disabled || loading}
{...restProps} {...restProps}
> >
{icon ? <span>{icon}</span> : null} {icon ? <span>{icon}</span> : null}

View File

@ -5,7 +5,7 @@ import { ReactNode } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { ITooltip, Tooltip as TooltipImpl } from 'react-tooltip'; import { ITooltip, Tooltip as TooltipImpl } from 'react-tooltip';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { usePreferencesStore } from '@/stores/preferences';
export type { PlacesType } from 'react-tooltip'; export type { PlacesType } from 'react-tooltip';
@ -29,7 +29,7 @@ function Tooltip({
style, style,
...restProps ...restProps
}: TooltipProps) { }: TooltipProps) {
const { darkMode } = useConceptOptions(); const darkMode = usePreferencesStore(state => state.darkMode);
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return null; return null;
} }

View File

@ -1,11 +1,13 @@
import { urls } from '@/app/urls'; 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 { useConceptNavigation } from '@/context/NavigationContext';
import TextURL from '../ui/TextURL'; import TextURL from '../ui/TextURL';
function ExpectedAnonymous() { function ExpectedAnonymous() {
const { user, logout } = useAuth(); const { user } = useAuth();
const { logout } = useLogout();
const router = useConceptNavigation(); const router = useConceptNavigation();
function logoutAndRedirect() { function logoutAndRedirect() {

View File

@ -1,14 +1,14 @@
'use client'; 'use client';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/backend/auth/useAuth';
import Loader from '../ui/Loader'; import Loader from '../ui/Loader';
import TextURL from '../ui/TextURL'; import TextURL from '../ui/TextURL';
function RequireAuth({ children }: React.PropsWithChildren) { function RequireAuth({ children }: React.PropsWithChildren) {
const { user, loading } = useAuth(); const { user, isLoading } = useAuth();
if (loading) { if (isLoading) {
return <Loader key='auth-loader' />; return <Loader key='auth-loader' />;
} }
if (user) { if (user) {

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { DataCallback } from '@/backend/apiTransport'; import { DataCallback } from '@/backend/apiTransport';
import { useAuth } from '@/backend/auth/useAuth';
import { import {
deleteLibraryItem, deleteLibraryItem,
getAdminLibrary, getAdminLibrary,
@ -21,11 +22,9 @@ import { matchLibraryItem, matchLibraryItemLocation } from '@/models/libraryAPI'
import { ILibraryFilter } from '@/models/miscellaneous'; import { ILibraryFilter } from '@/models/miscellaneous';
import { IRSForm, IRSFormCloneData, IRSFormData } from '@/models/rsform'; import { IRSForm, IRSFormCloneData, IRSFormData } from '@/models/rsform';
import { RSFormLoader } from '@/models/RSFormLoader'; import { RSFormLoader } from '@/models/RSFormLoader';
import { usePreferencesStore } from '@/stores/preferences';
import { contextOutsideScope } from '@/utils/labels'; import { contextOutsideScope } from '@/utils/labels';
import { useAuth } from './AuthContext';
import { useConceptOptions } from './ConceptOptionsContext';
interface ILibraryContext { interface ILibraryContext {
items: ILibraryItem[]; items: ILibraryItem[];
templates: ILibraryItem[]; templates: ILibraryItem[];
@ -62,8 +61,8 @@ export const useLibrary = (): ILibraryContext => {
}; };
export const LibraryState = ({ children }: React.PropsWithChildren) => { export const LibraryState = ({ children }: React.PropsWithChildren) => {
const { user, loading: userLoading } = useAuth(); const { user, isLoading: userLoading } = useAuth();
const { adminMode } = useConceptOptions(); const adminMode = usePreferencesStore(state => state.adminMode);
const [items, setItems] = useState<ILibraryItem[]>([]); const [items, setItems] = useState<ILibraryItem[]>([]);
const [templates, setTemplates] = useState<ILibraryItem[]>([]); const [templates, setTemplates] = useState<ILibraryItem[]>([]);

View File

@ -3,6 +3,7 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { DataCallback } from '@/backend/apiTransport'; import { DataCallback } from '@/backend/apiTransport';
import { useAuth } from '@/backend/auth/useAuth';
import { import {
patchLibraryItem, patchLibraryItem,
patchSetAccessPolicy, patchSetAccessPolicy,
@ -38,7 +39,6 @@ import {
import { UserID } from '@/models/user'; import { UserID } from '@/models/user';
import { contextOutsideScope } from '@/utils/labels'; import { contextOutsideScope } from '@/utils/labels';
import { useAuth } from './AuthContext';
import { useGlobalOss } from './GlobalOssContext'; import { useGlobalOss } from './GlobalOssContext';
import { useLibrary } from './LibraryContext'; import { useLibrary } from './LibraryContext';

View File

@ -3,6 +3,7 @@
import { createContext, useCallback, useContext, useMemo, useState } from 'react'; import { createContext, useCallback, useContext, useMemo, useState } from 'react';
import { DataCallback } from '@/backend/apiTransport'; import { DataCallback } from '@/backend/apiTransport';
import { useAuth } from '@/backend/auth/useAuth';
import { import {
patchLibraryItem, patchLibraryItem,
patchSetAccessPolicy, patchSetAccessPolicy,
@ -50,7 +51,6 @@ import {
import { UserID } from '@/models/user'; import { UserID } from '@/models/user';
import { contextOutsideScope } from '@/utils/labels'; import { contextOutsideScope } from '@/utils/labels';
import { useAuth } from './AuthContext';
import { useGlobalOss } from './GlobalOssContext'; import { useGlobalOss } from './GlobalOssContext';
import { useLibrary } from './LibraryContext'; import { useLibrary } from './LibraryContext';

View File

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

View File

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

View File

@ -7,19 +7,21 @@ import { IconReset } from '@/components/Icons';
import PickSchema from '@/components/select/PickSchema'; import PickSchema from '@/components/select/PickSchema';
import Label from '@/components/ui/Label'; import Label from '@/components/ui/Label';
import MiniButton from '@/components/ui/MiniButton'; 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 { useLibrary } from '@/context/LibraryContext';
import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library'; 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 { sortItemsForOSS } from '@/models/ossAPI';
import { useDialogsStore } from '@/stores/dialogs';
interface DlgChangeInputSchemaProps extends Pick<ModalProps, 'hideWindow'> { export interface DlgChangeInputSchemaProps {
oss: IOperationSchema; oss: IOperationSchema;
target: IOperation; 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 [selected, setSelected] = useState<LibraryItemID | undefined>(target.result ?? undefined);
const library = useLibrary(); const library = useLibrary();
const sortedItems = sortItemsForOSS(oss, library.items); const sortedItems = sortItemsForOSS(oss, library.items);
@ -38,9 +40,8 @@ function DlgChangeInputSchema({ oss, hideWindow, target, onSubmit }: DlgChangeIn
overflowVisible overflowVisible
header='Выбор концептуальной схемы' header='Выбор концептуальной схемы'
submitText='Подтвердить выбор' submitText='Подтвердить выбор'
hideWindow={hideWindow}
canSubmit={isValid} canSubmit={isValid}
onSubmit={() => onSubmit(selected)} onSubmit={() => onSubmit(target.id, selected)}
className={clsx('w-[35rem]', 'pb-3 px-6 cc-column')} className={clsx('w-[35rem]', 'pb-3 px-6 cc-column')}
> >
<div className='flex justify-between gap-3 items-center'> <div className='flex justify-between gap-3 items-center'>

View File

@ -3,23 +3,25 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useState } from 'react'; import { useState } from 'react';
import { useAuth } from '@/backend/auth/useAuth';
import SelectLocationContext from '@/components/select/SelectLocationContext'; import SelectLocationContext from '@/components/select/SelectLocationContext';
import SelectLocationHead from '@/components/select/SelectLocationHead'; import SelectLocationHead from '@/components/select/SelectLocationHead';
import Label from '@/components/ui/Label'; 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 TextArea from '@/components/ui/TextArea';
import { useAuth } from '@/context/AuthContext';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
import { LocationHead } from '@/models/library'; import { LocationHead } from '@/models/library';
import { combineLocation, validateLocation } from '@/models/libraryAPI'; import { combineLocation, validateLocation } from '@/models/libraryAPI';
import { useDialogsStore } from '@/stores/dialogs';
import { limits } from '@/utils/constants'; import { limits } from '@/utils/constants';
interface DlgChangeLocationProps extends Pick<ModalProps, 'hideWindow'> { export interface DlgChangeLocationProps {
initial: string; initial: string;
onChangeLocation: (newLocation: string) => void; 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 { user } = useAuth();
const [head, setHead] = useState<LocationHead>(initial.substring(0, 2) as LocationHead); const [head, setHead] = useState<LocationHead>(initial.substring(0, 2) as LocationHead);
const [body, setBody] = useState<string>(initial.substring(3)); const [body, setBody] = useState<string>(initial.substring(3));
@ -40,7 +42,6 @@ function DlgChangeLocation({ hideWindow, initial, onChangeLocation }: DlgChangeL
header='Изменение расположения' header='Изменение расположения'
submitText='Переместить' submitText='Переместить'
submitInvalidTooltip={`Допустимы буквы, цифры, подчерк, пробел и "!". Сегмент пути не может начинаться и заканчиваться пробелом. Общая длина (с корнем) не должна превышать ${limits.location_len}`} submitInvalidTooltip={`Допустимы буквы, цифры, подчерк, пробел и "!". Сегмент пути не может начинаться и заканчиваться пробелом. Общая длина (с корнем) не должна превышать ${limits.location_len}`}
hideWindow={hideWindow}
canSubmit={isValid} canSubmit={isValid}
onSubmit={() => onChangeLocation(location)} onSubmit={() => onChangeLocation(location)}
className={clsx('w-[35rem]', 'pb-3 px-6 flex gap-3 h-[9rem]')} className={clsx('w-[35rem]', 'pb-3 px-6 flex gap-3 h-[9rem]')}

View File

@ -5,6 +5,7 @@ import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { urls } from '@/app/urls'; import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth';
import { VisibilityIcon } from '@/components/DomainIcons'; import { VisibilityIcon } from '@/components/DomainIcons';
import SelectAccessPolicy from '@/components/select/SelectAccessPolicy'; import SelectAccessPolicy from '@/components/select/SelectAccessPolicy';
import SelectLocationContext from '@/components/select/SelectLocationContext'; import SelectLocationContext from '@/components/select/SelectLocationContext';
@ -12,27 +13,31 @@ import SelectLocationHead from '@/components/select/SelectLocationHead';
import Checkbox from '@/components/ui/Checkbox'; import Checkbox from '@/components/ui/Checkbox';
import Label from '@/components/ui/Label'; import Label from '@/components/ui/Label';
import MiniButton from '@/components/ui/MiniButton'; 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 TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import { useAuth } from '@/context/AuthContext';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
import { AccessPolicy, ILibraryItem, LocationHead } from '@/models/library'; import { AccessPolicy, ILibraryItem, LocationHead } from '@/models/library';
import { cloneTitle, combineLocation, validateLocation } from '@/models/libraryAPI'; import { cloneTitle, combineLocation, validateLocation } from '@/models/libraryAPI';
import { ConstituentaID, IRSFormCloneData } from '@/models/rsform'; import { ConstituentaID, IRSFormCloneData } from '@/models/rsform';
import { useDialogsStore } from '@/stores/dialogs';
import { information } from '@/utils/labels'; import { information } from '@/utils/labels';
interface DlgCloneLibraryItemProps extends Pick<ModalProps, 'hideWindow'> { export interface DlgCloneLibraryItemProps {
base: ILibraryItem; base: ILibraryItem;
initialLocation: string; initialLocation: string;
selected: ConstituentaID[]; selected: ConstituentaID[];
totalCount: number; 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 router = useConceptNavigation();
const { user } = useAuth(); const { user } = useAuth();
const [title, setTitle] = useState(cloneTitle(base)); const [title, setTitle] = useState(cloneTitle(base));
const [alias, setAlias] = useState(base.alias); const [alias, setAlias] = useState(base.alias);
const [comment, setComment] = useState(base.comment); const [comment, setComment] = useState(base.comment);
@ -77,7 +82,6 @@ function DlgCloneLibraryItem({ hideWindow, base, initialLocation, selected, tota
return ( return (
<Modal <Modal
header='Создание копии концептуальной схемы' header='Создание копии концептуальной схемы'
hideWindow={hideWindow}
canSubmit={canSubmit} canSubmit={canSubmit}
submitText='Создать' submitText='Создать'
onSubmit={handleSubmit} onSubmit={handleSubmit}

View File

@ -1 +0,0 @@
export { default } from './DlgConstituentaTemplate';

View File

@ -2,20 +2,22 @@
import { useState } from 'react'; import { useState } from 'react';
import Modal, { ModalProps } from '@/components/ui/Modal'; import Modal from '@/components/ui/Modal';
import usePartialUpdate from '@/hooks/usePartialUpdate'; import usePartialUpdate from '@/hooks/usePartialUpdate';
import { CstType, ICstCreateData, IRSForm } from '@/models/rsform'; import { CstType, ICstCreateData, IRSForm } from '@/models/rsform';
import { generateAlias } from '@/models/rsformAPI'; import { generateAlias } from '@/models/rsformAPI';
import { useDialogsStore } from '@/stores/dialogs';
import FormCreateCst from './FormCreateCst'; import FormCreateCst from './FormCreateCst';
interface DlgCreateCstProps extends Pick<ModalProps, 'hideWindow'> { export interface DlgCreateCstProps {
initial?: ICstCreateData; initial?: ICstCreateData;
schema: IRSForm; schema: IRSForm;
onCreate: (data: ICstCreateData) => void; 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 [validated, setValidated] = useState(false);
const [cstData, updateCstData] = usePartialUpdate( const [cstData, updateCstData] = usePartialUpdate(
initial || { initial || {
@ -35,7 +37,6 @@ function DlgCreateCst({ hideWindow, initial, schema, onCreate }: DlgCreateCstPro
return ( return (
<Modal <Modal
header='Создание конституенты' header='Создание конституенты'
hideWindow={hideWindow}
canSubmit={validated} canSubmit={validated}
onSubmit={handleSubmit} onSubmit={handleSubmit}
submitText='Создать' submitText='Создать'

View File

@ -10,13 +10,13 @@ import { useLibrary } from '@/context/LibraryContext';
import { LibraryItemID } from '@/models/library'; import { LibraryItemID } from '@/models/library';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
import { IOperationCreateData, IOperationSchema, OperationID, OperationType } from '@/models/oss'; import { IOperationCreateData, IOperationSchema, OperationID, OperationType } from '@/models/oss';
import { useDialogsStore } from '@/stores/dialogs';
import { describeOperationType, labelOperationType } from '@/utils/labels'; import { describeOperationType, labelOperationType } from '@/utils/labels';
import TabInputOperation from './TabInputOperation'; import TabInputOperation from './TabInputOperation';
import TabSynthesisOperation from './TabSynthesisOperation'; import TabSynthesisOperation from './TabSynthesisOperation';
interface DlgCreateOperationProps { export interface DlgCreateOperationProps {
hideWindow: () => void;
oss: IOperationSchema; oss: IOperationSchema;
onCreate: (data: IOperationCreateData) => void; onCreate: (data: IOperationCreateData) => void;
initialInputs: OperationID[]; initialInputs: OperationID[];
@ -27,7 +27,8 @@ export enum TabID {
SYNTHESIS = 1 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 library = useLibrary();
const [activeTab, setActiveTab] = useState(initialInputs.length > 0 ? TabID.SYNTHESIS : TabID.INPUT); const [activeTab, setActiveTab] = useState(initialInputs.length > 0 ? TabID.SYNTHESIS : TabID.INPUT);
@ -98,7 +99,6 @@ function DlgCreateOperation({ hideWindow, oss, onCreate, initialInputs }: DlgCre
<Modal <Modal
header='Создание операции' header='Создание операции'
submitText='Создать' submitText='Создать'
hideWindow={hideWindow}
canSubmit={isValid} canSubmit={isValid}
onSubmit={handleSubmit} onSubmit={handleSubmit}
className='w-[40rem] px-6 h-[32rem]' className='w-[40rem] px-6 h-[32rem]'

View File

@ -4,21 +4,23 @@ import clsx from 'clsx';
import { useState } from 'react'; import { useState } from 'react';
import Checkbox from '@/components/ui/Checkbox'; 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 TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import { IVersionCreateData, IVersionInfo } from '@/models/library'; import { IVersionCreateData, IVersionInfo } from '@/models/library';
import { nextVersion } from '@/models/libraryAPI'; import { nextVersion } from '@/models/libraryAPI';
import { ConstituentaID } from '@/models/rsform'; import { ConstituentaID } from '@/models/rsform';
import { useDialogsStore } from '@/stores/dialogs';
interface DlgCreateVersionProps extends Pick<ModalProps, 'hideWindow'> { export interface DlgCreateVersionProps {
versions: IVersionInfo[]; versions: IVersionInfo[];
onCreate: (data: IVersionCreateData) => void; onCreate: (data: IVersionCreateData) => void;
selected: ConstituentaID[]; selected: ConstituentaID[];
totalCount: number; 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 [version, setVersion] = useState(versions.length > 0 ? nextVersion(versions[0].version) : '1.0.0');
const [description, setDescription] = useState(''); const [description, setDescription] = useState('');
const [onlySelected, setOnlySelected] = useState(false); const [onlySelected, setOnlySelected] = useState(false);
@ -39,7 +41,6 @@ function DlgCreateVersion({ hideWindow, versions, selected, totalCount, onCreate
return ( return (
<Modal <Modal
header='Создание версии' header='Создание версии'
hideWindow={hideWindow}
canSubmit={canSubmit} canSubmit={canSubmit}
onSubmit={handleSubmit} onSubmit={handleSubmit}
submitText='Создать' submitText='Создать'

View File

@ -4,7 +4,7 @@ import clsx from 'clsx';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { TabList, TabPanel, Tabs } from 'react-tabs'; 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 TabLabel from '@/components/ui/TabLabel';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
import usePartialUpdate from '@/hooks/usePartialUpdate'; import usePartialUpdate from '@/hooks/usePartialUpdate';
@ -12,13 +12,14 @@ import { HelpTopic } from '@/models/miscellaneous';
import { CstType, ICstCreateData, IRSForm } from '@/models/rsform'; import { CstType, ICstCreateData, IRSForm } from '@/models/rsform';
import { generateAlias, validateNewAlias } from '@/models/rsformAPI'; import { generateAlias, validateNewAlias } from '@/models/rsformAPI';
import { inferTemplatedType, substituteTemplateArgs } from '@/models/rslangAPI'; import { inferTemplatedType, substituteTemplateArgs } from '@/models/rslangAPI';
import { useDialogsStore } from '@/stores/dialogs';
import { prompts } from '@/utils/labels'; import { prompts } from '@/utils/labels';
import FormCreateCst from '../DlgCreateCst/FormCreateCst'; import FormCreateCst from '../DlgCreateCst/FormCreateCst';
import TabArguments, { IArgumentsState } from './TabArguments'; import TabArguments, { IArgumentsState } from './TabArguments';
import TabTemplate, { ITemplateState } from './TabTemplate'; import TabTemplate, { ITemplateState } from './TabTemplate';
interface DlgConstituentaTemplateProps extends Pick<ModalProps, 'hideWindow'> { export interface DlgCstTemplateProps {
schema: IRSForm; schema: IRSForm;
onCreate: (data: ICstCreateData) => void; onCreate: (data: ICstCreateData) => void;
insertAfter?: number; insertAfter?: number;
@ -30,7 +31,8 @@ export enum TabID {
CONSTITUENTA = 2 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 { retrieveTemplate } = useLibrary();
const [activeTab, setActiveTab] = useState(TabID.TEMPLATE); const [activeTab, setActiveTab] = useState(TabID.TEMPLATE);
@ -128,7 +130,6 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
header='Создание конституенты из шаблона' header='Создание конституенты из шаблона'
submitText='Создать' submitText='Создать'
className='w-[43rem] h-[35rem] px-6' className='w-[43rem] h-[35rem] px-6'
hideWindow={hideWindow}
canSubmit={validated} canSubmit={validated}
beforeSubmit={handlePrompt} beforeSubmit={handlePrompt}
onSubmit={handleSubmit} onSubmit={handleSubmit}
@ -164,4 +165,4 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
); );
} }
export default DlgConstituentaTemplate; export default DlgCstTemplate;

View File

@ -0,0 +1 @@
export { default } from './DlgCstTemplate';

View File

@ -4,19 +4,22 @@ import clsx from 'clsx';
import { useState } from 'react'; import { useState } from 'react';
import Checkbox from '@/components/ui/Checkbox'; 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 { ConstituentaID, IRSForm } from '@/models/rsform';
import { useDialogsStore } from '@/stores/dialogs';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
import ListConstituents from './ListConstituents'; import ListConstituents from './ListConstituents';
interface DlgDeleteCstProps extends Pick<ModalProps, 'hideWindow'> { export interface DlgDeleteCstProps {
schema: IRSForm; schema: IRSForm;
selected: ConstituentaID[]; selected: ConstituentaID[];
onDelete: (items: ConstituentaID[]) => void; 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 [expandOut, setExpandOut] = useState(false);
const expansion: ConstituentaID[] = schema.graph.expandAllOutputs(selected); const expansion: ConstituentaID[] = schema.graph.expandAllOutputs(selected);
const hasInherited = selected.some( const hasInherited = selected.some(
@ -25,7 +28,7 @@ function DlgDeleteCst({ hideWindow, selected, schema, onDelete }: DlgDeleteCstPr
); );
function handleSubmit() { function handleSubmit() {
hideWindow(); hideDialog();
if (expandOut) { if (expandOut) {
onDelete(selected.concat(expansion)); onDelete(selected.concat(expansion));
} else { } else {
@ -38,7 +41,6 @@ function DlgDeleteCst({ hideWindow, selected, schema, onDelete }: DlgDeleteCstPr
canSubmit canSubmit
header='Удаление конституент' header='Удаление конституент'
submitText={expandOut ? 'Удалить с зависимыми' : 'Удалить'} submitText={expandOut ? 'Удалить с зависимыми' : 'Удалить'}
hideWindow={hideWindow}
onSubmit={handleSubmit} onSubmit={handleSubmit}
className={clsx('cc-column', 'max-w-[60vw] min-w-[30rem]', 'px-6')} className={clsx('cc-column', 'max-w-[60vw] min-w-[30rem]', 'px-6')}
> >

View File

@ -4,22 +4,24 @@ import clsx from 'clsx';
import { useState } from 'react'; import { useState } from 'react';
import Checkbox from '@/components/ui/Checkbox'; 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 TextInput from '@/components/ui/TextInput';
import { HelpTopic } from '@/models/miscellaneous'; 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; 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 [keepConstituents, setKeepConstituents] = useState(false);
const [deleteSchema, setDeleteSchema] = useState(false); const [deleteSchema, setDeleteSchema] = useState(false);
function handleSubmit() { function handleSubmit() {
onSubmit(keepConstituents, deleteSchema); onSubmit(target.id, keepConstituents, deleteSchema);
} }
return ( return (
@ -27,7 +29,6 @@ function DlgDeleteOperation({ hideWindow, target, onSubmit }: DlgDeleteOperation
overflowVisible overflowVisible
header='Удаление операции' header='Удаление операции'
submitText='Подтвердить удаление' submitText='Подтвердить удаление'
hideWindow={hideWindow}
canSubmit={true} canSubmit={true}
onSubmit={handleSubmit} onSubmit={handleSubmit}
className={clsx('w-[35rem]', 'pb-3 px-6 cc-column', 'select-none')} className={clsx('w-[35rem]', 'pb-3 px-6 cc-column', 'select-none')}

View File

@ -3,26 +3,26 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useState } from 'react'; import { useState } from 'react';
import { useUsers } from '@/backend/users/useUsers';
import { IconRemove } from '@/components/Icons'; import { IconRemove } from '@/components/Icons';
import SelectUser from '@/components/select/SelectUser'; import SelectUser from '@/components/select/SelectUser';
import Label from '@/components/ui/Label'; import Label from '@/components/ui/Label';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import Modal from '@/components/ui/Modal'; import Modal from '@/components/ui/Modal';
import { useUsers } from '@/context/UsersContext';
import { UserID } from '@/models/user'; import { UserID } from '@/models/user';
import { useDialogsStore } from '@/stores/dialogs';
import TableUsers from './TableUsers'; import TableUsers from './TableUsers';
interface DlgEditEditorsProps { export interface DlgEditEditorsProps {
editors: UserID[]; editors: UserID[];
setEditors: (newValue: UserID[]) => void; 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 [selected, setSelected] = useState<UserID[]>(editors);
const { users } = useUsers(); const { users } = useUsers();
const filtered = users.filter(user => !selected.includes(user.id));
function handleSubmit() { function handleSubmit() {
setEditors(selected); setEditors(selected);
@ -41,7 +41,6 @@ function DlgEditEditors({ hideWindow, editors, setEditors }: DlgEditEditorsProps
canSubmit canSubmit
header='Список редакторов' header='Список редакторов'
submitText='Сохранить список' submitText='Сохранить список'
hideWindow={hideWindow}
className='flex flex-col w-[35rem] px-6 gap-3 pb-6' className='flex flex-col w-[35rem] px-6 gap-3 pb-6'
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
@ -61,7 +60,12 @@ function DlgEditEditors({ hideWindow, editors, setEditors }: DlgEditEditorsProps
<div className='flex items-center gap-3'> <div className='flex items-center gap-3'>
<Label text='Добавить' /> <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> </div>
</Modal> </Modal>
); );

View File

@ -19,13 +19,13 @@ import {
} from '@/models/oss'; } from '@/models/oss';
import { SubstitutionValidator } from '@/models/ossAPI'; import { SubstitutionValidator } from '@/models/ossAPI';
import { ConstituentaID } from '@/models/rsform'; import { ConstituentaID } from '@/models/rsform';
import { useDialogsStore } from '@/stores/dialogs';
import TabArguments from './TabArguments'; import TabArguments from './TabArguments';
import TabOperation from './TabOperation'; import TabOperation from './TabOperation';
import TabSynthesis from './TabSynthesis'; import TabSynthesis from './TabSynthesis';
interface DlgEditOperationProps { export interface DlgEditOperationProps {
hideWindow: () => void;
oss: IOperationSchema; oss: IOperationSchema;
target: IOperation; target: IOperation;
onSubmit: (data: IOperationUpdateData) => void; onSubmit: (data: IOperationUpdateData) => void;
@ -37,7 +37,8 @@ export enum TabID {
SUBSTITUTION = 2 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 [activeTab, setActiveTab] = useState(TabID.CARD);
const [alias, setAlias] = useState(target.alias); const [alias, setAlias] = useState(target.alias);
@ -142,7 +143,6 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
<Modal <Modal
header='Редактирование операции' header='Редактирование операции'
submitText='Сохранить' submitText='Сохранить'
hideWindow={hideWindow}
canSubmit={canSubmit} canSubmit={canSubmit}
onSubmit={handleSubmit} onSubmit={handleSubmit}
className='w-[40rem] px-6 h-[32rem]' className='w-[40rem] px-6 h-[32rem]'

View File

@ -9,6 +9,7 @@ import TabLabel from '@/components/ui/TabLabel';
import { ReferenceType } from '@/models/language'; import { ReferenceType } from '@/models/language';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
import { IRSForm } from '@/models/rsform'; import { IRSForm } from '@/models/rsform';
import { useDialogsStore } from '@/stores/dialogs';
import { labelReferenceType } from '@/utils/labels'; import { labelReferenceType } from '@/utils/labels';
import TabEntityReference from './TabEntityReference'; import TabEntityReference from './TabEntityReference';
@ -22,8 +23,7 @@ export interface IReferenceInputState {
basePosition: number; basePosition: number;
} }
interface DlgEditReferenceProps { export interface DlgEditReferenceProps {
hideWindow: () => void;
schema: IRSForm; schema: IRSForm;
initial: IReferenceInputState; initial: IReferenceInputState;
onSave: (newRef: string) => void; onSave: (newRef: string) => void;
@ -34,7 +34,8 @@ export enum TabID {
SYNTACTIC = 1 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 [activeTab, setActiveTab] = useState(initial.type === ReferenceType.ENTITY ? TabID.ENTITY : TabID.SYNTACTIC);
const [reference, setReference] = useState(''); const [reference, setReference] = useState('');
const [isValid, setIsValid] = useState(false); const [isValid, setIsValid] = useState(false);
@ -43,7 +44,6 @@ function DlgEditReference({ hideWindow, schema, initial, onSave }: DlgEditRefere
<Modal <Modal
header='Редактирование ссылки' header='Редактирование ссылки'
submitText='Сохранить ссылку' submitText='Сохранить ссылку'
hideWindow={hideWindow}
canSubmit={isValid} canSubmit={isValid}
onSubmit={() => onSave(reference)} onSubmit={() => onSave(reference)}
className='w-[40rem] px-6 h-[32rem]' className='w-[40rem] px-6 h-[32rem]'

View File

@ -7,21 +7,21 @@ import MiniButton from '@/components/ui/MiniButton';
import Modal from '@/components/ui/Modal'; import Modal from '@/components/ui/Modal';
import TextArea from '@/components/ui/TextArea'; import TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import { useRSForm } from '@/context/RSFormContext';
import { IVersionData, IVersionInfo, VersionID } from '@/models/library'; import { IVersionData, IVersionInfo, VersionID } from '@/models/library';
import { useDialogsStore } from '@/stores/dialogs';
import TableVersions from './TableVersions'; import TableVersions from './TableVersions';
interface DlgEditVersionsProps { export interface DlgEditVersionsProps {
hideWindow: () => void;
versions: IVersionInfo[]; versions: IVersionInfo[];
onDelete: (versionID: VersionID) => void; onDelete: (versionID: VersionID) => void;
onUpdate: (versionID: VersionID, data: IVersionData) => void; onUpdate: (versionID: VersionID, data: IVersionData) => void;
} }
function DlgEditVersions({ hideWindow, versions, onDelete, onUpdate }: DlgEditVersionsProps) { function DlgEditVersions() {
const { processing } = useRSForm(); const { versions, onDelete, onUpdate } = useDialogsStore(state => state.props as DlgEditVersionsProps);
const [selected, setSelected] = useState<IVersionInfo | undefined>(undefined); const [selected, setSelected] = useState<IVersionInfo | undefined>(undefined);
const processing = false; // TODO: fix processing hook and versions update
const [version, setVersion] = useState(''); const [version, setVersion] = useState('');
const [description, setDescription] = useState(''); const [description, setDescription] = useState('');
@ -54,12 +54,7 @@ function DlgEditVersions({ hideWindow, versions, onDelete, onUpdate }: DlgEditVe
}, [selected]); }, [selected]);
return ( return (
<Modal <Modal readonly header='Редактирование версий' className='flex flex-col w-[40rem] px-6 gap-3 pb-6'>
readonly
header='Редактирование версий'
hideWindow={hideWindow}
className='flex flex-col w-[40rem] px-6 gap-3 pb-6'
>
<TableVersions <TableVersions
processing={processing} processing={processing}
items={versions} items={versions}

View File

@ -14,18 +14,19 @@ import { Grammeme, ITextRequest, IWordForm, IWordFormPlain } from '@/models/lang
import { parseGrammemes, wordFormEquals } from '@/models/languageAPI'; import { parseGrammemes, wordFormEquals } from '@/models/languageAPI';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
import { IConstituenta, TermForm } from '@/models/rsform'; import { IConstituenta, TermForm } from '@/models/rsform';
import { useDialogsStore } from '@/stores/dialogs';
import { prompts } from '@/utils/labels'; import { prompts } from '@/utils/labels';
import { IGrammemeOption, SelectorGrammemes, SelectorGrammemesList } from '@/utils/selectors'; import { IGrammemeOption, SelectorGrammemes, SelectorGrammemesList } from '@/utils/selectors';
import TableWordForms from './TableWordForms'; import TableWordForms from './TableWordForms';
interface DlgEditWordFormsProps { export interface DlgEditWordFormsProps {
hideWindow: () => void;
target: IConstituenta; target: IConstituenta;
onSave: (data: TermForm[]) => void; 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 textProcessor = useConceptText();
const [term, setTerm] = useState(''); const [term, setTerm] = useState('');
@ -123,7 +124,6 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
<Modal <Modal
canSubmit canSubmit
header='Редактирование словоформ' header='Редактирование словоформ'
hideWindow={hideWindow}
submitText='Сохранить' submitText='Сохранить'
onSubmit={handleSubmit} onSubmit={handleSubmit}
className='flex flex-col w-[40rem] px-6' className='flex flex-col w-[40rem] px-6'

View File

@ -1,24 +1,25 @@
'use client'; 'use client';
import Checkbox from '@/components/ui/Checkbox'; 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 usePartialUpdate from '@/hooks/usePartialUpdate';
import { GraphFilterParams } from '@/models/miscellaneous'; import { GraphFilterParams } from '@/models/miscellaneous';
import { CstType } from '@/models/rsform'; import { CstType } from '@/models/rsform';
import { useDialogsStore } from '@/stores/dialogs';
import { labelCstType } from '@/utils/labels'; import { labelCstType } from '@/utils/labels';
interface DlgGraphParamsProps extends Pick<ModalProps, 'hideWindow'> { export interface DlgGraphParamsProps {
initial: GraphFilterParams; initial: GraphFilterParams;
onConfirm: (params: GraphFilterParams) => void; 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); const [params, updateParams] = usePartialUpdate(initial);
return ( return (
<Modal <Modal
canSubmit canSubmit
hideWindow={hideWindow}
header='Настройки графа термов' header='Настройки графа термов'
onSubmit={() => onConfirm(params)} onSubmit={() => onConfirm(params)}
submitText='Применить' submitText='Применить'

View File

@ -4,18 +4,19 @@ import clsx from 'clsx';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { TabList, TabPanel, Tabs } from 'react-tabs'; 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 TabLabel from '@/components/ui/TabLabel';
import useRSFormDetails from '@/hooks/useRSFormDetails'; import useRSFormDetails from '@/hooks/useRSFormDetails';
import { LibraryItemID } from '@/models/library'; import { LibraryItemID } from '@/models/library';
import { ICstSubstitute } from '@/models/oss'; import { ICstSubstitute } from '@/models/oss';
import { ConstituentaID, IInlineSynthesisData, IRSForm } from '@/models/rsform'; import { ConstituentaID, IInlineSynthesisData, IRSForm } from '@/models/rsform';
import { useDialogsStore } from '@/stores/dialogs';
import TabConstituents from './TabConstituents'; import TabConstituents from './TabConstituents';
import TabSchema from './TabSchema'; import TabSchema from './TabSchema';
import TabSubstitutions from './TabSubstitutions'; import TabSubstitutions from './TabSubstitutions';
interface DlgInlineSynthesisProps extends Pick<ModalProps, 'hideWindow'> { export interface DlgInlineSynthesisProps {
receiver: IRSForm; receiver: IRSForm;
onInlineSynthesis: (data: IInlineSynthesisData) => void; onInlineSynthesis: (data: IInlineSynthesisData) => void;
} }
@ -26,7 +27,8 @@ export enum TabID {
SUBSTITUTIONS = 2 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 [activeTab, setActiveTab] = useState(TabID.SCHEMA);
const [donorID, setDonorID] = useState<LibraryItemID | undefined>(undefined); const [donorID, setDonorID] = useState<LibraryItemID | undefined>(undefined);
@ -60,7 +62,6 @@ function DlgInlineSynthesis({ hideWindow, receiver, onInlineSynthesis }: DlgInli
header='Импорт концептуальной схем' header='Импорт концептуальной схем'
submitText='Добавить конституенты' submitText='Добавить конституенты'
className='w-[40rem] h-[33rem] px-6' className='w-[40rem] h-[33rem] px-6'
hideWindow={hideWindow}
canSubmit={validated} canSubmit={validated}
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >

View File

@ -7,7 +7,7 @@ import { RelocateUpIcon } from '@/components/DomainIcons';
import PickMultiConstituenta from '@/components/select/PickMultiConstituenta'; import PickMultiConstituenta from '@/components/select/PickMultiConstituenta';
import SelectLibraryItem from '@/components/select/SelectLibraryItem'; import SelectLibraryItem from '@/components/select/SelectLibraryItem';
import MiniButton from '@/components/ui/MiniButton'; 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 DataLoader from '@/components/wrap/DataLoader';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
import useRSFormDetails from '@/hooks/useRSFormDetails'; import useRSFormDetails from '@/hooks/useRSFormDetails';
@ -16,15 +16,17 @@ import { HelpTopic } from '@/models/miscellaneous';
import { ICstRelocateData, IOperation, IOperationSchema } from '@/models/oss'; import { ICstRelocateData, IOperation, IOperationSchema } from '@/models/oss';
import { getRelocateCandidates } from '@/models/ossAPI'; import { getRelocateCandidates } from '@/models/ossAPI';
import { ConstituentaID } from '@/models/rsform'; import { ConstituentaID } from '@/models/rsform';
import { useDialogsStore } from '@/stores/dialogs';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
interface DlgRelocateConstituentsProps extends Pick<ModalProps, 'hideWindow'> { export interface DlgRelocateConstituentsProps {
oss: IOperationSchema; oss: IOperationSchema;
initialTarget?: IOperation; initialTarget?: IOperation;
onSubmit: (data: ICstRelocateData) => void; 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 library = useLibrary();
const [directionUp, setDirectionUp] = useState(true); const [directionUp, setDirectionUp] = useState(true);
@ -88,7 +90,6 @@ function DlgRelocateConstituents({ oss, hideWindow, initialTarget, onSubmit }: D
<Modal <Modal
header='Перенос конституент' header='Перенос конституент'
submitText='Переместить' submitText='Переместить'
hideWindow={hideWindow}
canSubmit={isValid} canSubmit={isValid}
onSubmit={handleSubmit} onSubmit={handleSubmit}
className={clsx('w-[40rem] h-[33rem]', 'py-3 px-6')} className={clsx('w-[40rem] h-[33rem]', 'py-3 px-6')}

View File

@ -3,25 +3,26 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useState } from 'react'; 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 SelectSingle from '@/components/ui/SelectSingle';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import { useRSForm } from '@/context/RSFormContext';
import usePartialUpdate from '@/hooks/usePartialUpdate'; import usePartialUpdate from '@/hooks/usePartialUpdate';
import { HelpTopic } from '@/models/miscellaneous'; 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 { generateAlias, validateNewAlias } from '@/models/rsformAPI';
import { useDialogsStore } from '@/stores/dialogs';
import { labelCstType } from '@/utils/labels'; import { labelCstType } from '@/utils/labels';
import { SelectorCstType } from '@/utils/selectors'; import { SelectorCstType } from '@/utils/selectors';
interface DlgRenameCstProps extends Pick<ModalProps, 'hideWindow'> { export interface DlgRenameCstProps {
schema: IRSForm;
initial: ICstRenameData; initial: ICstRenameData;
allowChangeType: boolean; allowChangeType: boolean;
onRename: (data: ICstRenameData) => void; onRename: (data: ICstRenameData) => void;
} }
function DlgRenameCst({ hideWindow, initial, allowChangeType, onRename }: DlgRenameCstProps) { function DlgRenameCst() {
const { schema } = useRSForm(); const { schema, initial, allowChangeType, onRename } = useDialogsStore(state => state.props as DlgRenameCstProps);
const [validated, setValidated] = useState(false); const [validated, setValidated] = useState(false);
const [cstData, updateData] = usePartialUpdate(initial); const [cstData, updateData] = usePartialUpdate(initial);
@ -42,7 +43,6 @@ function DlgRenameCst({ hideWindow, initial, allowChangeType, onRename }: DlgRen
header='Переименование конституенты' header='Переименование конституенты'
submitText='Переименовать' submitText='Переименовать'
submitInvalidTooltip='Введите незанятое имя, соответствующее типу' submitInvalidTooltip='Введите незанятое имя, соответствующее типу'
hideWindow={hideWindow}
canSubmit={validated} canSubmit={validated}
onSubmit={() => onRename(cstData)} onSubmit={() => onRename(cstData)}
className={clsx('w-[30rem]', 'py-6 pr-3 pl-6 flex gap-3 justify-center items-center ')} className={clsx('w-[30rem]', 'py-6 pr-3 pl-6 flex gap-3 justify-center items-center ')}

View File

@ -3,19 +3,21 @@
import { useState } from 'react'; import { useState } from 'react';
import { ReactFlowProvider } from 'reactflow'; import { ReactFlowProvider } from 'reactflow';
import Modal, { ModalProps } from '@/components/ui/Modal'; import Modal from '@/components/ui/Modal';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
import { SyntaxTree } from '@/models/rslang'; import { SyntaxTree } from '@/models/rslang';
import { useDialogsStore } from '@/stores/dialogs';
import ASTFlow from './ASTFlow'; import ASTFlow from './ASTFlow';
interface DlgShowASTProps extends Pick<ModalProps, 'hideWindow'> { export interface DlgShowASTProps {
syntaxTree: SyntaxTree; syntaxTree: SyntaxTree;
expression: string; 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 [hoverID, setHoverID] = useState<number | undefined>(undefined);
const hoverNode = syntaxTree.find(node => node.uid === hoverID); const hoverNode = syntaxTree.find(node => node.uid === hoverID);
@ -24,7 +26,6 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
return ( return (
<Modal <Modal
readonly readonly
hideWindow={hideWindow}
className='flex flex-col justify-stretch w-[calc(100dvw-3rem)] h-[calc(100dvh-6rem)]' className='flex flex-col justify-stretch w-[calc(100dvw-3rem)] h-[calc(100dvh-6rem)]'
helpTopic={HelpTopic.UI_FORMULA_TREE} helpTopic={HelpTopic.UI_FORMULA_TREE}
> >

View File

@ -3,19 +3,17 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { QRCodeSVG } from 'qrcode.react'; 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; target: string;
} }
function DlgShowQR({ hideWindow, target }: DlgShowQRProps) { function DlgShowQR() {
const { target } = useDialogsStore(state => state.props as DlgShowQRProps);
return ( return (
<Modal <Modal readonly className={clsx('w-[30rem]', 'py-12 pr-3 pl-6 flex gap-3 justify-center items-center')}>
readonly
hideWindow={hideWindow}
className={clsx('w-[30rem]', 'py-12 pr-3 pl-6 flex gap-3 justify-center items-center')}
>
<div className='bg-[#ffffff] p-4 border'> <div className='bg-[#ffffff] p-4 border'>
<QRCodeSVG value={target} size={256} /> <QRCodeSVG value={target} size={256} />
</div> </div>

View File

@ -3,19 +3,22 @@
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { ReactFlowProvider } from 'reactflow'; import { ReactFlowProvider } from 'reactflow';
import Modal, { ModalProps } from '@/components/ui/Modal'; import Modal from '@/components/ui/Modal';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
import { ITypeInfo } from '@/models/rslang'; import { ITypeInfo } from '@/models/rslang';
import { TMGraph } from '@/models/TMGraph'; import { TMGraph } from '@/models/TMGraph';
import { useDialogsStore } from '@/stores/dialogs';
import { errors } from '@/utils/labels'; import { errors } from '@/utils/labels';
import MGraphFlow from './MGraphFlow'; import MGraphFlow from './MGraphFlow';
interface DlgShowTypeGraphProps extends Pick<ModalProps, 'hideWindow'> { export interface DlgShowTypeGraphProps {
items: ITypeInfo[]; 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 graph = (() => {
const result = new TMGraph(); const result = new TMGraph();
items.forEach(item => result.addConstituenta(item.alias, item.result, item.args)); 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) { if (graph.nodes.length === 0) {
toast.error(errors.typeStructureFailed); toast.error(errors.typeStructureFailed);
hideWindow(); hideDialog();
return null; return null;
} }
@ -32,7 +35,6 @@ function DlgShowTypeGraph({ hideWindow, items }: DlgShowTypeGraphProps) {
<Modal <Modal
header='Граф ступеней' header='Граф ступеней'
readonly readonly
hideWindow={hideWindow}
className='flex flex-col justify-stretch w-[calc(100dvw-3rem)] h-[calc(100dvh-6rem)]' className='flex flex-col justify-stretch w-[calc(100dvw-3rem)] h-[calc(100dvh-6rem)]'
helpTopic={HelpTopic.UI_TYPE_GRAPH} helpTopic={HelpTopic.UI_TYPE_GRAPH}
> >

View File

@ -4,18 +4,20 @@ import clsx from 'clsx';
import { useState } from 'react'; import { useState } from 'react';
import PickSubstitutions from '@/components/select/PickSubstitutions'; 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 { HelpTopic } from '@/models/miscellaneous';
import { ICstSubstitute, ICstSubstituteData } from '@/models/oss'; import { ICstSubstitute, ICstSubstituteData } from '@/models/oss';
import { IRSForm } from '@/models/rsform'; import { IRSForm } from '@/models/rsform';
import { useDialogsStore } from '@/stores/dialogs';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
interface DlgSubstituteCstProps extends Pick<ModalProps, 'hideWindow'> { export interface DlgSubstituteCstProps {
schema: IRSForm; schema: IRSForm;
onSubstitute: (data: ICstSubstituteData) => void; 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 [substitutions, setSubstitutions] = useState<ICstSubstitute[]>([]);
const canSubmit = substitutions.length > 0; const canSubmit = substitutions.length > 0;
@ -31,7 +33,6 @@ function DlgSubstituteCst({ hideWindow, onSubstitute, schema }: DlgSubstituteCst
header='Отождествление' header='Отождествление'
submitText='Отождествить' submitText='Отождествить'
submitInvalidTooltip='Выберите две различные конституенты' submitInvalidTooltip='Выберите две различные конституенты'
hideWindow={hideWindow}
canSubmit={canSubmit} canSubmit={canSubmit}
onSubmit={handleSubmit} onSubmit={handleSubmit}
className={clsx('w-[40rem]', 'px-6 pb-3')} className={clsx('w-[40rem]', 'px-6 pb-3')}

View File

@ -6,16 +6,16 @@ import { toast } from 'react-toastify';
import Checkbox from '@/components/ui/Checkbox'; import Checkbox from '@/components/ui/Checkbox';
import FileInput from '@/components/ui/FileInput'; import FileInput from '@/components/ui/FileInput';
import Modal from '@/components/ui/Modal'; import Modal from '@/components/ui/Modal';
import { useRSForm } from '@/context/RSFormContext';
import { IRSFormUploadData } from '@/models/rsform'; import { IRSFormUploadData } from '@/models/rsform';
import { useDialogsStore } from '@/stores/dialogs';
import { EXTEOR_TRS_FILE } from '@/utils/constants'; import { EXTEOR_TRS_FILE } from '@/utils/constants';
interface DlgUploadRSFormProps { export interface DlgUploadRSFormProps {
hideWindow: () => void; upload: (data: IRSFormUploadData, callback: () => void) => void;
} }
function DlgUploadRSForm({ hideWindow }: DlgUploadRSFormProps) { function DlgUploadRSForm() {
const { upload } = useRSForm(); const { upload } = useDialogsStore(state => state.props as DlgUploadRSFormProps);
const [loadMetadata, setLoadMetadata] = useState(false); const [loadMetadata, setLoadMetadata] = useState(false);
const [file, setFile] = useState<File | undefined>(); const [file, setFile] = useState<File | undefined>();
@ -42,7 +42,6 @@ function DlgUploadRSForm({ hideWindow }: DlgUploadRSFormProps) {
return ( return (
<Modal <Modal
header='Импорт схемы из Экстеора' header='Импорт схемы из Экстеора'
hideWindow={hideWindow}
canSubmit={!!file} canSubmit={!!file}
onSubmit={handleSubmit} onSubmit={handleSubmit}
submitText='Загрузить' submitText='Загрузить'

View File

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

View File

@ -2,15 +2,15 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useAuth } from '@/backend/auth/useAuth';
import { getOssDetails } from '@/backend/oss'; import { getOssDetails } from '@/backend/oss';
import { type ErrorData } from '@/components/info/InfoError'; import { type ErrorData } from '@/components/info/InfoError';
import { useAuth } from '@/context/AuthContext';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
import { IOperationSchema, IOperationSchemaData } from '@/models/oss'; import { IOperationSchema, IOperationSchemaData } from '@/models/oss';
import { OssLoader } from '@/models/OssLoader'; import { OssLoader } from '@/models/OssLoader';
function useOssDetails({ target }: { target?: string }) { function useOssDetails({ target }: { target?: string }) {
const { loading: userLoading } = useAuth(); const { isLoading: userLoading } = useAuth();
const library = useLibrary(); const library = useLibrary();
const [schema, setInner] = useState<IOperationSchema | undefined>(undefined); const [schema, setInner] = useState<IOperationSchema | undefined>(undefined);
const [loading, setLoading] = useState(target != undefined); const [loading, setLoading] = useState(target != undefined);

View File

@ -2,14 +2,14 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useAuth } from '@/backend/auth/useAuth';
import { getRSFormDetails } from '@/backend/rsforms'; import { getRSFormDetails } from '@/backend/rsforms';
import { type ErrorData } from '@/components/info/InfoError'; import { type ErrorData } from '@/components/info/InfoError';
import { useAuth } from '@/context/AuthContext';
import { IRSForm, IRSFormData } from '@/models/rsform'; import { IRSForm, IRSFormData } from '@/models/rsform';
import { RSFormLoader } from '@/models/RSFormLoader'; import { RSFormLoader } from '@/models/RSFormLoader';
function useRSFormDetails({ target, version }: { target?: string; version?: string }) { function useRSFormDetails({ target, version }: { target?: string; version?: string }) {
const { loading: userLoading } = useAuth(); const { isLoading: userLoading } = useAuth();
const [schema, setInnerSchema] = useState<IRSForm | undefined>(undefined); const [schema, setInnerSchema] = useState<IRSForm | undefined>(undefined);
const [loading, setLoading] = useState(target != undefined); const [loading, setLoading] = useState(target != undefined);
const [error, setError] = useState<ErrorData>(undefined); const [error, setError] = useState<ErrorData>(undefined);

View File

@ -224,3 +224,32 @@ export interface Position2D {
x: number; x: number;
y: 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
}

View File

@ -26,35 +26,9 @@ export interface IUser {
* Represents CurrentUser information. * Represents CurrentUser information.
*/ */
export interface ICurrentUser extends Pick<IUser, 'id' | 'username' | 'is_staff'> { export interface ICurrentUser extends Pick<IUser, 'id' | 'username' | 'is_staff'> {
subscriptions: LibraryItemID[];
editor: 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. * Represents signup data, used to create new users.
*/ */
@ -63,11 +37,6 @@ export interface IUserSignupData extends Omit<IUser, 'is_staff' | 'id'> {
password2: string; 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}. * 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'> {} 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}. * Represents target {@link User}.
*/ */
@ -103,7 +64,7 @@ export interface ITargetUsers {
/** /**
* Represents user access mode. * Represents user access mode.
*/ */
export enum UserLevel { export enum UserRole {
READER = 0, READER = 0,
EDITOR, EDITOR,
OWNER, OWNER,

View File

@ -5,6 +5,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { urls } from '@/app/urls'; import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth';
import { VisibilityIcon } from '@/components/DomainIcons'; import { VisibilityIcon } from '@/components/DomainIcons';
import { IconDownload } from '@/components/Icons'; import { IconDownload } from '@/components/Icons';
import InfoError from '@/components/info/InfoError'; import InfoError from '@/components/info/InfoError';
@ -19,22 +20,23 @@ import Overlay from '@/components/ui/Overlay';
import SubmitButton from '@/components/ui/SubmitButton'; import SubmitButton from '@/components/ui/SubmitButton';
import TextArea from '@/components/ui/TextArea'; import TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import { useAuth } from '@/context/AuthContext';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library'; import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
import { ILibraryCreateData } from '@/models/library'; import { ILibraryCreateData } from '@/models/library';
import { combineLocation, validateLocation } from '@/models/libraryAPI'; import { combineLocation, validateLocation } from '@/models/libraryAPI';
import { useLibrarySearchStore } from '@/stores/librarySearch';
import { EXTEOR_TRS_FILE } from '@/utils/constants'; import { EXTEOR_TRS_FILE } from '@/utils/constants';
import { information } from '@/utils/labels'; import { information } from '@/utils/labels';
function FormCreateItem() { function FormCreateItem() {
const router = useConceptNavigation(); const router = useConceptNavigation();
const options = useConceptOptions();
const { user } = useAuth(); const { user } = useAuth();
const { createItem, processingError, setProcessingError, processing, folders } = useLibrary(); 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 [itemType, setItemType] = useState(LibraryItemType.RSFORM);
const [title, setTitle] = useState(''); const [title, setTitle] = useState('');
const [alias, setAlias] = useState(''); const [alias, setAlias] = useState('');
@ -81,7 +83,7 @@ function FormCreateItem() {
file: file, file: file,
fileName: file?.name fileName: file?.name
}; };
options.setLocation(location); setSearchLocation(location);
createItem(data, newItem => { createItem(data, newItem => {
toast.success(information.newLibraryItem); toast.success(information.newLibraryItem);
if (itemType == LibraryItemType.RSFORM) { if (itemType == LibraryItemType.RSFORM) {
@ -108,11 +110,11 @@ function FormCreateItem() {
}, []); }, []);
useEffect(() => { useEffect(() => {
if (!options.location) { if (!searchLocation) {
return; return;
} }
handleSelectLocation(options.location); handleSelectLocation(searchLocation);
}, [options.location, handleSelectLocation]); }, [searchLocation, handleSelectLocation]);
useEffect(() => { useEffect(() => {
if (itemType !== LibraryItemType.RSFORM) { if (itemType !== LibraryItemType.RSFORM) {

View File

@ -3,18 +3,18 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch'; import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useAppLayoutStore, useFitHeight } from '@/stores/appLayout';
import { resources } from '@/utils/constants'; import { resources } from '@/utils/constants';
function DatabaseSchemaPage() { function DatabaseSchemaPage() {
const { calculateHeight, setNoFooter } = useConceptOptions(); const hideFooter = useAppLayoutStore(state => state.hideFooter);
const panelHeight = calculateHeight('0px'); const panelHeight = useFitHeight('0px');
useEffect(() => { useEffect(() => {
setNoFooter(true); hideFooter(true);
return () => setNoFooter(false); return () => hideFooter(false);
}, [setNoFooter]); }, [hideFooter]);
return ( return (
<div className='cc-fade-in flex justify-center overflow-hidden' style={{ maxHeight: panelHeight }}> <div className='cc-fade-in flex justify-center overflow-hidden' style={{ maxHeight: panelHeight }}>

View File

@ -1,17 +1,17 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { urls } from '@/app/urls'; import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth';
import Loader from '@/components/ui/Loader'; import Loader from '@/components/ui/Loader';
import { useAuth } from '@/context/AuthContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
function HomePage() { function HomePage() {
const router = useConceptNavigation(); const router = useConceptNavigation();
const { user, loading } = useAuth(); const { user, isLoading } = useAuth();
useEffect(() => { useEffect(() => {
if (!loading) { if (!isLoading) {
if (!user) { if (!user) {
setTimeout(() => { setTimeout(() => {
router.replace(urls.manuals); router.replace(urls.manuals);
@ -22,7 +22,7 @@ function HomePage() {
}, PARAMETER.refreshTimeout); }, PARAMETER.refreshTimeout);
} }
} }
}, [router, user, loading]); }, [router, user, isLoading]);
return <Loader />; return <Loader />;
} }

View File

@ -1,24 +1,19 @@
'use client'; 'use client';
import fileDownload from 'js-file-download'; import fileDownload from 'js-file-download';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { IconCSV } from '@/components/Icons'; import { IconCSV } from '@/components/Icons';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import DataLoader from '@/components/wrap/DataLoader'; import DataLoader from '@/components/wrap/DataLoader';
import { useAuth } from '@/context/AuthContext';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
import DlgChangeLocation from '@/dialogs/DlgChangeLocation'; import { IRenameLocationData } from '@/models/library';
import useLocalStorage from '@/hooks/useLocalStorage'; import { useAppLayoutStore } from '@/stores/appLayout';
import { ILibraryItem, IRenameLocationData, LocationHead } from '@/models/library'; import { useDialogsStore } from '@/stores/dialogs';
import { ILibraryFilter } from '@/models/miscellaneous'; import { useLibraryFilter, useLibrarySearchStore } from '@/stores/librarySearch';
import { UserID } from '@/models/user';
import { storage } from '@/utils/constants';
import { information } from '@/utils/labels'; import { information } from '@/utils/labels';
import { convertToCSV, toggleTristateFlag } from '@/utils/utils'; import { convertToCSV } from '@/utils/utils';
import TableLibraryItems from './TableLibraryItems'; import TableLibraryItems from './TableLibraryItems';
import ToolbarSearch from './ToolbarSearch'; import ToolbarSearch from './ToolbarSearch';
@ -26,91 +21,29 @@ import ViewSideLocation from './ViewSideLocation';
function LibraryPage() { function LibraryPage() {
const library = useLibrary(); const library = useLibrary();
const { user } = useAuth(); const noNavigation = useAppLayoutStore(state => state.noNavigation);
const [items, setItems] = useState<ILibraryItem[]>([]);
const options = useConceptOptions();
const [query, setQuery] = useState(''); const folderMode = useLibrarySearchStore(state => state.folderMode);
const [path, setPath] = useState(''); const location = useLibrarySearchStore(state => state.location);
const setLocation = useLibrarySearchStore(state => state.setLocation);
const [head, setHead] = useLocalStorage<LocationHead | undefined>(storage.librarySearchHead, undefined); const filter = useLibraryFilter();
const [subfolders, setSubfolders] = useLocalStorage<boolean>(storage.librarySearchSubfolders, false); const items = library.applyFilter(filter);
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: ILibraryFilter = useMemo( const showChangeLocation = useDialogsStore(state => state.showChangeLocation);
() => ({
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 hasCustomFilter = function handleRenameLocation(newLocation: string) {
!!filter.path || const data: IRenameLocationData = {
!!filter.query || target: location,
filter.head !== undefined || new_location: newLocation
filter.isEditor !== undefined || };
filter.isOwned !== undefined || library.renameLocation(data, () => {
filter.isVisible !== true || setLocation(newLocation);
filter.filterUser !== undefined || toast.success(information.locationRenamed);
!!filter.location; });
}
useEffect(() => { function handleDownloadCSV() {
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(() => {
if (items.length === 0) { if (items.length === 0) {
toast.error(information.noDataToExport); toast.error(information.noDataToExport);
return; return;
@ -121,19 +54,12 @@ function LibraryPage() {
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
}, [items]); }
return ( return (
<DataLoader isLoading={library.loading} error={library.loadingError} hasNoData={library.items.length === 0}> <DataLoader isLoading={library.loading} error={library.loadingError} hasNoData={library.items.length === 0}>
{showRenameLocation ? (
<DlgChangeLocation
initial={options.location}
onChangeLocation={handleRenameLocation}
hideWindow={() => setShowRenameLocation(false)}
/>
) : null}
<Overlay <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' layer='z-tooltip'
className='cc-animate-position' className='cc-animate-position'
> >
@ -143,47 +69,16 @@ function LibraryPage() {
onClick={handleDownloadCSV} onClick={handleDownloadCSV}
/> />
</Overlay> </Overlay>
<ToolbarSearch <ToolbarSearch total={library.items.length ?? 0} filtered={items.length} />
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}
/>
<div className='cc-fade-in flex'> <div className='cc-fade-in flex'>
<ViewSideLocation <ViewSideLocation
isVisible={options.folderMode} isVisible={folderMode}
activeLocation={options.location}
onChangeActiveLocation={options.setLocation}
subfolders={subfolders}
folderTree={library.folders} folderTree={library.folders}
toggleFolderMode={toggleFolderMode} onRenameLocation={() => showChangeLocation({ initial: location, onChangeLocation: handleRenameLocation })}
toggleSubfolders={() => setSubfolders(prev => !prev)}
onRenameLocation={() => setShowRenameLocation(true)}
/> />
<TableLibraryItems <TableLibraryItems items={items} />
resetQuery={resetFilter}
items={items}
folderMode={options.folderMode}
toggleFolderMode={toggleFolderMode}
/>
</div> </div>
</DataLoader> </DataLoader>
); );

View File

@ -5,6 +5,7 @@ import { useLayoutEffect, useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { urls } from '@/app/urls'; import { urls } from '@/app/urls';
import { useLabelUser } from '@/backend/users/useLabelUser';
import { IconFolderTree } from '@/components/Icons'; import { IconFolderTree } from '@/components/Icons';
import BadgeLocation from '@/components/info/BadgeLocation'; import BadgeLocation from '@/components/info/BadgeLocation';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
@ -12,30 +13,31 @@ import DataTable, { createColumnHelper, IConditionalStyle, VisibilityState } fro
import FlexColumn from '@/components/ui/FlexColumn'; import FlexColumn from '@/components/ui/FlexColumn';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import TextURL from '@/components/ui/TextURL'; import TextURL from '@/components/ui/TextURL';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
import { useUsers } from '@/context/UsersContext';
import useLocalStorage from '@/hooks/useLocalStorage';
import useWindowSize from '@/hooks/useWindowSize'; import useWindowSize from '@/hooks/useWindowSize';
import { ILibraryItem, LibraryItemType } from '@/models/library'; 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 { APP_COLORS } from '@/styling/color';
import { storage } from '@/utils/constants';
interface TableLibraryItemsProps { interface TableLibraryItemsProps {
items: ILibraryItem[]; items: ILibraryItem[];
resetQuery: () => void;
folderMode: boolean;
toggleFolderMode: () => void;
} }
const columnHelper = createColumnHelper<ILibraryItem>(); const columnHelper = createColumnHelper<ILibraryItem>();
function TableLibraryItems({ items, resetQuery, folderMode, toggleFolderMode }: TableLibraryItemsProps) { function TableLibraryItems({ items }: TableLibraryItemsProps) {
const router = useConceptNavigation(); const router = useConceptNavigation();
const intl = useIntl(); const intl = useIntl();
const { getUserLabel } = useUsers(); const getUserLabel = useLabelUser();
const { calculateHeight } = useConceptOptions();
const [itemsPerPage, setItemsPerPage] = useLocalStorage<number>(storage.libraryPagination, 50); 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) { function handleOpenItem(item: ILibraryItem, event: CProps.EventMouse) {
const selection = window.getSelection(); 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>[] = [ const conditionalRowStyles: IConditionalStyle<ILibraryItem>[] = [
{ {
@ -164,7 +166,7 @@ function TableLibraryItems({ items, resetQuery, folderMode, toggleFolderMode }:
<p>Список схем пуст</p> <p>Список схем пуст</p>
<p className='flex gap-6'> <p className='flex gap-6'>
<TextURL text='Создать схему' href='/library/create' /> <TextURL text='Создать схему' href='/library/create' />
<TextURL text='Очистить фильтр' onClick={resetQuery} /> <TextURL text='Очистить фильтр' onClick={resetFilter} />
</p> </p>
</FlexColumn> </FlexColumn>
} }

View File

@ -19,10 +19,9 @@ import DropdownButton from '@/components/ui/DropdownButton';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import SearchBar from '@/components/ui/SearchBar'; import SearchBar from '@/components/ui/SearchBar';
import SelectorButton from '@/components/ui/SelectorButton'; import SelectorButton from '@/components/ui/SelectorButton';
import { useUsers } from '@/context/UsersContext';
import useDropdown from '@/hooks/useDropdown'; import useDropdown from '@/hooks/useDropdown';
import { LocationHead } from '@/models/library'; import { LocationHead } from '@/models/library';
import { UserID } from '@/models/user'; import { useHasCustomFilter, useLibrarySearchStore } from '@/stores/librarySearch';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
import { describeLocationHead, labelLocationHead } from '@/utils/labels'; import { describeLocationHead, labelLocationHead } from '@/utils/labels';
import { tripleToggleColor } from '@/utils/utils'; import { tripleToggleColor } from '@/utils/utils';
@ -30,65 +29,37 @@ import { tripleToggleColor } from '@/utils/utils';
interface ToolbarSearchProps { interface ToolbarSearchProps {
total: number; total: number;
filtered: 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({ function ToolbarSearch({ total, filtered }: ToolbarSearchProps) {
total,
filtered,
hasCustomFilter,
query,
onChangeQuery,
path,
onChangePath,
head,
onChangeHead,
folderMode,
toggleFolderMode,
isVisible,
toggleVisible,
isOwned,
toggleOwned,
isEditor,
toggleEditor,
filterUser,
onChangeFilterUser,
resetFilter
}: ToolbarSearchProps) {
const headMenu = useDropdown(); const headMenu = useDropdown();
const userMenu = 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; const userActive = isOwned !== undefined || isEditor !== undefined || filterUser !== undefined;
function handleChange(newValue: LocationHead | undefined) { function handleChange(newValue: LocationHead | undefined) {
headMenu.hide(); headMenu.hide();
onChangeHead(newValue); setHead(newValue);
} }
function handleToggleFolder() { function handleToggleFolder() {
@ -155,9 +126,8 @@ function ToolbarSearch({
noBorder noBorder
placeholder='Выберите владельца' placeholder='Выберите владельца'
className='min-w-[15rem] text-sm mx-1 mb-1' className='min-w-[15rem] text-sm mx-1 mb-1'
items={users}
value={filterUser} value={filterUser}
onSelectValue={onChangeFilterUser} onSelectValue={setFilterUser}
/> />
</Dropdown> </Dropdown>
</div> </div>
@ -177,7 +147,7 @@ function ToolbarSearch({
noBorder noBorder
className={clsx('min-w-[7rem] sm:min-w-[10rem] max-w-[20rem]', folderMode && 'flex-grow')} className={clsx('min-w-[7rem] sm:min-w-[10rem] max-w-[20rem]', folderMode && 'flex-grow')}
query={query} query={query}
onChangeQuery={onChangeQuery} onChangeQuery={setQuery}
/> />
{!folderMode ? ( {!folderMode ? (
<div ref={headMenu.ref} className='flex items-center h-full py-1 select-none'> <div ref={headMenu.ref} className='flex items-center h-full py-1 select-none'>
@ -236,7 +206,7 @@ function ToolbarSearch({
noBorder noBorder
className='w-[4.5rem] sm:w-[5rem] flex-grow' className='w-[4.5rem] sm:w-[5rem] flex-grow'
query={path} query={path}
onChangeQuery={onChangePath} onChangeQuery={setPath}
/> />
) : null} ) : null}
</div> </div>

View File

@ -1,62 +1,52 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useAuth } from '@/backend/auth/useAuth';
import { SubfoldersIcon } from '@/components/DomainIcons'; import { SubfoldersIcon } from '@/components/DomainIcons';
import { IconFolderEdit, IconFolderTree } from '@/components/Icons'; import { IconFolderEdit, IconFolderTree } from '@/components/Icons';
import BadgeHelp from '@/components/info/BadgeHelp'; import BadgeHelp from '@/components/info/BadgeHelp';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import SelectLocation from '@/components/select/SelectLocation'; import SelectLocation from '@/components/select/SelectLocation';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import { useAuth } from '@/context/AuthContext';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
import useWindowSize from '@/hooks/useWindowSize'; import useWindowSize from '@/hooks/useWindowSize';
import { FolderNode, FolderTree } from '@/models/FolderTree'; import { FolderNode, FolderTree } from '@/models/FolderTree';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
import { useFitHeight } from '@/stores/appLayout';
import { useLibrarySearchStore } from '@/stores/librarySearch';
import { PARAMETER, prefixes } from '@/utils/constants'; import { PARAMETER, prefixes } from '@/utils/constants';
import { information } from '@/utils/labels'; import { information } from '@/utils/labels';
interface ViewSideLocationProps { interface ViewSideLocationProps {
folderTree: FolderTree; folderTree: FolderTree;
isVisible: boolean; isVisible: boolean;
subfolders: boolean;
activeLocation: string;
onChangeActiveLocation: (newValue: string) => void;
toggleFolderMode: () => void;
toggleSubfolders: () => void;
onRenameLocation: () => void; onRenameLocation: () => void;
} }
function ViewSideLocation({ function ViewSideLocation({ folderTree, isVisible, onRenameLocation }: ViewSideLocationProps) {
folderTree,
activeLocation,
subfolders,
isVisible,
onChangeActiveLocation,
toggleFolderMode,
toggleSubfolders,
onRenameLocation
}: ViewSideLocationProps) {
const { user } = useAuth(); const { user } = useAuth();
const { items } = useLibrary(); const { items } = useLibrary();
const { calculateHeight } = useConceptOptions();
const windowSize = useWindowSize(); 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 = (() => { const canRename = (() => {
if (activeLocation.length <= 3 || !user) { if (location.length <= 3 || !user) {
return false; return false;
} }
if (user.is_staff) { if (user.is_staff) {
return true; return true;
} }
const owned = items.filter(item => item.owner == user.id); const owned = items.filter(item => item.owner == user.id);
const located = owned.filter( const located = owned.filter(item => item.location == location || item.location.startsWith(`${location}/`));
item => item.location == activeLocation || item.location.startsWith(`${activeLocation}/`)
);
return located.length !== 0; return located.length !== 0;
})(); })();
const maxHeight = calculateHeight('4.5rem'); const maxHeight = useFitHeight('4.5rem');
function handleClickFolder(event: CProps.EventMouse, target: FolderNode) { function handleClickFolder(event: CProps.EventMouse, target: FolderNode) {
event.preventDefault(); event.preventDefault();
@ -67,7 +57,7 @@ function ViewSideLocation({
.then(() => toast.success(information.pathReady)) .then(() => toast.success(information.pathReady))
.catch(console.error); .catch(console.error);
} else { } else {
onChangeActiveLocation(target.getPath()); setLocation(target.getPath());
} }
} }
@ -98,7 +88,7 @@ function ViewSideLocation({
onClick={onRenameLocation} onClick={onRenameLocation}
/> />
) : null} ) : null}
{!!activeLocation ? ( {!!location ? (
<MiniButton <MiniButton
title={subfolders ? 'Вложенные папки: Вкл' : 'Вложенные папки: Выкл'} // prettier: split-lines title={subfolders ? 'Вложенные папки: Вкл' : 'Вложенные папки: Выкл'} // prettier: split-lines
icon={<SubfoldersIcon value={subfolders} />} icon={<SubfoldersIcon value={subfolders} />}
@ -113,7 +103,7 @@ function ViewSideLocation({
</div> </div>
</div> </div>
<SelectLocation <SelectLocation
value={activeLocation} value={location}
folderTree={folderTree} folderTree={folderTree}
prefix={prefixes.folders_list} prefix={prefixes.folders_list}
onClick={handleClickFolder} onClick={handleClickFolder}

View File

@ -5,15 +5,15 @@ import clsx from 'clsx';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { urls } from '@/app/urls'; 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 InfoError, { ErrorData } from '@/components/info/InfoError';
import SubmitButton from '@/components/ui/SubmitButton'; import SubmitButton from '@/components/ui/SubmitButton';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import TextURL from '@/components/ui/TextURL'; import TextURL from '@/components/ui/TextURL';
import ExpectedAnonymous from '@/components/wrap/ExpectedAnonymous'; import ExpectedAnonymous from '@/components/wrap/ExpectedAnonymous';
import { useAuth } from '@/context/AuthContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
import useQueryStrings from '@/hooks/useQueryStrings'; import useQueryStrings from '@/hooks/useQueryStrings';
import { IUserLoginData } from '@/models/user';
import { resources } from '@/utils/constants'; import { resources } from '@/utils/constants';
function LoginPage() { function LoginPage() {
@ -21,23 +21,20 @@ function LoginPage() {
const query = useQueryStrings(); const query = useQueryStrings();
const userQuery = query.get('username'); 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 [username, setUsername] = useState(userQuery || '');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
useEffect(() => { useEffect(() => {
setError(undefined); reset();
}, [username, password, setError]); }, [username, password, reset]);
function handleSubmit(event: React.FormEvent<HTMLFormElement>) { function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault(); event.preventDefault();
if (!loading) { if (!isPending) {
const data: IUserLoginData = { login(username, password, () => {
username: username,
password: password
};
login(data, () => {
if (router.canBack()) { if (router.canBack()) {
router.back(); router.back();
} else { } else {
@ -78,7 +75,7 @@ function LoginPage() {
<SubmitButton <SubmitButton
text='Войти' text='Войти'
className='self-center w-[12rem] mt-3' className='self-center w-[12rem] mt-3'
loading={loading} loading={isPending}
disabled={!username || !password} disabled={!username || !password}
/> />
<div className='flex flex-col text-sm'> <div className='flex flex-col text-sm'>

View File

@ -3,10 +3,10 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { urls } from '@/app/urls'; import { urls } from '@/app/urls';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
import useQueryStrings from '@/hooks/useQueryStrings'; import useQueryStrings from '@/hooks/useQueryStrings';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
import { useMainHeight } from '@/stores/appLayout';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import TopicsList from './TopicsList'; import TopicsList from './TopicsList';
@ -17,7 +17,7 @@ function ManualsPage() {
const query = useQueryStrings(); const query = useQueryStrings();
const activeTopic = (query.get('topic') || HelpTopic.MAIN) as HelpTopic; const activeTopic = (query.get('topic') || HelpTopic.MAIN) as HelpTopic;
const { mainHeight } = useConceptOptions(); const mainHeight = useMainHeight();
const onSelectTopic = useCallback( const onSelectTopic = useCallback(
(newTopic: HelpTopic) => { (newTopic: HelpTopic) => {

View File

@ -6,9 +6,9 @@ import { useCallback } from 'react';
import { IconMenuFold, IconMenuUnfold } from '@/components/Icons'; import { IconMenuFold, IconMenuUnfold } from '@/components/Icons';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import SelectTree from '@/components/ui/SelectTree'; import SelectTree from '@/components/ui/SelectTree';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import useDropdown from '@/hooks/useDropdown'; import useDropdown from '@/hooks/useDropdown';
import { HelpTopic, topicParent } from '@/models/miscellaneous'; import { HelpTopic, topicParent } from '@/models/miscellaneous';
import { useAppLayoutStore, useFitHeight } from '@/stores/appLayout';
import { PARAMETER, prefixes } from '@/utils/constants'; import { PARAMETER, prefixes } from '@/utils/constants';
import { describeHelpTopic, labelHelpTopic } from '@/utils/labels'; import { describeHelpTopic, labelHelpTopic } from '@/utils/labels';
@ -19,7 +19,8 @@ interface TopicsDropdownProps {
function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownProps) { function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownProps) {
const menu = useDropdown(); const menu = useDropdown();
const { noNavigation, calculateHeight } = useConceptOptions(); const noNavigation = useAppLayoutStore(state => state.noNavigation);
const treeHeight = useFitHeight('4rem + 2px');
const handleSelectTopic = useCallback( const handleSelectTopic = useCallback(
(topic: HelpTopic) => { (topic: HelpTopic) => {
@ -67,7 +68,7 @@ function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownProps) {
'bg-prim-200' 'bg-prim-200'
)} )}
style={{ style={{
maxHeight: calculateHeight('4rem + 2px'), maxHeight: treeHeight,
transitionProperty: 'clip-path', transitionProperty: 'clip-path',
transitionDuration: `${PARAMETER.moveDuration}ms`, transitionDuration: `${PARAMETER.moveDuration}ms`,
clipPath: menu.isOpen ? 'inset(0% 0% 0% 0%)' : 'inset(0% 100% 0% 0%)' clipPath: menu.isOpen ? 'inset(0% 0% 0% 0%)' : 'inset(0% 100% 0% 0%)'

View File

@ -1,8 +1,8 @@
import clsx from 'clsx'; import clsx from 'clsx';
import SelectTree from '@/components/ui/SelectTree'; import SelectTree from '@/components/ui/SelectTree';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { HelpTopic, topicParent } from '@/models/miscellaneous'; import { HelpTopic, topicParent } from '@/models/miscellaneous';
import { useFitHeight } from '@/stores/appLayout';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
import { describeHelpTopic, labelHelpTopic } from '@/utils/labels'; import { describeHelpTopic, labelHelpTopic } from '@/utils/labels';
@ -12,7 +12,7 @@ interface TopicsStaticProps {
} }
function TopicsStatic({ activeTopic, onChangeTopic }: TopicsStaticProps) { function TopicsStatic({ activeTopic, onChangeTopic }: TopicsStaticProps) {
const { calculateHeight } = useConceptOptions(); const topicsHeight = useFitHeight('1rem + 2px');
return ( return (
<SelectTree <SelectTree
items={Object.values(HelpTopic).map(item => item as HelpTopic)} 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', 'text-xs sm:text-sm bg-prim-200',
'select-none' '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