diff --git a/cypress/e2e/propfind.spec.js b/cypress/e2e/propfind.spec.js index 2fc2afaf5fe..b1bfd8ada2b 100644 --- a/cypress/e2e/propfind.spec.js +++ b/cypress/e2e/propfind.spec.js @@ -9,7 +9,10 @@ const user = randUser() // Retries fail because folders / files already exist. describe('Text PROPFIND extension ', { retries: 0 }, function () { - const richWorkspace = 'nc:rich-workspace' + const PROPERTY_WORKSPACE = 'nc:rich-workspace' + const PROPERTY_WORKSPACE_FILE = 'nc:rich-workspace-file' + const PROPERTY_WORKSPACE_FLAT = 'nc:rich-workspace-flat' + const PROPERTY_WORKSPACE_FILE_FLAT = 'nc:rich-workspace-file-flat' before(function () { cy.createUser(user) @@ -17,6 +20,9 @@ describe('Text PROPFIND extension ', { retries: 0 }, function () { beforeEach(function () { cy.login(user) + cy.deleteFile('/Readme.md') + cy.deleteFile('/workspace-flat') + cy.deleteFile('/workspace') }) describe('with workspaces enabled', function () { @@ -24,36 +30,60 @@ describe('Text PROPFIND extension ', { retries: 0 }, 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( + cy.propfindFolder('/', 0, properties).should( 'have.property', - richWorkspace, + 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, + '', + ) }) - // 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 for flat properties', function () { + const properties = [ + PROPERTY_WORKSPACE_FLAT, + PROPERTY_WORKSPACE_FILE_FLAT, + ] + cy.visit('/apps/dashboard') + cy.createFolder('/workspace-flat') + cy.propfindFolder('/', 1, properties) + .then((results) => results.pop()) + .should('have.property', PROPERTY_WORKSPACE_FLAT, '') + cy.uploadFile('test.md', 'text/markdown', '/workspace-flat/Readme.md') + cy.propfindFolder('/', 1, properties) + .then((results) => results.pop()) + .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()) - .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()) - .should('have.property', richWorkspace, '## Hello world\n') + .should('have.property', PROPERTY_WORKSPACE, '## Hello world\n') }) }) @@ -66,13 +96,19 @@ describe('Text PROPFIND extension ', { retries: 0 }, 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()) - .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 9c990d7e6c7..3db3e302359 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -220,7 +220,9 @@ Cypress.Commands.add('waitForPreview', (name) => { }) Cypress.Commands.add('deleteFile', (path) => { - return axios.delete(`${url}/remote.php/webdav/${path}`) + return axios.delete(`${url}/remote.php/webdav/${path}`, { + validateStatus: (status) => status < 500, + }) }) Cypress.Commands.add('copyFile', (path, destinationPath) => { @@ -241,7 +243,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 = null) => { + const defaultProperties = ` + + ` + + const propsXml = properties + ? properties.map((p) => `<${p} />`).join('\n') + : defaultProperties + const rootPath = `${url}/remote.php/webdav/` const requestPath = path === '/' ? rootPath : `${rootPath}${path}` @@ -258,8 +268,7 @@ Cypress.Commands.add('propfindFolder', (path, depth = 0) => { xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns"> - - + ${propsXml} `, }) diff --git a/lib/DAV/WorkspacePlugin.php b/lib/DAV/WorkspacePlugin.php index fa4bc5568a7..eaac84a903c 100644 --- a/lib/DAV/WorkspacePlugin.php +++ b/lib/DAV/WorkspacePlugin.php @@ -27,9 +27,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, @@ -59,8 +60,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; } @@ -75,15 +80,28 @@ public function propFind(PropFind $propFind, INode $node) { return; } + $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) { + $propFind->handle(self::WORKSPACE_PROPERTY_FLAT, fn () => ''); + $propFind->handle(self::WORKSPACE_FILE_PROPERTY_FLAT, fn () => ''); + return; + } + $node = $node->getNode(); try { $file = $this->workspaceService->getFile($node); } catch (\Exception $e) { $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) { + $workspaceContentCallback = function () use ($file) { $cachedContent = ''; if ($file instanceof File) { $cache = $this->cacheFactory->createDistributed('text_workspace'); @@ -107,12 +125,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 c9338661e95..5fc169c4a42 100644 --- a/src/helpers/files.js +++ b/src/helpers/files.js @@ -33,7 +33,7 @@ export const addMenuRichWorkspace = () => { if (!window?.OCA?.Text?.RichWorkspaceEnabled) { return false } - if (Number(context.attributes['rich-workspace-file'])) { + if (Number(context.attributes['rich-workspace-file-flat'])) { return false } // Check read permission to not show option in file drop shares @@ -75,8 +75,8 @@ export const addMenuRichWorkspace = () => { showSuccess(t('text', 'Created "{name}"', { name: descriptionFile })) - context.attributes['rich-workspace-file'] = fileid - context.attributes['rich-workspace'] = '' + context.attributes['rich-workspace-file-flat'] = fileid + context.attributes['rich-workspace-flat'] = '' emit('files:node:created', file) emit('files:node:updated', context) @@ -115,8 +115,9 @@ export const FilesWorkspaceHeader = { console.debug('Destroying existing FilesHeaderRichWorkspaceInstance') } - const hasRichWorkspace = !!latestFolder.attributes['rich-workspace-file'] - const content = latestFolder.attributes['rich-workspace'] || '' + 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 @@ -140,10 +141,10 @@ export const FilesWorkspaceHeader = { } const hasRichWorkspace = - !!folder.attributes['rich-workspace-file'] && enabled(folder, view) + !!folder.attributes['rich-workspace-file-flat'] && enabled(folder, view) FilesHeaderRichWorkspaceInstance.hasRichWorkspace = hasRichWorkspace FilesHeaderRichWorkspaceInstance.content = - folder.attributes['rich-workspace'] || '' + folder.attributes['rich-workspace-flat'] || '' FilesHeaderRichWorkspaceInstance.path = folder.path || '' }, } diff --git a/src/init.js b/src/init.js index 7ae83f19dec..920f51b3bb1 100644 --- a/src/init.js +++ b/src/init.js @@ -12,8 +12,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()