From e25ddf6cfad960040e84adb68bbae2f97d424b99 Mon Sep 17 00:00:00 2001 From: edmonday Date: Mon, 2 Mar 2026 04:04:07 +0000 Subject: [PATCH 01/33] refactor: extract ScreenWrapper for shared screen layout Create ScreenWrapper component with responsive title/subtitle and migrate all 6 customization screens to use it. Fixes MediaScreen using wrong Typography variants and broken color prop. Refs: NES-1364 Co-Authored-By: Claude Opus 4.6 --- .../Screens/DoneScreen/DoneScreen.tsx | 48 +---- .../Screens/LanguageScreen/LanguageScreen.tsx | 188 ++++++++---------- .../Screens/LinksScreen/LinksScreen.tsx | 57 ++---- .../Screens/MediaScreen/MediaScreen.tsx | 50 +++-- .../Screens/ScreenWrapper/ScreenWrapper.tsx | 65 ++++++ .../Screens/ScreenWrapper/index.ts | 1 + .../Screens/SocialScreen/SocialScreen.tsx | 80 +++----- .../Screens/TextScreen/TextScreen.tsx | 160 ++++++--------- 8 files changed, 290 insertions(+), 359 deletions(-) create mode 100644 apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/ScreenWrapper.tsx create mode 100644 apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/index.ts diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx index 9fc51812375..9c94fbfbcd3 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx @@ -16,6 +16,7 @@ import Play3Icon from '@core/shared/ui/icons/Play3' import { ShareItem } from '../../../../Editor/Toolbar/Items/ShareItem' import { CustomizationScreen } from '../../../utils/getCustomizeFlowConfig' +import { ScreenWrapper } from '../ScreenWrapper' interface DoneScreenProps { handleScreenNavigation?: (screen: CustomizationScreen) => void @@ -40,48 +41,12 @@ export function DoneScreen({ return ( - - {t('Ready to Share!')} - - - {t('Ready to Share!')} - - - {t('Share your unique link on any platform.')} - - - {t('Share your unique link on any platform.')} - - - {steps.length > 0 && ( + {steps.length > 0 && ( )} @@ -131,6 +96,7 @@ export function DoneScreen({ {t('Go To Projects Dashboard')} + ) } diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx index a1fe586c64c..8eead9c3c37 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx @@ -24,6 +24,7 @@ import { useGetParentTemplateJourneyLanguages } from '../../../../../libs/useGet import { CustomizationScreen } from '../../../utils/getCustomizeFlowConfig' import { CustomizeFlowNextButton } from '../../CustomizeFlowNextButton' import { CardsPreview } from '../LinksScreen/CardsPreview' +import { ScreenWrapper } from '../ScreenWrapper' import { JourneyCustomizeTeamSelect } from './JourneyCustomizeTeamSelect' @@ -168,116 +169,97 @@ export function LanguageScreen({ return ( - - - {t("Let's Get Started!")} - - - {t('Get Started')} - - - {t('A few quick edits and your template will be ready to share.')} - + - {t("A few quick edits and it's ready to share!")} + {`'${journey?.title ?? ''}'`} - - - {`'${journey?.title ?? ''}'`} - - - {steps.length > 0 && } + {steps.length > 0 && } - - {({ handleSubmit, setFieldValue, values }) => ( -
- - - - {t('Select a language')} - - - {t('Select a language')} - - ({ - id: language?.id, - name: language?.name, - slug: language?.slug - }))} - onChange={(value) => setFieldValue('languageSelect', value)} - /> - {isSignedIn && ( - <> - - {t('Select a team')} - + + {({ handleSubmit, setFieldValue, values }) => ( + + + + + {t('Select a language')} + + + {t('Select a language')} + + ({ + id: language?.id, + name: language?.name, + slug: language?.slug + }))} + onChange={(value) => + setFieldValue('languageSelect', value) + } + /> + {isSignedIn && ( + <> + + {t('Select a team')} + - - {t('Select a team')} - - - - )} - handleSubmit()} - disabled={ - (templateCustomizationGuestFlow && !isSignedIn) || loading - } - ariaLabel={t('Next')} - /> - - - - )} - + + {t('Select a team')} + + + + )} + handleSubmit()} + disabled={ + (templateCustomizationGuestFlow && !isSignedIn) || loading + } + ariaLabel={t('Next')} + /> + +
+ + )} +
+
) } diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/LinksScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/LinksScreen.tsx index ff41feeb824..f128075d367 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/LinksScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/LinksScreen.tsx @@ -1,6 +1,5 @@ import { useMutation } from '@apollo/client' import Stack from '@mui/material/Stack' -import Typography from '@mui/material/Typography' import { Formik, FormikHelpers, FormikProvider } from 'formik' import { useTranslation } from 'next-i18next' import { ReactElement, useMemo } from 'react' @@ -28,6 +27,7 @@ import { countries } from '../../../../Editor/Slider/Settings/CanvasDetails/Prop import { CustomizationScreen } from '../../../utils/getCustomizeFlowConfig' import { getJourneyLinks } from '../../../utils/getJourneyLinks' import { CustomizeFlowNextButton } from '../../CustomizeFlowNextButton' +import { ScreenWrapper } from '../ScreenWrapper' import { CardsPreview } from './CardsPreview' import { LinksForm } from './LinksForm' @@ -197,49 +197,18 @@ export function LinksScreen({ width: '100%' }} > - - - {t('Links')} - - - {t('Links')} - - - {t( - 'This content contains buttons linking to external sites. Check them and update the links below.' - )} - - - {t( - 'Buttons here point to external sites. Check and update the links.' - )} - - - + + + >((acc, link) => { diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/MediaScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/MediaScreen.tsx index 2ba8f54673e..6998317b976 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/MediaScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/MediaScreen.tsx @@ -1,6 +1,4 @@ -import Box from '@mui/material/Box' import Stack from '@mui/material/Stack' -import Typography from '@mui/material/Typography' import { useTranslation } from 'next-i18next' import { ReactElement, useEffect, useState } from 'react' @@ -12,6 +10,7 @@ import { GetJourney_journey_blocks_StepBlock as StepBlock } from '@core/journeys import { getJourneyMedia } from '../../../utils/getJourneyMedia' import { CustomizeFlowNextButton } from '../../CustomizeFlowNextButton' import { useTemplateVideoUpload } from '../../TemplateVideoUploadProvider' +import { ScreenWrapper } from '../ScreenWrapper' import { CardsSection, @@ -83,30 +82,29 @@ export function MediaScreen({ handleNext }: MediaScreenProps): ReactElement { overflow: { xs: 'visible', sm: 'hidden' } }} > - - - {t('Media')} - - - {t('Personalize and manage your media assets')} - - - {showLogo && } - - {showImages && ( - - )} - {showVideos && } - handleNext()} - ariaLabel={t('Next')} - loading={hasActiveUploads} - /> + handleNext()} + ariaLabel={t('Next')} + loading={hasActiveUploads} + /> + } + > + {showLogo && } + + {showImages && ( + + )} + {showVideos && } + ) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/ScreenWrapper.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/ScreenWrapper.tsx new file mode 100644 index 00000000000..5aab822e4bd --- /dev/null +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/ScreenWrapper.tsx @@ -0,0 +1,65 @@ +import Stack from '@mui/material/Stack' +import { SxProps, Theme } from '@mui/material/styles' +import Typography from '@mui/material/Typography' +import { ReactElement, ReactNode } from 'react' + +interface ScreenWrapperProps { + title: string + mobileTitle?: string + subtitle: string + mobileSubtitle?: string + headerSx?: SxProps + footer?: ReactNode + children: ReactNode +} + +export function ScreenWrapper({ + title, + mobileTitle, + subtitle, + mobileSubtitle, + headerSx, + footer, + children +}: ScreenWrapperProps): ReactElement { + return ( + <> + + + {title} + + + {mobileTitle ?? title} + + + {subtitle} + + + {mobileSubtitle ?? subtitle} + + + {children} + {footer} + + ) +} diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/index.ts b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/index.ts new file mode 100644 index 00000000000..dfa2c06cdb7 --- /dev/null +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/index.ts @@ -0,0 +1 @@ +export { ScreenWrapper } from './ScreenWrapper' diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/SocialScreen/SocialScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/SocialScreen/SocialScreen.tsx index d054a95f5bb..ddf431b6261 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/SocialScreen/SocialScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/SocialScreen/SocialScreen.tsx @@ -1,5 +1,4 @@ import Stack from '@mui/material/Stack' -import Typography from '@mui/material/Typography' import { useTranslation } from 'next-i18next' import { ReactElement } from 'react' @@ -7,6 +6,7 @@ import { DescriptionEdit } from '../../../../Editor/Slider/Settings/SocialDetail import { TitleEdit } from '../../../../Editor/Slider/Settings/SocialDetails/TitleEdit' import { CustomizationScreen } from '../../../utils/getCustomizeFlowConfig' import { CustomizeFlowNextButton } from '../../CustomizeFlowNextButton' +import { ScreenWrapper } from '../ScreenWrapper' import { SocialScreenSocialImage } from './SocialScreenSocialImage' @@ -28,56 +28,36 @@ export function SocialScreen({ px: { xs: 5, sm: 20 } }} > - handleNext()} + ariaLabel={t('Done')} + /> + } > - {t('Final Details')} - - - {t('Final Details')} - - - {t('Customize how your invite appears when shared on social media.')} - - - {t('This is how your content will appear when shared on social media.')} - - - - - - - handleNext()} - ariaLabel={t('Done')} - /> + + + + + + ) } diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx index 0c70d8d5e8b..a9256acf9da 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx @@ -1,7 +1,6 @@ import { gql, useMutation } from '@apollo/client' import Box from '@mui/material/Box' import Stack from '@mui/material/Stack' -import Typography from '@mui/material/Typography' import { useTranslation } from 'next-i18next' import { ReactElement, useCallback, useEffect, useState } from 'react' @@ -11,6 +10,7 @@ import { GetJourney_journey_journeyCustomizationFields as JourneyCustomizationFi import { JourneyCustomizationFieldUpdate } from '../../../../../../__generated__/JourneyCustomizationFieldUpdate' import { CustomizationScreen } from '../../../utils/getCustomizeFlowConfig' import { CustomizeFlowNextButton } from '../../CustomizeFlowNextButton' +import { ScreenWrapper } from '../ScreenWrapper' export const JOURNEY_CUSTOMIZATION_FIELD_UPDATE = gql` mutation JourneyCustomizationFieldUpdate( @@ -202,101 +202,71 @@ export function TextScreen({ width: '100%' }} > - - - {t('Text')} - - - {t('Text')} - - - {t( - "Fill out the blue fields and we'll customize the content with your information." - )} - - - {t('Fill in the blue fields to customize the content.')} - - - - - {renderEditableText( - journey?.journeyCustomizationDescription ?? '', - replacementItems, - handleValueChange - )} + + } + > + + + {renderEditableText( + journey?.journeyCustomizationDescription ?? '', + replacementItems, + handleValueChange + )} + + - - - + ) } From e381512732d0649ec9bda3aa59bf972db2b8ad70 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 04:08:47 +0000 Subject: [PATCH 02/33] fix: lint issues --- .../Screens/DoneScreen/DoneScreen.tsx | 92 ++++++++++--------- .../Screens/LanguageScreen/LanguageScreen.tsx | 4 +- .../Screens/MediaScreen/MediaScreen.tsx | 5 +- .../Screens/TextScreen/TextScreen.tsx | 4 +- 4 files changed, 54 insertions(+), 51 deletions(-) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx index 9c94fbfbcd3..dfed7dbbb01 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx @@ -47,55 +47,59 @@ export function DoneScreen({ headerSx={{ pb: { xs: 4, sm: 6 } }} > {steps.length > 0 && ( - - )} + + )} - - - + + - {t('Preview')} - - - + + + + + {t('Go To Projects Dashboard')} + + ) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx index 8eead9c3c37..230a294f4b3 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx @@ -222,9 +222,7 @@ export function LanguageScreen({ name: language?.name, slug: language?.slug }))} - onChange={(value) => - setFieldValue('languageSelect', value) - } + onChange={(value) => setFieldValue('languageSelect', value)} /> {isSignedIn && ( <> diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/MediaScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/MediaScreen.tsx index 6998317b976..cfe7e36cc38 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/MediaScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/MediaScreen.tsx @@ -101,7 +101,10 @@ export function MediaScreen({ handleNext }: MediaScreenProps): ReactElement { handleStepClick={handleStepClick} /> {showImages && ( - + )} {showVideos && } diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx index a9256acf9da..22bdbbe7e3f 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx @@ -207,9 +207,7 @@ export function TextScreen({ subtitle={t( "Fill out the blue fields and we'll customize the content with your information." )} - mobileSubtitle={t( - 'Fill in the blue fields to customize the content.' - )} + mobileSubtitle={t('Fill in the blue fields to customize the content.')} headerSx={{ pb: 4 }} footer={ Date: Mon, 2 Mar 2026 22:16:23 +0000 Subject: [PATCH 03/33] refactor: remove unused headerSx prop from multiple screens Eliminate the headerSx prop from DoneScreen, LanguageScreen, LinksScreen, and ScreenWrapper components to streamline the code. Update TextScreen to maintain consistent mobileSubtitle formatting. --- .../MultiStepForm/Screens/DoneScreen/DoneScreen.tsx | 1 - .../Screens/LanguageScreen/LanguageScreen.tsx | 1 - .../MultiStepForm/Screens/LinksScreen/LinksScreen.tsx | 1 - .../Screens/ScreenWrapper/ScreenWrapper.tsx | 11 ++++------- .../MultiStepForm/Screens/TextScreen/TextScreen.tsx | 5 +++-- 5 files changed, 7 insertions(+), 12 deletions(-) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx index dfed7dbbb01..6977128cbe9 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx @@ -44,7 +44,6 @@ export function DoneScreen({ {steps.length > 0 && ( diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx index 230a294f4b3..59a70befb7d 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx @@ -176,7 +176,6 @@ export function LanguageScreen({ 'A few quick edits and your template will be ready to share.' )} mobileSubtitle={t("A few quick edits and it's ready to share!")} - headerSx={{ pb: { xs: 6, sm: 10 } }} > diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/ScreenWrapper.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/ScreenWrapper.tsx index 5aab822e4bd..72660e215db 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/ScreenWrapper.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/ScreenWrapper.tsx @@ -1,5 +1,4 @@ import Stack from '@mui/material/Stack' -import { SxProps, Theme } from '@mui/material/styles' import Typography from '@mui/material/Typography' import { ReactElement, ReactNode } from 'react' @@ -8,7 +7,6 @@ interface ScreenWrapperProps { mobileTitle?: string subtitle: string mobileSubtitle?: string - headerSx?: SxProps footer?: ReactNode children: ReactNode } @@ -18,15 +16,14 @@ export function ScreenWrapper({ mobileTitle, subtitle, mobileSubtitle, - headerSx, footer, children }: ScreenWrapperProps): ReactElement { return ( - <> - + + {children} {footer} - + ) } diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx index 22bdbbe7e3f..a00a99148ce 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx @@ -207,8 +207,9 @@ export function TextScreen({ subtitle={t( "Fill out the blue fields and we'll customize the content with your information." )} - mobileSubtitle={t('Fill in the blue fields to customize the content.')} - headerSx={{ pb: 4 }} + mobileSubtitle={t( + 'Fill in the blue fields to customize the content.' + )} footer={ Date: Mon, 2 Mar 2026 22:22:52 +0000 Subject: [PATCH 04/33] fix: lint issues --- .../MultiStepForm/Screens/TextScreen/TextScreen.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx index a00a99148ce..70a3d93e58d 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx @@ -207,9 +207,7 @@ export function TextScreen({ subtitle={t( "Fill out the blue fields and we'll customize the content with your information." )} - mobileSubtitle={t( - 'Fill in the blue fields to customize the content.' - )} + mobileSubtitle={t('Fill in the blue fields to customize the content.')} footer={ Date: Mon, 2 Mar 2026 22:48:37 +0000 Subject: [PATCH 05/33] feat: add email notification toggle to DoneScreen Refs: NES-1295 Co-Authored-By: Claude Opus 4.6 --- .../NotificationSwitch.spec.tsx | 15 +++++ .../NotificationSwitch/NotificationSwitch.tsx | 22 +++++--- .../Screens/DoneScreen/DoneScreen.spec.tsx | 56 +++++++++++++++++++ .../Screens/DoneScreen/DoneScreen.tsx | 16 ++++++ 4 files changed, 101 insertions(+), 8 deletions(-) diff --git a/apps/journeys-admin/src/components/AccessDialog/NotificationSwitch/NotificationSwitch.spec.tsx b/apps/journeys-admin/src/components/AccessDialog/NotificationSwitch/NotificationSwitch.spec.tsx index 9baa4c9a23f..7cbb5130c32 100644 --- a/apps/journeys-admin/src/components/AccessDialog/NotificationSwitch/NotificationSwitch.spec.tsx +++ b/apps/journeys-admin/src/components/AccessDialog/NotificationSwitch/NotificationSwitch.spec.tsx @@ -55,6 +55,21 @@ describe('NotificationSwitch', () => { ) }) + it('renders without Tooltip when name is not provided', async () => { + render( + + + + + + ) + + fireEvent.mouseOver(screen.getByRole('checkbox')) + await waitFor(() => + expect(screen.queryByRole('tooltip')).not.toBeInTheDocument() + ) + }) + it('does not update event email notifications when disabled', async () => { render( diff --git a/apps/journeys-admin/src/components/AccessDialog/NotificationSwitch/NotificationSwitch.tsx b/apps/journeys-admin/src/components/AccessDialog/NotificationSwitch/NotificationSwitch.tsx index 2196bb9fd4f..103ed3f66fb 100644 --- a/apps/journeys-admin/src/components/AccessDialog/NotificationSwitch/NotificationSwitch.tsx +++ b/apps/journeys-admin/src/components/AccessDialog/NotificationSwitch/NotificationSwitch.tsx @@ -60,16 +60,22 @@ export function NotificationSwitch({ } } + const switchElement = ( + + + + ) + + if (name == null) return switchElement + return ( - - - + {switchElement} ) } diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.spec.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.spec.tsx index d002acd995b..ca343623a4a 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.spec.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.spec.tsx @@ -12,6 +12,7 @@ import { } from '../../../../../../__generated__/globalTypes' import { GET_CUSTOM_DOMAINS } from '../../../../../libs/useCustomDomainsQuery/useCustomDomainsQuery' import { GET_JOURNEY_FOR_SHARING } from '../../../../../libs/useJourneyForShareLazyQuery/useJourneyForShareLazyQuery' +import { useJourneyNotifcationUpdateMock } from '../../../../../libs/useJourneyNotificationUpdate/useJourneyNotificationUpdate.mock' import { DoneScreen } from './DoneScreen' @@ -201,6 +202,61 @@ describe('DoneScreen', () => { expect(push).toHaveBeenCalledWith('/') }) + it('renders notification section heading and label', () => { + render( + + + + + + + + ) + + expect( + screen.getByText('Choose where responses go:') + ).toBeInTheDocument() + expect(screen.getByText('Send to my email')).toBeInTheDocument() + }) + + it('renders notification switch unchecked by default', () => { + render( + + + + + + + + ) + + const checkbox = screen.getByRole('checkbox') + expect(checkbox).not.toBeChecked() + }) + + it('fires notification update mutation when switch is toggled', async () => { + const result = jest + .fn() + .mockReturnValueOnce(useJourneyNotifcationUpdateMock.result) + render( + + + + + + + + ) + + fireEvent.click(screen.getByRole('checkbox')) + await waitFor(() => expect(result).toHaveBeenCalled()) + }) + it('opens the share dialog when clicked', async () => { const journeyWithTeam = { ...journey, diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx index 6977128cbe9..39d433daf2e 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx @@ -14,6 +14,7 @@ import { GetJourney_journey_blocks_StepBlock as StepBlock } from '@core/journeys import ArrowRightContained1Icon from '@core/shared/ui/icons/ArrowRightContained1' import Play3Icon from '@core/shared/ui/icons/Play3' +import { NotificationSwitch } from '../../../../AccessDialog/NotificationSwitch' import { ShareItem } from '../../../../Editor/Toolbar/Items/ShareItem' import { CustomizationScreen } from '../../../utils/getCustomizeFlowConfig' import { ScreenWrapper } from '../ScreenWrapper' @@ -99,6 +100,21 @@ export function DoneScreen({ {t('Go To Projects Dashboard')} + + + {t('Choose where responses go:')} + + + + {t('Send to my email')} + + + + ) From 7ef0dd656feea06731c58f7ddc3fbc9359287b86 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 22:57:26 +0000 Subject: [PATCH 06/33] fix: lint issues --- .../MultiStepForm/Screens/DoneScreen/DoneScreen.spec.tsx | 4 +--- .../MultiStepForm/Screens/DoneScreen/DoneScreen.tsx | 4 +--- .../MultiStepForm/Screens/TextScreen/TextScreen.tsx | 4 +--- libs/locales/en/apps-journeys-admin.json | 2 ++ 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.spec.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.spec.tsx index ca343623a4a..46255259d71 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.spec.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.spec.tsx @@ -213,9 +213,7 @@ describe('DoneScreen', () => { ) - expect( - screen.getByText('Choose where responses go:') - ).toBeInTheDocument() + expect(screen.getByText('Choose where responses go:')).toBeInTheDocument() expect(screen.getByText('Send to my email')).toBeInTheDocument() }) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx index 39d433daf2e..3af35982aa4 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx @@ -109,9 +109,7 @@ export function DoneScreen({ justifyContent="space-between" alignItems="center" > - - {t('Send to my email')} - + {t('Send to my email')} diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx index a00a99148ce..70a3d93e58d 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx @@ -207,9 +207,7 @@ export function TextScreen({ subtitle={t( "Fill out the blue fields and we'll customize the content with your information." )} - mobileSubtitle={t( - 'Fill in the blue fields to customize the content.' - )} + mobileSubtitle={t('Fill in the blue fields to customize the content.')} footer={ Date: Tue, 3 Mar 2026 01:45:22 +0000 Subject: [PATCH 07/33] feat: add Google Sheets sync row to DoneScreen Refs: NES-1297 Co-Authored-By: Claude Opus 4.6 --- .../Screens/DoneScreen/DoneScreen.spec.tsx | 108 ++++++++++++++++-- .../Screens/DoneScreen/DoneScreen.tsx | 73 +++++++++++- 2 files changed, 170 insertions(+), 11 deletions(-) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.spec.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.spec.tsx index ca343623a4a..a09becbd1af 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.spec.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.spec.tsx @@ -1,4 +1,4 @@ -import { MockedProvider } from '@apollo/client/testing' +import { MockedProvider, MockedResponse } from '@apollo/client/testing' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { NextRouter, useRouter } from 'next/router' import { SnackbarProvider } from 'notistack' @@ -14,7 +14,7 @@ import { GET_CUSTOM_DOMAINS } from '../../../../../libs/useCustomDomainsQuery/us import { GET_JOURNEY_FOR_SHARING } from '../../../../../libs/useJourneyForShareLazyQuery/useJourneyForShareLazyQuery' import { useJourneyNotifcationUpdateMock } from '../../../../../libs/useJourneyNotificationUpdate/useJourneyNotificationUpdate.mock' -import { DoneScreen } from './DoneScreen' +import { DoneScreen, GET_GOOGLE_SHEETS_SYNCS_FOR_DONE_SCREEN } from './DoneScreen' jest.mock('next/router', () => ({ __esModule: true, @@ -52,6 +52,30 @@ const getCustomDomainsMock = { } } +const googleSheetsSyncsNoActiveMock: MockedResponse = { + request: { + query: GET_GOOGLE_SHEETS_SYNCS_FOR_DONE_SCREEN, + variables: { filter: { journeyId: 'journeyId' } } + }, + result: { + data: { + googleSheetsSyncs: [] + } + } +} + +const googleSheetsSyncsWithActiveMock: MockedResponse = { + request: { + query: GET_GOOGLE_SHEETS_SYNCS_FOR_DONE_SCREEN, + variables: { filter: { journeyId: 'journeyId' } } + }, + result: { + data: { + googleSheetsSyncs: [{ id: 'syncId', deletedAt: null }] + } + } +} + const journeyForSharingMock = { request: { query: GET_JOURNEY_FOR_SHARING, @@ -122,7 +146,7 @@ describe('DoneScreen', () => { it('renders the completion message', () => { render( - + @@ -134,7 +158,7 @@ describe('DoneScreen', () => { it('renders first card of journey as preview', async () => { render( - + @@ -146,7 +170,7 @@ describe('DoneScreen', () => { it('renders all action buttons', () => { render( - + @@ -165,7 +189,7 @@ describe('DoneScreen', () => { } render( - + @@ -186,8 +210,16 @@ describe('DoneScreen', () => { id: 'test-journey-id' } + const syncsForTestJourneyMock: MockedResponse = { + request: { + query: GET_GOOGLE_SHEETS_SYNCS_FOR_DONE_SCREEN, + variables: { filter: { journeyId: 'test-journey-id' } } + }, + result: { data: { googleSheetsSyncs: [] } } + } + render( - + @@ -205,7 +237,7 @@ describe('DoneScreen', () => { it('renders notification section heading and label', () => { render( - + @@ -222,7 +254,7 @@ describe('DoneScreen', () => { it('renders notification switch unchecked by default', () => { render( - + @@ -243,6 +275,7 @@ describe('DoneScreen', () => { @@ -270,7 +303,7 @@ describe('DoneScreen', () => { render( - + @@ -313,4 +346,59 @@ describe('DoneScreen', () => { expect(screen.getByText('Link copied')).toBeInTheDocument() }) }) + + it('renders Sync to Google Sheets row with Sync button when no active syncs', async () => { + render( + + + + + + + + ) + + expect(screen.getByText('Sync to Google Sheets')).toBeInTheDocument() + await waitFor(() => { + expect(screen.getByTestId('GoogleSheetsSyncButton')).toHaveTextContent( + 'Sync' + ) + }) + }) + + it('renders Edit button when active syncs exist', async () => { + render( + + + + + + + + ) + + await waitFor(() => { + expect(screen.getByTestId('GoogleSheetsSyncButton')).toHaveTextContent( + 'Edit' + ) + }) + }) + + it('opens GoogleSheetsSyncDialog when Sync button is clicked', async () => { + render( + + + + + + + + ) + + fireEvent.click(screen.getByTestId('GoogleSheetsSyncButton')) + + await waitFor(() => { + expect(screen.getByRole('dialog')).toBeInTheDocument() + }) + }) }) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx index 39d433daf2e..fb3c4df1fdf 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx @@ -1,10 +1,11 @@ +import { gql, useQuery } from '@apollo/client' import Box from '@mui/material/Box' import Button from '@mui/material/Button' import Stack from '@mui/material/Stack' import Typography from '@mui/material/Typography' import { useRouter } from 'next/router' import { useTranslation } from 'next-i18next' -import { ReactElement } from 'react' +import { ReactElement, useState } from 'react' import { TreeBlock } from '@core/journeys/ui/block' import { useJourney } from '@core/journeys/ui/JourneyProvider' @@ -16,9 +17,27 @@ import Play3Icon from '@core/shared/ui/icons/Play3' import { NotificationSwitch } from '../../../../AccessDialog/NotificationSwitch' import { ShareItem } from '../../../../Editor/Toolbar/Items/ShareItem' +import { GoogleSheetsSyncDialog } from '../../../../JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/GoogleSheetsSyncDialog' import { CustomizationScreen } from '../../../utils/getCustomizeFlowConfig' import { ScreenWrapper } from '../ScreenWrapper' +export const GET_GOOGLE_SHEETS_SYNCS_FOR_DONE_SCREEN = gql` + query GoogleSheetsSyncsForDoneScreen($filter: GoogleSheetsSyncsFilter!) { + googleSheetsSyncs(filter: $filter) { + id + deletedAt + } + } +` + +interface GoogleSheetsSyncsForDoneScreenData { + googleSheetsSyncs: Array<{ id: string; deletedAt: string | null }> +} + +interface GoogleSheetsSyncsForDoneScreenVariables { + filter: { journeyId: string } +} + interface DoneScreenProps { handleScreenNavigation?: (screen: CustomizationScreen) => void } @@ -29,6 +48,19 @@ export function DoneScreen({ const { t } = useTranslation('apps-journeys-admin') const { journey } = useJourney() const router = useRouter() + const [syncDialogOpen, setSyncDialogOpen] = useState(false) + + const { data: syncsData, refetch: refetchSyncs } = useQuery< + GoogleSheetsSyncsForDoneScreenData, + GoogleSheetsSyncsForDoneScreenVariables + >(GET_GOOGLE_SHEETS_SYNCS_FOR_DONE_SCREEN, { + variables: { filter: { journeyId: journey?.id ?? '' } }, + skip: journey?.id == null + }) + + const hasActiveSyncs = + syncsData?.googleSheetsSyncs.some((sync) => sync.deletedAt == null) ?? false + const journeyPath = `/api/preview?slug=${journey?.slug}` const href = journey?.slug != null ? journeyPath : undefined @@ -40,6 +72,15 @@ export function DoneScreen({ if (journey?.id != null) void router.push('/') } + function handleSyncDialogOpen(): void { + setSyncDialogOpen(true) + } + + function handleSyncDialogClose(): void { + setSyncDialogOpen(false) + void refetchSyncs() + } + return ( + + + {t('Sync to Google Sheets')} + + + + {journey?.id != null && ( + + )} ) From c196de622c153e33e364e79e3cd93d9e4c531f91 Mon Sep 17 00:00:00 2001 From: edmonday Date: Tue, 3 Mar 2026 03:55:19 +0000 Subject: [PATCH 08/33] feat: implement Google integration creation flow in GoogleSheetsSyncDialog - Added mutation for creating Google integration. - Implemented OAuth code handling directly in the sync dialog. - Updated routing logic to support new integration flow. - Enhanced user feedback with success/error notifications. Refs: NES-1298 --- .../GoogleCreateIntegration.tsx | 5 + .../GoogleSheetsSyncDialog.tsx | 97 ++++++++++++++----- .../MultiStepForm/MultiStepForm.tsx | 2 +- .../Screens/DoneScreen/DoneScreen.tsx | 11 ++- 4 files changed, 89 insertions(+), 26 deletions(-) diff --git a/apps/journeys-admin/src/components/Google/GoogleCreateIntegration/GoogleCreateIntegration.tsx b/apps/journeys-admin/src/components/Google/GoogleCreateIntegration/GoogleCreateIntegration.tsx index bb82688da2f..695ba3c2521 100644 --- a/apps/journeys-admin/src/components/Google/GoogleCreateIntegration/GoogleCreateIntegration.tsx +++ b/apps/journeys-admin/src/components/Google/GoogleCreateIntegration/GoogleCreateIntegration.tsx @@ -112,6 +112,11 @@ export function GoogleCreateIntegration(): ReactElement { preventDuplicate: true }) } + const fallbackPath = + returnTo != null && returnTo !== '' + ? returnTo + : `/teams/${teamId}/integrations` + await router.push(fallbackPath) } finally { setLoading(false) } diff --git a/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.tsx b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.tsx index 213c51a0f2b..237720b58ce 100644 --- a/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.tsx +++ b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.tsx @@ -117,6 +117,14 @@ const BACKFILL_GOOGLE_SHEETS_SYNC = gql` } ` +const INTEGRATION_GOOGLE_CREATE = gql` + mutation IntegrationGoogleCreate($input: IntegrationGoogleCreateInput!) { + integrationGoogleCreate(input: $input) { + id + } + } +` + interface GoogleSheetsSyncItem { id: string spreadsheetId: string | null @@ -162,7 +170,7 @@ export function GoogleSheetsSyncDialog({ const { data: journeyData } = useQuery(GET_JOURNEY_CREATED_AT, { variables: { id: journeyId } }) - const { data: integrationsData } = useIntegrationQuery({ + const { data: integrationsData, refetch: refetchIntegrations } = useIntegrationQuery({ teamId: journeyData?.journey?.team?.id as string }) @@ -180,6 +188,7 @@ export function GoogleSheetsSyncDialog({ ) const [deleteSync] = useMutation(DELETE_GOOGLE_SHEETS_SYNC) const [backfillSync] = useMutation(BACKFILL_GOOGLE_SHEETS_SYNC) + const [integrationGoogleCreate] = useMutation(INTEGRATION_GOOGLE_CREATE) const [deletingSyncId, setDeletingSyncId] = useState(null) const [syncIdPendingDelete, setSyncIdPendingDelete] = useState( @@ -189,6 +198,48 @@ export function GoogleSheetsSyncDialog({ null ) + async function handleIntegrationCreate(authCode: string): Promise { + const teamId = journeyData?.journey?.team?.id + if (teamId == null || typeof window === 'undefined') return + + const redirectUri = `${window.location.origin}/api/integrations/google/callback` + + // Clean URL params immediately to prevent re-triggering + const newQuery = { ...router.query } + delete newQuery.code + delete newQuery.openSyncDialog + void router.replace( + { pathname: router.pathname, query: newQuery }, + undefined, + { shallow: true } + ) + + try { + const { data } = await integrationGoogleCreate({ + variables: { + input: { teamId, code: authCode, redirectUri } + } + }) + + if (data?.integrationGoogleCreate?.id != null) { + await refetchIntegrations() + enqueueSnackbar(t('Google integration created successfully'), { + variant: 'success' + }) + setGoogleDialogOpen(true) + } else { + enqueueSnackbar( + t('Google settings failed. Reload the page or try again.'), + { variant: 'error' } + ) + } + } catch (error) { + if (error instanceof Error) { + enqueueSnackbar(error.message, { variant: 'error' }) + } + } + } + useEffect(() => { if (!open) return void loadSyncs({ @@ -196,25 +247,26 @@ export function GoogleSheetsSyncDialog({ fetchPolicy: 'network-only' }) - // Check if returning from Google integration creation - const integrationCreated = router.query.integrationCreated === 'true' const openSyncDialog = router.query.openSyncDialog === 'true' + // New flow: handle OAuth code exchange directly + const authCode = router.query.code as string | undefined + if (authCode != null && openSyncDialog) { + void handleIntegrationCreate(authCode) + return + } + + // Backwards compat: old flow via GoogleCreateIntegration page + const integrationCreated = router.query.integrationCreated === 'true' if (integrationCreated && openSyncDialog) { - // Remove query parameters from URL const newQuery = { ...router.query } delete newQuery.integrationCreated delete newQuery.openSyncDialog void router.replace( - { - pathname: router.pathname, - query: newQuery - }, + { pathname: router.pathname, query: newQuery }, undefined, { shallow: true } ) - - // Open the "Add Google Sheets Sync" dialog setGoogleDialogOpen(true) enqueueSnackbar(t('Google integration created successfully'), { variant: 'success' @@ -242,7 +294,8 @@ export function GoogleSheetsSyncDialog({ if (syncsLoading) return // Skip if we're already handling integration creation return flow const integrationCreated = router.query.integrationCreated === 'true' - if (integrationCreated) return + const hasAuthCode = router.query.code != null + if (integrationCreated || hasAuthCode) return // If there are no active or history syncs, open the add dialog directly if (activeSyncs.length === 0 && historySyncs.length === 0) { @@ -1222,19 +1275,15 @@ export function GoogleSheetsSyncDialog({ color="primary" href={(() => { const teamId = journeyData?.journey?.team?.id - if (teamId == null) return undefined - - // Create returnTo URL that will redirect back to current page with sync dialog open - const currentPath = router.asPath.split('?')[0] - const returnTo = `${currentPath}?openSyncDialog=true` - - // Generate OAuth URL that will redirect to GoogleCreateIntegration page, - // which will then redirect back to returnTo after integration is created - const googleCreateIntegrationPath = `/teams/${teamId}/integrations/new/google?returnTo=${encodeURIComponent(returnTo)}` - return getGoogleOAuthUrl( - teamId, - googleCreateIntegrationPath - ) + if (teamId == null || typeof window === 'undefined') + return undefined + + // Build returnTo URL pointing directly back to the current page + const url = new URL(router.asPath, window.location.origin) + url.searchParams.set('openSyncDialog', 'true') + const returnTo = url.pathname + url.search + + return getGoogleOAuthUrl(teamId, returnTo) })()} sx={{ alignSelf: 'flex-end', diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/MultiStepForm.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/MultiStepForm.tsx index 26318601407..1cf6e34646c 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/MultiStepForm.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/MultiStepForm.tsx @@ -112,7 +112,7 @@ export function MultiStepForm(): ReactElement { journeyId, screens, activeScreen, - isGuest: user?.id == null, + isGuest: user?.clientInitialized === true && user?.id == null, guestFlowEnabled: templateCustomizationGuestFlow === true }) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx index fb3c4df1fdf..cf8e4749780 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx @@ -5,7 +5,7 @@ import Stack from '@mui/material/Stack' import Typography from '@mui/material/Typography' import { useRouter } from 'next/router' import { useTranslation } from 'next-i18next' -import { ReactElement, useState } from 'react' +import { ReactElement, useEffect, useState } from 'react' import { TreeBlock } from '@core/journeys/ui/block' import { useJourney } from '@core/journeys/ui/JourneyProvider' @@ -50,6 +50,15 @@ export function DoneScreen({ const router = useRouter() const [syncDialogOpen, setSyncDialogOpen] = useState(false) + // Auto-open sync dialog when returning from OAuth flow + useEffect(() => { + if (journey?.id == null) return + const openSyncDialog = router.query.openSyncDialog === 'true' + if (openSyncDialog) { + setSyncDialogOpen(true) + } + }, [journey?.id, router.query.openSyncDialog]) + const { data: syncsData, refetch: refetchSyncs } = useQuery< GoogleSheetsSyncsForDoneScreenData, GoogleSheetsSyncsForDoneScreenVariables From 600b109fe42c0d1ea80b4cb92cd5c69c2436ef82 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 04:04:45 +0000 Subject: [PATCH 09/33] fix: lint issues --- .../GoogleSheetsSyncDialog.tsx | 7 +-- .../Screens/DoneScreen/DoneScreen.spec.tsx | 53 ++++++++++++++----- .../Screens/DoneScreen/DoneScreen.tsx | 4 +- .../Screens/TextScreen/TextScreen.tsx | 4 +- libs/locales/en/apps-journeys-admin.json | 6 ++- 5 files changed, 50 insertions(+), 24 deletions(-) diff --git a/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.tsx b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.tsx index 237720b58ce..39e23dfeaa3 100644 --- a/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.tsx +++ b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.tsx @@ -170,9 +170,10 @@ export function GoogleSheetsSyncDialog({ const { data: journeyData } = useQuery(GET_JOURNEY_CREATED_AT, { variables: { id: journeyId } }) - const { data: integrationsData, refetch: refetchIntegrations } = useIntegrationQuery({ - teamId: journeyData?.journey?.team?.id as string - }) + const { data: integrationsData, refetch: refetchIntegrations } = + useIntegrationQuery({ + teamId: journeyData?.journey?.team?.id as string + }) const [googleDialogOpen, setGoogleDialogOpen] = useState(false) const [pickerActive, setPickerActive] = useState(false) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.spec.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.spec.tsx index a09becbd1af..9a8a66e5edb 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.spec.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.spec.tsx @@ -14,7 +14,10 @@ import { GET_CUSTOM_DOMAINS } from '../../../../../libs/useCustomDomainsQuery/us import { GET_JOURNEY_FOR_SHARING } from '../../../../../libs/useJourneyForShareLazyQuery/useJourneyForShareLazyQuery' import { useJourneyNotifcationUpdateMock } from '../../../../../libs/useJourneyNotificationUpdate/useJourneyNotificationUpdate.mock' -import { DoneScreen, GET_GOOGLE_SHEETS_SYNCS_FOR_DONE_SCREEN } from './DoneScreen' +import { + DoneScreen, + GET_GOOGLE_SHEETS_SYNCS_FOR_DONE_SCREEN +} from './DoneScreen' jest.mock('next/router', () => ({ __esModule: true, @@ -146,7 +149,9 @@ describe('DoneScreen', () => { it('renders the completion message', () => { render( - + @@ -158,7 +163,9 @@ describe('DoneScreen', () => { it('renders first card of journey as preview', async () => { render( - + @@ -170,7 +177,9 @@ describe('DoneScreen', () => { it('renders all action buttons', () => { render( - + @@ -189,7 +198,9 @@ describe('DoneScreen', () => { } render( - + @@ -237,7 +248,9 @@ describe('DoneScreen', () => { it('renders notification section heading and label', () => { render( - + @@ -245,16 +258,16 @@ describe('DoneScreen', () => { ) - expect( - screen.getByText('Choose where responses go:') - ).toBeInTheDocument() + expect(screen.getByText('Choose where responses go:')).toBeInTheDocument() expect(screen.getByText('Send to my email')).toBeInTheDocument() }) it('renders notification switch unchecked by default', () => { render( - + @@ -303,7 +316,13 @@ describe('DoneScreen', () => { render( - + @@ -350,7 +369,9 @@ describe('DoneScreen', () => { it('renders Sync to Google Sheets row with Sync button when no active syncs', async () => { render( - + @@ -369,7 +390,9 @@ describe('DoneScreen', () => { it('renders Edit button when active syncs exist', async () => { render( - + @@ -387,7 +410,9 @@ describe('DoneScreen', () => { it('opens GoogleSheetsSyncDialog when Sync button is clicked', async () => { render( - + diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx index cf8e4749780..5d74328b021 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx @@ -159,9 +159,7 @@ export function DoneScreen({ justifyContent="space-between" alignItems="center" > - - {t('Send to my email')} - + {t('Send to my email')} Date: Tue, 3 Mar 2026 04:11:10 +0000 Subject: [PATCH 10/33] feat: enhance GoogleSheetsSyncDialog with user authentication check - Integrated user authentication check before handling OAuth code exchange. - Updated effect dependencies to include user.clientInitialized for better synchronization. Refs: NES-1298 --- .../GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.tsx b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.tsx index 39e23dfeaa3..e55691a4347 100644 --- a/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.tsx +++ b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.tsx @@ -29,6 +29,7 @@ import useMediaQuery from '@mui/material/useMediaQuery' import { format } from 'date-fns' import { Form, Formik, FormikHelpers, FormikValues } from 'formik' import { useRouter } from 'next/router' +import { useUser } from 'next-firebase-auth' import { useTranslation } from 'next-i18next' import { useSnackbar } from 'notistack' import { KeyboardEvent, ReactElement, useEffect, useState } from 'react' @@ -164,6 +165,7 @@ export function GoogleSheetsSyncDialog({ const { t } = useTranslation('apps-journeys-admin') const { enqueueSnackbar } = useSnackbar() const router = useRouter() + const user = useUser() const theme = useTheme() const isMobile = useMediaQuery(theme.breakpoints.down('sm')) @@ -250,10 +252,12 @@ export function GoogleSheetsSyncDialog({ const openSyncDialog = router.query.openSyncDialog === 'true' - // New flow: handle OAuth code exchange directly + // New flow: handle OAuth code exchange directly (wait for auth to settle) const authCode = router.query.code as string | undefined if (authCode != null && openSyncDialog) { - void handleIntegrationCreate(authCode) + if (user.clientInitialized) { + void handleIntegrationCreate(authCode) + } return } @@ -273,7 +277,7 @@ export function GoogleSheetsSyncDialog({ variant: 'success' }) } - }, [open, journeyId, loadSyncs, router, enqueueSnackbar, t]) + }, [open, journeyId, loadSyncs, router, enqueueSnackbar, t, user.clientInitialized]) useEffect(() => { if (open) return From 141e92fc47e315d972cf69fe629b54daeb685e31 Mon Sep 17 00:00:00 2001 From: edmonday Date: Wed, 4 Mar 2026 00:44:49 +0000 Subject: [PATCH 11/33] style: ui changes for email --- .../Screens/DoneScreen/DoneScreen.tsx | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx index 3af35982aa4..9cc95b7b848 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx @@ -90,20 +90,29 @@ export function DoneScreen({ /> - - - + {t('Choose where responses go:')} + + {t('Choose Response Destination:')} + + ) From 69550dd8301a7f83794f0edaceb9ea657c60fb0a Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 01:02:01 +0000 Subject: [PATCH 12/33] fix: lint issues --- .../GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.tsx | 10 +++++++++- libs/locales/en/apps-journeys-admin.json | 6 +++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.tsx b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.tsx index e55691a4347..4d29bb0e2c3 100644 --- a/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.tsx +++ b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.tsx @@ -277,7 +277,15 @@ export function GoogleSheetsSyncDialog({ variant: 'success' }) } - }, [open, journeyId, loadSyncs, router, enqueueSnackbar, t, user.clientInitialized]) + }, [ + open, + journeyId, + loadSyncs, + router, + enqueueSnackbar, + t, + user.clientInitialized + ]) useEffect(() => { if (open) return diff --git a/libs/locales/en/apps-journeys-admin.json b/libs/locales/en/apps-journeys-admin.json index ba167119965..88f3d3c4b6a 100644 --- a/libs/locales/en/apps-journeys-admin.json +++ b/libs/locales/en/apps-journeys-admin.json @@ -1006,9 +1006,13 @@ "Edit Manually": "Edit Manually", "Ready to Share!": "Ready to Share!", "Share your unique link on any platform.": "Share your unique link on any platform.", - "Go To Projects Dashboard": "Go To Projects Dashboard", "Choose where responses go:": "Choose where responses go:", + "Choose Response Destination:": "Choose Response Destination:", "Send to my email": "Send to my email", + "Edit Google Sheets sync": "Edit Google Sheets sync", + "Edit": "Edit", + "Sync": "Sync", + "Go To Projects Dashboard": "Go To Projects Dashboard", "Share!": "Share!", "{{count}} more cards_one": "{{count}} more cards", "{{count}} more cards_other": "{{count}} more cards", From 438200587a31a9081a4f760a09e0910f2fc135a3 Mon Sep 17 00:00:00 2001 From: edmonday Date: Wed, 4 Mar 2026 02:51:14 +0000 Subject: [PATCH 13/33] chore: formatting --- .../MultiStepForm/Screens/DoneScreen/DoneScreen.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx index d03afd4ccec..49997500811 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx @@ -151,10 +151,16 @@ export function DoneScreen({ p: 5 }} > - + {t('Choose where responses go:')} - + {t('Choose Response Destination:')} Date: Wed, 4 Mar 2026 02:52:14 +0000 Subject: [PATCH 14/33] style: spacing --- .../MultiStepForm/Screens/DoneScreen/DoneScreen.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx index 9cc95b7b848..5ffe264e2f3 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx @@ -91,7 +91,7 @@ export function DoneScreen({ Date: Wed, 4 Mar 2026 02:58:14 +0000 Subject: [PATCH 15/33] fix: lint issues --- .../MultiStepForm/Screens/DoneScreen/DoneScreen.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx index 9095e4589b6..2113fe64b09 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx @@ -151,16 +151,10 @@ export function DoneScreen({ p: 5 }} > - + {t('Choose where responses go:')} - + {t('Choose Response Destination:')} Date: Fri, 6 Mar 2026 02:59:43 +0000 Subject: [PATCH 16/33] fix: styling and carousel --- .../Toolbar/Items/ShareItem/ShareItem.tsx | 8 +- .../MultiStepForm/MultiStepForm.tsx | 54 +---- .../Screens/DoneScreen/DoneScreen.tsx | 113 +++++----- .../Screens/LanguageScreen/LanguageScreen.tsx | 105 +++++----- .../LinksScreen/CardsPreview/CardsPreview.tsx | 3 +- .../Screens/LinksScreen/CardsPreview/index.ts | 2 +- .../Screens/LinksScreen/LinksScreen.tsx | 197 ++++++++---------- .../Screens/MediaScreen/MediaScreen.tsx | 18 -- .../Sections/CardsSection/CardsSection.tsx | 11 +- .../Screens/ScreenWrapper/ScreenWrapper.tsx | 13 +- .../Screens/SocialScreen/SocialScreen.tsx | 17 +- .../Screens/TextScreen/TextScreen.tsx | 133 ++++++------ .../TemplateCardPreview.tsx | 22 ++ .../TemplateCardPreview/index.ts | 2 +- .../templateCardPreviewConfig.ts | 16 +- 15 files changed, 331 insertions(+), 383 deletions(-) diff --git a/apps/journeys-admin/src/components/Editor/Toolbar/Items/ShareItem/ShareItem.tsx b/apps/journeys-admin/src/components/Editor/Toolbar/Items/ShareItem/ShareItem.tsx index 861b5b09d0c..b245f05420c 100644 --- a/apps/journeys-admin/src/components/Editor/Toolbar/Items/ShareItem/ShareItem.tsx +++ b/apps/journeys-admin/src/components/Editor/Toolbar/Items/ShareItem/ShareItem.tsx @@ -17,6 +17,7 @@ import { GetJourneyForSharing_journey as JourneyFromLazyQuery } from '../../../. import { JourneyFields as JourneyFromContext } from '../../../../../../__generated__/JourneyFields' import { useCustomDomainsQuery } from '../../../../../libs/useCustomDomainsQuery' import { Item } from '../Item/Item' +import { SxProps, Theme } from '@mui/material/styles' const EmbedJourneyDialog = dynamic( async () => @@ -50,7 +51,9 @@ interface ShareItemProps { handleCloseMenu?: () => void handleKeepMounted?: () => void buttonVariant?: 'icon' | 'default' + buttonSx?: SxProps setHasOpenDialog?: (hasOpenDialog: boolean) => void + buttonProps?: ComponentProps } /** @@ -70,7 +73,8 @@ export function ShareItem({ handleCloseMenu, handleKeepMounted, buttonVariant = 'icon', - setHasOpenDialog + setHasOpenDialog, + buttonProps }: ShareItemProps): ReactElement { const { t } = useTranslation('apps-journeys-admin') const { enqueueSnackbar } = useSnackbar() @@ -124,7 +128,7 @@ export function ShareItem({ variant={variant} label={t('Share')} onClick={handleShowMenu} - ButtonProps={{ variant: 'contained' }} + ButtonProps={{ variant: 'contained', ...buttonProps }} icon={buttonVariant === 'icon' ? : undefined} /> void, - handleScreenNavigation: (screen: CustomizationScreen) => void ): ReactElement { switch (screen) { case 'language': return ( ) case 'text': return ( ) case 'links': return ( ) case 'media': @@ -69,11 +62,10 @@ function renderScreen( return ( ) case 'done': - return + return default: return <> } @@ -128,16 +120,11 @@ export function MultiStepForm(): ReactElement { ) } - async function handleScreenNavigation( - screen: CustomizationScreen - ): Promise { - void router.replace(buildCustomizeUrl(journeyId, screen, undefined)) - } - return ( - - - - + {(hasEditableText || hasCustomizableLinks || hasCustomizableMedia) && ( @@ -180,17 +150,7 @@ export function MultiStepForm(): ReactElement { /> )} - - - {renderScreen(activeScreen, handleNext, handleScreenNavigation)} - + {renderScreen(activeScreen, handleNext)} diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx index 6977128cbe9..aa4cdc4fbb7 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx @@ -1,4 +1,3 @@ -import Box from '@mui/material/Box' import Button from '@mui/material/Button' import Stack from '@mui/material/Stack' import Typography from '@mui/material/Typography' @@ -15,16 +14,9 @@ import ArrowRightContained1Icon from '@core/shared/ui/icons/ArrowRightContained1 import Play3Icon from '@core/shared/ui/icons/Play3' import { ShareItem } from '../../../../Editor/Toolbar/Items/ShareItem' -import { CustomizationScreen } from '../../../utils/getCustomizeFlowConfig' import { ScreenWrapper } from '../ScreenWrapper' -interface DoneScreenProps { - handleScreenNavigation?: (screen: CustomizationScreen) => void -} - -export function DoneScreen({ - handleScreenNavigation -}: DoneScreenProps): ReactElement { +export function DoneScreen(): ReactElement { const { t } = useTranslation('apps-journeys-admin') const { journey } = useJourney() const router = useRouter() @@ -40,55 +32,10 @@ export function DoneScreen({ } return ( - - - {steps.length > 0 && ( - - )} - - - - - - - - - + - - + } + > + {steps.length > 0 && ( + + )} + + + + + + ) } diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx index 59a70befb7d..7d870435534 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx @@ -11,8 +11,6 @@ import { object, string } from 'yup' import { TreeBlock } from '@core/journeys/ui/block' import { useJourney } from '@core/journeys/ui/JourneyProvider' import { useTeam } from '@core/journeys/ui/TeamProvider' -import { TemplateCardPreview } from '@core/journeys/ui/TemplateView/TemplatePreviewTabs/TemplateCardPreview/TemplateCardPreview' -import { TemplateCardPreviewItem } from '@core/journeys/ui/TemplateView/TemplatePreviewTabs/TemplateCardPreview/TemplateCardPreviewItem' import { transformer } from '@core/journeys/ui/transformer' import { useJourneyDuplicateMutation } from '@core/journeys/ui/useJourneyDuplicateMutation' import { GetJourney_journey_blocks_StepBlock as StepBlock } from '@core/journeys/ui/useJourneyQuery/__generated__/GetJourney' @@ -21,21 +19,19 @@ import { LanguageAutocomplete } from '@core/shared/ui/LanguageAutocomplete' import { useGetChildTemplateJourneyLanguages } from '../../../../../libs/useGetChildTemplateJourneyLanguages' import { useGetParentTemplateJourneyLanguages } from '../../../../../libs/useGetParentTemplateJourneyLanguages' -import { CustomizationScreen } from '../../../utils/getCustomizeFlowConfig' import { CustomizeFlowNextButton } from '../../CustomizeFlowNextButton' -import { CardsPreview } from '../LinksScreen/CardsPreview' +import { CardsPreview, EDGE_FADE_PX } from '../LinksScreen/CardsPreview' import { ScreenWrapper } from '../ScreenWrapper' import { JourneyCustomizeTeamSelect } from './JourneyCustomizeTeamSelect' +import Box from '@mui/material/Box' interface LanguageScreenProps { handleNext: (overrideJourneyId?: string) => void - handleScreenNavigation: (screen: CustomizationScreen) => void } export function LanguageScreen({ handleNext, - handleScreenNavigation }: LanguageScreenProps): ReactElement { const { templateCustomizationGuestFlow } = useFlags() const { t } = useTranslation('journeys-ui') @@ -117,8 +113,6 @@ export function LanguageScreen({ const [journeyDuplicate] = useJourneyDuplicateMutation() - const FORM_SM_BREAKPOINT_WIDTH = '390px' - function shouldSkipDuplicate( journey: { template?: boolean | null @@ -168,40 +162,61 @@ export function LanguageScreen({ } return ( - - - - {`'${journey?.title ?? ''}'`} - - - {steps.length > 0 && } - - + {({ handleSubmit: formikHandleSubmit, setFieldValue, values }) => ( + formikHandleSubmit()} + disabled={ + (templateCustomizationGuestFlow && !isSignedIn) || loading + } + ariaLabel={t('Next')} + /> + } > - {({ handleSubmit, setFieldValue, values }) => ( + + + {`'${journey?.title ?? ''}'`} + + + {steps.length > 0 && } +
- + setFieldValue('languageSelect', value)} + onChange={(value) => + setFieldValue('languageSelect', value) + } /> {isSignedIn && ( <> @@ -243,20 +260,12 @@ export function LanguageScreen({ )} - handleSubmit()} - disabled={ - (templateCustomizationGuestFlow && !isSignedIn) || loading - } - ariaLabel={t('Next')} - />
- )} -
-
-
+
+ + )} +
) } diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/CardsPreview/CardsPreview.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/CardsPreview/CardsPreview.tsx index 0bfa7fac69d..1e9b575e458 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/CardsPreview/CardsPreview.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/CardsPreview/CardsPreview.tsx @@ -51,7 +51,7 @@ const IFRAME_SCALE = CONTAINER_WIDTH / FRAME_WIDTH const CONTAINER_HEIGHT = Math.round(FRAME_HEIGHT * IFRAME_SCALE) // Spacing and offsets -const EDGE_FADE_PX = 16 +export const EDGE_FADE_PX = 40 function CardsPreviewItem({ step }: CardsPreviewItemProps): ReactElement { const { journey } = useJourney() @@ -159,6 +159,7 @@ export function CardsPreview({ steps }: CardsPreviewProps): ReactElement { slidesPerView="auto" spaceBetween={12} slidesOffsetBefore={EDGE_FADE_PX} + slidesOffsetAfter={EDGE_FADE_PX} observer observeParents sx={{ diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/CardsPreview/index.ts b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/CardsPreview/index.ts index 0330eeabf1f..6e5e3f1a2bf 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/CardsPreview/index.ts +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/CardsPreview/index.ts @@ -1 +1 @@ -export { CardsPreview } from './CardsPreview' +export { CardsPreview, EDGE_FADE_PX } from './CardsPreview' diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/LinksScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/LinksScreen.tsx index 789e9be4806..654f460dacb 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/LinksScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/LinksScreen.tsx @@ -1,5 +1,4 @@ import { useMutation } from '@apollo/client' -import Stack from '@mui/material/Stack' import { Formik, FormikHelpers, FormikProvider } from 'formik' import { useTranslation } from 'next-i18next' import { ReactElement, useMemo } from 'react' @@ -24,7 +23,6 @@ import { useBlockActionLinkUpdateMutation } from '../../../../../libs/useBlockAc import { useBlockActionPhoneUpdateMutation } from '../../../../../libs/useBlockActionPhoneUpdateMutation' import { JOURNEY_CHAT_BUTTON_UPDATE } from '../../../../Editor/Slider/Settings/CanvasDetails/JourneyAppearance/Chat/ChatOption/Details/Details' import { countries } from '../../../../Editor/Slider/Settings/CanvasDetails/Properties/controls/Action/PhoneAction/countriesList' -import { CustomizationScreen } from '../../../utils/getCustomizeFlowConfig' import { getJourneyLinks } from '../../../utils/getJourneyLinks' import { CustomizeFlowNextButton } from '../../CustomizeFlowNextButton' import { ScreenWrapper } from '../ScreenWrapper' @@ -34,13 +32,9 @@ import { LinksForm } from './LinksForm' interface LinksScreenProps { handleNext: (overrideJourneyId?: string) => void - handleScreenNavigation: (screen: CustomizationScreen) => void } -export function LinksScreen({ - handleNext, - handleScreenNavigation -}: LinksScreenProps): ReactElement { +export function LinksScreen({ handleNext }: LinksScreenProps): ReactElement { const { t } = useTranslation('apps-journeys-admin') const { journey } = useJourney() const links = useMemo(() => getJourneyLinks(t, journey), [journey]) @@ -189,105 +183,98 @@ export function LinksScreen({ } return ( - >((acc, link) => { + if (link.linkType === 'phone') { + const block = journey?.blocks?.find((b) => b.id === link.id) + const action = + (block as any)?.action?.__typename === 'PhoneAction' + ? ((block as any).action as JourneyPhoneAction) + : undefined + const country = + action?.countryCode != null + ? countries.find((c) => c.countryCode === action.countryCode) + : undefined + const callingCode = country?.callingCode ?? '+' + const ccDigits = callingCode.replace(/[^\d]/g, '') + const prefix = ccDigits === '' ? '' : `+${ccDigits}` + const local = (action?.phone ?? '').startsWith(prefix) + ? (action?.phone ?? '').slice(prefix.length) + : (action?.phone ?? '').replace(/^\+/, '') + acc[`${link.id}__cc`] = callingCode + acc[`${link.id}__local`] = local + } else { + acc[link.id] = link.url ?? '' + } + return acc + }, {})} + validationSchema={object().shape( + links.reduce>>( + (acc, link) => { + if (link.linkType === 'email') { + acc[link.id] = string().email(t('Enter a valid email')) + } else if (link.linkType === 'phone') { + acc[`${link.id}__cc`] = string().test( + 'valid-cc', + t('Enter a valid calling code'), + (val) => { + if (val == null || val.trim() === '') return false + const normalized = val.startsWith('+') ? val : `+${val}` + return countries.some((c) => c.callingCode === normalized) + } + ) + acc[`${link.id}__local`] = string().test( + 'valid-local', + t('Enter a valid phone number'), + (val) => + val == null || + val.trim() === '' || + /^[0-9\s\-()]+$/.test(val.trim()) + ) + } else { + acc[link.id] = string().url(t('Enter a valid URL')) + } + return acc + }, + {} + ) + )} + validateOnSubmit={false} + onSubmit={handleFormSubmit} + validateOnMount > - - - - >((acc, link) => { - if (link.linkType === 'phone') { - const block = journey?.blocks?.find((b) => b.id === link.id) - const action = - (block as any)?.action?.__typename === 'PhoneAction' - ? ((block as any).action as JourneyPhoneAction) - : undefined - const country = - action?.countryCode != null - ? countries.find((c) => c.countryCode === action.countryCode) - : undefined - const callingCode = country?.callingCode ?? '+' - const ccDigits = callingCode.replace(/[^\d]/g, '') - const prefix = ccDigits === '' ? '' : `+${ccDigits}` - const local = (action?.phone ?? '').startsWith(prefix) - ? (action?.phone ?? '').slice(prefix.length) - : (action?.phone ?? '').replace(/^\+/, '') - acc[`${link.id}__cc`] = callingCode - acc[`${link.id}__local`] = local - } else { - acc[link.id] = link.url ?? '' - } - return acc - }, {})} - validationSchema={object().shape( - links.reduce>>( - (acc, link) => { - if (link.linkType === 'email') { - acc[link.id] = string().email(t('Enter a valid email')) - } else if (link.linkType === 'phone') { - acc[`${link.id}__cc`] = string().test( - 'valid-cc', - t('Enter a valid calling code'), - (val) => { - if (val == null || val.trim() === '') return false - const normalized = val.startsWith('+') ? val : `+${val}` - return countries.some((c) => c.callingCode === normalized) - } - ) - acc[`${link.id}__local`] = string().test( - 'valid-local', - t('Enter a valid phone number'), - (val) => - val == null || - val.trim() === '' || - /^[0-9\s\-()]+$/.test(val.trim()) - ) - } else { - acc[link.id] = string().url(t('Enter a valid URL')) - } - return acc - }, - {} - ) - )} - validateOnSubmit={false} - onSubmit={handleFormSubmit} - validateOnMount - > - {(formik) => ( - + {(formik) => ( + + + } + > + - - - )} - - + + + )} + ) } diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/MediaScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/MediaScreen.tsx index cfe7e36cc38..20b0895071f 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/MediaScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/MediaScreen.tsx @@ -1,4 +1,3 @@ -import Stack from '@mui/material/Stack' import { useTranslation } from 'next-i18next' import { ReactElement, useEffect, useState } from 'react' @@ -67,21 +66,6 @@ export function MediaScreen({ handleNext }: MediaScreenProps): ReactElement { setSelectedCardBlockId(getCardBlockIdFromStep(step)) } return ( - - } - - ) } diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/Sections/CardsSection/CardsSection.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/Sections/CardsSection/CardsSection.tsx index c23b32ec903..220dfe02ce7 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/Sections/CardsSection/CardsSection.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/Sections/CardsSection/CardsSection.tsx @@ -7,6 +7,7 @@ import { ReactElement } from 'react' import { TreeBlock } from '@core/journeys/ui/block' import { TemplateCardPreview } from '@core/journeys/ui/TemplateView/TemplatePreviewTabs/TemplateCardPreview/TemplateCardPreview' import { GetJourney_journey_blocks_StepBlock as StepBlock } from '@core/journeys/ui/useJourneyQuery/__generated__/GetJourney' +import { OVERFLOW_PX } from '@core/journeys/ui/TemplateView/TemplatePreviewTabs/TemplateCardPreview/templateCardPreviewConfig' interface CardsSectionProps { customizableSteps: Array> @@ -37,13 +38,19 @@ export function CardsSection({ gap={4} data-testid="CardsSection" sx={{ - width: '100%' + width: '100%', + overflow: 'visible' }} > {t('Cards')} - + - + + void - handleScreenNavigation: (screen: CustomizationScreen) => void } export function SocialScreen({ handleNext, - handleScreenNavigation }: SocialScreenProps): ReactElement { const { t } = useTranslation('apps-journeys-admin') return ( - - ) } diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx index 70a3d93e58d..a0b4df0a992 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx @@ -1,6 +1,5 @@ import { gql, useMutation } from '@apollo/client' import Box from '@mui/material/Box' -import Stack from '@mui/material/Stack' import { useTranslation } from 'next-i18next' import { ReactElement, useCallback, useEffect, useState } from 'react' @@ -8,7 +7,6 @@ import { useJourney } from '@core/journeys/ui/JourneyProvider' import { GetJourney_journey_journeyCustomizationFields as JourneyCustomizationField } from '../../../../../../__generated__/GetJourney' import { JourneyCustomizationFieldUpdate } from '../../../../../../__generated__/JourneyCustomizationFieldUpdate' -import { CustomizationScreen } from '../../../utils/getCustomizeFlowConfig' import { CustomizeFlowNextButton } from '../../CustomizeFlowNextButton' import { ScreenWrapper } from '../ScreenWrapper' @@ -126,12 +124,10 @@ const renderEditableText = ( interface TextScreenProps { handleNext: (overrideJourneyId?: string) => void - handleScreenNavigation: (screen: CustomizationScreen) => void } export function TextScreen({ handleNext, - handleScreenNavigation }: TextScreenProps): ReactElement { const { t } = useTranslation() const { journey } = useJourney() @@ -194,76 +190,67 @@ export function TextScreen({ } return ( - + } > - - } - > - - - {renderEditableText( - journey?.journeyCustomizationDescription ?? '', - replacementItems, - handleValueChange - )} - - + + + {renderEditableText( + journey?.journeyCustomizationDescription ?? '', + replacementItems, + handleValueChange + )} - - + + + ) } diff --git a/libs/journeys/ui/src/components/TemplateView/TemplatePreviewTabs/TemplateCardPreview/TemplateCardPreview.tsx b/libs/journeys/ui/src/components/TemplateView/TemplatePreviewTabs/TemplateCardPreview/TemplateCardPreview.tsx index dc383c7599e..b229efbc257 100644 --- a/libs/journeys/ui/src/components/TemplateView/TemplatePreviewTabs/TemplateCardPreview/TemplateCardPreview.tsx +++ b/libs/journeys/ui/src/components/TemplateView/TemplatePreviewTabs/TemplateCardPreview/TemplateCardPreview.tsx @@ -15,6 +15,7 @@ import { GetJourney_journey_blocks_StepBlock as StepBlock } from '../../../../li import { TemplateActionButton } from '../../TemplateViewHeader/TemplateActionButton/TemplateActionButton' import { + type BreakpointSwiperOptions, SELECTED_SCALE, type TemplateCardPreviewVariant, VARIANT_CONFIGS @@ -64,6 +65,16 @@ function TemplateCardPreviewPlaceholder({ const StyledSwiperSlide = styled(SwiperSlide)(() => ({})) const StyledSwiper = styled(Swiper)(() => ({})) +function getSpacerWidth( + cardWidth: number, + bp: BreakpointSwiperOptions +): string { + const selectedWidth = cardWidth * SELECTED_SCALE + const space = bp.spaceBetween ?? 0 + const offset = bp.slidesOffsetBefore ?? 0 + return `calc(100% - ${selectedWidth}px - ${space}px - ${offset}px)` +} + /** * Horizontal carousel of template step cards with optional "more cards" slide. * @@ -165,6 +176,17 @@ export function TemplateCardPreview({ ) })} + {variant === 'media' && ( + + )} {showMoreCardsSlide && steps.length > slidesToRender.length && ( export interface VariantConfig { @@ -31,6 +31,7 @@ export interface VariantConfig { } export const SELECTED_SCALE = 1.07 +export const OVERFLOW_PX = 40 const MEDIA_CARD_HEIGHT = 209 const PREVIEW_CARD_HEIGHT_XS = 295 const PREVIEW_CARD_HEIGHT_SM = 404 @@ -47,8 +48,8 @@ const PREVIEW_VARIANT_CONFIG: VariantConfig = { borderRadius: 4 }, breakpoints: { - xs: { spaceBetween: 12, slidesOffsetAfter: 0 }, - sm: { spaceBetween: 28, slidesOffsetAfter: 0 } + xs: { spaceBetween: 12 }, + sm: { spaceBetween: 28 } }, cardSx: { position: 'relative', @@ -91,8 +92,8 @@ const MEDIA_VARIANT_CONFIG: VariantConfig = { borderRadius: '24px' }, breakpoints: { - xs: { spaceBetween: 12, slidesOffsetAfter: 265 }, - sm: { spaceBetween: 12, slidesOffsetAfter: 260 } + xs: { spaceBetween: 12, slidesOffsetBefore: 0 }, + sm: { spaceBetween: 12, slidesOffsetBefore: OVERFLOW_PX } }, swiperProps: { mousewheel: { forceToAxis: true }, @@ -118,7 +119,8 @@ const MEDIA_VARIANT_CONFIG: VariantConfig = { borderRadius: '12px' }, swiperSx: { - overflow: 'visible', + width: '100%', + overflow: { xs: 'visible', sm: 'hidden' }, zIndex: 2, '& .swiper-wrapper': { alignItems: 'center' From 21f7f0b5a293c7419cd6f5288f25d0ead9c9644a Mon Sep 17 00:00:00 2001 From: edmonday Date: Fri, 6 Mar 2026 03:00:20 +0000 Subject: [PATCH 17/33] fix: remove border --- .../MultiStepForm/Screens/ScreenWrapper/ScreenWrapper.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/ScreenWrapper.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/ScreenWrapper.tsx index 660a46485f9..5182ffa0dd3 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/ScreenWrapper.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/ScreenWrapper.tsx @@ -23,12 +23,12 @@ export function ScreenWrapper({ - + Date: Fri, 6 Mar 2026 03:13:05 +0000 Subject: [PATCH 18/33] fix: add test --- .../MultiStepForm/MultiStepForm.tsx | 1 - .../ScreenWrapper/ScreenWrapper.spec.tsx | 103 ++++++++++++++++++ .../Screens/ScreenWrapper/ScreenWrapper.tsx | 10 ++ 3 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/ScreenWrapper.spec.tsx diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/MultiStepForm.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/MultiStepForm.tsx index 06225332216..51e0a1073ad 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/MultiStepForm.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/MultiStepForm.tsx @@ -79,7 +79,6 @@ export function MultiStepForm(): ReactElement { const { customizableMedia, templateCustomizationGuestFlow } = useFlags() const journeyId = journey?.id ?? '' - const link = `/journeys/${journeyId}` const { screens, diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/ScreenWrapper.spec.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/ScreenWrapper.spec.tsx new file mode 100644 index 00000000000..01da9ac2667 --- /dev/null +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/ScreenWrapper.spec.tsx @@ -0,0 +1,103 @@ +import { render, screen } from '@testing-library/react' + +import { ScreenWrapper } from './ScreenWrapper' + +describe('ScreenWrapper', () => { + it('renders title and subtitle', () => { + render( + +
content
+
+ ) + + expect(screen.getByTestId('ScreenWrapper')).toBeInTheDocument() + expect(screen.getAllByText('Test Title')[0]).toBeInTheDocument() + expect(screen.getAllByText('Test Subtitle')[0]).toBeInTheDocument() + }) + + it('renders children', () => { + render( + +
child content
+
+ ) + + expect(screen.getByTestId('child')).toBeInTheDocument() + }) + + it('renders footer when provided', () => { + render( + footer content} + > +
content
+
+ ) + + expect(screen.getByTestId('footer')).toBeInTheDocument() + }) + + it('does not render footer when not provided', () => { + render( + +
content
+
+ ) + + expect(screen.queryByTestId('footer')).not.toBeInTheDocument() + }) + + it('renders mobileTitle when provided', () => { + render( + +
content
+
+ ) + + expect(screen.getByText('Desktop Title')).toBeInTheDocument() + expect(screen.getByText('Mobile Title')).toBeInTheDocument() + }) + + it('falls back to title when mobileTitle is not provided', () => { + render( + +
content
+
+ ) + + const titleElements = screen.getAllByText('Shared Title') + expect(titleElements).toHaveLength(2) + }) + + it('renders mobileSubtitle when provided', () => { + render( + +
content
+
+ ) + + expect(screen.getByText('Desktop Subtitle')).toBeInTheDocument() + expect(screen.getByText('Mobile Subtitle')).toBeInTheDocument() + }) + + it('falls back to subtitle when mobileSubtitle is not provided', () => { + render( + +
content
+
+ ) + + const subtitleElements = screen.getAllByText('Shared Subtitle') + expect(subtitleElements).toHaveLength(2) + }) +}) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/ScreenWrapper.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/ScreenWrapper.tsx index 5182ffa0dd3..b8e706ab079 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/ScreenWrapper.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/ScreenWrapper.tsx @@ -11,6 +11,16 @@ interface ScreenWrapperProps { children: ReactNode } +/** + * Wraps a multi-step form screen with a responsive title, subtitle, and optional footer. + * + * @param title - The heading displayed on desktop viewports. + * @param mobileTitle - Optional heading override for mobile viewports. Falls back to `title`. + * @param subtitle - The subheading displayed on desktop viewports. + * @param mobileSubtitle - Optional subheading override for mobile viewports. Falls back to `subtitle`. + * @param footer - Optional content rendered below the children. + * @param children - The main screen content. + */ export function ScreenWrapper({ title, mobileTitle, From a1d426520ba78844abfbb0f5edd6a1f817659fc4 Mon Sep 17 00:00:00 2001 From: edmonday Date: Fri, 6 Mar 2026 03:23:08 +0000 Subject: [PATCH 19/33] test: fix tests --- .../MultiStepForm/MultiStepForm.spec.tsx | 154 +----------------- .../Screens/DoneScreen/DoneScreen.spec.tsx | 2 +- 2 files changed, 2 insertions(+), 154 deletions(-) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/MultiStepForm.spec.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/MultiStepForm.spec.tsx index 58ddbb74cdf..8cf69ef0c73 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/MultiStepForm.spec.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/MultiStepForm.spec.tsx @@ -131,15 +131,10 @@ jest.mock('./Screens', () => ({ ), - DoneScreen: ({ - handleScreenNavigation - }: { - handleScreenNavigation: (screen: string) => void - }) => ( + DoneScreen: () => (

Done Screen

+
) })) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/MultiStepForm.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/MultiStepForm.tsx index 51e0a1073ad..8c9c371c37c 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/MultiStepForm.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/MultiStepForm.tsx @@ -35,35 +35,19 @@ export const MULTI_STEP_FORM_MIN_HEIGHT = 900 function renderScreen( screen: CustomizationScreen, - handleNext: (overrideJourneyId?: string) => void, + handleNext: (overrideJourneyId?: string) => void ): ReactElement { switch (screen) { case 'language': - return ( - - ) + return case 'text': - return ( - - ) + return case 'links': - return ( - - ) + return case 'media': return case 'social': - return ( - - ) + return case 'done': return default: @@ -135,10 +119,7 @@ export function MultiStepForm(): ReactElement { overflow: 'hidden' }} > - + {(hasEditableText || hasCustomizableLinks || hasCustomizableMedia) && ( diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.spec.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.spec.tsx index c91b42a9c42..62b54da7a74 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.spec.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.spec.tsx @@ -162,9 +162,7 @@ describe('LanguageScreen', () => { value={{ journey: nonTemplateJourney, variant: 'customize' }} > - + @@ -199,9 +197,7 @@ describe('LanguageScreen', () => { - + @@ -301,9 +297,7 @@ describe('LanguageScreen', () => { value={{ journey: journeyWithFromTemplateId, variant: 'admin' }} > - + @@ -504,9 +498,7 @@ describe('LanguageScreen', () => { }} > - + @@ -559,9 +551,7 @@ describe('LanguageScreen', () => { - + @@ -619,9 +609,7 @@ describe('LanguageScreen', () => { value={{ journey: journeyWithImage, variant: 'admin' }} > - + @@ -643,9 +631,7 @@ describe('LanguageScreen', () => { - + diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx index ef30671784f..3c391807ded 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx @@ -1,3 +1,4 @@ +import Box from '@mui/material/Box' import FormControl from '@mui/material/FormControl' import Stack from '@mui/material/Stack' import Typography from '@mui/material/Typography' @@ -25,14 +26,13 @@ import { CardsPreview, EDGE_FADE_PX } from '../LinksScreen/CardsPreview' import { ScreenWrapper } from '../ScreenWrapper' import { JourneyCustomizeTeamSelect } from './JourneyCustomizeTeamSelect' -import Box from '@mui/material/Box' interface LanguageScreenProps { handleNext: (overrideJourneyId?: string) => void } export function LanguageScreen({ - handleNext, + handleNext }: LanguageScreenProps): ReactElement { const { templateCustomizationGuestFlow } = useFlags() const { t } = useTranslation('journeys-ui') @@ -251,9 +251,7 @@ export function LanguageScreen({ name: language?.name, slug: language?.slug }))} - onChange={(value) => - setFieldValue('languageSelect', value) - } + onChange={(value) => setFieldValue('languageSelect', value)} /> {isSignedIn && ( <> diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/LinksScreen.spec.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/LinksScreen.spec.tsx index 9918d1b53f5..89bb26e4190 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/LinksScreen.spec.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/LinksScreen.spec.tsx @@ -132,9 +132,7 @@ describe('LinksScreen', () => { render( - + ) @@ -290,9 +288,7 @@ describe('LinksScreen', () => { - + ) @@ -398,9 +394,7 @@ describe('LinksScreen', () => { - + ) @@ -467,9 +461,7 @@ describe('LinksScreen', () => { variant: 'admin' }} > - + ) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/LinksScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/LinksScreen.tsx index 654f460dacb..8f644536670 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/LinksScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/LinksScreen.tsx @@ -210,35 +210,32 @@ export function LinksScreen({ handleNext }: LinksScreenProps): ReactElement { return acc }, {})} validationSchema={object().shape( - links.reduce>>( - (acc, link) => { - if (link.linkType === 'email') { - acc[link.id] = string().email(t('Enter a valid email')) - } else if (link.linkType === 'phone') { - acc[`${link.id}__cc`] = string().test( - 'valid-cc', - t('Enter a valid calling code'), - (val) => { - if (val == null || val.trim() === '') return false - const normalized = val.startsWith('+') ? val : `+${val}` - return countries.some((c) => c.callingCode === normalized) - } - ) - acc[`${link.id}__local`] = string().test( - 'valid-local', - t('Enter a valid phone number'), - (val) => - val == null || - val.trim() === '' || - /^[0-9\s\-()]+$/.test(val.trim()) - ) - } else { - acc[link.id] = string().url(t('Enter a valid URL')) - } - return acc - }, - {} - ) + links.reduce>>((acc, link) => { + if (link.linkType === 'email') { + acc[link.id] = string().email(t('Enter a valid email')) + } else if (link.linkType === 'phone') { + acc[`${link.id}__cc`] = string().test( + 'valid-cc', + t('Enter a valid calling code'), + (val) => { + if (val == null || val.trim() === '') return false + const normalized = val.startsWith('+') ? val : `+${val}` + return countries.some((c) => c.callingCode === normalized) + } + ) + acc[`${link.id}__local`] = string().test( + 'valid-local', + t('Enter a valid phone number'), + (val) => + val == null || + val.trim() === '' || + /^[0-9\s\-()]+$/.test(val.trim()) + ) + } else { + acc[link.id] = string().url(t('Enter a valid URL')) + } + return acc + }, {}) )} validateOnSubmit={false} onSubmit={handleFormSubmit} diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/MediaScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/MediaScreen.tsx index 20b0895071f..470037d11ba 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/MediaScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/MediaScreen.tsx @@ -66,31 +66,28 @@ export function MediaScreen({ handleNext }: MediaScreenProps): ReactElement { setSelectedCardBlockId(getCardBlockIdFromStep(step)) } return ( - handleNext()} - ariaLabel={t('Next')} - loading={hasActiveUploads} - /> - } - > - {showLogo && } - - {showImages && ( - - )} - {showVideos && } - + handleNext()} + ariaLabel={t('Next')} + loading={hasActiveUploads} + /> + } + > + {showLogo && } + + {showImages && ( + + )} + {showVideos && } + ) } diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/Sections/CardsSection/CardsSection.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/Sections/CardsSection/CardsSection.tsx index 220dfe02ce7..dd26d14b5be 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/Sections/CardsSection/CardsSection.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/Sections/CardsSection/CardsSection.tsx @@ -6,8 +6,8 @@ import { ReactElement } from 'react' import { TreeBlock } from '@core/journeys/ui/block' import { TemplateCardPreview } from '@core/journeys/ui/TemplateView/TemplatePreviewTabs/TemplateCardPreview/TemplateCardPreview' -import { GetJourney_journey_blocks_StepBlock as StepBlock } from '@core/journeys/ui/useJourneyQuery/__generated__/GetJourney' import { OVERFLOW_PX } from '@core/journeys/ui/TemplateView/TemplatePreviewTabs/TemplateCardPreview/templateCardPreviewConfig' +import { GetJourney_journey_blocks_StepBlock as StepBlock } from '@core/journeys/ui/useJourneyQuery/__generated__/GetJourney' interface CardsSectionProps { customizableSteps: Array> diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/SocialScreen/SocialScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/SocialScreen/SocialScreen.tsx index b7da567ff76..e61b47aaa6c 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/SocialScreen/SocialScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/SocialScreen/SocialScreen.tsx @@ -13,38 +13,34 @@ interface SocialScreenProps { handleNext: (overrideJourneyId?: string) => void } -export function SocialScreen({ - handleNext, -}: SocialScreenProps): ReactElement { +export function SocialScreen({ handleNext }: SocialScreenProps): ReactElement { const { t } = useTranslation('apps-journeys-admin') return ( - handleNext()} - ariaLabel={t('Done')} - /> - } + handleNext()} + ariaLabel={t('Done')} + /> + } + > + - - - - - - + + + + + ) } diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.spec.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.spec.tsx index 025a46aec3d..0242ccec144 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.spec.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.spec.tsx @@ -38,9 +38,7 @@ describe('TextScreen', () => { render( - + ) @@ -54,9 +52,7 @@ describe('TextScreen', () => { render( - + ) @@ -113,9 +109,7 @@ describe('TextScreen', () => { render( - + ) @@ -144,9 +138,7 @@ describe('TextScreen', () => { render( - + ) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx index a0b4df0a992..86860b6274f 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx @@ -126,9 +126,7 @@ interface TextScreenProps { handleNext: (overrideJourneyId?: string) => void } -export function TextScreen({ - handleNext, -}: TextScreenProps): ReactElement { +export function TextScreen({ handleNext }: TextScreenProps): ReactElement { const { t } = useTranslation() const { journey } = useJourney() const [journeyCustomizationFieldUpdate, { loading: isSubmitting }] = diff --git a/libs/journeys/ui/src/components/TemplateView/TemplatePreviewTabs/TemplateCardPreview/TemplateCardPreview.tsx b/libs/journeys/ui/src/components/TemplateView/TemplatePreviewTabs/TemplateCardPreview/TemplateCardPreview.tsx index b229efbc257..a2aa3310529 100644 --- a/libs/journeys/ui/src/components/TemplateView/TemplatePreviewTabs/TemplateCardPreview/TemplateCardPreview.tsx +++ b/libs/journeys/ui/src/components/TemplateView/TemplatePreviewTabs/TemplateCardPreview/TemplateCardPreview.tsx @@ -183,7 +183,7 @@ export function TemplateCardPreview({ width: { xs: getSpacerWidth(cardWidth.xs, config.breakpoints.xs), sm: getSpacerWidth(cardWidth.sm, config.breakpoints.sm) - }, + } }} /> )} diff --git a/libs/journeys/ui/src/components/TemplateView/TemplatePreviewTabs/TemplateCardPreview/index.ts b/libs/journeys/ui/src/components/TemplateView/TemplatePreviewTabs/TemplateCardPreview/index.ts index 55aad589956..be44333288c 100644 --- a/libs/journeys/ui/src/components/TemplateView/TemplatePreviewTabs/TemplateCardPreview/index.ts +++ b/libs/journeys/ui/src/components/TemplateView/TemplatePreviewTabs/TemplateCardPreview/index.ts @@ -2,4 +2,4 @@ export { TemplateCardPreview } from './TemplateCardPreview' export { TemplateCardPreviewItem, type TemplateCardPreviewItemProps -} from './TemplateCardPreviewItem/TemplateCardPreviewItem' \ No newline at end of file +} from './TemplateCardPreviewItem/TemplateCardPreviewItem' diff --git a/libs/locales/en/apps-journeys-admin.json b/libs/locales/en/apps-journeys-admin.json index 2cdb9f03804..03a813ad353 100644 --- a/libs/locales/en/apps-journeys-admin.json +++ b/libs/locales/en/apps-journeys-admin.json @@ -1003,21 +1003,20 @@ "Link to project": "Link to project", "Feature Video Started": "Feature Video Started", "Feature Video Ended": "Feature Video Ended", - "Edit Manually": "Edit Manually", - "Ready to Share!": "Ready to Share!", + "Ready to share!": "Ready to share!", "Share your unique link on any platform.": "Share your unique link on any platform.", "Go To Projects Dashboard": "Go To Projects Dashboard", "Share!": "Share!", "{{count}} more cards_one": "{{count}} more cards", "{{count}} more cards_other": "{{count}} more cards", "Edit": "Edit", - "Links": "Links", - "This content contains buttons linking to external sites. Check them and update the links below.": "This content contains buttons linking to external sites. Check them and update the links below.", - "Buttons here point to external sites. Check and update the links.": "Buttons here point to external sites. Check and update the links.", "Enter a valid email": "Enter a valid email", "Enter a valid calling code": "Enter a valid calling code", "Enter a valid phone number": "Enter a valid phone number", "Enter a valid URL": "Enter a valid URL", + "Links": "Links", + "This content contains buttons linking to external sites. Check them and update the links below.": "This content contains buttons linking to external sites. Check them and update the links below.", + "Buttons here point to external sites. Check and update the links.": "Buttons here point to external sites. Check and update the links.", "Next": "Next", "Replace the links": "Replace the links", "Media": "Media", @@ -1030,9 +1029,8 @@ "Supports JPG, PNG, and GIF files.": "Supports JPG, PNG, and GIF files.", "Upload a video to see a preview here": "Upload a video to see a preview here", "Max size is 1 GB": "Max size is 1 GB", - "Final Details": "Final Details", - "Customize how your invite appears when shared on social media.": "Customize how your invite appears when shared on social media.", - "This is how your content will appear when shared on social media.": "This is how your content will appear when shared on social media.", + "Social Media": "Social Media", + "This is how your content will look on social media.": "This is how your content will look on social media.", "Failed to update social image, please try again later": "Failed to update social image, please try again later", "Social image updated": "Social image updated", "your.nextstep.is": "your.nextstep.is", diff --git a/libs/locales/en/journeys-ui.json b/libs/locales/en/journeys-ui.json index ef9ce200f88..7aeadf4622f 100644 --- a/libs/locales/en/journeys-ui.json +++ b/libs/locales/en/journeys-ui.json @@ -4,7 +4,7 @@ "Get Started": "Get Started", "A few quick edits and your template will be ready to share.": "A few quick edits and your template will be ready to share.", "A few quick edits and it's ready to share!": "A few quick edits and it's ready to share!", + "Next": "Next", "Select a language": "Select a language", - "Select a team": "Select a team", - "Next": "Next" + "Select a team": "Select a team" } From 863f28db77f7539fe435c7ec3c5610dda049fc8b Mon Sep 17 00:00:00 2001 From: edmonday Date: Sun, 8 Mar 2026 20:06:16 +0000 Subject: [PATCH 23/33] fix: add back the switch --- .../MultiStepForm/Screens/DoneScreen/DoneScreen.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx index 40bde723119..b82f314b1c6 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx @@ -93,6 +93,7 @@ export function DoneScreen(): ReactElement { } }} /> + ) From dba14e61c0c83e006eaaa57cef114d13087a0e38 Mon Sep 17 00:00:00 2001 From: edmonday Date: Sun, 8 Mar 2026 20:14:24 +0000 Subject: [PATCH 24/33] fix: remove validateOnSubmit from LinksScreen --- .../MultiStepForm/Screens/LinksScreen/LinksScreen.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/LinksScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/LinksScreen.tsx index 8f644536670..22fffd3994e 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/LinksScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/LinksScreen.tsx @@ -237,7 +237,6 @@ export function LinksScreen({ handleNext }: LinksScreenProps): ReactElement { return acc }, {}) )} - validateOnSubmit={false} onSubmit={handleFormSubmit} validateOnMount > From 95329dac5ee988687e77cb1c921a3a6d0126435b Mon Sep 17 00:00:00 2001 From: edmonday Date: Sun, 8 Mar 2026 20:32:32 +0000 Subject: [PATCH 25/33] fix: add response section --- .../Screens/DoneScreen/DoneScreen.tsx | 88 ++++++++++++------- 1 file changed, 54 insertions(+), 34 deletions(-) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx index b82f314b1c6..5dbe09264f6 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx @@ -54,46 +54,66 @@ export function DoneScreen(): ReactElement { )} - - } + sx={{ + borderWidth: 2, + borderRadius: 2, height: 48, - borderRadius: 2 - } + width: { xs: '100%', sm: 216 }, + borderColor: 'secondary.light' + }} + > + {t('Preview')} + + + + - + > + + {t('Choose where responses go:')} + + + {t('Send to my email')} + + +
) From a8c32708bfbf4ab831b185700d6edc51dbdaefe9 Mon Sep 17 00:00:00 2001 From: edmonday Date: Sun, 8 Mar 2026 21:15:05 +0000 Subject: [PATCH 26/33] fix: resolved comments --- .../src/components/Editor/Toolbar/Items/ShareItem/ShareItem.tsx | 1 - .../TemplateCustomization/MultiStepForm/MultiStepForm.spec.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/apps/journeys-admin/src/components/Editor/Toolbar/Items/ShareItem/ShareItem.tsx b/apps/journeys-admin/src/components/Editor/Toolbar/Items/ShareItem/ShareItem.tsx index 7f0ff8d5418..ad300532b35 100644 --- a/apps/journeys-admin/src/components/Editor/Toolbar/Items/ShareItem/ShareItem.tsx +++ b/apps/journeys-admin/src/components/Editor/Toolbar/Items/ShareItem/ShareItem.tsx @@ -51,7 +51,6 @@ interface ShareItemProps { handleCloseMenu?: () => void handleKeepMounted?: () => void buttonVariant?: 'icon' | 'default' - buttonSx?: SxProps setHasOpenDialog?: (hasOpenDialog: boolean) => void buttonProps?: ComponentProps } diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/MultiStepForm.spec.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/MultiStepForm.spec.tsx index 92a7255d4c0..c546f63488d 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/MultiStepForm.spec.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/MultiStepForm.spec.tsx @@ -143,7 +143,6 @@ jest.mock('./Screens', () => ({ DoneScreen: () => (

Done Screen

-
) })) From 65a82d21655ec0b8647da10d2b454b37f1a0a1f0 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 21:19:15 +0000 Subject: [PATCH 27/33] fix: lint issues --- .../LanguageScreen/LanguageScreen.spec.tsx | 36 +++++-------------- 1 file changed, 9 insertions(+), 27 deletions(-) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.spec.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.spec.tsx index d1f3768f149..7bfbdffe96a 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.spec.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.spec.tsx @@ -225,9 +225,7 @@ describe('LanguageScreen', () => { value={{ journey: nonTemplateJourney, variant: 'customize' }} > - + @@ -271,9 +269,7 @@ describe('LanguageScreen', () => { - + @@ -374,9 +370,7 @@ describe('LanguageScreen', () => { value={{ journey: journeyWithFromTemplateId, variant: 'admin' }} > - + @@ -486,9 +480,7 @@ describe('LanguageScreen', () => { - + @@ -584,9 +576,7 @@ describe('LanguageScreen', () => { - + @@ -774,9 +764,7 @@ describe('LanguageScreen', () => { }} > - + @@ -838,9 +826,7 @@ describe('LanguageScreen', () => { - + @@ -897,9 +883,7 @@ describe('LanguageScreen', () => { value={{ journey: journeyWithImage, variant: 'admin' }} > - + @@ -922,9 +906,7 @@ describe('LanguageScreen', () => { - + From 40da71c38f00e5f245e7370a784b300e9f08f525 Mon Sep 17 00:00:00 2001 From: edmonday Date: Sun, 8 Mar 2026 22:59:20 +0000 Subject: [PATCH 28/33] fix: add guest flow flag on button --- .../MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx index 6b1257259f8..db068ce2b66 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx @@ -310,7 +310,9 @@ export function LanguageScreen({ label={t('Next')} onClick={() => formikHandleSubmit()} disabled={ - (templateCustomizationGuestFlow && !isSignedIn) || loading + templateCustomizationGuestFlow == null || + !templateCustomizationGuestFlow || + loading } ariaLabel={t('Next')} /> From e33b412bc547cf5fcd8add0b4dc1f056926d52e5 Mon Sep 17 00:00:00 2001 From: edmonday Date: Mon, 9 Mar 2026 00:52:12 +0000 Subject: [PATCH 29/33] fix: positioning of google sheets button --- .../Screens/DoneScreen/DoneScreen.tsx | 68 ++++++++++--------- .../Screens/LanguageScreen/LanguageScreen.tsx | 2 - 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx index dd70f4847ff..2fd4f640f82 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx @@ -160,42 +160,48 @@ export function DoneScreen(): ReactElement { {t('Choose where responses go:')} - - {t('Send to my email')} - - - - - {t('Sync to Google Sheets')} - - + +
- {journey?.id != null && ( - - )} + {journey?.id != null && ( + + )} ) } diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx index db068ce2b66..976f92ebb38 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx @@ -26,7 +26,6 @@ import { useCurrentUserLazyQuery } from '../../../../../libs/useCurrentUserLazyQ import { useGetChildTemplateJourneyLanguages } from '../../../../../libs/useGetChildTemplateJourneyLanguages' import { useGetParentTemplateJourneyLanguages } from '../../../../../libs/useGetParentTemplateJourneyLanguages' import { useTeamCreateMutation } from '../../../../../libs/useTeamCreateMutation' -import { CustomizationScreen } from '../../../utils/getCustomizeFlowConfig' import { CustomizeFlowNextButton } from '../../CustomizeFlowNextButton' import { CardsPreview, EDGE_FADE_PX } from '../LinksScreen/CardsPreview' import { ScreenWrapper } from '../ScreenWrapper' @@ -378,7 +377,6 @@ export function LanguageScreen({ > {t('Select a team')} - Date: Mon, 9 Mar 2026 00:53:47 +0000 Subject: [PATCH 30/33] chore: remove box --- .../MultiStepForm/Screens/DoneScreen/DoneScreen.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx index 2fd4f640f82..e429fae1e2b 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx @@ -1,5 +1,4 @@ import { gql, useQuery } from '@apollo/client' -import Box from '@mui/material/Box' import Button from '@mui/material/Button' import Stack from '@mui/material/Stack' import Typography from '@mui/material/Typography' From 63f30f34ba53c1aa672e92bfffa3352327c3ca99 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 00:57:59 +0000 Subject: [PATCH 31/33] fix: lint issues --- libs/locales/en/apps-journeys-admin.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/libs/locales/en/apps-journeys-admin.json b/libs/locales/en/apps-journeys-admin.json index 5938b8569d7..21cd288d3fe 100644 --- a/libs/locales/en/apps-journeys-admin.json +++ b/libs/locales/en/apps-journeys-admin.json @@ -1005,17 +1005,15 @@ "Feature Video Ended": "Feature Video Ended", "Ready to share!": "Ready to share!", "Share your unique link on any platform.": "Share your unique link on any platform.", + "Go To Projects Dashboard": "Go To Projects Dashboard", "Choose where responses go:": "Choose where responses go:", - "Choose Response Destination:": "Choose Response Destination:", "Send to my email": "Send to my email", "Edit Google Sheets sync": "Edit Google Sheets sync", "Edit": "Edit", "Sync": "Sync", - "Go To Projects Dashboard": "Go To Projects Dashboard", "Share!": "Share!", "{{count}} more cards_one": "{{count}} more cards", "{{count}} more cards_other": "{{count}} more cards", - "Edit": "Edit", "Enter a valid email": "Enter a valid email", "Enter a valid calling code": "Enter a valid calling code", "Enter a valid phone number": "Enter a valid phone number", From 45061a2641e26a0960ec2b3606438336439e78ff Mon Sep 17 00:00:00 2001 From: edmonday Date: Mon, 9 Mar 2026 01:29:28 +0000 Subject: [PATCH 32/33] fix: hide google sheets sync --- .../GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.tsx b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.tsx index 4d29bb0e2c3..1166d708a40 100644 --- a/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.tsx +++ b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.tsx @@ -299,6 +299,11 @@ export function GoogleSheetsSyncDialog({ (sync) => sync.deletedAt != null ) + const syncsResolved = syncsCalled && !syncsLoading + const hasNoSyncs = + syncsResolved && activeSyncs.length === 0 && historySyncs.length === 0 + const hideMainDialog = !syncsResolved || hasNoSyncs + // Auto-open "Add Google Sheets Sync" dialog if there are no syncs useEffect(() => { if (!open) return @@ -533,6 +538,7 @@ export function GoogleSheetsSyncDialog({ ? `https://docs.google.com/spreadsheets/d/${syncResult.spreadsheetId}` : null) setGoogleDialogOpen(false) + if (hideMainDialog) onClose() actions.resetForm() if (typeof window !== 'undefined' && spreadsheetUrl != null) { window.open(spreadsheetUrl, '_blank', 'noopener,noreferrer') @@ -725,7 +731,7 @@ export function GoogleSheetsSyncDialog({ return ( <> { setGoogleDialogOpen(false) + if (hideMainDialog) onClose() resetForm() }} dialogTitle={{ @@ -1152,6 +1159,7 @@ export function GoogleSheetsSyncDialog({ color="primary" onClick={() => { setGoogleDialogOpen(false) + if (hideMainDialog) onClose() resetForm() }} sx={{ From 15b617b5ac574a1ef78226b375f513e0ea283e00 Mon Sep 17 00:00:00 2001 From: edmonday Date: Mon, 9 Mar 2026 03:09:09 +0000 Subject: [PATCH 33/33] refactor: simplify GoogleSheetsSyncDialog by removing unused queries and components --- .../AddSyncFormDialog/AddSyncFormDialog.tsx | 479 ++++++ .../AddSyncFormDialog/index.ts | 1 + .../DeleteSyncDialog/DeleteSyncDialog.tsx | 74 + .../DeleteSyncDialog/index.ts | 1 + .../GoogleSheetsSyncDialog.spec.tsx | 7 + .../GoogleSheetsSyncDialog.tsx | 1315 ++--------------- .../MobileSyncCard/MobileSyncCard.tsx | 145 ++ .../MobileSyncCard/index.ts | 1 + .../SyncTable/SyncTable.tsx | 244 +++ .../GoogleSheetsSyncDialog/SyncTable/index.ts | 1 + .../GoogleSheetsSyncDialog/graphql.ts | 84 ++ .../libs/googleSheetsSyncUtils.ts | 15 + .../libs/useGooglePicker/index.ts | 2 + .../libs/useGooglePicker/useGooglePicker.ts | 156 ++ .../libs/useGoogleSheetsSync/index.ts | 5 + .../useGoogleSheetsSync.ts | 142 ++ .../GoogleSheetsSyncDialog/types.ts | 35 + 17 files changed, 1478 insertions(+), 1229 deletions(-) create mode 100644 apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/AddSyncFormDialog/AddSyncFormDialog.tsx create mode 100644 apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/AddSyncFormDialog/index.ts create mode 100644 apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/DeleteSyncDialog/DeleteSyncDialog.tsx create mode 100644 apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/DeleteSyncDialog/index.ts create mode 100644 apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/MobileSyncCard/MobileSyncCard.tsx create mode 100644 apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/MobileSyncCard/index.ts create mode 100644 apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/SyncTable/SyncTable.tsx create mode 100644 apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/SyncTable/index.ts create mode 100644 apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/graphql.ts create mode 100644 apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/libs/googleSheetsSyncUtils.ts create mode 100644 apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/libs/useGooglePicker/index.ts create mode 100644 apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/libs/useGooglePicker/useGooglePicker.ts create mode 100644 apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/libs/useGoogleSheetsSync/index.ts create mode 100644 apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/libs/useGoogleSheetsSync/useGoogleSheetsSync.ts create mode 100644 apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/types.ts diff --git a/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/AddSyncFormDialog/AddSyncFormDialog.tsx b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/AddSyncFormDialog/AddSyncFormDialog.tsx new file mode 100644 index 00000000000..2a046d2646a --- /dev/null +++ b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/AddSyncFormDialog/AddSyncFormDialog.tsx @@ -0,0 +1,479 @@ +import FolderIcon from '@mui/icons-material/Folder' +import Box from '@mui/material/Box' +import Button from '@mui/material/Button' +import FormControl from '@mui/material/FormControl' +import MenuItem from '@mui/material/MenuItem' +import Select from '@mui/material/Select' +import TextField from '@mui/material/TextField' +import Typography from '@mui/material/Typography' +import { Form, Formik, FormikHelpers } from 'formik' +import { useTranslation } from 'next-i18next' +import { ReactElement } from 'react' +import { object, string } from 'yup' + +import { Dialog } from '@core/shared/ui/Dialog' +import ChevronDown from '@core/shared/ui/icons/ChevronDown' + +import { getGoogleOAuthUrl } from '../../../../../libs/googleOAuthUrl' + +import { SyncFormValues } from '../types' + +interface Integration { + __typename: string + id: string + accountEmail?: string | null +} + +interface AddSyncFormDialogProps { + open: boolean + onClose: () => void + pickerActive: boolean + integrations: Integration[] + teamId: string | undefined + journeyTitle: string | undefined + sheetsLoading: boolean + onSubmit: ( + values: SyncFormValues, + actions: FormikHelpers + ) => Promise + onOpenDrivePicker: ( + mode: 'folder' | 'sheet', + integrationId: string | undefined, + setFieldValue: (field: string, value: unknown) => void + ) => Promise + routerAsPath: string +} + +export function AddSyncFormDialog({ + open, + onClose, + pickerActive, + integrations, + teamId, + journeyTitle, + sheetsLoading, + onSubmit, + onOpenDrivePicker, + routerAsPath +}: AddSyncFormDialogProps): ReactElement { + const { t } = useTranslation('apps-journeys-admin') + + const googleIntegrations = integrations.filter( + (integration) => integration.__typename === 'IntegrationGoogle' + ) + + const validationSchema = object().shape({ + integrationId: string().required(t('Integration account is required')), + sheetName: string().required(t('Sheet tab name is required')), + spreadsheetTitle: string().when( + 'googleMode', + (googleMode: unknown, schema) => + googleMode === 'create' + ? schema.required(t('Sheet name is required')) + : schema.notRequired() + ) + }) + + const initialValues: SyncFormValues = { + integrationId: '', + googleMode: '', + spreadsheetTitle: '', + sheetName: '', + folderId: undefined, + folderName: undefined, + existingSpreadsheetId: undefined, + existingSpreadsheetName: undefined + } + + return ( + + {({ + values, + handleChange, + handleBlur, + handleSubmit, + errors, + touched, + resetForm, + setFieldValue + }) => ( + { + onClose() + resetForm() + }} + dialogTitle={{ + title: t('Sync to Google Sheets'), + closeButton: true + }} + divider={false} + maxWidth="sm" + sx={{ + zIndex: pickerActive ? 1 : undefined + }} + dialogActionChildren={ + + + + + } + > +
+ + + + + + + + {t('Google Account')} + + + {touched.integrationId != null && + errors.integrationId != null && ( + + {errors.integrationId } + + )} + + + + + + + {t('Spreadsheet Setup')} + + + + + {values.googleMode !== '' && ( + + {values.googleMode === 'create' ? ( + + + + {t( + 'Optional: Choose a folder in Google Drive to store your new spreadsheet.' + )} + + + ) : ( + + + + {t( + 'Select a spreadsheet from Google Drive to sync your data.' + )} + + + )} + {values.googleMode === 'create' && ( + + )} + + + )} + + +
+
+ )} +
+ ) +} + diff --git a/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/AddSyncFormDialog/index.ts b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/AddSyncFormDialog/index.ts new file mode 100644 index 00000000000..589b6e86196 --- /dev/null +++ b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/AddSyncFormDialog/index.ts @@ -0,0 +1 @@ +export { AddSyncFormDialog } from './AddSyncFormDialog' diff --git a/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/DeleteSyncDialog/DeleteSyncDialog.tsx b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/DeleteSyncDialog/DeleteSyncDialog.tsx new file mode 100644 index 00000000000..595d705263f --- /dev/null +++ b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/DeleteSyncDialog/DeleteSyncDialog.tsx @@ -0,0 +1,74 @@ +import Box from '@mui/material/Box' +import Button from '@mui/material/Button' +import Typography from '@mui/material/Typography' +import { useTranslation } from 'next-i18next' +import { ReactElement } from 'react' + +import { Dialog } from '@core/shared/ui/Dialog' + +interface DeleteSyncDialogProps { + syncIdPendingDelete: string | null + deletingSyncId: string | null + onClose: () => void + onDelete: (syncId: string) => void +} + +export function DeleteSyncDialog({ + syncIdPendingDelete, + deletingSyncId, + onClose, + onDelete +}: DeleteSyncDialogProps): ReactElement { + const { t } = useTranslation('apps-journeys-admin') + + return ( + { + if (deletingSyncId != null) return + onClose() + }} + dialogTitle={{ + title: t('Delete Google Sheets Sync'), + closeButton: true + }} + divider={false} + maxWidth="sm" + dialogActionChildren={ + + + + + } + > + + + {t( + "Data will no longer update in your Google Sheet if you delete this sync. Existing data will remain, but new updates won't be sent." + )} + + + {t('You will have to start a new sync to re-start syncing.')} + + + + ) +} diff --git a/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/DeleteSyncDialog/index.ts b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/DeleteSyncDialog/index.ts new file mode 100644 index 00000000000..b3e9421e150 --- /dev/null +++ b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/DeleteSyncDialog/index.ts @@ -0,0 +1 @@ +export { DeleteSyncDialog } from './DeleteSyncDialog' diff --git a/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.spec.tsx b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.spec.tsx index 04ef4603112..7013e46965b 100644 --- a/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.spec.tsx +++ b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.spec.tsx @@ -9,6 +9,13 @@ jest.mock('next-i18next', () => ({ }) })) +jest.mock('next-firebase-auth', () => ({ + useUser: () => ({ + clientInitialized: true, + id: 'user1' + }) +})) + const mockEnqueueSnackbar = jest.fn() jest.mock('notistack', () => ({ diff --git a/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.tsx b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.tsx index 1166d708a40..59267943a61 100644 --- a/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.tsx +++ b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/GoogleSheetsSyncDialog.tsx @@ -1,155 +1,38 @@ -import { gql, useLazyQuery, useMutation, useQuery } from '@apollo/client' -import FolderIcon from '@mui/icons-material/Folder' -import LaunchIcon from '@mui/icons-material/Launch' -import NorthEastIcon from '@mui/icons-material/NorthEast' -import RefreshIcon from '@mui/icons-material/Refresh' +import { useMutation, useQuery } from '@apollo/client' import Accordion from '@mui/material/Accordion' import AccordionDetails from '@mui/material/AccordionDetails' import AccordionSummary from '@mui/material/AccordionSummary' import Box from '@mui/material/Box' import Button from '@mui/material/Button' -import Chip from '@mui/material/Chip' import CircularProgress from '@mui/material/CircularProgress' -import FormControl from '@mui/material/FormControl' -import IconButton from '@mui/material/IconButton' -import Link from '@mui/material/Link' -import MenuItem from '@mui/material/MenuItem' -import Select from '@mui/material/Select' import { useTheme } from '@mui/material/styles' -import Table from '@mui/material/Table' -import TableBody from '@mui/material/TableBody' -import TableCell from '@mui/material/TableCell' -import TableContainer from '@mui/material/TableContainer' -import TableHead from '@mui/material/TableHead' -import TableRow from '@mui/material/TableRow' -import TextField from '@mui/material/TextField' -import Tooltip from '@mui/material/Tooltip' import Typography from '@mui/material/Typography' import useMediaQuery from '@mui/material/useMediaQuery' -import { format } from 'date-fns' -import { Form, Formik, FormikHelpers, FormikValues } from 'formik' +import { FormikHelpers } from 'formik' import { useRouter } from 'next/router' import { useUser } from 'next-firebase-auth' import { useTranslation } from 'next-i18next' import { useSnackbar } from 'notistack' -import { KeyboardEvent, ReactElement, useEffect, useState } from 'react' -import { object, string } from 'yup' +import { ReactElement, useEffect, useState } from 'react' import { Dialog } from '@core/shared/ui/Dialog' import ChevronDown from '@core/shared/ui/icons/ChevronDown' import Plus2Icon from '@core/shared/ui/icons/Plus2' -import Trash2Icon from '@core/shared/ui/icons/Trash2' -import { getGoogleOAuthUrl } from '../../../../libs/googleOAuthUrl' import { useIntegrationQuery } from '../../../../libs/useIntegrationQuery/useIntegrationQuery' -const GET_JOURNEY_CREATED_AT = gql` - query GoogleSheetsSyncDialogJourney($id: ID!) { - journey: adminJourney(id: $id, idType: databaseId) { - id - createdAt - title - team { - id - } - } - } -` - -const GET_GOOGLE_PICKER_TOKEN = gql` - query IntegrationGooglePickerToken($integrationId: ID!) { - integrationGooglePickerToken(integrationId: $integrationId) - } -` - -const GET_GOOGLE_SHEETS_SYNCS = gql` - query GoogleSheetsSyncs($filter: GoogleSheetsSyncsFilter!) { - googleSheetsSyncs(filter: $filter) { - id - spreadsheetId - sheetName - email - deletedAt - createdAt - integration { - __typename - id - ... on IntegrationGoogle { - accountEmail - } - } - } - } -` - -const EXPORT_TO_SHEETS = gql` - mutation JourneyVisitorExportToGoogleSheet( - $journeyId: ID! - $destination: JourneyVisitorGoogleSheetDestinationInput! - $integrationId: ID! - $timezone: String - ) { - journeyVisitorExportToGoogleSheet( - journeyId: $journeyId - destination: $destination - integrationId: $integrationId - timezone: $timezone - ) { - spreadsheetId - spreadsheetUrl - sheetName - } - } -` - -const DELETE_GOOGLE_SHEETS_SYNC = gql` - mutation GoogleSheetsSyncDialogDelete($id: ID!) { - googleSheetsSyncDelete(id: $id) { - id - } - } -` - -const BACKFILL_GOOGLE_SHEETS_SYNC = gql` - mutation GoogleSheetsSyncDialogBackfill($id: ID!) { - googleSheetsSyncBackfill(id: $id) { - id - } - } -` - -const INTEGRATION_GOOGLE_CREATE = gql` - mutation IntegrationGoogleCreate($input: IntegrationGoogleCreateInput!) { - integrationGoogleCreate(input: $input) { - id - } - } -` - -interface GoogleSheetsSyncItem { - id: string - spreadsheetId: string | null - sheetName: string | null - email: string | null - deletedAt: string | null - createdAt: string - integration: { - __typename: string - id: string - accountEmail?: string | null - } | null -} - -interface GoogleSheetsSyncsQueryData { - googleSheetsSyncs: GoogleSheetsSyncItem[] -} - -interface GoogleSheetsSyncsQueryVariables { - filter: { - journeyId?: string - integrationId?: string - } -} +import { AddSyncFormDialog } from './AddSyncFormDialog' +import { DeleteSyncDialog } from './DeleteSyncDialog' +import { + EXPORT_TO_SHEETS, + GET_JOURNEY_CREATED_AT, + INTEGRATION_GOOGLE_CREATE +} from './graphql' +import { MobileSyncCard } from './MobileSyncCard' +import { SyncTable } from './SyncTable' +import { SyncFormValues } from './types' +import { useGooglePicker } from './libs/useGooglePicker' +import { useGoogleSheetsSync } from './libs/useGoogleSheetsSync' interface GoogleSheetsSyncDialogProps { open: boolean @@ -178,36 +61,41 @@ export function GoogleSheetsSyncDialog({ }) const [googleDialogOpen, setGoogleDialogOpen] = useState(false) - const [pickerActive, setPickerActive] = useState(false) const [exportToSheets, { loading: sheetsLoading }] = useMutation(EXPORT_TO_SHEETS) - const [getPickerToken] = useLazyQuery(GET_GOOGLE_PICKER_TOKEN) - const [ + + const teamId = journeyData?.journey?.team?.id as string | undefined + + const { pickerActive, handleOpenDrivePicker } = useGooglePicker({ teamId }) + + const { loadSyncs, - { data: syncsData, loading: syncsLoading, called: syncsCalled } - ] = useLazyQuery( - GET_GOOGLE_SHEETS_SYNCS - ) - const [deleteSync] = useMutation(DELETE_GOOGLE_SHEETS_SYNC) - const [backfillSync] = useMutation(BACKFILL_GOOGLE_SHEETS_SYNC) + syncsLoading, + syncsCalled, + activeSyncs, + historySyncs, + syncsResolved, + hasNoSyncs, + deletingSyncId, + syncIdPendingDelete, + setSyncIdPendingDelete, + backfillingSyncId, + handleDeleteSync, + handleRequestDeleteSync, + handleBackfillSync + } = useGoogleSheetsSync({ journeyId, open }) + const [integrationGoogleCreate] = useMutation(INTEGRATION_GOOGLE_CREATE) - const [deletingSyncId, setDeletingSyncId] = useState(null) - const [syncIdPendingDelete, setSyncIdPendingDelete] = useState( - null - ) - const [backfillingSyncId, setBackfillingSyncId] = useState( - null - ) + const hideMainDialog = !syncsResolved || hasNoSyncs + const isGoogleActionDisabled = integrationsData == null async function handleIntegrationCreate(authCode: string): Promise { - const teamId = journeyData?.journey?.team?.id if (teamId == null || typeof window === 'undefined') return const redirectUri = `${window.location.origin}/api/integrations/google/callback` - // Clean URL params immediately to prevent re-triggering const newQuery = { ...router.query } delete newQuery.code delete newQuery.openSyncDialog @@ -243,6 +131,7 @@ export function GoogleSheetsSyncDialog({ } } + // Load syncs and handle OAuth return useEffect(() => { if (!open) return void loadSyncs({ @@ -252,7 +141,6 @@ export function GoogleSheetsSyncDialog({ const openSyncDialog = router.query.openSyncDialog === 'true' - // New flow: handle OAuth code exchange directly (wait for auth to settle) const authCode = router.query.code as string | undefined if (authCode != null && openSyncDialog) { if (user.clientInitialized) { @@ -261,7 +149,6 @@ export function GoogleSheetsSyncDialog({ return } - // Backwards compat: old flow via GoogleCreateIntegration page const integrationCreated = router.query.integrationCreated === 'true' if (integrationCreated && openSyncDialog) { const newQuery = { ...router.query } @@ -287,35 +174,15 @@ export function GoogleSheetsSyncDialog({ user.clientInitialized ]) - useEffect(() => { - if (open) return - if (deletingSyncId != null) return - setSyncIdPendingDelete(null) - }, [open, deletingSyncId]) - - const googleSheetsSyncs = syncsData?.googleSheetsSyncs ?? [] - const activeSyncs = googleSheetsSyncs.filter((sync) => sync.deletedAt == null) - const historySyncs = googleSheetsSyncs.filter( - (sync) => sync.deletedAt != null - ) - - const syncsResolved = syncsCalled && !syncsLoading - const hasNoSyncs = - syncsResolved && activeSyncs.length === 0 && historySyncs.length === 0 - const hideMainDialog = !syncsResolved || hasNoSyncs - - // Auto-open "Add Google Sheets Sync" dialog if there are no syncs + // Auto-open add dialog when no syncs exist useEffect(() => { if (!open) return - // Wait until the query has actually been executed at least once if (!syncsCalled) return if (syncsLoading) return - // Skip if we're already handling integration creation return flow const integrationCreated = router.query.integrationCreated === 'true' const hasAuthCode = router.query.code != null if (integrationCreated || hasAuthCode) return - // If there are no active or history syncs, open the add dialog directly if (activeSyncs.length === 0 && historySyncs.length === 0) { setGoogleDialogOpen(true) } @@ -328,180 +195,9 @@ export function GoogleSheetsSyncDialog({ router.query.integrationCreated ]) - function getStartedByLabel(sync: GoogleSheetsSyncItem): string { - if (sync.integration?.__typename === 'IntegrationGoogle') { - return sync.integration.accountEmail ?? sync.email ?? 'N/A' - } - - if (sync.email != null && sync.email !== '') return sync.email - - return 'N/A' - } - - function getSpreadsheetUrl(sync: GoogleSheetsSyncItem): string | null { - if (sync.spreadsheetId == null || sync.spreadsheetId === '') return null - return `https://docs.google.com/spreadsheets/d/${sync.spreadsheetId}` - } - - function handleOpenSyncRow(sync: GoogleSheetsSyncItem): void { - const spreadsheetUrl = getSpreadsheetUrl(sync) - if (spreadsheetUrl == null) { - enqueueSnackbar(t('Something went wrong, please try again!'), { - variant: 'error' - }) - return - } - - if (typeof window === 'undefined') return - - window.open(spreadsheetUrl, '_blank', 'noopener,noreferrer') - } - - function handleSyncRowKeyDown( - event: KeyboardEvent, - sync: GoogleSheetsSyncItem - ): void { - if (event.key === 'Enter') { - handleOpenSyncRow(sync) - return - } - - if (event.key === ' ') { - event.preventDefault() - handleOpenSyncRow(sync) - } - } - - async function handleOpenDrivePicker( - mode: 'folder' | 'sheet', - integrationId: string | undefined, - setFieldValue: (field: string, value: unknown) => void - ): Promise { - try { - const apiKey = process.env.NEXT_PUBLIC_FIREBASE_API_KEY - if (apiKey == null || apiKey === '') { - enqueueSnackbar(t('Missing Google API key'), { variant: 'error' }) - return - } - if (integrationId == null || integrationId === '') { - enqueueSnackbar(t('Select an integration account first'), { - variant: 'error' - }) - return - } - - let oauthToken: string | undefined | null - const teamId = journeyData?.journey?.team?.id - if (teamId != null) { - const { data: tokenData } = await getPickerToken({ - variables: { teamId, integrationId } - }) - oauthToken = tokenData?.integrationGooglePickerToken - } - if (oauthToken == null || oauthToken === '') { - enqueueSnackbar(t('Unable to authorize Google Picker'), { - variant: 'error' - }) - return - } - - await ensurePickerLoaded() - - // Mark picker as active to lower dialog z-index - setPickerActive(true) - - const googleAny: any = (window as any).google - const view = - mode === 'sheet' - ? new googleAny.picker.DocsView(googleAny.picker.ViewId.SPREADSHEETS) - : new googleAny.picker.DocsView( - googleAny.picker.ViewId.FOLDERS - ).setSelectFolderEnabled(true) - - const picker = new googleAny.picker.PickerBuilder() - .setOAuthToken(oauthToken) - .setDeveloperKey(apiKey) - .addView(view) - .setCallback((pickerData: any) => { - if (pickerData?.action === googleAny.picker.Action.PICKED) { - const doc = pickerData.docs?.[0] - if (doc != null) { - const docName = doc?.name ?? doc?.title ?? doc?.id ?? null - if (mode === 'sheet') { - setFieldValue('existingSpreadsheetId', doc.id) - setFieldValue('existingSpreadsheetName', docName ?? undefined) - } else { - setFieldValue('folderId', doc.id) - setFieldValue('folderName', docName ?? undefined) - } - } - } - - if ( - pickerData?.action === googleAny.picker.Action.PICKED || - pickerData?.action === googleAny.picker.Action.CANCEL - ) { - setPickerActive(false) - } - }) - .build() - - picker.setVisible(true) - elevatePickerZIndexWithRetries() - } catch (err) { - enqueueSnackbar(t('Failed to open Google Picker'), { variant: 'error' }) - setPickerActive(false) - } - } - - async function ensurePickerLoaded(): Promise { - const win = window as any - if (win.google?.picker != null) return - await new Promise((resolve, reject) => { - const script = document.createElement('script') - script.src = 'https://apis.google.com/js/api.js' - script.async = true - script.onload = () => { - const gapi = (window as any).gapi - if (gapi?.load != null) { - gapi.load('picker', { callback: resolve }) - } else { - resolve() - } - } - script.onerror = () => reject(new Error('Failed to load Google API')) - document.body.appendChild(script) - }) - } - - function elevatePickerZIndex(): void { - const pickerElements = document.querySelectorAll( - '.picker-dialog, .picker-dialog-bg, .picker.modal-dialog, [class*="picker"]' - ) - - if (pickerElements.length === 0) return - - // Ensure the Google Picker is always above any MUI dialog or overlay. - // Use a very high static value to stay above custom MUI z-index configurations. - const pickerZIndex = '99999' - - pickerElements.forEach((element) => { - element.style.zIndex = pickerZIndex - }) - } - - function elevatePickerZIndexWithRetries(attempts = 100, delayMs = 100): void { - elevatePickerZIndex() - if (attempts <= 1) return - setTimeout( - () => elevatePickerZIndexWithRetries(attempts - 1, delayMs), - delayMs - ) - } - async function handleExportToSheets( - values: FormikValues, - actions: FormikHelpers + values: SyncFormValues, + actions: FormikHelpers ): Promise { const destination = values.googleMode === 'create' @@ -519,7 +215,6 @@ export function GoogleSheetsSyncDialog({ } try { - // Get user's timezone to store with sync for consistent date formatting const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone const { data } = await exportToSheets({ @@ -554,178 +249,9 @@ export function GoogleSheetsSyncDialog({ } } - async function handleDeleteSync(syncId: string): Promise { - setDeletingSyncId(syncId) - try { - await deleteSync({ - variables: { id: syncId }, - refetchQueries: [ - { - query: GET_GOOGLE_SHEETS_SYNCS, - variables: { filter: { journeyId } } - } - ], - awaitRefetchQueries: true - }) - enqueueSnackbar(t('Sync removed'), { variant: 'success' }) - } catch (error) { - enqueueSnackbar((error as Error).message, { variant: 'error' }) - } finally { - setDeletingSyncId(null) - setSyncIdPendingDelete(null) - } - } - - function handleRequestDeleteSync(syncId: string): void { - setSyncIdPendingDelete(syncId) - } - - async function handleBackfillSync(syncId: string): Promise { - setBackfillingSyncId(syncId) - try { - await backfillSync({ - variables: { id: syncId } - }) - enqueueSnackbar( - t('Backfill started. Your sheet will be updated shortly.'), - { variant: 'success' } - ) - } catch (error) { - enqueueSnackbar((error as Error).message, { variant: 'error' }) - } finally { - setBackfillingSyncId(null) - } - } - - const isGoogleActionDisabled = integrationsData == null - - const validationSchema = object().shape({ - integrationId: string().required(t('Integration account is required')), - sheetName: string().required(t('Sheet tab name is required')), - spreadsheetTitle: string().when( - 'googleMode', - (googleMode: unknown, schema) => - googleMode === 'create' - ? schema.required(t('Sheet name is required')) - : schema.notRequired() - ) - }) - - const RenderMobileSyncCard = ({ - sync, - isHistory = false - }: { - sync: GoogleSheetsSyncItem - isHistory?: boolean - }): ReactElement => { - const spreadsheetUrl = getSpreadsheetUrl(sync) - const createdAtDate = new Date(sync.createdAt) - const formattedDate = !Number.isNaN(createdAtDate.getTime()) - ? format(createdAtDate, 'yyyy-MM-dd') - : 'N/A' - const startedBy = getStartedByLabel(sync) - const isDeleting = deletingSyncId === sync.id - - return ( - - - { - if (spreadsheetUrl == null) e.preventDefault() - }} - > - {sync.sheetName ?? sync.spreadsheetId ?? t('Not found')} - - - - - - - - {t('Sync Start:')}{' '} - - {formattedDate} - - - - {t('By:')}{' '} - - {startedBy} - - - - {!isHistory && ( - - - - - handleBackfillSync(sync.id)} - disabled={backfillingSyncId === sync.id || isDeleting} - size="small" - aria-label={t('Backfill sync')} - > - {backfillingSyncId === sync.id ? ( - - ) : ( - - )} - - - handleRequestDeleteSync(sync.id)} - disabled={isDeleting || backfillingSyncId === sync.id} - size="small" - > - {isDeleting ? ( - - ) : ( - - )} - - - - )} - - ) + function handleCloseAddDialog(): void { + setGoogleDialogOpen(false) + if (hideMainDialog) onClose() } return ( @@ -780,7 +306,14 @@ export function GoogleSheetsSyncDialog({
) : ( activeSyncs.map((sync) => ( - + )) )}
@@ -809,7 +342,10 @@ export function GoogleSheetsSyncDialog({ sx={{ '&:before': { display: 'none' } }} defaultExpanded > - } sx={{ px: 2 }}> + } + sx={{ px: 2 }} + > {t('Sync History')} @@ -823,7 +359,7 @@ export function GoogleSheetsSyncDialog({ ) : ( historySyncs.map((sync) => ( - ) : ( - - - - - {t('Sheet Name')} - {t('Sync Start')} - {t('Started By')} - {t('Status')} - - {t('Actions')} - - - - - {activeSyncs.map((sync) => { - const createdAtDate = new Date(sync.createdAt) - const formattedDate = !Number.isNaN(createdAtDate.getTime()) - ? format(createdAtDate, 'yyyy-MM-dd') - : 'N/A' - const startedBy = getStartedByLabel(sync) - const isDeleting = deletingSyncId === sync.id - - return ( - handleOpenSyncRow(sync)} - onKeyDown={(event) => handleSyncRowKeyDown(event, sync)} - sx={{ - cursor: 'pointer', - '&:focus-visible': { - outline: (theme) => - `2px solid ${theme.palette.primary.main}`, - outlineOffset: 2 - } - }} - > - - - - - {sync.sheetName ?? 'N/A'} - - - - - - - {formattedDate} - - {startedBy} - - - - - - - { - event.stopPropagation() - void handleBackfillSync(sync.id) - }} - > - {backfillingSyncId === sync.id ? ( - - ) : ( - - )} - - - { - event.stopPropagation() - handleRequestDeleteSync(sync.id) - }} - > - {isDeleting ? ( - - ) : ( - - )} - - - - - ) - })} - -
-
+ )} {!isMobile && ( @@ -1007,565 +395,34 @@ export function GoogleSheetsSyncDialog({ {t('No removed syncs yet.')} ) : ( - - - - - {t('Sheet Name')} - - {t('Removed At')} - - {t('Started By')} - {t('Status')} - - - - {historySyncs.map((sync) => { - const removedAtDate = sync.deletedAt - ? new Date(sync.deletedAt) - : null - const removedAt = - removedAtDate != null && - !Number.isNaN(removedAtDate.getTime()) - ? format(removedAtDate, 'yyyy-MM-dd') - : 'N/A' - const startedBy = getStartedByLabel(sync) - - return ( - handleOpenSyncRow(sync)} - onKeyDown={(event) => - handleSyncRowKeyDown(event, sync) - } - sx={{ - cursor: 'pointer', - '&:focus-visible': { - outline: (theme) => - `2px solid ${theme.palette.primary.main}`, - outlineOffset: 2 - } - }} - > - - - - - {sync.sheetName ?? 'N/A'} - - - - - - - {removedAt} - - {startedBy} - - - - - ) - })} - -
-
+ )} )} - - {({ - values, - handleChange, - handleBlur, - handleSubmit, - errors, - touched, - resetForm, - setFieldValue - }) => ( - { - setGoogleDialogOpen(false) - if (hideMainDialog) onClose() - resetForm() - }} - dialogTitle={{ - title: t('Sync to Google Sheets'), - closeButton: true - }} - divider={false} - maxWidth="sm" - sx={{ - zIndex: pickerActive ? 1 : undefined - }} - dialogActionChildren={ - - - - - } - > -
- - - - - - - - {t('Google Account')} - - - {touched.integrationId != null && - errors.integrationId != null && ( - - {errors.integrationId as string} - - )} - - - - - - - {t('Spreadsheet Setup')} - - - - - {values.googleMode !== '' && ( - - {values.googleMode === 'create' ? ( - - - - {t( - 'Optional: Choose a folder in Google Drive to store your new spreadsheet.' - )} - - - ) : ( - - - - {t( - 'Select a spreadsheet from Google Drive to sync your data.' - )} - - - )} - {values.googleMode === 'create' && ( - - )} - - - )} - - -
-
- )} -
- - { - if (deletingSyncId != null) return - setSyncIdPendingDelete(null) + onOpenDrivePicker={handleOpenDrivePicker} + routerAsPath={router.asPath} + /> + + setSyncIdPendingDelete(null)} + onDelete={(syncId) => { + void handleDeleteSync(syncId) }} - dialogTitle={{ - title: t('Delete Google Sheets Sync'), - closeButton: true - }} - divider={false} - maxWidth="sm" - dialogActionChildren={ - - - - - } - > - - - {t( - "Data will no longer update in your Google Sheet if you delete this sync. Existing data will remain, but new updates won't be sent." - )} - - - {t('You will have to start a new sync to re-start syncing.')} - - - + /> ) } diff --git a/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/MobileSyncCard/MobileSyncCard.tsx b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/MobileSyncCard/MobileSyncCard.tsx new file mode 100644 index 00000000000..b49ca263047 --- /dev/null +++ b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/MobileSyncCard/MobileSyncCard.tsx @@ -0,0 +1,145 @@ +import LaunchIcon from '@mui/icons-material/Launch' +import RefreshIcon from '@mui/icons-material/Refresh' +import Box from '@mui/material/Box' +import Chip from '@mui/material/Chip' +import CircularProgress from '@mui/material/CircularProgress' +import IconButton from '@mui/material/IconButton' +import Link from '@mui/material/Link' +import Tooltip from '@mui/material/Tooltip' +import Typography from '@mui/material/Typography' +import { format } from 'date-fns' +import { useTranslation } from 'next-i18next' +import { ReactElement } from 'react' + +import Trash2Icon from '@core/shared/ui/icons/Trash2' + +import { GoogleSheetsSyncItem } from '../types' +import { getSpreadsheetUrl, getStartedByLabel } from '../libs/googleSheetsSyncUtils' + +interface MobileSyncCardProps { + sync: GoogleSheetsSyncItem + isHistory?: boolean + deletingSyncId?: string | null + backfillingSyncId?: string | null + onRequestDelete?: (syncId: string) => void + onBackfill?: (syncId: string) => void +} + +export function MobileSyncCard({ + sync, + isHistory = false, + deletingSyncId, + backfillingSyncId, + onRequestDelete, + onBackfill +}: MobileSyncCardProps): ReactElement { + const { t } = useTranslation('apps-journeys-admin') + const spreadsheetUrl = getSpreadsheetUrl(sync) + const createdAtDate = new Date(sync.createdAt) + const formattedDate = !Number.isNaN(createdAtDate.getTime()) + ? format(createdAtDate, 'yyyy-MM-dd') + : 'N/A' + const startedBy = getStartedByLabel(sync) + const isDeleting = deletingSyncId === sync.id + + return ( + + + { + if (spreadsheetUrl == null) e.preventDefault() + }} + > + {sync.sheetName ?? sync.spreadsheetId ?? t('Not found')} + + + + + + + + {t('Sync Start:')}{' '} + + {formattedDate} + + + + {t('By:')}{' '} + + {startedBy} + + + + {!isHistory && ( + + + + + onBackfill?.(sync.id)} + disabled={backfillingSyncId === sync.id || isDeleting} + size="small" + aria-label={t('Backfill sync')} + > + {backfillingSyncId === sync.id ? ( + + ) : ( + + )} + + + onRequestDelete?.(sync.id)} + disabled={isDeleting || backfillingSyncId === sync.id} + size="small" + > + {isDeleting ? ( + + ) : ( + + )} + + + + )} + + ) +} diff --git a/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/MobileSyncCard/index.ts b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/MobileSyncCard/index.ts new file mode 100644 index 00000000000..86066a9b001 --- /dev/null +++ b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/MobileSyncCard/index.ts @@ -0,0 +1 @@ +export { MobileSyncCard } from './MobileSyncCard' diff --git a/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/SyncTable/SyncTable.tsx b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/SyncTable/SyncTable.tsx new file mode 100644 index 00000000000..fd4da0b436b --- /dev/null +++ b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/SyncTable/SyncTable.tsx @@ -0,0 +1,244 @@ +import LaunchIcon from '@mui/icons-material/Launch' +import NorthEastIcon from '@mui/icons-material/NorthEast' +import RefreshIcon from '@mui/icons-material/Refresh' +import Box from '@mui/material/Box' +import Chip from '@mui/material/Chip' +import CircularProgress from '@mui/material/CircularProgress' +import IconButton from '@mui/material/IconButton' +import Table from '@mui/material/Table' +import TableBody from '@mui/material/TableBody' +import TableCell from '@mui/material/TableCell' +import TableContainer from '@mui/material/TableContainer' +import TableHead from '@mui/material/TableHead' +import TableRow from '@mui/material/TableRow' +import Tooltip from '@mui/material/Tooltip' +import Typography from '@mui/material/Typography' +import { format } from 'date-fns' +import { useTranslation } from 'next-i18next' +import { useSnackbar } from 'notistack' +import { KeyboardEvent, ReactElement } from 'react' + +import Trash2Icon from '@core/shared/ui/icons/Trash2' + +import { GoogleSheetsSyncItem } from '../types' +import { getSpreadsheetUrl, getStartedByLabel } from '../libs/googleSheetsSyncUtils' + +interface SyncTableProps { + syncs: GoogleSheetsSyncItem[] + variant: 'active' | 'history' + deletingSyncId?: string | null + backfillingSyncId?: string | null + onRequestDelete?: (syncId: string) => void + onBackfill?: (syncId: string) => void +} + +export function SyncTable({ + syncs, + variant, + deletingSyncId, + backfillingSyncId, + onRequestDelete, + onBackfill +}: SyncTableProps): ReactElement { + const { t } = useTranslation('apps-journeys-admin') + const { enqueueSnackbar } = useSnackbar() + const isActive = variant === 'active' + + function handleOpenSyncRow(sync: GoogleSheetsSyncItem): void { + const spreadsheetUrl = getSpreadsheetUrl(sync) + if (spreadsheetUrl == null) { + enqueueSnackbar(t('Something went wrong, please try again!'), { + variant: 'error' + }) + return + } + if (typeof window === 'undefined') return + window.open(spreadsheetUrl, '_blank', 'noopener,noreferrer') + } + + function handleSyncRowKeyDown( + event: KeyboardEvent, + sync: GoogleSheetsSyncItem + ): void { + if (event.key === 'Enter') { + handleOpenSyncRow(sync) + return + } + if (event.key === ' ') { + event.preventDefault() + handleOpenSyncRow(sync) + } + } + + return ( + + + + + {t('Sheet Name')} + + {isActive ? t('Sync Start') : t('Removed At')} + + {t('Started By')} + {t('Status')} + {isActive && ( + + {t('Actions')} + + )} + + + + {syncs.map((sync) => { + const dateValue = isActive ? sync.createdAt : sync.deletedAt + const parsedDate = dateValue != null ? new Date(dateValue) : null + const formattedDate = + parsedDate != null && !Number.isNaN(parsedDate.getTime()) + ? format(parsedDate, 'yyyy-MM-dd') + : 'N/A' + const startedBy = getStartedByLabel(sync) + const isDeleting = deletingSyncId === sync.id + const LinkIcon = isActive ? NorthEastIcon : LaunchIcon + + return ( + handleOpenSyncRow(sync)} + onKeyDown={(event) => handleSyncRowKeyDown(event, sync)} + sx={{ + cursor: 'pointer', + '&:focus-visible': { + outline: (theme) => + `2px solid ${theme.palette.primary.main}`, + outlineOffset: 2 + } + }} + > + + + + + {sync.sheetName ?? 'N/A'} + + + + + + {formattedDate} + {startedBy} + + {isActive ? ( + + ) : ( + + )} + + {isActive && ( + + + + { + event.stopPropagation() + onBackfill?.(sync.id) + }} + > + {backfillingSyncId === sync.id ? ( + + ) : ( + + )} + + + { + event.stopPropagation() + onRequestDelete?.(sync.id) + }} + > + {isDeleting ? ( + + ) : ( + + )} + + + + )} + + ) + })} + +
+
+ ) +} diff --git a/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/SyncTable/index.ts b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/SyncTable/index.ts new file mode 100644 index 00000000000..8cf7ed9d1ae --- /dev/null +++ b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/SyncTable/index.ts @@ -0,0 +1 @@ +export { SyncTable } from './SyncTable' diff --git a/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/graphql.ts b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/graphql.ts new file mode 100644 index 00000000000..93ee650b70c --- /dev/null +++ b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/graphql.ts @@ -0,0 +1,84 @@ +import { gql } from '@apollo/client' + +export const GET_JOURNEY_CREATED_AT = gql` + query GoogleSheetsSyncDialogJourney($id: ID!) { + journey: adminJourney(id: $id, idType: databaseId) { + id + createdAt + title + team { + id + } + } + } +` + +export const GET_GOOGLE_PICKER_TOKEN = gql` + query IntegrationGooglePickerToken($integrationId: ID!) { + integrationGooglePickerToken(integrationId: $integrationId) + } +` + +export const GET_GOOGLE_SHEETS_SYNCS = gql` + query GoogleSheetsSyncs($filter: GoogleSheetsSyncsFilter!) { + googleSheetsSyncs(filter: $filter) { + id + spreadsheetId + sheetName + email + deletedAt + createdAt + integration { + __typename + id + ... on IntegrationGoogle { + accountEmail + } + } + } + } +` + +export const EXPORT_TO_SHEETS = gql` + mutation JourneyVisitorExportToGoogleSheet( + $journeyId: ID! + $destination: JourneyVisitorGoogleSheetDestinationInput! + $integrationId: ID! + $timezone: String + ) { + journeyVisitorExportToGoogleSheet( + journeyId: $journeyId + destination: $destination + integrationId: $integrationId + timezone: $timezone + ) { + spreadsheetId + spreadsheetUrl + sheetName + } + } +` + +export const DELETE_GOOGLE_SHEETS_SYNC = gql` + mutation GoogleSheetsSyncDialogDelete($id: ID!) { + googleSheetsSyncDelete(id: $id) { + id + } + } +` + +export const BACKFILL_GOOGLE_SHEETS_SYNC = gql` + mutation GoogleSheetsSyncDialogBackfill($id: ID!) { + googleSheetsSyncBackfill(id: $id) { + id + } + } +` + +export const INTEGRATION_GOOGLE_CREATE = gql` + mutation IntegrationGoogleCreate($input: IntegrationGoogleCreateInput!) { + integrationGoogleCreate(input: $input) { + id + } + } +` diff --git a/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/libs/googleSheetsSyncUtils.ts b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/libs/googleSheetsSyncUtils.ts new file mode 100644 index 00000000000..d659a696ad2 --- /dev/null +++ b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/libs/googleSheetsSyncUtils.ts @@ -0,0 +1,15 @@ +import { GoogleSheetsSyncItem } from '../types' + +export function getStartedByLabel(sync: GoogleSheetsSyncItem): string { + if (sync.integration?.__typename === 'IntegrationGoogle') { + return sync.integration.accountEmail ?? sync.email ?? 'N/A' + } + if (sync.email != null && sync.email !== '') return sync.email + return 'N/A' +} + +export function getSpreadsheetUrl(sync: GoogleSheetsSyncItem): string | null { + if (sync.spreadsheetId == null || sync.spreadsheetId === '') return null + return `https://docs.google.com/spreadsheets/d/${sync.spreadsheetId}` +} + diff --git a/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/libs/useGooglePicker/index.ts b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/libs/useGooglePicker/index.ts new file mode 100644 index 00000000000..236b0a84106 --- /dev/null +++ b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/libs/useGooglePicker/index.ts @@ -0,0 +1,2 @@ +export { useGooglePicker } from './useGooglePicker' +export type { UseGooglePickerParams, UseGooglePickerReturn } from './useGooglePicker' diff --git a/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/libs/useGooglePicker/useGooglePicker.ts b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/libs/useGooglePicker/useGooglePicker.ts new file mode 100644 index 00000000000..a39de7a0d0b --- /dev/null +++ b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/libs/useGooglePicker/useGooglePicker.ts @@ -0,0 +1,156 @@ +import { useLazyQuery } from '@apollo/client' +import { useTranslation } from 'next-i18next' +import { useSnackbar } from 'notistack' +import { useState } from 'react' + +import { GET_GOOGLE_PICKER_TOKEN } from '../../graphql' + +const PICKER_Z_INDEX = '99999' +const PICKER_RETRY_ATTEMPTS = 100 +const PICKER_RETRY_DELAY_MS = 100 + +export interface UseGooglePickerParams { + teamId: string | undefined +} + +export interface UseGooglePickerReturn { + pickerActive: boolean + handleOpenDrivePicker: ( + mode: 'folder' | 'sheet', + integrationId: string | undefined, + setFieldValue: (field: string, value: unknown) => void + ) => Promise +} + +export function useGooglePicker({ + teamId +}: UseGooglePickerParams): UseGooglePickerReturn { + const { t } = useTranslation('apps-journeys-admin') + const { enqueueSnackbar } = useSnackbar() + const [getPickerToken] = useLazyQuery(GET_GOOGLE_PICKER_TOKEN) + const [pickerActive, setPickerActive] = useState(false) + + async function ensurePickerLoaded(): Promise { + const win = window as any + if (win.google?.picker != null) return + await new Promise((resolve, reject) => { + const script = document.createElement('script') + script.src = 'https://apis.google.com/js/api.js' + script.async = true + script.onload = () => { + const gapi = (window as any).gapi + if (gapi?.load != null) { + gapi.load('picker', { callback: resolve }) + } else { + resolve() + } + } + script.onerror = () => reject(new Error('Failed to load Google API')) + document.body.appendChild(script) + }) + } + + function elevatePickerZIndex(): void { + const pickerElements = document.querySelectorAll( + '.picker-dialog, .picker-dialog-bg, .picker.modal-dialog, [class*="picker"]' + ) + if (pickerElements.length === 0) return + pickerElements.forEach((element) => { + element.style.zIndex = PICKER_Z_INDEX + }) + } + + function elevatePickerZIndexWithRetries( + attempts = PICKER_RETRY_ATTEMPTS, + delayMs = PICKER_RETRY_DELAY_MS + ): void { + elevatePickerZIndex() + if (attempts <= 1) return + setTimeout( + () => elevatePickerZIndexWithRetries(attempts - 1, delayMs), + delayMs + ) + } + + async function handleOpenDrivePicker( + mode: 'folder' | 'sheet', + integrationId: string | undefined, + setFieldValue: (field: string, value: unknown) => void + ): Promise { + try { + const apiKey = process.env.NEXT_PUBLIC_FIREBASE_API_KEY + if (apiKey == null || apiKey === '') { + enqueueSnackbar(t('Missing Google API key'), { variant: 'error' }) + return + } + if (integrationId == null || integrationId === '') { + enqueueSnackbar(t('Select an integration account first'), { + variant: 'error' + }) + return + } + + let oauthToken: string | undefined | null + if (teamId != null) { + const { data: tokenData } = await getPickerToken({ + variables: { teamId, integrationId } + }) + oauthToken = tokenData?.integrationGooglePickerToken + } + if (oauthToken == null || oauthToken === '') { + enqueueSnackbar(t('Unable to authorize Google Picker'), { + variant: 'error' + }) + return + } + + await ensurePickerLoaded() + + setPickerActive(true) + + const googleAny: any = (window as any).google + const view = + mode === 'sheet' + ? new googleAny.picker.DocsView(googleAny.picker.ViewId.SPREADSHEETS) + : new googleAny.picker.DocsView( + googleAny.picker.ViewId.FOLDERS + ).setSelectFolderEnabled(true) + + const picker = new googleAny.picker.PickerBuilder() + .setOAuthToken(oauthToken) + .setDeveloperKey(apiKey) + .addView(view) + .setCallback((pickerData: any) => { + if (pickerData?.action === googleAny.picker.Action.PICKED) { + const doc = pickerData.docs?.[0] + if (doc != null) { + const docName = doc?.name ?? doc?.title ?? doc?.id ?? null + if (mode === 'sheet') { + setFieldValue('existingSpreadsheetId', doc.id) + setFieldValue('existingSpreadsheetName', docName ?? undefined) + } else { + setFieldValue('folderId', doc.id) + setFieldValue('folderName', docName ?? undefined) + } + } + } + + if ( + pickerData?.action === googleAny.picker.Action.PICKED || + pickerData?.action === googleAny.picker.Action.CANCEL + ) { + setPickerActive(false) + } + }) + .build() + + picker.setVisible(true) + elevatePickerZIndexWithRetries() + } catch { + enqueueSnackbar(t('Failed to open Google Picker'), { variant: 'error' }) + setPickerActive(false) + } + } + + return { pickerActive, handleOpenDrivePicker } +} diff --git a/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/libs/useGoogleSheetsSync/index.ts b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/libs/useGoogleSheetsSync/index.ts new file mode 100644 index 00000000000..f6f974a0df7 --- /dev/null +++ b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/libs/useGoogleSheetsSync/index.ts @@ -0,0 +1,5 @@ +export { useGoogleSheetsSync } from './useGoogleSheetsSync' +export type { + UseGoogleSheetsSyncParams, + UseGoogleSheetsSyncReturn +} from './useGoogleSheetsSync' diff --git a/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/libs/useGoogleSheetsSync/useGoogleSheetsSync.ts b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/libs/useGoogleSheetsSync/useGoogleSheetsSync.ts new file mode 100644 index 00000000000..8a440c043ba --- /dev/null +++ b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/libs/useGoogleSheetsSync/useGoogleSheetsSync.ts @@ -0,0 +1,142 @@ +import { LazyQueryExecFunction, useLazyQuery, useMutation } from '@apollo/client' +import { useTranslation } from 'next-i18next' +import { useSnackbar } from 'notistack' +import { useEffect, useState } from 'react' + +import { + BACKFILL_GOOGLE_SHEETS_SYNC, + DELETE_GOOGLE_SHEETS_SYNC, + GET_GOOGLE_SHEETS_SYNCS +} from '../../graphql' +import { + GoogleSheetsSyncItem, + GoogleSheetsSyncsQueryData, + GoogleSheetsSyncsQueryVariables +} from '../../types' + +export interface UseGoogleSheetsSyncParams { + journeyId: string + open: boolean +} + +export interface UseGoogleSheetsSyncReturn { + loadSyncs: LazyQueryExecFunction< + GoogleSheetsSyncsQueryData, + GoogleSheetsSyncsQueryVariables + > + syncsLoading: boolean + syncsCalled: boolean + activeSyncs: GoogleSheetsSyncItem[] + historySyncs: GoogleSheetsSyncItem[] + syncsResolved: boolean + hasNoSyncs: boolean + deletingSyncId: string | null + syncIdPendingDelete: string | null + setSyncIdPendingDelete: (id: string | null) => void + backfillingSyncId: string | null + handleDeleteSync: (syncId: string) => Promise + handleRequestDeleteSync: (syncId: string) => void + handleBackfillSync: (syncId: string) => Promise +} + +export function useGoogleSheetsSync({ + journeyId, + open +}: UseGoogleSheetsSyncParams): UseGoogleSheetsSyncReturn { + const { t } = useTranslation('apps-journeys-admin') + const { enqueueSnackbar } = useSnackbar() + + const [ + loadSyncs, + { data: syncsData, loading: syncsLoading, called: syncsCalled } + ] = useLazyQuery( + GET_GOOGLE_SHEETS_SYNCS + ) + const [deleteSync] = useMutation(DELETE_GOOGLE_SHEETS_SYNC) + const [backfillSync] = useMutation(BACKFILL_GOOGLE_SHEETS_SYNC) + + const [deletingSyncId, setDeletingSyncId] = useState(null) + const [syncIdPendingDelete, setSyncIdPendingDelete] = useState( + null + ) + const [backfillingSyncId, setBackfillingSyncId] = useState( + null + ) + + useEffect(() => { + if (open) return + if (deletingSyncId != null) return + setSyncIdPendingDelete(null) + }, [open, deletingSyncId]) + + const googleSheetsSyncs = syncsData?.googleSheetsSyncs ?? [] + const activeSyncs = googleSheetsSyncs.filter( + (sync) => sync.deletedAt == null + ) + const historySyncs = googleSheetsSyncs.filter( + (sync) => sync.deletedAt != null + ) + const syncsResolved = syncsCalled && !syncsLoading + const hasNoSyncs = + syncsResolved && activeSyncs.length === 0 && historySyncs.length === 0 + + async function handleDeleteSync(syncId: string): Promise { + setDeletingSyncId(syncId) + try { + await deleteSync({ + variables: { id: syncId }, + refetchQueries: [ + { + query: GET_GOOGLE_SHEETS_SYNCS, + variables: { filter: { journeyId } } + } + ], + awaitRefetchQueries: true + }) + enqueueSnackbar(t('Sync removed'), { variant: 'success' }) + } catch (error) { + enqueueSnackbar((error as Error).message, { variant: 'error' }) + } finally { + setDeletingSyncId(null) + setSyncIdPendingDelete(null) + } + } + + function handleRequestDeleteSync(syncId: string): void { + setSyncIdPendingDelete(syncId) + } + + async function handleBackfillSync(syncId: string): Promise { + setBackfillingSyncId(syncId) + try { + await backfillSync({ + variables: { id: syncId } + }) + enqueueSnackbar( + t('Backfill started. Your sheet will be updated shortly.'), + { variant: 'success' } + ) + } catch (error) { + enqueueSnackbar((error as Error).message, { variant: 'error' }) + } finally { + setBackfillingSyncId(null) + } + } + + return { + loadSyncs, + syncsLoading, + syncsCalled, + activeSyncs, + historySyncs, + syncsResolved, + hasNoSyncs, + deletingSyncId, + syncIdPendingDelete, + setSyncIdPendingDelete, + backfillingSyncId, + handleDeleteSync, + handleRequestDeleteSync, + handleBackfillSync + } +} diff --git a/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/types.ts b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/types.ts new file mode 100644 index 00000000000..721d5b2bcef --- /dev/null +++ b/apps/journeys-admin/src/components/JourneyVisitorsList/FilterDrawer/GoogleSheetsSyncDialog/types.ts @@ -0,0 +1,35 @@ +export interface GoogleSheetsSyncItem { + id: string + spreadsheetId: string | null + sheetName: string | null + email: string | null + deletedAt: string | null + createdAt: string + integration: { + __typename: string + id: string + accountEmail?: string | null + } | null +} + +export interface GoogleSheetsSyncsQueryData { + googleSheetsSyncs: GoogleSheetsSyncItem[] +} + +export interface GoogleSheetsSyncsQueryVariables { + filter: { + journeyId?: string + integrationId?: string + } +} + +export interface SyncFormValues { + integrationId: string + googleMode: '' | 'create' | 'existing' + spreadsheetTitle: string + sheetName: string + folderId?: string + folderName?: string + existingSpreadsheetId?: string + existingSpreadsheetName?: string +}