mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Refactoring: apply prettier on save
This commit is contained in:
parent
87d3152e6c
commit
40cb8b4ce8
258
.vscode/settings.json
vendored
258
.vscode/settings.json
vendored
|
@ -1,137 +1,133 @@
|
||||||
{
|
{
|
||||||
"search.exclude": {
|
"search.exclude": {
|
||||||
".mypy_cache/": true,
|
".mypy_cache/": true,
|
||||||
".pytest_cache/": true
|
".pytest_cache/": true
|
||||||
},
|
},
|
||||||
"python.testing.unittestArgs": [
|
"python.testing.unittestArgs": ["-v", "-s", "./tests", "-p", "test*.py"],
|
||||||
"-v",
|
"python.testing.pytestEnabled": false,
|
||||||
"-s",
|
"python.testing.unittestEnabled": true,
|
||||||
"./tests",
|
"eslint.workingDirectories": [
|
||||||
"-p",
|
|
||||||
"test*.py"
|
|
||||||
],
|
|
||||||
"python.testing.pytestEnabled": false,
|
|
||||||
"python.testing.unittestEnabled": true,
|
|
||||||
"eslint.workingDirectories": [
|
|
||||||
{
|
|
||||||
"mode": "auto"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"python.analysis.typeCheckingMode": "off",
|
|
||||||
"python.analysis.diagnosticSeverityOverrides": {
|
|
||||||
// "reportOptionalMemberAccess": "none"
|
|
||||||
},
|
|
||||||
"python.analysis.ignore": ["**/tests/**", "**/node_modules/**", "**/venv/**"],
|
|
||||||
"python.analysis.packageIndexDepths": [
|
|
||||||
{
|
{
|
||||||
"name": "django",
|
"mode": "auto"
|
||||||
"depth": 5
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"colorize.include": [".tsx", ".jsx", ".ts", ".js"],
|
"python.analysis.typeCheckingMode": "off",
|
||||||
"colorize.languages": [
|
"python.analysis.diagnosticSeverityOverrides": {
|
||||||
"typescript",
|
// "reportOptionalMemberAccess": "none"
|
||||||
"javascript",
|
},
|
||||||
"css",
|
"python.analysis.ignore": ["**/tests/**", "**/node_modules/**", "**/venv/**"],
|
||||||
"typescriptreact",
|
"python.analysis.packageIndexDepths": [
|
||||||
"javascriptreact"
|
{
|
||||||
],
|
"name": "django",
|
||||||
"cSpell.words": [
|
"depth": 5
|
||||||
"ablt",
|
}
|
||||||
"accs",
|
],
|
||||||
"actv",
|
"colorize.include": [".tsx", ".jsx", ".ts", ".js"],
|
||||||
"ADJF",
|
"colorize.languages": [
|
||||||
"ADJS",
|
"typescript",
|
||||||
"ADVB",
|
"javascript",
|
||||||
"Analyse",
|
"css",
|
||||||
"Backquote",
|
"typescriptreact",
|
||||||
"BIGPR",
|
"javascriptreact"
|
||||||
"cctext",
|
],
|
||||||
"CIHT",
|
"cSpell.words": [
|
||||||
"clsx",
|
"ablt",
|
||||||
"codemirror",
|
"accs",
|
||||||
"Constituenta",
|
"actv",
|
||||||
"csrftoken",
|
"ADJF",
|
||||||
"cstlist",
|
"ADJS",
|
||||||
"csttype",
|
"ADVB",
|
||||||
"datv",
|
"Analyse",
|
||||||
"Debool",
|
"Backquote",
|
||||||
"Decart",
|
"BIGPR",
|
||||||
"EMPTYSET",
|
"cctext",
|
||||||
"exteor",
|
"CIHT",
|
||||||
"femn",
|
"clsx",
|
||||||
"filterset",
|
"codemirror",
|
||||||
"forceatlas",
|
"Constituenta",
|
||||||
"futr",
|
"csrftoken",
|
||||||
"Grammeme",
|
"cstlist",
|
||||||
"Grammemes",
|
"csttype",
|
||||||
"GRND",
|
"datv",
|
||||||
"impr",
|
"Debool",
|
||||||
"inan",
|
"Decart",
|
||||||
"indc",
|
"Downvote",
|
||||||
"INFN",
|
"EMPTYSET",
|
||||||
"Infr",
|
"exteor",
|
||||||
"INTJ",
|
"femn",
|
||||||
"Keymap",
|
"filterset",
|
||||||
"lezer",
|
"forceatlas",
|
||||||
"Litr",
|
"futr",
|
||||||
"loct",
|
"Grammeme",
|
||||||
"moprho",
|
"Grammemes",
|
||||||
"nomn",
|
"GRND",
|
||||||
"nooverlap",
|
"impr",
|
||||||
"NPRO",
|
"inan",
|
||||||
"NUMR",
|
"indc",
|
||||||
"perfectivity",
|
"INFN",
|
||||||
"ponomarev",
|
"Infr",
|
||||||
"PRCL",
|
"INTJ",
|
||||||
"PRTF",
|
"Keymap",
|
||||||
"PRTS",
|
"lezer",
|
||||||
"pssv",
|
"Litr",
|
||||||
"pyconcept",
|
"loct",
|
||||||
"pymorphy",
|
"moprho",
|
||||||
"Quantor",
|
"nomn",
|
||||||
"razdel",
|
"nooverlap",
|
||||||
"reagraph",
|
"NPRO",
|
||||||
"Reindex",
|
"NUMR",
|
||||||
"rsedit",
|
"perfectivity",
|
||||||
"rseditor",
|
"ponomarev",
|
||||||
"rsform",
|
"PRCL",
|
||||||
"rsforms",
|
"PRTF",
|
||||||
"rslang",
|
"PRTS",
|
||||||
"rstemplates",
|
"pssv",
|
||||||
"SIDELIST",
|
"pyconcept",
|
||||||
"signup",
|
"pymorphy",
|
||||||
"Slng",
|
"Quantor",
|
||||||
"SMALLPR",
|
"razdel",
|
||||||
"tanstack",
|
"reagraph",
|
||||||
"toastify",
|
"Reindex",
|
||||||
"tooltipic",
|
"rsedit",
|
||||||
"Viewset",
|
"rseditor",
|
||||||
"viewsets",
|
"rsform",
|
||||||
"wordform",
|
"rsforms",
|
||||||
"Wordforms",
|
"rslang",
|
||||||
"Булеан",
|
"rstemplates",
|
||||||
"Бурбаки",
|
"SIDELIST",
|
||||||
"Десинглетон",
|
"signup",
|
||||||
"компаратив",
|
"Slng",
|
||||||
"конституент",
|
"SMALLPR",
|
||||||
"Конституента",
|
"tanstack",
|
||||||
"конституенту",
|
"toastify",
|
||||||
"конституенты",
|
"tooltipic",
|
||||||
"неинтерпретируемый",
|
"Upvote",
|
||||||
"неитерируемого",
|
"Viewset",
|
||||||
"пересинтез",
|
"viewsets",
|
||||||
"Родоструктурная",
|
"wordform",
|
||||||
"Родоструктурное",
|
"Wordforms",
|
||||||
"Синглетон",
|
"Булеан",
|
||||||
"твор",
|
"Бурбаки",
|
||||||
"Терминологизация",
|
"Десинглетон",
|
||||||
"Цермелло",
|
"компаратив",
|
||||||
"ЦИВТ",
|
"конституент",
|
||||||
"Экстеор",
|
"Конституента",
|
||||||
"Экстеора",
|
"конституенту",
|
||||||
"Экстеоре"
|
"конституенты",
|
||||||
],
|
"неинтерпретируемый",
|
||||||
"cSpell.language": "en,ru",
|
"неитерируемого",
|
||||||
"cSpell.ignorePaths": ["node_modules/**", "*.json"]
|
"пересинтез",
|
||||||
|
"Родоструктурная",
|
||||||
|
"Родоструктурное",
|
||||||
|
"Синглетон",
|
||||||
|
"твор",
|
||||||
|
"Терминологизация",
|
||||||
|
"Цермелло",
|
||||||
|
"ЦИВТ",
|
||||||
|
"Экстеор",
|
||||||
|
"Экстеора",
|
||||||
|
"Экстеоре"
|
||||||
|
],
|
||||||
|
"cSpell.language": "en,ru",
|
||||||
|
"cSpell.ignorePaths": ["node_modules/**", "*.json"]
|
||||||
}
|
}
|
13
rsconcept/frontend/.prettierrc.json
Normal file
13
rsconcept/frontend/.prettierrc.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"useTabs": false,
|
||||||
|
"printWidth": 120,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"singleQuote": true,
|
||||||
|
"jsxSingleQuote": true,
|
||||||
|
"quoteProps": "consistent",
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"bracketSpacing": true
|
||||||
|
}
|
|
@ -20,35 +20,33 @@ import { globalIDs } from './utils/constants';
|
||||||
function Root() {
|
function Root() {
|
||||||
const { viewportHeight, mainHeight, showScroll } = useConceptTheme();
|
const { viewportHeight, mainHeight, showScroll } = useConceptTheme();
|
||||||
return (
|
return (
|
||||||
<NavigationState>
|
<NavigationState>
|
||||||
<div className='min-w-[30rem] clr-app antialiased'>
|
<div className='min-w-[30rem] clr-app antialiased'>
|
||||||
|
<ConceptToaster
|
||||||
|
className='mt-[4rem] text-sm' //
|
||||||
|
autoClose={3000}
|
||||||
|
draggable={false}
|
||||||
|
pauseOnFocusLoss={false}
|
||||||
|
/>
|
||||||
|
|
||||||
<ConceptToaster
|
<Navigation />
|
||||||
className='mt-[4rem] text-sm'
|
|
||||||
autoClose={3000}
|
|
||||||
draggable={false}
|
|
||||||
pauseOnFocusLoss={false}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Navigation />
|
<div
|
||||||
|
id={globalIDs.main_scroll}
|
||||||
<div id={globalIDs.main_scroll}
|
className='overflow-y-auto overscroll-none min-w-fit'
|
||||||
className='overscroll-none min-w-fit overflow-y-auto'
|
style={{
|
||||||
style={{
|
maxHeight: viewportHeight,
|
||||||
maxHeight: viewportHeight,
|
overflowY: showScroll ? 'scroll' : 'auto'
|
||||||
overflowY: showScroll ? 'scroll': 'auto'
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<main className='flex flex-col items-center' style={{ minHeight: mainHeight }}>
|
||||||
<main
|
<Outlet />
|
||||||
className='flex flex-col items-center'
|
</main>
|
||||||
style={{minHeight: mainHeight}}
|
<Footer />
|
||||||
>
|
</div>
|
||||||
<Outlet />
|
</div>
|
||||||
</main>
|
</NavigationState>
|
||||||
<Footer />
|
);
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</NavigationState>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
|
@ -59,48 +57,46 @@ const router = createBrowserRouter([
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
element: <HomePage />,
|
element: <HomePage />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'login',
|
path: 'login',
|
||||||
element: <LoginPage />,
|
element: <LoginPage />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'signup',
|
path: 'signup',
|
||||||
element: <RegisterPage />,
|
element: <RegisterPage />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'restore-password',
|
path: 'restore-password',
|
||||||
element: <RestorePasswordPage />,
|
element: <RestorePasswordPage />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'profile',
|
path: 'profile',
|
||||||
element: <UserProfilePage />,
|
element: <UserProfilePage />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'manuals',
|
path: 'manuals',
|
||||||
element: <ManualsPage />,
|
element: <ManualsPage />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'library',
|
path: 'library',
|
||||||
element: <LibraryPage />,
|
element: <LibraryPage />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'library/create',
|
path: 'library/create',
|
||||||
element: <CreateRSFormPage />,
|
element: <CreateRSFormPage />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'rsforms/:id',
|
path: 'rsforms/:id',
|
||||||
element: <RSFormPage />,
|
element: <RSFormPage />
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function App () {
|
function App() {
|
||||||
return (
|
return <RouterProvider router={router} />;
|
||||||
<RouterProvider router={router} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
|
@ -11,13 +11,10 @@ import { UsersState } from '@/context/UsersContext';
|
||||||
|
|
||||||
import ErrorFallback from './components/ErrorFallback';
|
import ErrorFallback from './components/ErrorFallback';
|
||||||
|
|
||||||
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjs.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.js', import.meta.url).toString();
|
||||||
'pdfjs-dist/build/pdf.worker.min.js',
|
|
||||||
import.meta.url,
|
|
||||||
).toString();
|
|
||||||
|
|
||||||
const resetState = () => {
|
const resetState = () => {
|
||||||
console.log('Resetting state after error fallback')
|
console.log('Resetting state after error fallback');
|
||||||
};
|
};
|
||||||
|
|
||||||
const logError = (error: Error, info: { componentStack?: string | null | undefined }) => {
|
const logError = (error: Error, info: { componentStack?: string | null | undefined }) => {
|
||||||
|
@ -27,6 +24,7 @@ const logError = (error: Error, info: { componentStack?: string | null | undefin
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
function GlobalProviders({ children }: { children: React.ReactNode }) {
|
function GlobalProviders({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary
|
<ErrorBoundary
|
||||||
|
|
|
@ -4,47 +4,54 @@ import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
import { CProps } from '../props';
|
import { CProps } from '../props';
|
||||||
|
|
||||||
interface ButtonProps
|
interface ButtonProps extends CProps.Control, CProps.Colors, CProps.Button {
|
||||||
extends CProps.Control, CProps.Colors, CProps.Button {
|
text?: string;
|
||||||
text?: string
|
icon?: React.ReactNode;
|
||||||
icon?: React.ReactNode
|
|
||||||
|
|
||||||
dense?: boolean
|
dense?: boolean;
|
||||||
loading?: boolean
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Button({
|
function Button({
|
||||||
text, icon, title,
|
text,
|
||||||
loading, dense, disabled, noBorder, noOutline,
|
icon,
|
||||||
|
title,
|
||||||
|
loading,
|
||||||
|
dense,
|
||||||
|
disabled,
|
||||||
|
noBorder,
|
||||||
|
noOutline,
|
||||||
colors = 'clr-btn-default',
|
colors = 'clr-btn-default',
|
||||||
className,
|
className,
|
||||||
...restProps
|
...restProps
|
||||||
}: ButtonProps) {
|
}: ButtonProps) {
|
||||||
return (
|
return (
|
||||||
<button type='button'
|
<button
|
||||||
disabled={disabled ?? loading}
|
type='button'
|
||||||
className={clsx(
|
disabled={disabled ?? loading}
|
||||||
'inline-flex gap-2 items-center justify-center',
|
className={clsx(
|
||||||
'select-none disabled:cursor-not-allowed',
|
'inline-flex gap-2 items-center justify-center',
|
||||||
{
|
'select-none disabled:cursor-not-allowed',
|
||||||
'border rounded': !noBorder,
|
{
|
||||||
'px-1': dense,
|
'border rounded': !noBorder,
|
||||||
'px-3 py-2': !dense,
|
'px-1': dense,
|
||||||
'cursor-progress': loading,
|
'px-3 py-2': !dense,
|
||||||
'cursor-pointer': !loading,
|
'cursor-progress': loading,
|
||||||
'outline-none': noOutline,
|
'cursor-pointer': !loading,
|
||||||
'clr-outline': !noOutline,
|
'outline-none': noOutline,
|
||||||
},
|
'clr-outline': !noOutline
|
||||||
className,
|
},
|
||||||
colors
|
className,
|
||||||
)}
|
colors
|
||||||
data-tooltip-id={title ? globalIDs.tooltip : undefined}
|
)}
|
||||||
data-tooltip-content={title}
|
data-tooltip-id={title ? globalIDs.tooltip : undefined}
|
||||||
{...restProps}
|
data-tooltip-content={title}
|
||||||
>
|
{...restProps}
|
||||||
{icon ? icon : null}
|
>
|
||||||
{text ? <span className='font-semibold'>{text}</span> : null}
|
{icon ? icon : null}
|
||||||
</button>);
|
{text ? <span className='font-semibold'>{text}</span> : null}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Button;
|
export default Button;
|
|
@ -7,21 +7,16 @@ import { CheckboxCheckedIcon } from '../Icons';
|
||||||
import { CProps } from '../props';
|
import { CProps } from '../props';
|
||||||
import Label from './Label';
|
import Label from './Label';
|
||||||
|
|
||||||
export interface CheckboxProps
|
export interface CheckboxProps extends Omit<CProps.Button, 'value' | 'onClick'> {
|
||||||
extends Omit<CProps.Button, 'value' | 'onClick'> {
|
label?: string;
|
||||||
label?: string
|
disabled?: boolean;
|
||||||
disabled?: boolean
|
|
||||||
|
|
||||||
value: boolean
|
value: boolean;
|
||||||
setValue?: (newValue: boolean) => void
|
setValue?: (newValue: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Checkbox({
|
function Checkbox({ id, disabled, label, title, className, value, setValue, ...restProps }: CheckboxProps) {
|
||||||
id, disabled, label, title,
|
const cursor = useMemo(() => {
|
||||||
className, value, setValue, ...restProps
|
|
||||||
}: CheckboxProps) {
|
|
||||||
const cursor = useMemo(
|
|
||||||
() => {
|
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
return 'cursor-not-allowed';
|
return 'cursor-not-allowed';
|
||||||
} else if (setValue) {
|
} else if (setValue) {
|
||||||
|
@ -40,32 +35,31 @@ function Checkbox({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button type='button' id={id}
|
<button
|
||||||
className={clsx(
|
type='button'
|
||||||
'flex items-center gap-2',
|
id={id}
|
||||||
'outline-none',
|
className={clsx('flex items-center gap-2', 'outline-none', 'text-start', cursor, className)}
|
||||||
'text-start',
|
disabled={disabled}
|
||||||
cursor,
|
onClick={handleClick}
|
||||||
className
|
data-tooltip-id={title ? globalIDs.tooltip : undefined}
|
||||||
)}
|
data-tooltip-content={title}
|
||||||
disabled={disabled}
|
{...restProps}
|
||||||
onClick={handleClick}
|
>
|
||||||
data-tooltip-id={title ? (globalIDs.tooltip) : undefined}
|
<div
|
||||||
data-tooltip-content={title}
|
className={clsx('max-w-[1rem] min-w-[1rem] h-4', 'border rounded-sm', {
|
||||||
{...restProps}
|
'clr-primary': value !== false,
|
||||||
>
|
'clr-app': value === false
|
||||||
<div className={clsx(
|
})}
|
||||||
'max-w-[1rem] min-w-[1rem] h-4',
|
>
|
||||||
'border rounded-sm',
|
{value ? (
|
||||||
{
|
<div className='mt-[1px] ml-[1px]'>
|
||||||
'clr-primary': value !== false,
|
<CheckboxCheckedIcon />
|
||||||
'clr-app': value === false
|
</div>
|
||||||
}
|
) : null}
|
||||||
)}>
|
</div>
|
||||||
{value ? <div className='mt-[1px] ml-[1px]'><CheckboxCheckedIcon /></div> : null}
|
<Label className={cursor} text={label} htmlFor={id} />
|
||||||
</div>
|
</button>
|
||||||
<Label className={cursor} text={label} htmlFor={id} />
|
);
|
||||||
</button>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Checkbox;
|
export default Checkbox;
|
|
@ -5,18 +5,14 @@ import { ThreeDots } from 'react-loader-spinner';
|
||||||
import { useConceptTheme } from '@/context/ThemeContext';
|
import { useConceptTheme } from '@/context/ThemeContext';
|
||||||
|
|
||||||
interface ConceptLoaderProps {
|
interface ConceptLoaderProps {
|
||||||
size?: number
|
size?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConceptLoader({size=10}: ConceptLoaderProps) {
|
export function ConceptLoader({ size = 10 }: ConceptLoaderProps) {
|
||||||
const {colors} = useConceptTheme();
|
const { colors } = useConceptTheme();
|
||||||
return (
|
return (
|
||||||
<div className='flex justify-center'>
|
<div className='flex justify-center'>
|
||||||
<ThreeDots
|
<ThreeDots color={colors.bgSelected} height={size * 10} width={size * 10} radius={size} />
|
||||||
color={colors.bgSelected}
|
</div>
|
||||||
height={size*10}
|
);
|
||||||
width={size*10}
|
|
||||||
radius={size}
|
|
||||||
/>
|
|
||||||
</div>);
|
|
||||||
}
|
}
|
|
@ -4,30 +4,28 @@ import { CProps } from '../props';
|
||||||
import Overlay from './Overlay';
|
import Overlay from './Overlay';
|
||||||
import TextInput from './TextInput';
|
import TextInput from './TextInput';
|
||||||
|
|
||||||
interface ConceptSearchProps
|
interface ConceptSearchProps extends CProps.Styling {
|
||||||
extends CProps.Styling {
|
value: string;
|
||||||
value: string
|
onChange?: (newValue: string) => void;
|
||||||
onChange?: (newValue: string) => void
|
noBorder?: boolean;
|
||||||
noBorder?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ConceptSearch({ value, onChange, noBorder, ...restProps }: ConceptSearchProps) {
|
function ConceptSearch({ value, onChange, noBorder, ...restProps }: ConceptSearchProps) {
|
||||||
return (
|
return (
|
||||||
<div {...restProps}>
|
<div {...restProps}>
|
||||||
<Overlay
|
<Overlay position='top-[-0.125rem] left-3 translate-y-1/2' className='pointer-events-none clr-text-controls'>
|
||||||
position='top-[-0.125rem] left-3 translate-y-1/2'
|
<BiSearchAlt2 size='1.25rem' />
|
||||||
className='pointer-events-none clr-text-controls'
|
</Overlay>
|
||||||
>
|
<TextInput
|
||||||
<BiSearchAlt2 size='1.25rem' />
|
noOutline
|
||||||
</Overlay>
|
placeholder='Поиск'
|
||||||
<TextInput noOutline
|
className='pl-10'
|
||||||
placeholder='Поиск'
|
noBorder={noBorder}
|
||||||
className='pl-10'
|
value={value}
|
||||||
noBorder={noBorder}
|
onChange={event => (onChange ? onChange(event.target.value) : undefined)}
|
||||||
value={value}
|
/>
|
||||||
onChange={event => (onChange ? onChange(event.target.value) : undefined)}
|
</div>
|
||||||
/>
|
);
|
||||||
</div>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ConceptSearch;
|
export default ConceptSearch;
|
|
@ -4,31 +4,28 @@ import { Tab } from 'react-tabs';
|
||||||
|
|
||||||
import { globalIDs } from '@/utils/constants';
|
import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
interface ConceptTabProps
|
interface ConceptTabProps extends Omit<TabProps, 'children'> {
|
||||||
extends Omit<TabProps, 'children'> {
|
label?: string;
|
||||||
label?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ConceptTab({
|
function ConceptTab({ label, title, className, ...otherProps }: ConceptTabProps) {
|
||||||
label, title, className,
|
|
||||||
...otherProps
|
|
||||||
}: ConceptTabProps) {
|
|
||||||
return (
|
return (
|
||||||
<Tab
|
<Tab
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'min-w-[6rem]',
|
'min-w-[6rem]',
|
||||||
'px-2 py-1 flex justify-center',
|
'px-2 py-1 flex justify-center',
|
||||||
'clr-tab',
|
'clr-tab',
|
||||||
'text-sm whitespace-nowrap small-caps font-semibold',
|
'text-sm whitespace-nowrap small-caps font-semibold',
|
||||||
'select-none hover:cursor-pointer',
|
'select-none hover:cursor-pointer',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
data-tooltip-id={title ? (globalIDs.tooltip) : undefined}
|
data-tooltip-id={title ? globalIDs.tooltip : undefined}
|
||||||
data-tooltip-content={title}
|
data-tooltip-content={title}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</Tab>);
|
</Tab>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ConceptTab.tabsRole = 'Tab';
|
ConceptTab.tabsRole = 'Tab';
|
||||||
|
|
|
@ -7,16 +7,16 @@ import { ITooltip, Tooltip } from 'react-tooltip';
|
||||||
|
|
||||||
import { useConceptTheme } from '@/context/ThemeContext';
|
import { useConceptTheme } from '@/context/ThemeContext';
|
||||||
|
|
||||||
interface ConceptTooltipProps
|
interface ConceptTooltipProps extends Omit<ITooltip, 'variant'> {
|
||||||
extends Omit<ITooltip, 'variant'> {
|
layer?: string;
|
||||||
layer?: string
|
text?: string;
|
||||||
text?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ConceptTooltip({
|
function ConceptTooltip({
|
||||||
text, children,
|
text,
|
||||||
layer='z-tooltip',
|
children,
|
||||||
place='bottom',
|
layer = 'z-tooltip',
|
||||||
|
place = 'bottom',
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
...restProps
|
...restProps
|
||||||
|
@ -26,25 +26,22 @@ function ConceptTooltip({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return createPortal(
|
return createPortal(
|
||||||
(<Tooltip
|
<Tooltip
|
||||||
delayShow={1000}
|
delayShow={1000}
|
||||||
delayHide={100}
|
delayHide={100}
|
||||||
opacity={0.97}
|
opacity={0.97}
|
||||||
className={clsx(
|
className={clsx('overflow-hidden', 'border shadow-md', layer, className)}
|
||||||
'overflow-hidden',
|
classNameArrow={layer}
|
||||||
'border shadow-md',
|
style={{ ...{ paddingTop: '2px', paddingBottom: '2px' }, ...style }}
|
||||||
layer,
|
variant={darkMode ? 'dark' : 'light'}
|
||||||
className
|
place={place}
|
||||||
)}
|
{...restProps}
|
||||||
classNameArrow={layer}
|
>
|
||||||
style={{...{ paddingTop: '2px', paddingBottom: '2px'}, ...style}}
|
{text ? text : null}
|
||||||
variant={(darkMode ? 'dark' : 'light')}
|
{children as ReactNode}
|
||||||
place={place}
|
</Tooltip>,
|
||||||
{...restProps}
|
document.body
|
||||||
>
|
);
|
||||||
{text ? text : null}
|
|
||||||
{children as ReactNode}
|
|
||||||
</Tooltip>), document.body);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ConceptTooltip;
|
export default ConceptTooltip;
|
|
@ -1,19 +1,19 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
interface DividerProps {
|
interface DividerProps {
|
||||||
vertical?: boolean
|
vertical?: boolean;
|
||||||
margins?: string
|
margins?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Divider({ vertical, margins = 'mx-2' }: DividerProps) {
|
function Divider({ vertical, margins = 'mx-2' }: DividerProps) {
|
||||||
return (
|
return (
|
||||||
<div className={clsx(
|
<div
|
||||||
margins,
|
className={clsx(margins, {
|
||||||
{
|
'border-x': vertical,
|
||||||
'border-x': vertical,
|
'border-y': !vertical
|
||||||
'border-y': !vertical
|
})}
|
||||||
}
|
/>
|
||||||
)}/>);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Divider;
|
export default Divider;
|
|
@ -5,43 +5,38 @@ import { animateDropdown } from '@/utils/animations';
|
||||||
|
|
||||||
import { CProps } from '../props';
|
import { CProps } from '../props';
|
||||||
|
|
||||||
interface DropdownProps
|
interface DropdownProps extends CProps.Styling {
|
||||||
extends CProps.Styling {
|
stretchLeft?: boolean;
|
||||||
stretchLeft?: boolean
|
isOpen: boolean;
|
||||||
isOpen: boolean
|
children: React.ReactNode;
|
||||||
children: React.ReactNode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Dropdown({
|
function Dropdown({ isOpen, stretchLeft, className, children, ...restProps }: DropdownProps) {
|
||||||
isOpen, stretchLeft,
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...restProps
|
|
||||||
}: DropdownProps) {
|
|
||||||
return (
|
return (
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
<motion.div
|
<motion.div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'z-modal-tooltip',
|
'z-modal-tooltip',
|
||||||
'absolute mt-3',
|
'absolute mt-3',
|
||||||
'flex flex-col',
|
'flex flex-col',
|
||||||
'border rounded-md shadow-lg',
|
'border rounded-md shadow-lg',
|
||||||
'text-sm',
|
'text-sm',
|
||||||
'clr-input',
|
'clr-input',
|
||||||
{
|
{
|
||||||
'right-0': stretchLeft,
|
'right-0': stretchLeft,
|
||||||
'left-0': !stretchLeft
|
'left-0': !stretchLeft
|
||||||
},
|
},
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
initial={false}
|
initial={false}
|
||||||
animate={isOpen ? 'open' : 'closed'}
|
animate={isOpen ? 'open' : 'closed'}
|
||||||
variants={animateDropdown}
|
variants={animateDropdown}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>);
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Dropdown;
|
export default Dropdown;
|
|
@ -6,44 +6,39 @@ import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
import { CProps } from '../props';
|
import { CProps } from '../props';
|
||||||
|
|
||||||
interface DropdownButtonProps
|
interface DropdownButtonProps extends CProps.AnimatedButton {
|
||||||
extends CProps.AnimatedButton {
|
text?: string;
|
||||||
text?: string
|
icon?: React.ReactNode;
|
||||||
icon?: React.ReactNode
|
|
||||||
|
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownButton({
|
function DropdownButton({ text, icon, className, title, onClick, children, ...restProps }: DropdownButtonProps) {
|
||||||
text, icon,
|
|
||||||
className, title,
|
|
||||||
onClick,
|
|
||||||
children,
|
|
||||||
...restProps
|
|
||||||
}: DropdownButtonProps) {
|
|
||||||
return (
|
return (
|
||||||
<motion.button type='button'
|
<motion.button
|
||||||
onClick={onClick}
|
type='button'
|
||||||
className={clsx(
|
onClick={onClick}
|
||||||
'px-3 py-1 inline-flex items-center gap-2',
|
className={clsx(
|
||||||
'text-left text-sm overflow-ellipsis whitespace-nowrap',
|
'px-3 py-1 inline-flex items-center gap-2',
|
||||||
'disabled:clr-text-controls',
|
'text-left text-sm overflow-ellipsis whitespace-nowrap',
|
||||||
{
|
'disabled:clr-text-controls',
|
||||||
'clr-hover': onClick,
|
{
|
||||||
'cursor-pointer disabled:cursor-not-allowed': onClick,
|
'clr-hover': onClick,
|
||||||
'cursor-default': !onClick
|
'cursor-pointer disabled:cursor-not-allowed': onClick,
|
||||||
},
|
'cursor-default': !onClick
|
||||||
className
|
},
|
||||||
)}
|
className
|
||||||
variants={animateDropdownItem}
|
)}
|
||||||
data-tooltip-id={title ? (globalIDs.tooltip) : undefined}
|
variants={animateDropdownItem}
|
||||||
data-tooltip-content={title}
|
data-tooltip-id={title ? globalIDs.tooltip : undefined}
|
||||||
{...restProps}
|
data-tooltip-content={title}
|
||||||
>
|
{...restProps}
|
||||||
{children ? children : null}
|
>
|
||||||
{!children && icon ? icon : null}
|
{children ? children : null}
|
||||||
{!children && text ? <span>{text}</span> : null}
|
{!children && icon ? icon : null}
|
||||||
</motion.button>);
|
{!children && text ? <span>{text}</span> : null}
|
||||||
|
</motion.button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DropdownButton;
|
export default DropdownButton;
|
|
@ -6,31 +6,28 @@ import { animateDropdownItem } from '@/utils/animations';
|
||||||
import Checkbox from './Checkbox';
|
import Checkbox from './Checkbox';
|
||||||
|
|
||||||
interface DropdownCheckboxProps {
|
interface DropdownCheckboxProps {
|
||||||
value: boolean
|
value: boolean;
|
||||||
label?: string
|
label?: string;
|
||||||
title?: string
|
title?: string;
|
||||||
disabled?: boolean
|
disabled?: boolean;
|
||||||
setValue?: (newValue: boolean) => void
|
setValue?: (newValue: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownCheckbox({ title, setValue, disabled, ...restProps }: DropdownCheckboxProps) {
|
function DropdownCheckbox({ title, setValue, disabled, ...restProps }: DropdownCheckboxProps) {
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
variants={animateDropdownItem}
|
variants={animateDropdownItem}
|
||||||
title={title}
|
title={title}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'px-3 py-1',
|
'px-3 py-1',
|
||||||
'text-left overflow-ellipsis whitespace-nowrap',
|
'text-left overflow-ellipsis whitespace-nowrap',
|
||||||
'disabled:clr-text-controls',
|
'disabled:clr-text-controls',
|
||||||
!!setValue && !disabled && 'clr-hover'
|
!!setValue && !disabled && 'clr-hover'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox disabled={disabled} setValue={setValue} {...restProps} />
|
||||||
disabled={disabled}
|
</motion.div>
|
||||||
setValue={setValue}
|
);
|
||||||
{...restProps}
|
|
||||||
/>
|
|
||||||
</motion.div>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DropdownCheckbox;
|
export default DropdownCheckbox;
|
|
@ -1,35 +1,37 @@
|
||||||
interface EmbedYoutubeProps {
|
interface EmbedYoutubeProps {
|
||||||
videoID: string
|
videoID: string;
|
||||||
pxHeight: number
|
pxHeight: number;
|
||||||
pxWidth?: number
|
pxWidth?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmbedYoutube({ videoID, pxHeight, pxWidth }: EmbedYoutubeProps) {
|
function EmbedYoutube({ videoID, pxHeight, pxWidth }: EmbedYoutubeProps) {
|
||||||
if (!pxWidth) {
|
if (!pxWidth) {
|
||||||
pxWidth = pxHeight * 16 / 9;
|
pxWidth = (pxHeight * 16) / 9;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='relative'
|
className='relative'
|
||||||
style={{
|
|
||||||
height: 0,
|
|
||||||
paddingBottom: `${pxHeight}px`,
|
|
||||||
paddingLeft: `${pxWidth}px`
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<iframe allowFullScreen
|
|
||||||
title='Встроенное видео Youtube'
|
|
||||||
allow='accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture'
|
|
||||||
className='absolute top-0 left-0 border'
|
|
||||||
style={{
|
style={{
|
||||||
minHeight: `${pxHeight}px`,
|
height: 0,
|
||||||
minWidth: `${pxWidth}px`
|
paddingBottom: `${pxHeight}px`,
|
||||||
|
paddingLeft: `${pxWidth}px`
|
||||||
}}
|
}}
|
||||||
width={`${pxWidth}px`}
|
>
|
||||||
height={`${pxHeight}px`}
|
<iframe
|
||||||
src={`https://www.youtube.com/embed/${videoID}`}
|
allowFullScreen
|
||||||
/>
|
title='Встроенное видео Youtube'
|
||||||
</div>);
|
allow='accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture'
|
||||||
|
className='absolute top-0 left-0 border'
|
||||||
|
style={{
|
||||||
|
minHeight: `${pxHeight}px`,
|
||||||
|
minWidth: `${pxWidth}px`
|
||||||
|
}}
|
||||||
|
width={`${pxWidth}px`}
|
||||||
|
height={`${pxHeight}px`}
|
||||||
|
src={`https://www.youtube.com/embed/${videoID}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EmbedYoutube;
|
export default EmbedYoutube;
|
|
@ -8,20 +8,14 @@ import { CProps } from '../props';
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
import Label from './Label';
|
import Label from './Label';
|
||||||
|
|
||||||
interface FileInputProps
|
interface FileInputProps extends Omit<CProps.Input, 'accept' | 'type'> {
|
||||||
extends Omit<CProps.Input, 'accept' | 'type'> {
|
label: string;
|
||||||
label: string
|
|
||||||
|
|
||||||
acceptType?: string
|
acceptType?: string;
|
||||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
|
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function FileInput({
|
function FileInput({ label, acceptType, title, className, style, onChange, ...restProps }: FileInputProps) {
|
||||||
label, acceptType, title,
|
|
||||||
className, style,
|
|
||||||
onChange,
|
|
||||||
...restProps
|
|
||||||
}: FileInputProps) {
|
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const [fileName, setFileName] = useState('');
|
const [fileName, setFileName] = useState('');
|
||||||
|
|
||||||
|
@ -41,29 +35,19 @@ function FileInput({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={clsx('py-2', 'flex flex-col gap-2 items-center', className)} style={style}>
|
||||||
className={clsx(
|
<input
|
||||||
'py-2',
|
type='file'
|
||||||
'flex flex-col gap-2 items-center',
|
ref={inputRef}
|
||||||
className
|
style={{ display: 'none' }}
|
||||||
)}
|
accept={acceptType}
|
||||||
style={style}
|
onChange={handleFileChange}
|
||||||
>
|
{...restProps}
|
||||||
<input type='file'
|
/>
|
||||||
ref={inputRef}
|
<Button text={label} icon={<BiUpload size='1.5rem' />} onClick={handleUploadClick} title={title} />
|
||||||
style={{ display: 'none' }}
|
<Label text={fileName} />
|
||||||
accept={acceptType}
|
</div>
|
||||||
onChange={handleFileChange}
|
);
|
||||||
{...restProps}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
text={label}
|
|
||||||
icon={<BiUpload size='1.5rem' />}
|
|
||||||
onClick={handleUploadClick}
|
|
||||||
title={title}
|
|
||||||
/>
|
|
||||||
<Label text={fileName} />
|
|
||||||
</div>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FileInput;
|
export default FileInput;
|
|
@ -4,23 +4,14 @@ import { classnames } from '@/utils/constants';
|
||||||
|
|
||||||
import { CProps } from '../props';
|
import { CProps } from '../props';
|
||||||
|
|
||||||
export interface FlexColumnProps
|
export interface FlexColumnProps extends CProps.Div {}
|
||||||
extends CProps.Div {}
|
|
||||||
|
|
||||||
function FlexColumn({
|
function FlexColumn({ className, children, ...restProps }: FlexColumnProps) {
|
||||||
className, children,
|
|
||||||
...restProps
|
|
||||||
}: FlexColumnProps) {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={clsx(classnames.flex_col, className)} {...restProps}>
|
||||||
className={clsx(
|
{children}
|
||||||
classnames.flex_col,
|
</div>
|
||||||
className
|
);
|
||||||
)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FlexColumn;
|
export default FlexColumn;
|
|
@ -3,10 +3,6 @@
|
||||||
|
|
||||||
import { GraphCanvas as GraphUI } from 'reagraph';
|
import { GraphCanvas as GraphUI } from 'reagraph';
|
||||||
|
|
||||||
export {
|
export { type GraphEdge, type GraphNode, type GraphCanvasRef, type LayoutTypes, Sphere, useSelection } from 'reagraph';
|
||||||
type GraphEdge, type GraphNode, type GraphCanvasRef,
|
|
||||||
type LayoutTypes,
|
|
||||||
Sphere, useSelection
|
|
||||||
} from 'reagraph';
|
|
||||||
|
|
||||||
export default GraphUI;
|
export default GraphUI;
|
|
@ -2,9 +2,8 @@ import clsx from 'clsx';
|
||||||
|
|
||||||
import { CProps } from '../props';
|
import { CProps } from '../props';
|
||||||
|
|
||||||
interface LabelProps
|
interface LabelProps extends CProps.Label {
|
||||||
extends CProps.Label {
|
text?: string;
|
||||||
text?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Label({ text, className, ...restProps }: LabelProps) {
|
function Label({ text, className, ...restProps }: LabelProps) {
|
||||||
|
@ -12,15 +11,10 @@ function Label({ text, className, ...restProps }: LabelProps) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<label
|
<label className={clsx('text-sm font-semibold whitespace-nowrap', className)} {...restProps}>
|
||||||
className={clsx(
|
{text}
|
||||||
'text-sm font-semibold whitespace-nowrap',
|
</label>
|
||||||
className
|
);
|
||||||
)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</label>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Label;
|
export default Label;
|
|
@ -1,24 +1,19 @@
|
||||||
interface LabeledValueProps {
|
interface LabeledValueProps {
|
||||||
id?: string
|
id?: string;
|
||||||
label: string
|
label: string;
|
||||||
text: string | number
|
text: string | number;
|
||||||
title?: string
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function LabeledValue({ id, label, text, title }: LabeledValueProps) {
|
function LabeledValue({ id, label, text, title }: LabeledValueProps) {
|
||||||
return (
|
return (
|
||||||
<div className='flex justify-between gap-3'>
|
<div className='flex justify-between gap-3'>
|
||||||
<label
|
<label className='font-semibold' title={title} htmlFor={id}>
|
||||||
className='font-semibold'
|
{label}
|
||||||
title={title}
|
</label>
|
||||||
htmlFor={id}
|
<span id={id}>{text}</span>
|
||||||
>
|
</div>
|
||||||
{label}
|
);
|
||||||
</label>
|
|
||||||
<span id={id}>
|
|
||||||
{text}
|
|
||||||
</span>
|
|
||||||
</div>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LabeledValue;
|
export default LabeledValue;
|
|
@ -4,37 +4,34 @@ import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
import { CProps } from '../props';
|
import { CProps } from '../props';
|
||||||
|
|
||||||
interface MiniButtonProps
|
interface MiniButtonProps extends CProps.Button {
|
||||||
extends CProps.Button {
|
icon: React.ReactNode;
|
||||||
icon: React.ReactNode
|
noHover?: boolean;
|
||||||
noHover?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function MiniButton({
|
function MiniButton({ icon, noHover, tabIndex, title, className, ...restProps }: MiniButtonProps) {
|
||||||
icon, noHover, tabIndex,
|
|
||||||
title, className,
|
|
||||||
...restProps
|
|
||||||
}: MiniButtonProps) {
|
|
||||||
return (
|
return (
|
||||||
<button type='button'
|
<button
|
||||||
tabIndex={tabIndex ?? -1}
|
type='button'
|
||||||
className={clsx(
|
tabIndex={tabIndex ?? -1}
|
||||||
'px-1 py-1',
|
className={clsx(
|
||||||
'rounded-full',
|
'px-1 py-1',
|
||||||
'clr-btn-clear',
|
'rounded-full',
|
||||||
'cursor-pointer disabled:cursor-not-allowed',
|
'clr-btn-clear',
|
||||||
{
|
'cursor-pointer disabled:cursor-not-allowed',
|
||||||
'outline-none': noHover,
|
{
|
||||||
'clr-hover': !noHover
|
'outline-none': noHover,
|
||||||
},
|
'clr-hover': !noHover
|
||||||
className
|
},
|
||||||
)}
|
className
|
||||||
data-tooltip-id={title ? (globalIDs.tooltip) : undefined}
|
)}
|
||||||
data-tooltip-content={title}
|
data-tooltip-id={title ? globalIDs.tooltip : undefined}
|
||||||
{...restProps}
|
data-tooltip-content={title}
|
||||||
>
|
{...restProps}
|
||||||
{icon}
|
>
|
||||||
</button>);
|
{icon}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MiniButton;
|
export default MiniButton;
|
|
@ -13,26 +13,30 @@ import Button from './Button';
|
||||||
import MiniButton from './MiniButton';
|
import MiniButton from './MiniButton';
|
||||||
import Overlay from './Overlay';
|
import Overlay from './Overlay';
|
||||||
|
|
||||||
export interface ModalProps
|
export interface ModalProps extends CProps.Styling {
|
||||||
extends CProps.Styling {
|
header?: string;
|
||||||
header?: string
|
submitText?: string;
|
||||||
submitText?: string
|
submitInvalidTooltip?: string;
|
||||||
submitInvalidTooltip?: string
|
|
||||||
|
|
||||||
readonly?: boolean
|
readonly?: boolean;
|
||||||
canSubmit?: boolean
|
canSubmit?: boolean;
|
||||||
|
|
||||||
hideWindow: () => void
|
hideWindow: () => void;
|
||||||
onSubmit?: () => void
|
onSubmit?: () => void;
|
||||||
onCancel?: () => void
|
onCancel?: () => void;
|
||||||
|
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Modal({
|
function Modal({
|
||||||
header, hideWindow, onSubmit,
|
header,
|
||||||
readonly, onCancel, canSubmit,
|
hideWindow,
|
||||||
submitInvalidTooltip, className,
|
onSubmit,
|
||||||
|
readonly,
|
||||||
|
onCancel,
|
||||||
|
canSubmit,
|
||||||
|
submitInvalidTooltip,
|
||||||
|
className,
|
||||||
children,
|
children,
|
||||||
submitText = 'Продолжить',
|
submitText = 'Продолжить',
|
||||||
...restProps
|
...restProps
|
||||||
|
@ -51,69 +55,58 @@ function Modal({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={clsx(
|
<div className={clsx('z-navigation', 'fixed top-0 left-0', 'w-full h-full', 'clr-modal-backdrop')} />
|
||||||
'z-navigation',
|
<motion.div
|
||||||
'fixed top-0 left-0',
|
ref={ref}
|
||||||
'w-full h-full',
|
|
||||||
'clr-modal-backdrop'
|
|
||||||
)}/>
|
|
||||||
<motion.div ref={ref}
|
|
||||||
className={clsx(
|
|
||||||
'z-modal',
|
|
||||||
'fixed bottom-1/2 left-1/2 -translate-x-1/2 translate-y-1/2',
|
|
||||||
'border shadow-md',
|
|
||||||
'clr-app'
|
|
||||||
)}
|
|
||||||
initial={{...animateModal.initial}}
|
|
||||||
animate={{...animateModal.animate}}
|
|
||||||
exit={{...animateModal.exit}}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
<Overlay position='right-[0.3rem] top-2'>
|
|
||||||
<MiniButton
|
|
||||||
title='Закрыть диалоговое окно [ESC]'
|
|
||||||
icon={<BiX size='1.25rem'/>}
|
|
||||||
onClick={handleCancel}
|
|
||||||
/>
|
|
||||||
</Overlay>
|
|
||||||
|
|
||||||
{header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null}
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'overflow-auto',
|
'z-modal',
|
||||||
className
|
'fixed bottom-1/2 left-1/2 -translate-x-1/2 translate-y-1/2',
|
||||||
|
'border shadow-md',
|
||||||
|
'clr-app'
|
||||||
)}
|
)}
|
||||||
style={{
|
initial={{ ...animateModal.initial }}
|
||||||
maxHeight: 'calc(100vh - 8rem)',
|
animate={{ ...animateModal.animate }}
|
||||||
maxWidth: 'calc(100vw - 2rem)',
|
exit={{ ...animateModal.exit }}
|
||||||
}}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{children}
|
<Overlay position='right-[0.3rem] top-2'>
|
||||||
</div>
|
<MiniButton title='Закрыть диалоговое окно [ESC]' icon={<BiX size='1.25rem' />} onClick={handleCancel} />
|
||||||
|
</Overlay>
|
||||||
|
|
||||||
<div className={clsx(
|
{header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null}
|
||||||
'z-modal-controls',
|
|
||||||
'px-6 py-3 flex gap-12 justify-center'
|
<div
|
||||||
)}>
|
className={clsx('overflow-auto', className)}
|
||||||
{!readonly ?
|
style={{
|
||||||
<Button autoFocus
|
maxHeight: 'calc(100vh - 8rem)',
|
||||||
text={submitText}
|
maxWidth: 'calc(100vw - 2rem)'
|
||||||
title={!canSubmit ? submitInvalidTooltip: ''}
|
}}
|
||||||
className='min-w-[8rem] min-h-[2.6rem]'
|
>
|
||||||
colors='clr-btn-primary'
|
{children}
|
||||||
disabled={!canSubmit}
|
</div>
|
||||||
onClick={handleSubmit}
|
|
||||||
/> : null}
|
<div className={clsx('z-modal-controls', 'px-6 py-3 flex gap-12 justify-center')}>
|
||||||
<Button
|
{!readonly ? (
|
||||||
text={readonly ? 'Закрыть' : 'Отмена'}
|
<Button
|
||||||
className='min-w-[8rem] min-h-[2.6rem]'
|
autoFocus
|
||||||
onClick={handleCancel}
|
text={submitText}
|
||||||
/>
|
title={!canSubmit ? submitInvalidTooltip : ''}
|
||||||
</div>
|
className='min-w-[8rem] min-h-[2.6rem]'
|
||||||
</motion.div>
|
colors='clr-btn-primary'
|
||||||
</>);
|
disabled={!canSubmit}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<Button
|
||||||
|
text={readonly ? 'Закрыть' : 'Отмена'}
|
||||||
|
className='min-w-[8rem] min-h-[2.6rem]'
|
||||||
|
onClick={handleCancel}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Modal;
|
export default Modal;
|
|
@ -1,34 +1,22 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { CProps } from '../props'
|
import { CProps } from '../props';
|
||||||
|
|
||||||
interface OverlayProps extends CProps.Styling {
|
interface OverlayProps extends CProps.Styling {
|
||||||
id?: string
|
id?: string;
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
position?: string
|
position?: string;
|
||||||
layer?: string
|
layer?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Overlay({
|
function Overlay({ children, className, position = 'top-0 right-0', layer = 'z-pop', ...restProps }: OverlayProps) {
|
||||||
children, className,
|
|
||||||
position='top-0 right-0',
|
|
||||||
layer='z-pop',
|
|
||||||
...restProps
|
|
||||||
}: OverlayProps) {
|
|
||||||
return (
|
return (
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
<div
|
<div className={clsx('absolute', className, position, layer)} {...restProps}>
|
||||||
className={clsx(
|
{children}
|
||||||
'absolute',
|
</div>
|
||||||
className,
|
</div>
|
||||||
position,
|
);
|
||||||
layer
|
|
||||||
)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Overlay;
|
export default Overlay;
|
|
@ -1,71 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import type { PDFDocumentProxy } from 'pdfjs-dist';
|
|
||||||
import { useMemo, useState } from 'react';
|
|
||||||
import { Document, Page } from 'react-pdf';
|
|
||||||
|
|
||||||
import useWindowSize from '@/hooks/useWindowSize';
|
|
||||||
import { graphLightT } from '@/utils/color';
|
|
||||||
|
|
||||||
import Overlay from './Overlay';
|
|
||||||
import PageControls from './PageControls';
|
|
||||||
|
|
||||||
const MAXIMUM_WIDTH = 1000;
|
|
||||||
const MINIMUM_WIDTH = 600;
|
|
||||||
|
|
||||||
interface PDFViewerProps {
|
|
||||||
file?: string | ArrayBuffer | Blob
|
|
||||||
}
|
|
||||||
|
|
||||||
function PDFViewer({ file }: PDFViewerProps) {
|
|
||||||
const windowSize = useWindowSize();
|
|
||||||
|
|
||||||
const [pageCount, setPageCount] = useState(0);
|
|
||||||
const [pageNumber, setPageNumber] = useState(1);
|
|
||||||
|
|
||||||
const pageWidth = useMemo(
|
|
||||||
() => {
|
|
||||||
return Math.max(MINIMUM_WIDTH, (Math.min((windowSize?.width ?? 0) - 300, MAXIMUM_WIDTH)));
|
|
||||||
}, [windowSize]);
|
|
||||||
|
|
||||||
function onDocumentLoadSuccess({ numPages }: PDFDocumentProxy) {
|
|
||||||
setPageCount(numPages);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Document
|
|
||||||
file={file}
|
|
||||||
onLoadSuccess={onDocumentLoadSuccess}
|
|
||||||
className='px-3'
|
|
||||||
loading='Загрузка PDF файла...'
|
|
||||||
error='Не удалось загрузить файл.'
|
|
||||||
>
|
|
||||||
<Overlay
|
|
||||||
position='top-6 left-1/2 -translate-x-1/2'
|
|
||||||
className='flex select-none'
|
|
||||||
>
|
|
||||||
<PageControls
|
|
||||||
pageCount={pageCount}
|
|
||||||
pageNumber={pageNumber}
|
|
||||||
setPageNumber={setPageNumber}
|
|
||||||
/>
|
|
||||||
</Overlay>
|
|
||||||
<Page
|
|
||||||
className='pointer-events-none select-none'
|
|
||||||
renderTextLayer={false}
|
|
||||||
renderAnnotationLayer={false}
|
|
||||||
pageNumber={pageNumber}
|
|
||||||
width={pageWidth}
|
|
||||||
canvasBackground={graphLightT.canvas.background}
|
|
||||||
/>
|
|
||||||
<Overlay position='bottom-6 left-1/2 -translate-x-1/2' className='flex select-none'>
|
|
||||||
<PageControls
|
|
||||||
pageCount={pageCount}
|
|
||||||
pageNumber={pageNumber}
|
|
||||||
setPageNumber={setPageNumber}
|
|
||||||
/>
|
|
||||||
</Overlay>
|
|
||||||
</Document>);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PDFViewer;
|
|
|
@ -1,44 +0,0 @@
|
||||||
import { BiChevronLeft, BiChevronRight, BiFirstPage, BiLastPage } from 'react-icons/bi';
|
|
||||||
|
|
||||||
interface PageControlsProps {
|
|
||||||
pageNumber: number
|
|
||||||
pageCount: number
|
|
||||||
setPageNumber: React.Dispatch<React.SetStateAction<number>>
|
|
||||||
}
|
|
||||||
|
|
||||||
function PageControls({ pageNumber, pageCount, setPageNumber }: PageControlsProps) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<button type='button'
|
|
||||||
className='clr-hover clr-text-controls'
|
|
||||||
onClick={() => setPageNumber(1)}
|
|
||||||
disabled={pageNumber < 2}
|
|
||||||
>
|
|
||||||
<BiFirstPage size='1.5rem' />
|
|
||||||
</button>
|
|
||||||
<button type='button'
|
|
||||||
className='clr-hover clr-text-controls'
|
|
||||||
onClick={() => setPageNumber(prev => prev - 1)}
|
|
||||||
disabled={pageNumber < 2}
|
|
||||||
>
|
|
||||||
<BiChevronLeft size='1.5rem' />
|
|
||||||
</button>
|
|
||||||
<p className='px-3 text-black'>Страница {pageNumber} из {pageCount}</p>
|
|
||||||
<button type='button'
|
|
||||||
className='clr-hover clr-text-controls'
|
|
||||||
onClick={() => setPageNumber(prev => prev + 1)}
|
|
||||||
disabled={pageNumber >= pageCount}
|
|
||||||
>
|
|
||||||
<BiChevronRight size='1.5rem' />
|
|
||||||
</button>
|
|
||||||
<button type='button'
|
|
||||||
className='clr-hover clr-text-controls'
|
|
||||||
onClick={() => setPageNumber(pageCount)}
|
|
||||||
disabled={pageNumber >= pageCount}
|
|
||||||
>
|
|
||||||
<BiLastPage size='1.5rem' />
|
|
||||||
</button>
|
|
||||||
</>);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PageControls;
|
|
|
@ -1,12 +1,9 @@
|
||||||
interface PrettyJsonProps {
|
interface PrettyJsonProps {
|
||||||
data: unknown
|
data: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PrettyJson({ data }: PrettyJsonProps) {
|
function PrettyJson({ data }: PrettyJsonProps) {
|
||||||
return (
|
return <pre>{JSON.stringify(data, null, 2)}</pre>;
|
||||||
<pre>
|
|
||||||
{JSON.stringify(data, null, 2)}
|
|
||||||
</pre>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PrettyJson;
|
export default PrettyJson;
|
|
@ -6,69 +6,69 @@ import Select, { GroupBase, Props, StylesConfig } from 'react-select';
|
||||||
import { useConceptTheme } from '@/context/ThemeContext';
|
import { useConceptTheme } from '@/context/ThemeContext';
|
||||||
import { selectDarkT, selectLightT } from '@/utils/color';
|
import { selectDarkT, selectLightT } from '@/utils/color';
|
||||||
|
|
||||||
export interface SelectMultiProps<
|
export interface SelectMultiProps<Option, Group extends GroupBase<Option> = GroupBase<Option>>
|
||||||
Option,
|
extends Omit<Props<Option, true, Group>, 'theme' | 'menuPortalTarget'> {
|
||||||
Group extends GroupBase<Option> = GroupBase<Option>
|
noPortal?: boolean;
|
||||||
>
|
|
||||||
extends Omit<Props<Option, true, Group>, 'theme' | 'menuPortalTarget'> {
|
|
||||||
noPortal?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectMulti<Option, Group extends GroupBase<Option> = GroupBase<Option>> ({
|
function SelectMulti<Option, Group extends GroupBase<Option> = GroupBase<Option>>({
|
||||||
noPortal, ...restProps
|
noPortal,
|
||||||
|
...restProps
|
||||||
}: SelectMultiProps<Option, Group>) {
|
}: SelectMultiProps<Option, Group>) {
|
||||||
const { darkMode, colors } = useConceptTheme();
|
const { darkMode, colors } = useConceptTheme();
|
||||||
const themeColors = useMemo(
|
const themeColors = useMemo(() => (!darkMode ? selectLightT : selectDarkT), [darkMode]);
|
||||||
() => !darkMode ? selectLightT : selectDarkT
|
|
||||||
, [darkMode]);
|
|
||||||
|
|
||||||
const adjustedStyles: StylesConfig<Option, true, Group> = useMemo(
|
const adjustedStyles: StylesConfig<Option, true, Group> = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
control: (styles, { isDisabled }) => ({
|
control: (styles, { isDisabled }) => ({
|
||||||
...styles,
|
...styles,
|
||||||
borderRadius: '0.25rem',
|
borderRadius: '0.25rem',
|
||||||
cursor: isDisabled ? 'not-allowed' : 'pointer',
|
cursor: isDisabled ? 'not-allowed' : 'pointer',
|
||||||
boxShadow: 'none'
|
boxShadow: 'none'
|
||||||
|
}),
|
||||||
|
option: (styles, { isSelected }) => ({
|
||||||
|
...styles,
|
||||||
|
backgroundColor: isSelected ? colors.bgSelected : styles.backgroundColor,
|
||||||
|
color: isSelected ? colors.fgSelected : styles.color,
|
||||||
|
borderWidth: '1px',
|
||||||
|
borderColor: colors.border
|
||||||
|
}),
|
||||||
|
menuPortal: styles => ({
|
||||||
|
...styles,
|
||||||
|
zIndex: 9999
|
||||||
|
}),
|
||||||
|
menuList: styles => ({
|
||||||
|
...styles,
|
||||||
|
padding: '0px'
|
||||||
|
}),
|
||||||
|
input: styles => ({ ...styles }),
|
||||||
|
placeholder: styles => ({ ...styles }),
|
||||||
|
multiValue: styles => ({
|
||||||
|
...styles,
|
||||||
|
borderRadius: '0.5rem',
|
||||||
|
backgroundColor: colors.bgSelected
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
option: (styles, { isSelected }) => ({
|
[colors]
|
||||||
...styles,
|
);
|
||||||
backgroundColor: isSelected ? colors.bgSelected : styles.backgroundColor,
|
|
||||||
color: isSelected ? colors.fgSelected : styles.color,
|
|
||||||
borderWidth: '1px',
|
|
||||||
borderColor: colors.border
|
|
||||||
}),
|
|
||||||
menuPortal: (styles) => ({
|
|
||||||
...styles,
|
|
||||||
zIndex: 9999
|
|
||||||
}),
|
|
||||||
menuList: (styles) => ({
|
|
||||||
...styles,
|
|
||||||
padding: '0px'
|
|
||||||
}),
|
|
||||||
input: (styles) => ({...styles}),
|
|
||||||
placeholder: (styles) => ({...styles}),
|
|
||||||
multiValue: (styles) => ({
|
|
||||||
...styles,
|
|
||||||
borderRadius: '0.5rem',
|
|
||||||
backgroundColor: colors.bgSelected,
|
|
||||||
})
|
|
||||||
}), [colors]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select isMulti
|
<Select
|
||||||
noOptionsMessage={() => 'Список пуст'}
|
isMulti
|
||||||
theme={theme => ({
|
noOptionsMessage={() => 'Список пуст'}
|
||||||
...theme,
|
theme={theme => ({
|
||||||
borderRadius: 0,
|
...theme,
|
||||||
colors: {
|
borderRadius: 0,
|
||||||
...theme.colors,
|
colors: {
|
||||||
...themeColors
|
...theme.colors,
|
||||||
},
|
...themeColors
|
||||||
})}
|
}
|
||||||
menuPortalTarget={!noPortal ? document.body : null}
|
})}
|
||||||
styles={adjustedStyles}
|
menuPortalTarget={!noPortal ? document.body : null}
|
||||||
{...restProps}
|
styles={adjustedStyles}
|
||||||
/>);
|
{...restProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SelectMulti;
|
export default SelectMulti;
|
|
@ -6,65 +6,64 @@ import Select, { GroupBase, Props, StylesConfig } from 'react-select';
|
||||||
import { useConceptTheme } from '@/context/ThemeContext';
|
import { useConceptTheme } from '@/context/ThemeContext';
|
||||||
import { selectDarkT, selectLightT } from '@/utils/color';
|
import { selectDarkT, selectLightT } from '@/utils/color';
|
||||||
|
|
||||||
interface SelectSingleProps<
|
interface SelectSingleProps<Option, Group extends GroupBase<Option> = GroupBase<Option>>
|
||||||
Option,
|
extends Omit<Props<Option, false, Group>, 'theme' | 'menuPortalTarget'> {
|
||||||
Group extends GroupBase<Option> = GroupBase<Option>
|
noPortal?: boolean;
|
||||||
>
|
|
||||||
extends Omit<Props<Option, false, Group>, 'theme' | 'menuPortalTarget'> {
|
|
||||||
noPortal?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectSingle<Option, Group extends GroupBase<Option> = GroupBase<Option>> ({
|
function SelectSingle<Option, Group extends GroupBase<Option> = GroupBase<Option>>({
|
||||||
noPortal, ...restProps
|
noPortal,
|
||||||
|
...restProps
|
||||||
}: SelectSingleProps<Option, Group>) {
|
}: SelectSingleProps<Option, Group>) {
|
||||||
const { darkMode, colors } = useConceptTheme();
|
const { darkMode, colors } = useConceptTheme();
|
||||||
const themeColors = useMemo(
|
const themeColors = useMemo(() => (!darkMode ? selectLightT : selectDarkT), [darkMode]);
|
||||||
() => !darkMode ? selectLightT : selectDarkT
|
|
||||||
, [darkMode]);
|
|
||||||
|
|
||||||
const adjustedStyles: StylesConfig<Option, false, Group> = useMemo(
|
const adjustedStyles: StylesConfig<Option, false, Group> = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
control: (styles, { isDisabled }) => ({
|
control: (styles, { isDisabled }) => ({
|
||||||
...styles,
|
...styles,
|
||||||
borderRadius: '0.25rem',
|
borderRadius: '0.25rem',
|
||||||
cursor: isDisabled ? 'not-allowed' : 'pointer',
|
cursor: isDisabled ? 'not-allowed' : 'pointer',
|
||||||
boxShadow: 'none'
|
boxShadow: 'none'
|
||||||
|
}),
|
||||||
|
menuPortal: styles => ({
|
||||||
|
...styles,
|
||||||
|
zIndex: 9999
|
||||||
|
}),
|
||||||
|
menuList: styles => ({
|
||||||
|
...styles,
|
||||||
|
padding: '0px'
|
||||||
|
}),
|
||||||
|
option: (styles, { isSelected }) => ({
|
||||||
|
...styles,
|
||||||
|
backgroundColor: isSelected ? colors.bgSelected : styles.backgroundColor,
|
||||||
|
color: isSelected ? colors.fgSelected : styles.color,
|
||||||
|
borderWidth: '1px',
|
||||||
|
borderColor: colors.border
|
||||||
|
}),
|
||||||
|
input: styles => ({ ...styles }),
|
||||||
|
placeholder: styles => ({ ...styles }),
|
||||||
|
singleValue: styles => ({ ...styles })
|
||||||
}),
|
}),
|
||||||
menuPortal: (styles) => ({
|
[colors]
|
||||||
...styles,
|
);
|
||||||
zIndex: 9999
|
|
||||||
}),
|
|
||||||
menuList: (styles) => ({
|
|
||||||
...styles,
|
|
||||||
padding: '0px'
|
|
||||||
}),
|
|
||||||
option: (styles, { isSelected }) => ({
|
|
||||||
...styles,
|
|
||||||
backgroundColor: isSelected ? colors.bgSelected : styles.backgroundColor,
|
|
||||||
color: isSelected ? colors.fgSelected : styles.color,
|
|
||||||
borderWidth: '1px',
|
|
||||||
borderColor: colors.border
|
|
||||||
}),
|
|
||||||
input: (styles) => ({...styles}),
|
|
||||||
placeholder: (styles) => ({...styles}),
|
|
||||||
singleValue: (styles) => ({...styles}),
|
|
||||||
}), [colors]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
noOptionsMessage={() => 'Список пуст'}
|
noOptionsMessage={() => 'Список пуст'}
|
||||||
theme={theme => ({
|
theme={theme => ({
|
||||||
...theme,
|
...theme,
|
||||||
borderRadius: 0,
|
borderRadius: 0,
|
||||||
colors: {
|
colors: {
|
||||||
...theme.colors,
|
...theme.colors,
|
||||||
...themeColors
|
...themeColors
|
||||||
},
|
}
|
||||||
})}
|
})}
|
||||||
menuPortalTarget={!noPortal ? document.body : null}
|
menuPortalTarget={!noPortal ? document.body : null}
|
||||||
styles={adjustedStyles}
|
styles={adjustedStyles}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>);
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SelectSingle;
|
export default SelectSingle;
|
|
@ -4,43 +4,46 @@ import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
import { CProps } from '../props';
|
import { CProps } from '../props';
|
||||||
|
|
||||||
interface SelectorButtonProps
|
interface SelectorButtonProps extends CProps.Button {
|
||||||
extends CProps.Button {
|
text?: string;
|
||||||
text?: string
|
icon?: React.ReactNode;
|
||||||
icon?: React.ReactNode
|
|
||||||
|
|
||||||
colors?: string
|
colors?: string;
|
||||||
transparent?: boolean
|
transparent?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectorButton({
|
function SelectorButton({
|
||||||
text, icon, title,
|
text,
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
colors = 'clr-btn-default',
|
colors = 'clr-btn-default',
|
||||||
className,
|
className,
|
||||||
transparent,
|
transparent,
|
||||||
...restProps
|
...restProps
|
||||||
}: SelectorButtonProps) {
|
}: SelectorButtonProps) {
|
||||||
return (
|
return (
|
||||||
<button type='button'
|
<button
|
||||||
data-tooltip-id={title ? (globalIDs.tooltip) : undefined}
|
type='button'
|
||||||
data-tooltip-content={title}
|
data-tooltip-id={title ? globalIDs.tooltip : undefined}
|
||||||
className={clsx(
|
data-tooltip-content={title}
|
||||||
'px-1 flex flex-start items-center gap-1',
|
className={clsx(
|
||||||
'text-sm small-caps select-none',
|
'px-1 flex flex-start items-center gap-1',
|
||||||
'text-btn clr-text-controls',
|
'text-sm small-caps select-none',
|
||||||
'disabled:cursor-not-allowed cursor-pointer',
|
'text-btn clr-text-controls',
|
||||||
{
|
'disabled:cursor-not-allowed cursor-pointer',
|
||||||
'clr-hover': transparent,
|
{
|
||||||
'border': !transparent,
|
'clr-hover': transparent,
|
||||||
},
|
'border': !transparent
|
||||||
className,
|
},
|
||||||
!transparent && colors
|
className,
|
||||||
)}
|
!transparent && colors
|
||||||
{...restProps}
|
)}
|
||||||
>
|
{...restProps}
|
||||||
{icon ? icon : null}
|
>
|
||||||
{text ? <div className={'font-semibold whitespace-nowrap pb-1'}>{text}</div> : null}
|
{icon ? icon : null}
|
||||||
</button>);
|
{text ? <div className={'font-semibold whitespace-nowrap pb-1'}>{text}</div> : null}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SelectorButton;
|
export default SelectorButton;
|
|
@ -2,36 +2,32 @@ import clsx from 'clsx';
|
||||||
|
|
||||||
import { CProps } from '../props';
|
import { CProps } from '../props';
|
||||||
|
|
||||||
interface SubmitButtonProps
|
interface SubmitButtonProps extends CProps.Button {
|
||||||
extends CProps.Button {
|
text?: string;
|
||||||
text?: string
|
loading?: boolean;
|
||||||
loading?: boolean
|
icon?: React.ReactNode;
|
||||||
icon?: React.ReactNode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function SubmitButton({
|
function SubmitButton({ text = 'ОК', icon, disabled, loading, className, ...restProps }: SubmitButtonProps) {
|
||||||
text = 'ОК',
|
|
||||||
icon, disabled, loading,
|
|
||||||
className,
|
|
||||||
...restProps
|
|
||||||
}: SubmitButtonProps) {
|
|
||||||
return (
|
return (
|
||||||
<button type='submit'
|
<button
|
||||||
className={clsx(
|
type='submit'
|
||||||
'px-3 py-2 flex gap-2 items-center justify-center',
|
className={clsx(
|
||||||
'border',
|
'px-3 py-2 flex gap-2 items-center justify-center',
|
||||||
'font-semibold',
|
'border',
|
||||||
'clr-btn-primary',
|
'font-semibold',
|
||||||
'select-none disabled:cursor-not-allowed',
|
'clr-btn-primary',
|
||||||
loading && 'cursor-progress',
|
'select-none disabled:cursor-not-allowed',
|
||||||
className
|
loading && 'cursor-progress',
|
||||||
)}
|
className
|
||||||
disabled={disabled ?? loading}
|
)}
|
||||||
{...restProps}
|
disabled={disabled ?? loading}
|
||||||
>
|
{...restProps}
|
||||||
{icon ? <span>{icon}</span> : null}
|
>
|
||||||
{text ? <span>{text}</span> : null}
|
{icon ? <span>{icon}</span> : null}
|
||||||
</button>);
|
{text ? <span>{text}</span> : null}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SubmitButton;
|
export default SubmitButton;
|
|
@ -2,39 +2,46 @@ import clsx from 'clsx';
|
||||||
|
|
||||||
import { CProps } from '../props';
|
import { CProps } from '../props';
|
||||||
|
|
||||||
interface SwitchButtonProps<ValueType>
|
interface SwitchButtonProps<ValueType> extends CProps.Styling {
|
||||||
extends CProps.Styling {
|
id?: string;
|
||||||
id?: string
|
value: ValueType;
|
||||||
value: ValueType
|
label?: string;
|
||||||
label?: string
|
icon?: React.ReactNode;
|
||||||
icon?: React.ReactNode
|
title?: string;
|
||||||
title?: string
|
|
||||||
|
|
||||||
isSelected?: boolean
|
isSelected?: boolean;
|
||||||
onSelect: (value: ValueType) => void
|
onSelect: (value: ValueType) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SwitchButton<ValueType>({
|
function SwitchButton<ValueType>({
|
||||||
value, icon, label, className,
|
value,
|
||||||
isSelected, onSelect, ...restProps
|
icon,
|
||||||
|
label,
|
||||||
|
className,
|
||||||
|
isSelected,
|
||||||
|
onSelect,
|
||||||
|
...restProps
|
||||||
}: SwitchButtonProps<ValueType>) {
|
}: SwitchButtonProps<ValueType>) {
|
||||||
return (
|
return (
|
||||||
<button type='button' tabIndex={-1}
|
<button
|
||||||
onClick={() => onSelect(value)}
|
type='button'
|
||||||
className={clsx(
|
tabIndex={-1}
|
||||||
'px-2 py-1',
|
onClick={() => onSelect(value)}
|
||||||
'border rounded-none',
|
className={clsx(
|
||||||
'font-semibold small-caps',
|
'px-2 py-1',
|
||||||
'clr-btn-clear clr-hover',
|
'border rounded-none',
|
||||||
'cursor-pointer',
|
'font-semibold small-caps',
|
||||||
isSelected && 'clr-selected',
|
'clr-btn-clear clr-hover',
|
||||||
className
|
'cursor-pointer',
|
||||||
)}
|
isSelected && 'clr-selected',
|
||||||
{...restProps}
|
className
|
||||||
>
|
)}
|
||||||
{icon ? icon : null}
|
{...restProps}
|
||||||
{label}
|
>
|
||||||
</button>);
|
{icon ? icon : null}
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SwitchButton;
|
export default SwitchButton;
|
|
@ -3,44 +3,52 @@ import clsx from 'clsx';
|
||||||
import { CProps } from '../props';
|
import { CProps } from '../props';
|
||||||
import Label from './Label';
|
import Label from './Label';
|
||||||
|
|
||||||
export interface TextAreaProps
|
export interface TextAreaProps extends CProps.Editor, CProps.Colors, CProps.TextArea {
|
||||||
extends CProps.Editor, CProps.Colors, CProps.TextArea {
|
dense?: boolean;
|
||||||
dense?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function TextArea({
|
function TextArea({
|
||||||
id, label, required, rows,
|
id,
|
||||||
dense, noBorder, noOutline,
|
label,
|
||||||
|
required,
|
||||||
|
rows,
|
||||||
|
dense,
|
||||||
|
noBorder,
|
||||||
|
noOutline,
|
||||||
className,
|
className,
|
||||||
colors = 'clr-input',
|
colors = 'clr-input',
|
||||||
...restProps
|
...restProps
|
||||||
}: TextAreaProps) {
|
}: TextAreaProps) {
|
||||||
return (
|
return (
|
||||||
<div className={clsx(
|
<div
|
||||||
{
|
|
||||||
'flex flex-col gap-2': !dense,
|
|
||||||
'flex items-center gap-3': dense
|
|
||||||
},
|
|
||||||
dense && className,
|
|
||||||
)}>
|
|
||||||
<Label text={label} htmlFor={id} />
|
|
||||||
<textarea id={id}
|
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'px-3 py-2',
|
|
||||||
'leading-tight',
|
|
||||||
{
|
{
|
||||||
'border': !noBorder,
|
'flex flex-col gap-2': !dense,
|
||||||
'flex-grow': dense,
|
'flex items-center gap-3': dense
|
||||||
'clr-outline': !noOutline
|
|
||||||
},
|
},
|
||||||
colors,
|
dense && className
|
||||||
!dense && className
|
|
||||||
)}
|
)}
|
||||||
rows={rows}
|
>
|
||||||
required={required}
|
<Label text={label} htmlFor={id} />
|
||||||
{...restProps}
|
<textarea
|
||||||
/>
|
id={id}
|
||||||
</div>);
|
className={clsx(
|
||||||
|
'px-3 py-2',
|
||||||
|
'leading-tight',
|
||||||
|
{
|
||||||
|
'border': !noBorder,
|
||||||
|
'flex-grow': dense,
|
||||||
|
'clr-outline': !noOutline
|
||||||
|
},
|
||||||
|
colors,
|
||||||
|
!dense && className
|
||||||
|
)}
|
||||||
|
rows={rows}
|
||||||
|
required={required}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TextArea;
|
export default TextArea;
|
|
@ -3,10 +3,9 @@ import clsx from 'clsx';
|
||||||
import { CProps } from '../props';
|
import { CProps } from '../props';
|
||||||
import Label from './Label';
|
import Label from './Label';
|
||||||
|
|
||||||
interface TextInputProps
|
interface TextInputProps extends CProps.Editor, CProps.Colors, CProps.Input {
|
||||||
extends CProps.Editor, CProps.Colors, CProps.Input {
|
dense?: boolean;
|
||||||
dense?: boolean
|
allowEnter?: boolean;
|
||||||
allowEnter?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function preventEnterCapture(event: React.KeyboardEvent<HTMLInputElement>) {
|
function preventEnterCapture(event: React.KeyboardEvent<HTMLInputElement>) {
|
||||||
|
@ -16,40 +15,49 @@ function preventEnterCapture(event: React.KeyboardEvent<HTMLInputElement>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function TextInput({
|
function TextInput({
|
||||||
id, label,
|
id,
|
||||||
dense, noBorder, noOutline, allowEnter, disabled,
|
label,
|
||||||
|
dense,
|
||||||
|
noBorder,
|
||||||
|
noOutline,
|
||||||
|
allowEnter,
|
||||||
|
disabled,
|
||||||
className,
|
className,
|
||||||
colors = 'clr-input',
|
colors = 'clr-input',
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
...restProps
|
...restProps
|
||||||
}: TextInputProps) {
|
}: TextInputProps) {
|
||||||
return (
|
return (
|
||||||
<div className={clsx(
|
<div
|
||||||
{
|
|
||||||
'flex flex-col gap-2': !dense,
|
|
||||||
'flex items-center gap-3': dense,
|
|
||||||
},
|
|
||||||
dense && className
|
|
||||||
)}>
|
|
||||||
<Label text={label} htmlFor={id} />
|
|
||||||
<input id={id}
|
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'py-2',
|
|
||||||
'leading-tight truncate hover:text-clip',
|
|
||||||
{
|
{
|
||||||
'px-3': !noBorder || !disabled,
|
'flex flex-col gap-2': !dense,
|
||||||
'flex-grow': dense,
|
'flex items-center gap-3': dense
|
||||||
'border': !noBorder,
|
|
||||||
'clr-outline': !noOutline
|
|
||||||
},
|
},
|
||||||
colors,
|
dense && className
|
||||||
!dense && className
|
|
||||||
)}
|
)}
|
||||||
onKeyDown={!allowEnter && !onKeyDown ? preventEnterCapture : onKeyDown}
|
>
|
||||||
disabled={disabled}
|
<Label text={label} htmlFor={id} />
|
||||||
{...restProps}
|
<input
|
||||||
/>
|
id={id}
|
||||||
</div>);
|
className={clsx(
|
||||||
|
'py-2',
|
||||||
|
'leading-tight truncate hover:text-clip',
|
||||||
|
{
|
||||||
|
'px-3': !noBorder || !disabled,
|
||||||
|
'flex-grow': dense,
|
||||||
|
'border': !noBorder,
|
||||||
|
'clr-outline': !noOutline
|
||||||
|
},
|
||||||
|
colors,
|
||||||
|
!dense && className
|
||||||
|
)}
|
||||||
|
onKeyDown={!allowEnter && !onKeyDown ? preventEnterCapture : onKeyDown}
|
||||||
|
disabled={disabled}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TextInput;
|
export default TextInput;
|
|
@ -1,34 +1,27 @@
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
|
||||||
interface TextURLProps {
|
interface TextURLProps {
|
||||||
text: string
|
text: string;
|
||||||
title?: string
|
title?: string;
|
||||||
href?: string
|
href?: string;
|
||||||
color?: string
|
color?: string;
|
||||||
onClick?: () => void
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TextURL({ text, href, title, color='clr-text-url', onClick }: TextURLProps) {
|
function TextURL({ text, href, title, color = 'clr-text-url', onClick }: TextURLProps) {
|
||||||
const design = `cursor-pointer hover:underline ${color}`;
|
const design = `cursor-pointer hover:underline ${color}`;
|
||||||
if (href) {
|
if (href) {
|
||||||
return (
|
return (
|
||||||
<Link tabIndex={-1}
|
<Link tabIndex={-1} className={design} title={title} to={href}>
|
||||||
className={design}
|
{text}
|
||||||
title={title}
|
</Link>
|
||||||
to={href}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</Link>
|
|
||||||
);
|
);
|
||||||
} else if (onClick) {
|
} else if (onClick) {
|
||||||
return (
|
return (
|
||||||
<span tabIndex={-1}
|
<span tabIndex={-1} className={design} onClick={onClick}>
|
||||||
className={design}
|
{text}
|
||||||
onClick={onClick}
|
</span>
|
||||||
>
|
);
|
||||||
{text}
|
|
||||||
</span>);
|
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
@ -8,26 +7,19 @@ import { CheckboxCheckedIcon, CheckboxNullIcon } from '../Icons';
|
||||||
import { CheckboxProps } from './Checkbox';
|
import { CheckboxProps } from './Checkbox';
|
||||||
import Label from './Label';
|
import Label from './Label';
|
||||||
|
|
||||||
export interface TristateProps
|
export interface TristateProps extends Omit<CheckboxProps, 'value' | 'setValue'> {
|
||||||
extends Omit<CheckboxProps, 'value' | 'setValue'> {
|
value: boolean | null;
|
||||||
value: boolean | null
|
setValue?: (newValue: boolean | null) => void;
|
||||||
setValue?: (newValue: boolean | null) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Tristate({
|
function Tristate({ id, disabled, label, title, className, value, setValue, ...restProps }: TristateProps) {
|
||||||
id, disabled, label, title,
|
const cursor = useMemo(() => {
|
||||||
className,
|
|
||||||
value, setValue,
|
|
||||||
...restProps
|
|
||||||
}: TristateProps) {
|
|
||||||
const cursor = useMemo(
|
|
||||||
() => {
|
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
return 'cursor-not-allowed';
|
return 'cursor-not-allowed';
|
||||||
} else if (setValue) {
|
} else if (setValue) {
|
||||||
return 'cursor-pointer';
|
return 'cursor-pointer';
|
||||||
} else {
|
} else {
|
||||||
return ''
|
return '';
|
||||||
}
|
}
|
||||||
}, [disabled, setValue]);
|
}, [disabled, setValue]);
|
||||||
|
|
||||||
|
@ -46,32 +38,36 @@ function Tristate({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button type='button' id={id}
|
<button
|
||||||
className={clsx(
|
type='button'
|
||||||
'flex items-center gap-2 text-start',
|
id={id}
|
||||||
'outline-none',
|
className={clsx('flex items-center gap-2 text-start', 'outline-none', cursor, className)}
|
||||||
cursor,
|
disabled={disabled}
|
||||||
className
|
onClick={handleClick}
|
||||||
)}
|
data-tooltip-id={title ? globalIDs.tooltip : undefined}
|
||||||
disabled={disabled}
|
data-tooltip-content={title}
|
||||||
onClick={handleClick}
|
{...restProps}
|
||||||
data-tooltip-id={title ? (globalIDs.tooltip) : undefined}
|
>
|
||||||
data-tooltip-content={title}
|
<div
|
||||||
{...restProps}
|
className={clsx('w-4 h-4', 'border rounded-sm', {
|
||||||
>
|
'clr-primary': value !== false,
|
||||||
<div className={clsx(
|
'clr-app': value === false
|
||||||
'w-4 h-4',
|
})}
|
||||||
'border rounded-sm',
|
>
|
||||||
{
|
{value ? (
|
||||||
'clr-primary': value !== false,
|
<div className='mt-[1px] ml-[1px]'>
|
||||||
'clr-app': value === false
|
<CheckboxCheckedIcon />
|
||||||
}
|
</div>
|
||||||
)}>
|
) : null}
|
||||||
{value ? <div className='mt-[1px] ml-[1px]'><CheckboxCheckedIcon /></div> : null}
|
{value == null ? (
|
||||||
{value == null ? <div className='mt-[1px] ml-[1px]'><CheckboxNullIcon /></div> : null}
|
<div className='mt-[1px] ml-[1px]'>
|
||||||
</div>
|
<CheckboxNullIcon />
|
||||||
<Label className={cursor} text={label} htmlFor={id} />
|
</div>
|
||||||
</button>);
|
) : null}
|
||||||
|
</div>
|
||||||
|
<Label className={cursor} text={label} htmlFor={id} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Tristate;
|
export default Tristate;
|
|
@ -2,16 +2,12 @@ import { ToastContainer, type ToastContainerProps } from 'react-toastify';
|
||||||
|
|
||||||
import { useConceptTheme } from '@/context/ThemeContext';
|
import { useConceptTheme } from '@/context/ThemeContext';
|
||||||
|
|
||||||
interface ToasterThemedProps extends Omit<ToastContainerProps, 'theme'>{}
|
interface ToasterThemedProps extends Omit<ToastContainerProps, 'theme'> {}
|
||||||
|
|
||||||
function ToasterThemed(props: ToasterThemedProps) {
|
function ToasterThemed(props: ToasterThemedProps) {
|
||||||
const { darkMode } = useConceptTheme();
|
const { darkMode } = useConceptTheme();
|
||||||
|
|
||||||
return (
|
return <ToastContainer theme={darkMode ? 'dark' : 'light'} {...props} />;
|
||||||
<ToastContainer
|
|
||||||
theme={ darkMode ? 'dark' : 'light'}
|
|
||||||
{...props}
|
|
||||||
/>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ToasterThemed;
|
export default ToasterThemed;
|
|
@ -2,10 +2,17 @@
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ColumnSort,
|
ColumnSort,
|
||||||
createColumnHelper, getCoreRowModel,
|
createColumnHelper,
|
||||||
getPaginationRowModel, getSortedRowModel,
|
getCoreRowModel,
|
||||||
PaginationState, RowData, type RowSelectionState,
|
getPaginationRowModel,
|
||||||
SortingState, TableOptions, useReactTable, type VisibilityState
|
getSortedRowModel,
|
||||||
|
PaginationState,
|
||||||
|
RowData,
|
||||||
|
type RowSelectionState,
|
||||||
|
SortingState,
|
||||||
|
TableOptions,
|
||||||
|
useReactTable,
|
||||||
|
type VisibilityState
|
||||||
} from '@tanstack/react-table';
|
} from '@tanstack/react-table';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
@ -20,39 +27,37 @@ import TableHeader from './TableHeader';
|
||||||
export { createColumnHelper, type ColumnSort, type RowSelectionState, type VisibilityState };
|
export { createColumnHelper, type ColumnSort, type RowSelectionState, type VisibilityState };
|
||||||
|
|
||||||
export interface IConditionalStyle<TData> {
|
export interface IConditionalStyle<TData> {
|
||||||
when: (rowData: TData) => boolean
|
when: (rowData: TData) => boolean;
|
||||||
style: React.CSSProperties
|
style: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DataTableProps<TData extends RowData>
|
export interface DataTableProps<TData extends RowData>
|
||||||
extends CProps.Styling, Pick<TableOptions<TData>,
|
extends CProps.Styling,
|
||||||
'data' | 'columns' |
|
Pick<TableOptions<TData>, 'data' | 'columns' | 'onRowSelectionChange' | 'onColumnVisibilityChange'> {
|
||||||
'onRowSelectionChange' | 'onColumnVisibilityChange'
|
dense?: boolean;
|
||||||
> {
|
headPosition?: string;
|
||||||
dense?: boolean
|
noHeader?: boolean;
|
||||||
headPosition?: string
|
noFooter?: boolean;
|
||||||
noHeader?: boolean
|
|
||||||
noFooter?: boolean
|
|
||||||
|
|
||||||
conditionalRowStyles?: IConditionalStyle<TData>[]
|
conditionalRowStyles?: IConditionalStyle<TData>[];
|
||||||
noDataComponent?: React.ReactNode
|
noDataComponent?: React.ReactNode;
|
||||||
|
|
||||||
onRowClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void
|
onRowClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void;
|
||||||
onRowDoubleClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void
|
onRowDoubleClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void;
|
||||||
|
|
||||||
enableRowSelection?: boolean
|
enableRowSelection?: boolean;
|
||||||
rowSelection?: RowSelectionState
|
rowSelection?: RowSelectionState;
|
||||||
|
|
||||||
enableHiding?: boolean
|
enableHiding?: boolean;
|
||||||
columnVisibility?: VisibilityState
|
columnVisibility?: VisibilityState;
|
||||||
|
|
||||||
enablePagination?: boolean
|
enablePagination?: boolean;
|
||||||
paginationPerPage?: number
|
paginationPerPage?: number;
|
||||||
paginationOptions?: number[]
|
paginationOptions?: number[];
|
||||||
onChangePaginationOption?: (newValue: number) => void
|
onChangePaginationOption?: (newValue: number) => void;
|
||||||
|
|
||||||
enableSorting?: boolean
|
enableSorting?: boolean;
|
||||||
initialSorting?: ColumnSort
|
initialSorting?: ColumnSort;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -60,11 +65,18 @@ extends CProps.Styling, Pick<TableOptions<TData>,
|
||||||
*
|
*
|
||||||
* @param headPosition - Top position of sticky header (0 if no other sticky elements are present).
|
* @param headPosition - Top position of sticky header (0 if no other sticky elements are present).
|
||||||
* No sticky header if omitted
|
* No sticky header if omitted
|
||||||
*/
|
*/
|
||||||
function DataTable<TData extends RowData>({
|
function DataTable<TData extends RowData>({
|
||||||
style, className,
|
style,
|
||||||
dense, headPosition, conditionalRowStyles, noFooter, noHeader,
|
className,
|
||||||
onRowClicked, onRowDoubleClicked, noDataComponent,
|
dense,
|
||||||
|
headPosition,
|
||||||
|
conditionalRowStyles,
|
||||||
|
noFooter,
|
||||||
|
noHeader,
|
||||||
|
onRowClicked,
|
||||||
|
onRowDoubleClicked,
|
||||||
|
noDataComponent,
|
||||||
|
|
||||||
enableRowSelection,
|
enableRowSelection,
|
||||||
rowSelection,
|
rowSelection,
|
||||||
|
@ -76,8 +88,8 @@ function DataTable<TData extends RowData>({
|
||||||
initialSorting,
|
initialSorting,
|
||||||
|
|
||||||
enablePagination,
|
enablePagination,
|
||||||
paginationPerPage=10,
|
paginationPerPage = 10,
|
||||||
paginationOptions=[10, 20, 30, 40, 50],
|
paginationOptions = [10, 20, 30, 40, 50],
|
||||||
onChangePaginationOption,
|
onChangePaginationOption,
|
||||||
|
|
||||||
...restProps
|
...restProps
|
||||||
|
@ -86,7 +98,7 @@ function DataTable<TData extends RowData>({
|
||||||
|
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
pageSize: paginationPerPage,
|
pageSize: paginationPerPage
|
||||||
});
|
});
|
||||||
|
|
||||||
const tableImpl = useReactTable({
|
const tableImpl = useReactTable({
|
||||||
|
@ -110,39 +122,39 @@ function DataTable<TData extends RowData>({
|
||||||
const isEmpty = tableImpl.getRowModel().rows.length === 0;
|
const isEmpty = tableImpl.getRowModel().rows.length === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(className)} style={style}>
|
<div className={clsx(className)} style={style}>
|
||||||
<table className='w-full'>
|
<table className='w-full'>
|
||||||
{!noHeader ?
|
{!noHeader ? (
|
||||||
<TableHeader
|
<TableHeader
|
||||||
table={tableImpl}
|
table={tableImpl}
|
||||||
enableRowSelection={enableRowSelection}
|
enableRowSelection={enableRowSelection}
|
||||||
enableSorting={enableSorting}
|
enableSorting={enableSorting}
|
||||||
headPosition={headPosition}
|
headPosition={headPosition}
|
||||||
/>: null}
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<TableBody
|
<TableBody
|
||||||
table={tableImpl}
|
table={tableImpl}
|
||||||
dense={dense}
|
dense={dense}
|
||||||
conditionalRowStyles={conditionalRowStyles}
|
conditionalRowStyles={conditionalRowStyles}
|
||||||
enableRowSelection={enableRowSelection}
|
enableRowSelection={enableRowSelection}
|
||||||
onRowClicked={onRowClicked}
|
onRowClicked={onRowClicked}
|
||||||
onRowDoubleClicked={onRowDoubleClicked}
|
onRowDoubleClicked={onRowDoubleClicked}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!noFooter ?
|
{!noFooter ? <TableFooter table={tableImpl} /> : null}
|
||||||
<TableFooter
|
</table>
|
||||||
table={tableImpl}
|
|
||||||
/>: null}
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{(enablePagination && !isEmpty) ?
|
{enablePagination && !isEmpty ? (
|
||||||
<PaginationTools
|
<PaginationTools
|
||||||
table={tableImpl}
|
table={tableImpl}
|
||||||
paginationOptions={paginationOptions}
|
paginationOptions={paginationOptions}
|
||||||
onChangePaginationOption={onChangePaginationOption}
|
onChangePaginationOption={onChangePaginationOption}
|
||||||
/> : null}
|
/>
|
||||||
{isEmpty ? (noDataComponent ?? <DefaultNoData />) : null}
|
) : null}
|
||||||
</div>);
|
{isEmpty ? noDataComponent ?? <DefaultNoData /> : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DataTable;
|
export default DataTable;
|
|
@ -1,8 +1,5 @@
|
||||||
function defaultNoDataComponent() {
|
function defaultNoDataComponent() {
|
||||||
return (
|
return <div className='p-2 text-center'>Данные отсутствуют</div>;
|
||||||
<div className='p-2 text-center'>
|
|
||||||
Данные отсутствуют
|
|
||||||
</div>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defaultNoDataComponent;
|
export default defaultNoDataComponent;
|
|
@ -8,90 +8,93 @@ import { BiChevronLeft, BiChevronRight, BiFirstPage, BiLastPage } from 'react-ic
|
||||||
import { prefixes } from '@/utils/constants';
|
import { prefixes } from '@/utils/constants';
|
||||||
|
|
||||||
interface PaginationToolsProps<TData> {
|
interface PaginationToolsProps<TData> {
|
||||||
table: Table<TData>
|
table: Table<TData>;
|
||||||
paginationOptions: number[]
|
paginationOptions: number[];
|
||||||
onChangePaginationOption?: (newValue: number) => void
|
onChangePaginationOption?: (newValue: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PaginationTools<TData>({ table, paginationOptions, onChangePaginationOption }: PaginationToolsProps<TData>) {
|
function PaginationTools<TData>({ 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) {
|
if (onChangePaginationOption) {
|
||||||
onChangePaginationOption(perPage);
|
onChangePaginationOption(perPage);
|
||||||
}
|
}
|
||||||
}, [onChangePaginationOption, table]);
|
},
|
||||||
|
[onChangePaginationOption, table]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(
|
<div className={clsx('flex justify-end items-center', 'my-2', 'text-sm', 'clr-text-controls', 'select-none')}>
|
||||||
'flex justify-end items-center',
|
<span className='mr-3'>
|
||||||
'my-2',
|
{`${table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1}
|
||||||
'text-sm',
|
|
||||||
'clr-text-controls',
|
|
||||||
'select-none'
|
|
||||||
)}>
|
|
||||||
<span className='mr-3'>
|
|
||||||
{`${table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1}
|
|
||||||
-
|
-
|
||||||
${Math.min(table.getFilteredRowModel().rows.length, (table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize)}
|
${Math.min(
|
||||||
|
table.getFilteredRowModel().rows.length,
|
||||||
|
(table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize
|
||||||
|
)}
|
||||||
из
|
из
|
||||||
${table.getFilteredRowModel().rows.length}`}
|
${table.getFilteredRowModel().rows.length}`}
|
||||||
</span>
|
</span>
|
||||||
<div className='flex'>
|
<div className='flex'>
|
||||||
<button type='button'
|
<button
|
||||||
className='clr-hover clr-text-controls'
|
type='button'
|
||||||
onClick={() => table.setPageIndex(0)}
|
className='clr-hover clr-text-controls'
|
||||||
disabled={!table.getCanPreviousPage()}
|
onClick={() => table.setPageIndex(0)}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
<BiFirstPage size='1.5rem' />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='clr-hover clr-text-controls'
|
||||||
|
onClick={() => table.previousPage()}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
<BiChevronLeft size='1.5rem' />
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
title='Номер страницы. Выделите для ручного ввода'
|
||||||
|
className='w-6 text-center clr-app'
|
||||||
|
value={table.getState().pagination.pageIndex + 1}
|
||||||
|
onChange={event => {
|
||||||
|
const page = event.target.value ? Number(event.target.value) - 1 : 0;
|
||||||
|
if (page + 1 <= table.getPageCount()) {
|
||||||
|
table.setPageIndex(page);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='clr-hover clr-text-controls'
|
||||||
|
onClick={() => table.nextPage()}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
<BiChevronRight size='1.5rem' />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='clr-hover clr-text-controls'
|
||||||
|
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
<BiLastPage size='1.5rem' />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
value={table.getState().pagination.pageSize}
|
||||||
|
onChange={handlePaginationOptionsChange}
|
||||||
|
className='mx-2 cursor-pointer clr-app'
|
||||||
>
|
>
|
||||||
<BiFirstPage size='1.5rem' />
|
{paginationOptions.map(pageSize => (
|
||||||
</button>
|
<option key={`${prefixes.page_size}${pageSize}`} value={pageSize}>
|
||||||
<button type='button'
|
{pageSize} на стр
|
||||||
className='clr-hover clr-text-controls'
|
</option>
|
||||||
onClick={() => table.previousPage()}
|
))}
|
||||||
disabled={!table.getCanPreviousPage()}
|
</select>
|
||||||
>
|
|
||||||
<BiChevronLeft size='1.5rem' />
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
title='Номер страницы. Выделите для ручного ввода'
|
|
||||||
className='w-6 text-center clr-app'
|
|
||||||
value={table.getState().pagination.pageIndex + 1}
|
|
||||||
onChange={event => {
|
|
||||||
const page = event.target.value ? Number(event.target.value) - 1 : 0;
|
|
||||||
if (page + 1 <= table.getPageCount()) {
|
|
||||||
table.setPageIndex(page);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<button type='button'
|
|
||||||
className='clr-hover clr-text-controls'
|
|
||||||
onClick={() => table.nextPage()}
|
|
||||||
disabled={!table.getCanNextPage()}
|
|
||||||
>
|
|
||||||
<BiChevronRight size='1.5rem' />
|
|
||||||
</button>
|
|
||||||
<button type='button'
|
|
||||||
className='clr-hover clr-text-controls'
|
|
||||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
|
||||||
disabled={!table.getCanNextPage()}
|
|
||||||
>
|
|
||||||
<BiLastPage size='1.5rem' />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<select
|
);
|
||||||
value={table.getState().pagination.pageSize}
|
|
||||||
onChange={handlePaginationOptionsChange}
|
|
||||||
className='mx-2 cursor-pointer clr-app'
|
|
||||||
>
|
|
||||||
{paginationOptions.map(
|
|
||||||
(pageSize) => (
|
|
||||||
<option key={`${prefixes.page_size}${pageSize}`} value={pageSize}>
|
|
||||||
{pageSize} на стр
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PaginationTools;
|
export default PaginationTools;
|
|
@ -3,20 +3,20 @@ import { Table } from '@tanstack/react-table';
|
||||||
import Tristate from '@/components/Common/Tristate';
|
import Tristate from '@/components/Common/Tristate';
|
||||||
|
|
||||||
interface SelectAllProps<TData> {
|
interface SelectAllProps<TData> {
|
||||||
table: Table<TData>
|
table: Table<TData>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectAll<TData>({ table }: SelectAllProps<TData>) {
|
function SelectAll<TData>({ table }: SelectAllProps<TData>) {
|
||||||
return (
|
return (
|
||||||
<Tristate tabIndex={-1}
|
<Tristate
|
||||||
title='Выделить все'
|
tabIndex={-1}
|
||||||
value={
|
title='Выделить все'
|
||||||
(!table.getIsAllPageRowsSelected() && table.getIsSomePageRowsSelected())
|
value={
|
||||||
? null
|
!table.getIsAllPageRowsSelected() && table.getIsSomePageRowsSelected() ? null : table.getIsAllPageRowsSelected()
|
||||||
: table.getIsAllPageRowsSelected()
|
}
|
||||||
}
|
setValue={value => table.toggleAllPageRowsSelected(value !== false)}
|
||||||
setValue={value => table.toggleAllPageRowsSelected(value !== false)}
|
/>
|
||||||
/>);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SelectAll;
|
export default SelectAll;
|
|
@ -3,15 +3,11 @@ import { Row } from '@tanstack/react-table';
|
||||||
import Checkbox from '@/components/Common/Checkbox';
|
import Checkbox from '@/components/Common/Checkbox';
|
||||||
|
|
||||||
interface SelectRowProps<TData> {
|
interface SelectRowProps<TData> {
|
||||||
row: Row<TData>
|
row: Row<TData>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectRow<TData>({ row }: SelectRowProps<TData>) {
|
function SelectRow<TData>({ row }: SelectRowProps<TData>) {
|
||||||
return (
|
return <Checkbox tabIndex={-1} value={row.getIsSelected()} setValue={row.getToggleSelectedHandler()} />;
|
||||||
<Checkbox tabIndex={-1}
|
|
||||||
value={row.getIsSelected()}
|
|
||||||
setValue={row.getToggleSelectedHandler()}
|
|
||||||
/>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SelectRow;
|
export default SelectRow;
|
|
@ -2,18 +2,18 @@ import { Column } from '@tanstack/react-table';
|
||||||
import { BiCaretDown, BiCaretUp } from 'react-icons/bi';
|
import { BiCaretDown, BiCaretUp } from 'react-icons/bi';
|
||||||
|
|
||||||
interface SortingIconProps<TData> {
|
interface SortingIconProps<TData> {
|
||||||
column: Column<TData>
|
column: Column<TData>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SortingIcon<TData>({ column }: SortingIconProps<TData>) {
|
function SortingIcon<TData>({ column }: SortingIconProps<TData>) {
|
||||||
return (<>
|
return (
|
||||||
{{
|
<>
|
||||||
desc: <BiCaretDown size='1rem' />,
|
{{
|
||||||
asc: <BiCaretUp size='1rem'/>,
|
desc: <BiCaretDown size='1rem' />,
|
||||||
}[column.getIsSorted() as string] ??
|
asc: <BiCaretUp size='1rem' />
|
||||||
<BiCaretDown size='1rem' className='opacity-0 hover:opacity-50' />
|
}[column.getIsSorted() as string] ?? <BiCaretDown size='1rem' className='opacity-0 hover:opacity-50' />}
|
||||||
}
|
</>
|
||||||
</>);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SortingIcon;
|
export default SortingIcon;
|
|
@ -4,19 +4,21 @@ import { IConditionalStyle } from '.';
|
||||||
import SelectRow from './SelectRow';
|
import SelectRow from './SelectRow';
|
||||||
|
|
||||||
interface TableBodyProps<TData> {
|
interface TableBodyProps<TData> {
|
||||||
table: Table<TData>
|
table: Table<TData>;
|
||||||
dense?: boolean
|
dense?: boolean;
|
||||||
enableRowSelection?: boolean
|
enableRowSelection?: boolean;
|
||||||
conditionalRowStyles?: IConditionalStyle<TData>[]
|
conditionalRowStyles?: IConditionalStyle<TData>[];
|
||||||
onRowClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void
|
onRowClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void;
|
||||||
onRowDoubleClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void
|
onRowDoubleClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TableBody<TData>({
|
function TableBody<TData>({
|
||||||
table, dense,
|
table,
|
||||||
|
dense,
|
||||||
enableRowSelection,
|
enableRowSelection,
|
||||||
conditionalRowStyles,
|
conditionalRowStyles,
|
||||||
onRowClicked, onRowDoubleClicked
|
onRowClicked,
|
||||||
|
onRowDoubleClicked
|
||||||
}: TableBodyProps<TData>) {
|
}: TableBodyProps<TData>) {
|
||||||
function handleRowClicked(row: Row<TData>, event: React.MouseEvent<Element, MouseEvent>) {
|
function handleRowClicked(row: Row<TData>, event: React.MouseEvent<Element, MouseEvent>) {
|
||||||
if (onRowClicked) {
|
if (onRowClicked) {
|
||||||
|
@ -28,47 +30,51 @@ function TableBody<TData>({
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRowStyles(row: Row<TData>) {
|
function getRowStyles(row: Row<TData>) {
|
||||||
return ({...conditionalRowStyles!
|
return {
|
||||||
.filter(item => item.when(row.original))
|
...conditionalRowStyles!
|
||||||
.reduce((prev, item) => ({...prev, ...item.style}), {})
|
.filter(item => item.when(row.original))
|
||||||
});
|
.reduce((prev, item) => ({ ...prev, ...item.style }), {})
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tbody>
|
<tbody>
|
||||||
{table.getRowModel().rows.map(
|
{table.getRowModel().rows.map((row: Row<TData>, index) => (
|
||||||
(row: Row<TData>, index) => (
|
<tr
|
||||||
<tr
|
key={row.id}
|
||||||
key={row.id}
|
className={
|
||||||
className={
|
row.getIsSelected()
|
||||||
row.getIsSelected() ? 'clr-selected clr-hover' :
|
? 'clr-selected clr-hover'
|
||||||
index % 2 === 0 ? 'clr-controls clr-hover' : 'clr-app clr-hover'
|
: index % 2 === 0
|
||||||
}
|
? 'clr-controls clr-hover'
|
||||||
style={conditionalRowStyles && getRowStyles(row)}
|
: 'clr-app clr-hover'
|
||||||
>
|
}
|
||||||
{enableRowSelection ?
|
style={conditionalRowStyles && getRowStyles(row)}
|
||||||
<td key={`select-${row.id}`} className='pl-3 pr-1 border-y'>
|
>
|
||||||
<SelectRow row={row} />
|
{enableRowSelection ? (
|
||||||
</td> : null}
|
<td key={`select-${row.id}`} className='pl-3 pr-1 border-y'>
|
||||||
{row.getVisibleCells().map(
|
<SelectRow row={row} />
|
||||||
(cell: Cell<TData, unknown>) => (
|
</td>
|
||||||
<td
|
) : null}
|
||||||
key={cell.id}
|
{row.getVisibleCells().map((cell: Cell<TData, unknown>) => (
|
||||||
className='px-2 border-y'
|
<td
|
||||||
style={{
|
key={cell.id}
|
||||||
cursor: onRowClicked || onRowDoubleClicked ? 'pointer': 'auto',
|
className='px-2 border-y'
|
||||||
paddingBottom: dense ? '0.25rem': '0.5rem',
|
style={{
|
||||||
paddingTop: dense ? '0.25rem': '0.5rem'
|
cursor: onRowClicked || onRowDoubleClicked ? 'pointer' : 'auto',
|
||||||
}}
|
paddingBottom: dense ? '0.25rem' : '0.5rem',
|
||||||
onClick={event => handleRowClicked(row, event)}
|
paddingTop: dense ? '0.25rem' : '0.5rem'
|
||||||
onDoubleClick={event => onRowDoubleClicked ? onRowDoubleClicked(row.original, event) : undefined}
|
}}
|
||||||
>
|
onClick={event => handleRowClicked(row, event)}
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
onDoubleClick={event => (onRowDoubleClicked ? onRowDoubleClicked(row.original, event) : undefined)}
|
||||||
</td>
|
>
|
||||||
))}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
</tr>
|
</td>
|
||||||
))}
|
))}
|
||||||
</tbody>);
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TableBody;
|
export default TableBody;
|
|
@ -1,24 +1,23 @@
|
||||||
import { flexRender, Header, HeaderGroup, Table } from '@tanstack/react-table';
|
import { flexRender, Header, HeaderGroup, Table } from '@tanstack/react-table';
|
||||||
|
|
||||||
interface TableFooterProps<TData> {
|
interface TableFooterProps<TData> {
|
||||||
table: Table<TData>
|
table: Table<TData>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TableFooter<TData>({ table }: TableFooterProps<TData>) {
|
function TableFooter<TData>({ table }: TableFooterProps<TData>) {
|
||||||
return (
|
return (
|
||||||
<tfoot>
|
<tfoot>
|
||||||
{table.getFooterGroups().map(
|
{table.getFooterGroups().map((footerGroup: HeaderGroup<TData>) => (
|
||||||
(footerGroup: HeaderGroup<TData>) => (
|
<tr key={footerGroup.id}>
|
||||||
<tr key={footerGroup.id}>
|
{footerGroup.headers.map((header: Header<TData, unknown>) => (
|
||||||
{footerGroup.headers.map(
|
<th key={header.id}>
|
||||||
(header: Header<TData, unknown>) => (
|
{!header.isPlaceholder ? flexRender(header.column.columnDef.footer, header.getContext()) : null}
|
||||||
<th key={header.id}>
|
</th>
|
||||||
{!header.isPlaceholder ? flexRender(header.column.columnDef.footer, header.getContext()) : null}
|
))}
|
||||||
</th>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tfoot>
|
||||||
))}
|
);
|
||||||
</tfoot>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TableFooter;
|
export default TableFooter;
|
|
@ -4,53 +4,52 @@ import SelectAll from './SelectAll';
|
||||||
import SortingIcon from './SortingIcon';
|
import SortingIcon from './SortingIcon';
|
||||||
|
|
||||||
interface TableHeaderProps<TData> {
|
interface TableHeaderProps<TData> {
|
||||||
table: Table<TData>
|
table: Table<TData>;
|
||||||
headPosition?: string
|
headPosition?: string;
|
||||||
enableRowSelection?: boolean
|
enableRowSelection?: boolean;
|
||||||
enableSorting?: boolean
|
enableSorting?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TableHeader<TData>({
|
function TableHeader<TData>({ table, headPosition, enableRowSelection, enableSorting }: TableHeaderProps<TData>) {
|
||||||
table, headPosition,
|
|
||||||
enableRowSelection, enableSorting
|
|
||||||
}: TableHeaderProps<TData>) {
|
|
||||||
return (
|
return (
|
||||||
<thead
|
<thead
|
||||||
className={`clr-app shadow-border`}
|
className={`clr-app shadow-border`}
|
||||||
style={{
|
style={{
|
||||||
top: headPosition,
|
top: headPosition,
|
||||||
position: 'sticky'
|
position: 'sticky'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{table.getHeaderGroups().map(
|
{table.getHeaderGroups().map((headerGroup: HeaderGroup<TData>) => (
|
||||||
(headerGroup: HeaderGroup<TData>) => (
|
<tr key={headerGroup.id}>
|
||||||
<tr key={headerGroup.id}>
|
{enableRowSelection ? (
|
||||||
{enableRowSelection ?
|
<th className='pl-3 pr-1'>
|
||||||
<th className='pl-3 pr-1'>
|
<SelectAll table={table} />
|
||||||
<SelectAll table={table} />
|
</th>
|
||||||
</th> : null}
|
) : null}
|
||||||
{headerGroup.headers.map(
|
{headerGroup.headers.map((header: Header<TData, unknown>) => (
|
||||||
(header: Header<TData, unknown>) => (
|
<th
|
||||||
<th key={header.id}
|
key={header.id}
|
||||||
colSpan={header.colSpan}
|
colSpan={header.colSpan}
|
||||||
className='px-2 py-2 text-xs font-semibold select-none whitespace-nowrap'
|
className='px-2 py-2 text-xs font-semibold select-none whitespace-nowrap'
|
||||||
style={{
|
style={{
|
||||||
textAlign: header.getSize() > 100 ? 'left': 'center',
|
textAlign: header.getSize() > 100 ? 'left' : 'center',
|
||||||
width: header.getSize(),
|
width: header.getSize(),
|
||||||
cursor: enableSorting && header.column.getCanSort() ? 'pointer': 'auto',
|
cursor: enableSorting && header.column.getCanSort() ? 'pointer' : 'auto'
|
||||||
}}
|
}}
|
||||||
onClick={enableSorting ? header.column.getToggleSortingHandler() : undefined}
|
onClick={enableSorting ? header.column.getToggleSortingHandler() : undefined}
|
||||||
>
|
>
|
||||||
{!header.isPlaceholder ? (
|
{!header.isPlaceholder ? (
|
||||||
<div className='flex gap-1'>
|
<div className='flex gap-1'>
|
||||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
{(enableSorting && header.column.getCanSort()) ? <SortingIcon column={header.column} /> : null}
|
{enableSorting && header.column.getCanSort() ? <SortingIcon column={header.column} /> : null}
|
||||||
</div>) : null}
|
</div>
|
||||||
</th>
|
) : null}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</thead>
|
||||||
))}
|
);
|
||||||
</thead>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TableHeader;
|
export default TableHeader;
|
|
@ -1,5 +1,7 @@
|
||||||
export {
|
export {
|
||||||
default,
|
default,
|
||||||
createColumnHelper, type IConditionalStyle,
|
createColumnHelper,
|
||||||
type RowSelectionState, type VisibilityState
|
type IConditionalStyle,
|
||||||
|
type RowSelectionState,
|
||||||
|
type VisibilityState
|
||||||
} from './DataTable';
|
} from './DataTable';
|
|
@ -5,14 +5,12 @@ import InfoError from './InfoError';
|
||||||
|
|
||||||
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
|
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col items-center antialiased clr-app' role='alert'>
|
<div className='flex flex-col items-center antialiased clr-app' role='alert'>
|
||||||
<h1>Что-то пошло не так!</h1>
|
<h1>Что-то пошло не так!</h1>
|
||||||
<Button
|
<Button onClick={resetErrorBoundary} text='Попробовать еще раз' />
|
||||||
onClick={resetErrorBoundary}
|
<InfoError error={error as Error} />
|
||||||
text='Попробовать еще раз'
|
</div>
|
||||||
/>
|
);
|
||||||
<InfoError error={error as Error} />
|
|
||||||
</div>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ErrorFallback;
|
export default ErrorFallback;
|
|
@ -12,23 +12,21 @@ function ExpectedAnonymous() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col items-center gap-3 py-6'>
|
<div className='flex flex-col items-center gap-3 py-6'>
|
||||||
<p className='font-semibold'>{`Вы вошли в систему как ${user?.username ?? ''}`}</p>
|
<p className='font-semibold'>{`Вы вошли в систему как ${user?.username ?? ''}`}</p>
|
||||||
<div className='flex gap-3'>
|
<div className='flex gap-3'>
|
||||||
<TextURL text='Новая схема' href='/library/create'/>
|
<TextURL text='Новая схема' href='/library/create' />
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<TextURL text='Библиотека' href='/library'/>
|
<TextURL text='Библиотека' href='/library' />
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<TextURL text='Справка' href='/manuals'/>
|
<TextURL text='Справка' href='/manuals' />
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<span
|
<span className='cursor-pointer hover:underline clr-text-url' onClick={logoutAndRedirect}>
|
||||||
className='cursor-pointer hover:underline clr-text-url'
|
Выйти
|
||||||
onClick={logoutAndRedirect}
|
</span>
|
||||||
>
|
</div>
|
||||||
Выйти
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ExpectedAnonymous;
|
export default ExpectedAnonymous;
|
|
@ -11,23 +11,25 @@ function Footer() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<footer tabIndex={-1}
|
<footer
|
||||||
className={clsx(
|
tabIndex={-1}
|
||||||
'z-navigation',
|
className={clsx(
|
||||||
'px-4 py-2 flex flex-col items-center gap-1',
|
'z-navigation',
|
||||||
'text-sm select-none whitespace-nowrap'
|
'px-4 py-2 flex flex-col items-center gap-1',
|
||||||
)}
|
'text-sm select-none whitespace-nowrap'
|
||||||
>
|
)}
|
||||||
<div className='flex gap-3'>
|
>
|
||||||
<TextURL text='Библиотека' href='/library' color='clr-footer'/>
|
<div className='flex gap-3'>
|
||||||
<TextURL text='Справка' href='/manuals' color='clr-footer'/>
|
<TextURL text='Библиотека' href='/library' color='clr-footer' />
|
||||||
<TextURL text='Центр Концепт' href={urls.concept} color='clr-footer'/>
|
<TextURL text='Справка' href='/manuals' color='clr-footer' />
|
||||||
<TextURL text='Экстеор' href='/manuals?topic=exteor' color='clr-footer'/>
|
<TextURL text='Центр Концепт' href={urls.concept} color='clr-footer' />
|
||||||
</div>
|
<TextURL text='Экстеор' href='/manuals?topic=exteor' color='clr-footer' />
|
||||||
<div>
|
</div>
|
||||||
<p className='clr-footer'>© 2024 ЦИВТ КОНЦЕПТ</p>
|
<div>
|
||||||
</div>
|
<p className='clr-footer'>© 2024 ЦИВТ КОНЦЕПТ</p>
|
||||||
</footer>);
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Footer;
|
export default Footer;
|
|
@ -3,18 +3,16 @@ import InfoConstituenta from '@/components/Shared/InfoConstituenta';
|
||||||
import { IConstituenta } from '@/models/rsform';
|
import { IConstituenta } from '@/models/rsform';
|
||||||
|
|
||||||
interface ConstituentaTooltipProps {
|
interface ConstituentaTooltipProps {
|
||||||
data: IConstituenta
|
data: IConstituenta;
|
||||||
anchor: string
|
anchor: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ConstituentaTooltip({ data, anchor }: ConstituentaTooltipProps) {
|
function ConstituentaTooltip({ data, anchor }: ConstituentaTooltipProps) {
|
||||||
return (
|
return (
|
||||||
<ConceptTooltip clickable
|
<ConceptTooltip clickable anchorSelect={anchor} className='max-w-[30rem]'>
|
||||||
anchorSelect={anchor}
|
<InfoConstituenta data={data} />
|
||||||
className='max-w-[30rem]'
|
</ConceptTooltip>
|
||||||
>
|
);
|
||||||
<InfoConstituenta data={data} />
|
|
||||||
</ConceptTooltip>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ConstituentaTooltip;
|
export default ConstituentaTooltip;
|
|
@ -7,32 +7,25 @@ import { HelpTopic } from '@/models/miscellaneous';
|
||||||
import { CProps } from '../props';
|
import { CProps } from '../props';
|
||||||
import InfoTopic from './InfoTopic';
|
import InfoTopic from './InfoTopic';
|
||||||
|
|
||||||
interface HelpButtonProps
|
interface HelpButtonProps extends CProps.Styling {
|
||||||
extends CProps.Styling {
|
topic: HelpTopic;
|
||||||
topic: HelpTopic
|
offset?: number;
|
||||||
offset?: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function HelpButton({ topic, ...restProps }: HelpButtonProps) {
|
function HelpButton({ topic, ...restProps }: HelpButtonProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div id={`help-${topic}`} className='p-1'>
|
||||||
id={`help-${topic}`}
|
<BiInfoCircle size='1.25rem' className='clr-text-primary' />
|
||||||
className='p-1'
|
<ConceptTooltip clickable anchorSelect={`#help-${topic}`} layer='z-modal-tooltip' {...restProps}>
|
||||||
>
|
<div className='relative'>
|
||||||
<BiInfoCircle size='1.25rem' className='clr-text-primary' />
|
<div className='absolute right-0 text-sm top-[0.4rem]'>
|
||||||
<ConceptTooltip clickable
|
<TextURL text='Справка...' href={`/manuals?topic=${topic}`} />
|
||||||
anchorSelect={`#help-${topic}`}
|
</div>
|
||||||
layer='z-modal-tooltip'
|
</div>
|
||||||
{...restProps}
|
<InfoTopic topic={topic} />
|
||||||
>
|
</ConceptTooltip>
|
||||||
<div className='relative'>
|
</div>
|
||||||
<div className='absolute right-0 text-sm top-[0.4rem]'>
|
);
|
||||||
<TextURL text='Справка...' href={`/manuals?topic=${topic}`} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<InfoTopic topic={topic} />
|
|
||||||
</ConceptTooltip>
|
|
||||||
</div>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default HelpButton;
|
export default HelpButton;
|
|
@ -2,6 +2,7 @@ import Divider from '@/components/Common/Divider';
|
||||||
import InfoCstStatus from '@/components/Shared/InfoCstStatus';
|
import InfoCstStatus from '@/components/Shared/InfoCstStatus';
|
||||||
|
|
||||||
function HelpConstituenta() {
|
function HelpConstituenta() {
|
||||||
|
// prettier-ignore
|
||||||
return (
|
return (
|
||||||
<div className='leading-tight'>
|
<div className='leading-tight'>
|
||||||
<h1>Редактор конституент</h1>
|
<h1>Редактор конституент</h1>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import TextURL from '@/components/Common/TextURL';
|
||||||
import { urls } from '@/utils/constants';
|
import { urls } from '@/utils/constants';
|
||||||
|
|
||||||
function HelpExteor() {
|
function HelpExteor() {
|
||||||
|
// prettier-ignore
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>Экстеор</h1>
|
<h1>Экстеор</h1>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { BiCheckShield, BiShareAlt } from 'react-icons/bi';
|
||||||
import { FiBell } from 'react-icons/fi';
|
import { FiBell } from 'react-icons/fi';
|
||||||
|
|
||||||
function HelpLibrary() {
|
function HelpLibrary() {
|
||||||
|
// prettier-ignore
|
||||||
return (
|
return (
|
||||||
<div className='max-w-[80rem]'>
|
<div className='max-w-[80rem]'>
|
||||||
<h1>Библиотека концептуальных схем</h1>
|
<h1>Библиотека концептуальных схем</h1>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import TextURL from '@/components/Common/TextURL';
|
||||||
import { urls } from '@/utils/constants';
|
import { urls } from '@/utils/constants';
|
||||||
|
|
||||||
function HelpMain() {
|
function HelpMain() {
|
||||||
|
// prettier-ignore
|
||||||
return (
|
return (
|
||||||
<div className='max-w-[60rem]'>
|
<div className='max-w-[60rem]'>
|
||||||
<h1>Портал</h1>
|
<h1>Портал</h1>
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
import PDFViewer from '@/components/Common/PDFViewer';
|
import PDFViewer from '@/components/PDFViewer';
|
||||||
import { resources } from '@/utils/constants';
|
import { resources } from '@/utils/constants';
|
||||||
|
|
||||||
function HelpPrivacy() {
|
function HelpPrivacy() {
|
||||||
return (
|
return <PDFViewer file={resources.privacy_policy} />;
|
||||||
<PDFViewer
|
|
||||||
file={resources.privacy_policy}
|
|
||||||
/>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default HelpPrivacy;
|
export default HelpPrivacy;
|
|
@ -2,6 +2,7 @@ import Divider from '@/components/Common/Divider';
|
||||||
import InfoCstStatus from '@/components/Shared/InfoCstStatus';
|
import InfoCstStatus from '@/components/Shared/InfoCstStatus';
|
||||||
|
|
||||||
function HelpRSFormItems() {
|
function HelpRSFormItems() {
|
||||||
|
// prettier-ignore
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>Горячие клавиши</h1>
|
<h1>Горячие клавиши</h1>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
function HelpRSFormMeta() {
|
function HelpRSFormMeta() {
|
||||||
|
// prettier-ignore
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>Паспорт схемы</h1>
|
<h1>Паспорт схемы</h1>
|
||||||
|
|
|
@ -9,13 +9,13 @@ const OPT_VIDEO_H = 1080;
|
||||||
function HelpRSLang() {
|
function HelpRSLang() {
|
||||||
const windowSize = useWindowSize();
|
const windowSize = useWindowSize();
|
||||||
|
|
||||||
const videoHeight = useMemo(
|
const videoHeight = useMemo(() => {
|
||||||
() => {
|
|
||||||
const viewH = windowSize.height ?? 0;
|
const viewH = windowSize.height ?? 0;
|
||||||
const viewW = windowSize.width ?? 0;
|
const viewW = windowSize.width ?? 0;
|
||||||
return Math.min(OPT_VIDEO_H, viewH - 320, Math.floor((viewW - 290)*9/16));
|
return Math.min(OPT_VIDEO_H, viewH - 320, Math.floor(((viewW - 290) * 9) / 16));
|
||||||
}, [windowSize]);
|
}, [windowSize]);
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-4'>
|
<div className='flex flex-col gap-4'>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
function HelpRSTemplates() {
|
function HelpRSTemplates() {
|
||||||
|
// prettier-ignore
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-2 pb-2 max-w-[80rem]'>
|
<div className='flex flex-col gap-2 pb-2 max-w-[80rem]'>
|
||||||
<h1>Банк выражений</h1>
|
<h1>Банк выражений</h1>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import InfoCstClass from '@/components/Shared/InfoCstClass';
|
||||||
import InfoCstStatus from '@/components/Shared/InfoCstStatus';
|
import InfoCstStatus from '@/components/Shared/InfoCstStatus';
|
||||||
|
|
||||||
function HelpTermGraph() {
|
function HelpTermGraph() {
|
||||||
|
// prettier-ignore
|
||||||
return (
|
return (
|
||||||
<div className='flex max-w-[80rem]'>
|
<div className='flex max-w-[80rem]'>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
function HelpTerminologyControl() {
|
function HelpTerminologyControl() {
|
||||||
|
// prettier-ignore
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-1 max-w-[80rem]'>
|
<div className='flex flex-col gap-1 max-w-[80rem]'>
|
||||||
<h1>Терминологизация</h1>
|
<h1>Терминологизация</h1>
|
||||||
|
|
|
@ -14,7 +14,7 @@ import HelpTermGraph from './HelpTermGraph';
|
||||||
import HelpTerminologyControl from './HelpTerminologyControl';
|
import HelpTerminologyControl from './HelpTerminologyControl';
|
||||||
|
|
||||||
interface InfoTopicProps {
|
interface InfoTopicProps {
|
||||||
topic: HelpTopic
|
topic: HelpTopic;
|
||||||
}
|
}
|
||||||
|
|
||||||
function InfoTopic({ topic }: InfoTopicProps) {
|
function InfoTopic({ topic }: InfoTopicProps) {
|
||||||
|
|
|
@ -1,30 +1,31 @@
|
||||||
// Search new icons at https://reactsvgicons.com/
|
// Search new icons at https://reactsvgicons.com/
|
||||||
|
|
||||||
interface IconSVGProps {
|
interface IconSVGProps {
|
||||||
viewBox: string
|
viewBox: string;
|
||||||
size?: string
|
size?: string;
|
||||||
className?: string
|
className?: string;
|
||||||
props?: React.SVGProps<SVGSVGElement>
|
props?: React.SVGProps<SVGSVGElement>;
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IconProps {
|
export interface IconProps {
|
||||||
size?: string
|
size?: string;
|
||||||
className?: string
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function IconSVG({ viewBox, size = '1.5rem', className, props, children }: IconSVGProps) {
|
function IconSVG({ viewBox, size = '1.5rem', className, props, children }: IconSVGProps) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
className={`w-[${size}] h-[${size}] ${className}`}
|
className={`w-[${size}] h-[${size}] ${className}`}
|
||||||
fill='currentColor'
|
fill='currentColor'
|
||||||
viewBox={viewBox}
|
viewBox={viewBox}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</svg>);
|
</svg>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EducationIcon(props: IconProps) {
|
export function EducationIcon(props: IconProps) {
|
||||||
|
@ -46,11 +47,7 @@ export function InDoorIcon(props: IconProps) {
|
||||||
|
|
||||||
export function CheckboxCheckedIcon() {
|
export function CheckboxCheckedIcon() {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg className='w-3 h-3' viewBox='0 0 512 512' fill='#ffffff'>
|
||||||
className='w-3 h-3'
|
|
||||||
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>
|
||||||
);
|
);
|
||||||
|
@ -58,11 +55,7 @@ export function CheckboxCheckedIcon() {
|
||||||
|
|
||||||
export function CheckboxNullIcon() {
|
export function CheckboxNullIcon() {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg className='w-3 h-3' viewBox='0 0 16 16' fill='#ffffff'>
|
||||||
className='w-3 h-3'
|
|
||||||
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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -7,10 +7,10 @@ import PrettyJson from './Common/PrettyJSON';
|
||||||
export type ErrorData = string | Error | AxiosError | undefined;
|
export type ErrorData = string | Error | AxiosError | undefined;
|
||||||
|
|
||||||
interface InfoErrorProps {
|
interface InfoErrorProps {
|
||||||
error: ErrorData
|
error: ErrorData;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DescribeError({error} : {error: ErrorData}) {
|
function DescribeError({ error }: { error: ErrorData }) {
|
||||||
if (!error) {
|
if (!error) {
|
||||||
return <p>Ошибки отсутствуют</p>;
|
return <p>Ошибки отсутствуют</p>;
|
||||||
} else if (typeof error === 'string') {
|
} else if (typeof error === 'string') {
|
||||||
|
@ -23,10 +23,11 @@ function DescribeError({error} : {error: ErrorData}) {
|
||||||
}
|
}
|
||||||
if (error.response.status === 404) {
|
if (error.response.status === 404) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>{'Обращение к несуществующему API'}</p>
|
<p>{'Обращение к несуществующему API'}</p>
|
||||||
<PrettyJson data={error} />
|
<PrettyJson data={error} />
|
||||||
</div>);
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
|
||||||
|
@ -35,20 +36,23 @@ function DescribeError({error} : {error: ErrorData}) {
|
||||||
<div>
|
<div>
|
||||||
<p className='underline'>Ошибка</p>
|
<p className='underline'>Ошибка</p>
|
||||||
<p>{error.message}</p>
|
<p>{error.message}</p>
|
||||||
{error.response.data && (<>
|
{error.response.data && (
|
||||||
<p className='mt-2 underline'>Описание</p>
|
<>
|
||||||
{isHtml ? <div dangerouslySetInnerHTML={{ __html: error.response.data as TrustedHTML }} /> : null}
|
<p className='mt-2 underline'>Описание</p>
|
||||||
{!isHtml ? <PrettyJson data={error.response.data as object} /> : null}
|
{isHtml ? <div dangerouslySetInnerHTML={{ __html: error.response.data as TrustedHTML }} /> : null}
|
||||||
</>)}
|
{!isHtml ? <PrettyJson data={error.response.data as object} /> : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function InfoError({ error }: InfoErrorProps) {
|
function InfoError({ error }: InfoErrorProps) {
|
||||||
return (
|
return (
|
||||||
<div className='px-3 py-2 min-w-[15rem] text-sm font-semibold select-text clr-text-warning'>
|
<div className='px-3 py-2 min-w-[15rem] text-sm font-semibold select-text clr-text-warning'>
|
||||||
<DescribeError error={error} />
|
<DescribeError error={error} />
|
||||||
</div>);
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default InfoError;
|
export default InfoError;
|
|
@ -3,9 +3,11 @@ import { useConceptTheme } from '@/context/ThemeContext';
|
||||||
function Logo() {
|
function Logo() {
|
||||||
const { darkMode } = useConceptTheme();
|
const { darkMode } = useConceptTheme();
|
||||||
return (
|
return (
|
||||||
<img alt='Логотип КонцептПортал'
|
<img
|
||||||
className='max-h-[1.6rem] min-w-[11.5rem]'
|
alt='Логотип КонцептПортал'
|
||||||
src={!darkMode ? '/logo_full.svg' : '/logo_full_dark.svg'}
|
className='max-h-[1.6rem] min-w-[11.5rem]'
|
||||||
/>);
|
src={!darkMode ? '/logo_full.svg' : '/logo_full_dark.svg'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
export default Logo;
|
export default Logo;
|
|
@ -13,7 +13,7 @@ import NavigationButton from './NavigationButton';
|
||||||
import ToggleNavigationButton from './ToggleNavigationButton';
|
import ToggleNavigationButton from './ToggleNavigationButton';
|
||||||
import UserMenu from './UserMenu';
|
import UserMenu from './UserMenu';
|
||||||
|
|
||||||
function Navigation () {
|
function Navigation() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const { noNavigationAnimation } = useConceptTheme();
|
const { noNavigationAnimation } = useConceptTheme();
|
||||||
|
|
||||||
|
@ -23,52 +23,41 @@ function Navigation () {
|
||||||
const navigateCreateNew = () => router.push('/library/create');
|
const navigateCreateNew = () => router.push('/library/create');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className={clsx(
|
<nav className={clsx('z-navigation', 'sticky top-0 left-0 right-0', 'clr-app', 'select-none')}>
|
||||||
'z-navigation',
|
<ToggleNavigationButton />
|
||||||
'sticky top-0 left-0 right-0',
|
<motion.div
|
||||||
'clr-app',
|
className={clsx('pl-2 pr-[0.9rem] h-[3rem]', 'flex justify-between', 'shadow-border')}
|
||||||
'select-none'
|
initial={false}
|
||||||
)}>
|
animate={!noNavigationAnimation ? 'open' : 'closed'}
|
||||||
<ToggleNavigationButton />
|
variants={animateNavigation}
|
||||||
<motion.div
|
|
||||||
className={clsx(
|
|
||||||
'pl-2 pr-[0.9rem] h-[3rem]',
|
|
||||||
'flex justify-between',
|
|
||||||
'shadow-border'
|
|
||||||
)}
|
|
||||||
initial={false}
|
|
||||||
animate={!noNavigationAnimation ? 'open' : 'closed'}
|
|
||||||
variants={animateNavigation}
|
|
||||||
>
|
|
||||||
<div tabIndex={-1}
|
|
||||||
className='flex items-center mr-2 cursor-pointer'
|
|
||||||
onClick={navigateHome}
|
|
||||||
>
|
>
|
||||||
<Logo />
|
<div tabIndex={-1} className='flex items-center mr-2 cursor-pointer' onClick={navigateHome}>
|
||||||
</div>
|
<Logo />
|
||||||
<div className='flex'>
|
</div>
|
||||||
<NavigationButton
|
<div className='flex'>
|
||||||
text='Новая схема'
|
<NavigationButton
|
||||||
title='Создать новую схему'
|
text='Новая схема'
|
||||||
icon={<FaSquarePlus size='1.5rem' />}
|
title='Создать новую схему'
|
||||||
onClick={navigateCreateNew}
|
icon={<FaSquarePlus size='1.5rem' />}
|
||||||
/>
|
onClick={navigateCreateNew}
|
||||||
<NavigationButton
|
/>
|
||||||
text='Библиотека'
|
<NavigationButton
|
||||||
title='Список схем'
|
text='Библиотека'
|
||||||
icon={<IoLibrary size='1.5rem' />}
|
title='Список схем'
|
||||||
onClick={navigateLibrary}
|
icon={<IoLibrary size='1.5rem' />}
|
||||||
/>
|
onClick={navigateLibrary}
|
||||||
<NavigationButton
|
/>
|
||||||
text='Справка'
|
<NavigationButton
|
||||||
title='Справочные материалы'
|
text='Справка'
|
||||||
icon={<EducationIcon />}
|
title='Справочные материалы'
|
||||||
onClick={navigateHelp}
|
icon={<EducationIcon />}
|
||||||
/>
|
onClick={navigateHelp}
|
||||||
<UserMenu />
|
/>
|
||||||
</div>
|
<UserMenu />
|
||||||
</motion.div>
|
</div>
|
||||||
</nav>);
|
</motion.div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Navigation;
|
export default Navigation;
|
|
@ -3,32 +3,35 @@ import clsx from 'clsx';
|
||||||
import { globalIDs } from '@/utils/constants';
|
import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
interface NavigationButtonProps {
|
interface NavigationButtonProps {
|
||||||
text?: string
|
text?: string;
|
||||||
icon: React.ReactNode
|
icon: React.ReactNode;
|
||||||
title?: string
|
title?: string;
|
||||||
onClick?: () => void
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function NavigationButton({ icon, title, onClick, text }: NavigationButtonProps) {
|
function NavigationButton({ icon, title, onClick, text }: NavigationButtonProps) {
|
||||||
return (
|
return (
|
||||||
<button type='button' tabIndex={-1}
|
<button
|
||||||
data-tooltip-id={title ? (globalIDs.tooltip) : undefined}
|
type='button'
|
||||||
data-tooltip-content={title}
|
tabIndex={-1}
|
||||||
onClick={onClick}
|
data-tooltip-id={title ? globalIDs.tooltip : undefined}
|
||||||
className={clsx(
|
data-tooltip-content={title}
|
||||||
'mr-1 h-full',
|
onClick={onClick}
|
||||||
'flex items-center gap-1',
|
className={clsx(
|
||||||
'clr-btn-nav',
|
'mr-1 h-full', //
|
||||||
'small-caps whitespace-nowrap',
|
'flex items-center gap-1',
|
||||||
{
|
'clr-btn-nav',
|
||||||
'px-2': text,
|
'small-caps whitespace-nowrap',
|
||||||
'px-4': !text
|
{
|
||||||
}
|
'px-2': text,
|
||||||
)}
|
'px-4': !text
|
||||||
>
|
}
|
||||||
{icon ? <span>{icon}</span> : null}
|
)}
|
||||||
{text ? <span className='font-semibold'>{text}</span> : null}
|
>
|
||||||
</button>);
|
{icon ? <span>{icon}</span> : null}
|
||||||
|
{text ? <span className='font-semibold'>{text}</span> : null}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NavigationButton;
|
export default NavigationButton;
|
|
@ -8,21 +8,24 @@ import { animateNavigationToggle } from '@/utils/animations';
|
||||||
function ToggleNavigationButton() {
|
function ToggleNavigationButton() {
|
||||||
const { noNavigationAnimation, toggleNoNavigation } = useConceptTheme();
|
const { noNavigationAnimation, toggleNoNavigation } = useConceptTheme();
|
||||||
return (
|
return (
|
||||||
<motion.button type='button' tabIndex={-1}
|
<motion.button
|
||||||
title={noNavigationAnimation ? 'Показать навигацию' : 'Скрыть навигацию'}
|
type='button'
|
||||||
className={clsx(
|
tabIndex={-1}
|
||||||
'absolute top-0 right-0 z-navigation flex items-center justify-center',
|
title={noNavigationAnimation ? 'Показать навигацию' : 'Скрыть навигацию'}
|
||||||
'clr-btn-nav',
|
className={clsx(
|
||||||
'select-none disabled:cursor-not-allowed'
|
'absolute top-0 right-0 z-navigation flex items-center justify-center',
|
||||||
)}
|
'clr-btn-nav',
|
||||||
onClick={toggleNoNavigation}
|
'select-none disabled:cursor-not-allowed'
|
||||||
initial={false}
|
)}
|
||||||
animate={noNavigationAnimation ? 'off' : 'on'}
|
onClick={toggleNoNavigation}
|
||||||
variants={animateNavigationToggle}
|
initial={false}
|
||||||
>
|
animate={noNavigationAnimation ? 'off' : 'on'}
|
||||||
{!noNavigationAnimation ? <RiPushpinFill /> : null}
|
variants={animateNavigationToggle}
|
||||||
{noNavigationAnimation ? <RiUnpinLine /> : null}
|
>
|
||||||
</motion.button>);
|
{!noNavigationAnimation ? <RiPushpinFill /> : null}
|
||||||
|
{noNavigationAnimation ? <RiUnpinLine /> : null}
|
||||||
|
</motion.button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ToggleNavigationButton;
|
export default ToggleNavigationButton;
|
|
@ -5,8 +5,8 @@ import { useConceptNavigation } from '@/context/NavigationContext';
|
||||||
import { useConceptTheme } from '@/context/ThemeContext';
|
import { useConceptTheme } from '@/context/ThemeContext';
|
||||||
|
|
||||||
interface UserDropdownProps {
|
interface UserDropdownProps {
|
||||||
isOpen: boolean
|
isOpen: boolean;
|
||||||
hideDropdown: () => void
|
hideDropdown: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
|
function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
|
||||||
|
@ -19,30 +19,22 @@ function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
|
||||||
router.push('/profile');
|
router.push('/profile');
|
||||||
};
|
};
|
||||||
|
|
||||||
const logoutAndRedirect =
|
const logoutAndRedirect = () => {
|
||||||
() => {
|
|
||||||
hideDropdown();
|
hideDropdown();
|
||||||
logout(() => router.push('/login/'));
|
logout(() => router.push('/login/'));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown className='w-36' stretchLeft isOpen={isOpen}>
|
<Dropdown className='w-36' stretchLeft isOpen={isOpen}>
|
||||||
<DropdownButton
|
<DropdownButton text={user?.username} title='Профиль пользователя' onClick={navigateProfile} />
|
||||||
text={user?.username}
|
<DropdownButton
|
||||||
title='Профиль пользователя'
|
text={darkMode ? 'Светлая тема' : 'Темная тема'}
|
||||||
onClick={navigateProfile}
|
title='Переключение темы оформления'
|
||||||
/>
|
onClick={toggleDarkMode}
|
||||||
<DropdownButton
|
/>
|
||||||
text={darkMode ? 'Светлая тема' : 'Темная тема'}
|
<DropdownButton text='Выйти...' className='font-semibold' onClick={logoutAndRedirect} />
|
||||||
title='Переключение темы оформления'
|
</Dropdown>
|
||||||
onClick={toggleDarkMode}
|
);
|
||||||
/>
|
|
||||||
<DropdownButton
|
|
||||||
text='Выйти...'
|
|
||||||
className='font-semibold'
|
|
||||||
onClick={logoutAndRedirect}
|
|
||||||
/>
|
|
||||||
</Dropdown>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default UserDropdown;
|
export default UserDropdown;
|
|
@ -15,23 +15,18 @@ function UserMenu() {
|
||||||
|
|
||||||
const navigateLogin = () => router.push('/login');
|
const navigateLogin = () => router.push('/login');
|
||||||
return (
|
return (
|
||||||
<div ref={menu.ref} className='h-full'>
|
<div ref={menu.ref} className='h-full'>
|
||||||
{!user ?
|
{!user ? (
|
||||||
<NavigationButton
|
<NavigationButton
|
||||||
title='Перейти на страницу логина'
|
title='Перейти на страницу логина'
|
||||||
icon={<InDoorIcon size='1.5rem' className='clr-text-primary' />}
|
icon={<InDoorIcon size='1.5rem' className='clr-text-primary' />}
|
||||||
onClick={navigateLogin}
|
onClick={navigateLogin}
|
||||||
/> : null}
|
/>
|
||||||
{user ?
|
) : null}
|
||||||
<NavigationButton
|
{user ? <NavigationButton icon={<FaCircleUser size='1.5rem' />} onClick={menu.toggle} /> : null}
|
||||||
icon={<FaCircleUser size='1.5rem' />}
|
<UserDropdown isOpen={!!user && menu.isOpen} hideDropdown={() => menu.hide()} />
|
||||||
onClick={menu.toggle}
|
</div>
|
||||||
/> : null}
|
);
|
||||||
<UserDropdown
|
|
||||||
isOpen={!!user && menu.isOpen}
|
|
||||||
hideDropdown={() => menu.hide()}
|
|
||||||
/>
|
|
||||||
</div>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default UserMenu;
|
export default UserMenu;
|
60
rsconcept/frontend/src/components/PDFViewer/PDFViewer.tsx
Normal file
60
rsconcept/frontend/src/components/PDFViewer/PDFViewer.tsx
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import type { PDFDocumentProxy } from 'pdfjs-dist';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { Document, Page } from 'react-pdf';
|
||||||
|
|
||||||
|
import useWindowSize from '@/hooks/useWindowSize';
|
||||||
|
import { graphLightT } from '@/utils/color';
|
||||||
|
|
||||||
|
import Overlay from '../Common/Overlay';
|
||||||
|
import PageControls from './PageControls';
|
||||||
|
|
||||||
|
const MAXIMUM_WIDTH = 1000;
|
||||||
|
const MINIMUM_WIDTH = 600;
|
||||||
|
|
||||||
|
interface PDFViewerProps {
|
||||||
|
file?: string | ArrayBuffer | Blob;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PDFViewer({ file }: PDFViewerProps) {
|
||||||
|
const windowSize = useWindowSize();
|
||||||
|
|
||||||
|
const [pageCount, setPageCount] = useState(0);
|
||||||
|
const [pageNumber, setPageNumber] = useState(1);
|
||||||
|
|
||||||
|
const pageWidth = useMemo(() => {
|
||||||
|
return Math.max(MINIMUM_WIDTH, Math.min((windowSize?.width ?? 0) - 300, MAXIMUM_WIDTH));
|
||||||
|
}, [windowSize]);
|
||||||
|
|
||||||
|
function onDocumentLoadSuccess({ numPages }: PDFDocumentProxy) {
|
||||||
|
setPageCount(numPages);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Document
|
||||||
|
file={file}
|
||||||
|
onLoadSuccess={onDocumentLoadSuccess}
|
||||||
|
className='px-3'
|
||||||
|
loading='Загрузка PDF файла...'
|
||||||
|
error='Не удалось загрузить файл.'
|
||||||
|
>
|
||||||
|
<Overlay position='top-6 left-1/2 -translate-x-1/2' className='flex select-none'>
|
||||||
|
<PageControls pageCount={pageCount} pageNumber={pageNumber} setPageNumber={setPageNumber} />
|
||||||
|
</Overlay>
|
||||||
|
<Page
|
||||||
|
className='pointer-events-none select-none'
|
||||||
|
renderTextLayer={false}
|
||||||
|
renderAnnotationLayer={false}
|
||||||
|
pageNumber={pageNumber}
|
||||||
|
width={pageWidth}
|
||||||
|
canvasBackground={graphLightT.canvas.background}
|
||||||
|
/>
|
||||||
|
<Overlay position='bottom-6 left-1/2 -translate-x-1/2' className='flex select-none'>
|
||||||
|
<PageControls pageCount={pageCount} pageNumber={pageNumber} setPageNumber={setPageNumber} />
|
||||||
|
</Overlay>
|
||||||
|
</Document>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PDFViewer;
|
51
rsconcept/frontend/src/components/PDFViewer/PageControls.tsx
Normal file
51
rsconcept/frontend/src/components/PDFViewer/PageControls.tsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import { BiChevronLeft, BiChevronRight, BiFirstPage, BiLastPage } from 'react-icons/bi';
|
||||||
|
|
||||||
|
interface PageControlsProps {
|
||||||
|
pageNumber: number;
|
||||||
|
pageCount: number;
|
||||||
|
setPageNumber: React.Dispatch<React.SetStateAction<number>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PageControls({ pageNumber, pageCount, setPageNumber }: PageControlsProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='clr-hover clr-text-controls'
|
||||||
|
onClick={() => setPageNumber(1)}
|
||||||
|
disabled={pageNumber < 2}
|
||||||
|
>
|
||||||
|
<BiFirstPage size='1.5rem' />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='clr-hover clr-text-controls'
|
||||||
|
onClick={() => setPageNumber(prev => prev - 1)}
|
||||||
|
disabled={pageNumber < 2}
|
||||||
|
>
|
||||||
|
<BiChevronLeft size='1.5rem' />
|
||||||
|
</button>
|
||||||
|
<p className='px-3 text-black'>
|
||||||
|
Страница {pageNumber} из {pageCount}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='clr-hover clr-text-controls'
|
||||||
|
onClick={() => setPageNumber(prev => prev + 1)}
|
||||||
|
disabled={pageNumber >= pageCount}
|
||||||
|
>
|
||||||
|
<BiChevronRight size='1.5rem' />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='clr-hover clr-text-controls'
|
||||||
|
onClick={() => setPageNumber(pageCount)}
|
||||||
|
disabled={pageNumber >= pageCount}
|
||||||
|
>
|
||||||
|
<BiLastPage size='1.5rem' />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PageControls;
|
1
rsconcept/frontend/src/components/PDFViewer/index.tsx
Normal file
1
rsconcept/frontend/src/components/PDFViewer/index.tsx
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './PDFViewer';
|
|
@ -14,7 +14,7 @@ import { useConceptTheme } from '@/context/ThemeContext';
|
||||||
|
|
||||||
import { ccBracketMatching } from './bracketMatching';
|
import { ccBracketMatching } from './bracketMatching';
|
||||||
import { RSLanguage } from './rslang';
|
import { RSLanguage } from './rslang';
|
||||||
import { getSymbolSubstitute,RSTextWrapper } from './textEditing';
|
import { getSymbolSubstitute, RSTextWrapper } from './textEditing';
|
||||||
import { rsHoverTooltip } from './tooltip';
|
import { rsHoverTooltip } from './tooltip';
|
||||||
|
|
||||||
const editorSetup: BasicSetupOptions = {
|
const editorSetup: BasicSetupOptions = {
|
||||||
|
@ -46,107 +46,105 @@ const editorSetup: BasicSetupOptions = {
|
||||||
};
|
};
|
||||||
|
|
||||||
interface RSInputProps
|
interface RSInputProps
|
||||||
extends Pick<ReactCodeMirrorProps,
|
extends Pick<
|
||||||
'id' | 'height' | 'minHeight' | 'maxHeight' | 'value' |
|
ReactCodeMirrorProps,
|
||||||
'onFocus' | 'onBlur' | 'placeholder' | 'style' | 'className'
|
'id' | 'height' | 'minHeight' | 'maxHeight' | 'value' | 'onFocus' | 'onBlur' | 'placeholder' | 'style' | 'className'
|
||||||
> {
|
> {
|
||||||
label?: string
|
label?: string;
|
||||||
disabled?: boolean
|
disabled?: boolean;
|
||||||
noTooltip?: boolean
|
noTooltip?: boolean;
|
||||||
onChange?: (newValue: string) => void
|
onChange?: (newValue: string) => void;
|
||||||
onAnalyze?: () => void
|
onAnalyze?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
|
const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
|
||||||
({
|
({ id, label, onChange, onAnalyze, disabled, noTooltip, className, style, ...restProps }, ref) => {
|
||||||
id, label, onChange, onAnalyze,
|
const { darkMode, colors } = useConceptTheme();
|
||||||
disabled, noTooltip,
|
const { schema } = useRSForm();
|
||||||
className, style,
|
|
||||||
...restProps
|
|
||||||
}, ref) => {
|
|
||||||
const { darkMode, colors } = useConceptTheme();
|
|
||||||
const { schema } = useRSForm();
|
|
||||||
|
|
||||||
const internalRef = useRef<ReactCodeMirrorRef>(null);
|
const internalRef = useRef<ReactCodeMirrorRef>(null);
|
||||||
const thisRef = useMemo(() => (!ref || typeof ref === 'function' ? internalRef : ref), [internalRef, ref]);
|
const thisRef = useMemo(() => (!ref || typeof ref === 'function' ? internalRef : ref), [internalRef, ref]);
|
||||||
|
|
||||||
const cursor = useMemo(() => !disabled ? 'cursor-text': 'cursor-default', [disabled]);
|
const cursor = useMemo(() => (!disabled ? 'cursor-text' : 'cursor-default'), [disabled]);
|
||||||
const customTheme: Extension = useMemo(
|
const customTheme: Extension = useMemo(
|
||||||
() => createTheme({
|
() =>
|
||||||
theme: darkMode ? 'dark' : 'light',
|
createTheme({
|
||||||
settings: {
|
theme: darkMode ? 'dark' : 'light',
|
||||||
fontFamily: 'inherit',
|
settings: {
|
||||||
background: !disabled ? colors.bgInput : colors.bgDefault,
|
fontFamily: 'inherit',
|
||||||
foreground: colors.fgDefault,
|
background: !disabled ? colors.bgInput : colors.bgDefault,
|
||||||
selection: colors.bgHover
|
foreground: colors.fgDefault,
|
||||||
},
|
selection: colors.bgHover
|
||||||
styles: [
|
},
|
||||||
{ tag: tags.name, color: colors.fgPurple, cursor: 'default' }, // GlobalID
|
styles: [
|
||||||
{ tag: tags.variableName, color: colors.fgGreen }, // LocalID
|
{ tag: tags.name, color: colors.fgPurple, cursor: 'default' }, // GlobalID
|
||||||
{ tag: tags.propertyName, color: colors.fgTeal }, // Radical
|
{ tag: tags.variableName, color: colors.fgGreen }, // LocalID
|
||||||
{ tag: tags.keyword, color: colors.fgBlue }, // keywords
|
{ tag: tags.propertyName, color: colors.fgTeal }, // Radical
|
||||||
{ tag: tags.literal, color: colors.fgBlue }, // literals
|
{ tag: tags.keyword, color: colors.fgBlue }, // keywords
|
||||||
{ tag: tags.controlKeyword, fontWeight: '500'}, // R | I | D
|
{ tag: tags.literal, color: colors.fgBlue }, // literals
|
||||||
{ tag: tags.unit, fontSize: '0.75rem' }, // indices
|
{ tag: tags.controlKeyword, fontWeight: '500' }, // R | I | D
|
||||||
{ tag: tags.brace, color:colors.fgPurple, fontWeight: '700' }, // braces (curly brackets)
|
{ tag: tags.unit, fontSize: '0.75rem' }, // indices
|
||||||
]
|
{ tag: tags.brace, color: colors.fgPurple, fontWeight: '700' } // braces (curly brackets)
|
||||||
}), [disabled, colors, darkMode]);
|
]
|
||||||
|
}),
|
||||||
|
[disabled, colors, darkMode]
|
||||||
|
);
|
||||||
|
|
||||||
const editorExtensions = useMemo(
|
const editorExtensions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
EditorView.lineWrapping,
|
EditorView.lineWrapping,
|
||||||
RSLanguage,
|
RSLanguage,
|
||||||
ccBracketMatching(darkMode),
|
ccBracketMatching(darkMode),
|
||||||
... noTooltip ? [] : [rsHoverTooltip(schema?.items || [])],
|
...(noTooltip ? [] : [rsHoverTooltip(schema?.items || [])])
|
||||||
], [darkMode, schema?.items, noTooltip]);
|
],
|
||||||
|
[darkMode, schema?.items, noTooltip]
|
||||||
|
);
|
||||||
|
|
||||||
const handleInput = useCallback(
|
const handleInput = useCallback(
|
||||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
if (!thisRef.current) {
|
if (!thisRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const text = new RSTextWrapper(thisRef.current as Required<ReactCodeMirrorRef>);
|
const text = new RSTextWrapper(thisRef.current as Required<ReactCodeMirrorRef>);
|
||||||
if (event.altKey) {
|
if (event.altKey) {
|
||||||
if (text.processAltKey(event.code, event.shiftKey)) {
|
if (text.processAltKey(event.code, event.shiftKey)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
} else if (!event.ctrlKey) {
|
} else if (!event.ctrlKey) {
|
||||||
const newSymbol = getSymbolSubstitute(event.code, event.shiftKey);
|
const newSymbol = getSymbolSubstitute(event.code, event.shiftKey);
|
||||||
if (newSymbol) {
|
if (newSymbol) {
|
||||||
text.replaceWith(newSymbol);
|
text.replaceWith(newSymbol);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
} else if (event.ctrlKey && event.code === 'KeyQ' && onAnalyze) {
|
} else if (event.ctrlKey && event.code === 'KeyQ' && onAnalyze) {
|
||||||
onAnalyze();
|
onAnalyze();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
}, [thisRef, onAnalyze]);
|
},
|
||||||
|
[thisRef, onAnalyze]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={clsx('flex flex-col gap-2', className, cursor)} style={style}>
|
||||||
className={clsx(
|
<Label text={label} htmlFor={id} />
|
||||||
'flex flex-col gap-2',
|
<CodeMirror
|
||||||
className,
|
id={id}
|
||||||
cursor
|
ref={thisRef}
|
||||||
)}
|
basicSetup={editorSetup}
|
||||||
style={style}
|
theme={customTheme}
|
||||||
>
|
extensions={editorExtensions}
|
||||||
<Label text={label} htmlFor={id}/>
|
indentWithTab={false}
|
||||||
<CodeMirror id={id}
|
onChange={onChange}
|
||||||
ref={thisRef}
|
editable={!disabled}
|
||||||
basicSetup={editorSetup}
|
onKeyDown={handleInput}
|
||||||
theme={customTheme}
|
{...restProps}
|
||||||
extensions={editorExtensions}
|
/>
|
||||||
indentWithTab={false}
|
</div>
|
||||||
onChange={onChange}
|
);
|
||||||
editable={!disabled}
|
}
|
||||||
onKeyDown={handleInput}
|
);
|
||||||
{...restProps}
|
|
||||||
/>
|
|
||||||
</div>);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default RSInput;
|
export default RSInput;
|
|
@ -3,8 +3,8 @@ import { Decoration, EditorView } from '@codemirror/view';
|
||||||
|
|
||||||
import { bracketsDarkT, bracketsLightT } from '@/utils/color';
|
import { bracketsDarkT, bracketsLightT } from '@/utils/color';
|
||||||
|
|
||||||
const matchingMark = Decoration.mark({class: 'cc-matchingBracket'});
|
const matchingMark = Decoration.mark({ class: 'cc-matchingBracket' });
|
||||||
const nonMatchingMark = Decoration.mark({class: 'cc-nonmatchingBracket'});
|
const nonMatchingMark = Decoration.mark({ class: 'cc-nonmatchingBracket' });
|
||||||
|
|
||||||
function bracketRender(match: MatchResult) {
|
function bracketRender(match: MatchResult) {
|
||||||
const decorations = [];
|
const decorations = [];
|
||||||
|
@ -24,7 +24,7 @@ export function ccBracketMatching(darkMode: boolean) {
|
||||||
return [
|
return [
|
||||||
bracketMatching({
|
bracketMatching({
|
||||||
renderMatch: bracketRender,
|
renderMatch: bracketRender,
|
||||||
brackets:'{}[]()'
|
brackets: '{}[]()'
|
||||||
}),
|
}),
|
||||||
darkMode ? darkTheme : lightTheme
|
darkMode ? darkTheme : lightTheme
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
import {styleTags, tags} from '@lezer/highlight';
|
import { styleTags, tags } from '@lezer/highlight';
|
||||||
|
|
||||||
export const highlighting = styleTags({
|
export const highlighting = styleTags({
|
||||||
Index: tags.unit,
|
'Index': tags.unit,
|
||||||
ComplexIndex: tags.unit,
|
'ComplexIndex': tags.unit,
|
||||||
Literal: tags.literal,
|
'Literal': tags.literal,
|
||||||
|
|
||||||
Radical: tags.propertyName,
|
'Radical': tags.propertyName,
|
||||||
Function: tags.name,
|
'Function': tags.name,
|
||||||
Predicate: tags.name,
|
'Predicate': tags.name,
|
||||||
Global: tags.name,
|
'Global': tags.name,
|
||||||
Local: tags.variableName,
|
'Local': tags.variableName,
|
||||||
|
|
||||||
TextFunction: tags.keyword,
|
'TextFunction': tags.keyword,
|
||||||
Filter: tags.keyword,
|
'Filter': tags.keyword,
|
||||||
PrefixR: tags.controlKeyword,
|
'PrefixR': tags.controlKeyword,
|
||||||
PrefixI: tags.controlKeyword,
|
'PrefixI': tags.controlKeyword,
|
||||||
PrefixD: tags.controlKeyword,
|
'PrefixD': tags.controlKeyword,
|
||||||
"{": tags.brace,
|
'{': tags.brace,
|
||||||
"}": tags.brace,
|
'}': tags.brace,
|
||||||
"|": tags.brace,
|
'|': tags.brace,
|
||||||
";": tags.brace,
|
';': tags.brace
|
||||||
});
|
});
|
|
@ -1,11 +1,9 @@
|
||||||
import {LRLanguage} from '@codemirror/language';
|
import { LRLanguage } from '@codemirror/language';
|
||||||
|
|
||||||
import { parser } from './parser';
|
import { parser } from './parser';
|
||||||
import { Function, Global, Predicate } from './parser.terms';
|
import { Function, Global, Predicate } from './parser.terms';
|
||||||
|
|
||||||
export const GlobalTokens: number[] = [
|
export const GlobalTokens: number[] = [Global, Function, Predicate];
|
||||||
Global, Function, Predicate
|
|
||||||
]
|
|
||||||
|
|
||||||
export const RSLanguage = LRLanguage.define({
|
export const RSLanguage = LRLanguage.define({
|
||||||
parser: parser,
|
parser: parser,
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { TokenID } from '@/models/rslang';
|
||||||
import { CodeMirrorWrapper } from '@/utils/codemirror';
|
import { CodeMirrorWrapper } from '@/utils/codemirror';
|
||||||
|
|
||||||
export function getSymbolSubstitute(keyCode: string, shiftPressed: boolean): string | undefined {
|
export function getSymbolSubstitute(keyCode: string, shiftPressed: boolean): string | undefined {
|
||||||
|
// prettier-ignore
|
||||||
if (shiftPressed) {
|
if (shiftPressed) {
|
||||||
switch (keyCode) {
|
switch (keyCode) {
|
||||||
case 'Backquote': return '∃';
|
case 'Backquote': return '∃';
|
||||||
|
@ -59,7 +60,7 @@ export function getSymbolSubstitute(keyCode: string, shiftPressed: boolean): str
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper class for RSLang editor.
|
* Wrapper class for RSLang editor.
|
||||||
*/
|
*/
|
||||||
export class RSTextWrapper extends CodeMirrorWrapper {
|
export class RSTextWrapper extends CodeMirrorWrapper {
|
||||||
constructor(object: Required<ReactCodeMirrorRef>) {
|
constructor(object: Required<ReactCodeMirrorRef>) {
|
||||||
super(object);
|
super(object);
|
||||||
|
@ -67,149 +68,216 @@ export class RSTextWrapper extends CodeMirrorWrapper {
|
||||||
|
|
||||||
insertToken(tokenID: TokenID): boolean {
|
insertToken(tokenID: TokenID): boolean {
|
||||||
const selection = this.getSelection();
|
const selection = this.getSelection();
|
||||||
const hasSelection = selection.from !== selection.to
|
const hasSelection = selection.from !== selection.to;
|
||||||
switch (tokenID) {
|
switch (tokenID) {
|
||||||
case TokenID.NT_DECLARATIVE_EXPR: {
|
case TokenID.NT_DECLARATIVE_EXPR: {
|
||||||
if (hasSelection) {
|
if (hasSelection) {
|
||||||
this.envelopeWith('D{ξ∈X1 | ', '}');
|
this.envelopeWith('D{ξ∈X1 | ', '}');
|
||||||
} else {
|
} else {
|
||||||
this.envelopeWith('D{ξ∈X1 | P1[ξ]', '}');
|
this.envelopeWith('D{ξ∈X1 | P1[ξ]', '}');
|
||||||
}
|
|
||||||
this.ref.view.dispatch({
|
|
||||||
selection: {
|
|
||||||
anchor: selection.from + 2,
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case TokenID.NT_IMPERATIVE_EXPR: {
|
|
||||||
if (hasSelection) {
|
|
||||||
this.envelopeWith('I{(σ, γ) | σ:∈X1; γ:=F1[σ]; ', '}');
|
|
||||||
} else {
|
|
||||||
this.envelopeWith('I{(σ, γ) | σ:∈X1; γ:=F1[σ]; P1[σ, γ]', '}');
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case TokenID.NT_RECURSIVE_FULL: {
|
|
||||||
if (hasSelection) {
|
|
||||||
this.envelopeWith('R{ξ:=D1 | F1[ξ]≠∅ | ', '}');
|
|
||||||
} else {
|
|
||||||
this.envelopeWith('R{ξ:=D1 | F1[ξ]≠∅ | ξ∪F1[ξ]', '}');
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case TokenID.BIGPR: this.envelopeWith('Pr1(', ')'); return true;
|
|
||||||
case TokenID.SMALLPR: this.envelopeWith('pr1(', ')'); return true;
|
|
||||||
case TokenID.FILTER: this.envelopeWith('Fi1[α](', ')'); return true;
|
|
||||||
case TokenID.REDUCE: this.envelopeWith('red(', ')'); return true;
|
|
||||||
case TokenID.CARD: this.envelopeWith('card(', ')'); return true;
|
|
||||||
case TokenID.BOOL: this.envelopeWith('bool(', ')'); return true;
|
|
||||||
case TokenID.DEBOOL: this.envelopeWith('debool(', ')'); return true;
|
|
||||||
|
|
||||||
case TokenID.PUNCTUATION_PL: {
|
|
||||||
this.envelopeWith('(', ')');
|
|
||||||
this.ref.view.dispatch({
|
|
||||||
selection: {
|
|
||||||
anchor: hasSelection ? selection.to: selection.from + 1,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case TokenID.PUNCTUATION_SL: {
|
|
||||||
this.envelopeWith('[', ']');
|
|
||||||
if (hasSelection) {
|
|
||||||
this.ref.view.dispatch({
|
this.ref.view.dispatch({
|
||||||
selection: {
|
selection: {
|
||||||
anchor: hasSelection ? selection.to: selection.from + 1,
|
anchor: selection.from + 2
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return true;
|
case TokenID.NT_IMPERATIVE_EXPR: {
|
||||||
}
|
if (hasSelection) {
|
||||||
case TokenID.BOOLEAN: {
|
this.envelopeWith('I{(σ, γ) | σ:∈X1; γ:=F1[σ]; ', '}');
|
||||||
const selStart = selection.from;
|
} else {
|
||||||
if (hasSelection && this.ref.view.state.sliceDoc(selStart, selStart + 1) === 'ℬ') {
|
this.envelopeWith('I{(σ, γ) | σ:∈X1; γ:=F1[σ]; P1[σ, γ]', '}');
|
||||||
this.envelopeWith('ℬ', '');
|
}
|
||||||
} else {
|
return true;
|
||||||
this.envelopeWith('ℬ(', ')');
|
|
||||||
}
|
}
|
||||||
return true;
|
case TokenID.NT_RECURSIVE_FULL: {
|
||||||
}
|
if (hasSelection) {
|
||||||
|
this.envelopeWith('R{ξ:=D1 | F1[ξ]≠∅ | ', '}');
|
||||||
|
} else {
|
||||||
|
this.envelopeWith('R{ξ:=D1 | F1[ξ]≠∅ | ξ∪F1[ξ]', '}');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case TokenID.BIGPR:
|
||||||
|
this.envelopeWith('Pr1(', ')');
|
||||||
|
return true;
|
||||||
|
case TokenID.SMALLPR:
|
||||||
|
this.envelopeWith('pr1(', ')');
|
||||||
|
return true;
|
||||||
|
case TokenID.FILTER:
|
||||||
|
this.envelopeWith('Fi1[α](', ')');
|
||||||
|
return true;
|
||||||
|
case TokenID.REDUCE:
|
||||||
|
this.envelopeWith('red(', ')');
|
||||||
|
return true;
|
||||||
|
case TokenID.CARD:
|
||||||
|
this.envelopeWith('card(', ')');
|
||||||
|
return true;
|
||||||
|
case TokenID.BOOL:
|
||||||
|
this.envelopeWith('bool(', ')');
|
||||||
|
return true;
|
||||||
|
case TokenID.DEBOOL:
|
||||||
|
this.envelopeWith('debool(', ')');
|
||||||
|
return true;
|
||||||
|
|
||||||
case TokenID.DECART: this.replaceWith('×'); return true;
|
case TokenID.PUNCTUATION_PL: {
|
||||||
case TokenID.QUANTOR_UNIVERSAL: this.replaceWith('∀'); return true;
|
this.envelopeWith('(', ')');
|
||||||
case TokenID.QUANTOR_EXISTS: this.replaceWith('∃'); return true;
|
this.ref.view.dispatch({
|
||||||
case TokenID.SET_IN: this.replaceWith('∈'); return true;
|
selection: {
|
||||||
case TokenID.SET_NOT_IN: this.replaceWith('∉'); return true;
|
anchor: hasSelection ? selection.to : selection.from + 1
|
||||||
case TokenID.LOGIC_OR: this.replaceWith('∨'); return true;
|
}
|
||||||
case TokenID.LOGIC_AND: this.replaceWith('&'); return true;
|
});
|
||||||
case TokenID.SUBSET_OR_EQ: this.replaceWith('⊆'); return true;
|
return true;
|
||||||
case TokenID.LOGIC_IMPLICATION: this.replaceWith('⇒'); return true;
|
}
|
||||||
case TokenID.SET_INTERSECTION: this.replaceWith('∩'); return true;
|
case TokenID.PUNCTUATION_SL: {
|
||||||
case TokenID.SET_UNION: this.replaceWith('∪'); return true;
|
this.envelopeWith('[', ']');
|
||||||
case TokenID.SET_MINUS: this.replaceWith('\\'); return true;
|
if (hasSelection) {
|
||||||
case TokenID.SET_SYMMETRIC_MINUS: this.replaceWith('∆'); return true;
|
this.ref.view.dispatch({
|
||||||
case TokenID.LIT_EMPTYSET: this.replaceWith('∅'); return true;
|
selection: {
|
||||||
case TokenID.LIT_WHOLE_NUMBERS: this.replaceWith('Z'); return true;
|
anchor: hasSelection ? selection.to : selection.from + 1
|
||||||
case TokenID.SUBSET: this.replaceWith('⊂'); return true;
|
}
|
||||||
case TokenID.NOT_SUBSET: this.replaceWith('⊄'); return true;
|
});
|
||||||
case TokenID.EQUAL: this.replaceWith('='); return true;
|
}
|
||||||
case TokenID.NOTEQUAL: this.replaceWith('≠'); return true;
|
return true;
|
||||||
case TokenID.LOGIC_NOT: this.replaceWith('¬'); return true;
|
}
|
||||||
case TokenID.LOGIC_EQUIVALENT: this.replaceWith('⇔'); return true;
|
case TokenID.BOOLEAN: {
|
||||||
case TokenID.GREATER_OR_EQ: this.replaceWith('≥'); return true;
|
const selStart = selection.from;
|
||||||
case TokenID.LESSER_OR_EQ: this.replaceWith('≤'); return true;
|
if (hasSelection && this.ref.view.state.sliceDoc(selStart, selStart + 1) === 'ℬ') {
|
||||||
case TokenID.PUNCTUATION_ASSIGN: this.replaceWith(':='); return true;
|
this.envelopeWith('ℬ', '');
|
||||||
case TokenID.PUNCTUATION_ITERATE: this.replaceWith(':∈'); return true;
|
} else {
|
||||||
case TokenID.MULTIPLY: this.replaceWith('*'); return true;
|
this.envelopeWith('ℬ(', ')');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
case TokenID.DECART:
|
||||||
|
this.replaceWith('×');
|
||||||
|
return true;
|
||||||
|
case TokenID.QUANTOR_UNIVERSAL:
|
||||||
|
this.replaceWith('∀');
|
||||||
|
return true;
|
||||||
|
case TokenID.QUANTOR_EXISTS:
|
||||||
|
this.replaceWith('∃');
|
||||||
|
return true;
|
||||||
|
case TokenID.SET_IN:
|
||||||
|
this.replaceWith('∈');
|
||||||
|
return true;
|
||||||
|
case TokenID.SET_NOT_IN:
|
||||||
|
this.replaceWith('∉');
|
||||||
|
return true;
|
||||||
|
case TokenID.LOGIC_OR:
|
||||||
|
this.replaceWith('∨');
|
||||||
|
return true;
|
||||||
|
case TokenID.LOGIC_AND:
|
||||||
|
this.replaceWith('&');
|
||||||
|
return true;
|
||||||
|
case TokenID.SUBSET_OR_EQ:
|
||||||
|
this.replaceWith('⊆');
|
||||||
|
return true;
|
||||||
|
case TokenID.LOGIC_IMPLICATION:
|
||||||
|
this.replaceWith('⇒');
|
||||||
|
return true;
|
||||||
|
case TokenID.SET_INTERSECTION:
|
||||||
|
this.replaceWith('∩');
|
||||||
|
return true;
|
||||||
|
case TokenID.SET_UNION:
|
||||||
|
this.replaceWith('∪');
|
||||||
|
return true;
|
||||||
|
case TokenID.SET_MINUS:
|
||||||
|
this.replaceWith('\\');
|
||||||
|
return true;
|
||||||
|
case TokenID.SET_SYMMETRIC_MINUS:
|
||||||
|
this.replaceWith('∆');
|
||||||
|
return true;
|
||||||
|
case TokenID.LIT_EMPTYSET:
|
||||||
|
this.replaceWith('∅');
|
||||||
|
return true;
|
||||||
|
case TokenID.LIT_WHOLE_NUMBERS:
|
||||||
|
this.replaceWith('Z');
|
||||||
|
return true;
|
||||||
|
case TokenID.SUBSET:
|
||||||
|
this.replaceWith('⊂');
|
||||||
|
return true;
|
||||||
|
case TokenID.NOT_SUBSET:
|
||||||
|
this.replaceWith('⊄');
|
||||||
|
return true;
|
||||||
|
case TokenID.EQUAL:
|
||||||
|
this.replaceWith('=');
|
||||||
|
return true;
|
||||||
|
case TokenID.NOTEQUAL:
|
||||||
|
this.replaceWith('≠');
|
||||||
|
return true;
|
||||||
|
case TokenID.LOGIC_NOT:
|
||||||
|
this.replaceWith('¬');
|
||||||
|
return true;
|
||||||
|
case TokenID.LOGIC_EQUIVALENT:
|
||||||
|
this.replaceWith('⇔');
|
||||||
|
return true;
|
||||||
|
case TokenID.GREATER_OR_EQ:
|
||||||
|
this.replaceWith('≥');
|
||||||
|
return true;
|
||||||
|
case TokenID.LESSER_OR_EQ:
|
||||||
|
this.replaceWith('≤');
|
||||||
|
return true;
|
||||||
|
case TokenID.PUNCTUATION_ASSIGN:
|
||||||
|
this.replaceWith(':=');
|
||||||
|
return true;
|
||||||
|
case TokenID.PUNCTUATION_ITERATE:
|
||||||
|
this.replaceWith(':∈');
|
||||||
|
return true;
|
||||||
|
case TokenID.MULTIPLY:
|
||||||
|
this.replaceWith('*');
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
processAltKey(keyCode: string, shiftPressed: boolean): boolean {
|
processAltKey(keyCode: string, shiftPressed: boolean): boolean {
|
||||||
|
// prettier-ignore
|
||||||
if (shiftPressed) {
|
if (shiftPressed) {
|
||||||
switch (keyCode) {
|
switch (keyCode) {
|
||||||
case 'KeyE': return this.insertToken(TokenID.DECART);
|
case 'KeyE': return this.insertToken(TokenID.DECART);
|
||||||
|
|
||||||
case 'Backquote': return this.insertToken(TokenID.NOTEQUAL);
|
case 'Backquote': return this.insertToken(TokenID.NOTEQUAL);
|
||||||
case 'Digit1': return this.insertToken(TokenID.SET_NOT_IN); // !
|
case 'Digit1': return this.insertToken(TokenID.SET_NOT_IN); // !
|
||||||
case 'Digit2': return this.insertToken(TokenID.NOT_SUBSET); // @
|
case 'Digit2': return this.insertToken(TokenID.NOT_SUBSET); // @
|
||||||
case 'Digit3': return this.insertToken(TokenID.LOGIC_OR); // #
|
case 'Digit3': return this.insertToken(TokenID.LOGIC_OR); // #
|
||||||
case 'Digit4': return this.insertToken(TokenID.LOGIC_EQUIVALENT); // $
|
case 'Digit4': return this.insertToken(TokenID.LOGIC_EQUIVALENT); // $
|
||||||
case 'Digit5': return this.insertToken(TokenID.SET_SYMMETRIC_MINUS); // %
|
case 'Digit5': return this.insertToken(TokenID.SET_SYMMETRIC_MINUS); // %
|
||||||
case 'Digit6': return this.insertToken(TokenID.PUNCTUATION_ASSIGN); // ^
|
case 'Digit6': return this.insertToken(TokenID.PUNCTUATION_ASSIGN); // ^
|
||||||
case 'Digit7': return this.insertToken(TokenID.GREATER_OR_EQ); // &
|
case 'Digit7': return this.insertToken(TokenID.GREATER_OR_EQ); // &
|
||||||
case 'Digit8': return this.insertToken(TokenID.LESSER_OR_EQ); // *
|
case 'Digit8': return this.insertToken(TokenID.LESSER_OR_EQ); // *
|
||||||
case 'Digit9': return this.insertToken(TokenID.PUNCTUATION_PL); // (
|
case 'Digit9': return this.insertToken(TokenID.PUNCTUATION_PL); // (
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
switch (keyCode) {
|
switch (keyCode) {
|
||||||
case 'KeyQ': return this.insertToken(TokenID.BIGPR);
|
case 'KeyQ': return this.insertToken(TokenID.BIGPR);
|
||||||
case 'KeyW': return this.insertToken(TokenID.SMALLPR);
|
case 'KeyW': return this.insertToken(TokenID.SMALLPR);
|
||||||
case 'KeyE': return this.insertToken(TokenID.BOOLEAN);
|
case 'KeyE': return this.insertToken(TokenID.BOOLEAN);
|
||||||
case 'KeyR': return this.insertToken(TokenID.REDUCE);
|
case 'KeyR': return this.insertToken(TokenID.REDUCE);
|
||||||
case 'KeyT': return this.insertToken(TokenID.NT_RECURSIVE_FULL);
|
case 'KeyT': return this.insertToken(TokenID.NT_RECURSIVE_FULL);
|
||||||
case 'KeyA': return this.insertToken(TokenID.SET_INTERSECTION);
|
case 'KeyA': return this.insertToken(TokenID.SET_INTERSECTION);
|
||||||
case 'KeyS': return this.insertToken(TokenID.SET_UNION);
|
case 'KeyS': return this.insertToken(TokenID.SET_UNION);
|
||||||
case 'KeyD': return this.insertToken(TokenID.NT_DECLARATIVE_EXPR);
|
case 'KeyD': return this.insertToken(TokenID.NT_DECLARATIVE_EXPR);
|
||||||
case 'KeyF': return this.insertToken(TokenID.FILTER);
|
case 'KeyF': return this.insertToken(TokenID.FILTER);
|
||||||
case 'KeyG': return this.insertToken(TokenID.NT_IMPERATIVE_EXPR);
|
case 'KeyG': return this.insertToken(TokenID.NT_IMPERATIVE_EXPR);
|
||||||
case 'KeyZ': return this.insertToken(TokenID.LIT_WHOLE_NUMBERS);
|
case 'KeyZ': return this.insertToken(TokenID.LIT_WHOLE_NUMBERS);
|
||||||
case 'KeyX': return this.insertToken(TokenID.LIT_EMPTYSET);
|
case 'KeyX': return this.insertToken(TokenID.LIT_EMPTYSET);
|
||||||
case 'KeyC': return this.insertToken(TokenID.CARD);
|
case 'KeyC': return this.insertToken(TokenID.CARD);
|
||||||
case 'KeyV': return this.insertToken(TokenID.DEBOOL);
|
case 'KeyV': return this.insertToken(TokenID.DEBOOL);
|
||||||
case 'KeyB': return this.insertToken(TokenID.BOOL);
|
case 'KeyB': return this.insertToken(TokenID.BOOL);
|
||||||
|
|
||||||
case 'Backquote': return this.insertToken(TokenID.LOGIC_NOT);
|
case 'Backquote': return this.insertToken(TokenID.LOGIC_NOT);
|
||||||
case 'Digit1': return this.insertToken(TokenID.SET_IN);
|
case 'Digit1': return this.insertToken(TokenID.SET_IN);
|
||||||
case 'Digit2': return this.insertToken(TokenID.SUBSET_OR_EQ);
|
case 'Digit2': return this.insertToken(TokenID.SUBSET_OR_EQ);
|
||||||
case 'Digit3': return this.insertToken(TokenID.LOGIC_AND);
|
case 'Digit3': return this.insertToken(TokenID.LOGIC_AND);
|
||||||
case 'Digit4': return this.insertToken(TokenID.LOGIC_IMPLICATION);
|
case 'Digit4': return this.insertToken(TokenID.LOGIC_IMPLICATION);
|
||||||
case 'Digit5': return this.insertToken(TokenID.SET_MINUS);
|
case 'Digit5': return this.insertToken(TokenID.SET_MINUS);
|
||||||
case 'Digit6': return this.insertToken(TokenID.PUNCTUATION_ITERATE);
|
case 'Digit6': return this.insertToken(TokenID.PUNCTUATION_ITERATE);
|
||||||
case 'Digit7': return this.insertToken(TokenID.SUBSET);
|
case 'Digit7': return this.insertToken(TokenID.SUBSET);
|
||||||
case 'Digit8': return this.insertToken(TokenID.MULTIPLY);
|
case 'Digit8': return this.insertToken(TokenID.MULTIPLY);
|
||||||
case 'BracketLeft': return this.insertToken(TokenID.PUNCTUATION_SL);
|
case 'BracketLeft': return this.insertToken(TokenID.PUNCTUATION_SL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { syntaxTree } from "@codemirror/language"
|
import { syntaxTree } from '@codemirror/language';
|
||||||
import { Extension } from '@codemirror/state';
|
import { Extension } from '@codemirror/state';
|
||||||
import { hoverTooltip } from '@codemirror/view';
|
import { hoverTooltip } from '@codemirror/view';
|
||||||
import { EditorState } from '@uiw/react-codemirror';
|
import { EditorState } from '@uiw/react-codemirror';
|
||||||
|
@ -15,15 +15,14 @@ function findAliasAt(pos: number, state: EditorState) {
|
||||||
let alias = '';
|
let alias = '';
|
||||||
let start = 0;
|
let start = 0;
|
||||||
let end = 0;
|
let end = 0;
|
||||||
nodes.forEach(
|
nodes.forEach(node => {
|
||||||
node => {
|
|
||||||
if (node.to <= lineEnd && node.from >= lineStart) {
|
if (node.to <= lineEnd && node.from >= lineStart) {
|
||||||
alias = text.slice(node.from - lineStart, node.to - lineStart);
|
alias = text.slice(node.from - lineStart, node.to - lineStart);
|
||||||
start = node.from;
|
start = node.from;
|
||||||
end = node.to;
|
end = node.to;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return {alias, start, end};
|
return { alias, start, end };
|
||||||
}
|
}
|
||||||
|
|
||||||
const globalsHoverTooltip = (items: IConstituenta[]) => {
|
const globalsHoverTooltip = (items: IConstituenta[]) => {
|
||||||
|
@ -38,9 +37,9 @@ const globalsHoverTooltip = (items: IConstituenta[]) => {
|
||||||
end: end,
|
end: end,
|
||||||
above: false,
|
above: false,
|
||||||
create: () => domTooltipConstituenta(cst)
|
create: () => domTooltipConstituenta(cst)
|
||||||
}
|
};
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export function rsHoverTooltip(items: IConstituenta[]): Extension {
|
export function rsHoverTooltip(items: IConstituenta[]): Extension {
|
||||||
return [globalsHoverTooltip(items)];
|
return [globalsHoverTooltip(items)];
|
||||||
|
|
|
@ -50,157 +50,158 @@ const editorSetup: BasicSetupOptions = {
|
||||||
};
|
};
|
||||||
|
|
||||||
interface RefsInputInputProps
|
interface RefsInputInputProps
|
||||||
extends Pick<ReactCodeMirrorProps,
|
extends Pick<ReactCodeMirrorProps, 'id' | 'height' | 'value' | 'className' | 'onFocus' | 'onBlur' | 'placeholder'> {
|
||||||
'id'| 'height' | 'value' | 'className' | 'onFocus' | 'onBlur' | 'placeholder'
|
label?: string;
|
||||||
> {
|
onChange?: (newValue: string) => void;
|
||||||
label?: string
|
items?: IConstituenta[];
|
||||||
onChange?: (newValue: string) => void
|
disabled?: boolean;
|
||||||
items?: IConstituenta[]
|
|
||||||
disabled?: boolean
|
|
||||||
|
|
||||||
initialValue?: string
|
initialValue?: string;
|
||||||
value?: string
|
value?: string;
|
||||||
resolved?: string
|
resolved?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
|
const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
|
||||||
({
|
({ id, label, disabled, items, initialValue, value, resolved, onFocus, onBlur, onChange, ...restProps }, ref) => {
|
||||||
id, label, disabled, items,
|
const { darkMode, colors } = useConceptTheme();
|
||||||
initialValue, value, resolved,
|
const { schema } = useRSForm();
|
||||||
onFocus, onBlur, onChange,
|
|
||||||
...restProps
|
|
||||||
}, ref) => {
|
|
||||||
const { darkMode, colors } = useConceptTheme();
|
|
||||||
const { schema } = useRSForm();
|
|
||||||
|
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
|
||||||
const [showEditor, setShowEditor] = useState(false);
|
const [showEditor, setShowEditor] = useState(false);
|
||||||
const [currentType, setCurrentType] = useState<ReferenceType>(ReferenceType.ENTITY);
|
const [currentType, setCurrentType] = useState<ReferenceType>(ReferenceType.ENTITY);
|
||||||
const [refText, setRefText] = useState('');
|
const [refText, setRefText] = useState('');
|
||||||
const [hintText, setHintText] = useState('');
|
const [hintText, setHintText] = useState('');
|
||||||
const [basePosition, setBasePosition] = useState(0);
|
const [basePosition, setBasePosition] = useState(0);
|
||||||
const [mainRefs, setMainRefs] = useState<string[]>([]);
|
const [mainRefs, setMainRefs] = useState<string[]>([]);
|
||||||
|
|
||||||
const internalRef = useRef<ReactCodeMirrorRef>(null);
|
const internalRef = useRef<ReactCodeMirrorRef>(null);
|
||||||
const thisRef = useMemo(() => (!ref || typeof ref === 'function' ? internalRef : ref), [internalRef, ref]);
|
const thisRef = useMemo(() => (!ref || typeof ref === 'function' ? internalRef : ref), [internalRef, ref]);
|
||||||
|
|
||||||
const cursor = useMemo(() => !disabled ? 'cursor-text': 'cursor-default', [disabled]);
|
const cursor = useMemo(() => (!disabled ? 'cursor-text' : 'cursor-default'), [disabled]);
|
||||||
const customTheme: Extension = useMemo(
|
const customTheme: Extension = useMemo(
|
||||||
() => createTheme({
|
() =>
|
||||||
theme: darkMode ? 'dark' : 'light',
|
createTheme({
|
||||||
settings: {
|
theme: darkMode ? 'dark' : 'light',
|
||||||
fontFamily: 'inherit',
|
settings: {
|
||||||
background: !disabled ? colors.bgInput : colors.bgDefault,
|
fontFamily: 'inherit',
|
||||||
foreground: colors.fgDefault,
|
background: !disabled ? colors.bgInput : colors.bgDefault,
|
||||||
selection: colors.bgHover
|
foreground: colors.fgDefault,
|
||||||
},
|
selection: colors.bgHover
|
||||||
styles: [
|
},
|
||||||
{ tag: tags.name, color: colors.fgPurple, cursor: 'default' }, // EntityReference
|
styles: [
|
||||||
{ tag: tags.literal, color: colors.fgTeal, cursor: 'default' }, // SyntacticReference
|
{ tag: tags.name, color: colors.fgPurple, cursor: 'default' }, // EntityReference
|
||||||
{ tag: tags.comment, color: colors.fgRed }, // Error
|
{ tag: tags.literal, color: colors.fgTeal, cursor: 'default' }, // SyntacticReference
|
||||||
]
|
{ tag: tags.comment, color: colors.fgRed } // Error
|
||||||
}), [disabled, colors, darkMode]);
|
]
|
||||||
|
}),
|
||||||
|
[disabled, colors, darkMode]
|
||||||
|
);
|
||||||
|
|
||||||
const editorExtensions = useMemo(
|
const editorExtensions = useMemo(
|
||||||
() => [
|
() => [EditorView.lineWrapping, NaturalLanguage, refsHoverTooltip(schema?.items || [], colors)],
|
||||||
EditorView.lineWrapping,
|
[schema?.items, colors]
|
||||||
NaturalLanguage,
|
);
|
||||||
refsHoverTooltip(schema?.items || [], colors)
|
|
||||||
], [schema?.items, colors]);
|
|
||||||
|
|
||||||
function handleChange(newValue: string) {
|
function handleChange(newValue: string) {
|
||||||
if (onChange) onChange(newValue);
|
if (onChange) onChange(newValue);
|
||||||
}
|
|
||||||
|
|
||||||
function handleFocusIn(event: React.FocusEvent<HTMLDivElement>) {
|
|
||||||
setIsFocused(true);
|
|
||||||
if (onFocus) onFocus(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleFocusOut(event: React.FocusEvent<HTMLDivElement>) {
|
|
||||||
setIsFocused(false);
|
|
||||||
if (onBlur) onBlur(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleInput = useCallback(
|
|
||||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
||||||
if (!thisRef.current?.view) {
|
|
||||||
event.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (event.ctrlKey && event.code === 'Space') {
|
|
||||||
const wrap = new CodeMirrorWrapper(thisRef.current as Required<ReactCodeMirrorRef>);
|
|
||||||
wrap.fixSelection(ReferenceTokens);
|
|
||||||
const nodes = wrap.getEnvelopingNodes(ReferenceTokens);
|
|
||||||
if (nodes.length !== 1) {
|
|
||||||
setCurrentType(ReferenceType.ENTITY);
|
|
||||||
setRefText('');
|
|
||||||
setHintText(wrap.getSelectionText());
|
|
||||||
} else {
|
|
||||||
setCurrentType(nodes[0].type.id === RefEntity ? ReferenceType.ENTITY : ReferenceType.SYNTACTIC);
|
|
||||||
setRefText(wrap.getSelectionText());
|
|
||||||
}
|
|
||||||
|
|
||||||
const selection = wrap.getSelection();
|
function handleFocusIn(event: React.FocusEvent<HTMLDivElement>) {
|
||||||
const mainNodes = wrap.getAllNodes([RefEntity]).filter(node => node.from >= selection.to || node.to <= selection.from);
|
setIsFocused(true);
|
||||||
setMainRefs(mainNodes.map(node => wrap.getText(node.from, node.to)));
|
if (onFocus) onFocus(event);
|
||||||
setBasePosition(mainNodes.filter(node => node.to <= selection.from).length);
|
|
||||||
|
|
||||||
setShowEditor(true);
|
|
||||||
}
|
}
|
||||||
}, [thisRef]);
|
|
||||||
|
|
||||||
const handleInputReference = useCallback(
|
function handleFocusOut(event: React.FocusEvent<HTMLDivElement>) {
|
||||||
(referenceText: string) => {
|
setIsFocused(false);
|
||||||
if (!thisRef.current?.view) {
|
if (onBlur) onBlur(event);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
thisRef.current.view.focus();
|
|
||||||
const wrap = new CodeMirrorWrapper(thisRef.current as Required<ReactCodeMirrorRef>);
|
|
||||||
wrap.replaceWith(referenceText);
|
|
||||||
}, [thisRef]);
|
|
||||||
|
|
||||||
return (<>
|
const handleInput = useCallback(
|
||||||
<AnimatePresence>
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
{showEditor ?
|
if (!thisRef.current?.view) {
|
||||||
<DlgEditReference
|
event.preventDefault();
|
||||||
hideWindow={() => setShowEditor(false)}
|
return;
|
||||||
items={items ?? []}
|
}
|
||||||
initial={{
|
if (event.ctrlKey && event.code === 'Space') {
|
||||||
type: currentType,
|
const wrap = new CodeMirrorWrapper(thisRef.current as Required<ReactCodeMirrorRef>);
|
||||||
refRaw: refText,
|
wrap.fixSelection(ReferenceTokens);
|
||||||
text: hintText,
|
const nodes = wrap.getEnvelopingNodes(ReferenceTokens);
|
||||||
basePosition: basePosition,
|
if (nodes.length !== 1) {
|
||||||
mainRefs: mainRefs
|
setCurrentType(ReferenceType.ENTITY);
|
||||||
}}
|
setRefText('');
|
||||||
onSave={handleInputReference}
|
setHintText(wrap.getSelectionText());
|
||||||
/> : null}
|
} else {
|
||||||
</AnimatePresence>
|
setCurrentType(nodes[0].type.id === RefEntity ? ReferenceType.ENTITY : ReferenceType.SYNTACTIC);
|
||||||
|
setRefText(wrap.getSelectionText());
|
||||||
|
}
|
||||||
|
|
||||||
<div className={clsx(
|
const selection = wrap.getSelection();
|
||||||
'flex flex-col gap-2',
|
const mainNodes = wrap
|
||||||
cursor
|
.getAllNodes([RefEntity])
|
||||||
)}>
|
.filter(node => node.from >= selection.to || node.to <= selection.from);
|
||||||
<Label text={label} htmlFor={id} />
|
setMainRefs(mainNodes.map(node => wrap.getText(node.from, node.to)));
|
||||||
<CodeMirror id={id} ref={thisRef}
|
setBasePosition(mainNodes.filter(node => node.to <= selection.from).length);
|
||||||
basicSetup={editorSetup}
|
|
||||||
theme={customTheme}
|
|
||||||
extensions={editorExtensions}
|
|
||||||
|
|
||||||
value={isFocused ? value : (value !== initialValue || showEditor ? value : resolved)}
|
setShowEditor(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[thisRef]
|
||||||
|
);
|
||||||
|
|
||||||
indentWithTab={false}
|
const handleInputReference = useCallback(
|
||||||
onChange={handleChange}
|
(referenceText: string) => {
|
||||||
editable={!disabled}
|
if (!thisRef.current?.view) {
|
||||||
onKeyDown={handleInput}
|
return;
|
||||||
onFocus={handleFocusIn}
|
}
|
||||||
onBlur={handleFocusOut}
|
thisRef.current.view.focus();
|
||||||
// spellCheck= // TODO: figure out while automatic spellcheck doesnt work or implement with extension
|
const wrap = new CodeMirrorWrapper(thisRef.current as Required<ReactCodeMirrorRef>);
|
||||||
{...restProps}
|
wrap.replaceWith(referenceText);
|
||||||
/>
|
},
|
||||||
</div>
|
[thisRef]
|
||||||
</>);
|
);
|
||||||
});
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AnimatePresence>
|
||||||
|
{showEditor ? (
|
||||||
|
<DlgEditReference
|
||||||
|
hideWindow={() => setShowEditor(false)}
|
||||||
|
items={items ?? []}
|
||||||
|
initial={{
|
||||||
|
type: currentType,
|
||||||
|
refRaw: refText,
|
||||||
|
text: hintText,
|
||||||
|
basePosition: basePosition,
|
||||||
|
mainRefs: mainRefs
|
||||||
|
}}
|
||||||
|
onSave={handleInputReference}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
<div className={clsx('flex flex-col gap-2', cursor)}>
|
||||||
|
<Label text={label} htmlFor={id} />
|
||||||
|
<CodeMirror
|
||||||
|
id={id}
|
||||||
|
ref={thisRef}
|
||||||
|
basicSetup={editorSetup}
|
||||||
|
theme={customTheme}
|
||||||
|
extensions={editorExtensions}
|
||||||
|
value={isFocused ? value : value !== initialValue || showEditor ? value : resolved}
|
||||||
|
indentWithTab={false}
|
||||||
|
onChange={handleChange}
|
||||||
|
editable={!disabled}
|
||||||
|
onKeyDown={handleInput}
|
||||||
|
onFocus={handleFocusIn}
|
||||||
|
onBlur={handleFocusOut}
|
||||||
|
// spellCheck= // TODO: figure out while automatic spellcheck doesn't work or implement with extension
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export default RefsInput;
|
export default RefsInput;
|
|
@ -1,4 +1,4 @@
|
||||||
import {styleTags, tags} from '@lezer/highlight';
|
import { styleTags, tags } from '@lezer/highlight';
|
||||||
|
|
||||||
export const highlighting = styleTags({
|
export const highlighting = styleTags({
|
||||||
RefEntity: tags.name,
|
RefEntity: tags.name,
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import {LRLanguage} from '@codemirror/language';
|
import { LRLanguage } from '@codemirror/language';
|
||||||
|
|
||||||
import { parser } from './parser';
|
import { parser } from './parser';
|
||||||
import { RefEntity, RefSyntactic } from './parser.terms';
|
import { RefEntity, RefSyntactic } from './parser.terms';
|
||||||
|
|
||||||
export const ReferenceTokens: number[] = [
|
export const ReferenceTokens: number[] = [RefSyntactic, RefEntity];
|
||||||
RefSyntactic, RefEntity
|
|
||||||
]
|
|
||||||
|
|
||||||
export const NaturalLanguage = LRLanguage.define({
|
export const NaturalLanguage = LRLanguage.define({
|
||||||
parser: parser,
|
parser: parser,
|
||||||
|
|
|
@ -13,12 +13,11 @@ const testData = [
|
||||||
['@{-1| черный }', '[Text[RefSyntactic[Offset][Nominal]]]'],
|
['@{-1| черный }', '[Text[RefSyntactic[Offset][Nominal]]]'],
|
||||||
['@{-100| черный слон }', '[Text[RefSyntactic[Offset][Nominal]]]'],
|
['@{-100| черный слон }', '[Text[RefSyntactic[Offset][Nominal]]]'],
|
||||||
['@{X1|VERB,past,sing}', '[Text[RefEntity[Global][Grams]]]'],
|
['@{X1|VERB,past,sing}', '[Text[RefEntity[Global][Grams]]]'],
|
||||||
['@{X12|VERB,past,sing}', '[Text[RefEntity[Global][Grams]]]'],
|
['@{X12|VERB,past,sing}', '[Text[RefEntity[Global][Grams]]]']
|
||||||
];
|
];
|
||||||
|
|
||||||
describe('Testing NaturalParser', () => {
|
describe('Testing NaturalParser', () => {
|
||||||
it.each(testData)('Parse %p',
|
it.each(testData)('Parse %p', (input: string, expectedTree: string) => {
|
||||||
(input: string, expectedTree: string) => {
|
|
||||||
// NOTE: use strict parser to determine exact error position
|
// NOTE: use strict parser to determine exact error position
|
||||||
// const tree = parser.configure({strict: true}).parse(input);
|
// const tree = parser.configure({strict: true}).parse(input);
|
||||||
const tree = parser.parse(input);
|
const tree = parser.parse(input);
|
||||||
|
|
|
@ -1,18 +1,22 @@
|
||||||
import { syntaxTree } from '@codemirror/language'
|
import { syntaxTree } from '@codemirror/language';
|
||||||
import { Extension } from '@codemirror/state';
|
import { Extension } from '@codemirror/state';
|
||||||
import { hoverTooltip } from '@codemirror/view';
|
import { hoverTooltip } from '@codemirror/view';
|
||||||
|
|
||||||
import { parseEntityReference, parseSyntacticReference } from '@/models/languageAPI';
|
import { parseEntityReference, parseSyntacticReference } from '@/models/languageAPI';
|
||||||
import { IConstituenta } from '@/models/rsform';
|
import { IConstituenta } from '@/models/rsform';
|
||||||
import { domTooltipEntityReference, domTooltipSyntacticReference, findContainedNodes, findEnvelopingNodes } from '@/utils/codemirror';
|
import {
|
||||||
|
domTooltipEntityReference,
|
||||||
|
domTooltipSyntacticReference,
|
||||||
|
findContainedNodes,
|
||||||
|
findEnvelopingNodes
|
||||||
|
} from '@/utils/codemirror';
|
||||||
import { IColorTheme } from '@/utils/color';
|
import { IColorTheme } from '@/utils/color';
|
||||||
|
|
||||||
import { ReferenceTokens } from './parse';
|
import { ReferenceTokens } from './parse';
|
||||||
import { RefEntity, RefSyntactic } from './parse/parser.terms';
|
import { RefEntity, RefSyntactic } from './parse/parser.terms';
|
||||||
|
|
||||||
export const globalsHoverTooltip = (items: IConstituenta[], colors: IColorTheme) => {
|
export const globalsHoverTooltip = (items: IConstituenta[], colors: IColorTheme) => {
|
||||||
return hoverTooltip(
|
return hoverTooltip((view, pos) => {
|
||||||
(view, pos) => {
|
|
||||||
const nodes = findEnvelopingNodes(pos, pos, syntaxTree(view.state), ReferenceTokens);
|
const nodes = findEnvelopingNodes(pos, pos, syntaxTree(view.state), ReferenceTokens);
|
||||||
if (nodes.length !== 1) {
|
if (nodes.length !== 1) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -28,7 +32,7 @@ export const globalsHoverTooltip = (items: IConstituenta[], colors: IColorTheme)
|
||||||
end: end,
|
end: end,
|
||||||
above: false,
|
above: false,
|
||||||
create: () => domTooltipEntityReference(ref, cst, colors)
|
create: () => domTooltipEntityReference(ref, cst, colors)
|
||||||
}
|
};
|
||||||
} else if (nodes[0].type.id === RefSyntactic) {
|
} else if (nodes[0].type.id === RefSyntactic) {
|
||||||
const ref = parseSyntacticReference(text);
|
const ref = parseSyntacticReference(text);
|
||||||
let masterText: string | undefined = undefined;
|
let masterText: string | undefined = undefined;
|
||||||
|
@ -50,12 +54,12 @@ export const globalsHoverTooltip = (items: IConstituenta[], colors: IColorTheme)
|
||||||
end: end,
|
end: end,
|
||||||
above: false,
|
above: false,
|
||||||
create: () => domTooltipSyntacticReference(ref, masterText)
|
create: () => domTooltipSyntacticReference(ref, masterText)
|
||||||
}
|
};
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export function refsHoverTooltip(items: IConstituenta[], colors: IColorTheme): Extension {
|
export function refsHoverTooltip(items: IConstituenta[], colors: IColorTheme): Extension {
|
||||||
return [globalsHoverTooltip(items, colors)];
|
return [globalsHoverTooltip(items, colors)];
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { useAuth } from '@/context/AuthContext';
|
||||||
import TextURL from './Common/TextURL';
|
import TextURL from './Common/TextURL';
|
||||||
|
|
||||||
interface RequireAuthProps {
|
interface RequireAuthProps {
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function RequireAuth({ children }: RequireAuthProps) {
|
function RequireAuth({ children }: RequireAuthProps) {
|
||||||
|
@ -15,12 +15,13 @@ function RequireAuth({ children }: RequireAuthProps) {
|
||||||
return children;
|
return children;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col items-center gap-1 mt-2'>
|
<div className='flex flex-col items-center gap-1 mt-2'>
|
||||||
<p className='mb-2'>Пожалуйста войдите в систему</p>
|
<p className='mb-2'>Пожалуйста войдите в систему</p>
|
||||||
<TextURL text='Войти в Портал' href='/login'/>
|
<TextURL text='Войти в Портал' href='/login' />
|
||||||
<TextURL text='Зарегистрироваться' href='/signup'/>
|
<TextURL text='Зарегистрироваться' href='/signup' />
|
||||||
<TextURL text='Начальная страница' href='/'/>
|
<TextURL text='Начальная страница' href='/' />
|
||||||
</div>);
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,46 +4,43 @@ import ConceptTooltip from '@/components/Common/ConceptTooltip';
|
||||||
import ConstituentaTooltip from '@/components/Help/ConstituentaTooltip';
|
import ConstituentaTooltip from '@/components/Help/ConstituentaTooltip';
|
||||||
import { IConstituenta } from '@/models/rsform';
|
import { IConstituenta } from '@/models/rsform';
|
||||||
import { isMockCst } from '@/models/rsformAPI';
|
import { isMockCst } from '@/models/rsformAPI';
|
||||||
import { colorFgCstStatus,IColorTheme } from '@/utils/color';
|
import { colorFgCstStatus, IColorTheme } from '@/utils/color';
|
||||||
import { describeExpressionStatus } from '@/utils/labels';
|
import { describeExpressionStatus } from '@/utils/labels';
|
||||||
|
|
||||||
interface ConstituentaBadgeProps {
|
interface ConstituentaBadgeProps {
|
||||||
prefixID?: string
|
prefixID?: string;
|
||||||
shortTooltip?: boolean
|
shortTooltip?: boolean;
|
||||||
value: IConstituenta
|
value: IConstituenta;
|
||||||
theme: IColorTheme
|
theme: IColorTheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ConstituentaBadge({ value, prefixID, shortTooltip, theme }: ConstituentaBadgeProps) {
|
function ConstituentaBadge({ value, prefixID, shortTooltip, theme }: ConstituentaBadgeProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id={`${prefixID}${value.alias}`}
|
id={`${prefixID}${value.alias}`}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'min-w-[3.1rem] max-w-[3.1rem]',
|
'min-w-[3.1rem] max-w-[3.1rem]',
|
||||||
'px-1',
|
'px-1',
|
||||||
'border rounded-md',
|
'border rounded-md',
|
||||||
'text-center font-semibold whitespace-nowrap'
|
'text-center font-semibold whitespace-nowrap'
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
borderColor: colorFgCstStatus(value.status, theme),
|
borderColor: colorFgCstStatus(value.status, theme),
|
||||||
color: colorFgCstStatus(value.status, theme),
|
color: colorFgCstStatus(value.status, theme),
|
||||||
backgroundColor: isMockCst(value) ? theme.bgWarning : theme.bgInput
|
backgroundColor: isMockCst(value) ? theme.bgWarning : theme.bgInput
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
{value.alias}
|
|
||||||
{!shortTooltip ?
|
|
||||||
<ConstituentaTooltip
|
|
||||||
anchor={`#${prefixID}${value.alias}`}
|
|
||||||
data={value}
|
|
||||||
/> : null}
|
|
||||||
{shortTooltip ?
|
|
||||||
<ConceptTooltip
|
|
||||||
anchorSelect={`#${prefixID}${value.alias}`}
|
|
||||||
place='right'
|
|
||||||
>
|
>
|
||||||
<p><b>Статус</b>: {describeExpressionStatus(value.status)}</p>
|
{value.alias}
|
||||||
</ConceptTooltip> : null}
|
{!shortTooltip ? <ConstituentaTooltip anchor={`#${prefixID}${value.alias}`} data={value} /> : null}
|
||||||
</div>);
|
{shortTooltip ? (
|
||||||
|
<ConceptTooltip anchorSelect={`#${prefixID}${value.alias}`} place='right'>
|
||||||
|
<p>
|
||||||
|
<b>Статус</b>: {describeExpressionStatus(value.status)}
|
||||||
|
</p>
|
||||||
|
</ConceptTooltip>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ConstituentaBadge;
|
export default ConstituentaBadge;
|
|
@ -12,35 +12,35 @@ import { describeConstituenta } from '@/utils/labels';
|
||||||
import ConstituentaBadge from './ConstituentaBadge';
|
import ConstituentaBadge from './ConstituentaBadge';
|
||||||
|
|
||||||
interface ConstituentaPickerProps {
|
interface ConstituentaPickerProps {
|
||||||
prefixID?: string
|
prefixID?: string;
|
||||||
data?: IConstituenta[]
|
data?: IConstituenta[];
|
||||||
rows?: number
|
rows?: number;
|
||||||
|
|
||||||
onBeginFilter?: (cst: IConstituenta) => boolean
|
onBeginFilter?: (cst: IConstituenta) => boolean;
|
||||||
describeFunc?: (cst: IConstituenta) => string
|
describeFunc?: (cst: IConstituenta) => string;
|
||||||
matchFunc?: (cst: IConstituenta, filter: string) => boolean
|
matchFunc?: (cst: IConstituenta, filter: string) => boolean;
|
||||||
|
|
||||||
value?: IConstituenta
|
value?: IConstituenta;
|
||||||
onSelectValue: (newValue: IConstituenta) => void
|
onSelectValue: (newValue: IConstituenta) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<IConstituenta>();
|
const columnHelper = createColumnHelper<IConstituenta>();
|
||||||
|
|
||||||
function ConstituentaPicker({
|
function ConstituentaPicker({
|
||||||
data, value,
|
data,
|
||||||
|
value,
|
||||||
rows = 4,
|
rows = 4,
|
||||||
prefixID = prefixes.cst_list,
|
prefixID = prefixes.cst_list,
|
||||||
describeFunc = describeConstituenta,
|
describeFunc = describeConstituenta,
|
||||||
matchFunc = (cst, filter) => matchConstituenta(cst, filter, CstMatchMode.ALL),
|
matchFunc = (cst, filter) => matchConstituenta(cst, filter, CstMatchMode.ALL),
|
||||||
onBeginFilter,
|
onBeginFilter,
|
||||||
onSelectValue
|
onSelectValue
|
||||||
} : ConstituentaPickerProps) {
|
}: ConstituentaPickerProps) {
|
||||||
const { colors } = useConceptTheme();
|
const { colors } = useConceptTheme();
|
||||||
const [filteredData, setFilteredData] = useState<IConstituenta[]>([]);
|
const [filteredData, setFilteredData] = useState<IConstituenta[]>([]);
|
||||||
const [filterText, setFilterText] = useState('');
|
const [filterText, setFilterText] = useState('');
|
||||||
|
|
||||||
useEffect(
|
useEffect(() => {
|
||||||
() => {
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
setFilteredData([]);
|
setFilteredData([]);
|
||||||
} else {
|
} else {
|
||||||
|
@ -51,57 +51,58 @@ function ConstituentaPicker({
|
||||||
setFilteredData(newData);
|
setFilteredData(newData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}, [data, filterText, matchFunc, onBeginFilter]);
|
}, [data, filterText, matchFunc, onBeginFilter]);
|
||||||
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() => [
|
() => [
|
||||||
columnHelper.accessor('alias', {
|
columnHelper.accessor('alias', {
|
||||||
id: 'alias',
|
id: 'alias',
|
||||||
size: 65,
|
size: 65,
|
||||||
minSize: 65,
|
minSize: 65,
|
||||||
maxSize: 65,
|
maxSize: 65,
|
||||||
cell: props =>
|
cell: props => <ConstituentaBadge theme={colors} value={props.row.original} prefixID={prefixID} />
|
||||||
<ConstituentaBadge
|
}),
|
||||||
theme={colors}
|
columnHelper.accessor(cst => describeFunc(cst), {
|
||||||
value={props.row.original}
|
id: 'description'
|
||||||
prefixID={prefixID}
|
})
|
||||||
/>
|
],
|
||||||
}),
|
[colors, prefixID, describeFunc]
|
||||||
columnHelper.accessor(cst => describeFunc(cst), {
|
);
|
||||||
id: 'description'
|
|
||||||
})
|
|
||||||
], [colors, prefixID, describeFunc]);
|
|
||||||
|
|
||||||
const size = useMemo(() => (`calc(2px + (2px + 1.8rem)*${rows})`), [rows]);
|
const size = useMemo(() => `calc(2px + (2px + 1.8rem)*${rows})`, [rows]);
|
||||||
|
|
||||||
const conditionalRowStyles = useMemo(
|
const conditionalRowStyles = useMemo(
|
||||||
(): IConditionalStyle<IConstituenta>[] => [{
|
(): IConditionalStyle<IConstituenta>[] => [
|
||||||
when: (cst: IConstituenta) => cst.id === value?.id,
|
{
|
||||||
style: { backgroundColor: colors.bgSelected },
|
when: (cst: IConstituenta) => cst.id === value?.id,
|
||||||
}], [value, colors]);
|
style: { backgroundColor: colors.bgSelected }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[value, colors]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ConceptSearch
|
<ConceptSearch value={filterText} onChange={newValue => setFilterText(newValue)} />
|
||||||
value={filterText}
|
<DataTable
|
||||||
onChange={newValue => setFilterText(newValue)}
|
dense
|
||||||
/>
|
noHeader
|
||||||
<DataTable dense noHeader noFooter
|
noFooter
|
||||||
className='overflow-y-auto text-sm border select-none'
|
className='overflow-y-auto text-sm border select-none'
|
||||||
style={{ maxHeight: size, minHeight: size }}
|
style={{ maxHeight: size, minHeight: size }}
|
||||||
data={filteredData}
|
data={filteredData}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
conditionalRowStyles={conditionalRowStyles}
|
conditionalRowStyles={conditionalRowStyles}
|
||||||
noDataComponent={
|
noDataComponent={
|
||||||
<span className='p-2 min-h-[5rem] flex flex-col justify-center text-center'>
|
<span className='p-2 min-h-[5rem] flex flex-col justify-center text-center'>
|
||||||
<p>Список конституент пуст</p>
|
<p>Список конституент пуст</p>
|
||||||
<p>Измените параметры фильтра</p>
|
<p>Измените параметры фильтра</p>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
onRowClicked={onSelectValue}
|
onRowClicked={onSelectValue}
|
||||||
/>
|
/>
|
||||||
</div>);
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ConstituentaPicker;
|
export default ConstituentaPicker;
|
|
@ -6,29 +6,30 @@ import { colorFgGrammeme } from '@/utils/color';
|
||||||
import { labelGrammeme } from '@/utils/labels';
|
import { labelGrammeme } from '@/utils/labels';
|
||||||
|
|
||||||
interface GrammemeBadgeProps {
|
interface GrammemeBadgeProps {
|
||||||
key?: string
|
key?: string;
|
||||||
grammeme: GramData
|
grammeme: GramData;
|
||||||
}
|
}
|
||||||
|
|
||||||
function GrammemeBadge({ key, grammeme }: GrammemeBadgeProps) {
|
function GrammemeBadge({ key, grammeme }: GrammemeBadgeProps) {
|
||||||
const { colors } = useConceptTheme();
|
const { colors } = useConceptTheme();
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={key}
|
key={key}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'min-w-[3rem]',
|
'min-w-[3rem]',
|
||||||
'px-1',
|
'px-1',
|
||||||
'border rounded-md',
|
'border rounded-md',
|
||||||
'text-sm font-semibold text-center whitespace-nowrap'
|
'text-sm font-semibold text-center whitespace-nowrap'
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
borderColor: colorFgGrammeme(grammeme, colors),
|
borderColor: colorFgGrammeme(grammeme, colors),
|
||||||
color: colorFgGrammeme(grammeme, colors),
|
color: colorFgGrammeme(grammeme, colors),
|
||||||
backgroundColor: colors.bgInput
|
backgroundColor: colors.bgInput
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{labelGrammeme(grammeme)}
|
{labelGrammeme(grammeme)}
|
||||||
</div>);
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GrammemeBadge;
|
export default GrammemeBadge;
|
|
@ -1,39 +1,42 @@
|
||||||
import { IConstituenta } from '@/models/rsform';
|
import { IConstituenta } from '@/models/rsform';
|
||||||
import { labelCstTypification } from '@/utils/labels';
|
import { labelCstTypification } from '@/utils/labels';
|
||||||
|
|
||||||
interface InfoConstituentaProps
|
interface InfoConstituentaProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
extends React.HTMLAttributes<HTMLDivElement> {
|
data: IConstituenta;
|
||||||
data: IConstituenta
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function InfoConstituenta({ data, ...restProps }: InfoConstituentaProps) {
|
function InfoConstituenta({ data, ...restProps }: InfoConstituentaProps) {
|
||||||
return (
|
return (
|
||||||
<div {...restProps}>
|
<div {...restProps}>
|
||||||
<h2>Конституента {data.alias}</h2>
|
<h2>Конституента {data.alias}</h2>
|
||||||
<p>
|
<p>
|
||||||
<b>Типизация: </b>
|
<b>Типизация: </b>
|
||||||
{labelCstTypification(data)}
|
{labelCstTypification(data)}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<b>Термин: </b>
|
<b>Термин: </b>
|
||||||
{data.term_resolved || data.term_raw}
|
{data.term_resolved || data.term_raw}
|
||||||
</p>
|
</p>
|
||||||
{data.definition_formal ?
|
{data.definition_formal ? (
|
||||||
<p>
|
<p>
|
||||||
<b>Выражение: </b>
|
<b>Выражение: </b>
|
||||||
{data.definition_formal}
|
{data.definition_formal}
|
||||||
</p> : null}
|
</p>
|
||||||
{data.definition_resolved ?
|
) : null}
|
||||||
<p>
|
{data.definition_resolved ? (
|
||||||
<b>Определение: </b>
|
<p>
|
||||||
{data.definition_resolved}
|
<b>Определение: </b>
|
||||||
</p> : null}
|
{data.definition_resolved}
|
||||||
{data.convention ?
|
</p>
|
||||||
<p>
|
) : null}
|
||||||
<b>Конвенция: </b>
|
{data.convention ? (
|
||||||
{data.convention}
|
<p>
|
||||||
</p> : null}
|
<b>Конвенция: </b>
|
||||||
</div>);
|
{data.convention}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default InfoConstituenta;
|
export default InfoConstituenta;
|
|
@ -7,38 +7,37 @@ import { prefixes } from '@/utils/constants';
|
||||||
import { describeCstClass, labelCstClass } from '@/utils/labels';
|
import { describeCstClass, labelCstClass } from '@/utils/labels';
|
||||||
|
|
||||||
interface InfoCstClassProps {
|
interface InfoCstClassProps {
|
||||||
header?: string
|
header?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function InfoCstClass({ header }: InfoCstClassProps) {
|
function InfoCstClass({ header }: InfoCstClassProps) {
|
||||||
const { colors } = useConceptTheme();
|
const { colors } = useConceptTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-1 mb-2'>
|
<div className='flex flex-col gap-1 mb-2'>
|
||||||
{header ? <h1>{header}</h1> : null}
|
{header ? <h1>{header}</h1> : null}
|
||||||
{Object.values(CstClass).map(
|
{Object.values(CstClass).map((cstClass, index) => {
|
||||||
(cclass, index) => {
|
return (
|
||||||
return (
|
<p key={`${prefixes.cst_status_list}${index}`}>
|
||||||
<p key={`${prefixes.cst_status_list}${index}`}>
|
<span
|
||||||
<span
|
className={clsx(
|
||||||
className={clsx(
|
'inline-block',
|
||||||
'inline-block',
|
'min-w-[7rem]',
|
||||||
'min-w-[7rem]',
|
'px-1',
|
||||||
'px-1',
|
'border',
|
||||||
'border',
|
'text-center text-sm small-caps font-semibold'
|
||||||
'text-center text-sm small-caps font-semibold'
|
)}
|
||||||
)}
|
style={{ backgroundColor: colorBgCstClass(cstClass, colors) }}
|
||||||
style={{backgroundColor: colorBgCstClass(cclass, colors)}}
|
>
|
||||||
>
|
{labelCstClass(cstClass)}
|
||||||
{labelCstClass(cclass)}
|
</span>
|
||||||
</span>
|
<span> - </span>
|
||||||
<span> - </span>
|
<span>{describeCstClass(cstClass)}</span>
|
||||||
<span>
|
</p>
|
||||||
{describeCstClass(cclass)}
|
);
|
||||||
</span>
|
})}
|
||||||
</p>);
|
</div>
|
||||||
})}
|
);
|
||||||
</div>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default InfoCstClass;
|
export default InfoCstClass;
|
|
@ -7,39 +7,37 @@ import { prefixes } from '@/utils/constants';
|
||||||
import { describeExpressionStatus, labelExpressionStatus } from '@/utils/labels';
|
import { describeExpressionStatus, labelExpressionStatus } from '@/utils/labels';
|
||||||
|
|
||||||
interface InfoCstStatusProps {
|
interface InfoCstStatusProps {
|
||||||
title?: string
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function InfoCstStatus({ title }: InfoCstStatusProps) {
|
function InfoCstStatus({ title }: InfoCstStatusProps) {
|
||||||
const { colors } = useConceptTheme();
|
const { colors } = useConceptTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-1 mb-2'>
|
<div className='flex flex-col gap-1 mb-2'>
|
||||||
{title ? <h1>{title}</h1> : null}
|
{title ? <h1>{title}</h1> : null}
|
||||||
{Object.values(ExpressionStatus)
|
{Object.values(ExpressionStatus)
|
||||||
.filter(status => status !== ExpressionStatus.UNDEFINED)
|
.filter(status => status !== ExpressionStatus.UNDEFINED)
|
||||||
.map(
|
.map((status, index) => (
|
||||||
(status, index) =>
|
<p key={`${prefixes.cst_status_list}${index}`}>
|
||||||
<p key={`${prefixes.cst_status_list}${index}`}>
|
<span
|
||||||
<span
|
className={clsx(
|
||||||
className={clsx(
|
'inline-block',
|
||||||
'inline-block',
|
'min-w-[7rem]',
|
||||||
'min-w-[7rem]',
|
'px-1',
|
||||||
'px-1',
|
'border',
|
||||||
'border',
|
'text-center text-sm small-caps font-semibold'
|
||||||
'text-center text-sm small-caps font-semibold'
|
)}
|
||||||
)}
|
style={{ backgroundColor: colorBgCstStatus(status, colors) }}
|
||||||
style={{backgroundColor: colorBgCstStatus(status, colors)}}
|
>
|
||||||
>
|
{labelExpressionStatus(status)}
|
||||||
{labelExpressionStatus(status)}
|
</span>
|
||||||
</span>
|
<span> - </span>
|
||||||
<span> - </span>
|
<span>{describeExpressionStatus(status)}</span>
|
||||||
<span>
|
</p>
|
||||||
{describeExpressionStatus(status)}
|
))}
|
||||||
</span>
|
</div>
|
||||||
</p>
|
);
|
||||||
)}
|
|
||||||
</div>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default InfoCstStatus;
|
export default InfoCstStatus;
|
|
@ -4,35 +4,36 @@ import { useUsers } from '@/context/UsersContext';
|
||||||
import { ILibraryItemEx } from '@/models/library';
|
import { ILibraryItemEx } from '@/models/library';
|
||||||
|
|
||||||
interface InfoLibraryItemProps {
|
interface InfoLibraryItemProps {
|
||||||
item?: ILibraryItemEx
|
item?: ILibraryItemEx;
|
||||||
}
|
}
|
||||||
|
|
||||||
function InfoLibraryItem({ item }: InfoLibraryItemProps) {
|
function InfoLibraryItem({ item }: InfoLibraryItemProps) {
|
||||||
const { getUserLabel } = useUsers();
|
const { getUserLabel } = useUsers();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-1'>
|
<div className='flex flex-col gap-1'>
|
||||||
<div className='flex'>
|
<div className='flex'>
|
||||||
<label className='font-semibold'>Владелец:</label>
|
<label className='font-semibold'>Владелец:</label>
|
||||||
<span className='min-w-[200px] ml-2 overflow-ellipsis overflow-hidden whitespace-nowrap'>
|
<span className='min-w-[200px] ml-2 overflow-ellipsis overflow-hidden whitespace-nowrap'>
|
||||||
{getUserLabel(item?.owner ?? null)}
|
{getUserLabel(item?.owner ?? null)}
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex'>
|
||||||
|
<label className='font-semibold'>Отслеживают:</label>
|
||||||
|
<span id='subscriber-count' className='ml-2'>
|
||||||
|
{item?.subscribers.length ?? 0}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex'>
|
||||||
|
<label className='font-semibold'>Дата обновления:</label>
|
||||||
|
<span className='ml-2'>{item && new Date(item?.time_update).toLocaleString(intl.locale)}</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex'>
|
||||||
|
<label className='font-semibold'>Дата создания:</label>
|
||||||
|
<span className='ml-8'>{item && new Date(item?.time_create).toLocaleString(intl.locale)}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex'>
|
);
|
||||||
<label className='font-semibold'>Отслеживают:</label>
|
|
||||||
<span id='subscriber-count' className='ml-2'>
|
|
||||||
{ item?.subscribers.length ?? 0 }
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className='flex'>
|
|
||||||
<label className='font-semibold'>Дата обновления:</label>
|
|
||||||
<span className='ml-2'>{item && new Date(item?.time_update).toLocaleString(intl.locale)}</span>
|
|
||||||
</div>
|
|
||||||
<div className='flex'>
|
|
||||||
<label className='font-semibold'>Дата создания:</label>
|
|
||||||
<span className='ml-8'>{item && new Date(item?.time_create).toLocaleString(intl.locale)}</span>
|
|
||||||
</div>
|
|
||||||
</div>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default InfoLibraryItem;
|
export default InfoLibraryItem;
|
||||||
|
|
|
@ -3,39 +3,33 @@ import { useEffect, useState } from 'react';
|
||||||
import SelectMulti, { SelectMultiProps } from '@/components/Common/SelectMulti';
|
import SelectMulti, { SelectMultiProps } from '@/components/Common/SelectMulti';
|
||||||
import { Grammeme } from '@/models/language';
|
import { Grammeme } from '@/models/language';
|
||||||
import { getCompatibleGrams } from '@/models/languageAPI';
|
import { getCompatibleGrams } from '@/models/languageAPI';
|
||||||
import { compareGrammemeOptions,IGrammemeOption, SelectorGrammemes } from '@/utils/selectors';
|
import { compareGrammemeOptions, IGrammemeOption, SelectorGrammemes } from '@/utils/selectors';
|
||||||
|
|
||||||
interface SelectGrammemeProps extends
|
interface SelectGrammemeProps extends Omit<SelectMultiProps<IGrammemeOption>, 'value' | 'onChange'> {
|
||||||
Omit<SelectMultiProps<IGrammemeOption>, 'value' | 'onChange'> {
|
value: IGrammemeOption[];
|
||||||
value: IGrammemeOption[]
|
setValue: React.Dispatch<React.SetStateAction<IGrammemeOption[]>>;
|
||||||
setValue: React.Dispatch<React.SetStateAction<IGrammemeOption[]>>
|
className?: string;
|
||||||
className?: string
|
placeholder?: string;
|
||||||
placeholder?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectGrammeme({
|
function SelectGrammeme({ value, setValue, ...restProps }: SelectGrammemeProps) {
|
||||||
value, setValue,
|
|
||||||
...restProps
|
|
||||||
}: SelectGrammemeProps) {
|
|
||||||
const [options, setOptions] = useState<IGrammemeOption[]>([]);
|
const [options, setOptions] = useState<IGrammemeOption[]>([]);
|
||||||
|
|
||||||
useEffect(
|
useEffect(() => {
|
||||||
() => {
|
|
||||||
const compatible = getCompatibleGrams(
|
const compatible = getCompatibleGrams(
|
||||||
value
|
value.filter(data => Object.values(Grammeme).includes(data.value as Grammeme)).map(data => data.value as Grammeme)
|
||||||
.filter(data => Object.values(Grammeme).includes(data.value as Grammeme))
|
|
||||||
.map(data => data.value as Grammeme)
|
|
||||||
);
|
);
|
||||||
setOptions(SelectorGrammemes.filter(({value}) => compatible.includes(value as Grammeme)));
|
setOptions(SelectorGrammemes.filter(({ value }) => compatible.includes(value as Grammeme)));
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectMulti
|
<SelectMulti
|
||||||
options={options}
|
options={options}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={newValue => setValue([...newValue].sort(compareGrammemeOptions))}
|
onChange={newValue => setValue([...newValue].sort(compareGrammemeOptions))}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>);
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SelectGrammeme;
|
export default SelectGrammeme;
|
|
@ -1,26 +1,21 @@
|
||||||
import Overlay from '@/components/Common/Overlay';
|
import Overlay from '@/components/Common/Overlay';
|
||||||
|
|
||||||
interface SelectedCounterProps {
|
interface SelectedCounterProps {
|
||||||
total: number
|
total: number;
|
||||||
selected: number
|
selected: number;
|
||||||
position?: string
|
position?: string;
|
||||||
hideZero?: boolean
|
hideZero?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectedCounter({
|
function SelectedCounter({ total, selected, hideZero, position = 'top-0 left-0' }: SelectedCounterProps) {
|
||||||
total, selected, hideZero,
|
|
||||||
position = 'top-0 left-0',
|
|
||||||
} : SelectedCounterProps) {
|
|
||||||
if (selected === 0 && hideZero) {
|
if (selected === 0 && hideZero) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Overlay
|
<Overlay position={`px-2 ${position}`} className='select-none whitespace-nowrap small-caps clr-app'>
|
||||||
position={`px-2 ${position}`}
|
Выбор {selected} из {total}
|
||||||
className='select-none whitespace-nowrap small-caps clr-app'
|
</Overlay>
|
||||||
>
|
);
|
||||||
Выбор {selected} из {total}
|
|
||||||
</Overlay>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SelectedCounter;
|
export default SelectedCounter;
|
|
@ -3,21 +3,18 @@ import { IWordForm } from '@/models/language';
|
||||||
import GrammemeBadge from './GrammemeBadge';
|
import GrammemeBadge from './GrammemeBadge';
|
||||||
|
|
||||||
interface WordFormBadgeProps {
|
interface WordFormBadgeProps {
|
||||||
keyPrefix?: string
|
keyPrefix?: string;
|
||||||
form: IWordForm
|
form: IWordForm;
|
||||||
}
|
}
|
||||||
|
|
||||||
function WordFormBadge({ keyPrefix, form }: WordFormBadgeProps) {
|
function WordFormBadge({ keyPrefix, form }: WordFormBadgeProps) {
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-wrap justify-start gap-1 select-none'>
|
<div className='flex flex-wrap justify-start gap-1 select-none'>
|
||||||
{form.grams.map(
|
{form.grams.map(gram => (
|
||||||
(gram) =>
|
<GrammemeBadge key={`${keyPrefix}-${gram}`} grammeme={gram} />
|
||||||
<GrammemeBadge
|
))}
|
||||||
key={`${keyPrefix}-${gram}`}
|
</div>
|
||||||
grammeme={gram}
|
);
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WordFormBadge;
|
export default WordFormBadge;
|
69
rsconcept/frontend/src/components/props.d.ts
vendored
69
rsconcept/frontend/src/components/props.d.ts
vendored
|
@ -2,39 +2,40 @@
|
||||||
import { HTMLMotionProps } from 'framer-motion';
|
import { HTMLMotionProps } from 'framer-motion';
|
||||||
|
|
||||||
export namespace CProps {
|
export namespace CProps {
|
||||||
|
export type Control = {
|
||||||
|
title?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
noBorder?: boolean;
|
||||||
|
noOutline?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type Control = {
|
export type Styling = {
|
||||||
title?: string
|
style?: React.CSSProperties;
|
||||||
disabled?: boolean
|
className?: string;
|
||||||
noBorder?: boolean
|
};
|
||||||
noOutline?: boolean
|
|
||||||
}
|
export type Editor = Control & {
|
||||||
|
label?: string;
|
||||||
export type Styling = {
|
};
|
||||||
style?: React.CSSProperties
|
|
||||||
className?: string
|
export type Colors = {
|
||||||
}
|
colors?: string;
|
||||||
|
};
|
||||||
export type Editor = Control & {
|
|
||||||
label?: string
|
export type Div = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
|
||||||
}
|
export type Button = Omit<
|
||||||
|
React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>,
|
||||||
export type Colors = {
|
'children' | 'type'
|
||||||
colors?: string
|
>;
|
||||||
}
|
export type Label = Omit<
|
||||||
|
React.DetailedHTMLProps<React.LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>,
|
||||||
export type Div = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
|
'children'
|
||||||
export type Button = Omit<
|
>;
|
||||||
React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>,
|
export type TextArea = React.DetailedHTMLProps<
|
||||||
'children' | 'type'
|
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
|
||||||
>;
|
HTMLTextAreaElement
|
||||||
export type Label = Omit<
|
>;
|
||||||
React.DetailedHTMLProps<React.LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>,
|
export type Input = React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
|
||||||
'children'
|
|
||||||
>;
|
export type AnimatedButton = Omit<HTMLMotionProps<'button'>, 'type'>;
|
||||||
export type TextArea = React.DetailedHTMLProps<React.TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>;
|
|
||||||
export type Input = React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
|
|
||||||
|
|
||||||
export type AnimatedButton = Omit<HTMLMotionProps<'button'>, 'type'>;
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -5,32 +5,25 @@ import { createContext, useContext, useState } from 'react';
|
||||||
import { UserAccessMode } from '@/models/miscellaneous';
|
import { UserAccessMode } from '@/models/miscellaneous';
|
||||||
|
|
||||||
interface IAccessModeContext {
|
interface IAccessModeContext {
|
||||||
mode: UserAccessMode
|
mode: UserAccessMode;
|
||||||
setMode: React.Dispatch<React.SetStateAction<UserAccessMode>>
|
setMode: React.Dispatch<React.SetStateAction<UserAccessMode>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AccessContext = createContext<IAccessModeContext | null>(null);
|
const AccessContext = createContext<IAccessModeContext | null>(null);
|
||||||
export const useAccessMode = () => {
|
export const useAccessMode = () => {
|
||||||
const context = useContext(AccessContext);
|
const context = useContext(AccessContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error(
|
throw new Error('useAccessMode has to be used within <AccessModeState.Provider>');
|
||||||
'useAccessMode has to be used within <AccessModeState.Provider>'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
};
|
||||||
|
|
||||||
interface AccessModeStateProps {
|
interface AccessModeStateProps {
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AccessModeState = ({ children }: AccessModeStateProps) => {
|
export const AccessModeState = ({ children }: AccessModeStateProps) => {
|
||||||
const [mode, setMode] = useState<UserAccessMode>(UserAccessMode.READER);
|
const [mode, setMode] = useState<UserAccessMode>(UserAccessMode.READER);
|
||||||
|
|
||||||
return (
|
return <AccessContext.Provider value={{ mode, setMode }}>{children}</AccessContext.Provider>;
|
||||||
<AccessContext.Provider
|
|
||||||
value={{ mode, setMode }}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</AccessContext.Provider>);
|
|
||||||
};
|
};
|
|
@ -10,34 +10,32 @@ import { IUserSignupData } from '@/models/library';
|
||||||
import { IUserProfile } from '@/models/library';
|
import { IUserProfile } from '@/models/library';
|
||||||
import { IUserInfo } from '@/models/library';
|
import { IUserInfo } from '@/models/library';
|
||||||
import { IUserUpdatePassword } from '@/models/library';
|
import { IUserUpdatePassword } from '@/models/library';
|
||||||
import { type DataCallback, getAuth, patchPassword,postLogin, postLogout, postSignup } from '@/utils/backendAPI';
|
import { type DataCallback, getAuth, patchPassword, postLogin, postLogout, postSignup } from '@/utils/backendAPI';
|
||||||
|
|
||||||
import { useUsers } from './UsersContext';
|
import { useUsers } from './UsersContext';
|
||||||
|
|
||||||
interface IAuthContext {
|
interface IAuthContext {
|
||||||
user: ICurrentUser | undefined
|
user: ICurrentUser | undefined;
|
||||||
login: (data: IUserLoginData, callback?: DataCallback) => void
|
login: (data: IUserLoginData, callback?: DataCallback) => void;
|
||||||
logout: (callback?: DataCallback) => void
|
logout: (callback?: DataCallback) => void;
|
||||||
signup: (data: IUserSignupData, callback?: DataCallback<IUserProfile>) => void
|
signup: (data: IUserSignupData, callback?: DataCallback<IUserProfile>) => void;
|
||||||
updatePassword: (data: IUserUpdatePassword, callback?: () => void) => void
|
updatePassword: (data: IUserUpdatePassword, callback?: () => void) => void;
|
||||||
loading: boolean
|
loading: boolean;
|
||||||
error: ErrorData
|
error: ErrorData;
|
||||||
setError: (error: ErrorData) => void
|
setError: (error: ErrorData) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthContext = createContext<IAuthContext | null>(null);
|
const AuthContext = createContext<IAuthContext | null>(null);
|
||||||
export const useAuth = () => {
|
export const useAuth = () => {
|
||||||
const context = useContext(AuthContext);
|
const context = useContext(AuthContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error(
|
throw new Error('useAuth has to be used within <AuthState.Provider>');
|
||||||
'useAuth has to be used within <AuthState.Provider>'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
};
|
||||||
|
|
||||||
interface AuthStateProps {
|
interface AuthStateProps {
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuthState = ({ children }: AuthStateProps) => {
|
export const AuthState = ({ children }: AuthStateProps) => {
|
||||||
|
@ -47,19 +45,21 @@ export const AuthState = ({ children }: AuthStateProps) => {
|
||||||
const [error, setError] = useState<ErrorData>(undefined);
|
const [error, setError] = useState<ErrorData>(undefined);
|
||||||
|
|
||||||
const reload = useCallback(
|
const reload = useCallback(
|
||||||
(callback?: () => void) => {
|
(callback?: () => void) => {
|
||||||
getAuth({
|
getAuth({
|
||||||
onError: () => setUser(undefined),
|
onError: () => setUser(undefined),
|
||||||
onSuccess: currentUser => {
|
onSuccess: currentUser => {
|
||||||
if (currentUser.id) {
|
if (currentUser.id) {
|
||||||
setUser(currentUser);
|
setUser(currentUser);
|
||||||
} else {
|
} else {
|
||||||
setUser(undefined);
|
setUser(undefined);
|
||||||
|
}
|
||||||
|
if (callback) callback();
|
||||||
}
|
}
|
||||||
if (callback) callback();
|
});
|
||||||
}
|
},
|
||||||
});
|
[setUser]
|
||||||
}, [setUser]);
|
);
|
||||||
|
|
||||||
function login(data: IUserLoginData, callback?: DataCallback) {
|
function login(data: IUserLoginData, callback?: DataCallback) {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
|
@ -68,9 +68,10 @@ export const AuthState = ({ children }: AuthStateProps) => {
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setLoading,
|
setLoading: setLoading,
|
||||||
onError: error => setError(error),
|
onError: error => setError(error),
|
||||||
onSuccess: newData => reload(() => {
|
onSuccess: newData =>
|
||||||
if (callback) callback(newData);
|
reload(() => {
|
||||||
})
|
if (callback) callback(newData);
|
||||||
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,9 +79,10 @@ export const AuthState = ({ children }: AuthStateProps) => {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
postLogout({
|
postLogout({
|
||||||
showError: true,
|
showError: true,
|
||||||
onSuccess: newData => reload(() => {
|
onSuccess: newData =>
|
||||||
if (callback) callback(newData);
|
reload(() => {
|
||||||
})
|
if (callback) callback(newData);
|
||||||
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,35 +93,38 @@ export const AuthState = ({ children }: AuthStateProps) => {
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setLoading,
|
setLoading: setLoading,
|
||||||
onError: error => setError(error),
|
onError: error => setError(error),
|
||||||
onSuccess: newData => reload(() => {
|
onSuccess: newData =>
|
||||||
users.push(newData as IUserInfo);
|
reload(() => {
|
||||||
if (callback) callback(newData);
|
users.push(newData as IUserInfo);
|
||||||
})
|
if (callback) callback(newData);
|
||||||
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatePassword = useCallback(
|
const updatePassword = useCallback(
|
||||||
(data: IUserUpdatePassword, callback?: () => void) => {
|
(data: IUserUpdatePassword, callback?: () => void) => {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
patchPassword({
|
patchPassword({
|
||||||
data: data,
|
data: data,
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setLoading,
|
setLoading: setLoading,
|
||||||
onError: error => setError(error),
|
onError: error => setError(error),
|
||||||
onSuccess: () => reload(() => {
|
onSuccess: () =>
|
||||||
if (callback) callback();
|
reload(() => {
|
||||||
})
|
if (callback) callback();
|
||||||
});
|
})
|
||||||
}, [reload]);
|
});
|
||||||
|
},
|
||||||
|
[reload]
|
||||||
|
);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
reload();
|
reload();
|
||||||
}, [reload])
|
}, [reload]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider
|
<AuthContext.Provider value={{ user, login, logout, signup, loading, error, setError, updatePassword }}>
|
||||||
value={{ user, login, logout, signup, loading, error, setError, updatePassword }}
|
{children}
|
||||||
>
|
</AuthContext.Provider>
|
||||||
{children}
|
);
|
||||||
</AuthContext.Provider>);
|
|
||||||
};
|
};
|
|
@ -8,41 +8,47 @@ import { matchLibraryItem } from '@/models/libraryAPI';
|
||||||
import { ILibraryFilter } from '@/models/miscellaneous';
|
import { ILibraryFilter } from '@/models/miscellaneous';
|
||||||
import { IRSForm, IRSFormCreateData, IRSFormData } from '@/models/rsform';
|
import { IRSForm, IRSFormCreateData, IRSFormData } from '@/models/rsform';
|
||||||
import { loadRSFormData } from '@/models/rsformAPI';
|
import { loadRSFormData } from '@/models/rsformAPI';
|
||||||
import { DataCallback, deleteLibraryItem, getLibrary, getRSFormDetails, getTemplates, postCloneLibraryItem, postNewRSForm } from '@/utils/backendAPI';
|
import {
|
||||||
|
DataCallback,
|
||||||
|
deleteLibraryItem,
|
||||||
|
getLibrary,
|
||||||
|
getRSFormDetails,
|
||||||
|
getTemplates,
|
||||||
|
postCloneLibraryItem,
|
||||||
|
postNewRSForm
|
||||||
|
} from '@/utils/backendAPI';
|
||||||
|
|
||||||
import { useAuth } from './AuthContext';
|
import { useAuth } from './AuthContext';
|
||||||
|
|
||||||
interface ILibraryContext {
|
interface ILibraryContext {
|
||||||
items: ILibraryItem[]
|
items: ILibraryItem[];
|
||||||
templates: ILibraryItem[]
|
templates: ILibraryItem[];
|
||||||
loading: boolean
|
loading: boolean;
|
||||||
processing: boolean
|
processing: boolean;
|
||||||
error: ErrorData
|
error: ErrorData;
|
||||||
setError: (error: ErrorData) => void
|
setError: (error: ErrorData) => void;
|
||||||
|
|
||||||
applyFilter: (params: ILibraryFilter) => ILibraryItem[]
|
applyFilter: (params: ILibraryFilter) => ILibraryItem[];
|
||||||
retrieveTemplate: (templateID: number, callback: (schema: IRSForm) => void) => void
|
retrieveTemplate: (templateID: number, callback: (schema: IRSForm) => void) => void;
|
||||||
createItem: (data: IRSFormCreateData, callback?: DataCallback<ILibraryItem>) => void
|
createItem: (data: IRSFormCreateData, callback?: DataCallback<ILibraryItem>) => void;
|
||||||
cloneItem: (target: number, data: IRSFormCreateData, callback: DataCallback<IRSFormData>) => void
|
cloneItem: (target: number, data: IRSFormCreateData, callback: DataCallback<IRSFormData>) => void;
|
||||||
destroyItem: (target: number, callback?: () => void) => void
|
destroyItem: (target: number, callback?: () => void) => void;
|
||||||
|
|
||||||
localUpdateItem: (data: ILibraryItem) => void
|
localUpdateItem: (data: ILibraryItem) => void;
|
||||||
localUpdateTimestamp: (target: number) => void
|
localUpdateTimestamp: (target: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LibraryContext = createContext<ILibraryContext | null>(null)
|
const LibraryContext = createContext<ILibraryContext | null>(null);
|
||||||
export const useLibrary = (): ILibraryContext => {
|
export const useLibrary = (): ILibraryContext => {
|
||||||
const context = useContext(LibraryContext);
|
const context = useContext(LibraryContext);
|
||||||
if (context === null) {
|
if (context === null) {
|
||||||
throw new Error(
|
throw new Error('useLibrary has to be used within <LibraryState.Provider>');
|
||||||
'useLibrary has to be used within <LibraryState.Provider>'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
};
|
||||||
|
|
||||||
interface LibraryStateProps {
|
interface LibraryStateProps {
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LibraryState = ({ children }: LibraryStateProps) => {
|
export const LibraryState = ({ children }: LibraryStateProps) => {
|
||||||
|
@ -56,51 +62,54 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
|
||||||
const [cachedTemplates, setCachedTemplates] = useState<IRSForm[]>([]);
|
const [cachedTemplates, setCachedTemplates] = useState<IRSForm[]>([]);
|
||||||
|
|
||||||
const applyFilter = useCallback(
|
const applyFilter = useCallback(
|
||||||
(params: ILibraryFilter) => {
|
(params: ILibraryFilter) => {
|
||||||
let result = items;
|
let result = items;
|
||||||
if (params.is_owned) {
|
if (params.is_owned) {
|
||||||
result = result.filter(item => item.owner === user?.id);
|
result = result.filter(item => item.owner === user?.id);
|
||||||
}
|
}
|
||||||
if (params.is_common !== undefined) {
|
if (params.is_common !== undefined) {
|
||||||
result = result.filter(item => item.is_common === params.is_common);
|
result = result.filter(item => item.is_common === params.is_common);
|
||||||
}
|
}
|
||||||
if (params.is_canonical !== undefined) {
|
if (params.is_canonical !== undefined) {
|
||||||
result = result.filter(item => item.is_canonical === params.is_canonical);
|
result = result.filter(item => item.is_canonical === params.is_canonical);
|
||||||
}
|
}
|
||||||
if (params.is_subscribed !== undefined) {
|
if (params.is_subscribed !== undefined) {
|
||||||
result = result.filter(item => user?.subscriptions.includes(item.id));
|
result = result.filter(item => user?.subscriptions.includes(item.id));
|
||||||
}
|
}
|
||||||
if (params.is_personal !== undefined) {
|
if (params.is_personal !== undefined) {
|
||||||
result = result.filter(item => user?.subscriptions.includes(item.id) || item.owner === user?.id);
|
result = result.filter(item => user?.subscriptions.includes(item.id) || item.owner === user?.id);
|
||||||
}
|
}
|
||||||
if (params.query) {
|
if (params.query) {
|
||||||
result = result.filter(item => matchLibraryItem(item, params.query!));
|
result = result.filter(item => matchLibraryItem(item, params.query!));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}, [items, user]);
|
},
|
||||||
|
[items, user]
|
||||||
|
);
|
||||||
|
|
||||||
const retrieveTemplate = useCallback(
|
const retrieveTemplate = useCallback(
|
||||||
(templateID: number, callback: (schema: IRSForm) => void) => {
|
(templateID: number, callback: (schema: IRSForm) => void) => {
|
||||||
const cached = cachedTemplates.find(schema => schema.id == templateID);
|
const cached = cachedTemplates.find(schema => schema.id == templateID);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
callback(cached);
|
callback(cached);
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
setError(undefined);
|
|
||||||
getRSFormDetails(String(templateID), {
|
|
||||||
showError: true,
|
|
||||||
setLoading: setLoading,
|
|
||||||
onError: error => setError(error),
|
|
||||||
onSuccess: data => {
|
|
||||||
const schema = loadRSFormData(data);
|
|
||||||
setCachedTemplates(prev => ([...prev, schema]));
|
|
||||||
callback(schema);
|
|
||||||
}
|
}
|
||||||
});
|
setError(undefined);
|
||||||
}, [cachedTemplates]);
|
getRSFormDetails(String(templateID), {
|
||||||
|
showError: true,
|
||||||
|
setLoading: setLoading,
|
||||||
|
onError: error => setError(error),
|
||||||
|
onSuccess: data => {
|
||||||
|
const schema = loadRSFormData(data);
|
||||||
|
setCachedTemplates(prev => [...prev, schema]);
|
||||||
|
callback(schema);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[cachedTemplates]
|
||||||
|
);
|
||||||
|
|
||||||
const reload = useCallback(
|
const reload = useCallback((callback?: () => void) => {
|
||||||
(callback?: () => void) => {
|
|
||||||
setItems([]);
|
setItems([]);
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
getLibrary({
|
getLibrary({
|
||||||
|
@ -125,78 +134,107 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
|
||||||
}, [reload, user]);
|
}, [reload, user]);
|
||||||
|
|
||||||
const localUpdateItem = useCallback(
|
const localUpdateItem = useCallback(
|
||||||
(data: ILibraryItem) => {
|
(data: ILibraryItem) => {
|
||||||
const libraryItem = items.find(item => item.id === data.id);
|
const libraryItem = items.find(item => item.id === data.id);
|
||||||
if (libraryItem) Object.assign(libraryItem, data);
|
if (libraryItem) Object.assign(libraryItem, data);
|
||||||
}, [items]);
|
},
|
||||||
|
[items]
|
||||||
|
);
|
||||||
|
|
||||||
const localUpdateTimestamp = useCallback(
|
const localUpdateTimestamp = useCallback(
|
||||||
(target: number) => {
|
(target: number) => {
|
||||||
const libraryItem = items.find(item => item.id === target);
|
const libraryItem = items.find(item => item.id === target);
|
||||||
if (libraryItem) {
|
if (libraryItem) {
|
||||||
libraryItem.time_update = Date();
|
libraryItem.time_update = Date();
|
||||||
}
|
}
|
||||||
}, [items]);
|
},
|
||||||
|
[items]
|
||||||
|
);
|
||||||
|
|
||||||
const createItem = useCallback(
|
const createItem = useCallback(
|
||||||
(data: IRSFormCreateData, callback?: DataCallback<ILibraryItem>) => {
|
(data: IRSFormCreateData, callback?: DataCallback<ILibraryItem>) => {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
postNewRSForm({
|
postNewRSForm({
|
||||||
data: data,
|
data: data,
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: error => setError(error),
|
onError: error => setError(error),
|
||||||
onSuccess: newSchema => reload(() => {
|
onSuccess: newSchema =>
|
||||||
if (user && !user.subscriptions.includes(newSchema.id)) {
|
reload(() => {
|
||||||
user.subscriptions.push(newSchema.id);
|
if (user && !user.subscriptions.includes(newSchema.id)) {
|
||||||
}
|
user.subscriptions.push(newSchema.id);
|
||||||
if (callback) callback(newSchema);
|
}
|
||||||
})
|
if (callback) callback(newSchema);
|
||||||
});
|
})
|
||||||
}, [reload, user]);
|
});
|
||||||
|
},
|
||||||
|
[reload, user]
|
||||||
|
);
|
||||||
|
|
||||||
const destroyItem = useCallback(
|
const destroyItem = useCallback(
|
||||||
(target: number, callback?: () => void) => {
|
(target: number, callback?: () => void) => {
|
||||||
setError(undefined)
|
setError(undefined);
|
||||||
deleteLibraryItem(String(target), {
|
deleteLibraryItem(String(target), {
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: error => setError(error),
|
onError: error => setError(error),
|
||||||
onSuccess: () => reload(() => {
|
onSuccess: () =>
|
||||||
if (user && user.subscriptions.includes(target)) {
|
reload(() => {
|
||||||
user.subscriptions.splice(user.subscriptions.findIndex(item => item === target), 1);
|
if (user && user.subscriptions.includes(target)) {
|
||||||
}
|
user.subscriptions.splice(
|
||||||
if (callback) callback();
|
user.subscriptions.findIndex(item => item === target),
|
||||||
})
|
1
|
||||||
});
|
);
|
||||||
}, [setError, reload, user]);
|
}
|
||||||
|
if (callback) callback();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setError, reload, user]
|
||||||
|
);
|
||||||
|
|
||||||
const cloneItem = useCallback(
|
const cloneItem = useCallback(
|
||||||
(target: number, data: IRSFormCreateData, callback: DataCallback<IRSFormData>) => {
|
(target: number, data: IRSFormCreateData, callback: DataCallback<IRSFormData>) => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setError(undefined)
|
setError(undefined);
|
||||||
postCloneLibraryItem(String(target), {
|
postCloneLibraryItem(String(target), {
|
||||||
data: data,
|
data: data,
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: error => setError(error),
|
onError: error => setError(error),
|
||||||
onSuccess: newSchema => reload(() => {
|
onSuccess: newSchema =>
|
||||||
if (user && !user.subscriptions.includes(newSchema.id)) {
|
reload(() => {
|
||||||
user.subscriptions.push(newSchema.id);
|
if (user && !user.subscriptions.includes(newSchema.id)) {
|
||||||
}
|
user.subscriptions.push(newSchema.id);
|
||||||
if (callback) callback(newSchema);
|
}
|
||||||
})
|
if (callback) callback(newSchema);
|
||||||
});
|
})
|
||||||
}, [reload, setError, user]);
|
});
|
||||||
|
},
|
||||||
|
[reload, setError, user]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LibraryContext.Provider value={{
|
<LibraryContext.Provider
|
||||||
items, templates, loading, processing, error, setError,
|
value={{
|
||||||
applyFilter, createItem, cloneItem, destroyItem, retrieveTemplate,
|
items,
|
||||||
localUpdateItem, localUpdateTimestamp
|
templates,
|
||||||
}}>
|
loading,
|
||||||
{children}
|
processing,
|
||||||
</LibraryContext.Provider>);
|
error,
|
||||||
}
|
setError,
|
||||||
|
applyFilter,
|
||||||
|
createItem,
|
||||||
|
cloneItem,
|
||||||
|
destroyItem,
|
||||||
|
retrieveTemplate,
|
||||||
|
localUpdateItem,
|
||||||
|
localUpdateTimestamp
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</LibraryContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user