Merge branch 'dev' of https://github.com/dikstub-rssa/simrs-fe into feat/user
This commit is contained in:
No files matched your search
@@ -0,0 +1,112 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '~/lib/utils'
|
||||
|
||||
interface Item {
|
||||
value: string
|
||||
label: string
|
||||
code?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
id: string
|
||||
modelValue?: string
|
||||
items: Item[]
|
||||
placeholder?: string
|
||||
searchPlaceholder?: string
|
||||
emptyMessage?: string
|
||||
class?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
const open = ref(false)
|
||||
|
||||
const selectedItem = computed(() =>
|
||||
props.items.find(item => item.value === props.modelValue),
|
||||
)
|
||||
|
||||
const displayText = computed(() =>
|
||||
selectedItem.value?.label || props.placeholder || '---pilih item',
|
||||
)
|
||||
|
||||
// Create searchable items with combined code and label for better search
|
||||
// Sort by:
|
||||
// 1. Selected item first (highest priority)
|
||||
// 2. Then by label alphabetically
|
||||
const searchableItems = computed(() => {
|
||||
const itemsWithSearch = props.items.map(item => ({
|
||||
...item,
|
||||
searchValue: `${item.code || ''} ${item.label}`.trim(),
|
||||
isSelected: item.value === props.modelValue,
|
||||
}))
|
||||
|
||||
return itemsWithSearch.sort((a, b) => {
|
||||
// Selected item always comes first
|
||||
if (a.isSelected && !b.isSelected) return -1
|
||||
if (!a.isSelected && b.isSelected) return 1
|
||||
|
||||
// If neither or both are selected, sort by label alphabetically
|
||||
return a.label.localeCompare(b.label)
|
||||
})
|
||||
})
|
||||
|
||||
function onSelect(item: Item) {
|
||||
emit('update:modelValue', item.value)
|
||||
open.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Popover v-model:open="open">
|
||||
<PopoverTrigger as-child>
|
||||
<Button
|
||||
:id="props.id"
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
:aria-expanded="open"
|
||||
:aria-controls="`${props.id}-list`"
|
||||
:aria-describedby="`${props.id}-search`"
|
||||
:class="cn(
|
||||
'w-full justify-between border-black bg-white hover:bg-gray-50 text-sm font-normal',
|
||||
!modelValue && 'text-muted-foreground',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
{{ displayText }}
|
||||
<Icon name="i-lucide-chevrons-up-down" class="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-full p-0">
|
||||
<Command>
|
||||
<CommandInput
|
||||
:id="`${props.id}-search`"
|
||||
class="h-9"
|
||||
:placeholder="searchPlaceholder || 'Cari...'"
|
||||
:aria-label="`Cari ${displayText}`"
|
||||
/>
|
||||
<CommandEmpty>{{ emptyMessage || 'Item tidak ditemukan.' }}</CommandEmpty>
|
||||
<CommandList :id="`${props.id}-list`" role="listbox">
|
||||
<CommandGroup>
|
||||
<CommandItem v-for="item in searchableItems" :key="item.value" :value="item.searchValue" @select="onSelect(item)">
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<span>{{ item.label }}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span v-if="item.code" class="text-xs text-muted-foreground">{{ item.code }}</span>
|
||||
<Icon
|
||||
name="i-lucide-check"
|
||||
:class="cn(
|
||||
'h-4 w-4',
|
||||
modelValue === item.value ? 'opacity-100' : 'opacity-0',
|
||||
)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</template>
|
||||
@@ -1,12 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
export interface XError {
|
||||
message: string
|
||||
[key: string]: any
|
||||
}
|
||||
import type { XErrors } from '~/types/error'
|
||||
|
||||
defineProps<{
|
||||
id?: string
|
||||
errors?: Record<string, XError>
|
||||
errors?: XErrors
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -14,7 +11,7 @@ defineProps<{
|
||||
<div class="grow">
|
||||
<slot />
|
||||
<div v-if="id && errors?.[id]" class="field-error-info">
|
||||
{{ errors[id].message }}
|
||||
{{ errors[id]?.message }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
labelFor: string
|
||||
size?: 'default' | 'narrow' | 'wide'
|
||||
height?: 'default' | 'compact'
|
||||
position?: 'default' | 'dynamic'
|
||||
@@ -47,7 +48,7 @@ const labelClass = computed(() => [props.stacked ? 'block mb-1 text-sm font-medi
|
||||
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<label :class="labelClass">
|
||||
<label :class="labelClass" :for="labelFor">
|
||||
<slot />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<script setup lang="ts">
|
||||
import { SelectRoot } from 'radix-vue'
|
||||
import {
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '~/components/pub/ui/select'
|
||||
import { cn } from '~/lib/utils'
|
||||
|
||||
interface Item {
|
||||
value: string
|
||||
label: string
|
||||
code?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue?: string
|
||||
items: Item[]
|
||||
placeholder?: string
|
||||
label?: string
|
||||
separator?: boolean
|
||||
class?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
// Sort items with selected item first, then alphabetically
|
||||
const sortedItems = computed(() => {
|
||||
const itemsWithSelection = props.items.map(item => ({
|
||||
...item,
|
||||
isSelected: item.value === props.modelValue,
|
||||
}))
|
||||
|
||||
return itemsWithSelection.sort((a, b) => {
|
||||
// Selected item always comes first
|
||||
if (a.isSelected && !b.isSelected) return -1
|
||||
if (!a.isSelected && b.isSelected) return 1
|
||||
|
||||
// If neither or both are selected, sort by label alphabetically
|
||||
return a.label.localeCompare(b.label)
|
||||
})
|
||||
})
|
||||
|
||||
function onValueChange(value: string) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectRoot :model-value="modelValue" @update:model-value="onValueChange">
|
||||
<SelectTrigger :class="cn('w-full', props.class)">
|
||||
<SelectValue :placeholder="placeholder || 'Pilih item'" />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel v-if="label">
|
||||
{{ label }}
|
||||
</SelectLabel>
|
||||
|
||||
<SelectItem
|
||||
v-for="item in sortedItems"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<span>{{ item.label }}</span>
|
||||
<span v-if="item.code" class="text-xs text-muted-foreground ml-2">
|
||||
{{ item.code }}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
|
||||
<SelectSeparator v-if="separator" />
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</SelectRoot>
|
||||
</template>
|
||||
Reference in New Issue
Block a user