|
|
|
@ -11,48 +11,70 @@ |
|
|
|
close-on-click-outside |
|
|
|
out-transition |
|
|
|
@update:open="onCancel"> |
|
|
|
<!-- Search or create input --> |
|
|
|
<div class="systemtags-picker__create"> |
|
|
|
<NcTextField :value.sync="input" |
|
|
|
:label="t('systemtags', 'Search or create tag')"> |
|
|
|
<TagIcon :size="20" /> |
|
|
|
</NcTextField> |
|
|
|
<NcButton> |
|
|
|
{{ t('systemtags', 'Create tag') }} |
|
|
|
</NcButton> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- Tags list --> |
|
|
|
<div class="systemtags-picker__tags"> |
|
|
|
<NcCheckboxRadioSwitch v-for="tag in filteredTags" |
|
|
|
:key="tag.id" |
|
|
|
:label="tag.displayName" |
|
|
|
:checked="isChecked(tag)" |
|
|
|
:indeterminate="isIndeterminate(tag)" |
|
|
|
:disabled="!tag.canAssign" |
|
|
|
@update:checked="onCheckUpdate(tag, $event)"> |
|
|
|
{{ formatTagName(tag) }} |
|
|
|
</NcCheckboxRadioSwitch> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- Note --> |
|
|
|
<div class="systemtags-picker__note"> |
|
|
|
<NcNoteCard v-if="!hasChanges" type="info"> |
|
|
|
{{ t('systemtags', 'Select or create tags to apply to all selected files') }} |
|
|
|
</NcNoteCard> |
|
|
|
<NcNoteCard v-else type="info"> |
|
|
|
<span v-html="statusMessage" /> |
|
|
|
</NcNoteCard> |
|
|
|
</div> |
|
|
|
<NcEmptyContent v-if="loading || done" :name="t('systemtags', 'Applying changes…')"> |
|
|
|
<template #icon> |
|
|
|
<NcLoadingIcon v-if="!done" /> |
|
|
|
<CheckIcon v-else fill-color="var(--color-success)" /> |
|
|
|
</template> |
|
|
|
</NcEmptyContent> |
|
|
|
|
|
|
|
<template v-else> |
|
|
|
<!-- Search or create input --> |
|
|
|
<div class="systemtags-picker__create"> |
|
|
|
<NcTextField :value.sync="input" |
|
|
|
:label="t('systemtags', 'Search or create tag')"> |
|
|
|
<TagIcon :size="20" /> |
|
|
|
</NcTextField> |
|
|
|
<NcButton> |
|
|
|
{{ t('systemtags', 'Create tag') }} |
|
|
|
</NcButton> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- Tags list --> |
|
|
|
<div v-if="filteredTags.length > 0" class="systemtags-picker__tags"> |
|
|
|
<NcCheckboxRadioSwitch v-for="tag in filteredTags" |
|
|
|
:key="tag.id" |
|
|
|
:label="tag.displayName" |
|
|
|
:checked="isChecked(tag)" |
|
|
|
:indeterminate="isIndeterminate(tag)" |
|
|
|
:disabled="!tag.canAssign" |
|
|
|
@update:checked="onCheckUpdate(tag, $event)"> |
|
|
|
{{ formatTagName(tag) }} |
|
|
|
</NcCheckboxRadioSwitch> |
|
|
|
</div> |
|
|
|
<NcEmptyContent v-else :name="t('systemtags', 'No tags found')"> |
|
|
|
<template #icon> |
|
|
|
<TagIcon /> |
|
|
|
</template> |
|
|
|
</NcEmptyContent> |
|
|
|
|
|
|
|
<!-- Note --> |
|
|
|
<div class="systemtags-picker__note"> |
|
|
|
<NcNoteCard v-if="!hasChanges" type="info"> |
|
|
|
{{ t('systemtags', 'Select or create tags to apply to all selected files') }} |
|
|
|
</NcNoteCard> |
|
|
|
<NcNoteCard v-else type="info"> |
|
|
|
<span v-html="statusMessage" /> |
|
|
|
</NcNoteCard> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
|
|
|
|
<template #actions> |
|
|
|
<NcButton type="tertiary" @click="onCancel"> |
|
|
|
<NcButton :disabled="loading || done" type="tertiary" @click="onCancel"> |
|
|
|
{{ t('systemtags', 'Cancel') }} |
|
|
|
</NcButton> |
|
|
|
<NcButton :disabled="!hasChanges" @click="onSubmit"> |
|
|
|
<NcButton :disabled="!hasChanges || loading || done" @click="onSubmit"> |
|
|
|
{{ t('systemtags', 'Apply changes') }} |
|
|
|
</NcButton> |
|
|
|
</template> |
|
|
|
|
|
|
|
<!-- Chip html for v-html tag rendering --> |
|
|
|
<div v-show="false"> |
|
|
|
<NcChip ref="chip" |
|
|
|
text="%s" |
|
|
|
type="primary" |
|
|
|
no-close /> |
|
|
|
</div> |
|
|
|
</NcDialog> |
|
|
|
</template> |
|
|
|
|
|
|
|
@ -63,18 +85,25 @@ import type { TagWithId } from '../types' |
|
|
|
|
|
|
|
import { defineComponent } from 'vue' |
|
|
|
import { emit } from '@nextcloud/event-bus' |
|
|
|
import { sanitize } from 'dompurify' |
|
|
|
import { showInfo } from '@nextcloud/dialogs' |
|
|
|
import { t } from '@nextcloud/l10n' |
|
|
|
import escapeHTML from 'escape-html' |
|
|
|
|
|
|
|
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' |
|
|
|
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' |
|
|
|
import NcChip from '@nextcloud/vue/dist/Components/NcChip.js' |
|
|
|
import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js' |
|
|
|
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js' |
|
|
|
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' |
|
|
|
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js' |
|
|
|
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' |
|
|
|
import TagIcon from 'vue-material-design-icons/Tag.vue' |
|
|
|
import CheckIcon from 'vue-material-design-icons/CheckCircle.vue' |
|
|
|
|
|
|
|
import logger from '../services/logger' |
|
|
|
import { getNodeSystemTags } from '../utils' |
|
|
|
import { showInfo } from '@nextcloud/dialogs' |
|
|
|
import { getTagObjects, setTagObjects } from '../services/api' |
|
|
|
import logger from '../services/logger' |
|
|
|
|
|
|
|
type TagListCount = { |
|
|
|
string: number |
|
|
|
@ -84,9 +113,14 @@ export default defineComponent({ |
|
|
|
name: 'SystemTagPicker', |
|
|
|
|
|
|
|
components: { |
|
|
|
CheckIcon, |
|
|
|
NcButton, |
|
|
|
NcCheckboxRadioSwitch, |
|
|
|
// eslint-disable-next-line vue/no-unused-components |
|
|
|
NcChip, |
|
|
|
NcDialog, |
|
|
|
NcEmptyContent, |
|
|
|
NcLoadingIcon, |
|
|
|
NcNoteCard, |
|
|
|
NcTextField, |
|
|
|
TagIcon, |
|
|
|
@ -113,8 +147,11 @@ export default defineComponent({ |
|
|
|
|
|
|
|
data() { |
|
|
|
return { |
|
|
|
input: '', |
|
|
|
done: false, |
|
|
|
loading: false, |
|
|
|
opened: true, |
|
|
|
|
|
|
|
input: '', |
|
|
|
tagList: {} as TagListCount, |
|
|
|
|
|
|
|
toAdd: [] as TagWithId[], |
|
|
|
@ -137,40 +174,44 @@ export default defineComponent({ |
|
|
|
}, |
|
|
|
|
|
|
|
statusMessage(): string { |
|
|
|
if (this.toAdd.length === 0 && this.toRemove.length === 0) { |
|
|
|
return '' |
|
|
|
} |
|
|
|
|
|
|
|
if (this.toAdd.length === 1 && this.toRemove.length === 1) { |
|
|
|
return t('systemtags', '{tag1} will be set and {tag2} will be removed from {count} files.', { |
|
|
|
tag1: this.toAdd[0].displayName, |
|
|
|
tag2: this.toRemove[0].displayName, |
|
|
|
tag1: this.formatTagChip(this.toAdd[0]), |
|
|
|
tag2: this.formatTagChip(this.toRemove[0]), |
|
|
|
count: this.nodes.length, |
|
|
|
}) |
|
|
|
}, undefined, { escape: false }) |
|
|
|
} |
|
|
|
|
|
|
|
const tagsAdd = this.toAdd.map(tag => tag.displayName) |
|
|
|
const tagsAdd = this.toAdd.map(this.formatTagChip) |
|
|
|
const lastTagAdd = tagsAdd.pop() as string |
|
|
|
const tagsRemove = this.toRemove.map(tag => tag.displayName) |
|
|
|
const tagsRemove = this.toRemove.map(this.formatTagChip) |
|
|
|
const lastTagRemove = tagsRemove.pop() as string |
|
|
|
|
|
|
|
const addStringSingular = t('systemtags', '{tag} will be set to {count} files.', { |
|
|
|
tag: this.toAdd[0]?.displayName, |
|
|
|
tag: lastTagAdd, |
|
|
|
count: this.nodes.length, |
|
|
|
}) |
|
|
|
}, undefined, { escape: false }) |
|
|
|
|
|
|
|
const removeStringSingular = t('systemtags', '{tag} will be removed from {count} files.', { |
|
|
|
tag: this.toRemove[0]?.displayName, |
|
|
|
tag: lastTagRemove, |
|
|
|
count: this.nodes.length, |
|
|
|
}) |
|
|
|
}, undefined, { escape: false }) |
|
|
|
|
|
|
|
const addStringPlural = t('systemtags', '{tags} and {lastTag} will be set to {count} files.', { |
|
|
|
tags: tagsAdd.join(', '), |
|
|
|
lastTag: lastTagAdd, |
|
|
|
count: this.nodes.length, |
|
|
|
}) |
|
|
|
}, undefined, { escape: false }) |
|
|
|
|
|
|
|
const removeStringPlural = t('systemtags', '{tags} and {lastTag} will be removed from {count} files.', { |
|
|
|
tags: tagsRemove.join(', '), |
|
|
|
lastTag: lastTagRemove, |
|
|
|
count: this.nodes.length, |
|
|
|
}) |
|
|
|
}, undefined, { escape: false }) |
|
|
|
|
|
|
|
// Singular |
|
|
|
if (this.toAdd.length === 1 && this.toRemove.length === 0) { |
|
|
|
@ -213,6 +254,13 @@ export default defineComponent({ |
|
|
|
}, |
|
|
|
|
|
|
|
methods: { |
|
|
|
// Format & sanitize a tag chip for v-html tag rendering |
|
|
|
formatTagChip(tag: TagWithId): string { |
|
|
|
const chip = this.$refs.chip as NcChip |
|
|
|
const chipHtml = chip.$el.outerHTML |
|
|
|
return chipHtml.replace('%s', escapeHTML(sanitize(tag.displayName))) |
|
|
|
}, |
|
|
|
|
|
|
|
formatTagName(tag: TagWithId): string { |
|
|
|
if (tag.userVisible) { |
|
|
|
return t('systemtags', '{displayName} (hidden)', { displayName: tag.displayName }) |
|
|
|
@ -226,11 +274,12 @@ export default defineComponent({ |
|
|
|
}, |
|
|
|
|
|
|
|
isChecked(tag: TagWithId): boolean { |
|
|
|
return this.tagList[tag.displayName] === this.nodes.length |
|
|
|
return tag.displayName in this.tagList |
|
|
|
&& this.tagList[tag.displayName] === this.nodes.length |
|
|
|
}, |
|
|
|
|
|
|
|
isIndeterminate(tag: TagWithId): boolean { |
|
|
|
return this.tagList[tag.displayName] |
|
|
|
return tag.displayName in this.tagList |
|
|
|
&& this.tagList[tag.displayName] !== 0 |
|
|
|
&& this.tagList[tag.displayName] !== this.nodes.length |
|
|
|
}, |
|
|
|
@ -247,9 +296,37 @@ export default defineComponent({ |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
onSubmit() { |
|
|
|
logger.debug('onSubmit') |
|
|
|
this.$emit('close', null) |
|
|
|
async onSubmit() { |
|
|
|
this.loading = true |
|
|
|
logger.debug('Applying tags', { |
|
|
|
toAdd: this.toAdd, |
|
|
|
toRemove: this.toRemove, |
|
|
|
}) |
|
|
|
|
|
|
|
// Add tags |
|
|
|
for (const tag of this.toAdd) { |
|
|
|
const { etag, objects } = await getTagObjects(tag, 'files') |
|
|
|
let ids = [...objects.map(obj => obj.id), ...this.nodes.map(node => node.fileid)] as number[] |
|
|
|
// Remove duplicates and empty ids |
|
|
|
ids = [...new Set(ids.filter(id => !!id))] |
|
|
|
await setTagObjects(tag, 'files', ids.map(id => ({ id, type: 'files' })), etag) |
|
|
|
} |
|
|
|
|
|
|
|
// Remove tags |
|
|
|
for (const tag of this.toRemove) { |
|
|
|
const { etag, objects } = await getTagObjects(tag, 'files') |
|
|
|
let ids = objects.map(obj => obj.id) as number[] |
|
|
|
// Remove the ids of the nodes and remove duplicates |
|
|
|
ids = [...new Set(ids.filter(id => !this.nodes.map(node => node.fileid).includes(id)))] |
|
|
|
await setTagObjects(tag, 'files', ids.map(id => ({ id, type: 'files' })), etag) |
|
|
|
} |
|
|
|
|
|
|
|
this.done = true |
|
|
|
this.loading = false |
|
|
|
setTimeout(() => { |
|
|
|
this.opened = false |
|
|
|
this.$emit('close', null) |
|
|
|
}, 2000) |
|
|
|
}, |
|
|
|
|
|
|
|
onCancel() { |
|
|
|
@ -291,4 +368,8 @@ export default defineComponent({ |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Rendered chip in note |
|
|
|
.nc-chip { |
|
|
|
display: inline !important; |
|
|
|
} |
|
|
|
</style> |