Skip to content

feat(spp_change_request_v2): improve CR creation wizard UX#29

Open
emjay0921 wants to merge 4 commits into19.0from
feat/cr-wizard-ux-improvements
Open

feat(spp_change_request_v2): improve CR creation wizard UX#29
emjay0921 wants to merge 4 commits into19.0from
feat/cr-wizard-ux-improvements

Conversation

@emjay0921
Copy link
Contributor

@emjay0921 emjay0921 commented Feb 11, 2026

Why is this change needed?

The CR creation wizard needed a better registrant search experience — searching by ID, live results, pagination, and improved UX flow from wizard → detail form → main CR form.

How was the change implemented?

  • Debounced search widget: Custom OWL SearchDelayField triggers search after 500ms typing delay
  • Search by name + IDs: Searches both name and reg_ids.value fields
  • Clickable HTML results: Custom CrSearchResultsField OWL widget renders results as a table with click-to-select via _selected_partner_id bridge field
  • Pagination: 10 results per page with Previous/Next navigation
  • ID display: Shows max 2 IDs per row with +N badge and native tooltip for overflow
  • Selected registrant info: Shows name, type, and all IDs as badges
  • Detail form improvements:
    • _compute_display_name on detail base shows CR reference instead of model,id
    • "Submit for Approval" → "Proceed" button that validates proposed changes exist before navigating to main CR form
  • Menu/view cleanup: Removed redundant New Request menu and View Registrant stat button; Preview Changes only in dev mode

Mirror of: https://github.com/OpenSPP/openspp-modules-v2/pull/294

New unit tests

Unit tests executed by the author

How to test manually

  1. Go to Change Requests → All Requests → New Request
  2. Select a type, type in the search field — results appear after 500ms
  3. Verify pagination works with Previous/Next when >10 results
  4. Click a row to select registrant, verify info card shows
  5. Click "Change Registrant" — previous search results should reappear
  6. Click "Create" — detail form opens with CR reference as breadcrumb name
  7. Make a change, click "Proceed" — navigates to main CR form
  8. Without changes, "Proceed" shows error

Related links

https://github.com/OpenSPP/openspp-modules-v2/pull/294


Note

Medium Risk
Touches core CR creation flow and introduces unsanitized HTML rendering (sanitize=False) plus new JS widgets/onchange bridges, which could cause UI regressions or injection risks if inputs aren’t strictly controlled.

Overview
Improves the Change Request creation wizard by replacing the registrant_id dropdown with a debounced, paginated live search (new search_delay and cr_search_results OWL field widgets) that searches by name and exact ID number, renders clickable HTML results, and supports switching the selected registrant.

Tightens the wizard→detail→CR workflow by adding action_proceed_to_cr (blocks proceeding if no proposed changes), setting detail records’ display_name to the CR reference, and updating detail form headers to use Proceed instead of submit actions.

Includes minor UX/access changes: hides the Preview smart button to dev-only, removes the redundant “New Request” menu item, renames the registrant link button, and gates “Check for Updates” in conflicts UI behind a new is_conflict_detection_enabled related flag.

Written by Cursor Bugbot for commit 6700624. This will update automatically on new commits. Configure here.

- Add debounced search field with custom OWL widget for live registrant search
- Search by name and registrant IDs (reg_ids.value)
- Render search results as clickable HTML table rows
- Show max 2 IDs per row with +N badge and tooltip for overflow
- Add pagination (10 per page) with Previous/Next navigation
- Display selected registrant info with all IDs as badges
- Preserve search state when changing registrant selection
- Add display_name to detail base using CR reference
- Replace Submit for Approval with Proceed button on detail forms
- Proceed validates proposed changes before navigating to main CR form
- Remove New Request menu item (use list view button instead)
- Remove View Registrant stat button, rename Open Form to View Registrant
- Show Preview Changes stat button only in developer mode
@gemini-code-assist
Copy link

Summary of Changes

Hello @emjay0921, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly overhauls the user experience for creating Change Requests, particularly focusing on the initial registrant selection process. The changes introduce a more dynamic and user-friendly search mechanism, providing live feedback and better information display. Additionally, the navigation between the wizard, detail forms, and the main Change Request form has been refined to ensure a smoother and more intuitive user journey, reducing friction and improving overall efficiency for users initiating change requests.

