diff options
Diffstat (limited to 'apps/files_sharing/src/files_views')
-rw-r--r-- | apps/files_sharing/src/files_views/publicFileDrop.ts | 60 | ||||
-rw-r--r-- | apps/files_sharing/src/files_views/publicFileShare.ts | 66 | ||||
-rw-r--r-- | apps/files_sharing/src/files_views/publicShare.ts | 28 | ||||
-rw-r--r-- | apps/files_sharing/src/files_views/shares.spec.ts | 132 | ||||
-rw-r--r-- | apps/files_sharing/src/files_views/shares.ts | 156 |
5 files changed, 442 insertions, 0 deletions
diff --git a/apps/files_sharing/src/files_views/publicFileDrop.ts b/apps/files_sharing/src/files_views/publicFileDrop.ts new file mode 100644 index 00000000000..65756e83c74 --- /dev/null +++ b/apps/files_sharing/src/files_views/publicFileDrop.ts @@ -0,0 +1,60 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import type { VueConstructor } from 'vue' + +import { Folder, Permission, View, getNavigation } from '@nextcloud/files' +import { defaultRemoteURL, defaultRootPath } from '@nextcloud/files/dav' +import { loadState } from '@nextcloud/initial-state' +import { translate as t } from '@nextcloud/l10n' +import svgCloudUpload from '@mdi/svg/svg/cloud-upload.svg?raw' +import Vue from 'vue' + +export default () => { + const foldername = loadState<string>('files_sharing', 'filename') + + let FilesViewFileDropEmptyContent: VueConstructor + let fileDropEmptyContentInstance: Vue + + const view = new View({ + id: 'public-file-drop', + name: t('files_sharing', 'File drop'), + caption: t('files_sharing', 'Upload files to {foldername}', { foldername }), + icon: svgCloudUpload, + order: 1, + + emptyView: async (div: HTMLDivElement) => { + if (FilesViewFileDropEmptyContent === undefined) { + const { default: component } = await import('../views/FilesViewFileDropEmptyContent.vue') + FilesViewFileDropEmptyContent = Vue.extend(component) + } + if (fileDropEmptyContentInstance) { + fileDropEmptyContentInstance.$destroy() + } + fileDropEmptyContentInstance = new FilesViewFileDropEmptyContent({ + propsData: { + foldername, + }, + }) + fileDropEmptyContentInstance.$mount(div) + }, + + getContents: async () => { + return { + contents: [], + // Fake a writeonly folder as root + folder: new Folder({ + id: 0, + source: `${defaultRemoteURL}${defaultRootPath}`, + root: defaultRootPath, + owner: null, + permissions: Permission.CREATE, + }), + } + }, + }) + + const Navigation = getNavigation() + Navigation.register(view) +} diff --git a/apps/files_sharing/src/files_views/publicFileShare.ts b/apps/files_sharing/src/files_views/publicFileShare.ts new file mode 100644 index 00000000000..caa7f862e57 --- /dev/null +++ b/apps/files_sharing/src/files_views/publicFileShare.ts @@ -0,0 +1,66 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import type { FileStat, ResponseDataDetailed } from 'webdav' +import { Folder, Permission, View, davGetDefaultPropfind, davRemoteURL, davResultToNode, davRootPath, getNavigation } from '@nextcloud/files' +import { translate as t } from '@nextcloud/l10n' +import { CancelablePromise } from 'cancelable-promise' +import LinkSvg from '@mdi/svg/svg/link.svg?raw' + +import { client } from '../../../files/src/services/WebdavClient' +import logger from '../services/logger' + +export default () => { + const view = new View({ + id: 'public-file-share', + name: t('files_sharing', 'Public file share'), + caption: t('files_sharing', 'Publicly shared file.'), + + emptyTitle: t('files_sharing', 'No file'), + emptyCaption: t('files_sharing', 'The file shared with you will show up here'), + + icon: LinkSvg, + order: 1, + + getContents: () => { + return new CancelablePromise(async (resolve, reject, onCancel) => { + const abort = new AbortController() + onCancel(() => abort.abort()) + try { + const node = await client.stat( + davRootPath, + { + data: davGetDefaultPropfind(), + details: true, + signal: abort.signal, + }, + ) as ResponseDataDetailed<FileStat> + + resolve({ + // We only have one file as the content + contents: [davResultToNode(node.data)], + // Fake a readonly folder as root + folder: new Folder({ + id: 0, + source: `${davRemoteURL}${davRootPath}`, + root: davRootPath, + owner: null, + permissions: Permission.READ, + attributes: { + // Ensure the share note is set on the root + note: node.data.props?.note, + }, + }), + }) + } catch (e) { + logger.error(e as Error) + reject(e as Error) + } + }) + }, + }) + + const Navigation = getNavigation() + Navigation.register(view) +} diff --git a/apps/files_sharing/src/files_views/publicShare.ts b/apps/files_sharing/src/files_views/publicShare.ts new file mode 100644 index 00000000000..4f5526bc829 --- /dev/null +++ b/apps/files_sharing/src/files_views/publicShare.ts @@ -0,0 +1,28 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { translate as t } from '@nextcloud/l10n' +import { View, getNavigation } from '@nextcloud/files' +import LinkSvg from '@mdi/svg/svg/link.svg?raw' + +import { getContents } from '../../../files/src/services/Files' + +export default () => { + const view = new View({ + id: 'public-share', + name: t('files_sharing', 'Public share'), + caption: t('files_sharing', 'Publicly shared files.'), + + emptyTitle: t('files_sharing', 'No files'), + emptyCaption: t('files_sharing', 'Files and folders shared with you will show up here'), + + icon: LinkSvg, + order: 1, + + getContents, + }) + + const Navigation = getNavigation() + Navigation.register(view) +} diff --git a/apps/files_sharing/src/files_views/shares.spec.ts b/apps/files_sharing/src/files_views/shares.spec.ts new file mode 100644 index 00000000000..7e5b59e0ad9 --- /dev/null +++ b/apps/files_sharing/src/files_views/shares.spec.ts @@ -0,0 +1,132 @@ +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +/* eslint-disable n/no-extraneous-import */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { OCSResponse } from '@nextcloud/typings/ocs' + +import { beforeEach, describe, expect, test, vi } from 'vitest' +import { Folder, Navigation, View, getNavigation } from '@nextcloud/files' +import * as ncInitialState from '@nextcloud/initial-state' +import axios from '@nextcloud/axios' + +import '../main' +import registerSharingViews from './shares' + +declare global { + interface Window { + _nc_navigation?: Navigation + } +} + +describe('Sharing views definition', () => { + let Navigation + beforeEach(() => { + delete window._nc_navigation + Navigation = getNavigation() + expect(window._nc_navigation).toBeDefined() + }) + + test('Default values', () => { + vi.spyOn(Navigation, 'register') + + expect(Navigation.views.length).toBe(0) + + registerSharingViews() + const shareOverviewView = Navigation.views.find(view => view.id === 'shareoverview') as View + const sharesChildViews = Navigation.views.filter(view => view.parent === 'shareoverview') as View[] + + expect(Navigation.register).toHaveBeenCalledTimes(7) + + // one main view and no children + expect(Navigation.views.length).toBe(7) + expect(shareOverviewView).toBeDefined() + expect(sharesChildViews.length).toBe(6) + + expect(shareOverviewView?.id).toBe('shareoverview') + expect(shareOverviewView?.name).toBe('Shares') + expect(shareOverviewView?.caption).toBe('Overview of shared files.') + expect(shareOverviewView?.icon).toMatch(/<svg.+<\/svg>/i) + expect(shareOverviewView?.order).toBe(20) + expect(shareOverviewView?.columns).toStrictEqual([]) + expect(shareOverviewView?.getContents).toBeDefined() + + const dataProvider = [ + { id: 'sharingin', name: 'Shared with you' }, + { id: 'sharingout', name: 'Shared with others' }, + { id: 'sharinglinks', name: 'Shared by link' }, + { id: 'filerequest', name: 'File requests' }, + { id: 'deletedshares', name: 'Deleted shares' }, + { id: 'pendingshares', name: 'Pending shares' }, + ] + + sharesChildViews.forEach((view, index) => { + expect(view?.id).toBe(dataProvider[index].id) + expect(view?.parent).toBe('shareoverview') + expect(view?.name).toBe(dataProvider[index].name) + expect(view?.caption).toBeDefined() + expect(view?.emptyTitle).toBeDefined() + expect(view?.emptyCaption).toBeDefined() + expect(view?.icon).match(/<svg.+<\/svg>/) + expect(view?.order).toBe(index + 1) + expect(view?.columns).toStrictEqual([]) + expect(view?.getContents).toBeDefined() + }) + }) + + test('Shared with others view is not registered if user has no storage quota', () => { + vi.spyOn(Navigation, 'register') + const spy = vi.spyOn(ncInitialState, 'loadState').mockImplementationOnce(() => ({ quota: 0 })) + + expect(Navigation.views.length).toBe(0) + registerSharingViews() + expect(Navigation.register).toHaveBeenCalledTimes(6) + expect(Navigation.views.length).toBe(6) + + const shareOverviewView = Navigation.views.find(view => view.id === 'shareoverview') as View + const sharesChildViews = Navigation.views.filter(view => view.parent === 'shareoverview') as View[] + expect(shareOverviewView).toBeDefined() + expect(sharesChildViews.length).toBe(5) + + expect(spy).toHaveBeenCalled() + expect(spy).toHaveBeenCalledWith('files', 'storageStats', { quota: -1 }) + + const sharedWithOthersView = Navigation.views.find(view => view.id === 'sharingout') + expect(sharedWithOthersView).toBeUndefined() + }) +}) + +describe('Sharing views contents', () => { + let Navigation + beforeEach(() => { + delete window._nc_navigation + Navigation = getNavigation() + expect(window._nc_navigation).toBeDefined() + }) + + test('Sharing overview get contents', async () => { + vi.spyOn(axios, 'get').mockImplementation(async (): Promise<any> => { + return { + data: { + ocs: { + meta: { + status: 'ok', + statuscode: 200, + message: 'OK', + }, + data: [], + }, + } as OCSResponse<any>, + } + }) + + registerSharingViews() + expect(Navigation.views.length).toBe(7) + Navigation.views.forEach(async (view: View) => { + const content = await view.getContents('/') + expect(content.contents).toStrictEqual([]) + expect(content.folder).toBeInstanceOf(Folder) + }) + }) +}) diff --git a/apps/files_sharing/src/files_views/shares.ts b/apps/files_sharing/src/files_views/shares.ts new file mode 100644 index 00000000000..fd5e908638c --- /dev/null +++ b/apps/files_sharing/src/files_views/shares.ts @@ -0,0 +1,156 @@ +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { translate as t } from '@nextcloud/l10n' +import { View, getNavigation } from '@nextcloud/files' +import { ShareType } from '@nextcloud/sharing' +import AccountClockSvg from '@mdi/svg/svg/account-clock.svg?raw' +import AccountGroupSvg from '@mdi/svg/svg/account-group-outline.svg?raw' +import AccountPlusSvg from '@mdi/svg/svg/account-plus-outline.svg?raw' +import AccountSvg from '@mdi/svg/svg/account.svg?raw' +import DeleteSvg from '@mdi/svg/svg/delete.svg?raw' +import FileUploadSvg from '@mdi/svg/svg/file-upload-outline.svg?raw' +import LinkSvg from '@mdi/svg/svg/link.svg?raw' + +import { getContents, isFileRequest } from '../services/SharingService' +import { loadState } from '@nextcloud/initial-state' + +export const sharesViewId = 'shareoverview' +export const sharedWithYouViewId = 'sharingin' +export const sharedWithOthersViewId = 'sharingout' +export const sharingByLinksViewId = 'sharinglinks' +export const deletedSharesViewId = 'deletedshares' +export const pendingSharesViewId = 'pendingshares' +export const fileRequestViewId = 'filerequest' + +export default () => { + const Navigation = getNavigation() + Navigation.register(new View({ + id: sharesViewId, + name: t('files_sharing', 'Shares'), + caption: t('files_sharing', 'Overview of shared files.'), + + emptyTitle: t('files_sharing', 'No shares'), + emptyCaption: t('files_sharing', 'Files and folders you shared or have been shared with you will show up here'), + + icon: AccountPlusSvg, + order: 20, + + columns: [], + + getContents: () => getContents(), + })) + + Navigation.register(new View({ + id: sharedWithYouViewId, + name: t('files_sharing', 'Shared with you'), + caption: t('files_sharing', 'List of files that are shared with you.'), + + emptyTitle: t('files_sharing', 'Nothing shared with you yet'), + emptyCaption: t('files_sharing', 'Files and folders others shared with you will show up here'), + + icon: AccountSvg, + order: 1, + parent: sharesViewId, + + columns: [], + + getContents: () => getContents(true, false, false, false), + })) + + // Don't show this view if the user has no storage quota + const storageStats = loadState('files', 'storageStats', { quota: -1 }) + if (storageStats.quota !== 0) { + Navigation.register(new View({ + id: sharedWithOthersViewId, + name: t('files_sharing', 'Shared with others'), + caption: t('files_sharing', 'List of files that you shared with others.'), + + emptyTitle: t('files_sharing', 'Nothing shared yet'), + emptyCaption: t('files_sharing', 'Files and folders you shared will show up here'), + + icon: AccountGroupSvg, + order: 2, + parent: sharesViewId, + + columns: [], + + getContents: () => getContents(false, true, false, false), + })) + } + + Navigation.register(new View({ + id: sharingByLinksViewId, + name: t('files_sharing', 'Shared by link'), + caption: t('files_sharing', 'List of files that are shared by link.'), + + emptyTitle: t('files_sharing', 'No shared links'), + emptyCaption: t('files_sharing', 'Files and folders you shared by link will show up here'), + + icon: LinkSvg, + order: 3, + parent: sharesViewId, + + columns: [], + + getContents: () => getContents(false, true, false, false, [ShareType.Link]), + })) + + Navigation.register(new View({ + id: fileRequestViewId, + name: t('files_sharing', 'File requests'), + caption: t('files_sharing', 'List of file requests.'), + + emptyTitle: t('files_sharing', 'No file requests'), + emptyCaption: t('files_sharing', 'File requests you have created will show up here'), + + icon: FileUploadSvg, + order: 4, + parent: sharesViewId, + + columns: [], + + getContents: () => getContents(false, true, false, false, [ShareType.Link, ShareType.Email]) + .then(({ folder, contents }) => { + return { + folder, + contents: contents.filter((node) => isFileRequest(node.attributes?.['share-attributes'] || [])), + } + }), + })) + + Navigation.register(new View({ + id: deletedSharesViewId, + name: t('files_sharing', 'Deleted shares'), + caption: t('files_sharing', 'List of shares you left.'), + + emptyTitle: t('files_sharing', 'No deleted shares'), + emptyCaption: t('files_sharing', 'Shares you have left will show up here'), + + icon: DeleteSvg, + order: 5, + parent: sharesViewId, + + columns: [], + + getContents: () => getContents(false, false, false, true), + })) + + Navigation.register(new View({ + id: pendingSharesViewId, + name: t('files_sharing', 'Pending shares'), + caption: t('files_sharing', 'List of unapproved shares.'), + + emptyTitle: t('files_sharing', 'No pending shares'), + emptyCaption: t('files_sharing', 'Shares you have received but not approved will show up here'), + + icon: AccountClockSvg, + order: 6, + parent: sharesViewId, + + columns: [], + + getContents: () => getContents(false, false, true, false), + })) +} |