Merge pull request #50739 from nextcloud/feat/share-grid-view

feat(sharing): Allow to set default view mode for public shares
This commit is contained in:
Ferdinand Thiessen 2025-02-11 11:59:03 +01:00 committed by GitHub
commit 5f423df9fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 215 additions and 29 deletions

15
apps/files_sharing/src/eventbus.d.ts vendored Normal file
View file

@ -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 {}

View file

@ -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 })
}
}
}

View file

@ -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

View file

@ -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')
})
})
})

2
dist/1031-1031.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/1031-1031.js.map vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/1031-1031.js.map.license vendored Symbolic link
View file

@ -0,0 +1 @@
1031-1031.js.license

2
dist/4915-4915.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
4915-4915.js.license

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long