Implement location selector button

This commit is contained in:
IRBorisov 2024-06-26 19:00:29 +03:00
parent a37fdf33e2
commit edb3bb4f10
7 changed files with 196 additions and 130 deletions

View File

@ -1,40 +1,109 @@
'use client';
import { useCallback } from 'react';
import clsx from 'clsx';
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import useDropdown from '@/hooks/useDropdown';
import { FolderTree } from '@/models/FolderTree';
import { FolderNode, FolderTree } from '@/models/FolderTree';
import { labelFolderNode } from '@/utils/labels';
import { IconFolderTree } from '../Icons';
import { IconFolder, IconFolderClosed, IconFolderEmpty, IconFolderOpened } from '../Icons';
import { CProps } from '../props';
import MiniButton from '../ui/MiniButton';
interface SelectLocationProps {
interface SelectLocationProps extends CProps.Styling {
value: string;
onChange: (newValue: string) => void;
folderTree: FolderTree;
prefix: string;
dense?: boolean;
onClick: (event: CProps.EventMouse, target: FolderNode) => void;
}
function SelectLocation({ value, onChange, folderTree }: SelectLocationProps) {
const menu = useDropdown();
function SelectLocation({ value, folderTree, dense, prefix, onClick, className, style }: SelectLocationProps) {
const activeNode = useMemo(() => folderTree.at(value), [folderTree, value]);
const handleChange = useCallback(
(newValue: string) => {
console.log(folderTree.roots.size);
console.log(value);
menu.hide();
onChange(newValue);
const items = useMemo(() => folderTree.getTree(), [folderTree]);
const [folded, setFolded] = useState<FolderNode[]>(items);
useLayoutEffect(() => {
setFolded(items.filter(item => item !== activeNode && (!activeNode || !activeNode.hasPredecessor(item))));
}, [items, activeNode]);
const onFoldItem = useCallback(
(target: FolderNode, showChildren: boolean) => {
setFolded(prev =>
items.filter(item => {
if (item === target) {
return !showChildren;
}
if (!showChildren && item.hasPredecessor(target)) {
return true;
} else {
return prev.includes(item);
}
})
);
},
[menu, onChange, value, folderTree]
[items]
);
const handleClickFold = useCallback(
(event: CProps.EventMouse, target: FolderNode, showChildren: boolean) => {
event.preventDefault();
event.stopPropagation();
onFoldItem(target, showChildren);
},
[onFoldItem]
);
return (
<div ref={menu.ref} className='h-full text-right'>
<div className={clsx('flex flex-col', 'cc-scroll-y', className)} style={style}>
{items.map((item, index) =>
!item.parent || !folded.includes(item.parent) ? (
<div
tabIndex={-1}
key={`${prefix}${index}`}
className={clsx(
!dense && 'min-h-[2.0825rem] sm:min-h-[2.3125rem]',
'pr-3 py-1 flex items-center gap-2',
'cc-scroll-row',
'clr-hover',
'cursor-pointer',
'leading-3 sm:leading-4',
activeNode === item && 'clr-selected'
)}
style={{ paddingLeft: `${(item.rank > 5 ? 5 : item.rank) * 0.5 + 0.5}rem` }}
onClick={event => onClick(event, item)}
>
{item.children.size > 0 ? (
<MiniButton
title='Проводник...'
icon={<IconFolderTree size='1.25rem' className='icon-green' />}
onClick={() => handleChange('/U/test')}
noPadding
noHover
icon={
folded.includes(item) ? (
item.filesInside ? (
<IconFolderClosed size='1rem' className='icon-primary' />
) : (
<IconFolderEmpty size='1rem' className='icon-primary' />
)
) : (
<IconFolderOpened size='1rem' className='icon-green' />
)
}
onClick={event => handleClickFold(event, item, folded.includes(item))}
/>
) : (
<div>
{item.filesInside ? (
<IconFolder size='1rem' className='clr-text-default' />
) : (
<IconFolderEmpty size='1rem' className='clr-text-controls' />
)}
</div>
)}
<div className='self-center text-start'>{labelFolderNode(item)}</div>
</div>
) : null
)}
</div>
);
}

View File

@ -0,0 +1,62 @@
'use client';
import clsx from 'clsx';
import { useCallback } from 'react';
import useDropdown from '@/hooks/useDropdown';
import { FolderTree } from '@/models/FolderTree';
import { prefixes } from '@/utils/constants';
import { IconFolderTree } from '../Icons';
import { CProps } from '../props';
import Dropdown from '../ui/Dropdown';
import MiniButton from '../ui/MiniButton';
import SelectLocation from './SelectLocation';
interface SelectLocationContextProps extends CProps.Styling {
value: string;
folderTree: FolderTree;
stretchTop?: boolean;
onChange: (newValue: string) => void;
}
function SelectLocationContext({ value, folderTree, onChange, className, style }: SelectLocationContextProps) {
const menu = useDropdown();
const handleClick = useCallback(
(event: CProps.EventMouse, newValue: string) => {
event.preventDefault();
event.stopPropagation();
menu.hide();
onChange(newValue);
},
[menu, onChange]
);
return (
<div ref={menu.ref} className='h-full text-right self-start mt-[-0.25rem] ml-[-1.5rem]'>
<MiniButton
title='Проводник...'
hideTitle={menu.isOpen}
icon={<IconFolderTree size='1.25rem' className='icon-green' />}
onClick={() => menu.toggle()}
/>
<Dropdown
isOpen={menu.isOpen}
className={clsx('w-[20rem] h-[12.5rem] z-modalTooltip mt-0', className)}
style={style}
>
<SelectLocation
folderTree={folderTree}
value={value}
prefix={prefixes.folders_list}
dense
onClick={(event, target) => handleClick(event, target.getPath())}
/>
</Dropdown>
</div>
);
}
export default SelectLocationContext;

View File

@ -7,11 +7,12 @@ import { CProps } from '../props';
interface DropdownProps extends CProps.Styling {
stretchLeft?: boolean;
stretchTop?: boolean;
isOpen: boolean;
children: React.ReactNode;
}
function Dropdown({ isOpen, stretchLeft, className, children, ...restProps }: DropdownProps) {
function Dropdown({ isOpen, stretchLeft, stretchTop, className, children, ...restProps }: DropdownProps) {
return (
<div className='relative'>
<motion.div
@ -25,7 +26,8 @@ function Dropdown({ isOpen, stretchLeft, className, children, ...restProps }: Dr
'clr-input',
{
'right-0': stretchLeft,
'left-0': !stretchLeft
'left-0': !stretchLeft,
'bottom-[2rem]': stretchTop
},
className
)}

View File

@ -1,13 +1,15 @@
'use client';
import clsx from 'clsx';
import { useMemo, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import SelectLocationContext from '@/components/select/SelectLocationContext';
import SelectLocationHead from '@/components/select/SelectLocationHead';
import Label from '@/components/ui/Label';
import Modal, { ModalProps } from '@/components/ui/Modal';
import TextArea from '@/components/ui/TextArea';
import { useAuth } from '@/context/AuthContext';
import { useLibrary } from '@/context/LibraryContext';
import { LocationHead } from '@/models/library';
import { combineLocation, validateLocation } from '@/models/libraryAPI';
import { limits } from '@/utils/constants';
@ -22,9 +24,16 @@ function DlgChangeLocation({ hideWindow, initial, onChangeLocation }: DlgChangeL
const [head, setHead] = useState<LocationHead>(initial.substring(0, 2) as LocationHead);
const [body, setBody] = useState<string>(initial.substring(3));
const { folders } = useLibrary();
const location = useMemo(() => combineLocation(head, body), [head, body]);
const isValid = useMemo(() => initial !== location && validateLocation(location), [initial, location]);
const handleSelectLocation = useCallback((newValue: string) => {
setHead(newValue.substring(0, 2) as LocationHead);
setBody(newValue.length > 3 ? newValue.substring(3) : '');
}, []);
function handleSubmit() {
onChangeLocation(location);
}
@ -41,13 +50,19 @@ function DlgChangeLocation({ hideWindow, initial, onChangeLocation }: DlgChangeL
className={clsx('w-[35rem]', 'pb-3 px-6 flex gap-3')}
>
<div className='flex flex-col gap-2 w-[7rem] h-min'>
<Label text='Корень' />
<Label className='select-none' text='Корень' />
<SelectLocationHead
value={head} // prettier: split-lines
onChange={setHead}
excluded={!user?.is_staff ? [LocationHead.LIBRARY] : []}
/>
</div>
<SelectLocationContext
folderTree={folders}
value={location}
onChange={handleSelectLocation}
className='max-h-[9.2rem]'
/>
<TextArea
id='dlg_cst_body'
label='Путь'

View File

@ -1,12 +1,13 @@
'use client';
import clsx from 'clsx';
import { useMemo, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { toast } from 'react-toastify';
import { urls } from '@/app/urls';
import { VisibilityIcon } from '@/components/DomainIcons';
import SelectAccessPolicy from '@/components/select/SelectAccessPolicy';
import SelectLocationContext from '@/components/select/SelectLocationContext';
import SelectLocationHead from '@/components/select/SelectLocationHead';
import Checkbox from '@/components/ui/Checkbox';
import Label from '@/components/ui/Label';
@ -44,10 +45,15 @@ function DlgCloneLibraryItem({ hideWindow, base, initialLocation, selected, tota
const [body, setBody] = useState(initialLocation.substring(3));
const location = useMemo(() => combineLocation(head, body), [head, body]);
const { cloneItem } = useLibrary();
const { cloneItem, folders } = useLibrary();
const canSubmit = useMemo(() => title !== '' && alias !== '' && validateLocation(location), [title, alias, location]);
const handleSelectLocation = useCallback((newValue: string) => {
setHead(newValue.substring(0, 2) as LocationHead);
setBody(newValue.length > 3 ? newValue.substring(3) : '');
}, []);
function handleSubmit() {
const data: IRSFormCloneData = {
item_type: base.item_type,
@ -118,6 +124,7 @@ function DlgCloneLibraryItem({ hideWindow, base, initialLocation, selected, tota
excluded={!user?.is_staff ? [LocationHead.LIBRARY] : []}
/>
</div>
<SelectLocationContext folderTree={folders} value={location} onChange={handleSelectLocation} />
<TextArea
id='dlg_cst_body'
label='Путь'

View File

@ -10,7 +10,7 @@ import { IconDownload } from '@/components/Icons';
import InfoError from '@/components/info/InfoError';
import SelectAccessPolicy from '@/components/select/SelectAccessPolicy';
import SelectItemType from '@/components/select/SelectItemType';
import SelectLocation from '@/components/select/SelectLocation';
import SelectLocationContext from '@/components/select/SelectLocationContext';
import SelectLocationHead from '@/components/select/SelectLocationHead';
import Button from '@/components/ui/Button';
import Label from '@/components/ui/Label';
@ -185,11 +185,7 @@ function FormCreateItem() {
excluded={!user?.is_staff ? [LocationHead.LIBRARY] : []}
/>
</div>
{user?.is_staff ? (
<div className='self-start mt-[-0.25rem] ml-[-1.5rem]'>
<SelectLocation folderTree={folders} value={location} onChange={handleSelectLocation} />
</div>
) : null}
<SelectLocationContext folderTree={folders} value={location} onChange={handleSelectLocation} />
<TextArea
id='dlg_cst_body'
label='Путь'

View File

@ -1,19 +1,18 @@
'use client';
import clsx from 'clsx';
import { motion } from 'framer-motion';
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import { useCallback } from 'react';
import { toast } from 'react-toastify';
import { IconFolder, IconFolderClosed, IconFolderEmpty, IconFolderOpened, IconFolderTree } from '@/components/Icons';
import { IconFolderTree } from '@/components/Icons';
import BadgeHelp from '@/components/info/BadgeHelp';
import { CProps } from '@/components/props';
import SelectLocation from '@/components/select/SelectLocation';
import MiniButton from '@/components/ui/MiniButton';
import { FolderNode, FolderTree } from '@/models/FolderTree';
import { HelpTopic } from '@/models/miscellaneous';
import { animateSideView } from '@/styling/animations';
import { PARAMETER, prefixes } from '@/utils/constants';
import { information, labelFolderNode } from '@/utils/labels';
import { information } from '@/utils/labels';
interface LibraryTableProps {
folders: FolderTree;
@ -23,33 +22,6 @@ interface LibraryTableProps {
}
function LibraryFolders({ folders, currentFolder, setFolder, toggleFolderMode }: LibraryTableProps) {
const activeNode = useMemo(() => folders.at(currentFolder), [folders, currentFolder]);
const items = useMemo(() => folders.getTree(), [folders]);
const [folded, setFolded] = useState<FolderNode[]>(items);
useLayoutEffect(() => {
setFolded(items.filter(item => item !== activeNode && (!activeNode || !activeNode.hasPredecessor(item))));
}, [items, activeNode]);
const onFoldItem = useCallback(
(target: FolderNode, showChildren: boolean) => {
setFolded(prev =>
items.filter(item => {
if (item === target) {
return !showChildren;
}
if (!showChildren && item.hasPredecessor(target)) {
return true;
} else {
return prev.includes(item);
}
})
);
},
[items]
);
const handleClickFolder = useCallback(
(event: CProps.EventMouse, target: FolderNode) => {
event.preventDefault();
@ -66,18 +38,9 @@ function LibraryFolders({ folders, currentFolder, setFolder, toggleFolderMode }:
[setFolder]
);
const handleClickFold = useCallback(
(event: CProps.EventMouse, target: FolderNode, showChildren: boolean) => {
event.preventDefault();
event.stopPropagation();
onFoldItem(target, showChildren);
},
[onFoldItem]
);
return (
<motion.div
className='flex flex-col select-none text:xs sm:text-sm'
className='flex flex-col select-none text:xs sm:text-sm max-w-[10rem] sm:max-w-[15rem] min-w-[10rem] sm:min-w-[15rem]'
initial={{ ...animateSideView.initial }}
animate={{ ...animateSideView.animate }}
exit={{ ...animateSideView.exit }}
@ -95,60 +58,12 @@ function LibraryFolders({ folders, currentFolder, setFolder, toggleFolderMode }:
onClick={toggleFolderMode}
/>
</div>
<div
className={clsx(
'max-w-[10rem] sm:max-w-[15rem] min-w-[10rem] sm:min-w-[15rem]',
'flex flex-col',
'cc-scroll-y'
)}
>
{items.map((item, index) =>
!item.parent || !folded.includes(item.parent) ? (
<div
tabIndex={-1}
key={`${prefixes.folders_list}${index}`}
className={clsx(
'min-h-[2.0825rem] sm:min-h-[2.3125rem]',
'pr-3 flex items-center gap-2',
'cc-scroll-row',
'clr-hover',
'cursor-pointer',
activeNode === item && 'clr-selected'
)}
style={{ paddingLeft: `${(item.rank > 5 ? 5 : item.rank) * 0.5 + 0.5}rem` }}
onClick={event => handleClickFolder(event, item)}
>
{item.children.size > 0 ? (
<MiniButton
noPadding
noHover
icon={
folded.includes(item) ? (
item.filesInside ? (
<IconFolderClosed size='1rem' className='icon-primary' />
) : (
<IconFolderEmpty size='1rem' className='icon-primary' />
)
) : (
<IconFolderOpened size='1rem' className='icon-green' />
)
}
onClick={event => handleClickFold(event, item, folded.includes(item))}
<SelectLocation
value={currentFolder}
folderTree={folders}
prefix={prefixes.folders_list}
onClick={handleClickFolder}
/>
) : (
<div>
{item.filesInside ? (
<IconFolder size='1rem' className='clr-text-default' />
) : (
<IconFolderEmpty size='1rem' className='clr-text-controls' />
)}
</div>
)}
<div className='self-center'>{labelFolderNode(item)}</div>
</div>
) : null
)}
</div>
</motion.div>
);
}