Created
January 7, 2026 10:01
-
-
Save bilalbhojani24/1114a00328e79da7d8d6db95e5f5a72c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // @ts-nocheck | |
| /* eslint-disable rippling-eslint/prefer-function-component */ | |
| import React from 'react'; | |
| import styled from '@emotion/styled'; | |
| import classNames from 'classnames'; | |
| import _ from 'lodash'; | |
| import { useTranslation } from '@rippling/lib-i18n'; | |
| import ActionCard from '@rippling/pebble/ActionCard'; | |
| import Animation from '@rippling/pebble/Animation'; | |
| import Atoms from '@rippling/pebble/Atoms'; | |
| import Button from '@rippling/pebble/Button'; | |
| import Spinner from '@rippling/pebble/Spinner'; | |
| import { ErrorBoundary } from '@rippling/pebble/WithErrorBoundary'; | |
| import { logWithSpinnerAntiPattern } from 'app/lib/log/patterns/withSpinnerAntiPatterns'; | |
| import Sentry from 'app/lib/log/sentry/sentry'; | |
| import useIsLoggedInAs from 'app/products/it/Identity/hooks/useIsLoggedInAs'; | |
| import { apiNavigateToPage as navigateToPage } from 'app/routes/routes.helpers'; | |
| import { handleGetDataErrors } from './getDataErrorHandler'; | |
| import WithSpinnerContainer from './withSpinnerContainer'; | |
| import './withSpinner.scss'; | |
| const OverlayPageContainer = styled.div` | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| height: 100%; | |
| width: 100%; | |
| background-color: ${({ theme }) => theme.colorSurfaceBright}; | |
| opacity: 0.5; | |
| z-index: 1000; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| `; | |
| function ErrorMessage({ error }) { | |
| const { ready, t } = useTranslation('core'); | |
| const isLoggedInAs = useIsLoggedInAs(); | |
| if (!ready) { | |
| return null; | |
| } | |
| if (isLoggedInAs && error?.status === 403) { | |
| return ( | |
| <ActionCard | |
| title={t('withSpinner.permissionIssue')} | |
| caption={t('withSpinner.noPermission')} | |
| animation={Animation.TYPES.EXCLAMATION_MARK} | |
| primaryAction={{ | |
| title: t('withSpinner.returnToDashboard'), | |
| onClick: () => navigateToPage('/dashboard'), | |
| }} | |
| /> | |
| ); | |
| } | |
| return ( | |
| <div className="withSpinner__apiError"> | |
| <Atoms.ApiError message={t('withSpinner.apiErrorMessage')} /> | |
| <div className="textAlign--center"> | |
| <Button onClick={() => navigateToPage('/dashboard')}> | |
| {t('withSpinner.returnToDashboard')} | |
| </Button> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| class RuntimeErrorBoundary extends ErrorBoundary { | |
| constructor(props) { | |
| super(props); | |
| this.state = { | |
| hasError: true, | |
| }; | |
| } | |
| } | |
| /** | |
| * @deprecated Use ErrorBoundary and Tanstack Query instead | |
| * https://rippling.atlassian.net/wiki/spaces/UP/pages/5310022126/Sunsetting+withSpinner | |
| */ | |
| export default (Component, Loader: React.FC | null = null, meta = {}) => { | |
| class ComponentWithSpinner extends Component { | |
| context: any; | |
| constructor(props, context) { | |
| super(props, context); | |
| this.state = { | |
| ...this.state, | |
| spinner: true, | |
| showSpinner: false, | |
| hasAPIFailed: false, | |
| pickedFormData: {}, // typeform input changes of only picked keys | |
| spinnerDelayElapsed: false, | |
| }; | |
| this.isUnMounted = false; | |
| this.isFetching = false; | |
| this._initalRenderCbCalled = false; | |
| this.antiPatternId = | |
| Date.now().toString() + Math.random().toString(36).substring(2); | |
| this.spinnerDelayTimeout = null; | |
| this.onFormChangeSetter = this.onFormChangeSetter.bind(this); | |
| } | |
| /** | |
| * Modified default setState function to absorb warning due to setState invoke after component unmount. | |
| * @param {object} nextState - object to apply on current state. | |
| * @param {function} afterStateUpdateCallback - the function will be called after state update | |
| */ | |
| setState(...args) { | |
| /** Passing call to the original function if the component is mounted */ | |
| if (!this.isUnMounted) { | |
| return super.setState(...args); | |
| } | |
| /** else, not doing anything because updating state | |
| * doesn't make sense at all as the component is already unmounted */ | |
| return undefined; | |
| } | |
| onFormChangeSetter(formInputChanges) { | |
| this.setState({ | |
| pickedFormData: formInputChanges, | |
| }); | |
| } | |
| async UNSAFE_componentWillMount() { | |
| if (!Component) return; | |
| if (_.isFunction(super.componentWillMount)) { | |
| await super.componentWillMount(); | |
| } | |
| this.isFetching = true; | |
| let hasAPIFailed = false; | |
| let dataFetcher = this.componentGetData || (() => {}); | |
| dataFetcher = dataFetcher.bind(this); | |
| try { | |
| await dataFetcher(); | |
| } catch (error) { | |
| hasAPIFailed = true; | |
| this.setState({ error }); | |
| const throwError = await handleGetDataErrors(error, meta); | |
| if (throwError) { | |
| throw error; | |
| } | |
| } finally { | |
| this.isFetching = false; | |
| if (!this.isUnMounted) { | |
| this.setState( | |
| { | |
| spinner: hasAPIFailed, | |
| hasAPIFailed, | |
| }, | |
| this.componentDidRender | |
| ); | |
| } | |
| } | |
| } | |
| componentDidRender = () => { | |
| let cb = super.componentDidInitialRender || _.noop; | |
| if (!this._initalRenderCbCalled) { | |
| cb = cb.bind(this); | |
| cb(); | |
| if (this.props.onInitialRender) { | |
| this.props.onInitialRender(); | |
| } | |
| } | |
| }; | |
| componentDidMount = () => { | |
| this.isUnMounted = false; | |
| if (_.isFunction(super.componentDidMount)) { | |
| super.componentDidMount(); | |
| } | |
| const componentName = | |
| Component.displayName || Component.name || 'UnknownComponent'; | |
| logWithSpinnerAntiPattern({ componentName, id: this.antiPatternId }); | |
| // Delay spinner rendering by 1000ms to prevent flashing for quick operations | |
| this.spinnerDelayTimeout = setTimeout(() => { | |
| if (!this.isUnMounted) { | |
| this.setState({ spinnerDelayElapsed: true }); | |
| } | |
| }, 1000); | |
| }; | |
| componentWillUnmount() { | |
| this.isUnMounted = true; | |
| if (this.spinnerDelayTimeout) { | |
| clearTimeout(this.spinnerDelayTimeout); | |
| this.spinnerDelayTimeout = null; | |
| } | |
| if (super.componentWillUnmount) { | |
| super.componentWillUnmount(); | |
| } | |
| } | |
| handleRenderCrash = e => { | |
| Sentry.captureRenderCrash(e, meta.executionNamespace); | |
| }; | |
| renderSpinnerAndApiError = () => { | |
| const { spinner, hasAPIFailed, error, spinnerDelayElapsed } = this.state; | |
| if (!spinner) { | |
| return null; | |
| } | |
| if (spinner && hasAPIFailed) { | |
| return <ErrorMessage error={error} />; | |
| } | |
| // Only show spinner after 1000ms delay to prevent flashing | |
| if (!spinnerDelayElapsed) { | |
| return null; | |
| } | |
| const spinnerText = super.getSpinnerText && super.getSpinnerText(); | |
| return <Spinner windowCentered title={spinnerText} />; | |
| }; | |
| render() { | |
| const { spinner, showSpinner, spinnerDelayElapsed } = this.state; | |
| const { isInOverlay } = this.props; | |
| const renderComponent = () => { | |
| try { | |
| return super.render(); | |
| } catch (e) { | |
| this.handleRenderCrash(e); | |
| return <RuntimeErrorBoundary />; | |
| } | |
| }; | |
| const getChildren = () => { | |
| if (Loader && spinner) { | |
| return <Loader />; | |
| } | |
| /** | |
| * Wrapper stylings are not required in overlay | |
| */ | |
| if (isInOverlay && !spinner && !showSpinner) { | |
| return renderComponent(); | |
| } | |
| return ( | |
| <WithSpinnerContainer isSpinning={spinner}> | |
| <div | |
| className={classNames( | |
| { 'swippable-area': !this.props.disableSwipeAnimation }, | |
| { 'hide-child-spinner': spinner } | |
| )} | |
| > | |
| {!spinner && renderComponent()} | |
| </div> | |
| {this.renderSpinnerAndApiError()} | |
| {showSpinner && spinnerDelayElapsed && ( | |
| <OverlayPageContainer> | |
| <Spinner /> | |
| </OverlayPageContainer> | |
| )} | |
| </WithSpinnerContainer> | |
| ); | |
| }; | |
| return ( | |
| <ErrorBoundary onError={this.handleRenderCrash}> | |
| {getChildren()} | |
| </ErrorBoundary> | |
| ); | |
| } | |
| } | |
| ComponentWithSpinner.isWithSpinner = true; | |
| ComponentWithSpinner.defaultProps = _.extend({}, Component.defaultProps, { | |
| disableSwipeAnimation: false, | |
| isWrappedWithSpinner: true, | |
| onInitialRender: _.noop, | |
| }); | |
| const I18N_SKIP_UNKNOWN = 'Unknown'; | |
| ComponentWithSpinner.displayName = `WithSpinner(${ | |
| Component.name || Component.displayName || I18N_SKIP_UNKNOWN | |
| })`; | |
| return ComponentWithSpinner as React.ComponentClass<any>; | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment