refactor(systemtags): migrate to new files sidebar API

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
Ferdinand Thiessen 2025-12-30 03:08:20 +01:00
parent f9a137ea87
commit 3726596ad0
No known key found for this signature in database
GPG key ID: 7E849AE05218500F
10 changed files with 110 additions and 159 deletions

View file

@ -141,7 +141,7 @@
</template>
<script lang="ts">
import type { Node } from '@nextcloud/files'
import type { INode } from '@nextcloud/files'
import type { PropType } from 'vue'
import type { Tag, TagWithId } from '../types.ts'
@ -216,11 +216,13 @@ export default defineComponent({
props: {
nodes: {
type: Array as PropType<Node[]>,
type: Array as PropType<INode[]>,
required: true,
},
},
emits: ['close'],
setup() {
return {
emit,
@ -381,7 +383,7 @@ export default defineComponent({
})
// Efficient way of counting tags and their occurrences
this.tagList = this.nodes.reduce((acc: TagListCount, node: Node) => {
this.tagList = this.nodes.reduce((acc: TagListCount, node: INode) => {
const tags = getNodeSystemTags(node) || []
tags.forEach((tag) => {
acc[tag] = (acc[tag] || 0) + 1
@ -531,7 +533,7 @@ export default defineComponent({
return
}
const nodes = [] as Node[]
const nodes = [] as INode[]
// Update nodes
this.toAdd.forEach((tag) => {

View file

@ -3,12 +3,12 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Node } from '@nextcloud/files'
import type { INode } from '@nextcloud/files'
import type { TagWithId } from './types.ts'
declare module '@nextcloud/event-bus' {
interface NextcloudEvents {
'systemtags:node:updated': Node
'systemtags:node:updated': INode
'systemtags:tag:deleted': TagWithId
'systemtags:tag:updated': TagWithId
'systemtags:tag:created': TagWithId

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Node } from '@nextcloud/files'
import type { INode } from '@nextcloud/files'
import TagMultipleSvg from '@mdi/svg/svg/tag-multiple-outline.svg?raw'
import { FileAction, Permission } from '@nextcloud/files'
@ -18,7 +18,7 @@ import { defineAsyncComponent } from 'vue'
* @param nodes Nodes to modify tags for
* @param nodes.nodes
*/
async function execBatch({ nodes }: { nodes: Node[] }): Promise<(null | boolean)[]> {
async function execBatch({ nodes }: { nodes: INode[] }): Promise<(null | boolean)[]> {
const response = await new Promise<null | boolean>((resolve) => {
spawnDialog(defineAsyncComponent(() => import('../components/SystemTagPicker.vue')), {
nodes,

View file

@ -0,0 +1,37 @@
/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import tagSvg from '@mdi/svg/svg/tag-outline.svg?raw'
import { registerSidebarAction } from '@nextcloud/files'
import { t } from '@nextcloud/l10n'
import { spawnDialog } from '@nextcloud/vue/functions/dialog'
import { defineAsyncComponent } from 'vue'
/**
* Register the "Add tags" action in the file sidebar
*/
export function registerFileSidebarAction() {
registerSidebarAction({
id: 'systemtags',
order: 20,
displayName() {
return t('systemtags', 'Add tags')
},
enabled() {
return true
},
iconSvgInline() {
return tagSvg
},
onClick({ node }) {
return spawnDialog(
defineAsyncComponent(() => import('../components/SystemTagPicker.vue')),
{
nodes: [node],
},
)
},
})
}

View file

@ -1,10 +1,12 @@
/**
/*!
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { registerFileAction } from '@nextcloud/files'
import { registerDavProperty } from '@nextcloud/files/dav'
import { action as bulkSystemTagsAction } from './files_actions/bulkSystemTagsAction.ts'
import { registerFileSidebarAction } from './files_actions/filesSidebarAction.ts'
import { action as inlineSystemTagsAction } from './files_actions/inlineSystemTagsAction.ts'
import { action as openInFilesAction } from './files_actions/openInFilesAction.ts'
import { registerSystemTagsView } from './files_views/systemtagsView.ts'
@ -16,6 +18,7 @@ registerFileAction(inlineSystemTagsAction)
registerFileAction(openInFilesAction)
registerSystemTagsView()
registerFileSidebarAction()
document.addEventListener('DOMContentLoaded', () => {
registerHotkeys()

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Node } from '@nextcloud/files'
import type { INode } from '@nextcloud/files'
import type { DAVResultResponseProps } from 'webdav'
import type { BaseTag, ServerTag, Tag, TagWithId } from './types.js'
@ -68,7 +68,7 @@ export function formatTag(initialTag: Tag | ServerTag): ServerTag {
*
* @param node
*/
export function getNodeSystemTags(node: Node): string[] {
export function getNodeSystemTags(node: INode): string[] {
const attribute = node.attributes?.['system-tags']?.['system-tag']
if (attribute === undefined) {
return []
@ -92,7 +92,7 @@ export function getNodeSystemTags(node: Node): string[] {
* @param node
* @param tags
*/
export function setNodeSystemTags(node: Node, tags: string[]): void {
export function setNodeSystemTags(node: INode, tags: string[]): void {
Vue.set(node.attributes, 'system-tags', {
'system-tag': tags,
})

View file

@ -1,11 +1,13 @@
/**
/*
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { randomBytes } from 'crypto'
import { closeSidebar, getRowForFile, triggerActionForFile } from '../files/FilesUtils.ts'
import { getRowForFile } from '../files/FilesUtils.ts'
import { addTagToFile } from './utils.ts'
describe('Systemtags: Files integration', { testIsolation: true }, () => {
let user: User
@ -21,31 +23,7 @@ describe('Systemtags: Files integration', { testIsolation: true }, () => {
it('See first assigned tag in the file list', () => {
const tag = randomBytes(8).toString('base64')
cy.intercept('PROPFIND', `**/remote.php/dav/files/${user.userId}/file.txt`).as('getNode')
getRowForFile('file.txt').should('be.visible')
triggerActionForFile('file.txt', 'details')
cy.wait('@getNode')
cy.get('[data-cy-sidebar]')
.should('be.visible')
.findByRole('button', { name: 'Actions' })
.should('be.visible')
.click()
cy.findByRole('menuitem', { name: 'Tags' })
.should('be.visible')
.click()
cy.intercept('PUT', '**/remote.php/dav/systemtags-relations/files/**').as('assignTag')
getCollaborativeTagsInput()
.type(`{selectAll}${tag}{enter}`)
cy.wait('@assignTag')
cy.wait('@getNode')
// Close the sidebar and reload to check the file list
closeSidebar()
addTagToFile('file.txt', tag)
cy.reload()
getRowForFile('file.txt')
@ -58,38 +36,8 @@ describe('Systemtags: Files integration', { testIsolation: true }, () => {
it('See two assigned tags are also shown in the file list', () => {
const tag1 = randomBytes(5).toString('base64')
const tag2 = randomBytes(5).toString('base64')
cy.intercept('PROPFIND', `**/remote.php/dav/files/${user.userId}/file.txt`).as('getNode')
getRowForFile('file.txt').should('be.visible')
triggerActionForFile('file.txt', 'details')
cy.wait('@getNode')
cy.get('[data-cy-sidebar]')
.should('be.visible')
.findByRole('button', { name: 'Actions' })
.should('be.visible')
.click()
cy.findByRole('menuitem', { name: 'Tags' })
.should('be.visible')
.click()
cy.intercept('PUT', '**/remote.php/dav/systemtags-relations/files/**').as('assignTag')
// Assign first tag
getCollaborativeTagsInput()
.type(`{selectAll}${tag1}{enter}`)
cy.wait('@assignTag')
cy.wait('@getNode')
// Assign second tag
getCollaborativeTagsInput()
.type(`{selectAll}${tag2}{enter}`)
cy.wait('@assignTag')
cy.wait('@getNode')
// Close the sidebar and reload to check the file list
closeSidebar()
addTagToFile('file.txt', tag1)
addTagToFile('file.txt', tag2)
cy.reload()
getRowForFile('file.txt')
@ -104,44 +52,9 @@ describe('Systemtags: Files integration', { testIsolation: true }, () => {
const tag1 = randomBytes(4).toString('base64')
const tag2 = randomBytes(4).toString('base64')
const tag3 = randomBytes(4).toString('base64')
cy.intercept('PROPFIND', `**/remote.php/dav/files/${user.userId}/file.txt`).as('getNode')
getRowForFile('file.txt').should('be.visible')
triggerActionForFile('file.txt', 'details')
cy.wait('@getNode')
cy.get('[data-cy-sidebar]')
.should('be.visible')
.findByRole('button', { name: 'Actions' })
.should('be.visible')
.click()
cy.findByRole('menuitem', { name: 'Tags' })
.should('be.visible')
.click()
cy.intercept('PUT', '**/remote.php/dav/systemtags-relations/files/**').as('assignTag')
// Assign first tag
getCollaborativeTagsInput()
.type(`{selectAll}${tag1}{enter}`)
cy.wait('@assignTag')
cy.wait('@getNode')
// Assign second tag
getCollaborativeTagsInput()
.type(`{selectAll}${tag2}{enter}`)
cy.wait('@assignTag')
cy.wait('@getNode')
// Assign third tag
getCollaborativeTagsInput()
.type(`{selectAll}${tag3}{enter}`)
cy.wait('@assignTag')
cy.wait('@getNode')
// Close the sidebar and reload to check the file list
closeSidebar()
addTagToFile('file.txt', tag1)
addTagToFile('file.txt', tag2)
addTagToFile('file.txt', tag3)
cy.reload()
getRowForFile('file.txt')
@ -163,10 +76,3 @@ describe('Systemtags: Files integration', { testIsolation: true }, () => {
})
})
})
function getCollaborativeTagsInput(): Cypress.Chainable<JQuery<HTMLElement>> {
return cy.get('[data-cy-sidebar]')
.findByRole('combobox', { name: /collaborative tags/i })
.should('be.visible')
.should('not.have.attr', 'disabled', { timeout: 5000 })
}

View file

@ -7,6 +7,7 @@ import type { User } from '@nextcloud/e2e-test-server/cypress'
import { randomBytes } from 'crypto'
import { getRowForFile, triggerActionForFile } from '../files/FilesUtils.ts'
import { createNewTagInDialog } from './utils.ts'
describe('Systemtags: Files sidebar integration', { testIsolation: true }, () => {
let user: User
@ -32,14 +33,9 @@ describe('Systemtags: Files sidebar integration', { testIsolation: true }, () =>
.should('be.visible')
.click()
cy.findByRole('menuitem', { name: 'Tags' })
cy.findByRole('menuitem', { name: 'Add tags' })
.click()
cy.intercept('PUT', '**/remote.php/dav/systemtags-relations/files/**').as('assignTag')
cy.get('[data-cy-sidebar]')
.findByRole('combobox', { name: /collaborative tags/i })
.should('be.visible')
.type(`${tag}{enter}`)
cy.wait('@assignTag')
createNewTagInDialog(tag)
})
})

View file

@ -6,7 +6,8 @@
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { randomBytes } from 'crypto'
import { closeSidebar, getRowForFile, getRowForFileId, triggerActionForFile } from '../files/FilesUtils.ts'
import { getRowForFile } from '../files/FilesUtils.ts'
import { addTagToFile } from './utils.ts'
describe('Systemtags: Files view', { testIsolation: true }, () => {
let user: User
@ -22,51 +23,20 @@ describe('Systemtags: Files view', { testIsolation: true }, () => {
it('See first assigned tag in the file list', () => {
const tag = randomBytes(8).toString('base64')
let tagId
// Tag the file
tagNode(tag, 'folder')
.then((id) => { tagId = id })
addTagToFile('folder', tag)
// open the tags view
cy.visit('/apps/files/tags').then(() => {
// see the tag
getRowForFileId(tagId).should('be.visible')
getRowForFile('folder').should('not.exist')
getRowForFile('file.txt').should('not.exist')
cy.findByRole('cell', { name: tag })
.should('be.visible')
.click()
// see that the tag has its content
getRowForFileId(tagId).find('[data-cy-files-list-row-name-link]').click()
getRowForFile('folder').should('be.visible')
getRowForFile('file.txt').should('not.exist')
})
})
})
function getCollaborativeTagsInput(): Cypress.Chainable<JQuery<HTMLElement>> {
return cy.get('[data-cy-sidebar]')
.findByRole('combobox', { name: /collaborative tags/i })
.should('be.visible')
.should('not.have.attr', 'disabled', { timeout: 5000 })
}
function tagNode(tag: string, node: string): Cypress.Chainable<number> {
getRowForFile(node).should('be.visible')
triggerActionForFile(node, 'details')
cy.get('[data-cy-sidebar]')
.should('be.visible')
.findByRole('button', { name: 'Actions' })
.should('be.visible')
.click()
cy.findByRole('menuitem', { name: 'Tags' })
.should('be.visible')
.click()
cy.intercept('PUT', '**/remote.php/dav/systemtags-relations/files/**').as('assignTag')
getCollaborativeTagsInput()
.type(`{selectAll}${tag}{enter}`)
cy.wait('@assignTag')
closeSidebar()
return cy.get('@assignTag')
.then(({ request }) => request.body.id)
}

View file

@ -0,0 +1,37 @@
/*
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { getRowForFile, triggerActionForFile } from '../files/FilesUtils.ts'
export function addTagToFile(fileName: string, newTag: string): void {
getRowForFile(fileName).should('be.visible')
triggerActionForFile(fileName, 'systemtags:bulk')
createNewTagInDialog(newTag)
}
export function createNewTagInDialog(newTag: string): void {
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')
cy.get('[data-cy-systemtags-picker-input]').type(newTag)
cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 0)
cy.get('[data-cy-systemtags-picker-button-create]').should('be.visible')
cy.get('[data-cy-systemtags-picker-button-create]').click()
cy.wait('@createTag')
// 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('@assignTagData')
cy.get('[data-cy-systemtags-picker]').should('not.exist')
}