118 lines
3.2 KiB
Vue
118 lines
3.2 KiB
Vue
<script setup lang="ts">
|
|
import { t } from '@/i18n'
|
|
|
|
import { computed } from 'vue'
|
|
import AuthorAvatar from '@/components/AuthorAvatar.vue'
|
|
import type { LinkableUser } from '@/lib/directoryTypes'
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
users: LinkableUser[]
|
|
status: 'loading' | 'ready' | 'error'
|
|
idlePlaceholder?: string
|
|
clearable?: boolean
|
|
disabled?: boolean
|
|
invalid?: boolean
|
|
}>(),
|
|
{ idlePlaceholder: '', clearable: false, disabled: false, invalid: false },
|
|
)
|
|
|
|
const emit = defineEmits<{ retry: []; open: [] }>()
|
|
|
|
const menuOpen = defineModel<boolean>('open', { default: false })
|
|
|
|
const userId = defineModel<number | null>({ default: null })
|
|
|
|
const items = computed(() =>
|
|
props.users.length === 0
|
|
? []
|
|
: [
|
|
{ type: 'label' as const, label: t('Users without an author') },
|
|
...props.users.map((user) => ({
|
|
label: user.label,
|
|
description: user.email,
|
|
value: user.id,
|
|
avatarName: user.label,
|
|
})),
|
|
],
|
|
)
|
|
|
|
const placeholder = computed(() =>
|
|
menuOpen.value || !props.idlePlaceholder ? t('Choose a user') : props.idlePlaceholder,
|
|
)
|
|
const chosen = computed(() => props.users.find((user) => user.id === userId.value))
|
|
|
|
function onOpen(value: boolean) {
|
|
if (value) {
|
|
emit('open')
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<USelectMenu
|
|
v-model="userId"
|
|
v-model:open="menuOpen"
|
|
:portal="false"
|
|
:content="{ position: 'inline' }"
|
|
:items="items"
|
|
value-key="value"
|
|
:search-input="false"
|
|
:placeholder="placeholder"
|
|
:loading="status === 'loading'"
|
|
:disabled="disabled"
|
|
:color="invalid ? 'error' : undefined"
|
|
:highlight="invalid"
|
|
:clear="clearable && userId !== null"
|
|
class="w-full"
|
|
:ui="{
|
|
base: 'h-11 rounded-none text-lg ring-(--ui-border-accented) md:h-10 md:text-sm',
|
|
trailingIcon: 'size-3.5 text-muted',
|
|
content: 'rounded-none shadow-(--shadow-popover)',
|
|
label: 'px-2 py-1.5 text-xs text-muted',
|
|
item: 'min-h-12 gap-2 px-2 py-1 data-[state=checked]:bg-elevated md:min-h-9',
|
|
itemDescription: 'truncate text-xs text-muted',
|
|
placeholder: 'text-muted',
|
|
}"
|
|
@update:open="onOpen"
|
|
>
|
|
<template #default>
|
|
<span v-if="chosen" class="truncate text-highlighted">{{ chosen.label }}</span>
|
|
<span v-else class="truncate text-muted">{{ placeholder }}</span>
|
|
</template>
|
|
<template #item-leading="{ item }">
|
|
<AuthorAvatar v-if="'avatarName' in item" :name="String(item.avatarName)" :size="24" />
|
|
</template>
|
|
<template #empty>
|
|
<div v-if="status === 'error'" class="flex flex-col items-start gap-2 p-3">
|
|
<span class="text-error">{{ t("Couldn't load users") }}</span>
|
|
<UButton
|
|
color="neutral"
|
|
variant="outline"
|
|
icon="i-lucide-rotate-cw"
|
|
class="h-11 gap-1.5 border border-accented bg-default px-2.5 text-xs text-highlighted ring-0 md:h-7"
|
|
:ui="{ leadingIcon: 'size-3.5' }"
|
|
@click="emit('retry')"
|
|
>
|
|
{{ t('Try again') }}
|
|
</UButton>
|
|
</div>
|
|
<div
|
|
v-else-if="status === 'loading'"
|
|
class="flex flex-col gap-2.5 p-3"
|
|
aria-busy="true"
|
|
>
|
|
<USkeleton
|
|
v-for="n in 3"
|
|
:key="n"
|
|
class="h-3 rounded-none bg-accented"
|
|
:class="n === 3 ? 'w-3/5' : 'w-4/5'"
|
|
/>
|
|
</div>
|
|
<div v-else class="p-3 text-muted">
|
|
{{ t('Every user already has an author.') }}
|
|
</div>
|
|
</template>
|
|
</USelectMenu>
|
|
</template>
|