Compare commits

..

No commits in common. "ab9f058b0a47ea3d7f7ce88552880c0653c4d85b" and "90be3b5aa77c51b8109b3f25859627e17ae8a0aa" have entirely different histories.

283 changed files with 3267 additions and 3706 deletions

View File

@ -63,7 +63,6 @@ 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,8 +37,7 @@ 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: {
@ -58,33 +57,8 @@ export default [
'react-refresh/only-export-components': ['off', { allowConstantExport: true }], 'react-refresh/only-export-components': ['off', { allowConstantExport: true }],
'simple-import-sort/imports': [ 'simple-import-sort/imports': 'warn',
'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,21 +9,20 @@
"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": "^4.1.0", "@hookform/resolvers": "^3.10.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.21.2", "@tanstack/react-table": "^8.20.6",
"@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.13", "html-to-image": "^1.11.11",
"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",
@ -31,7 +30,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.6", "react-intl": "^7.1.5",
"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",
@ -40,34 +39,33 @@
"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.2", "zod": "^3.24.1",
"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.4", "@types/node": "^22.13.1",
"@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-30d8a17-20250209", "babel-plugin-react-compiler": "^19.0.0-beta-37ed2a7-20241206",
"eslint": "^9.20.1", "eslint": "^9.19.0",
"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-30d8a17-20250209", "eslint-plugin-react-compiler": "^19.0.0-beta-37ed2a7-20241206",
"eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
"globals": "^15.15.0", "globals": "^15.14.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"postcss": "^8.5.2", "postcss": "^8.5.1",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"ts-jest": "^29.2.5", "ts-jest": "^29.2.5",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"typescript-eslint": "^8.24.0", "typescript-eslint": "^8.23.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 { GlobalLoader } from './GlobalLoader'; import ConceptToaster from './GlobalToaster';
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'>
<ToasterThemed <ConceptToaster
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,8 +47,9 @@ 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 }}>
<GlobalLoader /> <Suspense fallback={<Loader />}>
<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/library/dialogs/DlgCloneLibraryItem')); const DlgCloneLibraryItem = React.lazy(() => import('@/features/rsform/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/library/dialogs/DlgCreateVersion')); const DlgCreateVersion = React.lazy(() => import('@/features/rsform/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/library/dialogs/DlgEditVersions')); const DlgEditVersions = React.lazy(() => import('@/features/rsform/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

@ -1,34 +0,0 @@
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,7 +4,9 @@ import { usePreferencesStore } from '@/stores/preferences';
interface ToasterThemedProps extends Omit<ToastContainerProps, 'theme'> {} interface ToasterThemedProps extends Omit<ToastContainerProps, 'theme'> {}
export function ToasterThemed(props: ToasterThemedProps) { 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,9 +1,8 @@
'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,7 +7,6 @@ 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,6 +1,5 @@
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,5 +1,3 @@
import { useAuthSuspense, useLogout } from '@/features/auth';
import { Dropdown, DropdownButton } from '@/components/Dropdown'; import { Dropdown, DropdownButton } from '@/components/Dropdown';
import { import {
IconAdmin, IconAdmin,
@ -16,10 +14,11 @@ 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,7 +4,6 @@ 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,5 +1,6 @@
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';
@ -11,8 +12,6 @@ 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';
@ -23,7 +22,7 @@ export const Router = createBrowserRouter([
element: <ApplicationLayout />, element: <ApplicationLayout />,
errorElement: <ErrorFallback />, errorElement: <ErrorFallback />,
loader: prefetchAuth, loader: prefetchAuth,
hydrateFallbackElement: fallbackLoader(), hydrateFallbackElement: <Loader />,
children: [ children: [
{ {
path: '', path: '',
@ -99,11 +98,3 @@ 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,12 +1,10 @@
/** /**
* Module: generic API for backend REST communications using axios library. * Module: generic API for backend REST communications using axios library.
*/ */
import { toast } from 'react-toastify';
import axios, { AxiosError, AxiosRequestConfig } from 'axios'; import axios, { AxiosError, AxiosRequestConfig } from 'axios';
import { z, ZodError } from 'zod'; import { toast } from 'react-toastify';
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';
@ -43,32 +41,23 @@ 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, schema }: IAxiosGetRequest) { export function axiosGet<ResponseData>({ endpoint, options }: IAxiosGetRequest) {
return axiosInstance return axiosInstance
.get<ResponseData>(endpoint, options) .get<ResponseData>(endpoint, options)
.then(response => { .then(response => response.data)
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') {
if (error instanceof ZodError) { // Note: Ignore cancellation errors
toast.error(errorMsg.invalidResponse);
} else {
toast.error(extractErrorMessage(error)); toast.error(extractErrorMessage(error));
}
console.error(error); console.error(error);
} }
throw error; throw error;
@ -78,13 +67,11 @@ export function axiosGet<ResponseData>({ endpoint, options, schema }: IAxiosGetR
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);
@ -94,12 +81,8 @@ export function axiosPost<RequestData, ResponseData = void>({
} }
return response.data; return response.data;
}) })
.catch((error: Error | AxiosError | ZodError) => { .catch((error: Error | AxiosError) => {
if (error instanceof ZodError) {
toast.error(errorMsg.invalidResponse);
} else {
toast.error(extractErrorMessage(error)); toast.error(extractErrorMessage(error));
}
console.error(error); console.error(error);
throw error; throw error;
}); });
@ -108,13 +91,11 @@ 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);
@ -124,12 +105,8 @@ export function axiosDelete<RequestData, ResponseData = void>({
} }
return response.data; return response.data;
}) })
.catch((error: Error | AxiosError | ZodError) => { .catch((error: Error | AxiosError) => {
if (error instanceof ZodError) {
toast.error(errorMsg.invalidResponse);
} else {
toast.error(extractErrorMessage(error)); toast.error(extractErrorMessage(error));
}
console.error(error); console.error(error);
throw error; throw error;
}); });
@ -138,13 +115,11 @@ 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);
@ -154,12 +129,8 @@ export function axiosPatch<RequestData, ResponseData = void>({
} }
return response.data; return response.data;
}) })
.catch((error: Error | AxiosError | ZodError) => { .catch((error: Error | AxiosError) => {
if (error instanceof ZodError) {
toast.error(errorMsg.invalidResponse);
} else {
toast.error(extractErrorMessage(error)); toast.error(extractErrorMessage(error));
}
console.error(error); console.error(error);
throw error; throw error;
}); });

