| @@ -1,11 +1,13 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | |
| 3 | 3 | namespace Code_Snippets\Admin\Menus; |
| 4 | 4 | |
| 5 | +use Code_Snippets\Admin\Contextual_Help; | |
| 5 | 6 | use Code_Snippets\Settings\Settings_Fields; |
| 6 | 7 | use function Code_Snippets\code_snippets; |
| 7 | 8 | use function Code_Snippets\Settings\are_settings_unified; |
| 9 | +use function Code_Snippets\Settings\do_settings_fields_with_headings; | |
| 8 | 10 | use function Code_Snippets\Utils\enqueue_code_editor; |
| 9 | 11 | use function Code_Snippets\Utils\get_editor_themes; |
| 10 | 12 | use const Code_Snippets\PLUGIN_FILE; |
| 11 | 13 | use const Code_Snippets\PLUGIN_VERSION; |
| @@ -13,11 +15,10 @@ | ||
| 13 | 15 | use const Code_Snippets\Settings\OPTION_NAME; |
| 14 | 16 | use const Code_Snippets\Settings\CACHE_KEY; |
| 15 | 17 | |
| 16 | 18 | /** |
| 17 | - * This class handles the settings admin menu | |
| 19 | + * This class handles the settings admin menu. | |
| 18 | 20 | * |
| 19 | - * @since 2.4.0 | |
| 20 | 21 | * @package Code_Snippets |
| 21 | 22 | */ |
| 22 | 23 | class Settings_Menu extends Admin_Menu { |
| 23 | 24 | |
| @@ -26,9 +27,9 @@ | ||
| 26 | 27 | */ |
| 27 | 28 | public const SETTINGS_PAGE = 'code-snippets'; |
| 28 | 29 | |
| 29 | 30 | /** |
| 30 | - * Constructor | |
| 31 | + * Constructor. | |
| 31 | 32 | */ |
| 32 | 33 | public function __construct() { |
| 33 | 34 | parent::__construct( |
| 34 | 35 | 'settings', |
| @@ -37,8 +38,37 @@ | ||
| 37 | 38 | ); |
| 38 | 39 | } |
| 39 | 40 | |
| 40 | 41 | /** |
| 42 | + * Register the settings menu. | |
| 43 | + * | |
| 44 | + * Deliberately bypasses the snippet view-permission gate applied by the | |
| 45 | + * parent. The settings page is already restricted to the core capability | |
| 46 | + * (`manage_options` via `get_cap()`), so it stays admin-only regardless — | |
| 47 | + * but it must always remain reachable so an administrator who restricts | |
| 48 | + * their own role's snippet permissions can never lock themselves out of the | |
| 49 | + * Permissions tab needed to undo it. | |
| 50 | + */ | |
| 51 | + public function register() { | |
| 52 | + $this->add_menu( $this->slug, $this->label, $this->title ); | |
| 53 | + } | |
| 54 | + | |
| 55 | + /** | |
| 56 | + * The settings page is configuration territory (it includes the Permissions | |
| 57 | + * tab itself), so it must remain restricted to full administrators — never | |
| 58 | + * the broader snippet access capability that can be granted to other roles. | |
| 59 | + * This is also what keeps an administrator from ever losing access to the | |
| 60 | + * Permissions tab needed to undo a misconfiguration. | |
| 61 | + * | |
| 62 | + * @return string | |
| 63 | + */ | |
| 64 | + protected function menu_cap(): string { | |
| 65 | + return is_multisite() && ( is_network_admin() || ! code_snippets()->is_subsite_menu_enabled() ) | |
| 66 | + ? 'manage_network_options' | |
| 67 | + : 'manage_options'; | |
| 68 | + } | |
| 69 | + | |
| 70 | + /** | |
| 41 | 71 | * Executed when the admin page is loaded |
| 42 | 72 | */ |
| 43 | 73 | public function load() { |
| 44 | 74 | parent::load(); |
| @@ -50,8 +80,11 @@ | ||
| 50 | 80 | wp_safe_redirect( code_snippets()->get_menu_url( 'settings', 'admin' ) ); |
| 51 | 81 | exit; |
| 52 | 82 | } |
| 53 | 83 | } |
| 84 | + | |
| 85 | + $contextual_help = new Contextual_Help( 'settings' ); | |
| 86 | + $contextual_help->load(); | |
| 54 | 87 | } |
| 55 | 88 | |
| 56 | 89 | /** |
| 57 | 90 | * Enqueue the stylesheet for the settings menu |
| @@ -62,9 +95,9 @@ | ||
| 62 | 95 | |
| 63 | 96 | wp_enqueue_style( |
| 64 | 97 | $handle, |
| 65 | 98 | plugins_url( 'dist/settings.css', PLUGIN_FILE ), |
| 66 | - self::$style_deps + [ 'code-editor' ], | |
| 99 | + array_merge( self::$style_deps, [ 'code-editor' ] ), | |
| 67 | 100 | PLUGIN_VERSION |
| 68 | 101 | ); |
| 69 | 102 | |
| 70 | 103 | wp_enqueue_script( |
| @@ -69,9 +102,9 @@ | ||
| 69 | 102 | |
| 70 | 103 | wp_enqueue_script( |
| 71 | 104 | $handle, |
| 72 | 105 | plugins_url( 'dist/settings.js', PLUGIN_FILE ), |
| 73 | - self::$script_deps + [ 'code-snippets-code-editor' ], | |
| 106 | + array_merge( self::$script_deps, [ 'code-snippets-code-editor' ] ), | |
| 74 | 107 | PLUGIN_VERSION, |
| 75 | 108 | true |
| 76 | 109 | ); |
| 77 | 110 | |
| @@ -175,9 +208,9 @@ | ||
| 175 | 208 | * @param string $default_section Name of the default tab displayed. |
| 176 | 209 | * |
| 177 | 210 | * @return string |
| 178 | 211 | */ |
| 179 | - public function get_current_section( string $default_section = 'general' ): string { | |
| 212 | + public function get_current_section( string $default_section = '' ): string { | |
| 180 | 213 | $sections = $this->get_sections(); |
| 181 | 214 | |
| 182 | 215 | if ( ! $sections ) { |
| 183 | 216 | return $default_section; |
| @@ -182,11 +215,17 @@ | ||
| 182 | 215 | if ( ! $sections ) { |
| 183 | 216 | return $default_section; |
| 184 | 217 | } |
| 185 | 218 | |
| 219 | + // Fall back to the first registered tab. Naming a specific section here | |
| 220 | + // meant the page rendered empty whenever that section stopped existing. | |
| 221 | + $fallback = $default_section && isset( $sections[ $default_section ] ) | |
| 222 | + ? $default_section | |
| 223 | + : (string) array_key_first( $sections ); | |
| 224 | + | |
| 186 | 225 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Value is matched to registered sections. |
| 187 | - $active_tab = isset( $_REQUEST['section'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['section'] ) ) : $default_section; | |
| 188 | - return isset( $sections[ $active_tab ] ) ? $active_tab : $default_section; | |
| 226 | + $active_tab = isset( $_REQUEST['section'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['section'] ) ) : $fallback; | |
| 227 | + return isset( $sections[ $active_tab ] ) ? $active_tab : $fallback; | |
| 189 | 228 | } |
| 190 | 229 | |
| 191 | 230 | /** |
| 192 | 231 | * Render the admin screen |
| @@ -197,31 +236,37 @@ | ||
| 197 | 236 | $update_url = is_network_admin() ? add_query_arg( 'update_site_option', true ) : admin_url( 'options.php' ); |
| 198 | 237 | $current_section = $this->get_current_section(); |
| 199 | 238 | |
| 200 | 239 | ?> |
| 201 | - <div class="wrap" data-active-tab="<?php echo esc_attr( $current_section ); ?>"> | |
| 202 | - <h2> | |
| 203 | - <?php | |
| 204 | - esc_html_e( 'Settings', 'code-snippets' ); | |
| 240 | + <div class="wrap code-snippets-settings" data-active-tab="<?php echo esc_attr( $current_section ); ?>"> | |
| 241 | + <?php $this->render_section_tabs(); ?> | |
| 205 | 242 | |
| 206 | - if ( code_snippets()->is_compact_menu() ) { | |
| 207 | - $actions = [ | |
| 208 | - _x( 'Manage', 'snippets', 'code-snippets' ) => code_snippets()->get_menu_url(), | |
| 209 | - _x( 'Add New', 'snippet', 'code-snippets' ) => code_snippets()->get_menu_url( 'add' ), | |
| 210 | - _X( 'Import', 'snippets', 'code-snippets' ) => code_snippets()->get_menu_url( 'import' ), | |
| 211 | - ]; | |
| 243 | + <div class="snippets-page-header"> | |
| 244 | + <h1><?php esc_html_e( 'Snippets Settings', 'code-snippets' ); ?></h1> | |
| 245 | + </div> | |
| 212 | 246 | |
| 213 | - foreach ( $actions as $label => $url ) { | |
| 214 | - printf( | |
| 215 | - '<a href="%s" class="page-title-action">%s</a>', | |
| 216 | - esc_url( $url ), | |
| 217 | - esc_html( $label ) | |
| 218 | - ); | |
| 219 | - } | |
| 247 | + <?php | |
| 248 | + if ( code_snippets()->is_compact_menu() ) { | |
| 249 | + $actions = [ | |
| 250 | + _x( 'Manage', 'snippets', 'code-snippets' ) => code_snippets()->get_menu_url(), | |
| 251 | + _x( 'Add New', 'snippet', 'code-snippets' ) => code_snippets()->get_menu_url( 'add' ), | |
| 252 | + _x( 'Import', 'snippets', 'code-snippets' ) => code_snippets()->get_menu_url( 'import' ), | |
| 253 | + ]; | |
| 254 | + | |
| 255 | + echo '<p class="settings-page-actions">'; | |
| 256 | + | |
| 257 | + foreach ( $actions as $label => $url ) { | |
| 258 | + printf( | |
| 259 | + '<a href="%s" class="page-title-action">%s</a>', | |
| 260 | + esc_url( $url ), | |
| 261 | + esc_html( $label ) | |
| 262 | + ); | |
| 220 | 263 | } |
| 221 | - ?> | |
| 222 | - </h2> | |
| 223 | 264 | |
| 265 | + echo '</p>'; | |
| 266 | + } | |
| 267 | + ?> | |
| 268 | + | |
| 224 | 269 | <hr class="wp-header-end" /> |
| 225 | 270 | |
| 226 | 271 | <?php settings_errors( OPTION_NAME ); ?> |
| 227 | 272 | |
| @@ -229,9 +274,9 @@ | ||
| 229 | 274 | <input type="hidden" name="section" value="<?php echo esc_attr( $current_section ); ?>"> |
| 230 | 275 | <?php |
| 231 | 276 | |
| 232 | 277 | settings_fields( OPTION_GROUP ); |
| 233 | - $this->do_settings_tabs(); | |
| 278 | + $this->render_settings_sections(); | |
| 234 | 279 | ?> |
| 235 | 280 | <p class="submit"> |
| 236 | 281 | <?php |
| 237 | 282 | submit_button( null, 'primary', 'submit', false ); |
| @@ -249,20 +294,22 @@ | ||
| 249 | 294 | <?php |
| 250 | 295 | } |
| 251 | 296 | |
| 252 | 297 | /** |
| 253 | - * Output snippet settings in tabs | |
| 298 | + * Output the settings section subnavigation bar, styled to match the | |
| 299 | + * subnavigation on the snippet management pages, followed by the slot | |
| 300 | + * that adopts the Screen Options and Help tabs. | |
| 254 | 301 | */ |
| 255 | - protected function do_settings_tabs() { | |
| 302 | + protected function render_section_tabs() { | |
| 256 | 303 | $sections = $this->get_sections(); |
| 257 | 304 | $active_tab = $this->get_current_section(); |
| 258 | 305 | |
| 259 | - echo '<nav class="nav-tab-wrapper" id="settings-sections-tabs" aria-label="' . esc_attr__( 'Settings tabs', 'code-snippets' ) . '">'; | |
| 306 | + echo '<div class="snippet-type-nav-wrapper settings-type-nav-wrapper"><nav class="snippet-type-nav settings-type-nav" id="settings-sections-tabs" aria-label="' . esc_attr__( 'Settings tabs', 'code-snippets' ) . '"><ul>'; | |
| 260 | 307 | |
| 261 | 308 | foreach ( $sections as $section ) { |
| 262 | 309 | printf( |
| 263 | - '<a class="nav-tab%s" data-section="%s" href="%s">%s</a>', | |
| 264 | - esc_attr( $active_tab ) === $section['id'] ? ' nav-tab-active' : '', | |
| 310 | + '<li><a class="snippet-type-link%s" data-section="%s" href="%s"><span>%s</span></a></li>', | |
| 311 | + esc_attr( $active_tab ) === $section['id'] ? ' active-type' : '', | |
| 265 | 312 | esc_attr( $section['id'] ), |
| 266 | 313 | esc_url( add_query_arg( 'section', $section['id'] ) ), |
| 267 | 314 | esc_html( $section['title'] ) |
| 268 | 315 | ); |
| @@ -267,10 +314,18 @@ | ||
| 267 | 314 | esc_html( $section['title'] ) |
| 268 | 315 | ); |
| 269 | 316 | } |
| 270 | 317 | |
| 271 | - echo '</nav>'; | |
| 318 | + echo '</ul></nav></div>'; | |
| 319 | + echo '<div id="snippets-screen-meta-slot" class="snippets-screen-meta-slot"></div>'; | |
| 320 | + } | |
| 272 | 321 | |
| 322 | + /** | |
| 323 | + * Output snippet settings sections. | |
| 324 | + */ | |
| 325 | + protected function render_settings_sections() { | |
| 326 | + $sections = $this->get_sections(); | |
| 327 | + | |
| 273 | 328 | foreach ( $sections as $section ) { |
| 274 | 329 | if ( 'license' === $section['id'] ) { |
| 275 | 330 | continue; |
| 276 | 331 | } |
| @@ -276,9 +331,9 @@ | ||
| 276 | 331 | } |
| 277 | 332 | |
| 278 | 333 | if ( $section['title'] ) { |
| 279 | 334 | printf( |
| 280 | - '<h3 id="%s-settings" class="settings-section-title">%s</h3>' . "\n", | |
| 335 | + '<h2 id="%s-settings" class="settings-section-title">%s</h2>' . "\n", | |
| 281 | 336 | esc_attr( $section['id'] ), |
| 282 | 337 | esc_html( $section['title'] ) |
| 283 | 338 | ); |
| 284 | 339 | } |
| @@ -288,9 +343,9 @@ | ||
| 288 | 343 | } |
| 289 | 344 | |
| 290 | 345 | printf( '<div class="settings-section %s-settings"><table class="form-table">', esc_attr( $section['id'] ) ); |
| 291 | 346 | |
| 292 | - do_settings_fields( self::SETTINGS_PAGE, $section['id'] ); | |
| 347 | + do_settings_fields_with_headings( self::SETTINGS_PAGE, $section['id'] ); | |
| 293 | 348 | echo '</table></div>'; |
| 294 | 349 | } |
| 295 | 350 | } |
| 296 | 351 | |