Merge pull request #27 from kristianwind/nolimit-menu-and-name
Rename to Nolimit Discounts and move the page under the shared menu
This commit is contained in:
@@ -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>';
|
||||
}
|
||||
}
|
||||
@@ -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' );
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: WooCommerce Email Discount Manager
|
||||
* Plugin Name: Nolimit Discounts
|
||||
* Plugin URI: https://gitea.nolimit.dk/kw/woo-discount-manager
|
||||
* Update URI: https://gitea.nolimit.dk/kw/woo-discount-manager
|
||||
* Description: Apply discounts and show/hide shipping methods based on the customer's email address (full address, domain or partial match).
|
||||
@@ -123,6 +123,18 @@ final class WC_Email_Discount_Manager {
|
||||
_doing_it_wrong( __FUNCTION__, 'Unserializing is not allowed.', '1.0.0' );
|
||||
}
|
||||
|
||||
/**
|
||||
* The label on the shared parent menu all five plugins register under.
|
||||
*
|
||||
* A brand name, so it is returned as a literal rather than run through
|
||||
* __(): translating it would give the product a different name in Danish.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function shared_menu_title() {
|
||||
return 'Nolimit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap once all plugins are loaded.
|
||||
*/
|
||||
@@ -137,6 +149,20 @@ final class WC_Email_Discount_Manager {
|
||||
$this->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',
|
||||
|
||||
Reference in New Issue
Block a user