Add an endpoint and method for actioning tasks (https://github.com/woocommerce/woocommerce-admin/pull/7746)

* Add checks for actioned task status

* Update completion logic for task

* Add rest route for actioning tasks

* Add action in data store for actioning tasks

* Add test for actioning task

* Only prune isActioned from task data
This commit is contained in:
Joshua T Flowers 2021-10-05 13:07:50 -04:00 committed by GitHub
parent 5d7661eeb9
commit 93b42ad9ef
8 changed files with 219 additions and 48 deletions

View File

@ -107,14 +107,13 @@ const Marketing: React.FC< MarketingProps > = ( { onComplete } ) => {
const [ currentPlugin, setCurrentPlugin ] = useState< string | null >( const [ currentPlugin, setCurrentPlugin ] = useState< string | null >(
null null
); );
const { actionTask } = useDispatch( ONBOARDING_STORE_NAME );
const { installAndActivatePlugins } = useDispatch( PLUGINS_STORE_NAME ); const { installAndActivatePlugins } = useDispatch( PLUGINS_STORE_NAME );
const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
const { const {
activePlugins, activePlugins,
freeExtensions, freeExtensions,
installedPlugins, installedPlugins,
isResolving, isResolving,
trackedCompletedActions,
} = useSelect( ( select: WCDataSelector ) => { } = useSelect( ( select: WCDataSelector ) => {
const { getActivePlugins, getInstalledPlugins } = select( const { getActivePlugins, getInstalledPlugins } = select(
PLUGINS_STORE_NAME PLUGINS_STORE_NAME
@ -123,26 +122,11 @@ const Marketing: React.FC< MarketingProps > = ( { onComplete } ) => {
ONBOARDING_STORE_NAME ONBOARDING_STORE_NAME
); );
const {
getOption,
hasFinishedResolution: optionFinishedResolution,
} = select( OPTIONS_STORE_NAME );
const completedActions =
getOption( 'woocommerce_task_list_tracked_completed_actions' ) ||
EMPTY_ARRAY;
return { return {
activePlugins: getActivePlugins(), activePlugins: getActivePlugins(),
freeExtensions: getFreeExtensions(), freeExtensions: getFreeExtensions(),
installedPlugins: getInstalledPlugins(), installedPlugins: getInstalledPlugins(),
isResolving: ! ( isResolving: ! hasFinishedResolution( 'getFreeExtensions' ),
hasFinishedResolution( 'getFreeExtensions' ) &&
optionFinishedResolution( 'getOption', [
'woocommerce_task_list_tracked_completed_actions',
] )
),
trackedCompletedActions: completedActions,
}; };
} ); } );
@ -158,6 +142,7 @@ const Marketing: React.FC< MarketingProps > = ( { onComplete } ) => {
const installAndActivate = ( slug: string ) => { const installAndActivate = ( slug: string ) => {
setCurrentPlugin( slug ); setCurrentPlugin( slug );
actionTask( 'marketing' );
installAndActivatePlugins( [ slug ] ) installAndActivatePlugins( [ slug ] )
.then( ( response: { errors: Record< string, string > } ) => { .then( ( response: { errors: Record< string, string > } ) => {
recordEvent( 'tasklist_marketing_install', { recordEvent( 'tasklist_marketing_install', {
@ -167,18 +152,9 @@ const Marketing: React.FC< MarketingProps > = ( { onComplete } ) => {
), ),
} ); } );
if ( ! trackedCompletedActions.includes( 'marketing' ) ) {
updateOptions( {
woocommerce_task_list_tracked_completed_actions: [
...trackedCompletedActions,
'marketing',
],
} );
onComplete();
}
createNoticesFromResponse( response ); createNoticesFromResponse( response );
setCurrentPlugin( null ); setCurrentPlugin( null );
onComplete();
} ) } )
.catch( ( response: { errors: Record< string, string > } ) => { .catch( ( response: { errors: Record< string, string > } ) => {
createNoticesFromResponse( response ); createNoticesFromResponse( response );

View File

@ -26,6 +26,9 @@ const TYPES = {
HIDE_TASK_LIST_SUCCESS: 'HIDE_TASK_LIST_SUCCESS', HIDE_TASK_LIST_SUCCESS: 'HIDE_TASK_LIST_SUCCESS',
OPTIMISTICALLY_COMPLETE_TASK_REQUEST: OPTIMISTICALLY_COMPLETE_TASK_REQUEST:
'OPTIMISTICALLY_COMPLETE_TASK_REQUEST', 'OPTIMISTICALLY_COMPLETE_TASK_REQUEST',
ACTION_TASK_ERROR: 'ACTION_TASK_ERROR',
ACTION_TASK_REQUEST: 'ACTION_TASK_REQUEST',
ACTION_TASK_SUCCESS: 'ACTION_TASK_SUCCESS',
}; };
export default TYPES; export default TYPES;

View File

@ -199,6 +199,28 @@ export function setEmailPrefill( email ) {
}; };
} }
export function actionTaskError( taskId, error ) {
return {
type: TYPES.ACTION_TASK_ERROR,
taskId,
error,
};
}
export function actionTaskRequest( taskId ) {
return {
type: TYPES.ACTION_TASK_REQUEST,
taskId,
};
}
export function actionTaskSuccess( task ) {
return {
type: TYPES.ACTION_TASK_SUCCESS,
task,
};
}
export function* updateProfileItems( items ) { export function* updateProfileItems( items ) {
yield setIsRequesting( 'updateProfileItems', true ); yield setIsRequesting( 'updateProfileItems', true );
yield setError( 'updateProfileItems', null ); yield setError( 'updateProfileItems', null );
@ -347,3 +369,21 @@ export function* hideTaskList( id ) {
export function* optimisticallyCompleteTask( id ) { export function* optimisticallyCompleteTask( id ) {
yield optimisticallyCompleteTaskRequest( id ); yield optimisticallyCompleteTaskRequest( id );
} }
export function* actionTask( id ) {
yield actionTaskRequest( id );
try {
const task = yield apiFetch( {
path: `${ WC_ADMIN_NAMESPACE }/onboarding/tasks/${ id }/action`,
method: 'POST',
} );
yield actionTaskSuccess(
possiblyPruneTaskData( task, [ 'isActioned' ] )
);
} catch ( error ) {
yield actionTaskError( id, error );
throw new Error();
}
}

View File

@ -317,6 +317,39 @@ const onboarding = (
isComplete: true, isComplete: true,
} ), } ),
}; };
case TYPES.ACTION_TASK_ERROR:
return {
...state,
errors: {
...state.errors,
actionTask: error,
},
taskLists: getUpdatedTaskLists( state.taskLists, {
id: taskId,
isActioned: false,
} ),
};
case TYPES.ACTION_TASK_REQUEST:
return {
...state,
requesting: {
...state.requesting,
actionTask: true,
},
taskLists: getUpdatedTaskLists( state.taskLists, {
id: taskId,
isActioned: true,
} ),
};
case TYPES.ACTION_TASK_SUCCESS:
return {
...state,
requesting: {
...state.requesting,
actionTask: false,
},
taskLists: getUpdatedTaskLists( state.taskLists, task ),
};
default: default:
return state; return state;
} }

View File

@ -209,6 +209,19 @@ class OnboardingTasks extends \WC_REST_Data_Controller {
) )
); );
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[a-z0-9_\-]+)/action',
array(
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => array( $this, 'action_task' ),
'permission_callback' => array( $this, 'get_tasks_permission_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route( register_rest_route(
$this->namespace, $this->namespace,
'/' . $this->rest_base . '/(?P<id>[a-z0-9_\-]+)/undo_snooze', '/' . $this->rest_base . '/(?P<id>[a-z0-9_\-]+)/undo_snooze',
@ -951,4 +964,36 @@ class OnboardingTasks extends \WC_REST_Data_Controller {
return rest_ensure_response( $json ); return rest_ensure_response( $json );
} }
/**
* Action a single task.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Request|WP_Error
*/
public function action_task( $request ) {
$id = $request->get_param( 'id' );
$task = TaskLists::get_task( $id );
if ( ! $task && $id ) {
$task = new Task(
array(
'id' => $id,
)
);
}
if ( ! $task ) {
return new \WP_Error(
'woocommerce_rest_invalid_task',
__( 'Sorry, no task with that ID was found.', 'woocommerce-admin' ),
array(
'status' => 404,
)
);
}
$task->mark_actioned();
return rest_ensure_response( $task->get_json() );
}
} }

