Sort files chronologically + infinite scroll (#535)

This commit is contained in:
Montassar Ghanmy
2024-07-15 22:56:34 +01:00
committed by GitHub
parent 585da88b0d
commit e76da87916
25 changed files with 719 additions and 153 deletions
@@ -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: '' }));
}}
/>