F: Improve folders UI

This commit is contained in:
Ivan 2025-07-03 11:39:51 +03:00
parent cd62ad574f
commit 161a51fc45
5 changed files with 30 additions and 144 deletions

View File

@ -5,8 +5,7 @@ import {
IconFolderEdit, IconFolderEdit,
IconFolderEmpty, IconFolderEmpty,
IconFolderOpened, IconFolderOpened,
IconFolderSearch, IconLeftClose,
IconFolderTree,
IconOSS, IconOSS,
IconRSForm, IconRSForm,
IconSearch, IconSearch,
@ -45,28 +44,25 @@ export function HelpLibrary() {
<li> <li>
<IconShow size='1rem' className='inline-icon' /> фильтры атрибутов применяются по клику <IconShow size='1rem' className='inline-icon' /> фильтры атрибутов применяются по клику
</li> </li>
<li>
<IconSortAsc size='1rem' className='inline-icon' />
<IconSortDesc size='1rem' className='inline-icon' /> сортировка по клику на заголовок таблицы
</li>
<li> <li>
<IconUserSearch size='1rem' className='inline-icon' /> фильтр по пользователю <IconUserSearch size='1rem' className='inline-icon' /> фильтр по пользователю
</li> </li>
<li> <li>
<IconSearch size='1rem' className='inline-icon' /> фильтр по названию и шифру <IconSearch size='1rem' className='inline-icon' /> фильтр по названию и шифру
</li> </li>
<li>
<IconFolderSearch size='1rem' className='inline-icon' /> фильтр по расположению
</li>
<li> <li>
<IconFilterReset size='1rem' className='inline-icon' /> сбросить фильтры <IconFilterReset size='1rem' className='inline-icon' /> сбросить фильтры
</li> </li>
<li> <li>
<IconFolderTree size='1rem' className='inline-icon' /> переключение между Проводник и Таблица <IconLeftClose size='1rem' className='inline-icon' /> отображение Проводника
</li>
<li>
<IconSortAsc size='1rem' className='inline-icon' />
<IconSortDesc size='1rem' className='inline-icon' /> сортировка по клику на заголовок таблицы
</li> </li>
</ul> </ul>
<h2>Режим: Проводник</h2> <h2>Проводник</h2>
<ul> <ul>
<li> <li>
<IconFolderEdit size='1rem' className='inline-icon' /> переименовать выбранную <IconFolderEdit size='1rem' className='inline-icon' /> переименовать выбранную

View File

@ -6,7 +6,7 @@ export function IconShowSidebar({
size = '1.25rem', size = '1.25rem',
className, className,
isBottom isBottom
}: DomIconProps<boolean> & { isBottom: boolean }) { }: DomIconProps<boolean> & { isBottom?: boolean }) {
if (isBottom) { if (isBottom) {
if (value) { if (value) {
return <IconBottomClose size={size} className={className ?? 'icon-primary'} />; return <IconBottomClose size={size} className={className ?? 'icon-primary'} />;

View File

@ -4,27 +4,16 @@ import clsx from 'clsx';
import { SelectUser } from '@/features/users/components/select-user'; import { SelectUser } from '@/features/users/components/select-user';
import { MiniButton, SelectorButton } from '@/components/control'; import { MiniButton } from '@/components/control';
import { Dropdown, DropdownButton, useDropdown } from '@/components/dropdown'; import { Dropdown, DropdownButton, useDropdown } from '@/components/dropdown';
import { import { IconEditor, IconFilterReset, IconOwner, IconUserSearch } from '@/components/icons';
IconEditor,
IconFilterReset,
IconFolder,
IconFolderSearch,
IconFolderTree,
IconOwner,
IconUserSearch
} from '@/components/icons';
import { SearchBar } from '@/components/input'; import { SearchBar } from '@/components/input';
import { cn } from '@/components/utils'; import { cn } from '@/components/utils';
import { prefixes } from '@/utils/constants';
import { tripleToggleColor } from '@/utils/utils'; import { tripleToggleColor } from '@/utils/utils';
import { useLibrarySuspense } from '../../backend/use-library'; import { useLibrarySuspense } from '../../backend/use-library';
import { IconItemVisibility } from '../../components/icon-item-visibility'; import { IconItemVisibility } from '../../components/icon-item-visibility';
import { IconLocationHead } from '../../components/icon-location-head'; import { IconShowSidebar } from '../../components/icon-show-sidebar';
import { describeLocationHead, labelLocationHead } from '../../labels';
import { LocationHead } from '../../models/library';
import { useHasCustomFilter, useLibrarySearchStore } from '../../stores/library-search'; import { useHasCustomFilter, useLibrarySearchStore } from '../../stores/library-search';
interface ToolbarSearchProps { interface ToolbarSearchProps {
@ -36,14 +25,9 @@ interface ToolbarSearchProps {
export function ToolbarSearch({ className, total, filtered }: ToolbarSearchProps) { export function ToolbarSearch({ className, total, filtered }: ToolbarSearchProps) {
const { items } = useLibrarySuspense(); const { items } = useLibrarySuspense();
const userMenu = useDropdown(); const userMenu = useDropdown();
const headMenu = useDropdown();
const query = useLibrarySearchStore(state => state.query); const query = useLibrarySearchStore(state => state.query);
const setQuery = useLibrarySearchStore(state => state.setQuery); const setQuery = useLibrarySearchStore(state => state.setQuery);
const path = useLibrarySearchStore(state => state.path);
const setPath = useLibrarySearchStore(state => state.setPath);
const head = useLibrarySearchStore(state => state.head);
const setHead = useLibrarySearchStore(state => state.setHead);
const folderMode = useLibrarySearchStore(state => state.folderMode); const folderMode = useLibrarySearchStore(state => state.folderMode);
const toggleFolderMode = useLibrarySearchStore(state => state.toggleFolderMode); const toggleFolderMode = useLibrarySearchStore(state => state.toggleFolderMode);
const isOwned = useLibrarySearchStore(state => state.isOwned); const isOwned = useLibrarySearchStore(state => state.isOwned);
@ -64,24 +48,6 @@ export function ToolbarSearch({ className, total, filtered }: ToolbarSearchProps
return items.some(item => item.owner === userID); return items.some(item => item.owner === userID);
} }
function handleChange(newValue: LocationHead | null) {
headMenu.hide();
setHead(newValue);
}
function handleToggleFolder() {
headMenu.hide();
toggleFolderMode();
}
function handleFolderClick(event: React.MouseEvent<Element>) {
if (event.ctrlKey || event.metaKey) {
toggleFolderMode();
} else {
headMenu.toggle();
}
}
return ( return (
<div className={cn('flex gap-3 border-b text-sm bg-input items-center', className)}> <div className={cn('flex gap-3 border-b text-sm bg-input items-center', className)}>
<div className='ml-3 min-w-18 sm:min-w-30 select-none whitespace-nowrap'> <div className='ml-3 min-w-18 sm:min-w-30 select-none whitespace-nowrap'>
@ -89,6 +55,11 @@ export function ToolbarSearch({ className, total, filtered }: ToolbarSearchProps
</div> </div>
<div className='cc-icons h-full items-center'> <div className='cc-icons h-full items-center'>
<MiniButton
title='Отображение проводника'
icon={<IconShowSidebar value={!folderMode} size='1.25rem' />}
onClick={toggleFolderMode}
/>
<MiniButton <MiniButton
title='Видимость' title='Видимость'
icon={<IconItemVisibility value={true} className={tripleToggleColor(isVisible)} />} icon={<IconItemVisibility value={true} className={tripleToggleColor(isVisible)} />}
@ -119,7 +90,7 @@ export function ToolbarSearch({ className, total, filtered }: ToolbarSearchProps
aria-label='Выбор пользователя для фильтра по владельцу' aria-label='Выбор пользователя для фильтра по владельцу'
placeholder='Выберите владельца' placeholder='Выберите владельца'
noBorder noBorder
className='min-w-60 mx-1 mb-1' className='min-w-60 mx-1 mb-1 cc-hover-bg'
filter={filterNonEmptyUsers} filter={filterNonEmptyUsers}
value={filterUser} value={filterUser}
onChange={setFilterUser} onChange={setFilterUser}
@ -144,60 +115,6 @@ export function ToolbarSearch({ className, total, filtered }: ToolbarSearchProps
query={query} query={query}
onChangeQuery={setQuery} onChangeQuery={setQuery}
/> />
{!folderMode ? (
<div
ref={headMenu.ref}
onBlur={headMenu.handleBlur}
className='relative flex items-center h-full select-none'
>
<SelectorButton
className='rounded-lg py-1'
titleHtml={
(head ? describeLocationHead(head) : 'Выберите каталог') + '<br/><kbd>Ctrl + клик</kbd> - Проводник'
}
hideTitle={headMenu.isOpen}
icon={head ? <IconLocationHead value={head} size='1.25rem' /> : <IconFolderSearch size='1.25rem' />}
onClick={handleFolderClick}
/>
<Dropdown isOpen={headMenu.isOpen} stretchLeft>
<DropdownButton
text='проводник...'
title='Переключение в режим Проводник'
icon={<IconFolderTree size='1rem' className='icon-primary' />}
onClick={handleToggleFolder}
/>
<DropdownButton
text='отображать все'
title='Очистить фильтр по расположению'
icon={<IconFolder size='1rem' className='icon-primary' />}
onClick={() => handleChange(null)}
/>
{Object.values(LocationHead).map((head, index) => {
return (
<DropdownButton
key={`${prefixes.location_head_list}${index}`}
text={labelLocationHead(head)}
title={describeLocationHead(head)}
onClick={() => handleChange(head)}
icon={<IconLocationHead value={head} size='1rem' />}
/>
);
})}
</Dropdown>
</div>
) : null}
{!folderMode ? (
<SearchBar
id='path_search'
placeholder='Путь'
noIcon
noBorder
className='w-18 sm:w-20 grow ml-1'
query={path}
onChangeQuery={setPath}
/>
) : null}
</div> </div>
</div> </div>
); );

View File

@ -2,9 +2,7 @@ import { useIntl } from 'react-intl';
import { useLabelUser } from '@/features/users'; import { useLabelUser } from '@/features/users';
import { MiniButton } from '@/components/control';
import { createColumnHelper } from '@/components/data-table'; import { createColumnHelper } from '@/components/data-table';
import { IconFolderTree } from '@/components/icons';
import { useWindowSize } from '@/hooks/use-window-size'; import { useWindowSize } from '@/hooks/use-window-size';
import { type RO } from '@/utils/meta'; import { type RO } from '@/utils/meta';
@ -20,13 +18,6 @@ export function useLibraryColumns() {
const getUserLabel = useLabelUser(); const getUserLabel = useLabelUser();
const folderMode = useLibrarySearchStore(state => state.folderMode); const folderMode = useLibrarySearchStore(state => state.folderMode);
const toggleFolderMode = useLibrarySearchStore(state => state.toggleFolderMode);
function handleToggleFolder(event: React.MouseEvent<Element>) {
event.preventDefault();
event.stopPropagation();
toggleFolderMode();
}
return [ return [
...(folderMode ...(folderMode
@ -34,16 +25,7 @@ export function useLibraryColumns() {
: [ : [
columnHelper.accessor('location', { columnHelper.accessor('location', {
id: 'location', id: 'location',
header: () => ( header: 'Путь',
<MiniButton
titleHtml='Переключение в режим Проводник'
aria-label='Переключатель режима Проводник'
noPadding
className='ml-2 max-h-4 -translate-y-0.5'
onClick={handleToggleFolder}
icon={<IconFolderTree size='1.25rem' className='text-primary' />}
/>
),
size: 50, size: 50,
minSize: 50, minSize: 50,
maxSize: 50, maxSize: 50,

View File

@ -6,7 +6,7 @@ import { HelpTopic } from '@/features/help';
import { BadgeHelp } from '@/features/help/components/badge-help'; import { BadgeHelp } from '@/features/help/components/badge-help';
import { MiniButton } from '@/components/control'; import { MiniButton } from '@/components/control';
import { IconFolderEdit, IconFolderTree } from '@/components/icons'; import { IconFolderEdit } from '@/components/icons';
import { useFitHeight } from '@/stores/app-layout'; import { useFitHeight } from '@/stores/app-layout';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
import { infoMsg } from '@/utils/labels'; import { infoMsg } from '@/utils/labels';
@ -28,7 +28,6 @@ export function ViewSideLocation({ isVisible, onRenameLocation }: ViewSideLocati
const location = useLibrarySearchStore(state => state.location); const location = useLibrarySearchStore(state => state.location);
const setLocation = useLibrarySearchStore(state => state.setLocation); const setLocation = useLibrarySearchStore(state => state.setLocation);
const toggleFolderMode = useLibrarySearchStore(state => state.toggleFolderMode);
const subfolders = useLibrarySearchStore(state => state.subfolders); const subfolders = useLibrarySearchStore(state => state.subfolders);
const toggleSubfolders = useLibrarySearchStore(state => state.toggleSubfolders); const toggleSubfolders = useLibrarySearchStore(state => state.toggleSubfolders);
@ -69,27 +68,19 @@ export function ViewSideLocation({ isVisible, onRenameLocation }: ViewSideLocati
<div className='h-8 flex justify-between items-center pr-1 pl-0.5'> <div className='h-8 flex justify-between items-center pr-1 pl-0.5'>
<BadgeHelp topic={HelpTopic.UI_LIBRARY} contentClass='text-sm' offset={5} place='right-start' /> <BadgeHelp topic={HelpTopic.UI_LIBRARY} contentClass='text-sm' offset={5} place='right-start' />
<div className='cc-icons'> <div className='cc-icons'>
{canRename ? (
<MiniButton <MiniButton
titleHtml='<b>Редактирование пути</b><br/>Перемещаются только Ваши схемы<br/>в указанной папке (и подпапках)' titleHtml='<b>Редактирование пути</b><br/>Перемещаются только Ваши схемы<br/>в указанной папке (и подпапках)'
aria-label='Редактирование расположения' aria-label='Редактирование расположения'
icon={<IconFolderEdit size='1.25rem' className='icon-primary' />} icon={<IconFolderEdit size='1.25rem' className='icon-primary' />}
onClick={onRenameLocation} onClick={onRenameLocation}
disabled={!canRename}
/> />
) : null}
{!!location ? (
<MiniButton <MiniButton
title={subfolders ? 'Вложенные папки: Вкл' : 'Вложенные папки: Выкл'} title={subfolders ? 'Вложенные папки: Вкл' : 'Вложенные папки: Выкл'}
aria-label='Переключатель отображения вложенных папок' aria-label='Переключатель отображения вложенных папок'
icon={<IconShowSubfolders value={subfolders} />} icon={<IconShowSubfolders value={subfolders} />}
onClick={toggleSubfolders} onClick={toggleSubfolders}
/> />
) : null}
<MiniButton
title='Переключение в режим Таблица'
icon={<IconFolderTree size='1.25rem' className='text-primary' />}
onClick={toggleFolderMode}
/>
</div> </div>
</div> </div>
<SelectLocation <SelectLocation