Browse Source

feat(files_sharing): show Account menu on public pages

Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
pull/52537/head
skjnldsv 6 months ago
parent
commit
bf3ce79abd
  1. 5
      apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php
  2. 10
      apps/files_sharing/lib/Listener/LoadPublicFileRequestAuthListener.php
  3. 57
      apps/files_sharing/src/public-file-request.ts
  4. 86
      apps/files_sharing/src/public-nickname-handler.ts
  5. 138
      apps/files_sharing/src/views/PublicAuthPrompt.vue
  6. 2
      apps/files_sharing/tests/Controller/ShareControllerTest.php
  7. 9
      core/src/components/AccountMenu/AccountMenuEntry.vue
  8. 8
      core/src/components/PublicPageMenu/PublicPageMenuEntry.vue
  9. 15
      core/src/public-page-user-menu.ts
  10. 2
      core/src/views/AccountMenu.vue
  11. 135
      core/src/views/PublicPageUserMenu.vue
  12. 1
      core/templates/layout.public.php
  13. 10
      cypress/e2e/files_sharing/public-share/PublicShareUtils.ts
  14. 2
      cypress/e2e/files_sharing/public-share/copy-move-files.cy.ts
  15. 8
      cypress/e2e/files_sharing/public-share/default-view.cy.ts
  16. 6
      cypress/e2e/files_sharing/public-share/download.cy.ts
  17. 193
      cypress/e2e/files_sharing/public-share/header-avatar.cy.ts
  18. 2
      cypress/e2e/files_sharing/public-share/header-menu.cy.ts
  19. 2
      cypress/e2e/files_sharing/public-share/rename-files.cy.ts
  20. 24
      cypress/e2e/files_sharing/public-share/required-before-create.cy.ts
  21. 1
      lib/public/AppFramework/Http/Template/PublicTemplateResponse.php
  22. 14
      package-lock.json
  23. 4
      package.json
  24. 3
      webpack.modules.js

5
apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php

@ -107,13 +107,12 @@ class DefaultPublicShareTemplateProvider implements IPublicShareTemplateProvider
Util::addInitScript(Application::APP_ID, 'init');
Util::addInitScript(Application::APP_ID, 'init-public');
Util::addScript('files', 'main');
Util::addScript(Application::APP_ID, 'public-nickname-handler');
// Add file-request script if needed
$attributes = $share->getAttributes();
$isFileRequest = $attributes?->getAttribute('fileRequest', 'enabled') === true;
if ($isFileRequest) {
Util::addScript(Application::APP_ID, 'public-file-request');
}
$this->initialState->provideInitialState('isFileRequest', $isFileRequest);
// Load Viewer scripts
if (class_exists(LoadViewer::class)) {

10
apps/files_sharing/lib/Listener/LoadPublicFileRequestAuthListener.php

@ -10,6 +10,7 @@ namespace OCA\Files_Sharing\Listener;
use OCA\Files_Sharing\AppInfo\Application;
use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Share\IManager;
@ -19,6 +20,7 @@ use OCP\Util;
class LoadPublicFileRequestAuthListener implements IEventListener {
public function __construct(
private IManager $shareManager,
private IInitialState $initialState,
) {
}
@ -51,9 +53,9 @@ class LoadPublicFileRequestAuthListener implements IEventListener {
// Ignore, this is not a file request or the share does not exist
}
if ($isFileRequest) {
// Add the script to the public page
Util::addScript(Application::APP_ID, 'public-file-request');
}
Util::addScript(Application::APP_ID, 'public-nickname-handler');
// Add file-request script if needed
$this->initialState->provideInitialState('isFileRequest', $isFileRequest);
}
}

57
apps/files_sharing/src/public-file-request.ts

@ -1,57 +0,0 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { defineAsyncComponent } from 'vue'
import { getBuilder } from '@nextcloud/browser-storage'
import { getGuestNickname, setGuestNickname } from '@nextcloud/auth'
import { getUploader } from '@nextcloud/upload'
import { spawnDialog } from '@nextcloud/dialogs'
import logger from './services/logger'
const storage = getBuilder('files_sharing').build()
/**
* Setup file-request nickname header for the uploader
* @param nickname The nickname
*/
function registerFileRequestHeader(nickname: string) {
const uploader = getUploader()
uploader.setCustomHeader('X-NC-Nickname', encodeURIComponent(nickname))
logger.debug('Nickname header registered for uploader', { headers: uploader.customHeaders })
}
/**
* Callback when a nickname was chosen
* @param nickname The chosen nickname
*/
function onSetNickname(nickname: string): void {
// Set the nickname
setGuestNickname(nickname)
// Set the dialog as shown
storage.setItem('public-auth-prompt-shown', 'true')
// Register header for uploader
registerFileRequestHeader(nickname)
}
window.addEventListener('DOMContentLoaded', () => {
const nickname = getGuestNickname() ?? ''
const dialogShown = storage.getItem('public-auth-prompt-shown') !== null
// If we don't have a nickname or the public auth prompt hasn't been shown yet, show it
// We still show the prompt if the user has a nickname to double check
if (!nickname || !dialogShown) {
spawnDialog(
defineAsyncComponent(() => import('./views/PublicAuthPrompt.vue')),
{
nickname,
},
onSetNickname as (...rest: unknown[]) => void,
)
} else {
logger.debug('Public auth prompt already shown.', { nickname })
registerFileRequestHeader(nickname)
}
})

