Skip to content

Conversation

@sapayth
Copy link
Member

@sapayth sapayth commented Sep 5, 2025

part of #1038

related PR

Currently, the repeat field is supporting/repeating the whole field set, we are now supporting a single row-wise repeat of fields.

Summary by CodeRabbit

  • New Features

    • Enhanced table UI with sticky headers/footers/columns, zebra/hover states, RTL support, and consistent typography.
  • Bug Fixes

    • Repeatable field controls in admin now reliably show add/remove buttons and maintain proper indexing.
    • Radio inputs no longer use dynamic name binding.
  • Refactor

    • Simplified repeat-field normalization for more predictable form rendering.

Removed redundant dynamic :name bindings from radio field templates in both Vue and PHP templates for consistency. Enhanced admin form builder table styles with new classes for hover, zebra striping, sticky headers/footers, and improved RTL support. Simplified repeat field migration logic in wpuf_get_form_fields to ensure inner_fields is always a simple array, improving compatibility and maintainability.
Refactored the repeat field controls to dynamically create add/remove buttons, ensuring correct visibility and preventing rapid updates. Added MutationObserver to monitor button attribute changes and re-apply visibility logic after field initialization, improving reliability and consistency of repeat field UI in the admin posting interface.
@coderabbitai
Copy link

coderabbitai bot commented Sep 5, 2025

Walkthrough

This PR removes dynamic radio input name bindings in templates, adds a comprehensive .wpuf-table CSS module and nav sizing utilities, reworks admin repeat-field rendering with reindexing and MutationObserver-driven controls, and simplifies repeat-field normalization by flattening inner_fields.

Changes

Cohort / File(s) Summary
Radio template updates
admin/form-builder/assets/js/components/field-radio/template.php, assets/js-templates/form-components.php
Removed dynamic :name="'radio_' + editing_form_field.id + '_' + option_field.name'" bindings from radio input templates; minor whitespace adjustments.
Repeat-field admin logic
includes/Admin/Posting.php
Rewrote repeat-field UI handling: rebuilds add/remove controls per instance, explicit instance indexing and reindexing, reinitialization of new instances, MutationObserver to maintain button visibility, and guarded update calls.
Repeat-field normalization
wpuf-functions.php
Simplified wpuf_get_form_fields handling for repeat fields: flatten inner_fields to a simple array and remove legacy multi-column conversion logic.
Table & navigation CSS
assets/css/admin/form-builder.css
Added .wpuf-table styles (sticky headers/footers/left cols, borders, hover/zebra variants, RTL support, display: table) and bottom-navigation/label sizing utilities.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Potential focus areas:

  • includes/Admin/Posting.php — reindexing, MutationObserver lifecycle, and initialization timing.
  • wpuf-functions.php — correctness of flattening logic and impact on callers expecting previous multi-column structure.
  • Radio templates — verify forms relying on radio name behavior still submit/validate as expected.

Possibly related PRs

Suggested labels

Dev Review Done

Poem

I hop through rows and sticky heads,
Reindexing nests and tuning threads.
Radios quiet their naming song,
Tables pin where they belong.
A little rabbit cheers: "All set and strong!" 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'enhance: row population option for repeat field' clearly summarizes the main change: adding a row population option to the repeat field functionality.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (9)
assets/js-templates/form-components.php (1)

509-513: Radio group name change: verify uniqueness to avoid cross-group interference.

Switching to :name="option_field.name" is fine if each option_field.name is unique within the open options panel. If two radio option groups share the same name, selecting one will deselect the other at the browser level.

  • Action: Verify that no two radio options rendered simultaneously share the same option_field.name.
  • Optional hardening: suffix the editing field id to guarantee uniqueness.

Apply if desired:

-:name="option_field.name"
+:name="option_field.name + '_' + editing_form_field.id"

Also applies to: 524-529

includes/Admin/Posting.php (3)

688-729: Hide “Add” button when max repeats reached.

