From 2f463b564b02237c4e8460d48fc2b71334e4f094 Mon Sep 17 00:00:00 2001 From: "Xavier Bol (xbo)" Date: Thu, 12 Oct 2023 10:46:52 +0200 Subject: [PATCH] [IMP] hr_org_chart: add drag&drop in hierarchy view This commit allows the user to drag and drop a record (hierarchy node) into another one to set the one hovered as the manager of the one dragged. It also allows to drag a record into a row to define the manager of the row as the manager of the record dragged. This commit also adds an new attribute on `hierarchy` tag to use when we define that view for a certain model to enable the drag and drop feature. task-3376776 --- addons/hr_org_chart/__manifest__.py | 4 + addons/hr_org_chart/models/ir_ui_view.py | 1 + .../src/hooks/hierarchy_node_draggable.js | 102 +++++++++++++ .../src/scss/hierarchy.variables.dark.scss | 3 + .../static/src/scss/hierarchy.variables.scss | 3 + .../views/hierarchy/hierarchy_arch_parser.js | 6 +- .../src/views/hierarchy/hierarchy_card.scss | 13 ++ .../src/views/hierarchy/hierarchy_card.xml | 2 +- .../src/views/hierarchy/hierarchy_model.js | 144 +++++++++++++++++- .../src/views/hierarchy/hierarchy_renderer.js | 79 ++++++++++ .../views/hierarchy/hierarchy_renderer.scss | 6 + .../views/hr_department_views.xml | 2 +- addons/hr_org_chart/views/hr_views.xml | 2 +- 13 files changed, 357 insertions(+), 10 deletions(-) create mode 100644 addons/hr_org_chart/static/src/hooks/hierarchy_node_draggable.js create mode 100644 addons/hr_org_chart/static/src/scss/hierarchy.variables.dark.scss create mode 100644 addons/hr_org_chart/static/src/scss/hierarchy.variables.scss diff --git a/addons/hr_org_chart/__manifest__.py b/addons/hr_org_chart/__manifest__.py index 24d3a6ea5a9..e546443fa5d 100644 --- a/addons/hr_org_chart/__manifest__.py +++ b/addons/hr_org_chart/__manifest__.py @@ -24,9 +24,13 @@ This module extend the employee form with a organizational chart. 'hr_org_chart/static/src/scss/variables.scss', ], 'web.assets_backend': [ + 'hr_org_chart/static/src/scss/hierarchy.variables.scss', 'hr_org_chart/static/src/fields/*', 'hr_org_chart/static/src/views/**/*', ], + 'web.assets_web_dark': [ + ('before', 'hr_org_chart/static/src/scss/hierarchy.variables.scss', 'hr_org_chart/static/src/scss/**/*.variables.dark.scss'), + ], 'web.qunit_suite_tests': [ 'hr_org_chart/static/tests/**/*', ], diff --git a/addons/hr_org_chart/models/ir_ui_view.py b/addons/hr_org_chart/models/ir_ui_view.py index e298aafdac9..d285816a7f0 100644 --- a/addons/hr_org_chart/models/ir_ui_view.py +++ b/addons/hr_org_chart/models/ir_ui_view.py @@ -15,6 +15,7 @@ HIERARCHY_VALID_ATTRIBUTES = { 'parent_field', 'child_field', 'icon', + 'draggable', } class View(models.Model): diff --git a/addons/hr_org_chart/static/src/hooks/hierarchy_node_draggable.js b/addons/hr_org_chart/static/src/hooks/hierarchy_node_draggable.js new file mode 100644 index 00000000000..81040cbb52d --- /dev/null +++ b/addons/hr_org_chart/static/src/hooks/hierarchy_node_draggable.js @@ -0,0 +1,102 @@ +/** @odoo-module */ + +import { onWillUnmount, reactive, useEffect, useExternalListener } from "@odoo/owl"; +import { useThrottleForAnimation } from "@web/core/utils/timing"; +import { pick } from "@web/core/utils/objects"; +import { makeDraggableHook } from "@web/core/utils/draggable_hook_builder"; + +const hookParams = { + name: "useHierarchyNodeDraggable", + acceptedParams: { + rows: [String], + }, + defaultParams: { + rows: null, + }, + onComputeParams({ ctx, params }) { + // Row selector + ctx.rowSelector = params.rows || null; + if (ctx.rowSelector) { + ctx.fullSelector = `${ctx.rowSelector} ${ctx.fullSelector}`; + } + }, + onDragStart(params) { + const { ctx, addListener, callHandler } = params; + + const onElementPointerEnter = (ev) => { + const element = ev.currentTarget; + current.hierarchyElement = element; + callHandler("onElementEnter", { element }); + }; + + const onElementPointerLeave = (ev) => { + const element = ev.currentTarget; + current.hierarchyElement = null; + callHandler("onElementLeave", { element }); + }; + + const onRowPointerEnter = (ev) => { + const row = ev.currentTarget; + current.hierarchyRow = row; + callHandler("onRowEnter", { row }); + }; + + const onRowPointerLeave = (ev) => { + const row = ev.currentTarget; + current.hierarchyRow = null; + callHandler("onRowLeave", { row }); + }; + + const { ref, current, elementSelector, rowSelector } = ctx; + + for (const rowEl of ref.el.querySelectorAll(rowSelector)) { + addListener(rowEl, "pointerenter", onRowPointerEnter); + addListener(rowEl, "pointerleave", onRowPointerLeave); + } + + for (const siblingEl of ref.el.querySelectorAll(elementSelector)) { + if (siblingEl !== current.element) { + addListener(siblingEl, "pointerenter", onElementPointerEnter); + addListener(siblingEl, "pointerleave", onElementPointerLeave); + } + } + + return pick(current, "element", "row"); + }, + onDragEnd({ ctx }) { + return pick(ctx.current, "element", "row", "hierarchyRow"); + }, + onDrop({ ctx }) { + const { current } = ctx; + const rowElement = current.hierarchyRow; + const element = current.hierarchyElement; + if ((rowElement && rowElement !== current.row) || element) { + return { + element: current.element, + row: current.row, + nextRow: rowElement && current.row !== rowElement ? rowElement : null, + newParentNode: element, + }; + } + }, + onWillStartDrag({ ctx }) { + const { current, rowSelector } = ctx; + + if (rowSelector) { + current.row = current.element.closest(rowSelector); + } + + return pick(current, "element", "row"); + }, +}; + +export function useHierarchyNodeDraggable(params) { + const setupHooks = { + addListener: useExternalListener, + setup: useEffect, + teardown: onWillUnmount, + throttle: useThrottleForAnimation, + wrapState: reactive, + } + return makeDraggableHook({ ...hookParams, setupHooks })(params); +} diff --git a/addons/hr_org_chart/static/src/scss/hierarchy.variables.dark.scss b/addons/hr_org_chart/static/src/scss/hierarchy.variables.dark.scss new file mode 100644 index 00000000000..f6da125defc --- /dev/null +++ b/addons/hr_org_chart/static/src/scss/hierarchy.variables.dark.scss @@ -0,0 +1,3 @@ +$hierarchy_element_dragged-background: $gray-100 !default; +$hierarchy_element_hover-background: #{darken($gray-100, 5%)} !default; +$hierarchy_element_hover-border: #{$gray-100} !default; diff --git a/addons/hr_org_chart/static/src/scss/hierarchy.variables.scss b/addons/hr_org_chart/static/src/scss/hierarchy.variables.scss new file mode 100644 index 00000000000..b499e58b595 --- /dev/null +++ b/addons/hr_org_chart/static/src/scss/hierarchy.variables.scss @@ -0,0 +1,3 @@ +$hierarchy_element_dragged-background: white !default; +$hierarchy_element_hover-background: #e6f2f3 !default; +$hierarchy_element_hover-border: #017e84 !default; diff --git a/addons/hr_org_chart/static/src/views/hierarchy/hierarchy_arch_parser.js b/addons/hr_org_chart/static/src/views/hierarchy/hierarchy_arch_parser.js index 079922e26f4..86f002b97e4 100644 --- a/addons/hr_org_chart/static/src/views/hierarchy/hierarchy_arch_parser.js +++ b/addons/hr_org_chart/static/src/views/hierarchy/hierarchy_arch_parser.js @@ -2,12 +2,13 @@ import { visitXML } from "@web/core/utils/xml"; import { Field } from "@web/views/fields/field"; -import { getActiveActions } from "@web/views/utils"; +import { archParseBoolean, getActiveActions } from "@web/views/utils"; export class HierarchyArchParser { parse(xmlDoc, models, modelName) { const archInfo = { activeActions: getActiveActions(xmlDoc), + draggable: false, icon: "fa-share-alt o_hierarchy_icon", parentFieldName: "parent_id", fieldNodes: {}, @@ -45,6 +46,9 @@ export class HierarchyArchParser { } archInfo.childFieldName = childFieldName; } + if (node.hasAttribute("draggable")) { + archInfo.draggable = archParseBoolean(node.getAttribute("draggable")); + } if (node.hasAttribute("icon")) { archInfo.icon = node.getAttribute("icon"); } diff --git a/addons/hr_org_chart/static/src/views/hierarchy/hierarchy_card.scss b/addons/hr_org_chart/static/src/views/hierarchy/hierarchy_card.scss index bfed09e5eaf..f9d3ce1f18b 100644 --- a/addons/hr_org_chart/static/src/views/hierarchy/hierarchy_card.scss +++ b/addons/hr_org_chart/static/src/views/hierarchy/hierarchy_card.scss @@ -15,6 +15,19 @@ height: 30px; } + &.o_hierarchy_dragged { + .o_hierarchy_node { + background-color: $hierarchy_element_dragged-background; + } + } + + &.o_hierarchy_hover { + .o_hierarchy_node { + background-color: $hierarchy_element_hover-background; + border: 3px solid green !important; + } + } + .o_hierarchy_node { &:hover { cursor: pointer; diff --git a/addons/hr_org_chart/static/src/views/hierarchy/hierarchy_card.xml b/addons/hr_org_chart/static/src/views/hierarchy/hierarchy_card.xml index cd9ad21ae0c..4e30411c9b1 100644 --- a/addons/hr_org_chart/static/src/views/hierarchy/hierarchy_card.xml +++ b/addons/hr_org_chart/static/src/views/hierarchy/hierarchy_card.xml @@ -2,7 +2,7 @@ -
+