86
apps/files_sharing/src/public-nickname-handler.ts

@ -0,0 +1,86 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { getBuilder } from '@nextcloud/browser-storage'
import { getGuestNickname, type NextcloudUser } from '@nextcloud/auth'
import { getUploader } from '@nextcloud/upload'
import { loadState } from '@nextcloud/initial-state'
import { showGuestUserPrompt } from '@nextcloud/dialogs'
import { t } from '@nextcloud/l10n'
import logger from './services/logger'
import { subscribe } from '@nextcloud/event-bus'
const storage = getBuilder('files_sharing').build()
// Setup file-request nickname header for the uploader
const registerFileRequestHeader = (nickname: string) => {
const uploader = getUploader()
uploader.setCustomHeader('X-NC-Nickname', encodeURIComponent(nickname))
logger.debug('Nickname header registered for uploader', { headers: uploader.customHeaders })
}
// Callback when a nickname was chosen
const onUserInfoChanged = (guest: NextcloudUser) => {
logger.debug('User info changed', { guest })
registerFileRequestHeader(guest.displayName ?? '')
}
// Monitor nickname changes
subscribe('user:info:changed', onUserInfoChanged)
window.addEventListener('DOMContentLoaded', () => {
const nickname = getGuestNickname() ?? ''
const dialogShown = storage.getItem('public-auth-prompt-shown') !== null
// Check if a nickname is mandatory
const isFileRequest = loadState('files_sharing', 'isFileRequest', false)
const owner = loadState('files_sharing', 'owner', '')
const ownerDisplayName = loadState('files_sharing', 'ownerDisplayName', '')
const label = loadState('files_sharing', 'label', '')
const filename = loadState('files_sharing', 'filename', '')
// If the owner provided a custom label, use it instead of the filename
const folder = label || filename
const options = {
nickname,
notice: t('files_sharing', 'To upload files to {folder}, you need to provide your name first.', { folder }),
subtitle: undefined as string | undefined,
title: t('files_sharing', 'Upload files to {folder}', { folder }),
}
// If the guest already has a nickname, we just make them double check
if (nickname) {
options.notice = t('files_sharing', 'Please confirm your name to upload files to {folder}', { folder })
}
// If the account owner set their name as public,
// we show it in the subtitle
if (owner) {
options.subtitle = t('files_sharing', '{ownerDisplayName} shared a folder with you.', { ownerDisplayName })
}
// If this is a file request, then we need a nickname
if (isFileRequest) {
// If we don't have a nickname or the public auth prompt hasn't been shown yet, show it
// We still show the prompt if the user has a nickname to double check
if (!nickname || !dialogShown) {
logger.debug('Showing public auth prompt.', { nickname })
showGuestUserPrompt(options)
}
return
}
if (!dialogShown && !nickname) {
logger.debug('Public auth prompt not shown yet but nickname is not mandatory.', { nickname })
return
}
// Else, we just register the nickname header if any.
logger.debug('Public auth prompt already shown.', { nickname })
registerFileRequestHeader(nickname)
})

138
apps/files_sharing/src/views/PublicAuthPrompt.vue

@ -1,138 +0,0 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<NcDialog :buttons="dialogButtons"
class="public-auth-prompt"
data-cy-public-auth-prompt-dialog
is-form
:can-close="false"
:name="dialogName"
@submit="$emit('close', name)">
<p v-if="owner" class="public-auth-prompt__subtitle">
{{ t('files_sharing', '{ownerDisplayName} shared a folder with you.', { ownerDisplayName }) }}
</p>
<!-- Header -->
<NcNoteCard class="public-auth-prompt__header"
:text="t('files_sharing', 'To upload files, you need to provide your name first.')"
type="info" />
<!-- Form -->
<NcTextField ref="input"
class="public-auth-prompt__input"
data-cy-public-auth-prompt-dialog-name
:label="t('files_sharing', 'Name')"
:placeholder="t('files_sharing', 'Enter your name')"
minlength="2"
name="name"
required
:value.sync="name" />
</NcDialog>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { loadState } from '@nextcloud/initial-state'
import { t } from '@nextcloud/l10n'
import NcDialog from '@nextcloud/vue/components/NcDialog'
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import NcTextField from '@nextcloud/vue/components/NcTextField'
import { getGuestNameValidity } from '../services/GuestNameValidity'
export default defineComponent({
name: 'PublicAuthPrompt',
components: {
NcDialog,
NcNoteCard,
NcTextField,
},
props: {
/**
* Preselected nickname
* @default '' No name preselected by default
*/
nickname: {
type: String,
default: '',
},
},
setup() {
return {
t,
owner: loadState('files_sharing', 'owner', ''),
ownerDisplayName: loadState('files_sharing', 'ownerDisplayName', ''),
label: loadState('files_sharing', 'label', ''),
note: loadState('files_sharing', 'note', ''),
filename: loadState('files_sharing', 'filename', ''),
}
},
data() {
return {
name: '',
}
},
computed: {
dialogName() {
return this.t('files_sharing', 'Upload files to {folder}', { folder: this.label || this.filename })
},
dialogButtons() {
return [{
label: t('files_sharing', 'Submit name'),
type: 'primary',
nativeType: 'submit',
}]
},
},
watch: {
/** Reset name to pre-selected nickname (e.g. Talk / Collabora ) */
nickname: {
handler() {
this.name = this.nickname
},
immediate: true,
},
name() {
// Check validity of the new name
const newName = this.name.trim?.() || ''
const input = (this.$refs.input as Vue|undefined)?.$el.querySelector('input')
if (!input) {
return
}
const validity = getGuestNameValidity(newName)
input.setCustomValidity(validity)
input.reportValidity()
},
},
})
</script>
<style scoped lang="scss">
.public-auth-prompt {
&__subtitle {
// Smaller than dialog title
font-size: 1.25em;
margin-block: 0 calc(3 * var(--default-grid-baseline));
}
&__header {
margin-block: 0 calc(3 * var(--default-grid-baseline));
}
&__input {
margin-block: calc(4 * var(--default-grid-baseline)) calc(2 * var(--default-grid-baseline));
}
}
</style>

2
apps/files_sharing/tests/Controller/ShareControllerTest.php

@ -336,6 +336,7 @@ class ShareControllerTest extends \Test\TestCase {
'fileId' => 111,
'owner' => 'ownerUID',
'ownerDisplayName' => 'ownerDisplay',
'isFileRequest' => false,
];
$response = $this->shareController->showShare();
@ -480,6 +481,7 @@ class ShareControllerTest extends \Test\TestCase {
'disclaimer' => 'My disclaimer text',
'owner' => 'ownerUID',
'ownerDisplayName' => 'ownerDisplay',
'isFileRequest' => false,
'note' => 'The note',
'label' => 'A label',
];

9
core/src/components/AccountMenu/AccountMenuEntry.vue

@ -78,9 +78,14 @@ export default defineComponent({
},
methods: {
onClick(e) {
this.loading = true
onClick(e: MouseEvent) {
this.$emit('click', e)
// Allow to not show the loading indicator
// in case the click event was already handled
if (!e.defaultPrevented) {
this.loading = true
}
},
},
})

8
core/src/components/PublicPageMenu/PublicPageMenuEntry.vue

@ -11,22 +11,24 @@
role="presentation"
@click="$emit('click')">
<template #icon>
<div role="presentation" :class="['icon', icon, 'public-page-menu-entry__icon']" />
<slot v-if="$scopedSlots.icon" name="icon" />
<div v-else role="presentation" :class="['icon', icon, 'public-page-menu-entry__icon']" />
</template>
</NcListItem>
</template>
<script setup lang="ts">
import NcListItem from '@nextcloud/vue/components/NcListItem'
import { onMounted } from 'vue'
import NcListItem from '@nextcloud/vue/components/NcListItem'
const props = defineProps<{
/** Only emit click event but do not open href */
clickOnly?: boolean
// menu entry props
id: string
label: string
icon: string
icon?: string
href: string
details?: string
}>()

15
core/src/public-page-user-menu.ts

@ -0,0 +1,15 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { getCSPNonce } from '@nextcloud/auth'
import Vue from 'vue'
import PublicPageUserMenu from './views/PublicPageUserMenu.vue'
__webpack_nonce__ = getCSPNonce()
const View = Vue.extend(PublicPageUserMenu)
const instance = new View()
instance.$mount('#public-page-user-menu')

2
core/src/views/AccountMenu.vue

@ -211,7 +211,7 @@ export default defineComponent({
}
}
// Ensure we do not wast space, as the header menu sets a default width of 350px
// Ensure we do not waste space, as the header menu sets a default width of 350px
:deep(.header-menu__content) {
width: fit-content !important;
}

135
core/src/views/PublicPageUserMenu.vue

