F: Rework user filter for LibraryPage

This commit is contained in:
Ivan 2024-09-27 12:04:10 +03:00
parent 77be306cf1
commit df81c4e125
10 changed files with 134 additions and 56 deletions

View File

@ -75,7 +75,7 @@ export function SubfoldersIcon({ value, size = '1.25rem', className }: DomIconPr
if (value) {
return <IconSubfolders size={size} className={className ?? 'clr-text-green'} />;
} else {
return <IconSubfolders size={size} className={className ?? 'clr-text-controls'} />;
return <IconSubfolders size={size} className={className ?? 'clr-text-primary'} />;
}
}

View File

@ -34,6 +34,7 @@ export { LuMoon as IconDarkTheme } from 'react-icons/lu';
export { LuSun as IconLightTheme } from 'react-icons/lu';
export { LuFolderTree as IconFolderTree } from 'react-icons/lu';
export { LuFolder as IconFolder } from 'react-icons/lu';
export { LuFolderSearch as IconFolderSearch } from 'react-icons/lu';
export { LuFolders as IconSubfolders } from 'react-icons/lu';
export { LuFolderEdit as IconFolderEdit } from 'react-icons/lu';
export { LuFolderOpen as IconFolderOpened } from 'react-icons/lu';
@ -57,6 +58,7 @@ export { PiFileCsv as IconCSV } from 'react-icons/pi';
export { LuUserCircle2 as IconUser } from 'react-icons/lu';
export { FaCircleUser as IconUser2 } from 'react-icons/fa6';
export { TbUserEdit as IconEditor } from 'react-icons/tb';
export { TbUserSearch as IconUserSearch } from 'react-icons/tb';
export { LuCrown as IconOwner } from 'react-icons/lu';
export { TbMeteor as IconAdmin } from 'react-icons/tb';
export { TbMeteorOff as IconAdminOff } from 'react-icons/tb';

View File