Right now, at max count the “+” button still shows but does nothing. Read data-max-repeats here and hide the add button to avoid UX confusion.

Apply this diff:

-                    updateRepeatButtons: function($container) {
+                    updateRepeatButtons: function($container) {
                         var $instances = $container.find('.wpuf-repeat-instance');
                         var count = $instances.length;
+                        var maxRepeats = parseInt($container.data('max-repeats')) || -1;
@@
-                            // Add button: show only on last instance
-                            if (isLast) {
+                            // Add button: show only on last instance AND when under maxRepeats (if set)
+                            var canAddMore = (maxRepeats === -1) || (count < maxRepeats);
+                            if (isLast && canAddMore) {
                                 $controls.append(addButtonHtml);
                             }

607-626: IDs/labels may collide across instances when ids don’t use bracketed indexes.

You only rewrite id/for when they contain [...]. If an element’s id uses another scheme (e.g., hyphen-suffixed), clones can end up with duplicate ids and mismatched labels.

  • Option: store a data-base-id/data-base-for on first render and derive unique ids from it during cloning/reindexing.

734-765: MutationObserver scope/targets could be simplified.

Observing attribute changes on individual buttons is brittle (buttons are rebuilt) and may miss updates. Either:

  • Observe the container with {childList: true, subtree: true} and call a debounced updateRepeatButtons, or
  • Drop the observer and rely on explicit updateRepeatButtons calls (you already call it after add/remove/init).
assets/css/admin/form-builder.css (5)

1969-1976: Intersection z-index for pinned header cell.

The cell at the intersection of pinned rows and pinned columns should sit above both. Without it, edges can tear during scroll.

Apply this diff right after the sticky header rule:

   background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));
 }
 
+/* Ensure header+pinned intersection stays above */
+.wpuf-table :where(.wpuf-table-pin-rows):where(.wpuf-table-pin-cols) thead tr :is(th, td):first-child {
+  z-index: 2;
+}

927-937: Hover state doesn’t repaint pinned cells.

Row hover updates the row background but sticky pinned cells keep their base bg, producing a visual seam. Mirror the hover color onto pinned cells too.

   .wpuf-table tr.wpuf-hover:hover,
   .wpuf-table tr.wpuf-hover:nth-child(even):hover {
     --tw-bg-opacity: 1;
     background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));
   }
+
+  .wpuf-table tr.wpuf-hover:hover :where(.wpuf-table-pin-cols) :is(th, td) {
+    --tw-bg-opacity: 1;
+    background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));
+  }
 
   .wpuf-table-zebra tr.wpuf-hover:hover,
   .wpuf-table-zebra tr.wpuf-hover:nth-child(even):hover {
     --tw-bg-opacity: 1;
     background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)));
   }
+  .wpuf-table-zebra tr.wpuf-hover:hover :where(.wpuf-table-pin-cols) :is(th, td) {
+    --tw-bg-opacity: 1;
+    background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)));
+  }

1960-1967: Use logical alignment to simplify RTL.

Switch text-align: left to text-align: start so it auto-flips in RTL without extra overrides.

 .wpuf-table {
   position: relative;
   width: 100%;
   border-radius: var(--rounded-box, 1rem);
-  text-align: left;
+  text-align: start;
   font-size: 0.875rem;
   line-height: 1.25rem;
 }

2828-2831: Redundant RTL text-align override.

If you adopt text-align: start above, this RTL-specific override becomes unnecessary. Remove to reduce cascade complexity.

-.wpuf-table:where([dir="rtl"], [dir="rtl"] *) {
-  text-align: right;
-}

4081-4084: Confirm .wpuf-table { display: table; } won’t wrap a native <table>.

If .wpuf-table is applied to a wrapper div around a real <table>, forcing the wrapper to display: table can cause unexpected box metrics and interfere with layout/overflow. If the class is meant for the <table> element itself, all good; otherwise consider scoping to .wpuf-table table { display: table; }.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between aab3cf7 and d5a9268.

