From ee9b0a01934b3eb118912e5e60d9c196314912f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Thu, 17 Apr 2025 15:56:59 +0100 Subject: [PATCH 1/4] chore: demo misc improvements (#9796) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://linear.app/unleash/issue/2-2577/transition-screen-between-the-guides https://linear.app/unleash/issue/2-2582/page-scrolls-in-back Includes what should be the last batch of demo improvements at this stage: - Visually aligns the Back button for consistent layout - Replaces “Start” with “Start tutorial” for new topics - Updates topic titles for clarity and consistency - Applies bold styling to all step titles - Prevents page scroll when the step target is set to body --- .../DemoStepTooltip/DemoStepTooltip.tsx | 47 +++++++++---------- .../component/demo/DemoSteps/DemoSteps.tsx | 2 + frontend/src/component/demo/demo-topics.tsx | 16 +++---- 3 files changed, 31 insertions(+), 34 deletions(-) diff --git a/frontend/src/component/demo/DemoSteps/DemoStepTooltip/DemoStepTooltip.tsx b/frontend/src/component/demo/DemoSteps/DemoStepTooltip/DemoStepTooltip.tsx index 5d6ecd4edd..950b40f6ff 100644 --- a/frontend/src/component/demo/DemoSteps/DemoStepTooltip/DemoStepTooltip.tsx +++ b/frontend/src/component/demo/DemoSteps/DemoStepTooltip/DemoStepTooltip.tsx @@ -63,12 +63,18 @@ const StyledTooltipTitle = styled('div')(({ theme }) => ({ const StyledTooltipActions = styled('div')(({ theme }) => ({ display: 'flex', justifyContent: 'space-between', + alignItems: 'center', marginTop: theme.spacing(3), '&&& button': { fontSize: theme.fontSizes.smallBody, }, })); +const StyledBackButton = styled(Button)({ + padding: 0, + minWidth: 0, +}); + // @ts-ignore export interface IDemoStepTooltipProps extends TooltipRenderProps { step: ITutorialTopicStep; @@ -92,7 +98,7 @@ export const DemoStepTooltip = ({ }: IDemoStepTooltipProps) => { const nextLabel = stepIndex === 0 - ? 'Start' + ? 'Start tutorial' : stepIndex === topics[topic].steps.length - 1 ? 'Finish' : 'Next'; @@ -112,28 +118,23 @@ export const DemoStepTooltip = ({ - - {topics[topic].title} - - } - /> + + {step.title || topics[topic].title} + {step.content}
0 || stepIndex > 0} + condition={ + !step.hideBackButton && stepIndex > 0 + } show={ - + } />
@@ -164,25 +165,19 @@ export const DemoStepTooltip = ({ - - {topics[topic].title} - - } - /> + + {step.title || topics[topic].title} + {step.content}
0 || stepIndex > 0} + condition={!step.hideBackButton && stepIndex > 0} show={ - + } />
diff --git a/frontend/src/component/demo/DemoSteps/DemoSteps.tsx b/frontend/src/component/demo/DemoSteps/DemoSteps.tsx index 7e51695b7a..07f59d22ba 100644 --- a/frontend/src/component/demo/DemoSteps/DemoSteps.tsx +++ b/frontend/src/component/demo/DemoSteps/DemoSteps.tsx @@ -118,6 +118,8 @@ export const DemoSteps = ({ } if (action === ACTIONS.UPDATE) { + if (step.target === 'body') return; + const el = document.querySelector( step.target as string, ) as HTMLElement | null; diff --git a/frontend/src/component/demo/demo-topics.tsx b/frontend/src/component/demo/demo-topics.tsx index 4771cb29ea..69ccb92afc 100644 --- a/frontend/src/component/demo/demo-topics.tsx +++ b/frontend/src/component/demo/demo-topics.tsx @@ -37,10 +37,10 @@ const ENVIRONMENT = 'dev'; export const TOPICS: ITutorialTopic[] = [ { - title: 'Enable/disable a feature flag', + title: 'How to enable/disable a feature flag', steps: [ { - title: 'Enable/disable a feature flag', + title: 'How to enable/disable a feature flag', href: `/projects/${PROJECT}?sort=name`, target: 'body', placement: 'center', @@ -91,11 +91,11 @@ export const TOPICS: ITutorialTopic[] = [ ], }, { - title: 'Enable for a specific user', + title: 'Next: How to enable for a specific user', setup: specificUser, steps: [ { - title: 'Enable for a specific user', + title: 'Next: How to enable for a specific user', href: `/projects/${PROJECT}?sort=name`, target: 'body', placement: 'center', @@ -335,11 +335,11 @@ export const TOPICS: ITutorialTopic[] = [ ], }, { - title: 'Adjust gradual rollout', + title: 'Next: How to adjust gradual rollout', setup: gradualRollout, steps: [ { - title: 'Adjust gradual rollout', + title: 'Next: How to adjust gradual rollout', href: `/projects/${PROJECT}?sort=name`, target: 'body', placement: 'center', @@ -468,11 +468,11 @@ export const TOPICS: ITutorialTopic[] = [ ], }, { - title: 'Adjust variants', + title: 'Next: How to adjust variants', setup: variants, steps: [ { - title: 'Adjust variants', + title: 'Next: How to adjust variants', href: `/projects/${PROJECT}?sort=name`, target: 'body', placement: 'center', From 6403ae7f9beec57c4a527620b636eaf039dcb811 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Thu, 17 Apr 2025 17:40:04 +0200 Subject: [PATCH 2/4] feat: futureproofing last viewed page redirect (#9794) You should not be able to break initial page redirect even if you set '/' as target. It is not strictly needed in the current code path. This will create a redirect loop only if you manually modify local storage. It just makes this part safer if it is ever modified. --- frontend/src/component/InitialRedirect.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/src/component/InitialRedirect.tsx b/frontend/src/component/InitialRedirect.tsx index 5c0ebd201a..0589aad019 100644 --- a/frontend/src/component/InitialRedirect.tsx +++ b/frontend/src/component/InitialRedirect.tsx @@ -6,10 +6,12 @@ import Loader from './common/Loader/Loader'; import { useLocalStorageState } from 'hooks/useLocalStorageState'; import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser'; +const defaultPage = '/personal'; + export const useLastViewedPage = (location?: Location) => { const [state, setState] = useLocalStorageState( 'lastViewedPage', - '/personal', + defaultPage, 7 * 24 * 60 * 60 * 1000, // 7 days, left to promote seeing Personal dashboard from time to time ); @@ -55,5 +57,9 @@ export const InitialRedirect = () => { return ; } - return ; + if (lastViewedPage && lastViewedPage !== '/') { + return ; + } + + return ; }; From 78f0d02a84a71f51cfc0a909604b33ab80a37d93 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Fri, 18 Apr 2025 10:10:13 +0200 Subject: [PATCH 3/4] feat: snooze reminder tracking (#9798) --- .../CleanupReminder/CleanupReminder.tsx | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/frontend/src/component/feature/FeatureView/CleanupReminder/CleanupReminder.tsx b/frontend/src/component/feature/FeatureView/CleanupReminder/CleanupReminder.tsx index 56c3c9c835..3389eaf02b 100644 --- a/frontend/src/component/feature/FeatureView/CleanupReminder/CleanupReminder.tsx +++ b/frontend/src/component/feature/FeatureView/CleanupReminder/CleanupReminder.tsx @@ -18,6 +18,7 @@ import { FeatureArchiveNotAllowedDialog } from 'component/common/FeatureArchiveD import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog'; import { useNavigate } from 'react-router-dom'; import { useFlagReminders } from './useFlagReminders'; +import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; const StyledBox = styled(Box)(({ theme }) => ({ marginRight: theme.spacing(2), @@ -37,6 +38,7 @@ export const CleanupReminder: FC<{ onChange: () => void; }> = ({ feature, onChange }) => { const navigate = useNavigate(); + const { trackEvent } = usePlausibleTracker(); const [markCompleteDialogueOpen, setMarkCompleteDialogueOpen] = useState(false); @@ -123,7 +125,14 @@ export const CleanupReminder: FC<{ @@ -173,7 +182,14 @@ export const CleanupReminder: FC<{ action={ From e436cf72e6a4519128abb76e4fbd38b8927689b3 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Fri, 18 Apr 2025 11:42:43 +0200 Subject: [PATCH 4/4] feat: revert to production (#9802) --- .../CleanupReminder/CleanupReminder.test.tsx | 1 + .../CleanupReminder/CleanupReminder.tsx | 44 +++++++++++++------ .../FeatureLifecycle/FeatureLifecycle.tsx | 19 +++----- .../FeatureLifecycle/useUncomplete.ts | 31 +++++++++++++ 4 files changed, 69 insertions(+), 26 deletions(-) create mode 100644 frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/useUncomplete.ts diff --git a/frontend/src/component/feature/FeatureView/CleanupReminder/CleanupReminder.test.tsx b/frontend/src/component/feature/FeatureView/CleanupReminder/CleanupReminder.test.tsx index c34676bac8..3e26454534 100644 --- a/frontend/src/component/feature/FeatureView/CleanupReminder/CleanupReminder.test.tsx +++ b/frontend/src/component/feature/FeatureView/CleanupReminder/CleanupReminder.test.tsx @@ -59,6 +59,7 @@ test('render remove flag from code reminder', async () => { }); await screen.findByText('Time to remove flag from code?'); + await screen.findByText('Revert to production'); const reminder = await screen.findByText('Remind me later'); reminder.click(); diff --git a/frontend/src/component/feature/FeatureView/CleanupReminder/CleanupReminder.tsx b/frontend/src/component/feature/FeatureView/CleanupReminder/CleanupReminder.tsx index 3389eaf02b..5f7cfac9c9 100644 --- a/frontend/src/component/feature/FeatureView/CleanupReminder/CleanupReminder.tsx +++ b/frontend/src/component/feature/FeatureView/CleanupReminder/CleanupReminder.tsx @@ -19,6 +19,7 @@ import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/Feat import { useNavigate } from 'react-router-dom'; import { useFlagReminders } from './useFlagReminders'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; +import { useUncomplete } from '../FeatureOverview/FeatureLifecycle/useUncomplete'; const StyledBox = styled(Box)(({ theme }) => ({ marginRight: theme.spacing(2), @@ -43,6 +44,11 @@ export const CleanupReminder: FC<{ const [markCompleteDialogueOpen, setMarkCompleteDialogueOpen] = useState(false); const [archiveDialogueOpen, setArchiveDialogueOpen] = useState(false); + const { onUncompleteHandler, loading } = useUncomplete({ + feature: feature.name, + project: feature.project, + onChange, + }); const currentStage = populateCurrentStage(feature); const isRelevantType = @@ -180,19 +186,31 @@ export const CleanupReminder: FC<{ severity='warning' icon={} action={ - + + + + Revert to production + + } > Time to remove flag from code? diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycle.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycle.tsx index 191a8a507b..7b932b5e88 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycle.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycle.tsx @@ -1,12 +1,11 @@ import { FeatureLifecycleStageIcon } from 'component/common/FeatureLifecycle/FeatureLifecycleStageIcon'; import { FeatureLifecycleTooltip } from './FeatureLifecycleTooltip'; -import useFeatureLifecycleApi from 'hooks/api/actions/useFeatureLifecycleApi/useFeatureLifecycleApi'; import { populateCurrentStage } from './populateCurrentStage'; import type { FC } from 'react'; import type { Lifecycle } from 'interfaces/featureToggle'; -import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import { getFeatureLifecycleName } from 'component/common/FeatureLifecycle/getFeatureLifecycleName'; import { Box } from '@mui/material'; +import { useUncomplete } from './useUncomplete'; export interface LifecycleFeature { lifecycle?: Lifecycle; @@ -28,18 +27,12 @@ export const FeatureLifecycle: FC<{ expanded?: boolean; }> = ({ feature, expanded, onComplete, onUncomplete, onArchive }) => { const currentStage = populateCurrentStage(feature); - const { markFeatureUncompleted, loading } = useFeatureLifecycleApi(); - const { trackEvent } = usePlausibleTracker(); - const onUncompleteHandler = async () => { - await markFeatureUncompleted(feature.name, feature.project); - onUncomplete?.(); - trackEvent('feature-lifecycle', { - props: { - eventType: 'uncomplete', - }, - }); - }; + const { onUncompleteHandler, loading } = useUncomplete({ + feature: feature.name, + project: feature.project, + onChange: onUncomplete, + }); return currentStage ? ( ({ display: 'flex', gap: theme.spacing(0.5) })}> diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/useUncomplete.ts b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/useUncomplete.ts new file mode 100644 index 0000000000..ecbc143428 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/useUncomplete.ts @@ -0,0 +1,31 @@ +import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; +import useToast from 'hooks/useToast'; +import useFeatureLifecycleApi from 'hooks/api/actions/useFeatureLifecycleApi/useFeatureLifecycleApi'; +import { formatUnknownError } from 'utils/formatUnknownError'; + +export const useUncomplete = ({ + feature, + project, + onChange, +}: { feature: string; project: string; onChange?: () => void }) => { + const { trackEvent } = usePlausibleTracker(); + const { setToastApiError } = useToast(); + const { markFeatureUncompleted, loading } = useFeatureLifecycleApi(); + + const onUncompleteHandler = async () => { + try { + await markFeatureUncompleted(feature, project); + onChange?.(); + + trackEvent('feature-lifecycle', { + props: { + eventType: 'uncomplete', + }, + }); + } catch (e) { + setToastApiError(formatUnknownError(e)); + } + }; + + return { onUncompleteHandler, loading }; +};