Files
homebox/frontend/components/Item/Selector.vue
Tonya cbaf483788 migrate pages to shadcn (#628)
* feat: migrate tools page and label generator to shadcn

* chore: lint issues

* feat: also do profile page

* feat: shadcn 404 page

* feat: login page shadcn

* fix: daisyui ironically breaks the z height for the login page

* feat: componentise the language selector and add it to the login page

* feat: use nuxtlink

* feat: card and table made more shadcn

* feat: shadcn statscard

* chore: lint

* feat: shadcn labelchip and locationcard

* feat: shadcn locations page

* refactor: remove unused new item page

* chore: lint

* feat: shadcn item card

* fix: wrapping of location and lint

* feat: ctrl enter in text area in form submits form

* feat: begin shadcn locations page and remove pageqrcode comp in favour of integrating it into labelmaker

* chore: lint + remove unused code

* fix: remove uneeded margin

* feat: shadcn labels page and fix some issues with location

* feat: shadcn scanner

* chore: lint

* feat: begin shadcning item pages

* feat: shadcn maintenance page

* feat: begin shadcn search page

* fix: quick switch blurry text and crashing page when switching + incorrect z height for create menu

* feat: finish shadcn search page

* chore: lint

* feat: shadcn edit item page

* fix: quickmenumodal bug

* feat: shadcn item details page

* feat: remove all non-color related daisyui classes

* fix: type error

* fix: quick menu modal again :(
2025-04-20 08:58:03 +01:00

140 lines
4.3 KiB
Vue

<template>
<div class="flex flex-col gap-1">
<Label :for="id" class="px-1">
{{ 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">
<span>
<slot name="display" v-bind="{ item: value }">
{{ displayValue(value) || placeholder }}
</slot>
</span>
<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="searchPlaceholder" :display-value="_ => ''" />
<CommandEmpty>
{{ noResultsText }}
</CommandEmpty>
<CommandList>
<CommandGroup>
<CommandItem v-for="item in filtered" :key="itemKey(item)" :value="itemKey(item)" @select="select(item)">
<Check :class="cn('mr-2 h-4 w-4', isSelected(item) ? 'opacity-100' : 'opacity-0')" />
<slot name="display" v-bind="{ item }">
{{ displayValue(item) }}
</slot>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from "vue";
import { Check, ChevronsUpDown } from "lucide-vue-next";
import fuzzysort from "fuzzysort";
import { useVModel } from "@vueuse/core";
import { Button } from "~/components/ui/button";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "~/components/ui/command";
import { Label } from "~/components/ui/label";
import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover";
import { cn } from "~/lib/utils";
import { useId } from "#imports";
type ItemsObject = {
[key: string]: unknown;
};
interface Props {
label: string;
modelValue: string | ItemsObject | null | undefined;
items: ItemsObject[] | string[];
itemText?: string;
itemValue?: string;
search?: string;
searchPlaceholder?: string;
noResultsText?: string;
placeholder?: string;
}
const emit = defineEmits(["update:modelValue", "update:search"]);
const props = withDefaults(defineProps<Props>(), {
label: "",
modelValue: "",
items: () => [],
itemText: "text",
itemValue: "value",
search: "",
searchPlaceholder: "Type to search...",
noResultsText: "No Results Found",
placeholder: "Select...",
});
const id = useId();
const open = ref(false);
const search = ref(props.search);
const value = useVModel(props, "modelValue", emit);
watch(
() => props.search,
val => {
search.value = val;
}
);
watch(
() => search.value,
val => {
emit("update:search", val);
}
);
function isStrings(arr: string[] | ItemsObject[]): arr is string[] {
return typeof arr[0] === "string";
}
function displayValue(item: string | ItemsObject | null | undefined): string {
if (!item) return "";
if (typeof item === "string") return item;
return (item[props.itemText] as string) || "";
}
function itemKey(item: string | ItemsObject): string {
if (typeof item === "string") return item;
return (item[props.itemValue] as string) || displayValue(item);
}
function isSelected(item: string | ItemsObject): boolean {
if (!value.value) return false;
if (typeof item === "string") return value.value === item;
if (typeof value.value === "string") return itemKey(item) === value.value;
return itemKey(item) === itemKey(value.value);
}
function select(item: string | ItemsObject) {
if (isSelected(item)) {
value.value = null;
} else {
value.value = item;
}
open.value = false;
}
const filtered = computed(() => {
if (!search.value) return props.items;
if (isStrings(props.items)) {
return props.items.filter(item => item.toLowerCase().includes(search.value.toLowerCase()));
} else {
// Fuzzy search on itemText
return fuzzysort.go(search.value, props.items, { key: props.itemText, all: true }).map(i => i.obj);
}
});
</script>