Highlights

  • Enhanced Registrant Search UX: Implemented a new registrant search experience in the CR creation wizard, featuring a debounced search input, live results, search by name and ID, and pagination. This replaces the previous dropdown-based selection.
  • Improved Workflow from Wizard to CR Form: Streamlined the flow from the registrant selection wizard to the detail form and then to the main Change Request form. The detail form now displays the CR reference in the breadcrumb and includes a 'Proceed' button that validates proposed changes before navigating to the main CR.
  • Detailed Registrant Information Display: The selected registrant's information is now presented more comprehensively, showing their name, type (individual/group), and all associated IDs as badges, with native tooltips for overflow.
  • UI Cleanup and Simplification: Removed redundant UI elements such as the 'New Request' menu item and the 'View Registrant' stat button. The 'Preview Changes' button is now restricted to developer mode.
  • New Custom OWL Widgets: Introduced custom OWL widgets: SearchDelayField for debounced input and CrSearchResultsField for rendering clickable, paginated HTML search results.
Changelog
  • spp_change_request_v2/manifest.py
    • Added new CSS, JavaScript, and XML assets for the custom search and results widgets.
  • spp_change_request_v2/models/change_request_detail_base.py
    • Imported necessary Odoo modules for API, translation, and user errors.
    • Overrode _compute_display_name to show the Change Request name instead of the default model,id format.
    • Introduced action_proceed_to_cr method to navigate to the parent Change Request form, including a check for existing proposed changes.
  • spp_change_request_v2/models/res_partner.py
    • Added a computed field reg_id_display to format and display registrant IDs.
  • spp_change_request_v2/static/src/css/cr_search_results.css
    • Added CSS rules to ensure the CR search results widget takes full width in the form.
  • spp_change_request_v2/static/src/js/cr_search_results_field.js
    • Added a new OWL component CrSearchResultsField to render HTML search results and handle row clicks for registrant selection and pagination.
  • spp_change_request_v2/static/src/js/create_change_request.js
    • Updated the comment to reflect that row click handling for the CR create wizard is now managed by cr_search_results_field.js.
  • spp_change_request_v2/static/src/js/search_delay_field.js
    • Added a new OWL component SearchDelayField for a character input field that triggers onchange events after a 500ms typing delay.
  • spp_change_request_v2/static/src/xml/cr_search_results_field.xml
    • Added the XML template for the CrSearchResultsField OWL component.
  • spp_change_request_v2/static/src/xml/search_delay_field.xml
    • Added the XML template for the SearchDelayField OWL component.
  • spp_change_request_v2/views/change_request_views.xml
    • Removed the 'View Registrant' button from the Change Request form's button box.
    • Restricted the 'Preview Changes' button to base.group_no_one (developer mode).
    • Renamed the 'Open Form' button to 'View Registrant' and updated its action name from action_open_registrant to action_view_registrant.
  • spp_change_request_v2/views/create_wizard_views.xml
    • Replaced the direct registrant_id field with new fields search_text (using search_delay widget) and search_results_html (using cr_search_results widget) for an improved search experience.
    • Added invisible bridge fields _selected_partner_id and _search_page to facilitate client-server communication for search.
    • Enhanced the display of selected registrant information and added a 'Change Registrant' button.
  • spp_change_request_v2/views/detail_add_member_views.xml
    • Changed the 'Submit for Approval' button to 'Proceed' and updated its action to action_proceed_to_cr.
  • spp_change_request_v2/views/detail_change_hoh_views.xml
    • Changed the 'Submit for Approval' button to 'Proceed' and updated its action to action_proceed_to_cr.
  • spp_change_request_v2/views/detail_create_group_views.xml
    • Changed the 'Submit for Approval' button to 'Proceed' and updated its action to action_proceed_to_cr.
  • spp_change_request_v2/views/detail_edit_group_views.xml
    • Changed the 'Submit for Approval' button to 'Proceed' and updated its action to action_proceed_to_cr.
  • spp_change_request_v2/views/detail_edit_individual_views.xml
    • Changed the 'Submit for Approval' button to 'Proceed' and updated its action to action_proceed_to_cr.
  • spp_change_request_v2/views/detail_exit_registrant_views.xml
    • Changed the 'Submit for Approval' button to 'Proceed' and updated its action to action_proceed_to_cr.
  • spp_change_request_v2/views/detail_merge_registrants_views.xml
    • Changed the 'Submit for Approval' button to 'Proceed' and updated its action to action_proceed_to_cr.
  • spp_change_request_v2/views/detail_remove_member_views.xml
    • Changed the 'Submit for Approval' button to 'Proceed' and updated its action to action_proceed_to_cr.
  • spp_change_request_v2/views/detail_split_household_views.xml
    • Changed the 'Submit for Approval' button to 'Proceed' and updated its action to action_proceed_to_cr.
  • spp_change_request_v2/views/detail_transfer_member_views.xml
    • Changed the 'Submit for Approval' button to 'Proceed' and updated its action to action_proceed_to_cr.
  • spp_change_request_v2/views/detail_update_id_views.xml
    • Changed the 'Submit for Approval' button to 'Proceed' and updated its action to action_proceed_to_cr.
  • spp_change_request_v2/views/menus.xml
    • Removed the redundant 'New Request' menu item.
  • spp_change_request_v2/wizards/create_wizard.py
    • Added new fields search_text, search_results_html, _selected_partner_id, and _search_page to manage the new search functionality.
    • Implemented _onchange_selected_partner to update the registrant_id based on the selected partner ID from the frontend.
    • Implemented _onchange_search_text to reset search page and trigger a new search when the search text changes.
    • Implemented _onchange_search_page to re-render search results when the page number changes.
    • Added _get_search_domain to construct the search query for registrants based on text and type.
    • Developed _render_search_results to perform the search, handle pagination, and generate HTML for displaying results.
    • Updated _compute_registrant_info to provide a more detailed display of the selected registrant, including all IDs as badges.
    • Added action_clear_registrant to allow users to clear the selected registrant and restart the search.
    • Modified _onchange_request_type to clear the search text when the request type changes.
