cargagep-v2/frontend/src/components/UnitPicker.vue
Antoine Pelletier 7c38a1e606 wip
2026-08-25 11:09:50 +02:00

91 lines
3.1 KiB
Vue

<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { Input } from '@/components/ui/input'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import type { Unit } from '@/utils/types'
export type UnitChoice =
| { kind: 'known'; id: number; name: string }
| { kind: 'free'; name: string }
const model = defineModel<UnitChoice | undefined>()
const props = defineProps<{ units: Unit[]; id?: string; invalid?: boolean }>()
const FREE = '__free__'
const selected = ref<string>(
model.value?.kind === 'known' ? String(model.value.id) : model.value ? FREE : '',
)
const freeName = ref(model.value?.kind === 'free' ? model.value.name : '')
const onlyFree = computed(() => props.units.length === 0)
const showFreeInput = computed(() => onlyFree.value || selected.value === FREE)
/** The choice the two controls above currently stand for */
const local = computed<UnitChoice | undefined>(() => {
if (showFreeInput.value) {
const name = freeName.value.trim()
// An empty box is an unfinished choice, not another association: the field
// reads as unfilled, while the box itself stays open to type in.
return name ? { kind: 'free', name } : undefined
}
const unit = props.units.find((u) => String(u.id) === selected.value)
return unit ? { kind: 'known', id: unit.id, name: unit.name } : undefined
})
function sameChoice(left: UnitChoice | undefined, right: UnitChoice | undefined) {
if (!left || !right) return left === right
if (left.kind === 'known') return right.kind === 'known' && left.id === right.id
return right.kind === 'free' && left.name === right.name
}
watch(local, (choice) => {
model.value = choice
})
/**
* Follows the model when it says something other than what the controls say —
* a form being reset, mostly.
*
* Comparing the two, rather than watching for any change, is what keeps an
* emptied text box open: clearing it does report "nothing chosen", and that
* report must not be read back as somebody else cancelling the choice.
*/
watch(model, (choice) => {
if (sameChoice(choice, local.value)) return
selected.value = choice?.kind === 'known' ? String(choice.id) : choice ? FREE : ''
freeName.value = choice?.kind === 'free' ? choice.name : ''
})
</script>
<template>
<div class="grid gap-2">
<Select v-if="!onlyFree" v-model="selected">
<SelectTrigger :id="id" :aria-invalid="invalid || undefined" class="w-full">
<SelectValue :placeholder="$t('reservation.association-placeholder')" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="unit in units" :key="unit.id" :value="String(unit.id)">
{{ unit.name }}
</SelectItem>
<SelectItem :value="FREE">{{ $t('reservation.association-other') }}</SelectItem>
</SelectContent>
</Select>
<Input
v-if="showFreeInput"
:id="onlyFree ? id : undefined"
v-model="freeName"
:aria-label="$t('reservation.association')"
:placeholder="$t('reservation.association-placeholder')"
:aria-invalid="invalid || undefined"
/>
</div>
</template>