mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-09 00:18:00 +01:00
feat: add demo guide finish dialog (#3578)
https://linear.app/unleash/issue/2-925/add-final-tutorial-complete-dialog Adds the finish dialog, similar to https://github.com/Unleash/unleash/pull/3574 Also includes some fixes and improvements identified in the meantime. Relates to [roadmap](https://github.com/orgs/Unleash/projects/10) item: #3537 ![image](https://user-images.githubusercontent.com/14320932/233421564-447c559b-db70-4f72-a1d2-b6b5bef3707f.png) --------- Co-authored-by: Gastón Fournier <gaston@getunleash.io>
This commit is contained in:
parent
23223e6775
commit
0b4df3f53c
@ -90,13 +90,14 @@
|
|||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-chartjs-2": "4.3.1",
|
"react-chartjs-2": "4.3.1",
|
||||||
|
"react-confetti": "^6.1.0",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-dropzone": "14.2.3",
|
"react-dropzone": "14.2.3",
|
||||||
"react-error-boundary": "3.1.4",
|
"react-error-boundary": "3.1.4",
|
||||||
"react-hooks-global-state": "2.1.0",
|
"react-hooks-global-state": "2.1.0",
|
||||||
"react-joyride": "^2.5.3",
|
"react-joyride": "^2.5.3",
|
||||||
"react-markdown": "^8.0.4",
|
|
||||||
"react-linkify": "^1.0.0-alpha",
|
"react-linkify": "^1.0.0-alpha",
|
||||||
|
"react-markdown": "^8.0.4",
|
||||||
"react-router-dom": "6.8.1",
|
"react-router-dom": "6.8.1",
|
||||||
"react-table": "7.8.0",
|
"react-table": "7.8.0",
|
||||||
"react-test-renderer": "17.0.2",
|
"react-test-renderer": "17.0.2",
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { DemoTopics } from './DemoTopics/DemoTopics';
|
import { DemoTopics } from './DemoTopics/DemoTopics';
|
||||||
import { DemoSteps } from './DemoSteps/DemoSteps';
|
import { DemoSteps } from './DemoSteps/DemoSteps';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import { createLocalStorage } from 'utils/createLocalStorage';
|
import { createLocalStorage } from 'utils/createLocalStorage';
|
||||||
import { TOPICS } from './demo-topics';
|
import { TOPICS } from './demo-topics';
|
||||||
import { DemoDialogWelcome } from './DemoDialog/DemoDialogWelcome/DemoDialogWelcome';
|
import { DemoDialogWelcome } from './DemoDialog/DemoDialogWelcome/DemoDialogWelcome';
|
||||||
|
import { DemoDialogFinish } from './DemoDialog/DemoDialogFinish/DemoDialogFinish';
|
||||||
|
|
||||||
const defaultProgress = {
|
const defaultProgress = {
|
||||||
welcomeOpen: true,
|
welcomeOpen: true,
|
||||||
expanded: true,
|
expanded: true,
|
||||||
active: false,
|
topic: -1,
|
||||||
topic: 0,
|
|
||||||
steps: [0],
|
steps: [0],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -19,34 +17,47 @@ const { value: storedProgress, setValue: setStoredProgress } =
|
|||||||
createLocalStorage('Tutorial:v1', defaultProgress);
|
createLocalStorage('Tutorial:v1', defaultProgress);
|
||||||
|
|
||||||
export const Demo = () => {
|
export const Demo = () => {
|
||||||
const { uiConfig } = useUiConfig();
|
|
||||||
const [welcomeOpen, setWelcomeOpen] = useState(
|
const [welcomeOpen, setWelcomeOpen] = useState(
|
||||||
storedProgress.welcomeOpen ?? true
|
storedProgress.welcomeOpen ?? defaultProgress.welcomeOpen
|
||||||
);
|
);
|
||||||
const [active, setActive] = useState(false);
|
const [finishOpen, setFinishOpen] = useState(false);
|
||||||
const [expanded, setExpanded] = useState(storedProgress.expanded ?? true);
|
|
||||||
const [topic, setTopic] = useState(storedProgress.topic ?? 0);
|
|
||||||
const [steps, setSteps] = useState(storedProgress.steps ?? [0]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const [expanded, setExpanded] = useState(
|
||||||
if (storedProgress.active) {
|
storedProgress.expanded ?? defaultProgress.expanded
|
||||||
setTimeout(() => {
|
);
|
||||||
setActive(true);
|
const [topic, setTopic] = useState(
|
||||||
}, 1000);
|
storedProgress.topic ?? defaultProgress.topic
|
||||||
}
|
);
|
||||||
}, []);
|
const [steps, setSteps] = useState(
|
||||||
|
storedProgress.steps ?? defaultProgress.steps
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setStoredProgress({
|
setStoredProgress({
|
||||||
welcomeOpen,
|
welcomeOpen,
|
||||||
expanded,
|
expanded,
|
||||||
active,
|
|
||||||
topic,
|
topic,
|
||||||
steps,
|
steps,
|
||||||
});
|
});
|
||||||
}, [welcomeOpen, expanded, active, topic, steps]);
|
}, [welcomeOpen, expanded, topic, steps]);
|
||||||
|
|
||||||
if (!uiConfig.flags.demo) return null;
|
const onStart = () => {
|
||||||
|
setTopic(0);
|
||||||
|
setSteps([0]);
|
||||||
|
setExpanded(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFinish = () => {
|
||||||
|
const completedSteps = steps.reduce(
|
||||||
|
(acc, step) => acc + (step || 0),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
const totalSteps = TOPICS.flatMap(({ steps }) => steps).length;
|
||||||
|
|
||||||
|
if (completedSteps === totalSteps) {
|
||||||
|
setFinishOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -58,7 +69,17 @@ export const Demo = () => {
|
|||||||
}}
|
}}
|
||||||
onStart={() => {
|
onStart={() => {
|
||||||
setWelcomeOpen(false);
|
setWelcomeOpen(false);
|
||||||
setActive(true);
|
onStart();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<DemoDialogFinish
|
||||||
|
open={finishOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setFinishOpen(false);
|
||||||
|
}}
|
||||||
|
onRestart={() => {
|
||||||
|
setFinishOpen(false);
|
||||||
|
onStart();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<DemoTopics
|
<DemoTopics
|
||||||
@ -75,20 +96,16 @@ export const Demo = () => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
topics={TOPICS}
|
topics={TOPICS}
|
||||||
onShowWelcome={() => setWelcomeOpen(true)}
|
onWelcome={() => setWelcomeOpen(true)}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<DemoSteps
|
||||||
condition={active}
|
setExpanded={setExpanded}
|
||||||
show={
|
steps={steps}
|
||||||
<DemoSteps
|
setSteps={setSteps}
|
||||||
setExpanded={setExpanded}
|
topic={topic}
|
||||||
steps={steps}
|
setTopic={setTopic}
|
||||||
setSteps={setSteps}
|
topics={TOPICS}
|
||||||
topic={topic}
|
onFinish={onFinish}
|
||||||
setTopic={setTopic}
|
|
||||||
topics={TOPICS}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,66 @@
|
|||||||
|
import { Button, Typography, styled } from '@mui/material';
|
||||||
|
import { DemoDialog } from '../DemoDialog';
|
||||||
|
import Confetti from 'react-confetti';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
|
const StyledActions = styled('div')(({ theme }) => ({
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: '1fr 1fr',
|
||||||
|
gap: theme.spacing(3),
|
||||||
|
marginTop: theme.spacing(7.5),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledButton = styled(Button)(({ theme }) => ({
|
||||||
|
height: theme.spacing(7),
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface IDemoDialogFinishProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onRestart: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DemoDialogFinish = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onRestart,
|
||||||
|
}: IDemoDialogFinishProps) => (
|
||||||
|
<>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={open}
|
||||||
|
show={
|
||||||
|
<Confetti
|
||||||
|
recycle={false}
|
||||||
|
numberOfPieces={1000}
|
||||||
|
initialVelocityY={50}
|
||||||
|
gravity={0.3}
|
||||||
|
style={{ zIndex: 3000 }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DemoDialog open={open} onClose={onClose}>
|
||||||
|
<DemoDialog.Header>You finished the tutorial</DemoDialog.Header>
|
||||||
|
<Typography color="textSecondary" sx={{ mt: 4 }}>
|
||||||
|
Great job! Keep exploring Unleash, as this was just a small
|
||||||
|
example of its full potential. You can do the tutorial again at
|
||||||
|
any moment.
|
||||||
|
</Typography>
|
||||||
|
<StyledActions>
|
||||||
|
<StyledButton
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
onClick={onRestart}
|
||||||
|
>
|
||||||
|
Restart tutorial
|
||||||
|
</StyledButton>
|
||||||
|
<StyledButton
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</StyledButton>
|
||||||
|
</StyledActions>
|
||||||
|
</DemoDialog>
|
||||||
|
</>
|
||||||
|
);
|
@ -50,6 +50,7 @@ interface IDemoStepsProps {
|
|||||||
topic: number;
|
topic: number;
|
||||||
setTopic: React.Dispatch<React.SetStateAction<number>>;
|
setTopic: React.Dispatch<React.SetStateAction<number>>;
|
||||||
topics: ITutorialTopic[];
|
topics: ITutorialTopic[];
|
||||||
|
onFinish: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DemoSteps = ({
|
export const DemoSteps = ({
|
||||||
@ -59,54 +60,58 @@ export const DemoSteps = ({
|
|||||||
topic,
|
topic,
|
||||||
setTopic,
|
setTopic,
|
||||||
topics,
|
topics,
|
||||||
|
onFinish,
|
||||||
}: IDemoStepsProps) => {
|
}: IDemoStepsProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [run, setRun] = useState(false);
|
const [run, setRun] = useState(false);
|
||||||
const [flow, setFlow] = useState<'next' | 'back'>('next');
|
const [flow, setFlow] = useState<'next' | 'back' | 'load'>('load');
|
||||||
|
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
|
|
||||||
const skip = () => {
|
const setTopicStep = (topic: number, step?: number) => {
|
||||||
abortController.abort();
|
setRun(false);
|
||||||
setTopic(-1);
|
setTopic(topic);
|
||||||
setExpanded(false);
|
if (step !== undefined) {
|
||||||
|
setSteps(steps => {
|
||||||
|
const newSteps = [...steps];
|
||||||
|
newSteps[topic] = step;
|
||||||
|
return newSteps;
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const setStep = (topic: number, step: number) => {
|
const skip = () => {
|
||||||
setSteps(steps => {
|
abortController.abort();
|
||||||
const newSteps = [...steps];
|
setTopicStep(-1);
|
||||||
newSteps[topic] = step;
|
setExpanded(false);
|
||||||
return newSteps;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const back = () => {
|
const back = () => {
|
||||||
setFlow('back');
|
setFlow('back');
|
||||||
if (steps[topic] === 0) {
|
if (steps[topic] === 0) {
|
||||||
const newTopic = topic - 1;
|
const newTopic = topic - 1;
|
||||||
setTopic(newTopic);
|
setTopicStep(newTopic, topics[newTopic].steps.length - 1);
|
||||||
setStep(newTopic, topics[newTopic].steps.length - 1);
|
|
||||||
} else {
|
} else {
|
||||||
setStep(topic, steps[topic] - 1);
|
setTopicStep(topic, steps[topic] - 1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextTopic = () => {
|
const nextTopic = () => {
|
||||||
if (topic === topics.length - 1) {
|
if (topic === topics.length - 1) {
|
||||||
setTopic(-1);
|
setTopicStep(-1);
|
||||||
setExpanded(false);
|
setExpanded(false);
|
||||||
|
onFinish();
|
||||||
} else {
|
} else {
|
||||||
const newTopic = topic + 1;
|
const newTopic = topic + 1;
|
||||||
setTopic(newTopic);
|
setTopicStep(newTopic, 0);
|
||||||
setStep(newTopic, 0);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const next = (index = steps[topic]) => {
|
const next = (index = steps[topic]) => {
|
||||||
setFlow('next');
|
setFlow('next');
|
||||||
setStep(topic, index + 1);
|
setTopicStep(topic, index + 1);
|
||||||
if (index === topics[topic].steps.length - 1) {
|
if (index === topics[topic].steps.length - 1) {
|
||||||
nextTopic();
|
nextTopic();
|
||||||
}
|
}
|
||||||
@ -146,14 +151,6 @@ export const DemoSteps = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (run && !document.querySelector(step.target as string)) {
|
|
||||||
if (step.optional && flow === 'next') {
|
|
||||||
next();
|
|
||||||
} else {
|
|
||||||
back();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onBack = (step: ITutorialTopicStep) => {
|
const onBack = (step: ITutorialTopicStep) => {
|
||||||
@ -172,22 +169,39 @@ export const DemoSteps = ({
|
|||||||
back();
|
back();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const waitForLoad = (step: ITutorialTopicStep, tries = 0) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (document.querySelector(step.target as string)) {
|
||||||
|
setRun(true);
|
||||||
|
} else {
|
||||||
|
if (flow === 'next' && step.optional) {
|
||||||
|
next();
|
||||||
|
} else if (flow === 'back' || tries > 4) {
|
||||||
|
back();
|
||||||
|
} else {
|
||||||
|
waitForLoad(step, tries + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRun(false);
|
|
||||||
if (topic === -1) return;
|
if (topic === -1) return;
|
||||||
const currentTopic = topics[topic];
|
const currentTopic = topics[topic];
|
||||||
const currentStep = steps[topic];
|
const currentStepIndex = steps[topic];
|
||||||
const href = currentTopic.steps[currentStep]?.href;
|
const currentStep = currentTopic.steps[currentStepIndex];
|
||||||
if (href && location.pathname !== href) {
|
if (!currentStep) return;
|
||||||
navigate(href);
|
|
||||||
}
|
|
||||||
currentTopic.setup?.();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
if (currentStep.href && location.pathname !== currentStep.href) {
|
||||||
setRun(true);
|
navigate(currentStep.href);
|
||||||
}, 200);
|
}
|
||||||
|
waitForLoad(currentStep);
|
||||||
}, [topic, steps]);
|
}, [topic, steps]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (topic > -1) topics[topic].setup?.();
|
||||||
|
}, [topic]);
|
||||||
|
|
||||||
if (topic === -1) return null;
|
if (topic === -1) return null;
|
||||||
|
|
||||||
const joyrideSteps = topics[topic].steps.map(step => ({
|
const joyrideSteps = topics[topic].steps.map(step => ({
|
||||||
|
@ -138,7 +138,7 @@ interface IDemoTopicsProps {
|
|||||||
currentTopic: number;
|
currentTopic: number;
|
||||||
setCurrentTopic: (topic: number) => void;
|
setCurrentTopic: (topic: number) => void;
|
||||||
topics: ITutorialTopic[];
|
topics: ITutorialTopic[];
|
||||||
onShowWelcome: () => void;
|
onWelcome: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DemoTopics = ({
|
export const DemoTopics = ({
|
||||||
@ -148,7 +148,7 @@ export const DemoTopics = ({
|
|||||||
currentTopic,
|
currentTopic,
|
||||||
setCurrentTopic,
|
setCurrentTopic,
|
||||||
topics,
|
topics,
|
||||||
onShowWelcome,
|
onWelcome,
|
||||||
}: IDemoTopicsProps) => {
|
}: IDemoTopicsProps) => {
|
||||||
const completedSteps = steps.reduce((acc, step) => acc + (step || 0), 0);
|
const completedSteps = steps.reduce((acc, step) => acc + (step || 0), 0);
|
||||||
const totalSteps = topics.flatMap(({ steps }) => steps).length;
|
const totalSteps = topics.flatMap(({ steps }) => steps).length;
|
||||||
@ -203,7 +203,7 @@ export const DemoTopics = ({
|
|||||||
</StyledStep>
|
</StyledStep>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<StyledButton variant="outlined" onClick={onShowWelcome}>
|
<StyledButton variant="outlined" onClick={onWelcome}>
|
||||||
View demo link again
|
View demo link again
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
|
@ -185,6 +185,7 @@ export const TOPICS: ITutorialTopic[] = [
|
|||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
optional: true,
|
optional: true,
|
||||||
|
backCloseModal: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -289,6 +290,7 @@ export const TOPICS: ITutorialTopic[] = [
|
|||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
optional: true,
|
optional: true,
|
||||||
|
backCloseModal: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -7498,6 +7498,13 @@ react-chartjs-2@4.3.1:
|
|||||||
resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-4.3.1.tgz#9941e7397fb963f28bb557addb401e9ff96c6681"
|
resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-4.3.1.tgz#9941e7397fb963f28bb557addb401e9ff96c6681"
|
||||||
integrity sha512-5i3mjP6tU7QSn0jvb8I4hudTzHJqS8l00ORJnVwI2sYu0ihpj83Lv2YzfxunfxTZkscKvZu2F2w9LkwNBhj6xA==
|
integrity sha512-5i3mjP6tU7QSn0jvb8I4hudTzHJqS8l00ORJnVwI2sYu0ihpj83Lv2YzfxunfxTZkscKvZu2F2w9LkwNBhj6xA==
|
||||||
|
|
||||||
|
react-confetti@^6.1.0:
|
||||||
|
version "6.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-confetti/-/react-confetti-6.1.0.tgz#03dc4340d955acd10b174dbf301f374a06e29ce6"
|
||||||
|
integrity sha512-7Ypx4vz0+g8ECVxr88W9zhcQpbeujJAVqL14ZnXJ3I23mOI9/oBVTQ3dkJhUmB0D6XOtCZEM6N0Gm9PMngkORw==
|
||||||
|
dependencies:
|
||||||
|
tween-functions "^1.2.0"
|
||||||
|
|
||||||
react-dom@17.0.2:
|
react-dom@17.0.2:
|
||||||
version "17.0.2"
|
version "17.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
|
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
|
||||||
@ -8616,6 +8623,11 @@ tunnel-agent@^0.6.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer "^5.0.1"
|
safe-buffer "^5.0.1"
|
||||||
|
|
||||||
|
tween-functions@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/tween-functions/-/tween-functions-1.2.0.tgz#1ae3a50e7c60bb3def774eac707acbca73bbc3ff"
|
||||||
|
integrity sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==
|
||||||
|
|
||||||
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
|
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
|
||||||
version "0.14.5"
|
version "0.14.5"
|
||||||
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
|
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
|
||||||
|
Loading…
Reference in New Issue
Block a user