aboutsummaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
authorskjnldsv <skjnldsv@protonmail.com>2024-10-23 15:41:41 +0200
committerskjnldsv <skjnldsv@protonmail.com>2024-10-29 09:08:31 +0100
commitdb546e1f55814c4eee8df792a66922bf8d9c926f (patch)
tree9e35c65c1cdd0766a55b0d85f2c0b91f57ff10ea /apps
parent2cc377147619fed9838bd016ccff6e4766dd4a44 (diff)
downloadnextcloud-server-db546e1f55814c4eee8df792a66922bf8d9c926f.tar.gz
nextcloud-server-db546e1f55814c4eee8df792a66922bf8d9c926f.zip
feat(systemtags): create tag from bulk tagging dialog
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
Diffstat (limited to 'apps')
-rw-r--r--apps/systemtags/src/components/SystemTagPicker.vue67
-rw-r--r--apps/systemtags/src/files_actions/bulkSystemTagsAction.ts13
-rw-r--r--apps/systemtags/src/services/api.ts23
3 files changed, 73 insertions, 30 deletions
diff --git a/apps/systemtags/src/components/SystemTagPicker.vue b/apps/systemtags/src/components/SystemTagPicker.vue
index 43e7e8fa682..c06bebc3bdc 100644
--- a/apps/systemtags/src/components/SystemTagPicker.vue
+++ b/apps/systemtags/src/components/SystemTagPicker.vue
@@ -11,24 +11,25 @@
close-on-click-outside
out-transition
@update:open="onCancel">
- <NcEmptyContent v-if="loading || done" :name="t('systemtags', 'Applying tags changes…')">
+ <NcEmptyContent v-if="status === Status.LOADING || status === Status.DONE"
+ :name="t('systemtags', 'Applying tags changes…')">
<template #icon>
- <NcLoadingIcon v-if="!done" />
+ <NcLoadingIcon v-if="status === Status.LOADING" />
<CheckIcon v-else fill-color="var(--color-success)" />
</template>
</NcEmptyContent>
<template v-else>
<!-- Search or create input -->
- <div class="systemtags-picker__create">
+ <form class="systemtags-picker__create" @submit.stop.prevent="onNewTag">
<NcTextField :value.sync="input"
:label="t('systemtags', 'Search or create tag')">
<TagIcon :size="20" />
</NcTextField>
- <NcButton>
+ <NcButton :disabled="status === Status.CREATING_TAG" native-type="submit">
{{ t('systemtags', 'Create tag') }}
</NcButton>
- </div>
+ </form>
<!-- Tags list -->
<div v-if="filteredTags.length > 0" class="systemtags-picker__tags">
@@ -60,10 +61,10 @@
</template>
<template #actions>
- <NcButton :disabled="loading || done" type="tertiary" @click="onCancel">
+ <NcButton :disabled="status !== Status.BASE" type="tertiary" @click="onCancel">
{{ t('systemtags', 'Cancel') }}
</NcButton>
- <NcButton :disabled="!hasChanges || loading || done" @click="onSubmit">
+ <NcButton :disabled="!hasChanges || status !== Status.BASE" @click="onSubmit">
{{ t('systemtags', 'Apply changes') }}
</NcButton>
</template>
@@ -81,7 +82,7 @@
<script lang="ts">
import type { Node } from '@nextcloud/files'
import type { PropType } from 'vue'
-import type { TagWithId } from '../types'
+import type { Tag, TagWithId } from '../types'
import { defineComponent } from 'vue'
import { emit } from '@nextcloud/event-bus'
@@ -102,13 +103,20 @@ import TagIcon from 'vue-material-design-icons/Tag.vue'
import CheckIcon from 'vue-material-design-icons/CheckCircle.vue'
import { getNodeSystemTags, setNodeSystemTags } from '../utils'
-import { getTagObjects, setTagObjects } from '../services/api'
+import { createTag, fetchTag, fetchTags, getTagObjects, setTagObjects } from '../services/api'
import logger from '../services/logger'
type TagListCount = {
string: number
}
+enum Status {
+ BASE,
+ LOADING,
+ CREATING_TAG,
+ DONE,
+}
+
export default defineComponent({
name: 'SystemTagPicker',
@@ -131,27 +139,23 @@ export default defineComponent({
type: Array as PropType<Node[]>,
required: true,
},
-
- tags: {
- type: Array as PropType<TagWithId[]>,
- default: () => [],
- },
},
setup() {
return {
emit,
+ Status,
t,
}
},
data() {
return {
- done: false,
- loading: false,
+ status: Status.BASE,
opened: true,
input: '',
+ tags: [] as TagWithId[],
tagList: {} as TagListCount,
toAdd: [] as TagWithId[],
@@ -243,6 +247,10 @@ export default defineComponent({
},
beforeMount() {
+ fetchTags().then(tags => {
+ this.tags = tags
+ })
+
// Efficient way of counting tags and their occurrences
this.tagList = this.nodes.reduce((acc: TagListCount, node: Node) => {
const tags = getNodeSystemTags(node) || []
@@ -296,8 +304,28 @@ export default defineComponent({
}
},
+ async onNewTag() {
+ this.status = Status.CREATING_TAG
+ try {
+ const payload: Tag = {
+ displayName: this.input.trim(),
+ userAssignable: true,
+ userVisible: true,
+ canAssign: true,
+ }
+ const id = await createTag(payload)
+ const tag = await fetchTag(id)
+ this.tags.push(tag)
+ this.input = ''
+ } catch (error) {
+ showError((error as Error)?.message || t('systemtags', 'Failed to create tag'))
+ } finally {
+ this.status = Status.BASE
+ }
+ },
+
async onSubmit() {
- this.loading = true
+ this.status = Status.LOADING
logger.debug('Applying tags', {
toAdd: this.toAdd,
toRemove: this.toRemove,
@@ -336,7 +364,7 @@ export default defineComponent({
} catch (error) {
logger.error('Failed to apply tags', { error })
showError(t('systemtags', 'Failed to apply tags changes'))
- this.loading = false
+ this.status = Status.BASE
return
}
@@ -364,8 +392,7 @@ export default defineComponent({
// trigger update event
nodes.forEach(node => emit('systemtags:node:updated', node))
- this.done = true
- this.loading = false
+ this.status = Status.DONE
setTimeout(() => {
this.opened = false
this.$emit('close', null)
diff --git a/apps/systemtags/src/files_actions/bulkSystemTagsAction.ts b/apps/systemtags/src/files_actions/bulkSystemTagsAction.ts
index 72cb49952fc..709a4c7ebcb 100644
--- a/apps/systemtags/src/files_actions/bulkSystemTagsAction.ts
+++ b/apps/systemtags/src/files_actions/bulkSystemTagsAction.ts
@@ -5,22 +5,21 @@
import { type Node } from '@nextcloud/files'
import { defineAsyncComponent } from 'vue'
+import { getCurrentUser } from '@nextcloud/auth'
import { FileAction } from '@nextcloud/files'
+import { spawnDialog } from '@nextcloud/dialogs'
import { t } from '@nextcloud/l10n'
-import TagMultipleSvg from '@mdi/svg/svg/tag-multiple.svg?raw'
-import { getCurrentUser } from '@nextcloud/auth'
-import { spawnDialog } from '@nextcloud/dialogs'
-import { fetchTags } from '../services/api'
+import TagMultipleSvg from '@mdi/svg/svg/tag-multiple.svg?raw'
export const action = new FileAction({
id: 'systemtags:bulk',
displayName: () => t('systemtags', 'Manage tags'),
iconSvgInline: () => TagMultipleSvg,
+ // If the app is disabled, the action is not available anyway
enabled(nodes) {
- // Only for multiple nodes
- if (nodes.length <= 1) {
+ if (nodes.length > 0) {
return false
}
@@ -33,11 +32,9 @@ export const action = new FileAction({
},
async execBatch(nodes: Node[]) {
- const tags = await fetchTags()
const response = await new Promise<null|boolean>((resolve) => {
spawnDialog(defineAsyncComponent(() => import('../components/SystemTagPicker.vue')), {
nodes,
- tags,
}, (status) => {
resolve(status as null|boolean)
})
diff --git a/apps/systemtags/src/services/api.ts b/apps/systemtags/src/services/api.ts
index 39059d18347..4f202e07522 100644
--- a/apps/systemtags/src/services/api.ts
+++ b/apps/systemtags/src/services/api.ts
@@ -3,12 +3,12 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import type { FileStat, ResponseDataDetailed } from 'webdav'
+import type { FileStat, ResponseDataDetailed, WebDAVClientError } from 'webdav'
import type { ServerTag, Tag, TagWithId } from '../types.js'
import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
-import { translate as t } from '@nextcloud/l10n'
+import { t } from '@nextcloud/l10n'
import { davClient } from './davClient.js'
import { formatTag, parseIdFromLocation, parseTags } from '../utils'
@@ -22,6 +22,7 @@ export const fetchTagsPayload = `<?xml version="1.0"?>
<oc:user-visible />
<oc:user-assignable />
<oc:can-assign />
+ <d:getetag />
</d:prop>
</d:propfind>`
@@ -40,6 +41,20 @@ export const fetchTags = async (): Promise<TagWithId[]> => {
}
}
+export const fetchTag = async (tagId: number): Promise<TagWithId> => {
+ const path = '/systemtags/' + tagId
+ try {
+ const { data: tag } = await davClient.stat(path, {
+ data: fetchTagsPayload,
+ details: true
+ }) as ResponseDataDetailed<Required<FileStat>>
+ return parseTags([tag])[0]
+ } catch (error) {
+ logger.error(t('systemtags', 'Failed to load tag'), { error })
+ throw new Error(t('systemtags', 'Failed to load tag'))
+ }
+}
+
export const fetchLastUsedTagIds = async (): Promise<number[]> => {
const url = generateUrl('/apps/systemtags/lastused')
try {
@@ -71,6 +86,10 @@ export const createTag = async (tag: Tag | ServerTag): Promise<number> => {
logger.error(t('systemtags', 'Missing "Content-Location" header'))
throw new Error(t('systemtags', 'Missing "Content-Location" header'))
} catch (error) {
+ if ((error as WebDAVClientError)?.response?.status === 409) {
+ logger.error(t('systemtags', 'A tag with the same name already exists'), { error })
+ throw new Error(t('systemtags', 'A tag with the same name already exists'))
+ }
logger.error(t('systemtags', 'Failed to create tag'), { error })
throw new Error(t('systemtags', 'Failed to create tag'))
}