Files
homebox/frontend/components/Template/Selector.vue

180 lines
5.9 KiB
Vue

<template>
<!-- Compact mode: icon button only -->
<Popover v-if="compact" v-model:open="open">
<PopoverTrigger as-child>
<Button
:id="id"
variant="outline"
size="icon"
role="combobox"
:aria-expanded="open"
:class="value ? 'border-primary text-primary' : ''"
>
<MdiFileDocumentOutline class="size-5" />
</Button>
</PopoverTrigger>
<PopoverContent class="w-72 p-0" align="end">
<Command :ignore-filter="true">
<CommandInput
v-model="search"
:placeholder="$t('components.template.selector.search')"
:display-value="_ => ''"
/>
<CommandEmpty>{{ $t("components.template.selector.not_found") }}</CommandEmpty>
<CommandList>
<CommandGroup>
<CommandItem
v-for="template in filteredTemplates"
:key="template.id"
:value="template.id"
@select="selectTemplate(template)"
>
<Check :class="cn('mr-2 h-4 w-4', value?.id === template.id ? 'opacity-100' : 'opacity-0')" />
<div class="flex w-full flex-col">
<div>{{ template.name }}</div>
<div v-if="template.description" class="mt-1 line-clamp-1 text-xs text-muted-foreground">
{{ template.description }}
</div>
</div>
</CommandItem>
</CommandGroup>
<CommandSeparator v-if="value" />
<CommandGroup v-if="value">
<CommandItem v-if="value" value="clear-selection" @select="clearSelection">
<div class="flex w-full">
{{ $t("components.template.selector.clear") }}
</div>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<!-- Full mode: label + full-width button -->
<div v-else class="flex flex-col gap-1">
<Label :for="id" class="px-1">{{ $t("components.template.selector.label") }}</Label>
<Popover v-model:open="open">
<PopoverTrigger as-child>
<Button :id="id" variant="outline" role="combobox" :aria-expanded="open" class="w-full justify-between">
{{ value && value.name ? value.name : $t("components.template.selector.select") }}
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent class="w-[--reka-popper-anchor-width] p-0">
<Command :ignore-filter="true">
<CommandInput
v-model="search"
:placeholder="$t('components.template.selector.search')"
:display-value="_ => ''"
/>
<CommandEmpty>{{ $t("components.template.selector.not_found") }}</CommandEmpty>
<CommandList>
<CommandGroup>
<CommandItem
v-for="template in filteredTemplates"
:key="template.id"
:value="template.id"
@select="selectTemplate(template)"
>
<Check :class="cn('mr-2 h-4 w-4', value?.id === template.id ? 'opacity-100' : 'opacity-0')" />
<div class="flex w-full flex-col">
<div>{{ template.name }}</div>
<div v-if="template.description" class="mt-1 line-clamp-1 text-xs text-muted-foreground">
{{ template.description }}
</div>
</div>
</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandItem v-if="value" value="clear-selection" @select="clearSelection">
<X :class="cn('mr-2 h-4 w-4')" />
<div class="flex w-full">
<span class="text-destructive">{{ $t("components.template.selector.clear") }}</span>
</div>
</CommandItem>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
</template>
<script setup lang="ts">
import { Check, ChevronsUpDown, X } from "lucide-vue-next";
import fuzzysort from "fuzzysort";
import { Button } from "~/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "~/components/ui/command";
import { Label } from "~/components/ui/label";
import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover";
import { cn } from "~/lib/utils";
import type { ItemTemplateSummary } from "~~/lib/api/types/data-contracts";
import MdiFileDocumentOutline from "~icons/mdi/file-document-outline";
type Props = {
modelValue?: ItemTemplateSummary | null;
compact?: boolean;
};
const props = defineProps<Props>();
const emit = defineEmits(["update:modelValue", "template-selected"]);
const { compact } = toRefs(props);
const open = ref(false);
const search = ref("");
const id = useId();
const value = useVModel(props, "modelValue", emit);
const api = useUserApi();
const { data: templates } = useAsyncData("templates-selector", async () => {
const { data, error } = await api.templates.getAll();
if (error) {
return [];
}
return data;
});
function selectTemplate(template: ItemTemplateSummary) {
if (value.value?.id !== template.id) {
value.value = template;
emit("template-selected", template);
} else {
value.value = null;
emit("template-selected", null);
}
open.value = false;
}
function clearSelection() {
value.value = null;
emit("template-selected", null);
search.value = "";
open.value = false;
}
const filteredTemplates = computed(() => {
if (!templates.value) return [];
const filtered = fuzzysort.go(search.value, templates.value, { key: "name", all: true }).map(i => i.obj);
return filtered;
});
watch(
() => value.value,
() => {
if (!value.value) {
search.value = "";
}
}
);
</script>