ConceptPortal-public/rsconcept/frontend/src/pages/RSFormPage/elements/ViewSideConstituents.tsx

283 lines
9.7 KiB
TypeScript
Raw Normal View History

2023-09-10 20:17:18 +03:00
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
2023-07-25 20:27:29 +03:00
import Dropdown from '../../../components/common/Dropdown';
import DropdownButton from '../../../components/common/DropdownButton';
import SelectorButton from '../../../components/common/SelectorButton';
2023-09-10 20:17:18 +03:00
import DataTable, { createColumnHelper, IConditionalStyle, VisibilityState } from '../../../components/DataTable';
2023-10-16 02:17:31 +03:00
import { CogIcon, FilterIcon, MagnifyingGlassIcon } from '../../../components/Icons';
import ConstituentaBadge from '../../../components/shared/ConstituentaBadge';
2023-07-28 00:03:37 +03:00
import { useRSForm } from '../../../context/RSFormContext';
import { useConceptTheme } from '../../../context/ThemeContext';
2023-10-06 14:39:32 +03:00
import useDropdown from '../../../hooks/useDropdown';
2023-07-28 00:03:37 +03:00
import useLocalStorage from '../../../hooks/useLocalStorage';
2023-09-10 20:17:18 +03:00
import useWindowSize from '../../../hooks/useWindowSize';
2023-10-06 14:39:32 +03:00
import { DependencyMode as CstSource } from '../../../models/miscelanious';
2023-09-11 20:31:54 +03:00
import { CstMatchMode } from '../../../models/miscelanious';
import { applyGraphFilter } from '../../../models/miscelaniousAPI';
import { CstType, IConstituenta } from '../../../models/rsform';
import { createMockConstituenta, isMockCst, matchConstituenta } from '../../../models/rsformAPI';
import { extractGlobals } from '../../../models/rslangAPI';
import { prefixes } from '../../../utils/constants';
2023-10-06 14:39:32 +03:00
import {
describeConstituenta, describeCstMathchMode,
describeCstSource, labelCstMathchMode,
labelCstSource
} from '../../../utils/labels';
2023-07-20 17:11:03 +03:00
// Height that should be left to accomodate navigation panel + bottom margin
2023-08-27 15:39:49 +03:00
const LOCAL_NAVIGATION_H = '2.1rem';
2023-09-10 20:17:18 +03:00
// Window width cutoff for expression show
const COLUMN_EXPRESSION_HIDE_THRESHOLD = 1500;
2023-07-28 00:03:37 +03:00
interface ViewSideConstituentsProps {
2023-07-20 17:11:03 +03:00
expression: string
baseHeight: string
activeID?: number
onOpenEdit: (cstID: number) => void
}
const columnHelper = createColumnHelper<IConstituenta>();
function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }: ViewSideConstituentsProps) {
2023-09-10 20:17:18 +03:00
const windowSize = useWindowSize();
2023-08-27 15:39:49 +03:00
const { noNavigation, colors } = useConceptTheme();
const { schema } = useRSForm();
2023-09-10 20:17:18 +03:00
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({'expression': true})
const [filterMatch, setFilterMatch] = useLocalStorage('side-filter-match', CstMatchMode.ALL);
const [filterText, setFilterText] = useLocalStorage('side-filter-text', '');
2023-10-06 14:39:32 +03:00
const [filterSource, setFilterSource] = useLocalStorage('side-filter-dependency', CstSource.ALL);
2023-07-25 20:27:29 +03:00
const [filteredData, setFilteredData] = useState<IConstituenta[]>(schema?.items ?? []);
2023-07-20 17:11:03 +03:00
2023-10-06 14:39:32 +03:00
const matchModeMenu = useDropdown();
const sourceMenu = useDropdown();
2023-09-10 20:17:18 +03:00
useLayoutEffect(
() => {
setColumnVisibility(prev => {
const newValue = (windowSize.width ?? 0) >= COLUMN_EXPRESSION_HIDE_THRESHOLD;
if (newValue === prev['expression']) {
return prev;
} else {
return {'expression': newValue}
}
});
}, [windowSize]);
useLayoutEffect(
() => {
2023-07-20 17:11:03 +03:00
if (!schema?.items) {
setFilteredData([]);
return;
}
let filtered: IConstituenta[] = [];
2023-10-06 14:39:32 +03:00
if (filterSource === CstSource.EXPRESSION) {
const aliases = extractGlobals(expression);
filtered = schema.items.filter((cst) => aliases.has(cst.alias));
const names = filtered.map(cst => cst.alias)
2023-07-25 20:27:29 +03:00
const diff = Array.from(aliases).filter(name => !names.includes(name));
if (diff.length > 0) {
diff.forEach(
(alias, index) => filtered.push(
2023-09-21 14:58:01 +03:00
createMockConstituenta(
schema.id,
-index,
alias,
CstType.BASE,
'Конституента отсутствует'
)
)
);
}
} else if (!activeID) {
filtered = schema.items
2023-07-20 17:11:03 +03:00
} else {
filtered = applyGraphFilter(schema, activeID, filterSource);
2023-07-20 17:11:03 +03:00
}
if (filterText) {
filtered = filtered.filter(cst => matchConstituenta(cst, filterText, filterMatch));
}
setFilteredData(filtered);
2023-11-07 18:03:37 +03:00
}, [filterText, setFilteredData, filterSource, expression, schema?.items, schema, filterMatch, activeID]);
2023-07-20 17:11:03 +03:00
const handleRowClicked = useCallback(
2023-07-25 20:27:29 +03:00
(cst: IConstituenta, event: React.MouseEvent<Element, MouseEvent>) => {
if (event.altKey && !isMockCst(cst)) {
onOpenEdit(cst.id);
2023-07-20 17:11:03 +03:00
}
}, [onOpenEdit]);
2023-07-20 17:11:03 +03:00
const handleDoubleClick = useCallback(
(cst: IConstituenta) => {
if (!isMockCst(cst)) {
onOpenEdit(cst.id);
}
}, [onOpenEdit]);
2023-07-20 17:11:03 +03:00
2023-10-06 14:39:32 +03:00
const handleMatchModeChange = useCallback(
(newValue: CstMatchMode) => {
matchModeMenu.hide();
setFilterMatch(newValue);
}, [matchModeMenu, setFilterMatch]);
const handleSourceChange = useCallback(
(newValue: CstSource) => {
sourceMenu.hide();
setFilterSource(newValue);
}, [sourceMenu, setFilterSource]);
const columns = useMemo(
() => [
columnHelper.accessor('alias', {
id: 'alias',
header: 'Имя',
size: 65,
minSize: 65,
2023-10-16 01:22:08 +03:00
footer: undefined,
cell: props =>
<ConstituentaBadge
theme={colors}
value={props.row.original}
prefixID={prefixes.cst_list}
/>
}),
2023-09-21 14:58:01 +03:00
columnHelper.accessor(cst => describeConstituenta(cst), {
id: 'description',
header: 'Описание',
2023-10-04 18:46:52 +03:00
size: 1000,
2023-10-23 18:22:55 +03:00
minSize: 250,
2023-10-04 18:46:52 +03:00
maxSize: 1000,
cell: props =>
<div style={{
fontSize: 12,
color: isMockCst(props.row.original) ? colors.fgWarning : undefined
}}>
{props.getValue()}
</div>
}),
columnHelper.accessor('definition_formal', {
id: 'expression',
header: 'Выражение',
2023-10-04 18:46:52 +03:00
size: 2000,
minSize: 0,
2023-10-04 18:46:52 +03:00
maxSize: 2000,
2023-09-10 20:17:18 +03:00
enableHiding: true,
cell: props =>
<div style={{
fontSize: 12,
color: isMockCst(props.row.original) ? colors.fgWarning : undefined
}}>
{props.getValue()}
</div>
})
2023-08-27 00:19:19 +03:00
], [colors]);
2023-09-10 20:17:18 +03:00
const conditionalRowStyles = useMemo(
(): IConditionalStyle<IConstituenta>[] => [
{
when: (cst: IConstituenta) => cst.id === activeID,
style: {
backgroundColor: colors.bgSelected
},
}
], [activeID, colors]);
const maxHeight = useMemo(
() => {
const siblingHeight = `${baseHeight} - ${LOCAL_NAVIGATION_H}`
return (noNavigation ?
2023-10-14 21:17:21 +03:00
`calc(min(100vh - 8.2rem, ${siblingHeight}))`
: `calc(min(100vh - 11.7rem, ${siblingHeight}))`);
}, [noNavigation, baseHeight]);
return (<>
2023-10-23 18:22:55 +03:00
<div className='sticky top-0 left-0 right-0 flex items-stretch justify-between gap-1 pl-2 border-b clr-input'>
2023-10-04 20:38:58 +03:00
<div className='absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none text-controls'>
<MagnifyingGlassIcon />
</div>
<input type='text'
2023-10-23 18:22:55 +03:00
className='w-full min-w-[6rem] pr-2 pl-8 py-1 outline-none select-none hover:text-clip clr-input'
2023-10-06 14:39:32 +03:00
placeholder='Поиск'
value={filterText}
onChange={event => setFilterText(event.target.value)}
/>
2023-10-14 23:46:36 +03:00
<div className='flex'>
<div ref={matchModeMenu.ref}>
<SelectorButton transparent tabIndex={-1}
2023-10-14 23:46:36 +03:00
tooltip='Настройка атрибутов для фильтрации'
dimensions='w-fit h-full'
2023-10-16 02:17:31 +03:00
icon={<FilterIcon size={5} />}
2023-10-14 23:46:36 +03:00
text={labelCstMathchMode(filterMatch)}
onClick={matchModeMenu.toggle}
/>
{ matchModeMenu.isActive &&
<Dropdown stretchLeft>
{Object.values(CstMatchMode).filter(value => !isNaN(Number(value))).map(
2023-10-14 23:46:36 +03:00
(value, index) => {
const matchMode = value as CstMatchMode;
return (
<DropdownButton
key={`${prefixes.cst_match_mode_list}${index}`}
onClick={() => handleMatchModeChange(matchMode)}
>
<p><span className='font-semibold'>{labelCstMathchMode(matchMode)}:</span> {describeCstMathchMode(matchMode)}</p>
</DropdownButton>);
})}
</Dropdown>}
</div>
2023-10-06 14:39:32 +03:00
2023-10-14 23:46:36 +03:00
<div ref={sourceMenu.ref}>
<SelectorButton transparent tabIndex={-1}
2023-10-14 23:46:36 +03:00
tooltip='Настройка фильтрации по графу термов'
dimensions='w-fit h-full'
icon={<CogIcon size={4} />}
text={labelCstSource(filterSource)}
onClick={sourceMenu.toggle}
/>
{sourceMenu.isActive ?
2023-10-14 23:46:36 +03:00
<Dropdown stretchLeft>
{Object.values(CstSource).filter(value => !isNaN(Number(value))).map(
2023-10-14 23:46:36 +03:00
(value, index) => {
const source = value as CstSource;
return (
<DropdownButton
key={`${prefixes.cst_source_list}${index}`}
onClick={() => handleSourceChange(source)}
>
<p><span className='font-semibold'>{labelCstSource(source)}:</span> {describeCstSource(source)}</p>
</DropdownButton>);
})}
</Dropdown> : null}
2023-10-14 23:46:36 +03:00
</div>
2023-10-06 14:39:32 +03:00
</div>
</div>
2023-11-24 19:47:03 +03:00
<div className='overflow-y-auto text-sm overscroll-none' style={{maxHeight : `${maxHeight}`}}>
<DataTable dense noFooter
2023-07-20 17:11:03 +03:00
data={filteredData}
columns={columns}
2023-09-10 20:17:18 +03:00
conditionalRowStyles={conditionalRowStyles}
2023-09-30 12:47:27 +03:00
headPosition='0'
2023-09-10 20:17:18 +03:00
enableHiding
columnVisibility={columnVisibility}
onColumnVisibilityChange={setColumnVisibility}
noDataComponent={
2023-10-14 23:56:08 +03:00
<span className='flex flex-col justify-center p-2 text-center min-h-[5rem] select-none'>
<p>Список конституент пуст</p>
<p>Измените параметры фильтра</p>
</span>
}
2023-07-20 17:11:03 +03:00
onRowDoubleClicked={handleDoubleClick}
onRowClicked={handleRowClicked}
/>
</div>
</>);
2023-07-20 17:11:03 +03:00
}
2023-07-28 00:03:37 +03:00
export default ViewSideConstituents;