From 8f923018d436f2c345270e602301a35ea4bc976d Mon Sep 17 00:00:00 2001 From: Elizabeth Danzberger Date: Wed, 19 Nov 2025 12:40:11 -0500 Subject: [PATCH 1/5] chore: remove new file entry workaround Signed-off-by: Elizabeth Danzberger --- src/public.js | 3 -- src/services/api.js | 17 +------ src/view/NewFileMenu.js | 105 ---------------------------------------- 3 files changed, 1 insertion(+), 124 deletions(-) delete mode 100644 src/view/NewFileMenu.js diff --git a/src/public.js b/src/public.js index 42fba6f9e..2948c71cf 100644 --- a/src/public.js +++ b/src/public.js @@ -10,15 +10,12 @@ import { isDownloadHidden, } from './helpers/index.js' import { getCapabilities } from './services/capabilities.ts' -import { registerNewFileMenuEntries } from './view/NewFileMenu.js' document.addEventListener('DOMContentLoaded', () => { if (!isPublicShare() || !OCA.Viewer) { return } - registerNewFileMenuEntries() - const isEnabledFilesPdfViewer = getCapabilities().mimetypesNoDefaultOpen.includes('application/pdf') if ((isDownloadHidden() || !isEnabledFilesPdfViewer) && isPdf()) { diff --git a/src/services/api.js b/src/services/api.js index 4096919ee..c12c2f140 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -4,22 +4,7 @@ */ import axios from '@nextcloud/axios' -import { generateOcsUrl, generateFilePath } from '@nextcloud/router' -import { getSharingToken } from '@nextcloud/sharing/public' - -export const createEmptyFile = async (context, mimeType, fileName, templateId = null) => { - const shareToken = getSharingToken() - - const response = await axios.post(generateOcsUrl('apps/richdocuments/api/v1/file', 2), { - mimeType, - fileName, - directoryPath: context.dirname, - shareToken, - templateId, - }) - - return response.data -} +import { generateFilePath } from '@nextcloud/router' export const savePersonalSetting = (data) => { return axios.post(generateFilePath('richdocuments', 'ajax', 'personal.php'), data) diff --git a/src/view/NewFileMenu.js b/src/view/NewFileMenu.js deleted file mode 100644 index 4c1775623..000000000 --- a/src/view/NewFileMenu.js +++ /dev/null @@ -1,105 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import axios from '@nextcloud/axios' -import { emit } from '@nextcloud/event-bus' -import Types from '../helpers/types.js' -import { createEmptyFile } from '../services/api.js' -import { generateOcsUrl } from '@nextcloud/router' -import { showError, spawnDialog } from '@nextcloud/dialogs' -import { getCapabilities } from '../services/capabilities.ts' -import { File, addNewFileMenuEntry, isFilenameValid, getUniqueName } from '@nextcloud/files' -import TemplatePicker from '../components/Modal/TemplatePicker.vue' - -const createFileTypeEntry = (type, displayName, iconClass, index) => ({ - id: 'add-' + type.extension, - displayName: t('richdocuments', displayName), - iconClass, - order: 10 + index, - async handler(context, content) { - const filename = t('richdocuments', displayName) + '.' + type.extension - if (getCapabilities().templates) { - await openTemplatePicker(context, type.name, type.mime, filename, content) - } else { - await createDocument(context, type.mime, filename, null, content) - } - }, -}) - -export const registerNewFileMenuEntries = () => { - const fileTypes = [ - { type: Types.getFileType('document'), displayName: 'New document', iconClass: 'icon-filetype-document' }, - { type: Types.getFileType('spreadsheet'), displayName: 'New spreadsheet', iconClass: 'icon-filetype-spreadsheet' }, - { type: Types.getFileType('presentation'), displayName: 'New presentation', iconClass: 'icon-filetype-presentation' }, - { type: Types.getFileType('drawing'), displayName: 'New drawing', iconClass: 'icon-filetype-draw' }, - ] - - fileTypes.forEach(({ type, displayName, iconClass }, index) => { - addNewFileMenuEntry(createFileTypeEntry(type, displayName, iconClass, index)) - }) -} - -const createDocument = async (context, mimetype, filename, templateId = null, content = []) => { - if (!isFilenameValid(filename)) { - return - } - - filename = getUniqueName( - filename, - content.map((n) => n.basename), - ) - - try { - const response = await createEmptyFile(context, mimetype, filename, templateId) - const { data } = response - - const file = new File({ - source: context.source + '/' + filename, - basename: filename, - id: data.id, - mtime: new Date(data.mtime), - mime: data.mimetype, - owner: null, - permissions: data.permissions, - root: context?.root, - }) - emit('files:node:created', file) - - if (response && response.status === 'success') { - OCA.Viewer.open({ path: context.dirname + '/' + filename }) - } else { - showError(t('core', 'Could not create file') + ': ' + response.data.message) - } - } catch (error) { - console.error(error) - showError(t('core', 'Could not create file')) - } -} - -const openTemplatePicker = async (context, type, mimetype, filename, content = []) => { - try { - const { data: response } = await axios.get(generateOcsUrl(`apps/richdocuments/api/v1/templates/${type}`, 2)) - const templates = response.ocs.data - - if (templates.length === 1) { - await createDocument(context, mimetype, filename, templates[0].id, content) - return - } - - await spawnDialog(TemplatePicker, { - templates, - suggestedFilename: filename, - content, - initialTemplateId: templates[0]?.id, - }, (templateId, filename) => { - if (templateId) { - createDocument(context, mimetype, filename, templateId, content) - } - }) - } catch (error) { - console.error(error) - showError(t('core', 'Could not load templates')) - } -} From 71cde2dad9e110554f9837ad15b619fa1bfa506f Mon Sep 17 00:00:00 2001 From: Elizabeth Danzberger Date: Wed, 19 Nov 2025 13:45:49 -0500 Subject: [PATCH 2/5] fix: allow new file menu on public shares Signed-off-by: Elizabeth Danzberger --- lib/Listener/RegisterTemplateFileCreatorListener.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/Listener/RegisterTemplateFileCreatorListener.php b/lib/Listener/RegisterTemplateFileCreatorListener.php index d17b1d208..49bc63b40 100644 --- a/lib/Listener/RegisterTemplateFileCreatorListener.php +++ b/lib/Listener/RegisterTemplateFileCreatorListener.php @@ -34,7 +34,13 @@ class RegisterTemplateFileCreatorListener implements IEventListener { return; } - if (!$this->permissionManager->isEnabledForUser() || !$this->permissionManager->userCanEdit() || empty($this->capabilitiesService->getCapabilities())) { + if (empty($this->capabilitiesService->getCapabilities())) { + return; + } + + $user = $this->permissionManager->loggedInUser(); + $userCanCreate = $this->permissionManager->isEnabledForUser($user) || $this->permissionManager->userCanEdit($user); + if ($user && !$userCanCreate) { return; } From 2006bc30642dbe9cb45cd0a47b3bdecd8b402bdd Mon Sep 17 00:00:00 2001 From: Elizabeth Danzberger Date: Wed, 19 Nov 2025 15:00:23 -0500 Subject: [PATCH 3/5] feat(test): add test coverage Signed-off-by: Elizabeth Danzberger --- cypress/e2e/new.spec.js | 53 +++++++++++++------------------------ cypress/e2e/share-link.js | 33 ++++++++++++++++++++++- cypress/support/commands.js | 14 ++++++++++ 3 files changed, 64 insertions(+), 36 deletions(-) diff --git a/cypress/e2e/new.spec.js b/cypress/e2e/new.spec.js index f68ba52ff..b6b50a0d6 100644 --- a/cypress/e2e/new.spec.js +++ b/cypress/e2e/new.spec.js @@ -2,8 +2,8 @@ * SPDX-FileCopyrightText: 2023 Julius Härtl * SPDX-License-Identifier: AGPL-3.0-or-later */ -// FIXME: Re-renable once 28 has file creation again working -describe.skip('Create new office files', function() { + +describe('New file menu', function() { let randUser before(function() { @@ -18,51 +18,34 @@ describe.skip('Create new office files', function() { }) it('Shows create file entries', function() { - cy.get('.files-controls .button.new') + cy.get('form[data-cy-upload-picker=""]') .should('be.visible') .click() - cy.get('.newFileMenu', { timeout: 10000 }) + cy.get('button[role="menuitem"]') + .contains('New document') .should('be.visible') - .contains('.menuitem', 'New document') - .should('be.visible') - .find('.icon') - .should('have.css', 'background-image') - cy.get('.files-controls .button.new') + cy.get('form[data-cy-upload-picker=""]') .click() - cy.get('.newFileMenu', { timeout: 10000 }) + cy.get('li[data-cy-upload-picker-menu-entry="upload-file"]') .should('not.be.visible') }) - const newFileTypeLabels = [ - 'document', 'spreadsheet', 'presentation', 'diagram', - ] - newFileTypeLabels.forEach((filetype) => { - it('Create empty ' + filetype + ' file', function() { - cy.get('.files-controls .button.new') - .should('be.visible') - .click() + describe('Creates a new file', function() { + const newFileTypeLabels = [ + 'document', 'spreadsheet', 'presentation', 'diagram', + ] + newFileTypeLabels.forEach((filetype) => { + it('Create empty ' + filetype + ' file', function() { + cy.newFileFromMenu(filetype, 'MyNewFile') + cy.waitForViewer() + cy.waitForCollabora() - cy.get('.newFileMenu', { timeout: 10000 }) - .should('be.visible') - .contains('.menuitem', 'New ' + filetype) - .as('menuitem') - .should('be.visible') - .click() + cy.screenshot('new-file-' + filetype) - cy.get('@menuitem').find('.filenameform input[type=text]').type('MyNewFile') - cy.get('@menuitem').find('.filenameform .icon-confirm').click() - - cy.waitForViewer() - cy.waitForCollabora() - - cy.screenshot('new-file-' + filetype) - - cy.get('@loleafletframe').within(() => { - cy.get('#closebutton').click() - cy.waitForViewerClose() + cy.closeDocument() }) }) }) diff --git a/cypress/e2e/share-link.js b/cypress/e2e/share-link.js index 49999bef0..1e8752f74 100644 --- a/cypress/e2e/share-link.js +++ b/cypress/e2e/share-link.js @@ -8,7 +8,7 @@ import { randHash } from '../utils/index.js' const shareOwner = new User(randHash(), randHash()) const otherUser = new User(randHash(), randHash()) -describe.skip('Public sharing of office documents', () => { +describe('Public sharing of office documents', () => { before(function() { cy.nextcloudTestingAppConfigSet('richdocuments', 'doc_format', '') cy.createUser(shareOwner) @@ -105,6 +105,37 @@ describe.skip('Public sharing of office documents', () => { }) }) }) + + describe('New file', () => { + before(() => { + cy.createFolder(shareOwner, '/Shared-Folder') + cy.createFolder(shareOwner, '/Shared-Folder/Subfolder') + }) + + it('Creates a new file in a public share as a guest', () => { + cy.shareLink(shareOwner, '/Shared-Folder', { permissions: 13 }).then((token) => { + cy.logout() + + cy.visit(`/s/${token}`, { + onBeforeLoad(win) { + cy.spy(win, 'postMessage').as('postMessage') + }, + }) + + cy.get('tr[data-cy-files-list-row-name="Subfolder"]') + .should('be.visible') + .click() + + cy.newFileFromMenu('document', 'MyNewFile') + waitForCollabora() + + // Make sure the document is still in the correct subfolder + cy.reload() + cy.get('tr[data-cy-files-list-row-name="MyNewFile.odt"]') + .should('be.visible') + }) + }) + }) }) }) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 0ca453c29..fe7edbc98 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -454,3 +454,17 @@ Cypress.Commands.add('makeTalkRoomPublic', (user, token, password = '') => { }) }) +Cypress.Commands.add('newFileFromMenu', (fileType = 'document', fileName = 'MyNewFile') => { + cy.get('form[data-cy-upload-picker=""]') + .should('be.visible') + .click() + + cy.get('button[role="menuitem"]') + .contains('New ' + fileType) + .should('be.visible') + .click() + + cy.get('input[data-cy-files-new-node-dialog-input=""]') + .should('be.visible') + .type(fileName + '{enter}') +}) From f5688096499be8799a39fb15f32725627c5a00db Mon Sep 17 00:00:00 2001 From: Elizabeth Danzberger Date: Wed, 19 Nov 2025 17:17:43 -0500 Subject: [PATCH 4/5] fix: adjust boolean logic Signed-off-by: Elizabeth Danzberger --- lib/Listener/RegisterTemplateFileCreatorListener.php | 2 +- tests/lib/Listener/RegisterTemplateFileCreatorListenerTest.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/Listener/RegisterTemplateFileCreatorListener.php b/lib/Listener/RegisterTemplateFileCreatorListener.php index 49bc63b40..a7728217b 100644 --- a/lib/Listener/RegisterTemplateFileCreatorListener.php +++ b/lib/Listener/RegisterTemplateFileCreatorListener.php @@ -39,7 +39,7 @@ class RegisterTemplateFileCreatorListener implements IEventListener { } $user = $this->permissionManager->loggedInUser(); - $userCanCreate = $this->permissionManager->isEnabledForUser($user) || $this->permissionManager->userCanEdit($user); + $userCanCreate = $this->permissionManager->isEnabledForUser($user) && $this->permissionManager->userCanEdit($user); if ($user && !$userCanCreate) { return; } diff --git a/tests/lib/Listener/RegisterTemplateFileCreatorListenerTest.php b/tests/lib/Listener/RegisterTemplateFileCreatorListenerTest.php index 65d042c40..9aa55077d 100644 --- a/tests/lib/Listener/RegisterTemplateFileCreatorListenerTest.php +++ b/tests/lib/Listener/RegisterTemplateFileCreatorListenerTest.php @@ -114,6 +114,7 @@ class RegisterTemplateFileCreatorListenerTest extends TestCase { public function testHandleDoesNotRegisterIfUserCannotEdit() { $event = $this->createMock(RegisterTemplateCreatorEvent::class); $event->method('getTemplateManager')->willReturn($this->templateManager); + $this->permissionManager->method('loggedInUser')->willReturn('user'); $this->permissionManager->method('isEnabledForUser')->willReturn(true); $this->permissionManager->method('userCanEdit')->willReturn(false); $this->capabilitiesService->method('getCapabilities')->willReturn(['something']); From c41ba04656079f6f67d7ccd80e2092c955678ce3 Mon Sep 17 00:00:00 2001 From: Elizabeth Danzberger Date: Wed, 19 Nov 2025 17:36:25 -0500 Subject: [PATCH 5/5] fix(test): more specificity Signed-off-by: Elizabeth Danzberger --- cypress/support/commands.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index fe7edbc98..8000036a8 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -455,7 +455,8 @@ Cypress.Commands.add('makeTalkRoomPublic', (user, token, password = '') => { }) Cypress.Commands.add('newFileFromMenu', (fileType = 'document', fileName = 'MyNewFile') => { - cy.get('form[data-cy-upload-picker=""]') + cy.get('div[data-cy-files-content-breadcrumbs=""]') + .find('form[data-cy-upload-picker=""]') .should('be.visible') .click()