优化手工开票表单校验提示

This commit is contained in:
andy
2026-07-17 17:02:27 +07:00
parent b24bfb5882
commit 56db5cacee
5 changed files with 458 additions and 22 deletions

View File

@@ -315,6 +315,13 @@ export default {
noPdfUrl: 'The backend did not return a PDF URL.',
backendTotals: 'Backend totals',
validationTitle: 'Please fix the form',
fieldErrors: {
required: 'Required',
invalidEmail: 'Invalid email',
dateOrder: 'Invalid date order',
positiveNumber: 'Must be greater than 0',
nonNegativeNumber: 'Cannot be less than 0',
},
statuses: {
SUCCEEDED: 'Generated',
GENERATED: 'Generated',

View File

@@ -315,6 +315,13 @@ export default {
noPdfUrl: 'หลังบ้านไม่ได้ส่งลิงก์ PDF กลับมา',
backendTotals: 'ยอดจากหลังบ้าน',
validationTitle: 'กรุณาแก้ไขแบบฟอร์ม',
fieldErrors: {
required: 'จำเป็นต้องกรอก',
invalidEmail: 'รูปแบบ Email ไม่ถูกต้อง',
dateOrder: 'ลำดับวันที่ไม่ถูกต้อง',
positiveNumber: 'ต้องมากกว่า 0',
nonNegativeNumber: 'ต้องไม่ต่ำกว่า 0',
},
statuses: {
SUCCEEDED: 'สร้างแล้ว',
GENERATED: 'สร้างแล้ว',

View File

@@ -315,6 +315,13 @@ export default {
noPdfUrl: '后端未返回 PDF 链接。',
backendTotals: '后端返回金额',
validationTitle: '请先修正表单',
fieldErrors: {
required: '必填',
invalidEmail: 'Email 格式不正确',
dateOrder: '日期顺序不正确',
positiveNumber: '必须大于 0',
nonNegativeNumber: '不能小于 0',
},
statuses: {
SUCCEEDED: '已生成',
GENERATED: '已生成',

View File

@@ -270,6 +270,62 @@ describe('ReservationManualInvoiceView', () => {
expect(wrapper.text()).not.toContain('例如 includingBF')
})
it('marks required fields and highlights empty inputs after submit', async () => {
const wrapper = mountView()
expect(wrapper.find('label[for="invoice-company"].field-label--required').exists()).toBe(true)
expect(wrapper.find('[data-testid="recipient-company-input"]').classes()).not.toContain('is-invalid')
await wrapper.find('[data-testid="manual-invoice-submit"]').trigger('click')
await flushPromises()
expect(service.generateManualReservationInvoice).not.toHaveBeenCalled()
expect(wrapper.find('[data-testid="recipient-company-input"]').classes()).toContain('is-invalid')
expect(wrapper.find('[data-testid="recipient-company-input"]').attributes('aria-invalid')).toBe('true')
expect(wrapper.find('[data-testid="manual-invoice-field-error-company"]').text()).toBe(
zhCN.manualInvoice.fieldErrors.required,
)
expect(wrapper.find('[data-testid="booking-group-name"]').classes()).toContain('is-invalid')
expect(wrapper.find('[data-testid="primary-room-count"]').classes()).toContain('is-invalid')
expect(wrapper.find('[data-testid="charge-description-0"]').classes()).toContain('is-invalid')
await wrapper.find('[data-testid="recipient-company-input"]').setValue('Manual Company')
expect(wrapper.find('[data-testid="recipient-company-input"]').classes()).not.toContain('is-invalid')
expect(wrapper.find('[data-testid="manual-invoice-field-error-company"]').exists()).toBe(false)
})
it('shows field-level validation messages for invalid email, dates, and numbers', async () => {
const wrapper = mountView()
await wrapper.find('[data-testid="recipient-company-code"]').setValue('QBD')
await fillMinimumInvoiceForm(wrapper)
await wrapper.find('[data-testid="recipient-email"]').setValue('bad-email')
await wrapper.find('[data-testid="booking-departure-date"]').setValue('2026-07-19')
await wrapper.find('[data-testid="booking-extra-bed-rate"]').setValue('-1')
await wrapper.find('[data-testid="charge-rate-0"]').setValue('0')
await wrapper.find('[data-testid="manual-invoice-submit"]').trigger('click')
await flushPromises()
expect(service.generateManualReservationInvoice).not.toHaveBeenCalled()
expect(wrapper.find('[data-testid="recipient-email"]').classes()).toContain('is-invalid')
expect(wrapper.find('[data-testid="manual-invoice-field-error-email"]').text()).toBe(
zhCN.manualInvoice.fieldErrors.invalidEmail,
)
expect(wrapper.find('[data-testid="booking-departure-date"]').classes()).toContain('is-invalid')
expect(wrapper.find('[data-testid="manual-invoice-field-error-departure-date"]').text()).toBe(
zhCN.manualInvoice.fieldErrors.dateOrder,
)
expect(wrapper.find('[data-testid="booking-extra-bed-rate"]').classes()).toContain('is-invalid')
expect(wrapper.find('[data-testid="manual-invoice-field-error-extra-bed-rate"]').text()).toBe(
zhCN.manualInvoice.fieldErrors.nonNegativeNumber,
)
expect(wrapper.find('[data-testid="charge-rate-0"]').classes()).toContain('is-invalid')
expect(wrapper.find('[data-testid="manual-invoice-field-error-charge-0-rate"]').text()).toBe(
zhCN.manualInvoice.fieldErrors.positiveNumber,
)
})
it('shows final-format date previews like the invoice prototype', async () => {
const wrapper = mountView()

View File

@@ -42,7 +42,10 @@
<div class="form-grid">
<div class="field">
<div class="field__heading">
<label for="invoice-company">Company</label>
<label
for="invoice-company"
class="field-label field-label--required"
>Company</label>
</div>
<div class="directory-control directory-control--manual">
<select
@@ -67,14 +70,27 @@
type="text"
autocomplete="off"
:placeholder="t('manualInvoice.companyInputPlaceholder')"
:class="validationClass('company')"
v-bind="validationAria('company')"
required
>
</div>
<p
v-if="fieldError('company')"
:id="fieldErrorId('company')"
class="field-error"
:data-testid="fieldErrorTestId('company')"
>
{{ fieldError('company') }}
</p>
</div>
<div class="field">
<div class="field__heading">
<label for="invoice-attention">Attention</label>
<label
for="invoice-attention"
class="field-label field-label--required"
>Attention</label>
</div>
<div class="directory-control directory-control--manual">
<select
@@ -99,47 +115,90 @@
type="text"
autocomplete="off"
:placeholder="t('manualInvoice.attentionInputPlaceholder')"
:class="validationClass('attention')"
v-bind="validationAria('attention')"
required
>
</div>
<p
v-if="fieldError('attention')"
:id="fieldErrorId('attention')"
class="field-error"
:data-testid="fieldErrorTestId('attention')"
>
{{ fieldError('attention') }}
</p>
</div>
<label class="field field--wide">
<span class="field-label">Address</span>
<span class="field-label field-label--required">Address</span>
<textarea
v-model.trim="form.recipient.address"
data-testid="recipient-address"
rows="3"
:placeholder="t('manualInvoice.addressInputPlaceholder')"
:class="validationClass('address')"
v-bind="validationAria('address')"
required
/>
<span
v-if="fieldError('address')"
:id="fieldErrorId('address')"
class="field-error"
:data-testid="fieldErrorTestId('address')"
>
{{ fieldError('address') }}
</span>
</label>
<label class="field">
<span class="field-label">Tel</span>
<span class="field-label field-label--required">Tel</span>
<input
v-model.trim="form.recipient.telephone"
data-testid="recipient-telephone"
type="tel"
autocomplete="off"
:class="validationClass('telephone')"
v-bind="validationAria('telephone')"
required
>
<span
v-if="fieldError('telephone')"
:id="fieldErrorId('telephone')"
class="field-error"
:data-testid="fieldErrorTestId('telephone')"
>
{{ fieldError('telephone') }}
</span>
</label>
<label class="field">
<span class="field-label">Email</span>
<span class="field-label field-label--required">Email</span>
<input
v-model.trim="form.recipient.email"
data-testid="recipient-email"
type="email"
autocomplete="off"
:class="validationClass('email')"
v-bind="validationAria('email')"
required
>
<span
v-if="fieldError('email')"
:id="fieldErrorId('email')"
class="field-error"
:data-testid="fieldErrorTestId('email')"
>
{{ fieldError('email') }}
</span>
</label>
<div class="field">
<div class="field__heading">
<label for="invoice-booking-date">Booking Date</label>
<label
for="invoice-booking-date"
class="field-label field-label--required"
>Booking Date</label>
</div>
<div class="date-control">
<input
@@ -147,6 +206,8 @@
v-model="form.document.booking_date"
data-testid="document-booking-date"
type="date"
:class="validationClass('booking-date')"
v-bind="validationAria('booking-date')"
required
@input="markDocumentDatesEdited"
>
@@ -158,6 +219,14 @@
<strong>{{ formatDatePreview(form.document.booking_date) }}</strong>
</output>
</div>
<p
v-if="fieldError('booking-date')"
:id="fieldErrorId('booking-date')"
class="field-error"
:data-testid="fieldErrorTestId('booking-date')"
>
{{ fieldError('booking-date') }}
</p>
</div>
</div>
</section>
@@ -177,19 +246,32 @@
<div class="form-grid">
<label class="field field--wide">
<span class="field-label">Group Name</span>
<span class="field-label field-label--required">Group Name</span>
<input
v-model.trim="form.booking.group_name"
data-testid="booking-group-name"
type="text"
autocomplete="off"
:class="validationClass('group-name')"
v-bind="validationAria('group-name')"
required
>
<span
v-if="fieldError('group-name')"
:id="fieldErrorId('group-name')"
class="field-error"
:data-testid="fieldErrorTestId('group-name')"
>
{{ fieldError('group-name') }}
</span>
</label>
<div class="field">
<div class="field__heading">
<label for="invoice-arrival-date">Arrival Date</label>
<label
for="invoice-arrival-date"
class="field-label field-label--required"
>Arrival Date</label>
</div>
<div class="date-control">
<input
@@ -197,6 +279,8 @@
v-model="form.booking.arrival_date"
data-testid="booking-arrival-date"
type="date"
:class="validationClass('arrival-date')"
v-bind="validationAria('arrival-date')"
required
>
<output
@@ -208,11 +292,22 @@
<strong>{{ formatDatePreview(form.booking.arrival_date) }}</strong>
</output>
</div>
<p
v-if="fieldError('arrival-date')"
:id="fieldErrorId('arrival-date')"
class="field-error"
:data-testid="fieldErrorTestId('arrival-date')"
>
{{ fieldError('arrival-date') }}
</p>
</div>
<div class="field">
<div class="field__heading">
<label for="invoice-departure-date">Departure Date</label>
<label
for="invoice-departure-date"
class="field-label field-label--required"
>Departure Date</label>
</div>
<div class="date-control">
<input
@@ -220,6 +315,8 @@
v-model="form.booking.departure_date"
data-testid="booking-departure-date"
type="date"
:class="validationClass('departure-date')"
v-bind="validationAria('departure-date')"
required
>
<output
@@ -231,11 +328,22 @@
<strong>{{ formatDatePreview(form.booking.departure_date) }}</strong>
</output>
</div>
<p
v-if="fieldError('departure-date')"
:id="fieldErrorId('departure-date')"
class="field-error"
:data-testid="fieldErrorTestId('departure-date')"
>
{{ fieldError('departure-date') }}
</p>
</div>
<div class="field">
<div class="field__heading">
<label for="invoice-due-date">Due Date</label>
<label
for="invoice-due-date"
class="field-label field-label--required"
>Due Date</label>
</div>
<div class="date-control">
<input
@@ -243,6 +351,8 @@
v-model="form.document.due_date"
data-testid="document-due-date"
type="date"
:class="validationClass('due-date')"
v-bind="validationAria('due-date')"
required
@input="markDocumentDatesEdited"
>
@@ -255,6 +365,14 @@
<strong>{{ formatDatePreview(form.document.due_date) }}</strong>
</output>
</div>
<p
v-if="fieldError('due-date')"
:id="fieldErrorId('due-date')"
class="field-error"
:data-testid="fieldErrorTestId('due-date')"
>
{{ fieldError('due-date') }}
</p>
</div>
<div class="field">
@@ -263,7 +381,7 @@
</div>
<div class="room-rate-control">
<label>
<span class="control-caption">金额</span>
<span class="control-caption control-caption--required">金额</span>
<div class="number-control">
<input
v-model="primaryCharge.rate"
@@ -271,6 +389,8 @@
type="text"
inputmode="decimal"
autocomplete="off"
:class="validationClass('primary-rate')"
v-bind="validationAria('primary-rate')"
required
>
<span
@@ -278,6 +398,14 @@
aria-hidden="true"
>rm/n</span>
</div>
<span
v-if="fieldError('primary-rate')"
:id="fieldErrorId('primary-rate')"
class="field-error"
:data-testid="fieldErrorTestId('primary-rate')"
>
{{ fieldError('primary-rate') }}
</span>
</label>
<label>
<span class="control-caption">说明</span>
@@ -300,12 +428,25 @@
type="text"
inputmode="decimal"
autocomplete="off"
:class="validationClass('extra-bed-rate')"
v-bind="validationAria('extra-bed-rate')"
>
<span
v-if="fieldError('extra-bed-rate')"
:id="fieldErrorId('extra-bed-rate')"
class="field-error"
:data-testid="fieldErrorTestId('extra-bed-rate')"
>
{{ fieldError('extra-bed-rate') }}
</span>
</label>
<div class="field">
<div class="field__heading">
<label for="invoice-room-count">No.of Room (s)</label>
<label
for="invoice-room-count"
class="field-label field-label--required"
>No.of Room (s)</label>
</div>
<div class="number-control">
<input
@@ -315,6 +456,8 @@
type="text"
inputmode="numeric"
autocomplete="off"
:class="validationClass('primary-quantity')"
v-bind="validationAria('primary-quantity')"
required
>
<span
@@ -322,11 +465,22 @@
aria-hidden="true"
>Rooms</span>
</div>
<p
v-if="fieldError('primary-quantity')"
:id="fieldErrorId('primary-quantity')"
class="field-error"
:data-testid="fieldErrorTestId('primary-quantity')"
>
{{ fieldError('primary-quantity') }}
</p>
</div>
<div class="field">
<div class="field__heading">
<label for="invoice-night-count">No.of Night (s)</label>
<label
for="invoice-night-count"
class="field-label field-label--required"
>No.of Night (s)</label>
</div>
<div class="number-control">
<input
@@ -336,6 +490,8 @@
type="text"
inputmode="numeric"
autocomplete="off"
:class="validationClass('primary-nights')"
v-bind="validationAria('primary-nights')"
required
>
<span
@@ -343,6 +499,14 @@
aria-hidden="true"
>Nights</span>
</div>
<p
v-if="fieldError('primary-nights')"
:id="fieldErrorId('primary-nights')"
class="field-error"
:data-testid="fieldErrorTestId('primary-nights')"
>
{{ fieldError('primary-nights') }}
</p>
</div>
</div>
</section>
@@ -403,57 +567,107 @@
<div class="charge-grid">
<label class="field charge-field--description">
<span class="field-label">Description</span>
<span class="field-label field-label--required">Description</span>
<input
v-model.trim="charge.description"
:data-testid="`charge-description-${index}`"
type="text"
autocomplete="off"
:class="validationClass(chargeFieldKey(index, 'description'))"
v-bind="validationAria(chargeFieldKey(index, 'description'))"
required
>
<span
v-if="fieldError(chargeFieldKey(index, 'description'))"
:id="fieldErrorId(chargeFieldKey(index, 'description'))"
class="field-error"
:data-testid="fieldErrorTestId(chargeFieldKey(index, 'description'))"
>
{{ fieldError(chargeFieldKey(index, 'description')) }}
</span>
</label>
<label class="field charge-field--room-type">
<span class="field-label">Room Types</span>
<span class="field-label field-label--required">Room Types</span>
<input
v-model.trim="charge.room_type"
:data-testid="`charge-room-type-${index}`"
type="text"
autocomplete="off"
:class="validationClass(chargeFieldKey(index, 'room-type'))"
v-bind="validationAria(chargeFieldKey(index, 'room-type'))"
required
>
<span
v-if="fieldError(chargeFieldKey(index, 'room-type'))"
:id="fieldErrorId(chargeFieldKey(index, 'room-type'))"
class="field-error"
:data-testid="fieldErrorTestId(chargeFieldKey(index, 'room-type'))"
>
{{ fieldError(chargeFieldKey(index, 'room-type')) }}
</span>
</label>
<label class="field">
<span class="field-label">Quantity (s)</span>
<span class="field-label field-label--required">Quantity (s)</span>
<input
v-model="charge.quantity"
:data-testid="`charge-quantity-${index}`"
type="text"
inputmode="numeric"
autocomplete="off"
:class="validationClass(chargeFieldKey(index, 'quantity'))"
v-bind="validationAria(chargeFieldKey(index, 'quantity'))"
required
>
<span
v-if="fieldError(chargeFieldKey(index, 'quantity'))"
:id="fieldErrorId(chargeFieldKey(index, 'quantity'))"
class="field-error"
:data-testid="fieldErrorTestId(chargeFieldKey(index, 'quantity'))"
>
{{ fieldError(chargeFieldKey(index, 'quantity')) }}
</span>
</label>
<label class="field">
<span class="field-label">Rate</span>
<span class="field-label field-label--required">Rate</span>
<input
v-model="charge.rate"
:data-testid="`charge-rate-${index}`"
type="text"
inputmode="decimal"
autocomplete="off"
:class="validationClass(chargeFieldKey(index, 'rate'))"
v-bind="validationAria(chargeFieldKey(index, 'rate'))"
required
>
<span
v-if="fieldError(chargeFieldKey(index, 'rate'))"
:id="fieldErrorId(chargeFieldKey(index, 'rate'))"
class="field-error"
:data-testid="fieldErrorTestId(chargeFieldKey(index, 'rate'))"
>
{{ fieldError(chargeFieldKey(index, 'rate')) }}
</span>
</label>
<label class="field">
<span class="field-label">Night (s)</span>
<span class="field-label field-label--required">Night (s)</span>
<input
v-model="charge.nights"
:data-testid="`charge-nights-${index}`"
type="text"
inputmode="numeric"
autocomplete="off"
:class="validationClass(chargeFieldKey(index, 'nights'))"
v-bind="validationAria(chargeFieldKey(index, 'nights'))"
required
>
<span
v-if="fieldError(chargeFieldKey(index, 'nights'))"
:id="fieldErrorId(chargeFieldKey(index, 'nights'))"
class="field-error"
:data-testid="fieldErrorTestId(chargeFieldKey(index, 'nights'))"
>
{{ fieldError(chargeFieldKey(index, 'nights')) }}
</span>
</label>
<div class="charge-amount">
<span>Amount (Bht)</span>
@@ -650,6 +864,9 @@ interface GenerationErrorDescription {
technicalDetails: string[]
}
type FieldErrorMap = Record<string, string>
type ChargeFieldName = 'description' | 'room-type' | 'quantity' | 'rate' | 'nights'
const recipientCompanies: RecipientCompanySeed[] = [
{
company_code: 'LIAN_TAI',
@@ -772,6 +989,7 @@ const downloadMessageKey = ref('')
const alertTone = ref<'error' | 'success'>('error')
const generationResult = ref<ManualInvoiceGenerationResult | null>(null)
const documentDatesEdited = ref(false)
const validationAttempted = ref(false)
const recipientContacts = computed(() => {
return recipientCompanies.find((company) => company.company_code === form.recipient.company_code)?.contacts ?? []
@@ -781,6 +999,7 @@ const primaryCharge = computed<ChargeForm>(() => form.charges[0] as ChargeForm)
const sourceGroupName = computed(() => form.booking.group_name.trim() || '未填写')
const downloadMessage = computed(() => (downloadMessageKey.value ? t(downloadMessageKey.value) : ''))
const fieldErrors = computed<FieldErrorMap>(() => (validationAttempted.value ? collectFieldErrors() : {}))
const previewTotals = computed(() => {
const total = form.charges.reduce((sum, charge) => sum + lineAmount(charge), 0)
@@ -881,6 +1100,7 @@ function lineAmount(charge: ChargeForm): number {
async function submitInvoice(): Promise<void> {
technicalDetails.value = []
downloadMessageKey.value = ''
validationAttempted.value = true
messages.value = validateForm()
alertTone.value = 'error'
if (messages.value.length > 0) {
@@ -1007,7 +1227,7 @@ function validateForm(): string[] {
) {
errors.push(t('manualInvoice.errors.departureBeforeArrival'))
}
if (extraBedRate !== null && extraBedRate < 0) {
if (form.booking.extra_bed_rate.trim() && (extraBedRate === null || extraBedRate < 0)) {
errors.push(t('manualInvoice.errors.extraBedNegative'))
}
if (charges.some((charge) => charge.quantity <= 0 || charge.rate <= 0 || charge.nights <= 0)) {
@@ -1016,6 +1236,112 @@ function validateForm(): string[] {
return [...new Set(errors)]
}
function collectFieldErrors(): FieldErrorMap {
const errors: FieldErrorMap = {}
const positiveNumber = t('manualInvoice.fieldErrors.positiveNumber')
setRequiredError(errors, 'company', form.recipient.company)
setRequiredError(errors, 'attention', form.recipient.attention)
setRequiredError(errors, 'address', form.recipient.address)
setRequiredError(errors, 'telephone', form.recipient.telephone)
setRequiredError(errors, 'email', form.recipient.email)
if (form.recipient.email.trim() && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.recipient.email)) {
errors.email = t('manualInvoice.fieldErrors.invalidEmail')
}
setRequiredError(errors, 'booking-date', form.document.booking_date)
setRequiredError(errors, 'due-date', form.document.due_date)
if (form.document.booking_date && form.document.due_date && form.document.due_date < form.document.booking_date) {
errors['due-date'] = t('manualInvoice.fieldErrors.dateOrder')
}
setRequiredError(errors, 'group-name', form.booking.group_name)
setRequiredError(errors, 'arrival-date', form.booking.arrival_date)
setRequiredError(errors, 'departure-date', form.booking.departure_date)
if (
form.booking.arrival_date &&
form.booking.departure_date &&
form.booking.departure_date <= form.booking.arrival_date
) {
errors['departure-date'] = t('manualInvoice.fieldErrors.dateOrder')
}
const extraBedRate = optionalNumberValue(form.booking.extra_bed_rate)
if (form.booking.extra_bed_rate.trim() && (extraBedRate === null || extraBedRate < 0)) {
errors['extra-bed-rate'] = t('manualInvoice.fieldErrors.nonNegativeNumber')
}
form.charges.forEach((charge, index) => {
setRequiredError(errors, chargeFieldKey(index, 'description'), charge.description)
setRequiredError(errors, chargeFieldKey(index, 'room-type'), charge.room_type)
if (numberValue(charge.quantity) <= 0) {
errors[chargeFieldKey(index, 'quantity')] = positiveNumber
}
if (numberValue(charge.rate) <= 0) {
errors[chargeFieldKey(index, 'rate')] = positiveNumber
}
if (numberValue(charge.nights) <= 0) {
errors[chargeFieldKey(index, 'nights')] = positiveNumber
}
})
const firstQuantityError = errors[chargeFieldKey(0, 'quantity')]
if (firstQuantityError) {
errors['primary-quantity'] = firstQuantityError
}
const firstRateError = errors[chargeFieldKey(0, 'rate')]
if (firstRateError) {
errors['primary-rate'] = firstRateError
}
const firstNightsError = errors[chargeFieldKey(0, 'nights')]
if (firstNightsError) {
errors['primary-nights'] = firstNightsError
}
return errors
}
function setRequiredError(errors: FieldErrorMap, fieldKey: string, value: string): void {
if (!value.trim()) {
errors[fieldKey] = t('manualInvoice.fieldErrors.required')
}
}
function fieldError(fieldKey: string): string {
return fieldErrors.value[fieldKey] ?? ''
}
function fieldErrorId(fieldKey: string): string {
return `manual-invoice-error-${fieldKey}`
}
function fieldErrorTestId(fieldKey: string): string {
return `manual-invoice-field-error-${fieldKey}`
}
function validationClass(fieldKey: string): Record<string, boolean> {
return {
'is-invalid': Boolean(fieldError(fieldKey)),
}
}
function validationAria(fieldKey: string): Record<string, string | undefined> {
if (!fieldError(fieldKey)) {
return {
'aria-invalid': undefined,
'aria-describedby': undefined,
}
}
return {
'aria-invalid': 'true',
'aria-describedby': fieldErrorId(fieldKey),
}
}
function chargeFieldKey(index: number, fieldName: ChargeFieldName): string {
return `charge-${index}-${fieldName}`
}
function describeGenerationError(error: unknown): GenerationErrorDescription {
if (error instanceof ApiError) {
const authKey = error.status === 401 ? 'AUTH_401' : error.status === 403 ? 'AUTH_403' : null
@@ -1371,14 +1697,15 @@ function isRecord(value: unknown): value is Record<string, unknown> {
width: 100%;
min-width: 0;
min-height: 40px;
border: 1px solid var(--th-color-border);
border: 1px solid #b9c7dc;
border-radius: 6px;
background: var(--th-color-white);
background: #fbfdff;
color: var(--th-color-navy-950);
font: inherit;
font-weight: 700;
padding: 10px 11px;
outline: none;
box-shadow: 0 1px 2px rgb(15 23 42 / 5%), inset 0 0 0 1px rgb(15 23 42 / 3%);
transition: border-color 180ms ease, box-shadow 180ms ease, background-color 180ms ease;
}
@@ -1392,7 +1719,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
.field select:hover,
.field textarea:hover,
.charge-grid input:hover {
border-color: #b9c8dc;
border-color: #8fa4c2;
}
.field input:focus-visible,
@@ -1408,6 +1735,38 @@ function isRecord(value: unknown): value is Record<string, unknown> {
outline-offset: 2px;
}
.field input.is-invalid,
.field select.is-invalid,
.field textarea.is-invalid,
.charge-grid input.is-invalid {
border-color: #e11d48;
background: #fff7f9;
box-shadow: 0 0 0 3px rgb(225 29 72 / 12%), inset 0 0 0 1px rgb(225 29 72 / 8%);
}
.date-control input.is-invalid + .date-preview,
.number-control input.is-invalid + .number-suffix {
border-color: #e11d48;
background: #fff1f3;
color: #be123c;
}
.field-label--required::after,
.control-caption--required::after {
content: "*";
margin-left: 4px;
color: #e11d48;
font-weight: 900;
}
.field-error {
margin: -2px 0 0;
color: #be123c;
font-size: 11px;
font-weight: 800;
line-height: 1.45;
}
.directory-control {
display: grid;
min-width: 0;