Compare commits
18 Commits
90be3b5aa7
...
ab9f058b0a
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ab9f058b0a | ||
![]() |
a34af217eb | ||
![]() |
aae57d51f1 | ||
![]() |
75c0a6f848 | ||
![]() |
483fc93b58 | ||
![]() |
1b62c8fbc2 | ||
![]() |
1f73941866 | ||
![]() |
c58a8ec969 | ||
![]() |
ef463897c4 | ||
![]() |
4fe0936b05 | ||
![]() |
e5c595e99e | ||
![]() |
4b450384c4 | ||
![]() |
cdcf1a9c43 | ||
![]() |
d5854366a9 | ||
![]() |
3c92e07d0f | ||
![]() |
6bb02c6462 | ||
![]() |
53a795d3ec | ||
![]() |
1ab4ce2556 |
|
@ -63,6 +63,7 @@ This readme file is used mostly to document project dependencies and conventions
|
|||
- tailwindcss
|
||||
- postcss
|
||||
- autoprefixer
|
||||
- eslint-plugin-import
|
||||
- eslint-plugin-react-compiler
|
||||
- eslint-plugin-simple-import-sort
|
||||
- eslint-plugin-react-hooks
|
||||
|
|
|
@ -4,7 +4,7 @@ import typescriptParser from '@typescript-eslint/parser';
|
|||
import reactPlugin from 'eslint-plugin-react';
|
||||
import reactCompilerPlugin from 'eslint-plugin-react-compiler';
|
||||
import reactHooksPlugin from 'eslint-plugin-react-hooks';
|
||||
|
||||
import importPlugin from 'eslint-plugin-import';
|
||||
import simpleImportSort from 'eslint-plugin-simple-import-sort';
|
||||
|
||||
export default [
|
||||
|
@ -37,7 +37,8 @@ export default [
|
|||
'react': reactPlugin,
|
||||
'react-compiler': reactCompilerPlugin,
|
||||
'react-hooks': reactHooksPlugin,
|
||||
'simple-import-sort': simpleImportSort
|
||||
'simple-import-sort': simpleImportSort,
|
||||
'import': importPlugin
|
||||
},
|
||||
settings: { react: { version: 'detect' } },
|
||||
rules: {
|
||||
|
@ -57,8 +58,33 @@ export default [
|
|||
|
||||
'react-refresh/only-export-components': ['off', { allowConstantExport: true }],
|
||||
|
||||
'simple-import-sort/imports': 'warn',
|
||||
'simple-import-sort/imports': [
|
||||
'warn',
|
||||
{
|
||||
groups: [
|
||||
// Node.js builtins.
|
||||
[
|
||||
'^(assert|buffer|child_process|cluster|console|constants|crypto|dgram|dns|domain|events|fs|http|https|module|net|os|path|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|tty|url|util|vm|zlib|freelist|v8|process|async_hooks|http2|perf_hooks)(/.*|$)'
|
||||
],
|
||||
// Packages. `react` related packages come first.
|
||||
['^react', '^@?\\w'],
|
||||
// Global app and features
|
||||
['^(@/app|@/features)(/.*|$)'],
|
||||
// Internal packages.
|
||||
['^(@)(/.*|$)'],
|
||||
// Side effect imports.
|
||||
['^\\u0000'],
|
||||
// Parent imports. Put `..` last.
|
||||
['^\\.\\.(?!/?$)', '^\\.\\./?$'],
|
||||
// Other relative imports. Put same-folder imports and `.` last.
|
||||
['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'],
|
||||
// Style imports.
|
||||
['^.+\\.s?css$']
|
||||
]
|
||||
}
|
||||
],
|
||||
'simple-import-sort/exports': 'error',
|
||||
'import/no-duplicates': 'warn',
|
||||
|
||||
...reactHooksPlugin.configs.recommended.rules
|
||||
}
|
||||
|
|
957
rsconcept/frontend/package-lock.json
generated
957
rsconcept/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -9,20 +9,21 @@
|
|||
"dev": "vite --host",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
|
||||
"lintFix": "eslint . --report-unused-disable-directives --max-warnings 0 --fix",
|
||||
"preview": "vite preview --port 3000"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dagrejs/dagre": "^1.1.4",
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
"@hookform/resolvers": "^4.1.0",
|
||||
"@lezer/lr": "^1.4.2",
|
||||
"@tanstack/react-query": "^5.66.0",
|
||||
"@tanstack/react-query-devtools": "^5.66.0",
|
||||
"@tanstack/react-table": "^8.20.6",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"@uiw/codemirror-themes": "^4.23.8",
|
||||
"@uiw/react-codemirror": "^4.23.8",
|
||||
"axios": "^1.7.9",
|
||||
"clsx": "^2.1.1",
|
||||
"html-to-image": "^1.11.11",
|
||||
"html-to-image": "^1.11.13",
|
||||
"js-file-download": "^0.4.12",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.0.0",
|
||||
|
@ -30,7 +31,7 @@
|
|||
"react-error-boundary": "^5.0.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-intl": "^7.1.5",
|
||||
"react-intl": "^7.1.6",
|
||||
"react-router": "^7.1.5",
|
||||
"react-select": "^5.10.0",
|
||||
"react-tabs": "^6.1.0",
|
||||
|
@ -39,33 +40,34 @@
|
|||
"react-zoom-pan-pinch": "^3.7.0",
|
||||
"reactflow": "^11.11.4",
|
||||
"use-debounce": "^10.0.4",
|
||||
"zod": "^3.24.1",
|
||||
"zod": "^3.24.2",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lezer/generator": "^1.7.2",
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/node": "^22.13.4",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
||||
"@typescript-eslint/parser": "^8.0.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"babel-plugin-react-compiler": "^19.0.0-beta-37ed2a7-20241206",
|
||||
"eslint": "^9.19.0",
|
||||
"babel-plugin-react-compiler": "^19.0.0-beta-30d8a17-20250209",
|
||||
"eslint": "^9.20.1",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-react-compiler": "^19.0.0-beta-37ed2a7-20241206",
|
||||
"eslint-plugin-react-compiler": "^19.0.0-beta-30d8a17-20250209",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"globals": "^15.14.0",
|
||||
"globals": "^15.15.0",
|
||||
"jest": "^29.7.0",
|
||||
"postcss": "^8.5.1",
|
||||
"postcss": "^8.5.2",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"ts-jest": "^29.2.5",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.23.0",
|
||||
"typescript-eslint": "^8.24.0",
|
||||
"vite": "^6.1.0"
|
||||
},
|
||||
"overrides": {
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import { Suspense } from 'react';
|
||||
import { Outlet } from 'react-router';
|
||||
|
||||
import { Loader } from '@/components/Loader';
|
||||
import { ModalLoader } from '@/components/Modal';
|
||||
import { useAppLayoutStore, useMainHeight, useViewportHeight } from '@/stores/appLayout';
|
||||
import { globals } from '@/utils/constants';
|
||||
|
||||
import { NavigationState } from './Navigation/NavigationContext';
|
||||
import { Footer } from './Footer';
|
||||
import { GlobalDialogs } from './GlobalDialogs';
|
||||
import ConceptToaster from './GlobalToaster';
|
||||
import { GlobalLoader } from './GlobalLoader';
|
||||
import { ToasterThemed } from './GlobalToaster';
|
||||
import { GlobalTooltips } from './GlobalTooltips';
|
||||
import { Navigation } from './Navigation';
|
||||
import { NavigationState } from './Navigation/NavigationContext';
|
||||
|
||||
function ApplicationLayout() {
|
||||
const mainHeight = useMainHeight();
|
||||
|
@ -24,7 +24,7 @@ function ApplicationLayout() {
|
|||
return (
|
||||
<NavigationState>
|
||||
<div className='min-w-[20rem] antialiased h-full max-w-[120rem] mx-auto'>
|
||||
<ConceptToaster
|
||||
<ToasterThemed
|
||||
className='text-[14px] cc-animate-position'
|
||||
style={{ marginTop: noNavigationAnimation ? '1.5rem' : '3.5rem' }}
|
||||
autoClose={3000}
|
||||
|
@ -47,9 +47,8 @@ function ApplicationLayout() {
|
|||
}}
|
||||
>
|
||||
<main className='cc-scroll-y' style={{ overflowY: showScroll ? 'scroll' : 'auto', minHeight: mainHeight }}>
|
||||
<Suspense fallback={<Loader />}>
|
||||
<GlobalLoader />
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</main>
|
||||
{!noNavigation && !noFooter ? <Footer /> : null}
|
||||
</div>
|
||||
|
|
|
@ -6,17 +6,17 @@ import { DialogType, useDialogsStore } from '@/stores/dialogs';
|
|||
|
||||
const DlgChangeInputSchema = React.lazy(() => import('@/features/oss/dialogs/DlgChangeInputSchema'));
|
||||
const DlgChangeLocation = React.lazy(() => import('@/features/library/dialogs/DlgChangeLocation'));
|
||||
const DlgCloneLibraryItem = React.lazy(() => import('@/features/rsform/dialogs/DlgCloneLibraryItem'));
|
||||
const DlgCloneLibraryItem = React.lazy(() => import('@/features/library/dialogs/DlgCloneLibraryItem'));
|
||||
const DlgCreateCst = React.lazy(() => import('@/features/rsform/dialogs/DlgCreateCst'));
|
||||
const DlgCreateOperation = React.lazy(() => import('@/features/oss/dialogs/DlgCreateOperation'));
|
||||
const DlgCreateVersion = React.lazy(() => import('@/features/rsform/dialogs/DlgCreateVersion'));
|
||||
const DlgCreateVersion = React.lazy(() => import('@/features/library/dialogs/DlgCreateVersion'));
|
||||
const DlgCstTemplate = React.lazy(() => import('@/features/rsform/dialogs/DlgCstTemplate'));
|
||||
const DlgDeleteCst = React.lazy(() => import('@/features/rsform/dialogs/DlgDeleteCst'));
|
||||
const DlgDeleteOperation = React.lazy(() => import('@/features/oss/dialogs/DlgDeleteOperation'));
|
||||
const DlgEditEditors = React.lazy(() => import('@/features/library/dialogs/DlgEditEditors'));
|
||||
const DlgEditOperation = React.lazy(() => import('@/features/oss/dialogs/DlgEditOperation'));
|
||||
const DlgEditReference = React.lazy(() => import('@/features/rsform/dialogs/DlgEditReference'));
|
||||
const DlgEditVersions = React.lazy(() => import('@/features/rsform/dialogs/DlgEditVersions'));
|
||||
const DlgEditVersions = React.lazy(() => import('@/features/library/dialogs/DlgEditVersions'));
|
||||
const DlgEditWordForms = React.lazy(() => import('@/features/rsform/dialogs/DlgEditWordForms'));
|
||||
const DlgGraphParams = React.lazy(() => import('@/features/rsform/dialogs/DlgGraphParams'));
|
||||
const DlgInlineSynthesis = React.lazy(() => import('@/features/rsform/dialogs/DlgInlineSynthesis'));
|
||||
|
|
34
rsconcept/frontend/src/app/GlobalLoader.tsx
Normal file
34
rsconcept/frontend/src/app/GlobalLoader.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { useNavigation } from 'react-router';
|
||||
import clsx from 'clsx';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
|
||||
import { Loader } from '@/components/Loader';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
||||
// TODO: add animation
|
||||
export function GlobalLoader() {
|
||||
const navigation = useNavigation();
|
||||
|
||||
const isLoading = navigation.state === 'loading';
|
||||
const [loadingDebounced] = useDebounce(isLoading, PARAMETER.navigationPopupDelay);
|
||||
|
||||
if (!loadingDebounced) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='fixed top-0 left-0 w-full h-full z-modal cursor-default'>
|
||||
<div className={clsx('z-navigation', 'fixed top-0 left-0', 'w-full h-full', 'backdrop-blur-[3px] opacity-50')} />
|
||||
<div className={clsx('z-navigation', 'fixed top-0 left-0', 'w-full h-full', 'bg-prim-0 opacity-25')} />
|
||||
<div
|
||||
className={clsx(
|
||||
'px-10 mb-10',
|
||||
'z-modal absolute bottom-1/2 left-1/2 -translate-x-1/2 translate-y-1/2',
|
||||
'border rounded-xl bg-prim-100'
|
||||
)}
|
||||
>
|
||||
<Loader scale={6} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
'use client';
|
||||
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import { queryClient } from '@/backend/queryClient';
|
||||
|
||||
|
|
|
@ -4,9 +4,7 @@ import { usePreferencesStore } from '@/stores/preferences';
|
|||
|
||||
interface ToasterThemedProps extends Omit<ToastContainerProps, 'theme'> {}
|
||||
|
||||
function ToasterThemed(props: ToasterThemedProps) {
|
||||
export function ToasterThemed(props: ToasterThemedProps) {
|
||||
const darkMode = usePreferencesStore(state => state.darkMode);
|
||||
return <ToastContainer theme={darkMode ? 'dark' : 'light'} {...props} />;
|
||||
}
|
||||
|
||||
export default ToasterThemed;
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
'use client';
|
||||
|
||||
import InfoConstituenta from '@/features/rsform/components/InfoConstituenta';
|
||||
|
||||
import { Tooltip } from '@/components/Container';
|
||||
import { Loader } from '@/components/Loader';
|
||||
import InfoConstituenta from '@/features/rsform/components/InfoConstituenta';
|
||||
import { useTooltipsStore } from '@/stores/tooltips';
|
||||
import { globals } from '@/utils/constants';
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import { useAppLayoutStore } from '@/stores/appLayout';
|
|||
import { PARAMETER } from '@/utils/constants';
|
||||
|
||||
import { urls } from '../urls';
|
||||
|
||||
import Logo from './Logo';
|
||||
import NavigationButton from './NavigationButton';
|
||||
import { useConceptNavigation } from './NavigationContext';
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { useAuthSuspense } from '@/features/auth';
|
||||
|
||||
import { IconLogin, IconUser2 } from '@/components/Icons';
|
||||
import { useAuthSuspense } from '@/features/auth/backend/useAuth';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
|
||||
import NavigationButton from './NavigationButton';
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { useAuthSuspense, useLogout } from '@/features/auth';
|
||||
|
||||
import { Dropdown, DropdownButton } from '@/components/Dropdown';
|
||||
import {
|
||||
IconAdmin,
|
||||
|
@ -14,11 +16,10 @@ import {
|
|||
IconUser
|
||||
} from '@/components/Icons';
|
||||
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 { urls } from '../urls';
|
||||
|
||||
import { useConceptNavigation } from './NavigationContext';
|
||||
|
||||
interface UserDropdownProps {
|
||||
|
|
|
@ -4,6 +4,7 @@ import { useDropdown } from '@/components/Dropdown';
|
|||
import { Loader } from '@/components/Loader';
|
||||
|
||||
import { urls } from '../urls';
|
||||
|
||||
import { useConceptNavigation } from './NavigationContext';
|
||||
import UserButton from './UserButton';
|
||||
import UserDropdown from './UserDropdown';
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { createBrowserRouter } from 'react-router';
|
||||
|
||||
import { Loader } from '@/components/Loader';
|
||||
import { prefetchAuth } from '@/features/auth/backend/useAuth';
|
||||
import LoginPage from '@/features/auth/pages/LoginPage';
|
||||
import HomePage from '@/features/home/HomePage';
|
||||
|
@ -12,6 +11,8 @@ import { prefetchRSForm } from '@/features/rsform/backend/useRSForm';
|
|||
import { prefetchProfile } from '@/features/users/backend/useProfile';
|
||||
import { prefetchUsers } from '@/features/users/backend/useUsers';
|
||||
|
||||
import { Loader } from '@/components/Loader';
|
||||
|
||||
import ApplicationLayout from './ApplicationLayout';
|
||||
import { ErrorFallback } from './ErrorFallback';
|
||||
import { routes } from './urls';
|
||||
|
@ -22,7 +23,7 @@ export const Router = createBrowserRouter([
|
|||
element: <ApplicationLayout />,
|
||||
errorElement: <ErrorFallback />,
|
||||
loader: prefetchAuth,
|
||||
hydrateFallbackElement: <Loader />,
|
||||
hydrateFallbackElement: fallbackLoader(),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
|
@ -98,3 +99,11 @@ function parseRSFormURL(id: string | undefined, url: string) {
|
|||
function parseOssURL(id: string | undefined) {
|
||||
return { itemID: id ? Number(id) : undefined };
|
||||
}
|
||||
|
||||
function fallbackLoader() {
|
||||
return (
|
||||
<div className='flex justify-center items-center h-[100dvh]'>
|
||||
<Loader scale={6} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
/**
|
||||
* Module: generic API for backend REST communications using axios library.
|
||||
*/
|
||||
import axios, { AxiosError, AxiosRequestConfig } from 'axios';
|
||||
import { toast } from 'react-toastify';
|
||||
import axios, { AxiosError, AxiosRequestConfig } from 'axios';
|
||||
import { z, ZodError } from 'zod';
|
||||
|
||||
import { buildConstants } from '@/utils/buildConstants';
|
||||
import { errorMsg } from '@/utils/labels';
|
||||
import { extractErrorMessage } from '@/utils/utils';
|
||||
|
||||
export { AxiosError } from 'axios';
|
||||
|
@ -41,23 +43,32 @@ export interface IAxiosRequest<RequestData, ResponseData> {
|
|||
endpoint: string;
|
||||
request?: IFrontRequest<RequestData, ResponseData>;
|
||||
options?: AxiosRequestConfig;
|
||||
schema?: z.ZodType;
|
||||
}
|
||||
|
||||
export interface IAxiosGetRequest {
|
||||
endpoint: string;
|
||||
options?: AxiosRequestConfig;
|
||||
signal?: AbortSignal;
|
||||
schema?: z.ZodType;
|
||||
}
|
||||
|
||||
// ================ Transport API calls ================
|
||||
export function axiosGet<ResponseData>({ endpoint, options }: IAxiosGetRequest) {
|
||||
export function axiosGet<ResponseData>({ endpoint, options, schema }: IAxiosGetRequest) {
|
||||
return axiosInstance
|
||||
.get<ResponseData>(endpoint, options)
|
||||
.then(response => response.data)
|
||||
.then(response => {
|
||||
schema?.parse(response.data);
|
||||
return response.data;
|
||||
})
|
||||
.catch((error: Error | AxiosError) => {
|
||||
if (error.name !== 'CanceledError') {
|
||||
// Note: Ignore cancellation errors
|
||||
if (error.name !== 'CanceledError') {
|
||||
if (error instanceof ZodError) {
|
||||
toast.error(errorMsg.invalidResponse);
|
||||
} else {
|
||||
toast.error(extractErrorMessage(error));
|
||||
}
|
||||
console.error(error);
|
||||
}
|
||||
throw error;
|
||||
|
@ -67,11 +78,13 @@ export function axiosGet<ResponseData>({ endpoint, options }: IAxiosGetRequest)
|
|||
export function axiosPost<RequestData, ResponseData = void>({
|
||||
endpoint,
|
||||
request,
|
||||
options
|
||||
options,
|
||||
schema
|
||||
}: IAxiosRequest<RequestData, ResponseData>) {
|
||||
return axiosInstance
|
||||
.post<ResponseData>(endpoint, request?.data, options)
|
||||
.then(response => {
|
||||
schema?.parse(response.data);
|
||||
if (request?.successMessage) {
|
||||
if (typeof request.successMessage === 'string') {
|
||||
toast.success(request.successMessage);
|
||||
|
@ -81,8 +94,12 @@ export function axiosPost<RequestData, ResponseData = void>({
|
|||
}
|
||||
return response.data;
|
||||
})
|
||||
.catch((error: Error | AxiosError) => {
|
||||
.catch((error: Error | AxiosError | ZodError) => {
|
||||
if (error instanceof ZodError) {
|
||||
toast.error(errorMsg.invalidResponse);
|
||||
} else {
|
||||
toast.error(extractErrorMessage(error));
|
||||
}
|
||||
console.error(error);
|
||||
throw error;
|
||||
});
|
||||
|
@ -91,11 +108,13 @@ export function axiosPost<RequestData, ResponseData = void>({
|
|||
export function axiosDelete<RequestData, ResponseData = void>({
|
||||
endpoint,
|
||||
request,
|
||||
options
|
||||
options,
|
||||
schema
|
||||
}: IAxiosRequest<RequestData, ResponseData>) {
|
||||
return axiosInstance
|
||||
.delete<ResponseData>(endpoint, options)
|
||||
.then(response => {
|
||||
schema?.parse(response.data);
|
||||
if (request?.successMessage) {
|
||||
if (typeof request.successMessage === 'string') {
|
||||
toast.success(request.successMessage);
|
||||
|
@ -105,8 +124,12 @@ export function axiosDelete<RequestData, ResponseData = void>({
|
|||
}
|
||||
return response.data;
|
||||
})
|
||||
.catch((error: Error | AxiosError) => {
|
||||
.catch((error: Error | AxiosError | ZodError) => {
|
||||
if (error instanceof ZodError) {
|
||||
toast.error(errorMsg.invalidResponse);
|
||||
} else {
|
||||
toast.error(extractErrorMessage(error));
|
||||
}
|
||||
console.error(error);
|
||||
throw error;
|
||||
});
|
||||
|
@ -115,11 +138,13 @@ export function axiosDelete<RequestData, ResponseData = void>({
|
|||
export function axiosPatch<RequestData, ResponseData = void>({
|
||||
endpoint,
|
||||
request,
|
||||
options
|
||||
options,
|
||||
schema
|
||||
}: IAxiosRequest<RequestData, ResponseData>) {
|
||||
return axiosInstance
|
||||
.patch<ResponseData>(endpoint, request?.data, options)
|
||||
.then(response => {
|
||||
schema?.parse(response.data);
|
||||
if (request?.successMessage) {
|
||||
if (typeof request.successMessage === 'string') {
|
||||
toast.success(request.successMessage);
|
||||
|
@ -129,8 +154,12 @@ export function axiosPatch<RequestData, ResponseData = void>({
|
|||
}
|
||||
return response.data;
|
||||
})
|
||||
.catch((error: Error | AxiosError) => {
|
||||
.catch((error: Error | AxiosError | ZodError) => {
|
||||
if (error instanceof ZodError) {
|
||||
toast.error(errorMsg.invalidResponse);
|
||||
} else {
|
||||
toast.error(extractErrorMessage(error));
|
||||
}
|
||||
console.error(error);
|
||||
throw error;
|
||||
});
|
||||
|
|
|
@ -7,3 +7,18 @@ export const DELAYS = {
|
|||
staleMedium: 1 * 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 ?? '']
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { ReactNode } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { ITooltip, Tooltip as TooltipImpl } from 'react-tooltip';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
'use client';
|
||||
'use no memo';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
ColumnSort,
|
||||
createColumnHelper,
|
||||
|
@ -15,9 +16,9 @@ import {
|
|||
useReactTable,
|
||||
type VisibilityState
|
||||
} from '@tanstack/react-table';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { CProps } from '../props';
|
||||
|
||||
import DefaultNoData from './DefaultNoData';
|
||||
import PaginationTools from './PaginationTools';
|
||||
import TableBody from './TableBody';
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
'use client';
|
||||
'use no memo';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { Table } from '@tanstack/react-table';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { prefixes } from '@/utils/constants';
|
||||
|
||||
|
|
|
@ -4,8 +4,9 @@ import { Cell, flexRender, Row, Table } from '@tanstack/react-table';
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { CProps } from '../props';
|
||||
import { IConditionalStyle } from '.';
|
||||
|
||||
import SelectRow from './SelectRow';
|
||||
import { IConditionalStyle } from '.';
|
||||
|
||||
interface TableBodyProps<TData> {
|
||||
table: Table<TData>;
|
||||
|
|
|
@ -1,18 +1,10 @@
|
|||
import { AccessPolicy, LibraryItemType, LocationHead } from '@/features/library/models/library';
|
||||
import { CstType, ExpressionStatus } from '@/features/rsform/models/rsform';
|
||||
import { LocationHead } from '@/features/library/models/library';
|
||||
import { ExpressionStatus } from '@/features/rsform/models/rsform';
|
||||
import { CstMatchMode, DependencyMode } from '@/features/rsform/stores/cstSearch';
|
||||
|
||||
import {
|
||||
IconAlias,
|
||||
IconBusiness,
|
||||
IconCstAxiom,
|
||||
IconCstBaseSet,
|
||||
IconCstConstSet,
|
||||
IconCstFunction,
|
||||
IconCstPredicate,
|
||||
IconCstStructured,
|
||||
IconCstTerm,
|
||||
IconCstTheorem,
|
||||
IconFilter,
|
||||
IconFormula,
|
||||
IconGraphCollapse,
|
||||
|
@ -22,12 +14,8 @@ import {
|
|||
IconHide,
|
||||
IconMoveDown,
|
||||
IconMoveUp,
|
||||
IconOSS,
|
||||
IconPrivate,
|
||||
IconProps,
|
||||
IconProtected,
|
||||
IconPublic,
|
||||
IconRSForm,
|
||||
IconSettings,
|
||||
IconShow,
|
||||
IconStatusError,
|
||||
|
@ -45,28 +33,6 @@ export interface DomIconProps<RequestData> extends IconProps {
|
|||
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. */
|
||||
export function VisibilityIcon({ value, size = '1.25rem', className }: DomIconProps<boolean>) {
|
||||
if (value) {
|
||||
|
@ -149,28 +115,6 @@ export function StatusIcon({ value, size = '1.25rem', className }: DomIconProps<
|
|||
}
|
||||
}
|
||||
|
||||
/** Icon for constituenta type. */
|
||||
export function CstTypeIcon({ value, size = '1.25rem', className }: DomIconProps<CstType>) {
|
||||
switch (value) {
|
||||
case CstType.BASE:
|
||||
return <IconCstBaseSet size={size} className={className ?? 'text-ok-600'} />;
|
||||
case CstType.CONSTANT:
|
||||
return <IconCstConstSet size={size} className={className ?? 'text-ok-600'} />;
|
||||
case CstType.STRUCTURED:
|
||||
return <IconCstStructured size={size} className={className ?? 'text-ok-600'} />;
|
||||
case CstType.TERM:
|
||||
return <IconCstTerm size={size} className={className ?? 'text-sec-600'} />;
|
||||
case CstType.AXIOM:
|
||||
return <IconCstAxiom size={size} className={className ?? 'text-warn-600'} />;
|
||||
case CstType.FUNCTION:
|
||||
return <IconCstFunction size={size} className={className ?? 'text-sec-600'} />;
|
||||
case CstType.PREDICATE:
|
||||
return <IconCstPredicate size={size} className={className ?? 'text-warn-600'} />;
|
||||
case CstType.THEOREM:
|
||||
return <IconCstTheorem size={size} className={className ?? 'text-warn-600'} />;
|
||||
}
|
||||
}
|
||||
|
||||
/** Icon for relocation direction. */
|
||||
export function RelocateUpIcon({ value, size = '1.25rem', className }: DomIconProps<boolean>) {
|
||||
if (value) {
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import clsx from 'clsx';
|
||||
import { ZodError } from 'zod';
|
||||
|
||||
import { AxiosError, isAxiosError } from '@/backend/apiTransport';
|
||||
import { isResponseHtml } from '@/utils/utils';
|
||||
|
||||
import { PrettyJson } from './View';
|
||||
|
||||
export type ErrorData = string | Error | AxiosError | undefined | null;
|
||||
export type ErrorData = string | Error | AxiosError | ZodError | undefined | null;
|
||||
|
||||
interface InfoErrorProps {
|
||||
error: ErrorData;
|
||||
|
@ -16,6 +17,13 @@ function DescribeError({ error }: { error: ErrorData }) {
|
|||
return <p>Ошибки отсутствуют</p>;
|
||||
} else if (typeof error === 'string') {
|
||||
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)) {
|
||||
return (
|
||||
<div className='mt-6'>
|
||||
|
|
|
@ -4,6 +4,7 @@ import { globals } from '@/utils/constants';
|
|||
|
||||
import { CheckboxChecked, CheckboxNull } from '../Icons';
|
||||
import { CProps } from '../props';
|
||||
|
||||
import { CheckboxProps } from './Checkbox';
|
||||
|
||||
export interface CheckboxTristateProps extends Omit<CheckboxProps, 'value' | 'onChange'> {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import clsx from 'clsx';
|
||||
import { FieldError, GlobalError } from 'react-hook-form';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { CProps } from '../props';
|
||||
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { useRef, useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { Button } from '../Control';
|
||||
import { IconUpload } from '../Icons';
|
||||
import { CProps } from '../props';
|
||||
|
||||
import { Label } from './Label';
|
||||
|
||||
interface FileInputProps extends Omit<CProps.Input, 'accept' | 'type'> {
|
||||
|
|
|
@ -2,9 +2,10 @@ import clsx from 'clsx';
|
|||
|
||||
import { Overlay } from '@/components/Container';
|
||||
import { IconSearch } from '@/components/Icons';
|
||||
import { TextInput } from '@/components/Input';
|
||||
import { CProps } from '@/components/props';
|
||||
|
||||
import { TextInput } from './TextInput';
|
||||
|
||||
interface SearchBarProps extends CProps.Styling {
|
||||
/** Id of the search bar. */
|
||||
id?: string;
|
|
@ -1,5 +1,5 @@
|
|||
import clsx from 'clsx';
|
||||
import { useEffect, useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { globals, PARAMETER } from '@/utils/constants';
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import clsx from 'clsx';
|
|||
|
||||
import { Label } from '../Input/Label';
|
||||
import { CProps } from '../props';
|
||||
|
||||
import { ErrorField } from './ErrorField';
|
||||
|
||||
export interface TextAreaProps extends CProps.Editor, CProps.ErrorProcessing, CProps.Colors, CProps.TextArea {
|
||||
|
|
|
@ -2,6 +2,7 @@ import clsx from 'clsx';
|
|||
|
||||
import { Label } from '../Input/Label';
|
||||
import { CProps } from '../props';
|
||||
|
||||
import { ErrorField } from './ErrorField';
|
||||
|
||||
interface TextInputProps extends CProps.Editor, CProps.ErrorProcessing, CProps.Colors, CProps.Input {
|
||||
|
|
|
@ -3,6 +3,7 @@ export { CheckboxTristate } from './CheckboxTristate';
|
|||
export { ErrorField } from './ErrorField';
|
||||
export { FileInput } from './FileInput';
|
||||
export { Label } from './Label';
|
||||
export { SearchBar } from './SearchBar';
|
||||
export { SelectMulti, type SelectMultiProps } from './SelectMulti';
|
||||
export { SelectSingle, type SelectSingleProps } from './SelectSingle';
|
||||
export { SelectTree } from './SelectTree';
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { HelpTopic } from '@/features/help/models/helpTopic';
|
||||
import { BadgeHelp, HelpTopic } from '@/features/help';
|
||||
|
||||
import useEscapeKey from '@/hooks/useEscapeKey';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
@ -11,7 +12,7 @@ import { prepareTooltip } from '@/utils/utils';
|
|||
import { Button, MiniButton, SubmitButton } from '../Control';
|
||||
import { IconClose } from '../Icons';
|
||||
import { CProps } from '../props';
|
||||
import { BadgeHelp } from '../shared/BadgeHelp';
|
||||
|
||||
import { ModalBackdrop } from './ModalBackdrop';
|
||||
|
||||
export interface ModalProps extends CProps.Styling {
|
||||
|
@ -43,6 +44,9 @@ interface ModalFormProps extends ModalProps {
|
|||
|
||||
/** Callback to be called after submit. */
|
||||
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
|
||||
|
||||
/** Callback to be called when modal is canceled. */
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -60,13 +64,19 @@ export function ModalForm({
|
|||
submitInvalidTooltip,
|
||||
beforeSubmit,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
|
||||
helpTopic,
|
||||
hideHelpWhen,
|
||||
...restProps
|
||||
}: React.PropsWithChildren<ModalFormProps>) {
|
||||
const hideDialog = useDialogsStore(state => state.hideDialog);
|
||||
useEscapeKey(hideDialog);
|
||||
|
||||
function handleCancel() {
|
||||
onCancel?.();
|
||||
hideDialog();
|
||||
}
|
||||
useEscapeKey(handleCancel);
|
||||
|
||||
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||
if (beforeSubmit && !beforeSubmit()) {
|
||||
|
@ -78,7 +88,7 @@ export function ModalForm({
|
|||
|
||||
return (
|
||||
<div className='fixed top-0 left-0 w-full h-full z-modal cursor-default'>
|
||||
<ModalBackdrop onHide={hideDialog} />
|
||||
<ModalBackdrop onHide={handleCancel} />
|
||||
<form
|
||||
className={clsx(
|
||||
'cc-animate-modal',
|
||||
|
@ -98,7 +108,7 @@ export function ModalForm({
|
|||
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
|
||||
icon={<IconClose size='1.25rem' />}
|
||||
className='float-right mt-2 mr-2'
|
||||
onClick={hideDialog}
|
||||
onClick={handleCancel}
|
||||
/>
|
||||
|
||||
{header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null}
|
||||
|
@ -125,7 +135,7 @@ export function ModalForm({
|
|||
className='min-w-[7rem]'
|
||||
disabled={!canSubmit}
|
||||
/>
|
||||
<Button text='Отмена' className='min-w-[7rem]' onClick={hideDialog} />
|
||||
<Button text='Отмена' className='min-w-[7rem]' onClick={handleCancel} />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { Loader } from '@/components/Loader';
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { BadgeHelp } from '@/features/help';
|
||||
|
||||
import useEscapeKey from '@/hooks/useEscapeKey';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
@ -9,7 +11,7 @@ import { prepareTooltip } from '@/utils/utils';
|
|||
|
||||
import { Button, MiniButton } from '../Control';
|
||||
import { IconClose } from '../Icons';
|
||||
import { BadgeHelp } from '../shared/BadgeHelp';
|
||||
|
||||
import { ModalBackdrop } from './ModalBackdrop';
|
||||
import { ModalProps } from './ModalForm';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import clsx from 'clsx';
|
||||
import type { TabProps as TabPropsImpl } from 'react-tabs';
|
||||
import { Tab as TabImpl } from 'react-tabs';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { globals } from '@/utils/constants';
|
||||
|
||||
|
|
|
@ -1,77 +1,17 @@
|
|||
import { queryOptions } from '@tanstack/react-query';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport';
|
||||
import { DELAYS } from '@/backend/configuration';
|
||||
import { errorMsg, infoMsg } from '@/utils/labels';
|
||||
import { infoMsg } 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;
|
||||
}
|
||||
import {
|
||||
IChangePasswordDTO,
|
||||
ICurrentUser,
|
||||
IPasswordTokenDTO,
|
||||
IRequestPasswordDTO,
|
||||
IResetPasswordDTO,
|
||||
IUserLoginDTO
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Authentication API.
|
||||
|
|
71
rsconcept/frontend/src/features/auth/backend/types.ts
Normal file
71
rsconcept/frontend/src/features/auth/backend/types.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { errorMsg } from '@/utils/labels';
|
||||
|
||||
/**
|
||||
* Represents CurrentUser information.
|
||||
*/
|
||||
export interface ICurrentUser {
|
||||
id: number | null;
|
||||
username: string;
|
||||
is_staff: boolean;
|
||||
editor: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents login data, used to authenticate users.
|
||||
*/
|
||||
export const schemaUserLogin = z.object({
|
||||
username: z.string().nonempty(errorMsg.requiredField),
|
||||
password: z.string().nonempty(errorMsg.requiredField)
|
||||
});
|
||||
|
||||
/**
|
||||
* Represents login data, used to authenticate users.
|
||||
*/
|
||||
export type IUserLoginDTO = z.infer<typeof schemaUserLogin>;
|
||||
|
||||
/**
|
||||
* Represents data needed to update password for current user.
|
||||
*/
|
||||
export const schemaChangePassword = z
|
||||
.object({
|
||||
old_password: z.string().nonempty(errorMsg.requiredField),
|
||||
new_password: z.string().nonempty(errorMsg.requiredField),
|
||||
new_password2: z.string().nonempty(errorMsg.requiredField)
|
||||
})
|
||||
.refine(schema => schema.new_password === schema.new_password2, {
|
||||
path: ['new_password2'],
|
||||
message: errorMsg.passwordsMismatch
|
||||
})
|
||||
.refine(schema => schema.old_password !== schema.new_password, {
|
||||
path: ['new_password'],
|
||||
message: errorMsg.passwordsSame
|
||||
});
|
||||
|
||||
/**
|
||||
* Represents data needed to update password for current user.
|
||||
*/
|
||||
export type IChangePasswordDTO = z.infer<typeof schemaChangePassword>;
|
||||
|
||||
/**
|
||||
* Represents password reset request data.
|
||||
*/
|
||||
export interface IRequestPasswordDTO {
|
||||
email: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents password reset data.
|
||||
*/
|
||||
export interface IResetPasswordDTO {
|
||||
password: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents password token data.
|
||||
*/
|
||||
export interface IPasswordTokenDTO {
|
||||
token: string;
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { authApi, IChangePasswordDTO } from './api';
|
||||
import { authApi } from './api';
|
||||
import { IChangePasswordDTO } from './types';
|
||||
|
||||
export const useChangePassword = () => {
|
||||
const client = useQueryClient();
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { authApi, IUserLoginDTO } from './api';
|
||||
import { authApi } from './api';
|
||||
import { IUserLoginDTO } from './types';
|
||||
|
||||
export const useLogin = () => {
|
||||
const client = useQueryClient();
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { authApi, IRequestPasswordDTO } from './api';
|
||||
import { authApi } from './api';
|
||||
import { IRequestPasswordDTO } from './types';
|
||||
|
||||
export const useRequestPasswordReset = () => {
|
||||
const mutation = useMutation({
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { authApi, IPasswordTokenDTO, IResetPasswordDTO } from './api';
|
||||
import { authApi } from './api';
|
||||
import { IPasswordTokenDTO, IResetPasswordDTO } from './types';
|
||||
|
||||
export const useResetPassword = () => {
|
||||
const validateMutation = useMutation({
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { urls, useConceptNavigation } from '@/app';
|
||||
|
||||
import { TextURL } from '@/components/Control';
|
||||
|
||||
import { useAuthSuspense } from '../backend/useAuth';
|
||||
import { useLogout } from '../backend/useLogout';
|
||||
|
||||
function ExpectedAnonymous() {
|
||||
export function ExpectedAnonymous() {
|
||||
const { user } = useAuthSuspense();
|
||||
const { logout } = useLogout();
|
||||
const router = useConceptNavigation();
|
||||
|
@ -30,5 +31,3 @@ function ExpectedAnonymous() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExpectedAnonymous;
|
||||
|
|
|
@ -4,7 +4,7 @@ import { TextURL } from '@/components/Control';
|
|||
|
||||
import { useAuthSuspense } from '../backend/useAuth';
|
||||
|
||||
function RequireAuth({ children }: React.PropsWithChildren) {
|
||||
export function RequireAuth({ children }: React.PropsWithChildren) {
|
||||
const { isAnonymous } = useAuthSuspense();
|
||||
|
||||
if (isAnonymous) {
|
||||
|
@ -19,5 +19,3 @@ function RequireAuth({ children }: React.PropsWithChildren) {
|
|||
}
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
export default RequireAuth;
|
||||
|
|
5
rsconcept/frontend/src/features/auth/index.ts
Normal file
5
rsconcept/frontend/src/features/auth/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export { useAuthSuspense } from './backend/useAuth';
|
||||
export { useChangePassword } from './backend/useChangePassword';
|
||||
export { useLogout } from './backend/useLogout';
|
||||
export { ExpectedAnonymous } from './components/ExpectedAnonymous';
|
||||
export { RequireAuth } from './components/RequireAuth';
|
|
@ -1,10 +1,11 @@
|
|||
'use client';
|
||||
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import clsx from 'clsx';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { urls, useConceptNavigation } from '@/app';
|
||||
|
||||
import { isAxiosError } from '@/backend/apiTransport';
|
||||
import { SubmitButton, TextURL } from '@/components/Control';
|
||||
import { ErrorData } from '@/components/InfoError';
|
||||
|
@ -12,10 +13,10 @@ import { TextInput } from '@/components/Input';
|
|||
import useQueryStrings from '@/hooks/useQueryStrings';
|
||||
import { resources } from '@/utils/constants';
|
||||
|
||||
import { IUserLoginDTO, schemaUserLogin } from '../backend/api';
|
||||
import { IUserLoginDTO, schemaUserLogin } from '../backend/types';
|
||||
import { useAuthSuspense } from '../backend/useAuth';
|
||||
import { useLogin } from '../backend/useLogin';
|
||||
import ExpectedAnonymous from '../components/ExpectedAnonymous';
|
||||
import { ExpectedAnonymous } from '../components/ExpectedAnonymous';
|
||||
|
||||
function LoginPage() {
|
||||
const router = useConceptNavigation();
|
||||
|
@ -26,7 +27,6 @@ function LoginPage() {
|
|||
register,
|
||||
handleSubmit,
|
||||
clearErrors,
|
||||
resetField,
|
||||
formState: { errors }
|
||||
} = useForm({
|
||||
resolver: zodResolver(schemaUserLogin),
|
||||
|
@ -38,7 +38,6 @@ function LoginPage() {
|
|||
|
||||
function onSubmit(data: IUserLoginDTO) {
|
||||
return login(data).then(() => {
|
||||
resetField('password');
|
||||
if (router.canBack()) {
|
||||
router.back();
|
||||
} else {
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { useEffect, useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { urls, useConceptNavigation } from '@/app';
|
||||
|
||||
import { isAxiosError } from '@/backend/apiTransport';
|
||||
import { SubmitButton } from '@/components/Control';
|
||||
import { ErrorData, InfoError } from '@/components/InfoError';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { isAxiosError } from '@/backend/apiTransport';
|
||||
import { SubmitButton, TextURL } from '@/components/Control';
|
||||
|
|
|
@ -5,9 +5,10 @@ import { TextURL } from '@/components/Control';
|
|||
import { IconHelp } from '@/components/Icons';
|
||||
import { Loader } from '@/components/Loader';
|
||||
import { CProps } from '@/components/props';
|
||||
import { HelpTopic } from '@/features/help/models/helpTopic';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
|
||||
import { HelpTopic } from '../models/helpTopic';
|
||||
|
||||
const TopicPage = React.lazy(() => import('@/features/help/pages/ManualsPage/TopicPage'));
|
||||
|
||||
interface BadgeHelpProps extends CProps.Styling {
|
|
@ -3,6 +3,7 @@ import clsx from 'clsx';
|
|||
import { colorBgCstClass } from '@/features/rsform/colors';
|
||||
import { describeCstClass, labelCstClass } from '@/features/rsform/labels';
|
||||
import { CstClass } from '@/features/rsform/models/rsform';
|
||||
|
||||
import { prefixes } from '@/utils/constants';
|
||||
|
||||
interface InfoCstClassProps {
|
||||
|
|
|
@ -3,6 +3,7 @@ import clsx from 'clsx';
|
|||
import { colorBgCstStatus } from '@/features/rsform/colors';
|
||||
import { describeExpressionStatus, labelExpressionStatus } from '@/features/rsform/labels';
|
||||
import { ExpressionStatus } from '@/features/rsform/models/rsform';
|
||||
|
||||
import { prefixes } from '@/utils/constants';
|
||||
|
||||
interface InfoCstStatusProps {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { urls } from '@/app';
|
||||
|
||||
import { TextURL } from '@/components/Control';
|
||||
|
||||
import { HelpTopic } from '../models/helpTopic';
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { prefixes } from '@/utils/constants';
|
||||
|
||||
import { HelpTopic, topicParent } from '../models/helpTopic';
|
||||
|
||||
import { TopicItem } from './TopicItem';
|
||||
|
||||
interface SubtopicsProps {
|
||||
|
|
2
rsconcept/frontend/src/features/help/index.ts
Normal file
2
rsconcept/frontend/src/features/help/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { BadgeHelp } from './components/BadgeHelp';
|
||||
export { HelpTopic } from './models/helpTopic';
|
|
@ -1,4 +1,5 @@
|
|||
import { urls } from '@/app';
|
||||
|
||||
import { TextURL } from '@/components/Control';
|
||||
import { external_urls } from '@/utils/constants';
|
||||
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
'use client';
|
||||
|
||||
import { urls, useConceptNavigation } from '@/app';
|
||||
|
||||
import useQueryStrings from '@/hooks/useQueryStrings';
|
||||
import { useMainHeight } from '@/stores/appLayout';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
||||
import { HelpTopic } from '../../models/helpTopic';
|
||||
|
||||
import TopicsList from './TopicsList';
|
||||
import ViewTopic from './ViewTopic';
|
||||
|
||||
|
|
|
@ -9,10 +9,8 @@ import { SelectTree } from '@/components/Input';
|
|||
import { useAppLayoutStore, useFitHeight } from '@/stores/appLayout';
|
||||
import { PARAMETER, prefixes } from '@/utils/constants';
|
||||
|
||||
import { describeHelpTopic } from '../../labels';
|
||||
import { labelHelpTopic } from '../../labels';
|
||||
import { topicParent } from '../../models/helpTopic';
|
||||
import { HelpTopic } from '../../models/helpTopic';
|
||||
import { describeHelpTopic, labelHelpTopic } from '../../labels';
|
||||
import { HelpTopic, topicParent } from '../../models/helpTopic';
|
||||
|
||||
interface TopicsDropdownProps {
|
||||
activeTopic: HelpTopic;
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import useWindowSize from '@/hooks/useWindowSize';
|
||||
|
||||
import { HelpTopic } from '../../models/helpTopic';
|
||||
|
||||
import TopicsDropdown from './TopicsDropdown';
|
||||
import TopicsStatic from './TopicsStatic';
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
'use client';
|
||||
|
||||
import TopicPage from '@/features/help/pages/ManualsPage/TopicPage';
|
||||
import { useMainHeight } from '@/stores/appLayout';
|
||||
|
||||
import { HelpTopic } from '../../models/helpTopic';
|
||||
|
||||
import TopicPage from './TopicPage';
|
||||
|
||||
interface ViewTopicProps {
|
||||
topic: HelpTopic;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useLayoutEffect } from 'react';
|
||||
import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch';
|
||||
|
||||
import { useAppLayoutStore, useFitHeight } from '@/stores/appLayout';
|
||||
|
@ -10,7 +10,7 @@ export function Component() {
|
|||
const hideFooter = useAppLayoutStore(state => state.hideFooter);
|
||||
const panelHeight = useFitHeight('0px');
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
hideFooter(true);
|
||||
return () => hideFooter(false);
|
||||
}, [hideFooter]);
|
||||
|
|
|
@ -1,27 +1,17 @@
|
|||
import { useEffect } from 'react';
|
||||
|
||||
import { urls, useConceptNavigation } from '@/app';
|
||||
import { Loader } from '@/components/Loader';
|
||||
import { useAuthSuspense } from '@/features/auth/backend/useAuth';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { useAuthSuspense } from '@/features/auth';
|
||||
|
||||
function HomePage() {
|
||||
const router = useConceptNavigation();
|
||||
const { isAnonymous } = useAuthSuspense();
|
||||
|
||||
useEffect(() => {
|
||||
if (isAnonymous) {
|
||||
setTimeout(() => {
|
||||
router.replace(urls.manuals);
|
||||
}, PARAMETER.refreshTimeout);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
router.replace(urls.library);
|
||||
}, PARAMETER.refreshTimeout);
|
||||
}
|
||||
}, [router, isAnonymous]);
|
||||
|
||||
return <Loader />;
|
||||
return null;
|
||||
}
|
||||
|
||||
export default HomePage;
|
||||
|
|
|
@ -1,145 +1,42 @@
|
|||
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 { DELAYS } from '@/backend/configuration';
|
||||
import { ossApi } from '@/features/oss/backend/api';
|
||||
import { IRSFormDTO, rsformsApi } from '@/features/rsform/backend/api';
|
||||
import { errorMsg, infoMsg } from '@/utils/labels';
|
||||
import { DELAYS, KEYS } from '@/backend/configuration';
|
||||
import { infoMsg } from '@/utils/labels';
|
||||
|
||||
import { AccessPolicy, ILibraryItem, IVersionInfo, LibraryItemID, LibraryItemType, VersionID } from '../models/library';
|
||||
import { validateLocation } from '../models/libraryAPI';
|
||||
|
||||
/**
|
||||
* Represents update data for renaming Location.
|
||||
*/
|
||||
export interface IRenameLocationDTO {
|
||||
target: string;
|
||||
new_location: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents data, used for cloning {@link IRSForm}.
|
||||
*/
|
||||
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>;
|
||||
import {
|
||||
AccessPolicy,
|
||||
ICloneLibraryItemDTO,
|
||||
ICreateLibraryItemDTO,
|
||||
ILibraryItem,
|
||||
IRenameLocationDTO,
|
||||
IUpdateLibraryItemDTO,
|
||||
IVersionCreateDTO,
|
||||
IVersionInfo,
|
||||
IVersionUpdateDTO,
|
||||
schemaLibraryItem,
|
||||
schemaLibraryItemArray,
|
||||
schemaVersionInfo
|
||||
} from './types';
|
||||
|
||||
export const libraryApi = {
|
||||
baseKey: 'library',
|
||||
libraryListKey: ['library', 'list'],
|
||||
baseKey: KEYS.library,
|
||||
libraryListKey: KEYS.composite.libraryList,
|
||||
|
||||
getItemQueryOptions: ({ itemID, itemType }: { itemID: LibraryItemID; itemType: LibraryItemType }) => {
|
||||
return itemType === LibraryItemType.RSFORM
|
||||
? rsformsApi.getRSFormQueryOptions({ itemID })
|
||||
: ossApi.getOssQueryOptions({ itemID });
|
||||
},
|
||||
getLibraryQueryOptions: ({ isAdmin }: { isAdmin: boolean }) =>
|
||||
queryOptions({
|
||||
queryKey: [...libraryApi.libraryListKey, isAdmin ? 'admin' : 'user'],
|
||||
staleTime: DELAYS.staleMedium,
|
||||
queryFn: meta =>
|
||||
axiosGet<ILibraryItem[]>({
|
||||
schema: schemaLibraryItemArray,
|
||||
endpoint: isAdmin ? '/api/library/all' : '/api/library/active',
|
||||
options: { signal: meta.signal }
|
||||
})
|
||||
|
@ -150,6 +47,7 @@ export const libraryApi = {
|
|||
staleTime: DELAYS.staleMedium,
|
||||
queryFn: meta =>
|
||||
axiosGet<ILibraryItem[]>({
|
||||
schema: schemaLibraryItemArray,
|
||||
endpoint: '/api/library/templates',
|
||||
options: { signal: meta.signal }
|
||||
})
|
||||
|
@ -157,6 +55,7 @@ export const libraryApi = {
|
|||
|
||||
createItem: (data: ICreateLibraryItemDTO) =>
|
||||
axiosPost<ICreateLibraryItemDTO, ILibraryItem>({
|
||||
schema: schemaLibraryItem,
|
||||
endpoint: !data.file ? '/api/library' : '/api/rsforms/create-detailed',
|
||||
request: {
|
||||
data: data,
|
||||
|
@ -172,13 +71,14 @@ export const libraryApi = {
|
|||
}),
|
||||
updateItem: (data: IUpdateLibraryItemDTO) =>
|
||||
axiosPatch<IUpdateLibraryItemDTO, ILibraryItem>({
|
||||
schema: schemaLibraryItem,
|
||||
endpoint: `/api/library/${data.id}`,
|
||||
request: {
|
||||
data: data,
|
||||
successMessage: infoMsg.changesSaved
|
||||
}
|
||||
}),
|
||||
setOwner: ({ itemID, owner }: { itemID: LibraryItemID; owner: number }) =>
|
||||
setOwner: ({ itemID, owner }: { itemID: number; owner: number }) =>
|
||||
axiosPatch({
|
||||
endpoint: `/api/library/${itemID}/set-owner`,
|
||||
request: {
|
||||
|
@ -186,7 +86,7 @@ export const libraryApi = {
|
|||
successMessage: infoMsg.changesSaved
|
||||
}
|
||||
}),
|
||||
setLocation: ({ itemID, location }: { itemID: LibraryItemID; location: string }) =>
|
||||
setLocation: ({ itemID, location }: { itemID: number; location: string }) =>
|
||||
axiosPatch({
|
||||
endpoint: `/api/library/${itemID}/set-location`,
|
||||
request: {
|
||||
|
@ -194,7 +94,7 @@ export const libraryApi = {
|
|||
successMessage: infoMsg.moveComplete
|
||||
}
|
||||
}),
|
||||
setAccessPolicy: ({ itemID, policy }: { itemID: LibraryItemID; policy: AccessPolicy }) =>
|
||||
setAccessPolicy: ({ itemID, policy }: { itemID: number; policy: AccessPolicy }) =>
|
||||
axiosPatch({
|
||||
endpoint: `/api/library/${itemID}/set-access-policy`,
|
||||
request: {
|
||||
|
@ -202,7 +102,7 @@ export const libraryApi = {
|
|||
successMessage: infoMsg.changesSaved
|
||||
}
|
||||
}),
|
||||
setEditors: ({ itemID, editors }: { itemID: LibraryItemID; editors: number[] }) =>
|
||||
setEditors: ({ itemID, editors }: { itemID: number; editors: number[] }) =>
|
||||
axiosPatch({
|
||||
endpoint: `/api/library/${itemID}/set-editors`,
|
||||
request: {
|
||||
|
@ -211,7 +111,7 @@ export const libraryApi = {
|
|||
}
|
||||
}),
|
||||
|
||||
deleteItem: (target: LibraryItemID) =>
|
||||
deleteItem: (target: number) =>
|
||||
axiosDelete({
|
||||
endpoint: `/api/library/${target}`,
|
||||
request: {
|
||||
|
@ -220,6 +120,7 @@ export const libraryApi = {
|
|||
}),
|
||||
cloneItem: (data: ICloneLibraryItemDTO) =>
|
||||
axiosPost<ICloneLibraryItemDTO, IRSFormDTO>({
|
||||
schema: schemaRSForm,
|
||||
endpoint: `/api/library/${data.id}/clone`,
|
||||
request: {
|
||||
data: data,
|
||||
|
@ -235,30 +136,33 @@ export const libraryApi = {
|
|||
}
|
||||
}),
|
||||
|
||||
versionCreate: ({ itemID, data }: { itemID: LibraryItemID; data: IVersionCreateDTO }) =>
|
||||
versionCreate: ({ itemID, data }: { itemID: number; data: IVersionCreateDTO }) =>
|
||||
axiosPost<IVersionCreateDTO, IVersionCreatedResponse>({
|
||||
schema: schemaVersionCreatedResponse,
|
||||
endpoint: `/api/library/${itemID}/create-version`,
|
||||
request: {
|
||||
data: data,
|
||||
successMessage: infoMsg.newVersion(data.version)
|
||||
}
|
||||
}),
|
||||
versionRestore: ({ versionID }: { versionID: VersionID }) =>
|
||||
versionRestore: ({ versionID }: { versionID: number }) =>
|
||||
axiosPatch<undefined, IRSFormDTO>({
|
||||
schema: schemaRSForm,
|
||||
endpoint: `/api/versions/${versionID}/restore`,
|
||||
request: {
|
||||
successMessage: infoMsg.versionRestored
|
||||
}
|
||||
}),
|
||||
versionUpdate: (data: IVersionUpdateDTO) =>
|
||||
versionUpdate: (data: { itemID: number; version: IVersionUpdateDTO }) =>
|
||||
axiosPatch<IVersionUpdateDTO, IVersionInfo>({
|
||||
endpoint: `/api/versions/${data.id}`,
|
||||
schema: schemaVersionInfo,
|
||||
endpoint: `/api/versions/${data.version.id}`,
|
||||
request: {
|
||||
data: data,
|
||||
data: data.version,
|
||||
successMessage: infoMsg.changesSaved
|
||||
}
|
||||
}),
|
||||
versionDelete: (data: { itemID: LibraryItemID; versionID: VersionID }) =>
|
||||
versionDelete: (data: { itemID: number; versionID: number }) =>
|
||||
axiosDelete({
|
||||
endpoint: `/api/versions/${data.versionID}`,
|
||||
request: {
|
||||
|
|
142
rsconcept/frontend/src/features/library/backend/types.ts
Normal file
142
rsconcept/frontend/src/features/library/backend/types.ts
Normal file
|
@ -0,0 +1,142 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { errorMsg } from '@/utils/labels';
|
||||
|
||||
import { validateLocation } from '../models/libraryAPI';
|
||||
|
||||
/** Represents type of library items. */
|
||||
export enum LibraryItemType {
|
||||
RSFORM = 'rsform',
|
||||
OSS = 'oss'
|
||||
}
|
||||
|
||||
/** Represents Access policy for library items.*/
|
||||
export enum AccessPolicy {
|
||||
PUBLIC = 'public',
|
||||
PROTECTED = 'protected',
|
||||
PRIVATE = 'private'
|
||||
}
|
||||
|
||||
/** Represents library item common data typical for all item types. */
|
||||
export type ILibraryItem = z.infer<typeof schemaLibraryItem>;
|
||||
|
||||
/** Represents {@link ILibraryItem} data loaded for both OSS and RSForm. */
|
||||
export interface ILibraryItemData extends ILibraryItem {
|
||||
editors: number[];
|
||||
}
|
||||
|
||||
/** Represents update data for renaming Location. */
|
||||
export interface IRenameLocationDTO {
|
||||
target: string;
|
||||
new_location: string;
|
||||
}
|
||||
|
||||
/** Represents library item version information. */
|
||||
export type IVersionInfo = z.infer<typeof schemaVersionInfo>;
|
||||
|
||||
/** Represents data, used for cloning {@link IRSForm}. */
|
||||
export type ICloneLibraryItemDTO = z.infer<typeof schemaCloneLibraryItem>;
|
||||
|
||||
/** Represents data, used for creating {@link IRSForm}. */
|
||||
export type ICreateLibraryItemDTO = z.infer<typeof schemaCreateLibraryItem>;
|
||||
|
||||
/** Represents update data for editing {@link ILibraryItem}. */
|
||||
export type IUpdateLibraryItemDTO = z.infer<typeof schemaUpdateLibraryItem>;
|
||||
|
||||
/** Create version metadata in persistent storage. */
|
||||
export type IVersionCreateDTO = z.infer<typeof schemaVersionCreate>;
|
||||
|
||||
/** Represents version data, intended to update version metadata in persistent storage. */
|
||||
export type IVersionUpdateDTO = z.infer<typeof schemaVersionUpdate>;
|
||||
|
||||
// ======= SCHEMAS =========
|
||||
|
||||
export const schemaLibraryItem = z.object({
|
||||
id: z.coerce.number(),
|
||||
item_type: z.nativeEnum(LibraryItemType),
|
||||
title: z.string(),
|
||||
alias: z.string().nonempty(),
|
||||
comment: z.string(),
|
||||
visible: z.boolean(),
|
||||
read_only: z.boolean(),
|
||||
location: z.string(),
|
||||
access_policy: z.nativeEnum(AccessPolicy),
|
||||
|
||||
time_create: z.string(),
|
||||
time_update: z.string(),
|
||||
owner: z.coerce.number().nullable()
|
||||
});
|
||||
|
||||
export const schemaLibraryItemArray = z.array(schemaLibraryItem);
|
||||
|
||||
export const schemaCloneLibraryItem = schemaLibraryItem
|
||||
.pick({
|
||||
id: true,
|
||||
item_type: true,
|
||||
title: true,
|
||||
alias: true,
|
||||
comment: true,
|
||||
visible: true,
|
||||
read_only: true,
|
||||
location: true,
|
||||
access_policy: true
|
||||
})
|
||||
.extend({
|
||||
title: z.string().nonempty(errorMsg.requiredField),
|
||||
alias: z.string().nonempty(errorMsg.requiredField),
|
||||
location: z.string().refine(data => validateLocation(data), { message: errorMsg.invalidLocation }),
|
||||
|
||||
items: z.array(z.number()).optional()
|
||||
});
|
||||
|
||||
export const schemaCreateLibraryItem = z
|
||||
.object({
|
||||
item_type: z.nativeEnum(LibraryItemType),
|
||||
title: z.string().optional(),
|
||||
alias: z.string().optional(),
|
||||
comment: z.string(),
|
||||
visible: z.boolean(),
|
||||
read_only: z.boolean(),
|
||||
location: z.string().refine(data => validateLocation(data), { message: errorMsg.invalidLocation }),
|
||||
access_policy: z.nativeEnum(AccessPolicy),
|
||||
|
||||
file: z.instanceof(File).optional(),
|
||||
fileName: z.string().optional()
|
||||
})
|
||||
.refine(data => !!data.file || !!data.title, {
|
||||
path: ['title'],
|
||||
message: errorMsg.requiredField
|
||||
})
|
||||
.refine(data => !!data.file || !!data.alias, {
|
||||
path: ['alias'],
|
||||
message: errorMsg.requiredField
|
||||
});
|
||||
|
||||
export const schemaUpdateLibraryItem = z.object({
|
||||
id: z.number(),
|
||||
item_type: z.nativeEnum(LibraryItemType),
|
||||
title: z.string().nonempty(errorMsg.requiredField),
|
||||
alias: z.string().nonempty(errorMsg.requiredField),
|
||||
comment: z.string(),
|
||||
visible: z.boolean(),
|
||||
read_only: z.boolean()
|
||||
});
|
||||
|
||||
export const schemaVersionInfo = z.object({
|
||||
id: z.coerce.number(),
|
||||
version: z.string(),
|
||||
description: z.string(),
|
||||
time_create: z.string()
|
||||
});
|
||||
|
||||
export const schemaVersionUpdate = z.object({
|
||||
id: z.number(),
|
||||
version: z.string().nonempty(errorMsg.requiredField),
|
||||
description: z.string()
|
||||
});
|
||||
|
||||
export const schemaVersionCreate = z.object({
|
||||
version: z.string(),
|
||||
description: z.string(),
|
||||
items: z.array(z.number()).optional()
|
||||
});
|
|
@ -1,7 +1,8 @@
|
|||
import { useAuthSuspense } from '@/features/auth/backend/useAuth';
|
||||
import { useAuthSuspense } from '@/features/auth';
|
||||
|
||||
import { ILibraryFilter } from '../models/library';
|
||||
import { matchLibraryItem, matchLibraryItemLocation } from '../models/libraryAPI';
|
||||
|
||||
import { useLibrary } from './useLibrary';
|
||||
|
||||
export function useApplyLibraryFilter(filter: ILibraryFilter) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { ICloneLibraryItemDTO, libraryApi } from './api';
|
||||
import { libraryApi } from './api';
|
||||
import { ICloneLibraryItemDTO } from './types';
|
||||
|
||||
export const useCloneItem = () => {
|
||||
const client = useQueryClient();
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { ICreateLibraryItemDTO, libraryApi } from './api';
|
||||
import { libraryApi } from './api';
|
||||
import { ICreateLibraryItemDTO } from './types';
|
||||
|
||||
export const useCreateItem = () => {
|
||||
const client = useQueryClient();
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { ossApi } from '@/features/oss/backend/api';
|
||||
import { rsformsApi } from '@/features/rsform/backend/api';
|
||||
import { KEYS } from '@/backend/configuration';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
||||
import { LibraryItemID } from '../models/library';
|
||||
import { libraryApi } from './api';
|
||||
|
||||
export const useDeleteItem = () => {
|
||||
|
@ -17,16 +15,16 @@ export const useDeleteItem = () => {
|
|||
setTimeout(
|
||||
() =>
|
||||
void Promise.allSettled([
|
||||
client.invalidateQueries({ queryKey: [ossApi.baseKey] }),
|
||||
client.resetQueries({ queryKey: rsformsApi.getRSFormQueryOptions({ itemID: variables }).queryKey }),
|
||||
client.resetQueries({ queryKey: ossApi.getOssQueryOptions({ itemID: variables }).queryKey })
|
||||
client.invalidateQueries({ queryKey: [KEYS.oss] }),
|
||||
client.resetQueries({ queryKey: KEYS.composite.rsItem({ itemID: variables }) }),
|
||||
client.resetQueries({ queryKey: KEYS.composite.ossItem({ itemID: variables }) })
|
||||
]).catch(console.error),
|
||||
PARAMETER.navigationDuration
|
||||
);
|
||||
}
|
||||
});
|
||||
return {
|
||||
deleteItem: (target: LibraryItemID) => mutation.mutateAsync(target),
|
||||
deleteItem: (target: number) => mutation.mutateAsync(target),
|
||||
isPending: mutation.isPending
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { FolderTree } from '@/features/library/models/FolderTree';
|
||||
|
||||
import { FolderTree } from '../models/FolderTree';
|
||||
import { LocationHead } from '../models/library';
|
||||
|
||||
import { useLibrary } from './useLibrary';
|
||||
|
||||
export function useFolders() {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useAuthSuspense } from '@/features/auth';
|
||||
|
||||
import { queryClient } from '@/backend/queryClient';
|
||||
import { useAuthSuspense } from '@/features/auth/backend/useAuth';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
|
||||
import { libraryApi } from './api';
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
import { useIsMutating } from '@tanstack/react-query';
|
||||
|
||||
import { ossApi } from '@/features/oss/backend/api';
|
||||
import { rsformsApi } from '@/features/rsform/backend/api';
|
||||
|
||||
import { libraryApi } from './api';
|
||||
import { KEYS } from '@/backend/configuration';
|
||||
|
||||
export const useMutatingLibrary = () => {
|
||||
const countMutations = useIsMutating({ mutationKey: [libraryApi.baseKey] });
|
||||
const countOss = useIsMutating({ mutationKey: [ossApi.baseKey] });
|
||||
const countRSForm = useIsMutating({ mutationKey: [rsformsApi.baseKey] });
|
||||
const countMutations = useIsMutating({ mutationKey: [KEYS.library] });
|
||||
const countOss = useIsMutating({ mutationKey: [KEYS.oss] });
|
||||
const countRSForm = useIsMutating({ mutationKey: [KEYS.rsform] });
|
||||
return countMutations + countOss + countRSForm !== 0;
|
||||
};
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { ossApi } from '@/features/oss/backend/api';
|
||||
import { rsformsApi } from '@/features/rsform/backend/api';
|
||||
import { KEYS } from '@/backend/configuration';
|
||||
|
||||
import { IRenameLocationDTO, libraryApi } from './api';
|
||||
import { libraryApi } from './api';
|
||||
import { IRenameLocationDTO } from './types';
|
||||
|
||||
export const useRenameLocation = () => {
|
||||
const client = useQueryClient();
|
||||
|
@ -12,9 +12,9 @@ export const useRenameLocation = () => {
|
|||
mutationFn: libraryApi.renameLocation,
|
||||
onSuccess: () =>
|
||||
Promise.allSettled([
|
||||
client.invalidateQueries({ queryKey: [libraryApi.baseKey] }),
|
||||
client.invalidateQueries({ queryKey: [rsformsApi.baseKey] }),
|
||||
client.invalidateQueries({ queryKey: [ossApi.baseKey] })
|
||||
client.invalidateQueries({ queryKey: [KEYS.library] }),
|
||||
client.invalidateQueries({ queryKey: [KEYS.rsform] }),
|
||||
client.invalidateQueries({ queryKey: [KEYS.oss] })
|
||||
])
|
||||
});
|
||||
return {
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { IOperationSchemaDTO, ossApi } from '@/features/oss/backend/api';
|
||||
import { rsformsApi } from '@/features/rsform/backend/api';
|
||||
import { IOperationSchemaDTO } from '@/features/oss/backend/types';
|
||||
import { IRSFormDTO } from '@/features/rsform/backend/types';
|
||||
|
||||
import { KEYS } from '@/backend/configuration';
|
||||
|
||||
import { AccessPolicy, ILibraryItem, LibraryItemID } from '../models/library';
|
||||
import { libraryApi } from './api';
|
||||
import { AccessPolicy, ILibraryItem } from './types';
|
||||
|
||||
export const useSetAccessPolicy = () => {
|
||||
const client = useQueryClient();
|
||||
|
@ -12,26 +14,28 @@ export const useSetAccessPolicy = () => {
|
|||
mutationKey: [libraryApi.baseKey, 'set-location'],
|
||||
mutationFn: libraryApi.setAccessPolicy,
|
||||
onSuccess: (_, variables) => {
|
||||
const ossKey = ossApi.getOssQueryOptions({ itemID: variables.itemID }).queryKey;
|
||||
const ossKey = KEYS.composite.ossItem({ itemID: variables.itemID });
|
||||
const ossData: IOperationSchemaDTO | undefined = client.getQueryData(ossKey);
|
||||
if (ossData) {
|
||||
client.setQueryData(ossKey, { ...ossData, access_policy: variables.policy });
|
||||
return Promise.allSettled([
|
||||
client.invalidateQueries({ queryKey: libraryApi.libraryListKey }),
|
||||
client.invalidateQueries({ queryKey: KEYS.composite.libraryList }),
|
||||
...ossData.items
|
||||
.map(item => {
|
||||
if (!item.result) {
|
||||
return;
|
||||
}
|
||||
const itemKey = rsformsApi.getRSFormQueryOptions({ itemID: item.result }).queryKey;
|
||||
const itemKey = KEYS.composite.rsItem({ itemID: item.result });
|
||||
return client.invalidateQueries({ queryKey: itemKey });
|
||||
})
|
||||
.filter(item => !!item)
|
||||
]);
|
||||
}
|
||||
|
||||
const rsKey = rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey;
|
||||
client.setQueryData(rsKey, prev => (!prev ? undefined : { ...prev, access_policy: variables.policy }));
|
||||
const rsKey = KEYS.composite.rsItem({ itemID: variables.itemID });
|
||||
client.setQueryData(rsKey, (prev: IRSFormDTO | undefined) =>
|
||||
!prev ? undefined : { ...prev, access_policy: variables.policy }
|
||||
);
|
||||
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
|
||||
prev?.map(item => (item.id === variables.itemID ? { ...item, access_policy: variables.policy } : item))
|
||||
);
|
||||
|
@ -39,6 +43,6 @@ export const useSetAccessPolicy = () => {
|
|||
});
|
||||
|
||||
return {
|
||||
setAccessPolicy: (data: { itemID: LibraryItemID; policy: AccessPolicy }) => mutation.mutateAsync(data)
|
||||
setAccessPolicy: (data: { itemID: number; policy: AccessPolicy }) => mutation.mutateAsync(data)
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { ossApi } from '@/features/oss/backend/api';
|
||||
import { rsformsApi } from '@/features/rsform/backend/api';
|
||||
import { IOperationSchemaDTO } from '@/features/oss/backend/types';
|
||||
import { IRSFormDTO } from '@/features/rsform/backend/types';
|
||||
|
||||
import { KEYS } from '@/backend/configuration';
|
||||
|
||||
import { libraryApi } from './api';
|
||||
|
||||
|
@ -11,8 +13,8 @@ export const useSetEditors = () => {
|
|||
mutationKey: [libraryApi.baseKey, 'set-location'],
|
||||
mutationFn: libraryApi.setEditors,
|
||||
onSuccess: (_, variables) => {
|
||||
const ossKey = ossApi.getOssQueryOptions({ itemID: variables.itemID }).queryKey;
|
||||
const ossData = client.getQueryData(ossKey);
|
||||
const ossKey = KEYS.composite.ossItem({ itemID: variables.itemID });
|
||||
const ossData: IOperationSchemaDTO | undefined = client.getQueryData(ossKey);
|
||||
if (ossData) {
|
||||
client.setQueryData(ossKey, { ...ossData, editors: variables.editors });
|
||||
return Promise.allSettled(
|
||||
|
@ -21,15 +23,17 @@ export const useSetEditors = () => {
|
|||
if (!item.result) {
|
||||
return;
|
||||
}
|
||||
const itemKey = rsformsApi.getRSFormQueryOptions({ itemID: item.result }).queryKey;
|
||||
const itemKey = KEYS.composite.rsItem({ itemID: item.result });
|
||||
return client.invalidateQueries({ queryKey: itemKey });
|
||||
})
|
||||
.filter(item => !!item)
|
||||
);
|
||||
}
|
||||
|
||||
const rsKey = rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey;
|
||||
client.setQueryData(rsKey, prev => (!prev ? undefined : { ...prev, editors: variables.editors }));
|
||||
const rsKey = KEYS.composite.rsItem({ itemID: variables.itemID });
|
||||
client.setQueryData(rsKey, (prev: IRSFormDTO | undefined) =>
|
||||
!prev ? undefined : { ...prev, editors: variables.editors }
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { IOperationSchemaDTO, ossApi } from '@/features/oss/backend/api';
|
||||
import { rsformsApi } from '@/features/rsform/backend/api';
|
||||
import { IOperationSchemaDTO } from '@/features/oss/backend/types';
|
||||
import { IRSFormDTO } from '@/features/rsform/backend/types';
|
||||
|
||||
import { KEYS } from '@/backend/configuration';
|
||||
|
||||
import { ILibraryItem, LibraryItemID } from '../models/library';
|
||||
import { libraryApi } from './api';
|
||||
import { ILibraryItem } from './types';
|
||||
|
||||
export const useSetLocation = () => {
|
||||
const client = useQueryClient();
|
||||
|
@ -12,7 +14,7 @@ export const useSetLocation = () => {
|
|||
mutationKey: [libraryApi.baseKey, 'set-location'],
|
||||
mutationFn: libraryApi.setLocation,
|
||||
onSuccess: (_, variables) => {
|
||||
const ossKey = ossApi.getOssQueryOptions({ itemID: variables.itemID }).queryKey;
|
||||
const ossKey = KEYS.composite.ossItem({ itemID: variables.itemID });
|
||||
const ossData: IOperationSchemaDTO | undefined = client.getQueryData(ossKey);
|
||||
if (ossData) {
|
||||
client.setQueryData(ossKey, { ...ossData, location: variables.location });
|
||||
|
@ -23,15 +25,17 @@ export const useSetLocation = () => {
|
|||
if (!item.result) {
|
||||
return;
|
||||
}
|
||||
const itemKey = rsformsApi.getRSFormQueryOptions({ itemID: item.result }).queryKey;
|
||||
const itemKey = KEYS.composite.rsItem({ itemID: item.result });
|
||||
return client.invalidateQueries({ queryKey: itemKey });
|
||||
})
|
||||
.filter(item => !!item)
|
||||
]);
|
||||
}
|
||||
|
||||
const rsKey = rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey;
|
||||
client.setQueryData(rsKey, prev => (!prev ? undefined : { ...prev, location: variables.location }));
|
||||
const rsKey = KEYS.composite.rsItem({ itemID: variables.itemID });
|
||||
client.setQueryData(rsKey, (prev: IRSFormDTO | undefined) =>
|
||||
!prev ? undefined : { ...prev, location: variables.location }
|
||||
);
|
||||
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
|
||||
prev?.map(item => (item.id === variables.itemID ? { ...item, location: variables.location } : item))
|
||||
);
|
||||
|
@ -39,6 +43,6 @@ export const useSetLocation = () => {
|
|||
});
|
||||
|
||||
return {
|
||||
setLocation: (data: { itemID: LibraryItemID; location: string }) => mutation.mutateAsync(data)
|
||||
setLocation: (data: { itemID: number; location: string }) => mutation.mutateAsync(data)
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { IOperationSchemaDTO, ossApi } from '@/features/oss/backend/api';
|
||||
import { rsformsApi } from '@/features/rsform/backend/api';
|
||||
import { IOperationSchemaDTO } from '@/features/oss/backend/types';
|
||||
import { IRSFormDTO } from '@/features/rsform/backend/types';
|
||||
|
||||
import { KEYS } from '@/backend/configuration';
|
||||
|
||||
import { ILibraryItem } from '../models/library';
|
||||
import { libraryApi } from './api';
|
||||
import { ILibraryItem } from './types';
|
||||
|
||||
export const useSetOwner = () => {
|
||||
const client = useQueryClient();
|
||||
|
@ -12,7 +14,7 @@ export const useSetOwner = () => {
|
|||
mutationKey: [libraryApi.baseKey, 'set-owner'],
|
||||
mutationFn: libraryApi.setOwner,
|
||||
onSuccess: (_, variables) => {
|
||||
const ossKey = ossApi.getOssQueryOptions({ itemID: variables.itemID }).queryKey;
|
||||
const ossKey = KEYS.composite.ossItem({ itemID: variables.itemID });
|
||||
const ossData: IOperationSchemaDTO | undefined = client.getQueryData(ossKey);
|
||||
if (ossData) {
|
||||
client.setQueryData(ossKey, { ...ossData, owner: variables.owner });
|
||||
|
@ -23,15 +25,17 @@ export const useSetOwner = () => {
|
|||
if (!item.result) {
|
||||
return;
|
||||
}
|
||||
const itemKey = rsformsApi.getRSFormQueryOptions({ itemID: item.result }).queryKey;
|
||||
const itemKey = KEYS.composite.rsItem({ itemID: item.result });
|
||||
return client.invalidateQueries({ queryKey: itemKey });
|
||||
})
|
||||
.filter(item => !!item)
|
||||
]);
|
||||
}
|
||||
|
||||
const rsKey = rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey;
|
||||
client.setQueryData(rsKey, prev => (!prev ? undefined : { ...prev, owner: variables.owner }));
|
||||
const rsKey = KEYS.composite.rsItem({ itemID: variables.itemID });
|
||||
client.setQueryData(rsKey, (prev: IRSFormDTO | undefined) =>
|
||||
!prev ? undefined : { ...prev, owner: variables.owner }
|
||||
);
|
||||
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
|
||||
prev?.map(item => (item.id === variables.itemID ? { ...item, owner: variables.owner } : item))
|
||||
);
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { IOperationSchemaDTO, ossApi } from '@/features/oss/backend/api';
|
||||
import { IRSFormDTO } from '@/features/rsform/backend/api';
|
||||
import { IOperationSchemaDTO } from '@/features/oss/backend/types';
|
||||
import { IRSFormDTO } from '@/features/rsform/backend/types';
|
||||
|
||||
import { ILibraryItem, LibraryItemType } from '../models/library';
|
||||
import { IUpdateLibraryItemDTO, libraryApi } from './api';
|
||||
import { KEYS } from '@/backend/configuration';
|
||||
|
||||
import { libraryApi } from './api';
|
||||
import { ILibraryItem, IUpdateLibraryItemDTO, LibraryItemType } from './types';
|
||||
|
||||
export const useUpdateItem = () => {
|
||||
const client = useQueryClient();
|
||||
|
@ -12,7 +14,10 @@ export const useUpdateItem = () => {
|
|||
mutationKey: [libraryApi.baseKey, 'update-item'],
|
||||
mutationFn: libraryApi.updateItem,
|
||||
onSuccess: (data: ILibraryItem) => {
|
||||
const itemKey = libraryApi.getItemQueryOptions({ itemID: data.id, itemType: data.item_type }).queryKey;
|
||||
const itemKey =
|
||||
data.item_type === LibraryItemType.RSFORM
|
||||
? KEYS.composite.rsItem({ itemID: data.id })
|
||||
: KEYS.composite.ossItem({ itemID: data.id });
|
||||
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
|
||||
prev?.map(item => (item.id === data.id ? data : item))
|
||||
);
|
||||
|
@ -23,9 +28,7 @@ export const useUpdateItem = () => {
|
|||
const schema: IRSFormDTO | undefined = client.getQueryData(itemKey);
|
||||
if (schema) {
|
||||
return Promise.allSettled(
|
||||
schema.oss.map(item =>
|
||||
client.invalidateQueries({ queryKey: ossApi.getOssQueryOptions({ itemID: item.id }).queryKey })
|
||||
)
|
||||
schema.oss.map(item => client.invalidateQueries({ queryKey: KEYS.composite.ossItem({ itemID: item.id }) }))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { ILibraryItem, LibraryItemID } from '../models/library';
|
||||
import { libraryApi } from './api';
|
||||
import { ILibraryItem } from './types';
|
||||
|
||||
export function useUpdateTimestamp() {
|
||||
const client = useQueryClient();
|
||||
return {
|
||||
updateTimestamp: (target: LibraryItemID) =>
|
||||
updateTimestamp: (target: number) =>
|
||||
client.setQueryData(
|
||||
libraryApi.libraryListKey, //
|
||||
(prev: ILibraryItem[] | undefined) =>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { rsformsApi } from '@/features/rsform/backend/api';
|
||||
import { KEYS } from '@/backend/configuration';
|
||||
|
||||
import { LibraryItemID } from '../models/library';
|
||||
import { IVersionCreateDTO, libraryApi } from './api';
|
||||
import { libraryApi } from './api';
|
||||
import { IVersionCreateDTO } from './types';
|
||||
import { useUpdateTimestamp } from './useUpdateTimestamp';
|
||||
|
||||
export const useVersionCreate = () => {
|
||||
|
@ -13,12 +13,12 @@ export const useVersionCreate = () => {
|
|||
mutationKey: [libraryApi.baseKey, 'create-version'],
|
||||
mutationFn: libraryApi.versionCreate,
|
||||
onSuccess: data => {
|
||||
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.schema.id }).queryKey, data.schema);
|
||||
client.setQueryData(KEYS.composite.rsItem({ itemID: data.schema.id }), data.schema);
|
||||
updateTimestamp(data.schema.id);
|
||||
}
|
||||
});
|
||||
return {
|
||||
versionCreate: (data: { itemID: LibraryItemID; data: IVersionCreateDTO }) =>
|
||||
versionCreate: (data: { itemID: number; data: IVersionCreateDTO }) =>
|
||||
mutation.mutateAsync(data).then(response => response.version)
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { IRSFormDTO, rsformsApi } from '@/features/rsform/backend/api';
|
||||
import { IRSFormDTO } from '@/features/rsform/backend/types';
|
||||
|
||||
import { KEYS } from '@/backend/configuration';
|
||||
|
||||
import { LibraryItemID, VersionID } from '../models/library';
|
||||
import { libraryApi } from './api';
|
||||
|
||||
export const useVersionDelete = () => {
|
||||
|
@ -11,9 +12,7 @@ export const useVersionDelete = () => {
|
|||
mutationKey: [libraryApi.baseKey, 'delete-version'],
|
||||
mutationFn: libraryApi.versionDelete,
|
||||
onSuccess: (_, variables) => {
|
||||
client.setQueryData(
|
||||
rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey,
|
||||
(prev: IRSFormDTO | undefined) =>
|
||||
client.setQueryData(KEYS.composite.rsItem({ itemID: variables.itemID }), (prev: IRSFormDTO | undefined) =>
|
||||
!prev
|
||||
? undefined
|
||||
: {
|
||||
|
@ -24,6 +23,6 @@ export const useVersionDelete = () => {
|
|||
}
|
||||
});
|
||||
return {
|
||||
versionDelete: (data: { itemID: LibraryItemID; versionID: VersionID }) => mutation.mutateAsync(data)
|
||||
versionDelete: (data: { itemID: number; versionID: number }) => mutation.mutateAsync(data)
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { rsformsApi } from '@/features/rsform/backend/api';
|
||||
import { KEYS } from '@/backend/configuration';
|
||||
|
||||
import { VersionID } from '../models/library';
|
||||
import { libraryApi } from './api';
|
||||
|
||||
export const useVersionRestore = () => {
|
||||
|
@ -11,11 +10,11 @@ export const useVersionRestore = () => {
|
|||
mutationKey: [libraryApi.baseKey, 'restore-version'],
|
||||
mutationFn: libraryApi.versionRestore,
|
||||
onSuccess: data => {
|
||||
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data);
|
||||
client.setQueryData(KEYS.composite.rsItem({ itemID: data.id }), data);
|
||||
return client.invalidateQueries({ queryKey: [libraryApi.baseKey] });
|
||||
}
|
||||
});
|
||||
return {
|
||||
versionRestore: (data: { versionID: VersionID }) => mutation.mutateAsync(data)
|
||||
versionRestore: (data: { versionID: number }) => mutation.mutateAsync(data)
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,32 +1,31 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { IRSFormDTO, rsformsApi } from '@/features/rsform/backend/api';
|
||||
import { IRSFormDTO } from '@/features/rsform/backend/types';
|
||||
|
||||
import { IVersionUpdateDTO, libraryApi } from './api';
|
||||
import { KEYS } from '@/backend/configuration';
|
||||
|
||||
import { libraryApi } from './api';
|
||||
import { IVersionUpdateDTO } from './types';
|
||||
|
||||
export const useVersionUpdate = () => {
|
||||
const client = useQueryClient();
|
||||
const mutation = useMutation({
|
||||
mutationKey: [libraryApi.baseKey, 'update-version'],
|
||||
mutationFn: libraryApi.versionUpdate,
|
||||
onSuccess: data => {
|
||||
client.setQueryData(
|
||||
rsformsApi.getRSFormQueryOptions({ itemID: data.item }).queryKey,
|
||||
(prev: IRSFormDTO | undefined) =>
|
||||
onSuccess: (data, variables) => {
|
||||
client.setQueryData(KEYS.composite.rsItem({ itemID: variables.itemID }), (prev: IRSFormDTO | undefined) =>
|
||||
!prev
|
||||
? undefined
|
||||
: {
|
||||
...prev,
|
||||
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 {
|
||||
versionUpdate: (data: IVersionUpdateDTO) => mutation.mutateAsync(data)
|
||||
versionUpdate: (data: { itemID: number; version: IVersionUpdateDTO }) => mutation.mutateAsync(data)
|
||||
};
|
||||
};
|
||||
|
|
|
@ -2,6 +2,8 @@ import { Suspense } from 'react';
|
|||
import { useIntl } from 'react-intl';
|
||||
|
||||
import { urls, useConceptNavigation } from '@/app';
|
||||
import { InfoUsers, SelectUser, useLabelUser, useRoleStore, UserRole } from '@/features/users';
|
||||
|
||||
import { Overlay, Tooltip } from '@/components/Container';
|
||||
import { MiniButton } from '@/components/Control';
|
||||
import { useDropdown } from '@/components/Dropdown';
|
||||
|
@ -16,19 +18,28 @@ import {
|
|||
import { Loader } from '@/components/Loader';
|
||||
import { CProps } from '@/components/props';
|
||||
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 { useModificationStore } from '@/stores/modification';
|
||||
import { prefixes } from '@/utils/constants';
|
||||
import { promptText } from '@/utils/labels';
|
||||
|
||||
import { ILibraryItemData } from '../backend/types';
|
||||
import { useMutatingLibrary } from '../backend/useMutatingLibrary';
|
||||
import { useSetLocation } from '../backend/useSetLocation';
|
||||
import { useSetOwner } from '../backend/useSetOwner';
|
||||
import { ILibraryItemEditor } from '../models/library';
|
||||
import { useLibrarySearchStore } from '../stores/librarySearch';
|
||||
|
||||
/**
|
||||
* Represents common {@link ILibraryItem} editor controller.
|
||||
*/
|
||||
export interface ILibraryItemEditor {
|
||||
schema: ILibraryItemData;
|
||||
deleteSchema: () => void;
|
||||
|
||||
isMutable: boolean;
|
||||
isAttachedToOSS: boolean;
|
||||
}
|
||||
|
||||
interface EditorLibraryItemProps {
|
||||
controller: ILibraryItemEditor;
|
||||
}
|
||||
|
|
|
@ -7,15 +7,16 @@ import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
|
|||
import { IconOSS } from '@/components/Icons';
|
||||
import { Label } from '@/components/Input';
|
||||
import { CProps } from '@/components/props';
|
||||
import { ILibraryItemReference } from '@/features/library/models/library';
|
||||
import { prefixes } from '@/utils/constants';
|
||||
|
||||
import { ILibraryItemReference } from '../models/library';
|
||||
|
||||
interface MiniSelectorOSSProps extends CProps.Styling {
|
||||
items: ILibraryItemReference[];
|
||||
onSelect: (event: CProps.EventMouse, newValue: ILibraryItemReference) => void;
|
||||
}
|
||||
|
||||
function MiniSelectorOSS({ items, onSelect, className, ...restProps }: MiniSelectorOSSProps) {
|
||||
export function MiniSelectorOSS({ items, onSelect, className, ...restProps }: MiniSelectorOSSProps) {
|
||||
const ossMenu = useDropdown();
|
||||
|
||||
function onToggle(event: CProps.EventMouse) {
|
||||
|
@ -50,5 +51,3 @@ function MiniSelectorOSS({ items, onSelect, className, ...restProps }: MiniSelec
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MiniSelectorOSS;
|
|
@ -1,25 +1,26 @@
|
|||
import clsx from 'clsx';
|
||||
import { useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { FlexColumn } from '@/components/Container';
|
||||
import { MiniButton } from '@/components/Control';
|
||||
import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/DataTable';
|
||||
import { Dropdown, useDropdown } from '@/components/Dropdown';
|
||||
import { IconClose, IconFolderTree } from '@/components/Icons';
|
||||
import { SearchBar } from '@/components/Input';
|
||||
import { CProps } from '@/components/props';
|
||||
import { SearchBar } from '@/components/shared/SearchBar';
|
||||
import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/features/library/models/library';
|
||||
import { matchLibraryItem } from '@/features/library/models/libraryAPI';
|
||||
import { APP_COLORS } from '@/styling/colors';
|
||||
import { prefixes } from '@/utils/constants';
|
||||
|
||||
import SelectLocation from '../../library/components/SelectLocation';
|
||||
import { ILibraryItem, LibraryItemType } from '../backend/types';
|
||||
import { matchLibraryItem } from '../models/libraryAPI';
|
||||
|
||||
import SelectLocation from './SelectLocation';
|
||||
|
||||
interface PickSchemaProps extends CProps.Styling {
|
||||
id?: string;
|
||||
value: LibraryItemID | null;
|
||||
onChange: (newValue: LibraryItemID) => void;
|
||||
value: number | null;
|
||||
onChange: (newValue: number) => void;
|
||||
|
||||
initialFilter?: string;
|
||||
rows?: number;
|
|
@ -1,13 +1,14 @@
|
|||
'use client';
|
||||
|
||||
import { MiniButton } from '@/components/Control';
|
||||
import { PolicyIcon } from '@/components/DomainIcons';
|
||||
import { DomIconProps } from '@/components/DomainIcons';
|
||||
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
|
||||
import { IconPrivate, IconProtected, IconPublic } from '@/components/Icons';
|
||||
import { CProps } from '@/components/props';
|
||||
import { prefixes } from '@/utils/constants';
|
||||
import { describeAccessPolicy, labelAccessPolicy } from '@/utils/labels';
|
||||
|
||||
import { AccessPolicy } from '../models/library';
|
||||
import { AccessPolicy } from '../backend/types';
|
||||
import { describeAccessPolicy, labelAccessPolicy } from '../labels';
|
||||
|
||||
interface SelectAccessPolicyProps extends CProps.Styling {
|
||||
value: AccessPolicy;
|
||||
|
@ -17,7 +18,7 @@ interface SelectAccessPolicyProps extends CProps.Styling {
|
|||
stretchLeft?: boolean;
|
||||
}
|
||||
|
||||
function SelectAccessPolicy({ value, disabled, stretchLeft, onChange, ...restProps }: SelectAccessPolicyProps) {
|
||||
export function SelectAccessPolicy({ value, disabled, stretchLeft, onChange, ...restProps }: SelectAccessPolicyProps) {
|
||||
const menu = useDropdown();
|
||||
|
||||
function handleChange(newValue: AccessPolicy) {
|
||||
|
@ -52,4 +53,14 @@ function SelectAccessPolicy({ value, disabled, stretchLeft, onChange, ...restPro
|
|||
);
|
||||
}
|
||||
|
||||
export default SelectAccessPolicy;
|
||||
/** Icon for access policy. */
|
||||
function PolicyIcon({ value, size = '1.25rem', className }: DomIconProps<AccessPolicy>) {
|
||||
switch (value) {
|
||||
case AccessPolicy.PRIVATE:
|
||||
return <IconPrivate size={size} className={className ?? 'text-warn-600'} />;
|
||||
case AccessPolicy.PROTECTED:
|
||||
return <IconProtected size={size} className={className ?? 'text-sec-600'} />;
|
||||
case AccessPolicy.PUBLIC:
|
||||
return <IconPublic size={size} className={className ?? 'text-ok-600'} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
'use client';
|
||||
|
||||
import { SelectorButton } from '@/components/Control';
|
||||
import { ItemTypeIcon } from '@/components/DomainIcons';
|
||||
import { DomIconProps } from '@/components/DomainIcons';
|
||||
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
|
||||
import { IconOSS, IconRSForm } from '@/components/Icons';
|
||||
import { CProps } from '@/components/props';
|
||||
import { prefixes } from '@/utils/constants';
|
||||
import { describeLibraryItemType, labelLibraryItemType } from '@/utils/labels';
|
||||
|
||||
import { LibraryItemType } from '../models/library';
|
||||
import { LibraryItemType } from '../backend/types';
|
||||
import { describeLibraryItemType, labelLibraryItemType } from '../labels';
|
||||
|
||||
interface SelectItemTypeProps extends CProps.Styling {
|
||||
value: LibraryItemType;
|
||||
|
@ -16,7 +17,7 @@ interface SelectItemTypeProps extends CProps.Styling {
|
|||
stretchLeft?: boolean;
|
||||
}
|
||||
|
||||
function SelectItemType({ value, disabled, stretchLeft, onChange, ...restProps }: SelectItemTypeProps) {
|
||||
export function SelectItemType({ value, disabled, stretchLeft, onChange, ...restProps }: SelectItemTypeProps) {
|
||||
const menu = useDropdown();
|
||||
|
||||
function handleChange(newValue: LibraryItemType) {
|
||||
|
@ -53,4 +54,12 @@ function SelectItemType({ value, disabled, stretchLeft, onChange, ...restProps }
|
|||
);
|
||||
}
|
||||
|
||||
export default SelectItemType;
|
||||
/** Icon for library item type. */
|
||||
function ItemTypeIcon({ value, size = '1.25rem', className }: DomIconProps<LibraryItemType>) {
|
||||
switch (value) {
|
||||
case LibraryItemType.RSFORM:
|
||||
return <IconRSForm size={size} className={className ?? 'text-sec-600'} />;
|
||||
case LibraryItemType.OSS:
|
||||
return <IconOSS size={size} className={className ?? 'text-ok-600'} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import clsx from 'clsx';
|
|||
import { SelectSingle } from '@/components/Input';
|
||||
import { CProps } from '@/components/props';
|
||||
|
||||
import { ILibraryItem, LibraryItemID } from '../models/library';
|
||||
import { ILibraryItem } from '../backend/types';
|
||||
import { matchLibraryItem } from '../models/libraryAPI';
|
||||
|
||||
interface SelectLibraryItemProps extends CProps.Styling {
|
||||
|
@ -17,7 +17,7 @@ interface SelectLibraryItemProps extends CProps.Styling {
|
|||
noBorder?: boolean;
|
||||
}
|
||||
|
||||
function SelectLibraryItem({
|
||||
export function SelectLibraryItem({
|
||||
className,
|
||||
items,
|
||||
value,
|
||||
|
@ -31,9 +31,9 @@ function SelectLibraryItem({
|
|||
label: `${cst.alias}: ${cst.title}`
|
||||
})) ?? [];
|
||||
|
||||
function filter(option: { value: LibraryItemID | undefined; label: string }, inputValue: string) {
|
||||
const item = items?.find(item => item.id === option.value);
|
||||
return !item ? false : matchLibraryItem(item, inputValue);
|
||||
function filter(option: { value: string | undefined; label: string }, query: string) {
|
||||
const item = items?.find(item => item.id === Number(option.value));
|
||||
return !item ? false : matchLibraryItem(item, query);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -42,12 +42,9 @@ function SelectLibraryItem({
|
|||
options={options}
|
||||
value={value ? { value: value.id, label: `${value.alias}: ${value.title}` } : null}
|
||||
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}
|
||||
placeholder={placeholder}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectLibraryItem;
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { useEffect, useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { MiniButton } from '@/components/Control';
|
||||
import { IconFolder, IconFolderClosed, IconFolderEmpty, IconFolderOpened } from '@/components/Icons';
|
||||
import { CProps } from '@/components/props';
|
||||
import { FolderNode } from '@/features/library/models/FolderTree';
|
||||
|
||||
import { useFolders } from '../backend/useFolders';
|
||||
import { labelFolderNode } from '../labels';
|
||||
import { FolderNode } from '../models/FolderTree';
|
||||
|
||||
interface SelectLocationProps extends CProps.Styling {
|
||||
value: string;
|
||||
|
|
|
@ -17,7 +17,7 @@ interface SelectLocationContextProps extends CProps.Styling {
|
|||
stretchTop?: boolean;
|
||||
}
|
||||
|
||||
function SelectLocationContext({
|
||||
export function SelectLocationContext({
|
||||
value,
|
||||
title = 'Проводник...',
|
||||
onChange,
|
||||
|
@ -56,5 +56,3 @@ function SelectLocationContext({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectLocationContext;
|
||||
|
|
|
@ -17,7 +17,13 @@ interface SelectLocationHeadProps extends CProps.Styling {
|
|||
excluded?: LocationHead[];
|
||||
}
|
||||
|
||||
function SelectLocationHead({ value, excluded = [], onChange, className, ...restProps }: SelectLocationHeadProps) {
|
||||
export function SelectLocationHead({
|
||||
value,
|
||||
excluded = [],
|
||||
onChange,
|
||||
className,
|
||||
...restProps
|
||||
}: SelectLocationHeadProps) {
|
||||
const menu = useDropdown();
|
||||
|
||||
function handleChange(newValue: LocationHead) {
|
||||
|
@ -60,5 +66,3 @@ function SelectLocationHead({ value, excluded = [], onChange, className, ...rest
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectLocationHead;
|
||||
|
|
|
@ -4,21 +4,21 @@ import clsx from 'clsx';
|
|||
|
||||
import { SelectSingle } from '@/components/Input';
|
||||
import { CProps } from '@/components/props';
|
||||
import { IVersionInfo, VersionID } from '@/features/library/models/library';
|
||||
|
||||
import { labelVersion } from '../labels';
|
||||
import { labelVersion } from '../../rsform/labels';
|
||||
import { IVersionInfo } from '../backend/types';
|
||||
|
||||
interface SelectVersionProps extends CProps.Styling {
|
||||
id?: string;
|
||||
items?: IVersionInfo[];
|
||||
value?: VersionID;
|
||||
onChange: (newValue?: VersionID) => void;
|
||||
value?: number;
|
||||
onChange: (newValue?: number) => void;
|
||||
|
||||
placeholder?: string;
|
||||
noBorder?: boolean;
|
||||
}
|
||||
|
||||
function SelectVersion({ id, className, items, value, onChange, ...restProps }: SelectVersionProps) {
|
||||
export function SelectVersion({ id, className, items, value, onChange, ...restProps }: SelectVersionProps) {
|
||||
const options = [
|
||||
{
|
||||
value: undefined,
|
||||
|
@ -46,5 +46,3 @@ function SelectVersion({ id, className, items, value, onChange, ...restProps }:
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectVersion;
|
|
@ -1,18 +1,20 @@
|
|||
import { BadgeHelp, HelpTopic } from '@/features/help';
|
||||
import { useRoleStore, UserRole } from '@/features/users';
|
||||
|
||||
import { Overlay } from '@/components/Container';
|
||||
import { MiniButton } from '@/components/Control';
|
||||
import { VisibilityIcon } from '@/components/DomainIcons';
|
||||
import { IconImmutable, IconMutable } from '@/components/Icons';
|
||||
import { Label } from '@/components/Input';
|
||||
import { BadgeHelp } from '@/components/shared/BadgeHelp';
|
||||
import { HelpTopic } from '@/features/help/models/helpTopic';
|
||||
import { useMutatingLibrary } from '@/features/library/backend/useMutatingLibrary';
|
||||
import { useSetAccessPolicy } from '@/features/library/backend/useSetAccessPolicy';
|
||||
import SelectAccessPolicy from '@/features/library/components/SelectAccessPolicy';
|
||||
import { AccessPolicy, ILibraryItemEditor } from '@/features/library/models/library';
|
||||
import { useRoleStore } from '@/features/users';
|
||||
import { UserRole } from '@/features/users/models/user';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
||||
import { AccessPolicy } from '../backend/types';
|
||||
import { useMutatingLibrary } from '../backend/useMutatingLibrary';
|
||||
import { useSetAccessPolicy } from '../backend/useSetAccessPolicy';
|
||||
|
||||
import { ILibraryItemEditor } from './EditorLibraryItem';
|
||||
import { SelectAccessPolicy } from './SelectAccessPolicy';
|
||||
|
||||
interface ToolbarItemAccessProps {
|
||||
visible: boolean;
|
||||
toggleVisible: () => void;
|
||||
|
@ -21,7 +23,13 @@ interface ToolbarItemAccessProps {
|
|||
controller: ILibraryItemEditor;
|
||||
}
|
||||
|
||||
function ToolbarItemAccess({ visible, toggleVisible, readOnly, toggleReadOnly, controller }: ToolbarItemAccessProps) {
|
||||
export function ToolbarItemAccess({
|
||||
visible,
|
||||
toggleVisible,
|
||||
readOnly,
|
||||
toggleReadOnly,
|
||||
controller
|
||||
}: ToolbarItemAccessProps) {
|
||||
const role = useRoleStore(state => state.role);
|
||||
const isProcessing = useMutatingLibrary();
|
||||
const policy = controller.schema.access_policy;
|
||||
|
@ -66,5 +74,3 @@ function ToolbarItemAccess({ visible, toggleVisible, readOnly, toggleReadOnly, c
|
|||
</Overlay>
|
||||
);
|
||||
}
|
||||
|
||||
export default ToolbarItemAccess;
|
|
@ -1,19 +1,20 @@
|
|||
'use client';
|
||||
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import clsx from 'clsx';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useAuthSuspense } from '@/features/auth';
|
||||
|
||||
import { Label, TextArea } from '@/components/Input';
|
||||
import { ModalForm } from '@/components/Modal';
|
||||
import { useAuthSuspense } from '@/features/auth/backend/useAuth';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { limits } from '@/utils/constants';
|
||||
import { errorMsg } from '@/utils/labels';
|
||||
|
||||
import SelectLocationContext from '../components/SelectLocationContext';
|
||||
import SelectLocationHead from '../components/SelectLocationHead';
|
||||
import { SelectLocationContext } from '../components/SelectLocationContext';
|
||||
import { SelectLocationHead } from '../components/SelectLocationHead';
|
||||
import { LocationHead } from '../models/library';
|
||||
import { combineLocation, validateLocation } from '../models/libraryAPI';
|
||||
|
||||
|
|
|
@ -1,30 +1,30 @@
|
|||
'use client';
|
||||
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import clsx from 'clsx';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
|
||||
import { urls, useConceptNavigation } from '@/app';
|
||||
import { useAuthSuspense } from '@/features/auth';
|
||||
|
||||
import { MiniButton } from '@/components/Control';
|
||||
import { VisibilityIcon } from '@/components/DomainIcons';
|
||||
import { Checkbox, Label, TextArea, TextInput } from '@/components/Input';
|
||||
import { ModalForm } from '@/components/Modal';
|
||||
import { useAuthSuspense } from '@/features/auth/backend/useAuth';
|
||||
import { ICloneLibraryItemDTO, schemaCloneLibraryItem } from '@/features/library/backend/api';
|
||||
import { useCloneItem } from '@/features/library/backend/useCloneItem';
|
||||
import SelectAccessPolicy from '@/features/library/components/SelectAccessPolicy';
|
||||
import SelectLocationContext from '@/features/library/components/SelectLocationContext';
|
||||
import SelectLocationHead from '@/features/library/components/SelectLocationHead';
|
||||
import { AccessPolicy, ILibraryItem, LocationHead } from '@/features/library/models/library';
|
||||
import { cloneTitle, combineLocation } from '@/features/library/models/libraryAPI';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
|
||||
import { ConstituentaID } from '../models/rsform';
|
||||
import { AccessPolicy, ICloneLibraryItemDTO, ILibraryItem, schemaCloneLibraryItem } from '../backend/types';
|
||||
import { useCloneItem } from '../backend/useCloneItem';
|
||||
import { SelectAccessPolicy } from '../components/SelectAccessPolicy';
|
||||
import { SelectLocationContext } from '../components/SelectLocationContext';
|
||||
import { SelectLocationHead } from '../components/SelectLocationHead';
|
||||
import { LocationHead } from '../models/library';
|
||||
import { cloneTitle, combineLocation } from '../models/libraryAPI';
|
||||
|
||||
export interface DlgCloneLibraryItemProps {
|
||||
base: ILibraryItem;
|
||||
initialLocation: string;
|
||||
selected: ConstituentaID[];
|
||||
selected: number[];
|
||||
totalCount: number;
|
||||
}
|
||||
|
|
@ -1,42 +1,36 @@
|
|||
'use client';
|
||||
|
||||
import { Controller, useForm, useWatch } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import clsx from 'clsx';
|
||||
import { Controller, useForm, useWatch } from 'react-hook-form';
|
||||
|
||||
import { Checkbox, TextArea, TextInput } from '@/components/Input';
|
||||
import { ModalForm } from '@/components/Modal';
|
||||
import { IVersionCreateDTO, schemaVersionCreate } from '@/features/library/backend/api';
|
||||
import { useVersionCreate } from '@/features/library/backend/useVersionCreate';
|
||||
import { IVersionInfo, LibraryItemID, VersionID } from '@/features/library/models/library';
|
||||
import { nextVersion } from '@/features/library/models/libraryAPI';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { errorMsg } from '@/utils/labels';
|
||||
|
||||
import { ConstituentaID } from '../models/rsform';
|
||||
import { IVersionCreateDTO, IVersionInfo, schemaVersionCreate } from '../backend/types';
|
||||
import { useVersionCreate } from '../backend/useVersionCreate';
|
||||
import { nextVersion } from '../models/libraryAPI';
|
||||
|
||||
export interface DlgCreateVersionProps {
|
||||
itemID: LibraryItemID;
|
||||
itemID: number;
|
||||
versions: IVersionInfo[];
|
||||
onCreate: (newVersion: VersionID) => void;
|
||||
selected: ConstituentaID[];
|
||||
onCreate: (newVersion: number) => void;
|
||||
selected: number[];
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
function DlgCreateVersion() {
|
||||
const {
|
||||
itemID, //
|
||||
versions,
|
||||
selected,
|
||||
totalCount,
|
||||
onCreate
|
||||
} = useDialogsStore(state => state.props as DlgCreateVersionProps);
|
||||
const { itemID, versions, selected, totalCount, onCreate } = useDialogsStore(
|
||||
state => state.props as DlgCreateVersionProps
|
||||
);
|
||||
const { versionCreate } = useVersionCreate();
|
||||
|
||||
const { register, handleSubmit, control } = useForm<IVersionCreateDTO>({
|
||||
resolver: zodResolver(schemaVersionCreate),
|
||||
defaultValues: {
|
||||
version: versions.length > 0 ? nextVersion(versions[0].version) : '1.0.0',
|
||||
version: versions.length > 0 ? nextVersion(versions[versions.length - 1].version) : '1.0.0',
|
||||
description: '',
|
||||
items: undefined
|
||||
}
|
|
@ -1,13 +1,14 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { SelectUser, TableUsers, useUsers } from '@/features/users';
|
||||
|
||||
import { MiniButton } from '@/components/Control';
|
||||
import { IconRemove } from '@/components/Icons';
|
||||
import { Label } from '@/components/Input';
|
||||
import { ModalForm } from '@/components/Modal';
|
||||
import { SelectUser, TableUsers, useUsers } from '@/features/users';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
|
||||
import { useSetEditors } from '../../backend/useSetEditors';
|
||||
|
|
|
@ -1,28 +1,29 @@
|
|||
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useMemo } from 'react';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import { useRSFormSuspense } from '@/features/rsform';
|
||||
|
||||
import { MiniButton } from '@/components/Control';
|
||||
import { IconReset, IconSave } from '@/components/Icons';
|
||||
import { TextArea, TextInput } from '@/components/Input';
|
||||
import { ModalView } from '@/components/Modal';
|
||||
import { IVersionUpdateDTO, schemaVersionUpdate } from '@/features/library/backend/api';
|
||||
import { useMutatingLibrary } from '@/features/library/backend/useMutatingLibrary';
|
||||
import { useVersionDelete } from '@/features/library/backend/useVersionDelete';
|
||||
import { useVersionUpdate } from '@/features/library/backend/useVersionUpdate';
|
||||
import { LibraryItemID, VersionID } from '@/features/library/models/library';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { errorMsg } from '@/utils/labels';
|
||||
|
||||
import { useRSFormSuspense } from '../../backend/useRSForm';
|
||||
import { IVersionUpdateDTO, schemaVersionUpdate } from '../../backend/types';
|
||||
import { useMutatingLibrary } from '../../backend/useMutatingLibrary';
|
||||
import { useVersionDelete } from '../../backend/useVersionDelete';
|
||||
import { useVersionUpdate } from '../../backend/useVersionUpdate';
|
||||
|
||||
import TableVersions from './TableVersions';
|
||||
|
||||
export interface DlgEditVersionsProps {
|
||||
itemID: LibraryItemID;
|
||||
afterDelete: (targetVersion: VersionID) => void;
|
||||
itemID: number;
|
||||
afterDelete: (targetVersion: number) => void;
|
||||
}
|
||||
|
||||
function DlgEditVersions() {
|
||||
|
@ -42,9 +43,9 @@ function DlgEditVersions() {
|
|||
} = useForm<IVersionUpdateDTO>({
|
||||
resolver: zodResolver(schemaVersionUpdate),
|
||||
defaultValues: {
|
||||
id: schema.versions[0].id,
|
||||
version: schema.versions[0].version,
|
||||
description: schema.versions[0].description
|
||||
id: schema.versions[schema.versions.length - 1].id,
|
||||
version: schema.versions[schema.versions.length - 1].version,
|
||||
description: schema.versions[schema.versions.length - 1].description
|
||||
},
|
||||
context: { schema: schema }
|
||||
});
|
||||
|
@ -56,7 +57,7 @@ function DlgEditVersions() {
|
|||
[schema, versionID, versionName]
|
||||
);
|
||||
|
||||
function handleSelectVersion(targetVersion: VersionID) {
|
||||
function handleSelectVersion(targetVersion: number) {
|
||||
const ver = schema.versions.find(ver => ver.id === targetVersion);
|
||||
if (!ver) {
|
||||
return;
|
||||
|
@ -64,7 +65,7 @@ function DlgEditVersions() {
|
|||
reset({ ...ver });
|
||||
}
|
||||
|
||||
function handleDeleteVersion(targetVersion: VersionID) {
|
||||
function handleDeleteVersion(targetVersion: number) {
|
||||
const nextVer = schema.versions.find(ver => ver.id !== targetVersion);
|
||||
void versionDelete({ itemID: itemID, versionID: targetVersion }).then(() => {
|
||||
if (!nextVer) {
|
||||
|
@ -80,20 +81,20 @@ function DlgEditVersions() {
|
|||
if (!isDirty || isProcessing || !isValid) {
|
||||
return;
|
||||
}
|
||||
void versionUpdate(data).then(() => reset({ ...data }));
|
||||
void versionUpdate({ itemID: itemID, version: data }).then(() => reset({ ...data }));
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalView header='Редактирование версий' className='flex flex-col w-[40rem] px-6 gap-3 pb-6'>
|
||||
<TableVersions
|
||||
processing={isProcessing}
|
||||
items={schema.versions}
|
||||
items={schema.versions.reverse()}
|
||||
onDelete={handleDeleteVersion}
|
||||
onSelect={handleSelectVersion}
|
||||
selected={versionID}
|
||||
/>
|
||||
|
||||
<form className='flex' onSubmit={event => void handleSubmit(onUpdate)(event)}>
|
||||
<form className='flex items-center ' onSubmit={event => void handleSubmit(onUpdate)(event)}>
|
||||
<TextInput
|
||||
id='dlg_version'
|
||||
{...register('version')}
|
||||
|
@ -102,7 +103,7 @@ function DlgEditVersions() {
|
|||
className='w-[16rem] mr-3'
|
||||
error={formErrors.version}
|
||||
/>
|
||||
<div className='cc-icons'>
|
||||
<div className='cc-icons h-fit'>
|
||||
<MiniButton
|
||||
type='submit'
|
||||
title={isValid ? 'Сохранить изменения' : errorMsg.versionTaken}
|
|
@ -1,20 +1,21 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { useIntl } from 'react-intl';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { MiniButton } from '@/components/Control';
|
||||
import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/DataTable';
|
||||
import { IconRemove } from '@/components/Icons';
|
||||
import { IVersionInfo, VersionID } from '@/features/library/models/library';
|
||||
import { APP_COLORS } from '@/styling/colors';
|
||||
|
||||
import { IVersionInfo } from '../../backend/types';
|
||||
|
||||
interface TableVersionsProps {
|
||||
processing: boolean;
|
||||
items: IVersionInfo[];
|
||||
selected?: VersionID;
|
||||
onDelete: (versionID: VersionID) => void;
|
||||
onSelect: (versionID: VersionID) => void;
|
||||
selected?: number;
|
||||
onDelete: (versionID: number) => void;
|
||||
onSelect: (versionID: number) => void;
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<IVersionInfo>();
|
||||
|
@ -22,7 +23,7 @@ const columnHelper = createColumnHelper<IVersionInfo>();
|
|||
function TableVersions({ processing, items, onDelete, selected, onSelect }: TableVersionsProps) {
|
||||
const intl = useIntl();
|
||||
|
||||
function handleDeleteVersion(event: React.MouseEvent, targetVersion: VersionID) {
|
||||
function handleDeleteVersion(event: React.MouseEvent, targetVersion: number) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onDelete(targetVersion);
|
16
rsconcept/frontend/src/features/library/index.ts
Normal file
16
rsconcept/frontend/src/features/library/index.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
export { AccessPolicy, type ILibraryItem, type IVersionInfo, LibraryItemType } from './backend/types';
|
||||
export { useDeleteItem } from './backend/useDeleteItem';
|
||||
export { useLibrary, useLibrarySuspense } from './backend/useLibrary';
|
||||
export { useMutatingLibrary } from './backend/useMutatingLibrary';
|
||||
export { useTemplatesSuspense } from './backend/useTemplates';
|
||||
export { useUpdateItem } from './backend/useUpdateItem';
|
||||
export { useUpdateTimestamp } from './backend/useUpdateTimestamp';
|
||||
export { useVersionRestore } from './backend/useVersionRestore';
|
||||
export { EditorLibraryItem, type ILibraryItemEditor } from './components/EditorLibraryItem';
|
||||
export { MiniSelectorOSS } from './components/MiniSelectorOSS';
|
||||
export { PickSchema } from './components/PickSchema';
|
||||
export { SelectLibraryItem } from './components/SelectLibraryItem';
|
||||
export { SelectVersion } from './components/SelectVersion';
|
||||
export { ToolbarItemAccess } from './components/ToolbarItemAccess';
|
||||
export { type ILibraryItemReference } from './models/library';
|
||||
export { useLibrarySearchStore } from './stores/librarySearch';
|
|
@ -1,3 +1,4 @@
|
|||
import { AccessPolicy, LibraryItemType } from './backend/types';
|
||||
import { FolderNode } from './models/FolderTree';
|
||||
import { LocationHead } from './models/library';
|
||||
import { validateLocation } from './models/libraryAPI';
|
||||
|
@ -45,3 +46,52 @@ export function labelFolderNode(node: FolderNode): string {
|
|||
export function describeFolderNode(node: FolderNode): string {
|
||||
return `${node.filesInside} | ${node.filesTotal}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves label for {@link AccessPolicy}.
|
||||
*/
|
||||
export function labelAccessPolicy(policy: AccessPolicy): string {
|
||||
// prettier-ignore
|
||||
switch (policy) {
|
||||
case AccessPolicy.PRIVATE: return 'Личный';
|
||||
case AccessPolicy.PROTECTED: return 'Защищенный';
|
||||
case AccessPolicy.PUBLIC: return 'Открытый';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves description for {@link AccessPolicy}.
|
||||
*/
|
||||
export function describeAccessPolicy(policy: AccessPolicy): string {
|
||||
// prettier-ignore
|
||||
switch (policy) {
|
||||
case AccessPolicy.PRIVATE:
|
||||
return 'Доступ только для владельца';
|
||||
case AccessPolicy.PROTECTED:
|
||||
return 'Доступ для владельца и редакторов';
|
||||
case AccessPolicy.PUBLIC:
|
||||
return 'Открытый доступ';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves label for {@link LibraryItemType}.
|
||||
*/
|
||||
export function labelLibraryItemType(itemType: LibraryItemType): string {
|
||||
// prettier-ignore
|
||||
switch (itemType) {
|
||||
case LibraryItemType.RSFORM: return 'КС';
|
||||
case LibraryItemType.OSS: return 'ОСС';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves description for {@link LibraryItemType}.
|
||||
*/
|
||||
export function describeLibraryItemType(itemType: LibraryItemType): string {
|
||||
// prettier-ignore
|
||||
switch (itemType) {
|
||||
case LibraryItemType.RSFORM: return 'Концептуальная схема';
|
||||
case LibraryItemType.OSS: return 'Операционная схема синтеза';
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user