diff options
author | skjnldsv <skjnldsv@protonmail.com> | 2024-10-23 15:41:41 +0200 |
---|---|---|
committer | skjnldsv <skjnldsv@protonmail.com> | 2024-10-29 09:08:31 +0100 |
commit | db546e1f55814c4eee8df792a66922bf8d9c926f (patch) | |
tree | 9e35c65c1cdd0766a55b0d85f2c0b91f57ff10ea /apps | |
parent | 2cc377147619fed9838bd016ccff6e4766dd4a44 (diff) | |
download | nextcloud-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.vue | 67 | ||||
-rw-r--r-- | apps/systemtags/src/files_actions/bulkSystemTagsAction.ts | 13 | ||||
-rw-r--r-- | apps/systemtags/src/services/api.ts | 23 |
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')) } |