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.
    • Added responsive bottom navigation size utilities (XS/SM/MD/LG).
  • Bug Fixes

    • Standardized radio field naming for correct grouping and submission.
    • Repeatable field controls in admin now reliably show add/remove buttons and maintain proper indexing.
  • Refactor

    • Simplified repeat-field normalization for more predictable form rendering.
    • Removed leftover debug logs.

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.
Copy link

coderabbitai bot commented Sep 5, 2025

Walkthrough

This PR changes radio input name bindings in templates, adds new table and bottom navigation CSS utilities, overhauls admin repeat-field rendering with MutationObserver-driven controls and reindexing, and simplifies repeat-field normalization in wpuf_get_form_fields by flattening inner_fields and removing multi-column conversion logic.

Changes

Cohort / File(s) Summary
Radio name binding updates
admin/form-builder/assets/js/components/field-radio/template.php, assets/js-templates/form-components.php
Replaced dynamic radio input name ('radio_'+id+'_'+name) with static binding to option_field.name across radio templates; minor whitespace cleanup.
Admin repeat-field logic
includes/Admin/Posting.php
Reworked repeat-field UI: MutationObserver-managed add/remove button rendering, instance indexing, attribute renaming, guarded updates, reinitialization hooks, and initial/state refresh routines. Removed debug logs.
Repeat-field normalization
wpuf-functions.php
Simplified wpuf_get_form_fields: removed legacy multi-column conversion; normalized inner_fields to flat arrays and flattened column-1 when present.
Table and nav CSS utilities
assets/css/admin/form-builder.css
Added .wpuf-table styling (sticky headers/footers/left cols, borders, hover, RTL, display rules) and bottom navigation size utilities (.wpuf-btm-nav-xx, label sizing).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

Dev Review Done

Poem

I hop through fields of forms and rows,
Pinning headers where the data flows.
Click-add, click-remove—instances align,
Names reindexed, everything’s fine.
Tables gleam, RTL in tow—
A bunny nods: “Ship it!” 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • 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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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.

Comment on lines +1985 to +1996
.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)));
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Pinned column rule likely broken (left and right set simultaneously) and excludes td.

Setting both left: 0 and right: 0 on sticky cells will stretch them and cause overlap; also only th is targeted (not td). Prefer logical properties and include both header/body cells. Add a stacking context for pinned cells.

Apply this diff:

-.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 :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 tbody tr:nth-child(even) :where(.wpuf-table-pin-cols tr th) {
+.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)));
 }
📝 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
.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)));
}
🤖 Prompt for AI Agents
In assets/css/admin/form-builder.css around lines 1985 to 1996, the
pinned-column rule sets both left:0 and right:0 (which stretches/stacks cells)
and only targets th, excluding td; change the rule to use logical
inset-inline-start (or left) only (remove right/right:auto), expand the selector
to include both th and td (e.g. :where(.wpuf-table-pin-cols tr th,
.wpuf-table-pin-cols tr td)), and add a stacking context (z-index and/or
transform: translateZ(0) or will-change) so pinned cells sit above other content
while preserving background color and opacity variables.

Comment on lines +1857 to 1863
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants