Browse Source

feat(systemtags): create tag from bulk tagging dialog

Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
pull/48786/head
skjnldsv 1 year ago
parent
commit
db546e1f55
  1. 67
      apps/systemtags/src/components/SystemTagPicker.vue
  2. 13
      apps/systemtags/src/files_actions/bulkSystemTagsAction.ts
  3. 23
      apps/systemtags/src/services/api.ts

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

13
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)
})

23
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'))
}

Loading…
Cancel
Save