@automattic/launchpad 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +35 -0
  3. package/dist/cjs/badge/index.js +13 -0
  4. package/dist/cjs/badge/index.js.map +1 -0
  5. package/dist/cjs/badge/style.scss +50 -0
  6. package/dist/cjs/checklist/index.js +18 -0
  7. package/dist/cjs/checklist/index.js.map +1 -0
  8. package/dist/cjs/checklist/style.scss +9 -0
  9. package/dist/cjs/checklist-item/index.js +35 -0
  10. package/dist/cjs/checklist-item/index.js.map +1 -0
  11. package/dist/cjs/checklist-item/style.scss +214 -0
  12. package/dist/cjs/index.js +14 -0
  13. package/dist/cjs/index.js.map +1 -0
  14. package/dist/cjs/launchpad.js +19 -0
  15. package/dist/cjs/launchpad.js.map +1 -0
  16. package/dist/cjs/setup-actions.js +156 -0
  17. package/dist/cjs/setup-actions.js.map +1 -0
  18. package/dist/cjs/types.js +3 -0
  19. package/dist/cjs/types.js.map +1 -0
  20. package/dist/esm/badge/index.js +10 -0
  21. package/dist/esm/badge/index.js.map +1 -0
  22. package/dist/esm/badge/style.scss +50 -0
  23. package/dist/esm/checklist/index.js +15 -0
  24. package/dist/esm/checklist/index.js.map +1 -0
  25. package/dist/esm/checklist/style.scss +9 -0
  26. package/dist/esm/checklist-item/index.js +32 -0
  27. package/dist/esm/checklist-item/index.js.map +1 -0
  28. package/dist/esm/checklist-item/style.scss +214 -0
  29. package/dist/esm/index.js +6 -0
  30. package/dist/esm/index.js.map +1 -0
  31. package/dist/esm/launchpad.js +16 -0
  32. package/dist/esm/launchpad.js.map +1 -0
  33. package/dist/esm/setup-actions.js +151 -0
  34. package/dist/esm/setup-actions.js.map +1 -0
  35. package/dist/esm/types.js +2 -0
  36. package/dist/esm/types.js.map +1 -0
  37. package/dist/tsconfig-cjs.tsbuildinfo +1 -0
  38. package/dist/tsconfig.tsbuildinfo +1 -0
  39. package/dist/types/badge/index.d.ts +10 -0
  40. package/dist/types/badge/index.d.ts.map +1 -0
  41. package/dist/types/checklist/index.d.ts +12 -0
  42. package/dist/types/checklist/index.d.ts.map +1 -0
  43. package/dist/types/checklist-item/index.d.ts +11 -0
  44. package/dist/types/checklist-item/index.d.ts.map +1 -0
  45. package/dist/types/index.d.ts +6 -0
  46. package/dist/types/index.d.ts.map +1 -0
  47. package/dist/types/launchpad.d.ts +12 -0
  48. package/dist/types/launchpad.d.ts.map +1 -0
  49. package/dist/types/setup-actions.d.ts +3 -0
  50. package/dist/types/setup-actions.d.ts.map +1 -0
  51. package/dist/types/types.d.ts +64 -0
  52. package/dist/types/types.d.ts.map +1 -0
  53. package/jest.config.js +5 -0
  54. package/package.json +59 -0
  55. package/src/badge/index.tsx +27 -0
  56. package/src/badge/style.scss +50 -0
  57. package/src/checklist/index.tsx +43 -0
  58. package/src/checklist/style.scss +9 -0
  59. package/src/checklist-item/index.tsx +94 -0
  60. package/src/checklist-item/style.scss +214 -0
  61. package/src/index.ts +5 -0
  62. package/src/launchpad.tsx +45 -0
  63. package/src/setup-actions.ts +168 -0
  64. package/src/test/badge.tsx +50 -0
  65. package/src/test/checklist-item.tsx +96 -0
  66. package/src/test/checklist.tsx +25 -0
  67. package/src/test/launchpad.tsx +49 -0
  68. package/src/test/lib/fixtures.ts +322 -0
  69. package/src/types.ts +70 -0
  70. package/tsconfig-cjs.json +7 -0
  71. package/tsconfig.json +11 -0
