Skip to content

Conversation

SebastianWiz
Copy link
Contributor

Context

⛑️ Ticket(s): https://secure.helpscout.net/conversation/3072037627/89219?viewId=8172236

Summary

This snippet lets users duplicate a child entry multiple times in one go: using the snippet, clicking “Duplicate” now prompts for a quantity, applies any maxCopies setting and field entry limit during validation, then runs the gpnf_duplicate_entry AJAX request that many times in sequence.

Loom video showing the snippet coming soon.

Copy link

coderabbitai bot commented Sep 18, 2025

Walkthrough

Adds a new JavaScript snippet for Gravity Perks Nested Forms that enables duplicating a child entry multiple times, with prompt-driven copy count, per-target configuration, sequential AJAX duplication, UI refreshes, and a public instance method to trigger the flow.

Changes

Cohort / File(s) Summary
Nested Forms: duplicate child entry multiple times
gp-nested-forms/gpnf-duplicate-child-entry-multiple-times.js
New script introducing: CONFIG for targets, init/binding to GPNF instances, sequential duplicate queue via AJAX to gpnf_duplicate_entry, UI refresh with GPNestedForms.loadEntry, public method gpnf.duplicateEntry(entryId, $trigger), form matching helper, auto-init via events/global scan.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Trigger as UI Trigger
  participant GPNF as GPNF Instance
  participant Server as AJAX Endpoint (gpnf_duplicate_entry)
  participant UI as GPNestedForms UI

  User->>Trigger: Click "Duplicate"
  Trigger->>GPNF: gpnf.duplicateEntry(entryId, $trigger)
  GPNF->>User: Prompt for copy count
  alt Valid count within limits
    loop For each copy (sequential)
      GPNF->>Server: POST duplicate_once(entryId, context)
      Server-->>GPNF: Success or Error
      alt Success
        GPNF->>UI: GPNestedForms.loadEntry(...)
        GPNF->>GPNF: gform doAction (notify)
      else Error
        GPNF-->>User: Alert error and stop sequence
      end
    end
    GPNF-->>User: Re-enable trigger, end
  else Invalid/zero count
    GPNF-->>User: Cancel / no-op
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • saifsultanc

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title explicitly references the new file and states a new snippet was added, which directly reflects the PR's primary change (adding a snippet to duplicate nested child entries multiple times). Including the filename in backticks is slightly noisy but the title remains clear and relevant for reviewers scanning history.
Description Check ✅ Passed The PR description follows the repository template by providing a Context section with a ticket link and a Summary that clearly explains the snippet's purpose, validation rules, and sequential duplication behavior; the note about a Loom video pending is acceptable. The description gives reviewers sufficient context and intent to evaluate the changes.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch SebastianWiz-patch-2

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

github-actions bot commented Sep 18, 2025

Fails
🚫

Commit message 'duplicate-child-entry-multiple-times.js: Added new snippet' needs to end in a period or exclamation.

Warnings
⚠️ When ready, don't forget to request reviews on this pull request from your fellow wizards.

Generated by 🚫 dangerJS against 472f2fc

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: 1

🧹 Nitpick comments (6)
gp-nested-forms/duplicate-child-entry-multiple-times.js (6)

74-91: Input handling: trim, sanitize, and safer current-count lookup.

