@arquimedes.co/eureka-forms 3.0.73-test → 3.0.74-test

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -286,6 +286,6 @@ function MaterialBookingStep({ step, editable, fetchSlots: customFetchSlots }) {
286
286
  const personSelectionMode = usesEdge
287
287
  ? (offer?.personSelection ?? PersonSelectionMode.NONE)
288
288
  : step.personSelectionMode;
289
- return (_jsx("div", { ref: ref, style: cssVars, children: _jsx(ErkBookingPicker, { value: value, onChange: handleChange, label: step.label, description: step.description, required: step.required, durationMinutes: offer?.durationMinutes ?? step.durationMinutes, bookingMode: step.bookingMode, temporaryLockMinutes: step.temporaryLockMinutes, personSelectionMode: personSelectionMode, persons: usesEdge ? persons : step.persons, eventTemplates: usesEdge ? eventTemplates : undefined, timeZone: offer?.timeZone, knownContact: knownContact, askEmail: askEmail, lapsed: lapsed, fetchSlots: usesEdge ? fetchSlots : customFetchSlots, fetchDays: usesEdge ? fetchDays : undefined, holdSlot: usesEdge ? holdSlot : undefined, disabled: usesEdge && !offer, readOnly: isReadOnly, error: error?.message, loadError: usesEdge ? offerError : undefined, onRetryLoad: retryOffer }) }));
289
+ return (_jsx("div", { ref: ref, style: cssVars, children: _jsx(ErkBookingPicker, { value: value, onChange: handleChange, label: step.label, description: step.description, required: step.required, durationMinutes: offer?.durationMinutes ?? step.durationMinutes, bookingMode: step.bookingMode, temporaryLockMinutes: step.temporaryLockMinutes, personSelectionMode: personSelectionMode, persons: usesEdge ? persons : step.persons, eventTemplates: usesEdge ? eventTemplates : undefined, timeZone: offer?.timeZone, knownContact: knownContact, askEmail: askEmail, lapsed: lapsed, fetchSlots: usesEdge ? fetchSlots : customFetchSlots, fetchDays: usesEdge ? fetchDays : undefined, holdSlot: usesEdge ? holdSlot : undefined, disabled: usesEdge && !offer, loading: usesEdge && !offer && !offerError, readOnly: isReadOnly, error: error?.message, loadError: usesEdge ? offerError : undefined, onRetryLoad: retryOffer }) }));
290
290
  }
291
291
  export default MaterialBookingStep;
@@ -256,6 +256,20 @@ describe('MaterialBookingStep', () => {
256
256
  contact: { name: 'Ana Gómez', email: 'ana@correo.com' },
257
257
  });
258
258
  });
