diff --git a/frontend/src/components/Controls/Autocomplete.vue b/frontend/src/components/Controls/Autocomplete.vue index 2d3cea03..0a1616bf 100644 --- a/frontend/src/components/Controls/Autocomplete.vue +++ b/frontend/src/components/Controls/Autocomplete.vue @@ -1,142 +1,110 @@ @@ -147,15 +115,16 @@ import { ComboboxInput, ComboboxOptions, ComboboxOption, + ComboboxButton, } from '@headlessui/vue' -import { Popover } from 'frappe-ui' +import { ref, computed, useAttrs, useSlots, watch } from 'vue' import { ChevronDown, X } from 'lucide-vue-next' -import { ref, computed, useAttrs, useSlots, watch, nextTick } from 'vue' +import { watchDebounced } from '@vueuse/core' const props = defineProps({ modelValue: { - type: String, - default: '', + type: [String, Object], + default: null, }, options: { type: Array, @@ -186,107 +155,95 @@ const props = defineProps({ default: true, }, }) -const emit = defineEmits(['update:modelValue', 'update:query', 'change']) -const query = ref('') -const showOptions = ref(false) -const search = ref(null) +const emit = defineEmits(['update:modelValue', 'update:query', 'change']) const attrs = useAttrs() const slots = useSlots() +const selectedValue = ref(props.modelValue) +const query = ref('') const valuePropPassed = computed(() => 'value' in attrs) -const selectedValue = computed({ - get() { - return valuePropPassed.value ? attrs.value : props.modelValue - }, - set(val) { - query.value = '' - if (val) { - showOptions.value = false - } - emit(valuePropPassed.value ? 'change' : 'update:modelValue', val) - }, +watch(selectedValue, (val) => { + if (!val?.value) return + query.value = '' + console.log('Selected value changed:', val) + emit(valuePropPassed.value ? 'change' : 'update:modelValue', val) }) -function close() { - showOptions.value = false +function clearValue() { + emit('update:modelValue', null) } const groups = computed(() => { - if (!props.options || props.options.length == 0) return [] + if (!props.options?.length) return [] - let groups = props.options[0]?.group + const normalized = props.options[0]?.group ? props.options : [{ group: '', items: props.options }] - - return groups - .map((group, i) => { - return { - key: i, - group: group.group, - hideLabel: group.hideLabel || false, - items: props.filterable ? filterOptions(group.items) : group.items, - } - }) + return normalized + .map((group, i) => ({ + key: i, + group: group.group, + hideLabel: group.hideLabel || false, + items: props.filterable ? filterOptions(group.items) : group.items, + })) .filter((group) => group.items.length > 0) }) function filterOptions(options) { - if (!query.value) { - return options - } - return options.filter((option) => { - let searchTexts = [option.label, option.value] - return searchTexts.some((text) => - (text || '').toString().toLowerCase().includes(query.value.toLowerCase()) - ) - }) + if (!query.value) return options + const q = query.value.toLowerCase() + return options.filter((option) => + [option.label, option.value] + .filter(Boolean) + .some((text) => text.toString().toLowerCase().includes(q)) + ) } function displayValue(option) { + if (!option) return '' + if (typeof option === 'string') { - let allOptions = groups.value.flatMap((group) => group.items) - let selectedOption = allOptions.find((o) => o.value === option) - return selectedOption?.label || option + const flat = groups.value.flatMap((g) => g.items) + const match = flat.find((o) => o.value === option) + return match?.label || option } - return option?.label + + return option.label } -watch(query, (q) => { - emit('update:query', q) -}) +watchDebounced( + query, + (val) => { + emit('update:query', val) + }, + { debounce: 300 } +) -watch(showOptions, (val) => { - if (val) { - nextTick(() => { - search.value.el.focus() - }) - } -}) - -const textColor = computed(() => { - return props.disabled ? 'text-ink-gray-5' : 'text-ink-gray-8' -}) +const textColor = computed(() => + props.disabled ? 'text-ink-gray-5' : 'text-ink-gray-8' +) const inputClasses = computed(() => { - let sizeClasses = { + const sizeClasses = { sm: 'text-base rounded h-7', md: 'text-base rounded h-8', lg: 'text-lg rounded-md h-10', xl: 'text-xl rounded-md h-10', }[props.size] - let paddingClasses = { + const paddingClasses = { sm: 'py-1.5 px-2', md: 'py-1.5 px-2.5', lg: 'py-1.5 px-3', xl: 'py-1.5 px-3', }[props.size] - let variant = props.disabled ? 'disabled' : props.variant - let variantClasses = { + const variant = props.disabled ? 'disabled' : props.variant + + const variantClasses = { subtle: 'border border-gray-100 bg-surface-gray-2 placeholder-ink-gray-4 hover:border-outline-gray-modals hover:bg-surface-gray-3 focus:bg-surface-white focus:border-outline-gray-4 focus:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-outline-gray-3', outline: @@ -307,6 +264,4 @@ const inputClasses = computed(() => { 'transition-colors w-full', ] }) - -defineExpose({ query }) diff --git a/frontend/src/components/Controls/Link.vue b/frontend/src/components/Controls/Link.vue index 5742a6fe..91175399 100644 --- a/frontend/src/components/Controls/Link.vue +++ b/frontend/src/components/Controls/Link.vue @@ -11,7 +11,6 @@ :size="attrs.size || 'sm'" :variant="attrs.variant" :placeholder="attrs.placeholder" - :filterable="false" :readonly="attrs.readonly" >