import { FormPageState, ReportBI, WixPatternsContainer } from '@wix/bex-core'; import { action, computed, makeObservable, observable } from 'mobx'; import { FieldValues, UseFormReturn } from 'react-hook-form'; import { SettingsPageStateBIReporter } from './SettingsPageStateBIReporter'; import { EventEmitter, TypedEmitter } from '@wix/bex-core/events'; export interface SettingsPageStateParams< T, V extends FieldValues = FieldValues, > { /** * A function that fetches the settings data. * @returns a promise that resolves to an object containing the settings. */ fetch: () => Promise<{ settings: T | undefined }>; /** * A function to handle the save action. */ onSave: () => Promise; /** * A function to handle the cancel action. */ onCancel: () => Promise; /** * An optional object with an optional message, or it can directly get a string message. */ saveSuccessToast?: { message?: string } | string; /** * An optional function to display an error toast message when a save operation fails. * @returns An object with a `message` and an `action` object. Alternatively, it can return an object with an optional `message` and a `tryAgain` flag, or a string message. * The previous return types are still supported but are now deprecated. */ saveErrorToast?: ( err: unknown, params: { retry: () => void }, ) => | { message?: string; /** * @deprecated use `action` instead */ tryAgain?: boolean; action?: { text: string; onClick: () => void }; } | string; form: UseFormReturn; /** * An object representing the container, possibly for dependency injection or component management. */ container: WixPatternsContainer; } export interface SettingsPageStateInterface {} export class SettingsPageState implements SettingsPageStateInterface { readonly fetch: SettingsPageStateParams['fetch']; readonly _onSave: SettingsPageStateParams['onSave']; readonly _onCancel: SettingsPageStateParams['onCancel']; readonly saveSuccessToast?: SettingsPageStateParams['saveSuccessToast']; readonly saveErrorToast?: SettingsPageStateParams['saveErrorToast']; _settings: T | null = null; readonly container: WixPatternsContainer; readonly reportBI: ReportBI; readonly bi: SettingsPageStateBIReporter; readonly formPage: FormPageState; readonly events = new EventEmitter() as TypedEmitter<{ save: () => void; saveError: (err: unknown) => void; }>; constructor(params: SettingsPageStateParams) { this.fetch = params.fetch; this._onSave = params.onSave; this._onCancel = params.onCancel; this.saveSuccessToast = params.saveSuccessToast; this.saveErrorToast = params.saveErrorToast; this.formPage = new FormPageState({ form: params.form }); this.container = params.container; this.reportBI = this.container.createBILogger({ componentType: 'Settings page', pageType: 'Settings', route: window.location.pathname, }); this.bi = new SettingsPageStateBIReporter({ reportBI: this.reportBI, settingsPageState: this, }); this.bi.init(); makeObservable(this, { handleSubmit: action, _settings: observable.ref, setSettings: action, isDirty: computed, }); } init() { const { formPage: { initTask }, container, } = this; const { onBeforeUnload } = container; const onBeforeUnloadSubscription = onBeforeUnload?.( action((event: Event) => { if (!this.isDirty) { return; } event.preventDefault(); this.bi.reportOnBeforeUnload(); }), ); initTask.runOnce(async () => { const [data] = await Promise.all([ this.fetch(), container.initTask.status.promise, ]); this.setSettings(data.settings ?? null); this.bi.appLoaded?.(); }); return () => { onBeforeUnloadSubscription?.remove(); }; } retryFetch() { this.formPage.retryFetch(); } setSettings(newSettings: T | null) { this._settings = newSettings; } get settings(): T | null { return this._settings; } get isFetching() { return this.formPage.isFetching; } get form() { return this.formPage.form; } get isDirty() { return this.formPage.isDirty; } onCancel() { this.bi.reportButtonClick({ ctaName: 'Cancel' }); this._onCancel(); } _showErrorToast(err: any, e?: React.BaseSyntheticEvent) { const errorToast = this.saveErrorToast?.(err, { retry: () => this.handleSubmit(e), }); const message = typeof errorToast === 'string' ? errorToast : errorToast?.message; const showTryAgain = typeof errorToast === 'string' ? true : errorToast?.tryAgain !== false; const action = typeof errorToast === 'string' ? undefined : errorToast?.action; this.container.showToast?.({ type: 'ERROR', biName: 'cairo-settings-page-save-error-toast', message: message ?? this.container.translate( err?.code === 'ERR_NETWORK' ? 'cairo.entityPage.saveError-offline.toast.description' : 'cairo.entityPage.saveError-technical.toast.description', ), action: action || showTryAgain ? { uiType: 'LINK', text: action?.text ?? this.container.translate('cairo.toast.retry'), onClick: () => { if (action?.onClick) { action.onClick(); } else { this.bi.reportSaveTryAgain(); this.handleSubmit(e); } }, removeToastOnClick: true, } : undefined, }); } _showSuccessToast() { const message = typeof this.saveSuccessToast === 'string' ? this.saveSuccessToast : this.saveSuccessToast?.message; this.container.showToast?.({ type: 'SUCCESS', biName: 'cairo-settings-page-save-success-toast', message: message ?? this.container.translate('cairo.entityPage.success.toast'), }); } async handleSubmit(e?: React.BaseSyntheticEvent) { if (this.formPage.submitTask.status.isLoading) { return; } this.events.emit('save'); const { form, formPage: { submitTask }, bi, } = this; submitTask.run(async () => { await form.handleSubmit( async () => { bi.reportButtonClick({ ctaName: 'Save', isValid: true }); try { if (!this.isFetching) { await this._onSave(); form.reset(form.getValues()); } this._showSuccessToast(); } catch (err) { this._showErrorToast(err, e); this.container.errorMonitor.captureException(err); this.events.emit('saveError', err); form.setError('root.serverError', {}); // https://react-hook-form.com/docs/useform/seterror } }, () => { bi.reportButtonClick({ ctaName: 'Save', isValid: false }); }, // can be used to report validation errors )(e); }); await this.formPage.submitTask.status.promise; } get initTask() { return this.formPage.initTask; } get submitTask() { return this.formPage.submitTask; } }