Activity
  • The pull request author, emjay0921, has provided a detailed description outlining the necessity, implementation, and manual testing steps for the changes.
  • The author has confirmed that new unit tests were added and executed.
  • The PR description includes a mirror link to the original repository, indicating this might be a backport or cross-repository synchronization.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request significantly improves the user experience of the Change Request creation wizard by implementing a debounced, paginated search for registrants. The changes are well-executed, introducing new OWL components for the search functionality and updating the backend logic accordingly. The overall implementation is robust and enhances usability. My review includes a few suggestions to further improve performance, maintainability, and conciseness of the code.

Comment on lines +19 to +28
for rec in self:
if rec.reg_ids:
parts = []
for rid in rec.reg_ids:
if rid.value:
label = rid.id_type_as_str or "ID"
parts.append(f"{label} ({rid.value})")
rec.reg_id_display = ", ".join(parts) if parts else ""
else:
rec.reg_id_display = ""

Choose a reason for hiding this comment

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

medium

The logic in this method can be made more concise and Pythonic by using a list comprehension. This would eliminate the need for the outer if/else block and the conditional expression within the join call, making the code easier to read and maintain.

        parts = [
            f"{(rid.id_type_as_str or 'ID')} ({rid.value})"
            for rid in rec.reg_ids
            if rid.value
        ]
        rec.reg_id_display = ", ".join(parts)

Comment on lines +18 to +53
setup() {
this.containerRef = useRef("container");
onMounted(() => this._attachClickHandler());
onPatched(() => this._attachClickHandler());
}

get htmlContent() {
return this.props.record.data[this.props.name] || "";
}

_attachClickHandler() {
const el = this.containerRef.el;
if (!el) return;
// Row selection
el.querySelectorAll(".o_cr_search_result").forEach((row) => {
row.onclick = (ev) => {
ev.preventDefault();
ev.stopPropagation();
const partnerId = parseInt(row.dataset.partnerId);
if (partnerId) {
this.props.record.update({_selected_partner_id: partnerId});
}
};
});
// Pagination
el.querySelectorAll(".o_cr_page_prev, .o_cr_page_next").forEach((link) => {
link.onclick = (ev) => {
ev.preventDefault();
ev.stopPropagation();
const page = parseInt(link.dataset.page);
if (!isNaN(page) && page >= 0) {
this.props.record.update({_search_page: page});
}
};
});
}