@ -110,6 +110,9 @@ export const LibraryState = ({ children }: React.PropsWithChildren) => {
if (filter.isEditor !== undefined) {
result = result.filter(item => filter.isEditor == user?.editor.includes(item.id));
}
if (filter.filterUser !== undefined) {
result = result.filter(item => filter.filterUser === item.owner);
}
if (!filter.folderMode && filter.path) {
result = result.filter(item => matchLibraryItemLocation(item, filter.path!));
}

View File

@ -6,6 +6,7 @@ import { Node } from 'reactflow';
import { LibraryItemType, LocationHead } from './library';
import { IOperation } from './oss';
import { UserID } from './user';
/**
* Represents graph dependency mode.
@ -182,6 +183,7 @@ export interface ILibraryFilter {
isVisible?: boolean;
isOwned?: boolean;
isEditor?: boolean;
filterUser?: UserID;
}
/**

View File

@ -16,6 +16,7 @@ import DlgChangeLocation from '@/dialogs/DlgChangeLocation';
import useLocalStorage from '@/hooks/useLocalStorage';
import { ILibraryItem, IRenameLocationData, LocationHead } from '@/models/library';
import { ILibraryFilter } from '@/models/miscellaneous';
import { UserID } from '@/models/user';
import { storage } from '@/utils/constants';
import { information } from '@/utils/labels';
import { convertToCSV, toggleTristateFlag } from '@/utils/utils';
@ -36,8 +37,9 @@ function LibraryPage() {
const [head, setHead] = useLocalStorage<LocationHead | undefined>(storage.librarySearchHead, undefined);
const [subfolders, setSubfolders] = useLocalStorage<boolean>(storage.librarySearchSubfolders, false);
const [isVisible, setIsVisible] = useLocalStorage<boolean | undefined>(storage.librarySearchVisible, true);
const [isOwned, setIsOwned] = useLocalStorage<boolean | undefined>(storage.librarySearchEditor, undefined);
const [isOwned, setIsOwned] = useLocalStorage<boolean | undefined>(storage.librarySearchOwned, undefined);
const [isEditor, setIsEditor] = useLocalStorage<boolean | undefined>(storage.librarySearchEditor, undefined);
const [filterUser, setFilterUser] = useLocalStorage<UserID | undefined>(storage.librarySearchUser, undefined);
const [showRenameLocation, setShowRenameLocation] = useState(false);
const filter: ILibraryFilter = useMemo(
@ -50,9 +52,22 @@ function LibraryPage() {
isVisible: user ? isVisible : true,
folderMode: options.folderMode,
subfolders: subfolders,
location: options.location
location: options.location,
filterUser: filterUser
}),
[head, path, query, isEditor, isOwned, isVisible, user, options.folderMode, options.location, subfolders]
[
head,
path,
query,
isEditor,
isOwned,
isVisible,
user,
options.folderMode,
options.location,
subfolders,
filterUser
]
);
const hasCustomFilter = useMemo(
@ -63,6 +78,7 @@ function LibraryPage() {
filter.isEditor !== undefined ||
filter.isOwned !== undefined ||
filter.isVisible !== true ||
filter.filterUser !== undefined ||
!!filter.location,
[filter]
);
@ -84,8 +100,9 @@ function LibraryPage() {
setIsVisible(true);
setIsOwned(undefined);
setIsEditor(undefined);
setFilterUser(undefined);
options.setLocation('');
}, [setHead, setIsVisible, setIsOwned, setIsEditor, options.setLocation]);
}, [setHead, setIsVisible, setIsOwned, setIsEditor, setFilterUser, options.setLocation]);
const promptRenameLocation = useCallback(() => {
setShowRenameLocation(true);
@ -186,6 +203,8 @@ function LibraryPage() {
toggleVisible={toggleVisible}
isEditor={isEditor}
toggleEditor={toggleEditor}
filterUser={filterUser}
setFilterUser={setFilterUser}
resetFilter={resetFilter}
folderMode={options.folderMode}
toggleFolderMode={toggleFolderMode}

View File

@ -1,19 +1,31 @@
'use client';
import clsx from 'clsx';
import { useCallback } from 'react';
import { motion } from 'framer-motion';
import { useCallback, useMemo } from 'react';
import { LocationIcon, VisibilityIcon } from '@/components/DomainIcons';
import { IconEditor, IconFilterReset, IconFolder, IconFolderTree, IconOwner } from '@/components/Icons';
import {
IconEditor,
IconFilterReset,
IconFolder,
IconFolderSearch,
IconFolderTree,
IconOwner,
IconUserSearch
} from '@/components/Icons';
import { CProps } from '@/components/props';
import SelectUser from '@/components/select/SelectUser';
import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton';
import MiniButton from '@/components/ui/MiniButton';
import SearchBar from '@/components/ui/SearchBar';
import SelectorButton from '@/components/ui/SelectorButton';
import { useAuth } from '@/context/AuthContext';
import { useUsers } from '@/context/UsersContext';
import useDropdown from '@/hooks/useDropdown';
import { LocationHead } from '@/models/library';
import { UserID } from '@/models/user';
import { animateDropdownItem } from '@/styling/animations';
import { prefixes } from '@/utils/constants';
import { describeLocationHead, labelLocationHead } from '@/utils/labels';
import { tripleToggleColor } from '@/utils/utils';
@ -39,6 +51,9 @@ interface ToolbarSearchProps {
toggleOwned: () => void;
isEditor: boolean | undefined;
toggleEditor: () => void;
filterUser: UserID | undefined;
setFilterUser: React.Dispatch<React.SetStateAction<UserID | undefined>>;
resetFilter: () => void;
}
@ -63,10 +78,19 @@ function ToolbarSearch({
toggleOwned,
isEditor,
toggleEditor,
filterUser,
setFilterUser,
resetFilter
}: ToolbarSearchProps) {
const { user } = useAuth();
const headMenu = useDropdown();
const userMenu = useDropdown();
const { users } = useUsers();
const userActive = useMemo(
() => isOwned !== undefined || isEditor !== undefined || filterUser !== undefined,
[isOwned, isEditor, filterUser]
);
const handleChange = useCallback(
(newValue: LocationHead | undefined) => {
@ -106,7 +130,7 @@ function ToolbarSearch({
<div
className={clsx(
'ml-3 pt-1 self-center',
'min-w-[4.5rem] sm:min-w-[5.5rem]',
'min-w-[4.5rem] sm:min-w-[7.4rem]',
'select-none',
'whitespace-nowrap'
)}
@ -114,41 +138,58 @@ function ToolbarSearch({
{filtered} из {total}
</div>
{user ? (
<div className='cc-icons'>
<MiniButton
title='Видимость'
icon={<VisibilityIcon value={true} className={tripleToggleColor(isVisible)} />}
onClick={toggleVisible}
/>
<div className='cc-icons'>
<MiniButton
title='Видимость'
icon={<VisibilityIcon value={true} className={tripleToggleColor(isVisible)} />}
onClick={toggleVisible}
/>
<div ref={userMenu.ref} className='flex'>
<MiniButton
title='Я - Владелец'
icon={<IconOwner size='1.25rem' className={tripleToggleColor(isOwned)} />}
onClick={toggleOwned}
/>
<MiniButton
title='Я - Редактор'
icon={<IconEditor size='1.25rem' className={tripleToggleColor(isEditor)} />}
onClick={toggleEditor}
/>
<MiniButton
title='Сбросить фильтры'
icon={<IconFilterReset size='1.25rem' className='icon-primary' />}
onClick={resetFilter}
disabled={!hasCustomFilter}
title='Поиск пользователя'
hideTitle={userMenu.isOpen}
icon={<IconUserSearch size='1.25rem' className={userActive ? 'icon-green' : 'icon-primary'} />}
onClick={userMenu.toggle}
/>
<Dropdown isOpen={userMenu.isOpen}>
<DropdownButton
text='Я - Владелец'
icon={<IconOwner size='1.25rem' className={tripleToggleColor(isOwned)} />}
onClick={toggleOwned}
/>
<DropdownButton
text='Я - Редактор'
icon={<IconEditor size='1.25rem' className={tripleToggleColor(isEditor)} />}
onClick={toggleEditor}
/>
<motion.div className='px-1 pb-1' variants={animateDropdownItem}>
<SelectUser
noBorder
placeholder='Выберите владельца'
className='min-w-[15rem] text-sm'
items={users}
value={filterUser}
onSelectValue={setFilterUser}
/>
</motion.div>
</Dropdown>
</div>
) : null}
<div className='flex h-full'>
<MiniButton
title='Сбросить фильтры'
icon={<IconFilterReset size='1.25rem' className='icon-primary' />}
onClick={resetFilter}
disabled={!hasCustomFilter}
/>
</div>
<div className='flex h-full flex-grow pr-4'>
<SearchBar
id='library_search'
placeholder='Поиск'
noBorder
className='min-w-[7rem] sm:min-w-[10rem]'
className={clsx('min-w-[7rem] sm:min-w-[10rem] max-w-[20rem]', folderMode && 'flex-grow')}
value={query}
onChange={setQuery}
/>
@ -163,7 +204,7 @@ function ToolbarSearch({
head ? (
<LocationIcon value={head} size='1.25rem' />
) : (
<IconFolder size='1.25rem' className='clr-text-controls' />
<IconFolderSearch size='1.25rem' className='clr-text-controls' />
)
}
onClick={handleFolderClick}
@ -171,7 +212,7 @@ function ToolbarSearch({
/>
<Dropdown isOpen={headMenu.isOpen} stretchLeft className='z-modalTooltip'>
<DropdownButton className='w-[10rem]' title='Переключение в режим Проводник' onClick={handleToggleFolder}>
<DropdownButton title='Переключение в режим Проводник' onClick={handleToggleFolder}>
<div className='inline-flex items-center gap-3'>
<IconFolderTree size='1rem' className='clr-text-controls' />
<span>проводник...</span>
@ -207,7 +248,7 @@ function ToolbarSearch({
placeholder='Путь'
noIcon
noBorder
className='min-w-[4.5rem] sm:min-w-[5rem]'
className='w-[4.5rem] sm:w-[5rem] flex-grow'
value={path}
onChange={setPath}
/>

View File

@ -89,13 +89,18 @@ function ViewSideLocation({
place='right-start'
/>
<div className='cc-icons'>
{canRename ? (
<MiniButton
icon={<IconFolderEdit size='1.25rem' className='icon-primary' />}
titleHtml='<b>Редактирование пути</b><br/>Перемещаются только Ваши схемы<br/>в указанной папке (и подпапках)'
onClick={onRenameLocation}
/>
) : null}
<MiniButton
icon={<IconFolderEdit size='1.25rem' className='icon-primary' />}
titleHtml='<b>Редактирование пути</b><br/>Перемещаются только Ваши схемы<br/>в указанной папке (и подпапках)'
onClick={onRenameLocation}
disabled={!canRename}
title='Вложенные папки' // prettier: split-lines
icon={<SubfoldersIcon value={subfolders} />}
onClick={toggleSubfolders}
/>
<MiniButton title='Вложенные папки' icon={<SubfoldersIcon value={subfolders} />} onClick={toggleSubfolders} />
<MiniButton
icon={<IconFolderTree size='1.25rem' className='icon-green' />}
title='Переключение в режим Поиск'

View File

@ -5,6 +5,7 @@ import {
IconFolderEdit,
IconFolderEmpty,
IconFolderOpened,
IconFolderSearch,
IconFolderTree,
IconOSS,
IconRSForm,
@ -12,7 +13,8 @@ import {
IconShow,
IconSortAsc,
IconSortDesc,
IconSubfolders
IconSubfolders,
IconUserSearch
} from '@/components/Icons';
import LinkTopic from '@/components/ui/LinkTopic';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
@ -43,11 +45,14 @@ function HelpLibrary() {
<IconSortAsc size='1rem' className='inline-icon' />
<IconSortDesc size='1rem' className='inline-icon' /> сортировка по клику на заголовок таблицы
</li>
<li>
<IconUserSearch size='1rem' className='inline-icon' /> фильтр по пользователю
</li>
<li>
<IconSearch size='1rem' className='inline-icon' /> фильтр по названию и шифру
</li>
<li>
<IconFolder size='1rem' className='inline-icon' /> фильтр по расположению
<IconFolderSearch size='1rem' className='inline-icon' /> фильтр по расположению
</li>
<li>
<IconFilterReset size='1rem' className='inline-icon' /> сбросить фильтры

View File

@ -117,6 +117,7 @@ export const storage = {
librarySearchVisible: 'library.search.visible',
librarySearchOwned: 'library.search.owned',
librarySearchEditor: 'library.search.editor',
librarySearchUser: 'library.search.user',
libraryPagination: 'library.pagination',
rsgraphFilter: 'rsgraph.filter2',

View File

@ -362,11 +362,11 @@ export function describeExpressionStatus(status: ExpressionStatus): string {
export function labelHelpTopic(topic: HelpTopic): string {
// prettier-ignore
switch (topic) {
case HelpTopic.MAIN: return 'Портал';
case HelpTopic.MAIN: return '🏠 Портал';
case HelpTopic.THESAURUS: return 'Тезаурус';
case HelpTopic.THESAURUS: return '📖 Тезаурус';
case HelpTopic.INTERFACE: return 'Интерфейс';
case HelpTopic.INTERFACE: return '🦄 Интерфейс';
case HelpTopic.UI_LIBRARY: return 'Библиотека';
case HelpTopic.UI_RS_MENU: return 'Меню схемы';
case HelpTopic.UI_RS_CARD: return 'Карточка схемы';
@ -378,7 +378,7 @@ export function labelHelpTopic(topic: HelpTopic): string {
case HelpTopic.UI_CST_CLASS: return 'Класс конституенты';
case HelpTopic.UI_OSS_GRAPH: return 'Граф синтеза';
case HelpTopic.CONCEPTUAL: return 'Концептуализация';
case HelpTopic.CONCEPTUAL: return '♨️ Концептуализация';
case HelpTopic.CC_SYSTEM: return 'Система определений';
case HelpTopic.CC_CONSTITUENTA: return 'Конституента';
case HelpTopic.CC_RELATIONS: return 'Связи понятий';
@ -386,24 +386,24 @@ export function labelHelpTopic(topic: HelpTopic): string {
case HelpTopic.CC_OSS: return 'Операционная схема';
case HelpTopic.CC_PROPAGATION: return 'Сквозные изменения';
case HelpTopic.RSLANG: return 'Экспликация';
case HelpTopic.RSLANG: return '🚀 Экспликация';
case HelpTopic.RSL_TYPES: return 'Типизация';
case HelpTopic.RSL_CORRECT: return 'Переносимость';
case HelpTopic.RSL_INTERPRET: return 'Интерпретируемость';
case HelpTopic.RSL_OPERATIONS: return 'Операции';
case HelpTopic.RSL_TEMPLATES: return 'Банк выражений';
case HelpTopic.TERM_CONTROL: return 'Терминологизация';
case HelpTopic.ACCESS: return 'Доступы';
case HelpTopic.VERSIONS: return 'Версионирование';
case HelpTopic.TERM_CONTROL: return '🪸 Терминологизация';
case HelpTopic.ACCESS: return '👀 Доступы';
case HelpTopic.VERSIONS: return '🏺 Версионирование';
case HelpTopic.INFO: return 'Информация';
case HelpTopic.INFO: return '📰 Информация';
case HelpTopic.INFO_RULES: return 'Правила';
case HelpTopic.INFO_CONTRIB: return 'Разработчики';
case HelpTopic.INFO_PRIVACY: return 'Обработка данных';
case HelpTopic.INFO_API: return 'REST API';
case HelpTopic.EXTEOR: return 'Экстеор';
case HelpTopic.EXTEOR: return '🖥️ Экстеор';
}
}