/** @odoo-module alias=web_gantt.GanttRenderer */ import AbstractRenderer from 'web.AbstractRenderer'; import core from 'web.core'; import GanttRow from 'web_gantt.GanttRow'; import qweb from 'web.QWeb'; import session from 'web.session'; import utils from 'web.utils'; import ConnectorContainer from './connector/connector_container'; import { device, isDebug } from 'web.config'; import { ComponentWrapper, WidgetAdapterMixin } from 'web.OwlCompatibility'; const QWeb = core.qweb; const _t = core._t; export default AbstractRenderer.extend(WidgetAdapterMixin, { config: { GanttRow: GanttRow }, custom_events: _.extend({}, AbstractRenderer.prototype.custom_events, { start_dragging: '_onStartDragging', start_no_dragging: '_onStartNoDragging', stop_dragging: '_onStopDragging', stop_no_dragging: '_onStopNoDragging', }), DECORATIONS: [ 'decoration-secondary', 'decoration-success', 'decoration-info', 'decoration-warning', 'decoration-danger', ], sampleDataTargets: [ '.o_gantt_row', ], /** * @override * @param {Widget} parent * @param {Object} state * @param {Object} params * @param {boolean} params.canCreate * @param {boolean} params.canEdit * @param {boolean} params.canCellCreate * @param {Object} params.cellPrecisions * @param {string} params.colorField * @param {Object} params.fieldsInfo * @param {Object} params.SCALES * @param {string} params.string * @param {string} params.totalRow * @param {string} [params.popoverTemplate] */ init(parent, state, params) { this._super.apply(this, arguments); this.$draggedPill = null; this.$draggedPillClone = null; this.canCreate = params.canCreate; this.canCellCreate = params.canCellCreate; this.canEdit = params.canEdit; this.canPlan = params.canPlan; this.cellPrecisions = params.cellPrecisions; this.colorField = params.colorField; this.disableDragdrop = params.disableDragdrop; this.progressField = params.progressField; this.consolidationParams = params.consolidationParams; this.fieldsInfo = params.fieldsInfo; this.SCALES = params.SCALES; this.string = params.string; this.totalRow = params.totalRow; this.collapseFirstLevel = params.collapseFirstLevel; this.thumbnails = params.thumbnails; this.dependencyEnabled = params.dependencyEnabled; this.pillLabel = params.pillLabel; this.dependencyField = params.dependencyField this.rowWidgets = {}; // Pill decoration colors, By default display primary color for pill this.pillDecorations = _.chain(this.arch.attrs) .pick((value, key) => { return this.DECORATIONS.indexOf(key) >= 0; }).mapObject((value) => { return py.parse(py.tokenize(value)); }).value(); if (params.popoverTemplate) { this.popoverQWeb = new qweb(isDebug(), {_s: session.origin}); this.popoverQWeb.add_template(utils.json_node_to_xml(params.popoverTemplate)); } else { this.popoverQWeb = QWeb; } this.isRTL = _t.database.parameters.direction === "rtl"; this.template_to_use = "GanttView"; this.firstRendering = true; if (this.dependencyEnabled) { this._initialize_connectors(); this._preventHoverEffect = false; this._connectorsStrokeColors = this._getStrokeColors(); this._connectorsStrokeWarningColors = this._getStrokeWarningColors(); this._connectorsStrokeErrorColors = this._getStrokeErrorColors(); this._connectorsOutlineStrokeColor = this._getOutlineStrokeColors(); this._connectorsCssSelectors = { bullet: '.o_connector_creator_bullet', pill: '.o_gantt_pill', pillWrapper: '.o_gantt_pill_wrapper', wrapper: '.o_connector_creator_wrapper', groupByNoGroup: '.o_gantt_row_nogroup', }; this.events = Object.assign({ }, this.events, { 'mouseenter .o_gantt_pill, .o_connector_creator_wrapper': '_onPillMouseEnter', 'mouseleave .o_gantt_pill, .o_connector_creator_wrapper': '_onPillMouseLeave', }); } }, /** * Called each time the renderer is attached into the DOM. */ on_attach_callback() { this._isInDom = true; core.bus.on("keydown", this, this._onKeydown); core.bus.on("keyup", this, this._onKeyup); if (!this.disableDragdrop) { this._setRowsDroppable(); } if (this.dependencyEnabled) { WidgetAdapterMixin.on_attach_callback.call(this); // As we need the source and target of the connectors to be part of the dom, // we need to use the on_attach_callback in order to have the first rendering successful. this._mountConnectorContainer(); window.addEventListener('resize', this._throttledReRender); } }, /** * Called each time the renderer is detached from the DOM. */ on_detach_callback() { this._isInDom = false; core.bus.off("keydown", this, this._onKeydown); core.bus.off("keyup", this, this._onKeyup); _.invoke(this.rowWidgets, 'on_detach_callback'); if (this.dependencyEnabled) { WidgetAdapterMixin.on_detach_callback.call(this); this._connectorContainerComponent.unmount(); } }, /** * @override */ destroy() { this._super(...arguments); if (this.dependencyEnabled) { window.removeEventListener('resize', this._throttledReRender); } }, /** * @override */ async start() { await this._super(...arguments); if (this.dependencyEnabled) { this._connectorContainerComponent = new ComponentWrapper(this, ConnectorContainer, this._getConnectorContainerProps()); this._throttledReRender = _.throttle(async () => { await this.updateConnectorContainerComponent(); }, 100); } }, /** * Make sure the connectorManager Component is updated each time the view is updated. * * @override */ async update() { if (this.dependencyEnabled) { await this.updateConnectorContainerComponent(); } await this._super(...arguments); }, //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- /** * Sets the class on the gantt_view corresponding to the mode. * This class is used to prevent the magnifier and + buttons during connection creation. * * @param {boolean} in_creation */ set_connector_creation_mode(in_creation) { this.el.classList.toggle('o_grabbing', in_creation); }, /** * Toggles the highlighting of the connector. * * @param {ConnectorContainer.Connector.props} connector * @param {boolean} highlighted */ toggleConnectorHighlighting(connector, highlighted) { const masterPill = this._rowsAndRecordsDict.rows[connector.data.masterRowId].records[connector.data.masterId].pillElement; const slavePill = this._rowsAndRecordsDict.rows[connector.data.slaveRowId].records[connector.data.slaveId].pillElement; const sourceConnectorCreatorInfo = this._getConnectorCreatorInfo(masterPill); const targetConnectorCreatorInfo = this._getConnectorCreatorInfo(slavePill); if (!this._isConnectorCreatorDragged(sourceConnectorCreatorInfo)) { sourceConnectorCreatorInfo.pill.classList.toggle('highlight', highlighted); } if (!this._isConnectorCreatorDragged(targetConnectorCreatorInfo)) { targetConnectorCreatorInfo.pill.classList.toggle('highlight', highlighted); } }, /** * Toggles the preventConnectorsHover props of the connector container. * * @param {boolean} prevent */ togglePreventConnectorsHoverEffect(prevent){ this._preventHoverEffect = prevent; if (this.dependencyEnabled && this._shouldRenderConnectors()) { this._connectorContainerComponent.update(this._getConnectorContainerProps()); } }, /** * Toggles the highlighting of the pill and connector creator of the provided element. * * @param {HTMLElement} element * @param {boolean} highlighted */ async togglePillHighlighting(element, highlighted) { const connectorCreatorInfo = this._getConnectorCreatorInfo(element); if (connectorCreatorInfo.pill.dataset.id != 0) { const connectedConnectors = Object.values(this._connectors) .filter((connector) => { const ids = [connector.data.slaveId, connector.data.masterId]; return ids.includes( parseInt(connectorCreatorInfo.pill.dataset.id) ); }); if (connectedConnectors.length) { connectedConnectors.forEach((connector) => { connector.hovered = highlighted; connector.canBeRemoved = !highlighted; }); await this._connectorContainerComponent.update(this._getConnectorContainerProps()); } if (!(this._rowsAndRecordsDict && this._rowsAndRecordsDict.records[connectorCreatorInfo.pill.dataset.id] && this._rowsAndRecordsDict.records[connectorCreatorInfo.pill.dataset.id].rowsInfo)) return; for (const pill of Object.values(this._rowsAndRecordsDict.records[connectorCreatorInfo.pill.dataset.id].rowsInfo).map((rowInfo) => rowInfo.pillElement)) { const tempConnectorCreatorInfo = this._getConnectorCreatorInfo(pill); if (highlighted || !this._isConnectorCreatorDragged(tempConnectorCreatorInfo)) { tempConnectorCreatorInfo.pill.classList.toggle('highlight', highlighted); if (connectorCreatorInfo.pill === tempConnectorCreatorInfo.pill) { for (const connectorCreator of tempConnectorCreatorInfo.connectorCreators) { connectorCreator.classList.toggle('invisible', !highlighted); } } } } } }, /** * Re-render a given row and its sub-rows. This typically occurs when a row * is collapsed/expanded, to prevent from re-rendering the whole view. * * @param {Object} rowState part of the state concerning the row to update * @returns {Promise} */ updateRow(rowState) { const oldRowIds = [rowState.id].concat(rowState.childrenRowIds); const oldRows = []; oldRowIds.forEach((rowId) => { if (this.rowWidgets[rowId]) { oldRows.push(this.rowWidgets[rowId]); delete this.rowWidgets[rowId]; } }); this.proms = []; const rows = this._renderRows([rowState], rowState.groupedBy); const proms = this.proms; delete this.proms; return Promise.all(proms).then(() => { let $previousRow = oldRows[0].$el; rows.forEach((row) => { row.$el.insertAfter($previousRow); $previousRow = row.$el; }); _.invoke(oldRows, 'destroy'); if (!this.disableDragdrop) { this._setRowsDroppable(); } if (this.dependencyEnabled && this._shouldRenderConnectors()) { this.updateConnectorContainerComponent(); } }); }, /** * Update the ConnectorContainer component with updated connectors. * @returns {Promise} */ async updateConnectorContainerComponent() { await this._connectorContainerComponent.update(this._generateAndGetConnectorContainerProps()); }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * Applies the style to the connector depending on the gantt date start and stop values. * * @param {Object} connector * @param {Object} masterRecord the record the slaveRecord depends on. * @param {Object} slaveRecord the record that depends on the masterRecord. * @private */ _applySpecialColors(connector, masterRecord, slaveRecord) { let specialColors; if (slaveRecord[this.state.dateStartField].isBefore(masterRecord[this.state.dateStopField])) { specialColors = this._connectorsStrokeWarningColors; if (slaveRecord[this.state.dateStartField].isBefore(masterRecord[this.state.dateStartField])) { specialColors = this._connectorsStrokeErrorColors; } } if (specialColors) { connector['style'] = { stroke: { color: specialColors.stroke, hoveredColor: specialColors.hoveredStroke, } }; } }, /** * Updates the connectors state in regards to the records state and returns the props. * * @return {Object} the props to pass to the ConnectorContainer * @private */ _generateAndGetConnectorContainerProps() { this._preventHoverEffect = false; this._initialize_connectors(); if (this._shouldRenderConnectors()) { this._generateConnectors(); } return this._getConnectorContainerProps(); }, /** * Updates the connectors state according to the records state. * * @private */ _generateConnectors() { /* First we need to build a dictionary in order to be able to manage the cases when a record is present multiple times in the gantt view, in order to draw the connectors accordingly. Structure of dict: { records : { #ID_RECORD_1: { record: STATE_RECORD, rowsInfo: { #ID_ROW_1: { pillElement: HTMLElementPill1, }, ... } }, ... }, rows: { #ID_ROW_1: { records: { #ID_RECORD_1: { pillElement: HTMLElementPill1, record: STATE_RECORD }, ... } }, ... }, } */ this._rowsAndRecordsDict = { records: { }, rows: { }, }; for (const row of this.state.rows) { // We need to remove the closing "}]" from the row.id in order to ensure that things works // smoothly when collapse_first_level option is activated. Then we need to escape '"' & // '\' from the row.id before calling the querySelector. const rowElementSelector = `${this._connectorsCssSelectors.groupByNoGroup}[data-row-id^="${row.id.replace("}]", "").replace(/["\\]/g, '\\$&')}"]`; const rowElement = this.el.querySelector(rowElementSelector); if (!rowElement) continue; this._rowsAndRecordsDict.rows[row.id] = { records: { } }; for (const record of row.records) { if (!this._shouldRenderRecordConnectors(record)) { continue; } const recordElementSelector = `${this._connectorsCssSelectors.pill}[data-id="${record.id}"]`; const pillElement = rowElement.querySelector(recordElementSelector); this._rowsAndRecordsDict.rows[row.id].records[record.id] = { pillElement: pillElement, record: record, }; if (!(record.id in this._rowsAndRecordsDict.records)) { this._rowsAndRecordsDict.records[record.id] = { record: record, rowsInfo: { }, }; } this._rowsAndRecordsDict.records[record.id].rowsInfo[row.id] = { pillElement: pillElement, }; } } // Then we go over the rows and records one by one in order to create the connectors const connector_id_generator = { _value: 1, getNext() { return this._value++; } }; for (const record of this.state.records) { const connectors = this._generateConnectorsForRecord(record, connector_id_generator); Object.assign(this._connectors, connectors); } }, /** * Generates the connectors using the dependencyField of the provided slave record. * * @param {Object} slaveRecord the slave record. * @param {{ getNext(): Number }} connector_id_generator a connector_id generator. * @private */ _generateConnectorsForRecord(slaveRecord, connector_id_generator) { const result = {}; for (const masterId of slaveRecord[this.dependencyField]) { if (masterId in this._rowsAndRecordsDict.records) { let connectors = []; if (!this._rowsAndRecordsDict.records[slaveRecord.id]) continue; for (const slaveRowId in this._rowsAndRecordsDict.records[slaveRecord.id].rowsInfo) { if (!this._rowsAndRecordsDict.records[masterId]) continue; for (const masterRowId in this._rowsAndRecordsDict.records[masterId].rowsInfo) { /** * Having: * * B dependent on A * * C dependent on B * * D dependent on C * Prevent: * * Connectors between B & C that are not in the same group if B is in same group than C: * G1 B --- C B --- C * / \ / \ / \ * G2 A D => A D * \ / \ / \ / * G3 B --- C B --- C * * Connectors between A & B if A has already a link to B in the same group: * G1 --------- B --------- B * / / / * G2 A / => A * / * G3 A ----------- B A ----------- B * Allow: * * Connectors between C & B when A & B are always present in the same groups * G1 A ------ B A ------ B * / * G2 A => A ==== * \ * G3 A ------ B A ------ B */ if (masterRowId === slaveRowId || !( slaveRecord.id in this._rowsAndRecordsDict.rows[masterRowId].records || masterId in this._rowsAndRecordsDict.rows[slaveRowId].records ) || Object.keys(this._rowsAndRecordsDict.records[slaveRecord.id].rowsInfo).every( (rowId) => (masterRowId !== rowId && masterId in this._rowsAndRecordsDict.rows[rowId].records) ) || Object.keys(this._rowsAndRecordsDict.records[masterId].rowsInfo).every( (rowId) => (slaveRowId !== rowId && slaveRecord.id in this._rowsAndRecordsDict.rows[rowId].records) ) ) { connectors.push( this._generateConnector( masterRowId, this._rowsAndRecordsDict.records[masterId].record, slaveRowId, slaveRecord, connector_id_generator) ); } } } for (const connector of connectors) { result[connector.id] = connector; } } } return result; }, /** * * @param Number masterRowId the row id of the masterRecord (in order to handle m2m grouping) * @param {Object} masterRecord the record the slaveRecord depends on. * @param Number slaveRowId the row id of the slave record (in order to handle m2m grouping) * @param {Object} slaveRecord the record that depends on the masterRecord. * @param {{ getNext(): Number }} connector_id_generator a connector_id generator. * @return {Object} a connector for the provided parameters. * @private */ _generateConnector(masterRowId, masterRecord, slaveRowId, slaveRecord, connector_id_generator) { const masterRecordPill = this._rowsAndRecordsDict.rows[masterRowId].records[masterRecord.id].pillElement; const slaveRecordPill = this._rowsAndRecordsDict.rows[slaveRowId].records[slaveRecord.id].pillElement; let source = this._connectorContainerComponent.componentRef.comp.getAnchorsPositions(masterRecordPill); let target = this._connectorContainerComponent.componentRef.comp.getAnchorsPositions(slaveRecordPill); const connector = { id: connector_id_generator.getNext(), source: source.right, canBeRemoved: true, data: { slaveId: slaveRecord.id, slaveRowId: slaveRowId, masterId: masterRecord.id, masterRowId: masterRowId, }, target: target.left, }; this._applySpecialColors(connector, masterRecord, slaveRecord) return connector; }, /** * Determines if a dragged pill aims to be copied or updated * @private * @param {jQueryEvent} event */ _getAction(event) { return event.ctrlKey || event.metaKey ? 'copy': 'reschedule'; }, /** * Gets the connector creator info for the provided element. * * @param {HTMLElement} element HTMLElement with a class of either o_connector_creator_bullet, * o_connector_creator_wrapper, o_gantt_pill or o_gantt_pill_wrapper. * @returns {{pillWrapper: HTMLElement, pill: HTMLElement, connectorCreators: Array}} * @private */ _getConnectorCreatorInfo(element) { let connectorCreators = []; let pill = null; if (element.matches(this._connectorsCssSelectors.pillWrapper)) { element = element.querySelector(this._connectorsCssSelectors.pill); } if (element.matches(this._connectorsCssSelectors.bullet)) { element = element.closest(this._connectorsCssSelectors.wrapper); } if (element.matches(this._connectorsCssSelectors.pill)) { pill = element; connectorCreators = Array.from(element.parentElement.querySelectorAll(this._connectorsCssSelectors.wrapper)); } else if (element.matches(this._connectorsCssSelectors.wrapper)) { connectorCreators = [element]; pill = element.parentElement.querySelector(this._connectorsCssSelectors.pill); } return { pill: pill, pillWrapper: pill.parentElement, connectorCreators: connectorCreators, }; }, /** * Returns the props according to the current connectors state * * @returns {Object} the props to pass to the ConnectorContainer. * @private */ _getConnectorContainerProps() { return { connectors: this._connectors, defaultStyle: { slackness: 0.9, stroke: { color: this._connectorsStrokeColors.stroke, hoveredColor: this._connectorsStrokeColors.hoveredStroke, width: 2, }, outlineStroke: { color: this._connectorsOutlineStrokeColor.stroke, hoveredColor: this._connectorsOutlineStrokeColor.hoveredStroke, width: 1, } }, hoverEaseWidth: 10, preventHoverEffect: this._preventHoverEffect, sourceQuerySelector: this._connectorsCssSelectors.bullet, targetQuerySelector: this._connectorsCssSelectors.pillWrapper, onCreationAbort: this._onConnectorCreationAbort.bind(this), onCreationDone: this._onConnectorCreationDone.bind(this), onCreationStart: this._onConnectorCreationStart.bind(this), onMouseOut: this._onConnectorMouseOut.bind(this), onMouseOver: this._onConnectorMouseOver.bind(this), onRemoveButtonClick: this._onConnectorRemoveButtonClick.bind(this), onRescheduleLaterButtonClick: this._onConnectorRescheduleLaterButtonClick.bind(this), onRescheduleSoonerButtonClick: this._onConnectorRescheduleSoonerButtonClick.bind(this), }; }, /** * Gets the rgba css string corresponding to the provided parameters. * * @param {number} r - [0, 255] * @param {number} g - [0, 255] * @param {number} b - [0, 255] * @param {number} [a = 1] - [0, 1] * @return {string} the css color. * @private */ _getCssRGBAColor(r, g, b, a) { return `rgba(${ r }, ${ g }, ${ b }, ${ a || 1 })`; }, /** * Format focus date which is used to display in gantt header (see XML * template). * * @private */ _getFocusDateFormat() { const focusDate = this.state.focusDate; switch (this.state.scale) { case 'day': return focusDate.format('dddd, MMMM DD, YYYY'); case 'week': // const dateStart = focusDate.clone().startOf('week').format('DD MMMM YYYY'); const dateStart = focusDate.clone().startOf('week').format('YYYY MMMM DD '); // const dateEnd = focusDate.clone().endOf('week').format('DD MMMM YYYY'); const dateEnd = focusDate.clone().endOf('week').format('YYYY MMMM DD '); return _.str.sprintf('%s - %s', dateStart, dateEnd); case 'month': return focusDate.format('MMMM YYYY'); case 'year': return focusDate.format('YYYY'); default: break; } }, /** * Gets the outline stroke's rgba css strings for both the stroke and its hovered state in error state. * * @return {{ stroke: {string}, hoveredStroke: {string} }} * @private */ _getOutlineStrokeColors() { return this._getStrokeAndHoveredStrokeColor(255, 255, 255); }, /** * Get pills info * * @param {Object} row * @param {*} groupLevel */ _getPillsInfo(row, groupLevel) { return { resId: row.resId, pills: row.records, groupLevel: groupLevel, progressBar: row.progressBar, }; }, /** * Get dates between gantt start and gantt stop date to render gantt slots * * @private * @returns {Moment[]} */ _getSlotsDates() { const token = this.SCALES[this.state.scale].interval; const stopDate = this.state.stopDate; let day = this.state.startDate; const dates = []; while (day <= stopDate) { dates.push(day); day = day.clone().add(1, token); } return dates; }, /** * Gets the stroke's rgba css string corresponding to the provided parameters for both the stroke and its * hovered state. * * @param {number} r - [0, 255] * @param {number} g - [0, 255] * @param {number} b - [0, 255] * @return {{ stroke: {string}, hoveredStroke: {string} }} the css colors. * @private */ _getStrokeAndHoveredStrokeColor(r, g, b) { return { stroke: this._getCssRGBAColor(r, g, b, 0.5), hoveredStroke: this._getCssRGBAColor(r, g, b, 1), }; }, /** * Gets the stroke's rgba css strings for both the stroke and its hovered state. * * @return {{ stroke: {string}, hoveredStroke: {string} }} * @private */ _getStrokeColors() { return this._getStrokeAndHoveredStrokeColor(143, 143, 143); }, /** * Gets the stroke's rgba css strings for both the stroke and its hovered state in error state. * * @return {{ stroke: {string}, hoveredStroke: {string} }} * @private */ _getStrokeErrorColors() { return this._getStrokeAndHoveredStrokeColor(211, 65, 59); }, /** * Gets the stroke's rgba css strings for both the stroke and its hovered state in warning state. * * @return {{ stroke: {string}, hoveredStroke: {string} }} * @private */ _getStrokeWarningColors() { return this._getStrokeAndHoveredStrokeColor(236, 151, 31); }, /** * Initialize the _connectors attribute and delete its associated _rowsAndRecordsDict attribute. * @private */ _initialize_connectors() { this._connectors = { }; delete this._rowsAndRecordsDict; }, /** * Gets whether the provided connector creator is the source element of the currently dragged connector. * * @param {{pill: HTMLElement, connectorCreators: Array}} connectorCreatorInfo * @returns {boolean} * @private */ _isConnectorCreatorDragged(connectorCreatorInfo) { return this._connectorInCreation && this._connectorInCreation.data.sourceElement.dataset.id === connectorCreatorInfo.pill.dataset.id; }, /** * Mounts the ConnectorContainer Component if needed. * * @returns {Promise} * @private */ async _mountConnectorContainer() { this.el.classList.toggle('position-relative', true); if (this._connectorContainerComponent.status === 'mounted') { await this._connectorContainerComponent.unmount(); } await this._connectorContainerComponent.mount(this.el); await this.updateConnectorContainerComponent(); }, /** * Prepare view info which is used by GanttRow widget * * @private * @returns {Object} */ _prepareViewInfo() { return { colorField: this.colorField, progressField: this.progressField, consolidationParams: this.consolidationParams, state: this.state, fieldsInfo: this.fieldsInfo, slots: this._getSlotsDates(), pillDecorations: this.pillDecorations, popoverQWeb: this.popoverQWeb, activeScaleInfo: { precision: this.cellPrecisions[this.state.scale], interval: this.SCALES[this.state.scale].cellPrecisions[this.cellPrecisions[this.state.scale]], time: this.SCALES[this.state.scale].time, }, }; }, /** * @override * @private */ async _render() { await this._super(...arguments); if (this._isInDom && this.dependencyEnabled) { // If the renderer is not yet part of the dom (during first rendering), then // the call will be performed in the on_attach_callback. await this._mountConnectorContainer(); } }, /** * Renders gantt view and its rows. * * @override */ async _renderView() { const oldRowWidgets = Object.keys(this.rowWidgets).map((rowId) => { return this.rowWidgets[rowId]; }); this.rowWidgets = {}; this.viewInfo = this._prepareViewInfo(); this.proms = []; const rows = this._renderRows(this.state.rows, this.state.groupedBy); let totalRow; if (this.totalRow) { totalRow = this._renderTotalRow(); } this.proms.push(this._super.apply(this, arguments)); const proms = this.proms; delete this.proms; return Promise.all(proms).then(() => { _.invoke(oldRowWidgets, 'destroy'); if (this.firstRendering) { this._replaceElement(QWeb.render(this.template_to_use, {widget: this, isMobile: device.isMobile})); this.firstRendering = false; } else { const newContent = $(QWeb.render(this.template_to_use, {widget: this, isMobile: device.isMobile})); this.$el.html(newContent[0].innerHTML); } const $containment = $('
'); const $rowContainer = this.$('.o_gantt_row_container'); $rowContainer.append($containment); if (!this.state.groupedBy.length) { $containment.css(this.isRTL ? {right: 0} : {left: 0}); } rows.forEach((row) => { row.$el.appendTo($rowContainer); }); if (totalRow) { totalRow.$el.appendTo(this.$('.o_gantt_total_row_container')); } if (this._isInDom && !this.disableDragdrop) { this._setRowsDroppable(); } if (this.state.isSample) { this._renderNoContentHelper(); } }); }, /** * Render rows outside the DOM, so that we can insert them to the DOM once * they are all ready. * * @private * @param {Object[]} rows recursive structure of records according to * groupBys * @param {string[]} groupedBy * @returns {Promise} resolved with the row widgets */ _renderRows(rows, groupedBy) { let rowWidgets = []; const groupLevel = this.state.groupedBy.length - groupedBy.length; // FIXME: could we get rid of collapseFirstLevel in Renderer, and fully // handle this in Model? let hideSidebar = groupedBy.length === 0; if (this.collapseFirstLevel) { hideSidebar = this.state.groupedBy.length === 0; } rows.forEach((row) => { const pillsInfo = this._getPillsInfo(row, groupLevel); if (groupedBy.length) { pillsInfo.groupName = row.name; pillsInfo.groupedByField = row.groupedByField; } const params = { canCreate: this.canCreate, canCellCreate: this.canCellCreate, canEdit: this.canEdit, canPlan: this.canPlan, isGroup: row.isGroup, consolidate: (groupLevel === 0) && (this.state.groupedBy[0] === this.consolidationParams.maxField), hideSidebar: hideSidebar, isOpen: row.isOpen, disableDragdrop: this.disableDragdrop, rowId: row.id, fromServer: row.fromServer, scales: this.SCALES, unavailabilities: row.unavailabilities, pillLabel: this.pillLabel, }; if (this.thumbnails && row.groupedByField && row.groupedByField in this.thumbnails){ params.thumbnail = {model: this.fieldsInfo[row.groupedByField].relation, field: this.thumbnails[row.groupedByField],}; } rowWidgets.push(this._renderRow(pillsInfo, params)); if (row.isGroup && row.isOpen) { const subRowWidgets = this._renderRows(row.rows, groupedBy.slice(1)); rowWidgets = rowWidgets.concat(subRowWidgets); } }); return rowWidgets; }, /** * Render a row outside the DOM. * * Note that we directly call the private function _widgetRenderAndInsert to * prevent from generating a documentFragment for each row we have to * render. The Widget API should offer a proper way to start a widget * without inserting it anywhere. * * @private * @param {Object} pillsInfo * @param {Object} params * @returns {Promise} resolved when the row is ready */ _renderRow(pillsInfo, params) { const ganttRow = new this.config.GanttRow(this, pillsInfo, this.viewInfo, params); this.rowWidgets[ganttRow.rowId] = ganttRow; this.proms.push(ganttRow._widgetRenderAndInsert(() => {})); return ganttRow; }, /** * Renders the total row outside the DOM, so that we can insert it to the * DOM once all rows are ready. * * @returns {Promise