-
Notifications
You must be signed in to change notification settings - Fork 0
feat(spp_change_request_v2): improve CR creation wizard UX #29
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
base: 19.0
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| /* Force the search results widget to take full width in the form */ | ||
| .o_field_cr_search_results, | ||
| .o_field_widget:has(.o_field_cr_search_results) { | ||
| width: 100% !important; | ||
| max-width: 100% !important; | ||
| display: block !important; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| /** @odoo-module **/ | ||
|
|
||
| import {Component, onMounted, onPatched, useRef} from "@odoo/owl"; | ||
| import {_t} from "@web/core/l10n/translation"; | ||
| import {registry} from "@web/core/registry"; | ||
| import {standardFieldProps} from "@web/views/fields/standard_field_props"; | ||
|
|
||
| /** | ||
| * Custom widget that renders HTML search results and handles row clicks. | ||
| * When a row with class "o_cr_search_result" is clicked, it writes the | ||
| * partner ID to the _selected_partner_id bridge field, which triggers | ||
| * a server-side onchange to set registrant_id. | ||
| */ | ||
| export class CrSearchResultsField extends Component { | ||
| static template = "spp_change_request_v2.CrSearchResultsField"; | ||
| static props = {...standardFieldProps}; | ||
|
|
||
| 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}); | ||
| } | ||
| }; | ||
| }); | ||
| } | ||
|
Comment on lines
+18
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For better performance and maintainability, consider using event delegation for handling clicks instead of attaching individual Also, it's a good practice to always provide the radix parameter to 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});
}
}
} |
||
| } | ||
|
|
||
| export const crSearchResultsField = { | ||
| component: CrSearchResultsField, | ||
| displayName: _t("CR Search Results"), | ||
| supportedTypes: ["html"], | ||
| }; | ||
|
|
||
| registry.category("fields").add("cr_search_results", crSearchResultsField); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| /** @odoo-module **/ | ||
|
|
||
| import {Component, useEffect, useRef, onWillUnmount} from "@odoo/owl"; | ||
| import {_t} from "@web/core/l10n/translation"; | ||
| import {useDebounced} from "@web/core/utils/timing"; | ||
| import {registry} from "@web/core/registry"; | ||
| import {standardFieldProps} from "@web/views/fields/standard_field_props"; | ||
|
|
||
| /** | ||
| * Char field that triggers onchange after a typing delay (500ms). | ||
| * Used for search fields where we want live results without waiting for blur. | ||
| */ | ||
| export class SearchDelayField extends Component { | ||
| static template = "spp_change_request_v2.SearchDelayField"; | ||
| static props = { | ||
| ...standardFieldProps, | ||
| placeholder: {type: String, optional: true}, | ||
| }; | ||
|
|
||
| setup() { | ||
| this.inputRef = useRef("input"); | ||
|
|
||
| this.debouncedCommit = useDebounced((value) => { | ||
| this.props.record.update({[this.props.name]: value}); | ||
| }, 500); | ||
|
|
||
| // Keep input in sync when record updates externally (e.g. onchange clears it) | ||
| useEffect( | ||
| () => { | ||
| const el = this.inputRef.el; | ||
| if (el) { | ||
| const recordValue = this.props.record.data[this.props.name] || ""; | ||
| if (el !== document.activeElement || !recordValue) { | ||
| el.value = recordValue; | ||
| } | ||
| } | ||
| }, | ||
| () => [this.props.record.data[this.props.name]] | ||
| ); | ||
| } | ||
|
|
||
| get value() { | ||
| return this.props.record.data[this.props.name] || ""; | ||
| } | ||
|
|
||
| onInput(ev) { | ||
| this.debouncedCommit(ev.target.value); | ||
| } | ||
| } | ||
|
|
||
| export const searchDelayField = { | ||
| component: SearchDelayField, | ||
| displayName: _t("Search with Delay"), | ||
| supportedTypes: ["char"], | ||
| extractProps: ({placeholder}) => ({placeholder}), | ||
| }; | ||
|
|
||
| registry.category("fields").add("search_delay", searchDelayField); |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,8 @@ | ||||||
| <?xml version="1.0" encoding="UTF-8" ?> | ||||||
| <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"/> | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The inline style
Suggested change
|
||||||
| </t> | ||||||
|
|
||||||
| </templates> | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| <?xml version="1.0" encoding="UTF-8" ?> | ||
| <templates xml:space="preserve"> | ||
|
|
||
| <t t-name="spp_change_request_v2.SearchDelayField"> | ||
| <input t-ref="input" | ||
| type="text" | ||
| class="o_input" | ||
| t-att-value="value" | ||
| t-att-placeholder="props.placeholder || ''" | ||
| t-att-readonly="props.readonly ? 'readonly' : undefined" | ||
| t-on-input="onInput"/> | ||
| </t> | ||
|
|
||
| </templates> |
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 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/elseblock and the conditional expression within thejoincall, making the code easier to read and maintain.