R: Remove unused useMemo and useCallback
Some checks failed
Frontend CI / build (22.x) (push) Has been cancelled

This commit is contained in:
Ivan 2024-12-13 21:31:09 +03:00
parent 254b6a64d5
commit 609752a9d6
82 changed files with 1736 additions and 2416 deletions

View File

@ -6,7 +6,7 @@ import { createTheme } from '@uiw/codemirror-themes';
import CodeMirror, { BasicSetupOptions, ReactCodeMirrorProps, ReactCodeMirrorRef } from '@uiw/react-codemirror'; import CodeMirror, { BasicSetupOptions, ReactCodeMirrorProps, ReactCodeMirrorRef } from '@uiw/react-codemirror';
import clsx from 'clsx'; import clsx from 'clsx';
import { EditorView } from 'codemirror'; import { EditorView } from 'codemirror';
import { forwardRef, useCallback, useMemo, useRef } from 'react'; import { forwardRef, useRef } from 'react';
import Label from '@/components/ui/Label'; import Label from '@/components/ui/Label';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
@ -66,102 +66,92 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
const { darkMode, colors } = useConceptOptions(); const { darkMode, colors } = useConceptOptions();
const internalRef = useRef<ReactCodeMirrorRef>(null); const internalRef = useRef<ReactCodeMirrorRef>(null);
const thisRef = useMemo(() => (!ref || typeof ref === 'function' ? internalRef : ref), [internalRef, ref]); const thisRef = !ref || typeof ref === 'function' ? internalRef : ref;
const cursor = useMemo(() => (!disabled ? 'cursor-text' : 'cursor-default'), [disabled]); const cursor = !disabled ? 'cursor-text' : 'cursor-default';
const customTheme: Extension = useMemo( const customTheme: Extension = 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, caret: colors.fgDefault
selection: colors.bgHover, },
caret: colors.fgDefault styles: [
}, { tag: tags.name, color: colors.fgPurple, cursor: schema ? 'default' : cursor }, // GlobalID
styles: [ { tag: tags.variableName, color: colors.fgGreen }, // LocalID
{ tag: tags.name, color: colors.fgPurple, cursor: schema ? 'default' : cursor }, // 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: '400' }, // R | I | D
{ tag: tags.literal, color: colors.fgBlue }, // literals { tag: tags.unit, fontSize: '0.75rem' }, // indices
{ tag: tags.controlKeyword, fontWeight: '400' }, // R | I | D { tag: tags.brace, color: colors.fgPurple, fontWeight: '600' } // braces (curly brackets)
{ tag: tags.unit, fontSize: '0.75rem' }, // indices ]
{ tag: tags.brace, color: colors.fgPurple, fontWeight: '600' } // braces (curly brackets) });
]
}),
[disabled, colors, darkMode, schema, cursor]
);
const editorExtensions = useMemo( const editorExtensions = [
() => [ EditorView.lineWrapping,
EditorView.lineWrapping, RSLanguage,
RSLanguage, ccBracketMatching(darkMode),
ccBracketMatching(darkMode), ...(!schema || !onOpenEdit ? [] : [rsNavigation(schema, onOpenEdit)]),
...(!schema || !onOpenEdit ? [] : [rsNavigation(schema, onOpenEdit)]), ...(noTooltip || !schema ? [] : [rsHoverTooltip(schema, onOpenEdit !== undefined)])
...(noTooltip || !schema ? [] : [rsHoverTooltip(schema, onOpenEdit !== undefined)]) ];
],
[darkMode, schema, noTooltip, onOpenEdit]
);
const handleInput = useCallback( function handleInput(event: React.KeyboardEvent<HTMLDivElement>) {
(event: React.KeyboardEvent<HTMLDivElement>) => { if (!thisRef.current) {
if (!thisRef.current) { return;
}
const text = new RSTextWrapper(thisRef.current as Required<ReactCodeMirrorRef>);
if ((event.ctrlKey || event.metaKey) && event.code === 'Space') {
const selection = text.getSelection();
if (!selection.empty || !schema) {
return; return;
} }
const text = new RSTextWrapper(thisRef.current as Required<ReactCodeMirrorRef>); const wordRange = text.getWord(selection.from);
if ((event.ctrlKey || event.metaKey) && event.code === 'Space') { if (wordRange) {
const selection = text.getSelection(); const word = text.getText(wordRange.from, wordRange.to);
if (!selection.empty || !schema) { if (word.length > 2 && (word.startsWith('Pr') || word.startsWith('pr'))) {
text.setSelection(wordRange.from, wordRange.from + 2);
if (word.startsWith('Pr')) {
text.replaceWith('pr');
} else {
text.replaceWith('Pr');
}
event.preventDefault();
event.stopPropagation();
return; return;
} }
const wordRange = text.getWord(selection.from); }
if (wordRange) {
const word = text.getText(wordRange.from, wordRange.to);
if (word.length > 2 && (word.startsWith('Pr') || word.startsWith('pr'))) {
text.setSelection(wordRange.from, wordRange.from + 2);
if (word.startsWith('Pr')) {
text.replaceWith('pr');
} else {
text.replaceWith('Pr');
}
event.preventDefault();
event.stopPropagation();
return;
}
}
const hint = text.getText(selection.from - 1, selection.from); const hint = text.getText(selection.from - 1, selection.from);
const type = guessCstType(hint); const type = guessCstType(hint);
if (hint === getCstTypePrefix(type)) { if (hint === getCstTypePrefix(type)) {
text.setSelection(selection.from - 1, selection.from); text.setSelection(selection.from - 1, selection.from);
} }
const takenAliases = [...extractGlobals(thisRef.current.view?.state.doc.toString() ?? '')]; const takenAliases = [...extractGlobals(thisRef.current.view?.state.doc.toString() ?? '')];
const newAlias = generateAlias(type, schema, takenAliases); const newAlias = generateAlias(type, schema, takenAliases);
text.replaceWith(newAlias); text.replaceWith(newAlias);
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
} else if (event.altKey) { } else if (event.altKey) {
if (text.processAltKey(event.code, event.shiftKey)) { if (text.processAltKey(event.code, event.shiftKey)) {
event.preventDefault();
event.stopPropagation();
}
} else if (!(event.ctrlKey || event.metaKey)) {
const newSymbol = getSymbolSubstitute(event.code, event.shiftKey);
if (newSymbol) {
text.replaceWith(newSymbol);
event.preventDefault();
event.stopPropagation();
}
} else if (event.code === 'KeyQ' && onAnalyze) {
onAnalyze();
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
} }
}, } else if (!(event.ctrlKey || event.metaKey)) {
[thisRef, onAnalyze, schema] const newSymbol = getSymbolSubstitute(event.code, event.shiftKey);
); if (newSymbol) {
text.replaceWith(newSymbol);
event.preventDefault();
event.stopPropagation();
}
} else if (event.code === 'KeyQ' && onAnalyze) {
onAnalyze();
event.preventDefault();
event.stopPropagation();
}
}
return ( return (
<div className={clsx('flex flex-col gap-2', className, cursor)} style={style}> <div className={clsx('flex flex-col gap-2', className, cursor)} style={style}>

View File

@ -6,7 +6,7 @@ import { createTheme } from '@uiw/codemirror-themes';
import CodeMirror, { BasicSetupOptions, ReactCodeMirrorProps, ReactCodeMirrorRef } from '@uiw/react-codemirror'; import CodeMirror, { BasicSetupOptions, ReactCodeMirrorProps, ReactCodeMirrorRef } from '@uiw/react-codemirror';
import clsx from 'clsx'; import clsx from 'clsx';
import { EditorView } from 'codemirror'; import { EditorView } from 'codemirror';
import { forwardRef, useCallback, useMemo, useRef, useState } from 'react'; import { forwardRef, useRef, useState } from 'react';
import Label from '@/components/ui/Label'; import Label from '@/components/ui/Label';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
@ -103,39 +103,32 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
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 = !ref || typeof ref === 'function' ? internalRef : ref;
const cursor = useMemo(() => (!disabled ? 'cursor-text' : 'cursor-default'), [disabled]); const cursor = !disabled ? 'cursor-text' : 'cursor-default';
const customTheme: Extension = useMemo( const customTheme: Extension = 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, caret: colors.fgDefault
selection: colors.bgHover, },
caret: colors.fgDefault 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]
);
const editorExtensions = useMemo( const editorExtensions = [
() => [ EditorView.lineWrapping,
EditorView.lineWrapping, EditorView.contentAttributes.of({ spellcheck: 'true' }),
EditorView.contentAttributes.of({ spellcheck: 'true' }), NaturalLanguage,
NaturalLanguage, ...(!schema || !onOpenEdit ? [] : [refsNavigation(schema, onOpenEdit)]),
...(!schema || !onOpenEdit ? [] : [refsNavigation(schema, onOpenEdit)]), ...(schema ? [refsHoverTooltip(schema, colors, onOpenEdit !== undefined)] : [])
...(schema ? [refsHoverTooltip(schema, colors, onOpenEdit !== undefined)] : []) ];
],
[schema, colors, onOpenEdit]
);
function handleChange(newValue: string) { function handleChange(newValue: string) {
if (onChange) onChange(newValue); if (onChange) onChange(newValue);
@ -151,58 +144,52 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
if (onBlur) onBlur(event); if (onBlur) onBlur(event);
} }
const handleInput = useCallback( function handleInput(event: React.KeyboardEvent<HTMLDivElement>) {
(event: React.KeyboardEvent<HTMLDivElement>) => { if (!thisRef.current?.view) {
if (!thisRef.current?.view) { event.preventDefault();
event.preventDefault(); event.stopPropagation();
event.stopPropagation(); return;
return; }
} if ((event.ctrlKey || event.metaKey) && event.code === 'Space') {
if ((event.ctrlKey || event.metaKey) && event.code === 'Space') { event.preventDefault();
event.preventDefault(); event.stopPropagation();
event.stopPropagation();
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();
const mainNodes = wrap
.getAllNodes([RefEntity])
.filter(node => node.from >= selection.to || node.to <= selection.from);
setMainRefs(mainNodes.map(node => wrap.getText(node.from, node.to)));
setBasePosition(mainNodes.filter(node => node.to <= selection.from).length);
setShowEditor(true);
}
},
[thisRef]
);
const handleInputReference = useCallback(
(referenceText: string) => {
if (!thisRef.current?.view) {
return;
}
thisRef.current.view.focus();
const wrap = new CodeMirrorWrapper(thisRef.current as Required<ReactCodeMirrorRef>); const wrap = new CodeMirrorWrapper(thisRef.current as Required<ReactCodeMirrorRef>);
wrap.replaceWith(referenceText); wrap.fixSelection(ReferenceTokens);
}, const nodes = wrap.getEnvelopingNodes(ReferenceTokens);
[thisRef] 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 hideEditReference = useCallback(() => { const selection = wrap.getSelection();
const mainNodes = wrap
.getAllNodes([RefEntity])
.filter(node => node.from >= selection.to || node.to <= selection.from);
setMainRefs(mainNodes.map(node => wrap.getText(node.from, node.to)));
setBasePosition(mainNodes.filter(node => node.to <= selection.from).length);
setShowEditor(true);
}
}
function handleInputReference(referenceText: string) {
if (!thisRef.current?.view) {
return;
}
thisRef.current.view.focus();
const wrap = new CodeMirrorWrapper(thisRef.current as Required<ReactCodeMirrorRef>);
wrap.replaceWith(referenceText);
}
function hideEditReference() {
setShowEditor(false); setShowEditor(false);
setTimeout(() => thisRef.current?.view?.focus(), PARAMETER.refreshTimeout); setTimeout(() => thisRef.current?.view?.focus(), PARAMETER.refreshTimeout);
}, [thisRef]); }
return ( return (
<div className={clsx('flex flex-col gap-2', cursor)}> <div className={clsx('flex flex-col gap-2', cursor)}>

View File

@ -1,7 +1,6 @@
'use client'; 'use client';
import { createColumnHelper } from '@tanstack/react-table'; import { createColumnHelper } from '@tanstack/react-table';
import { useMemo } from 'react';
import Tooltip from '@/components/ui/Tooltip'; import Tooltip from '@/components/ui/Tooltip';
import { OssNodeInternal } from '@/models/miscellaneous'; import { OssNodeInternal } from '@/models/miscellaneous';
@ -19,47 +18,30 @@ interface TooltipOperationProps {
const columnHelper = createColumnHelper<ICstSubstituteEx>(); const columnHelper = createColumnHelper<ICstSubstituteEx>();
function TooltipOperation({ node, anchor }: TooltipOperationProps) { function TooltipOperation({ node, anchor }: TooltipOperationProps) {
const columns = useMemo( const columns = [
() => [ columnHelper.accessor('substitution_term', {
columnHelper.accessor('substitution_term', { id: 'substitution_term',
id: 'substitution_term', size: 200
size: 200 }),
}), columnHelper.accessor('substitution_alias', {
columnHelper.accessor('substitution_alias', { id: 'substitution_alias',
id: 'substitution_alias', size: 50
size: 50 }),
}), columnHelper.display({
columnHelper.display({ id: 'status',
id: 'status', header: '',
header: '', size: 40,
size: 40, cell: () => <IconPageRight size='1.2rem' />
cell: () => <IconPageRight size='1.2rem' /> }),
}), columnHelper.accessor('original_alias', {
columnHelper.accessor('original_alias', { id: 'original_alias',
id: 'original_alias', size: 50
size: 50 }),
}), columnHelper.accessor('original_term', {
columnHelper.accessor('original_term', { id: 'original_term',
id: 'original_term', size: 200
size: 200 })
}) ];
],
[]
);
const table = useMemo(
() => (
<DataTable
dense
noHeader
noFooter
className='text-sm border select-none mb-2'
data={node.data.operation.substitutions}
columns={columns}
/>
),
[columns, node]
);
return ( return (
<Tooltip layer='z-modalTooltip' anchorSelect={anchor} className='max-w-[35rem] max-h-[40rem] dense'> <Tooltip layer='z-modalTooltip' anchorSelect={anchor} className='max-w-[35rem] max-h-[40rem] dense'>
@ -90,7 +72,14 @@ function TooltipOperation({ node, anchor }: TooltipOperationProps) {
</p> </p>
) : null} ) : null}
{node.data.operation.substitutions.length > 0 ? ( {node.data.operation.substitutions.length > 0 ? (
table <DataTable
dense
noHeader
noFooter
className='text-sm border select-none mb-2'
data={node.data.operation.substitutions}
columns={columns}
/>
) : node.data.operation.operation_type !== OperationType.INPUT ? ( ) : node.data.operation.operation_type !== OperationType.INPUT ? (
<p> <p>
<b>Отождествления:</b> Отсутствуют <b>Отождествления:</b> Отсутствуют

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/ui/DataTable'; import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/ui/DataTable';
import SearchBar from '@/components/ui/SearchBar'; import SearchBar from '@/components/ui/SearchBar';
@ -64,33 +64,27 @@ function PickConstituenta({
} }
}, [data, filterText, matchFunc, onBeginFilter]); }, [data, filterText, matchFunc, onBeginFilter]);
const columns = useMemo( const columns = [
() => [ columnHelper.accessor('alias', {
columnHelper.accessor('alias', { id: 'alias',
id: 'alias', size: 65,
size: 65, minSize: 65,
minSize: 65, maxSize: 65,
maxSize: 65, cell: props => <BadgeConstituenta theme={colors} value={props.row.original} prefixID={prefixID} />
cell: props => <BadgeConstituenta theme={colors} value={props.row.original} prefixID={prefixID} /> }),
}), columnHelper.accessor(cst => describeFunc(cst), {
columnHelper.accessor(cst => describeFunc(cst), { id: 'description',
id: 'description', size: 1000,
size: 1000, minSize: 1000
minSize: 1000 })
}) ];
],
[colors, prefixID, describeFunc]
);
const conditionalRowStyles = useMemo( const conditionalRowStyles: IConditionalStyle<IConstituenta>[] = [
(): IConditionalStyle<IConstituenta>[] => [ {
{ when: (cst: IConstituenta) => cst.id === value?.id,
when: (cst: IConstituenta) => cst.id === value?.id, style: { backgroundColor: colors.bgSelected }
style: { backgroundColor: colors.bgSelected } }
} ];
],
[value, colors]
);
return ( return (
<div className={clsx('border divide-y', className)} {...restProps}> <div className={clsx('border divide-y', className)} {...restProps}>

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import DataTable, { createColumnHelper, RowSelectionState } from '@/components/ui/DataTable'; import DataTable, { createColumnHelper, RowSelectionState } from '@/components/ui/DataTable';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
@ -49,7 +49,8 @@ function PickMultiConstituenta({
const [filtered, setFiltered] = useState<IConstituenta[]>(data); const [filtered, setFiltered] = useState<IConstituenta[]>(data);
const [filterText, setFilterText] = useState(''); const [filterText, setFilterText] = useState('');
const foldedGraph = useMemo(() => { // TODO: extract graph fold logic to separate function
const foldedGraph = (() => {
if (data.length === schema.items.length) { if (data.length === schema.items.length) {
return schema.graph; return schema.graph;
} }
@ -66,7 +67,7 @@ function PickMultiConstituenta({
newGraph.foldNode(item.id); newGraph.foldNode(item.id);
}); });
return newGraph; return newGraph;
}, [data, schema.graph, schema.items]); })();
useEffect(() => { useEffect(() => {
if (filtered.length === 0) { if (filtered.length === 0) {
@ -105,22 +106,19 @@ function PickMultiConstituenta({
} }
} }
const columns = useMemo( const columns = [
() => [ columnHelper.accessor('alias', {
columnHelper.accessor('alias', { id: 'alias',
id: 'alias', header: () => <span className='pl-3'>Имя</span>,
header: () => <span className='pl-3'>Имя</span>, size: 65,
size: 65, cell: props => <BadgeConstituenta theme={colors} value={props.row.original} prefixID={prefixID} />
cell: props => <BadgeConstituenta theme={colors} value={props.row.original} prefixID={prefixID} /> }),
}), columnHelper.accessor(cst => describeConstituenta(cst), {
columnHelper.accessor(cst => describeConstituenta(cst), { id: 'description',
id: 'description', size: 1000,
size: 1000, header: 'Описание'
header: 'Описание' })
}) ];
],
[colors, prefixID]
);
return ( return (
<div className={clsx(noBorder ? '' : 'border', className)} {...restProps}> <div className={clsx(noBorder ? '' : 'border', className)} {...restProps}>

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useMemo, useState } from 'react'; import { useState } from 'react';
import { IconMoveDown, IconMoveUp, IconRemove } from '@/components/Icons'; import { IconMoveDown, IconMoveUp, IconRemove } from '@/components/Icons';
import SelectOperation from '@/components/select/SelectOperation'; import SelectOperation from '@/components/select/SelectOperation';
@ -23,108 +23,92 @@ interface PickMultiOperationProps extends CProps.Styling {
const columnHelper = createColumnHelper<IOperation>(); const columnHelper = createColumnHelper<IOperation>();
function PickMultiOperation({ rows, items, selected, setSelected, className, ...restProps }: PickMultiOperationProps) { function PickMultiOperation({ rows, items, selected, setSelected, className, ...restProps }: PickMultiOperationProps) {
const selectedItems = useMemo( const selectedItems = selected.map(itemID => items.find(item => item.id === itemID)!);
() => selected.map(itemID => items.find(item => item.id === itemID)!), const nonSelectedItems = items.filter(item => !selected.includes(item.id));
[items, selected]
);
const nonSelectedItems = useMemo(() => items.filter(item => !selected.includes(item.id)), [items, selected]);
const [lastSelected, setLastSelected] = useState<IOperation | undefined>(undefined); const [lastSelected, setLastSelected] = useState<IOperation | undefined>(undefined);
const handleDelete = useCallback( function handleDelete(operation: OperationID) {
(operation: OperationID) => setSelected(prev => prev.filter(item => item !== operation)), setSelected(prev => prev.filter(item => item !== operation));
[setSelected] }
);
const handleSelect = useCallback( function handleSelect(operation?: IOperation) {
(operation?: IOperation) => { if (operation) {
if (operation) { setLastSelected(operation);
setLastSelected(operation); setSelected(prev => [...prev, operation.id]);
setSelected(prev => [...prev, operation.id]); setTimeout(() => setLastSelected(undefined), 1000);
setTimeout(() => setLastSelected(undefined), 1000); }
} }
},
[setSelected]
);
const handleMoveUp = useCallback( function handleMoveUp(operation: OperationID) {
(operation: OperationID) => { const index = selected.indexOf(operation);
const index = selected.indexOf(operation); if (index > 0) {
if (index > 0) { setSelected(prev => {
setSelected(prev => { const newSelected = [...prev];
const newSelected = [...prev]; newSelected[index] = newSelected[index - 1];
newSelected[index] = newSelected[index - 1]; newSelected[index - 1] = operation;
newSelected[index - 1] = operation; return newSelected;
return newSelected; });
}); }
} }
},
[setSelected, selected]
);
const handleMoveDown = useCallback( function handleMoveDown(operation: OperationID) {
(operation: OperationID) => { const index = selected.indexOf(operation);
const index = selected.indexOf(operation); if (index < selected.length - 1) {
if (index < selected.length - 1) { setSelected(prev => {
setSelected(prev => { const newSelected = [...prev];
const newSelected = [...prev]; newSelected[index] = newSelected[index + 1];
newSelected[index] = newSelected[index + 1]; newSelected[index + 1] = operation;
newSelected[index + 1] = operation; return newSelected;
return newSelected; });
}); }
} }
},
[setSelected, selected]
);
const columns = useMemo( const columns = [
() => [ columnHelper.accessor('alias', {
columnHelper.accessor('alias', { id: 'alias',
id: 'alias', header: 'Шифр',
header: 'Шифр', size: 300,
size: 300, minSize: 150,
minSize: 150, maxSize: 300
maxSize: 300 }),
}), columnHelper.accessor('title', {
columnHelper.accessor('title', { id: 'title',
id: 'title', header: 'Название',
header: 'Название', size: 1200,
size: 1200, minSize: 300,
minSize: 300, maxSize: 1200,
maxSize: 1200, cell: props => <div className='text-ellipsis'>{props.getValue()}</div>
cell: props => <div className='text-ellipsis'>{props.getValue()}</div> }),
}), columnHelper.display({
columnHelper.display({ id: 'actions',
id: 'actions', size: 0,
size: 0, cell: props => (
cell: props => ( <div className='flex gap-1 w-fit'>
<div className='flex gap-1 w-fit'> <MiniButton
<MiniButton noHover
noHover className='px-0'
className='px-0' title='Удалить'
title='Удалить' icon={<IconRemove size='1rem' className='icon-red' />}
icon={<IconRemove size='1rem' className='icon-red' />} onClick={() => handleDelete(props.row.original.id)}
onClick={() => handleDelete(props.row.original.id)} />
/> <MiniButton
<MiniButton noHover
noHover className='px-0'
className='px-0' title='Выше'
title='Выше' icon={<IconMoveUp size='1rem' className='icon-primary' />}
icon={<IconMoveUp size='1rem' className='icon-primary' />} onClick={() => handleMoveUp(props.row.original.id)}
onClick={() => handleMoveUp(props.row.original.id)} />
/> <MiniButton
<MiniButton noHover
noHover title='Ниже'
title='Ниже' className='px-0'
className='px-0' icon={<IconMoveDown size='1rem' className='icon-primary' />}
icon={<IconMoveDown size='1rem' className='icon-primary' />} onClick={() => handleMoveDown(props.row.original.id)}
onClick={() => handleMoveDown(props.row.original.id)} />
/> </div>
</div> )
) })
}) ];
],
[handleDelete, handleMoveUp, handleMoveDown]
);
return ( return (
<div <div

View File

@ -1,5 +1,5 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/ui/DataTable'; import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/ui/DataTable';
@ -51,10 +51,7 @@ function PickSchema({
const [filterText, setFilterText] = useState(initialFilter); const [filterText, setFilterText] = useState(initialFilter);
const [filterLocation, setFilterLocation] = useState(''); const [filterLocation, setFilterLocation] = useState('');
const [filtered, setFiltered] = useState<ILibraryItem[]>([]); const [filtered, setFiltered] = useState<ILibraryItem[]>([]);
const baseFiltered = useMemo( const baseFiltered = items.filter(item => item.item_type === itemType && (!baseFilter || baseFilter(item)));
() => items.filter(item => item.item_type === itemType && (!baseFilter || baseFilter(item))),
[items, itemType, baseFilter]
);
const locationMenu = useDropdown(); const locationMenu = useDropdown();
@ -68,59 +65,50 @@ function PickSchema({
setFiltered(newFiltered); setFiltered(newFiltered);
}, [filterText, filterLocation, baseFiltered]); }, [filterText, filterLocation, baseFiltered]);
const columns = useMemo( const columns = [
() => [ columnHelper.accessor('alias', {
columnHelper.accessor('alias', { id: 'alias',
id: 'alias', header: 'Шифр',
header: 'Шифр', size: 150,
size: 150, minSize: 80,
minSize: 80, maxSize: 150
maxSize: 150 }),
}), columnHelper.accessor('title', {
columnHelper.accessor('title', { id: 'title',
id: 'title', header: 'Название',
header: 'Название', size: 1200,
size: 1200, minSize: 200,
minSize: 200, maxSize: 1200,
maxSize: 1200, cell: props => <div className='text-ellipsis'>{props.getValue()}</div>
cell: props => <div className='text-ellipsis'>{props.getValue()}</div> }),
}), columnHelper.accessor('time_update', {
columnHelper.accessor('time_update', { id: 'time_update',
id: 'time_update', header: 'Дата',
header: 'Дата', cell: props => (
cell: props => ( <div className='whitespace-nowrap'>
<div className='whitespace-nowrap'> {new Date(props.getValue()).toLocaleString(intl.locale, {
{new Date(props.getValue()).toLocaleString(intl.locale, { year: '2-digit',
year: '2-digit', month: '2-digit',
month: '2-digit', day: '2-digit'
day: '2-digit' })}
})} </div>
</div> )
) })
}) ];
],
[intl]
);
const conditionalRowStyles = useMemo( const conditionalRowStyles: IConditionalStyle<ILibraryItem>[] = [
(): IConditionalStyle<ILibraryItem>[] => [ {
{ when: (item: ILibraryItem) => item.id === value,
when: (item: ILibraryItem) => item.id === value, style: { backgroundColor: colors.bgSelected }
style: { backgroundColor: colors.bgSelected } }
} ];
],
[value, colors]
);
const handleLocationClick = useCallback( function handleLocationClick(event: CProps.EventMouse, newValue: string) {
(event: CProps.EventMouse, newValue: string) => { event.preventDefault();
event.preventDefault(); event.stopPropagation();
event.stopPropagation(); locationMenu.hide();
locationMenu.hide(); setFilterLocation(newValue);
setFilterLocation(newValue); }
},
[locationMenu]
);
return ( return (
<div className={clsx('border divide-y', className)} {...restProps}> <div className={clsx('border divide-y', className)} {...restProps}>

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useMemo, useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import BadgeConstituenta from '@/components/info/BadgeConstituenta'; import BadgeConstituenta from '@/components/info/BadgeConstituenta';
@ -62,59 +62,47 @@ function PickSubstitutions({
const toggleDelete = () => setDeleteRight(prev => !prev); const toggleDelete = () => setDeleteRight(prev => !prev);
const [ignores, setIgnores] = useState<ICstSubstitute[]>([]); const [ignores, setIgnores] = useState<ICstSubstitute[]>([]);
const filteredSuggestions = useMemo( const filteredSuggestions =
() => suggestions?.filter(
suggestions?.filter( item => !ignores.find(ignore => ignore.original === item.original && ignore.substitution === item.substitution)
item => !ignores.find(ignore => ignore.original === item.original && ignore.substitution === item.substitution) ) ?? [];
) ?? [],
[ignores, suggestions]
);
const getSchemaByCst = useCallback( const substitutionData: IMultiSubstitution[] = [
(id: ConstituentaID): IRSForm | undefined => { ...substitutions.map(item => ({
for (const schema of schemas) { original_source: getSchemaByCst(item.original)!,
const cst = schema.cstByID.get(id); original: getConstituenta(item.original)!,
if (cst) { substitution: getConstituenta(item.substitution)!,
return schema; substitution_source: getSchemaByCst(item.substitution)!,
} is_suggestion: false
})),
...filteredSuggestions.map(item => ({
original_source: getSchemaByCst(item.original)!,
original: getConstituenta(item.original)!,
substitution: getConstituenta(item.substitution)!,
substitution_source: getSchemaByCst(item.substitution)!,
is_suggestion: true
}))
];
function getSchemaByCst(id: ConstituentaID): IRSForm | undefined {
for (const schema of schemas) {
const cst = schema.cstByID.get(id);
if (cst) {
return schema;
} }
return undefined; }
}, return undefined;
[schemas] }
);
const getConstituenta = useCallback( function getConstituenta(id: ConstituentaID): IConstituenta | undefined {
(id: ConstituentaID): IConstituenta | undefined => { for (const schema of schemas) {
for (const schema of schemas) { const cst = schema.cstByID.get(id);
const cst = schema.cstByID.get(id); if (cst) {
if (cst) { return cst;
return cst;
}
} }
return undefined; }
}, return undefined;
[schemas] }
);
const substitutionData: IMultiSubstitution[] = useMemo(
() => [
...substitutions.map(item => ({
original_source: getSchemaByCst(item.original)!,
original: getConstituenta(item.original)!,
substitution: getConstituenta(item.substitution)!,
substitution_source: getSchemaByCst(item.substitution)!,
is_suggestion: false
})),
...filteredSuggestions.map(item => ({
original_source: getSchemaByCst(item.original)!,
original: getConstituenta(item.original)!,
substitution: getConstituenta(item.substitution)!,
substitution_source: getSchemaByCst(item.substitution)!,
is_suggestion: true
}))
],
[getConstituenta, getSchemaByCst, substitutions, filteredSuggestions]
);
function addSubstitution() { function addSubstitution() {
if (!leftCst || !rightCst) { if (!leftCst || !rightCst) {
@ -145,120 +133,105 @@ function PickSubstitutions({
setRightCst(undefined); setRightCst(undefined);
} }
const handleDeclineSuggestion = useCallback( function handleDeclineSuggestion(item: IMultiSubstitution) {
(item: IMultiSubstitution) => { setIgnores(prev => [...prev, { original: item.original.id, substitution: item.substitution.id }]);
setIgnores(prev => [...prev, { original: item.original.id, substitution: item.substitution.id }]); }
},
[setIgnores]
);
const handleAcceptSuggestion = useCallback( function handleAcceptSuggestion(item: IMultiSubstitution) {
(item: IMultiSubstitution) => { setSubstitutions(prev => [...prev, { original: item.original.id, substitution: item.substitution.id }]);
setSubstitutions(prev => [...prev, { original: item.original.id, substitution: item.substitution.id }]); }
},
[setSubstitutions]
);
const handleDeleteSubstitution = useCallback( function handleDeleteSubstitution(target: IMultiSubstitution) {
(target: IMultiSubstitution) => { handleDeclineSuggestion(target);
handleDeclineSuggestion(target); setSubstitutions(prev => {
setSubstitutions(prev => { const newItems: ICstSubstitute[] = [];
const newItems: ICstSubstitute[] = []; prev.forEach(item => {
prev.forEach(item => { if (item.original !== target.original.id || item.substitution !== target.substitution.id) {
if (item.original !== target.original.id || item.substitution !== target.substitution.id) { newItems.push(item);
newItems.push(item);
}
});
return newItems;
});
},
[setSubstitutions, handleDeclineSuggestion]
);
const columns = useMemo(
() => [
columnHelper.accessor(item => item.substitution_source.alias, {
id: 'left_schema',
size: 100,
cell: props => <div className='min-w-[10.5rem] text-ellipsis text-left'>{props.getValue()}</div>
}),
columnHelper.accessor(item => item.substitution.alias, {
id: 'left_alias',
size: 65,
cell: props => (
<BadgeConstituenta
theme={colors}
value={props.row.original.substitution}
prefixID={`${prefixID}_${props.row.index}_1_`}
/>
)
}),
columnHelper.display({
id: 'status',
size: 0,
cell: () => <IconPageRight size='1.2rem' />
}),
columnHelper.accessor(item => item.original.alias, {
id: 'right_alias',
size: 65,
cell: props => (
<BadgeConstituenta
theme={colors}
value={props.row.original.original}
prefixID={`${prefixID}_${props.row.index}_2_`}
/>
)
}),
columnHelper.accessor(item => item.original_source.alias, {
id: 'right_schema',
size: 100,
cell: props => <div className='min-w-[8rem] text-ellipsis text-right'>{props.getValue()}</div>
}),
columnHelper.display({
id: 'actions',
size: 0,
cell: props =>
props.row.original.is_suggestion ? (
<div className='max-w-fit'>
<MiniButton
noHover
title='Принять предложение'
icon={<IconAccept size='1rem' className='icon-green' />}
onClick={() => handleAcceptSuggestion(props.row.original)}
/>
<MiniButton
noHover
title='Игнорировать предложение'
icon={<IconRemove size='1rem' className='icon-red' />}
onClick={() => handleDeclineSuggestion(props.row.original)}
/>
</div>
) : (
<div className='max-w-fit'>
<MiniButton
noHover
title='Удалить'
icon={<IconRemove size='1rem' className='icon-red' />}
onClick={() => handleDeleteSubstitution(props.row.original)}
/>
</div>
)
})
],
[handleDeleteSubstitution, handleDeclineSuggestion, handleAcceptSuggestion, colors, prefixID]
);
const conditionalRowStyles = useMemo(
(): IConditionalStyle<IMultiSubstitution>[] => [
{
when: (item: IMultiSubstitution) => item.is_suggestion,
style: {
backgroundColor: colors.bgOrange50
} }
});
return newItems;
});
}
const columns = [
columnHelper.accessor(item => item.substitution_source.alias, {
id: 'left_schema',
size: 100,
cell: props => <div className='min-w-[10.5rem] text-ellipsis text-left'>{props.getValue()}</div>
}),
columnHelper.accessor(item => item.substitution.alias, {
id: 'left_alias',
size: 65,
cell: props => (
<BadgeConstituenta
theme={colors}
value={props.row.original.substitution}
prefixID={`${prefixID}_${props.row.index}_1_`}
/>
)
}),
columnHelper.display({
id: 'status',
size: 0,
cell: () => <IconPageRight size='1.2rem' />
}),
columnHelper.accessor(item => item.original.alias, {
id: 'right_alias',
size: 65,
cell: props => (
<BadgeConstituenta
theme={colors}
value={props.row.original.original}
prefixID={`${prefixID}_${props.row.index}_2_`}
/>
)
}),
columnHelper.accessor(item => item.original_source.alias, {
id: 'right_schema',
size: 100,
cell: props => <div className='min-w-[8rem] text-ellipsis text-right'>{props.getValue()}</div>
}),
columnHelper.display({
id: 'actions',
size: 0,
cell: props =>
props.row.original.is_suggestion ? (
<div className='max-w-fit'>
<MiniButton
noHover
title='Принять предложение'
icon={<IconAccept size='1rem' className='icon-green' />}
onClick={() => handleAcceptSuggestion(props.row.original)}
/>
<MiniButton
noHover
title='Игнорировать предложение'
icon={<IconRemove size='1rem' className='icon-red' />}
onClick={() => handleDeclineSuggestion(props.row.original)}
/>
</div>
) : (
<div className='max-w-fit'>
<MiniButton
noHover
title='Удалить'
icon={<IconRemove size='1rem' className='icon-red' />}
onClick={() => handleDeleteSubstitution(props.row.original)}
/>
</div>
)
})
];
const conditionalRowStyles: IConditionalStyle<IMultiSubstitution>[] = [
{
when: (item: IMultiSubstitution) => item.is_suggestion,
style: {
backgroundColor: colors.bgOrange50
} }
], }
[colors] ];
);
return ( return (
<div className={clsx('flex flex-col', className)} {...restProps}> <div className={clsx('flex flex-col', className)} {...restProps}>

View File

@ -1,7 +1,6 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useMemo } from 'react';
import { CstMatchMode } from '@/models/miscellaneous'; import { CstMatchMode } from '@/models/miscellaneous';
import { ConstituentaID, IConstituenta } from '@/models/rsform'; import { ConstituentaID, IConstituenta } from '@/models/rsform';
@ -28,22 +27,16 @@ function SelectConstituenta({
placeholder = 'Выберите конституенту', placeholder = 'Выберите конституенту',
...restProps ...restProps
}: SelectConstituentaProps) { }: SelectConstituentaProps) {
const options = useMemo(() => { const options =
return ( items?.map(cst => ({
items?.map(cst => ({ value: cst.id,
value: cst.id, label: `${cst.alias}${cst.is_inherited ? '*' : ''}: ${describeConstituenta(cst)}`
label: `${cst.alias}${cst.is_inherited ? '*' : ''}: ${describeConstituenta(cst)}` })) ?? [];
})) ?? []
);
}, [items]);
const filter = useCallback( function filter(option: { value: ConstituentaID | undefined; label: string }, inputValue: string) {
(option: { value: ConstituentaID | undefined; label: string }, inputValue: string) => { const cst = items?.find(item => item.id === option.value);
const cst = items?.find(item => item.id === option.value); return !cst ? false : matchConstituenta(cst, inputValue, CstMatchMode.ALL);
return !cst ? false : matchConstituenta(cst, inputValue, CstMatchMode.ALL); }
},
[items]
);
return ( return (
<SelectSingle <SelectSingle

View File

@ -1,7 +1,6 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useMemo } from 'react';
import { ILibraryItem, LibraryItemID } from '@/models/library'; import { ILibraryItem, LibraryItemID } from '@/models/library';
import { matchLibraryItem } from '@/models/libraryAPI'; import { matchLibraryItem } from '@/models/libraryAPI';
@ -26,22 +25,16 @@ function SelectLibraryItem({
placeholder = 'Выберите схему', placeholder = 'Выберите схему',
...restProps ...restProps
}: SelectLibraryItemProps) { }: SelectLibraryItemProps) {
const options = useMemo(() => { const options =
return ( items?.map(cst => ({
items?.map(cst => ({ value: cst.id,
value: cst.id, label: `${cst.alias}: ${cst.title}`
label: `${cst.alias}: ${cst.title}` })) ?? [];
})) ?? []
);
}, [items]);
const filter = useCallback( function filter(option: { value: LibraryItemID | undefined; label: string }, inputValue: string) {
(option: { value: LibraryItemID | undefined; label: string }, inputValue: string) => { const item = items?.find(item => item.id === option.value);
const item = items?.find(item => item.id === option.value); return !item ? false : matchLibraryItem(item, inputValue);
return !item ? false : matchLibraryItem(item, inputValue); }
},
[items]
);
return ( return (
<SelectSingle <SelectSingle

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import { FolderNode, FolderTree } from '@/models/FolderTree'; import { FolderNode, FolderTree } from '@/models/FolderTree';
import { labelFolderNode } from '@/utils/labels'; import { labelFolderNode } from '@/utils/labels';
@ -19,41 +19,34 @@ interface SelectLocationProps extends CProps.Styling {
} }
function SelectLocation({ value, folderTree, dense, prefix, onClick, className, style }: SelectLocationProps) { function SelectLocation({ value, folderTree, dense, prefix, onClick, className, style }: SelectLocationProps) {
const activeNode = useMemo(() => folderTree.at(value), [folderTree, value]); const activeNode = folderTree.at(value);
const items = folderTree.getTree();
const items = useMemo(() => folderTree.getTree(), [folderTree]);
const [folded, setFolded] = useState<FolderNode[]>(items); const [folded, setFolded] = useState<FolderNode[]>(items);
useEffect(() => { useEffect(() => {
setFolded(items.filter(item => item !== activeNode && !activeNode?.hasPredecessor(item))); setFolded(items.filter(item => item !== activeNode && !activeNode?.hasPredecessor(item)));
}, [items, activeNode]); }, [items, activeNode]);
const onFoldItem = useCallback( function onFoldItem(target: FolderNode, showChildren: boolean) {
(target: FolderNode, showChildren: boolean) => { setFolded(prev =>
setFolded(prev => items.filter(item => {
items.filter(item => { if (item === target) {
if (item === target) { return !showChildren;
return !showChildren; }
} if (!showChildren && item.hasPredecessor(target)) {
if (!showChildren && item.hasPredecessor(target)) { return true;
return true; } else {
} else { return prev.includes(item);
return prev.includes(item); }
} })
}) );
); }
},
[items]
);
const handleClickFold = useCallback( function handleClickFold(event: CProps.EventMouse, target: FolderNode, showChildren: boolean) {
(event: CProps.EventMouse, target: FolderNode, showChildren: boolean) => { event.preventDefault();
event.preventDefault(); event.stopPropagation();
event.stopPropagation(); onFoldItem(target, showChildren);
onFoldItem(target, showChildren); }
},
[onFoldItem]
);
return ( return (
<div className={clsx('flex flex-col', 'cc-scroll-y', className)} style={style}> <div className={clsx('flex flex-col', 'cc-scroll-y', className)} style={style}>

View File

@ -1,7 +1,6 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useMemo } from 'react';
import { IOperation, OperationID } from '@/models/oss'; import { IOperation, OperationID } from '@/models/oss';
import { matchOperation } from '@/models/ossAPI'; import { matchOperation } from '@/models/ossAPI';
@ -26,22 +25,16 @@ function SelectOperation({
placeholder = 'Выберите операцию', placeholder = 'Выберите операцию',
...restProps ...restProps
}: SelectOperationProps) { }: SelectOperationProps) {
const options = useMemo(() => { const options =
return ( items?.map(cst => ({
items?.map(cst => ({ value: cst.id,
value: cst.id, label: `${cst.alias}: ${cst.title}`
label: `${cst.alias}: ${cst.title}` })) ?? [];
})) ?? []
);
}, [items]);
const filter = useCallback( function filter(option: { value: OperationID | undefined; label: string }, inputValue: string) {
(option: { value: OperationID | undefined; label: string }, inputValue: string) => { const operation = items?.find(item => item.id === option.value);
const operation = items?.find(item => item.id === option.value); return !operation ? false : matchOperation(operation, inputValue);
return !operation ? false : matchOperation(operation, inputValue); }
},
[items]
);
return ( return (
<SelectSingle <SelectSingle

View File

@ -1,7 +1,6 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useMemo } from 'react';
import { useUsers } from '@/context/UsersContext'; import { useUsers } from '@/context/UsersContext';
import { IUserInfo, UserID } from '@/models/user'; import { IUserInfo, UserID } from '@/models/user';
@ -28,22 +27,16 @@ function SelectUser({
...restProps ...restProps
}: SelectUserProps) { }: SelectUserProps) {
const { getUserLabel } = useUsers(); const { getUserLabel } = useUsers();
const options = useMemo(() => { const options =
return ( items?.map(user => ({
items?.map(user => ({ value: user.id,
value: user.id, label: getUserLabel(user.id)
label: getUserLabel(user.id) })) ?? [];
})) ?? []
);
}, [items, getUserLabel]);
const filter = useCallback( function filter(option: { value: UserID | undefined; label: string }, inputValue: string) {
(option: { value: UserID | undefined; label: string }, inputValue: string) => { const user = items?.find(item => item.id === option.value);
const user = items?.find(item => item.id === option.value); return !user ? false : matchUser(user, inputValue);
return !user ? false : matchUser(user, inputValue); }
},
[items]
);
return ( return (
<SelectSingle <SelectSingle

View File

@ -1,7 +1,6 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useMemo } from 'react';
import { IVersionInfo, VersionID } from '@/models/library'; import { IVersionInfo, VersionID } from '@/models/library';
import { labelVersion } from '@/utils/labels'; import { labelVersion } from '@/utils/labels';
@ -20,22 +19,21 @@ interface SelectVersionProps extends CProps.Styling {
} }
function SelectVersion({ id, className, items, value, onSelectValue, ...restProps }: SelectVersionProps) { function SelectVersion({ id, className, items, value, onSelectValue, ...restProps }: SelectVersionProps) {
const options = useMemo(() => { const options = [
return [ {
{ value: undefined,
value: undefined, label: labelVersion(undefined)
label: labelVersion(undefined) },
}, ...(items?.map(version => ({
...(items?.map(version => ({ value: version.id,
value: version.id, label: version.version
label: version.version })) ?? [])
})) ?? []) ];
];
}, [items]); const valueLabel = (() => {
const valueLabel = useMemo(() => {
const version = items?.find(ver => ver.id === value); const version = items?.find(ver => ver.id === value);
return version ? version.version : labelVersion(undefined); return version ? version.version : labelVersion(undefined);
}, [items, value]); })();
return ( return (
<SelectSingle <SelectSingle

View File

@ -1,5 +1,4 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useMemo } from 'react';
import { globals } from '@/utils/constants'; import { globals } from '@/utils/constants';
@ -34,15 +33,7 @@ function Checkbox({
setValue, setValue,
...restProps ...restProps
}: CheckboxProps) { }: CheckboxProps) {
const cursor = useMemo(() => { const cursor = disabled ? 'cursor-arrow' : setValue ? 'cursor-pointer' : '';
if (disabled) {
return 'cursor-arrow';
} else if (setValue) {
return 'cursor-pointer';
} else {
return '';
}
}, [disabled, setValue]);
function handleClick(event: CProps.EventMouse): void { function handleClick(event: CProps.EventMouse): void {
event.preventDefault(); event.preventDefault();

View File

@ -1,5 +1,4 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useMemo } from 'react';
import { globals } from '@/utils/constants'; import { globals } from '@/utils/constants';
@ -29,15 +28,7 @@ function CheckboxTristate({
setValue, setValue,
...restProps ...restProps
}: CheckboxTristateProps) { }: CheckboxTristateProps) {
const cursor = useMemo(() => { const cursor = disabled ? 'cursor-arrow' : setValue ? 'cursor-pointer' : '';
if (disabled) {
return 'cursor-arrow';
} else if (setValue) {
return 'cursor-pointer';
} else {
return '';
}
}, [disabled, setValue]);
function handleClick(event: CProps.EventMouse): void { function handleClick(event: CProps.EventMouse): void {
event.preventDefault(); event.preventDefault();

View File

@ -1,7 +1,5 @@
'use client'; 'use client';
import { useMemo } from 'react';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import useWindowSize from '@/hooks/useWindowSize'; import useWindowSize from '@/hooks/useWindowSize';
@ -29,10 +27,8 @@ function PDFViewer({ file, offsetXpx, minWidth = MINIMUM_WIDTH }: PDFViewerProps
const windowSize = useWindowSize(); const windowSize = useWindowSize();
const { calculateHeight } = useConceptOptions(); const { calculateHeight } = useConceptOptions();
const pageWidth = useMemo(() => { const pageWidth = Math.max(minWidth, Math.min((windowSize?.width ?? 0) - (offsetXpx ?? 0) - 10, MAXIMUM_WIDTH));
return Math.max(minWidth, Math.min((windowSize?.width ?? 0) - (offsetXpx ?? 0) - 10, MAXIMUM_WIDTH)); const pageHeight = calculateHeight('1rem');
}, [windowSize, offsetXpx, minWidth]);
const pageHeight = useMemo(() => calculateHeight('1rem'), [calculateHeight]);
return <embed src={`${file}#toolbar=0`} className='p-3' style={{ width: pageWidth, height: pageHeight }} />; return <embed src={`${file}#toolbar=0`} className='p-3' style={{ width: pageWidth, height: pageHeight }} />;
} }

View File

@ -1,6 +1,5 @@
'use client'; 'use client';
import { useMemo } from 'react';
import Select, { import Select, {
ClearIndicatorProps, ClearIndicatorProps,
components, components,
@ -54,58 +53,55 @@ function SelectMulti<Option, Group extends GroupBase<Option> = GroupBase<Option>
}: SelectMultiProps<Option, Group>) { }: SelectMultiProps<Option, Group>) {
const { darkMode, colors } = useConceptOptions(); const { darkMode, colors } = useConceptOptions();
const size = useWindowSize(); const size = useWindowSize();
const themeColors = useMemo(() => (!darkMode ? selectLightT : selectDarkT), [darkMode]); const themeColors = !darkMode ? selectLightT : selectDarkT;
const adjustedStyles: StylesConfig<Option, true, Group> = useMemo( const adjustedStyles: StylesConfig<Option, true, Group> = {
() => ({ container: defaultStyles => ({
container: defaultStyles => ({ ...defaultStyles,
...defaultStyles, borderRadius: '0.25rem'
borderRadius: '0.25rem'
}),
control: (styles, { isDisabled }) => ({
...styles,
borderRadius: '0.25rem',
cursor: isDisabled ? 'not-allowed' : 'pointer',
boxShadow: 'none'
}),
option: (styles, { isSelected }) => ({
...styles,
padding: '0.25rem 0.75rem',
fontSize: '0.875rem',
lineHeight: '1.25rem',
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: 0
}),
input: styles => ({ ...styles }),
placeholder: styles => ({ ...styles }),
multiValue: styles => ({
...styles,
borderRadius: '0.5rem',
backgroundColor: colors.bgSelected
}),
dropdownIndicator: base => ({
...base,
paddingTop: 0,
paddingBottom: 0
}),
clearIndicator: base => ({
...base,
paddingTop: 0,
paddingBottom: 0
})
}), }),
[colors] control: (styles, { isDisabled }) => ({
); ...styles,
borderRadius: '0.25rem',
cursor: isDisabled ? 'not-allowed' : 'pointer',
boxShadow: 'none'
}),
option: (styles, { isSelected }) => ({
...styles,
padding: '0.25rem 0.75rem',
fontSize: '0.875rem',
lineHeight: '1.25rem',
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: 0
}),
input: styles => ({ ...styles }),
placeholder: styles => ({ ...styles }),
multiValue: styles => ({
...styles,
borderRadius: '0.5rem',
backgroundColor: colors.bgSelected
}),
dropdownIndicator: base => ({
...base,
paddingTop: 0,
paddingBottom: 0
}),
clearIndicator: base => ({
...base,
paddingTop: 0,
paddingBottom: 0
})
};
return ( return (
<Select <Select

View File

@ -1,6 +1,5 @@
'use client'; 'use client';
import { useMemo } from 'react';
import Select, { import Select, {
ClearIndicatorProps, ClearIndicatorProps,
components, components,
@ -56,55 +55,52 @@ function SelectSingle<Option, Group extends GroupBase<Option> = GroupBase<Option
}: SelectSingleProps<Option, Group>) { }: SelectSingleProps<Option, Group>) {
const { darkMode, colors } = useConceptOptions(); const { darkMode, colors } = useConceptOptions();
const size = useWindowSize(); const size = useWindowSize();
const themeColors = useMemo(() => (!darkMode ? selectLightT : selectDarkT), [darkMode]); const themeColors = !darkMode ? selectLightT : selectDarkT;
const adjustedStyles: StylesConfig<Option, false, Group> = useMemo( const adjustedStyles: StylesConfig<Option, false, Group> = {
() => ({ container: defaultStyles => ({
container: defaultStyles => ({ ...defaultStyles,
...defaultStyles, borderRadius: '0.25rem'
borderRadius: '0.25rem'
}),
control: (defaultStyles, { isDisabled }) => ({
...defaultStyles,
borderRadius: '0.25rem',
...(noBorder ? { borderWidth: 0 } : {}),
cursor: isDisabled ? 'not-allowed' : 'pointer',
boxShadow: 'none'
}),
menuPortal: defaultStyles => ({
...defaultStyles,
zIndex: 9999
}),
menuList: defaultStyles => ({
...defaultStyles,
padding: 0
}),
option: (defaultStyles, { isSelected }) => ({
...defaultStyles,
padding: '0.25rem 0.75rem',
fontSize: '0.875rem',
lineHeight: '1.25rem',
backgroundColor: isSelected ? colors.bgSelected : defaultStyles.backgroundColor,
color: isSelected ? colors.fgSelected : defaultStyles.color,
borderWidth: '1px',
borderColor: colors.border
}),
input: defaultStyles => ({ ...defaultStyles }),
placeholder: defaultStyles => ({ ...defaultStyles }),
singleValue: defaultStyles => ({ ...defaultStyles }),
dropdownIndicator: base => ({
...base,
paddingTop: 0,
paddingBottom: 0
}),
clearIndicator: base => ({
...base,
paddingTop: 0,
paddingBottom: 0
})
}), }),
[colors, noBorder] control: (defaultStyles, { isDisabled }) => ({
); ...defaultStyles,
borderRadius: '0.25rem',
...(noBorder ? { borderWidth: 0 } : {}),
cursor: isDisabled ? 'not-allowed' : 'pointer',
boxShadow: 'none'
}),
menuPortal: defaultStyles => ({
...defaultStyles,
zIndex: 9999
}),
menuList: defaultStyles => ({
...defaultStyles,
padding: 0
}),
option: (defaultStyles, { isSelected }) => ({
...defaultStyles,
padding: '0.25rem 0.75rem',
fontSize: '0.875rem',
lineHeight: '1.25rem',
backgroundColor: isSelected ? colors.bgSelected : defaultStyles.backgroundColor,
color: isSelected ? colors.fgSelected : defaultStyles.color,
borderWidth: '1px',
borderColor: colors.border
}),
input: defaultStyles => ({ ...defaultStyles }),
placeholder: defaultStyles => ({ ...defaultStyles }),
singleValue: defaultStyles => ({ ...defaultStyles }),
dropdownIndicator: base => ({
...base,
paddingTop: 0,
paddingBottom: 0
}),
clearIndicator: base => ({
...base,
paddingTop: 0,
paddingBottom: 0
})
};
return ( return (
<Select <Select

View File

@ -1,5 +1,5 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import { globals, PARAMETER } from '@/utils/constants'; import { globals, PARAMETER } from '@/utils/constants';
@ -44,51 +44,39 @@ function SelectTree<ItemType>({
prefix, prefix,
...restProps ...restProps
}: SelectTreeProps<ItemType>) { }: SelectTreeProps<ItemType>) {
const foldable = useMemo( const foldable = new Set(items.filter(item => getParent(item) !== item).map(item => getParent(item)));
() => new Set(items.filter(item => getParent(item) !== item).map(item => getParent(item))),
[items, getParent]
);
const [folded, setFolded] = useState<ItemType[]>(items); const [folded, setFolded] = useState<ItemType[]>(items);
useEffect(() => { useEffect(() => {
setFolded(items.filter(item => getParent(value) !== item && getParent(getParent(value)) !== item)); setFolded(items.filter(item => getParent(value) !== item && getParent(getParent(value)) !== item));
}, [value, getParent, items]); }, [value, getParent, items]);
const onFoldItem = useCallback( function onFoldItem(target: ItemType, showChildren: boolean) {
(target: ItemType, showChildren: boolean) => { setFolded(prev =>
setFolded(prev => items.filter(item => {
items.filter(item => { if (item === target) {
if (item === target) { return !showChildren;
return !showChildren; }
} if (!showChildren && (getParent(item) === target || getParent(getParent(item)) === target)) {
if (!showChildren && (getParent(item) === target || getParent(getParent(item)) === target)) { return true;
return true; } else {
} else { return prev.includes(item);
return prev.includes(item); }
} })
}) );
); }
},
[items, getParent]
);
const handleClickFold = useCallback( function handleClickFold(event: CProps.EventMouse, target: ItemType, showChildren: boolean) {
(event: CProps.EventMouse, target: ItemType, showChildren: boolean) => { event.preventDefault();
event.preventDefault(); event.stopPropagation();
event.stopPropagation(); onFoldItem(target, showChildren);
onFoldItem(target, showChildren); }
},
[onFoldItem]
);
const handleSetValue = useCallback( function handleSetValue(event: CProps.EventMouse, target: ItemType) {
(event: CProps.EventMouse, target: ItemType) => { event.preventDefault();
event.preventDefault(); event.stopPropagation();
event.stopPropagation(); onChangeValue(target);
onChangeValue(target); }
},
[onChangeValue]
);
return ( return (
<div {...restProps}> <div {...restProps}>

View File

@ -1,5 +1,4 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useMemo } from 'react';
import { globals } from '@/utils/constants'; import { globals } from '@/utils/constants';
@ -50,7 +49,8 @@ function ValueIcon({
onClick, onClick,
...restProps ...restProps
}: ValueIconProps) { }: ValueIconProps) {
const isSmall = useMemo(() => !smallThreshold || String(value).length < smallThreshold, [value, smallThreshold]); // TODO: use CSS instead of threshold
const isSmall = !smallThreshold || String(value).length < smallThreshold;
return ( return (
<div <div
className={clsx( className={clsx(

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useMemo, useState } from 'react'; import { useState } from 'react';
import { IconReset } from '@/components/Icons'; import { IconReset } from '@/components/Icons';
import PickSchema from '@/components/select/PickSchema'; import PickSchema from '@/components/select/PickSchema';
@ -22,18 +22,16 @@ interface DlgChangeInputSchemaProps extends Pick<ModalProps, 'hideWindow'> {
function DlgChangeInputSchema({ oss, hideWindow, target, onSubmit }: DlgChangeInputSchemaProps) { function DlgChangeInputSchema({ oss, hideWindow, target, onSubmit }: DlgChangeInputSchemaProps) {
const [selected, setSelected] = useState<LibraryItemID | undefined>(target.result ?? undefined); const [selected, setSelected] = useState<LibraryItemID | undefined>(target.result ?? undefined);
const library = useLibrary(); const library = useLibrary();
const sortedItems = useMemo(() => sortItemsForOSS(oss, library.items), [oss, library.items]); const sortedItems = sortItemsForOSS(oss, library.items);
const isValid = target.result !== selected;
const baseFilter = useCallback( function baseFilter(item: ILibraryItem) {
(item: ILibraryItem) => !oss.schemas.includes(item.id) || item.id === selected || item.id === target.result, return !oss.schemas.includes(item.id) || item.id === selected || item.id === target.result;
[oss, selected, target] }
);
const isValid = useMemo(() => target.result !== selected, [target, selected]); function handleSelectLocation(newValue: LibraryItemID) {
const handleSelectLocation = useCallback((newValue: LibraryItemID) => {
setSelected(newValue); setSelected(newValue);
}, []); }
return ( return (
<Modal <Modal

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useMemo, useState } from 'react'; import { useState } from 'react';
import SelectLocationContext from '@/components/select/SelectLocationContext'; import SelectLocationContext from '@/components/select/SelectLocationContext';
import SelectLocationHead from '@/components/select/SelectLocationHead'; import SelectLocationHead from '@/components/select/SelectLocationHead';
@ -26,13 +26,13 @@ function DlgChangeLocation({ hideWindow, initial, onChangeLocation }: DlgChangeL
const { folders } = useLibrary(); const { folders } = useLibrary();
const location = useMemo(() => combineLocation(head, body), [head, body]); const location = combineLocation(head, body);
const isValid = useMemo(() => initial !== location && validateLocation(location), [initial, location]); const isValid = initial !== location && validateLocation(location);
const handleSelectLocation = useCallback((newValue: string) => { function handleSelectLocation(newValue: string) {
setHead(newValue.substring(0, 2) as LocationHead); setHead(newValue.substring(0, 2) as LocationHead);
setBody(newValue.length > 3 ? newValue.substring(3) : ''); setBody(newValue.length > 3 ? newValue.substring(3) : '');
}, []); }
return ( return (
<Modal <Modal

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useMemo, useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { urls } from '@/app/urls'; import { urls } from '@/app/urls';
@ -43,16 +43,16 @@ function DlgCloneLibraryItem({ hideWindow, base, initialLocation, selected, tota
const [head, setHead] = useState(initialLocation.substring(0, 2) as LocationHead); const [head, setHead] = useState(initialLocation.substring(0, 2) as LocationHead);
const [body, setBody] = useState(initialLocation.substring(3)); const [body, setBody] = useState(initialLocation.substring(3));
const location = useMemo(() => combineLocation(head, body), [head, body]); const location = combineLocation(head, body);
const { cloneItem, folders } = useLibrary(); const { cloneItem, folders } = useLibrary();
const canSubmit = useMemo(() => title !== '' && alias !== '' && validateLocation(location), [title, alias, location]); const canSubmit = title !== '' && alias !== '' && validateLocation(location);
const handleSelectLocation = useCallback((newValue: string) => { function handleSelectLocation(newValue: string) {
setHead(newValue.substring(0, 2) as LocationHead); setHead(newValue.substring(0, 2) as LocationHead);
setBody(newValue.length > 3 ? newValue.substring(3) : ''); setBody(newValue.length > 3 ? newValue.substring(3) : '');
}, []); }
function handleSubmit() { function handleSubmit() {
const data: IRSFormCloneData = { const data: IRSFormCloneData = {

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import { TabList, TabPanel, Tabs } from 'react-tabs'; import { TabList, TabPanel, Tabs } from 'react-tabs';
import Modal, { ModalProps } from '@/components/ui/Modal'; import Modal, { ModalProps } from '@/components/ui/Modal';
@ -123,35 +123,6 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
setValidated(!!template.prototype && validateNewAlias(constituenta.alias, constituenta.cst_type, schema)); setValidated(!!template.prototype && validateNewAlias(constituenta.alias, constituenta.cst_type, schema));
}, [constituenta.alias, constituenta.cst_type, schema, template.prototype]); }, [constituenta.alias, constituenta.cst_type, schema, template.prototype]);
const templatePanel = useMemo(
() => (
<TabPanel>
<TabTemplate state={template} partialUpdate={updateTemplate} templateSchema={templateSchema} />
</TabPanel>
),
[template, templateSchema, updateTemplate]
);
const argumentsPanel = useMemo(
() => (
<TabPanel>
<TabArguments schema={schema} state={substitutes} partialUpdate={updateSubstitutes} />
</TabPanel>
),
[schema, substitutes, updateSubstitutes]
);
const editorPanel = useMemo(
() => (
<TabPanel>
<div className='cc-fade-in cc-column'>
<FormCreateCst state={constituenta} partialUpdate={updateConstituenta} schema={schema} />
</div>
</TabPanel>
),
[constituenta, updateConstituenta, schema]
);
return ( return (
<Modal <Modal
header='Создание конституенты из шаблона' header='Создание конституенты из шаблона'
@ -175,9 +146,19 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
<TabLabel label='Конституента' title='Редактирование конституенты' className='w-[8rem]' /> <TabLabel label='Конституента' title='Редактирование конституенты' className='w-[8rem]' />
</TabList> </TabList>
{templatePanel} <TabPanel>
{argumentsPanel} <TabTemplate state={template} partialUpdate={updateTemplate} templateSchema={templateSchema} />
{editorPanel} </TabPanel>
<TabPanel>
<TabArguments schema={schema} state={substitutes} partialUpdate={updateSubstitutes} />
</TabPanel>
<TabPanel>
<div className='cc-fade-in cc-column'>
<FormCreateCst state={constituenta} partialUpdate={updateConstituenta} schema={schema} />
</div>
</TabPanel>
</Tabs> </Tabs>
</Modal> </Modal>
); );

View File

@ -2,7 +2,7 @@
import { createColumnHelper } from '@tanstack/react-table'; import { createColumnHelper } from '@tanstack/react-table';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import { IconAccept, IconRemove, IconReset } from '@/components/Icons'; import { IconAccept, IconRemove, IconReset } from '@/components/Icons';
import RSInput from '@/components/RSInput'; import RSInput from '@/components/RSInput';
@ -33,13 +33,8 @@ function TabArguments({ state, schema, partialUpdate }: TabArgumentsProps) {
const [selectedCst, setSelectedCst] = useState<IConstituenta | undefined>(undefined); const [selectedCst, setSelectedCst] = useState<IConstituenta | undefined>(undefined);
const [selectedArgument, setSelectedArgument] = useState<IArgumentValue | undefined>(undefined); const [selectedArgument, setSelectedArgument] = useState<IArgumentValue | undefined>(undefined);
const [argumentValue, setArgumentValue] = useState(''); const [argumentValue, setArgumentValue] = useState('');
const isModified = selectedArgument && argumentValue !== selectedArgument.value;
const isModified = useMemo(
() => selectedArgument && argumentValue !== selectedArgument.value,
[selectedArgument, argumentValue]
);
useEffect(() => { useEffect(() => {
if (!selectedArgument && state.arguments.length > 0) { if (!selectedArgument && state.arguments.length > 0) {
@ -47,103 +42,91 @@ function TabArguments({ state, schema, partialUpdate }: TabArgumentsProps) {
} }
}, [state.arguments, selectedArgument]); }, [state.arguments, selectedArgument]);
const handleSelectArgument = useCallback((arg: IArgumentValue) => { function handleSelectArgument(arg: IArgumentValue) {
setSelectedArgument(arg); setSelectedArgument(arg);
if (arg.value) { if (arg.value) {
setArgumentValue(arg.value); setArgumentValue(arg.value);
} }
}, []); }
const handleSelectConstituenta = useCallback((cst: IConstituenta) => { function handleSelectConstituenta(cst: IConstituenta) {
setSelectedCst(cst); setSelectedCst(cst);
setArgumentValue(cst.alias); setArgumentValue(cst.alias);
}, []); }
const handleClearArgument = useCallback( function handleClearArgument(target: IArgumentValue) {
(target: IArgumentValue) => { const newArg = { ...target, value: '' };
const newArg = { ...target, value: '' }; partialUpdate({
partialUpdate({ arguments: state.arguments.map(arg => (arg.alias !== target.alias ? arg : newArg))
arguments: state.arguments.map(arg => (arg.alias !== target.alias ? arg : newArg)) });
}); setSelectedArgument(newArg);
setSelectedArgument(newArg); }
},
[partialUpdate, state.arguments]
);
const handleReset = useCallback(() => { function handleReset() {
setArgumentValue(selectedArgument?.value ?? ''); setArgumentValue(selectedArgument?.value ?? '');
}, [selectedArgument]); }
const handleAssignArgument = useCallback( function handleAssignArgument(target: IArgumentValue, value: string) {
(target: IArgumentValue, value: string) => { const newArg = { ...target, value: value };
const newArg = { ...target, value: value }; partialUpdate({
partialUpdate({ arguments: state.arguments.map(arg => (arg.alias !== target.alias ? arg : newArg))
arguments: state.arguments.map(arg => (arg.alias !== target.alias ? arg : newArg)) });
}); setSelectedArgument(newArg);
setSelectedArgument(newArg); }
},
[partialUpdate, state.arguments]
);
const columns = useMemo( const columns = [
() => [ argumentsHelper.accessor('alias', {
argumentsHelper.accessor('alias', { id: 'alias',
id: 'alias', size: 40,
size: 40, minSize: 40,
minSize: 40, maxSize: 40,
maxSize: 40, cell: props => <div className='text-center'>{props.getValue()}</div>
cell: props => <div className='text-center'>{props.getValue()}</div> }),
}), argumentsHelper.accessor(arg => arg.value || 'свободный аргумент', {
argumentsHelper.accessor(arg => arg.value || 'свободный аргумент', { id: 'value',
id: 'value', size: 200,
size: 200, minSize: 200,
minSize: 200, maxSize: 200
maxSize: 200 }),
}), argumentsHelper.accessor(arg => arg.typification, {
argumentsHelper.accessor(arg => arg.typification, { id: 'type',
id: 'type', enableHiding: true,
enableHiding: true, cell: props => (
cell: props => ( <div
<div className={clsx(
className={clsx( 'min-w-[9.3rem] max-w-[9.3rem]', // prettier: split lines
'min-w-[9.3rem] max-w-[9.3rem]', // prettier: split lines 'text-sm break-words'
'text-sm break-words' )}
)} >
> {props.getValue()}
{props.getValue()} </div>
</div> )
) }),
}), argumentsHelper.display({
argumentsHelper.display({ id: 'actions',
id: 'actions', size: 0,
size: 0, cell: props => (
cell: props => ( <div className='h-[1.25rem] w-[1.25rem]'>
<div className='h-[1.25rem] w-[1.25rem]'> {props.row.original.value ? (
{props.row.original.value ? ( <MiniButton
<MiniButton title='Очистить значение'
title='Очистить значение' noPadding
noPadding noHover
noHover icon={<IconRemove size='1.25rem' className='icon-red' />}
icon={<IconRemove size='1.25rem' className='icon-red' />} onClick={() => handleClearArgument(props.row.original)}
onClick={() => handleClearArgument(props.row.original)} />
/> ) : null}
) : null} </div>
</div> )
) })
}) ];
],
[handleClearArgument]
);
const conditionalRowStyles = useMemo( const conditionalRowStyles: IConditionalStyle<IArgumentValue>[] = [
(): IConditionalStyle<IArgumentValue>[] => [ {
{ when: (arg: IArgumentValue) => arg.alias === selectedArgument?.alias,
when: (arg: IArgumentValue) => arg.alias === selectedArgument?.alias, style: { backgroundColor: colors.bgSelected }
style: { backgroundColor: colors.bgSelected } }
} ];
],
[selectedArgument, colors]
);
return ( return (
<div className='cc-fade-in'> <div className='cc-fade-in'>

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { Dispatch, useEffect, useMemo, useState } from 'react'; import { Dispatch, useEffect, useState } from 'react';
import RSInput from '@/components/RSInput'; import RSInput from '@/components/RSInput';
import PickConstituenta from '@/components/select/PickConstituenta'; import PickConstituenta from '@/components/select/PickConstituenta';
@ -28,36 +28,23 @@ function TabTemplate({ state, partialUpdate, templateSchema }: TabTemplateProps)
const [filteredData, setFilteredData] = useState<IConstituenta[]>([]); const [filteredData, setFilteredData] = useState<IConstituenta[]>([]);
const prototypeInfo = useMemo(() => { const prototypeInfo = !state.prototype
if (!state.prototype) { ? ''
return ''; : `${state.prototype?.term_raw}${state.prototype?.definition_raw ? `${state.prototype?.definition_raw}` : ''}`;
} else {
return `${state.prototype?.term_raw}${
state.prototype?.definition_raw ? `${state.prototype?.definition_raw}` : ''
}`;
}
}, [state.prototype]);
const templateSelector = useMemo( const templateSelector = templates.map(template => ({
() => value: template.id,
templates.map(template => ({ label: template.title
value: template.id, }));
label: template.title
})),
[templates]
);
const categorySelector = useMemo((): { value: number; label: string }[] => { const categorySelector: { value: number; label: string }[] = !templateSchema
if (!templateSchema) { ? []
return []; : templateSchema.items
} .filter(cst => cst.cst_type === CATEGORY_CST_TYPE)
return templateSchema.items .map(cst => ({
.filter(cst => cst.cst_type === CATEGORY_CST_TYPE) value: cst.id,
.map(cst => ({ label: cst.term_raw
value: cst.id, }));
label: cst.term_raw
}));
}, [templateSchema]);
useEffect(() => { useEffect(() => {
if (templates.length > 0 && !state.templateID) { if (templates.length > 0 && !state.templateID) {

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import BadgeHelp from '@/components/info/BadgeHelp'; import BadgeHelp from '@/components/info/BadgeHelp';
import RSInput from '@/components/RSInput'; import RSInput from '@/components/RSInput';
@ -26,9 +26,9 @@ interface FormCreateCstProps {
function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreateCstProps) { function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreateCstProps) {
const [forceComment, setForceComment] = useState(false); const [forceComment, setForceComment] = useState(false);
const isBasic = useMemo(() => isBasicConcept(state.cst_type), [state]); const isBasic = isBasicConcept(state.cst_type);
const isElementary = useMemo(() => isBaseSet(state.cst_type), [state]); const isElementary = isBaseSet(state.cst_type);
const showConvention = useMemo(() => !!state.convention || forceComment || isBasic, [state, forceComment, isBasic]); const showConvention = !!state.convention || forceComment || isBasic;
useEffect(() => { useEffect(() => {
setForceComment(false); setForceComment(false);
@ -40,10 +40,9 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
} }
}, [state.alias, state.cst_type, schema, setValidated]); }, [state.alias, state.cst_type, schema, setValidated]);
const handleTypeChange = useCallback( function handleTypeChange(target: CstType) {
(target: CstType) => partialUpdate({ cst_type: target, alias: generateAlias(target, schema) }), return partialUpdate({ cst_type: target, alias: generateAlias(target, schema) });
[partialUpdate, schema] }
);
return ( return (
<> <>

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import { TabList, TabPanel, Tabs } from 'react-tabs'; import { TabList, TabPanel, Tabs } from 'react-tabs';
import Modal from '@/components/ui/Modal'; import Modal from '@/components/ui/Modal';
@ -38,7 +38,7 @@ function DlgCreateOperation({ hideWindow, oss, onCreate, initialInputs }: DlgCre
const [attachedID, setAttachedID] = useState<LibraryItemID | undefined>(undefined); const [attachedID, setAttachedID] = useState<LibraryItemID | undefined>(undefined);
const [createSchema, setCreateSchema] = useState(false); const [createSchema, setCreateSchema] = useState(false);
const isValid = useMemo(() => { const isValid = (() => {
if (alias === '') { if (alias === '') {
return false; return false;
} }
@ -51,7 +51,7 @@ function DlgCreateOperation({ hideWindow, oss, onCreate, initialInputs }: DlgCre
} }
} }
return true; return true;
}, [alias, activeTab, inputs, attachedID, oss.items]); })();
useEffect(() => { useEffect(() => {
if (attachedID) { if (attachedID) {
@ -82,60 +82,17 @@ function DlgCreateOperation({ hideWindow, oss, onCreate, initialInputs }: DlgCre
onCreate(data); onCreate(data);
}; };
const handleSelectTab = useCallback( function handleSelectTab(newTab: TabID, last: TabID) {
(newTab: TabID, last: TabID) => { if (last === newTab) {
if (last === newTab) { return;
return; }
} if (newTab === TabID.INPUT) {
if (newTab === TabID.INPUT) { setAttachedID(undefined);
setAttachedID(undefined); } else {
} else { setInputs(initialInputs);
setInputs(initialInputs); }
} setActiveTab(newTab);
setActiveTab(newTab); }
},
[setActiveTab, initialInputs]
);
const inputPanel = useMemo(
() => (
<TabPanel>
<TabInputOperation
oss={oss}
alias={alias}
onChangeAlias={setAlias}
comment={comment}
onChangeComment={setComment}
title={title}
onChangeTitle={setTitle}
attachedID={attachedID}
onChangeAttachedID={setAttachedID}
createSchema={createSchema}
onChangeCreateSchema={setCreateSchema}
/>
</TabPanel>
),
[alias, comment, title, attachedID, oss, createSchema, setAlias]
);
const synthesisPanel = useMemo(
() => (
<TabPanel>
<TabSynthesisOperation
oss={oss}
alias={alias}
onChangeAlias={setAlias}
comment={comment}
onChangeComment={setComment}
title={title}
onChangeTitle={setTitle}
inputs={inputs}
setInputs={setInputs}
/>
</TabPanel>
),
[oss, alias, comment, title, inputs, setAlias]
);
return ( return (
<Modal <Modal
@ -164,8 +121,35 @@ function DlgCreateOperation({ hideWindow, oss, onCreate, initialInputs }: DlgCre
/> />
</TabList> </TabList>
{inputPanel} <TabPanel>
{synthesisPanel} <TabInputOperation
oss={oss}
alias={alias}
onChangeAlias={setAlias}
comment={comment}
onChangeComment={setComment}
title={title}
onChangeTitle={setTitle}
attachedID={attachedID}
onChangeAttachedID={setAttachedID}
createSchema={createSchema}
onChangeCreateSchema={setCreateSchema}
/>
</TabPanel>
<TabPanel>
<TabSynthesisOperation
oss={oss}
alias={alias}
onChangeAlias={setAlias}
comment={comment}
onChangeComment={setComment}
title={title}
onChangeTitle={setTitle}
inputs={inputs}
setInputs={setInputs}
/>
</TabPanel>
</Tabs> </Tabs>
</Modal> </Modal>
); );

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useCallback, useEffect, useMemo } from 'react'; import { useEffect } from 'react';
import { IconReset } from '@/components/Icons'; import { IconReset } from '@/components/Icons';
import PickSchema from '@/components/select/PickSchema'; import PickSchema from '@/components/select/PickSchema';
@ -41,9 +41,12 @@ function TabInputOperation({
createSchema, createSchema,
onChangeCreateSchema onChangeCreateSchema
}: TabInputOperationProps) { }: TabInputOperationProps) {
const baseFilter = useCallback((item: ILibraryItem) => !oss.schemas.includes(item.id), [oss]);
const library = useLibrary(); const library = useLibrary();
const sortedItems = useMemo(() => sortItemsForOSS(oss, library.items), [oss, library.items]); const sortedItems = sortItemsForOSS(oss, library.items);
function baseFilter(item: ILibraryItem) {
return !oss.schemas.includes(item.id);
}
useEffect(() => { useEffect(() => {
if (createSchema) { if (createSchema) {

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useMemo, useState } from 'react'; import { useState } from 'react';
import Checkbox from '@/components/ui/Checkbox'; import Checkbox from '@/components/ui/Checkbox';
import Modal, { ModalProps } from '@/components/ui/Modal'; import Modal, { ModalProps } from '@/components/ui/Modal';
@ -23,9 +23,7 @@ function DlgCreateVersion({ hideWindow, versions, selected, totalCount, onCreate
const [description, setDescription] = useState(''); const [description, setDescription] = useState('');
const [onlySelected, setOnlySelected] = useState(false); const [onlySelected, setOnlySelected] = useState(false);
const canSubmit = useMemo(() => { const canSubmit = !versions.find(ver => ver.version === version);
return !versions.find(ver => ver.version === version);
}, [versions, version]);
function handleSubmit() { function handleSubmit() {
const data: IVersionCreateData = { const data: IVersionCreateData = {

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useMemo, useState } from 'react'; import { useState } from 'react';
import Checkbox from '@/components/ui/Checkbox'; import Checkbox from '@/components/ui/Checkbox';
import Modal, { ModalProps } from '@/components/ui/Modal'; import Modal, { ModalProps } from '@/components/ui/Modal';
@ -18,12 +18,9 @@ interface DlgDeleteCstProps extends Pick<ModalProps, 'hideWindow'> {
function DlgDeleteCst({ hideWindow, selected, schema, onDelete }: DlgDeleteCstProps) { function DlgDeleteCst({ hideWindow, selected, schema, onDelete }: DlgDeleteCstProps) {
const [expandOut, setExpandOut] = useState(false); const [expandOut, setExpandOut] = useState(false);
const expansion: ConstituentaID[] = useMemo( const expansion: ConstituentaID[] = schema.graph.expandAllOutputs(selected);
() => schema.graph.expandAllOutputs(selected), // prettier: split-lines const hasInherited = selected.some(
[selected, schema.graph] id => schema.inheritance.find(item => item.parent === id),
);
const hasInherited = useMemo(
() => selected.some(id => schema.inheritance.find(item => item.parent === id), [selected, schema.inheritance]),
[selected, schema.inheritance] [selected, schema.inheritance]
); );

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useMemo, useState } from 'react'; import { useState } from 'react';
import { IconRemove } from '@/components/Icons'; import { IconRemove } from '@/components/Icons';
import SelectUser from '@/components/select/SelectUser'; import SelectUser from '@/components/select/SelectUser';
@ -22,20 +22,19 @@ interface DlgEditEditorsProps {
function DlgEditEditors({ hideWindow, editors, setEditors }: DlgEditEditorsProps) { function DlgEditEditors({ hideWindow, editors, setEditors }: DlgEditEditorsProps) {
const [selected, setSelected] = useState<UserID[]>(editors); const [selected, setSelected] = useState<UserID[]>(editors);
const { users } = useUsers(); const { users } = useUsers();
const filtered = useMemo(() => users.filter(user => !selected.includes(user.id)), [users, selected]); const filtered = users.filter(user => !selected.includes(user.id));
function handleSubmit() { function handleSubmit() {
setEditors(selected); setEditors(selected);
} }
const onDeleteEditor = useCallback((target: UserID) => setSelected(prev => prev.filter(id => id !== target)), []); function onDeleteEditor(target: UserID) {
setSelected(prev => prev.filter(id => id !== target));
}
const onAddEditor = useCallback((target: UserID) => setSelected(prev => [...prev, target]), []); function onAddEditor(target: UserID) {
setSelected(prev => [...prev, target]);
const usersTable = useMemo( }
() => <TableUsers items={users.filter(user => selected.includes(user.id))} onDelete={onDeleteEditor} />,
[users, selected, onDeleteEditor]
);
return ( return (
<Modal <Modal
@ -58,7 +57,7 @@ function DlgEditEditors({ hideWindow, editors, setEditors }: DlgEditEditorsProps
/> />
</div> </div>
{usersTable} <TableUsers items={users.filter(user => selected.includes(user.id))} onDelete={onDeleteEditor} />
<div className='flex items-center gap-3'> <div className='flex items-center gap-3'>
<Label text='Добавить' /> <Label text='Добавить' />

View File

@ -1,7 +1,5 @@
'use client'; 'use client';
import { useMemo } from 'react';
import { IconRemove } from '@/components/Icons'; import { IconRemove } from '@/components/Icons';
import DataTable, { createColumnHelper } from '@/components/ui/DataTable'; import DataTable, { createColumnHelper } from '@/components/ui/DataTable';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
@ -15,36 +13,33 @@ interface TableUsersProps {
const columnHelper = createColumnHelper<IUserInfo>(); const columnHelper = createColumnHelper<IUserInfo>();
function TableUsers({ items, onDelete }: TableUsersProps) { function TableUsers({ items, onDelete }: TableUsersProps) {
const columns = useMemo( const columns = [
() => [ columnHelper.accessor('last_name', {
columnHelper.accessor('last_name', { id: 'last_name',
id: 'last_name', size: 400,
size: 400, header: 'Фамилия'
header: 'Фамилия' }),
}), columnHelper.accessor('first_name', {
columnHelper.accessor('first_name', { id: 'first_name',
id: 'first_name', size: 400,
size: 400, header: 'Имя'
header: 'Имя' }),
}), columnHelper.display({
columnHelper.display({ id: 'actions',
id: 'actions', size: 0,
size: 0, cell: props => (
cell: props => ( <div className='h-[1.25rem] w-[1.25rem]'>
<div className='h-[1.25rem] w-[1.25rem]'> <MiniButton
<MiniButton title='Удалить из списка'
title='Удалить из списка' noHover
noHover noPadding
noPadding icon={<IconRemove size='1.25rem' className='icon-red' />}
icon={<IconRemove size='1.25rem' className='icon-red' />} onClick={() => onDelete(props.row.original.id)}
onClick={() => onDelete(props.row.original.id)} />
/> </div>
</div> )
) })
}) ];
],
[onDelete]
);
return ( return (
<DataTable <DataTable

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { TabList, TabPanel, Tabs } from 'react-tabs'; import { TabList, TabPanel, Tabs } from 'react-tabs';
import Modal from '@/components/ui/Modal'; import Modal from '@/components/ui/Modal';
@ -47,9 +47,9 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
const [isCorrect, setIsCorrect] = useState(true); const [isCorrect, setIsCorrect] = useState(true);
const [validationText, setValidationText] = useState(''); const [validationText, setValidationText] = useState('');
const initialInputs = useMemo(() => oss.graph.expandInputs([target.id]), [oss.graph, target.id]); const initialInputs = oss.graph.expandInputs([target.id]);
const [inputs, setInputs] = useState<OperationID[]>(initialInputs); const [inputs, setInputs] = useState<OperationID[]>(initialInputs);
const inputOperations = useMemo(() => inputs.map(id => oss.operationByID.get(id)!), [inputs, oss.operationByID]); const inputOperations = inputs.map(id => oss.operationByID.get(id)!);
const [needPreload, setNeedPreload] = useState(false); const [needPreload, setNeedPreload] = useState(false);
const [schemasIDs, setSchemaIDs] = useState<LibraryItemID[]>([]); const [schemasIDs, setSchemaIDs] = useState<LibraryItemID[]>([]);
@ -58,33 +58,16 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
const [suggestions, setSuggestions] = useState<ICstSubstitute[]>([]); const [suggestions, setSuggestions] = useState<ICstSubstitute[]>([]);
const cache = useRSFormCache(); const cache = useRSFormCache();
const schemas = useMemo( const schemas = schemasIDs.map(id => cache.data.find(item => item.id === id)).filter(item => item !== undefined);
() => schemasIDs.map(id => cache.data.find(item => item.id === id)).filter(item => item !== undefined),
[schemasIDs, cache.data]
);
const isModified = useMemo( const isModified =
() => alias !== target.alias ||
alias !== target.alias || title !== target.title ||
title !== target.title || comment !== target.comment ||
comment !== target.comment || JSON.stringify(initialInputs) !== JSON.stringify(inputs) ||
JSON.stringify(initialInputs) !== JSON.stringify(inputs) || JSON.stringify(substitutions) !== JSON.stringify(target.substitutions);
JSON.stringify(substitutions) !== JSON.stringify(target.substitutions),
[
alias,
title,
comment,
target.alias,
target.title,
target.comment,
initialInputs,
inputs,
substitutions,
target.substitutions
]
);
const canSubmit = useMemo(() => isModified && alias !== '', [isModified, alias]); const canSubmit = isModified && alias !== '';
const getSchemaByCst = useCallback( const getSchemaByCst = useCallback(
(id: ConstituentaID) => { (id: ConstituentaID) => {
@ -140,7 +123,7 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
setSuggestions(validator.suggestions); setSuggestions(validator.suggestions);
}, [substitutions, cache.loading, schemas, schemasIDs.length]); }, [substitutions, cache.loading, schemas, schemasIDs.length]);
const handleSubmit = useCallback(() => { function handleSubmit() {
const data: IOperationUpdateData = { const data: IOperationUpdateData = {
target: target.id, target: target.id,
item_data: { item_data: {
@ -153,55 +136,7 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
substitutions: target.operation_type !== OperationType.SYNTHESIS ? undefined : substitutions substitutions: target.operation_type !== OperationType.SYNTHESIS ? undefined : substitutions
}; };
onSubmit(data); onSubmit(data);
}, [alias, comment, title, inputs, substitutions, target, onSubmit]); }
const cardPanel = useMemo(
() => (
<TabPanel>
<TabOperation
alias={alias}
onChangeAlias={setAlias}
comment={comment}
onChangeComment={setComment}
title={title}
onChangeTitle={setTitle}
/>
</TabPanel>
),
[alias, comment, title, setAlias]
);
const argumentsPanel = useMemo(
() => (
<TabPanel>
<TabArguments
target={target.id} // prettier: split-lines
oss={oss}
inputs={inputs}
setInputs={setInputs}
/>
</TabPanel>
),
[oss, target, inputs, setInputs]
);
const synthesisPanel = useMemo(
() => (
<TabPanel>
<TabSynthesis
schemas={schemas}
loading={cache.loading}
error={cache.error}
validationText={validationText}
isCorrect={isCorrect}
substitutions={substitutions}
setSubstitutions={setSubstitutions}
suggestions={suggestions}
/>
</TabPanel>
),
[cache.loading, cache.error, substitutions, suggestions, schemas, validationText, isCorrect]
);
return ( return (
<Modal <Modal
@ -234,9 +169,41 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
) : null} ) : null}
</TabList> </TabList>
{cardPanel} <TabPanel>
{target.operation_type === OperationType.SYNTHESIS ? argumentsPanel : null} <TabOperation
{target.operation_type === OperationType.SYNTHESIS ? synthesisPanel : null} alias={alias}
onChangeAlias={setAlias}
comment={comment}
onChangeComment={setComment}
title={title}
onChangeTitle={setTitle}
/>
</TabPanel>
{target.operation_type === OperationType.SYNTHESIS ? (
<TabPanel>
<TabArguments
target={target.id} // prettier: split-lines
oss={oss}
inputs={inputs}
setInputs={setInputs}
/>
</TabPanel>
) : null}
{target.operation_type === OperationType.SYNTHESIS ? (
<TabPanel>
<TabSynthesis
schemas={schemas}
loading={cache.loading}
error={cache.error}
validationText={validationText}
isCorrect={isCorrect}
substitutions={substitutions}
setSubstitutions={setSubstitutions}
suggestions={suggestions}
/>
</TabPanel>
) : null}
</Tabs> </Tabs>
</Modal> </Modal>
); );

View File

@ -1,7 +1,5 @@
'use client'; 'use client';
import { useMemo } from 'react';
import PickMultiOperation from '@/components/select/PickMultiOperation'; import PickMultiOperation from '@/components/select/PickMultiOperation';
import FlexColumn from '@/components/ui/FlexColumn'; import FlexColumn from '@/components/ui/FlexColumn';
import Label from '@/components/ui/Label'; import Label from '@/components/ui/Label';
@ -15,11 +13,8 @@ interface TabArgumentsProps {
} }
function TabArguments({ oss, inputs, target, setInputs }: TabArgumentsProps) { function TabArguments({ oss, inputs, target, setInputs }: TabArgumentsProps) {
const potentialCycle = useMemo(() => [target, ...oss.graph.expandAllOutputs([target])], [target, oss.graph]); const potentialCycle = [target, ...oss.graph.expandAllOutputs([target])];
const filtered = useMemo( const filtered = oss.items.filter(item => !potentialCycle.includes(item.id));
() => oss.items.filter(item => !potentialCycle.includes(item.id)),
[oss.items, potentialCycle]
);
return ( return (
<div className='cc-fade-in cc-column'> <div className='cc-fade-in cc-column'>
<FlexColumn> <FlexColumn>

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useMemo, useState } from 'react'; import { useState } from 'react';
import { TabList, TabPanel, Tabs } from 'react-tabs'; import { TabList, TabPanel, Tabs } from 'react-tabs';
import Modal from '@/components/ui/Modal'; import Modal from '@/components/ui/Modal';
@ -36,42 +36,16 @@ export enum TabID {
function DlgEditReference({ hideWindow, schema, initial, onSave }: DlgEditReferenceProps) { function DlgEditReference({ hideWindow, schema, initial, onSave }: DlgEditReferenceProps) {
const [activeTab, setActiveTab] = useState(initial.type === ReferenceType.ENTITY ? TabID.ENTITY : TabID.SYNTACTIC); const [activeTab, setActiveTab] = useState(initial.type === ReferenceType.ENTITY ? TabID.ENTITY : TabID.SYNTACTIC);
const [reference, setReference] = useState(''); const [reference, setReference] = useState('');
const [isValid, setIsValid] = useState(false); const [isValid, setIsValid] = useState(false);
const handleSubmit = () => onSave(reference);
const entityPanel = useMemo(
() => (
<TabPanel>
<TabEntityReference
initial={initial}
schema={schema}
onChangeReference={setReference}
onChangeValid={setIsValid}
/>
</TabPanel>
),
[initial, schema]
);
const syntacticPanel = useMemo(
() => (
<TabPanel>
<TabSyntacticReference initial={initial} onChangeReference={setReference} onChangeValid={setIsValid} />
</TabPanel>
),
[initial]
);
return ( return (
<Modal <Modal
header='Редактирование ссылки' header='Редактирование ссылки'
submitText='Сохранить ссылку' submitText='Сохранить ссылку'
hideWindow={hideWindow} hideWindow={hideWindow}
canSubmit={isValid} canSubmit={isValid}
onSubmit={handleSubmit} onSubmit={() => onSave(reference)}
className='w-[40rem] px-6 h-[32rem]' className='w-[40rem] px-6 h-[32rem]'
helpTopic={HelpTopic.TERM_CONTROL} helpTopic={HelpTopic.TERM_CONTROL}
> >
@ -89,8 +63,18 @@ function DlgEditReference({ hideWindow, schema, initial, onSave }: DlgEditRefere
/> />
</TabList> </TabList>
{entityPanel} <TabPanel>
{syntacticPanel} <TabEntityReference
initial={initial}
schema={schema}
onChangeReference={setReference}
onChangeValid={setIsValid}
/>
</TabPanel>
<TabPanel>
<TabSyntacticReference initial={initial} onChangeReference={setReference} onChangeValid={setIsValid} />
</TabPanel>
</Tabs> </Tabs>
</Modal> </Modal>
); );

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import { ReferenceType } from '@/models/language'; import { ReferenceType } from '@/models/language';
@ -18,14 +18,14 @@ function TabSyntacticReference({ initial, onChangeValid, onChangeReference }: Ta
const [nominal, setNominal] = useState(''); const [nominal, setNominal] = useState('');
const [offset, setOffset] = useState(1); const [offset, setOffset] = useState(1);
const mainLink = useMemo(() => { const mainLink = (() => {
const position = offset > 0 ? initial.basePosition + (offset - 1) : initial.basePosition + offset; const position = offset > 0 ? initial.basePosition + (offset - 1) : initial.basePosition + offset;
if (offset === 0 || position < 0 || position >= initial.mainRefs.length) { if (offset === 0 || position < 0 || position >= initial.mainRefs.length) {
return 'Некорректное значение смещения'; return 'Некорректное значение смещения';
} else { } else {
return initial.mainRefs[position]; return initial.mainRefs[position];
} }
}, [initial, offset]); })();
useEffect(() => { useEffect(() => {
if (initial.refRaw && initial.type === ReferenceType.SYNTACTIC) { if (initial.refRaw && initial.type === ReferenceType.SYNTACTIC) {

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import { IconReset, IconSave } from '@/components/Icons'; import { IconReset, IconSave } from '@/components/Icons';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
@ -26,19 +26,8 @@ function DlgEditVersions({ hideWindow, versions, onDelete, onUpdate }: DlgEditVe
const [version, setVersion] = useState(''); const [version, setVersion] = useState('');
const [description, setDescription] = useState(''); const [description, setDescription] = useState('');
const isValid = useMemo(() => { const isValid = selected && versions.every(ver => ver.id === selected.id || ver.version != version);
if (!selected) { const isModified = selected && (selected.version != version || selected.description != description);
return false;
}
return versions.every(ver => ver.id === selected.id || ver.version != version);
}, [selected, version, versions]);
const isModified = useMemo(() => {
if (!selected) {
return false;
}
return selected.version != version || selected.description != description;
}, [version, description, selected]);
function handleUpdate() { function handleUpdate() {
if (!isModified || !selected || processing || !isValid) { if (!isModified || !selected || processing || !isValid) {
@ -64,19 +53,6 @@ function DlgEditVersions({ hideWindow, versions, onDelete, onUpdate }: DlgEditVe
setDescription(selected?.description ?? ''); setDescription(selected?.description ?? '');
}, [selected]); }, [selected]);
const versionsTable = useMemo(
() => (
<TableVersions
processing={processing}
items={versions}
onDelete={onDelete}
onSelect={versionID => setSelected(versions.find(ver => ver.id === versionID))}
selected={selected?.id}
/>
),
[processing, versions, onDelete, selected?.id]
);
return ( return (
<Modal <Modal
readonly readonly
@ -84,7 +60,14 @@ function DlgEditVersions({ hideWindow, versions, onDelete, onUpdate }: DlgEditVe
hideWindow={hideWindow} hideWindow={hideWindow}
className='flex flex-col w-[40rem] px-6 gap-3 pb-6' className='flex flex-col w-[40rem] px-6 gap-3 pb-6'
> >
{versionsTable} <TableVersions
processing={processing}
items={versions}
onDelete={onDelete}
onSelect={versionID => setSelected(versions.find(ver => ver.id === versionID))}
selected={selected?.id}
/>
<div className='flex'> <div className='flex'>
<TextInput <TextInput
id='dlg_version' id='dlg_version'

View File

@ -1,7 +1,6 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useMemo } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { IconRemove } from '@/components/Icons'; import { IconRemove } from '@/components/Icons';
@ -24,67 +23,61 @@ function TableVersions({ processing, items, onDelete, selected, onSelect }: Tabl
const intl = useIntl(); const intl = useIntl();
const { colors } = useConceptOptions(); const { colors } = useConceptOptions();
const columns = useMemo( const columns = [
() => [ columnHelper.accessor('version', {
columnHelper.accessor('version', { id: 'version',
id: 'version', header: 'Версия',
header: 'Версия', cell: props => <div className='min-w-[6rem] max-w-[6rem] text-ellipsis'>{props.getValue()}</div>
cell: props => <div className='min-w-[6rem] max-w-[6rem] text-ellipsis'>{props.getValue()}</div> }),
}), columnHelper.accessor('description', {
columnHelper.accessor('description', { id: 'description',
id: 'description', header: 'Описание',
header: 'Описание', size: 800,
size: 800, minSize: 800,
minSize: 800, maxSize: 800,
maxSize: 800, cell: props => <div className='text-ellipsis'>{props.getValue()}</div>
cell: props => <div className='text-ellipsis'>{props.getValue()}</div> }),
}), columnHelper.accessor('time_create', {
columnHelper.accessor('time_create', { id: 'time_create',
id: 'time_create', header: 'Дата создания',
header: 'Дата создания', cell: props => (
cell: props => ( <div className='whitespace-nowrap'>
<div className='whitespace-nowrap'> {new Date(props.getValue()).toLocaleString(intl.locale, {
{new Date(props.getValue()).toLocaleString(intl.locale, { year: '2-digit',
year: '2-digit', month: '2-digit',
month: '2-digit', day: '2-digit',
day: '2-digit', hour: '2-digit',
hour: '2-digit', minute: '2-digit'
minute: '2-digit' })}
})} </div>
</div> )
) }),
}), columnHelper.display({
columnHelper.display({ id: 'actions',
id: 'actions', size: 0,
size: 0, cell: props => (
cell: props => ( <div className='h-[1.25rem] w-[1.25rem]'>
<div className='h-[1.25rem] w-[1.25rem]'> <MiniButton
<MiniButton title='Удалить версию'
title='Удалить версию' noHover
noHover noPadding
noPadding disabled={processing}
disabled={processing} icon={<IconRemove size='1.25rem' className='icon-red' />}
icon={<IconRemove size='1.25rem' className='icon-red' />} onClick={() => onDelete(props.row.original.id)}
onClick={() => onDelete(props.row.original.id)} />
/> </div>
</div> )
) })
}) ];
],
[onDelete, intl, processing]
);
const conditionalRowStyles = useMemo( const conditionalRowStyles: IConditionalStyle<IVersionInfo>[] = [
(): IConditionalStyle<IVersionInfo>[] => [ {
{ when: (version: IVersionInfo) => version.id === selected,
when: (version: IVersionInfo) => version.id === selected, style: {
style: { backgroundColor: colors.bgSelected
backgroundColor: colors.bgSelected
}
} }
], }
[selected, colors] ];
);
return ( return (
<DataTable <DataTable

View File

@ -1,7 +1,6 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useMemo } from 'react';
import { IconRemove } from '@/components/Icons'; import { IconRemove } from '@/components/Icons';
import BadgeWordForm from '@/components/info/BadgeWordForm'; import BadgeWordForm from '@/components/info/BadgeWordForm';
@ -19,53 +18,47 @@ interface TableWordFormsProps {
const columnHelper = createColumnHelper<IWordForm>(); const columnHelper = createColumnHelper<IWordForm>();
function TableWordForms({ forms, setForms, onFormSelect }: TableWordFormsProps) { function TableWordForms({ forms, setForms, onFormSelect }: TableWordFormsProps) {
const handleDeleteRow = useCallback( function handleDeleteRow(row: number) {
(row: number) => { setForms(prev => {
setForms(prev => { const newForms: IWordForm[] = [];
const newForms: IWordForm[] = []; prev.forEach((form, index) => {
prev.forEach((form, index) => { if (index !== row) {
if (index !== row) { newForms.push(form);
newForms.push(form); }
}
});
return newForms;
}); });
}, return newForms;
[setForms] });
); }
const columns = useMemo( const columns = [
() => [ columnHelper.accessor('text', {
columnHelper.accessor('text', { id: 'text',
id: 'text', size: 350,
size: 350, minSize: 500,
minSize: 500, maxSize: 500,
maxSize: 500, cell: props => <div className='min-w-[25rem]'>{props.getValue()}</div>
cell: props => <div className='min-w-[25rem]'>{props.getValue()}</div> }),
}), columnHelper.accessor('grams', {
columnHelper.accessor('grams', { id: 'grams',
id: 'grams', size: 0,
size: 0, cell: props => <BadgeWordForm keyPrefix={props.cell.id} form={props.row.original} />
cell: props => <BadgeWordForm keyPrefix={props.cell.id} form={props.row.original} /> }),
}), columnHelper.display({
columnHelper.display({ id: 'actions',
id: 'actions', size: 0,
size: 0, cell: props => (
cell: props => ( <div className='h-[1.25rem] w-[1.25rem]'>
<div className='h-[1.25rem] w-[1.25rem]'> <MiniButton
<MiniButton noHover
noHover noPadding
noPadding title='Удалить словоформу'
title='Удалить словоформу' icon={<IconRemove size='1.25rem' className='icon-red' />}
icon={<IconRemove size='1.25rem' className='icon-red' />} onClick={() => handleDeleteRow(props.row.index)}
onClick={() => handleDeleteRow(props.row.index)} />
/> </div>
</div> )
) })
}) ];
],
[handleDeleteRow]
);
return ( return (
<DataTable <DataTable

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import { TabList, TabPanel, Tabs } from 'react-tabs'; import { TabList, TabPanel, Tabs } from 'react-tabs';
import Modal, { ModalProps } from '@/components/ui/Modal'; import Modal, { ModalProps } from '@/components/ui/Modal';
@ -35,7 +35,7 @@ function DlgInlineSynthesis({ hideWindow, receiver, onInlineSynthesis }: DlgInli
const source = useRSFormDetails({ target: donorID ? String(donorID) : undefined }); const source = useRSFormDetails({ target: donorID ? String(donorID) : undefined });
const validated = useMemo(() => !!source.schema && selected.length > 0, [source.schema, selected]); const validated = !!source.schema && selected.length > 0;
function handleSubmit() { function handleSubmit() {
if (!source.schema) { if (!source.schema) {
@ -55,43 +55,6 @@ function DlgInlineSynthesis({ hideWindow, receiver, onInlineSynthesis }: DlgInli
setSubstitutions([]); setSubstitutions([]);
}, [source.schema]); }, [source.schema]);
const schemaPanel = useMemo(
() => (
<TabPanel>
<TabSchema selected={donorID} setSelected={setDonorID} receiver={receiver} />
</TabPanel>
),
[donorID, receiver]
);
const itemsPanel = useMemo(
() => (
<TabPanel>
<TabConstituents
schema={source.schema}
loading={source.loading}
selected={selected}
setSelected={setSelected}
/>
</TabPanel>
),
[source.schema, source.loading, selected]
);
const substitutesPanel = useMemo(
() => (
<TabPanel>
<TabSubstitutions
receiver={receiver}
source={source.schema}
selected={selected}
loading={source.loading}
substitutions={substitutions}
setSubstitutions={setSubstitutions}
/>
</TabPanel>
),
[source.schema, source.loading, receiver, selected, substitutions]
);
return ( return (
<Modal <Modal
header='Импорт концептуальной схем' header='Импорт концептуальной схем'
@ -113,9 +76,29 @@ function DlgInlineSynthesis({ hideWindow, receiver, onInlineSynthesis }: DlgInli
<TabLabel label='Отождествления' title='Таблица отождествлений' className='w-[8rem]' /> <TabLabel label='Отождествления' title='Таблица отождествлений' className='w-[8rem]' />
</TabList> </TabList>
{schemaPanel} <TabPanel>
{itemsPanel} <TabSchema selected={donorID} setSelected={setDonorID} receiver={receiver} />
{substitutesPanel} </TabPanel>
<TabPanel>
<TabConstituents
schema={source.schema}
loading={source.loading}
selected={selected}
setSelected={setSelected}
/>
</TabPanel>
<TabPanel>
<TabSubstitutions
receiver={receiver}
source={source.schema}
selected={selected}
loading={source.loading}
substitutions={substitutions}
setSubstitutions={setSubstitutions}
/>
</TabPanel>
</Tabs> </Tabs>
</Modal> </Modal>
); );

View File

@ -1,7 +1,5 @@
'use client'; 'use client';
import { useMemo } from 'react';
import PickSchema from '@/components/select/PickSchema'; import PickSchema from '@/components/select/PickSchema';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
@ -17,8 +15,8 @@ interface TabSchemaProps {
function TabSchema({ selected, receiver, setSelected }: TabSchemaProps) { function TabSchema({ selected, receiver, setSelected }: TabSchemaProps) {
const library = useLibrary(); const library = useLibrary();
const selectedInfo = useMemo(() => library.items.find(item => item.id === selected), [selected, library.items]); const selectedInfo = library.items.find(item => item.id === selected);
const sortedItems = useMemo(() => sortItemsForInlineSynthesis(receiver, library.items), [receiver, library.items]); const sortedItems = sortItemsForInlineSynthesis(receiver, library.items);
return ( return (
<div className='cc-fade-in flex flex-col'> <div className='cc-fade-in flex flex-col'>

View File

@ -1,12 +1,10 @@
'use client'; 'use client';
import { useCallback, useMemo } from 'react';
import { ErrorData } from '@/components/info/InfoError'; import { ErrorData } from '@/components/info/InfoError';
import PickSubstitutions from '@/components/select/PickSubstitutions'; import PickSubstitutions from '@/components/select/PickSubstitutions';
import DataLoader from '@/components/wrap/DataLoader'; import DataLoader from '@/components/wrap/DataLoader';
import { ICstSubstitute } from '@/models/oss'; import { ICstSubstitute } from '@/models/oss';
import { ConstituentaID, IConstituenta, IRSForm } from '@/models/rsform'; import { ConstituentaID, IRSForm } from '@/models/rsform';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
interface TabSubstitutionsProps { interface TabSubstitutionsProps {
@ -32,12 +30,7 @@ function TabSubstitutions({
substitutions, substitutions,
setSubstitutions setSubstitutions
}: TabSubstitutionsProps) { }: TabSubstitutionsProps) {
const filter = useCallback( const schemas = [...(source ? [source] : []), ...(receiver ? [receiver] : [])];
(cst: IConstituenta) => cst.id !== source?.id || selected.includes(cst.id),
[selected, source]
);
const schemas = useMemo(() => [...(source ? [source] : []), ...(receiver ? [receiver] : [])], [source, receiver]);
return ( return (
<DataLoader isLoading={loading} error={error} hasNoData={!source}> <DataLoader isLoading={loading} error={error} hasNoData={!source}>
@ -47,7 +40,7 @@ function TabSubstitutions({
rows={10} rows={10}
prefixID={prefixes.cst_inline_synth_substitutes} prefixID={prefixes.cst_inline_synth_substitutes}
schemas={schemas} schemas={schemas}
filter={filter} filter={cst => cst.id !== source?.id || selected.includes(cst.id)}
/> />
</DataLoader> </DataLoader>
); );

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useMemo, useState } from 'react'; import { useState } from 'react';
import { RelocateUpIcon } from '@/components/DomainIcons'; import { RelocateUpIcon } from '@/components/DomainIcons';
import PickMultiConstituenta from '@/components/select/PickMultiConstituenta'; import PickMultiConstituenta from '@/components/select/PickMultiConstituenta';
@ -33,10 +33,11 @@ function DlgRelocateConstituents({ oss, hideWindow, initialTarget, onSubmit }: D
const [source, setSource] = useState<ILibraryItem | undefined>( const [source, setSource] = useState<ILibraryItem | undefined>(
library.items.find(item => item.id === initialTarget?.result) library.items.find(item => item.id === initialTarget?.result)
); );
const isValid = !!destination && selected.length > 0;
const operation = useMemo(() => oss.items.find(item => item.result === source?.id), [oss, source]); const operation = oss.items.find(item => item.result === source?.id);
const sourceSchemas = useMemo(() => library.items.filter(item => oss.schemas.includes(item.id)), [library, oss]); const sourceSchemas = library.items.filter(item => oss.schemas.includes(item.id));
const destinationSchemas = useMemo(() => { const destinationSchemas = (() => {
if (!operation) { if (!operation) {
return []; return [];
} }
@ -45,35 +46,34 @@ function DlgRelocateConstituents({ oss, hideWindow, initialTarget, onSubmit }: D
? node.inputs.map(id => oss.operationByID.get(id)!.result).filter(id => id !== null) ? node.inputs.map(id => oss.operationByID.get(id)!.result).filter(id => id !== null)
: node.outputs.map(id => oss.operationByID.get(id)!.result).filter(id => id !== null); : node.outputs.map(id => oss.operationByID.get(id)!.result).filter(id => id !== null);
return ids.map(id => library.items.find(item => item.id === id)).filter(item => item !== undefined); return ids.map(id => library.items.find(item => item.id === id)).filter(item => item !== undefined);
}, [oss, library.items, operation, directionUp]); })();
const sourceData = useRSFormDetails({ target: source ? String(source.id) : undefined }); const sourceData = useRSFormDetails({ target: source ? String(source.id) : undefined });
const filteredConstituents = useMemo(() => { const filteredConstituents = (() => {
if (!sourceData.schema || !destination || !operation) { if (!sourceData.schema || !destination || !operation) {
return []; return [];
} }
const destinationOperation = oss.items.find(item => item.result === destination.id); const destinationOperation = oss.items.find(item => item.result === destination.id);
return getRelocateCandidates(operation.id, destinationOperation!.id, sourceData.schema, oss); return getRelocateCandidates(operation.id, destinationOperation!.id, sourceData.schema, oss);
}, [destination, operation, sourceData.schema, oss]); })();
const isValid = useMemo(() => !!destination && selected.length > 0, [destination, selected]); function toggleDirection() {
const toggleDirection = useCallback(() => {
setDirectionUp(prev => !prev); setDirectionUp(prev => !prev);
setDestination(undefined); setDestination(undefined);
}, []); }
const handleSelectSource = useCallback((newValue: ILibraryItem | undefined) => { function handleSelectSource(newValue: ILibraryItem | undefined) {
setSource(newValue); setSource(newValue);
setDestination(undefined); setDestination(undefined);
setSelected([]); setSelected([]);
}, []); }
const handleSelectDestination = useCallback((newValue: ILibraryItem | undefined) => { function handleSelectDestination(newValue: ILibraryItem | undefined) {
setDestination(newValue); setDestination(newValue);
setSelected([]); setSelected([]);
}, []); }
const handleSubmit = useCallback(() => { function handleSubmit() {
if (!destination) { if (!destination) {
return; return;
} }
@ -82,7 +82,7 @@ function DlgRelocateConstituents({ oss, hideWindow, initialTarget, onSubmit }: D
items: selected items: selected
}; };
onSubmit(data); onSubmit(data);
}, [destination, onSubmit, selected]); }
return ( return (
<Modal <Modal

View File

@ -1,8 +1,7 @@
'use client'; 'use client';
import { useCallback, useMemo, useState } from 'react'; import { useState } from 'react';
import { ReactFlowProvider } from 'reactflow'; import { ReactFlowProvider } from 'reactflow';
import { Node } from 'reactflow';
import Modal, { ModalProps } from '@/components/ui/Modal'; import Modal, { ModalProps } from '@/components/ui/Modal';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
@ -20,10 +19,7 @@ interface DlgShowASTProps extends Pick<ModalProps, 'hideWindow'> {
function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) { function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
const { colors } = useConceptOptions(); const { colors } = useConceptOptions();
const [hoverID, setHoverID] = useState<number | undefined>(undefined); const [hoverID, setHoverID] = useState<number | undefined>(undefined);
const hoverNode = useMemo(() => syntaxTree.find(node => node.uid === hoverID), [hoverID, syntaxTree]); const hoverNode = syntaxTree.find(node => node.uid === hoverID);
const handleHoverIn = useCallback((node: Node) => setHoverID(Number(node.id)), []);
const handleHoverOut = useCallback(() => setHoverID(undefined), []);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
@ -51,8 +47,8 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
<ReactFlowProvider> <ReactFlowProvider>
<ASTFlow <ASTFlow
data={syntaxTree} data={syntaxTree}
onNodeEnter={handleHoverIn} onNodeEnter={node => setHoverID(Number(node.id))}
onNodeLeave={handleHoverOut} onNodeLeave={() => setHoverID(undefined)}
onChangeDragging={setIsDragging} onChangeDragging={setIsDragging}
/> />
</ReactFlowProvider> </ReactFlowProvider>

View File

@ -1,6 +1,5 @@
'use client'; 'use client';
import { useMemo } from 'react';
import { Handle, Position } from 'reactflow'; import { Handle, Position } from 'reactflow';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
@ -27,7 +26,7 @@ interface ASTNodeInternal {
function ASTNode(node: ASTNodeInternal) { function ASTNode(node: ASTNodeInternal) {
const { colors } = useConceptOptions(); const { colors } = useConceptOptions();
const label = useMemo(() => labelSyntaxTree(node.data), [node.data]); const label = labelSyntaxTree(node.data);
return ( return (
<> <>

View File

@ -1,6 +1,5 @@
'use client'; 'use client';
import { useMemo } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { ReactFlowProvider } from 'reactflow'; import { ReactFlowProvider } from 'reactflow';
@ -17,11 +16,11 @@ interface DlgShowTypeGraphProps extends Pick<ModalProps, 'hideWindow'> {
} }
function DlgShowTypeGraph({ hideWindow, items }: DlgShowTypeGraphProps) { function DlgShowTypeGraph({ hideWindow, items }: DlgShowTypeGraphProps) {
const graph = useMemo(() => { const graph = (() => {
const result = new TMGraph(); const result = new TMGraph();
items.forEach(item => result.addConstituenta(item.alias, item.result, item.args)); items.forEach(item => result.addConstituenta(item.alias, item.result, item.args));
return result; return result;
}, [items]); })();
if (graph.nodes.length === 0) { if (graph.nodes.length === 0) {
toast.error(errors.typeStructureFailed); toast.error(errors.typeStructureFailed);

View File

@ -1,6 +1,5 @@
'use client'; 'use client';
import { useMemo } from 'react';
import { Handle, Position } from 'reactflow'; import { Handle, Position } from 'reactflow';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
@ -22,12 +21,9 @@ interface MGraphNodeInternal {
function MGraphNode(node: MGraphNodeInternal) { function MGraphNode(node: MGraphNodeInternal) {
const { colors } = useConceptOptions(); const { colors } = useConceptOptions();
const tooltipText = useMemo( const tooltipText =
() => (node.data.annotations.length === 0 ? '' : `Конституенты: ${node.data.annotations.join(' ')}<br/>`) +
(node.data.annotations.length === 0 ? '' : `Конституенты: ${node.data.annotations.join(' ')}<br/>`) + node.data.text;
node.data.text,
[node.data]
);
return ( return (
<> <>

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useMemo, useState } from 'react'; import { useState } from 'react';
import PickSubstitutions from '@/components/select/PickSubstitutions'; import PickSubstitutions from '@/components/select/PickSubstitutions';
import Modal, { ModalProps } from '@/components/ui/Modal'; import Modal, { ModalProps } from '@/components/ui/Modal';
@ -17,8 +17,7 @@ interface DlgSubstituteCstProps extends Pick<ModalProps, 'hideWindow'> {
function DlgSubstituteCst({ hideWindow, onSubstitute, schema }: DlgSubstituteCstProps) { function DlgSubstituteCst({ hideWindow, onSubstitute, schema }: DlgSubstituteCstProps) {
const [substitutions, setSubstitutions] = useState<ICstSubstitute[]>([]); const [substitutions, setSubstitutions] = useState<ICstSubstitute[]>([]);
const canSubmit = substitutions.length > 0;
const canSubmit = useMemo(() => substitutions.length > 0, [substitutions]);
function handleSubmit() { function handleSubmit() {
const data: ICstSubstituteData = { const data: ICstSubstituteData = {

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { urls } from '@/app/urls'; import { urls } from '@/app/urls';
@ -45,8 +45,8 @@ function FormCreateItem() {
const [head, setHead] = useState(LocationHead.USER); const [head, setHead] = useState(LocationHead.USER);
const [body, setBody] = useState(''); const [body, setBody] = useState('');
const location = useMemo(() => combineLocation(head, body), [head, body]); const location = combineLocation(head, body);
const isValid = useMemo(() => validateLocation(location), [location]); const isValid = validateLocation(location);
const [fileName, setFileName] = useState(''); const [fileName, setFileName] = useState('');
const [file, setFile] = useState<File | undefined>(); const [file, setFile] = useState<File | undefined>();

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useEffect, useMemo } from 'react'; import { useEffect } from 'react';
import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch'; import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
@ -9,7 +9,7 @@ import { resources } from '@/utils/constants';
function DatabaseSchemaPage() { function DatabaseSchemaPage() {
const { calculateHeight, setNoFooter } = useConceptOptions(); const { calculateHeight, setNoFooter } = useConceptOptions();
const panelHeight = useMemo(() => calculateHeight('0px'), [calculateHeight]); const panelHeight = calculateHeight('0px');
useEffect(() => { useEffect(() => {
setNoFooter(true); setNoFooter(true);

View File

@ -69,28 +69,21 @@ function LibraryPage() {
] ]
); );
const hasCustomFilter = useMemo( const hasCustomFilter =
() => !!filter.path ||
!!filter.path || !!filter.query ||
!!filter.query || filter.head !== undefined ||
filter.head !== undefined || filter.isEditor !== undefined ||
filter.isEditor !== undefined || filter.isOwned !== undefined ||
filter.isOwned !== undefined || filter.isVisible !== true ||
filter.isVisible !== true || filter.filterUser !== undefined ||
filter.filterUser !== undefined || !!filter.location;
!!filter.location,
[filter]
);
useEffect(() => { useEffect(() => {
setItems(library.applyFilter(filter)); setItems(library.applyFilter(filter));
}, [library, library.items.length, filter]); }, [library, library.items.length, filter]);
const toggleVisible = useCallback(() => setIsVisible(prev => toggleTristateFlag(prev)), [setIsVisible]); const toggleFolderMode = () => options.setFolderMode(prev => !prev);
const toggleOwned = useCallback(() => setIsOwned(prev => toggleTristateFlag(prev)), [setIsOwned]);
const toggleEditor = useCallback(() => setIsEditor(prev => toggleTristateFlag(prev)), [setIsEditor]);
const toggleFolderMode = useCallback(() => options.setFolderMode(prev => !prev), [options]);
const toggleSubfolders = useCallback(() => setSubfolders(prev => !prev), [setSubfolders]);
const resetFilter = useCallback(() => { const resetFilter = useCallback(() => {
setQuery(''); setQuery('');
@ -103,10 +96,6 @@ function LibraryPage() {
options.setLocation(''); options.setLocation('');
}, [setHead, setIsVisible, setIsOwned, setIsEditor, setFilterUser, options]); }, [setHead, setIsVisible, setIsOwned, setIsEditor, setFilterUser, options]);
const promptRenameLocation = useCallback(() => {
setShowRenameLocation(true);
}, []);
const handleRenameLocation = useCallback( const handleRenameLocation = useCallback(
(newLocation: string) => { (newLocation: string) => {
const data: IRenameLocationData = { const data: IRenameLocationData = {
@ -134,43 +123,6 @@ function LibraryPage() {
} }
}, [items]); }, [items]);
const viewLibrary = useMemo(
() => (
<TableLibraryItems
resetQuery={resetFilter}
items={items}
folderMode={options.folderMode}
toggleFolderMode={toggleFolderMode}
/>
),
[resetFilter, items, options.folderMode, toggleFolderMode]
);
const viewLocations = useMemo(
() => (
<ViewSideLocation
isVisible={options.folderMode}
activeLocation={options.location}
onChangeActiveLocation={options.setLocation}
subfolders={subfolders}
folderTree={library.folders}
toggleFolderMode={toggleFolderMode}
toggleSubfolders={toggleSubfolders}
onRenameLocation={promptRenameLocation}
/>
),
[
options.location,
library.folders,
options.setLocation,
options.folderMode,
toggleFolderMode,
promptRenameLocation,
toggleSubfolders,
subfolders
]
);
return ( return (
<DataLoader isLoading={library.loading} error={library.loadingError} hasNoData={library.items.length === 0}> <DataLoader isLoading={library.loading} error={library.loadingError} hasNoData={library.items.length === 0}>
{showRenameLocation ? ( {showRenameLocation ? (
@ -203,10 +155,10 @@ function LibraryPage() {
onChangeHead={setHead} onChangeHead={setHead}
isVisible={isVisible} isVisible={isVisible}
isOwned={isOwned} isOwned={isOwned}
toggleOwned={toggleOwned} toggleOwned={() => setIsOwned(prev => toggleTristateFlag(prev))}
toggleVisible={toggleVisible} toggleVisible={() => setIsVisible(prev => toggleTristateFlag(prev))}
isEditor={isEditor} isEditor={isEditor}
toggleEditor={toggleEditor} toggleEditor={() => setIsEditor(prev => toggleTristateFlag(prev))}
filterUser={filterUser} filterUser={filterUser}
onChangeFilterUser={setFilterUser} onChangeFilterUser={setFilterUser}
resetFilter={resetFilter} resetFilter={resetFilter}
@ -215,8 +167,23 @@ function LibraryPage() {
/> />
<div className='cc-fade-in flex'> <div className='cc-fade-in flex'>
{viewLocations} <ViewSideLocation
{viewLibrary} isVisible={options.folderMode}
activeLocation={options.location}
onChangeActiveLocation={options.setLocation}
subfolders={subfolders}
folderTree={library.folders}
toggleFolderMode={toggleFolderMode}
toggleSubfolders={() => setSubfolders(prev => !prev)}
onRenameLocation={() => setShowRenameLocation(true)}
/>
<TableLibraryItems
resetQuery={resetFilter}
items={items}
folderMode={options.folderMode}
toggleFolderMode={toggleFolderMode}
/>
</div> </div>
</DataLoader> </DataLoader>
); );

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useLayoutEffect, useMemo, useState } from 'react'; import { useLayoutEffect, useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { urls } from '@/app/urls'; import { urls } from '@/app/urls';
@ -54,106 +54,97 @@ function TableLibraryItems({ items, resetQuery, folderMode, toggleFolderMode }:
}); });
}, [windowSize]); }, [windowSize]);
const handleToggleFolder = useCallback( function handleToggleFolder(event: CProps.EventMouse) {
(event: CProps.EventMouse) => { event.preventDefault();
event.preventDefault(); event.stopPropagation();
event.stopPropagation(); toggleFolderMode();
toggleFolderMode(); }
},
[toggleFolderMode]
);
const columns = useMemo( const columns = [
() => [ ...(folderMode
...(folderMode ? []
? [] : [
: [ columnHelper.accessor('location', {
columnHelper.accessor('location', { id: 'location',
id: 'location', header: () => (
header: () => ( <MiniButton
<MiniButton noPadding
noPadding noHover
noHover className='pl-2 max-h-[1rem] translate-y-[-0.125rem]'
className='pl-2 max-h-[1rem] translate-y-[-0.125rem]' onClick={handleToggleFolder}
onClick={handleToggleFolder} titleHtml='Переключение в режим Проводник'
titleHtml='Переключение в режим Проводник' icon={<IconFolderTree size='1.25rem' className='clr-text-controls' />}
icon={<IconFolderTree size='1.25rem' className='clr-text-controls' />} />
/> ),
), size: 50,
size: 50, minSize: 50,
minSize: 50, maxSize: 50,
maxSize: 50, enableSorting: true,
enableSorting: true, cell: props => <BadgeLocation location={props.getValue()} />,
cell: props => <BadgeLocation location={props.getValue()} />, sortingFn: 'text'
sortingFn: 'text' })
]),
columnHelper.accessor('alias', {
id: 'alias',
header: 'Шифр',
size: 150,
minSize: 80,
maxSize: 150,
enableSorting: true,
cell: props => <div className='min-w-[5rem]'>{props.getValue()}</div>,
sortingFn: 'text'
}),
columnHelper.accessor('title', {
id: 'title',
header: 'Название',
size: 1200,
minSize: 200,
maxSize: 1200,
enableSorting: true,
sortingFn: 'text'
}),
columnHelper.accessor(item => item.owner ?? 0, {
id: 'owner',
header: 'Владелец',
size: 400,
minSize: 100,
maxSize: 400,
cell: props => getUserLabel(props.getValue()),
enableSorting: true,
sortingFn: 'text'
}),
columnHelper.accessor('time_update', {
id: 'time_update',
header: windowSize.isSmall ? 'Дата' : 'Обновлена',
cell: props => (
<div className='whitespace-nowrap'>
{new Date(props.getValue()).toLocaleString(intl.locale, {
year: '2-digit',
month: '2-digit',
day: '2-digit',
...(!windowSize.isSmall && {
hour: '2-digit',
minute: '2-digit'
}) })
]), })}
columnHelper.accessor('alias', { </div>
id: 'alias', ),
header: 'Шифр', enableSorting: true,
size: 150, sortingFn: 'datetime',
minSize: 80, sortDescFirst: true
maxSize: 150, })
enableSorting: true, ];
cell: props => <div className='min-w-[5rem]'>{props.getValue()}</div>,
sortingFn: 'text'
}),
columnHelper.accessor('title', {
id: 'title',
header: 'Название',
size: 1200,
minSize: 200,
maxSize: 1200,
enableSorting: true,
sortingFn: 'text'
}),
columnHelper.accessor(item => item.owner ?? 0, {
id: 'owner',
header: 'Владелец',
size: 400,
minSize: 100,
maxSize: 400,
cell: props => getUserLabel(props.getValue()),
enableSorting: true,
sortingFn: 'text'
}),
columnHelper.accessor('time_update', {
id: 'time_update',
header: windowSize.isSmall ? 'Дата' : 'Обновлена',
cell: props => (
<div className='whitespace-nowrap'>
{new Date(props.getValue()).toLocaleString(intl.locale, {
year: '2-digit',
month: '2-digit',
day: '2-digit',
...(!windowSize.isSmall && {
hour: '2-digit',
minute: '2-digit'
})
})}
</div>
),
enableSorting: true,
sortingFn: 'datetime',
sortDescFirst: true
})
],
[intl, getUserLabel, windowSize, handleToggleFolder, folderMode]
);
const tableHeight = useMemo(() => calculateHeight('2.2rem'), [calculateHeight]); const tableHeight = calculateHeight('2.2rem');
const conditionalRowStyles = useMemo( const conditionalRowStyles: IConditionalStyle<ILibraryItem>[] = [
(): IConditionalStyle<ILibraryItem>[] => [ {
{ when: (item: ILibraryItem) => item.item_type === LibraryItemType.OSS,
when: (item: ILibraryItem) => item.item_type === LibraryItemType.OSS, style: {
style: { color: colors.fgGreen
color: colors.fgGreen
}
} }
], }
[colors] ];
);
return ( return (
<DataTable <DataTable

View File

@ -1,7 +1,6 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useMemo } from 'react';
import { LocationIcon, VisibilityIcon } from '@/components/DomainIcons'; import { LocationIcon, VisibilityIcon } from '@/components/DomainIcons';
import { import {
@ -85,34 +84,25 @@ function ToolbarSearch({
const userMenu = useDropdown(); const userMenu = useDropdown();
const { users } = useUsers(); const { users } = useUsers();
const userActive = useMemo( const userActive = isOwned !== undefined || isEditor !== undefined || filterUser !== undefined;
() => isOwned !== undefined || isEditor !== undefined || filterUser !== undefined,
[isOwned, isEditor, filterUser]
);
const handleChange = useCallback( function handleChange(newValue: LocationHead | undefined) {
(newValue: LocationHead | undefined) => { headMenu.hide();
headMenu.hide(); onChangeHead(newValue);
onChangeHead(newValue); }
},
[headMenu, onChangeHead]
);
const handleToggleFolder = useCallback(() => { function handleToggleFolder() {
headMenu.hide(); headMenu.hide();
toggleFolderMode(); toggleFolderMode();
}, [headMenu, toggleFolderMode]); }
const handleFolderClick = useCallback( function handleFolderClick(event: CProps.EventMouse) {
(event: CProps.EventMouse) => { if (event.ctrlKey || event.metaKey) {
if (event.ctrlKey || event.metaKey) { toggleFolderMode();
toggleFolderMode(); } else {
} else { headMenu.toggle();
headMenu.toggle(); }
} }
},
[headMenu, toggleFolderMode]
);
return ( return (
<div <div

View File

@ -1,5 +1,4 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useMemo } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { SubfoldersIcon } from '@/components/DomainIcons'; import { SubfoldersIcon } from '@/components/DomainIcons';
@ -43,7 +42,7 @@ function ViewSideLocation({
const { calculateHeight } = useConceptOptions(); const { calculateHeight } = useConceptOptions();
const windowSize = useWindowSize(); const windowSize = useWindowSize();
const canRename = useMemo(() => { const canRename = (() => {
if (activeLocation.length <= 3 || !user) { if (activeLocation.length <= 3 || !user) {
return false; return false;
} }
@ -55,25 +54,22 @@ function ViewSideLocation({
item => item.location == activeLocation || item.location.startsWith(`${activeLocation}/`) item => item.location == activeLocation || item.location.startsWith(`${activeLocation}/`)
); );
return located.length !== 0; return located.length !== 0;
}, [activeLocation, user, items]); })();
const maxHeight = useMemo(() => calculateHeight('4.5rem'), [calculateHeight]); const maxHeight = calculateHeight('4.5rem');
const handleClickFolder = useCallback( function handleClickFolder(event: CProps.EventMouse, target: FolderNode) {
(event: CProps.EventMouse, target: FolderNode) => { event.preventDefault();
event.preventDefault(); event.stopPropagation();
event.stopPropagation(); if (event.ctrlKey || event.metaKey) {
if (event.ctrlKey || event.metaKey) { navigator.clipboard
navigator.clipboard .writeText(target.getPath())
.writeText(target.getPath()) .then(() => toast.success(information.pathReady))
.then(() => toast.success(information.pathReady)) .catch(console.error);
.catch(console.error); } else {
} else { onChangeActiveLocation(target.getPath());
onChangeActiveLocation(target.getPath()); }
} }
},
[onChangeActiveLocation]
);
return ( return (
<div <div

View File

@ -1,5 +1,3 @@
import { useMemo } from 'react';
import EmbedYoutube from '@/components/ui/EmbedYoutube'; import EmbedYoutube from '@/components/ui/EmbedYoutube';
import useWindowSize from '@/hooks/useWindowSize'; import useWindowSize from '@/hooks/useWindowSize';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
@ -10,12 +8,12 @@ import Subtopics from '../Subtopics';
function HelpRSLang() { function HelpRSLang() {
const windowSize = useWindowSize(); const windowSize = useWindowSize();
const videoHeight = useMemo(() => { const videoHeight = (() => {
const viewH = windowSize.height ?? 0; const viewH = windowSize.height ?? 0;
const viewW = windowSize.width ?? 0; const viewW = windowSize.width ?? 0;
const availableWidth = viewW - (windowSize.isSmall ? 35 : 310); const availableWidth = viewW - (windowSize.isSmall ? 35 : 310);
return Math.min(1080, Math.max(viewH - 450, 300), Math.floor((availableWidth * 9) / 16)); return Math.min(1080, Math.max(viewH - 450, 300), Math.floor((availableWidth * 9) / 16));
}, [windowSize]); })();
// prettier-ignore // prettier-ignore
return ( return (

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { import {
IconChild, IconChild,
@ -51,7 +51,7 @@ function NodeContextMenu({
const controller = useOssEdit(); const controller = useOssEdit();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const readyForSynthesis = useMemo(() => { const readyForSynthesis = (() => {
if (operation.operation_type !== OperationType.SYNTHESIS) { if (operation.operation_type !== OperationType.SYNTHESIS) {
return false; return false;
} }
@ -70,7 +70,7 @@ function NodeContextMenu({
} }
return true; return true;
}, [operation, controller.schema]); })();
const handleHide = useCallback(() => { const handleHide = useCallback(() => {
setIsOpen(false); setIsOpen(false);

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import { toPng } from 'html-to-image'; import { toPng } from 'html-to-image';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { import {
Background, Background,
@ -102,117 +102,82 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
}, PARAMETER.graphRefreshDelay); }, PARAMETER.graphRefreshDelay);
}, [model.schema, setNodes, setEdges, setIsModified, toggleReset, edgeStraight, edgeAnimate]); }, [model.schema, setNodes, setEdges, setIsModified, toggleReset, edgeStraight, edgeAnimate]);
const getPositions = useCallback( function getPositions() {
() => return nodes.map(node => ({
nodes.map(node => ({ id: Number(node.id),
id: Number(node.id), position_x: node.position.x,
position_x: node.position.x, position_y: node.position.y
position_y: node.position.y }));
})), }
[nodes]
);
const handleNodesChange = useCallback( function handleNodesChange(changes: NodeChange[]) {
(changes: NodeChange[]) => { if (changes.some(change => change.type === 'position' && change.position)) {
if (changes.some(change => change.type === 'position' && change.position)) { setIsModified(true);
setIsModified(true); }
} onNodesChange(changes);
onNodesChange(changes); }
},
[onNodesChange, setIsModified]
);
const handleSavePositions = useCallback(() => { function handleSavePositions() {
controller.savePositions(getPositions(), () => setIsModified(false)); controller.savePositions(getPositions(), () => setIsModified(false));
}, [controller, getPositions, setIsModified]); }
const handleCreateOperation = useCallback( function handleCreateOperation(inputs: OperationID[]) {
(inputs: OperationID[]) => { if (!controller.schema) {
if (!controller.schema) { return;
return; }
} const positions = getPositions();
const target = flow.project({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
controller.promptCreateOperation({
defaultX: target.x,
defaultY: target.y,
inputs: inputs,
positions: positions,
callback: () => flow.fitView({ duration: PARAMETER.zoomDuration })
});
}
const positions = getPositions(); function handleDeleteOperation(target: OperationID) {
const target = flow.project({ x: window.innerWidth / 2, y: window.innerHeight / 2 }); if (!controller.canDelete(target)) {
controller.promptCreateOperation({ return;
defaultX: target.x, }
defaultY: target.y, controller.promptDeleteOperation(target, getPositions());
inputs: inputs, }
positions: positions,
callback: () => flow.fitView({ duration: PARAMETER.zoomDuration })
});
},
[controller, getPositions, flow]
);
const handleDeleteOperation = useCallback( function handleDeleteSelected() {
(target: OperationID) => {
if (!controller.canDelete(target)) {
return;
}
controller.promptDeleteOperation(target, getPositions());
},
[controller, getPositions]
);
const handleDeleteSelected = useCallback(() => {
if (controller.selected.length !== 1) { if (controller.selected.length !== 1) {
return; return;
} }
handleDeleteOperation(controller.selected[0]); handleDeleteOperation(controller.selected[0]);
}, [controller, handleDeleteOperation]); }
const handleCreateInput = useCallback( function handleCreateInput(target: OperationID) {
(target: OperationID) => { controller.createInput(target, getPositions());
controller.createInput(target, getPositions()); }
},
[controller, getPositions]
);
const handleEditSchema = useCallback( function handleEditSchema(target: OperationID) {
(target: OperationID) => { controller.promptEditInput(target, getPositions());
controller.promptEditInput(target, getPositions()); }
},
[controller, getPositions]
);
const handleEditOperation = useCallback( function handleEditOperation(target: OperationID) {
(target: OperationID) => { controller.promptEditOperation(target, getPositions());
controller.promptEditOperation(target, getPositions()); }
},
[controller, getPositions]
);
const handleExecuteOperation = useCallback( function handleExecuteOperation(target: OperationID) {
(target: OperationID) => { controller.executeOperation(target, getPositions());
controller.executeOperation(target, getPositions()); }
},
[controller, getPositions]
);
const handleExecuteSelected = useCallback(() => { function handleExecuteSelected() {
if (controller.selected.length !== 1) { if (controller.selected.length !== 1) {
return; return;
} }
handleExecuteOperation(controller.selected[0]); handleExecuteOperation(controller.selected[0]);
}, [controller, handleExecuteOperation]); }
const handleRelocateConstituents = useCallback( function handleRelocateConstituents(target: OperationID) {
(target: OperationID) => { controller.promptRelocateConstituents(target, getPositions());
controller.promptRelocateConstituents(target, getPositions()); }
},
[controller, getPositions]
);
const handleFitView = useCallback(() => { function handleSaveImage() {
flow.fitView({ duration: PARAMETER.zoomDuration });
}, [flow]);
const handleResetPositions = useCallback(() => {
setToggleReset(prev => !prev);
}, []);
const handleSaveImage = useCallback(() => {
if (!model.schema) { if (!model.schema) {
return; return;
} }
@ -246,44 +211,38 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
console.error(error); console.error(error);
toast.error(errors.imageFailed); toast.error(errors.imageFailed);
}); });
}, [colors, nodes, model.schema]); }
const handleContextMenu = useCallback( function handleContextMenu(event: CProps.EventMouse, node: OssNode) {
(event: CProps.EventMouse, node: OssNode) => { event.preventDefault();
event.preventDefault(); event.stopPropagation();
event.stopPropagation();
setMenuProps({ setMenuProps({
operation: node.data.operation, operation: node.data.operation,
cursorX: event.clientX, cursorX: event.clientX,
cursorY: event.clientY cursorY: event.clientY
}); });
controller.setShowTooltip(false); controller.setShowTooltip(false);
}, }
[controller]
);
const handleContextMenuHide = useCallback(() => { function handleContextMenuHide() {
controller.setShowTooltip(true); controller.setShowTooltip(true);
setMenuProps(undefined); setMenuProps(undefined);
}, [controller]); }
const handleCanvasClick = useCallback(() => { function handleCanvasClick() {
handleContextMenuHide(); handleContextMenuHide();
}, [handleContextMenuHide]); }
const handleNodeDoubleClick = useCallback( function handleNodeDoubleClick(event: CProps.EventMouse, node: OssNode) {
(event: CProps.EventMouse, node: OssNode) => { event.preventDefault();
event.preventDefault(); event.stopPropagation();
event.stopPropagation(); if (node.data.operation.result) {
if (node.data.operation.result) { controller.openOperationSchema(Number(node.id));
controller.openOperationSchema(Number(node.id)); } else {
} else { handleEditOperation(Number(node.id));
handleEditOperation(Number(node.id)); }
} }
},
[handleEditOperation, controller]
);
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) { function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
if (controller.isProcessing) { if (controller.isProcessing) {
@ -312,41 +271,6 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
} }
} }
const graph = useMemo(
() => (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={handleNodesChange}
onEdgesChange={onEdgesChange}
onNodeDoubleClick={handleNodeDoubleClick}
edgesFocusable={false}
nodesFocusable={false}
fitView
nodeTypes={OssNodeTypes}
maxZoom={ZOOM_MAX}
minZoom={ZOOM_MIN}
nodesConnectable={false}
snapToGrid={true}
snapGrid={[PARAMETER.ossGridSize, PARAMETER.ossGridSize]}
onNodeContextMenu={handleContextMenu}
onClick={handleCanvasClick}
>
{showGrid ? <Background gap={PARAMETER.ossGridSize} /> : null}
</ReactFlow>
),
[
nodes,
edges,
handleNodesChange,
handleContextMenu,
handleCanvasClick,
onEdgesChange,
handleNodeDoubleClick,
showGrid
]
);
return ( return (
<div tabIndex={-1} onKeyDown={handleKeyDown}> <div tabIndex={-1} onKeyDown={handleKeyDown}>
<Overlay position='top-[1.9rem] pt-1 right-1/2 translate-x-1/2' className='rounded-b-2xl cc-blur'> <Overlay position='top-[1.9rem] pt-1 right-1/2 translate-x-1/2' className='rounded-b-2xl cc-blur'>
@ -355,12 +279,12 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
showGrid={showGrid} showGrid={showGrid}
edgeAnimate={edgeAnimate} edgeAnimate={edgeAnimate}
edgeStraight={edgeStraight} edgeStraight={edgeStraight}
onFitView={handleFitView} onFitView={() => flow.fitView({ duration: PARAMETER.zoomDuration })}
onCreate={() => handleCreateOperation(controller.selected)} onCreate={() => handleCreateOperation(controller.selected)}
onDelete={handleDeleteSelected} onDelete={handleDeleteSelected}
onEdit={() => handleEditOperation(controller.selected[0])} onEdit={() => handleEditOperation(controller.selected[0])}
onExecute={handleExecuteSelected} onExecute={handleExecuteSelected}
onResetPositions={handleResetPositions} onResetPositions={() => setToggleReset(prev => !prev)}
onSavePositions={handleSavePositions} onSavePositions={handleSavePositions}
onSaveImage={handleSaveImage} onSaveImage={handleSaveImage}
toggleShowGrid={() => setShowGrid(prev => !prev)} toggleShowGrid={() => setShowGrid(prev => !prev)}
@ -381,7 +305,26 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
/> />
) : null} ) : null}
<div className='cc-fade-in relative w-[100vw]' style={{ height: mainHeight, fontFamily: 'Rubik' }}> <div className='cc-fade-in relative w-[100vw]' style={{ height: mainHeight, fontFamily: 'Rubik' }}>
{graph} <ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={handleNodesChange}
onEdgesChange={onEdgesChange}
onNodeDoubleClick={handleNodeDoubleClick}
edgesFocusable={false}
nodesFocusable={false}
fitView
nodeTypes={OssNodeTypes}
maxZoom={ZOOM_MAX}
minZoom={ZOOM_MIN}
nodesConnectable={false}
snapToGrid={true}
snapGrid={[PARAMETER.ossGridSize, PARAMETER.ossGridSize]}
onNodeContextMenu={handleContextMenu}
onClick={handleCanvasClick}
>
{showGrid ? <Background gap={PARAMETER.ossGridSize} /> : null}
</ReactFlow>
</div> </div>
</div> </div>
); );

View File

@ -1,7 +1,6 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useMemo } from 'react';
import { import {
IconAnimation, IconAnimation,
@ -63,11 +62,8 @@ function ToolbarOssGraph({
toggleEdgeStraight toggleEdgeStraight
}: ToolbarOssGraphProps) { }: ToolbarOssGraphProps) {
const controller = useOssEdit(); const controller = useOssEdit();
const selectedOperation = useMemo( const selectedOperation = controller.schema?.operationByID.get(controller.selected[0]);
() => controller.schema?.operationByID.get(controller.selected[0]), const readyForSynthesis = (() => {
[controller.selected, controller.schema]
);
const readyForSynthesis = useMemo(() => {
if (!selectedOperation || selectedOperation.operation_type !== OperationType.SYNTHESIS) { if (!selectedOperation || selectedOperation.operation_type !== OperationType.SYNTHESIS) {
return false; return false;
} }
@ -86,7 +82,7 @@ function ToolbarOssGraph({
} }
return true; return true;
}, [selectedOperation, controller.schema]); })();
return ( return (
<div className='flex flex-col items-center'> <div className='flex flex-col items-center'>

View File

@ -2,7 +2,7 @@
import axios from 'axios'; import axios from 'axios';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import { TabList, TabPanel, Tabs } from 'react-tabs'; import { TabList, TabPanel, Tabs } from 'react-tabs';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@ -64,19 +64,16 @@ function OssTabs() {
setNoFooter(activeTab === OssTabID.GRAPH); setNoFooter(activeTab === OssTabID.GRAPH);
}, [activeTab, setNoFooter]); }, [activeTab, setNoFooter]);
const navigateTab = useCallback( function navigateTab(tab: OssTabID) {
(tab: OssTabID) => { if (!schema) {
if (!schema) { return;
return; }
} const url = urls.oss_props({
const url = urls.oss_props({ id: schema.id,
id: schema.id, tab: tab
tab: tab });
}); router.push(url);
router.push(url); }
},
[router, schema]
);
function onSelectTab(index: number, last: number, event: Event) { function onSelectTab(index: number, last: number, event: Event) {
if (last === index) { if (last === index) {
@ -97,7 +94,7 @@ function OssTabs() {
navigateTab(index); navigateTab(index);
} }
const onDestroySchema = useCallback(() => { function onDestroySchema() {
if (!schema || !window.confirm(prompts.deleteOSS)) { if (!schema || !window.confirm(prompts.deleteOSS)) {
return; return;
} }
@ -105,29 +102,7 @@ function OssTabs() {
toast.success(information.itemDestroyed); toast.success(information.itemDestroyed);
router.push(urls.library); router.push(urls.library);
}); });
}, [schema, destroyItem, router]); }
const cardPanel = useMemo(
() => (
<TabPanel>
<EditorRSForm
isModified={isModified} // prettier: split lines
setIsModified={setIsModified}
onDestroy={onDestroySchema}
/>
</TabPanel>
),
[isModified, onDestroySchema]
);
const graphPanel = useMemo(
() => (
<TabPanel>
<EditorTermGraph isModified={isModified} setIsModified={setIsModified} />
</TabPanel>
),
[isModified]
);
return ( return (
<OssEditState selected={selected} setSelected={setSelected}> <OssEditState selected={selected} setSelected={setSelected}>
@ -151,8 +126,17 @@ function OssTabs() {
</Overlay> </Overlay>
<div className='overflow-x-hidden'> <div className='overflow-x-hidden'>
{cardPanel} <TabPanel>
{graphPanel} <EditorRSForm
isModified={isModified} // prettier: split lines
setIsModified={setIsModified}
onDestroy={onDestroySchema}
/>
</TabPanel>
<TabPanel>
<EditorTermGraph isModified={isModified} setIsModified={setIsModified} />
</TabPanel>
</div> </div>
</Tabs> </Tabs>
) : null} ) : null}

View File

@ -2,7 +2,7 @@
import axios from 'axios'; import axios from 'axios';
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import { urls } from '@/app/urls'; import { urls } from '@/app/urls';
import InfoError, { ErrorData } from '@/components/info/InfoError'; import InfoError, { ErrorData } from '@/components/info/InfoError';
@ -24,17 +24,10 @@ function PasswordChangePage() {
const [newPassword, setNewPassword] = useState(''); const [newPassword, setNewPassword] = useState('');
const [newPasswordRepeat, setNewPasswordRepeat] = useState(''); const [newPasswordRepeat, setNewPasswordRepeat] = useState('');
const passwordColor = useMemo(() => { const passwordColor =
if (!!newPassword && !!newPasswordRepeat && newPassword !== newPasswordRepeat) { !!newPassword && !!newPasswordRepeat && newPassword !== newPasswordRepeat ? 'clr-warning' : 'clr-input';
return 'clr-warning';
} else {
return 'clr-input';
}
}, [newPassword, newPasswordRepeat]);
const canSubmit = useMemo(() => { const canSubmit = !!newPassword && !!newPasswordRepeat && newPassword === newPasswordRepeat;
return !!newPassword && !!newPasswordRepeat && newPassword === newPasswordRepeat;
}, [newPassword, newPasswordRepeat]);
function handleSubmit(event: React.FormEvent<HTMLFormElement>) { function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault(); event.preventDefault();

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useMemo, useState } from 'react'; import { useState } from 'react';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import useLocalStorage from '@/hooks/useLocalStorage'; import useLocalStorage from '@/hooks/useLocalStorage';
@ -32,12 +32,8 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }
const [showList, setShowList] = useLocalStorage(storage.rseditShowList, true); const [showList, setShowList] = useLocalStorage(storage.rseditShowList, true);
const [toggleReset, setToggleReset] = useState(false); const [toggleReset, setToggleReset] = useState(false);
const disabled = useMemo( const disabled = !activeCst || !controller.isContentEditable || controller.isProcessing;
() => !activeCst || !controller.isContentEditable || controller.isProcessing, const isNarrow = !!windowSize.width && windowSize.width <= SIDELIST_LAYOUT_THRESHOLD;
[activeCst, controller.isContentEditable, controller.isProcessing]
);
const isNarrow = useMemo(() => !!windowSize.width && windowSize.width <= SIDELIST_LAYOUT_THRESHOLD, [windowSize]);
function handleInput(event: React.KeyboardEvent<HTMLDivElement>) { function handleInput(event: React.KeyboardEvent<HTMLDivElement>) {
if (disabled) { if (disabled) {

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useLayoutEffect, useMemo, useState } from 'react'; import { useEffect, useLayoutEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { IconChild, IconPredecessor, IconSave } from '@/components/Icons'; import { IconChild, IconPredecessor, IconSave } from '@/components/Icons';
@ -58,26 +58,19 @@ function FormConstituenta({
const [typification, setTypification] = useState('N/A'); const [typification, setTypification] = useState('N/A');
const [showTypification, setShowTypification] = useState(false); const [showTypification, setShowTypification] = useState(false);
const [localParse, setLocalParse] = useState<IExpressionParse | undefined>(undefined); const [localParse, setLocalParse] = useState<IExpressionParse | undefined>(undefined);
const typeInfo = useMemo( const typeInfo = state
() => ? {
state alias: state.alias,
? { result: localParse ? localParse.typification : state.parse.typification,
alias: state.alias, args: localParse ? localParse.args : state.parse.args
result: localParse ? localParse.typification : state.parse.typification, }
args: localParse ? localParse.args : state.parse.args : undefined;
}
: undefined,
[state, localParse]
);
const [forceComment, setForceComment] = useState(false); const [forceComment, setForceComment] = useState(false);
const isBasic = useMemo(() => !!state && isBasicConcept(state.cst_type), [state]); const isBasic = !!state && isBasicConcept(state.cst_type);
const isElementary = useMemo(() => !!state && isBaseSet(state.cst_type), [state]); const isElementary = !!state && isBaseSet(state.cst_type);
const showConvention = useMemo( const showConvention = !state || !!state.convention || forceComment || isBasic;
() => !state || !!state.convention || forceComment || isBasic,
[state, forceComment, isBasic]
);
useEffect(() => { useEffect(() => {
if (state) { if (state) {

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import { ReactCodeMirrorRef } from '@uiw/react-codemirror'; import { ReactCodeMirrorRef } from '@uiw/react-codemirror';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import BadgeHelp from '@/components/info/BadgeHelp'; import BadgeHelp from '@/components/info/BadgeHelp';
@ -99,7 +99,7 @@ function EditorRSExpression({
}); });
} }
const onShowError = useCallback((error: IRSErrorDescription, prefixLen: number) => { function onShowError(error: IRSErrorDescription, prefixLen: number) {
if (!rsInput.current) { if (!rsInput.current) {
return; return;
} }
@ -112,9 +112,9 @@ function EditorRSExpression({
} }
}); });
rsInput.current?.view?.focus(); rsInput.current?.view?.focus();
}, []); }
const handleEdit = useCallback((id: TokenID, key?: string) => { function handleEdit(id: TokenID, key?: string) {
if (!rsInput.current?.editor || !rsInput.current.state || !rsInput.current.view) { if (!rsInput.current?.editor || !rsInput.current.state || !rsInput.current.view) {
return; return;
} }
@ -126,7 +126,7 @@ function EditorRSExpression({
} }
rsInput.current?.view?.focus(); rsInput.current?.view?.focus();
setIsModified(true); setIsModified(true);
}, []); }
function handleShowAST(event: CProps.EventMouse) { function handleShowAST(event: CProps.EventMouse) {
if (event.ctrlKey) { if (event.ctrlKey) {
@ -148,17 +148,6 @@ function EditorRSExpression({
} }
} }
const controls = useMemo(
() => (
<RSEditorControls
isOpen={showControls && (!disabled || (model.processing && !activeCst.is_inherited))}
disabled={disabled}
onEdit={handleEdit}
/>
),
[showControls, disabled, model.processing, handleEdit, activeCst]
);
return ( return (
<div className='cc-fade-in'> <div className='cc-fade-in'>
{showAST ? ( {showAST ? (
@ -201,7 +190,11 @@ function EditorRSExpression({
{...restProps} {...restProps}
/> />
{controls} <RSEditorControls
isOpen={showControls && (!disabled || (model.processing && !activeCst.is_inherited))}
disabled={disabled}
onEdit={handleEdit}
/>
<ParsingResult <ParsingResult
isOpen={!!parser.parseData && parser.parseData.errors.length > 0} isOpen={!!parser.parseData && parser.parseData.errors.length > 0}

View File

@ -1,7 +1,6 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useMemo } from 'react';
import { StatusIcon } from '@/components/DomainIcons'; import { StatusIcon } from '@/components/DomainIcons';
import Loader from '@/components/ui/Loader'; import Loader from '@/components/ui/Loader';
@ -24,7 +23,7 @@ interface StatusBarProps {
function StatusBar({ isModified, processing, activeCst, parseData, onAnalyze }: StatusBarProps) { function StatusBar({ isModified, processing, activeCst, parseData, onAnalyze }: StatusBarProps) {
const { colors } = useConceptOptions(); const { colors } = useConceptOptions();
const status = useMemo(() => { const status = (() => {
if (isModified) { if (isModified) {
return ExpressionStatus.UNKNOWN; return ExpressionStatus.UNKNOWN;
} }
@ -33,7 +32,7 @@ function StatusBar({ isModified, processing, activeCst, parseData, onAnalyze }:
return inferStatus(parse, parseData.valueClass); return inferStatus(parse, parseData.valueClass);
} }
return inferStatus(activeCst.parse.status, activeCst.parse.valueClass); return inferStatus(activeCst.parse.status, activeCst.parse.valueClass);
}, [isModified, activeCst, parseData]); })();
return ( return (
<div <div

View File

@ -1,5 +1,3 @@
import { useMemo } from 'react';
import { VisibilityIcon } from '@/components/DomainIcons'; import { VisibilityIcon } from '@/components/DomainIcons';
import { IconImmutable, IconMutable } from '@/components/Icons'; import { IconImmutable, IconMutable } from '@/components/Icons';
import BadgeHelp from '@/components/info/BadgeHelp'; import BadgeHelp from '@/components/info/BadgeHelp';
@ -23,10 +21,7 @@ interface ToolbarItemAccessProps {
function ToolbarItemAccess({ visible, toggleVisible, readOnly, toggleReadOnly, controller }: ToolbarItemAccessProps) { function ToolbarItemAccess({ visible, toggleVisible, readOnly, toggleReadOnly, controller }: ToolbarItemAccessProps) {
const { accessLevel } = useAccessMode(); const { accessLevel } = useAccessMode();
const policy = useMemo( const policy = controller.schema?.access_policy ?? AccessPolicy.PRIVATE;
() => controller.schema?.access_policy ?? AccessPolicy.PRIVATE,
[controller.schema?.access_policy]
);
return ( return (
<Overlay position='top-[4.5rem] right-0 w-[12rem] pr-2' className='flex' layer='z-bottom'> <Overlay position='top-[4.5rem] right-0 w-[12rem] pr-2' className='flex' layer='z-bottom'>

View File

@ -1,7 +1,5 @@
'use client'; 'use client';
import { useMemo } from 'react';
import { IconDestroy, IconSave, IconShare } from '@/components/Icons'; import { IconDestroy, IconSave, IconShare } from '@/components/Icons';
import BadgeHelp from '@/components/info/BadgeHelp'; import BadgeHelp from '@/components/info/BadgeHelp';
import MiniSelectorOSS from '@/components/select/MiniSelectorOSS'; import MiniSelectorOSS from '@/components/select/MiniSelectorOSS';
@ -26,9 +24,9 @@ interface ToolbarRSFormCardProps {
function ToolbarRSFormCard({ modified, controller, onSubmit, onDestroy }: ToolbarRSFormCardProps) { function ToolbarRSFormCard({ modified, controller, onSubmit, onDestroy }: ToolbarRSFormCardProps) {
const { accessLevel } = useAccessMode(); const { accessLevel } = useAccessMode();
const canSave = useMemo(() => modified && !controller.isProcessing, [modified, controller.isProcessing]); const canSave = modified && !controller.isProcessing;
const ossSelector = useMemo(() => { const ossSelector = (() => {
if (!controller.schema || controller.schema?.item_type !== LibraryItemType.RSFORM) { if (!controller.schema || controller.schema?.item_type !== LibraryItemType.RSFORM) {
return null; return null;
} }
@ -42,7 +40,7 @@ function ToolbarRSFormCard({ modified, controller, onSubmit, onDestroy }: Toolba
onSelect={(event, value) => (controller as IRSEditContext).viewOSS(value.id, event.ctrlKey || event.metaKey)} onSelect={(event, value) => (controller as IRSEditContext).viewOSS(value.id, event.ctrlKey || event.metaKey)}
/> />
); );
}, [controller]); })();
return ( return (
<Overlay position='cc-tab-tools' className='cc-icons'> <Overlay position='cc-tab-tools' className='cc-icons'>

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import fileDownload from 'js-file-download'; import fileDownload from 'js-file-download';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { IconCSV } from '@/components/Icons'; import { IconCSV } from '@/components/Icons';
@ -54,7 +54,7 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
} }
}, [filterText, controller.schema?.items, controller.schema]); }, [filterText, controller.schema?.items, controller.schema]);
const handleDownloadCSV = useCallback(() => { function handleDownloadCSV() {
if (!controller.schema || filtered.length === 0) { if (!controller.schema || filtered.length === 0) {
toast.error(information.noDataToExport); toast.error(information.noDataToExport);
return; return;
@ -65,7 +65,7 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
}, [filtered, controller]); }
function handleRowSelection(updater: React.SetStateAction<RowSelectionState>) { function handleRowSelection(updater: React.SetStateAction<RowSelectionState>) {
if (!controller.schema) { if (!controller.schema) {
@ -136,7 +136,7 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
return false; return false;
} }
const tableHeight = useMemo(() => calculateHeight('4.05rem + 5px'), [calculateHeight]); const tableHeight = calculateHeight('4.05rem + 5px');
return ( return (
<> <>

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useLayoutEffect, useMemo, useState } from 'react'; import { useLayoutEffect, useState } from 'react';
import BadgeConstituenta from '@/components/info/BadgeConstituenta'; import BadgeConstituenta from '@/components/info/BadgeConstituenta';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
@ -59,82 +59,73 @@ function TableRSList({
}); });
}, [windowSize]); }, [windowSize]);
const handleRowClicked = useCallback( function handleRowClicked(cst: IConstituenta, event: CProps.EventMouse) {
(cst: IConstituenta, event: CProps.EventMouse) => { if (event.altKey) {
if (event.altKey) {
event.preventDefault();
onEdit(cst.id);
}
},
[onEdit]
);
const handleRowDoubleClicked = useCallback(
(cst: IConstituenta, event: CProps.EventMouse) => {
event.preventDefault(); event.preventDefault();
onEdit(cst.id); onEdit(cst.id);
}, }
[onEdit] }
);
const columns = useMemo( function handleRowDoubleClicked(cst: IConstituenta, event: CProps.EventMouse) {
() => [ event.preventDefault();
columnHelper.accessor('alias', { onEdit(cst.id);
id: 'alias', }
header: () => <span className='pl-3'>Имя</span>,
size: 65, const columns = [
minSize: 65, columnHelper.accessor('alias', {
maxSize: 65, id: 'alias',
cell: props => <BadgeConstituenta theme={colors} value={props.row.original} prefixID={prefixes.cst_list} /> header: () => <span className='pl-3'>Имя</span>,
}), size: 65,
columnHelper.accessor(cst => labelCstTypification(cst), { minSize: 65,
id: 'type', maxSize: 65,
header: 'Типизация', cell: props => <BadgeConstituenta theme={colors} value={props.row.original} prefixID={prefixes.cst_list} />
enableHiding: true, }),
size: 150, columnHelper.accessor(cst => labelCstTypification(cst), {
minSize: 150, id: 'type',
maxSize: 200, header: 'Типизация',
cell: props => ( enableHiding: true,
<div className={clsx('min-w-[9.3rem] max-w-[9.3rem]', 'text-xs break-words')}> size: 150,
{truncateToSymbol(props.getValue(), PARAMETER.typificationTruncate)} minSize: 150,
</div> maxSize: 200,
) cell: props => (
}), <div className={clsx('min-w-[9.3rem] max-w-[9.3rem]', 'text-xs break-words')}>
columnHelper.accessor(cst => cst.term_resolved || cst.term_raw || '', { {truncateToSymbol(props.getValue(), PARAMETER.typificationTruncate)}
id: 'term', </div>
header: 'Термин', )
size: 500, }),
minSize: 150, columnHelper.accessor(cst => cst.term_resolved || cst.term_raw || '', {
maxSize: 500 id: 'term',
}), header: 'Термин',
columnHelper.accessor('definition_formal', { size: 500,
id: 'expression', minSize: 150,
header: 'Формальное определение', maxSize: 500
size: 1000, }),
minSize: 300, columnHelper.accessor('definition_formal', {
maxSize: 1000, id: 'expression',
cell: props => <div className='break-all text-pretty'>{props.getValue()}</div> header: 'Формальное определение',
}), size: 1000,
columnHelper.accessor(cst => cst.definition_resolved || cst.definition_raw || '', { minSize: 300,
id: 'definition', maxSize: 1000,
header: 'Текстовое определение', cell: props => <div className='break-all text-pretty'>{props.getValue()}</div>
size: 1000, }),
minSize: 200, columnHelper.accessor(cst => cst.definition_resolved || cst.definition_raw || '', {
maxSize: 1000, id: 'definition',
cell: props => <TextContent text={props.getValue()} maxLength={DEFINITION_MAX_SYMBOLS} /> header: 'Текстовое определение',
}), size: 1000,
columnHelper.accessor('convention', { minSize: 200,
id: 'convention', maxSize: 1000,
header: 'Конвенция / Комментарий', cell: props => <TextContent text={props.getValue()} maxLength={DEFINITION_MAX_SYMBOLS} />
size: 500, }),
minSize: 100, columnHelper.accessor('convention', {
maxSize: 500, id: 'convention',
enableHiding: true, header: 'Конвенция / Комментарий',
cell: props => <TextContent text={props.getValue()} maxLength={COMMENT_MAX_SYMBOLS} /> size: 500,
}) minSize: 100,
], maxSize: 500,
[colors] enableHiding: true,
); cell: props => <TextContent text={props.getValue()} maxLength={COMMENT_MAX_SYMBOLS} />
})
];
return ( return (
<DataTable <DataTable

View File

@ -1,5 +1,3 @@
import { useMemo } from 'react';
import { IconHelp } from '@/components/Icons'; import { IconHelp } from '@/components/Icons';
import Tooltip from '@/components/ui/Tooltip'; import Tooltip from '@/components/ui/Tooltip';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
@ -17,7 +15,7 @@ function SchemasGuide({ schema }: SchemasGuideProps) {
const { colors } = useConceptOptions(); const { colors } = useConceptOptions();
const library = useLibrary(); const library = useLibrary();
const schemas = useMemo(() => { const schemas = (() => {
const processed = new Set<LibraryItemID>(); const processed = new Set<LibraryItemID>();
const aliases: string[] = []; const aliases: string[] = [];
const indexes: number[] = []; const indexes: number[] = [];
@ -39,7 +37,7 @@ function SchemasGuide({ schema }: SchemasGuideProps) {
result.push(aliases[trueIndex]); result.push(aliases[trueIndex]);
} }
return result; return result;
}, [schema, library.items]); })();
return ( return (
<div tabIndex={-1} id={globals.graph_schemas} className='p-1'> <div tabIndex={-1} id={globals.graph_schemas} className='p-1'>

View File

@ -2,7 +2,7 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { toPng } from 'html-to-image'; import { toPng } from 'html-to-image';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { import {
Edge, Edge,
@ -89,9 +89,7 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const [hoverID, setHoverID] = useState<ConstituentaID | undefined>(undefined); const [hoverID, setHoverID] = useState<ConstituentaID | undefined>(undefined);
const hoverCst = useMemo(() => { const hoverCst = hoverID && controller.schema?.cstByID.get(hoverID);
return hoverID && controller.schema?.cstByID.get(hoverID);
}, [controller.schema?.cstByID, hoverID]);
const [hoverCstDebounced] = useDebounce(hoverCst, PARAMETER.graphPopupDelay); const [hoverCstDebounced] = useDebounce(hoverCst, PARAMETER.graphPopupDelay);
const [hoverLeft, setHoverLeft] = useState(true); const [hoverLeft, setHoverLeft] = useState(true);
@ -223,14 +221,11 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
controller.promptDeleteCst(); controller.promptDeleteCst();
} }
const handleChangeParams = useCallback( function handleChangeParams(params: GraphFilterParams) {
(params: GraphFilterParams) => { setFilterParams(params);
setFilterParams(params); }
},
[setFilterParams]
);
const handleSaveImage = useCallback(() => { function handleSaveImage() {
if (!controller.schema) { if (!controller.schema) {
return; return;
} }
@ -264,7 +259,7 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
console.error(error); console.error(error);
toast.error(errors.imageFailed); toast.error(errors.imageFailed);
}); });
}, [colors, nodes, controller.schema]); }
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) { function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
if (controller.isProcessing) { if (controller.isProcessing) {
@ -288,7 +283,7 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
} }
} }
const handleFoldDerived = useCallback(() => { function handleFoldDerived() {
setFilterParams(prev => ({ setFilterParams(prev => ({
...prev, ...prev,
foldDerived: !prev.foldDerived foldDerived: !prev.foldDerived
@ -296,99 +291,37 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
setTimeout(() => { setTimeout(() => {
setToggleResetView(prev => !prev); setToggleResetView(prev => !prev);
}, PARAMETER.graphRefreshDelay); }, PARAMETER.graphRefreshDelay);
}, [setFilterParams, setToggleResetView]); }
const handleSetFocus = useCallback( function handleSetFocus(cstID: ConstituentaID | undefined) {
(cstID: ConstituentaID | undefined) => { const target = cstID !== undefined ? controller.schema?.cstByID.get(cstID) : cstID;
const target = cstID !== undefined ? controller.schema?.cstByID.get(cstID) : cstID; setFocusCst(prev => (prev === target ? undefined : target));
setFocusCst(prev => (prev === target ? undefined : target)); if (target) {
if (target) { controller.setSelected([]);
controller.setSelected([]); }
} }
},
[controller]
);
const handleNodeClick = useCallback( function handleNodeClick(event: CProps.EventMouse, cstID: ConstituentaID) {
(event: CProps.EventMouse, cstID: ConstituentaID) => { if (event.altKey) {
if (event.altKey) {
event.preventDefault();
event.stopPropagation();
handleSetFocus(cstID);
}
},
[handleSetFocus]
);
const handleNodeDoubleClick = useCallback(
(event: CProps.EventMouse, cstID: ConstituentaID) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
onOpenEdit(cstID); handleSetFocus(cstID);
}, }
[onOpenEdit] }
);
const handleNodeEnter = useCallback( function handleNodeDoubleClick(event: CProps.EventMouse, cstID: ConstituentaID) {
(event: CProps.EventMouse, cstID: ConstituentaID) => { event.preventDefault();
setHoverID(cstID); event.stopPropagation();
setHoverLeft( onOpenEdit(cstID);
event.clientX / window.innerWidth >= PARAMETER.graphHoverXLimit || }
event.clientY / window.innerHeight >= PARAMETER.graphHoverYLimit
);
},
[setHoverID, setHoverLeft]
);
const handleNodeLeave = useCallback(() => { function handleNodeEnter(event: CProps.EventMouse, cstID: ConstituentaID) {
setHoverID(undefined); setHoverID(cstID);
}, [setHoverID]); setHoverLeft(
event.clientX / window.innerWidth >= PARAMETER.graphHoverXLimit ||
const selectors = useMemo( event.clientY / window.innerHeight >= PARAMETER.graphHoverYLimit
() => <GraphSelectors schema={controller.schema} coloring={coloring} onChangeColoring={setColoring} />, );
[coloring, controller.schema, setColoring] }
);
const viewHidden = useMemo(
() => (
<ViewHidden
items={hidden}
selected={controller.selected}
schema={controller.schema}
coloringScheme={coloring}
toggleSelection={controller.toggleSelect}
setFocus={handleSetFocus}
onEdit={onOpenEdit}
/>
),
[hidden, controller.selected, controller.schema, coloring, controller.toggleSelect, handleSetFocus, onOpenEdit]
);
const graph = useMemo(
() => (
<div className='relative outline-none w-[100dvw]' style={{ height: mainHeight }}>
<ReactFlow
nodes={nodes}
onNodesChange={onNodesChange}
edges={edges}
fitView
edgesFocusable={false}
nodesFocusable={false}
nodesConnectable={false}
nodeTypes={TGNodeTypes}
edgeTypes={TGEdgeTypes}
maxZoom={ZOOM_MAX}
minZoom={ZOOM_MIN}
onNodeDragStart={() => setIsDragging(true)}
onNodeDragStop={() => setIsDragging(false)}
onNodeMouseEnter={(event, node) => handleNodeEnter(event, Number(node.id))}
onNodeMouseLeave={handleNodeLeave}
onNodeClick={(event, node) => handleNodeClick(event, Number(node.id))}
onNodeDoubleClick={(event, node) => handleNodeDoubleClick(event, Number(node.id))}
/>
</div>
),
[nodes, edges, mainHeight, handleNodeClick, handleNodeDoubleClick, handleNodeLeave, handleNodeEnter, onNodesChange]
);
return ( return (
<> <>
@ -480,12 +413,40 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
<Overlay position='top-[6.15rem] sm:top-[5.9rem] left-0' className='flex gap-1'> <Overlay position='top-[6.15rem] sm:top-[5.9rem] left-0' className='flex gap-1'>
<div className='flex flex-col ml-2 w-[13.5rem]'> <div className='flex flex-col ml-2 w-[13.5rem]'>
{selectors} <GraphSelectors schema={controller.schema} coloring={coloring} onChangeColoring={setColoring} />
{viewHidden} <ViewHidden
items={hidden}
selected={controller.selected}
schema={controller.schema}
coloringScheme={coloring}
toggleSelection={controller.toggleSelect}
setFocus={handleSetFocus}
onEdit={onOpenEdit}
/>
</div> </div>
</Overlay> </Overlay>
{graph} <div className='relative outline-none w-[100dvw]' style={{ height: mainHeight }}>
<ReactFlow
nodes={nodes}
onNodesChange={onNodesChange}
edges={edges}
fitView
edgesFocusable={false}
nodesFocusable={false}
nodesConnectable={false}
nodeTypes={TGNodeTypes}
edgeTypes={TGEdgeTypes}
maxZoom={ZOOM_MAX}
minZoom={ZOOM_MIN}
onNodeDragStart={() => setIsDragging(true)}
onNodeDragStop={() => setIsDragging(false)}
onNodeMouseEnter={(event, node) => handleNodeEnter(event, Number(node.id))}
onNodeMouseLeave={() => setHoverID(undefined)}
onNodeClick={(event, node) => handleNodeClick(event, Number(node.id))}
onNodeDoubleClick={(event, node) => handleNodeDoubleClick(event, Number(node.id))}
/>
</div>
</div> </div>
</> </>
); );

View File

@ -1,7 +1,6 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useMemo } from 'react';
import { IconDropArrow, IconDropArrowUp } from '@/components/Icons'; import { IconDropArrow, IconDropArrowUp } from '@/components/Icons';
import TooltipConstituenta from '@/components/info/TooltipConstituenta'; import TooltipConstituenta from '@/components/info/TooltipConstituenta';
@ -30,19 +29,16 @@ interface ViewHiddenProps {
function ViewHidden({ items, selected, toggleSelection, setFocus, schema, coloringScheme, onEdit }: ViewHiddenProps) { function ViewHidden({ items, selected, toggleSelection, setFocus, schema, coloringScheme, onEdit }: ViewHiddenProps) {
const { colors, calculateHeight } = useConceptOptions(); const { colors, calculateHeight } = useConceptOptions();
const windowSize = useWindowSize(); const windowSize = useWindowSize();
const localSelected = useMemo(() => items.filter(id => selected.includes(id)), [items, selected]); const localSelected = items.filter(id => selected.includes(id));
const [isFolded, setIsFolded] = useLocalStorage(storage.rsgraphFoldHidden, false); const [isFolded, setIsFolded] = useLocalStorage(storage.rsgraphFoldHidden, false);
const handleClick = useCallback( function handleClick(cstID: ConstituentaID, event: CProps.EventMouse) {
(cstID: ConstituentaID, event: CProps.EventMouse) => { if (event.ctrlKey || event.metaKey) {
if (event.ctrlKey || event.metaKey) { setFocus(cstID);
setFocus(cstID); } else {
} else { toggleSelection(cstID);
toggleSelection(cstID); }
} }
},
[setFocus, toggleSelection]
);
if (!schema || items.length <= 0) { if (!schema || items.length <= 0) {
return null; return null;

View File

@ -1,6 +1,5 @@
'use client'; 'use client';
import { useMemo } from 'react';
import { Handle, Position } from 'reactflow'; import { Handle, Position } from 'reactflow';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
@ -34,10 +33,7 @@ interface TGNodeInternal {
function TGNode(node: TGNodeInternal) { function TGNode(node: TGNodeInternal) {
const { colors } = useConceptOptions(); const { colors } = useConceptOptions();
const description = useMemo( const description = truncateToLastWord(node.data.description, MAX_DESCRIPTION_LENGTH);
() => truncateToLastWord(node.data.description, MAX_DESCRIPTION_LENGTH),
[node.data.description]
);
return ( return (
<> <>

View File

@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import { Graph } from '@/models/Graph'; import { Graph } from '@/models/Graph';
import { GraphFilterParams } from '@/models/miscellaneous'; import { GraphFilterParams } from '@/models/miscellaneous';
@ -7,7 +7,7 @@ import { ConstituentaID, CstType, IConstituenta, IRSForm } from '@/models/rsform
function useGraphFilter(schema: IRSForm | undefined, params: GraphFilterParams, focusCst: IConstituenta | undefined) { function useGraphFilter(schema: IRSForm | undefined, params: GraphFilterParams, focusCst: IConstituenta | undefined) {
const [filtered, setFiltered] = useState<Graph>(new Graph()); const [filtered, setFiltered] = useState<Graph>(new Graph());
const allowedTypes: CstType[] = useMemo(() => { const allowedTypes: CstType[] = (() => {
const result: CstType[] = []; const result: CstType[] = [];
if (params.allowBase) result.push(CstType.BASE); if (params.allowBase) result.push(CstType.BASE);
if (params.allowStruct) result.push(CstType.STRUCTURED); if (params.allowStruct) result.push(CstType.STRUCTURED);
@ -18,7 +18,7 @@ function useGraphFilter(schema: IRSForm | undefined, params: GraphFilterParams,
if (params.allowConstant) result.push(CstType.CONSTANT); if (params.allowConstant) result.push(CstType.CONSTANT);
if (params.allowTheorem) result.push(CstType.THEOREM); if (params.allowTheorem) result.push(CstType.THEOREM);
return result; return result;
}, [params]); })();
useEffect(() => { useEffect(() => {
if (!schema) { if (!schema) {

View File

@ -2,7 +2,7 @@
import axios from 'axios'; import axios from 'axios';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import { TabList, TabPanel, Tabs } from 'react-tabs'; import { TabList, TabPanel, Tabs } from 'react-tabs';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@ -54,13 +54,13 @@ function RSTabs() {
useBlockNavigation(isModified); useBlockNavigation(isModified);
const [selected, setSelected] = useState<ConstituentaID[]>([]); const [selected, setSelected] = useState<ConstituentaID[]>([]);
const activeCst: IConstituenta | undefined = useMemo(() => { const activeCst: IConstituenta | undefined = (() => {
if (!schema || selected.length === 0) { if (!schema || selected.length === 0) {
return undefined; return undefined;
} else { } else {
return schema.cstByID.get(selected.at(-1)!); return schema.cstByID.get(selected.at(-1)!);
} }
}, [schema, selected]); })();
useEffect(() => { useEffect(() => {
if (schema) { if (schema) {
@ -86,32 +86,29 @@ function RSTabs() {
return () => setNoFooter(false); return () => setNoFooter(false);
}, [activeTab, cstQuery, setSelected, schema, setNoFooter, setIsModified]); }, [activeTab, cstQuery, setSelected, schema, setNoFooter, setIsModified]);
const navigateTab = useCallback( function navigateTab(tab: RSTabID, activeID?: ConstituentaID) {
(tab: RSTabID, activeID?: ConstituentaID) => { if (!schema) {
if (!schema) { return;
return; }
} const url = urls.schema_props({
const url = urls.schema_props({ id: schema.id,
id: schema.id, tab: tab,
tab: tab, active: activeID,
active: activeID, version: version
version: version });
}); if (activeID) {
if (activeID) { if (tab === activeTab && tab !== RSTabID.CST_EDIT) {
if (tab === activeTab && tab !== RSTabID.CST_EDIT) {
router.replace(url);
} else {
router.push(url);
}
} else if (tab !== activeTab && tab === RSTabID.CST_EDIT && schema.items.length > 0) {
activeID = schema.items[0].id;
router.replace(url); router.replace(url);
} else { } else {
router.push(url); router.push(url);
} }
}, } else if (tab !== activeTab && tab === RSTabID.CST_EDIT && schema.items.length > 0) {
[router, schema, activeTab, version] activeID = schema.items[0].id;
); router.replace(url);
} else {
router.push(url);
}
}
function onSelectTab(index: number, last: number, event: Event) { function onSelectTab(index: number, last: number, event: Event) {
if (last === index) { if (last === index) {
@ -132,48 +129,39 @@ function RSTabs() {
navigateTab(index, selected.length > 0 ? selected.at(-1) : undefined); navigateTab(index, selected.length > 0 ? selected.at(-1) : undefined);
} }
const onCreateCst = useCallback( function onCreateCst(newCst: IConstituentaMeta) {
(newCst: IConstituentaMeta) => { navigateTab(activeTab, newCst.id);
navigateTab(activeTab, newCst.id); if (activeTab === RSTabID.CST_LIST) {
if (activeTab === RSTabID.CST_LIST) { setTimeout(() => {
setTimeout(() => { const element = document.getElementById(`${prefixes.cst_list}${newCst.alias}`);
const element = document.getElementById(`${prefixes.cst_list}${newCst.alias}`); if (element) {
if (element) { element.scrollIntoView({
element.scrollIntoView({ behavior: 'smooth',
behavior: 'smooth', block: 'nearest',
block: 'nearest', inline: 'end'
inline: 'end' });
}); }
} }, PARAMETER.refreshTimeout);
}, PARAMETER.refreshTimeout); }
} }
},
[activeTab, navigateTab]
);
const onDeleteCst = useCallback( function onDeleteCst(newActive?: ConstituentaID) {
(newActive?: ConstituentaID) => { if (!newActive) {
if (!newActive) { navigateTab(RSTabID.CST_LIST);
navigateTab(RSTabID.CST_LIST); } else if (activeTab === RSTabID.CST_EDIT) {
} else if (activeTab === RSTabID.CST_EDIT) { navigateTab(activeTab, newActive);
navigateTab(activeTab, newActive); } else {
} else { navigateTab(activeTab);
navigateTab(activeTab); }
} }
},
[activeTab, navigateTab]
);
const onOpenCst = useCallback( function onOpenCst(cstID: ConstituentaID) {
(cstID: ConstituentaID) => { if (cstID !== activeCst?.id || activeTab !== RSTabID.CST_EDIT) {
if (cstID !== activeCst?.id || activeTab !== RSTabID.CST_EDIT) { navigateTab(RSTabID.CST_EDIT, cstID);
navigateTab(RSTabID.CST_EDIT, cstID); }
} }
},
[navigateTab, activeCst, activeTab]
);
const onDestroySchema = useCallback(() => { function onDestroySchema() {
if (!schema || !window.confirm(prompts.deleteLibraryItem)) { if (!schema || !window.confirm(prompts.deleteLibraryItem)) {
return; return;
} }
@ -187,52 +175,7 @@ function RSTabs() {
router.push(urls.library); router.push(urls.library);
} }
}); });
}, [schema, library, oss, router]); }
const cardPanel = useMemo(
() => (
<TabPanel>
<EditorRSForm
isModified={isModified} // prettier: split lines
setIsModified={setIsModified}
onDestroy={onDestroySchema}
/>
</TabPanel>
),
[isModified, onDestroySchema]
);
const listPanel = useMemo(
() => (
<TabPanel>
<EditorRSList onOpenEdit={onOpenCst} />
</TabPanel>
),
[onOpenCst]
);
const editorPanel = useMemo(
() => (
<TabPanel>
<EditorConstituenta
isModified={isModified}
setIsModified={setIsModified}
activeCst={activeCst}
onOpenEdit={onOpenCst}
/>
</TabPanel>
),
[isModified, setIsModified, activeCst, onOpenCst]
);
const graphPanel = useMemo(
() => (
<TabPanel>
<EditorTermGraph onOpenEdit={onOpenCst} />
</TabPanel>
),
[onOpenCst]
);
return ( return (
<RSEditState <RSEditState
@ -270,10 +213,30 @@ function RSTabs() {
</Overlay> </Overlay>
<div className='overflow-x-hidden'> <div className='overflow-x-hidden'>
{cardPanel} <TabPanel>
{listPanel} <EditorRSForm
{editorPanel} isModified={isModified} // prettier: split lines
{graphPanel} setIsModified={setIsModified}
onDestroy={onDestroySchema}
/>
</TabPanel>
<TabPanel>
<EditorRSList onOpenEdit={onOpenCst} />
</TabPanel>
<TabPanel>
<EditorConstituenta
isModified={isModified}
setIsModified={setIsModified}
activeCst={activeCst}
onOpenEdit={onOpenCst}
/>
</TabPanel>
<TabPanel>
<EditorTermGraph onOpenEdit={onOpenCst} />
</TabPanel>
</div> </div>
</Tabs> </Tabs>
) : null} ) : null}

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import { IconChild } from '@/components/Icons'; import { IconChild } from '@/components/Icons';
import SelectGraphFilter from '@/components/select/SelectGraphFilter'; import SelectGraphFilter from '@/components/select/SelectGraphFilter';
@ -58,16 +58,6 @@ function ConstituentsSearch({ schema, activeID, activeExpression, dense, setFilt
showInherited showInherited
]); ]);
const selectGraph = useMemo(
() => <SelectGraphFilter value={filterSource} onChange={newValue => setFilterSource(newValue)} dense={dense} />,
[filterSource, setFilterSource, dense]
);
const selectMatchMode = useMemo(
() => <SelectMatchMode value={filterMatch} onChange={newValue => setFilterMatch(newValue)} dense={dense} />,
[filterMatch, setFilterMatch, dense]
);
return ( return (
<div className='flex border-b clr-input rounded-t-md'> <div className='flex border-b clr-input rounded-t-md'>
<SearchBar <SearchBar
@ -77,8 +67,8 @@ function ConstituentsSearch({ schema, activeID, activeExpression, dense, setFilt
query={filterText} query={filterText}
onChangeQuery={setFilterText} onChangeQuery={setFilterText}
/> />
{selectMatchMode} <SelectMatchMode value={filterMatch} onChange={newValue => setFilterMatch(newValue)} dense={dense} />
{selectGraph} <SelectGraphFilter value={filterSource} onChange={newValue => setFilterSource(newValue)} dense={dense} />
{schema && schema?.stats.count_inherited > 0 ? ( {schema && schema?.stats.count_inherited > 0 ? (
<MiniButton <MiniButton
noHover noHover

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useCallback, useEffect, useMemo } from 'react'; import { useEffect } from 'react';
import BadgeConstituenta from '@/components/info/BadgeConstituenta'; import BadgeConstituenta from '@/components/info/BadgeConstituenta';
import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/ui/DataTable'; import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/ui/DataTable';
@ -50,75 +50,62 @@ function TableSideConstituents({
} }
}, [activeCst, autoScroll]); }, [activeCst, autoScroll]);
const handleRowClicked = useCallback( const columns = [
(cst: IConstituenta) => { columnHelper.accessor('alias', {
onOpenEdit(cst.id); id: 'alias',
}, header: () => <span className='pl-3'>Имя</span>,
[onOpenEdit] size: 65,
); minSize: 65,
footer: undefined,
cell: props => (
<BadgeConstituenta
className='mr-[-0.5rem]'
theme={colors}
value={props.row.original}
prefixID={prefixes.cst_side_table}
/>
)
}),
columnHelper.accessor(cst => describeConstituenta(cst), {
id: 'description',
header: 'Описание',
size: 1000,
minSize: 250,
maxSize: 1000,
cell: props => (
<TextContent
noTooltip
text={props.getValue()}
maxLength={DESCRIPTION_MAX_SYMBOLS}
style={{
textWrap: 'pretty',
fontSize: 12
}}
/>
)
})
];
const columns = useMemo( const conditionalRowStyles: IConditionalStyle<IConstituenta>[] = [
() => [ {
columnHelper.accessor('alias', { when: (cst: IConstituenta) => !!activeCst && cst.id === activeCst?.id,
id: 'alias', style: {
header: () => <span className='pl-3'>Имя</span>, backgroundColor: colors.bgSelected
size: 65,
minSize: 65,
footer: undefined,
cell: props => (
<BadgeConstituenta
className='mr-[-0.5rem]'
theme={colors}
value={props.row.original}
prefixID={prefixes.cst_side_table}
/>
)
}),
columnHelper.accessor(cst => describeConstituenta(cst), {
id: 'description',
header: 'Описание',
size: 1000,
minSize: 250,
maxSize: 1000,
cell: props => (
<TextContent
noTooltip
text={props.getValue()}
maxLength={DESCRIPTION_MAX_SYMBOLS}
style={{
textWrap: 'pretty',
fontSize: 12
}}
/>
)
})
],
[colors]
);
const conditionalRowStyles = useMemo(
(): IConditionalStyle<IConstituenta>[] => [
{
when: (cst: IConstituenta) => !!activeCst && cst.id === activeCst?.id,
style: {
backgroundColor: colors.bgSelected
}
},
{
when: (cst: IConstituenta) => !!activeCst && cst.spawner === activeCst?.id && cst.id !== activeCst?.id,
style: {
backgroundColor: colors.bgOrange50
}
},
{
when: (cst: IConstituenta) => activeCst?.id !== undefined && cst.spawn.includes(activeCst.id),
style: {
backgroundColor: colors.bgGreen50
}
} }
], },
[activeCst, colors] {
); when: (cst: IConstituenta) => !!activeCst && cst.spawner === activeCst?.id && cst.id !== activeCst?.id,
style: {
backgroundColor: colors.bgOrange50
}
},
{
when: (cst: IConstituenta) => activeCst?.id !== undefined && cst.spawn.includes(activeCst.id),
style: {
backgroundColor: colors.bgGreen50
}
}
];
return ( return (
<DataTable <DataTable
@ -137,7 +124,7 @@ function TableSideConstituents({
<p>Измените параметры фильтра</p> <p>Измените параметры фильтра</p>
</NoData> </NoData>
} }
onRowClicked={handleRowClicked} onRowClicked={cst => onOpenEdit(cst.id)}
/> />
); );
} }

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useMemo, useState } from 'react'; import { useState } from 'react';
import { useAccessMode } from '@/context/AccessModeContext'; import { useAccessMode } from '@/context/AccessModeContext';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
@ -32,23 +32,6 @@ function ViewConstituents({ expression, schema, activeCst, isBottom, onOpenEdit,
const [filteredData, setFilteredData] = useState<IConstituenta[]>(schema?.items ?? []); const [filteredData, setFilteredData] = useState<IConstituenta[]>(schema?.items ?? []);
const table = useMemo(
() => (
<TableSideConstituents
maxHeight={
isBottom
? calculateHeight(accessLevel !== UserLevel.READER ? '42rem' : '35rem', '10rem')
: calculateHeight('8.2rem')
}
items={filteredData}
activeCst={activeCst}
onOpenEdit={onOpenEdit}
autoScroll={!isBottom}
/>
),
[isBottom, filteredData, activeCst, onOpenEdit, calculateHeight, accessLevel]
);
return ( return (
<div <div
className={clsx( className={clsx(
@ -73,7 +56,17 @@ function ViewConstituents({ expression, schema, activeCst, isBottom, onOpenEdit,
activeExpression={expression} activeExpression={expression}
setFiltered={setFilteredData} setFiltered={setFilteredData}
/> />
{table} <TableSideConstituents
maxHeight={
isBottom
? calculateHeight(accessLevel !== UserLevel.READER ? '42rem' : '35rem', '10rem')
: calculateHeight('8.2rem')
}
items={filteredData}
activeCst={activeCst}
onOpenEdit={onOpenEdit}
autoScroll={!isBottom}
/>
</div> </div>
); );
} }

View File

@ -2,7 +2,7 @@
import axios from 'axios'; import axios from 'axios';
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { urls } from '@/app/urls'; import { urls } from '@/app/urls';
@ -37,10 +37,7 @@ function FormSignup() {
const [acceptPrivacy, setAcceptPrivacy] = useState(false); const [acceptPrivacy, setAcceptPrivacy] = useState(false);
const [acceptRules, setAcceptRules] = useState(false); const [acceptRules, setAcceptRules] = useState(false);
const isValid = useMemo( const isValid = acceptPrivacy && acceptRules && !!email && !!username;
() => acceptPrivacy && acceptRules && !!email && !!username,
[acceptPrivacy, acceptRules, email, username]
);
useEffect(() => { useEffect(() => {
setError(undefined); setError(undefined);

View File

@ -2,7 +2,7 @@
import axios from 'axios'; import axios from 'axios';
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { urls } from '@/app/urls'; import { urls } from '@/app/urls';
@ -23,17 +23,10 @@ function EditorPassword() {
const [newPassword, setNewPassword] = useState(''); const [newPassword, setNewPassword] = useState('');
const [newPasswordRepeat, setNewPasswordRepeat] = useState(''); const [newPasswordRepeat, setNewPasswordRepeat] = useState('');
const passwordColor = useMemo(() => { const passwordColor =
if (!!newPassword && !!newPasswordRepeat && newPassword !== newPasswordRepeat) { !!newPassword && !!newPasswordRepeat && newPassword !== newPasswordRepeat ? 'clr-warning' : 'clr-input';
return 'clr-warning';
} else {
return 'clr-input';
}
}, [newPassword, newPasswordRepeat]);
const canSubmit = useMemo(() => { const canSubmit = !!oldPassword && !!newPassword && !!newPasswordRepeat && newPassword === newPasswordRepeat;
return !!oldPassword && !!newPassword && !!newPasswordRepeat && newPassword === newPasswordRepeat;
}, [newPassword, newPasswordRepeat, oldPassword]);
function handleSubmit(event: React.FormEvent<HTMLFormElement>) { function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault(); event.preventDefault();

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import axios from 'axios'; import axios from 'axios';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import InfoError, { ErrorData } from '@/components/info/InfoError'; import InfoError, { ErrorData } from '@/components/info/InfoError';
@ -20,12 +20,9 @@ function EditorProfile() {
const [first_name, setFirstName] = useState(user?.first_name ?? ''); const [first_name, setFirstName] = useState(user?.first_name ?? '');
const [last_name, setLastName] = useState(user?.last_name ?? ''); const [last_name, setLastName] = useState(user?.last_name ?? '');
const isModified: boolean = useMemo(() => { const isModified =
if (!user) { user != undefined && (user.email !== email || user.first_name !== first_name || user.last_name !== last_name);
return false;
}
return user.email !== email || user.first_name !== first_name || user.last_name !== last_name;
}, [user, email, first_name, last_name]);
useBlockNavigation(isModified); useBlockNavigation(isModified);
useEffect(() => { useEffect(() => {