Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.7.0] - 2026-02-15

### Added
- Admin settings page under Settings → Elayne Blocks for block management
- Ability to selectively enable/disable individual blocks
- Checkbox controls for all 6 blocks (Carousel, Slide, Mega Menu, FAQ Tabs, FAQ Tab Answer, Search Overlay Trigger)
- Parent-child block dependency enforcement (Carousel→Slide, FAQ Tabs→FAQ Tab Answer)
- Bulk actions: "Enable All" and "Disable All" buttons
- Real-time JavaScript dependency handling in settings UI
- Confirmation dialogs when disabling parent blocks with dependencies
- Block descriptions and dependency warnings in settings UI
- Admin CSS and JavaScript for enhanced settings page interactivity

### Changed
- Block registration now respects admin settings (disabled blocks won't register)
- Carousel asset loading now checks both settings and block presence for optimization
- Settings stored persistently in WordPress options (`elayne_blocks_enabled`)

### Security
- Proper capability checks (`manage_options`) for settings access
- WordPress Settings API integration with automatic nonce handling
- Input sanitization and validation for all settings
- PHPCS WordPress coding standards compliance

### Documentation
- Added `docs/ADMIN-PANEL-PLAN.md` - Complete implementation plan
- Added `docs/ADMIN-PANEL-TESTING.md` - Comprehensive testing guide
- Added `docs/ADMIN-PANEL-IMPLEMENTATION.md` - Implementation summary

## [2.6.0] - 2026-02-15

### Changed
Expand Down
80 changes: 80 additions & 0 deletions assets/admin/admin-settings.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* Admin Settings Page Styles
*
* @package ELayneBlocks
*/

.elayne-blocks-bulk-actions {
margin: 20px 0;
padding: 15px;
background: #f9f9f9;
border: 1px solid #ddd;
border-radius: 4px;
}

.elayne-blocks-bulk-actions .button {
margin-right: 10px;
}

/* Settings field styling */
#elayne_blocks_main_section .form-table th {
width: 250px;
vertical-align: top;
padding-top: 15px;
}

#elayne_blocks_main_section .form-table td {
padding-top: 10px;
}

#elayne_blocks_main_section .form-table td label {
display: inline-flex;
align-items: center;
gap: 8px;
}

#elayne_blocks_main_section .form-table td input[type="checkbox"] {
margin: 0;
}

/* Description text styling */
#elayne_blocks_main_section .description {
margin-top: 8px;
margin-bottom: 0;
color: #666;
font-size: 13px;
line-height: 1.5;
}

#elayne_blocks_main_section .description strong {
color: #d63638;
}

/* Child block indentation */
#elayne_blocks_main_section tr[data-parent] th,
#elayne_blocks_main_section tr[data-parent] td {
padding-left: 30px;
}

/* Disabled state for child blocks */
#elayne_blocks_main_section input[type="checkbox"]:disabled {
opacity: 0.5;
cursor: not-allowed;
}

/* Dependency warning styling */
.elayne-dependency-warning {
display: inline-block;
margin-left: 10px;
padding: 4px 8px;
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 3px;
font-size: 12px;
color: #856404;
}

/* Success message styling */
.notice.settings-error {
margin-top: 15px;
}
85 changes: 85 additions & 0 deletions assets/admin/admin-settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* Admin Settings Page JavaScript
*
* Handles bulk actions and parent-child block dependencies.
*
* @package ELayneBlocks
*/

