diff --git a/addons/crm/static/src/views/crm_kanban/crm_kanban_renderer.xml b/addons/crm/static/src/views/crm_kanban/crm_kanban_renderer.xml index f90080e2218..6016e1355cf 100644 --- a/addons/crm/static/src/views/crm_kanban/crm_kanban_renderer.xml +++ b/addons/crm/static/src/views/crm_kanban/crm_kanban_renderer.xml @@ -2,7 +2,7 @@ - + diff --git a/addons/web/static/src/core/dropdown/dropdown.js b/addons/web/static/src/core/dropdown/dropdown.js index 3a30e527aa2..95a80d24e84 100644 --- a/addons/web/static/src/core/dropdown/dropdown.js +++ b/addons/web/static/src/core/dropdown/dropdown.js @@ -122,6 +122,12 @@ export class Dropdown extends Component { } }, }; + if (this.props.container) { + positioningOptions.container = () => + typeof this.props.container === "function" + ? this.props.container() + : this.props.container; + } this.state.directionCaretClass = DIRECTION_CARET_CLASS[direction]; this.togglerRef = useRef("togglerRef"); if (this.props.toggler === "parent") { @@ -390,5 +396,9 @@ Dropdown.props = { type: Boolean, optional: true, }, + container: { + type: [Element, Function], + optional: true, + }, }; Dropdown.template = "web.Dropdown"; diff --git a/addons/web/static/src/core/position_hook.js b/addons/web/static/src/core/position_hook.js index 53e1f8d5de7..6962aba3a14 100644 --- a/addons/web/static/src/core/position_hook.js +++ b/addons/web/static/src/core/position_hook.js @@ -11,7 +11,7 @@ import { onWillUnmount, useEffect, useExternalListener, useRef } from "@odoo/owl /** * @typedef {{ * popper?: string; - * container?: HTMLElement; + * container?: HTMLElement | (() => HTMLElement); * margin?: number; * position?: Direction | Position; * onPositioned?: PositionEventHandler; @@ -92,6 +92,10 @@ function getBestPosition(reference, popper, { container, margin, position }) { variantKey === "fit" ? FIT_FLIP_ORDER[directionKey] : DIRECTION_FLIP_ORDER[directionKey]; const variants = VARIANT_FLIP_ORDER[variantKey]; + if (typeof container === "function") { + container = container(); + } + // Boxes const popBox = popper.getBoundingClientRect(); const refBox = reference.getBoundingClientRect(); diff --git a/addons/web/static/src/views/kanban/kanban_controller.js b/addons/web/static/src/views/kanban/kanban_controller.js index e65e0ea729e..bce6c0e24d8 100644 --- a/addons/web/static/src/views/kanban/kanban_controller.js +++ b/addons/web/static/src/views/kanban/kanban_controller.js @@ -35,13 +35,13 @@ export class KanbanController extends Component { rootState, }); - const rootRef = useRef("root"); - useViewButtons(this.model, rootRef, { + this.rootRef = useRef("root"); + useViewButtons(this.model, this.rootRef, { beforeExecuteAction: this.beforeExecuteActionButton.bind(this), afterExecuteAction: this.afterExecuteActionButton.bind(this), }); useSetupView({ - rootRef, + rootRef: this.rootRef, getGlobalState: () => { return { resIds: this.model.root.records.map((rec) => rec.resId), // WOWL: ask LPE why? @@ -121,6 +121,10 @@ export class KanbanController extends Component { async afterExecuteActionButton(clickParams) {} async onUpdatedPager() {} + + scrollTop() { + this.rootRef.el.querySelector(".o_content").scrollTo({ top: 0 }); + } } KanbanController.template = `web.KanbanView`; diff --git a/addons/web/static/src/views/kanban/kanban_controller.scss b/addons/web/static/src/views/kanban/kanban_controller.scss index 1af7bb3c790..f98899a71ed 100644 --- a/addons/web/static/src/views/kanban/kanban_controller.scss +++ b/addons/web/static/src/views/kanban/kanban_controller.scss @@ -60,7 +60,7 @@ &:focus, &:focus-within { - z-index: 1; // show the shadow on top of the previous & next cards in grouped mode + margin-bottom: 0; // show the shadow on top of the previous & next cards in grouped mode } // ------- Kanban Record, v11 Layout ------- @@ -511,9 +511,35 @@ background-color: var(--KanbanColumn__highlight-background) !important; box-shadow: -1px 0px 0px 0px var(--KanbanColumn__highlight-border) inset, 1px 0px 0px 0px var(--KanbanColumn__highlight-border) inset; + + .o_kanban_header { + // hack to display correct background color + margin: 0 calc(-1 * var(--KanbanGroup-padding-h) + 1px); + padding: 0 calc(var(--KanbanGroup-padding-h) - 1px); + } } } + .o_kanban_group .o_kanban_header, + .o_quick_create_folded { + position: sticky; // keep it visible + z-index: 1; + } + + .o_kanban_group:not(.o_column_folded) .o_kanban_header { + background-color: $o-webclient-background-color; + + .dropdown-item { + max-width: calc(var(--KanbanRecord-width) * 0.75); + text-overflow: ellipsis; + overflow: hidden; + } + } + + .o_quick_create_folded { + top: 1rem; + } + .o_column_quick_create, .o_kanban_group:not(.o_column_folded) .o_kanban_header_title { // Makes them come on top of the "no-content" background gradient. @@ -562,12 +588,6 @@ .o_column_quick_create { scroll-snap-align: center; overflow-y: scroll; - - .o_kanban_header { - position: sticky; // keep it visible - z-index: 1; - background-color: inherit; - } } } diff --git a/addons/web/static/src/views/kanban/kanban_controller.xml b/addons/web/static/src/views/kanban/kanban_controller.xml index e44abc58231..05aa60d05db 100644 --- a/addons/web/static/src/views/kanban/kanban_controller.xml +++ b/addons/web/static/src/views/kanban/kanban_controller.xml @@ -15,6 +15,7 @@ forceGlobalClick="props.forceGlobalClick" openRecord.bind="openRecord" noContentHelp="props.info.noContentHelp" + scrollTop.bind="scrollTop" /> diff --git a/addons/web/static/src/views/kanban/kanban_renderer.js b/addons/web/static/src/views/kanban/kanban_renderer.js index 6896cf6f3a1..bc4c62ac7e2 100644 --- a/addons/web/static/src/views/kanban/kanban_renderer.js +++ b/addons/web/static/src/views/kanban/kanban_renderer.js @@ -25,6 +25,29 @@ const DRAGGABLE_GROUP_TYPES = ["many2one"]; const MOVABLE_RECORD_TYPES = ["char", "boolean", "integer", "selection", "many2one"]; export class KanbanRenderer extends Component { + static template = "web.KanbanRenderer"; + static components = { + Dropdown, + DropdownItem, + ColumnProgress, + KanbanColumnQuickCreate, + KanbanRecord, + KanbanRecordQuickCreate, + }; + static props = [ + "archInfo", + "Compiler?", // optional in stable for backward compatibility + "list", + "openRecord", + "readonly", + "forceGlobalClick?", + "noContentHelp?", + "scrollTop?", + ]; + static defaultProps = { + scrollTop: () => {}, + }; + setup() { this.dialogClose = []; this.state = useState({ @@ -40,12 +63,12 @@ export class KanbanRenderer extends Component { // Sortable let dataRecordId; let dataGroupId; - const rootRef = useRef("root"); + this.rootRef = useRef("root"); if (this.canUseSortable) { useSortable({ enable: () => this.canResequenceRecords, // Params - ref: rootRef, + ref: this.rootRef, elements: ".o_record_draggable", ignore: ".dropdown", groups: () => this.props.list.isGrouped && ".o_kanban_group", @@ -66,7 +89,7 @@ export class KanbanRenderer extends Component { useSortable({ enable: () => this.canResequenceGroups, // Params - ref: rootRef, + ref: this.rootRef, elements: ".o_group_draggable", handle: ".o_column_title", cursor: "move", @@ -81,7 +104,7 @@ export class KanbanRenderer extends Component { }); } - useBounceButton(rootRef, (clickedEl) => { + useBounceButton(this.rootRef, (clickedEl) => { if (!this.props.list.count || this.props.list.model.useSampleModel) { return clickedEl.matches( [ @@ -105,7 +128,7 @@ export class KanbanRenderer extends Component { if (model.useSampleModel || !model.hasData()) { return; } - const firstCard = rootRef.el.querySelector(".o_kanban_record"); + const firstCard = this.rootRef.el.querySelector(".o_kanban_record"); if (firstCard) { // Focus first kanban card firstCard.focus(); @@ -127,10 +150,10 @@ export class KanbanRenderer extends Component { } return; }, - { area: () => rootRef.el } + { area: () => this.rootRef.el } ); - const arrowsOptions = { area: () => rootRef.el, allowRepeat: true }; + const arrowsOptions = { area: () => this.rootRef.el, allowRepeat: true }; if (this.env.searchModel) { useHotkey( "ArrowUp", @@ -260,7 +283,7 @@ export class KanbanRenderer extends Component { if (!this.env.isSmall && group.isFolded) { classes.push("o_column_folded"); } - if (!group.isFolded && !group.hasActiveProgressValue) { + if (!group.isFolded) { classes.push("bg-100"); } if (group.progressBars.length) { @@ -447,13 +470,18 @@ export class KanbanRenderer extends Component { }); } + getGroupEl(groupId) { + return this.rootRef.el.querySelector(`.o_kanban_group[data-id="${groupId}"]`); + } + // ------------------------------------------------------------------------ // Handlers // ------------------------------------------------------------------------ - onGroupClick(group) { + async onGroupClick(group, ev) { if (!this.env.isSmall && group.isFolded) { - group.toggle(); + await group.toggle(); + this.props.scrollTop(); } } @@ -603,22 +631,3 @@ export class KanbanRenderer extends Component { }; } } - -KanbanRenderer.props = [ - "archInfo", - "Compiler?", // optional in stable for backward compatibility - "list", - "openRecord", - "readonly", - "forceGlobalClick?", - "noContentHelp?", -]; -KanbanRenderer.components = { - Dropdown, - DropdownItem, - ColumnProgress, - KanbanColumnQuickCreate, - KanbanRecord, - KanbanRecordQuickCreate, -}; -KanbanRenderer.template = "web.KanbanRenderer"; diff --git a/addons/web/static/src/views/kanban/kanban_renderer.xml b/addons/web/static/src/views/kanban/kanban_renderer.xml index 414c3a5f421..e5aa39e879c 100644 --- a/addons/web/static/src/views/kanban/kanban_renderer.xml +++ b/addons/web/static/src/views/kanban/kanban_renderer.xml @@ -15,10 +15,10 @@ t-on-click="(ev) => this.onGroupClick(group, ev)" >
-
+
- + @@ -56,7 +56,7 @@
- +
diff --git a/addons/web/static/src/views/view_components/column_progress.js b/addons/web/static/src/views/view_components/column_progress.js index 4adca00e199..0f918bcc0ea 100644 --- a/addons/web/static/src/views/view_components/column_progress.js +++ b/addons/web/static/src/views/view_components/column_progress.js @@ -11,5 +11,14 @@ export class ColumnProgress extends Component { static props = { aggregate: { type: Object }, group: { type: Object }, + onBarClicked: { type: Function, optional: true }, }; + static defaultProps = { + onBarClicked: () => {}, + }; + + async onBarClick(progressBar) { + await this.props.group.filterProgressValue(progressBar.value); + this.props.onBarClicked(); + } } diff --git a/addons/web/static/src/views/view_components/column_progress.xml b/addons/web/static/src/views/view_components/column_progress.xml index 9d78e83d8b5..509b5a59080 100644 --- a/addons/web/static/src/views/view_components/column_progress.xml +++ b/addons/web/static/src/views/view_components/column_progress.xml @@ -15,7 +15,7 @@ t-att-aria-valuenow="progressBar.count" t-attf-data-tooltip="{{ progressBar.count }} {{ progressBar.string }}" data-tooltip-delay="0" - t-on-click="() => props.group.filterProgressValue(progressBar.value)" + t-on-click="() => this.onBarClick(progressBar)" />
diff --git a/addons/web/static/tests/views/kanban_view_tests.js b/addons/web/static/tests/views/kanban_view_tests.js index 5c13502ee02..ff302cffca1 100644 --- a/addons/web/static/tests/views/kanban_view_tests.js +++ b/addons/web/static/tests/views/kanban_view_tests.js @@ -12314,4 +12314,46 @@ QUnit.module("Views", (hooks) => { ); assert.containsN(target, ".o_kanban_group .o_kanban_record", 16); }); + + QUnit.test("scroll on group unfold and progressbar click", async (assert) => { + assert.expect(7); + + await makeView({ + type: "kanban", + resModel: "partner", + serverData, + arch: ` + + + + Record + + `, + groupBy: ["product_id"], + async mockRPC(route, args, performRPC) { + if (args.method === "web_read_group") { + const result = await performRPC(route, args); + if (result.groups.length) { + result.groups[0].__fold = false; + result.groups[1].__fold = true; + } + return result; + } + }, + }); + + const content = target.querySelector(".o_content"); + content.scrollTo = (params) => { + assert.step("scrolled"); + assert.strictEqual(params.top, 0); + }; + + await click(getProgressBars(0)[0]); + assert.verifySteps(["scrolled"]); + + const column1 = getColumn(1); + assert.hasClass(column1, "o_column_folded"); + await click(column1); + assert.verifySteps(["scrolled"]); + }); });