📒 Files selected for processing (5)
  • admin/form-builder/assets/js/components/field-radio/template.php (0 hunks)
  • assets/css/admin/form-builder.css (6 hunks)
  • assets/js-templates/form-components.php (2 hunks)
  • includes/Admin/Posting.php (4 hunks)
  • wpuf-functions.php (1 hunks)
💤 Files with no reviewable changes (1)
  • admin/form-builder/assets/js/components/field-radio/template.php
🔇 Additional comments (3)
assets/js-templates/form-components.php (2)

24-27: No-op cleanup looks good.

Whitespace-only change; no behavioral impact.


123-126: No-op cleanup looks good.

Whitespace-only change; no behavioral impact.

assets/css/admin/form-builder.css (1)

2980-3030: Bottom-nav size utilities look good.

Consistent height and label typography scale across xs/sm/md/lg; naming matches existing utility patterns.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Flattening repeat inner_fields drops columns > column-1 (data loss/regression risk).

Current logic keeps only column-1 and discards column-2, column-3, etc. Existing multi-column repeat fields would silently lose inner fields.

  • Fix: flatten all column buckets in order instead of only column-1.

Apply this diff:

-            // Ensure inner_fields is a simple array (not column structure)
-            if ( empty( $field['inner_fields'] ) ) {
-                $field['inner_fields'] = [];
-            } elseif ( isset( $field['inner_fields']['column-1'] ) ) {
-                // Convert column structure to simple array
-                $field['inner_fields'] = $field['inner_fields']['column-1'];
-            }
+            // Ensure inner_fields is a simple array (not column structure)
+            if ( empty( $field['inner_fields'] ) ) {
+                $field['inner_fields'] = [];
+            } elseif ( is_array( $field['inner_fields'] ) && isset( $field['inner_fields']['column-1'] ) ) {
+                // Convert column structure to simple array by concatenating all columns
+                $columns = $field['inner_fields'];
+                // keep natural order: column-1, column-2, ...
+                ksort( $columns, SORT_NATURAL );
+                $flat = [];
+                foreach ( $columns as $col => $items ) {
+                    if ( is_array( $items ) ) {
+                        foreach ( $items as $it ) {
+                            $flat[] = $it;
+                        }
+                    }
+                }
+                $field['inner_fields'] = $flat;
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Ensure inner_fields is a simple array (not column structure)
if ( empty( $field['inner_fields'] ) ) {
$old_id = $field['id'];
$old_meta = $field['name'];
$old_label = $field['label'];
$new_id = wpuf_form_field_id_generator();
$field['template'] = 'text_field';
// set the new compatible values
$field['id'] = $new_id;
$field['name'] = $old_meta . '_' . $new_id;
$field['label'] = '';
$field['inner_fields']['column-1'] = [ $field ];
$field['inner_fields']['column-2'] = [];
$field['inner_fields']['column-3'] = [];
$field['template'] = 'repeat_field';
$field['columns'] = 1;
$field['min_column'] = 1;
$field['max_column'] = 3;
$field['column_space'] = 5;
$field['id'] = $old_id;
$field['label'] = $old_label;
$field['name'] = $old_meta;
}
// if old repeat field format
if ( empty( $field['inner_columns_size'] ) ) {
$field['inner_columns_size']['column-1'] = '100%';
$field['inner_columns_size']['column-2'] = '100%';
$field['inner_columns_size']['column-3'] = '100%';
$field['inner_fields'] = [];
} elseif ( isset( $field['inner_fields']['column-1'] ) ) {
// Convert column structure to simple array
$field['inner_fields'] = $field['inner_fields']['column-1'];
}
// Ensure inner_fields is a simple array (not column structure)
if ( empty( $field['inner_fields'] ) ) {
$field['inner_fields'] = [];
} elseif ( is_array( $field['inner_fields'] ) && isset( $field['inner_fields']['column-1'] ) ) {
// Convert column structure to simple array by concatenating all columns
$columns = $field['inner_fields'];
// keep natural order: column-1, column-2, ...
ksort( $columns, SORT_NATURAL );
$flat = [];
foreach ( $columns as $col => $items ) {
if ( is_array( $items ) ) {
foreach ( $items as $it ) {
$flat[] = $it;
}
}
}
$field['inner_fields'] = $flat;
}
🤖 Prompt for AI Agents
In wpuf-functions.php around lines 1857 to 1863 the current logic flattens
repeat inner_fields by keeping only 'column-1', which drops 'column-2',
'column-3', etc.; change the code to detect a column-bucketed structure and
merge all column-* buckets in numeric order into a single simple array
(preserving field order), e.g. collect keys matching /^column-(\d+)$/, sort by
the captured index, then append each bucket's entries into
$field['inner_fields']; also ensure the existing empty/already-simple-array
branches remain unchanged so non-column structures are not altered.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
assets/css/admin/form-builder.css (1)

2076-2112: Duplicate .wpuf-table / pinned‑row/col rules and still‑broken pinned‑column behavior

This block duplicates the earlier .wpuf-table definitions (lines 2038–2074), including the pinned‑column rule:

  • .wpuf-table :where(.wpuf-table-pin-cols tr th) sets both left: 0 and right: 0, which will stretch sticky cells across the horizontal extent and can cause overlap when scrolling.
  • It only targets th, so pinned body cells (td) won’t stick with their header.
  • There’s no explicit stacking context/z‑index on pinned cells, so they may end up under neighbor content.

Given there’s already an earlier copy of the same rules, this both preserves the bug and bloats the CSS.

Consider (a) consolidating to a single .wpuf-table/pinning block and (b) updating the pinned‑column rules as previously suggested to use a single inline start offset, include both th and td, and add a stacking context, e.g.:

-.wpuf-table :where(.wpuf-table-pin-cols tr th) {
-  position: sticky;
-  left: 0px;
-  right: 0px;
-  --tw-bg-opacity: 1;
-  background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));
-}
-
-.wpuf-table-zebra tbody tr:nth-child(even) :where(.wpuf-table-pin-cols tr th) {
-  --tw-bg-opacity: 1;
-  background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));
-}
+.wpuf-table :where(.wpuf-table-pin-cols tr :is(th, td)) {
+  position: sticky;
+  inset-inline-start: 0;
+  z-index: 1;
+  --tw-bg-opacity: 1;
+  background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));
+}
+
+.wpuf-table-zebra :where(.wpuf-table-pin-cols) tbody tr:nth-child(even) :is(th, td) {
+  --tw-bg-opacity: 1;
+  background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));
+}

Apply this to whichever single block you keep.

🧹 Nitpick comments (4)
includes/Admin/Posting.php (3)

471-493: Repeat field admin rendering depends entirely on Pro Field_Repeat class

The special-case branch for template === 'repeat_field' && input_type === 'repeat' looks correct and keeps other fields on the existing rendering path. Note that in environments where WeDevs\Wpuf\Pro\Fields\Field_Repeat is not loaded, repeat fields will silently not render in the admin metabox. If that’s not intended, consider a graceful fallback (e.g., render first row via the generic field object as before).


581-649: Reindexing logic is a bit fragile and has some unused parameters/locals

Functionally this add/clone path will work as long as your repeat field markup follows the expected [0], [1], … pattern, but there are a few maintainability/robustness issues:

  • fieldName is passed into addRepeatInstance and reindexInstances but never used.
  • instanceIndex locals captured on click are never used.
  • The name / id / for updates rely on original*.replace(/\[\d+\]/, '[' + newIndex + ']'), which only rewrites the first numeric bracket. That’s fine for simple field[0] patterns, but will be a no-op if the first bracket is non‑numeric (e.g. field[key][0]) and may be surprising for more complex nested structures.

Consider tightening this by:

  • Dropping unused variables/parameters, or wiring fieldName into the selector to avoid over‑eager replacements.
  • Making the reindexing logic explicitly target the repeat index portion (e.g., fieldName[index][…]) instead of “first numeric bracket” so future field templates don’t accidentally break this assumption.

