- Replace all AppDrawer with MalioDrawer across 10 drawer components - Replace native <button> with MalioButton/MalioButtonIcon in all pages and components - Fix TimeTrackingExportDrawer: use MalioSelectCheckbox for multi-select filters - Add Malio design system colors (m-btn-*, m-disabled, m-surface) to tailwind.config.ts - Align toggle button heights with MalioButton (h-[40px]) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
160 lines
5.0 KiB
Vue
160 lines
5.0 KiB
Vue
<template>
|
|
<div class="mt-5">
|
|
<p class="mb-2 text-sm font-medium text-neutral-700">{{ $t('bookstack.links.title') }}</p>
|
|
|
|
<!-- Search -->
|
|
<div class="relative">
|
|
<MalioInputText
|
|
v-model="searchQuery"
|
|
:placeholder="$t('bookstack.links.searchPlaceholder')"
|
|
input-class="w-full"
|
|
/>
|
|
|
|
<!-- Dropdown results -->
|
|
<div
|
|
v-if="searchResults.length > 0"
|
|
class="absolute z-30 mt-1 w-full rounded-md border border-neutral-200 bg-white shadow-lg"
|
|
>
|
|
<button
|
|
v-for="result in searchResults"
|
|
:key="`${result.type}-${result.id}`"
|
|
type="button"
|
|
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-neutral-50"
|
|
@click="handleAdd(result)"
|
|
>
|
|
<Icon
|
|
:name="result.type === 'page' ? 'mdi:file-document-outline' : 'mdi:book-outline'"
|
|
size="16"
|
|
class="shrink-0 text-neutral-400"
|
|
/>
|
|
<span class="truncate">{{ result.name }}</span>
|
|
<span class="ml-auto shrink-0 text-xs text-neutral-400">{{ result.type }}</span>
|
|
</button>
|
|
</div>
|
|
|
|
<p v-if="searchQuery.length >= 2 && !isSearching && searchResults.length === 0 && hasSearched" class="mt-1 text-xs text-neutral-400">
|
|
{{ $t('bookstack.links.noResults') }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Linked documents -->
|
|
<div v-if="links.length > 0" class="mt-3 space-y-1">
|
|
<div
|
|
v-for="link in links"
|
|
:key="link.id"
|
|
class="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-neutral-50"
|
|
>
|
|
<Icon
|
|
:name="link.bookstackType === 'page' ? 'mdi:file-document-outline' : 'mdi:book-outline'"
|
|
size="16"
|
|
class="shrink-0 text-neutral-400"
|
|
/>
|
|
<a
|
|
:href="link.url"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="truncate text-primary-500 hover:underline"
|
|
>
|
|
{{ link.title }}
|
|
</a>
|
|
<MalioButtonIcon
|
|
icon="mdi:close"
|
|
aria-label="Supprimer le lien"
|
|
variant="ghost"
|
|
icon-size="16"
|
|
button-class="ml-auto shrink-0 text-neutral-300 hover:text-red-500"
|
|
@click="handleRemove(link.id)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<p v-else-if="!isLoading" class="mt-2 text-xs text-neutral-400">
|
|
{{ $t('bookstack.links.empty') }}
|
|
</p>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { BookStackLink, BookStackSearchResult } from '~/services/dto/bookstack'
|
|
import { useBookStackService } from '~/services/bookstack'
|
|
|
|
const props = defineProps<{
|
|
taskId: number
|
|
}>()
|
|
|
|
const { getLinks, addLink, removeLink, search } = useBookStackService()
|
|
|
|
const links = ref<BookStackLink[]>([])
|
|
const searchQuery = ref('')
|
|
const searchResults = ref<BookStackSearchResult[]>([])
|
|
const isLoading = ref(true)
|
|
const isSearching = ref(false)
|
|
const hasSearched = ref(false)
|
|
|
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
|
|
|
watch(searchQuery, (query) => {
|
|
if (debounceTimer) clearTimeout(debounceTimer)
|
|
hasSearched.value = false
|
|
searchResults.value = []
|
|
|
|
if (query.trim().length < 2) {
|
|
return
|
|
}
|
|
|
|
debounceTimer = setTimeout(async () => {
|
|
isSearching.value = true
|
|
try {
|
|
searchResults.value = await search(props.taskId, query.trim())
|
|
} catch {
|
|
searchResults.value = []
|
|
} finally {
|
|
isSearching.value = false
|
|
hasSearched.value = true
|
|
}
|
|
}, 300)
|
|
})
|
|
|
|
async function handleAdd(result: BookStackSearchResult) {
|
|
searchQuery.value = ''
|
|
searchResults.value = []
|
|
hasSearched.value = false
|
|
|
|
// Check if already linked
|
|
if (links.value.some(l => l.bookstackId === result.id && l.bookstackType === result.type)) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
const created = await addLink(props.taskId, {
|
|
bookstackId: result.id,
|
|
bookstackType: result.type,
|
|
title: result.name,
|
|
url: result.url,
|
|
})
|
|
links.value.unshift(created)
|
|
} catch {
|
|
// Error handled by useApi toast
|
|
}
|
|
}
|
|
|
|
async function handleRemove(linkId: number) {
|
|
try {
|
|
await removeLink(props.taskId, linkId)
|
|
links.value = links.value.filter(l => l.id !== linkId)
|
|
} catch {
|
|
// Error handled by useApi toast
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
links.value = await getLinks(props.taskId)
|
|
} catch {
|
|
// Error handled by useApi toast
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
})
|
|
</script>
|