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()