|
| 1 | +<!-- |
| 2 | + - SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors |
| 3 | + - SPDX-License-Identifier: AGPL-3.0-or-later |
| 4 | +--> |
| 5 | +<template> |
| 6 | + <div> |
| 7 | + <NcTextField |
| 8 | + v-if="availableTags.length > 5" |
| 9 | + v-model="searchQuery" |
| 10 | + type="search" |
| 11 | + :label="t('systemtags', 'Search tags')" /> |
| 12 | + <NcButton |
| 13 | + v-for="tag of shownTags" |
| 14 | + :key="tag.id" |
| 15 | + alignment="start" |
| 16 | + :pressed="isSelected(tag)" |
| 17 | + variant="tertiary" |
| 18 | + wide |
| 19 | + @update:pressed="toggleTag(tag, $event)"> |
| 20 | + <template #icon> |
| 21 | + <NcIconSvgWrapper :path="mdiTagOutline" /> |
| 22 | + </template> |
| 23 | + {{ tag.displayName }} |
| 24 | + </NcButton> |
| 25 | + <span v-if="shownTags.length === 0 && !loading"> |
| 26 | + {{ t('systemtags', 'No tags available') }} |
| 27 | + </span> |
| 28 | + </div> |
| 29 | +</template> |
| 30 | + |
| 31 | +<script setup lang="ts"> |
| 32 | +import type { TagsFilter } from '../files_filters/TagsFilter.ts' |
| 33 | +import type { TagWithId } from '../types.ts' |
| 34 | +
|
| 35 | +import { mdiTagOutline } from '@mdi/js' |
| 36 | +import { t } from '@nextcloud/l10n' |
| 37 | +import { computed, onMounted, onUnmounted, ref, watch } from 'vue' |
| 38 | +import NcButton from '@nextcloud/vue/components/NcButton' |
| 39 | +import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' |
| 40 | +import NcTextField from '@nextcloud/vue/components/NcTextField' |
| 41 | +import { fetchTags } from '../services/api.ts' |
| 42 | +
|
| 43 | +const props = defineProps<{ |
| 44 | + filter: TagsFilter |
| 45 | +}>() |
| 46 | +
|
| 47 | +const loading = ref(true) |
| 48 | +const searchQuery = ref('') |
| 49 | +const availableTags = ref<TagWithId[]>([]) |
| 50 | +const selectedTags = ref<TagWithId[]>([...props.filter.selectedTags]) |
| 51 | +
|
| 52 | +watch(selectedTags, () => { |
| 53 | + props.filter.setTags(selectedTags.value.length > 0 ? [...selectedTags.value] : undefined) |
| 54 | +}) |
| 55 | +
|
| 56 | +onMounted(async () => { |
| 57 | + try { |
| 58 | + const tags = await fetchTags() |
| 59 | + availableTags.value = tags.filter((tag) => tag.userVisible) |
| 60 | + } finally { |
| 61 | + loading.value = false |
| 62 | + } |
| 63 | + props.filter.addEventListener('reset', resetFilter) |
| 64 | + props.filter.addEventListener('deselect', onDeselect) |
| 65 | +}) |
| 66 | +
|
| 67 | +onUnmounted(() => { |
| 68 | + props.filter.removeEventListener('reset', resetFilter) |
| 69 | + props.filter.removeEventListener('deselect', onDeselect) |
| 70 | +}) |
| 71 | +
|
| 72 | +const shownTags = computed(() => { |
| 73 | + if (!searchQuery.value) { |
| 74 | + return availableTags.value |
| 75 | + } |
| 76 | + const query = searchQuery.value.toLocaleLowerCase() |
| 77 | + return availableTags.value.filter((tag) => tag.displayName.toLocaleLowerCase().includes(query)) |
| 78 | +}) |
| 79 | +
|
| 80 | +/** |
| 81 | + * Check if a tag is currently selected |
| 82 | + * |
| 83 | + * @param tag The tag to check |
| 84 | + */ |
| 85 | +function isSelected(tag: TagWithId): boolean { |
| 86 | + return selectedTags.value.some((t) => t.id === tag.id) |
| 87 | +} |
| 88 | +
|
| 89 | +/** |
| 90 | + * Toggle a tag from the selected list |
| 91 | + * |
| 92 | + * @param tag The tag to toggle |
| 93 | + * @param selected Whether the tag should be selected |
| 94 | + */ |
| 95 | +function toggleTag(tag: TagWithId, selected: boolean) { |
| 96 | + selectedTags.value = selectedTags.value.filter((t) => t.id !== tag.id) |
| 97 | + if (selected) { |
| 98 | + selectedTags.value = [...selectedTags.value, tag] |
| 99 | + } |
| 100 | +} |
| 101 | +
|
| 102 | +/** |
| 103 | + * Reset selected tags (triggered by filter reset event) |
| 104 | + */ |
| 105 | +function resetFilter() { |
| 106 | + selectedTags.value = [] |
| 107 | +} |
| 108 | +
|
| 109 | +/** |
| 110 | + * Remove a single tag from selected (triggered by chip removal) |
| 111 | + * |
| 112 | + * @param event The deselect custom event carrying the tag ID |
| 113 | + */ |
| 114 | +function onDeselect(event: CustomEvent<number>) { |
| 115 | + selectedTags.value = selectedTags.value.filter((t) => t.id !== event.detail) |
| 116 | +} |
| 117 | +</script> |
0 commit comments