diff --git a/dist/tryton-sao.js b/dist/tryton-sao.js index 4c0b763..02bb1c1 100644 --- a/dist/tryton-sao.js +++ b/dist/tryton-sao.js @@ -3610,7 +3610,14 @@ var Sao = { }; Sao.common.parse_datetime = function(datetime_format, value) { - var date = moment(value, Sao.common.moment_format(datetime_format)); + var formats = [ + Sao.common.moment_format(datetime_format), + 'YYYY-MM-DDTHH:mm', + 'YYYY-MM-DDTHH:mm:ss', + 'YYYY-MM-DD HH:mm', + 'YYYY-MM-DD HH:mm:ss', + ]; + var date = moment(value, formats, true); if (date.isValid()) { date = Sao.DateTime(date.year(), date.month(), date.date(), date.hour(), date.minute(), date.second(), @@ -16797,6 +16804,21 @@ function eval_pyson(value){ this.input.appendTo(this.icon); Sao.common.ICONFACTORY.get_icon_img('tryton-date') .appendTo(this.icon); + this.icon.on('click', (event_) => { + if (this.date.prop('readonly')) { + return; + } + if (event_.target == this.input[0]) { + return; + } + event_.preventDefault(); + event_.stopPropagation(); + if (this.input[0].showPicker) { + this.input[0].showPicker(); + } else { + this.input.trigger('click'); + } + }); } this.date.change(this.focus_out.bind(this)); var mousetrap = new Mousetrap(this.date[0]); @@ -16885,7 +16907,7 @@ function eval_pyson(value){ Sao.View.Form.DateTime = Sao.class_(Sao.View.Form.Date, { class_: 'form-datetime', _input: 'datetime-local', - _input_format: '%Y-%m-%dT%H:%M:%S', + _input_format: '%Y-%m-%dT%H:%M', _format: Sao.common.format_datetime, _parse: Sao.common.parse_datetime, get_format: function() { @@ -16910,9 +16932,10 @@ function eval_pyson(value){ _parse: Sao.common.parse_time, init: function(view, attributes) { Sao.View.Form.Time._super.init.call(this, view, attributes); - if (~navigator.userAgent.indexOf("Firefox")) { - // time input on Firefox does not have a pop-up - this.input.parent().hide(); + if (this.icon) { + this.input.remove(); + this.icon.remove(); + delete this.icon; } }, get_format: function() { @@ -18351,6 +18374,7 @@ function eval_pyson(value){ view_ids: (attributes.view_ids || '').split(','), views_preload: attributes.views || {}, order: attributes.order, + one2many: true, row_activate: this.activate.bind(this), exclude_field: attributes.relation_field || null, limit: null, @@ -18577,12 +18601,22 @@ function eval_pyson(value){ this.screen.domain = domain; } this.screen.size_limit = size_limit; + if (this.attributes.height !== undefined && + this.screen.current_view && + this.screen.current_view.set_embedded_height) { + this.screen.current_view.set_embedded_height( + this.attributes.height); + } this.screen.display(); if (this.attributes.height !== undefined) { this.screen.current_view.el .find('.treeview,.list-form').first() .css('min-height', this.attributes.height + 'px') .css('max-height', this.attributes.height + 'px'); + if (this.screen.current_view.set_embedded_height) { + this.screen.current_view.set_embedded_height( + this.attributes.height); + } } }); }, @@ -21981,8 +22015,26 @@ function eval_pyson(value){ }, _parse_field: function(node, attributes) { var name = attributes.name; - var ColumnFactory = Sao.View.TreeXMLViewParser.WIDGETS[ - attributes.widget]; + var field = this.view.screen.model.fields[name]; + var widget = attributes.widget || (field && field.description.type); + var column_widget = widget == 'variation' ? + (field && field.description.type || 'char') : widget; + attributes.widget = widget; + var ColumnFactory = Sao.View.TreeXMLViewParser.WIDGETS[column_widget]; + if (!ColumnFactory) { + console.error('[TREE][UNKNOWN_WIDGET]', { + viewId: this.view.view_id, + model: this.view.screen && this.view.screen.model_name, + field: name, + widget: column_widget, + attributes: attributes, + node: node.outerHTML, + availableWidgets: Object.keys( + Sao.View.TreeXMLViewParser.WIDGETS).sort(), + }); + attributes.widget = 'char'; + ColumnFactory = Sao.View.Tree.CharColumn; + } var column = new ColumnFactory(this.view.screen.model, attributes); if (!this.view.widgets[name]) { this.view.widgets[name] = []; @@ -22022,9 +22074,21 @@ function eval_pyson(value){ column.prefixes.push( new Sao.View.Tree.Symbol(attributes, 0)); } + var field_attrs = this.field_attrs[name] || + (field && field.description) || {}; + if (!this.field_attrs[name]) { + console.warn('[TREE][MISSING_FIELD_ATTRS]', { + viewId: this.view.view_id, + model: this.view.screen && this.view.screen.model_name, + field: name, + widget: attributes.widget, + attributes: attributes, + fallbackFieldAttributes: field_attrs, + }); + } if (!this.view.attributes.sequence && !this.view.children_field && - this.field_attrs[name].sortable !== false){ + field_attrs.sortable !== false){ column.sortable = true; } this.view.columns.push(column); @@ -22041,6 +22105,18 @@ function eval_pyson(value){ 'class': 'value', }); this.view.sum_widgets.set(column, [sum, aggregate]); + if (window.console && this.view instanceof Sao.View.AGGridTree) { + console.info('[AGGRID][SUMMARY_SUM_COLUMN_JSON]', + JSON.stringify({ + viewId: this.view.view_id, + model: this.view.screen && + this.view.screen.model_name, + name: attributes.name || null, + label: attributes.string || attributes.name || '', + widget: attributes.widget || '', + sum: attributes.sum || '', + })); + } } }, _parse_button: function(node, attributes) { @@ -22462,6 +22538,8 @@ function eval_pyson(value){ (Sao.View.AGGridTree._next_debug_id || 0) + 1; this._debug_id = Sao.View.AGGridTree._next_debug_id; this.children_field = children_field; + this.is_embedded_one2many = Boolean( + screen.attributes && screen.attributes.one2many); this.optionals = []; this.sum_widgets = new Map(); this.columns = []; @@ -22524,6 +22602,9 @@ function eval_pyson(value){ this.toolbar = jQuery('
', { 'class': 'ag-grid-tree-toolbar', }).appendTo(this.el); + if (this.is_embedded_one2many) { + this.el.addClass('ag-grid-tree-embedded-one2many'); + } this.column_menu = jQuery('', { 'class': 'ag-grid-tree-columns', }).appendTo(this.toolbar); @@ -22610,6 +22691,14 @@ function eval_pyson(value){ this._toggle_filter_logic_menu(); }) .appendTo(this.toolbar); + this.import_layout_input = jQuery('', { + 'type': 'file', + 'accept': 'application/json,.json', + 'class': 'ag-grid-view-import-input', + }).hide().appendTo(this.el); + this.import_layout_input.on('change', evt => { + this._import_layout_from_input(evt.currentTarget); + }); this.filter_logic_panel = jQuery('', { 'class': 'ag-grid-filter-logic-panel', }).hide().appendTo(jQuery(document.body)); @@ -22685,8 +22774,18 @@ function eval_pyson(value){ }); Sao.View.AGGridTree._super.init.call(this, view_id, screen, xml); - this._persisted_state = this._load_persisted_state(); - if (this._persisted_state && this._persisted_state.valueFilters) { + if (this.is_embedded_one2many) { + this._layout_store = this._normalize_layout_store(null); + this._persisted_state = null; + this._sao_value_filters = {}; + this._restored_value_filter_state = null; + this._sao_value_filter_restore_guard = false; + } else { + this._persisted_state = this._load_persisted_state(); + } + if (!this.is_embedded_one2many && + this._persisted_state && + this._persisted_state.valueFilters) { this._apply_sao_value_filter_state( this._persisted_state.valueFilters); } @@ -22967,43 +23066,6 @@ function eval_pyson(value){ }); }, _trace_aggrid_column_resize: function(reason, params, extra) { - if (!window.console) { - return; - } - var column_api = this.gridColumnApi || this.gridApi; - var column_state = column_api && column_api.getColumnState ? - column_api.getColumnState() : []; - var target_col_id = null; - if (params && params.column) { - if (params.column.getColId) { - target_col_id = params.column.getColId(); - } else if (params.column.colId) { - target_col_id = params.column.colId; - } - } - console.info('[AGGRID][COLUMN_RESIZE_JSON]', - JSON.stringify(jQuery.extend({ - reason: reason, - instanceId: this._debug_id, - viewId: this.view_id, - model: this.screen && this.screen.model_name, - targetColId: target_col_id, - finished: params && Object.prototype.hasOwnProperty.call( - params, 'finished') ? params.finished : null, - source: params && params.source || null, - resizeActive: Boolean(this._column_resize_active), - activeLayoutId: this._active_layout_id || null, - columnWidths: (column_state || []) - .filter(state => state && state.colId != '__selection__') - .map(state => ({ - colId: state.colId, - width: state.width, - minWidth: state.minWidth || null, - flex: state.flex || null, - })), - pendingTimers: this._layout_refresh_timers ? - this._layout_refresh_timers.length : 0, - }, extra || {}))); }, _refresh_grid_layout_now: function() { if (!this.gridApi || !this.grid_host || !this.grid_host.length) { @@ -23080,6 +23142,9 @@ function eval_pyson(value){ window.addEventListener('resize', this._window_resize_handler); }, _persist_grid_state: function(options) { + if (this.is_embedded_one2many) { + return; + } options = options || {}; var state = this._capture_grid_state(); state.valueFilters = this._merge_restored_value_filter_state( @@ -24050,6 +24115,110 @@ function eval_pyson(value){ this._layout_store.defaultPresetId = null; this._apply_layout_state(this._get_standard_layout_state(), null); }, + _sanitize_layout_file_name: function(name) { + name = String(name || Sao.i18n.gettext('AG Grid view')).trim(); + name = name.replace(/[\\/:*?"<>|]+/g, '-') + .replace(/\s+/g, ' ') + .trim(); + return name || 'ag-grid-view'; + }, + _get_layout_export_payload: function() { + var preset = this._find_layout_preset(this._active_layout_id); + var name = preset && !this._layout_dirty ? preset.name : + this._get_active_layout_name().replace(/\s+\*$/, ''); + return { + type: 'sao-ag-grid-view', + version: 1, + model: this.screen && this.screen.model_name || null, + viewId: this.view_id || null, + name: name || Sao.i18n.gettext('AG Grid view'), + exportedAt: new Date().toISOString(), + state: this._capture_grid_state(), + }; + }, + _export_current_layout: function() { + var payload = this._get_layout_export_payload(); + var file_name = this._sanitize_layout_file_name( + payload.model + '-' + payload.name) + '.json'; + Sao.common.download_file( + JSON.stringify(payload, null, 2), + file_name, + {type: 'application/json;charset=utf-8'}); + }, + _decode_imported_layout_text: function(data) { + if (typeof data == 'string') { + return data; + } + if (data instanceof Uint8Array) { + return new TextDecoder('utf-8').decode(data); + } + return String(data || ''); + }, + _import_layout_from_input: function(input) { + var file = input && input.files && input.files[0]; + if (!file) { + return; + } + Sao.common.get_file_data(file, data => { + var payload; + try { + payload = JSON.parse(this._decode_imported_layout_text(data)); + } catch (error) { + Sao.common.warning.run( + Sao.i18n.gettext('The selected file is not a valid AG Grid view.'), + Sao.i18n.gettext('Import view')); + return; + } + this._import_layout_payload(payload); + }); + }, + _import_layout_payload: function(payload) { + if (!payload || payload.type != 'sao-ag-grid-view' || + !payload.state) { + Sao.common.warning.run( + Sao.i18n.gettext('The selected file is not a valid AG Grid view.'), + Sao.i18n.gettext('Import view')); + return; + } + if (payload.model && payload.model != this.screen.model_name) { + Sao.common.warning.run( + Sao.i18n.gettext('This view is for another model.') + + ' ' + payload.model + ' / ' + this.screen.model_name, + Sao.i18n.gettext('Import view')); + return; + } + var name = String(payload.name || + Sao.i18n.gettext('Imported view')).trim(); + if (!name) { + name = Sao.i18n.gettext('Imported view'); + } + this._layout_store = this._normalize_layout_store( + this._layout_store || {}); + var existing_names = new Set((this._layout_store.presets || []) + .map(preset => preset.name)); + var base_name = name; + var suffix = 2; + while (existing_names.has(name)) { + name = base_name + ' ' + suffix; + suffix += 1; + } + var now = new Date().toISOString(); + var preset = { + id: this._new_layout_id(), + name: name, + importedAt: now, + updatedAt: now, + state: jQuery.extend(true, {}, payload.state), + }; + this._layout_store.presets.push(preset); + this._active_layout_id = preset.id; + this._layout_dirty = false; + this._layout_store.activePresetId = preset.id; + this._layout_store.currentState = jQuery.extend(true, {}, preset.state); + this._persisted_state = jQuery.extend(true, {}, preset.state); + this._save_layout_store(); + this._apply_layout_state(preset.state, preset.id); + }, _save_layout_as: function() { var name = window.prompt(Sao.i18n.gettext('View name')); if (!name) { @@ -24271,6 +24440,30 @@ function eval_pyson(value){ evt.preventDefault(); this._apply_standard_layout_state(); }).appendTo(actions); + var transfer_actions = jQuery('', { + 'class': 'ag-grid-tree-layouts-transfer-actions', + }).appendTo(actions); + jQuery('', { + 'class': 'btn btn-default btn-sm ag-grid-view-transfer-button', + 'type': 'button', + 'title': Sao.i18n.gettext('Export view'), + 'aria-label': Sao.i18n.gettext('Export view'), + 'text': Sao.i18n.gettext('Export'), + }).click(evt => { + evt.preventDefault(); + this._export_current_layout(); + }).appendTo(transfer_actions); + jQuery('', { + 'class': 'btn btn-default btn-sm ag-grid-view-transfer-button', + 'type': 'button', + 'title': Sao.i18n.gettext('Import view'), + 'aria-label': Sao.i18n.gettext('Import view'), + 'text': Sao.i18n.gettext('Import'), + }).click(evt => { + evt.preventDefault(); + this.import_layout_input.val(null); + this.import_layout_input[0].click(); + }).appendTo(transfer_actions); }, _render_grouping_panel: function() { if (!this.grouping_panel) { @@ -24635,6 +24828,12 @@ function eval_pyson(value){ this._sync_selection_header_checkbox(); this._update_summary_bar(); }, + onFirstDataRendered: () => { + window.requestAnimationFrame(() => this._update_summary_bar()); + }, + onRowDataUpdated: () => { + window.requestAnimationFrame(() => this._update_summary_bar()); + }, onColumnMoved: this._on_grid_column_layout_changed.bind(this), onColumnPinned: this._on_grid_column_layout_changed.bind(this), onColumnResized: this._on_grid_column_resized.bind(this), @@ -24844,16 +25043,6 @@ function eval_pyson(value){ }, }; - console.info('[AGGRID][ALIGN_COLUMN]', { - colId: name, - label: column.attributes.string || '', - widget: column.attributes.widget || '', - className: column.class_ || '', - rightAligned: right_aligned, - cellClass: col_def.cellClass, - headerClass: col_def.headerClass, - }); - column_defs.push(col_def); }); return column_defs; @@ -26077,7 +26266,7 @@ function eval_pyson(value){ var pair = item.split(':'); return pair.length < 2 ? '' : this._normalize_badge_text_key(pair.shift()); - }).filter(Boolean); + }).filter(value => value && value != '*'); }, _get_record_field_value: function(record, name) { if (!record || !name) { @@ -26169,6 +26358,8 @@ function eval_pyson(value){ } else if (Object.prototype.hasOwnProperty.call( mapped, String(normalized_text).toLowerCase())) { color = mapped[String(normalized_text).toLowerCase()]; + } else if (Object.prototype.hasOwnProperty.call(mapped, '*')) { + color = mapped['*']; } if (!color && attributes.badge_color_field) { color = this._get_record_field_value( @@ -26714,7 +26905,7 @@ function eval_pyson(value){ } return rows; }, - _get_summary_badge_columns: function() { + _get_displayed_column_map: function() { var visible_columns = {}; var column_api = this.gridColumnApi || this.gridApi; if (column_api && column_api.getAllDisplayedColumns) { @@ -26724,37 +26915,336 @@ function eval_pyson(value){ } }); } + return visible_columns; + }, + _get_viewport_column_map: function() { + var visible_columns = {}; + var column_api = this.gridColumnApi || this.gridApi; + var columns = null; + if (column_api && column_api.getAllDisplayedVirtualColumns) { + columns = column_api.getAllDisplayedVirtualColumns(); + } else if (column_api && column_api.getAllDisplayedColumns) { + columns = column_api.getAllDisplayedColumns(); + } + (columns || []).forEach(column => { + if (column && column.getColId) { + visible_columns[column.getColId()] = true; + } + }); + return visible_columns; + }, + _is_summary_column_visible: function(column, visible_columns) { + var name = column && column.attributes && column.attributes.name; + return name && (!Object.keys(visible_columns).length || + visible_columns[name]); + }, + _get_summary_badge_columns: function() { return this.columns.filter(column => { var name = column.attributes && column.attributes.name; - var label = column.attributes && - (column.attributes.string || column.attributes.name) || ''; - var is_status_column = /(^|[_\s-])(status|state|type)([_\s-]|$)/i - .test(String(name || '') + ' ' + String(label || '')); - if (!name || (!this._is_badge_column(column) && - !is_status_column)) { - return false; - } - return !Object.keys(visible_columns).length || - visible_columns[name]; + return name && column.attributes.widget == 'badge'; }); }, - _render_summary_item: function(label, value, color) { + _get_summary_sum_columns: function() { + var columns = []; + this.sum_widgets.forEach((sum_widget, column) => { + if (column && column.attributes && column.attributes.name) { + columns.push(column); + } + }); + return columns; + }, + _get_summary_variation_columns: function() { + return this.columns.filter(column => { + var attributes = column && column.attributes || {}; + return attributes.name && + attributes.widget == 'variation' && + attributes.variation_summary == '1' && + attributes.variation_previous && + attributes.variation_group_by; + }); + }, + _render_summary_item: function( + label, value, color, class_name, variation_text, + variation_class) { var item = jQuery('', { - 'class': 'ag-grid-summary-item', + 'class': 'ag-grid-summary-item' + + (class_name ? ' ' + class_name : ''), }); jQuery('', { 'class': 'ag-grid-summary-label', 'text': label, }).appendTo(item); + if (variation_text) { + jQuery('', { + 'class': 'ag-grid-summary-variation-badge ' + + (variation_class || ''), + 'text': variation_text, + }).appendTo(item); + } + var value_wrap = jQuery('', { + 'class': 'ag-grid-summary-value-wrap', + }).appendTo(item); var value_el = jQuery('', { 'class': 'ag-grid-summary-value', 'text': value, - }).appendTo(item); + }).appendTo(value_wrap); if (color) { value_el.css('color', color); } return item; }, + _trace_summary_bar: function( + rows, badge_columns, sum_columns, variation_columns, summary_items) { + if (!window.console) { + return; + } + var describe = column => ({ + name: column && column.attributes && column.attributes.name || null, + label: column && column.attributes && + (column.attributes.string || column.attributes.name) || '', + widget: column && column.attributes && column.attributes.widget || '', + sum: column && column.attributes && column.attributes.sum || '', + badge: Boolean(column && column.attributes && + column.attributes.widget == 'badge'), + }); + console.info('[AGGRID][SUMMARY_BAR_JSON]', JSON.stringify({ + instanceId: this._debug_id, + viewId: this.view_id, + model: this.screen && this.screen.model_name, + rowCount: rows.length, + viewportColumns: Object.keys(this._get_viewport_column_map()), + displayedColumns: Object.keys(this._get_displayed_column_map()), + badgeColumns: badge_columns.map(describe), + sumColumns: sum_columns.map(describe), + variationColumns: variation_columns.map(describe), + sumWidgetColumns: Array.from(this.sum_widgets.keys()).map(describe), + badgeLikeColumns: this.columns.filter(column => + this._is_badge_column(column)).map(describe), + summaryItems: summary_items.map(item => ({ + label: item.label, + count: item.count, + className: item.className || '', + })), + })); + }, + _get_summary_column_load_fields: function(column) { + var attributes = column && column.attributes || {}; + var fields = []; + if (attributes.name) { + fields.push(attributes.name); + } + if (attributes.widget == 'variation') { + if (attributes.variation_previous) { + fields.push(attributes.variation_previous); + } + if (attributes.variation_group_by) { + fields.push(attributes.variation_group_by); + } + } + if (attributes.sum_variation == '1' && attributes.sum_previous) { + fields.push(attributes.sum_previous); + } + return fields.filter((field, index) => + field && fields.indexOf(field) == index); + }, + _load_summary_column_values: function(rows, columns, reason) { + if (this._summary_sum_loading) { + return null; + } + var promises = []; + var field_names = []; + columns.forEach(column => { + var names = this._get_summary_column_load_fields(column); + field_names = field_names.concat(names); + rows.forEach(row => { + var record = row && row.__record; + names.forEach(name => { + if (record && record.id >= 0 && + !record.get_loaded([name])) { + promises.push(record.load(name, true, false)); + } + }); + }); + }); + if (!promises.length) { + return null; + } + this._summary_sum_loading = true; + if (window.console) { + console.info('[AGGRID][SUMMARY_SUM_LOAD_JSON]', + JSON.stringify({ + instanceId: this._debug_id, + viewId: this.view_id, + model: this.screen && this.screen.model_name, + reason: reason || 'summary', + fields: field_names.filter((field, index) => + field && field_names.indexOf(field) == index), + promiseCount: promises.length, + })); + } + return jQuery.when.apply(jQuery, promises).always(() => { + this._summary_sum_loading = false; + }); + }, + _load_summary_sum_values: function(rows, columns) { + return this._load_summary_column_values(rows, columns, 'sum'); + }, + _load_summary_badge_values: function(rows, columns) { + return this._load_summary_column_values(rows, columns, 'badge'); + }, + _append_sum_summary_item: function(summary_items, rows, column) { + var sum = null; + var name = column && column.attributes && column.attributes.name; + var field = name ? this.screen.model.fields[name] : null; + var previous_sum = null; + var previous_name = column && column.attributes && + column.attributes.sum_previous; + var with_variation = column && column.attributes && + column.attributes.sum_variation == '1' && previous_name; + rows.forEach(row => { + var record = row && row.__record; + if (!field || !record || record.id >= 0 && + !record.get_loaded([name])) { + return; + } + var value = field.get(record); + if (value && value.isTimeDelta) { + value = value.asSeconds(); + } + if (value !== null && value !== undefined && value !== '') { + sum = sum === null ? value : sum + value; + } + if (with_variation) { + var previous = this._get_record_raw_field_value( + record, previous_name); + if (previous !== null && previous !== undefined && + previous !== '') { + previous = Number(previous); + if (isFinite(previous)) { + previous_sum = previous_sum === null ? + previous : previous_sum + previous; + } + } + } + }); + var variation = null; + if (with_variation && sum !== null && previous_sum !== null && + previous_sum !== 0) { + variation = ((sum - previous_sum) / + Math.abs(previous_sum)) * 100; + } + summary_items.push({ + label: column.attributes.string || column.attributes.name, + count: sum === null ? '-' : + this._format_aggregate_value(column, sum), + color: '#111111', + className: 'ag-grid-summary-item-sum', + variationText: variation === null ? '' : + this._format_sum_variation_percent(variation), + variationClass: variation === null ? '' : + (variation < 0 ? 'ag-grid-summary-variation-negative' : + 'ag-grid-summary-variation-positive'), + }); + }, + _get_record_raw_field_value: function(record, name) { + if (!record || !name) { + return null; + } + var field = this.screen.model.fields[name]; + if (!field || record.id >= 0 && !record.get_loaded([name])) { + return null; + } + var value = field.get(record); + if (value && value.isTimeDelta) { + value = value.asSeconds(); + } + if (value === null || value === undefined || value === '') { + return null; + } + return value; + }, + _get_record_text_field_value: function(record, name) { + if (!record || !name) { + return ''; + } + var field = this.screen.model.fields[name]; + if (!field || record.id >= 0 && !record.get_loaded([name])) { + return ''; + } + try { + return field.get_client(record); + } catch (error) { + var value = this._get_record_raw_field_value(record, name); + return value === null || value === undefined ? '' : String(value); + } + }, + _format_variation_percent: function(value) { + var locale = Sao.i18n.BC47(Sao.i18n.getlang()); + var sign = value > 0 ? '+' : ''; + return sign + value.toLocaleString(locale, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }) + '%'; + }, + _format_sum_variation_percent: function(value) { + var locale = Sao.i18n.BC47(Sao.i18n.getlang()); + var sign = value >= 0 ? '+' : ''; + return sign + value.toLocaleString(locale, { + minimumFractionDigits: 0, + maximumFractionDigits: 2, + }) + '%'; + }, + _append_variation_summary_items: function(summary_items, rows, column) { + var attributes = column && column.attributes || {}; + var current_name = attributes.name; + var previous_name = attributes.variation_previous; + var group_name = attributes.variation_group_by; + var groups = {}; + rows.forEach(row => { + var record = row && row.__record; + if (!record) { + return; + } + var current = Number(this._get_record_raw_field_value( + record, current_name)); + var previous = Number(this._get_record_raw_field_value( + record, previous_name)); + var group = this._get_record_text_field_value( + record, group_name) || Sao.i18n.gettext('(Empty)'); + if (!isFinite(current) || !isFinite(previous) || previous === 0) { + return; + } + if (!groups[group]) { + groups[group] = { + current: 0, + previous: 0, + }; + } + groups[group].current += current; + groups[group].previous += previous; + }); + Object.keys(groups).sort((a, b) => String(a).localeCompare( + String(b), undefined, {numeric: true, sensitivity: 'base'})) + .forEach(group => { + var values = groups[group]; + if (!values.previous) { + return; + } + var variation = ((values.current - values.previous) / + Math.abs(values.previous)) * 100; + summary_items.push({ + label: group, + count: this._format_variation_percent(variation), + color: variation > 0 ? '#16a34a' : + variation < 0 ? '#dc2626' : '#64748b', + className: 'ag-grid-summary-item-variation' + + (variation > 0 ? ' ag-grid-summary-variation-up' : + variation < 0 ? + ' ag-grid-summary-variation-down' : + ' ag-grid-summary-variation-flat'), + }); + }); + }, _get_default_badge_summary_color: function(value) { var key = String(value || '').toLowerCase(); var palette = { @@ -26785,8 +27275,16 @@ function eval_pyson(value){ var order = []; var empty = Sao.i18n.gettext('(Empty)'); rows.forEach(row => { - var value = row[name] ? - this._normalize_badge_text_key(row[name]) : empty; + var raw_value = ''; + if (column && row && row.__record) { + raw_value = this._get_column_text_value( + column, row.__record); + } + if (!raw_value && row) { + raw_value = row[name]; + } + var value = raw_value ? + this._normalize_badge_text_key(raw_value) : empty; if (value == empty) { return; } @@ -26836,27 +27334,6 @@ function eval_pyson(value){ }); return Boolean(order.length); }, - _get_summary_status_sources_from_grid: function() { - var sources = []; - var seen = {}; - (this._grid_columns || []).forEach(col_def => { - var name = col_def && col_def.field || col_def && col_def.colId; - if (!name || name == '__selection__' || seen[name]) { - return; - } - var label = col_def.headerName || name; - if (!/(status|state|type|shipping|shipment|open\/physic)/i - .test(String(name) + ' ' + String(label))) { - return; - } - seen[name] = true; - sources.push({ - name: name, - label: label, - }); - }); - return sources; - }, _update_summary_bar: function() { if (!this.summary_bar || !this.summary_bar.length) { return; @@ -26869,25 +27346,38 @@ function eval_pyson(value){ rows.length.toLocaleString(locale), '#145369')); var summary_items = []; - var summarized_names = {}; - this._get_summary_badge_columns().forEach(column => { - if (this._append_badge_summary_items( - summary_items, rows, column)) { - summarized_names[column.attributes.name] = true; - } - }); - this._get_summary_status_sources_from_grid().forEach(source => { - if (summarized_names[source.name]) { - return; - } + var sum_columns = this._get_summary_sum_columns(); + var badge_columns = this._get_summary_badge_columns(); + var variation_columns = this._get_summary_variation_columns(); + var summary_load = this._load_summary_column_values( + rows, badge_columns.concat(sum_columns, variation_columns), + 'summary'); + if (summary_load) { + summary_load.then(() => this._update_summary_bar()); + } + badge_columns.forEach(column => { this._append_badge_summary_items( - summary_items, rows, null, source); + summary_items, rows, column); }); + sum_columns.forEach(column => { + this._append_sum_summary_item(summary_items, rows, column); + }); + variation_columns.forEach(column => { + this._append_variation_summary_items( + summary_items, rows, column); + }); + this._trace_summary_bar( + rows, badge_columns, sum_columns, variation_columns, + summary_items); summary_items.forEach(item => { this.summary_bar.append(this._render_summary_item( item.label, - item.count.toLocaleString(locale), - item.color)); + typeof item.count == 'number' ? + item.count.toLocaleString(locale) : item.count, + item.color, + item.className, + item.variationText, + item.variationClass)); }); }, on_copy: function() { @@ -27052,7 +27542,10 @@ function eval_pyson(value){ var readable_min_height = 420; var maximum_height = 720; var height; - if (available >= compact_min_height) { + if (this.is_embedded_one2many && this._embedded_height) { + height = Math.max(80, this._embedded_height - toolbar_height - + summary_height - external_scroll_height); + } else if (available >= compact_min_height) { height = Math.max( compact_min_height, Math.min(maximum_height, available)); @@ -27104,6 +27597,17 @@ function eval_pyson(value){ }); } }, + set_embedded_height: function(height) { + height = parseInt(height, 10); + if (!this.is_embedded_one2many || isNaN(height) || height <= 0) { + return; + } + this._embedded_height = height; + this._resize_grid_host(); + if (this.gridApi && this.gridApi.doLayout) { + this.gridApi.doLayout(); + } + }, _refresh_grid_layout_after_row_data: function(row_data) { if (!this.gridApi) { return; @@ -27319,7 +27823,7 @@ function eval_pyson(value){ } else if (diagnostics.relationWidget) { diagnostics.fallbackReason = diagnostics.relationWidget + '_relation_widget'; - } else if (diagnostics.embeddedContext) { + } else if (diagnostics.embeddedContext && diagnostics.editable) { diagnostics.fallbackReason = 'embedded_relation_tree'; } else if (diagnostics.editable) { diagnostics.fallbackReason = 'editable_view'; @@ -30481,6 +30985,7 @@ function eval_pyson(value){ 'timedelta': Sao.View.Tree.TimeDeltaColumn, 'tolerance_gauge': Sao.View.Tree.ToleranceGauge, 'url': Sao.View.Tree.URLColumn, + 'variation': Sao.View.Tree.CharColumn, }; Sao.View.EditableTree = {}; diff --git a/tryton-sao.css b/tryton-sao.css index 9dcdc09..fb1ee3c 100644 --- a/tryton-sao.css +++ b/tryton-sao.css @@ -9691,20 +9691,20 @@ img.icon { margin: 0 0 5px; } .ag-grid-tree-toolbar > .ag-grid-screen-filter-toggle-bar { - margin: 0 10px 0 auto; + margin: 0 4px 0 auto; } .ag-grid-screen-filter-toggle { display: inline-flex; align-items: center; - gap: 7px; - min-height: 30px; - padding: 4px 10px; - border: 1px solid #d6e4df; - border-radius: 8px; - background: linear-gradient(180deg, #f6fbf9 0%, #edf5f1 100%); + gap: 5px; + min-height: 26px; + padding: 2px 7px; + border: 1px solid #d8e2df; + border-radius: 6px; + background: #ffffff; color: #355154; - box-shadow: 0 2px 7px rgba(53, 81, 84, 0.1); - font-size: 12px; + box-shadow: none; + font-size: 11.5px; font-weight: 600; line-height: 1.2; text-transform: none !important; @@ -9713,18 +9713,18 @@ img.icon { } .ag-grid-screen-filter-toggle:hover, .ag-grid-screen-filter-toggle:focus { - background: linear-gradient(180deg, #f2faf7 0%, #e7f2ed 100%); + background: #f6faf8; border-color: #bfd5cd; color: #28484b; - box-shadow: 0 3px 9px rgba(53, 81, 84, 0.13); + box-shadow: 0 1px 1px rgba(53, 81, 84, 0.05); } .ag-grid-screen-filter-toggle:active { transform: translateY(1px); - box-shadow: 0 1px 5px rgba(53, 81, 84, 0.1); + box-shadow: none; } .ag-grid-screen-filter-toggle .icon { - width: 14px; - height: 14px; + width: 13px; + height: 13px; opacity: 0.78; } @media screen and (max-width: 991px) { @@ -11122,24 +11122,26 @@ html[theme="default"] .screen-container .nav-tabs > li.active > a:hover { display: flex; align-items: center; justify-content: flex-start; - gap: 12px; - padding: 6px 0; + gap: 5px; + min-height: 32px; + padding: 2px 0 4px; } .ag-grid-tree-columns > .btn, .ag-grid-tree-grouping > .btn, .ag-grid-tree-layouts > .btn, -.ag-grid-value-filter-mode { +.ag-grid-value-filter-mode, +.ag-grid-view-transfer-button { display: inline-flex; align-items: center; - gap: 8px; - min-height: 34px; - padding: 6px 12px; - border: 1px solid #d6e4df; - border-radius: 8px; - background: linear-gradient(180deg, #f6fbf9 0%, #edf5f1 100%); + gap: 5px; + min-height: 27px; + padding: 3px 8px; + border: 1px solid #d8e2df; + border-radius: 6px; + background: #ffffff; color: #355154; - box-shadow: 0 3px 10px rgba(53, 81, 84, 0.12); + box-shadow: none; text-transform: none !important; transition: background-color 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease; @@ -11149,20 +11151,23 @@ html[theme="default"] .screen-container .nav-tabs > li.active > a:hover { .ag-grid-tree-grouping > .btn:hover, .ag-grid-tree-layouts > .btn:hover, .ag-grid-value-filter-mode:hover, +.ag-grid-view-transfer-button:hover, .ag-grid-tree-columns > .btn:focus, .ag-grid-tree-grouping > .btn:focus, .ag-grid-tree-layouts > .btn:focus, -.ag-grid-value-filter-mode:focus { +.ag-grid-value-filter-mode:focus, +.ag-grid-view-transfer-button:focus { background: linear-gradient(180deg, #f2faf7 0%, #e7f2ed 100%); border-color: #bfd5cd; color: #28484b; - box-shadow: 0 5px 14px rgba(53, 81, 84, 0.16); + box-shadow: 0 1px 1px rgba(53, 81, 84, 0.05); } .ag-grid-tree-columns > .btn:active, .ag-grid-tree-grouping > .btn:active, .ag-grid-tree-layouts > .btn:active, -.ag-grid-value-filter-mode:active { +.ag-grid-value-filter-mode:active, +.ag-grid-view-transfer-button:active { transform: translateY(1px); box-shadow: 0 2px 8px rgba(53, 81, 84, 0.12); } @@ -11171,18 +11176,30 @@ html[theme="default"] .screen-container .nav-tabs > li.active > a:hover { .ag-grid-tree-grouping-label, .ag-grid-tree-layouts-label, .ag-grid-value-filter-mode-label { - font-size: 12px; + font-size: 11.5px; font-weight: 600; - letter-spacing: 0.01em; + letter-spacing: 0; text-transform: none; } .ag-grid-value-filter-mode.active { - background: linear-gradient(180deg, #e0f4f1 0%, #cbe9e3 100%); + background: #f2faf8; border-color: #8fc3bd; color: #0f6f6d; } +.ag-grid-view-transfer-button { + font-size: 11.5px; + font-weight: 600; +} + +.ag-grid-tree-layouts-transfer-actions { + display: inline-flex; + align-items: center; + gap: 6px; + margin-left: auto; +} + .ag-grid-tree-grouping-panel .btn, .ag-grid-tree-layouts-panel .btn, .ag-grid-filter-logic-panel .btn { @@ -11289,7 +11306,7 @@ html[theme="default"] .screen-container .nav-tabs > li.active > a:hover { justify-content: center; width: 16px; height: 16px; - color: #2b8c87; + color: #6f8f8b; vertical-align: middle; } @@ -11308,6 +11325,14 @@ html[theme="default"] .screen-container .nav-tabs > li.active > a:hover { fill: none; } +.ag-grid-tree-columns > .btn:hover .ag-grid-toolbar-icon, +.ag-grid-tree-grouping > .btn:hover .ag-grid-toolbar-icon, +.ag-grid-tree-layouts > .btn:hover .ag-grid-toolbar-icon, +.ag-grid-value-filter-mode:hover .ag-grid-toolbar-icon, +.ag-grid-value-filter-mode.active .ag-grid-toolbar-icon { + color: #2b8c87; +} + .ag-grid-tree-grouping-panel { width: 420px; max-width: calc(100vw - 24px); @@ -11637,6 +11662,7 @@ html[theme="default"] .ag-grid-tree-grouping-panel input[type="checkbox"]:after gap: 3px; padding: 8px 18px; border-right: 1px solid #e7efeb; + position: relative; } .ag-grid-summary-item:last-child { @@ -11661,6 +11687,78 @@ html[theme="default"] .ag-grid-tree-grouping-panel input[type="checkbox"]:after line-height: 1.15; } +.ag-grid-summary-value-wrap { + display: inline-flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.ag-grid-summary-variation-badge { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + top: 7px; + right: 10px; + min-height: 16px; + padding: 0 5px; + border-radius: 4px; + font-size: 10.5px; + font-weight: 800; + line-height: 1.1; + white-space: nowrap; +} + +.ag-grid-summary-variation-positive { + border: none; + background: transparent; + color: #16a34a; +} + +.ag-grid-summary-variation-negative { + border: none; + background: transparent; + color: #dc2626; +} + +.ag-grid-summary-item-sum .ag-grid-summary-value { + color: #111111; + font-weight: 800; +} + +.ag-grid-summary-item-sum { + flex-basis: 180px; + min-width: 165px; + max-width: 230px; + padding-left: 22px; + padding-right: 28px; +} + +.ag-grid-summary-item-variation { + flex-basis: 165px; + min-width: 145px; + max-width: 220px; + background: #fbfdfc; +} + +.ag-grid-summary-item-variation .ag-grid-summary-value { + font-size: 17px; + font-weight: 800; +} + +.ag-grid-summary-variation-up .ag-grid-summary-value { + color: #16a34a; +} + +.ag-grid-summary-variation-down .ag-grid-summary-value { + color: #dc2626; +} + +.ag-grid-summary-variation-flat .ag-grid-summary-value { + color: #64748b; +} + .ag-grid-tree-host { flex: 0 0 auto; min-height: 520px; @@ -11790,6 +11888,12 @@ html[theme="default"] .ag-grid-tree-grouping-panel input[type="checkbox"]:after border-right: 1px solid #e3ebe7; } +.ag-grid-tree-host .ag-header-cell:not([col-id="__selection__"]), +.ag-grid-tree-host .ag-cell:not([col-id="__selection__"]) { + padding-left: 10px; + padding-right: 14px; +} + .ag-grid-tree-host .ag-header-cell:last-child, .ag-grid-tree-host .ag-cell:last-child { border-right: none; @@ -11819,6 +11923,10 @@ html[theme="default"] .ag-grid-tree-grouping-panel input[type="checkbox"]:after justify-content: flex-end; } +.ag-grid-tree-host .ag-grid-right-aligned-cell { + padding-right: 18px; +} + .ag-grid-tree-host .ag-grid-badge-cell { display: flex; align-items: center; @@ -12772,6 +12880,7 @@ html[data-theme-mode="dark"] .screen-container .filter-box .btn, html[data-theme-mode="dark"] .ag-grid-tree-columns > .btn, html[data-theme-mode="dark"] .ag-grid-tree-grouping > .btn, html[data-theme-mode="dark"] .ag-grid-tree-layouts > .btn, +html[data-theme-mode="dark"] .ag-grid-view-transfer-button, html[data-theme-mode="dark"] .ag-grid-tree-grouping-panel .btn, html[data-theme-mode="dark"] .ag-grid-tree-layouts-panel .btn, html[data-theme-mode="dark"] .ag-grid-filter-logic-panel .btn { @@ -12787,6 +12896,7 @@ html[data-theme-mode="dark"] .screen-container .filter-box .btn:hover, html[data-theme-mode="dark"] .ag-grid-tree-columns > .btn:hover, html[data-theme-mode="dark"] .ag-grid-tree-grouping > .btn:hover, html[data-theme-mode="dark"] .ag-grid-tree-layouts > .btn:hover, +html[data-theme-mode="dark"] .ag-grid-view-transfer-button:hover, html[data-theme-mode="dark"] .ag-grid-tree-grouping-panel .btn:hover, html[data-theme-mode="dark"] .ag-grid-tree-layouts-panel .btn:hover, html[data-theme-mode="dark"] .ag-grid-filter-logic-panel .btn:hover { @@ -12835,6 +12945,26 @@ html[data-theme-mode="dark"] .ag-grid-summary-value { color: #8fe1d8; } +html[data-theme-mode="dark"] .ag-grid-summary-item-sum .ag-grid-summary-value { + color: #111111; +} + +html[data-theme-mode="dark"] .ag-grid-summary-item-variation { + background: #172321; +} + +html[data-theme-mode="dark"] .ag-grid-summary-variation-positive { + border-color: transparent; + background: transparent; + color: #86efac; +} + +html[data-theme-mode="dark"] .ag-grid-summary-variation-negative { + border-color: transparent; + background: transparent; + color: #fca5a5; +} + html[data-theme-mode="dark"] .form-tolerance-gauge { color: #dce7e2; } diff --git a/tryton-sao.js b/tryton-sao.js index 4c0b763..02bb1c1 100644 --- a/tryton-sao.js +++ b/tryton-sao.js @@ -3610,7 +3610,14 @@ var Sao = { }; Sao.common.parse_datetime = function(datetime_format, value) { - var date = moment(value, Sao.common.moment_format(datetime_format)); + var formats = [ + Sao.common.moment_format(datetime_format), + 'YYYY-MM-DDTHH:mm', + 'YYYY-MM-DDTHH:mm:ss', + 'YYYY-MM-DD HH:mm', + 'YYYY-MM-DD HH:mm:ss', + ]; + var date = moment(value, formats, true); if (date.isValid()) { date = Sao.DateTime(date.year(), date.month(), date.date(), date.hour(), date.minute(), date.second(), @@ -16797,6 +16804,21 @@ function eval_pyson(value){ this.input.appendTo(this.icon); Sao.common.ICONFACTORY.get_icon_img('tryton-date') .appendTo(this.icon); + this.icon.on('click', (event_) => { + if (this.date.prop('readonly')) { + return; + } + if (event_.target == this.input[0]) { + return; + } + event_.preventDefault(); + event_.stopPropagation(); + if (this.input[0].showPicker) { + this.input[0].showPicker(); + } else { + this.input.trigger('click'); + } + }); } this.date.change(this.focus_out.bind(this)); var mousetrap = new Mousetrap(this.date[0]); @@ -16885,7 +16907,7 @@ function eval_pyson(value){ Sao.View.Form.DateTime = Sao.class_(Sao.View.Form.Date, { class_: 'form-datetime', _input: 'datetime-local', - _input_format: '%Y-%m-%dT%H:%M:%S', + _input_format: '%Y-%m-%dT%H:%M', _format: Sao.common.format_datetime, _parse: Sao.common.parse_datetime, get_format: function() { @@ -16910,9 +16932,10 @@ function eval_pyson(value){ _parse: Sao.common.parse_time, init: function(view, attributes) { Sao.View.Form.Time._super.init.call(this, view, attributes); - if (~navigator.userAgent.indexOf("Firefox")) { - // time input on Firefox does not have a pop-up - this.input.parent().hide(); + if (this.icon) { + this.input.remove(); + this.icon.remove(); + delete this.icon; } }, get_format: function() { @@ -18351,6 +18374,7 @@ function eval_pyson(value){ view_ids: (attributes.view_ids || '').split(','), views_preload: attributes.views || {}, order: attributes.order, + one2many: true, row_activate: this.activate.bind(this), exclude_field: attributes.relation_field || null, limit: null, @@ -18577,12 +18601,22 @@ function eval_pyson(value){ this.screen.domain = domain; } this.screen.size_limit = size_limit; + if (this.attributes.height !== undefined && + this.screen.current_view && + this.screen.current_view.set_embedded_height) { + this.screen.current_view.set_embedded_height( + this.attributes.height); + } this.screen.display(); if (this.attributes.height !== undefined) { this.screen.current_view.el .find('.treeview,.list-form').first() .css('min-height', this.attributes.height + 'px') .css('max-height', this.attributes.height + 'px'); + if (this.screen.current_view.set_embedded_height) { + this.screen.current_view.set_embedded_height( + this.attributes.height); + } } }); }, @@ -21981,8 +22015,26 @@ function eval_pyson(value){ }, _parse_field: function(node, attributes) { var name = attributes.name; - var ColumnFactory = Sao.View.TreeXMLViewParser.WIDGETS[ - attributes.widget]; + var field = this.view.screen.model.fields[name]; + var widget = attributes.widget || (field && field.description.type); + var column_widget = widget == 'variation' ? + (field && field.description.type || 'char') : widget; + attributes.widget = widget; + var ColumnFactory = Sao.View.TreeXMLViewParser.WIDGETS[column_widget]; + if (!ColumnFactory) { + console.error('[TREE][UNKNOWN_WIDGET]', { + viewId: this.view.view_id, + model: this.view.screen && this.view.screen.model_name, + field: name, + widget: column_widget, + attributes: attributes, + node: node.outerHTML, + availableWidgets: Object.keys( + Sao.View.TreeXMLViewParser.WIDGETS).sort(), + }); + attributes.widget = 'char'; + ColumnFactory = Sao.View.Tree.CharColumn; + } var column = new ColumnFactory(this.view.screen.model, attributes); if (!this.view.widgets[name]) { this.view.widgets[name] = []; @@ -22022,9 +22074,21 @@ function eval_pyson(value){ column.prefixes.push( new Sao.View.Tree.Symbol(attributes, 0)); } + var field_attrs = this.field_attrs[name] || + (field && field.description) || {}; + if (!this.field_attrs[name]) { + console.warn('[TREE][MISSING_FIELD_ATTRS]', { + viewId: this.view.view_id, + model: this.view.screen && this.view.screen.model_name, + field: name, + widget: attributes.widget, + attributes: attributes, + fallbackFieldAttributes: field_attrs, + }); + } if (!this.view.attributes.sequence && !this.view.children_field && - this.field_attrs[name].sortable !== false){ + field_attrs.sortable !== false){ column.sortable = true; } this.view.columns.push(column); @@ -22041,6 +22105,18 @@ function eval_pyson(value){ 'class': 'value', }); this.view.sum_widgets.set(column, [sum, aggregate]); + if (window.console && this.view instanceof Sao.View.AGGridTree) { + console.info('[AGGRID][SUMMARY_SUM_COLUMN_JSON]', + JSON.stringify({ + viewId: this.view.view_id, + model: this.view.screen && + this.view.screen.model_name, + name: attributes.name || null, + label: attributes.string || attributes.name || '', + widget: attributes.widget || '', + sum: attributes.sum || '', + })); + } } }, _parse_button: function(node, attributes) { @@ -22462,6 +22538,8 @@ function eval_pyson(value){ (Sao.View.AGGridTree._next_debug_id || 0) + 1; this._debug_id = Sao.View.AGGridTree._next_debug_id; this.children_field = children_field; + this.is_embedded_one2many = Boolean( + screen.attributes && screen.attributes.one2many); this.optionals = []; this.sum_widgets = new Map(); this.columns = []; @@ -22524,6 +22602,9 @@ function eval_pyson(value){ this.toolbar = jQuery('', { 'class': 'ag-grid-tree-toolbar', }).appendTo(this.el); + if (this.is_embedded_one2many) { + this.el.addClass('ag-grid-tree-embedded-one2many'); + } this.column_menu = jQuery('', { 'class': 'ag-grid-tree-columns', }).appendTo(this.toolbar); @@ -22610,6 +22691,14 @@ function eval_pyson(value){ this._toggle_filter_logic_menu(); }) .appendTo(this.toolbar); + this.import_layout_input = jQuery('', { + 'type': 'file', + 'accept': 'application/json,.json', + 'class': 'ag-grid-view-import-input', + }).hide().appendTo(this.el); + this.import_layout_input.on('change', evt => { + this._import_layout_from_input(evt.currentTarget); + }); this.filter_logic_panel = jQuery('', { 'class': 'ag-grid-filter-logic-panel', }).hide().appendTo(jQuery(document.body)); @@ -22685,8 +22774,18 @@ function eval_pyson(value){ }); Sao.View.AGGridTree._super.init.call(this, view_id, screen, xml); - this._persisted_state = this._load_persisted_state(); - if (this._persisted_state && this._persisted_state.valueFilters) { + if (this.is_embedded_one2many) { + this._layout_store = this._normalize_layout_store(null); + this._persisted_state = null; + this._sao_value_filters = {}; + this._restored_value_filter_state = null; + this._sao_value_filter_restore_guard = false; + } else { + this._persisted_state = this._load_persisted_state(); + } + if (!this.is_embedded_one2many && + this._persisted_state && + this._persisted_state.valueFilters) { this._apply_sao_value_filter_state( this._persisted_state.valueFilters); } @@ -22967,43 +23066,6 @@ function eval_pyson(value){ }); }, _trace_aggrid_column_resize: function(reason, params, extra) { - if (!window.console) { - return; - } - var column_api = this.gridColumnApi || this.gridApi; - var column_state = column_api && column_api.getColumnState ? - column_api.getColumnState() : []; - var target_col_id = null; - if (params && params.column) { - if (params.column.getColId) { - target_col_id = params.column.getColId(); - } else if (params.column.colId) { - target_col_id = params.column.colId; - } - } - console.info('[AGGRID][COLUMN_RESIZE_JSON]', - JSON.stringify(jQuery.extend({ - reason: reason, - instanceId: this._debug_id, - viewId: this.view_id, - model: this.screen && this.screen.model_name, - targetColId: target_col_id, - finished: params && Object.prototype.hasOwnProperty.call( - params, 'finished') ? params.finished : null, - source: params && params.source || null, - resizeActive: Boolean(this._column_resize_active), - activeLayoutId: this._active_layout_id || null, - columnWidths: (column_state || []) - .filter(state => state && state.colId != '__selection__') - .map(state => ({ - colId: state.colId, - width: state.width, - minWidth: state.minWidth || null, - flex: state.flex || null, - })), - pendingTimers: this._layout_refresh_timers ? - this._layout_refresh_timers.length : 0, - }, extra || {}))); }, _refresh_grid_layout_now: function() { if (!this.gridApi || !this.grid_host || !this.grid_host.length) { @@ -23080,6 +23142,9 @@ function eval_pyson(value){ window.addEventListener('resize', this._window_resize_handler); }, _persist_grid_state: function(options) { + if (this.is_embedded_one2many) { + return; + } options = options || {}; var state = this._capture_grid_state(); state.valueFilters = this._merge_restored_value_filter_state( @@ -24050,6 +24115,110 @@ function eval_pyson(value){ this._layout_store.defaultPresetId = null; this._apply_layout_state(this._get_standard_layout_state(), null); }, + _sanitize_layout_file_name: function(name) { + name = String(name || Sao.i18n.gettext('AG Grid view')).trim(); + name = name.replace(/[\\/:*?"<>|]+/g, '-') + .replace(/\s+/g, ' ') + .trim(); + return name || 'ag-grid-view'; + }, + _get_layout_export_payload: function() { + var preset = this._find_layout_preset(this._active_layout_id); + var name = preset && !this._layout_dirty ? preset.name : + this._get_active_layout_name().replace(/\s+\*$/, ''); + return { + type: 'sao-ag-grid-view', + version: 1, + model: this.screen && this.screen.model_name || null, + viewId: this.view_id || null, + name: name || Sao.i18n.gettext('AG Grid view'), + exportedAt: new Date().toISOString(), + state: this._capture_grid_state(), + }; + }, + _export_current_layout: function() { + var payload = this._get_layout_export_payload(); + var file_name = this._sanitize_layout_file_name( + payload.model + '-' + payload.name) + '.json'; + Sao.common.download_file( + JSON.stringify(payload, null, 2), + file_name, + {type: 'application/json;charset=utf-8'}); + }, + _decode_imported_layout_text: function(data) { + if (typeof data == 'string') { + return data; + } + if (data instanceof Uint8Array) { + return new TextDecoder('utf-8').decode(data); + } + return String(data || ''); + }, + _import_layout_from_input: function(input) { + var file = input && input.files && input.files[0]; + if (!file) { + return; + } + Sao.common.get_file_data(file, data => { + var payload; + try { + payload = JSON.parse(this._decode_imported_layout_text(data)); + } catch (error) { + Sao.common.warning.run( + Sao.i18n.gettext('The selected file is not a valid AG Grid view.'), + Sao.i18n.gettext('Import view')); + return; + } + this._import_layout_payload(payload); + }); + }, + _import_layout_payload: function(payload) { + if (!payload || payload.type != 'sao-ag-grid-view' || + !payload.state) { + Sao.common.warning.run( + Sao.i18n.gettext('The selected file is not a valid AG Grid view.'), + Sao.i18n.gettext('Import view')); + return; + } + if (payload.model && payload.model != this.screen.model_name) { + Sao.common.warning.run( + Sao.i18n.gettext('This view is for another model.') + + ' ' + payload.model + ' / ' + this.screen.model_name, + Sao.i18n.gettext('Import view')); + return; + } + var name = String(payload.name || + Sao.i18n.gettext('Imported view')).trim(); + if (!name) { + name = Sao.i18n.gettext('Imported view'); + } + this._layout_store = this._normalize_layout_store( + this._layout_store || {}); + var existing_names = new Set((this._layout_store.presets || []) + .map(preset => preset.name)); + var base_name = name; + var suffix = 2; + while (existing_names.has(name)) { + name = base_name + ' ' + suffix; + suffix += 1; + } + var now = new Date().toISOString(); + var preset = { + id: this._new_layout_id(), + name: name, + importedAt: now, + updatedAt: now, + state: jQuery.extend(true, {}, payload.state), + }; + this._layout_store.presets.push(preset); + this._active_layout_id = preset.id; + this._layout_dirty = false; + this._layout_store.activePresetId = preset.id; + this._layout_store.currentState = jQuery.extend(true, {}, preset.state); + this._persisted_state = jQuery.extend(true, {}, preset.state); + this._save_layout_store(); + this._apply_layout_state(preset.state, preset.id); + }, _save_layout_as: function() { var name = window.prompt(Sao.i18n.gettext('View name')); if (!name) { @@ -24271,6 +24440,30 @@ function eval_pyson(value){ evt.preventDefault(); this._apply_standard_layout_state(); }).appendTo(actions); + var transfer_actions = jQuery('', { + 'class': 'ag-grid-tree-layouts-transfer-actions', + }).appendTo(actions); + jQuery('', { + 'class': 'btn btn-default btn-sm ag-grid-view-transfer-button', + 'type': 'button', + 'title': Sao.i18n.gettext('Export view'), + 'aria-label': Sao.i18n.gettext('Export view'), + 'text': Sao.i18n.gettext('Export'), + }).click(evt => { + evt.preventDefault(); + this._export_current_layout(); + }).appendTo(transfer_actions); + jQuery('', { + 'class': 'btn btn-default btn-sm ag-grid-view-transfer-button', + 'type': 'button', + 'title': Sao.i18n.gettext('Import view'), + 'aria-label': Sao.i18n.gettext('Import view'), + 'text': Sao.i18n.gettext('Import'), + }).click(evt => { + evt.preventDefault(); + this.import_layout_input.val(null); + this.import_layout_input[0].click(); + }).appendTo(transfer_actions); }, _render_grouping_panel: function() { if (!this.grouping_panel) { @@ -24635,6 +24828,12 @@ function eval_pyson(value){ this._sync_selection_header_checkbox(); this._update_summary_bar(); }, + onFirstDataRendered: () => { + window.requestAnimationFrame(() => this._update_summary_bar()); + }, + onRowDataUpdated: () => { + window.requestAnimationFrame(() => this._update_summary_bar()); + }, onColumnMoved: this._on_grid_column_layout_changed.bind(this), onColumnPinned: this._on_grid_column_layout_changed.bind(this), onColumnResized: this._on_grid_column_resized.bind(this), @@ -24844,16 +25043,6 @@ function eval_pyson(value){ }, }; - console.info('[AGGRID][ALIGN_COLUMN]', { - colId: name, - label: column.attributes.string || '', - widget: column.attributes.widget || '', - className: column.class_ || '', - rightAligned: right_aligned, - cellClass: col_def.cellClass, - headerClass: col_def.headerClass, - }); - column_defs.push(col_def); }); return column_defs; @@ -26077,7 +26266,7 @@ function eval_pyson(value){ var pair = item.split(':'); return pair.length < 2 ? '' : this._normalize_badge_text_key(pair.shift()); - }).filter(Boolean); + }).filter(value => value && value != '*'); }, _get_record_field_value: function(record, name) { if (!record || !name) { @@ -26169,6 +26358,8 @@ function eval_pyson(value){ } else if (Object.prototype.hasOwnProperty.call( mapped, String(normalized_text).toLowerCase())) { color = mapped[String(normalized_text).toLowerCase()]; + } else if (Object.prototype.hasOwnProperty.call(mapped, '*')) { + color = mapped['*']; } if (!color && attributes.badge_color_field) { color = this._get_record_field_value( @@ -26714,7 +26905,7 @@ function eval_pyson(value){ } return rows; }, - _get_summary_badge_columns: function() { + _get_displayed_column_map: function() { var visible_columns = {}; var column_api = this.gridColumnApi || this.gridApi; if (column_api && column_api.getAllDisplayedColumns) { @@ -26724,37 +26915,336 @@ function eval_pyson(value){ } }); } + return visible_columns; + }, + _get_viewport_column_map: function() { + var visible_columns = {}; + var column_api = this.gridColumnApi || this.gridApi; + var columns = null; + if (column_api && column_api.getAllDisplayedVirtualColumns) { + columns = column_api.getAllDisplayedVirtualColumns(); + } else if (column_api && column_api.getAllDisplayedColumns) { + columns = column_api.getAllDisplayedColumns(); + } + (columns || []).forEach(column => { + if (column && column.getColId) { + visible_columns[column.getColId()] = true; + } + }); + return visible_columns; + }, + _is_summary_column_visible: function(column, visible_columns) { + var name = column && column.attributes && column.attributes.name; + return name && (!Object.keys(visible_columns).length || + visible_columns[name]); + }, + _get_summary_badge_columns: function() { return this.columns.filter(column => { var name = column.attributes && column.attributes.name; - var label = column.attributes && - (column.attributes.string || column.attributes.name) || ''; - var is_status_column = /(^|[_\s-])(status|state|type)([_\s-]|$)/i - .test(String(name || '') + ' ' + String(label || '')); - if (!name || (!this._is_badge_column(column) && - !is_status_column)) { - return false; - } - return !Object.keys(visible_columns).length || - visible_columns[name]; + return name && column.attributes.widget == 'badge'; }); }, - _render_summary_item: function(label, value, color) { + _get_summary_sum_columns: function() { + var columns = []; + this.sum_widgets.forEach((sum_widget, column) => { + if (column && column.attributes && column.attributes.name) { + columns.push(column); + } + }); + return columns; + }, + _get_summary_variation_columns: function() { + return this.columns.filter(column => { + var attributes = column && column.attributes || {}; + return attributes.name && + attributes.widget == 'variation' && + attributes.variation_summary == '1' && + attributes.variation_previous && + attributes.variation_group_by; + }); + }, + _render_summary_item: function( + label, value, color, class_name, variation_text, + variation_class) { var item = jQuery('', { - 'class': 'ag-grid-summary-item', + 'class': 'ag-grid-summary-item' + + (class_name ? ' ' + class_name : ''), }); jQuery('', { 'class': 'ag-grid-summary-label', 'text': label, }).appendTo(item); + if (variation_text) { + jQuery('', { + 'class': 'ag-grid-summary-variation-badge ' + + (variation_class || ''), + 'text': variation_text, + }).appendTo(item); + } + var value_wrap = jQuery('', { + 'class': 'ag-grid-summary-value-wrap', + }).appendTo(item); var value_el = jQuery('', { 'class': 'ag-grid-summary-value', 'text': value, - }).appendTo(item); + }).appendTo(value_wrap); if (color) { value_el.css('color', color); } return item; }, + _trace_summary_bar: function( + rows, badge_columns, sum_columns, variation_columns, summary_items) { + if (!window.console) { + return; + } + var describe = column => ({ + name: column && column.attributes && column.attributes.name || null, + label: column && column.attributes && + (column.attributes.string || column.attributes.name) || '', + widget: column && column.attributes && column.attributes.widget || '', + sum: column && column.attributes && column.attributes.sum || '', + badge: Boolean(column && column.attributes && + column.attributes.widget == 'badge'), + }); + console.info('[AGGRID][SUMMARY_BAR_JSON]', JSON.stringify({ + instanceId: this._debug_id, + viewId: this.view_id, + model: this.screen && this.screen.model_name, + rowCount: rows.length, + viewportColumns: Object.keys(this._get_viewport_column_map()), + displayedColumns: Object.keys(this._get_displayed_column_map()), + badgeColumns: badge_columns.map(describe), + sumColumns: sum_columns.map(describe), + variationColumns: variation_columns.map(describe), + sumWidgetColumns: Array.from(this.sum_widgets.keys()).map(describe), + badgeLikeColumns: this.columns.filter(column => + this._is_badge_column(column)).map(describe), + summaryItems: summary_items.map(item => ({ + label: item.label, + count: item.count, + className: item.className || '', + })), + })); + }, + _get_summary_column_load_fields: function(column) { + var attributes = column && column.attributes || {}; + var fields = []; + if (attributes.name) { + fields.push(attributes.name); + } + if (attributes.widget == 'variation') { + if (attributes.variation_previous) { + fields.push(attributes.variation_previous); + } + if (attributes.variation_group_by) { + fields.push(attributes.variation_group_by); + } + } + if (attributes.sum_variation == '1' && attributes.sum_previous) { + fields.push(attributes.sum_previous); + } + return fields.filter((field, index) => + field && fields.indexOf(field) == index); + }, + _load_summary_column_values: function(rows, columns, reason) { + if (this._summary_sum_loading) { + return null; + } + var promises = []; + var field_names = []; + columns.forEach(column => { + var names = this._get_summary_column_load_fields(column); + field_names = field_names.concat(names); + rows.forEach(row => { + var record = row && row.__record; + names.forEach(name => { + if (record && record.id >= 0 && + !record.get_loaded([name])) { + promises.push(record.load(name, true, false)); + } + }); + }); + }); + if (!promises.length) { + return null; + } + this._summary_sum_loading = true; + if (window.console) { + console.info('[AGGRID][SUMMARY_SUM_LOAD_JSON]', + JSON.stringify({ + instanceId: this._debug_id, + viewId: this.view_id, + model: this.screen && this.screen.model_name, + reason: reason || 'summary', + fields: field_names.filter((field, index) => + field && field_names.indexOf(field) == index), + promiseCount: promises.length, + })); + } + return jQuery.when.apply(jQuery, promises).always(() => { + this._summary_sum_loading = false; + }); + }, + _load_summary_sum_values: function(rows, columns) { + return this._load_summary_column_values(rows, columns, 'sum'); + }, + _load_summary_badge_values: function(rows, columns) { + return this._load_summary_column_values(rows, columns, 'badge'); + }, + _append_sum_summary_item: function(summary_items, rows, column) { + var sum = null; + var name = column && column.attributes && column.attributes.name; + var field = name ? this.screen.model.fields[name] : null; + var previous_sum = null; + var previous_name = column && column.attributes && + column.attributes.sum_previous; + var with_variation = column && column.attributes && + column.attributes.sum_variation == '1' && previous_name; + rows.forEach(row => { + var record = row && row.__record; + if (!field || !record || record.id >= 0 && + !record.get_loaded([name])) { + return; + } + var value = field.get(record); + if (value && value.isTimeDelta) { + value = value.asSeconds(); + } + if (value !== null && value !== undefined && value !== '') { + sum = sum === null ? value : sum + value; + } + if (with_variation) { + var previous = this._get_record_raw_field_value( + record, previous_name); + if (previous !== null && previous !== undefined && + previous !== '') { + previous = Number(previous); + if (isFinite(previous)) { + previous_sum = previous_sum === null ? + previous : previous_sum + previous; + } + } + } + }); + var variation = null; + if (with_variation && sum !== null && previous_sum !== null && + previous_sum !== 0) { + variation = ((sum - previous_sum) / + Math.abs(previous_sum)) * 100; + } + summary_items.push({ + label: column.attributes.string || column.attributes.name, + count: sum === null ? '-' : + this._format_aggregate_value(column, sum), + color: '#111111', + className: 'ag-grid-summary-item-sum', + variationText: variation === null ? '' : + this._format_sum_variation_percent(variation), + variationClass: variation === null ? '' : + (variation < 0 ? 'ag-grid-summary-variation-negative' : + 'ag-grid-summary-variation-positive'), + }); + }, + _get_record_raw_field_value: function(record, name) { + if (!record || !name) { + return null; + } + var field = this.screen.model.fields[name]; + if (!field || record.id >= 0 && !record.get_loaded([name])) { + return null; + } + var value = field.get(record); + if (value && value.isTimeDelta) { + value = value.asSeconds(); + } + if (value === null || value === undefined || value === '') { + return null; + } + return value; + }, + _get_record_text_field_value: function(record, name) { + if (!record || !name) { + return ''; + } + var field = this.screen.model.fields[name]; + if (!field || record.id >= 0 && !record.get_loaded([name])) { + return ''; + } + try { + return field.get_client(record); + } catch (error) { + var value = this._get_record_raw_field_value(record, name); + return value === null || value === undefined ? '' : String(value); + } + }, + _format_variation_percent: function(value) { + var locale = Sao.i18n.BC47(Sao.i18n.getlang()); + var sign = value > 0 ? '+' : ''; + return sign + value.toLocaleString(locale, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }) + '%'; + }, + _format_sum_variation_percent: function(value) { + var locale = Sao.i18n.BC47(Sao.i18n.getlang()); + var sign = value >= 0 ? '+' : ''; + return sign + value.toLocaleString(locale, { + minimumFractionDigits: 0, + maximumFractionDigits: 2, + }) + '%'; + }, + _append_variation_summary_items: function(summary_items, rows, column) { + var attributes = column && column.attributes || {}; + var current_name = attributes.name; + var previous_name = attributes.variation_previous; + var group_name = attributes.variation_group_by; + var groups = {}; + rows.forEach(row => { + var record = row && row.__record; + if (!record) { + return; + } + var current = Number(this._get_record_raw_field_value( + record, current_name)); + var previous = Number(this._get_record_raw_field_value( + record, previous_name)); + var group = this._get_record_text_field_value( + record, group_name) || Sao.i18n.gettext('(Empty)'); + if (!isFinite(current) || !isFinite(previous) || previous === 0) { + return; + } + if (!groups[group]) { + groups[group] = { + current: 0, + previous: 0, + }; + } + groups[group].current += current; + groups[group].previous += previous; + }); + Object.keys(groups).sort((a, b) => String(a).localeCompare( + String(b), undefined, {numeric: true, sensitivity: 'base'})) + .forEach(group => { + var values = groups[group]; + if (!values.previous) { + return; + } + var variation = ((values.current - values.previous) / + Math.abs(values.previous)) * 100; + summary_items.push({ + label: group, + count: this._format_variation_percent(variation), + color: variation > 0 ? '#16a34a' : + variation < 0 ? '#dc2626' : '#64748b', + className: 'ag-grid-summary-item-variation' + + (variation > 0 ? ' ag-grid-summary-variation-up' : + variation < 0 ? + ' ag-grid-summary-variation-down' : + ' ag-grid-summary-variation-flat'), + }); + }); + }, _get_default_badge_summary_color: function(value) { var key = String(value || '').toLowerCase(); var palette = { @@ -26785,8 +27275,16 @@ function eval_pyson(value){ var order = []; var empty = Sao.i18n.gettext('(Empty)'); rows.forEach(row => { - var value = row[name] ? - this._normalize_badge_text_key(row[name]) : empty; + var raw_value = ''; + if (column && row && row.__record) { + raw_value = this._get_column_text_value( + column, row.__record); + } + if (!raw_value && row) { + raw_value = row[name]; + } + var value = raw_value ? + this._normalize_badge_text_key(raw_value) : empty; if (value == empty) { return; } @@ -26836,27 +27334,6 @@ function eval_pyson(value){ }); return Boolean(order.length); }, - _get_summary_status_sources_from_grid: function() { - var sources = []; - var seen = {}; - (this._grid_columns || []).forEach(col_def => { - var name = col_def && col_def.field || col_def && col_def.colId; - if (!name || name == '__selection__' || seen[name]) { - return; - } - var label = col_def.headerName || name; - if (!/(status|state|type|shipping|shipment|open\/physic)/i - .test(String(name) + ' ' + String(label))) { - return; - } - seen[name] = true; - sources.push({ - name: name, - label: label, - }); - }); - return sources; - }, _update_summary_bar: function() { if (!this.summary_bar || !this.summary_bar.length) { return; @@ -26869,25 +27346,38 @@ function eval_pyson(value){ rows.length.toLocaleString(locale), '#145369')); var summary_items = []; - var summarized_names = {}; - this._get_summary_badge_columns().forEach(column => { - if (this._append_badge_summary_items( - summary_items, rows, column)) { - summarized_names[column.attributes.name] = true; - } - }); - this._get_summary_status_sources_from_grid().forEach(source => { - if (summarized_names[source.name]) { - return; - } + var sum_columns = this._get_summary_sum_columns(); + var badge_columns = this._get_summary_badge_columns(); + var variation_columns = this._get_summary_variation_columns(); + var summary_load = this._load_summary_column_values( + rows, badge_columns.concat(sum_columns, variation_columns), + 'summary'); + if (summary_load) { + summary_load.then(() => this._update_summary_bar()); + } + badge_columns.forEach(column => { this._append_badge_summary_items( - summary_items, rows, null, source); + summary_items, rows, column); }); + sum_columns.forEach(column => { + this._append_sum_summary_item(summary_items, rows, column); + }); + variation_columns.forEach(column => { + this._append_variation_summary_items( + summary_items, rows, column); + }); + this._trace_summary_bar( + rows, badge_columns, sum_columns, variation_columns, + summary_items); summary_items.forEach(item => { this.summary_bar.append(this._render_summary_item( item.label, - item.count.toLocaleString(locale), - item.color)); + typeof item.count == 'number' ? + item.count.toLocaleString(locale) : item.count, + item.color, + item.className, + item.variationText, + item.variationClass)); }); }, on_copy: function() { @@ -27052,7 +27542,10 @@ function eval_pyson(value){ var readable_min_height = 420; var maximum_height = 720; var height; - if (available >= compact_min_height) { + if (this.is_embedded_one2many && this._embedded_height) { + height = Math.max(80, this._embedded_height - toolbar_height - + summary_height - external_scroll_height); + } else if (available >= compact_min_height) { height = Math.max( compact_min_height, Math.min(maximum_height, available)); @@ -27104,6 +27597,17 @@ function eval_pyson(value){ }); } }, + set_embedded_height: function(height) { + height = parseInt(height, 10); + if (!this.is_embedded_one2many || isNaN(height) || height <= 0) { + return; + } + this._embedded_height = height; + this._resize_grid_host(); + if (this.gridApi && this.gridApi.doLayout) { + this.gridApi.doLayout(); + } + }, _refresh_grid_layout_after_row_data: function(row_data) { if (!this.gridApi) { return; @@ -27319,7 +27823,7 @@ function eval_pyson(value){ } else if (diagnostics.relationWidget) { diagnostics.fallbackReason = diagnostics.relationWidget + '_relation_widget'; - } else if (diagnostics.embeddedContext) { + } else if (diagnostics.embeddedContext && diagnostics.editable) { diagnostics.fallbackReason = 'embedded_relation_tree'; } else if (diagnostics.editable) { diagnostics.fallbackReason = 'editable_view'; @@ -30481,6 +30985,7 @@ function eval_pyson(value){ 'timedelta': Sao.View.Tree.TimeDeltaColumn, 'tolerance_gauge': Sao.View.Tree.ToleranceGauge, 'url': Sao.View.Tree.URLColumn, + 'variation': Sao.View.Tree.CharColumn, }; Sao.View.EditableTree = {};