@arquimedes.co/eureka-forms 3.0.73 → 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.
- package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.js +1 -1
- package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.test.js +14 -0
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.d.ts +8 -1
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.js +6 -5
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.module.css +5 -0
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.test.js +15 -0
- package/package.json +1 -1
|
@@ -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
|
|
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 ? (
|
|
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) }))] })] })] }) })] }));
|
|
@@ -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":
|
|
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",
|