From 65e43dd88115b46e2d8585cf6dd7af4a89693e68 Mon Sep 17 00:00:00 2001 From: "Michael (mcm)" Date: Wed, 4 Jan 2023 09:22:01 +0000 Subject: [PATCH] [IMP] web: make kanban group header sticky This commit makes the kanban group header sticky, it means that when the user scrolls the headers will still be visible on top of the view. closes odoo/odoo#109060 Task: 2050014 Related: odoo/enterprise#35611 Signed-off-by: Aaron Bohy (aab) --- .../views/crm_kanban/crm_kanban_renderer.xml | 2 +- .../web/static/src/core/dropdown/dropdown.js | 10 +++ addons/web/static/src/core/position_hook.js | 6 +- .../src/views/kanban/kanban_controller.js | 10 ++- .../src/views/kanban/kanban_controller.scss | 34 ++++++++-- .../src/views/kanban/kanban_controller.xml | 1 + .../src/views/kanban/kanban_renderer.js | 67 +++++++++++-------- .../src/views/kanban/kanban_renderer.xml | 6 +- .../views/view_components/column_progress.js | 9 +++ .../views/view_components/column_progress.xml | 2 +- .../static/tests/views/kanban_view_tests.js | 42 ++++++++++++ 11 files changed, 144 insertions(+), 45 deletions(-) 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"]); + }); });