Changeset 3422666
- Timestamp:
- 12/18/2025 09:26:44 AM (3 months ago)
- Location:
- folders-4-gravity
- Files:
-
- 2 added
- 22 edited
- 1 copied
-
tags/1.0.6 (copied) (copied from folders-4-gravity/trunk)
-
tags/1.0.6/folders-4-gravity.php (modified) (3 diffs)
-
tags/1.0.6/includes/class-gravity-ops-form-folders.php (modified) (2 diffs)
-
tags/1.0.6/readme.txt (modified) (3 diffs)
-
tags/1.0.6/vendor-prefixed/autoload.php (modified) (1 diff)
-
tags/1.0.6/vendor-prefixed/composer/autoload_real.php (modified) (2 diffs)
-
tags/1.0.6/vendor-prefixed/composer/autoload_static.php (modified) (2 diffs)
-
tags/1.0.6/vendor-prefixed/composer/installed.json (modified) (2 diffs)
-
tags/1.0.6/vendor-prefixed/composer/installed.php (modified) (2 diffs)
-
tags/1.0.6/vendor-prefixed/gravityops/core/src/Admin/AdminShell.php (modified) (17 diffs)
-
tags/1.0.6/vendor-prefixed/gravityops/core/src/Admin/SuiteMenu.php (modified) (12 diffs)
-
tags/1.0.6/vendor-prefixed/gravityops/core/src/Admin/functions.php (added)
-
tags/1.0.6/vendor-prefixed/gravityops/core/src/SuiteRegistry.php (modified) (6 diffs)
-
trunk/folders-4-gravity.php (modified) (3 diffs)
-
trunk/includes/class-gravity-ops-form-folders.php (modified) (2 diffs)
-
trunk/readme.txt (modified) (3 diffs)
-
trunk/vendor-prefixed/autoload.php (modified) (1 diff)
-
trunk/vendor-prefixed/composer/autoload_real.php (modified) (2 diffs)
-
trunk/vendor-prefixed/composer/autoload_static.php (modified) (2 diffs)
-
trunk/vendor-prefixed/composer/installed.json (modified) (2 diffs)
-
trunk/vendor-prefixed/composer/installed.php (modified) (2 diffs)
-
trunk/vendor-prefixed/gravityops/core/src/Admin/AdminShell.php (modified) (17 diffs)
-
trunk/vendor-prefixed/gravityops/core/src/Admin/SuiteMenu.php (modified) (12 diffs)
-
trunk/vendor-prefixed/gravityops/core/src/Admin/functions.php (added)
-
trunk/vendor-prefixed/gravityops/core/src/SuiteRegistry.php (modified) (6 diffs)
Legend:
- Unmodified
- Added
- Removed
-
folders-4-gravity/tags/1.0.6/folders-4-gravity.php
r3420844 r3422666 5 5 * Author URI: https://brightleafdigital.io/ 6 6 * Description: Organize your Gravity Forms and Gravity Views by folders. 7 * Version: 1.0. 57 * Version: 1.0.6 8 8 * Author: BrightLeaf Digital 9 9 * License: GPL-2.0+ 10 10 * Requires PHP: 8.0 11 11 */ 12 13 use F4G\GravityOps\Core\Admin\AdminShell; 12 14 13 15 if ( ! defined( 'ABSPATH' ) ) { … … 16 18 17 19 require_once __DIR__ . '/vendor-prefixed/autoload.php'; 20 21 require_once __DIR__ . '/vendor/autoload.php'; 22 23 // Instantiate this plugin's copy of the AdminShell early so provider negotiation can happen on plugins_loaded. 24 add_action( 25 'plugins_loaded', 26 function () { 27 AdminShell::instance(); 28 }, 29 1 30 ); 18 31 19 32 … … 28 41 return; 29 42 } 43 44 // Ensure GravityOps shared assets resolve when library is vendor-installed in this plugin. 45 add_filter( 46 'gravityops_assets_base_url', 47 function ( $url ) { 48 return $url ?: plugins_url( 'vendor-prefixed/gravityops/core/assets/', __FILE__ ); 49 } 50 ); 30 51 31 52 add_action( -
folders-4-gravity/tags/1.0.6/includes/class-gravity-ops-form-folders.php
r3420844 r3422666 6 6 use F4G\GravityOps\Core\Admin\AdminShell; 7 7 use F4G\GravityOps\Core\Utils\AssetHelper as Assets; 8 use function F4G\GravityOps\Core\Admin\gravityops_shell; 8 9 9 10 if ( ! defined( 'ABSPATH' ) ) { … … 164 165 // Register the GravityOps AdminShell page for the free Folders plugin. 165 166 // Tabs: Overview (render), Help (render), Affiliation (external link) 166 AdminShell::instance()->register_plugin_page(167 gravityops_shell()->register_plugin_page( 167 168 'folders-4-gravity', 168 169 [ -
folders-4-gravity/tags/1.0.6/readme.txt
r3420844 r3422666 4 4 Requires at least: 6.5 5 5 Tested up to: 6.9 6 Stable tag: 1.0. 56 Stable tag: 1.0.6 7 7 Requires PHP: 8.0 8 8 License: GPLv2 … … 155 155 == Changelog == 156 156 157 ### 1.0.6 158 - Fixed a bug with new admin menu 159 157 160 ### 1.0.5 158 - =Updated plugin menu161 - Updated plugin menu 159 162 160 163 ### 1.0.4 … … 169 172 - Split Forms and Views into separate dashboard widgets. 170 173 171 ### 1.0.1172 - Added initial dashboard widgets.173 174 174 175 175 == Upgrade Notice == -
folders-4-gravity/tags/1.0.6/vendor-prefixed/autoload.php
r3420844 r3422666 20 20 require_once __DIR__ . '/composer/autoload_real.php'; 21 21 22 return ComposerAutoloaderInit c64224000443d31aa951e25675df6b88::getLoader();22 return ComposerAutoloaderInit4d4b64ecb88cd03a3424690392b1e3bb::getLoader(); -
folders-4-gravity/tags/1.0.6/vendor-prefixed/composer/autoload_real.php
r3420844 r3422666 3 3 // autoload_real.php @generated by Composer 4 4 5 class ComposerAutoloaderInit c64224000443d31aa951e25675df6b885 class ComposerAutoloaderInit4d4b64ecb88cd03a3424690392b1e3bb 6 6 { 7 7 private static $loader; … … 25 25 require __DIR__ . '/platform_check.php'; 26 26 27 spl_autoload_register(array('ComposerAutoloaderInit c64224000443d31aa951e25675df6b88', 'loadClassLoader'), true, true);27 spl_autoload_register(array('ComposerAutoloaderInit4d4b64ecb88cd03a3424690392b1e3bb', 'loadClassLoader'), true, true); 28 28 self::$loader = $loader = new \F4G\Composer\Autoload\ClassLoader(\dirname(__DIR__)); 29 spl_autoload_unregister(array('ComposerAutoloaderInit c64224000443d31aa951e25675df6b88', 'loadClassLoader'));29 spl_autoload_unregister(array('ComposerAutoloaderInit4d4b64ecb88cd03a3424690392b1e3bb', 'loadClassLoader')); 30 30 31 31 require __DIR__ . '/autoload_static.php'; 32 call_user_func(\F4G\Composer\Autoload\ComposerStaticInit c64224000443d31aa951e25675df6b88::getInitializer($loader));32 call_user_func(\F4G\Composer\Autoload\ComposerStaticInit4d4b64ecb88cd03a3424690392b1e3bb::getInitializer($loader)); 33 33 34 34 $loader->setClassMapAuthoritative(true); -
folders-4-gravity/tags/1.0.6/vendor-prefixed/composer/autoload_static.php
r3420844 r3422666 5 5 namespace F4G\Composer\Autoload; 6 6 7 class ComposerStaticInit c64224000443d31aa951e25675df6b887 class ComposerStaticInit4d4b64ecb88cd03a3424690392b1e3bb 8 8 { 9 9 public static $prefixLengthsPsr4 = array ( … … 36 36 { 37 37 return \Closure::bind(function () use ($loader) { 38 $loader->prefixLengthsPsr4 = ComposerStaticInit c64224000443d31aa951e25675df6b88::$prefixLengthsPsr4;39 $loader->prefixDirsPsr4 = ComposerStaticInit c64224000443d31aa951e25675df6b88::$prefixDirsPsr4;40 $loader->classMap = ComposerStaticInit c64224000443d31aa951e25675df6b88::$classMap;38 $loader->prefixLengthsPsr4 = ComposerStaticInit4d4b64ecb88cd03a3424690392b1e3bb::$prefixLengthsPsr4; 39 $loader->prefixDirsPsr4 = ComposerStaticInit4d4b64ecb88cd03a3424690392b1e3bb::$prefixDirsPsr4; 40 $loader->classMap = ComposerStaticInit4d4b64ecb88cd03a3424690392b1e3bb::$classMap; 41 41 42 42 }, null, ClassLoader::class); -
folders-4-gravity/tags/1.0.6/vendor-prefixed/composer/installed.json
r3420844 r3422666 3 3 { 4 4 "name": "gravityops/core", 5 "version": "1.0. 18",6 "version_normalized": "1.0. 18.0",5 "version": "1.0.21", 6 "version_normalized": "1.0.21.0", 7 7 "source": { 8 8 "type": "git", 9 9 "url": "git@github.com:Eitan-brightleaf/gravityops.git", 10 "reference": " a74352489b658ed6a7f1c588abb2e18aae065a68"10 "reference": "5d859a7cca5cf8c1e469c80a88e755fb1be7c522" 11 11 }, 12 12 "dist": { 13 13 "type": "zip", 14 "url": "https://api.github.com/repos/Eitan-brightleaf/gravityops/zipball/ a74352489b658ed6a7f1c588abb2e18aae065a68",15 "reference": " a74352489b658ed6a7f1c588abb2e18aae065a68",14 "url": "https://api.github.com/repos/Eitan-brightleaf/gravityops/zipball/5d859a7cca5cf8c1e469c80a88e755fb1be7c522", 15 "reference": "5d859a7cca5cf8c1e469c80a88e755fb1be7c522", 16 16 "shasum": "" 17 17 }, … … 19 19 "php": ">=7.4" 20 20 }, 21 "time": "2025-12-1 6T09:58:09+00:00",21 "time": "2025-12-18T07:45:02+00:00", 22 22 "type": "library", 23 23 "installation-source": "source", -
folders-4-gravity/tags/1.0.6/vendor-prefixed/composer/installed.php
r3420844 r3422666 5 5 'pretty_version' => 'dev-main', 6 6 'version' => 'dev-main', 7 'reference' => ' edc84c3f8028b3fc71f3205575dd4457cad99bf7',7 'reference' => '58837dca80970d5fde4a728d8d9066cba6ce26a7', 8 8 'type' => 'library', 9 9 'install_path' => __DIR__ . '/../', … … 17 17 'gravityops/core' => 18 18 array ( 19 'pretty_version' => '1.0. 18',20 'version' => '1.0. 18.0',21 'reference' => ' a74352489b658ed6a7f1c588abb2e18aae065a68',19 'pretty_version' => '1.0.21', 20 'version' => '1.0.21.0', 21 'reference' => '5d859a7cca5cf8c1e469c80a88e755fb1be7c522', 22 22 'type' => 'library', 23 23 'install_path' => __DIR__ . '/../gravityops/core', -
folders-4-gravity/tags/1.0.6/vendor-prefixed/gravityops/core/src/Admin/AdminShell.php
r3420844 r3422666 16 16 17 17 /** 18 * Library/core version used for provider negotiation. 19 * The highest version across loaded copies should be selected as provider. 20 */ 21 public const CORE_VERSION = '1.0.21'; 22 23 /** 18 24 * Holds the singleton instance. 19 25 * … … 21 27 */ 22 28 private static ?AdminShell $instance = null; 29 30 /** 31 * Tracks whether the chosen provider has already booted (registered hooks). 32 * Prevents multiple copies from attaching side‑effect hooks. 33 * 34 * @var bool 35 */ 36 private static bool $did_boot = false; 23 37 24 38 /** … … 35 49 * Get singleton instance. 36 50 */ 37 public static function instance(): AdminShell { 38 // First, try to get a shared instance provided by any loaded copy via filter. 51 public static function instance() { 52 // New: negotiate best provider by highest version (cross‑namespace). 53 $best = apply_filters( 'gravityops_shell_provider', null ); 54 if ( is_array( $best ) && isset( $best['shell'] ) ) { 55 return $best['shell']; 56 } 57 58 // Legacy: try a shared instance provided by any copy. 39 59 $shared = apply_filters( 'gravityops_shell_instance', null ); 40 60 if ( $shared instanceof self ) { 41 61 return $shared; 42 62 } 63 43 64 if ( null === self::$instance ) { 44 65 self::$instance = new self(); … … 53 74 */ 54 75 private function __construct() { 55 // Cross-namespace shared instance resolver: first provider wins. 56 $existing = apply_filters( 'gravityops_shell_instance', null ); 57 if ( $existing && $existing !== $this ) { 58 // Another copy has already provided an instance; avoid duplicate hooks. 59 return; 60 } 61 62 // Expose this instance for other namespaces/copies to reuse. 76 // Expose this instance for other namespaces/copies to reuse (legacy "first copy wins"). 63 77 add_filter( 64 78 'gravityops_shell_instance', … … 69 83 ); 70 84 71 // Defer adding menus until after all plugins have registered. 72 add_action( 'admin_menu', [ $this, 'register_menus' ], 99 ); 73 // Do NOT hide submenus by default; users reported accessibility issues. 74 // If needed in the future, this can be reintroduced behind a filter. 75 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); 85 // Versioned provider negotiation: highest version wins. 86 add_filter( 87 'gravityops_shell_provider', 88 function ( $best ) { 89 $mine = [ 90 'version' => self::CORE_VERSION, 91 'shell' => $this, 92 ]; 93 if ( ! is_array( $best ) || empty( $best['version'] ) ) { 94 return $mine; 95 } 96 return version_compare( (string) $mine['version'], (string) $best['version'], '>' ) ? $mine : $best; 97 }, 98 1 99 ); 100 101 // Defer side‑effect hooks until we negotiate the best provider. 102 // Each copy registers this, but only the selected provider will actually boot. 103 // If instantiated after plugins_loaded has already fired, boot immediately. 104 if ( function_exists( 'did_action' ) && did_action( 'plugins_loaded' ) ) { 105 $this->maybe_boot(); 106 } else { 107 add_action( 'plugins_loaded', [ $this, 'maybe_boot' ], 20 ); 108 } 76 109 } 77 110 … … 84 117 * @return bool True if the page exists. 85 118 */ 86 public static function has_page( string $slug ): bool{119 public static function has_page( $slug ) { 87 120 $provider = apply_filters( 'gravityops_shell_instance', null ); 88 121 if ( $provider instanceof self ) { … … 112 145 * @return array<string,array<string,string>> Tabs config array suitable to merge with register_plugin_page() tabs. 113 146 */ 114 public static function freemius_tabs( string $plugin_slug, array $labels = [] ): array{147 public static function freemius_tabs( $plugin_slug, $labels = [] ) { 115 148 $default_labels = [ 116 149 'account' => 'Account', … … 164 197 * @return void 165 198 */ 166 public static function render_feeds_list( array $feeds_and_forms, string $gf_subview_slug, string $plugin_short_title, string $toggle_action ): void{199 public static function render_feeds_list( $feeds_and_forms, $gf_subview_slug, $plugin_short_title, $toggle_action ) { 167 200 echo '<div class="gops-card">'; 168 201 echo '<h2 class="gops-title" style="margin:0 0 10px;">' . esc_html( $plugin_short_title ) . ' Feeds</h2>'; … … 224 257 * @return void 225 258 */ 226 public static function process_feed_toggle( string $action_prefix, string$return_url ) {259 public static function process_feed_toggle( $action_prefix, $return_url ) { 227 260 if ( empty( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) || 'POST' !== ( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ?? '' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized 228 261 wp_safe_redirect( admin_url( $return_url ) ); … … 262 295 * @return void 263 296 */ 264 public static function render_help_tab( array $links ): void{297 public static function render_help_tab( $links ) { 265 298 echo '<div class="gops-card">'; 266 299 echo '<h2 class="gops-title" style="margin:0 0 8px;">Help</h2>'; … … 282 315 * @param array $args Page configuration. 283 316 */ 284 public function register_plugin_page( string $slug, array $args ): void { 285 // If another copy is the active provider, forward registration to it. 317 public function register_plugin_page( $slug, $args ) { 318 // If another copy is the active provider (new negotiation), forward registration to it first. 319 $best = apply_filters( 'gravityops_shell_provider', null ); 320 if ( is_array( $best ) && isset( $best['shell'] ) && $best['shell'] !== $this && is_object( $best['shell'] ) && method_exists( $best['shell'], 'register_plugin_page' ) ) { 321 $best['shell']->register_plugin_page( $slug, $args ); 322 return; 323 } 324 // Legacy shared-instance forwarding as a fallback. 286 325 $provider = apply_filters( 'gravityops_shell_instance', null ); 287 326 if ( $provider && $provider !== $this && is_object( $provider ) && method_exists( $provider, 'register_plugin_page' ) ) { 288 // Call method on the provider (may be a different class/namespace).289 327 $provider->register_plugin_page( $slug, $args ); 290 328 return; … … 304 342 * Add the submenu pages under the shared GravityOps parent. 305 343 */ 306 public function register_menus() : void{344 public function register_menus() { 307 345 if ( class_exists( __NAMESPACE__ . '\\SuiteMenu' ) ) { 308 346 SuiteMenu::ensure_parent_menu(); … … 342 380 * @param string $hook Current admin page hook. 343 381 */ 344 public function enqueue_assets( $hook ) : void{382 public function enqueue_assets( $hook ) { 345 383 $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended 346 384 … … 375 413 376 414 /** 415 * Boot the chosen provider (register side‑effect hooks). Called once. 416 * 417 * @return void 418 */ 419 private function boot() { 420 if ( self::$did_boot ) { 421 return; 422 } 423 // Defer adding menus until after all plugins have registered. 424 add_action( 'admin_menu', [ $this, 'register_menus' ], 99 ); 425 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); 426 self::$did_boot = true; 427 } 428 429 /** 430 * On plugins_loaded, negotiate the best provider and boot only that one. 431 * Each copy registers this callback, but only the selected provider acts. 432 * 433 * @return void 434 */ 435 public function maybe_boot() { 436 if ( self::$did_boot ) { 437 return; 438 } 439 $best = apply_filters( 'gravityops_shell_provider', null ); 440 if ( is_array( $best ) && isset( $best['shell'] ) ) { 441 if ( $best['shell'] === $this ) { 442 $this->boot(); 443 } 444 return; 445 } 446 // If no provider negotiation is available, fall back to local boot (legacy environments). 447 $shared = apply_filters( 'gravityops_shell_instance', null ); 448 if ( $shared && $shared !== $this ) { 449 // Another copy is acting as shared instance; let it boot on its own call. 450 return; 451 } 452 $this->boot(); 453 } 454 455 /** 377 456 * Render the complete page including header, pills, and tab content. 378 457 * … … 380 459 * @param array $args Page config. 381 460 */ 382 public function render_page( string $slug, array $args ): void{461 public function render_page( $slug, $args ) { 383 462 if ( ! current_user_can( $args['capability'] ?? 'manage_options' ) ) { 384 463 return; … … 435 514 * @return void 436 515 */ 437 private function render_header_only( array $args ): void{516 private function render_header_only( $args ) { 438 517 ?> 439 518 <header class="gops-header"> … … 471 550 * @return array{css:string,js:string} 472 551 */ 473 public static function resolve_assets_urls() : array{552 public static function resolve_assets_urls() { 474 553 $base_url = apply_filters( 'gravityops_assets_base_url', '' ); 475 554 $css = ''; -
folders-4-gravity/tags/1.0.6/vendor-prefixed/gravityops/core/src/Admin/SuiteMenu.php
r3420844 r3422666 30 30 */ 31 31 private static $did_notice_hook = false; 32 33 /** 34 * Track whether the parent GravityOps menu has already been registered. 35 * Prevents duplicate add_menu_page() calls and ensures a single owner of the parent page. 36 * 37 * @var bool 38 */ 39 private static $did_parent_menu = false; 32 40 33 41 /** … … 55 63 */ 56 64 public static function ensure_parent_menu( $capability = '' ) { 57 global $menu; 58 59 $menu_slug = 'gravity_ops'; 60 61 // If another plugin in the suite has already created the menu, we don't need to do anything. 62 if ( ! empty( $menu ) && in_array( $menu_slug, array_column( $menu, 2 ), true ) ) { 63 return; 65 global $menu; 66 67 $menu_slug = 'gravity_ops'; 68 69 // Internal idempotency guard — if we've already added the parent once this request, bail. 70 if ( self::$did_parent_menu ) { 71 return; 72 } 73 74 // If another plugin in the suite has already created the menu, we don't need to do anything. 75 if ( ! empty( $menu ) && in_array( $menu_slug, array_column( $menu, 2 ), true ) ) { 76 return; 64 77 } 65 78 … … 128 141 if ( $hook ) { 129 142 add_action( 'load-' . $hook, [ self::class, 'maybe_handle_plugin_toggle' ] ); 130 } 143 } 144 145 // Mark parent as registered to avoid duplicate add_menu_page calls in mixed environments. 146 self::$did_parent_menu = true; 131 147 132 148 // Ensure assets are enqueued at the proper time and only on our screen. … … 134 150 add_action( 'admin_enqueue_scripts', [ self::class, 'enqueue_assets' ] ); 135 151 self::$did_enqueue_hook = true; 136 }152 } 137 153 138 154 // Register admin notices renderer (error/success messages from actions below). … … 140 156 add_action( 'admin_notices', [ self::class, 'render_admin_notices' ] ); 141 157 self::$did_notice_hook = true; 142 }158 } 143 159 } 144 160 … … 173 189 $updates_count = 0; 174 190 175 // Annotate registry items with runtime state .191 // Annotate registry items with runtime state (supports premium/free variant pairs). 176 192 foreach ( $registry as $key => $item ) { 177 193 $type = $item['type'] ?? 'plugin'; … … 179 195 continue; 180 196 } 181 182 $file = $item['plugin_file']; 183 $is_installed = isset( $installed_plugins[ $file ] ); 184 $is_active = $is_installed && is_plugin_active( $file ); 185 $version = $is_installed ? ( $installed_plugins[ $file ]['Version'] ?? '' ) : ''; 186 187 $has_update = isset( $update_response[ $file ] ); 188 if ( $has_update ) { 197 $primary_file = (string) $item['plugin_file']; 198 $premium_file = isset( $item['plugin_files']['premium'] ) ? (string) $item['plugin_files']['premium'] : ''; 199 $free_file = isset( $item['plugin_files']['free'] ) ? (string) $item['plugin_files']['free'] : ''; 200 201 // Helper to read state for a specific plugin file 202 $read_state = function ( string $file ) use ( $installed_plugins, $update_response ) { 203 $installed = isset( $installed_plugins[ $file ] ); 204 $active = $installed && is_plugin_active( $file ); 205 $version = $installed ? ( $installed_plugins[ $file ]['Version'] ?? '' ) : ''; 206 $has_upd = isset( $update_response[ $file ] ); 207 $new_ver = $has_upd ? ( $update_response[ $file ]->new_version ?? '' ) : ''; 208 return [ $installed, $active, $version, $has_upd, $new_ver ]; 209 }; 210 211 // Default path: single-file plugins 212 if ( empty( $premium_file ) && empty( $free_file ) ) { 213 [ $is_installed, $is_active, $version, $has_update, $new_version ] = $read_state( $primary_file ); 214 if ( $has_update ) { 215 ++$updates_count; 216 } 217 $registry[ $key ]['is_installed'] = $is_installed; 218 $registry[ $key ]['is_active'] = $is_active; 219 $registry[ $key ]['version'] = $version; 220 $registry[ $key ]['has_update'] = $has_update; 221 $registry[ $key ]['new_version'] = $new_version; 222 $registry[ $key ]['plugin_file_active'] = $is_active ? $primary_file : ''; 223 $registry[ $key ]['plugin_file_action'] = $primary_file; 224 $registry[ $key ]['uses_free'] = false; 225 continue; 226 } 227 228 // Variant-aware path: prefer premium when present 229 $is_installed_prem = false; 230 $is_active_prem = false; 231 $ver_prem = ''; 232 $upd_prem = false; 233 $new_prem = ''; 234 $is_installed_free = false; 235 $is_active_free = false; 236 $ver_free = ''; 237 $upd_free = false; 238 $new_free = ''; 239 240 if ( $premium_file ) { 241 [ $is_installed_prem, $is_active_prem, $ver_prem, $upd_prem, $new_prem ] = $read_state( $premium_file ); 242 } 243 if ( $free_file ) { 244 [ $is_installed_free, $is_active_free, $ver_free, $upd_free, $new_free ] = $read_state( $free_file ); 245 } 246 247 $is_installed = $is_installed_prem || $is_installed_free; 248 $is_active = $is_active_prem || $is_active_free; 249 $uses_free = false; 250 $version = ''; 251 $file_active = ''; 252 $has_update = false; 253 $new_version = ''; 254 255 // Choose versions and update flags based on precedence 256 if ( $is_active_prem ) { 257 $file_active = $premium_file; 258 $version = $ver_prem; 259 } elseif ( $is_active_free ) { 260 $file_active = $free_file; 261 $version = $ver_free; 262 $uses_free = true; 263 } elseif ( $is_installed_prem ) { 264 $version = $ver_prem; 265 } elseif ( $is_installed_free ) { 266 $version = $ver_free; 267 $uses_free = true; // Only free exists → show Free badge 268 } 269 270 // Updates: consider whichever is installed; prefer premium if both 271 if ( $upd_prem || $upd_free ) { 272 $has_update = $upd_prem || $upd_free; 273 $new_version = $upd_prem ? $new_prem : $new_free; 189 274 ++$updates_count; 190 275 } 191 276 192 $registry[ $key ]['is_installed'] = $is_installed; 193 $registry[ $key ]['is_active'] = $is_active; 194 $registry[ $key ]['version'] = $version; 195 $registry[ $key ]['has_update'] = $has_update; 196 $registry[ $key ]['new_version'] = $has_update ? ( $update_response[ $file ]->new_version ?? '' ) : ''; 277 // Activation target: if active → target the active file for deactivation. 278 // If inactive → prefer premium when installed; else free. 279 if ( $is_active ) { 280 $file_action = $file_active ?: ( $is_installed_prem ? $premium_file : $free_file ); 281 } else { 282 $file_action = $is_installed_prem ? $premium_file : ( $free_file ?: $premium_file ); 283 } 284 285 $registry[ $key ]['is_installed'] = $is_installed; 286 $registry[ $key ]['is_active'] = $is_active; 287 $registry[ $key ]['version'] = $version; 288 $registry[ $key ]['has_update'] = $has_update; 289 $registry[ $key ]['new_version'] = $new_version; 290 $registry[ $key ]['plugin_file_active'] = $file_active; 291 $registry[ $key ]['plugin_file_action'] = $file_action ?: $primary_file; 292 $registry[ $key ]['uses_free'] = $uses_free; 197 293 } 198 294 … … 319 415 <?php echo $is_active ? 'Active' : ( $is_installed ? 'Inactive' : 'Not Installed' ); ?> 320 416 </span> 417 <?php if ( ! empty( $item['uses_free'] ) ) : ?> 418 <span class="gops-badge">Free</span> 419 <?php endif; ?> 321 420 <?php if ( $version ) : ?> 322 421 <span class="gops-tile__version">v<?php echo esc_html( $version ); ?></span> … … 331 430 <?php if ( current_user_can( 'activate_plugins' ) ) : ?> 332 431 <form class="gops-action" method="post" action="<?php echo esc_url( $base_url ); ?>"> 333 <?php wp_nonce_field( 'gops_toggle_' . $item['plugin_file'], 'gops_nonce' ); ?>432 <?php wp_nonce_field( 'gops_toggle_' . ( $item['plugin_file_action'] ?? $item['plugin_file'] ), 'gops_nonce' ); ?> 334 433 <input type="hidden" name="gops-action" value="<?php echo $is_active ? 'deactivate' : 'activate'; ?>" /> 335 <input type="hidden" name="plugin" value="<?php echo esc_attr( $i tem['plugin_file']); ?>" />434 <input type="hidden" name="plugin" value="<?php echo esc_attr( $is_active ? ( $item['plugin_file_active'] ?: ( $item['plugin_file_action'] ?? $item['plugin_file'] ) ) : ( $item['plugin_file_action'] ?? $item['plugin_file'] ) ); ?>" /> 336 435 <button class="button<?php echo $is_active ? '' : ' button-primary'; ?>" type="submit"><?php echo $is_active ? 'Deactivate' : 'Activate'; ?></button> 337 436 </form> … … 358 457 * @return string Icon URL or empty string if none found. 359 458 */ 360 private static function resolve_plugin_icon_url( array $item ): string{459 private static function resolve_plugin_icon_url( $item ) { 361 460 $url = apply_filters( 'gravityops_plugin_icon_url', '', $item ); 362 461 if ( ! empty( $url ) ) { … … 422 521 * @return void 423 522 */ 424 public static function enqueue_assets( $hook ) : void{523 public static function enqueue_assets( $hook ) { 425 524 $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended 426 525 $is_gravityops_screen = ( false !== strpos( (string) $hook, 'gravity_ops' ) ) || ( 'gravity_ops' === $page ); … … 452 551 * @return void 453 552 */ 454 public static function render_admin_notices() : void{553 public static function render_admin_notices() { 455 554 $is_gravityops_screen = isset( $_GET['page'] ) && 'gravity_ops' === sanitize_key( wp_unslash( $_GET['page'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended 456 555 if ( ! $is_gravityops_screen ) { -
folders-4-gravity/tags/1.0.6/vendor-prefixed/gravityops/core/src/SuiteRegistry.php
r3420844 r3422666 25 25 * 'description', 'marketing_url', 'docs_url', 'is_free', and 'icon_html'. 26 26 */ 27 public static function all() : array{27 public static function all() { 28 28 // NOTE: External links are placeholders. Replace with final URLs where marked. 29 29 return [ … … 54 54 'slug' => 'integrate-asana-with-gravity-forms', 55 55 'plugin_file' => 'integrate-asana-with-gravity-forms/integrate-asana-with-gravity-forms.php', 56 // Free + Premium variants: prefer premium folder when present 57 'plugin_files' => [ 58 'premium' => 'integrate-asana-with-gravity-forms-premium/integrate-asana-with-gravity-forms.php', 59 'free' => 'integrate-asana-with-gravity-forms/integrate-asana-with-gravity-forms.php', 60 ], 56 61 'name' => 'Integrate Asana with Gravity Forms', 57 62 'description' => 'Turn form submissions into Asana tasks instantly.', … … 65 70 'slug' => 'mass_email_notifications_for_gf', 66 71 'plugin_file' => 'mass-email-notifications-for-gravity-forms/mass-email-notifications-for-gf.php', 72 // Free + Premium variants: prefer premium folder when present 73 'plugin_files' => [ 74 'premium' => 'mass-email-notifications-for-gravity-forms-premium/mass-email-notifications-for-gf.php', 75 'free' => 'mass-email-notifications-for-gravity-forms/mass-email-notifications-for-gf.php', 76 ], 67 77 'name' => 'Mass Email Notifications', 68 78 'description' => 'Send bulk emails to Gravity Forms entries.', … … 75 85 'type' => 'plugin', 76 86 'slug' => 'kanban-view-for-gravity-view', 77 'plugin_file' => 'kanban-view-for-gravity-view /kanban-view-for-gv.php',87 'plugin_file' => 'kanban-view-for-gravity-view-premium/kanban-view-for-gv.php', 78 88 'name' => 'Kanban View for GravityView', 79 89 'description' => 'Turn GravityView into a kanban project board.', … … 86 96 'type' => 'plugin', 87 97 'slug' => 'Recurring_Form_Submissions_For_Gravity_Form', 88 'plugin_file' => 'recurring-form-submissions-for-gravity-forms /recurring-form-submissions-for-gravity-form.php',98 'plugin_file' => 'recurring-form-submissions-for-gravity-forms-premium/recurring-form-submissions-for-gravity-form.php', 89 99 'name' => 'Recurring Form Submissions', 90 100 'description' => 'Schedule recurring Gravity Forms submissions.', … … 97 107 'type' => 'plugin', 98 108 'slug' => 'gravity_ops_global_variables', 99 'plugin_file' => 'global-variables-for-gravity-math/global-variables-for-gravity-math.php', 109 // Premium-only plugin uses a -premium folder suffix 110 'plugin_file' => 'global-variables-for-gravity-math-premium/global-variables-for-gravity-math.php', 100 111 'name' => 'Global Variables', 101 112 'description' => 'Create shared variables for GravityMath formulas.', -
folders-4-gravity/trunk/folders-4-gravity.php
r3420844 r3422666 5 5 * Author URI: https://brightleafdigital.io/ 6 6 * Description: Organize your Gravity Forms and Gravity Views by folders. 7 * Version: 1.0. 57 * Version: 1.0.6 8 8 * Author: BrightLeaf Digital 9 9 * License: GPL-2.0+ 10 10 * Requires PHP: 8.0 11 11 */ 12 13 use F4G\GravityOps\Core\Admin\AdminShell; 12 14 13 15 if ( ! defined( 'ABSPATH' ) ) { … … 16 18 17 19 require_once __DIR__ . '/vendor-prefixed/autoload.php'; 20 21 require_once __DIR__ . '/vendor/autoload.php'; 22 23 // Instantiate this plugin's copy of the AdminShell early so provider negotiation can happen on plugins_loaded. 24 add_action( 25 'plugins_loaded', 26 function () { 27 AdminShell::instance(); 28 }, 29 1 30 ); 18 31 19 32 … … 28 41 return; 29 42 } 43 44 // Ensure GravityOps shared assets resolve when library is vendor-installed in this plugin. 45 add_filter( 46 'gravityops_assets_base_url', 47 function ( $url ) { 48 return $url ?: plugins_url( 'vendor-prefixed/gravityops/core/assets/', __FILE__ ); 49 } 50 ); 30 51 31 52 add_action( -
folders-4-gravity/trunk/includes/class-gravity-ops-form-folders.php
r3420844 r3422666 6 6 use F4G\GravityOps\Core\Admin\AdminShell; 7 7 use F4G\GravityOps\Core\Utils\AssetHelper as Assets; 8 use function F4G\GravityOps\Core\Admin\gravityops_shell; 8 9 9 10 if ( ! defined( 'ABSPATH' ) ) { … … 164 165 // Register the GravityOps AdminShell page for the free Folders plugin. 165 166 // Tabs: Overview (render), Help (render), Affiliation (external link) 166 AdminShell::instance()->register_plugin_page(167 gravityops_shell()->register_plugin_page( 167 168 'folders-4-gravity', 168 169 [ -
folders-4-gravity/trunk/readme.txt
r3420844 r3422666 4 4 Requires at least: 6.5 5 5 Tested up to: 6.9 6 Stable tag: 1.0. 56 Stable tag: 1.0.6 7 7 Requires PHP: 8.0 8 8 License: GPLv2 … … 155 155 == Changelog == 156 156 157 ### 1.0.6 158 - Fixed a bug with new admin menu 159 157 160 ### 1.0.5 158 - =Updated plugin menu161 - Updated plugin menu 159 162 160 163 ### 1.0.4 … … 169 172 - Split Forms and Views into separate dashboard widgets. 170 173 171 ### 1.0.1172 - Added initial dashboard widgets.173 174 174 175 175 == Upgrade Notice == -
folders-4-gravity/trunk/vendor-prefixed/autoload.php
r3420844 r3422666 20 20 require_once __DIR__ . '/composer/autoload_real.php'; 21 21 22 return ComposerAutoloaderInit c64224000443d31aa951e25675df6b88::getLoader();22 return ComposerAutoloaderInit4d4b64ecb88cd03a3424690392b1e3bb::getLoader(); -
folders-4-gravity/trunk/vendor-prefixed/composer/autoload_real.php
r3420844 r3422666 3 3 // autoload_real.php @generated by Composer 4 4 5 class ComposerAutoloaderInit c64224000443d31aa951e25675df6b885 class ComposerAutoloaderInit4d4b64ecb88cd03a3424690392b1e3bb 6 6 { 7 7 private static $loader; … … 25 25 require __DIR__ . '/platform_check.php'; 26 26 27 spl_autoload_register(array('ComposerAutoloaderInit c64224000443d31aa951e25675df6b88', 'loadClassLoader'), true, true);27 spl_autoload_register(array('ComposerAutoloaderInit4d4b64ecb88cd03a3424690392b1e3bb', 'loadClassLoader'), true, true); 28 28 self::$loader = $loader = new \F4G\Composer\Autoload\ClassLoader(\dirname(__DIR__)); 29 spl_autoload_unregister(array('ComposerAutoloaderInit c64224000443d31aa951e25675df6b88', 'loadClassLoader'));29 spl_autoload_unregister(array('ComposerAutoloaderInit4d4b64ecb88cd03a3424690392b1e3bb', 'loadClassLoader')); 30 30 31 31 require __DIR__ . '/autoload_static.php'; 32 call_user_func(\F4G\Composer\Autoload\ComposerStaticInit c64224000443d31aa951e25675df6b88::getInitializer($loader));32 call_user_func(\F4G\Composer\Autoload\ComposerStaticInit4d4b64ecb88cd03a3424690392b1e3bb::getInitializer($loader)); 33 33 34 34 $loader->setClassMapAuthoritative(true); -
folders-4-gravity/trunk/vendor-prefixed/composer/autoload_static.php
r3420844 r3422666 5 5 namespace F4G\Composer\Autoload; 6 6 7 class ComposerStaticInit c64224000443d31aa951e25675df6b887 class ComposerStaticInit4d4b64ecb88cd03a3424690392b1e3bb 8 8 { 9 9 public static $prefixLengthsPsr4 = array ( … … 36 36 { 37 37 return \Closure::bind(function () use ($loader) { 38 $loader->prefixLengthsPsr4 = ComposerStaticInit c64224000443d31aa951e25675df6b88::$prefixLengthsPsr4;39 $loader->prefixDirsPsr4 = ComposerStaticInit c64224000443d31aa951e25675df6b88::$prefixDirsPsr4;40 $loader->classMap = ComposerStaticInit c64224000443d31aa951e25675df6b88::$classMap;38 $loader->prefixLengthsPsr4 = ComposerStaticInit4d4b64ecb88cd03a3424690392b1e3bb::$prefixLengthsPsr4; 39 $loader->prefixDirsPsr4 = ComposerStaticInit4d4b64ecb88cd03a3424690392b1e3bb::$prefixDirsPsr4; 40 $loader->classMap = ComposerStaticInit4d4b64ecb88cd03a3424690392b1e3bb::$classMap; 41 41 42 42 }, null, ClassLoader::class); -
folders-4-gravity/trunk/vendor-prefixed/composer/installed.json
r3420844 r3422666 3 3 { 4 4 "name": "gravityops/core", 5 "version": "1.0. 18",6 "version_normalized": "1.0. 18.0",5 "version": "1.0.21", 6 "version_normalized": "1.0.21.0", 7 7 "source": { 8 8 "type": "git", 9 9 "url": "git@github.com:Eitan-brightleaf/gravityops.git", 10 "reference": " a74352489b658ed6a7f1c588abb2e18aae065a68"10 "reference": "5d859a7cca5cf8c1e469c80a88e755fb1be7c522" 11 11 }, 12 12 "dist": { 13 13 "type": "zip", 14 "url": "https://api.github.com/repos/Eitan-brightleaf/gravityops/zipball/ a74352489b658ed6a7f1c588abb2e18aae065a68",15 "reference": " a74352489b658ed6a7f1c588abb2e18aae065a68",14 "url": "https://api.github.com/repos/Eitan-brightleaf/gravityops/zipball/5d859a7cca5cf8c1e469c80a88e755fb1be7c522", 15 "reference": "5d859a7cca5cf8c1e469c80a88e755fb1be7c522", 16 16 "shasum": "" 17 17 }, … … 19 19 "php": ">=7.4" 20 20 }, 21 "time": "2025-12-1 6T09:58:09+00:00",21 "time": "2025-12-18T07:45:02+00:00", 22 22 "type": "library", 23 23 "installation-source": "source", -
folders-4-gravity/trunk/vendor-prefixed/composer/installed.php
r3420844 r3422666 5 5 'pretty_version' => 'dev-main', 6 6 'version' => 'dev-main', 7 'reference' => ' edc84c3f8028b3fc71f3205575dd4457cad99bf7',7 'reference' => '58837dca80970d5fde4a728d8d9066cba6ce26a7', 8 8 'type' => 'library', 9 9 'install_path' => __DIR__ . '/../', … … 17 17 'gravityops/core' => 18 18 array ( 19 'pretty_version' => '1.0. 18',20 'version' => '1.0. 18.0',21 'reference' => ' a74352489b658ed6a7f1c588abb2e18aae065a68',19 'pretty_version' => '1.0.21', 20 'version' => '1.0.21.0', 21 'reference' => '5d859a7cca5cf8c1e469c80a88e755fb1be7c522', 22 22 'type' => 'library', 23 23 'install_path' => __DIR__ . '/../gravityops/core', -
folders-4-gravity/trunk/vendor-prefixed/gravityops/core/src/Admin/AdminShell.php
r3420844 r3422666 16 16 17 17 /** 18 * Library/core version used for provider negotiation. 19 * The highest version across loaded copies should be selected as provider. 20 */ 21 public const CORE_VERSION = '1.0.21'; 22 23 /** 18 24 * Holds the singleton instance. 19 25 * … … 21 27 */ 22 28 private static ?AdminShell $instance = null; 29 30 /** 31 * Tracks whether the chosen provider has already booted (registered hooks). 32 * Prevents multiple copies from attaching side‑effect hooks. 33 * 34 * @var bool 35 */ 36 private static bool $did_boot = false; 23 37 24 38 /** … … 35 49 * Get singleton instance. 36 50 */ 37 public static function instance(): AdminShell { 38 // First, try to get a shared instance provided by any loaded copy via filter. 51 public static function instance() { 52 // New: negotiate best provider by highest version (cross‑namespace). 53 $best = apply_filters( 'gravityops_shell_provider', null ); 54 if ( is_array( $best ) && isset( $best['shell'] ) ) { 55 return $best['shell']; 56 } 57 58 // Legacy: try a shared instance provided by any copy. 39 59 $shared = apply_filters( 'gravityops_shell_instance', null ); 40 60 if ( $shared instanceof self ) { 41 61 return $shared; 42 62 } 63 43 64 if ( null === self::$instance ) { 44 65 self::$instance = new self(); … … 53 74 */ 54 75 private function __construct() { 55 // Cross-namespace shared instance resolver: first provider wins. 56 $existing = apply_filters( 'gravityops_shell_instance', null ); 57 if ( $existing && $existing !== $this ) { 58 // Another copy has already provided an instance; avoid duplicate hooks. 59 return; 60 } 61 62 // Expose this instance for other namespaces/copies to reuse. 76 // Expose this instance for other namespaces/copies to reuse (legacy "first copy wins"). 63 77 add_filter( 64 78 'gravityops_shell_instance', … … 69 83 ); 70 84 71 // Defer adding menus until after all plugins have registered. 72 add_action( 'admin_menu', [ $this, 'register_menus' ], 99 ); 73 // Do NOT hide submenus by default; users reported accessibility issues. 74 // If needed in the future, this can be reintroduced behind a filter. 75 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); 85 // Versioned provider negotiation: highest version wins. 86 add_filter( 87 'gravityops_shell_provider', 88 function ( $best ) { 89 $mine = [ 90 'version' => self::CORE_VERSION, 91 'shell' => $this, 92 ]; 93 if ( ! is_array( $best ) || empty( $best['version'] ) ) { 94 return $mine; 95 } 96 return version_compare( (string) $mine['version'], (string) $best['version'], '>' ) ? $mine : $best; 97 }, 98 1 99 ); 100 101 // Defer side‑effect hooks until we negotiate the best provider. 102 // Each copy registers this, but only the selected provider will actually boot. 103 // If instantiated after plugins_loaded has already fired, boot immediately. 104 if ( function_exists( 'did_action' ) && did_action( 'plugins_loaded' ) ) { 105 $this->maybe_boot(); 106 } else { 107 add_action( 'plugins_loaded', [ $this, 'maybe_boot' ], 20 ); 108 } 76 109 } 77 110 … … 84 117 * @return bool True if the page exists. 85 118 */ 86 public static function has_page( string $slug ): bool{119 public static function has_page( $slug ) { 87 120 $provider = apply_filters( 'gravityops_shell_instance', null ); 88 121 if ( $provider instanceof self ) { … … 112 145 * @return array<string,array<string,string>> Tabs config array suitable to merge with register_plugin_page() tabs. 113 146 */ 114 public static function freemius_tabs( string $plugin_slug, array $labels = [] ): array{147 public static function freemius_tabs( $plugin_slug, $labels = [] ) { 115 148 $default_labels = [ 116 149 'account' => 'Account', … … 164 197 * @return void 165 198 */ 166 public static function render_feeds_list( array $feeds_and_forms, string $gf_subview_slug, string $plugin_short_title, string $toggle_action ): void{199 public static function render_feeds_list( $feeds_and_forms, $gf_subview_slug, $plugin_short_title, $toggle_action ) { 167 200 echo '<div class="gops-card">'; 168 201 echo '<h2 class="gops-title" style="margin:0 0 10px;">' . esc_html( $plugin_short_title ) . ' Feeds</h2>'; … … 224 257 * @return void 225 258 */ 226 public static function process_feed_toggle( string $action_prefix, string$return_url ) {259 public static function process_feed_toggle( $action_prefix, $return_url ) { 227 260 if ( empty( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) || 'POST' !== ( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ?? '' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized 228 261 wp_safe_redirect( admin_url( $return_url ) ); … … 262 295 * @return void 263 296 */ 264 public static function render_help_tab( array $links ): void{297 public static function render_help_tab( $links ) { 265 298 echo '<div class="gops-card">'; 266 299 echo '<h2 class="gops-title" style="margin:0 0 8px;">Help</h2>'; … … 282 315 * @param array $args Page configuration. 283 316 */ 284 public function register_plugin_page( string $slug, array $args ): void { 285 // If another copy is the active provider, forward registration to it. 317 public function register_plugin_page( $slug, $args ) { 318 // If another copy is the active provider (new negotiation), forward registration to it first. 319 $best = apply_filters( 'gravityops_shell_provider', null ); 320 if ( is_array( $best ) && isset( $best['shell'] ) && $best['shell'] !== $this && is_object( $best['shell'] ) && method_exists( $best['shell'], 'register_plugin_page' ) ) { 321 $best['shell']->register_plugin_page( $slug, $args ); 322 return; 323 } 324 // Legacy shared-instance forwarding as a fallback. 286 325 $provider = apply_filters( 'gravityops_shell_instance', null ); 287 326 if ( $provider && $provider !== $this && is_object( $provider ) && method_exists( $provider, 'register_plugin_page' ) ) { 288 // Call method on the provider (may be a different class/namespace).289 327 $provider->register_plugin_page( $slug, $args ); 290 328 return; … … 304 342 * Add the submenu pages under the shared GravityOps parent. 305 343 */ 306 public function register_menus() : void{344 public function register_menus() { 307 345 if ( class_exists( __NAMESPACE__ . '\\SuiteMenu' ) ) { 308 346 SuiteMenu::ensure_parent_menu(); … … 342 380 * @param string $hook Current admin page hook. 343 381 */ 344 public function enqueue_assets( $hook ) : void{382 public function enqueue_assets( $hook ) { 345 383 $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended 346 384 … … 375 413 376 414 /** 415 * Boot the chosen provider (register side‑effect hooks). Called once. 416 * 417 * @return void 418 */ 419 private function boot() { 420 if ( self::$did_boot ) { 421 return; 422 } 423 // Defer adding menus until after all plugins have registered. 424 add_action( 'admin_menu', [ $this, 'register_menus' ], 99 ); 425 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); 426 self::$did_boot = true; 427 } 428 429 /** 430 * On plugins_loaded, negotiate the best provider and boot only that one. 431 * Each copy registers this callback, but only the selected provider acts. 432 * 433 * @return void 434 */ 435 public function maybe_boot() { 436 if ( self::$did_boot ) { 437 return; 438 } 439 $best = apply_filters( 'gravityops_shell_provider', null ); 440 if ( is_array( $best ) && isset( $best['shell'] ) ) { 441 if ( $best['shell'] === $this ) { 442 $this->boot(); 443 } 444 return; 445 } 446 // If no provider negotiation is available, fall back to local boot (legacy environments). 447 $shared = apply_filters( 'gravityops_shell_instance', null ); 448 if ( $shared && $shared !== $this ) { 449 // Another copy is acting as shared instance; let it boot on its own call. 450 return; 451 } 452 $this->boot(); 453 } 454 455 /** 377 456 * Render the complete page including header, pills, and tab content. 378 457 * … … 380 459 * @param array $args Page config. 381 460 */ 382 public function render_page( string $slug, array $args ): void{461 public function render_page( $slug, $args ) { 383 462 if ( ! current_user_can( $args['capability'] ?? 'manage_options' ) ) { 384 463 return; … … 435 514 * @return void 436 515 */ 437 private function render_header_only( array $args ): void{516 private function render_header_only( $args ) { 438 517 ?> 439 518 <header class="gops-header"> … … 471 550 * @return array{css:string,js:string} 472 551 */ 473 public static function resolve_assets_urls() : array{552 public static function resolve_assets_urls() { 474 553 $base_url = apply_filters( 'gravityops_assets_base_url', '' ); 475 554 $css = ''; -
folders-4-gravity/trunk/vendor-prefixed/gravityops/core/src/Admin/SuiteMenu.php
r3420844 r3422666 30 30 */ 31 31 private static $did_notice_hook = false; 32 33 /** 34 * Track whether the parent GravityOps menu has already been registered. 35 * Prevents duplicate add_menu_page() calls and ensures a single owner of the parent page. 36 * 37 * @var bool 38 */ 39 private static $did_parent_menu = false; 32 40 33 41 /** … … 55 63 */ 56 64 public static function ensure_parent_menu( $capability = '' ) { 57 global $menu; 58 59 $menu_slug = 'gravity_ops'; 60 61 // If another plugin in the suite has already created the menu, we don't need to do anything. 62 if ( ! empty( $menu ) && in_array( $menu_slug, array_column( $menu, 2 ), true ) ) { 63 return; 65 global $menu; 66 67 $menu_slug = 'gravity_ops'; 68 69 // Internal idempotency guard — if we've already added the parent once this request, bail. 70 if ( self::$did_parent_menu ) { 71 return; 72 } 73 74 // If another plugin in the suite has already created the menu, we don't need to do anything. 75 if ( ! empty( $menu ) && in_array( $menu_slug, array_column( $menu, 2 ), true ) ) { 76 return; 64 77 } 65 78 … … 128 141 if ( $hook ) { 129 142 add_action( 'load-' . $hook, [ self::class, 'maybe_handle_plugin_toggle' ] ); 130 } 143 } 144 145 // Mark parent as registered to avoid duplicate add_menu_page calls in mixed environments. 146 self::$did_parent_menu = true; 131 147 132 148 // Ensure assets are enqueued at the proper time and only on our screen. … … 134 150 add_action( 'admin_enqueue_scripts', [ self::class, 'enqueue_assets' ] ); 135 151 self::$did_enqueue_hook = true; 136 }152 } 137 153 138 154 // Register admin notices renderer (error/success messages from actions below). … … 140 156 add_action( 'admin_notices', [ self::class, 'render_admin_notices' ] ); 141 157 self::$did_notice_hook = true; 142 }158 } 143 159 } 144 160 … … 173 189 $updates_count = 0; 174 190 175 // Annotate registry items with runtime state .191 // Annotate registry items with runtime state (supports premium/free variant pairs). 176 192 foreach ( $registry as $key => $item ) { 177 193 $type = $item['type'] ?? 'plugin'; … … 179 195 continue; 180 196 } 181 182 $file = $item['plugin_file']; 183 $is_installed = isset( $installed_plugins[ $file ] ); 184 $is_active = $is_installed && is_plugin_active( $file ); 185 $version = $is_installed ? ( $installed_plugins[ $file ]['Version'] ?? '' ) : ''; 186 187 $has_update = isset( $update_response[ $file ] ); 188 if ( $has_update ) { 197 $primary_file = (string) $item['plugin_file']; 198 $premium_file = isset( $item['plugin_files']['premium'] ) ? (string) $item['plugin_files']['premium'] : ''; 199 $free_file = isset( $item['plugin_files']['free'] ) ? (string) $item['plugin_files']['free'] : ''; 200 201 // Helper to read state for a specific plugin file 202 $read_state = function ( string $file ) use ( $installed_plugins, $update_response ) { 203 $installed = isset( $installed_plugins[ $file ] ); 204 $active = $installed && is_plugin_active( $file ); 205 $version = $installed ? ( $installed_plugins[ $file ]['Version'] ?? '' ) : ''; 206 $has_upd = isset( $update_response[ $file ] ); 207 $new_ver = $has_upd ? ( $update_response[ $file ]->new_version ?? '' ) : ''; 208 return [ $installed, $active, $version, $has_upd, $new_ver ]; 209 }; 210 211 // Default path: single-file plugins 212 if ( empty( $premium_file ) && empty( $free_file ) ) { 213 [ $is_installed, $is_active, $version, $has_update, $new_version ] = $read_state( $primary_file ); 214 if ( $has_update ) { 215 ++$updates_count; 216 } 217 $registry[ $key ]['is_installed'] = $is_installed; 218 $registry[ $key ]['is_active'] = $is_active; 219 $registry[ $key ]['version'] = $version; 220 $registry[ $key ]['has_update'] = $has_update; 221 $registry[ $key ]['new_version'] = $new_version; 222 $registry[ $key ]['plugin_file_active'] = $is_active ? $primary_file : ''; 223 $registry[ $key ]['plugin_file_action'] = $primary_file; 224 $registry[ $key ]['uses_free'] = false; 225 continue; 226 } 227 228 // Variant-aware path: prefer premium when present 229 $is_installed_prem = false; 230 $is_active_prem = false; 231 $ver_prem = ''; 232 $upd_prem = false; 233 $new_prem = ''; 234 $is_installed_free = false; 235 $is_active_free = false; 236 $ver_free = ''; 237 $upd_free = false; 238 $new_free = ''; 239 240 if ( $premium_file ) { 241 [ $is_installed_prem, $is_active_prem, $ver_prem, $upd_prem, $new_prem ] = $read_state( $premium_file ); 242 } 243 if ( $free_file ) { 244 [ $is_installed_free, $is_active_free, $ver_free, $upd_free, $new_free ] = $read_state( $free_file ); 245 } 246 247 $is_installed = $is_installed_prem || $is_installed_free; 248 $is_active = $is_active_prem || $is_active_free; 249 $uses_free = false; 250 $version = ''; 251 $file_active = ''; 252 $has_update = false; 253 $new_version = ''; 254 255 // Choose versions and update flags based on precedence 256 if ( $is_active_prem ) { 257 $file_active = $premium_file; 258 $version = $ver_prem; 259 } elseif ( $is_active_free ) { 260 $file_active = $free_file; 261 $version = $ver_free; 262 $uses_free = true; 263 } elseif ( $is_installed_prem ) { 264 $version = $ver_prem; 265 } elseif ( $is_installed_free ) { 266 $version = $ver_free; 267 $uses_free = true; // Only free exists → show Free badge 268 } 269 270 // Updates: consider whichever is installed; prefer premium if both 271 if ( $upd_prem || $upd_free ) { 272 $has_update = $upd_prem || $upd_free; 273 $new_version = $upd_prem ? $new_prem : $new_free; 189 274 ++$updates_count; 190 275 } 191 276 192 $registry[ $key ]['is_installed'] = $is_installed; 193 $registry[ $key ]['is_active'] = $is_active; 194 $registry[ $key ]['version'] = $version; 195 $registry[ $key ]['has_update'] = $has_update; 196 $registry[ $key ]['new_version'] = $has_update ? ( $update_response[ $file ]->new_version ?? '' ) : ''; 277 // Activation target: if active → target the active file for deactivation. 278 // If inactive → prefer premium when installed; else free. 279 if ( $is_active ) { 280 $file_action = $file_active ?: ( $is_installed_prem ? $premium_file : $free_file ); 281 } else { 282 $file_action = $is_installed_prem ? $premium_file : ( $free_file ?: $premium_file ); 283 } 284 285 $registry[ $key ]['is_installed'] = $is_installed; 286 $registry[ $key ]['is_active'] = $is_active; 287 $registry[ $key ]['version'] = $version; 288 $registry[ $key ]['has_update'] = $has_update; 289 $registry[ $key ]['new_version'] = $new_version; 290 $registry[ $key ]['plugin_file_active'] = $file_active; 291 $registry[ $key ]['plugin_file_action'] = $file_action ?: $primary_file; 292 $registry[ $key ]['uses_free'] = $uses_free; 197 293 } 198 294 … … 319 415 <?php echo $is_active ? 'Active' : ( $is_installed ? 'Inactive' : 'Not Installed' ); ?> 320 416 </span> 417 <?php if ( ! empty( $item['uses_free'] ) ) : ?> 418 <span class="gops-badge">Free</span> 419 <?php endif; ?> 321 420 <?php if ( $version ) : ?> 322 421 <span class="gops-tile__version">v<?php echo esc_html( $version ); ?></span> … … 331 430 <?php if ( current_user_can( 'activate_plugins' ) ) : ?> 332 431 <form class="gops-action" method="post" action="<?php echo esc_url( $base_url ); ?>"> 333 <?php wp_nonce_field( 'gops_toggle_' . $item['plugin_file'], 'gops_nonce' ); ?>432 <?php wp_nonce_field( 'gops_toggle_' . ( $item['plugin_file_action'] ?? $item['plugin_file'] ), 'gops_nonce' ); ?> 334 433 <input type="hidden" name="gops-action" value="<?php echo $is_active ? 'deactivate' : 'activate'; ?>" /> 335 <input type="hidden" name="plugin" value="<?php echo esc_attr( $i tem['plugin_file']); ?>" />434 <input type="hidden" name="plugin" value="<?php echo esc_attr( $is_active ? ( $item['plugin_file_active'] ?: ( $item['plugin_file_action'] ?? $item['plugin_file'] ) ) : ( $item['plugin_file_action'] ?? $item['plugin_file'] ) ); ?>" /> 336 435 <button class="button<?php echo $is_active ? '' : ' button-primary'; ?>" type="submit"><?php echo $is_active ? 'Deactivate' : 'Activate'; ?></button> 337 436 </form> … … 358 457 * @return string Icon URL or empty string if none found. 359 458 */ 360 private static function resolve_plugin_icon_url( array $item ): string{459 private static function resolve_plugin_icon_url( $item ) { 361 460 $url = apply_filters( 'gravityops_plugin_icon_url', '', $item ); 362 461 if ( ! empty( $url ) ) { … … 422 521 * @return void 423 522 */ 424 public static function enqueue_assets( $hook ) : void{523 public static function enqueue_assets( $hook ) { 425 524 $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended 426 525 $is_gravityops_screen = ( false !== strpos( (string) $hook, 'gravity_ops' ) ) || ( 'gravity_ops' === $page ); … … 452 551 * @return void 453 552 */ 454 public static function render_admin_notices() : void{553 public static function render_admin_notices() { 455 554 $is_gravityops_screen = isset( $_GET['page'] ) && 'gravity_ops' === sanitize_key( wp_unslash( $_GET['page'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended 456 555 if ( ! $is_gravityops_screen ) { -
folders-4-gravity/trunk/vendor-prefixed/gravityops/core/src/SuiteRegistry.php
r3420844 r3422666 25 25 * 'description', 'marketing_url', 'docs_url', 'is_free', and 'icon_html'. 26 26 */ 27 public static function all() : array{27 public static function all() { 28 28 // NOTE: External links are placeholders. Replace with final URLs where marked. 29 29 return [ … … 54 54 'slug' => 'integrate-asana-with-gravity-forms', 55 55 'plugin_file' => 'integrate-asana-with-gravity-forms/integrate-asana-with-gravity-forms.php', 56 // Free + Premium variants: prefer premium folder when present 57 'plugin_files' => [ 58 'premium' => 'integrate-asana-with-gravity-forms-premium/integrate-asana-with-gravity-forms.php', 59 'free' => 'integrate-asana-with-gravity-forms/integrate-asana-with-gravity-forms.php', 60 ], 56 61 'name' => 'Integrate Asana with Gravity Forms', 57 62 'description' => 'Turn form submissions into Asana tasks instantly.', … … 65 70 'slug' => 'mass_email_notifications_for_gf', 66 71 'plugin_file' => 'mass-email-notifications-for-gravity-forms/mass-email-notifications-for-gf.php', 72 // Free + Premium variants: prefer premium folder when present 73 'plugin_files' => [ 74 'premium' => 'mass-email-notifications-for-gravity-forms-premium/mass-email-notifications-for-gf.php', 75 'free' => 'mass-email-notifications-for-gravity-forms/mass-email-notifications-for-gf.php', 76 ], 67 77 'name' => 'Mass Email Notifications', 68 78 'description' => 'Send bulk emails to Gravity Forms entries.', … … 75 85 'type' => 'plugin', 76 86 'slug' => 'kanban-view-for-gravity-view', 77 'plugin_file' => 'kanban-view-for-gravity-view /kanban-view-for-gv.php',87 'plugin_file' => 'kanban-view-for-gravity-view-premium/kanban-view-for-gv.php', 78 88 'name' => 'Kanban View for GravityView', 79 89 'description' => 'Turn GravityView into a kanban project board.', … … 86 96 'type' => 'plugin', 87 97 'slug' => 'Recurring_Form_Submissions_For_Gravity_Form', 88 'plugin_file' => 'recurring-form-submissions-for-gravity-forms /recurring-form-submissions-for-gravity-form.php',98 'plugin_file' => 'recurring-form-submissions-for-gravity-forms-premium/recurring-form-submissions-for-gravity-form.php', 89 99 'name' => 'Recurring Form Submissions', 90 100 'description' => 'Schedule recurring Gravity Forms submissions.', … … 97 107 'type' => 'plugin', 98 108 'slug' => 'gravity_ops_global_variables', 99 'plugin_file' => 'global-variables-for-gravity-math/global-variables-for-gravity-math.php', 109 // Premium-only plugin uses a -premium folder suffix 110 'plugin_file' => 'global-variables-for-gravity-math-premium/global-variables-for-gravity-math.php', 100 111 'name' => 'Global Variables', 101 112 'description' => 'Create shared variables for GravityMath formulas.',
Note: See TracChangeset
for help on using the changeset viewer.