@wix/headless-restaurants-olo 0.0.39 → 0.0.41
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/cjs/dist/react/FulfillmentDetails.d.ts +87 -0
- package/cjs/dist/react/FulfillmentDetails.js +206 -1
- package/cjs/dist/react/core/FulfillmentDetails.d.ts +96 -1
- package/cjs/dist/react/core/FulfillmentDetails.js +240 -1
- package/cjs/dist/services/fulfillment-details-service.js +14 -6
- package/cjs/dist/services/fulfillments-service.js +12 -4
- package/cjs/dist/services/item-details-service.d.ts +1 -1
- package/cjs/dist/services/olo-settings-service.d.ts +1 -1
- package/dist/react/FulfillmentDetails.d.ts +87 -0
- package/dist/react/FulfillmentDetails.js +206 -1
- package/dist/react/core/FulfillmentDetails.d.ts +96 -1
- package/dist/react/core/FulfillmentDetails.js +240 -1
- package/dist/services/fulfillment-details-service.js +14 -6
- package/dist/services/fulfillments-service.js +12 -4
- package/dist/services/item-details-service.d.ts +1 -1
- package/dist/services/olo-settings-service.d.ts +1 -1
- package/package.json +3 -2
|
@@ -354,4 +354,91 @@ interface DeliveryAddressProps extends Omit<React.ComponentPropsWithoutRef<'div'
|
|
|
354
354
|
* ```
|
|
355
355
|
*/
|
|
356
356
|
export declare const DeliveryAddress: React.ForwardRefExoticComponent<DeliveryAddressProps & React.RefAttributes<HTMLDivElement>>;
|
|
357
|
+
export interface AddressPrediction {
|
|
358
|
+
/** The human-readable name of the prediction */
|
|
359
|
+
description?: string;
|
|
360
|
+
/** The id of the prediction that can be used in place API */
|
|
361
|
+
searchId?: string;
|
|
362
|
+
/** Contains the main text of a prediction, usually the name of the place */
|
|
363
|
+
mainText?: string;
|
|
364
|
+
/** Contains the secondary text of a prediction, usually the location of the place */
|
|
365
|
+
secondaryText?: string;
|
|
366
|
+
}
|
|
367
|
+
interface AddressPickerProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'children'> {
|
|
368
|
+
/** Whether to render as a child component */
|
|
369
|
+
asChild?: boolean;
|
|
370
|
+
/** Placeholder text for the address input */
|
|
371
|
+
placeholder?: string;
|
|
372
|
+
/** Optional class name for the input element */
|
|
373
|
+
inputClassName?: string;
|
|
374
|
+
/** Optional class name for the predictions container */
|
|
375
|
+
predictionsClassName?: string;
|
|
376
|
+
/** Optional class name for individual prediction items */
|
|
377
|
+
predictionClassName?: string;
|
|
378
|
+
/** Optional class name for the prediction main text */
|
|
379
|
+
predictionMainTextClassName?: string;
|
|
380
|
+
/** Optional class name for the prediction secondary text */
|
|
381
|
+
predictionSecondaryTextClassName?: string;
|
|
382
|
+
/** Optional class name for the prediction description */
|
|
383
|
+
predictionDescriptionClassName?: string;
|
|
384
|
+
/** Minimum input length before showing predictions (default: 2) */
|
|
385
|
+
minInputLength?: number;
|
|
386
|
+
/** Children render prop that receives the address picker props */
|
|
387
|
+
children?: AsChildChildren<{
|
|
388
|
+
inputValue: string;
|
|
389
|
+
onInputChange: (value: string) => void;
|
|
390
|
+
predictions: AddressPrediction[];
|
|
391
|
+
isLoading: boolean;
|
|
392
|
+
onPredictionSelect: (prediction: AddressPrediction) => Promise<void>;
|
|
393
|
+
isDelivery: boolean;
|
|
394
|
+
dispatchType: DispatchType | null;
|
|
395
|
+
}> | React.ReactNode;
|
|
396
|
+
}
|
|
397
|
+
export declare const AddressPicker: React.ForwardRefExoticComponent<AddressPickerProps & React.RefAttributes<HTMLDivElement>>;
|
|
398
|
+
interface SaveButtonProps extends Omit<React.ComponentPropsWithoutRef<'button'>, 'children'> {
|
|
399
|
+
/** Whether to render as a child component */
|
|
400
|
+
asChild?: boolean;
|
|
401
|
+
/** Text to display while saving */
|
|
402
|
+
savingText?: string;
|
|
403
|
+
/** Text to display when save is available */
|
|
404
|
+
saveText?: string;
|
|
405
|
+
/** Children render prop that receives the save state */
|
|
406
|
+
children?: AsChildChildren<{
|
|
407
|
+
onSave: () => void;
|
|
408
|
+
isSaving: boolean;
|
|
409
|
+
canSave: boolean;
|
|
410
|
+
hasChanges: boolean;
|
|
411
|
+
selectedTimeSlot: {
|
|
412
|
+
id: string;
|
|
413
|
+
startTime: Date;
|
|
414
|
+
endTime: Date;
|
|
415
|
+
dispatchType: DispatchType;
|
|
416
|
+
} | null;
|
|
417
|
+
dispatchType: DispatchType | null;
|
|
418
|
+
}> | React.ReactNode;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Headless component for saving fulfillment details
|
|
422
|
+
* Saves the selected time slot to the FulfillmentsService
|
|
423
|
+
*
|
|
424
|
+
* @example
|
|
425
|
+
* ```tsx
|
|
426
|
+
* // Default usage - renders a save button
|
|
427
|
+
* <FulfillmentDetails.SaveButton saveText="Confirm" savingText="Saving..." />
|
|
428
|
+
*
|
|
429
|
+
* // Using asChild pattern for custom rendering
|
|
430
|
+
* <FulfillmentDetails.SaveButton asChild>
|
|
431
|
+
* {({ onSave, isSaving, canSave, hasChanges }) => (
|
|
432
|
+
* <button
|
|
433
|
+
* onClick={onSave}
|
|
434
|
+
* disabled={!canSave || isSaving}
|
|
435
|
+
* className={hasChanges ? 'bg-primary' : 'bg-secondary'}
|
|
436
|
+
* >
|
|
437
|
+
* {isSaving ? 'Saving...' : hasChanges ? 'Save Changes' : 'Confirm'}
|
|
438
|
+
* </button>
|
|
439
|
+
* )}
|
|
440
|
+
* </FulfillmentDetails.SaveButton>
|
|
441
|
+
* ```
|
|
442
|
+
*/
|
|
443
|
+
export declare const SaveButton: React.ForwardRefExoticComponent<SaveButtonProps & React.RefAttributes<HTMLButtonElement>>;
|
|
357
444
|
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import React from 'react';
|
|
2
|
+
import React, { useMemo, useRef, useEffect } from 'react';
|
|
3
3
|
import { CoreFulfillmentDetails } from './core/index.js';
|
|
4
4
|
import { FulfillmentTypeEnum, } from '../types/fulfillments-types.js';
|
|
5
5
|
import { AsChildSlot } from '@wix/headless-utils/react';
|
|
@@ -27,6 +27,11 @@ var TestIds;
|
|
|
27
27
|
TestIds["fulfillmentType"] = "fulfillment-type";
|
|
28
28
|
TestIds["deliveryAddress"] = "fulfillment-delivery-address";
|
|
29
29
|
TestIds["deliveryAddressInput"] = "fulfillment-delivery-address-input";
|
|
30
|
+
TestIds["addressPicker"] = "fulfillment-address-picker";
|
|
31
|
+
TestIds["addressPickerInput"] = "fulfillment-address-picker-input";
|
|
32
|
+
TestIds["addressPickerPredictions"] = "fulfillment-address-picker-predictions";
|
|
33
|
+
TestIds["addressPickerPrediction"] = "fulfillment-address-picker-prediction";
|
|
34
|
+
TestIds["saveButton"] = "fulfillment-save-button";
|
|
30
35
|
})(TestIds || (TestIds = {}));
|
|
31
36
|
/**
|
|
32
37
|
* Root headless component for Fulfillment Details
|
|
@@ -357,3 +362,203 @@ export const DeliveryAddress = React.forwardRef(({ children, asChild, className,
|
|
|
357
362
|
} }));
|
|
358
363
|
});
|
|
359
364
|
DeliveryAddress.displayName = 'FulfillmentDetails.DeliveryAddress';
|
|
365
|
+
const AddressPickerInput = React.memo(({ inputValue, onInputChange, predictions, isLoading, placeholder, inputClassName, predictionsClassName, predictionClassName, predictionMainTextClassName, predictionSecondaryTextClassName, predictionDescriptionClassName, onPredictionSelect, }) => {
|
|
366
|
+
// Track focused prediction index for keyboard navigation
|
|
367
|
+
const [focusedIndex, setFocusedIndex] = React.useState(-1);
|
|
368
|
+
const listRef = useRef(null);
|
|
369
|
+
const itemRefs = useRef([]);
|
|
370
|
+
// Reset focused index when predictions change
|
|
371
|
+
useEffect(() => {
|
|
372
|
+
setFocusedIndex(-1);
|
|
373
|
+
itemRefs.current = [];
|
|
374
|
+
}, [predictions.length]);
|
|
375
|
+
// Use ref to store latest onInputChange to keep handleChange stable
|
|
376
|
+
const onInputChangeRef = useRef(onInputChange);
|
|
377
|
+
useEffect(() => {
|
|
378
|
+
onInputChangeRef.current = onInputChange;
|
|
379
|
+
}, [onInputChange]);
|
|
380
|
+
// Use ref to store latest onPredictionSelect
|
|
381
|
+
const onPredictionSelectRef = useRef(onPredictionSelect);
|
|
382
|
+
useEffect(() => {
|
|
383
|
+
onPredictionSelectRef.current = onPredictionSelect;
|
|
384
|
+
}, [onPredictionSelect]);
|
|
385
|
+
// Stable onChange handler that always calls the latest onInputChange
|
|
386
|
+
const handleChange = useMemo(() => (e) => {
|
|
387
|
+
onInputChangeRef.current(e.target.value);
|
|
388
|
+
// Reset focus when user types
|
|
389
|
+
setFocusedIndex(-1);
|
|
390
|
+
}, []);
|
|
391
|
+
// Handle keyboard navigation
|
|
392
|
+
const handleKeyDown = useMemo(() => (e) => {
|
|
393
|
+
if (predictions.length === 0)
|
|
394
|
+
return;
|
|
395
|
+
switch (e.key) {
|
|
396
|
+
case 'ArrowDown':
|
|
397
|
+
e.preventDefault();
|
|
398
|
+
setFocusedIndex((prev) => {
|
|
399
|
+
const nextIndex = prev < predictions.length - 1 ? prev + 1 : 0;
|
|
400
|
+
// Scroll into view
|
|
401
|
+
setTimeout(() => {
|
|
402
|
+
itemRefs.current[nextIndex]?.scrollIntoView({
|
|
403
|
+
block: 'nearest',
|
|
404
|
+
behavior: 'smooth',
|
|
405
|
+
});
|
|
406
|
+
}, 0);
|
|
407
|
+
return nextIndex;
|
|
408
|
+
});
|
|
409
|
+
break;
|
|
410
|
+
case 'ArrowUp':
|
|
411
|
+
e.preventDefault();
|
|
412
|
+
setFocusedIndex((prev) => {
|
|
413
|
+
const nextIndex = prev > 0 ? prev - 1 : predictions.length - 1;
|
|
414
|
+
// Scroll into view
|
|
415
|
+
setTimeout(() => {
|
|
416
|
+
itemRefs.current[nextIndex]?.scrollIntoView({
|
|
417
|
+
block: 'nearest',
|
|
418
|
+
behavior: 'smooth',
|
|
419
|
+
});
|
|
420
|
+
}, 0);
|
|
421
|
+
return nextIndex;
|
|
422
|
+
});
|
|
423
|
+
break;
|
|
424
|
+
case 'Enter':
|
|
425
|
+
if (focusedIndex >= 0 && focusedIndex < predictions.length) {
|
|
426
|
+
const selectedPrediction = predictions[focusedIndex];
|
|
427
|
+
if (selectedPrediction) {
|
|
428
|
+
e.preventDefault();
|
|
429
|
+
onPredictionSelectRef.current(selectedPrediction);
|
|
430
|
+
setFocusedIndex(-1);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
break;
|
|
434
|
+
case 'Escape':
|
|
435
|
+
setFocusedIndex(-1);
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
}, [predictions, focusedIndex]);
|
|
439
|
+
const inputElement = useMemo(() => (_jsx("input", { type: "text", className: inputClassName, value: inputValue, onChange: handleChange, onKeyDown: handleKeyDown, placeholder: placeholder, "data-testid": TestIds.addressPickerInput, "aria-busy": isLoading, "aria-expanded": predictions.length > 0, "aria-controls": predictions.length > 0 ? 'address-picker-predictions' : undefined, role: "combobox" })), [
|
|
440
|
+
inputValue,
|
|
441
|
+
inputClassName,
|
|
442
|
+
placeholder,
|
|
443
|
+
handleChange,
|
|
444
|
+
handleKeyDown,
|
|
445
|
+
isLoading,
|
|
446
|
+
predictions.length,
|
|
447
|
+
]);
|
|
448
|
+
const predictionsList = predictions.length > 0 ? (_jsx("ul", { ref: listRef, id: "address-picker-predictions", className: predictionsClassName, "data-testid": TestIds.addressPickerPredictions, role: "listbox", children: predictions.map((prediction, index) => {
|
|
449
|
+
const isFocused = focusedIndex === index;
|
|
450
|
+
return (_jsxs("li", { ref: (el) => {
|
|
451
|
+
itemRefs.current[index] = el;
|
|
452
|
+
}, className: predictionClassName, onClick: () => {
|
|
453
|
+
onPredictionSelect(prediction);
|
|
454
|
+
setFocusedIndex(-1);
|
|
455
|
+
}, onMouseEnter: () => setFocusedIndex(index), onMouseLeave: () => {
|
|
456
|
+
// Only clear focus if it was set by mouse (not keyboard)
|
|
457
|
+
// Keep keyboard focus until user types or selects
|
|
458
|
+
}, "data-testid": TestIds.addressPickerPrediction, role: "option", "aria-selected": isFocused, "data-focused": isFocused ? 'true' : undefined, children: [prediction.mainText && (_jsx("div", { className: predictionMainTextClassName, children: prediction.mainText })), prediction.secondaryText && (_jsx("div", { className: predictionSecondaryTextClassName, children: prediction.secondaryText })), !prediction.mainText && !prediction.secondaryText && (_jsx("div", { className: predictionDescriptionClassName, children: prediction.description }))] }, prediction.searchId || index));
|
|
459
|
+
}) })) : null;
|
|
460
|
+
return (_jsxs(_Fragment, { children: [inputElement, predictionsList] }));
|
|
461
|
+
},
|
|
462
|
+
// Custom comparison function to prevent re-renders when only predictions array reference changes
|
|
463
|
+
// but the actual predictions content is the same
|
|
464
|
+
(prevProps, nextProps) => {
|
|
465
|
+
// Check if predictions array has actually changed (by comparing lengths and content)
|
|
466
|
+
const predictionsChanged = prevProps.predictions.length !== nextProps.predictions.length ||
|
|
467
|
+
prevProps.predictions.some((pred, index) => pred.searchId !== nextProps.predictions[index]?.searchId ||
|
|
468
|
+
pred.description !== nextProps.predictions[index]?.description);
|
|
469
|
+
return (prevProps.inputValue === nextProps.inputValue &&
|
|
470
|
+
prevProps.isLoading === nextProps.isLoading &&
|
|
471
|
+
prevProps.placeholder === nextProps.placeholder &&
|
|
472
|
+
prevProps.inputClassName === nextProps.inputClassName &&
|
|
473
|
+
prevProps.predictionsClassName === nextProps.predictionsClassName &&
|
|
474
|
+
prevProps.predictionClassName === nextProps.predictionClassName &&
|
|
475
|
+
prevProps.predictionMainTextClassName ===
|
|
476
|
+
nextProps.predictionMainTextClassName &&
|
|
477
|
+
prevProps.predictionSecondaryTextClassName ===
|
|
478
|
+
nextProps.predictionSecondaryTextClassName &&
|
|
479
|
+
prevProps.predictionDescriptionClassName ===
|
|
480
|
+
nextProps.predictionDescriptionClassName &&
|
|
481
|
+
prevProps.onInputChange === nextProps.onInputChange &&
|
|
482
|
+
prevProps.onPredictionSelect === nextProps.onPredictionSelect &&
|
|
483
|
+
!predictionsChanged);
|
|
484
|
+
});
|
|
485
|
+
export const AddressPicker = React.forwardRef(({ children, asChild, className, placeholder = 'Enter delivery address', inputClassName, predictionsClassName, predictionClassName, predictionMainTextClassName, predictionSecondaryTextClassName, predictionDescriptionClassName, minInputLength = 2, ...rest }, ref) => {
|
|
486
|
+
return (_jsx(CoreFulfillmentDetails.AddressPicker, { children: ({ inputValue, onInputChange, predictions, isLoading, onPredictionSelect, isDelivery, dispatchType, }) => {
|
|
487
|
+
// Don't render anything if not delivery
|
|
488
|
+
if (!isDelivery) {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
// Memoize defaultContent to prevent recreation when only predictions change
|
|
492
|
+
// This ensures the input element maintains focus
|
|
493
|
+
// Note: predictions is passed to AddressPickerInput which handles it internally,
|
|
494
|
+
// so we don't need to recreate defaultContent when only predictions change
|
|
495
|
+
const defaultContent = useMemo(() => (_jsx("div", { ref: ref, className: className, "data-testid": TestIds.addressPicker, ...rest, children: _jsx(AddressPickerInput, { inputValue: inputValue, onInputChange: onInputChange, predictions: predictions, isLoading: isLoading, placeholder: placeholder, inputClassName: inputClassName, predictionsClassName: predictionsClassName, predictionClassName: predictionClassName, predictionMainTextClassName: predictionMainTextClassName, predictionSecondaryTextClassName: predictionSecondaryTextClassName, predictionDescriptionClassName: predictionDescriptionClassName, onPredictionSelect: onPredictionSelect }) })),
|
|
496
|
+
// Include predictions in deps so AddressPickerInput receives updates
|
|
497
|
+
// AddressPickerInput is memoized and will only re-render when necessary
|
|
498
|
+
[
|
|
499
|
+
inputValue,
|
|
500
|
+
onInputChange,
|
|
501
|
+
predictions,
|
|
502
|
+
isLoading,
|
|
503
|
+
onPredictionSelect,
|
|
504
|
+
placeholder,
|
|
505
|
+
inputClassName,
|
|
506
|
+
predictionsClassName,
|
|
507
|
+
predictionClassName,
|
|
508
|
+
predictionMainTextClassName,
|
|
509
|
+
predictionSecondaryTextClassName,
|
|
510
|
+
predictionDescriptionClassName,
|
|
511
|
+
className,
|
|
512
|
+
// Note: rest and ref are intentionally excluded as they should be stable
|
|
513
|
+
]);
|
|
514
|
+
return (_jsx(AsChildSlot, { asChild: asChild, customElement: children, customElementProps: {
|
|
515
|
+
inputValue,
|
|
516
|
+
onInputChange,
|
|
517
|
+
predictions,
|
|
518
|
+
isLoading,
|
|
519
|
+
onPredictionSelect,
|
|
520
|
+
isDelivery,
|
|
521
|
+
dispatchType,
|
|
522
|
+
}, content: defaultContent, children: defaultContent }));
|
|
523
|
+
} }));
|
|
524
|
+
});
|
|
525
|
+
AddressPicker.displayName = 'FulfillmentDetails.AddressPicker';
|
|
526
|
+
/**
|
|
527
|
+
* Headless component for saving fulfillment details
|
|
528
|
+
* Saves the selected time slot to the FulfillmentsService
|
|
529
|
+
*
|
|
530
|
+
* @example
|
|
531
|
+
* ```tsx
|
|
532
|
+
* // Default usage - renders a save button
|
|
533
|
+
* <FulfillmentDetails.SaveButton saveText="Confirm" savingText="Saving..." />
|
|
534
|
+
*
|
|
535
|
+
* // Using asChild pattern for custom rendering
|
|
536
|
+
* <FulfillmentDetails.SaveButton asChild>
|
|
537
|
+
* {({ onSave, isSaving, canSave, hasChanges }) => (
|
|
538
|
+
* <button
|
|
539
|
+
* onClick={onSave}
|
|
540
|
+
* disabled={!canSave || isSaving}
|
|
541
|
+
* className={hasChanges ? 'bg-primary' : 'bg-secondary'}
|
|
542
|
+
* >
|
|
543
|
+
* {isSaving ? 'Saving...' : hasChanges ? 'Save Changes' : 'Confirm'}
|
|
544
|
+
* </button>
|
|
545
|
+
* )}
|
|
546
|
+
* </FulfillmentDetails.SaveButton>
|
|
547
|
+
* ```
|
|
548
|
+
*/
|
|
549
|
+
export const SaveButton = React.forwardRef(({ children, asChild, className, savingText = 'Saving...', saveText = 'Save', disabled, ...rest }, ref) => {
|
|
550
|
+
return (_jsx(CoreFulfillmentDetails.SaveButton, { children: ({ onSave, isSaving, canSave, hasChanges, selectedTimeSlot, dispatchType, }) => {
|
|
551
|
+
const isDisabled = disabled || !canSave || isSaving;
|
|
552
|
+
const buttonText = isSaving ? savingText : saveText;
|
|
553
|
+
const defaultContent = (_jsx("button", { ref: ref, type: "button", className: className, onClick: onSave, disabled: isDisabled, "data-testid": TestIds.saveButton, "data-saving": isSaving, "data-can-save": canSave, "data-has-changes": hasChanges, ...rest, children: buttonText }));
|
|
554
|
+
return (_jsx(AsChildSlot, { asChild: asChild, customElement: children, customElementProps: {
|
|
555
|
+
onSave,
|
|
556
|
+
isSaving,
|
|
557
|
+
canSave,
|
|
558
|
+
hasChanges,
|
|
559
|
+
selectedTimeSlot,
|
|
560
|
+
dispatchType,
|
|
561
|
+
}, content: defaultContent, children: defaultContent }));
|
|
562
|
+
} }));
|
|
563
|
+
});
|
|
564
|
+
SaveButton.displayName = 'FulfillmentDetails.SaveButton';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { type FulfillmentDetailsServiceConfig } from '../../services/fulfillment-details-service.js';
|
|
3
|
-
import { DispatchType, FulfillmentTypeEnum } from '../../types/fulfillments-types.js';
|
|
3
|
+
import { DispatchType, FulfillmentTypeEnum, TimeSlot } from '../../types/fulfillments-types.js';
|
|
4
4
|
export interface RootProps {
|
|
5
5
|
children: React.ReactNode;
|
|
6
6
|
fulfillmentDetailsServiceConfig?: FulfillmentDetailsServiceConfig;
|
|
@@ -142,3 +142,98 @@ export interface DeliveryAddressProps {
|
|
|
142
142
|
* ```
|
|
143
143
|
*/
|
|
144
144
|
export declare const DeliveryAddress: React.FC<DeliveryAddressProps>;
|
|
145
|
+
export interface AddressPrediction {
|
|
146
|
+
/** The human-readable name of the prediction */
|
|
147
|
+
description?: string;
|
|
148
|
+
/** The id of the prediction that can be used in place API */
|
|
149
|
+
searchId?: string;
|
|
150
|
+
/** Contains the main text of a prediction, usually the name of the place */
|
|
151
|
+
mainText?: string;
|
|
152
|
+
/** Contains the secondary text of a prediction, usually the location of the place */
|
|
153
|
+
secondaryText?: string;
|
|
154
|
+
}
|
|
155
|
+
export interface AddressPickerProps {
|
|
156
|
+
children: (props: {
|
|
157
|
+
/** Current input value */
|
|
158
|
+
inputValue: string;
|
|
159
|
+
/** Update the input value */
|
|
160
|
+
onInputChange: (value: string) => void;
|
|
161
|
+
/** List of address predictions */
|
|
162
|
+
predictions: AddressPrediction[];
|
|
163
|
+
/** Whether predictions are being loaded */
|
|
164
|
+
isLoading: boolean;
|
|
165
|
+
/** Select a prediction and fetch place details */
|
|
166
|
+
onPredictionSelect: (prediction: AddressPrediction) => Promise<void>;
|
|
167
|
+
/** Whether delivery is selected */
|
|
168
|
+
isDelivery: boolean;
|
|
169
|
+
/** Current dispatch type */
|
|
170
|
+
dispatchType: DispatchType | null;
|
|
171
|
+
}) => React.ReactNode;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Component that provides address autocomplete functionality
|
|
175
|
+
* Uses Atlas Autocomplete to predict addresses and Atlas Places to get place details
|
|
176
|
+
* Only provides functionality when dispatch type is DELIVERY
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```tsx
|
|
180
|
+
* <CoreFulfillmentDetails.AddressPicker>
|
|
181
|
+
* {({ inputValue, onInputChange, predictions, onPredictionSelect, isDelivery }) => (
|
|
182
|
+
* isDelivery && (
|
|
183
|
+
* <div>
|
|
184
|
+
* <input
|
|
185
|
+
* value={inputValue}
|
|
186
|
+
* onChange={(e) => onInputChange(e.target.value)}
|
|
187
|
+
* placeholder="Enter address"
|
|
188
|
+
* />
|
|
189
|
+
* {predictions.length > 0 && (
|
|
190
|
+
* <ul>
|
|
191
|
+
* {predictions.map((prediction, index) => (
|
|
192
|
+
* <li
|
|
193
|
+
* key={prediction.searchId || index}
|
|
194
|
+
* onClick={() => onPredictionSelect(prediction)}
|
|
195
|
+
* >
|
|
196
|
+
* {prediction.description}
|
|
197
|
+
* </li>
|
|
198
|
+
* ))}
|
|
199
|
+
* </ul>
|
|
200
|
+
* )}
|
|
201
|
+
* </div>
|
|
202
|
+
* )
|
|
203
|
+
* )}
|
|
204
|
+
* </CoreFulfillmentDetails.AddressPicker>
|
|
205
|
+
* ```
|
|
206
|
+
*/
|
|
207
|
+
export declare const AddressPicker: React.FC<AddressPickerProps>;
|
|
208
|
+
export interface SaveButtonProps {
|
|
209
|
+
children: (props: {
|
|
210
|
+
/** Save the current fulfillment details to the fulfillments service */
|
|
211
|
+
onSave: () => void;
|
|
212
|
+
/** Whether save operation is in progress */
|
|
213
|
+
isSaving: boolean;
|
|
214
|
+
/** Whether there are valid details to save */
|
|
215
|
+
canSave: boolean;
|
|
216
|
+
/** Whether the details have been modified */
|
|
217
|
+
hasChanges: boolean;
|
|
218
|
+
/** The currently selected time slot */
|
|
219
|
+
selectedTimeSlot: TimeSlot | null;
|
|
220
|
+
/** The currently selected dispatch type */
|
|
221
|
+
dispatchType: DispatchType | null;
|
|
222
|
+
}) => React.ReactNode;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Component that provides save functionality for fulfillment details
|
|
226
|
+
* Saves the selected time slot from FulfillmentDetailsService to FulfillmentsService
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```tsx
|
|
230
|
+
* <CoreFulfillmentDetails.SaveButton>
|
|
231
|
+
* {({ onSave, isSaving, canSave }) => (
|
|
232
|
+
* <button onClick={onSave} disabled={!canSave || isSaving}>
|
|
233
|
+
* {isSaving ? 'Saving...' : 'Save'}
|
|
234
|
+
* </button>
|
|
235
|
+
* )}
|
|
236
|
+
* </CoreFulfillmentDetails.SaveButton>
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
239
|
+
export declare const SaveButton: React.FC<SaveButtonProps>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useState, useEffect, useRef } from 'react';
|
|
3
3
|
import { WixServices, useService } from '@wix/services-manager-react';
|
|
4
4
|
import { FulfillmentsServiceDefinition } from '../../services/fulfillments-service.js';
|
|
5
5
|
import { FulfillmentDetailsService, FulfillmentDetailsServiceDefinition, loadFulfillmentDetailsServiceConfig, } from '../../services/fulfillment-details-service.js';
|
|
@@ -7,6 +7,7 @@ import { OLOSettingsServiceDefinition } from '../../services/olo-settings-servic
|
|
|
7
7
|
import { DispatchType, FulfillmentTypeEnum, } from '../../types/fulfillments-types.js';
|
|
8
8
|
import { createServicesMap } from '@wix/services-manager';
|
|
9
9
|
import { formatTimeRange, getToday, getFutureDate, } from '../../utils/date-utils.js';
|
|
10
|
+
import { autocomplete, places } from '@wix/atlas';
|
|
10
11
|
/**
|
|
11
12
|
* Root component for FulfillmentDetails
|
|
12
13
|
* Provides context for all fulfillment detail sub-components
|
|
@@ -222,3 +223,241 @@ export const DeliveryAddress = ({ children, }) => {
|
|
|
222
223
|
onAddressClear,
|
|
223
224
|
});
|
|
224
225
|
};
|
|
226
|
+
/**
|
|
227
|
+
* Component that provides address autocomplete functionality
|
|
228
|
+
* Uses Atlas Autocomplete to predict addresses and Atlas Places to get place details
|
|
229
|
+
* Only provides functionality when dispatch type is DELIVERY
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* ```tsx
|
|
233
|
+
* <CoreFulfillmentDetails.AddressPicker>
|
|
234
|
+
* {({ inputValue, onInputChange, predictions, onPredictionSelect, isDelivery }) => (
|
|
235
|
+
* isDelivery && (
|
|
236
|
+
* <div>
|
|
237
|
+
* <input
|
|
238
|
+
* value={inputValue}
|
|
239
|
+
* onChange={(e) => onInputChange(e.target.value)}
|
|
240
|
+
* placeholder="Enter address"
|
|
241
|
+
* />
|
|
242
|
+
* {predictions.length > 0 && (
|
|
243
|
+
* <ul>
|
|
244
|
+
* {predictions.map((prediction, index) => (
|
|
245
|
+
* <li
|
|
246
|
+
* key={prediction.searchId || index}
|
|
247
|
+
* onClick={() => onPredictionSelect(prediction)}
|
|
248
|
+
* >
|
|
249
|
+
* {prediction.description}
|
|
250
|
+
* </li>
|
|
251
|
+
* ))}
|
|
252
|
+
* </ul>
|
|
253
|
+
* )}
|
|
254
|
+
* </div>
|
|
255
|
+
* )
|
|
256
|
+
* )}
|
|
257
|
+
* </CoreFulfillmentDetails.AddressPicker>
|
|
258
|
+
* ```
|
|
259
|
+
*/
|
|
260
|
+
export const AddressPicker = ({ children }) => {
|
|
261
|
+
const fulfillmentDetailsService = useService(FulfillmentDetailsServiceDefinition);
|
|
262
|
+
const dispatchType = fulfillmentDetailsService.dispatchType?.get();
|
|
263
|
+
const isDelivery = dispatchType === DispatchType.DELIVERY;
|
|
264
|
+
const currentAddress = fulfillmentDetailsService?.address?.get();
|
|
265
|
+
const [inputValue, setInputValue] = useState(currentAddress?.formattedAddress ||
|
|
266
|
+
currentAddress?.addressLine ||
|
|
267
|
+
'');
|
|
268
|
+
const [predictions, setPredictions] = useState([]);
|
|
269
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
270
|
+
const [sessionToken, setSessionToken] = useState(null);
|
|
271
|
+
const isUserTypingRef = useRef(false);
|
|
272
|
+
const typingTimeoutRef = useRef(null);
|
|
273
|
+
// Generate a new session token when input changes
|
|
274
|
+
useEffect(() => {
|
|
275
|
+
if (inputValue.length > 0) {
|
|
276
|
+
const newToken = crypto.randomUUID();
|
|
277
|
+
setSessionToken(newToken);
|
|
278
|
+
}
|
|
279
|
+
}, [inputValue]);
|
|
280
|
+
// Update input value when address changes externally
|
|
281
|
+
// Only update if user is not actively typing to prevent focus loss
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
// Skip update if user is currently typing
|
|
284
|
+
if (isUserTypingRef.current) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const address = fulfillmentDetailsService?.address?.get();
|
|
288
|
+
if (address) {
|
|
289
|
+
const formatted = address.formattedAddress || address.addressLine || '';
|
|
290
|
+
if (formatted !== inputValue) {
|
|
291
|
+
setInputValue(formatted);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
else if (!address && inputValue) {
|
|
295
|
+
setInputValue('');
|
|
296
|
+
}
|
|
297
|
+
}, [currentAddress, inputValue]);
|
|
298
|
+
const onInputChange = async (value) => {
|
|
299
|
+
// Clear any existing timeout
|
|
300
|
+
if (typingTimeoutRef.current) {
|
|
301
|
+
clearTimeout(typingTimeoutRef.current);
|
|
302
|
+
}
|
|
303
|
+
// Mark that user is typing to prevent external updates from overwriting
|
|
304
|
+
isUserTypingRef.current = true;
|
|
305
|
+
setInputValue(value);
|
|
306
|
+
// Reset typing flag after user stops typing for 1 second
|
|
307
|
+
typingTimeoutRef.current = setTimeout(() => {
|
|
308
|
+
isUserTypingRef.current = false;
|
|
309
|
+
typingTimeoutRef.current = null;
|
|
310
|
+
}, 1000);
|
|
311
|
+
if (value.length < 2) {
|
|
312
|
+
setPredictions([]);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
setIsLoading(true);
|
|
316
|
+
try {
|
|
317
|
+
// Check if autocomplete module is available
|
|
318
|
+
if (!autocomplete || typeof autocomplete.predict !== 'function') {
|
|
319
|
+
throw new Error('Autocomplete predict function is not available');
|
|
320
|
+
}
|
|
321
|
+
const response = await autocomplete.predict(value, {
|
|
322
|
+
sessionToken: sessionToken || undefined,
|
|
323
|
+
});
|
|
324
|
+
const addressPredictions = response.predictions?.map((prediction) => ({
|
|
325
|
+
description: prediction.description,
|
|
326
|
+
searchId: prediction.searchId,
|
|
327
|
+
mainText: prediction.textStructure?.mainText,
|
|
328
|
+
secondaryText: prediction.textStructure?.secondaryText,
|
|
329
|
+
})) || [];
|
|
330
|
+
setPredictions(addressPredictions);
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
console.error('Error fetching address predictions:', error);
|
|
334
|
+
// Log more details about the error
|
|
335
|
+
if (error?.response?.status === 404) {
|
|
336
|
+
console.error('API endpoint not found. Make sure you are in a Wix environment with proper permissions.');
|
|
337
|
+
}
|
|
338
|
+
setPredictions([]);
|
|
339
|
+
}
|
|
340
|
+
finally {
|
|
341
|
+
setIsLoading(false);
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
const onPredictionSelect = async (prediction) => {
|
|
345
|
+
if (!prediction.searchId) {
|
|
346
|
+
console.error('Prediction missing searchId');
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
setIsLoading(true);
|
|
350
|
+
try {
|
|
351
|
+
// Check if places module is available
|
|
352
|
+
if (!places || typeof places.getPlace !== 'function') {
|
|
353
|
+
throw new Error('Places getPlace function is not available');
|
|
354
|
+
}
|
|
355
|
+
const response = await places.getPlace(prediction.searchId, {
|
|
356
|
+
sessionToken: sessionToken || undefined,
|
|
357
|
+
});
|
|
358
|
+
if (response.place?.address) {
|
|
359
|
+
const address = response.place.address;
|
|
360
|
+
const addressLine = address.addressLine ||
|
|
361
|
+
address.addressLine1 ||
|
|
362
|
+
(address.streetAddress
|
|
363
|
+
? `${address.streetAddress.number || ''} ${address.streetAddress.name || ''}`.trim()
|
|
364
|
+
: '') ||
|
|
365
|
+
'';
|
|
366
|
+
// Construct formatted address from components
|
|
367
|
+
const addressParts = [
|
|
368
|
+
addressLine,
|
|
369
|
+
address.city,
|
|
370
|
+
address.subdivision,
|
|
371
|
+
address.postalCode,
|
|
372
|
+
address.country,
|
|
373
|
+
].filter(Boolean);
|
|
374
|
+
const formattedAddress = addressParts.join(', ') || prediction.description || '';
|
|
375
|
+
const addressFields = {
|
|
376
|
+
addressLine,
|
|
377
|
+
addressLine2: address.addressLine2 || '',
|
|
378
|
+
city: address.city || '',
|
|
379
|
+
subdivision: address.subdivision || '',
|
|
380
|
+
postalCode: address.postalCode || '',
|
|
381
|
+
country: address.country || '',
|
|
382
|
+
formattedAddress,
|
|
383
|
+
};
|
|
384
|
+
// TODO - check first available time slot & available dates
|
|
385
|
+
fulfillmentDetailsService.setAddress(addressFields);
|
|
386
|
+
// Clear any pending timeout and mark that this is a programmatic update, not user typing
|
|
387
|
+
if (typingTimeoutRef.current) {
|
|
388
|
+
clearTimeout(typingTimeoutRef.current);
|
|
389
|
+
typingTimeoutRef.current = null;
|
|
390
|
+
}
|
|
391
|
+
isUserTypingRef.current = false;
|
|
392
|
+
setInputValue(formattedAddress);
|
|
393
|
+
setPredictions([]);
|
|
394
|
+
// Invalidate session token after place selection
|
|
395
|
+
setSessionToken(null);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
console.error('Error fetching place details:', error);
|
|
400
|
+
}
|
|
401
|
+
finally {
|
|
402
|
+
setIsLoading(false);
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
return children({
|
|
406
|
+
inputValue,
|
|
407
|
+
onInputChange,
|
|
408
|
+
predictions,
|
|
409
|
+
isLoading,
|
|
410
|
+
onPredictionSelect,
|
|
411
|
+
isDelivery,
|
|
412
|
+
dispatchType,
|
|
413
|
+
});
|
|
414
|
+
};
|
|
415
|
+
/**
|
|
416
|
+
* Component that provides save functionality for fulfillment details
|
|
417
|
+
* Saves the selected time slot from FulfillmentDetailsService to FulfillmentsService
|
|
418
|
+
*
|
|
419
|
+
* @example
|
|
420
|
+
* ```tsx
|
|
421
|
+
* <CoreFulfillmentDetails.SaveButton>
|
|
422
|
+
* {({ onSave, isSaving, canSave }) => (
|
|
423
|
+
* <button onClick={onSave} disabled={!canSave || isSaving}>
|
|
424
|
+
* {isSaving ? 'Saving...' : 'Save'}
|
|
425
|
+
* </button>
|
|
426
|
+
* )}
|
|
427
|
+
* </CoreFulfillmentDetails.SaveButton>
|
|
428
|
+
* ```
|
|
429
|
+
*/
|
|
430
|
+
export const SaveButton = ({ children }) => {
|
|
431
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
432
|
+
const fulfillmentsService = useService(FulfillmentsServiceDefinition);
|
|
433
|
+
const fulfillmentDetailsService = useService(FulfillmentDetailsServiceDefinition);
|
|
434
|
+
const selectedTimeSlot = fulfillmentDetailsService?.timeslot?.get() ?? null;
|
|
435
|
+
const dispatchType = fulfillmentDetailsService?.dispatchType?.get() ?? null;
|
|
436
|
+
// Compare with the current fulfillments service state to determine if there are changes
|
|
437
|
+
const currentFulfillmentsTimeSlot = fulfillmentsService?.selectedTimeSlot?.get() ?? null;
|
|
438
|
+
const hasChanges = selectedTimeSlot?.id !== currentFulfillmentsTimeSlot?.id ||
|
|
439
|
+
selectedTimeSlot?.startTime?.getTime() !==
|
|
440
|
+
currentFulfillmentsTimeSlot?.startTime?.getTime();
|
|
441
|
+
const canSave = selectedTimeSlot !== null;
|
|
442
|
+
const onSave = () => {
|
|
443
|
+
if (!selectedTimeSlot || isSaving || !hasChanges)
|
|
444
|
+
return;
|
|
445
|
+
setIsSaving(true);
|
|
446
|
+
try {
|
|
447
|
+
console.log('onSave', selectedTimeSlot);
|
|
448
|
+
// Save the selected time slot to the fulfillments service
|
|
449
|
+
fulfillmentsService.setSelectedTimeSlot(selectedTimeSlot);
|
|
450
|
+
}
|
|
451
|
+
finally {
|
|
452
|
+
setIsSaving(false);
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
return children({
|
|
456
|
+
onSave,
|
|
457
|
+
isSaving,
|
|
458
|
+
canSave,
|
|
459
|
+
hasChanges,
|
|
460
|
+
selectedTimeSlot,
|
|
461
|
+
dispatchType,
|
|
462
|
+
});
|
|
463
|
+
};
|