aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFerdinand Thiessen <opensource@fthiessen.de>2025-02-06 11:50:39 +0100
committerFerdinand Thiessen <opensource@fthiessen.de>2025-02-11 12:19:05 +0100
commit8003b270c59a07aae106f215c76a8fd62510896e (patch)
tree581ed504feed5d03a6b418a259df4473f3ac1115
parent53b79b7f1e5da6d55fd1871c950e0fbdae3d686a (diff)
downloadnextcloud-server-8003b270c59a07aae106f215c76a8fd62510896e.tar.gz
nextcloud-server-8003b270c59a07aae106f215c76a8fd62510896e.zip
feat(sharing): Allow to set default view mode for public shares
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
-rw-r--r--apps/files_sharing/src/eventbus.d.ts15
-rw-r--r--apps/files_sharing/src/init-public.ts34
-rw-r--r--apps/files_sharing/src/views/SharingDetailsTab.vue73
-rw-r--r--cypress/e2e/files_sharing/public-share/default-view.cy.ts102
4 files changed, 205 insertions, 19 deletions
diff --git a/apps/files_sharing/src/eventbus.d.ts b/apps/files_sharing/src/eventbus.d.ts
new file mode 100644
index 00000000000..cc10ff8468f
--- /dev/null
+++ b/apps/files_sharing/src/eventbus.d.ts
@@ -0,0 +1,15 @@
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import type { Folder, Node } from '@nextcloud/files'
+
+declare module '@nextcloud/event-bus' {
+ export interface NextcloudEvents {
+ // mapping of 'event name' => 'event type'
+ 'files:list:updated': { folder: Folder, contents: Node[] }
+ 'files:config:updated': { key: string, value: boolean }
+ }
+}
+
+export {}
diff --git a/apps/files_sharing/src/init-public.ts b/apps/files_sharing/src/init-public.ts
index 79aab58bd1d..72a3098a0e6 100644
--- a/apps/files_sharing/src/init-public.ts
+++ b/apps/files_sharing/src/init-public.ts
@@ -2,13 +2,16 @@
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import { getNavigation } from '@nextcloud/files'
+import type { ShareAttribute } from './sharing.d.ts'
+import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
+import { Folder, getNavigation } from '@nextcloud/files'
import { loadState } from '@nextcloud/initial-state'
import registerFileDropView from './files_views/publicFileDrop.ts'
import registerPublicShareView from './files_views/publicShare.ts'
import registerPublicFileShareView from './files_views/publicFileShare.ts'
-import RouterService from '../../files/src/services/RouterService'
-import router from './router'
+import RouterService from '../../files/src/services/RouterService.ts'
+import router from './router/index.ts'
+import logger from './services/logger.ts'
registerFileDropView()
registerPublicShareView()
@@ -33,3 +36,28 @@ if (fileId !== null) {
{ ...window.OCP.Files.Router.query, openfile: 'true' },
)
}
+
+// When the file list is loaded we need to apply the "userconfig" setup on the share
+subscribe('files:list:updated', loadShareConfig)
+
+/**
+ * Event handler to load the view config for the current share.
+ * This is done on the `files:list:updated` event to ensure the list and especially the config store was correctly initialized.
+ *
+ * @param context The event context
+ * @param context.folder The current folder
+ */
+function loadShareConfig({ folder }: { folder: Folder }) {
+ // Only setup config once
+ unsubscribe('files:list:updated', loadShareConfig)
+
+ // Share attributes (the same) are set on all folders of a share
+ if (folder.attributes['share-attributes']) {
+ const shareAttributes = JSON.parse(folder.attributes['share-attributes'] || '[]') as Array<ShareAttribute>
+ const gridViewAttribute = shareAttributes.find(({ scope, key }: ShareAttribute) => scope === 'config' && key === 'grid_view')
+ if (gridViewAttribute !== undefined) {
+ logger.debug('Loading share attributes', { gridViewAttribute })
+ emit('files:config:updated', { key: 'grid_view', value: gridViewAttribute.value === true })
+ }
+ }
+}
diff --git a/apps/files_sharing/src/views/SharingDetailsTab.vue b/apps/files_sharing/src/views/SharingDetailsTab.vue
index df420cf7af2..b0369df6339 100644
--- a/apps/files_sharing/src/views/SharingDetailsTab.vue
+++ b/apps/files_sharing/src/views/SharingDetailsTab.vue
@@ -169,7 +169,7 @@
@update:checked="queueUpdate('hideDownload')">
{{ t('files_sharing', 'Hide download') }}
</NcCheckboxRadioSwitch>
- <NcCheckboxRadioSwitch v-if="!isPublicShare"
+ <NcCheckboxRadioSwitch v-else
:disabled="!canSetDownload"
:checked.sync="canDownload"
data-cy-files-sharing-share-permissions-checkbox="download">
@@ -183,6 +183,10 @@
:placeholder="t('files_sharing', 'Enter a note for the share recipient')"
:value.sync="share.note" />
</template>
+ <NcCheckboxRadioSwitch v-if="isPublicShare && isFolder"
+ :checked.sync="showInGridView">
+ {{ t('files_sharing', 'Show files in grid view') }}
+ </NcCheckboxRadioSwitch>
<ExternalShareAction v-for="action in externalLinkActions"
:id="action.id"
ref="externalLinkActions"
@@ -439,28 +443,29 @@ export default {
this.updateAtomicPermissions({ isReshareChecked: checked })
},
},
+
+ /**
+ * Change the default view for public shares from "list" to "grid"
+ */
+ showInGridView: {
+ get() {
+ return this.getShareAttribute('config', 'grid_view', false)
+ },
+ /** @param {boolean} value If the default view should be changed to "grid" */
+ set(value) {
+ this.setShareAttribute('config', 'grid_view', value)
+ },
+ },
+
/**
* Can the sharee download files or only view them ?
*/
canDownload: {
get() {
- return this.share.attributes?.find(attr => attr.key === 'download')?.value ?? true
+ return this.getShareAttribute('permissions', 'download', true)
},
set(checked) {
- // Find the 'download' attribute and update its value
- const downloadAttr = this.share.attributes?.find(attr => attr.key === 'download')
- if (downloadAttr) {
- downloadAttr.value = checked
- } else {
- if (this.share.attributes === null) {
- this.$set(this.share, 'attributes', [])
- }
- this.share.attributes.push({
- scope: 'permissions',
- key: 'download',
- value: checked,
- })
- }
+ this.setShareAttribute('permissions', 'download', checked)
},
},
/**
@@ -783,6 +788,42 @@ export default {
},
methods: {
+ /**
+ * Set a share attribute on the current share
+ * @param {string} scope The attribute scope
+ * @param {string} key The attribute key
+ * @param {boolean} value The value
+ */
+ setShareAttribute(scope, key, value) {
+ if (!this.share.attributes) {
+ this.$set(this.share, 'attributes', [])
+ }
+
+ const attribute = this.share.attributes
+ .find((attr) => attr.scope === scope || attr.key === key)
+
+ if (attribute) {
+ attribute.value = value
+ } else {
+ this.share.attributes.push({
+ scope,
+ key,
+ value,
+ })
+ }
+ },
+
+ /**
+ * Get the value of a share attribute
+ * @param {string} scope The attribute scope
+ * @param {string} key The attribute key
+ * @param {undefined|boolean} fallback The fallback to return if not found
+ */
+ getShareAttribute(scope, key, fallback = undefined) {
+ const attribute = this.share.attributes?.find((attr) => attr.scope === scope && attr.key === key)
+ return attribute?.value ?? fallback
+ },
+
async generateNewToken() {
if (this.loadingToken) {
return
diff --git a/cypress/e2e/files_sharing/public-share/default-view.cy.ts b/cypress/e2e/files_sharing/public-share/default-view.cy.ts
new file mode 100644
index 00000000000..62796a6420a
--- /dev/null
+++ b/cypress/e2e/files_sharing/public-share/default-view.cy.ts
@@ -0,0 +1,102 @@
+/*!
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import type { User } from '@nextcloud/cypress'
+import { getRowForFile } from '../../files/FilesUtils.ts'
+import { createShare, setupData } from './setup-public-share.ts'
+
+describe('files_sharing: Public share - setting the default view mode', () => {
+
+ let user: User
+
+ beforeEach(() => {
+ cy.createRandomUser()
+ .then(($user) => (user = $user))
+ .then(() => setupData(user, 'shared'))
+ })
+
+ it('is by default in list view', () => {
+ const context = { user }
+ createShare(context, 'shared')
+ .then((url) => {
+ cy.logout()
+ cy.visit(url!)
+
+ // See file is visible
+ getRowForFile('foo.txt').should('be.visible')
+ // See we are in list view
+ cy.findByRole('button', { name: 'Switch to grid view' })
+ .should('be.visible')
+ .and('not.be.disabled')
+ })
+ })
+
+ it('can be toggled by user', () => {
+ const context = { user }
+ createShare(context, 'shared')
+ .then((url) => {
+ cy.logout()
+ cy.visit(url!)
+
+ // See file is visible
+ getRowForFile('foo.txt')
+ .should('be.visible')
+ // See we are in list view
+ .find('.files-list__row-icon')
+ .should(($el) => expect($el.outerWidth()).to.be.lessThan(99))
+
+ // See the grid view toggle
+ cy.findByRole('button', { name: 'Switch to grid view' })
+ .should('be.visible')
+ .and('not.be.disabled')
+ // And can change to grid view
+ .click()
+
+ // See we are in grid view
+ getRowForFile('foo.txt')
+ .find('.files-list__row-icon')
+ .should(($el) => expect($el.outerWidth()).to.be.greaterThan(99))
+
+ // See the grid view toggle is now the list view toggle
+ cy.findByRole('button', { name: 'Switch to list view' })
+ .should('be.visible')
+ .and('not.be.disabled')
+ })
+ })
+
+ it('can be changed to default grid view', () => {
+ const context = { user }
+ createShare(context, 'shared')
+ .then((url) => {
+ // Can set the "grid" view checkbox
+ cy.findByRole('list', { name: 'Link shares' })
+ .findAllByRole('listitem')
+ .first()
+ .findByRole('button', { name: /Actions/i })
+ .click()
+ cy.findByRole('menuitem', { name: /Customize link/i }).click()
+ cy.findByRole('checkbox', { name: /Show files in grid view/i })
+ .scrollIntoView()
+ cy.findByRole('checkbox', { name: /Show files in grid view/i })
+ .should('not.be.checked')
+ .check({ force: true })
+
+ // Wait for the share update
+ cy.intercept('PUT', '**/ocs/v2.php/apps/files_sharing/api/v1/shares/*').as('updateShare')
+ cy.findByRole('button', { name: 'Update share' }).click()
+ cy.wait('@updateShare').its('response.statusCode').should('eq', 200)
+
+ // Logout and visit the share
+ cy.logout()
+ cy.visit(url!)
+
+ // See file is visible
+ getRowForFile('foo.txt').should('be.visible')
+ // See we are in list view
+ cy.findByRole('button', { name: 'Switch to list view' })
+ .should('be.visible')
+ .and('not.be.disabled')
+ })
+ })
+})