Compare commits

..

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

283 changed files with 3267 additions and 3706 deletions

View File

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

View File

@ -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,8 +37,7 @@ export default [
'react': reactPlugin,
'react-compiler': reactCompilerPlugin,
'react-hooks': reactHooksPlugin,
'simple-import-sort': simpleImportSort,
'import': importPlugin
'simple-import-sort': simpleImportSort
},
settings: { react: { version: 'detect' } },
rules: {
@ -58,33 +57,8 @@ export default [
'react-refresh/only-export-components': ['off', { allowConstantExport: true }],
'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/imports': 'warn',
'simple-import-sort/exports': 'error',
'import/no-duplicates': 'warn',
...reactHooksPlugin.configs.recommended.rules
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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 { GlobalLoader } from './GlobalLoader';
import { ToasterThemed } from './GlobalToaster';
import ConceptToaster 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'>
<ToasterThemed
<ConceptToaster
className='text-[14px] cc-animate-position'
style={{ marginTop: noNavigationAnimation ? '1.5rem' : '3.5rem' }}
autoClose={3000}
@ -47,8 +47,9 @@ function ApplicationLayout() {
}}
>
<main className='cc-scroll-y' style={{ overflowY: showScroll ? 'scroll' : 'auto', minHeight: mainHeight }}>
<GlobalLoader />
<Outlet />
<Suspense fallback={<Loader />}>
<Outlet />
</Suspense>
</main>
{!noNavigation && !noFooter ? <Footer /> : null}
</div>

View File

@ -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/library/dialogs/DlgCloneLibraryItem'));
const DlgCloneLibraryItem = React.lazy(() => import('@/features/rsform/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/library/dialogs/DlgCreateVersion'));
const DlgCreateVersion = React.lazy(() => import('@/features/rsform/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/library/dialogs/DlgEditVersions'));
const DlgEditVersions = React.lazy(() => import('@/features/rsform/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'));

View File

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

View File

@ -1,8 +1,8 @@
'use client';
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';

View File

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

View File

@ -1,9 +1,8 @@
'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';

View File

@ -7,7 +7,6 @@ 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';

View File

@ -1,6 +1,5 @@
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';

View File

@ -1,5 +1,3 @@
import { useAuthSuspense, useLogout } from '@/features/auth';
import { Dropdown, DropdownButton } from '@/components/Dropdown';
import {
IconAdmin,
@ -16,10 +14,11 @@ 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 {

View File

@ -4,7 +4,6 @@ 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';

View File

@ -1,5 +1,6 @@
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';
@ -11,8 +12,6 @@ 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';
@ -23,7 +22,7 @@ export const Router = createBrowserRouter([
element: <ApplicationLayout />,
errorElement: <ErrorFallback />,
loader: prefetchAuth,
hydrateFallbackElement: fallbackLoader(),
hydrateFallbackElement: <Loader />,
children: [
{
path: '',
@ -99,11 +98,3 @@ 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>
);
}

View File

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

View File

@ -7,18 +7,3 @@ 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 ?? '']
}
};

View File

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

View File

@ -1,7 +1,6 @@
'use client';
'use no memo';
import { useMemo, useState } from 'react';
import {
ColumnSort,
createColumnHelper,
@ -16,9 +15,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';

View File

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

View File

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

View File

@ -1,10 +1,18 @@
import { LocationHead } from '@/features/library/models/library';
import { ExpressionStatus } from '@/features/rsform/models/rsform';
import { AccessPolicy, LibraryItemType, LocationHead } from '@/features/library/models/library';
import { CstType, 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,
@ -14,8 +22,12 @@ import {
IconHide,
IconMoveDown,
IconMoveUp,
IconOSS,
IconPrivate,
IconProps,
IconProtected,
IconPublic,
IconRSForm,
IconSettings,
IconShow,
IconStatusError,
@ -33,6 +45,28 @@ 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) {
@ -115,6 +149,28 @@ export function StatusIcon({ value, size = '1.25rem', className }: DomIconProps<
}
}
/** Icon for constituenta type. */
export function CstTypeIcon({ value, size = '1.25rem', className }: DomIconProps<CstType>) {
switch (value) {
case CstType.BASE:
return <IconCstBaseSet size={size} className={className ?? 'text-ok-600'} />;
case CstType.CONSTANT:
return <IconCstConstSet size={size} className={className ?? 'text-ok-600'} />;
case CstType.STRUCTURED:
return <IconCstStructured size={size} className={className ?? 'text-ok-600'} />;
case CstType.TERM:
return <IconCstTerm size={size} className={className ?? 'text-sec-600'} />;
case CstType.AXIOM:
return <IconCstAxiom size={size} className={className ?? 'text-warn-600'} />;
case CstType.FUNCTION:
return <IconCstFunction size={size} className={className ?? 'text-sec-600'} />;
case CstType.PREDICATE:
return <IconCstPredicate size={size} className={className ?? 'text-warn-600'} />;
case CstType.THEOREM:
return <IconCstTheorem size={size} className={className ?? 'text-warn-600'} />;
}
}
/** Icon for relocation direction. */
export function RelocateUpIcon({ value, size = '1.25rem', className }: DomIconProps<boolean>) {
if (value) {

View File

@ -1,12 +1,11 @@
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 | ZodError | undefined | null;
export type ErrorData = string | Error | AxiosError | undefined | null;
interface InfoErrorProps {
error: ErrorData;
@ -17,13 +16,6 @@ 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'>

View File

@ -4,7 +4,6 @@ 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'> {

View File

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

View File

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

View File

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

View File

@ -2,7 +2,6 @@ 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 {

View File

@ -2,7 +2,6 @@ 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 {

View File

@ -3,7 +3,6 @@ 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';

View File

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

View File

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

View File

@ -2,8 +2,6 @@
import clsx from 'clsx';
import { BadgeHelp } from '@/features/help';
import useEscapeKey from '@/hooks/useEscapeKey';
import { useDialogsStore } from '@/stores/dialogs';
import { PARAMETER } from '@/utils/constants';
@ -11,7 +9,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';

View File

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

View File

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

View File

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

View File

@ -1,17 +1,77 @@
import { queryOptions } from '@tanstack/react-query';
import { z } from 'zod';
import { axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport';
import { DELAYS } from '@/backend/configuration';
import { infoMsg } from '@/utils/labels';
import { errorMsg, infoMsg } from '@/utils/labels';
import {
IChangePasswordDTO,
ICurrentUser,
IPasswordTokenDTO,
IRequestPasswordDTO,
IResetPasswordDTO,
IUserLoginDTO
} from './types';
/**
* 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;
}
/**
* Authentication API.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,10 @@
'use client';
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';
@ -13,10 +12,10 @@ import { TextInput } from '@/components/Input';
import useQueryStrings from '@/hooks/useQueryStrings';
import { resources } from '@/utils/constants';
import { IUserLoginDTO, schemaUserLogin } from '../backend/types';
import { IUserLoginDTO, schemaUserLogin } from '../backend/api';
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();
@ -27,6 +26,7 @@ function LoginPage() {
register,
handleSubmit,
clearErrors,
resetField,
formState: { errors }
} = useForm({
resolver: zodResolver(schemaUserLogin),
@ -38,6 +38,7 @@ function LoginPage() {
function onSubmit(data: IUserLoginDTO) {
return login(data).then(() => {
resetField('password');
if (router.canBack()) {
router.back();
} else {

View File

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

View File

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

View File

@ -3,7 +3,6 @@ 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 {

View File

@ -3,7 +3,6 @@ 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 {

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,11 @@
'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';

View File

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

View File

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

View File

@ -1,11 +1,10 @@
'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;
}

View File

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

View File

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

View File

@ -1,42 +1,145 @@
import { queryOptions } from '@tanstack/react-query';
import {
IRSFormDTO,
IVersionCreatedResponse,
schemaRSForm,
schemaVersionCreatedResponse
} from '@/features/rsform/backend/types';
import { z } from 'zod';
import { axiosDelete, axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport';
import { DELAYS, KEYS } from '@/backend/configuration';
import { infoMsg } from '@/utils/labels';
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 {
AccessPolicy,
ICloneLibraryItemDTO,
ICreateLibraryItemDTO,
ILibraryItem,
IRenameLocationDTO,
IUpdateLibraryItemDTO,
IVersionCreateDTO,
IVersionInfo,
IVersionUpdateDTO,
schemaLibraryItem,
schemaLibraryItemArray,
schemaVersionInfo
} from './types';
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>;
export const libraryApi = {
baseKey: KEYS.library,
libraryListKey: KEYS.composite.libraryList,
baseKey: 'library',
libraryListKey: ['library', 'list'],
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 }
})
@ -47,7 +150,6 @@ export const libraryApi = {
staleTime: DELAYS.staleMedium,
queryFn: meta =>
axiosGet<ILibraryItem[]>({
schema: schemaLibraryItemArray,
endpoint: '/api/library/templates',
options: { signal: meta.signal }
})
@ -55,7 +157,6 @@ export const libraryApi = {
createItem: (data: ICreateLibraryItemDTO) =>
axiosPost<ICreateLibraryItemDTO, ILibraryItem>({
schema: schemaLibraryItem,
endpoint: !data.file ? '/api/library' : '/api/rsforms/create-detailed',
request: {
data: data,
@ -71,14 +172,13 @@ 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: number; owner: number }) =>
setOwner: ({ itemID, owner }: { itemID: LibraryItemID; owner: number }) =>
axiosPatch({
endpoint: `/api/library/${itemID}/set-owner`,
request: {
@ -86,7 +186,7 @@ export const libraryApi = {
successMessage: infoMsg.changesSaved
}
}),
setLocation: ({ itemID, location }: { itemID: number; location: string }) =>
setLocation: ({ itemID, location }: { itemID: LibraryItemID; location: string }) =>
axiosPatch({
endpoint: `/api/library/${itemID}/set-location`,
request: {
@ -94,7 +194,7 @@ export const libraryApi = {
successMessage: infoMsg.moveComplete
}
}),
setAccessPolicy: ({ itemID, policy }: { itemID: number; policy: AccessPolicy }) =>
setAccessPolicy: ({ itemID, policy }: { itemID: LibraryItemID; policy: AccessPolicy }) =>
axiosPatch({
endpoint: `/api/library/${itemID}/set-access-policy`,
request: {
@ -102,7 +202,7 @@ export const libraryApi = {
successMessage: infoMsg.changesSaved
}
}),
setEditors: ({ itemID, editors }: { itemID: number; editors: number[] }) =>
setEditors: ({ itemID, editors }: { itemID: LibraryItemID; editors: number[] }) =>
axiosPatch({
endpoint: `/api/library/${itemID}/set-editors`,
request: {
@ -111,7 +211,7 @@ export const libraryApi = {
}
}),
deleteItem: (target: number) =>
deleteItem: (target: LibraryItemID) =>
axiosDelete({
endpoint: `/api/library/${target}`,
request: {
@ -120,7 +220,6 @@ export const libraryApi = {
}),
cloneItem: (data: ICloneLibraryItemDTO) =>
axiosPost<ICloneLibraryItemDTO, IRSFormDTO>({
schema: schemaRSForm,
endpoint: `/api/library/${data.id}/clone`,
request: {
data: data,
@ -136,33 +235,30 @@ export const libraryApi = {
}
}),
versionCreate: ({ itemID, data }: { itemID: number; data: IVersionCreateDTO }) =>
versionCreate: ({ itemID, data }: { itemID: LibraryItemID; data: IVersionCreateDTO }) =>
axiosPost<IVersionCreateDTO, IVersionCreatedResponse>({
schema: schemaVersionCreatedResponse,
endpoint: `/api/library/${itemID}/create-version`,
request: {
data: data,
successMessage: infoMsg.newVersion(data.version)
}
}),
versionRestore: ({ versionID }: { versionID: number }) =>
versionRestore: ({ versionID }: { versionID: VersionID }) =>
axiosPatch<undefined, IRSFormDTO>({
schema: schemaRSForm,
endpoint: `/api/versions/${versionID}/restore`,
request: {
successMessage: infoMsg.versionRestored
}
}),
versionUpdate: (data: { itemID: number; version: IVersionUpdateDTO }) =>
versionUpdate: (data: IVersionUpdateDTO) =>
axiosPatch<IVersionUpdateDTO, IVersionInfo>({
schema: schemaVersionInfo,
endpoint: `/api/versions/${data.version.id}`,
endpoint: `/api/versions/${data.id}`,
request: {
data: data.version,
data: data,
successMessage: infoMsg.changesSaved
}
}),
versionDelete: (data: { itemID: number; versionID: number }) =>
versionDelete: (data: { itemID: LibraryItemID; versionID: VersionID }) =>
axiosDelete({
endpoint: `/api/versions/${data.versionID}`,
request: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,7 @@
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';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,9 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { KEYS } from '@/backend/configuration';
import { rsformsApi } from '@/features/rsform/backend/api';
import { libraryApi } from './api';
import { IVersionCreateDTO } from './types';
import { LibraryItemID } from '../models/library';
import { IVersionCreateDTO, libraryApi } from './api';
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(KEYS.composite.rsItem({ itemID: data.schema.id }), data.schema);
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.schema.id }).queryKey, data.schema);
updateTimestamp(data.schema.id);
}
});
return {
versionCreate: (data: { itemID: number; data: IVersionCreateDTO }) =>
versionCreate: (data: { itemID: LibraryItemID; data: IVersionCreateDTO }) =>
mutation.mutateAsync(data).then(response => response.version)
};
};

View File

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

View File

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

View File

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

View File

@ -2,8 +2,6 @@ import { Suspense } from 'react';
import { useIntl } from 'react-intl';
import { 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';
@ -18,28 +16,19 @@ 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;
}

View File

@ -1,14 +1,13 @@
'use client';
import { MiniButton } from '@/components/Control';
import { DomIconProps } from '@/components/DomainIcons';
import { PolicyIcon } 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 '../backend/types';
import { describeAccessPolicy, labelAccessPolicy } from '../labels';
import { AccessPolicy } from '../models/library';
interface SelectAccessPolicyProps extends CProps.Styling {
value: AccessPolicy;
@ -18,7 +17,7 @@ interface SelectAccessPolicyProps extends CProps.Styling {
stretchLeft?: boolean;
}
export function SelectAccessPolicy({ value, disabled, stretchLeft, onChange, ...restProps }: SelectAccessPolicyProps) {
function SelectAccessPolicy({ value, disabled, stretchLeft, onChange, ...restProps }: SelectAccessPolicyProps) {
const menu = useDropdown();
function handleChange(newValue: AccessPolicy) {
@ -53,14 +52,4 @@ export function SelectAccessPolicy({ value, disabled, stretchLeft, onChange, ...
);
}
/** 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'} />;
}
}
export default SelectAccessPolicy;

View File

@ -1,14 +1,13 @@
'use client';
import { SelectorButton } from '@/components/Control';
import { DomIconProps } from '@/components/DomainIcons';
import { ItemTypeIcon } 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 '../backend/types';
import { describeLibraryItemType, labelLibraryItemType } from '../labels';
import { LibraryItemType } from '../models/library';
interface SelectItemTypeProps extends CProps.Styling {
value: LibraryItemType;
@ -17,7 +16,7 @@ interface SelectItemTypeProps extends CProps.Styling {
stretchLeft?: boolean;
}
export function SelectItemType({ value, disabled, stretchLeft, onChange, ...restProps }: SelectItemTypeProps) {
function SelectItemType({ value, disabled, stretchLeft, onChange, ...restProps }: SelectItemTypeProps) {
const menu = useDropdown();
function handleChange(newValue: LibraryItemType) {
@ -54,12 +53,4 @@ export function SelectItemType({ value, disabled, stretchLeft, onChange, ...rest
);
}
/** 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'} />;
}
}
export default SelectItemType;

View File

@ -5,7 +5,7 @@ import clsx from 'clsx';
import { SelectSingle } from '@/components/Input';
import { CProps } from '@/components/props';
import { ILibraryItem } from '../backend/types';
import { ILibraryItem, LibraryItemID } from '../models/library';
import { matchLibraryItem } from '../models/libraryAPI';
interface SelectLibraryItemProps extends CProps.Styling {
@ -17,7 +17,7 @@ interface SelectLibraryItemProps extends CProps.Styling {
noBorder?: boolean;
}
export function SelectLibraryItem({
function SelectLibraryItem({
className,
items,
value,
@ -31,9 +31,9 @@ export function SelectLibraryItem({
label: `${cst.alias}: ${cst.title}`
})) ?? [];
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);
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);
}
return (
@ -42,9 +42,12 @@ export 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;

View File

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

View File

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

View File

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

View File

@ -1,20 +1,19 @@
'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';

View File

@ -1,14 +1,13 @@
'use client';
import { useState } from 'react';
import clsx from 'clsx';
import { SelectUser, TableUsers, useUsers } from '@/features/users';
import { useState } from 'react';
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';

View File

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

View File

@ -1,4 +1,3 @@
import { AccessPolicy, LibraryItemType } from './backend/types';
import { FolderNode } from './models/FolderTree';
import { LocationHead } from './models/library';
import { validateLocation } from './models/libraryAPI';
@ -46,52 +45,3 @@ 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 'Операционная схема синтеза';
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,8 +2,6 @@
import clsx from 'clsx';
import { SelectUser } from '@/features/users';
import { MiniButton, SelectorButton } from '@/components/Control';
import { LocationIcon, VisibilityIcon } from '@/components/DomainIcons';
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
@ -16,8 +14,9 @@ import {
IconOwner,
IconUserSearch
} from '@/components/Icons';
import { SearchBar } from '@/components/Input';
import { CProps } from '@/components/props';
import { SearchBar } from '@/components/shared/SearchBar';
import { SelectUser } from '@/features/users';
import { prefixes } from '@/utils/constants';
import { tripleToggleColor } from '@/utils/utils';

View File

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

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