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 DataTable, { createColumnHelper, IConditionalStyle } from '@/components/ui/DataTable';
import SearchBar from '@/components/ui/SearchBar';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useLibrary } from '@/context/LibraryContext';
import useDropdown from '@/hooks/useDropdown';
import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library';
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 MiniButton from '../ui/MiniButton';
import SelectLocation from './SelectLocation';
interface PickSchemaProps {
id?: string;
@ -35,18 +43,27 @@ function PickSchema({
}: PickSchemaProps) {
const intl = useIntl();
const { colors } = useConceptOptions();
const { folders } = useLibrary();
const [filterText, setFilterText] = useState(initialFilter);
const [filterLocation, setFilterLocation] = useState('');
const [filtered, setFiltered] = useState<ILibraryItem[]>([]);
const baseFiltered = useMemo(
() => items.filter(item => item.item_type === itemType && (!baseFilter || baseFilter(item))),
[items, itemType, baseFilter]
);
const locationMenu = useDropdown();
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);
}, [filterText]);
}, [filterText, filterLocation]);
const columns = useMemo(
() => [
@ -92,15 +109,51 @@ function PickSchema({
[value, colors]
);
const handleLocationClick = useCallback(
(event: CProps.EventMouse, newValue: string) => {
event.preventDefault();
event.stopPropagation();
locationMenu.hide();
setFilterLocation(newValue);
},
[locationMenu]
);
return (
<div className='border divide-y'>
<SearchBar
id={id ? `${id}__search` : undefined}
className='clr-input'
noBorder
value={filterText}
onChange={newValue => setFilterText(newValue)}
/>
<div className='flex justify-between clr-input items-center pr-1'>
<SearchBar
id={id ? `${id}__search` : undefined}
className='clr-input w-full'
noBorder
value={filterText}
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
id={id}
rows={rows}

View File

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

View File

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

View File

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

View File

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

View File

@ -32,6 +32,8 @@ export enum LocationHead {
LIBRARY = '/L'
}
export const BASIC_SCHEMAS = '/L/Базовые';
/**
* 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.alias + '@invalid')).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 {
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}.
*
* @param oss - The {@link IOperationSchema} to be sorted.
* @param items - The items to be sorted.
*/
export function sortItemsForOSS(oss: IOperationSchema, items: ILibraryItem[]): ILibraryItem[] {
const result = items.filter(item => item.location === oss.location);

View File

@ -4,6 +4,7 @@
import { TextMatcher } from '@/utils/utils';
import { BASIC_SCHEMAS, ILibraryItem } from './library';
import { CstMatchMode } from './miscellaneous';
import {
CATEGORY_CST_TYPE,
@ -308,3 +309,31 @@ export function generateAlias(type: CstType, schema: IRSForm, takenAliases: stri
}
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 { items } = useLibrary();
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(
(event: CProps.EventMouse, target: FolderNode) => {
event.preventDefault();
@ -56,20 +71,6 @@ function ViewSideLocation({
[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 (
<motion.div
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='Редактировать'
title='Редактировать операцию'
icon={<IconEdit2 size='1rem' className='icon-primary' />}
disabled={controller.isProcessing}
disabled={!controller.isMutable || controller.isProcessing}
onClick={handleEditOperation}
/>

View File

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