diff --git a/tdrive/frontend/src/app/features/drive/api-client/api-client.ts b/tdrive/frontend/src/app/features/drive/api-client/api-client.ts index 4cda0676..4841952f 100644 --- a/tdrive/frontend/src/app/features/drive/api-client/api-client.ts +++ b/tdrive/frontend/src/app/features/drive/api-client/api-client.ts @@ -132,11 +132,12 @@ export class DriveApiClient { ); } - static async search(searchString: string, options?: BaseSearchOptions) { + static async search(searchString: string, view?: string,options?: BaseSearchOptions) { const companyId = options?.company_id ? options.company_id : Workspace.currentGroupId; const query = `/internal/services/documents/v1/companies/${companyId}/search`; const searchData = { search: searchString, + view: view }; const res = await Api.post(query, searchData); this.logger.debug( @@ -147,4 +148,18 @@ export class DriveApiClient { return res; } + + static async sharedWithMe(options?: BaseSearchOptions) { + const companyId = options?.company_id ? options.company_id : Workspace.currentGroupId; + const query = `/internal/services/documents/v1/companies/${companyId}/shared-with-me`; + const filterData = {}; + const res = await Api.post(query, filterData); + this.logger.debug( + `Drive shared with me by filter "${JSON.stringify(filterData)}". Found`, + res.entities.length, + 'drive item(s)', + ); + + return res; + } } diff --git a/tdrive/frontend/src/app/features/drive/hooks/use-drive-actions.tsx b/tdrive/frontend/src/app/features/drive/hooks/use-drive-actions.tsx index 87cb0dbc..4f13b603 100644 --- a/tdrive/frontend/src/app/features/drive/hooks/use-drive-actions.tsx +++ b/tdrive/frontend/src/app/features/drive/hooks/use-drive-actions.tsx @@ -4,7 +4,7 @@ import { useCallback } from 'react'; import { useRecoilCallback } from 'recoil'; import { DriveApiClient } from '../api-client/api-client'; import { DriveItemAtom, DriveItemChildrenAtom } from '../state/store'; -import { DriveItem, DriveItemVersion } from '../types'; +import { DriveItem, DriveItemDetails, DriveItemVersion } from '../types'; /** * Returns the children of a drive item @@ -18,20 +18,59 @@ export const useDriveActions = () => { ({ set, snapshot }) => async (parentId: string) => { if (parentId) { - try { - const details = await DriveApiClient.get(companyId, parentId); + if (parentId == "shared-with-me") { + const details = { + path: [ + { + id: "shared-with-me", + name: "Shared with me" + } + ], + item: { + id: "root", + parent_id: "", + company_id: "", + workspace_id: "", + name: "Shared with me", + size: 0, + description: "", + tags: [], + in_trash: false, + is_directory: true, + extension: "", + added: "", + last_modified: "", + last_version_cache: {}, + access_info: {}, + }, + versions: [], + children: [], + access: "manage", + websockets: [ + { + room: "/companies/aa7ffdd0-fadb-11ed-b891-510fe3501b2b/documents/item/root", + token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhYTU0Y2YyMC1mYWRiLTExZWQtYjg5MS01MTBmZTM1MDFiMmIiLCJuYW1lIjoiL2NvbXBhbmllcy9hYTdmZmRkMC1mYWRiLTExZWQtYjg5MS01MTBmZTM1MDFiMmIvZG9jdW1lbnRzL2l0ZW0vcm9vdCIsImlhdCI6MTY4ODk1MDk2OCwibmJmIjoxNjg2MjcyNTA4fQ.H6BFRcLG3Op32sqKi45Pf1s2YKcVbMxGZGPJal06l1g" + } + ] + }; set(DriveItemChildrenAtom(parentId), details.children); - set(DriveItemAtom(parentId), details); - for (const child of details.children) { - const currentValue = snapshot.getLoadable(DriveItemAtom(child.id)).contents; - if (!currentValue) { - //only update if not already in cache to avoid concurrent updates - set(DriveItemAtom(child.id), { item: child }); - } - } return details; - } catch (e) { - ToasterService.error('Unable to load your files.'); + } else { + try { + const details = await DriveApiClient.get(companyId, parentId); + set(DriveItemChildrenAtom(parentId), details.children); + set(DriveItemAtom(parentId), details); + for (const child of details.children) { + const currentValue = snapshot.getLoadable(DriveItemAtom(child.id)).contents; + if (!currentValue) { + //only update if not already in cache to avoid concurrent updates + set(DriveItemAtom(child.id), { item: child }); + } + } + return details; + } catch (e) { + ToasterService.error('Unable to load your files.'); + } } } }, diff --git a/tdrive/frontend/src/app/features/drive/hooks/use-shared-with-me-drive-items.tsx b/tdrive/frontend/src/app/features/drive/hooks/use-shared-with-me-drive-items.tsx new file mode 100644 index 00000000..5407a538 --- /dev/null +++ b/tdrive/frontend/src/app/features/drive/hooks/use-shared-with-me-drive-items.tsx @@ -0,0 +1,80 @@ +import { DriveApiClient } from '@features/drive/api-client/api-client'; +import { useGlobalEffect } from '@features/global/hooks/use-global-effect'; +import { LoadingState } from '@features/global/state/atoms/Loading'; +import { delayRequest } from '@features/global/utils/managedSearchRequest'; +import useRouterCompany from '@features/router/hooks/use-router-company'; +import _ from 'lodash'; +import { useRecoilState, useRecoilValue } from 'recoil'; +import { SharedWithMeDriveItemsResultsState } from '../state/shared-with-me-drive-items-result'; +import { SearchInputState } from '../../search/state/search-input'; + +export const useSharedWithMeDriveItemsLoading = () => { + return useRecoilValue(LoadingState('useSearchDriveItems')); +}; + +let currentQuery = ''; + +export const useSharedWithMeDriveItems = () => { + const companyId = useRouterCompany(); + const searchInput = useRecoilValue(SearchInputState); + const [loading, setLoading] = useRecoilState(LoadingState('useSearchDriveItems')); + + const [items, setItems] = useRecoilState(SharedWithMeDriveItemsResultsState(companyId)); + + const opt = _.omitBy( + { + limit: 25, + workspace_id: searchInput.workspaceId, + company_id: companyId, + channel_id: searchInput.channelId, + }, + _.isUndefined, + ); + + const refresh = async () => { + setLoading(true); + + const query = searchInput.query; + currentQuery = query; + + const response = await DriveApiClient.sharedWithMe(opt); + console.log("response is: ", response); + const results = response.entities || []; + + const update = { + results, + nextPage: '', + // nextPage: response.next_page_token, + }; + + if (currentQuery !== query) { + return; + } + setItems(update); + setLoading(false); + }; + + const loadMore = async () => { + //Not implemented + console.error('Not implemented'); + }; + + useGlobalEffect( + 'useSearchDriveItems', + () => { + (async () => { + setLoading(true); + if (searchInput.query) { + delayRequest('useSearchDriveItems', async () => { + await refresh(); + }); + } else { + refresh(); + } + })(); + }, + [searchInput.channelId, searchInput.workspaceId], + ); + + return { loading, driveItems: [...items.results], loadMore, refresh }; +}; diff --git a/tdrive/frontend/src/app/features/drive/state/shared-with-me-drive-items-result.tsx b/tdrive/frontend/src/app/features/drive/state/shared-with-me-drive-items-result.tsx new file mode 100644 index 00000000..f5e1ef4a --- /dev/null +++ b/tdrive/frontend/src/app/features/drive/state/shared-with-me-drive-items-result.tsx @@ -0,0 +1,22 @@ +import { DriveItem } from '@features/drive/types'; +import { atomFamily, selectorFamily } from 'recoil'; + +export type SharedWithMeDriveItemsResults = { + results: DriveItem[]; + nextPage: string | null; +}; + +export const SharedWithMeDriveItemsResultsState = atomFamily({ + key: 'SharedWithMeDriveItemsResultsState', + default: () => ({ results: [], nextPage: '' }), +}); + +export const SearchFilesResultsNumberSelector = selectorFamily({ + key: 'SharedWithMeDriveItemsResultsNumberSelector', + get: + (companyId: string) => + ({ get }) => { + const snapshot = get(SharedWithMeDriveItemsResultsState(companyId)); + return snapshot.results.length; + }, +}); diff --git a/tdrive/frontend/src/app/features/router/hooks/use-router-view.ts b/tdrive/frontend/src/app/features/router/hooks/use-router-view.ts new file mode 100644 index 00000000..06e6d5ce --- /dev/null +++ b/tdrive/frontend/src/app/features/router/hooks/use-router-view.ts @@ -0,0 +1,12 @@ +import { useRecoilValue, useSetRecoilState } from 'recoil'; +import RouterService from '@features/router/services/router-service'; +import { RouterState } from '@features/router/state/atoms/router'; +import { RouteViewSelector } from '@features/router/state/selectors/router-selector'; + +export default function useRouteView() { + const setClientState = useSetRecoilState(RouterState); + RouterService.setRecoilState = setClientState; + const viewId = useRecoilValue(RouteViewSelector); + + return viewId; +} diff --git a/tdrive/frontend/src/app/features/router/services/router-service.ts b/tdrive/frontend/src/app/features/router/services/router-service.ts index c4a5ff76..2e70c678 100644 --- a/tdrive/frontend/src/app/features/router/services/router-service.ts +++ b/tdrive/frontend/src/app/features/router/services/router-service.ts @@ -25,6 +25,7 @@ export type RouteType = { export type ClientStateType = { companyId?: string; + viewId?: string; workspaceId?: string; channelId?: string; messageId?: string; @@ -47,6 +48,7 @@ class RouterServices extends Observable { //List of client sub paths clientSubPathnames: Readonly = [ '/client/:companyId', + '/client/:companyId/v/:viewId', '/client/:companyId/w/:workspaceId', '/client/:companyId/w/:workspaceId/c/:channelId', '/client/:companyId/w/:workspaceId/c/:channelId/t/:threadId', @@ -61,6 +63,7 @@ class RouterServices extends Observable { pathnames: Readonly = { CLIENT: '/client', + SHARED_WITH_ME: '/client/:companyId/shared-with-me', SHARED: '/shared/:companyId/:appName/:documentId/t/:token', LOGIN: '/login', LOGOUT: '/logout', @@ -107,6 +110,15 @@ class RouterServices extends Observable { withErrorBoundary: true, }, }, + { + path: this.pathnames.SHARED_WITH_ME, + key: 'client_shared', + exact: false, + component: App, + options: { + withErrorBoundary: true, + }, + }, { path: this.pathnames.SHARED, key: 'shared', @@ -167,6 +179,7 @@ class RouterServices extends Observable { const reducedState: any = { companyId: match?.params?.companyId || '', + viewId: match?.params?.viewId || '', workspaceId: match?.params?.workspaceId || '', channelId: match?.params?.channelId || '', messageId: match?.params?.messageId || '', @@ -177,6 +190,7 @@ class RouterServices extends Observable { token: match?.params?.token || '', appName: match?.params?.appName || '', shared: !!this.match(this.pathnames.SHARED), + sharedWithMe: !!this.match(this.pathnames.SHARED_WITH_ME), }; const queryParameters = this.allowedQueryParameters[match?.path]; @@ -270,6 +284,8 @@ class RouterServices extends Observable { return ( `${this.pathnames.CLIENT}` + (state.companyId ? `/${state.companyId}` : '') + + (state.viewId ? `/v/${state.viewId}` : '') + + (state.sharedWithMe ? `/shared-with-me` : '') + (state.workspaceId ? `/w/${state.workspaceId}` : '') + (state.channelId ? `/c/${state.channelId}` : '') + (state.threadId ? `/t/${state.threadId}` : '') + diff --git a/tdrive/frontend/src/app/features/router/state/selectors/router-selector.ts b/tdrive/frontend/src/app/features/router/state/selectors/router-selector.ts index 886ae4ca..940b7eca 100644 --- a/tdrive/frontend/src/app/features/router/state/selectors/router-selector.ts +++ b/tdrive/frontend/src/app/features/router/state/selectors/router-selector.ts @@ -6,6 +6,11 @@ export const RouterCompanySelector = selector({ get: ({ get }) => get(RouterState)?.companyId || '', }); +export const RouteViewSelector = selector({ + key: 'RouterViewSelector', + get: ({ get }) => get(RouterState)?.viewId || '', +}); + export const RouterWorkspaceSelector = selector({ key: 'RouterWorkspaceSelector', get: ({ get }) => get(RouterState)?.workspaceId || '', diff --git a/tdrive/frontend/src/app/features/search/hooks/use-search-drive-items.tsx b/tdrive/frontend/src/app/features/search/hooks/use-search-drive-items.tsx index c03ebdaa..5c96b2fa 100644 --- a/tdrive/frontend/src/app/features/search/hooks/use-search-drive-items.tsx +++ b/tdrive/frontend/src/app/features/search/hooks/use-search-drive-items.tsx @@ -42,7 +42,7 @@ export const useSearchDriveItems = () => { const query = searchInput.query; currentQuery = query; - const response = await DriveApiClient.search(searchInput.query, opt); + const response = await DriveApiClient.search(searchInput.query, "", opt); let results = response.entities || []; if (isRecent) results = results.sort( diff --git a/tdrive/frontend/src/app/views/client/body/drive/browser.tsx b/tdrive/frontend/src/app/views/client/body/drive/browser.tsx index 40c1ea0f..a40803a5 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/browser.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/browser.tsx @@ -1,4 +1,4 @@ -import { ChevronDownIcon } from '@heroicons/react/outline'; +import { ChevronDownIcon, DotsHorizontalIcon } from '@heroicons/react/outline'; import { Button } from '@atoms/button/button'; import { Base, BaseSmall, Subtitle, Title } from '@atoms/text'; import Menu from '@components/menus/menu'; @@ -11,6 +11,7 @@ import { useDriveUpload } from '@features/drive/hooks/use-drive-upload'; import { DriveItemSelectedList } from '@features/drive/state/store'; import { formatBytes } from '@features/drive/utils'; import useRouterCompany from '@features/router/hooks/use-router-company'; +import useRouterView from '@features/router/hooks/use-router-view'; import _ from 'lodash'; import { memo, Suspense, useCallback, useEffect, useRef, useState } from 'react'; import { atomFamily, useRecoilState, useSetRecoilState } from 'recoil'; @@ -25,6 +26,9 @@ import { CreateModalAtom } from './modals/create'; import { PropertiesModal } from './modals/properties'; import { AccessModal } from './modals/update-access'; import { VersionsModal } from './modals/versions'; +import { SharedFilesTable } from './shared-files-table'; +import RouterServices from '@features/router/services/router-service'; +import useRouteState from 'app/features/router/hooks/use-route-state'; export const DriveCurrentFolderAtom = atomFamily< string, @@ -119,119 +123,129 @@ export default memo( .sort((a, b) => a.name.localeCompare(b.name)); const onBuildContextMenu = useOnBuildContextMenu(children, initialParentId); + const { viewId } = useRouteState(); return ( - { - const tree = await getFilesTree(event); - setCreationModalState({ parent_id: '', open: false }); - uploadTree(tree, { - companyId, - parentId, - }); - }} - > - - - - - - - }> - - + <> + {viewId == 'shared-with-me' ? ( + <> + + + ) : ( + { + const tree = await getFilesTree(event); + setCreationModalState({ parent_id: '', open: false }); + uploadTree(tree, { + companyId, + parentId, + }); + }} + > + + + + + + + }> + + +
+
+ +
-
-
- -
+ {access !== 'read' && ( + {formatBytes(item?.size || 0)} used in this folder + )} + onBuildContextMenu(details)}> + {' '} + + +
- - - -
+
+ {folders.length > 0 && ( + <> + Folders -
- {folders.length > 0 && ( - <> - Folders + {folders.map((child, index) => ( + { + return setParentId(child.id); + }} + checked={checked[child.id] || false} + onCheck={v => + setChecked(_.pickBy({ ...checked, [child.id]: v }, _.identity)) + } + onBuildContextMenu={() => onBuildContextMenu(details, child)} + /> + ))} +
+ + )} - {folders.map((child, index) => ( - Documents + + {documents.length === 0 && !loading && ( +
+ Nothing here. + {!inTrash && access != 'read' && ( + <> + + Drag and drop files to upload them or click on the 'Add document' button. + +
+ + + )} +
+ )} + + {documents.map((child, index) => ( + { - return setParentId(child.id); - }} checked={checked[child.id] || false} onCheck={v => setChecked(_.pickBy({ ...checked, [child.id]: v }, _.identity))} onBuildContextMenu={() => onBuildContextMenu(details, child)} /> ))} -
- - )} - - Documents - - {documents.length === 0 && !loading && ( -
- Nothing here. - {!inTrash && access != 'read' && ( - <> - - Drag and drop files to upload them or click on the 'Add document' button. - -
- - - )}
- )} - - {documents.map((child, index) => ( - setChecked(_.pickBy({ ...checked, [child.id]: v }, _.identity))} - onBuildContextMenu={() => onBuildContextMenu(details, child)} - /> - ))} -
-
- +
+ + )} + ); }, ); diff --git a/tdrive/frontend/src/app/views/client/body/drive/context-menu.tsx b/tdrive/frontend/src/app/views/client/body/drive/context-menu.tsx index dd0e7165..6d7ad3e1 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/context-menu.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/context-menu.tsx @@ -289,3 +289,64 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s ], ); }; + +export const onBuildFileTypeContextMenu = () => { + const menuItems = [ + { + type: 'menu', + text: 'PDF', + }, + { + type: 'menu', + text: 'DOC', + }, + { + type: 'menu', + text: 'PNG', + }, + ]; + return menuItems; +}; +export const onBuildPeopleContextMenu = () => { + const menuItems = [ + { + type: 'menu', + text: 'Dwho', + }, + ]; + return menuItems; +}; +export const onBuildDateContextMenu = () => { + const menuItems = [ + { + type: 'menu', + text: 'All', + }, + { + type: 'menu', + text: 'Today', + }, + { + type: 'menu', + text: 'Last week', + }, + { + type: 'menu', + text: 'Last month', + }, + { + type: 'menu', + text: 'Range', + }, + ]; + return menuItems; +}; +export const onBuildFileContextMenu = (id: string) => { + const menuItems = [ + { + type: 'menu', + text: 'Download' + }, + ]; + return menuItems; +}; diff --git a/tdrive/frontend/src/app/views/client/body/drive/shared-files-table.tsx b/tdrive/frontend/src/app/views/client/body/drive/shared-files-table.tsx new file mode 100644 index 00000000..f85bfcc1 --- /dev/null +++ b/tdrive/frontend/src/app/views/client/body/drive/shared-files-table.tsx @@ -0,0 +1,139 @@ +import { useState } from 'react'; +import { ChevronDownIcon, ChevronUpIcon, DotsHorizontalIcon } from '@heroicons/react/outline'; +import { Button } from '@atoms/button/button'; +import { Base, BaseSmall, Title } from '@atoms/text'; +import Menu from '@components/menus/menu'; +import { + onBuildFileTypeContextMenu, + onBuildPeopleContextMenu, + onBuildDateContextMenu, + onBuildFileContextMenu, +} from './context-menu'; +import { useSharedWithMeDriveItems } from '@features/drive/hooks/use-shared-with-me-drive-items'; + +export const SharedFilesTable = () => { + const { driveItems, loading } = useSharedWithMeDriveItems(); + const [sortBy, setSortBy] = useState('name'); + const [sortOrder, setSortOrder] = useState('asc'); + + const handleSort = (column: string) => { + if (sortBy === column) { + setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); + } else { + setSortBy(column); + setSortOrder('asc'); + } + }; + + const renderSortIcon = (column: string) => { + if (sortBy === column) { + if (sortOrder === 'asc') { + return ; + } else { + return ; + } + } + return null; + }; + return ( + <> + Shared with me + {/* Filters */} +
+
+ onBuildFileTypeContextMenu()}> + + +
+ +
+ onBuildPeopleContextMenu()}> + + +
+ +
+ onBuildDateContextMenu()}> + + +
+
+ Documents: +
+ + + + + + + + + + + {!loading && + driveItems + .sort((a: any, b: any) => { + // Perform sorting based on the selected column and order + console.log("a is: ", a); + console.log("b is: ", b); + if (sortBy === 'name') { + return sortOrder === 'asc' + ? a.name.localeCompare(b.name) + : b.name.localeCompare(a.name); + } + return 0; // No sorting by default + }) + .map((file: any, index: any) => ( + + + + + + + ))} + +
+ handleSort('name')}> + Name {renderSortIcon('name')} + + + handleSort('name')}> + Shared By {renderSortIcon('name')} + + + handleSort('name')}> + Shared Date {renderSortIcon('name')} + + + Edit +
+ {file.name} + Dwho2023-05-05 + + +
+
+ + ); +}; diff --git a/tdrive/frontend/src/app/views/client/side-bar/index.tsx b/tdrive/frontend/src/app/views/client/side-bar/index.tsx index 963f220f..58df51f8 100644 --- a/tdrive/frontend/src/app/views/client/side-bar/index.tsx +++ b/tdrive/frontend/src/app/views/client/side-bar/index.tsx @@ -7,7 +7,10 @@ import { ShareIcon, TrashIcon, UserIcon, + UserGroupIcon, } from '@heroicons/react/outline'; +import useRouterCompany from '@features/router/hooks/use-router-company'; +import useRouterWorkspace from '@features/router/hooks/use-router-workspace'; import { useCurrentUser } from 'app/features/users/hooks/use-current-user'; import { useRecoilState } from 'recoil'; import { Title } from '../../../atoms/text'; @@ -17,8 +20,14 @@ import Account from '../common/account'; import AppGrid from '../common/app-grid'; import DiskUsage from '../common/disk-usage'; import Actions from './actions'; +import { useHistory, useLocation } from 'react-router-dom'; +import RouterServices from '@features/router/services/router-service'; export default () => { + const history = useHistory(); + const location = useLocation(); + const company = useRouterCompany(); + const workspace = useRouterWorkspace(); const [parentId, setParentId] = useRecoilState( DriveCurrentFolderAtom({ initialFolderId: 'root' }), ); @@ -30,6 +39,7 @@ export default () => { let folderType = 'home'; if ((path || [])[0]?.id === 'user_' + user?.id) folderType = 'personal'; if (inTrash) folderType = 'trash'; + const { viewId } = RouterServices.getStateFromRoute(); return (
@@ -50,21 +60,29 @@ export default () => {
Drive + {false && ( <>