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

View File

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

View File

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

View File

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

View File

@ -225,36 +225,33 @@ export const TOPICS: ITutorialTopic[] = [
},
{
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: (
<>
<Description>
Save your strategy to apply it.
Finally, toggle{' '}
<Badge as="span">demoApp.step2</Badge>
</Description>
<Badge
sx={{ marginTop: 2 }}
icon={<InfoOutlinedIcon />}
>
Look at the demo page after saving!
Look at the demo page to see your changes!
</Badge>
</>
),
backCloseModal: 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,
nextButton: true,
},
],
},
@ -332,35 +329,32 @@ export const TOPICS: ITutorialTopic[] = [
},
{
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: (
<>
<Description>
Save your strategy to apply it.
Finally, toggle{' '}
<Badge as="span">demoApp.step3</Badge>
</Description>
<Badge
sx={{ marginTop: 2 }}
icon={<InfoOutlinedIcon />}
>
Look at the demo page after saving!
Look at the demo page to see your changes!
</Badge>
</>
),
},
{
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,
nextButton: true,
},
],
},
@ -507,19 +501,26 @@ export const TOPICS: ITutorialTopic[] = [
},
{
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: (
<>
<Description>
Save your variants to apply them.
Finally, toggle{' '}
<Badge as="span">demoApp.step4</Badge>
</Description>
<Badge
sx={{ marginTop: 2 }}
icon={<InfoOutlinedIcon />}
>
Look at the demo page after saving!
Look at the demo page to see your changes!
</Badge>
</>
),
nextButton: true,
},
],
},

View File

@ -193,6 +193,15 @@ const theme = {
*/
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
*/
@ -268,6 +277,10 @@ export default createTheme({
// Skeleton
MuiCssBaseline: {
styleOverrides: {
'#react-joyride-portal ~ .MuiDialog-root': {
zIndex: 1500,
},
'.skeleton': {
'&::before': {
backgroundColor: theme.palette.background.elevation1,

View File

@ -179,6 +179,15 @@ const theme = {
*/
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
*/
@ -254,6 +263,10 @@ export default createTheme({
// Skeleton
MuiCssBaseline: {
styleOverrides: {
'#react-joyride-portal ~ .MuiDialog-root': {
zIndex: 1500,
},
'.skeleton': {
'&::before': {
backgroundColor: theme.palette.background.elevation1,

View File

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