import { ConditionalModalState, FiltersMap, ShowToast, Tags, WixPatternsError, } from '@wix/bex-core'; import { TagsBulkAssignPopoverState, TagsBulkAssignPopoverStateParams, } from './TagsBulkAssignPopoverState'; import { BulkActionModalRenderPropParams } from '../components/BulkAction'; import { computed, makeObservable, observable, runInAction, action, } from 'mobx'; import { getAsyncJob } from '@wix/bex-utils/@wix/ambassador-infra-asyncjobs-v1-async-job/http'; import { AsyncJob } from '@wix/bex-utils/@wix/ambassador-infra-asyncjobs-v1-async-job/types'; export const POLL_DELAY = 500; export const MILLIS_FOR_SHOWING_SUCCESS = 1000; export const MAX_JOB_RETRIES_COUNT = 4; export interface AsyncBulkUpdateTagsParams extends Pick< BulkActionModalRenderPropParams, 'allSelected' | 'selectedValues' | 'uncheckedValues' | 'query' > { assignTags: Tags; unassignTags: Tags; } export interface TagsBulkAssignAsyncStateParams extends Pick< TagsBulkAssignPopoverStateParams, 'asyncBulkUpdateTags' | 'collectionState' > { container: TagsBulkAssignPopoverState['container']; optimisticActions: TagsBulkAssignPopoverState['optimisticActions']; tagsBIReporter: TagsBulkAssignPopoverState['tagsBIReporter']; } export class TagsBulkAssignAsyncState { readonly collectionState; readonly optimisticActions; readonly container; readonly tagsBIReporter; readonly asyncBulkUpdateTags; readonly asyncJobProgressModalState = new ConditionalModalState(); job: AsyncJob | null = null; timeout?: number; isDuringApplyChanges = false; currentInvocationParams: AsyncBulkUpdateTagsParams | null = null; updatingTagsToast: ReturnType | null = null; getJobRetriesCount = 0; constructor(params: TagsBulkAssignAsyncStateParams) { const { collectionState, optimisticActions, container, tagsBIReporter, asyncBulkUpdateTags, } = params; this.collectionState = collectionState; this.optimisticActions = optimisticActions; this.container = container; this.tagsBIReporter = tagsBIReporter; this.asyncBulkUpdateTags = asyncBulkUpdateTags; makeObservable(this, { job: observable.ref, isDuringApplyChanges: observable, init: action, applyChanges: action, pollJobStatus: action, canBulkAssignAsync: computed, isJobCompleted: computed, isJobFailed: computed, isJobEnded: computed, isJobInProgress: computed, updatedItemsPercentage: computed, }); } init() { const { onBeforeUnload } = this.container; const onBeforeUnloadSubscription = onBeforeUnload?.( action((event: Event) => { if (this.isDuringApplyChanges) { event.preventDefault(); } }), ); const disposers = [ () => { onBeforeUnloadSubscription?.remove(); }, () => { if (this.timeout) { window.clearTimeout(this.timeout); } }, ]; return () => { disposers.forEach((disposer) => disposer()); }; } async applyChanges({ newAssignedTagIds, newUnassignedTagIds, }: { newAssignedTagIds: string[]; newUnassignedTagIds: string[]; }) { this.isDuringApplyChanges = true; const { bulkSelect, result } = this.collectionState; const { allSelected, selectedValues, uncheckedValues } = bulkSelect; const params = { allSelected, selectedValues, uncheckedValues, query: result.originQuery, assignTags: { privateTags: { tagIds: newAssignedTagIds } }, unassignTags: { privateTags: { tagIds: newUnassignedTagIds } }, }; await this.triggerBulkAssign(params); } async triggerBulkAssign(params: AsyncBulkUpdateTagsParams) { if (!this.asyncBulkUpdateTags) { throw new Error( 'tried to bulk assign tags async without asyncBulkUpdateTags prop given', ); } this.currentInvocationParams = params; try { await this.startTagUpdateProcess(params); } catch (error) { this.showFailToCreateJobToast(error); this.resetStateParams(); } } async startTagUpdateProcess(params: AsyncBulkUpdateTagsParams) { this.openAsyncJobProgressModal(); const { jobId } = await this.asyncBulkUpdateTags!(params); await this.pollJobStatus(jobId); } async pollJobStatus(jobId: string) { let job: AsyncJob | null = null; try { job = await this.fetchJobStatus(jobId); } catch (error) { this.handlePollingError(error); return; } runInAction(() => { this.job = job; if (this.isJobInProgress || !job) { this.scheduleNextPoll(jobId); } else if (this.isJobEnded) { this.handleJobEnd(); } }); } handlePollingError(error: unknown) { this.asyncJobProgressModalState.modal.close(); this.showFailedToPollToast(error); this.resetStateParams(); } async fetchJobStatus(jobId: string): Promise { try { const { data: { job }, } = await this.container.errorHandler.withErrorHandler( () => this.container.httpClient.request(getAsyncJob({ jobId })), {}, ); this.getJobRetriesCount = 0; return job as AsyncJob; } catch (error) { if (this.getJobRetriesCount >= MAX_JOB_RETRIES_COUNT) { throw new WixPatternsError({ originalException: error, errorCode: 'BulkAssignTagsFailed', }); } this.container.errorHandler.reportRetryAttempt?.(error); this.getJobRetriesCount++; return null; } } scheduleNextPoll(jobId: string): void { this.timeout = window.setTimeout(() => { runInAction(() => this.pollJobStatus(jobId)); }, POLL_DELAY); } handleJobEnd(): void { this.timeout = window.setTimeout(() => { runInAction(() => { this.scheduleJobCompletionHandling(); }); }, MILLIS_FOR_SHOWING_SUCCESS); } scheduleJobCompletionHandling() { const lastInvocationParams = this.currentInvocationParams; const { showToast, translate: t } = this.container; if (this.isJobCompleted) { this.handleJobSuccess(lastInvocationParams); } else if (this.isJobFailed) { showToast?.({ type: 'ERROR', message: t('cairo.tags.bulkItemUpdate.toast.error', { total: this.job?.counts?.failCount!, }), biName: 'cairo-update-bulk-assign-tags-async-job-status-fail', }); } this.resetStateParams(); } handleJobSuccess( lastInvocationParams: AsyncBulkUpdateTagsParams | null, ) { const failCount = this.job?.counts?.failCount; if (failCount) { this.showPartialFailureToast({ failCount, lastInvocationParams }); } this.showJobCompletedSuccessfullyToast(); this.refreshCollection(); } resetStateParams() { this.getJobRetriesCount = 0; this.job = null; this.currentInvocationParams = null; this.isDuringApplyChanges = false; this.updatingTagsToast?.remove(); this.updatingTagsToast = null; this.closeAsyncJobProgressModal(); } showPartialFailureToast({ failCount, lastInvocationParams, }: { failCount: number; lastInvocationParams: AsyncBulkUpdateTagsParams | null; }) { this.container.showToast?.({ type: 'WARNING', message: this.container.translate( 'cairo.tags.bulkItemUpdate.toast.error', { total: failCount }, ), action: { text: this.container.translate( 'cairo.tags.bulkItemUpdate.toast.error.CTA', ), onClick: async () => { if (lastInvocationParams) { await this.triggerBulkAssign(lastInvocationParams); } }, removeToastOnClick: true, }, biName: 'cairo-update-bulk-assign-tags-async-fail', }); } showJobCompletedSuccessfullyToast() { this.container.showToast?.({ type: 'SUCCESS', message: this.container.translate( 'cairo.tags.bulkItemUpdate.toast.success', ), biName: 'cairo-update-bulk-assign-tags-async-success', }); } showFailToCreateJobToast(error: unknown) { const { errorHandler, showToast, translate: t } = this.container; const resolvedError = errorHandler?.getResolvedError?.(error); showToast?.({ type: 'ERROR', message: resolvedError?.message ?? t('cairo.tags.bulkItemUpdate.toast.error', { total: this.collectionState.bulkSelect.selectedCountOrTotal, }), action: resolvedError?.action, biName: 'cairo-update-bulk-assign-tags-async-create-job-fail', details: resolvedError?.requestId ? { requestId: resolvedError.requestId } : undefined, }); } showFailedToPollToast(error: unknown) { const { errorHandler, showToast } = this.container; const resolvedError = errorHandler?.getResolvedError?.(error); showToast?.({ type: 'ERROR', message: resolvedError?.message!, action: resolvedError?.action, biName: 'cairo-update-bulk-assign-tags-async-poll-fail', details: resolvedError?.requestId ? { requestId: resolvedError.requestId } : undefined, }); } refreshCollection() { this.collectionState.refreshAllPages(); } openAsyncJobProgressModal = () => { this.asyncJobProgressModalState.modal.open(); }; closeAsyncJobProgressModal = () => { this.asyncJobProgressModalState.modal.close(); if (this.isJobInProgress) { this.updatingTagsToast = this.container.showToast?.({ type: 'STANDARD', message: this.container.translate( 'cairo.tags.bulkItemUpdate.updatingTags.toast', ), action: { text: this.container.translate( 'cairo.tags.bulkItemUpdate.updatingTags.toast.CTA', ), onClick: async () => { this.openAsyncJobProgressModal(); }, removeToastOnClick: true, }, timeout: 'NONE', biName: 'cairo-updating-bulk-assign-tags-async', }) || null; } }; get isJobCompleted() { return this.job?.status === 'FINISHED'; } get isJobFailed() { return this.job?.status === 'FAILED'; } get isJobEnded() { return this.isJobCompleted || this.isJobFailed; } get isJobInProgress() { return ( this.job?.status === 'PROCESSING' || this.job?.status === 'INITIALIZED' ); } get canBulkAssignAsync() { return !!this.asyncBulkUpdateTags; } get updatedItemsPercentage() { if (!this.job) { return 0; } const alreadyUpdatedItemsCount = (this.job.counts?.successCount || 0) + (this.job.counts?.failCount || 0); const totalUpdatingItemsCount = this.job?.counts?.total || this.collectionState.bulkSelect.selectedCountOrTotal; return Math.floor( 100 * (alreadyUpdatedItemsCount / totalUpdatingItemsCount), ); } }