123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- import {useCallback, useEffect, useMemo, useRef} from 'react';
- import styled from '@emotion/styled';
- import {AnimatePresence, motion} from 'framer-motion';
- import HighlightTopRight from 'sentry-images/pattern/highlight-top-right.svg';
- import {updateOnboardingTask} from 'sentry/actionCreators/onboardingTasks';
- import SidebarPanel from 'sentry/components/sidebar/sidebarPanel';
- import {CommonSidebarProps} from 'sentry/components/sidebar/types';
- import Tooltip from 'sentry/components/tooltip';
- import {t} from 'sentry/locale';
- import space from 'sentry/styles/space';
- import {OnboardingTask, OnboardingTaskKey, Project} from 'sentry/types';
- import testableTransition from 'sentry/utils/testableTransition';
- import useApi from 'sentry/utils/useApi';
- import useOrganization from 'sentry/utils/useOrganization';
- import withProjects from 'sentry/utils/withProjects';
- import {usePersistedOnboardingState} from 'sentry/views/onboarding/utils';
- import ProgressHeader from './progressHeader';
- import Task from './task';
- import {getMergedTasks} from './taskConfig';
- import {findActiveTasks, findCompleteTasks, findUpcomingTasks, taskIsDone} from './utils';
- type Props = Pick<CommonSidebarProps, 'orientation' | 'collapsed'> & {
- onClose: () => void;
- projects: Project[];
- };
- /**
- * How long (in ms) to delay before beginning to mark tasks complete
- */
- const INITIAL_MARK_COMPLETE_TIMEOUT = 600;
- /**
- * How long (in ms) to delay between marking each unseen task as complete.
- */
- const COMPLETION_SEEN_TIMEOUT = 800;
- const Heading = styled(motion.div)`
- display: flex;
- color: ${p => p.theme.purple300};
- font-size: ${p => p.theme.fontSizeExtraSmall};
- text-transform: uppercase;
- font-weight: 600;
- line-height: 1;
- margin-top: ${space(3)};
- `;
- Heading.defaultProps = {
- layout: true,
- transition: testableTransition(),
- };
- const customizedTasksHeading = <Heading key="customized">{t('The Basics')}</Heading>;
- const completeNowHeading = <Heading key="now">{t('Next Steps')}</Heading>;
- const upcomingTasksHeading = (
- <Heading key="upcoming">
- <Tooltip
- containerDisplayMode="block"
- title={t('Some tasks should be completed before completing these tasks')}
- >
- {t('Level Up')}
- </Tooltip>
- </Heading>
- );
- const completedTasksHeading = <Heading key="complete">{t('Completed')}</Heading>;
- function OnboardingWizardSidebar({collapsed, orientation, onClose, projects}: Props) {
- const api = useApi();
- const organization = useOrganization();
- const [onboardingState, setOnboardingState] = usePersistedOnboardingState();
- const markCompletionTimeout = useRef<number | undefined>();
- const markCompletionSeenTimeout = useRef<number | undefined>();
- function completionTimeout(time: number): Promise<void> {
- window.clearTimeout(markCompletionTimeout.current);
- return new Promise(resolve => {
- markCompletionTimeout.current = window.setTimeout(resolve, time);
- });
- }
- function seenTimeout(time: number): Promise<void> {
- window.clearTimeout(markCompletionSeenTimeout.current);
- return new Promise(resolve => {
- markCompletionSeenTimeout.current = window.setTimeout(resolve, time);
- });
- }
- const {allTasks, customTasks, active, upcoming, complete} = useMemo(() => {
- const all = getMergedTasks({
- organization,
- projects,
- onboardingState: onboardingState || undefined,
- }).filter(task => task.display);
- const tasks = all.filter(task => !task.renderCard);
- return {
- allTasks: all,
- customTasks: all.filter(task => task.renderCard),
- active: tasks.filter(findActiveTasks),
- upcoming: tasks.filter(findUpcomingTasks),
- complete: tasks.filter(findCompleteTasks),
- };
- }, [organization, projects, onboardingState]);
- const markTasksAsSeen = useCallback(
- async function () {
- const unseenTasks = allTasks
- .filter(task => taskIsDone(task) && !task.completionSeen)
- .map(task => task.task);
- // Incrementally mark tasks as seen. This gives the card completion
- // animations time before we move each task into the completed section.
- for (const task of unseenTasks) {
- await seenTimeout(COMPLETION_SEEN_TIMEOUT);
- updateOnboardingTask(api, organization, {task, completionSeen: true});
- }
- },
- [api, organization, allTasks]
- );
- const markSeenOnOpen = useCallback(
- async function () {
- // Add a minor delay to marking tasks complete to account for the animation
- // opening of the sidebar panel
- await completionTimeout(INITIAL_MARK_COMPLETE_TIMEOUT);
- markTasksAsSeen();
- },
- [markTasksAsSeen]
- );
- useEffect(() => {
- markSeenOnOpen();
- return () => {
- window.clearTimeout(markCompletionTimeout.current);
- window.clearTimeout(markCompletionSeenTimeout.current);
- };
- }, [markSeenOnOpen]);
- function makeTaskUpdater(status: OnboardingTask['status']) {
- return (task: OnboardingTaskKey) =>
- updateOnboardingTask(api, organization, {task, status, completionSeen: true});
- }
- function renderItem(task: OnboardingTask) {
- return (
- <AnimatedTaskItem
- task={task}
- key={`${task.task}`}
- onSkip={makeTaskUpdater('skipped')}
- onMarkComplete={makeTaskUpdater('complete')}
- />
- );
- }
- const completeList = (
- <CompleteList key="complete-group">
- <AnimatePresence initial={false}>{complete.map(renderItem)}</AnimatePresence>
- </CompleteList>
- );
- const customizedCards = customTasks
- .map(task =>
- task.renderCard?.({
- organization,
- task,
- onboardingState,
- setOnboardingState,
- projects,
- })
- )
- .filter(card => !!card);
- const items = [
- customizedCards.length > 0 && customizedTasksHeading,
- ...customizedCards,
- active.length > 0 && completeNowHeading,
- ...active.map(renderItem),
- upcoming.length > 0 && upcomingTasksHeading,
- ...upcoming.map(renderItem),
- complete.length > 0 && completedTasksHeading,
- completeList,
- ];
- return (
- <TaskSidebarPanel collapsed={collapsed} hidePanel={onClose} orientation={orientation}>
- <TopRight src={HighlightTopRight} />
- <ProgressHeader allTasks={allTasks} completedTasks={complete} />
- <TaskList>
- <AnimatePresence initial={false}>{items}</AnimatePresence>
- </TaskList>
- </TaskSidebarPanel>
- );
- }
- const TaskSidebarPanel = styled(SidebarPanel)`
- width: 450px;
- `;
- const AnimatedTaskItem = motion(Task);
- AnimatedTaskItem.defaultProps = {
- initial: 'initial',
- animate: 'animate',
- exit: 'exit',
- layout: true,
- variants: {
- initial: {
- opacity: 0,
- y: 40,
- },
- animate: {
- opacity: 1,
- y: 0,
- transition: testableTransition({
- delay: 0.8,
- when: 'beforeChildren',
- staggerChildren: 0.3,
- }),
- },
- exit: {
- y: 20,
- z: -10,
- opacity: 0,
- transition: {duration: 0.2},
- },
- },
- };
- const TaskList = styled('div')`
- display: grid;
- grid-auto-flow: row;
- gap: ${space(1)};
- margin: ${space(1)} ${space(4)} ${space(4)} ${space(4)};
- `;
- const CompleteList = styled('div')`
- display: grid;
- grid-auto-flow: row;
- > div {
- transition: border-radius 500ms;
- }
- > div:not(:first-of-type) {
- margin-top: -1px;
- border-top-left-radius: 0;
- border-top-right-radius: 0;
- }
- > div:not(:last-of-type) {
- border-bottom-left-radius: 0;
- border-bottom-right-radius: 0;
- }
- `;
- const TopRight = styled('img')`
- position: absolute;
- top: 0;
- right: 0;
- width: 60%;
- `;
- export default withProjects(OnboardingWizardSidebar);
|