mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
F: Add search feature to multiple cst selector
Some checks failed
Frontend CI / build (22.x) (push) Has been cancelled
Some checks failed
Frontend CI / build (22.x) (push) Has been cancelled
This commit is contained in:
parent
e7678c979a
commit
8f1fbcde3d
|
@ -1,10 +1,18 @@
|
|||
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
|
||||
import { CstMatchMode, DependencyMode } from '@/models/miscellaneous';
|
||||
import { ExpressionStatus } from '@/models/rsform';
|
||||
import { CstType, ExpressionStatus } from '@/models/rsform';
|
||||
|
||||
import {
|
||||
IconAlias,
|
||||
IconBusiness,
|
||||
IconCstAxiom,
|
||||
IconCstBaseSet,
|
||||
IconCstConstSet,
|
||||
IconCstFunction,
|
||||
IconCstPredicate,
|
||||
IconCstStructured,
|
||||
IconCstTerm,
|
||||
IconCstTheorem,
|
||||
IconFilter,
|
||||
IconFormula,
|
||||
IconGraphCollapse,
|
||||
|
@ -132,3 +140,24 @@ export function StatusIcon({ value, size = '1.25rem', className }: DomIconProps<
|
|||
return <IconStatusError size={size} className={className} />;
|
||||
}
|
||||
}
|
||||
|
||||
export function CstTypeIcon({ value, size = '1.25rem', className }: DomIconProps<CstType>) {
|
||||
switch (value) {
|
||||
case CstType.BASE:
|
||||
return <IconCstBaseSet size={size} className={className ?? 'clr-text-green'} />;
|
||||
case CstType.CONSTANT:
|
||||
return <IconCstConstSet size={size} className={className ?? 'clr-text-green'} />;
|
||||
case CstType.STRUCTURED:
|
||||
return <IconCstStructured size={size} className={className ?? 'clr-text-green'} />;
|
||||
case CstType.TERM:
|
||||
return <IconCstTerm size={size} className={className ?? 'clr-text-primary'} />;
|
||||
case CstType.AXIOM:
|
||||
return <IconCstAxiom size={size} className={className ?? 'clr-text-red'} />;
|
||||
case CstType.FUNCTION:
|
||||
return <IconCstFunction size={size} className={className ?? 'clr-text-primary'} />;
|
||||
case CstType.PREDICATE:
|
||||
return <IconCstPredicate size={size} className={className ?? 'clr-text-red'} />;
|
||||
case CstType.THEOREM:
|
||||
return <IconCstTheorem size={size} className={className ?? 'clr-text-red'} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,12 +5,14 @@ import { useLayoutEffect, useMemo, useState } from 'react';
|
|||
|
||||
import DataTable, { createColumnHelper, RowSelectionState } from '@/components/ui/DataTable';
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { CstMatchMode } from '@/models/miscellaneous';
|
||||
import { ConstituentaID, IConstituenta, IRSForm } from '@/models/rsform';
|
||||
import { isBasicConcept } from '@/models/rsformAPI';
|
||||
import { isBasicConcept, matchConstituenta } from '@/models/rsformAPI';
|
||||
import { describeConstituenta } from '@/utils/labels';
|
||||
|
||||
import BadgeConstituenta from '../info/BadgeConstituenta';
|
||||
import NoData from '../ui/NoData';
|
||||
import SearchBar from '../ui/SearchBar';
|
||||
import ToolbarGraphSelection from './ToolbarGraphSelection';
|
||||
|
||||
interface PickMultiConstituentaProps {
|
||||
|
@ -28,18 +30,30 @@ const columnHelper = createColumnHelper<IConstituenta>();
|
|||
function PickMultiConstituenta({ id, schema, prefixID, rows, selected, setSelected }: PickMultiConstituentaProps) {
|
||||
const { colors } = useConceptOptions();
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [filtered, setFiltered] = useState<IConstituenta[]>(schema?.items ?? []);
|
||||
const [filterText, setFilterText] = useState('');
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!schema || selected.length === 0) {
|
||||
if (filtered.length === 0) {
|
||||
setRowSelection({});
|
||||
} else {
|
||||
const newRowSelection: RowSelectionState = {};
|
||||
schema.items.forEach((cst, index) => {
|
||||
newRowSelection[String(index)] = selected.includes(cst.id);
|
||||
});
|
||||
setRowSelection(newRowSelection);
|
||||
return;
|
||||
}
|
||||
}, [selected, schema]);
|
||||
const newRowSelection: RowSelectionState = {};
|
||||
filtered.forEach((cst, index) => {
|
||||
newRowSelection[String(index)] = selected.includes(cst.id);
|
||||
});
|
||||
setRowSelection(newRowSelection);
|
||||
}, [filtered, setRowSelection, selected]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!schema || schema.items.length === 0) {
|
||||
setFiltered([]);
|
||||
} else if (filterText) {
|
||||
setFiltered(schema.items.filter(cst => matchConstituenta(cst, filterText, CstMatchMode.ALL)));
|
||||
} else {
|
||||
setFiltered(schema.items);
|
||||
}
|
||||
}, [filterText, schema?.items, schema]);
|
||||
|
||||
function handleRowSelection(updater: React.SetStateAction<RowSelectionState>) {
|
||||
if (!schema) {
|
||||
|
@ -47,12 +61,12 @@ function PickMultiConstituenta({ id, schema, prefixID, rows, selected, setSelect
|
|||
} else {
|
||||
const newRowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
|
||||
const newSelection: ConstituentaID[] = [];
|
||||
schema.items.forEach((cst, index) => {
|
||||
filtered.forEach((cst, index) => {
|
||||
if (newRowSelection[String(index)] === true) {
|
||||
newSelection.push(cst.id);
|
||||
}
|
||||
});
|
||||
setSelected(newSelection);
|
||||
setSelected(prev => [...prev.filter(cst_id => !filtered.find(cst => cst.id === cst_id)), ...newSelection]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -75,10 +89,17 @@ function PickMultiConstituenta({ id, schema, prefixID, rows, selected, setSelect
|
|||
|
||||
return (
|
||||
<div>
|
||||
<div className='flex items-end gap-3 mb-3'>
|
||||
<span className='w-[24ch] select-none whitespace-nowrap'>
|
||||
<div className='flex justify-between items-center gap-3 clr-input px-3 border-x border-t rounded-t-md'>
|
||||
<div className='w-[24ch] select-none whitespace-nowrap'>
|
||||
Выбраны {selected.length} из {schema?.items.length ?? 0}
|
||||
</span>
|
||||
</div>
|
||||
<SearchBar
|
||||
id='dlg_constituents_search'
|
||||
noBorder
|
||||
className='min-w-[6rem] pr-2 flex-grow'
|
||||
value={filterText}
|
||||
onChange={setFilterText}
|
||||
/>
|
||||
{schema ? (
|
||||
<ToolbarGraphSelection
|
||||
graph={schema.graph}
|
||||
|
@ -86,7 +107,7 @@ function PickMultiConstituenta({ id, schema, prefixID, rows, selected, setSelect
|
|||
isOwned={cstID => !schema.cstByID.get(cstID)?.is_inherited}
|
||||
setSelected={setSelected}
|
||||
emptySelection={selected.length === 0}
|
||||
className='w-full ml-8'
|
||||
className='w-fit'
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
@ -97,7 +118,7 @@ function PickMultiConstituenta({ id, schema, prefixID, rows, selected, setSelect
|
|||
rows={rows}
|
||||
contentHeight='1.3rem'
|
||||
className={clsx('cc-scroll-y', 'border', 'text-sm', 'select-none')}
|
||||
data={schema?.items ?? []}
|
||||
data={filtered}
|
||||
columns={columns}
|
||||
headPosition='0rem'
|
||||
enableRowSelection
|
||||
|
|
|
@ -96,6 +96,7 @@ function PickSchema({
|
|||
<div className='border divide-y'>
|
||||
<SearchBar
|
||||
id={id ? `${id}__search` : undefined}
|
||||
className='clr-input'
|
||||
noBorder
|
||||
value={filterText}
|
||||
onChange={newValue => setFilterText(newValue)}
|
||||
|
|
|
@ -18,7 +18,7 @@ function Dropdown({ isOpen, stretchLeft, stretchTop, className, children, ...res
|
|||
<motion.div
|
||||
tabIndex={-1}
|
||||
className={clsx(
|
||||
'z-modalTooltip',
|
||||
'z-topmost',
|
||||
'absolute mt-3',
|
||||
'flex flex-col',
|
||||
'border rounded-md shadow-lg',
|
||||
|
|
|
@ -27,7 +27,7 @@ function SearchBar({ id, value, noIcon, onChange, noBorder, placeholder = 'По
|
|||
noOutline
|
||||
placeholder={placeholder}
|
||||
type='search'
|
||||
className={clsx('w-full outline-none', !noIcon && 'pl-10')}
|
||||
className={clsx('w-full outline-none bg-transparent', !noIcon && 'pl-10')}
|
||||
noBorder={noBorder}
|
||||
value={value}
|
||||
onChange={event => (onChange ? onChange(event.target.value) : undefined)}
|
||||
|
|
|
@ -29,7 +29,7 @@ function Tooltip({
|
|||
}
|
||||
return createPortal(
|
||||
<TooltipImpl
|
||||
delayShow={1000}
|
||||
delayShow={750}
|
||||
delayHide={100}
|
||||
opacity={1}
|
||||
className={clsx(
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import fileDownload from 'js-file-download';
|
||||
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { IconCSV } from '@/components/Icons';
|
||||
import SelectedCounter from '@/components/info/SelectedCounter';
|
||||
import { type RowSelectionState } from '@/components/ui/DataTable';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
import SearchBar from '@/components/ui/SearchBar';
|
||||
import AnimateFade from '@/components/wrap/AnimateFade';
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { ConstituentaID, CstType } from '@/models/rsform';
|
||||
import { CstMatchMode } from '@/models/miscellaneous';
|
||||
import { ConstituentaID, CstType, IConstituenta } from '@/models/rsform';
|
||||
import { matchConstituenta } from '@/models/rsformAPI';
|
||||
import { information } from '@/utils/labels';
|
||||
import { convertToCSV } from '@/utils/utils';
|
||||
|
||||
|
@ -29,30 +30,43 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
|
|||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const controller = useRSEdit();
|
||||
|
||||
const [filtered, setFiltered] = useState<IConstituenta[]>(controller.schema?.items ?? []);
|
||||
const [filterText, setFilterText] = useState('');
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!controller.schema || controller.selected.length === 0) {
|
||||
if (filtered.length === 0) {
|
||||
setRowSelection({});
|
||||
} else {
|
||||
const newRowSelection: RowSelectionState = {};
|
||||
controller.schema.items.forEach((cst, index) => {
|
||||
newRowSelection[String(index)] = controller.selected.includes(cst.id);
|
||||
});
|
||||
setRowSelection(newRowSelection);
|
||||
return;
|
||||
}
|
||||
}, [controller.selected, controller.schema]);
|
||||
const newRowSelection: RowSelectionState = {};
|
||||
filtered.forEach((cst, index) => {
|
||||
newRowSelection[String(index)] = controller.selected.includes(cst.id);
|
||||
});
|
||||
setRowSelection(newRowSelection);
|
||||
}, [filtered, setRowSelection, controller.selected]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!controller.schema || controller.schema.items.length === 0) {
|
||||
setFiltered([]);
|
||||
} else if (filterText) {
|
||||
setFiltered(controller.schema.items.filter(cst => matchConstituenta(cst, filterText, CstMatchMode.ALL)));
|
||||
} else {
|
||||
setFiltered(controller.schema.items);
|
||||
}
|
||||
}, [filterText, controller.schema?.items, controller.schema]);
|
||||
|
||||
const handleDownloadCSV = useCallback(() => {
|
||||
if (!controller.schema || controller.schema.items.length === 0) {
|
||||
if (!controller.schema || filtered.length === 0) {
|
||||
toast.error(information.noDataToExport);
|
||||
return;
|
||||
}
|
||||
const blob = convertToCSV(controller.schema.items);
|
||||
const blob = convertToCSV(filtered);
|
||||
try {
|
||||
fileDownload(blob, `${controller.schema.alias}.csv`, 'text/csv;charset=utf-8;');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, [controller]);
|
||||
}, [filtered, controller]);
|
||||
|
||||
function handleRowSelection(updater: React.SetStateAction<RowSelectionState>) {
|
||||
if (!controller.schema) {
|
||||
|
@ -60,12 +74,15 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
|
|||
} else {
|
||||
const newRowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
|
||||
const newSelection: ConstituentaID[] = [];
|
||||
controller.schema.items.forEach((cst, index) => {
|
||||
filtered.forEach((cst, index) => {
|
||||
if (newRowSelection[String(index)] === true) {
|
||||
newSelection.push(cst.id);
|
||||
}
|
||||
});
|
||||
controller.setSelected(newSelection);
|
||||
controller.setSelected(prev => [
|
||||
...prev.filter(cst_id => !filtered.find(cst => cst.id === cst_id)),
|
||||
...newSelection
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,21 +144,21 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
|
|||
{controller.isContentEditable ? <ToolbarRSList /> : null}
|
||||
<AnimateFade tabIndex={-1} onKeyDown={handleKeyDown}>
|
||||
{controller.isContentEditable ? (
|
||||
<SelectedCounter
|
||||
totalCount={controller.schema?.stats?.count_all ?? 0}
|
||||
selectedCount={controller.selected.length}
|
||||
position='top-[0.3rem] left-2'
|
||||
/>
|
||||
<div className='flex items-center border-b'>
|
||||
<div className='px-2'>
|
||||
Выбор {controller.selected.length} из {controller.schema?.stats?.count_all ?? 0}
|
||||
</div>
|
||||
<SearchBar
|
||||
id='constituents_search'
|
||||
noBorder
|
||||
className='w-[8rem]'
|
||||
value={filterText}
|
||||
onChange={setFilterText}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className={clsx('border-b', {
|
||||
'pt-[2.3rem]': controller.isContentEditable,
|
||||
'relative top-[-1px]': !controller.isContentEditable
|
||||
})}
|
||||
/>
|
||||
|
||||
<Overlay position='top-[0.25rem] right-[1rem]' layer='z-tooltip'>
|
||||
<Overlay position='top-[0.25rem] right-[1rem]' layer='z-navigation'>
|
||||
<MiniButton
|
||||
title='Выгрузить в формате CSV'
|
||||
icon={<IconCSV size='1.25rem' className='icon-green' />}
|
||||
|
@ -150,7 +167,7 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
|
|||
</Overlay>
|
||||
|
||||
<TableRSList
|
||||
items={controller.schema?.items}
|
||||
items={filtered}
|
||||
maxHeight={tableHeight}
|
||||
enableSelection={controller.isContentEditable}
|
||||
selected={rowSelection}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { CstTypeIcon } from '@/components/DomainIcons';
|
||||
import {
|
||||
IconClone,
|
||||
IconDestroy,
|
||||
|
@ -16,7 +17,6 @@ import Overlay from '@/components/ui/Overlay';
|
|||
import useDropdown from '@/hooks/useDropdown';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
import { CstType } from '@/models/rsform';
|
||||
import { getCstTypePrefix } from '@/models/rsformAPI';
|
||||
import { prefixes } from '@/utils/constants';
|
||||
import { getCstTypeShortcut, labelCstType, prepareTooltip } from '@/utils/labels';
|
||||
|
||||
|
@ -27,7 +27,10 @@ function ToolbarRSList() {
|
|||
const insertMenu = useDropdown();
|
||||
|
||||
return (
|
||||
<Overlay position='top-1 right-1/2 translate-x-1/2' className='items-start cc-icons'>
|
||||
<Overlay
|
||||
position='top-1 right-4 translate-x-0 md:right-1/2 md:translate-x-1/2'
|
||||
className='cc-icons items-start outline-none transition-all duration-500'
|
||||
>
|
||||
{controller.schema && controller.schema?.oss.length > 0 ? (
|
||||
<MiniSelectorOSS
|
||||
items={controller.schema.oss}
|
||||
|
@ -52,18 +55,6 @@ function ToolbarRSList() {
|
|||
disabled={controller.isProcessing || controller.nothingSelected}
|
||||
onClick={controller.moveDown}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Клонировать конституенту', 'Alt + V')}
|
||||
icon={<IconClone size='1.25rem' className='icon-green' />}
|
||||
disabled={controller.isProcessing || controller.selected.length !== 1}
|
||||
onClick={controller.cloneCst}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Добавить новую конституенту...', 'Alt + `')}
|
||||
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
||||
disabled={controller.isProcessing}
|
||||
onClick={() => controller.createCst(undefined, false)}
|
||||
/>
|
||||
<div ref={insertMenu.ref}>
|
||||
<MiniButton
|
||||
title='Добавить пустую конституенту'
|
||||
|
@ -72,17 +63,30 @@ function ToolbarRSList() {
|
|||
disabled={controller.isProcessing}
|
||||
onClick={insertMenu.toggle}
|
||||
/>
|
||||
<Dropdown isOpen={insertMenu.isOpen}>
|
||||
<Dropdown isOpen={insertMenu.isOpen} className='-translate-x-1/2 md:translate-x-0'>
|
||||
{Object.values(CstType).map(typeStr => (
|
||||
<DropdownButton
|
||||
key={`${prefixes.csttype_list}${typeStr}`}
|
||||
text={`${getCstTypePrefix(typeStr as CstType)}1 — ${labelCstType(typeStr as CstType)}`}
|
||||
text={labelCstType(typeStr as CstType)}
|
||||
icon={<CstTypeIcon value={typeStr as CstType} size='1.25rem' />}
|
||||
onClick={() => controller.createCst(typeStr as CstType, true)}
|
||||
titleHtml={getCstTypeShortcut(typeStr as CstType)}
|
||||
/>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Добавить новую конституенту...', 'Alt + `')}
|
||||
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
||||
disabled={controller.isProcessing}
|
||||
onClick={() => controller.createCst(undefined, false)}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Клонировать конституенту', 'Alt + V')}
|
||||
icon={<IconClone size='1.25rem' className='icon-green' />}
|
||||
disabled={controller.isProcessing || controller.selected.length !== 1}
|
||||
onClick={controller.cloneCst}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Удалить выбранные', 'Delete')}
|
||||
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
||||
|
|
|
@ -267,7 +267,7 @@ function RSTabs() {
|
|||
<TabLabel label='Граф термов' />
|
||||
</TabList>
|
||||
|
||||
<AnimateFade className='overflow-y-auto' style={{ maxHeight: panelHeight }}>
|
||||
<AnimateFade className='overflow-y-auto overflow-x-hidden' style={{ maxHeight: panelHeight }}>
|
||||
{cardPanel}
|
||||
{listPanel}
|
||||
{editorPanel}
|
||||
|
|
Loading…
Reference in New Issue
Block a user