diff --git a/plugins/woocommerce-admin/client/analytics/settings/config.js b/plugins/woocommerce-admin/client/analytics/settings/config.js index dbe92100b8f..681c2b714e9 100644 --- a/plugins/woocommerce-admin/client/analytics/settings/config.js +++ b/plugins/woocommerce-admin/client/analytics/settings/config.js @@ -37,6 +37,38 @@ const orderStatuses = Object.keys( wcSettings.orderStatuses ) } ); export const analyticsSettings = applyFilters( SETTINGS_FILTER, [ + { + name: 'woocommerce_rebuild_reports_data', + label: __( 'Rebuild reports data:', 'wc-admin' ), + inputType: 'button', + inputText: __( 'Rebuild reports', 'wc-admin' ), + helpText: __( + 'This tool will rebuild all of the information used by the reports. ' + + 'Data will be processed in the background and may take some time depending on the size of your store.', + 'wc-admin' + ), + callback: ( resolve, reject, addNotice ) => { + const errorMessage = __( 'There was a problem rebuilding your report data.', 'wc-admin' ); + + apiFetch( { path: '/wc/v3/system_status/tools/rebuild_stats', method: 'PUT' } ) + .then( response => { + if ( response.success ) { + addNotice( { status: 'success', message: response.message } ); + // @todo This should be changed to detect when the lookup table population is complete. + setTimeout( () => resolve(), 300000 ); + } else { + addNotice( { status: 'error', message: errorMessage } ); + reject(); + } + } ) + .catch( error => { + if ( error && error.message ) { + addNotice( { status: 'error', message: error.message } ); + } + reject(); + } ); + }, + }, { name: 'woocommerce_excluded_report_order_statuses', label: __( 'Excluded Statuses:', 'wc-admin' ), @@ -89,36 +121,4 @@ export const analyticsSettings = applyFilters( SETTINGS_FILTER, [ initialValue: wcSettings.wcAdminSettings.woocommerce_actionable_order_statuses || [], defaultValue: [ 'processing', 'on-hold' ], }, - { - name: 'woocommerce_rebuild_reports_data', - label: __( 'Rebuild reports data:', 'wc-admin' ), - inputType: 'button', - inputText: __( 'Rebuild reports', 'wc-admin' ), - helpText: __( - 'This tool will rebuild all of the information used by the reports. ' + - 'Data will be processed in the background and may take some time depending on the size of your store.', - 'wc-admin' - ), - callback: ( resolve, reject, addNotice ) => { - const errorMessage = __( 'There was a problem rebuilding your report data.', 'wc-admin' ); - - apiFetch( { path: '/wc/v3/system_status/tools/rebuild_stats', method: 'PUT' } ) - .then( response => { - if ( response.success ) { - addNotice( { status: 'success', message: response.message } ); - // @todo This should be changed to detect when the lookup table population is complete. - setTimeout( () => resolve(), 300000 ); - } else { - addNotice( { status: 'error', message: errorMessage } ); - reject(); - } - } ) - .catch( error => { - if ( error && error.message ) { - addNotice( { status: 'error', message: error.message } ); - } - reject(); - } ); - }, - }, ] ); diff --git a/plugins/woocommerce-admin/client/layout/store-alerts/index.js b/plugins/woocommerce-admin/client/layout/store-alerts/index.js index 58c80b286f4..366e23bd27b 100644 --- a/plugins/woocommerce-admin/client/layout/store-alerts/index.js +++ b/plugins/woocommerce-admin/client/layout/store-alerts/index.js @@ -8,6 +8,8 @@ import { IconButton, Button, Dashicon } from '@wordpress/components'; import classnames from 'classnames'; import interpolateComponents from 'interpolate-components'; import { compose } from '@wordpress/compose'; +import { noop } from 'lodash'; +import { withDispatch } from '@wordpress/data'; /** * WooCommerce dependencies @@ -64,7 +66,7 @@ class StoreAlerts extends Component { const alerts = this.props.alerts || []; const preloadAlertCount = wcSettings.alertCount && parseInt( wcSettings.alertCount ); - if ( preloadAlertCount > 0 && 0 === alerts.length ) { + if ( preloadAlertCount > 0 && this.props.isLoading ) { return 1 } />; } else if ( 0 === alerts.length ) { return null; @@ -78,11 +80,22 @@ class StoreAlerts extends Component { 'is-alert-error': 'error' === type, 'is-alert-update': 'update' === type, } ); - const actions = alert.actions.map( action => ( - - ) ); + + const actions = alert.actions.map( action => { + const markStatus = () => { + this.props.updateNote( alert.id, { status: action.status } ); + }; + return ( + + ); + } ); return ( { - const { getNotes } = select( 'wc-api' ); + const { getNotes, isGetNotesRequesting } = select( 'wc-api' ); const alertsQuery = { page: 1, per_page: QUERY_DEFAULTS.pageSize, type: 'error,update', + status: 'unactioned', }; - const alerts = getNotes( alertsQuery ); + // Filter out notes that may have been marked actioned or not delayed after the initial request + const filterNotes = note => 'unactioned' === note.status; + const alerts = getNotes( alertsQuery ).filter( filterNotes ); - return { alerts }; + const isLoading = isGetNotesRequesting( alertsQuery ); + + return { + alerts, + isLoading, + }; + } ), + withDispatch( dispatch => { + const { updateNote } = dispatch( 'wc-api' ); + return { + updateNote, + }; } ) )( StoreAlerts ); diff --git a/plugins/woocommerce-admin/client/wc-api/notes/index.js b/plugins/woocommerce-admin/client/wc-api/notes/index.js index cd0cff2f807..094ff3a82fa 100644 --- a/plugins/woocommerce-admin/client/wc-api/notes/index.js +++ b/plugins/woocommerce-admin/client/wc-api/notes/index.js @@ -4,8 +4,10 @@ */ import operations from './operations'; import selectors from './selectors'; +import mutations from './mutations'; export default { operations, selectors, + mutations, }; diff --git a/plugins/woocommerce-admin/client/wc-api/notes/mutations.js b/plugins/woocommerce-admin/client/wc-api/notes/mutations.js new file mode 100644 index 00000000000..15f98427aae --- /dev/null +++ b/plugins/woocommerce-admin/client/wc-api/notes/mutations.js @@ -0,0 +1,10 @@ +/** @format */ + +const updateNote = operations => ( noteId, noteFields ) => { + const resourceKey = 'note'; + operations.update( [ resourceKey ], { [ resourceKey ]: { noteId, ...noteFields } } ); +}; + +export default { + updateNote, +}; diff --git a/plugins/woocommerce-admin/client/wc-api/notes/operations.js b/plugins/woocommerce-admin/client/wc-api/notes/operations.js index 7ef24bd83c9..06ab2f596de 100644 --- a/plugins/woocommerce-admin/client/wc-api/notes/operations.js +++ b/plugins/woocommerce-admin/client/wc-api/notes/operations.js @@ -19,6 +19,10 @@ function read( resourceNames, fetch = apiFetch ) { return [ ...readNotes( resourceNames, fetch ), ...readNoteQueries( resourceNames, fetch ) ]; } +function update( resourceNames, data, fetch = apiFetch ) { + return [ ...updateNote( resourceNames, data, fetch ) ]; +} + function readNoteQueries( resourceNames, fetch ) { const filteredNames = resourceNames.filter( name => isResourcePrefix( name, 'note-query' ) ); @@ -71,6 +75,25 @@ function readNote( resourceName, fetch ) { } ); } +function updateNote( resourceNames, data, fetch ) { + const resourceName = 'note'; + if ( resourceNames.includes( resourceName ) ) { + const { noteId, ...noteFields } = data[ resourceName ]; + const url = `${ NAMESPACE }/admin/notes/${ noteId }`; + return [ + fetch( { path: url, method: 'PUT', data: noteFields } ) + .then( note => { + return { [ resourceName + ':' + noteId ]: { data: note } }; + } ) + .catch( error => { + return { [ resourceName + ':' + noteId ]: { error } }; + } ), + ]; + } + return []; +} + export default { read, + update, }; diff --git a/plugins/woocommerce-admin/client/wc-api/wc-api-spec.js b/plugins/woocommerce-admin/client/wc-api/wc-api-spec.js index c9e9132b6f1..6e559cb4288 100644 --- a/plugins/woocommerce-admin/client/wc-api/wc-api-spec.js +++ b/plugins/woocommerce-admin/client/wc-api/wc-api-spec.js @@ -16,6 +16,7 @@ function createWcApiSpec() { mutations: { ...settings.mutations, ...user.mutations, + ...notes.mutations, }, selectors: { ...items.selectors, @@ -42,6 +43,7 @@ function createWcApiSpec() { return [ ...settings.operations.update( resourceNames, data ), ...user.operations.update( resourceNames, data ), + ...notes.operations.update( resourceNames, data ), ]; }, }, diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-admin-notes-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-admin-notes-controller.php index a2d9a1ecc91..79bec1cbdce 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-admin-notes-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-admin-notes-controller.php @@ -63,6 +63,11 @@ class WC_Admin_REST_Admin_Notes_Controller extends WC_REST_CRUD_Controller { 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_items_permissions_check' ), + ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); @@ -124,6 +129,12 @@ class WC_Admin_REST_Admin_Notes_Controller extends WC_REST_CRUD_Controller { $args['type'] = $type; } + $status = isset( $request['status'] ) ? $request['status'] : ''; + $status = sanitize_text_field( $status ); + if ( ! empty( $status ) ) { + $args['status'] = $status; + } + $notes = WC_Admin_Notes::get_notes( 'edit', $args ); $data = array(); @@ -134,7 +145,7 @@ class WC_Admin_REST_Admin_Notes_Controller extends WC_REST_CRUD_Controller { } $response = rest_ensure_response( $data ); - $response->header( 'X-WP-Total', WC_Admin_Notes::get_notes_count() ); + $response->header( 'X-WP-Total', WC_Admin_Notes::get_notes_count( $type, $status ) ); return $response; } @@ -167,6 +178,49 @@ class WC_Admin_REST_Admin_Notes_Controller extends WC_REST_CRUD_Controller { return true; } + /** + * Update a single note. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function update_item( $request ) { + $note = WC_Admin_Notes::get_note( $request->get_param( 'id' ) ); + + if ( ! $note ) { + return new WP_Error( + 'woocommerce_admin_notes_invalid_id', + __( 'Sorry, there is no resouce with that ID.', 'wc-admin' ), + array( 'status' => 404 ) + ); + } + + // @todo Status is the only field that can be updated at the moment. We should also implement the "date reminder" setting. + $note_changed = false; + if ( ! is_null( $request->get_param( 'status' ) ) ) { + $note->set_status( $request->get_param( 'status' ) ); + $note_changed = true; + } + + if ( $note_changed ) { + $note->save(); + } + return $this->get_item( $request ); + } + + /** + * Makes sure the current user has access to WRITE the settings APIs. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|bool + */ + public function update_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'wc-admin' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + /** * Prepare a path or query for serialization to the client. * @@ -204,8 +258,9 @@ class WC_Admin_REST_Admin_Notes_Controller extends WC_REST_CRUD_Controller { $data['title'] = stripslashes( $data['title'] ); $data['content'] = stripslashes( $data['content'] ); foreach ( (array) $data['actions'] as $key => $value ) { - $data['actions'][ $key ]->label = stripslashes( $data['actions'][ $key ]->label ); - $data['actions'][ $key ]->url = $this->prepare_query_for_response( $data['actions'][ $key ]->query ); + $data['actions'][ $key ]->label = stripslashes( $data['actions'][ $key ]->label ); + $data['actions'][ $key ]->url = $this->prepare_query_for_response( $data['actions'][ $key ]->query ); + $data['actions'][ $key ]->status = stripslashes( $data['actions'][ $key ]->status ); } $data = $this->filter_response_by_context( $data, $context ); @@ -233,6 +288,29 @@ class WC_Admin_REST_Admin_Notes_Controller extends WC_REST_CRUD_Controller { return apply_filters( 'woocommerce_rest_prepare_admin_note', $response, $data, $request ); } + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = array(); + $params['context'] = $this->get_context_param( array( 'default' => 'view' ) ); + $params['type'] = array( + 'description' => __( 'Type of note.', 'wc-admin' ), + 'type' => 'string', + 'enum' => WC_Admin_Note::get_allowed_types(), + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['status'] = array( + 'description' => __( 'Status of note.', 'wc-admin' ), + 'type' => 'string', + 'enum' => WC_Admin_Note::get_allowed_statuses(), + 'validate_callback' => 'rest_validate_request_arg', + ); + return $params; + } + /** * Get the note's schema, conforming to JSON Schema. * @@ -296,7 +374,6 @@ class WC_Admin_REST_Admin_Notes_Controller extends WC_REST_CRUD_Controller { 'description' => __( 'The status of the note (e.g. unactioned, actioned).', 'wc-admin' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), - 'readonly' => true, ), 'source' => array( 'description' => __( 'Source of the note.', 'wc-admin' ), @@ -320,7 +397,7 @@ class WC_Admin_REST_Admin_Notes_Controller extends WC_REST_CRUD_Controller { 'description' => __( 'Date after which the user should be reminded of the note, if any.', 'wc-admin' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), - 'readonly' => true, + 'readonly' => true, // @todo Allow date_reminder to be updated. ), 'date_reminder_gmt' => array( 'description' => __( 'Date after which the user should be reminded of the note, if any (GMT).', 'wc-admin' ), diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-install.php b/plugins/woocommerce-admin/includes/class-wc-admin-install.php index 7a323af069d..2925e09cdf5 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-install.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-install.php @@ -16,7 +16,7 @@ class WC_Admin_Install { * * @todo get this dynamically? */ - const VERSION_NUMBER = '0.6.0'; + const VERSION_NUMBER = '0.8.0'; /** * Plugin version option name. @@ -76,6 +76,8 @@ class WC_Admin_Install { self::create_tables(); self::update_wc_admin_version(); + WC_Admin_Notes_Historical_Data::add_note(); + delete_transient( 'wc_admin_installing' ); do_action( 'wc_admin_installed' ); @@ -174,6 +176,7 @@ class WC_Admin_Install { name varchar(255) NOT NULL, label varchar(255) NOT NULL, query longtext NOT NULL, + status varchar(255) NOT NULL, PRIMARY KEY (action_id), KEY note_id (note_id) ) $collate; diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-note.php b/plugins/woocommerce-admin/includes/class-wc-admin-note.php index 422b70e197b..6e08c9fba84 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-note.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-note.php @@ -365,6 +365,7 @@ class WC_Admin_Note extends WC_Data { /** * Set note data for potential re-localization. * + * @todo Set a default empty array? https://github.com/woocommerce/wc-admin/pull/1763#pullrequestreview-212442921. * @param object $content_data Note data. */ public function set_content_data( $content_data ) { @@ -450,11 +451,13 @@ class WC_Admin_Note extends WC_Data { * @param string $name Label name (not presented to user). * @param string $label Note label (e.g. presented as button label). * @param string $query Note query (for redirect). + * @param string $status The status to set for the action should on click. */ - public function add_action( $name, $label, $query ) { - $name = wc_clean( $name ); - $label = wc_clean( $label ); - $query = wc_clean( $query ); + public function add_action( $name, $label, $query, $status = '' ) { + $name = wc_clean( $name ); + $label = wc_clean( $label ); + $query = wc_clean( $query ); + $status = wc_clean( $status ); if ( empty( $name ) ) { $this->error( 'admin_note_invalid_data', __( 'The admin note action name prop cannot be empty.', 'wc-admin' ) ); @@ -469,9 +472,10 @@ class WC_Admin_Note extends WC_Data { } $action = array( - 'name' => $name, - 'label' => $label, - 'query' => $query, + 'name' => $name, + 'label' => $label, + 'query' => $query, + 'status' => $status, ); $note_actions = $this->get_prop( 'actions', 'edit' ); diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-notes-historical-data.php b/plugins/woocommerce-admin/includes/class-wc-admin-notes-historical-data.php new file mode 100644 index 00000000000..922f22caced --- /dev/null +++ b/plugins/woocommerce-admin/includes/class-wc-admin-notes-historical-data.php @@ -0,0 +1,50 @@ +get_notes_with_name( self::NOTE_NAME ); + if ( ! empty( $note_ids ) ) { + return; + } + + $note = new WC_Admin_Note(); + $note->set_title( __( 'WooCommerce Admin: Historical Analytics Data', 'wc-admin' ) ); + $note->set_content( __( 'To view your historical analytics data, you must process your existing orders and customers.', 'wc-admin' ) ); + $note->set_type( WC_Admin_Note::E_WC_ADMIN_NOTE_UPDATE ); + $note->set_icon( 'info' ); + $note->set_name( self::NOTE_NAME ); + $note->set_content_data( (object) array() ); + $note->set_source( 'woocommerce-admin' ); + // @todo Add remind me later option. See https://github.com/woocommerce/wc-admin/issues/1756. + $note->add_action( + 'get-started', + __( 'Get Started', 'wc-admin' ), + '?page=wc-admin#/analytics/settings', + 'actioned' + ); + + $note->save(); + } +} + +new WC_Admin_Notes_Historical_Data(); diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-notes-new-sales-record.php b/plugins/woocommerce-admin/includes/class-wc-admin-notes-new-sales-record.php index 924af710a3f..c4b30fe1899 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-notes-new-sales-record.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-notes-new-sales-record.php @@ -94,7 +94,7 @@ class WC_Admin_Notes_New_Sales_Record { $note->set_type( WC_Admin_Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_icon( 'trophy' ); $note->set_name( self::NOTE_NAME ); - $note->set_source( 'wc-admin' ); + $note->set_source( 'woocommerce-admin' ); $note->add_action( 'view-report', __( 'View report', 'wc-admin' ), '?page=wc-admin#/analytics' ); $note->save(); } diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-notes-settings-notes.php b/plugins/woocommerce-admin/includes/class-wc-admin-notes-settings-notes.php index 16ebae887ad..78008f3689f 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-notes-settings-notes.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-notes-settings-notes.php @@ -42,7 +42,7 @@ class WC_Admin_Notes_Settings_Notes { $note->set_type( WC_Admin_Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_icon( 'info' ); $note->set_name( $name ); - $note->set_source( 'wc-admin' ); + $note->set_source( 'woocommerce-admin' ); $note->add_action( 'open-customizer', __( 'Open Customizer', 'wc-admin' ), diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-notes-woo-subscriptions-notes.php b/plugins/woocommerce-admin/includes/class-wc-admin-notes-woo-subscriptions-notes.php index 1f12b2b4e9e..0c3fc1abddf 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-notes-woo-subscriptions-notes.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-notes-woo-subscriptions-notes.php @@ -185,7 +185,7 @@ class WC_Admin_Notes_Woo_Subscriptions_Notes { $note->set_type( WC_Admin_Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_icon( 'info' ); $note->set_name( self::CONNECTION_NOTE_NAME ); - $note->set_source( 'wc-admin' ); + $note->set_source( 'woocommerce-admin' ); $note->add_action( 'connect', __( 'Connect', 'wc-admin' ), @@ -334,7 +334,7 @@ class WC_Admin_Notes_Woo_Subscriptions_Notes { $note->set_type( WC_Admin_Note::E_WC_ADMIN_NOTE_WARNING ); $note->set_icon( 'notice' ); $note->set_name( self::SUBSCRIPTION_NOTE_NAME ); - $note->set_source( 'wc-admin' ); + $note->set_source( 'woocommerce-admin' ); $note->clear_actions(); $note->add_action( 'enable-autorenew', @@ -398,7 +398,7 @@ class WC_Admin_Notes_Woo_Subscriptions_Notes { $note->set_type( WC_Admin_Note::E_WC_ADMIN_NOTE_WARNING ); $note->set_icon( 'notice' ); $note->set_name( self::SUBSCRIPTION_NOTE_NAME ); - $note->set_source( 'wc-admin' ); + $note->set_source( 'woocommerce-admin' ); $note->clear_actions(); $note->add_action( 'renew-subscription', diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-notes.php b/plugins/woocommerce-admin/includes/class-wc-admin-notes.php index ac7c71011e0..20390eaf664 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-notes.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-notes.php @@ -64,11 +64,12 @@ class WC_Admin_Notes { * Get the total number of notes * * @param string $type Comma separated list of note types. + * @param string $status Comma separated list of statuses. * @return int */ - public static function get_notes_count( $type = '' ) { + public static function get_notes_count( $type = '', $status = '' ) { $data_store = WC_Data_Store::load( 'admin-note' ); - return $data_store->get_notes_count( $type ); + return $data_store->get_notes_count( $type, $status ); } /** diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-reports-sync.php b/plugins/woocommerce-admin/includes/class-wc-admin-reports-sync.php index a816c2b6a5a..ec7dbe5dea3 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-reports-sync.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-sync.php @@ -148,7 +148,6 @@ class WC_Admin_Reports_Sync { ); } - /** * Schedule an action to process a single Order. * diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-notes-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-notes-data-store.php index 51a8e4af5bf..8460c91499c 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-notes-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-notes-data-store.php @@ -197,13 +197,13 @@ class WC_Admin_Notes_Data_Store extends WC_Data_Store_WP implements WC_Object_Da $actions = $wpdb->get_results( $wpdb->prepare( - "SELECT name, label, query FROM {$wpdb->prefix}wc_admin_note_actions WHERE note_id = %d", + "SELECT name, label, query, status FROM {$wpdb->prefix}wc_admin_note_actions WHERE note_id = %d", $note->get_id() ) ); if ( $actions ) { foreach ( $actions as $action ) { - $note->add_action( $action->name, $action->label, $action->query ); + $note->add_action( $action->name, $action->label, $action->query, $action->status ); } } } @@ -232,6 +232,7 @@ class WC_Admin_Notes_Data_Store extends WC_Data_Store_WP implements WC_Object_Da 'name' => $action->name, 'label' => $action->label, 'query' => $action->query, + 'status' => $action->status, ) ); } @@ -258,6 +259,49 @@ class WC_Admin_Notes_Data_Store extends WC_Data_Store_WP implements WC_Object_Da $offset = $per_page * ( $page - 1 ); + $where_clauses = $this->get_notes_where_clauses( $args ); + + $query = $wpdb->prepare( + "SELECT note_id, title, content FROM {$wpdb->prefix}wc_admin_notes WHERE 1=1{$where_clauses} ORDER BY note_id DESC LIMIT %d, %d", + $offset, + $per_page + ); // WPCS: unprepared SQL ok. + + return $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + } + + /** + * Return a count of notes. + * + * @param string $type Comma separated list of note types. + * @param string $status Comma separated list of statuses. + * @return array An array of objects containing a note id. + */ + public function get_notes_count( $type = '', $status = '' ) { + global $wpdb; + + $where_clauses = $this->get_notes_where_clauses( + array( + 'type' => $type, + 'status' => $status, + ) + ); + + if ( ! empty( $where_clauses ) ) { + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + return $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}wc_admin_notes WHERE 1=1{$where_clauses}" ); + } + + return $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}wc_admin_notes" ); + } + + /** + * Return where clauses for getting notes by status and type. For use in both the count and listing queries. + * + * @param array $args Array of args to pass. + * @return string Where clauses for the query. + */ + public function get_notes_where_clauses( $args = array() ) { $allowed_types = WC_Admin_Note::get_allowed_types(); $where_type_array = array(); if ( isset( $args['type'] ) ) { @@ -269,54 +313,32 @@ class WC_Admin_Notes_Data_Store extends WC_Data_Store_WP implements WC_Object_Da } } } - $escaped_where_types = implode( ',', $where_type_array ); - if ( empty( $escaped_where_types ) ) { - $query = $wpdb->prepare( - "SELECT note_id, title, content FROM {$wpdb->prefix}wc_admin_notes ORDER BY note_id DESC LIMIT %d, %d", - $offset, - $per_page - ); - } else { - $query = $wpdb->prepare( - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - "SELECT note_id, title, content FROM {$wpdb->prefix}wc_admin_notes WHERE type IN ($escaped_where_types) ORDER BY note_id DESC LIMIT %d, %d", - $offset, - $per_page - ); - } - - return $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - } - - /** - * Return a count of notes. - * - * @param string $type Comma separated list of note types. - * @return array An array of objects containing a note id. - */ - public function get_notes_count( $type = '' ) { - global $wpdb; - - $allowed_types = WC_Admin_Note::get_allowed_types(); - $where_type_array = array(); - if ( ! empty( $type ) ) { - $args_types = explode( ',', $type ); - foreach ( (array) $args_types as $args_type ) { - $args_type = trim( $args_type ); - if ( in_array( $args_type, $allowed_types, true ) ) { - $where_type_array[] = "'" . esc_sql( $args_type ) . "'"; + $allowed_statuses = WC_Admin_Note::get_allowed_statuses(); + $where_status_array = array(); + if ( isset( $args['status'] ) ) { + $args_statuses = explode( ',', $args['status'] ); + foreach ( (array) $args_statuses as $args_status ) { + $args_status = trim( $args_status ); + if ( in_array( $args_status, $allowed_statuses, true ) ) { + $where_status_array[] = "'" . esc_sql( $args_status ) . "'"; } } } - $escaped_where_types = implode( ',', $where_type_array ); + + $escaped_where_types = implode( ',', $where_type_array ); + $escaped_status_types = implode( ',', $where_status_array ); + $where_clauses = ''; if ( ! empty( $escaped_where_types ) ) { - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - return $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}wc_admin_notes WHERE type IN ($escaped_where_types)" ); + $where_clauses .= " AND type IN ($escaped_where_types)"; } - return $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}wc_admin_notes" ); + if ( ! empty( $escaped_status_types ) ) { + $where_clauses .= " AND status IN ($escaped_status_types)"; + } + + return $where_clauses; } /** diff --git a/plugins/woocommerce-admin/lib/client-assets.php b/plugins/woocommerce-admin/lib/client-assets.php index 7a1aff2b1bc..4625b818354 100644 --- a/plugins/woocommerce-admin/lib/client-assets.php +++ b/plugins/woocommerce-admin/lib/client-assets.php @@ -191,7 +191,7 @@ function wc_admin_print_script_settings() { 'weekdaysShort' => array_values( $wp_locale->weekday_abbrev ), ), 'currentUserData' => $current_user_data, - 'alertCount' => WC_Admin_Notes::get_notes_count( 'error,update' ), + 'alertCount' => WC_Admin_Notes::get_notes_count( 'error,update', 'unactioned' ), ); $settings = wc_admin_add_custom_settings( $settings ); diff --git a/plugins/woocommerce-admin/tests/api/admin-notes.php b/plugins/woocommerce-admin/tests/api/admin-notes.php index 316926e9794..da396631c0d 100644 --- a/plugins/woocommerce-admin/tests/api/admin-notes.php +++ b/plugins/woocommerce-admin/tests/api/admin-notes.php @@ -106,6 +106,45 @@ class WC_Tests_API_Admin_Notes extends WC_REST_Unit_Test_Case { $this->assertEquals( 401, $response->get_status() ); } + /** + * Test updating a single note. + */ + public function test_update_note() { + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint . '/1' ) ); + $note = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 'unactioned', $note['status'] ); + + $request = new WP_REST_Request( 'PUT', $this->endpoint . '/1' ); + $request->set_body_params( + array( + 'status' => 'actioned', + ) + ); + + $response = $this->server->dispatch( $request ); + $note = $response->get_data(); + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 'actioned', $note['status'] ); + } + + /** + * Test updating a single note without permission. It should fail. + */ + public function test_update_note_without_permission() { + $request = new WP_REST_Request( 'PUT', $this->endpoint . '/1' ); + $request->set_body_params( + array( + 'status' => 'actioned', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + /** * Test getting lots of notes. * @@ -139,6 +178,31 @@ class WC_Tests_API_Admin_Notes extends WC_REST_Unit_Test_Case { $this->assertEquals( $notes[0]['title'], 'PHPUNIT_TEST_NOTE_2_TITLE' ); } + + /** + * Test getting notes of a certain status. + */ + public function test_get_actioned_notes() { + wp_set_current_user( $this->user ); + + $request = new WP_REST_Request( 'GET', $this->endpoint ); + $request->set_query_params( array( 'status' => 'actioned' ) ); + $response = $this->server->dispatch( $request ); + $notes = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 1, count( $notes ) ); + $this->assertEquals( $notes[0]['title'], 'PHPUNIT_TEST_NOTE_2_TITLE' ); + + $request = new WP_REST_Request( 'GET', $this->endpoint ); + $request->set_query_params( array( 'status' => 'invalid' ) ); + $response = $this->server->dispatch( $request ); + $notes = $response->get_data(); + + // get_notes returns all results since 'status' is not one of actioned or unactioned. + $this->assertEquals( 2, count( $notes ) ); + } + /** * Test getting lots of notes without permission. It should fail. * diff --git a/plugins/woocommerce-admin/tests/framework/helpers/class-wc-helper-admin-notes.php b/plugins/woocommerce-admin/tests/framework/helpers/class-wc-helper-admin-notes.php index c1a0e8983f3..f53b9074cae 100644 --- a/plugins/woocommerce-admin/tests/framework/helpers/class-wc-helper-admin-notes.php +++ b/plugins/woocommerce-admin/tests/framework/helpers/class-wc-helper-admin-notes.php @@ -55,6 +55,7 @@ class WC_Helper_Admin_Notes { $note_2->set_icon( 'info' ); $note_2->set_name( 'PHPUNIT_TEST_NOTE_NAME' ); $note_2->set_source( 'PHPUNIT_TEST' ); + $note_2->set_status( WC_Admin_Note::E_WC_ADMIN_NOTE_ACTIONED ); // This note has no actions. $note_2->save(); diff --git a/plugins/woocommerce-admin/wc-admin.php b/plugins/woocommerce-admin/wc-admin.php index 3459e9cb8fe..2e47bd414c8 100755 --- a/plugins/woocommerce-admin/wc-admin.php +++ b/plugins/woocommerce-admin/wc-admin.php @@ -153,6 +153,7 @@ function wc_admin_plugins_loaded() { require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-notes-new-sales-record.php'; require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-notes-settings-notes.php'; require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-notes-woo-subscriptions-notes.php'; + require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-notes-historical-data.php'; // Verify we have a proper build. if ( ! wc_admin_build_file_exists() ) {