✨ Sort files chronologically + infinite scroll (#535)
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
<svg width="13" height="10" viewBox="0 0 13 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.16 8.385C5.16 8.21393 5.22796 8.04988 5.34892 7.92892C5.46988 7.80795 5.63393 7.74 5.805 7.74H7.095C7.26606 7.74 7.43012 7.80795 7.55108 7.92892C7.67205 8.04988 7.74 8.21393 7.74 8.385C7.74 8.55606 7.67205 8.72012 7.55108 8.84108C7.43012 8.96204 7.26606 9.03 7.095 9.03H5.805C5.63393 9.03 5.46988 8.96204 5.34892 8.84108C5.22796 8.72012 5.16 8.55606 5.16 8.385ZM2.58 4.515C2.58 4.34394 2.64796 4.17988 2.76892 4.05892C2.88988 3.93795 3.05394 3.87 3.225 3.87H9.675C9.84606 3.87 10.0101 3.93795 10.1311 4.05892C10.252 4.17988 10.32 4.34394 10.32 4.515C10.32 4.68606 10.252 4.85012 10.1311 4.97108C10.0101 5.09205 9.84606 5.16 9.675 5.16H3.225C3.05394 5.16 2.88988 5.09205 2.76892 4.97108C2.64796 4.85012 2.58 4.68606 2.58 4.515ZM0 0.645C0 0.473935 0.0679553 0.309877 0.188916 0.188916C0.309877 0.0679553 0.473935 0 0.645 0H12.255C12.4261 0 12.5901 0.0679553 12.7111 0.188916C12.832 0.309877 12.9 0.473935 12.9 0.645C12.9 0.816065 12.832 0.980123 12.7111 1.10108C12.5901 1.22204 12.4261 1.29 12.255 1.29H0.645C0.473935 1.29 0.309877 1.22204 0.188916 1.10108C0.0679553 0.980123 0 0.816065 0 0.645Z" fill="#6A6A6A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -22,6 +22,7 @@ import {
|
||||
StatusCheckIcon,
|
||||
UsersIcon,
|
||||
CheckOutlineIcon,
|
||||
SortIcon,
|
||||
} from '@atoms/icons-agnostic/index';
|
||||
|
||||
export default {
|
||||
@@ -69,6 +70,7 @@ const Template: ComponentStory<any> = () => {
|
||||
<Icon icon={<StatusCheckDoubleIcon />} title="StatusCheckDouble" />
|
||||
<Icon icon={<StatusCheckIcon />} title="StatusCheck" />
|
||||
<Icon icon={<UsersIcon />} title="Users" />
|
||||
<Icon icon={<SortIcon />} title="Sort" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -20,6 +20,7 @@ import { ReactComponent as X } from './assets/x.svg';
|
||||
import { ReactComponent as StatusCheckDouble } from './assets/status-check-double.svg';
|
||||
import { ReactComponent as StatusCheck } from './assets/status-check.svg';
|
||||
import { ReactComponent as Users } from './assets/users.svg';
|
||||
import { ReactComponent as Sort } from './assets/sort.svg';
|
||||
|
||||
export const CopyIcon = (props: ComponentProps<'svg'>) => <CopySvg {...props} />;
|
||||
export const DeleteIcon = (props: ComponentProps<'svg'>) => <DeleteSvg {...props} />;
|
||||
@@ -43,3 +44,4 @@ export const StatusCheckDoubleIcon = (props: ComponentProps<'svg'>) => (
|
||||
);
|
||||
export const StatusCheckIcon = (props: ComponentProps<'svg'>) => <StatusCheck {...props} />;
|
||||
export const UsersIcon = (props: ComponentProps<'svg'>) => <Users {...props} />;
|
||||
export const SortIcon = (props: ComponentProps<'svg'>) => <Sort {...props} />;
|
||||
|
||||
@@ -26,6 +26,7 @@ export default class Menu extends React.Component {
|
||||
super(props);
|
||||
this.state = {
|
||||
isMenuOpen: false,
|
||||
by: ''
|
||||
};
|
||||
this.open = false; // Added initialization for open state
|
||||
this.container = React.createRef(); // Ref for container div
|
||||
@@ -94,6 +95,10 @@ export default class Menu extends React.Component {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.props.sortData !== nextProps.sortData) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -118,7 +123,10 @@ export default class Menu extends React.Component {
|
||||
}}
|
||||
className={this.props.className}
|
||||
>
|
||||
<>
|
||||
{this.props.children}
|
||||
</>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Api from '../../global/framework/api-service';
|
||||
import { BrowseFilter, DriveItem, DriveItemDetails, DriveItemVersion } from '../types';
|
||||
import { BrowseFilter, BrowsePaginate, BrowseQuery, BrowseSort, DriveItem, DriveItemDetails, DriveItemVersion } from '../types';
|
||||
import Workspace from '@deprecated/workspaces/workspaces';
|
||||
import Logger from 'features/global/framework/logger-service';
|
||||
import { JWTDataType } from 'app/features/auth/jwt-storage-service';
|
||||
@@ -70,10 +70,14 @@ export class DriveApiClient {
|
||||
);
|
||||
}
|
||||
|
||||
static async browse(companyId: string, id: string | 'trash' | '', filter: BrowseFilter) {
|
||||
return await Api.post<BrowseFilter, DriveItemDetails>(
|
||||
static async browse(companyId: string, id: string | 'trash' | '', filter: BrowseFilter, sort: BrowseSort, paginate: BrowsePaginate) {
|
||||
return await Api.post<BrowseQuery, DriveItemDetails>(
|
||||
`/internal/services/documents/v1/companies/${companyId}/browse/${id}${appendTdriveToken()}`,
|
||||
filter,
|
||||
{
|
||||
filter,
|
||||
sort,
|
||||
paginate
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -134,11 +138,11 @@ export class DriveApiClient {
|
||||
return Api.route(`/internal/services/documents/v1/companies/${companyId}/item/${id}/download`);
|
||||
}
|
||||
|
||||
static async getDownloadZipUrl(companyId: string, ids: string[]) {
|
||||
static async getDownloadZipUrl(companyId: string, ids: string[], isDirectory?: boolean) {
|
||||
// const { token } = await DriveApiClient.getDownloadToken(companyId, ids);
|
||||
return Api.route(
|
||||
`/internal/services/documents/v1/companies/${companyId}/item/download/zip` +
|
||||
`?items=${ids.join(',')}`
|
||||
`?items=${ids.join(',')}&is_directory=${isDirectory}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ToasterService } from '@features/global/services/toaster-service';
|
||||
import useRouterCompany from '@features/router/hooks/use-router-company';
|
||||
import { useCallback } from 'react';
|
||||
import { useRecoilValue, useRecoilCallback } from 'recoil';
|
||||
import { useRecoilValue, useRecoilCallback, useRecoilState } from 'recoil';
|
||||
import { DriveApiClient } from '../api-client/api-client';
|
||||
import { DriveItemAtom, DriveItemChildrenAtom } from '../state/store';
|
||||
import { DriveItemAtom, DriveItemChildrenAtom, DriveItemPagination, DriveItemSort } from '../state/store';
|
||||
import { BrowseFilter, DriveItem, DriveItemVersion } from '../types';
|
||||
import { SharedWithMeFilterState } from '../state/shared-with-me-filter';
|
||||
import Languages from 'features/global/services/languages-service';
|
||||
@@ -16,18 +16,26 @@ import { useUserQuota } from 'features/users/hooks/use-user-quota';
|
||||
export const useDriveActions = () => {
|
||||
const companyId = useRouterCompany();
|
||||
const sharedFilter = useRecoilValue(SharedWithMeFilterState);
|
||||
const sortItem = useRecoilValue(DriveItemSort);
|
||||
const [paginateItem, _] = useRecoilState(DriveItemPagination);
|
||||
const { getQuota } = useUserQuota();
|
||||
|
||||
const refresh = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
async (parentId: string) => {
|
||||
async (parentId: string, resetPagination?: boolean) => {
|
||||
if (parentId) {
|
||||
const filter: BrowseFilter = {
|
||||
company_id: companyId,
|
||||
mime_type: sharedFilter.mimeType.value,
|
||||
};
|
||||
let pagination = await snapshot.getPromise(DriveItemPagination);
|
||||
|
||||
if (resetPagination) {
|
||||
pagination = { page: 0, limit: pagination.limit };
|
||||
set(DriveItemPagination, pagination);
|
||||
}
|
||||
try {
|
||||
const details = await DriveApiClient.browse(companyId, parentId, filter);
|
||||
const details = await DriveApiClient.browse(companyId, parentId, filter, sortItem, pagination);
|
||||
set(DriveItemChildrenAtom(parentId), details.children);
|
||||
set(DriveItemAtom(parentId), details);
|
||||
for (const child of details.children) {
|
||||
@@ -40,10 +48,12 @@ export const useDriveActions = () => {
|
||||
return details;
|
||||
} catch (e) {
|
||||
ToasterService.error(Languages.t('hooks.use-drive-actions.unable_load_file'));
|
||||
} finally {
|
||||
set(DriveItemPagination, { page: pagination.limit, limit: pagination.limit });
|
||||
}
|
||||
}
|
||||
},
|
||||
[companyId],
|
||||
[companyId, sortItem],
|
||||
);
|
||||
|
||||
const create = useCallback(
|
||||
@@ -54,7 +64,7 @@ export const useDriveActions = () => {
|
||||
try {
|
||||
const driveFile = await DriveApiClient.create(companyId, { item, version });
|
||||
|
||||
await refresh(driveFile.parent_id);
|
||||
await refresh(driveFile.parent_id, true);
|
||||
await getQuota();
|
||||
|
||||
return driveFile;
|
||||
@@ -89,9 +99,9 @@ export const useDriveActions = () => {
|
||||
);
|
||||
|
||||
const downloadZip = useCallback(
|
||||
async (ids: string[]) => {
|
||||
async (ids: string[], isDirectory?: boolean) => {
|
||||
try {
|
||||
const url = await DriveApiClient.getDownloadZipUrl(companyId, ids);
|
||||
const url = await DriveApiClient.getDownloadZipUrl(companyId, ids, isDirectory);
|
||||
(window as any).open(url, '_blank').focus();
|
||||
} catch (e) {
|
||||
ToasterService.error(Languages.t('hooks.use-drive-actions.unable_download_file'));
|
||||
@@ -129,9 +139,9 @@ export const useDriveActions = () => {
|
||||
async (update: Partial<DriveItem>, id: string, parentId: string) => {
|
||||
try {
|
||||
await DriveApiClient.update(companyId, id, update);
|
||||
await refresh(id || '');
|
||||
await refresh(parentId || '');
|
||||
if (update?.parent_id !== parentId) await refresh(update?.parent_id || '');
|
||||
await refresh(id || '', true);
|
||||
await refresh(parentId || '', true);
|
||||
if (update?.parent_id !== parentId) await refresh(update?.parent_id || '', true);
|
||||
} catch (e) {
|
||||
ToasterService.error(Languages.t('hooks.use-drive-actions.unable_update_file'));
|
||||
}
|
||||
@@ -156,5 +166,25 @@ export const useDriveActions = () => {
|
||||
[refresh],
|
||||
);
|
||||
|
||||
return { create, refresh, download, downloadZip, remove, restore, update, updateLevel };
|
||||
const nextPage = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
async (parentId: string) => {
|
||||
const filter: BrowseFilter = {
|
||||
company_id: companyId,
|
||||
mime_type: sharedFilter.mimeType.value,
|
||||
};
|
||||
const pagination = await snapshot.getPromise(DriveItemPagination);
|
||||
const details = await DriveApiClient.browse(
|
||||
companyId,
|
||||
parentId,
|
||||
filter,
|
||||
sortItem,
|
||||
pagination
|
||||
);
|
||||
return details;
|
||||
},
|
||||
[paginateItem, refresh],
|
||||
);
|
||||
|
||||
return { create, refresh, download, downloadZip, remove, restore, update, updateLevel, nextPage };
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ToasterService } from '@features/global/services/toaster-service';
|
||||
import { LoadingStateInitTrue } from '@features/global/state/atoms/Loading';
|
||||
import useRouterCompany from '@features/router/hooks/use-router-company';
|
||||
import { useCallback } from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { DriveItemAtom, DriveItemChildrenAtom } from '../state/store';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { DriveItemAtom, DriveItemChildrenAtom, DriveItemPagination } from '../state/store';
|
||||
import { DriveItem } from '../types';
|
||||
import { useDriveActions } from './use-drive-actions';
|
||||
import { useDriveUpload } from './use-drive-upload';
|
||||
@@ -17,8 +17,10 @@ import short from 'short-uuid';
|
||||
export const useDriveItem = (id: string) => {
|
||||
const companyId = useRouterCompany();
|
||||
const item = useRecoilValue(DriveItemAtom(id));
|
||||
const children = useRecoilValue(DriveItemChildrenAtom(id));
|
||||
// const children = useRecoilValue(DriveItemChildrenAtom(id));
|
||||
const [children, setChildren] = useRecoilState(DriveItemChildrenAtom(id));
|
||||
const [loading, setLoading] = useRecoilState(LoadingStateInitTrue('useDriveItem-' + id));
|
||||
const [_, setPaginateItem] = useRecoilState(DriveItemPagination);
|
||||
const {
|
||||
refresh: refreshItem,
|
||||
create,
|
||||
@@ -26,19 +28,20 @@ export const useDriveItem = (id: string) => {
|
||||
updateLevel: _updateLevel,
|
||||
remove: _remove,
|
||||
restore: _restore,
|
||||
nextPage,
|
||||
} = useDriveActions();
|
||||
const { uploadVersion: _uploadVersion } = useDriveUpload();
|
||||
|
||||
const refresh = useCallback(
|
||||
async (parentId: string) => {
|
||||
async (parentId: string, resetPagination?: boolean) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await refreshItem(parentId);
|
||||
setPaginateItem(prev => ({ ...prev, page: 0 }));
|
||||
await refreshItem(parentId, resetPagination);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[setLoading, refreshItem],
|
||||
[id, setLoading, refreshItem],
|
||||
);
|
||||
|
||||
const remove = useCallback(async () => {
|
||||
@@ -100,6 +103,51 @@ export const useDriveItem = (id: string) => {
|
||||
[companyId, id, setLoading, refresh, item?.item?.parent_id],
|
||||
);
|
||||
|
||||
const loadNextPage = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
async (id: string) => {
|
||||
// get current pagination state
|
||||
const pagination = await snapshot.getPromise(DriveItemPagination);
|
||||
|
||||
// if end is true, do not load more
|
||||
if (pagination.lastPage === true) return;
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const details = await nextPage(id);
|
||||
if (details.children.length === 0) {
|
||||
set(DriveItemPagination, prev => ({
|
||||
...prev,
|
||||
lastPage: true,
|
||||
}));
|
||||
}
|
||||
// set children and remove duplicates
|
||||
setChildren(prev => [
|
||||
...prev,
|
||||
...details.children.filter(
|
||||
(item, index, self) =>
|
||||
index === self.findIndex(t => t.id === item.id),
|
||||
),
|
||||
]);
|
||||
} catch (e) {
|
||||
// set pagination end to true
|
||||
set(DriveItemPagination, prev => ({
|
||||
...prev,
|
||||
lastPage: true,
|
||||
}));
|
||||
console.log('error loading next page: ', e);
|
||||
ToasterService.error('Unable to load more items.');
|
||||
} finally {
|
||||
set(DriveItemPagination, prev => ({
|
||||
...prev,
|
||||
page: (prev.page + prev.limit),
|
||||
}));
|
||||
}
|
||||
setLoading(false);
|
||||
},
|
||||
[id, nextPage],
|
||||
);
|
||||
|
||||
const inTrash =
|
||||
id.includes('trash') ||
|
||||
item?.path?.some(i => i?.parent_id?.includes('trash')) ||
|
||||
@@ -123,6 +171,7 @@ export const useDriveItem = (id: string) => {
|
||||
updateLevel,
|
||||
remove,
|
||||
refresh,
|
||||
loadNextPage,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -29,11 +29,17 @@ export const useDrivePreviewModal = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const openWithId: (id: string) => void = (id: string) => {
|
||||
DriveApiClient.get(company, id).then((item) => {
|
||||
open(item?.item);
|
||||
});
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
setStatus({ item: null, loading: true });
|
||||
}
|
||||
|
||||
return { open, close, isOpen: !!status.item };
|
||||
return { open, close, isOpen: !!status.item, openWithId };
|
||||
};
|
||||
|
||||
export const useDrivePreview = () => {
|
||||
|
||||
@@ -50,7 +50,7 @@ export const useDriveUpload = () => {
|
||||
// Create all directories
|
||||
logger.debug("Start creating directories ...");
|
||||
const filesPerParentId = await FileUploadService.createDirectories(tree.tree, context);
|
||||
await refresh(context.parentId);
|
||||
await refresh(context.parentId, true);
|
||||
logger.debug("All directories created");
|
||||
|
||||
// Upload files into directories
|
||||
@@ -90,7 +90,7 @@ export const useDriveUpload = () => {
|
||||
},
|
||||
});
|
||||
}
|
||||
await refresh(context.parentId);
|
||||
await refresh(context.parentId, true);
|
||||
};
|
||||
|
||||
const uploadFromUrl = (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { atomFamily, atom } from 'recoil';
|
||||
import { DriveItem, DriveItemDetails } from '../types';
|
||||
import { BrowsePaginate, BrowseSort, DriveItem, DriveItemDetails } from '../types';
|
||||
|
||||
export const DriveItemChildrenAtom = atomFamily<DriveItem[], string>({
|
||||
key: 'DriveItemChildrenAtom',
|
||||
@@ -15,3 +15,20 @@ export const DriveItemSelectedList = atom<{[key: string]: boolean }>({
|
||||
key: 'DriveItemSelectedList',
|
||||
default: {}
|
||||
});
|
||||
|
||||
export const DriveItemSort = atom<BrowseSort>({
|
||||
key: 'DriveItemSort',
|
||||
default: {
|
||||
by: 'date',
|
||||
order: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
export const DriveItemPagination = atom<BrowsePaginate>({
|
||||
key: 'DriveItemPagination',
|
||||
default: {
|
||||
page: 0,
|
||||
limit: 15,
|
||||
lastPage: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,8 +1,25 @@
|
||||
export type BrowseQuery = {
|
||||
filter: BrowseFilter;
|
||||
sort: BrowseSort;
|
||||
paginate: BrowsePaginate;
|
||||
}
|
||||
|
||||
export type BrowseFilter = {
|
||||
company_id: string;
|
||||
mime_type?: string;
|
||||
}
|
||||
|
||||
export type BrowseSort = {
|
||||
by: string;
|
||||
order: string;
|
||||
}
|
||||
|
||||
export type BrowsePaginate = {
|
||||
page: number;
|
||||
limit: number;
|
||||
lastPage?: boolean;
|
||||
};
|
||||
|
||||
export type DriveItemDetails = {
|
||||
item: DriveItem;
|
||||
versions: DriveItemVersion[];
|
||||
|
||||
@@ -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