Rename to Nolimit Discounts and move the page under the shared menu
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 <folder>/<file>, 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) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
23d826530b
commit
9f19688900
@@ -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' )
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
/**
|
||||
* The one admin menu the manager plugins share.
|
||||
*
|
||||
* ┌──────────────────────────────────────────────────────────────────────────┐
|
||||
* │ THIS FILE IS BYTE-IDENTICAL IN EVERY MANAGER PLUGIN. │
|
||||
* │ Do not edit it to add something for one plugin. Extend it through the │
|
||||
* │ `wpmgr_dashboard_cards` filter, which is what it is for. │
|
||||
* │ Contract version 1.0.0 — frozen. See "Why it is frozen" below. │
|
||||
* └──────────────────────────────────────────────────────────────────────────┘
|
||||
*
|
||||
* The problem it solves: five plugins from the same family, each adding a
|
||||
* top-level menu, is five entries in a sidebar that already has fifteen. So
|
||||
* they share one parent, "Manager", and each adds a submenu under it.
|
||||
*
|
||||
* Three things about how it does that are load-bearing, and none is obvious.
|
||||
*
|
||||
* **1. Every declaration is guarded.** Five plugins each `require_once` their
|
||||
* own copy of this file, in one request. `require_once` only deduplicates by
|
||||
* resolved path, and these are five different paths — so without the
|
||||
* `function_exists()` guard, activating the second plugin is
|
||||
* `Fatal error: Cannot redeclare wpmgr_ensure_menu()`. A white screen on a
|
||||
* shop, caused by nothing except owning two of these plugins. It is the same
|
||||
* shape as the update-hook collision: a fault that exists only when there is
|
||||
* more than one, which means no repository's own tests can find it.
|
||||
*
|
||||
* **2. Whichever copy loads first wins, and that order is arbitrary** — it is
|
||||
* plugin activation order, effectively alphabetical by directory. PHP cannot
|
||||
* un-declare a function, so there is no honest way for a newer copy to replace
|
||||
* an older one that has already been loaded. The only stable answer is that
|
||||
* the file never changes. Hence: **frozen**. The parent slug, the capability,
|
||||
* the position and the filter name are a contract between five repositories
|
||||
* now, and anything new arrives through the filter.
|
||||
*
|
||||
* **3. The dashboard is built from a filter, not owned by a plugin.** If the
|
||||
* plugin that happened to register the menu is deactivated, the menu is
|
||||
* re-registered by the next one on the following request and the page still
|
||||
* renders — because the page's content comes from whoever is installed, not
|
||||
* from whoever created it. Registering a callback that lives in one plugin
|
||||
* would mean deactivating that plugin takes the other four's screens with it.
|
||||
*
|
||||
* Each plugin calls `wpmgr_ensure_menu()` at the top of its own `admin_menu`
|
||||
* callback, before its own `add_submenu_page( 'wpmgr', … )`. Not on some other
|
||||
* hook: `add_submenu_page` against a parent that does not exist yet silently
|
||||
* fails, and whose `admin_menu` runs first is, again, arbitrary.
|
||||
*
|
||||
* The package below is the family, not any one plugin: this file is
|
||||
* byte-identical in five repositories, and a tag naming whichever of them it
|
||||
* was first written in reads as a mistake in the other four — which invites
|
||||
* somebody to "fix" it, in one repository, which is the exact edit the freeze
|
||||
* exists to prevent.
|
||||
*
|
||||
* @package WordPress_Manager_Family
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
if ( ! defined( 'WPMGR_MENU_VERSION' ) ) {
|
||||
define( 'WPMGR_MENU_VERSION', '1.0.0' );
|
||||
}
|
||||
|
||||
if ( ! defined( 'WPMGR_MENU_SLUG' ) ) {
|
||||
define( 'WPMGR_MENU_SLUG', 'wpmgr' );
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'wpmgr_ensure_menu' ) ) {
|
||||
|
||||
/**
|
||||
* Register the shared parent menu, unless somebody already has.
|
||||
*
|
||||
* Call this first thing in your own `admin_menu` callback.
|
||||
*/
|
||||
function wpmgr_ensure_menu() {
|
||||
global $admin_page_hooks;
|
||||
|
||||
if ( isset( $admin_page_hooks[ WPMGR_MENU_SLUG ] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* The label on the shared parent menu.
|
||||
*
|
||||
* Left untranslated here on purpose: this file is byte-identical in
|
||||
* five plugins, so it cannot carry any one of their text domains, and
|
||||
* a string with the wrong domain is a string that is silently never
|
||||
* translated. Any plugin may translate it in its own domain through
|
||||
* this filter.
|
||||
*
|
||||
* @param string $title The menu title.
|
||||
*/
|
||||
$title = apply_filters( 'wpmgr_menu_title', 'Manager' );
|
||||
|
||||
add_menu_page(
|
||||
$title,
|
||||
$title,
|
||||
'manage_options',
|
||||
WPMGR_MENU_SLUG,
|
||||
'wpmgr_render_dashboard',
|
||||
'dashicons-admin-generic',
|
||||
// Just below WooCommerce (55.x) and above Appearance (60). The
|
||||
// fractional position is not decoration: an integer collides with
|
||||
// whatever core or another plugin put there, and the loser is
|
||||
// simply dropped.
|
||||
58.9
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The shared landing page.
|
||||
*
|
||||
* Nothing here knows which plugins exist. Every card comes from the
|
||||
* filter, so the page is correct whichever of them registered the menu and
|
||||
* whichever of them have since been deactivated.
|
||||
*/
|
||||
function wpmgr_render_dashboard() {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_die( esc_html( 'You do not have permission to view this page.' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Cards on the shared dashboard, one per installed manager plugin.
|
||||
*
|
||||
* Each entry:
|
||||
* title string Plugin name, already translated by its owner.
|
||||
* status string 'ok' | 'warn' | 'fail' — what the check said.
|
||||
* headline string One short line: the number that matters.
|
||||
* lines array label => 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 '<div class="wrap wpmgr-dashboard">';
|
||||
echo '<h1>' . esc_html( apply_filters( 'wpmgr_menu_title', 'Manager' ) ) . '</h1>';
|
||||
|
||||
if ( empty( $cards ) ) {
|
||||
echo '<p>Nothing to report.</p></div>';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
echo '<style>'
|
||||
. '.wpmgr-cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px;margin-top:20px}'
|
||||
. '.wpmgr-card{background:#fff;border:1px solid #dcdcde;border-radius:8px;padding:0;overflow:hidden}'
|
||||
. '.wpmgr-card header{padding:14px 18px;color:#fff;display:flex;justify-content:space-between;align-items:center}'
|
||||
. '.wpmgr-card header h2{margin:0;font-size:14px;color:inherit}'
|
||||
. '.wpmgr-card .wpmgr-body{padding:16px 18px}'
|
||||
. '.wpmgr-headline{font-size:22px;font-weight:600;margin:0 0 10px}'
|
||||
. '.wpmgr-card dl{display:grid;grid-template-columns:1fr auto;gap:4px 12px;margin:0;font-size:13px}'
|
||||
. '.wpmgr-card dt{color:#646970;margin:0}.wpmgr-card dd{margin:0;text-align:right}'
|
||||
. '.wpmgr-state{font-size:11px;text-transform:uppercase;letter-spacing:.04em;'
|
||||
. 'background:rgba(255,255,255,.22);padding:2px 8px;border-radius:10px}'
|
||||
. '.wpmgr-card footer{padding:12px 18px;border-top:1px solid #f0f0f1}'
|
||||
. '</style>';
|
||||
|
||||
echo '<div class="wpmgr-cards">';
|
||||
|
||||
$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 '<section class="wpmgr-card">';
|
||||
printf(
|
||||
'<header style="background:%s"><h2>%s</h2><span class="wpmgr-state">%s</span></header>',
|
||||
esc_attr( $colour ),
|
||||
esc_html( $card['title'] ),
|
||||
esc_html( $states[ $status ] )
|
||||
);
|
||||
|
||||
echo '<div class="wpmgr-body">';
|
||||
|
||||
if ( ! empty( $card['headline'] ) ) {
|
||||
echo '<p class="wpmgr-headline">' . esc_html( $card['headline'] ) . '</p>';
|
||||
}
|
||||
|
||||
if ( ! empty( $card['lines'] ) && is_array( $card['lines'] ) ) {
|
||||
echo '<dl>';
|
||||
foreach ( $card['lines'] as $label => $value ) {
|
||||
echo '<dt>' . esc_html( $label ) . '</dt><dd>' . esc_html( $value ) . '</dd>';
|
||||
}
|
||||
echo '</dl>';
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
|
||||
if ( ! empty( $card['url'] ) ) {
|
||||
printf(
|
||||
'<footer><a class="button" href="%s">%s</a></footer>',
|
||||
esc_url( $card['url'] ),
|
||||
esc_html( isset( $card['action'] ) ? $card['action'] : 'Open' )
|
||||
);
|
||||
}
|
||||
|
||||
echo '</section>';
|
||||
}
|
||||
|
||||
echo '</div></div>';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user