extract search functionality

This commit is contained in:
Hayden 2022-10-23 19:47:29 -08:00
parent 55724f4c17
commit b8374d8cc8
4 changed files with 231 additions and 7 deletions

View file

@ -0,0 +1,149 @@
<template>
<div ref="menu" class="form-control w-full">
<label class="label">
<span class="label-text">{{ label }}</span>
</label>
<div class="dropdown dropdown-top sm:dropdown-end">
<div class="relative">
<input
v-model="isearch"
tabindex="0"
class="input w-full items-center flex flex-wrap border border-gray-400 rounded-lg"
@keyup.enter="selectFirst"
/>
<button
v-if="!!modelValue && Object.keys(modelValue).length !== 0"
style="transform: translateY(-50%)"
class="top-1/2 absolute right-2 btn btn-xs btn-circle no-animation"
@click="clear"
>
x
</button>
</div>
<ul
tabindex="0"
style="display: inline"
class="dropdown-content mb-1 menu shadow border border-gray-400 rounded bg-base-100 w-full z-[9999] max-h-60 overflow-y-scroll"
>
<li v-for="(obj, idx) in filtered" :key="idx">
<button type="button" @click="select(obj)">
{{ usingObjects ? obj[itemText] : obj }}
</button>
</li>
<li class="hidden first:flex">
<button disabled>
{{ noResultsText }}
</button>
</li>
</ul>
</div>
</div>
</template>
<script lang="ts" setup>
type ItemsObject = {
text?: string;
value?: string;
[key: string]: unknown;
};
interface Props {
label: string;
modelValue: string | ItemsObject;
items: string[] | ItemsObject[];
itemText?: keyof ItemsObject;
itemValue?: keyof ItemsObject;
search?: string;
noResultsText?: string;
}
const emit = defineEmits(["update:modelValue", "update:search"]);
const props = withDefaults(defineProps<Props>(), {
label: "",
modelValue: "",
items: () => [],
itemText: "text",
itemValue: "value",
search: "",
noResultsText: "No Results Found",
});
function clear() {
select(value.value);
}
const isearch = ref("");
watch(isearch, () => {
internalSearch.value = isearch.value;
});
const internalSearch = useVModel(props, "search", emit);
const value = useVModel(props, "modelValue", emit);
const usingObjects = computed(() => {
return props.items.length > 0 && typeof props.items[0] === "object";
});
/**
* isStrings is a type guard function to check if the items are an array of string
*/
function isStrings(_arr: string[] | ItemsObject[]): _arr is string[] {
return !usingObjects.value;
}
function selectFirst() {
if (filtered.value.length > 0) {
select(filtered.value[0]);
}
}
watch(
value,
() => {
if (value.value) {
if (typeof value.value === "string") {
isearch.value = value.value;
} else {
isearch.value = value.value[props.itemText] as string;
}
}
},
{
immediate: true,
}
);
function select(obj: string | ItemsObject) {
if (isStrings(props.items)) {
if (obj === value.value) {
value.value = "";
return;
}
value.value = obj;
} else {
if (obj === value.value) {
value.value = {};
return;
}
value.value = obj;
}
}
const filtered = computed(() => {
if (!isearch.value || isearch.value === "") {
return props.items;
}
if (isStrings(props.items)) {
return props.items.filter(item => item.toLowerCase().includes(isearch.value.toLowerCase()));
} else {
return props.items.filter(item => {
if (props.itemText && props.itemText in item) {
return (item[props.itemText] as string).toLowerCase().includes(isearch.value.toLowerCase());
}
return false;
});
}
});
</script>

View file

@ -0,0 +1,36 @@
import { ItemSummary, LabelSummary, LocationSummary } from "~~/lib/api/types/data-contracts";
import { UserClient } from "~~/lib/api/user";
type SearchOptions = {
immediate?: boolean;
};
export function useItemSearch(client: UserClient, opts?: SearchOptions) {
const query = ref("");
const locations = ref<LocationSummary[]>([]);
const labels = ref<LabelSummary[]>([]);
const results = ref<ItemSummary[]>([]);
watchDebounced(query, search, { debounce: 250, maxWait: 1000 });
async function search() {
const locIds = locations.value.map(l => l.id);
const labelIds = labels.value.map(l => l.id);
const { data, error } = await client.items.getAll({ q: query.value, locations: locIds, labels: labelIds });
if (error) {
return;
}
results.value = data.items;
}
if (opts?.immediate) {
search();
}
return {
query,
results,
locations,
labels,
};
}

