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) { if (value) {
return <IconSubfolders size={size} className={className ?? 'clr-text-green'} />; return <IconSubfolders size={size} className={className ?? 'clr-text-green'} />;
} else { } 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 { LuSun as IconLightTheme } from 'react-icons/lu';
export { LuFolderTree as IconFolderTree } from 'react-icons/lu'; export { LuFolderTree as IconFolderTree } from 'react-icons/lu';
export { LuFolder as IconFolder } 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 { LuFolders as IconSubfolders } from 'react-icons/lu';
export { LuFolderEdit as IconFolderEdit } from 'react-icons/lu'; export { LuFolderEdit as IconFolderEdit } from 'react-icons/lu';
export { LuFolderOpen as IconFolderOpened } 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 { LuUserCircle2 as IconUser } from 'react-icons/lu';
export { FaCircleUser as IconUser2 } from 'react-icons/fa6'; export { FaCircleUser as IconUser2 } from 'react-icons/fa6';
export { TbUserEdit as IconEditor } from 'react-icons/tb'; 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 { LuCrown as IconOwner } from 'react-icons/lu';
export { TbMeteor as IconAdmin } from 'react-icons/tb'; export { TbMeteor as IconAdmin } from 'react-icons/tb';
export { TbMeteorOff as IconAdminOff } 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) { if (filter.isEditor !== undefined) {
result = result.filter(item => filter.isEditor == user?.editor.includes(item.id)); 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) { if (!filter.folderMode && filter.path) {
result = result.filter(item => matchLibraryItemLocation(item, 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 { LibraryItemType, LocationHead } from './library';
import { IOperation } from './oss'; import { IOperation } from './oss';
import { UserID } from './user';
/** /**
* Represents graph dependency mode. * Represents graph dependency mode.
@ -182,6 +183,7 @@ export interface ILibraryFilter {
isVisible?: boolean; isVisible?: boolean;
isOwned?: boolean; isOwned?: boolean;
isEditor?: boolean; isEditor?: boolean;
filterUser?: UserID;
} }
/** /**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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