From 74f68627661e76ce2315ac1f0ab459469be84583 Mon Sep 17 00:00:00 2001 From: "Grigorii K. Shartsev" Date: Thu, 27 Jun 2024 15:55:04 +0200 Subject: [PATCH 1/4] perf(rich_workspace): only add property for parent Signed-off-by: Grigorii K. Shartsev --- lib/DAV/WorkspacePlugin.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/DAV/WorkspacePlugin.php b/lib/DAV/WorkspacePlugin.php index b7cd454bf1b..c115ab83c61 100644 --- a/lib/DAV/WorkspacePlugin.php +++ b/lib/DAV/WorkspacePlugin.php @@ -78,6 +78,12 @@ public function propFind(PropFind $propFind, INode $node) { return; } + // Only return the property for the parent node and ignore it for further in depth nodes + // Otherwise requesting parent with description files for all the children makes a huge performance impact for external storages children + if ($propFind->getDepth() !== $this->server->getHTTPDepth()) { + return; + } + $node = $node->getNode(); try { $file = $this->workspaceService->getFile($node); @@ -85,7 +91,6 @@ public function propFind(PropFind $propFind, INode $node) { $file = null; } - // Only return the property for the parent node and ignore it for further in depth nodes $propFind->handle(self::WORKSPACE_PROPERTY, function () use ($file) { $cachedContent = ''; if ($file instanceof File) { From 0517913fd3b3e44f9c7cb8dd587bf189cc270b0e Mon Sep 17 00:00:00 2001 From: "Grigorii K. Shartsev" Date: Fri, 28 Jun 2024 14:21:22 +0200 Subject: [PATCH 2/4] test(rich_workspace): never add rich workspace property to nesteds Signed-off-by: Grigorii K. Shartsev --- cypress/e2e/propfind.spec.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cypress/e2e/propfind.spec.js b/cypress/e2e/propfind.spec.js index 7b53b9308bf..04a83d6a303 100644 --- a/cypress/e2e/propfind.spec.js +++ b/cypress/e2e/propfind.spec.js @@ -40,8 +40,7 @@ describe('Text PROPFIND extension ', function() { .should('have.property', richWorkspace, '') }) - // Android app relies on this when navigating nested folders - it('adds rich workspace property to nested folders', function() { + it('never adds rich workspace property to nested folders', function() { cy.createFolder('/workspace') // FIXME: Ideally we do not need a page context for those tests at all // For now the dashboard avoids that we have failing requests due to conflicts when updating the file @@ -52,9 +51,8 @@ describe('Text PROPFIND extension ', function() { cy.uploadFile('test.md', 'text/markdown', '/workspace/Readme.md') cy.propfindFolder('/', 1) .then(results => results.pop().propStat[0].properties) - .should('have.property', richWorkspace, '## Hello world\n') + .should('not.have.property', richWorkspace) }) - }) describe('with workspaces disabled', function() { From be1604f8a14af49fe3d6e7aa76e78994c1d57fd3 Mon Sep 17 00:00:00 2001 From: Julius Knorr Date: Wed, 9 Apr 2025 22:51:02 +0200 Subject: [PATCH 3/4] fix: Add extra property to fetch only the top most workspace Signed-off-by: Julius Knorr --- cypress/e2e/propfind.spec.js | 60 +++++++++++++++++++++++++----------- cypress/support/commands.js | 16 +++------- lib/DAV/WorkspacePlugin.php | 41 ++++++++++++++++-------- src/helpers/files.js | 10 +++--- src/init.js | 4 +-- 5 files changed, 83 insertions(+), 48 deletions(-) diff --git a/cypress/e2e/propfind.spec.js b/cypress/e2e/propfind.spec.js index 04a83d6a303..65139bacb0e 100644 --- a/cypress/e2e/propfind.spec.js +++ b/cypress/e2e/propfind.spec.js @@ -8,7 +8,10 @@ import { randUser } from '../utils/index.js' const user = randUser() describe('Text PROPFIND extension ', function() { - const richWorkspace = '{http://nextcloud.org/ns}rich-workspace' + const PROPERTY_WORKSPACE = '{http://nextcloud.org/ns}rich-workspace' + const PROPERTY_WORKSPACE_FILE = '{http://nextcloud.org/ns}rich-workspace-file' + const PROPERTY_WORKSPACE_FLAT = '{http://nextcloud.org/ns}rich-workspace-flat' + const PROPERTY_WORKSPACE_FILE_FLAT = '{http://nextcloud.org/ns}rich-workspace-file-flat' before(function() { cy.createUser(user) @@ -24,34 +27,55 @@ describe('Text PROPFIND extension ', function() { cy.configureText('workspace_enabled', 1) }) - // Android app relies on this to detect rich workspace availability it('always adds rich workspace property', function() { + const properties = [PROPERTY_WORKSPACE_FLAT, PROPERTY_WORKSPACE_FILE_FLAT] cy.uploadFile('empty.md', 'text/markdown', '/Readme.md') // FIXME: Ideally we do not need a page context for those tests at all // For now the dashboard avoids that we have failing requests due to conflicts when updating the file cy.visit('/apps/dashboard') - cy.propfindFolder('/') - .should('have.property', richWorkspace, '') + cy.propfindFolder('/', 0, properties) + .should('have.property', PROPERTY_WORKSPACE_FLAT, '') cy.uploadFile('test.md', 'text/markdown', '/Readme.md') - cy.propfindFolder('/') - .should('have.property', richWorkspace, '## Hello world\n') + cy.propfindFolder('/', 0, properties) + .should('have.property', PROPERTY_WORKSPACE_FLAT, '## Hello world\n') cy.deleteFile('/Readme.md') - cy.propfindFolder('/') - .should('have.property', richWorkspace, '') + cy.propfindFolder('/', 0, properties) + .should('have.property', PROPERTY_WORKSPACE_FLAT, '') }) - it('never adds rich workspace property to nested folders', function() { + it('never adds rich workspace property to nested folders for flat properties', function() { + const properties = [PROPERTY_WORKSPACE_FLAT, PROPERTY_WORKSPACE_FILE_FLAT] + // FIXME: Ideally we do not need a page context for those tests at all + // For now the dashboard avoids that we have failing requests due to conflicts when updating the file + cy.visit('/apps/dashboard') + cy.createFolder('/workspace-flat') + cy.propfindFolder('/', 1, properties) + .then(results => results.pop().propStat[0].properties) + .should('have.property', PROPERTY_WORKSPACE_FLAT, '') + cy.uploadFile('test.md', 'text/markdown', '/workspace-flat/Readme.md') + cy.propfindFolder('/', 1, properties) + .then(results => results.pop().propStat[0].properties) + .should('not.have.property', PROPERTY_WORKSPACE_FLAT) + cy.deleteFile('/workspace-flat/Readme.md') + cy.propfindFolder('/', 1, properties) + .then(results => results.pop().propStat[0].properties) + .should('have.property', PROPERTY_WORKSPACE_FLAT, '') + }) + + // Android app relies on this to detect rich workspace availability in subfolders properly + it('adds rich workspace property to nested folders for the default properties', function() { + const properties = [PROPERTY_WORKSPACE, PROPERTY_WORKSPACE_FILE] cy.createFolder('/workspace') // FIXME: Ideally we do not need a page context for those tests at all // For now the dashboard avoids that we have failing requests due to conflicts when updating the file cy.visit('/apps/dashboard') - cy.propfindFolder('/', 1) + cy.propfindFolder('/', 1, properties) .then(results => results.pop().propStat[0].properties) - .should('have.property', richWorkspace, '') + .should('have.property', PROPERTY_WORKSPACE, '') cy.uploadFile('test.md', 'text/markdown', '/workspace/Readme.md') - cy.propfindFolder('/', 1) + cy.propfindFolder('/', 1, properties) .then(results => results.pop().propStat[0].properties) - .should('not.have.property', richWorkspace) + .should('not.property', PROPERTY_WORKSPACE, 'Hello world\n') }) }) @@ -65,15 +89,15 @@ describe('Text PROPFIND extension ', function() { // FIXME: Ideally we do not need a page context for those tests at all // For now the dashboard avoids that we have failing requests due to conflicts when updating the file cy.visit('/apps/dashboard') - cy.propfindFolder('/') - .should('not.have.property', richWorkspace) + cy.propfindFolder('/', 1, [PROPERTY_WORKSPACE_FLAT, PROPERTY_WORKSPACE_FILE_FLAT]) + .should('not.have.property', PROPERTY_WORKSPACE_FLAT) cy.uploadFile('test.md', 'text/markdown', '/Readme.md') - cy.propfindFolder('/') - .should('not.have.property', richWorkspace) + cy.propfindFolder('/', 1, [PROPERTY_WORKSPACE_FLAT, PROPERTY_WORKSPACE_FILE_FLAT]) + .should('not.have.property', PROPERTY_WORKSPACE_FLAT) cy.createFolder('/without-workspace') cy.propfindFolder('/', 1) .then(results => results.pop().propStat[0].properties) - .should('not.have.property', richWorkspace) + .should('not.have.property', PROPERTY_WORKSPACE_FLAT) }) }) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 2b3df5d4a29..e986edbbaab 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -225,21 +225,15 @@ Cypress.Commands.add('getFileContent', (path) => { .then(response => response.data) }) -Cypress.Commands.add('propfindFolder', (path, depth = 0) => { +Cypress.Commands.add('propfindFolder', (path, depth = 0, properties = []) => { return cy.window(silent) .then(win => { const files = win.OC.Files - const PROPERTY_WORKSPACE_FILE - = `{${files.Client.NS_NEXTCLOUD}}rich-workspace-file` - const PROPERTY_WORKSPACE - = `{${files.Client.NS_NEXTCLOUD}}rich-workspace` - const properties = [ - ...files.getClient().getPropfindProperties(), - PROPERTY_WORKSPACE_FILE, - PROPERTY_WORKSPACE, - ] const client = files.getClient().getClient() - return client.propFind(client.baseUrl + path, properties, depth) + return client.propFind(client.baseUrl + path, [ + ...properties, + ...files.getClient().getPropfindProperties(), + ], depth) .then((results) => { cy.log(`Propfind returned ${results.status}`) if (depth) { diff --git a/lib/DAV/WorkspacePlugin.php b/lib/DAV/WorkspacePlugin.php index c115ab83c61..a89c2d78087 100644 --- a/lib/DAV/WorkspacePlugin.php +++ b/lib/DAV/WorkspacePlugin.php @@ -29,9 +29,10 @@ class WorkspacePlugin extends ServerPlugin { public const WORKSPACE_PROPERTY = '{http://nextcloud.org/ns}rich-workspace'; public const WORKSPACE_FILE_PROPERTY = '{http://nextcloud.org/ns}rich-workspace-file'; + public const WORKSPACE_PROPERTY_FLAT = '{http://nextcloud.org/ns}rich-workspace-flat'; + public const WORKSPACE_FILE_PROPERTY_FLAT = '{http://nextcloud.org/ns}rich-workspace-file-flat'; - /** @var Server */ - private $server; + private Server $server; public function __construct( private WorkspaceService $workspaceService, @@ -62,8 +63,12 @@ public function initialize(Server $server) { public function propFind(PropFind $propFind, INode $node) { - if (!in_array(self::WORKSPACE_PROPERTY, $propFind->getRequestedProperties()) - && !in_array(self::WORKSPACE_FILE_PROPERTY, $propFind->getRequestedProperties())) { + if (!array_intersect([ + self::WORKSPACE_PROPERTY, + self::WORKSPACE_FILE_PROPERTY, + self::WORKSPACE_PROPERTY_FLAT, + self::WORKSPACE_FILE_PROPERTY_FLAT + ], $propFind->getRequestedProperties())) { return; } @@ -78,9 +83,16 @@ public function propFind(PropFind $propFind, INode $node) { return; } - // Only return the property for the parent node and ignore it for further in depth nodes - // Otherwise requesting parent with description files for all the children makes a huge performance impact for external storages children - if ($propFind->getDepth() !== $this->server->getHTTPDepth()) { + $shouldFetchChildren = array_intersect([ + self::WORKSPACE_PROPERTY, + self::WORKSPACE_FILE_PROPERTY, + ], $propFind->getRequestedProperties()); + + // In most cases we only need the workspace property for the root node + // So we can skip the propFind for further nodes for performance reasons + // Fetching the workspace property for all children is still required for mobile apps + + if ($propFind->getDepth() !== $this->server->getHTTPDepth() && !$shouldFetchChildren) { return; } @@ -90,8 +102,7 @@ public function propFind(PropFind $propFind, INode $node) { } catch (\Exception $e) { $file = null; } - - $propFind->handle(self::WORKSPACE_PROPERTY, function () use ($file) { + $workspaceContentCallback = function () use ($file) { $cachedContent = ''; if ($file instanceof File) { $cache = $this->cacheFactory->createDistributed('text_workspace'); @@ -115,12 +126,18 @@ public function propFind(PropFind $propFind, INode $node) { } } return $cachedContent; - }); - $propFind->handle(self::WORKSPACE_FILE_PROPERTY, function () use ($file) { + }; + + $workspaceFileCallback = function () use ($file) { if ($file instanceof File) { return $file->getFileInfo()->getId(); } return ''; - }); + }; + + $propFind->handle(self::WORKSPACE_PROPERTY, $workspaceContentCallback); + $propFind->handle(self::WORKSPACE_PROPERTY_FLAT, $workspaceContentCallback); + $propFind->handle(self::WORKSPACE_FILE_PROPERTY, $workspaceFileCallback); + $propFind->handle(self::WORKSPACE_FILE_PROPERTY_FLAT, $workspaceFileCallback); } } diff --git a/src/helpers/files.js b/src/helpers/files.js index 141df4cfa4a..06c637792f0 100644 --- a/src/helpers/files.js +++ b/src/helpers/files.js @@ -119,7 +119,7 @@ export const addMenuRichWorkspace = () => { displayName: t('text', 'Add folder description'), category: NewMenuEntryCategory.Other, enabled(context) { - if (Number(context.attributes['rich-workspace-file'])) { + if (Number(context.attributes['rich-workspace-file-flat'])) { return false } return (context.permissions & Permission.CREATE) !== 0 @@ -180,9 +180,9 @@ export const FilesWorkspaceHeader = new Header({ vm.$destroy() vm = null } - const hasRichWorkspace = !!folder.attributes['rich-workspace-file'] || !!newWorkspaceCreated + const hasRichWorkspace = !!folder.attributes['rich-workspace-file-flat'] || !!newWorkspaceCreated const path = newWorkspaceCreated ? dirname(newWorkspaceCreated.path) : folder.path - const content = newWorkspaceCreated ? '' : folder.attributes['rich-workspace'] + const content = newWorkspaceCreated ? '' : folder.attributes['rich-workspace-flat'] newWorkspaceCreated = false @@ -220,10 +220,10 @@ export const FilesWorkspaceHeader = new Header({ // removing the rendered element from the DOM // This is only relevant if switching to a folder that has no content as then the render function is not called - const hasRichWorkspace = !!folder.attributes['rich-workspace-file'] + const hasRichWorkspace = !!folder.attributes['rich-workspace-file-flat'] vm.path = folder.path vm.hasRichWorkspace = hasRichWorkspace - vm.content = folder.attributes['rich-workspace'] + vm.content = folder.attributes['rich-workspace-flat'] }, }) diff --git a/src/init.js b/src/init.js index 087df50b2b4..acbb1f53361 100644 --- a/src/init.js +++ b/src/init.js @@ -11,8 +11,8 @@ import 'vite/modulepreload-polyfill' const workspaceAvailable = loadState('text', 'workspace_available') -registerDavProperty('nc:rich-workspace', { nc: 'http://nextcloud.org/ns' }) -registerDavProperty('nc:rich-workspace-file', { nc: 'http://nextcloud.org/ns' }) +registerDavProperty('nc:rich-workspace-flat', { nc: 'http://nextcloud.org/ns' }) +registerDavProperty('nc:rich-workspace-file-flat', { nc: 'http://nextcloud.org/ns' }) if (workspaceAvailable) { addMenuRichWorkspace() From 56a6ac4ef9e0e78b9dba3fc7769f85afd16d4a6e Mon Sep 17 00:00:00 2001 From: Benjamin Frueh Date: Mon, 20 Apr 2026 23:32:21 +0200 Subject: [PATCH 4/4] chore: conflict resolution to apply the changes on main Signed-off-by: Benjamin Frueh --- cypress/e2e/propfind.spec.js | 77 +++--- cypress/support/commands.js | 516 ++++++++++++++++++++--------------- src/helpers/files.js | 243 ++++++----------- 3 files changed, 411 insertions(+), 425 deletions(-) diff --git a/cypress/e2e/propfind.spec.js b/cypress/e2e/propfind.spec.js index 65139bacb0e..a2d46e6ad77 100644 --- a/cypress/e2e/propfind.spec.js +++ b/cypress/e2e/propfind.spec.js @@ -7,98 +7,85 @@ import { randUser } from '../utils/index.js' const user = randUser() -describe('Text PROPFIND extension ', function() { - const PROPERTY_WORKSPACE = '{http://nextcloud.org/ns}rich-workspace' - const PROPERTY_WORKSPACE_FILE = '{http://nextcloud.org/ns}rich-workspace-file' - const PROPERTY_WORKSPACE_FLAT = '{http://nextcloud.org/ns}rich-workspace-flat' - const PROPERTY_WORKSPACE_FILE_FLAT = '{http://nextcloud.org/ns}rich-workspace-file-flat' +// Retries fail because folders / files already exist. +describe('Text PROPFIND extension ', { retries: 0 }, function () { + const PROPERTY_WORKSPACE = 'nc:rich-workspace' + const PROPERTY_WORKSPACE_FLAT = 'nc:rich-workspace-flat' - before(function() { + before(function () { cy.createUser(user) }) - beforeEach(function() { + beforeEach(function () { cy.login(user) }) - describe('with workspaces enabled', function() { - - beforeEach(function() { + describe('with workspaces enabled', function () { + beforeEach(function () { cy.configureText('workspace_enabled', 1) }) - it('always adds rich workspace property', function() { - const properties = [PROPERTY_WORKSPACE_FLAT, PROPERTY_WORKSPACE_FILE_FLAT] + it('always adds rich workspace property', function () { cy.uploadFile('empty.md', 'text/markdown', '/Readme.md') - // FIXME: Ideally we do not need a page context for those tests at all - // For now the dashboard avoids that we have failing requests due to conflicts when updating the file cy.visit('/apps/dashboard') - cy.propfindFolder('/', 0, properties) + cy.propfindFolder('/', 0) .should('have.property', PROPERTY_WORKSPACE_FLAT, '') cy.uploadFile('test.md', 'text/markdown', '/Readme.md') - cy.propfindFolder('/', 0, properties) + cy.propfindFolder('/', 0) .should('have.property', PROPERTY_WORKSPACE_FLAT, '## Hello world\n') cy.deleteFile('/Readme.md') - cy.propfindFolder('/', 0, properties) + cy.propfindFolder('/', 0) .should('have.property', PROPERTY_WORKSPACE_FLAT, '') }) - it('never adds rich workspace property to nested folders for flat properties', function() { - const properties = [PROPERTY_WORKSPACE_FLAT, PROPERTY_WORKSPACE_FILE_FLAT] - // FIXME: Ideally we do not need a page context for those tests at all - // For now the dashboard avoids that we have failing requests due to conflicts when updating the file + it('never adds rich workspace property to nested folders for flat properties', function () { cy.visit('/apps/dashboard') cy.createFolder('/workspace-flat') - cy.propfindFolder('/', 1, properties) - .then(results => results.pop().propStat[0].properties) + cy.propfindFolder('/', 1) + .then((results) => results.pop().propStat[0].properties) .should('have.property', PROPERTY_WORKSPACE_FLAT, '') cy.uploadFile('test.md', 'text/markdown', '/workspace-flat/Readme.md') - cy.propfindFolder('/', 1, properties) - .then(results => results.pop().propStat[0].properties) + cy.propfindFolder('/', 1) + .then((results) => results.pop().propStat[0].properties) .should('not.have.property', PROPERTY_WORKSPACE_FLAT) cy.deleteFile('/workspace-flat/Readme.md') - cy.propfindFolder('/', 1, properties) - .then(results => results.pop().propStat[0].properties) + cy.propfindFolder('/', 1) + .then((results) => results.pop().propStat[0].properties) .should('have.property', PROPERTY_WORKSPACE_FLAT, '') }) // Android app relies on this to detect rich workspace availability in subfolders properly - it('adds rich workspace property to nested folders for the default properties', function() { - const properties = [PROPERTY_WORKSPACE, PROPERTY_WORKSPACE_FILE] + it('adds rich workspace property to nested folders for the default properties', function () { cy.createFolder('/workspace') - // FIXME: Ideally we do not need a page context for those tests at all - // For now the dashboard avoids that we have failing requests due to conflicts when updating the file cy.visit('/apps/dashboard') - cy.propfindFolder('/', 1, properties) - .then(results => results.pop().propStat[0].properties) + cy.propfindFolder('/', 1) + .then((results) => results.pop().propStat[0].properties) .should('have.property', PROPERTY_WORKSPACE, '') cy.uploadFile('test.md', 'text/markdown', '/workspace/Readme.md') - cy.propfindFolder('/', 1, properties) - .then(results => results.pop().propStat[0].properties) - .should('not.property', PROPERTY_WORKSPACE, 'Hello world\n') + cy.propfindFolder('/', 1) + .then((results) => results.pop().propStat[0].properties) + .should('have.property', PROPERTY_WORKSPACE, '## Hello world\n') }) }) - describe('with workspaces disabled', function() { - - beforeEach(function() { + describe('with workspaces disabled', function () { + beforeEach(function () { cy.configureText('workspace_enabled', 0) }) - it('does not return a rich workspace property', function() { + it('does not return a rich workspace property', function () { // FIXME: Ideally we do not need a page context for those tests at all // For now the dashboard avoids that we have failing requests due to conflicts when updating the file cy.visit('/apps/dashboard') - cy.propfindFolder('/', 1, [PROPERTY_WORKSPACE_FLAT, PROPERTY_WORKSPACE_FILE_FLAT]) + cy.propfindFolder('/', 1) .should('not.have.property', PROPERTY_WORKSPACE_FLAT) cy.uploadFile('test.md', 'text/markdown', '/Readme.md') - cy.propfindFolder('/', 1, [PROPERTY_WORKSPACE_FLAT, PROPERTY_WORKSPACE_FILE_FLAT]) + cy.propfindFolder('/', 1) .should('not.have.property', PROPERTY_WORKSPACE_FLAT) cy.createFolder('/without-workspace') cy.propfindFolder('/', 1) - .then(results => results.pop().propStat[0].properties) + .then((results) => results.pop().propStat[0].properties) .should('not.have.property', PROPERTY_WORKSPACE_FLAT) }) - }) -}) +}) \ No newline at end of file diff --git a/cypress/support/commands.js b/cypress/support/commands.js index e986edbbaab..3abdd38157e 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -4,14 +4,8 @@ */ import axios from '@nextcloud/axios' -import { emit } from '@nextcloud/event-bus' import { addCommands } from '@nextcloud/e2e-test-server/cypress' -import { addCompareSnapshotCommand } from 'cypress-visual-regression/dist/command' - -// eslint-disable-next-line no-unused-vars,n/no-extraneous-import -import regeneratorRuntime from 'regenerator-runtime' - -addCompareSnapshotCommand({ capture: 'fullPage' }) +import { emit } from '@nextcloud/event-bus' const url = Cypress.config('baseUrl').replace(/\/index.php\/?$/g, '') Cypress.env('baseUrl', url) @@ -33,8 +27,9 @@ Cypress.Commands.overwrite('login', (login, user) => { win.location.href = 'about:blank' }) login(user) - cy.request('/csrftoken', silent) - .then(({ body }) => emit('csrf-token-update', body)) + cy.request('/csrftoken', silent).then(({ body }) => + emit('csrf-token-update', body), + ) cy.wrap(user, silent).as('currentUser') }) @@ -50,21 +45,28 @@ Cypress.Commands.add('openDirectEditingToken', (token) => { }) Cypress.Commands.add('uploadFile', (fileName, mimeType, target) => { - return cy.fixture(fileName, 'binary') + return cy + .fixture(fileName, 'binary') .then(Cypress.Blob.binaryStringToBlob) - .then(blob => { + .then((blob) => { if (typeof target !== 'undefined') { fileName = target } - return axios.put( - `${url}/remote.php/webdav/${fileName}`, - blob.size > 0 ? blob : '', - { headers: { 'Content-Type': mimeType } }, - ).then(response => { - const fileId = Number(response.headers['oc-fileid']?.split('oc')?.[0]) - Cypress.log({ message: `"${target}" (${response.status}): ${fileId}` }) - return cy.wrap(fileId, silent) - }) + return axios + .put( + `${url}/remote.php/webdav/${fileName}`, + blob.size > 0 ? blob : '', + { headers: { 'Content-Type': mimeType } }, + ) + .then((response) => { + const fileId = Number( + response.headers['oc-fileid']?.split('oc')?.[0], + ) + Cypress.log({ + message: `"${target}" (${response.status}): ${fileId}`, + }) + return cy.wrap(fileId, silent) + }) }) }) @@ -72,42 +74,43 @@ Cypress.Commands.add('downloadFile', (fileName) => { return axios.get(`${url}/remote.php/webdav/${fileName}`) }) -Cypress.Commands.add('createFile', (target, content, mimeType = 'text/markdown', headers = {}) => { - const blob = new Blob([content], { type: mimeType }) - return axios.put( - `${url}/remote.php/webdav/${target}`, - blob.size > 0 ? blob : '', - { - headers: { 'Content-Type': mimeType, ...headers }, - }, - ).then((response) => { - const fileId = Number(response.headers['oc-fileid']?.split('oc')?.[0]) - Cypress.log({ message: `"${target}" (${response.status}): ${fileId}` }) - return fileId - }) -}) +Cypress.Commands.add( + 'createFile', + (target, content, mimeType = 'text/markdown', headers = {}) => { + const blob = new Blob([content], { type: mimeType }) + return axios + .put(`${url}/remote.php/webdav/${target}`, blob.size > 0 ? blob : '', { + headers: { 'Content-Type': mimeType, ...headers }, + }) + .then((response) => { + const fileId = Number( + response.headers['oc-fileid']?.split('oc')?.[0], + ) + Cypress.log({ + message: `"${target}" (${response.status}): ${fileId}`, + }) + return fileId + }) + }, +) Cypress.Commands.add('createMarkdown', (fileName, content, reload = true) => { - return cy.createFile(fileName, content, 'text/markdown') - .then((fileId) => { - if (reload) { - cy.reloadFileList() - } - return cy.wrap(fileId) - }) + return cy.createFile(fileName, content, 'text/markdown').then((fileId) => { + if (reload) { + cy.reloadFileList() + } + return cy.wrap(fileId) + }) }) Cypress.Commands.add('shareFileToUser', (path, targetUser, shareData = {}) => { Cypress.log() - return axios.post( - `${url}/ocs/v2.php/apps/files_sharing/api/v1/shares`, - { - path, - shareType: 0, - shareWith: targetUser.userId, - ...shareData, - }, - ) + return axios.post(`${url}/ocs/v2.php/apps/files_sharing/api/v1/shares`, { + path, + shareType: 0, + shareWith: targetUser.userId, + ...shareData, + }) }) Cypress.Commands.add('testName', () => { @@ -119,46 +122,55 @@ Cypress.Commands.add('testName', () => { }) Cypress.Commands.add('createTestFolder', () => { - return cy.testName().then(folderName => { + return cy.testName().then((folderName) => { cy.createFolder(folderName) return cy.wrap(folderName) }) }) Cypress.Commands.add('visitTestFolder', (visitOptions = {}) => { - return cy.testName().then(folderName => { + return cy.testName().then((folderName) => { const url = `apps/files?dir=/${encodeURIComponent(folderName)}` return cy.visit(url, visitOptions) }) }) Cypress.Commands.add('uploadTestFile', (source = 'empty.md') => { - return cy.testName().then(name => { + return cy.testName().then((name) => { return cy.uploadFile(source, 'text/markdown', `${name}.md`) }) }) Cypress.Commands.add('openTestFile', () => { - return cy.testName().then(name => cy.openFile(`${name}.md`)) -}) - -Cypress.Commands.add('isolateTest', ({ sourceFile = 'empty.md', targetFile = null, onBeforeLoad } = {}) => { - targetFile = targetFile || sourceFile - cy.createTestFolder().then(folderName => { - cy.uploadFile(sourceFile, 'text/markdown', `${encodeURIComponent(folderName)}/${targetFile}`) - window.__currentDirectory = folderName - return cy.visitTestFolder({ onBeforeLoad }) - .then(() => ({ folderName, fileName: targetFile })) - }) -}) + return cy.testName().then((name) => cy.openFile(`${name}.md`)) +}) + +Cypress.Commands.add( + 'isolateTest', + ({ sourceFile = 'empty.md', targetFile = null, onBeforeLoad } = {}) => { + targetFile = targetFile || sourceFile + cy.createTestFolder().then((folderName) => { + cy.uploadFile( + sourceFile, + 'text/markdown', + `${encodeURIComponent(folderName)}/${targetFile}`, + ) + window.__currentDirectory = folderName + return cy + .visitTestFolder({ onBeforeLoad }) + .then(() => ({ folderName, fileName: targetFile })) + }) + }, +) Cypress.Commands.add('shareFile', (path, options = {}) => { const shareType = window.OC?.Share?.SHARE_TYPE_LINK ?? 3 - return axios.post( - `${url}/ocs/v2.php/apps/files_sharing/api/v1/shares`, - { path, shareType }, - ) - .then(response => response.data.ocs.data) + return axios + .post(`${url}/ocs/v2.php/apps/files_sharing/api/v1/shares`, { + path, + shareType, + }) + .then((response) => response.data.ocs.data) .then(({ token, id }) => { Cypress.log({ message: `"${path}" (${id}): ${token}` }) if (!options.edit) { @@ -167,38 +179,39 @@ Cypress.Commands.add('shareFile', (path, options = {}) => { // Same permissions makeing the share editable in the UI would set // 1 = read; 2 = write; 16 = share; const permissions = 19 - return axios.put( - `${url}/ocs/v2.php/apps/files_sharing/api/v1/shares/${id}`, - { permissions }, - ).then(() => token) + return axios + .put(`${url}/ocs/v2.php/apps/files_sharing/api/v1/shares/${id}`, { + permissions, + }) + .then(() => token) }) }) Cypress.Commands.add('createFolder', (target) => { - cy.get('@currentUser', silent).then(({ userId }) => { - const rootPath = `${url}/remote.php/dav/files/${encodeURIComponent(userId)}` - const dirPath = target.split('/').map(encodeURIComponent).join('/') - return axios.request( - `${rootPath}/${dirPath}`, - { method: 'MKCOL' }, - ) - }).then((response) => parseInt(response.headers['oc-fileid'])) + cy.get('@currentUser', silent) + .then(({ userId }) => { + const rootPath = `${url}/remote.php/dav/files/${encodeURIComponent(userId)}` + const dirPath = target.split('/').map(encodeURIComponent).join('/') + return axios.request(`${rootPath}/${dirPath}`, { method: 'MKCOL' }) + }) + .then((response) => parseInt(response.headers['oc-fileid'])) }) Cypress.Commands.add('moveFile', (path, destinationPath) => { - return axios.request({ - method: 'MOVE', - url: `${url}/remote.php/webdav/${path}`, - headers: { - Destination: `${url}/remote.php/webdav/${destinationPath}`, - }, - }).then(response => response.body) + return axios + .request({ + method: 'MOVE', + url: `${url}/remote.php/webdav/${path}`, + headers: { + Destination: `${url}/remote.php/webdav/${destinationPath}`, + }, + }) + .then((response) => response.body) }) // For files wait for preview to load and release lock -Cypress.Commands.add('waitForPreview', name => { - cy.getFile(name) - .scrollIntoView({ offset: { top: -200 } }) +Cypress.Commands.add('waitForPreview', (name) => { + cy.getFile(name).scrollIntoView({ offset: { top: -200 } }) cy.getFile(name) .find('.files-list__row-icon img') .should('be.visible') @@ -211,64 +224,114 @@ Cypress.Commands.add('deleteFile', (path) => { }) Cypress.Commands.add('copyFile', (path, destinationPath) => { - return axios.request({ - method: 'COPY', - url: `${url}/remote.php/webdav/${path}`, - headers: { - Destination: `${url}/remote.php/webdav/${destinationPath}`, - }, - }).then(response => response.body) + return axios + .request({ + method: 'COPY', + url: `${url}/remote.php/webdav/${path}`, + headers: { + Destination: `${url}/remote.php/webdav/${destinationPath}`, + }, + }) + .then((response) => response.body) }) Cypress.Commands.add('getFileContent', (path) => { - return axios.get(`${url}/remote.php/webdav/${path}`) - .then(response => response.data) -}) - -Cypress.Commands.add('propfindFolder', (path, depth = 0, properties = []) => { - return cy.window(silent) - .then(win => { - const files = win.OC.Files - const client = files.getClient().getClient() - return client.propFind(client.baseUrl + path, [ - ...properties, - ...files.getClient().getPropfindProperties(), - ], depth) - .then((results) => { - cy.log(`Propfind returned ${results.status}`) - if (depth) { - return results.body - } else { - return results.body.propStat[0].properties + return axios + .get(`${url}/remote.php/webdav/${path}`) + .then((response) => response.data) +}) + +Cypress.Commands.add('propfindFolder', (path, depth = 0) => { + const rootPath = `${url}/remote.php/webdav/` + const requestPath = path === '/' ? rootPath : `${rootPath}${path}` + + return axios + .request({ + method: 'PROPFIND', + url: requestPath, + headers: { + Depth: depth, + 'Content-Type': 'application/xml', + }, + data: ` + + + + + + + +`, + }) + .then((response) => { + const parser = new DOMParser() + const xmlDoc = parser.parseFromString(response.data, 'text/xml') + const responses = xmlDoc.querySelectorAll('d\\:response, response') + const results = Array.from(responses).map((resp) => { + const props = {} + const propStats = resp.querySelectorAll('d\\:propstat, propstat') + propStats.forEach((propStat) => { + const status = + propStat.querySelector('d\\:status, status')?.textContent + + // Skip properties with 404 status ( not found) + if (status?.includes('404')) { + return } + + const propElements = resp.querySelectorAll( + 'd\\:prop > *, prop > * ', + ) + + propElements.forEach((prop) => { + const tagName = prop.localName + const namespace = prop.namespaceURI + + let key = tagName + if (namespace === 'http://nextcloud.org/ns') { + key = `nc:${tagName}` + } else if (namespace === 'http://owncloud.org/ns') { + key = `oc:${tagName}` + } + + props[key] = prop.textContent || '' + }) }) + return props + }) + + return depth > 0 ? results : results[0] || {} }) }) Cypress.Commands.add('reloadFileList', () => { - return cy.get('.vue-crumb:last-child a').click({ force: true }) + cy.get('[title="Reload current directory"] button').click() + return cy.get('button').contains('Reload content').click() }) Cypress.Commands.add('openFolder', (name) => { const url = `**/${encodeURI(name)}` - cy.intercept({ method: 'PROPFIND', url }) - .as(`open-${name}`) + cy.intercept({ method: 'PROPFIND', url }).as(`open-${name}`) cy.openFile(name) cy.wait(`@open-${name}`) }) Cypress.Commands.add('getFileId', (fileName, params = {}) => { - return cy.get(`[data-cy-files-list] tr[data-cy-files-list-row-name="${fileName}"]`) + return cy + .get(`[data-cy-files-list] tr[data-cy-files-list-row-name="${fileName}"]`) .invoke('attr', 'data-cy-files-list-row-fileid') }) Cypress.Commands.add('openFile', (fileName, params = {}) => { - cy.get(`[data-cy-files-list] tr[data-cy-files-list-row-name="${fileName}"] [data-cy-files-list-row-name-link]`).click(params) + cy.get( + `[data-cy-files-list] tr[data-cy-files-list-row-name="${fileName}"] [data-cy-files-list-row-name-link]`, + ).click(params) }) Cypress.Commands.add('closeFile', (params = {}) => { - cy.intercept({ method: 'POST', url: '**/apps/text/session/*/close' }) - .as('close') + cy.intercept({ method: 'POST', url: '**/apps/text/session/*/close' }).as('close') cy.get('#viewer .modal-header button.header-close').click(params) cy.get('#viewer .modal-header').should('not.exist') cy.wait('@close', { timeout: 7000 }) @@ -276,34 +339,32 @@ Cypress.Commands.add('closeFile', (params = {}) => { let closeData = null Cypress.Commands.add('interceptCreate', () => { - return cy.intercept({ method: 'PUT', url: '**/session/*/create' }, (req) => { - closeData = { - url: ('' + req.url).replace('create', 'close'), - } - req.continue((res) => { + return cy + .intercept({ method: 'PUT', url: '**/session/*/create' }, (req) => { closeData = { - ...closeData, - ...res.body, + url: ('' + req.url).replace('create', 'close'), } + req.continue((res) => { + closeData = { + ...closeData, + ...res.body, + } + }) }) - }).as('create') + .as('create') }) Cypress.Commands.add('closeInterceptedSession', (shareToken = undefined) => { - return axios.post( - closeData.url, - { - documentId: closeData.session.documentId, - sessionId: closeData.session.id, - sessionToken: closeData.session.token, - token: shareToken, - }, - ) + return axios.post(closeData.url, { + documentId: closeData.session.documentId, + sessionId: closeData.session.id, + sessionToken: closeData.session.token, + token: shareToken, + }) }) -Cypress.Commands.add('getFile', fileName => { +Cypress.Commands.add('getFile', (fileName) => { return cy.get(`[data-cy-files-list] [data-cy-files-list-row-name="${fileName}"]`) - }) Cypress.Commands.add('getModal', () => { @@ -317,39 +378,54 @@ Cypress.Commands.add('getEditor', { prevSubject: 'optional' }, (subject) => { }) Cypress.Commands.add('getMenu', { prevSubject: 'optional' }, (subject) => { - return (subject ? cy.wrap(subject) : cy.getEditor()) - .find('[data-text-el="menubar"]') + return (subject ? cy.wrap(subject) : cy.getEditor()).find( + '[data-text-el="menubar"]', + ) }) // Get menu entry even if moved into overflow menu Cypress.Commands.add('getMenuEntry', (name) => { cy.getMenu().then(($body) => { - if ($body.find(`div.text-menubar__entries > [data-text-action-entry="${name}"]`).length) { + if ( + $body.find( + `div.text-menubar__entries > [data-text-action-entry="${name}"]`, + ).length + ) { return cy.getActionEntry(name) } return cy.getSubmenuEntry('remain', name) }) }) -Cypress.Commands.add('getSubmenuEntry', { prevSubject: 'optional' }, (subject, parent, name) => { - return (subject ? cy.wrap(subject) : cy.getMenu()) - .getActionEntry(parent) - .click() - .then(() => cy.getActionSubEntry(name)) -}) +Cypress.Commands.add( + 'getSubmenuEntry', + { prevSubject: 'optional' }, + (subject, parent, name) => { + return (subject ? cy.wrap(subject) : cy.getMenu()) + .getActionEntry(parent) + .click() + .then(() => cy.getActionSubEntry(name)) + }, +) -Cypress.Commands.add('getActionEntry', { prevSubject: 'optional' }, (subject, name) => { - return (subject ? cy.wrap(subject) : cy.getMenu()) - .find(`[data-text-action-entry="${name}"]`) -}) +Cypress.Commands.add( + 'getActionEntry', + { prevSubject: 'optional' }, + (subject, name) => { + return (subject ? cy.wrap(subject) : cy.getMenu()).find( + `[data-text-action-entry="${name}"]`, + ) + }, +) Cypress.Commands.add('getActionSubEntry', (name) => { return cy.get('div[data-text-el="menubar"]').getActionEntry(name) }) Cypress.Commands.add('getContent', { prevSubject: 'optional' }, (subject) => { - return (subject ? cy.wrap(subject) : cy.getEditor()) - .find('.ProseMirror', { timeout: 10000 }) + return (subject ? cy.wrap(subject) : cy.getEditor()).find('.ProseMirror', { + timeout: 10000, + }) }) Cypress.Commands.add('getOutline', () => { @@ -368,52 +444,54 @@ Cypress.Commands.add('clearContent', () => { }) Cypress.Commands.add('insertLine', (line = '') => { - cy.getContent() - .type(`${line}{enter}`) + cy.getContent().type(`${line}{enter}`) }) Cypress.Commands.add('openWorkspace', () => { cy.createDescription() cy.get('#rich-workspace .editor__content').click({ force: true }) - cy.getEditor().find('[data-text-el="editor-content-wrapper"]').click({ force: true }) + cy.getEditor() + .find('[data-text-el="editor-content-wrapper"]') + .click({ force: true }) return cy.getContent() }) Cypress.Commands.add('configureText', (key, value) => { - return axios.post( - `${url}/index.php/apps/text/settings`, - { key, value }, - ) + return axios.post(`${url}/index.php/apps/text/settings`, { key, value }) }) Cypress.Commands.add('showHiddenFiles', (value = true) => { Cypress.log() - return axios.put( - `${url}/index.php/apps/files/api/v1/config/show_hidden`, - { value }, - ) + return axios.put(`${url}/index.php/apps/files/api/v1/config/show_hidden`, { + value, + }) }) Cypress.Commands.add('setAppConfig', (key, value) => { Cypress.log() - return axios.post( - `${url}/ocs/v2.php/apps/testing/api/v1/app/text/${key}`, - { value }, - ) + return axios.post(`${url}/ocs/v2.php/apps/testing/api/v1/app/text/${key}`, { + value, + }) }) -Cypress.Commands.add('createDescription', (buttonLabel = 'Add folder description') => { - const url = '**/remote.php/dav/files/**' - cy.intercept({ method: 'PUT', url }) - .as('addDescription') +Cypress.Commands.add( + 'createDescription', + (buttonLabel = 'Add folder description') => { + const url = '**/remote.php/dav/files/**' + cy.intercept({ method: 'PUT', url }).as('addDescription') - cy.get('[data-cy-files-list] tr[data-cy-files-list-row-name="Readme.md"]').should('not.exist') - cy.get('[data-cy-files-content-breadcrumbs] [data-cy-upload-picker] button.action-item__menutoggle').click() - cy.get('li.upload-picker__menu-entry button').contains(buttonLabel).click() + cy.get( + '[data-cy-files-list] tr[data-cy-files-list-row-name="Readme.md"]', + ).should('not.exist') + cy.get( + '.files-list__header [data-cy-upload-picker] button.action-item__menutoggle', + ).click() + cy.get('li.upload-picker__menu-entry button').contains(buttonLabel).click() - cy.wait('@addDescription') -}) + cy.wait('@addDescription') + }, +) Cypress.Commands.add('setCssMedia', (media) => { cy.log(`Setting CSS media to ${media}`) @@ -425,43 +503,47 @@ Cypress.Commands.add('setCssMedia', (media) => { }) }) -Cypress.Commands.add('createDirectEditingLink', path => { - return axios.request({ - method: 'POST', - url: `${url}/ocs/v2.php/apps/files/api/v1/directEditing/open?format=json`, - data: { path }, - headers: { - 'OCS-ApiRequest': 'true', - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }).then(response => { - const token = response.data?.ocs?.data?.url - Cypress.log({ message: token }) - return token - }) +Cypress.Commands.add('createDirectEditingLink', (path) => { + return axios + .request({ + method: 'POST', + url: `${url}/ocs/v2.php/apps/files/api/v1/directEditing/open?format=json`, + data: { path }, + headers: { + 'OCS-ApiRequest': 'true', + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }) + .then((response) => { + const token = response.data?.ocs?.data?.url + Cypress.log({ message: token }) + return token + }) }) -Cypress.Commands.add('createDirectEditingLinkForNewFile', path => { - return axios.request({ - method: 'POST', - url: `${url}/ocs/v2.php/apps/files/api/v1/directEditing/create?format=json`, - data: { - path, - editorId: 'text', - creatorId: 'textdocument', - }, - headers: { - 'OCS-ApiRequest': 'true', - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }).then(response => { - const token = response.data?.ocs?.data?.url - Cypress.log({ message: token }) - return token - }) +Cypress.Commands.add('createDirectEditingLinkForNewFile', (path) => { + return axios + .request({ + method: 'POST', + url: `${url}/ocs/v2.php/apps/files/api/v1/directEditing/create?format=json`, + data: { + path, + editorId: 'text', + creatorId: 'textdocument', + }, + headers: { + 'OCS-ApiRequest': 'true', + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }) + .then((response) => { + const token = response.data?.ocs?.data?.url + Cypress.log({ message: token }) + return token + }) }) Cypress.on( 'uncaught:exception', - err => !err.message.includes('ResizeObserver loop limit exceeded'), -) + (err) => !err.message.includes('ResizeObserver loop limit exceeded'), +) \ No newline at end of file diff --git a/src/helpers/files.js b/src/helpers/files.js index 06c637792f0..9472d1fbd77 100644 --- a/src/helpers/files.js +++ b/src/helpers/files.js @@ -3,137 +3,58 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { dirname } from 'path' -import { emit } from '@nextcloud/event-bus' import { getCurrentUser } from '@nextcloud/auth' -import { getSharingToken } from '@nextcloud/sharing/public' -import { Header, addNewFileMenuEntry, Permission, File, NewMenuEntryCategory } from '@nextcloud/files' -import { imagePath } from '@nextcloud/router' -import { loadState } from '@nextcloud/initial-state' -import { showSuccess, showError } from '@nextcloud/dialogs' import axios from '@nextcloud/axios' +import { showError, showSuccess } from '@nextcloud/dialogs' +import { emit } from '@nextcloud/event-bus' +import { + addNewFileMenuEntry, + File, + NewMenuEntryCategory, + Permission, +} from '@nextcloud/files' +import { loadState } from '@nextcloud/initial-state' import TextSvg from '@mdi/svg/svg/text.svg?raw' -import { openMimetypes } from './mime.js' +import { t } from '@nextcloud/l10n' +import Vue from 'vue' const FILE_ACTION_IDENTIFIER = 'Edit with text app' -const registerFileCreate = () => { - const newFileMenuPlugin = { - attach(menu) { - const fileList = menu.fileList - - // only attach to main file list, public view is not supported yet - if (fileList.id !== 'files' && fileList.id !== 'files.public') { - return - } - - // register the new menu entry - menu.addMenuEntry({ - id: 'file', - displayName: t('text', 'New text file'), - templateName: t('text', 'New text file') + '.' + loadState('text', 'default_file_extension'), - iconClass: 'icon-filetype-text', - fileType: 'file', - actionLabel: t('text', 'Create new text file'), - actionHandler(name) { - fileList.createFile(name).then(function(status, data) { - const fileInfoModel = new OCA.Files.FileInfoModel(data) - if (typeof OCA.Viewer !== 'undefined') { - OCA.Files.fileActions.triggerAction('view', fileInfoModel, fileList) - } else if (typeof OCA.Viewer === 'undefined') { - OCA.Files.fileActions.triggerAction(FILE_ACTION_IDENTIFIER, fileInfoModel, fileList) - } - }) - }, - }) - }, - } - OC.Plugins.register('OCA.Files.NewFileMenu', newFileMenuPlugin) -} - -const registerFileActionFallback = () => { - const sharingToken = getSharingToken() - const filesTable = document.querySelector('#preview table.files-filestable') - if (!sharingToken || !filesTable) { - const ViewerRoot = document.createElement('div') - ViewerRoot.id = 'text-viewer-fallback' - document.body.appendChild(ViewerRoot) - const registerAction = (mime) => OCA.Files.fileActions.register( - mime, - FILE_ACTION_IDENTIFIER, - OC.PERMISSION_UPDATE | OC.PERMISSION_READ, - imagePath('core', 'actions/rename'), - (filename) => { - const file = window.FileList.findFile(filename) - Promise.all([ - import('vue'), - import(/* webpackChunkName: "files-modal" */'./../components/PublicFilesEditor.vue'), - ]).then((imports) => { - const path = window.FileList.getCurrentDirectory() + '/' + filename - const Vue = imports[0].default - Vue.prototype.t = window.t - Vue.prototype.n = window.n - Vue.prototype.OCA = window.OCA - const Editor = imports[1].default - const vm = new Vue({ - render: function(h) { // eslint-disable-line - const self = this - return h(Editor, { - props: { - fileId: file ? file.id : null, - active: true, - shareToken: sharingToken, - relativePath: path, - mimeType: file.mimetype, - }, - on: { - close: function() { // eslint-disable-line - self.$destroy() - }, - }, - }) - }, - }) - vm.$mount(ViewerRoot) - }) - }, - t('text', 'Edit'), - ) - - for (let i = 0; i < openMimetypes.length; i++) { - registerAction(openMimetypes[i]) - OCA.Files.fileActions.setDefault(openMimetypes[i], FILE_ACTION_IDENTIFIER) - } - } - -} - -let newWorkspaceCreated = false - export const addMenuRichWorkspace = () => { - const descriptionFile = t('text', 'Readme') + '.' + loadState('text', 'default_file_extension') + const descriptionFile = + t('text', 'Readme') + '.' + loadState('text', 'default_file_extension') addNewFileMenuEntry({ id: 'rich-workspace-init', displayName: t('text', 'Add folder description'), category: NewMenuEntryCategory.Other, enabled(context) { + if (!window?.OCA?.Text?.RichWorkspaceEnabled) { + return false + } if (Number(context.attributes['rich-workspace-file-flat'])) { return false } - return (context.permissions & Permission.CREATE) !== 0 + // Check read permission to not show option in file drop shares + return ( + (context.permissions & Permission.READ) !== 0 + && (context.permissions & Permission.CREATE) !== 0 + ) }, iconSvgInline: TextSvg, async handler(context, content) { const contentNames = content.map((node) => node.basename) if (contentNames.includes(descriptionFile)) { - showError(t('text', '"{name}" already exist!', { name: descriptionFile })) + showError( + t('text', '"{name}" already exist!', { name: descriptionFile }), + ) return } - const source = context.encodedSource + '/' + encodeURIComponent(descriptionFile) + const source = + context.encodedSource + '/' + encodeURIComponent(descriptionFile) const response = await axios({ method: 'PUT', url: source, @@ -154,81 +75,77 @@ export const addMenuRichWorkspace = () => { showSuccess(t('text', 'Created "{name}"', { name: descriptionFile })) - if (contentNames.length === 0) { - // We currently have no way to reliably trigger the filelist header rendering - // When starting off in a new empty folder the header will only be rendered on a new PROPFIND - newWorkspaceCreated = file - } + context.attributes['rich-workspace-file-flat'] = fileid + context.attributes['rich-workspace-flat'] = '' + emit('files:node:created', file) + emit('files:node:updated', context) }, }) } -let vm = null +let FilesHeaderRichWorkspaceView +let FilesHeaderRichWorkspaceInstance +let latestFolder + +const enabled = (_, view) => { + return ['files', 'favorites', 'public-share', 'personal'].includes(view.id) +} -export const FilesWorkspaceHeader = new Header({ +/** + * @type {import('@nexcloud/files').IFileListHeader} + */ +export const FilesWorkspaceHeader = { id: 'workspace', order: 10, - - enabled(folder, view) { - return view.id === 'files' || view.id === 'favorites' - }, - - async render(el, folder, view) { - if (vm) { - // Enforce destroying of the old rendering and rerender as the FilesListHeader calls render on every folder change - vm.$destroy() - vm = null + enabled, + + render: async (el, folder) => { + latestFolder = folder + // Import the RichWorkspace component only when needed + if (!FilesHeaderRichWorkspaceView) { + FilesHeaderRichWorkspaceView = ( + await import('../views/RichWorkspace.vue') + ).default } - const hasRichWorkspace = !!folder.attributes['rich-workspace-file-flat'] || !!newWorkspaceCreated - const path = newWorkspaceCreated ? dirname(newWorkspaceCreated.path) : folder.path - const content = newWorkspaceCreated ? '' : folder.attributes['rich-workspace-flat'] - - newWorkspaceCreated = false - - const { default: RichWorkspace } = await import('./../views/RichWorkspace.vue') - import('vue').then((module) => { - el.id = 'files-workspace-wrapper' + // If an instance already exists, destroy it before creating a new one + if (FilesHeaderRichWorkspaceInstance) { + FilesHeaderRichWorkspaceInstance.$destroy() + console.debug('Destroying existing FilesHeaderRichWorkspaceInstance') + } - // Todo: remove this hack - const Vue = module.default - Vue.prototype.t = window.t - Vue.prototype.n = window.n - Vue.prototype.OCA = window.OCA + const hasRichWorkspace = !!latestFolder.attributes['rich-workspace-file-flat'] + const content = latestFolder.attributes['rich-workspace-flat'] || '' + const path = latestFolder.path || '' + + // Create a new instance of the RichWorkspace component + FilesHeaderRichWorkspaceInstance = new Vue({ + extends: FilesHeaderRichWorkspaceView, + propsData: { + content, + hasRichWorkspace, + path, + }, + }).$mount(el) - const View = Vue.extend(RichWorkspace) - vm = new View({ - propsData: { - path, - hasRichWorkspace, - content, - }, - }).$mount(el) - }) + window.FilesHeaderRichWorkspaceInstance = FilesHeaderRichWorkspaceInstance }, updated(folder, view) { - newWorkspaceCreated = false - - if (!vm) { - console.warn('No vue instance found for FilesWorkspaceHeader') + latestFolder = folder + if (!FilesHeaderRichWorkspaceInstance) { + console.error('No vue instance found for FilesWorkspaceHeader') return } - // Currently there is not much use in updating the vue instance props since render is called on every folder change - // removing the rendered element from the DOM - // This is only relevant if switching to a folder that has no content as then the render function is not called - - const hasRichWorkspace = !!folder.attributes['rich-workspace-file-flat'] - vm.path = folder.path - vm.hasRichWorkspace = hasRichWorkspace - vm.content = folder.attributes['rich-workspace-flat'] + const hasRichWorkspace = + !!folder.attributes['rich-workspace-file-flat'] && enabled(folder, view) + FilesHeaderRichWorkspaceInstance.hasRichWorkspace = hasRichWorkspace + FilesHeaderRichWorkspaceInstance.content = + folder.attributes['rich-workspace-flat'] || '' + FilesHeaderRichWorkspaceInstance.path = folder.path || '' }, -}) - -export { - registerFileActionFallback, - registerFileCreate, - FILE_ACTION_IDENTIFIER, } + +export { FILE_ACTION_IDENTIFIER } \ No newline at end of file