View file

@ -4,6 +4,7 @@
import { useLabelStore } from "~~/stores/labels"; import { useLabelStore } from "~~/stores/labels";
import { useLocationStore } from "~~/stores/locations"; import { useLocationStore } from "~~/stores/locations";
import { capitalize } from "~~/lib/strings"; import { capitalize } from "~~/lib/strings";
import Autocomplete from "~~/components/Form/Autocomplete.vue";
definePageMeta({ definePageMeta({
middleware: ["auth"], middleware: ["auth"],
@ -37,6 +38,10 @@
} }
} }
if (data.parent) {
parent.value = data.parent;
}
return data; return data;
}); });
@ -49,7 +54,7 @@
...item.value, ...item.value,
locationId: item.value.location?.id, locationId: item.value.location?.id,
labelIds: item.value.labels.map(l => l.id), labelIds: item.value.labels.map(l => l.id),
parentId: null, parentId: parent.value ? parent.value.id : null,
}; };
const { error } = await api.items.update(itemId.value, payload); const { error } = await api.items.update(itemId.value, payload);
@ -258,7 +263,6 @@
async function updateAttachment() { async function updateAttachment() {
editState.loading = true; editState.loading = true;
console.log(editState.type);
const { error, data } = await api.items.updateAttachment(itemId.value, editState.id, { const { error, data } = await api.items.updateAttachment(itemId.value, editState.id, {
title: editState.title, title: editState.title,
type: editState.type, type: editState.type,
@ -308,6 +312,9 @@
timeValue: null, timeValue: null,
}); });
} }
const { query, results } = useItemSearch(api, { immediate: false });
const parent = ref();
</script> </script>
<template> <template>
@ -364,6 +371,16 @@
compare-key="id" compare-key="id"
/> />
<FormMultiselect v-model="item.labels" label="Labels" :items="labels ?? []" /> <FormMultiselect v-model="item.labels" label="Labels" :items="labels ?? []" />
<Autocomplete
v-if="!preferences.editorSimpleView"
v-model="parent"
v-model:search="query"
:items="results"
item-text="name"
label="Parent Item"
no-results-text="Type to search..."
/>
</div> </div>
<div class="border-t border-gray-300 sm:p-0"> <div class="border-t border-gray-300 sm:p-0">

View file

@ -76,6 +76,10 @@
name: "Description", name: "Description",
text: item.value?.description, text: item.value?.description,
}, },
{
name: "Quantity",
text: item.value?.quantity,
},
{ {
name: "Serial Number", name: "Serial Number",
text: item.value?.serialNumber, text: item.value?.serialNumber,
@ -272,12 +276,23 @@
<span class="text-base-content"> <span class="text-base-content">
{{ item ? item.name : "" }} {{ item ? item.name : "" }}
</span> </span>
<div v-if="item.parent" class="text-sm breadcrumbs pb-0">
<ul class="text-base-content/70">
<li>
<NuxtLink :to="`/item/${item.parent.id}`"> {{ item.parent.name }}</NuxtLink>
</li>
<li>{{ item.name }}</li>
</ul>
</div>
<template #description> <template #description>
<p class="text-sm text-base-content font-bold pb-0 mb-0"> <div class="flex flex-wrap gap-2 mt-3">
{{ item.location.name }} - Quantity {{ item.quantity }} <NuxtLink ref="badge" class="badge p-3" :to="`/location/${item.location.id}`">
</p> <Icon name="heroicons-map-pin" class="mr-2 swap-on"></Icon>
<div v-if="item.labels && item.labels.length > 0" class="flex flex-wrap gap-3 mt-3"> {{ item.location.name }}
<LabelChip v-for="label in item.labels" :key="label.id" class="badge-primary" :label="label" /> </NuxtLink>
<template v-if="item.labels && item.labels.length > 0">
<LabelChip v-for="label in item.labels" :key="label.id" class="badge-primary" :label="label" />
</template>
</div> </div>
</template> </template>
</BaseSectionHeader> </BaseSectionHeader>
@ -363,5 +378,12 @@
</BaseCard> </BaseCard>
</div> </div>
</section> </section>
<section class="my-6 px-3">
<BaseSectionHeader v-if="item && item.children && item.children.length > 0"> Child Items </BaseSectionHeader>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<ItemCard v-for="child in item.children" :key="child.id" :item="child" />
</div>
</section>
</BaseContainer> </BaseContainer>
</template> </template>