Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions apps/files_sharing/src/files_views/shares.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@ import registerSharingViews from './shares.ts'

import '../main.ts'

const getCapabilities = vi.hoisted(() => vi.fn())
vi.mock('@nextcloud/capabilities', () => ({ getCapabilities }))

const navigation = getNavigation()
beforeEach(() => {
vi.resetAllMocks()
getCapabilities.mockReturnValue({ files_sharing: { public: { enabled: true } } })

const views = [...navigation.views]
for (const view of views) {
Expand Down Expand Up @@ -91,6 +95,25 @@ describe('Sharing views definition', () => {
const sharedWithOthersView = navigation.views.find((view) => view.id === 'sharingout')
expect(sharedWithOthersView).toBeUndefined()
})

test.for([
['disabled', { files_sharing: { public: { enabled: false } } }],
['not available', {}],
] as const)('Shared by link view is not registered if public sharing is %s', ([, capabilities]) => {
vi.spyOn(navigation, 'register')
getCapabilities.mockReturnValue(capabilities)

expect(navigation.views.length).toBe(0)
registerSharingViews()
expect(navigation.register).toHaveBeenCalledTimes(6)
expect(navigation.views.length).toBe(6)

const sharesChildViews = navigation.views.filter((view) => view.parent === 'shareoverview') as View[]
expect(sharesChildViews.length).toBe(5)

const sharingByLinksView = navigation.views.find((view) => view.id === 'sharinglinks')
expect(sharingByLinksView).toBeUndefined()
})
})

describe('Sharing views contents', () => {
Expand Down
28 changes: 16 additions & 12 deletions apps/files_sharing/src/files_views/shares.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import AccountPlusSvg from '@mdi/svg/svg/account-plus-outline.svg?raw'
import FileUploadSvg from '@mdi/svg/svg/file-upload-outline.svg?raw'
import LinkSvg from '@mdi/svg/svg/link.svg?raw'
import DeleteSvg from '@mdi/svg/svg/trash-can-outline.svg?raw'
import { getCapabilities } from '@nextcloud/capabilities'
import { getNavigation, View } from '@nextcloud/files'
import { loadState } from '@nextcloud/initial-state'
import { t } from '@nextcloud/l10n'
Expand Down Expand Up @@ -80,22 +81,25 @@ export default () => {
}))
}

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.'),
// Don't show this view if sharing by link is disabled.
if (getCapabilities().files_sharing?.public.enabled) {
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'),
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,
icon: LinkSvg,
order: 3,
parent: sharesViewId,

columns: [],
columns: [],

getContents: () => getContents(false, true, false, false, [ShareType.Link]),
}))
getContents: () => getContents(false, true, false, false, [ShareType.Link]),
}))
}

Navigation.register(new View({
id: fileRequestViewId,
Expand Down
4 changes: 2 additions & 2 deletions dist/files_sharing-init.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/files_sharing-init.js.map

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { runOcc } from '@nextcloud/e2e-test-server'
import { expect, test } from '../../support/fixtures/files-page.ts'

/**
* The "Shared by link" view lists link shares, so it must only be registered
* while public link sharing is allowed instance-wide.
*/
test.describe('files_sharing: Shared by link view', () => {
test.afterAll(async () => {
await runOcc(['config:app:delete', 'core', 'shareapi_allow_links'])
})

test('is listed while link sharing is enabled', async ({ filesListPage, filesNavigation }) => {
await runOcc(['config:app:set', '--value', 'yes', 'core', 'shareapi_allow_links'])

await filesListPage.open('shareoverview')
await filesNavigation.expandNavigationEntry('Shares')

await expect(filesNavigation.getNavigationEntry('Shared by link')).toBeVisible()
})

test('is not listed when link sharing is disabled', async ({ filesListPage, filesNavigation }) => {
await runOcc(['config:app:set', '--value', 'no', 'core', 'shareapi_allow_links'])

await filesListPage.open('shareoverview')
await filesNavigation.expandNavigationEntry('Shares')

// The sibling views are expanded and visible, so a missing entry is really
// an unregistered view and not just a collapsed parent.
await expect(filesNavigation.getNavigationEntry('Shared with others')).toBeVisible()
await expect(filesNavigation.getNavigationEntry('Shared by link')).toHaveCount(0)
})
})
42 changes: 42 additions & 0 deletions tests/playwright/support/sections/FilesNavigationPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import type { Locator, Page } from '@playwright/test'

import { expect } from '@playwright/test'

/**
* The left-hand files navigation (the view list: All files, Favorites, Recent, …).
* Distinct from {@link NavigationHeaderPage}, which models the top app bar.
Expand Down Expand Up @@ -52,6 +54,46 @@ export class FilesNavigationPage {
.click()
}

/**
* A navigation entry addressed by its visible name, e.g. "Shared by link".
*
* @param name - The name of the view as shown in the navigation
*/
getNavigationEntry(name: string): Locator {
return this.navigation().getByRole('link', { name, exact: true })
}

/**
* The list item wrapping a navigation entry - it also contains the entry's
* collapse toggle and, once expanded, its child entries.
*
* @param name - The name of the view as shown in the navigation
*/
getNavigationEntryItem(name: string): Locator {
return this.navigation()
.getByRole('listitem')
.filter({ has: this.page.getByRole('link', { name, exact: true }) })
.first()
}

/**
* Expand a collapsible navigation entry by name to reveal its child entries.
*
* @param name - The name of the view as shown in the navigation
*/
async expandNavigationEntry(name: string): Promise<void> {
const item = this.getNavigationEntryItem(name)
await expect(item).toBeVisible()

await expect(async () => {
const toggle = item.getByRole('button', { name: 'Open menu' })
if (await toggle.isVisible()) {
await toggle.click()
}
await expect(item.getByRole('button', { name: 'Collapse menu' })).toBeVisible()
}).toPass()
}

/** The "Files settings" dialog opened from the navigation footer. */
settingsDialog(): Locator {
return this.page.getByRole('dialog', { name: 'Files settings' })
Expand Down
Loading