Move the settings screen to its own top-level menu

Adds an "M365 Login" menu entry with a Microsoft-style icon and one
submenu per tab (Connection, Button, Security). The active tab is taken
from the URL, kept in the post-save redirect and highlighted in the
submenu. Notices are printed explicitly because top-level pages do not
include options-head.php. Links, docs and screenshots updated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJxAHYdMfKPoN4koRc4Ci2
This commit is contained in:
friloo 2026-09-22 17:07:53 +00:00
parent 8766927123
commit 662756bc75
No known key found for this signature in database
12 changed files with 664 additions and 576 deletions

View file

@ -18,6 +18,7 @@ class M365_Login_Admin {
const AJAX_GROUPS = 'm365_login_search_groups';
const AJAX_CERT = 'm365_login_certificate';
const POST_CERT = 'm365_login_download_cert';
const MENU_ICON = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMCAyMCI+PHBhdGggZmlsbD0iYmxhY2siIGQ9Ik0yIDJoNy41djcuNUgyek0xMC41IDJIMTh2Ny41aC03LjV6TTIgMTAuNWg3LjVWMThIMnpNMTAuNSAxMC41SDE4VjE4aC03LjV6Ii8+PC9zdmc+';
const NONCE_TEST = 'm365_login_test';
/**
@ -69,19 +70,85 @@ class M365_Login_Admin {
add_action( 'admin_post_' . self::POST_CERT, array( $this, 'download_certificate' ) );
add_action( 'update_option_' . M365_LOGIN_OPTION, array( $this->graph, 'flush_token' ) );
add_action( 'admin_notices', array( $this, 'setup_notice' ) );
add_filter( 'submenu_file', array( $this, 'highlight_submenu' ) );
}
/**
* Adds the menu entry under Settings.
* Adds a top-level menu entry with one submenu per tab.
*/
public function menu() {
$this->hook = add_options_page(
$this->hook = add_menu_page(
__( 'M365 Login', 'm365-login' ),
__( 'M365 Login', 'm365-login' ),
'manage_options',
self::PAGE,
array( $this, 'render' )
array( $this, 'render' ),
self::MENU_ICON,
81
);
foreach ( self::tabs() as $tab => $label ) {
add_submenu_page(
self::PAGE,
$label . ' ' . __( 'M365 Login', 'm365-login' ),
$label,
'manage_options',
'connection' === $tab ? self::PAGE : 'admin.php?page=' . self::PAGE . '&tab=' . $tab
);
}
}
/**
* Tab slugs and labels.
*
* @return string[]
*/
public static function tabs() {
return array(
'connection' => __( 'Connection', 'm365-login' ),
'button' => __( 'Button', 'm365-login' ),
'security' => __( 'Security', 'm365-login' ),
);
}
/**
* Currently requested tab (from ?tab=).
*
* @return string
*/
private function current_tab() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only UI state.
$tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : '';
return array_key_exists( $tab, self::tabs() ) ? $tab : '';
}
/**
* Highlights the submenu entry matching the requested tab.
*
* @param string|null $submenu_file Current submenu file.
* @return string|null
*/
public function highlight_submenu( $submenu_file ) {
$screen = get_current_screen();
if ( ! $screen || $this->hook !== $screen->id ) {
return $submenu_file;
}
$tab = $this->current_tab();
if ( '' === $tab || 'connection' === $tab ) {
return self::PAGE;
}
return 'admin.php?page=' . self::PAGE . '&tab=' . $tab;
}
/**
* URL of the settings screen (optionally a specific tab).
*
* @param string $tab Tab slug.
* @return string
*/
public static function url( $tab = '' ) {
$url = admin_url( 'admin.php?page=' . self::PAGE );
return '' === $tab ? $url : add_query_arg( 'tab', $tab, $url );
}
/**
@ -116,7 +183,7 @@ class M365_Login_Admin {
printf(
'<div class="notice notice-info is-dismissible"><p>%s <a href="%s">%s</a></p></div>',
esc_html__( 'M365 Login is active but not connected to Microsoft Entra ID yet.', 'm365-login' ),
esc_url( admin_url( 'options-general.php?page=' . self::PAGE ) ),
esc_url( self::url() ),
esc_html__( 'Open the settings', 'm365-login' )
);
}
@ -322,13 +389,16 @@ class M365_Login_Admin {
</span>
</header>
<?php settings_errors(); ?>
<form method="post" action="options.php" class="m365-admin__form" novalidate>
<?php settings_fields( self::GROUP ); ?>
<input type="hidden" name="_wp_http_referer" value="<?php echo esc_attr( self::url( $this->current_tab() ) ); ?>" id="m365-referer" />
<nav class="m365-admin__tabs" role="tablist">
<button type="button" class="m365-admin__tab is-active" role="tab" data-tab="connection" aria-selected="true"><?php esc_html_e( 'Connection', 'm365-login' ); ?></button>
<button type="button" class="m365-admin__tab" role="tab" data-tab="button" aria-selected="false"><?php esc_html_e( 'Button', 'm365-login' ); ?></button>
<button type="button" class="m365-admin__tab" role="tab" data-tab="security" aria-selected="false"><?php esc_html_e( 'Security', 'm365-login' ); ?></button>
<nav class="m365-admin__tabs" role="tablist" data-initial-tab="<?php echo esc_attr( $this->current_tab() ); ?>">
<?php foreach ( self::tabs() as $tab => $label ) : ?>
<button type="button" class="m365-admin__tab<?php echo 'connection' === $tab ? ' is-active' : ''; ?>" role="tab" data-tab="<?php echo esc_attr( $tab ); ?>" aria-selected="<?php echo 'connection' === $tab ? 'true' : 'false'; ?>"><?php echo esc_html( $label ); ?></button>
<?php endforeach; ?>
</nav>
<div class="m365-admin__layout">