diff --git a/assets/css/jumper.css b/assets/css/jumper.css
deleted file mode 100644
index 865f20b8..00000000
--- a/assets/css/jumper.css
+++ /dev/null
@@ -1,408 +0,0 @@
-#wu-jumper {
- box-sizing: border-box;
- position: fixed;
- top: 25%;
- left: 50%;
- width: 500px;
- margin-left: -250px;
- background: #f9f9f9;
- z-index: 99999;
- border-radius: 5px;
- border: solid 1px #ccc;
- -webkit-box-shadow: 0 0 5000px 5000px rgba(0, 0, 0, 0.2);
- -moz-box-shadow: 0 0 5000px 5000px rgba(0, 0, 0, 0.2);
- box-shadow: 0 0 5000px 5000px rgba(0, 0, 0, 0.2);
- /* @group Base */
- /* @end */
- /* @group Single Chosen */
- /* @end */
- /* @group Results */
- /* @end */
- /* @group Multi Chosen */
- /* @end */
- /* @end */
- /* @group Disabled Support */
- /* @end */
- /* @group Retina compatibility */
- /* @end */
-}
-#wu-jumper .wu-jumper-icon-container::before {
- font-family: dashicons-wu, sans-serif !important;
- content: "\e900";
- position: absolute;
- top: 50%;
- right: 15px;
- font-size: 36px;
- margin-top: -18px;
- height: 36px;
- line-height: 36px;
-}
-#wu-jumper * {
- box-sizing: border-box;
-}
-#wu-jumper label {
- display: block;
- text-transform: uppercase;
- padding: 20px;
-}
-#wu-jumper .selectize-control {
- width: 100%;
-}
-#wu-jumper .wu-chosen-container {
- position: relative;
- display: inline-block;
- vertical-align: middle;
- font-size: 13px;
- zoom: 1;
- -webkit-user-select: none;
- -moz-user-select: none;
- user-select: none;
-}
-#wu-jumper .wu-chosen-container-single .wu-chosen-drop {
- margin-top: -1px;
- border-radius: 0 0 4px 4px;
- background-clip: padding-box;
- border: solid 1px #ccc;
- border-top: none;
-}
-#wu-jumper .wu-chosen-container .wu-chosen-drop {
- position: absolute;
- top: 100%;
- left: -9999px;
- z-index: 1010;
- width: 100%;
- border-top: 0;
- background: #fff;
-}
-#wu-jumper .wu-chosen-container.wu-chosen-with-drop .wu-chosen-drop {
- left: 0;
-}
-#wu-jumper .wu-chosen-container * {
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-#wu-jumper .wu-chosen-container a {
- cursor: pointer;
-}
-#wu-jumper .wu-chosen-container .search-choice .group-name,
-#wu-jumper .wu-chosen-container .wu-chosen-single .group-name {
- margin-right: 4px;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- font-weight: normal;
- color: #999;
-}
-#wu-jumper .wu-chosen-container .search-choice .group-name::after,
-#wu-jumper .wu-chosen-container .wu-chosen-single .group-name::after {
- content: ":";
- padding-left: 2px;
- vertical-align: top;
-}
-#wu-jumper .selectize-control.single .selectize-input::after {
- display: none !important;
-}
-#wu-jumper .selectize-dropdown {
- margin-top: 0 !important;
- background-color: #fcfcfc;
- border: solid 1px #ddd;
-}
-#wu-jumper .selectize-input {
- margin-bottom: -7px;
- position: relative;
- display: block;
- overflow: hidden;
- color: #444;
- border: none;
- box-shadow: none;
- background: none;
- text-decoration: none;
- white-space: nowrap;
- line-height: 70px;
- padding: 0 15px;
-}
-#wu-jumper .selectize-input .item {
- font-size: 18pt;
-}
-#wu-jumper .selectize-input input {
- font-size: 18pt;
-}
-#wu-jumper .selectize-input.single::after {
- display: none !important;
-}
-#wu-jumper .wu-chosen-container-single .wu-chosen-default {
- color: #999;
-}
-#wu-jumper .wu-chosen-container-single .wu-chosen-single span {
- display: block;
- overflow: hidden;
- margin-right: 26px;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-#wu-jumper .wu-chosen-container-single .wu-chosen-single-with-deselect span {
- margin-right: 38px;
-}
-#wu-jumper .wu-chosen-container-single .wu-chosen-single abbr {
- position: absolute;
- top: 6px;
- right: 26px;
- display: block;
- width: 12px;
- height: 12px;
- font-size: 1px;
-}
-#wu-jumper .wu-chosen-container-single .wu-chosen-single abbr:hover {
- background-position: -42px -10px;
-}
-#wu-jumper .wu-chosen-container-single.wu-chosen-disabled .wu-chosen-single abbr:hover {
- background-position: -42px -10px;
-}
-#wu-jumper .wu-chosen-container-single .wu-chosen-single div {
- position: absolute;
- top: 0;
- right: 0;
- display: block;
- width: 18px;
- height: 100%;
-}
-#wu-jumper .wu-chosen-container-single .wu-chosen-single div b {
- display: block;
- width: 100%;
- height: 100%;
-}
-#wu-jumper .wu-chosen-container-single .wu-chosen-search {
- position: relative;
- z-index: 1010;
- margin: 0;
- white-space: nowrap;
-}
-#wu-jumper .wu-chosen-container-single .wu-chosen-search input[type=text] {
- margin: 0;
- padding: 4px 20px 4px 5px;
- width: 100%;
- height: auto;
- outline: 0;
- box-shadow: none !important;
- background-color: #fcfcfc;
- border: none;
- border-bottom: solid 1px #ddd;
- border-top: solid 1px #ddd;
- font-size: 1em;
- font-family: sans-serif;
- line-height: normal;
- border-radius: 0;
-}
-#wu-jumper .wu-chosen-container-single.wu-chosen-container-single-nosearch .wu-chosen-search {
- position: absolute;
- left: -9999px;
-}
-#wu-jumper .wu-chosen-container .wu-chosen-results {
- color: #444;
- position: relative;
- overflow-x: hidden;
- overflow-y: auto;
- margin: 0 4px 4px 0;
- padding: 0 0 0 4px;
- max-height: 240px;
- -webkit-overflow-scrolling: touch;
-}
-#wu-jumper .wu-chosen-container .wu-chosen-results li {
- display: none;
- margin: 0;
- padding: 15px 6px;
- list-style: none;
- line-height: 15px;
- word-wrap: break-word;
- -webkit-touch-callout: none;
-}
-#wu-jumper .wu-chosen-container .wu-chosen-results li.active-result {
- display: list-item;
- cursor: pointer;
-}
-#wu-jumper .wu-chosen-container .wu-chosen-results li.disabled-result {
- display: list-item;
- color: #ccc;
- cursor: default;
-}
-#wu-jumper .wu-chosen-container .wu-chosen-results li.highlighted {
- color: #fff;
-}
-#wu-jumper .wu-chosen-container .wu-chosen-results li.no-results {
- color: #777;
- display: list-item;
- background: #f4f4f4;
-}
-#wu-jumper .wu-chosen-container .wu-chosen-results li.group-result {
- display: list-item;
- font-weight: bold;
- cursor: default;
-}
-#wu-jumper .wu-chosen-container .wu-chosen-results li.group-option {
- padding-left: 15px;
-}
-#wu-jumper .wu-chosen-container .wu-chosen-results li em {
- font-style: normal;
- text-decoration: underline;
-}
-#wu-jumper .wu-chosen-container-multi .wu-chosen-choices {
- position: relative;
- overflow: hidden;
- margin: 0;
- padding: 0 5px;
- width: 100%;
- height: auto !important;
- height: 1%;
- background-color: #fff;
- cursor: text;
-}
-#wu-jumper .wu-chosen-container-multi .wu-chosen-choices li {
- float: left;
- list-style: none;
-}
-#wu-jumper .wu-chosen-container-multi .wu-chosen-choices li.search-field {
- margin: 0;
- padding: 0;
- white-space: nowrap;
-}
-#wu-jumper .wu-chosen-container-multi .wu-chosen-choices li.search-field input[type=text] {
- margin: 1px 0;
- padding: 0;
- height: 25px;
- outline: 0;
- background: transparent !important;
- color: #999;
- font-size: 100%;
- font-family: sans-serif;
- line-height: normal;
- border-radius: 0;
-}
-#wu-jumper .wu-chosen-container-multi .wu-chosen-choices li.search-choice {
- position: relative;
- margin: 3px 5px 3px 0;
- padding: 3px 20px 3px 5px;
- max-width: 100%;
- border-radius: 3px;
- background-color: #eee;
- background-size: 100% 19px;
- background-repeat: repeat-x;
- background-clip: padding-box;
- color: #333;
- line-height: 13px;
- cursor: default;
-}
-#wu-jumper .wu-chosen-container-multi .wu-chosen-choices li.search-choice span {
- word-wrap: break-word;
-}
-#wu-jumper .wu-chosen-container-multi .wu-chosen-choices li.search-choice-focus .search-choice-close {
- background-position: -42px -10px;
-}
-#wu-jumper .wu-chosen-container-multi .wu-chosen-choices li.search-choice .search-choice-close {
- position: absolute;
- top: 4px;
- right: 3px;
- display: block;
- width: 12px;
- height: 12px;
- font-size: 1px;
-}
-#wu-jumper .wu-chosen-container-multi .wu-chosen-choices li.search-choice .search-choice-close:hover {
- background-position: -42px -10px;
-}
-#wu-jumper .wu-chosen-container-multi .wu-chosen-choices li.search-choice-disabled {
- padding-right: 5px;
- background-color: #e4e4e4;
- color: #666;
-}
-#wu-jumper .wu-chosen-container-multi .wu-chosen-choices li.search-choice-focus {
- background: #d4d4d4;
-}
-#wu-jumper .wu-chosen-container-multi .wu-chosen-results {
- margin: 0;
- padding: 0;
-}
-#wu-jumper .wu-chosen-container-multi .wu-chosen-drop .result-selected {
- display: list-item;
- color: #ccc;
- cursor: default;
-}
-#wu-jumper .wu-chosen-disabled .wu-chosen-single {
- cursor: default;
-}
-#wu-jumper .wu-chosen-container-active.wu-chosen-with-drop .wu-chosen-single {
- -moz-border-radius-bottomright: 0;
- border-bottom-right-radius: 0;
- -moz-border-radius-bottomleft: 0;
- border-bottom-left-radius: 0;
-}
-#wu-jumper .wu-chosen-container-active.wu-chosen-with-drop .wu-chosen-single div {
- border-left: none;
- background: transparent;
-}
-#wu-jumper .wu-chosen-container-active.wu-chosen-with-drop .wu-chosen-single div b {
- background-position: -18px 2px;
-}
-#wu-jumper .wu-chosen-container-active .wu-chosen-choices li.search-field input[type=text] {
- color: #222 !important;
-}
-#wu-jumper .wu-chosen-disabled {
- opacity: 0.5 !important;
- cursor: default;
-}
-#wu-jumper .wu-chosen-disabled .wu-chosen-container-multi.wu-chosen-choices li.search-choice .search-choice-close {
- cursor: default;
-}
-@media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 144dpi), only screen and (min-resolution: 1.5dppx) {
- #wu-jumper .wu-chosen-rtl .wu-chosen-search input[type=text],
- #wu-jumper .wu-chosen-container-single .wu-chosen-single abbr,
- #wu-jumper .wu-chosen-container-single .wu-chosen-single div b,
- #wu-jumper .wu-chosen-container-single .wu-chosen-search input[type=text],
- #wu-jumper .wu-chosen-container-multi .wu-chosen-choices .search-choice .search-choice-close,
- #wu-jumper .wu-chosen-container .wu-chosen-results-scroll-down span,
- #wu-jumper .wu-chosen-container .wu-chosen-results-scroll-up span {
- background-size: 52px 37px !important;
- background-repeat: no-repeat !important;
- }
-}
-
-#wu_jumper_select_chosen { /* stylelint-disable-line selector-id-pattern */
- margin: 0;
- width: 100% !important;
- padding: 0;
-}
-
-#wu_jumper_select_chosen input { /* stylelint-disable-line selector-id-pattern */
- outline: none;
- width: 100%;
- padding: 20px !important;
-}
-
-span.wu-keys {
- margin-right: 20px;
-}
-
-span.wu-keys-key {
- display: inline-block;
- padding: 0 5px 1px;
- border: solid 2px #999;
- border-radius: 3px;
- margin: 0 2px;
-}
-
-.wu-jumper-loading,
-.wu-jumper-redirecting {
- border-top: solid 1px #cecece;
- padding: 10px;
- text-align: center;
- display: none;
-}
-
-body.rtl #wu-jumper .wu-chosen-container-single .wu-chosen-single::after {
- right: auto;
- left: 15px;
-}
-body.rtl #wu-jumper .wu-chosen-container-single .wu-chosen-single span {
- margin-right: 0;
- margin-left: 26px;
-}
\ No newline at end of file
diff --git a/assets/js/command-palette.js b/assets/js/command-palette.js
new file mode 100644
index 00000000..58a228d6
--- /dev/null
+++ b/assets/js/command-palette.js
@@ -0,0 +1,335 @@
+/**
+ * Command Palette Integration for Ultimate Multisite
+ *
+ * Registers dynamic commands for searching entities using WordPress Command Palette.
+ *
+ * @package WP_Ultimo
+ * @since 2.1.0
+ */
+
+(function (wp) {
+ 'use strict';
+
+ // Check if wp.commands is available
+ if (!wp || !wp.commands || !wp.element) {
+ console.log('[Ultimate Multisite] Command palette not available - wp.commands or wp.element missing');
+ return;
+ }
+
+ console.log('[Ultimate Multisite] Command palette API detected, initializing...');
+
+ const { useState, useEffect, useRef } = wp.element;
+ const { __ } = wp.i18n;
+ const apiFetch = wp.apiFetch;
+
+ // Get configuration from localized script
+ const config = window.wuCommandPalette || {};
+ const entities = config.entities || {};
+ const restUrl = config.restUrl || '';
+ const networkAdminUrl = config.networkAdminUrl || '';
+ const customLinks = config.customLinks || [];
+
+ console.log('[Ultimate Multisite] Configuration loaded:', {
+ entitiesCount: Object.keys(entities).length,
+ restUrl: restUrl,
+ customLinksCount: customLinks.length,
+ entities: entities
+ });
+
+ /**
+ * Custom hook for searching entities.
+ * Called by the command palette with search parameter.
+ *
+ * @param {Object} params Parameters object.
+ * @param {string} params.search The search term.
+ * @return {Object} Commands and loading state.
+ */
+ function useEntitySearch({ search }) {
+ const [commands, setCommands] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const searchTimeoutRef = useRef(null);
+ const cacheRef = useRef({});
+
+ useEffect(() => {
+ // Clear any pending timeouts
+ if (searchTimeoutRef.current) {
+ clearTimeout(searchTimeoutRef.current);
+ }
+
+ // Minimum 2 characters required
+ if (!search || search.length < 2) {
+ setCommands([]);
+ setIsLoading(false);
+ return;
+ }
+
+ // Check cache
+ if (cacheRef.current[search]) {
+ setCommands(cacheRef.current[search]);
+ setIsLoading(false);
+ return;
+ }
+
+ // Debounce: wait 300ms before searching
+ setIsLoading(true);
+
+ searchTimeoutRef.current = setTimeout(() => {
+ const searchUrl = restUrl + '?' + new URLSearchParams({
+ query: search,
+ limit: 15
+ }).toString();
+
+ console.log('[Ultimate Multisite] Searching:', searchUrl);
+
+ apiFetch({
+ url: searchUrl
+ })
+ .then((response) => {
+ console.log('[Ultimate Multisite] Search response:', response);
+ const results = response.results || [];
+
+ const cmds = results.map((result) => {
+ const cmd = {
+ name: 'ultimate-multisite/' + result.type + '-' + result.id,
+ label: result.title,
+ searchLabel: result.title + ' ' + result.subtitle,
+ callback: ({ close }) => {
+ close();
+ window.location.href = result.url;
+ }
+ };
+
+ // Add icon if available
+ const icon = getIcon(result.icon);
+ if (icon) {
+ cmd.icon = icon;
+ }
+
+ return cmd;
+ });
+
+ // Cache the results
+ cacheRef.current[search] = cmds;
+
+ console.log('[Ultimate Multisite] Commands generated:', cmds.length);
+ setCommands(cmds);
+ setIsLoading(false);
+ })
+ .catch((error) => {
+ console.error('[Ultimate Multisite] Search error:', error);
+ setCommands([]);
+ setIsLoading(false);
+ });
+ }, 300);
+
+ return () => {
+ if (searchTimeoutRef.current) {
+ clearTimeout(searchTimeoutRef.current);
+ }
+ };
+ }, [search]);
+
+ return {
+ commands,
+ isLoading
+ };
+ }
+
+ /**
+ * Get icon component for command palette.
+ *
+ * WordPress command palette expects icon components from @wordpress/icons package.
+ * We'll map dashicon names to WordPress icons when available.
+ *
+ * @param {string} icon Dashicon name.
+ * @return {Object|undefined} WordPress icon component or undefined.
+ */
+ function getIcon(icon) {
+ // Check if @wordpress/icons is available
+ if (!wp.icons) {
+ console.log('[Ultimate Multisite] wp.icons not available');
+ return undefined;
+ }
+
+ // Map dashicon names to @wordpress/icons
+ const iconMap = {
+ 'admin-users': wp.icons.people,
+ 'admin-multisite': wp.icons.layout,
+ 'id-alt': wp.icons.card,
+ 'money-alt': wp.icons.payment,
+ 'products': wp.icons.box,
+ 'networking': wp.icons.globe,
+ 'tickets-alt': wp.icons.tag,
+ 'rest-api': wp.icons.code,
+ 'megaphone': wp.icons.megaphone,
+ 'feedback': wp.icons.commentContent,
+ 'dashboard': wp.icons.home,
+ 'admin-settings': wp.icons.settings,
+ 'info': wp.icons.info,
+ 'external': wp.icons.external
+ };
+
+ const mappedIcon = iconMap[icon];
+ if (!mappedIcon) {
+ console.log('[Ultimate Multisite] Icon not found:', icon);
+ }
+
+ return mappedIcon || undefined;
+ }
+
+ /**
+ * Register the entity search command loader.
+ */
+ function registerEntitySearchLoader() {
+ if (!wp.data || !wp.data.dispatch || !wp.commands || !wp.commands.store) {
+ return;
+ }
+
+ const { registerCommandLoader } = wp.data.dispatch(wp.commands.store);
+
+ registerCommandLoader({
+ name: 'ultimate-multisite/entity-search',
+ hook: useEntitySearch
+ });
+
+ console.log('[Ultimate Multisite] Command loader registered');
+ }
+
+ /**
+ * Register static commands for custom links.
+ */
+ function registerCustomLinks() {
+ if (!customLinks || customLinks.length === 0) {
+ return;
+ }
+
+ const { registerCommand } = wp.data.dispatch(wp.commands.store);
+
+ customLinks.forEach((link, index) => {
+ const cmd = {
+ name: 'ultimate-multisite/custom-link-' + index,
+ label: link.title,
+ callback: ({ close }) => {
+ close();
+ window.location.href = link.url;
+ }
+ };
+
+ // Add external link icon
+ const icon = getIcon('external');
+ if (icon) {
+ cmd.icon = icon;
+ }
+
+ registerCommand(cmd);
+ });
+ }
+
+ /**
+ * Register static commands for Ultimate Multisite pages.
+ */
+ function registerStaticCommands() {
+ if (!wp.data || !wp.data.dispatch || !wp.commands || !wp.commands.store) {
+ return;
+ }
+
+ const { registerCommand } = wp.data.dispatch(wp.commands.store);
+
+ // Register common Ultimate Multisite pages
+ const staticCommands = [];
+
+ // Dashboard command
+ const dashboardCmd = {
+ name: 'ultimate-multisite/dashboard',
+ label: __('Ultimate Multisite Dashboard', 'ultimate-multisite'),
+ callback: ({ close }) => {
+ close();
+ window.location.href = networkAdminUrl + 'admin.php?page=wp-ultimo';
+ }
+ };
+ const dashboardIcon = getIcon('dashboard');
+ if (dashboardIcon) dashboardCmd.icon = dashboardIcon;
+ staticCommands.push(dashboardCmd);
+
+ // Settings command
+ const settingsCmd = {
+ name: 'ultimate-multisite/settings',
+ label: __('Ultimate Multisite Settings', 'ultimate-multisite'),
+ callback: ({ close }) => {
+ close();
+ window.location.href = networkAdminUrl + 'admin.php?page=wp-ultimo-settings';
+ }
+ };
+ const settingsIcon = getIcon('admin-settings');
+ if (settingsIcon) settingsCmd.icon = settingsIcon;
+ staticCommands.push(settingsCmd);
+
+ // System info command
+ const systemInfoCmd = {
+ name: 'ultimate-multisite/system-info',
+ label: __('System Information', 'ultimate-multisite'),
+ callback: ({ close }) => {
+ close();
+ window.location.href = networkAdminUrl + 'admin.php?page=wp-ultimo-system-info';
+ }
+ };
+ const systemInfoIcon = getIcon('info');
+ if (systemInfoIcon) systemInfoCmd.icon = systemInfoIcon;
+ staticCommands.push(systemInfoCmd);
+
+ // Register list pages for each entity type
+ Object.keys(entities).forEach((slug) => {
+ const entity = entities[slug];
+
+ const cmd = {
+ name: 'ultimate-multisite/list-' + slug,
+ label: entity.label_plural || entity.label + 's',
+ callback: ({ close }) => {
+ close();
+ window.location.href = networkAdminUrl + 'admin.php?page=wu-' + slug.replace('_', '-') + 's';
+ }
+ };
+
+ // Add icon if available
+ const icon = getIcon(entity.icon);
+ if (icon) {
+ cmd.icon = icon;
+ }
+
+ staticCommands.push(cmd);
+ });
+
+ // Register all static commands
+ staticCommands.forEach((command) => {
+ try {
+ registerCommand(command);
+ } catch (error) {
+ console.error('Error registering command:', command.name, error);
+ }
+ });
+ }
+
+ /**
+ * Initialize command palette integration.
+ */
+ function init() {
+ // Wait for DOM to be ready
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ return;
+ }
+
+ // Register entity search loader (dynamic commands)
+ registerEntitySearchLoader();
+
+ // Register static commands
+ registerStaticCommands();
+
+ // Register custom links
+ registerCustomLinks();
+ }
+
+ // Initialize
+ init();
+
+})(window.wp);
diff --git a/assets/js/jumper.js b/assets/js/jumper.js
deleted file mode 100644
index a9ed0067..00000000
--- a/assets/js/jumper.js
+++ /dev/null
@@ -1,165 +0,0 @@
-/* eslint-disable no-undef */
-(function($) {
-
- $(document).ready(function() {
-
- // Adds WUChosen.js to our custom select input
- const $jumper = $('#wu-jumper-select').selectize({
- create: false,
- maxItems: 1,
- optgroupField: 'group',
- optgroupValueField: 'value',
- searchField: ['text', 'name', 'display_name', 'domain', 'title', 'desc', 'code'],
- render: {
- option(option) {
-
- if (typeof option.model === 'undefined') {
-
- option.model = 'jumper-link';
-
- } // end if;
-
- if (typeof option.text === 'undefined') {
-
- option.text = option.reference_code || option.name || option.title || option.display_name || option.code;
-
- } // end if;
-
- if (typeof option.group === 'undefined') {
-
- option.group = option.model;
-
- } // end if;
-
- const template_html = jQuery('#wu-template-' + option.model).length ?
- jQuery('#wu-template-' + option.model).html() :
- jQuery('#wu-template-default').html();
-
- const template = _.template(template_html, {
- interpolate: /\{\{(.+?)\}\}/g,
- });
-
- return template(option);
-
- },
- },
- load(query, callback) {
-
- if (! query.length) {
-
- return callback();
-
- } // end if;
-
- $('#wu-jumper .wu-jumper-loading').show();
-
- jQuery.ajax({
- // eslint-disable-next-line no-undef
- url: wu_jumper_vars.ajaxurl,
- type: 'POST',
- data: {
- action: 'wu_search',
- model: 'all',
- number: 99,
- query: {
- search: '*' + query + '*',
- },
- },
- error() {
-
- callback();
-
- },
- success(res) {
-
- $('#wu-jumper .wu-jumper-loading').hide();
-
- callback(res);
-
- },
- });
-
- },
- });
-
- const is_local_url = function(url) {
-
- return url.toLowerCase().indexOf(wu_jumper_vars.base_url) >= 0 || url.toLowerCase().indexOf(wu_jumper_vars.network_base_url) >= 0;
-
- }; // end is_local_url
-
- // Every time the value changes, we need to redirect the user
- $jumper.on('change', function() {
-
- // Check if we need to open this in a new tab
- if (is_local_url($(this).val())) {
-
- window.location.href = $(this).val();
-
- $(this).parent().parent().find('.wu-jumper-redirecting').show();
-
- } else {
-
- window.open($(this).val(), '_blank');
-
- $($jumper.parent()).hide();
-
- } // end if;
-
- });
-
- // Closes on clicking other elements
- $(document).on('click', ':not(#wu-jumper-button-trigger)', function(e) {
-
- const target = e.target;
-
- if ($(target).attr('id') === 'wu-jumper-button-trigger' || $(target).parent().attr('id') === 'wu-jumper-button-trigger') {
-
- return;
-
- } // end if;
-
- if (! $(target).is($jumper.parent()) && ! $(target).parents().is($jumper.parent())) {
-
- $($jumper.parent().parent()).hide();
-
- } // end if;
-
- });
-
- const trigger_key = wu_jumper_vars.trigger_key.charAt(0);
-
- // Our bar is hidden by default, we need to display it when a certain shortcut is pressed
- Mousetrap.bind(['command+option+' + trigger_key, 'ctrl+alt+' + trigger_key], function(e) {
-
- e.preventDefault();
-
- open_jumper();
-
- }); // end mousetrap;
-
- $(document).on('click', '#wu-jumper-button-trigger', function(e) {
-
- e.preventDefault();
-
- open_jumper();
-
- });
-
- /**
- * Actually opens the jumper.
- */
- function open_jumper() {
-
- $('#wu-jumper').show();
-
- $('#wu-jumper').find('input').focus();
-
- return false;
-
- } // end open_jumper;
-
- });
-
-}(jQuery));
-
diff --git a/inc/apis/class-command-palette-rest-controller.php b/inc/apis/class-command-palette-rest-controller.php
new file mode 100644
index 00000000..1ed19528
--- /dev/null
+++ b/inc/apis/class-command-palette-rest-controller.php
@@ -0,0 +1,266 @@
+namespace,
+ '/' . $this->rest_base . '/search',
+ [
+ [
+ 'methods' => \WP_REST_Server::READABLE,
+ 'callback' => [$this, 'search'],
+ 'permission_callback' => [$this, 'search_permissions_check'],
+ 'args' => $this->get_search_params(),
+ ],
+ ]
+ );
+ }
+
+ /**
+ * Get the query params for search.
+ *
+ * @since 2.1.0
+ * @return array
+ */
+ public function get_search_params(): array {
+
+ return [
+ 'query' => [
+ 'description' => __('Search query string.', 'ultimate-multisite'),
+ 'type' => 'string',
+ 'required' => true,
+ 'sanitize_callback' => 'sanitize_text_field',
+ 'validate_callback' => function ($param) {
+ return strlen($param) >= 2;
+ },
+ ],
+ 'entity_type' => [
+ 'description' => __('Entity type to search (optional).', 'ultimate-multisite'),
+ 'type' => 'string',
+ 'required' => false,
+ 'sanitize_callback' => 'sanitize_key',
+ ],
+ 'limit' => [
+ 'description' => __('Maximum number of results to return.', 'ultimate-multisite'),
+ 'type' => 'integer',
+ 'required' => false,
+ 'default' => 15,
+ 'minimum' => 1,
+ 'maximum' => 20,
+ 'sanitize_callback' => 'absint',
+ ],
+ ];
+ }
+
+ /**
+ * Check permissions for search endpoint.
+ *
+ * @since 2.1.0
+ *
+ * @param \WP_REST_Request $request The request object.
+ * @return bool|\WP_Error
+ */
+ public function search_permissions_check($request): bool {
+ unset($request);
+
+ if (! current_user_can('manage_network')) {
+ return new \WP_Error(
+ 'rest_forbidden',
+ __('You do not have permission to search.', 'ultimate-multisite'),
+ ['status' => 403]
+ );
+ }
+
+ return true;
+ }
+
+ /**
+ * Handle search request.
+ *
+ * @since 2.1.0
+ *
+ * @param \WP_REST_Request $request The request object.
+ * @return \WP_REST_Response|\WP_Error
+ */
+ public function search($request) {
+
+ $query = $request->get_param('query');
+ $entity_type = $request->get_param('entity_type');
+ $limit = $request->get_param('limit') ?: 15;
+
+ // Minimum query length
+ if (strlen($query) < 2) {
+ return rest_ensure_response(
+ [
+ 'results' => [],
+ 'message' => __('Query must be at least 2 characters.', 'ultimate-multisite'),
+ ]
+ );
+ }
+
+ $results = [];
+
+ // Get registered entities from Command Palette Manager
+ $manager = \WP_Ultimo\UI\Command_Palette_Manager::get_instance();
+ $entities = $manager->get_registered_entities();
+
+ // If no entities registered, return empty results
+ if (empty($entities)) {
+ return rest_ensure_response(
+ [
+ 'results' => [],
+ 'total' => 0,
+ 'message' => __('No searchable entities registered yet.', 'ultimate-multisite'),
+ ]
+ );
+ }
+
+ // If entity_type is specified, search only that type
+ if (! empty($entity_type) && isset($entities[ $entity_type ])) {
+ $results = $this->search_entity_type($entity_type, $query, $limit);
+ } else {
+ // Search all entity types
+ $entity_count = count($entities);
+ $per_type_limit = max(1, floor($limit / $entity_count));
+
+ foreach ($entities as $slug => $config) {
+ $entity_results = $this->search_entity_type($slug, $query, $per_type_limit);
+ $results = array_merge($results, $entity_results);
+
+ // Stop if we've reached the limit
+ if (count($results) >= $limit) {
+ break;
+ }
+ }
+
+ // Trim to limit
+ $results = array_slice($results, 0, $limit);
+ }
+
+ /**
+ * Filter command palette search results.
+ *
+ * @since 2.1.0
+ *
+ * @param array $results Search results.
+ * @param string $query Search query.
+ * @param string $entity_type Entity type (empty if searching all).
+ */
+ $results = apply_filters('wu_command_palette_search_results', $results, $query, $entity_type);
+
+ return rest_ensure_response(
+ [
+ 'results' => $results,
+ 'total' => count($results),
+ ]
+ );
+ }
+
+ /**
+ * Search a specific entity type.
+ *
+ * @since 2.1.0
+ *
+ * @param string $entity_slug Entity slug.
+ * @param string $query Search query.
+ * @param int $limit Maximum results.
+ * @return array
+ */
+ protected function search_entity_type(string $entity_slug, string $query, int $limit): array {
+
+ // Get the manager class for this entity
+ $manager_class = $this->get_manager_class($entity_slug);
+
+ if (! $manager_class || ! class_exists($manager_class)) {
+ return [];
+ }
+
+ $manager = $manager_class::get_instance();
+
+ if (! $manager || ! method_exists($manager, 'search_for_command_palette')) {
+ return [];
+ }
+
+ // Check user capability
+ $config = $manager->get_command_palette_config();
+
+ if (! empty($config['capability']) && ! current_user_can($config['capability'])) {
+ return [];
+ }
+
+ return $manager->search_for_command_palette($query, $limit);
+ }
+
+ /**
+ * Get the manager class name for an entity slug.
+ *
+ * @since 2.1.0
+ *
+ * @param string $entity_slug Entity slug.
+ * @return string|null
+ */
+ protected function get_manager_class(string $entity_slug): ?string {
+
+ $manager_name = str_replace(' ', '_', ucwords(str_replace(['_', '-'], ' ', $entity_slug)));
+
+ $class_name = "\\WP_Ultimo\\Managers\\{$manager_name}_Manager";
+
+ return class_exists($class_name) ? $class_name : null;
+ }
+}
diff --git a/inc/apis/trait-command-palette.php b/inc/apis/trait-command-palette.php
new file mode 100644
index 00000000..2d89ceb9
--- /dev/null
+++ b/inc/apis/trait-command-palette.php
@@ -0,0 +1,339 @@
+command_palette_enabled) {
+ return;
+ }
+
+ // Register this entity with the Command Palette Manager
+ add_action('init', [$this, 'register_with_command_palette_manager'], 20);
+ }
+
+ /**
+ * Register this entity with the Command Palette Manager.
+ *
+ * @since 2.1.0
+ * @return void
+ */
+ public function register_with_command_palette_manager(): void {
+
+ $manager = \WP_Ultimo\UI\Command_Palette_Manager::get_instance();
+
+ if (! $manager) {
+ return;
+ }
+
+ $config = $this->get_command_palette_config();
+
+ if (empty($config)) {
+ return;
+ }
+
+ $manager->register_entity_type($this->slug, $config);
+ }
+
+ /**
+ * Get the command palette configuration for this entity.
+ * Managers can override this method to customize their configuration.
+ *
+ * @since 2.1.0
+ * @return array
+ */
+ public function get_command_palette_config(): array {
+
+ $display_name = $this->get_entity_display_name();
+
+ return [
+ 'label' => $display_name,
+ 'label_plural' => $this->get_entity_display_name_plural(),
+ 'icon' => $this->get_entity_icon(),
+ 'edit_url_pattern' => $this->get_edit_url_pattern(),
+ 'search_fields' => $this->get_search_fields(),
+ 'capability' => "wu_read_{$this->slug}",
+ ];
+ }
+
+ /**
+ * Get the display name for this entity (singular).
+ *
+ * @since 2.1.0
+ * @return string
+ */
+ protected function get_entity_display_name(): string {
+
+ $name = str_replace(['_', '-'], ' ', $this->slug);
+
+ return ucfirst($name);
+ }
+
+ /**
+ * Get the display name for this entity (plural).
+ *
+ * @since 2.1.0
+ * @return string
+ */
+ protected function get_entity_display_name_plural(): string {
+
+ return $this->get_entity_display_name() . 's';
+ }
+
+ /**
+ * Get the dashicon for this entity.
+ *
+ * @since 2.1.0
+ * @return string
+ */
+ protected function get_entity_icon(): string {
+
+ $icons = [
+ 'customer' => 'admin-users',
+ 'site' => 'admin-multisite',
+ 'membership' => 'id-alt',
+ 'payment' => 'money-alt',
+ 'product' => 'products',
+ 'domain' => 'networking',
+ 'discount_code' => 'tickets-alt',
+ 'webhook' => 'rest-api',
+ 'broadcast' => 'megaphone',
+ 'checkout_form' => 'feedback',
+ ];
+
+ return $icons[ $this->slug ] ?? 'admin-generic';
+ }
+
+ /**
+ * Get the edit URL pattern for this entity.
+ *
+ * @since 2.1.0
+ * @return string
+ */
+ protected function get_edit_url_pattern(): string {
+
+ return "admin.php?page=wp-ultimo-edit-{$this->slug}&id=%d";
+ }
+
+ /**
+ * Get the fields to search for this entity.
+ *
+ * @since 2.1.0
+ * @return array
+ */
+ protected function get_search_fields(): array {
+
+ $default_fields = ['name', 'display_name', 'title'];
+
+ // Entity-specific search fields
+ $entity_fields = [
+ 'customer' => ['display_name', 'email', 'user_login'],
+ 'site' => ['title', 'domain', 'path'],
+ 'membership' => ['reference_code', 'customer_id'],
+ 'payment' => ['reference_code', 'customer_id'],
+ 'product' => ['name', 'slug'],
+ 'domain' => ['domain', 'primary_domain'],
+ 'discount_code' => ['code', 'name'],
+ 'webhook' => ['name', 'webhook_url'],
+ 'broadcast' => ['title', 'slug'],
+ 'checkout_form' => ['name', 'slug'],
+ ];
+
+ return $entity_fields[ $this->slug ] ?? $default_fields;
+ }
+
+ /**
+ * Search entities for the command palette.
+ * This method is called by the REST controller.
+ *
+ * @since 2.1.0
+ *
+ * @param string $query The search query.
+ * @param int $limit Maximum number of results to return.
+ * @return array Array of search results.
+ */
+ public function search_for_command_palette(string $query, int $limit = 15): array {
+
+ if (strlen($query) < 2) {
+ return [];
+ }
+
+ // Use the existing model query function
+ $getter_function = "wu_get_{$this->slug}s";
+
+ if (! function_exists($getter_function)) {
+ return [];
+ }
+
+ $results = call_user_func(
+ $getter_function,
+ [
+ 'search' => "*{$query}*",
+ 'number' => $limit,
+ ]
+ );
+
+ if (empty($results)) {
+ return [];
+ }
+
+ return array_map([$this, 'format_result_for_command_palette'], $results);
+ }
+
+ /**
+ * Format a single result for the command palette.
+ *
+ * @since 2.1.0
+ *
+ * @param object $item The entity object.
+ * @return array Formatted result.
+ */
+ protected function format_result_for_command_palette($item): array {
+
+ $config = $this->get_command_palette_config();
+
+ $title = $this->get_item_title($item);
+ $subtitle = $this->get_item_subtitle($item);
+ $url = $this->get_item_url($item, $config['edit_url_pattern']);
+
+ return [
+ 'id' => $item->get_id(),
+ 'type' => $this->slug,
+ 'title' => $title,
+ 'subtitle' => $subtitle,
+ 'url' => $url,
+ 'icon' => $config['icon'],
+ ];
+ }
+
+ /**
+ * Get the display title for an item.
+ *
+ * @since 2.1.0
+ *
+ * @param object $item The entity object.
+ * @return string
+ */
+ protected function get_item_title($item): string {
+
+ if (method_exists($item, 'get_display_name')) {
+ return $item->get_display_name();
+ }
+
+ if (method_exists($item, 'get_title')) {
+ return $item->get_title();
+ }
+
+ if (method_exists($item, 'get_name')) {
+ return $item->get_name();
+ }
+
+ return sprintf('#%d', $item->get_id());
+ }
+
+ /**
+ * Get the subtitle/description for an item.
+ *
+ * @since 2.1.0
+ *
+ * @param object $item The entity object.
+ * @return string
+ */
+ protected function get_item_subtitle($item): string {
+
+ $parts = [];
+
+ // Add ID
+ // Translators: %d the id of the item.
+ $parts[] = sprintf(__('ID: %d', 'ultimate-multisite'), $item->get_id());
+
+ // Entity-specific subtitles
+ switch ($this->slug) {
+ case 'customer':
+ if (method_exists($item, 'get_email_address')) {
+ $parts[] = $item->get_email_address();
+ }
+ break;
+
+ case 'site':
+ if (method_exists($item, 'get_domain')) {
+ $parts[] = $item->get_domain();
+ }
+ break;
+
+ case 'membership':
+ case 'payment':
+ if (method_exists($item, 'get_reference_code')) {
+ $parts[] = $item->get_reference_code();
+ }
+ break;
+
+ case 'domain':
+ if (method_exists($item, 'get_domain')) {
+ $parts[] = $item->get_domain();
+ }
+ break;
+
+ case 'discount_code':
+ if (method_exists($item, 'get_code')) {
+ $parts[] = $item->get_code();
+ }
+ break;
+ }
+
+ return implode(' • ', array_filter($parts));
+ }
+
+ /**
+ * Get the edit URL for an item.
+ *
+ * @since 2.1.0
+ *
+ * @param object $item The entity object.
+ * @param string $pattern URL pattern with %d placeholder.
+ * @return string
+ */
+ protected function get_item_url($item, string $pattern): string {
+
+ $url = sprintf($pattern, $item->get_id());
+
+ return network_admin_url($url);
+ }
+}
diff --git a/inc/class-ajax.php b/inc/class-ajax.php
index 71cd4a04..0af45ac0 100644
--- a/inc/class-ajax.php
+++ b/inc/class-ajax.php
@@ -27,16 +27,6 @@ class Ajax implements \WP_Ultimo\Interfaces\Singleton {
* @since 2.0.0
*/
public function init(): void {
- /*
- * Load search endpoints.
- */
- add_action('wu_ajax_wu_search', [$this, 'search_models']);
-
- /*
- * Adds the Selectize templates to the admin_footer.
- */
- add_action('in_admin_footer', [$this, 'render_selectize_templates']);
-
/*
* Load search endpoints.
*/
@@ -77,341 +67,4 @@ public function refresh_list_table(): void {
do_action('wu_list_table_fetch_ajax_results', $table_id);
}
-
- /**
- * Search models using our ajax endpoint.
- *
- * @since 2.0.0
- * @return void
- */
- public function search_models(): void {
-
- /**
- * Fires before the processing of the search request.
- *
- * @since 2.0.0
- */
- do_action('wu_before_search_models');
-
- if (wu_request('model') === 'all') {
- $this->search_all_models();
-
- return;
- }
-
- $args = wp_parse_args(
- $_REQUEST, // phpcs:ignore WordPress.Security.NonceVerification.Recommended
- [
- 'model' => 'membership',
- 'query' => [],
- 'exclude' => [], // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude
- ]
- );
-
- $query = array_merge(
- $args['query'],
- [
- 'number' => -1,
- ]
- );
-
- if ($args['exclude']) {
- if (is_string($args['exclude'])) {
- $args['exclude'] = explode(',', $args['exclude']); // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude
-
- $args['exclude'] = array_map('trim', $args['exclude']); // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude
- }
-
- $query['id__not_in'] = $args['exclude'];
- }
-
- if (wu_get_isset($args, 'include')) {
- if (is_string($args['include'])) {
- $args['include'] = explode(',', $args['include']);
-
- $args['include'] = array_map('trim', $args['include']);
- }
-
- $query['id__in'] = $args['include'];
- }
-
- /*
- * Deal with site
- */
- if ('site' === $args['model']) {
- if (wu_get_isset($query, 'id__in')) {
- $query['blog_id__in'] = $query['id__in'];
-
- unset($query['id__in']);
- }
-
- if (wu_get_isset($query, 'id__not_in')) {
- $query['blog_id__not_in'] = $query['id__not_in'];
-
- unset($query['id__not_in']);
- }
- }
-
- $results = [];
-
- if ('user' === $args['model']) {
- $results = $this->search_wordpress_users($query);
- } elseif ('page' === $args['model']) {
- $results = get_posts(
- [
- 'post_type' => 'page',
- 'post_status' => 'publish',
- 'numberposts' => -1,
- 'exclude' => $query['id__not_in'] ?? '', // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude
- ]
- );
- } elseif ('setting' === $args['model']) {
- $results = $this->search_wp_ultimo_setting($query);
- } else {
- $model_func = 'wu_get_' . strtolower((string) $args['model']) . 's';
-
- if (function_exists($model_func)) {
- $results = $model_func($query);
- }
- }
-
- // Try search by hash if do not have any result
- if (empty($results)) {
- $model_func = 'wu_get_' . strtolower((string) $args['model']) . '_by_hash';
-
- if (function_exists($model_func)) {
- $result = $model_func(trim((string) $query['search'], '*'));
-
- $results = $result ? [$result] : [];
- }
- }
-
- wp_send_json($results);
-
- exit;
- }
-
- /**
- * Search all models for Jumper.
- *
- * @since 2.0.0
- * @return void
- */
- public function search_all_models(): void {
-
- $query = array_merge(
- wu_request('query', []),
- [
- 'number' => 10000,
- ]
- );
-
- $results_user = array_map(
- function ($item) {
-
- $item->model = 'user';
-
- $item->group = 'Users';
-
- $item->value = network_admin_url("user-edit.php?user_id={$item->ID}");
-
- return $item;
- },
- $this->search_wordpress_users($query)
- );
-
- $results_settings = array_map(
- function ($item) {
-
- $item['model'] = 'setting';
-
- $item['group'] = 'Settings';
-
- $item['value'] = $item['url'];
-
- return $item;
- },
- $this->search_wp_ultimo_setting($query)
- );
-
- $data = array_merge($results_user, $results_settings);
-
- /**
- * Allow plugin developers to add more search models functions.
- *
- * @since 2.0.0
- */
- $data_sources = apply_filters(
- 'wu_search_models_functions',
- [
- 'wu_get_customers',
- 'wu_get_products',
- 'wu_get_plans',
- 'wu_get_domains',
- 'wu_get_sites',
- 'wu_get_memberships',
- 'wu_get_payments',
- 'wu_get_broadcasts',
- 'wu_get_checkout_forms',
- ]
- );
-
- foreach ($data_sources as $function) {
- $results = call_user_func($function, $query);
-
- array_map(
- function ($item) {
-
- $url = str_replace('_', '-', (string) $item->model);
-
- $item->value = wu_network_admin_url(
- "wp-ultimo-edit-{$url}",
- [
- 'id' => $item->get_id(),
- ]
- );
-
- $item->group = ucwords((string) $item->model) . 's';
-
- return $item;
- },
- $results
- );
-
- $discount_codes = array_map(
- function ($item) {
-
- $discount = $item->to_array();
-
- $discount['value'] = wu_network_admin_url(
- 'wp-ultimo-edit-discount-code',
- [
- 'id' => $discount['id'],
- ]
- );
-
- $discount['group'] = 'Discount Codes';
-
- return $discount;
- },
- wu_get_discount_codes($query)
- );
-
- $data = array_merge($data, $results, $discount_codes);
- }
-
- wp_send_json($data);
- }
-
- /**
- * Search for Ultimate Multisite settings to help customers find them.
- *
- * @since 2.0.0
- *
- * @param array $query Query arguments.
- */
- public function search_wp_ultimo_setting($query): array {
-
- $sections = \WP_Ultimo\Settings::get_instance()->get_sections();
-
- $all_fields = [];
-
- foreach ($sections as $section_slug => $section) {
- $section['fields'] = array_map(
- function ($item) use ($section, $section_slug) {
-
- $item['section'] = $section_slug;
-
- $item['section_title'] = wu_get_isset($section, 'title', '');
-
- $item['url'] = wu_network_admin_url(
- 'wp-ultimo-settings',
- [
- 'tab' => $section_slug,
- ]
- ) . '#' . $item['setting_id'];
-
- return $item;
- },
- $section['fields']
- );
-
- $all_fields = array_merge($all_fields, $section['fields']);
- }
-
- $_settings = \Arrch\Arrch::find(
- $all_fields,
- [
- 'sort_key' => 'title',
- 'where' => [
- ['setting_id', '~', trim((string) $query['search'], '*')],
- ['type', '!=', 'header'],
- ],
- ]
- );
-
- return array_values($_settings);
- }
-
- /**
- * Handles the special case of searching native WP users.
- *
- * @since 2.0.0
- *
- * @param array $query Query arguments.
- * @return array
- */
- public function search_wordpress_users($query) {
-
- $results = get_users(
- [
- 'blog_id' => 0,
- 'search' => '*' . $query['search'] . '*',
- 'search_columns' => [
- 'ID',
- 'user_login',
- 'user_email',
- 'user_url',
- 'user_nicename',
- 'display_name',
- ],
- ]
- );
-
- $results = array_map(
- function ($item) {
-
- $item->data->user_pass = '';
-
- $item->data->avatar = get_avatar(
- $item->data->user_email,
- 40,
- 'identicon',
- '',
- [
- 'force_display' => true,
- 'class' => 'wu-rounded-full wu-mr-3',
- ]
- );
-
- return $item->data;
- },
- $results
- );
-
- return $results;
- }
-
- /**
- * Adds the selectize templates to the admin footer.
- *
- * @since 2.0.0
- * @return void
- */
- public function render_selectize_templates(): void {
-
- if (current_user_can('manage_network')) {
- wu_get_template('ui/selectize-templates');
- }
- }
}
diff --git a/inc/class-wp-ultimo.php b/inc/class-wp-ultimo.php
index 59c49438..31d1ce67 100644
--- a/inc/class-wp-ultimo.php
+++ b/inc/class-wp-ultimo.php
@@ -450,9 +450,14 @@ protected function load_extra_components(): void {
WP_Ultimo\Debug\Debug::get_instance();
/*
- * Loads the Jumper UI
+ * Loads the Command Palette integration
*/
- WP_Ultimo\UI\Jumper::get_instance();
+ WP_Ultimo\UI\Command_Palette_Manager::get_instance();
+
+ /*
+ * Loads the Command Palette REST controller
+ */
+ WP_Ultimo\Apis\Command_Palette_Rest_Controller::get_instance();
/*
* Loads the Template Previewer
diff --git a/inc/managers/class-broadcast-manager.php b/inc/managers/class-broadcast-manager.php
index 6c46fecc..68f329c6 100644
--- a/inc/managers/class-broadcast-manager.php
+++ b/inc/managers/class-broadcast-manager.php
@@ -28,6 +28,7 @@ class Broadcast_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api;
use \WP_Ultimo\Apis\WP_CLI;
use \WP_Ultimo\Apis\MCP_Abilities;
+ use \WP_Ultimo\Apis\Command_Palette;
use \WP_Ultimo\Traits\Singleton;
/**
@@ -60,6 +61,8 @@ public function init(): void {
$this->enable_mcp_abilities();
+ $this->enable_command_palette();
+
/**
* Add unseen broadcast notices to the panel.
*/
diff --git a/inc/managers/class-checkout-form-manager.php b/inc/managers/class-checkout-form-manager.php
index 8c611594..02457ef4 100644
--- a/inc/managers/class-checkout-form-manager.php
+++ b/inc/managers/class-checkout-form-manager.php
@@ -25,6 +25,7 @@ class Checkout_Form_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api;
use \WP_Ultimo\Apis\WP_CLI;
+ use \WP_Ultimo\Apis\Command_Palette;
use \WP_Ultimo\Traits\Singleton;
/**
@@ -54,5 +55,7 @@ public function init(): void {
$this->enable_rest_api();
$this->enable_wp_cli();
+
+ $this->enable_command_palette();
}
}
diff --git a/inc/managers/class-customer-manager.php b/inc/managers/class-customer-manager.php
index c4c25463..91c1e332 100644
--- a/inc/managers/class-customer-manager.php
+++ b/inc/managers/class-customer-manager.php
@@ -28,6 +28,7 @@ class Customer_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api;
use \WP_Ultimo\Apis\WP_CLI;
use \WP_Ultimo\Apis\MCP_Abilities;
+ use \WP_Ultimo\Apis\Command_Palette;
use \WP_Ultimo\Traits\Singleton;
/**
@@ -60,6 +61,8 @@ public function init(): void {
$this->enable_mcp_abilities();
+ $this->enable_command_palette();
+
add_action(
'init',
function () {
diff --git a/inc/managers/class-discount-code-manager.php b/inc/managers/class-discount-code-manager.php
index ec0f2e96..9ee288ce 100644
--- a/inc/managers/class-discount-code-manager.php
+++ b/inc/managers/class-discount-code-manager.php
@@ -27,6 +27,7 @@ class Discount_Code_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api;
use \WP_Ultimo\Apis\WP_CLI;
use \WP_Ultimo\Apis\MCP_Abilities;
+ use \WP_Ultimo\Apis\Command_Palette;
use \WP_Ultimo\Traits\Singleton;
/**
@@ -59,6 +60,8 @@ public function init(): void {
$this->enable_mcp_abilities();
+ $this->enable_command_palette();
+
add_action('wu_gateway_payment_processed', [$this, 'maybe_add_use_on_payment_received']);
}
diff --git a/inc/managers/class-domain-manager.php b/inc/managers/class-domain-manager.php
index 4273338b..f40757cc 100644
--- a/inc/managers/class-domain-manager.php
+++ b/inc/managers/class-domain-manager.php
@@ -29,6 +29,7 @@ class Domain_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api;
use \WP_Ultimo\Apis\WP_CLI;
use \WP_Ultimo\Apis\MCP_Abilities;
+ use \WP_Ultimo\Apis\Command_Palette;
use \WP_Ultimo\Traits\Singleton;
/**
@@ -135,6 +136,8 @@ public function init(): void {
$this->enable_mcp_abilities();
+ $this->enable_command_palette();
+
$this->set_cookie_domain();
add_action('plugins_loaded', [$this, 'load_integrations']);
diff --git a/inc/managers/class-membership-manager.php b/inc/managers/class-membership-manager.php
index fb5c416e..ba8cb9e5 100644
--- a/inc/managers/class-membership-manager.php
+++ b/inc/managers/class-membership-manager.php
@@ -27,6 +27,7 @@ class Membership_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api;
use \WP_Ultimo\Apis\WP_CLI;
use \WP_Ultimo\Apis\MCP_Abilities;
+ use \WP_Ultimo\Apis\Command_Palette;
use \WP_Ultimo\Traits\Singleton;
const LOG_FILE_NAME = 'memberships';
@@ -60,6 +61,8 @@ public function init(): void {
$this->enable_mcp_abilities();
+ $this->enable_command_palette();
+
add_action(
'init',
function () {
diff --git a/inc/managers/class-payment-manager.php b/inc/managers/class-payment-manager.php
index ac2861a2..9d340d80 100644
--- a/inc/managers/class-payment-manager.php
+++ b/inc/managers/class-payment-manager.php
@@ -31,6 +31,7 @@ class Payment_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api;
use \WP_Ultimo\Apis\WP_CLI;
use \WP_Ultimo\Apis\MCP_Abilities;
+ use \WP_Ultimo\Apis\Command_Palette;
use \WP_Ultimo\Traits\Singleton;
const LOG_FILE_NAME = 'payments';
@@ -64,6 +65,8 @@ public function init(): void {
$this->enable_mcp_abilities();
+ $this->enable_command_palette();
+
$this->register_forms();
add_action(
diff --git a/inc/managers/class-product-manager.php b/inc/managers/class-product-manager.php
index 89151c45..6a7f1e09 100644
--- a/inc/managers/class-product-manager.php
+++ b/inc/managers/class-product-manager.php
@@ -27,6 +27,7 @@ class Product_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api;
use \WP_Ultimo\Apis\WP_CLI;
use \WP_Ultimo\Apis\MCP_Abilities;
+ use \WP_Ultimo\Apis\Command_Palette;
use \WP_Ultimo\Traits\Singleton;
/**
@@ -58,5 +59,7 @@ public function init(): void {
$this->enable_wp_cli();
$this->enable_mcp_abilities();
+
+ $this->enable_command_palette();
}
}
diff --git a/inc/managers/class-site-manager.php b/inc/managers/class-site-manager.php
index 63233b38..c6b09b66 100644
--- a/inc/managers/class-site-manager.php
+++ b/inc/managers/class-site-manager.php
@@ -29,6 +29,7 @@ class Site_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api;
use \WP_Ultimo\Apis\WP_CLI;
use \WP_Ultimo\Apis\MCP_Abilities;
+ use \WP_Ultimo\Apis\Command_Palette;
use \WP_Ultimo\Traits\Singleton;
/**
@@ -61,6 +62,8 @@ public function init(): void {
$this->enable_mcp_abilities();
+ $this->enable_command_palette();
+
add_action('after_setup_theme', [$this, 'additional_thumbnail_sizes']);
add_action('wp_ajax_wu_get_screenshot', [$this, 'get_site_screenshot']);
diff --git a/inc/managers/class-webhook-manager.php b/inc/managers/class-webhook-manager.php
index 492eff17..e1db0eb7 100644
--- a/inc/managers/class-webhook-manager.php
+++ b/inc/managers/class-webhook-manager.php
@@ -27,6 +27,7 @@ class Webhook_Manager extends Base_Manager {
use \WP_Ultimo\Apis\Rest_Api;
use \WP_Ultimo\Apis\WP_CLI;
use \WP_Ultimo\Apis\MCP_Abilities;
+ use \WP_Ultimo\Apis\Command_Palette;
use \WP_Ultimo\Traits\Singleton;
/**
@@ -75,6 +76,8 @@ public function init(): void {
$this->enable_mcp_abilities();
+ $this->enable_command_palette();
+
add_action('init', [$this, 'register_webhook_listeners']);
add_action('wp_ajax_wu_send_test_event', [$this, 'send_test_event']);
diff --git a/inc/ui/class-command-palette-manager.php b/inc/ui/class-command-palette-manager.php
new file mode 100644
index 00000000..04f1d1e1
--- /dev/null
+++ b/inc/ui/class-command-palette-manager.php
@@ -0,0 +1,247 @@
+=');
+
+ /**
+ * Filter whether command palette is available.
+ *
+ * @since 2.1.0
+ *
+ * @param bool $is_available Whether command palette is available.
+ */
+ return apply_filters('wu_is_command_palette_available', $is_available);
+ }
+
+ /**
+ * Check if we should load command palette in current context.
+ *
+ * @since 2.1.0
+ * @return bool
+ */
+ protected function should_load_command_palette(): bool {
+
+ // Only in admin
+ if (! is_admin()) {
+ return false;
+ }
+
+ // Check feature availability
+ if (! $this->is_command_palette_available()) {
+ return false;
+ }
+
+ // Check user capability
+ if (! current_user_can('manage_network')) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Register an entity type for command palette search.
+ *
+ * @since 2.1.0
+ *
+ * @param string $slug Entity slug (e.g., 'customer', 'site').
+ * @param array $config Entity configuration.
+ * @return void
+ */
+ public function register_entity_type(string $slug, array $config): void {
+
+ $this->registered_entities[ $slug ] = $config;
+ }
+
+ /**
+ * Get all registered entity types.
+ *
+ * @since 2.1.0
+ * @return array
+ */
+ public function get_registered_entities(): array {
+
+ return $this->registered_entities;
+ }
+
+ /**
+ * Enqueue command palette scripts and styles.
+ *
+ * @since 2.1.0
+ * @return void
+ */
+ public function enqueue_scripts(): void {
+
+ if (! $this->should_load_command_palette()) {
+ return;
+ }
+
+ // Enqueue command palette integration
+ wp_enqueue_script(
+ 'wu-command-palette',
+ wu_get_asset('command-palette.js', 'js'),
+ ['wp-commands', 'wp-element', 'wp-i18n', 'wp-api-fetch', 'wp-components', 'wp-icons'],
+ wu_get_version(),
+ true
+ );
+
+ // Pass configuration to JavaScript
+ wp_localize_script(
+ 'wu-command-palette',
+ 'wuCommandPalette',
+ [
+ 'entities' => $this->registered_entities,
+ 'restUrl' => rest_url('ultimate-multisite/v1/command-palette/search'),
+ 'nonce' => wp_create_nonce('wp_rest'),
+ 'networkAdminUrl' => network_admin_url(),
+ 'customLinks' => $this->get_custom_links(),
+ 'i18n' => [
+ 'searchPlaceholder' => __('Search anything...', 'ultimate-multisite'),
+ 'noResults' => __('No results found', 'ultimate-multisite'),
+ 'minChars' => __('Type at least 2 characters to search', 'ultimate-multisite'),
+ ],
+ ]
+ );
+ }
+
+ /**
+ * Get custom links from settings.
+ *
+ * @since 2.1.0
+ * @return array
+ */
+ protected function get_custom_links(): array {
+
+ $saved_links = wu_get_setting('jumper_custom_links', '');
+
+ if (empty($saved_links)) {
+ return [];
+ }
+
+ $custom_links = [];
+ $lines = explode(PHP_EOL, (string) $saved_links);
+
+ foreach ($lines as $line) {
+ $line = trim($line);
+
+ if (empty($line)) {
+ continue;
+ }
+
+ // Format: Title : URL
+ $parts = explode(':', $line, 2);
+
+ if (count($parts) === 2) {
+ $title = trim($parts[0]);
+ $url = trim($parts[1]);
+
+ if (! empty($title) && ! empty($url)) {
+ $custom_links[] = [
+ 'title' => $title,
+ 'url' => $url,
+ ];
+ }
+ }
+ }
+
+ return $custom_links;
+ }
+
+ /**
+ * Add command palette settings.
+ *
+ * @since 2.1.0
+ * @return void
+ */
+ public function add_settings(): void {
+ wu_register_settings_section(
+ 'tools',
+ [
+ 'title' => __('Tools', 'ultimate-multisite'),
+ 'desc' => __('Tools and utilities for managing your network.', 'ultimate-multisite'),
+ 'icon' => 'dashicons-wu-tools',
+ ]
+ );
+
+ wu_register_settings_field(
+ 'tools',
+ 'command_palette_header',
+ [
+ 'title' => __('Command Palette', 'ultimate-multisite'),
+ 'desc' => __('Quick navigation and search using WordPress Command Palette (Ctrl/Cmd+K).', 'ultimate-multisite'),
+ 'type' => 'header',
+ ]
+ );
+
+ wu_register_settings_field(
+ 'tools',
+ 'jumper_custom_links',
+ [
+ 'title' => __('Custom Links', 'ultimate-multisite'),
+ 'desc' => __('Add custom links to the Command Palette. Add one per line, with the format "Title : URL".', 'ultimate-multisite'),
+ 'placeholder' => __('My Custom Link : https://example.com', 'ultimate-multisite'),
+ 'type' => 'textarea',
+ 'html_attr' => [
+ 'rows' => 4,
+ ],
+ ]
+ );
+ }
+}
diff --git a/inc/ui/class-jumper.php b/inc/ui/class-jumper.php
deleted file mode 100644
index db15aa42..00000000
--- a/inc/ui/class-jumper.php
+++ /dev/null
@@ -1,570 +0,0 @@
- $page,
- 'jumper' => $this,
- ]
- );
- }
-
- /**
- * Loads the necessary elements to display the Jumper.
- *
- * @since 2.0.0
- * @return void
- */
- public function load_jumper(): void {
-
- if ($this->is_jumper_enabled() && is_admin()) {
- add_action('wu_header_right', [$this, 'add_jumper_trigger']);
-
- add_action('admin_init', [$this, 'rebuild_menu']);
-
- add_action('admin_enqueue_scripts', [$this, 'enqueue_scripts']);
-
- add_action('admin_enqueue_scripts', [$this, 'enqueue_styles']);
-
- add_action('admin_footer', [$this, 'output']);
-
- add_filter('update_footer', [$this, 'add_jumper_footer_message'], 200);
-
- add_action('wu_after_save_settings', [$this, 'clear_jump_cache_on_save']);
-
- add_filter('wu_link_list', [$this, 'add_wp_ultimo_extra_links']);
-
- add_filter('wu_link_list', [$this, 'add_user_custom_links']);
- }
- }
-
- /**
- * Clear the jumper menu cache on settings save
- *
- * We need to do this to make sure that we clear the menu when the admin
- * adds a new custom menu item.
- *
- * @since 2.0.0
- *
- * @param array $settings Settings being saved.
- * @return void
- */
- public function clear_jump_cache_on_save($settings): void {
-
- if (isset($settings['jumper_custom_links'])) {
- delete_site_transient($this->transient_key);
- }
- }
-
- /**
- * Rebuilds the jumper menu via a trigger URL.
- *
- * @since 2.0.0
- * @return void
- */
- public function rebuild_menu(): void {
-
- if (isset($_GET[ $this->reset_slug ]) && isset($_GET['nonce']) && current_user_can('manage_network') && wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['nonce'])), 'reset_password')) {
- delete_site_transient($this->transient_key);
-
- wp_safe_redirect(network_admin_url());
-
- exit;
- }
- }
-
- /**
- * Retrieves the custom links added by the super admin
- *
- * @since 2.0.0
- * @return array
- */
- public function get_user_custom_links() {
-
- $treated_lines = [];
-
- $saved_links = wu_get_setting('jumper_custom_links');
-
- $lines = explode(PHP_EOL, (string) $saved_links);
-
- foreach ($lines as $line) {
- $link_elements = explode(':', $line, 2);
-
- if (count($link_elements) === 2) {
- $title = trim($link_elements[1]);
-
- $treated_lines[ $title ] = trim($link_elements[0]);
- }
- }
-
- return $treated_lines;
- }
-
- /**
- * Add the custom links to the Jumper menu
- *
- * @since 2.0.0
- *
- * @param array $links Jumper links already saved.
- * @return array
- */
- public function add_user_custom_links($links) {
-
- $custom_links = $this->get_user_custom_links();
-
- if ( ! empty($custom_links)) {
- $links[ __('Custom Links', 'ultimate-multisite') ] = $custom_links;
- }
-
- return $links;
- }
-
- /**
- * Add Ultimate Multisite settings links to the Jumper menu.
- *
- * @since 2.0.0
- *
- * @param array $links Ultimate Multisite settings array.
- * @return array
- */
- public function add_wp_ultimo_extra_links($links) {
-
- if (isset($links['WP Ultimo'])) {
- $settings_tabs = [
- 'general' => __('General', 'ultimate-multisite'),
- 'network' => __('Network Settings', 'ultimate-multisite'),
- 'gateways' => __('Payment Gateways', 'ultimate-multisite'),
- 'domain_mapping' => __('Domain Mapping & SSL', 'ultimate-multisite'),
- 'emails' => __('Emails', 'ultimate-multisite'),
- 'styling' => __('Styling', 'ultimate-multisite'),
- 'tools' => __('Tools', 'ultimate-multisite'),
- 'advanced' => __('Advanced', 'ultimate-multisite'),
- 'activation' => __('Activation & Support', 'ultimate-multisite'),
- ];
-
- foreach ($settings_tabs as $tab => $tab_label) {
- $url = network_admin_url('admin.php?page=wp-ultimo-settings&wu-tab=' . $tab);
-
- // translators: The placeholder represents the title of the Settings tab.
- $links['WP Ultimo'][ $url ] = sprintf(__('Settings: %s', 'ultimate-multisite'), $tab_label);
- }
-
- $links['WP Ultimo'][ network_admin_url('admin.php?page=wp-ultimo-settings&wu-tab=tools') ] = __('Settings: Webhooks', 'ultimate-multisite');
-
- $links['WP Ultimo'][ network_admin_url('admin.php?page=wp-ultimo-system-info&wu-tab=logs') ] = __('System Info: Logs', 'ultimate-multisite');
-
- /**
- * Adds Main Site Dashboard
- */
- if (isset($links[ __('Sites') ])) { // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
- $main_site_url = get_admin_url(get_current_site()->site_id);
-
- $links[ __('Sites') ][ $main_site_url ] = __('Main Site Dashboard', 'ultimate-multisite'); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
- }
- }
-
- return $links;
- }
-
- /**
- * Get the trigger key defined by the user.
- *
- * @since 2.0.0
- */
- public function get_defined_trigger_key(): string {
-
- return substr((string) wu_get_setting('jumper_key', 'g'), 0, 1);
- }
-
- /**
- * Get the trigger key combination depending on the OS
- *
- * - For Win & Linux: ctrl + alt + key defined by user;
- * - For Mac: command + option + key defined by user.
- *
- * @since 2.0.0
- *
- * @param string $os OS to get the key combination for. Options: win or osx.
- * @return array
- */
- public function get_keys($os = 'win') {
-
- $trigger_key = $this->get_defined_trigger_key();
-
- $keys = [
- 'win' => ['ctrl', 'alt', $trigger_key],
- 'osx' => ['command', 'option', $trigger_key],
- ];
-
- return $keys[ $os ] ?? $keys['win'];
- }
-
- /**
- * Changes the helper footer message about the Jumper and its trigger
- *
- * @since 2.0.0
- *
- * @param string $text The default WordPress right footer message.
- * @return string
- */
- public function add_jumper_footer_message($text) {
-
- if ( ! wu_get_setting('jumper_display_tip', true)) {
- return $text;
- }
-
- $os = isset($_SERVER['HTTP_USER_AGENT']) && stristr(sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])), 'mac') ? 'osx' : 'win';
-
- $keys = $this->get_keys($os);
-
- $html = '';
-
- foreach ($keys as $key) {
- $html .= '' . $key . '+';
- }
-
- $html = trim($html, '+');
-
- // translators: the %s placeholder is the key combination to trigger the Jumper.
- return '' . sprintf(__('Quick Tip: Use %s to jump between pages.', 'ultimate-multisite'), $html) . '' . $text;
- }
-
- /**
- * Enqueues the JavaScript files necessary to make the jumper work.
- *
- * @since 2.0.0
- * @return void
- */
- public function enqueue_scripts(): void {
-
- wp_register_script('wu-mousetrap', wu_get_asset('mousetrap.js', 'js/lib'), ['jquery'], wu_get_version(), true);
-
- wp_register_script('wu-jumper', wu_get_asset('jumper.js', 'js'), ['jquery', 'wu-selectize', 'wu-mousetrap', 'underscore'], wu_get_version(), true);
-
- wp_localize_script(
- 'wu-jumper',
- 'wu_jumper_vars',
- [
- 'not_found_message' => __('Nothing found for', 'ultimate-multisite'),
- 'trigger_key' => $this->get_defined_trigger_key(),
- 'network_base_url' => network_admin_url(),
- 'ajaxurl' => wu_ajax_url(),
- 'base_url' => get_admin_url(get_current_site()->site_id),
- ]
- );
-
- wp_enqueue_script('wu-jumper');
-
- wp_enqueue_style('wu-admin');
- }
-
- /**
- * Enqueues the CSS files necessary to make the jumper work.
- *
- * @since 2.0.0
- * @return void
- */
- public function enqueue_styles(): void {
-
- wp_enqueue_style('wu-jumper', wu_get_asset('jumper.css', 'css'), [], wu_get_version());
- }
-
- /**
- * Outputs the actual HTML markup of the Jumper.
- *
- * @since 2.0.0
- * @return void
- */
- public function output(): void {
-
- wu_get_template(
- 'ui/jumper',
- [
- 'menu_groups' => $this->get_link_list(),
- ]
- );
- }
-
- /**
- * Get the full page URL for admin pages.
- *
- * @since 2.0.0
- *
- * @param string $url URL of the menu item.
- */
- public function get_menu_page_url($url): string {
-
- $final_url = menu_page_url($url, false);
-
- return str_replace(admin_url(), network_admin_url(), $final_url);
- }
-
- /**
- * Returns the URL of a jumper menu item
- *
- * If the URL is an absolute URL, returns the full-url.
- * If the URL is relative, we return the full URL using WordPress url functions.
- *
- * @since 2.0.0
- *
- * @param string $url URL of the menu item.
- * @return string
- */
- public function get_target_url($url) {
-
- if (str_contains($url, 'http')) {
- return $url;
- }
-
- if (str_contains($url, '.php')) {
- return network_admin_url($url);
- }
-
- return $this->get_menu_page_url($url);
- }
-
- /**
- * Builds the list of links based on the $menu and $submenu globals.
- *
- * @since 2.0.0
- *
- * @return array
- */
- public function build_link_list() {
-
- return Logger::track_time(
- 'jumper',
- __('Regenerating Jumper menu items', 'ultimate-multisite'),
- function () {
-
- global $menu, $submenu;
-
- // This variable is going to carry our options
- $choices = [];
-
- // Prevent first run bug
- if ( ! is_array($menu) || ! is_array($submenu)) {
- return [];
- }
-
- // Loop all submenus so que can get our final
- foreach ($submenu as $menu_name => $submenu_items) {
- $title = $this->search_recursive($menu_name, $menu);
-
- $string = wu_get_isset($title, 0, '');
-
- $title = preg_replace('/[0-9]+/', '', wp_strip_all_tags($string));
-
- // If parent does not exists, skip
- if ( ! empty($title) && is_array($submenu_items)) {
-
- // We have to loop now each submenu
- foreach ($submenu_items as $submenu_item) {
- $url = $this->get_target_url($submenu_item[2]);
-
- // Add to our choices the admin urls
- }
- }
- }
-
- $choices = apply_filters('wu_link_list', $choices);
-
- set_site_transient($this->transient_key, $choices, 10 * MINUTE_IN_SECONDS);
-
- return $choices;
- }
- );
- }
-
- /**
- * Gets the cached menu list saved.
- *
- * @since 2.0.0
- * @return array
- */
- public function get_saved_menu() {
-
- $saved_menu = get_site_transient($this->transient_key);
-
- return $saved_menu ?: [];
- }
-
- /**
- * Returns the link list.
- *
- * @since 2.0.0
- * @return array
- */
- public function get_link_list() {
-
- $should_rebuild_menu = ! get_site_transient($this->transient_key);
-
- return $should_rebuild_menu && is_network_admin() ? $this->build_link_list() : $this->get_saved_menu();
- }
-
- /**
- * Filter the Ultimate Multisite settings to add Jumper options
- *
- * @since 2.0.0
- *
- * @return void
- */
- public function add_settings(): void {
-
- wu_register_settings_section(
- 'tools',
- [
- 'title' => __('Tools', 'ultimate-multisite'),
- 'desc' => __('Tools', 'ultimate-multisite'),
- 'icon' => 'dashicons-wu-tools',
- ]
- );
-
- wu_register_settings_field(
- 'tools',
- 'tools_header',
- [
- 'title' => __('Jumper', 'ultimate-multisite'),
- 'desc' => __('Spotlight-like search bar that allows you to easily access everything on your network.', 'ultimate-multisite'),
- 'type' => 'header',
- ]
- );
-
- wu_register_settings_field(
- 'tools',
- 'enable_jumper',
- [
- 'title' => __('Enable Jumper', 'ultimate-multisite'),
- 'desc' => __('Turn this option on to make the Jumper available on your network.', 'ultimate-multisite'),
- 'type' => 'toggle',
- 'default' => 1,
- ]
- );
-
- wu_register_settings_field(
- 'tools',
- 'jumper_key',
- [
- 'title' => __('Trigger Key', 'ultimate-multisite'),
- 'desc' => __('Change the keyboard key used in conjunction with ctrl + alt (or cmd + option), to trigger the Jumper box.', 'ultimate-multisite'),
- 'type' => 'text',
- 'default' => 'g',
- 'require' => [
- 'enable_jumper' => 1,
- ],
- ]
- );
-
- wu_register_settings_field(
- 'tools',
- 'jumper_custom_links',
- [
- 'title' => __('Custom Links', 'ultimate-multisite'),
- 'desc' => __('Use this textarea to add custom links to the Jumper. Add one per line, with the format "Title : url".', 'ultimate-multisite'),
- 'placeholder' => __('Tile of Custom Link : http://link.com', 'ultimate-multisite'),
- 'type' => 'textarea',
- 'html_attr' => [
- 'rows' => 4,
- ],
- 'require' => [
- 'enable_jumper' => 1,
- ],
- ]
- );
- }
-
- /**
- * Helper function to recursively seach an array.
- *
- * @since 2.0.0
- *
- * @param string $needle String to seach recursively.
- * @param array $haystack Array to search.
- * @return mixed
- */
- public function search_recursive($needle, $haystack) {
-
- foreach ($haystack as $key => $value) {
- $current_key = $key;
-
- if ($needle === $value || (is_array($value) && $this->search_recursive($needle, $value) !== false)) {
- return $value;
- }
- }
-
- return false;
- }
-}