F: Improve schema picker filtering

This commit is contained in:
Ivan 2024-09-12 13:27:20 +03:00
parent dc60c79f81
commit f7354c9883
13 changed files with 131 additions and 36 deletions

View File

@ -1,13 +1,21 @@
import { useLayoutEffect, useMemo, useState } from 'react'; import { useCallback, useLayoutEffect, useMemo, 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';
import SearchBar from '@/components/ui/SearchBar'; import SearchBar from '@/components/ui/SearchBar';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useLibrary } from '@/context/LibraryContext';
import useDropdown from '@/hooks/useDropdown';
import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library'; import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library';
import { matchLibraryItem } from '@/models/libraryAPI'; import { matchLibraryItem } from '@/models/libraryAPI';
import { prefixes } from '@/utils/constants';
import { IconClose, IconFolderTree } from '../Icons';
import { CProps } from '../props';
import Dropdown from '../ui/Dropdown';
import FlexColumn from '../ui/FlexColumn'; import FlexColumn from '../ui/FlexColumn';
import MiniButton from '../ui/MiniButton';
import SelectLocation from './SelectLocation';
interface PickSchemaProps { interface PickSchemaProps {
id?: string; id?: string;
@ -35,18 +43,27 @@ function PickSchema({
}: PickSchemaProps) { }: PickSchemaProps) {
const intl = useIntl(); const intl = useIntl();
const { colors } = useConceptOptions(); const { colors } = useConceptOptions();
const { folders } = useLibrary();
const [filterText, setFilterText] = useState(initialFilter); const [filterText, setFilterText] = useState(initialFilter);
const [filterLocation, setFilterLocation] = useState('');
const [filtered, setFiltered] = useState<ILibraryItem[]>([]); const [filtered, setFiltered] = useState<ILibraryItem[]>([]);
const baseFiltered = useMemo( const baseFiltered = useMemo(
() => items.filter(item => item.item_type === itemType && (!baseFilter || baseFilter(item))), () => items.filter(item => item.item_type === itemType && (!baseFilter || baseFilter(item))),
[items, itemType, baseFilter] [items, itemType, baseFilter]
); );
const locationMenu = useDropdown();
useLayoutEffect(() => { useLayoutEffect(() => {
const newFiltered = baseFiltered.filter(item => matchLibraryItem(item, filterText)); let newFiltered = baseFiltered.filter(item => matchLibraryItem(item, filterText));
if (filterLocation.length > 0) {
newFiltered = newFiltered.filter(
item => item.location === filterLocation || item.location.startsWith(`${filterLocation}/`)
);
}
setFiltered(newFiltered); setFiltered(newFiltered);
}, [filterText]); }, [filterText, filterLocation]);
const columns = useMemo( const columns = useMemo(
() => [ () => [
@ -92,15 +109,51 @@ function PickSchema({
[value, colors] [value, colors]
); );
const handleLocationClick = useCallback(
(event: CProps.EventMouse, newValue: string) => {
event.preventDefault();
event.stopPropagation();
locationMenu.hide();
setFilterLocation(newValue);
},
[locationMenu]
);
return ( return (
<div className='border divide-y'> <div className='border divide-y'>
<div className='flex justify-between clr-input items-center pr-1'>
<SearchBar <SearchBar
id={id ? `${id}__search` : undefined} id={id ? `${id}__search` : undefined}
className='clr-input' className='clr-input w-full'
noBorder noBorder
value={filterText} value={filterText}
onChange={newValue => setFilterText(newValue)} onChange={newValue => setFilterText(newValue)}
/> />
<div ref={locationMenu.ref}>
<MiniButton
icon={<IconFolderTree size='1.25rem' className={!!filterLocation ? 'icon-green' : 'icon-primary'} />}
title='Фильтр по расположению'
className='mt-1'
onClick={() => locationMenu.toggle()}
/>
<Dropdown isOpen={locationMenu.isOpen} stretchLeft className='w-[20rem] h-[12.5rem] z-modalTooltip mt-0'>
<SelectLocation
folderTree={folders}
value={filterLocation}
prefix={prefixes.folders_list}
dense
onClick={(event, target) => handleLocationClick(event, target.getPath())}
/>
</Dropdown>
</div>
{filterLocation.length > 0 ? (
<MiniButton
icon={<IconClose size='1.25rem' className='icon-red' />}
title='Сбросить фильтр'
onClick={() => setFilterLocation('')}
/>
) : null}
</div>
<DataTable <DataTable
id={id} id={id}
rows={rows} rows={rows}

View File

@ -15,13 +15,21 @@ import SelectLocation from './SelectLocation';
interface SelectLocationContextProps extends CProps.Styling { interface SelectLocationContextProps extends CProps.Styling {
value: string; value: string;
title?: string;
folderTree: FolderTree; folderTree: FolderTree;
stretchTop?: boolean; stretchTop?: boolean;
onChange: (newValue: string) => void; onChange: (newValue: string) => void;
} }
function SelectLocationContext({ value, folderTree, onChange, className, style }: SelectLocationContextProps) { function SelectLocationContext({
value,
title = 'Проводник...',
folderTree,
onChange,
className,
style
}: SelectLocationContextProps) {
const menu = useDropdown(); const menu = useDropdown();
const handleClick = useCallback( const handleClick = useCallback(
@ -37,7 +45,7 @@ function SelectLocationContext({ value, folderTree, onChange, className, style }
return ( return (
<div ref={menu.ref} className='h-full text-right self-start mt-[-0.25rem] ml-[-1.5rem]'> <div ref={menu.ref} className='h-full text-right self-start mt-[-0.25rem] ml-[-1.5rem]'>
<MiniButton <MiniButton
title='Проводник...' title={title}
hideTitle={menu.isOpen} hideTitle={menu.isOpen}
icon={<IconFolderTree size='1.25rem' className='icon-green' />} icon={<IconFolderTree size='1.25rem' className='icon-green' />}
onClick={() => menu.toggle()} onClick={() => menu.toggle()}

View File

@ -63,7 +63,7 @@ function DlgChangeInputSchema({ oss, hideWindow, target, onSubmit }: DlgChangeIn
itemType={LibraryItemType.RSFORM} itemType={LibraryItemType.RSFORM}
value={selected} // prettier: split-line value={selected} // prettier: split-line
onSelectValue={handleSelectLocation} onSelectValue={handleSelectLocation}
rows={8} rows={14}
baseFilter={baseFilter} baseFilter={baseFilter}
/> />
</Modal> </Modal>

View File

@ -58,7 +58,7 @@ function DlgInlineSynthesis({ hideWindow, receiver, onInlineSynthesis }: DlgInli
const schemaPanel = useMemo( const schemaPanel = useMemo(
() => ( () => (
<TabPanel> <TabPanel>
<TabSchema selected={donorID} setSelected={setDonorID} /> <TabSchema selected={donorID} setSelected={setDonorID} receiver={receiver} />
</TabPanel> </TabPanel>
), ),
[donorID] [donorID]

View File

@ -7,15 +7,19 @@ import TextInput from '@/components/ui/TextInput';
import AnimateFade from '@/components/wrap/AnimateFade'; import AnimateFade from '@/components/wrap/AnimateFade';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
import { LibraryItemID, LibraryItemType } from '@/models/library'; import { LibraryItemID, LibraryItemType } from '@/models/library';
import { IRSForm } from '@/models/rsform';
import { sortItemsForInlineSynthesis } from '@/models/rsformAPI';
interface TabSchemaProps { interface TabSchemaProps {
selected?: LibraryItemID; selected?: LibraryItemID;
setSelected: (newValue: LibraryItemID) => void; setSelected: (newValue: LibraryItemID) => void;
receiver: IRSForm;
} }
function TabSchema({ selected, 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 = useMemo(() => library.items.find(item => item.id === selected), [selected, library.items]);
const sortedItems = useMemo(() => sortItemsForInlineSynthesis(receiver, library.items), [receiver, library.items]);
return ( return (
<AnimateFade className='flex flex-col'> <AnimateFade className='flex flex-col'>
@ -33,7 +37,7 @@ function TabSchema({ selected, setSelected }: TabSchemaProps) {
</div> </div>
<PickSchema <PickSchema
id='dlg_schema_picker' // prettier: split lines id='dlg_schema_picker' // prettier: split lines
items={library.items} items={sortedItems}
itemType={LibraryItemType.RSFORM} itemType={LibraryItemType.RSFORM}
rows={14} rows={14}
value={selected} value={selected}

View File

@ -32,6 +32,8 @@ export enum LocationHead {
LIBRARY = '/L' LIBRARY = '/L'
} }
export const BASIC_SCHEMAS = '/L/Базовые';
/** /**
* Represents {@link LibraryItem} identifier type. * Represents {@link LibraryItem} identifier type.
*/ */

View File

@ -44,7 +44,7 @@ describe('Testing matching LibraryItem', () => {
expect(matchLibraryItem(item1, item1.title + '@invalid')).toEqual(false); expect(matchLibraryItem(item1, item1.title + '@invalid')).toEqual(false);
expect(matchLibraryItem(item1, item1.alias + '@invalid')).toEqual(false); expect(matchLibraryItem(item1, item1.alias + '@invalid')).toEqual(false);
expect(matchLibraryItem(item1, item1.time_create)).toEqual(false); expect(matchLibraryItem(item1, item1.time_create)).toEqual(false);
expect(matchLibraryItem(item1, item1.comment)).toEqual(false); expect(matchLibraryItem(item1, item1.comment)).toEqual(true);
}); });
}); });

View File

@ -17,7 +17,7 @@ const LOCATION_REGEXP = /^\/[PLUS]((\/[!\d\p{L}]([!\d\p{L}\- ]*[!\d\p{L}])?)*)?$
*/ */
export function matchLibraryItem(target: ILibraryItem, query: string): boolean { export function matchLibraryItem(target: ILibraryItem, query: string): boolean {
const matcher = new TextMatcher(query); const matcher = new TextMatcher(query);
return matcher.test(target.alias) || matcher.test(target.title); return matcher.test(target.alias) || matcher.test(target.title) || matcher.test(target.comment);
} }
/** /**

View File

@ -26,9 +26,6 @@ export function matchOperation(target: IOperation, query: string): boolean {
/** /**
* Sorts library items relevant for the specified {@link IOperationSchema}. * Sorts library items relevant for the specified {@link IOperationSchema}.
*
* @param oss - The {@link IOperationSchema} to be sorted.
* @param items - The items to be sorted.
*/ */
export function sortItemsForOSS(oss: IOperationSchema, items: ILibraryItem[]): ILibraryItem[] { export function sortItemsForOSS(oss: IOperationSchema, items: ILibraryItem[]): ILibraryItem[] {
const result = items.filter(item => item.location === oss.location); const result = items.filter(item => item.location === oss.location);

View File

@ -4,6 +4,7 @@
import { TextMatcher } from '@/utils/utils'; import { TextMatcher } from '@/utils/utils';
import { BASIC_SCHEMAS, ILibraryItem } from './library';
import { CstMatchMode } from './miscellaneous'; import { CstMatchMode } from './miscellaneous';
import { import {
CATEGORY_CST_TYPE, CATEGORY_CST_TYPE,
@ -308,3 +309,31 @@ export function generateAlias(type: CstType, schema: IRSForm, takenAliases: stri
} }
return alias; return alias;
} }
/**
* Sorts library items relevant for InlineSynthesis with specified {@link IRSForm}.
*/
export function sortItemsForInlineSynthesis(receiver: IRSForm, items: ILibraryItem[]): ILibraryItem[] {
const result = items.filter(item => item.location === receiver.location);
for (const item of items) {
if (item.visible && item.owner === item.owner && !result.includes(item)) {
result.push(item);
}
}
for (const item of items) {
if (!result.includes(item) && item.location.startsWith(BASIC_SCHEMAS)) {
result.push(item);
}
}
for (const item of items) {
if (item.visible && !result.includes(item)) {
result.push(item);
}
}
for (const item of items) {
if (!result.includes(item)) {
result.push(item);
}
}
return result;
}

View File

@ -40,6 +40,21 @@ function ViewSideLocation({
const { user } = useAuth(); const { user } = useAuth();
const { items } = useLibrary(); const { items } = useLibrary();
const windowSize = useWindowSize(); const windowSize = useWindowSize();
const canRename = useMemo(() => {
if (active.length <= 3 || !user) {
return false;
}
if (user.is_staff) {
return true;
}
const owned = items.filter(item => item.owner == user.id);
const located = owned.filter(item => item.location == active || item.location.startsWith(`${active}/`));
return located.length !== 0;
}, [active, user, items]);
const animations = useMemo(() => animateSideMinWidth(windowSize.isSmall ? '10rem' : '15rem'), [windowSize]);
const handleClickFolder = useCallback( const handleClickFolder = useCallback(
(event: CProps.EventMouse, target: FolderNode) => { (event: CProps.EventMouse, target: FolderNode) => {
event.preventDefault(); event.preventDefault();
@ -56,20 +71,6 @@ function ViewSideLocation({
[setActive] [setActive]
); );
const canRename = useMemo(() => {
if (active.length <= 3 || !user) {
return false;
}
if (user.is_staff) {
return true;
}
const owned = items.filter(item => item.owner == user.id);
const located = owned.filter(item => item.location == active || item.location.startsWith(`${active}/`));
return located.length !== 0;
}, [active, user, items]);
const animations = useMemo(() => animateSideMinWidth(windowSize.isSmall ? '10rem' : '15rem'), [windowSize]);
return ( return (
<motion.div <motion.div
className={clsx('max-w-[10rem] sm:max-w-[15rem]', 'flex flex-col', 'text:xs sm:text-sm', 'select-none')} className={clsx('max-w-[10rem] sm:max-w-[15rem]', 'flex flex-col', 'text:xs sm:text-sm', 'select-none')}

View File

@ -111,7 +111,7 @@ function NodeContextMenu({
text='Редактировать' text='Редактировать'
title='Редактировать операцию' title='Редактировать операцию'
icon={<IconEdit2 size='1rem' className='icon-primary' />} icon={<IconEdit2 size='1rem' className='icon-primary' />}
disabled={controller.isProcessing} disabled={!controller.isMutable || controller.isProcessing}
onClick={handleEditOperation} onClick={handleEditOperation}
/> />

View File

@ -132,6 +132,7 @@
.clr-btn-nav, .clr-btn-nav,
.clr-btn-clear { .clr-btn-clear {
color: var(--cl-fg-80); color: var(--cl-fg-80);
background-color: transparent;
&:disabled { &:disabled {
color: var(--cl-fg-60); color: var(--cl-fg-60);
} }