Trim the prompt input, keep integer parsing explicit, and tolerate either observable or array for viewModel.entries.

 		gpnf.duplicateEntry = function( entryId, $trigger ) {
 
 			var message = config.prompt || 'How many times should this entry be duplicated?';
-			var input = window.prompt( message, '1' );
+			var input = ( window.prompt( message, '1' ) || '' ).trim();
 
 			if ( input === null ) {
 				return;
 			}
 
-			var copies = parseInt( input, 10 );
+			var copies = parseInt( input, 10 );
 
 			if ( isNaN( copies ) || copies < 1 ) {
 				copies = 1;
 			}
@@
 			if ( max !== '' && max != null ) {
 				max = parseInt( max, 10 );
 
 				if ( ! isNaN( max ) ) {
-					var current = gpnf.viewModel && gpnf.viewModel.entries ? gpnf.viewModel.entries().length : 0;
+					var current = 0;
+					if ( gpnf.viewModel && gpnf.viewModel.entries ) {
+						if ( typeof gpnf.viewModel.entries === 'function' ) {
+							current = gpnf.viewModel.entries().length;
+						} else if ( Array.isArray( gpnf.viewModel.entries ) ) {
+							current = gpnf.viewModel.entries.length;
+						}
+					}
 					copies = Math.min( copies, Math.max( max - current, 0 ) );
 				}
 			}

Also applies to: 99-106


112-117: Accessibility: reflect disabled state for non-button triggers.

Anchors won’t honor disabled; add aria-disabled/aria-busy and an optional CSS class.

 			var disableTarget = $trigger && typeof $trigger.prop === 'function' ? $trigger : null;
 
 			if ( disableTarget ) {
-				disableTarget.prop( 'disabled', true );
+				disableTarget.prop( 'disabled', true )
+					.attr( { 'aria-disabled': 'true', 'aria-busy': 'true' } )
+					.addClass( 'is-disabled' );
 			}
@@
 			} ).always( function() {
 				if ( disableTarget ) {
-					disableTarget.prop( 'disabled', false );
+					disableTarget.prop( 'disabled', false )
+						.attr( { 'aria-disabled': 'false' } )
+						.removeAttr( 'aria-busy' )
+						.removeClass( 'is-disabled' );
 				}
 			} );

118-124: Behavioral choice: stop-on-first-failure vs continue.

Current queue aborts on first failure. If partial success is preferable, catch per-iteration and continue, then surface a summary error.

Do you want the queue to continue duplicating after a single failure? If yes, I can provide a revised loop with per-call error capture.


137-151: Form-context check could be stricter.

If multiple forms with the same Nested Field ID are present and GFFORMID is undefined, this may initialize across forms. Prefer checking gpnf.$form against the current form element if available.

 	function matchesForm( gpnf ) {
 		if ( typeof gpnf.formId !== 'number' ) {
 			return false;
 		}
 
 		if ( typeof GFFORMID !== 'undefined' ) {
 			var currentFormId = parseInt( GFFORMID, 10 );
 
 			if ( ! isNaN( currentFormId ) ) {
 				return gpnf.formId === currentFormId;
 			}
 		}
 
-		return true;
+		return !!( gpnf.$form && gpnf.$form.length );
 	}

153-161: Avoid iterating inherited props on window.

Guard with hasOwnProperty to skip prototype pollution and non-enumerables.

-	for ( var key in window ) {
-		if ( key.indexOf( 'GPNestedForms_' ) === 0 ) {
-			init( window[ key ] );
-		}
-	}
+	for ( var key in window ) {
+		if ( Object.prototype.hasOwnProperty.call( window, key ) && key.indexOf( 'GPNestedForms_' ) === 0 ) {
+			init( window[ key ] );
+		}
+	}

16-25: Config UX: optional hard cap when no entry limit is set.

If entryLimitMax is unset, consider a conservative default cap (e.g., 50) to prevent accidental large queues.

Do you want me to add a defaultMaxCopies fallback applied only when neither maxCopies nor an entry limit is present?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0b6d8f6 and 3e06c46.

📒 Files selected for processing (1)
  • gp-nested-forms/duplicate-child-entry-multiple-times.js (1 hunks)
🔇 Additional comments (1)
gp-nested-forms/duplicate-child-entry-multiple-times.js (1)

74-134: No method-name collision — local override only. Repo search found gpnf.duplicateEntry defined only in gp-nested-forms/duplicate-child-entry-multiple-times.js (line 74); no other definitions or call sites detected.

