diff --git a/packages/js/data/changelog/dev-migrate-onboarding-store-to-ts b/packages/js/data/changelog/dev-migrate-onboarding-store-to-ts new file mode 100644 index 00000000000..7e955f6b2b1 --- /dev/null +++ b/packages/js/data/changelog/dev-migrate-onboarding-store-to-ts @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Migrate onboarding data store to TS diff --git a/packages/js/data/src/onboarding/action-types.js b/packages/js/data/src/onboarding/action-types.ts similarity index 99% rename from packages/js/data/src/onboarding/action-types.js rename to packages/js/data/src/onboarding/action-types.ts index 4fb27c9950e..690016fbbb5 100644 --- a/packages/js/data/src/onboarding/action-types.js +++ b/packages/js/data/src/onboarding/action-types.ts @@ -36,6 +36,6 @@ const TYPES = { VISITED_TASK: 'VISITED_TASK', KEEP_COMPLETED_TASKS_REQUEST: 'KEEP_COMPLETED_TASKS_REQUEST', KEEP_COMPLETED_TASKS_SUCCESS: 'KEEP_COMPLETED_TASKS_SUCCESS', -}; +} as const; export default TYPES; diff --git a/packages/js/data/src/onboarding/actions.js b/packages/js/data/src/onboarding/actions.ts similarity index 57% rename from packages/js/data/src/onboarding/actions.js rename to packages/js/data/src/onboarding/actions.ts index bda9be7ffe1..8628f22031c 100644 --- a/packages/js/data/src/onboarding/actions.js +++ b/packages/js/data/src/onboarding/actions.ts @@ -11,22 +11,30 @@ import TYPES from './action-types'; import { WC_ADMIN_NAMESPACE } from '../constants'; import { DeprecatedTasks } from './deprecated-tasks'; import { STORE_NAME as OPTIONS_STORE_NAME } from '../options/constants'; +import { + ExtensionList, + ProfileItems, + TaskListType, + TaskType, + OnboardingProductType, +} from './types'; +import { Plugin } from '../plugins/types'; -export function getFreeExtensionsError( error ) { +export function getFreeExtensionsError( error: unknown ) { return { type: TYPES.GET_FREE_EXTENSIONS_ERROR, error, }; } -export function getFreeExtensionsSuccess( freeExtensions ) { +export function getFreeExtensionsSuccess( freeExtensions: ExtensionList[] ) { return { type: TYPES.GET_FREE_EXTENSIONS_SUCCESS, freeExtensions, }; } -export function setError( selector, error ) { +export function setError( selector: string, error: unknown ) { return { type: TYPES.SET_ERROR, selector, @@ -34,7 +42,7 @@ export function setError( selector, error ) { }; } -export function setIsRequesting( selector, isRequesting ) { +export function setIsRequesting( selector: string, isRequesting: boolean ) { return { type: TYPES.SET_IS_REQUESTING, selector, @@ -42,7 +50,7 @@ export function setIsRequesting( selector, isRequesting ) { }; } -export function setProfileItems( profileItems, replace = false ) { +export function setProfileItems( profileItems: ProfileItems, replace = false ) { return { type: TYPES.SET_PROFILE_ITEMS, profileItems, @@ -50,21 +58,21 @@ export function setProfileItems( profileItems, replace = false ) { }; } -export function getTaskListsError( error ) { +export function getTaskListsError( error: unknown ) { return { type: TYPES.GET_TASK_LISTS_ERROR, error, }; } -export function getTaskListsSuccess( taskLists ) { +export function getTaskListsSuccess( taskLists: TaskListType[] ) { return { type: TYPES.GET_TASK_LISTS_SUCCESS, taskLists, }; } -export function snoozeTaskError( taskId, error ) { +export function snoozeTaskError( taskId: string, error: unknown ) { return { type: TYPES.SNOOZE_TASK_ERROR, taskId, @@ -72,21 +80,21 @@ export function snoozeTaskError( taskId, error ) { }; } -export function snoozeTaskRequest( taskId ) { +export function snoozeTaskRequest( taskId: string ) { return { type: TYPES.SNOOZE_TASK_REQUEST, taskId, }; } -export function snoozeTaskSuccess( task ) { +export function snoozeTaskSuccess( task: Partial< TaskType > ) { return { type: TYPES.SNOOZE_TASK_SUCCESS, task, }; } -export function undoSnoozeTaskError( taskId, error ) { +export function undoSnoozeTaskError( taskId: string, error: unknown ) { return { type: TYPES.UNDO_SNOOZE_TASK_ERROR, taskId, @@ -94,21 +102,21 @@ export function undoSnoozeTaskError( taskId, error ) { }; } -export function undoSnoozeTaskRequest( taskId ) { +export function undoSnoozeTaskRequest( taskId: string ) { return { type: TYPES.UNDO_SNOOZE_TASK_REQUEST, taskId, }; } -export function undoSnoozeTaskSuccess( task ) { +export function undoSnoozeTaskSuccess( task: Partial< TaskType > ) { return { type: TYPES.UNDO_SNOOZE_TASK_SUCCESS, task, }; } -export function dismissTaskError( taskId, error ) { +export function dismissTaskError( taskId: string, error: unknown ) { return { type: TYPES.DISMISS_TASK_ERROR, taskId, @@ -116,21 +124,21 @@ export function dismissTaskError( taskId, error ) { }; } -export function dismissTaskRequest( taskId ) { +export function dismissTaskRequest( taskId: string ) { return { type: TYPES.DISMISS_TASK_REQUEST, taskId, }; } -export function dismissTaskSuccess( task ) { +export function dismissTaskSuccess( task: Partial< TaskType > ) { return { type: TYPES.DISMISS_TASK_SUCCESS, task, }; } -export function undoDismissTaskError( taskId, error ) { +export function undoDismissTaskError( taskId: string, error: unknown ) { return { type: TYPES.UNDO_DISMISS_TASK_ERROR, taskId, @@ -138,21 +146,21 @@ export function undoDismissTaskError( taskId, error ) { }; } -export function undoDismissTaskRequest( taskId ) { +export function undoDismissTaskRequest( taskId: string ) { return { type: TYPES.UNDO_DISMISS_TASK_REQUEST, taskId, }; } -export function undoDismissTaskSuccess( task ) { +export function undoDismissTaskSuccess( task: Partial< TaskType > ) { return { type: TYPES.UNDO_DISMISS_TASK_SUCCESS, task, }; } -export function hideTaskListError( taskListId, error ) { +export function hideTaskListError( taskListId: string, error: unknown ) { return { type: TYPES.HIDE_TASK_LIST_ERROR, taskListId, @@ -160,14 +168,14 @@ export function hideTaskListError( taskListId, error ) { }; } -export function hideTaskListRequest( taskListId ) { +export function hideTaskListRequest( taskListId: string ) { return { type: TYPES.HIDE_TASK_LIST_REQUEST, taskListId, }; } -export function hideTaskListSuccess( taskList ) { +export function hideTaskListSuccess( taskList: TaskListType ) { return { type: TYPES.HIDE_TASK_LIST_SUCCESS, taskList, @@ -175,7 +183,7 @@ export function hideTaskListSuccess( taskList ) { }; } -export function unhideTaskListError( taskListId, error ) { +export function unhideTaskListError( taskListId: string, error: unknown ) { return { type: TYPES.UNHIDE_TASK_LIST_ERROR, taskListId, @@ -183,14 +191,14 @@ export function unhideTaskListError( taskListId, error ) { }; } -export function unhideTaskListRequest( taskListId ) { +export function unhideTaskListRequest( taskListId: string ) { return { type: TYPES.UNHIDE_TASK_LIST_REQUEST, taskListId, }; } -export function unhideTaskListSuccess( taskList ) { +export function unhideTaskListSuccess( taskList: TaskListType ) { return { type: TYPES.UNHIDE_TASK_LIST_SUCCESS, taskList, @@ -198,14 +206,17 @@ export function unhideTaskListSuccess( taskList ) { }; } -export function optimisticallyCompleteTaskRequest( taskId ) { +export function optimisticallyCompleteTaskRequest( taskId: string ) { return { type: TYPES.OPTIMISTICALLY_COMPLETE_TASK_REQUEST, taskId, }; } -export function keepCompletedTaskListSuccess( taskListId, keepCompletedList ) { +export function keepCompletedTaskListSuccess( + taskListId: string, + keepCompletedList: 'yes' | 'no' +) { return { type: TYPES.KEEP_COMPLETED_TASKS_SUCCESS, taskListId, @@ -213,28 +224,28 @@ export function keepCompletedTaskListSuccess( taskListId, keepCompletedList ) { }; } -export function visitedTask( taskId ) { +export function visitedTask( taskId: string ) { return { type: TYPES.VISITED_TASK, taskId, }; } -export function setPaymentMethods( paymentMethods ) { +export function setPaymentMethods( paymentMethods: Plugin[] ) { return { type: TYPES.GET_PAYMENT_METHODS_SUCCESS, paymentMethods, }; } -export function setEmailPrefill( email ) { +export function setEmailPrefill( email: string ) { return { type: TYPES.SET_EMAIL_PREFILL, emailPrefill: email, }; } -export function actionTaskError( taskId, error ) { +export function actionTaskError( taskId: string, error: unknown ) { return { type: TYPES.ACTION_TASK_ERROR, taskId, @@ -242,39 +253,43 @@ export function actionTaskError( taskId, error ) { }; } -export function actionTaskRequest( taskId ) { +export function actionTaskRequest( taskId: string ) { return { type: TYPES.ACTION_TASK_REQUEST, taskId, }; } -export function actionTaskSuccess( task ) { +export function actionTaskSuccess( task: Partial< TaskType > ) { return { type: TYPES.ACTION_TASK_SUCCESS, task, }; } -export function getProductTypesSuccess( productTypes ) { +export function getProductTypesSuccess( + productTypes: OnboardingProductType[] +) { return { type: TYPES.GET_PRODUCT_TYPES_SUCCESS, productTypes, }; } -export function getProductTypesError( error ) { +export function getProductTypesError( error: unknown ) { return { type: TYPES.GET_PRODUCT_TYPES_ERROR, error, }; } -export function* keepCompletedTaskList( taskListId ) { +export function* keepCompletedTaskList( taskListId: string ) { const updateOptionsParams = { woocommerce_task_list_keep_completed: 'yes', }; - const response = yield controls.dispatch( + const response: { + success: 'yes' | 'no'; + } = yield controls.dispatch( OPTIONS_STORE_NAME, 'updateOptions', updateOptionsParams @@ -284,12 +299,15 @@ export function* keepCompletedTaskList( taskListId ) { } } -export function* updateProfileItems( items ) { +export function* updateProfileItems( items: ProfileItems ) { yield setIsRequesting( 'updateProfileItems', true ); yield setError( 'updateProfileItems', null ); try { - const results = yield apiFetch( { + const results: { + items: ProfileItems; + status: string; + } = yield apiFetch( { path: `${ WC_ADMIN_NAMESPACE }/onboarding/profile`, method: 'POST', data: items, @@ -309,11 +327,11 @@ export function* updateProfileItems( items ) { } } -export function* snoozeTask( id ) { +export function* snoozeTask( id: string ) { yield snoozeTaskRequest( id ); try { - const task = yield apiFetch( { + const task: TaskType = yield apiFetch( { path: `${ WC_ADMIN_NAMESPACE }/onboarding/tasks/${ id }/snooze`, method: 'POST', } ); @@ -331,11 +349,11 @@ export function* snoozeTask( id ) { } } -export function* undoSnoozeTask( id ) { +export function* undoSnoozeTask( id: string ) { yield undoSnoozeTaskRequest( id ); try { - const task = yield apiFetch( { + const task: TaskType = yield apiFetch( { path: `${ WC_ADMIN_NAMESPACE }/onboarding/tasks/${ id }/undo_snooze`, method: 'POST', } ); @@ -353,11 +371,11 @@ export function* undoSnoozeTask( id ) { } } -export function* dismissTask( id ) { +export function* dismissTask( id: string ) { yield dismissTaskRequest( id ); try { - const task = yield apiFetch( { + const task: TaskType = yield apiFetch( { path: `${ WC_ADMIN_NAMESPACE }/onboarding/tasks/${ id }/dismiss`, method: 'POST', } ); @@ -374,11 +392,11 @@ export function* dismissTask( id ) { } } -export function* undoDismissTask( id ) { +export function* undoDismissTask( id: string ) { yield undoDismissTaskRequest( id ); try { - const task = yield apiFetch( { + const task: TaskType = yield apiFetch( { path: `${ WC_ADMIN_NAMESPACE }/onboarding/tasks/${ id }/undo_dismiss`, method: 'POST', } ); @@ -395,11 +413,11 @@ export function* undoDismissTask( id ) { } } -export function* hideTaskList( id ) { +export function* hideTaskList( id: string ) { yield hideTaskListRequest( id ); try { - const taskList = yield apiFetch( { + const taskList: TaskListType = yield apiFetch( { path: `${ WC_ADMIN_NAMESPACE }/onboarding/tasks/${ id }/hide`, method: 'POST', } ); @@ -411,11 +429,11 @@ export function* hideTaskList( id ) { } } -export function* unhideTaskList( id ) { +export function* unhideTaskList( id: string ) { yield unhideTaskListRequest( id ); try { - const taskList = yield apiFetch( { + const taskList: TaskListType = yield apiFetch( { path: `${ WC_ADMIN_NAMESPACE }/onboarding/tasks/${ id }/unhide`, method: 'POST', } ); @@ -427,15 +445,15 @@ export function* unhideTaskList( id ) { } } -export function* optimisticallyCompleteTask( id ) { +export function* optimisticallyCompleteTask( id: string ) { yield optimisticallyCompleteTaskRequest( id ); } -export function* actionTask( id ) { +export function* actionTask( id: string ) { yield actionTaskRequest( id ); try { - const task = yield apiFetch( { + const task: TaskType = yield apiFetch( { path: `${ WC_ADMIN_NAMESPACE }/onboarding/tasks/${ id }/action`, method: 'POST', } ); @@ -448,3 +466,41 @@ export function* actionTask( id ) { throw new Error(); } } + +export type Action = ReturnType< + | typeof getFreeExtensionsError + | typeof getFreeExtensionsSuccess + | typeof setError + | typeof setIsRequesting + | typeof setProfileItems + | typeof snoozeTaskRequest + | typeof snoozeTaskSuccess + | typeof snoozeTaskError + | typeof getTaskListsError + | typeof getTaskListsSuccess + | typeof undoSnoozeTaskError + | typeof undoSnoozeTaskSuccess + | typeof dismissTaskError + | typeof dismissTaskSuccess + | typeof dismissTaskRequest + | typeof undoDismissTaskError + | typeof undoDismissTaskSuccess + | typeof undoDismissTaskRequest + | typeof undoSnoozeTaskRequest + | typeof hideTaskListError + | typeof hideTaskListSuccess + | typeof hideTaskListRequest + | typeof unhideTaskListError + | typeof unhideTaskListSuccess + | typeof unhideTaskListRequest + | typeof optimisticallyCompleteTaskRequest + | typeof keepCompletedTaskListSuccess + | typeof visitedTask + | typeof setPaymentMethods + | typeof setEmailPrefill + | typeof actionTaskError + | typeof actionTaskSuccess + | typeof actionTaskRequest + | typeof getProductTypesError + | typeof getProductTypesSuccess +>; diff --git a/packages/js/data/src/onboarding/deprecated-tasks.js b/packages/js/data/src/onboarding/deprecated-tasks.ts similarity index 87% rename from packages/js/data/src/onboarding/deprecated-tasks.js rename to packages/js/data/src/onboarding/deprecated-tasks.ts index 50958b5d736..e00783d41dd 100644 --- a/packages/js/data/src/onboarding/deprecated-tasks.js +++ b/packages/js/data/src/onboarding/deprecated-tasks.ts @@ -5,6 +5,11 @@ import { applyFilters } from '@wordpress/hooks'; import { parse } from 'qs'; import deprecated from '@wordpress/deprecated'; +/** + * Internal dependencies + */ +import { TaskListType, TaskType, DeprecatedTaskType } from './types'; + function getQuery() { const searchString = window.location && window.location.search; if ( ! searchString ) { @@ -19,6 +24,11 @@ function getQuery() { * A simple class to handle deprecated tasks using the woocommerce_admin_onboarding_task_list filter. */ export class DeprecatedTasks { + filteredTasks: DeprecatedTaskType[]; + tasks: { + [ key: string ]: DeprecatedTaskType[]; + }; + constructor() { /** * **Deprecated** Filter Onboarding tasks. @@ -32,7 +42,7 @@ export class DeprecatedTasks { 'woocommerce_admin_onboarding_task_list', [], getQuery() - ); + ) as DeprecatedTaskType[]; if ( this.filteredTasks && this.filteredTasks.length > 0 ) { deprecated( 'woocommerce_admin_onboarding_task_list', { version: '2.10.0', @@ -72,7 +82,7 @@ export class DeprecatedTasks { : null; } - mergeDeprecatedCallbackFunctions( taskLists ) { + mergeDeprecatedCallbackFunctions( taskLists: TaskListType[] ) { if ( this.filteredTasks.length > 0 ) { for ( const taskList of taskLists ) { // Merge any extended task list items, to keep the callback functions around. @@ -99,10 +109,10 @@ export class DeprecatedTasks { * @param {Array} keys to keep in the task object. * @return {Object} task with the keys specified. */ - static possiblyPruneTaskData( task, keys ) { + static possiblyPruneTaskData( task: TaskType, keys: ( keyof TaskType )[] ) { if ( ! task.time && ! task.title ) { // client side task - return keys.reduce( + return keys.reduce< Partial< TaskType > >( ( simplifiedTask, key ) => { return { ...simplifiedTask, diff --git a/packages/js/data/src/onboarding/index.ts b/packages/js/data/src/onboarding/index.ts index d2b84f7afb3..a1e5b73193d 100644 --- a/packages/js/data/src/onboarding/index.ts +++ b/packages/js/data/src/onboarding/index.ts @@ -1,12 +1,10 @@ /** * External dependencies */ - import { registerStore } from '@wordpress/data'; import { controls } from '@wordpress/data-controls'; import { SelectFromMap, DispatchFromMap } from '@automattic/data-stores'; import { Reducer, AnyAction } from 'redux'; - /** * Internal dependencies */ @@ -14,11 +12,13 @@ import { STORE_NAME } from './constants'; import * as selectors from './selectors'; import * as actions from './actions'; import * as resolvers from './resolvers'; -import reducer from './reducer'; -import { WPDataSelectors } from '../types'; +import reducer, { State } from './reducer'; +import { WPDataActions, WPDataSelectors } from '../types'; +export * from './types'; +export type { State }; -registerStore( STORE_NAME, { - reducer: reducer as Reducer< ReturnType< Reducer >, AnyAction >, +registerStore< State >( STORE_NAME, { + reducer: reducer as Reducer< State, AnyAction >, actions, controls, selectors, @@ -27,12 +27,12 @@ registerStore( STORE_NAME, { export const ONBOARDING_STORE_NAME = STORE_NAME; -export type OnboardingSelector = SelectFromMap< typeof selectors >; +export type OnboardingSelector = SelectFromMap< typeof selectors > & + WPDataSelectors; declare module '@wordpress/data' { - // TODO: convert action.js to TS - function dispatch( key: typeof STORE_NAME ): DispatchFromMap< AnyAction >; - function select( + function dispatch( key: typeof STORE_NAME - ): SelectFromMap< typeof selectors > & WPDataSelectors; + ): DispatchFromMap< typeof actions & WPDataActions >; + function select( key: typeof STORE_NAME ): OnboardingSelector; } diff --git a/packages/js/data/src/onboarding/reducer.js b/packages/js/data/src/onboarding/reducer.ts similarity index 73% rename from packages/js/data/src/onboarding/reducer.js rename to packages/js/data/src/onboarding/reducer.ts index c7c1e84c424..206007aedde 100644 --- a/packages/js/data/src/onboarding/reducer.js +++ b/packages/js/data/src/onboarding/reducer.ts @@ -1,9 +1,17 @@ +/** + * External dependencies + */ + +import type { Reducer } from 'redux'; + /** * Internal dependencies */ import TYPES from './action-types'; +import { Action } from './actions'; +import { OnboardingState, TaskListType, TaskType } from './types'; -export const defaultState = { +export const defaultState: OnboardingState = { errors: {}, freeExtensions: [], profileItems: { @@ -31,7 +39,10 @@ export const defaultState = { taskLists: {}, }; -const getUpdatedTaskLists = ( taskLists, args ) => { +const getUpdatedTaskLists = ( + taskLists: Record< string, TaskListType >, + args: Partial< TaskType > +) => { return Object.keys( taskLists ).reduce( ( lists, taskListId ) => { return { @@ -54,46 +65,29 @@ const getUpdatedTaskLists = ( taskLists, args ) => { ); }; -const onboarding = ( +const reducer: Reducer< OnboardingState, Action > = ( state = defaultState, - { - freeExtensions, - type, - profileItems, - emailPrefill, - paymentMethods, - productTypes, - replace, - error, - isRequesting, - selector, - task, - taskId, - taskListId, - taskList, - taskLists, - keepCompletedTaskList, - } + action ) => { - switch ( type ) { + switch ( action.type ) { case TYPES.SET_PROFILE_ITEMS: return { ...state, - profileItems: replace - ? profileItems - : { ...state.profileItems, ...profileItems }, + profileItems: action.replace + ? action.profileItems + : { ...state.profileItems, ...action.profileItems }, }; case TYPES.SET_EMAIL_PREFILL: return { ...state, - emailPrefill, + emailPrefill: action.emailPrefill, }; case TYPES.SET_ERROR: return { ...state, errors: { ...state.errors, - [ selector ]: error, + [ action.selector ]: action.error, }, }; case TYPES.SET_IS_REQUESTING: @@ -101,25 +95,25 @@ const onboarding = ( ...state, requesting: { ...state.requesting, - [ selector ]: isRequesting, + [ action.selector ]: action.isRequesting, }, }; case TYPES.GET_PAYMENT_METHODS_SUCCESS: return { ...state, - paymentMethods, + paymentMethods: action.paymentMethods, }; case TYPES.GET_PRODUCT_TYPES_SUCCESS: return { ...state, - productTypes, + productTypes: action.productTypes, }; case TYPES.GET_PRODUCT_TYPES_ERROR: return { ...state, errors: { ...state.errors, - productTypes: error, + productTypes: action.error, }, }; case TYPES.GET_FREE_EXTENSIONS_ERROR: @@ -127,26 +121,26 @@ const onboarding = ( ...state, errors: { ...state.errors, - getFreeExtensions: error, + getFreeExtensions: action.error, }, }; case TYPES.GET_FREE_EXTENSIONS_SUCCESS: return { ...state, - freeExtensions, + freeExtensions: action.freeExtensions, }; case TYPES.GET_TASK_LISTS_ERROR: return { ...state, errors: { ...state.errors, - getTaskLists: error, + getTaskLists: action.error, }, }; case TYPES.GET_TASK_LISTS_SUCCESS: return { ...state, - taskLists: taskLists.reduce( ( lists, list ) => { + taskLists: action.taskLists.reduce( ( lists, list ) => { return { ...lists, [ list.id ]: list, @@ -158,10 +152,10 @@ const onboarding = ( ...state, errors: { ...state.errors, - dismissTask: error, + dismissTask: action.error, }, taskLists: getUpdatedTaskLists( state.taskLists, { - id: taskId, + id: action.taskId, isDismissed: false, } ), }; @@ -173,7 +167,7 @@ const onboarding = ( dismissTask: true, }, taskLists: getUpdatedTaskLists( state.taskLists, { - id: taskId, + id: action.taskId, isDismissed: true, } ), }; @@ -184,17 +178,17 @@ const onboarding = ( ...state.requesting, dismissTask: false, }, - taskLists: getUpdatedTaskLists( state.taskLists, task ), + taskLists: getUpdatedTaskLists( state.taskLists, action.task ), }; case TYPES.UNDO_DISMISS_TASK_ERROR: return { ...state, errors: { ...state.errors, - undoDismissTask: error, + undoDismissTask: action.error, }, taskLists: getUpdatedTaskLists( state.taskLists, { - id: taskId, + id: action.taskId, isDismissed: true, } ), }; @@ -206,7 +200,7 @@ const onboarding = ( undoDismissTask: true, }, taskLists: getUpdatedTaskLists( state.taskLists, { - id: taskId, + id: action.taskId, isDismissed: false, } ), }; @@ -217,17 +211,17 @@ const onboarding = ( ...state.requesting, undoDismissTask: false, }, - taskLists: getUpdatedTaskLists( state.taskLists, task ), + taskLists: getUpdatedTaskLists( state.taskLists, action.task ), }; case TYPES.SNOOZE_TASK_ERROR: return { ...state, errors: { ...state.errors, - snoozeTask: error, + snoozeTask: action.error, }, taskLists: getUpdatedTaskLists( state.taskLists, { - id: taskId, + id: action.taskId, isSnoozed: false, } ), }; @@ -239,7 +233,7 @@ const onboarding = ( snoozeTask: true, }, taskLists: getUpdatedTaskLists( state.taskLists, { - id: taskId, + id: action.taskId, isSnoozed: true, } ), }; @@ -250,17 +244,17 @@ const onboarding = ( ...state.requesting, snoozeTask: false, }, - taskLists: getUpdatedTaskLists( state.taskLists, task ), + taskLists: getUpdatedTaskLists( state.taskLists, action.task ), }; case TYPES.UNDO_SNOOZE_TASK_ERROR: return { ...state, errors: { ...state.errors, - undoSnoozeTask: error, + undoSnoozeTask: action.error, }, taskLists: getUpdatedTaskLists( state.taskLists, { - id: taskId, + id: action.taskId, isSnoozed: true, } ), }; @@ -272,7 +266,7 @@ const onboarding = ( undoSnoozeTask: true, }, taskLists: getUpdatedTaskLists( state.taskLists, { - id: taskId, + id: action.taskId, isSnoozed: false, } ), }; @@ -283,19 +277,19 @@ const onboarding = ( ...state.requesting, undoSnoozeTask: false, }, - taskLists: getUpdatedTaskLists( state.taskLists, task ), + taskLists: getUpdatedTaskLists( state.taskLists, action.task ), }; case TYPES.HIDE_TASK_LIST_ERROR: return { ...state, errors: { ...state.errors, - hideTaskList: error, + hideTaskList: action.error, }, taskLists: { ...state.taskLists, - [ taskListId ]: { - ...state.taskLists[ taskListId ], + [ action.taskListId ]: { + ...state.taskLists[ action.taskListId ], isHidden: false, isVisible: true, }, @@ -310,8 +304,8 @@ const onboarding = ( }, taskLists: { ...state.taskLists, - [ taskListId ]: { - ...state.taskLists[ taskListId ], + [ action.taskListId ]: { + ...state.taskLists[ action.taskListId ], isHidden: true, isVisible: false, }, @@ -326,7 +320,7 @@ const onboarding = ( }, taskLists: { ...state.taskLists, - [ taskListId ]: taskList, + [ action.taskListId ]: action.taskList, }, }; case TYPES.UNHIDE_TASK_LIST_ERROR: @@ -334,12 +328,12 @@ const onboarding = ( ...state, errors: { ...state.errors, - unhideTaskList: error, + unhideTaskList: action.error, }, taskLists: { ...state.taskLists, - [ taskListId ]: { - ...state.taskLists[ taskListId ], + [ action.taskListId ]: { + ...state.taskLists[ action.taskListId ], isHidden: true, isVisible: false, }, @@ -354,8 +348,8 @@ const onboarding = ( }, taskLists: { ...state.taskLists, - [ taskListId ]: { - ...state.taskLists[ taskListId ], + [ action.taskListId ]: { + ...state.taskLists[ action.taskListId ], isHidden: false, isVisible: true, }, @@ -370,7 +364,7 @@ const onboarding = ( }, taskLists: { ...state.taskLists, - [ taskListId ]: taskList, + [ action.taskListId ]: action.taskList, }, }; case TYPES.KEEP_COMPLETED_TASKS_SUCCESS: @@ -378,9 +372,9 @@ const onboarding = ( ...state, taskLists: { ...state.taskLists, - [ taskListId ]: { - ...state.taskLists[ taskListId ], - keepCompletedTaskList, + [ action.taskListId ]: { + ...state.taskLists[ action.taskListId ], + keepCompletedTaskList: action.keepCompletedTaskList, }, }, }; @@ -388,7 +382,7 @@ const onboarding = ( return { ...state, taskLists: getUpdatedTaskLists( state.taskLists, { - id: taskId, + id: action.taskId, isComplete: true, } ), }; @@ -396,7 +390,7 @@ const onboarding = ( return { ...state, taskLists: getUpdatedTaskLists( state.taskLists, { - id: taskId, + id: action.taskId, isVisited: true, } ), }; @@ -405,10 +399,10 @@ const onboarding = ( ...state, errors: { ...state.errors, - actionTask: error, + actionTask: action.error, }, taskLists: getUpdatedTaskLists( state.taskLists, { - id: taskId, + id: action.taskId, isActioned: false, } ), }; @@ -420,7 +414,7 @@ const onboarding = ( actionTask: true, }, taskLists: getUpdatedTaskLists( state.taskLists, { - id: taskId, + id: action.taskId, isActioned: true, } ), }; @@ -431,11 +425,12 @@ const onboarding = ( ...state.requesting, actionTask: false, }, - taskLists: getUpdatedTaskLists( state.taskLists, task ), + taskLists: getUpdatedTaskLists( state.taskLists, action.task ), }; default: return state; } }; -export default onboarding; +export type State = ReturnType< typeof reducer >; +export default reducer; diff --git a/packages/js/data/src/onboarding/resolvers.js b/packages/js/data/src/onboarding/resolvers.ts similarity index 84% rename from packages/js/data/src/onboarding/resolvers.js rename to packages/js/data/src/onboarding/resolvers.ts index 466a2109625..ef9b03ed38b 100644 --- a/packages/js/data/src/onboarding/resolvers.js +++ b/packages/js/data/src/onboarding/resolvers.ts @@ -22,13 +22,20 @@ import { getProductTypesError, } from './actions'; import { DeprecatedTasks } from './deprecated-tasks'; +import { + ExtensionList, + OnboardingProductType, + ProfileItems, + TaskListType, +} from './types'; +import { Plugin } from '../plugins/types'; const resolveSelect = controls && controls.resolveSelect ? controls.resolveSelect : select; export function* getProfileItems() { try { - const results = yield apiFetch( { + const results: ProfileItems = yield apiFetch( { path: WC_ADMIN_NAMESPACE + '/onboarding/profile', method: 'GET', } ); @@ -41,7 +48,9 @@ export function* getProfileItems() { export function* getEmailPrefill() { try { - const results = yield apiFetch( { + const results: { + email: string; + } = yield apiFetch( { path: WC_ADMIN_NAMESPACE + '/onboarding/profile/experimental_get_email_prefill', @@ -57,7 +66,7 @@ export function* getEmailPrefill() { export function* getTaskLists() { const deprecatedTasks = new DeprecatedTasks(); try { - const results = yield apiFetch( { + const results: TaskListType[] = yield apiFetch( { path: WC_ADMIN_NAMESPACE + '/onboarding/tasks', method: deprecatedTasks.hasDeprecatedTasks() ? 'POST' : 'GET', data: deprecatedTasks.getPostData(), @@ -85,7 +94,7 @@ export function* getTask() { export function* getPaymentGatewaySuggestions() { try { - const results = yield apiFetch( { + const results: Plugin[] = yield apiFetch( { path: WC_ADMIN_NAMESPACE + '/payment-gateway-suggestions', method: 'GET', } ); @@ -98,7 +107,7 @@ export function* getPaymentGatewaySuggestions() { export function* getFreeExtensions() { try { - const results = yield apiFetch( { + const results: ExtensionList[] = yield apiFetch( { path: WC_ADMIN_NAMESPACE + '/onboarding/free-extensions', method: 'GET', } ); @@ -111,7 +120,7 @@ export function* getFreeExtensions() { export function* getProductTypes() { try { - const results = yield apiFetch( { + const results: OnboardingProductType[] = yield apiFetch( { path: WC_ADMIN_NAMESPACE + '/onboarding/product-types', method: 'GET', } ); diff --git a/packages/js/data/src/onboarding/selectors.ts b/packages/js/data/src/onboarding/selectors.ts index 5647a048650..b73c1111211 100644 --- a/packages/js/data/src/onboarding/selectors.ts +++ b/packages/js/data/src/onboarding/selectors.ts @@ -11,7 +11,7 @@ import { TaskListType, OnboardingState, ExtensionList, - ProfileItemsState, + ProfileItems, OnboardingProductType, } from './types'; import { WPDataSelectors } from '../types'; @@ -25,7 +25,7 @@ export const getFreeExtensions = ( export const getProfileItems = ( state: OnboardingState -): ProfileItemsState | Record< string, never > => { +): ProfileItems | Record< string, never > => { return state.profileItems || {}; }; diff --git a/packages/js/data/src/onboarding/test/reducer.js b/packages/js/data/src/onboarding/test/reducer.ts similarity index 52% rename from packages/js/data/src/onboarding/test/reducer.js rename to packages/js/data/src/onboarding/test/reducer.ts index d7b2efbff84..64074e86117 100644 --- a/packages/js/data/src/onboarding/test/reducer.js +++ b/packages/js/data/src/onboarding/test/reducer.ts @@ -8,8 +8,40 @@ import reducer, { defaultState } from '../reducer'; import TYPES from '../action-types'; +const profileItems = { + business_extensions: [], + completed: false, + industry: null, + number_employees: null, + other_platform: null, + other_platform_name: '', + product_count: null, + product_types: null, + revenue: null, + selling_venues: null, + setup_client: false, + skipped: true, + theme: null, + wccom_connected: null, + is_agree_marketing: null, + store_email: null, +}; + +const paymentMethods = [ + { + id: '', + content: '', + plugins: [], + title: '', + category_additional: [], + category_other: [], + image: '', + }, +]; + describe( 'plugins reducer', () => { it( 'should return a default state', () => { + // @ts-expect-error - we're testing the default state const state = reducer( undefined, {} ); expect( state ).toEqual( defaultState ); } ); @@ -17,52 +49,53 @@ describe( 'plugins reducer', () => { it( 'should handle SET_PROFILE_ITEMS', () => { const state = reducer( { - profileItems: { previousItem: 'value' }, + // @ts-expect-error - we're only testing profileItems + profileItems, }, { type: TYPES.SET_PROFILE_ITEMS, - profileItems: { propertyName: 'value' }, + profileItems: { is_agree_marketing: true }, } ); - expect( state.profileItems ).toHaveProperty( 'previousItem' ); - expect( state.profileItems ).toHaveProperty( 'propertyName' ); - expect( state.profileItems.propertyName ).toBe( 'value' ); + expect( state.profileItems.is_agree_marketing ).toBe( true ); } ); it( 'should handle SET_PROFILE_ITEMS with replace', () => { const state = reducer( { - profileItems: { previousItem: 'value' }, + // @ts-expect-error - we're only testing profileItems + profileItems, }, { type: TYPES.SET_PROFILE_ITEMS, - profileItems: { propertyName: 'value' }, + profileItems: { is_agree_marketing: true }, replace: true, } ); - expect( state.profileItems ).not.toHaveProperty( 'previousItem' ); - expect( state.profileItems ).toHaveProperty( 'propertyName' ); - expect( state.profileItems.propertyName ).toBe( 'value' ); + expect( state.profileItems ).not.toHaveProperty( 'store_email' ); + expect( state.profileItems ).toHaveProperty( 'is_agree_marketing' ); + expect( state.profileItems.is_agree_marketing ).toBe( true ); } ); it( 'should handle GET_PAYMENT_METHODS_SUCCESS', () => { const state = reducer( + // @ts-expect-error - we're only testing paymentMethods { - paymentMethods: [ { previousItem: 'value' } ], + paymentMethods, }, { type: TYPES.GET_PAYMENT_METHODS_SUCCESS, - paymentMethods: [ { newItem: 'changed' } ], + paymentMethods: [ { image_72x72: 'changed' } ], } ); expect( state.paymentMethods[ 0 ] ).not.toHaveProperty( 'previousItem' ); - expect( state.paymentMethods[ 0 ] ).toHaveProperty( 'newItem' ); - expect( state.paymentMethods[ 0 ].newItem ).toBe( 'changed' ); + expect( state.paymentMethods[ 0 ] ).toHaveProperty( 'image_72x72' ); + expect( state.paymentMethods[ 0 ].image_72x72 ).toBe( 'changed' ); } ); it( 'should handle SET_ERROR', () => { @@ -73,6 +106,7 @@ describe( 'plugins reducer', () => { } ); /* eslint-disable dot-notation */ + // @ts-expect-error we're asserting error properties expect( state.errors[ 'getProfileItems' ].code ).toBe( 'error' ); /* eslint-enable dot-notation */ } ); diff --git a/packages/js/data/src/onboarding/types.ts b/packages/js/data/src/onboarding/types.ts index 2bc05fafcc5..a41a295ba18 100644 --- a/packages/js/data/src/onboarding/types.ts +++ b/packages/js/data/src/onboarding/types.ts @@ -30,6 +30,27 @@ export type TaskType = { taxJarActivated?: boolean; avalaraActivated?: boolean; }; + // Possibly added in DeprecatedTasks.mergeDeprecatedCallbackFunctions + isDeprecated?: boolean; +}; + +// reference: https://github.com/woocommerce/woocommerce-admin/blob/75cf5292f66bf69202f67356d143743a8796a7f6/docs/examples/extensions/add-task/js/index.js#L77-L101 +export type DeprecatedTaskType = { + key: string; + title: string; + content: string; + container: React.ReactNode; + completed: boolean; + visible: boolean; + additionalInfo: string; + time: string; + isDismissable: boolean; + onDelete: () => void; + onDismiss: () => void; + allowRemindMeLater: string; + remindMeLater: () => () => void; + level?: string; + type?: string; }; export type TaskListSection = { @@ -60,7 +81,7 @@ export type TaskListType = { export type OnboardingState = { freeExtensions: ExtensionList[]; - profileItems: ProfileItemsState; + profileItems: ProfileItems; taskLists: Record< string, TaskListType >; paymentMethods: Plugin[]; productTypes: OnboardingProductType[]; @@ -104,7 +125,7 @@ export type RevenueTypeSlug = | '50000-250000' | 'more-than-250000'; -export type ProfileItemsState = { +export type ProfileItems = { business_extensions: [ ] | null; completed: boolean | null; industry: Industry[] | null; diff --git a/packages/js/data/src/onboarding/with-onboarding-hydration.js b/packages/js/data/src/onboarding/with-onboarding-hydration.js deleted file mode 100644 index b36032bee38..00000000000 --- a/packages/js/data/src/onboarding/with-onboarding-hydration.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * External dependencies - */ -import { createHigherOrderComponent } from '@wordpress/compose'; -import { useSelect } from '@wordpress/data'; -import { createElement, useRef } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { STORE_NAME } from './constants'; - -export const withOnboardingHydration = ( data ) => { - let hydratedProfileItems = false; - - return createHigherOrderComponent( - ( OriginalComponent ) => ( props ) => { - const onboardingRef = useRef( data ); - - useSelect( ( select, registry ) => { - if ( ! onboardingRef.current ) { - return; - } - - const { isResolving, hasFinishedResolution } = select( - STORE_NAME - ); - const { - startResolution, - finishResolution, - setProfileItems, - } = registry.dispatch( STORE_NAME ); - - const { profileItems } = onboardingRef.current; - - if ( - profileItems && - ! hydratedProfileItems && - ! isResolving( 'getProfileItems', [] ) && - ! hasFinishedResolution( 'getProfileItems', [] ) - ) { - startResolution( 'getProfileItems', [] ); - setProfileItems( profileItems, true ); - finishResolution( 'getProfileItems', [] ); - - hydratedProfileItems = true; - } - }, [] ); - - return ; - }, - 'withOnboardingHydration' - ); -}; diff --git a/packages/js/data/src/onboarding/with-onboarding-hydration.tsx b/packages/js/data/src/onboarding/with-onboarding-hydration.tsx new file mode 100644 index 00000000000..914d01f9fda --- /dev/null +++ b/packages/js/data/src/onboarding/with-onboarding-hydration.tsx @@ -0,0 +1,62 @@ +/** + * External dependencies + */ +import { createHigherOrderComponent } from '@wordpress/compose'; +import { useSelect } from '@wordpress/data'; +import { createElement, useRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { STORE_NAME } from './constants'; +import { ProfileItems } from './types'; +import { OnboardingSelector } from './'; + +export const withOnboardingHydration = ( data: { + profileItems: ProfileItems; +} ) => { + let hydratedProfileItems = false; + + return createHigherOrderComponent< Record< string, unknown > >( + ( OriginalComponent ) => ( props ) => { + const onboardingRef = useRef( data ); + + useSelect( + // @ts-expect-error // @ts-expect-error registry is not defined in the wp.data typings + ( select: ( s: string ) => OnboardingSelector, registry ) => { + if ( ! onboardingRef.current ) { + return; + } + + const { isResolving, hasFinishedResolution } = select( + STORE_NAME + ); + const { + startResolution, + finishResolution, + setProfileItems, + } = registry.dispatch( STORE_NAME ); + + const { profileItems } = onboardingRef.current; + + if ( + profileItems && + ! hydratedProfileItems && + ! isResolving( 'getProfileItems', [] ) && + ! hasFinishedResolution( 'getProfileItems', [] ) + ) { + startResolution( 'getProfileItems', [] ); + setProfileItems( profileItems, true ); + finishResolution( 'getProfileItems', [] ); + + hydratedProfileItems = true; + } + }, + [] + ); + + return ; + }, + 'withOnboardingHydration' + ); +}; diff --git a/packages/js/data/src/types/wp-data.ts b/packages/js/data/src/types/wp-data.ts index 22e04f270f2..46b5287fc6b 100644 --- a/packages/js/data/src/types/wp-data.ts +++ b/packages/js/data/src/types/wp-data.ts @@ -1,15 +1,24 @@ // Type for the basic selectors built into @wordpress/data, note these // types define the interface for the public selectors, so state is not an // argument. +// [wp.data.getSelectors](https://github.com/WordPress/gutenberg/blob/319deee5f4d4838d6bc280e9e2be89c7f43f2509/packages/data/src/store/index.js#L16-L20) +// [selector.js](https://github.com/WordPress/gutenberg/blob/trunk/packages/data/src/redux-store/metadata/selectors.js#L48-L52) export type WPDataSelectors = { + getIsResolving: ( selector: string, args?: string[] ) => boolean; hasStartedResolution: ( selector: string, args?: string[] ) => boolean; hasFinishedResolution: ( selector: string, args?: string[] ) => boolean; isResolving: ( selector: string, args?: string[] ) => boolean; + getCachedResolvers: () => unknown; }; +// [wp.data.getActions](https://github.com/WordPress/gutenberg/blob/319deee5f4d4838d6bc280e9e2be89c7f43f2509/packages/data/src/store/index.js#L31-L35) +// [actions.js](https://github.com/WordPress/gutenberg/blob/aa2bed9010aa50467cb43063e370b70a91591e9b/packages/data/src/redux-store/metadata/actions.js) export type WPDataActions = { startResolution: ( selector: string, args?: string[] ) => void; finishResolution: ( selector: string, args?: string[] ) => void; + invalidateResolution: ( selector: string ) => void; + invalidateResolutionForStore: ( selector: string ) => void; + invalidateResolutionForStoreSelector: ( selector: string ) => void; }; // Omitting state from selector parameter diff --git a/plugins/woocommerce-admin/client/tasks/deprecated-tasks.tsx b/plugins/woocommerce-admin/client/tasks/deprecated-tasks.tsx index 9893639c501..66ff5330455 100644 --- a/plugins/woocommerce-admin/client/tasks/deprecated-tasks.tsx +++ b/plugins/woocommerce-admin/client/tasks/deprecated-tasks.tsx @@ -4,18 +4,19 @@ import { registerPlugin } from '@wordpress/plugins'; import { WooOnboardingTask } from '@woocommerce/onboarding'; import { useSelect } from '@wordpress/data'; -import { ONBOARDING_STORE_NAME, TaskType } from '@woocommerce/data'; +import { + ONBOARDING_STORE_NAME, + TaskType, + DeprecatedTaskType, +} from '@woocommerce/data'; import { useEffect, useState } from '@wordpress/element'; -type DeprecatedTask = TaskType & { - container?: React.ReactNode; - isDeprecated?: boolean; -}; +type MergedTask = TaskType & DeprecatedTaskType; const DeprecatedWooOnboardingTaskFills = () => { - const [ deprecatedTasks, setDeprecatedTasks ] = useState< - DeprecatedTask[] - >( [] ); + const [ deprecatedTasks, setDeprecatedTasks ] = useState< MergedTask[] >( + [] + ); const { isResolving, taskLists } = useSelect( ( select ) => { return { isResolving: select( ONBOARDING_STORE_NAME ).isResolving( @@ -27,20 +28,18 @@ const DeprecatedWooOnboardingTaskFills = () => { useEffect( () => { if ( taskLists && taskLists.length > 0 ) { - const deprecatedTasksWithContainer = []; + const deprecatedTasksWithContainer: MergedTask[] = []; for ( const tasklist of taskLists ) { for ( const task of tasklist.tasks ) { if ( - ( task as DeprecatedTask ).isDeprecated && - ( task as DeprecatedTask ).container + ( task as MergedTask ).isDeprecated && + ( task as MergedTask ).container ) { - deprecatedTasksWithContainer.push( task ); + deprecatedTasksWithContainer.push( task as MergedTask ); } } } - setDeprecatedTasks( - deprecatedTasksWithContainer as DeprecatedTask[] - ); + setDeprecatedTasks( deprecatedTasksWithContainer ); } }, [ taskLists ] ); diff --git a/plugins/woocommerce/changelog/dev-migrate-onboarding-store-to-ts b/plugins/woocommerce/changelog/dev-migrate-onboarding-store-to-ts new file mode 100644 index 00000000000..82912e3487d --- /dev/null +++ b/plugins/woocommerce/changelog/dev-migrate-onboarding-store-to-ts @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Simply update deprecated-tasks.tsx type annotations + +