259
+ it('says it is loading the hours until the offer lands, instead of a mute disabled button', async () => {
260
+ let deliver = () => { };
261
+ vi.mocked(fetchBookingOffer).mockReturnValueOnce(new Promise((resolve) => {
262
+ deliver = resolve;
263
+ }));
264
+ renderStep(bookingStep(), { 'email-step': 'ana@correo.com', 'name-step': 'Ana Gómez' });
265
+ const loading = screen.getByRole('button', { name: 'Cargando horarios…' });
266
+ expect(loading).toBeDisabled();
267
+ expect(loading).toHaveAttribute('aria-busy', 'true');
268
+ expect(screen.queryByRole('button', { name: 'Elegir horario' })).not.toBeInTheDocument();
269
+ deliver(OFFER);
270
+ await waitFor(() => expect(screen.getByRole('button', { name: 'Elegir horario' })).toBeEnabled());
271
+ expect(screen.queryByRole('button', { name: 'Cargando horarios…' })).not.toBeInTheDocument();
272
+ });
259
273
  it('offers to retry when the offer does not load, and loads it on retry', async () => {
260
274
  const user = userEvent.setup();
261
275
  vi.mocked(fetchBookingOffer)
@@ -6,6 +6,8 @@ import { BookablePerson, BookingEventTemplateOption, BookingMode, BookingSlot, B
6
6
  export interface BookingPickerCopy {
7
7
  /** Botón que abre el selector. */
8
8
  open: string;
9
+ /** El mismo botón mientras el host carga la oferta: dice que está cargando en vez de quedar mudo. */
10
+ loading: string;
9
11
  /** Paso final y su botón. */
10
12
  confirm: string;
11
13
  /** El botón mientras se confirma. */
@@ -82,6 +84,11 @@ export interface ErkBookingPickerProps {
82
84
  }) => Promise<Partial<BookingValue>>;
83
85
  /** Deshabilita el botón de abrir mientras el host termina de cargar la oferta. */
84
86
  disabled?: boolean;
87
+ /**
88
+ * La oferta todavía se está cargando: el botón de abrir muestra un spinner y `copy.loading` en vez
89
+ * de quedar deshabilitado sin explicación.
90
+ */
91
+ loading?: boolean;
85
92
  /** No pide nombre ni correo: la cita que se está moviendo ya los tiene. */
86
93
  withoutContact?: boolean;
87
94
  /** Textos del wizard; por defecto los de una reserva nueva dentro de un formulario. */
@@ -104,5 +111,5 @@ export interface ErkBookingPickerProps {
104
111
  * Siguiente. "Reservar" retira el horario de la oferta por unos minutos, cierra el modal y deja el
105
112
  * resumen; la cita se confirma cuando se envía el formulario.
106
113
  */
107
- declare function ErkBookingPicker({ value, onChange, label, description, required, durationMinutes, bookingMode, temporaryLockMinutes, personSelectionMode, persons, eventTemplates, timeZone, knownContact, askEmail, lapsed, fetchSlots, fetchDays, holdSlot, disabled, withoutContact, copy: copyOverrides, readOnly, error, loadError, onRetryLoad, }: ErkBookingPickerProps): JSX.Element;
114
+ declare function ErkBookingPicker({ value, onChange, label, description, required, durationMinutes, bookingMode, temporaryLockMinutes, personSelectionMode, persons, eventTemplates, timeZone, knownContact, askEmail, lapsed, fetchSlots, fetchDays, holdSlot, disabled, loading: offerLoading, withoutContact, copy: copyOverrides, readOnly, error, loadError, onRetryLoad, }: ErkBookingPickerProps): JSX.Element;
108
115
  export default ErkBookingPicker;
@@ -1,6 +1,6 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
3
- import { Dialog } from '@mui/material';
3
+ import { CircularProgress, Dialog } from '@mui/material';
4
4
  import { addDays, addMonths, format, isBefore, isSameDay, isSameMonth, startOfDay, startOfMonth, startOfWeek, } from 'date-fns';
5
5
  import { es } from 'date-fns/locale';
6
6
  import { BookingMode, PersonSelectionMode, } from '../../@Types/BookingFormStep';
@@ -78,6 +78,7 @@ const PAPER_SX = {
78
78
  };
79
79
  const BOOKING_COPY = {
80
80
  open: 'Elegir horario',
81
+ loading: 'Cargando horarios…',
81
82
  confirm: 'Reservar',
82
83
  confirming: 'Reservando…',
83
84
  title: 'Revisa y reserva tu horario',
@@ -157,7 +158,7 @@ function SummaryRow({ icon, title, sub, change, onChange, }) {
157
158
  * Siguiente. "Reservar" retira el horario de la oferta por unos minutos, cierra el modal y deja el
158
159
  * resumen; la cita se confirma cuando se envía el formulario.
159
160
  */
160
- function ErkBookingPicker({ value, onChange, label, description, required = false, durationMinutes, bookingMode, temporaryLockMinutes, personSelectionMode, persons, eventTemplates, timeZone, knownContact, askEmail = false, lapsed, fetchSlots, fetchDays, holdSlot, disabled = false, withoutContact = false, copy: copyOverrides, readOnly = false, error, loadError, onRetryLoad, }) {
161
+ function ErkBookingPicker({ value, onChange, label, description, required = false, durationMinutes, bookingMode, temporaryLockMinutes, personSelectionMode, persons, eventTemplates, timeZone, knownContact, askEmail = false, lapsed, fetchSlots, fetchDays, holdSlot, disabled = false, loading: offerLoading = false, withoutContact = false, copy: copyOverrides, readOnly = false, error, loadError, onRetryLoad, }) {
161
162
  const copy = { ...BOOKING_COPY, ...copyOverrides };
162
163
  const needsPerson = !!personSelectionMode && personSelectionMode !== PersonSelectionMode.NONE;
163
164
  const personsOptional = personSelectionMode === PersonSelectionMode.OPTIONAL;
@@ -433,7 +434,7 @@ function ErkBookingPicker({ value, onChange, label, description, required = fals
433
434
  if (!date)
434
435
  return _jsx("p", { className: styles.empty, children: "Elige un d\u00EDa para ver sus horas." });
435
436
  if (loading)
436
- return _jsx("p", { className: styles.empty, children: "Buscando horarios\u2026" });
437
+ return (_jsxs("p", { className: cx(styles.empty, styles.loadingLine), "aria-busy": "true", children: [_jsx(CircularProgress, { size: 14, color: "inherit" }), "Buscando horarios\u2026"] }));
437
438
  if (slotsError) {
438
439
  return (_jsxs("div", { className: styles.retry, role: "alert", children: [_jsx("span", { children: slotsError }), _jsx(ErkButton, { text: "Reintentar", variant: "outlined", onClick: () => void loadSlots(date, person?.id, eventTemplate?.id) })] }));
439
440
  }
@@ -453,7 +454,7 @@ function ErkBookingPicker({ value, onChange, label, description, required = fals
453
454
  : selectable
454
455
  ? styles.dayFree
455
456
  : isSameDay(d, today) && styles.dayToday), onClick: () => pickDay(d), children: format(d, 'd') }, d.toISOString()));
456
- }) }), daysError ? (_jsxs("div", { className: styles.retry, role: "alert", children: [_jsx("span", { children: daysError }), _jsx(ErkButton, { text: "Reintentar", variant: "outlined", onClick: () => setDaysAttempt((n) => n + 1) })] })) : fetchDays && days === undefined ? (_jsx("p", { className: styles.calNote, children: "Buscando d\u00EDas con horarios\u2026" })) : days?.length === 0 ? (_jsx("p", { className: styles.calNote, children: "No hay d\u00EDas con horarios disponibles." })) : (_jsx("p", { className: styles.calNote, children: fetchDays
457
+ }) }), daysError ? (_jsxs("div", { className: styles.retry, role: "alert", children: [_jsx("span", { children: daysError }), _jsx(ErkButton, { text: "Reintentar", variant: "outlined", onClick: () => setDaysAttempt((n) => n + 1) })] })) : fetchDays && days === undefined ? (_jsxs("p", { className: cx(styles.calNote, styles.loadingLine), "aria-busy": "true", children: [_jsx(CircularProgress, { size: 12, color: "inherit" }), "Buscando d\u00EDas con horarios\u2026"] })) : days?.length === 0 ? (_jsx("p", { className: styles.calNote, children: "No hay d\u00EDas con horarios disponibles." })) : (_jsx("p", { className: styles.calNote, children: fetchDays
457
458
  ? 'Solo se pueden elegir los días con horarios disponibles.'
458
459
  : 'Solo se muestran fechas a partir de hoy.' }))] }), _jsxs("div", { className: styles.hours, children: [_jsxs("div", { className: styles.hoursHead, children: [_jsx("span", { className: styles.hoursTitle, children: date ? longDay(date) : 'Elige un día' }), _jsx("span", { className: styles.hoursSub, children: zoneText ? `${zoneText} · citas de ${duration} min` : `Citas de ${duration} min` }), localStart && slot && (_jsxs("span", { className: styles.hoursSub, children: [slot.startTime, " es ", localStart, " en tu hora."] }))] }), holdError && (_jsx("div", { className: styles.alert, role: "alert", children: holdError })), renderHours()] })] }));
459
460
  const renderConfirm = () => (_jsxs("div", { className: styles.split, children: [_jsxs("div", { className: styles.summary, children: [_jsx("p", { className: styles.overline, children: "Tu cita" }), eventTemplate && (_jsx(SummaryRow, { icon: _jsx(ClockIcon, { size: 16 }), title: eventTemplate.name, sub: `${eventTemplate.durationMinutes} min`, change: "Cambiar el tipo de atenci\u00F3n", onChange: () => goTo('type') })), needsPerson && personLabel && (_jsx(SummaryRow, { icon: _jsx(PersonIcon, { size: 16 }), title: personLabel, sub: person ? person.role : 'El sistema asigna a quien esté disponible', change: "Cambiar la persona", onChange: () => goTo('person') })), date && (_jsx(SummaryRow, { icon: _jsx(CalendarIcon, { size: 16 }), title: longDay(date), sub: [hourText, `${duration} min`, localStart ? `${localStart} en tu hora` : undefined]
@@ -463,7 +464,7 @@ function ErkBookingPicker({ value, onChange, label, description, required = fals
463
464
  : 'Te enviaremos la confirmación a tu correo.' }))] }), askName && (_jsx(ErkTextField, { label: "Nombre", value: contactName, onChange: setContactName, required: true })), askContactEmail && (_jsx(ErkTextField, { label: "Correo", type: "email", value: contactEmail, onChange: setContactEmail, onBlur: () => setEmailTouched(true), required: true, error: emailTouched && emailInvalid, helperText: emailTouched && contactEmail.trim() && emailInvalid
464
465
  ? 'Escribe un correo válido.'
465
466
  : NO_EMAIL_HINT })), lockNote] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.question, children: [_jsx("h3", { className: styles.q, children: "Confirmaci\u00F3n" }), _jsx("p", { className: styles.qsub, children: "Usaremos los datos que ya escribiste en el formulario." })] }), _jsxs("div", { className: styles.known, children: [knownName && (_jsxs("div", { className: styles.knownRow, children: [_jsx(PersonIcon, { size: 16, className: styles.knownIcon }), _jsx("span", { className: styles.knownValue, children: knownName }), _jsx("span", { className: styles.knownFrom, children: "del campo Nombre" })] })), knownEmail && (_jsxs("div", { className: styles.knownRow, children: [_jsx(EmailIcon, { size: 16, className: styles.knownIcon }), _jsx("span", { className: styles.knownValue, children: knownEmail }), _jsx("span", { className: styles.knownFrom, children: "del campo Correo" })] }))] }), _jsx("p", { className: styles.fineprint, children: "\u00BFHay un error? Cierra el selector y corr\u00EDgelo en el formulario; el horario sigue elegido." }), lockNote] })) })] }));
466
- return (_jsxs("div", { ref: containerRef, className: styles.container, children: [head, _jsx("div", { className: styles.control, children: _jsx(ErkButton, { text: copy.open, variant: "outlined", color: error ? 'error' : 'primary', disabled: disabled, startIcon: _jsx(CalendarIcon, { size: 16 }), sx: { height: 36, padding: '0 14px', ...OUTLINED_DISABLED }, onClick: () => {
467
+ return (_jsxs("div", { ref: containerRef, className: styles.container, children: [head, _jsx("div", { className: styles.control, children: _jsx(ErkButton, { text: offerLoading ? copy.loading : copy.open, variant: "outlined", color: error ? 'error' : 'primary', disabled: disabled || offerLoading, "aria-busy": offerLoading || undefined, startIcon: offerLoading ? _jsx(CircularProgress, { size: 14, color: "inherit" }) : _jsx(CalendarIcon, { size: 16 }), sx: { height: 36, padding: '0 14px', ...OUTLINED_DISABLED }, onClick: () => {
467
468
  setTheme(themeOf(containerRef.current));
468
469
  setOpen(true);
469
470
  } }) }), contactLine && (_jsx("div", { className: styles.note, children: contactLine === 'email' ? (_jsxs(_Fragment, { children: ["Te confirmaremos a ", _jsx("b", { children: knownEmail }), " cuando env\u00EDes el formulario."] })) : (contactLine) })), loadError && (_jsxs("div", { className: cx(styles.errorMsg, styles.loadError), role: "alert", children: [_jsx("span", { children: loadError }), onRetryLoad && (_jsx(ErkButton, { text: "Reintentar", variant: "outlined", sx: { height: 28, fontSize: 13 }, onClick: onRetryLoad }))] })), lapsed && !open && (_jsx("div", { className: styles.errorMsg, role: "alert", children: lapsed })), error && (_jsx("div", { className: styles.errorMsg, role: "alert", children: error })), _jsx(Dialog, { open: open, onClose: () => setOpen(false), maxWidth: false, "aria-labelledby": titleId, slotProps: { paper: { sx: PAPER_SX } }, children: _jsxs("div", { className: styles.card, style: theme, children: [_jsxs("header", { className: styles.header, children: [_jsxs("div", { className: styles.headText, children: [_jsx("h2", { id: titleId, className: styles.title, children: label }), _jsxs("div", { className: styles.meta, children: [_jsxs("span", { className: styles.metaItem, children: [_jsx(ClockIcon, { size: 14 }), duration, " min"] }), mode && (_jsxs("span", { className: styles.metaItem, children: [_jsx(ModeIcon, { mode: mode, size: 14 }), MODE_LABEL[mode]] }))] })] }), _jsx("button", { type: "button", "aria-label": "Cerrar", className: styles.close, onClick: () => setOpen(false), children: _jsx(CloseIcon, { size: 16 }) })] }), _jsx("nav", { className: styles.stepper, "aria-label": "Progreso", children: wizardKeys.map((key, i) => (_jsxs("span", { className: styles.stepGroup, children: [_jsxs("button", { type: "button", disabled: i >= wizStep, "aria-current": i === wizStep ? 'step' : undefined, onClick: () => setWizStep(i), className: cx(styles.step, i < wizStep && styles.stepDone, i === wizStep && styles.stepOn), children: [_jsx("span", { className: styles.stepNum, children: i < wizStep ? _jsx(CheckIcon, { size: 12 }) : i + 1 }), _jsx("span", { className: styles.stepLabel, children: stepLabel(key) })] }), i < wizardKeys.length - 1 && _jsx("span", { className: styles.stepSep })] }, key))) }), _jsxs("div", { className: styles.body, children: [currentKey === 'type' && renderType(), currentKey === 'person' && renderPerson(), currentKey === 'date' && renderDate(), currentKey === 'confirm' && renderConfirm()] }, currentKey), _jsxs("footer", { className: styles.footer, children: [_jsxs("div", { className: styles.footSummary, "aria-live": "polite", children: [chips.map((chip) => (_jsx("span", { className: cx(styles.chip, chip.accent && styles.chipAccent), children: chip.text }, chip.key))), hint && _jsx("span", { className: styles.footHint, children: hint })] }), _jsxs("div", { className: styles.footActions, children: [wizStep > 0 && (_jsx(ErkButton, { text: "Atr\u00E1s", variant: "outlined", sx: NAV_SX, onClick: () => setWizStep(wizStep - 1) })), isFinal ? (_jsx(ErkButton, { text: holding ? copy.confirming : copy.confirm, sx: ACCENT_SX, className: styles.primaryAction, disabled: !slot || holding || !canAdvance, onClick: () => void confirm() })) : (_jsx(ErkButton, { text: "Siguiente", sx: { ...NAV_SX, padding: '0 22px' }, className: styles.primaryAction, disabled: !canAdvance, onClick: () => setWizStep(wizStep + 1) }))] })] })] }) })] }));
@@ -43,6 +43,11 @@
43
43
  margin: 5px 0 0 14px;
44
44
  color: var(--eureka-error, #cc2936);
45
45
  }
46
+ .loadingLine {
47
+ display: inline-flex;
48
+ align-items: center;
49
+ gap: 8px;
50
+ }
46
51
  .loadError {
47
52
  display: flex;
48
53
  align-items: center;
@@ -564,3 +564,18 @@ describe('ErkBookingPicker (wizard)', () => {
564
564
  expect(screen.queryByRole('button')).not.toBeInTheDocument();
565
565
  });
566
566
  });
567
+ describe('while the host loads the offer', () => {
568
+ it('shows a loading button instead of a mute disabled one, and opens once loaded', async () => {
569
+ const user = userEvent.setup();
570
+ const { rerender } = render(_jsx(MaterialProviders, { formStyle: InternalFormStyle, children: _jsx(ErkBookingPicker, { value: null, onChange: () => { }, label: "Agendar cita", durationMinutes: 50, personSelectionMode: PersonSelectionMode.NONE, disabled: true, loading: true }) }));
571
+ const loading = screen.getByRole('button', { name: 'Cargando horarios…' });
572
+ expect(loading).toBeDisabled();
573
+ expect(loading).toHaveAttribute('aria-busy', 'true');
574
+ rerender(_jsx(MaterialProviders, { formStyle: InternalFormStyle, children: _jsx(ErkBookingPicker, { value: null, onChange: () => { }, label: "Agendar cita", durationMinutes: 50, personSelectionMode: PersonSelectionMode.NONE }) }));
575
+ const open = screen.getByRole('button', { name: 'Elegir horario' });
576
+ expect(open).toBeEnabled();
577
+ expect(open).not.toHaveAttribute('aria-busy');
578
+ await user.click(open);
579
+ expect(screen.getByRole('dialog')).toBeInTheDocument();
580
+ });
581
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arquimedes.co/eureka-forms",
3
3
  "repository": "git://github.com/Arquimede5/Eureka-Forms.git",
4
- "version":"3.0.73-test",
4
+ "version":"3.0.74-test",
5
5
  "scripts": {
6
6
  "watch": "node node_modules/@typescript/native/bin/tsc --noEmit --watch --project tsconfig.app.json",
7
7
  "start": "vite",