Skip to content

Conversation

@emjay0921
Copy link
Contributor

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

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

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