diff --git a/includes/class-admin.php b/includes/class-admin.php index 7c28dd6..8b2c933 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -64,13 +64,30 @@ class WC_EDM_Admin { } /** - * Register the submenu page under WooCommerce. + * Register the page under the shared Nolimit menu. + * + * wpmgr_ensure_menu() must be called first and is a no-op when another of + * the five plugins already registered the parent — whichever booted first + * owns it, and none of them may assume it is theirs. + * + * The page slug is unchanged, so admin.php?page=… still resolves: moving a + * page between parents does not move its URL, and an existing bookmark or + * a link in an email keeps working. */ public function register_menu() { + if ( function_exists( 'wpmgr_ensure_menu' ) ) { + wpmgr_ensure_menu(); + $parent = WPMGR_MENU_SLUG; + } else { + // The shared file is missing — a half-copied upgrade. Fall back to + // the old home rather than losing the page entirely. + $parent = 'woocommerce'; + } + add_submenu_page( - 'woocommerce', - __( 'Email Discounts', 'wc-email-discount' ), - __( 'Email Discounts', 'wc-email-discount' ), + $parent, + __( 'Discounts', 'wc-email-discount' ), + __( 'Discounts', 'wc-email-discount' ), 'manage_woocommerce', self::PAGE_SLUG, array( $this, 'render_page' ) diff --git a/includes/shared-menu.php b/includes/shared-menu.php new file mode 100644 index 0000000..45dd266 --- /dev/null +++ b/includes/shared-menu.php @@ -0,0 +1,221 @@ + value, a handful at most. + * url string Where "Open" goes. + * colour string The plugin's own colour, as #rrggbb. + * + * Implementations must not measure anything here. This filter runs on + * an admin page load; a card that walks the disk or makes an HTTP + * request turns somebody else's dashboard into a slow page, and they + * will not know why. + * + * @param array $cards The cards so far. + */ + $cards = apply_filters( 'wpmgr_dashboard_cards', array() ); + + echo '
'; + echo '

' . esc_html( apply_filters( 'wpmgr_menu_title', 'Manager' ) ) . '

'; + + if ( empty( $cards ) ) { + echo '

Nothing to report.

'; + + return; + } + + echo ''; + + echo '
'; + + $states = array( + 'ok' => 'OK', + 'warn' => 'Attention', + 'fail' => 'Action needed', + ); + + foreach ( $cards as $card ) { + if ( empty( $card['title'] ) ) { + continue; + } + + $colour = isset( $card['colour'] ) && preg_match( '/^#[0-9a-f]{6}$/i', $card['colour'] ) + ? $card['colour'] + : '#50575e'; + + $status = isset( $card['status'] ) && isset( $states[ $card['status'] ] ) ? $card['status'] : 'ok'; + + echo '
'; + printf( + '

%s

%s
', + esc_attr( $colour ), + esc_html( $card['title'] ), + esc_html( $states[ $status ] ) + ); + + echo '
'; + + if ( ! empty( $card['headline'] ) ) { + echo '

' . esc_html( $card['headline'] ) . '

'; + } + + if ( ! empty( $card['lines'] ) && is_array( $card['lines'] ) ) { + echo '
'; + foreach ( $card['lines'] as $label => $value ) { + echo '
' . esc_html( $label ) . '
' . esc_html( $value ) . '
'; + } + echo '
'; + } + + echo '
'; + + if ( ! empty( $card['url'] ) ) { + printf( + '', + esc_url( $card['url'] ), + esc_html( isset( $card['action'] ) ? $card['action'] : 'Open' ) + ); + } + + echo '
'; + } + + echo '
'; + } +} diff --git a/tests/smoke.php b/tests/smoke.php index 77edebd..0ad08be 100644 --- a/tests/smoke.php +++ b/tests/smoke.php @@ -227,6 +227,29 @@ $request = new WP_REST_Request( 'GET', '/' . WC_EDM_REST_API::REST_NAMESPACE . $response = rest_get_server()->dispatch( $request ); wc_edm_is( 'and they refuse an unauthenticated caller', 401, $response->get_status() ); +/* -------------------------------------------------------------- nolimit -- */ + +// This plugin claims the shared parent menu's label. Asserted through the +// filter rather than by reading the menu, because the menu is only built in an +// admin request and this runs under WP-CLI. +wc_edm_is( + 'the shared menu is labelled Nolimit', + 'Nolimit', + apply_filters( 'wpmgr_menu_title', 'Manager' ) +); + +// includes/shared-menu.php is byte-identical in all five plugins, and which +// copy a site loads is decided by activation order. Editing it here would make +// a site with three of five updated show one label or another depending on who +// booted first — and PHP cannot un-declare a function, so the first copy wins +// for the whole request. The hash is the guard: change the file and this goes +// red rather than the behaviour going strange on somebody's shop. +wc_edm_is( + 'and the shared menu file is the family copy, unedited', + 'f4862d5c3a2d24dadddaaec90c9221d61f2923964e164fd525a800ddcec63185', + hash_file( 'sha256', WC_EDM_PLUGIN_DIR . 'includes/shared-menu.php' ) +); + /* ------------------------------------------------------------ woocommerce -- */ $compatible = \Automattic\WooCommerce\Utilities\FeaturesUtil::get_compatible_plugins_for_feature( 'custom_order_tables' ); diff --git a/woocommerce-email-discount-manager.php b/woocommerce-email-discount-manager.php index 1ef3ad7..74394aa 100644 --- a/woocommerce-email-discount-manager.php +++ b/woocommerce-email-discount-manager.php @@ -1,6 +1,6 @@ updater = new WC_EDM_Updater(); } + // Also deliberately before the WooCommerce check, and for the same + // reason as the updater above: the shared parent menu is registered by + // whichever of the five plugins boots first, and its label must not + // depend on whether THIS plugin got past a gate. + // + // Registered here rather than by editing includes/shared-menu.php: + // that file is byte-identical in all five, and which copy a site loads + // is decided by activation order. Changing it there would make a site + // with three of five updated show "Manager" or "Nolimit" depending on + // who booted first, and change its mind when a plugin is deactivated. + // The filter is idempotent, so a plugin still on the old build changes + // nothing. + add_filter( 'wpmgr_menu_title', array( $this, 'shared_menu_title' ) ); + if ( ! class_exists( 'WooCommerce' ) ) { add_action( 'admin_notices', array( $this, 'woocommerce_missing_notice' ) ); return; @@ -208,6 +234,7 @@ final class WC_Email_Discount_Manager { $files = array_merge( $files, array( + 'includes/shared-menu.php', 'includes/class-admin.php', 'includes/class-admin-groups.php', 'includes/class-admin-emails.php',