mirror of
https://github.com/frappe/lms.git
synced 2026-05-02 13:39:31 +03:00
feat(control): better ux, inline create functionality for link
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user