Compare commits
24 Commits
ff18a22b14
...
65c210b047
Author | SHA1 | Date | |
---|---|---|---|
![]() |
65c210b047 | ||
![]() |
ed89591af9 | ||
![]() |
a60f63455c | ||
![]() |
503e447073 | ||
![]() |
a2dd94637b | ||
![]() |
4ec2272c5f | ||
![]() |
023987d511 | ||
![]() |
342c315c45 | ||
![]() |
ab551a3ece | ||
![]() |
ea97a7a075 | ||
![]() |
85faab2078 | ||
![]() |
aac1b072bc | ||
![]() |
cb178e69cd | ||
![]() |
fb23c32ca8 | ||
![]() |
2b0f074ffa | ||
![]() |
f5cf5af7d7 | ||
![]() |
af415d27b4 | ||
![]() |
15cd3fb306 | ||
![]() |
92d3d2676b | ||
![]() |
c04ea8993e | ||
![]() |
1643cd737c | ||
![]() |
3d63c25845 | ||
![]() |
aea9dececf | ||
![]() |
7c89a04255 |
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
@ -114,6 +114,7 @@
|
||||||
"lezer",
|
"lezer",
|
||||||
"Litr",
|
"Litr",
|
||||||
"loct",
|
"loct",
|
||||||
|
"mgraph",
|
||||||
"moprho",
|
"moprho",
|
||||||
"multiword",
|
"multiword",
|
||||||
"mypy",
|
"mypy",
|
||||||
|
@ -154,6 +155,8 @@
|
||||||
"rsforms",
|
"rsforms",
|
||||||
"rsgraph",
|
"rsgraph",
|
||||||
"rslang",
|
"rslang",
|
||||||
|
"rslist",
|
||||||
|
"rstabs",
|
||||||
"rstemplates",
|
"rstemplates",
|
||||||
"setexpr",
|
"setexpr",
|
||||||
"SIDELIST",
|
"SIDELIST",
|
||||||
|
|
2
TODO.txt
2
TODO.txt
|
@ -67,3 +67,5 @@ https://drf-standardized-errors.readthedocs.io/en/latest/error_response.html
|
||||||
https://stackoverflow.com/questions/28838170/multilevel-json-diff-in-python
|
https://stackoverflow.com/questions/28838170/multilevel-json-diff-in-python
|
||||||
|
|
||||||
- Documentation platform. Consider diplodoc
|
- Documentation platform. Consider diplodoc
|
||||||
|
|
||||||
|
- nuqs useQueryState
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"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",
|
"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",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:e2e": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
"dev": "vite --host",
|
"dev": "vite --host",
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
export { Navigation } from './Navigation';
|
|
|
@ -1,23 +1,23 @@
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
import { Outlet } from 'react-router';
|
import { Outlet } from 'react-router';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { ModalLoader } from '@/components/Modal';
|
import { ModalLoader } from '@/components/modal';
|
||||||
import { useAppLayoutStore, useMainHeight, useViewportHeight } from '@/stores/appLayout';
|
import { useAppLayoutStore, useMainHeight, useViewportHeight } from '@/stores/app-layout';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
|
||||||
import { NavigationState } from './Navigation/NavigationContext';
|
import { NavigationState } from './navigation/navigation-context';
|
||||||
import { Footer } from './Footer';
|
import { Footer } from './footer';
|
||||||
import { GlobalDialogs } from './GlobalDialogs';
|
import { GlobalDialogs } from './global-dialogs';
|
||||||
import { GlobalLoader } from './GlobalLoader';
|
import { GlobalLoader } from './global-Loader';
|
||||||
import { ToasterThemed } from './GlobalToaster';
|
import { ToasterThemed } from './global-toaster';
|
||||||
import { GlobalTooltips } from './GlobalTooltips';
|
import { GlobalTooltips } from './global-tooltips';
|
||||||
import { MutationErrors } from './MutationErrors';
|
import { MutationErrors } from './mutation-errors';
|
||||||
import { Navigation } from './Navigation';
|
import { Navigation } from './navigation';
|
||||||
|
|
||||||
export function ApplicationLayout() {
|
export function ApplicationLayout() {
|
||||||
const mainHeight = useMainHeight();
|
const mainHeight = useMainHeight();
|
||||||
const viewportHeight = useViewportHeight();
|
const viewportHeight = useViewportHeight();
|
||||||
const showScroll = useAppLayoutStore(state => !state.noScroll);
|
|
||||||
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
|
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
|
||||||
const noNavigation = useAppLayoutStore(state => state.noNavigation);
|
const noNavigation = useAppLayoutStore(state => state.noNavigation);
|
||||||
const noFooter = useAppLayoutStore(state => state.noFooter);
|
const noFooter = useAppLayoutStore(state => state.noFooter);
|
||||||
|
@ -27,8 +27,7 @@ export function ApplicationLayout() {
|
||||||
<NavigationState>
|
<NavigationState>
|
||||||
<div className='min-w-80 antialiased h-full max-w-480 mx-auto'>
|
<div className='min-w-80 antialiased h-full max-w-480 mx-auto'>
|
||||||
<ToasterThemed
|
<ToasterThemed
|
||||||
className='text-[14px]'
|
className={clsx('sm:text-[14px]/[20px] text-[12px]/[16px]', noNavigationAnimation ? 'mt-6' : 'mt-14')}
|
||||||
style={{ marginTop: noNavigationAnimation ? '1.5rem' : '3.5rem' }}
|
|
||||||
autoClose={3000}
|
autoClose={3000}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
pauseOnFocusLoss={false}
|
pauseOnFocusLoss={false}
|
||||||
|
@ -46,7 +45,7 @@ export function ApplicationLayout() {
|
||||||
style={{ maxHeight: viewportHeight }}
|
style={{ maxHeight: viewportHeight }}
|
||||||
inert={activeDialog !== null}
|
inert={activeDialog !== null}
|
||||||
>
|
>
|
||||||
<main className='cc-scroll-y' style={{ overflowY: showScroll ? 'scroll' : 'auto', minHeight: mainHeight }}>
|
<main className='cc-scroll-y overflow-y-auto' style={{ minHeight: mainHeight }}>
|
||||||
<GlobalLoader />
|
<GlobalLoader />
|
||||||
<MutationErrors />
|
<MutationErrors />
|
||||||
<Outlet />
|
<Outlet />
|
|
@ -1,7 +1,7 @@
|
||||||
import { useNavigate, useRouteError } from 'react-router';
|
import { useNavigate, useRouteError } from 'react-router';
|
||||||
|
|
||||||
import { Button } from '@/components/Control';
|
import { Button } from '@/components/control';
|
||||||
import { InfoError } from '@/components/InfoError';
|
import { InfoError } from '@/components/info-error';
|
||||||
|
|
||||||
export function ErrorFallback() {
|
export function ErrorFallback() {
|
||||||
const error = useRouteError();
|
const error = useRouteError();
|
|
@ -1,6 +1,6 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { TextURL } from '@/components/Control';
|
import { TextURL } from '@/components/control';
|
||||||
import { external_urls } from '@/utils/constants';
|
import { external_urls } from '@/utils/constants';
|
||||||
|
|
||||||
export function Footer() {
|
export function Footer() {
|
|
@ -1,8 +1,8 @@
|
||||||
import { useNavigation } from 'react-router';
|
import { useNavigation } from 'react-router';
|
||||||
import { useDebounce } from 'use-debounce';
|
import { useDebounce } from 'use-debounce';
|
||||||
|
|
||||||
import { Loader } from '@/components/Loader';
|
import { Loader } from '@/components/loader';
|
||||||
import { ModalBackdrop } from '@/components/Modal/ModalBackdrop';
|
import { ModalBackdrop } from '@/components/modal/modal-backdrop';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
|
||||||
export function GlobalLoader() {
|
export function GlobalLoader() {
|
||||||
|
@ -18,7 +18,7 @@ export function GlobalLoader() {
|
||||||
return (
|
return (
|
||||||
<div className='cc-modal-wrapper'>
|
<div className='cc-modal-wrapper'>
|
||||||
<ModalBackdrop />
|
<ModalBackdrop />
|
||||||
<div className='cc-fade-in px-10 border rounded-xl bg-prim-100'>
|
<div className='z-pop cc-fade-in px-10 border rounded-xl bg-prim-100'>
|
||||||
<Loader scale={6} />
|
<Loader scale={6} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -5,113 +5,113 @@ import React from 'react';
|
||||||
import { DialogType, useDialogsStore } from '@/stores/dialogs';
|
import { DialogType, useDialogsStore } from '@/stores/dialogs';
|
||||||
|
|
||||||
const DlgChangeInputSchema = React.lazy(() =>
|
const DlgChangeInputSchema = React.lazy(() =>
|
||||||
import('@/features/oss/dialogs/DlgChangeInputSchema').then(module => ({ default: module.DlgChangeInputSchema }))
|
import('@/features/oss/dialogs/dlg-change-input-schema').then(module => ({ default: module.DlgChangeInputSchema }))
|
||||||
);
|
);
|
||||||
const DlgChangeLocation = React.lazy(() =>
|
const DlgChangeLocation = React.lazy(() =>
|
||||||
import('@/features/library/dialogs/DlgChangeLocation').then(module => ({
|
import('@/features/library/dialogs/dlg-change-location').then(module => ({
|
||||||
default: module.DlgChangeLocation
|
default: module.DlgChangeLocation
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const DlgCloneLibraryItem = React.lazy(() =>
|
const DlgCloneLibraryItem = React.lazy(() =>
|
||||||
import('@/features/library/dialogs/DlgCloneLibraryItem').then(module => ({
|
import('@/features/library/dialogs/dlg-clone-library-item').then(module => ({
|
||||||
default: module.DlgCloneLibraryItem
|
default: module.DlgCloneLibraryItem
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const DlgCreateCst = React.lazy(() =>
|
const DlgCreateCst = React.lazy(() =>
|
||||||
import('@/features/rsform/dialogs/DlgCreateCst').then(module => ({ default: module.DlgCreateCst }))
|
import('@/features/rsform/dialogs/dlg-create-cst').then(module => ({ default: module.DlgCreateCst }))
|
||||||
);
|
);
|
||||||
const DlgCreateOperation = React.lazy(() =>
|
const DlgCreateOperation = React.lazy(() =>
|
||||||
import('@/features/oss/dialogs/DlgCreateOperation').then(module => ({
|
import('@/features/oss/dialogs/dlg-create-operation').then(module => ({
|
||||||
default: module.DlgCreateOperation
|
default: module.DlgCreateOperation
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const DlgCreateVersion = React.lazy(() =>
|
const DlgCreateVersion = React.lazy(() =>
|
||||||
import('@/features/library/dialogs/DlgCreateVersion').then(module => ({
|
import('@/features/library/dialogs/dlg-create-version').then(module => ({
|
||||||
default: module.DlgCreateVersion
|
default: module.DlgCreateVersion
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const DlgCstTemplate = React.lazy(() =>
|
const DlgCstTemplate = React.lazy(() =>
|
||||||
import('@/features/rsform/dialogs/DlgCstTemplate').then(module => ({
|
import('@/features/rsform/dialogs/dlg-cst-template').then(module => ({
|
||||||
default: module.DlgCstTemplate
|
default: module.DlgCstTemplate
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const DlgDeleteCst = React.lazy(() =>
|
const DlgDeleteCst = React.lazy(() =>
|
||||||
import('@/features/rsform/dialogs/DlgDeleteCst').then(module => ({
|
import('@/features/rsform/dialogs/dlg-delete-cst').then(module => ({
|
||||||
default: module.DlgDeleteCst
|
default: module.DlgDeleteCst
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const DlgDeleteOperation = React.lazy(() =>
|
const DlgDeleteOperation = React.lazy(() =>
|
||||||
import('@/features/oss/dialogs/DlgDeleteOperation').then(module => ({
|
import('@/features/oss/dialogs/dlg-delete-operation').then(module => ({
|
||||||
default: module.DlgDeleteOperation
|
default: module.DlgDeleteOperation
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const DlgEditEditors = React.lazy(() =>
|
const DlgEditEditors = React.lazy(() =>
|
||||||
import('@/features/library/dialogs/DlgEditEditors').then(module => ({
|
import('@/features/library/dialogs/dlg-edit-editors').then(module => ({
|
||||||
default: module.DlgEditEditors
|
default: module.DlgEditEditors
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const DlgEditOperation = React.lazy(() =>
|
const DlgEditOperation = React.lazy(() =>
|
||||||
import('@/features/oss/dialogs/DlgEditOperation').then(module => ({
|
import('@/features/oss/dialogs/dlg-edit-operation').then(module => ({
|
||||||
default: module.DlgEditOperation
|
default: module.DlgEditOperation
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const DlgEditReference = React.lazy(() =>
|
const DlgEditReference = React.lazy(() =>
|
||||||
import('@/features/rsform/dialogs/DlgEditReference').then(module => ({
|
import('@/features/rsform/dialogs/dlg-edit-reference').then(module => ({
|
||||||
default: module.DlgEditReference
|
default: module.DlgEditReference
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const DlgEditVersions = React.lazy(() =>
|
const DlgEditVersions = React.lazy(() =>
|
||||||
import('@/features/library/dialogs/DlgEditVersions').then(module => ({
|
import('@/features/library/dialogs/dlg-edit-versions').then(module => ({
|
||||||
default: module.DlgEditVersions
|
default: module.DlgEditVersions
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const DlgEditWordForms = React.lazy(() =>
|
const DlgEditWordForms = React.lazy(() =>
|
||||||
import('@/features/rsform/dialogs/DlgEditWordForms').then(module => ({
|
import('@/features/rsform/dialogs/dlg-edit-word-forms').then(module => ({
|
||||||
default: module.DlgEditWordForms
|
default: module.DlgEditWordForms
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const DlgInlineSynthesis = React.lazy(() =>
|
const DlgInlineSynthesis = React.lazy(() =>
|
||||||
import('@/features/rsform/dialogs/DlgInlineSynthesis').then(module => ({
|
import('@/features/rsform/dialogs/dlg-inline-synthesis').then(module => ({
|
||||||
default: module.DlgInlineSynthesis
|
default: module.DlgInlineSynthesis
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const DlgRelocateConstituents = React.lazy(() =>
|
const DlgRelocateConstituents = React.lazy(() =>
|
||||||
import('@/features/oss/dialogs/DlgRelocateConstituents').then(module => ({
|
import('@/features/oss/dialogs/dlg-relocate-constituents').then(module => ({
|
||||||
default: module.DlgRelocateConstituents
|
default: module.DlgRelocateConstituents
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const DlgRenameCst = React.lazy(() =>
|
const DlgRenameCst = React.lazy(() =>
|
||||||
import('@/features/rsform/dialogs/DlgRenameCst').then(module => ({
|
import('@/features/rsform/dialogs/dlg-rename-cst').then(module => ({
|
||||||
default: module.DlgRenameCst
|
default: module.DlgRenameCst
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const DlgShowAST = React.lazy(() =>
|
const DlgShowAST = React.lazy(() =>
|
||||||
import('@/features/rsform/dialogs/DlgShowAST').then(module => ({
|
import('@/features/rsform/dialogs/dlg-show-ast').then(module => ({
|
||||||
default: module.DlgShowAST
|
default: module.DlgShowAST
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const DlgShowQR = React.lazy(() =>
|
const DlgShowQR = React.lazy(() =>
|
||||||
import('@/features/rsform/dialogs/DlgShowQR').then(module => ({
|
import('@/features/rsform/dialogs/dlg-show-qr').then(module => ({
|
||||||
default: module.DlgShowQR
|
default: module.DlgShowQR
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const DlgShowTypeGraph = React.lazy(() =>
|
const DlgShowTypeGraph = React.lazy(() =>
|
||||||
import('@/features/rsform/dialogs/DlgShowTypeGraph').then(module => ({
|
import('@/features/rsform/dialogs/dlg-show-type-graph').then(module => ({
|
||||||
default: module.DlgShowTypeGraph
|
default: module.DlgShowTypeGraph
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const DlgSubstituteCst = React.lazy(() =>
|
const DlgSubstituteCst = React.lazy(() =>
|
||||||
import('@/features/rsform/dialogs/DlgSubstituteCst').then(module => ({
|
import('@/features/rsform/dialogs/dlg-substitute-cst').then(module => ({
|
||||||
default: module.DlgSubstituteCst
|
default: module.DlgSubstituteCst
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const DlgUploadRSForm = React.lazy(() =>
|
const DlgUploadRSForm = React.lazy(() =>
|
||||||
import('@/features/rsform/dialogs/DlgUploadRSForm').then(module => ({
|
import('@/features/rsform/dialogs/dlg-upload-rsform').then(module => ({
|
||||||
default: module.DlgUploadRSForm
|
default: module.DlgUploadRSForm
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const DlgGraphParams = React.lazy(() =>
|
const DlgGraphParams = React.lazy(() =>
|
||||||
import('@/features/rsform/dialogs/DlgGraphParams').then(module => ({ default: module.DlgGraphParams }))
|
import('@/features/rsform/dialogs/dlg-graph-params').then(module => ({ default: module.DlgGraphParams }))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const GlobalDialogs = () => {
|
export const GlobalDialogs = () => {
|
|
@ -4,7 +4,7 @@ import { IntlProvider } from 'react-intl';
|
||||||
import { QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||||
|
|
||||||
import { queryClient } from '@/backend/queryClient';
|
import { queryClient } from '@/backend/query-client';
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
export function GlobalProviders({ children }: React.PropsWithChildren) {
|
export function GlobalProviders({ children }: React.PropsWithChildren) {
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Tooltip } from '@/components/Container';
|
import { Tooltip } from '@/components/container';
|
||||||
import { globalIDs } from '@/utils/constants';
|
import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
export const GlobalTooltips = () => {
|
export const GlobalTooltips = () => {
|
|
@ -1,9 +1,9 @@
|
||||||
export { useConceptNavigation } from './Navigation/NavigationContext';
|
export { useConceptNavigation } from './navigation/navigation-context';
|
||||||
export { useBlockNavigation } from './Navigation/NavigationContext';
|
export { useBlockNavigation } from './navigation/navigation-context';
|
||||||
export { urls } from './urls';
|
export { urls } from './urls';
|
||||||
import { RouterProvider } from 'react-router';
|
import { RouterProvider } from 'react-router';
|
||||||
|
|
||||||
import { Router } from './Router';
|
import { Router } from './router';
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return <RouterProvider router={Router} />;
|
return <RouterProvider router={Router} />;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { useMutationErrors } from '@/backend/useMutationErrors';
|
import { useMutationErrors } from '@/backend/use-mutation-errors';
|
||||||
import { Button } from '@/components/Control';
|
import { Button } from '@/components/control';
|
||||||
import { DescribeError } from '@/components/InfoError';
|
import { DescribeError } from '@/components/info-error';
|
||||||
import { ModalBackdrop } from '@/components/Modal/ModalBackdrop';
|
import { ModalBackdrop } from '@/components/modal/modal-backdrop';
|
||||||
import { useEscapeKey } from '@/hooks/useEscapeKey';
|
import { useEscapeKey } from '@/hooks/use-escape-key';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
|
||||||
export function MutationErrors() {
|
export function MutationErrors() {
|
||||||
|
@ -20,7 +20,7 @@ export function MutationErrors() {
|
||||||
return (
|
return (
|
||||||
<div className='cc-modal-wrapper'>
|
<div className='cc-modal-wrapper'>
|
||||||
<ModalBackdrop onHide={resetErrors} />
|
<ModalBackdrop onHide={resetErrors} />
|
||||||
<div className='px-10 py-3 flex flex-col items-center border rounded-xl bg-prim-100' role='alertdialog'>
|
<div className='z-pop px-10 py-3 flex flex-col items-center border rounded-xl bg-prim-100' role='alertdialog'>
|
||||||
<h1 className='py-2 select-none'>Ошибка при обработке</h1>
|
<h1 className='py-2 select-none'>Ошибка при обработке</h1>
|
||||||
<div className='px-3 flex flex-col text-warn-600 text-sm font-semibold select-text'>
|
<div className='px-3 flex flex-col text-warn-600 text-sm font-semibold select-text'>
|
||||||
<DescribeError error={mutationErrors[0]} />
|
<DescribeError error={mutationErrors[0]} />
|
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,4 +1,4 @@
|
||||||
import { useWindowSize } from '@/hooks/useWindowSize';
|
import { useWindowSize } from '@/hooks/use-window-size';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
|
|
||||||
export function Logo() {
|
export function Logo() {
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { createContext, use, useEffect, useState } from 'react';
|
import { createContext, use, useEffect, useRef, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
export interface NavigationProps {
|
export interface NavigationProps {
|
||||||
|
@ -19,8 +19,7 @@ interface INavigationContext {
|
||||||
|
|
||||||
canBack: () => boolean;
|
canBack: () => boolean;
|
||||||
|
|
||||||
isBlocked: boolean;
|
setRequireConfirmation: (value: boolean) => void;
|
||||||
setIsBlocked: (value: boolean) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NavigationContext = createContext<INavigationContext | null>(null);
|
export const NavigationContext = createContext<INavigationContext | null>(null);
|
||||||
|
@ -35,11 +34,11 @@ export const useConceptNavigation = () => {
|
||||||
export const NavigationState = ({ children }: React.PropsWithChildren) => {
|
export const NavigationState = ({ children }: React.PropsWithChildren) => {
|
||||||
const router = useNavigate();
|
const router = useNavigate();
|
||||||
|
|
||||||
const [isBlocked, setIsBlocked] = useState(false);
|
const isBlocked = useRef(false);
|
||||||
const [internalNavigation, setInternalNavigation] = useState(false);
|
const [internalNavigation, setInternalNavigation] = useState(false);
|
||||||
|
|
||||||
function validate() {
|
function validate() {
|
||||||
return !isBlocked || confirm('Изменения не сохранены. Вы уверены что хотите совершить переход?');
|
return !isBlocked.current || confirm('Изменения не сохранены. Вы уверены что хотите совершить переход?');
|
||||||
}
|
}
|
||||||
|
|
||||||
function canBack() {
|
function canBack() {
|
||||||
|
@ -50,7 +49,7 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
|
||||||
if (props.newTab) {
|
if (props.newTab) {
|
||||||
window.open(`${props.path}`, '_blank');
|
window.open(`${props.path}`, '_blank');
|
||||||
} else if (props.force || validate()) {
|
} else if (props.force || validate()) {
|
||||||
setIsBlocked(false);
|
isBlocked.current = false;
|
||||||
setInternalNavigation(true);
|
setInternalNavigation(true);
|
||||||
Promise.resolve(router(props.path, { viewTransition: true })).catch(console.error);
|
Promise.resolve(router(props.path, { viewTransition: true })).catch(console.error);
|
||||||
}
|
}
|
||||||
|
@ -60,7 +59,7 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
|
||||||
if (props.newTab) {
|
if (props.newTab) {
|
||||||
window.open(`${props.path}`, '_blank');
|
window.open(`${props.path}`, '_blank');
|
||||||
} else if (props.force || validate()) {
|
} else if (props.force || validate()) {
|
||||||
setIsBlocked(false);
|
isBlocked.current = false;
|
||||||
setInternalNavigation(true);
|
setInternalNavigation(true);
|
||||||
return router(props.path, { viewTransition: true });
|
return router(props.path, { viewTransition: true });
|
||||||
}
|
}
|
||||||
|
@ -68,14 +67,14 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
|
||||||
|
|
||||||
function replace(props: Omit<NavigationProps, 'newTab'>) {
|
function replace(props: Omit<NavigationProps, 'newTab'>) {
|
||||||
if (props.force || validate()) {
|
if (props.force || validate()) {
|
||||||
setIsBlocked(false);
|
isBlocked.current = false;
|
||||||
Promise.resolve(router(props.path, { replace: true, viewTransition: true })).catch(console.error);
|
Promise.resolve(router(props.path, { replace: true, viewTransition: true })).catch(console.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function replaceAsync(props: Omit<NavigationProps, 'newTab'>): void | Promise<void> {
|
function replaceAsync(props: Omit<NavigationProps, 'newTab'>): void | Promise<void> {
|
||||||
if (props.force || validate()) {
|
if (props.force || validate()) {
|
||||||
setIsBlocked(false);
|
isBlocked.current = false;
|
||||||
return router(props.path, { replace: true, viewTransition: true });
|
return router(props.path, { replace: true, viewTransition: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,14 +82,14 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
|
||||||
function back(force?: boolean) {
|
function back(force?: boolean) {
|
||||||
if (force || validate()) {
|
if (force || validate()) {
|
||||||
Promise.resolve(router(-1)).catch(console.error);
|
Promise.resolve(router(-1)).catch(console.error);
|
||||||
setIsBlocked(false);
|
isBlocked.current = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function forward(force?: boolean) {
|
function forward(force?: boolean) {
|
||||||
if (force || validate()) {
|
if (force || validate()) {
|
||||||
Promise.resolve(router(1)).catch(console.error);
|
Promise.resolve(router(1)).catch(console.error);
|
||||||
setIsBlocked(false);
|
isBlocked.current = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,8 +103,7 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
|
||||||
back,
|
back,
|
||||||
forward,
|
forward,
|
||||||
canBack,
|
canBack,
|
||||||
isBlocked,
|
setRequireConfirmation: (value: boolean) => (isBlocked.current = value)
|
||||||
setIsBlocked
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
@ -114,9 +112,9 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useBlockNavigation(isBlocked: boolean) {
|
export function useBlockNavigation(isBlocked: boolean) {
|
||||||
const router = useConceptNavigation();
|
const { setRequireConfirmation } = useConceptNavigation();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
router.setIsBlocked(isBlocked);
|
setRequireConfirmation(isBlocked);
|
||||||
return () => router.setIsBlocked(false);
|
return () => setRequireConfirmation(false);
|
||||||
}, [router, isBlocked]);
|
}, [setRequireConfirmation, isBlocked]);
|
||||||
}
|
}
|
|
@ -1,14 +1,16 @@
|
||||||
import { IconLibrary2, IconManuals, IconNewItem2 } from '@/components/Icons';
|
import clsx from 'clsx';
|
||||||
import { useWindowSize } from '@/hooks/useWindowSize';
|
|
||||||
import { useAppLayoutStore } from '@/stores/appLayout';
|
import { IconLibrary2, IconManuals, IconNewItem2 } from '@/components/icons';
|
||||||
|
import { useWindowSize } from '@/hooks/use-window-size';
|
||||||
|
import { useAppLayoutStore } from '@/stores/app-layout';
|
||||||
|
|
||||||
import { urls } from '../urls';
|
import { urls } from '../urls';
|
||||||
|
|
||||||
import { Logo } from './Logo';
|
import { Logo } from './logo';
|
||||||
import { NavigationButton } from './NavigationButton';
|
import { NavigationButton } from './navigation-button';
|
||||||
import { useConceptNavigation } from './NavigationContext';
|
import { useConceptNavigation } from './navigation-context';
|
||||||
import { ToggleNavigation } from './ToggleNavigation';
|
import { ToggleNavigation } from './toggle-navigation';
|
||||||
import { UserMenu } from './UserMenu';
|
import { UserMenu } from './user-menu';
|
||||||
|
|
||||||
export function Navigation() {
|
export function Navigation() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
|
@ -28,11 +30,11 @@ export function Navigation() {
|
||||||
<nav className='z-navigation sticky top-0 left-0 right-0 select-none bg-prim-100'>
|
<nav className='z-navigation sticky top-0 left-0 right-0 select-none bg-prim-100'>
|
||||||
<ToggleNavigation />
|
<ToggleNavigation />
|
||||||
<div
|
<div
|
||||||
className='pl-2 pr-6 sm:pr-4 h-12 flex cc-shadow-border'
|
className={clsx(
|
||||||
style={{
|
'pl-2 pr-6 sm:pr-4 h-12 flex cc-shadow-border',
|
||||||
maxHeight: noNavigationAnimation ? '0rem' : '3rem',
|
'transition-[max-height,translate] ease-bezier duration-(--duration-move)',
|
||||||
translate: noNavigationAnimation ? '0 -1.5rem' : '0'
|
noNavigationAnimation ? '-translate-y-6 max-h-0' : 'max-h-12'
|
||||||
}}
|
)}
|
||||||
>
|
>
|
||||||
<div className='flex items-center mr-auto cursor-pointer' onClick={!size.isSmall ? navigateHome : undefined}>
|
<div className='flex items-center mr-auto cursor-pointer' onClick={!size.isSmall ? navigateHome : undefined}>
|
||||||
<Logo />
|
<Logo />
|
|
@ -1,7 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { IconDarkTheme, IconLightTheme, IconPin, IconUnpin } from '@/components/Icons';
|
import { IconDarkTheme, IconLightTheme, IconPin, IconUnpin } from '@/components/icons';
|
||||||
import { useAppLayoutStore } from '@/stores/appLayout';
|
import { useAppLayoutStore } from '@/stores/app-layout';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import { globalIDs } from '@/utils/constants';
|
import { globalIDs } from '@/utils/constants';
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { useAuthSuspense } from '@/features/auth';
|
import { useAuthSuspense } from '@/features/auth';
|
||||||
|
|
||||||
import { IconLogin, IconUser2 } from '@/components/Icons';
|
import { IconLogin, IconUser2 } from '@/components/icons';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import { globalIDs } from '@/utils/constants';
|
import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
import { NavigationButton } from './NavigationButton';
|
import { NavigationButton } from './navigation-button';
|
||||||
|
|
||||||
interface UserButtonProps {
|
interface UserButtonProps {
|
||||||
onLogin: () => void;
|
onLogin: () => void;
|
|
@ -1,7 +1,7 @@
|
||||||
import { useAuthSuspense } from '@/features/auth';
|
import { useAuthSuspense } from '@/features/auth';
|
||||||
import { useLogout } from '@/features/auth/backend/useLogout';
|
import { useLogout } from '@/features/auth/backend/use-logout';
|
||||||
|
|
||||||
import { Dropdown, DropdownButton } from '@/components/Dropdown';
|
import { Dropdown, DropdownButton } from '@/components/dropdown';
|
||||||
import {
|
import {
|
||||||
IconAdmin,
|
IconAdmin,
|
||||||
IconAdminOff,
|
IconAdminOff,
|
||||||
|
@ -15,13 +15,13 @@ import {
|
||||||
IconLogout,
|
IconLogout,
|
||||||
IconRESTapi,
|
IconRESTapi,
|
||||||
IconUser
|
IconUser
|
||||||
} from '@/components/Icons';
|
} from '@/components/icons';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import { globalIDs } from '@/utils/constants';
|
import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
import { urls } from '../urls';
|
import { urls } from '../urls';
|
||||||
|
|
||||||
import { useConceptNavigation } from './NavigationContext';
|
import { useConceptNavigation } from './navigation-context';
|
||||||
|
|
||||||
interface UserDropdownProps {
|
interface UserDropdownProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
|
@ -1,13 +1,13 @@
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
import { useDropdown } from '@/components/Dropdown';
|
import { useDropdown } from '@/components/dropdown';
|
||||||
import { Loader } from '@/components/Loader';
|
import { Loader } from '@/components/loader';
|
||||||
|
|
||||||
import { urls } from '../urls';
|
import { urls } from '../urls';
|
||||||
|
|
||||||
import { useConceptNavigation } from './NavigationContext';
|
import { useConceptNavigation } from './navigation-context';
|
||||||
import { UserButton } from './UserButton';
|
import { UserButton } from './user-button';
|
||||||
import { UserDropdown } from './UserDropdown';
|
import { UserDropdown } from './user-dropdown';
|
||||||
|
|
||||||
export function UserMenu() {
|
export function UserMenu() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
|
@ -1,20 +1,20 @@
|
||||||
import { createBrowserRouter } from 'react-router';
|
import { createBrowserRouter } from 'react-router';
|
||||||
|
|
||||||
import { prefetchAuth } from '@/features/auth/backend/useAuth';
|
import { prefetchAuth } from '@/features/auth/backend/use-auth';
|
||||||
import { LoginPage } from '@/features/auth/pages/LoginPage';
|
import { LoginPage } from '@/features/auth/pages/login-page';
|
||||||
import { HomePage } from '@/features/home/HomePage';
|
import { HomePage } from '@/features/home/home-page';
|
||||||
import { NotFoundPage } from '@/features/home/NotFoundPage';
|
import { NotFoundPage } from '@/features/home/not-found-page';
|
||||||
import { prefetchLibrary } from '@/features/library/backend/useLibrary';
|
import { prefetchLibrary } from '@/features/library/backend/use-library';
|
||||||
import { CreateItemPage } from '@/features/library/pages/CreateItemPage';
|
import { CreateItemPage } from '@/features/library/pages/create-item-page';
|
||||||
import { prefetchOSS } from '@/features/oss/backend/useOSS';
|
import { prefetchOSS } from '@/features/oss/backend/use-oss';
|
||||||
import { prefetchRSForm } from '@/features/rsform/backend/useRSForm';
|
import { prefetchRSForm } from '@/features/rsform/backend/use-rsform';
|
||||||
import { prefetchProfile } from '@/features/users/backend/useProfile';
|
import { prefetchProfile } from '@/features/users/backend/use-profile';
|
||||||
import { prefetchUsers } from '@/features/users/backend/useUsers';
|
import { prefetchUsers } from '@/features/users/backend/use-users';
|
||||||
|
|
||||||
import { Loader } from '@/components/Loader';
|
import { Loader } from '@/components/loader';
|
||||||
|
|
||||||
import { ApplicationLayout } from './ApplicationLayout';
|
import { ApplicationLayout } from './application-layout';
|
||||||
import { ErrorFallback } from './ErrorFallback';
|
import { ErrorFallback } from './error-fallback';
|
||||||
import { routes } from './urls';
|
import { routes } from './urls';
|
||||||
|
|
||||||
export const Router = createBrowserRouter([
|
export const Router = createBrowserRouter([
|
||||||
|
@ -39,25 +39,25 @@ export const Router = createBrowserRouter([
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: routes.signup,
|
path: routes.signup,
|
||||||
lazy: () => import('@/features/users/pages/RegisterPage')
|
lazy: () => import('@/features/users/pages/register-page')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: routes.profile,
|
path: routes.profile,
|
||||||
loader: prefetchProfile,
|
loader: prefetchProfile,
|
||||||
lazy: () => import('@/features/users/pages/UserProfilePage')
|
lazy: () => import('@/features/users/pages/user-profile-page')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: routes.restore_password,
|
path: routes.restore_password,
|
||||||
lazy: () => import('@/features/auth/pages/RestorePasswordPage')
|
lazy: () => import('@/features/auth/pages/restore-password-page')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: routes.password_change,
|
path: routes.password_change,
|
||||||
lazy: () => import('@/features/auth/pages/PasswordChangePage')
|
lazy: () => import('@/features/auth/pages/password-change-page')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: routes.library,
|
path: routes.library,
|
||||||
loader: () => Promise.allSettled([prefetchLibrary(), prefetchUsers()]),
|
loader: () => Promise.allSettled([prefetchLibrary(), prefetchUsers()]),
|
||||||
lazy: () => import('@/features/library/pages/LibraryPage')
|
lazy: () => import('@/features/library/pages/library-page')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: routes.create_schema,
|
path: routes.create_schema,
|
||||||
|
@ -66,24 +66,24 @@ export const Router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: `${routes.rsforms}/:id`,
|
path: `${routes.rsforms}/:id`,
|
||||||
loader: data => prefetchRSForm(parseRSFormURL(data.params.id, data.request.url)),
|
loader: data => prefetchRSForm(parseRSFormURL(data.params.id, data.request.url)),
|
||||||
lazy: () => import('@/features/rsform/pages/RSFormPage')
|
lazy: () => import('@/features/rsform/pages/rsform-page')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${routes.oss}/:id`,
|
path: `${routes.oss}/:id`,
|
||||||
loader: data => prefetchOSS(parseOssURL(data.params.id)),
|
loader: data => prefetchOSS(parseOssURL(data.params.id)),
|
||||||
lazy: () => import('@/features/oss/pages/OssPage')
|
lazy: () => import('@/features/oss/pages/oss-page')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: routes.manuals,
|
path: routes.manuals,
|
||||||
lazy: () => import('@/features/help/pages/ManualsPage')
|
lazy: () => import('@/features/help/pages/manuals-page')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${routes.icons}`,
|
path: `${routes.icons}`,
|
||||||
lazy: () => import('@/features/home/IconsPage')
|
lazy: () => import('@/features/home/icons-page')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${routes.database_schema}`,
|
path: `${routes.database_schema}`,
|
||||||
lazy: () => import('@/features/home/DatabaseSchemaPage')
|
lazy: () => import('@/features/home/database-schema-page')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
* Module: Internal navigation constants.
|
* Module: Internal navigation constants.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { buildConstants } from '@/utils/buildConstants';
|
import { buildConstants } from '@/utils/build-constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Routes.
|
* Routes.
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { toast } from 'react-toastify';
|
||||||
import axios, { type AxiosError, type AxiosRequestConfig } from 'axios';
|
import axios, { type AxiosError, type AxiosRequestConfig } from 'axios';
|
||||||
import { type z, ZodError } from 'zod';
|
import { type z, ZodError } from 'zod';
|
||||||
|
|
||||||
import { buildConstants } from '@/utils/buildConstants';
|
import { buildConstants } from '@/utils/build-constants';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
import { errorMsg } from '@/utils/labels';
|
import { errorMsg } from '@/utils/labels';
|
||||||
import { extractErrorMessage } from '@/utils/utils';
|
import { extractErrorMessage } from '@/utils/utils';
|
|
@ -1,7 +1,7 @@
|
||||||
import { QueryClient } from '@tanstack/react-query';
|
import { QueryClient } from '@tanstack/react-query';
|
||||||
import { type ZodError } from 'zod';
|
import { type ZodError } from 'zod';
|
||||||
|
|
||||||
import { type AxiosError } from './apiTransport';
|
import { type AxiosError } from './api-transport';
|
||||||
import { DELAYS } from './configuration';
|
import { DELAYS } from './configuration';
|
||||||
|
|
||||||
declare module '@tanstack/react-query' {
|
declare module '@tanstack/react-query' {
|
|
@ -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 './MiniButton';
|
|
||||||
export { SelectorButton } from './SelectorButton';
|
|
||||||
export { SubmitButton } from './SubmitButton';
|
|
||||||
export { TextURL } from './TextURL';
|
|
|
@ -1,20 +0,0 @@
|
||||||
'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' />}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,104 +0,0 @@
|
||||||
'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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
'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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
export { Dropdown } from './Dropdown';
|
|
||||||
export { DropdownButton } from './DropdownButton';
|
|
||||||
export { DropdownCheckbox } from './DropdownCheckbox';
|
|
||||||
export { useDropdown } from './useDropdown';
|
|
|
@ -1,11 +0,0 @@
|
||||||
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';
|
|
|
@ -1,83 +0,0 @@
|
||||||
'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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
export { ModalForm } from './ModalForm';
|
|
||||||
export { ModalLoader } from './ModalLoader';
|
|
||||||
export { ModalView } from './ModalView';
|
|
|
@ -1,9 +0,0 @@
|
||||||
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';
|
|
|
@ -17,12 +17,9 @@ export function Divider({ vertical, margins = 'mx-2', className, ...restProps }:
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
margins, //
|
vertical ? 'border-x' : 'border-y', //
|
||||||
className,
|
margins,
|
||||||
{
|
className
|
||||||
'border-x': vertical,
|
|
||||||
'border-y': !vertical
|
|
||||||
}
|
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...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';
|
|
@ -26,7 +26,6 @@ export function Tooltip({
|
||||||
layer = 'z-tooltip',
|
layer = 'z-tooltip',
|
||||||
place = 'bottom',
|
place = 'bottom',
|
||||||
className,
|
className,
|
||||||
style,
|
|
||||||
...restProps
|
...restProps
|
||||||
}: TooltipProps) {
|
}: TooltipProps) {
|
||||||
const darkMode = usePreferencesStore(state => state.darkMode);
|
const darkMode = usePreferencesStore(state => state.darkMode);
|
||||||
|
@ -40,6 +39,7 @@ export function Tooltip({
|
||||||
opacity={1}
|
opacity={1}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'relative',
|
'relative',
|
||||||
|
'py-0.5! px-2!',
|
||||||
'max-h-[calc(100svh-6rem)]',
|
'max-h-[calc(100svh-6rem)]',
|
||||||
'overflow-y-auto overflow-x-hidden sm:overflow-hidden overscroll-contain',
|
'overflow-y-auto overflow-x-hidden sm:overflow-hidden overscroll-contain',
|
||||||
'border shadow-md',
|
'border shadow-md',
|
||||||
|
@ -48,7 +48,6 @@ export function Tooltip({
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
classNameArrow={layer}
|
classNameArrow={layer}
|
||||||
style={{ ...{ paddingTop: '2px', paddingBottom: '2px', paddingLeft: '8px', paddingRight: '8px' }, ...style }}
|
|
||||||
variant={darkMode ? 'dark' : 'light'}
|
variant={darkMode ? 'dark' : 'light'}
|
||||||
place={place}
|
place={place}
|
||||||
{...restProps}
|
{...restProps}
|
|
@ -43,15 +43,10 @@ export function Button({
|
||||||
'inline-flex gap-2 items-center justify-center',
|
'inline-flex gap-2 items-center justify-center',
|
||||||
'font-medium select-none disabled:cursor-auto',
|
'font-medium select-none disabled:cursor-auto',
|
||||||
'clr-btn-default cc-animate-color',
|
'clr-btn-default cc-animate-color',
|
||||||
{
|
dense ? 'px-1' : 'px-3 py-1',
|
||||||
'border rounded-sm': !noBorder,
|
loading ? 'cursor-progress' : 'cursor-pointer',
|
||||||
'px-1': dense,
|
noOutline ? 'outline-hidden' : 'clr-outline',
|
||||||
'px-3 py-1': !dense,
|
!noBorder && 'border rounded-sm',
|
||||||
'cursor-progress': loading,
|
|
||||||
'cursor-pointer': !loading,
|
|
||||||
'outline-hidden': noOutline,
|
|
||||||
'clr-outline': !noOutline
|
|
||||||
},
|
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
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 './mini-button';
|
||||||
|
export { SelectorButton } from './selector-button';
|
||||||
|
export { SubmitButton } from './submit-button';
|
||||||
|
export { TextURL } from './text-url';
|
|
@ -41,11 +41,8 @@ export function MiniButton({
|
||||||
'rounded-lg',
|
'rounded-lg',
|
||||||
'clr-text-controls cc-animate-color',
|
'clr-text-controls cc-animate-color',
|
||||||
'cursor-pointer disabled:cursor-auto',
|
'cursor-pointer disabled:cursor-auto',
|
||||||
{
|
noHover ? 'outline-hidden' : 'clr-hover',
|
||||||
'px-1 py-1': !noPadding,
|
!noPadding && 'px-1 py-1',
|
||||||
'outline-hidden': noHover,
|
|
||||||
'clr-hover': !noHover
|
|
||||||
},
|
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
|
@ -38,10 +38,7 @@ export function SelectorButton({
|
||||||
'text-btn clr-text-controls',
|
'text-btn clr-text-controls',
|
||||||
'disabled:cursor-auto cursor-pointer',
|
'disabled:cursor-auto cursor-pointer',
|
||||||
'cc-animate-color',
|
'cc-animate-color',
|
||||||
{
|
transparent ? 'clr-hover' : 'clr-btn-default border',
|
||||||
'clr-hover': transparent,
|
|
||||||
'clr-btn-default border': !transparent
|
|
||||||
},
|
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
|
@ -5,25 +5,21 @@ import { useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
type ColumnSort,
|
type ColumnSort,
|
||||||
createColumnHelper,
|
createColumnHelper,
|
||||||
getCoreRowModel,
|
|
||||||
getPaginationRowModel,
|
|
||||||
getSortedRowModel,
|
|
||||||
type PaginationState,
|
|
||||||
type RowData,
|
type RowData,
|
||||||
type RowSelectionState,
|
type RowSelectionState,
|
||||||
type SortingState,
|
|
||||||
type TableOptions,
|
type TableOptions,
|
||||||
useReactTable,
|
|
||||||
type VisibilityState
|
type VisibilityState
|
||||||
} from '@tanstack/react-table';
|
} from '@tanstack/react-table';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { type Styling } from '../props';
|
import { type Styling } from '../props';
|
||||||
|
|
||||||
import { DefaultNoData } from './DefaultNoData';
|
import { DefaultNoData } from './default-no-data';
|
||||||
import { PaginationTools } from './PaginationTools';
|
import { PaginationTools } from './pagination-tools';
|
||||||
import { TableBody } from './TableBody';
|
import { TableBody } from './table-body';
|
||||||
import { TableFooter } from './TableFooter';
|
import { TableFooter } from './table-footer';
|
||||||
import { TableHeader } from './TableHeader';
|
import { TableHeader } from './table-header';
|
||||||
|
import { useDataTable } from './use-data-table';
|
||||||
|
|
||||||
export { type ColumnSort, createColumnHelper, type RowSelectionState, type VisibilityState };
|
export { type ColumnSort, createColumnHelper, type RowSelectionState, type VisibilityState };
|
||||||
|
|
||||||
|
@ -124,49 +120,16 @@ export function DataTable<TData extends RowData>({
|
||||||
onRowDoubleClicked,
|
onRowDoubleClicked,
|
||||||
noDataComponent,
|
noDataComponent,
|
||||||
|
|
||||||
enableRowSelection,
|
paginationPerPage,
|
||||||
rowSelection,
|
|
||||||
|
|
||||||
enableHiding,
|
|
||||||
columnVisibility,
|
|
||||||
|
|
||||||
enableSorting,
|
|
||||||
initialSorting,
|
|
||||||
|
|
||||||
enablePagination,
|
|
||||||
paginationPerPage = 10,
|
|
||||||
paginationOptions = [10, 20, 30, 40, 50],
|
paginationOptions = [10, 20, 30, 40, 50],
|
||||||
onChangePaginationOption,
|
|
||||||
|
|
||||||
...restProps
|
...restProps
|
||||||
}: DataTableProps<TData>) {
|
}: DataTableProps<TData>) {
|
||||||
const [sorting, setSorting] = useState<SortingState>(initialSorting ? [initialSorting] : []);
|
|
||||||
const [lastSelected, setLastSelected] = useState<string | null>(null);
|
const [lastSelected, setLastSelected] = useState<string | null>(null);
|
||||||
|
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
const table = useDataTable({ paginationPerPage, ...restProps });
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: paginationPerPage
|
|
||||||
});
|
|
||||||
|
|
||||||
const tableImpl = useReactTable({
|
const isEmpty = table.getRowModel().rows.length === 0;
|
||||||
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(() => {
|
const fixedSize = useMemo(() => {
|
||||||
if (!rows) {
|
if (!rows) {
|
||||||
|
@ -180,49 +143,46 @@ export function DataTable<TData extends RowData>({
|
||||||
}, [rows, dense, noHeader, contentHeight]);
|
}, [rows, dense, noHeader, contentHeight]);
|
||||||
|
|
||||||
const columnSizeVars = useMemo(() => {
|
const columnSizeVars = useMemo(() => {
|
||||||
const headers = tableImpl.getFlatHeaders();
|
const headers = table.getFlatHeaders();
|
||||||
const colSizes: Record<string, number> = {};
|
const colSizes: Record<string, number> = {};
|
||||||
for (const header of headers) {
|
for (const header of headers) {
|
||||||
colSizes[`--header-${header.id}-size`] = header.getSize();
|
colSizes[`--header-${header.id}-size`] = header.getSize();
|
||||||
colSizes[`--col-${header.column.id}-size`] = header.column.getSize();
|
colSizes[`--col-${header.column.id}-size`] = header.column.getSize();
|
||||||
}
|
}
|
||||||
return colSizes;
|
return colSizes;
|
||||||
}, [tableImpl]);
|
}, [table]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div tabIndex={-1} id={id} className={className} style={{ minHeight: fixedSize, maxHeight: fixedSize, ...style }}>
|
<div
|
||||||
|
tabIndex={-1}
|
||||||
|
id={id}
|
||||||
|
className={clsx('table-auto', className)}
|
||||||
|
style={{ minHeight: fixedSize, maxHeight: fixedSize, ...style }}
|
||||||
|
>
|
||||||
<table className='w-full' style={{ ...columnSizeVars }}>
|
<table className='w-full' style={{ ...columnSizeVars }}>
|
||||||
{!noHeader ? (
|
{!noHeader ? (
|
||||||
<TableHeader
|
<TableHeader table={table} headPosition={headPosition} resetLastSelected={() => setLastSelected(null)} />
|
||||||
table={tableImpl}
|
|
||||||
enableRowSelection={enableRowSelection}
|
|
||||||
enableSorting={enableSorting}
|
|
||||||
headPosition={headPosition}
|
|
||||||
resetLastSelected={() => setLastSelected(null)}
|
|
||||||
/>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<TableBody
|
<TableBody
|
||||||
table={tableImpl}
|
table={table}
|
||||||
dense={dense}
|
dense={dense}
|
||||||
noHeader={noHeader}
|
noHeader={noHeader}
|
||||||
conditionalRowStyles={conditionalRowStyles}
|
conditionalRowStyles={conditionalRowStyles}
|
||||||
enableRowSelection={enableRowSelection}
|
|
||||||
lastSelected={lastSelected}
|
lastSelected={lastSelected}
|
||||||
onChangeLastSelected={setLastSelected}
|
onChangeLastSelected={setLastSelected}
|
||||||
onRowClicked={onRowClicked}
|
onRowClicked={onRowClicked}
|
||||||
onRowDoubleClicked={onRowDoubleClicked}
|
onRowDoubleClicked={onRowDoubleClicked}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!noFooter ? <TableFooter table={tableImpl} /> : null}
|
{!noFooter ? <TableFooter table={table} /> : null}
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{enablePagination && !isEmpty ? (
|
{!!paginationPerPage && !isEmpty ? (
|
||||||
<PaginationTools
|
<PaginationTools
|
||||||
id={id ? `${id}__pagination` : undefined}
|
id={id ? `${id}__pagination` : undefined}
|
||||||
table={tableImpl}
|
table={table}
|
||||||
paginationOptions={paginationOptions}
|
paginationOptions={paginationOptions}
|
||||||
onChangePaginationOption={onChangePaginationOption}
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{isEmpty ? noDataComponent ?? <DefaultNoData /> : null}
|
{isEmpty ? noDataComponent ?? <DefaultNoData /> : null}
|
|
@ -4,4 +4,4 @@ export {
|
||||||
type IConditionalStyle,
|
type IConditionalStyle,
|
||||||
type RowSelectionState,
|
type RowSelectionState,
|
||||||
type VisibilityState
|
type VisibilityState
|
||||||
} from './DataTable';
|
} from './data-table';
|
|
@ -1,35 +1,26 @@
|
||||||
'use client';
|
|
||||||
'use no memo';
|
'use no memo';
|
||||||
|
'use client';
|
||||||
|
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { type Table } from '@tanstack/react-table';
|
import { type Table } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { prefixes } from '@/utils/constants';
|
import { prefixes } from '@/utils/constants';
|
||||||
|
|
||||||
import { IconPageFirst, IconPageLast, IconPageLeft, IconPageRight } from '../Icons';
|
import { IconPageFirst, IconPageLast, IconPageLeft, IconPageRight } from '../icons';
|
||||||
|
|
||||||
interface PaginationToolsProps<TData> {
|
interface PaginationToolsProps<TData> {
|
||||||
id?: string;
|
id?: string;
|
||||||
table: Table<TData>;
|
table: Table<TData>;
|
||||||
paginationOptions: number[];
|
paginationOptions: number[];
|
||||||
onChangePaginationOption?: (newValue: number) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PaginationTools<TData>({
|
export function PaginationTools<TData>({ id, table, paginationOptions }: PaginationToolsProps<TData>) {
|
||||||
id,
|
|
||||||
table,
|
|
||||||
paginationOptions,
|
|
||||||
onChangePaginationOption
|
|
||||||
}: PaginationToolsProps<TData>) {
|
|
||||||
const handlePaginationOptionsChange = useCallback(
|
const handlePaginationOptionsChange = useCallback(
|
||||||
(event: React.ChangeEvent<HTMLSelectElement>) => {
|
(event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
const perPage = Number(event.target.value);
|
const perPage = Number(event.target.value);
|
||||||
table.setPageSize(perPage);
|
table.setPageSize(perPage);
|
||||||
if (onChangePaginationOption) {
|
|
||||||
onChangePaginationOption(perPage);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[onChangePaginationOption, table]
|
[table]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { type Table } from '@tanstack/react-table';
|
import { type Table } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { CheckboxTristate } from '../Input';
|
import { CheckboxTristate } from '../input';
|
||||||
|
|
||||||
interface SelectAllProps<TData> {
|
interface SelectAllProps<TData> {
|
||||||
table: Table<TData>;
|
table: Table<TData>;
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { type Row } from '@tanstack/react-table';
|
import { type Row } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { Checkbox } from '../Input';
|
import { Checkbox } from '../input';
|
||||||
|
|
||||||
interface SelectRowProps<TData> {
|
interface SelectRowProps<TData> {
|
||||||
row: Row<TData>;
|
row: Row<TData>;
|
|
@ -0,0 +1,15 @@
|
||||||
|
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' />;
|
||||||
|
}
|
112
rsconcept/frontend/src/components/data-table/table-body.tsx
Normal file
112
rsconcept/frontend/src/components/data-table/table-body.tsx
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
'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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
'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>
|
||||||
|
);
|
||||||
|
}
|
125
rsconcept/frontend/src/components/data-table/use-data-table.ts
Normal file
125
rsconcept/frontend/src/components/data-table/use-data-table.ts
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
'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;
|
||||||
|
}
|
|
@ -39,11 +39,7 @@ export function DropdownButton({
|
||||||
'text-left text-sm text-ellipsis whitespace-nowrap',
|
'text-left text-sm text-ellipsis whitespace-nowrap',
|
||||||
'disabled:clr-text-controls',
|
'disabled:clr-text-controls',
|
||||||
'cc-animate-color',
|
'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
|
className
|
||||||
)}
|
)}
|
||||||
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
|
@ -1,6 +1,6 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { Checkbox, type CheckboxProps } from '../Input';
|
import { Checkbox, type CheckboxProps } from '../input';
|
||||||
|
|
||||||
/** Animated {@link Checkbox} inside a {@link Dropdown} item. */
|
/** Animated {@link Checkbox} inside a {@link Dropdown} item. */
|
||||||
export function DropdownCheckbox({ onChange: setValue, disabled, ...restProps }: CheckboxProps) {
|
export function DropdownCheckbox({ onChange: setValue, disabled, ...restProps }: CheckboxProps) {
|
|
@ -1,8 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { PARAMETER } from '@/utils/constants';
|
|
||||||
|
|
||||||
import { type Styling } from '../props';
|
import { type Styling } from '../props';
|
||||||
|
|
||||||
interface DropdownProps extends Styling {
|
interface DropdownProps extends Styling {
|
||||||
|
@ -36,36 +34,19 @@ export function Dropdown({
|
||||||
margin,
|
margin,
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
style,
|
|
||||||
...restProps
|
...restProps
|
||||||
}: React.PropsWithChildren<DropdownProps>) {
|
}: React.PropsWithChildren<DropdownProps>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'z-topmost absolute',
|
'cc-dropdown isolate z-topmost absolute grid bg-prim-0 border rounded-md shadow-lg text-sm',
|
||||||
{
|
stretchLeft ? 'right-0' : 'left-0',
|
||||||
'right-0': stretchLeft,
|
stretchTop ? 'bottom-0' : 'top-full',
|
||||||
'left-0': !stretchLeft,
|
isOpen && 'open',
|
||||||
'bottom-0': stretchTop,
|
|
||||||
'top-full': !stretchTop
|
|
||||||
},
|
|
||||||
'grid',
|
|
||||||
'border rounded-md shadow-lg',
|
|
||||||
'clr-input',
|
|
||||||
'text-sm',
|
|
||||||
margin,
|
margin,
|
||||||
className
|
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}
|
aria-hidden={!isOpen}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
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 './dropdown-button';
|
||||||
|
export { DropdownCheckbox } from './dropdown-checkbox';
|
||||||
|
export { useDropdown } from './use-dropdown';
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
|
|
||||||
import { useClickedOutside } from '@/hooks/useClickedOutside';
|
import { useClickedOutside } from '@/hooks/use-clicked-outside';
|
||||||
|
|
||||||
export function useDropdown() {
|
export function useDropdown() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
|
@ -196,9 +196,10 @@ export function IconLogin(props: IconProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function CheckboxChecked() {
|
export function CheckboxChecked() {
|
||||||
return (
|
return (
|
||||||
<svg className='w-4 h-4 p-0.75' viewBox='0 0 512 512' fill='#ffffff'>
|
<svg className='w-4 h-4 p-0.75 -ml-0.25' 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' />
|
<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>
|
</svg>
|
||||||
);
|
);
|
||||||
|
@ -206,8 +207,8 @@ export function CheckboxChecked() {
|
||||||
|
|
||||||
export function CheckboxNull() {
|
export function CheckboxNull() {
|
||||||
return (
|
return (
|
||||||
<svg className='w-4 h-4 p-0.25' viewBox='0 0 16 16' fill='#ffffff'>
|
<svg className='w-4 h-4 px-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' />
|
<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>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -1,10 +1,10 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { ZodError } from 'zod';
|
import { ZodError } from 'zod';
|
||||||
|
|
||||||
import { type AxiosError, isAxiosError } from '@/backend/apiTransport';
|
import { type AxiosError, isAxiosError } from '@/backend/api-transport';
|
||||||
import { isResponseHtml } from '@/utils/utils';
|
import { isResponseHtml } from '@/utils/utils';
|
||||||
|
|
||||||
import { PrettyJson } from './View';
|
import { PrettyJson } from './view';
|
||||||
|
|
||||||
export type ErrorData = string | Error | AxiosError | ZodError;
|
export type ErrorData = string | Error | AxiosError | ZodError;
|
||||||
|
|
||||||
|
@ -33,18 +33,7 @@ export function DescribeError({ error }: { error: ErrorData }) {
|
||||||
<p>
|
<p>
|
||||||
<b>Message:</b> {error.message}
|
<b>Message:</b> {error.message}
|
||||||
</p>
|
</p>
|
||||||
{error.stack && (
|
{error.stack && <pre className='whitespace-pre-wrap p-2 overflow-x-auto break-words'>{error.stack}</pre>}
|
||||||
<pre
|
|
||||||
style={{
|
|
||||||
whiteSpace: 'pre-wrap',
|
|
||||||
wordWrap: 'break-word',
|
|
||||||
padding: '6px',
|
|
||||||
overflowX: 'auto'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{error.stack}
|
|
||||||
</pre>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -2,9 +2,9 @@ import clsx from 'clsx';
|
||||||
|
|
||||||
import { globalIDs } from '@/utils/constants';
|
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'> {
|
export interface CheckboxTristateProps extends Omit<CheckboxProps, 'value' | 'onChange'> {
|
||||||
/** Current value - `null`, `true` or `false`. */
|
/** Current value - `null`, `true` or `false`. */
|
||||||
|
@ -66,11 +66,8 @@ export function CheckboxTristate({
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-4 h-4', //
|
'w-4 h-4', //
|
||||||
'border rounded-xs',
|
'border rounded-sm',
|
||||||
{
|
value === false ? 'bg-prim-100' : 'bg-sec-600 text-sec-0'
|
||||||
'bg-sec-600 text-sec-0': value !== false,
|
|
||||||
'bg-prim-100': value === false
|
|
||||||
}
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{value ? <CheckboxChecked /> : null}
|
{value ? <CheckboxChecked /> : null}
|
|
@ -2,7 +2,7 @@ import clsx from 'clsx';
|
||||||
|
|
||||||
import { globalIDs } from '@/utils/constants';
|
import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
import { CheckboxChecked } from '../Icons';
|
import { CheckboxChecked } from '../icons';
|
||||||
import { type Button } from '../props';
|
import { type Button } from '../props';
|
||||||
|
|
||||||
export interface CheckboxProps extends Omit<Button, 'value' | 'onClick' | 'onChange'> {
|
export interface CheckboxProps extends Omit<Button, 'value' | 'onClick' | 'onChange'> {
|
||||||
|
@ -65,11 +65,8 @@ export function Checkbox({
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-4 h-4', //
|
'w-4 h-4', //
|
||||||
'border rounded-xs',
|
'border rounded-sm',
|
||||||
{
|
value === false ? 'bg-prim-100' : 'bg-sec-600 text-sec-0'
|
||||||
'bg-sec-600 text-sec-0': value !== false,
|
|
||||||
'bg-prim-100': value === false
|
|
||||||
}
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{value ? <CheckboxChecked /> : null}
|
{value ? <CheckboxChecked /> : null}
|
|
@ -3,11 +3,11 @@
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { Button } from '../Control';
|
import { Button } from '../control';
|
||||||
import { IconUpload } from '../Icons';
|
import { IconUpload } from '../icons';
|
||||||
import { type Titled } from '../props';
|
import { type Titled } from '../props';
|
||||||
|
|
||||||
import { Label } from './Label';
|
import { Label } from './label';
|
||||||
|
|
||||||
interface FileInputProps extends Titled, Omit<React.ComponentProps<'input'>, 'accept' | 'type'> {
|
interface FileInputProps extends Titled, Omit<React.ComponentProps<'input'>, 'accept' | 'type'> {
|
||||||
/** Label to display in file upload button. */
|
/** Label to display in file upload button. */
|
||||||
|
@ -46,7 +46,7 @@ export function FileInput({ id, label, acceptType, title, className, style, onCh
|
||||||
id={id}
|
id={id}
|
||||||
type='file'
|
type='file'
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
style={{ display: 'none' }}
|
className='hidden'
|
||||||
accept={acceptType}
|
accept={acceptType}
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
{...restProps}
|
{...restProps}
|
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 './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,9 +1,9 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { IconSearch } from '@/components/Icons';
|
import { IconSearch } from '@/components/icons';
|
||||||
import { type Styling } from '@/components/props';
|
import { type Styling } from '@/components/props';
|
||||||
|
|
||||||
import { TextInput } from './TextInput';
|
import { TextInput } from './text-input';
|
||||||
|
|
||||||
interface SearchBarProps extends Styling {
|
interface SearchBarProps extends Styling {
|
||||||
/** Id of the search bar. */
|
/** Id of the search bar. */
|
||||||
|
@ -39,10 +39,10 @@ export function SearchBar({
|
||||||
...restProps
|
...restProps
|
||||||
}: SearchBarProps) {
|
}: SearchBarProps) {
|
||||||
return (
|
return (
|
||||||
<div className={clsx('relative', className)} {...restProps}>
|
<div className={clsx('relative flex items-center', className)} {...restProps}>
|
||||||
{!noIcon ? (
|
{!noIcon ? (
|
||||||
<IconSearch
|
<IconSearch
|
||||||
className='absolute -top-0.5 left-3 translate-y-1/2 pointer-events-none clr-text-controls'
|
className='absolute -top-0.5 left-2 translate-y-1/2 pointer-events-none clr-text-controls'
|
||||||
size='1.25rem'
|
size='1.25rem'
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -52,7 +52,7 @@ export function SearchBar({
|
||||||
transparent
|
transparent
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
type='search'
|
type='search'
|
||||||
className={clsx('bg-transparent', !noIcon && 'pl-10')}
|
className={clsx('bg-transparent', !noIcon && 'pl-8')}
|
||||||
noBorder={noBorder}
|
noBorder={noBorder}
|
||||||
value={query}
|
value={query}
|
||||||
onChange={event => onChangeQuery?.(event.target.value)}
|
onChange={event => onChangeQuery?.(event.target.value)}
|
|
@ -9,10 +9,10 @@ import Select, {
|
||||||
type StylesConfig
|
type StylesConfig
|
||||||
} from 'react-select';
|
} from 'react-select';
|
||||||
|
|
||||||
import { useWindowSize } from '@/hooks/useWindowSize';
|
import { useWindowSize } from '@/hooks/use-window-size';
|
||||||
import { APP_COLORS, SELECT_THEME } from '@/styling/colors';
|
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>>(
|
function DropdownIndicator<Option, Group extends GroupBase<Option> = GroupBase<Option>>(
|
||||||
props: DropdownIndicatorProps<Option, true, Group>
|
props: DropdownIndicatorProps<Option, true, Group>
|
|
@ -9,10 +9,10 @@ import Select, {
|
||||||
type StylesConfig
|
type StylesConfig
|
||||||
} from 'react-select';
|
} from 'react-select';
|
||||||
|
|
||||||
import { useWindowSize } from '@/hooks/useWindowSize';
|
import { useWindowSize } from '@/hooks/use-window-size';
|
||||||
import { APP_COLORS, SELECT_THEME } from '@/styling/colors';
|
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>>(
|
function DropdownIndicator<Option, Group extends GroupBase<Option> = GroupBase<Option>>(
|
||||||
props: DropdownIndicatorProps<Option, false, Group>
|
props: DropdownIndicatorProps<Option, false, Group>
|
|
@ -1,10 +1,10 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { globalIDs, PARAMETER } from '@/utils/constants';
|
import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
import { MiniButton } from '../Control';
|
import { MiniButton } from '../control';
|
||||||
import { IconDropArrow, IconPageRight } from '../Icons';
|
import { IconDropArrow, IconPageRight } from '../icons';
|
||||||
import { type Styling } from '../props';
|
import { type Styling } from '../props';
|
||||||
|
|
||||||
interface SelectTreeProps<ItemType> extends Styling {
|
interface SelectTreeProps<ItemType> extends Styling {
|
||||||
|
@ -89,27 +89,13 @@ export function SelectTree<ItemType>({
|
||||||
<div
|
<div
|
||||||
key={`${prefix}${index}`}
|
key={`${prefix}${index}`}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'relative',
|
'cc-tree-item relative cc-scroll-row clr-hover',
|
||||||
'pr-3 pl-6 border-b',
|
isActive ? 'max-h-7 py-1 border-b' : 'max-h-0 opacity-0 pointer-events-none',
|
||||||
'cc-scroll-row',
|
value === item && 'clr-selected'
|
||||||
'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-id={globalIDs.tooltip}
|
||||||
data-tooltip-html={getDescription(item)}
|
data-tooltip-html={getDescription(item)}
|
||||||
onClick={event => handleClickItem(event, 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) ? (
|
{foldable.has(item) ? (
|
||||||
<MiniButton
|
<MiniButton
|
|
@ -1,9 +1,9 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { Label } from '../Input/Label';
|
|
||||||
import { type Editor, type ErrorProcessing, type Titled } from '../props';
|
import { type Editor, type ErrorProcessing, type Titled } from '../props';
|
||||||
|
|
||||||
import { ErrorField } from './ErrorField';
|
import { ErrorField } from './error-field';
|
||||||
|
import { Label } from './label';
|
||||||
|
|
||||||
export interface TextAreaProps extends Editor, ErrorProcessing, Titled, React.ComponentProps<'textarea'> {
|
export interface TextAreaProps extends Editor, ErrorProcessing, Titled, React.ComponentProps<'textarea'> {
|
||||||
/** Indicates that the input should be transparent. */
|
/** Indicates that the input should be transparent. */
|
||||||
|
@ -40,11 +40,8 @@ export function TextArea({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-full',
|
'w-full', //
|
||||||
{
|
dense ? 'flex grow items-center gap-3' : 'flex flex-col',
|
||||||
'flex flex-col': !dense,
|
|
||||||
'flex grow items-center gap-3': dense
|
|
||||||
},
|
|
||||||
dense && className
|
dense && className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -55,16 +52,13 @@ export function TextArea({
|
||||||
'px-3 py-2',
|
'px-3 py-2',
|
||||||
'leading-tight',
|
'leading-tight',
|
||||||
'overflow-x-hidden overflow-y-auto',
|
'overflow-x-hidden overflow-y-auto',
|
||||||
{
|
!noBorder && 'border',
|
||||||
'field-sizing-content': fitContent,
|
fitContent && 'field-sizing-content',
|
||||||
'resize-none': noResize,
|
noResize && 'resize-none',
|
||||||
'border': !noBorder,
|
transparent ? 'bg-transparent' : 'clr-input',
|
||||||
'grow max-w-full': dense,
|
!noOutline && 'clr-outline',
|
||||||
'mt-2': !dense && !!label,
|
dense && 'grow max-w-full',
|
||||||
'clr-outline': !noOutline,
|
!dense && !!label && 'mt-2',
|
||||||
'bg-transparent': transparent,
|
|
||||||
'clr-input': !transparent
|
|
||||||
},
|
|
||||||
!dense && className
|
!dense && className
|
||||||
)}
|
)}
|
||||||
rows={rows}
|
rows={rows}
|
|
@ -1,9 +1,9 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { Label } from '../Input/Label';
|
|
||||||
import { type Editor, type ErrorProcessing, type Titled } from '../props';
|
import { type Editor, type ErrorProcessing, type Titled } from '../props';
|
||||||
|
|
||||||
import { ErrorField } from './ErrorField';
|
import { ErrorField } from './error-field';
|
||||||
|
import { Label } from './label';
|
||||||
|
|
||||||
interface TextInputProps extends Editor, ErrorProcessing, Titled, React.ComponentProps<'input'> {
|
interface TextInputProps extends Editor, ErrorProcessing, Titled, React.ComponentProps<'input'> {
|
||||||
/** Indicates that the input should be transparent. */
|
/** Indicates that the input should be transparent. */
|
||||||
|
@ -42,10 +42,7 @@ export function TextInput({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
{
|
dense ? 'flex items-center gap-3' : 'flex flex-col', //
|
||||||
'flex flex-col': !dense,
|
|
||||||
'flex items-center gap-3': dense
|
|
||||||
},
|
|
||||||
dense && className
|
dense && className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -55,15 +52,12 @@ export function TextInput({
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'min-w-0 py-2',
|
'min-w-0 py-2',
|
||||||
'leading-tight truncate hover:text-clip',
|
'leading-tight truncate hover:text-clip',
|
||||||
{
|
transparent ? 'bg-transparent' : 'clr-input',
|
||||||
'px-3': !noBorder || !disabled,
|
!noBorder && 'border',
|
||||||
'grow max-w-full': dense,
|
!noOutline && 'clr-outline',
|
||||||
'mt-2': !dense && !!label,
|
(!noBorder || !disabled) && 'px-3',
|
||||||
'border': !noBorder,
|
dense && 'grow max-w-full',
|
||||||
'clr-outline': !noOutline,
|
!dense && !!label && 'mt-2',
|
||||||
'bg-transparent': transparent,
|
|
||||||
'clr-input': !transparent
|
|
||||||
},
|
|
||||||
!dense && className
|
!dense && className
|
||||||
)}
|
)}
|
||||||
onKeyDown={!allowEnter && !onKeyDown ? preventEnterCapture : onKeyDown}
|
onKeyDown={!allowEnter && !onKeyDown ? preventEnterCapture : onKeyDown}
|
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 './modal-form';
|
||||||
|
export { ModalLoader } from './modal-loader';
|
||||||
|
export { ModalView } from './modal-view';
|
|
@ -5,15 +5,15 @@ import clsx from 'clsx';
|
||||||
import { type HelpTopic } from '@/features/help';
|
import { type HelpTopic } from '@/features/help';
|
||||||
import { BadgeHelp } from '@/features/help/components';
|
import { BadgeHelp } from '@/features/help/components';
|
||||||
|
|
||||||
import { useEscapeKey } from '@/hooks/useEscapeKey';
|
import { useEscapeKey } from '@/hooks/use-escape-key';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { prepareTooltip } from '@/utils/utils';
|
import { prepareTooltip } from '@/utils/utils';
|
||||||
|
|
||||||
import { Button, MiniButton, SubmitButton } from '../Control';
|
import { Button, MiniButton, SubmitButton } from '../control';
|
||||||
import { IconClose } from '../Icons';
|
import { IconClose } from '../icons';
|
||||||
import { type Styling } from '../props';
|
import { type Styling } from '../props';
|
||||||
|
|
||||||
import { ModalBackdrop } from './ModalBackdrop';
|
import { ModalBackdrop } from './modal-backdrop';
|
||||||
|
|
||||||
export interface ModalProps extends Styling {
|
export interface ModalProps extends Styling {
|
||||||
/** Title of the modal window. */
|
/** Title of the modal window. */
|
||||||
|
@ -90,7 +90,7 @@ export function ModalForm({
|
||||||
<div className='cc-modal-wrapper'>
|
<div className='cc-modal-wrapper'>
|
||||||
<ModalBackdrop onHide={handleCancel} />
|
<ModalBackdrop onHide={handleCancel} />
|
||||||
<form
|
<form
|
||||||
className='cc-animate-modal grid border rounded-xl bg-prim-100'
|
className='cc-animate-modal relative grid border rounded-xl bg-prim-100'
|
||||||
role='dialog'
|
role='dialog'
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
aria-labelledby='modal-title'
|
aria-labelledby='modal-title'
|
||||||
|
@ -98,7 +98,7 @@ export function ModalForm({
|
||||||
{helpTopic && !hideHelpWhen?.() ? (
|
{helpTopic && !hideHelpWhen?.() ? (
|
||||||
<BadgeHelp
|
<BadgeHelp
|
||||||
topic={helpTopic}
|
topic={helpTopic}
|
||||||
className='absolute z-pop left-0 mt-2 ml-2'
|
className='absolute z-top top-2 left-2'
|
||||||
padding='p-0'
|
padding='p-0'
|
||||||
contentClass='sm:max-w-160'
|
contentClass='sm:max-w-160'
|
||||||
/>
|
/>
|
||||||
|
@ -109,7 +109,7 @@ export function ModalForm({
|
||||||
aria-label='Закрыть'
|
aria-label='Закрыть'
|
||||||
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
|
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
|
||||||
icon={<IconClose size='1.25rem' />}
|
icon={<IconClose size='1.25rem' />}
|
||||||
className='absolute z-pop right-0 mt-2 mr-2'
|
className='absolute z-pop top-2 right-2'
|
||||||
onClick={hideDialog}
|
onClick={hideDialog}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -124,10 +124,7 @@ export function ModalForm({
|
||||||
'@container/modal',
|
'@container/modal',
|
||||||
'max-h-[calc(100svh-8rem)] max-w-[100svw] xs:max-w-[calc(100svw-2rem)]',
|
'max-h-[calc(100svh-8rem)] max-w-[100svw] xs:max-w-[calc(100svw-2rem)]',
|
||||||
'overscroll-contain outline-hidden',
|
'overscroll-contain outline-hidden',
|
||||||
{
|
overflowVisible ? 'overflow-visible' : 'overflow-auto',
|
||||||
'overflow-auto': !overflowVisible,
|
|
||||||
'overflow-visible': overflowVisible
|
|
||||||
},
|
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
|
@ -1,6 +1,6 @@
|
||||||
import { Loader } from '@/components/Loader';
|
import { Loader } from '@/components/loader';
|
||||||
|
|
||||||
import { ModalBackdrop } from './ModalBackdrop';
|
import { ModalBackdrop } from './modal-backdrop';
|
||||||
|
|
||||||
export function ModalLoader() {
|
export function ModalLoader() {
|
||||||
return (
|
return (
|
103
rsconcept/frontend/src/components/modal/modal-view.tsx
Normal file
103
rsconcept/frontend/src/components/modal/modal-view.tsx
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
'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,2 +1,2 @@
|
||||||
export { TabLabel } from './TabLabel';
|
export { TabLabel } from './tab-label';
|
||||||
export { TabList, TabPanel, Tabs } from 'react-tabs';
|
export { TabList, TabPanel, Tabs } from 'react-tabs';
|
|
@ -18,9 +18,8 @@ export function EmbedYoutube({ videoID, pxHeight, pxWidth }: EmbedYoutubeProps)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='relative'
|
className='relative h-0'
|
||||||
style={{
|
style={{
|
||||||
height: 0,
|
|
||||||
paddingBottom: `${pxHeight}px`,
|
paddingBottom: `${pxHeight}px`,
|
||||||
paddingLeft: `${pxWidth}px`
|
paddingLeft: `${pxWidth}px`
|
||||||
}}
|
}}
|
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 './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';
|
|
@ -18,11 +18,9 @@ export function Indicator({ icon, title, titleHtml, hideTitle, noPadding, classN
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'clr-text-controls',
|
'clr-text-controls', //
|
||||||
'outline-hidden',
|
'outline-hidden',
|
||||||
{
|
!noPadding && 'px-1 py-1',
|
||||||
'px-1 py-1': !noPadding
|
|
||||||
},
|
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
|
@ -1,7 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useWindowSize } from '@/hooks/useWindowSize';
|
import { useWindowSize } from '@/hooks/use-window-size';
|
||||||
import { useFitHeight } from '@/stores/appLayout';
|
import { useFitHeight } from '@/stores/app-layout';
|
||||||
|
|
||||||
/** Maximum width of the viewer. */
|
/** Maximum width of the viewer. */
|
||||||
const MAXIMUM_WIDTH = 1600;
|
const MAXIMUM_WIDTH = 1600;
|
|
@ -2,7 +2,7 @@ import clsx from 'clsx';
|
||||||
|
|
||||||
import { globalIDs } from '@/utils/constants';
|
import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
import { MiniButton } from '../Control';
|
import { MiniButton } from '../control';
|
||||||
import { type Styling, type Titled } from '../props';
|
import { type Styling, type Titled } from '../props';
|
||||||
|
|
||||||
interface ValueIconProps extends Styling, Titled {
|
interface ValueIconProps extends Styling, Titled {
|
||||||
|
@ -57,7 +57,7 @@ export function ValueIcon({
|
||||||
'flex items-center',
|
'flex items-center',
|
||||||
'text-right',
|
'text-right',
|
||||||
'hover:cursor-default',
|
'hover:cursor-default',
|
||||||
{ 'justify-between gap-6': !dense, 'gap-1': dense },
|
dense ? 'gap-1' : 'justify-between gap-6',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
|
@ -1,6 +1,6 @@
|
||||||
import { type Styling, type Titled } from '@/components/props';
|
import { type Styling, type Titled } from '@/components/props';
|
||||||
|
|
||||||
import { ValueIcon } from './ValueIcon';
|
import { ValueIcon } from './value-icon';
|
||||||
|
|
||||||
// characters - threshold for small labels - small font
|
// characters - threshold for small labels - small font
|
||||||
const SMALL_THRESHOLD = 3;
|
const SMALL_THRESHOLD = 3;
|
|
@ -1,6 +1,6 @@
|
||||||
import { queryOptions } from '@tanstack/react-query';
|
import { queryOptions } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport';
|
import { axiosGet, axiosPatch, axiosPost } from '@/backend/api-transport';
|
||||||
import { DELAYS, KEYS } from '@/backend/configuration';
|
import { DELAYS, KEYS } from '@/backend/configuration';
|
||||||
import { infoMsg } from '@/utils/labels';
|
import { infoMsg } from '@/utils/labels';
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { queryClient } from '@/backend/queryClient';
|
import { queryClient } from '@/backend/query-client';
|
||||||
|
|
||||||
import { authApi } from './api';
|
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