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(
+ '',
+ 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',