mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
F: Improve schema picker filtering
This commit is contained in:
parent
dc60c79f81
commit
f7354c9883
|
@ -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'>
|
||||||
<SearchBar
|
<div className='flex justify-between clr-input items-center pr-1'>
|
||||||
id={id ? `${id}__search` : undefined}
|
<SearchBar
|
||||||
className='clr-input'
|
id={id ? `${id}__search` : undefined}
|
||||||
noBorder
|
className='clr-input w-full'
|
||||||
value={filterText}
|
noBorder
|
||||||
onChange={newValue => setFilterText(newValue)}
|
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
|
<DataTable
|
||||||
id={id}
|
id={id}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
|
|
|
@ -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()}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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')}
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user