651-687: reindexInstances works but could over‑rewrite attributes in complex markup

reindexInstances correctly normalizes data-instance, name, id, and for across instances, which is key for saving meta reliably. The same /\[\d+\]/ replacement is applied to every element that happens to have a [ in its attribute, though, which could unintentionally touch nested or unrelated bracketed attributes inside the instance if such markup is ever introduced.

If you expect more complex repeat templates in the future, consider scoping the rewrite to attributes that start with the repeat field’s base name (e.g., ^${fieldName}\[\d+\]) rather than any bracketed segment.

assets/css/admin/form-builder.css (1)

3005-3023: Redundant table border/head/foot rules

The rules for:

  • .wpuf-table :where(thead tr, tbody tr:not(:last-child), tbody tr:first-child:last-child)
  • .wpuf-table :where(thead, tfoot)
  • .wpuf-table :where(tfoot)

are defined earlier (2981–2999) and then repeated verbatim here. Functionally harmless, but it increases CSS size and makes future edits easier to miss in one copy.

You can safely remove this second block and keep a single source of truth near the primary .wpuf-table definitions.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d5a9268 and 504a0ac.

📒 Files selected for processing (5)
  • admin/form-builder/assets/js/components/field-radio/template.php (0 hunks)
  • assets/css/admin/form-builder.css (2 hunks)
  • assets/js-templates/form-components.php (4 hunks)
  • includes/Admin/Posting.php (7 hunks)
  • wpuf-functions.php (1 hunks)
💤 Files with no reviewable changes (1)
  • admin/form-builder/assets/js/components/field-radio/template.php
🚧 Files skipped from review as they are similar to previous changes (2)
  • wpuf-functions.php
  • assets/js-templates/form-components.php
🔇 Additional comments (4)
includes/Admin/Posting.php (4)

558-579: initRepeatField assumes at least one .wpuf-repeat-instance per container

The initializer logic is straightforward and the event bindings look good. It does, however, assume that each .wpuf-repeat-container has at least one .wpuf-repeat-instance child (otherwise later code relying on .first() and .clone() will end up with an empty clone). Please confirm the markup always includes an initial instance; if not, add a guard that creates a first instance template or bails out cleanly.


689-730: Button update guard + MutationObserver: behavior is reasonable but edge‑casey

Using $container.data('updating-buttons') to guard updateRepeatButtons and then clearing it via setTimeout is a pragmatic way to avoid MutationObserver feedback loops. It does, however, mean that multiple updates within that 100ms window will be coalesced into one (or dropped), which might matter if other scripts mutate button classes/styles rapidly.

Given the admin‑only context this is probably acceptable; just be aware of the coalescing behavior if you later integrate other code that toggles these controls aggressively.


735-794: MutationObserver wiring for repeat controls looks safe

The MutationObserver definition and initial observation setup are sound, and the global observer is in scope for use inside addRepeatInstance. The additional re‑application of updateRepeatButtons after initialization (immediate, +100ms, +1000ms) should help keep the UI consistent with async field initializers.

No changes needed here; just ensure you’ve smoke‑tested add/remove in a few complex forms (with conditionals, file fields, etc.) to confirm the observer doesn’t cause unexpected reflows.


150-164: Admin field initializer assets: looks consistent, but ensure handle is registered

The Selectize/intlTelInput + wpuf-field-initialization enqueue + localization look fine for the admin metabox context; just confirm that the wpuf-field-initialization handle is registered (with its dependencies on selectize/intlTelInput) before this runs so the script and localized object are reliably available.

@Rubaiyat-E-Mohammad Rubaiyat-E-Mohammad added QA Approved This PR is approved by the QA team and removed needs: testing labels Nov 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

QA Approved This PR is approved by the QA team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants