From 68f2623a0e91116bef9e593671f7c6a80d5b6d56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rabbi=20Islam=20Rony=20=E2=9A=A1=EF=B8=8F?= <35329385+RabbiIslamRony@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:45:03 +0600 Subject: [PATCH 1/5] docs: add themes extensions page skill --- AGENTS.md | 1 + .../README.md | 47 +++ .../SKILL.md | 140 +++++++ .../references/account-license-journey-map.md | 120 ++++++ .../references/api-data-flow-report.md | 392 ++++++++++++++++++ .../references/dynamic-data-contract.md | 131 ++++++ .../references/page-architecture-map.md | 113 +++++ .../performance-improvement-guide.md | 140 +++++++ .../references/qa-checklist.md | 115 +++++ .../references/rewrite-issue-register.md | 212 ++++++++++ 10 files changed, 1411 insertions(+) create mode 100644 docs/agents/directorist-themes-extensions-page/README.md create mode 100644 docs/agents/directorist-themes-extensions-page/SKILL.md create mode 100644 docs/agents/directorist-themes-extensions-page/references/account-license-journey-map.md create mode 100644 docs/agents/directorist-themes-extensions-page/references/api-data-flow-report.md create mode 100644 docs/agents/directorist-themes-extensions-page/references/dynamic-data-contract.md create mode 100644 docs/agents/directorist-themes-extensions-page/references/page-architecture-map.md create mode 100644 docs/agents/directorist-themes-extensions-page/references/performance-improvement-guide.md create mode 100644 docs/agents/directorist-themes-extensions-page/references/qa-checklist.md create mode 100644 docs/agents/directorist-themes-extensions-page/references/rewrite-issue-register.md diff --git a/AGENTS.md b/AGENTS.md index 296bf9c655..2a5394c0b2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -326,6 +326,7 @@ Widgets: all-listing, all-categories, all-locations, category, location, tag, se **Lifecycle:** Authentication → download → install → activate (via AJAX) **Key AJAX actions:** `atbdp_authenticate_the_customer`, `atbdp_download_file`, `atbdp_install_file_from_subscriptions` **Filters:** `directorist_extensions_aliases` (deprecated name mapping), `directorist_required_extensions` +**Themes & Extensions page agent:** Before redesigning, auditing, adding no-reload interactions, or changing product/account flows on `edit.php?post_type=at_biz_dir&page=atbdp-extension`, load `docs/agents/directorist-themes-extensions-page/SKILL.md`. Re-collect dynamic product/account state every time and do not store runtime snapshots as docs truth. ## Notes diff --git a/docs/agents/directorist-themes-extensions-page/README.md b/docs/agents/directorist-themes-extensions-page/README.md new file mode 100644 index 0000000000..e312c2150d --- /dev/null +++ b/docs/agents/directorist-themes-extensions-page/README.md @@ -0,0 +1,47 @@ +# Directorist Themes & Extensions Page Agent + +This folder contains the repo-local skill/playbook for agents working on the Directorist admin `Themes & Extensions` page. + +## What This Skill Is For + +Use it when working on: + +- `wp-admin/edit.php?post_type=at_biz_dir&page=atbdp-extension` +- Directorist account connection from the admin product page +- extension/theme subscription discovery +- product install, update, activation, bulk actions, or required-extension UI +- no-reload performance improvements on this page +- responsive redesign of this page from a new design + +Do not use it for the frontend user dashboard or installed extension settings unless the task directly connects those systems back to this admin page. + +## How To Use + +Tell the agent: + +```text +Use docs/agents/directorist-themes-extensions-page/SKILL.md for this task. +``` + +Then provide the design, bug, feature request, or admin URL. + +The agent should inspect the live admin page with Agent Browser, read the mapped source files, cross-check Directorist docs when copy/product claims are involved, and produce a feasibility report before implementation. + +## Dynamic Data Policy + +This skill must not store site-specific runtime data. Product counts, installed items, subscribed products, update counts, connected user/account state, screenshots, and network logs must be collected fresh during each task and treated as temporary evidence only. + +## Files In This Package + +- `SKILL.md`: required agent workflow and safety rules +- `references/page-architecture-map.md`: code, templates, assets, hooks, endpoints, and remote dependencies +- `references/api-data-flow-report.md`: step-by-step source/API flow from page load to rendered sections and product actions +- `references/dynamic-data-contract.md`: dynamic data shapes and collection methods +- `references/account-license-journey-map.md`: account, license, subscription, and product-action journeys +- `references/performance-improvement-guide.md`: reload-heavy behavior and safe no-reload strategy +- `references/rewrite-issue-register.md`: full rewrite issue notes, fix priorities, and regression checklist +- `references/qa-checklist.md`: verification matrix and destructive-action safeguards + +## Maintenance + +Update this package only with durable architecture facts, stable contracts, confirmed issue patterns, and safe workflow improvements. Do not add local runtime snapshots. diff --git a/docs/agents/directorist-themes-extensions-page/SKILL.md b/docs/agents/directorist-themes-extensions-page/SKILL.md new file mode 100644 index 0000000000..041e881dd2 --- /dev/null +++ b/docs/agents/directorist-themes-extensions-page/SKILL.md @@ -0,0 +1,140 @@ +--- +name: directorist-themes-extensions-page-agent +description: 'Use this repo-local skill when redesigning, auditing, planning, testing, or implementing features for the Directorist WordPress admin Themes & Extensions page at edit.php?post_type=at_biz_dir&page=atbdp-extension. Trigger for work on Directorist extension/theme discovery, account connection, subscription/license management, install/update/activate flows, no-reload performance improvements, or designs that touch this admin page. This skill is strict: re-collect dynamic runtime state every time, preserve existing customer compatibility, and never run destructive product actions without explicit confirmation.' +--- + +# Directorist Themes & Extensions Page Agent + +This skill guides safe work on the Directorist admin `Themes & Extensions` page. The page is production-critical because it connects customer accounts, subscriptions, product downloads, plugin/theme activation, updates, and promotional product discovery. + +## Core Rule + +Re-collect current runtime data on every task. Do not treat previously observed product counts, subscription contents, installed item lists, update counts, user names, screenshots, or local site values as source of truth. + +Store and use architecture, data contracts, selectors, endpoint names, and journey rules only. + +## Required Context Pass + +Before planning or editing, inspect these files: + +- `includes/classes/class-extension.php` +- `views/admin-templates/theme-extensions/theme-extension.php` +- `views/admin-templates/theme-extensions/auth/license-auth-section.php` +- `views/admin-templates/theme-extensions/statistics/statistics.php` +- `views/admin-templates/theme-extensions/my-themes-extensions/my-themes-extensions.php` +- `views/admin-templates/theme-extensions/my-themes-extensions/extensions-tab.php` +- `views/admin-templates/theme-extensions/my-themes-extensions/themes-tab.php` +- `views/admin-templates/theme-extensions/all-themes-extensions.php` +- `assets/src/js/admin/components/subscriptionManagement.js` +- `assets/src/js/admin/admin.js` +- `assets/src/scss/layout/admin/admin-style.scss` +- `includes/asset-loader/helper.php` +- `includes/asset-loader/init.php` +- `includes/asset-loader/scripts.php` +- `includes/asset-loader/localized_data.php` + +For product docs, copy, labels, or public claims, cross-check: + +- `README.md` +- `readme.txt` +- Official Directorist docs: `https://directorist.com/documentation/directorist/` +- Official themes docs: `https://directorist.com/documentation/themes/` +- Official extensions docs: `https://directorist.com/documentation/extensions` + +Load reference files as needed: + +- Architecture and file map: `references/page-architecture-map.md` +- API/source flow report: `references/api-data-flow-report.md` +- Runtime state and data shapes: `references/dynamic-data-contract.md` +- Account/license journeys: `references/account-license-journey-map.md` +- Performance and no-reload guidance: `references/performance-improvement-guide.md` +- Full rewrite issue register and fix priorities: `references/rewrite-issue-register.md` +- Verification checklist: `references/qa-checklist.md` + +## Live Inspection Requirement + +Use Agent Browser for the live admin page when a logged-in session is available: + +```bash +agent-browser --session directorist-themes-extensions --profile Default --ignore-https-errors open "https://directorist-core.local/wp-admin/edit.php?post_type=at_biz_dir&page=atbdp-extension" +agent-browser --session directorist-themes-extensions snapshot -c -d 4 +agent-browser --session directorist-themes-extensions eval 'JSON.stringify({url:location.href,title:document.title,connected:!!document.querySelector("#purchase-refresh-form"),auth:!!document.querySelector("#atbdp-directorist-license-login-form"),overflow:document.documentElement.scrollWidth>innerWidth})' +``` + +Treat this as an inspection step only. Do not save the collected runtime values into this skill or its references. + +Close the browser session after inspection: + +```bash +agent-browser --session directorist-themes-extensions close +``` + +## Safety Rules + +- Never click or automate Install, Update, Activate, Deactivate, Uninstall, Logout, Refresh Purchase, or theme switch actions on a real/client site without explicit confirmation for that exact action. +- Preserve the page slug `atbdp-extension`, parent post type `at_biz_dir`, capability `manage_options`, existing AJAX action names, nonce expectations, response compatibility, settings links, filters, aliases, and legacy user meta keys. +- Preserve legacy misspellings that are part of stored data or public contracts, including `_atbdp_has_subscriptions_sassion` and `atbdp_close_subscriptions_sassion`. +- Do not remove or repurpose old selectors, classes, template paths, filters, or action hooks without a compatibility shim and explicit migration plan. +- Treat install, update, download, uninstall, and theme activation as high-risk operations. Use read-only inspection by default. +- Keep this page separate from installed extension settings. Settings-panel work belongs to `docs/agents/directorist-settings-panel/SKILL.md`. +- Treat Figma/design input as design intent. Do not rewrite backend product/install logic only to match a visual design. + +## Performance Rule + +This page is PHP-rendered admin markup plus legacy jQuery. Improve performance through progressive enhancement first: + +1. Keep the current server-rendered page as canonical fallback. +2. Add small, tested no-reload flows around existing AJAX responses. +3. Re-fetch or re-render canonical state after expensive server actions. +4. Keep full page reload fallback when state cannot be safely reconciled. + +Preferred rewrite architecture: keep PHP-rendered templates as the compatibility baseline, introduce focused PHP service/resolver classes behind existing AJAX actions, and add a small page-specific JavaScript state adapter for no-reload UI updates. + +Do not replace the page with Vue/React or a new persistence/API layer unless the user explicitly approves that architecture change after seeing the compatibility risks. Vue 2 is legacy in the settings panel, and React should only be considered for this page after an explicit full admin UI migration decision. + +## Required First Output + +Before implementation, produce a compact report: + +```markdown +# Themes & Extensions Feasibility Report + +## Summary +- Request: +- Recommended approach: +- Risk level: + +## Dynamic State Rechecked +- Live page: +- Connected/account state: +- Product state source: +- Docs checked: + +## Easy To Achieve +- ... + +## Complex Or Risky +- ... + +## Affected Areas +- PHP/templates: +- AJAX/server actions: +- JS/state updates: +- SCSS/responsive: +- External Directorist/EDD APIs: + +## Compatibility Rules +- ... + +## Verification Plan +- Static checks: +- Agent Browser checks: +- No-reload/performance checks: +- Destructive-action policy: +``` + +If the task is a small docs-only update, the report can be shorter but must still say whether dynamic state was intentionally not stored. + +## Maintenance + +Update the reference files when a future task confirms a durable architecture fact, compatibility rule, or recurring issue. Do not update them with local runtime counts, purchased product lists, user-specific state, or one-time screenshots. diff --git a/docs/agents/directorist-themes-extensions-page/references/account-license-journey-map.md b/docs/agents/directorist-themes-extensions-page/references/account-license-journey-map.md new file mode 100644 index 0000000000..6ffde7e84c --- /dev/null +++ b/docs/agents/directorist-themes-extensions-page/references/account-license-journey-map.md @@ -0,0 +1,120 @@ +# Account And License Journey Map + +This map covers the Directorist.com account, license, subscription, and product-management journey inside the admin `Themes & Extensions` page. It does not cover the frontend `[directorist_user_dashboard]`. + +## Page Load Journey + +1. WordPress loads the admin submenu page `atbdp-extension`. +2. `ATBDP_Extensions::initial_setup()` prepares aliases, required extensions, product lists, and update data. +3. `show_extension_view()` reads current user meta to decide whether the account is connected. +4. The root template renders either the account connection form or connected account product-management UI. +5. The promo catalog is rendered as product discovery regardless of account state. + +All values are dynamic and must be re-collected on each task. + +## Logged-Out Account Journey + +1. Template `auth/license-auth-section.php` renders `#atbdp-directorist-license-login-form`. +2. `subscriptionManagement.js` intercepts submit. +3. AJAX action `atbdp_authenticate_the_customer` sends username, password, and nonce. +4. `authenticate_the_customer()` calls the Directorist remote account/licensing endpoint. +5. On success, user meta is updated with connected state and available subscription data. +6. Current JS reloads the page after success. + +Performance opportunity: replace unconditional reload with a state refresh/render step, but keep reload fallback when canonical state cannot be reconstructed safely. + +## Disconnected Account Recommendation + +When the account is not connected, preserve the current page behavior: render the Directorist account-connect form and the promo marketplace/discovery section only. + +- Do not expose installed premium product management in the disconnected page state by default. +- Do not show Settings, Active status, Activate, Deactivate, Update, or local management actions for installed premium products while disconnected unless a future task explicitly changes this policy. +- Already installed premium products can continue working in WordPress, but this page should not manage them while the Directorist account session is disconnected. +- Premium install, package download, update, refresh purchase, and license validation must require connected account/subscription data. +- The disconnected state should include a clear account-connect CTA for managing subscriptions, installs, and updates. +- Recommended helper copy: `Already installed extensions will keep working. Connect your Directorist account to manage subscriptions, installs, and updates.` +- This keeps the safest behavior for existing customer sites while reducing confusion about whether installed premium products stop working when the account is disconnected. + +## Connected Account Journey + +1. Statistics section renders current extension/theme availability and update status from server-side overview data. +2. `my-themes-extensions.php` renders tabs for extension and theme management. +3. Installed products, subscribed products, required products, active theme, and available subscription themes are all generated from live data. +4. Refresh Purchase opens a password confirmation flow and calls `atbdp_refresh_purchase_status`. +5. Logout calls `atbdp_close_subscriptions_sassion` and clears connected account state. + +Do not store the resulting product or account values in docs. + +## Product Action Journeys + +### Install From Subscription + +- UI selector family: `.file-install-btn` +- AJAX action: `atbdp_install_file_from_subscriptions` +- Server path: `handle_file_install_request_from_subscriptions()` -> `install_file_from_subscriptions()` +- Risk: filesystem download, unzip/copy, license activation, package host validation, plugin/theme destination changes. + +### Activate Plugin + +- UI selector family: `.plugin-active-btn` +- AJAX action: `atbdp_activate_plugin` +- Server path: `activate_plugin()` +- Risk: activation can trigger extension code, dependencies, fatal errors, or `WP_Error`. + +### Update Plugin + +- UI selector family: `.ext-update-btn` +- AJAX action: `atbdp_update_plugins` +- Server path: `handle_plugins_update_request()` -> update/download helpers +- Risk: remote version checks, download package, filesystem replacement. + +### Bulk Plugin Action + +- Form: `#atbdp-my-extensions-form` +- AJAX action: `atbdp_plugins_bulk_action` +- Supported tasks include activate, deactivate, and uninstall. +- Risk: uninstall is destructive and must not be automated without explicit confirmation. + +### Plugin Uninstall Recommendation + +- Keep uninstall available for compatibility with the existing page, but make it a protected danger action in the rewrite. +- Do not expose uninstall as an easy primary action. +- Place uninstall under a danger/overflow menu with clear destructive styling. +- Require an explicit confirmation modal that shows the extension name and explains that plugin files will be deleted and site features may break. +- Do not run uninstall from Agent Browser or tests on a real/client site without explicit confirmation for that exact action. +- After server success, re-check canonical WordPress plugin state before updating the UI; keep full reload fallback when state cannot be reconciled. + +### Required Extensions + +- Form: `#atbdp-required-extensions-form` +- UI can show install, activate, or external get-now paths depending on ownership and install state. +- Required extension data comes from `directorist_required_extensions` and current product/install state. + +### Theme Activate/Update + +- Theme activation action: `atbdp_activate_theme` +- Theme update action: `atbdp_update_theme` +- Risk: `switch_theme()` changes the live site theme, and update/download touches theme files. +- Locked recommendation: every theme activation/switch must require an explicit confirmation modal before calling `atbdp_activate_theme`. +- The confirmation must name the theme and warn that activating it changes the live site's active theme and may affect layout, menus, widgets, headers/footers, and theme settings. +- Theme activation should not be treated like a normal button status change. It is a live-site change and must not run from accidental click, hover, keyboard focus, or Agent Browser automation without explicit confirmation. +- After server success, re-check canonical active theme state before updating UI; keep full reload fallback when state cannot be safely reconciled. + +## External Handoff Points + +The page can link outside wp-admin for: + +- Product details +- Get Now / purchase flows +- Demo links +- Directorist.com dashboard/account/support +- Official docs + +External links must open intentionally and should not be mistaken for local AJAX state. + +## Journey Safety + +- Inspect forms and selectors read-only by default. +- Use mocked AJAX, local throwaway sites, or explicit approval for action testing. +- For no-reload improvements, update the local UI only after server success and then reconcile with canonical server state. +- Keep full reload fallback available for remote/API/filesystem failures. diff --git a/docs/agents/directorist-themes-extensions-page/references/api-data-flow-report.md b/docs/agents/directorist-themes-extensions-page/references/api-data-flow-report.md new file mode 100644 index 0000000000..1f5720cbcb --- /dev/null +++ b/docs/agents/directorist-themes-extensions-page/references/api-data-flow-report.md @@ -0,0 +1,392 @@ +# API And Data Flow Report + +This report documents durable data sources and API routes for the Directorist admin `Themes & Extensions` page. It must not contain runtime product counts, installed product lists, subscribed product lists, user names, screenshots, or one-time AJAX payloads from a specific site. + +## Summary + +The page data does not come from one API. It is assembled from: + +- Local Directorist product definitions in `ATBDP_Extensions`. +- Optional remote product catalog data from `app.directorist.com`. +- Directorist.com account/license data fetched through WordPress AJAX handlers. +- Local WordPress plugin, theme, update, option, transient, and user-meta state. +- Existing `admin-ajax.php` actions that connect the jQuery admin UI to PHP handlers. + +Frontend JavaScript should not call Directorist.com directly. Keep the browser talking to local WordPress AJAX, then let PHP own remote API calls, WordPress state reads, filesystem work, and compatibility behavior. + +## Step-By-Step Page Load Flow + +1. WordPress loads `wp-admin/edit.php?post_type=at_biz_dir&page=atbdp-extension`. +2. `ATBDP_Extensions::__construct()` registers the admin menu, AJAX actions, menu styling, and `initial_setup()` only for this page. +3. `setup_ajax_actions()` registers the `wp_ajax_atbdp_*` action handlers when the current admin can `manage_options`. +4. `initial_setup()` prepares extension aliases, calls `wp_update_plugins()`, reads required extensions from `directorist_required_extensions`, and calls `setup_products_list()`. +5. `setup_products_list()` builds `$this->extensions` and `$this->themes`. +6. `show_extension_view()` reads connected-account state from user meta, builds extension/theme overview arrays, and loads `admin-templates/theme-extensions/theme-extension`. +7. The root template renders the account-connect form when disconnected, or statistics plus managed products when connected, and always renders the promo marketplace section. + +## Product Catalog Source + +Default behavior is local-first: + +- `ATBDP_Extensions::$load_from_api` is `false`. +- Extensions come from `ATBDP_Extensions::get_default_extensions()`. +- Themes come from `ATBDP_Extensions::get_default_themes()`. +- Filters can modify these lists: + - `atbdp_extension_list` + - `atbdp_theme_list` + +Optional remote catalog path: + +- `Directorist\Core\API::get_products()` +- Base URL: `https://app.directorist.com/wp-json/directorist/` +- Endpoint: `v1/get-remote-products` +- Effective URL: `https://app.directorist.com/wp-json/directorist/v1/get-remote-products` +- Cache key: `directorist_products` +- Cache duration: `30 * DAY_IN_SECONDS` +- Empty remote response falls back to local defaults. + +Use the product catalog for product names, descriptions, thumbnails, product links, demo links, active promo flags, item IDs, optional plugin base overrides, and future optional product badge/status metadata. Do not store fetched catalog output in docs. + +Product copy/source policy for rewrite: + +- Prefer product API data for product name, description, thumbnail, product link, demo link, active promo flag, item ID, and plugin base when the API provides usable values. +- Keep local product arrays as safe fallback for product name, description, thumbnail, product link, demo link, item ID, and plugin base when the API is unavailable, empty, malformed, missing a field, or disabled. +- Do not render blank cards only because remote copy is missing; merge API data over local defaults by product key where possible. +- Cross-check public product claims against local `README.md`/`readme.txt` and official Directorist docs before changing visible copy. +- Badge/status values are different: render them only from product API or explicit local filters. Do not invent hardcoded badge/status fallback from local copy, product order, slug, or name. + +Future badge/status support should be added to this catalog contract as optional API data. EDD product meta or a dedicated product badge setting on Directorist.com can feed the API, but the core plugin UI should consume the product API field. Do not use normal EDD/WordPress `post_status` as the badge source because it represents product availability, not display labels such as `New`. + +Recommended optional badge object: + +```json +{ + "type": "new", + "label": "New", + "expires_at": "2026-09-01" +} +``` + +Render no badge when this field is absent, expired, malformed, or disabled by filters. + +## Account Connection API + +Browser action: + +- Form: `#atbdp-directorist-license-login-form` +- JavaScript file: `assets/src/js/admin/components/subscriptionManagement.js` +- Local AJAX URL: `directorist_admin.ajaxurl` +- AJAX action: `atbdp_authenticate_the_customer` +- Request fields: `username`, `password`, `nonce` + +Server handler: + +- `ATBDP_Extensions::authenticate_the_customer()` +- Remote method: `ATBDP_Extensions::remote_authenticate_user()` +- Remote endpoint: `https://directorist.com/wp-json/directorist/v1/licencing` +- Method in code: `wp_remote_get()` +- Request body: `user`, `password` +- Headers include a Directorist user-agent and `Accept: application/json`. + +Successful response is normalized into `license_data`, then stored under current user meta: + +- `_atbdp_subscribed_username` +- `_atbdp_has_subscriptions_sassion` +- `_themes_available_in_subscriptions` +- `_plugins_available_in_subscriptions` + +Keep the `sassion` spelling because it is part of the existing data/action contract. + +## Subscription Reads + +After connection, the page usually reads subscription data from local user meta instead of calling Directorist.com on every page load. + +Extension subscription reader: + +- `ATBDP_Extensions::get_purchased_extension_list()` +- Meta key: `_plugins_available_in_subscriptions` +- Filter: `directorist_purchased_extension_list` + +Theme subscription reader: + +- `ATBDP_Extensions::get_purchased_theme_list()` +- Meta key: `_themes_available_in_subscriptions` +- Filter: `directorist_purchased_theme_list` + +The subscription arrays are keyed from product permalinks by stripping: + +- `http://directorist.com/product/` +- `https://directorist.com/product/` +- `/` + +## Refresh Purchase Flow + +Browser action: + +- Form/action area: purchase refresh form in connected account UI +- AJAX action: `atbdp_refresh_purchase_status` +- Request fields: `password`, `nonce` + +Server flow: + +1. `handle_refresh_purchase_status_request()` validates capability and nonce. +2. `refresh_purchase_status()` reads `_atbdp_subscribed_username`. +3. It calls `remote_authenticate_user()` again against `https://directorist.com/wp-json/directorist/v1/licencing`. +4. It rewrites `_themes_available_in_subscriptions` and `_plugins_available_in_subscriptions`. +5. If the username/session is missing, it deletes `_atbdp_has_subscriptions_sassion` and returns a reload-required status. + +Current JavaScript reloads after success. A future no-reload version should refresh canonical state through a server response or state-summary endpoint. + +## Logout Flow + +Browser action: + +- Button selector family: `.subscriptions-logout-btn` +- AJAX action: `atbdp_close_subscriptions_sassion` +- Request fields: `hard_logout`, `nonce` + +Server flow: + +- Always deletes `_atbdp_has_subscriptions_sassion`. +- If hard logout is enabled, also deletes: + - `_atbdp_subscribed_username` + - `_themes_available_in_subscriptions` + - `_plugins_available_in_subscriptions` + +Current JavaScript reloads after success. + +## Installed Extension Data + +Installed extension state is local WordPress data: + +- `get_plugins()` +- `is_plugin_active()` +- official extension keys from `$this->extensions` +- alias map from `directorist_extensions_aliases` + +`get_extensions_overview()` builds: + +- installed extension list +- active extension total +- outdated extension list +- available-in-subscription list +- promo extension list +- required extension list + +Use this server-side overview as canonical for rendered extension sections. + +## Extension Update API + +Browser action: + +- Button selector family: `.ext-update-btn` +- AJAX action: `atbdp_update_plugins` +- Request fields: optional `plugin_key`, `nonce` + +Server flow: + +1. `handle_plugins_update_request()` validates capability and nonce. +2. `update_plugins()` calls `get_outdated_extensions_via_api()`. +3. `get_outdated_extensions_via_api()` reads installed plugins via `get_plugins()`. +4. It reads purchased extensions from `_plugins_available_in_subscriptions`. +5. It checks each eligible installed product against EDD version data. + +Remote EDD version check: + +- Method: `wp_remote_post()` +- Endpoint: `https://directorist.com` +- Body fields: + - `edd_action=get_version` + - `license` + - `item_id` + - `version` + - `slug` + - `author=AazzTech` + - `url=home_url()` + - `beta=false` +- SSL verification is controlled by `edd_sl_api_request_verify_ssl`. +- Cache key prefix: `directorist_ext_version_` +- Cache duration: `3 * HOUR_IN_SECONDS` + +If update is requested, download URL is fetched through the product-data API before filesystem update. + +## Theme Data And Update Flow + +Theme state is local WordPress theme data: + +- `wp_get_theme()` +- `wp_get_themes()` +- `get_option('stylesheet')` +- `get_site_transient('update_themes')` +- WordPress theme screenshot/customizer APIs + +Browser actions: + +- Theme activation: `atbdp_activate_theme` +- Theme update: `atbdp_update_theme` + +Theme activation calls `switch_theme()`, so it must be treated as live-site destructive behavior. + +Theme update flow: + +1. `handle_theme_update_request()` validates capability and nonce. +2. `update_the_themes()` checks `get_site_transient('update_themes')`. +3. It reads purchased themes from `_themes_available_in_subscriptions`. +4. It calls `get_file_download_link()` or falls back to the WordPress theme update package when available. +5. It downloads and installs through `download_theme()`. + +## Download Link API + +Used for plugin and theme update/install flows when a subscription item has a license and item ID. + +- Method: `wp_remote_get()` +- Endpoint: `https://directorist.com/wp-json/directorist/v1/get-product-data/` +- Request body: + - `product_type` + - `license` + - `item_id` + - `get_info=download_link` + - optional `beta=true` +- Returns: download URL in `response['data']` +- Current code sets `sslverify` to `false`. + +Downloaded packages are then passed to `download_plugin()` or `download_theme()`. Current host verification allows `directorist.com` only in `is_varified_host()`. + +## License Activation API + +Used before install/download flows and when adding products to `_atbdp_purchased_products`. + +- Method: `wp_remote_get()` +- Endpoint: `https://directorist.com` +- Request body: + - `edd_action=activate_license` + - `url=home_url()` + - `item_id` + - `license` +- Current code sets `sslverify` to `false`. +- `item_name_mismatch` can still be treated as success when returned `item_id` matches the requested item ID. + +Successful activation can update `_atbdp_purchased_products`. + +## Install From Subscription Flow + +Browser action: + +- Button selector family: `.file-install-btn` +- AJAX action: `atbdp_install_file_from_subscriptions` +- Request fields: `item_key`, `type`, `nonce` + +Server flow: + +1. `handle_file_install_request_from_subscriptions()` validates capability and nonce. +2. `install_file_from_subscriptions()` validates item key and product type. +3. It selects subscription source: + - `plugin` -> `_plugins_available_in_subscriptions` + - `theme` -> `_themes_available_in_subscriptions` +4. It confirms the item exists in the current subscription list. +5. It activates the license through EDD. +6. It selects beta or normal download link. +7. It calls `download_plugin()` or `download_theme()`. + +This is high-risk because it can write plugin/theme files. + +## Plugin Activation And Bulk Actions + +Plugin activation: + +- Browser selector family: `.plugin-active-btn` +- AJAX action: `atbdp_activate_plugin` +- Server action: `activate_plugin($plugin_key)` + +Bulk installed-extension form: + +- Form: `#atbdp-my-extensions-form` +- AJAX action: `atbdp_plugins_bulk_action` +- Tasks: `activate`, `deactivate`, `uninstall` +- Bulk nonce field in JS: `directorist_nonce` + +Uninstall calls `delete_plugins()`, so it must not be automated on real/client sites without explicit confirmation. + +## Required Extensions + +Required extensions do not come directly from a remote endpoint. They are composed from: + +- `directorist_required_extensions` filter +- current official extension catalog +- extension alias map +- purchased extension user meta +- plugin folder existence under the plugins directory +- active plugin option state + +`prepare_the_final_requred_extension_list()` outputs required products with: + +- recommending references +- base plugin file +- purchased state +- installed state + +The UI then decides whether to show install, activate, or external get-now actions. + +## Promo Product Rendering + +Promo cards are rendered from `$this->extensions` and `$this->themes`, after filtering out installed/subscribed products for connected users. + +Promo links are passed through `ATBDP_Upgrade::promo_link()`. + +These links can leave wp-admin for Directorist product, purchase, demo, account, or support pages. Treat them as external handoff points, not local state transitions. + +## Rendered Sections + +Root template: + +- Disconnected: account connect form. +- Connected: statistics plus managed themes/extensions. +- Always: all themes/extensions promo marketplace. + +Statistics section uses: + +- active extension total +- available extension total +- available theme total +- extension update total +- theme update total + +Installed extensions section uses: + +- `installed_extension_list` +- `outdated_plugin_list` +- `extension_list` +- `settings_url` + +Subscribed extensions section uses: + +- `extensions_available_in_subscriptions` +- `extension_list` +- alias map + +Themes tab uses: + +- `current_active_theme_info` +- `themes_available_in_subscriptions` +- installed theme data +- update state + +## Known API/Data Issues + +- `handle_file_download_request()` has `if ( 'plugin' !== $type || 'theme' !== $type )`, which is always invalid for both valid types. Prefer `atbdp_install_file_from_subscriptions` path unless this is intentionally fixed. +- `activate_plugin()` currently calls `activate_plugin()` without checking `WP_Error`. +- `handle_license_activation_request()` exists but is not registered in `setup_ajax_actions()`. +- `get_customers_purchased()` references stale/undefined variables and should not be reused blindly. +- Many successful AJAX paths immediately call `location.reload()`. +- Remote calls mix REST-style Directorist endpoints and EDD action endpoints. +- Some remote calls set `sslverify` to `false`; future hardening must be planned carefully for existing customers. + +## Redesign Guidance + +For no-reload or page-speed improvements: + +1. Keep browser calls pointed at local `admin-ajax.php`. +2. Keep PHP as the remote API and WordPress state boundary. +3. Add a stable server-side state summary before removing reloads. +4. Let expensive actions show progress in-place, then reconcile with canonical server state. +5. Keep full page reload as fallback for install, update, activation, theme switch, remote API failure, and filesystem failure. diff --git a/docs/agents/directorist-themes-extensions-page/references/dynamic-data-contract.md b/docs/agents/directorist-themes-extensions-page/references/dynamic-data-contract.md new file mode 100644 index 0000000000..ec80c0074e --- /dev/null +++ b/docs/agents/directorist-themes-extensions-page/references/dynamic-data-contract.md @@ -0,0 +1,131 @@ +# Dynamic Data Contract + +This page is driven by live WordPress state, remote Directorist/EDD state, user meta, filters, and current filesystem/plugin/theme state. Do not store runtime output in this docs agent. + +## What Not To Store + +Never commit these values into the skill: + +- Product counts +- Installed extension lists +- Active/inactive extension lists +- Subscribed extension or theme lists +- Update counts +- Current theme name +- Connected username/account details +- Local screenshots or network logs +- Site-specific AJAX responses + +Use those values only as temporary evidence for the current task. + +## Product Item Shapes + +Extension catalog items commonly include: + +- `name` +- `description` +- `link` +- `thumbnail` +- `active` +- `item_id` +- `base` when the plugin base differs from the catalog slug +- optional `badge` object when remote/API support exists + +Theme catalog items commonly include: + +- `name` +- `description` +- `link` +- `demo_link` +- `thumbnail` +- `active` +- optional `badge` object when remote/API support exists + +## Product Copy Source Policy + +- Product API data is the preferred source for product name, description, thumbnail, product link, demo link, active promo flag, item ID, and plugin base when valid API values exist. +- Local default product arrays remain the fallback source for product name, description, thumbnail, product link, demo link, item ID, and plugin base. +- Merge API data over local defaults by product key when possible so missing remote fields do not create blank cards. +- If the API is unavailable, empty, malformed, or missing a non-badge field, render the local fallback field. +- Badge/status fields do not use hardcoded local fallback. Render badge/status only when provided by product API data or explicit filters. +- Do not store the current merged catalog output in this skill. + +Optional product badge/status shape for future rewrite: + +- `badge.type`: machine-readable badge type such as `new`, `beta`, `popular`, `sale`, or `featured` +- `badge.label`: display label such as `New` +- `badge.expires_at`: optional expiration date/time; expired badges must not render + +Badge/status data should come from Directorist.com product API data. EDD product meta, a dedicated product badge setting, or taxonomy can be the upstream source on Directorist.com, but the core plugin should consume the API field. Local product-list filters may add or override badge data for compatibility/testing. Do not infer badges from product order, names, slugs, or local runtime state. + +Purchased/subscribed items returned from the account journey may include title, slug/key, item id, license, URL, download/package data, or product type. Re-check the current handler before relying on a field. + +## Runtime State Categories + +The UI must handle these dynamic states: + +- Not connected to Directorist account +- Not connected while Directorist premium products may already be installed locally; the page should still keep the current disconnected UI unless explicitly changed +- Not connected messaging should clarify that already installed extensions keep working, while account connection is required to manage subscriptions, installs, and updates +- Connected account with no purchased products +- Connected account with subscribed plugins +- Connected account with subscribed themes +- Product installed and inactive +- Product installed and active +- Product installed and outdated +- Product installed with expired or missing license/update entitlement +- Product subscribed but not installed +- Product required by another feature +- Product required but not purchased +- Promo-only product +- Promo/subscribed/installed product with optional API-provided badge +- Active theme +- Installed inactive theme +- Subscribed theme not installed +- Theme update available +- Remote API failure +- Filesystem/download failure +- Capability or nonce failure +- Beta package mode + +These are categories, not counts. Always collect current values during the task. + +## Current State Collection + +Use read-only browser checks: + +```bash +agent-browser --session directorist-themes-extensions --profile Default --ignore-https-errors open "https://directorist-core.local/wp-admin/edit.php?post_type=at_biz_dir&page=atbdp-extension" +agent-browser --session directorist-themes-extensions snapshot -c -d 4 +agent-browser --session directorist-themes-extensions eval 'JSON.stringify({connected:!!document.querySelector("#purchase-refresh-form"),auth:!!document.querySelector("#atbdp-directorist-license-login-form"),hasRequired:!!document.querySelector("#atbdp-required-extensions-form"),overflow:document.documentElement.scrollWidth>innerWidth})' +``` + +Use source checks: + +```bash +rg -n "setup_ajax_actions|show_extension_view|setup_products_list|get_extensions_overview|get_themes_overview" includes/classes/class-extension.php +rg -n "location.reload|atbdp_|file-install-btn|plugin-active-btn|theme-activate-btn" assets/src/js/admin/components/subscriptionManagement.js +rg -n "theme-extensions|atbdp-extension|directorist_required_extensions|directorist_extensions_aliases" includes views assets/src/js assets/src/scss +``` + +Close the browser session when finished: + +```bash +agent-browser --session directorist-themes-extensions close +``` + +## Compatibility Contracts + +Keep these stable unless a migration plan is explicitly approved: + +- Page slug: `atbdp-extension` +- Parent post type: `at_biz_dir` +- Capability: `manage_options` +- AJAX actions listed in `page-architecture-map.md` +- User meta keys for connected account and subscriptions +- Filters for product lists, required extensions, and aliases +- Settings links back to the settings panel +- Existing template override expectations for admin templates +- Existing selector/class hooks used by custom CSS or scripts + +New no-reload behavior must preserve the same server-side action contracts or provide a compatibility wrapper. diff --git a/docs/agents/directorist-themes-extensions-page/references/page-architecture-map.md b/docs/agents/directorist-themes-extensions-page/references/page-architecture-map.md new file mode 100644 index 0000000000..1b8ec43276 --- /dev/null +++ b/docs/agents/directorist-themes-extensions-page/references/page-architecture-map.md @@ -0,0 +1,113 @@ +# Themes & Extensions Page Architecture Map + +Use this map for source orientation. Re-check files before each task because product catalogs, active plugins, subscriptions, themes, and update availability are dynamic. + +## Admin Entry + +- Page URL: `wp-admin/edit.php?post_type=at_biz_dir&page=atbdp-extension` +- Admin screen id: `at_biz_dir_page_atbdp-extension` +- Main class: `ATBDP_Extensions` in `includes/classes/class-extension.php` +- Menu registration: `admin_menu()` adds the submenu under `edit.php?post_type=at_biz_dir` +- Capability: `manage_options` +- Render callback: `show_extension_view()` + +## Server Lifecycle + +- Constructor registers `setup_ajax_actions()` on `admin_init`. +- When `$_GET['page']` is `atbdp-extension`, constructor also registers `initial_setup()` on `admin_init`. +- `initial_setup()` prepares extension aliases, calls WordPress plugin update checks, loads required extensions through `directorist_required_extensions`, and prepares products through `setup_products_list()`. +- `show_extension_view()` builds render data and loads `admin-templates/theme-extensions/theme-extension`. + +## Product Sources + +- Default extension list: `ATBDP_Extensions::get_default_extensions_list()` +- Default theme list: `ATBDP_Extensions::get_default_themes_list()` +- Local filters: + - `atbdp_extension_list` + - `atbdp_theme_list` + - `directorist_required_extensions` + - `directorist_extensions_aliases` +- Optional remote catalog: `Directorist\Core\API::get_products()` in `includes/classes/class-directorist-api.php` +- Remote catalog endpoint: `https://app.directorist.com/wp-json/directorist/v1/get-remote-products` +- Remote catalog cache: transient `directorist_products` + +Do not commit fetched product payloads into docs. Store only the source and contract. + +## Account And Subscription State + +- Connected-account flag user meta: `_atbdp_has_subscriptions_sassion` +- Connected username user meta: `_atbdp_subscribed_username` +- Subscribed plugins user meta: `_plugins_available_in_subscriptions` +- Subscribed themes user meta: `_themes_available_in_subscriptions` +- Refresh and logout behavior are controlled by class methods in `class-extension.php` and jQuery handlers in `subscriptionManagement.js`. + +Preserve the misspelled `sassion` keys/actions because they are compatibility contracts. + +## Templates + +- Root wrapper: `views/admin-templates/theme-extensions/theme-extension.php` +- Logged-out account form: `theme-extensions/auth/license-auth-section.php` +- Connected statistics: `theme-extensions/statistics/statistics.php` +- Connected product area: `theme-extensions/my-themes-extensions/my-themes-extensions.php` +- Extensions tab: `theme-extensions/my-themes-extensions/extensions-tab.php` +- Themes tab: `theme-extensions/my-themes-extensions/themes-tab.php` +- Promo marketplace: `theme-extensions/all-themes-extensions.php` + +Template output is server-rendered PHP and must remain usable without new JavaScript state management. + +## JavaScript And Assets + +- Admin entry: `assets/src/js/admin/admin.js` +- Page behavior: `assets/src/js/admin/components/subscriptionManagement.js` +- Enqueued admin script: `directorist-admin-script` +- Enqueued admin CSS: `directorist-admin-style` +- Localized object: `directorist_admin`, localized to `jquery` in `includes/asset-loader/localized_data.php` +- Screen detection: `includes/asset-loader/helper.php` +- Enqueue rules: `includes/asset-loader/init.php` +- Script handles: `includes/asset-loader/scripts.php` +- Main styles are currently in `assets/src/scss/layout/admin/admin-style.scss` + +## AJAX Actions + +Registered by `ATBDP_Extensions::setup_ajax_actions()`: + +- `atbdp_authenticate_the_customer` +- `atbdp_download_file` +- `atbdp_install_file_from_subscriptions` +- `atbdp_plugins_bulk_action` +- `atbdp_activate_theme` +- `atbdp_activate_plugin` +- `atbdp_update_plugins` +- `atbdp_update_theme` +- `atbdp_refresh_purchase_status` +- `atbdp_close_subscriptions_sassion` + +Most actions use `directorist_admin.nonce`; bulk plugin actions use `directorist_admin.directorist_nonce`. Re-check each handler before changing request shape. + +## Remote Dependencies + +- Directorist account authentication: `https://directorist.com/wp-json/directorist/v1/licencing` +- Product data/download links: `https://directorist.com/wp-json/directorist/v1/get-product-data/` +- EDD software licensing version checks: `https://directorist.com` with `edd_action=get_version` +- Optional remote product catalog: `https://app.directorist.com/wp-json/directorist/v1/get-remote-products` + +Network failures must leave the page usable with local defaults and clear error states. + +## Settings Page Connection + +The settings panel links admins back to this page, but it is a separate domain: + +- Settings URL pattern: `edit.php?post_type=at_biz_dir&page=atbdp-settings#extension_settings__extensions_general` +- Settings-panel skill: `docs/agents/directorist-settings-panel/SKILL.md` +- Do not document installed extension settings as core marketplace/discovery behavior. + +## Existing Issue Patterns + +- The current UI uses tables and fixed widths that can become fragile on narrow screens. +- `subscriptionManagement.js` uses many `location.reload()` calls after AJAX success. +- Old login continuation/download checklist code exists after an immediate reload and should be treated as stale unless revived intentionally. +- `handle_file_download_request()` must be reviewed carefully before reuse because the type validation path is known-risk. +- Plugin/theme install and update code touches the filesystem and must be treated as high risk. +- The theme "What's new" modal must not ship hardcoded dummy changelog content in a redesign. + +For full rewrite fix priorities, read `references/rewrite-issue-register.md` before planning implementation. diff --git a/docs/agents/directorist-themes-extensions-page/references/performance-improvement-guide.md b/docs/agents/directorist-themes-extensions-page/references/performance-improvement-guide.md new file mode 100644 index 0000000000..6b868b38cf --- /dev/null +++ b/docs/agents/directorist-themes-extensions-page/references/performance-improvement-guide.md @@ -0,0 +1,140 @@ +# Performance Improvement Guide + +This page currently uses PHP-rendered admin templates plus legacy jQuery in `subscriptionManagement.js`. It is not a Vue settings-manager page. + +## Current Tech + +- Server-rendered PHP templates produce the initial UI. +- jQuery handles form submits, button clicks, bulk selection, install/update/activate calls, tabs, and feedback. +- AJAX requests go through `admin-ajax.php` using `wp_ajax_atbdp_*` handlers. +- The page often reloads after successful AJAX actions to force canonical state. +- CSS is bundled through the legacy admin stylesheet. + +## Reload-Heavy Areas + +Review `assets/src/js/admin/components/subscriptionManagement.js` for `location.reload()` before planning performance work. + +Known reload patterns: + +- Account connect success +- Product install success +- Plugin activation success +- Plugin update success +- Theme activation success +- Theme update success +- Refresh purchase success or session reset +- Logout success +- Bulk action success +- Old reload link handler + +Do not remove reloads blindly. They currently protect correctness by forcing a full server re-render. + +## Recommended Direction + +Use progressive enhancement: + +1. Preserve the existing PHP-rendered page as the canonical fallback. +2. Add a small client-side state adapter for this page only. +3. Normalize current AJAX response handling into success, error, loading, and stale-state branches. +4. After successful low-risk actions, update affected buttons/cards/counters in place. +5. After high-risk filesystem or theme/plugin actions, re-fetch canonical state or keep reload fallback. +6. Record and compare reload count, network calls, interaction latency, and layout stability. + +Avoid a full Vue/React rewrite until the user explicitly approves a larger architecture change. + +## Framework Decision + +Default rewrite direction: use PHP-rendered templates plus a small page-specific JavaScript adapter, not Vue or React. + +The page rewrite is approved as a full UI rewrite, not a staged old/new UI rollout. Do not add a feature flag or rollout toggle by default unless a future task explicitly asks for it. Keep server-rendered compatibility behavior and reload fallbacks for safety; those are not the same as keeping two parallel UIs. + +Reasons: + +- This page is currently PHP templates plus legacy jQuery, not the Vue settings-manager app. +- Keeping server-rendered markup preserves fallback behavior for existing customer sites. +- A small adapter requires less code than a SPA and can be rolled back safely. +- Existing `wp_ajax_atbdp_*` actions, selectors, filters, user meta keys, and template paths can remain stable. +- Vue 2 is already legacy in the settings panel, so adding new Vue 2 surface area is not future-friendly. +- React is available in WordPress admin, but using it here would require a larger state/API migration and broader compatibility QA. + +Preferred implementation shape: + +1. Add PHP service/resolver classes behind current handlers for product catalog, account/session state, installed-product state, update state, install/update/download operations, and response formatting. +2. Keep the current PHP templates as the canonical render path and fallback. +3. Add a small JS adapter around existing AJAX calls for loading states, inline notices, card/row replacement, counters, and state-summary refresh. +4. Return structured responses from existing actions while preserving compatibility for older response readers. +5. Use full reload fallback when canonical state cannot be safely reconciled. + +Only consider React or a new SPA-style architecture if a future task explicitly approves a full admin UI migration strategy for this page. + +## Safer No-Reload Candidates + +- Account connect feedback before final state refresh +- Refresh Purchase status messages +- Button loading/error states +- Tab navigation and card visibility +- Non-destructive validation errors +- Promo/product detail modal interactions +- Counter/card updates after a server-confirmed state refresh + +## Conservative No-Reload Candidates + +- Plugin install +- Plugin update +- Plugin activation +- Theme update +- Theme activation +- Bulk activate/deactivate + +For these, show progress without reload, but reconcile with canonical server state before claiming completion. Keep reload fallback. + +## High-Risk Or Destructive + +- Plugin uninstall +- Theme switch on a live/client site +- Filesystem replacement during update/install +- Remote package download and unzip +- Any flow that deletes an existing plugin/theme folder + +Do not automate these on client sites without explicit confirmation. + +Theme activation rewrite policy: + +- Require an explicit confirmation modal every time before calling the theme activation AJAX action. +- Confirmation must name the theme and warn that the live site's active theme will change and layout, menus, widgets, headers/footers, and theme settings may be affected. +- Do not optimistically mark a theme active before server success and canonical active-theme recheck. +- Keep reload fallback for theme activation because `switch_theme()` changes global site state. + +Uninstall rewrite policy: + +- Keep uninstall for backward compatibility, but never as a primary one-click action. +- Put uninstall behind a danger/overflow menu and an explicit confirmation modal. +- Confirmation must name the extension and warn that files will be deleted and dependent site features may break. +- Treat uninstall as reload-fallback-first: after server acceptance, re-check canonical plugin state before updating UI. + +## Backend Improvement Targets + +Before a deeper rewrite, plan a service layer behind existing AJAX actions: + +- Product catalog resolver +- Account/session resolver +- Purchased product mapper +- Install/update/download service +- Action response formatter +- State summary endpoint for client-side refresh + +Maintain the current AJAX action names and response compatibility while introducing safer internals. + +## Page Speed Acceptance + +Future performance work should measure: + +- Full page reload count per journey +- Number of AJAX calls per action +- Time from click/submit to visible feedback +- Time from server success to stable UI +- Console errors and page errors +- Mobile horizontal overflow +- Layout shift during loading and state changes + +Do not report performance success only from code review. Verify in Agent Browser. diff --git a/docs/agents/directorist-themes-extensions-page/references/qa-checklist.md b/docs/agents/directorist-themes-extensions-page/references/qa-checklist.md new file mode 100644 index 0000000000..65e378928c --- /dev/null +++ b/docs/agents/directorist-themes-extensions-page/references/qa-checklist.md @@ -0,0 +1,115 @@ +# QA Checklist + +Use this checklist for Themes & Extensions page work. Current runtime data must be collected fresh and not stored in this docs agent. + +## Static Checks + +- Confirm `ATBDP_Extensions::setup_ajax_actions()` still registers expected `wp_ajax_atbdp_*` actions. +- Confirm `show_extension_view()` still passes required data to the root template. +- Confirm `subscriptionManagement.js` action names match server handlers. +- Confirm new code preserves existing nonce usage or provides compatibility. +- Confirm new copy is escaped and translatable in PHP. +- Confirm changed selectors/classes do not remove legacy hooks without shims. +- Confirm docs/copy claims against `README.md`, `readme.txt`, and official Directorist docs. +- Confirm product names, descriptions, thumbnails, and links use API values when present and local defaults when API fields are missing or invalid. +- Confirm product badges/status render only from product API/filter-provided fields and do not rely on hardcoded product slugs or product order. +- Confirm no old/new UI feature flag or rollout toggle was added unless explicitly requested. + +## Agent Browser Checks + +Open the local admin page: + +```bash +agent-browser --session directorist-themes-extensions --profile Default --ignore-https-errors open "https://directorist-core.local/wp-admin/edit.php?post_type=at_biz_dir&page=atbdp-extension" +``` + +Inspect without destructive actions: + +```bash +agent-browser --session directorist-themes-extensions snapshot -c -d 4 +agent-browser --session directorist-themes-extensions console --clear +agent-browser --session directorist-themes-extensions errors --clear +``` + +Mobile overflow check: + +```bash +agent-browser --session directorist-themes-extensions set viewport 390 844 +agent-browser --session directorist-themes-extensions eval 'JSON.stringify({scrollWidth:document.documentElement.scrollWidth,innerWidth:innerWidth,overflow:document.documentElement.scrollWidth>innerWidth})' +``` + +Close the session: + +```bash +agent-browser --session directorist-themes-extensions close +``` + +## Dynamic State Matrix + +Verify behavior against current site state for: + +- Not connected account +- Not connected account while premium products may already be installed locally; verify the page still shows the auth/connect state and does not expose installed-product management actions +- Connected account +- Subscribed product not installed +- Installed product inactive +- Installed product active +- Installed product outdated +- Required extension +- Promo-only product +- Product with API/filter-provided badge +- Product without badge data +- Product with expired badge data +- Product with missing API name, description, thumbnail, or link to verify local fallback fields +- Active theme +- Installed inactive theme +- Theme update available +- Remote API failure +- Nonce failure +- Capability failure +- Filesystem failure + +Use categories only in docs. Do not preserve observed counts or product lists. + +## Performance Checks + +- Count full page reloads before and after the change. +- Confirm loading states appear immediately after click/submit. +- Confirm server errors are shown without leaving disabled buttons stuck. +- Confirm no-reload UI updates are reconciled with canonical server state. +- Confirm full reload fallback still works. +- Compare desktop and mobile interaction latency. +- Check console and page errors after each tested journey. + +## Destructive-Action Safeguards + +Do not execute these on a real/client site without explicit confirmation: + +- Install plugin/theme +- Update plugin/theme +- Activate plugin +- Deactivate plugin +- Uninstall plugin +- Activate/switch theme +- Logout connected account +- Bulk action +- Refresh purchase if it requires real credentials + +When these need QA, use a disposable local site, mocks, or a server-side test harness. + +Uninstall-specific QA for rewrite: + +- Confirm uninstall is in a danger/overflow area, not a primary action. +- Confirm confirmation modal names the extension and warns that files will be deleted and site features may break. +- Confirm cancellation makes no AJAX request. +- Confirm success is shown only after server success and canonical plugin-state recheck or reload fallback. +- Confirm failure restores the UI and shows inline feedback. + +Theme-activation-specific QA for rewrite: + +- Confirm clicking a theme Activate button opens a confirmation modal instead of sending AJAX immediately. +- Confirm the modal names the theme and warns that the live site's active theme will change. +- Confirm cancellation makes no AJAX request. +- Confirm confirmation sends the existing `atbdp_activate_theme` action only after explicit user approval. +- Confirm active-theme UI updates only after server success and canonical active-theme recheck or reload fallback. +- Confirm failure restores the UI and shows inline feedback. diff --git a/docs/agents/directorist-themes-extensions-page/references/rewrite-issue-register.md b/docs/agents/directorist-themes-extensions-page/references/rewrite-issue-register.md new file mode 100644 index 0000000000..488d580a64 --- /dev/null +++ b/docs/agents/directorist-themes-extensions-page/references/rewrite-issue-register.md @@ -0,0 +1,212 @@ +# Rewrite Issue Register + +Use this register when planning or implementing a full rewrite of the Directorist admin `Themes & Extensions` page. These are durable issue patterns and fix priorities, not runtime data snapshots. Always re-check the current page, source, and Directorist docs before acting. + +## Rewrite Principle + +The page is an account-connected product management surface. It is not only a marketing catalog. A rewrite must preserve account connection, subscription discovery, install, activate, update, required-extension, settings-link, theme, and promo-product journeys while improving layout, speed, and failure handling. + +Keep existing page slug, AJAX action names, nonce contracts, filters, aliases, user meta keys, selectors needed for compatibility, and fallback server-rendered behavior unless an explicit migration plan is approved. + +The approved direction is a full UI rewrite of this page, not a staged old/new UI rollout. Do not plan a separate feature flag or rollout toggle by default unless a future task explicitly asks for one. Full UI rewrite still means preserving backend contracts, server-rendered compatibility behavior, reload fallbacks, and high-risk action safeguards. + +## Preferred Rewrite Architecture + +- Do not default to a Vue or React rewrite for this page. +- Keep PHP-rendered templates as the compatibility baseline and canonical fallback. +- Reduce complexity by adding focused PHP service/resolver classes behind the existing `wp_ajax_atbdp_*` actions. +- Add a small page-specific JavaScript state adapter for no-reload behavior instead of a full SPA. +- The adapter should own request lifecycle, button loading states, inline notices, row/card replacement, counters, and canonical state refresh. +- Existing actions should gradually return structured responses with `success`, `message`, `item_key`, `next_state`, `requires_reload`, and optional rendered HTML/state-summary data while preserving backward compatibility. +- Vue 2 should not be expanded just because the settings panel uses Vue; it is legacy surface area. +- React should only be considered after an explicit larger admin UI migration decision, because it requires a new state/API layer and broader QA. +- This architecture is the lowest-risk default for the 20k customer base: fewer moving parts, easier rollback, existing server-rendered fallback, and stable public contracts. + +## Disconnected Account Policy + +- Preserve current disconnected-account behavior by default. +- When `_atbdp_has_subscriptions_sassion` is empty, the page should render the account-connect form and the marketplace/promo discovery section. +- Do not expose installed premium product management in the disconnected state by default. +- Do not show Settings, Active status, Activate, Deactivate, Update, or local management actions for installed premium plugins/themes while disconnected unless a future task explicitly changes this policy. +- Existing installed/active premium extensions can continue working elsewhere in WordPress, but the Themes & Extensions page should not manage them while disconnected. +- License-backed actions must remain gated behind connected account/subscription data: new premium install, premium package download, premium update, refresh purchase, and any server-side license validation flow. +- Improve disconnected-state messaging without changing behavior. Recommended helper copy: `Already installed extensions will keep working. Connect your Directorist account to manage subscriptions, installs, and updates.` +- The intent is to keep a clear account-first product-management surface, avoid license/update confusion, and reduce support risk for existing customer sites. +- Connected-state page model can still group products as: + - `Installed`: local WordPress plugin/theme state rendered after account connection. + - `Available in your account`: subscription/license data rendered after account connection. + - `Marketplace`: external product discovery, visible regardless of connection state. + +## Remote Product Badge Policy + +- New, beta, popular, sale, or similar product badges/status should be driven by the product API data contract, not hardcoded in plugin templates. +- The current product catalog contract may not include badge data. Re-check `Directorist\Core\API::get_products()` and the remote `v1/get-remote-products` response before implementation. +- Preferred remote API shape is a structured optional field: + +```json +"badge": { + "type": "new", + "label": "New", + "expires_at": "2026-09-01" +} +``` + +- `type` should be a machine-readable enum such as `new`, `beta`, `popular`, `sale`, or `featured`. +- `label` should be the display text from the API and escaped/translated safely where applicable. +- `expires_at` should be optional and handled server-side or client-side so expired badges do not render. +- The core plugin UI should read badge/status values from the product API. EDD product meta, custom taxonomy, or a dedicated badge setting can be the upstream source on Directorist.com, but the Themes & Extensions page should depend on the API field, not direct EDD assumptions. +- Do not depend on normal WordPress/EDD `post_status`; `publish` and `draft` indicate product availability, not UI badge state. +- Badge rendering must be optional and backward compatible. If no `badge` field exists, render no badge. +- Local filters `atbdp_extension_list` and `atbdp_theme_list` should still be able to add or override badge data for compatibility and testing. +- Do not infer `New` from product order, product name, local install date, or a hardcoded slug list. +- Because the product catalog is cached, badge changes from the API must account for cache invalidation or acceptable cache delay. + +## Product Copy Source Policy + +- Product API data is preferred for product names, descriptions, thumbnails, product links, demo links, active promo flags, item IDs, and plugin bases when valid fields exist. +- Local default product arrays must remain a safe fallback for names, descriptions, thumbnails, links, demo links, item IDs, and plugin bases. +- Merge API product data over local defaults by product key where possible; do not allow missing remote fields to create blank cards or broken product links. +- If the API is unavailable, empty, malformed, or missing a non-badge field, use the local fallback field. +- Badge/status behavior remains stricter: badge/status must come from product API data or explicit filters only. Do not create hardcoded badge/status fallback from local product names, slugs, order, or descriptions. +- Cross-check user-facing product claims, labels, and descriptions against local `README.md`/`readme.txt` and official Directorist docs before changing copy. + +## High Priority Issues To Fix + +### Responsive Layout + +- Current extension management UI is table-first and uses fixed/minimum widths in the extension name and action areas. +- This can create page-level horizontal overflow on mobile and narrow admin layouts. +- Full rewrite should use a responsive management layout: table on wide screens only if needed, card/list rows on narrow screens, stable action menus, and no page-level overflow. +- Always re-check with Agent Browser desktop and mobile viewports. + +Primary areas: + +- `views/admin-templates/theme-extensions/my-themes-extensions/extensions-tab.php` +- `views/admin-templates/theme-extensions/my-themes-extensions/themes-tab.php` +- `assets/src/scss/layout/admin/admin-style.scss` + +### Reload-Heavy UX + +- `assets/src/js/admin/components/subscriptionManagement.js` relies heavily on `location.reload()` after successful account connect, install, activate, update, refresh purchase, logout, uninstall, and bulk actions. +- Do not remove reloads blindly. They currently force canonical server-rendered state. +- Add progressive no-reload behavior behind existing AJAX actions. Use a small page-state adapter, loading states, normalized error handling, and canonical state refresh before claiming completion. +- Keep full reload fallback for high-risk actions and unreconciled states. + +### Dead Login Continuation Flow + +- The account-connect success path reloads immediately, leaving older checklist/download continuation code unreachable. +- During rewrite, either remove the dead path or intentionally rebuild it as a supported flow. +- Do not revive old checklist behavior without checking current account, subscription, and install contracts. + +### Registered Download Handler Type Bug + +- `ATBDP_Extensions::handle_file_download_request()` currently has invalid product type validation: valid `plugin` and `theme` values cannot pass the condition. +- Do not reuse this handler for new flows until the condition and response contract are fixed. +- Prefer `atbdp_install_file_from_subscriptions` for current subscription installs unless this legacy handler is intentionally repaired and tested. + +### Plugin Activation Error Handling + +- Single plugin activation calls WordPress `activate_plugin()` without checking for `WP_Error`. +- A rewrite must surface activation errors, avoid false-success UI, and keep the button recoverable on failure. +- Bulk activation already checks `WP_Error`; align single activation response behavior with that pattern. + +### Filesystem Install And Update Safety + +- Plugin/theme download, install, and update flows write to `wp-content/plugins` and `wp-content/themes`. +- Existing code can delete an existing destination directory before fully validating package extraction and copy success. +- Full rewrite should introduce a safer install/update service behind existing AJAX actions: + - validate download host and URL before download; + - validate `download_url()`, `unzip_file()`, and `copy_dir()` results; + - verify expected package structure before replacing an existing directory; + - clean temp directories reliably; + - restore temporary error handlers; + - return structured failures with recovery instructions; + - never mark UI complete until server confirms final state. + +### Plugin Uninstall Safety + +- Keep uninstall available for compatibility with the existing page, but make it a protected danger action. +- Do not remove the behavior unless a future migration explicitly chooses to delegate uninstall fully to the WordPress Plugins screen. +- Do not expose uninstall as a primary one-click action. +- Place uninstall under a danger/overflow menu. +- Require a confirmation modal that names the extension and explains that plugin files will be deleted and dependent site features may break. +- The server should confirm capability, nonce, plugin target validity, and `delete_plugins()` result before returning success. +- The UI must not mark uninstall complete until canonical WordPress plugin state has been re-checked, or a reload fallback has completed. +- Failed uninstall must restore the action UI and show inline failure feedback. + +### Theme Update State Defensive Checks + +- Theme update code assumes `get_site_transient( 'update_themes' )` is an object with a `response` property. +- Rewrite code must handle missing, false, malformed, or empty update transient state without PHP warnings. +- Theme activation calls `switch_theme()` and must remain a high-risk action. +- Locked recommendation: every theme activation/switch requires an explicit confirmation modal before the AJAX request is sent. +- The confirmation modal must name the theme and warn that activating it changes the live site's active theme and may affect layout, menus, widgets, headers/footers, and theme settings. +- Theme activation must not be triggered by a single accidental click. Do not run it from Agent Browser or automated QA on a real/client site without explicit confirmation. +- Do not show active-theme success UI until server success and canonical active-theme state has been re-checked, or reload fallback has completed. + +### Stale License/Purchase Helpers + +- `handle_license_activation_request()` exists but is not registered by `setup_ajax_actions()`. +- `get_customers_purchased()` and older purchased/download helpers contain stale variable usage and duplicated fields. +- Do not base new logic on these helpers without a focused audit. Prefer a new service layer behind existing public contracts. + +### JavaScript State And Error Handling + +- Current JS uses repeated state flags, undeclared assignments in some bulk paths, inconsistent error resets, browser `alert()` for several failures, and success paths that reload without inspecting response status. +- Rewrite should centralize request state, disable and restore controls consistently, show inline feedback, and keep action buttons usable after errors. +- Bulk flows should aggregate per-item results and continue safely where appropriate. + +### Accessibility, Semantics, And I18n + +- Current templates include hardcoded English strings, duplicate IDs, empty or `#` action links, external `target="_blank"` links without `rel`, placeholder-only fields, and action controls implemented as links. +- Rewrite should use buttons for local actions, unique IDs, visible labels or accessible labels, `rel="noopener noreferrer"` for external blank-target links, translatable strings, and clear keyboard/focus behavior. + +### Theme Changelog Modal + +- The theme "What's new" modal contains dummy hardcoded version and changelog content. +- Do not ship this content in a redesign. Either connect it to real update/changelog data or remove/hide the feature. + +### Remote API And Credential Handling + +- Browser JS should continue talking to local WordPress AJAX only. PHP owns Directorist.com and EDD remote calls. +- Existing remote flows mix Directorist REST endpoints and EDD action endpoints; keep response normalization server-side. +- Review `sslverify => false` usages carefully. Harden only with a compatibility plan and useful error handling for customer sites. +- Never log or store account passwords beyond the current request. Keep account connection and refresh purchase flows careful around credentials. + +## Rewrite Fix Order + +1. Preserve contracts first: page slug, capability, AJAX action names, nonces, filters, aliases, meta keys, and existing settings/product links. +2. Preserve current disconnected behavior: account-connect form plus marketplace only, with no installed premium product management while disconnected. +3. Implement product copy merging with API data as primary and local product arrays as fallback for non-badge fields. +4. Add optional product badge/status support through product API contracts, with no hardcoded badge fallback. +5. Add focused PHP service/resolver classes behind existing AJAX actions instead of introducing Vue/React as the first rewrite step. +6. Add server-side state summary or render-partial capability before removing reloads. +7. Build a response formatter for all page AJAX handlers: success, error, message, action, item key, canonical state needed, reload fallback. +8. Add a small page-specific JavaScript state adapter around existing AJAX calls. +9. Replace fragile table layout with responsive rows/cards while preserving old selectors or compatibility wrappers. +10. Fix low-risk no-reload flows first: account-connect feedback, refresh purchase errors, tabs, filtering/search, inline errors, and button states. +11. Harden single plugin activation and bulk result handling. +12. Harden install/update/download services before changing their UI behavior. +13. Keep uninstall for compatibility, but move it behind a protected danger action with confirmation and canonical plugin-state revalidation. +14. Require confirmation modal for every theme switch, then revalidate canonical active-theme state or use reload fallback. +15. Treat plugin/theme update and filesystem replacement as conservative flows with server revalidation and reload fallback. +16. Remove or rebuild stale/dead code only after verifying no custom integration depends on old selectors or action names. + +## Regression Checklist For Full Rewrite + +- Disconnected account state renders and validates without page errors. +- Disconnected account state keeps the current behavior: account-connect form plus marketplace, without installed premium product management actions. +- Connected account state renders statistics, extensions, themes, required items, and promo items from dynamic server state. +- Premium install/update/download actions remain gated behind connected account/subscription/license state. +- Product names, descriptions, thumbnails, and links use API data when present and local fallback when API fields are missing. +- Product badges render only from API/filter-provided badge data and disappear safely when absent or expired. +- Existing installed active, installed inactive, subscribed-not-installed, outdated, required, promo-only, active theme, inactive theme, and update-available states are all represented. +- No page-level horizontal overflow on mobile admin widths. +- No action leaves buttons permanently disabled after failure. +- No local action uses `alert()` as the only feedback. +- Uninstall is not exposed as a one-click primary action; it requires explicit confirmation and recovers cleanly on failure. +- Theme activation requires explicit confirmation every time and does not show success before active-theme state is confirmed. +- No success UI is shown before server acceptance. +- High-risk actions re-check canonical WordPress state before final UI update. +- Console and page errors stay clean after load, tab changes, and non-destructive interactions. +- Official Directorist docs and local `README.md`/`readme.txt` are checked before changing public claims, product labels, or descriptions. From 1d34357094d7fb1b75d98d5fd3547a7a939c4a52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rabbi=20Islam=20Rony=20=E2=9A=A1=EF=B8=8F?= <35329385+RabbiIslamRony@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:26:05 +0600 Subject: [PATCH 2/5] docs: add themes extensions licensing prd --- .../README.md | 1 + .../SKILL.md | 1 + .../references/api-data-flow-report.md | 140 +++++ .../references/licensing-system-prd.md | 526 ++++++++++++++++++ .../references/rewrite-issue-register.md | 1 + 5 files changed, 669 insertions(+) create mode 100644 docs/agents/directorist-themes-extensions-page/references/licensing-system-prd.md diff --git a/docs/agents/directorist-themes-extensions-page/README.md b/docs/agents/directorist-themes-extensions-page/README.md index e312c2150d..ee1f71f2d2 100644 --- a/docs/agents/directorist-themes-extensions-page/README.md +++ b/docs/agents/directorist-themes-extensions-page/README.md @@ -36,6 +36,7 @@ This skill must not store site-specific runtime data. Product counts, installed - `SKILL.md`: required agent workflow and safety rules - `references/page-architecture-map.md`: code, templates, assets, hooks, endpoints, and remote dependencies - `references/api-data-flow-report.md`: step-by-step source/API flow from page load to rendered sections and product actions +- `references/licensing-system-prd.md`: to-the-point product/system requirements for account, license, extension, and theme setup - `references/dynamic-data-contract.md`: dynamic data shapes and collection methods - `references/account-license-journey-map.md`: account, license, subscription, and product-action journeys - `references/performance-improvement-guide.md`: reload-heavy behavior and safe no-reload strategy diff --git a/docs/agents/directorist-themes-extensions-page/SKILL.md b/docs/agents/directorist-themes-extensions-page/SKILL.md index 041e881dd2..b5243c6069 100644 --- a/docs/agents/directorist-themes-extensions-page/SKILL.md +++ b/docs/agents/directorist-themes-extensions-page/SKILL.md @@ -45,6 +45,7 @@ Load reference files as needed: - Architecture and file map: `references/page-architecture-map.md` - API/source flow report: `references/api-data-flow-report.md` +- Licensing system PRD: `references/licensing-system-prd.md` - Runtime state and data shapes: `references/dynamic-data-contract.md` - Account/license journeys: `references/account-license-journey-map.md` - Performance and no-reload guidance: `references/performance-improvement-guide.md` diff --git a/docs/agents/directorist-themes-extensions-page/references/api-data-flow-report.md b/docs/agents/directorist-themes-extensions-page/references/api-data-flow-report.md index 1f5720cbcb..b37600e6b1 100644 --- a/docs/agents/directorist-themes-extensions-page/references/api-data-flow-report.md +++ b/docs/agents/directorist-themes-extensions-page/references/api-data-flow-report.md @@ -381,6 +381,146 @@ Themes tab uses: - Remote calls mix REST-style Directorist endpoints and EDD action endpoints. - Some remote calls set `sslverify` to `false`; future hardening must be planned carefully for existing customers. +## Future API Improvement Feedback + +Use these notes when changing the Themes & Extensions API layer. They are design constraints and improvement targets, not current runtime data. + +### Browser Boundary + +- Keep browser JavaScript calling local WordPress AJAX only. +- Do not call Directorist.com, EDD, or package download URLs directly from the browser. +- Let PHP own remote authentication, license activation, product catalog reads, package URL resolution, filesystem work, and canonical WordPress state checks. +- Preserve existing `wp_ajax_atbdp_*` action names and request fields; add new response fields in a backward-compatible way. + +### Response Contract + +Create one normalized response formatter for all page actions while preserving legacy fields that current JavaScript or third-party code may expect. + +Recommended response fields: + +```json +{ + "success": true, + "code": "plugin_activated", + "message": "Plugin activated.", + "action": "activate_plugin", + "item_key": "directorist-extension-slug", + "type": "plugin", + "next_state": "active", + "requires_reload": false, + "state": {}, + "html": {} +} +``` + +Rules: + +- `success` must be the canonical boolean for new code. +- `code` must be machine-readable and stable for UI handling, logs, and tests. +- `message` must be safe for display and translatable when generated locally. +- `requires_reload` must be explicit, especially for filesystem, update, theme switch, and unreconciled remote failures. +- `state` may include a fresh canonical state summary, but docs must never store example site-specific values from it. +- `html` may include rendered row/card partials when that is safer than duplicating template logic in JavaScript. + +### State Summary Endpoint + +Add a read-only state endpoint before removing reloads from mutation flows. + +Recommended local action: + +- `atbdp_get_themes_extensions_state` + +Recommended state groups: + +- `account`: connected/disconnected capability and refresh/logout availability. +- `statistics`: counts and update flags generated at request time only. +- `extensions`: installed, active, inactive, outdated, subscribed, required, promo, and action availability. +- `themes`: active, installed, inactive, subscribed, update availability, and action availability. +- `notices`: non-sensitive connection, license, update, and remote API messages. +- `html`: optional server-rendered partials for rows/cards/counters. + +Rules: + +- Keep it capability and nonce protected. +- Return no passwords, raw licenses, subscription secrets, or user-identifying account data. +- Generate state from canonical server reads each time; do not let the browser invent final install/update/activation state. +- Use this endpoint after successful mutations and after recoverable API failures that may leave the UI stale. + +### Error Codes And Failure Shape + +Map mixed Directorist REST, EDD, WordPress, and filesystem failures into stable local error codes. + +Recommended code categories: + +- `capability_denied` +- `nonce_invalid` +- `account_disconnected` +- `auth_failed` +- `subscription_missing` +- `license_missing` +- `license_activation_failed` +- `remote_unreachable` +- `remote_invalid_response` +- `download_unavailable` +- `download_host_invalid` +- `filesystem_unavailable` +- `package_invalid` +- `install_failed` +- `update_failed` +- `activation_failed` +- `theme_switch_failed` +- `requires_reload` + +Rules: + +- Never show false success when WordPress returns `WP_Error`. +- Include developer-safe diagnostic context only when it does not expose passwords, raw licenses, or private account data. +- Prefer inline recoverable errors in the UI over browser `alert()` calls. + +### Product Catalog API + +Future product catalog API changes should be versioned and optional-field friendly. + +Recommended additions: + +- `schema_version` +- `catalog_version` or another cache-busting marker. +- `badge` object for `new`, `beta`, `popular`, `sale`, or similar labels. +- `status` or `availability` only when it has a clear UI meaning separate from EDD/WordPress `post_status`. +- stable product key/slug, item ID, product type, link, thumbnail, description, plugin base, demo link, and active promo flag. + +Rules: + +- Merge API product fields over local defaults by product key. +- Keep local fallback for non-badge fields when API data is unavailable or incomplete. +- Render badge/status only from API data or explicit filters. +- Do not infer badges from product name, slug, order, install date, or local hardcoded lists. +- Account for product catalog cache delay when adding time-sensitive badge behavior. + +### Cache And Freshness + +- Keep product catalog caching separate from account/subscription state. +- Do not cache user subscription/license state as if it were public catalog data. +- Prefer shorter or version-busted cache behavior for badge/status metadata than for long-lived product copy when product marketing needs faster updates. +- Add explicit cache invalidation or refresh behavior for support/debug flows if remote product data appears stale. +- Keep EDD version-check caching scoped enough to avoid cross-product contamination. + +### Credential And Transport Safety + +- Do not store account passwords after a request finishes. +- Do not log passwords, raw licenses, or raw subscription payloads. +- Prefer remote account authentication over `POST` for future API work; keep compatibility with current behavior until Directorist.com supports the safer contract. +- Review every `sslverify => false` path. Harden with compatibility filters and actionable error messages rather than silently breaking older customer sites. +- Use reasonable remote timeouts and map timeout failures to recoverable UI messages. + +### Mutation Safety + +- Add per-item action locking or request IDs for install, update, activate, uninstall, refresh, and theme switch to reduce duplicate-click and concurrent-request risk. +- Do not mark UI state final until the server accepts the action and a canonical state recheck confirms the result. +- For install/update, validate download URL, host, package structure, unzip result, copy result, and final plugin/theme presence before returning success. +- For theme switch, require explicit UI confirmation before AJAX and re-check `get_option( 'stylesheet' )` after success. +- For uninstall, require explicit UI confirmation and re-check plugin filesystem/active state after success. + ## Redesign Guidance For no-reload or page-speed improvements: diff --git a/docs/agents/directorist-themes-extensions-page/references/licensing-system-prd.md b/docs/agents/directorist-themes-extensions-page/references/licensing-system-prd.md new file mode 100644 index 0000000000..3ebac79e47 --- /dev/null +++ b/docs/agents/directorist-themes-extensions-page/references/licensing-system-prd.md @@ -0,0 +1,526 @@ +# Licensing System PRD + +This PRD defines how the Directorist admin `Themes & Extensions` page should handle account connection, licenses, subscriptions, extension setup, and theme setup during the full UI rewrite. + +It is intentionally product/system focused. It must not store runtime account data, current product counts, subscribed product lists, installed plugin lists, usernames, screenshots, or one-time API payloads. + +## Objective + +Build a safer, faster, account-connected product management system for Directorist themes and extensions while preserving existing customer compatibility. + +The page should let a site admin: + +- Connect a Directorist.com account. +- See which extensions and themes are installed, active, available in the account, required, outdated, or marketplace-only. +- Install entitled premium extensions/themes. +- Activate installed extensions. +- Update entitled premium extensions/themes. +- Switch Directorist themes only after explicit confirmation. +- Refresh purchase/subscription state. +- Disconnect the Directorist account without breaking already installed products. + +## Non-Goals + +- Do not redesign the frontend `[directorist_user_dashboard]`. +- Do not replace the whole page with Vue/React unless a future explicit architecture decision approves it. +- Do not create a new payment/subscription platform inside the plugin. +- Do not expose raw license keys, passwords, or private subscription payloads in browser state. +- Do not add an old/new UI rollout toggle by default. +- Do not store runtime product/account values in docs. + +## Core Product Rule + +The Directorist account/license connection controls product management on this page. It should not control whether already installed premium extensions keep running elsewhere in WordPress. + +Disconnected behavior stays as currently approved: + +- Show the Directorist account connection form. +- Show marketplace/promo product discovery. +- Do not show local premium product management actions while disconnected. +- Already installed premium products can keep working in WordPress. +- Show helper copy: `Already installed extensions will keep working. Connect your Directorist account to manage subscriptions, installs, and updates.` + +## System Actors + +- WordPress admin user: must have `manage_options`. +- Local WordPress site: source of installed plugin/theme state. +- Directorist plugin PHP: owns local AJAX, remote calls, entitlement normalization, install/update logic, and canonical state. +- Browser JavaScript: calls local WordPress AJAX only and updates UI from server-confirmed state. +- Directorist.com account/licensing API: verifies account credentials and returns subscription/license data. +- Directorist.com EDD license API: activates/checks licenses for package access and updates. +- app.directorist.com product API: optional product catalog/copy/badge source. + +## Main Data Sources + +### Product Catalog + +Purpose: define the official extension/theme catalog and marketplace cards. + +Sources: + +- Local fallback arrays from `ATBDP_Extensions::get_default_extensions()`. +- Local fallback arrays from `ATBDP_Extensions::get_default_themes()`. +- Optional remote catalog through `Directorist\Core\API::get_products()`. +- Filters: `atbdp_extension_list`, `atbdp_theme_list`. + +Rules: + +- API product fields are preferred when valid. +- Local product arrays remain fallback for non-badge fields. +- Merge API over local defaults by product key. +- Badge/status fields must come from product API data or explicit filters only. +- Do not infer badge/status from product name, slug, order, local install date, or hardcoded lists. + +### Account And Subscription State + +Purpose: determine whether the admin account is connected and which products are entitled. + +Stored local user meta: + +- `_atbdp_subscribed_username` +- `_atbdp_has_subscriptions_sassion` +- `_themes_available_in_subscriptions` +- `_plugins_available_in_subscriptions` + +Rules: + +- Preserve the `sassion` misspelling because it is a legacy contract. +- Treat this user meta as a local cache of account state, not public catalog truth. +- Refresh purchase should revalidate against Directorist.com before changing entitlement state. +- Do not expose raw license keys or private subscription payloads to browser JavaScript. + +### Local WordPress Product State + +Purpose: determine what is installed, active, outdated, required, or currently active theme. + +Sources: + +- `get_plugins()` +- `is_plugin_active()` +- `wp_get_theme()` +- `wp_get_themes()` +- `get_option( 'stylesheet' )` +- `get_site_transient( 'update_plugins' )` +- `get_site_transient( 'update_themes' )` +- `directorist_required_extensions` +- `directorist_extensions_aliases` + +Rules: + +- Local WordPress state is canonical for installed/active/final action result. +- Browser UI must not invent final install/update/activation state. +- After high-risk actions, re-check canonical server state or use reload fallback. + +## Required Backend Components + +### ProductCatalogResolver + +Builds the extension/theme catalog. + +Responsibilities: + +- Load local fallback catalog. +- Optionally merge remote API catalog. +- Apply existing filters. +- Normalize product keys, item IDs, links, thumbnails, descriptions, plugin base, demo links, active flags, and optional badge data. +- Avoid blank cards when remote fields are missing. + +### AccountConnectionService + +Handles connected/disconnected account state. + +Responsibilities: + +- Authenticate Directorist.com credentials through the existing local AJAX action. +- Store connected account state in existing user meta. +- Refresh purchase/subscription state. +- Logout/disconnect by clearing the same legacy meta keys. +- Never persist account password after the request. + +### EntitlementResolver + +Converts subscription payloads into product action permissions. + +Responsibilities: + +- Read `_plugins_available_in_subscriptions` and `_themes_available_in_subscriptions`. +- Match entitlement records to product catalog keys. +- Determine whether install/update/download/license actions are available. +- Hide premium management actions when the account is disconnected. +- Return safe action availability data, not raw secrets. + +### ProductStateResolver + +Builds canonical current state for the page. + +Responsibilities: + +- Determine installed, active, inactive, outdated, required, subscribed-not-installed, promo-only, active theme, inactive theme, and update-available states. +- Resolve alias/deprecated extension keys. +- Create state groups for extensions, themes, required items, and statistics. +- Produce server-rendered partials when needed for no-reload UI updates. + +### LicenseService + +Owns EDD license activation/version/package eligibility. + +Responsibilities: + +- Activate a product license before install/download when required. +- Check EDD version data for update eligibility. +- Normalize EDD errors into stable local error codes. +- Treat missing, invalid, expired, or remote-failed licenses as recoverable UI states. +- Never return raw license data to the browser unless already exposed by legacy behavior and required for compatibility. + +### PackageInstallService + +Owns plugin/theme download, validation, install, update, and cleanup. + +Responsibilities: + +- Request package URLs from Directorist.com only from PHP. +- Validate package host and URL. +- Validate `download_url()`, `unzip_file()`, extracted package structure, and copy result. +- Avoid deleting/replacing existing folders before validating the new package. +- Clean temporary files/directories. +- Return structured errors and keep reload fallback for unreconciled filesystem state. + +### PageStateEndpoint + +New recommended read-only AJAX action: + +- `atbdp_get_themes_extensions_state` + +Responsibilities: + +- Return canonical account, extension, theme, required-product, statistics, notice, and optional HTML partial state. +- Be capability and nonce protected. +- Return no passwords, raw licenses, usernames, or private account payloads. +- Support no-reload UI refresh after successful actions. + +### ResponseFormatter + +Normalizes all page action responses. + +Recommended shape: + +```json +{ + "success": true, + "code": "plugin_activated", + "message": "Plugin activated.", + "action": "activate_plugin", + "item_key": "product-key", + "type": "plugin", + "next_state": "active", + "requires_reload": false, + "state": {}, + "html": {} +} +``` + +Rules: + +- Preserve legacy response fields where existing JS or third-party code may depend on them. +- New JS should use `success`, `code`, `message`, `requires_reload`, and canonical `state`. +- Errors should be inline and recoverable where possible. + +## Core User Journeys + +### 1. Page Load + +1. Admin opens `edit.php?post_type=at_biz_dir&page=atbdp-extension`. +2. PHP validates capability and prepares aliases, required extensions, product catalog, account state, and local WP state. +3. If account is disconnected, render connect form plus marketplace. +4. If account is connected, render statistics, installed/subscribed extension management, theme management, required products, and marketplace. +5. Browser JS enhances interactions without replacing server-rendered fallback. + +### 2. Connect Account + +1. Admin submits Directorist.com username/password. +2. Browser calls local `atbdp_authenticate_the_customer`. +3. PHP calls Directorist.com account/licensing API. +4. PHP stores connected account and subscription state in legacy user meta. +5. UI refreshes via state endpoint or reload fallback. +6. Password is discarded after the request. + +Success state: + +- Connected account UI becomes available. +- Subscribed products can be shown from canonical server state. + +Failure state: + +- Show inline authentication/remote/API error. +- Keep connect form usable. +- Do not store partial/unsafe credential state. + +### 3. Refresh Purchase + +1. Admin confirms password for refresh. +2. Browser calls local `atbdp_refresh_purchase_status`. +3. PHP re-authenticates against Directorist.com and rewrites subscription meta. +4. UI refreshes canonical state. + +Failure state: + +- Existing visible state remains stable. +- Show inline error. +- If session is invalid, return `requires_reload` or disconnected state. + +### 4. Disconnect Account + +1. Admin clicks logout/disconnect. +2. Browser calls local `atbdp_close_subscriptions_sassion`. +3. PHP clears connected session meta. +4. UI returns to disconnected state. + +Rules: + +- Installed premium products keep running elsewhere in WordPress. +- This page no longer shows premium management actions while disconnected. + +### 5. Install Extension From Subscription + +1. Product must be in connected account entitlement state. +2. Browser calls local `atbdp_install_file_from_subscriptions`. +3. PHP validates capability, nonce, entitlement, license, download URL, package, and filesystem operation. +4. PHP installs the extension package. +5. UI re-checks canonical plugin state. + +Rules: + +- Do not mark installed until server confirms installed state. +- Keep full reload fallback for filesystem ambiguity. +- Show recoverable errors for invalid license, missing entitlement, failed download, invalid package, and filesystem failure. + +### 6. Activate Extension + +1. Product must be installed and inactive. +2. Browser calls local `atbdp_activate_plugin`. +3. PHP calls WordPress `activate_plugin()`. +4. PHP must check `WP_Error`. +5. UI re-checks canonical active plugin state. + +Rules: + +- Do not show false success on `WP_Error`. +- If activation fails, restore button and show inline error. + +### 7. Update Extension + +1. Product must be installed, entitled, and update-available. +2. PHP checks EDD version data and download eligibility. +3. PHP validates package and safely replaces plugin files. +4. UI re-checks installed version/update state. + +Rules: + +- Treat update as high-risk filesystem mutation. +- Prefer progress UI plus reload fallback over optimistic final state. + +### 8. Required Extension + +1. Required items come from `directorist_required_extensions`. +2. PHP matches required product to catalog, aliases, subscription state, install state, and active state. +3. UI shows the next safe action: install, activate, or get-now/external handoff. + +Rules: + +- Required status is not a remote endpoint by itself. +- Required-product install/update still follows the same entitlement and license rules. + +### 9. Install Theme From Subscription + +1. Product must be in connected account entitlement state. +2. Browser calls existing install action with `type=theme`. +3. PHP validates entitlement, license, package URL, package structure, and theme install result. +4. UI re-checks `wp_get_themes()` and theme update state. + +Rules: + +- Installing a theme is filesystem mutation but does not switch the live theme by itself. +- Keep reload fallback if theme state cannot be reconciled. + +### 10. Activate Theme + +1. Theme must be installed and inactive. +2. UI shows explicit confirmation modal every time. +3. After confirmation, browser calls local `atbdp_activate_theme`. +4. PHP calls `switch_theme()`. +5. UI re-checks `get_option( 'stylesheet' )`. + +Confirmation copy must communicate: + +- This changes the live site's active theme. +- Layout, menus, widgets, headers/footers, and theme settings may be affected. + +Rules: + +- Never trigger theme switch from a single accidental click. +- Do not automate theme switch on real/client sites without explicit confirmation. +- Do not show success before canonical active-theme state is confirmed. + +### 11. Update Theme + +1. Theme must be installed, entitled, and update-available. +2. PHP checks theme update transient and entitlement. +3. PHP validates package/download and updates theme files. +4. UI re-checks canonical theme version/update state. + +Rules: + +- Handle missing or malformed `update_themes` transient defensively. +- Keep reload fallback for filesystem or state uncertainty. + +### 12. Marketplace / Get Now + +1. Promo-only product cards are visible as product discovery. +2. Links may go to Directorist product, pricing, account, demo, support, or docs pages. +3. External links are handoff points, not local state changes. + +Rules: + +- Use API product copy when available and local fallback when not. +- Product claims should be cross-checked with local readme files and official docs before copy changes. + +## License States + +The UI and API should support these states as categories: + +- `connected`: account session exists locally. +- `disconnected`: no local account session. +- `entitled`: current account has subscription/license access for product. +- `not_entitled`: product is marketplace-only for this account. +- `license_active`: remote license activation succeeded for the requested product. +- `license_missing`: entitlement record lacks a usable license. +- `license_invalid`: EDD activation/check failed. +- `license_expired`: remote license indicates renewal required. +- `license_remote_failed`: Directorist.com/EDD could not be reached or returned invalid response. +- `installed`: product exists locally. +- `active`: plugin active or theme is current stylesheet. +- `inactive`: installed but not active. +- `update_available`: local version is behind canonical update state. +- `requires_reload`: server cannot safely reconcile state in-place. + +Already installed products should not be presented as broken only because the account is disconnected. The page should say connection is required for subscription management, installs, downloads, and updates. + +## API Requirements + +Existing local AJAX actions must remain compatible: + +- `atbdp_authenticate_the_customer` +- `atbdp_install_file_from_subscriptions` +- `atbdp_plugins_bulk_action` +- `atbdp_activate_theme` +- `atbdp_activate_plugin` +- `atbdp_update_plugins` +- `atbdp_update_theme` +- `atbdp_refresh_purchase_status` +- `atbdp_close_subscriptions_sassion` + +Recommended new local AJAX action: + +- `atbdp_get_themes_extensions_state` + +Remote API calls stay server-side: + +- Account/license auth: `https://directorist.com/wp-json/directorist/v1/licencing` +- Product data/download link: `https://directorist.com/wp-json/directorist/v1/get-product-data/` +- EDD license activation/version: `https://directorist.com` +- Optional product catalog: `https://app.directorist.com/wp-json/directorist/v1/get-remote-products` + +## UI Requirements + +- Keep disconnected UI simple: connect form plus marketplace. +- Connected UI should group products by clear state: installed, available in account, required, themes, marketplace. +- Show action buttons only when the server says the action is available. +- Use inline notices instead of browser `alert()` as the primary feedback. +- Use button loading states and disable duplicate clicks while a request is running. +- Require confirmation for uninstall and theme activation. +- Keep external product/docs/account links visually distinct from local AJAX actions. +- Mobile layout must avoid horizontal overflow. + +## Performance Requirements + +- Keep PHP-rendered templates as canonical fallback. +- Add no-reload behavior through a small page-specific JS state adapter. +- Use the state endpoint after safe or successful actions. +- Avoid full reload for simple UI state changes and recoverable errors. +- Keep reload fallback for install, update, theme switch, uninstall, remote API failure, and filesystem uncertainty. + +## Security And Privacy Requirements + +- Require `manage_options` for all local actions. +- Verify nonce for all local actions. +- Sanitize request fields and escape rendered output. +- Never store passwords after request completion. +- Never log passwords, raw licenses, or private subscription payloads. +- Do not expose raw subscription/license data to the browser. +- Review `sslverify => false` paths with compatibility-safe hardening and clear error handling. + +## Compatibility Requirements + +Preserve: + +- Page slug: `atbdp-extension` +- Parent post type: `at_biz_dir` +- Capability: `manage_options` +- Legacy AJAX action names +- Legacy user meta keys +- `directorist_extensions_aliases` +- `directorist_required_extensions` +- `atbdp_extension_list` +- `atbdp_theme_list` +- Existing settings/product/external links +- Existing template/selector compatibility where practical + +## Error Handling Requirements + +Use stable local error codes such as: + +- `capability_denied` +- `nonce_invalid` +- `account_disconnected` +- `auth_failed` +- `subscription_missing` +- `license_missing` +- `license_activation_failed` +- `license_expired` +- `remote_unreachable` +- `remote_invalid_response` +- `download_unavailable` +- `download_host_invalid` +- `filesystem_unavailable` +- `package_invalid` +- `install_failed` +- `update_failed` +- `activation_failed` +- `theme_switch_failed` +- `requires_reload` + +## Acceptance Criteria + +- A disconnected admin sees the account-connect form and marketplace only. +- A connected admin sees product-management sections built from current server state. +- Installed premium products are not implied to stop working when disconnected. +- Premium install/update/download requires connected entitlement/license state. +- Extension activation checks `WP_Error` and never shows false success. +- Theme activation always requires confirmation and re-checks active theme after success. +- Uninstall is available only as a protected danger action with confirmation. +- Product badges render only from API/filter data. +- Product copy uses API data with local fallback for missing non-badge fields. +- No high-risk action shows final success before canonical server state confirms it. +- No page-level horizontal overflow on mobile admin widths. +- All local action failures leave controls recoverable. +- Official Directorist docs and local readme files are checked before changing public product claims. + +## Documentation References + +- Local API map: `api-data-flow-report.md` +- Runtime data contract: `dynamic-data-contract.md` +- Account journey map: `account-license-journey-map.md` +- Rewrite issue register: `rewrite-issue-register.md` +- Official Directorist docs: `https://directorist.com/documentation/directorist/` +- Official themes docs: `https://directorist.com/documentation/themes/` +- Official extensions docs: `https://directorist.com/documentation/extensions/` diff --git a/docs/agents/directorist-themes-extensions-page/references/rewrite-issue-register.md b/docs/agents/directorist-themes-extensions-page/references/rewrite-issue-register.md index 488d580a64..99bcd75a51 100644 --- a/docs/agents/directorist-themes-extensions-page/references/rewrite-issue-register.md +++ b/docs/agents/directorist-themes-extensions-page/references/rewrite-issue-register.md @@ -172,6 +172,7 @@ Primary areas: - Existing remote flows mix Directorist REST endpoints and EDD action endpoints; keep response normalization server-side. - Review `sslverify => false` usages carefully. Harden only with a compatibility plan and useful error handling for customer sites. - Never log or store account passwords beyond the current request. Keep account connection and refresh purchase flows careful around credentials. +- Before changing API behavior, read `api-data-flow-report.md#future-api-improvement-feedback` and apply its guidance for normalized responses, state-summary refresh, stable error codes, product catalog versioning, cache freshness, credential handling, and mutation safety. ## Rewrite Fix Order From cff8b449a4e74c263883747fd5d30acc5dcc6941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rabbi=20Islam=20Rony=20=E2=9A=A1=EF=B8=8F?= <35329385+RabbiIslamRony@users.noreply.github.com> Date: Tue, 21 Jul 2026 17:14:38 +0600 Subject: [PATCH 3/5] improve: redesign themes extensions admin page --- assets/css/directorist-themes-extensions.css | 1505 +++++++++++++++++ assets/js/directorist-themes-extensions.js | 774 +++++++++ .../SKILL.md | 1 + .../references/qa-checklist.md | 14 + .../references/rewrite-issue-register.md | 68 +- includes/asset-loader/init.php | 2 + .../theme-extensions/theme-extension.php | 837 ++++++++- 7 files changed, 3181 insertions(+), 20 deletions(-) create mode 100644 assets/css/directorist-themes-extensions.css create mode 100644 assets/js/directorist-themes-extensions.js diff --git a/assets/css/directorist-themes-extensions.css b/assets/css/directorist-themes-extensions.css new file mode 100644 index 0000000000..3c4835c371 --- /dev/null +++ b/assets/css/directorist-themes-extensions.css @@ -0,0 +1,1505 @@ +.directorist-te-page { + --accent: #3e62f5; + --accent-hover: #2f50e8; + --accent-tint: #eef2ff; + --text: #111827; + --muted: #64748b; + --faint: #94a3b8; + --border: #e5e7eb; + --border-card: #e9ebef; + --border-soft: #eef0f3; + --line: #f1f5f9; + --surface: #ffffff; + --bg: #f3f4f6; + --bg-soft: #f8fafc; + --success: #16a34a; + --warning: #d97706; + --warning-tint: #fff7ed; + --danger: #dc2626; + --danger-tint: #fef2f2; + --r-xs: 6px; + --r-sm: 8px; + --r-md: 10px; + --shadow: 0 1px 2px rgba(15, 23, 42, 0.05); + --shadow-pop: 0 10px 26px rgba(15, 23, 42, 0.1); + color: var(--text); + background: var(--bg); + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; + font-size: 14px; + line-height: 1.5; + letter-spacing: 0; + -webkit-font-smoothing: antialiased; +} + +#directorist.directorist-te-page.wrap { + margin: 0; +} + +.directorist-te-page *, +.directorist-te-page *::before, +.directorist-te-page *::after { + box-sizing: border-box; +} + +.directorist-te-page [hidden], +.directorist-te-row.is-hidden { + display: none !important; +} + +.directorist-te-shell { + max-width: 100%; + margin: 30px 0 56px; + padding: 0 24px 0 4px; + overflow: visible; + background: transparent; +} + +.directorist-te-header { + position: sticky; + top: 32px; + z-index: 40; + display: flex; + align-items: center; + gap: 24px; + height: 62px; + margin: 0; + padding: 0 24px; + background: var(--surface); + border-radius: var(--r-md); + border-bottom: 1px solid var(--border); +} + +.directorist-te-brand, +.directorist-te-nav, +.directorist-te-resource-links, +.directorist-te-top-right, +.directorist-te-top-search, +.directorist-te-header-icon, +.directorist-te-avatar, +.directorist-te-hero, +.directorist-te-search, +.directorist-te-update-banner, +.directorist-te-toolbar-row, +.directorist-te-segmented, +.directorist-te-bulkbar, +.directorist-te-row__title, +.directorist-te-row__meta, +.directorist-te-row__actions, +.directorist-te-upsell, +.directorist-te-footer { + display: flex; + align-items: center; +} + +.directorist-te-brand { + gap: 10px; + min-width: 0; + flex-shrink: 0; +} + +.directorist-te-brand__mark { + width: 30px; + height: 30px; + display: grid; + place-items: center; + flex-shrink: 0; +} + +.directorist-te-brand__mark img { + display: block; + width: 30px; + height: 30px; +} + +.directorist-te-brand__name { + color: var(--text); + font-size: 15px; + font-weight: 700; + white-space: nowrap; +} + +.directorist-te-nav { + gap: 4px; + min-width: 0; + flex: 1; +} + +.directorist-te-nav a { + padding: 8px 10px; + border: 0; + border-radius: var(--r-xs); + background: transparent; + color: var(--muted); + font-size: 13px; + font-weight: 500; + line-height: 1; + text-decoration: none; + white-space: nowrap; +} + +.directorist-te-nav a:hover, +.directorist-te-nav a:focus, +.directorist-te-nav a.active { + color: var(--accent); + background: var(--accent-tint); + box-shadow: none; + outline: none; +} + +.directorist-te-resource-links { + gap: 18px; + margin-left: auto; + flex-shrink: 0; +} + +.directorist-te-resource-links a { + color: var(--muted); + font-size: 13px; + font-weight: 500; + line-height: 1; + text-decoration: none; + white-space: nowrap; +} + +.directorist-te-resource-links a:hover, +.directorist-te-resource-links a:focus { + color: var(--accent); + outline: none; +} + +.directorist-te-top-right { + gap: 10px; + flex-shrink: 0; +} + +.directorist-te-top-search { + width: 220px; + height: 34px; + gap: 7px; + padding: 0 10px; + border: 1px solid var(--border); + border-radius: var(--r-sm); + background: var(--bg-soft); +} + +.directorist-te-top-search i, +.directorist-te-search i { + color: var(--faint); + font-size: 16px; +} + +.directorist-te-top-search input, +.directorist-te-search input { + width: 100%; + min-height: 0; + margin: 0; + padding: 0; + border: 0; + outline: 0; + background: transparent; + color: var(--text); + box-shadow: none; + font-size: 13px; +} + +.directorist-te-top-search input:focus, +.directorist-te-search input:focus { + border: 0; + box-shadow: none; +} + +.directorist-te-header-icon { + position: relative; + width: 34px; + height: 34px; + justify-content: center; + border: 1px solid var(--border); + border-radius: var(--r-sm); + background: var(--surface); + color: var(--muted); +} + +.directorist-te-header-icon span { + position: absolute; + top: 8px; + right: 8px; + width: 7px; + height: 7px; + border: 2px solid var(--surface); + border-radius: 50%; + background: var(--danger); +} + +.directorist-te-avatar { + height: 34px; + gap: 6px; + padding: 0 7px 0 3px; + border: 1px solid var(--border); + border-radius: 999px; + background: var(--surface); + color: var(--muted); +} + +.directorist-te-avatar img { + width: 28px; + height: 28px; + border-radius: 50%; +} + +.directorist-te-hero { + justify-content: space-between; + align-items: flex-end; + gap: 14px; + margin: 26px 0 18px; + padding-top: 0; +} + +.directorist-te-hero h1 { + margin: 0; + padding: 0; + color: var(--text); + font-size: 20px; + font-weight: 700; + line-height: 30px; + letter-spacing: 0; +} + +.directorist-te-hero p { + max-width: 680px; + margin: 3px 0 0; + color: var(--muted); + font-size: 13.5px; + line-height: 20.25px; +} + +.directorist-te-search { + width: 260px; + height: 32px; + gap: 7px; + padding: 7px 12px; + border: 1px solid var(--border); + border-radius: var(--r-sm); + background: var(--bg-soft); +} + +.directorist-te-connect, +.directorist-te-account { + margin: 0 0 16px; + padding: 16px 18px; + border: 1px solid var(--border-card); + border-radius: var(--r-md); + background: var(--surface); + box-shadow: var(--shadow); +} + +.directorist-te-connect { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(340px, 480px); + gap: 22px; + align-items: center; +} + +.directorist-te-connect.account-connect, +#directorist.atbd_wrapper.directorist-te-page .directorist-te-connect.account-connect { + width: auto; + max-width: none; + margin: 0 0 16px; + padding: 16px 18px; + border-radius: var(--r-md); + text-align: left; +} + +.directorist-te-connect h2, +.directorist-te-upsell h2, +.directorist-te-empty h2 { + margin: 0; + color: var(--text); + font-size: 15px; + font-weight: 700; + line-height: 1.35; +} + +.directorist-te-connect p, +#directorist.atbd_wrapper.directorist-te-page .directorist-te-connect p, +.directorist-te-upsell p, +.directorist-te-empty p { + margin: 4px 0 0; + color: var(--muted); + font-size: 12.5px; + line-height: 1.45; + text-align: left; +} + +.directorist-te-eyebrow { + display: inline-block; + margin-bottom: 5px; + color: var(--accent); + font-size: 10.5px; + font-weight: 700; + line-height: 1; + text-transform: uppercase; +} + +.directorist-te-field-row { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.directorist-te-field-row label > span:not(.directorist-te-password-control) { + display: block; + margin-bottom: 5px; + color: var(--muted); + font-size: 12px; + font-weight: 600; +} + +.directorist-te-field-row input, +.directorist-te-refresh-form input { + width: 100%; + min-height: 34px; + margin: 0; + border: 1px solid var(--border); + border-radius: var(--r-xs); + background: var(--surface); + box-shadow: none; +} + +.directorist-te-field-row input[aria-invalid="true"] { + border-color: var(--danger); + box-shadow: 0 0 0 2px rgba(220, 38, 38, 0.08); +} + +.directorist-te-connect-form.is-submitting input { + background: var(--bg-soft); +} + +.directorist-te-connect-form.is-submitting input, +.directorist-te-connect-form.is-submitting button { + cursor: wait; +} + +.directorist-te-password-control { + position: relative; + display: block; +} + +.directorist-te-field-row .directorist-te-password-control input { + padding-right: 42px; +} + +.directorist-te-password-toggle { + position: absolute; + top: 50%; + right: 8px; + display: inline-flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + margin: 0; + padding: 0; + border: 0; + border-radius: var(--r-xs); + background: transparent; + color: var(--muted); + cursor: pointer; + transform: translateY(-50%); +} + +.directorist-te-password-toggle:hover, +.directorist-te-password-toggle:focus { + color: var(--accent); +} + +.directorist-te-password-toggle:focus { + outline: 2px solid rgba(62, 98, 245, 0.24); + outline-offset: 1px; +} + +.directorist-te-password-toggle i { + font-size: 16px; +} + +.directorist-te-feedback { + margin-top: 8px; +} + +.directorist-te-feedback .atbdp-form-alert { + margin: 0; + padding: 8px 10px; + border-radius: var(--r-xs); + font-size: 12.5px; + line-height: 1.4; +} + +.directorist-te-feedback .atbdp-form-alert + .atbdp-form-alert { + margin-top: 6px; +} + +.directorist-te-feedback .atbdp-form-alert-danger, +.directorist-te-feedback .atbdp-form-alert-error { + border: 1px solid rgba(220, 38, 38, 0.16); + background: var(--danger-tint); + color: var(--danger); +} + +.directorist-te-feedback .atbdp-form-alert-success { + border: 1px solid rgba(22, 163, 74, 0.16); + background: #f0fdf4; + color: var(--success); +} + +.directorist-te-btn, +.directorist-te-icon-btn, +.directorist-te-bulk-action, +.directorist-te-bulk-clear, +.directorist-te-tab, +.directorist-te-segmented button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 7px; + margin: 0; + border: 1px solid transparent; + border-radius: var(--r-sm); + cursor: pointer; + font-weight: 500; + line-height: 1; + text-decoration: none; + white-space: nowrap; + transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease, box-shadow 0.12s ease; +} + +.directorist-te-btn { + min-height: 34px; + padding: 8px 14px; + font-size: 13px; +} + +.directorist-te-btn--primary, +.directorist-te-update-all { + border-color: var(--accent); + background: var(--accent); + color: #fff; +} + +.directorist-te-btn--primary:hover, +.directorist-te-btn--primary:focus, +.directorist-te-update-all:hover, +.directorist-te-update-all:focus { + border-color: var(--accent-hover); + background: var(--accent-hover); + color: #fff; + outline: none; +} + +.directorist-te-btn--secondary, +.directorist-te-btn--ghost, +.directorist-te-bulk-action { + border-color: var(--border); + background: var(--surface); + color: var(--text); +} + +.directorist-te-btn--secondary:hover, +.directorist-te-btn--secondary:focus, +.directorist-te-btn--ghost:hover, +.directorist-te-btn--ghost:focus, +.directorist-te-bulk-action:hover, +.directorist-te-bulk-action:focus { + border-color: var(--accent); + background: var(--accent-tint); + color: var(--accent); + outline: none; +} + +.ext-update-btn, +.theme-update-btn, +.directorist-te-update-all { + border-color: var(--warning); + background: var(--warning); + color: #fff; +} + +.ext-update-btn:hover, +.ext-update-btn:focus, +.theme-update-btn:hover, +.theme-update-btn:focus, +.directorist-te-update-all:hover, +.directorist-te-update-all:focus { + border-color: #b45309; + background: #b45309; + color: #fff; +} + +.directorist-te-icon-btn { + width: 28px; + height: 28px; + padding: 0; + border-color: transparent; + border-radius: var(--r-xs); + background: transparent; + color: var(--faint); + font-size: 17px; +} + +.directorist-te-icon-btn:hover, +.directorist-te-icon-btn:focus, +.directorist-te-menu .ext-action-drop.active { + background: var(--line); + color: var(--text); + outline: none; +} + +.directorist-te-icon-btn--danger:hover, +.directorist-te-icon-btn--danger:focus, +.directorist-te-menu-link--danger, +.directorist-te-bulk-action--danger { + color: var(--danger); +} + +.directorist-te-account { + display: grid; + grid-template-columns: repeat(3, minmax(0, 160px)) minmax(320px, 1fr); + gap: 18px; + align-items: center; +} + +.directorist-te-stat span { + display: block; + color: var(--text); + font-size: 22px; + font-weight: 700; + line-height: 1; +} + +.directorist-te-stat p { + margin: 5px 0 0; + color: var(--muted); + font-size: 12px; +} + +.directorist-te-account__actions { + justify-content: flex-end; + gap: 10px; + min-width: 0; +} + +.directorist-te-refresh-panel { + width: 0; + overflow: hidden; +} + +.directorist-te-refresh-form { + width: 330px; + gap: 8px; +} + +.directorist-te-update-banner { + gap: 13px; + margin: 0 0 16px; + padding: 13px 16px; + border: 1px solid #f5d9a8; + border-radius: var(--r-md); + background: linear-gradient(90deg, var(--warning-tint), #fff8ee); +} + +.directorist-te-update-icon { + width: 36px; + height: 36px; + display: grid; + place-items: center; + flex: 0 0 auto; + border-radius: 9px; + background: linear-gradient(135deg, #f59e0b, #f97316); + color: #fff; + font-size: 20px; +} + +.directorist-te-update-banner > div:nth-child(2) { + min-width: 0; + flex: 1; +} + +.directorist-te-update-banner h2 { + margin: 0; + color: var(--text); + font-size: 13.5px; + font-weight: 600; + line-height: 1.35; +} + +.directorist-te-update-banner p { + margin: 2px 0 0; + color: var(--muted); + font-size: 12.5px; + line-height: 1.4; +} + +.directorist-te-toolbar { + margin: 0; + padding: 0; +} + +.directorist-te-toolbar-main { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 18px; + margin-bottom: 16px; + border-bottom: 1px solid var(--border); +} + +.directorist-te-tabs { + flex: 1 1 auto; + gap: 0; + margin-bottom: 0; + overflow-x: auto; + border-bottom: 0; +} + +.directorist-te-tab { + position: relative; + min-height: 42px; + margin-right: 26px; + padding: 11px 2px; + border: 0; + border-radius: 0; + background: transparent; + color: var(--muted); + font-size: 15px; + font-weight: 600; +} + +.directorist-te-tab::after { + position: absolute; + right: 0; + bottom: -1px; + left: 0; + height: 3px; + border-radius: 3px 3px 0 0; + background: transparent; + content: ""; +} + +.directorist-te-tab.is-active { + color: var(--accent); +} + +.directorist-te-tab.is-active::after { + background: var(--accent); +} + +.directorist-te-tab span, +.directorist-te-segmented span { + color: var(--faint); + font-size: 12px; + font-weight: 700; +} + +.directorist-te-tab.is-active span, +.directorist-te-segmented button.is-active span { + color: var(--accent); +} + +.directorist-te-toolbar-row { + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 20px; +} + +.directorist-te-toolbar-row--filters { + align-items: center; + justify-content: flex-start; +} + +.directorist-te-toolbar-catalog { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 12px; + margin-left: auto; + padding-bottom: 8px; +} + +.directorist-te-toolbar-search { + width: 260px; + flex: 0 0 260px; +} + +.directorist-te-segmented { + gap: 2px; + padding: 3px; + border: 1px solid var(--border); + border-radius: 999px; + background: var(--bg-soft); +} + +.directorist-te-segmented button { + min-height: 29px; + padding: 6px 13px; + border: 0; + border-radius: 999px; + background: transparent; + color: var(--muted); + font-size: 12.5px; + font-weight: 500; +} + +.directorist-te-segmented button.is-active { + background: var(--surface); + color: var(--accent); + box-shadow: var(--shadow); +} + +.directorist-te-count { + color: var(--muted); + font-size: 12.5px; +} + +.directorist-te-products { + padding: 0; +} + +.directorist-te-legacy-bulk { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); +} + +.directorist-te-bulkbar { + position: sticky; + top: 94px; + z-index: 35; + gap: 14px; + margin-bottom: 16px; + padding: 11px 14px; + border: 1px solid var(--border-card); + border-radius: var(--r-md); + background: var(--surface); + box-shadow: var(--shadow-pop); +} + +.directorist-te-bulkbar[hidden] { + display: none !important; +} + +.directorist-te-selected-count { + color: var(--text); + font-size: 13px; + font-weight: 600; + white-space: nowrap; +} + +.directorist-te-bulkbar__sep { + width: 1px; + height: 22px; + flex-shrink: 0; + background: var(--border); +} + +.directorist-te-bulk-action { + min-height: 31px; + padding: 6px 10px; + font-size: 12.5px; +} + +.directorist-te-bulk-action--danger:hover, +.directorist-te-bulk-action--danger:focus { + border-color: var(--danger); + background: var(--danger-tint); + color: var(--danger); +} + +.directorist-te-bulk-clear { + min-height: 31px; + margin-left: auto; + padding: 6px 10px; + border: 0; + background: transparent; + color: var(--muted); + font-size: 13px; + font-weight: 500; +} + +.directorist-te-bulk-clear:hover, +.directorist-te-bulk-clear:focus { + background: var(--bg-soft); + color: var(--text); + outline: none; +} + +.directorist-te-list { + display: block; + overflow: hidden; + border: 1px solid var(--border-card); + border-radius: var(--r-md); + background: var(--surface); + box-shadow: var(--shadow); +} + +.directorist-te-list-head, +.directorist-te-row { + display: grid; + grid-template-columns: 22px 40px minmax(0, 1fr) 150px 300px; + align-items: center; + gap: 14px; +} + +.directorist-te-list-head { + min-height: 41px; + padding: 10px 18px; + border-bottom: 1px solid var(--border-soft); + background: var(--bg-soft); + color: var(--faint); + font-size: 10px; + font-weight: 700; + line-height: 1; + text-transform: uppercase; +} + +.directorist-te-list-head__addon { + grid-column: 3; +} + +.directorist-te-list-head__status { + grid-column: 4; +} + +.directorist-te-page--disconnected .directorist-te-list-head, +.directorist-te-page--disconnected .directorist-te-row { + grid-template-columns: 40px minmax(0, 1fr) 150px 180px; +} + +.directorist-te-page--disconnected .directorist-te-list-head__addon { + grid-column: 1 / 3; +} + +.directorist-te-page--disconnected .directorist-te-list-head__status { + grid-column: 3; +} + +.directorist-te-row { + min-height: 66px; + padding: 12px 18px; + background: var(--surface); + transition: background 0.12s ease, box-shadow 0.12s ease; +} + +.directorist-te-row + .directorist-te-row, +.directorist-te-row + .directorist-te-empty--filter { + border-top: 1px solid var(--border-soft); +} + +.directorist-te-row:hover { + background: var(--bg-soft); +} + +.directorist-te-row.selected { + box-shadow: inset 3px 0 0 var(--accent); +} + +.directorist-te-row__select { + width: 22px; + min-width: 22px; +} + +.directorist-te-checkbox { + position: relative; + width: 20px; + height: 20px; + display: inline-grid; + place-items: center; + margin: 0; + border: 2px solid #cbd5e1; + border-radius: 5px; + background: var(--surface); + cursor: pointer; +} + +.directorist-te-checkbox input { + position: absolute; + inset: -2px; + width: 20px; + height: 20px; + margin: 0; + opacity: 0; + cursor: pointer; +} + +.directorist-te-checkbox i { + color: #fff; + font-size: 13px; + line-height: 1; + opacity: 0; +} + +.directorist-te-checkbox:has(input:checked), +.directorist-te-checkbox.is-checked { + border-color: var(--accent); + background: var(--accent); +} + +.directorist-te-checkbox:has(input:checked) i, +.directorist-te-checkbox.is-checked i { + opacity: 1; +} + +.directorist-te-row__media { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + border-radius: var(--r-md); + background: var(--line); +} + +.directorist-te-row__media img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.directorist-te-row__content { + min-width: 0; +} + +.directorist-te-row__title { + gap: 8px; + min-width: 0; +} + +.directorist-te-row__title h2 { + min-width: 0; + margin: 0; + overflow: hidden; + color: var(--text); + font-size: 14px; + font-weight: 600; + line-height: 1.3; + text-overflow: ellipsis; + white-space: nowrap; +} + +.directorist-te-version { + color: var(--faint); + font-size: 12px; + white-space: nowrap; +} + +.directorist-te-row__content p { + margin: 3px 0 0; + overflow: hidden; + color: var(--muted); + font-size: 12.5px; + line-height: 1.4; + text-overflow: ellipsis; + white-space: nowrap; +} + +.directorist-te-row__meta { + gap: 6px; + flex-wrap: wrap; + margin-top: 5px; +} + +.directorist-te-type, +.directorist-te-badge { + display: inline-flex; + align-items: center; + min-height: 18px; + padding: 2px 7px; + border-radius: 5px; + background: var(--line); + color: var(--muted); + font-size: 10px; + font-weight: 700; + line-height: 1; + text-transform: uppercase; + white-space: nowrap; +} + +.directorist-te-type--theme { + color: #166534; + background: #dcfce7; +} + +.directorist-te-badge { + color: #7c2d12; + background: #ffedd5; +} + +.directorist-te-row__status { + min-width: 0; +} + +.directorist-te-status { + display: inline-flex; + align-items: center; + gap: 7px; + max-width: 100%; + color: var(--muted); + font-size: 12.5px; + font-weight: 500; + line-height: 1.3; +} + +.directorist-te-status::before { + width: 8px; + height: 8px; + flex: 0 0 auto; + border-radius: 50%; + background: var(--faint); + content: ""; +} + +.directorist-te-status--active::before { + background: var(--success); +} + +.directorist-te-status--installed::before { + background: #0ea5b7; +} + +.directorist-te-status--update { + color: var(--warning); +} + +.directorist-te-status--update::before, +.directorist-te-status--required::before { + background: var(--warning); +} + +.directorist-te-status--not-installed::before, +.directorist-te-status--marketplace::before { + background: #cbd5e1; +} + +.directorist-te-row__actions { + justify-content: flex-end; + gap: 8px; + min-width: 0; +} + +.directorist-te-row__actions > .directorist-te-btn { + min-width: 104px; + min-height: 31px; + padding: 6px 12px; + font-size: 12.5px; +} + +.directorist-te-row__actions > .directorist-te-btn--details { + min-width: 96px; +} + +.directorist-te-row__actions > .directorist-te-btn--demo { + min-width: 64px; +} + +.directorist-te-menu { + position: relative; + flex: 0 0 auto; +} + +.directorist-te-menu__items { + position: absolute; + top: calc(100% + 7px); + right: 0; + z-index: 50; + min-width: 184px; + display: none; + padding: 6px; + border: 1px solid var(--border); + border-radius: var(--r-sm); + background: var(--surface); + box-shadow: var(--shadow-pop); +} + +.directorist-te-menu .ext-action-drop.active + .directorist-te-menu__items, +.directorist-te-menu__items.is-open { + display: grid; + gap: 2px; +} + +.directorist-te-menu-link { + min-height: 34px; + display: flex; + align-items: center; + padding: 0 10px; + border-radius: var(--r-xs); + color: var(--text); + font-size: 13px; + font-weight: 500; + text-decoration: none; +} + +.directorist-te-menu-link:hover, +.directorist-te-menu-link:focus { + background: var(--bg-soft); + color: var(--accent); + outline: none; +} + +.directorist-te-menu-link--danger:hover, +.directorist-te-menu-link--danger:focus { + background: var(--danger-tint); + color: var(--danger); +} + +.directorist-te-empty { + padding: 42px 18px; + text-align: center; + background: var(--surface); +} + +.directorist-te-empty--filter { + display: none; +} + +.directorist-te-empty--filter.is-visible { + display: block; +} + +.directorist-te-empty > i { + display: block; + margin-bottom: 8px; + color: var(--faint); + font-size: 28px; +} + +.directorist-te-empty h2 { + text-align: center; +} + +.directorist-te-empty p { + text-align: center; +} + +.directorist-te-empty-reset { + margin: 14px auto 0; +} + +.directorist-te-upsell { + justify-content: space-between; + gap: 20px; + margin: 16px 0 0; + padding: 22px 26px; + border-radius: var(--r-md); + background: linear-gradient(135deg, #111827, #1e293b); + color: #fff; +} + +.directorist-te-upsell h2, +.directorist-te-upsell p { + color: #fff; +} + +.directorist-te-upsell p { + opacity: 0.78; +} + +.directorist-te-upsell .directorist-te-btn { + border-color: #fff; + background: #fff; + color: #111827; +} + +.directorist-te-footer { + justify-content: center; + gap: 7px; + max-width: 1200px; + margin: 22px 0 0; + padding-top: 22px; + border-top: 1px solid var(--border); + color: var(--muted); + font-size: 12.5px; + text-align: center; +} + +.directorist-te-footer a { + color: var(--accent); + text-decoration: none; +} + +@media (max-width: 1120px) { + .directorist-te-top-right { + display: none; + } + + .directorist-te-list-head, + .directorist-te-row { + grid-template-columns: 22px 40px minmax(0, 1fr) 132px 270px; + } + + .directorist-te-page--disconnected .directorist-te-list-head, + .directorist-te-page--disconnected .directorist-te-row { + grid-template-columns: 40px minmax(0, 1fr) 132px 180px; + } +} + +@media (max-width: 960px) { + .directorist-te-account { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .directorist-te-account__actions { + grid-column: 1 / -1; + justify-content: flex-start; + flex-wrap: wrap; + } + + .directorist-te-connect { + grid-template-columns: 1fr; + } +} + +@media (max-width: 782px) { + .directorist-te-header { + top: 46px; + } +} + +@media (max-width: 720px) { + .directorist-te-shell { + margin: 20px 0 36px; + } + + .directorist-te-header { + height: auto; + min-height: 62px; + flex-wrap: wrap; + gap: 12px; + margin: 0; + padding: 12px 16px; + } + + .directorist-te-nav { + order: 3; + width: 100%; + overflow-x: auto; + flex-wrap: nowrap; + } + + .directorist-te-resource-links { + gap: 14px; + margin-left: auto; + overflow-x: auto; + } + + .directorist-te-hero { + align-items: stretch; + flex-direction: column; + margin-top: 20px; + } + + .directorist-te-search { + width: 100%; + } + + .directorist-te-toolbar-search { + flex: 1 1 220px; + min-width: 0; + } + + .directorist-te-connect, + .directorist-te-connect.account-connect, + #directorist.atbd_wrapper.directorist-te-page .directorist-te-connect.account-connect, + .directorist-te-account, + .directorist-te-update-banner, + .directorist-te-toolbar, + .directorist-te-upsell { + margin-right: 0; + margin-left: 0; + } + + .directorist-te-toolbar-main { + align-items: stretch; + flex-direction: column; + gap: 12px; + margin-bottom: 14px; + } + + .directorist-te-toolbar-catalog { + width: 100%; + margin-left: 0; + padding-bottom: 12px; + flex-wrap: wrap; + justify-content: space-between; + } + + .directorist-te-products { + padding-right: 0; + padding-left: 0; + } + + .directorist-te-update-banner, + .directorist-te-upsell { + align-items: stretch; + flex-direction: column; + } + + .directorist-te-field-row, + .directorist-te-account { + grid-template-columns: 1fr; + } + + .directorist-te-account__actions { + grid-column: auto; + } + + .directorist-te-refresh-form { + width: min(330px, calc(100vw - 64px)); + } + + .directorist-te-bulkbar { + top: 108px; + flex-wrap: wrap; + } + + .directorist-te-bulk-clear { + margin-left: 0; + } + + .directorist-te-list-head, + .directorist-te-row { + grid-template-columns: 22px 36px minmax(0, 1fr) auto; + gap: 12px; + } + + .directorist-te-page--disconnected .directorist-te-list-head, + .directorist-te-page--disconnected .directorist-te-row { + grid-template-columns: 36px minmax(0, 1fr) auto; + } + + .directorist-te-list-head__status, + .directorist-te-row__status { + display: none; + } + + .directorist-te-row__media { + width: 36px; + height: 36px; + } + + .directorist-te-row__actions { + justify-content: flex-end; + } + + .directorist-te-row__actions > .directorist-te-btn { + min-width: 0; + } +} + +@media (max-width: 520px) { + .directorist-te-list-head, + .directorist-te-row { + grid-template-columns: 22px minmax(0, 1fr); + } + + .directorist-te-page--disconnected .directorist-te-list-head, + .directorist-te-page--disconnected .directorist-te-row { + grid-template-columns: minmax(0, 1fr); + } + + .directorist-te-list-head { + padding-right: 14px; + padding-left: 14px; + } + + .directorist-te-list-head__addon { + grid-column: 2; + } + + .directorist-te-page--disconnected .directorist-te-list-head__addon { + grid-column: 1; + } + + .directorist-te-row { + padding-right: 14px; + padding-left: 14px; + } + + .directorist-te-row__media { + display: none; + } + + .directorist-te-row__content, + .directorist-te-row__actions { + grid-column: 2; + } + + .directorist-te-page--disconnected .directorist-te-row__content, + .directorist-te-page--disconnected .directorist-te-row__actions { + grid-column: 1; + } + + .directorist-te-page--disconnected .directorist-te-row { + grid-template-columns: 40px minmax(0, 1fr); + align-items: start; + row-gap: 10px; + } + + .directorist-te-page--disconnected .directorist-te-row__media { + grid-column: 1; + display: flex; + } + + .directorist-te-page--disconnected .directorist-te-row__content { + grid-column: 2; + } + + .directorist-te-page--disconnected .directorist-te-row__title { + align-items: flex-start; + flex-wrap: wrap; + } + + .directorist-te-page--disconnected .directorist-te-row__title h2 { + white-space: normal; + } + + .directorist-te-page--disconnected .directorist-te-row__actions { + grid-column: 1 / -1; + } + + .directorist-te-page--disconnected .directorist-te-row__actions > .directorist-te-btn { + width: 100%; + } + + .directorist-te-row__actions { + justify-content: flex-start; + flex-wrap: wrap; + margin-top: 2px; + } + + .directorist-te-row__content p { + white-space: normal; + } + + .directorist-te-segmented { + width: 100%; + overflow-x: auto; + } + + .directorist-te-upsell, + .directorist-te-footer { + align-items: stretch; + } +} + +@media (max-width: 360px) { + .directorist-te-page--disconnected .directorist-te-header { + align-items: flex-start; + } + + .directorist-te-page--disconnected .directorist-te-resource-links { + width: 100%; + margin-left: 0; + padding-top: 2px; + } +} diff --git a/assets/js/directorist-themes-extensions.js b/assets/js/directorist-themes-extensions.js new file mode 100644 index 0000000000..3846e82859 --- /dev/null +++ b/assets/js/directorist-themes-extensions.js @@ -0,0 +1,774 @@ +(function ($) { + 'use strict'; + + const page = $('.directorist-te-page'); + + if (!page.length) { + return; + } + + const state = { + type: 'all', + status: 'all', + query: '', + }; + + const rows = () => $('.directorist-te-row'); + const visibleRows = () => rows().filter(':not(.is-hidden)'); + const selectableChecks = () => + $('#atbdp-my-extensions-form .extension-name-checkbox'); + const checkedSelectableChecks = () => selectableChecks().filter(':checked'); + const visibleSelectableChecks = () => + selectableChecks().filter(function () { + return !$(this).closest('.directorist-te-row').hasClass('is-hidden'); + }); + const updateBanner = () => $('.directorist-te-update-banner'); + const updatePill = () => + $('.directorist-te-segmented button[data-filter-status="update"] span'); + + const ajaxConfig = () => window.directorist_admin || {}; + const ajaxUrl = () => ajaxConfig().ajaxurl || window.ajaxurl; + const nonce = () => ajaxConfig().nonce || ''; + const directoristNonce = () => ajaxConfig().directorist_nonce || nonce(); + + function statusTokens(row) { + return String($(row).data('product-status') || '') + .split(/\s+/) + .filter(Boolean); + } + + function rowHasStatus(row, status) { + const tokens = statusTokens(row); + + if (status === 'all') { + return true; + } + + if (status === 'installed') { + return ( + tokens.includes('installed') || + tokens.includes('active') || + tokens.includes('update') + ); + } + + if (status === 'not-installed') { + return ( + tokens.includes('not-installed') || + tokens.includes('marketplace') + ); + } + + return tokens.includes(status); + } + + function setButtonLoading(button, label) { + const $button = $(button); + + if (!$button.data('default-html')) { + $button.data('default-html', $button.html()); + } + + $button.prop('disabled', true).html( + ` ${label}` + ); + } + + function resetButton(button) { + const $button = $(button); + const defaultHtml = $button.data('default-html'); + + $button.prop('disabled', false); + + if (defaultHtml) { + $button.html(defaultHtml); + } + } + + function setCheckbox(checkbox, checked) { + const $checkbox = $(checkbox); + const $row = $checkbox.closest('.directorist-te-row'); + const $label = $checkbox.closest('.directorist-te-checkbox'); + + $checkbox.prop('checked', checked); + $row.toggleClass('selected', checked); + $label.toggleClass('is-checked', checked); + } + + function syncMasterCheckbox() { + const master = $('#select-all-installed'); + + if (!master.length) { + return; + } + + const visible = visibleSelectableChecks(); + const checked = visible.filter(':checked'); + const isAll = visible.length > 0 && checked.length === visible.length; + const isSome = checked.length > 0 && checked.length < visible.length; + + master.prop('checked', isAll); + master.prop('indeterminate', isSome); + master + .closest('.directorist-te-checkbox') + .toggleClass('is-checked', isAll || isSome); + } + + function updateCountsAndEmptyState() { + const shown = visibleRows().length; + const label = shown === 1 ? '1 add-on' : `${shown} add-ons`; + + $('.directorist-te-count').text(label); + $('.directorist-te-empty--filter').toggleClass('is-visible', shown === 0); + $('.directorist-te-upsell').toggle( + state.type === 'all' && state.status === 'all' && !state.query.trim() + ); + } + + function scopedUpdateCount() { + return rows() + .filter(function () { + const typeMatches = + state.type === 'all' || + $(this).data('product-type') === state.type; + + return typeMatches && statusTokens(this).includes('update'); + }) + .length; + } + + function updateUpdateBanner() { + const banner = updateBanner(); + + if (!banner.length) { + return; + } + + const updates = scopedUpdateCount(); + const hasSelection = checkedSelectableChecks().length > 0; + const shouldShow = + updates > 0 && + !hasSelection && + ['all', 'installed', 'update'].includes(state.status); + + banner.prop('hidden', !shouldShow); + updatePill().text(updates).toggle(updates > 0); + } + + function updateBulkBar() { + const checked = checkedSelectableChecks(); + const count = checked.length; + const bulkbar = $('.directorist-te-bulkbar'); + + bulkbar.prop('hidden', count === 0).toggleClass('show', count > 0); + $('.directorist-te-selected-count').text(`${count} selected`); + + syncMasterCheckbox(); + updateUpdateBanner(); + } + + function updateFilters() { + const query = state.query.trim().toLowerCase(); + + rows().each(function () { + const row = $(this); + const typeMatches = + state.type === 'all' || row.data('product-type') === state.type; + const statusMatches = rowHasStatus(this, state.status); + const textMatches = + !query || + String(row.data('search-text') || '').includes(query); + + row.toggleClass( + 'is-hidden', + !(typeMatches && statusMatches && textMatches) + ); + }); + + updateCountsAndEmptyState(); + updateBulkBar(); + } + + function escapeHtml(value) { + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + + function formMessage(form, key, fallback) { + return form.attr(`data-${key}`) || fallback; + } + + function connectFeedback(form) { + return form.find('.directorist-te-feedback').first(); + } + + function renderConnectFeedback(form, type, message) { + connectFeedback(form) + .attr('role', 'alert') + .html( + `
${escapeHtml( + message + )}
` + ); + } + + function clearConnectFeedback(form) { + connectFeedback(form).attr('role', 'status').empty(); + form.find('input[name="username"], input[name="password"]').removeAttr( + 'aria-invalid' + ); + } + + function setConnectBusy(form, busy) { + const submitButton = form.find('button[type="submit"]').first(); + const fields = form.find('input[name="username"], input[name="password"]'); + + if (!submitButton.data('default-html')) { + submitButton.data('default-html', submitButton.html()); + } + + form.toggleClass('is-submitting', busy).attr('aria-busy', busy); + fields.prop('disabled', busy); + submitButton.prop('disabled', busy); + + if (busy) { + submitButton.html( + formMessage(form, 'connecting-label', 'Connecting...') + ); + return; + } + + submitButton.html(submitButton.data('default-html')); + } + + function isAuthRequest(settings) { + const data = settings?.data || ''; + + if (typeof data === 'string') { + try { + return ( + new URLSearchParams(data).get('action') === + 'atbdp_authenticate_the_customer' + ); + } catch (error) { + return data.includes('action=atbdp_authenticate_the_customer'); + } + } + + return data?.action === 'atbdp_authenticate_the_customer'; + } + + function resetCatalogFilters() { + state.type = 'all'; + state.status = 'all'; + state.query = ''; + + $('.directorist-te-tab').removeClass('is-active'); + $('.directorist-te-tab[data-filter-type="all"]').addClass('is-active'); + $('.directorist-te-segmented button').removeClass('is-active'); + $('.directorist-te-segmented button[data-filter-status="all"]').addClass( + 'is-active' + ); + $('.directorist-te-search-input').val(''); + updateFilters(); + $('.directorist-te-search-input').first().trigger('focus'); + } + + function runPluginBulkTask(task, pluginItems, button) { + if (!pluginItems.length) { + return; + } + + setButtonLoading(button, 'Working'); + + $.ajax({ + type: 'post', + url: ajaxUrl(), + data: { + action: 'atbdp_plugins_bulk_action', + task, + plugin_items: pluginItems, + directorist_nonce: directoristNonce(), + }, + success(response) { + if (response?.status && response.status.success === false) { + alert(response.status.message || 'Action failed.'); + resetButton(button); + return; + } + + window.location.reload(); + }, + error() { + alert('Action failed. Please reload the page and try again.'); + resetButton(button); + }, + }); + } + + function runSelectedPluginUpdates(pluginItems, button) { + const queue = pluginItems.slice(); + + if (!queue.length) { + return; + } + + setButtonLoading(button, 'Updating'); + + const next = () => { + const pluginKey = queue.shift(); + + if (!pluginKey) { + window.location.reload(); + return; + } + + $.ajax({ + type: 'post', + url: ajaxUrl(), + data: { + action: 'atbdp_update_plugins', + plugin_key: pluginKey, + nonce: nonce(), + }, + success(response) { + if ( + response?.status && + response.status.success === false + ) { + alert(response.status.message || 'Update failed.'); + resetButton(button); + return; + } + + next(); + }, + error() { + alert('Update failed. Please reload the page and try again.'); + resetButton(button); + }, + }); + }; + + next(); + } + + $('.directorist-te-tab').on('click', function () { + $('.directorist-te-tab').removeClass('is-active'); + $(this).addClass('is-active'); + state.type = $(this).data('filter-type') || 'all'; + updateFilters(); + }); + + $('.directorist-te-segmented button').on('click', function () { + $('.directorist-te-segmented button').removeClass('is-active'); + $(this).addClass('is-active'); + state.status = $(this).data('filter-status') || 'all'; + updateFilters(); + }); + + $('.directorist-te-search-input').on('input', function () { + state.query = $(this).val(); + updateFilters(); + }); + + $('.directorist-te-empty-reset').on('click', function () { + resetCatalogFilters(); + }); + + $('#atbdp-directorist-license-login-form.directorist-te-connect-form').on( + 'submit', + function (event) { + const form = $(this); + const username = form.find('input[name="username"]').first(); + const password = form.find('input[name="password"]').first(); + + clearConnectFeedback(form); + + if (!username.val().trim()) { + event.preventDefault(); + event.stopImmediatePropagation(); + username.attr('aria-invalid', 'true').trigger('focus'); + renderConnectFeedback( + form, + 'danger', + formMessage( + form, + 'username-required', + 'Enter your Directorist account username or email.' + ) + ); + return; + } + + if (!password.val()) { + event.preventDefault(); + event.stopImmediatePropagation(); + password.attr('aria-invalid', 'true').trigger('focus'); + renderConnectFeedback( + form, + 'danger', + formMessage( + form, + 'password-required', + 'Enter your Directorist account password.' + ) + ); + return; + } + + setConnectBusy(form, true); + } + ); + + $( + '#atbdp-directorist-license-login-form.directorist-te-connect-form input[name="username"], #atbdp-directorist-license-login-form.directorist-te-connect-form input[name="password"]' + ).on('input', function () { + const form = $(this).closest('.directorist-te-connect-form'); + + $(this).removeAttr('aria-invalid'); + + if (!form.find('[aria-invalid="true"]').length) { + connectFeedback(form).attr('role', 'status').empty(); + } + }); + + $(document).on('ajaxSuccess', function (_event, _xhr, settings, response) { + if (!isAuthRequest(settings)) { + return; + } + + const form = $( + '#atbdp-directorist-license-login-form.directorist-te-connect-form' + ); + + if (!form.length || response?.has_previous_subscriptions) { + return; + } + + if (response?.status?.success) { + return; + } + + if (!connectFeedback(form).text().trim()) { + renderConnectFeedback( + form, + 'danger', + formMessage( + form, + 'unexpected-error', + 'Could not connect. Please check your details and try again.' + ) + ); + } + + setConnectBusy(form, false); + }); + + $(document).on('ajaxError', function (_event, _xhr, settings) { + if (!isAuthRequest(settings)) { + return; + } + + const form = $( + '#atbdp-directorist-license-login-form.directorist-te-connect-form' + ); + + if (!form.length) { + return; + } + + renderConnectFeedback( + form, + 'danger', + formMessage( + form, + 'network-error', + 'Could not reach Directorist.com. Please try again.' + ) + ); + setConnectBusy(form, false); + }); + + $('.directorist-te-password-toggle').on('click', function (event) { + event.preventDefault(); + + const button = $(this); + const input = button + .closest('.directorist-te-password-control') + .find('input[name="password"]') + .first(); + + if (!input.length) { + return; + } + + const isHidden = input.attr('type') === 'password'; + const nextLabel = isHidden + ? button.data('hide-label') + : button.data('show-label'); + + input.attr('type', isHidden ? 'text' : 'password').trigger('focus'); + button.attr({ + 'aria-label': nextLabel, + 'aria-pressed': isHidden ? 'true' : 'false', + }); + button.find('i').toggleClass('la-eye', !isHidden); + button.find('i').toggleClass('la-eye-slash', isHidden); + }); + + $('#atbdp-my-extensions-form').on( + 'change', + '.extension-name-checkbox', + function () { + setCheckbox(this, this.checked); + updateBulkBar(); + } + ); + + $('#select-all-installed').on('change', function () { + const checked = this.checked; + + visibleSelectableChecks().each(function () { + setCheckbox(this, checked); + }); + + updateBulkBar(); + }); + + $('.directorist-te-bulk-clear').on('click', function () { + selectableChecks().each(function () { + setCheckbox(this, false); + }); + + updateBulkBar(); + }); + + $(document).on('keydown', function (event) { + if (event.key !== 'Escape' || !checkedSelectableChecks().length) { + return; + } + + selectableChecks().each(function () { + setCheckbox(this, false); + }); + + $('.ext-action-drop').removeClass('active'); + updateBulkBar(); + }); + + $('.directorist-te-bulk-action').on('click', function (event) { + event.preventDefault(); + + const task = $(this).data('task'); + const pluginItems = checkedSelectableChecks() + .map(function () { + return this.id; + }) + .get(); + + if (!task || !pluginItems.length) { + return; + } + + if ( + task === 'uninstall' && + !window.confirm( + 'Delete removes the selected plugin files from this site. Continue?' + ) + ) { + return; + } + + if (task === 'update') { + runSelectedPluginUpdates(pluginItems, this); + return; + } + + runPluginBulkTask(task, pluginItems, this); + }); + + $('.directorist-te-single-plugin-task').on('click', function (event) { + event.preventDefault(); + + const task = $(this).data('task'); + const target = $(this).data('target'); + + if (!task || !target) { + return; + } + + runPluginBulkTask(task, [target], this); + }); + + $('.directorist-te-update-all').on('click', function (event) { + event.preventDefault(); + + const button = this; + const updateExtensions = $(button).data('update-extensions') === 1; + const updateThemes = $(button).data('update-themes') === 1; + const queue = []; + + if (updateExtensions) { + queue.push('atbdp_update_plugins'); + } + + if (updateThemes) { + queue.push('atbdp_update_theme'); + } + + if (!queue.length) { + return; + } + + setButtonLoading(button, 'Updating'); + + const next = () => { + const action = queue.shift(); + + if (!action) { + window.location.reload(); + return; + } + + $.ajax({ + type: 'post', + url: ajaxUrl(), + data: { + action, + nonce: nonce(), + }, + success(response) { + if ( + response?.status && + response.status.success === false + ) { + alert(response.status.message || 'Update failed.'); + resetButton(button); + return; + } + + next(); + }, + error() { + alert('Update failed. Please reload the page and try again.'); + resetButton(button); + }, + }); + }; + + next(); + }); + + $('.directorist-te-menu-toggle').on('click', function (event) { + event.preventDefault(); + event.stopPropagation(); + + const wasOpen = $(this).hasClass('active'); + + $('.ext-action-drop').removeClass('active'); + + if (!wasOpen) { + $(this).addClass('active'); + } + }); + + $('.directorist-te-menu__items').on('click', function (event) { + event.stopPropagation(); + }); + + $('.directorist-te-menu-link').on('click', function () { + $('.ext-action-drop').removeClass('active'); + }); + + $(document).on('click', function (event) { + if ($(event.target).closest('.directorist-te-menu').length) { + return; + } + + $('.ext-action-drop').removeClass('active'); + }); + + document.addEventListener( + 'click', + function (event) { + const menuToggle = event.target.closest( + '.directorist-te-menu-toggle' + ); + + if (menuToggle) { + event.preventDefault(); + event.stopPropagation(); + + const wasOpen = menuToggle.classList.contains('active'); + + document + .querySelectorAll('.ext-action-drop.active') + .forEach((toggle) => toggle.classList.remove('active')); + + if (!wasOpen) { + menuToggle.classList.add('active'); + } + + return; + } + + const themeButton = event.target.closest('.theme-activate-btn'); + + if (themeButton && !themeButton.dataset.directoristConfirmed) { + if ( + !window.confirm( + 'Activating this theme changes the live site theme. Continue?' + ) + ) { + event.preventDefault(); + event.stopPropagation(); + event.stopImmediatePropagation(); + return; + } + + themeButton.dataset.directoristConfirmed = '1'; + window.setTimeout(() => { + delete themeButton.dataset.directoristConfirmed; + }, 1000); + } + + const uninstallLink = event.target.closest('.ext-action-uninstall'); + + if (!uninstallLink) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + event.stopImmediatePropagation(); + + const target = uninstallLink.dataset.target; + + if (!target) { + return; + } + + if ( + !window.confirm( + 'Delete removes this plugin from the server. Continue?' + ) + ) { + return; + } + + runPluginBulkTask('uninstall', [target], uninstallLink); + }, + true + ); + + selectableChecks().each(function () { + setCheckbox(this, this.checked); + }); + + updateFilters(); +})(jQuery); diff --git a/docs/agents/directorist-themes-extensions-page/SKILL.md b/docs/agents/directorist-themes-extensions-page/SKILL.md index b5243c6069..462a10bdf5 100644 --- a/docs/agents/directorist-themes-extensions-page/SKILL.md +++ b/docs/agents/directorist-themes-extensions-page/SKILL.md @@ -76,6 +76,7 @@ agent-browser --session directorist-themes-extensions close - Preserve the page slug `atbdp-extension`, parent post type `at_biz_dir`, capability `manage_options`, existing AJAX action names, nonce expectations, response compatibility, settings links, filters, aliases, and legacy user meta keys. - Preserve legacy misspellings that are part of stored data or public contracts, including `_atbdp_has_subscriptions_sassion` and `atbdp_close_subscriptions_sassion`. - Do not remove or repurpose old selectors, classes, template paths, filters, or action hooks without a compatibility shim and explicit migration plan. +- After a new UI replaces an old page section, audit old design-specific CSS and remove unused rules after verifying they are not used by the rewritten page, legacy compatibility shims, template overrides, or other Directorist admin screens. - Treat install, update, download, uninstall, and theme activation as high-risk operations. Use read-only inspection by default. - Keep this page separate from installed extension settings. Settings-panel work belongs to `docs/agents/directorist-settings-panel/SKILL.md`. - Treat Figma/design input as design intent. Do not rewrite backend product/install logic only to match a visual design. diff --git a/docs/agents/directorist-themes-extensions-page/references/qa-checklist.md b/docs/agents/directorist-themes-extensions-page/references/qa-checklist.md index 65e378928c..6da30c8a5d 100644 --- a/docs/agents/directorist-themes-extensions-page/references/qa-checklist.md +++ b/docs/agents/directorist-themes-extensions-page/references/qa-checklist.md @@ -14,6 +14,9 @@ Use this checklist for Themes & Extensions page work. Current runtime data must - Confirm product names, descriptions, thumbnails, and links use API values when present and local defaults when API fields are missing or invalid. - Confirm product badges/status render only from product API/filter-provided fields and do not rely on hardcoded product slugs or product order. - Confirm no old/new UI feature flag or rollout toggle was added unless explicitly requested. +- Confirm disconnected-state username is first in tab order but is not autofocused on normal page load. +- Confirm disconnected-state resource links are plain Docs, Tutorials, and Support links with external-link safety attributes. +- Confirm disconnected-state account copy changes based on local official Directorist product detection: normal subscription copy when none are installed, installed-product copy when local Directorist extensions/themes are present. ## Agent Browser Checks @@ -81,6 +84,17 @@ Use categories only in docs. Do not preserve observed counts or product lists. - Compare desktop and mobile interaction latency. - Check console and page errors after each tested journey. +## Disconnected Accessibility Checks + +- Confirm the disconnected page remains browse-first: account connect form plus marketplace catalog are both reachable without forced focus. +- Confirm username and password fields have visible labels. +- Confirm pressing Enter inside username/password submits the account-connect form. +- Confirm password visibility toggle changes the input type, icon, `aria-label`, and `aria-pressed`. +- Confirm connect loading state disables controls only during the active request and restores them after failure. +- Confirm empty username, empty password, wrong credentials, API unavailable, nonce failure, and unexpected errors render inline form feedback. +- Confirm disconnected search no-result state shows an inline empty state and a clear/reset affordance. +- Confirm mobile disconnected view uses a one-column connect form, does not open the keyboard on load, and has no horizontal page overflow. + ## Destructive-Action Safeguards Do not execute these on a real/client site without explicit confirmation: diff --git a/docs/agents/directorist-themes-extensions-page/references/rewrite-issue-register.md b/docs/agents/directorist-themes-extensions-page/references/rewrite-issue-register.md index 99bcd75a51..3cebc77d1a 100644 --- a/docs/agents/directorist-themes-extensions-page/references/rewrite-issue-register.md +++ b/docs/agents/directorist-themes-extensions-page/references/rewrite-issue-register.md @@ -22,21 +22,59 @@ The approved direction is a full UI rewrite of this page, not a staged old/new U - React should only be considered after an explicit larger admin UI migration decision, because it requires a new state/API layer and broader QA. - This architecture is the lowest-risk default for the 20k customer base: fewer moving parts, easier rollback, existing server-rendered fallback, and stable public contracts. +## Locked Reference Design Behavior + +When implementing from the approved HTML demo design, match the behavior as well as the static visuals. Do not store demo product rows or local runtime values as truth. + +- Use a compact admin-app layout: resource topbar, page head, optional account/connect panel, update banner, type tabs, status segmented control, list rows, upsell, and footer. +- Keep the core sizing rhythm from the reference: 1200px max content container, 24px desktop gutters, 62px topbar, 42px type tabs, 41px list header, and 66px dense desktop rows. Adapt only for WordPress admin chrome and responsive constraints. +- Top resource navigation should include Dashboard, Themes & Extensions, Docs, Tutorials, and Support. Docs, Tutorials, and Support are external handoff links; the active Themes & Extensions item should not duplicate the WordPress sidebar as the primary navigation source. +- On narrow screens, resource navigation can become a horizontal scroll area. Do not create page-level horizontal overflow. +- Filtering is cumulative: product type tab, product status segment, and search query combine together. A search query should continue to apply when switching tabs or status. +- Visible add-on count updates after filtering. If no rows match, show an inline empty state instead of leaving a blank list. +- The upsell/library banner shows only in the unfiltered default catalog view. Hide it during search, type filtering, or status filtering. +- Update banner visibility depends on update availability in the current type scope. Hide the banner while rows are selected. +- Row selection should visually set a selected row state and reveal a sticky bulk-action bar with `N selected`, plus Clear and Escape reset behavior. +- Preserve real safety constraints: only rows backed by safe existing bulk contracts should be selectable for bulk management. In the disconnected marketplace-only state, do not invent bulk management actions only to match the static demo. +- Master select applies to visible selectable rows only, and should show all/some/none visual states. +- Kebab menus open one at a time, outside click closes them, and destructive actions remain inside the overflow/danger path rather than primary row buttons. +- Safe read-only actions such as `View Details` and `Demo` should be visible row actions instead of being hidden only inside the overflow menu. Keep the overflow menu for secondary alternatives and protected/destructive actions. +- Do not render duplicate visible links to the same destination with different labels such as `Get It Now` and `View Details`. If disconnected marketplace rows only have a product details URL, show `View Details` as the single product link. Use `Get It Now` only when it points to a distinct purchase/checkout flow or when it is the single intended purchase CTA. +- Do not trigger Install, Update, Activate, Deactivate, Uninstall, Logout, Refresh Purchase, or theme switch during design QA without explicit confirmation. + ## Disconnected Account Policy - Preserve current disconnected-account behavior by default. +- Treat `logout` in this page context as Directorist account disconnect, not WordPress admin logout. - When `_atbdp_has_subscriptions_sassion` is empty, the page should render the account-connect form and the marketplace/promo discovery section. - Do not expose installed premium product management in the disconnected state by default. - Do not show Settings, Active status, Activate, Deactivate, Update, or local management actions for installed premium plugins/themes while disconnected unless a future task explicitly changes this policy. +- Do not show disconnected users management-only UI affordances such as header account utilities, Installed/Updates status filters, master select checkboxes, row selection, or bulk-action controls. Keep browsing affordances such as add-on search, type tabs, counts, product detail links, and Get It Now links. +- In the disconnected marketplace catalog, place add-on search with the catalog toolbar/count area instead of the hero. On desktop, keep type tabs, search, and visible count in one horizontal toolbar when space allows. Search controls the product list, so it should stay visually near the list it filters. +- In the disconnected marketplace catalog, do not show a top page menu for Dashboard or Themes & Extensions. The WordPress admin sidebar owns page navigation. Keep only right-aligned resource links for Docs, Tutorials, and Support. - Existing installed/active premium extensions can continue working elsewhere in WordPress, but the Themes & Extensions page should not manage them while disconnected. - License-backed actions must remain gated behind connected account/subscription data: new premium install, premium package download, premium update, refresh purchase, and any server-side license validation flow. -- Improve disconnected-state messaging without changing behavior. Recommended helper copy: `Already installed extensions will keep working. Connect your Directorist account to manage subscriptions, installs, and updates.` +- Improve disconnected-state messaging without changing behavior. If no official Directorist products are installed locally, use normal subscription-management copy. If official Directorist extensions/themes are installed locally while the account is disconnected, use installed-product copy such as `We found Directorist products installed on this site. They will keep working. Connect your Directorist account to verify subscriptions, install new products, receive updates, and manage license-backed product actions.` - The intent is to keep a clear account-first product-management surface, avoid license/update confusion, and reduce support risk for existing customer sites. - Connected-state page model can still group products as: - `Installed`: local WordPress plugin/theme state rendered after account connection. - `Available in your account`: subscription/license data rendered after account connection. - `Marketplace`: external product discovery, visible regardless of connection state. +## Disconnected UX And Accessibility Recommendations + +- Keep the disconnected state browse-first and connect-when-needed. Do not force login as the only first action. +- Do not autofocus the username field on normal page load. Autofocus can steal keyboard/screen-reader context and opens the mobile keyboard immediately. Username should remain first in the natural tab order. +- Focus the username field only after explicit user intent, such as a future `Connect account` CTA or a `#connect-account` deep link. +- Keep the account-connect form as a real form so pressing Enter inside username/password submits the connect request. +- Keep the password visibility toggle on the account password field. It must update the input type, icon, `aria-label`, and `aria-pressed` state. +- Show inline form feedback for empty username, empty password, wrong credentials, unavailable API, nonce/capability failure, and unexpected errors. Avoid browser-only alert feedback for normal validation failures. +- On connect submit, disable the username/password inputs and submit button only while the request is active. Show an immediate loading state such as `Connecting...`, and restore controls on failure. +- Keep Docs, Tutorials, and Support as simple right-aligned resource links in disconnected state. External links should use `target="_blank"` with `rel="noopener noreferrer"` and clear accessible text where needed. +- Keep disconnected marketplace actions read-only unless they are distinct external purchase/detail actions. Prefer `View Details` and `Demo`; use `Get It Now` only when it points to a real distinct purchase/checkout destination. +- If search/filter returns no results, show an inline empty state with a clear/reset affordance instead of a blank product list. +- On mobile, keep the connect form one-column, avoid autofocus, keep search near the product list, and verify there is no page-level horizontal overflow. + ## Remote Product Badge Policy - New, beta, popular, sale, or similar product badges/status should be driven by the product API data contract, not hardcoded in plugin templates. @@ -92,6 +130,18 @@ Primary areas: - Add progressive no-reload behavior behind existing AJAX actions. Use a small page-state adapter, loading states, normalized error handling, and canonical state refresh before claiming completion. - Keep full reload fallback for high-risk actions and unreconciled states. +### Legacy Design CSS Cleanup + +- A full UI rewrite should not leave old page-specific design styles in place when they are no longer used. +- After replacing an old section or selector set, audit legacy styles in `assets/src/scss/layout/admin/admin-style.scss`, generated/admin CSS, and any new page-specific stylesheet. +- Remove unused old design-specific rules only after confirming they are not used by: + - the rewritten Themes & Extensions page; + - compatibility selector shims kept for existing users; + - theme/template overrides; + - other Directorist admin screens that share the same classes. +- Prefer scoped new page styles over broad global overrides. If a legacy rule must stay for compatibility, document why it stays and isolate new styles with page-specific selectors. +- Verify the cleanup with source search, desktop/mobile Agent Browser checks, and console/page-error checks. Do not remove old classes that existing JavaScript still reads unless the JS is migrated or a shim remains. + ### Dead Login Continuation Flow - The account-connect success path reloads immediately, leaving older checklist/download continuation code unreachable. @@ -185,13 +235,14 @@ Primary areas: 7. Build a response formatter for all page AJAX handlers: success, error, message, action, item key, canonical state needed, reload fallback. 8. Add a small page-specific JavaScript state adapter around existing AJAX calls. 9. Replace fragile table layout with responsive rows/cards while preserving old selectors or compatibility wrappers. -10. Fix low-risk no-reload flows first: account-connect feedback, refresh purchase errors, tabs, filtering/search, inline errors, and button states. -11. Harden single plugin activation and bulk result handling. -12. Harden install/update/download services before changing their UI behavior. -13. Keep uninstall for compatibility, but move it behind a protected danger action with confirmation and canonical plugin-state revalidation. -14. Require confirmation modal for every theme switch, then revalidate canonical active-theme state or use reload fallback. -15. Treat plugin/theme update and filesystem replacement as conservative flows with server revalidation and reload fallback. -16. Remove or rebuild stale/dead code only after verifying no custom integration depends on old selectors or action names. +10. Audit and remove unused old design-specific CSS after confirming compatibility selectors and other admin screens do not depend on it. +11. Fix low-risk no-reload flows first: account-connect feedback, refresh purchase errors, tabs, filtering/search, inline errors, and button states. +12. Harden single plugin activation and bulk result handling. +13. Harden install/update/download services before changing their UI behavior. +14. Keep uninstall for compatibility, but move it behind a protected danger action with confirmation and canonical plugin-state revalidation. +15. Require confirmation modal for every theme switch, then revalidate canonical active-theme state or use reload fallback. +16. Treat plugin/theme update and filesystem replacement as conservative flows with server revalidation and reload fallback. +17. Remove or rebuild stale/dead code only after verifying no custom integration depends on old selectors or action names. ## Regression Checklist For Full Rewrite @@ -203,6 +254,7 @@ Primary areas: - Product badges render only from API/filter-provided badge data and disappear safely when absent or expired. - Existing installed active, installed inactive, subscribed-not-installed, outdated, required, promo-only, active theme, inactive theme, and update-available states are all represented. - No page-level horizontal overflow on mobile admin widths. +- Old design-specific CSS that is no longer used by this page, compatibility shims, overrides, or shared admin screens has been removed or explicitly documented as intentionally retained. - No action leaves buttons permanently disabled after failure. - No local action uses `alert()` as the only feedback. - Uninstall is not exposed as a one-click primary action; it requires explicit confirmation and recovers cleanly on failure. diff --git a/includes/asset-loader/init.php b/includes/asset-loader/init.php index 0d4490b977..8f0c3b2947 100644 --- a/includes/asset-loader/init.php +++ b/includes/asset-loader/init.php @@ -281,7 +281,9 @@ public static function admin_scripts( string $hook_suffix ) { wp_enqueue_media(); } elseif ( Helper::is_admin_page( 'extensions' ) ) { wp_enqueue_style( 'directorist-admin-style' ); + wp_enqueue_style( 'directorist-themes-extensions', DIRECTORIST_ASSETS . 'css/directorist-themes-extensions.css', [ 'directorist-admin-style' ], ATBDP_VERSION ); wp_enqueue_script( 'directorist-admin-script' ); + wp_enqueue_script( 'directorist-themes-extensions', DIRECTORIST_ASSETS . 'js/directorist-themes-extensions.js', [ 'jquery', 'directorist-admin-script' ], ATBDP_VERSION, true ); wp_enqueue_script( 'directorist-tooltip' ); // Inline styles diff --git a/views/admin-templates/theme-extensions/theme-extension.php b/views/admin-templates/theme-extensions/theme-extension.php index d44131f1f6..6a64e61bb1 100644 --- a/views/admin-templates/theme-extensions/theme-extension.php +++ b/views/admin-templates/theme-extensions/theme-extension.php @@ -1,16 +1,829 @@ -
- -
+get_extension_alias_key( $extension_key ); + + if ( isset( $extensions[ $extension_key ] ) ) { + return $extensions[ $extension_key ]; + } + + if ( ! empty( $alias_key ) && isset( $extensions[ $alias_key ] ) ) { + return $extensions[ $alias_key ]; + } + + return []; +}; + +$get_product_badges = static function( $product ) { + if ( ! is_array( $product ) ) { + return []; + } + + $badges = []; + foreach ( [ 'badge', 'badges', 'product_badge', 'product_badges', 'product_status', 'release_status' ] as $badge_key ) { + if ( empty( $product[ $badge_key ] ) ) { + continue; + } - load_template( 'admin-templates/theme-extensions/auth/license-auth-section', $args ); - } else { - ATBDP()->load_template( 'admin-templates/theme-extensions/statistics/statistics', $args ); - ATBDP()->load_template( 'admin-templates/theme-extensions/my-themes-extensions/my-themes-extensions', $args ); + $badge_values = is_array( $product[ $badge_key ] ) ? $product[ $badge_key ] : [ $product[ $badge_key ] ]; + foreach ( $badge_values as $badge_value ) { + if ( is_array( $badge_value ) ) { + $badge_value = $badge_value['label'] ?? $badge_value['name'] ?? ''; + } + + if ( is_scalar( $badge_value ) && '' !== trim( (string) $badge_value ) ) { + $badges[] = trim( (string) $badge_value ); + } } + } + + return array_values( array_unique( $badges ) ); +}; + +$get_link = static function( $product ) { + if ( empty( $product['link'] ) ) { + return '#'; + } + + return ATBDP_Upgrade::promo_link( $product['link'] ); +}; + +$get_image = static function( $product ) { + return ! empty( $product['thumbnail'] ) ? $product['thumbnail'] : DIRECTORIST_ASSETS . 'images/no-image.png'; +}; + +$get_details_action = static function( $product ) use ( $get_link ) { + return [ + 'label' => __( 'View Details', 'directorist' ), + 'href' => $get_link( $product ), + 'class' => 'directorist-te-btn directorist-te-btn--ghost directorist-te-btn--details', + 'external' => true, + ]; +}; + +$get_demo_action = static function( $product ) { + if ( empty( $product['demo_link'] ) ) { + return null; + } + + return [ + 'label' => __( 'Demo', 'directorist' ), + 'href' => $product['demo_link'], + 'class' => 'directorist-te-btn directorist-te-btn--ghost directorist-te-btn--demo', + 'external' => true, + ]; +}; + +$add_row = static function( $row ) use ( &$rows, &$seen_rows ) { + if ( empty( $row['key'] ) || isset( $seen_rows[ $row['key'] ] ) ) { + return; + } + + $seen_rows[ $row['key'] ] = true; + $rows[] = $row; +}; + +$render_attrs = static function( $attrs ) { + if ( empty( $attrs ) || ! is_array( $attrs ) ) { + return; + } + + foreach ( $attrs as $name => $value ) { + printf( ' %s="%s"', esc_attr( $name ), esc_attr( $value ) ); + } +}; + +$render_action = static function( $action ) use ( $render_attrs ) { + if ( empty( $action['label'] ) ) { + return; + } + + $href = ! empty( $action['href'] ) ? $action['href'] : '#'; + $class = ! empty( $action['class'] ) ? $action['class'] : 'directorist-te-btn'; + $target = ! empty( $action['external'] ) ? ' target="_blank" rel="noopener noreferrer"' : ''; + ?> + > + + + + + + + + $required_extension ) { + $extension_alias = $args['ATBDP_Extensions']->get_extension_alias_key( $extension_key ); + $required_base = ! empty( $required_extension['base'] ) ? $required_extension['base'] : "{$extension_key}/{$extension_key}.php"; + + foreach ( array_filter( [ $extension_key, $extension_alias, $required_base, preg_replace( '/\/.+/', '', $required_base ) ] ) as $required_lookup_key ) { + $required_lookup[ $required_lookup_key ] = $required_extension; + } + } +} + +if ( $is_logged_in && ! empty( $args['installed_extension_list'] ) && is_array( $args['installed_extension_list'] ) ) { + foreach ( $args['installed_extension_list'] as $extension_base => $extension ) { + $extension_key = preg_replace( '/\/.+/', '', $extension_base ); + $product = $get_extension_product( $extension_key ); + $name = ! empty( $extension['Name'] ) ? $extension['Name'] : ( $product['name'] ?? $extension_key ); + $version = ! empty( $extension['Version'] ) ? $extension['Version'] : ''; + $description = $product['description'] ?? ( $extension['Description'] ?? '' ); + $has_update = in_array( $extension_base, $outdated_keys, true ); + $is_active = is_plugin_active( $extension_base ); + $is_required = isset( $required_lookup[ $extension_base ] ) || isset( $required_lookup[ $extension_key ] ); + $status = $has_update ? 'update installed active' : ( $is_active ? 'active installed' : 'installed' ); + $status_label = $has_update ? __( 'Update available', 'directorist' ) : ( $is_active ? __( 'Active', 'directorist' ) : __( 'Installed', 'directorist' ) ); + $primary = $has_update + ? [ + 'label' => $is_beta ? __( 'Update Beta', 'directorist' ) : __( 'Update', 'directorist' ), + 'class' => 'directorist-te-btn directorist-te-btn--primary ext-update-btn', + 'attrs' => [ 'data-key' => $extension_base ], + 'icon' => 'la la-refresh', + ] + : ( + $is_active + ? [ + 'label' => __( 'Settings', 'directorist' ), + 'href' => $args['settings_url'], + 'class' => 'directorist-te-btn directorist-te-btn--secondary', + 'icon' => 'la la-settings', + ] + : [ + 'label' => __( 'Activate', 'directorist' ), + 'class' => 'directorist-te-btn directorist-te-btn--primary plugin-active-btn', + 'attrs' => [ + 'data-type' => 'plugin', + 'data-key' => $extension_base, + ], + 'icon' => 'la la-check', + ] + ); + + $menu = [ + [ + 'label' => __( 'Settings', 'directorist' ), + 'href' => $args['settings_url'], + 'class' => 'directorist-te-menu-link', + ], + ]; + + if ( $is_active ) { + $menu[] = [ + 'label' => __( 'Deactivate', 'directorist' ), + 'class' => 'directorist-te-menu-link directorist-te-single-plugin-task', + 'attrs' => [ + 'data-task' => 'deactivate', + 'data-target' => $extension_base, + ], + ]; + } + + $menu[] = [ + 'label' => __( 'Uninstall', 'directorist' ), + 'class' => 'directorist-te-menu-link directorist-te-menu-link--danger ext-action-uninstall', + 'attrs' => [ 'data-target' => $extension_base ], + ]; + + $badges = $get_product_badges( $product ); + if ( $is_required ) { + $badges[] = __( 'Required', 'directorist' ); + } + + $add_row( + [ + 'key' => 'extension-installed-' . $extension_base, + 'type' => 'extension', + 'status' => $status, + 'name' => $name, + 'version' => $version, + 'description' => $description, + 'image' => $get_image( $product ), + 'badges' => $badges, + 'statusLabel' => $status_label, + 'primary' => $primary, + 'menu' => $menu, + 'bulk' => [ + 'id' => $extension_base, + 'name' => $extension_base, + 'class' => 'extension-name-checkbox directorist-te-check', + ], + ] + ); + } +} + +if ( $is_logged_in && ! empty( $args['current_active_theme_info'] ) && is_array( $args['current_active_theme_info'] ) ) { + $active_theme = $args['current_active_theme_info']; + $theme_key = ! empty( $active_theme['stylesheet'] ) ? $active_theme['stylesheet'] : sanitize_title( $active_theme['name'] ?? '' ); + $product = $themes[ $theme_key ] ?? []; + $has_update = ! empty( $active_theme['has_update'] ); + + $add_row( + [ + 'key' => 'theme-active-' . $theme_key, + 'type' => 'theme', + 'status' => $has_update ? 'update active installed' : 'active installed', + 'name' => $active_theme['name'] ?? __( 'Active Theme', 'directorist' ), + 'version' => $active_theme['version'] ?? '', + 'description' => $product['description'] ?? __( 'Currently active WordPress theme for this site.', 'directorist' ), + 'image' => ! empty( $active_theme['thumbnail'] ) ? $active_theme['thumbnail'] : $get_image( $product ), + 'badges' => array_merge( $get_product_badges( $product ), [ __( 'Active theme', 'directorist' ) ] ), + 'statusLabel' => $has_update ? __( 'Update available', 'directorist' ) : __( 'Active', 'directorist' ), + 'primary' => $has_update + ? [ + 'label' => $is_beta ? __( 'Update Beta', 'directorist' ) : __( 'Update', 'directorist' ), + 'class' => 'directorist-te-btn directorist-te-btn--primary theme-update-btn', + 'attrs' => [ 'data-target' => $active_theme['stylesheet'] ?? '' ], + 'icon' => 'la la-refresh', + ] + : [ + 'label' => __( 'Customize', 'directorist' ), + 'href' => $active_theme['customizer_link'] ?? '#', + 'class' => 'directorist-te-btn directorist-te-btn--secondary', + 'external' => true, + 'icon' => 'la la-sliders', + ], + 'menu' => [ + [ + 'label' => __( 'Customize', 'directorist' ), + 'href' => $active_theme['customizer_link'] ?? '#', + 'class' => 'directorist-te-menu-link', + 'external' => true, + ], + ], + ] + ); +} + +if ( $is_logged_in && ! empty( $args['extensions_available_in_subscriptions'] ) && is_array( $args['extensions_available_in_subscriptions'] ) ) { + foreach ( $args['extensions_available_in_subscriptions'] as $extension_key => $extension ) { + $product = array_merge( $get_extension_product( $extension_key ), is_array( $extension ) ? $extension : [] ); + $name = $product['title'] ?? $product['name'] ?? $extension_key; + $description = $product['description'] ?? ''; + + $add_row( + [ + 'key' => 'extension-subscription-' . $extension_key, + 'type' => 'extension', + 'status' => 'not-installed', + 'name' => $name, + 'description' => $description, + 'image' => $get_image( $product ), + 'badges' => $get_product_badges( $product ), + 'statusLabel' => __( 'Not installed', 'directorist' ), + 'primary' => [ + 'label' => $is_beta ? __( 'Install Beta', 'directorist' ) : __( 'Install', 'directorist' ), + 'class' => 'directorist-te-btn directorist-te-btn--primary file-install-btn', + 'attrs' => [ + 'data-type' => 'plugin', + 'data-key' => $extension_key, + ], + 'icon' => 'la la-download', + ], + 'details' => $is_purchased ? $get_details_action( $product ) : null, + 'menu' => [], + ] + ); + } +} + +if ( $is_logged_in && ! empty( $args['required_extensions_list'] ) && is_array( $args['required_extensions_list'] ) ) { + foreach ( $args['required_extensions_list'] as $extension_key => $required_extension ) { + $required_base = ! empty( $required_extension['base'] ) ? $required_extension['base'] : "{$extension_key}/{$extension_key}.php"; + + if ( isset( $seen_rows[ 'extension-installed-' . $required_base ] ) || ! empty( $required_extension['installed'] ) ) { + continue; + } + + $product = $get_extension_product( $extension_key ); + $name = $product['name'] ?? $extension_key; + $description = $product['description'] ?? ''; + $is_purchased = ! empty( $required_extension['purchased'] ); + + $add_row( + [ + 'key' => 'extension-required-' . $extension_key, + 'type' => 'extension', + 'status' => $is_purchased ? 'required not-installed' : 'marketplace required not-installed', + 'name' => $name, + 'description' => $description, + 'image' => $get_image( $product ), + 'badges' => array_merge( $get_product_badges( $product ), [ __( 'Required', 'directorist' ) ] ), + 'statusLabel' => $is_purchased ? __( 'Required', 'directorist' ) : __( 'Required purchase', 'directorist' ), + 'primary' => $is_purchased + ? [ + 'label' => $is_beta ? __( 'Install Beta', 'directorist' ) : __( 'Install', 'directorist' ), + 'class' => 'directorist-te-btn directorist-te-btn--primary file-install-btn', + 'attrs' => [ + 'data-type' => 'plugin', + 'data-key' => $extension_key, + ], + 'icon' => 'la la-download', + ] + : [ + 'label' => __( 'Get It Now', 'directorist' ), + 'href' => $get_link( $product ), + 'class' => 'directorist-te-btn directorist-te-btn--secondary', + 'external' => true, + ], + 'details' => $is_purchased ? $get_details_action( $product ) : null, + 'menu' => [], + ] + ); + } +} + +if ( $is_logged_in && ! empty( $args['themes_available_in_subscriptions'] ) && is_array( $args['themes_available_in_subscriptions'] ) ) { + foreach ( $args['themes_available_in_subscriptions'] as $theme_key => $theme ) { + $product = array_merge( $themes[ $theme_key ] ?? [], is_array( $theme ) ? $theme : [] ); + $is_installed = ! empty( $product['is_installed'] ); + + $add_row( + [ + 'key' => 'theme-subscription-' . $theme_key, + 'type' => 'theme', + 'status' => $is_installed ? 'installed' : 'not-installed', + 'name' => $product['name'] ?? $theme_key, + 'version' => $product['version'] ?? '', + 'description' => $product['description'] ?? '', + 'image' => $get_image( $product ), + 'badges' => $get_product_badges( $product ), + 'statusLabel' => $is_installed ? __( 'Installed', 'directorist' ) : __( 'Not installed', 'directorist' ), + 'primary' => $is_installed + ? [ + 'label' => __( 'Activate', 'directorist' ), + 'class' => 'directorist-te-btn directorist-te-btn--primary theme-activate-btn', + 'attrs' => [ 'data-target' => $product['stylesheet'] ?? $theme_key ], + 'icon' => 'la la-check', + ] + : [ + 'label' => $is_beta ? __( 'Install Beta', 'directorist' ) : __( 'Install', 'directorist' ), + 'class' => 'directorist-te-btn directorist-te-btn--primary file-install-btn', + 'attrs' => [ + 'data-type' => 'theme', + 'data-key' => $theme_key, + ], + 'icon' => 'la la-download', + ], + 'demo' => $get_demo_action( $product ), + 'details' => $get_details_action( $product ), + 'menu' => array_filter( + [ + ! empty( $product['customizer_link'] ) + ? [ + 'label' => __( 'Live Preview', 'directorist' ), + 'href' => $product['customizer_link'], + 'class' => 'directorist-te-menu-link', + 'external' => true, + ] + : null, + ] + ), + ] + ); + } +} + +$promo_extensions = ! empty( $args['extensions_promo_list'] ) && is_array( $args['extensions_promo_list'] ) ? $args['extensions_promo_list'] : []; +if ( ! $is_logged_in && empty( $promo_extensions ) && ! empty( $extensions ) ) { + $promo_extensions = $extensions; +} + +foreach ( $promo_extensions as $extension_key => $extension ) { + $product = is_array( $extension ) ? $extension : []; + $add_row( + [ + 'key' => 'extension-promo-' . $extension_key, + 'type' => 'extension', + 'status' => 'marketplace not-installed', + 'name' => $product['name'] ?? $extension_key, + 'description' => $product['description'] ?? '', + 'image' => $get_image( $product ), + 'badges' => $get_product_badges( $product ), + 'statusLabel' => __( 'Marketplace', 'directorist' ), + 'primary' => $get_details_action( $product ), + 'details' => null, + 'menu' => [], + ] + ); +} + +$promo_themes = ! empty( $args['themes_promo_list'] ) && is_array( $args['themes_promo_list'] ) ? $args['themes_promo_list'] : []; +if ( ! $is_logged_in && empty( $promo_themes ) && ! empty( $themes ) ) { + $promo_themes = $themes; +} + +foreach ( $promo_themes as $theme_key => $theme ) { + $product = is_array( $theme ) ? $theme : []; + $add_row( + [ + 'key' => 'theme-promo-' . $theme_key, + 'type' => 'theme', + 'status' => 'marketplace not-installed', + 'name' => $product['name'] ?? $theme_key, + 'description' => $product['description'] ?? '', + 'image' => $get_image( $product ), + 'badges' => $get_product_badges( $product ), + 'statusLabel' => __( 'Marketplace', 'directorist' ), + 'primary' => $get_details_action( $product ), + 'demo' => $get_demo_action( $product ), + 'details' => null, + 'menu' => [], + ] + ); +} + +$total_updates = (int) ( $args['total_outdated_extensions'] ?? 0 ) + (int) ( $args['total_outdated_themes'] ?? 0 ); +$total_rows = count( $rows ); +$extension_rows = count( + array_filter( + $rows, + static function( $row ) { + return ! empty( $row['type'] ) && 'extension' === $row['type']; + } + ) +); +$theme_rows = count( + array_filter( + $rows, + static function( $row ) { + return ! empty( $row['type'] ) && 'theme' === $row['type']; + } + ) +); +$update_rows = count( + array_filter( + $rows, + static function( $row ) { + return ! empty( $row['status'] ) && false !== strpos( $row['status'], 'update' ); + } + ) +); +?> + +
+
+
+
+
+ + +
+ + +
+ + + +
+ + + +
+ +
+
+

+

+
+
+ + + + + + + + 0 ) : ?> +
+ +
+

+ +

+

+
+ +
+ + +
+
+
+ + + +
+
+ + +
+
+ +
+
+ + + + +
+
+ +
+ +
+
+ + + + + + + +
+
+ + + + + +
+ +
+ +
+ + + +
+ + +
+ +
+ +
+
+

+ + + + + + +
+ +
+ + + +
+ + +

+ +
+ +
+ +
+ +
+ + + + + + + + +
+ +
+ + + +
+
+ +
+
+ +
+ +

+

+ +
+
+ +
+

+

+
+ +
+
+ +
+
+

+

+
+ + + + +
- ATBDP()->load_template( 'admin-templates/theme-extensions/all-themes-extensions', $args ); - ?> +
+ + + + +
+
-
\ No newline at end of file +
From 5dcbddaeabbf59389f5352ac57cde7d1e92b86d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rabbi=20Islam=20Rony=20=E2=9A=A1=EF=B8=8F?= <35329385+RabbiIslamRony@users.noreply.github.com> Date: Wed, 22 Jul 2026 17:09:40 +0600 Subject: [PATCH 4/5] improve: redesign themes extensions admin page Redesign the Themes & Extensions admin surface with the new connected and disconnected page shell while preserving the existing page slug, AJAX action names, product filters, and subscription/user-meta contracts. Add page-scoped CSS/JS for dashboard/add-ons view switching, account dropdown behavior, searchable/highlighted product rows, compact badges, responsive bulk actions, and safer connected/disconnected product actions. Add local fallback entries and images for Directorist Notifications Pro and Directorist Divi Integration, plus badge normalization support for scalar and structured API badge data. Document the architecture, API, licensing, badge, safety, and current implementation decisions in the repo-local Themes & Extensions skill references. Generated assets/build files are intentionally excluded from this commit. --- assets/css/directorist-themes-extensions.css | 1209 +++++++++++++++-- .../directorist-divi-integration.jpg | Bin 0 -> 7688 bytes .../directorist-notifications-pro.jpg | Bin 0 -> 8925 bytes assets/js/directorist-themes-extensions.js | 706 +++++++++- .../SKILL.md | 3 + .../references/account-license-journey-map.md | 2 + .../references/api-data-flow-report.md | 51 +- .../current-implementation-change-notes.md | 127 ++ .../references/dynamic-data-contract.md | 10 +- .../references/qa-checklist.md | 2 + .../references/rewrite-issue-register.md | 72 +- includes/classes/class-extension.php | 111 +- .../auth/license-auth-section.php | 4 +- .../theme-extensions/theme-extension.php | 572 ++++++-- 14 files changed, 2617 insertions(+), 252 deletions(-) create mode 100644 assets/images/extensions/directorist-divi-integration.jpg create mode 100644 assets/images/extensions/directorist-notifications-pro.jpg create mode 100644 docs/agents/directorist-themes-extensions-page/references/current-implementation-change-notes.md diff --git a/assets/css/directorist-themes-extensions.css b/assets/css/directorist-themes-extensions.css index 3c4835c371..9b69fc261c 100644 --- a/assets/css/directorist-themes-extensions.css +++ b/assets/css/directorist-themes-extensions.css @@ -13,14 +13,19 @@ --bg: #f3f4f6; --bg-soft: #f8fafc; --success: #16a34a; + --success-tint: #eafaf0; --warning: #d97706; --warning-tint: #fff7ed; --danger: #dc2626; --danger-tint: #fef2f2; + --info: #2c99ff; + --info-tint: #e9f4ff; --r-xs: 6px; --r-sm: 8px; --r-md: 10px; - --shadow: 0 1px 2px rgba(15, 23, 42, 0.05); + --shadow-card: 0 1px 2px rgba(16, 24, 40, 0.04), 0 1px 3px rgba(16, 24, 40, 0.04); + --shadow-hover: 0 12px 32px rgba(16, 24, 40, 0.1), 0 2px 4px rgba(16, 24, 40, 0.06); + --shadow: var(--shadow-card); --shadow-pop: 0 10px 26px rgba(15, 23, 42, 0.1); color: var(--text); background: var(--bg); @@ -69,6 +74,12 @@ border-bottom: 1px solid var(--border); } +.directorist-te-page--connected .directorist-te-header { + position: static; + top: auto; + z-index: auto; +} + .directorist-te-brand, .directorist-te-nav, .directorist-te-resource-links, @@ -76,14 +87,15 @@ .directorist-te-top-search, .directorist-te-header-icon, .directorist-te-avatar, +.directorist-te-account-menu__item, .directorist-te-hero, .directorist-te-search, .directorist-te-update-banner, .directorist-te-toolbar-row, .directorist-te-segmented, .directorist-te-bulkbar, +.directorist-te-refresh-form, .directorist-te-row__title, -.directorist-te-row__meta, .directorist-te-row__actions, .directorist-te-upsell, .directorist-te-footer { @@ -124,7 +136,8 @@ flex: 1; } -.directorist-te-nav a { +.directorist-te-nav a, +.directorist-te-nav button { padding: 8px 10px; border: 0; border-radius: var(--r-xs); @@ -132,14 +145,19 @@ color: var(--muted); font-size: 13px; font-weight: 500; + font-family: inherit; line-height: 1; text-decoration: none; white-space: nowrap; + cursor: pointer; } .directorist-te-nav a:hover, .directorist-te-nav a:focus, -.directorist-te-nav a.active { +.directorist-te-nav a.active, +.directorist-te-nav button:hover, +.directorist-te-nav button:focus, +.directorist-te-nav button.active { color: var(--accent); background: var(--accent-tint); box-shadow: none; @@ -167,6 +185,11 @@ outline: none; } +.directorist-te-page--connected .directorist-te-top-right .directorist-te-resource-links { + gap: 18px; + margin-left: 0; +} + .directorist-te-top-right { gap: 10px; flex-shrink: 0; @@ -233,11 +256,13 @@ .directorist-te-avatar { height: 34px; gap: 6px; + margin: 0; padding: 0 7px 0 3px; border: 1px solid var(--border); border-radius: 999px; background: var(--surface); color: var(--muted); + cursor: pointer; } .directorist-te-avatar img { @@ -246,6 +271,107 @@ border-radius: 50%; } +.directorist-te-avatar:hover, +.directorist-te-avatar:focus { + border-color: var(--accent); + color: var(--accent); + outline: none; +} + +.directorist-te-avatar[aria-expanded="true"] { + border-color: var(--accent); + box-shadow: 0 0 0 2px rgba(62, 98, 245, 0.1); +} + +.directorist-te-account-menu { + position: relative; + flex-shrink: 0; +} + +.directorist-te-account-dropdown { + position: absolute; + top: calc(100% + 8px); + right: 0; + z-index: 80; + width: 326px; + padding: 10px; + border: 1px solid var(--border); + border-radius: var(--r-md); + background: var(--surface); + box-shadow: 0 16px 36px rgba(15, 23, 42, 0.12); +} + +.directorist-te-account-summary { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; + padding: 2px 2px 10px; + border-bottom: 1px solid var(--border); +} + +.directorist-te-account-summary strong, +.directorist-te-account-summary span { + display: block; +} + +.directorist-te-account-summary strong { + color: var(--text); + font-size: 16px; + font-weight: 700; + line-height: 1; +} + +.directorist-te-account-summary span { + margin-top: 4px; + color: var(--muted); + font-size: 10.5px; + font-weight: 500; + line-height: 1.25; +} + +.directorist-te-account-dropdown__actions { + display: grid; + gap: 6px; + padding-top: 8px; +} + +.directorist-te-account-dropdown__item { + width: 100%; + min-height: 34px; + justify-content: flex-start; + gap: 8px; + margin: 0; + padding: 8px 10px; + border: 0; + border-radius: var(--r-sm); + background: transparent; + color: var(--text); + box-shadow: none; + font-size: 13px; + font-weight: 500; + line-height: 1; + text-align: left; + text-decoration: none; + cursor: pointer; +} + +.directorist-te-account-dropdown__item:hover, +.directorist-te-account-dropdown__item:focus { + background: var(--accent-tint); + color: var(--accent); + outline: none; +} + +.directorist-te-account-dropdown__item--danger { + color: var(--danger); +} + +.directorist-te-account-dropdown__item--danger:hover, +.directorist-te-account-dropdown__item--danger:focus { + background: var(--danger-tint); + color: var(--danger); +} + .directorist-te-hero { justify-content: space-between; align-items: flex-end; @@ -282,8 +408,7 @@ background: var(--bg-soft); } -.directorist-te-connect, -.directorist-te-account { +.directorist-te-connect { margin: 0 0 16px; padding: 16px 18px; border: 1px solid var(--border-card); @@ -502,98 +627,753 @@ color: var(--text); } -.directorist-te-btn--secondary:hover, -.directorist-te-btn--secondary:focus, -.directorist-te-btn--ghost:hover, -.directorist-te-btn--ghost:focus, -.directorist-te-bulk-action:hover, -.directorist-te-bulk-action:focus { - border-color: var(--accent); - background: var(--accent-tint); +.directorist-te-btn--soft { + border-color: transparent; + background: var(--accent-tint); + color: var(--accent); +} + +.directorist-te-btn--secondary:hover, +.directorist-te-btn--secondary:focus, +.directorist-te-btn--ghost:hover, +.directorist-te-btn--ghost:focus, +.directorist-te-btn--soft:hover, +.directorist-te-btn--soft:focus, +.directorist-te-bulk-action:hover, +.directorist-te-bulk-action:focus { + border-color: var(--accent); + background: var(--accent-tint); + color: var(--accent); + outline: none; +} + +.ext-update-btn, +.theme-update-btn, +.directorist-te-update-all { + border-color: var(--warning); + background: var(--warning); + color: #fff; +} + +.ext-update-btn:hover, +.ext-update-btn:focus, +.theme-update-btn:hover, +.theme-update-btn:focus, +.directorist-te-update-all:hover, +.directorist-te-update-all:focus { + border-color: #b45309; + background: #b45309; + color: #fff; +} + +.directorist-te-icon-btn { + width: 28px; + height: 28px; + padding: 0; + border-color: transparent; + border-radius: var(--r-xs); + background: transparent; + color: var(--faint); + font-size: 17px; +} + +.directorist-te-icon-btn:hover, +.directorist-te-icon-btn:focus, +.directorist-te-menu .ext-action-drop.active { + background: var(--line); + color: var(--text); + outline: none; +} + +.directorist-te-icon-btn--danger:hover, +.directorist-te-icon-btn--danger:focus, +.directorist-te-menu-link--danger, +.directorist-te-bulk-action--danger { + color: var(--danger); +} + +.directorist-te-refresh-panel { + overflow: hidden; +} + +.directorist-te-refresh-panel[hidden] { + width: 0 !important; +} + +.directorist-te-refresh-form { + width: 100%; + gap: 8px; +} + +.directorist-te-view:not([hidden]) { + animation: directoristTeFade 0.2s ease; +} + +@keyframes directoristTeFade { + from { + opacity: 0; + transform: translateY(5px); + } + + to { + opacity: 1; + transform: none; + } +} + +.directorist-te-dashboard { + margin-top: 26px; +} + +.directorist-te-dashboard-welcome, +.directorist-te-dashboard-nudge, +.directorist-te-dashboard-card, +.directorist-te-dashboard-recommendations { + border: 1px solid var(--border-card); + border-radius: var(--r-md); + background: var(--surface); + box-shadow: var(--shadow); +} + +.directorist-te-dashboard-welcome { + position: relative; + display: flex; + align-items: center; + gap: 18px; + overflow: hidden; + padding: 22px 24px; +} + +.directorist-te-dashboard-welcome::before { + position: absolute; + inset: 0; + background: radial-gradient(820px 200px at 90% -40%, rgba(62, 98, 245, 0.08), transparent 70%); + content: ""; + pointer-events: none; +} + +.directorist-te-dashboard-welcome > * { + position: relative; + z-index: 1; +} + +.directorist-te-dashboard-welcome h1 { + margin: 0 0 3px; + color: var(--text); + font-size: 20px; + font-weight: 700; + line-height: 1.25; +} + +.directorist-te-dashboard-welcome p, +.directorist-te-dashboard-nudge p, +.directorist-te-dashboard-recommendations > p { + margin: 0; + color: var(--muted); + font-size: 13.5px; + line-height: 1.5; +} + +.directorist-te-dashboard-welcome__actions { + display: flex; + gap: 10px; + margin-left: auto; +} + +.directorist-te-dashboard-nudge { + margin-top: 18px; + padding: 16px 18px; + background: linear-gradient(180deg, var(--surface), var(--bg-soft)); +} + +.directorist-te-dashboard-nudge__top { + display: flex; + align-items: center; + gap: 13px; + margin-bottom: 13px; +} + +.directorist-te-dashboard-ring { + position: relative; + width: 46px; + height: 46px; + flex: 0 0 46px; + color: var(--accent); +} + +.directorist-te-dashboard-ring svg { + display: block; + transform: rotate(-90deg); +} + +.directorist-te-dashboard-ring span { + position: absolute; + inset: 0; + display: grid; + place-items: center; + color: var(--text); + font-size: 12px; + font-weight: 700; +} + +.directorist-te-dashboard-nudge h2, +.directorist-te-dashboard-card__head h2, +.directorist-te-dashboard-recommendations__head h2 { + margin: 0; + color: var(--text); + font-size: 14px; + font-weight: 600; + line-height: 1.35; +} + +.directorist-te-dashboard-nudge__dismiss { + width: 28px; + height: 28px; + display: grid; + place-items: center; + margin: 0 0 0 auto; + padding: 0; + border: 0; + border-radius: var(--r-xs); + background: transparent; + color: var(--faint); + cursor: pointer; +} + +.directorist-te-dashboard-nudge__dismiss:hover, +.directorist-te-dashboard-nudge__dismiss:focus { + background: var(--surface); + color: var(--muted); + outline: none; +} + +.directorist-te-dashboard-steps { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); + gap: 9px; +} + +.directorist-te-dashboard-step { + display: flex; + align-items: center; + gap: 10px; + min-height: 42px; + margin: 0; + padding: 10px 12px; + border: 1px solid var(--border); + border-radius: var(--r-xs); + background: var(--surface); + color: var(--text); + font-size: 12.5px; + font-weight: 500; + line-height: 1.35; + text-align: left; + cursor: pointer; +} + +.directorist-te-dashboard-step:hover, +.directorist-te-dashboard-step:focus { + border-color: var(--accent); + box-shadow: var(--shadow); + outline: none; +} + +.directorist-te-dashboard-step > span { + width: 20px; + height: 20px; + display: grid; + place-items: center; + flex: 0 0 20px; + border: 2px solid var(--border); + border-radius: 50%; + color: transparent; + font-size: 12px; +} + +.directorist-te-dashboard-step.is-done { + color: var(--faint); + opacity: 0.75; + text-decoration: line-through; +} + +.directorist-te-dashboard-step.is-done > span { + border-color: var(--success); + background: var(--success); + color: #fff; +} + +.directorist-te-dashboard-step > .la-angle-right { + margin-left: auto; + color: var(--faint); +} + +.directorist-te-dashboard-metrics { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 14px; + margin-top: 18px; +} + +.directorist-te-dashboard-metric { + position: relative; + overflow: hidden; + padding: 17px; + border: 1px solid var(--border-card); + border-radius: var(--r-md); + background: var(--surface); + box-shadow: var(--shadow); + transition: box-shadow 0.18s ease, transform 0.18s ease; +} + +.directorist-te-dashboard-metric:hover { + box-shadow: var(--shadow-hover); + transform: translateY(-2px); +} + +.directorist-te-dashboard-metric--attention { + border-color: #f5d9a8; + background: linear-gradient(180deg, #fff, #fffaf2); +} + +.directorist-te-dashboard-metric__top { + display: flex; + align-items: center; + gap: 11px; + margin-bottom: 13px; +} + +.directorist-te-dashboard-icon { + width: 38px; + height: 38px; + display: grid; + place-items: center; + flex: 0 0 38px; + border-radius: 9px; + color: #fff; + font-size: 19px; +} + +.directorist-te-dashboard-icon--blue { + background: linear-gradient(135deg, #4f6ef7, #7d93ff); +} + +.directorist-te-dashboard-icon--teal { + background: linear-gradient(135deg, #0ea5b7, #34c7d6); +} + +.directorist-te-dashboard-icon--amber { + background: linear-gradient(135deg, #f08a24, #ffb15c); +} + +.directorist-te-dashboard-icon--violet { + background: linear-gradient(135deg, #7c5cff, #a98bff); +} + +.directorist-te-dashboard-metric strong { + display: block; + color: var(--text); + font-size: 24px; + font-weight: 700; + letter-spacing: 0; + line-height: 1.1; +} + +.directorist-te-dashboard-metric__top div > span { + color: var(--muted); + font-size: 12.5px; + font-weight: 500; +} + +.directorist-te-dashboard-metric__foot { + display: flex; + align-items: center; + gap: 8px; + margin-top: 11px; + padding-top: 11px; + border-top: 1px solid var(--border-soft); + color: var(--faint); + font-size: 11.5px; +} + +.directorist-te-dashboard-metric__foot > span { + display: inline-flex; + align-items: center; + gap: 3px; + padding: 2px 7px; + border-radius: 999px; + background: #eafaf0; + color: var(--success); + font-size: 11.5px; + font-weight: 600; +} + +.directorist-te-dashboard-metric__foot a { + margin-left: auto; + color: var(--warning); + font-size: 12px; + font-weight: 600; + text-decoration: none; +} + +.directorist-te-dashboard-metric-spark { + position: absolute; + right: 0; + bottom: 0; + width: 96px; + height: 42px; + opacity: 0.5; + pointer-events: none; +} + +.directorist-te-dashboard-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr); + gap: 18px; + margin-top: 18px; +} + +.directorist-te-dashboard-card { + overflow: hidden; + display: flex; + flex-direction: column; +} + +.directorist-te-dashboard-card__head { + display: flex; + align-items: center; + gap: 9px; + padding: 15px 18px; + border-bottom: 1px solid var(--border-soft); +} + +.directorist-te-dashboard-card__head > i, +.directorist-te-dashboard-recommendations__head > i { + color: var(--accent); + font-size: 18px; +} + +.directorist-te-dashboard-card__head a { + margin-left: auto; + color: var(--accent); + font-size: 12.5px; + font-weight: 600; + text-decoration: none; +} + +.directorist-te-dashboard-actions { + display: flex; + flex: 1; + flex-direction: column; + justify-content: space-evenly; + gap: 2px; + padding: 7px; +} + +.directorist-te-dashboard-action { + display: grid; + grid-template-columns: 36px minmax(0, 1fr) 16px; + grid-template-areas: + "icon title arrow" + "icon meta arrow"; + align-items: center; + gap: 0 12px; + padding: 10px 11px; + border-radius: var(--r-xs); + text-decoration: none; +} + +.directorist-te-dashboard-action:hover, +.directorist-te-dashboard-action:focus { + background: var(--bg-soft); + outline: none; +} + +.directorist-te-dashboard-action > span { + grid-area: icon; + width: 36px; + height: 36px; + display: grid; + place-items: center; + border-radius: var(--r-sm); + background: var(--accent-tint); + color: var(--accent); + font-size: 17px; +} + +.directorist-te-dashboard-action:nth-child(2) > span { + background: #eafaf0; + color: var(--success); +} + +.directorist-te-dashboard-action:nth-child(3) > span { + background: #fef3e6; + color: var(--warning); +} + +.directorist-te-dashboard-action:nth-child(4) > span { + background: #f3eeff; + color: #7c5cff; +} + +.directorist-te-dashboard-action:nth-child(5) > span { + background: #e9f4ff; + color: #2c99ff; +} + +.directorist-te-dashboard-action strong { + grid-area: title; + color: var(--text); + font-size: 13px; + font-weight: 600; + line-height: 1.3; +} + +.directorist-te-dashboard-action em { + grid-area: meta; + overflow: hidden; + color: var(--muted); + font-size: 12px; + font-style: normal; + line-height: 1.35; + text-overflow: ellipsis; + white-space: nowrap; +} + +.directorist-te-dashboard-action > .la-angle-right { + grid-area: arrow; + color: var(--faint); + font-size: 16px; +} + +.directorist-te-dashboard-action:hover > .la-angle-right, +.directorist-te-dashboard-action:focus > .la-angle-right { + color: var(--accent); + transform: translateX(2px); +} + +.directorist-te-dashboard-activity { + display: flex; + flex: 1; + flex-direction: column; + justify-content: space-evenly; + gap: 0; + padding: 7px; +} + +.directorist-te-dashboard-activity > div { + display: grid; + grid-template-columns: 33px minmax(0, 1fr) auto; + gap: 12px; + align-items: flex-start; + padding: 11px; + border-radius: var(--r-xs); +} + +.directorist-te-dashboard-activity > div + div { + border-top: 1px solid var(--border-soft); +} + +.directorist-te-dashboard-activity > div:hover { + background: var(--bg-soft); +} + +.directorist-te-dashboard-activity p { + min-width: 0; + margin: 0; + color: var(--muted); + font-size: 12.5px; + line-height: 1.35; +} + +.directorist-te-dashboard-activity strong, +.directorist-te-dashboard-activity small { + display: block; +} + +.directorist-te-dashboard-activity strong { + margin-bottom: 1px; + color: var(--text); + font-size: 13px; + font-weight: 600; +} + +.directorist-te-dashboard-activity b { + color: var(--text); + font-weight: 600; +} + +.directorist-te-dashboard-activity strong span { + display: inline-flex; + margin-left: 6px; + padding: 1px 6px; + border-radius: 5px; + background: var(--info-tint); + color: var(--info); + font-size: 10px; + font-weight: 600; + vertical-align: middle; +} + +.directorist-te-dashboard-activity small { + margin-top: 2px; + color: var(--faint); + font-size: 11.5px; +} + +.directorist-te-dashboard-activity-icon { + width: 33px; + height: 33px; + display: grid; + place-items: center; + flex: 0 0 33px; + border-radius: 8px; + font-size: 16px; +} + +.directorist-te-dashboard-activity-icon--blue { + background: var(--accent-tint); + color: var(--accent); +} + +.directorist-te-dashboard-activity-icon--green { + background: var(--success-tint); + color: var(--success); +} + +.directorist-te-dashboard-activity-icon--amber { + background: var(--warning-tint); + color: var(--warning); +} + +.directorist-te-dashboard-activity-icon--violet { + background: #f3eeff; + color: #7c5cff; +} + +.directorist-te-dashboard-activity-icon--info { + background: var(--info-tint); + color: var(--info); +} + +.directorist-te-dashboard-activity .directorist-te-btn { + align-self: center; + min-height: 32px; + padding: 6px 12px; + font-size: 12.5px; +} + +.directorist-te-dashboard-recommendations { + margin-top: 18px; + padding: 18px 20px 8px; +} + +.directorist-te-dashboard-recommendations__head { + display: flex; + align-items: center; + gap: 9px; + margin-bottom: 2px; +} + +.directorist-te-dashboard-recommendations__head span { + margin-left: auto; color: var(--accent); - outline: none; + font-size: 12px; + font-weight: 500; } -.ext-update-btn, -.theme-update-btn, -.directorist-te-update-all { - border-color: var(--warning); - background: var(--warning); - color: #fff; +.directorist-te-dashboard-recommendations > p { + max-width: 640px; + margin-bottom: 6px; } -.ext-update-btn:hover, -.ext-update-btn:focus, -.theme-update-btn:hover, -.theme-update-btn:focus, -.directorist-te-update-all:hover, -.directorist-te-update-all:focus { - border-color: #b45309; - background: #b45309; - color: #fff; +.directorist-te-dashboard-recommendations__grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); } -.directorist-te-icon-btn { - width: 28px; - height: 28px; - padding: 0; - border-color: transparent; - border-radius: var(--r-xs); - background: transparent; - color: var(--faint); - font-size: 17px; +.directorist-te-dashboard-recommendations__grid > div { + display: flex; + flex-direction: column; + padding: 16px 18px; + border-left: 1px solid var(--border-soft); } -.directorist-te-icon-btn:hover, -.directorist-te-icon-btn:focus, -.directorist-te-menu .ext-action-drop.active { - background: var(--line); - color: var(--text); - outline: none; +.directorist-te-dashboard-recommendations__grid > div:first-child { + border-left: 0; + padding-left: 2px; } -.directorist-te-icon-btn--danger:hover, -.directorist-te-icon-btn--danger:focus, -.directorist-te-menu-link--danger, -.directorist-te-bulk-action--danger { - color: var(--danger); +.directorist-te-dashboard-recommendation-top { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 7px; } -.directorist-te-account { +.directorist-te-dashboard-recommendation-icon { + width: 30px; + height: 30px; display: grid; - grid-template-columns: repeat(3, minmax(0, 160px)) minmax(320px, 1fr); - gap: 18px; - align-items: center; + place-items: center; + flex: 0 0 30px; + border-radius: var(--r-sm); + color: #fff; + font-size: 15px; } -.directorist-te-stat span { - display: block; +.directorist-te-dashboard-recommendation-icon--green { + background: var(--success); +} + +.directorist-te-dashboard-recommendation-icon--orange { + background: #f08a24; +} + +.directorist-te-dashboard-recommendation-icon--violet { + background: #7c5cff; +} + +.directorist-te-dashboard-recommendations__grid h3 { + margin: 0; color: var(--text); - font-size: 22px; - font-weight: 700; - line-height: 1; + font-size: 13px; + font-weight: 600; + line-height: 1.35; } -.directorist-te-stat p { - margin: 5px 0 0; +.directorist-te-dashboard-recommendations__grid p { + flex: 1; + margin: 0 0 12px; color: var(--muted); font-size: 12px; + line-height: 1.45; } -.directorist-te-account__actions { - justify-content: flex-end; - gap: 10px; - min-width: 0; +.directorist-te-dashboard-recommendations__grid .directorist-te-btn { + align-self: flex-start; + min-height: 32px; + padding: 8px 16px; + border-color: transparent; + background: var(--accent-tint); + color: var(--accent); } -.directorist-te-refresh-panel { - width: 0; - overflow: hidden; +.directorist-te-dashboard-footer { + display: flex; + justify-content: center; + gap: 7px; + margin: 22px 0 0; + padding-top: 22px; + border-top: 1px solid var(--border); + color: var(--faint); + font-size: 12px; + text-align: center; } -.directorist-te-refresh-form { - width: 330px; - gap: 8px; +.directorist-te-dashboard-footer a { + color: var(--accent); + font-weight: 600; + text-decoration: none; } .directorist-te-update-banner { @@ -703,6 +1483,20 @@ color: var(--accent); } +.directorist-te-segmented .directorist-te-update-count, +.directorist-te-segmented button.is-active .directorist-te-update-count { + min-width: 16px; + height: 16px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 1px 6px; + border-radius: 99px; + background: var(--warning); + color: #fff; + font-size: 10px; +} + .directorist-te-toolbar-row { justify-content: space-between; gap: 12px; @@ -802,12 +1596,47 @@ background: var(--border); } +.directorist-te-bulkbar__empty { + color: var(--muted); + font-size: 12.5px; +} + +.directorist-te-bulkbar__notice { + color: var(--muted); + font-size: 12px; +} + +.directorist-te-bulkbar__empty[hidden], +.directorist-te-bulkbar__notice[hidden] { + display: none !important; +} + .directorist-te-bulk-action { min-height: 31px; padding: 6px 10px; font-size: 12.5px; } +.directorist-te-bulk-action__count { + min-width: 16px; + height: 16px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 5px; + border-radius: 999px; + background: var(--accent-tint); + color: var(--accent); + font-size: 10px; + font-weight: 700; + line-height: 16px; +} + +.directorist-te-bulk-action--danger .directorist-te-bulk-action__count { + background: var(--danger-tint); + color: var(--danger); +} + .directorist-te-bulk-action--danger:hover, .directorist-te-bulk-action--danger:focus { border-color: var(--danger); @@ -875,6 +1704,10 @@ grid-template-columns: 40px minmax(0, 1fr) 150px 180px; } +.directorist-te-page--disconnected .directorist-te-row[data-product-type="theme"] { + grid-template-columns: 64px minmax(0, 1fr) 150px 180px; +} + .directorist-te-page--disconnected .directorist-te-list-head__addon { grid-column: 1 / 3; } @@ -938,6 +1771,17 @@ opacity: 0; } +.directorist-te-checkbox--disabled { + border-color: var(--border); + background: var(--bg-soft); + cursor: not-allowed; + opacity: 0.58; +} + +.directorist-te-checkbox--disabled input { + cursor: not-allowed; +} + .directorist-te-checkbox:has(input:checked), .directorist-te-checkbox.is-checked { border-color: var(--accent); @@ -966,6 +1810,11 @@ object-fit: cover; } +.directorist-te-page--disconnected .directorist-te-row[data-product-type="theme"] .directorist-te-row__media { + width: 64px; + height: 42px; +} + .directorist-te-row__content { min-width: 0; } @@ -1003,16 +1852,19 @@ white-space: nowrap; } -.directorist-te-row__meta { - gap: 6px; - flex-wrap: wrap; - margin-top: 5px; +.directorist-te-page .directorist-te-search-highlight { + padding: 0 1px; + border-radius: 3px; + color: inherit; + background: #fef3c7; + font: inherit; } .directorist-te-type, .directorist-te-badge { display: inline-flex; align-items: center; + flex: 0 0 auto; min-height: 18px; padding: 2px 7px; border-radius: 5px; @@ -1025,14 +1877,40 @@ white-space: nowrap; } -.directorist-te-type--theme { +.directorist-te-type--theme, +.directorist-te-type--directorist-theme { color: #166534; background: #dcfce7; } +.directorist-te-type--wordpress-theme { + color: #475569; + background: #f1f5f9; +} + .directorist-te-badge { - color: #7c2d12; - background: #ffedd5; + min-height: 15px; + padding: 1px 5px; + border-radius: 4px; + color: #64748b; + background: #f8fafc; + font-size: 8.5px; + font-weight: 700; +} + +.directorist-te-badge--new { + color: #2563eb; + background: #eff6ff; +} + +.directorist-te-badge--trending { + color: #7c3aed; + background: #f5f3ff; +} + +.directorist-te-badge--popular { + color: #059669; + background: #ecfdf5; } .directorist-te-row__status { @@ -1069,6 +1947,7 @@ .directorist-te-status--update { color: var(--warning); + font-weight: 600; } .directorist-te-status--update::before, @@ -1228,7 +2107,8 @@ } @media (max-width: 1120px) { - .directorist-te-top-right { + .directorist-te-top-search, + .directorist-te-header-icon { display: none; } @@ -1241,19 +2121,34 @@ .directorist-te-page--disconnected .directorist-te-row { grid-template-columns: 40px minmax(0, 1fr) 132px 180px; } + + .directorist-te-page--disconnected .directorist-te-row[data-product-type="theme"] { + grid-template-columns: 64px minmax(0, 1fr) 132px 180px; + } } -@media (max-width: 960px) { - .directorist-te-account { - grid-template-columns: repeat(3, minmax(0, 1fr)); +@media (max-width: 980px) { + .directorist-te-dashboard-metrics { + grid-template-columns: repeat(2, minmax(0, 1fr)); } - .directorist-te-account__actions { - grid-column: 1 / -1; - justify-content: flex-start; - flex-wrap: wrap; + .directorist-te-dashboard-grid, + .directorist-te-dashboard-recommendations__grid { + grid-template-columns: minmax(0, 1fr); + } + + .directorist-te-dashboard-recommendations__grid > div { + border-top: 1px solid var(--border-soft); + border-left: 0; + padding-left: 2px; + } + + .directorist-te-dashboard-recommendations__grid > div:first-child { + border-top: 0; } +} +@media (max-width: 960px) { .directorist-te-connect { grid-template-columns: 1fr; } @@ -1270,6 +2165,11 @@ margin: 20px 0 36px; } + .directorist-te-page--disconnected .directorist-te-header { + position: static; + top: auto; + } + .directorist-te-header { height: auto; min-height: 62px; @@ -1279,6 +2179,14 @@ padding: 12px 16px; } + .directorist-te-page--connected .directorist-te-header { + align-items: flex-start; + } + + .directorist-te-account-dropdown { + width: min(326px, calc(100vw - 36px)); + } + .directorist-te-nav { order: 3; width: 100%; @@ -1286,6 +2194,36 @@ flex-wrap: nowrap; } + .directorist-te-page--connected .directorist-te-nav { + flex: 0 0 100%; + padding-top: 2px; + } + + .directorist-te-dashboard { + margin-top: 20px; + } + + .directorist-te-dashboard-welcome { + align-items: flex-start; + flex-direction: column; + } + + .directorist-te-dashboard-welcome__actions { + width: 100%; + margin-left: 0; + flex-wrap: wrap; + } + + .directorist-te-dashboard-recommendations__head { + align-items: flex-start; + flex-wrap: wrap; + } + + .directorist-te-dashboard-recommendations__head span { + flex: 0 0 100%; + margin-left: 0; + } + .directorist-te-resource-links { gap: 14px; margin-left: auto; @@ -1310,7 +2248,6 @@ .directorist-te-connect, .directorist-te-connect.account-connect, #directorist.atbd_wrapper.directorist-te-page .directorist-te-connect.account-connect, - .directorist-te-account, .directorist-te-update-banner, .directorist-te-toolbar, .directorist-te-upsell { @@ -1326,11 +2263,18 @@ } .directorist-te-toolbar-catalog { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; width: 100%; margin-left: 0; padding-bottom: 12px; - flex-wrap: wrap; - justify-content: space-between; + justify-content: stretch; + } + + .directorist-te-count { + align-self: center; + justify-self: end; + white-space: nowrap; } .directorist-te-products { @@ -1344,17 +2288,12 @@ flex-direction: column; } - .directorist-te-field-row, - .directorist-te-account { + .directorist-te-field-row { grid-template-columns: 1fr; } - .directorist-te-account__actions { - grid-column: auto; - } - .directorist-te-refresh-form { - width: min(330px, calc(100vw - 64px)); + width: 100%; } .directorist-te-bulkbar { @@ -1377,6 +2316,10 @@ grid-template-columns: 36px minmax(0, 1fr) auto; } + .directorist-te-page--disconnected .directorist-te-row[data-product-type="theme"] { + grid-template-columns: 64px minmax(0, 1fr) auto; + } + .directorist-te-list-head__status, .directorist-te-row__status { display: none; @@ -1397,6 +2340,49 @@ } @media (max-width: 520px) { + .directorist-te-nav { + gap: 2px; + } + + .directorist-te-nav a, + .directorist-te-nav button { + padding: 7px 6px; + font-size: 12px; + } + + .directorist-te-dashboard-metrics { + grid-template-columns: minmax(0, 1fr); + } + + .directorist-te-dashboard-welcome, + .directorist-te-dashboard-nudge, + .directorist-te-dashboard-recommendations { + padding-right: 14px; + padding-left: 14px; + } + + .directorist-te-dashboard-welcome__actions .directorist-te-btn { + width: 100%; + } + + .directorist-te-dashboard-nudge__top { + align-items: flex-start; + } + + .directorist-te-dashboard-activity > div { + grid-template-columns: 33px minmax(0, 1fr); + } + + .directorist-te-dashboard-activity .directorist-te-btn { + grid-column: 2; + justify-self: flex-start; + align-self: flex-start; + } + + .directorist-te-dashboard-action em { + white-space: normal; + } + .directorist-te-list-head, .directorist-te-row { grid-template-columns: 22px minmax(0, 1fr); @@ -1434,6 +2420,18 @@ grid-column: 2; } + .directorist-te-page--connected .directorist-te-row__title { + align-items: flex-start; + flex-wrap: wrap; + } + + .directorist-te-page--connected .directorist-te-row__title h2 { + flex: 0 0 100%; + overflow: visible; + text-overflow: clip; + white-space: normal; + } + .directorist-te-page--disconnected .directorist-te-row__content, .directorist-te-page--disconnected .directorist-te-row__actions { grid-column: 1; @@ -1445,11 +2443,20 @@ row-gap: 10px; } + .directorist-te-page--disconnected .directorist-te-row[data-product-type="theme"] { + grid-template-columns: 64px minmax(0, 1fr); + } + .directorist-te-page--disconnected .directorist-te-row__media { grid-column: 1; display: flex; } + .directorist-te-page--disconnected .directorist-te-row[data-product-type="theme"] .directorist-te-row__media { + width: 64px; + height: 42px; + } + .directorist-te-page--disconnected .directorist-te-row__content { grid-column: 2; } @@ -1471,6 +2478,16 @@ width: 100%; } + .directorist-te-page--disconnected .directorist-te-row[data-product-type="theme"] .directorist-te-row__actions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + width: 100%; + } + + .directorist-te-page--disconnected .directorist-te-row[data-product-type="theme"] .directorist-te-row__actions > .directorist-te-btn { + width: 100%; + } + .directorist-te-row__actions { justify-content: flex-start; flex-wrap: wrap; diff --git a/assets/images/extensions/directorist-divi-integration.jpg b/assets/images/extensions/directorist-divi-integration.jpg new file mode 100644 index 0000000000000000000000000000000000000000..201be7de95eb3ce33150a4cfe8778218518a031a GIT binary patch literal 7688 zcmb7ocQjnlxBs1C)aW(3=siS>F3|;1MjO2(I)f1R006q3z|A7?7{J5D zh2Y}iK_C!3JUo0tQbIxk0zyg>5+YI>N?KYPN@{9426iSode*zt)XY50toJy$xVUJU z_yl=51=u;cIR7~T-EJi$AfzB9q~N5drsw=WkDD%lf&dr;Cru;{YIBd;&bkE&7BU0O5dfa3Oev#9&#MNN}txcbZ2>hFv zG63f~14;dMt8^o_b;1uQ+A3+MsA~ihNJ-Xj8+Co}c$TPM&nP^b%s?=?`HjXs%+@lE zXcBx6#N>OBCf!uhWl55_=Xj?C(#T;m%?p;4+$RzWOJ*#E1eHa&#Y#LM1ZE3JR2X6< zha3PBV}>^fQ$hz2No#gos6a!~c%KJfTY<+m^RxpbBVR~I$jBUJ6Pb}Gw5!)ZIm6kd z%)l-*74El9Nj*g$E`%`F5mI`DvJo~uco4XdNYsDtt6ARS8=Ss-re_xF%ha!3Nl(SM6(15SmW8`8@%oxP2z8OGo35iP+!^W9_$nA_yG~+wW1p)SzW>Z^x;!pSp$T!7isK5qub+A&nXgo+%n8*0(ww_ff_o751YQ~a6>ZK zu}T-Sp>6{-aS|2{V8#jN__^*Uc+ibt1h@L-pXJ$Up&#T+O{W zi7FCkOK4dE37wo_RTXgo3G^Yr-fBrs5UK)BQX;1SVj;E_4*@0B)|$bp!jy>CGThI6 z4h?K>l?dkB+=#01H#ie$=-j7{L^p{?N*X@FO%9D*BTp{LB4h^<_Of}c-L7swFF2v2 zj%XaOhd-QIGl_sSzL#A$R(3*imrumj9b*O|gs&i&D+WJ?_V^fBbs76)bxYvB;F6)u zrIgw-?zc(6N2`Q?7RkFX^wd?XO5H57I#N?ef9b-WrJ8U2)6659_QD`_8-4irkK*q7 z1;(@lf3ks%8bt4D*N5bEYIQY_s%~(7R9#1y_{0?|!3cckrtZQDBW)*vpalXeYtKKqm5|Rya|ouSxbuXl2KkbKp+*-463h1 z7x|ueM)@j!JujaR;j;Kgd+H~%WbojQ?Z@4SD@5m--k}N>?LhbHllZH_)GHGX$csJg zO3CG6tM^L(Lc&*?PieBsAy!n@dN9Ikr<|Y{NgP|&8jt}P-zeg(^~2Rkj6i#o%1CLh zz;CMoMz0;ZX3M1U_WsIJS^f&X4F$XfnwG&Ijg@>3wiOt~x#H zXpo~b!A6GIzotK>q-{GL8_!Ie+}oj=^6MJanxUO z>Gj%JwJ(vFKj`pOj4V5UNA@)vQ>jLCAg(vmGj{!)d*|@3P30pREFxi}>_c4@F=c;| zV15zO74Ajrbikwfb8X&OX46wfOO5KHc>R5yU{kI8&vql!lh5X-QFI>+oF5DbMST#& zA?xL>sMy{rEDLg=Xw(p95DFn4wVUT)!pu3Kl84CG&XdlWPiKO3Qf21kO%>69F+K>_ zbXh*#$Tkz4?8wT?9PepryOEP<$!;Bf&zL{<9))-KXu<%heZ8I11WJ z`8zINP01R|agr06k-H3r#~r%_3hh`a4+(Q0Qx1z%d{dxJRQ7(;Q{eRzTWm7ct3K3F zKnqo6T$z52t`ZCqDCC@ddSnm%`$nYfB^~QXh=R4nc@3tLHC20?6*4eImXtcHAh6)A zefF~RS)1-;s7^@V-Qk7@(;h+$T=sAZrI;%k)p)u>Nfhp`y-98WZnP8hyIyBE(y zXPb2gL36(^_8F?*vPzDfb-u7R&MN!{c=~fLlB3DMf^PJT(#%LNSx?9*fLAPMUxPt!W%(?Bb{g6PscKnF`;Fj=VME?&1G-kEc}k=!rM!#C5lx zUOW1F11*9;QE6|Z z)6wN6wC$~4^1(4b&DAuP?<6IuWynW1jo#~}=oM-WzG&4IgFQ(4K{tQ8@31OVyW4y% zy40!`y%|rofyVgwh9nVSY;vX%pv{>Y?>W!^GYu#mZAZ0StP8DAzW-A(wt;f#>8x)BhB5=vyyk{i$I9~q>d+x$Uj37f!@Q`8 z4Ssiy)*<-RC5KsidQ+!u-@dd=$G7UA&)#9iFFr_^?RsK~S40XI1ogvzsrwDtEfHf_wFR#Vx<6IL97JK^r17g3AC6uV z|7~FHq(k&|e;z#guv~impvfNd*4HcG`L_$9wa(Y?<`%SLUDY3SagNtX0s%St&@7l9 zdzlZ6rd74S3$p4m^zoPb@EQgIV>B2f+5e^Sf-5ZU*$69P@&MFywjEBg1?&9Wx0zFD zdye2tqt_2e1LKySz{Xyn&i#eyLT4P`9=5xD7<*Rc8tIE!50HZCNT+_;DSlpTWb|jJ zy=aM2JD|69mThy(yzy7VVo+k6a;$;fbYgi?u1qW|W<&~N1I4U6vIX)yZTILcudwM6 zrORxYPRb&v?MjwW3C`_1sF$>2+qaoFgsZ|Vh7+s|WN7IqoAVhOOx@0BB95GcAFniD zSdVG$u+q#y|G#}?e|Qu)&=+K-8FG+0vb*4yZEbrl#C1oy1kuMJ_mw!Yn=&N0Mq>}$ zvt_>XV3WUnDhawaQ!9*ofdt8jwxeVLm?HIy3p&gJS(5PULz&lu>h~?auZ%a&%^-d^ zAD_|GYz)W>VAQui=XdI=x6aMNdVJKp9Z3a%vJyA4w0=!culZMW;OSh6Y{MNMs)`;L zvEF`jGfS$^_LF;(l3RWvvcCdY|AgHD>O8Slj}@+(242-Pk+w=eJ z;K_qC-5JdFq!B6#RXS4_7bde`>?`sT@NxH-l5qPjyty{MkkZB!{q1qsr|Z#6Xz~x= z9hC34Z{^ee%N?cjpIQ>1QEJP+t@fsMD<7IiW>QA&Q{dz2(XVnkY%8{&wE2BDIuDW3 z&bt3=S*)MqDASXS7x>gj9$aY(MswT32NcNWEFA^{U5KLVYvh%`JP#vUJU^yO^RU0j z?KvDHJ0B35G*HV;_#G3tB_|#7{}iY~QR79_}%MiKTeY4UhR+cUjb83D&X# z^5jgi)bxc3g&ES5uPI}VdvTMahmt&E1*?^UpX!GrncfYUdYWG=+d0MdGjfX;6Ox?N zGY=K&6&rU+R<1`C)BJvlqkhHpO1XKqs>A86OfMw{doy9*=d&^sbd{Y^Jk}B~E$XJyipmf@^n9T+ z!hk3Bq1xkz!;`vW#+)$QeP0`iqHb}Vn82gcg&gL4NOVAU(2|tBe2gyjMo7BTzFOTJ zw^gM(!uX+}r=t||zM5|CQzRkUlveqp%g>Eb3Z-u1yi%tZww&o7538)nhMNUvni!L( zralz(hYAOXlg^AZcS`6dt|6ooHP5$WGE=4>lI01`J}57<`m(Q0=<#5ONZcLT5a7N( z(=xX#I-T2pJ>Gfm@?rZjYq>&gsP)}oy!bxpTJUFRyr>nyg_->Z3(d8|ZQro_dcuMy zLqUJldiL>I_x|9y2=nk*$ot=4Ix-1a&7-=y@J0LlgG)_}B+a!zBFW*;GCgx4U5mR6 zwJ>!>WN^;UTz>nvugFG>LR_us(j3pSHnShbRIz9e!i*A$qYPYm1Ebn^#?z(cBwZ6m z!!{V?DxB8JC}%n+i87KMgm>WUvUH=vvWn6-Kx%Cj%G?EKBL1DPr4hdD5sSd$xZ8W& zl5(o)Q4_1#?2mj)#Q6Sz^_JB~b9ycc{QG2_Y8#DcJFHLTmvlO5yG|`!>hCjSFd%$RFaYnNX-%HxKa%HIL{emcnozeZ% z{$6SSfYLp91KG4EAa78#p8G>*L92Zrau_J0$KWdpxRD4^9zPW75B~ z5>>y46%I{dg6}Ahr8rRy=zM7?2V9H&yeOAZM5`wV#CC-auPG8Q`T7+aiaLgHQ)zvr5*SGHVi1j(gzpn$-IrO4{WxMa+~nSfW7bH#ZbeG9 z`Zpk7saEjkvXypXH^>ts%zBa(v|hOBGIz~=7B$)Ik#_%|`k)Gb5BOUO#e_utYmPp6 zzll;5%dR09Y%pUoe8?{MHB(JrM4PWqu<|L{ho z5t3?)9_FKFR;Bb6l}X{|c%H^R0-=KF9n!i0SPNXkNO`t5)PE7R;({V!9MKFCJ7E4` zytgaN{cXVm`|OKFSd4IkDj!t7%uR^eBt2F-naKGKW4X-K*OG#8~y{V4ESR=DT^qM;G;o zKZ-ait2sMy{{4ipD>M!Ic1Fl4y}R|UeYFcR-QC1qt8H%GhYQ@c^E;pzavR#?tU1C1 ztQ;9!Hw3@l!sw3^JpP@+#UJ@I;$RzN)k@>SUF5Gomwvdf}>pdD+pR>Kh6le-cmIF8?BYaisD6rVWfp@qro=G*fW zG#&r@Ya;JJ>hX-Qz@=oGWA2|IzL#@8iI-)U5>z&q#Ge}s4ToqvoNJQ4_Il4EI%CYF>P>?p(rHe_pwKbSCB;=V#=Awm`~CaDF$Cr= z0h52>df4X?4mOBj+5nfpQ!rFOrow<+=Y_$2gX})hk|A;SL_|wTBbJTDxju)+9XO@7 zDHbn*7%KVaR5`r$$+e;|pp9QJ`D6OHSy7wzngiqG=!X+;+eBJg)l-q5UeZ^aP%|Xd z+yE;_{bgf0nXaGNHt)&q*GE-1lpW{%xEqjYy+3vRC_lfF_0suz-V(K-Wcdr zV81^3708A&e1Y{nmgXFw-S+Xd3VQmfJizc>?yi?wg;BahBt8Fr6!fbiBn%6k@rL@^ z5VsaUDJ!;$Ad31P0y(E~9}?8@`dkF=UzqXV-+JeOC;6?<$9ocAxS*9O?d{!mf`+ZF z)+lZMY_FyF)MmPc5yKDZ^=Cp3q^5b>Cri%)I`b{9E{tDdcD2Mu|3u7nkeOR|pIkD? zn1l^?x*A>*hU7yx(^>1Xw`r#Li=U5r)`R}@lo*crbsd74Y$t^so4=paJ|POv$h!f4 z-Y&MOw3boJc4oz%qbz+A!_RMd-CI`pq5;L~yIdDhrbT7fk)Moe-ilYRPwXmhCayE? z>Q&XQUR=jawv+H6kAy-*tw~fZ$im~L@LR@cMe0}Z&tM&iSSQ-17JfJ7_7q{&ldJt7 zP4YutYJurYOGi`%W@?d#W)$QETI|yEf>Gjt8A(dIvB4AdF^5_?3&Xb@op94C?klP+ zjrGPMHnFz?t@{leX)O0XlSDm?QB^76kci%@7NC^MNKd7TXoj0@kTX%#vAJ-k8-fx+ z_klDb8~%@>_JWD!?kYspc?^r(xEX0(Mhx^el@Q^dVGT)ujd$I89yB}yaPsb0NpG(W z5Cle7Dg*G2DhmOYU(ogpi;4~LP_dQB7RY!eA15TUqGOyg&l{yg2m;|4siVLp$>2v| zid2V&o$@>&R0~Xz2cIxa2B0)fTBy5PBf3XHSLy7+k{rkk`cPh?2L)iGaWIs(AURel zfLMUB6bX-i`>FQ9r4q(N^x;xcXe-N3dKf$?qAHRP1I0tJbMhaFScqpOKB_ zX(SPK0~+Km!(sngryGKkk`R{7*puyRQ>C$F8bqrhWi%{JzoOq*)K~~H9*+^n((?)N zVxWyU=7u;(bA5bX8++x}x(uSWvK2gttssp}aVUKO0L2OdTI{X2N2X-t=OUR#g2&TU zD!4S+c=L@dBGV?pl##jol%eV3(T2kihhk)v27wkGjS3dT?+kJzZltS6n`i1{7DKc5 zm-h_FxlcPkKH5oQ+m2gYEI7lihbyhR?`(^IMItG5nL-H-Oefo}mUl7CL@lsbtV~C*Gv{dQ&=LE}H`;d#66}D|*sq zb%|DydXU8~rIuka)AXrp6C7ky=3in|r}_XTPgNNdubmXHztn)ku#rN%zlacIyQbR0 zPHMS&-L)OWXhF!m^Yd6H6KgbFQ<`R*S4$JnV=P(h+8WFY(g@k|l1ERd<2C-=Noo`(S*}AY&K4z8>+yTGK7;NZlJ-fxA5gL23H@F9B`qg2B>& zWg^~c)ZNUEg({XAYO`N#3+pymRcZ!$9YiX_&au8KUMSoCj4if;H$PPP)Zn^;Hv`DV zR-C2jgOF#9hCxlL8iIxl+tZ+_>~nPl?c7T{$&NX)gh_$!>J(N z8K8Tlsjtcz4V{*4Yd^bfyH%;W;ujg*^ZV^DNqM)n*Ktlrn&`>|X-3&kufQ5m9 ziGhKIiHV7Yg@uhvh>MGZgG)+4fJX=iU zZzkrwx&H&S2k01>SlBqIQ8i)!8ag@}1|}vxHp+Yy1r2aNO7a+slvf6uj9kkS_(abm zfs&C=R$J$FVp4wLGQ|t4x=zF>Gryqc(jgP8KzNbdn}Ygp_qtGO9-yQBTiL&KiIbp4 z@B7%8*cj-48Fb$x!6bc5#w(*m{zTg{A%6*rg3sCm$glIduJaI^Q9zc-3UPEd4ZufD zctC4EE_aQ+8W&!5Xs%ZNVz`QTr`dHxgi3jPx)yU?HhC3=j8v4kG}FFF=6 zPZ;t41D@A!F)%Rj85qck!#%@=!{i43v4o6SuE*TQ4DDwpV@2BdpYv?voC&>@h&Cg z4p6W#YT28{p~Ku_6=Zx_>!b}foHtiBgM-uqVhRfC%aQMcH0A;gM|(y7L~Iy{@6#N1 z7FgoNrEL3R9&Yn7G=eA${kkLHMGA(8LkdEcXcDC9)sp~)Vw zg=dU1_sn)lZ-Keyw1V>bh;|7wo3PzBeqe_!Zo#wFrB%^czkY-+4n4(Z`Q{hl?v<DkxHOt-S4iPqC>XBm1Aa!96aLs_AC2w8P4MK|IYwtR zJ-SXOIuqF(Pwfj^5Z?2hIC%1wSW1g^%nW(|oG=Cq@@A2Q)?I+lSj@*soz|OK?AQZ) zk+s>(Z@@55U8d)Y;gGyS16rK9jpZU;EUX7%-vBbGh=MxsXaHLb28P%?Q~+4Kk}pYU zBp-i8Ms2UJA%H;|8b1p=)!QLG{u4@mGQs2rZm@2s-@R!a|J4`b#hm?mv@vl%B2ffT zZdj!lC>4qDuw2XPu>Vps2%@95a zP@;NRN#q({{Nw?C#G&D6GAdyGFKEt?U{e!Gbi=PqmC3fT5|6}xE{trJYJoQnzn&P+ z6}On$aA(x$wa=pGI_3nZ1o=Se&4eHS?eJ{tHrGE4?f&cy-3O z=w&j<=;%71Sb42oh~T*Zst#h#kk8}h@-9X=4wys2rf{I6wsvz78=Dd|HW3S?PT#wqX9s>s^ zYH}X-Q&%96ruOKiS$@i&U~@FP-UznYO-BJL7-HM+5?;yZPo@1>=`H2VR}M$re?sWr zEhJ~wlwJpRIdkPI7JY8@_~bGz66t*EY;ONgSMN&2Vi@loE<<^UlUu#~HU7o&Vjh9GiI{ z4!OJoc&oV)V!v`)ARKw#lhpRqi$=T$_x9n)k=Tn*YL`3)9ozjs>+87S3mhg+;AQhl z7*Fq)*!_(h3N_I-P>*`k6d9;zNQx`M?rjpqh%z-J2AmX^-pHqP55wdO{qSb@=KN7M z{vero4;UlXxrYXXi`LaUG|sw*p=s26azz(+fGx)lXO$HSmjOXlF<8BCKz--o4dQ3P zp%(d@x2JU@(Z3fKLu}pJEz9* z2#lduzr|kHyjOwZ6TZ_d8J-NWx$eFUmCoOsKbd`*`yUz+_Zsg2@&9YXTs9_UQd#+y z@cC8L4BZ)T7QU^gr_CO>@QH2Iw2%e+BdvE&Qa7lWY}jOVakQyqJoyu#Yes6%e!l)! zfJ8;gqx~e8#F>MdT4&tSs*^WiBN7CPN9L7pofui@hI_ss9Bd5(44O@K=`v}E+z z%fB)^!9*g-`Nl@`bwLbv92uKooO0sRs-3LaeH(l99N;p1ZGEPot>+q2v!rKZiH;NI z;4R}(`q;px7$Xn0g>Nx1Nbw$^iO8UJ&f-WFO{(BK$JHn9C^IwLaHdjYRZF+~0h1_9 ztYGFzuz2a_-_P;iA$OvzaS=&PB3FvXX6>e~zB9}rxO1N42+)%cQfuz0x?=F+9|$*FW@nwgZng7Axz~)HmeJDz96j5Pw{l1g zXGRB29nE99ETqB+PI>3h$}w3+?~DD8j-fM-pz=8#W+D;5HK~>5vtI-XBV%UUR$<0Y z)DJURZ01FN+yR18z+{qx1iq3lmL=(Ce70wJh`-W!?m7kOWgGY9~>kiTo1l4y` zSwmWS0h~Ye!7zQXx_(mVfm^fEX1UXHs=|?j*foVqMo{xg;Dm#`F>6c8yLVi|_VFq3 z(G#!7L(n8&=o9bGi-ooWvqAf7F%H8Z@Q&u0*I{T&&iV1?>>k#*3EW$Lx>QQJ#NEYR zUN~pZAG5G<#CF`RZCn)`LY{QMT&iqfm zJU6C%)L-6>!>RkLOZj1GjzTZUhs)zD-ml_XiN?4!GI77=nOtSqNC#p>yEQLPc-~Lu ziqZ=ky`wM)J{95B`zC5SyP|m^-$lcr$XZ$IOnE3c^gwT6#Ql%4f2N|vc*J?8Ah{s% z=pL}%M6QCt&4?-uHbDEPBa-HvxQTBRIks98Tw1gT3j#*Rt=Ko?lO%LPD5ihKBK9JE zMQL#7(Q}^!j_*hb?cmJx4ZGa-)Xx|(BR8azx$4OdhUi?ZTk;N1Yf@{YHH;N#c>AFe z5_bUo5&{nGx#EpLG1D3o4;)538BC9|1nn@?jR=QkgeROQ?2UzhohZ&)p9|9O9rJVT zh+y`Ij1#gxhTP&m4_nKU67l%QSDQCA>oh~%D`Q@7#246JyFM#_Hh-=_vnZ^A18|_s zt5NPOd_UIl#?^U`EByVdJ#PaQmFDc1HWB(AdF>=0f>pEAqHRBmYy0ltr*xBuxA}D2 zJ3Bk&7eSS`negK>(tzM)Mt{e(ep3(O_C4a!Y53=e4c%?9rN1xZf;7=h5QW&i@P{?plB zgW*%G0rRgu89v)1Rk_L>H0uh^&u?&dZ4Y*=93PXA*P$_RV_NKV@#1$@Yhr)#=`B9n zYmQ}IrK+c>R2qP~+Y=LOuM4ga0^UY{+$V=qO?p1!s}@QZ(g(_~VKCT07-F3?G%FJm zzD;@UM;qmckA|$x4Br7BeCdn$(w^qKa254sfD$hhzXASU&rck7w#DU}k>OxCF|bR4 z?N}3d>6qN%S2MEaflro6P;R)hxZx-y6krlpVfS^gM?kF7{}ObIW*uvyCMwdbSDr>& zAf~vyvwYwvB5Ipf($l`XxI%rTgBi$d@ zQuns*rwH;f82PHr|j(5r|p$mX;?(4b1?hwZn2``;)L9$%Q zFG`0|$B_tu0Q`U;4`aECyeZ@93~b%D`Bet06$w*`z|^y(FekpntcKAbM%2c};s4PA z^7(ooC2XTHU%Y57YMeoIL}C^@E)8ZXra`tG?EBu+71C#^RuzqSN{RWhkP!Ip9h!m`qECCvuy$K7r$o7@ zi&5H@pOhZTpaxo=xv_|I=|!n`dm=)a4PC>M$7B8A4ZGKom2uvsRdtPxNj52s&@!v3 zQpeBo6LbVj0TVHI0Bx4j#niQEr=>3Ij(IBTfYUe9q5%c11Vg>RDVx{}) z3JUk-FovuLni#Jwhq+HV5!t-G-io}kZSW<@nK$ZxIju(Lbdg*SXXJa zUGe#W4{W-*{RLRk$kM3&`<=jW_++DB0s1qf&P8a1e!X zp-VNk1sVHSw^;d^I9-Tc(Gguy(zDa>F)hqWnhUqsTE{hQr;6<5Hk@7z5T0bYmGO0I(UV5ccX4j;a8RqfEW z52|63))|{y7q^DpPOIse5RE&))X-pO-H=k22Z;sMRLoBHw>>70VTlyNA*!)?{pv=s zY}-oJX|NPNqO`Lly3vZAj%$BdZuN>n)+h9v{}KCE7VC@642NFHo0F?{g*Kfn>XM_m z#X&Qn^a7mcGLQeeM2d0@jvu>Oges?R+0c}-p5i;W=M%d6iTm&QdL+qx1)$AXs{4O<)@q%0&mX7!&Fd~6f&%=kTMd!Z{&a2z} z3bvJ>@oqLx zJ9~#T%Z(0e$4mF4F`313Z@UB7dne>-@k^gZ;Ga#`l0~|fvDrS6MZ-4z&@uR%0_F!> z9vl~%pJq2|RSWeTYr^k0nr9AyFjp0no#b;&tfmC4^qHqBDzMKZ2JNDfc`Di3w0kG7Js)`HSv?*OH(v~i-&RY0FXhQqfeyan%XY=L0 zWq&ZlV7Su_>7kld(M>Js)N5c?$4#oJd)6#k!^se@WeP$%OTZ5z%t^>-j<~Y5ris1U z)Ekd>R}a5!Tk{t?x{FWtG@Ee}5oYVzA;56U{9SX@_HY}m=uqMg0v&hJjmGrfG zf5V#QSqJ*@pMGBtOi1EsS0IXd)8LYKCsebCeUE&o7t6&$qQLY9suqMS&Zhnn4*KoZ zjjZ|Ha+}&~K*<-uRX-$u3+dapJ1BmNw>q(KbXTubL(tAq!%koWk`CMq)5Lt?GtioW zkDylQ0np(F;2b`MwSUmmEp15rjc}cLD2L8dGhUwSMDPB;GEbE4zQ#$BHRz3pc#5v+ z>6#oCm`vb1Vhr+w_u*Kp4T2Dqj**dH_7o70Ej*#mu142ogFBnvh#R`Y{?wQSgRYgE_wtA_ezaB0 zl`R)r#iTU(oJ}CwLpY=5ETqC1uOt7jYOKy!VNzArZ0QprO0*4; z*I9tpiyf++aRU+)b>gnk$)o^(QUKV}V45*de9B$MD z`*hTiPDG)2s=juHU20>EE7JS|qDkONOxLlyHSdW849RS}b;8o8- zC=tf3RcfSvdR(Ep&&4zQ-U`YG725Zy5(=;TFc<(~VQ(l_T#8zCKIhatW^%p4IR&N> z1}-%TvO4OIzgHOTo}Bw=H>B=PqGlW6$2%~8q2A-o&P{?Gs$d%gskz2dwFpmr^PYAF z`8Q=iSW`N(wtFb?4s-G+H-4F$FM9j2E zcqaJmIpY$FYsl-uaXPWRxyfoiW@{+Ni{4hZhEVxu&9SLsx*Wp$8NYjqw#`{iV8s3b zZnAxQ@+*4dGT3;o$$VYjj9pFfg8Yj^XtCn1;Vo4Ci-Y(rHLkZ}=|0SzqRpDFtgJl7e9<8OJyVqR^Kj&rIlG4fD#SIy`1E{pSN8Pes z9w|ESI|`*aLlGus^4BDBLk!Vo8x)JjC;I#GiefHVr+v3UdX49C@Bn(nvr2HS-Dd8T zqvLi_JfBEo{sz+uYdk&|J=rh2!5oB8dejB~N>mJ-k~4iO@UyxPd`djNhr_`S=Jd%D z>2!Fze-5#Rui`Mo=&LZ$|t2Pl~2A7|HLWgDH zm*{WjlhfE?-GBakDJf;2-Nl21W&|;9gkRPIb9St~D*r6)L;@>?n+5|dPIV9Br{A86 zgS!xVcK|&}O(((%3nSz8)YdW6^=urD<%B+3 z)071{^(?8~_SM#qMP7Zoqy+~}y-p*K-|GH%fJTLjn=-LdnCYwl%-}{3v~rfWjcwLq zwDY;bYxyj1TALaoU0&JoTG9Fa^czRLsRNB0Rnw5ye}4H+2A7H2n)>dnq9Wf8Xn7ay z`d5Gg)g1uFnrc?lZN$<5cdBUjO%HS3@stPe4+pFq5y;z3!}cRgeP*g!dvwX>Tf7v^ zCQP{WMAauA{VAJbdsIgDmN+c@B{A`X2LRLyxyLs=RYgx%TAje~>{V^lb($P%e?DFH%S;|E;Ii^yJhN=;I?DSdy)5s&ryuj(Q^73O!I!B9pcJ z)kTG9u{e_g;s?tylK>%_!m`I((ti>k)RX;Rz=}fo-rwK&pFdxWCi$C#_45DX{w-sY Vzjs#@l60Tn2h%WOo;P<>{{>1qrojLJ literal 0 HcmV?d00001 diff --git a/assets/js/directorist-themes-extensions.js b/assets/js/directorist-themes-extensions.js index 3846e82859..7dbdf119a8 100644 --- a/assets/js/directorist-themes-extensions.js +++ b/assets/js/directorist-themes-extensions.js @@ -16,7 +16,7 @@ const rows = () => $('.directorist-te-row'); const visibleRows = () => rows().filter(':not(.is-hidden)'); const selectableChecks = () => - $('#atbdp-my-extensions-form .extension-name-checkbox'); + $('#atbdp-my-extensions-form .directorist-te-select-checkbox'); const checkedSelectableChecks = () => selectableChecks().filter(':checked'); const visibleSelectableChecks = () => selectableChecks().filter(function () { @@ -24,13 +24,144 @@ }); const updateBanner = () => $('.directorist-te-update-banner'); const updatePill = () => - $('.directorist-te-segmented button[data-filter-status="update"] span'); + $( + '.directorist-te-segmented button[data-filter-status="update"] .directorist-te-update-count' + ); const ajaxConfig = () => window.directorist_admin || {}; const ajaxUrl = () => ajaxConfig().ajaxurl || window.ajaxurl; const nonce = () => ajaxConfig().nonce || ''; const directoristNonce = () => ajaxConfig().directorist_nonce || nonce(); + function normalizeSearchText(value) { + return String(value || '') + .toLowerCase() + .replace(/[-_]+/g, ' ') + .replace(/\s+/g, ' ') + .trim(); + } + + function rowDataText(row, key) { + return normalizeSearchText($(row).attr(`data-${key}`) || ''); + } + + function escapeRegExp(value) { + return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } + + function searchHighlightTargets(row) { + return $(row).find( + '.directorist-te-row__title h2, .directorist-te-row__content > p' + ); + } + + function originalHighlightText(element) { + const $element = $(element); + const originalText = $element.data('directorist-te-original-text'); + + if (typeof originalText !== 'undefined') { + return originalText; + } + + const text = $element.text(); + $element.data('directorist-te-original-text', text); + + return text; + } + + function renderHighlightedText(text, query, exactTokenOnly) { + const rawQuery = String(query || '').trim(); + + if (!rawQuery) { + return escapeHtml(text); + } + + const pattern = exactTokenOnly + ? new RegExp( + `(^|[^A-Za-z0-9])(${escapeRegExp( + rawQuery + )})(?=[^A-Za-z0-9]|$)`, + 'gi' + ) + : new RegExp(escapeRegExp(rawQuery), 'gi'); + let html = ''; + let lastIndex = 0; + let match; + + while ((match = pattern.exec(text)) !== null) { + const matchText = exactTokenOnly ? match[2] : match[0]; + const matchIndex = exactTokenOnly + ? match.index + match[1].length + : match.index; + + html += escapeHtml(text.slice(lastIndex, matchIndex)); + html += `${escapeHtml( + matchText + )}`; + lastIndex = matchIndex + matchText.length; + + if (pattern.lastIndex === match.index) { + pattern.lastIndex += 1; + } + } + + html += escapeHtml(text.slice(lastIndex)); + + return html; + } + + function updateSearchHighlights(query, exactTokenOnly) { + const normalizedQuery = normalizeSearchText(query); + + rows().each(function () { + const rowIsVisible = !$(this).hasClass('is-hidden'); + + searchHighlightTargets(this).each(function () { + const text = originalHighlightText(this); + const normalizedText = normalizeSearchText(text); + const hasMatch = exactTokenOnly + ? normalizedText.split(/\s+/).includes(normalizedQuery) + : normalizedText.includes(normalizedQuery); + + if (!normalizedQuery || !rowIsVisible || !hasMatch) { + $(this).text(text); + return; + } + + $(this).html(renderHighlightedText(text, query, exactTokenOnly)); + }); + }); + } + + function collectBadgeSearchTerms() { + const terms = new Set(); + + rows().each(function () { + const rawTerms = $(this).attr('data-badge-search-terms') || '[]'; + + try { + const parsedTerms = JSON.parse(rawTerms); + + if (Array.isArray(parsedTerms)) { + parsedTerms.forEach((term) => { + const normalizedTerm = normalizeSearchText(term); + + if (normalizedTerm) { + terms.add(normalizedTerm); + } + }); + } + } catch (error) { + rowDataText(this, 'badge-search-text') + .split(/\s+/) + .filter(Boolean) + .forEach((term) => terms.add(term)); + } + }); + + return terms; + } + function statusTokens(row) { return String($(row).data('product-status') || '') .split(/\s+/) @@ -90,11 +221,72 @@ const $row = $checkbox.closest('.directorist-te-row'); const $label = $checkbox.closest('.directorist-te-checkbox'); + if ($checkbox.prop('disabled')) { + return; + } + $checkbox.prop('checked', checked); $row.toggleClass('selected', checked); $label.toggleClass('is-checked', checked); } + function checkboxActions(checkbox) { + return String($(checkbox).attr('data-bulk-actions') || '') + .split(/\s+/) + .filter(Boolean); + } + + function selectedBulkItems(task) { + return checkedSelectableChecks() + .map(function () { + const actions = checkboxActions(this); + + if (task && !actions.includes(task)) { + return null; + } + + return { + item: $(this).attr('data-bulk-item') || this.value || this.id, + type: $(this).attr('data-bulk-type') || 'plugin', + actions, + }; + }) + .get() + .filter(Boolean); + } + + function selectedActionCounts() { + const actionCounts = {}; + + checkedSelectableChecks().each(function () { + checkboxActions(this).forEach((action) => { + actionCounts[action] = (actionCounts[action] || 0) + 1; + }); + }); + + return actionCounts; + } + + function itemCountText(count) { + return count === 1 ? '1 selected item' : `${count} selected items`; + } + + function bulkActionLabel(button) { + const $button = $(button); + let label = $button.data('directoristTeBulkLabel'); + + if (label) { + return label; + } + + label = + $button.find('.directorist-te-bulk-action__label').text().trim() || + $button.text().trim(); + $button.data('directoristTeBulkLabel', label); + + return label; + } + function syncMasterCheckbox() { const master = $('#select-all-installed'); @@ -107,7 +299,7 @@ const isAll = visible.length > 0 && checked.length === visible.length; const isSome = checked.length > 0 && checked.length < visible.length; - master.prop('checked', isAll); + master.prop('checked', isAll).prop('disabled', visible.length === 0); master.prop('indeterminate', isSome); master .closest('.directorist-te-checkbox') @@ -159,25 +351,73 @@ const checked = checkedSelectableChecks(); const count = checked.length; const bulkbar = $('.directorist-te-bulkbar'); + const actionCounts = selectedActionCounts(); + let visibleActions = 0; bulkbar.prop('hidden', count === 0).toggleClass('show', count > 0); - $('.directorist-te-selected-count').text(`${count} selected`); + $('.directorist-te-selected-count').text( + count === 1 ? '1 selected' : `${count} selected` + ); + + $('.directorist-te-bulk-action').each(function () { + const task = String($(this).data('task') || ''); + const actionCount = actionCounts[task] || 0; + const skippedCount = Math.max(count - actionCount, 0); + const isVisible = count > 0 && actionCount > 0; + const label = bulkActionLabel(this); + const hint = + `${label} will run on ${itemCountText(actionCount)}.` + + (skippedCount + ? ` ${itemCountText(skippedCount)} will be skipped.` + : ''); + + $(this) + .prop('hidden', !isVisible) + .prop('disabled', !isVisible) + .attr('data-eligible-count', actionCount) + .attr('title', isVisible ? hint : '') + .attr('aria-label', isVisible ? hint : label); + + $(this) + .find('.directorist-te-bulk-action__count') + .text(actionCount) + .prop('hidden', !isVisible); + + if (isVisible) { + visibleActions++; + } + }); + + $('.directorist-te-bulkbar__empty').prop( + 'hidden', + count === 0 || visibleActions > 0 + ); + $('.directorist-te-bulkbar__notice').prop( + 'hidden', + count === 0 || visibleActions === 0 + ); syncMasterCheckbox(); updateUpdateBanner(); } function updateFilters() { - const query = state.query.trim().toLowerCase(); + const query = normalizeSearchText(state.query); + const isExactBadgeQuery = query && collectBadgeSearchTerms().has(query); rows().each(function () { const row = $(this); const typeMatches = state.type === 'all' || row.data('product-type') === state.type; const statusMatches = rowHasStatus(this, state.status); + const searchText = rowDataText(this, 'search-text'); + const badgeSearchText = rowDataText(this, 'badge-search-text'); const textMatches = !query || - String(row.data('search-text') || '').includes(query); + (isExactBadgeQuery + ? badgeSearchText.includes(query) + : searchText.includes(query) || + badgeSearchText.includes(query)); row.toggleClass( 'is-hidden', @@ -185,6 +425,7 @@ ); }); + updateSearchHighlights(state.query, isExactBadgeQuery); updateCountsAndEmptyState(); updateBulkBar(); } @@ -202,6 +443,28 @@ return form.attr(`data-${key}`) || fallback; } + function isRemoteConnectionMessage(message) { + return /curl|operation timed out|timed out|could not resolve|ssl|connection|network/i.test( + String(message || '') + ); + } + + function authLogMessages(response) { + const log = response?.status?.log; + + if (!log || typeof log !== 'object') { + return []; + } + + return Object.keys(log) + .map((key) => log[key]?.message || '') + .filter(Boolean); + } + + function authHasRemoteConnectionError(response) { + return authLogMessages(response).some(isRemoteConnectionMessage); + } + function connectFeedback(form) { return form.find('.directorist-te-feedback').first(); } @@ -278,6 +541,305 @@ $('.directorist-te-search-input').first().trigger('focus'); } + function refreshPanelFocusable(panel) { + return panel.find('a, button, input, select, textarea'); + } + + function setRefreshPanelHidden(panel, hidden) { + panel.prop('hidden', hidden).attr('aria-hidden', hidden ? 'true' : 'false'); + + if (hidden) { + refreshPanelFocusable(panel).attr('tabindex', '-1'); + return; + } + + refreshPanelFocusable(panel).removeAttr('tabindex'); + } + + function setRefreshTriggerHidden(wrapper, hidden) { + wrapper + .prop('hidden', hidden) + .attr('aria-hidden', hidden ? 'true' : 'false'); + + if (hidden) { + wrapper.find('a, button').attr('tabindex', '-1'); + return; + } + + wrapper.find('a, button').removeAttr('tabindex'); + } + + function setupAccountMenu() { + const menu = $('.directorist-te-account-menu'); + const toggle = menu.find('.directorist-te-avatar').first(); + const dropdown = menu.find('.directorist-te-account-dropdown').first(); + let closeTimer = null; + + if (!menu.length || !toggle.length || !dropdown.length) { + return; + } + + function refreshPanelIsOpen() { + const panel = dropdown.find('.directorist-te-refresh-panel').first(); + + return panel.length && !panel.prop('hidden'); + } + + function setAccountMenuOpen(open) { + window.clearTimeout(closeTimer); + dropdown.prop('hidden', !open).attr('aria-hidden', open ? 'false' : 'true'); + toggle.attr('aria-expanded', open ? 'true' : 'false'); + + if (!open) { + const panel = dropdown.find('.directorist-te-refresh-panel').first(); + const wrapper = dropdown.find('.purchase-refresh-btn-wrapper').first(); + + if (panel.length && wrapper.length) { + panel.find('input[name="password"]').val(''); + setRefreshPanelHidden(panel, true); + setRefreshTriggerHidden(wrapper, false); + } + } + } + + function scheduleClose() { + window.clearTimeout(closeTimer); + + if (refreshPanelIsOpen()) { + return; + } + + closeTimer = window.setTimeout(function () { + if (menu.find(':focus').length) { + return; + } + + setAccountMenuOpen(false); + }, 120); + } + + setAccountMenuOpen(false); + + menu.on('focusout.directoristTeAccount', scheduleClose); + + toggle.on('click.directoristTeAccount', function (event) { + event.preventDefault(); + setAccountMenuOpen(dropdown.prop('hidden')); + }); + + $(document).on('click.directoristTeAccount', function (event) { + if ($(event.target).closest(menu).length) { + return; + } + + setAccountMenuOpen(false); + }); + + $(document).on('keydown.directoristTeAccount', function (event) { + if ('Escape' !== event.key || dropdown.prop('hidden')) { + return; + } + + setAccountMenuOpen(false); + toggle.trigger('focus'); + }); + } + + function setupRefreshPurchasePanel() { + const panel = $('.directorist-te-refresh-panel'); + const triggerWrapper = $('.purchase-refresh-btn-wrapper'); + + if (!panel.length || !triggerWrapper.length) { + return; + } + + setRefreshPanelHidden(panel, true); + setRefreshTriggerHidden(triggerWrapper, false); + + $('.purchase-refresh-btn') + .off('click') + .on('click.directoristTeRefresh', function (event) { + event.preventDefault(); + + const wrapper = $(this).parent(); + + setRefreshPanelHidden(panel, false); + setRefreshTriggerHidden(wrapper, true); + + if (panel.closest('.directorist-te-account-dropdown').length) { + panel.find('input[name="password"]').trigger('focus'); + return; + } + + const targetWidth = Math.min( + panel.find('.directorist-te-refresh-form').outerWidth() || 330, + panel.parent().width() || 330 + ); + + wrapper.stop(true, true).animate({ width: 0 }, 250); + panel + .stop(true, true) + .css({ width: 0 }) + .animate({ width: targetWidth }, 250, function () { + panel.css('width', ''); + panel.find('input[name="password"]').trigger('focus'); + }); + }); + + $('.et-close-auth-btn') + .off('click') + .on('click.directoristTeRefresh', function (event) { + event.preventDefault(); + + const wrapper = $('.purchase-refresh-btn-wrapper'); + const trigger = wrapper.find('.purchase-refresh-btn').first(); + const targetWidth = trigger.outerWidth() || 160; + + if (panel.closest('.directorist-te-account-dropdown').length) { + panel.find('input[name="password"]').val(''); + setRefreshPanelHidden(panel, true); + setRefreshTriggerHidden(wrapper, false); + trigger.trigger('focus'); + return; + } + + panel + .stop(true, true) + .animate({ width: 0 }, 250, function () { + panel.find('input[name="password"]').val(''); + panel.css('width', ''); + setRefreshPanelHidden(panel, true); + }); + + setRefreshTriggerHidden(wrapper, false); + wrapper + .stop(true, true) + .css({ width: 0 }) + .animate({ width: targetWidth }, 250, function () { + wrapper.css('width', ''); + trigger.trigger('focus'); + }); + }); + } + + function clearProductSelection() { + selectableChecks().each(function () { + setCheckbox(this, false); + }); + + updateBulkBar(); + } + + function closeProductMenus() { + $('.ext-action-drop').removeClass('active'); + } + + function setupViewSwitcher() { + const switchers = $('[data-directorist-te-view-target]'); + const views = $('[data-directorist-te-view]'); + + if (!switchers.length || !views.length) { + return; + } + + function setActiveView(target) { + const targetView = views.filter(function () { + return $(this).attr('data-directorist-te-view') === target; + }); + + if (!targetView.length) { + return; + } + + views.each(function () { + const isActive = + $(this).attr('data-directorist-te-view') === target; + + $(this) + .prop('hidden', !isActive) + .attr('aria-hidden', isActive ? 'false' : 'true') + .toggleClass('is-active', isActive); + }); + + switchers.each(function () { + const isActive = + $(this).attr('data-directorist-te-view-target') === target; + + $(this).toggleClass('active', isActive); + + if (isActive) { + $(this).attr('aria-current', 'page'); + return; + } + + $(this).removeAttr('aria-current'); + }); + + closeProductMenus(); + + if (target === 'dashboard') { + clearProductSelection(); + } + + if (target === 'addons') { + updateFilters(); + } + } + + switchers.on('click.directoristTeView', function (event) { + event.preventDefault(); + setActiveView($(this).attr('data-directorist-te-view-target')); + }); + } + + function setupDashboardPreview() { + const dashboard = $('.directorist-te-dashboard'); + const nudge = dashboard.find('.directorist-te-dashboard-nudge'); + const steps = dashboard.find('.directorist-te-dashboard-step'); + const ring = dashboard.find('.directorist-te-dashboard-ring circle').last(); + const ringLabel = dashboard.find('.directorist-te-dashboard-ring span'); + const radius = 19; + const circumference = 2 * Math.PI * radius; + + if (!dashboard.length) { + return; + } + + function paintProgress() { + if (!steps.length || !ring.length || !ringLabel.length) { + return; + } + + const done = steps.filter('.is-done').length; + const percent = Math.round((done / steps.length) * 100); + + ring + .attr('stroke-dasharray', circumference.toFixed(1)) + .attr( + 'stroke-dashoffset', + (circumference * (1 - percent / 100)).toFixed(1) + ); + ringLabel.text(`${percent}%`); + } + + paintProgress(); + + dashboard + .find('.directorist-te-dashboard-nudge__dismiss') + .on('click.directoristTeDashboard', function () { + nudge.prop('hidden', true).attr('aria-hidden', 'true'); + }); + + steps.on('click.directoristTeDashboard', function () { + if ($(this).hasClass('is-done')) { + return; + } + + $(this).addClass('is-done'); + paintProgress(); + }); + } + function runPluginBulkTask(task, pluginItems, button) { if (!pluginItems.length) { return; @@ -310,19 +872,19 @@ }); } - function runSelectedPluginUpdates(pluginItems, button) { - const queue = pluginItems.slice(); + function runSelectedInstalls(items, button) { + const queue = items.slice(); if (!queue.length) { return; } - setButtonLoading(button, 'Updating'); + setButtonLoading(button, 'Installing'); const next = () => { - const pluginKey = queue.shift(); + const item = queue.shift(); - if (!pluginKey) { + if (!item) { window.location.reload(); return; } @@ -331,10 +893,67 @@ type: 'post', url: ajaxUrl(), data: { - action: 'atbdp_update_plugins', - plugin_key: pluginKey, + action: 'atbdp_install_file_from_subscriptions', + item_key: item.item, + type: item.type, nonce: nonce(), }, + success(response) { + if ( + response?.status && + response.status.success === false + ) { + alert(response.status.message || 'Install failed.'); + resetButton(button); + return; + } + + next(); + }, + error() { + alert('Install failed. Please reload the page and try again.'); + resetButton(button); + }, + }); + }; + + next(); + } + + function runSelectedUpdates(items, button) { + const queue = items.slice(); + + if (!queue.length) { + return; + } + + setButtonLoading(button, 'Updating'); + + const next = () => { + const item = queue.shift(); + + if (!item) { + window.location.reload(); + return; + } + + const data = + item.type === 'theme' + ? { + action: 'atbdp_update_theme', + theme_stylesheet: item.item, + nonce: nonce(), + } + : { + action: 'atbdp_update_plugins', + plugin_key: item.item, + nonce: nonce(), + }; + + $.ajax({ + type: 'post', + url: ajaxUrl(), + data, success(response) { if ( response?.status && @@ -399,7 +1018,7 @@ formMessage( form, 'username-required', - 'Enter your Directorist account username or email.' + 'Enter your Directorist account username or email address.' ) ); return; @@ -454,14 +1073,27 @@ return; } - if (!connectFeedback(form).text().trim()) { + if ( + authHasRemoteConnectionError(response) || + isRemoteConnectionMessage(connectFeedback(form).text()) + ) { + renderConnectFeedback( + form, + 'danger', + formMessage( + form, + 'network-error', + 'Could not reach Directorist.com. Please try again.' + ) + ); + } else { renderConnectFeedback( form, 'danger', formMessage( form, - 'unexpected-error', - 'Could not connect. Please check your details and try again.' + 'invalid-credentials', + 'The username, email address, or password is incorrect. Please check your details and try again.' ) ); } @@ -523,7 +1155,7 @@ $('#atbdp-my-extensions-form').on( 'change', - '.extension-name-checkbox', + '.directorist-te-select-checkbox', function () { setCheckbox(this, this.checked); updateBulkBar(); @@ -565,27 +1197,45 @@ event.preventDefault(); const task = $(this).data('task'); - const pluginItems = checkedSelectableChecks() - .map(function () { - return this.id; - }) - .get(); + const selectedItems = selectedBulkItems(task); + const selectedCount = checkedSelectableChecks().length; + const skippedCount = Math.max(selectedCount - selectedItems.length, 0); - if (!task || !pluginItems.length) { + if (!task || !selectedItems.length) { return; } if ( task === 'uninstall' && !window.confirm( - 'Delete removes the selected plugin files from this site. Continue?' + `Delete removes ${itemCountText( + selectedItems.length + )} from this site.` + + (skippedCount + ? ` ${itemCountText(skippedCount)} will be skipped.` + : '') + + ' Continue?' ) ) { return; } + if (task === 'install') { + runSelectedInstalls(selectedItems, this); + return; + } + if (task === 'update') { - runSelectedPluginUpdates(pluginItems, this); + runSelectedUpdates(selectedItems, this); + return; + } + + const pluginItems = selectedItems + .filter((item) => item.type === 'plugin') + .map((item) => item.item); + + if (pluginItems.length !== selectedItems.length) { + alert('This bulk action is only available for plugins.'); return; } @@ -770,5 +1420,9 @@ setCheckbox(this, this.checked); }); + setupAccountMenu(); + setupRefreshPurchasePanel(); + setupViewSwitcher(); + setupDashboardPreview(); updateFilters(); })(jQuery); diff --git a/docs/agents/directorist-themes-extensions-page/SKILL.md b/docs/agents/directorist-themes-extensions-page/SKILL.md index 462a10bdf5..baac39e187 100644 --- a/docs/agents/directorist-themes-extensions-page/SKILL.md +++ b/docs/agents/directorist-themes-extensions-page/SKILL.md @@ -50,6 +50,7 @@ Load reference files as needed: - Account/license journeys: `references/account-license-journey-map.md` - Performance and no-reload guidance: `references/performance-improvement-guide.md` - Full rewrite issue register and fix priorities: `references/rewrite-issue-register.md` +- Current implementation changes and locked UI decisions: `references/current-implementation-change-notes.md` - Verification checklist: `references/qa-checklist.md` ## Live Inspection Requirement @@ -73,10 +74,12 @@ agent-browser --session directorist-themes-extensions close ## Safety Rules - Never click or automate Install, Update, Activate, Deactivate, Uninstall, Logout, Refresh Purchase, or theme switch actions on a real/client site without explicit confirmation for that exact action. +- Treat the disconnected view as locked as of 2026-07-22. Unless the user explicitly asks for disconnected-view changes in the current task, do not change disconnected-view PHP markup, copy, header/resource links, connect-card UX, marketplace/search/tab layout, product-row actions, CSS/responsive behavior, or JS behavior that only affects disconnected users. - Preserve the page slug `atbdp-extension`, parent post type `at_biz_dir`, capability `manage_options`, existing AJAX action names, nonce expectations, response compatibility, settings links, filters, aliases, and legacy user meta keys. - Preserve legacy misspellings that are part of stored data or public contracts, including `_atbdp_has_subscriptions_sassion` and `atbdp_close_subscriptions_sassion`. - Do not remove or repurpose old selectors, classes, template paths, filters, or action hooks without a compatibility shim and explicit migration plan. - After a new UI replaces an old page section, audit old design-specific CSS and remove unused rules after verifying they are not used by the rewritten page, legacy compatibility shims, template overrides, or other Directorist admin screens. +- Keep new Themes & Extensions styles page-scoped. Do not add broad global admin selectors or shared Directorist selectors for this page unless a reusable design-system change is explicitly requested and verified across affected screens. - Treat install, update, download, uninstall, and theme activation as high-risk operations. Use read-only inspection by default. - Keep this page separate from installed extension settings. Settings-panel work belongs to `docs/agents/directorist-settings-panel/SKILL.md`. - Treat Figma/design input as design intent. Do not rewrite backend product/install logic only to match a visual design. diff --git a/docs/agents/directorist-themes-extensions-page/references/account-license-journey-map.md b/docs/agents/directorist-themes-extensions-page/references/account-license-journey-map.md index 6ffde7e84c..85b6e6caaf 100644 --- a/docs/agents/directorist-themes-extensions-page/references/account-license-journey-map.md +++ b/docs/agents/directorist-themes-extensions-page/references/account-license-journey-map.md @@ -27,6 +27,8 @@ Performance opportunity: replace unconditional reload with a state refresh/rende When the account is not connected, preserve the current page behavior: render the Directorist account-connect form and the promo marketplace/discovery section only. +- Locked as of 2026-07-22: the disconnected view is complete and should be treated as out of scope for future Themes & Extensions work unless the user explicitly asks for disconnected-view changes in that task. +- Do not alter disconnected-view layout, account-connect copy, resource links, search/tabs/count placement, product row/card actions, CSS/responsive behavior, or page-specific JS behavior by accident while working on connected-state, badge, API, licensing, install/update, or theme-management features. - Do not expose installed premium product management in the disconnected page state by default. - Do not show Settings, Active status, Activate, Deactivate, Update, or local management actions for installed premium products while disconnected unless a future task explicitly changes this policy. - Already installed premium products can continue working in WordPress, but this page should not manage them while the Directorist account session is disconnected. diff --git a/docs/agents/directorist-themes-extensions-page/references/api-data-flow-report.md b/docs/agents/directorist-themes-extensions-page/references/api-data-flow-report.md index b37600e6b1..45eef543b1 100644 --- a/docs/agents/directorist-themes-extensions-page/references/api-data-flow-report.md +++ b/docs/agents/directorist-themes-extensions-page/references/api-data-flow-report.md @@ -57,18 +57,61 @@ Product copy/source policy for rewrite: Future badge/status support should be added to this catalog contract as optional API data. EDD product meta or a dedicated product badge setting on Directorist.com can feed the API, but the core plugin UI should consume the product API field. Do not use normal EDD/WordPress `post_status` as the badge source because it represents product availability, not display labels such as `New`. -Recommended optional badge object: +Recommended optional badge payload: ```json { - "type": "new", - "label": "New", - "expires_at": "2026-09-01" + "badges": [ + { + "type": "new", + "label": "New", + "expires_at": "2026-09-01" + } + ] } ``` Render no badge when this field is absent, expired, malformed, or disabled by filters. +Directorist.com API implementation sketch: + +```php +function directorist_get_product_api_badges( $product_id ) { + $raw_badges = get_post_meta( $product_id, '_directorist_product_badges', true ); + $raw_badges = is_array( $raw_badges ) ? $raw_badges : []; + $badges = []; + $now = time(); + + foreach ( $raw_badges as $raw_badge ) { + $type = isset( $raw_badge['type'] ) ? sanitize_key( $raw_badge['type'] ) : ''; + $label = isset( $raw_badge['label'] ) ? sanitize_text_field( $raw_badge['label'] ) : ''; + $expires_at = isset( $raw_badge['expires_at'] ) ? sanitize_text_field( $raw_badge['expires_at'] ) : ''; + + if ( '' === $type || '' === $label ) { + continue; + } + + if ( '' !== $expires_at ) { + $expires_timestamp = strtotime( $expires_at ); + + if ( false !== $expires_timestamp && $expires_timestamp < $now ) { + continue; + } + } + + $badges[] = [ + 'type' => $type, + 'label' => $label, + 'expires_at' => '' !== $expires_at ? $expires_at : null, + ]; + } + + return $badges; +} +``` + +When building each product item for `v1/get-remote-products`, only include `badges` when `directorist_get_product_api_badges( $product_id )` returns a non-empty array. + ## Account Connection API Browser action: diff --git a/docs/agents/directorist-themes-extensions-page/references/current-implementation-change-notes.md b/docs/agents/directorist-themes-extensions-page/references/current-implementation-change-notes.md new file mode 100644 index 0000000000..6fd20de6cc --- /dev/null +++ b/docs/agents/directorist-themes-extensions-page/references/current-implementation-change-notes.md @@ -0,0 +1,127 @@ +# Current Implementation Change Notes + +These notes capture durable Themes & Extensions page changes made during the current redesign pass. They are implementation context for future rewrite work, not runtime truth. + +Do not store runtime product counts, installed product lists, update counts, account names, subscription contents, or local screenshots here. Re-check live state at task time. + +## Scope + +- Page: `edit.php?post_type=at_biz_dir&page=atbdp-extension`. +- Template: `views/admin-templates/theme-extensions/theme-extension.php`. +- Page JS: `assets/js/directorist-themes-extensions.js`. +- Page CSS: `assets/css/directorist-themes-extensions.css`. +- Product data source class: `includes/classes/class-extension.php`. +- Disconnected view remains locked unless a future task explicitly asks to change it. + +## Disconnected View + +- Disconnected view is treated as complete and locked. +- Header only keeps Directorist branding and resource links for Docs, Tutorials, and Support. +- Connected-state navigation, account menu, settings, active/update controls, and local product management are not shown while disconnected. +- Account form uses `Username or email address` with an email-style placeholder. +- Password field has a show/hide toggle. +- The page focuses the username field for disconnected users. +- Messaging clarifies that already installed Directorist products keep working, but account connection is required for subscription installs, updates, and license-backed management. +- Disconnected product rows use marketplace/details-oriented actions instead of install/update/activate/deactivate/delete controls. + +## Connected Header And Account Menu + +- The large connected account stats/action card was removed from the body. +- Connected header navigation now uses `Dashboard` and `Add-ons` as internal page views. +- The existing Themes & Extensions catalog is labeled `Add-ons` in the header nav, but keeps the original page slug, selectors, forms, rows, and AJAX contracts. +- `Docs`, `Tutorials`, and `Support` remain external resource links, not dashboard tabs, and are grouped on the right side of the connected header like the disconnected view. +- The `Dashboard` view is currently a static design-preview surface based on the supplied reference HTML. It must be replaced with dynamic Directorist data before production release decisions are made. +- The static Dashboard preview inner cards were aligned to the supplied reference HTML pattern: metric sparkline decoration, soft recent-activity icons, the five-item activity list, icon/title recommendation cards, and soft recommendation install buttons. +- Account summary moved into the avatar dropdown so the product list starts closer to the page title/update banner. +- Avatar dropdown is click-only for opening. Hover and focus alone do not open it. +- Dropdown closes on outside click, Escape, and focus leaving the menu. +- Dropdown contains dynamic account summary values, `Refresh purchases`, and `Disconnect`. +- `Refresh purchases` keeps the existing `#purchase-refresh-form` and `atbdp_refresh_purchase_status` AJAX action. +- `Disconnect` keeps `.subscriptions-logout-btn` and `atbdp_close_subscriptions_sassion` compatibility. +- Refresh purchase password inputs are hidden from tab order until the user opens that panel. +- Connected header search is intentionally still static for now. + +## Product Catalog And Toolbar + +- Search was moved into the toolbar near the catalog count. +- Primary tabs keep dynamic All, Extensions, and Themes counts. +- Status segmented filter keeps All, Installed, Not installed, and Updates. +- Updates count renders as a small warning pill. +- Update status labels render the target version when available, for example `vX.Y.Z available`; fallback remains `Update available`. +- Theme update metadata now carries `new_version` from the WordPress theme update transient into active theme row state. + +## Badges + +- Badge rendering supports scalar badges such as `New` and structured badges such as `{ type, label, expires_at }`. +- Empty, malformed, duplicate, or expired badges are ignored. +- Badge class names are generated from badge type, for example `directorist-te-badge--new`, `--popular`, and `--trending`. +- Local badge fallback was added only for existing local product entries. API/filter-provided badge data remains the desired source of truth. +- Badge terms are searchable. +- Search highlights matching text in product titles and descriptions. +- Badge styling was reduced to keep row height compact. + +## Local Product Fallback Additions + +- Added local fallback entries for Directorist Notifications Pro and Directorist Divi Integration because the product API can return these products while the local fallback list did not previously include them. +- Added local images for those fallback products. +- These entries should remain display/catalog fallbacks only; live product metadata should still come from API/filter data when available. + +## Active Theme Handling + +- The active site theme row remains visible because it explains what theme controls the live WordPress site and provides access to Customize. +- If the active theme is a Directorist theme, type label is `Directorist theme`. +- If the active theme is not in the Directorist theme catalog, type label is `WordPress theme`. +- Active theme badge is `Active site theme`. +- Default WordPress themes are not presented as Directorist products. +- Active theme row keeps a single visible `Customize` action. Duplicate overflow `Customize` was removed. +- Theme activation remains high risk and should require explicit confirmation before calling `atbdp_activate_theme`. + +## Row Actions + +- Product detail actions are exposed directly instead of hiding essential discovery behind overflow. +- Demo actions are exposed directly where applicable. +- Active plugin rows use `Settings` as the primary action when no update is available. +- Active installed plugin overflow contains `Deactivate` only when applicable. +- Inactive installed plugin overflow uses `Delete plugin` as the destructive file-removal action. +- `Delete` remains protected by confirmation and should never be exposed as an easy primary action. +- Duplicate Settings/Customize-style actions should be avoided. + +## Selection And Bulk Actions + +- Rows with no safe bulk action render a disabled checkbox instead of silently omitting the selection cell. +- Master checkbox selects visible selectable rows only. +- Bulk bar no longer requires every selected item to share the same action. +- Bulk action visibility is now based on the union of eligible selected actions. +- Each visible bulk action shows a dynamic eligible-item count. +- Clicking a bulk action runs only the selected items that support that action; unsupported selected items are skipped. +- Button title/ARIA text explains how many selected items will run and how many will be skipped. +- Delete bulk action remains destructive and requires confirmation. +- Delete is limited to items marked eligible for uninstall/delete, not active plugins. +- Existing AJAX contracts are preserved: + - `atbdp_install_file_from_subscriptions` + - `atbdp_update_plugins` + - `atbdp_update_theme` + - `atbdp_plugins_bulk_action` + +## Connected View Responsive Fixes + +- Connected header is not sticky, preventing overlap with product rows. +- Connected nav can wrap/scroll on smaller screens without clipping the active label. +- Product titles and badges are allowed to wrap on narrow screens. +- Toolbar/search/count and bulk bar were checked for no horizontal overflow. + +## Compatibility Rules Preserved + +- Page slug remains `atbdp-extension`. +- Parent post type remains `at_biz_dir`. +- Existing AJAX action names and nonce patterns are preserved. +- Existing selectors/classes needed by legacy JS are preserved where actions still depend on them. +- Legacy user meta key `_atbdp_has_subscriptions_sassion` and AJAX action `atbdp_close_subscriptions_sassion` are preserved, including misspellings. +- Product filters and local fallback data remain part of the product merge path. + +## Known Follow-Up Notes + +- Many expensive actions still use full reload after server success. Future no-reload work should progressively enhance around existing AJAX responses and re-fetch canonical state. +- Connected header search remains static by decision and should be handled in a separate task. +- API badge source should move to Directorist.com/EDD product metadata so local fallback badges can eventually be removed. +- Build assets were not regenerated during these source edits unless explicitly stated in the task. diff --git a/docs/agents/directorist-themes-extensions-page/references/dynamic-data-contract.md b/docs/agents/directorist-themes-extensions-page/references/dynamic-data-contract.md index ec80c0074e..458a666c64 100644 --- a/docs/agents/directorist-themes-extensions-page/references/dynamic-data-contract.md +++ b/docs/agents/directorist-themes-extensions-page/references/dynamic-data-contract.md @@ -50,11 +50,13 @@ Theme catalog items commonly include: - Badge/status fields do not use hardcoded local fallback. Render badge/status only when provided by product API data or explicit filters. - Do not store the current merged catalog output in this skill. -Optional product badge/status shape for future rewrite: +Optional product badge/status shape: -- `badge.type`: machine-readable badge type such as `new`, `beta`, `popular`, `sale`, or `featured` -- `badge.label`: display label such as `New` -- `badge.expires_at`: optional expiration date/time; expired badges must not render +- Preferred API field: `badges`, an array of badge objects. +- Legacy/filter-compatible field: `badge`, a single badge object or scalar label. +- `type`: machine-readable badge type such as `new`, `beta`, `popular`, `sale`, `trending`, or `featured` +- `label`: display label such as `New` +- `expires_at`: optional expiration date/time; expired badges must not render Badge/status data should come from Directorist.com product API data. EDD product meta, a dedicated product badge setting, or taxonomy can be the upstream source on Directorist.com, but the core plugin should consume the API field. Local product-list filters may add or override badge data for compatibility/testing. Do not infer badges from product order, names, slugs, or local runtime state. diff --git a/docs/agents/directorist-themes-extensions-page/references/qa-checklist.md b/docs/agents/directorist-themes-extensions-page/references/qa-checklist.md index 6da30c8a5d..d385bf4461 100644 --- a/docs/agents/directorist-themes-extensions-page/references/qa-checklist.md +++ b/docs/agents/directorist-themes-extensions-page/references/qa-checklist.md @@ -10,10 +10,12 @@ Use this checklist for Themes & Extensions page work. Current runtime data must - Confirm new code preserves existing nonce usage or provides compatibility. - Confirm new copy is escaped and translatable in PHP. - Confirm changed selectors/classes do not remove legacy hooks without shims. +- Confirm page styles are scoped to the Themes & Extensions page root/classes and do not introduce broad global admin or shared Directorist selector changes. - Confirm docs/copy claims against `README.md`, `readme.txt`, and official Directorist docs. - Confirm product names, descriptions, thumbnails, and links use API values when present and local defaults when API fields are missing or invalid. - Confirm product badges/status render only from product API/filter-provided fields and do not rely on hardcoded product slugs or product order. - Confirm no old/new UI feature flag or rollout toggle was added unless explicitly requested. +- Confirm disconnected-view files/selectors/styles were not changed unless the current task explicitly requested disconnected-view changes. - Confirm disconnected-state username is first in tab order but is not autofocused on normal page load. - Confirm disconnected-state resource links are plain Docs, Tutorials, and Support links with external-link safety attributes. - Confirm disconnected-state account copy changes based on local official Directorist product detection: normal subscription copy when none are installed, installed-product copy when local Directorist extensions/themes are present. diff --git a/docs/agents/directorist-themes-extensions-page/references/rewrite-issue-register.md b/docs/agents/directorist-themes-extensions-page/references/rewrite-issue-register.md index 3cebc77d1a..0a38d2c5f0 100644 --- a/docs/agents/directorist-themes-extensions-page/references/rewrite-issue-register.md +++ b/docs/agents/directorist-themes-extensions-page/references/rewrite-issue-register.md @@ -22,6 +22,16 @@ The approved direction is a full UI rewrite of this page, not a staged old/new U - React should only be considered after an explicit larger admin UI migration decision, because it requires a new state/API layer and broader QA. - This architecture is the lowest-risk default for the 20k customer base: fewer moving parts, easier rollback, existing server-rendered fallback, and stable public contracts. +## Current Styling Source Note + +- The current rewritten Themes & Extensions page styles are in the direct page stylesheet `assets/css/directorist-themes-extensions.css`. +- They are not currently authored in `assets/src/scss/`. +- The stylesheet is enqueued directly for the `atbdp-extension` admin page from the asset loader. +- Styles for this page must remain scoped to the Themes & Extensions root/classes, such as `#directorist.directorist-te-page` and `.directorist-te-*`. Avoid broad selectors like `.wrap`, `.button`, `.notice`, `table`, or shared Directorist admin classes unless the change is intentionally global and verified across affected admin screens. +- Keep page-only design tokens and custom properties under the Themes & Extensions page root so they do not override other Directorist admin screens. +- Future rewrite or hardening work should decide explicitly whether to keep this page-specific direct CSS file or migrate the styles into the SCSS/build pipeline. +- Do not commit generated `assets/build/*` changes as part of this decision unless a build output update is intentionally requested. + ## Locked Reference Design Behavior When implementing from the approved HTML demo design, match the behavior as well as the static visuals. Do not store demo product rows or local runtime values as truth. @@ -44,9 +54,11 @@ When implementing from the approved HTML demo design, match the behavior as well ## Disconnected Account Policy +- Locked decision, 2026-07-22: disconnected view is done. Do not change it in future rewrite work unless the user explicitly requests disconnected-view changes in the current task. - Preserve current disconnected-account behavior by default. - Treat `logout` in this page context as Directorist account disconnect, not WordPress admin logout. - When `_atbdp_has_subscriptions_sassion` is empty, the page should render the account-connect form and the marketplace/promo discovery section. +- Keep disconnected-view PHP markup, copy, resource links, search/tabs/count placement, product-row actions, page-scoped CSS/responsive behavior, and JS behavior stable by default. - Do not expose installed premium product management in the disconnected state by default. - Do not show Settings, Active status, Activate, Deactivate, Update, or local management actions for installed premium plugins/themes while disconnected unless a future task explicitly changes this policy. - Do not show disconnected users management-only UI affordances such as header account utilities, Installed/Updates status filters, master select checkboxes, row selection, or bulk-action controls. Keep browsing affordances such as add-on search, type tabs, counts, product detail links, and Get It Now links. @@ -82,14 +94,17 @@ When implementing from the approved HTML demo design, match the behavior as well - Preferred remote API shape is a structured optional field: ```json -"badge": { - "type": "new", - "label": "New", - "expires_at": "2026-09-01" -} +"badges": [ + { + "type": "new", + "label": "New", + "expires_at": "2026-09-01" + } +] ``` -- `type` should be a machine-readable enum such as `new`, `beta`, `popular`, `sale`, or `featured`. +- `badge` as a single object or scalar label may remain supported for filters and backward compatibility. +- `type` should be a machine-readable enum such as `new`, `beta`, `popular`, `trending`, `sale`, or `featured`. - `label` should be the display text from the API and escaped/translated safely where applicable. - `expires_at` should be optional and handled server-side or client-side so expired badges do not render. - The core plugin UI should read badge/status values from the product API. EDD product meta, custom taxonomy, or a dedicated badge setting can be the upstream source on Directorist.com, but the Themes & Extensions page should depend on the API field, not direct EDD assumptions. @@ -99,6 +114,51 @@ When implementing from the approved HTML demo design, match the behavior as well - Do not infer `New` from product order, product name, local install date, or a hardcoded slug list. - Because the product catalog is cached, badge changes from the API must account for cache invalidation or acceptable cache delay. +### Public Marketing Page Badge Scrape + +Last refreshed scrape: 2026-07-22. + +Scraped from: + +- `https://directorist.com/extensions/` +- `https://directorist.com/themes/` + +This scrape is a temporary implementation reference only. Do not treat these product-to-badge mappings as canonical product data in the plugin. Re-scrape the marketing pages and, more importantly, re-check the product API before implementing or shipping badge behavior. + +Observed markup pattern: + +- Product cards expose badge machine values through `data-badges`, for example `data-badges="new trending"`. +- Visible badges render as spans such as `.badge-new`, `.badge-trending`, and `.badge-popular`. +- Observed badge labels: `New`, `Trending`, `Popular`. + +Observed extension badges: + +- Directorist Notifications Pro: `New` +- Directorist Divi Integration: `New` +- Directorist AI Search: `New` +- Directorist Listing Importer: `New`, `Trending` +- Directorist Search Alert: `New` +- Directorist Announcement: `New` +- AddonsKit for Bricks: `New` +- HelpGent Integration: `New` +- Digital Marketplace: `Trending` +- Job Manager: `Trending` +- Booking (Reservation & Appointment): `Popular` +- Listings with Map: `Popular` +- Business Hours: `Popular` +- WooCommerce Pricing Plans: `Popular` +- PayPal Payment Gateway: `Popular` +- Pricing Plans: `Popular`, `Trending` + +Observed theme badges: + +- dJobs: `New` +- dHotels: `Popular` +- dClassified: `Trending` +- OneListing Pro: `Popular`, `Trending` +- dCar: `Popular` +- dDoctors: `Popular` + ## Product Copy Source Policy - Product API data is preferred for product names, descriptions, thumbnails, product links, demo links, active promo flags, item IDs, and plugin bases when valid fields exist. diff --git a/includes/classes/class-extension.php b/includes/classes/class-extension.php index dfb442759b..432dc73b75 100644 --- a/includes/classes/class-extension.php +++ b/includes/classes/class-extension.php @@ -236,8 +236,39 @@ public function setup_products_list() { } } + private static function get_product_badge( $type, $label, $expires_at = null ) { + $badge = [ + 'type' => $type, + 'label' => $label, + ]; + + if ( null !== $expires_at ) { + $badge['expires_at'] = $expires_at; + } + + return $badge; + } + public static function get_default_extensions() { return [ + 'directorist-notifications-pro' => [ + 'name' => 'Directorist Notifications Pro', + 'description' => __( 'Send instant browser push notifications for listings, payments, reviews, renewals, and other important directory events.', 'directorist' ), + 'link' => 'https://directorist.com/product/directorist-notifications-pro/', + 'thumbnail' => ATBDP_URL . 'assets/images/extensions/directorist-notifications-pro.jpg', + 'active' => true, + 'item_id' => 371698, + 'badges' => [ self::get_product_badge( 'new', __( 'New', 'directorist' ) ) ], + ], + 'directorist-divi-integration' => [ + 'name' => 'Directorist Divi Integration', + 'description' => __( 'Build and customize your directory visually with native Divi 5 modules.', 'directorist' ), + 'link' => 'https://directorist.com/product/directorist-divi-integration/', + 'thumbnail' => ATBDP_URL . 'assets/images/extensions/directorist-divi-integration.jpg', + 'active' => true, + 'item_id' => 371246, + 'badges' => [ self::get_product_badge( 'new', __( 'New', 'directorist' ) ) ], + ], 'directorist-ai-search' => [ 'name' => 'Directorist AI Search', 'description' => __( 'AI-powered directory search that understands intent and improves listing discovery.', 'directorist' ), @@ -245,6 +276,7 @@ public static function get_default_extensions() { 'thumbnail' => ATBDP_URL . 'assets/images/extensions/AI-Search-Preview.jpg', 'active' => true, 'item_id' => 370908, + 'badges' => [ self::get_product_badge( 'new', __( 'New', 'directorist' ) ) ], ], 'directorist-listing-importer' => [ 'name' => 'Directorist Listing Importer', @@ -253,6 +285,10 @@ public static function get_default_extensions() { 'thumbnail' => ATBDP_URL . 'assets/images/extensions/directorist-listing-importer.png', 'active' => true, 'item_id' => 370853, + 'badges' => [ + self::get_product_badge( 'new', __( 'New', 'directorist' ) ), + self::get_product_badge( 'trending', __( 'Trending', 'directorist' ) ), + ], ], 'directorist-analytics' => [ @@ -289,6 +325,7 @@ public static function get_default_extensions() { 'thumbnail' => ATBDP_URL . 'assets/images/extensions/directorist-search-alert.png', 'active' => true, 'item_id' => 323908, + 'badges' => [ self::get_product_badge( 'new', __( 'New', 'directorist' ) ) ], ], 'directorist-announcement' => [ @@ -298,6 +335,7 @@ public static function get_default_extensions() { 'thumbnail' => ATBDP_URL . 'assets/images/extensions/directorist-announcement.svg', 'active' => true, 'item_id' => 308031, + 'badges' => [ self::get_product_badge( 'new', __( 'New', 'directorist' ) ) ], ], 'addonskit-for-bricks' => [ @@ -307,6 +345,7 @@ public static function get_default_extensions() { 'thumbnail' => ATBDP_URL . 'assets/images/extensions/addonskit-bricks.svg', 'active' => true, 'item_id' => 307581, + 'badges' => [ self::get_product_badge( 'new', __( 'New', 'directorist' ) ) ], ], 'directorist-coupon' => [ @@ -333,6 +372,7 @@ public static function get_default_extensions() { 'base' => 'directorist-listings-with-map/directorist-listings-map.php', 'active' => true, 'item_id' => 13794, + 'badges' => [ self::get_product_badge( 'popular', __( 'Popular', 'directorist' ) ) ], ], 'directorist-pricing-plans' => [ 'name' => 'Pricing Plans', @@ -341,6 +381,10 @@ public static function get_default_extensions() { 'thumbnail' => ATBDP_URL . 'assets/images/extensions/pricing-plans.png', 'active' => true, 'item_id' => 13776, + 'badges' => [ + self::get_product_badge( 'popular', __( 'Popular', 'directorist' ) ), + self::get_product_badge( 'trending', __( 'Trending', 'directorist' ) ), + ], ], 'directorist-woocommerce-pricing-plans' => [ 'name' => 'WooCommerce Pricing Plans', @@ -349,6 +393,7 @@ public static function get_default_extensions() { 'thumbnail' => ATBDP_URL . 'assets/images/extensions/woo-pricing-plans.png', 'active' => true, 'item_id' => 13784, + 'badges' => [ self::get_product_badge( 'popular', __( 'Popular', 'directorist' ) ) ], ], 'directorist-paypal' => [ 'name' => 'PayPal Payment Gateway', @@ -357,6 +402,7 @@ public static function get_default_extensions() { 'thumbnail' => ATBDP_URL . 'assets/images/extensions/paypal-gateway.png', 'active' => true, 'item_id' => 13702, + 'badges' => [ self::get_product_badge( 'popular', __( 'Popular', 'directorist' ) ) ], ], 'directorist-stripe' => [ 'name' => 'Stripe Payment Gateway', @@ -414,6 +460,7 @@ public static function get_default_extensions() { 'base' => 'directorist-business-hours/bd-business-hour.php', 'active' => true, 'item_id' => 13714, + 'badges' => [ self::get_product_badge( 'popular', __( 'Popular', 'directorist' ) ) ], ], 'directorist-slider-carousel' => [ 'name' => 'Listings Slider & Carousel', @@ -439,6 +486,7 @@ public static function get_default_extensions() { 'thumbnail' => ATBDP_URL . 'assets/images/extensions/booking.png', 'active' => true, 'item_id' => 21718, + 'badges' => [ self::get_product_badge( 'popular', __( 'Popular', 'directorist' ) ) ], ], 'directorist-gallery' => [ 'name' => 'Image Gallery', @@ -504,6 +552,7 @@ public static function get_default_extensions() { 'thumbnail' => ATBDP_URL . 'assets/images/extensions/jobs-manager.svg', 'active' => true, 'item_id' => 134332, + 'badges' => [ self::get_product_badge( 'trending', __( 'Trending', 'directorist' ) ) ], ], 'directorist-mailchimp-integration' => [ 'name' => 'Mailchimp Integration', @@ -520,6 +569,7 @@ public static function get_default_extensions() { 'thumbnail' => ATBDP_URL . 'assets/images/extensions/helpgent.svg', 'active' => true, 'item_id' => 188735, + 'badges' => [ self::get_product_badge( 'new', __( 'New', 'directorist' ) ) ], ], 'directorist-wpml-integration' => [ 'name' => 'WPML Integration', @@ -536,6 +586,7 @@ public static function get_default_extensions() { 'thumbnail' => ATBDP_URL . 'assets/images/extensions/marketplace.svg', 'active' => true, 'item_id' => 148417, + 'badges' => [ self::get_product_badge( 'trending', __( 'Trending', 'directorist' ) ) ], ], 'directorist-gamipress-integration' => [ 'name' => 'Gamipress Integration', @@ -558,6 +609,7 @@ public static function get_default_themes() { 'demo_link' => 'https://demo.directorist.com/theme/djobs/', 'thumbnail' => ATBDP_URL . 'assets/images/themes/djobs.png', 'active' => true, + 'badges' => [ self::get_product_badge( 'new', __( 'New', 'directorist' ) ) ], ], 'dhotels' => [ 'name' => 'dHotels', @@ -566,6 +618,7 @@ public static function get_default_themes() { 'demo_link' => 'https://demo.directorist.com/theme/dhotels/', 'thumbnail' => ATBDP_URL . 'assets/images/themes/dhotels.png', 'active' => true, + 'badges' => [ self::get_product_badge( 'popular', __( 'Popular', 'directorist' ) ) ], ], 'dclassified' => [ 'name' => 'dClassified', @@ -574,6 +627,7 @@ public static function get_default_themes() { 'demo_link' => 'https://demo.directorist.com/theme/dclassified/', 'thumbnail' => ATBDP_URL . 'assets/images/themes/dclassified.png', 'active' => true, + 'badges' => [ self::get_product_badge( 'trending', __( 'Trending', 'directorist' ) ) ], ], 'onelisting' => [ 'name' => 'OneListing', @@ -590,6 +644,10 @@ public static function get_default_themes() { 'demo_link' => 'https://demo.directorist.com/theme/onelisting-pro/', 'thumbnail' => ATBDP_URL . 'assets/images/themes/onelisting.png', 'active' => true, + 'badges' => [ + self::get_product_badge( 'popular', __( 'Popular', 'directorist' ) ), + self::get_product_badge( 'trending', __( 'Trending', 'directorist' ) ), + ], ], 'dplace' => [ 'name' => 'dPlace', @@ -622,6 +680,7 @@ public static function get_default_themes() { 'demo_link' => 'https://demo.directorist.com/theme/dcar/', 'thumbnail' => ATBDP_URL . 'assets/images/themes/dcar.png', 'active' => true, + 'badges' => [ self::get_product_badge( 'popular', __( 'Popular', 'directorist' ) ) ], ], 'dlist' => [ 'name' => 'dList', @@ -646,6 +705,7 @@ public static function get_default_themes() { 'demo_link' => 'https://demo.directorist.com/theme/ddoctors/', 'thumbnail' => ATBDP_URL . 'assets/images/themes/ddoctors.png', 'active' => true, + 'badges' => [ self::get_product_badge( 'popular', __( 'Popular', 'directorist' ) ) ], ], 'dlawyers' => [ 'name' => 'dLawyers', @@ -2646,7 +2706,7 @@ public function get_themes_overview() { $sovware_themes = ( is_array( $this->themes ) ) ? array_keys( $this->themes ) : []; $theme_updates = get_site_transient( 'update_themes' ); - $outdated_themes = $theme_updates->response; + $outdated_themes = ( is_object( $theme_updates ) && isset( $theme_updates->response ) && is_array( $theme_updates->response ) ) ? $theme_updates->response : array(); $outdated_themes_keys = ( is_array( $outdated_themes ) ) ? array_keys( $outdated_themes ) : []; $all_themes = wp_get_themes(); @@ -2660,13 +2720,29 @@ public function get_themes_overview() { if ( in_array( $theme_base, $sovware_themes ) ) { $customizer_link = "customize.php?theme={$theme_data->stylesheet}&return=%2Fwp-admin%2Fthemes.php"; $customizer_link = admin_url( $customizer_link ); + $has_theme_update = isset( $outdated_themes[ $theme_data->stylesheet ] ); + $theme_update_info = $has_theme_update ? $outdated_themes[ $theme_data->stylesheet ] : array(); + $theme_new_version = ''; + + if ( is_object( $theme_update_info ) ) { + $theme_update_info = get_object_vars( $theme_update_info ); + } + + if ( is_array( $theme_update_info ) ) { + if ( ! empty( $theme_update_info['new_version'] ) && is_scalar( $theme_update_info['new_version'] ) ) { + $theme_new_version = sanitize_text_field( $theme_update_info['new_version'] ); + } elseif ( ! empty( $theme_update_info['version'] ) && is_scalar( $theme_update_info['version'] ) ) { + $theme_new_version = sanitize_text_field( $theme_update_info['version'] ); + } + } $installed_theme_list[ $theme_base ] = [ 'name' => $theme_data->name, 'version' => $theme_data->version, 'thumbnail' => $theme_data->get_screenshot(), 'customizer_link' => $customizer_link, - 'has_update' => ( in_array( $theme_data->stylesheet, $outdated_themes_keys ) ) ? true : false, + 'has_update' => $has_theme_update, + 'new_version' => $theme_new_version, 'stylesheet' => $theme_data->stylesheet, ]; @@ -2674,7 +2750,7 @@ public function get_themes_overview() { $total_active_themes++; } - if ( in_array( $theme_base, $outdated_themes_keys ) ) { + if ( $has_theme_update ) { $total_outdated_themes++; } } @@ -2755,7 +2831,9 @@ public function get_current_active_theme_info( array $args = [] ) { $customizer_link = admin_url( $customizer_link ); // Check form theme update - $has_update = isset( $args['installed_theme_list'][ $current_active_theme->stylesheet ] ) ? $args['installed_theme_list'][ $current_active_theme->stylesheet ]['has_update'] : ''; + $active_theme_state = isset( $args['installed_theme_list'][ $current_active_theme->stylesheet ] ) ? $args['installed_theme_list'][ $current_active_theme->stylesheet ] : array(); + $has_update = isset( $active_theme_state['has_update'] ) ? $active_theme_state['has_update'] : ''; + $new_version = isset( $active_theme_state['new_version'] ) ? $active_theme_state['new_version'] : ''; $active_theme_info = [ 'name' => $current_active_theme->name, @@ -2763,6 +2841,7 @@ public function get_current_active_theme_info( array $args = [] ) { 'thumbnail' => $current_active_theme->get_screenshot(), 'customizer_link' => $customizer_link, 'has_update' => $has_update, + 'new_version' => $new_version, 'stylesheet' => $current_active_theme->stylesheet, ]; @@ -2877,6 +2956,14 @@ public static function remote_activate_license( $license_item = [] ) { return $status; } + private static function get_remote_auth_connection_error_message() { + return __( 'Could not reach Directorist.com. Please try again.', 'directorist' ); + } + + private static function get_remote_auth_invalid_credentials_message() { + return __( 'The username, email address, or password is incorrect. Please check your details and try again.', 'directorist' ); + } + // remote_authenticate_user public static function remote_authenticate_user( $user_credentials = [] ) { $status = [ 'success' => true ]; @@ -2903,14 +2990,20 @@ public static function remote_authenticate_user( $user_credentials = [] ) { $response = wp_remote_get( $url, $config ); if ( is_wp_error( $response ) ) { - $status['success'] = false; - $status['message'] = Directorist\Helper::get_first_wp_error_message( $response ); + $status['success'] = false; + $status['error_code'] = $response->get_error_code(); + $status['message'] = self::get_remote_auth_connection_error_message(); } else { + $response_code = wp_remote_retrieve_response_code( $response ); $response_body = is_string( $response['body'] ) ? json_decode( $response['body'], true ) : $response['body']; + + if ( empty( $response_body ) && in_array( $response_code, [ 401, 403 ], true ) ) { + $status['message'] = self::get_remote_auth_invalid_credentials_message(); + } } } catch ( Exception $e ) { $status['success'] = false; - $status['message'] = $e->getMessage(); + $status['message'] = self::get_remote_auth_connection_error_message(); } if ( is_array( $response_body ) ) { @@ -2919,6 +3012,10 @@ public static function remote_authenticate_user( $user_credentials = [] ) { if ( empty( $response_body['success'] ) ) { $status['success'] = false; + + if ( empty( $status['message'] ) && empty( $status['log'] ) ) { + $status['message'] = self::get_remote_auth_invalid_credentials_message(); + } } $status['response'] = $response_body; diff --git a/views/admin-templates/theme-extensions/auth/license-auth-section.php b/views/admin-templates/theme-extensions/auth/license-auth-section.php index 9d5cd4845d..61f7075057 100644 --- a/views/admin-templates/theme-extensions/auth/license-auth-section.php +++ b/views/admin-templates/theme-extensions/auth/license-auth-section.php @@ -8,7 +8,7 @@
@@ -29,4 +29,4 @@
-
\ No newline at end of file + diff --git a/views/admin-templates/theme-extensions/theme-extension.php b/views/admin-templates/theme-extensions/theme-extension.php index 6a64e61bb1..a90fc92632 100644 --- a/views/admin-templates/theme-extensions/theme-extension.php +++ b/views/admin-templates/theme-extensions/theme-extension.php @@ -43,25 +43,128 @@ return []; } - $badges = []; + $normalize_badge = static function( $badge_value ) { + $label = ''; + $type = ''; + $expires_at = ''; + + if ( is_array( $badge_value ) ) { + $label = $badge_value['label'] ?? $badge_value['name'] ?? ''; + $type = $badge_value['type'] ?? ''; + $expires_at = $badge_value['expires_at'] ?? ''; + + if ( is_scalar( $type ) && '' === trim( (string) $label ) && '' !== trim( (string) $type ) ) { + $label = ucwords( str_replace( [ '-', '_' ], ' ', (string) $type ) ); + } + } elseif ( is_scalar( $badge_value ) ) { + $label = $badge_value; + } + + if ( ! is_scalar( $label ) || '' === trim( (string) $label ) ) { + return null; + } + + $label = trim( (string) $label ); + $type = is_scalar( $type ) && '' !== trim( (string) $type ) ? sanitize_title( (string) $type ) : sanitize_title( $label ); + + if ( is_scalar( $expires_at ) && '' !== trim( (string) $expires_at ) ) { + $expires_timestamp = strtotime( (string) $expires_at ); + + if ( false !== $expires_timestamp && $expires_timestamp < time() ) { + return null; + } + } + + return [ + 'type' => $type ?: 'default', + 'label' => $label, + ]; + }; + + $badges = []; + $seen_badges = []; foreach ( [ 'badge', 'badges', 'product_badge', 'product_badges', 'product_status', 'release_status' ] as $badge_key ) { if ( empty( $product[ $badge_key ] ) ) { continue; } - $badge_values = is_array( $product[ $badge_key ] ) ? $product[ $badge_key ] : [ $product[ $badge_key ] ]; + $raw_badges = $product[ $badge_key ]; + $is_single = is_array( $raw_badges ) && array_intersect( [ 'label', 'name', 'type', 'expires_at' ], array_keys( $raw_badges ) ); + + $badge_values = is_array( $raw_badges ) && ! $is_single ? $raw_badges : [ $raw_badges ]; foreach ( $badge_values as $badge_value ) { - if ( is_array( $badge_value ) ) { - $badge_value = $badge_value['label'] ?? $badge_value['name'] ?? ''; + $badge = $normalize_badge( $badge_value ); + + if ( empty( $badge ) ) { + continue; } - if ( is_scalar( $badge_value ) && '' !== trim( (string) $badge_value ) ) { - $badges[] = trim( (string) $badge_value ); + $badge_id = strtolower( $badge['type'] . '|' . $badge['label'] ); + + if ( isset( $seen_badges[ $badge_id ] ) ) { + continue; } + + $seen_badges[ $badge_id ] = true; + $badges[] = $badge; } } - return array_values( array_unique( $badges ) ); + return $badges; +}; + +$normalize_search_text = static function( $value ) { + if ( ! is_scalar( $value ) ) { + return ''; + } + + $value = strtolower( remove_accents( wp_strip_all_tags( (string) $value ) ) ); + $value = preg_replace( '/[-_]+/', ' ', $value ); + $value = preg_replace( '/\s+/', ' ', $value ); + + return trim( (string) $value ); +}; + +$get_badge_search_terms = static function( $badges ) use ( $normalize_search_text ) { + if ( empty( $badges ) || ! is_array( $badges ) ) { + return []; + } + + $terms = []; + foreach ( $badges as $badge ) { + $badge_label = is_array( $badge ) ? ( $badge['label'] ?? '' ) : $badge; + $badge_type = is_array( $badge ) ? ( $badge['type'] ?? '' ) : ''; + + if ( is_scalar( $badge_label ) && '' !== trim( (string) $badge_label ) ) { + $terms[] = $normalize_search_text( $badge_label ); + } + + if ( is_scalar( $badge_type ) && '' !== trim( (string) $badge_type ) ) { + $terms[] = $normalize_search_text( $badge_type ); + } + } + + return array_values( array_unique( array_filter( $terms ) ) ); +}; + +$get_row_search_index = static function( $row ) use ( $normalize_search_text, $get_badge_search_terms ) { + $search_text = $normalize_search_text( + implode( + ' ', + [ + $row['name'] ?? '', + $row['description'] ?? '', + $row['typeLabel'] ?? '', + ] + ) + ); + $badge_terms = $get_badge_search_terms( $row['badges'] ?? [] ); + + return [ + 'text' => $search_text, + 'badge_text' => implode( ' ', $badge_terms ), + 'badge_terms' => wp_json_encode( $badge_terms ) ?: '[]', + ]; }; $get_link = static function( $product ) { @@ -117,6 +220,55 @@ } }; +$get_bulk_control = static function( $id, $item, $type, $actions, $class = '' ) { + $item = is_scalar( $item ) ? (string) $item : ''; + $type = is_scalar( $type ) ? (string) $type : ''; + $actions = array_values( array_unique( array_filter( array_map( 'sanitize_key', (array) $actions ) ) ) ); + + if ( '' === $item || empty( $actions ) || ! in_array( $type, [ 'plugin', 'theme' ], true ) ) { + return null; + } + + return [ + 'id' => $id, + 'name' => 'directorist_te_bulk[]', + 'class' => trim( 'directorist-te-select-checkbox directorist-te-check ' . $class ), + 'attrs' => [ + 'value' => $item, + 'data-bulk-item' => $item, + 'data-bulk-type' => $type, + 'data-bulk-actions' => implode( ' ', $actions ), + ], + ]; +}; + +$get_update_version = static function( $update ) { + if ( is_object( $update ) ) { + $update = get_object_vars( $update ); + } + + if ( ! is_array( $update ) ) { + return ''; + } + + foreach ( [ 'new_version', 'version' ] as $version_key ) { + if ( ! empty( $update[ $version_key ] ) && is_scalar( $update[ $version_key ] ) ) { + return ltrim( trim( (string) $update[ $version_key ] ), 'vV' ); + } + } + + return ''; +}; + +$get_update_status_label = static function( $version ) { + if ( is_scalar( $version ) && '' !== trim( (string) $version ) ) { + /* translators: %s: available product version. */ + return sprintf( __( 'v%s available', 'directorist' ), ltrim( trim( (string) $version ), 'vV' ) ); + } + + return __( 'Update available', 'directorist' ); +}; + $render_action = static function( $action ) use ( $render_attrs ) { if ( empty( $action['label'] ) ) { return; @@ -139,8 +291,11 @@ if ( empty( $label ) ) { return; } + + /* translators: %s: product status label. */ + $status_title = sprintf( __( 'Status: %s', 'directorist' ), $label ); ?> - + $is_beta ? __( 'Update Beta', 'directorist' ) : __( 'Update', 'directorist' ), @@ -194,13 +350,15 @@ ] ); - $menu = [ - [ + $menu = []; + + if ( $has_update && $is_active ) { + $menu[] = [ 'label' => __( 'Settings', 'directorist' ), 'href' => $args['settings_url'], 'class' => 'directorist-te-menu-link', - ], - ]; + ]; + } if ( $is_active ) { $menu[] = [ @@ -211,19 +369,28 @@ 'data-target' => $extension_base, ], ]; + } else { + $menu[] = [ + 'label' => __( 'Delete plugin', 'directorist' ), + 'class' => 'directorist-te-menu-link directorist-te-menu-link--danger ext-action-uninstall', + 'attrs' => [ 'data-target' => $extension_base ], + ]; } - $menu[] = [ - 'label' => __( 'Uninstall', 'directorist' ), - 'class' => 'directorist-te-menu-link directorist-te-menu-link--danger ext-action-uninstall', - 'attrs' => [ 'data-target' => $extension_base ], - ]; - $badges = $get_product_badges( $product ); if ( $is_required ) { $badges[] = __( 'Required', 'directorist' ); } + $bulk_actions = []; + if ( $has_update ) { + $bulk_actions[] = 'update'; + } + $bulk_actions[] = $is_active ? 'deactivate' : 'activate'; + if ( ! $is_active ) { + $bulk_actions[] = 'uninstall'; + } + $add_row( [ 'key' => 'extension-installed-' . $extension_base, @@ -237,11 +404,7 @@ 'statusLabel' => $status_label, 'primary' => $primary, 'menu' => $menu, - 'bulk' => [ - 'id' => $extension_base, - 'name' => $extension_base, - 'class' => 'extension-name-checkbox directorist-te-check', - ], + 'bulk' => $get_bulk_control( $extension_base, $extension_base, 'plugin', $bulk_actions, 'extension-name-checkbox' ), ] ); } @@ -252,18 +415,22 @@ $theme_key = ! empty( $active_theme['stylesheet'] ) ? $active_theme['stylesheet'] : sanitize_title( $active_theme['name'] ?? '' ); $product = $themes[ $theme_key ] ?? []; $has_update = ! empty( $active_theme['has_update'] ); + $update_version = $get_update_version( $active_theme ); + $is_directorist_active_theme = ! empty( $product ); $add_row( [ 'key' => 'theme-active-' . $theme_key, 'type' => 'theme', + 'typeLabel' => $is_directorist_active_theme ? __( 'Directorist theme', 'directorist' ) : __( 'WordPress theme', 'directorist' ), + 'typeClass' => $is_directorist_active_theme ? 'directorist-theme' : 'wordpress-theme', 'status' => $has_update ? 'update active installed' : 'active installed', 'name' => $active_theme['name'] ?? __( 'Active Theme', 'directorist' ), 'version' => $active_theme['version'] ?? '', 'description' => $product['description'] ?? __( 'Currently active WordPress theme for this site.', 'directorist' ), 'image' => ! empty( $active_theme['thumbnail'] ) ? $active_theme['thumbnail'] : $get_image( $product ), - 'badges' => array_merge( $get_product_badges( $product ), [ __( 'Active theme', 'directorist' ) ] ), - 'statusLabel' => $has_update ? __( 'Update available', 'directorist' ) : __( 'Active', 'directorist' ), + 'badges' => array_merge( $get_product_badges( $product ), [ __( 'Active site theme', 'directorist' ) ] ), + 'statusLabel' => $has_update ? $get_update_status_label( $update_version ) : __( 'Active', 'directorist' ), 'primary' => $has_update ? [ 'label' => $is_beta ? __( 'Update Beta', 'directorist' ) : __( 'Update', 'directorist' ), @@ -278,23 +445,19 @@ 'external' => true, 'icon' => 'la la-sliders', ], - 'menu' => [ - [ - 'label' => __( 'Customize', 'directorist' ), - 'href' => $active_theme['customizer_link'] ?? '#', - 'class' => 'directorist-te-menu-link', - 'external' => true, - ], - ], + 'menu' => [], + 'bulk' => $has_update ? $get_bulk_control( 'directorist-te-theme-update-' . sanitize_html_class( $theme_key ), $active_theme['stylesheet'] ?? $theme_key, 'theme', [ 'update' ] ) : null, ] ); } if ( $is_logged_in && ! empty( $args['extensions_available_in_subscriptions'] ) && is_array( $args['extensions_available_in_subscriptions'] ) ) { foreach ( $args['extensions_available_in_subscriptions'] as $extension_key => $extension ) { - $product = array_merge( $get_extension_product( $extension_key ), is_array( $extension ) ? $extension : [] ); + $extension = is_array( $extension ) ? $extension : []; + $product = array_merge( $get_extension_product( $extension_key ), $extension ); $name = $product['title'] ?? $product['name'] ?? $extension_key; $description = $product['description'] ?? ''; + $is_purchased = ! empty( $extension['purchased'] ); $add_row( [ @@ -317,6 +480,7 @@ ], 'details' => $is_purchased ? $get_details_action( $product ) : null, 'menu' => [], + 'bulk' => $get_bulk_control( 'directorist-te-install-' . sanitize_html_class( $extension_key ), $extension_key, 'plugin', [ 'install' ] ), ] ); } @@ -363,6 +527,7 @@ ], 'details' => $is_purchased ? $get_details_action( $product ) : null, 'menu' => [], + 'bulk' => $is_purchased ? $get_bulk_control( 'directorist-te-required-install-' . sanitize_html_class( $extension_key ), $extension_key, 'plugin', [ 'install' ] ) : null, ] ); } @@ -377,6 +542,8 @@ [ 'key' => 'theme-subscription-' . $theme_key, 'type' => 'theme', + 'typeLabel' => __( 'Directorist theme', 'directorist' ), + 'typeClass' => 'directorist-theme', 'status' => $is_installed ? 'installed' : 'not-installed', 'name' => $product['name'] ?? $theme_key, 'version' => $product['version'] ?? '', @@ -414,6 +581,7 @@ : null, ] ), + 'bulk' => ! $is_installed ? $get_bulk_control( 'directorist-te-theme-install-' . sanitize_html_class( $theme_key ), $theme_key, 'theme', [ 'install' ] ) : null, ] ); } @@ -454,6 +622,8 @@ [ 'key' => 'theme-promo-' . $theme_key, 'type' => 'theme', + 'typeLabel' => __( 'Directorist theme', 'directorist' ), + 'typeClass' => 'directorist-theme', 'status' => 'marketplace not-installed', 'name' => $product['name'] ?? $theme_key, 'description' => $product['description'] ?? '', @@ -507,24 +677,77 @@ static function( $row ) { -