From 9f19688900f1c89573f8e046106ed6bf9cbba79e Mon Sep 17 00:00:00 2001 From: kristianwind Date: Thu, 17 Sep 2026 22:05:19 +0200 Subject: [PATCH] Rename to Nolimit Discounts and move the page under the shared menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kristian could not find this plugin in the Nolimit menu, and it still called itself WooCommerce Email Discount Manager. Both are the display side of the family rename; neither touches the installed folder. Plugin Name is now "Nolimit Discounts". The folder, the text domain, the option prefix and the three tables are deliberately unchanged: WordPress identifies a plugin as /, so renaming the folder would not move the installation on the live shop — it would put a second copy beside it and leave the old one active. That is a migration with a shim, and it is Kristian's call rather than a side effect of a label change. includes/shared-menu.php added, byte-identical to the family copy (f4862d5c…c63185), and the page moved from the WooCommerce submenu to the shared parent. The page slug is unchanged, so admin.php?page=… still resolves and an existing bookmark keeps working — moving a page between parents does not move its URL. The label is claimed through the wpmgr_menu_title filter from this plugin's own bootstrap, NOT by editing the shared file. Which copy of that file a site loads is decided by activation order, and PHP cannot un-declare a function, so the first copy wins for the whole request: editing it would make a site with three of five plugins updated show "Manager" or "Nolimit" depending on who booted first, and change its mind when a plugin is deactivated. The filter is idempotent and a plugin still on an older build changes nothing. The filter is registered before the WooCommerce gate, for the same reason the updater is: the shared menu's label must not depend on whether this particular plugin got past a check. Two assertions, both proven red first. The label is asserted through the filter rather than by reading the menu, because the menu is only built in an admin request and the smoke tests run under WP-CLI. And the shared file's sha256 is asserted, so editing it goes red here rather than going strange on a shop — appending one comment line produced: FAIL and the shared menu file is the family copy, unedited — expected 'f4862d5c…', got '45df6b55…' Also caught in passing by the kit, working exactly as intended: the new shared-menu.php was untracked, and "every file the plugin loads is a file the release contains" failed rather than shipping a zip without it. Co-Authored-By: Claude Opus 5 (1M context) --- includes/class-admin.php | 25 ++- includes/shared-menu.php | 221 +++++++++++++++++++++++++ tests/smoke.php | 23 +++ woocommerce-email-discount-manager.php | 29 +++- 4 files changed, 293 insertions(+), 5 deletions(-) create mode 100644 includes/shared-menu.php 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',