From b9605a3003e9b8fb88fcfbc0c8c447195813279c Mon Sep 17 00:00:00 2001 From: Aryan Ghugare Date: Wed, 15 Apr 2026 21:52:28 +0530 Subject: [PATCH] Refactor SnackBar duration handling and progress bar visibility logic (#19712) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes error snackbars disappearing too quickly by **disabling auto-dismiss for error variants by default**. Error snackbars now remain visible until the user closes them. Closes #19694 ## What changed - Error snackbars no longer default to a 6s timeout (they only auto-dismiss if an explicit `duration` is provided). - Non-error snackbars keep the existing default auto-dismiss behavior (6s). - Progress bar animation/visibility is tied to auto-dismiss (no progress bar when there’s no duration). **Files** - packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx ## How to test 1. Trigger a long error snackbar (example: spreadsheet/CSV import error with a long message). 2. Confirm the snackbar **stays visible** until clicking **Close**. 3. Trigger a success/info snackbar and confirm it **still auto-dismisses** after ~6s. ## Notes - Call sites that explicitly pass `options.duration` for error snackbars will continue to auto-dismiss (intentional). --------- Co-authored-by: Charles Bochet --- .../snack-bar-manager/components/SnackBar.tsx | 80 +++++++++---------- .../components/SnackBarProvider.tsx | 12 +-- .../__stories__/SnackBar.stories.tsx | 46 +++++++++++ .../utils/buildErrorAction.ts | 6 +- 4 files changed, 93 insertions(+), 51 deletions(-) diff --git a/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx b/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx index c74b2e9d71..fae8bdd6c8 100644 --- a/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx +++ b/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx @@ -9,9 +9,9 @@ import { useContext, useMemo, } from 'react'; -import { Link } from 'react-router-dom'; import { isDefined } from 'twenty-shared/utils'; import { + HorizontalSeparator, IconAlertTriangle, IconInfoCircle, IconSquareRoundedCheck, @@ -19,6 +19,7 @@ import { } from 'twenty-ui/display'; import { ProgressBar, useProgressAnimation } from 'twenty-ui/feedback'; import { LightButton, LightIconButton } from 'twenty-ui/input'; +import { UndecoratedLink } from 'twenty-ui/navigation'; import { MOBILE_VIEWPORT, ThemeContext, @@ -39,9 +40,9 @@ export type SnackBarProps = Pick, 'id'> & { duration?: number; icon?: ReactNode; message: string; - actionText?: string; - actionOnClick?: () => void; - actionTo?: string; + buttonLabel?: string; + buttonOnClick?: () => void; + buttonTo?: string; detailedMessage?: string; onCancel?: () => void; onClose?: () => void; @@ -56,9 +57,9 @@ const StyledContainer = styled.div` border-radius: ${themeCssVariables.border.radius.md}; box-shadow: ${themeCssVariables.boxShadow.strong}; box-sizing: border-box; - cursor: pointer; margin-top: ${themeCssVariables.spacing[2]}; - padding: ${themeCssVariables.spacing[2]}; + padding: ${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[2]} + ${themeCssVariables.spacing[1]}; position: relative; width: 296px; @@ -70,12 +71,15 @@ const StyledContainer = styled.div` const StyledProgressBarContainer = styled.div` bottom: 0; - height: auto; left: 0; pointer-events: none; position: absolute; right: 0; top: 0; + + & > [role='progressbar'] { + height: 100%; + } `; const StyledHeader = styled.div` @@ -112,24 +116,15 @@ const StyledDescription = styled.div` width: 200px; `; -const StyledLinkContainer = styled.div` - > a { - color: ${themeCssVariables.font.color.tertiary}; - display: block; - font-size: ${themeCssVariables.font.size.sm}; - max-width: 200px; - overflow: hidden; - padding-left: ${themeCssVariables.spacing[6]}; - text-overflow: ellipsis; - white-space: nowrap; - &:hover { - color: ${themeCssVariables.font.color.secondary}; - } - } +const StyledBottomActionContainer = styled.div` + margin-top: ${themeCssVariables.spacing[2]}; `; -const StyledActionButton = styled.div` - padding-left: ${themeCssVariables.spacing[6]}; +const StyledBottomAction = styled.div` + align-items: center; + display: flex; + justify-content: flex-end; + padding-top: ${themeCssVariables.spacing[1]}; `; const defaultAriaLabelByVariant: Record< @@ -151,9 +146,9 @@ export const SnackBar = ({ id, message, detailedMessage, - actionText, - actionOnClick, - actionTo, + buttonLabel, + buttonOnClick, + buttonTo, onCancel, onClose, role = 'status', @@ -205,15 +200,11 @@ export const SnackBar = ({ }, [iconComponent, variant, i18n, theme.icon.size.md, theme.snackBar]); const handleMouseEnter = () => { - if (progressAnimation?.state === 'running') { - progressAnimation.pause(); - } + progressAnimation?.pause(); }; const handleMouseLeave = () => { - if (progressAnimation?.state === 'paused') { - progressAnimation.play(); - } + progressAnimation?.play(); }; const sanitizedMessage = sanitizeMessageToRenderInSnackbar(message); @@ -251,16 +242,21 @@ export const SnackBar = ({ {isDefined(sanitizedDetailedMessage) && ( {sanitizedDetailedMessage} )} - {actionText && actionTo && ( - - {actionText} - - )} - {actionText && actionOnClick && !actionTo && ( - - - - )} + {isDefined(buttonLabel) && + (isDefined(buttonOnClick) || isDefined(buttonTo)) && ( + + + + {isDefined(buttonTo) ? ( + + + + ) : ( + + )} + + + )} ); }; diff --git a/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/components/SnackBarProvider.tsx b/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/components/SnackBarProvider.tsx index abd9644d01..d3211d7da0 100644 --- a/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/components/SnackBarProvider.tsx +++ b/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/components/SnackBarProvider.tsx @@ -58,9 +58,9 @@ export const SnackBarProvider = ({ children }: React.PropsWithChildren) => { message, detailedMessage, variant, - actionText, - actionOnClick, - actionTo, + buttonLabel, + buttonOnClick, + buttonTo, }) => ( { message, detailedMessage, variant, - actionText, - actionOnClick, - actionTo, + buttonLabel, + buttonOnClick, + buttonTo, }} onClose={() => handleSnackBarClose(id)} /> diff --git a/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/components/__stories__/SnackBar.stories.tsx b/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/components/__stories__/SnackBar.stories.tsx index fcbed02210..faefb61249 100644 --- a/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/components/__stories__/SnackBar.stories.tsx +++ b/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/components/__stories__/SnackBar.stories.tsx @@ -41,6 +41,33 @@ export const Default: Story = { }, }; +export const WithBottomButton: Story = { + args: { + variant: SnackBarVariant.Error, + message: 'An error has occurred', + detailedMessage: 'Error during useFindManyRecord...', + buttonLabel: 'Open Record', + buttonOnClick: fn(), + }, + decorators: [ComponentDecorator], + parameters: { + chromatic: { disableSnapshot: true }, + }, +}; + +export const SuccessWithButton: Story = { + args: { + variant: SnackBarVariant.Success, + message: 'Record created successfully', + buttonLabel: 'View Record', + buttonOnClick: fn(), + }, + decorators: [ComponentDecorator], + parameters: { + chromatic: { disableSnapshot: true }, + }, +}; + export const Catalog: CatalogStory = { args: { onCancel: fn(), @@ -63,3 +90,22 @@ export const Catalog: CatalogStory = { }, }, }; + +export const CatalogWithButton: CatalogStory = { + args: { + buttonLabel: 'Open Record', + buttonOnClick: fn(), + }, + decorators: [CatalogDecorator], + parameters: { + catalog: { + dimensions: [ + { + name: 'variants', + values: Object.values(SnackBarVariant), + props: (variant: SnackBarVariant) => ({ variant }), + }, + ], + }, + }, +}; diff --git a/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/utils/buildErrorAction.ts b/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/utils/buildErrorAction.ts index 048f42752c..df3fb2c3e9 100644 --- a/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/utils/buildErrorAction.ts +++ b/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/utils/buildErrorAction.ts @@ -7,7 +7,7 @@ import { type SnackBarOptions } from '@/ui/feedback/snack-bar-manager/states/sna export const buildErrorAction = ( apolloError?: ErrorLike, -): Pick | null => { +): Pick | null => { if (!apolloError) { return null; } @@ -16,8 +16,8 @@ export const buildErrorAction = ( if (isDefined(conflictingRecord)) { return { - actionText: t`View existing record`, - actionTo: getAppPath(AppPath.RecordShowPage, { + buttonLabel: t`View existing record`, + buttonTo: getAppPath(AppPath.RecordShowPage, { objectNameSingular: conflictingRecord.conflictingObjectNameSingular, objectRecordId: conflictingRecord.conflictingRecordId, }),