sidebar.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. import {Component} from 'react';
  2. import styled from '@emotion/styled';
  3. import {AnimatePresence, motion} from 'framer-motion';
  4. import HighlightTopRight from 'sentry-images/pattern/highlight-top-right.svg';
  5. import {updateOnboardingTask} from 'app/actionCreators/onboardingTasks';
  6. import {Client} from 'app/api';
  7. import SidebarPanel from 'app/components/sidebar/sidebarPanel';
  8. import {CommonSidebarProps} from 'app/components/sidebar/types';
  9. import Tooltip from 'app/components/tooltip';
  10. import {t} from 'app/locale';
  11. import space from 'app/styles/space';
  12. import {OnboardingTask, OnboardingTaskKey, Organization} from 'app/types';
  13. import testableTransition from 'app/utils/testableTransition';
  14. import withApi from 'app/utils/withApi';
  15. import withOrganization from 'app/utils/withOrganization';
  16. import ProgressHeader from './progressHeader';
  17. import Task from './task';
  18. import {getMergedTasks} from './taskConfig';
  19. import {findActiveTasks, findCompleteTasks, findUpcomingTasks, taskIsDone} from './utils';
  20. type Props = Pick<CommonSidebarProps, 'orientation' | 'collapsed'> & {
  21. api: Client;
  22. organization: Organization;
  23. onClose: () => void;
  24. };
  25. /**
  26. * How long (in ms) to delay before beginning to mark tasks complete
  27. */
  28. const INITIAL_MARK_COMPLETE_TIMEOUT = 600;
  29. /**
  30. * How long (in ms) to delay between marking each unseen task as complete.
  31. */
  32. const COMPLETION_SEEN_TIMEOUT = 800;
  33. const doTimeout = (timeout: number) =>
  34. new Promise(resolve => setTimeout(resolve, timeout));
  35. const Heading = styled(motion.div)`
  36. display: flex;
  37. color: ${p => p.theme.purple300};
  38. font-size: ${p => p.theme.fontSizeExtraSmall};
  39. text-transform: uppercase;
  40. font-weight: 600;
  41. line-height: 1;
  42. margin-top: ${space(3)};
  43. `;
  44. Heading.defaultProps = {
  45. layout: true,
  46. transition: testableTransition(),
  47. };
  48. const completeNowHeading = <Heading key="now">{t('The Basics')}</Heading>;
  49. const upcomingTasksHeading = (
  50. <Heading key="upcoming">
  51. <Tooltip
  52. containerDisplayMode="block"
  53. title={t('Some tasks should be completed before completing these tasks')}
  54. >
  55. {t('Level Up')}
  56. </Tooltip>
  57. </Heading>
  58. );
  59. const completedTasksHeading = <Heading key="complete">{t('Completed')}</Heading>;
  60. class OnboardingWizardSidebar extends Component<Props> {
  61. async componentDidMount() {
  62. // Add a minor delay to marking tasks complete to account for the animation
  63. // opening of the sidebar panel
  64. await doTimeout(INITIAL_MARK_COMPLETE_TIMEOUT);
  65. this.markTasksAsSeen();
  66. }
  67. async markTasksAsSeen() {
  68. const unseenTasks = this.segmentedTasks.all
  69. .filter(task => taskIsDone(task) && !task.completionSeen)
  70. .map(task => task.task);
  71. // Incrementally mark tasks as seen. This gives the card completion
  72. // animations time before we move each task into the completed section.
  73. for (const task of unseenTasks) {
  74. await doTimeout(COMPLETION_SEEN_TIMEOUT);
  75. const {api, organization} = this.props;
  76. updateOnboardingTask(api, organization, {
  77. task,
  78. completionSeen: true,
  79. });
  80. }
  81. }
  82. get segmentedTasks() {
  83. const {organization} = this.props;
  84. const all = getMergedTasks(organization).filter(task => task.display);
  85. const active = all.filter(findActiveTasks);
  86. const upcoming = all.filter(findUpcomingTasks);
  87. const complete = all.filter(findCompleteTasks);
  88. return {active, upcoming, complete, all};
  89. }
  90. makeTaskUpdater = (status: OnboardingTask['status']) => (task: OnboardingTaskKey) => {
  91. const {api, organization} = this.props;
  92. updateOnboardingTask(api, organization, {task, status, completionSeen: true});
  93. };
  94. renderItem = (task: OnboardingTask) => (
  95. <AnimatedTaskItem
  96. task={task}
  97. key={`${task.task}`}
  98. onSkip={this.makeTaskUpdater('skipped')}
  99. onMarkComplete={this.makeTaskUpdater('complete')}
  100. />
  101. );
  102. render() {
  103. const {collapsed, orientation, onClose} = this.props;
  104. const {all, active, upcoming, complete} = this.segmentedTasks;
  105. const completeList = (
  106. <CompleteList key="complete-group">
  107. <AnimatePresence initial={false}>{complete.map(this.renderItem)}</AnimatePresence>
  108. </CompleteList>
  109. );
  110. const items = [
  111. active.length > 0 && completeNowHeading,
  112. ...active.map(this.renderItem),
  113. upcoming.length > 0 && upcomingTasksHeading,
  114. ...upcoming.map(this.renderItem),
  115. complete.length > 0 && completedTasksHeading,
  116. completeList,
  117. ];
  118. return (
  119. <TaskSidebarPanel
  120. collapsed={collapsed}
  121. hidePanel={onClose}
  122. orientation={orientation}
  123. >
  124. <TopRight src={HighlightTopRight} />
  125. <ProgressHeader allTasks={all} completedTasks={complete} />
  126. <TaskList>
  127. <AnimatePresence initial={false}>{items}</AnimatePresence>
  128. </TaskList>
  129. </TaskSidebarPanel>
  130. );
  131. }
  132. }
  133. const TaskSidebarPanel = styled(SidebarPanel)`
  134. width: 450px;
  135. `;
  136. const AnimatedTaskItem = motion(Task);
  137. AnimatedTaskItem.defaultProps = {
  138. initial: 'initial',
  139. animate: 'animate',
  140. exit: 'exit',
  141. layout: true,
  142. variants: {
  143. initial: {
  144. opacity: 0,
  145. y: 40,
  146. },
  147. animate: {
  148. opacity: 1,
  149. y: 0,
  150. transition: testableTransition({
  151. delay: 0.8,
  152. when: 'beforeChildren',
  153. staggerChildren: 0.3,
  154. }),
  155. },
  156. exit: {
  157. y: 20,
  158. z: -10,
  159. opacity: 0,
  160. transition: {duration: 0.2},
  161. },
  162. },
  163. };
  164. const TaskList = styled('div')`
  165. display: grid;
  166. grid-auto-flow: row;
  167. grid-gap: ${space(1)};
  168. margin: ${space(1)} ${space(4)} ${space(4)} ${space(4)};
  169. `;
  170. const CompleteList = styled('div')`
  171. display: grid;
  172. grid-auto-flow: row;
  173. > div {
  174. transition: border-radius 500ms;
  175. }
  176. > div:not(:first-of-type) {
  177. margin-top: -1px;
  178. border-top-left-radius: 0;
  179. border-top-right-radius: 0;
  180. }
  181. > div:not(:last-of-type) {
  182. border-bottom-left-radius: 0;
  183. border-bottom-right-radius: 0;
  184. }
  185. `;
  186. const TopRight = styled('img')`
  187. position: absolute;
  188. top: 0;
  189. right: 0;
  190. width: 60%;
  191. `;
  192. export default withApi(withOrganization(OnboardingWizardSidebar));