import { ComputedQuery, Filter, FiltersMap, fixDomainForBI, PaginationModeRef, parseFqdn, PickerState, prefetchTranslations, RawFilters, selectionStatusWixPatternsPreset, withoutDefaults, } from '@wix/bex-core'; import { PromiseState, reactive } from '@wix/bex-utils'; import { UsePickerModalParams } from './usePickerModal'; import { MultipleSelection } from '../../components/MaxSelection'; import { OnSelectOptions, RawFiltersParams } from './UsePickerModalParamsBase'; import { CustomModalContent } from '../customModalContent'; import mapValues from 'lodash/mapValues'; import some from 'lodash/some'; import { loadStart, pickerPickerOpenButton, pickerPickModalCtaButtonClickOrImmediateCta, } from '@wix/bex-core/bi'; import type { PickerContentState } from '../../state'; export class PickerModalState { readonly multipleRef = {} as { current: MultipleSelection | undefined }; readonly pickerContentRef = {} as { current: | { onCreateNew?: (items?: T[]) => unknown; } | undefined; }; readonly onSelectRef = {} as { current: (items: T[], options: OnSelectOptions) => unknown; }; readonly disableAutoSelectionRef = {} as { current: boolean | undefined }; readonly bi; readonly _isModalOpen; readonly _isCustomContentVisible = reactive(false); readonly _customContent = reactive | undefined>( undefined, ); readonly _isOpenButtonClicked = reactive(false); readonly fetchFirstPage; readonly fetchPickerComponent; readonly fetchInitialProps; readonly minContentHeight; readonly shouldFilterNotAffectTotalCount; readonly queryName; readonly fetchData; readonly limit; readonly _paginationModeRef = {} as PaginationModeRef; readonly events; readonly filters; readonly environment; constructor(params: UsePickerModalParams) { const { onSelect, errorMonitor, biLoggerFactory, environment, queryName, isOpenInitially = false, skipFirstPagePrefetch, } = params; this.minContentHeight = params.minContentHeight ?? 300; this.queryName = params.queryName; this.fetchData = params.fetchData; this.limit = params.limit; this._paginationModeRef.current = params.paginationMode; this.events = params.events; this.filters = params.filters ?? ({} as Partial>); this.shouldFilterNotAffectTotalCount = params.shouldFilterNotAffectTotalCount ?? (() => false); this.environment = environment; this.bi = biLoggerFactory() .updateDefaults({ ...(environment.providerType === 'bm' && { _hostingPlatform: 'business-manager', }), artifactId: environment.artifactId, componentName: environment.componentName, cairoVersion: environment.uiLibVersion, ...parseFqdn(queryName), type: 'modal', componentType: 'Picker Modal', }) .logger(); this._isModalOpen = reactive(isOpenInitially); this.onSelectRef = { current: onSelect }; const onPrefetchPromiseError = (err: unknown) => { errorMonitor.captureException(err); this.events?.onError?.({ err, isOnline: true, }); }; this.fetchFirstPage = new PromiseState({ onError: onPrefetchPromiseError, fn: async () => { const prefetchComputedQuery = ({ filters: queryFilters, }: { filters: RawFilters; }): ComputedQuery => { const hasActiveFilters = some( queryFilters, (filter) => filter?.initialValue != null, ); return { page: 1, limit: params.limit ?? 100, offset: 0, cursor: null, search: undefined, rawSearch: '', filters: queryFilters, rawFilters: queryFilters, hasActiveFilters, hasNonPersistentActiveFilters: hasActiveFilters, }; }; const firstPagePromise = !skipFirstPagePrefetch ? this.fetchData( prefetchComputedQuery({ filters: mapValues(this.filters, (filter) => filter != null ? filter.initialValue : filter, ) as ComputedQuery }>['filters'], }), ) : undefined; const [firstPageResult] = await Promise.all([firstPagePromise]); return { firstPageResult, }; }, }); this.fetchPickerComponent = new PromiseState({ onError: onPrefetchPromiseError, fn: async () => { return (await params.pickerContentComponent()).default; }, }); this.fetchInitialProps = new PromiseState({ onError: onPrefetchPromiseError, fn: async () => { const [cairoMessages] = await Promise.all([ prefetchTranslations({ errorMonitor })(environment.language).catch( (e) => { console.error(e); // Fallback to `PickerModalContent` -> `WixPatternsContainer` i18n init return {}; }, ), params.fetchInitialProps?.(), ]); return { cairoMessages }; }, }); } get isCustomContentVisible() { return this._isCustomContentVisible.value; } get isModalOpen() { return this._isModalOpen.value; } get paginationMode() { return this._paginationModeRef.current; } set paginationMode(value: 'offset' | 'cursor' | undefined) { this._paginationModeRef.current = value; } prefetch = async () => { try { await Promise.all([ this.fetchPickerComponent.once(), this.fetchFirstPage.once(), this.fetchInitialProps.once(), ]); return true; } catch (e) { console.error(e); return false; } }; hover = () => { this.bi.report( pickerPickerOpenButton({ action: 'hover', ...this._commonDynamicBiParams(), }), ); this.prefetch(); }; onCreateNew = async (items?: T[]) => { const { pickerContentRef, _isCustomContentVisible } = this; pickerContentRef.current?.onCreateNew?.(items); if (_isCustomContentVisible.value) { _isCustomContentVisible.value = false; } }; onPrimaryButtonClick = ( state: | (PickerContentState & { // Before multiple status refactor compat indeterminateMap?: { partialModeKeys?: string[] }; }) | PickerState, additionalStepData?: unknown, ) => { const { picker, indeterminateMap } = 'picker' in state ? state : { picker: state, indeterminateMap: undefined }; const { collection } = picker; const { _isModalOpen, onSelectRef } = this; const { bulkSelect: { selectedValues, uncheckedValues, allSelected, indeterminateSet, select, }, } = collection; _isModalOpen.value = false; const selectedCount = allSelected ? collection.result.total - uncheckedValues.length : selectedValues.length; onSelectRef.current(selectedValues, { uncheckedValues, isSelectAll: allSelected, selectedCount, // backwards compat with pikachu GA partiallySelectedIds: indeterminateSet ? Array.from(indeterminateSet) : indeterminateMap?.partialModeKeys ?? [], values: (select.toArray ?? []).map(({ value, status }) => ({ status, value: value.item, })), defaultStatus: select.defaultStatus, additionalStepData, }); }; openModal = () => { const { _isOpenButtonClicked, _isModalOpen } = this; _isOpenButtonClicked.value = true; _isModalOpen.value = true; }; closeModal = () => { const { _isModalOpen } = this; _isModalOpen.value = false; }; openModalOrCTA = async () => { const { bi, _isOpenButtonClicked, fetchFirstPage, onSelectRef } = this; bi.report( pickerPickerOpenButton({ action: 'click', ...this._commonDynamicBiParams(), }), ); _isOpenButtonClicked.value = true; try { await fetchFirstPage.once(); const { status } = fetchFirstPage; if ( status.value.type === 'success' && status.value.data.firstPageResult != null ) { const { firstPageResult: { total, items }, } = status.value.data; if (!this.disableAutoSelectionRef.current && total === 1) { bi.report( pickerPickModalCtaButtonClickOrImmediateCta({ isImmediate: true, itemsCnt: 1, listSize: total, filteredListSize: total, ...this._commonDynamicBiParams(), }), ); onSelectRef.current(items, { uncheckedValues: [], isSelectAll: false, selectedCount: items.length, partiallySelectedIds: [], values: [], defaultStatus: selectionStatusWixPatternsPreset.checked, }); return; } } } catch (e) { console.error(e); } this._isModalOpen.value = true; }; get isOpenButtonClicked() { return this._isOpenButtonClicked.value; } get noAvailableItemsFound() { const { fetchFirstPage } = this; return ( fetchFirstPage.status.value.type === 'success' && fetchFirstPage.status.value.data.firstPageResult?.available === 0 ); } get showButtonLoadingIndication() { const { _isOpenButtonClicked, fetchFirstPage } = this; return ( _isOpenButtonClicked.value && fetchFirstPage.status.value.type === 'loading' ); } get customContent() { return this._customContent.value; } _commonDynamicBiParams(): {} { return { maxItems: this.getMaxItems(), }; } getMaxItems = () => { const { current: multiple } = this.multipleRef; return typeof multiple === 'number' && Number.isInteger(multiple) ? multiple : multiple ? undefined : 1; }; onIsOpenChanged() { const { isModalOpen } = this; if (isModalOpen) { this.prefetch(); this._reportModalLoaded(); } } init() { const { bi } = this; bi.report( pickerPickerOpenButton({ action: 'render', ...this._commonDynamicBiParams(), }), ); // if the modal is already open, report startLoad BI, otherwise prefetch dependencies if (!this.isModalOpen) { // the timeout delay (500) indicates how soon (or late) we want to execute the callback, in case requestIdleCallback is not implemented const _requestIdleCallback = window.requestIdleCallback ?? ((cb: () => void) => { return window.setTimeout(cb, 500); }); const requestId = _requestIdleCallback(() => { this.fetchFirstPage.once(); }); return () => { if (window.cancelIdleCallback) { window.cancelIdleCallback(requestId); } else { window.clearTimeout(requestId); } }; } return; } _reportModalLoaded() { this.bi.report( withoutDefaults(loadStart)({ ...this._commonDynamicBiParams(), url: fixDomainForBI(window.location.href), featuresAvailability: JSON.stringify({}), }), ); } }