Merge pull request 'Let the event form submit and say what is wrong' (#177) from fix/submit-gate-explains-itself into dev

Reviewed-on: #177
This commit is contained in:
padreug 2026-09-29 08:02:08 +00:00
commit 9b48ba73e2

View file

@ -428,13 +428,34 @@ watch(
}, },
); );
const { resetForm, meta } = form; const { resetForm } = form;
const isFormValid = computed(
() => /** Field name -> what the organiser calls it, for the invalid-submit toast. */
meta.value.valid && const FIELD_LABELS: Record<string, string> = {
!hasPromoErrors(promoErrors.value) && name: "Title",
!hasWaveErrors.value, 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 isLoading = ref(false);
const today = computed(() => format(new Date(), "yyyy-MM-dd")); const today = computed(() => format(new Date(), "yyyy-MM-dd"));
@ -448,8 +469,22 @@ function toggleCategory(cat: string) {
} }
} }
const onSubmit = form.handleSubmit(async (formValues) => { const onSubmit = form.handleSubmit(
if (!isFormValid.value) return; 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) { if (!paymentService) {
toastService.error("Payment service not available"); toastService.error("Payment service not available");
@ -468,7 +503,10 @@ const onSubmit = form.handleSubmit(async (formValues) => {
const eventData: CreateEventRequest = { const eventData: CreateEventRequest = {
name: formValues.name, name: formValues.name,
event_start_date: withLocalTzOffset( 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) { if (!isEditMode.value) {
@ -486,7 +524,9 @@ const onSubmit = form.handleSubmit(async (formValues) => {
} }
if (formValues.location) eventData.location = formValues.location; if (formValues.location) eventData.location = formValues.location;
if (bannerImages.value.length > 0) { 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) { } else if (isEditMode.value) {
// User cleared the banner during edit — propagate the null so the // User cleared the banner during edit — propagate the null so the
// backend wipes the field instead of keeping the old image. // backend wipes the field instead of keeping the old image.
@ -601,7 +641,28 @@ const onSubmit = form.handleSubmit(async (formValues) => {
} finally { } finally {
isLoading.value = false; 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) => { const handleOpenChange = (open: boolean) => {
if (!open && !isLoading.value) { if (!open && !isLoading.value) {
@ -1176,10 +1237,17 @@ const handleOpenChange = (open: boolean) => {
> >
Cancel Cancel
</Button> </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 <Button
type="submit" type="submit"
class="w-full sm:w-auto" class="w-full sm:w-auto"
:disabled="isLoading || !isFormValid" :disabled="isLoading"
> >
<Loader2 v-if="isLoading" class="w-4 h-4 mr-2 animate-spin" /> <Loader2 v-if="isLoading" class="w-4 h-4 mr-2 animate-spin" />
{{ {{