Implement location selector button
Some checks are pending
Frontend CI / build (18.x) (push) Waiting to run

This commit is contained in:
IRBorisov 2024-06-26 18:59:49 +03:00
parent dbfab8446c
commit 0ee686f6a4
7 changed files with 196 additions and 130 deletions

View File

@ -1,40 +1,109 @@
'use client'; 'use client';
import { useCallback } from 'react'; import clsx from 'clsx';
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import useDropdown from '@/hooks/useDropdown'; import { FolderNode, FolderTree } from '@/models/FolderTree';
import { 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'; import MiniButton from '../ui/MiniButton';
interface SelectLocationProps { interface SelectLocationProps extends CProps.Styling {
value: string; value: string;
onChange: (newValue: string) => void;
folderTree: FolderTree; folderTree: FolderTree;
prefix: string;
dense?: boolean;
onClick: (event: CProps.EventMouse, target: FolderNode) => void;
} }
function SelectLocation({ value, onChange, folderTree }: SelectLocationProps) { function SelectLocation({ value, folderTree, dense, prefix, onClick, className, style }: SelectLocationProps) {
const menu = useDropdown(); const activeNode = useMemo(() => folderTree.at(value), [folderTree, value]);
const handleChange = useCallback( const items = useMemo(() => folderTree.getTree(), [folderTree]);
(newValue: string) => { const [folded, setFolded] = useState<FolderNode[]>(items);
console.log(folderTree.roots.size);
console.log(value); useLayoutEffect(() => {
menu.hide(); setFolded(items.filter(item => item !== activeNode && (!activeNode || !activeNode.hasPredecessor(item))));
onChange(newValue); }, [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 ( 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 <MiniButton
title='Проводник...' noPadding
icon={<IconFolderTree size='1.25rem' className='icon-green' />} noHover
onClick={() => handleChange('/U/test')} 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> </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 { interface DropdownProps extends CProps.Styling {
stretchLeft?: boolean; stretchLeft?: boolean;
stretchTop?: boolean;
isOpen: boolean; isOpen: boolean;
children: React.ReactNode; children: React.ReactNode;
} }
function Dropdown({ isOpen, stretchLeft, className, children, ...restProps }: DropdownProps) { function Dropdown({ isOpen, stretchLeft, stretchTop, className, children, ...restProps }: DropdownProps) {
return ( return (
<div className='relative'> <div className='relative'>
<motion.div <motion.div
@ -25,7 +26,8 @@ function Dropdown({ isOpen, stretchLeft, className, children, ...restProps }: Dr
'clr-input', 'clr-input',
{ {
'right-0': stretchLeft, 'right-0': stretchLeft,
'left-0': !stretchLeft 'left-0': !stretchLeft,
'bottom-[2rem]': stretchTop
}, },
className className
)} )}

View File

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

View File

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

View File

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

View File

@ -1,19 +1,18 @@
'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { useCallback, useLayoutEffect, useMemo, useState } from 'react'; import { useCallback } from 'react';
import { toast } from 'react-toastify'; 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 BadgeHelp from '@/components/info/BadgeHelp';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import SelectLocation from '@/components/select/SelectLocation';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import { FolderNode, FolderTree } from '@/models/FolderTree'; import { FolderNode, FolderTree } from '@/models/FolderTree';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
import { animateSideView } from '@/styling/animations'; import { animateSideView } from '@/styling/animations';
import { PARAMETER, prefixes } from '@/utils/constants'; import { PARAMETER, prefixes } from '@/utils/constants';
import { information, labelFolderNode } from '@/utils/labels'; import { information } from '@/utils/labels';
interface LibraryTableProps { interface LibraryTableProps {
folders: FolderTree; folders: FolderTree;
@ -23,33 +22,6 @@ interface LibraryTableProps {
} }
function LibraryFolders({ folders, currentFolder, setFolder, toggleFolderMode }: 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( const handleClickFolder = useCallback(
(event: CProps.EventMouse, target: FolderNode) => { (event: CProps.EventMouse, target: FolderNode) => {
event.preventDefault(); event.preventDefault();
@ -66,18 +38,9 @@ function LibraryFolders({ folders, currentFolder, setFolder, toggleFolderMode }:
[setFolder] [setFolder]
); );
const handleClickFold = useCallback(
(event: CProps.EventMouse, target: FolderNode, showChildren: boolean) => {
event.preventDefault();
event.stopPropagation();
onFoldItem(target, showChildren);
},
[onFoldItem]
);
return ( return (
<motion.div <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 }} initial={{ ...animateSideView.initial }}
animate={{ ...animateSideView.animate }} animate={{ ...animateSideView.animate }}
exit={{ ...animateSideView.exit }} exit={{ ...animateSideView.exit }}
@ -95,60 +58,12 @@ function LibraryFolders({ folders, currentFolder, setFolder, toggleFolderMode }:
onClick={toggleFolderMode} onClick={toggleFolderMode}
/> />
</div> </div>
<div <SelectLocation
className={clsx( value={currentFolder}
'max-w-[10rem] sm:max-w-[15rem] min-w-[10rem] sm:min-w-[15rem]', folderTree={folders}
'flex flex-col', prefix={prefixes.folders_list}
'cc-scroll-y' onClick={handleClickFolder}
)}
>
{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))}
/> />
) : (
<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> </motion.div>
); );
} }