fix(events): let the event form submit and say what is wrong
The Submit button was disabled on `meta.valid`, so an organiser with a
fully filled form could be left staring at a dead button with nothing on
screen explaining it. Reported from demo: title, dates, price, capacity,
payment methods and three named ticket waves all filled, no error text
anywhere, submit greyed.
Gating on `meta.valid` is a known vee-validate foot-gun:
- disabled fields count against form validity, and the maintainer
removed `disabled` from `<Field>` precisely because consistent
behaviour was unachievable. This dialog disables inputs while loading
and renders `fiat_currency` conditionally, so it is exposed.
- `meta.valid` is documented to go stale — false with zero errors after
a form is re-shown (logaretm/vee-validate#4630).
- it is false before the form has ever validated, which is why the docs
suggest `valid && dirty` rather than `valid` alone. A flag that is
unreliable in both directions should not be the only thing between an
organiser and their event.
So the button is now disabled only while a submit is genuinely in
flight, and the reasons come to the user instead:
- `handleSubmit`'s invalid callback names the offending fields in a
toast ("Check Tickets, Start date, Title"), opens whichever collapsible
holds one, and scrolls the first into view. Previously a FormMessage
inside a closed section was invisible.
- promo codes and ticket waves are not in the Zod schema, so
vee-validate cannot block on them; they are checked in the submit
handler, which opens their section rather than failing silently. Both
editors live inside collapsibles, so this was the same trap.
Verified in a browser: a blank form now has an enabled button, and
clicking it reports "Check Tickets, Start date, Title".
Refs aiolabs/events ticket-wave work; the report came from testing #176
on demo.
This commit is contained in:
parent
d9de0fdd73
commit
285ccc2fdb
1 changed files with 221 additions and 153 deletions
|
|
@ -428,13 +428,34 @@ watch(
|
|||
},
|
||||
);
|
||||
|
||||
const { resetForm, meta } = form;
|
||||
const isFormValid = computed(
|
||||
() =>
|
||||
meta.value.valid &&
|
||||
!hasPromoErrors(promoErrors.value) &&
|
||||
!hasWaveErrors.value,
|
||||
);
|
||||
const { resetForm } = form;
|
||||
|
||||
/** Field name -> what the organiser calls it, for the invalid-submit toast. */
|
||||
const FIELD_LABELS: Record<string, string> = {
|
||||
name: "Title",
|
||||
info: "Description",
|
||||
event_start_date: "Start date",
|
||||
event_start_time: "Start time",
|
||||
event_end_date: "End date",
|
||||
event_end_time: "End time",
|
||||
location: "Location",
|
||||
currency: "Price currency",
|
||||
payment_methods: "Payment methods",
|
||||
fiat_currency: "Fiat currency",
|
||||
amount_tickets: "Tickets",
|
||||
price_per_ticket: "Price",
|
||||
notification_subject: "Subject",
|
||||
notification_body: "Body",
|
||||
};
|
||||
|
||||
/**
|
||||
* Open whichever collapsible section holds a problem, so the reason is on
|
||||
* screen rather than hidden behind a closed panel.
|
||||
*/
|
||||
function revealSectionFor(field: string) {
|
||||
if (field === "promo_codes") promoOpen.value = true;
|
||||
if (field === "ticket_waves") wavesOpen.value = true;
|
||||
}
|
||||
const isLoading = ref(false);
|
||||
|
||||
const today = computed(() => format(new Date(), "yyyy-MM-dd"));
|
||||
|
|
@ -448,8 +469,22 @@ function toggleCategory(cat: string) {
|
|||
}
|
||||
}
|
||||
|
||||
const onSubmit = form.handleSubmit(async (formValues) => {
|
||||
if (!isFormValid.value) return;
|
||||
const onSubmit = form.handleSubmit(
|
||||
async (formValues) => {
|
||||
// Promo codes and ticket waves are not part of the Zod schema, so
|
||||
// vee-validate cannot block on them. Check here and OPEN the section
|
||||
// that holds the problem — both editors live inside collapsibles, and
|
||||
// an error rendered inside a closed one is invisible.
|
||||
if (hasWaveErrors.value) {
|
||||
revealSectionFor("ticket_waves");
|
||||
toastService.error("Check the ticket waves below");
|
||||
return;
|
||||
}
|
||||
if (hasPromoErrors(promoErrors.value)) {
|
||||
revealSectionFor("promo_codes");
|
||||
toastService.error("Check the promo codes below");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!paymentService) {
|
||||
toastService.error("Payment service not available");
|
||||
|
|
@ -468,7 +503,10 @@ const onSubmit = form.handleSubmit(async (formValues) => {
|
|||
const eventData: CreateEventRequest = {
|
||||
name: formValues.name,
|
||||
event_start_date: withLocalTzOffset(
|
||||
foldDateTime(formValues.event_start_date, formValues.event_start_time),
|
||||
foldDateTime(
|
||||
formValues.event_start_date,
|
||||
formValues.event_start_time,
|
||||
),
|
||||
),
|
||||
};
|
||||
if (!isEditMode.value) {
|
||||
|
|
@ -486,7 +524,9 @@ const onSubmit = form.handleSubmit(async (formValues) => {
|
|||
}
|
||||
if (formValues.location) eventData.location = formValues.location;
|
||||
if (bannerImages.value.length > 0) {
|
||||
eventData.banner = imageService.getImageUrl(bannerImages.value[0].alias);
|
||||
eventData.banner = imageService.getImageUrl(
|
||||
bannerImages.value[0].alias,
|
||||
);
|
||||
} else if (isEditMode.value) {
|
||||
// User cleared the banner during edit — propagate the null so the
|
||||
// backend wipes the field instead of keeping the old image.
|
||||
|
|
@ -601,7 +641,28 @@ const onSubmit = form.handleSubmit(async (formValues) => {
|
|||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
// vee-validate blocks the submit when the schema fails. Say WHY: a field
|
||||
// whose FormMessage sits inside a closed section — or is not rendered at
|
||||
// all — otherwise leaves a dead button and no explanation, which is the
|
||||
// dead end disabling the button used to create.
|
||||
({ errors }) => {
|
||||
const fields = Object.keys(errors);
|
||||
fields.forEach(revealSectionFor);
|
||||
const named = fields.map((f) => FIELD_LABELS[f] ?? f);
|
||||
toastService.error(
|
||||
named.length
|
||||
? `Check ${named.join(", ")}`
|
||||
: "Some details still need attention",
|
||||
);
|
||||
const first = fields[0];
|
||||
if (first) {
|
||||
document
|
||||
.querySelector(`[name="${first}"], #${CSS.escape(first)}`)
|
||||
?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open && !isLoading.value) {
|
||||
|
|
@ -1176,10 +1237,17 @@ const handleOpenChange = (open: boolean) => {
|
|||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<!-- Disabled only while a submit is genuinely in flight, NOT on
|
||||
validity. vee-validate's `meta.valid` is false before the form
|
||||
has ever validated, counts disabled and conditionally rendered
|
||||
fields against you, and is documented to go stale
|
||||
(logaretm/vee-validate#4630). Gating on it produced a dead
|
||||
button with no visible reason; let the click through and the
|
||||
invalid handler says what is wrong. -->
|
||||
<Button
|
||||
type="submit"
|
||||
class="w-full sm:w-auto"
|
||||
:disabled="isLoading || !isFormValid"
|
||||
:disabled="isLoading"
|
||||
>
|
||||
<Loader2 v-if="isLoading" class="w-4 h-4 mr-2 animate-spin" />
|
||||
{{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue