mirror of
https://github.com/nextcloud/server.git
synced 2026-06-09 08:44:07 -04:00
feat(systemtags): create tag from bulk tagging dialog
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
This commit is contained in:
parent
2cc3771476
commit
db546e1f55
3 changed files with 74 additions and 31 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -5,22 +5,21 @@
|
|||
import { type Node } from '@nextcloud/files'
|
||||
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import { FileAction } from '@nextcloud/files'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import TagMultipleSvg from '@mdi/svg/svg/tag-multiple.svg?raw'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
|
||||
import { FileAction } from '@nextcloud/files'
|
||||
import { spawnDialog } from '@nextcloud/dialogs'
|
||||
import { fetchTags } from '../services/api'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
|
||||
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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue