# Divi Engine settings framework migration playbook

This document captures what worked during the Divi Machine migration to the new settings framework, so future plugin migrations (for example, Divi BodyCommerce) can move faster without repeating the same research.

## Goal

Migrate a plugin from legacy settings UI to the shared settings framework while keeping:

- Existing option name and option IDs
- Existing data shapes in the database
- Safe fallback to legacy mode when needed

## Source of truth and scope

For shared settings framework behavior, use the canonical framework patterns already used in:

- `settings-framework`
- Mature plugin implementations (for structure and UX patterns)

For plugin-specific behavior, only implement plugin logic in:

- `includes/settings/specific/settings.php`
- `includes/settings/src/specific/**`

Do not put plugin-specific logic into shared framework files unless it is truly generic.

## Migration phases

## 1) Bootstrap and mode switching

Add a plugin-level mode gate (legacy vs new settings):

- Define a helper like `plugin_should_use_legacy_settings()`
- Respect forced constants for debugging/rollout:
  - `*_FORCE_LEGACY_SETTINGS`
  - `*_FORCE_NEW_SETTINGS`
- Fallback to legacy mode if required framework files are missing
- Use legacy mode in known shared plugin contexts where needed

Then conditionally load:

- Legacy mode:
  - legacy settings file(s)
- New mode:
  - `includes/settings/organization/divi-engine/class-common-settings.php`
  - `includes/settings/specific/settings.php`

Also update plugin action links so the Settings link points to:

- Legacy page in legacy mode
- `admin.php?page=divi-engine#<plugin-slug>` in new mode

## 2) Keep database compatibility (critical)

In `specific/settings.php`, set the option name filter so new settings writes to the same row used by legacy:

- `de_option_name_<plugin-slug>`

Example outcome for Divi Machine:

- New and legacy UIs both use `divi-machine_options`

Also preserve legacy value formats in sanitization:

- Toggles often need `'on'/'off'` strings (not booleans)
- Some list fields need keyed objects with `'on'/'off'` values
- Textarea/editor fields may need unslashed raw content

Add helper sanitizers for consistency:

- toggle sanitizer
- editor value sanitizer
- on/off array sanitizer

## 3) Register plugin with framework

In `specific/settings.php`:

- Implement `de_settings_register_plugin`
- Register slug, label, plugin pages bundle URL, versions
- Add cache-busting to pages bundle version using `filemtime()`

Why this matters:

- Prevents stale JS after deployment
- Reduces false “page not registered” issues from browser cache

## 4) React page registration and structure

Use a plugin entry that:

- Discovers plugin pages from `src/specific/pages/**`
- Registers plugin pages with framework registry
- Retries registration on timing races (short delayed retries + DOMContentLoaded)

Use a settings page shell with:

- Sidebar + sections
- `useSettingsResource({ slug, defaults })`
- `SettingsFormProvider`
- Section routing by hash where needed

## 5) Dynamic runtime data

If React needs server-derived options (for example ACF groups), inject data on admin page load:

- `admin_enqueue_scripts`
- `wp_add_inline_script(...)`
- Place data on `window.diviEngineApiSettings` namespace

Pattern used:

- Search ACF groups shown with readable labels (not raw IDs)

## 6) REST endpoints for tool actions and dynamic fields

When old UI relied on admin-ajax or dynamic server lists:

- Add plugin REST routes in `specific/settings.php`
- Keep permissions strict (`manage_options`)
- Return simple `options` arrays (`[{ value, label }]`) for fieldsets

For legacy action reuse:

- Proxy existing admin-ajax handlers through REST instead of rewriting business logic

Used for Divi Machine:

- import/export
- migrate shortcodes
- module override dynamic post types/ACF fields

## 7) Build and verify

Run:

- `npm run build:pages` in `includes/settings`
- PHP lint on key changed files
- Open settings page and verify:
  - Page registers correctly
  - Save persists into legacy option row
  - Dynamic fields load
  - Conditional show/hide behavior works

## Known pitfalls and fixes

## Pitfall: plugin page JS is enqueued, but page says “No page registered”

Common causes:

- Registration race (plugin pages bundle runs before core registry ready)
- Stale cached bundle
- Slug mismatch between plugin config and page exports

Fixes:

- Add registration retries in `plugin-entry.js`
- Use cache-busted version for pages bundle
- Ensure only the correct page slug is registered

## Pitfall: dynamic dropdowns only show fallback values

Common causes:

- Fieldset still bound to static options constant
- REST base URL/nonce source mismatch

Fixes:

- Bind fieldset to state populated from REST
- Confirm REST source uses `window.diviEngineApiSettings.restUrl` and nonce

## Pitfall: user-facing labels show technical IDs

Fix:

- Build `{ value, label }` server-side from plugin APIs and inject/fetch labels for UI

## Pitfall: search and advanced controls visible when parent toggle is off

Fix:

- Use conditional rendering (`showIf` or explicit conditions) tied to the parent setting

## Migration checklist (copy for next plugin)

- [ ] Add legacy/new settings mode gate in main plugin bootstrap
- [ ] Keep option name identical via `de_option_name_<slug>`
- [ ] Implement defaults + sanitization with legacy data shapes
- [ ] Register plugin pages bundle with cache-busted version
- [ ] Add plugin entry registration retries
- [ ] Build settings sections in `src/specific`
- [ ] Add runtime data injection for dynamic options
- [ ] Add REST routes for tool actions and dynamic field option sources
- [ ] Proxy legacy admin-ajax logic where rewriting is risky
- [ ] Update Settings action link for both modes
- [ ] Build bundle and lint PHP/JS
- [ ] Update `changelog.txt` with user-facing entries

## File map used in Divi Machine

Core bootstrap:

- `divi-machine.php`
- `includes/modules/divi-ajax-filter/divi/init.php`

Plugin-specific settings backend:

- `includes/settings/specific/settings.php`

Plugin-specific settings UI:

- `includes/settings/src/specific/pages/divi-machine.jsx`
- `includes/settings/src/specific/plugin-entry.js`
- `includes/settings/src/specific/components/settings-sections/*`

Build artifacts:

- `includes/settings/dist/settings-app.js`
- `includes/settings/dist/divi-machine-pages.js`

## Recommendation for BodyCommerce migration

Start with this exact order:

1. Bootstrap mode gate and option-name compatibility
2. Plugin registration and page loading
3. Minimal working page with save
4. One section at a time (migrate legacy fields in vertical slices)
5. Dynamic endpoints and tool actions last

This order catches integration issues early before field-level migration complexity grows.
