PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.12
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.12
1.1.12 1.1.11 1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 All 36 releases
← All changes | includes/core/payload.php +270 -48 1.1.9 → 1.1.12 View file →
@@ -348,8 +348,11 @@
348 348 }
349 349 $sub_entry = array(
350 350 'title' => $sub_title,
351 351 'url' => $sub_url,
352 + // The registered slug, kept for the window-tab merge
353 + // below and stripped again before the payload ships.
354 + 'slug' => (string) $row['slug'],
352 355 );
353 356 if ( $row['external'] ) {
354 357 // Consumers that route a URL into a window skip these;
355 358 // the ones that can hand a link to the browser mark
@@ -399,8 +402,47 @@
399 402 if ( $parent_is_container && $url === $parent_url ) {
400 403 continue;
401 404 }
402 405
406 + // A native window in charge of this menu owns its rows too:
407 + // whatever it offers as a tab, the dock offers as a row, same
408 + // labels and same order, each one tagged with the tab it
409 + // opens. The first tab IS the menu's own page, so it becomes
410 + // the self-label rather than a second row for the tile's own
411 + // destination. See `App::menu()`.
412 + $window_tabs = openstation_app_menu_tabs( $identity_slug );
413 + if ( $window_tabs ) {
414 + $self_label = $window_tabs[0]['label'];
415 + $claimed = array();
416 + foreach ( $window_tabs as $tab ) {
417 + if ( '' !== $tab['page'] ) {
418 + $claimed[] = $tab['page'];
419 + }
420 + }
421 + // A page this window has no tab for is still a page: a
422 + // plugin's screen registered under this menu, a taxonomy
423 + // someone added. Dropping those would make them
424 + // unreachable from the dock, so they follow the window's
425 + // own rows rather than being replaced by them.
426 + $kept = array();
427 + foreach ( $sub_items as $sub_entry ) {
428 + if ( ! in_array( $sub_entry['slug'], $claimed, true ) ) {
429 + $kept[] = $sub_entry;
430 + }
431 + }
432 + $sub_items = array();
433 + foreach ( array_slice( $window_tabs, 1 ) as $tab ) {
434 + $sub_items[] = array(
435 + 'title' => $tab['label'],
436 + 'url' => add_query_arg( 'os_tab', $tab['id'], $url ),
437 + );
438 + }
439 + $sub_items = array_merge( $sub_items, $kept );
440 + }
441 + foreach ( $sub_items as $i => $sub_entry ) {
442 + unset( $sub_items[ $i ]['slug'] );
443 + }
444 +
403 445 $dock_item = array(
404 446 'id' => sanitize_key( $item[5] ?? $item[2] ),
405 447 'title' => $title,
406 448 'icon' => $icon,
@@ -454,11 +496,12 @@
454 496 * URLs) would therefore fill the dock with tiles that can only ever
455 497 * escape to a browser tab, which breaks the shell's navigation model.
456 498 * Those entries are dropped from the payload instead.
457 499 *
458 - * `self_admin_url()`, `admin_url()` and `home_url()` hosts all count as
459 - * ours: a site can run its admin on a different domain than its front
460 - * end, and the network admin lives on the network's own.
500 + * The menu's own admin (`openstation_menu_admin_url()`), `admin_url()`
501 + * and `home_url()` hosts all count as ours: a site can run its admin on
502 + * a different domain than its front end, and the network admin lives on
503 + * the network's own.
461 504 *
462 505 * @param string $url Absolute URL, as returned by `openstation_menu_item_url()`.
463 506 * @return bool True when the URL is off-site.
464 507 */
@@ -467,9 +510,9 @@
467 510 $external = false;
468 511
469 512 if ( $host ) {
470 513 $ours = array();
471 - foreach ( array( self_admin_url(), admin_url(), home_url() ) as $known ) {
514 + foreach ( array( openstation_menu_admin_url(), admin_url(), home_url() ) as $known ) {
472 515 $known_host = wp_parse_url( $known, PHP_URL_HOST );
473 516 if ( $known_host ) {
474 517 $ours[] = strtolower( $known_host );
475 518 }
@@ -729,12 +772,13 @@
729 772
730 773 /**
731 774 * Filters whether a dock item supports multiple open windows.
732 775 *
733 - * Return true to let the user open more than one window of this page.
734 - * A "+" affordance appears on the dock icon and a "Open another" action
735 - * becomes available in the window's title-bar menu. Singletons (false)
736 - * always focus the existing window when re-opened.
776 + * Return true to advertise this page as multi-capable: an instance
777 + * rail appears under the dock icon and an "Open another" action
778 + * becomes available in the window's title-bar menu. It does not gate
779 + * the submenu, which opens a window of its own on every pick either
780 + * way; a tile click focuses the menu's open window.
737 781 *
738 782 * @param bool $multi Whether this page is multi-capable.
739 783 * @param string $menu_slug The menu slug (e.g. `edit.php?post_type=page`).
740 784 */
@@ -1677,8 +1721,13 @@
1677 1721 // the server's capability-gated view). See
1678 1722 // openstation_menu_signature().
1679 1723 $payload['menuSig'] = openstation_menu_signature();
1680 1724
1725 + // Each script dependency's payload once, entries carry handles.
1726 + $script_dep_payloads = array();
1727 + $payload = openstation_compact_script_deps( $payload, $script_dep_payloads );
1728 + $payload['scriptDepPayloads'] = (object) $script_dep_payloads;
1729 +
1681 1730 return $payload;
1682 1731 }
1683 1732
1684 1733 /**
@@ -1872,8 +1921,12 @@
1872 1921 if ( $dep_handle === $handle ) {
1873 1922 continue;
1874 1923 }
1875 1924 $payload = openstation_resolve_script_payload( $dep_handle );
1925 + // An alias (no `src`) stays in the list when it carries inline
1926 + // data — that data is the whole reason it was declared, and a
1927 + // plugin's config blob commonly rides one. Nothing to fetch
1928 + // AND nothing to run is the only thing dropped.
1876 1929 if ( '' === $payload['url']
1877 1930 && empty( $payload['before'] )
1878 1931 && empty( $payload['after'] )
1879 1932 && empty( $payload['l10n'] ) ) {
@@ -1894,8 +1947,102 @@
1894 1947 return $out;
1895 1948 }
1896 1949
1897 1950 /**
1951 + * Move every entry's `scriptDeps` payloads into one map.
1952 + *
1953 + * {@see openstation_resolve_script_dependencies()} returns the full
1954 + * payload of each dependency (URL, l10n, before/after), and ~20 entry
1955 + * builders call it. A dependency shared by N entries was therefore
1956 + * serialized N times: one plugin's 5.5 KB localized object became
1957 + * ~400 KB of a 489 KB `openStationConfig` (GH#892). This replaces each
1958 + * entry's `scriptDeps` list with its handles and puts each handle's
1959 + * payload in `$map` once. The shell resolves the handles back before
1960 + * any consumer reads them; see `src/script-dep-payloads.ts`.
1961 + *
1962 + * ENTRY DEPTH ONLY. `$payload` is a payload whose top-level values are
1963 + * entry lists (`serverWidgets`, `serverCommandScripts`, ...), and only
1964 + * a `scriptDeps` sitting directly on one of those entries is touched.
1965 + * The key is the shell's there. Deeper down it is a plugin's metadata
1966 + * (`settings => [ 'scriptDeps' => ... ]`), and rewriting it would hoist
1967 + * foreign data into the map first-wins and hydrate every real
1968 + * dependency of that handle to it. Scoping by depth rather than by a
1969 + * list of keys means a new builder is covered without an edit here.
1970 + * It also leaves every other branch of the payload unassigned, so
1971 + * PHP's copy-on-write never has to copy them.
1972 + *
1973 + * Every string left in a compacted list has an entry in `$map`: a bare
1974 + * handle a builder (or a filter on one) emitted is resolved here, by
1975 + * the same rule as {@see openstation_resolve_script_dependencies()}.
1976 + * A handle with nothing to fetch and nothing to run is dropped, as it
1977 + * is there. The client can then treat a string with no map entry as a
1978 + * payload from somewhere else, not a dependency this side dropped.
1979 + *
1980 + * Runs on the finished payload, so every builder, and every filter on
1981 + * a builder's output, still sees the full shape.
1982 + *
1983 + * @param array $payload Payload whose top-level values are entry lists.
1984 + * @param array $map Handle => dependency payload, filled in place.
1985 + * @return array The payload with each entry's `scriptDeps` reduced to handles.
1986 + */
1987 +function openstation_compact_script_deps( $payload, array &$map ) {
1988 + if ( ! is_array( $payload ) ) {
1989 + return $payload;
1990 + }
1991 + foreach ( $payload as $list_key => $entries ) {
1992 + if ( ! is_array( $entries ) ) {
1993 + continue;
1994 + }
1995 + foreach ( $entries as $entry_key => $entry ) {
1996 + if ( ! is_array( $entry ) || ! isset( $entry['scriptDeps'] ) || ! is_array( $entry['scriptDeps'] ) ) {
1997 + continue;
1998 + }
1999 + $payload[ $list_key ][ $entry_key ]['scriptDeps'] = openstation_compact_script_dep_list( $entry['scriptDeps'], $map );
2000 + }
2001 + }
2002 + return $payload;
2003 +}
2004 +
2005 +/**
2006 + * One entry's `scriptDeps` list, reduced to handles.
2007 + *
2008 + * @param array $deps Dependency payloads and/or bare handles.
2009 + * @param array $map Handle => dependency payload, filled in place.
2010 + * @return array Handles, in order; anything unkeyable passes through.
2011 + */
2012 +function openstation_compact_script_dep_list( array $deps, array &$map ) {
2013 + $handles = array();
2014 + foreach ( $deps as $dep ) {
2015 + if ( is_string( $dep ) && '' !== $dep ) {
2016 + if ( ! isset( $map[ $dep ] ) ) {
2017 + $payload = openstation_resolve_script_payload( $dep );
2018 + if ( '' === $payload['url']
2019 + && empty( $payload['before'] )
2020 + && empty( $payload['after'] )
2021 + && empty( $payload['l10n'] ) ) {
2022 + continue;
2023 + }
2024 + $payload['handle'] = $dep;
2025 + $map[ $dep ] = $payload;
2026 + }
2027 + $handles[] = $dep;
2028 + continue;
2029 + }
2030 + if ( is_array( $dep ) && isset( $dep['handle'] ) && '' !== (string) $dep['handle'] ) {
2031 + $handle = (string) $dep['handle'];
2032 + if ( ! isset( $map[ $handle ] ) ) {
2033 + $map[ $handle ] = $dep;
2034 + }
2035 + $handles[] = $handle;
2036 + continue;
2037 + }
2038 + // A handle-less payload has nothing to key it by.
2039 + $handles[] = $dep;
2040 + }
2041 + return $handles;
2042 +}
2043 +
2044 +/**
1898 2045 * Resolve a registered WP script handle into the full payload the
1899 2046 * shell needs to lazy-load it without going through `wp_print_scripts()`.
1900 2047 *
1901 2048 * Returns:
@@ -1919,10 +2066,14 @@
1919 2066 * around the lazy `<script src>` in the same order
1920 2067 * `WP_Scripts::do_item()` would have used.
1921 2068 *
1922 2069 * Returns an empty payload (`array( 'url' => '' )`) when the handle
1923 - * is unregistered or has no source — callers treat that as "no
1924 - * script to load."
2070 + * is unregistered. A registered handle with no source — an alias
2071 + * carrying only inline data — also comes back with an empty `url`,
2072 + * but its `before` / `after` / `l10n` are kept: callers that load a
2073 + * bundle treat an empty `url` as "nothing to fetch", and the
2074 + * dependency walk ({@see openstation_resolve_script_dependencies()})
2075 + * still replays what the alias would have printed.
1925 2076 *
1926 2077 * Shared between `openstation_register_window()` and
1927 2078 * `openstation_register_widget()` (and every other registration that
1928 2079 * relies on lazy script loading in the shell) because all of them
@@ -1950,20 +2101,31 @@
1950 2101 return $empty;
1951 2102 }
1952 2103 $registered = $wp_scripts->registered[ $handle ];
1953 2104 $src = is_string( $registered->src ) ? $registered->src : '';
1954 - if ( '' === $src ) {
1955 - return $empty;
1956 - }
1957 2105
1958 - // Normalize relative paths + attach cache-bust ver.
1959 - $resolved = $src;
1960 - if ( 0 === strpos( $resolved, '/' ) && 0 !== strpos( $resolved, '//' ) ) {
1961 - $resolved = site_url( $resolved );
2106 + // A handle with no `src` is an ALIAS — WordPress's supported way
2107 + // to ship inline-only JavaScript (`wp_register_script( $h, false )`
2108 + // plus `wp_add_inline_script()`), and a common home for a plugin's
2109 + // config blob: registering it as a *dependency* of every bundle is
2110 + // what guarantees the config runs first, whatever the enqueue
2111 + // order. `WP_Scripts::do_item()` prints an alias's localized data
2112 + // and its before/after snippets and returns before the `<script
2113 + // src>` it does not have. The payload mirrors that: `url` stays
2114 + // empty (there is nothing to fetch) and the inline data is kept,
2115 + // so a dependency walk can replay it. Translations are not: Core
2116 + // only prints those for a handle it printed a tag for.
2117 + $resolved = '';
2118 + if ( '' !== $src ) {
2119 + // Normalize relative paths + attach cache-bust ver.
2120 + $resolved = $src;
2121 + if ( 0 === strpos( $resolved, '/' ) && 0 !== strpos( $resolved, '//' ) ) {
2122 + $resolved = site_url( $resolved );
2123 + }
2124 + if ( ! empty( $registered->ver ) ) {
2125 + $resolved = add_query_arg( 'ver', $registered->ver, $resolved );
2126 + }
1962 2127 }
1963 - if ( ! empty( $registered->ver ) ) {
1964 - $resolved = add_query_arg( 'ver', $registered->ver, $resolved );
1965 - }
1966 2128
1967 2129 // Harvest `extra` data the lazy-load path would otherwise drop.
1968 2130 $before = array();
1969 2131 $after = array();
@@ -1998,9 +2160,9 @@
1998 2160 // `wp.i18n.setLocaleData( JSON, 'domain' )` snippet that the print
1999 2161 // pipeline emits before the script body. `print_translations(
2000 2162 // $handle, false )` returns the snippet without echoing.
2001 2163 $translations = '';
2002 - if ( method_exists( $wp_scripts, 'print_translations' ) ) {
2164 + if ( '' !== $resolved && method_exists( $wp_scripts, 'print_translations' ) ) {
2003 2165 $captured = $wp_scripts->print_translations( $handle, false );
2004 2166 if ( is_string( $captured ) ) {
2005 2167 $translations = $captured;
2006 2168 }
@@ -2174,24 +2336,16 @@
2174 2336 $script_probe->done = array();
2175 2337 $script_probe->all_deps( $script_roots );
2176 2338 foreach ( $script_probe->to_do as $handle ) {
2177 2339 $payload = openstation_resolve_script_payload( $handle );
2178 - if ( '' === $payload['url'] ) {
2179 - // Src-less aggregator — keep it only for its inline data.
2180 - $registered = isset( $scripts->registered[ $handle ] ) ? $scripts->registered[ $handle ] : null;
2181 - if ( $registered ) {
2182 - foreach ( array( 'before', 'after' ) as $position ) {
2183 - if ( isset( $registered->extra[ $position ] ) && is_array( $registered->extra[ $position ] ) ) {
2184 - $payload[ $position ] = array_values( array_filter( array_map( 'strval', $registered->extra[ $position ] ) ) );
2185 - }
2186 - }
2187 - if ( ! empty( $registered->extra['data'] ) && is_string( $registered->extra['data'] ) ) {
2188 - $payload['l10n'][] = $registered->extra['data'];
2189 - }
2190 - }
2191 - if ( empty( $payload['before'] ) && empty( $payload['after'] ) && empty( $payload['l10n'] ) ) {
2192 - continue;
2193 - }
2340 + // A src-less aggregator is kept only for its inline data — the
2341 + // resolver harvests that for an alias — and dropped when it
2342 + // carries none.
2343 + if ( '' === $payload['url']
2344 + && empty( $payload['before'] )
2345 + && empty( $payload['after'] )
2346 + && empty( $payload['l10n'] ) ) {
2347 + continue;
2194 2348 }
2195 2349 // Core's `initializeCommandPalette( {…} )` inline embeds the
2196 2350 // serialized admin-menu command list — ~20 KB that the boot
2197 2351 // page ALREADY carries as `window.__openStationMenuCommands`
@@ -2365,8 +2519,12 @@
2365 2519 * harvested `wp_localize_script` / `wp_add_inline_script` /
2366 2520 * translations, see `openstation_resolve_script_payload()` — lives
2367 2521 * ONCE per handle in `scriptData`, and the shell joins the two on
2368 2522 * receipt (`hydrateServerEntries()` in `src/native-windows.ts`).
2523 + * Each loadable handle's entry also names its dependency closure in
2524 + * `deps` (ordered handles, every one of them a key of the same map)
2525 + * so the lazy loader can bring a bundle's declared packages — and
2526 + * a src-less alias carrying its config — into the tab before it.
2369 2527 *
2370 2528 * The split exists because script data is a property of the HANDLE,
2371 2529 * not of the window: every App Framework window rides
2372 2530 * `openstation-app-runtime`, and inlining each entry's resolved copy
@@ -2383,9 +2541,9 @@
2383 2541 * Style data stays inline on the entries — it never had a
2384 2542 * duplication problem worth a second map ( companion styles across
2385 2543 * the whole registry total ~2 KB ).
2386 2544 *
2387 - * @return array{windows:array[],scriptData:array<string,array{url:string,before:string[],after:string[],l10n:string[],translations:string}>}
2545 + * @return array{windows:array[],scriptData:array<string,array{url:string,before:string[],after:string[],l10n:string[],translations:string,deps:string[]}>}
2388 2546 */
2389 2547 function openstation_collect_native_windows_payload() {
2390 2548 $empty = array(
2391 2549 'windows' => array(),
@@ -2417,24 +2575,59 @@
2417 2575 $registry = array_filter( $registry, 'openstation_native_window_offered_here' );
2418 2576
2419 2577 $script_data = array();
2420 2578
2579 + // Handles resolved as a bundle to LOAD (a window's script, a
2580 + // companion, a tab) and what that visit answered — the handle, or
2581 + // '' for nothing to load — as opposed to reached only as
2582 + // somebody's dependency. A handle can be both — resolved as a
2583 + // dependency first, then named as a window's own script — and
2584 + // only the bundle visit computes its own closure.
2585 + $resolved_as_bundle = array();
2586 +
2421 2587 // Resolve a handle into the map, once. Returns the handle when it
2422 2588 // resolved to something loadable, '' when it did not (never
2423 2589 // registered, no src) — the same silent drop the inline shape
2424 2590 // applied to companions and tab scripts.
2425 - $collect_handle = static function ( $handle ) use ( &$script_data ) {
2591 + //
2592 + // The handle's dependency closure rides along as `deps`: an
2593 + // ordered handle list, each of which lands in the same map. A
2594 + // bundle delivered lazily never goes through WordPress's own
2595 + // dependency resolution — the loader injects one URL — so a
2596 + // window declaring `wp-api-fetch` found `wp.apiFetch` undefined,
2597 + // and one whose config rides a src-less alias handle (a common
2598 + // shape: `wp_register_script( $h, false )` plus
2599 + // `wp_add_inline_script()`, declared as the bundle's dependency
2600 + // so it always runs first) booted with no config at all. Anything
2601 + // the document already ran is skipped on the client, so a page
2602 + // that carried the packages anyway pays nothing.
2603 + $collect_handle = static function ( $handle ) use ( &$script_data, &$resolved_as_bundle ) {
2426 2604 $handle = (string) $handle;
2427 2605 if ( '' === $handle ) {
2428 2606 return '';
2429 2607 }
2430 - if ( isset( $script_data[ $handle ] ) ) {
2431 - return $handle;
2608 + if ( isset( $resolved_as_bundle[ $handle ] ) ) {
2609 + return $resolved_as_bundle[ $handle ];
2432 2610 }
2433 - $payload = openstation_resolve_script_payload( $handle );
2611 + $payload = isset( $script_data[ $handle ] )
2612 + ? $script_data[ $handle ]
2613 + : openstation_resolve_script_payload( $handle );
2434 2614 if ( '' === $payload['url'] ) {
2615 + $resolved_as_bundle[ $handle ] = '';
2435 2616 return '';
2436 2617 }
2618 + $resolved_as_bundle[ $handle ] = $handle;
2619 + $deps = array();
2620 + foreach ( openstation_resolve_script_dependencies( $handle ) as $dep ) {
2621 + $dep_handle = (string) $dep['handle'];
2622 + unset( $dep['handle'] );
2623 + if ( ! isset( $script_data[ $dep_handle ] ) ) {
2624 + $dep['deps'] = array();
2625 + $script_data[ $dep_handle ] = $dep;
2626 + }
2627 + $deps[] = $dep_handle;
2628 + }
2629 + $payload['deps'] = $deps;
2437 2630 $script_data[ $handle ] = $payload;
2438 2631 return $handle;
2439 2632 };
2440 2633
@@ -2582,8 +2775,9 @@
2582 2775 'styleHandle' => $style_handle,
2583 2776 'styleInline' => $style_payload['inline'],
2584 2777 'companionStyles' => $companion_styles,
2585 2778 'tabs' => $tab_descriptors,
2779 + 'menuPages' => isset( $entry['menu_pages'] ) ? array_values( (array) $entry['menu_pages'] ) : array(),
2586 2780 );
2587 2781 }
2588 2782
2589 2783 // Append each handle's synthesized config set to its map entry —
@@ -2684,17 +2878,45 @@
2684 2878 return file_exists( ABSPATH . 'wp-admin/' . $file );
2685 2879 }
2686 2880
2687 2881 /**
2882 + * The admin URL a menu slug resolves against.
2883 + *
2884 + * Follows the admin the request is in: the network admin's own URL there,
2885 + * because its globals carry network slugs (`sites.php`, `settings.php`)
2886 + * that exist only under `wp-admin/network/`, and the site admin's
2887 + * everywhere else.
2888 + *
2889 + * The same answer `self_admin_url()` gives, without its filter. That
2890 + * filter receives the path, so a host can use it to send one screen
2891 + * somewhere else, and WordPress.com points `plugin-install.php` at its own
2892 + * installer. Resolved through it, the wp-admin original of a menu row the
2893 + * host replaced reads as off-site, and the dock drops it along with the
2894 + * replacement, which is how Plugins > Add Plugin disappears there.
2895 + *
2896 + * @param string $path Optional. Path relative to the admin URL.
2897 + * @return string Absolute admin URL.
2898 + */
2899 +function openstation_menu_admin_url( $path = '' ) {
2900 + if ( is_network_admin() ) {
2901 + return network_admin_url( $path );
2902 + }
2903 + if ( is_user_admin() ) {
2904 + return user_admin_url( $path );
2905 + }
2906 + return admin_url( $path );
2907 +}
2908 +
2909 +/**
2688 2910 * Converts a menu item slug to a full admin URL.
2689 2911 *
2690 - * Resolution goes through `self_admin_url()`, not `admin_url()`: in the
2691 - * network admin the same globals carry network slugs (`sites.php`,
2692 - * `settings.php`) that exist only under `wp-admin/network/`.
2912 + * Resolution goes through {@see openstation_menu_admin_url()}, which
2913 + * follows the admin the request is in without passing through the
2914 + * filterable `self_admin_url()`.
2693 2915 *
2694 2916 * Handles three slug shapes:
2695 2917 * 1. Direct file references (`edit.php`, `upload.php`) — passed
2696 - * through `self_admin_url()` as-is.
2918 + * through `openstation_menu_admin_url()` as-is.
2697 2919 * 2. Plain plugin page slugs (`my-plugin`) — routed through
2698 2920 * `admin.php?page=<slug>` with the slug `rawurlencode()`d.
2699 2921 * 3. Plugin page slugs that embed extra query parameters
2700 2922 * (`wc-admin&path=/customers`) — split on the first `&`, the
@@ -2755,9 +2977,9 @@
2755 2977 if (
2756 2978 false !== strpos( $slug, '.php' ) &&
2757 2979 ( ! isset( $_parent_pages[ $slug ] ) || openstation_is_admin_file_slug( $slug ) )
2758 2980 ) {
2759 - return esc_url_raw( self_admin_url( $slug ) );
2981 + return esc_url_raw( openstation_menu_admin_url( $slug ) );
2760 2982 }
2761 2983
2762 2984 // Plugin page slug with embedded query parameters
2763 2985 // (e.g., 'wc-admin&path=/customers'). Split the page slug from
@@ -2797,9 +3019,9 @@
2797 3019 $host = add_query_arg( 'page', $slug, $parent_slug );
2798 3020 }
2799 3021 }
2800 3022
2801 - $url = self_admin_url( $host );
3023 + $url = openstation_menu_admin_url( $host );
2802 3024 if ( ! empty( $extra_args ) ) {
2803 3025 $url = add_query_arg( $extra_args, $url );
2804 3026 }
2805 3027 return esc_url_raw( $url );