Compare commits

...

18 Commits

Author SHA1 Message Date
Ivan
ab9f058b0a F: Implement schema checks for backend + small fixes
Some checks are pending
Frontend CI / build (22.x) (push) Waiting to run
2025-02-18 23:38:33 +03:00
Ivan
a34af217eb R: Refactor type definitions 2025-02-18 19:39:54 +03:00
Ivan
aae57d51f1 M: Fix minor issues 2025-02-17 16:18:36 +03:00
Ivan
75c0a6f848 F: Add zod validation for backend responses for rsforms 2025-02-17 14:40:04 +03:00
Ivan
483fc93b58 R: Remove redundant useEffects 2025-02-15 18:32:37 +03:00
Ivan
1b62c8fbc2 F: Improve global loader visuals 2025-02-15 17:38:32 +03:00
Ivan
1f73941866 npm update 2025-02-15 17:16:21 +03:00
Ivan
c58a8ec969 F: Add global loader for navigation 2025-02-15 17:10:14 +03:00
Ivan
ef463897c4 F: Rework edit wordforms dialog 2025-02-15 16:42:33 +03:00
Ivan
4fe0936b05 B: Hotfix 2025-02-15 15:39:13 +03:00
Ivan
e5c595e99e B: Fix versions ordering 2025-02-15 15:36:32 +03:00
Ivan
4b450384c4 F: Rework reference editor dialog 2025-02-15 15:33:37 +03:00
Ivan
cdcf1a9c43 R: Remove redundant useEffect 2025-02-14 14:11:05 +03:00
Ivan
d5854366a9 F: Rework cst create and template dialogs 2025-02-14 02:41:31 +03:00
Ivan
3c92e07d0f R: Improve import sorting 2025-02-12 21:36:03 +03:00
Ivan
6bb02c6462 R: Refine feature boundaries and dependencies 2025-02-12 20:53:01 +03:00
Ivan
53a795d3ec R: library feature boundary 2025-02-12 15:12:59 +03:00
Ivan
1ab4ce2556 R: Refactor auth and help feature boundary 2025-02-12 13:44:19 +03:00
283 changed files with 3707 additions and 3268 deletions

View File

@ -63,6 +63,7 @@ This readme file is used mostly to document project dependencies and conventions
- tailwindcss - tailwindcss
- postcss - postcss
- autoprefixer - autoprefixer
- eslint-plugin-import
- eslint-plugin-react-compiler - eslint-plugin-react-compiler
- eslint-plugin-simple-import-sort - eslint-plugin-simple-import-sort
- eslint-plugin-react-hooks - eslint-plugin-react-hooks

View File