Comment on lines +44 to +72
// Wrap GPNF's duplicate endpoint in a promise so we can queue requests sequentially.
var duplicateOnce = (function() {
var nonce = window.GPNFData && window.GPNFData.nonces ? window.GPNFData.nonces.duplicateEntry : '';

return function( entryId ) {
return $.post( gpnf.ajaxUrl, {
action: 'gpnf_duplicate_entry',
nonce: nonce,
gpnf_entry_id: entryId,
gpnf_parent_form_id: gpnf.formId,
gpnf_nested_form_field_id: gpnf.fieldId,
gpnf_context: gpnf.ajaxContext
} ).then( function( response ) {
if ( ! response || ! response.success ) {
return $.Deferred().reject( response && response.data ? response.data : 'Unable to duplicate entry.' );
}

if ( window.GPNestedForms && typeof window.GPNestedForms.loadEntry === 'function' ) {
window.GPNestedForms.loadEntry( response.data );
}

if ( window.gform && typeof window.gform.doAction === 'function' ) {
window.gform.doAction( 'gpnf_post_duplicate_entry', response.data.entry, response );
}

return response;
} );
};
})();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix: call instance loadEntry; add nonce guard; harden AJAX.

Use the instance method (gpnf.loadEntry) rather than a global static, fail fast if the nonce is missing, and add a timeout/dataType for robustness.

 		// Wrap GPNF's duplicate endpoint in a promise so we can queue requests sequentially.
 		var duplicateOnce = (function() {
-			var nonce = window.GPNFData && window.GPNFData.nonces ? window.GPNFData.nonces.duplicateEntry : '';
+			var nonce = window.GPNFData && window.GPNFData.nonces
+				? (window.GPNFData.nonces.duplicateEntry || window.GPNFData.nonces.duplicate_entry || '')
+				: '';
 
 			return function( entryId ) {
-				return $.post( gpnf.ajaxUrl, {
-					action: 'gpnf_duplicate_entry',
-					nonce: nonce,
-					gpnf_entry_id: entryId,
-					gpnf_parent_form_id: gpnf.formId,
-					gpnf_nested_form_field_id: gpnf.fieldId,
-					gpnf_context: gpnf.ajaxContext
-				} ).then( function( response ) {
+				if ( ! nonce ) {
+					return $.Deferred().reject( 'Missing nonce for duplicate action.' );
+				}
+
+				return $.ajax( {
+					url: gpnf.ajaxUrl,
+					method: 'POST',
+					dataType: 'json',
+					timeout: 15000,
+					data: {
+						action: 'gpnf_duplicate_entry',
+						nonce: nonce,
+						gpnf_entry_id: entryId,
+						gpnf_parent_form_id: gpnf.formId,
+						gpnf_nested_form_field_id: gpnf.fieldId,
+						gpnf_context: gpnf.ajaxContext
+					}
+				} ).then( function( response ) {
 					if ( ! response || ! response.success ) {
 						return $.Deferred().reject( response && response.data ? response.data : 'Unable to duplicate entry.' );
 					}
 
-					if ( window.GPNestedForms && typeof window.GPNestedForms.loadEntry === 'function' ) {
-						window.GPNestedForms.loadEntry( response.data );
-					}
+					if ( gpnf && typeof gpnf.loadEntry === 'function' ) {
+						gpnf.loadEntry( response.data );
+					} else if ( window.GPNestedForms && typeof window.GPNestedForms.loadEntry === 'function' ) {
+						window.GPNestedForms.loadEntry( response.data );
+					}
 
 					if ( window.gform && typeof window.gform.doAction === 'function' ) {
-						window.gform.doAction( 'gpnf_post_duplicate_entry', response.data.entry, response );
+						window.gform.doAction( 'gpnf_post_duplicate_entry', response.data.entry, response, gpnf );
 					}
 
 					return response;
 				} );
 			};
 		})();
📝 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
// Wrap GPNF's duplicate endpoint in a promise so we can queue requests sequentially.
var duplicateOnce = (function() {
var nonce = window.GPNFData && window.GPNFData.nonces ? window.GPNFData.nonces.duplicateEntry : '';
return function( entryId ) {
return $.post( gpnf.ajaxUrl, {
action: 'gpnf_duplicate_entry',
nonce: nonce,
gpnf_entry_id: entryId,
gpnf_parent_form_id: gpnf.formId,
gpnf_nested_form_field_id: gpnf.fieldId,
gpnf_context: gpnf.ajaxContext
} ).then( function( response ) {
if ( ! response || ! response.success ) {
return $.Deferred().reject( response && response.data ? response.data : 'Unable to duplicate entry.' );
}
if ( window.GPNestedForms && typeof window.GPNestedForms.loadEntry === 'function' ) {
window.GPNestedForms.loadEntry( response.data );
}
if ( window.gform && typeof window.gform.doAction === 'function' ) {
window.gform.doAction( 'gpnf_post_duplicate_entry', response.data.entry, response );
}
return response;
} );
};
})();
// Wrap GPNF's duplicate endpoint in a promise so we can queue requests sequentially.
var duplicateOnce = (function() {
var nonce = window.GPNFData && window.GPNFData.nonces
? (window.GPNFData.nonces.duplicateEntry || window.GPNFData.nonces.duplicate_entry || '')
: '';
return function( entryId ) {
if ( ! nonce ) {
return $.Deferred().reject( 'Missing nonce for duplicate action.' );
}
return $.ajax( {
url: gpnf.ajaxUrl,
method: 'POST',
dataType: 'json',
timeout: 15000,
data: {
action: 'gpnf_duplicate_entry',
nonce: nonce,
gpnf_entry_id: entryId,
gpnf_parent_form_id: gpnf.formId,
gpnf_nested_form_field_id: gpnf.fieldId,
gpnf_context: gpnf.ajaxContext
}
} ).then( function( response ) {
if ( ! response || ! response.success ) {
return $.Deferred().reject( response && response.data ? response.data : 'Unable to duplicate entry.' );
}
if ( gpnf && typeof gpnf.loadEntry === 'function' ) {
gpnf.loadEntry( response.data );
} else if ( window.GPNestedForms && typeof window.GPNestedForms.loadEntry === 'function' ) {
window.GPNestedForms.loadEntry( response.data );
}
if ( window.gform && typeof window.gform.doAction === 'function' ) {
window.gform.doAction( 'gpnf_post_duplicate_entry', response.data.entry, response, gpnf );
}
return response;
} );
};
})();
🤖 Prompt for AI Agents
In gp-nested-forms/duplicate-child-entry-multiple-times.js around lines 44 to
72, the duplicateOnce wrapper should fail early if the nonce is missing, use the
instance method gpnf.loadEntry (not window.GPNestedForms.loadEntry), and make
the AJAX call more robust by specifying dataType and a timeout and by using
$.ajax so you can reject on network/error conditions; update the code to check
that nonce and gpnf exist before sending, call gpnf.loadEntry(response.data)
when available, use $.ajax({ url: gpnf.ajaxUrl, method: 'POST', dataType:
'json', timeout: 10000, data: { ... } }).then(...).catch(...) and reject with
the response.data or a clear error string on failure.

@SebastianWiz SebastianWiz changed the title duplicate-child-entry-multiple-times.js: Added new snippet gpnf-duplicate-child-entry-multiple-times.js: Added new snippet Sep 18, 2025
@SebastianWiz SebastianWiz changed the title gpnf-duplicate-child-entry-multiple-times.js: Added new snippet gpnf-duplicate-child-entry-multiple-times.js: Added new snippet. Sep 18, 2025
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)
gp-nested-forms/gpnf-duplicate-child-entry-multiple-times.js (1)

44-72: Fix: call instance loadEntry; add nonce guard; harden AJAX.

Use the instance method (gpnf.loadEntry) rather than a global static, fail fast if the nonce is missing, and add a timeout/dataType for robustness.

 		// Wrap GPNF's duplicate endpoint in a promise so we can queue requests sequentially.
 		var duplicateOnce = (function() {
-			var nonce = window.GPNFData && window.GPNFData.nonces ? window.GPNFData.nonces.duplicateEntry : '';
+			var nonce = window.GPNFData && window.GPNFData.nonces
+				? (window.GPNFData.nonces.duplicateEntry || window.GPNFData.nonces.duplicate_entry || '')
+				: '';
 
 			return function( entryId ) {
-				return $.post( gpnf.ajaxUrl, {
-					action: 'gpnf_duplicate_entry',
-					nonce: nonce,
-					gpnf_entry_id: entryId,
-					gpnf_parent_form_id: gpnf.formId,
-					gpnf_nested_form_field_id: gpnf.fieldId,
-					gpnf_context: gpnf.ajaxContext
-				} ).then( function( response ) {
+				if ( ! nonce ) {
+					return $.Deferred().reject( 'Missing nonce for duplicate action.' );
+				}
+
+				return $.ajax( {
+					url: gpnf.ajaxUrl,
+					method: 'POST',
+					dataType: 'json',
+					timeout: 15000,
+					data: {
+						action: 'gpnf_duplicate_entry',
+						nonce: nonce,
+						gpnf_entry_id: entryId,
+						gpnf_parent_form_id: gpnf.formId,
+						gpnf_nested_form_field_id: gpnf.fieldId,
+						gpnf_context: gpnf.ajaxContext
+					}
+				} ).then( function( response ) {
 					if ( ! response || ! response.success ) {
 						return $.Deferred().reject( response && response.data ? response.data : 'Unable to duplicate entry.' );
 					}
 
-					if ( window.GPNestedForms && typeof window.GPNestedForms.loadEntry === 'function' ) {
-						window.GPNestedForms.loadEntry( response.data );
-					}
+					if ( gpnf && typeof gpnf.loadEntry === 'function' ) {
+						gpnf.loadEntry( response.data );
+					} else if ( window.GPNestedForms && typeof window.GPNestedForms.loadEntry === 'function' ) {
+						window.GPNestedForms.loadEntry( response.data );
+					}
 
 					if ( window.gform && typeof window.gform.doAction === 'function' ) {
-						window.gform.doAction( 'gpnf_post_duplicate_entry', response.data.entry, response );
+						window.gform.doAction( 'gpnf_post_duplicate_entry', response.data.entry, response, gpnf );
 					}
 
 					return response;
 				} );
 			};
 		})();
🧹 Nitpick comments (5)
gp-nested-forms/gpnf-duplicate-child-entry-multiple-times.js (5)

76-77: Consider improving user experience for input prompts.

The native window.prompt provides limited UX and may be blocked by browsers. Consider using a custom modal dialog for better user experience and validation feedback.

Would you like me to provide an implementation using a custom modal dialog that offers better validation feedback and a more polished user experience?


86-87: Add validation message when default value is applied.

When the user enters an invalid value (non-numeric or less than 1), the code silently defaults to 1 without informing the user. Consider showing a notification.

 			if ( isNaN( copies ) || copies < 1 ) {
+				console.warn( 'Invalid input. Defaulting to 1 copy.' );
 				copies = 1;
 			}

108-110: Add user feedback when reaching entry limit.

When the entry limit prevents duplication (copies < 1), the function returns silently. Users might be confused why nothing happened.

 			if ( copies < 1 ) {
+				if ( max !== '' && max != null ) {
+					window.alert( 'Entry limit reached. No additional copies can be created.' );
+				}
 				return;
 			}

119-123: Consider adding progress indication for multiple duplications.

When duplicating many entries, users have no visibility into the progress. Consider adding a progress indicator for better UX, especially for larger copy counts.

Would you like me to provide an implementation that displays a progress indicator during the duplication process?


157-161: Consider defensive programming for global object iteration.

Iterating over all global window properties could potentially process unexpected objects. Consider adding additional validation to ensure the objects are GPNF instances.

 	for ( var key in window ) {
 		if ( key.indexOf( 'GPNestedForms_' ) === 0 ) {
-			init( window[ key ] );
+			var obj = window[ key ];
+			if ( obj && typeof obj === 'object' && typeof obj.fieldId === 'number' ) {
+				init( obj );
+			}
 		}
 	}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3e06c46 and 472f2fc.

📒 Files selected for processing (1)
  • gp-nested-forms/gpnf-duplicate-child-entry-multiple-times.js (1 hunks)

@saifsultanc saifsultanc merged commit 54407eb into master Sep 20, 2025
5 of 6 checks passed
@saifsultanc saifsultanc deleted the SebastianWiz-patch-2 branch September 20, 2025 05:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants