Plugin Directory

Changeset 3768035


Ignore:
Timestamp:
10/27/2026 08:36:17 AM (74 minutes ago)
Author:
storelly
Message:

Update to version 1.9.0 from GitHub

Location:
storelly-product-builder-for-woocommerce
Files:
4 added
32 edited
1 copied

Legend:

Unmodified
Added
Removed
  • storelly-product-builder-for-woocommerce/tags/1.9.0/includes/admin/class-admin-menu.php

    r3568219 r3768035  
    99 *   1. Re-orders the existing submenu items into three task bands
    1010 *      (BUILD / SELL / CONFIGURE) and injects non-clickable section headings.
    11  *   2. Swaps the washed-out PNG menu icon for a crisp inline SVG.
     11 *   2. Swaps the washed-out PNG menu icon for an SVG mask drawn in currentColor.
    1212 *
    1313 * Safety: the re-order is loss-proof — any submenu item not named in the
    … …  
    6464                protected static function desired_order() {
    6565                        $opts   = defined( 'SPBWC_PB_OPTIONS_SLUG' ) ? SPBWC_PB_OPTIONS_SLUG : 'storelly-product-builder-for-woocommerce-options';
    66                         $emails = defined( 'SPBWC_PB_EMAILS_SLUG' ) ? SPBWC_PB_EMAILS_SLUG : 'storelly-product-builder-for-woocommerce-emails';
    6766
    6867                        return array(
    … …  
    8584                                array( 'heading', __( 'Configure', 'storelly-product-builder-for-woocommerce' ), 'configure' ),
    8685                                $opts,
    87                                 SPBWC_PB_LICENSE_SLUG,
    88                                 $emails,
    89                                 $opts . '/global-import',
    9086                        );
    9187                }
    … …  
    135131                        }
    136132
     133                        // Pages folded into Settings tabs stay registered (their URLs, caps and
     134                        // asset hooks are unchanged) but are hidden from the menu by class.
     135                        $hidden = class_exists( 'SPBWC_Settings_Tabs' ) ? SPBWC_Settings_Tabs::moved_pages() : array();
     136
    137137                        // Loss-proof: append anything we did not explicitly place.
    138138                        foreach ( $submenu[ $parent ] as $item ) {
    139139                                $slug = isset( $item[2] ) ? $item[2] : '';
    140140                                if ( '' !== $slug && empty( $used[ $slug ] ) ) {
     141                                        if ( in_array( $slug, $hidden, true ) ) {
     142                                                $item[4] = ( isset( $item[4] ) ? $item[4] . ' ' : '' ) . SPBWC_Settings_Tabs::HIDDEN_CLASS;
     143                                        }
    141144                                        $ordered[] = $item;
    142145                                }
    … …  
    147150
    148151                /**
    149                  * Replace the top-level menu icon (index 6 of its $menu row) with a
    150                  * crisp inline SVG so it reads cleanly on the dark admin sidebar.
     152                 * Replace the top-level menu icon (index 6 of its $menu row) with 'none':
     153                 * WP then prints an empty .wp-menu-image that admin-menu.css fills with
     154                 * the Storelly mark as a CSS mask in currentColor, so the icon follows
     155                 * WP's idle / hover / current colours like a dashicon.
    151156                 *
    152157                 * @param array  $menu   The global $menu (by reference via global).
    … …  
    157162                                return;
    158163                        }
    159                         $svg = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a7aaad' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polygon points='12 2 2 7 12 12 22 7 12 2'/><polyline points='2 17 12 22 22 17'/><polyline points='2 12 12 17 22 12'/></svg>";
    160                         $uri = 'data:image/svg+xml;base64,' . base64_encode( str_replace( '%23', '#', $svg ) );
    161164                        foreach ( $menu as $key => $row ) {
    162165                                if ( isset( $row[2] ) && $row[2] === $parent && isset( $menu[ $key ][6] ) ) {
    163                                         $menu[ $key ][6] = $uri;
     166                                        $menu[ $key ][6] = 'none';
    164167                                        break;
    165168                                }
  • storelly-product-builder-for-woocommerce/tags/1.9.0/includes/class-admin-options.php

    r3761204 r3768035  
    4040            // REST API — Pricing Options grid (GET, read-only, faster than admin-ajax.php).
    4141            add_action( 'rest_api_init', array( $this, 'spbwc_register_rest_routes' ) );
     42            // Menu badge cache (Custom Orders to fulfil): drop it when an order moves.
     43            add_action( 'woocommerce_order_status_changed', array( __CLASS__, 'flush_custom_order_count' ) );
     44            add_action( 'woocommerce_new_order', array( __CLASS__, 'flush_custom_order_count' ) );
    4245        }
    4346        public function spbwc_ajax() {
    … …  
    7073                'spbwc_swap_product_option'      => false, // admin only
    7174                'spbwc_unlink_product_option'    => false, // admin only
     75                // Overview: refresh the Storelly stats after the page has painted (A11)
     76                'spbwc_overview_stats'           => false, // admin only
    7277            );
    7378            foreach ($ajax_events as $ajax_event => $nopriv) {
    … …  
    294299                add_submenu_page(
    295300                    SPBWC_PB_OVERVIEW_SLUG,
    296                     esc_html__('General Settings', 'storelly-product-builder-for-woocommerce'),
    297                     esc_html__('General Settings', 'storelly-product-builder-for-woocommerce'),
     301                    esc_html__('Settings', 'storelly-product-builder-for-woocommerce'),
     302                    esc_html__('Settings', 'storelly-product-builder-for-woocommerce'),
    298303                    'manage_options',
    299304                    SPBWC_PB_OPTIONS_SLUG,
    … …  
    324329                    array($this, 'spbwc_products_manager')
    325330                );
     331                // Same bubble as Quotes and B2B: custom orders paid and waiting to be made.
     332                $spbwc_orders_menu_title = esc_html__('Custom Orders', 'storelly-product-builder-for-woocommerce');
     333                $spbwc_orders_to_fulfil  = self::count_custom_orders_to_fulfil();
     334                if ( $spbwc_orders_to_fulfil > 0 ) {
     335                    $spbwc_orders_menu_title .= ' <span class="awaiting-mod"><span class="pending-count">' . esc_html( number_format_i18n( $spbwc_orders_to_fulfil ) ) . '</span></span>';
     336                }
    326337                add_submenu_page(
    327338                    SPBWC_PB_OVERVIEW_SLUG,
    328339                    esc_html__('Custom Orders', 'storelly-product-builder-for-woocommerce'),
    329                     esc_html__('Custom Orders', 'storelly-product-builder-for-woocommerce'),
     340                    $spbwc_orders_menu_title,
    330341                    'manage_options',
    331342                    SPBWC_PB_ORDERS_SLUG,
    … …  
    382393                    array($this, 'spbwc_global_import')
    383394                );
     395                // System and About sit under the Storelly menu (hidden there; reached
     396                // through the Settings tab bar) so WP opens that menu on them.
    384397                add_submenu_page(
    385                     SPBWC_PB_OPTIONS_SLUG,
     398                    SPBWC_PB_OVERVIEW_SLUG,
    386399                    esc_html__('System Infor', 'storelly-product-builder-for-woocommerce'),
    387400                    esc_html__('System Infor', 'storelly-product-builder-for-woocommerce'),
    … …  
    391404                );
    392405                add_submenu_page(
    393                     SPBWC_PB_OPTIONS_SLUG,
     406                    SPBWC_PB_OVERVIEW_SLUG,
    394407                    esc_html__('About', 'storelly-product-builder-for-woocommerce'),
    395408                    esc_html__('About', 'storelly-product-builder-for-woocommerce'),
    … …  
    511524            if ( $hook === 'toplevel_page_' . SPBWC_PB_OVERVIEW_SLUG ) {
    512525                wp_enqueue_style('spbwc-overview-css');
     526                // Fills in the Storelly numbers when they were not cached yet (A11).
     527                $spbwc_stats = class_exists( 'SPBWC_License_Manager' ) ? SPBWC_License_Manager::get_cached_overview_stats() : null;
     528                if ( is_wp_error( $spbwc_stats ) && 'no_cache' === $spbwc_stats->get_error_code() ) {
     529                    wp_enqueue_script( 'spbwc-overview-stats', SPBWC_PB_JS_URL . 'overview-stats.js', array(), SPBWC_PB_VERSION, true );
     530                    wp_localize_script( 'spbwc-overview-stats', 'spbwcOverviewStats', array(
     531                        'ajaxUrl'  => admin_url( 'admin-ajax.php' ),
     532                        'nonce'    => wp_create_nonce( 'spbwc_overview_stats' ),
     533                        'subtitle' => __( 'Live numbers from your Storelly dashboard. Each card links straight to the related management screen.', 'storelly-product-builder-for-woocommerce' ),
     534                    ) );
     535                }
    513536            }
    514537
    … …  
    19601983                    </div>
    19611984                </header>
    1962                 <?php // Anchor so WP admin notices land below the hero, not on it. ?>
    1963                 <hr class="wp-header-end" />
     1985                <?php if ( class_exists( 'SPBWC_Settings_Tabs' ) ) { SPBWC_Settings_Tabs::render( 'system' ); } ?>
    19641986
    19651987                <div class="spbwc-block">
    … …  
    20262048                    </div>
    20272049                </header>
    2028                 <?php // Anchor so WP admin notices land below the hero, not on it. ?>
    2029                 <hr class="wp-header-end" />
     2050                <?php if ( class_exists( 'SPBWC_Settings_Tabs' ) ) { SPBWC_Settings_Tabs::render( 'about' ); } ?>
    20302051
    20312052                <!-- Brand intro -->
    … …  
    20922113            <?php
    20932114        }
     2115        /** Transient holding the Custom Orders menu badge count. */
     2116        const CUSTOM_ORDER_COUNT_KEY = 'spbwc_custom_orders_to_fulfil';
     2117
     2118        /**
     2119         * Orders in "processing" (paid, not yet shipped) that contain at least one
     2120         * builder line item. Runs on every admin page for the menu badge, so it is
     2121         * cached for 10 minutes and dropped whenever an order changes status.
     2122         *
     2123         * @return int
     2124         */
     2125        public static function count_custom_orders_to_fulfil() {
     2126            $cached = get_transient( self::CUSTOM_ORDER_COUNT_KEY );
     2127            if ( false !== $cached ) {
     2128                return (int) $cached;
     2129            }
     2130            $count = 0;
     2131            if ( function_exists( 'wc_get_orders' ) ) {
     2132                $ids = wc_get_orders( array( 'status' => array( 'processing' ), 'return' => 'ids', 'limit' => 500 ) );
     2133                if ( ! empty( $ids ) ) {
     2134                    global $wpdb; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- Global variable $wpdb.
     2135                    $ids          = array_map( 'absint', $ids );
     2136                    $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
     2137                    // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Cached in a transient above; table names from $wpdb, ids via placeholders.
     2138                    $count = (int) $wpdb->get_var(
     2139                        $wpdb->prepare(
     2140                            "SELECT COUNT(DISTINCT oi.order_id)
     2141                            FROM {$wpdb->prefix}woocommerce_order_items oi
     2142                            INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta oim ON oi.order_item_id = oim.order_item_id
     2143                            WHERE oim.meta_key = '_pcpb_option_price'
     2144                            AND oi.order_item_type = 'line_item'
     2145                            AND oi.order_id IN ({$placeholders})",
     2146                            $ids
     2147                        )
     2148                    );
     2149                    // phpcs:enable
     2150                }
     2151            }
     2152            set_transient( self::CUSTOM_ORDER_COUNT_KEY, $count, 10 * MINUTE_IN_SECONDS );
     2153            return $count;
     2154        }
     2155
     2156        /** Drop the cached Custom Orders badge count. */
     2157        public static function flush_custom_order_count() {
     2158            delete_transient( self::CUSTOM_ORDER_COUNT_KEY );
     2159        }
     2160
    20942161        public function spbwc_orders_manager() {
    20952162            if ( ! current_user_can( 'spbwc_manage_product_builder' ) ) {
    … …  
    44884555
    44894556            // --- Remote (Storelly) stats overlay ---
    4490             $remote_stats = SPBWC_License_Manager::get_overview_stats();
     4557            // Cache only: the API call moved to the spbwc_overview_stats AJAX (A11), so a
     4558            // slow or unreachable Storelly never holds up this page.
     4559            $remote_stats = SPBWC_License_Manager::get_cached_overview_stats();
    44914560
    44924561            // --- License info ---
    … …  
    45044573
    45054574            include_once( SPBWC_PB_PLUGIN_DIR . 'views/overview.php' );
     4575        }
     4576
     4577        /**
     4578         * AJAX: fetch the Storelly overview stats after the Overview page has
     4579         * painted (A11). The result is cached by get_overview_stats().
     4580         * wp_ajax_spbwc_overview_stats
     4581         */
     4582        public function spbwc_overview_stats() {
     4583            if ( ! current_user_can( 'manage_options' ) ) {
     4584                wp_send_json_error( null, 403 );
     4585            }
     4586            check_ajax_referer( 'spbwc_overview_stats', 'nonce' );
     4587            $stats = SPBWC_License_Manager::get_overview_stats();
     4588            if ( is_wp_error( $stats ) ) {
     4589                wp_send_json_error( array( 'code' => $stats->get_error_code() ) );
     4590            }
     4591            wp_send_json_success( array(
     4592                'products' => number_format_i18n( (int) $stats['total_products'] ),
     4593                'orders'   => number_format_i18n( (int) $stats['total_orders'] ),
     4594                'quotes'   => number_format_i18n( (int) $stats['total_quotes'] ),
     4595            ) );
    45064596        }
    45074597
  • storelly-product-builder-for-woocommerce/tags/1.9.0/includes/class-global-import-admin.php

    r3568219 r3768035  
    102102                    </div>
    103103                </header>
     104                <?php if ( class_exists( 'SPBWC_Settings_Tabs' ) ) { SPBWC_Settings_Tabs::render( 'setup' ); } ?>
    104105                <div id="spbwc-global-import-app"></div>
    105106            </div>
  • storelly-product-builder-for-woocommerce/tags/1.9.0/includes/class-license-manager.php

    r3761204 r3768035  
    225225         * @return array|WP_Error
    226226         */
     227        /**
     228         * Overview stats from the cache only - never calls the API, so the
     229         * Overview page can render at once (A11). WP_Error 'no_cache' means the
     230         * store is connected but the numbers have to be refreshed (by AJAX).
     231         *
     232         * @return array|WP_Error
     233         */
     234        public static function get_cached_overview_stats() {
     235            $cached = get_transient( self::TRANSIENT_OVERVIEW_STATS );
     236            if ( false !== $cached ) {
     237                return $cached;
     238            }
     239            $api_keys = get_option( 'spbwc_connect_api_keys', array() );
     240            if ( empty( $api_keys['business_id'] ) ) {
     241                return new WP_Error( 'no_business', __( 'No business ID configured.', 'storelly-product-builder-for-woocommerce' ) );
     242            }
     243            if ( get_transient( self::TRANSIENT_OVERVIEW_STATS . '_failed' ) ) {
     244                return new WP_Error( 'api_error', __( 'Could not retrieve overview stats.', 'storelly-product-builder-for-woocommerce' ) );
     245            }
     246            return new WP_Error( 'no_cache', __( 'Overview stats not cached yet.', 'storelly-product-builder-for-woocommerce' ) );
     247        }
     248
    227249        public static function get_overview_stats() {
    228250            $cached = get_transient( self::TRANSIENT_OVERVIEW_STATS );
    … …  
    242264
    243265            if ( is_wp_error( $resp ) || empty( $resp['success'] ) ) {
     266                // Remember the failure for 5 minutes so every Overview visit does not retry.
     267                set_transient( self::TRANSIENT_OVERVIEW_STATS . '_failed', 1, 5 * MINUTE_IN_SECONDS );
    244268                return new WP_Error( 'api_error', __( 'Could not retrieve overview stats.', 'storelly-product-builder-for-woocommerce' ) );
    245269            }
  • storelly-product-builder-for-woocommerce/tags/1.9.0/includes/email/class-email-admin.php

    r3568219 r3768035  
    259259                    </div>
    260260                </header>
    261                 <?php // WP relocates admin notices to right after the first <h1>; this marker
    262                       // makes them land below the hero instead of on top of the gradient. ?>
    263                 <hr class="wp-header-end" />
     261                <?php if ( class_exists( 'SPBWC_Settings_Tabs' ) ) { SPBWC_Settings_Tabs::render( 'emails' ); } ?>
    264262
    265263                <?php if ( isset( $_GET['spbwc_test_sent'] ) ) : // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display-only flash. ?>
  • storelly-product-builder-for-woocommerce/tags/1.9.0/readme.txt

    r3761204 r3768035  
    77Requires PHP: 7.4
    88Requires Plugins: woocommerce
    9 Stable tag: 1.8.0
     9Stable tag: 1.9.0
    1010License: GPLv2 or later
    1111License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    238238== Changelog ==
    239239
     240= 1.9.0 =
     241* **New: one Settings area with tabs.** Settings, Emails, Cloud & License, Setup Wizard, System and About now share one tab bar, and the Storelly menu shows a single "Settings" item. Every old link and bookmark keeps working.
     242* **Faster Overview.** The Overview no longer waits for Storelly Cloud before it shows; the cloud numbers fill in afterwards without moving the page, and a failed connection is not retried on every visit.
     243* New: a count badge on Custom Orders for paid orders that are waiting to be made.
     244* New: Setup Wizard card on the Overview; "License & billing" is now "Account & Plan".
     245* The Storelly menu icon now follows the admin colour scheme like other menu icons.
     246* Fixed: notices from other plugins no longer appear inside the page header on Settings, Account & Plan, Setup Wizard and the option editor.
     247
    240248= 1.8.0 =
    241249* **New: Download PDF always gives you a file.** On the free plan it now builds a preview PDF on your own site, marked "Made with Storelly", and offers the clean print-ready version through Storelly Cloud. Nothing is sent off-site for the preview.
    … …  
    367375== Upgrade Notice ==
    368376
     377= 1.9.0 =
     378Settings, Emails, Account & Plan, Setup Wizard, System and About are now tabs of one Settings area, and the Overview opens without waiting for Storelly Cloud.
     379
    369380= 1.8.0 =
    370381Free stores now get a watermarked preview when they click Download PDF, the upgrade notice only shows after you use a Cloud feature, and the quote form's Select field is a real dropdown.
  • storelly-product-builder-for-woocommerce/tags/1.9.0/static/css/admin-menu.css

    r3568219 r3768035  
    5959        vertical-align: top;
    6060}
     61
     62/* === Pages folded into Settings tabs (S2) ===================== */
     63/* Still registered (URLs, caps and asset hooks unchanged), only    */
     64/* hidden from the submenu; reached through the Settings tab bar.   */
     65#adminmenu .wp-submenu li.spbwc-menu-hidden {
     66        display: none;
     67}
     68
     69/* === Menu icon as a mask (S2) ================================= */
     70/* The icon is drawn with currentColor, so it follows WP's idle,    */
     71/* hover and current colours like a dashicon (a data-URI <img> is   */
     72/* only repainted by WP when it uses fill="", not stroke).          */
     73#adminmenu .toplevel_page_storelly-product-builder-for-woocommerce-overview div.wp-menu-image::before {
     74        content: "";
     75        display: block;
     76        width: 20px;
     77        height: 20px;
     78        margin: 7px auto 0;
     79        padding: 0;
     80        background-color: currentColor;
     81        -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolygon points='12 2 2 7 12 12 22 7 12 2'/%3E%3Cpolyline points='2 17 12 22 22 17'/%3E%3Cpolyline points='2 12 12 17 22 12'/%3E%3C/svg%3E") center / 20px 20px no-repeat;
     82        mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolygon points='12 2 2 7 12 12 22 7 12 2'/%3E%3Cpolyline points='2 17 12 22 22 17'/%3E%3Cpolyline points='2 12 12 17 22 12'/%3E%3C/svg%3E") center / 20px 20px no-repeat;
     83}
  • storelly-product-builder-for-woocommerce/tags/1.9.0/static/css/overview.css

    r3568219 r3768035  
    188188    padding: 2px 8px;
    189189    background: var(--st-brand, #1d4ed8);
    190     color: #fff;
     190    color: var(--nbd-st-text-inverse, #fff);
    191191    font-size: 11px;
    192192    font-weight: 700;
    … …  
    202202    height: 40px;
    203203    border-radius: var(--nbd-radius, 10px);
    204     background: #fff;
     204    background: var(--nbd-st-bg, #fff);
    205205    border: 1px solid var(--nbd-st-border, #e2e8f0);
    206206    color: var(--st-brand, #1d4ed8);
    … …  
    373373    .spbwc-welcome__cloud-row { flex-direction: column; }
    374374}
     375
     376/* A11: numbers refreshed after paint keep the same box (digits of equal width,
     377 * one line reserved), so a late update never shifts the cards. */
     378.spbwc-stat-card__value {
     379    font-variant-numeric: tabular-nums;
     380    min-height: 1.2em;
     381}
     382
     383/* The "Local data" badge is hidden by overview-stats.js once live numbers arrive;
     384 * its display:inline-flex would otherwise win over the hidden attribute. */
     385.spbwc-page-hero__badge[hidden] {
     386    display: none;
     387}
  • storelly-product-builder-for-woocommerce/tags/1.9.0/static/css/storelly-admin-ui.css

    r3761204 r3768035  
    18781878    margin: var(--nbd-space-2) 0 0;
    18791879}
     1880
     1881/* === Settings tab bar (S2 / U2) ===============================
     1882 * One bar under the hero of every Settings page (General, Emails,
     1883 * Cloud & License, Setup Wizard, System, About). Pages keep their own
     1884 * slugs; the bar makes them read as one tabbed Settings area.
     1885 * ============================================================= */
     1886.spbwc-settings-tabbar {
     1887    display: flex;
     1888    flex-wrap: wrap;
     1889    gap: var(--nbd-space-1);
     1890    margin-block: calc(-1 * var(--nbd-space-2)) var(--nbd-space-6);
     1891    border-block-end: 1px solid var(--nbd-st-border-light);
     1892}
     1893
     1894.spbwc-settings-tabbar__tab {
     1895    display: inline-flex;
     1896    align-items: center;
     1897    gap: var(--nbd-space-2);
     1898    padding: var(--nbd-space-3) var(--nbd-space-4);
     1899    margin-block-end: -1px;
     1900    border-block-end: 2px solid transparent;
     1901    color: var(--nbd-st-text-soft);
     1902    font-size: var(--text-md);
     1903    font-weight: var(--font-semibold, 600);
     1904    text-decoration: none;
     1905}
     1906
     1907.spbwc-settings-tabbar__tab .dashicons {
     1908    font-size: var(--text-xl);
     1909    width: var(--text-xl);
     1910    height: var(--text-xl);
     1911}
     1912
     1913.spbwc-settings-tabbar__tab:hover,
     1914.spbwc-settings-tabbar__tab:focus {
     1915    color: var(--st-brand);
     1916}
     1917
     1918.spbwc-settings-tabbar__tab:focus-visible {
     1919    outline: 2px solid var(--st-brand);
     1920    outline-offset: -2px;
     1921    box-shadow: none;
     1922}
     1923
     1924.spbwc-settings-tabbar__tab.is-active {
     1925    color: var(--st-brand);
     1926    border-block-end-color: var(--st-brand);
     1927}
  • storelly-product-builder-for-woocommerce/tags/1.9.0/storelly-product-builder-for-woocommerce.php

    r3761204 r3768035  
    99Plugin URI:             https://storelly.com/product-builder
    1010Description:            Create product builder for Woocommerce products
    11 Version:                1.8.0
     11Version:                1.9.0
    1212Requires Plugins:       woocommerce
    1313WC requires at least:   6.0.0
    … …  
    2525$spbwc_basedir    = $spbwc_upload_dir['basedir'];
    2626$spbwc_baseurl    = $spbwc_upload_dir['baseurl'];
    27 define('SPBWC_PB_VERSION',                  '1.8.0');
     27define('SPBWC_PB_VERSION',                  '1.9.0');
    2828define('SPBWC_PB_NUMBER_VERSION',           138);
    2929define('SPBWC_PB_PLUGIN_URL',               plugin_dir_url(__FILE__));
    … …  
    162162require_once(SPBWC_PB_PLUGIN_DIR .  'includes/class-admin-options.php');
    163163require_once(SPBWC_PB_PLUGIN_DIR .  'includes/admin/class-admin-menu.php');
     164require_once(SPBWC_PB_PLUGIN_DIR .  'includes/admin/class-settings-tabs.php');
    164165require_once(SPBWC_PB_PLUGIN_DIR .  'includes/class-i18n-notice.php');
    165166require_once(SPBWC_PB_PLUGIN_DIR .  'includes/class-onboarding.php');
  • storelly-product-builder-for-woocommerce/tags/1.9.0/views/license.php

    r3568219 r3768035  
    8585        </div>
    8686    </header>
     87    <?php if ( class_exists( 'SPBWC_Settings_Tabs' ) ) { SPBWC_Settings_Tabs::render( 'license' ); } ?>
    8788
    8889    <?php if ( $just_connected ) : ?>
  • storelly-product-builder-for-woocommerce/tags/1.9.0/views/menu-settings.php

    r3568219 r3768035  
    106106        </div>
    107107    </header>
     108
     109    <?php if ( class_exists( 'SPBWC_Settings_Tabs' ) ) { SPBWC_Settings_Tabs::render( 'general' ); } ?>
    108110
    109111    <?php if ($message && $status) : ?>
  • storelly-product-builder-for-woocommerce/tags/1.9.0/views/options/edit-option.php

    r3568314 r3768035  
    103103            </div>
    104104        </header>
     105        <?php // WP moves admin notices below this marker instead of into the hero. ?>
     106        <hr class="wp-header-end" />
    105107
    106108        <!-- ──────────────── Flash message ──────────────── -->
  • storelly-product-builder-for-woocommerce/tags/1.9.0/views/overview.php

    r3568219 r3768035  
    103103                    <?php esc_html_e( 'Storelly Product Builder', 'storelly-product-builder-for-woocommerce' ); ?>
    104104                    <?php if ( ! $is_remote_ok ) : ?>
    105                         <a class="spbwc-page-hero__badge" href="<?php echo esc_url( $license_url ); ?>"
     105                        <a id="spbwc-local-data-badge" class="spbwc-page-hero__badge" href="<?php echo esc_url( $license_url ); ?>"
    106106                           title="<?php esc_attr_e( 'Couldn\'t reach Storelly — showing your local database counts. Connect to enable live metrics.', 'storelly-product-builder-for-woocommerce' ); ?>">
    107107                            <span class="dashicons dashicons-database" aria-hidden="true"></span>
    … …  
    734734                <?php esc_html_e( 'Key metrics', 'storelly-product-builder-for-woocommerce' ); ?>
    735735            </h2>
    736             <p class="spbwc-section__subtitle">
     736            <p class="spbwc-section__subtitle" id="spbwc-metrics-subtitle">
    737737                <?php
    738738                if ( $is_remote_ok ) {
    … …  
    753753                    <div class="spbwc-stat-card__label"><?php esc_html_e( 'Products', 'storelly-product-builder-for-woocommerce' ); ?></div>
    754754                </div>
    755                 <div class="spbwc-stat-card__value"><?php echo esc_html( number_format_i18n( $disp_products ) ); ?></div>
     755                <div class="spbwc-stat-card__value" data-spbwc-stat="products"><?php echo esc_html( number_format_i18n( $disp_products ) ); ?></div>
    756756                <p class="spbwc-stat-card__hint">
    757757                    <?php esc_html_e( 'WooCommerce products that have the Storelly product builder enabled.', 'storelly-product-builder-for-woocommerce' ); ?>
    … …  
    791791                    <div class="spbwc-stat-card__label"><?php esc_html_e( 'Orders', 'storelly-product-builder-for-woocommerce' ); ?></div>
    792792                </div>
    793                 <div class="spbwc-stat-card__value"><?php echo esc_html( number_format_i18n( $disp_orders ) ); ?></div>
     793                <div class="spbwc-stat-card__value" data-spbwc-stat="orders"><?php echo esc_html( number_format_i18n( $disp_orders ) ); ?></div>
    794794                <p class="spbwc-stat-card__hint">
    795795                    <?php esc_html_e( 'All-time WooCommerce orders containing a customized Storelly product.', 'storelly-product-builder-for-woocommerce' ); ?>
    … …  
    810810                    <div class="spbwc-stat-card__label"><?php esc_html_e( 'Quote Requests', 'storelly-product-builder-for-woocommerce' ); ?></div>
    811811                </div>
    812                 <div class="spbwc-stat-card__value"><?php echo esc_html( number_format_i18n( $disp_quotes ) ); ?></div>
     812                <div class="spbwc-stat-card__value" data-spbwc-stat="quotes"><?php echo esc_html( number_format_i18n( $disp_quotes ) ); ?></div>
    813813                <p class="spbwc-stat-card__hint">
    814814                    <?php esc_html_e( 'Customer-submitted quote requests awaiting your response or follow-up.', 'storelly-product-builder-for-woocommerce' ); ?>
    … …  
    14401440                    </div>
    14411441                    <h3 class="spbwc-quick-card__title">
    1442                         <?php esc_html_e( 'License & billing', 'storelly-product-builder-for-woocommerce' ); ?>
     1442                        <?php esc_html_e( 'Account & Plan', 'storelly-product-builder-for-woocommerce' ); ?>
    14431443                    </h3>
    14441444                </div>
    … …  
    14481448                <div class="spbwc-quick-card__footer">
    14491449                    <span class="spbwc-cta-btn">
    1450                         <?php esc_html_e( 'Open license', 'storelly-product-builder-for-woocommerce' ); ?>
     1450                        <?php esc_html_e( 'Open Account & Plan', 'storelly-product-builder-for-woocommerce' ); ?>
    14511451                        <span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span>
    14521452                    </span>
    … …  
    14891489                    <span class="spbwc-cta-btn">
    14901490                        <?php esc_html_e( 'Manage emails', 'storelly-product-builder-for-woocommerce' ); ?>
     1491                        <span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span>
     1492                    </span>
     1493                </div>
     1494            </a>
     1495
     1496            <a class="spbwc-quick-card" href="<?php echo esc_url( admin_url( 'admin.php?page=' . SPBWC_PB_OPTIONS_SLUG . '/global-import' ) ); ?>">
     1497                <div class="spbwc-quick-card__head">
     1498                    <div class="spbwc-quick-card__icon">
     1499                        <span class="dashicons dashicons-admin-tools" aria-hidden="true"></span>
     1500                    </div>
     1501                    <h3 class="spbwc-quick-card__title">
     1502                        <?php esc_html_e( 'Setup Wizard', 'storelly-product-builder-for-woocommerce' ); ?>
     1503                    </h3>
     1504                </div>
     1505                <p class="spbwc-quick-card__desc">
     1506                    <?php esc_html_e( 'Import sample products, or turn your existing WooCommerce variations into pricing options in one step.', 'storelly-product-builder-for-woocommerce' ); ?>
     1507                </p>
     1508                <div class="spbwc-quick-card__footer">
     1509                    <span class="spbwc-cta-btn">
     1510                        <?php esc_html_e( 'Open Setup Wizard', 'storelly-product-builder-for-woocommerce' ); ?>
    14911511                        <span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span>
    14921512                    </span>
  • storelly-product-builder-for-woocommerce/tags/1.9.0/views/setup-wizard/landing.php

    r3568219 r3768035  
    157157                </div>
    158158        </section>
     159        <?php if ( class_exists( 'SPBWC_Settings_Tabs' ) ) { SPBWC_Settings_Tabs::render( 'setup' ); } ?>
    159160
    160161        <?php if ( $spbwc_demo_cleaned >= 0 ) : ?>
  • storelly-product-builder-for-woocommerce/tags/1.9.0/views/setup-wizard/woo-seed.php

    r3568219 r3768035  
    4242                </div>
    4343        </section>
     44        <?php if ( class_exists( 'SPBWC_Settings_Tabs' ) ) { SPBWC_Settings_Tabs::render( 'setup' ); } ?>
    4445
    4546        <div id="spbwc-woo-seed-app" class="spbwc-block spbwc-ws-app">
  • storelly-product-builder-for-woocommerce/trunk/includes/admin/class-admin-menu.php

    r3568219 r3768035  
    99 *   1. Re-orders the existing submenu items into three task bands
    1010 *      (BUILD / SELL / CONFIGURE) and injects non-clickable section headings.
    11  *   2. Swaps the washed-out PNG menu icon for a crisp inline SVG.
     11 *   2. Swaps the washed-out PNG menu icon for an SVG mask drawn in currentColor.
    1212 *
    1313 * Safety: the re-order is loss-proof — any submenu item not named in the
    … …  
    6464                protected static function desired_order() {
    6565                        $opts   = defined( 'SPBWC_PB_OPTIONS_SLUG' ) ? SPBWC_PB_OPTIONS_SLUG : 'storelly-product-builder-for-woocommerce-options';
    66                         $emails = defined( 'SPBWC_PB_EMAILS_SLUG' ) ? SPBWC_PB_EMAILS_SLUG : 'storelly-product-builder-for-woocommerce-emails';
    6766
    6867                        return array(
    … …  
    8584                                array( 'heading', __( 'Configure', 'storelly-product-builder-for-woocommerce' ), 'configure' ),
    8685                                $opts,
    87                                 SPBWC_PB_LICENSE_SLUG,
    88                                 $emails,
    89                                 $opts . '/global-import',
    9086                        );
    9187                }
    … …  
    135131                        }
    136132
     133                        // Pages folded into Settings tabs stay registered (their URLs, caps and
     134                        // asset hooks are unchanged) but are hidden from the menu by class.
     135                        $hidden = class_exists( 'SPBWC_Settings_Tabs' ) ? SPBWC_Settings_Tabs::moved_pages() : array();
     136
    137137                        // Loss-proof: append anything we did not explicitly place.
    138138                        foreach ( $submenu[ $parent ] as $item ) {
    139139                                $slug = isset( $item[2] ) ? $item[2] : '';
    140140                                if ( '' !== $slug && empty( $used[ $slug ] ) ) {
     141                                        if ( in_array( $slug, $hidden, true ) ) {
     142                                                $item[4] = ( isset( $item[4] ) ? $item[4] . ' ' : '' ) . SPBWC_Settings_Tabs::HIDDEN_CLASS;
     143                                        }
    141144                                        $ordered[] = $item;
    142145                                }
    … …  
    147150
    148151                /**
    149                  * Replace the top-level menu icon (index 6 of its $menu row) with a
    150                  * crisp inline SVG so it reads cleanly on the dark admin sidebar.
     152                 * Replace the top-level menu icon (index 6 of its $menu row) with 'none':
     153                 * WP then prints an empty .wp-menu-image that admin-menu.css fills with
     154                 * the Storelly mark as a CSS mask in currentColor, so the icon follows
     155                 * WP's idle / hover / current colours like a dashicon.
    151156                 *
    152157                 * @param array  $menu   The global $menu (by reference via global).
    … …  
    157162                                return;
    158163                        }
    159                         $svg = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a7aaad' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polygon points='12 2 2 7 12 12 22 7 12 2'/><polyline points='2 17 12 22 22 17'/><polyline points='2 12 12 17 22 12'/></svg>";
    160                         $uri = 'data:image/svg+xml;base64,' . base64_encode( str_replace( '%23', '#', $svg ) );
    161164                        foreach ( $menu as $key => $row ) {
    162165                                if ( isset( $row[2] ) && $row[2] === $parent && isset( $menu[ $key ][6] ) ) {
    163                                         $menu[ $key ][6] = $uri;
     166                                        $menu[ $key ][6] = 'none';
    164167                                        break;
    165168                                }
  • storelly-product-builder-for-woocommerce/trunk/includes/class-admin-options.php

    r3761204 r3768035  
    4040            // REST API — Pricing Options grid (GET, read-only, faster than admin-ajax.php).
    4141            add_action( 'rest_api_init', array( $this, 'spbwc_register_rest_routes' ) );
     42            // Menu badge cache (Custom Orders to fulfil): drop it when an order moves.
     43            add_action( 'woocommerce_order_status_changed', array( __CLASS__, 'flush_custom_order_count' ) );
     44            add_action( 'woocommerce_new_order', array( __CLASS__, 'flush_custom_order_count' ) );
    4245        }
    4346        public function spbwc_ajax() {
    … …  
    7073                'spbwc_swap_product_option'      => false, // admin only
    7174                'spbwc_unlink_product_option'    => false, // admin only
     75                // Overview: refresh the Storelly stats after the page has painted (A11)
     76                'spbwc_overview_stats'           => false, // admin only
    7277            );
    7378            foreach ($ajax_events as $ajax_event => $nopriv) {
    … …  
    294299                add_submenu_page(
    295300                    SPBWC_PB_OVERVIEW_SLUG,
    296                     esc_html__('General Settings', 'storelly-product-builder-for-woocommerce'),
    297                     esc_html__('General Settings', 'storelly-product-builder-for-woocommerce'),
     301                    esc_html__('Settings', 'storelly-product-builder-for-woocommerce'),
     302                    esc_html__('Settings', 'storelly-product-builder-for-woocommerce'),
    298303                    'manage_options',
    299304                    SPBWC_PB_OPTIONS_SLUG,
    … …  
    324329                    array($this, 'spbwc_products_manager')
    325330                );
     331                // Same bubble as Quotes and B2B: custom orders paid and waiting to be made.
     332                $spbwc_orders_menu_title = esc_html__('Custom Orders', 'storelly-product-builder-for-woocommerce');
     333                $spbwc_orders_to_fulfil  = self::count_custom_orders_to_fulfil();
     334                if ( $spbwc_orders_to_fulfil > 0 ) {
     335                    $spbwc_orders_menu_title .= ' <span class="awaiting-mod"><span class="pending-count">' . esc_html( number_format_i18n( $spbwc_orders_to_fulfil ) ) . '</span></span>';
     336                }
    326337                add_submenu_page(
    327338                    SPBWC_PB_OVERVIEW_SLUG,
    328339                    esc_html__('Custom Orders', 'storelly-product-builder-for-woocommerce'),
    329                     esc_html__('Custom Orders', 'storelly-product-builder-for-woocommerce'),
     340                    $spbwc_orders_menu_title,
    330341                    'manage_options',
    331342                    SPBWC_PB_ORDERS_SLUG,
    … …  
    382393                    array($this, 'spbwc_global_import')
    383394                );
     395                // System and About sit under the Storelly menu (hidden there; reached
     396                // through the Settings tab bar) so WP opens that menu on them.
    384397                add_submenu_page(
    385                     SPBWC_PB_OPTIONS_SLUG,
     398                    SPBWC_PB_OVERVIEW_SLUG,
    386399                    esc_html__('System Infor', 'storelly-product-builder-for-woocommerce'),
    387400                    esc_html__('System Infor', 'storelly-product-builder-for-woocommerce'),
    … …  
    391404                );
    392405                add_submenu_page(
    393                     SPBWC_PB_OPTIONS_SLUG,
     406                    SPBWC_PB_OVERVIEW_SLUG,
    394407                    esc_html__('About', 'storelly-product-builder-for-woocommerce'),
    395408                    esc_html__('About', 'storelly-product-builder-for-woocommerce'),
    … …  
    511524            if ( $hook === 'toplevel_page_' . SPBWC_PB_OVERVIEW_SLUG ) {
    512525                wp_enqueue_style('spbwc-overview-css');
     526                // Fills in the Storelly numbers when they were not cached yet (A11).
     527                $spbwc_stats = class_exists( 'SPBWC_License_Manager' ) ? SPBWC_License_Manager::get_cached_overview_stats() : null;
     528                if ( is_wp_error( $spbwc_stats ) && 'no_cache' === $spbwc_stats->get_error_code() ) {
     529                    wp_enqueue_script( 'spbwc-overview-stats', SPBWC_PB_JS_URL . 'overview-stats.js', array(), SPBWC_PB_VERSION, true );
     530                    wp_localize_script( 'spbwc-overview-stats', 'spbwcOverviewStats', array(
     531                        'ajaxUrl'  => admin_url( 'admin-ajax.php' ),
     532                        'nonce'    => wp_create_nonce( 'spbwc_overview_stats' ),
     533                        'subtitle' => __( 'Live numbers from your Storelly dashboard. Each card links straight to the related management screen.', 'storelly-product-builder-for-woocommerce' ),
     534                    ) );
     535                }
    513536            }
    514537
    … …  
    19601983                    </div>
    19611984                </header>
    1962                 <?php // Anchor so WP admin notices land below the hero, not on it. ?>
    1963                 <hr class="wp-header-end" />
     1985                <?php if ( class_exists( 'SPBWC_Settings_Tabs' ) ) { SPBWC_Settings_Tabs::render( 'system' ); } ?>
    19641986
    19651987                <div class="spbwc-block">
    … …  
    20262048                    </div>
    20272049                </header>
    2028                 <?php // Anchor so WP admin notices land below the hero, not on it. ?>
    2029                 <hr class="wp-header-end" />
     2050                <?php if ( class_exists( 'SPBWC_Settings_Tabs' ) ) { SPBWC_Settings_Tabs::render( 'about' ); } ?>
    20302051
    20312052                <!-- Brand intro -->
    … …  
    20922113            <?php
    20932114        }
     2115        /** Transient holding the Custom Orders menu badge count. */
     2116        const CUSTOM_ORDER_COUNT_KEY = 'spbwc_custom_orders_to_fulfil';
     2117
     2118        /**
     2119         * Orders in "processing" (paid, not yet shipped) that contain at least one
     2120         * builder line item. Runs on every admin page for the menu badge, so it is
     2121         * cached for 10 minutes and dropped whenever an order changes status.
     2122         *
     2123         * @return int
     2124         */
     2125        public static function count_custom_orders_to_fulfil() {
     2126            $cached = get_transient( self::CUSTOM_ORDER_COUNT_KEY );
     2127            if ( false !== $cached ) {
     2128                return (int) $cached;
     2129            }
     2130            $count = 0;
     2131            if ( function_exists( 'wc_get_orders' ) ) {
     2132                $ids = wc_get_orders( array( 'status' => array( 'processing' ), 'return' => 'ids', 'limit' => 500 ) );
     2133                if ( ! empty( $ids ) ) {
     2134                    global $wpdb; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- Global variable $wpdb.
     2135                    $ids          = array_map( 'absint', $ids );
     2136                    $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
     2137                    // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Cached in a transient above; table names from $wpdb, ids via placeholders.
     2138                    $count = (int) $wpdb->get_var(
     2139                        $wpdb->prepare(
     2140                            "SELECT COUNT(DISTINCT oi.order_id)
     2141                            FROM {$wpdb->prefix}woocommerce_order_items oi
     2142                            INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta oim ON oi.order_item_id = oim.order_item_id
     2143                            WHERE oim.meta_key = '_pcpb_option_price'
     2144                            AND oi.order_item_type = 'line_item'
     2145                            AND oi.order_id IN ({$placeholders})",
     2146                            $ids
     2147                        )
     2148                    );
     2149                    // phpcs:enable
     2150                }
     2151            }
     2152            set_transient( self::CUSTOM_ORDER_COUNT_KEY, $count, 10 * MINUTE_IN_SECONDS );
     2153            return $count;
     2154        }
     2155
     2156        /** Drop the cached Custom Orders badge count. */
     2157        public static function flush_custom_order_count() {
     2158            delete_transient( self::CUSTOM_ORDER_COUNT_KEY );
     2159        }
     2160
    20942161        public function spbwc_orders_manager() {
    20952162            if ( ! current_user_can( 'spbwc_manage_product_builder' ) ) {
    … …  
    44884555
    44894556            // --- Remote (Storelly) stats overlay ---
    4490             $remote_stats = SPBWC_License_Manager::get_overview_stats();
     4557            // Cache only: the API call moved to the spbwc_overview_stats AJAX (A11), so a
     4558            // slow or unreachable Storelly never holds up this page.
     4559            $remote_stats = SPBWC_License_Manager::get_cached_overview_stats();
    44914560
    44924561            // --- License info ---
    … …  
    45044573
    45054574            include_once( SPBWC_PB_PLUGIN_DIR . 'views/overview.php' );
     4575        }
     4576
     4577        /**
     4578         * AJAX: fetch the Storelly overview stats after the Overview page has
     4579         * painted (A11). The result is cached by get_overview_stats().
     4580         * wp_ajax_spbwc_overview_stats
     4581         */
     4582        public function spbwc_overview_stats() {
     4583            if ( ! current_user_can( 'manage_options' ) ) {
     4584                wp_send_json_error( null, 403 );
     4585            }
     4586            check_ajax_referer( 'spbwc_overview_stats', 'nonce' );
     4587            $stats = SPBWC_License_Manager::get_overview_stats();
     4588            if ( is_wp_error( $stats ) ) {
     4589                wp_send_json_error( array( 'code' => $stats->get_error_code() ) );
     4590            }
     4591            wp_send_json_success( array(
     4592                'products' => number_format_i18n( (int) $stats['total_products'] ),
     4593                'orders'   => number_format_i18n( (int) $stats['total_orders'] ),
     4594                'quotes'   => number_format_i18n( (int) $stats['total_quotes'] ),
     4595            ) );
    45064596        }
    45074597
  • storelly-product-builder-for-woocommerce/trunk/includes/class-global-import-admin.php

    r3568219 r3768035  
    102102                    </div>
    103103                </header>
     104                <?php if ( class_exists( 'SPBWC_Settings_Tabs' ) ) { SPBWC_Settings_Tabs::render( 'setup' ); } ?>
    104105                <div id="spbwc-global-import-app"></div>
    105106            </div>
  • storelly-product-builder-for-woocommerce/trunk/includes/class-license-manager.php

    r3761204 r3768035  
    225225         * @return array|WP_Error
    226226         */
     227        /**
     228         * Overview stats from the cache only - never calls the API, so the
     229         * Overview page can render at once (A11). WP_Error 'no_cache' means the
     230         * store is connected but the numbers have to be refreshed (by AJAX).
     231         *
     232         * @return array|WP_Error
     233         */
     234        public static function get_cached_overview_stats() {
     235            $cached = get_transient( self::TRANSIENT_OVERVIEW_STATS );
     236            if ( false !== $cached ) {
     237                return $cached;
     238            }
     239            $api_keys = get_option( 'spbwc_connect_api_keys', array() );
     240            if ( empty( $api_keys['business_id'] ) ) {
     241                return new WP_Error( 'no_business', __( 'No business ID configured.', 'storelly-product-builder-for-woocommerce' ) );
     242            }
     243            if ( get_transient( self::TRANSIENT_OVERVIEW_STATS . '_failed' ) ) {
     244                return new WP_Error( 'api_error', __( 'Could not retrieve overview stats.', 'storelly-product-builder-for-woocommerce' ) );
     245            }
     246            return new WP_Error( 'no_cache', __( 'Overview stats not cached yet.', 'storelly-product-builder-for-woocommerce' ) );
     247        }
     248
    227249        public static function get_overview_stats() {
    228250            $cached = get_transient( self::TRANSIENT_OVERVIEW_STATS );
    … …  
    242264
    243265            if ( is_wp_error( $resp ) || empty( $resp['success'] ) ) {
     266                // Remember the failure for 5 minutes so every Overview visit does not retry.
     267                set_transient( self::TRANSIENT_OVERVIEW_STATS . '_failed', 1, 5 * MINUTE_IN_SECONDS );
    244268                return new WP_Error( 'api_error', __( 'Could not retrieve overview stats.', 'storelly-product-builder-for-woocommerce' ) );
    245269            }
  • storelly-product-builder-for-woocommerce/trunk/includes/email/class-email-admin.php

    r3568219 r3768035  
    259259                    </div>
    260260                </header>
    261                 <?php // WP relocates admin notices to right after the first <h1>; this marker
    262                       // makes them land below the hero instead of on top of the gradient. ?>
    263                 <hr class="wp-header-end" />
     261                <?php if ( class_exists( 'SPBWC_Settings_Tabs' ) ) { SPBWC_Settings_Tabs::render( 'emails' ); } ?>
    264262
    265263                <?php if ( isset( $_GET['spbwc_test_sent'] ) ) : // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display-only flash. ?>
  • storelly-product-builder-for-woocommerce/trunk/readme.txt

    r3761204 r3768035  
    77Requires PHP: 7.4
    88Requires Plugins: woocommerce
    9 Stable tag: 1.8.0
     9Stable tag: 1.9.0
    1010License: GPLv2 or later
    1111License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    238238== Changelog ==
    239239
     240= 1.9.0 =
     241* **New: one Settings area with tabs.** Settings, Emails, Cloud & License, Setup Wizard, System and About now share one tab bar, and the Storelly menu shows a single "Settings" item. Every old link and bookmark keeps working.
     242* **Faster Overview.** The Overview no longer waits for Storelly Cloud before it shows; the cloud numbers fill in afterwards without moving the page, and a failed connection is not retried on every visit.
     243* New: a count badge on Custom Orders for paid orders that are waiting to be made.
     244* New: Setup Wizard card on the Overview; "License & billing" is now "Account & Plan".
     245* The Storelly menu icon now follows the admin colour scheme like other menu icons.
     246* Fixed: notices from other plugins no longer appear inside the page header on Settings, Account & Plan, Setup Wizard and the option editor.
     247
    240248= 1.8.0 =
    241249* **New: Download PDF always gives you a file.** On the free plan it now builds a preview PDF on your own site, marked "Made with Storelly", and offers the clean print-ready version through Storelly Cloud. Nothing is sent off-site for the preview.
    … …  
    367375== Upgrade Notice ==
    368376
     377= 1.9.0 =
     378Settings, Emails, Account & Plan, Setup Wizard, System and About are now tabs of one Settings area, and the Overview opens without waiting for Storelly Cloud.
     379
    369380= 1.8.0 =
    370381Free stores now get a watermarked preview when they click Download PDF, the upgrade notice only shows after you use a Cloud feature, and the quote form's Select field is a real dropdown.
  • storelly-product-builder-for-woocommerce/trunk/static/css/admin-menu.css

    r3568219 r3768035  
    5959        vertical-align: top;
    6060}
     61
     62/* === Pages folded into Settings tabs (S2) ===================== */
     63/* Still registered (URLs, caps and asset hooks unchanged), only    */
     64/* hidden from the submenu; reached through the Settings tab bar.   */
     65#adminmenu .wp-submenu li.spbwc-menu-hidden {
     66        display: none;
     67}
     68
     69/* === Menu icon as a mask (S2) ================================= */
     70/* The icon is drawn with currentColor, so it follows WP's idle,    */
     71/* hover and current colours like a dashicon (a data-URI <img> is   */
     72/* only repainted by WP when it uses fill="", not stroke).          */
     73#adminmenu .toplevel_page_storelly-product-builder-for-woocommerce-overview div.wp-menu-image::before {
     74        content: "";
     75        display: block;
     76        width: 20px;
     77        height: 20px;
     78        margin: 7px auto 0;
     79        padding: 0;
     80        background-color: currentColor;
     81        -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolygon points='12 2 2 7 12 12 22 7 12 2'/%3E%3Cpolyline points='2 17 12 22 22 17'/%3E%3Cpolyline points='2 12 12 17 22 12'/%3E%3C/svg%3E") center / 20px 20px no-repeat;
     82        mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolygon points='12 2 2 7 12 12 22 7 12 2'/%3E%3Cpolyline points='2 17 12 22 22 17'/%3E%3Cpolyline points='2 12 12 17 22 12'/%3E%3C/svg%3E") center / 20px 20px no-repeat;
     83}
  • storelly-product-builder-for-woocommerce/trunk/static/css/overview.css

    r3568219 r3768035  
    188188    padding: 2px 8px;
    189189    background: var(--st-brand, #1d4ed8);
    190     color: #fff;
     190    color: var(--nbd-st-text-inverse, #fff);
    191191    font-size: 11px;
    192192    font-weight: 700;
    … …  
    202202    height: 40px;
    203203    border-radius: var(--nbd-radius, 10px);
    204     background: #fff;
     204    background: var(--nbd-st-bg, #fff);
    205205    border: 1px solid var(--nbd-st-border, #e2e8f0);
    206206    color: var(--st-brand, #1d4ed8);
    … …  
    373373    .spbwc-welcome__cloud-row { flex-direction: column; }
    374374}
     375
     376/* A11: numbers refreshed after paint keep the same box (digits of equal width,
     377 * one line reserved), so a late update never shifts the cards. */
     378.spbwc-stat-card__value {
     379    font-variant-numeric: tabular-nums;
     380    min-height: 1.2em;
     381}
     382
     383/* The "Local data" badge is hidden by overview-stats.js once live numbers arrive;
     384 * its display:inline-flex would otherwise win over the hidden attribute. */
     385.spbwc-page-hero__badge[hidden] {
     386    display: none;
     387}
  • storelly-product-builder-for-woocommerce/trunk/static/css/storelly-admin-ui.css

    r3761204 r3768035  
    18781878    margin: var(--nbd-space-2) 0 0;
    18791879}
     1880
     1881/* === Settings tab bar (S2 / U2) ===============================
     1882 * One bar under the hero of every Settings page (General, Emails,
     1883 * Cloud & License, Setup Wizard, System, About). Pages keep their own
     1884 * slugs; the bar makes them read as one tabbed Settings area.
     1885 * ============================================================= */
     1886.spbwc-settings-tabbar {
     1887    display: flex;
     1888    flex-wrap: wrap;
     1889    gap: var(--nbd-space-1);
     1890    margin-block: calc(-1 * var(--nbd-space-2)) var(--nbd-space-6);
     1891    border-block-end: 1px solid var(--nbd-st-border-light);
     1892}
     1893
     1894.spbwc-settings-tabbar__tab {
     1895    display: inline-flex;
     1896    align-items: center;
     1897    gap: var(--nbd-space-2);
     1898    padding: var(--nbd-space-3) var(--nbd-space-4);
     1899    margin-block-end: -1px;
     1900    border-block-end: 2px solid transparent;
     1901    color: var(--nbd-st-text-soft);
     1902    font-size: var(--text-md);
     1903    font-weight: var(--font-semibold, 600);
     1904    text-decoration: none;
     1905}
     1906
     1907.spbwc-settings-tabbar__tab .dashicons {
     1908    font-size: var(--text-xl);
     1909    width: var(--text-xl);
     1910    height: var(--text-xl);
     1911}
     1912
     1913.spbwc-settings-tabbar__tab:hover,
     1914.spbwc-settings-tabbar__tab:focus {
     1915    color: var(--st-brand);
     1916}
     1917
     1918.spbwc-settings-tabbar__tab:focus-visible {
     1919    outline: 2px solid var(--st-brand);
     1920    outline-offset: -2px;
     1921    box-shadow: none;
     1922}
     1923
     1924.spbwc-settings-tabbar__tab.is-active {
     1925    color: var(--st-brand);
     1926    border-block-end-color: var(--st-brand);
     1927}
  • storelly-product-builder-for-woocommerce/trunk/storelly-product-builder-for-woocommerce.php

    r3761204 r3768035  
    99Plugin URI:             https://storelly.com/product-builder
    1010Description:            Create product builder for Woocommerce products
    11 Version:                1.8.0
     11Version:                1.9.0
    1212Requires Plugins:       woocommerce
    1313WC requires at least:   6.0.0
    … …  
    2525$spbwc_basedir    = $spbwc_upload_dir['basedir'];
    2626$spbwc_baseurl    = $spbwc_upload_dir['baseurl'];
    27 define('SPBWC_PB_VERSION',                  '1.8.0');
     27define('SPBWC_PB_VERSION',                  '1.9.0');
    2828define('SPBWC_PB_NUMBER_VERSION',           138);
    2929define('SPBWC_PB_PLUGIN_URL',               plugin_dir_url(__FILE__));
    … …  
    162162require_once(SPBWC_PB_PLUGIN_DIR .  'includes/class-admin-options.php');
    163163require_once(SPBWC_PB_PLUGIN_DIR .  'includes/admin/class-admin-menu.php');
     164require_once(SPBWC_PB_PLUGIN_DIR .  'includes/admin/class-settings-tabs.php');
    164165require_once(SPBWC_PB_PLUGIN_DIR .  'includes/class-i18n-notice.php');
    165166require_once(SPBWC_PB_PLUGIN_DIR .  'includes/class-onboarding.php');
  • storelly-product-builder-for-woocommerce/trunk/views/license.php

    r3568219 r3768035  
    8585        </div>
    8686    </header>
     87    <?php if ( class_exists( 'SPBWC_Settings_Tabs' ) ) { SPBWC_Settings_Tabs::render( 'license' ); } ?>
    8788
    8889    <?php if ( $just_connected ) : ?>
  • storelly-product-builder-for-woocommerce/trunk/views/menu-settings.php

    r3568219 r3768035  
    106106        </div>
    107107    </header>
     108
     109    <?php if ( class_exists( 'SPBWC_Settings_Tabs' ) ) { SPBWC_Settings_Tabs::render( 'general' ); } ?>
    108110
    109111    <?php if ($message && $status) : ?>
  • storelly-product-builder-for-woocommerce/trunk/views/options/edit-option.php

    r3568314 r3768035  
    103103            </div>
    104104        </header>
     105        <?php // WP moves admin notices below this marker instead of into the hero. ?>
     106        <hr class="wp-header-end" />
    105107
    106108        <!-- ──────────────── Flash message ──────────────── -->
  • storelly-product-builder-for-woocommerce/trunk/views/overview.php

    r3568219 r3768035  
    103103                    <?php esc_html_e( 'Storelly Product Builder', 'storelly-product-builder-for-woocommerce' ); ?>
    104104                    <?php if ( ! $is_remote_ok ) : ?>
    105                         <a class="spbwc-page-hero__badge" href="<?php echo esc_url( $license_url ); ?>"
     105                        <a id="spbwc-local-data-badge" class="spbwc-page-hero__badge" href="<?php echo esc_url( $license_url ); ?>"
    106106                           title="<?php esc_attr_e( 'Couldn\'t reach Storelly — showing your local database counts. Connect to enable live metrics.', 'storelly-product-builder-for-woocommerce' ); ?>">
    107107                            <span class="dashicons dashicons-database" aria-hidden="true"></span>
    … …  
    734734                <?php esc_html_e( 'Key metrics', 'storelly-product-builder-for-woocommerce' ); ?>
    735735            </h2>
    736             <p class="spbwc-section__subtitle">
     736            <p class="spbwc-section__subtitle" id="spbwc-metrics-subtitle">
    737737                <?php
    738738                if ( $is_remote_ok ) {
    … …  
    753753                    <div class="spbwc-stat-card__label"><?php esc_html_e( 'Products', 'storelly-product-builder-for-woocommerce' ); ?></div>
    754754                </div>
    755                 <div class="spbwc-stat-card__value"><?php echo esc_html( number_format_i18n( $disp_products ) ); ?></div>
     755                <div class="spbwc-stat-card__value" data-spbwc-stat="products"><?php echo esc_html( number_format_i18n( $disp_products ) ); ?></div>
    756756                <p class="spbwc-stat-card__hint">
    757757                    <?php esc_html_e( 'WooCommerce products that have the Storelly product builder enabled.', 'storelly-product-builder-for-woocommerce' ); ?>
    … …  
    791791                    <div class="spbwc-stat-card__label"><?php esc_html_e( 'Orders', 'storelly-product-builder-for-woocommerce' ); ?></div>
    792792                </div>
    793                 <div class="spbwc-stat-card__value"><?php echo esc_html( number_format_i18n( $disp_orders ) ); ?></div>
     793                <div class="spbwc-stat-card__value" data-spbwc-stat="orders"><?php echo esc_html( number_format_i18n( $disp_orders ) ); ?></div>
    794794                <p class="spbwc-stat-card__hint">
    795795                    <?php esc_html_e( 'All-time WooCommerce orders containing a customized Storelly product.', 'storelly-product-builder-for-woocommerce' ); ?>
    … …  
    810810                    <div class="spbwc-stat-card__label"><?php esc_html_e( 'Quote Requests', 'storelly-product-builder-for-woocommerce' ); ?></div>
    811811                </div>
    812                 <div class="spbwc-stat-card__value"><?php echo esc_html( number_format_i18n( $disp_quotes ) ); ?></div>
     812                <div class="spbwc-stat-card__value" data-spbwc-stat="quotes"><?php echo esc_html( number_format_i18n( $disp_quotes ) ); ?></div>
    813813                <p class="spbwc-stat-card__hint">
    814814                    <?php esc_html_e( 'Customer-submitted quote requests awaiting your response or follow-up.', 'storelly-product-builder-for-woocommerce' ); ?>
    … …  
    14401440                    </div>
    14411441                    <h3 class="spbwc-quick-card__title">
    1442                         <?php esc_html_e( 'License & billing', 'storelly-product-builder-for-woocommerce' ); ?>
     1442                        <?php esc_html_e( 'Account & Plan', 'storelly-product-builder-for-woocommerce' ); ?>
    14431443                    </h3>
    14441444                </div>
    … …  
    14481448                <div class="spbwc-quick-card__footer">
    14491449                    <span class="spbwc-cta-btn">
    1450                         <?php esc_html_e( 'Open license', 'storelly-product-builder-for-woocommerce' ); ?>
     1450                        <?php esc_html_e( 'Open Account & Plan', 'storelly-product-builder-for-woocommerce' ); ?>
    14511451                        <span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span>
    14521452                    </span>
    … …  
    14891489                    <span class="spbwc-cta-btn">
    14901490                        <?php esc_html_e( 'Manage emails', 'storelly-product-builder-for-woocommerce' ); ?>
     1491                        <span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span>
     1492                    </span>
     1493                </div>
     1494            </a>
     1495
     1496            <a class="spbwc-quick-card" href="<?php echo esc_url( admin_url( 'admin.php?page=' . SPBWC_PB_OPTIONS_SLUG . '/global-import' ) ); ?>">
     1497                <div class="spbwc-quick-card__head">
     1498                    <div class="spbwc-quick-card__icon">
     1499                        <span class="dashicons dashicons-admin-tools" aria-hidden="true"></span>
     1500                    </div>
     1501                    <h3 class="spbwc-quick-card__title">
     1502                        <?php esc_html_e( 'Setup Wizard', 'storelly-product-builder-for-woocommerce' ); ?>
     1503                    </h3>
     1504                </div>
     1505                <p class="spbwc-quick-card__desc">
     1506                    <?php esc_html_e( 'Import sample products, or turn your existing WooCommerce variations into pricing options in one step.', 'storelly-product-builder-for-woocommerce' ); ?>
     1507                </p>
     1508                <div class="spbwc-quick-card__footer">
     1509                    <span class="spbwc-cta-btn">
     1510                        <?php esc_html_e( 'Open Setup Wizard', 'storelly-product-builder-for-woocommerce' ); ?>
    14911511                        <span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span>
    14921512                    </span>
  • storelly-product-builder-for-woocommerce/trunk/views/setup-wizard/landing.php

    r3568219 r3768035  
    157157                </div>
    158158        </section>
     159        <?php if ( class_exists( 'SPBWC_Settings_Tabs' ) ) { SPBWC_Settings_Tabs::render( 'setup' ); } ?>
    159160
    160161        <?php if ( $spbwc_demo_cleaned >= 0 ) : ?>
  • storelly-product-builder-for-woocommerce/trunk/views/setup-wizard/woo-seed.php

    r3568219 r3768035  
    4242                </div>
    4343        </section>
     44        <?php if ( class_exists( 'SPBWC_Settings_Tabs' ) ) { SPBWC_Settings_Tabs::render( 'setup' ); } ?>
    4445
    4546        <div id="spbwc-woo-seed-app" class="spbwc-block spbwc-ws-app">
Note: See TracChangeset for help on using the changeset viewer.