PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.11
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.11
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 0.8.8 All 35 releases
← All changes | includes/framework/class-app.php +168 -1 1.1.71.1.11 View file →
@@ -79,8 +79,9 @@
79 79 /**
80 80 * @var array<string,mixed>
81 81 */
82 82 private $nav = array(
83 + 'admin' => 'site',
83 84 'placement' => 'dock',
84 85 'nav_kind' => 'app',
85 86 'dock_order' => 0,
86 87 'placeable' => false,
@@ -112,8 +113,16 @@
112 113 */
113 114 private $defaults = array();
114 115
115 116 /**
117 + * The admin menu this window answers for, and the tabs that ARE
118 + * that menu's pages. See {@see App::menu()}.
119 + *
120 + * @var array<string,mixed>|null
121 + */
122 + private $menu = null;
123 +
124 + /**
116 125 * @var callable|null
117 126 */
118 127 private $mount = null;
119 128
@@ -301,8 +310,23 @@
301 310 return $this;
302 311 }
303 312
304 313 /**
314 + * Which admin's shell offers the window. `site` (the default) is
315 + * every site's shell and never the network admin's — right for an
316 + * app that reads the current site; `network` is the network admin's
317 + * shell only; `any` is both. On a single-site install the network
318 + * admin does not exist, so `network` there offers the app nowhere.
319 + *
320 + * @param string $admin `site` | `network` | `any`.
321 + * @return self
322 + */
323 + public function admin( $admin ) {
324 + $this->nav['admin'] = in_array( $admin, array( 'site', 'network', 'any' ), true ) ? $admin : 'site';
325 + return $this;
326 + }
327 +
328 + /**
305 329 * What the window is to the navigation model.
306 330 *
307 331 * @param string $kind `app` | `control`.
308 332 * @return self
@@ -487,8 +511,110 @@
487 511 return $this;
488 512 }
489 513
490 514 /**
515 + * Declare the admin menu this window answers for, and the tabs
516 + * that ARE that menu's pages.
517 + *
518 + * A window that replaces an admin screen replaces its menu too:
519 + * whatever the window offers as a tab, the dock offers as a row,
520 + * with the same label and in the same order, and picking a row
521 + * opens the window on that tab. One declaration drives all three
522 + * halves of that:
523 + *
524 + * - the dock's submenu for `$slug` becomes these tabs (the first
525 + * is the menu's own page, so it becomes the tile's label);
526 + * - each row's URL is the menu's own tagged `os_tab=<id>`, which
527 + * the shell's remap reads back as the tab to open on;
528 + * - `tab` becomes declared state, written on mount and reopen;
529 + * - the tabs reach the client view as `menuTabs`, which its strip
530 + * renders from, so the two lists cannot drift.
531 + *
532 + * A tab's value is its label, or `array( 'label' => …, 'page' => … )`
533 + * naming the submenu slug it replaces (`user-new.php`). That page
534 + * is which of wp-admin's own rows this window answers for — the
535 + * rest are kept, so a plugin's page under this menu stays
536 + * reachable — and what the shell routes here from anywhere else.
537 + *
538 + * @param string $slug Admin menu slug, e.g. `users.php`.
539 + * @param array<string,mixed>|callable $tabs Ordered `id => label|array`, or a
540 + * callable returning one (for tabs
541 + * that depend on capabilities).
542 + * @param callable|null $enabled Optional gate — the opt-in that
543 + * decides whether this window answers
544 + * for the menu at all. Default: always.
545 + * @return self
546 + */
547 + public function menu( $slug, $tabs, $enabled = null ) {
548 + $this->menu = array(
549 + 'slug' => (string) $slug,
550 + 'tabs' => $tabs,
551 + 'enabled' => $enabled,
552 + );
553 + return $this;
554 + }
555 +
556 + /**
557 + * The declared menu's tabs for the CURRENT user, as an ordered
558 + * list of `array( 'id', 'label' )`. Empty when no menu is declared.
559 + *
560 + * Not gated: these are the window's tabs whoever opened it and
561 + * whatever the opt-in says, and the client view renders its strip
562 + * from them. The gate decides only whether the DOCK's submenu
563 + * becomes this list — see {@see App::menu_owns_dock()}.
564 + *
565 + * @return array<int,array<string,string>>
566 + */
567 + public function menu_tabs() {
568 + if ( ! $this->menu ) {
569 + return array();
570 + }
571 + $tabs = is_callable( $this->menu['tabs'] )
572 + ? (array) call_user_func( $this->menu['tabs'] )
573 + : (array) $this->menu['tabs'];
574 + $out = array();
575 + foreach ( $tabs as $id => $tab ) {
576 + $id = strtolower( (string) preg_replace( '/[^a-zA-Z0-9_-]/', '', (string) $id ) );
577 + $label = is_array( $tab ) ? (string) ( $tab['label'] ?? '' ) : (string) $tab;
578 + if ( '' === $id || '' === $label ) {
579 + continue;
580 + }
581 + $out[] = array(
582 + 'id' => $id,
583 + 'label' => $label,
584 + // The wp-admin submenu slug this tab stands in for,
585 + // '' for a tab wp-admin has no page for.
586 + 'page' => is_array( $tab ) ? (string) ( $tab['page'] ?? '' ) : '',
587 + );
588 + }
589 + return $out;
590 + }
591 +
592 + /**
593 + * The admin menu slug this window answers for, `''` when none.
594 + *
595 + * @return string
596 + */
597 + public function menu_slug() {
598 + return $this->menu ? (string) $this->menu['slug'] : '';
599 + }
600 +
601 + /**
602 + * Whether this window is the one answering for its menu right
603 + * now — the per-user opt-in that chooses between it and the
604 + * classic screen. Only then does the dock's submenu become the
605 + * window's tabs.
606 + *
607 + * @return bool
608 + */
609 + public function menu_owns_dock() {
610 + if ( ! $this->menu ) {
611 + return false;
612 + }
613 + return ! is_callable( $this->menu['enabled'] ) || (bool) call_user_func( $this->menu['enabled'] );
614 + }
615 +
616 + /**
491 617 * Runs once, before the first render, with the fresh state.
492 618 *
493 619 * @param callable $mount `function ( State $state, Os $os )`.
494 620 * @return self
@@ -765,8 +891,15 @@
765 891 $config = $this->config;
766 892 foreach ( $this->config_lazy as $callable ) {
767 893 $config = array_merge( $config, (array) call_user_func( $callable, $this ) );
768 894 }
895 + // The declared menu's tabs, for a client view that renders its
896 + // strip from them — the only way the strip and the dock's
897 + // submenu cannot drift, since both read this list.
898 + $tabs = $this->menu_tabs();
899 + if ( $tabs ) {
900 + $config['menuTabs'] = $tabs;
901 + }
769 902 return $config;
770 903 }
771 904
772 905 // ----------------------------------------------------------- readers
@@ -785,9 +918,19 @@
785 918 *
786 919 * @return array<string,mixed>
787 920 */
788 921 public function defaults() {
789 - return $this->defaults;
922 + if ( ! $this->menu || array_key_exists( 'tab', $this->defaults ) ) {
923 + return $this->defaults;
924 + }
925 + // Declared here rather than by every app that declares a menu:
926 + // the runtime writes this key from the `tab` open-time param,
927 + // and a key the schema does not carry would be dropped.
928 + $tabs = $this->menu_tabs();
929 + return array_merge(
930 + array( 'tab' => $tabs ? $tabs[0]['id'] : '' ),
931 + $this->defaults
932 + );
790 933 }
791 934
792 935 /**
793 936 * Whether an action is declared.
@@ -795,8 +938,9 @@
795 938 * @param string $name Action name.
796 939 * @return bool
797 940 */
798 941 public function has_action( $name ) {
942 + $this->ensure_menu_reopen();
799 943 return isset( $this->actions[ (string) $name ] );
800 944 }
801 945
802 946 /**
@@ -804,12 +948,32 @@
804 948 *
805 949 * @return string[]
806 950 */
807 951 public function action_names() {
952 + $this->ensure_menu_reopen();
808 953 return array_keys( $this->actions );
809 954 }
810 955
811 956 /**
957 + * A window that declares a menu answers `reopen`, whether or not
958 + * it wrote a handler for one.
959 + *
960 + * The client dispatches a lifecycle action only when the manifest
961 + * says the app declared it, so without this the runtime's own
962 + * "land on the tab the opener named" never runs on a window that
963 + * is already open — the case a dock row for another tab IS.
964 + * Registered here rather than in {@see App::menu()} so an app's
965 + * own actions keep the order they were declared in.
966 + *
967 + * @return void
968 + */
969 + private function ensure_menu_reopen() {
970 + if ( $this->menu && ! isset( $this->actions['reopen'] ) ) {
971 + $this->actions['reopen'] = static function () {};
972 + }
973 + }
974 +
975 + /**
812 976 * Run the mount hook, if any.
813 977 *
814 978 * @param State $state State.
815 979 * @param Os $os Host handle.
@@ -986,8 +1150,9 @@
986 1150 'width' => $this->size['width'],
987 1151 'height' => $this->size['height'],
988 1152 'min_width' => $this->size['min_width'],
989 1153 'min_height' => $this->size['min_height'],
1154 + 'admin' => $this->nav['admin'],
990 1155 'placement' => $this->nav['placement'],
991 1156 'nav_kind' => $this->nav['nav_kind'],
992 1157 'dock_order' => $this->nav['dock_order'],
993 1158 'placeable' => $this->nav['placeable'],
@@ -1000,8 +1165,10 @@
1000 1165 'title_bar_buttons' => $this->title_bar_buttons,
1001 1166 'window_actions' => $this->window_actions,
1002 1167 'appearance' => $this->appearance,
1003 1168 'config' => $this->resolved_config(),
1169 + 'menu' => $this->menu_slug(),
1170 + 'menu_tabs' => $this->menu_tabs(),
1004 1171 'tabs' => $this->tabs(),
1005 1172 'channels' => $this->channels,
1006 1173 'watch' => $this->watch,
1007 1174 'client' => $this->client_path(),