Choose a reason for hiding this comment

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

medium

For better performance and maintainability, consider using event delegation for handling clicks instead of attaching individual onclick handlers to each result row and pagination link. By adding a single event listener to the container, you can avoid re-attaching handlers on every render (onPatched) and reduce the total number of event listeners, which is more efficient, especially with many search results.

Also, it's a good practice to always provide the radix parameter to parseInt to avoid unexpected behavior.

    setup() {
        this.containerRef = useRef("container");
        onMounted(() => {
            this.containerRef.el.addEventListener("click", this._onClick.bind(this));
        });
    }

    get htmlContent() {
        return this.props.record.data[this.props.name] || "";
    }

    _onClick(ev) {
        // Row selection
        const row = ev.target.closest(".o_cr_search_result");
        if (row) {
            ev.preventDefault();
            ev.stopPropagation();
            const partnerId = parseInt(row.dataset.partnerId, 10);
            if (partnerId) {
                this.props.record.update({_selected_partner_id: partnerId});
            }
            return;
        }

        // Pagination
        const pageLink = ev.target.closest(".o_cr_page_prev, .o_cr_page_next");
        if (pageLink) {
            ev.preventDefault();
            ev.stopPropagation();
            const page = parseInt(pageLink.dataset.page, 10);
            if (!isNaN(page) && page >= 0) {
                this.props.record.update({_search_page: page});
            }
        }
    }

<templates xml:space="preserve">

<t t-name="spp_change_request_v2.CrSearchResultsField">
<div t-ref="container" class="o_field_cr_search_results" style="width:100%" t-out="htmlContent"/>

Choose a reason for hiding this comment

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

medium

The inline style style="width:100%" is redundant, as the o_field_cr_search_results class already has its width set to 100% !important in the associated CSS file (cr_search_results.css). It's best practice to rely on the stylesheet for styling to improve maintainability and separation of concerns. Please remove the inline style.

Suggested change
<div t-ref="container" class="o_field_cr_search_results" style="width:100%" t-out="htmlContent"/>
<div t-ref="container" class="o_field_cr_search_results" t-out="htmlContent"/>

Comment on lines 262 to 348
def _render_search_results(self):
"""Search and render paginated HTML results."""
search_domain = self._get_search_domain()
total = self.env["res.partner"].search_count(search_domain)

if not total:
self.search_results_html = Markup(
"<p class='text-muted'>No registrants found.</p>"
)
return

page = self._search_page or 0
page_size = self._SEARCH_PAGE_SIZE
max_page = (total - 1) // page_size
page = min(page, max_page)

offset = page * page_size
partners = self.env["res.partner"].search(
search_domain, limit=page_size, offset=offset
)

rows = []
for p in partners:
# Build ALL IDs in "TypeName (value)" format, show max 2
id_parts = []
if p.reg_ids:
for rid in p.reg_ids:
if rid.value:
label = rid.id_type_as_str or "ID"
id_parts.append(f"{label} ({rid.value})")
if not id_parts:
id_html = Markup("")
id_title = ""
elif len(id_parts) <= 2:
id_html = escape(", ".join(id_parts))
id_title = ""
else:
visible = escape(", ".join(id_parts[:2]))
extra = len(id_parts) - 2
id_html = Markup(
'{} <span class="badge text-bg-secondary ms-1">'
"+{} <i class='fa fa-info-circle'></i></span>"
).format(visible, extra)
id_title = ", ".join(id_parts)
ptype = (
'<i class="fa fa-users"></i> Group'
if p.is_group
else '<i class="fa fa-user"></i> Individual'
)
rows.append(
Markup(
'<tr class="o_cr_search_result" style="cursor:pointer"'
' data-partner-id="{}" data-partner-name="{}">'
"<td>{}</td>"
'<td title="{}">{}</td>'
"<td>{}</td></tr>"
).format(
p.id,
escape(p.name or ""),
escape(p.name or ""),
escape(id_title),
id_html,
Markup(ptype),
)
)

table = Markup(
'<table class="table table-hover table-sm mb-0 w-100">'
"<thead><tr><th>Name</th><th>ID</th><th>Type</th></tr></thead>"
"<tbody>{}</tbody></table>"
).format(Markup("").join(rows))

