feat(control): better ux, inline create functionality for link

This commit is contained in:
raizasafeel
2026-03-09 23:58:46 +05:30
parent 5a0bbae746
commit 29d11a42df
2 changed files with 94 additions and 26 deletions
@@ -9,7 +9,7 @@
nullable nullable
v-slot="{ open: isComboboxOpen }" v-slot="{ open: isComboboxOpen }"
> >
<Popover class="w-full" v-model:show="showOptions"> <Popover class="w-full" v-model:show="showOptions" :matchTargetWidth="true">
<template #target="{ open: openPopover, togglePopover }"> <template #target="{ open: openPopover, togglePopover }">
<slot name="target" v-bind="{ open: openPopover, togglePopover }"> <slot name="target" v-bind="{ open: openPopover, togglePopover }">
<div class="w-full"> <div class="w-full">
@@ -92,7 +92,8 @@
> >
<li <li
:class="[ :class="[
'flex items-center rounded px-2.5 py-2 text-base', 'flex items-center rounded px-2.5 text-base py-1.5',
optionLines(option).secondary ? '' : 'h-7',
{ 'bg-surface-gray-2': active }, { 'bg-surface-gray-2': active },
]" ]"
> >
@@ -104,16 +105,15 @@
name="item-label" name="item-label"
v-bind="{ active, selected, option }" v-bind="{ active, selected, option }"
> >
<div class="flex flex-col gap-1 p-1"> <div class="flex flex-col px-1" :class="optionLines(option).secondary ? 'gap-0.5' : ''">
<div class="text-base font-medium text-ink-gray-8"> <div class="text-base font-medium text-ink-gray-8">
{{ {{ optionLines(option).primary }}
option.value == option.label && option.description
? option.description
: option.label
}}
</div> </div>
<div class="text-sm text-ink-gray-5"> <div
{{ option.value }} v-if="optionLines(option).secondary"
class="text-sm text-ink-gray-5"
>
{{ optionLines(option).secondary }}
</div> </div>
</div> </div>
</slot> </slot>
@@ -245,6 +245,18 @@ function filterOptions(options) {
}) })
} }
function optionLines(option) {
const primary = option.label
let secondary = null
if (option.description && option.description !== primary) {
secondary = option.description
} else if (option.value && option.value !== primary) {
secondary = option.value
}
return { primary, secondary }
}
function displayValue(option) { function displayValue(option) {
if (typeof option === 'string') { if (typeof option === 'string') {
let allOptions = groups.value.flatMap((group) => group.items) let allOptions = groups.value.flatMap((group) => group.items)
+72 -16
View File
@@ -30,28 +30,48 @@
</template> </template>
<template #footer="{ value, close }"> <template #footer="{ value, close }">
<div v-if="attrs.onCreate"> <div v-if="creating" class="flex items-center gap-1">
<Button <button
variant="ghost" class="p-1 rounded hover:bg-surface-gray-3 text-ink-gray-5"
class="w-full !justify-start" @click="creating = false"
:label="__('Create New')" :aria-label="__('Cancel')"
@click="attrs.onCreate(value, close)"
> >
<template #prefix> <ArrowLeft class="size-4 stroke-1.5" />
<Plus class="h-4 w-4 stroke-1.5" /> </button>
</template> <FormControl
v-model="newItemName"
class="flex-1 min-w-0"
size="sm"
:placeholder="__(props.inlineCreatePlaceholder)"
/>
<Button
variant="solid"
size="sm"
:disabled="!newItemName.trim()"
@click="submitCreate"
:aria-label="__('Create')"
>
{{ __('Create') }}
</Button> </Button>
</div> </div>
<div> <div v-else class="flex justify-between">
<Button <Button
variant="ghost" variant="ghost"
class="w-full !justify-start"
:label="__('Clear')"
@click="() => clearValue(close)" @click="() => clearValue(close)"
:aria-label="__('Clear')"
>
{{ __('Clear') }}
</Button>
<Button
v-if="props.onCreate"
variant="ghost"
@click="handleCreate(close)"
:aria-label="__('Create New')"
> >
<template #prefix> <template #prefix>
<X class="h-4 w-4 stroke-1.5" /> <Plus class="size-4 stroke-1.5" />
</template> </template>
{{ __('Create New') }}
</Button> </Button>
</div> </div>
</template> </template>
@@ -63,8 +83,8 @@
<script setup> <script setup>
import Autocomplete from '@/components/Controls/Autocomplete.vue' import Autocomplete from '@/components/Controls/Autocomplete.vue'
import { watchDebounced } from '@vueuse/core' import { watchDebounced } from '@vueuse/core'
import { createResource, Button } from 'frappe-ui' import { createResource, Button, FormControl } from 'frappe-ui'
import { Plus, X } from 'lucide-vue-next' import { Plus, ArrowLeft } from 'lucide-vue-next'
import { useAttrs, computed, ref } from 'vue' import { useAttrs, computed, ref } from 'vue'
import { useSettings } from '@/stores/settings' import { useSettings } from '@/stores/settings'
@@ -85,11 +105,25 @@ const props = defineProps({
type: String, type: String,
default: '', default: '',
}, },
inlineCreate: {
type: Boolean,
default: false,
},
inlineCreatePlaceholder: {
type: String,
default: 'Enter...',
},
onCreate: {
type: Function,
default: null,
},
}) })
const emit = defineEmits(['update:modelValue', 'change']) const emit = defineEmits(['update:modelValue', 'change'])
const attrs = useAttrs() const attrs = useAttrs()
const valuePropPassed = computed(() => 'value' in attrs) const valuePropPassed = computed(() => 'value' in attrs)
const creating = ref(false)
const newItemName = ref('')
const value = computed({ const value = computed({
get: () => (valuePropPassed.value ? attrs.value : props.modelValue), get: () => (valuePropPassed.value ? attrs.value : props.modelValue),
@@ -105,6 +139,26 @@ const autocomplete = ref(null)
const text = ref('') const text = ref('')
const settingsStore = useSettings() const settingsStore = useSettings()
function handleCreate(close) {
if (props.inlineCreate) {
creating.value = true
return
}
if (props.onCreate) {
props.onCreate(null, close)
}
}
function submitCreate() {
if (!newItemName.value.trim() || !props.onCreate) return
const value = newItemName.value.trim()
props.onCreate(value, () => {
creating.value = false
newItemName.value = ''
reload()
})
}
watchDebounced( watchDebounced(
() => autocomplete.value?.query, () => autocomplete.value?.query,
(val) => { (val) => {
@@ -153,7 +207,7 @@ const options = createResource({
}, },
}) })
const reload = (val) => { const reload = (val = '') => {
options.update({ options.update({
params: { params: {
txt: val, txt: val,
@@ -178,4 +232,6 @@ const labelClasses = computed(() => {
'text-ink-gray-5', 'text-ink-gray-5',
] ]
}) })
defineExpose({ reload })
</script> </script>