Skip to content

Commit

Permalink
ref(quick-start): Add task completion animation (#82523)
Browse files Browse the repository at this point in the history
  • Loading branch information
priscilawebdev authored Jan 7, 2025
1 parent fa39340 commit dfa10eb
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 79 deletions.
13 changes: 9 additions & 4 deletions static/app/components/onboardingWizard/newSidebar.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ describe('NewSidebar', function () {
collapsed={false}
gettingStartedTasks={gettingStartedTasks.map(task => ({
...task,
completionSeen: true,
status: 'complete',
}))}
beyondBasicsTasks={beyondBasicsTasks}
Expand Down Expand Up @@ -130,6 +131,7 @@ describe('NewSidebar', function () {
// Tasks from the second group should be visible
expect(await screen.findByText(beyondBasicsTasks[0]!.title)).toBeInTheDocument();

// Click skip task
await userEvent.click(screen.getByRole('button', {name: 'Skip Task'}));

// Confirmation to skip should be visible
Expand All @@ -146,6 +148,13 @@ describe('NewSidebar', function () {
expect(screen.getByRole('link', {name: 'Join our Discord'})).toBeInTheDocument();
expect(screen.getByRole('link', {name: 'Visit Help Center'})).toBeInTheDocument();

// Dismiss skip confirmation
await userEvent.click(screen.getByRole('button', {name: 'Dismiss Skip'}));
expect(screen.queryByText(/Not sure what to do/)).not.toBeInTheDocument();

// Click skip task again
await userEvent.click(screen.getByRole('button', {name: 'Skip Task'}));

// Click 'Just Skip'
await userEvent.click(screen.getByRole('button', {name: 'Just Skip'}));
await waitFor(() => {
Expand All @@ -159,9 +168,5 @@ describe('NewSidebar', function () {
})
);
});

// Dismiss skip confirmation
await userEvent.click(screen.getByRole('button', {name: 'Dismiss Skip'}));
expect(screen.queryByText(/Not sure what to do/)).not.toBeInTheDocument();
});
});
128 changes: 100 additions & 28 deletions static/app/components/onboardingWizard/newSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Fragment, useCallback, useEffect, useMemo, useState} from 'react';
import {css, useTheme} from '@emotion/react';
import styled from '@emotion/styled';
import {motion} from 'framer-motion';
import {AnimatePresence, motion} from 'framer-motion';
import partition from 'lodash/partition';

import HighlightTopRight from 'sentry-images/pattern/highlight-top-right.svg';
Expand All @@ -14,7 +14,7 @@ import {Chevron} from 'sentry/components/chevron';
import {DropdownMenu} from 'sentry/components/dropdownMenu';
import InteractionStateLayer from 'sentry/components/interactionStateLayer';
import type {useOnboardingTasks} from 'sentry/components/onboardingWizard/useOnboardingTasks';
import {taskIsDone} from 'sentry/components/onboardingWizard/utils';
import {findCompleteTasks, taskIsDone} from 'sentry/components/onboardingWizard/utils';
import ProgressRing from 'sentry/components/progressRing';
import SidebarPanel from 'sentry/components/sidebar/sidebarPanel';
import type {CommonSidebarProps} from 'sentry/components/sidebar/types';
Expand All @@ -34,6 +34,7 @@ import {space} from 'sentry/styles/space';
import {type OnboardingTask, OnboardingTaskKey} from 'sentry/types/onboarding';
import {trackAnalytics} from 'sentry/utils/analytics';
import {isDemoModeEnabled} from 'sentry/utils/demoMode';
import testableTransition from 'sentry/utils/testableTransition';
import useApi from 'sentry/utils/useApi';
import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
import useOrganization from 'sentry/utils/useOrganization';
Expand All @@ -57,7 +58,9 @@ const orderedBeyondBasicsTasks = [
];

function groupTasksByCompletion(tasks: OnboardingTask[]) {
const [completedTasks, incompletedTasks] = partition(tasks, task => taskIsDone(task));
const [completedTasks, incompletedTasks] = partition(tasks, task =>
findCompleteTasks(task)
);
return {
completedTasks,
incompletedTasks,
Expand Down Expand Up @@ -129,7 +132,7 @@ function TaskStatusIcon({status, tooltipText, progress}: TaskStatusIconProps) {
`}
/>
) : status === 'pending' ? (
<IconSync // TODO(Telemetry): Remove pending status
<IconSync
css={css`
color: ${theme.pink400};
height: ${theme.fontSizeLarge};
Expand Down Expand Up @@ -322,7 +325,7 @@ function Task({task, hidePanel, showWaitingIndicator}: TaskProps) {
}, [task.status, task.pendingTitle]);

return (
<TaskWrapper>
<TaskWrapper layout={showSkipConfirmation ? false : true}>
<TaskCard
onClick={
task.status === 'complete' || task.status === 'skipped'
Expand Down Expand Up @@ -365,14 +368,67 @@ function Task({task, hidePanel, showWaitingIndicator}: TaskProps) {
/>
{showSkipConfirmation && (
<SkipConfirmation
onConfirm={() => handleMarkSkipped(task.task)}
onConfirm={() => {
handleMarkSkipped(task.task);
setShowSkipConfirmation(false);
}}
onDismiss={() => setShowSkipConfirmation(false)}
/>
)}
</TaskWrapper>
);
}

interface ExpandedTaskGroupProps {
hidePanel: () => void;
sortedTasks: OnboardingTask[];
taskKeyForWaitingIndicator: OnboardingTaskKey | undefined;
}

function ExpandedTaskGroup({
sortedTasks,
hidePanel,
taskKeyForWaitingIndicator,
}: ExpandedTaskGroupProps) {
const api = useApi();
const organization = useOrganization();

const markTasksAsSeen = useCallback(() => {
const unseenDoneTasks = sortedTasks
.filter(task => taskIsDone(task) && !task.completionSeen)
.map(task => task.task);

for (const unseenDoneTask of unseenDoneTasks) {
updateOnboardingTask(api, organization, {
task: unseenDoneTask,
completionSeen: true,
});
}
}, [api, organization, sortedTasks]);

useEffect(() => {
markTasksAsSeen();
}, []); // eslint-disable-line react-hooks/exhaustive-deps

return (
<Fragment>
<hr />
<TaskGroupBody>
<AnimatePresence initial={false}>
{sortedTasks.map(sortedTask => (
<Task
key={sortedTask.task}
task={sortedTask}
hidePanel={hidePanel}
showWaitingIndicator={taskKeyForWaitingIndicator === sortedTask.task}
/>
))}
</AnimatePresence>
</TaskGroupBody>
</Fragment>
);
}

interface TaskGroupProps {
/**
* Used for analytics
Expand All @@ -398,11 +454,16 @@ function TaskGroup({
const organization = useOrganization();
const [isExpanded, setIsExpanded] = useState(expanded);
const {completedTasks, incompletedTasks} = groupTasksByCompletion(tasks);

const [taskGroupComplete, setTaskGroupComplete] = useLocalStorageState(
`quick-start:${organization.slug}:${group}-completed`,
false
);

const doneTasks = useMemo(() => {
return tasks.filter(task => taskIsDone(task));
}, [tasks]);

useEffect(() => {
setIsExpanded(expanded);
}, [expanded]);
Expand Down Expand Up @@ -434,20 +495,20 @@ function TaskGroup({
description={
tasks.length > 1
? tct('[totalCompletedTasks] out of [totalTasks] tasks completed', {
totalCompletedTasks: completedTasks.length,
totalCompletedTasks: doneTasks.length,
totalTasks: tasks.length,
})
: tct('[totalCompletedTasks] out of [totalTasks] task completed', {
totalCompletedTasks: completedTasks.length,
totalCompletedTasks: doneTasks.length,
totalTasks: tasks.length,
})
}
hasProgress={completedTasks.length > 0}
hasProgress={doneTasks.length > 0}
onClick={toggleable ? () => setIsExpanded(!isExpanded) : undefined}
icon={
<TaskStatusIcon
status={completedTasks.length === tasks.length ? 'complete' : 'inProgress'}
progress={completedTasks.length / tasks.length}
status={doneTasks.length === tasks.length ? 'complete' : 'inProgress'}
progress={doneTasks.length / tasks.length}
/>
}
actions={
Expand All @@ -461,22 +522,11 @@ function TaskGroup({
}
/>
{isExpanded && (
<Fragment>
<hr />
<TaskGroupBody>
{incompletedTasks.map(task => (
<Task
key={task.task}
task={task}
hidePanel={hidePanel}
showWaitingIndicator={taskKeyForWaitingIndicator === task.task}
/>
))}
{completedTasks.map(task => (
<Task key={task.task} task={task} hidePanel={hidePanel} />
))}
</TaskGroupBody>
</Fragment>
<ExpandedTaskGroup
sortedTasks={[...incompletedTasks, ...completedTasks]}
hidePanel={hidePanel}
taskKeyForWaitingIndicator={taskKeyForWaitingIndicator}
/>
)}
</TaskGroupWrapper>
);
Expand Down Expand Up @@ -592,7 +642,7 @@ const TaskGroupHeader = styled(TaskCard)<{hasProgress: boolean}>`
}
`;

const TaskGroupBody = styled(motion.ul)`
const TaskGroupBody = styled('ul')`
border-radius: ${p => p.theme.borderRadius};
list-style-type: none;
padding: 0;
Expand All @@ -601,8 +651,30 @@ const TaskGroupBody = styled(motion.ul)`

const TaskWrapper = styled(motion.li)`
gap: ${space(1)};
background-color: ${p => p.theme.background};
`;

TaskWrapper.defaultProps = {
initial: false,
animate: 'animate',
layout: true,
variants: {
initial: {
opacity: 0,
y: 40,
},
animate: {
opacity: 1,
y: 0,
transition: testableTransition({
delay: 0.8,
when: 'beforeChildren',
staggerChildren: 0.3,
}),
},
},
};

const TaskActions = styled('div')`
display: flex;
flex-direction: column;
Expand Down
12 changes: 2 additions & 10 deletions static/app/components/sidebar/newOnboardingStatus.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,7 @@ function renderMockRequests(organization: Organization) {
},
});

const postOnboardingTasksMock = MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/onboarding-tasks/`,
method: 'POST',
body: {task: OnboardingTaskKey.FIRST_PROJECT, completionSeen: true},
});

return {getOnboardingTasksMock, postOnboardingTasksMock};
return {getOnboardingTasksMock};
}

describe('Onboarding Status', function () {
Expand All @@ -41,8 +35,7 @@ describe('Onboarding Status', function () {
],
});

const {getOnboardingTasksMock, postOnboardingTasksMock} =
renderMockRequests(organization);
const {getOnboardingTasksMock} = renderMockRequests(organization);

const handleShowPanel = jest.fn();

Expand All @@ -69,7 +62,6 @@ describe('Onboarding Status', function () {
// Open the panel
await userEvent.click(screen.getByRole('button', {name: 'Onboarding'}));
await waitFor(() => expect(getOnboardingTasksMock).toHaveBeenCalled());
await waitFor(() => expect(postOnboardingTasksMock).toHaveBeenCalled());
expect(handleShowPanel).toHaveBeenCalled();
});

Expand Down
Loading

0 comments on commit dfa10eb

Please sign in to comment.