@@ -0,0 +1,45 @@
1
+ import { useLaunchpad } from '@automattic/data-stores';
2
+ import { useRef, useMemo } from 'react';
3
+ import Checklist from './checklist';
4
+ import type { Task } from './types';
5
+ import type { UseLaunchpadOptions } from '@automattic/data-stores';
6
+
7
+ export interface LaunchpadProps {
8
+ siteSlug: string | null;
9
+ checklistSlug?: string | 0 | null | undefined;
10
+ makeLastTaskPrimaryAction?: boolean;
11
+ taskFilter?: ( tasks: Task[] ) => Task[];
12
+ useLaunchpadOptions?: UseLaunchpadOptions;
13
+ }
14
+
15
+ const Launchpad = ( {
16
+ siteSlug,
17
+ checklistSlug,
18
+ taskFilter,
19
+ makeLastTaskPrimaryAction,
20
+ useLaunchpadOptions = {},
21
+ }: LaunchpadProps ) => {
22
+ const launchpadData = useLaunchpad( siteSlug || '', checklistSlug, useLaunchpadOptions );
23
+ const { isFetchedAfterMount, data } = launchpadData;
24
+ const tasks = useRef< Task[] >( [] );
25
+
26
+ useMemo( () => {
27
+ const originalTasks = data.checklist || [];
28
+ tasks.current = taskFilter ? taskFilter( originalTasks ) : originalTasks;
29
+ }, [ data, taskFilter ] );
30
+
31
+ return (
32
+ <div className="launchpad__checklist-wrapper">
33
+ { isFetchedAfterMount ? (
34
+ <Checklist
35
+ tasks={ tasks.current }
36
+ makeLastTaskPrimaryAction={ makeLastTaskPrimaryAction }
37
+ />
38
+ ) : (
39
+ <Checklist.Placeholder />
40
+ ) }
41
+ </div>
42
+ );
43
+ };
44
+
45
+ export default Launchpad;
@@ -0,0 +1,168 @@
1
+ import config from '@automattic/calypso-config';
2
+ import { updateLaunchpadSettings } from '@automattic/data-stores';
3
+ import { isMobile } from '@automattic/viewport';
4
+ import { addQueryArgs } from '@wordpress/url';
5
+ import wpcomRequest from 'wpcom-proxy-request';
6
+ import type { LaunchpadTaskActionsProps, Task } from './types';
7
+
8
+ const TASKS_TO_COMPLETE_ON_CLICK = [
9
+ 'add_about_page',
10
+ 'manage_paid_newsletter_plan',
11
+ 'earn_money',
12
+ 'manage_subscribers',
13
+ 'connect_social_media',
14
+ 'drive_traffic',
15
+ ];
16
+
17
+ export const setUpActionsForTasks = ( {
18
+ siteSlug,
19
+ tasks,
20
+ tracksData,
21
+ extraActions,
22
+ uiContext = 'calypso',
23
+ }: LaunchpadTaskActionsProps ): Task[] => {
24
+ const { recordTracksEvent, checklistSlug, tasklistCompleted, launchpadContext } = tracksData;
25
+ const { setShareSiteModalIsOpen, siteLaunched, setActiveChecklist } = extraActions;
26
+
27
+ //Record click events for tasks
28
+ const recordTaskClickTracksEvent = ( task: Task ) => {
29
+ recordTracksEvent( 'calypso_launchpad_task_clicked', {
30
+ checklist_slug: checklistSlug,
31
+ checklist_completed: tasklistCompleted,
32
+ task_id: task.id,
33
+ is_completed: task.completed,
34
+ context: launchpadContext,
35
+ order: task.order,
36
+ } );
37
+ };
38
+
39
+ // Add actions to the tasks.
40
+ return tasks.map( ( task: Task ) => {
41
+ let action: () => void;
42
+ let logMissingCalypsoPath = false;
43
+
44
+ if ( uiContext === 'calypso' && task.calypso_path !== undefined ) {
45
+ let targetPath = task.calypso_path;
46
+
47
+ if ( task.id === 'drive_traffic' && ! isMobile() ) {
48
+ targetPath = addQueryArgs( targetPath, { tour: 'marketingConnectionsTour' } );
49
+ }
50
+
51
+ // Enable task in 'calypso' context
52
+ if ( task.id === 'setup_general' ) {
53
+ task.disabled = false;
54
+ }
55
+
56
+ action = () => {
57
+ if ( siteSlug && TASKS_TO_COMPLETE_ON_CLICK.includes( task.id ) ) {
58
+ updateLaunchpadSettings( siteSlug, {
59
+ checklist_statuses: { [ task.id ]: true },
60
+ } );
61
+ }
62
+ window.location.assign( targetPath );
63
+ };
64
+ } else {
65
+ switch ( task.id ) {
66
+ case 'share_site':
67
+ action = () => {
68
+ setShareSiteModalIsOpen?.( true );
69
+ };
70
+ break;
71
+
72
+ case 'site_title':
73
+ logMissingCalypsoPath = true;
74
+ action = () => {
75
+ window.location.assign( `/settings/general/${ siteSlug }` );
76
+ };
77
+ break;
78
+
79
+ case 'domain_claim':
80
+ case 'domain_upsell':
81
+ case 'domain_customize':
82
+ logMissingCalypsoPath = true;
83
+ action = () => {
84
+ window.location.assign( `/domains/add/${ siteSlug }` );
85
+ };
86
+ break;
87
+ case 'drive_traffic':
88
+ logMissingCalypsoPath = true;
89
+ action = () => {
90
+ const url = isMobile()
91
+ ? `/marketing/connections/${ siteSlug }`
92
+ : `/marketing/connections/${ siteSlug }?tour=marketingConnectionsTour`;
93
+ window.location.assign( url );
94
+ };
95
+ break;
96
+ case 'add_new_page':
97
+ logMissingCalypsoPath = true;
98
+ action = () => {
99
+ window.location.assign( `/page/${ siteSlug }` );
100
+ };
101
+ break;
102
+ case 'edit_page':
103
+ logMissingCalypsoPath = true;
104
+ action = () => {
105
+ window.location.assign( `/pages/${ siteSlug }` );
106
+ };
107
+ break;
108
+ case 'update_about_page':
109
+ logMissingCalypsoPath = true;
110
+ action = () => {
111
+ window.location.assign( `/page/${ siteSlug }/${ task?.extra_data?.about_page_id }` );
112
+ };
113
+ break;
114
+ case 'customize_welcome_message':
115
+ logMissingCalypsoPath = true;
116
+ action = () => {
117
+ if ( config.isEnabled( 'settings/newsletter-settings-page' ) ) {
118
+ window.location.assign( `/settings/newsletter/${ siteSlug }` );
119
+ } else {
120
+ window.location.assign( `/settings/reading/${ siteSlug }#newsletter-settings` );
121
+ }
122
+ };
123
+ break;
124
+ case 'manage_subscribers':
125
+ logMissingCalypsoPath = true;
126
+ action = () => {
127
+ window.location.assign( `/subscribers/${ siteSlug }` );
128
+ };
129
+ break;
130
+ case 'site_launched':
131
+ case 'blog_launched':
132
+ case 'videopress_launched':
133
+ case 'link_in_bio_launched':
134
+ action = async () => {
135
+ await wpcomRequest( {
136
+ path: `/sites/${ siteSlug }/launch`,
137
+ apiVersion: '1.1',
138
+ method: 'post',
139
+ } );
140
+ siteLaunched?.();
141
+ };
142
+ break;
143
+ default:
144
+ logMissingCalypsoPath = true;
145
+ break;
146
+ }
147
+ }
148
+
149
+ if ( logMissingCalypsoPath ) {
150
+ recordTracksEvent( 'calypso_launchpad_task_missing_calypso_path', {
151
+ checklist_slug: checklistSlug,
152
+ task_id: task.id,
153
+ is_completed: task.completed,
154
+ context: launchpadContext,
155
+ } );
156
+ }
157
+
158
+ const actionDispatch = () => {
159
+ recordTaskClickTracksEvent( task );
160
+ if ( siteSlug && setActiveChecklist ) {
161
+ setActiveChecklist( siteSlug, checklistSlug );
162
+ }
163
+ action?.();
164
+ };
165
+
166
+ return { ...task, actionDispatch };
167
+ } );
168
+ };
@@ -0,0 +1,50 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import React from 'react';
3
+ import Badge from '../badge/index';
4
+ import '@testing-library/jest-dom';
5
+
6
+ describe( 'Badge', () => {
7
+ test( 'should have badge class', () => {
8
+ const { container } = render( <Badge /> );
9
+ expect( container.getElementsByClassName( 'badge' ).length ).toBe( 1 );
10
+ } );
11
+
12
+ test( 'should have proper type class (warning)', () => {
13
+ const { container } = render( <Badge type="warning" /> );
14
+ expect( container.getElementsByClassName( 'badge badge--warning' ).length ).toBe( 1 );
15
+ } );
16
+
17
+ test( 'should have proper type class (success)', () => {
18
+ const { container } = render( <Badge type="success" /> );
19
+ expect( container.getElementsByClassName( 'badge badge--success' ).length ).toBe( 1 );
20
+ } );
21
+
22
+ test( 'should have proper type class (info)', () => {
23
+ const { container } = render( <Badge type="info" /> );
24
+ expect( container.getElementsByClassName( 'badge badge--info' ).length ).toBe( 1 );
25
+ } );
26
+
27
+ test( 'should have proper type class (info-blue)', () => {
28
+ const { container } = render( <Badge type="info-blue" /> );
29
+ expect( container.getElementsByClassName( 'badge badge--info-blue' ).length ).toBe( 1 );
30
+ } );
31
+
32
+ test( 'should have proper type class (error)', () => {
33
+ const { container } = render( <Badge type="error" /> );
34
+ expect( container.getElementsByClassName( 'badge badge--error' ).length ).toBe( 1 );
35
+ } );
36
+
37
+ test( 'should have proper type class (default)', () => {
38
+ const { container } = render( <Badge /> );
39
+ expect( container.getElementsByClassName( 'badge badge--warning' ).length ).toBe( 1 );
40
+ } );
41
+
42
+ test( 'should contains the passed children wrapped by a feature-example div', () => {
43
+ render(
44
+ <Badge>
45
+ <div>arbitrary-text-content</div>
46
+ </Badge>
47
+ );
48
+ expect( screen.getByText( 'arbitrary-text-content' ) ).toBeInTheDocument();
49
+ } );
50
+ } );
@@ -0,0 +1,96 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import React from 'react';
3
+ import ChecklistItem from '../checklist-item';
4
+ import { buildTask } from './lib/fixtures';
5
+ import '@testing-library/jest-dom';
6
+
7
+ describe( 'ChecklistItem', () => {
8
+ describe( 'when the task requires a badge', () => {
9
+ it( 'displays a badge', () => {
10
+ const badge_text = 'Badge Text';
11
+ render( <ChecklistItem task={ buildTask( { badge_text } ) } /> );
12
+ expect( screen.getByText( badge_text ) ).toBeTruthy();
13
+ } );
14
+ } );
15
+
16
+ describe( 'when the task is completed', () => {
17
+ it( 'shows the task completed icon', () => {
18
+ render( <ChecklistItem task={ buildTask( { completed: true } ) } /> );
19
+ const taskCompleteIcon = screen.queryByLabelText( 'Task complete' );
20
+ expect( taskCompleteIcon ).toBeTruthy();
21
+ } );
22
+ it( 'hides the task enabled icon', () => {
23
+ render( <ChecklistItem task={ buildTask( { completed: true } ) } /> );
24
+ const taskEnabledIcon = screen.queryByLabelText( 'Task enabled' );
25
+ expect( taskEnabledIcon ).toBeFalsy();
26
+ } );
27
+ it( 'disables the task', () => {
28
+ render( <ChecklistItem task={ buildTask( { completed: true } ) } /> );
29
+ const taskButton = screen.queryByRole( 'button' );
30
+ expect( taskButton ).toHaveAttribute( 'disabled' );
31
+ } );
32
+
33
+ describe( 'and the task is kept enabled', () => {
34
+ it( 'hides the task enabled icon', () => {
35
+ render( <ChecklistItem task={ buildTask( { completed: true, disabled: false } ) } /> );
36
+ const taskEnabledIcon = screen.queryByLabelText( 'Task enabled' );
37
+ expect( taskEnabledIcon ).toBeFalsy();
38
+ } );
39
+ it( 'enables the task', () => {
40
+ render( <ChecklistItem task={ buildTask( { completed: true, disabled: false } ) } /> );
41
+ const taskButton = screen.queryByRole( 'button' );
42
+ expect( taskButton ).not.toHaveAttribute( 'disabled' );
43
+ } );
44
+ } );
45
+ } );
46
+
47
+ describe( 'when a task is incomplete', () => {
48
+ it( 'hides the task complete icon', () => {
49
+ render( <ChecklistItem task={ buildTask( { completed: false } ) } /> );
50
+ const taskCompleteIcon = screen.queryByLabelText( 'Task complete' );
51
+ expect( taskCompleteIcon ).toBeFalsy();
52
+ } );
53
+ } );
54
+
55
+ describe( 'and the task depends on the completion of other tasks', () => {
56
+ it( 'hides the task enabled icon', () => {
57
+ const otherTaskCompleted = false;
58
+ render(
59
+ <ChecklistItem task={ buildTask( { completed: false, disabled: ! otherTaskCompleted } ) } />
60
+ );
61
+ const taskEnabledIcon = screen.queryByLabelText( 'Task enabled' );
62
+ expect( taskEnabledIcon ).toBeFalsy();
63
+ } );
64
+ it( 'disables the task', () => {
65
+ const otherTaskCompleted = false;
66
+ render(
67
+ <ChecklistItem task={ buildTask( { completed: false, disabled: ! otherTaskCompleted } ) } />
68
+ );
69
+ const taskButton = screen.queryByRole( 'button' );
70
+ expect( taskButton ).toHaveAttribute( 'disabled' );
71
+ } );
72
+ } );
73
+
74
+ describe( 'and the task does not depend on the completion of other tasks', () => {
75
+ it( 'shows the task enabled icon', () => {
76
+ render( <ChecklistItem task={ buildTask( { completed: false, disabled: false } ) } /> );
77
+ const taskEnabledIcon = screen.queryByLabelText( 'Task enabled' );
78
+ expect( taskEnabledIcon ).toBeTruthy();
79
+ } );
80
+ it( 'enables the task', () => {
81
+ render( <ChecklistItem task={ buildTask( { completed: false, disabled: false } ) } /> );
82
+ const taskButton = screen.queryByRole( 'button' );
83
+ expect( taskButton ).not.toHaveAttribute( 'disabled' );
84
+ } );
85
+ } );
86
+
87
+ describe( 'when a task is a primary action', () => {
88
+ it( 'displays a primary button', () => {
89
+ render(
90
+ <ChecklistItem task={ buildTask( { completed: false } ) } isPrimaryAction={ true } />
91
+ );
92
+ const taskButton = screen.queryByRole( 'button' );
93
+ expect( taskButton?.className ).toContain( 'checklist-item__checklist-primary-button' );
94
+ } );
95
+ } );
96
+ } );
@@ -0,0 +1,25 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import React from 'react';
3
+ import Checklist from '../checklist';
4
+ import { buildTask } from './lib/fixtures';
5
+ import '@testing-library/jest-dom';
6
+
7
+ describe( 'Checklist', () => {
8
+ describe( 'when provided no tasks', () => {
9
+ it( 'then no tasks are rendered', () => {
10
+ render( <Checklist tasks={ [] } /> );
11
+ const checklistItems = screen.queryByRole( 'listitem' );
12
+ expect( checklistItems ).not.toBeInTheDocument();
13
+ } );
14
+ } );
15
+
16
+ describe( 'when a number of tasks are provided', () => {
17
+ it( 'then the same number of tasks are rendered', () => {
18
+ render(
19
+ <Checklist tasks={ [ buildTask( { id: 'task1' } ), buildTask( { id: 'task2' } ) ] } />
20
+ );
21
+ const checklistItems = screen.getAllByRole( 'listitem' );
22
+ expect( checklistItems.length ).toBe( 2 );
23
+ } );
24
+ } );
25
+ } );
@@ -0,0 +1,49 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import React from 'react';
3
+ import Launchpad from '../launchpad';
4
+ import '@testing-library/jest-dom';
5
+ import type { Task } from '../types';
6
+
7
+ jest.mock( '@automattic/data-stores', () => {
8
+ const { buildTask } = require( './lib/fixtures' );
9
+ return {
10
+ useLaunchpad: jest.fn( () => {
11
+ return {
12
+ isFetchedAfterMount: true,
13
+ data: {
14
+ checklist: [
15
+ buildTask( { id: 'task1' } ),
16
+ buildTask( { id: 'task2' } ),
17
+ buildTask( { id: 'task3' } ),
18
+ ],
19
+ },
20
+ };
21
+ } ),
22
+ };
23
+ } );
24
+
25
+ describe( 'Launchpad', () => {
26
+ describe( 'when no taskFilter is provided', () => {
27
+ it( 'then all tasks from useLaunchpad are rendered', () => {
28
+ render( <Launchpad siteSlug="any site" /> );
29
+ const checklistItems = screen.queryAllByRole( 'listitem' );
30
+ expect( checklistItems.length ).toBe( 3 );
31
+ } );
32
+ } );
33
+
34
+ describe( 'when taskFilter is provided', () => {
35
+ it( 'then render only tasks returned from the callback', () => {
36
+ const filter = ( tasks: Task[] ) => {
37
+ // return only the first task
38
+ return [ tasks[ 0 ] ];
39
+ };
40
+
41
+ render( <Launchpad siteSlug="any site" taskFilter={ filter } /> );
42
+ const checklistItems = screen.queryAllByRole( 'listitem' );
43
+ expect( checklistItems.length ).toBe( 1 );
44
+
45
+ const taskId = checklistItems[ 0 ].querySelector( 'button' )?.getAttribute( 'data-task' );
46
+ expect( taskId ).toBe( 'task1' );
47
+ } );
48
+ } );
49
+ } );