✨ Sort files chronologically + infinite scroll (#535)
This commit is contained in:
@@ -7,21 +7,23 @@ import UploadZone from '@components/uploads/upload-zone';
|
||||
import { setTdriveTabToken } from '@features/drive/api-client/api-client';
|
||||
import { useDriveItem } from '@features/drive/hooks/use-drive-item';
|
||||
import { useDriveUpload } from '@features/drive/hooks/use-drive-upload';
|
||||
import { DriveItemSelectedList } from '@features/drive/state/store';
|
||||
import { DriveItemSelectedList, DriveItemSort } from '@features/drive/state/store';
|
||||
import { formatBytes } from '@features/drive/utils';
|
||||
import useRouterCompany from '@features/router/hooks/use-router-company';
|
||||
import _ from 'lodash';
|
||||
import _, { set } from 'lodash';
|
||||
import { memo, Suspense, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { atomFamily, useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { atomFamily, useRecoilState, useSetRecoilState, useRecoilValue } from 'recoil';
|
||||
import { DrivePreview } from '../../viewer/drive-preview';
|
||||
import {
|
||||
useOnBuildContextMenu,
|
||||
useOnBuildFileTypeContextMenu,
|
||||
useOnBuildPeopleContextMenu,
|
||||
useOnBuildDateContextMenu,
|
||||
useOnBuildSortContextMenu,
|
||||
} from './context-menu';
|
||||
import {DocumentRow, DocumentRowOverlay} from './documents/document-row';
|
||||
import { FolderRow } from './documents/folder-row';
|
||||
import { FolderRowSkeleton } from './documents/folder-row-skeleton';
|
||||
import HeaderPath from './header-path';
|
||||
import { ConfirmDeleteModal } from './modals/confirm-delete';
|
||||
import { ConfirmTrashModal } from './modals/confirm-trash';
|
||||
@@ -46,6 +48,8 @@ import { ConfirmModalAtom } from './modals/confirm-move/index';
|
||||
import { useCurrentUser } from 'app/features/users/hooks/use-current-user';
|
||||
import { ConfirmModal } from './modals/confirm-move';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { SortIcon } from 'app/atoms/icons-agnostic';
|
||||
import { useDrivePreview, useDrivePreviewLoading } from 'app/features/drive/hooks/use-drive-preview';
|
||||
|
||||
export const DriveCurrentFolderAtom = atomFamily<
|
||||
string,
|
||||
@@ -70,19 +74,32 @@ export default memo(
|
||||
const { user } = useCurrentUser();
|
||||
const companyId = useRouterCompany();
|
||||
const history = useHistory();
|
||||
const role = user ? (user?.companies || []).find(company => company?.company.id === companyId)?.role : "member";
|
||||
const role = user
|
||||
? (user?.companies || []).find(company => company?.company.id === companyId)?.role
|
||||
: 'member';
|
||||
setTdriveTabToken(tdriveTabContextToken || null);
|
||||
const [filter, __] = useRecoilState(SharedWithMeFilterState);
|
||||
const { viewId, dirId } = useRouteState();
|
||||
const { viewId, dirId, itemId } = useRouteState();
|
||||
const { status } = useDrivePreview();
|
||||
const { openWithId, close } = useDrivePreview();
|
||||
const [sortLabel] = useRecoilState(DriveItemSort)
|
||||
const { loading: isModalLoading } = useDrivePreviewLoading();
|
||||
const [parentId, _setParentId] = useRecoilState(
|
||||
DriveCurrentFolderAtom({ context: context, initialFolderId: dirId || viewId || initialParentId || 'user_'+user?.id }),
|
||||
DriveCurrentFolderAtom({
|
||||
context: context,
|
||||
initialFolderId: dirId || viewId || initialParentId || 'user_' + user?.id,
|
||||
}),
|
||||
);
|
||||
|
||||
// set the initial view to the user's home directory
|
||||
useEffect(() => {
|
||||
!dirId && !viewId && history.push(RouterServices.generateRouteFromState({viewId: parentId}));
|
||||
!dirId &&
|
||||
!viewId &&
|
||||
history.push(RouterServices.generateRouteFromState({ viewId: parentId }));
|
||||
}, [viewId, dirId]);
|
||||
|
||||
|
||||
|
||||
const [loadingParentChange, setLoadingParentChange] = useState(false);
|
||||
const {
|
||||
sharedWithMe,
|
||||
@@ -94,6 +111,7 @@ export default memo(
|
||||
children,
|
||||
loading: loadingParent,
|
||||
path,
|
||||
loadNextPage,
|
||||
} = useDriveItem(parentId);
|
||||
const { uploadTree } = useDriveUpload();
|
||||
|
||||
@@ -131,10 +149,6 @@ export default memo(
|
||||
if (item?.id) setUploadModalState({ open: true, parent_id: item.id });
|
||||
}, [item?.id, setUploadModalState]);
|
||||
|
||||
const selectedCount = Object.values(checked).filter(v => v).length;
|
||||
const folders = children
|
||||
.filter(i => i.is_directory)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
const documents = (
|
||||
item?.is_directory === false
|
||||
? //We use this hack for public shared single file
|
||||
@@ -142,27 +156,28 @@ export default memo(
|
||||
? [item]
|
||||
: []
|
||||
: children
|
||||
)
|
||||
.filter(i => !i.is_directory)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
).filter(i => !i.is_directory);
|
||||
|
||||
const selectedCount = Object.values(checked).filter(v => v).length;
|
||||
|
||||
const onBuildContextMenu = useOnBuildContextMenu(children, initialParentId);
|
||||
const onBuildSortContextMenu = useOnBuildSortContextMenu();
|
||||
|
||||
const handleDragOver = (event: { preventDefault: () => void; }) => {
|
||||
const handleDragOver = (event: { preventDefault: () => void }) => {
|
||||
event.preventDefault();
|
||||
}
|
||||
const handleDrop = async (event: {dataTransfer: any; preventDefault: () => void;}) => {
|
||||
};
|
||||
const handleDrop = async (event: { dataTransfer: any; preventDefault: () => void }) => {
|
||||
event.preventDefault();
|
||||
const dataTransfer = event.dataTransfer;
|
||||
if (dataTransfer) {
|
||||
const tree = await getFilesTree(dataTransfer);
|
||||
setCreationModalState({ parent_id: '', open: false });
|
||||
await uploadTree(tree, {
|
||||
companyId,
|
||||
parentId,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (dataTransfer) {
|
||||
const tree = await getFilesTree(dataTransfer);
|
||||
setCreationModalState({ parent_id: '', open: false });
|
||||
await uploadTree(tree, {
|
||||
companyId,
|
||||
parentId,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const buildFileTypeContextMenu = useOnBuildFileTypeContextMenu();
|
||||
const buildPeopleContextMen = useOnBuildPeopleContextMenu();
|
||||
@@ -170,24 +185,24 @@ export default memo(
|
||||
const setConfirmModalState = useSetRecoilState(ConfirmModalAtom);
|
||||
const [activeIndex, setActiveIndex] = useState(null);
|
||||
const [activeChild, setActiveChild] = useState(null);
|
||||
const {update} = useDriveActions();
|
||||
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) {
|
||||
function handleDragStart(event: any) {
|
||||
setActiveIndex(event.active.id);
|
||||
setActiveChild(event.active.data.current.child.props.item);
|
||||
}
|
||||
function handleDragEnd(event:any) {
|
||||
function handleDragEnd(event: any) {
|
||||
setActiveIndex(null);
|
||||
setActiveChild(null);
|
||||
if (event.over){
|
||||
if (event.over) {
|
||||
setConfirmModalState({
|
||||
open: true,
|
||||
parent_id: inTrash ? 'root' : event.over.data.current.child.props.item.id,
|
||||
@@ -204,35 +219,49 @@ export default memo(
|
||||
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 ' : ''),
|
||||
(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)),
|
||||
onCheck: (v: boolean) => setChecked(_.pickBy({ ...checked, [child.id]: v }, _.identity)),
|
||||
onBuildContextMenu: () => onBuildContextMenu(details, child),
|
||||
inPublicSharing,
|
||||
};
|
||||
return (
|
||||
isMobile ? (
|
||||
<DocumentRow {...commonProps} />
|
||||
) : (
|
||||
<Draggable id={index} key={index}>
|
||||
<DocumentRow {...commonProps} />
|
||||
</Draggable>
|
||||
)
|
||||
return isMobile ? (
|
||||
<DocumentRow {...commonProps} />
|
||||
) : (
|
||||
<Draggable id={index}>
|
||||
<DocumentRow {...commonProps} />
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
|
||||
// Infinite scroll
|
||||
const scrollViwer = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleScroll = async () => {
|
||||
const scrollTop = scrollViwer.current?.scrollTop || 0;
|
||||
const scrollHeight = scrollViwer.current?.scrollHeight || 0;
|
||||
const clientHeight = scrollViwer.current?.clientHeight || 0;
|
||||
if (scrollTop > 0 && scrollTop + clientHeight >= scrollHeight) {
|
||||
await loadNextPage(parentId);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if(!loading) scrollViwer.current?.addEventListener('scroll', handleScroll, { passive: true });
|
||||
return () => {
|
||||
scrollViwer.current?.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, [parentId, loading]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -363,6 +392,19 @@ export default memo(
|
||||
{formatBytes(item?.size || 0)} {Languages.t('scenes.app.drive.used')}
|
||||
</BaseSmall>
|
||||
)}
|
||||
|
||||
<Menu menu={() => onBuildSortContextMenu()} sortData={sortLabel}>
|
||||
{' '}
|
||||
<Button theme="outline" className="ml-4 flex flex-row items-center">
|
||||
<SortIcon
|
||||
className={`h-4 w-4 mr-2 -ml-1 ${
|
||||
sortLabel.order === 'asc' ? 'transform rotate-180' : ''
|
||||
}`}
|
||||
/>
|
||||
<span>By {sortLabel.by}</span>
|
||||
<ChevronDownIcon className="h-4 w-4 ml-2 -mr-1" />
|
||||
</Button>
|
||||
</Menu>
|
||||
{viewId !== 'shared_with_me' && (
|
||||
<Menu menu={() => onBuildContextMenu(details)}>
|
||||
{' '}
|
||||
@@ -380,43 +422,7 @@ export default memo(
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
{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={() => {
|
||||
const route = RouterServices.generateRouteFromState({
|
||||
dirId: child.id,
|
||||
});
|
||||
history.push(route);
|
||||
if (inPublicSharing) 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" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Title className="mb-2 block">{Languages.t('scenes.app.drive.documents')}</Title>
|
||||
|
||||
<div className="grow overflow-auto" ref={scrollViwer}>
|
||||
{documents.length === 0 && !loading && (
|
||||
<div className="mt-4 text-center border-2 border-dashed rounded-md p-8">
|
||||
<Subtitle className="block mb-2">
|
||||
@@ -437,19 +443,46 @@ export default memo(
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{documents.map((child, index) => draggableMarkup(index, child))}
|
||||
{children.map((child, index) =>
|
||||
child.is_directory ? (
|
||||
<Droppable id={index} key={index}>
|
||||
<FolderRow
|
||||
key={index}
|
||||
className={
|
||||
(index === 0 ? 'rounded-t-md ' : '') +
|
||||
(index === children.length - 1 ? 'rounded-b-md ' : '')
|
||||
}
|
||||
item={child}
|
||||
onClick={() => {
|
||||
const route = RouterServices.generateRouteFromState({
|
||||
dirId: child.id,
|
||||
});
|
||||
history.push(route);
|
||||
if (inPublicSharing) return setParentId(child.id);
|
||||
}}
|
||||
checked={checked[child.id] || false}
|
||||
onCheck={v =>
|
||||
setChecked(_.pickBy({ ...checked, [child.id]: v }, _.identity))
|
||||
}
|
||||
onBuildContextMenu={() => onBuildContextMenu(details, child)}
|
||||
/>
|
||||
</Droppable>
|
||||
) : (
|
||||
draggableMarkup(index, child)
|
||||
),
|
||||
)}
|
||||
<DragOverlay>
|
||||
{activeIndex ? (
|
||||
<DocumentRowOverlay
|
||||
className={
|
||||
(activeIndex === 0 ? 'rounded-t-md ' : '') +
|
||||
(activeIndex === documents.length - 1 ? 'rounded-b-md ' : '')
|
||||
(activeIndex === children.length - 1 ? 'rounded-b-md ' : '')
|
||||
}
|
||||
item={activeChild}
|
||||
></DocumentRowOverlay>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
{loading && <FolderRowSkeleton />}
|
||||
</div>
|
||||
</DndContext>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ import { DriveApiClient, getPublicLinkToken } from '@features/drive/api-client/a
|
||||
import { useDriveActions } from '@features/drive/hooks/use-drive-actions';
|
||||
import { getPublicLink } from '@features/drive/hooks/use-drive-item';
|
||||
import { useDrivePreview } from '@features/drive/hooks/use-drive-preview';
|
||||
import { DriveItemSelectedList } from '@features/drive/state/store';
|
||||
import { DriveItemSelectedList, DriveItemSort } from '@features/drive/state/store';
|
||||
import { DriveItem, DriveItemDetails } from '@features/drive/types';
|
||||
import { ToasterService } from '@features/global/services/toaster-service';
|
||||
import { copyToClipboard } from '@features/global/utils/CopyClipboard';
|
||||
@@ -24,7 +24,7 @@ import { getCurrentUserList } from '@features/users/hooks/use-user-list';
|
||||
import useRouteState from 'app/features/router/hooks/use-route-state';
|
||||
import RouterServices from '@features/router/services/router-service';
|
||||
import useRouterCompany from '@features/router/hooks/use-router-company';
|
||||
import _ from 'lodash';
|
||||
import _, { set } from 'lodash';
|
||||
import Languages from 'features/global/services/languages-service';
|
||||
import { hasAnyPublicLinkAccess } from '@features/files/utils/access-info-helpers';
|
||||
|
||||
@@ -301,9 +301,7 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s
|
||||
hide: inTrash,
|
||||
onClick: () => {
|
||||
if (parent.children && parent.children.length > 0) {
|
||||
const idsFromArray = getIdsFromArray(parent.children);
|
||||
console.log("Download zip file with docs: " + idsFromArray);
|
||||
downloadZip(idsFromArray);
|
||||
downloadZip([parent.item!.id]);
|
||||
} else if (parent.item) {
|
||||
console.log("Download folder itself");
|
||||
download(parent.item.id);
|
||||
@@ -519,3 +517,84 @@ export const useOnBuildFileContextMenu = () => {
|
||||
[download, preview],
|
||||
);
|
||||
};
|
||||
|
||||
export const useOnBuildSortContextMenu = () => {
|
||||
const [sortItem, setSortItem] = useRecoilState(DriveItemSort);
|
||||
return useCallback(() => {
|
||||
const menuItems = [
|
||||
{
|
||||
type: 'menu',
|
||||
text: 'Date',
|
||||
icon: sortItem.by === 'date' ? 'check' : 'sort-check',
|
||||
onClick: () => {
|
||||
// keep the old value for sortItem and change the by value
|
||||
setSortItem(prevSortItem => {
|
||||
const newSortItem = {
|
||||
...prevSortItem,
|
||||
by: 'date',
|
||||
};
|
||||
return newSortItem;
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'menu',
|
||||
text: 'Name',
|
||||
icon: sortItem.by === 'name' ? 'check' : 'sort-check',
|
||||
onClick: () => {
|
||||
setSortItem(prevSortItem => {
|
||||
const newSortItem = {
|
||||
...prevSortItem,
|
||||
by: 'name',
|
||||
};
|
||||
return newSortItem;
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'menu',
|
||||
text: 'Size',
|
||||
icon: sortItem.by === 'size' ? 'check' : 'sort-check',
|
||||
onClick: () => {
|
||||
setSortItem(prevSortItem => {
|
||||
const newSortItem = {
|
||||
...prevSortItem,
|
||||
by: 'size',
|
||||
};
|
||||
return newSortItem;
|
||||
});
|
||||
},
|
||||
},
|
||||
{type:"separator"},
|
||||
{
|
||||
type: 'menu',
|
||||
text: 'Ascending',
|
||||
icon: sortItem.order === 'asc' ? 'check' : 'sort-check',
|
||||
onClick: () => {
|
||||
setSortItem(prevSortItem => {
|
||||
const newSortItem = {
|
||||
...prevSortItem,
|
||||
order: 'asc',
|
||||
};
|
||||
return newSortItem;
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'menu',
|
||||
text: 'Descending',
|
||||
icon: sortItem.order === 'desc' ? 'check' : 'sort-check',
|
||||
onClick: () => {
|
||||
setSortItem(prevSortItem => {
|
||||
const newSortItem = {
|
||||
...prevSortItem,
|
||||
order: 'desc',
|
||||
};
|
||||
return newSortItem;
|
||||
});
|
||||
},
|
||||
}
|
||||
];
|
||||
return menuItems;
|
||||
}, [sortItem]);
|
||||
};
|
||||
|
||||
@@ -27,24 +27,13 @@ export const DocumentRow = ({
|
||||
}: DriveItemProps) => {
|
||||
const history = useHistory();
|
||||
const [hover, setHover] = useState(false);
|
||||
const { open, close } = useDrivePreview();
|
||||
const { open, close, isOpen } = useDrivePreview();
|
||||
const company = useRouterCompany();
|
||||
const { itemId } = useRouteState();
|
||||
|
||||
useEffect(() => {
|
||||
// close the preview if the item is not set or the user navigated away
|
||||
if (!itemId) {
|
||||
close();
|
||||
}
|
||||
// open the preview if the item is set
|
||||
if (itemId == item.id) {
|
||||
open(item);
|
||||
}
|
||||
}, [itemId]);
|
||||
|
||||
const preview = () => {
|
||||
open(item);
|
||||
history.push(RouterServices.generateRouteFromState({ companyId: company, itemId: item.id }));
|
||||
if (inPublicSharing) open(item);
|
||||
// history.push(RouterServices.generateRouteFromState({ companyId: company, itemId: item.id }));
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
|
||||
export const FolderRowSkeleton = () => {
|
||||
return (
|
||||
<div className="flex flex-row items-center border border-zinc-200 dark:border-zinc-800 -mt-px px-4 py-3 cursor-pointer animate-pulse">
|
||||
<div className="mr-2 -ml-1">
|
||||
<div className="bg-gray-300 h-5 w-5 rounded-full"></div>
|
||||
</div>
|
||||
<div className="grow text-ellipsis whitespace-nowrap overflow-hidden">
|
||||
<div className="bg-gray-300 h-6 w-3/4 rounded"></div>
|
||||
</div>
|
||||
<div className="shrink-0 ml-4">
|
||||
<div className="bg-gray-300 h-5 w-5 rounded-full"></div>
|
||||
</div>
|
||||
<div className="shrink-0 ml-4 text-right minWidth80">
|
||||
<div className="bg-gray-300 h-4 w-20 rounded"></div>
|
||||
</div>
|
||||
<div className="shrink-0 ml-4">
|
||||
<div className="bg-gray-300 h-8 w-8 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -104,6 +104,7 @@ export const DrivePreview: React.FC<DrivePreviewProps> = ({ items }) => {
|
||||
className="z-10 cursor-pointer absolute right-5 top-5 w-20 h-20 text-white hover:text-black rounded-full p-1 bg-gray-500 hover:bg-white bg-opacity-25"
|
||||
onClick={() => {
|
||||
close();
|
||||
// small delay to allow the modal to close
|
||||
history.push(RouterServices.generateRouteFromState({ companyId: company, itemId: '' }));
|
||||
}}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user