Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat(systemtags): create tag from bulk tagging dialog
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
  • Loading branch information
skjnldsv committed Oct 29, 2024
commit db546e1f55814c4eee8df792a66922bf8d9c926f
67 changes: 47 additions & 20 deletions apps/systemtags/src/components/SystemTagPicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down Expand Up @@ -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>
Expand All @@ -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'
Expand All @@ -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',

Expand All @@ -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[],
Expand Down Expand Up @@ -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) || []
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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)
Expand Down
13 changes: 5 additions & 8 deletions apps/systemtags/src/files_actions/bulkSystemTagsAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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)
})
Expand Down
23 changes: 21 additions & 2 deletions apps/systemtags/src/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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>`

Expand All @@ -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 {
Expand Down Expand Up @@ -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'))
}
Expand Down