@ -0,0 +1,135 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<NcHeaderMenu id="public-page-user-menu"
class="public-page-user-menu"
is-nav
:aria-label="t('core', 'User menu')"
:description="avatarDescription">
<template #trigger>
<NcAvatar class="public-page-user-menu__avatar"
disable-menu
disable-tooltip
is-guest
:user="displayName || '?'" />
</template>
<ul class="public-page-user-menu__list">
<!-- Privacy notice -->
<NcNoteCard class="public-page-user-menu__list-note"
:text="privacyNotice"
type="info" />
<!-- Nickname dialog -->
<AccountMenuEntry id="set-nickname"
:name="!displayName ? t('core', 'Set public name') : t('core', 'Change public name')"
href="#"
@click.prevent.stop="setNickname">
<template #icon>
<IconAccount />
</template>
</AccountMenuEntry>
</ul>
</NcHeaderMenu>
</template>
<script lang="ts">
import type { NextcloudUser } from '@nextcloud/auth'
import '@nextcloud/dialogs/style.css'
import { defineComponent } from 'vue'
import { getGuestUser } from '@nextcloud/auth'
import { showGuestUserPrompt } from '@nextcloud/dialogs'
import { subscribe } from '@nextcloud/event-bus'
import { t } from '@nextcloud/l10n'
import NcAvatar from '@nextcloud/vue/components/NcAvatar'
import NcHeaderMenu from '@nextcloud/vue/components/NcHeaderMenu'
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import IconAccount from 'vue-material-design-icons/Account.vue'
import AccountMenuEntry from '../components/AccountMenu/AccountMenuEntry.vue'
export default defineComponent({
name: 'PublicPageUserMenu',
components: {
AccountMenuEntry,
IconAccount,
NcAvatar,
NcHeaderMenu,
NcNoteCard,
},
setup() {
return {
t,
}
},
data() {
return {
displayName: getGuestUser().displayName,
}
},
computed: {
avatarDescription(): string {
return t('core', 'User menu')
},
privacyNotice(): string {
return this.displayName
? t('core', 'You will be identified as {user} by the account owner.', { user: this.displayName })
: t('core', 'You are currently not identified.')
},
},
mounted() {
subscribe('user:info:changed', (user: NextcloudUser) => {
this.displayName = user.displayName || ''
})
},
methods: {
setNickname() {
showGuestUserPrompt({
nickname: this.displayName,
cancellable: true,
})
},
},
})
</script>
<style scoped lang="scss">
.public-page-user-menu {
box-sizing: border-box;
// Ensure we do not waste space, as the header menu sets a default width of 350px
:deep(.header-menu__content) {
width: fit-content !important;
}
&__list-note {
padding-block: 5px !important;
padding-inline: 5px !important;
max-width: 300px;
margin: 5px !important;
margin-bottom: 0 !important;
}
&__list {
display: inline-flex;
flex-direction: column;
padding-block: var(--default-grid-baseline) 0;
padding-inline: 0 var(--default-grid-baseline);
> :deep(li) {
box-sizing: border-box;
// basically "fit-content"
flex: 0 1;
}
}
}
</style>

1
core/templates/layout.public.php

@ -77,6 +77,7 @@ p($theme->getTitle());
<div class="header-end">
<div id="public-page-menu"></div>
<div id="public-page-user-menu"></div>
</div>
</header>

10
cypress/e2e/files_sharing/public-share/setup-public-share.ts → cypress/e2e/files_sharing/public-share/PublicShareUtils.ts

