diff --git a/plugins/woocommerce-beta-tester/api/api.php b/plugins/woocommerce-beta-tester/api/api.php index a6a26c624a5..328ea175e60 100644 --- a/plugins/woocommerce-beta-tester/api/api.php +++ b/plugins/woocommerce-beta-tester/api/api.php @@ -61,3 +61,4 @@ require 'live-branches/manifest.php'; require 'live-branches/install.php'; require 'tools/set-block-template-logging-threshold.php'; require 'remote-spec-validator/class-wca-test-helper-remote-spec-validator.php'; +require 'remote-inbox-notifications/class-wca-test-helper-remote-inbox-notifications.php'; \ No newline at end of file diff --git a/plugins/woocommerce-beta-tester/api/remote-inbox-notifications/class-wc-beta-tester-remote-inbox-notifications-helper.php b/plugins/woocommerce-beta-tester/api/remote-inbox-notifications/class-wc-beta-tester-remote-inbox-notifications-helper.php new file mode 100644 index 00000000000..7635f43c332 --- /dev/null +++ b/plugins/woocommerce-beta-tester/api/remote-inbox-notifications/class-wc-beta-tester-remote-inbox-notifications-helper.php @@ -0,0 +1,140 @@ +delete( $wpdb->prefix . 'wc_admin_notes', array( 'note_id' => $id ) ); + $wpdb->delete( $wpdb->prefix . 'wc_admin_note_actions', array( 'note_id' => $id ) ); + return true; + } + + /** + * Delete all notifications and their associated actions from the database. + * + * @global wpdb $wpdb WordPress database abstraction object. + * @return array Associative array containing the count of deleted notes and actions. + */ + public static function delete_all() { + global $wpdb; + + $deleted_note_count = $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_admin_notes" ); + $deleted_action_count = $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_admin_note_actions" ); + return array( + 'deleted_note_count' => $deleted_note_count, + 'deleted_action_count' => $deleted_action_count, + ); + } + + /** + * Test a specific notification by its name and optionally run it. + * + * @param string $name The name of the notification to test. + * @param string|null $locale The locale of the notification. Defaults to the current system locale. + * @param bool $run Whether to run the notification if the test passes. Defaults to false. + * @return true|WP_Error True on success, WP_Error on failure. + */ + public static function test( $name, $locale = null, $run = false ) { + $notifications = static::get_transient(); + + if ( ! $locale ) { + $locale = get_locale(); + } + + if ( ! isset( $notifications[ $locale ][ $name ] ) ) { + return new WP_Error( + 404, + "'{$name}' was not found in the latest remote inbox notification transient. Please re-run the import command.", + ); + } + + $spec = $notifications[ $locale ][ $name ]; + $test = true; + $failed_rules = array(); + $rule_processor = new GetRuleProcessor(); + foreach ( $spec->rules as $rule ) { + if ( ! is_object( $rule ) ) { + $test = false; + break; + } + $processor = $rule_processor->get_processor( $rule->type ); + $processor_result = $processor->process( $rule, null ); + if ( ! $processor_result ) { + $test = false; + $failed_rules[] = $rule; + } + } + + if ( $test && $run ) { + $stored_state = RemoteInboxNotificationsEngine::get_stored_state(); + SpecRunner::run_spec( $spec, $stored_state ); + } + + if ( ! $test ) { + return new WP_Error( + 400, + "Test failed for '{$name}'", + array( 'failed_rules' => $failed_rules ) + ); + } + + return true; + } + + /** + * Import specifications and store them in the transient. + * + * @param array $specs An array of specifications to import. + * @return bool True on success. + */ + public static function import( $specs ) { + $stored_state = RemoteInboxNotificationsEngine::get_stored_state(); + $transient = static::get_transient(); + + foreach ( $specs as $spec ) { + SpecRunner::run_spec( $spec, $stored_state ); + if ( isset( $spec->locales ) && is_array( $spec->locales ) ) { + foreach ( $spec->locales as $locale ) { + $transient[ $locale->locale ][ $spec->slug ] = $spec; + } + } + } + + set_transient( static::get_transient_name(), $transient ); + + return true; + } +} diff --git a/plugins/woocommerce-beta-tester/api/remote-inbox-notifications/class-wca-test-helper-remote-inbox-notifications.php b/plugins/woocommerce-beta-tester/api/remote-inbox-notifications/class-wca-test-helper-remote-inbox-notifications.php new file mode 100644 index 00000000000..cdba35039b5 --- /dev/null +++ b/plugins/woocommerce-beta-tester/api/remote-inbox-notifications/class-wca-test-helper-remote-inbox-notifications.php @@ -0,0 +1,195 @@ + 'GET', + ) +); + +register_woocommerce_admin_test_helper_rest_route( + '/remote-inbox-notifications/(?P\d+)/delete', + array( WCA_Test_Helper_Remote_Inbox_Notifications::class, 'delete' ), + array( + 'methods' => 'POST', + 'args' => array( + 'id' => array( + 'description' => 'Rest API endpoint.', + 'type' => 'integer', + 'required' => true, + ), + ), + ) +); + +register_woocommerce_admin_test_helper_rest_route( + '/remote-inbox-notifications/(?P(.*)+)/test', + array( WCA_Test_Helper_Remote_Inbox_Notifications::class, 'test' ), + array( + 'methods' => 'GET', + 'args' => array( + 'name' => array( + 'description' => 'Note name.', + 'type' => 'string', + 'required' => true, + ), + ), + ) +); + +register_woocommerce_admin_test_helper_rest_route( + '/remote-inbox-notifications/delete-all', + array( WCA_Test_Helper_Remote_Inbox_Notifications::class, 'delete_all_items' ), + array( + 'methods' => 'POST', + ) +); + +register_woocommerce_admin_test_helper_rest_route( + '/remote-inbox-notifications/import', + array( WCA_Test_Helper_Remote_Inbox_Notifications::class, 'import' ), + array( + 'methods' => 'POST', + ) +); + + +/** + * Class WCA_Test_Helper_Remote_Inbox_Notifications. + */ +class WCA_Test_Helper_Remote_Inbox_Notifications { + + /** + * Delete a notification. + * + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public static function delete( $request ) { + WC_Beta_Tester_Remote_Inbox_Notifications_Helper::delete_by_id( $request->get_param( 'id' ) ); + + return new WP_REST_Response( + array( + 'success' => true, + 'message' => 'Remote inbox notification deleted.', + ), + 200 + ); + } + + /** + * Delete all notifications. + * + * @return WP_REST_Response + */ + public static function delete_all_items() { + $deleted = WC_Beta_Tester_Remote_Inbox_Notifications_Helper::delete_all(); + return new WP_REST_Response( $deleted, 200 ); + } + + /** + * Return a list of class-based notes. + * These should be excluded from the list of notes to be displayed in the inbox as we don't have control over them. + * + * @return array + */ + private static function get_notes_to_exclude() { + $vars = array( 'other_note_classes', 'note_classes_to_added_or_updated' ); + $note_names = array(); + $reflection = new ReflectionClass( '\Automattic\WooCommerce\Internal\Admin\Events' ); + foreach ( $vars as $var ) { + $property = $reflection->getProperty( $var ); + $property->setAccessible( true ); + $notes = $property->getValue(); + $note_names = array_merge( + $note_names, + array_map( + function ( $note ) { + return $note::NOTE_NAME; + }, + $notes + ) + ); + } + + return $note_names; + } + + /** + * Return all notifications. + * + * @return WP_REST_Response + */ + public static function get_items() { + global $wpdb; + $items = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}wc_admin_notes ORDER BY note_id desc", ARRAY_A ); + + $notes_added_via_classes = self::get_notes_to_exclude(); + $items = array_filter( + $items, + function ( $item ) use ( $notes_added_via_classes ) { + return ! in_array( $item['name'], $notes_added_via_classes, true ); + } + ); + + return new WP_REST_Response( array_values( $items ), 200 ); + } + + /** + * Test and run a remote inbox notification. + * + * @param WP_REST_Request $request The full request data. + * + * @return WP_REST_Response + */ + public static function test( $request ) { + $name = $request->get_param( 'name' ); + $result = WC_Beta_Tester_Remote_Inbox_Notifications_Helper::test( $name, null, true ); + + if ( $result instanceof WP_Error ) { + $message = $result->get_error_data(); + } else { + $message = $name . ': All rules passed successfully'; + } + + return new WP_REST_Response( + array( + 'success' => true === $result, + 'message' => $message, + ), + 200 + ); + } + + /** + * Import remote inbox notifications. + * + * @param WP_REST_Request $request The full request data. + * + * @return WP_REST_Response + */ + public static function import( $request ) { + // Get the JSON data from the request body. + $specs = json_decode( wp_json_encode( $request->get_json_params() ) ); + WC_Beta_Tester_Remote_Inbox_Notifications_Helper::import( $specs ); + + return new WP_REST_Response( + array( + 'success' => true, + 'message' => 'Remote inbox notifications imported.', + ), + 200 + ); + } +} diff --git a/plugins/woocommerce-beta-tester/changelog/48735-feature-remote-notiifcation-importer b/plugins/woocommerce-beta-tester/changelog/48735-feature-remote-notiifcation-importer new file mode 100644 index 00000000000..7972cb80e25 --- /dev/null +++ b/plugins/woocommerce-beta-tester/changelog/48735-feature-remote-notiifcation-importer @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Adds a new tool to the WCA Test Helper that helps import remote inbox notifications from staging or production for testing purposes \ No newline at end of file diff --git a/plugins/woocommerce-beta-tester/src/app/app.js b/plugins/woocommerce-beta-tester/src/app/app.js index 699bba2e7e3..a612bef66c6 100644 --- a/plugins/woocommerce-beta-tester/src/app/app.js +++ b/plugins/woocommerce-beta-tester/src/app/app.js @@ -14,6 +14,7 @@ import { default as Experiments } from '../experiments'; import { default as Features } from '../features'; import { default as RestAPIFilters } from '../rest-api-filters'; import RemoteSpecValidator from '../remote-spec-validator'; +import RemoteInboxNotifications from '../remote-inbox-notifications'; const tabs = applyFilters( 'woocommerce_admin_test_helper_tabs', [ { @@ -21,11 +22,6 @@ const tabs = applyFilters( 'woocommerce_admin_test_helper_tabs', [ title: 'Options', content: , }, - { - name: 'admin-notes', - title: 'Admin notes', - content: , - }, { name: 'tools', title: 'Tools', @@ -47,9 +43,9 @@ const tabs = applyFilters( 'woocommerce_admin_test_helper_tabs', [ content: , }, { - name: 'remote-spec-validator', - title: 'Remote Spec Rule Validator', - content: , + name: 'remote-inbox-notifications', + title: 'Remote Inbox Notifications', + content: , }, ] ); diff --git a/plugins/woocommerce-beta-tester/src/index.scss b/plugins/woocommerce-beta-tester/src/index.scss index 7a6dc32fec1..32a3604fae3 100644 --- a/plugins/woocommerce-beta-tester/src/index.scss +++ b/plugins/woocommerce-beta-tester/src/index.scss @@ -183,4 +183,44 @@ form.rest-api-filter-new-form { .btn-validate { float: right; } +} + +#wc-admin-test-helper-remote-inbox-notifications { + pre { + margin: 0; + padding: 0; + } + .action-btns { + justify-content: end; + display: flex; + gap: 5px; + margin-top: 10px; + margin-bottom: 10px; + .btn-danger { + margin-right: auto; + } + } + .notification-actions { + display: flex; + gap: 5px; + justify-content: end; + } + .components-notice { + margin: 0px 0px 10px 0px; + } + + .button.staging { + background-color: #de9816; + color: #fff; + border-color: #de9816; + } + + .column-status { + display: flex; + svg { + width: 18px; + height: 18px; + margin-left: 2px; + } + } } \ No newline at end of file diff --git a/plugins/woocommerce-beta-tester/src/remote-inbox-notifications/data/action-types.js b/plugins/woocommerce-beta-tester/src/remote-inbox-notifications/data/action-types.js new file mode 100644 index 00000000000..d6af44d8a86 --- /dev/null +++ b/plugins/woocommerce-beta-tester/src/remote-inbox-notifications/data/action-types.js @@ -0,0 +1,8 @@ +const TYPES = { + SET_NOTIFICATIONS: 'SET_NOTIFICATIONS', + SET_IS_LOADING: 'SET_IS_LOADING', + DELETE_NOTIFICATION: 'DELETE_NOTIFICATION', + SET_NOTICE: 'SET_NOTICE', +}; + +export default TYPES; diff --git a/plugins/woocommerce-beta-tester/src/remote-inbox-notifications/data/actions.js b/plugins/woocommerce-beta-tester/src/remote-inbox-notifications/data/actions.js new file mode 100644 index 00000000000..31fe6d2d76f --- /dev/null +++ b/plugins/woocommerce-beta-tester/src/remote-inbox-notifications/data/actions.js @@ -0,0 +1,140 @@ +/** + * External dependencies + */ +import { apiFetch } from '@wordpress/data-controls'; +import { controls } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import TYPES from './action-types'; +import { API_NAMESPACE, STORE_KEY } from './constants'; + +/** + * Initialize the state + * + * @param {Array} notifications + */ +export function setNotifications( notifications ) { + return { + type: TYPES.SET_NOTIFICATIONS, + notifications, + }; +} + +export function setLoadingState( isLoading ) { + return { + type: TYPES.SET_IS_LOADING, + isLoading, + }; +} + +export function setNotice( notice ) { + return { + type: TYPES.SET_NOTICE, + notice, + }; +} + +export function* importNotifications( notifications ) { + try { + yield apiFetch( { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + path: `${ API_NAMESPACE }/remote-inbox-notifications/import`, + body: JSON.stringify( notifications ), + } ); + + yield controls.dispatch( + STORE_KEY, + 'invalidateResolutionForStoreSelector', + 'getNotifications' + ); + } catch ( error ) { + setNotice( { + message: 'Failed to import notifications', + status: 'error', + } ); + } +} + +export function* deleteNotification( id ) { + try { + yield apiFetch( { + method: 'POST', + path: `${ API_NAMESPACE }/remote-inbox-notifications/${ id }/delete`, + } ); + + yield { + type: TYPES.DELETE_NOTIFICATION, + id, + }; + setNotice( { + message: 'Notifications deleted successfully.', + status: 'success', + } ); + } catch { + setNotice( { + message: 'Failed to delete notification', + status: 'error', + } ); + } +} + +export function* testNotification( name ) { + try { + const response = yield apiFetch( { + method: 'GET', + path: `${ API_NAMESPACE }/remote-inbox-notifications/${ name }/test`, + } ); + + if ( response.success ) { + yield controls.dispatch( + STORE_KEY, + 'invalidateResolutionForStoreSelector', + 'getNotifications' + ); + } + + yield setNotice( { + message: + typeof response.message === 'string' + ? response.message + : 'The following rules have failed.\n\n' + + JSON.stringify( response.message, null, 2 ), + status: response.success ? 'success' : 'error', + } ); + } catch ( e ) { + setNotice( { + message: 'Failed to test notification', + status: 'error', + } ); + } +} + +export function* deleteAllNotifications() { + try { + yield apiFetch( { + method: 'POST', + path: `${ API_NAMESPACE }/remote-inbox-notifications/delete-all`, + } ); + + yield controls.dispatch( + STORE_KEY, + 'invalidateResolutionForStoreSelector', + 'getNotifications' + ); + + setNotice( { + message: 'All notifications deleted successfully.', + status: 'success', + } ); + } catch { + setNotice( { + message: 'Failed to delete all notifications', + status: 'error', + } ); + } +} diff --git a/plugins/woocommerce-beta-tester/src/remote-inbox-notifications/data/constants.js b/plugins/woocommerce-beta-tester/src/remote-inbox-notifications/data/constants.js new file mode 100644 index 00000000000..1d421ef6cf3 --- /dev/null +++ b/plugins/woocommerce-beta-tester/src/remote-inbox-notifications/data/constants.js @@ -0,0 +1,2 @@ +export const STORE_KEY = 'wc-admin-helper/remote-inbox-notifications'; +export const API_NAMESPACE = '/wc-admin-test-helper'; diff --git a/plugins/woocommerce-beta-tester/src/remote-inbox-notifications/data/index.js b/plugins/woocommerce-beta-tester/src/remote-inbox-notifications/data/index.js new file mode 100644 index 00000000000..e476479ea88 --- /dev/null +++ b/plugins/woocommerce-beta-tester/src/remote-inbox-notifications/data/index.js @@ -0,0 +1,22 @@ +/** + * External dependencies + */ +import { registerStore } from '@wordpress/data'; +import { controls } from '@wordpress/data-controls'; + +/** + * Internal dependencies + */ +import * as actions from './actions'; +import * as resolvers from './resolvers'; +import * as selectors from './selectors'; +import reducer from './reducer'; +import { STORE_KEY } from './constants'; + +export default registerStore( STORE_KEY, { + actions, + selectors, + resolvers, + controls, + reducer, +} ); diff --git a/plugins/woocommerce-beta-tester/src/remote-inbox-notifications/data/reducer.js b/plugins/woocommerce-beta-tester/src/remote-inbox-notifications/data/reducer.js new file mode 100644 index 00000000000..6fbe43eca92 --- /dev/null +++ b/plugins/woocommerce-beta-tester/src/remote-inbox-notifications/data/reducer.js @@ -0,0 +1,48 @@ +/** + * Internal dependencies + */ +import TYPES from './action-types'; + +const DEFAULT_STATE = { + notifications: [], + isLoading: true, + notice: { + status: 'success', + message: '', + }, +}; + +const reducer = ( state = DEFAULT_STATE, action ) => { + switch ( action.type ) { + case TYPES.SET_IS_LOADING: + return { + ...state, + isLoading: action.isLoading, + }; + case TYPES.SET_NOTIFICATIONS: + return { + ...state, + notifications: action.notifications, + isLoading: false, + }; + case TYPES.SET_NOTICE: + return { + ...state, + notice: { + ...state.notice, + ...action.notice, + }, + }; + case TYPES.DELETE_NOTIFICATION: + return { + ...state, + notifications: state.notifications.filter( + ( item ) => item.note_id !== action.id + ), + }; + default: + return state; + } +}; + +export default reducer; diff --git a/plugins/woocommerce-beta-tester/src/remote-inbox-notifications/data/resolvers.js b/plugins/woocommerce-beta-tester/src/remote-inbox-notifications/data/resolvers.js new file mode 100644 index 00000000000..e8eb83eea50 --- /dev/null +++ b/plugins/woocommerce-beta-tester/src/remote-inbox-notifications/data/resolvers.js @@ -0,0 +1,23 @@ +/** + * External dependencies + */ +import { apiFetch } from '@wordpress/data-controls'; + +/** + * Internal dependencies + */ +import { setLoadingState, setNotifications } from './actions'; + +export function* getNotifications() { + yield setLoadingState( true ); + + try { + const response = yield apiFetch( { + path: 'wc-admin-test-helper/remote-inbox-notifications', + } ); + + yield setNotifications( response ); + } catch ( error ) { + throw new Error(); + } +} diff --git a/plugins/woocommerce-beta-tester/src/remote-inbox-notifications/data/selectors.js b/plugins/woocommerce-beta-tester/src/remote-inbox-notifications/data/selectors.js new file mode 100644 index 00000000000..558fa17b8b4 --- /dev/null +++ b/plugins/woocommerce-beta-tester/src/remote-inbox-notifications/data/selectors.js @@ -0,0 +1,11 @@ +export function getNotifications( state ) { + return state.notifications; +} + +export function isLoading( state ) { + return state.isLoading; +} + +export function getNotice( state ) { + return state.notice; +} diff --git a/plugins/woocommerce-beta-tester/src/remote-inbox-notifications/index.js b/plugins/woocommerce-beta-tester/src/remote-inbox-notifications/index.js new file mode 100644 index 00000000000..edd7529f55d --- /dev/null +++ b/plugins/woocommerce-beta-tester/src/remote-inbox-notifications/index.js @@ -0,0 +1,235 @@ +/** + * External dependencies + */ +import { withDispatch, withSelect } from '@wordpress/data'; +import { compose } from '@wordpress/compose'; +import { Notice } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { STORE_KEY } from './data/constants'; +import './data'; + +function RemoteInboxNotifications( { + notifications, + deleteNotification, + importNotifications, + deleteAllNotifications, + testNotification, + isLoading, + notice, + setNotice, +} ) { + const importFromUrl = async ( _url ) => { + const preDefinedUrls = { + staging: + 'https://staging.woocommerce.com/wp-json/wccom/inbox-notifications/2.0/notifications.json', + production: + 'https://woocommerce.com/wp-json/wccom/inbox-notifications/2.0/notifications.json', + }; + + const url = preDefinedUrls[ _url ] || _url; + + try { + const response = await fetch( url ); + const data = await response.json(); + importNotifications( data ); + setNotice( { + message: 'Notifications imported successfully.', + status: 'success', + } ); + } catch ( error ) { + if ( _url === 'staging' ) { + const messages = { + staging: + 'Failed to fetch notifications. Please make sure you are connected to Automattic proxy.', + production: error.message, + }; + setNotice( { + message: messages[ _url ], + status: 'error', + } ); + } + } + }; + const renderLoading = () => { + return ( + + + Loading... + + + ); + }; + + const renderTableData = () => { + if ( notifications.length === 0 ) { + return ( + + + No Notifications Found + + + ); + } + + return notifications.map( ( notification, index ) => { + return ( + + { notification.note_id } + { notification.name } + { notification.type } + { notification.status } + + + + + + ); + } ); + }; + + return ( + <> +
+
+ { + if ( + confirm( + 'Are you sure you want to delete all notifications?' + ) + ) { + deleteAllNotifications(); + } + } } + /> + { + const url = prompt( + 'Enter the URL to import notifications from' + ); + if ( url ) { + importFromUrl( url ); + } + } } + /> + { + if ( + confirm( + 'Are you sure you want to import notifications from staging? Existing notifications will be overwritten.' + ) + ) { + importFromUrl( 'staging' ); + } + } } + /> + { + if ( + confirm( + 'Are you sure you want to import notifications from production? Existing notifications will be overwritten.' + ) + ) { + importFromUrl( 'production' ); + } + } } + /> +
+ { notice.message.length > 0 && ( + { + setNotice( { message: '' } ); + } } + > +
{ notice.message }
+
+ ) } + + + + + + + + + + + + { isLoading ? renderLoading() : renderTableData() } + +
I.DName + Type + + Status +
+
+ + ); +} + +export default compose( + withSelect( ( select ) => { + const { getNotifications, isLoading, getNotice } = select( STORE_KEY ); + const notifications = getNotifications(); + const notice = getNotice(); + + return { + notice, + notifications, + isLoading: isLoading(), + }; + } ), + withDispatch( ( dispatch ) => { + const { + deleteNotification, + importNotifications, + deleteAllNotifications, + testNotification, + setNotice, + } = dispatch( STORE_KEY ); + + return { + testNotification, + deleteAllNotifications, + setNotice, + deleteNotification, + importNotifications, + }; + } ) +)( RemoteInboxNotifications );