View File

@ -7,18 +7,3 @@ 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,7 +1,6 @@
'use client'; 'use client';
'use no memo'; 'use no memo';
import { useMemo, useState } from 'react';
import { import {
ColumnSort, ColumnSort,
createColumnHelper, createColumnHelper,
@ -16,9 +15,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,9 +4,8 @@ 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 SelectRow from './SelectRow';
import { IConditionalStyle } from '.'; import { IConditionalStyle } from '.';
import SelectRow from './SelectRow';
interface TableBodyProps<TData> { interface TableBodyProps<TData> {
table: Table<TData>; table: Table<TData>;

View File

@ -1,10 +1,18 @@
import { LocationHead } from '@/features/library/models/library'; import { AccessPolicy, LibraryItemType, LocationHead } from '@/features/library/models/library';
import { ExpressionStatus } from '@/features/rsform/models/rsform'; import { CstType, 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,
@ -14,8 +22,12 @@ import {
IconHide, IconHide,
IconMoveDown, IconMoveDown,
IconMoveUp, IconMoveUp,
IconOSS,
IconPrivate,
IconProps, IconProps,
IconProtected,
IconPublic, IconPublic,
IconRSForm,
IconSettings, IconSettings,
IconShow, IconShow,
IconStatusError, IconStatusError,
@ -33,6 +45,28 @@ 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) {
@ -115,6 +149,28 @@ 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,12 +1,11 @@
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 | ZodError | undefined | null; export type ErrorData = string | Error | AxiosError | undefined | null;
interface InfoErrorProps { interface InfoErrorProps {
error: ErrorData; error: ErrorData;
@ -17,13 +16,6 @@ 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,7 +4,6 @@ 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 { FieldError, GlobalError } from 'react-hook-form';
import clsx from 'clsx'; import clsx from 'clsx';
import { FieldError, GlobalError } from 'react-hook-form';
import { CProps } from '../props'; import { CProps } from '../props';

View File

@ -1,12 +1,11 @@
'use client'; 'use client';
import { useRef, useState } from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import { useRef, useState } from 'react';
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

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

View File

@ -2,7 +2,6 @@ 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,7 +2,6 @@ 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,7 +3,6 @@ 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,8 +2,7 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { BadgeHelp, HelpTopic } from '@/features/help'; import { HelpTopic } from '@/features/help/models/helpTopic';
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';
@ -12,7 +11,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 {
@ -44,9 +43,6 @@ 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;
} }
/** /**
@ -64,19 +60,13 @@ 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()) {
@ -88,7 +78,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={handleCancel} /> <ModalBackdrop onHide={hideDialog} />
<form <form
className={clsx( className={clsx(
'cc-animate-modal', 'cc-animate-modal',
@ -108,7 +98,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={handleCancel} onClick={hideDialog}
/> />
{header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null} {header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null}
@ -135,7 +125,7 @@ export function ModalForm({
className='min-w-[7rem]' className='min-w-[7rem]'
disabled={!canSubmit} disabled={!canSubmit}
/> />
<Button text='Отмена' className='min-w-[7rem]' onClick={handleCancel} /> <Button text='Отмена' className='min-w-[7rem]' onClick={hideDialog} />
</div> </div>
</form> </form>
</div> </div>

View File

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

View File

@ -2,8 +2,6 @@
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';
@ -11,7 +9,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

@ -5,10 +5,9 @@ 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

@ -2,10 +2,9 @@ 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,17 +1,77 @@
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 { infoMsg } from '@/utils/labels'; import { errorMsg, infoMsg } from '@/utils/labels';
import { /**
IChangePasswordDTO, * Represents CurrentUser information.
ICurrentUser, */
IPasswordTokenDTO, export interface ICurrentUser {
IRequestPasswordDTO, id: number | null;
IResetPasswordDTO, username: string;
IUserLoginDTO is_staff: boolean;
} from './types'; 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;
}
/** /**
* Authentication API. * Authentication API.

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,10 @@
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';
export function ExpectedAnonymous() { function ExpectedAnonymous() {
const { user } = useAuthSuspense(); const { user } = useAuthSuspense();
const { logout } = useLogout(); const { logout } = useLogout();
const router = useConceptNavigation(); const router = useConceptNavigation();
@ -31,3 +30,5 @@ export 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';
export function RequireAuth({ children }: React.PropsWithChildren) { function RequireAuth({ children }: React.PropsWithChildren) {
const { isAnonymous } = useAuthSuspense(); const { isAnonymous } = useAuthSuspense();
if (isAnonymous) { if (isAnonymous) {
@ -19,3 +19,5 @@ export function RequireAuth({ children }: React.PropsWithChildren) {
} }
return <>{children}</>; return <>{children}</>;
} }
export default RequireAuth;

View File

@ -1,5 +0,0 @@
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,11 +1,10 @@
'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';
@ -13,10 +12,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/types'; import { IUserLoginDTO, schemaUserLogin } from '../backend/api';
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();
@ -27,6 +26,7 @@ function LoginPage() {
register, register,
handleSubmit, handleSubmit,
clearErrors, clearErrors,
resetField,
formState: { errors } formState: { errors }
} = useForm({ } = useForm({
resolver: zodResolver(schemaUserLogin), resolver: zodResolver(schemaUserLogin),
@ -38,6 +38,7 @@ 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,10 +1,9 @@
'use client'; 'use client';
import { useEffect, useState } from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useState } from 'react';
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 { useState } from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import { useState } from 'react';
import { isAxiosError } from '@/backend/apiTransport'; import { isAxiosError } from '@/backend/apiTransport';
import { SubmitButton, TextURL } from '@/components/Control'; import { SubmitButton, TextURL } from '@/components/Control';

View File

@ -3,7 +3,6 @@ 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,7 +3,6 @@ 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,5 +1,4 @@
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,7 +1,6 @@
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

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

View File

@ -1,5 +1,4 @@
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,13 +1,11 @@
'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,8 +9,10 @@ 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, labelHelpTopic } from '../../labels'; import { describeHelpTopic } from '../../labels';
import { HelpTopic, topicParent } from '../../models/helpTopic'; import { labelHelpTopic } from '../../labels';
import { topicParent } from '../../models/helpTopic';
import { HelpTopic } from '../../models/helpTopic';
interface TopicsDropdownProps { interface TopicsDropdownProps {
activeTopic: HelpTopic; activeTopic: HelpTopic;

View File

@ -3,7 +3,6 @@
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,11 +1,10 @@
'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 { useLayoutEffect } from 'react'; import { useEffect } 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');
useLayoutEffect(() => { useEffect(() => {
hideFooter(true); hideFooter(true);
return () => hideFooter(false); return () => hideFooter(false);
}, [hideFooter]); }, [hideFooter]);

View File

@ -1,17 +1,27 @@
import { useEffect } from 'react';
import { urls, useConceptNavigation } from '@/app'; import { urls, useConceptNavigation } from '@/app';
import { useAuthSuspense } from '@/features/auth'; import { Loader } from '@/components/Loader';
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) {
setTimeout(() => {
router.replace(urls.manuals); router.replace(urls.manuals);
}, PARAMETER.refreshTimeout);
} else { } else {
setTimeout(() => {
router.replace(urls.library); router.replace(urls.library);
}, PARAMETER.refreshTimeout);
} }
}, [router, isAnonymous]);
return null; return <Loader />;
} }
export default HomePage; export default HomePage;

View File

@ -1,42 +1,145 @@
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, KEYS } from '@/backend/configuration'; import { DELAYS } from '@/backend/configuration';
import { infoMsg } from '@/utils/labels'; import { ossApi } from '@/features/oss/backend/api';
import { IRSFormDTO, rsformsApi } from '@/features/rsform/backend/api';
import { errorMsg, infoMsg } from '@/utils/labels';
import { import { AccessPolicy, ILibraryItem, IVersionInfo, LibraryItemID, LibraryItemType, VersionID } from '../models/library';
AccessPolicy, import { validateLocation } from '../models/libraryAPI';
ICloneLibraryItemDTO,
ICreateLibraryItemDTO, /**
ILibraryItem, * Represents update data for renaming Location.
IRenameLocationDTO, */
IUpdateLibraryItemDTO, export interface IRenameLocationDTO {
IVersionCreateDTO, target: string;
IVersionInfo, new_location: string;
IVersionUpdateDTO, }
schemaLibraryItem,
schemaLibraryItemArray, /**
schemaVersionInfo * Represents data, used for cloning {@link IRSForm}.
} 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: KEYS.library, baseKey: 'library',
libraryListKey: KEYS.composite.libraryList, libraryListKey: ['library', 'list'],
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 }
}) })
@ -47,7 +150,6 @@ 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 }
}) })
@ -55,7 +157,6 @@ 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,
@ -71,14 +172,13 @@ 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: number; owner: number }) => setOwner: ({ itemID, owner }: { itemID: LibraryItemID; owner: number }) =>
axiosPatch({ axiosPatch({
endpoint: `/api/library/${itemID}/set-owner`, endpoint: `/api/library/${itemID}/set-owner`,
request: { request: {
@ -86,7 +186,7 @@ export const libraryApi = {
successMessage: infoMsg.changesSaved successMessage: infoMsg.changesSaved
} }
}), }),
setLocation: ({ itemID, location }: { itemID: number; location: string }) => setLocation: ({ itemID, location }: { itemID: LibraryItemID; location: string }) =>
axiosPatch({ axiosPatch({
endpoint: `/api/library/${itemID}/set-location`, endpoint: `/api/library/${itemID}/set-location`,
request: { request: {
@ -94,7 +194,7 @@ export const libraryApi = {
successMessage: infoMsg.moveComplete successMessage: infoMsg.moveComplete
} }
}), }),
setAccessPolicy: ({ itemID, policy }: { itemID: number; policy: AccessPolicy }) => setAccessPolicy: ({ itemID, policy }: { itemID: LibraryItemID; policy: AccessPolicy }) =>
axiosPatch({ axiosPatch({
endpoint: `/api/library/${itemID}/set-access-policy`, endpoint: `/api/library/${itemID}/set-access-policy`,
request: { request: {
@ -102,7 +202,7 @@ export const libraryApi = {
successMessage: infoMsg.changesSaved successMessage: infoMsg.changesSaved
} }
}), }),
setEditors: ({ itemID, editors }: { itemID: number; editors: number[] }) => setEditors: ({ itemID, editors }: { itemID: LibraryItemID; editors: number[] }) =>
axiosPatch({ axiosPatch({
endpoint: `/api/library/${itemID}/set-editors`, endpoint: `/api/library/${itemID}/set-editors`,
request: { request: {
@ -111,7 +211,7 @@ export const libraryApi = {
} }
}), }),
deleteItem: (target: number) => deleteItem: (target: LibraryItemID) =>
axiosDelete({ axiosDelete({
endpoint: `/api/library/${target}`, endpoint: `/api/library/${target}`,
request: { request: {
@ -120,7 +220,6 @@ 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,
@ -136,33 +235,30 @@ export const libraryApi = {
} }
}), }),
versionCreate: ({ itemID, data }: { itemID: number; data: IVersionCreateDTO }) => versionCreate: ({ itemID, data }: { itemID: LibraryItemID; 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: number }) => versionRestore: ({ versionID }: { versionID: VersionID }) =>
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: { itemID: number; version: IVersionUpdateDTO }) => versionUpdate: (data: IVersionUpdateDTO) =>
axiosPatch<IVersionUpdateDTO, IVersionInfo>({ axiosPatch<IVersionUpdateDTO, IVersionInfo>({
schema: schemaVersionInfo, endpoint: `/api/versions/${data.id}`,
endpoint: `/api/versions/${data.version.id}`,
request: { request: {
data: data.version, data: data,
successMessage: infoMsg.changesSaved successMessage: infoMsg.changesSaved
} }
}), }),
versionDelete: (data: { itemID: number; versionID: number }) => versionDelete: (data: { itemID: LibraryItemID; versionID: VersionID }) =>
axiosDelete({ axiosDelete({
endpoint: `/api/versions/${data.versionID}`, endpoint: `/api/versions/${data.versionID}`,
request: { request: {

View File

@ -1,142 +0,0 @@
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,8 +1,7 @@
import { useAuthSuspense } from '@/features/auth'; import { useAuthSuspense } from '@/features/auth/backend/useAuth';
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,7 +1,6 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { libraryApi } from './api'; import { ICloneLibraryItemDTO, libraryApi } from './api';
import { ICloneLibraryItemDTO } from './types';
export const useCloneItem = () => { export const useCloneItem = () => {
const client = useQueryClient(); const client = useQueryClient();

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { FolderTree } from '../models/FolderTree'; import { FolderTree } from '@/features/library/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,8 +1,7 @@
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,10 +1,13 @@
import { useIsMutating } from '@tanstack/react-query'; import { useIsMutating } from '@tanstack/react-query';
import { KEYS } from '@/backend/configuration'; import { ossApi } from '@/features/oss/backend/api';
import { rsformsApi } from '@/features/rsform/backend/api';
import { libraryApi } from './api';
export const useMutatingLibrary = () => { export const useMutatingLibrary = () => {
const countMutations = useIsMutating({ mutationKey: [KEYS.library] }); const countMutations = useIsMutating({ mutationKey: [libraryApi.baseKey] });
const countOss = useIsMutating({ mutationKey: [KEYS.oss] }); const countOss = useIsMutating({ mutationKey: [ossApi.baseKey] });
const countRSForm = useIsMutating({ mutationKey: [KEYS.rsform] }); const countRSForm = useIsMutating({ mutationKey: [rsformsApi.baseKey] });
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 { KEYS } from '@/backend/configuration'; import { ossApi } from '@/features/oss/backend/api';
import { rsformsApi } from '@/features/rsform/backend/api';
import { libraryApi } from './api'; import { IRenameLocationDTO, 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: [KEYS.library] }), client.invalidateQueries({ queryKey: [libraryApi.baseKey] }),
client.invalidateQueries({ queryKey: [KEYS.rsform] }), client.invalidateQueries({ queryKey: [rsformsApi.baseKey] }),
client.invalidateQueries({ queryKey: [KEYS.oss] }) client.invalidateQueries({ queryKey: [ossApi.baseKey] })
]) ])
}); });
return { return {

View File

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

View File

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

View File

@ -1,12 +1,10 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { IOperationSchemaDTO } from '@/features/oss/backend/types'; import { IOperationSchemaDTO, ossApi } from '@/features/oss/backend/api';
import { IRSFormDTO } from '@/features/rsform/backend/types'; import { rsformsApi } from '@/features/rsform/backend/api';
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();
@ -14,7 +12,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 = KEYS.composite.ossItem({ itemID: variables.itemID }); const ossKey = ossApi.getOssQueryOptions({ itemID: variables.itemID }).queryKey;
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 });
@ -25,17 +23,15 @@ export const useSetLocation = () => {
if (!item.result) { if (!item.result) {
return; return;
} }
const itemKey = KEYS.composite.rsItem({ itemID: item.result }); const itemKey = rsformsApi.getRSFormQueryOptions({ itemID: item.result }).queryKey;
return client.invalidateQueries({ queryKey: itemKey }); return client.invalidateQueries({ queryKey: itemKey });
}) })
.filter(item => !!item) .filter(item => !!item)
]); ]);
} }
const rsKey = KEYS.composite.rsItem({ itemID: variables.itemID }); const rsKey = rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey;
client.setQueryData(rsKey, (prev: IRSFormDTO | undefined) => client.setQueryData(rsKey, prev => (!prev ? undefined : { ...prev, location: variables.location }));
!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))
); );
@ -43,6 +39,6 @@ export const useSetLocation = () => {
}); });
return { return {
setLocation: (data: { itemID: number; location: string }) => mutation.mutateAsync(data) setLocation: (data: { itemID: LibraryItemID; location: string }) => mutation.mutateAsync(data)
}; };
}; };

View File

@ -1,12 +1,10 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { IOperationSchemaDTO } from '@/features/oss/backend/types'; import { IOperationSchemaDTO, ossApi } from '@/features/oss/backend/api';
import { IRSFormDTO } from '@/features/rsform/backend/types'; import { rsformsApi } from '@/features/rsform/backend/api';
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();
@ -14,7 +12,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 = KEYS.composite.ossItem({ itemID: variables.itemID }); const ossKey = ossApi.getOssQueryOptions({ itemID: variables.itemID }).queryKey;
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 });
@ -25,17 +23,15 @@ export const useSetOwner = () => {
if (!item.result) { if (!item.result) {
return; return;
} }
const itemKey = KEYS.composite.rsItem({ itemID: item.result }); const itemKey = rsformsApi.getRSFormQueryOptions({ itemID: item.result }).queryKey;
return client.invalidateQueries({ queryKey: itemKey }); return client.invalidateQueries({ queryKey: itemKey });
}) })
.filter(item => !!item) .filter(item => !!item)
]); ]);
} }
const rsKey = KEYS.composite.rsItem({ itemID: variables.itemID }); const rsKey = rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey;
client.setQueryData(rsKey, (prev: IRSFormDTO | undefined) => client.setQueryData(rsKey, prev => (!prev ? undefined : { ...prev, owner: variables.owner }));
!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,12 +1,10 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { IOperationSchemaDTO } from '@/features/oss/backend/types'; import { IOperationSchemaDTO, ossApi } from '@/features/oss/backend/api';
import { IRSFormDTO } from '@/features/rsform/backend/types'; import { IRSFormDTO } from '@/features/rsform/backend/api';
import { KEYS } from '@/backend/configuration'; import { ILibraryItem, LibraryItemType } from '../models/library';
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();
@ -14,10 +12,7 @@ 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 = const itemKey = libraryApi.getItemQueryOptions({ itemID: data.id, itemType: data.item_type }).queryKey;
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))
); );
@ -28,7 +23,9 @@ 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 => client.invalidateQueries({ queryKey: KEYS.composite.ossItem({ itemID: item.id }) })) schema.oss.map(item =>
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: number) => updateTimestamp: (target: LibraryItemID) =>
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 { KEYS } from '@/backend/configuration'; import { rsformsApi } from '@/features/rsform/backend/api';
import { libraryApi } from './api'; import { LibraryItemID } from '../models/library';
import { IVersionCreateDTO } from './types'; import { IVersionCreateDTO, libraryApi } from './api';
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(KEYS.composite.rsItem({ itemID: data.schema.id }), data.schema); client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.schema.id }).queryKey, data.schema);
updateTimestamp(data.schema.id); updateTimestamp(data.schema.id);
} }
}); });
return { return {
versionCreate: (data: { itemID: number; data: IVersionCreateDTO }) => versionCreate: (data: { itemID: LibraryItemID; data: IVersionCreateDTO }) =>
mutation.mutateAsync(data).then(response => response.version) mutation.mutateAsync(data).then(response => response.version)
}; };
}; };

View File

@ -1,9 +1,8 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { IRSFormDTO } from '@/features/rsform/backend/types'; import { IRSFormDTO, rsformsApi } from '@/features/rsform/backend/api';
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 = () => {
@ -12,7 +11,9 @@ 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(KEYS.composite.rsItem({ itemID: variables.itemID }), (prev: IRSFormDTO | undefined) => client.setQueryData(
rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey,
(prev: IRSFormDTO | undefined) =>
!prev !prev
? undefined ? undefined
: { : {
@ -23,6 +24,6 @@ export const useVersionDelete = () => {
} }
}); });
return { return {
versionDelete: (data: { itemID: number; versionID: number }) => mutation.mutateAsync(data) versionDelete: (data: { itemID: LibraryItemID; versionID: VersionID }) => mutation.mutateAsync(data)
}; };
}; };

View File

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

View File

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

View File

@ -2,8 +2,6 @@ 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';
@ -18,28 +16,19 @@ 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

@ -1,14 +1,13 @@
'use client'; 'use client';
import { MiniButton } from '@/components/Control'; import { MiniButton } from '@/components/Control';
import { DomIconProps } from '@/components/DomainIcons'; import { PolicyIcon } 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 '../backend/types'; import { AccessPolicy } from '../models/library';
import { describeAccessPolicy, labelAccessPolicy } from '../labels';
interface SelectAccessPolicyProps extends CProps.Styling { interface SelectAccessPolicyProps extends CProps.Styling {
value: AccessPolicy; value: AccessPolicy;
@ -18,7 +17,7 @@ interface SelectAccessPolicyProps extends CProps.Styling {
stretchLeft?: boolean; stretchLeft?: boolean;
} }
export function SelectAccessPolicy({ value, disabled, stretchLeft, onChange, ...restProps }: SelectAccessPolicyProps) { function SelectAccessPolicy({ value, disabled, stretchLeft, onChange, ...restProps }: SelectAccessPolicyProps) {
const menu = useDropdown(); const menu = useDropdown();
function handleChange(newValue: AccessPolicy) { function handleChange(newValue: AccessPolicy) {
@ -53,14 +52,4 @@ export function SelectAccessPolicy({ value, disabled, stretchLeft, onChange, ...
); );
} }
/** Icon for access policy. */ export default SelectAccessPolicy;
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,14 +1,13 @@
'use client'; 'use client';
import { SelectorButton } from '@/components/Control'; import { SelectorButton } from '@/components/Control';
import { DomIconProps } from '@/components/DomainIcons'; import { ItemTypeIcon } 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 '../backend/types'; import { LibraryItemType } from '../models/library';
import { describeLibraryItemType, labelLibraryItemType } from '../labels';
interface SelectItemTypeProps extends CProps.Styling { interface SelectItemTypeProps extends CProps.Styling {
value: LibraryItemType; value: LibraryItemType;
@ -17,7 +16,7 @@ interface SelectItemTypeProps extends CProps.Styling {
stretchLeft?: boolean; stretchLeft?: boolean;
} }
export function SelectItemType({ value, disabled, stretchLeft, onChange, ...restProps }: SelectItemTypeProps) { function SelectItemType({ value, disabled, stretchLeft, onChange, ...restProps }: SelectItemTypeProps) {
const menu = useDropdown(); const menu = useDropdown();
function handleChange(newValue: LibraryItemType) { function handleChange(newValue: LibraryItemType) {
@ -54,12 +53,4 @@ export function SelectItemType({ value, disabled, stretchLeft, onChange, ...rest
); );
} }
/** Icon for library item type. */ export default SelectItemType;
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 } from '../backend/types'; import { ILibraryItem, LibraryItemID } from '../models/library';
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;
} }
export function SelectLibraryItem({ function SelectLibraryItem({
className, className,
items, items,
value, value,
@ -31,9 +31,9 @@ export function SelectLibraryItem({
label: `${cst.alias}: ${cst.title}` label: `${cst.alias}: ${cst.title}`
})) ?? []; })) ?? [];
function filter(option: { value: string | undefined; label: string }, query: string) { function filter(option: { value: LibraryItemID | undefined; label: string }, inputValue: string) {
const item = items?.find(item => item.id === Number(option.value)); const item = items?.find(item => item.id === option.value);
return !item ? false : matchLibraryItem(item, query); return !item ? false : matchLibraryItem(item, inputValue);
} }
return ( return (
@ -42,9 +42,12 @@ export 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 { useEffect, useState } from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useState } from 'react';
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;
} }
export function SelectLocationContext({ function SelectLocationContext({
value, value,
title = 'Проводник...', title = 'Проводник...',
onChange, onChange,
@ -56,3 +56,5 @@ export function SelectLocationContext({
</div> </div>
); );
} }
export default SelectLocationContext;

View File

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

View File

@ -1,20 +1,19 @@
'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,14 +1,13 @@
'use client'; 'use client';
import { useState } from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import { useState } from 'react';
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,16 +0,0 @@
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,4 +1,3 @@
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';
@ -46,52 +45,3 @@ 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 'Операционная схема синтеза';
}
}

View File

@ -2,7 +2,22 @@
* Module: Models for LibraryItem. * Module: Models for LibraryItem.
*/ */
import { LibraryItemType } from '../backend/types'; /**
* 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 valid location headers. * Represents valid location headers.
@ -16,12 +31,74 @@ export enum LocationHead {
export const BASIC_SCHEMAS = '/L/Базовые'; export const BASIC_SCHEMAS = '/L/Базовые';
/**
* Represents {@link LibraryItem} identifier type.
*/
export type LibraryItemID = number;
/**
* Represents {@link Version} identifier type.
*/
export type VersionID = number;
/**
* Represents library item version information.
*/
export interface IVersionInfo {
id: VersionID;
item: LibraryItemID;
version: string;
description: string;
time_create: string;
}
/**
* Represents library item common data typical for all item types.
*/
export interface ILibraryItem {
id: LibraryItemID;
item_type: LibraryItemType;
title: string;
alias: string;
comment: string;
visible: boolean;
read_only: boolean;
location: string;
access_policy: AccessPolicy;
time_create: string;
time_update: string;
owner: number | null;
}
/**
* Represents {@link ILibraryItem} constant data loaded for both OSS and RSForm.
*/
export interface ILibraryItemData extends ILibraryItem {
editors: number[];
}
/** /**
* Represents {@link ILibraryItem} minimal reference data. * Represents {@link ILibraryItem} minimal reference data.
*/ */
export interface ILibraryItemReference { export interface ILibraryItemReference extends Pick<ILibraryItem, 'id' | 'alias'> {}
id: number;
alias: string; /**
* Represents {@link ILibraryItem} extended data with versions.
*/
export interface ILibraryItemVersioned extends ILibraryItemData {
version?: VersionID;
versions: IVersionInfo[];
}
/**
* Represents common {@link ILibraryItem} editor controller.
*/
export interface ILibraryItemEditor {
schema: ILibraryItemData;
deleteSchema: () => void;
isMutable: boolean;
isAttachedToOSS: boolean;
} }
/** /**

View File

@ -1,6 +1,4 @@
import { AccessPolicy, ILibraryItem, LibraryItemType } from '../backend/types'; import { AccessPolicy, ILibraryItem, LibraryItemType, LocationHead } from './library';
import { LocationHead } from './library';
import { matchLibraryItem, validateLocation } from './libraryAPI'; import { matchLibraryItem, validateLocation } from './libraryAPI';
describe('Testing matching LibraryItem', () => { describe('Testing matching LibraryItem', () => {

View File

@ -5,7 +5,7 @@
import { limits } from '@/utils/constants'; import { limits } from '@/utils/constants';
import { TextMatcher } from '@/utils/utils'; import { TextMatcher } from '@/utils/utils';
import { ILibraryItem } from '../backend/types'; import { ILibraryItem } from './library';
const LOCATION_REGEXP = /^\/[PLUS]((\/[!\d\p{L}]([!\d\p{L}\- ]*[!\d\p{L}])?)*)?$/u; // cspell:disable-line const LOCATION_REGEXP = /^\/[PLUS]((\/[!\d\p{L}]([!\d\p{L}\- ]*[!\d\p{L}])?)*)?$/u; // cspell:disable-line

View File

@ -1,4 +1,4 @@
import { RequireAuth } from '@/features/auth'; import RequireAuth from '@/features/auth/components/RequireAuth';
import FormCreateItem from './FormCreateItem'; import FormCreateItem from './FormCreateItem';

View File

@ -1,28 +1,27 @@
'use client'; 'use client';
import { useRef } from 'react';
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 { useRef } from 'react';
import { Controller, useForm, useWatch } from 'react-hook-form';
import { urls, useConceptNavigation } from '@/app'; import { urls, useConceptNavigation } from '@/app';
import { useAuthSuspense } from '@/features/auth';
import { Overlay } from '@/components/Container'; import { Overlay } from '@/components/Container';
import { Button, MiniButton, SubmitButton } from '@/components/Control'; import { Button, MiniButton, SubmitButton } from '@/components/Control';
import { VisibilityIcon } from '@/components/DomainIcons'; import { VisibilityIcon } from '@/components/DomainIcons';
import { IconDownload } from '@/components/Icons'; import { IconDownload } from '@/components/Icons';
import { InfoError } from '@/components/InfoError'; import { InfoError } from '@/components/InfoError';
import { Label, TextArea, TextInput } from '@/components/Input'; import { Label, TextArea, TextInput } from '@/components/Input';
import { useAuthSuspense } from '@/features/auth/backend/useAuth';
import { EXTEOR_TRS_FILE } from '@/utils/constants'; import { EXTEOR_TRS_FILE } from '@/utils/constants';
import { AccessPolicy, ICreateLibraryItemDTO, LibraryItemType, schemaCreateLibraryItem } from '../../backend/types'; import { ICreateLibraryItemDTO, schemaCreateLibraryItem } from '../../backend/api';
import { useCreateItem } from '../../backend/useCreateItem'; import { useCreateItem } from '../../backend/useCreateItem';
import { SelectAccessPolicy } from '../../components/SelectAccessPolicy'; import SelectAccessPolicy from '../../components/SelectAccessPolicy';
import { SelectItemType } from '../../components/SelectItemType'; import SelectItemType from '../../components/SelectItemType';
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 { AccessPolicy, LibraryItemType, LocationHead } from '../../models/library';
import { combineLocation } from '../../models/libraryAPI'; import { combineLocation } from '../../models/libraryAPI';
import { useLibrarySearchStore } from '../../stores/librarySearch'; import { useLibrarySearchStore } from '../../stores/librarySearch';

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import { toast } from 'react-toastify';
import fileDownload from 'js-file-download'; import fileDownload from 'js-file-download';
import { toast } from 'react-toastify';
import { Overlay } from '@/components/Container'; import { Overlay } from '@/components/Container';
import { MiniButton } from '@/components/Control'; import { MiniButton } from '@/components/Control';
@ -15,7 +15,6 @@ import { useApplyLibraryFilter } from '../../backend/useApplyLibraryFilter';
import { useLibrarySuspense } from '../../backend/useLibrary'; import { useLibrarySuspense } from '../../backend/useLibrary';
import { useRenameLocation } from '../../backend/useRenameLocation'; import { useRenameLocation } from '../../backend/useRenameLocation';
import { useCreateLibraryFilter, useLibrarySearchStore } from '../../stores/librarySearch'; import { useCreateLibraryFilter, useLibrarySearchStore } from '../../stores/librarySearch';
import TableLibraryItems from './TableLibraryItems'; import TableLibraryItems from './TableLibraryItems';
import ToolbarSearch from './ToolbarSearch'; import ToolbarSearch from './ToolbarSearch';
import ViewSideLocation from './ViewSideLocation'; import ViewSideLocation from './ViewSideLocation';

View File

@ -1,24 +1,23 @@
'use client'; 'use client';
import clsx from 'clsx';
import { useLayoutEffect, useState } from 'react'; import { useLayoutEffect, useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import clsx from 'clsx';
import { urls, useConceptNavigation } from '@/app'; import { urls, useConceptNavigation } from '@/app';
import { useLabelUser } from '@/features/users';
import { FlexColumn } from '@/components/Container'; import { FlexColumn } from '@/components/Container';
import { MiniButton, TextURL } from '@/components/Control'; import { MiniButton, TextURL } from '@/components/Control';
import DataTable, { createColumnHelper, IConditionalStyle, VisibilityState } from '@/components/DataTable'; import DataTable, { createColumnHelper, IConditionalStyle, VisibilityState } from '@/components/DataTable';
import { IconFolderTree } from '@/components/Icons'; import { IconFolderTree } from '@/components/Icons';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import { useLabelUser } from '@/features/users';
import useWindowSize from '@/hooks/useWindowSize'; import useWindowSize from '@/hooks/useWindowSize';
import { useFitHeight } from '@/stores/appLayout'; import { useFitHeight } from '@/stores/appLayout';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import { APP_COLORS } from '@/styling/colors'; import { APP_COLORS } from '@/styling/colors';
import { ILibraryItem, LibraryItemType } from '../../backend/types';
import BadgeLocation from '../../components/BadgeLocation'; import BadgeLocation from '../../components/BadgeLocation';
import { ILibraryItem, LibraryItemType } from '../../models/library';
import { useLibrarySearchStore } from '../../stores/librarySearch'; import { useLibrarySearchStore } from '../../stores/librarySearch';
interface TableLibraryItemsProps { interface TableLibraryItemsProps {

View File

@ -2,8 +2,6 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { SelectUser } from '@/features/users';
import { MiniButton, SelectorButton } from '@/components/Control'; import { MiniButton, SelectorButton } from '@/components/Control';
import { LocationIcon, VisibilityIcon } from '@/components/DomainIcons'; import { LocationIcon, VisibilityIcon } from '@/components/DomainIcons';
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown'; import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
@ -16,8 +14,9 @@ import {
IconOwner, IconOwner,
IconUserSearch IconUserSearch
} from '@/components/Icons'; } 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 { SelectUser } from '@/features/users';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
import { tripleToggleColor } from '@/utils/utils'; import { tripleToggleColor } from '@/utils/utils';

View File

@ -1,13 +1,14 @@
import { toast } from 'react-toastify';
import clsx from 'clsx'; import clsx from 'clsx';
import { toast } from 'react-toastify';
import { useAuthSuspense } from '@/features/auth';
import { BadgeHelp, HelpTopic } from '@/features/help';
import { MiniButton } from '@/components/Control'; import { MiniButton } from '@/components/Control';
import { SubfoldersIcon } from '@/components/DomainIcons'; import { SubfoldersIcon } from '@/components/DomainIcons';
import { IconFolderEdit, IconFolderTree } from '@/components/Icons'; import { IconFolderEdit, IconFolderTree } from '@/components/Icons';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import { BadgeHelp } from '@/components/shared/BadgeHelp';
import { useAuthSuspense } from '@/features/auth/backend/useAuth';
import { HelpTopic } from '@/features/help/models/helpTopic';
import { FolderNode } from '@/features/library/models/FolderTree';
import useWindowSize from '@/hooks/useWindowSize'; import useWindowSize from '@/hooks/useWindowSize';
import { useFitHeight } from '@/stores/appLayout'; import { useFitHeight } from '@/stores/appLayout';
import { PARAMETER, prefixes } from '@/utils/constants'; import { PARAMETER, prefixes } from '@/utils/constants';
@ -15,7 +16,6 @@ import { infoMsg } from '@/utils/labels';
import { useLibrary } from '../../backend/useLibrary'; import { useLibrary } from '../../backend/useLibrary';
import SelectLocation from '../../components/SelectLocation'; import SelectLocation from '../../components/SelectLocation';
import { FolderNode } from '../../models/FolderTree';
import { useLibrarySearchStore } from '../../stores/librarySearch'; import { useLibrarySearchStore } from '../../stores/librarySearch';
interface ViewSideLocationProps { interface ViewSideLocationProps {

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