Implement location selector button
Some checks are pending
Frontend CI / build (18.x) (push) Waiting to run
Some checks are pending
Frontend CI / build (18.x) (push) Waiting to run
This commit is contained in:
parent
dbfab8446c
commit
0ee686f6a4
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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
|
||||
)}
|
||||
|
|
|
@ -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='Путь'
|
||||
|
|
|
@ -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='Путь'
|
||||
|
|
|
@ -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='Путь'
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user