-
Notifications
You must be signed in to change notification settings - Fork 23
Bugfix/v16/activecampaign/pagination filtering #284
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bugfix/v16/activecampaign/pagination filtering #284
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR implements pagination and filtering improvements for the ActiveCampaign integration in Umbraco 16, addressing issues from #283. Instead of client-side filtering, the changes enable server-side filtering and pagination by passing search queries to the backend API.
Key changes:
- Added server-side search query parameter support throughout the data flow chain
- Modified the forms modal to trigger server calls for both filtering and pagination
- Implemented proper URI construction with URL encoding for search queries
Reviewed Changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| Umbraco.Cms.Integrations.Crm.ActiveCampaign.csproj | Version bumped to 5.1.0 for the new release |
| umbraco-package.json | Version bumped to 5.1.0 to match the project version |
| activecampaign-forms-modal.element.ts | Refactored filtering to use server-side search instead of client-side array filtering |
| activecampaign-forms.repository.ts | Added searchQuery parameter support to pass to the API service |
| activecampaign-forms.context.ts | Added searchQuery parameter to propagate the search term through the data layer |
| GetFormsPagedController.cs | Implemented URI building logic with search query support and proper URL encoding |
| form-picker-property-editor.element.ts | Removed extraneous blank line for code cleanup |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| return queryParamsDictionary.Count == 0 | ||
| ? uri | ||
| : string.Format("{0}&{1}", uri, string.Join("&", queryParamsDictionary.Select(kvp => $"{kvp.Key}={kvp.Value}"))); |
Copilot
AI
Oct 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The URI building logic concatenates query parameters manually. Consider using UriBuilder or QueryHelpers.AddQueryString for more maintainable and robust query string construction.
| async #handleFilterInput(event: UUIInputEvent) { | ||
| let query = (event.target.value as string) || ''; | ||
| query = query.toLowerCase(); | ||
| this._searchQuery = query; | ||
|
|
||
| const result = !query | ||
| ? this._forms | ||
| : this._forms.filter((form) => form.name.toLowerCase().includes(query)); | ||
|
|
||
| this._filteredForms = result; | ||
| await this.#loadForms(this._currentPageNumber, this._searchQuery); | ||
| } |
Copilot
AI
Oct 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The filter input handler triggers a server call on every keystroke without debouncing, which could result in excessive API requests. Consider implementing debouncing to reduce server load and improve performance.
| let query = (event.target.value as string) || ''; | ||
| query = query.toLowerCase(); | ||
| this._searchQuery = query; | ||
|
|
Copilot
AI
Oct 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When filtering is applied, the page number should be reset to 1 to avoid displaying empty results if the current page exceeds the filtered result set. Consider resetting this._currentPageNumber to 1 before calling #loadForms.
| this._currentPageNumber = 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| // Clear existing timeout | ||
| if (this.#filterTimeout) { | ||
| clearTimeout(this.#filterTimeout); | ||
| } | ||
|
|
||
| this._filteredForms = result; | ||
| this.#filterTimeout = setTimeout(async () => { | ||
| this._currentPageNumber = 1; | ||
| await this.#loadForms(this._currentPageNumber, this._searchQuery); | ||
| }, 500); |
Copilot
AI
Oct 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The timeout is not cleared when the component is destroyed, which could lead to attempted state updates after the element is removed from the DOM. Add cleanup in a disconnectedCallback or similar lifecycle method to clear this.#filterTimeout.
| { | ||
| var uri = $"{baseAddress}{ApiPath}?limit={Constants.DefaultPageSize}"; | ||
|
|
||
| Dictionary<string, string> queryParamsDictionary = new Dictionary<string, string>(); |
Copilot
AI
Oct 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Use object initializer syntax for better readability: var queryParamsDictionary = new Dictionary<string, string>(); or Dictionary<string, string> queryParamsDictionary = new(); (if using C# 9+).
| Dictionary<string, string> queryParamsDictionary = new Dictionary<string, string>(); | |
| var queryParamsDictionary = new Dictionary<string, string>(); |
|
|
||
| return queryParamsDictionary.Count == 0 | ||
| ? uri | ||
| : string.Format("{0}&{1}", uri, string.Join("&", queryParamsDictionary.Select(kvp => $"{kvp.Key}={kvp.Value}"))); |
Copilot
AI
Oct 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Use string interpolation instead of string.Format for better readability: $\"{uri}&{string.Join(\"&\", queryParamsDictionary.Select(kvp => $\"{kvp.Key}={kvp.Value}\"))}\"
| : string.Format("{0}&{1}", uri, string.Join("&", queryParamsDictionary.Select(kvp => $"{kvp.Key}={kvp.Value}"))); | |
| : $"{uri}&{string.Join("&", queryParamsDictionary.Select(kvp => $"{kvp.Key}={kvp.Value}"))}"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated 2 comments.
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| { | ||
| var uri = $"{baseAddress}{ApiPath}?limit={Constants.DefaultPageSize}"; | ||
|
|
||
| Dictionary<string, string> queryParamsDictionary = new(); |
Copilot
AI
Oct 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The queryParamsDictionary is declared but never populated or used. The query parameters are being added directly to the URI using QueryHelpers.AddQueryString. This unused variable should be removed along with the unnecessary conditional logic at lines 67-69 that references it.
| using Microsoft.AspNetCore.Mvc; | ||
| using Microsoft.AspNetCore.WebUtilities; | ||
| using Microsoft.Extensions.Options; | ||
| using System.Web; |
Copilot
AI
Oct 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The System.Web import at line 6 is unnecessary since QueryHelpers.AddQueryString from Microsoft.AspNetCore.WebUtilities is being used for URL encoding. Remove the unused System.Web import.
| using System.Web; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| } | ||
|
|
||
| disconnectedCallback() { | ||
| clearTimeout(this.#filterTimeout); |
Copilot
AI
Oct 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The clearTimeout call may fail if #filterTimeout is undefined. While clearTimeout handles undefined gracefully in browsers, the explicit check on line 93-95 suggests this code expects defined values. Consider adding a conditional check or calling super.disconnectedCallback() to follow the component lifecycle pattern.
| clearTimeout(this.#filterTimeout); | |
| super.disconnectedCallback(); | |
| if (this.#filterTimeout !== undefined) { | |
| clearTimeout(this.#filterTimeout); | |
| } |
| [ProducesResponseType(StatusCodes.Status403Forbidden)] | ||
| [ProducesResponseType(StatusCodes.Status500InternalServerError)] | ||
| public async Task<IActionResult> GetForms([FromQuery] int? page = 1) | ||
| public async Task<IActionResult> GetForms([FromQuery] int? page = 1, string? searchQuery = "") |
Copilot
AI
Oct 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The searchQuery parameter should be marked with [FromQuery] attribute for consistency with the page parameter and to make the API contract explicit. Without it, model binding may behave unexpectedly in certain scenarios.
| public async Task<IActionResult> GetForms([FromQuery] int? page = 1, string? searchQuery = "") | |
| public async Task<IActionResult> GetForms([FromQuery] int? page = 1, [FromQuery] string? searchQuery = "") |
|
|
||
| private string BuildRequestUri(string baseAddress, int page, string searchQuery) | ||
| { | ||
| var uri = $"{baseAddress}{ApiPath}?limit={Constants.DefaultPageSize}"; |
Copilot
AI
Oct 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The URI construction manually appends '?limit=' without checking if ApiPath already contains query parameters. If ApiPath includes a query string, this will produce malformed URIs. Use QueryHelpers.AddQueryString for the limit parameter as well to handle this correctly.
| var uri = $"{baseAddress}{ApiPath}?limit={Constants.DefaultPageSize}"; | |
| var uri = QueryHelpers.AddQueryString($"{baseAddress}{ApiPath}", "limit", Constants.DefaultPageSize.ToString()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated 1 comment.
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| disconnectedCallback() { | ||
| if (this.#filterTimeout) { | ||
| clearTimeout(this.#filterTimeout); | ||
| } | ||
| } |
Copilot
AI
Oct 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The disconnectedCallback should call super.disconnectedCallback() to ensure proper cleanup of parent class resources.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| } | ||
|
|
||
| #handleFilterInput(event: UUIInputEvent) { | ||
| async #handleFilterInput(event: UUIInputEvent) { |
Copilot
AI
Oct 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The method is marked as async but doesn't await the setTimeout callback. This can lead to unhandled promise rejections if the loadForms call fails. Consider removing async from the method signature since the actual async work happens in the timeout callback.
| async #handleFilterInput(event: UUIInputEvent) { | |
| #handleFilterInput(event: UUIInputEvent) { |
| var uri = $"{baseAddress}{ApiPath}?limit={Constants.DefaultPageSize}"; | ||
|
|
||
| if (page > 1) | ||
| { | ||
| uri = QueryHelpers.AddQueryString(uri, "offset", ((page - 1) * Constants.DefaultPageSize).ToString()); | ||
| } | ||
|
|
||
| if (!string.IsNullOrWhiteSpace(searchQuery)) | ||
| { | ||
| uri = QueryHelpers.AddQueryString(uri, "search", searchQuery); | ||
| } | ||
|
|
Copilot
AI
Oct 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The URI construction manually adds '?limit=' which could create malformed URIs if ApiPath already contains query parameters. Use QueryHelpers.AddQueryString for all parameters including 'limit' to ensure proper URI construction.
| var uri = $"{baseAddress}{ApiPath}?limit={Constants.DefaultPageSize}"; | |
| if (page > 1) | |
| { | |
| uri = QueryHelpers.AddQueryString(uri, "offset", ((page - 1) * Constants.DefaultPageSize).ToString()); | |
| } | |
| if (!string.IsNullOrWhiteSpace(searchQuery)) | |
| { | |
| uri = QueryHelpers.AddQueryString(uri, "search", searchQuery); | |
| } | |
| var uri = $"{baseAddress}{ApiPath}"; | |
| var queryParams = new Dictionary<string, string> | |
| { | |
| { "limit", Constants.DefaultPageSize.ToString() } | |
| }; | |
| if (page > 1) | |
| { | |
| queryParams["offset"] = ((page - 1) * Constants.DefaultPageSize).ToString(); | |
| } | |
| if (!string.IsNullOrWhiteSpace(searchQuery)) | |
| { | |
| queryParams["search"] = searchQuery; | |
| } | |
| uri = QueryHelpers.AddQueryString(uri, queryParams); |
| this.#filterTimeout = setTimeout(async () => { | ||
| this._currentPageNumber = 1; | ||
| await this.#loadForms(this._currentPageNumber, this._searchQuery); | ||
| }, 500); |
Copilot
AI
Oct 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The 500ms debounce timeout is hardcoded. Consider extracting this to a constant (e.g., SEARCH_DEBOUNCE_MS) to improve maintainability and make it easier to adjust the debounce delay if needed.
The fix from #283 for the ActiveCampaign integration targeting Umbraco 16, but instead of the AngularJS changes the
#handleFilterInputand#onPageChangewill trigger a call to the server for retrieving the list of forms.