108 lines
2.9 KiB
Vue
108 lines
2.9 KiB
Vue
<script setup lang="ts">
|
|
import { t } from '@/i18n'
|
|
|
|
import { computed, nextTick, watch } from 'vue'
|
|
import EntryField from '@/components/EntryField.vue'
|
|
import type { BuiltinField } from '@/lib/contentTypes'
|
|
import type { EntryIssues, EntryValues } from '@/lib/entryForm'
|
|
import type { MediaMap, MediaSummary } from '@/lib/mediaField'
|
|
import type { RelationSummaries, RelationSummary } from '@/lib/relationField'
|
|
import { groupFields } from '@/lib/entryLayout'
|
|
|
|
const FOCUSABLE =
|
|
'[data-field-focus], input:not([type="hidden"]), textarea, [role="spinbutton"], [role="switch"], [role="combobox"]'
|
|
|
|
const props = defineProps<{
|
|
fields: BuiltinField[]
|
|
values: EntryValues
|
|
images: MediaMap
|
|
relations: RelationSummaries
|
|
typeName: string
|
|
issues: EntryIssues
|
|
firstIssue: string | null
|
|
focusToken: number
|
|
locked: boolean
|
|
saving: boolean
|
|
creating: boolean
|
|
isDefault: (field: BuiltinField) => boolean
|
|
entryLine: string
|
|
createdLine: string
|
|
updatedLine: string
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
update: [
|
|
key: string,
|
|
value: unknown,
|
|
image?: MediaSummary | null,
|
|
relations?: RelationSummary[],
|
|
]
|
|
submit: []
|
|
}>()
|
|
|
|
const rows = computed(() => groupFields(props.fields))
|
|
|
|
async function focusFirstIssue() {
|
|
await nextTick()
|
|
const key = props.firstIssue
|
|
if (!key) {
|
|
return
|
|
}
|
|
const wrapper = [...document.querySelectorAll<HTMLElement>('[data-entry-field]')].find(
|
|
(candidate) => candidate.dataset.entryField === key,
|
|
)
|
|
const control = wrapper?.querySelector<HTMLElement>(FOCUSABLE)
|
|
control?.focus()
|
|
;(wrapper ?? control)?.scrollIntoView?.({ block: 'center' })
|
|
}
|
|
|
|
watch(() => props.focusToken, focusFirstIssue)
|
|
</script>
|
|
|
|
<template>
|
|
<form
|
|
id="entry-form"
|
|
novalidate
|
|
data-testid="entry-form"
|
|
class="flex max-w-190 flex-col gap-5 border border-default bg-default p-4 transition-opacity md:p-6"
|
|
:class="locked ? 'opacity-70' : ''"
|
|
:aria-busy="saving ? 'true' : undefined"
|
|
@submit.prevent="emit('submit')"
|
|
>
|
|
<div
|
|
v-for="row in rows"
|
|
:key="row.map((field) => field.key).join('+')"
|
|
class="grid gap-4"
|
|
:class="row.length > 1 ? 'md:grid-cols-2' : ''"
|
|
>
|
|
<EntryField
|
|
v-for="field in row"
|
|
:key="field.key"
|
|
:field="field"
|
|
:value="values[field.key]"
|
|
:image="images[field.key] ?? null"
|
|
:relations="relations[field.key] ?? []"
|
|
:type-name="typeName"
|
|
:issue="issues[field.key]"
|
|
:is-default="isDefault(field)"
|
|
:disabled="locked"
|
|
@update="
|
|
(key, value, image, summaries) => emit('update', key, value, image, summaries)
|
|
"
|
|
/>
|
|
</div>
|
|
|
|
<dl
|
|
data-testid="entry-meta"
|
|
class="grid grid-cols-1 gap-x-4 gap-y-1.5 border-t border-default pt-4 text-xs/4.5 md:grid-cols-[auto_1fr]"
|
|
>
|
|
<dt class="text-muted">{{ t('Entry') }}</dt>
|
|
<dd :class="creating ? '' : 'font-mono'">{{ entryLine }}</dd>
|
|
<dt class="text-muted">{{ t('Created by') }}</dt>
|
|
<dd>{{ createdLine }}</dd>
|
|
<dt class="text-muted">{{ t('Updated by') }}</dt>
|
|
<dd>{{ updatedLine }}</dd>
|
|
</dl>
|
|
</form>
|
|
</template>
|