Skip to content

Commit 4be03c6

Browse files
mrabbaniclaude
andauthored
Feat/settings id keyed dependencies (#92)
* feat(settings): support plain field-id keys for dependency lookups Plugin-ui's formatSettingsData() rebuilds dependency_key as a dot-path (parent.child.field) and the values map is keyed by that dot-path. Dependencies declared with a plain field id (e.g., 'commission_type' instead of 'commission.commission.commission_type') therefore failed to resolve via values[dep.key]. Add an id-keyed fallback so both formats work side by side: - Export buildIdIndex(schema) from settings-formatter — produces a { field_id: dependency_key } map from the hierarchical schema. - evaluateDependencies() accepts an optional idIndex argument. When values[dep.key] is undefined and an idIndex is supplied, the function resolves dep.key as a field id and re-reads values via the index. - SettingsProvider builds the idIndex (memoised on schema) and threads it through shouldDisplay → evaluateDependencies. Backwards compatible: callers that don't pass idIndex see identical behavior. Consumers whose backend guarantees globally-unique field ids (e.g., flat-storage schemas) can now use id-keyed dependencies, which stay valid across structural moves (no parent path to update). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(settings): formatter preserves server-supplied dependency_key; falls back to id The formatter used to rebuild `dependency_key` on every render by joining the parent chain into a dot-path. That silently overwrote whatever the server sent. Now we prefer the server value and fall back to the element id when missing (Phase 1 of the dependency_key cleanup; consumers are updated in Phase 2). - formatSettingsData: keep `child.dependency_key` if present, else `child.id` - formatSettingsData: same fallback for the root page (was hard-coded `''`) - Add jest unit tests covering nested preservation, nested fallback, and the page-itself case Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(settings): consumers read element.id; dependency_key becomes vestigial Now that the server emits and consumes flat element ids exclusively (Phase 2 of the dependency_key cleanup), every plugin-ui consumer was keying off element.dependency_key for values, errors, dirty tracking, and onChange callbacks. Replace those reads with element.id: - field-renderer.tsx: merged element value/validationError lookups - fields.tsx: all 20 onChange(element.dependency_key!, ...) call sites - settings-context.tsx: scopeFieldKeysMap, findElement, comments - settings-formatter.ts: * extractValues now keys by el.id * validations/dependencies self pointer uses child.id * buildIdIndex collapses to identity (kept for API stability; evaluateDependencies fallback path is now a no-op for properly keyed schemas, slated for removal in Task 11) - settings-types.ts: SettingsProps.values doc comment dependency_key is left on the type and on the formatter back-compat assignments to avoid breaking external consumers in this commit; Task 11 removes the residue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(settings): drop dependency_key from types, formatter, and tests Task 11 of the dependency_key cleanup. Removes: - dependency_key field from the SettingsElement type - formatter back-compat assignments (child.dependency_key = ... || id) - buildIdIndex helper + idIndex parameter on evaluateDependencies (the helper collapsed to identity in Task 8 and is now dead — dep keys are plain field ids, read directly from the flat values map) - buildIdIndex import + idIndex usage in settings-context - ~472 dependency_key literals from Settings.stories.tsx fixtures - example dependency_key fields from DeveloperGuide.mdx code samples Rewrites the formatter unit tests to assert id preservation through nested parent resolution and that dependency_key no longer materializes on enriched elements. Settings.mdx prose mentions are left for Task 12 (documentation phase). * docs(settings): drop dependency_key from developer guides and stories Add DOKAN_NEXT_MAJOR deprecation callout to CLAUDE.md, DEVELOPER_GUIDE.md, src/DeveloperGuide.mdx, and src/components/settings/Settings.mdx. Replace prose and embedded code-example mentions of dependency_key with the field's id, and update treeValues/flatValues descriptions and examples to the flat id-keyed shape (no dot-paths). * feat(settings): add danger_switch and info_preview field variants danger_switch — a dedicated variant for destructive toggles (e.g. "clear all data on uninstall"). Renders in a destructive-tinted card, always confirms the off → on transition through an AlertDialog, and reads confirm_modal (title / description / confirmText / cancelText / optional checkboxLabel for an acknowledgement gate) from the schema. Built as a separate variant rather than overloading SwitchField with should_confirm / switcher_type / is_danger flags, so the simple switch stays simple. info_preview — generic "pick a few things to display" field with an optional preview on the right. Renders label, description, and a checkbox list driven by element.options; value shape is Record<string, boolean> keyed by option.value. Preview slot resolution order: 1. ${hookPrefix}_settings_info_preview_field_preview filter (receives (null, element, value) → may return a React node). 2. element.image_url rendered as <img>. 3. nothing. The filter-based preview slot lets consumers attach dynamic previews (charts, mocks, live HTML) without re-implementing the whole field. Strengthens switcher_type and confirm_modal types in settings-types.ts so schema authors get autocomplete instead of Record<string, any>. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(settings): cross-field sum_max validation rule + auto re-validation Add a `sum_max` rule to validateField that accepts either: params: { field: 'sibling_id', max: 28 } // single sibling params: { fields: ['a', 'b', 'c'], max: 100 } // N siblings The rule sums self + every referenced sibling and fails when the total exceeds `max`. Defensive: when allValues is not supplied or any sibling value is missing/non-numeric, the rule passes silently to avoid false positives on partial value snapshots (e.g. during initial hydration). validateField now takes an optional `allValues` snapshot parameter so any future cross-field rule can read sibling state without callers having to re-plumb every site. Single-field rules remain unchanged. In settings-context, updateValue now: - Builds a local `nextValues` snapshot and feeds it to validateField. - Walks the schema to find every sibling whose `validations[*].params` references the just-changed key (via `field` or `fields[]`) and re-validates them. So when a user fixes one side of a cross-field constraint, the OTHER side's stale error auto-clears. Use case: a schema can now declare `monthly_billing_day + due_period <= 28` by attaching the same `sum_max` rule to both fields with the symmetric `field` pointing at the sibling. Editing either field surfaces the error on that field; fixing it clears both. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(ui): select dropdown overflow, trigger truncation, anchored-mode clipping Three independent fixes in `Select`, all surfaced while wiring the AI Assist provider/model selects: 1. Trigger value (SelectValue): add `min-w-0 truncate` so long titles ellipsis inside the fixed-width trigger instead of pushing the chevron out of the flex track and rendering over the value. 2. Dropdown options (SelectItem.ItemText): replace `shrink-0 whitespace-nowrap` with `min-w-0 whitespace-normal break-words text-start`. Pickers must never truncate the option label — the user needs the full text to choose. Long titles now wrap to multiple lines within the popup's `--anchor-width` and the popup scrolls vertically. 3. SelectContent default `alignItemWithTrigger`: flip from `true` to `false`. base-ui's anchored mode measures item heights at open time to position the popup so the selected option sits on the trigger; with wrap-enabled items the pre-paint measurement reports a smaller height than the actual wrapped height, so first-open positions the popup above the viewport and clips the top option. Subsequent opens use the settled heights and render fine. Defaulting to a standard "open below trigger" dropdown removes the inconsistency. Callers can still opt into anchored mode explicitly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent e4894b4 commit 4be03c6

12 files changed

Lines changed: 531 additions & 595 deletions

CLAUDE.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# @wedevs/plugin-ui
22

3+
> **Note (DOKAN_NEXT_MAJOR):** The `dependency_key` attribute has been removed
4+
> from the settings schema. Use the field's `id` directly. `show_if` and
5+
> `dependencies` rule keys are now flat field ids — dot-paths are no longer
6+
> supported. See the [dependency_key cleanup plan][cleanup-plan] for migration
7+
> context.
8+
>
9+
> [cleanup-plan]: https://github.com/getdokan/dokan/blob/refactor/simplify-settings-to-flat-array/docs/superpowers/plans/2026-05-18-dependency-key-cleanup.md
10+
311
Scoped, themeable React component library for WordPress plugins. Built on ShadCN patterns, Tailwind CSS v4, and Base-UI primitives.
412

513
## Architecture
@@ -55,15 +63,15 @@ import { Settings } from '@wedevs/plugin-ui';
5563

5664
<Settings
5765
schema={settingsSchema} // SettingsElement[] (flat or hierarchical)
58-
values={values} // Record<string, any> keyed by dependency_key
66+
values={values} // Record<string, any> keyed by field id
5967
onChange={(scopeId, key, value) => {
6068
setValues(prev => ({ ...prev, [key]: value }));
6169
}}
6270
onSave={async (scopeId, treeValues, flatValues) => {
63-
// treeValues: nested object built from dot-separated keys
64-
// e.g. { dokan: { general: { store_name: "..." } } }
65-
// flatValues: original flat dot-keyed values
66-
// e.g. { "dokan.general.store_name": "..." }
71+
// treeValues: object keyed by field id
72+
// e.g. { store_name: "...", enable_tax: true }
73+
// flatValues: same shape — flat id-keyed values
74+
// e.g. { store_name: "...", enable_tax: true }
6775
await api.post(`/settings/${scopeId}`, treeValues);
6876
}}
6977
renderSaveButton={({ dirty, hasErrors, onSave }) => (
@@ -76,7 +84,7 @@ import { Settings } from '@wedevs/plugin-ui';
7684

7785
### Key Concepts
7886

79-
- **`dependency_key`**: Unique key on each field element, used as the key in `values` and `flatValues`
87+
- **`id`**: Unique key on each field element, used as the key in `values` and `flatValues`
8088
- **Dependencies**: Elements can conditionally show/hide based on other field values via `dependencies` array
8189
- **Validation**: Per-field `validations` array with rules and error messages
8290
- **Dirty tracking**: Per-scope (subpage/page) dirty state; resets only on successful save

DEVELOPER_GUIDE.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# @wedevs/plugin-ui — Developer Guide
22

3+
> **Note (DOKAN_NEXT_MAJOR):** The `dependency_key` attribute has been removed
4+
> from the settings schema. Use the field's `id` directly. `show_if` and
5+
> `dependencies` rule keys are now flat field ids — dot-paths are no longer
6+
> supported. See the [dependency_key cleanup plan][cleanup-plan] for migration
7+
> context.
8+
>
9+
> [cleanup-plan]: https://github.com/getdokan/dokan/blob/refactor/simplify-settings-to-flat-array/docs/superpowers/plans/2026-05-18-dependency-key-cleanup.md
10+
311
A ShadCN-style React component library built for WordPress plugins. Provides 50+ themed, accessible UI components powered by Tailwind CSS v4, `@base-ui/react` headless primitives, and first-class WordPress integration.
412

513
---
@@ -1088,23 +1096,20 @@ const schema: SettingsElement[] = [
10881096
type: 'field',
10891097
variant: 'text',
10901098
label: __('Store Name', 'my-plugin'),
1091-
dependency_key: 'store_name',
10921099
default: '',
10931100
},
10941101
{
10951102
id: 'enable_tax',
10961103
type: 'field',
10971104
variant: 'switch',
10981105
label: __('Enable Tax', 'my-plugin'),
1099-
dependency_key: 'enable_tax',
11001106
default: false,
11011107
},
11021108
{
11031109
id: 'tax_rate',
11041110
type: 'field',
11051111
variant: 'number',
11061112
label: __('Tax Rate (%)', 'my-plugin'),
1107-
dependency_key: 'tax_rate',
11081113
dependencies: [{ key: 'enable_tax', value: true, comparison: '=' }],
11091114
},
11101115
],

src/DeveloperGuide.mdx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ import { Meta } from "@storybook/addon-docs/blocks";
66

77
# @wedevs/plugin-ui — Developer Guide
88

9+
> **Note (DOKAN_NEXT_MAJOR):** The `dependency_key` attribute has been removed
10+
> from the settings schema. Use the field's `id` directly. `show_if` and
11+
> `dependencies` rule keys are now flat field ids — dot-paths are no longer
12+
> supported. See the [dependency_key cleanup plan](https://github.com/getdokan/dokan/blob/refactor/simplify-settings-to-flat-array/docs/superpowers/plans/2026-05-18-dependency-key-cleanup.md)
13+
> for migration context.
14+
915
A ShadCN-style React component library built for WordPress plugins. Provides 50+ themed, accessible UI components powered by Tailwind CSS v4, `@base-ui/react` headless primitives, and first-class WordPress integration.
1016

1117
---
@@ -892,9 +898,9 @@ const schema: SettingsElement[] = [
892898
children: [{
893899
id: 'basic_section', type: 'section', label: __('Basic Settings', 'my-plugin'),
894900
children: [
895-
{ id: 'store_name', type: 'field', variant: 'text', label: __('Store Name', 'my-plugin'), dependency_key: 'store_name', default: '' },
896-
{ id: 'enable_tax', type: 'field', variant: 'switch', label: __('Enable Tax', 'my-plugin'), dependency_key: 'enable_tax', default: false },
897-
{ id: 'tax_rate', type: 'field', variant: 'number', label: __('Tax Rate (%)', 'my-plugin'), dependency_key: 'tax_rate',
901+
{ id: 'store_name', type: 'field', variant: 'text', label: __('Store Name', 'my-plugin'), default: '' },
902+
{ id: 'enable_tax', type: 'field', variant: 'switch', label: __('Enable Tax', 'my-plugin'), default: false },
903+
{ id: 'tax_rate', type: 'field', variant: 'number', label: __('Tax Rate (%)', 'my-plugin'),
898904
dependencies: [{ key: 'enable_tax', value: true, comparison: '=' }] },
899905
],
900906
}],

src/components/settings/Settings.mdx

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ import { Meta } from "@storybook/addon-docs/blocks";
66

77
# Settings Component
88

9+
> **Note (DOKAN_NEXT_MAJOR):** The `dependency_key` attribute has been removed
10+
> from the settings schema. Use the field's `id` directly. `show_if` and
11+
> `dependencies` rule keys are now flat field ids — dot-paths are no longer
12+
> supported. See the [dependency_key cleanup plan](https://github.com/getdokan/dokan/blob/refactor/simplify-settings-to-flat-array/docs/superpowers/plans/2026-05-18-dependency-key-cleanup.md)
13+
> for migration context.
14+
915
A reusable, schema-driven settings page that renders a full UI from a JSON structure.
1016
Designed for WordPress plugin settings with built-in extensibility.
1117

@@ -51,11 +57,10 @@ const schema: SettingsElement[] = [
5157
label: "Main",
5258
children: [
5359
{
54-
id: "site_name_field",
60+
id: "site_name",
5561
type: "field",
5662
variant: "text",
5763
label: "Site Name",
58-
dependency_key: "site_name",
5964
default: "",
6065
},
6166
],
@@ -78,8 +83,8 @@ function MySettingsPage() {
7883
setValues((prev) => ({ ...prev, [key]: value }));
7984
}}
8085
onSave={(scopeId, treeValues, flatValues) => {
81-
// treeValues: nested object from dot-separated keys
82-
// flatValues: original flat dot-keyed values
86+
// treeValues: object keyed by field id
87+
// flatValues: same shape — flat id-keyed values
8388
console.log("Saving", scopeId, treeValues, flatValues);
8489
}}
8590
renderSaveButton={({ dirty, hasErrors, onSave }) => (
@@ -277,15 +282,13 @@ Groups multiple fields in a horizontal row (e.g. min/max pair).
277282
"id": "min_price",
278283
"type": "field",
279284
"variant": "number",
280-
"label": "Min",
281-
"dependency_key": "min_price"
285+
"label": "Min"
282286
},
283287
{
284288
"id": "max_price",
285289
"type": "field",
286290
"variant": "number",
287-
"label": "Max",
288-
"dependency_key": "max_price"
291+
"label": "Max"
289292
}
290293
]
291294
}
@@ -297,13 +300,12 @@ An individual form input. The `variant` determines which UI control renders.
297300

298301
```json
299302
{
300-
"id": "store_name_field",
303+
"id": "store_name",
301304
"type": "field",
302305
"variant": "text",
303306
"label": "Store Name",
304307
"description": "The display name of your store.",
305308
"tooltip": "Visible to customers.",
306-
"dependency_key": "store_name",
307309
"default": "My Store",
308310
"placeholder": "Enter store name"
309311
}
@@ -400,8 +402,8 @@ Used by `select`, `radio_capsule`, `customize_radio`, and `multicheck`:
400402

401403
```json
402404
{
405+
"id": "enable_feature",
403406
"variant": "switch",
404-
"dependency_key": "enable_feature",
405407
"enable_state": { "value": "on", "title": "Enabled" },
406408
"disable_state": { "value": "off", "title": "Disabled" }
407409
}
@@ -430,11 +432,10 @@ const flatData = [
430432
{ id: "basic_tab", type: "tab", label: "Basic", subpage_id: "store" },
431433
{ id: "main_section", type: "section", label: "Main", tab_id: "basic_tab" },
432434
{
433-
id: "name_field",
435+
id: "store_name",
434436
type: "field",
435437
variant: "text",
436438
label: "Store Name",
437-
dependency_key: "store_name",
438439
section_id: "main_section",
439440
},
440441
];
@@ -510,7 +511,7 @@ Fired whenever a field value changes.
510511
<tr>
511512
<td><code>key</code></td>
512513
<td><code>string</code></td>
513-
<td>The field's <code>dependency_key</code></td>
514+
<td>The field's <code>id</code></td>
514515
</tr>
515516
<tr>
516517
<td><code>value</code></td>
@@ -550,12 +551,12 @@ Fired when the save button is clicked. Only receives values **scoped to the acti
550551
<tr>
551552
<td><code>treeValues</code></td>
552553
<td><code>{'Record<string, any>'}</code></td>
553-
<td>Nested object built from dot-separated keys (e.g. <code>{'{"dokan":{"general":{"store_name":"..."}}}'}</code>)</td>
554+
<td>Object keyed by field id (e.g. <code>{'{"store_name":"...","enable_tax":true}'}</code>)</td>
554555
</tr>
555556
<tr>
556557
<td><code>flatValues</code></td>
557558
<td><code>{'Record<string, any>'}</code></td>
558-
<td>Original flat dot-keyed values (e.g. <code>{'{"dokan.general.store_name":"..."}'}</code>)</td>
559+
<td>Same shape — flat id-keyed values (e.g. <code>{'{"store_name":"...","enable_tax":true}'}</code>)</td>
559560
</tr>
560561
</tbody>
561562
</table>
@@ -574,7 +575,7 @@ Fired when the save button is clicked. Only receives values **scoped to the acti
574575
### Server-Side Validation Errors
575576

576577
If your API returns field-level validation errors, **throw an object** with an `errors` property
577-
from `onSave`. The keys must match field `dependency_key` values:
578+
from `onSave`. The keys must match field `id` values:
578579

579580
```tsx
580581
<Settings
@@ -587,9 +588,9 @@ from `onSave`. The keys must match field `dependency_key` values:
587588

588589
if (!res.ok) {
589590
const data = await res.json();
590-
// Throw with { errors: { [dependency_key]: "message" } }
591+
// Throw with { errors: { [fieldId]: "message" } }
591592
throw { errors: data.errors };
592-
// e.g. { errors: { "dokan.general.store_name": "Store name already taken" } }
593+
// e.g. { errors: { "store_name": "Store name already taken" } }
593594
}
594595
}}
595596
/>
@@ -714,11 +715,10 @@ Fields can be conditionally shown/hidden based on other field values using the
714715

715716
```json
716717
{
717-
"id": "tax_rate_field",
718+
"id": "tax_rate",
718719
"type": "field",
719720
"variant": "number",
720721
"label": "Tax Rate (%)",
721-
"dependency_key": "tax_rate",
722722
"dependencies": [
723723
{
724724
"key": "enable_tax",
@@ -745,7 +745,7 @@ This field only appears when `enable_tax` is `true`.
745745
<tr>
746746
<td><code>key</code></td>
747747
<td><code>string</code></td>
748-
<td><code>dependency_key</code> of the field to watch</td>
748+
<td><code>id</code> of the field to watch</td>
749749
</tr>
750750
<tr>
751751
<td><code>value</code></td>
@@ -803,7 +803,7 @@ const schema = formatSettingsData(flatApiResponse);
803803

804804
### `extractValues(schema)`
805805

806-
Walks a hierarchical schema and extracts all `dependency_key` to `value` pairs into a flat object.
806+
Walks a hierarchical schema and extracts all `id` to `value` pairs into a flat object.
807807
Useful for initializing the `values` prop.
808808

809809
```tsx
@@ -839,7 +839,7 @@ const initialValues = extractValues(schema);
839839
<td><code>values</code></td>
840840
<td><code>{'Record<string, any>'}</code></td>
841841
<td><code>{'{}'}</code></td>
842-
<td>Current field values keyed by <code>dependency_key</code></td>
842+
<td>Current field values keyed by <code>id</code></td>
843843
</tr>
844844
<tr>
845845
<td><code>onChange</code></td>

0 commit comments

Comments
 (0)