Co-authored-by: Anton SHEPILOV <ashepilov@linagora.com> Co-authored-by: montaghanmy <monta.ghanmy@gmail.com>
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import React, { MouseEventHandler } from 'react';
|
||||
import {useDraggable} from '@dnd-kit/core';
|
||||
|
||||
type DraggableProps={
|
||||
children: React.ReactNode
|
||||
id: number
|
||||
}
|
||||
|
||||
export function Draggable(props:DraggableProps) {
|
||||
const {attributes, listeners, setNodeRef, transform} = useDraggable({
|
||||
id: `draggable-${props.id+1}`,
|
||||
data: {
|
||||
child: props.children
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} {...listeners} {...attributes}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import {useDroppable} from '@dnd-kit/core';
|
||||
|
||||
type DroppableProps={
|
||||
children: React.ReactNode
|
||||
id: any
|
||||
}
|
||||
|
||||
export function Droppable(props:DroppableProps) {
|
||||
const { isOver, setNodeRef } = useDroppable({
|
||||
id: `droppable-${props.id}`,
|
||||
data: {
|
||||
child: props.children
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} className={isOver ? "bg-sky-500/[.18] rounded-t-md rounded-b-md" : ""}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
useOnBuildPeopleContextMenu,
|
||||
useOnBuildDateContextMenu,
|
||||
} from './context-menu';
|
||||
import { DocumentRow } from './documents/document-row';
|
||||
import {DocumentRow, DocumentRowOverlay} from './documents/document-row';
|
||||
import { FolderRow } from './documents/folder-row';
|
||||
import HeaderPath from './header-path';
|
||||
import { ConfirmDeleteModal } from './modals/confirm-delete';
|
||||
@@ -36,11 +36,19 @@ import useRouteState from 'app/features/router/hooks/use-route-state';
|
||||
import { SharedWithMeFilterState } from '@features/drive/state/shared-with-me-filter';
|
||||
import MenusManager from '@components/menus/menus-manager.jsx';
|
||||
import Languages from 'features/global/services/languages-service';
|
||||
import {DndContext, useSensors, useSensor, PointerSensor, DragOverlay} from '@dnd-kit/core';
|
||||
import { Droppable } from 'app/features/dragndrop/hook/droppable';
|
||||
import { Draggable } from 'app/features/dragndrop/hook/draggable';
|
||||
import { useDriveActions } from '@features/drive/hooks/use-drive-actions';
|
||||
import { ConfirmModalAtom } from './modals/confirm-move/index';
|
||||
import { useCurrentUser } from 'app/features/users/hooks/use-current-user';
|
||||
import { ConfirmModal } from './modals/confirm-move';
|
||||
|
||||
|
||||
|
||||
export const DriveCurrentFolderAtom = atomFamily<
|
||||
string,
|
||||
{ context?: string; initialFolderId: string }
|
||||
string,
|
||||
{ context?: string; initialFolderId: string }
|
||||
>({
|
||||
key: 'DriveCurrentFolderAtom',
|
||||
default: options => options.initialFolderId || 'root',
|
||||
@@ -158,13 +166,78 @@ export default memo(
|
||||
const buildFileTypeContextMenu = useOnBuildFileTypeContextMenu();
|
||||
const buildPeopleContextMen = useOnBuildPeopleContextMenu();
|
||||
const buildDateContextMenu = useOnBuildDateContextMenu();
|
||||
const setConfirmModalState = useSetRecoilState(ConfirmModalAtom);
|
||||
const [activeIndex, setActiveIndex] = useState(null);
|
||||
const [activeChild, setActiveChild] = useState(null);
|
||||
const {update} = useDriveActions();
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 8,
|
||||
},
|
||||
})
|
||||
);
|
||||
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
||||
|
||||
function handleDragStart(event:any) {
|
||||
setActiveIndex(event.active.id);
|
||||
setActiveChild(event.active.data.current.child.props.item);
|
||||
}
|
||||
function handleDragEnd(event:any) {
|
||||
setActiveIndex(null);
|
||||
setActiveChild(null);
|
||||
if (event.over){
|
||||
setConfirmModalState({
|
||||
open: true,
|
||||
parent_id: inTrash ? 'root' : event.over.data.current.child.props.item.id,
|
||||
mode: 'move',
|
||||
title:
|
||||
Languages.t('components.item_context_menu.move.modal_header') +
|
||||
` '${event.active.data.current.child.props.item.name}'`,
|
||||
onSelected: async ids => {
|
||||
await update(
|
||||
{
|
||||
parent_id: ids[0],
|
||||
},
|
||||
event.active.data.current.child.props.item.id,
|
||||
event.active.data.current.child.props.item.parent_id,
|
||||
);
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function draggableMarkup(index: number, child: any) {
|
||||
const commonProps = {
|
||||
key: index,
|
||||
className:
|
||||
(index === 0 ? 'rounded-t-md ' : '') +
|
||||
(index === documents.length - 1 ? 'rounded-b-md ' : ''),
|
||||
item: child,
|
||||
checked: checked[child.id] || false,
|
||||
onCheck: (v: boolean) =>
|
||||
setChecked(_.pickBy({ ...checked, [child.id]: v }, _.identity)),
|
||||
onBuildContextMenu: () => onBuildContextMenu(details, child),
|
||||
};
|
||||
return (
|
||||
isMobile ? (
|
||||
<DocumentRow {...commonProps} />
|
||||
) : (
|
||||
<Draggable id={index}>
|
||||
<DocumentRow {...commonProps} />
|
||||
</Draggable>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{viewId == 'shared-with-me' ? (
|
||||
<>
|
||||
<Suspense fallback={<></>}>
|
||||
<DrivePreview items={documents} />
|
||||
<DrivePreview items={documents}/>
|
||||
</Suspense>
|
||||
<SharedFilesTable />
|
||||
</>
|
||||
@@ -197,8 +270,9 @@ export default memo(
|
||||
<PropertiesModal />
|
||||
<ConfirmDeleteModal />
|
||||
<ConfirmTrashModal />
|
||||
<ConfirmModal />
|
||||
<Suspense fallback={<></>}>
|
||||
<DrivePreview items={documents} />
|
||||
<DrivePreview items={documents}/>
|
||||
</Suspense>
|
||||
<div
|
||||
className={
|
||||
@@ -297,69 +371,76 @@ export default memo(
|
||||
</Menu>
|
||||
</div>
|
||||
|
||||
<div className="grow overflow-auto">
|
||||
{folders.length > 0 && (
|
||||
<>
|
||||
<Title className="mb-2 block">{Languages.t('scenes.app.drive.folders')}</Title>
|
||||
|
||||
{folders.map((child, index) => (
|
||||
<FolderRow
|
||||
key={index}
|
||||
className={
|
||||
(index === 0 ? 'rounded-t-md ' : '') +
|
||||
(index === folders.length - 1 ? 'rounded-b-md ' : '')
|
||||
}
|
||||
item={child}
|
||||
onClick={() => {
|
||||
return setParentId(child.id);
|
||||
}}
|
||||
checked={checked[child.id] || false}
|
||||
onCheck={v =>
|
||||
setChecked(_.pickBy({ ...checked, [child.id]: v }, _.identity))
|
||||
}
|
||||
onBuildContextMenu={() => onBuildContextMenu(details, child)}
|
||||
/>
|
||||
))}
|
||||
<div className="my-6" />
|
||||
</>
|
||||
)}
|
||||
<DndContext sensors={sensors} onDragEnd={handleDragEnd} onDragStart={handleDragStart}>
|
||||
<div className="grow overflow-auto">
|
||||
|
||||
{folders.length > 0 && (
|
||||
<>
|
||||
<Title className="mb-2 block">{Languages.t('scenes.app.drive.folders')}</Title>
|
||||
|
||||
<Title className="mb-2 block">{Languages.t('scenes.app.drive.documents')}</Title>
|
||||
{folders.map((child, index) => (
|
||||
<Droppable id={index} key={index}>
|
||||
<FolderRow
|
||||
key={index}
|
||||
className={
|
||||
(index === 0 ? 'rounded-t-md ' : '') +
|
||||
(index === folders.length - 1 ? 'rounded-b-md ' : '')
|
||||
}
|
||||
item={child}
|
||||
onClick={() => {
|
||||
return setParentId(child.id);
|
||||
}}
|
||||
checked={checked[child.id] || false}
|
||||
onCheck={v =>
|
||||
setChecked(_.pickBy({ ...checked, [child.id]: v }, _.identity))
|
||||
}
|
||||
onBuildContextMenu={() => onBuildContextMenu(details, child)}
|
||||
/>
|
||||
</Droppable>
|
||||
))}
|
||||
<div className="my-6" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{documents.length === 0 && !loading && (
|
||||
<div className="mt-4 text-center border-2 border-dashed rounded-md p-8">
|
||||
<Subtitle className="block mb-2">
|
||||
{Languages.t('scenes.app.drive.nothing')}
|
||||
</Subtitle>
|
||||
{!inTrash && access != 'read' && (
|
||||
<>
|
||||
<Base>{Languages.t('scenes.app.drive.drag_and_drop')}</Base>
|
||||
<br />
|
||||
<Button onClick={() => openItemModal()} theme="primary" className="mt-4">
|
||||
{Languages.t('scenes.app.drive.add_doc')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Title className="mb-2 block">{Languages.t('scenes.app.drive.documents')}</Title>
|
||||
|
||||
{documents.map((child, index) => (
|
||||
<DocumentRow
|
||||
key={index}
|
||||
className={
|
||||
(index === 0 ? 'rounded-t-md ' : '') +
|
||||
(index === documents.length - 1 ? 'rounded-b-md ' : '')
|
||||
}
|
||||
item={child}
|
||||
checked={checked[child.id] || false}
|
||||
onCheck={v => setChecked(_.pickBy({ ...checked, [child.id]: v }, _.identity))}
|
||||
onBuildContextMenu={() => onBuildContextMenu(details, child)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{documents.length === 0 && !loading && (
|
||||
<div className="mt-4 text-center border-2 border-dashed rounded-md p-8">
|
||||
<Subtitle className="block mb-2">
|
||||
{Languages.t('scenes.app.drive.nothing')}
|
||||
</Subtitle>
|
||||
{!inTrash && access != 'read' && (
|
||||
<>
|
||||
<Base>{Languages.t('scenes.app.drive.drag_and_drop')}</Base>
|
||||
<br />
|
||||
<Button onClick={() => openItemModal()} theme="primary" className="mt-4">
|
||||
{Languages.t('scenes.app.drive.add_doc')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{documents.map((child, index) => (
|
||||
draggableMarkup(index, child)
|
||||
))}
|
||||
<DragOverlay>
|
||||
{activeIndex ? (
|
||||
<DocumentRowOverlay className={
|
||||
(activeIndex === 0 ? 'rounded-t-md ' : '') +
|
||||
(activeIndex === documents.length - 1 ? 'rounded-b-md ' : '')}
|
||||
item={activeChild}
|
||||
></DocumentRowOverlay>
|
||||
): null}
|
||||
</DragOverlay>
|
||||
|
||||
</div>
|
||||
</DndContext>
|
||||
</div>
|
||||
</UploadZone>
|
||||
)}
|
||||
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -11,6 +11,10 @@ export type DriveItemProps = {
|
||||
onBuildContextMenu: () => Promise<any[]>;
|
||||
};
|
||||
|
||||
export type DriveItemOverlayProps = {
|
||||
item: DriveItem|null;
|
||||
className: string;
|
||||
};
|
||||
export const menuBuilder = async () => {};
|
||||
|
||||
export const CheckableIcon = ({
|
||||
|
||||
@@ -19,7 +19,7 @@ import fileUploadApiClient from '@features/files/api/file-upload-api-client';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Avatar from '../../../../../atoms/avatar';
|
||||
import { PublicIcon } from '../components/public-icon';
|
||||
import { CheckableIcon, DriveItemProps } from './common';
|
||||
import {CheckableIcon, DriveItemOverlayProps, DriveItemProps} from './common';
|
||||
import { getDevice } from '../../../../../features/global/utils/device';
|
||||
import './style.scss';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
@@ -143,3 +143,21 @@ export const DocumentRow = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const DocumentRowOverlay = ({
|
||||
item,
|
||||
className,
|
||||
}: DriveItemOverlayProps) => {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
'flex flex-row items-center border border-zinc-200 dark:border-zinc-800 -mt-px px-4 py-3 cursor-pointer ' +
|
||||
(className || '')
|
||||
}
|
||||
>
|
||||
<div className="grow text-ellipsis whitespace-nowrap overflow-hidden">
|
||||
<Base>{item?.name}</Base>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button } from '@atoms/button/button';
|
||||
import { Modal, ModalContent } from '@atoms/modal';
|
||||
import { useDriveItem } from '@features/drive/hooks/use-drive-item';
|
||||
import { DriveItem } from '@features/drive/types';
|
||||
import { atom, useRecoilState } from 'recoil';
|
||||
import Languages from '@features/global/services/languages-service';
|
||||
|
||||
export type ConfirmModalType = {
|
||||
open: boolean;
|
||||
parent_id: string;
|
||||
mode: 'move' | 'select-file' | 'select-files';
|
||||
title: string;
|
||||
onSelected: (ids: string[]) => Promise<void>;
|
||||
};
|
||||
|
||||
export const ConfirmModalAtom = atom<ConfirmModalType>({
|
||||
key: 'ConfirmModalAtom',
|
||||
default: {
|
||||
open: false,
|
||||
parent_id: '',
|
||||
mode: 'move',
|
||||
title: '',
|
||||
onSelected: async () => {},
|
||||
},
|
||||
});
|
||||
|
||||
const ConfirmModalContent = () => {
|
||||
const [state, setState] = useRecoilState(ConfirmModalAtom);
|
||||
const [selected, setSelected] = useState<DriveItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const parentId = state.parent_id;
|
||||
|
||||
const { item: parent } = useDriveItem(parentId);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.mode === 'select-file' && parent) setSelected([]);
|
||||
if (state.mode === 'move' && parent) setSelected([parent]);
|
||||
}, [parentId, parent?.id]);
|
||||
|
||||
const handleClose = async () => {
|
||||
setLoading(true);
|
||||
await state.onSelected(selected.map((i) => i.id));
|
||||
setState({ ...state, open: false });
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalContent title={state.title}>
|
||||
<Button
|
||||
disabled={selected.length === 0}
|
||||
loading={loading}
|
||||
theme="primary"
|
||||
className="float-right"
|
||||
onClick={handleClose}
|
||||
>
|
||||
<>{Languages.t('components.SelectorModalContent_move_to')} '{selected[0]?.name}'</>
|
||||
</Button>
|
||||
</ModalContent>
|
||||
);
|
||||
};
|
||||
|
||||
export const ConfirmModal = () => {
|
||||
const [state, setState] = useRecoilState(ConfirmModalAtom);
|
||||
|
||||
const handleClose = () => {
|
||||
setState({ ...state, open: false });
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={state.open} onClose={handleClose}>
|
||||
<ConfirmModalContent key={state.parent_id} />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user