1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

feat: demo guide improvements (#3676)

https://linear.app/unleash/issue/2-986/feedback-from-sebastian

Implements the items mentioned in the task:
 - Refactors logic to track completion separately;
- When finishing a topic, jumps to the next unfinished topic it can
find;
- Shows the finish dialog when finishing a topic, as long as completion
is 100%;
- Changes the guide overlay behavior and implements the necessary
changes to adapt to light and dark mode;
- Fixes an issue where some guide dialogs would close when clicking
outside;
- Added a final "toggle" step for each topic (still needs alignment,
different task);
- Improve navigation logic to hopefully fix the feature toggle name
sorting;


![image](https://user-images.githubusercontent.com/14320932/236003007-6e441acc-f933-4eb0-93a4-4b6c15a45b96.png)

Relates to [roadmap](https://github.com/orgs/Unleash/projects/10) item:
#3537
This commit is contained in:
Nuno Góis 2023-05-03 19:47:35 +01:00 committed by GitHub
parent d2999df7fd
commit 710b2a6d5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 167 additions and 112 deletions

View File

@ -14,7 +14,8 @@ const defaultProgress = {
welcomeOpen: true, welcomeOpen: true,
expanded: true, expanded: true,
topic: -1, topic: -1,
steps: [0], step: 0,
stepsCompletion: Array(TOPICS.length).fill(0),
}; };
interface IDemoProps { interface IDemoProps {
@ -26,7 +27,7 @@ export const Demo = ({ children }: IDemoProps): JSX.Element => {
const { trackEvent } = usePlausibleTracker(); const { trackEvent } = usePlausibleTracker();
const { value: storedProgress, setValue: setStoredProgress } = const { value: storedProgress, setValue: setStoredProgress } =
createLocalStorage('Tutorial:v1', defaultProgress); createLocalStorage('Tutorial:v1.1', defaultProgress);
const [welcomeOpen, setWelcomeOpen] = useState( const [welcomeOpen, setWelcomeOpen] = useState(
storedProgress.welcomeOpen ?? defaultProgress.welcomeOpen storedProgress.welcomeOpen ?? defaultProgress.welcomeOpen
@ -40,8 +41,11 @@ export const Demo = ({ children }: IDemoProps): JSX.Element => {
const [topic, setTopic] = useState( const [topic, setTopic] = useState(
storedProgress.topic ?? defaultProgress.topic storedProgress.topic ?? defaultProgress.topic
); );
const [steps, setSteps] = useState( const [step, setStep] = useState(
storedProgress.steps ?? defaultProgress.steps storedProgress.step ?? defaultProgress.step
);
const [stepsCompletion, setStepsCompletion] = useState(
storedProgress.stepsCompletion ?? defaultProgress.stepsCompletion
); );
useEffect(() => { useEffect(() => {
@ -49,32 +53,26 @@ export const Demo = ({ children }: IDemoProps): JSX.Element => {
welcomeOpen, welcomeOpen,
expanded, expanded,
topic, topic,
steps, step,
stepsCompletion,
}); });
}, [welcomeOpen, expanded, topic, steps]); }, [welcomeOpen, expanded, topic, step, stepsCompletion]);
const onStart = () => { const onStart = () => {
setTopic(0); setTopic(0);
setSteps([0]); setStep(0);
setStepsCompletion(Array(TOPICS.length).fill(0));
setExpanded(true); setExpanded(true);
}; };
const onFinish = () => { const onFinish = () => {
const completedSteps = steps.reduce( setFinishOpen(true);
(acc, step) => acc + (step || 0),
1
);
const totalSteps = TOPICS.flatMap(({ steps }) => steps).length;
if (completedSteps === totalSteps) { trackEvent('demo', {
setFinishOpen(true); props: {
eventType: 'finish',
trackEvent('demo', { },
props: { });
eventType: 'finish',
},
});
}
}; };
if (!uiConfig.flags.demo) return children; if (!uiConfig.flags.demo) return children;
@ -141,15 +139,11 @@ export const Demo = ({ children }: IDemoProps): JSX.Element => {
<DemoTopics <DemoTopics
expanded={expanded} expanded={expanded}
setExpanded={setExpanded} setExpanded={setExpanded}
steps={steps} stepsCompletion={stepsCompletion}
currentTopic={topic} currentTopic={topic}
setCurrentTopic={(topic: number) => { setCurrentTopic={(topic: number) => {
setTopic(topic); setTopic(topic);
setSteps(steps => { setStep(0);
const newSteps = [...steps];
newSteps[topic] = 0;
return newSteps;
});
trackEvent('demo', { trackEvent('demo', {
props: { props: {
@ -171,8 +165,10 @@ export const Demo = ({ children }: IDemoProps): JSX.Element => {
/> />
<DemoSteps <DemoSteps
setExpanded={setExpanded} setExpanded={setExpanded}
steps={steps} step={step}
setSteps={setSteps} setStep={setStep}
stepsCompletion={stepsCompletion}
setStepsCompletion={setStepsCompletion}
topic={topic} topic={topic}
setTopic={setTopic} setTopic={setTopic}
topics={TOPICS} topics={TOPICS}

View File

@ -23,13 +23,13 @@ const StyledDialog = styled(Dialog)(({ theme }) => ({
const StyledTooltip = styled('div')(({ theme }) => ({ const StyledTooltip = styled('div')(({ theme }) => ({
'@keyframes pulse': { '@keyframes pulse': {
'0%': { '0%': {
boxShadow: `0 0 0 0 ${alpha(theme.palette.primary.main, 0.7)}`, boxShadow: `0 0 0 0 ${alpha(theme.palette.spotlight.pulse, 1)}`,
}, },
'70%': { '70%': {
boxShadow: `0 0 0 10px ${alpha(theme.palette.primary.main, 0)}`, boxShadow: `0 0 0 16px ${alpha(theme.palette.spotlight.pulse, 0)}`,
}, },
'100%': { '100%': {
boxShadow: `0 0 0 0 ${alpha(theme.palette.primary.main, 0)}`, boxShadow: `0 0 0 0 ${alpha(theme.palette.spotlight.pulse, 0)}`,
}, },
}, },
position: 'relative', position: 'relative',
@ -70,7 +70,7 @@ export interface IDemoStepTooltipProps extends TooltipRenderProps {
step: ITutorialTopicStep; step: ITutorialTopicStep;
topic: number; topic: number;
topics: ITutorialTopic[]; topics: ITutorialTopic[];
steps: number[]; stepIndex: number;
onClose: () => void; onClose: () => void;
onBack: (step: ITutorialTopicStep) => void; onBack: (step: ITutorialTopicStep) => void;
onNext: (step: number) => void; onNext: (step: number) => void;
@ -81,7 +81,7 @@ export const DemoStepTooltip = ({
step, step,
topic, topic,
topics, topics,
steps, stepIndex,
onClose, onClose,
onBack, onBack,
onNext, onNext,
@ -95,6 +95,7 @@ export const DemoStepTooltip = ({
if (r !== 'backdropClick') onClose(); if (r !== 'backdropClick') onClose();
}} }}
transitionDuration={0} transitionDuration={0}
hideBackdrop
> >
<StyledCloseButton aria-label="close" onClick={onClose}> <StyledCloseButton aria-label="close" onClick={onClose}>
<CloseIcon /> <CloseIcon />
@ -114,7 +115,7 @@ export const DemoStepTooltip = ({
<StyledTooltipActions> <StyledTooltipActions>
<div> <div>
<ConditionallyRender <ConditionallyRender
condition={topic > 0 || steps[topic] > 0} condition={topic > 0 || stepIndex > 0}
show={ show={
<Button <Button
variant="outlined" variant="outlined"
@ -130,12 +131,12 @@ export const DemoStepTooltip = ({
condition={Boolean(step.nextButton)} condition={Boolean(step.nextButton)}
show={ show={
<Button <Button
onClick={() => onNext(steps[topic])} onClick={() => onNext(stepIndex)}
variant="contained" variant="contained"
sx={{ alignSelf: 'flex-end' }} sx={{ alignSelf: 'flex-end' }}
> >
{topic === topics.length - 1 && {topic === topics.length - 1 &&
steps[topic] === stepIndex ===
topics[topic].steps.length - 1 topics[topic].steps.length - 1
? 'Finish' ? 'Finish'
: 'Next'} : 'Next'}
@ -169,7 +170,7 @@ export const DemoStepTooltip = ({
<StyledTooltipActions> <StyledTooltipActions>
<div> <div>
<ConditionallyRender <ConditionallyRender
condition={topic > 0 || steps[topic] > 0} condition={topic > 0 || stepIndex > 0}
show={ show={
<Button <Button
variant="outlined" variant="outlined"
@ -185,12 +186,12 @@ export const DemoStepTooltip = ({
condition={Boolean(step.nextButton)} condition={Boolean(step.nextButton)}
show={ show={
<Button <Button
onClick={() => onNext(steps[topic])} onClick={() => onNext(stepIndex)}
variant="contained" variant="contained"
sx={{ alignSelf: 'flex-end' }} sx={{ alignSelf: 'flex-end' }}
> >
{topic === topics.length - 1 && {topic === topics.length - 1 &&
steps[topic] === topics[topic].steps.length - 1 stepIndex === topics[topic].steps.length - 1
? 'Finish' ? 'Finish'
: 'Next'} : 'Next'}
</Button> </Button>

View File

@ -12,8 +12,10 @@ import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
interface IDemoStepsProps { interface IDemoStepsProps {
setExpanded: React.Dispatch<React.SetStateAction<boolean>>; setExpanded: React.Dispatch<React.SetStateAction<boolean>>;
steps: number[]; step: number;
setSteps: React.Dispatch<React.SetStateAction<number[]>>; setStep: React.Dispatch<React.SetStateAction<number>>;
stepsCompletion: number[];
setStepsCompletion: React.Dispatch<React.SetStateAction<number[]>>;
topic: number; topic: number;
setTopic: React.Dispatch<React.SetStateAction<number>>; setTopic: React.Dispatch<React.SetStateAction<number>>;
topics: ITutorialTopic[]; topics: ITutorialTopic[];
@ -22,8 +24,10 @@ interface IDemoStepsProps {
export const DemoSteps = ({ export const DemoSteps = ({
setExpanded, setExpanded,
steps, step,
setSteps, setStep,
stepsCompletion,
setStepsCompletion,
topic, topic,
setTopic, setTopic,
topics, topics,
@ -40,14 +44,17 @@ export const DemoSteps = ({
const setTopicStep = (topic: number, step?: number) => { const setTopicStep = (topic: number, step?: number) => {
setRun(false); setRun(false);
setTopic(topic);
if (step !== undefined) { if (step !== undefined) {
setSteps(steps => { if (stepsCompletion[topic] < step) {
const newSteps = [...steps]; setStepsCompletion(steps => {
newSteps[topic] = step; const newSteps = [...steps];
return newSteps; newSteps[topic] = step;
}); return newSteps;
});
}
setStep(step);
} }
setTopic(topic);
}; };
const close = () => { const close = () => {
@ -58,33 +65,41 @@ export const DemoSteps = ({
props: { props: {
eventType: 'close', eventType: 'close',
topic: topics[topic].title, topic: topics[topic].title,
step: steps[topic] + 1, step: step + 1,
}, },
}); });
}; };
const back = () => { const back = () => {
setFlow('back'); setFlow('back');
if (steps[topic] === 0) { if (step === 0) {
const newTopic = topic - 1; const newTopic = topic - 1;
setTopicStep(newTopic, topics[newTopic].steps.length - 1); setTopicStep(newTopic, topics[newTopic].steps.length - 1);
} else { } else {
setTopicStep(topic, steps[topic] - 1); setTopicStep(topic, step - 1);
} }
}; };
const nextTopic = () => { const nextTopic = () => {
if (topic === topics.length - 1) { const currentTopic = topic;
const nextUnfinishedTopic =
topics.findIndex(
(topic, index) =>
index !== currentTopic &&
stepsCompletion[index] < topic.steps.length
) ?? -1;
if (nextUnfinishedTopic === -1) {
setTopicStep(-1); setTopicStep(-1);
setExpanded(false); setExpanded(false);
onFinish(); onFinish();
} else { } else {
const newTopic = topic + 1; setTopicStep(nextUnfinishedTopic, 0);
setTopicStep(newTopic, 0);
} }
}; };
const next = (index = steps[topic]) => { const next = (index = step) => {
setFlow('next'); setFlow('next');
setTopicStep(topic, index + 1); setTopicStep(topic, index + 1);
if (index === topics[topic].steps.length - 1) { if (index === topics[topic].steps.length - 1) {
@ -137,7 +152,12 @@ export const DemoSteps = ({
'click', 'click',
e => { e => {
const targetEl = e.target as HTMLElement; const targetEl = e.target as HTMLElement;
if (!targetEl.closest('.__floater')) if (
!targetEl.closest('.__floater') &&
!targetEl.className.includes(
'react-joyride__overlay'
)
)
clickHandler(e); clickHandler(e);
}, },
{ {
@ -189,18 +209,18 @@ export const DemoSteps = ({
useEffect(() => { useEffect(() => {
if (topic === -1) return; if (topic === -1) return;
const currentTopic = topics[topic]; const currentTopic = topics[topic];
const currentStepIndex = steps[topic]; const currentStep = currentTopic.steps[step];
const currentStep = currentTopic.steps[currentStepIndex];
if (!currentStep) return; if (!currentStep) return;
if ( if (
currentStep.href && currentStep.href &&
location.pathname !== currentStep.href.split('?')[0] !location.pathname.endsWith(currentStep.href.split('?')[0])
) { ) {
navigate(currentStep.href); navigate(currentStep.href);
} }
waitForLoad(currentStep); waitForLoad(currentStep);
}, [topic, steps]); }, [topic, step]);
useEffect(() => { useEffect(() => {
if (topic > -1) topics[topic].setup?.(); if (topic > -1) topics[topic].setup?.();
@ -216,7 +236,7 @@ export const DemoSteps = ({
return ( return (
<Joyride <Joyride
run={run} run={run}
stepIndex={steps[topic]} stepIndex={step}
callback={joyrideCallback} callback={joyrideCallback}
steps={joyrideSteps} steps={joyrideSteps}
disableScrolling disableScrolling
@ -238,14 +258,12 @@ export const DemoSteps = ({
}, },
spotlight: { spotlight: {
borderRadius: theme.shape.borderRadiusMedium, borderRadius: theme.shape.borderRadiusMedium,
border: `2px solid ${theme.palette.primary.main}`, border: `2px solid ${theme.palette.spotlight.border}`,
outline: `2px solid ${theme.palette.secondary.border}`, outline: `2px solid ${theme.palette.spotlight.outline}`,
backgroundColor: 'transparent',
animation: 'pulse 2s infinite', animation: 'pulse 2s infinite',
}, },
overlay: { overlay: {
backgroundColor: 'transparent', backgroundColor: 'rgba(0, 0, 0, 0.3)',
mixBlendMode: 'unset',
}, },
}} }}
tooltipComponent={( tooltipComponent={(
@ -257,7 +275,7 @@ export const DemoSteps = ({
{...props} {...props}
topic={topic} topic={topic}
topics={topics} topics={topics}
steps={steps} stepIndex={step}
onClose={close} onClose={close}
onBack={onBack} onBack={onBack}
onNext={next} onNext={next}

View File

@ -144,7 +144,7 @@ const StyledButton = styled(Button)(({ theme }) => ({
interface IDemoTopicsProps { interface IDemoTopicsProps {
expanded: boolean; expanded: boolean;
setExpanded: React.Dispatch<React.SetStateAction<boolean>>; setExpanded: React.Dispatch<React.SetStateAction<boolean>>;
steps: number[]; stepsCompletion: number[];
currentTopic: number; currentTopic: number;
setCurrentTopic: (topic: number) => void; setCurrentTopic: (topic: number) => void;
topics: ITutorialTopic[]; topics: ITutorialTopic[];
@ -154,13 +154,16 @@ interface IDemoTopicsProps {
export const DemoTopics = ({ export const DemoTopics = ({
expanded, expanded,
setExpanded, setExpanded,
steps, stepsCompletion,
currentTopic, currentTopic,
setCurrentTopic, setCurrentTopic,
topics, topics,
onWelcome, onWelcome,
}: IDemoTopicsProps) => { }: IDemoTopicsProps) => {
const completedSteps = steps.reduce((acc, step) => acc + (step || 0), 0); const completedSteps = stepsCompletion.reduce(
(acc, step) => acc + (step || 0),
0
);
const totalSteps = topics.flatMap(({ steps }) => steps).length; const totalSteps = topics.flatMap(({ steps }) => steps).length;
const percentage = (completedSteps / totalSteps) * 100; const percentage = (completedSteps / totalSteps) * 100;
@ -194,7 +197,8 @@ export const DemoTopics = ({
</Typography> </Typography>
{topics.map((topic, index) => { {topics.map((topic, index) => {
const selected = currentTopic === index; const selected = currentTopic === index;
const completed = steps[index] === topic.steps.length; const completed =
stepsCompletion[index] === topic.steps.length;
return ( return (
<StyledStep <StyledStep
key={topic.title} key={topic.title}

View File

@ -225,36 +225,33 @@ export const TOPICS: ITutorialTopic[] = [
}, },
{ {
target: 'button[data-testid="STRATEGY_FORM_SUBMIT_ID"]', target: 'button[data-testid="STRATEGY_FORM_SUBMIT_ID"]',
content: <Description>Save your strategy.</Description>,
backCloseModal: true,
},
{
target: 'button[data-testid="DIALOGUE_CONFIRM_ID"]',
content: <Description>Confirm your changes.</Description>,
optional: true,
backCloseModal: true,
},
{
href: `/projects/${PROJECT}?sort=name`,
target: `div[data-testid="TOGGLE-demoApp.step2-${ENVIRONMENT}"]`,
content: ( content: (
<> <>
<Description> <Description>
Save your strategy to apply it. Finally, toggle{' '}
<Badge as="span">demoApp.step2</Badge>
</Description> </Description>
<Badge <Badge
sx={{ marginTop: 2 }} sx={{ marginTop: 2 }}
icon={<InfoOutlinedIcon />} icon={<InfoOutlinedIcon />}
> >
Look at the demo page after saving! Look at the demo page to see your changes!
</Badge> </Badge>
</> </>
), ),
backCloseModal: true, nextButton: true,
},
{
target: 'button[data-testid="DIALOGUE_CONFIRM_ID"]',
content: (
<>
<Description>Confirm your changes.</Description>
<Badge
sx={{ marginTop: 2 }}
icon={<InfoOutlinedIcon />}
>
Look at the demo page after saving!
</Badge>
</>
),
optional: true,
backCloseModal: true,
}, },
], ],
}, },
@ -332,35 +329,32 @@ export const TOPICS: ITutorialTopic[] = [
}, },
{ {
target: 'button[data-testid="STRATEGY_FORM_SUBMIT_ID"]', target: 'button[data-testid="STRATEGY_FORM_SUBMIT_ID"]',
content: <Description>Save your strategy.</Description>,
},
{
target: 'button[data-testid="DIALOGUE_CONFIRM_ID"]',
content: <Description>Confirm your changes.</Description>,
optional: true,
backCloseModal: true,
},
{
href: `/projects/${PROJECT}?sort=name`,
target: `div[data-testid="TOGGLE-demoApp.step3-${ENVIRONMENT}"]`,
content: ( content: (
<> <>
<Description> <Description>
Save your strategy to apply it. Finally, toggle{' '}
<Badge as="span">demoApp.step3</Badge>
</Description> </Description>
<Badge <Badge
sx={{ marginTop: 2 }} sx={{ marginTop: 2 }}
icon={<InfoOutlinedIcon />} icon={<InfoOutlinedIcon />}
> >
Look at the demo page after saving! Look at the demo page to see your changes!
</Badge> </Badge>
</> </>
), ),
}, nextButton: true,
{
target: 'button[data-testid="DIALOGUE_CONFIRM_ID"]',
content: (
<>
<Description>Confirm your changes.</Description>
<Badge
sx={{ marginTop: 2 }}
icon={<InfoOutlinedIcon />}
>
Look at the demo page after saving!
</Badge>
</>
),
optional: true,
backCloseModal: true,
}, },
], ],
}, },
@ -507,19 +501,26 @@ export const TOPICS: ITutorialTopic[] = [
}, },
{ {
target: 'button[data-testid="DIALOGUE_CONFIRM_ID"]', target: 'button[data-testid="DIALOGUE_CONFIRM_ID"]',
content: <Description>Save your variants.</Description>,
},
{
href: `/projects/${PROJECT}?sort=name`,
target: `div[data-testid="TOGGLE-demoApp.step4-${ENVIRONMENT}"]`,
content: ( content: (
<> <>
<Description> <Description>
Save your variants to apply them. Finally, toggle{' '}
<Badge as="span">demoApp.step4</Badge>
</Description> </Description>
<Badge <Badge
sx={{ marginTop: 2 }} sx={{ marginTop: 2 }}
icon={<InfoOutlinedIcon />} icon={<InfoOutlinedIcon />}
> >
Look at the demo page after saving! Look at the demo page to see your changes!
</Badge> </Badge>
</> </>
), ),
nextButton: true,
}, },
], ],
}, },

View File

@ -193,6 +193,15 @@ const theme = {
*/ */
highlight: 'rgba(255, 234, 204, 0.7)', highlight: 'rgba(255, 234, 204, 0.7)',
/**
* Used for the interactive guide spotlight
*/
spotlight: {
border: '#8c89bf',
outline: '#bcb9f3',
pulse: '#bcb9f3',
},
/** /**
* Background color used for the API command in the sidebar * Background color used for the API command in the sidebar
*/ */
@ -268,6 +277,10 @@ export default createTheme({
// Skeleton // Skeleton
MuiCssBaseline: { MuiCssBaseline: {
styleOverrides: { styleOverrides: {
'#react-joyride-portal ~ .MuiDialog-root': {
zIndex: 1500,
},
'.skeleton': { '.skeleton': {
'&::before': { '&::before': {
backgroundColor: theme.palette.background.elevation1, backgroundColor: theme.palette.background.elevation1,

View File

@ -179,6 +179,15 @@ const theme = {
*/ */
highlight: colors.orange[200], highlight: colors.orange[200],
/**
* Used for the interactive guide spotlight
*/
spotlight: {
border: '#463cfb',
outline: '#6058f5',
pulse: '#463cfb',
},
/** /**
* Background color used for the API command in the sidebar * Background color used for the API command in the sidebar
*/ */
@ -254,6 +263,10 @@ export default createTheme({
// Skeleton // Skeleton
MuiCssBaseline: { MuiCssBaseline: {
styleOverrides: { styleOverrides: {
'#react-joyride-portal ~ .MuiDialog-root': {
zIndex: 1500,
},
'.skeleton': { '.skeleton': {
'&::before': { '&::before': {
backgroundColor: theme.palette.background.elevation1, backgroundColor: theme.palette.background.elevation1,

View File

@ -91,6 +91,15 @@ declare module '@mui/material/styles' {
*/ */
highlight: string; highlight: string;
/**
* Used for the interactive guide spotlight
*/
spotlight: {
border: string;
outline: string;
pulse: string;
};
/** /**
* For Links * For Links
*/ */