View File

@ -114,6 +114,13 @@ class Task {
*/ */
const SNOOZED_OPTION = 'woocommerce_task_list_remind_me_later_tasks'; const SNOOZED_OPTION = 'woocommerce_task_list_remind_me_later_tasks';
/**
* Name of the actioned option.
*
* @var string
*/
const ACTIONED_OPTION = 'woocommerce_task_list_tracked_completed_actions';
/** /**
* Duration to milisecond mapping. * Duration to milisecond mapping.
* *
@ -297,6 +304,7 @@ class Task {
'canView' => $this->can_view, 'canView' => $this->can_view,
'time' => $this->time, 'time' => $this->time,
'level' => $this->level, 'level' => $this->level,
'isActioned' => $this->is_actioned(),
'isDismissed' => $this->is_dismissed(), 'isDismissed' => $this->is_dismissed(),
'isDismissable' => $this->is_dismissable, 'isDismissable' => $this->is_dismissable,
'isSnoozed' => $this->is_snoozed(), 'isSnoozed' => $this->is_snoozed(),
@ -305,4 +313,32 @@ class Task {
); );
} }
/**
* Mark a task as actioned. Used to verify an action has taken place in some tasks.
*
* @return bool
*/
public function mark_actioned() {
$actioned = get_option( self::ACTIONED_OPTION, array() );
$actioned[] = $this->id;
$update = update_option( self::ACTIONED_OPTION, array_unique( $actioned ) );
if ( $update ) {
wc_admin_record_tracks_event( 'tasklist_actioned_task', array( 'task_name' => $this->id ) );
}
return $update;
}
/**
* Check if a task has been actioned.
*
* @return bool
*/
public function is_actioned() {
$actioned = get_option( self::ACTIONED_OPTION, array() );
return in_array( $this->id, $actioned, true );
}
} }