@ -96,12 +96,12 @@ function checkExpirationDateState(enforced: boolean, hasDefault: boolean) {
* @param shareName The name of the shared folder
* @param options The share options
*/
export function createShare(context: ShareContext, shareName: string, options: ShareOptions | null = null) {
export function createLinkShare(context: ShareContext, shareName: string, options: ShareOptions | null = null): Cypress.Chainable<string> {
cy.login(context.user)
cy.visit('/apps/files')
openSharingPanel(shareName)
cy.intercept('POST', '**/ocs/v2.php/apps/files_sharing/api/v1/shares').as('createShare')
cy.intercept('POST', '**/ocs/v2.php/apps/files_sharing/api/v1/shares').as('createLinkShare')
cy.findByRole('button', { name: 'Create a new share link' }).click()
// Conduct optional checks based on the provided options
if (options) {
@ -111,14 +111,14 @@ export function createShare(context: ShareContext, shareName: string, options: S
cy.findByRole('button', { name: 'Create share' }).click()
}
return cy.wait('@createShare')
return cy.wait('@createLinkShare')
.should(({ response }) => {
expect(response?.statusCode).to.eq(200)
const url = response?.body?.ocs?.data?.url
expect(url).to.match(/^https?:\/\//)
context.url = url
})
.then(() => cy.wrap(context.url))
.then(() => cy.wrap(context.url as string))
}
/**
@ -173,7 +173,7 @@ export function setupPublicShare(shareName = 'shared'): Cypress.Chainable<string
defaultShareContext.user = user
})
.then(() => setupData(defaultShareContext.user, shareName))
.then(() => createShare(defaultShareContext, shareName))
.then(() => createLinkShare(defaultShareContext, shareName))
.then((url) => {
shareData.shareUrl = url
})

2
cypress/e2e/files_sharing/public-share/copy-move-files.cy.ts

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { copyFile, getRowForFile, moveFile, navigateToFolder } from '../../files/FilesUtils.ts'
import { getShareUrl, setupPublicShare } from './setup-public-share.ts'
import { getShareUrl, setupPublicShare } from './PublicShareUtils.ts'
describe('files_sharing: Public share - copy and move files', { testIsolation: true }, () => {

8
cypress/e2e/files_sharing/public-share/default-view.cy.ts

@ -4,7 +4,7 @@
*/
import type { User } from '@nextcloud/cypress'
import { getRowForFile } from '../../files/FilesUtils.ts'
import { createShare, setupData } from './setup-public-share.ts'
import { createLinkShare, setupData } from './PublicShareUtils.ts'
describe('files_sharing: Public share - setting the default view mode', () => {
@ -18,7 +18,7 @@ describe('files_sharing: Public share - setting the default view mode', () => {
it('is by default in list view', () => {
const context = { user }
createShare(context, 'shared')
createLinkShare(context, 'shared')
.then((url) => {
cy.logout()
cy.visit(url!)
@ -34,7 +34,7 @@ describe('files_sharing: Public share - setting the default view mode', () => {
it('can be toggled by user', () => {
const context = { user }
createShare(context, 'shared')
createLinkShare(context, 'shared')
.then((url) => {
cy.logout()
cy.visit(url!)
@ -67,7 +67,7 @@ describe('files_sharing: Public share - setting the default view mode', () => {
it('can be changed to default grid view', () => {
const context = { user }
createShare(context, 'shared')
createLinkShare(context, 'shared')
.then((url) => {
// Can set the "grid" view checkbox
cy.findByRole('list', { name: 'Link shares' })

6
cypress/e2e/files_sharing/public-share/download.cy.ts

@ -4,7 +4,7 @@
*/
// @ts-expect-error The package is currently broken - but works...
import { deleteDownloadsFolderBeforeEach } from 'cypress-delete-downloads-folder'
import { createShare, getShareUrl, openLinkShareDetails, setupPublicShare, type ShareContext } from './setup-public-share.ts'
import { createLinkShare, getShareUrl, openLinkShareDetails, setupPublicShare, type ShareContext } from './PublicShareUtils.ts'
import { getRowForFile, getRowForFileId, triggerActionForFile, triggerActionForFileId } from '../../files/FilesUtils.ts'
import { zipFileContains } from '../../../support/utils/assertions.ts'
import type { User } from '@nextcloud/cypress'
@ -22,7 +22,7 @@ describe('files_sharing: Public share - downloading files', { testIsolation: tru
cy.uploadContent(user, new Blob(['<content>foo</content>']), 'text/plain', '/file.txt')
.then(({ headers }) => { fileId = Number.parseInt(headers['oc-fileid']) })
cy.login(user)
createShare(context, 'file.txt')
createLinkShare(context, 'file.txt')
.then(() => cy.logout())
.then(() => cy.visit(context.url!))
})
@ -179,7 +179,7 @@ describe('files_sharing: Public share - downloading files', { testIsolation: tru
cy.mkdir(user, '/test')
context = { user }
createShare(context, 'test')
createLinkShare(context, 'test')
cy.login(context.user)
cy.visit('/apps/files')
})

193
cypress/e2e/files_sharing/public-share/header-avatar.cy.ts

@ -0,0 +1,193 @@
/*!
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { ShareContext } from './PublicShareUtils.ts'
import { createLinkShare, setupData } from './PublicShareUtils.ts'
/**
* This tests ensures that on public shares the header avatar menu correctly works
*/
describe('files_sharing: Public share - header avatar menu', { testIsolation: true }, () => {
let context: ShareContext
let firstPublicShareUrl = ''
let secondPublicShareUrl = ''
before(() => {
cy.createRandomUser()
.then((user) => {
context = {
user,
url: undefined,
}
setupData(context.user, 'public1')
setupData(context.user, 'public2')
createLinkShare(context, 'public1').then((shareUrl) => {
firstPublicShareUrl = shareUrl
cy.log(`Created first share with URL: ${shareUrl}`)
})
createLinkShare(context, 'public2').then((shareUrl) => {
secondPublicShareUrl = shareUrl
cy.log(`Created second share with URL: ${shareUrl}`)
})
})
})
beforeEach(() => {
cy.logout()
cy.visit(firstPublicShareUrl)
})
it('See the undefined avatar menu', () => {
cy.get('header')
.findByRole('navigation', { name: /User menu/i })
.should('be.visible')
.findByRole('button', { name: /User menu/i })
.should('be.visible')
.click()
cy.get('#header-menu-public-page-user-menu')
.as('headerMenu')
// Note that current guest user is not identified
cy.get('@headerMenu')
.should('be.visible')
.findByRole('note')
.should('be.visible')
.should('contain', 'not identified')
// Button to set guest name
cy.get('@headerMenu')
.findByRole('link', { name: /Set public name/i })
.should('be.visible')
})
it('Can set public name', () => {
cy.get('header')
.findByRole('navigation', { name: /User menu/i })
.should('be.visible')
.findByRole('button', { name: /User menu/i })
.should('be.visible')
.as('userMenuButton')
// Open the user menu
cy.get('@userMenuButton').click()
cy.get('#header-menu-public-page-user-menu')
.as('headerMenu')
cy.get('@headerMenu')
.findByRole('link', { name: /Set public name/i })
.should('be.visible')
.click()
// Check the dialog is visible
cy.findByRole('dialog', { name: /Guest identification/i })
.should('be.visible')
.as('guestIdentificationDialog')
// Check the note is visible
cy.get('@guestIdentificationDialog')
.findByRole('note')
.should('contain', 'not identified')
// Check the input is visible
cy.get('@guestIdentificationDialog')
.findByRole('textbox', { name: /Name/i })
.should('be.visible')
.type('{selectAll}John Doe{enter}')
// Check that the dialog is closed
cy.get('@guestIdentificationDialog')
.should('not.exist')
// Check that the avatar changed
cy.get('@userMenuButton')
.find('img')
.invoke('attr', 'src')
.should('include', 'avatar/guest/John%20Doe')
})
it('Guest name us persistent and can be changed', () => {
cy.get('header')
.findByRole('navigation', { name: /User menu/i })
.should('be.visible')
.findByRole('button', { name: /User menu/i })
.should('be.visible')
.as('userMenuButton')
// Open the user menu
cy.get('@userMenuButton').click()
cy.get('#header-menu-public-page-user-menu')
.as('headerMenu')
cy.get('@headerMenu')
.findByRole('link', { name: /Set public name/i })
.should('be.visible')
.click()
// Check the dialog is visible
cy.findByRole('dialog', { name: /Guest identification/i })
.should('be.visible')
.as('guestIdentificationDialog')
// Set the name
cy.get('@guestIdentificationDialog')
.findByRole('textbox', { name: /Name/i })
.should('be.visible')
.type('{selectAll}Jane Doe{enter}')
// Check that the dialog is closed
cy.get('@guestIdentificationDialog')
.should('not.exist')
// Create another share
cy.visit(secondPublicShareUrl)
cy.get('header')
.findByRole('navigation', { name: /User menu/i })
.should('be.visible')
.findByRole('button', { name: /User menu/i })
.should('be.visible')
.as('userMenuButton')
// Open the user menu
cy.get('@userMenuButton').click()
cy.get('#header-menu-public-page-user-menu')
.as('headerMenu')
// See the note with the current name
cy.get('@headerMenu')
.findByRole('note')
.should('contain', 'You will be identified as Jane Doe')
cy.get('@headerMenu')
.findByRole('link', { name: /Change public name/i })
.should('be.visible')
.click()
// Check the dialog is visible
cy.findByRole('dialog', { name: /Guest identification/i })
.should('be.visible')
.as('guestIdentificationDialog')
// Check that the note states the current name
// cy.get('@guestIdentificationDialog')
// .findByRole('note')
// .should('contain', 'are currently identified as Jane Doe')
// Change the name
cy.get('@guestIdentificationDialog')
.findByRole('textbox', { name: /Name/i })
.should('be.visible')
.type('{selectAll}Foo Bar{enter}')
// Check that the dialog is closed
cy.get('@guestIdentificationDialog')
.should('not.exist')
// Check that the avatar changed with the second name
cy.get('@userMenuButton')
.find('img')
.invoke('attr', 'src')
.should('include', 'avatar/guest/Foo%20Bar')
})
})

2
cypress/e2e/files_sharing/public-share/header-menu.cy.ts

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { haveValidity, zipFileContains } from '../../../support/utils/assertions.ts'
import { getShareUrl, setupPublicShare } from './setup-public-share.ts'
import { getShareUrl, setupPublicShare } from './PublicShareUtils.ts'
/**
* This tests ensures that on public shares the header actions menu correctly works

2
cypress/e2e/files_sharing/public-share/rename-files.cy.ts

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { getRowForFile, haveValidity, triggerActionForFile } from '../../files/FilesUtils.ts'
import { getShareUrl, setupPublicShare } from './setup-public-share.ts'
import { getShareUrl, setupPublicShare } from './PublicShareUtils.ts'
describe('files_sharing: Public share - renaming files', { testIsolation: true }, () => {

24
cypress/e2e/files_sharing/public-share/required-before-create.cy.ts

@ -3,10 +3,10 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { ShareContext } from './setup-public-share.ts'
import type { ShareContext } from './PublicShareUtils.ts'
import type { ShareOptions } from '../ShareOptionsType.ts'
import { defaultShareOptions } from '../ShareOptionsType.ts'
import { setupData, createShare } from './setup-public-share.ts'
import { setupData, createLinkShare } from './PublicShareUtils.ts'
describe('files_sharing: Before create checks', () => {
@ -49,7 +49,7 @@ describe('files_sharing: Before create checks', () => {
applyShareOptions(shareOptions)
const shareName = 'passwordAndExpireEnforced'
setupData(shareContext.user, shareName)
createShare(shareContext, shareName, shareOptions).then((shareUrl) => {
createLinkShare(shareContext, shareName, shareOptions).then((shareUrl) => {
shareContext.url = shareUrl
cy.log(`Created share with URL: ${shareUrl}`)
})
@ -64,7 +64,7 @@ describe('files_sharing: Before create checks', () => {
applyShareOptions(shareOptions)
const shareName = 'passwordEnforcedDefaultExpire'
setupData(shareContext.user, shareName)
createShare(shareContext, shareName, shareOptions).then((shareUrl) => {
createLinkShare(shareContext, shareName, shareOptions).then((shareUrl) => {
shareContext.url = shareUrl
cy.log(`Created share with URL: ${shareUrl}`)
})
@ -79,7 +79,7 @@ describe('files_sharing: Before create checks', () => {
applyShareOptions(shareOptions)
const shareName = 'defaultPasswordExpireEnforced'
setupData(shareContext.user, shareName)
createShare(shareContext, shareName, shareOptions).then((shareUrl) => {
createLinkShare(shareContext, shareName, shareOptions).then((shareUrl) => {
shareContext.url = shareUrl
cy.log(`Created share with URL: ${shareUrl}`)
})
@ -93,7 +93,7 @@ describe('files_sharing: Before create checks', () => {
applyShareOptions(shareOptions)
const shareName = 'defaultPasswordAndExpire'
setupData(shareContext.user, shareName)
createShare(shareContext, shareName, shareOptions).then((shareUrl) => {
createLinkShare(shareContext, shareName, shareOptions).then((shareUrl) => {
shareContext.url = shareUrl
cy.log(`Created share with URL: ${shareUrl}`)
})
@ -109,7 +109,7 @@ describe('files_sharing: Before create checks', () => {
applyShareOptions(shareOptions)
const shareName = 'passwordEnforcedExpireSetNotEnforced'
setupData(shareContext.user, shareName)
createShare(shareContext, shareName, shareOptions).then((shareUrl) => {
createLinkShare(shareContext, shareName, shareOptions).then((shareUrl) => {
shareContext.url = shareUrl
cy.log(`Created share with URL: ${shareUrl}`)
})
@ -125,7 +125,7 @@ describe('files_sharing: Before create checks', () => {
applyShareOptions(shareOptions)
const shareName = 'defaultPasswordAndExpirationNotEnforced'
setupData(shareContext.user, shareName)
createShare(shareContext, shareName, shareOptions).then((shareUrl) => {
createLinkShare(shareContext, shareName, shareOptions).then((shareUrl) => {
shareContext.url = shareUrl
cy.log(`Created share with URL: ${shareUrl}`)
})
@ -141,7 +141,7 @@ describe('files_sharing: Before create checks', () => {
applyShareOptions(shareOptions)
const shareName = 'noPasswordExpireEnforced'
setupData(shareContext.user, shareName)
createShare(shareContext, shareName, shareOptions).then((shareUrl) => {
createLinkShare(shareContext, shareName, shareOptions).then((shareUrl) => {
shareContext.url = shareUrl
cy.log(`Created share with URL: ${shareUrl}`)
})
@ -157,7 +157,7 @@ describe('files_sharing: Before create checks', () => {
applyShareOptions(shareOptions)
const shareName = 'defaultExpireNoPasswordEnforced'
setupData(shareContext.user, shareName)
createShare(shareContext, shareName, shareOptions).then((shareUrl) => {
createLinkShare(shareContext, shareName, shareOptions).then((shareUrl) => {
shareContext.url = shareUrl
cy.log(`Created share with URL: ${shareUrl}`)
})
@ -173,7 +173,7 @@ describe('files_sharing: Before create checks', () => {
const shareName = 'noPasswordExpireDefault'
setupData(shareContext.user, shareName)
createShare(shareContext, shareName, shareOptions).then((shareUrl) => {
createLinkShare(shareContext, shareName, shareOptions).then((shareUrl) => {
shareContext.url = shareUrl
cy.log(`Created share with URL: ${shareUrl}`)
})
@ -183,7 +183,7 @@ describe('files_sharing: Before create checks', () => {
applyShareOptions()
const shareName = 'noPasswordNoExpireNoDefaults'
setupData(shareContext.user, shareName)
createShare(shareContext, shareName, null).then((shareUrl) => {
createLinkShare(shareContext, shareName, null).then((shareUrl) => {
shareContext.url = shareUrl
cy.log(`Created share with URL: ${shareUrl}`)
})

1
lib/public/AppFramework/Http/Template/PublicTemplateResponse.php

@ -44,6 +44,7 @@ class PublicTemplateResponse extends TemplateResponse {
) {
parent::__construct($appName, $templateName, $params, 'public', $status, $headers);
\OCP\Util::addScript('core', 'public-page-menu');
\OCP\Util::addScript('core', 'public-page-user-menu');
$state = \OCP\Server::get(IInitialStateService::class);
$state->provideLazyInitialState('core', 'public-page-menu', function () {

14
package-lock.json

@ -12,13 +12,13 @@
"@chenfengyuan/vue-qrcode": "^1.0.2",
"@mdi/js": "^7.4.47",
"@mdi/svg": "^7.4.47",
"@nextcloud/auth": "^2.4.0",
"@nextcloud/auth": "^2.5.0",
"@nextcloud/axios": "^2.5.1",
"@nextcloud/browser-storage": "^0.4.0",
"@nextcloud/browserslist-config": "^3.0.1",
"@nextcloud/calendar-availability-vue": "^2.2.6",
"@nextcloud/capabilities": "^1.2.0",
"@nextcloud/dialogs": "^6.3.0",
"@nextcloud/dialogs": "^6.3.1",
"@nextcloud/event-bus": "^3.3.2",
"@nextcloud/files": "^3.10.2",
"@nextcloud/initial-state": "^2.2.0",
@ -3902,19 +3902,19 @@
"license": "MIT"
},
"node_modules/@nextcloud/dialogs": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/@nextcloud/dialogs/-/dialogs-6.3.0.tgz",
"integrity": "sha512-6WbWiBnGKvcj5UCG0raQhhU7fso1bNX1KEH2iN8PKTAGfxtXAD6XQ48HLuPjUtSZgrpm1azc2cAkECA18SXJaA==",
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@nextcloud/dialogs/-/dialogs-6.3.1.tgz",
"integrity": "sha512-lklTssGdphRZKoR07pYU88btqguEKcQjEpKYom342i1eiMPiejgmoPZEignWJvJhpaN9CT5FoGndCrqqS3BswA==",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@mdi/js": "^7.4.47",
"@nextcloud/auth": "^2.5.0",
"@nextcloud/auth": "^2.5.1",
"@nextcloud/axios": "^2.5.1",
"@nextcloud/browser-storage": "^0.4.0",
"@nextcloud/event-bus": "^3.3.2",
"@nextcloud/files": "^3.10.2",
"@nextcloud/initial-state": "^2.2.0",
"@nextcloud/l10n": "^3.2.0",
"@nextcloud/l10n": "^3.3.0",
"@nextcloud/router": "^3.0.1",
"@nextcloud/sharing": "^0.2.4",
"@nextcloud/typings": "^1.9.1",

4
package.json

@ -43,13 +43,13 @@
"@chenfengyuan/vue-qrcode": "^1.0.2",
"@mdi/js": "^7.4.47",
"@mdi/svg": "^7.4.47",
"@nextcloud/auth": "^2.4.0",
"@nextcloud/auth": "^2.5.0",
"@nextcloud/axios": "^2.5.1",
"@nextcloud/browser-storage": "^0.4.0",
"@nextcloud/browserslist-config": "^3.0.1",
"@nextcloud/calendar-availability-vue": "^2.2.6",
"@nextcloud/capabilities": "^1.2.0",
"@nextcloud/dialogs": "^6.3.0",
"@nextcloud/dialogs": "^6.3.1",
"@nextcloud/event-bus": "^3.3.2",
"@nextcloud/files": "^3.10.2",
"@nextcloud/initial-state": "^2.2.0",

3
webpack.modules.js

@ -19,6 +19,7 @@ module.exports = {
main: path.join(__dirname, 'core/src', 'main.js'),
maintenance: path.join(__dirname, 'core/src', 'maintenance.js'),
'public-page-menu': path.resolve(__dirname, 'core/src', 'public-page-menu.ts'),
'public-page-user-menu': path.resolve(__dirname, 'core/src', 'public-page-user-menu.ts'),
recommendedapps: path.join(__dirname, 'core/src', 'recommendedapps.js'),
systemtags: path.resolve(__dirname, 'core/src', 'systemtags/merged-systemtags.js'),
'unified-search': path.join(__dirname, 'core/src', 'unified-search.ts'),
@ -58,7 +59,7 @@ module.exports = {
'init-public': path.join(__dirname, 'apps/files_sharing/src', 'init-public.ts'),
main: path.join(__dirname, 'apps/files_sharing/src', 'main.ts'),
'personal-settings': path.join(__dirname, 'apps/files_sharing/src', 'personal-settings.js'),
'public-file-request': path.join(__dirname, 'apps/files_sharing/src', 'public-file-request.ts'),
'public-nickname-handler': path.join(__dirname, 'apps/files_sharing/src', 'public-nickname-handler.ts'),
},
files_trashbin: {
init: path.join(__dirname, 'apps/files_trashbin/src', 'files-init.ts'),

Loading…
Cancel
Save