(function ($) {
'use strict';

$(document).ready(function () {
// Handle Enable All button.
$('#elayne-enable-all').on('click', function (e) {
e.preventDefault();
$('.elayne-block-checkbox').prop('checked', true);
});

// Handle Disable All button.
$('#elayne-disable-all').on('click', function (e) {
e.preventDefault();
$('.elayne-block-checkbox').prop('checked', false);
});

// Handle parent-child dependencies.
function handleDependencies() {
// Carousel -> Slide dependency.
const carouselCheckbox = $('#elayne_blocks_carousel');
const slideCheckbox = $('#elayne_blocks_slide');

if (carouselCheckbox.length && slideCheckbox.length) {
if (!carouselCheckbox.prop('checked')) {
slideCheckbox.prop('checked', false).prop('disabled', true);
} else {
slideCheckbox.prop('disabled', false);
}
}

// FAQ Tabs -> FAQ Tab Answer dependency.
const faqTabsCheckbox = $('#elayne_blocks_faq-tabs');
const faqTabAnswerCheckbox = $('#elayne_blocks_faq-tab-answer');

if (faqTabsCheckbox.length && faqTabAnswerCheckbox.length) {
if (!faqTabsCheckbox.prop('checked')) {
faqTabAnswerCheckbox.prop('checked', false).prop('disabled', true);
} else {
faqTabAnswerCheckbox.prop('disabled', false);
}
}
}

// Run on page load.
handleDependencies();

// Run when checkboxes change.
$('.elayne-block-checkbox').on('change', function () {
handleDependencies();
});

// Warn before disabling parent blocks with dependent children.
$('#elayne_blocks_carousel, #elayne_blocks_faq-tabs').on('change', function () {
const isChecked = $(this).prop('checked');
const blockId = $(this).attr('id');
let dependentBlockName = '';

if (blockId === 'elayne_blocks_carousel') {
dependentBlockName = 'Slide';
} else if (blockId === 'elayne_blocks_faq-tabs') {
dependentBlockName = 'FAQ Tab Answer';
}

if (!isChecked && dependentBlockName) {
const message =
'Disabling this block will also disable the ' +
dependentBlockName +
' block. Continue?';

if (!confirm(message)) {
$(this).prop('checked', true);
handleDependencies();
}
}
});
});
})(jQuery);
122 changes: 122 additions & 0 deletions docs/ADMIN-PANEL-CODE-REVIEW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Admin Settings Panel Code Review (PR #33)

**Branch:** admin-settings
**PR:** #33
**Review Date:** 2026-02-19

---

## What's Good

**Architecture & Feature Design**
- Correct use of the WordPress Settings API (`register_setting`, `add_settings_section`, `add_settings_field`) — the idiomatic approach
- Capability check (`current_user_can('manage_options')`) in the page renderer
- Input sanitized via a dedicated `elayne_blocks_sanitize_settings()` callback
- Server-side dependency enforcement in `elayne_blocks_validate_dependencies()` — JS alone isn't trustworthy
- Assets scoped to only the settings page via the `$hook` check in `elayne_blocks_enqueue_admin_assets()`
- `is_admin()` guard around the admin include
- ABSPATH guard in `settings-page.php`
- The actual feature (block enable/disable) is genuinely useful

---

## Bugs

### 1. `plugins_url()` asset path is broken — `settings-page.php:269-276`

```php
// Current (buggy):
plugins_url( 'assets/admin/admin-settings.css', __DIR__ . '/../..' )
```

`plugins_url()` expects a **file** path as its second arg, not a directory. Passing `__DIR__ . '/../..'` produces a URL like `wp-content/plugins/elayne-blocks/includes/assets/admin/admin-settings.css` (wrong) or relative to the plugins root entirely. The constant `ELAYNE_BLOCKS_PLUGIN_URL` is already defined in `elayne-blocks.php` for exactly this purpose:

```php
// Correct:
ELAYNE_BLOCKS_PLUGIN_URL . 'assets/admin/admin-settings.css'
```

### 2. CSS `tr[data-parent]` selector never matches — `admin-settings.css:54-57`

```css
#elayne_blocks_main_section tr[data-parent] th,
#elayne_blocks_main_section tr[data-parent] td { padding-left: 30px; }
```

The PHP sets `data-parent` on the `<input>` element, not the `<tr>`. The indentation for child blocks (Slide, FAQ Tab Answer) never renders. The `data-parent` attribute would need to be on the `<tr>` (which the Settings API generates automatically from `add_settings_field`) — this requires wrapping logic or a different CSS approach.

### 3. Defaults mismatch in carousel assets check — `elayne-blocks.php:89`

```php
// In init hook (correct — full defaults):
$enabled_blocks = get_option('elayne_blocks_enabled', array(
'carousel' => true, 'slide' => true, /* all 6 blocks */
));

// In wp_enqueue_scripts hook (wrong — partial default):
$enabled_blocks = get_option('elayne_blocks_enabled', array('carousel' => true));
```

The second `get_option()` has a partial default. If the option doesn't exist yet, the carousel check works fine, but if the option IS set without a `carousel` key (edge case), `empty($enabled_blocks['carousel'])` would skip carousel assets even when they should load.

---

## Architecture Issues

### 4. Settings block list is hardcoded, defeating auto-discovery

The plugin's stated architecture is dynamic block discovery (scanning `/blocks` for `build/block.json`). But `elayne_blocks_get_available_blocks()` hardcodes all 6 blocks. Adding a new block requires updating both the `/blocks` directory AND this function — partially breaking the auto-discovery model. The block list could be derived by reading `block.json` files at admin init time instead.

### 5. JS dependency logic is hardcoded instead of data-driven — `admin-settings.js:26-83`

`handleDependencies()` hardcodes specific element IDs (`#elayne_blocks_carousel`, `#elayne_blocks_faq-tabs`), and the warning dialog hardcodes English block names. But the `data-parent` attribute is already set on each child checkbox. The JS could read that attribute generically, making it work for any future parent-child pair without JS changes.

### 6. `elayne_blocks_get_default_settings()` duplicated inline

The defaults array in `settings-page.php` (`elayne_blocks_get_default_settings()`) is duplicated inline in `elayne-blocks.php:50-57`. Since the include is inside `is_admin()`, the function isn't available on the frontend. The function should either be extracted to a shared file or the defaults maintained in one place.

---

## UX Issues

### 7. "Disable All" silently bypasses the confirmation dialog

`$('.elayne-block-checkbox').prop('checked', false)` does not trigger jQuery `change` events, so neither the confirmation warning nor `handleDependencies()` runs when clicking "Disable All". All blocks go unchecked silently, which is fine for the checkbox state, but `handleDependencies()` not running means disabled-state styling (opacity, cursor) is inconsistent until the next interaction.

### 8. Confirmation dialog text is not translatable

The `confirm()` string in `admin-settings.js:73-77` is hardcoded English — no i18n mechanism (e.g., `wp_localize_script`) is used.

---

## Minor Issues

### 9. Dead CSS

`.elayne-dependency-warning` class is defined in `admin-settings.css:66-75` but never added to any element in the PHP or JS.

### 10. Plugin description header outdated

`elayne-blocks.php:4` still says "including Mega Menu, Carousel, and Slide blocks" but the plugin now has 6 blocks.

### 11. Three verbose doc files

~860 lines of docs across `ADMIN-PANEL-IMPLEMENTATION.md`, `ADMIN-PANEL-PLAN.md`, and `ADMIN-PANEL-TESTING.md`. The plan file is pre-implementation thinking that doesn't add long-term value. The implementation details and testing results could be condensed into the CHANGELOG or a single doc.

---

## Priority Summary

| Priority | Issue |
|---|---|
| **Bug** | `plugins_url()` asset path broken — CSS/JS may not load |
| **Bug** | `tr[data-parent]` CSS never matches — child block indentation missing |
| **Bug** | Carousel defaults mismatch (minor edge case) |
| **Architecture** | Settings list is hardcoded vs. auto-discovery pattern |
| **Architecture** | JS dependency logic should use `data-parent` generically |
| **Architecture** | Defaults duplicated across two files |
| **UX** | "Disable All" bypasses dependency handling |
| **UX** | Confirm dialog not translatable |
| Minor | Dead CSS, outdated plugin description, doc file bloat |

The core concept is solid and the WordPress API usage is correct — the main things to fix before merging are the `plugins_url()` bug (which likely breaks asset loading entirely) and the CSS selector mismatch.
Loading