# Pagination header
start = offset + 1
end = min(offset + page_size, total)
prev_cls = "text-muted" if page == 0 else "o_cr_page_prev"
next_cls = "text-muted" if page >= max_page else "o_cr_page_next"
pagination = Markup(
'<div class="d-flex justify-content-between align-items-center mb-2 px-1">'
'<small class="text-muted">{}-{} of {}</small>'
"<div>"
'<a class="{} me-3" style="cursor:pointer" data-page="{}">← Previous</a>'
'<a class="{}" style="cursor:pointer" data-page="{}">Next →</a>'
"</div></div>"
).format(start, end, total, prev_cls, page - 1, next_cls, page + 1)

self.search_results_html = pagination + table

Choose a reason for hiding this comment

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

medium

This method manually constructs a significant amount of HTML using string formatting. While it's done securely with Markup and escape, this approach can be difficult to read and maintain.

For better separation of concerns, consider refactoring this to use a server-side QWeb template. You could define the template in an XML file and render it using self.env['ir.qweb']._render(). This would make the Python code cleaner and the HTML structure more manageable.

…xact match for ID search

- Remove ID column from search results table (show Name and Type only)
- Remove ID badges from selected registrant info card
- Change reg_ids.value search from ilike (partial) to = (exact match)
@codecov
Copy link

codecov bot commented Feb 18, 2026

Codecov Report

❌ Patch coverage is 31.18280% with 64 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (19.0@619f6a8). Learn more about missing BASE report.

Files with missing lines Patch % Lines
spp_change_request_v2/wizards/create_wizard.py 29.23% 46 Missing ⚠️
...ge_request_v2/models/change_request_detail_base.py 35.71% 9 Missing ⚠️
spp_change_request_v2/models/res_partner.py 30.76% 9 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             19.0      #29   +/-   ##
=======================================
  Coverage        ?   62.98%           
=======================================
  Files           ?      239           
  Lines           ?    19213           
  Branches        ?        0           
=======================================
  Hits            ?    12101           
  Misses          ?     7112           
  Partials        ?        0           
Flag Coverage Δ
spp_api_v2_change_request 66.61% <ø> (?)
spp_base_common 92.81% <ø> (?)
spp_change_request_v2 72.09% <31.18%> (?)
spp_cr_types_advanced 98.43% <ø> (?)
spp_cr_types_base 98.50% <ø> (?)
spp_mis_demo_v2 75.34% <ø> (?)
spp_programs 49.57% <ø> (?)
spp_security 51.08% <ø> (?)
spp_starter_social_registry 87.50% <ø> (?)
spp_starter_sp_mis 86.95% <ø> (?)
spp_studio_change_requests 90.23% <ø> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

This is the final PR Bugbot will review for you during this billing cycle

Your free Bugbot reviews will reset on March 17

Details

Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

parts.append(f"{label} ({rid.value})")
rec.reg_id_display = ", ".join(parts) if parts else ""
else:
rec.reg_id_display = ""
Copy link

Choose a reason for hiding this comment

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

Unused computed field reg_id_display never referenced

Low Severity

The reg_id_display computed field and its _compute_reg_id_display method are added to res.partner but never referenced anywhere else — not in any XML view, template, or other Python code. The PR description mentions "ID display: Shows max 2 IDs per row with +N badge" and "Selected registrant info: Shows name, type, and all IDs as badges," but neither the search results table in _render_search_results nor the _compute_registrant_info method uses this field. This is dead code that appears to be leftover from an incomplete feature.

Fix in Cursor Fix in Web

def _onchange_request_type(self):
"""Clear registrant if it doesn't match the new target type."""
"""Clear registrant and search if type changes."""
self.search_text = False
Copy link

Choose a reason for hiding this comment

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

Redundant type-matching conditional after unconditional search clear

Low Severity

The newly added self.search_text = False at the top of _onchange_request_type triggers a cascading _onchange_search_text call, which unconditionally clears registrant_id. This makes the subsequent conditional check (lines 380–384 that only clear registrant_id when the target type mismatches) dead code — registrant_id will always be cleared by the cascading onchange regardless of whether the type matches. The conditional misleadingly suggests selective clearing still happens.

Fix in Cursor Fix in Web

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant