From 228b8af48491466e64db6e60fbdb5acbebc16e54 Mon Sep 17 00:00:00 2001 From: Pawel Boguslawski Date: Thu, 2 Jul 2026 15:07:36 +0200 Subject: [PATCH 1/2] fix(sharing): hide "Shared by link" when sharing by link is disabled Related: https://github.com/nextcloud/server/issues/50323 Author-Change-Id: IB#1156670 Signed-off-by: Pawel Boguslawski --- apps/files_sharing/src/files_views/shares.ts | 28 +++++++++++--------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/apps/files_sharing/src/files_views/shares.ts b/apps/files_sharing/src/files_views/shares.ts index 112bc2909988b..629936161b868 100644 --- a/apps/files_sharing/src/files_views/shares.ts +++ b/apps/files_sharing/src/files_views/shares.ts @@ -15,6 +15,7 @@ import { loadState } from '@nextcloud/initial-state' import { t } from '@nextcloud/l10n' import { ShareType } from '@nextcloud/sharing' import { getContents, isFileRequest } from '../services/SharingService.ts' +import { getCapabilities } from '@nextcloud/capabilities' export const sharesViewId = 'shareoverview' export const sharedWithYouViewId = 'sharingin' @@ -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, From 7da887c5996d704be4971987982ff277f0d16ee2 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Tue, 18 Aug 2026 21:52:32 +0200 Subject: [PATCH 2/2] fix: adjust code to comply with ESLint and align code with tests Signed-off-by: Ferdinand Thiessen --- .../src/files_views/shares.spec.ts | 23 ++++++++++ apps/files_sharing/src/files_views/shares.ts | 4 +- .../admin-settings-allow-links.spec.ts | 38 +++++++++++++++++ .../support/sections/FilesNavigationPage.ts | 42 +++++++++++++++++++ 4 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 tests/playwright/e2e/files_sharing/admin-settings-allow-links.spec.ts diff --git a/apps/files_sharing/src/files_views/shares.spec.ts b/apps/files_sharing/src/files_views/shares.spec.ts index 43c9fdf589851..e6b5a25222ad6 100644 --- a/apps/files_sharing/src/files_views/shares.spec.ts +++ b/apps/files_sharing/src/files_views/shares.spec.ts @@ -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) { @@ -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', () => { diff --git a/apps/files_sharing/src/files_views/shares.ts b/apps/files_sharing/src/files_views/shares.ts index 629936161b868..dc901a9b2d7b2 100644 --- a/apps/files_sharing/src/files_views/shares.ts +++ b/apps/files_sharing/src/files_views/shares.ts @@ -10,12 +10,12 @@ 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' import { ShareType } from '@nextcloud/sharing' import { getContents, isFileRequest } from '../services/SharingService.ts' -import { getCapabilities } from '@nextcloud/capabilities' export const sharesViewId = 'shareoverview' export const sharedWithYouViewId = 'sharingin' @@ -82,7 +82,7 @@ export default () => { } // Don't show this view if sharing by link is disabled. - if (getCapabilities().files_sharing.public.enabled) { + if (getCapabilities().files_sharing?.public.enabled) { Navigation.register(new View({ id: sharingByLinksViewId, name: t('files_sharing', 'Shared by link'), diff --git a/tests/playwright/e2e/files_sharing/admin-settings-allow-links.spec.ts b/tests/playwright/e2e/files_sharing/admin-settings-allow-links.spec.ts new file mode 100644 index 0000000000000..97d0968de3ed6 --- /dev/null +++ b/tests/playwright/e2e/files_sharing/admin-settings-allow-links.spec.ts @@ -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) + }) +}) diff --git a/tests/playwright/support/sections/FilesNavigationPage.ts b/tests/playwright/support/sections/FilesNavigationPage.ts index 23f32fafd986c..5a448d1852469 100644 --- a/tests/playwright/support/sections/FilesNavigationPage.ts +++ b/tests/playwright/support/sections/FilesNavigationPage.ts @@ -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. @@ -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 { + 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' })