# admin-ui

Generic Jetpack wp-admin UI elements
## How to use
### Menu Registration

Use the Admin_Menu class to add your plugin under the Jetpack top level menu in WP-Admin.

This package will make sure to register the top level menu, if not registered yet, and will add the new menu(s) item(s) under it.

Use the `add_menu` to register your menu, no need to do it inside the `admin_menu` hook. You can do it in your plugin initialization.

The parameters this method gets are the same parameters `add_submenu_page` gets, except that you don't need to inform `parent` menu.

Example:

```PHP
use Automattic\Jetpack\Admin_UI\Admin_Menu;

$page_suffix = Admin_Menu::add_menu(
	__( 'My Awesome plugin', 'my-awesome-plugin' ),
	__( 'My Awesome plugin', 'my-awesome-plugin' ),
	'manage_options',
	'my-awesome-plugin',
	'__my_plugin_page_callback'
);
add_action( 'load-' . $page_suffix, 'my_plugin_do_stuff_on_page_load' );

```

### Menu visibility

An item can declare what gates it, so that turning its feature off removes it from the sidebar. Pass the declaration as the seventh argument:

```PHP
Admin_Menu::add_menu(
	'Jetpack Search',
	'Search',
	'manage_options',
	'jetpack-search',
	array( $this, 'render' ),
	null,
	array(
		'product' => 'search',
		'key'     => 'jetpack-search',
	)
);
```

| Key | Meaning |
| -- | -- |
| `product` | A My Jetpack product slug — the products with cards on the My Jetpack page. Preferred, and covers most items. |
| `module` | A Jetpack module name. Only for items with no product behind them. |
| `key` | The name a host uses for this item in the filter below. Declare one on every item. Falls back to the menu slug when absent. |

**Prefer `product`.** A product is not the same thing as a plugin. It may be gated by a Jetpack module, by a standalone plugin, or by either:

| Gated by | Example products |
| -- | -- |
| A Jetpack module | Stats, Forms, Newsletter, AI |
| A standalone plugin | Boost, Akismet, CRM |
| Either — the standalone plugin when it's installed, the module otherwise | Social, Search, VideoPress, Backup, Protect |
| The Jetpack plugin alone, because the product has no module of its own | Scan |

The product's `is_activated()` already knows which of those applies, so a registration site names the product and never has to work out which kind it is. It asks only whether the site has switched the product on, never whether it has a plan: a lapsed plan keeps the item, so the route back to upgrading survives, and resolving a gate never makes a request to WordPress.com.

Reach for `module` only when a sidebar item has no product class at all. SEO is the example: it's gated by the `seo-tools` module and has no My Jetpack card, so there's no product to name. An item that declares neither is always shown.

A name this site has no module for cannot be answered, so it keeps the item rather than removing it — a typo in a gate is inert. Off the Jetpack plugin that covers any module a standalone plugin has not declared through `jetpack_get_available_standalone_modules`, so declare one there before expecting a `module` gate to do anything.

A `module` gate reads `Modules::is_active()`, which always answers true on WordPress.com Simple, so gates there never remove anything. Hosts on Simple shape the sidebar through the filter below.

**Always declare a `key`**, as a fixed string rather than a reference to the page's slug constant, so renaming a page never renames what hosts have written into their filters.

Everything fails open. An item that declares no gate, a gate naming a product that isn't registered, and a site where My Jetpack didn't initialize all leave the item in place, so declaring a gate can only ever remove an item deliberately.

Declaring a gate is a commitment that the page renders something sensible when the gate is unsatisfied, because a host can force it visible anyway — see below.

### Host control

`jetpack_admin_menu_visibility` filters a map of item key to state. Absent keys stay `default`.

| State | Meaning |
| -- | -- |
| `default` | Show the item if its gate is satisfied. What every item does unless a host says otherwise. |
| `visible` | Show the item whatever its gate says. |
| `hidden` | Keep the item out whatever its gate says. |

Every state governs the sidebar entry only. An item that is hidden, or whose gate is unsatisfied, still registers its page, so `admin.php?page=…` keeps working for My Jetpack's Manage buttons, bookmarks, and support links.

```PHP
add_filter(
	'jetpack_admin_menu_visibility',
	function ( $items ) {
		// Keep an entry point for a product this platform sells but the site hasn't activated.
		$items['jetpack-search'] = Admin_Menu::VISIBILITY_VISIBLE;
		// Drop a product this platform doesn't offer.
		$items['jetpack-videopress'] = Admin_Menu::VISIBILITY_HIDDEN;

		return $items;
	}
);
```

The whole map is passed at once so two mu-plugins setting different keys merge rather than clobber each other. `visible` cannot expose a page to someone who lacks the capability for it: an item is only registered for users who have it.

Only items registered through `Admin_Menu::add_menu()` or `Admin_Menu::add_top_level_menu()` are in the map. Anything added with a bare `add_submenu_page()` or `add_menu_page()` is out of this filter's reach.

### Top level pages

A few Jetpack pages deliberately sit outside the Jetpack menu. `add_top_level_menu()` registers those under the same gate, filter and hidden-but-reachable behavior as `add_menu()`. The parameters mirror `add_menu_page()`, with the visibility declaration appended:

```PHP
$page_suffix = Admin_Menu::add_top_level_menu(
	__( 'My Dashboard', 'my-plugin' ),
	__( 'My Dashboard', 'my-plugin' ),
	'manage_options',
	'my-dashboard',
	array( $this, 'render' ),
	'dashicons-chart-bar',
	30,
	array( 'key' => 'jetpack-my-dashboard' )
);
```

A gate removes the item wherever it resolves false, so check it holds in every context that registers the page — a standalone plugin can run without the Jetpack plugin a `product` gate may assume.

Stats and Premium Analytics register this way, under the keys `jetpack-stats` and `jetpack-premium-analytics`.

Unlike `add_menu()`, the page gets neither the core-notice CSS nor the WPDS design tokens; call `Admin_Menu::hide_core_admin_notices()` or `Admin_Menu::enqueue_design_tokens()` on its own `load-` hook for either. Registering only top level items never builds the Jetpack menu.

## Security

Need to report a security vulnerability? Go to [https://automattic.com/security/](https://automattic.com/security/) or directly to our security bug bounty site [https://hackerone.com/automattic](https://hackerone.com/automattic).

## License

admin-ui is licensed under [GNU General Public License v2 (or later)](./LICENSE.txt)

