aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apps/dav/lib/SystemTag/SystemTagPlugin.php7
-rw-r--r--apps/files/src/components/FileEntry/FileEntryCheckbox.vue1
-rw-r--r--apps/files/src/components/FilesListTableHeader.vue2
-rw-r--r--apps/files/src/components/FilesListTableHeaderActions.vue3
-rw-r--r--apps/systemtags/src/components/SystemTagPicker.vue28
-rw-r--r--apps/systemtags/src/event-bus.d.ts4
-rw-r--r--apps/systemtags/src/files_actions/bulkSystemTagsAction.ts2
-rw-r--r--cypress/e2e/files/FilesUtils.ts27
-rw-r--r--cypress/e2e/systemtags/files-bulk-action.cy.ts354
-rw-r--r--lib/private/SystemTag/SystemTagObjectMapper.php2
-rw-r--r--lib/public/SystemTag/ISystemTagObjectMapper.php2
11 files changed, 414 insertions, 18 deletions
diff --git a/apps/dav/lib/SystemTag/SystemTagPlugin.php b/apps/dav/lib/SystemTag/SystemTagPlugin.php
index c88a69f1154..00585953b29 100644
--- a/apps/dav/lib/SystemTag/SystemTagPlugin.php
+++ b/apps/dav/lib/SystemTag/SystemTagPlugin.php
@@ -21,7 +21,6 @@ use OCP\Util;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\Conflict;
use Sabre\DAV\Exception\Forbidden;
-use Sabre\DAV\Exception\PreconditionFailed;
use Sabre\DAV\Exception\UnsupportedMediaType;
use Sabre\DAV\PropFind;
use Sabre\DAV\PropPatch;
@@ -218,8 +217,8 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
$propFind->setPath(str_replace('systemtags-assigned/', 'systemtags/', $propFind->getPath()));
}
- $propFind->handle(FilesPlugin::GETETAG_PROPERTYNAME, function () use ($node): string|null {
- return $node->getSystemTag()->getETag();
+ $propFind->handle(FilesPlugin::GETETAG_PROPERTYNAME, function () use ($node): string {
+ return '"' . ($node->getSystemTag()->getETag() ?? '') . '"';
});
$propFind->handle(self::ID_PROPERTYNAME, function () use ($node) {
@@ -379,7 +378,7 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
if (isset($props[self::OBJECTIDS_PROPERTYNAME])) {
$propValue = $props[self::OBJECTIDS_PROPERTYNAME];
- if (!($propValue instanceof SystemTagsObjectList) || count($propValue?->getObjects() ?: []) === 0) {
+ if (!($propValue instanceof SystemTagsObjectList) || count($propValue->getObjects()) === 0) {
throw new BadRequest('Invalid object-ids property');
}
diff --git a/apps/files/src/components/FileEntry/FileEntryCheckbox.vue b/apps/files/src/components/FileEntry/FileEntryCheckbox.vue
index f2ab39d7525..9caa08cfe22 100644
--- a/apps/files/src/components/FileEntry/FileEntryCheckbox.vue
+++ b/apps/files/src/components/FileEntry/FileEntryCheckbox.vue
@@ -9,6 +9,7 @@
<NcCheckboxRadioSwitch v-else
:aria-label="ariaLabel"
:checked="isSelected"
+ data-cy-files-list-row-checkbox
@update:checked="onSelectionChange" />
</td>
</template>
diff --git a/apps/files/src/components/FilesListTableHeader.vue b/apps/files/src/components/FilesListTableHeader.vue
index e91cfa055c1..c8334abda5e 100644
--- a/apps/files/src/components/FilesListTableHeader.vue
+++ b/apps/files/src/components/FilesListTableHeader.vue
@@ -6,7 +6,7 @@
<tr class="files-list__row-head">
<th class="files-list__column files-list__row-checkbox"
@keyup.esc.exact="resetSelection">
- <NcCheckboxRadioSwitch v-bind="selectAllBind" @update:checked="onToggleAll" />
+ <NcCheckboxRadioSwitch v-bind="selectAllBind" data-cy-files-list-selection-checkbox @update:checked="onToggleAll" />
</th>
<!-- Columns display -->
diff --git a/apps/files/src/components/FilesListTableHeaderActions.vue b/apps/files/src/components/FilesListTableHeaderActions.vue
index a5732220441..fa5f7d4bd5f 100644
--- a/apps/files/src/components/FilesListTableHeaderActions.vue
+++ b/apps/files/src/components/FilesListTableHeaderActions.vue
@@ -3,7 +3,7 @@
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
- <div class="files-list__column files-list__row-actions-batch">
+ <div class="files-list__column files-list__row-actions-batch" data-cy-files-list-selection-actions>
<NcActions ref="actionsMenu"
container="#app-content-vue"
:disabled="!!loading || areSomeNodesLoading"
@@ -15,6 +15,7 @@
:key="action.id"
:aria-label="action.displayName(nodes, currentView) + ' ' + t('files', '(selected)') /** TRANSLATORS: Selected like 'selected files and folders' */"
:class="'files-list__row-actions-batch-' + action.id"
+ :data-cy-files-list-selection-action="action.id"
@click="onActionClick(action)">
<template #icon>
<NcLoadingIcon v-if="loading === action.id" :size="18" />
diff --git a/apps/systemtags/src/components/SystemTagPicker.vue b/apps/systemtags/src/components/SystemTagPicker.vue
index c06bebc3bdc..732d509ac24 100644
--- a/apps/systemtags/src/components/SystemTagPicker.vue
+++ b/apps/systemtags/src/components/SystemTagPicker.vue
@@ -23,22 +23,28 @@
<!-- Search or create input -->
<form class="systemtags-picker__create" @submit.stop.prevent="onNewTag">
<NcTextField :value.sync="input"
- :label="t('systemtags', 'Search or create tag')">
+ :label="t('systemtags', 'Search or create tag')"
+ data-cy-systemtags-picker-input>
<TagIcon :size="20" />
</NcTextField>
- <NcButton :disabled="status === Status.CREATING_TAG" native-type="submit">
+ <NcButton :disabled="status === Status.CREATING_TAG"
+ native-type="submit"
+ data-cy-systemtags-picker-input-submit>
{{ t('systemtags', 'Create tag') }}
</NcButton>
</form>
<!-- Tags list -->
- <div v-if="filteredTags.length > 0" class="systemtags-picker__tags">
+ <div v-if="filteredTags.length > 0"
+ class="systemtags-picker__tags"
+ data-cy-systemtags-picker-tags>
<NcCheckboxRadioSwitch v-for="tag in filteredTags"
:key="tag.id"
:label="tag.displayName"
:checked="isChecked(tag)"
:indeterminate="isIndeterminate(tag)"
:disabled="!tag.canAssign"
+ :data-cy-systemtags-picker-tag="tag.id"
@update:checked="onCheckUpdate(tag, $event)">
{{ formatTagName(tag) }}
</NcCheckboxRadioSwitch>
@@ -61,10 +67,15 @@
</template>
<template #actions>
- <NcButton :disabled="status !== Status.BASE" type="tertiary" @click="onCancel">
+ <NcButton :disabled="status !== Status.BASE"
+ type="tertiary"
+ data-cy-systemtags-picker-button-cancel
+ @click="onCancel">
{{ t('systemtags', 'Cancel') }}
</NcButton>
- <NcButton :disabled="!hasChanges || status !== Status.BASE" @click="onSubmit">
+ <NcButton :disabled="!hasChanges || status !== Status.BASE"
+ data-cy-systemtags-picker-button-submit
+ @click="onSubmit">
{{ t('systemtags', 'Apply changes') }}
</NcButton>
</template>
@@ -270,11 +281,11 @@ export default defineComponent({
},
formatTagName(tag: TagWithId): string {
- if (tag.userVisible) {
+ if (!tag.userVisible) {
return t('systemtags', '{displayName} (hidden)', { displayName: tag.displayName })
}
- if (tag.userAssignable) {
+ if (!tag.userAssignable) {
return t('systemtags', '{displayName} (restricted)', { displayName: tag.displayName })
}
@@ -317,6 +328,9 @@ export default defineComponent({
const tag = await fetchTag(id)
this.tags.push(tag)
this.input = ''
+
+ // Check the newly created tag
+ this.onCheckUpdate(tag, true)
} catch (error) {
showError((error as Error)?.message || t('systemtags', 'Failed to create tag'))
} finally {
diff --git a/apps/systemtags/src/event-bus.d.ts b/apps/systemtags/src/event-bus.d.ts
index 368d0acaf0f..4009f3f372b 100644
--- a/apps/systemtags/src/event-bus.d.ts
+++ b/apps/systemtags/src/event-bus.d.ts
@@ -1,3 +1,7 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
import type { Node } from '@nextcloud/files'
declare module '@nextcloud/event-bus' {
diff --git a/apps/systemtags/src/files_actions/bulkSystemTagsAction.ts b/apps/systemtags/src/files_actions/bulkSystemTagsAction.ts
index 709a4c7ebcb..ad2ebab70c4 100644
--- a/apps/systemtags/src/files_actions/bulkSystemTagsAction.ts
+++ b/apps/systemtags/src/files_actions/bulkSystemTagsAction.ts
@@ -19,7 +19,7 @@ export const action = new FileAction({
// If the app is disabled, the action is not available anyway
enabled(nodes) {
- if (nodes.length > 0) {
+ if (nodes.length === 0) {
return false
}
diff --git a/cypress/e2e/files/FilesUtils.ts b/cypress/e2e/files/FilesUtils.ts
index 0f2b1154200..182972ee44c 100644
--- a/cypress/e2e/files/FilesUtils.ts
+++ b/cypress/e2e/files/FilesUtils.ts
@@ -14,11 +14,15 @@ export const getActionButtonForFile = (filename: string) => getActionsForFile(fi
export const triggerActionForFileId = (fileid: number, actionId: string) => {
getActionButtonForFileId(fileid).click()
- cy.get(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"] > button`).should('exist').click()
+ // Getting the last button to avoid the one from popup fading out
+ cy.get(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"] > button`).last()
+ .should('exist').click()
}
export const triggerActionForFile = (filename: string, actionId: string) => {
getActionButtonForFile(filename).click()
- cy.get(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"] > button`).should('exist').click()
+ // Getting the last button to avoid the one from popup fading out
+ cy.get(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"] > button`).last()
+ .should('exist').click()
}
export const triggerInlineActionForFileId = (fileid: number, actionId: string) => {
@@ -28,6 +32,25 @@ export const triggerInlineActionForFile = (filename: string, actionId: string) =
getActionsForFile(filename).get(`button[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`).should('exist').click()
}
+export const selectAllFiles = () => {
+ cy.get('[data-cy-files-list-selection-checkbox]').findByRole('checkbox').click({ force: true })
+}
+export const selectRowForFile = (filename: string) => {
+ getRowForFile(filename)
+ .find('[data-cy-files-list-row-checkbox]')
+ .findByRole('checkbox')
+ .click({ force: true })
+ .should('be.checked')
+ cy.get('[data-cy-files-list-selection-checkbox]').findByRole('checkbox').should('satisfy', (elements) => {
+ return elements.length === 1 && (elements[0].checked === true || elements[0].indeterminate === true)
+ })
+
+}
+
+export const triggerSelectionAction = (actionId: string) => {
+ cy.get(`button[data-cy-files-list-selection-action="${CSS.escape(actionId)}"]`).should('exist').click()
+}
+
export const moveFile = (fileName: string, dirPath: string) => {
getRowForFile(fileName).should('be.visible')
triggerActionForFile(fileName, 'move-copy')
diff --git a/cypress/e2e/systemtags/files-bulk-action.cy.ts b/cypress/e2e/systemtags/files-bulk-action.cy.ts
new file mode 100644
index 00000000000..bfc2280a9d8
--- /dev/null
+++ b/cypress/e2e/systemtags/files-bulk-action.cy.ts
@@ -0,0 +1,354 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import type { User } from '@nextcloud/cypress'
+import { randomBytes } from 'crypto'
+import { getRowForFile, selectAllFiles, selectRowForFile, triggerSelectionAction } from '../files/FilesUtils'
+import { createShare } from '../files_sharing/FilesSharingUtils'
+
+let tags = {} as Record<string, number>
+const files = [
+ 'file1.txt',
+ 'file2.txt',
+ 'file3.txt',
+ 'file4.txt',
+ 'file5.txt',
+]
+
+function resetTags() {
+ tags = {}
+ for (const tag in [0, 1, 2, 3, 4]) {
+ tags[randomBytes(8).toString('base64').slice(0, 6)] = 0
+ }
+
+ // delete any existing tags
+ cy.runOccCommand('tag:list --output=json').then((output) => {
+ Object.keys(JSON.parse(output.stdout)).forEach((id) => {
+ cy.runOccCommand(`tag:delete ${id}`)
+ })
+ })
+
+ // create tags
+ Object.keys(tags).forEach((tag) => {
+ cy.runOccCommand(`tag:add ${tag} public --output=json`).then((output) => {
+ tags[tag] = JSON.parse(output.stdout).id as number
+ })
+ })
+ cy.log('Using tags', tags)
+}
+
+function expectInlineTagForFile(file: string, tags: string[]) {
+ getRowForFile(file)
+ .find('[data-systemtags-fileid]')
+ .findAllByRole('listitem')
+ .should('have.length', tags.length)
+ .each(tag => {
+ expect(tag.text()).to.be.oneOf(tags)
+ })
+}
+
+function triggerTagManagementDialogAction() {
+ cy.intercept('PROPFIND', '/remote.php/dav/systemtags/').as('getTagsList')
+ triggerSelectionAction('systemtags:bulk')
+ cy.wait('@getTagsList')
+ cy.get('[data-cy-systemtags-picker]').should('be.visible')
+}
+
+describe('Systemtags: Files bulk action', { testIsolation: false }, () => {
+ let snapshot: string
+ let user1: User
+ let user2: User
+
+ before(() => {
+ cy.createRandomUser().then((_user1) => {
+ user1 = _user1
+ cy.createRandomUser().then((_user2) => {
+ user2 = _user2
+ })
+
+ files.forEach((file) => {
+ cy.uploadContent(user1, new Blob([]), 'text/plain', '/' + file)
+ })
+ })
+
+ resetTags()
+ })
+
+ it('Can assign tag to selection', () => {
+ cy.login(user1)
+ cy.visit('/apps/files')
+
+ files.forEach((file) => {
+ getRowForFile(file).should('be.visible')
+ })
+ selectRowForFile('file2.txt')
+ selectRowForFile('file4.txt')
+
+ triggerTagManagementDialogAction()
+ cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 5)
+
+ cy.intercept('PROPFIND', '/remote.php/dav/systemtags/*/files').as('getTagData')
+ cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*/files').as('assignTagData')
+
+ const tag = Object.keys(tags)[3]
+ cy.get(`[data-cy-systemtags-picker-tag=${tags[tag]}]`).should('be.visible')
+ .findByRole('checkbox').click({ force: true })
+ cy.get('[data-cy-systemtags-picker-button-submit]').click()
+
+ cy.wait('@getTagData')
+ cy.wait('@assignTagData')
+ cy.get('[data-cy-systemtags-picker]').should('not.exist')
+
+ expectInlineTagForFile('file2.txt', [tag])
+ expectInlineTagForFile('file4.txt', [tag])
+ })
+
+ it('Can assign multiple tags to selection', () => {
+ cy.login(user1)
+ cy.visit('/apps/files')
+
+ files.forEach((file) => {
+ getRowForFile(file).should('be.visible')
+ })
+ selectAllFiles()
+
+ triggerTagManagementDialogAction()
+ cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 5)
+
+ cy.intercept('PROPFIND', '/remote.php/dav/systemtags/*/files').as('getTagData')
+ cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*/files').as('assignTagData')
+
+ const prevTag = Object.keys(tags)[3]
+ const tag1 = Object.keys(tags)[1]
+ const tag2 = Object.keys(tags)[2]
+ cy.get(`[data-cy-systemtags-picker-tag=${tags[tag1]}]`).should('be.visible')
+ .findByRole('checkbox').click({ force: true })
+ cy.get(`[data-cy-systemtags-picker-tag=${tags[tag2]}]`).should('be.visible')
+ .findByRole('checkbox').click({ force: true })
+ cy.get('[data-cy-systemtags-picker-button-submit]').click()
+
+ cy.wait('@getTagData')
+ cy.wait('@assignTagData')
+ cy.get('@getTagData.all').should('have.length', 2)
+ cy.get('@assignTagData.all').should('have.length', 2)
+ cy.get('[data-cy-systemtags-picker]').should('not.exist')
+
+ expectInlineTagForFile('file1.txt', [tag1, tag2])
+ expectInlineTagForFile('file2.txt', [prevTag, tag1, tag2])
+ expectInlineTagForFile('file3.txt', [tag1, tag2])
+ expectInlineTagForFile('file4.txt', [prevTag, tag1, tag2])
+ expectInlineTagForFile('file5.txt', [tag1, tag2])
+ })
+
+ it('Can remove tag from selection', () => {
+ cy.login(user1)
+ cy.visit('/apps/files')
+
+ files.forEach((file) => {
+ getRowForFile(file).should('be.visible')
+ })
+ selectRowForFile('file1.txt')
+ selectRowForFile('file3.txt')
+ selectRowForFile('file4.txt')
+
+ triggerTagManagementDialogAction()
+ cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 5)
+
+ cy.intercept('PROPFIND', '/remote.php/dav/systemtags/*/files').as('getTagData')
+ cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*/files').as('assignTagData')
+
+ const firstTag = Object.keys(tags)[3]
+ const tag1 = Object.keys(tags)[1]
+ const tag2 = Object.keys(tags)[2]
+ cy.get(`[data-cy-systemtags-picker-tag=${tags[tag2]}]`).should('be.visible')
+ .findByRole('checkbox').click({ force: true })
+ cy.get('[data-cy-systemtags-picker-button-submit]').click()
+
+ cy.wait('@getTagData')
+ cy.wait('@assignTagData')
+ cy.get('[data-cy-systemtags-picker]').should('not.exist')
+
+ expectInlineTagForFile('file1.txt', [tag1])
+ expectInlineTagForFile('file2.txt', [firstTag, tag1, tag2])
+ expectInlineTagForFile('file3.txt', [tag1])
+ expectInlineTagForFile('file4.txt', [firstTag, tag1])
+ expectInlineTagForFile('file5.txt', [tag1, tag2])
+
+ })
+
+ it('Can remove multiple tags from selection', () => {
+ cy.login(user1)
+ cy.visit('/apps/files')
+
+ files.forEach((file) => {
+ getRowForFile(file).should('be.visible')
+ })
+ selectAllFiles()
+
+ triggerTagManagementDialogAction()
+ cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 5)
+
+ cy.intercept('PROPFIND', '/remote.php/dav/systemtags/*/files').as('getTagData')
+ cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*/files').as('assignTagData')
+
+ cy.get('[data-cy-systemtags-picker-tag] input:indeterminate').should('exist')
+ .click({ force: true, multiple: true })
+ // indeterminate became checked
+ cy.get('[data-cy-systemtags-picker-tag] input:checked').should('exist')
+ .click({ force: true, multiple: true })
+ // now all are unchecked
+ cy.get('[data-cy-systemtags-picker-button-submit]').click()
+
+ cy.wait('@getTagData')
+ cy.wait('@assignTagData')
+ cy.get('@getTagData.all').should('have.length', 3)
+ cy.get('@assignTagData.all').should('have.length', 3)
+ cy.get('[data-cy-systemtags-picker]').should('not.exist')
+
+ expectInlineTagForFile('file1.txt', [])
+ expectInlineTagForFile('file2.txt', [])
+ expectInlineTagForFile('file3.txt', [])
+ expectInlineTagForFile('file4.txt', [])
+ expectInlineTagForFile('file5.txt', [])
+ })
+
+ it('Can assign and remove multiple tags as a secondary user', () => {
+ // Create new users
+ cy.createRandomUser().then((_user1) => {
+ user1 = _user1
+ cy.createRandomUser().then((_user2) => {
+ user2 = _user2
+ })
+
+ files.forEach((file) => {
+ cy.uploadContent(user1, new Blob([]), 'text/plain', '/' + file)
+ })
+ })
+
+ cy.login(user1)
+ cy.visit('/apps/files')
+
+ files.forEach((file) => {
+ getRowForFile(file).should('be.visible')
+ })
+ selectAllFiles()
+
+ triggerTagManagementDialogAction()
+ cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 5)
+
+ cy.intercept('PROPFIND', '/remote.php/dav/systemtags/*/files').as('getTagData1')
+ cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*/files').as('assignTagData1')
+
+ const tag1 = Object.keys(tags)[0]
+ const tag2 = Object.keys(tags)[3]
+ cy.get(`[data-cy-systemtags-picker-tag=${tags[tag1]}]`).should('be.visible')
+ .findByRole('checkbox').click({ force: true })
+ cy.get(`[data-cy-systemtags-picker-tag=${tags[tag2]}]`).should('be.visible')
+ .findByRole('checkbox').click({ force: true })
+ cy.get('[data-cy-systemtags-picker-button-submit]').click()
+
+ cy.wait('@getTagData1')
+ cy.wait('@assignTagData1')
+ cy.get('@getTagData1.all').should('have.length', 2)
+ cy.get('@assignTagData1.all').should('have.length', 2)
+ cy.get('[data-cy-systemtags-picker]').should('not.exist')
+
+ expectInlineTagForFile('file1.txt', [tag1, tag2])
+ expectInlineTagForFile('file2.txt', [tag1, tag2])
+ expectInlineTagForFile('file3.txt', [tag1, tag2])
+ expectInlineTagForFile('file4.txt', [tag1, tag2])
+ expectInlineTagForFile('file5.txt', [tag1, tag2])
+
+ createShare('file1.txt', user2.userId)
+ createShare('file3.txt', user2.userId)
+
+ cy.login(user2)
+ cy.visit('/apps/files')
+
+ getRowForFile('file1.txt').should('be.visible')
+ getRowForFile('file3.txt').should('be.visible')
+
+ expectInlineTagForFile('file1.txt', [tag1, tag2])
+ expectInlineTagForFile('file3.txt', [tag1, tag2])
+
+ selectRowForFile('file1.txt')
+ selectRowForFile('file3.txt')
+ triggerTagManagementDialogAction()
+ cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 5)
+
+ cy.intercept('PROPFIND', '/remote.php/dav/systemtags/*/files').as('getTagData2')
+ cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*/files').as('assignTagData2')
+
+ cy.get(`[data-cy-systemtags-picker-tag=${tags[tag1]}]`).should('be.visible')
+ .findByRole('checkbox').click({ force: true })
+ cy.get(`[data-cy-systemtags-picker-tag=${tags[tag2]}]`).should('be.visible')
+ .findByRole('checkbox').click({ force: true })
+ cy.get('[data-cy-systemtags-picker-button-submit]').click()
+
+ cy.wait('@getTagData2')
+ cy.wait('@assignTagData2')
+ cy.get('@getTagData2.all').should('have.length', 2)
+ cy.get('@assignTagData2.all').should('have.length', 2)
+ cy.get('[data-cy-systemtags-picker]').should('not.exist')
+
+ expectInlineTagForFile('file1.txt', [])
+ expectInlineTagForFile('file3.txt', [])
+
+ cy.login(user1)
+ cy.visit('/apps/files')
+
+ expectInlineTagForFile('file1.txt', [])
+ expectInlineTagForFile('file3.txt', [])
+ })
+
+ it('Can create tag and assign files to it', () => {
+ cy.createRandomUser().then((user1) => {
+ files.forEach((file) => {
+ cy.uploadContent(user1, new Blob([]), 'text/plain', '/' + file)
+ })
+
+ cy.login(user1)
+ cy.visit('/apps/files')
+
+ files.forEach((file) => {
+ getRowForFile(file).should('be.visible')
+ })
+ selectAllFiles()
+
+ triggerTagManagementDialogAction()
+ cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 5)
+
+ cy.intercept('POST', '/remote.php/dav/systemtags').as('createTag')
+ cy.intercept('PROPFIND', '/remote.php/dav/systemtags/*/files').as('getTagData')
+ cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*/files').as('assignTagData')
+
+ const newTag = randomBytes(8).toString('base64').slice(0, 6)
+ cy.get('[data-cy-systemtags-picker-input]').type(newTag)
+ cy.get('[data-cy-systemtags-picker-input-submit]').click()
+
+ cy.wait('@createTag')
+ cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 6)
+ // Verify the new tag is selected by default
+ cy.get('[data-cy-systemtags-picker-tag]').contains(newTag)
+ .parents('[data-cy-systemtags-picker-tag]')
+ .findByRole('checkbox', { hidden: true }).should('be.checked')
+
+ // Apply changes
+ cy.get('[data-cy-systemtags-picker-button-submit]').click()
+
+ cy.wait('@getTagData')
+ cy.wait('@assignTagData')
+ cy.get('@getTagData.all').should('have.length', 1)
+ cy.get('@assignTagData.all').should('have.length', 1)
+ cy.get('[data-cy-systemtags-picker]').should('not.exist')
+
+ expectInlineTagForFile('file1.txt', [newTag])
+ expectInlineTagForFile('file2.txt', [newTag])
+ expectInlineTagForFile('file3.txt', [newTag])
+ expectInlineTagForFile('file4.txt', [newTag])
+ expectInlineTagForFile('file5.txt', [newTag])
+ })
+ })
+})
diff --git a/lib/private/SystemTag/SystemTagObjectMapper.php b/lib/private/SystemTag/SystemTagObjectMapper.php
index ddb04fa968d..09a7ce1a6ed 100644
--- a/lib/private/SystemTag/SystemTagObjectMapper.php
+++ b/lib/private/SystemTag/SystemTagObjectMapper.php
@@ -204,7 +204,7 @@ class SystemTagObjectMapper implements ISystemTagObjectMapper {
/**
* Update the etag for the given tags.
*
- * @param int[] $tagIds
+ * @param string[] $tagIds
*/
private function updateEtagForTags(array $tagIds): void {
// Update etag after assigning tags
diff --git a/lib/public/SystemTag/ISystemTagObjectMapper.php b/lib/public/SystemTag/ISystemTagObjectMapper.php
index 96e8c1e848a..c604fa93c58 100644
--- a/lib/public/SystemTag/ISystemTagObjectMapper.php
+++ b/lib/public/SystemTag/ISystemTagObjectMapper.php
@@ -129,7 +129,7 @@ interface ISystemTagObjectMapper {
* @param string $tagId tag id
* @param string $objectType object type
* @param string[] $objectIds list of object ids
- *
+ *
* @throws TagNotFoundException if the tag does not exist
* @since 31.0.0
*/