Plugin Directory

Changeset 3422666


Ignore:
Timestamp:
12/18/2025 09:26:44 AM (3 months ago)
Author:
eitanatbrightleaf
Message:

Update to version 1.0.6 from GitHub

Location:
folders-4-gravity
Files:
2 added
22 edited
1 copied

Legend:

Unmodified
Added
Removed
  • folders-4-gravity/tags/1.0.6/folders-4-gravity.php

    r3420844 r3422666  
    55 * Author URI: https://brightleafdigital.io/
    66 * Description: Organize your Gravity Forms and Gravity Views by folders.
    7  * Version: 1.0.5
     7 * Version: 1.0.6
    88 * Author: BrightLeaf Digital
    99 * License: GPL-2.0+
    1010 * Requires PHP: 8.0
    1111 */
     12
     13use F4G\GravityOps\Core\Admin\AdminShell;
    1214
    1315if ( ! defined( 'ABSPATH' ) ) {
     
    1618
    1719require_once __DIR__ . '/vendor-prefixed/autoload.php';
     20
     21require_once __DIR__ . '/vendor/autoload.php';
     22
     23// Instantiate this plugin's copy of the AdminShell early so provider negotiation can happen on plugins_loaded.
     24add_action(
     25    'plugins_loaded',
     26    function () {
     27        AdminShell::instance();
     28    },
     29    1
     30);
    1831
    1932
     
    2841    return;
    2942}
     43
     44// Ensure GravityOps shared assets resolve when library is vendor-installed in this plugin.
     45add_filter(
     46    'gravityops_assets_base_url',
     47    function ( $url ) {
     48        return $url ?: plugins_url( 'vendor-prefixed/gravityops/core/assets/', __FILE__ );
     49    }
     50);
    3051
    3152add_action(
  • folders-4-gravity/tags/1.0.6/includes/class-gravity-ops-form-folders.php

    r3420844 r3422666  
    66use F4G\GravityOps\Core\Admin\AdminShell;
    77use F4G\GravityOps\Core\Utils\AssetHelper as Assets;
     8use function F4G\GravityOps\Core\Admin\gravityops_shell;
    89
    910if ( ! defined( 'ABSPATH' ) ) {
     
    164165        // Register the GravityOps AdminShell page for the free Folders plugin.
    165166        // Tabs: Overview (render), Help (render), Affiliation (external link)
    166         AdminShell::instance()->register_plugin_page(
     167        gravityops_shell()->register_plugin_page(
    167168            'folders-4-gravity',
    168169            [
  • folders-4-gravity/tags/1.0.6/readme.txt

    r3420844 r3422666  
    44Requires at least: 6.5
    55Tested up to: 6.9
    6 Stable tag: 1.0.5
     6Stable tag: 1.0.6
    77Requires PHP: 8.0
    88License: GPLv2
     
    155155== Changelog ==
    156156
     157### 1.0.6
     158- Fixed a bug with new admin menu
     159
    157160### 1.0.5
    158 - =Updated plugin menu
     161- Updated plugin menu
    159162
    160163### 1.0.4
     
    169172- Split Forms and Views into separate dashboard widgets.
    170173
    171 ### 1.0.1
    172 - Added initial dashboard widgets.
    173 
    174174
    175175== Upgrade Notice ==
  • folders-4-gravity/tags/1.0.6/vendor-prefixed/autoload.php

    r3420844 r3422666  
    2020require_once __DIR__ . '/composer/autoload_real.php';
    2121
    22 return ComposerAutoloaderInitc64224000443d31aa951e25675df6b88::getLoader();
     22return ComposerAutoloaderInit4d4b64ecb88cd03a3424690392b1e3bb::getLoader();
  • folders-4-gravity/tags/1.0.6/vendor-prefixed/composer/autoload_real.php

    r3420844 r3422666  
    33// autoload_real.php @generated by Composer
    44
    5 class ComposerAutoloaderInitc64224000443d31aa951e25675df6b88
     5class ComposerAutoloaderInit4d4b64ecb88cd03a3424690392b1e3bb
    66{
    77    private static $loader;
     
    2525        require __DIR__ . '/platform_check.php';
    2626
    27         spl_autoload_register(array('ComposerAutoloaderInitc64224000443d31aa951e25675df6b88', 'loadClassLoader'), true, true);
     27        spl_autoload_register(array('ComposerAutoloaderInit4d4b64ecb88cd03a3424690392b1e3bb', 'loadClassLoader'), true, true);
    2828        self::$loader = $loader = new \F4G\Composer\Autoload\ClassLoader(\dirname(__DIR__));
    29         spl_autoload_unregister(array('ComposerAutoloaderInitc64224000443d31aa951e25675df6b88', 'loadClassLoader'));
     29        spl_autoload_unregister(array('ComposerAutoloaderInit4d4b64ecb88cd03a3424690392b1e3bb', 'loadClassLoader'));
    3030
    3131        require __DIR__ . '/autoload_static.php';
    32         call_user_func(\F4G\Composer\Autoload\ComposerStaticInitc64224000443d31aa951e25675df6b88::getInitializer($loader));
     32        call_user_func(\F4G\Composer\Autoload\ComposerStaticInit4d4b64ecb88cd03a3424690392b1e3bb::getInitializer($loader));
    3333
    3434        $loader->setClassMapAuthoritative(true);
  • folders-4-gravity/tags/1.0.6/vendor-prefixed/composer/autoload_static.php

    r3420844 r3422666  
    55namespace F4G\Composer\Autoload;
    66
    7 class ComposerStaticInitc64224000443d31aa951e25675df6b88
     7class ComposerStaticInit4d4b64ecb88cd03a3424690392b1e3bb
    88{
    99    public static $prefixLengthsPsr4 = array (
     
    3636    {
    3737        return \Closure::bind(function () use ($loader) {
    38             $loader->prefixLengthsPsr4 = ComposerStaticInitc64224000443d31aa951e25675df6b88::$prefixLengthsPsr4;
    39             $loader->prefixDirsPsr4 = ComposerStaticInitc64224000443d31aa951e25675df6b88::$prefixDirsPsr4;
    40             $loader->classMap = ComposerStaticInitc64224000443d31aa951e25675df6b88::$classMap;
     38            $loader->prefixLengthsPsr4 = ComposerStaticInit4d4b64ecb88cd03a3424690392b1e3bb::$prefixLengthsPsr4;
     39            $loader->prefixDirsPsr4 = ComposerStaticInit4d4b64ecb88cd03a3424690392b1e3bb::$prefixDirsPsr4;
     40            $loader->classMap = ComposerStaticInit4d4b64ecb88cd03a3424690392b1e3bb::$classMap;
    4141
    4242        }, null, ClassLoader::class);
  • folders-4-gravity/tags/1.0.6/vendor-prefixed/composer/installed.json

    r3420844 r3422666  
    33        {
    44            "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",
    77            "source": {
    88                "type": "git",
    99                "url": "git@github.com:Eitan-brightleaf/gravityops.git",
    10                 "reference": "a74352489b658ed6a7f1c588abb2e18aae065a68"
     10                "reference": "5d859a7cca5cf8c1e469c80a88e755fb1be7c522"
    1111            },
    1212            "dist": {
    1313                "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",
    1616                "shasum": ""
    1717            },
     
    1919                "php": ">=7.4"
    2020            },
    21             "time": "2025-12-16T09:58:09+00:00",
     21            "time": "2025-12-18T07:45:02+00:00",
    2222            "type": "library",
    2323            "installation-source": "source",
  • folders-4-gravity/tags/1.0.6/vendor-prefixed/composer/installed.php

    r3420844 r3422666  
    55    'pretty_version' => 'dev-main',
    66    'version' => 'dev-main',
    7     'reference' => 'edc84c3f8028b3fc71f3205575dd4457cad99bf7',
     7    'reference' => '58837dca80970d5fde4a728d8d9066cba6ce26a7',
    88    'type' => 'library',
    99    'install_path' => __DIR__ . '/../',
     
    1717    'gravityops/core' =>
    1818    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',
    2222      'type' => 'library',
    2323      'install_path' => __DIR__ . '/../gravityops/core',
  • folders-4-gravity/tags/1.0.6/vendor-prefixed/gravityops/core/src/Admin/AdminShell.php

    r3420844 r3422666  
    1616
    1717    /**
     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    /**
    1824     * Holds the singleton instance.
    1925     *
     
    2127     */
    2228    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;
    2337
    2438    /**
     
    3549     * Get singleton instance.
    3650     */
    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.
    3959        $shared = apply_filters( 'gravityops_shell_instance', null );
    4060        if ( $shared instanceof self ) {
    4161            return $shared;
    4262        }
     63
    4364        if ( null === self::$instance ) {
    4465            self::$instance = new self();
     
    5374     */
    5475    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").
    6377        add_filter(
    6478            'gravityops_shell_instance',
     
    6983        );
    7084
    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        }
    76109    }
    77110
     
    84117     * @return bool True if the page exists.
    85118     */
    86     public static function has_page( string $slug ): bool {
     119    public static function has_page( $slug ) {
    87120        $provider = apply_filters( 'gravityops_shell_instance', null );
    88121        if ( $provider instanceof self ) {
     
    112145     * @return array<string,array<string,string>> Tabs config array suitable to merge with register_plugin_page() tabs.
    113146     */
    114     public static function freemius_tabs( string $plugin_slug, array $labels = [] ): array {
     147    public static function freemius_tabs( $plugin_slug, $labels = [] ) {
    115148        $default_labels = [
    116149            'account'     => 'Account',
     
    164197     * @return void
    165198     */
    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 ) {
    167200        echo '<div class="gops-card">';
    168201        echo '<h2 class="gops-title" style="margin:0 0 10px;">' . esc_html( $plugin_short_title ) . ' Feeds</h2>';
     
    224257     * @return void
    225258     */
    226     public static function process_feed_toggle( string $action_prefix, string $return_url ) {
     259    public static function process_feed_toggle( $action_prefix, $return_url ) {
    227260        if ( empty( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) || 'POST' !== ( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ?? '' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
    228261            wp_safe_redirect( admin_url( $return_url ) );
     
    262295     * @return void
    263296     */
    264     public static function render_help_tab( array $links ): void {
     297    public static function render_help_tab( $links ) {
    265298        echo '<div class="gops-card">';
    266299        echo '<h2 class="gops-title" style="margin:0 0 8px;">Help</h2>';
     
    282315     * @param array  $args Page configuration.
    283316     */
    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.
    286325        $provider = apply_filters( 'gravityops_shell_instance', null );
    287326        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).
    289327            $provider->register_plugin_page( $slug, $args );
    290328            return;
     
    304342     * Add the submenu pages under the shared GravityOps parent.
    305343     */
    306     public function register_menus(): void {
     344    public function register_menus() {
    307345        if ( class_exists( __NAMESPACE__ . '\\SuiteMenu' ) ) {
    308346            SuiteMenu::ensure_parent_menu();
     
    342380     * @param string $hook Current admin page hook.
    343381     */
    344     public function enqueue_assets( $hook ): void {
     382    public function enqueue_assets( $hook ) {
    345383        $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    346384
     
    375413
    376414    /**
     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    /**
    377456     * Render the complete page including header, pills, and tab content.
    378457     *
     
    380459     * @param array  $args Page config.
    381460     */
    382     public function render_page( string $slug, array $args ): void {
     461    public function render_page( $slug, $args ) {
    383462        if ( ! current_user_can( $args['capability'] ?? 'manage_options' ) ) {
    384463            return;
     
    435514     * @return void
    436515     */
    437     private function render_header_only( array $args ): void {
     516    private function render_header_only( $args ) {
    438517        ?>
    439518        <header class="gops-header">
     
    471550     * @return array{css:string,js:string}
    472551     */
    473     public static function resolve_assets_urls(): array {
     552    public static function resolve_assets_urls() {
    474553        $base_url = apply_filters( 'gravityops_assets_base_url', '' );
    475554        $css      = '';
  • folders-4-gravity/tags/1.0.6/vendor-prefixed/gravityops/core/src/Admin/SuiteMenu.php

    r3420844 r3422666  
    3030     */
    3131    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;
    3240
    3341    /**
     
    5563     */
    5664    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;
    6477        }
    6578
     
    128141        if ( $hook ) {
    129142            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;
    131147
    132148        // Ensure assets are enqueued at the proper time and only on our screen.
     
    134150            add_action( 'admin_enqueue_scripts', [ self::class, 'enqueue_assets' ] );
    135151            self::$did_enqueue_hook = true;
    136         }
     152        }
    137153
    138154        // Register admin notices renderer (error/success messages from actions below).
     
    140156            add_action( 'admin_notices', [ self::class, 'render_admin_notices' ] );
    141157            self::$did_notice_hook = true;
    142         }
     158        }
    143159    }
    144160
     
    173189        $updates_count     = 0;
    174190
    175         // Annotate registry items with runtime state.
     191        // Annotate registry items with runtime state (supports premium/free variant pairs).
    176192        foreach ( $registry as $key => $item ) {
    177193            $type = $item['type'] ?? 'plugin';
     
    179195                continue;
    180196            }
    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;
    189274                ++$updates_count;
    190275            }
    191276
    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;
    197293        }
    198294
     
    319415                                <?php echo $is_active ? 'Active' : ( $is_installed ? 'Inactive' : 'Not Installed' ); ?>
    320416                            </span>
     417                            <?php if ( ! empty( $item['uses_free'] ) ) : ?>
     418                                <span class="gops-badge">Free</span>
     419                            <?php endif; ?>
    321420                            <?php if ( $version ) : ?>
    322421                                <span class="gops-tile__version">v<?php echo esc_html( $version ); ?></span>
     
    331430                                <?php if ( current_user_can( 'activate_plugins' ) ) : ?>
    332431                                    <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' ); ?>
    334433                                        <input type="hidden" name="gops-action" value="<?php echo $is_active ? 'deactivate' : 'activate'; ?>" />
    335                                         <input type="hidden" name="plugin" value="<?php echo esc_attr( $item['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'] ) ); ?>" />
    336435                                        <button class="button<?php echo $is_active ? '' : ' button-primary'; ?>" type="submit"><?php echo $is_active ? 'Deactivate' : 'Activate'; ?></button>
    337436                                    </form>
     
    358457     * @return string Icon URL or empty string if none found.
    359458     */
    360     private static function resolve_plugin_icon_url( array $item ): string {
     459    private static function resolve_plugin_icon_url( $item ) {
    361460        $url = apply_filters( 'gravityops_plugin_icon_url', '', $item );
    362461        if ( ! empty( $url ) ) {
     
    422521     * @return void
    423522     */
    424     public static function enqueue_assets( $hook ): void {
     523    public static function enqueue_assets( $hook ) {
    425524        $page                 = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    426525        $is_gravityops_screen = ( false !== strpos( (string) $hook, 'gravity_ops' ) ) || ( 'gravity_ops' === $page );
     
    452551     * @return void
    453552     */
    454     public static function render_admin_notices(): void {
     553    public static function render_admin_notices() {
    455554        $is_gravityops_screen = isset( $_GET['page'] ) && 'gravity_ops' === sanitize_key( wp_unslash( $_GET['page'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    456555        if ( ! $is_gravityops_screen ) {
  • folders-4-gravity/tags/1.0.6/vendor-prefixed/gravityops/core/src/SuiteRegistry.php

    r3420844 r3422666  
    2525     *               'description', 'marketing_url', 'docs_url', 'is_free', and 'icon_html'.
    2626     */
    27     public static function all(): array {
     27    public static function all() {
    2828        // NOTE: External links are placeholders. Replace with final URLs where marked.
    2929        return [
     
    5454                'slug'          => 'integrate-asana-with-gravity-forms',
    5555                '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                ],
    5661                'name'          => 'Integrate Asana with Gravity Forms',
    5762                'description'   => 'Turn form submissions into Asana tasks instantly.',
     
    6570                'slug'          => 'mass_email_notifications_for_gf',
    6671                '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                ],
    6777                'name'          => 'Mass Email Notifications',
    6878                'description'   => 'Send bulk emails to Gravity Forms entries.',
     
    7585                'type'          => 'plugin',
    7686                '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',
    7888                'name'          => 'Kanban View for GravityView',
    7989                'description'   => 'Turn GravityView into a kanban project board.',
     
    8696                'type'          => 'plugin',
    8797                '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',
    8999                'name'          => 'Recurring Form Submissions',
    90100                'description'   => 'Schedule recurring Gravity Forms submissions.',
     
    97107                'type'          => 'plugin',
    98108                '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',
    100111                'name'          => 'Global Variables',
    101112                'description'   => 'Create shared variables for GravityMath formulas.',
  • folders-4-gravity/trunk/folders-4-gravity.php

    r3420844 r3422666  
    55 * Author URI: https://brightleafdigital.io/
    66 * Description: Organize your Gravity Forms and Gravity Views by folders.
    7  * Version: 1.0.5
     7 * Version: 1.0.6
    88 * Author: BrightLeaf Digital
    99 * License: GPL-2.0+
    1010 * Requires PHP: 8.0
    1111 */
     12
     13use F4G\GravityOps\Core\Admin\AdminShell;
    1214
    1315if ( ! defined( 'ABSPATH' ) ) {
     
    1618
    1719require_once __DIR__ . '/vendor-prefixed/autoload.php';
     20
     21require_once __DIR__ . '/vendor/autoload.php';
     22
     23// Instantiate this plugin's copy of the AdminShell early so provider negotiation can happen on plugins_loaded.
     24add_action(
     25    'plugins_loaded',
     26    function () {
     27        AdminShell::instance();
     28    },
     29    1
     30);
    1831
    1932
     
    2841    return;
    2942}
     43
     44// Ensure GravityOps shared assets resolve when library is vendor-installed in this plugin.
     45add_filter(
     46    'gravityops_assets_base_url',
     47    function ( $url ) {
     48        return $url ?: plugins_url( 'vendor-prefixed/gravityops/core/assets/', __FILE__ );
     49    }
     50);
    3051
    3152add_action(
  • folders-4-gravity/trunk/includes/class-gravity-ops-form-folders.php

    r3420844 r3422666  
    66use F4G\GravityOps\Core\Admin\AdminShell;
    77use F4G\GravityOps\Core\Utils\AssetHelper as Assets;
     8use function F4G\GravityOps\Core\Admin\gravityops_shell;
    89
    910if ( ! defined( 'ABSPATH' ) ) {
     
    164165        // Register the GravityOps AdminShell page for the free Folders plugin.
    165166        // Tabs: Overview (render), Help (render), Affiliation (external link)
    166         AdminShell::instance()->register_plugin_page(
     167        gravityops_shell()->register_plugin_page(
    167168            'folders-4-gravity',
    168169            [
  • folders-4-gravity/trunk/readme.txt

    r3420844 r3422666  
    44Requires at least: 6.5
    55Tested up to: 6.9
    6 Stable tag: 1.0.5
     6Stable tag: 1.0.6
    77Requires PHP: 8.0
    88License: GPLv2
     
    155155== Changelog ==
    156156
     157### 1.0.6
     158- Fixed a bug with new admin menu
     159
    157160### 1.0.5
    158 - =Updated plugin menu
     161- Updated plugin menu
    159162
    160163### 1.0.4
     
    169172- Split Forms and Views into separate dashboard widgets.
    170173
    171 ### 1.0.1
    172 - Added initial dashboard widgets.
    173 
    174174
    175175== Upgrade Notice ==
  • folders-4-gravity/trunk/vendor-prefixed/autoload.php

    r3420844 r3422666  
    2020require_once __DIR__ . '/composer/autoload_real.php';
    2121
    22 return ComposerAutoloaderInitc64224000443d31aa951e25675df6b88::getLoader();
     22return ComposerAutoloaderInit4d4b64ecb88cd03a3424690392b1e3bb::getLoader();
  • folders-4-gravity/trunk/vendor-prefixed/composer/autoload_real.php

    r3420844 r3422666  
    33// autoload_real.php @generated by Composer
    44
    5 class ComposerAutoloaderInitc64224000443d31aa951e25675df6b88
     5class ComposerAutoloaderInit4d4b64ecb88cd03a3424690392b1e3bb
    66{
    77    private static $loader;
     
    2525        require __DIR__ . '/platform_check.php';
    2626
    27         spl_autoload_register(array('ComposerAutoloaderInitc64224000443d31aa951e25675df6b88', 'loadClassLoader'), true, true);
     27        spl_autoload_register(array('ComposerAutoloaderInit4d4b64ecb88cd03a3424690392b1e3bb', 'loadClassLoader'), true, true);
    2828        self::$loader = $loader = new \F4G\Composer\Autoload\ClassLoader(\dirname(__DIR__));
    29         spl_autoload_unregister(array('ComposerAutoloaderInitc64224000443d31aa951e25675df6b88', 'loadClassLoader'));
     29        spl_autoload_unregister(array('ComposerAutoloaderInit4d4b64ecb88cd03a3424690392b1e3bb', 'loadClassLoader'));
    3030
    3131        require __DIR__ . '/autoload_static.php';
    32         call_user_func(\F4G\Composer\Autoload\ComposerStaticInitc64224000443d31aa951e25675df6b88::getInitializer($loader));
     32        call_user_func(\F4G\Composer\Autoload\ComposerStaticInit4d4b64ecb88cd03a3424690392b1e3bb::getInitializer($loader));
    3333
    3434        $loader->setClassMapAuthoritative(true);
  • folders-4-gravity/trunk/vendor-prefixed/composer/autoload_static.php

    r3420844 r3422666  
    55namespace F4G\Composer\Autoload;
    66
    7 class ComposerStaticInitc64224000443d31aa951e25675df6b88
     7class ComposerStaticInit4d4b64ecb88cd03a3424690392b1e3bb
    88{
    99    public static $prefixLengthsPsr4 = array (
     
    3636    {
    3737        return \Closure::bind(function () use ($loader) {
    38             $loader->prefixLengthsPsr4 = ComposerStaticInitc64224000443d31aa951e25675df6b88::$prefixLengthsPsr4;
    39             $loader->prefixDirsPsr4 = ComposerStaticInitc64224000443d31aa951e25675df6b88::$prefixDirsPsr4;
    40             $loader->classMap = ComposerStaticInitc64224000443d31aa951e25675df6b88::$classMap;
     38            $loader->prefixLengthsPsr4 = ComposerStaticInit4d4b64ecb88cd03a3424690392b1e3bb::$prefixLengthsPsr4;
     39            $loader->prefixDirsPsr4 = ComposerStaticInit4d4b64ecb88cd03a3424690392b1e3bb::$prefixDirsPsr4;
     40            $loader->classMap = ComposerStaticInit4d4b64ecb88cd03a3424690392b1e3bb::$classMap;
    4141
    4242        }, null, ClassLoader::class);
  • folders-4-gravity/trunk/vendor-prefixed/composer/installed.json

    r3420844 r3422666  
    33        {
    44            "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",
    77            "source": {
    88                "type": "git",
    99                "url": "git@github.com:Eitan-brightleaf/gravityops.git",
    10                 "reference": "a74352489b658ed6a7f1c588abb2e18aae065a68"
     10                "reference": "5d859a7cca5cf8c1e469c80a88e755fb1be7c522"
    1111            },
    1212            "dist": {
    1313                "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",
    1616                "shasum": ""
    1717            },
     
    1919                "php": ">=7.4"
    2020            },
    21             "time": "2025-12-16T09:58:09+00:00",
     21            "time": "2025-12-18T07:45:02+00:00",
    2222            "type": "library",
    2323            "installation-source": "source",
  • folders-4-gravity/trunk/vendor-prefixed/composer/installed.php

    r3420844 r3422666  
    55    'pretty_version' => 'dev-main',
    66    'version' => 'dev-main',
    7     'reference' => 'edc84c3f8028b3fc71f3205575dd4457cad99bf7',
     7    'reference' => '58837dca80970d5fde4a728d8d9066cba6ce26a7',
    88    'type' => 'library',
    99    'install_path' => __DIR__ . '/../',
     
    1717    'gravityops/core' =>
    1818    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',
    2222      'type' => 'library',
    2323      'install_path' => __DIR__ . '/../gravityops/core',
  • folders-4-gravity/trunk/vendor-prefixed/gravityops/core/src/Admin/AdminShell.php

    r3420844 r3422666  
    1616
    1717    /**
     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    /**
    1824     * Holds the singleton instance.
    1925     *
     
    2127     */
    2228    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;
    2337
    2438    /**
     
    3549     * Get singleton instance.
    3650     */
    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.
    3959        $shared = apply_filters( 'gravityops_shell_instance', null );
    4060        if ( $shared instanceof self ) {
    4161            return $shared;
    4262        }
     63
    4364        if ( null === self::$instance ) {
    4465            self::$instance = new self();
     
    5374     */
    5475    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").
    6377        add_filter(
    6478            'gravityops_shell_instance',
     
    6983        );
    7084
    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        }
    76109    }
    77110
     
    84117     * @return bool True if the page exists.
    85118     */
    86     public static function has_page( string $slug ): bool {
     119    public static function has_page( $slug ) {
    87120        $provider = apply_filters( 'gravityops_shell_instance', null );
    88121        if ( $provider instanceof self ) {
     
    112145     * @return array<string,array<string,string>> Tabs config array suitable to merge with register_plugin_page() tabs.
    113146     */
    114     public static function freemius_tabs( string $plugin_slug, array $labels = [] ): array {
     147    public static function freemius_tabs( $plugin_slug, $labels = [] ) {
    115148        $default_labels = [
    116149            'account'     => 'Account',
     
    164197     * @return void
    165198     */
    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 ) {
    167200        echo '<div class="gops-card">';
    168201        echo '<h2 class="gops-title" style="margin:0 0 10px;">' . esc_html( $plugin_short_title ) . ' Feeds</h2>';
     
    224257     * @return void
    225258     */
    226     public static function process_feed_toggle( string $action_prefix, string $return_url ) {
     259    public static function process_feed_toggle( $action_prefix, $return_url ) {
    227260        if ( empty( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) || 'POST' !== ( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ?? '' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
    228261            wp_safe_redirect( admin_url( $return_url ) );
     
    262295     * @return void
    263296     */
    264     public static function render_help_tab( array $links ): void {
     297    public static function render_help_tab( $links ) {
    265298        echo '<div class="gops-card">';
    266299        echo '<h2 class="gops-title" style="margin:0 0 8px;">Help</h2>';
     
    282315     * @param array  $args Page configuration.
    283316     */
    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.
    286325        $provider = apply_filters( 'gravityops_shell_instance', null );
    287326        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).
    289327            $provider->register_plugin_page( $slug, $args );
    290328            return;
     
    304342     * Add the submenu pages under the shared GravityOps parent.
    305343     */
    306     public function register_menus(): void {
     344    public function register_menus() {
    307345        if ( class_exists( __NAMESPACE__ . '\\SuiteMenu' ) ) {
    308346            SuiteMenu::ensure_parent_menu();
     
    342380     * @param string $hook Current admin page hook.
    343381     */
    344     public function enqueue_assets( $hook ): void {
     382    public function enqueue_assets( $hook ) {
    345383        $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    346384
     
    375413
    376414    /**
     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    /**
    377456     * Render the complete page including header, pills, and tab content.
    378457     *
     
    380459     * @param array  $args Page config.
    381460     */
    382     public function render_page( string $slug, array $args ): void {
     461    public function render_page( $slug, $args ) {
    383462        if ( ! current_user_can( $args['capability'] ?? 'manage_options' ) ) {
    384463            return;
     
    435514     * @return void
    436515     */
    437     private function render_header_only( array $args ): void {
     516    private function render_header_only( $args ) {
    438517        ?>
    439518        <header class="gops-header">
     
    471550     * @return array{css:string,js:string}
    472551     */
    473     public static function resolve_assets_urls(): array {
     552    public static function resolve_assets_urls() {
    474553        $base_url = apply_filters( 'gravityops_assets_base_url', '' );
    475554        $css      = '';
  • folders-4-gravity/trunk/vendor-prefixed/gravityops/core/src/Admin/SuiteMenu.php

    r3420844 r3422666  
    3030     */
    3131    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;
    3240
    3341    /**
     
    5563     */
    5664    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;
    6477        }
    6578
     
    128141        if ( $hook ) {
    129142            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;
    131147
    132148        // Ensure assets are enqueued at the proper time and only on our screen.
     
    134150            add_action( 'admin_enqueue_scripts', [ self::class, 'enqueue_assets' ] );
    135151            self::$did_enqueue_hook = true;
    136         }
     152        }
    137153
    138154        // Register admin notices renderer (error/success messages from actions below).
     
    140156            add_action( 'admin_notices', [ self::class, 'render_admin_notices' ] );
    141157            self::$did_notice_hook = true;
    142         }
     158        }
    143159    }
    144160
     
    173189        $updates_count     = 0;
    174190
    175         // Annotate registry items with runtime state.
     191        // Annotate registry items with runtime state (supports premium/free variant pairs).
    176192        foreach ( $registry as $key => $item ) {
    177193            $type = $item['type'] ?? 'plugin';
     
    179195                continue;
    180196            }
    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;
    189274                ++$updates_count;
    190275            }
    191276
    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;
    197293        }
    198294
     
    319415                                <?php echo $is_active ? 'Active' : ( $is_installed ? 'Inactive' : 'Not Installed' ); ?>
    320416                            </span>
     417                            <?php if ( ! empty( $item['uses_free'] ) ) : ?>
     418                                <span class="gops-badge">Free</span>
     419                            <?php endif; ?>
    321420                            <?php if ( $version ) : ?>
    322421                                <span class="gops-tile__version">v<?php echo esc_html( $version ); ?></span>
     
    331430                                <?php if ( current_user_can( 'activate_plugins' ) ) : ?>
    332431                                    <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' ); ?>
    334433                                        <input type="hidden" name="gops-action" value="<?php echo $is_active ? 'deactivate' : 'activate'; ?>" />
    335                                         <input type="hidden" name="plugin" value="<?php echo esc_attr( $item['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'] ) ); ?>" />
    336435                                        <button class="button<?php echo $is_active ? '' : ' button-primary'; ?>" type="submit"><?php echo $is_active ? 'Deactivate' : 'Activate'; ?></button>
    337436                                    </form>
     
    358457     * @return string Icon URL or empty string if none found.
    359458     */
    360     private static function resolve_plugin_icon_url( array $item ): string {
     459    private static function resolve_plugin_icon_url( $item ) {
    361460        $url = apply_filters( 'gravityops_plugin_icon_url', '', $item );
    362461        if ( ! empty( $url ) ) {
     
    422521     * @return void
    423522     */
    424     public static function enqueue_assets( $hook ): void {
     523    public static function enqueue_assets( $hook ) {
    425524        $page                 = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    426525        $is_gravityops_screen = ( false !== strpos( (string) $hook, 'gravity_ops' ) ) || ( 'gravity_ops' === $page );
     
    452551     * @return void
    453552     */
    454     public static function render_admin_notices(): void {
     553    public static function render_admin_notices() {
    455554        $is_gravityops_screen = isset( $_GET['page'] ) && 'gravity_ops' === sanitize_key( wp_unslash( $_GET['page'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    456555        if ( ! $is_gravityops_screen ) {
  • folders-4-gravity/trunk/vendor-prefixed/gravityops/core/src/SuiteRegistry.php

    r3420844 r3422666  
    2525     *               'description', 'marketing_url', 'docs_url', 'is_free', and 'icon_html'.
    2626     */
    27     public static function all(): array {
     27    public static function all() {
    2828        // NOTE: External links are placeholders. Replace with final URLs where marked.
    2929        return [
     
    5454                'slug'          => 'integrate-asana-with-gravity-forms',
    5555                '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                ],
    5661                'name'          => 'Integrate Asana with Gravity Forms',
    5762                'description'   => 'Turn form submissions into Asana tasks instantly.',
     
    6570                'slug'          => 'mass_email_notifications_for_gf',
    6671                '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                ],
    6777                'name'          => 'Mass Email Notifications',
    6878                'description'   => 'Send bulk emails to Gravity Forms entries.',
     
    7585                'type'          => 'plugin',
    7686                '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',
    7888                'name'          => 'Kanban View for GravityView',
    7989                'description'   => 'Turn GravityView into a kanban project board.',
     
    8696                'type'          => 'plugin',
    8797                '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',
    8999                'name'          => 'Recurring Form Submissions',
    90100                'description'   => 'Schedule recurring Gravity Forms submissions.',
     
    97107                'type'          => 'plugin',
    98108                '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',
    100111                'name'          => 'Global Variables',
    101112                'description'   => 'Create shared variables for GravityMath formulas.',
Note: See TracChangeset for help on using the changeset viewer.