import { CollectionState, ErrorMonitor, FiltersMap, Fqdn, KeyedItem, PartialWindow, PickerEvents, PickerState, ReportBI, TaskState, withoutIrrelevant, } from '@wix/bex-core'; import { replaceUrlCurlySegments } from '../utils/replaceUrlCurlySegments'; import { action, makeObservable, observable, runInAction } from 'mobx'; import { validateEncodedUrl } from '../utils/validateEncodedUrl'; import { LogoType } from '../hooks'; import { cairoCtaClicked, newItemCreationEnd, newItemCreationStart, } from '@wix/bex-core/bi'; import { registerMatchMedia } from '../utils/matchMediaQuery'; const withoutDefaults = withoutIrrelevant< | 'currentTab' | 'currentView' | 'currentFilters' | 'currentSortOrder' | 'listSize' | 'filteredListSize' | 'maxItems' | 'initialItems' | 'type' | 'componentType' >(); type OnNavigate = (params: { item: T; index: number; url: string; }) => unknown; export interface PickerStandaloneEvents extends PickerEvents { readonly onSingleItemAutoNavigation?: OnNavigate; readonly onContinueToDestinationUrl?: OnNavigate; } export interface PickerStandaloneStateParams< T, F extends FiltersMap = FiltersMap, > { collection: CollectionState; initialSelect?: true | ((item: T, index: number) => boolean); noItemsAvailableDestinationUrl?: string; defaultDestinationUrl: string | ((item: T) => string); window: PartialWindow; errorMonitor: ErrorMonitor; logoType: LogoType; urlSegmentPropertyMap?: Record; events?: PickerStandaloneEvents; shouldFilterNotAffectTotalCount?: (filterName: keyof F) => boolean; createBILogger:

(defaultParams: P) => ReportBI; } const supportedProtocols = ['https:', 'http:']; function tryParseUrl(str: string) { try { const url = new URL(str); validateEncodedUrl(url); if (!supportedProtocols.includes(url.protocol)) { throw new Error(`Unsupported action url protocol: ${url.protocol}`); } return url; } catch (e) { console.error(e); } return undefined; } export class PickerStandaloneState { readonly picker: PickerState; readonly initialSelect?: PickerStandaloneStateParams['initialSelect']; readonly noItemsAvailableDestinationUrl?: string; readonly defaultDestinationUrl: string | ((item: T) => string); primaryButtonText: string | null; title?: string | null | undefined; subtitle?: string | null | undefined; destinationUrlTemplate?: URL; readonly reportBi; readonly window: PartialWindow; readonly errorMonitor: ErrorMonitor; readonly urlSegmentPropertyMap?: Record; readonly events: PickerStandaloneEvents; readonly initTask = new TaskState(); readonly logoType: PickerStandaloneStateParams['logoType']; submitted = false; isMobileView: boolean; isDuringSearch: boolean; _hasCreateNewAction?: boolean; constructor(params: Readonly>) { this.reportBi = params.createBILogger({ ...params.collection.fqdn, type: 'standalone', componentType: 'Picker Standalone', }); this.picker = new PickerState({ getMaxItems: () => 1, collection: params.collection, shouldFilterNotAffectTotalCount: params.shouldFilterNotAffectTotalCount, events: params.events, reportBi: this.reportBi, loadStartTime: 0, // for standalone we want measure from start of navigation }); this.noItemsAvailableDestinationUrl = params.noItemsAvailableDestinationUrl; this.defaultDestinationUrl = params.defaultDestinationUrl; this.initialSelect = params.initialSelect; this.window = params.window; this.errorMonitor = params.errorMonitor; this.urlSegmentPropertyMap = params.urlSegmentPropertyMap; this.events = params.events ?? {}; this.logoType = params.logoType; const windowUrl = new URL(this.window.location.href); this.primaryButtonText = windowUrl.searchParams.get('primaryButtonText'); this.title = windowUrl.searchParams.get('title')?.trim(); this.subtitle = windowUrl.searchParams.get('subtitle'); const maybeActionUrlString = windowUrl.searchParams.get('actionUrl'); if (maybeActionUrlString) { this.destinationUrlTemplate = tryParseUrl(maybeActionUrlString); } this.isMobileView = !this.window.matchMedia(`(min-width: 768px)`).matches; this.isDuringSearch = false; makeObservable(this, { handlePrimaryButtonClick: action.bound, submitted: observable.ref, isMobileView: observable.ref, isDuringSearch: observable.ref, setIsDuringSearch: action, }); } onCreateNewClick() { this.reportBi( withoutDefaults(newItemCreationStart)({ ...(this.picker._commonDynamicBiParams() as {}), }), ); } onCreateNewActionCompleted({ itemId }: { itemId?: string }) { this.reportBi( withoutDefaults(newItemCreationEnd)({ ...this.picker._commonDynamicBiParams(), resultType: 'success', itemId, }), ); } init() { const { picker, initTask, noItemsAvailableDestinationUrl, initialSelect, window, errorMonitor, events: { onSingleItemAutoNavigation }, } = this; const matchMedia = this.window.matchMedia(`(min-width: 768px)`); const matchMediaChangeListener = ({ matches }: { matches: boolean }) => { this.isMobileView = !matches; }; const disposeMedia = registerMatchMedia({ errorMonitor, matchMediaChangeListener, matchMedia, }); const disposers = [ picker.init(), this.subscribeResultUpdated(), disposeMedia, ]; initTask.run( async () => { await picker.initTask.status.promise; await runInAction(async () => { const { collection } = picker; const { result, hasAvailableItems, bulkSelect, query: { search: { isEmpty: searchIsEmpty }, }, } = collection; const { status, keyedItems, total } = result; if ( status.isSuccess && !hasAvailableItems && !this._hasCreateNewAction && noItemsAvailableDestinationUrl ) { window.location.href = noItemsAvailableDestinationUrl; await new Promise(() => {}); // keep showing loader while navigating return; } if ( status.isSuccess && total === 1 && !this._hasCreateNewAction && searchIsEmpty ) { const navigated = this.navigateToDestinationUrl( keyedItems[0], onSingleItemAutoNavigation, ); if (navigated) { await new Promise(() => {}); // keep showing loader while navigating } return; } if (initialSelect) { const selectedItem = initialSelect === true ? keyedItems[0] : keyedItems.find(({ item }, index) => initialSelect(item, index), ); if (selectedItem != null) { bulkSelect.selectOneItem(selectedItem); } } }); }, (error) => { errorMonitor.captureException(error); }, ); return () => { for (const disposer of disposers) { disposer(); } }; } private resolveDestinationUrl(item: T) { const { destinationUrlTemplate, defaultDestinationUrl, urlSegmentPropertyMap, } = this; if (destinationUrlTemplate) { return replaceUrlCurlySegments( destinationUrlTemplate, item, urlSegmentPropertyMap, ); } if (typeof defaultDestinationUrl === 'string') { const url = tryParseUrl(defaultDestinationUrl); if (url) { return replaceUrlCurlySegments(url, item, urlSegmentPropertyMap); } return defaultDestinationUrl; } return defaultDestinationUrl(item); } private navigateToDestinationUrl( { item, index }: KeyedItem, onNavigate: OnNavigate | undefined, ) { const { window } = this; const destinationUrl = this.resolveDestinationUrl(item); const url = destinationUrl.toString(); onNavigate?.({ item, index, url }); window.location.href = url; return true; } handlePrimaryButtonClick(keyedItem: KeyedItem) { const { events: { onContinueToDestinationUrl }, submitted, picker, reportBi, } = this; if (submitted) { return; } picker.toggle(keyedItem, { clear: true }); reportBi( cairoCtaClicked({ ...this.picker._commonDynamicBiParams(), url: typeof window !== 'undefined' ? window.location.href : '', ctaIndex: 1, numCtas: 1, location: 'pickerStandalone', moreActionsIndex: 0, ctaName: 'continue', }), ); this.submitted = this.navigateToDestinationUrl( keyedItem, onContinueToDestinationUrl, ); } setIsDuringSearch(newVal: boolean = !this.isDuringSearch) { this.isDuringSearch = newVal; } private subscribeResultUpdated() { const onResultUpdated = action(() => { const { picker: { collection: { result, bulkSelect }, }, } = this; bulkSelect.select.filter((value) => result.has(value.key)); }); const { picker: { collection }, } = this; collection.emitter.on('resultUpdated', onResultUpdated); return () => { collection.emitter.off('resultUpdated', onResultUpdated); }; } }