@ -4,7 +4,7 @@ import typescriptParser from '@typescript-eslint/parser';
import reactPlugin from 'eslint-plugin-react'; import reactPlugin from 'eslint-plugin-react';
import reactCompilerPlugin from 'eslint-plugin-react-compiler'; import reactCompilerPlugin from 'eslint-plugin-react-compiler';
import reactHooksPlugin from 'eslint-plugin-react-hooks'; import reactHooksPlugin from 'eslint-plugin-react-hooks';
import importPlugin from 'eslint-plugin-import';
import simpleImportSort from 'eslint-plugin-simple-import-sort'; import simpleImportSort from 'eslint-plugin-simple-import-sort';
export default [ export default [
@ -37,7 +37,8 @@ export default [
'react': reactPlugin, 'react': reactPlugin,
'react-compiler': reactCompilerPlugin, 'react-compiler': reactCompilerPlugin,
'react-hooks': reactHooksPlugin, 'react-hooks': reactHooksPlugin,
'simple-import-sort': simpleImportSort 'simple-import-sort': simpleImportSort,
'import': importPlugin
}, },
settings: { react: { version: 'detect' } }, settings: { react: { version: 'detect' } },
rules: { rules: {
@ -57,8 +58,33 @@ export default [
'react-refresh/only-export-components': ['off', { allowConstantExport: true }], 'react-refresh/only-export-components': ['off', { allowConstantExport: true }],
'simple-import-sort/imports': 'warn', 'simple-import-sort/imports': [
'warn',
{
groups: [
// Node.js builtins.
[
'^(assert|buffer|child_process|cluster|console|constants|crypto|dgram|dns|domain|events|fs|http|https|module|net|os|path|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|tty|url|util|vm|zlib|freelist|v8|process|async_hooks|http2|perf_hooks)(/.*|$)'
],
// Packages. `react` related packages come first.
['^react', '^@?\\w'],
// Global app and features
['^(@/app|@/features)(/.*|$)'],
// Internal packages.
['^(@)(/.*|$)'],
// Side effect imports.
['^\\u0000'],
// Parent imports. Put `..` last.
['^\\.\\.(?!/?$)', '^\\.\\./?$'],
// Other relative imports. Put same-folder imports and `.` last.
['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'],
// Style imports.
['^.+\\.s?css$']
]
}
],
'simple-import-sort/exports': 'error', 'simple-import-sort/exports': 'error',
'import/no-duplicates': 'warn',
...reactHooksPlugin.configs.recommended.rules ...reactHooksPlugin.configs.recommended.rules
} }

File diff suppressed because it is too large Load Diff

View File

@ -9,20 +9,21 @@
"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",
"lintFix": "eslint . --report-unused-disable-directives --max-warnings 0 --fix",
"preview": "vite preview --port 3000" "preview": "vite preview --port 3000"
}, },
"dependencies": { "dependencies": {
"@dagrejs/dagre": "^1.1.4", "@dagrejs/dagre": "^1.1.4",
"@hookform/resolvers": "^3.10.0", "@hookform/resolvers": "^4.1.0",
"@lezer/lr": "^1.4.2", "@lezer/lr": "^1.4.2",
"@tanstack/react-query": "^5.66.0", "@tanstack/react-query": "^5.66.0",
"@tanstack/react-query-devtools": "^5.66.0", "@tanstack/react-query-devtools": "^5.66.0",
"@tanstack/react-table": "^8.20.6", "@tanstack/react-table": "^8.21.2",
"@uiw/codemirror-themes": "^4.23.8", "@uiw/codemirror-themes": "^4.23.8",
"@uiw/react-codemirror": "^4.23.8", "@uiw/react-codemirror": "^4.23.8",
"axios": "^1.7.9", "axios": "^1.7.9",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.13",
"js-file-download": "^0.4.12", "js-file-download": "^0.4.12",
"qrcode.react": "^4.2.0", "qrcode.react": "^4.2.0",
"react": "^19.0.0", "react": "^19.0.0",
@ -30,7 +31,7 @@
"react-error-boundary": "^5.0.0", "react-error-boundary": "^5.0.0",
"react-hook-form": "^7.54.2", "react-hook-form": "^7.54.2",
"react-icons": "^5.4.0", "react-icons": "^5.4.0",
"react-intl": "^7.1.5", "react-intl": "^7.1.6",
"react-router": "^7.1.5", "react-router": "^7.1.5",
"react-select": "^5.10.0", "react-select": "^5.10.0",
"react-tabs": "^6.1.0", "react-tabs": "^6.1.0",
@ -39,33 +40,34 @@
"react-zoom-pan-pinch": "^3.7.0", "react-zoom-pan-pinch": "^3.7.0",
"reactflow": "^11.11.4", "reactflow": "^11.11.4",
"use-debounce": "^10.0.4", "use-debounce": "^10.0.4",
"zod": "^3.24.1", "zod": "^3.24.2",
"zustand": "^5.0.3" "zustand": "^5.0.3"
}, },
"devDependencies": { "devDependencies": {
"@lezer/generator": "^1.7.2", "@lezer/generator": "^1.7.2",
"@playwright/test": "^1.50.1", "@playwright/test": "^1.50.1",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/node": "^22.13.1", "@types/node": "^22.13.4",
"@types/react": "^19.0.8", "@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3", "@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-30d8a17-20250209",
"eslint": "^9.19.0", "eslint": "^9.20.1",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-react": "^7.37.4", "eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-compiler": "^19.0.0-beta-37ed2a7-20241206", "eslint-plugin-react-compiler": "^19.0.0-beta-30d8a17-20250209",
"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.14.0", "globals": "^15.15.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"postcss": "^8.5.1", "postcss": "^8.5.2",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"ts-jest": "^29.2.5", "ts-jest": "^29.2.5",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"typescript-eslint": "^8.23.0", "typescript-eslint": "^8.24.0",
"vite": "^6.1.0" "vite": "^6.1.0"
}, },
"overrides": { "overrides": {

View File

@ -1,17 +1,17 @@
import { Suspense } from 'react'; import { Suspense } from 'react';
import { Outlet } from 'react-router'; import { Outlet } from 'react-router';
import { Loader } from '@/components/Loader';
import { ModalLoader } from '@/components/Modal'; import { ModalLoader } from '@/components/Modal';
import { useAppLayoutStore, useMainHeight, useViewportHeight } from '@/stores/appLayout'; import { useAppLayoutStore, useMainHeight, useViewportHeight } from '@/stores/appLayout';
import { globals } from '@/utils/constants'; import { globals } from '@/utils/constants';
import { NavigationState } from './Navigation/NavigationContext';
import { Footer } from './Footer'; import { Footer } from './Footer';
import { GlobalDialogs } from './GlobalDialogs'; import { GlobalDialogs } from './GlobalDialogs';
import ConceptToaster from './GlobalToaster'; import { GlobalLoader } from './GlobalLoader';
import { ToasterThemed } from './GlobalToaster';
import { GlobalTooltips } from './GlobalTooltips'; import { GlobalTooltips } from './GlobalTooltips';
import { Navigation } from './Navigation'; import { Navigation } from './Navigation';
import { NavigationState } from './Navigation/NavigationContext';
function ApplicationLayout() { function ApplicationLayout() {
const mainHeight = useMainHeight(); const mainHeight = useMainHeight();
@ -24,7 +24,7 @@ function ApplicationLayout() {
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'>
<ConceptToaster <ToasterThemed
className='text-[14px] cc-animate-position' className='text-[14px] cc-animate-position'
style={{ marginTop: noNavigationAnimation ? '1.5rem' : '3.5rem' }} style={{ marginTop: noNavigationAnimation ? '1.5rem' : '3.5rem' }}
autoClose={3000} autoClose={3000}
@ -47,9 +47,8 @@ function ApplicationLayout() {
}} }}
> >
<main className='cc-scroll-y' style={{ overflowY: showScroll ? 'scroll' : 'auto', minHeight: mainHeight }}> <main className='cc-scroll-y' style={{ overflowY: showScroll ? 'scroll' : 'auto', minHeight: mainHeight }}>
<Suspense fallback={<Loader />}> <GlobalLoader />
<Outlet /> <Outlet />
</Suspense>
</main> </main>
{!noNavigation && !noFooter ? <Footer /> : null} {!noNavigation && !noFooter ? <Footer /> : null}
</div> </div>

View File

@ -6,17 +6,17 @@ import { DialogType, useDialogsStore } from '@/stores/dialogs';
const DlgChangeInputSchema = React.lazy(() => import('@/features/oss/dialogs/DlgChangeInputSchema')); const DlgChangeInputSchema = React.lazy(() => import('@/features/oss/dialogs/DlgChangeInputSchema'));
const DlgChangeLocation = React.lazy(() => import('@/features/library/dialogs/DlgChangeLocation')); const DlgChangeLocation = React.lazy(() => import('@/features/library/dialogs/DlgChangeLocation'));
const DlgCloneLibraryItem = React.lazy(() => import('@/features/rsform/dialogs/DlgCloneLibraryItem')); const DlgCloneLibraryItem = React.lazy(() => import('@/features/library/dialogs/DlgCloneLibraryItem'));
const DlgCreateCst = React.lazy(() => import('@/features/rsform/dialogs/DlgCreateCst')); const DlgCreateCst = React.lazy(() => import('@/features/rsform/dialogs/DlgCreateCst'));
const DlgCreateOperation = React.lazy(() => import('@/features/oss/dialogs/DlgCreateOperation')); const DlgCreateOperation = React.lazy(() => import('@/features/oss/dialogs/DlgCreateOperation'));
const DlgCreateVersion = React.lazy(() => import('@/features/rsform/dialogs/DlgCreateVersion')); const DlgCreateVersion = React.lazy(() => import('@/features/library/dialogs/DlgCreateVersion'));
const DlgCstTemplate = React.lazy(() => import('@/features/rsform/dialogs/DlgCstTemplate')); const DlgCstTemplate = React.lazy(() => import('@/features/rsform/dialogs/DlgCstTemplate'));
const DlgDeleteCst = React.lazy(() => import('@/features/rsform/dialogs/DlgDeleteCst')); const DlgDeleteCst = React.lazy(() => import('@/features/rsform/dialogs/DlgDeleteCst'));
const DlgDeleteOperation = React.lazy(() => import('@/features/oss/dialogs/DlgDeleteOperation')); const DlgDeleteOperation = React.lazy(() => import('@/features/oss/dialogs/DlgDeleteOperation'));
const DlgEditEditors = React.lazy(() => import('@/features/library/dialogs/DlgEditEditors')); const DlgEditEditors = React.lazy(() => import('@/features/library/dialogs/DlgEditEditors'));
const DlgEditOperation = React.lazy(() => import('@/features/oss/dialogs/DlgEditOperation')); const DlgEditOperation = React.lazy(() => import('@/features/oss/dialogs/DlgEditOperation'));
const DlgEditReference = React.lazy(() => import('@/features/rsform/dialogs/DlgEditReference')); const DlgEditReference = React.lazy(() => import('@/features/rsform/dialogs/DlgEditReference'));
const DlgEditVersions = React.lazy(() => import('@/features/rsform/dialogs/DlgEditVersions')); const DlgEditVersions = React.lazy(() => import('@/features/library/dialogs/DlgEditVersions'));
const DlgEditWordForms = React.lazy(() => import('@/features/rsform/dialogs/DlgEditWordForms')); const DlgEditWordForms = React.lazy(() => import('@/features/rsform/dialogs/DlgEditWordForms'));
const DlgGraphParams = React.lazy(() => import('@/features/rsform/dialogs/DlgGraphParams')); const DlgGraphParams = React.lazy(() => import('@/features/rsform/dialogs/DlgGraphParams'));
const DlgInlineSynthesis = React.lazy(() => import('@/features/rsform/dialogs/DlgInlineSynthesis')); const DlgInlineSynthesis = React.lazy(() => import('@/features/rsform/dialogs/DlgInlineSynthesis'));

View File

@ -0,0 +1,34 @@
import { useNavigation } from 'react-router';
import clsx from 'clsx';
import { useDebounce } from 'use-debounce';
import { Loader } from '@/components/Loader';
import { PARAMETER } from '@/utils/constants';
// TODO: add animation
export function GlobalLoader() {
const navigation = useNavigation();
const isLoading = navigation.state === 'loading';
const [loadingDebounced] = useDebounce(isLoading, PARAMETER.navigationPopupDelay);
if (!loadingDebounced) {
return null;
}
return (
<div className='fixed top-0 left-0 w-full h-full z-modal cursor-default'>
<div className={clsx('z-navigation', 'fixed top-0 left-0', 'w-full h-full', 'backdrop-blur-[3px] opacity-50')} />
<div className={clsx('z-navigation', 'fixed top-0 left-0', 'w-full h-full', 'bg-prim-0 opacity-25')} />
<div
className={clsx(
'px-10 mb-10',
'z-modal absolute bottom-1/2 left-1/2 -translate-x-1/2 translate-y-1/2',
'border rounded-xl bg-prim-100'
)}
>
<Loader scale={6} />
</div>
</div>
);
}

View File

@ -1,8 +1,8 @@
'use client'; 'use client';
import { IntlProvider } from 'react-intl';
import { QueryClientProvider } from '@tanstack/react-query'; import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { IntlProvider } from 'react-intl';
import { queryClient } from '@/backend/queryClient'; import { queryClient } from '@/backend/queryClient';

View File

@ -4,9 +4,7 @@ import { usePreferencesStore } from '@/stores/preferences';
interface ToasterThemedProps extends Omit<ToastContainerProps, 'theme'> {} interface ToasterThemedProps extends Omit<ToastContainerProps, 'theme'> {}
function ToasterThemed(props: ToasterThemedProps) { export function ToasterThemed(props: ToasterThemedProps) {
const darkMode = usePreferencesStore(state => state.darkMode); const darkMode = usePreferencesStore(state => state.darkMode);
return <ToastContainer theme={darkMode ? 'dark' : 'light'} {...props} />; return <ToastContainer theme={darkMode ? 'dark' : 'light'} {...props} />;
} }
export default ToasterThemed;

View File

@ -1,8 +1,9 @@
'use client'; 'use client';
import InfoConstituenta from '@/features/rsform/components/InfoConstituenta';
import { Tooltip } from '@/components/Container'; import { Tooltip } from '@/components/Container';
import { Loader } from '@/components/Loader'; import { Loader } from '@/components/Loader';
import InfoConstituenta from '@/features/rsform/components/InfoConstituenta';
import { useTooltipsStore } from '@/stores/tooltips'; import { useTooltipsStore } from '@/stores/tooltips';
import { globals } from '@/utils/constants'; import { globals } from '@/utils/constants';

View File

@ -7,6 +7,7 @@ import { useAppLayoutStore } from '@/stores/appLayout';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { urls } from '../urls'; import { urls } from '../urls';
import Logo from './Logo'; import Logo from './Logo';
import NavigationButton from './NavigationButton'; import NavigationButton from './NavigationButton';
import { useConceptNavigation } from './NavigationContext'; import { useConceptNavigation } from './NavigationContext';

View File

@ -1,5 +1,6 @@
import { useAuthSuspense } from '@/features/auth';
import { IconLogin, IconUser2 } from '@/components/Icons'; import { IconLogin, IconUser2 } from '@/components/Icons';
import { useAuthSuspense } from '@/features/auth/backend/useAuth';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import NavigationButton from './NavigationButton'; import NavigationButton from './NavigationButton';

View File

@ -1,3 +1,5 @@
import { useAuthSuspense, useLogout } from '@/features/auth';
import { Dropdown, DropdownButton } from '@/components/Dropdown'; import { Dropdown, DropdownButton } from '@/components/Dropdown';
import { import {
IconAdmin, IconAdmin,
@ -14,11 +16,10 @@ import {
IconUser IconUser
} from '@/components/Icons'; } from '@/components/Icons';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import { useAuthSuspense } from '@/features/auth/backend/useAuth';
import { useLogout } from '@/features/auth/backend/useLogout';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import { urls } from '../urls'; import { urls } from '../urls';
import { useConceptNavigation } from './NavigationContext'; import { useConceptNavigation } from './NavigationContext';
interface UserDropdownProps { interface UserDropdownProps {

View File

@ -4,6 +4,7 @@ import { useDropdown } from '@/components/Dropdown';
import { Loader } from '@/components/Loader'; import { Loader } from '@/components/Loader';
import { urls } from '../urls'; import { urls } from '../urls';
import { useConceptNavigation } from './NavigationContext'; import { useConceptNavigation } from './NavigationContext';
import UserButton from './UserButton'; import UserButton from './UserButton';
import UserDropdown from './UserDropdown'; import UserDropdown from './UserDropdown';

View File

@ -1,6 +1,5 @@
import { createBrowserRouter } from 'react-router'; import { createBrowserRouter } from 'react-router';
import { Loader } from '@/components/Loader';
import { prefetchAuth } from '@/features/auth/backend/useAuth'; import { prefetchAuth } from '@/features/auth/backend/useAuth';
import LoginPage from '@/features/auth/pages/LoginPage'; import LoginPage from '@/features/auth/pages/LoginPage';
import HomePage from '@/features/home/HomePage'; import HomePage from '@/features/home/HomePage';
@ -12,6 +11,8 @@ import { prefetchRSForm } from '@/features/rsform/backend/useRSForm';
import { prefetchProfile } from '@/features/users/backend/useProfile'; import { prefetchProfile } from '@/features/users/backend/useProfile';
import { prefetchUsers } from '@/features/users/backend/useUsers'; import { prefetchUsers } from '@/features/users/backend/useUsers';
import { Loader } from '@/components/Loader';
import ApplicationLayout from './ApplicationLayout'; import ApplicationLayout from './ApplicationLayout';
import { ErrorFallback } from './ErrorFallback'; import { ErrorFallback } from './ErrorFallback';
import { routes } from './urls'; import { routes } from './urls';
@ -22,7 +23,7 @@ export const Router = createBrowserRouter([
element: <ApplicationLayout />, element: <ApplicationLayout />,
errorElement: <ErrorFallback />, errorElement: <ErrorFallback />,
loader: prefetchAuth, loader: prefetchAuth,
hydrateFallbackElement: <Loader />, hydrateFallbackElement: fallbackLoader(),
children: [ children: [
{ {
path: '', path: '',
@ -98,3 +99,11 @@ function parseRSFormURL(id: string | undefined, url: string) {
function parseOssURL(id: string | undefined) { function parseOssURL(id: string | undefined) {
return { itemID: id ? Number(id) : undefined }; return { itemID: id ? Number(id) : undefined };
} }
function fallbackLoader() {
return (
<div className='flex justify-center items-center h-[100dvh]'>
<Loader scale={6} />
</div>
);
}

View File

@ -1,10 +1,12 @@
/** /**
* Module: generic API for backend REST communications using axios library. * Module: generic API for backend REST communications using axios library.
*/ */
import axios, { AxiosError, AxiosRequestConfig } from 'axios';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import axios, { AxiosError, AxiosRequestConfig } from 'axios';
import { z, ZodError } from 'zod';
import { buildConstants } from '@/utils/buildConstants'; import { buildConstants } from '@/utils/buildConstants';
import { errorMsg } from '@/utils/labels';
import { extractErrorMessage } from '@/utils/utils'; import { extractErrorMessage } from '@/utils/utils';
export { AxiosError } from 'axios'; export { AxiosError } from 'axios';
@ -41,23 +43,32 @@ export interface IAxiosRequest<RequestData, ResponseData> {
endpoint: string; endpoint: string;
request?: IFrontRequest<RequestData, ResponseData>; request?: IFrontRequest<RequestData, ResponseData>;
options?: AxiosRequestConfig; options?: AxiosRequestConfig;
schema?: z.ZodType;
} }
export interface IAxiosGetRequest { export interface IAxiosGetRequest {
endpoint: string; endpoint: string;
options?: AxiosRequestConfig; options?: AxiosRequestConfig;
signal?: AbortSignal; signal?: AbortSignal;
schema?: z.ZodType;
} }
// ================ Transport API calls ================ // ================ Transport API calls ================
export function axiosGet<ResponseData>({ endpoint, options }: IAxiosGetRequest) { export function axiosGet<ResponseData>({ endpoint, options, schema }: IAxiosGetRequest) {
return axiosInstance return axiosInstance
.get<ResponseData>(endpoint, options) .get<ResponseData>(endpoint, options)
.then(response => response.data) .then(response => {
schema?.parse(response.data);
return response.data;
})
.catch((error: Error | AxiosError) => { .catch((error: Error | AxiosError) => {
// Note: Ignore cancellation errors
if (error.name !== 'CanceledError') { if (error.name !== 'CanceledError') {
// Note: Ignore cancellation errors if (error instanceof ZodError) {
toast.error(extractErrorMessage(error)); toast.error(errorMsg.invalidResponse);
} else {
toast.error(extractErrorMessage(error));
}
console.error(error); console.error(error);
} }
throw error; throw error;
@ -67,11 +78,13 @@ export function axiosGet<ResponseData>({ endpoint, options }: IAxiosGetRequest)
export function axiosPost<RequestData, ResponseData = void>({ export function axiosPost<RequestData, ResponseData = void>({
endpoint, endpoint,
request, request,
options options,
schema
}: IAxiosRequest<RequestData, ResponseData>) { }: IAxiosRequest<RequestData, ResponseData>) {
return axiosInstance return axiosInstance
.post<ResponseData>(endpoint, request?.data, options) .post<ResponseData>(endpoint, request?.data, options)
.then(response => { .then(response => {
schema?.parse(response.data);
if (request?.successMessage) { if (request?.successMessage) {
if (typeof request.successMessage === 'string') { if (typeof request.successMessage === 'string') {
toast.success(request.successMessage); toast.success(request.successMessage);
@ -81,8 +94,12 @@ export function axiosPost<RequestData, ResponseData = void>({
} }
return response.data; return response.data;
}) })
.catch((error: Error | AxiosError) => { .catch((error: Error | AxiosError | ZodError) => {
toast.error(extractErrorMessage(error)); if (error instanceof ZodError) {
toast.error(errorMsg.invalidResponse);
} else {
toast.error(extractErrorMessage(error));
}
console.error(error); console.error(error);
throw error; throw error;
}); });
@ -91,11 +108,13 @@ export function axiosPost<RequestData, ResponseData = void>({
export function axiosDelete<RequestData, ResponseData = void>({ export function axiosDelete<RequestData, ResponseData = void>({
endpoint, endpoint,
request, request,
options options,
schema
}: IAxiosRequest<RequestData, ResponseData>) { }: IAxiosRequest<RequestData, ResponseData>) {
return axiosInstance return axiosInstance
.delete<ResponseData>(endpoint, options) .delete<ResponseData>(endpoint, options)
.then(response => { .then(response => {
schema?.parse(response.data);
if (request?.successMessage) { if (request?.successMessage) {
if (typeof request.successMessage === 'string') { if (typeof request.successMessage === 'string') {
toast.success(request.successMessage); toast.success(request.successMessage);
@ -105,8 +124,12 @@ export function axiosDelete<RequestData, ResponseData = void>({
} }
return response.data; return response.data;
}) })
.catch((error: Error | AxiosError) => { .catch((error: Error | AxiosError | ZodError) => {
toast.error(extractErrorMessage(error)); if (error instanceof ZodError) {
toast.error(errorMsg.invalidResponse);
} else {
toast.error(extractErrorMessage(error));
}
console.error(error); console.error(error);
throw error; throw error;
}); });
@ -115,11 +138,13 @@ export function axiosDelete<RequestData, ResponseData = void>({
export function axiosPatch<RequestData, ResponseData = void>({ export function axiosPatch<RequestData, ResponseData = void>({
endpoint, endpoint,
request, request,
options options,
schema
}: IAxiosRequest<RequestData, ResponseData>) { }: IAxiosRequest<RequestData, ResponseData>) {
return axiosInstance return axiosInstance
.patch<ResponseData>(endpoint, request?.data, options) .patch<ResponseData>(endpoint, request?.data, options)
.then(response => { .then(response => {
schema?.parse(response.data);
if (request?.successMessage) { if (request?.successMessage) {
if (typeof request.successMessage === 'string') { if (typeof request.successMessage === 'string') {
toast.success(request.successMessage); toast.success(request.successMessage);
@ -129,8 +154,12 @@ export function axiosPatch<RequestData, ResponseData = void>({
} }
return response.data; return response.data;
}) })
.catch((error: Error | AxiosError) => { .catch((error: Error | AxiosError | ZodError) => {
toast.error(extractErrorMessage(error)); if (error instanceof ZodError) {
toast.error(errorMsg.invalidResponse);
} else {
toast.error(extractErrorMessage(error));
}
console.error(error); console.error(error);
throw error; throw error;
}); });

View File

@ -7,3 +7,18 @@ export const DELAYS = {
staleMedium: 1 * 60 * 60 * 1000, staleMedium: 1 * 60 * 60 * 1000,
staleLong: 24 * 60 * 60 * 1000 staleLong: 24 * 60 * 60 * 1000
}; };
/** API keys for local cache. */
export const KEYS = {
oss: 'oss',
rsform: 'rsform',
library: 'library',
users: 'users',
cctext: 'cctext',
composite: {
libraryList: ['library', 'list'],
ossItem: ({ itemID }: { itemID?: number }) => [KEYS.oss, 'item', itemID],
rsItem: ({ itemID, version }: { itemID?: number; version?: number }) => [KEYS.rsform, 'item', itemID, version ?? '']
}
};

View File

@ -1,9 +1,9 @@
'use client'; 'use client';
import clsx from 'clsx';
import { ReactNode } from 'react'; 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 clsx from 'clsx';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';

View File

@ -1,6 +1,7 @@
'use client'; 'use client';
'use no memo'; 'use no memo';
import { useMemo, useState } from 'react';
import { import {
ColumnSort, ColumnSort,
createColumnHelper, createColumnHelper,
@ -15,9 +16,9 @@ import {
useReactTable, useReactTable,
type VisibilityState type VisibilityState
} from '@tanstack/react-table'; } from '@tanstack/react-table';
import { useMemo, useState } from 'react';
import { CProps } from '../props'; import { CProps } from '../props';
import DefaultNoData from './DefaultNoData'; import DefaultNoData from './DefaultNoData';
import PaginationTools from './PaginationTools'; import PaginationTools from './PaginationTools';
import TableBody from './TableBody'; import TableBody from './TableBody';

View File

@ -1,9 +1,9 @@
'use client'; 'use client';
'use no memo'; 'use no memo';
import { useCallback } from 'react';
import { Table } from '@tanstack/react-table'; import { Table } from '@tanstack/react-table';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback } from 'react';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';

View File

@ -4,8 +4,9 @@ import { Cell, flexRender, Row, Table } from '@tanstack/react-table';
import clsx from 'clsx'; import clsx from 'clsx';
import { CProps } from '../props'; import { CProps } from '../props';
import { IConditionalStyle } from '.';
import SelectRow from './SelectRow'; import SelectRow from './SelectRow';
import { IConditionalStyle } from '.';
interface TableBodyProps<TData> { interface TableBodyProps<TData> {
table: Table<TData>; table: Table<TData>;

View File

@ -1,18 +1,10 @@
import { AccessPolicy, LibraryItemType, LocationHead } from '@/features/library/models/library'; import { LocationHead } from '@/features/library/models/library';
import { CstType, ExpressionStatus } from '@/features/rsform/models/rsform'; import { ExpressionStatus } from '@/features/rsform/models/rsform';
import { CstMatchMode, DependencyMode } from '@/features/rsform/stores/cstSearch'; import { CstMatchMode, DependencyMode } from '@/features/rsform/stores/cstSearch';
import { import {
IconAlias, IconAlias,
IconBusiness, IconBusiness,
IconCstAxiom,
IconCstBaseSet,
IconCstConstSet,
IconCstFunction,
IconCstPredicate,
IconCstStructured,
IconCstTerm,
IconCstTheorem,
IconFilter, IconFilter,
IconFormula, IconFormula,
IconGraphCollapse, IconGraphCollapse,
@ -22,12 +14,8 @@ import {
IconHide, IconHide,
IconMoveDown, IconMoveDown,
IconMoveUp, IconMoveUp,
IconOSS,
IconPrivate,
IconProps, IconProps,
IconProtected,
IconPublic, IconPublic,
IconRSForm,
IconSettings, IconSettings,
IconShow, IconShow,
IconStatusError, IconStatusError,
@ -45,28 +33,6 @@ export interface DomIconProps<RequestData> extends IconProps {
value: RequestData; value: RequestData;
} }
/** Icon for library item type. */
export function ItemTypeIcon({ value, size = '1.25rem', className }: DomIconProps<LibraryItemType>) {
switch (value) {
case LibraryItemType.RSFORM:
return <IconRSForm size={size} className={className ?? 'text-sec-600'} />;
case LibraryItemType.OSS:
return <IconOSS size={size} className={className ?? 'text-ok-600'} />;
}
}
/** Icon for access policy. */
export function PolicyIcon({ value, size = '1.25rem', className }: DomIconProps<AccessPolicy>) {
switch (value) {
case AccessPolicy.PRIVATE:
return <IconPrivate size={size} className={className ?? 'text-warn-600'} />;
case AccessPolicy.PROTECTED:
return <IconProtected size={size} className={className ?? 'text-sec-600'} />;
case AccessPolicy.PUBLIC:
return <IconPublic size={size} className={className ?? 'text-ok-600'} />;
}
}
/** Icon for visibility. */ /** Icon for visibility. */
export function VisibilityIcon({ value, size = '1.25rem', className }: DomIconProps<boolean>) { export function VisibilityIcon({ value, size = '1.25rem', className }: DomIconProps<boolean>) {
if (value) { if (value) {
@ -149,28 +115,6 @@ export function StatusIcon({ value, size = '1.25rem', className }: DomIconProps<
} }
} }
/** Icon for constituenta type. */
export function CstTypeIcon({ value, size = '1.25rem', className }: DomIconProps<CstType>) {
switch (value) {
case CstType.BASE:
return <IconCstBaseSet size={size} className={className ?? 'text-ok-600'} />;
case CstType.CONSTANT:
return <IconCstConstSet size={size} className={className ?? 'text-ok-600'} />;
case CstType.STRUCTURED:
return <IconCstStructured size={size} className={className ?? 'text-ok-600'} />;
case CstType.TERM:
return <IconCstTerm size={size} className={className ?? 'text-sec-600'} />;
case CstType.AXIOM:
return <IconCstAxiom size={size} className={className ?? 'text-warn-600'} />;
case CstType.FUNCTION:
return <IconCstFunction size={size} className={className ?? 'text-sec-600'} />;
case CstType.PREDICATE:
return <IconCstPredicate size={size} className={className ?? 'text-warn-600'} />;
case CstType.THEOREM:
return <IconCstTheorem size={size} className={className ?? 'text-warn-600'} />;
}
}
/** Icon for relocation direction. */ /** Icon for relocation direction. */
export function RelocateUpIcon({ value, size = '1.25rem', className }: DomIconProps<boolean>) { export function RelocateUpIcon({ value, size = '1.25rem', className }: DomIconProps<boolean>) {
if (value) { if (value) {

View File

@ -1,11 +1,12 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { ZodError } from 'zod';
import { AxiosError, isAxiosError } from '@/backend/apiTransport'; import { AxiosError, isAxiosError } from '@/backend/apiTransport';
import { isResponseHtml } from '@/utils/utils'; import { isResponseHtml } from '@/utils/utils';
import { PrettyJson } from './View'; import { PrettyJson } from './View';
export type ErrorData = string | Error | AxiosError | undefined | null; export type ErrorData = string | Error | AxiosError | ZodError | undefined | null;
interface InfoErrorProps { interface InfoErrorProps {
error: ErrorData; error: ErrorData;
@ -16,6 +17,13 @@ function DescribeError({ error }: { error: ErrorData }) {
return <p>Ошибки отсутствуют</p>; return <p>Ошибки отсутствуют</p>;
} else if (typeof error === 'string') { } else if (typeof error === 'string') {
return <p>{error}</p>; return <p>{error}</p>;
} else if (error instanceof ZodError) {
return (
<div className='mt-6'>
<p>Ошибка валидации данных</p>
<PrettyJson data={JSON.parse(error.toString()) as unknown} />;
</div>
);
} else if (!isAxiosError(error)) { } else if (!isAxiosError(error)) {
return ( return (
<div className='mt-6'> <div className='mt-6'>

View File

@ -4,6 +4,7 @@ import { globals } from '@/utils/constants';
import { CheckboxChecked, CheckboxNull } from '../Icons'; import { CheckboxChecked, CheckboxNull } from '../Icons';
import { CProps } from '../props'; import { CProps } from '../props';
import { CheckboxProps } from './Checkbox'; import { CheckboxProps } from './Checkbox';
export interface CheckboxTristateProps extends Omit<CheckboxProps, 'value' | 'onChange'> { export interface CheckboxTristateProps extends Omit<CheckboxProps, 'value' | 'onChange'> {

View File

@ -1,5 +1,5 @@
import clsx from 'clsx';
import { FieldError, GlobalError } from 'react-hook-form'; import { FieldError, GlobalError } from 'react-hook-form';
import clsx from 'clsx';
import { CProps } from '../props'; import { CProps } from '../props';

View File

@ -1,11 +1,12 @@
'use client'; 'use client';
import clsx from 'clsx';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import clsx from 'clsx';
import { Button } from '../Control'; import { Button } from '../Control';
import { IconUpload } from '../Icons'; import { IconUpload } from '../Icons';
import { CProps } from '../props'; import { CProps } from '../props';
import { Label } from './Label'; import { Label } from './Label';
interface FileInputProps extends Omit<CProps.Input, 'accept' | 'type'> { interface FileInputProps extends Omit<CProps.Input, 'accept' | 'type'> {

View File

@ -2,9 +2,10 @@ import clsx from 'clsx';
import { Overlay } from '@/components/Container'; import { Overlay } from '@/components/Container';
import { IconSearch } from '@/components/Icons'; import { IconSearch } from '@/components/Icons';
import { TextInput } from '@/components/Input';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import { TextInput } from './TextInput';
interface SearchBarProps extends CProps.Styling { interface SearchBarProps extends CProps.Styling {
/** Id of the search bar. */ /** Id of the search bar. */
id?: string; id?: string;

View File

@ -1,5 +1,5 @@
import clsx from 'clsx';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import clsx from 'clsx';
import { globals, PARAMETER } from '@/utils/constants'; import { globals, PARAMETER } from '@/utils/constants';

View File

@ -2,6 +2,7 @@ import clsx from 'clsx';
import { Label } from '../Input/Label'; import { Label } from '../Input/Label';
import { CProps } from '../props'; import { CProps } from '../props';
import { ErrorField } from './ErrorField'; import { ErrorField } from './ErrorField';
export interface TextAreaProps extends CProps.Editor, CProps.ErrorProcessing, CProps.Colors, CProps.TextArea { export interface TextAreaProps extends CProps.Editor, CProps.ErrorProcessing, CProps.Colors, CProps.TextArea {

View File

@ -2,6 +2,7 @@ import clsx from 'clsx';
import { Label } from '../Input/Label'; import { Label } from '../Input/Label';
import { CProps } from '../props'; import { CProps } from '../props';
import { ErrorField } from './ErrorField'; import { ErrorField } from './ErrorField';
interface TextInputProps extends CProps.Editor, CProps.ErrorProcessing, CProps.Colors, CProps.Input { interface TextInputProps extends CProps.Editor, CProps.ErrorProcessing, CProps.Colors, CProps.Input {

View File

@ -3,6 +3,7 @@ export { CheckboxTristate } from './CheckboxTristate';
export { ErrorField } from './ErrorField'; export { ErrorField } from './ErrorField';
export { FileInput } from './FileInput'; export { FileInput } from './FileInput';
export { Label } from './Label'; export { Label } from './Label';
export { SearchBar } from './SearchBar';
export { SelectMulti, type SelectMultiProps } from './SelectMulti'; export { SelectMulti, type SelectMultiProps } from './SelectMulti';
export { SelectSingle, type SelectSingleProps } from './SelectSingle'; export { SelectSingle, type SelectSingleProps } from './SelectSingle';
export { SelectTree } from './SelectTree'; export { SelectTree } from './SelectTree';

View File

@ -2,7 +2,8 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { HelpTopic } from '@/features/help/models/helpTopic'; import { BadgeHelp, HelpTopic } from '@/features/help';
import useEscapeKey from '@/hooks/useEscapeKey'; import useEscapeKey from '@/hooks/useEscapeKey';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
@ -11,7 +12,7 @@ import { prepareTooltip } from '@/utils/utils';
import { Button, MiniButton, SubmitButton } from '../Control'; import { Button, MiniButton, SubmitButton } from '../Control';
import { IconClose } from '../Icons'; import { IconClose } from '../Icons';
import { CProps } from '../props'; import { CProps } from '../props';
import { BadgeHelp } from '../shared/BadgeHelp';
import { ModalBackdrop } from './ModalBackdrop'; import { ModalBackdrop } from './ModalBackdrop';
export interface ModalProps extends CProps.Styling { export interface ModalProps extends CProps.Styling {
@ -43,6 +44,9 @@ interface ModalFormProps extends ModalProps {
/** Callback to be called after submit. */ /** Callback to be called after submit. */
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void; onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
/** Callback to be called when modal is canceled. */
onCancel?: () => void;
} }
/** /**
@ -60,13 +64,19 @@ export function ModalForm({
submitInvalidTooltip, submitInvalidTooltip,
beforeSubmit, beforeSubmit,
onSubmit, onSubmit,
onCancel,
helpTopic, helpTopic,
hideHelpWhen, hideHelpWhen,
...restProps ...restProps
}: React.PropsWithChildren<ModalFormProps>) { }: React.PropsWithChildren<ModalFormProps>) {
const hideDialog = useDialogsStore(state => state.hideDialog); const hideDialog = useDialogsStore(state => state.hideDialog);
useEscapeKey(hideDialog);
function handleCancel() {
onCancel?.();
hideDialog();
}
useEscapeKey(handleCancel);
function handleSubmit(event: React.FormEvent<HTMLFormElement>) { function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
if (beforeSubmit && !beforeSubmit()) { if (beforeSubmit && !beforeSubmit()) {
@ -78,7 +88,7 @@ export function ModalForm({
return ( return (
<div className='fixed top-0 left-0 w-full h-full z-modal cursor-default'> <div className='fixed top-0 left-0 w-full h-full z-modal cursor-default'>
<ModalBackdrop onHide={hideDialog} /> <ModalBackdrop onHide={handleCancel} />
<form <form
className={clsx( className={clsx(
'cc-animate-modal', 'cc-animate-modal',
@ -98,7 +108,7 @@ export function ModalForm({
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')} titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
icon={<IconClose size='1.25rem' />} icon={<IconClose size='1.25rem' />}
className='float-right mt-2 mr-2' className='float-right mt-2 mr-2'
onClick={hideDialog} onClick={handleCancel}
/> />
{header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null} {header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null}
@ -125,7 +135,7 @@ export function ModalForm({
className='min-w-[7rem]' className='min-w-[7rem]'
disabled={!canSubmit} disabled={!canSubmit}
/> />
<Button text='Отмена' className='min-w-[7rem]' onClick={hideDialog} /> <Button text='Отмена' className='min-w-[7rem]' onClick={handleCancel} />
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,5 +1,3 @@
'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { Loader } from '@/components/Loader'; import { Loader } from '@/components/Loader';

View File

@ -2,6 +2,8 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { BadgeHelp } from '@/features/help';
import useEscapeKey from '@/hooks/useEscapeKey'; import useEscapeKey from '@/hooks/useEscapeKey';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
@ -9,7 +11,7 @@ import { prepareTooltip } from '@/utils/utils';
import { Button, MiniButton } from '../Control'; import { Button, MiniButton } from '../Control';
import { IconClose } from '../Icons'; import { IconClose } from '../Icons';
import { BadgeHelp } from '../shared/BadgeHelp';
import { ModalBackdrop } from './ModalBackdrop'; import { ModalBackdrop } from './ModalBackdrop';
import { ModalProps } from './ModalForm'; import { ModalProps } from './ModalForm';

View File

@ -1,6 +1,6 @@
import clsx from 'clsx';
import type { TabProps as TabPropsImpl } from 'react-tabs'; import type { TabProps as TabPropsImpl } from 'react-tabs';
import { Tab as TabImpl } from 'react-tabs'; import { Tab as TabImpl } from 'react-tabs';
import clsx from 'clsx';
import { globals } from '@/utils/constants'; import { globals } from '@/utils/constants';

View File

@ -1,77 +1,17 @@
import { queryOptions } from '@tanstack/react-query'; import { queryOptions } from '@tanstack/react-query';
import { z } from 'zod';
import { axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport'; import { axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport';
import { DELAYS } from '@/backend/configuration'; import { DELAYS } from '@/backend/configuration';
import { errorMsg, infoMsg } from '@/utils/labels'; import { infoMsg } from '@/utils/labels';
/** import {
* Represents CurrentUser information. IChangePasswordDTO,
*/ ICurrentUser,
export interface ICurrentUser { IPasswordTokenDTO,
id: number | null; IRequestPasswordDTO,
username: string; IResetPasswordDTO,
is_staff: boolean; IUserLoginDTO
editor: number[]; } from './types';
}
/**
* Represents login data, used to authenticate users.
*/
export const schemaUserLogin = z.object({
username: z.string().nonempty(errorMsg.requiredField),
password: z.string().nonempty(errorMsg.requiredField)
});
/**
* Represents login data, used to authenticate users.
*/
export type IUserLoginDTO = z.infer<typeof schemaUserLogin>;
/**
* Represents data needed to update password for current user.
*/
export const schemaChangePassword = z
.object({
old_password: z.string().nonempty(errorMsg.requiredField),
new_password: z.string().nonempty(errorMsg.requiredField),
new_password2: z.string().nonempty(errorMsg.requiredField)
})
.refine(schema => schema.new_password === schema.new_password2, {
path: ['new_password2'],
message: errorMsg.passwordsMismatch
})
.refine(schema => schema.old_password !== schema.new_password, {
path: ['new_password'],
message: errorMsg.passwordsSame
});
/**
* Represents data needed to update password for current user.
*/
export type IChangePasswordDTO = z.infer<typeof schemaChangePassword>;
/**
* Represents password reset request data.
*/
export interface IRequestPasswordDTO {
email: string;
}
/**
* Represents password reset data.
*/
export interface IResetPasswordDTO {
password: string;
token: string;
}
/**
* Represents password token data.
*/
export interface IPasswordTokenDTO {
token: string;
}
/** /**
* Authentication API. * Authentication API.

View File

@ -0,0 +1,71 @@
import { z } from 'zod';
import { errorMsg } from '@/utils/labels';
/**
* Represents CurrentUser information.
*/
export interface ICurrentUser {
id: number | null;
username: string;
is_staff: boolean;
editor: number[];
}
/**
* Represents login data, used to authenticate users.
*/
export const schemaUserLogin = z.object({
username: z.string().nonempty(errorMsg.requiredField),
password: z.string().nonempty(errorMsg.requiredField)
});
/**
* Represents login data, used to authenticate users.
*/
export type IUserLoginDTO = z.infer<typeof schemaUserLogin>;
/**
* Represents data needed to update password for current user.
*/
export const schemaChangePassword = z
.object({
old_password: z.string().nonempty(errorMsg.requiredField),
new_password: z.string().nonempty(errorMsg.requiredField),
new_password2: z.string().nonempty(errorMsg.requiredField)
})
.refine(schema => schema.new_password === schema.new_password2, {
path: ['new_password2'],
message: errorMsg.passwordsMismatch
})
.refine(schema => schema.old_password !== schema.new_password, {
path: ['new_password'],
message: errorMsg.passwordsSame
});
/**
* Represents data needed to update password for current user.
*/
export type IChangePasswordDTO = z.infer<typeof schemaChangePassword>;
/**
* Represents password reset request data.
*/
export interface IRequestPasswordDTO {
email: string;
}
/**
* Represents password reset data.
*/
export interface IResetPasswordDTO {
password: string;
token: string;
}
/**
* Represents password token data.
*/
export interface IPasswordTokenDTO {
token: string;
}

View File

@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { authApi, IChangePasswordDTO } from './api'; import { authApi } from './api';
import { IChangePasswordDTO } from './types';
export const useChangePassword = () => { export const useChangePassword = () => {
const client = useQueryClient(); const client = useQueryClient();

View File

@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { authApi, IUserLoginDTO } from './api'; import { authApi } from './api';
import { IUserLoginDTO } from './types';
export const useLogin = () => { export const useLogin = () => {
const client = useQueryClient(); const client = useQueryClient();

View File

@ -1,6 +1,7 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import { authApi, IRequestPasswordDTO } from './api'; import { authApi } from './api';
import { IRequestPasswordDTO } from './types';
export const useRequestPasswordReset = () => { export const useRequestPasswordReset = () => {
const mutation = useMutation({ const mutation = useMutation({

View File

@ -1,6 +1,7 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import { authApi, IPasswordTokenDTO, IResetPasswordDTO } from './api'; import { authApi } from './api';
import { IPasswordTokenDTO, IResetPasswordDTO } from './types';
export const useResetPassword = () => { export const useResetPassword = () => {
const validateMutation = useMutation({ const validateMutation = useMutation({

View File

@ -1,10 +1,11 @@
import { urls, useConceptNavigation } from '@/app'; import { urls, useConceptNavigation } from '@/app';
import { TextURL } from '@/components/Control'; import { TextURL } from '@/components/Control';
import { useAuthSuspense } from '../backend/useAuth'; import { useAuthSuspense } from '../backend/useAuth';
import { useLogout } from '../backend/useLogout'; import { useLogout } from '../backend/useLogout';
function ExpectedAnonymous() { export function ExpectedAnonymous() {
const { user } = useAuthSuspense(); const { user } = useAuthSuspense();
const { logout } = useLogout(); const { logout } = useLogout();
const router = useConceptNavigation(); const router = useConceptNavigation();
@ -30,5 +31,3 @@ function ExpectedAnonymous() {
</div> </div>
); );
} }
export default ExpectedAnonymous;

View File

@ -4,7 +4,7 @@ import { TextURL } from '@/components/Control';
import { useAuthSuspense } from '../backend/useAuth'; import { useAuthSuspense } from '../backend/useAuth';
function RequireAuth({ children }: React.PropsWithChildren) { export function RequireAuth({ children }: React.PropsWithChildren) {
const { isAnonymous } = useAuthSuspense(); const { isAnonymous } = useAuthSuspense();
if (isAnonymous) { if (isAnonymous) {
@ -19,5 +19,3 @@ function RequireAuth({ children }: React.PropsWithChildren) {
} }
return <>{children}</>; return <>{children}</>;
} }
export default RequireAuth;

View File

@ -0,0 +1,5 @@
export { useAuthSuspense } from './backend/useAuth';
export { useChangePassword } from './backend/useChangePassword';
export { useLogout } from './backend/useLogout';
export { ExpectedAnonymous } from './components/ExpectedAnonymous';
export { RequireAuth } from './components/RequireAuth';

View File

@ -1,10 +1,11 @@
'use client'; 'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import clsx from 'clsx'; import clsx from 'clsx';
import { useForm } from 'react-hook-form';
import { urls, useConceptNavigation } from '@/app'; import { urls, useConceptNavigation } from '@/app';
import { isAxiosError } from '@/backend/apiTransport'; import { isAxiosError } from '@/backend/apiTransport';
import { SubmitButton, TextURL } from '@/components/Control'; import { SubmitButton, TextURL } from '@/components/Control';
import { ErrorData } from '@/components/InfoError'; import { ErrorData } from '@/components/InfoError';
@ -12,10 +13,10 @@ import { TextInput } from '@/components/Input';
import useQueryStrings from '@/hooks/useQueryStrings'; import useQueryStrings from '@/hooks/useQueryStrings';
import { resources } from '@/utils/constants'; import { resources } from '@/utils/constants';
import { IUserLoginDTO, schemaUserLogin } from '../backend/api'; import { IUserLoginDTO, schemaUserLogin } from '../backend/types';
import { useAuthSuspense } from '../backend/useAuth'; import { useAuthSuspense } from '../backend/useAuth';
import { useLogin } from '../backend/useLogin'; import { useLogin } from '../backend/useLogin';
import ExpectedAnonymous from '../components/ExpectedAnonymous'; import { ExpectedAnonymous } from '../components/ExpectedAnonymous';
function LoginPage() { function LoginPage() {
const router = useConceptNavigation(); const router = useConceptNavigation();
@ -26,7 +27,6 @@ function LoginPage() {
register, register,
handleSubmit, handleSubmit,
clearErrors, clearErrors,
resetField,
formState: { errors } formState: { errors }
} = useForm({ } = useForm({
resolver: zodResolver(schemaUserLogin), resolver: zodResolver(schemaUserLogin),
@ -38,7 +38,6 @@ function LoginPage() {
function onSubmit(data: IUserLoginDTO) { function onSubmit(data: IUserLoginDTO) {
return login(data).then(() => { return login(data).then(() => {
resetField('password');
if (router.canBack()) { if (router.canBack()) {
router.back(); router.back();
} else { } else {

View File

@ -1,9 +1,10 @@
'use client'; 'use client';
import clsx from 'clsx';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import clsx from 'clsx';
import { urls, useConceptNavigation } from '@/app'; import { urls, useConceptNavigation } from '@/app';
import { isAxiosError } from '@/backend/apiTransport'; import { isAxiosError } from '@/backend/apiTransport';
import { SubmitButton } from '@/components/Control'; import { SubmitButton } from '@/components/Control';
import { ErrorData, InfoError } from '@/components/InfoError'; import { ErrorData, InfoError } from '@/components/InfoError';

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx';
import { useState } from 'react'; import { useState } from 'react';
import clsx from 'clsx';
import { isAxiosError } from '@/backend/apiTransport'; import { isAxiosError } from '@/backend/apiTransport';
import { SubmitButton, TextURL } from '@/components/Control'; import { SubmitButton, TextURL } from '@/components/Control';

View File

@ -5,9 +5,10 @@ import { TextURL } from '@/components/Control';
import { IconHelp } from '@/components/Icons'; import { IconHelp } from '@/components/Icons';
import { Loader } from '@/components/Loader'; import { Loader } from '@/components/Loader';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import { HelpTopic } from '@/features/help/models/helpTopic';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import { HelpTopic } from '../models/helpTopic';
const TopicPage = React.lazy(() => import('@/features/help/pages/ManualsPage/TopicPage')); const TopicPage = React.lazy(() => import('@/features/help/pages/ManualsPage/TopicPage'));
interface BadgeHelpProps extends CProps.Styling { interface BadgeHelpProps extends CProps.Styling {

View File

@ -3,6 +3,7 @@ import clsx from 'clsx';
import { colorBgCstClass } from '@/features/rsform/colors'; import { colorBgCstClass } from '@/features/rsform/colors';
import { describeCstClass, labelCstClass } from '@/features/rsform/labels'; import { describeCstClass, labelCstClass } from '@/features/rsform/labels';
import { CstClass } from '@/features/rsform/models/rsform'; import { CstClass } from '@/features/rsform/models/rsform';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
interface InfoCstClassProps { interface InfoCstClassProps {

View File

@ -3,6 +3,7 @@ import clsx from 'clsx';
import { colorBgCstStatus } from '@/features/rsform/colors'; import { colorBgCstStatus } from '@/features/rsform/colors';
import { describeExpressionStatus, labelExpressionStatus } from '@/features/rsform/labels'; import { describeExpressionStatus, labelExpressionStatus } from '@/features/rsform/labels';
import { ExpressionStatus } from '@/features/rsform/models/rsform'; import { ExpressionStatus } from '@/features/rsform/models/rsform';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
interface InfoCstStatusProps { interface InfoCstStatusProps {

View File

@ -1,4 +1,5 @@
import { urls } from '@/app'; import { urls } from '@/app';
import { TextURL } from '@/components/Control'; import { TextURL } from '@/components/Control';
import { HelpTopic } from '../models/helpTopic'; import { HelpTopic } from '../models/helpTopic';

View File

@ -1,6 +1,7 @@
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
import { HelpTopic, topicParent } from '../models/helpTopic'; import { HelpTopic, topicParent } from '../models/helpTopic';
import { TopicItem } from './TopicItem'; import { TopicItem } from './TopicItem';
interface SubtopicsProps { interface SubtopicsProps {

View File

@ -0,0 +1,2 @@
export { BadgeHelp } from './components/BadgeHelp';
export { HelpTopic } from './models/helpTopic';

View File

@ -1,4 +1,5 @@
import { urls } from '@/app'; import { urls } from '@/app';
import { TextURL } from '@/components/Control'; import { TextURL } from '@/components/Control';
import { external_urls } from '@/utils/constants'; import { external_urls } from '@/utils/constants';

View File

@ -1,11 +1,13 @@
'use client'; 'use client';
import { urls, useConceptNavigation } from '@/app'; import { urls, useConceptNavigation } from '@/app';
import useQueryStrings from '@/hooks/useQueryStrings'; import useQueryStrings from '@/hooks/useQueryStrings';
import { useMainHeight } from '@/stores/appLayout'; import { useMainHeight } from '@/stores/appLayout';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { HelpTopic } from '../../models/helpTopic'; import { HelpTopic } from '../../models/helpTopic';
import TopicsList from './TopicsList'; import TopicsList from './TopicsList';
import ViewTopic from './ViewTopic'; import ViewTopic from './ViewTopic';

View File

@ -9,10 +9,8 @@ import { SelectTree } from '@/components/Input';
import { useAppLayoutStore, useFitHeight } from '@/stores/appLayout'; import { useAppLayoutStore, useFitHeight } from '@/stores/appLayout';
import { PARAMETER, prefixes } from '@/utils/constants'; import { PARAMETER, prefixes } from '@/utils/constants';
import { describeHelpTopic } from '../../labels'; import { describeHelpTopic, labelHelpTopic } from '../../labels';
import { labelHelpTopic } from '../../labels'; import { HelpTopic, topicParent } from '../../models/helpTopic';
import { topicParent } from '../../models/helpTopic';
import { HelpTopic } from '../../models/helpTopic';
interface TopicsDropdownProps { interface TopicsDropdownProps {
activeTopic: HelpTopic; activeTopic: HelpTopic;

View File

@ -3,6 +3,7 @@
import useWindowSize from '@/hooks/useWindowSize'; import useWindowSize from '@/hooks/useWindowSize';
import { HelpTopic } from '../../models/helpTopic'; import { HelpTopic } from '../../models/helpTopic';
import TopicsDropdown from './TopicsDropdown'; import TopicsDropdown from './TopicsDropdown';
import TopicsStatic from './TopicsStatic'; import TopicsStatic from './TopicsStatic';

View File

@ -1,10 +1,11 @@
'use client'; 'use client';
import TopicPage from '@/features/help/pages/ManualsPage/TopicPage';
import { useMainHeight } from '@/stores/appLayout'; import { useMainHeight } from '@/stores/appLayout';
import { HelpTopic } from '../../models/helpTopic'; import { HelpTopic } from '../../models/helpTopic';
import TopicPage from './TopicPage';
interface ViewTopicProps { interface ViewTopicProps {
topic: HelpTopic; topic: HelpTopic;
} }

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useEffect } from 'react'; import { useLayoutEffect } from 'react';
import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch'; import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch';
import { useAppLayoutStore, useFitHeight } from '@/stores/appLayout'; import { useAppLayoutStore, useFitHeight } from '@/stores/appLayout';
@ -10,7 +10,7 @@ export function Component() {
const hideFooter = useAppLayoutStore(state => state.hideFooter); const hideFooter = useAppLayoutStore(state => state.hideFooter);
const panelHeight = useFitHeight('0px'); const panelHeight = useFitHeight('0px');
useEffect(() => { useLayoutEffect(() => {
hideFooter(true); hideFooter(true);
return () => hideFooter(false); return () => hideFooter(false);
}, [hideFooter]); }, [hideFooter]);

View File

@ -1,27 +1,17 @@
import { useEffect } from 'react';
import { urls, useConceptNavigation } from '@/app'; import { urls, useConceptNavigation } from '@/app';
import { Loader } from '@/components/Loader'; import { useAuthSuspense } from '@/features/auth';
import { useAuthSuspense } from '@/features/auth/backend/useAuth';
import { PARAMETER } from '@/utils/constants';
function HomePage() { function HomePage() {
const router = useConceptNavigation(); const router = useConceptNavigation();
const { isAnonymous } = useAuthSuspense(); const { isAnonymous } = useAuthSuspense();
useEffect(() => { if (isAnonymous) {
if (isAnonymous) { router.replace(urls.manuals);
setTimeout(() => { } else {
router.replace(urls.manuals); router.replace(urls.library);
}, PARAMETER.refreshTimeout); }
} else {
setTimeout(() => {
router.replace(urls.library);
}, PARAMETER.refreshTimeout);
}
}, [router, isAnonymous]);
return <Loader />; return null;
} }
export default HomePage; export default HomePage;

View File

@ -1,145 +1,42 @@
import { queryOptions } from '@tanstack/react-query'; import { queryOptions } from '@tanstack/react-query';
import { z } from 'zod';
import {
IRSFormDTO,
IVersionCreatedResponse,
schemaRSForm,
schemaVersionCreatedResponse
} from '@/features/rsform/backend/types';
import { axiosDelete, axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport'; import { axiosDelete, axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport';
import { DELAYS } from '@/backend/configuration'; import { DELAYS, KEYS } from '@/backend/configuration';
import { ossApi } from '@/features/oss/backend/api'; import { infoMsg } from '@/utils/labels';
import { IRSFormDTO, rsformsApi } from '@/features/rsform/backend/api';
import { errorMsg, infoMsg } from '@/utils/labels';
import { AccessPolicy, ILibraryItem, IVersionInfo, LibraryItemID, LibraryItemType, VersionID } from '../models/library'; import {
import { validateLocation } from '../models/libraryAPI'; AccessPolicy,
ICloneLibraryItemDTO,
/** ICreateLibraryItemDTO,
* Represents update data for renaming Location. ILibraryItem,
*/ IRenameLocationDTO,
export interface IRenameLocationDTO { IUpdateLibraryItemDTO,
target: string; IVersionCreateDTO,
new_location: string; IVersionInfo,
} IVersionUpdateDTO,
schemaLibraryItem,
/** schemaLibraryItemArray,
* Represents data, used for cloning {@link IRSForm}. schemaVersionInfo
*/ } from './types';
export const schemaCloneLibraryItem = z.object({
id: z.number(),
item_type: z.nativeEnum(LibraryItemType),
title: z.string().nonempty(errorMsg.requiredField),
alias: z.string().nonempty(errorMsg.requiredField),
comment: z.string(),
visible: z.boolean(),
read_only: z.boolean(),
location: z.string().refine(data => validateLocation(data), { message: errorMsg.invalidLocation }),
access_policy: z.nativeEnum(AccessPolicy),
items: z.array(z.number()).optional()
});
/**
* Represents data, used for cloning {@link IRSForm}.
*/
export type ICloneLibraryItemDTO = z.infer<typeof schemaCloneLibraryItem>;
/**
* Represents data, used for creating {@link IRSForm}.
*/
export const schemaCreateLibraryItem = z
.object({
item_type: z.nativeEnum(LibraryItemType),
title: z.string().optional(),
alias: z.string().optional(),
comment: z.string(),
visible: z.boolean(),
read_only: z.boolean(),
location: z.string().refine(data => validateLocation(data), { message: errorMsg.invalidLocation }),
access_policy: z.nativeEnum(AccessPolicy),
file: z.instanceof(File).optional(),
fileName: z.string().optional()
})
.refine(data => !!data.file || !!data.title, {
path: ['title'],
message: errorMsg.requiredField
})
.refine(data => !!data.file || !!data.alias, {
path: ['alias'],
message: errorMsg.requiredField
});
/**
* Represents data, used for creating {@link IRSForm}.
*/
export type ICreateLibraryItemDTO = z.infer<typeof schemaCreateLibraryItem>;
/**
* Represents update data for editing {@link ILibraryItem}.
*/
export const schemaUpdateLibraryItem = z.object({
id: z.number(),
item_type: z.nativeEnum(LibraryItemType),
title: z.string().nonempty(errorMsg.requiredField),
alias: z.string().nonempty(errorMsg.requiredField),
comment: z.string(),
visible: z.boolean(),
read_only: z.boolean()
});
/**
* Represents update data for editing {@link ILibraryItem}.
*/
export type IUpdateLibraryItemDTO = z.infer<typeof schemaUpdateLibraryItem>;
/**
* Create version metadata in persistent storage.
*/
export const schemaVersionCreate = z.object({
version: z.string(),
description: z.string(),
items: z.array(z.number()).optional()
});
/**
* Create version metadata in persistent storage.
*/
export type IVersionCreateDTO = z.infer<typeof schemaVersionCreate>;
/**
* Represents data response when creating {@link IVersionInfo}.
*/
export interface IVersionCreatedResponse {
version: number;
schema: IRSFormDTO;
}
/**
* Represents version data, intended to update version metadata in persistent storage.
*/
export const schemaVersionUpdate = z.object({
id: z.number(),
version: z.string().nonempty(errorMsg.requiredField),
description: z.string()
});
/**
* Represents version data, intended to update version metadata in persistent storage.
*/
export type IVersionUpdateDTO = z.infer<typeof schemaVersionUpdate>;
export const libraryApi = { export const libraryApi = {
baseKey: 'library', baseKey: KEYS.library,
libraryListKey: ['library', 'list'], libraryListKey: KEYS.composite.libraryList,
getItemQueryOptions: ({ itemID, itemType }: { itemID: LibraryItemID; itemType: LibraryItemType }) => {
return itemType === LibraryItemType.RSFORM
? rsformsApi.getRSFormQueryOptions({ itemID })
: ossApi.getOssQueryOptions({ itemID });
},
getLibraryQueryOptions: ({ isAdmin }: { isAdmin: boolean }) => getLibraryQueryOptions: ({ isAdmin }: { isAdmin: boolean }) =>
queryOptions({ queryOptions({
queryKey: [...libraryApi.libraryListKey, isAdmin ? 'admin' : 'user'], queryKey: [...libraryApi.libraryListKey, isAdmin ? 'admin' : 'user'],
staleTime: DELAYS.staleMedium, staleTime: DELAYS.staleMedium,
queryFn: meta => queryFn: meta =>
axiosGet<ILibraryItem[]>({ axiosGet<ILibraryItem[]>({
schema: schemaLibraryItemArray,
endpoint: isAdmin ? '/api/library/all' : '/api/library/active', endpoint: isAdmin ? '/api/library/all' : '/api/library/active',
options: { signal: meta.signal } options: { signal: meta.signal }
}) })
@ -150,6 +47,7 @@ export const libraryApi = {
staleTime: DELAYS.staleMedium, staleTime: DELAYS.staleMedium,
queryFn: meta => queryFn: meta =>
axiosGet<ILibraryItem[]>({ axiosGet<ILibraryItem[]>({
schema: schemaLibraryItemArray,
endpoint: '/api/library/templates', endpoint: '/api/library/templates',
options: { signal: meta.signal } options: { signal: meta.signal }
}) })
@ -157,6 +55,7 @@ export const libraryApi = {
createItem: (data: ICreateLibraryItemDTO) => createItem: (data: ICreateLibraryItemDTO) =>
axiosPost<ICreateLibraryItemDTO, ILibraryItem>({ axiosPost<ICreateLibraryItemDTO, ILibraryItem>({
schema: schemaLibraryItem,
endpoint: !data.file ? '/api/library' : '/api/rsforms/create-detailed', endpoint: !data.file ? '/api/library' : '/api/rsforms/create-detailed',
request: { request: {
data: data, data: data,
@ -172,13 +71,14 @@ export const libraryApi = {
}), }),
updateItem: (data: IUpdateLibraryItemDTO) => updateItem: (data: IUpdateLibraryItemDTO) =>
axiosPatch<IUpdateLibraryItemDTO, ILibraryItem>({ axiosPatch<IUpdateLibraryItemDTO, ILibraryItem>({
schema: schemaLibraryItem,
endpoint: `/api/library/${data.id}`, endpoint: `/api/library/${data.id}`,
request: { request: {
data: data, data: data,
successMessage: infoMsg.changesSaved successMessage: infoMsg.changesSaved
} }
}), }),
setOwner: ({ itemID, owner }: { itemID: LibraryItemID; owner: number }) => setOwner: ({ itemID, owner }: { itemID: number; owner: number }) =>
axiosPatch({ axiosPatch({
endpoint: `/api/library/${itemID}/set-owner`, endpoint: `/api/library/${itemID}/set-owner`,
request: { request: {
@ -186,7 +86,7 @@ export const libraryApi = {
successMessage: infoMsg.changesSaved successMessage: infoMsg.changesSaved
} }
}), }),
setLocation: ({ itemID, location }: { itemID: LibraryItemID; location: string }) => setLocation: ({ itemID, location }: { itemID: number; location: string }) =>
axiosPatch({ axiosPatch({
endpoint: `/api/library/${itemID}/set-location`, endpoint: `/api/library/${itemID}/set-location`,
request: { request: {
@ -194,7 +94,7 @@ export const libraryApi = {
successMessage: infoMsg.moveComplete successMessage: infoMsg.moveComplete
} }
}), }),
setAccessPolicy: ({ itemID, policy }: { itemID: LibraryItemID; policy: AccessPolicy }) => setAccessPolicy: ({ itemID, policy }: { itemID: number; policy: AccessPolicy }) =>
axiosPatch({ axiosPatch({
endpoint: `/api/library/${itemID}/set-access-policy`, endpoint: `/api/library/${itemID}/set-access-policy`,
request: { request: {
@ -202,7 +102,7 @@ export const libraryApi = {
successMessage: infoMsg.changesSaved successMessage: infoMsg.changesSaved
} }
}), }),
setEditors: ({ itemID, editors }: { itemID: LibraryItemID; editors: number[] }) => setEditors: ({ itemID, editors }: { itemID: number; editors: number[] }) =>
axiosPatch({ axiosPatch({
endpoint: `/api/library/${itemID}/set-editors`, endpoint: `/api/library/${itemID}/set-editors`,
request: { request: {
@ -211,7 +111,7 @@ export const libraryApi = {
} }
}), }),
deleteItem: (target: LibraryItemID) => deleteItem: (target: number) =>
axiosDelete({ axiosDelete({
endpoint: `/api/library/${target}`, endpoint: `/api/library/${target}`,
request: { request: {
@ -220,6 +120,7 @@ export const libraryApi = {
}), }),
cloneItem: (data: ICloneLibraryItemDTO) => cloneItem: (data: ICloneLibraryItemDTO) =>
axiosPost<ICloneLibraryItemDTO, IRSFormDTO>({ axiosPost<ICloneLibraryItemDTO, IRSFormDTO>({
schema: schemaRSForm,
endpoint: `/api/library/${data.id}/clone`, endpoint: `/api/library/${data.id}/clone`,
request: { request: {
data: data, data: data,
@ -235,30 +136,33 @@ export const libraryApi = {
} }
}), }),
versionCreate: ({ itemID, data }: { itemID: LibraryItemID; data: IVersionCreateDTO }) => versionCreate: ({ itemID, data }: { itemID: number; data: IVersionCreateDTO }) =>
axiosPost<IVersionCreateDTO, IVersionCreatedResponse>({ axiosPost<IVersionCreateDTO, IVersionCreatedResponse>({
schema: schemaVersionCreatedResponse,
endpoint: `/api/library/${itemID}/create-version`, endpoint: `/api/library/${itemID}/create-version`,
request: { request: {
data: data, data: data,
successMessage: infoMsg.newVersion(data.version) successMessage: infoMsg.newVersion(data.version)
} }
}), }),
versionRestore: ({ versionID }: { versionID: VersionID }) => versionRestore: ({ versionID }: { versionID: number }) =>
axiosPatch<undefined, IRSFormDTO>({ axiosPatch<undefined, IRSFormDTO>({
schema: schemaRSForm,
endpoint: `/api/versions/${versionID}/restore`, endpoint: `/api/versions/${versionID}/restore`,
request: { request: {
successMessage: infoMsg.versionRestored successMessage: infoMsg.versionRestored
} }
}), }),
versionUpdate: (data: IVersionUpdateDTO) => versionUpdate: (data: { itemID: number; version: IVersionUpdateDTO }) =>
axiosPatch<IVersionUpdateDTO, IVersionInfo>({ axiosPatch<IVersionUpdateDTO, IVersionInfo>({
endpoint: `/api/versions/${data.id}`, schema: schemaVersionInfo,
endpoint: `/api/versions/${data.version.id}`,
request: { request: {
data: data, data: data.version,
successMessage: infoMsg.changesSaved successMessage: infoMsg.changesSaved
} }
}), }),
versionDelete: (data: { itemID: LibraryItemID; versionID: VersionID }) => versionDelete: (data: { itemID: number; versionID: number }) =>
axiosDelete({ axiosDelete({
endpoint: `/api/versions/${data.versionID}`, endpoint: `/api/versions/${data.versionID}`,
request: { request: {

View File

@ -0,0 +1,142 @@
import { z } from 'zod';
import { errorMsg } from '@/utils/labels';
import { validateLocation } from '../models/libraryAPI';
/** Represents type of library items. */
export enum LibraryItemType {
RSFORM = 'rsform',
OSS = 'oss'
}
/** Represents Access policy for library items.*/
export enum AccessPolicy {
PUBLIC = 'public',
PROTECTED = 'protected',
PRIVATE = 'private'
}
/** Represents library item common data typical for all item types. */
export type ILibraryItem = z.infer<typeof schemaLibraryItem>;
/** Represents {@link ILibraryItem} data loaded for both OSS and RSForm. */
export interface ILibraryItemData extends ILibraryItem {
editors: number[];
}
/** Represents update data for renaming Location. */
export interface IRenameLocationDTO {
target: string;
new_location: string;
}
/** Represents library item version information. */
export type IVersionInfo = z.infer<typeof schemaVersionInfo>;
/** Represents data, used for cloning {@link IRSForm}. */
export type ICloneLibraryItemDTO = z.infer<typeof schemaCloneLibraryItem>;
/** Represents data, used for creating {@link IRSForm}. */
export type ICreateLibraryItemDTO = z.infer<typeof schemaCreateLibraryItem>;
/** Represents update data for editing {@link ILibraryItem}. */
export type IUpdateLibraryItemDTO = z.infer<typeof schemaUpdateLibraryItem>;
/** Create version metadata in persistent storage. */
export type IVersionCreateDTO = z.infer<typeof schemaVersionCreate>;
/** Represents version data, intended to update version metadata in persistent storage. */
export type IVersionUpdateDTO = z.infer<typeof schemaVersionUpdate>;
// ======= SCHEMAS =========
export const schemaLibraryItem = z.object({
id: z.coerce.number(),
item_type: z.nativeEnum(LibraryItemType),
title: z.string(),
alias: z.string().nonempty(),
comment: z.string(),
visible: z.boolean(),
read_only: z.boolean(),
location: z.string(),
access_policy: z.nativeEnum(AccessPolicy),
time_create: z.string(),
time_update: z.string(),
owner: z.coerce.number().nullable()
});
export const schemaLibraryItemArray = z.array(schemaLibraryItem);
export const schemaCloneLibraryItem = schemaLibraryItem
.pick({
id: true,
item_type: true,
title: true,
alias: true,
comment: true,
visible: true,
read_only: true,
location: true,
access_policy: true
})
.extend({
title: z.string().nonempty(errorMsg.requiredField),
alias: z.string().nonempty(errorMsg.requiredField),
location: z.string().refine(data => validateLocation(data), { message: errorMsg.invalidLocation }),
items: z.array(z.number()).optional()
});
export const schemaCreateLibraryItem = z
.object({
item_type: z.nativeEnum(LibraryItemType),
title: z.string().optional(),
alias: z.string().optional(),
comment: z.string(),
visible: z.boolean(),
read_only: z.boolean(),
location: z.string().refine(data => validateLocation(data), { message: errorMsg.invalidLocation }),
access_policy: z.nativeEnum(AccessPolicy),
file: z.instanceof(File).optional(),
fileName: z.string().optional()
})
.refine(data => !!data.file || !!data.title, {
path: ['title'],
message: errorMsg.requiredField
})
.refine(data => !!data.file || !!data.alias, {
path: ['alias'],
message: errorMsg.requiredField
});
export const schemaUpdateLibraryItem = z.object({
id: z.number(),
item_type: z.nativeEnum(LibraryItemType),
title: z.string().nonempty(errorMsg.requiredField),
alias: z.string().nonempty(errorMsg.requiredField),
comment: z.string(),
visible: z.boolean(),
read_only: z.boolean()
});
export const schemaVersionInfo = z.object({
id: z.coerce.number(),
version: z.string(),
description: z.string(),
time_create: z.string()
});
export const schemaVersionUpdate = z.object({
id: z.number(),
version: z.string().nonempty(errorMsg.requiredField),
description: z.string()
});
export const schemaVersionCreate = z.object({
version: z.string(),
description: z.string(),
items: z.array(z.number()).optional()
});

View File

@ -1,7 +1,8 @@
import { useAuthSuspense } from '@/features/auth/backend/useAuth'; import { useAuthSuspense } from '@/features/auth';
import { ILibraryFilter } from '../models/library'; import { ILibraryFilter } from '../models/library';
import { matchLibraryItem, matchLibraryItemLocation } from '../models/libraryAPI'; import { matchLibraryItem, matchLibraryItemLocation } from '../models/libraryAPI';
import { useLibrary } from './useLibrary'; import { useLibrary } from './useLibrary';
export function useApplyLibraryFilter(filter: ILibraryFilter) { export function useApplyLibraryFilter(filter: ILibraryFilter) {

View File

@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { ICloneLibraryItemDTO, libraryApi } from './api'; import { libraryApi } from './api';
import { ICloneLibraryItemDTO } from './types';
export const useCloneItem = () => { export const useCloneItem = () => {
const client = useQueryClient(); const client = useQueryClient();

View File

@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { ICreateLibraryItemDTO, libraryApi } from './api'; import { libraryApi } from './api';
import { ICreateLibraryItemDTO } from './types';
export const useCreateItem = () => { export const useCreateItem = () => {
const client = useQueryClient(); const client = useQueryClient();

View File

@ -1,10 +1,8 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { ossApi } from '@/features/oss/backend/api'; import { KEYS } from '@/backend/configuration';
import { rsformsApi } from '@/features/rsform/backend/api';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { LibraryItemID } from '../models/library';
import { libraryApi } from './api'; import { libraryApi } from './api';
export const useDeleteItem = () => { export const useDeleteItem = () => {
@ -17,16 +15,16 @@ export const useDeleteItem = () => {
setTimeout( setTimeout(
() => () =>
void Promise.allSettled([ void Promise.allSettled([
client.invalidateQueries({ queryKey: [ossApi.baseKey] }), client.invalidateQueries({ queryKey: [KEYS.oss] }),
client.resetQueries({ queryKey: rsformsApi.getRSFormQueryOptions({ itemID: variables }).queryKey }), client.resetQueries({ queryKey: KEYS.composite.rsItem({ itemID: variables }) }),
client.resetQueries({ queryKey: ossApi.getOssQueryOptions({ itemID: variables }).queryKey }) client.resetQueries({ queryKey: KEYS.composite.ossItem({ itemID: variables }) })
]).catch(console.error), ]).catch(console.error),
PARAMETER.navigationDuration PARAMETER.navigationDuration
); );
} }
}); });
return { return {
deleteItem: (target: LibraryItemID) => mutation.mutateAsync(target), deleteItem: (target: number) => mutation.mutateAsync(target),
isPending: mutation.isPending isPending: mutation.isPending
}; };
}; };

View File

@ -1,6 +1,6 @@
import { FolderTree } from '@/features/library/models/FolderTree'; import { FolderTree } from '../models/FolderTree';
import { LocationHead } from '../models/library'; import { LocationHead } from '../models/library';
import { useLibrary } from './useLibrary'; import { useLibrary } from './useLibrary';
export function useFolders() { export function useFolders() {

View File

@ -1,7 +1,8 @@
import { useQuery, useSuspenseQuery } from '@tanstack/react-query'; import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
import { useAuthSuspense } from '@/features/auth';
import { queryClient } from '@/backend/queryClient'; import { queryClient } from '@/backend/queryClient';
import { useAuthSuspense } from '@/features/auth/backend/useAuth';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import { libraryApi } from './api'; import { libraryApi } from './api';

View File

@ -1,13 +1,10 @@
import { useIsMutating } from '@tanstack/react-query'; import { useIsMutating } from '@tanstack/react-query';
import { ossApi } from '@/features/oss/backend/api'; import { KEYS } from '@/backend/configuration';
import { rsformsApi } from '@/features/rsform/backend/api';
import { libraryApi } from './api';
export const useMutatingLibrary = () => { export const useMutatingLibrary = () => {
const countMutations = useIsMutating({ mutationKey: [libraryApi.baseKey] }); const countMutations = useIsMutating({ mutationKey: [KEYS.library] });
const countOss = useIsMutating({ mutationKey: [ossApi.baseKey] }); const countOss = useIsMutating({ mutationKey: [KEYS.oss] });
const countRSForm = useIsMutating({ mutationKey: [rsformsApi.baseKey] }); const countRSForm = useIsMutating({ mutationKey: [KEYS.rsform] });
return countMutations + countOss + countRSForm !== 0; return countMutations + countOss + countRSForm !== 0;
}; };

View File

@ -1,9 +1,9 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { ossApi } from '@/features/oss/backend/api'; import { KEYS } from '@/backend/configuration';
import { rsformsApi } from '@/features/rsform/backend/api';
import { IRenameLocationDTO, libraryApi } from './api'; import { libraryApi } from './api';
import { IRenameLocationDTO } from './types';
export const useRenameLocation = () => { export const useRenameLocation = () => {
const client = useQueryClient(); const client = useQueryClient();
@ -12,9 +12,9 @@ export const useRenameLocation = () => {
mutationFn: libraryApi.renameLocation, mutationFn: libraryApi.renameLocation,
onSuccess: () => onSuccess: () =>
Promise.allSettled([ Promise.allSettled([
client.invalidateQueries({ queryKey: [libraryApi.baseKey] }), client.invalidateQueries({ queryKey: [KEYS.library] }),
client.invalidateQueries({ queryKey: [rsformsApi.baseKey] }), client.invalidateQueries({ queryKey: [KEYS.rsform] }),
client.invalidateQueries({ queryKey: [ossApi.baseKey] }) client.invalidateQueries({ queryKey: [KEYS.oss] })
]) ])
}); });
return { return {

View File

@ -1,10 +1,12 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { IOperationSchemaDTO, ossApi } from '@/features/oss/backend/api'; import { IOperationSchemaDTO } from '@/features/oss/backend/types';
import { rsformsApi } from '@/features/rsform/backend/api'; import { IRSFormDTO } from '@/features/rsform/backend/types';
import { KEYS } from '@/backend/configuration';
import { AccessPolicy, ILibraryItem, LibraryItemID } from '../models/library';
import { libraryApi } from './api'; import { libraryApi } from './api';
import { AccessPolicy, ILibraryItem } from './types';
export const useSetAccessPolicy = () => { export const useSetAccessPolicy = () => {
const client = useQueryClient(); const client = useQueryClient();
@ -12,26 +14,28 @@ export const useSetAccessPolicy = () => {
mutationKey: [libraryApi.baseKey, 'set-location'], mutationKey: [libraryApi.baseKey, 'set-location'],
mutationFn: libraryApi.setAccessPolicy, mutationFn: libraryApi.setAccessPolicy,
onSuccess: (_, variables) => { onSuccess: (_, variables) => {
const ossKey = ossApi.getOssQueryOptions({ itemID: variables.itemID }).queryKey; const ossKey = KEYS.composite.ossItem({ itemID: variables.itemID });
const ossData: IOperationSchemaDTO | undefined = client.getQueryData(ossKey); const ossData: IOperationSchemaDTO | undefined = client.getQueryData(ossKey);
if (ossData) { if (ossData) {
client.setQueryData(ossKey, { ...ossData, access_policy: variables.policy }); client.setQueryData(ossKey, { ...ossData, access_policy: variables.policy });
return Promise.allSettled([ return Promise.allSettled([
client.invalidateQueries({ queryKey: libraryApi.libraryListKey }), client.invalidateQueries({ queryKey: KEYS.composite.libraryList }),
...ossData.items ...ossData.items
.map(item => { .map(item => {
if (!item.result) { if (!item.result) {
return; return;
} }
const itemKey = rsformsApi.getRSFormQueryOptions({ itemID: item.result }).queryKey; const itemKey = KEYS.composite.rsItem({ itemID: item.result });
return client.invalidateQueries({ queryKey: itemKey }); return client.invalidateQueries({ queryKey: itemKey });
}) })
.filter(item => !!item) .filter(item => !!item)
]); ]);
} }
const rsKey = rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey; const rsKey = KEYS.composite.rsItem({ itemID: variables.itemID });
client.setQueryData(rsKey, prev => (!prev ? undefined : { ...prev, access_policy: variables.policy })); client.setQueryData(rsKey, (prev: IRSFormDTO | undefined) =>
!prev ? undefined : { ...prev, access_policy: variables.policy }
);
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) => client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
prev?.map(item => (item.id === variables.itemID ? { ...item, access_policy: variables.policy } : item)) prev?.map(item => (item.id === variables.itemID ? { ...item, access_policy: variables.policy } : item))
); );
@ -39,6 +43,6 @@ export const useSetAccessPolicy = () => {
}); });
return { return {
setAccessPolicy: (data: { itemID: LibraryItemID; policy: AccessPolicy }) => mutation.mutateAsync(data) setAccessPolicy: (data: { itemID: number; policy: AccessPolicy }) => mutation.mutateAsync(data)
}; };
}; };

View File

@ -1,7 +1,9 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { ossApi } from '@/features/oss/backend/api'; import { IOperationSchemaDTO } from '@/features/oss/backend/types';
import { rsformsApi } from '@/features/rsform/backend/api'; import { IRSFormDTO } from '@/features/rsform/backend/types';
import { KEYS } from '@/backend/configuration';
import { libraryApi } from './api'; import { libraryApi } from './api';
@ -11,8 +13,8 @@ export const useSetEditors = () => {
mutationKey: [libraryApi.baseKey, 'set-location'], mutationKey: [libraryApi.baseKey, 'set-location'],
mutationFn: libraryApi.setEditors, mutationFn: libraryApi.setEditors,
onSuccess: (_, variables) => { onSuccess: (_, variables) => {
const ossKey = ossApi.getOssQueryOptions({ itemID: variables.itemID }).queryKey; const ossKey = KEYS.composite.ossItem({ itemID: variables.itemID });
const ossData = client.getQueryData(ossKey); const ossData: IOperationSchemaDTO | undefined = client.getQueryData(ossKey);
if (ossData) { if (ossData) {
client.setQueryData(ossKey, { ...ossData, editors: variables.editors }); client.setQueryData(ossKey, { ...ossData, editors: variables.editors });
return Promise.allSettled( return Promise.allSettled(
@ -21,15 +23,17 @@ export const useSetEditors = () => {
if (!item.result) { if (!item.result) {
return; return;
} }
const itemKey = rsformsApi.getRSFormQueryOptions({ itemID: item.result }).queryKey; const itemKey = KEYS.composite.rsItem({ itemID: item.result });
return client.invalidateQueries({ queryKey: itemKey }); return client.invalidateQueries({ queryKey: itemKey });
}) })
.filter(item => !!item) .filter(item => !!item)
); );
} }
const rsKey = rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey; const rsKey = KEYS.composite.rsItem({ itemID: variables.itemID });
client.setQueryData(rsKey, prev => (!prev ? undefined : { ...prev, editors: variables.editors })); client.setQueryData(rsKey, (prev: IRSFormDTO | undefined) =>
!prev ? undefined : { ...prev, editors: variables.editors }
);
} }
}); });

View File

@ -1,10 +1,12 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { IOperationSchemaDTO, ossApi } from '@/features/oss/backend/api'; import { IOperationSchemaDTO } from '@/features/oss/backend/types';
import { rsformsApi } from '@/features/rsform/backend/api'; import { IRSFormDTO } from '@/features/rsform/backend/types';
import { KEYS } from '@/backend/configuration';
import { ILibraryItem, LibraryItemID } from '../models/library';
import { libraryApi } from './api'; import { libraryApi } from './api';
import { ILibraryItem } from './types';
export const useSetLocation = () => { export const useSetLocation = () => {
const client = useQueryClient(); const client = useQueryClient();
@ -12,7 +14,7 @@ export const useSetLocation = () => {
mutationKey: [libraryApi.baseKey, 'set-location'], mutationKey: [libraryApi.baseKey, 'set-location'],
mutationFn: libraryApi.setLocation, mutationFn: libraryApi.setLocation,
onSuccess: (_, variables) => { onSuccess: (_, variables) => {
const ossKey = ossApi.getOssQueryOptions({ itemID: variables.itemID }).queryKey; const ossKey = KEYS.composite.ossItem({ itemID: variables.itemID });
const ossData: IOperationSchemaDTO | undefined = client.getQueryData(ossKey); const ossData: IOperationSchemaDTO | undefined = client.getQueryData(ossKey);
if (ossData) { if (ossData) {
client.setQueryData(ossKey, { ...ossData, location: variables.location }); client.setQueryData(ossKey, { ...ossData, location: variables.location });
@ -23,15 +25,17 @@ export const useSetLocation = () => {
if (!item.result) { if (!item.result) {
return; return;
} }
const itemKey = rsformsApi.getRSFormQueryOptions({ itemID: item.result }).queryKey; const itemKey = KEYS.composite.rsItem({ itemID: item.result });
return client.invalidateQueries({ queryKey: itemKey }); return client.invalidateQueries({ queryKey: itemKey });
}) })
.filter(item => !!item) .filter(item => !!item)
]); ]);
} }
const rsKey = rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey; const rsKey = KEYS.composite.rsItem({ itemID: variables.itemID });
client.setQueryData(rsKey, prev => (!prev ? undefined : { ...prev, location: variables.location })); client.setQueryData(rsKey, (prev: IRSFormDTO | undefined) =>
!prev ? undefined : { ...prev, location: variables.location }
);
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) => client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
prev?.map(item => (item.id === variables.itemID ? { ...item, location: variables.location } : item)) prev?.map(item => (item.id === variables.itemID ? { ...item, location: variables.location } : item))
); );
@ -39,6 +43,6 @@ export const useSetLocation = () => {
}); });
return { return {
setLocation: (data: { itemID: LibraryItemID; location: string }) => mutation.mutateAsync(data) setLocation: (data: { itemID: number; location: string }) => mutation.mutateAsync(data)
}; };
}; };

View File

@ -1,10 +1,12 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { IOperationSchemaDTO, ossApi } from '@/features/oss/backend/api'; import { IOperationSchemaDTO } from '@/features/oss/backend/types';
import { rsformsApi } from '@/features/rsform/backend/api'; import { IRSFormDTO } from '@/features/rsform/backend/types';
import { KEYS } from '@/backend/configuration';
import { ILibraryItem } from '../models/library';
import { libraryApi } from './api'; import { libraryApi } from './api';
import { ILibraryItem } from './types';
export const useSetOwner = () => { export const useSetOwner = () => {
const client = useQueryClient(); const client = useQueryClient();
@ -12,7 +14,7 @@ export const useSetOwner = () => {
mutationKey: [libraryApi.baseKey, 'set-owner'], mutationKey: [libraryApi.baseKey, 'set-owner'],
mutationFn: libraryApi.setOwner, mutationFn: libraryApi.setOwner,
onSuccess: (_, variables) => { onSuccess: (_, variables) => {
const ossKey = ossApi.getOssQueryOptions({ itemID: variables.itemID }).queryKey; const ossKey = KEYS.composite.ossItem({ itemID: variables.itemID });
const ossData: IOperationSchemaDTO | undefined = client.getQueryData(ossKey); const ossData: IOperationSchemaDTO | undefined = client.getQueryData(ossKey);
if (ossData) { if (ossData) {
client.setQueryData(ossKey, { ...ossData, owner: variables.owner }); client.setQueryData(ossKey, { ...ossData, owner: variables.owner });
@ -23,15 +25,17 @@ export const useSetOwner = () => {
if (!item.result) { if (!item.result) {
return; return;
} }
const itemKey = rsformsApi.getRSFormQueryOptions({ itemID: item.result }).queryKey; const itemKey = KEYS.composite.rsItem({ itemID: item.result });
return client.invalidateQueries({ queryKey: itemKey }); return client.invalidateQueries({ queryKey: itemKey });
}) })
.filter(item => !!item) .filter(item => !!item)
]); ]);
} }
const rsKey = rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey; const rsKey = KEYS.composite.rsItem({ itemID: variables.itemID });
client.setQueryData(rsKey, prev => (!prev ? undefined : { ...prev, owner: variables.owner })); client.setQueryData(rsKey, (prev: IRSFormDTO | undefined) =>
!prev ? undefined : { ...prev, owner: variables.owner }
);
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) => client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
prev?.map(item => (item.id === variables.itemID ? { ...item, owner: variables.owner } : item)) prev?.map(item => (item.id === variables.itemID ? { ...item, owner: variables.owner } : item))
); );

View File

@ -1,10 +1,12 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { IOperationSchemaDTO, ossApi } from '@/features/oss/backend/api'; import { IOperationSchemaDTO } from '@/features/oss/backend/types';
import { IRSFormDTO } from '@/features/rsform/backend/api'; import { IRSFormDTO } from '@/features/rsform/backend/types';
import { ILibraryItem, LibraryItemType } from '../models/library'; import { KEYS } from '@/backend/configuration';
import { IUpdateLibraryItemDTO, libraryApi } from './api';
import { libraryApi } from './api';
import { ILibraryItem, IUpdateLibraryItemDTO, LibraryItemType } from './types';
export const useUpdateItem = () => { export const useUpdateItem = () => {
const client = useQueryClient(); const client = useQueryClient();
@ -12,7 +14,10 @@ export const useUpdateItem = () => {
mutationKey: [libraryApi.baseKey, 'update-item'], mutationKey: [libraryApi.baseKey, 'update-item'],
mutationFn: libraryApi.updateItem, mutationFn: libraryApi.updateItem,
onSuccess: (data: ILibraryItem) => { onSuccess: (data: ILibraryItem) => {
const itemKey = libraryApi.getItemQueryOptions({ itemID: data.id, itemType: data.item_type }).queryKey; const itemKey =
data.item_type === LibraryItemType.RSFORM
? KEYS.composite.rsItem({ itemID: data.id })
: KEYS.composite.ossItem({ itemID: data.id });
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) => client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
prev?.map(item => (item.id === data.id ? data : item)) prev?.map(item => (item.id === data.id ? data : item))
); );
@ -23,9 +28,7 @@ export const useUpdateItem = () => {
const schema: IRSFormDTO | undefined = client.getQueryData(itemKey); const schema: IRSFormDTO | undefined = client.getQueryData(itemKey);
if (schema) { if (schema) {
return Promise.allSettled( return Promise.allSettled(
schema.oss.map(item => schema.oss.map(item => client.invalidateQueries({ queryKey: KEYS.composite.ossItem({ itemID: item.id }) }))
client.invalidateQueries({ queryKey: ossApi.getOssQueryOptions({ itemID: item.id }).queryKey })
)
); );
} }
} }

View File

@ -1,12 +1,12 @@
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { ILibraryItem, LibraryItemID } from '../models/library';
import { libraryApi } from './api'; import { libraryApi } from './api';
import { ILibraryItem } from './types';
export function useUpdateTimestamp() { export function useUpdateTimestamp() {
const client = useQueryClient(); const client = useQueryClient();
return { return {
updateTimestamp: (target: LibraryItemID) => updateTimestamp: (target: number) =>
client.setQueryData( client.setQueryData(
libraryApi.libraryListKey, // libraryApi.libraryListKey, //
(prev: ILibraryItem[] | undefined) => (prev: ILibraryItem[] | undefined) =>

View File

@ -1,9 +1,9 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { rsformsApi } from '@/features/rsform/backend/api'; import { KEYS } from '@/backend/configuration';
import { LibraryItemID } from '../models/library'; import { libraryApi } from './api';
import { IVersionCreateDTO, libraryApi } from './api'; import { IVersionCreateDTO } from './types';
import { useUpdateTimestamp } from './useUpdateTimestamp'; import { useUpdateTimestamp } from './useUpdateTimestamp';
export const useVersionCreate = () => { export const useVersionCreate = () => {
@ -13,12 +13,12 @@ export const useVersionCreate = () => {
mutationKey: [libraryApi.baseKey, 'create-version'], mutationKey: [libraryApi.baseKey, 'create-version'],
mutationFn: libraryApi.versionCreate, mutationFn: libraryApi.versionCreate,
onSuccess: data => { onSuccess: data => {
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.schema.id }).queryKey, data.schema); client.setQueryData(KEYS.composite.rsItem({ itemID: data.schema.id }), data.schema);
updateTimestamp(data.schema.id); updateTimestamp(data.schema.id);
} }
}); });
return { return {
versionCreate: (data: { itemID: LibraryItemID; data: IVersionCreateDTO }) => versionCreate: (data: { itemID: number; data: IVersionCreateDTO }) =>
mutation.mutateAsync(data).then(response => response.version) mutation.mutateAsync(data).then(response => response.version)
}; };
}; };

View File

@ -1,8 +1,9 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { IRSFormDTO, rsformsApi } from '@/features/rsform/backend/api'; import { IRSFormDTO } from '@/features/rsform/backend/types';
import { KEYS } from '@/backend/configuration';
import { LibraryItemID, VersionID } from '../models/library';
import { libraryApi } from './api'; import { libraryApi } from './api';
export const useVersionDelete = () => { export const useVersionDelete = () => {
@ -11,19 +12,17 @@ export const useVersionDelete = () => {
mutationKey: [libraryApi.baseKey, 'delete-version'], mutationKey: [libraryApi.baseKey, 'delete-version'],
mutationFn: libraryApi.versionDelete, mutationFn: libraryApi.versionDelete,
onSuccess: (_, variables) => { onSuccess: (_, variables) => {
client.setQueryData( client.setQueryData(KEYS.composite.rsItem({ itemID: variables.itemID }), (prev: IRSFormDTO | undefined) =>
rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey, !prev
(prev: IRSFormDTO | undefined) => ? undefined
!prev : {
? undefined ...prev,
: { versions: prev.versions.filter(version => version.id !== variables.versionID)
...prev, }
versions: prev.versions.filter(version => version.id !== variables.versionID)
}
); );
} }
}); });
return { return {
versionDelete: (data: { itemID: LibraryItemID; versionID: VersionID }) => mutation.mutateAsync(data) versionDelete: (data: { itemID: number; versionID: number }) => mutation.mutateAsync(data)
}; };
}; };

View File

@ -1,8 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { rsformsApi } from '@/features/rsform/backend/api'; import { KEYS } from '@/backend/configuration';
import { VersionID } from '../models/library';
import { libraryApi } from './api'; import { libraryApi } from './api';
export const useVersionRestore = () => { export const useVersionRestore = () => {
@ -11,11 +10,11 @@ export const useVersionRestore = () => {
mutationKey: [libraryApi.baseKey, 'restore-version'], mutationKey: [libraryApi.baseKey, 'restore-version'],
mutationFn: libraryApi.versionRestore, mutationFn: libraryApi.versionRestore,
onSuccess: data => { onSuccess: data => {
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data); client.setQueryData(KEYS.composite.rsItem({ itemID: data.id }), data);
return client.invalidateQueries({ queryKey: [libraryApi.baseKey] }); return client.invalidateQueries({ queryKey: [libraryApi.baseKey] });
} }
}); });
return { return {
versionRestore: (data: { versionID: VersionID }) => mutation.mutateAsync(data) versionRestore: (data: { versionID: number }) => mutation.mutateAsync(data)
}; };
}; };

View File

@ -1,32 +1,31 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { IRSFormDTO, rsformsApi } from '@/features/rsform/backend/api'; import { IRSFormDTO } from '@/features/rsform/backend/types';
import { IVersionUpdateDTO, libraryApi } from './api'; import { KEYS } from '@/backend/configuration';
import { libraryApi } from './api';
import { IVersionUpdateDTO } from './types';
export const useVersionUpdate = () => { export const useVersionUpdate = () => {
const client = useQueryClient(); const client = useQueryClient();
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [libraryApi.baseKey, 'update-version'], mutationKey: [libraryApi.baseKey, 'update-version'],
mutationFn: libraryApi.versionUpdate, mutationFn: libraryApi.versionUpdate,
onSuccess: data => { onSuccess: (data, variables) => {
client.setQueryData( client.setQueryData(KEYS.composite.rsItem({ itemID: variables.itemID }), (prev: IRSFormDTO | undefined) =>
rsformsApi.getRSFormQueryOptions({ itemID: data.item }).queryKey, !prev
(prev: IRSFormDTO | undefined) => ? undefined
!prev : {
? undefined ...prev,
: { versions: prev.versions.map(version =>
...prev, version.id === data.id ? { ...version, description: data.description, version: data.version } : version
versions: prev.versions.map(version => )
version.id === data.id }
? { ...version, description: data.description, version: data.version }
: version
)
}
); );
} }
}); });
return { return {
versionUpdate: (data: IVersionUpdateDTO) => mutation.mutateAsync(data) versionUpdate: (data: { itemID: number; version: IVersionUpdateDTO }) => mutation.mutateAsync(data)
}; };
}; };

View File

@ -2,6 +2,8 @@ import { Suspense } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { urls, useConceptNavigation } from '@/app'; import { urls, useConceptNavigation } from '@/app';
import { InfoUsers, SelectUser, useLabelUser, useRoleStore, UserRole } from '@/features/users';
import { Overlay, Tooltip } from '@/components/Container'; import { Overlay, Tooltip } from '@/components/Container';
import { MiniButton } from '@/components/Control'; import { MiniButton } from '@/components/Control';
import { useDropdown } from '@/components/Dropdown'; import { useDropdown } from '@/components/Dropdown';
@ -16,19 +18,28 @@ import {
import { Loader } from '@/components/Loader'; import { Loader } from '@/components/Loader';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import { ValueIcon } from '@/components/View'; import { ValueIcon } from '@/components/View';
import { InfoUsers, SelectUser, useLabelUser, useRoleStore } from '@/features/users';
import { UserRole } from '@/features/users/models/user';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { useModificationStore } from '@/stores/modification'; import { useModificationStore } from '@/stores/modification';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
import { promptText } from '@/utils/labels'; import { promptText } from '@/utils/labels';
import { ILibraryItemData } from '../backend/types';
import { useMutatingLibrary } from '../backend/useMutatingLibrary'; import { useMutatingLibrary } from '../backend/useMutatingLibrary';
import { useSetLocation } from '../backend/useSetLocation'; import { useSetLocation } from '../backend/useSetLocation';
import { useSetOwner } from '../backend/useSetOwner'; import { useSetOwner } from '../backend/useSetOwner';
import { ILibraryItemEditor } from '../models/library';
import { useLibrarySearchStore } from '../stores/librarySearch'; import { useLibrarySearchStore } from '../stores/librarySearch';
/**
* Represents common {@link ILibraryItem} editor controller.
*/
export interface ILibraryItemEditor {
schema: ILibraryItemData;
deleteSchema: () => void;
isMutable: boolean;
isAttachedToOSS: boolean;
}
interface EditorLibraryItemProps { interface EditorLibraryItemProps {
controller: ILibraryItemEditor; controller: ILibraryItemEditor;
} }

View File

@ -7,15 +7,16 @@ import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
import { IconOSS } from '@/components/Icons'; import { IconOSS } from '@/components/Icons';
import { Label } from '@/components/Input'; import { Label } from '@/components/Input';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import { ILibraryItemReference } from '@/features/library/models/library';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
import { ILibraryItemReference } from '../models/library';
interface MiniSelectorOSSProps extends CProps.Styling { interface MiniSelectorOSSProps extends CProps.Styling {
items: ILibraryItemReference[]; items: ILibraryItemReference[];
onSelect: (event: CProps.EventMouse, newValue: ILibraryItemReference) => void; onSelect: (event: CProps.EventMouse, newValue: ILibraryItemReference) => void;
} }
function MiniSelectorOSS({ items, onSelect, className, ...restProps }: MiniSelectorOSSProps) { export function MiniSelectorOSS({ items, onSelect, className, ...restProps }: MiniSelectorOSSProps) {
const ossMenu = useDropdown(); const ossMenu = useDropdown();
function onToggle(event: CProps.EventMouse) { function onToggle(event: CProps.EventMouse) {
@ -50,5 +51,3 @@ function MiniSelectorOSS({ items, onSelect, className, ...restProps }: MiniSelec
</div> </div>
); );
} }
export default MiniSelectorOSS;

View File

@ -1,25 +1,26 @@
import clsx from 'clsx';
import { useState } from 'react'; import { useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import clsx from 'clsx';
import { FlexColumn } from '@/components/Container'; import { FlexColumn } from '@/components/Container';
import { MiniButton } from '@/components/Control'; import { MiniButton } from '@/components/Control';
import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/DataTable'; import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/DataTable';
import { Dropdown, useDropdown } from '@/components/Dropdown'; import { Dropdown, useDropdown } from '@/components/Dropdown';
import { IconClose, IconFolderTree } from '@/components/Icons'; import { IconClose, IconFolderTree } from '@/components/Icons';
import { SearchBar } from '@/components/Input';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import { SearchBar } from '@/components/shared/SearchBar';
import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/features/library/models/library';
import { matchLibraryItem } from '@/features/library/models/libraryAPI';
import { APP_COLORS } from '@/styling/colors'; import { APP_COLORS } from '@/styling/colors';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
import SelectLocation from '../../library/components/SelectLocation'; import { ILibraryItem, LibraryItemType } from '../backend/types';
import { matchLibraryItem } from '../models/libraryAPI';
import SelectLocation from './SelectLocation';
interface PickSchemaProps extends CProps.Styling { interface PickSchemaProps extends CProps.Styling {
id?: string; id?: string;
value: LibraryItemID | null; value: number | null;
onChange: (newValue: LibraryItemID) => void; onChange: (newValue: number) => void;
initialFilter?: string; initialFilter?: string;
rows?: number; rows?: number;

View File

@ -1,13 +1,14 @@
'use client'; 'use client';
import { MiniButton } from '@/components/Control'; import { MiniButton } from '@/components/Control';
import { PolicyIcon } from '@/components/DomainIcons'; import { DomIconProps } from '@/components/DomainIcons';
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown'; import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
import { IconPrivate, IconProtected, IconPublic } from '@/components/Icons';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
import { describeAccessPolicy, labelAccessPolicy } from '@/utils/labels';
import { AccessPolicy } from '../models/library'; import { AccessPolicy } from '../backend/types';
import { describeAccessPolicy, labelAccessPolicy } from '../labels';
interface SelectAccessPolicyProps extends CProps.Styling { interface SelectAccessPolicyProps extends CProps.Styling {
value: AccessPolicy; value: AccessPolicy;
@ -17,7 +18,7 @@ interface SelectAccessPolicyProps extends CProps.Styling {
stretchLeft?: boolean; stretchLeft?: boolean;
} }
function SelectAccessPolicy({ value, disabled, stretchLeft, onChange, ...restProps }: SelectAccessPolicyProps) { export function SelectAccessPolicy({ value, disabled, stretchLeft, onChange, ...restProps }: SelectAccessPolicyProps) {
const menu = useDropdown(); const menu = useDropdown();
function handleChange(newValue: AccessPolicy) { function handleChange(newValue: AccessPolicy) {
@ -52,4 +53,14 @@ function SelectAccessPolicy({ value, disabled, stretchLeft, onChange, ...restPro
); );
} }
export default SelectAccessPolicy; /** Icon for access policy. */
function PolicyIcon({ value, size = '1.25rem', className }: DomIconProps<AccessPolicy>) {
switch (value) {
case AccessPolicy.PRIVATE:
return <IconPrivate size={size} className={className ?? 'text-warn-600'} />;
case AccessPolicy.PROTECTED:
return <IconProtected size={size} className={className ?? 'text-sec-600'} />;
case AccessPolicy.PUBLIC:
return <IconPublic size={size} className={className ?? 'text-ok-600'} />;
}
}

View File

@ -1,13 +1,14 @@
'use client'; 'use client';
import { SelectorButton } from '@/components/Control'; import { SelectorButton } from '@/components/Control';
import { ItemTypeIcon } from '@/components/DomainIcons'; import { DomIconProps } from '@/components/DomainIcons';
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown'; import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
import { IconOSS, IconRSForm } from '@/components/Icons';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
import { describeLibraryItemType, labelLibraryItemType } from '@/utils/labels';
import { LibraryItemType } from '../models/library'; import { LibraryItemType } from '../backend/types';
import { describeLibraryItemType, labelLibraryItemType } from '../labels';
interface SelectItemTypeProps extends CProps.Styling { interface SelectItemTypeProps extends CProps.Styling {
value: LibraryItemType; value: LibraryItemType;
@ -16,7 +17,7 @@ interface SelectItemTypeProps extends CProps.Styling {
stretchLeft?: boolean; stretchLeft?: boolean;
} }
function SelectItemType({ value, disabled, stretchLeft, onChange, ...restProps }: SelectItemTypeProps) { export function SelectItemType({ value, disabled, stretchLeft, onChange, ...restProps }: SelectItemTypeProps) {
const menu = useDropdown(); const menu = useDropdown();
function handleChange(newValue: LibraryItemType) { function handleChange(newValue: LibraryItemType) {
@ -53,4 +54,12 @@ function SelectItemType({ value, disabled, stretchLeft, onChange, ...restProps }
); );
} }
export default SelectItemType; /** Icon for library item type. */
function ItemTypeIcon({ value, size = '1.25rem', className }: DomIconProps<LibraryItemType>) {
switch (value) {
case LibraryItemType.RSFORM:
return <IconRSForm size={size} className={className ?? 'text-sec-600'} />;
case LibraryItemType.OSS:
return <IconOSS size={size} className={className ?? 'text-ok-600'} />;
}
}

View File

@ -5,7 +5,7 @@ import clsx from 'clsx';
import { SelectSingle } from '@/components/Input'; import { SelectSingle } from '@/components/Input';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import { ILibraryItem, LibraryItemID } from '../models/library'; import { ILibraryItem } from '../backend/types';
import { matchLibraryItem } from '../models/libraryAPI'; import { matchLibraryItem } from '../models/libraryAPI';
interface SelectLibraryItemProps extends CProps.Styling { interface SelectLibraryItemProps extends CProps.Styling {
@ -17,7 +17,7 @@ interface SelectLibraryItemProps extends CProps.Styling {
noBorder?: boolean; noBorder?: boolean;
} }
function SelectLibraryItem({ export function SelectLibraryItem({
className, className,
items, items,
value, value,
@ -31,9 +31,9 @@ function SelectLibraryItem({
label: `${cst.alias}: ${cst.title}` label: `${cst.alias}: ${cst.title}`
})) ?? []; })) ?? [];
function filter(option: { value: LibraryItemID | undefined; label: string }, inputValue: string) { function filter(option: { value: string | undefined; label: string }, query: string) {
const item = items?.find(item => item.id === option.value); const item = items?.find(item => item.id === Number(option.value));
return !item ? false : matchLibraryItem(item, inputValue); return !item ? false : matchLibraryItem(item, query);
} }
return ( return (
@ -42,12 +42,9 @@ function SelectLibraryItem({
options={options} options={options}
value={value ? { value: value.id, label: `${value.alias}: ${value.title}` } : null} value={value ? { value: value.id, label: `${value.alias}: ${value.title}` } : null}
onChange={data => onChange(items?.find(cst => cst.id === data?.value))} onChange={data => onChange(items?.find(cst => cst.id === data?.value))}
// @ts-expect-error: TODO: use type definitions from react-select in filter object
filterOption={filter} filterOption={filter}
placeholder={placeholder} placeholder={placeholder}
{...restProps} {...restProps}
/> />
); );
} }
export default SelectLibraryItem;

View File

@ -1,15 +1,15 @@
'use client'; 'use client';
import clsx from 'clsx';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import clsx from 'clsx';
import { MiniButton } from '@/components/Control'; import { MiniButton } from '@/components/Control';
import { IconFolder, IconFolderClosed, IconFolderEmpty, IconFolderOpened } from '@/components/Icons'; import { IconFolder, IconFolderClosed, IconFolderEmpty, IconFolderOpened } from '@/components/Icons';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import { FolderNode } from '@/features/library/models/FolderTree';
import { useFolders } from '../backend/useFolders'; import { useFolders } from '../backend/useFolders';
import { labelFolderNode } from '../labels'; import { labelFolderNode } from '../labels';
import { FolderNode } from '../models/FolderTree';
interface SelectLocationProps extends CProps.Styling { interface SelectLocationProps extends CProps.Styling {
value: string; value: string;

View File

@ -17,7 +17,7 @@ interface SelectLocationContextProps extends CProps.Styling {
stretchTop?: boolean; stretchTop?: boolean;
} }
function SelectLocationContext({ export function SelectLocationContext({
value, value,
title = 'Проводник...', title = 'Проводник...',
onChange, onChange,
@ -56,5 +56,3 @@ function SelectLocationContext({
</div> </div>
); );
} }
export default SelectLocationContext;

View File

@ -17,7 +17,13 @@ interface SelectLocationHeadProps extends CProps.Styling {
excluded?: LocationHead[]; excluded?: LocationHead[];
} }
function SelectLocationHead({ value, excluded = [], onChange, className, ...restProps }: SelectLocationHeadProps) { export function SelectLocationHead({
value,
excluded = [],
onChange,
className,
...restProps
}: SelectLocationHeadProps) {
const menu = useDropdown(); const menu = useDropdown();
function handleChange(newValue: LocationHead) { function handleChange(newValue: LocationHead) {
@ -60,5 +66,3 @@ function SelectLocationHead({ value, excluded = [], onChange, className, ...rest
</div> </div>
); );
} }
export default SelectLocationHead;

View File

@ -4,21 +4,21 @@ import clsx from 'clsx';
import { SelectSingle } from '@/components/Input'; import { SelectSingle } from '@/components/Input';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import { IVersionInfo, VersionID } from '@/features/library/models/library';
import { labelVersion } from '../labels'; import { labelVersion } from '../../rsform/labels';
import { IVersionInfo } from '../backend/types';
interface SelectVersionProps extends CProps.Styling { interface SelectVersionProps extends CProps.Styling {
id?: string; id?: string;
items?: IVersionInfo[]; items?: IVersionInfo[];
value?: VersionID; value?: number;
onChange: (newValue?: VersionID) => void; onChange: (newValue?: number) => void;
placeholder?: string; placeholder?: string;
noBorder?: boolean; noBorder?: boolean;
} }
function SelectVersion({ id, className, items, value, onChange, ...restProps }: SelectVersionProps) { export function SelectVersion({ id, className, items, value, onChange, ...restProps }: SelectVersionProps) {
const options = [ const options = [
{ {
value: undefined, value: undefined,
@ -46,5 +46,3 @@ function SelectVersion({ id, className, items, value, onChange, ...restProps }:
/> />
); );
} }
export default SelectVersion;

View File

@ -1,18 +1,20 @@
import { BadgeHelp, HelpTopic } from '@/features/help';
import { useRoleStore, UserRole } from '@/features/users';
import { Overlay } from '@/components/Container'; import { Overlay } from '@/components/Container';
import { MiniButton } from '@/components/Control'; import { MiniButton } from '@/components/Control';
import { VisibilityIcon } from '@/components/DomainIcons'; import { VisibilityIcon } from '@/components/DomainIcons';
import { IconImmutable, IconMutable } from '@/components/Icons'; import { IconImmutable, IconMutable } from '@/components/Icons';
import { Label } from '@/components/Input'; import { Label } from '@/components/Input';
import { BadgeHelp } from '@/components/shared/BadgeHelp';
import { HelpTopic } from '@/features/help/models/helpTopic';
import { useMutatingLibrary } from '@/features/library/backend/useMutatingLibrary';
import { useSetAccessPolicy } from '@/features/library/backend/useSetAccessPolicy';
import SelectAccessPolicy from '@/features/library/components/SelectAccessPolicy';
import { AccessPolicy, ILibraryItemEditor } from '@/features/library/models/library';
import { useRoleStore } from '@/features/users';
import { UserRole } from '@/features/users/models/user';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { AccessPolicy } from '../backend/types';
import { useMutatingLibrary } from '../backend/useMutatingLibrary';
import { useSetAccessPolicy } from '../backend/useSetAccessPolicy';
import { ILibraryItemEditor } from './EditorLibraryItem';
import { SelectAccessPolicy } from './SelectAccessPolicy';
interface ToolbarItemAccessProps { interface ToolbarItemAccessProps {
visible: boolean; visible: boolean;
toggleVisible: () => void; toggleVisible: () => void;
@ -21,7 +23,13 @@ interface ToolbarItemAccessProps {
controller: ILibraryItemEditor; controller: ILibraryItemEditor;
} }
function ToolbarItemAccess({ visible, toggleVisible, readOnly, toggleReadOnly, controller }: ToolbarItemAccessProps) { export function ToolbarItemAccess({
visible,
toggleVisible,
readOnly,
toggleReadOnly,
controller
}: ToolbarItemAccessProps) {
const role = useRoleStore(state => state.role); const role = useRoleStore(state => state.role);
const isProcessing = useMutatingLibrary(); const isProcessing = useMutatingLibrary();
const policy = controller.schema.access_policy; const policy = controller.schema.access_policy;
@ -66,5 +74,3 @@ function ToolbarItemAccess({ visible, toggleVisible, readOnly, toggleReadOnly, c
</Overlay> </Overlay>
); );
} }
export default ToolbarItemAccess;

View File

@ -1,19 +1,20 @@
'use client'; 'use client';
import { Controller, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import clsx from 'clsx'; import clsx from 'clsx';
import { Controller, useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { useAuthSuspense } from '@/features/auth';
import { Label, TextArea } from '@/components/Input'; import { Label, TextArea } from '@/components/Input';
import { ModalForm } from '@/components/Modal'; import { ModalForm } from '@/components/Modal';
import { useAuthSuspense } from '@/features/auth/backend/useAuth';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { limits } from '@/utils/constants'; import { limits } from '@/utils/constants';
import { errorMsg } from '@/utils/labels'; import { errorMsg } from '@/utils/labels';
import SelectLocationContext from '../components/SelectLocationContext'; import { SelectLocationContext } from '../components/SelectLocationContext';
import SelectLocationHead from '../components/SelectLocationHead'; import { SelectLocationHead } from '../components/SelectLocationHead';
import { LocationHead } from '../models/library'; import { LocationHead } from '../models/library';
import { combineLocation, validateLocation } from '../models/libraryAPI'; import { combineLocation, validateLocation } from '../models/libraryAPI';

View File

@ -1,30 +1,30 @@
'use client'; 'use client';
import { Controller, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import clsx from 'clsx'; import clsx from 'clsx';
import { Controller, useForm } from 'react-hook-form';
import { urls, useConceptNavigation } from '@/app'; import { urls, useConceptNavigation } from '@/app';
import { useAuthSuspense } from '@/features/auth';
import { MiniButton } from '@/components/Control'; import { MiniButton } from '@/components/Control';
import { VisibilityIcon } from '@/components/DomainIcons'; import { VisibilityIcon } from '@/components/DomainIcons';
import { Checkbox, Label, TextArea, TextInput } from '@/components/Input'; import { Checkbox, Label, TextArea, TextInput } from '@/components/Input';
import { ModalForm } from '@/components/Modal'; import { ModalForm } from '@/components/Modal';
import { useAuthSuspense } from '@/features/auth/backend/useAuth';
import { ICloneLibraryItemDTO, schemaCloneLibraryItem } from '@/features/library/backend/api';
import { useCloneItem } from '@/features/library/backend/useCloneItem';
import SelectAccessPolicy from '@/features/library/components/SelectAccessPolicy';
import SelectLocationContext from '@/features/library/components/SelectLocationContext';
import SelectLocationHead from '@/features/library/components/SelectLocationHead';
import { AccessPolicy, ILibraryItem, LocationHead } from '@/features/library/models/library';
import { cloneTitle, combineLocation } from '@/features/library/models/libraryAPI';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { ConstituentaID } from '../models/rsform'; import { AccessPolicy, ICloneLibraryItemDTO, ILibraryItem, schemaCloneLibraryItem } from '../backend/types';
import { useCloneItem } from '../backend/useCloneItem';
import { SelectAccessPolicy } from '../components/SelectAccessPolicy';
import { SelectLocationContext } from '../components/SelectLocationContext';
import { SelectLocationHead } from '../components/SelectLocationHead';
import { LocationHead } from '../models/library';
import { cloneTitle, combineLocation } from '../models/libraryAPI';
export interface DlgCloneLibraryItemProps { export interface DlgCloneLibraryItemProps {
base: ILibraryItem; base: ILibraryItem;
initialLocation: string; initialLocation: string;
selected: ConstituentaID[]; selected: number[];
totalCount: number; totalCount: number;
} }

View File

@ -1,42 +1,36 @@
'use client'; 'use client';
import { Controller, useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import clsx from 'clsx'; import clsx from 'clsx';
import { Controller, useForm, useWatch } from 'react-hook-form';
import { Checkbox, TextArea, TextInput } from '@/components/Input'; import { Checkbox, TextArea, TextInput } from '@/components/Input';
import { ModalForm } from '@/components/Modal'; import { ModalForm } from '@/components/Modal';
import { IVersionCreateDTO, schemaVersionCreate } from '@/features/library/backend/api';
import { useVersionCreate } from '@/features/library/backend/useVersionCreate';
import { IVersionInfo, LibraryItemID, VersionID } from '@/features/library/models/library';
import { nextVersion } from '@/features/library/models/libraryAPI';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { errorMsg } from '@/utils/labels'; import { errorMsg } from '@/utils/labels';
import { ConstituentaID } from '../models/rsform'; import { IVersionCreateDTO, IVersionInfo, schemaVersionCreate } from '../backend/types';
import { useVersionCreate } from '../backend/useVersionCreate';
import { nextVersion } from '../models/libraryAPI';
export interface DlgCreateVersionProps { export interface DlgCreateVersionProps {
itemID: LibraryItemID; itemID: number;
versions: IVersionInfo[]; versions: IVersionInfo[];
onCreate: (newVersion: VersionID) => void; onCreate: (newVersion: number) => void;
selected: ConstituentaID[]; selected: number[];
totalCount: number; totalCount: number;
} }
function DlgCreateVersion() { function DlgCreateVersion() {
const { const { itemID, versions, selected, totalCount, onCreate } = useDialogsStore(
itemID, // state => state.props as DlgCreateVersionProps
versions, );
selected,
totalCount,
onCreate
} = useDialogsStore(state => state.props as DlgCreateVersionProps);
const { versionCreate } = useVersionCreate(); const { versionCreate } = useVersionCreate();
const { register, handleSubmit, control } = useForm<IVersionCreateDTO>({ const { register, handleSubmit, control } = useForm<IVersionCreateDTO>({
resolver: zodResolver(schemaVersionCreate), resolver: zodResolver(schemaVersionCreate),
defaultValues: { defaultValues: {
version: versions.length > 0 ? nextVersion(versions[0].version) : '1.0.0', version: versions.length > 0 ? nextVersion(versions[versions.length - 1].version) : '1.0.0',
description: '', description: '',
items: undefined items: undefined
} }

View File

@ -1,13 +1,14 @@
'use client'; 'use client';
import clsx from 'clsx';
import { useState } from 'react'; import { useState } from 'react';
import clsx from 'clsx';
import { SelectUser, TableUsers, useUsers } from '@/features/users';
import { MiniButton } from '@/components/Control'; import { MiniButton } from '@/components/Control';
import { IconRemove } from '@/components/Icons'; import { IconRemove } from '@/components/Icons';
import { Label } from '@/components/Input'; import { Label } from '@/components/Input';
import { ModalForm } from '@/components/Modal'; import { ModalForm } from '@/components/Modal';
import { SelectUser, TableUsers, useUsers } from '@/features/users';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { useSetEditors } from '../../backend/useSetEditors'; import { useSetEditors } from '../../backend/useSetEditors';

View File

@ -1,28 +1,29 @@
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler 'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
'use client'; 'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useForm, useWatch } from 'react-hook-form'; import { useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useRSFormSuspense } from '@/features/rsform';
import { MiniButton } from '@/components/Control'; import { MiniButton } from '@/components/Control';
import { IconReset, IconSave } from '@/components/Icons'; import { IconReset, IconSave } from '@/components/Icons';
import { TextArea, TextInput } from '@/components/Input'; import { TextArea, TextInput } from '@/components/Input';
import { ModalView } from '@/components/Modal'; import { ModalView } from '@/components/Modal';
import { IVersionUpdateDTO, schemaVersionUpdate } from '@/features/library/backend/api';
import { useMutatingLibrary } from '@/features/library/backend/useMutatingLibrary';
import { useVersionDelete } from '@/features/library/backend/useVersionDelete';
import { useVersionUpdate } from '@/features/library/backend/useVersionUpdate';
import { LibraryItemID, VersionID } from '@/features/library/models/library';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { errorMsg } from '@/utils/labels'; import { errorMsg } from '@/utils/labels';
import { useRSFormSuspense } from '../../backend/useRSForm'; import { IVersionUpdateDTO, schemaVersionUpdate } from '../../backend/types';
import { useMutatingLibrary } from '../../backend/useMutatingLibrary';
import { useVersionDelete } from '../../backend/useVersionDelete';
import { useVersionUpdate } from '../../backend/useVersionUpdate';
import TableVersions from './TableVersions'; import TableVersions from './TableVersions';
export interface DlgEditVersionsProps { export interface DlgEditVersionsProps {
itemID: LibraryItemID; itemID: number;
afterDelete: (targetVersion: VersionID) => void; afterDelete: (targetVersion: number) => void;
} }
function DlgEditVersions() { function DlgEditVersions() {
@ -42,9 +43,9 @@ function DlgEditVersions() {
} = useForm<IVersionUpdateDTO>({ } = useForm<IVersionUpdateDTO>({
resolver: zodResolver(schemaVersionUpdate), resolver: zodResolver(schemaVersionUpdate),
defaultValues: { defaultValues: {
id: schema.versions[0].id, id: schema.versions[schema.versions.length - 1].id,
version: schema.versions[0].version, version: schema.versions[schema.versions.length - 1].version,
description: schema.versions[0].description description: schema.versions[schema.versions.length - 1].description
}, },
context: { schema: schema } context: { schema: schema }
}); });
@ -56,7 +57,7 @@ function DlgEditVersions() {
[schema, versionID, versionName] [schema, versionID, versionName]
); );
function handleSelectVersion(targetVersion: VersionID) { function handleSelectVersion(targetVersion: number) {
const ver = schema.versions.find(ver => ver.id === targetVersion); const ver = schema.versions.find(ver => ver.id === targetVersion);
if (!ver) { if (!ver) {
return; return;
@ -64,7 +65,7 @@ function DlgEditVersions() {
reset({ ...ver }); reset({ ...ver });
} }
function handleDeleteVersion(targetVersion: VersionID) { function handleDeleteVersion(targetVersion: number) {
const nextVer = schema.versions.find(ver => ver.id !== targetVersion); const nextVer = schema.versions.find(ver => ver.id !== targetVersion);
void versionDelete({ itemID: itemID, versionID: targetVersion }).then(() => { void versionDelete({ itemID: itemID, versionID: targetVersion }).then(() => {
if (!nextVer) { if (!nextVer) {
@ -80,20 +81,20 @@ function DlgEditVersions() {
if (!isDirty || isProcessing || !isValid) { if (!isDirty || isProcessing || !isValid) {
return; return;
} }
void versionUpdate(data).then(() => reset({ ...data })); void versionUpdate({ itemID: itemID, version: data }).then(() => reset({ ...data }));
} }
return ( return (
<ModalView header='Редактирование версий' className='flex flex-col w-[40rem] px-6 gap-3 pb-6'> <ModalView header='Редактирование версий' className='flex flex-col w-[40rem] px-6 gap-3 pb-6'>
<TableVersions <TableVersions
processing={isProcessing} processing={isProcessing}
items={schema.versions} items={schema.versions.reverse()}
onDelete={handleDeleteVersion} onDelete={handleDeleteVersion}
onSelect={handleSelectVersion} onSelect={handleSelectVersion}
selected={versionID} selected={versionID}
/> />
<form className='flex' onSubmit={event => void handleSubmit(onUpdate)(event)}> <form className='flex items-center ' onSubmit={event => void handleSubmit(onUpdate)(event)}>
<TextInput <TextInput
id='dlg_version' id='dlg_version'
{...register('version')} {...register('version')}
@ -102,7 +103,7 @@ function DlgEditVersions() {
className='w-[16rem] mr-3' className='w-[16rem] mr-3'
error={formErrors.version} error={formErrors.version}
/> />
<div className='cc-icons'> <div className='cc-icons h-fit'>
<MiniButton <MiniButton
type='submit' type='submit'
title={isValid ? 'Сохранить изменения' : errorMsg.versionTaken} title={isValid ? 'Сохранить изменения' : errorMsg.versionTaken}

View File

@ -1,20 +1,21 @@
'use client'; 'use client';
import clsx from 'clsx';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import clsx from 'clsx';
import { MiniButton } from '@/components/Control'; import { MiniButton } from '@/components/Control';
import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/DataTable'; import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/DataTable';
import { IconRemove } from '@/components/Icons'; import { IconRemove } from '@/components/Icons';
import { IVersionInfo, VersionID } from '@/features/library/models/library';
import { APP_COLORS } from '@/styling/colors'; import { APP_COLORS } from '@/styling/colors';
import { IVersionInfo } from '../../backend/types';
interface TableVersionsProps { interface TableVersionsProps {
processing: boolean; processing: boolean;
items: IVersionInfo[]; items: IVersionInfo[];
selected?: VersionID; selected?: number;
onDelete: (versionID: VersionID) => void; onDelete: (versionID: number) => void;
onSelect: (versionID: VersionID) => void; onSelect: (versionID: number) => void;
} }
const columnHelper = createColumnHelper<IVersionInfo>(); const columnHelper = createColumnHelper<IVersionInfo>();
@ -22,7 +23,7 @@ const columnHelper = createColumnHelper<IVersionInfo>();
function TableVersions({ processing, items, onDelete, selected, onSelect }: TableVersionsProps) { function TableVersions({ processing, items, onDelete, selected, onSelect }: TableVersionsProps) {
const intl = useIntl(); const intl = useIntl();
function handleDeleteVersion(event: React.MouseEvent, targetVersion: VersionID) { function handleDeleteVersion(event: React.MouseEvent, targetVersion: number) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
onDelete(targetVersion); onDelete(targetVersion);

View File

@ -0,0 +1,16 @@
export { AccessPolicy, type ILibraryItem, type IVersionInfo, LibraryItemType } from './backend/types';
export { useDeleteItem } from './backend/useDeleteItem';
export { useLibrary, useLibrarySuspense } from './backend/useLibrary';
export { useMutatingLibrary } from './backend/useMutatingLibrary';
export { useTemplatesSuspense } from './backend/useTemplates';
export { useUpdateItem } from './backend/useUpdateItem';
export { useUpdateTimestamp } from './backend/useUpdateTimestamp';
export { useVersionRestore } from './backend/useVersionRestore';
export { EditorLibraryItem, type ILibraryItemEditor } from './components/EditorLibraryItem';
export { MiniSelectorOSS } from './components/MiniSelectorOSS';
export { PickSchema } from './components/PickSchema';
export { SelectLibraryItem } from './components/SelectLibraryItem';
export { SelectVersion } from './components/SelectVersion';
export { ToolbarItemAccess } from './components/ToolbarItemAccess';
export { type ILibraryItemReference } from './models/library';
export { useLibrarySearchStore } from './stores/librarySearch';

View File

@ -1,3 +1,4 @@
import { AccessPolicy, LibraryItemType } from './backend/types';
import { FolderNode } from './models/FolderTree'; import { FolderNode } from './models/FolderTree';
import { LocationHead } from './models/library'; import { LocationHead } from './models/library';
import { validateLocation } from './models/libraryAPI'; import { validateLocation } from './models/libraryAPI';
@ -45,3 +46,52 @@ export function labelFolderNode(node: FolderNode): string {
export function describeFolderNode(node: FolderNode): string { export function describeFolderNode(node: FolderNode): string {
return `${node.filesInside} | ${node.filesTotal}`; return `${node.filesInside} | ${node.filesTotal}`;
} }
/**
* Retrieves label for {@link AccessPolicy}.
*/
export function labelAccessPolicy(policy: AccessPolicy): string {
// prettier-ignore
switch (policy) {
case AccessPolicy.PRIVATE: return 'Личный';
case AccessPolicy.PROTECTED: return 'Защищенный';
case AccessPolicy.PUBLIC: return 'Открытый';
}
}
/**
* Retrieves description for {@link AccessPolicy}.
*/
export function describeAccessPolicy(policy: AccessPolicy): string {
// prettier-ignore
switch (policy) {
case AccessPolicy.PRIVATE:
return 'Доступ только для владельца';
case AccessPolicy.PROTECTED:
return 'Доступ для владельца и редакторов';
case AccessPolicy.PUBLIC:
return 'Открытый доступ';
}
}
/**
* Retrieves label for {@link LibraryItemType}.
*/
export function labelLibraryItemType(itemType: LibraryItemType): string {
// prettier-ignore
switch (itemType) {
case LibraryItemType.RSFORM: return 'КС';
case LibraryItemType.OSS: return 'ОСС';
}
}
/**
* Retrieves description for {@link LibraryItemType}.
*/
export function describeLibraryItemType(itemType: LibraryItemType): string {
// prettier-ignore
switch (itemType) {
case LibraryItemType.RSFORM: return 'Концептуальная схема';
case LibraryItemType.OSS: return 'Операционная схема синтеза';
}
}

Some files were not shown because too many files have changed in this diff Show More