Compare commits
No commits in common. "65c210b0477f372f52f1ac70596996471211e003" and "ff18a22b14f417b426192d8ef5bb13871862b70d" have entirely different histories.
65c210b047
...
ff18a22b14
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
@ -114,7 +114,6 @@
|
|||
"lezer",
|
||||
"Litr",
|
||||
"loct",
|
||||
"mgraph",
|
||||
"moprho",
|
||||
"multiword",
|
||||
"mypy",
|
||||
|
@ -155,8 +154,6 @@
|
|||
"rsforms",
|
||||
"rsgraph",
|
||||
"rslang",
|
||||
"rslist",
|
||||
"rstabs",
|
||||
"rstemplates",
|
||||
"setexpr",
|
||||
"SIDELIST",
|
||||
|
|
2
TODO.txt
2
TODO.txt
|
@ -67,5 +67,3 @@ https://drf-standardized-errors.readthedocs.io/en/latest/error_response.html
|
|||
https://stackoverflow.com/questions/28838170/multilevel-json-diff-in-python
|
||||
|
||||
- Documentation platform. Consider diplodoc
|
||||
|
||||
- nuqs useQueryState
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"generate": "lezer-generator src/components/rs-input/rslang/rslang-fast.grammar -o src/components/rs-input/rslang/parser.ts && lezer-generator src/components/rs-input/rslang/rslang-ast.grammar -o src/components/rs-input/rslang/parserAST.ts && lezer-generator src/components/refs-input/parse/refs-text.grammar -o src/components/refs-input/parse/parser.ts",
|
||||
"generate": "lezer-generator src/components/RSInput/rslang/rslangFast.grammar -o src/components/RSInput/rslang/parser.ts && lezer-generator src/components/RSInput/rslang/rslangAST.grammar -o src/components/RSInput/rslang/parserAST.ts && lezer-generator src/components/RefsInput/parse/refsText.grammar -o src/components/RefsInput/parse/parser.ts",
|
||||
"test": "jest",
|
||||
"test:e2e": "playwright test",
|
||||
"dev": "vite --host",
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
import { Suspense } from 'react';
|
||||
import { Outlet } from 'react-router';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { ModalLoader } from '@/components/modal';
|
||||
import { useAppLayoutStore, useMainHeight, useViewportHeight } from '@/stores/app-layout';
|
||||
import { ModalLoader } from '@/components/Modal';
|
||||
import { useAppLayoutStore, useMainHeight, useViewportHeight } from '@/stores/appLayout';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
|
||||
import { NavigationState } from './navigation/navigation-context';
|
||||
import { Footer } from './footer';
|
||||
import { GlobalDialogs } from './global-dialogs';
|
||||
import { GlobalLoader } from './global-Loader';
|
||||
import { ToasterThemed } from './global-toaster';
|
||||
import { GlobalTooltips } from './global-tooltips';
|
||||
import { MutationErrors } from './mutation-errors';
|
||||
import { Navigation } from './navigation';
|
||||
import { NavigationState } from './Navigation/NavigationContext';
|
||||
import { Footer } from './Footer';
|
||||
import { GlobalDialogs } from './GlobalDialogs';
|
||||
import { GlobalLoader } from './GlobalLoader';
|
||||
import { ToasterThemed } from './GlobalToaster';
|
||||
import { GlobalTooltips } from './GlobalTooltips';
|
||||
import { MutationErrors } from './MutationErrors';
|
||||
import { Navigation } from './Navigation';
|
||||
|
||||
export function ApplicationLayout() {
|
||||
const mainHeight = useMainHeight();
|
||||
const viewportHeight = useViewportHeight();
|
||||
const showScroll = useAppLayoutStore(state => !state.noScroll);
|
||||
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
|
||||
const noNavigation = useAppLayoutStore(state => state.noNavigation);
|
||||
const noFooter = useAppLayoutStore(state => state.noFooter);
|
||||
|
@ -27,7 +27,8 @@ export function ApplicationLayout() {
|
|||
<NavigationState>
|
||||
<div className='min-w-80 antialiased h-full max-w-480 mx-auto'>
|
||||
<ToasterThemed
|
||||
className={clsx('sm:text-[14px]/[20px] text-[12px]/[16px]', noNavigationAnimation ? 'mt-6' : 'mt-14')}
|
||||
className='text-[14px]'
|
||||
style={{ marginTop: noNavigationAnimation ? '1.5rem' : '3.5rem' }}
|
||||
autoClose={3000}
|
||||
draggable={false}
|
||||
pauseOnFocusLoss={false}
|
||||
|
@ -45,7 +46,7 @@ export function ApplicationLayout() {
|
|||
style={{ maxHeight: viewportHeight }}
|
||||
inert={activeDialog !== null}
|
||||
>
|
||||
<main className='cc-scroll-y overflow-y-auto' style={{ minHeight: mainHeight }}>
|
||||
<main className='cc-scroll-y' style={{ overflowY: showScroll ? 'scroll' : 'auto', minHeight: mainHeight }}>
|
||||
<GlobalLoader />
|
||||
<MutationErrors />
|
||||
<Outlet />
|
|
@ -1,7 +1,7 @@
|
|||
import { useNavigate, useRouteError } from 'react-router';
|
||||
|
||||
import { Button } from '@/components/control';
|
||||
import { InfoError } from '@/components/info-error';
|
||||
import { Button } from '@/components/Control';
|
||||
import { InfoError } from '@/components/InfoError';
|
||||
|
||||
export function ErrorFallback() {
|
||||
const error = useRouteError();
|
|
@ -1,6 +1,6 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { TextURL } from '@/components/control';
|
||||
import { TextURL } from '@/components/Control';
|
||||
import { external_urls } from '@/utils/constants';
|
||||
|
||||
export function Footer() {
|
|
@ -5,113 +5,113 @@ import React from 'react';
|
|||
import { DialogType, useDialogsStore } from '@/stores/dialogs';
|
||||
|
||||
const DlgChangeInputSchema = React.lazy(() =>
|
||||
import('@/features/oss/dialogs/dlg-change-input-schema').then(module => ({ default: module.DlgChangeInputSchema }))
|
||||
import('@/features/oss/dialogs/DlgChangeInputSchema').then(module => ({ default: module.DlgChangeInputSchema }))
|
||||
);
|
||||
const DlgChangeLocation = React.lazy(() =>
|
||||
import('@/features/library/dialogs/dlg-change-location').then(module => ({
|
||||
import('@/features/library/dialogs/DlgChangeLocation').then(module => ({
|
||||
default: module.DlgChangeLocation
|
||||
}))
|
||||
);
|
||||
const DlgCloneLibraryItem = React.lazy(() =>
|
||||
import('@/features/library/dialogs/dlg-clone-library-item').then(module => ({
|
||||
import('@/features/library/dialogs/DlgCloneLibraryItem').then(module => ({
|
||||
default: module.DlgCloneLibraryItem
|
||||
}))
|
||||
);
|
||||
const DlgCreateCst = React.lazy(() =>
|
||||
import('@/features/rsform/dialogs/dlg-create-cst').then(module => ({ default: module.DlgCreateCst }))
|
||||
import('@/features/rsform/dialogs/DlgCreateCst').then(module => ({ default: module.DlgCreateCst }))
|
||||
);
|
||||
const DlgCreateOperation = React.lazy(() =>
|
||||
import('@/features/oss/dialogs/dlg-create-operation').then(module => ({
|
||||
import('@/features/oss/dialogs/DlgCreateOperation').then(module => ({
|
||||
default: module.DlgCreateOperation
|
||||
}))
|
||||
);
|
||||
const DlgCreateVersion = React.lazy(() =>
|
||||
import('@/features/library/dialogs/dlg-create-version').then(module => ({
|
||||
import('@/features/library/dialogs/DlgCreateVersion').then(module => ({
|
||||
default: module.DlgCreateVersion
|
||||
}))
|
||||
);
|
||||
const DlgCstTemplate = React.lazy(() =>
|
||||
import('@/features/rsform/dialogs/dlg-cst-template').then(module => ({
|
||||
import('@/features/rsform/dialogs/DlgCstTemplate').then(module => ({
|
||||
default: module.DlgCstTemplate
|
||||
}))
|
||||
);
|
||||
const DlgDeleteCst = React.lazy(() =>
|
||||
import('@/features/rsform/dialogs/dlg-delete-cst').then(module => ({
|
||||
import('@/features/rsform/dialogs/DlgDeleteCst').then(module => ({
|
||||
default: module.DlgDeleteCst
|
||||
}))
|
||||
);
|
||||
const DlgDeleteOperation = React.lazy(() =>
|
||||
import('@/features/oss/dialogs/dlg-delete-operation').then(module => ({
|
||||
import('@/features/oss/dialogs/DlgDeleteOperation').then(module => ({
|
||||
default: module.DlgDeleteOperation
|
||||
}))
|
||||
);
|
||||
const DlgEditEditors = React.lazy(() =>
|
||||
import('@/features/library/dialogs/dlg-edit-editors').then(module => ({
|
||||
import('@/features/library/dialogs/DlgEditEditors').then(module => ({
|
||||
default: module.DlgEditEditors
|
||||
}))
|
||||
);
|
||||
const DlgEditOperation = React.lazy(() =>
|
||||
import('@/features/oss/dialogs/dlg-edit-operation').then(module => ({
|
||||
import('@/features/oss/dialogs/DlgEditOperation').then(module => ({
|
||||
default: module.DlgEditOperation
|
||||
}))
|
||||
);
|
||||
const DlgEditReference = React.lazy(() =>
|
||||
import('@/features/rsform/dialogs/dlg-edit-reference').then(module => ({
|
||||
import('@/features/rsform/dialogs/DlgEditReference').then(module => ({
|
||||
default: module.DlgEditReference
|
||||
}))
|
||||
);
|
||||
const DlgEditVersions = React.lazy(() =>
|
||||
import('@/features/library/dialogs/dlg-edit-versions').then(module => ({
|
||||
import('@/features/library/dialogs/DlgEditVersions').then(module => ({
|
||||
default: module.DlgEditVersions
|
||||
}))
|
||||
);
|
||||
const DlgEditWordForms = React.lazy(() =>
|
||||
import('@/features/rsform/dialogs/dlg-edit-word-forms').then(module => ({
|
||||
import('@/features/rsform/dialogs/DlgEditWordForms').then(module => ({
|
||||
default: module.DlgEditWordForms
|
||||
}))
|
||||
);
|
||||
const DlgInlineSynthesis = React.lazy(() =>
|
||||
import('@/features/rsform/dialogs/dlg-inline-synthesis').then(module => ({
|
||||
import('@/features/rsform/dialogs/DlgInlineSynthesis').then(module => ({
|
||||
default: module.DlgInlineSynthesis
|
||||
}))
|
||||
);
|
||||
const DlgRelocateConstituents = React.lazy(() =>
|
||||
import('@/features/oss/dialogs/dlg-relocate-constituents').then(module => ({
|
||||
import('@/features/oss/dialogs/DlgRelocateConstituents').then(module => ({
|
||||
default: module.DlgRelocateConstituents
|
||||
}))
|
||||
);
|
||||
const DlgRenameCst = React.lazy(() =>
|
||||
import('@/features/rsform/dialogs/dlg-rename-cst').then(module => ({
|
||||
import('@/features/rsform/dialogs/DlgRenameCst').then(module => ({
|
||||
default: module.DlgRenameCst
|
||||
}))
|
||||
);
|
||||
const DlgShowAST = React.lazy(() =>
|
||||
import('@/features/rsform/dialogs/dlg-show-ast').then(module => ({
|
||||
import('@/features/rsform/dialogs/DlgShowAST').then(module => ({
|
||||
default: module.DlgShowAST
|
||||
}))
|
||||
);
|
||||
const DlgShowQR = React.lazy(() =>
|
||||
import('@/features/rsform/dialogs/dlg-show-qr').then(module => ({
|
||||
import('@/features/rsform/dialogs/DlgShowQR').then(module => ({
|
||||
default: module.DlgShowQR
|
||||
}))
|
||||
);
|
||||
const DlgShowTypeGraph = React.lazy(() =>
|
||||
import('@/features/rsform/dialogs/dlg-show-type-graph').then(module => ({
|
||||
import('@/features/rsform/dialogs/DlgShowTypeGraph').then(module => ({
|
||||
default: module.DlgShowTypeGraph
|
||||
}))
|
||||
);
|
||||
const DlgSubstituteCst = React.lazy(() =>
|
||||
import('@/features/rsform/dialogs/dlg-substitute-cst').then(module => ({
|
||||
import('@/features/rsform/dialogs/DlgSubstituteCst').then(module => ({
|
||||
default: module.DlgSubstituteCst
|
||||
}))
|
||||
);
|
||||
const DlgUploadRSForm = React.lazy(() =>
|
||||
import('@/features/rsform/dialogs/dlg-upload-rsform').then(module => ({
|
||||
import('@/features/rsform/dialogs/DlgUploadRSForm').then(module => ({
|
||||
default: module.DlgUploadRSForm
|
||||
}))
|
||||
);
|
||||
const DlgGraphParams = React.lazy(() =>
|
||||
import('@/features/rsform/dialogs/dlg-graph-params').then(module => ({ default: module.DlgGraphParams }))
|
||||
import('@/features/rsform/dialogs/DlgGraphParams').then(module => ({ default: module.DlgGraphParams }))
|
||||
);
|
||||
|
||||
export const GlobalDialogs = () => {
|
|
@ -1,8 +1,8 @@
|
|||
import { useNavigation } from 'react-router';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
|
||||
import { Loader } from '@/components/loader';
|
||||
import { ModalBackdrop } from '@/components/modal/modal-backdrop';
|
||||
import { Loader } from '@/components/Loader';
|
||||
import { ModalBackdrop } from '@/components/Modal/ModalBackdrop';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
||||
export function GlobalLoader() {
|
||||
|
@ -18,7 +18,7 @@ export function GlobalLoader() {
|
|||
return (
|
||||
<div className='cc-modal-wrapper'>
|
||||
<ModalBackdrop />
|
||||
<div className='z-pop cc-fade-in px-10 border rounded-xl bg-prim-100'>
|
||||
<div className='cc-fade-in px-10 border rounded-xl bg-prim-100'>
|
||||
<Loader scale={6} />
|
||||
</div>
|
||||
</div>
|
|
@ -4,7 +4,7 @@ import { IntlProvider } from 'react-intl';
|
|||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
|
||||
import { queryClient } from '@/backend/query-client';
|
||||
import { queryClient } from '@/backend/queryClient';
|
||||
|
||||
// prettier-ignore
|
||||
export function GlobalProviders({ children }: React.PropsWithChildren) {
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { Tooltip } from '@/components/container';
|
||||
import { Tooltip } from '@/components/Container';
|
||||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
export const GlobalTooltips = () => {
|
|
@ -1,8 +1,8 @@
|
|||
import { useMutationErrors } from '@/backend/use-mutation-errors';
|
||||
import { Button } from '@/components/control';
|
||||
import { DescribeError } from '@/components/info-error';
|
||||
import { ModalBackdrop } from '@/components/modal/modal-backdrop';
|
||||
import { useEscapeKey } from '@/hooks/use-escape-key';
|
||||
import { useMutationErrors } from '@/backend/useMutationErrors';
|
||||
import { Button } from '@/components/Control';
|
||||
import { DescribeError } from '@/components/InfoError';
|
||||
import { ModalBackdrop } from '@/components/Modal/ModalBackdrop';
|
||||
import { useEscapeKey } from '@/hooks/useEscapeKey';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
|
||||
export function MutationErrors() {
|
||||
|
@ -20,7 +20,7 @@ export function MutationErrors() {
|
|||
return (
|
||||
<div className='cc-modal-wrapper'>
|
||||
<ModalBackdrop onHide={resetErrors} />
|
||||
<div className='z-pop px-10 py-3 flex flex-col items-center border rounded-xl bg-prim-100' role='alertdialog'>
|
||||
<div className='px-10 py-3 flex flex-col items-center border rounded-xl bg-prim-100' role='alertdialog'>
|
||||
<h1 className='py-2 select-none'>Ошибка при обработке</h1>
|
||||
<div className='px-3 flex flex-col text-warn-600 text-sm font-semibold select-text'>
|
||||
<DescribeError error={mutationErrors[0]} />
|
|
@ -1,4 +1,4 @@
|
|||
import { useWindowSize } from '@/hooks/use-window-size';
|
||||
import { useWindowSize } from '@/hooks/useWindowSize';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
|
||||
export function Logo() {
|
|
@ -1,16 +1,14 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { IconLibrary2, IconManuals, IconNewItem2 } from '@/components/icons';
|
||||
import { useWindowSize } from '@/hooks/use-window-size';
|
||||
import { useAppLayoutStore } from '@/stores/app-layout';
|
||||
import { IconLibrary2, IconManuals, IconNewItem2 } from '@/components/Icons';
|
||||
import { useWindowSize } from '@/hooks/useWindowSize';
|
||||
import { useAppLayoutStore } from '@/stores/appLayout';
|
||||
|
||||
import { urls } from '../urls';
|
||||
|
||||
import { Logo } from './logo';
|
||||
import { NavigationButton } from './navigation-button';
|
||||
import { useConceptNavigation } from './navigation-context';
|
||||
import { ToggleNavigation } from './toggle-navigation';
|
||||
import { UserMenu } from './user-menu';
|
||||
import { Logo } from './Logo';
|
||||
import { NavigationButton } from './NavigationButton';
|
||||
import { useConceptNavigation } from './NavigationContext';
|
||||
import { ToggleNavigation } from './ToggleNavigation';
|
||||
import { UserMenu } from './UserMenu';
|
||||
|
||||
export function Navigation() {
|
||||
const router = useConceptNavigation();
|
||||
|
@ -30,11 +28,11 @@ export function Navigation() {
|
|||
<nav className='z-navigation sticky top-0 left-0 right-0 select-none bg-prim-100'>
|
||||
<ToggleNavigation />
|
||||
<div
|
||||
className={clsx(
|
||||
'pl-2 pr-6 sm:pr-4 h-12 flex cc-shadow-border',
|
||||
'transition-[max-height,translate] ease-bezier duration-(--duration-move)',
|
||||
noNavigationAnimation ? '-translate-y-6 max-h-0' : 'max-h-12'
|
||||
)}
|
||||
className='pl-2 pr-6 sm:pr-4 h-12 flex cc-shadow-border'
|
||||
style={{
|
||||
maxHeight: noNavigationAnimation ? '0rem' : '3rem',
|
||||
translate: noNavigationAnimation ? '0 -1.5rem' : '0'
|
||||
}}
|
||||
>
|
||||
<div className='flex items-center mr-auto cursor-pointer' onClick={!size.isSmall ? navigateHome : undefined}>
|
||||
<Logo />
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { createContext, use, useEffect, useRef, useState } from 'react';
|
||||
import { createContext, use, useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
export interface NavigationProps {
|
||||
|
@ -19,7 +19,8 @@ interface INavigationContext {
|
|||
|
||||
canBack: () => boolean;
|
||||
|
||||
setRequireConfirmation: (value: boolean) => void;
|
||||
isBlocked: boolean;
|
||||
setIsBlocked: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export const NavigationContext = createContext<INavigationContext | null>(null);
|
||||
|
@ -34,11 +35,11 @@ export const useConceptNavigation = () => {
|
|||
export const NavigationState = ({ children }: React.PropsWithChildren) => {
|
||||
const router = useNavigate();
|
||||
|
||||
const isBlocked = useRef(false);
|
||||
const [isBlocked, setIsBlocked] = useState(false);
|
||||
const [internalNavigation, setInternalNavigation] = useState(false);
|
||||
|
||||
function validate() {
|
||||
return !isBlocked.current || confirm('Изменения не сохранены. Вы уверены что хотите совершить переход?');
|
||||
return !isBlocked || confirm('Изменения не сохранены. Вы уверены что хотите совершить переход?');
|
||||
}
|
||||
|
||||
function canBack() {
|
||||
|
@ -49,7 +50,7 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
|
|||
if (props.newTab) {
|
||||
window.open(`${props.path}`, '_blank');
|
||||
} else if (props.force || validate()) {
|
||||
isBlocked.current = false;
|
||||
setIsBlocked(false);
|
||||
setInternalNavigation(true);
|
||||
Promise.resolve(router(props.path, { viewTransition: true })).catch(console.error);
|
||||
}
|
||||
|
@ -59,7 +60,7 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
|
|||
if (props.newTab) {
|
||||
window.open(`${props.path}`, '_blank');
|
||||
} else if (props.force || validate()) {
|
||||
isBlocked.current = false;
|
||||
setIsBlocked(false);
|
||||
setInternalNavigation(true);
|
||||
return router(props.path, { viewTransition: true });
|
||||
}
|
||||
|
@ -67,14 +68,14 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
|
|||
|
||||
function replace(props: Omit<NavigationProps, 'newTab'>) {
|
||||
if (props.force || validate()) {
|
||||
isBlocked.current = false;
|
||||
setIsBlocked(false);
|
||||
Promise.resolve(router(props.path, { replace: true, viewTransition: true })).catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
function replaceAsync(props: Omit<NavigationProps, 'newTab'>): void | Promise<void> {
|
||||
if (props.force || validate()) {
|
||||
isBlocked.current = false;
|
||||
setIsBlocked(false);
|
||||
return router(props.path, { replace: true, viewTransition: true });
|
||||
}
|
||||
}
|
||||
|
@ -82,14 +83,14 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
|
|||
function back(force?: boolean) {
|
||||
if (force || validate()) {
|
||||
Promise.resolve(router(-1)).catch(console.error);
|
||||
isBlocked.current = false;
|
||||
setIsBlocked(false);
|
||||
}
|
||||
}
|
||||
|
||||
function forward(force?: boolean) {
|
||||
if (force || validate()) {
|
||||
Promise.resolve(router(1)).catch(console.error);
|
||||
isBlocked.current = false;
|
||||
setIsBlocked(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -103,7 +104,8 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
|
|||
back,
|
||||
forward,
|
||||
canBack,
|
||||
setRequireConfirmation: (value: boolean) => (isBlocked.current = value)
|
||||
isBlocked,
|
||||
setIsBlocked
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
@ -112,9 +114,9 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
|
|||
};
|
||||
|
||||
export function useBlockNavigation(isBlocked: boolean) {
|
||||
const { setRequireConfirmation } = useConceptNavigation();
|
||||
const router = useConceptNavigation();
|
||||
useEffect(() => {
|
||||
setRequireConfirmation(isBlocked);
|
||||
return () => setRequireConfirmation(false);
|
||||
}, [setRequireConfirmation, isBlocked]);
|
||||
router.setIsBlocked(isBlocked);
|
||||
return () => router.setIsBlocked(false);
|
||||
}, [router, isBlocked]);
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { IconDarkTheme, IconLightTheme, IconPin, IconUnpin } from '@/components/icons';
|
||||
import { useAppLayoutStore } from '@/stores/app-layout';
|
||||
import { IconDarkTheme, IconLightTheme, IconPin, IconUnpin } from '@/components/Icons';
|
||||
import { useAppLayoutStore } from '@/stores/appLayout';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { globalIDs } from '@/utils/constants';
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import { useAuthSuspense } from '@/features/auth';
|
||||
|
||||
import { IconLogin, IconUser2 } from '@/components/icons';
|
||||
import { IconLogin, IconUser2 } from '@/components/Icons';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
import { NavigationButton } from './navigation-button';
|
||||
import { NavigationButton } from './NavigationButton';
|
||||
|
||||
interface UserButtonProps {
|
||||
onLogin: () => void;
|
|
@ -1,7 +1,7 @@
|
|||
import { useAuthSuspense } from '@/features/auth';
|
||||
import { useLogout } from '@/features/auth/backend/use-logout';
|
||||
import { useLogout } from '@/features/auth/backend/useLogout';
|
||||
|
||||
import { Dropdown, DropdownButton } from '@/components/dropdown';
|
||||
import { Dropdown, DropdownButton } from '@/components/Dropdown';
|
||||
import {
|
||||
IconAdmin,
|
||||
IconAdminOff,
|
||||
|
@ -15,13 +15,13 @@ import {
|
|||
IconLogout,
|
||||
IconRESTapi,
|
||||
IconUser
|
||||
} from '@/components/icons';
|
||||
} from '@/components/Icons';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
import { urls } from '../urls';
|
||||
|
||||
import { useConceptNavigation } from './navigation-context';
|
||||
import { useConceptNavigation } from './NavigationContext';
|
||||
|
||||
interface UserDropdownProps {
|
||||
isOpen: boolean;
|
|
@ -1,13 +1,13 @@
|
|||
import { Suspense } from 'react';
|
||||
|
||||
import { useDropdown } from '@/components/dropdown';
|
||||
import { Loader } from '@/components/loader';
|
||||
import { useDropdown } from '@/components/Dropdown';
|
||||
import { Loader } from '@/components/Loader';
|
||||
|
||||
import { urls } from '../urls';
|
||||
|
||||
import { useConceptNavigation } from './navigation-context';
|
||||
import { UserButton } from './user-button';
|
||||
import { UserDropdown } from './user-dropdown';
|
||||
import { useConceptNavigation } from './NavigationContext';
|
||||
import { UserButton } from './UserButton';
|
||||
import { UserDropdown } from './UserDropdown';
|
||||
|
||||
export function UserMenu() {
|
||||
const router = useConceptNavigation();
|
1
rsconcept/frontend/src/app/Navigation/index.tsx
Normal file
1
rsconcept/frontend/src/app/Navigation/index.tsx
Normal file
|
@ -0,0 +1 @@
|
|||
export { Navigation } from './Navigation';
|
|
@ -1,20 +1,20 @@
|
|||
import { createBrowserRouter } from 'react-router';
|
||||
|
||||
import { prefetchAuth } from '@/features/auth/backend/use-auth';
|
||||
import { LoginPage } from '@/features/auth/pages/login-page';
|
||||
import { HomePage } from '@/features/home/home-page';
|
||||
import { NotFoundPage } from '@/features/home/not-found-page';
|
||||
import { prefetchLibrary } from '@/features/library/backend/use-library';
|
||||
import { CreateItemPage } from '@/features/library/pages/create-item-page';
|
||||
import { prefetchOSS } from '@/features/oss/backend/use-oss';
|
||||
import { prefetchRSForm } from '@/features/rsform/backend/use-rsform';
|
||||
import { prefetchProfile } from '@/features/users/backend/use-profile';
|
||||
import { prefetchUsers } from '@/features/users/backend/use-users';
|
||||
import { prefetchAuth } from '@/features/auth/backend/useAuth';
|
||||
import { LoginPage } from '@/features/auth/pages/LoginPage';
|
||||
import { HomePage } from '@/features/home/HomePage';
|
||||
import { NotFoundPage } from '@/features/home/NotFoundPage';
|
||||
import { prefetchLibrary } from '@/features/library/backend/useLibrary';
|
||||
import { CreateItemPage } from '@/features/library/pages/CreateItemPage';
|
||||
import { prefetchOSS } from '@/features/oss/backend/useOSS';
|
||||
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 { Loader } from '@/components/Loader';
|
||||
|
||||
import { ApplicationLayout } from './application-layout';
|
||||
import { ErrorFallback } from './error-fallback';
|
||||
import { ApplicationLayout } from './ApplicationLayout';
|
||||
import { ErrorFallback } from './ErrorFallback';
|
||||
import { routes } from './urls';
|
||||
|
||||
export const Router = createBrowserRouter([
|
||||
|
@ -39,25 +39,25 @@ export const Router = createBrowserRouter([
|
|||
},
|
||||
{
|
||||
path: routes.signup,
|
||||
lazy: () => import('@/features/users/pages/register-page')
|
||||
lazy: () => import('@/features/users/pages/RegisterPage')
|
||||
},
|
||||
{
|
||||
path: routes.profile,
|
||||
loader: prefetchProfile,
|
||||
lazy: () => import('@/features/users/pages/user-profile-page')
|
||||
lazy: () => import('@/features/users/pages/UserProfilePage')
|
||||
},
|
||||
{
|
||||
path: routes.restore_password,
|
||||
lazy: () => import('@/features/auth/pages/restore-password-page')
|
||||
lazy: () => import('@/features/auth/pages/RestorePasswordPage')
|
||||
},
|
||||
{
|
||||
path: routes.password_change,
|
||||
lazy: () => import('@/features/auth/pages/password-change-page')
|
||||
lazy: () => import('@/features/auth/pages/PasswordChangePage')
|
||||
},
|
||||
{
|
||||
path: routes.library,
|
||||
loader: () => Promise.allSettled([prefetchLibrary(), prefetchUsers()]),
|
||||
lazy: () => import('@/features/library/pages/library-page')
|
||||
lazy: () => import('@/features/library/pages/LibraryPage')
|
||||
},
|
||||
{
|
||||
path: routes.create_schema,
|
||||
|
@ -66,24 +66,24 @@ export const Router = createBrowserRouter([
|
|||
{
|
||||
path: `${routes.rsforms}/:id`,
|
||||
loader: data => prefetchRSForm(parseRSFormURL(data.params.id, data.request.url)),
|
||||
lazy: () => import('@/features/rsform/pages/rsform-page')
|
||||
lazy: () => import('@/features/rsform/pages/RSFormPage')
|
||||
},
|
||||
{
|
||||
path: `${routes.oss}/:id`,
|
||||
loader: data => prefetchOSS(parseOssURL(data.params.id)),
|
||||
lazy: () => import('@/features/oss/pages/oss-page')
|
||||
lazy: () => import('@/features/oss/pages/OssPage')
|
||||
},
|
||||
{
|
||||
path: routes.manuals,
|
||||
lazy: () => import('@/features/help/pages/manuals-page')
|
||||
lazy: () => import('@/features/help/pages/ManualsPage')
|
||||
},
|
||||
{
|
||||
path: `${routes.icons}`,
|
||||
lazy: () => import('@/features/home/icons-page')
|
||||
lazy: () => import('@/features/home/IconsPage')
|
||||
},
|
||||
{
|
||||
path: `${routes.database_schema}`,
|
||||
lazy: () => import('@/features/home/database-schema-page')
|
||||
lazy: () => import('@/features/home/DatabaseSchemaPage')
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
export { useConceptNavigation } from './navigation/navigation-context';
|
||||
export { useBlockNavigation } from './navigation/navigation-context';
|
||||
export { useConceptNavigation } from './Navigation/NavigationContext';
|
||||
export { useBlockNavigation } from './Navigation/NavigationContext';
|
||||
export { urls } from './urls';
|
||||
import { RouterProvider } from 'react-router';
|
||||
|
||||
import { Router } from './router';
|
||||
import { Router } from './Router';
|
||||
|
||||
export function App() {
|
||||
return <RouterProvider router={Router} />;
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export { Navigation } from './navigation';
|
|
@ -2,7 +2,7 @@
|
|||
* Module: Internal navigation constants.
|
||||
*/
|
||||
|
||||
import { buildConstants } from '@/utils/build-constants';
|
||||
import { buildConstants } from '@/utils/buildConstants';
|
||||
|
||||
/**
|
||||
* Routes.
|
||||
|
|
|
@ -5,7 +5,7 @@ import { toast } from 'react-toastify';
|
|||
import axios, { type AxiosError, type AxiosRequestConfig } from 'axios';
|
||||
import { type z, ZodError } from 'zod';
|
||||
|
||||
import { buildConstants } from '@/utils/build-constants';
|
||||
import { buildConstants } from '@/utils/buildConstants';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { errorMsg } from '@/utils/labels';
|
||||
import { extractErrorMessage } from '@/utils/utils';
|
|
@ -1,7 +1,7 @@
|
|||
import { QueryClient } from '@tanstack/react-query';
|
||||
import { type ZodError } from 'zod';
|
||||
|
||||
import { type AxiosError } from './api-transport';
|
||||
import { type AxiosError } from './apiTransport';
|
||||
import { DELAYS } from './configuration';
|
||||
|
||||
declare module '@tanstack/react-query' {
|
|
@ -17,9 +17,12 @@ export function Divider({ vertical, margins = 'mx-2', className, ...restProps }:
|
|||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
vertical ? 'border-x' : 'border-y', //
|
||||
margins,
|
||||
className
|
||||
margins, //
|
||||
className,
|
||||
{
|
||||
'border-x': vertical,
|
||||
'border-y': !vertical
|
||||
}
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
|
@ -26,6 +26,7 @@ export function Tooltip({
|
|||
layer = 'z-tooltip',
|
||||
place = 'bottom',
|
||||
className,
|
||||
style,
|
||||
...restProps
|
||||
}: TooltipProps) {
|
||||
const darkMode = usePreferencesStore(state => state.darkMode);
|
||||
|
@ -39,7 +40,6 @@ export function Tooltip({
|
|||
opacity={1}
|
||||
className={clsx(
|
||||
'relative',
|
||||
'py-0.5! px-2!',
|
||||
'max-h-[calc(100svh-6rem)]',
|
||||
'overflow-y-auto overflow-x-hidden sm:overflow-hidden overscroll-contain',
|
||||
'border shadow-md',
|
||||
|
@ -48,6 +48,7 @@ export function Tooltip({
|
|||
className
|
||||
)}
|
||||
classNameArrow={layer}
|
||||
style={{ ...{ paddingTop: '2px', paddingBottom: '2px', paddingLeft: '8px', paddingRight: '8px' }, ...style }}
|
||||
variant={darkMode ? 'dark' : 'light'}
|
||||
place={place}
|
||||
{...restProps}
|
2
rsconcept/frontend/src/components/Container/index.tsx
Normal file
2
rsconcept/frontend/src/components/Container/index.tsx
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { Divider } from './Divider';
|
||||
export { type PlacesType, Tooltip } from './Tooltip';
|
|
@ -43,10 +43,15 @@ export function Button({
|
|||
'inline-flex gap-2 items-center justify-center',
|
||||
'font-medium select-none disabled:cursor-auto',
|
||||
'clr-btn-default cc-animate-color',
|
||||
dense ? 'px-1' : 'px-3 py-1',
|
||||
loading ? 'cursor-progress' : 'cursor-pointer',
|
||||
noOutline ? 'outline-hidden' : 'clr-outline',
|
||||
!noBorder && 'border rounded-sm',
|
||||
{
|
||||
'border rounded-sm': !noBorder,
|
||||
'px-1': dense,
|
||||
'px-3 py-1': !dense,
|
||||
'cursor-progress': loading,
|
||||
'cursor-pointer': !loading,
|
||||
'outline-hidden': noOutline,
|
||||
'clr-outline': !noOutline
|
||||
},
|
||||
className
|
||||
)}
|
||||
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
|
@ -41,8 +41,11 @@ export function MiniButton({
|
|||
'rounded-lg',
|
||||
'clr-text-controls cc-animate-color',
|
||||
'cursor-pointer disabled:cursor-auto',
|
||||
noHover ? 'outline-hidden' : 'clr-hover',
|
||||
!noPadding && 'px-1 py-1',
|
||||
{
|
||||
'px-1 py-1': !noPadding,
|
||||
'outline-hidden': noHover,
|
||||
'clr-hover': !noHover
|
||||
},
|
||||
className
|
||||
)}
|
||||
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
|
@ -38,7 +38,10 @@ export function SelectorButton({
|
|||
'text-btn clr-text-controls',
|
||||
'disabled:cursor-auto cursor-pointer',
|
||||
'cc-animate-color',
|
||||
transparent ? 'clr-hover' : 'clr-btn-default border',
|
||||
{
|
||||
'clr-hover': transparent,
|
||||
'clr-btn-default border': !transparent
|
||||
},
|
||||
className
|
||||
)}
|
||||
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
5
rsconcept/frontend/src/components/Control/index.tsx
Normal file
5
rsconcept/frontend/src/components/Control/index.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
export { Button } from './Button';
|
||||
export { MiniButton } from './MiniButton';
|
||||
export { SelectorButton } from './SelectorButton';
|
||||
export { SubmitButton } from './SubmitButton';
|
||||
export { TextURL } from './TextURL';
|
|
@ -5,21 +5,25 @@ import { useMemo, useState } from 'react';
|
|||
import {
|
||||
type ColumnSort,
|
||||
createColumnHelper,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
type PaginationState,
|
||||
type RowData,
|
||||
type RowSelectionState,
|
||||
type SortingState,
|
||||
type TableOptions,
|
||||
useReactTable,
|
||||
type VisibilityState
|
||||
} from '@tanstack/react-table';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { type Styling } from '../props';
|
||||
|
||||
import { DefaultNoData } from './default-no-data';
|
||||
import { PaginationTools } from './pagination-tools';
|
||||
import { TableBody } from './table-body';
|
||||
import { TableFooter } from './table-footer';
|
||||
import { TableHeader } from './table-header';
|
||||
import { useDataTable } from './use-data-table';
|
||||
import { DefaultNoData } from './DefaultNoData';
|
||||
import { PaginationTools } from './PaginationTools';
|
||||
import { TableBody } from './TableBody';
|
||||
import { TableFooter } from './TableFooter';
|
||||
import { TableHeader } from './TableHeader';
|
||||
|
||||
export { type ColumnSort, createColumnHelper, type RowSelectionState, type VisibilityState };
|
||||
|
||||
|
@ -120,16 +124,49 @@ export function DataTable<TData extends RowData>({
|
|||
onRowDoubleClicked,
|
||||
noDataComponent,
|
||||
|
||||
paginationPerPage,
|
||||
enableRowSelection,
|
||||
rowSelection,
|
||||
|
||||
enableHiding,
|
||||
columnVisibility,
|
||||
|
||||
enableSorting,
|
||||
initialSorting,
|
||||
|
||||
enablePagination,
|
||||
paginationPerPage = 10,
|
||||
paginationOptions = [10, 20, 30, 40, 50],
|
||||
onChangePaginationOption,
|
||||
|
||||
...restProps
|
||||
}: DataTableProps<TData>) {
|
||||
const [sorting, setSorting] = useState<SortingState>(initialSorting ? [initialSorting] : []);
|
||||
const [lastSelected, setLastSelected] = useState<string | null>(null);
|
||||
|
||||
const table = useDataTable({ paginationPerPage, ...restProps });
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: paginationPerPage
|
||||
});
|
||||
|
||||
const isEmpty = table.getRowModel().rows.length === 0;
|
||||
const tableImpl = useReactTable({
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: enableSorting ? getSortedRowModel() : undefined,
|
||||
getPaginationRowModel: enablePagination ? getPaginationRowModel() : undefined,
|
||||
|
||||
state: {
|
||||
pagination: pagination,
|
||||
sorting: sorting,
|
||||
rowSelection: rowSelection ?? {},
|
||||
columnVisibility: columnVisibility ?? {}
|
||||
},
|
||||
enableHiding: enableHiding,
|
||||
onPaginationChange: enablePagination ? setPagination : undefined,
|
||||
onSortingChange: enableSorting ? setSorting : undefined,
|
||||
enableMultiRowSelection: enableRowSelection,
|
||||
...restProps
|
||||
});
|
||||
|
||||
const isEmpty = tableImpl.getRowModel().rows.length === 0;
|
||||
|
||||
const fixedSize = useMemo(() => {
|
||||
if (!rows) {
|
||||
|
@ -143,46 +180,49 @@ export function DataTable<TData extends RowData>({
|
|||
}, [rows, dense, noHeader, contentHeight]);
|
||||
|
||||
const columnSizeVars = useMemo(() => {
|
||||
const headers = table.getFlatHeaders();
|
||||
const headers = tableImpl.getFlatHeaders();
|
||||
const colSizes: Record<string, number> = {};
|
||||
for (const header of headers) {
|
||||
colSizes[`--header-${header.id}-size`] = header.getSize();
|
||||
colSizes[`--col-${header.column.id}-size`] = header.column.getSize();
|
||||
}
|
||||
return colSizes;
|
||||
}, [table]);
|
||||
}, [tableImpl]);
|
||||
|
||||
return (
|
||||
<div
|
||||
tabIndex={-1}
|
||||
id={id}
|
||||
className={clsx('table-auto', className)}
|
||||
style={{ minHeight: fixedSize, maxHeight: fixedSize, ...style }}
|
||||
>
|
||||
<div tabIndex={-1} id={id} className={className} style={{ minHeight: fixedSize, maxHeight: fixedSize, ...style }}>
|
||||
<table className='w-full' style={{ ...columnSizeVars }}>
|
||||
{!noHeader ? (
|
||||
<TableHeader table={table} headPosition={headPosition} resetLastSelected={() => setLastSelected(null)} />
|
||||
<TableHeader
|
||||
table={tableImpl}
|
||||
enableRowSelection={enableRowSelection}
|
||||
enableSorting={enableSorting}
|
||||
headPosition={headPosition}
|
||||
resetLastSelected={() => setLastSelected(null)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<TableBody
|
||||
table={table}
|
||||
table={tableImpl}
|
||||
dense={dense}
|
||||
noHeader={noHeader}
|
||||
conditionalRowStyles={conditionalRowStyles}
|
||||
enableRowSelection={enableRowSelection}
|
||||
lastSelected={lastSelected}
|
||||
onChangeLastSelected={setLastSelected}
|
||||
onRowClicked={onRowClicked}
|
||||
onRowDoubleClicked={onRowDoubleClicked}
|
||||
/>
|
||||
|
||||
{!noFooter ? <TableFooter table={table} /> : null}
|
||||
{!noFooter ? <TableFooter table={tableImpl} /> : null}
|
||||
</table>
|
||||
|
||||
{!!paginationPerPage && !isEmpty ? (
|
||||
{enablePagination && !isEmpty ? (
|
||||
<PaginationTools
|
||||
id={id ? `${id}__pagination` : undefined}
|
||||
table={table}
|
||||
table={tableImpl}
|
||||
paginationOptions={paginationOptions}
|
||||
onChangePaginationOption={onChangePaginationOption}
|
||||
/>
|
||||
) : null}
|
||||
{isEmpty ? noDataComponent ?? <DefaultNoData /> : null}
|
|
@ -1,26 +1,35 @@
|
|||
'use no memo';
|
||||
'use client';
|
||||
'use no memo';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { type Table } from '@tanstack/react-table';
|
||||
|
||||
import { prefixes } from '@/utils/constants';
|
||||
|
||||
import { IconPageFirst, IconPageLast, IconPageLeft, IconPageRight } from '../icons';
|
||||
import { IconPageFirst, IconPageLast, IconPageLeft, IconPageRight } from '../Icons';
|
||||
|
||||
interface PaginationToolsProps<TData> {
|
||||
id?: string;
|
||||
table: Table<TData>;
|
||||
paginationOptions: number[];
|
||||
onChangePaginationOption?: (newValue: number) => void;
|
||||
}
|
||||
|
||||
export function PaginationTools<TData>({ id, table, paginationOptions }: PaginationToolsProps<TData>) {
|
||||
export function PaginationTools<TData>({
|
||||
id,
|
||||
table,
|
||||
paginationOptions,
|
||||
onChangePaginationOption
|
||||
}: PaginationToolsProps<TData>) {
|
||||
const handlePaginationOptionsChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const perPage = Number(event.target.value);
|
||||
table.setPageSize(perPage);
|
||||
if (onChangePaginationOption) {
|
||||
onChangePaginationOption(perPage);
|
||||
}
|
||||
},
|
||||
[table]
|
||||
[onChangePaginationOption, table]
|
||||
);
|
||||
|
||||
return (
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { type Table } from '@tanstack/react-table';
|
||||
|
||||
import { CheckboxTristate } from '../input';
|
||||
import { CheckboxTristate } from '../Input';
|
||||
|
||||
interface SelectAllProps<TData> {
|
||||
table: Table<TData>;
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { type Row } from '@tanstack/react-table';
|
||||
|
||||
import { Checkbox } from '../input';
|
||||
import { Checkbox } from '../Input';
|
||||
|
||||
interface SelectRowProps<TData> {
|
||||
row: Row<TData>;
|
20
rsconcept/frontend/src/components/DataTable/SortingIcon.tsx
Normal file
20
rsconcept/frontend/src/components/DataTable/SortingIcon.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
'use no memo';
|
||||
|
||||
import { type Column } from '@tanstack/react-table';
|
||||
|
||||
import { IconSortAsc, IconSortDesc } from '../Icons';
|
||||
|
||||
interface SortingIconProps<TData> {
|
||||
column: Column<TData>;
|
||||
}
|
||||
|
||||
export function SortingIcon<TData>({ column }: SortingIconProps<TData>) {
|
||||
return (
|
||||
<>
|
||||
{{
|
||||
desc: <IconSortDesc size='1rem' />,
|
||||
asc: <IconSortAsc size='1rem' />
|
||||
}[column.getIsSorted() as string] ?? <IconSortDesc size='1rem' className='opacity-0 group-hover:opacity-25' />}
|
||||
</>
|
||||
);
|
||||
}
|
104
rsconcept/frontend/src/components/DataTable/TableBody.tsx
Normal file
104
rsconcept/frontend/src/components/DataTable/TableBody.tsx
Normal file
|
@ -0,0 +1,104 @@
|
|||
'use no memo';
|
||||
|
||||
import { type Cell, flexRender, type Row, type Table } from '@tanstack/react-table';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { SelectRow } from './SelectRow';
|
||||
import { type IConditionalStyle } from '.';
|
||||
|
||||
interface TableBodyProps<TData> {
|
||||
table: Table<TData>;
|
||||
dense?: boolean;
|
||||
noHeader?: boolean;
|
||||
enableRowSelection?: boolean;
|
||||
conditionalRowStyles?: IConditionalStyle<TData>[];
|
||||
|
||||
lastSelected: string | null;
|
||||
onChangeLastSelected: (newValue: string | null) => void;
|
||||
|
||||
onRowClicked?: (rowData: TData, event: React.MouseEvent<Element>) => void;
|
||||
onRowDoubleClicked?: (rowData: TData, event: React.MouseEvent<Element>) => void;
|
||||
}
|
||||
|
||||
export function TableBody<TData>({
|
||||
table,
|
||||
dense,
|
||||
noHeader,
|
||||
enableRowSelection,
|
||||
conditionalRowStyles,
|
||||
lastSelected,
|
||||
onChangeLastSelected,
|
||||
onRowClicked,
|
||||
onRowDoubleClicked
|
||||
}: TableBodyProps<TData>) {
|
||||
function handleRowClicked(target: Row<TData>, event: React.MouseEvent<Element>) {
|
||||
onRowClicked?.(target.original, event);
|
||||
if (enableRowSelection && target.getCanSelect()) {
|
||||
if (event.shiftKey && !!lastSelected && lastSelected !== target.id) {
|
||||
const { rows, rowsById } = table.getRowModel();
|
||||
const lastIndex = rowsById[lastSelected].index;
|
||||
const currentIndex = target.index;
|
||||
const toggleRows = rows.slice(
|
||||
lastIndex > currentIndex ? currentIndex : lastIndex + 1,
|
||||
lastIndex > currentIndex ? lastIndex : currentIndex + 1
|
||||
);
|
||||
const newSelection: Record<string, boolean> = {};
|
||||
toggleRows.forEach(row => {
|
||||
newSelection[row.id] = !target.getIsSelected();
|
||||
});
|
||||
table.setRowSelection(prev => ({ ...prev, ...newSelection }));
|
||||
onChangeLastSelected(null);
|
||||
} else {
|
||||
onChangeLastSelected(target.id);
|
||||
target.toggleSelected(!target.getIsSelected());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getRowStyles(row: Row<TData>) {
|
||||
return {
|
||||
...conditionalRowStyles!
|
||||
.filter(item => item.when(row.original))
|
||||
.reduce((prev, item) => ({ ...prev, ...item.style }), {})
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<tbody>
|
||||
{table.getRowModel().rows.map((row: Row<TData>, index) => (
|
||||
<tr
|
||||
key={row.id}
|
||||
className={clsx(
|
||||
'cc-scroll-row',
|
||||
'clr-hover cc-animate-color',
|
||||
!noHeader && 'scroll-mt-[calc(2px+2rem)]',
|
||||
row.getIsSelected() ? 'clr-selected' : 'odd:bg-prim-200 even:bg-prim-100'
|
||||
)}
|
||||
style={{ ...(conditionalRowStyles ? getRowStyles(row) : []) }}
|
||||
>
|
||||
{enableRowSelection ? (
|
||||
<td key={`select-${row.id}`} className='pl-3 pr-1 border-y'>
|
||||
<SelectRow row={row} onChangeLastSelected={onChangeLastSelected} />
|
||||
</td>
|
||||
) : null}
|
||||
{row.getVisibleCells().map((cell: Cell<TData, unknown>) => (
|
||||
<td
|
||||
key={cell.id}
|
||||
className='px-2 align-middle border-y'
|
||||
style={{
|
||||
cursor: onRowClicked || onRowDoubleClicked ? 'pointer' : 'auto',
|
||||
paddingBottom: dense ? '0.25rem' : '0.5rem',
|
||||
paddingTop: dense ? '0.25rem' : '0.5rem',
|
||||
width: noHeader && index === 0 ? `calc(var(--col-${cell.column.id}-size) * 1px)` : 'auto'
|
||||
}}
|
||||
onClick={event => handleRowClicked(row, event)}
|
||||
onDoubleClick={event => onRowDoubleClicked?.(row.original, event)}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
);
|
||||
}
|
62
rsconcept/frontend/src/components/DataTable/TableHeader.tsx
Normal file
62
rsconcept/frontend/src/components/DataTable/TableHeader.tsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
'use no memo';
|
||||
|
||||
import { flexRender, type Header, type HeaderGroup, type Table } from '@tanstack/react-table';
|
||||
|
||||
import { SelectAll } from './SelectAll';
|
||||
import { SortingIcon } from './SortingIcon';
|
||||
|
||||
interface TableHeaderProps<TData> {
|
||||
table: Table<TData>;
|
||||
headPosition?: string;
|
||||
enableRowSelection?: boolean;
|
||||
enableSorting?: boolean;
|
||||
resetLastSelected: () => void;
|
||||
}
|
||||
|
||||
export function TableHeader<TData>({
|
||||
table,
|
||||
headPosition,
|
||||
enableRowSelection,
|
||||
enableSorting,
|
||||
resetLastSelected
|
||||
}: TableHeaderProps<TData>) {
|
||||
return (
|
||||
<thead
|
||||
className='bg-prim-100 cc-shadow-border'
|
||||
style={{
|
||||
top: headPosition,
|
||||
position: 'sticky'
|
||||
}}
|
||||
>
|
||||
{table.getHeaderGroups().map((headerGroup: HeaderGroup<TData>) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{enableRowSelection ? (
|
||||
<th className='pl-3 pr-1' scope='col'>
|
||||
<SelectAll table={table} resetLastSelected={resetLastSelected} />
|
||||
</th>
|
||||
) : null}
|
||||
{headerGroup.headers.map((header: Header<TData, unknown>) => (
|
||||
<th
|
||||
key={header.id}
|
||||
colSpan={header.colSpan}
|
||||
scope='col'
|
||||
className='cc-table-header group'
|
||||
style={{
|
||||
width: `calc(var(--header-${header?.id}-size) * 1px)`,
|
||||
cursor: enableSorting && header.column.getCanSort() ? 'pointer' : 'auto'
|
||||
}}
|
||||
onClick={enableSorting ? header.column.getToggleSortingHandler() : undefined}
|
||||
>
|
||||
{!header.isPlaceholder ? (
|
||||
<span className='inline-flex gap-1'>
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
{enableSorting && header.column.getCanSort() ? <SortingIcon column={header.column} /> : null}
|
||||
</span>
|
||||
) : null}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
);
|
||||
}
|
|
@ -4,4 +4,4 @@ export {
|
|||
type IConditionalStyle,
|
||||
type RowSelectionState,
|
||||
type VisibilityState
|
||||
} from './data-table';
|
||||
} from './DataTable';
|
|
@ -1,6 +1,8 @@
|
|||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
||||
import { type Styling } from '../props';
|
||||
|
||||
interface DropdownProps extends Styling {
|
||||
|
@ -34,19 +36,36 @@ export function Dropdown({
|
|||
margin,
|
||||
className,
|
||||
children,
|
||||
style,
|
||||
...restProps
|
||||
}: React.PropsWithChildren<DropdownProps>) {
|
||||
return (
|
||||
<div
|
||||
tabIndex={-1}
|
||||
className={clsx(
|
||||
'cc-dropdown isolate z-topmost absolute grid bg-prim-0 border rounded-md shadow-lg text-sm',
|
||||
stretchLeft ? 'right-0' : 'left-0',
|
||||
stretchTop ? 'bottom-0' : 'top-full',
|
||||
isOpen && 'open',
|
||||
'z-topmost absolute',
|
||||
{
|
||||
'right-0': stretchLeft,
|
||||
'left-0': !stretchLeft,
|
||||
'bottom-0': stretchTop,
|
||||
'top-full': !stretchTop
|
||||
},
|
||||
'grid',
|
||||
'border rounded-md shadow-lg',
|
||||
'clr-input',
|
||||
'text-sm',
|
||||
margin,
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
willChange: 'clip-path, transform',
|
||||
transitionProperty: 'clip-path, transform',
|
||||
transitionDuration: `${PARAMETER.dropdownDuration}ms`,
|
||||
transitionTimingFunction: 'ease-in-out',
|
||||
transform: isOpen ? 'translateY(0)' : 'translateY(-10%)',
|
||||
clipPath: isOpen ? 'inset(0% 0% 0% 0%)' : 'inset(10% 0% 90% 0%)',
|
||||
...style
|
||||
}}
|
||||
aria-hidden={!isOpen}
|
||||
{...restProps}
|
||||
>
|
|
@ -39,7 +39,11 @@ export function DropdownButton({
|
|||
'text-left text-sm text-ellipsis whitespace-nowrap',
|
||||
'disabled:clr-text-controls',
|
||||
'cc-animate-color',
|
||||
!!onClick ? 'clr-hover cursor-pointer disabled:cursor-auto' : 'clr-btn-default',
|
||||
{
|
||||
'clr-hover': onClick,
|
||||
'cursor-pointer disabled:cursor-auto': onClick,
|
||||
'cursor-default': !onClick
|
||||
},
|
||||
className
|
||||
)}
|
||||
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
|
@ -1,6 +1,6 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { Checkbox, type CheckboxProps } from '../input';
|
||||
import { Checkbox, type CheckboxProps } from '../Input';
|
||||
|
||||
/** Animated {@link Checkbox} inside a {@link Dropdown} item. */
|
||||
export function DropdownCheckbox({ onChange: setValue, disabled, ...restProps }: CheckboxProps) {
|
4
rsconcept/frontend/src/components/Dropdown/index.tsx
Normal file
4
rsconcept/frontend/src/components/Dropdown/index.tsx
Normal file
|
@ -0,0 +1,4 @@
|
|||
export { Dropdown } from './Dropdown';
|
||||
export { DropdownButton } from './DropdownButton';
|
||||
export { DropdownCheckbox } from './DropdownCheckbox';
|
||||
export { useDropdown } from './useDropdown';
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
import { useClickedOutside } from '@/hooks/use-clicked-outside';
|
||||
import { useClickedOutside } from '@/hooks/useClickedOutside';
|
||||
|
||||
export function useDropdown() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
|
@ -196,10 +196,9 @@ export function IconLogin(props: IconProps) {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
export function CheckboxChecked() {
|
||||
return (
|
||||
<svg className='w-4 h-4 p-0.75 -ml-0.25' viewBox='0 0 512 512' fill='#ffffff'>
|
||||
<svg className='w-4 h-4 p-0.75' viewBox='0 0 512 512' fill='#ffffff'>
|
||||
<path d='M470.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 338.7l233.4-233.3c12.5-12.5 32.8-12.5 45.3 0z' />
|
||||
</svg>
|
||||
);
|
||||
|
@ -207,8 +206,8 @@ export function CheckboxChecked() {
|
|||
|
||||
export function CheckboxNull() {
|
||||
return (
|
||||
<svg className='w-4 h-4 px-0.25' viewBox='0 0 16 16' fill='#ffffff'>
|
||||
<svg className='w-4 h-4 p-0.25' viewBox='0 0 16 16' fill='#ffffff'>
|
||||
<path d='M2 7.75A.75.75 0 012.75 7h10a.75.75 0 010 1.5h-10A.75.75 0 012 7.75z' />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
import clsx from 'clsx';
|
||||
import { ZodError } from 'zod';
|
||||
|
||||
import { type AxiosError, isAxiosError } from '@/backend/api-transport';
|
||||
import { type AxiosError, isAxiosError } from '@/backend/apiTransport';
|
||||
import { isResponseHtml } from '@/utils/utils';
|
||||
|
||||
import { PrettyJson } from './view';
|
||||
import { PrettyJson } from './View';
|
||||
|
||||
export type ErrorData = string | Error | AxiosError | ZodError;
|
||||
|
||||
|
@ -33,7 +33,18 @@ export function DescribeError({ error }: { error: ErrorData }) {
|
|||
<p>
|
||||
<b>Message:</b> {error.message}
|
||||
</p>
|
||||
{error.stack && <pre className='whitespace-pre-wrap p-2 overflow-x-auto break-words'>{error.stack}</pre>}
|
||||
{error.stack && (
|
||||
<pre
|
||||
style={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordWrap: 'break-word',
|
||||
padding: '6px',
|
||||
overflowX: 'auto'
|
||||
}}
|
||||
>
|
||||
{error.stack}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -2,7 +2,7 @@ import clsx from 'clsx';
|
|||
|
||||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
import { CheckboxChecked } from '../icons';
|
||||
import { CheckboxChecked } from '../Icons';
|
||||
import { type Button } from '../props';
|
||||
|
||||
export interface CheckboxProps extends Omit<Button, 'value' | 'onClick' | 'onChange'> {
|
||||
|
@ -65,8 +65,11 @@ export function Checkbox({
|
|||
<div
|
||||
className={clsx(
|
||||
'w-4 h-4', //
|
||||
'border rounded-sm',
|
||||
value === false ? 'bg-prim-100' : 'bg-sec-600 text-sec-0'
|
||||
'border rounded-xs',
|
||||
{
|
||||
'bg-sec-600 text-sec-0': value !== false,
|
||||
'bg-prim-100': value === false
|
||||
}
|
||||
)}
|
||||
>
|
||||
{value ? <CheckboxChecked /> : null}
|
|
@ -2,9 +2,9 @@ import clsx from 'clsx';
|
|||
|
||||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
import { CheckboxChecked, CheckboxNull } from '../icons';
|
||||
import { CheckboxChecked, CheckboxNull } from '../Icons';
|
||||
|
||||
import { type CheckboxProps } from './checkbox';
|
||||
import { type CheckboxProps } from './Checkbox';
|
||||
|
||||
export interface CheckboxTristateProps extends Omit<CheckboxProps, 'value' | 'onChange'> {
|
||||
/** Current value - `null`, `true` or `false`. */
|
||||
|
@ -66,8 +66,11 @@ export function CheckboxTristate({
|
|||
<div
|
||||
className={clsx(
|
||||
'w-4 h-4', //
|
||||
'border rounded-sm',
|
||||
value === false ? 'bg-prim-100' : 'bg-sec-600 text-sec-0'
|
||||
'border rounded-xs',
|
||||
{
|
||||
'bg-sec-600 text-sec-0': value !== false,
|
||||
'bg-prim-100': value === false
|
||||
}
|
||||
)}
|
||||
>
|
||||
{value ? <CheckboxChecked /> : null}
|
|
@ -3,11 +3,11 @@
|
|||
import { useRef, useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { Button } from '../control';
|
||||
import { IconUpload } from '../icons';
|
||||
import { Button } from '../Control';
|
||||
import { IconUpload } from '../Icons';
|
||||
import { type Titled } from '../props';
|
||||
|
||||
import { Label } from './label';
|
||||
import { Label } from './Label';
|
||||
|
||||
interface FileInputProps extends Titled, Omit<React.ComponentProps<'input'>, 'accept' | 'type'> {
|
||||
/** Label to display in file upload button. */
|
||||
|
@ -46,7 +46,7 @@ export function FileInput({ id, label, acceptType, title, className, style, onCh
|
|||
id={id}
|
||||
type='file'
|
||||
ref={inputRef}
|
||||
className='hidden'
|
||||
style={{ display: 'none' }}
|
||||
accept={acceptType}
|
||||
onChange={handleFileChange}
|
||||
{...restProps}
|
|
@ -1,9 +1,9 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { IconSearch } from '@/components/icons';
|
||||
import { IconSearch } from '@/components/Icons';
|
||||
import { type Styling } from '@/components/props';
|
||||
|
||||
import { TextInput } from './text-input';
|
||||
import { TextInput } from './TextInput';
|
||||
|
||||
interface SearchBarProps extends Styling {
|
||||
/** Id of the search bar. */
|
||||
|
@ -39,10 +39,10 @@ export function SearchBar({
|
|||
...restProps
|
||||
}: SearchBarProps) {
|
||||
return (
|
||||
<div className={clsx('relative flex items-center', className)} {...restProps}>
|
||||
<div className={clsx('relative', className)} {...restProps}>
|
||||
{!noIcon ? (
|
||||
<IconSearch
|
||||
className='absolute -top-0.5 left-2 translate-y-1/2 pointer-events-none clr-text-controls'
|
||||
className='absolute -top-0.5 left-3 translate-y-1/2 pointer-events-none clr-text-controls'
|
||||
size='1.25rem'
|
||||
/>
|
||||
) : null}
|
||||
|
@ -52,7 +52,7 @@ export function SearchBar({
|
|||
transparent
|
||||
placeholder={placeholder}
|
||||
type='search'
|
||||
className={clsx('bg-transparent', !noIcon && 'pl-8')}
|
||||
className={clsx('bg-transparent', !noIcon && 'pl-10')}
|
||||
noBorder={noBorder}
|
||||
value={query}
|
||||
onChange={event => onChangeQuery?.(event.target.value)}
|
|
@ -9,10 +9,10 @@ import Select, {
|
|||
type StylesConfig
|
||||
} from 'react-select';
|
||||
|
||||
import { useWindowSize } from '@/hooks/use-window-size';
|
||||
import { useWindowSize } from '@/hooks/useWindowSize';
|
||||
import { APP_COLORS, SELECT_THEME } from '@/styling/colors';
|
||||
|
||||
import { IconClose, IconDropArrow, IconDropArrowUp } from '../icons';
|
||||
import { IconClose, IconDropArrow, IconDropArrowUp } from '../Icons';
|
||||
|
||||
function DropdownIndicator<Option, Group extends GroupBase<Option> = GroupBase<Option>>(
|
||||
props: DropdownIndicatorProps<Option, true, Group>
|
|
@ -9,10 +9,10 @@ import Select, {
|
|||
type StylesConfig
|
||||
} from 'react-select';
|
||||
|
||||
import { useWindowSize } from '@/hooks/use-window-size';
|
||||
import { useWindowSize } from '@/hooks/useWindowSize';
|
||||
import { APP_COLORS, SELECT_THEME } from '@/styling/colors';
|
||||
|
||||
import { IconClose, IconDropArrow, IconDropArrowUp } from '../icons';
|
||||
import { IconClose, IconDropArrow, IconDropArrowUp } from '../Icons';
|
||||
|
||||
function DropdownIndicator<Option, Group extends GroupBase<Option> = GroupBase<Option>>(
|
||||
props: DropdownIndicatorProps<Option, false, Group>
|
|
@ -1,10 +1,10 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { globalIDs } from '@/utils/constants';
|
||||
import { globalIDs, PARAMETER } from '@/utils/constants';
|
||||
|
||||
import { MiniButton } from '../control';
|
||||
import { IconDropArrow, IconPageRight } from '../icons';
|
||||
import { MiniButton } from '../Control';
|
||||
import { IconDropArrow, IconPageRight } from '../Icons';
|
||||
import { type Styling } from '../props';
|
||||
|
||||
interface SelectTreeProps<ItemType> extends Styling {
|
||||
|
@ -89,13 +89,27 @@ export function SelectTree<ItemType>({
|
|||
<div
|
||||
key={`${prefix}${index}`}
|
||||
className={clsx(
|
||||
'cc-tree-item relative cc-scroll-row clr-hover',
|
||||
isActive ? 'max-h-7 py-1 border-b' : 'max-h-0 opacity-0 pointer-events-none',
|
||||
value === item && 'clr-selected'
|
||||
'relative',
|
||||
'pr-3 pl-6 border-b',
|
||||
'cc-scroll-row',
|
||||
'bg-prim-200 clr-hover cc-animate-color',
|
||||
'cursor-pointer',
|
||||
value === item && 'clr-selected',
|
||||
!isActive && 'pointer-events-none'
|
||||
)}
|
||||
data-tooltip-id={globalIDs.tooltip}
|
||||
data-tooltip-html={getDescription(item)}
|
||||
onClick={event => handleClickItem(event, item)}
|
||||
style={{
|
||||
borderBottomWidth: isActive ? '1px' : '0px',
|
||||
willChange: 'max-height, opacity, padding',
|
||||
transitionProperty: 'max-height, opacity, padding',
|
||||
transitionDuration: `${PARAMETER.moveDuration}ms`,
|
||||
paddingTop: isActive ? '0.25rem' : '0',
|
||||
paddingBottom: isActive ? '0.25rem' : '0',
|
||||
maxHeight: isActive ? '1.75rem' : '0',
|
||||
opacity: isActive ? '1' : '0'
|
||||
}}
|
||||
>
|
||||
{foldable.has(item) ? (
|
||||
<MiniButton
|
|
@ -1,9 +1,9 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { Label } from '../Input/Label';
|
||||
import { type Editor, type ErrorProcessing, type Titled } from '../props';
|
||||
|
||||
import { ErrorField } from './error-field';
|
||||
import { Label } from './label';
|
||||
import { ErrorField } from './ErrorField';
|
||||
|
||||
export interface TextAreaProps extends Editor, ErrorProcessing, Titled, React.ComponentProps<'textarea'> {
|
||||
/** Indicates that the input should be transparent. */
|
||||
|
@ -40,8 +40,11 @@ export function TextArea({
|
|||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'w-full', //
|
||||
dense ? 'flex grow items-center gap-3' : 'flex flex-col',
|
||||
'w-full',
|
||||
{
|
||||
'flex flex-col': !dense,
|
||||
'flex grow items-center gap-3': dense
|
||||
},
|
||||
dense && className
|
||||
)}
|
||||
>
|
||||
|
@ -52,13 +55,16 @@ export function TextArea({
|
|||
'px-3 py-2',
|
||||
'leading-tight',
|
||||
'overflow-x-hidden overflow-y-auto',
|
||||
!noBorder && 'border',
|
||||
fitContent && 'field-sizing-content',
|
||||
noResize && 'resize-none',
|
||||
transparent ? 'bg-transparent' : 'clr-input',
|
||||
!noOutline && 'clr-outline',
|
||||
dense && 'grow max-w-full',
|
||||
!dense && !!label && 'mt-2',
|
||||
{
|
||||
'field-sizing-content': fitContent,
|
||||
'resize-none': noResize,
|
||||
'border': !noBorder,
|
||||
'grow max-w-full': dense,
|
||||
'mt-2': !dense && !!label,
|
||||
'clr-outline': !noOutline,
|
||||
'bg-transparent': transparent,
|
||||
'clr-input': !transparent
|
||||
},
|
||||
!dense && className
|
||||
)}
|
||||
rows={rows}
|
|
@ -1,9 +1,9 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { Label } from '../Input/Label';
|
||||
import { type Editor, type ErrorProcessing, type Titled } from '../props';
|
||||
|
||||
import { ErrorField } from './error-field';
|
||||
import { Label } from './label';
|
||||
import { ErrorField } from './ErrorField';
|
||||
|
||||
interface TextInputProps extends Editor, ErrorProcessing, Titled, React.ComponentProps<'input'> {
|
||||
/** Indicates that the input should be transparent. */
|
||||
|
@ -42,7 +42,10 @@ export function TextInput({
|
|||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
dense ? 'flex items-center gap-3' : 'flex flex-col', //
|
||||
{
|
||||
'flex flex-col': !dense,
|
||||
'flex items-center gap-3': dense
|
||||
},
|
||||
dense && className
|
||||
)}
|
||||
>
|
||||
|
@ -52,12 +55,15 @@ export function TextInput({
|
|||
className={clsx(
|
||||
'min-w-0 py-2',
|
||||
'leading-tight truncate hover:text-clip',
|
||||
transparent ? 'bg-transparent' : 'clr-input',
|
||||
!noBorder && 'border',
|
||||
!noOutline && 'clr-outline',
|
||||
(!noBorder || !disabled) && 'px-3',
|
||||
dense && 'grow max-w-full',
|
||||
!dense && !!label && 'mt-2',
|
||||
{
|
||||
'px-3': !noBorder || !disabled,
|
||||
'grow max-w-full': dense,
|
||||
'mt-2': !dense && !!label,
|
||||
'border': !noBorder,
|
||||
'clr-outline': !noOutline,
|
||||
'bg-transparent': transparent,
|
||||
'clr-input': !transparent
|
||||
},
|
||||
!dense && className
|
||||
)}
|
||||
onKeyDown={!allowEnter && !onKeyDown ? preventEnterCapture : onKeyDown}
|
11
rsconcept/frontend/src/components/Input/index.tsx
Normal file
11
rsconcept/frontend/src/components/Input/index.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
export { Checkbox, type CheckboxProps } from './Checkbox';
|
||||
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';
|
||||
export { TextArea } from './TextArea';
|
||||
export { TextInput } from './TextInput';
|
|
@ -5,15 +5,15 @@ import clsx from 'clsx';
|
|||
import { type HelpTopic } from '@/features/help';
|
||||
import { BadgeHelp } from '@/features/help/components';
|
||||
|
||||
import { useEscapeKey } from '@/hooks/use-escape-key';
|
||||
import { useEscapeKey } from '@/hooks/useEscapeKey';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { prepareTooltip } from '@/utils/utils';
|
||||
|
||||
import { Button, MiniButton, SubmitButton } from '../control';
|
||||
import { IconClose } from '../icons';
|
||||
import { Button, MiniButton, SubmitButton } from '../Control';
|
||||
import { IconClose } from '../Icons';
|
||||
import { type Styling } from '../props';
|
||||
|
||||
import { ModalBackdrop } from './modal-backdrop';
|
||||
import { ModalBackdrop } from './ModalBackdrop';
|
||||
|
||||
export interface ModalProps extends Styling {
|
||||
/** Title of the modal window. */
|
||||
|
@ -90,7 +90,7 @@ export function ModalForm({
|
|||
<div className='cc-modal-wrapper'>
|
||||
<ModalBackdrop onHide={handleCancel} />
|
||||
<form
|
||||
className='cc-animate-modal relative grid border rounded-xl bg-prim-100'
|
||||
className='cc-animate-modal grid border rounded-xl bg-prim-100'
|
||||
role='dialog'
|
||||
onSubmit={handleSubmit}
|
||||
aria-labelledby='modal-title'
|
||||
|
@ -98,7 +98,7 @@ export function ModalForm({
|
|||
{helpTopic && !hideHelpWhen?.() ? (
|
||||
<BadgeHelp
|
||||
topic={helpTopic}
|
||||
className='absolute z-top top-2 left-2'
|
||||
className='absolute z-pop left-0 mt-2 ml-2'
|
||||
padding='p-0'
|
||||
contentClass='sm:max-w-160'
|
||||
/>
|
||||
|
@ -109,7 +109,7 @@ export function ModalForm({
|
|||
aria-label='Закрыть'
|
||||
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
|
||||
icon={<IconClose size='1.25rem' />}
|
||||
className='absolute z-pop top-2 right-2'
|
||||
className='absolute z-pop right-0 mt-2 mr-2'
|
||||
onClick={hideDialog}
|
||||
/>
|
||||
|
||||
|
@ -124,7 +124,10 @@ export function ModalForm({
|
|||
'@container/modal',
|
||||
'max-h-[calc(100svh-8rem)] max-w-[100svw] xs:max-w-[calc(100svw-2rem)]',
|
||||
'overscroll-contain outline-hidden',
|
||||
overflowVisible ? 'overflow-visible' : 'overflow-auto',
|
||||
{
|
||||
'overflow-auto': !overflowVisible,
|
||||
'overflow-visible': overflowVisible
|
||||
},
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
|
@ -1,6 +1,6 @@
|
|||
import { Loader } from '@/components/loader';
|
||||
import { Loader } from '@/components/Loader';
|
||||
|
||||
import { ModalBackdrop } from './modal-backdrop';
|
||||
import { ModalBackdrop } from './ModalBackdrop';
|
||||
|
||||
export function ModalLoader() {
|
||||
return (
|
83
rsconcept/frontend/src/components/Modal/ModalView.tsx
Normal file
83
rsconcept/frontend/src/components/Modal/ModalView.tsx
Normal file
|
@ -0,0 +1,83 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { BadgeHelp } from '@/features/help/components';
|
||||
|
||||
import { useEscapeKey } from '@/hooks/useEscapeKey';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { prepareTooltip } from '@/utils/utils';
|
||||
|
||||
import { Button, MiniButton } from '../Control';
|
||||
import { IconClose } from '../Icons';
|
||||
|
||||
import { ModalBackdrop } from './ModalBackdrop';
|
||||
import { type ModalProps } from './ModalForm';
|
||||
|
||||
interface ModalViewProps extends ModalProps {}
|
||||
|
||||
/**
|
||||
* Displays a customizable modal window with submit form.
|
||||
*/
|
||||
export function ModalView({
|
||||
children,
|
||||
className,
|
||||
header,
|
||||
overflowVisible,
|
||||
helpTopic,
|
||||
hideHelpWhen,
|
||||
...restProps
|
||||
}: React.PropsWithChildren<ModalViewProps>) {
|
||||
const hideDialog = useDialogsStore(state => state.hideDialog);
|
||||
useEscapeKey(hideDialog);
|
||||
|
||||
return (
|
||||
<div className='cc-modal-wrapper'>
|
||||
<ModalBackdrop onHide={hideDialog} />
|
||||
<div className='cc-animate-modal grid border rounded-xl bg-prim-100' role='dialog'>
|
||||
{helpTopic && !hideHelpWhen?.() ? (
|
||||
<BadgeHelp
|
||||
topic={helpTopic}
|
||||
className='absolute z-pop left-0 mt-2 ml-2'
|
||||
padding='p-0'
|
||||
contentClass='sm:max-w-160'
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<MiniButton
|
||||
noPadding
|
||||
aria-label='Закрыть'
|
||||
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
|
||||
icon={<IconClose size='1.25rem' />}
|
||||
className='absolute z-pop right-0 mt-2 mr-2'
|
||||
onClick={hideDialog}
|
||||
/>
|
||||
|
||||
{header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null}
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
'@container/modal',
|
||||
'max-h-[calc(100svh-8rem)] max-w-[100svw] xs:max-w-[calc(100svw-2rem)]',
|
||||
'overscroll-contain outline-hidden',
|
||||
{
|
||||
'overflow-auto': !overflowVisible,
|
||||
'overflow-visible': overflowVisible
|
||||
},
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
text='Закрыть'
|
||||
aria-label='Закрыть'
|
||||
className='z-pop my-2 mx-auto text-sm min-w-28'
|
||||
onClick={hideDialog}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
3
rsconcept/frontend/src/components/Modal/index.tsx
Normal file
3
rsconcept/frontend/src/components/Modal/index.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export { ModalForm } from './ModalForm';
|
||||
export { ModalLoader } from './ModalLoader';
|
||||
export { ModalView } from './ModalView';
|
|
@ -1,2 +1,2 @@
|
|||
export { TabLabel } from './tab-label';
|
||||
export { TabLabel } from './TabLabel';
|
||||
export { TabList, TabPanel, Tabs } from 'react-tabs';
|
|
@ -18,8 +18,9 @@ export function EmbedYoutube({ videoID, pxHeight, pxWidth }: EmbedYoutubeProps)
|
|||
}
|
||||
return (
|
||||
<div
|
||||
className='relative h-0'
|
||||
className='relative'
|
||||
style={{
|
||||
height: 0,
|
||||
paddingBottom: `${pxHeight}px`,
|
||||
paddingLeft: `${pxWidth}px`
|
||||
}}
|
|
@ -18,9 +18,11 @@ export function Indicator({ icon, title, titleHtml, hideTitle, noPadding, classN
|
|||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'clr-text-controls', //
|
||||
'clr-text-controls',
|
||||
'outline-hidden',
|
||||
!noPadding && 'px-1 py-1',
|
||||
{
|
||||
'px-1 py-1': !noPadding
|
||||
},
|
||||
className
|
||||
)}
|
||||
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { useWindowSize } from '@/hooks/use-window-size';
|
||||
import { useFitHeight } from '@/stores/app-layout';
|
||||
import { useWindowSize } from '@/hooks/useWindowSize';
|
||||
import { useFitHeight } from '@/stores/appLayout';
|
||||
|
||||
/** Maximum width of the viewer. */
|
||||
const MAXIMUM_WIDTH = 1600;
|
|
@ -2,7 +2,7 @@ import clsx from 'clsx';
|
|||
|
||||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
import { MiniButton } from '../control';
|
||||
import { MiniButton } from '../Control';
|
||||
import { type Styling, type Titled } from '../props';
|
||||
|
||||
interface ValueIconProps extends Styling, Titled {
|
||||
|
@ -57,7 +57,7 @@ export function ValueIcon({
|
|||
'flex items-center',
|
||||
'text-right',
|
||||
'hover:cursor-default',
|
||||
dense ? 'gap-1' : 'justify-between gap-6',
|
||||
{ 'justify-between gap-6': !dense, 'gap-1': dense },
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
|
@ -1,6 +1,6 @@
|
|||
import { type Styling, type Titled } from '@/components/props';
|
||||
|
||||
import { ValueIcon } from './value-icon';
|
||||
import { ValueIcon } from './ValueIcon';
|
||||
|
||||
// characters - threshold for small labels - small font
|
||||
const SMALL_THRESHOLD = 3;
|
9
rsconcept/frontend/src/components/View/index.tsx
Normal file
9
rsconcept/frontend/src/components/View/index.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
export { EmbedYoutube } from './EmbedYoutube';
|
||||
export { Indicator } from './Indicator';
|
||||
export { NoData } from './NoData';
|
||||
export { PDFViewer } from './PDFViewer';
|
||||
export { PrettyJson } from './PrettyJSON';
|
||||
export { TextContent } from './TextContent';
|
||||
export { ValueIcon } from './ValueIcon';
|
||||
export { ValueLabeled } from './ValueLabeled';
|
||||
export { ValueStats } from './ValueStats';
|
|
@ -1,2 +0,0 @@
|
|||
export { Divider } from './divider';
|
||||
export { type PlacesType, Tooltip } from './tooltip';
|
|
@ -1,5 +0,0 @@
|
|||
export { Button } from './button';
|
||||
export { MiniButton } from './mini-button';
|
||||
export { SelectorButton } from './selector-button';
|
||||
export { SubmitButton } from './submit-button';
|
||||
export { TextURL } from './text-url';
|
|
@ -1,15 +0,0 @@
|
|||
import { IconSortAsc, IconSortDesc } from '../icons';
|
||||
|
||||
interface SortingIconProps {
|
||||
sortDirection?: 'asc' | 'desc' | false;
|
||||
}
|
||||
|
||||
export function SortingIcon({ sortDirection }: SortingIconProps) {
|
||||
if (sortDirection === 'asc') {
|
||||
return <IconSortAsc size='1rem' />;
|
||||
}
|
||||
if (sortDirection === 'desc') {
|
||||
return <IconSortDesc size='1rem' />;
|
||||
}
|
||||
return <IconSortDesc size='1rem' className='opacity-0 group-hover:opacity-25 transition-opacity' />;
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
'use no memo';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { type Cell, flexRender, type Row, type Table } from '@tanstack/react-table';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { SelectRow } from './select-row';
|
||||
import { type IConditionalStyle } from '.';
|
||||
|
||||
interface TableBodyProps<TData> {
|
||||
table: Table<TData>;
|
||||
dense?: boolean;
|
||||
noHeader?: boolean;
|
||||
conditionalRowStyles?: IConditionalStyle<TData>[];
|
||||
|
||||
lastSelected: string | null;
|
||||
onChangeLastSelected: (newValue: string | null) => void;
|
||||
|
||||
onRowClicked?: (rowData: TData, event: React.MouseEvent<Element>) => void;
|
||||
onRowDoubleClicked?: (rowData: TData, event: React.MouseEvent<Element>) => void;
|
||||
}
|
||||
|
||||
export function TableBody<TData>({
|
||||
table,
|
||||
dense,
|
||||
noHeader,
|
||||
conditionalRowStyles,
|
||||
lastSelected,
|
||||
onChangeLastSelected,
|
||||
onRowClicked,
|
||||
onRowDoubleClicked
|
||||
}: TableBodyProps<TData>) {
|
||||
const handleRowClicked = useCallback(
|
||||
(target: Row<TData>, event: React.MouseEvent<Element>) => {
|
||||
onRowClicked?.(target.original, event);
|
||||
if (target.getCanSelect()) {
|
||||
if (event.shiftKey && !!lastSelected && lastSelected !== target.id) {
|
||||
const { rows, rowsById } = table.getRowModel();
|
||||
const lastIndex = rowsById[lastSelected].index;
|
||||
const currentIndex = target.index;
|
||||
const toggleRows = rows.slice(
|
||||
lastIndex > currentIndex ? currentIndex : lastIndex + 1,
|
||||
lastIndex > currentIndex ? lastIndex : currentIndex + 1
|
||||
);
|
||||
const newSelection: Record<string, boolean> = {};
|
||||
toggleRows.forEach(row => {
|
||||
newSelection[row.id] = !target.getIsSelected();
|
||||
});
|
||||
table.setRowSelection(prev => ({ ...prev, ...newSelection }));
|
||||
onChangeLastSelected(null);
|
||||
} else {
|
||||
onChangeLastSelected(target.id);
|
||||
target.toggleSelected(!target.getIsSelected());
|
||||
}
|
||||
}
|
||||
},
|
||||
[table, lastSelected, onChangeLastSelected, onRowClicked]
|
||||
);
|
||||
|
||||
const getRowStyles = useCallback(
|
||||
(row: Row<TData>) => {
|
||||
return {
|
||||
...conditionalRowStyles!
|
||||
.filter(item => item.when(row.original))
|
||||
.reduce((prev, item) => ({ ...prev, ...item.style }), {})
|
||||
};
|
||||
},
|
||||
[conditionalRowStyles]
|
||||
);
|
||||
|
||||
return (
|
||||
<tbody>
|
||||
{table.getRowModel().rows.map((row: Row<TData>, index) => (
|
||||
<tr
|
||||
key={row.id}
|
||||
className={clsx(
|
||||
'cc-scroll-row',
|
||||
'clr-hover cc-animate-color',
|
||||
!noHeader && 'scroll-mt-[calc(2px+2rem)]',
|
||||
table.options.enableRowSelection && row.getIsSelected()
|
||||
? 'clr-selected'
|
||||
: 'odd:bg-prim-200 even:bg-prim-100'
|
||||
)}
|
||||
style={{ ...(conditionalRowStyles ? getRowStyles(row) : []) }}
|
||||
onClick={event => handleRowClicked(row, event)}
|
||||
onDoubleClick={event => onRowDoubleClicked?.(row.original, event)}
|
||||
>
|
||||
{table.options.enableRowSelection ? (
|
||||
<td key={`select-${row.id}`} className='pl-2 border-y'>
|
||||
<SelectRow row={row} onChangeLastSelected={onChangeLastSelected} />
|
||||
</td>
|
||||
) : null}
|
||||
{row.getVisibleCells().map((cell: Cell<TData, unknown>) => (
|
||||
<td
|
||||
key={cell.id}
|
||||
className={clsx(
|
||||
'px-2 align-middle border-y',
|
||||
dense ? 'py-1' : 'py-2',
|
||||
onRowClicked || onRowDoubleClicked ? 'cursor-pointer' : 'cursor-auto'
|
||||
)}
|
||||
style={{
|
||||
width: noHeader && index === 0 ? `calc(var(--col-${cell.column.id}-size) * 1px)` : undefined
|
||||
}}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
);
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
'use no memo';
|
||||
|
||||
import { flexRender, type Header, type HeaderGroup, type Table } from '@tanstack/react-table';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { SelectAll } from './select-all';
|
||||
import { SortingIcon } from './sorting-icon';
|
||||
|
||||
interface TableHeaderProps<TData> {
|
||||
table: Table<TData>;
|
||||
headPosition?: string;
|
||||
resetLastSelected: () => void;
|
||||
}
|
||||
|
||||
export function TableHeader<TData>({ table, headPosition, resetLastSelected }: TableHeaderProps<TData>) {
|
||||
return (
|
||||
<thead className='sticky bg-prim-100 cc-shadow-border' style={{ top: headPosition }}>
|
||||
{table.getHeaderGroups().map((headerGroup: HeaderGroup<TData>) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{table.options.enableRowSelection ? (
|
||||
<th className='pl-2' scope='col'>
|
||||
<SelectAll table={table} resetLastSelected={resetLastSelected} />
|
||||
</th>
|
||||
) : null}
|
||||
{headerGroup.headers.map((header: Header<TData, unknown>) => (
|
||||
<th
|
||||
key={header.id}
|
||||
colSpan={header.colSpan}
|
||||
scope='col'
|
||||
className={clsx(
|
||||
'cc-table-header group',
|
||||
table.options.enableSorting && header.column.getCanSort() ? 'cursor-pointer' : 'cursor-auto'
|
||||
)}
|
||||
style={{ width: `calc(var(--header-${header?.id}-size) * 1px)` }}
|
||||
onClick={table.options.enableSorting ? header.column.getToggleSortingHandler() : undefined}
|
||||
>
|
||||
{!header.isPlaceholder ? (
|
||||
<span className='inline-flex gap-1'>
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
{table.options.enableSorting && header.column.getCanSort() ? (
|
||||
<SortingIcon sortDirection={header.column.getIsSorted()} />
|
||||
) : null}
|
||||
</span>
|
||||
) : null}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
);
|
||||
}
|
|
@ -1,125 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
import {
|
||||
type ColumnSort,
|
||||
createColumnHelper,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
type PaginationState,
|
||||
type RowData,
|
||||
type RowSelectionState,
|
||||
type SortingState,
|
||||
type TableOptions,
|
||||
type Updater,
|
||||
useReactTable,
|
||||
type VisibilityState
|
||||
} from '@tanstack/react-table';
|
||||
|
||||
export { type ColumnSort, createColumnHelper, type RowSelectionState, type VisibilityState };
|
||||
|
||||
/** Style to conditionally apply to rows. */
|
||||
export interface IConditionalStyle<TData> {
|
||||
/** Callback to determine if the style should be applied. */
|
||||
when: (rowData: TData) => boolean;
|
||||
|
||||
/** Style to apply. */
|
||||
style: React.CSSProperties;
|
||||
}
|
||||
|
||||
export interface UseDataTableProps<TData extends RowData>
|
||||
extends Pick<TableOptions<TData>, 'data' | 'columns' | 'onRowSelectionChange' | 'onColumnVisibilityChange'> {
|
||||
/** Enable row selection. */
|
||||
enableRowSelection?: boolean;
|
||||
|
||||
/** Current row selection. */
|
||||
rowSelection?: RowSelectionState;
|
||||
|
||||
/** Enable hiding of columns. */
|
||||
enableHiding?: boolean;
|
||||
|
||||
/** Current column visibility. */
|
||||
columnVisibility?: VisibilityState;
|
||||
|
||||
/** Enable pagination. */
|
||||
enablePagination?: boolean;
|
||||
|
||||
/** Number of rows per page. */
|
||||
paginationPerPage?: number;
|
||||
|
||||
/** Callback to be called when the pagination option is changed. */
|
||||
onChangePaginationOption?: (newValue: number) => void;
|
||||
|
||||
/** Enable sorting. */
|
||||
enableSorting?: boolean;
|
||||
|
||||
/** Initial sorting. */
|
||||
initialSorting?: ColumnSort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dta representation as a table.
|
||||
*
|
||||
* @param headPosition - Top position of sticky header (0 if no other sticky elements are present).
|
||||
* No sticky header if omitted
|
||||
*/
|
||||
export function useDataTable<TData extends RowData>({
|
||||
enableRowSelection,
|
||||
rowSelection,
|
||||
|
||||
enableHiding,
|
||||
columnVisibility,
|
||||
|
||||
enableSorting,
|
||||
initialSorting,
|
||||
|
||||
enablePagination,
|
||||
paginationPerPage = 10,
|
||||
onChangePaginationOption,
|
||||
|
||||
...restProps
|
||||
}: UseDataTableProps<TData>) {
|
||||
const [sorting, setSorting] = useState<SortingState>(initialSorting ? [initialSorting] : []);
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: paginationPerPage
|
||||
});
|
||||
|
||||
const handleChangePagination = useCallback(
|
||||
(updater: Updater<PaginationState>) => {
|
||||
setPagination(prev => {
|
||||
const resolvedValue = typeof updater === 'function' ? updater(prev) : updater;
|
||||
if (onChangePaginationOption && prev.pageSize !== resolvedValue.pageSize) {
|
||||
onChangePaginationOption(resolvedValue.pageSize);
|
||||
}
|
||||
return resolvedValue;
|
||||
});
|
||||
},
|
||||
[onChangePaginationOption]
|
||||
);
|
||||
|
||||
const table = useReactTable({
|
||||
state: {
|
||||
pagination: pagination,
|
||||
sorting: sorting,
|
||||
rowSelection: rowSelection,
|
||||
columnVisibility: columnVisibility
|
||||
},
|
||||
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
|
||||
enableSorting: enableSorting,
|
||||
getSortedRowModel: enableSorting ? getSortedRowModel() : undefined,
|
||||
onSortingChange: enableSorting ? setSorting : undefined,
|
||||
|
||||
getPaginationRowModel: enablePagination ? getPaginationRowModel() : undefined,
|
||||
onPaginationChange: enablePagination ? handleChangePagination : undefined,
|
||||
|
||||
enableHiding: enableHiding,
|
||||
enableMultiRowSelection: enableRowSelection,
|
||||
enableRowSelection: enableRowSelection,
|
||||
...restProps
|
||||
});
|
||||
return table;
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
export { Dropdown } from './dropdown';
|
||||
export { DropdownButton } from './dropdown-button';
|
||||
export { DropdownCheckbox } from './dropdown-checkbox';
|
||||
export { useDropdown } from './use-dropdown';
|
|
@ -1,11 +0,0 @@
|
|||
export { Checkbox, type CheckboxProps } from './checkbox';
|
||||
export { CheckboxTristate } from './checkbox-tristate';
|
||||
export { ErrorField } from './error-field';
|
||||
export { FileInput } from './file-input';
|
||||
export { Label } from './label';
|
||||
export { SearchBar } from './search-bar';
|
||||
export { SelectMulti, type SelectMultiProps } from './select-multi';
|
||||
export { SelectSingle, type SelectSingleProps } from './select-single';
|
||||
export { SelectTree } from './select-tree';
|
||||
export { TextArea } from './text-area';
|
||||
export { TextInput } from './text-input';
|
|
@ -1,3 +0,0 @@
|
|||
export { ModalForm } from './modal-form';
|
||||
export { ModalLoader } from './modal-loader';
|
||||
export { ModalView } from './modal-view';
|
|
@ -1,103 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { BadgeHelp } from '@/features/help/components';
|
||||
|
||||
import { useEscapeKey } from '@/hooks/use-escape-key';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { prepareTooltip } from '@/utils/utils';
|
||||
|
||||
import { Button, MiniButton } from '../control';
|
||||
import { IconClose } from '../icons';
|
||||
|
||||
import { ModalBackdrop } from './modal-backdrop';
|
||||
import { type ModalProps } from './modal-form';
|
||||
|
||||
interface ModalViewProps extends ModalProps {
|
||||
/** Float all UI elements on top of contents. */
|
||||
fullScreen?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a customizable modal window with submit form.
|
||||
*/
|
||||
export function ModalView({
|
||||
children,
|
||||
className,
|
||||
header,
|
||||
overflowVisible,
|
||||
helpTopic,
|
||||
hideHelpWhen,
|
||||
fullScreen,
|
||||
...restProps
|
||||
}: React.PropsWithChildren<ModalViewProps>) {
|
||||
const hideDialog = useDialogsStore(state => state.hideDialog);
|
||||
useEscapeKey(hideDialog);
|
||||
|
||||
return (
|
||||
<div className='cc-modal-wrapper'>
|
||||
<ModalBackdrop onHide={hideDialog} />
|
||||
<div className='cc-animate-modal relative grid border rounded-xl bg-prim-100' role='dialog'>
|
||||
{helpTopic && !hideHelpWhen?.() ? (
|
||||
<BadgeHelp
|
||||
topic={helpTopic}
|
||||
className='absolute z-pop top-2 left-2'
|
||||
padding='p-0'
|
||||
contentClass='sm:max-w-160'
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<MiniButton
|
||||
noPadding
|
||||
aria-label='Закрыть'
|
||||
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
|
||||
icon={<IconClose size='1.25rem' />}
|
||||
className='absolute z-pop top-2 right-2'
|
||||
onClick={hideDialog}
|
||||
/>
|
||||
|
||||
{header ? (
|
||||
<h1
|
||||
className={clsx(
|
||||
'px-12 py-2 select-none',
|
||||
fullScreen && 'z-pop absolute top-0 right-1/2 translate-x-1/2 backdrop-blur-xs bg-prim-100/90 rounded-2xl'
|
||||
)}
|
||||
>
|
||||
{header}
|
||||
</h1>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
'@container/modal',
|
||||
'max-h-[calc(100svh-8rem)] max-w-[100svw] xs:max-w-[calc(100svw-2rem)]',
|
||||
'overscroll-contain outline-hidden',
|
||||
overflowVisible ? 'overflow-visible' : 'overflow-auto',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{!fullScreen ? (
|
||||
<Button
|
||||
text='Закрыть'
|
||||
aria-label='Закрыть'
|
||||
className={clsx(
|
||||
'my-2 mx-auto text-sm min-w-28',
|
||||
fullScreen && 'z-pop absolute bottom-0 right-1/2 translate-x-1/2'
|
||||
)}
|
||||
onClick={hideDialog}
|
||||
/>
|
||||
) : (
|
||||
<div className='z-pop absolute bottom-0 right-1/2 translate-x-1/2 p-3 rounded-xl bg-prim-100/90 backdrop-blur-xs'>
|
||||
{' '}
|
||||
<Button text='Закрыть' aria-label='Закрыть' className='text-sm min-w-28' onClick={hideDialog} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
export { EmbedYoutube } from './embed-youtube';
|
||||
export { Indicator } from './indicator';
|
||||
export { NoData } from './no-data';
|
||||
export { PDFViewer } from './pdf-viewer';
|
||||
export { PrettyJson } from './pretty-json';
|
||||
export { TextContent } from './text-content';
|
||||
export { ValueIcon } from './value-icon';
|
||||
export { ValueLabeled } from './value-labeled';
|
||||
export { ValueStats } from './value-stats';
|
|
@ -1,6 +1,6 @@
|
|||
import { queryOptions } from '@tanstack/react-query';
|
||||
|
||||
import { axiosGet, axiosPatch, axiosPost } from '@/backend/api-transport';
|
||||
import { axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport';
|
||||
import { DELAYS, KEYS } from '@/backend/configuration';
|
||||
import { infoMsg } from '@/utils/labels';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
||||
|
||||
import { queryClient } from '@/backend/query-client';
|
||||
import { queryClient } from '@/backend/queryClient';
|
||||
|
||||
import { authApi } from './api';
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user