View File

@ -3,6 +3,7 @@
namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks; namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;
use Automattic\WooCommerce\Admin\Features\Features; use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
use Automattic\WooCommerce\Admin\Features\RemoteFreeExtensions\Init as RemoteFreeExtensions; use Automattic\WooCommerce\Admin\Features\RemoteFreeExtensions\Init as RemoteFreeExtensions;
/** /**
@ -23,23 +24,37 @@ class Marketing {
'woocommerce-admin' 'woocommerce-admin'
), ),
'is_complete' => self::has_installed_extensions(), 'is_complete' => self::has_installed_extensions(),
'can_view' => Features::is_enabled( 'remote-free-extensions' ) && count( self::get_bundles() ) > 0, 'can_view' => Features::is_enabled( 'remote-free-extensions' ) && count( self::get_plugins() ) > 0,
'time' => __( '1 minute', 'woocommerce-admin' ), 'time' => __( '1 minute', 'woocommerce-admin' ),
); );
} }
/** /**
* Get the marketing bundles. * Get the marketing plugins.
* *
* @return array * @return array
*/ */
public static function get_bundles() { public static function get_plugins() {
return RemoteFreeExtensions::get_extensions( $bundles = RemoteFreeExtensions::get_extensions(
array( array(
'reach', 'reach',
'grow', 'grow',
) )
); );
return array_reduce(
$bundles,
function( $plugins, $bundle ) {
$visible = array();
foreach ( $bundle['plugins'] as $plugin ) {
if ( $plugin->is_visible ) {
$visible[] = $plugin;
}
}
return array_merge( $plugins, $visible );
},
array()
);
} }
/** /**
@ -48,22 +63,29 @@ class Marketing {
* @return bool * @return bool
*/ */
public static function has_installed_extensions() { public static function has_installed_extensions() {
$bundles = self::get_bundles(); $plugins = self::get_plugins();
$remaining = array();
$installed = array();
return array_reduce( foreach ( $plugins as $plugin ) {
$bundles, if ( ! $plugin->is_installed ) {
function( $has_installed, $bundle ) { $remaining[] = $plugin;
if ( $has_installed ) { } else {
$installed[] = $plugin;
}
}
// All extensions installed.
if ( count( $remaining ) === 0 ) {
return true; return true;
} }
foreach ( $bundle['plugins'] as $plugin ) {
if ( $plugin->is_installed ) { // Make sure the task has been actioned and at least one extension is installed.
$task = new Task( array( 'id' => 'marketing' ) );
if ( count( $installed ) > 0 && $task->is_actioned() ) {
return true; return true;
} }
}
return false; return false;
},
false
);
} }
} }

View File

@ -213,5 +213,21 @@ class WC_Tests_OnboardingTasks_Task extends WC_Unit_Test_Case {
$this->assertArrayHasKey( 'snoozedUntil', $json ); $this->assertArrayHasKey( 'snoozedUntil', $json );
} }
/**
* Tests that a task can be actioned.
*/
public function test_action_task() {
$task = new Task(
array(
'id' => 'wc-unit-test-task',
)
);
$update = $task->mark_actioned();
$actioned = get_option( Task::ACTIONED_OPTION, array() );
$this->assertEquals( true, $update );
$this->assertContains( $task->id, $actioned );
}
} }