diff --git a/dist/tryton-sao.css b/dist/tryton-sao.css index 9dcdc09..a8b315d 100644 --- a/dist/tryton-sao.css +++ b/dist/tryton-sao.css @@ -11167,6 +11167,78 @@ html[theme="default"] .screen-container .nav-tabs > li.active > a:hover { box-shadow: 0 2px 8px rgba(53, 81, 84, 0.12); } +.ag-filter-apply-panel { + display: flex; + justify-content: flex-end; + gap: 5px; + padding: 6px 8px 8px; +} + +.ag-filter-apply-panel button, +.ag-filter-apply-panel .ag-button, +.ag-filter-apply-panel .ag-standard-button, +.ag-popup .ag-filter-apply-panel button, +.ag-popup .ag-filter-apply-panel .ag-button, +.ag-popup .ag-filter-apply-panel .ag-standard-button, +.ag-theme-alpine .ag-filter-apply-panel button, +.ag-theme-alpine .ag-filter-apply-panel .ag-button, +.ag-theme-alpine .ag-filter-apply-panel .ag-standard-button { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 27px; + padding: 3px 8px; + border: 1px solid #d8e2df !important; + border-radius: 6px !important; + background: #ffffff !important; + color: #355154 !important; + font-size: 11.5px; + font-weight: 600; + letter-spacing: 0; + line-height: 1.2; + text-transform: none !important; + box-shadow: none !important; + transition: background-color 0.18s ease, border-color 0.18s ease, + box-shadow 0.18s ease, transform 0.18s ease; +} + +.ag-filter-apply-panel button:hover, +.ag-filter-apply-panel .ag-button:hover, +.ag-filter-apply-panel .ag-standard-button:hover, +.ag-filter-apply-panel button:focus, +.ag-filter-apply-panel .ag-button:focus, +.ag-filter-apply-panel .ag-standard-button:focus, +.ag-popup .ag-filter-apply-panel button:hover, +.ag-popup .ag-filter-apply-panel .ag-button:hover, +.ag-popup .ag-filter-apply-panel .ag-standard-button:hover, +.ag-popup .ag-filter-apply-panel button:focus, +.ag-popup .ag-filter-apply-panel .ag-button:focus, +.ag-popup .ag-filter-apply-panel .ag-standard-button:focus, +.ag-theme-alpine .ag-filter-apply-panel button:hover, +.ag-theme-alpine .ag-filter-apply-panel .ag-button:hover, +.ag-theme-alpine .ag-filter-apply-panel .ag-standard-button:hover, +.ag-theme-alpine .ag-filter-apply-panel button:focus, +.ag-theme-alpine .ag-filter-apply-panel .ag-button:focus, +.ag-theme-alpine .ag-filter-apply-panel .ag-standard-button:focus { + background: linear-gradient(180deg, #f2faf7 0%, #e7f2ed 100%) !important; + border-color: #bfd5cd !important; + color: #28484b !important; + box-shadow: 0 1px 1px rgba(53, 81, 84, 0.05) !important; +} + +.ag-filter-apply-panel button:active, +.ag-filter-apply-panel .ag-button:active, +.ag-filter-apply-panel .ag-standard-button:active, +.ag-popup .ag-filter-apply-panel button:active, +.ag-popup .ag-filter-apply-panel .ag-button:active, +.ag-popup .ag-filter-apply-panel .ag-standard-button:active, +.ag-theme-alpine .ag-filter-apply-panel button:active, +.ag-theme-alpine .ag-filter-apply-panel .ag-button:active, +.ag-theme-alpine .ag-filter-apply-panel .ag-standard-button:active { + transform: translateY(1px); + box-shadow: 0 2px 8px rgba(53, 81, 84, 0.12) !important; +} + .ag-grid-tree-columns-label, .ag-grid-tree-grouping-label, .ag-grid-tree-layouts-label, @@ -11183,6 +11255,133 @@ html[theme="default"] .screen-container .nav-tabs > li.active > a:hover { color: #0f6f6d; } +.ag-grid-active-filter-chips { + display: inline-flex; + align-items: center; + gap: 4px; + min-width: 0; + max-width: min(520px, 38vw); + min-height: 32px; + overflow: hidden; +} + +.ag-grid-active-filter-chips.drag-active { + min-width: 180px; + padding: 2px 6px; + border: 1px dashed #8fc3bd; + border-radius: 8px; + background: #f6fbf9; +} + +.ag-grid-active-filter-chips.drag-over { + border-color: #0f817d; + background: #eef8f6; +} + +.ag-grid-active-filter-drop-hint { + color: #0f6f6d; + font-size: 11px; + font-weight: 700; + white-space: nowrap; +} + +.ag-grid-filter-draggable-cell { + cursor: grab; +} + +.ag-grid-filter-draggable-cell:active { + cursor: grabbing; +} + +.ag-grid-active-filter-chip { + display: grid; + grid-template-columns: minmax(0, 1fr) 14px; + grid-template-rows: auto auto; + column-gap: 6px; + row-gap: 1px; + align-items: center; + max-width: 190px; + min-height: 32px; + padding: 3px 5px 3px 8px; + border: 1px solid #c9ded8; + border-radius: 7px; + background: #f8fbfa; + color: #285256; + font-size: 10.5px; + font-weight: 600; + line-height: 1.2; + box-shadow: none; +} + +.ag-grid-active-filter-chip:hover, +.ag-grid-active-filter-chip:focus { + border-color: #92bfb8; + background: #eff8f5; + color: #0f6f6d; +} + +.ag-grid-active-filter-chip-name, +.ag-grid-active-filter-chip-detail { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ag-grid-active-filter-chip-name { + grid-column: 1; + grid-row: 1; + color: #637b87; + font-size: 10px; + font-weight: 700; +} + +.ag-grid-active-filter-chip-value { + display: inline-flex; + grid-column: 1; + grid-row: 2; + min-width: 0; + gap: 4px; + color: #0f4f60; +} + +.ag-grid-active-filter-chip-operator { + flex: 0 0 auto; + color: #0f817d; + font-weight: 800; +} + +.ag-grid-active-filter-chip-detail { + font-weight: 700; +} + +.ag-grid-active-filter-chip-remove { + display: inline-flex; + grid-column: 2; + grid-row: 1 / span 2; + align-items: center; + justify-content: center; + width: 14px; + height: 14px; + border-radius: 50%; + color: #6e8588; + font-size: 11px; + line-height: 1; +} + +.ag-grid-active-filter-chip-remove:hover { + background: #dcebe7; + color: #0f6f6d; +} + +.ag-grid-active-filter-chip-more { + display: inline-flex; + max-width: none; + min-height: 27px; + padding: 2px 7px; + border-radius: 999px; +} + .ag-grid-tree-grouping-panel .btn, .ag-grid-tree-layouts-panel .btn, .ag-grid-filter-logic-panel .btn { @@ -12774,7 +12973,10 @@ 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-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 { +html[data-theme-mode="dark"] .ag-grid-filter-logic-panel .btn, +html[data-theme-mode="dark"] .ag-theme-alpine .ag-filter-apply-panel button, +html[data-theme-mode="dark"] .ag-theme-alpine .ag-filter-apply-panel .ag-button, +html[data-theme-mode="dark"] .ag-theme-alpine .ag-filter-apply-panel .ag-standard-button { background: linear-gradient(180deg, #213130 0%, #1a2726 100%); border-color: #35504d; color: #dce7e2; @@ -12789,7 +12991,10 @@ 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-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 { +html[data-theme-mode="dark"] .ag-grid-filter-logic-panel .btn:hover, +html[data-theme-mode="dark"] .ag-theme-alpine .ag-filter-apply-panel button:hover, +html[data-theme-mode="dark"] .ag-theme-alpine .ag-filter-apply-panel .ag-button:hover, +html[data-theme-mode="dark"] .ag-theme-alpine .ag-filter-apply-panel .ag-standard-button:hover { background: linear-gradient(180deg, #27403d 0%, #203230 100%); border-color: #4f716c; color: #f0faf6; diff --git a/dist/tryton-sao.js b/dist/tryton-sao.js index 02bb1c1..67c9131 100644 --- a/dist/tryton-sao.js +++ b/dist/tryton-sao.js @@ -15008,7 +15008,8 @@ function eval_pyson(value){ }, _parse_form: function(node, attributes) { var container = new Sao.View.Form.Container( - Number(node.getAttribute('col') || 4)); + Number(node.getAttribute('col') || 4), + attributes.col_widths); this.view.containers.push(container); this.parse_child(node, container); if (this._containers.length > 0) { @@ -15205,7 +15206,8 @@ function eval_pyson(value){ return; } var container = new Sao.View.Form.Container( - Number(node.getAttribute('col') || 4)); + Number(node.getAttribute('col') || 4), + attributes.col_widths); this.view.containers.push(container); this.parse_child(node, container); var page = new Sao.View.Form.Page( @@ -15216,7 +15218,8 @@ function eval_pyson(value){ }, _parse_group: function(node, attributes) { var group = new Sao.View.Form.Container( - Number(node.getAttribute('col') || 4)); + Number(node.getAttribute('col') || 4), + attributes.col_widths); this.view.containers.push(group); this.parse_child(node, group); @@ -15259,7 +15262,8 @@ function eval_pyson(value){ _parse_child: function(node, attributes) { var paned = this.container; var container = new Sao.View.Form.Container( - Number(node.getAttribute('col') || 4)); + Number(node.getAttribute('col') || 4), + attributes.col_widths); this.view.containers.push(container); this.parse_child(node, container); @@ -15528,9 +15532,10 @@ function eval_pyson(value){ }); Sao.View.Form.Container = Sao.class_(Object, { - init: function(col=4) { + init: function(col=4, col_widths=null) { if (col < 0) col = 0; this.col = col; + this.col_widths = this._parse_col_widths(col_widths); this.el = jQuery('
', { 'class': 'form-container' }); @@ -15547,6 +15552,21 @@ function eval_pyson(value){ this._grid_cols = []; this._grid_rows = []; }, + _parse_col_widths: function(col_widths) { + if (!col_widths) { + return []; + } + return String(col_widths).split(',').map(width => { + width = String(width || '').trim(); + if (!width) { + return null; + } + if (/^\d+(\.\d+)?$/.test(width)) { + return width + 'px'; + } + return width; + }); + }, add_row: function() { this._col = 1; this._row += 1; @@ -15659,7 +15679,15 @@ function eval_pyson(value){ var i; var col = this.col <= 0 ? this._col : this.col; - if (this._xexpand.size) { + if (this.col_widths.length) { + for (i = 1; i <= col; i++) { + this._grid_cols.push( + this.col_widths[i - 1] || + (this._xexpand.has(i) ? + `minmax(min-content, ${col}fr)` : + 'min-content')); + } + } else if (this._xexpand.size) { for (i = 1; i <= col; i++) { if (this._xexpand.has(i)) { this._grid_cols.push(`minmax(min-content, ${col}fr)`); @@ -22691,6 +22719,29 @@ function eval_pyson(value){ this._toggle_filter_logic_menu(); }) .appendTo(this.toolbar); + this.active_filter_chips = jQuery('', { + 'class': 'ag-grid-active-filter-chips', + 'aria-live': 'polite', + }).hide().appendTo(this.toolbar); + this.active_filter_chips.on('dragover', evt => { + if (!this._filter_drag_payload) { + return; + } + evt.preventDefault(); + evt.originalEvent.dataTransfer.dropEffect = 'copy'; + this.active_filter_chips.addClass('drag-over'); + }); + this.active_filter_chips.on('dragleave', evt => { + if (evt.currentTarget.contains(evt.relatedTarget)) { + return; + } + this.active_filter_chips.removeClass('drag-over'); + }); + this.active_filter_chips.on('drop', evt => { + evt.preventDefault(); + this.active_filter_chips.removeClass('drag-over'); + this._apply_filter_drop(evt.originalEvent); + }); this.import_layout_input = jQuery('', { 'type': 'file', 'accept': 'application/json,.json', @@ -22774,17 +22825,8 @@ function eval_pyson(value){ }); Sao.View.AGGridTree._super.init.call(this, view_id, screen, xml); - 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 = this._load_persisted_state(); + if (this._persisted_state && this._persisted_state.valueFilters) { this._apply_sao_value_filter_state( this._persisted_state.valueFilters); @@ -22792,6 +22834,7 @@ function eval_pyson(value){ this._apply_saved_sort_to_screen_order(); this._update_layout_button(); this._update_sao_value_filter_mode_button(); + this._render_active_filter_chips(); this._update_copy_state(); this._init_grid(); this._install_resize_observer(); @@ -22921,7 +22964,33 @@ function eval_pyson(value){ }); }, _get_layout_store_cache_key: function() { - return this._get_storage_key(); + return this._get_layout_storage_key(); + }, + _get_layout_storage_key: function() { + var key = this._get_storage_key(); + if (!this.is_embedded_one2many) { + return key; + } + var attributes = this.screen && this.screen.attributes || {}; + var context_parts = [ + 'embedded_one2many', + attributes.name, + attributes.field, + attributes.relation_field, + attributes.one2many, + attributes.parent_name, + attributes.child_name, + this.screen && this.screen.exclude_field, + this.screen && this.screen.group && + this.screen.group.parent_name, + this.screen && this.screen.group && + this.screen.group.child_name, + ].map(function(value) { + return String(value || '').replace(/[:\s]+/g, '_'); + }).filter(function(value) { + return Boolean(value); + }); + return [key].concat(context_parts).join(':'); }, _new_layout_id: function() { return 'layout_' + String(Date.now()) + '_' + @@ -22973,7 +23042,8 @@ function eval_pyson(value){ return this._normalize_layout_store(null); } try { - var raw_state = localStorage.getItem(this._get_storage_key()); + var raw_state = localStorage.getItem( + this._get_layout_storage_key()); if (!raw_state) { return this._normalize_layout_store(null); } @@ -23018,7 +23088,7 @@ function eval_pyson(value){ } try { localStorage.setItem( - this._get_storage_key(), + this._get_layout_storage_key(), JSON.stringify(store)); } catch (error) { Sao.Logger.warn( @@ -23142,9 +23212,6 @@ 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( @@ -23344,6 +23411,7 @@ function eval_pyson(value){ this._sao_value_filter_groups = state.groups ? jQuery.extend(true, {}, state.groups) : null; this._update_sao_value_filter_mode_button(); + this._render_active_filter_chips(); }, _apply_saved_sort_to_screen_order: function() { if (!this._persisted_state || !this._persisted_state.columns) { @@ -24827,6 +24895,7 @@ function eval_pyson(value){ this._schedule_state_save(); this._sync_selection_header_checkbox(); this._update_summary_bar(); + this._render_active_filter_chips(); }, onFirstDataRendered: () => { window.requestAnimationFrame(() => this._update_summary_bar()); @@ -25003,6 +25072,10 @@ function eval_pyson(value){ } return ''; } + if (this._is_date_filter_column(column)) { + return this._get_column_date_filter_value( + column, params.data.__record); + } return this._get_column_text_value( column, params.data.__record); }, @@ -25023,11 +25096,16 @@ function eval_pyson(value){ if (!params.data.__record) { return ''; } + var rendered; if (badge_column) { - return this._render_badge_cell( + rendered = this._render_badge_cell( + column, params.data.__record); + } else { + rendered = this._render_cell( column, params.data.__record); } - return this._render_cell(column, params.data.__record); + return this._make_filter_draggable_cell( + rendered, column, params.data.__record); }, tooltipValueGetter: params => { if (!params.data || !params.data.__record || @@ -25230,9 +25308,6 @@ function eval_pyson(value){ case 'numeric': return 'agNumberColumnFilter'; case 'date': - if (this._get_distinct_filter_values(column)) { - return Sao.View.AGGridValueSetFilter; - } return 'agDateColumnFilter'; default: if (this._get_distinct_filter_values(column)) { @@ -25242,6 +25317,9 @@ function eval_pyson(value){ } }, _get_column_filter_params: function(column) { + if (this._is_date_filter_column(column)) { + return this._get_date_column_filter_params(column); + } var values = this._get_distinct_filter_values(column); if (!values) { return null; @@ -25260,6 +25338,135 @@ function eval_pyson(value){ searchPlaceholder: Sao.i18n.gettext('Search'), }; }, + _is_date_filter_column: function(column) { + return Boolean(column && column.attributes && + column.attributes.widget == 'date'); + }, + _get_date_column_filter_params: function(column) { + var relative_options = this._get_relative_date_filter_options(); + return { + browserDatePicker: true, + buttons: ['reset', 'apply'], + closeOnApply: true, + maxNumConditions: 1, + defaultOption: 'empty', + filterOptions: ['empty'].concat(relative_options).concat([ + 'inRange', + 'equals', + 'lessThan', + 'greaterThan', + 'blank', + 'notBlank', + ]), + inRangeInclusive: true, + comparator: function(filter_date, cell_value) { + if (!cell_value) { + return 0; + } + var cell_date = cell_value instanceof Date ? cell_value : + new Date(cell_value); + if (isNaN(cell_date.getTime())) { + return 0; + } + var cell_midnight = new Date( + cell_date.getFullYear(), + cell_date.getMonth(), + cell_date.getDate()); + if (cell_midnight < filter_date) { + return -1; + } + if (cell_midnight > filter_date) { + return 1; + } + return 0; + }, + }; + }, + _get_relative_date_filter_options: function() { + var make_option = (key, label, range_getter) => { + return { + displayKey: 'sao' + key, + displayName: Sao.i18n.gettext(label), + numberOfInputs: 0, + predicate: (filter_values, cell_value) => { + var cell_date = this._normalize_date_filter_value(cell_value); + if (!cell_date) { + return false; + } + var range = range_getter(); + return cell_date >= range.start && cell_date < range.end; + }, + }; + }; + return [ + make_option('Today', 'Today', () => { + var start = this._start_of_today(); + return { + start: start, + end: this._add_days(start, 1), + }; + }), + make_option('Last7Days', 'Last 7 Days', () => { + var today = this._start_of_today(); + return { + start: this._add_days(today, -7), + end: this._add_days(today, 1), + }; + }), + make_option('Last30Days', 'Last 30 Days', () => { + var today = this._start_of_today(); + return { + start: this._add_days(today, -30), + end: this._add_days(today, 1), + }; + }), + make_option('LastMonth', 'Last Month', () => { + var today = this._start_of_today(); + var start = new Date( + today.getFullYear(), today.getMonth() - 1, 1); + var end = new Date(today.getFullYear(), today.getMonth(), 1); + return { + start: start, + end: end, + }; + }), + make_option('ThisMonth', 'This Month', () => { + var today = this._start_of_today(); + var start = new Date(today.getFullYear(), today.getMonth(), 1); + var end = new Date( + today.getFullYear(), today.getMonth() + 1, 1); + return { + start: start, + end: end, + }; + }), + make_option('YearToDate', 'Year to Date', () => { + var today = this._start_of_today(); + return { + start: new Date(today.getFullYear(), 0, 1), + end: this._add_days(today, 1), + }; + }), + ]; + }, + _start_of_today: function() { + var now = new Date(); + return new Date(now.getFullYear(), now.getMonth(), now.getDate()); + }, + _add_days: function(date, days) { + return new Date( + date.getFullYear(), date.getMonth(), date.getDate() + days); + }, + _normalize_date_filter_value: function(value) { + if (!value) { + return null; + } + var date = value instanceof Date ? value : new Date(value); + if (isNaN(date.getTime())) { + return null; + } + return new Date(date.getFullYear(), date.getMonth(), date.getDate()); + }, _get_sao_value_filter_state: function(col_id) { var filter = this._sao_value_filters && this._sao_value_filters[col_id]; @@ -25285,7 +25492,113 @@ function eval_pyson(value){ }, _is_sao_value_filter_user_source: function(source) { return ~['checkbox-change', 'all-button', 'none-button', - 'clear-button'].indexOf(source); + 'clear-button', 'drag-drop'].indexOf(source); + }, + _make_filter_draggable_cell: function(element, column, record) { + if (!element || !column || !column.attributes || + !column.attributes.name || !record) { + return element; + } + var value = this._get_column_text_value(column, record); + if (!value) { + return element; + } + var wrapped = jQuery(element); + wrapped.attr('draggable', 'true') + .addClass('ag-grid-filter-draggable-cell') + .on('dragstart', evt => { + var payload = { + colId: column.attributes.name, + label: this._get_column_label(column.attributes.name), + value: String(value), + }; + this._filter_drag_payload = payload; + evt.originalEvent.dataTransfer.effectAllowed = 'copy'; + evt.originalEvent.dataTransfer.setData( + 'application/x-sao-ag-grid-filter', + JSON.stringify(payload)); + evt.originalEvent.dataTransfer.setData( + 'text/plain', + payload.label + ' = ' + payload.value); + this._set_filter_dropzone_active(true, payload); + }) + .on('dragend', () => { + this._filter_drag_payload = null; + this._set_filter_dropzone_active(false); + }); + return wrapped[0]; + }, + _set_filter_dropzone_active: function(active, payload) { + if (!this.active_filter_chips) { + return; + } + this.active_filter_chips + .toggleClass('drag-active', Boolean(active)) + .removeClass('drag-over'); + if (active && !this._get_active_filter_chip_entries().length) { + this.active_filter_chips.empty(); + jQuery('', { + 'class': 'ag-grid-active-filter-drop-hint', + 'text': payload ? + Sao.i18n.gettext('Drop to filter %1') + .replace('%1', payload.label) : + Sao.i18n.gettext('Drop to filter'), + }).appendTo(this.active_filter_chips); + this.active_filter_chips.css('display', 'inline-flex'); + } else if (!active) { + this._render_active_filter_chips(); + } + }, + _get_filter_drop_payload: function(event) { + var payload = this._filter_drag_payload; + if (!payload && event && event.dataTransfer) { + try { + payload = JSON.parse(event.dataTransfer.getData( + 'application/x-sao-ag-grid-filter') || 'null'); + } catch (error) { + payload = null; + } + } + if (!payload || !payload.colId || !payload.value) { + return null; + } + return payload; + }, + _apply_filter_drop: function(event) { + var payload = this._get_filter_drop_payload(event); + this._filter_drag_payload = null; + if (!payload) { + this._set_filter_dropzone_active(false); + return; + } + var column = this._find_column_by_id(payload.colId); + if (!column) { + this._set_filter_dropzone_active(false); + return; + } + var empty = Sao.i18n.gettext('(Empty)'); + var value = String(payload.value || '') || empty; + var all_values = this._get_live_filter_values( + column, payload.colId).map(item => String(item || empty)); + if (!~all_values.indexOf(value)) { + all_values.push(value); + } + var existing = this._sao_value_filters && + this._sao_value_filters[payload.colId]; + var selected = existing && existing.selected ? + Array.from(existing.selected).map(item => String(item)) : []; + if (!selected.length) { + selected = [value]; + } else if (!~selected.indexOf(value)) { + selected.push(value); + } + this._set_sao_value_filter( + payload.colId, + selected, + all_values, + 'drag-drop', + {value: value}); + this._set_filter_dropzone_active(false); }, _forget_sao_value_filter_saved_state: function(col_id) { var remove_filter = value_filters => { @@ -25358,6 +25671,7 @@ function eval_pyson(value){ } this._apply_sao_value_filters(); this._render_filter_logic_panel(); + this._render_active_filter_chips(); this._schedule_state_save(); }, _trace_sao_value_filter_change: function(reason, col_id, selected, all, @@ -25599,6 +25913,208 @@ function eval_pyson(value){ .find('.ag-grid-value-filter-mode-label') .text(' ' + Sao.i18n.gettext(any ? 'Any filter' : 'All filters')); }, + _get_column_label: function(col_id) { + var column = this._find_column_by_id(col_id); + return column && column.attributes ? + column.attributes.string || column.attributes.name || col_id : + col_id; + }, + _describe_grid_filter_model: function(model) { + if (!model) { + return { + operator: '', + value: '', + }; + } + var type = model.type || model.operator || ''; + var value = model.filter; + if (value === undefined || value === null || value === '') { + value = model.dateFrom || model.filterTo || ''; + } + if (Array.isArray(model.values)) { + return { + operator: '=', + value: model.values.length <= 2 ? + model.values.join(', ') : + Sao.i18n.gettext('%1 values').replace('%1', model.values.length), + }; + } + if (model.condition1 || model.condition2) { + return { + operator: '', + value: Sao.i18n.gettext('complex'), + }; + } + var relative_date_labels = { + saoToday: 'Today', + saoLast7Days: 'Last 7 Days', + saoLast30Days: 'Last 30 Days', + saoLastMonth: 'Last Month', + saoThisMonth: 'This Month', + saoYearToDate: 'Year to Date', + }; + if (relative_date_labels[type]) { + return { + operator: '', + value: Sao.i18n.gettext(relative_date_labels[type]), + }; + } + var operators = { + equals: '=', + notEqual: '!=', + contains: 'contains', + notContains: 'not contains', + startsWith: 'starts', + endsWith: 'ends', + lessThan: '<', + lessThanOrEqual: '<=', + greaterThan: '>', + greaterThanOrEqual: '>=', + inRange: 'between', + }; + return { + operator: operators[type] || type || '=', + value: value, + }; + }, + _get_active_filter_chip_entries: function() { + var entries = []; + Object.keys(this._sao_value_filters || {}).forEach(col_id => { + var filter = this._sao_value_filters[col_id]; + if (!this._is_sao_value_filter_active(filter)) { + return; + } + var selected = Array.from(filter.selected || []).sort(); + var all = Array.from(filter.all || []).sort(); + var selected_lookup = new Set(selected); + var excluded = all.filter(value => !selected_lookup.has(value)); + var display_values = excluded.length && + excluded.length < selected.length ? excluded : selected; + var operator = display_values === excluded ? '<>' : '='; + var detail = display_values.length <= 2 ? + display_values.join(', ') : + Sao.i18n.gettext('%1 values').replace( + '%1', display_values.length); + entries.push({ + type: 'sao', + colId: col_id, + label: this._get_column_label(col_id), + operator: operator, + detail: detail, + }); + }); + var filter_model = this.gridApi && this.gridApi.getFilterModel ? + this._strip_sao_value_set_filter_model( + this.gridApi.getFilterModel()) : {}; + Object.keys(filter_model || {}).forEach(col_id => { + var description = + this._describe_grid_filter_model(filter_model[col_id]); + entries.push({ + type: 'grid', + colId: col_id, + label: this._get_column_label(col_id), + operator: description.operator, + detail: description.value, + }); + }); + return entries; + }, + _open_filter_for_chip: function(entry) { + if (entry.type == 'sao') { + this._show_filter_logic_menu(); + return; + } + if (!this.gridApi) { + return; + } + if (this.gridApi.showColumnFilter) { + this.gridApi.showColumnFilter(entry.colId); + } else if (this.gridApi.showColumnMenu) { + this.gridApi.showColumnMenu(entry.colId); + } + }, + _remove_filter_chip: function(entry) { + if (entry.type == 'sao') { + delete this._sao_value_filters[entry.colId]; + this._forget_sao_value_filter_saved_state(entry.colId); + this._apply_sao_value_filters(); + this._render_filter_logic_panel(); + this._schedule_state_save(); + this._render_active_filter_chips(); + return; + } + if (!this.gridApi || !this.gridApi.getFilterModel || + !this.gridApi.setFilterModel) { + return; + } + var model = jQuery.extend(true, {}, this.gridApi.getFilterModel() || {}); + delete model[entry.colId]; + this.gridApi.setFilterModel(model); + if (this.gridApi.onFilterChanged) { + this.gridApi.onFilterChanged(); + } + }, + _render_active_filter_chips: function() { + if (!this.active_filter_chips) { + return; + } + var entries = this._get_active_filter_chip_entries(); + this.active_filter_chips.empty(); + if (!entries.length) { + this.active_filter_chips.hide(); + return; + } + var visible = entries.slice(0, 4); + visible.forEach(entry => { + var text = entry.label + (entry.operator ? ' ' + + entry.operator : '') + (entry.detail ? ' ' + + entry.detail : ''); + var chip = jQuery('', { + 'class': 'ag-grid-active-filter-chip', + 'type': 'button', + 'title': text, + }).click(evt => { + evt.preventDefault(); + this._open_filter_for_chip(entry); + }).appendTo(this.active_filter_chips); + jQuery('', { + 'class': 'ag-grid-active-filter-chip-name', + 'text': entry.label, + }).appendTo(chip); + var value = jQuery('', { + 'class': 'ag-grid-active-filter-chip-value', + }).appendTo(chip); + jQuery('', { + 'class': 'ag-grid-active-filter-chip-operator', + 'text': entry.operator || '=', + }).appendTo(value); + jQuery('', { + 'class': 'ag-grid-active-filter-chip-detail', + 'text': entry.detail || '', + }).appendTo(value); + jQuery('', { + 'class': 'ag-grid-active-filter-chip-remove', + 'text': 'x', + 'title': Sao.i18n.gettext('Remove filter'), + }).click(evt => { + evt.preventDefault(); + evt.stopPropagation(); + this._remove_filter_chip(entry); + }).appendTo(chip); + }); + if (entries.length > visible.length) { + jQuery('', { + 'class': 'ag-grid-active-filter-chip ag-grid-active-filter-chip-more', + 'type': 'button', + 'text': '+' + String(entries.length - visible.length), + 'title': Sao.i18n.gettext('Show filter logic'), + }).click(evt => { + evt.preventDefault(); + this._show_filter_logic_menu(); + }).appendTo(this.active_filter_chips); + } + this.active_filter_chips.css('display', 'inline-flex'); + }, _get_active_sao_value_filter_entries: function() { return Object.keys(this._sao_value_filters || {}) .filter(col_id => this._is_sao_value_filter_active( @@ -26229,6 +26745,47 @@ function eval_pyson(value){ return ''; } }, + _get_column_date_filter_value: function(column, record) { + if (!column || !record || !column.attributes.name) { + return null; + } + var name = column.attributes.name; + var value = null; + try { + var field = record.model && record.model.fields && + record.model.fields[name]; + if (field && field.get_client) { + value = field.get_client(record); + } + } catch (error) { + value = null; + } + if (!value && record._values && + Object.prototype.hasOwnProperty.call(record._values, name)) { + value = record._values[name]; + } + if (!value) { + return null; + } + if (value instanceof Date) { + return new Date( + value.getFullYear(), value.getMonth(), value.getDate()); + } + if (value.isDate || value.isDateTime) { + return new Date(value.year(), value.month(), value.date()); + } + if (typeof value == 'string') { + var date = moment(value, [ + 'YYYY-MM-DD', + Sao.common.date_format( + this.screen && this.screen.context && + this.screen.context.date_format), + ], true); + return date.isValid() ? + new Date(date.year(), date.month(), date.date()) : null; + } + return null; + }, _is_badge_column: function(column) { if (!column || !column.attributes) { return false; @@ -35527,7 +36084,8 @@ function eval_pyson(value){ Sao.View.BoardXMLViewParser = Sao.class_(Sao.View.FormXMLViewParser, { _parse_board: function(node, attributes) { var container = new Sao.View.Form.Container( - Number(node.getAttribute('col') || 4)); + Number(node.getAttribute('col') || 4), + attributes.col_widths); this.view.el.append(container.el); this.parse_child(node, container); if (this._containers.length > 0) { diff --git a/tryton-sao.css b/tryton-sao.css index fb1ee3c..122ca81 100644 --- a/tryton-sao.css +++ b/tryton-sao.css @@ -11172,6 +11172,78 @@ html[theme="default"] .screen-container .nav-tabs > li.active > a:hover { box-shadow: 0 2px 8px rgba(53, 81, 84, 0.12); } +.ag-filter-apply-panel { + display: flex; + justify-content: flex-end; + gap: 5px; + padding: 6px 8px 8px; +} + +.ag-filter-apply-panel button, +.ag-filter-apply-panel .ag-button, +.ag-filter-apply-panel .ag-standard-button, +.ag-popup .ag-filter-apply-panel button, +.ag-popup .ag-filter-apply-panel .ag-button, +.ag-popup .ag-filter-apply-panel .ag-standard-button, +.ag-theme-alpine .ag-filter-apply-panel button, +.ag-theme-alpine .ag-filter-apply-panel .ag-button, +.ag-theme-alpine .ag-filter-apply-panel .ag-standard-button { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 27px; + padding: 3px 8px; + border: 1px solid #d8e2df !important; + border-radius: 6px !important; + background: #ffffff !important; + color: #355154 !important; + font-size: 11.5px; + font-weight: 600; + letter-spacing: 0; + line-height: 1.2; + text-transform: none !important; + box-shadow: none !important; + transition: background-color 0.18s ease, border-color 0.18s ease, + box-shadow 0.18s ease, transform 0.18s ease; +} + +.ag-filter-apply-panel button:hover, +.ag-filter-apply-panel .ag-button:hover, +.ag-filter-apply-panel .ag-standard-button:hover, +.ag-filter-apply-panel button:focus, +.ag-filter-apply-panel .ag-button:focus, +.ag-filter-apply-panel .ag-standard-button:focus, +.ag-popup .ag-filter-apply-panel button:hover, +.ag-popup .ag-filter-apply-panel .ag-button:hover, +.ag-popup .ag-filter-apply-panel .ag-standard-button:hover, +.ag-popup .ag-filter-apply-panel button:focus, +.ag-popup .ag-filter-apply-panel .ag-button:focus, +.ag-popup .ag-filter-apply-panel .ag-standard-button:focus, +.ag-theme-alpine .ag-filter-apply-panel button:hover, +.ag-theme-alpine .ag-filter-apply-panel .ag-button:hover, +.ag-theme-alpine .ag-filter-apply-panel .ag-standard-button:hover, +.ag-theme-alpine .ag-filter-apply-panel button:focus, +.ag-theme-alpine .ag-filter-apply-panel .ag-button:focus, +.ag-theme-alpine .ag-filter-apply-panel .ag-standard-button:focus { + background: linear-gradient(180deg, #f2faf7 0%, #e7f2ed 100%) !important; + border-color: #bfd5cd !important; + color: #28484b !important; + box-shadow: 0 1px 1px rgba(53, 81, 84, 0.05) !important; +} + +.ag-filter-apply-panel button:active, +.ag-filter-apply-panel .ag-button:active, +.ag-filter-apply-panel .ag-standard-button:active, +.ag-popup .ag-filter-apply-panel button:active, +.ag-popup .ag-filter-apply-panel .ag-button:active, +.ag-popup .ag-filter-apply-panel .ag-standard-button:active, +.ag-theme-alpine .ag-filter-apply-panel button:active, +.ag-theme-alpine .ag-filter-apply-panel .ag-button:active, +.ag-theme-alpine .ag-filter-apply-panel .ag-standard-button:active { + transform: translateY(1px); + box-shadow: 0 2px 8px rgba(53, 81, 84, 0.12) !important; +} + .ag-grid-tree-columns-label, .ag-grid-tree-grouping-label, .ag-grid-tree-layouts-label, @@ -11188,6 +11260,133 @@ html[theme="default"] .screen-container .nav-tabs > li.active > a:hover { color: #0f6f6d; } +.ag-grid-active-filter-chips { + display: inline-flex; + align-items: center; + gap: 4px; + min-width: 0; + max-width: min(520px, 38vw); + min-height: 32px; + overflow: hidden; +} + +.ag-grid-active-filter-chips.drag-active { + min-width: 180px; + padding: 2px 6px; + border: 1px dashed #8fc3bd; + border-radius: 8px; + background: #f6fbf9; +} + +.ag-grid-active-filter-chips.drag-over { + border-color: #0f817d; + background: #eef8f6; +} + +.ag-grid-active-filter-drop-hint { + color: #0f6f6d; + font-size: 11px; + font-weight: 700; + white-space: nowrap; +} + +.ag-grid-filter-draggable-cell { + cursor: grab; +} + +.ag-grid-filter-draggable-cell:active { + cursor: grabbing; +} + +.ag-grid-active-filter-chip { + display: grid; + grid-template-columns: minmax(0, 1fr) 14px; + grid-template-rows: auto auto; + column-gap: 6px; + row-gap: 1px; + align-items: center; + max-width: 190px; + min-height: 32px; + padding: 3px 5px 3px 8px; + border: 1px solid #c9ded8; + border-radius: 7px; + background: #f8fbfa; + color: #285256; + font-size: 10.5px; + font-weight: 600; + line-height: 1.2; + box-shadow: none; +} + +.ag-grid-active-filter-chip:hover, +.ag-grid-active-filter-chip:focus { + border-color: #92bfb8; + background: #eff8f5; + color: #0f6f6d; +} + +.ag-grid-active-filter-chip-name, +.ag-grid-active-filter-chip-detail { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ag-grid-active-filter-chip-name { + grid-column: 1; + grid-row: 1; + color: #637b87; + font-size: 10px; + font-weight: 700; +} + +.ag-grid-active-filter-chip-value { + display: inline-flex; + grid-column: 1; + grid-row: 2; + min-width: 0; + gap: 4px; + color: #0f4f60; +} + +.ag-grid-active-filter-chip-operator { + flex: 0 0 auto; + color: #0f817d; + font-weight: 800; +} + +.ag-grid-active-filter-chip-detail { + font-weight: 700; +} + +.ag-grid-active-filter-chip-remove { + display: inline-flex; + grid-column: 2; + grid-row: 1 / span 2; + align-items: center; + justify-content: center; + width: 14px; + height: 14px; + border-radius: 50%; + color: #6e8588; + font-size: 11px; + line-height: 1; +} + +.ag-grid-active-filter-chip-remove:hover { + background: #dcebe7; + color: #0f6f6d; +} + +.ag-grid-active-filter-chip-more { + display: inline-flex; + max-width: none; + min-height: 27px; + padding: 2px 7px; + border-radius: 999px; +} + .ag-grid-view-transfer-button { font-size: 11.5px; font-weight: 600; @@ -12883,7 +13082,10 @@ 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 { +html[data-theme-mode="dark"] .ag-grid-filter-logic-panel .btn, +html[data-theme-mode="dark"] .ag-theme-alpine .ag-filter-apply-panel button, +html[data-theme-mode="dark"] .ag-theme-alpine .ag-filter-apply-panel .ag-button, +html[data-theme-mode="dark"] .ag-theme-alpine .ag-filter-apply-panel .ag-standard-button { background: linear-gradient(180deg, #213130 0%, #1a2726 100%); border-color: #35504d; color: #dce7e2; @@ -12899,7 +13101,10 @@ 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 { +html[data-theme-mode="dark"] .ag-grid-filter-logic-panel .btn:hover, +html[data-theme-mode="dark"] .ag-theme-alpine .ag-filter-apply-panel button:hover, +html[data-theme-mode="dark"] .ag-theme-alpine .ag-filter-apply-panel .ag-button:hover, +html[data-theme-mode="dark"] .ag-theme-alpine .ag-filter-apply-panel .ag-standard-button:hover { background: linear-gradient(180deg, #27403d 0%, #203230 100%); border-color: #4f716c; color: #f0faf6; diff --git a/tryton-sao.js b/tryton-sao.js index 02bb1c1..67c9131 100644 --- a/tryton-sao.js +++ b/tryton-sao.js @@ -15008,7 +15008,8 @@ function eval_pyson(value){ }, _parse_form: function(node, attributes) { var container = new Sao.View.Form.Container( - Number(node.getAttribute('col') || 4)); + Number(node.getAttribute('col') || 4), + attributes.col_widths); this.view.containers.push(container); this.parse_child(node, container); if (this._containers.length > 0) { @@ -15205,7 +15206,8 @@ function eval_pyson(value){ return; } var container = new Sao.View.Form.Container( - Number(node.getAttribute('col') || 4)); + Number(node.getAttribute('col') || 4), + attributes.col_widths); this.view.containers.push(container); this.parse_child(node, container); var page = new Sao.View.Form.Page( @@ -15216,7 +15218,8 @@ function eval_pyson(value){ }, _parse_group: function(node, attributes) { var group = new Sao.View.Form.Container( - Number(node.getAttribute('col') || 4)); + Number(node.getAttribute('col') || 4), + attributes.col_widths); this.view.containers.push(group); this.parse_child(node, group); @@ -15259,7 +15262,8 @@ function eval_pyson(value){ _parse_child: function(node, attributes) { var paned = this.container; var container = new Sao.View.Form.Container( - Number(node.getAttribute('col') || 4)); + Number(node.getAttribute('col') || 4), + attributes.col_widths); this.view.containers.push(container); this.parse_child(node, container); @@ -15528,9 +15532,10 @@ function eval_pyson(value){ }); Sao.View.Form.Container = Sao.class_(Object, { - init: function(col=4) { + init: function(col=4, col_widths=null) { if (col < 0) col = 0; this.col = col; + this.col_widths = this._parse_col_widths(col_widths); this.el = jQuery('', { 'class': 'form-container' }); @@ -15547,6 +15552,21 @@ function eval_pyson(value){ this._grid_cols = []; this._grid_rows = []; }, + _parse_col_widths: function(col_widths) { + if (!col_widths) { + return []; + } + return String(col_widths).split(',').map(width => { + width = String(width || '').trim(); + if (!width) { + return null; + } + if (/^\d+(\.\d+)?$/.test(width)) { + return width + 'px'; + } + return width; + }); + }, add_row: function() { this._col = 1; this._row += 1; @@ -15659,7 +15679,15 @@ function eval_pyson(value){ var i; var col = this.col <= 0 ? this._col : this.col; - if (this._xexpand.size) { + if (this.col_widths.length) { + for (i = 1; i <= col; i++) { + this._grid_cols.push( + this.col_widths[i - 1] || + (this._xexpand.has(i) ? + `minmax(min-content, ${col}fr)` : + 'min-content')); + } + } else if (this._xexpand.size) { for (i = 1; i <= col; i++) { if (this._xexpand.has(i)) { this._grid_cols.push(`minmax(min-content, ${col}fr)`); @@ -22691,6 +22719,29 @@ function eval_pyson(value){ this._toggle_filter_logic_menu(); }) .appendTo(this.toolbar); + this.active_filter_chips = jQuery('', { + 'class': 'ag-grid-active-filter-chips', + 'aria-live': 'polite', + }).hide().appendTo(this.toolbar); + this.active_filter_chips.on('dragover', evt => { + if (!this._filter_drag_payload) { + return; + } + evt.preventDefault(); + evt.originalEvent.dataTransfer.dropEffect = 'copy'; + this.active_filter_chips.addClass('drag-over'); + }); + this.active_filter_chips.on('dragleave', evt => { + if (evt.currentTarget.contains(evt.relatedTarget)) { + return; + } + this.active_filter_chips.removeClass('drag-over'); + }); + this.active_filter_chips.on('drop', evt => { + evt.preventDefault(); + this.active_filter_chips.removeClass('drag-over'); + this._apply_filter_drop(evt.originalEvent); + }); this.import_layout_input = jQuery('', { 'type': 'file', 'accept': 'application/json,.json', @@ -22774,17 +22825,8 @@ function eval_pyson(value){ }); Sao.View.AGGridTree._super.init.call(this, view_id, screen, xml); - 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 = this._load_persisted_state(); + if (this._persisted_state && this._persisted_state.valueFilters) { this._apply_sao_value_filter_state( this._persisted_state.valueFilters); @@ -22792,6 +22834,7 @@ function eval_pyson(value){ this._apply_saved_sort_to_screen_order(); this._update_layout_button(); this._update_sao_value_filter_mode_button(); + this._render_active_filter_chips(); this._update_copy_state(); this._init_grid(); this._install_resize_observer(); @@ -22921,7 +22964,33 @@ function eval_pyson(value){ }); }, _get_layout_store_cache_key: function() { - return this._get_storage_key(); + return this._get_layout_storage_key(); + }, + _get_layout_storage_key: function() { + var key = this._get_storage_key(); + if (!this.is_embedded_one2many) { + return key; + } + var attributes = this.screen && this.screen.attributes || {}; + var context_parts = [ + 'embedded_one2many', + attributes.name, + attributes.field, + attributes.relation_field, + attributes.one2many, + attributes.parent_name, + attributes.child_name, + this.screen && this.screen.exclude_field, + this.screen && this.screen.group && + this.screen.group.parent_name, + this.screen && this.screen.group && + this.screen.group.child_name, + ].map(function(value) { + return String(value || '').replace(/[:\s]+/g, '_'); + }).filter(function(value) { + return Boolean(value); + }); + return [key].concat(context_parts).join(':'); }, _new_layout_id: function() { return 'layout_' + String(Date.now()) + '_' + @@ -22973,7 +23042,8 @@ function eval_pyson(value){ return this._normalize_layout_store(null); } try { - var raw_state = localStorage.getItem(this._get_storage_key()); + var raw_state = localStorage.getItem( + this._get_layout_storage_key()); if (!raw_state) { return this._normalize_layout_store(null); } @@ -23018,7 +23088,7 @@ function eval_pyson(value){ } try { localStorage.setItem( - this._get_storage_key(), + this._get_layout_storage_key(), JSON.stringify(store)); } catch (error) { Sao.Logger.warn( @@ -23142,9 +23212,6 @@ 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( @@ -23344,6 +23411,7 @@ function eval_pyson(value){ this._sao_value_filter_groups = state.groups ? jQuery.extend(true, {}, state.groups) : null; this._update_sao_value_filter_mode_button(); + this._render_active_filter_chips(); }, _apply_saved_sort_to_screen_order: function() { if (!this._persisted_state || !this._persisted_state.columns) { @@ -24827,6 +24895,7 @@ function eval_pyson(value){ this._schedule_state_save(); this._sync_selection_header_checkbox(); this._update_summary_bar(); + this._render_active_filter_chips(); }, onFirstDataRendered: () => { window.requestAnimationFrame(() => this._update_summary_bar()); @@ -25003,6 +25072,10 @@ function eval_pyson(value){ } return ''; } + if (this._is_date_filter_column(column)) { + return this._get_column_date_filter_value( + column, params.data.__record); + } return this._get_column_text_value( column, params.data.__record); }, @@ -25023,11 +25096,16 @@ function eval_pyson(value){ if (!params.data.__record) { return ''; } + var rendered; if (badge_column) { - return this._render_badge_cell( + rendered = this._render_badge_cell( + column, params.data.__record); + } else { + rendered = this._render_cell( column, params.data.__record); } - return this._render_cell(column, params.data.__record); + return this._make_filter_draggable_cell( + rendered, column, params.data.__record); }, tooltipValueGetter: params => { if (!params.data || !params.data.__record || @@ -25230,9 +25308,6 @@ function eval_pyson(value){ case 'numeric': return 'agNumberColumnFilter'; case 'date': - if (this._get_distinct_filter_values(column)) { - return Sao.View.AGGridValueSetFilter; - } return 'agDateColumnFilter'; default: if (this._get_distinct_filter_values(column)) { @@ -25242,6 +25317,9 @@ function eval_pyson(value){ } }, _get_column_filter_params: function(column) { + if (this._is_date_filter_column(column)) { + return this._get_date_column_filter_params(column); + } var values = this._get_distinct_filter_values(column); if (!values) { return null; @@ -25260,6 +25338,135 @@ function eval_pyson(value){ searchPlaceholder: Sao.i18n.gettext('Search'), }; }, + _is_date_filter_column: function(column) { + return Boolean(column && column.attributes && + column.attributes.widget == 'date'); + }, + _get_date_column_filter_params: function(column) { + var relative_options = this._get_relative_date_filter_options(); + return { + browserDatePicker: true, + buttons: ['reset', 'apply'], + closeOnApply: true, + maxNumConditions: 1, + defaultOption: 'empty', + filterOptions: ['empty'].concat(relative_options).concat([ + 'inRange', + 'equals', + 'lessThan', + 'greaterThan', + 'blank', + 'notBlank', + ]), + inRangeInclusive: true, + comparator: function(filter_date, cell_value) { + if (!cell_value) { + return 0; + } + var cell_date = cell_value instanceof Date ? cell_value : + new Date(cell_value); + if (isNaN(cell_date.getTime())) { + return 0; + } + var cell_midnight = new Date( + cell_date.getFullYear(), + cell_date.getMonth(), + cell_date.getDate()); + if (cell_midnight < filter_date) { + return -1; + } + if (cell_midnight > filter_date) { + return 1; + } + return 0; + }, + }; + }, + _get_relative_date_filter_options: function() { + var make_option = (key, label, range_getter) => { + return { + displayKey: 'sao' + key, + displayName: Sao.i18n.gettext(label), + numberOfInputs: 0, + predicate: (filter_values, cell_value) => { + var cell_date = this._normalize_date_filter_value(cell_value); + if (!cell_date) { + return false; + } + var range = range_getter(); + return cell_date >= range.start && cell_date < range.end; + }, + }; + }; + return [ + make_option('Today', 'Today', () => { + var start = this._start_of_today(); + return { + start: start, + end: this._add_days(start, 1), + }; + }), + make_option('Last7Days', 'Last 7 Days', () => { + var today = this._start_of_today(); + return { + start: this._add_days(today, -7), + end: this._add_days(today, 1), + }; + }), + make_option('Last30Days', 'Last 30 Days', () => { + var today = this._start_of_today(); + return { + start: this._add_days(today, -30), + end: this._add_days(today, 1), + }; + }), + make_option('LastMonth', 'Last Month', () => { + var today = this._start_of_today(); + var start = new Date( + today.getFullYear(), today.getMonth() - 1, 1); + var end = new Date(today.getFullYear(), today.getMonth(), 1); + return { + start: start, + end: end, + }; + }), + make_option('ThisMonth', 'This Month', () => { + var today = this._start_of_today(); + var start = new Date(today.getFullYear(), today.getMonth(), 1); + var end = new Date( + today.getFullYear(), today.getMonth() + 1, 1); + return { + start: start, + end: end, + }; + }), + make_option('YearToDate', 'Year to Date', () => { + var today = this._start_of_today(); + return { + start: new Date(today.getFullYear(), 0, 1), + end: this._add_days(today, 1), + }; + }), + ]; + }, + _start_of_today: function() { + var now = new Date(); + return new Date(now.getFullYear(), now.getMonth(), now.getDate()); + }, + _add_days: function(date, days) { + return new Date( + date.getFullYear(), date.getMonth(), date.getDate() + days); + }, + _normalize_date_filter_value: function(value) { + if (!value) { + return null; + } + var date = value instanceof Date ? value : new Date(value); + if (isNaN(date.getTime())) { + return null; + } + return new Date(date.getFullYear(), date.getMonth(), date.getDate()); + }, _get_sao_value_filter_state: function(col_id) { var filter = this._sao_value_filters && this._sao_value_filters[col_id]; @@ -25285,7 +25492,113 @@ function eval_pyson(value){ }, _is_sao_value_filter_user_source: function(source) { return ~['checkbox-change', 'all-button', 'none-button', - 'clear-button'].indexOf(source); + 'clear-button', 'drag-drop'].indexOf(source); + }, + _make_filter_draggable_cell: function(element, column, record) { + if (!element || !column || !column.attributes || + !column.attributes.name || !record) { + return element; + } + var value = this._get_column_text_value(column, record); + if (!value) { + return element; + } + var wrapped = jQuery(element); + wrapped.attr('draggable', 'true') + .addClass('ag-grid-filter-draggable-cell') + .on('dragstart', evt => { + var payload = { + colId: column.attributes.name, + label: this._get_column_label(column.attributes.name), + value: String(value), + }; + this._filter_drag_payload = payload; + evt.originalEvent.dataTransfer.effectAllowed = 'copy'; + evt.originalEvent.dataTransfer.setData( + 'application/x-sao-ag-grid-filter', + JSON.stringify(payload)); + evt.originalEvent.dataTransfer.setData( + 'text/plain', + payload.label + ' = ' + payload.value); + this._set_filter_dropzone_active(true, payload); + }) + .on('dragend', () => { + this._filter_drag_payload = null; + this._set_filter_dropzone_active(false); + }); + return wrapped[0]; + }, + _set_filter_dropzone_active: function(active, payload) { + if (!this.active_filter_chips) { + return; + } + this.active_filter_chips + .toggleClass('drag-active', Boolean(active)) + .removeClass('drag-over'); + if (active && !this._get_active_filter_chip_entries().length) { + this.active_filter_chips.empty(); + jQuery('', { + 'class': 'ag-grid-active-filter-drop-hint', + 'text': payload ? + Sao.i18n.gettext('Drop to filter %1') + .replace('%1', payload.label) : + Sao.i18n.gettext('Drop to filter'), + }).appendTo(this.active_filter_chips); + this.active_filter_chips.css('display', 'inline-flex'); + } else if (!active) { + this._render_active_filter_chips(); + } + }, + _get_filter_drop_payload: function(event) { + var payload = this._filter_drag_payload; + if (!payload && event && event.dataTransfer) { + try { + payload = JSON.parse(event.dataTransfer.getData( + 'application/x-sao-ag-grid-filter') || 'null'); + } catch (error) { + payload = null; + } + } + if (!payload || !payload.colId || !payload.value) { + return null; + } + return payload; + }, + _apply_filter_drop: function(event) { + var payload = this._get_filter_drop_payload(event); + this._filter_drag_payload = null; + if (!payload) { + this._set_filter_dropzone_active(false); + return; + } + var column = this._find_column_by_id(payload.colId); + if (!column) { + this._set_filter_dropzone_active(false); + return; + } + var empty = Sao.i18n.gettext('(Empty)'); + var value = String(payload.value || '') || empty; + var all_values = this._get_live_filter_values( + column, payload.colId).map(item => String(item || empty)); + if (!~all_values.indexOf(value)) { + all_values.push(value); + } + var existing = this._sao_value_filters && + this._sao_value_filters[payload.colId]; + var selected = existing && existing.selected ? + Array.from(existing.selected).map(item => String(item)) : []; + if (!selected.length) { + selected = [value]; + } else if (!~selected.indexOf(value)) { + selected.push(value); + } + this._set_sao_value_filter( + payload.colId, + selected, + all_values, + 'drag-drop', + {value: value}); + this._set_filter_dropzone_active(false); }, _forget_sao_value_filter_saved_state: function(col_id) { var remove_filter = value_filters => { @@ -25358,6 +25671,7 @@ function eval_pyson(value){ } this._apply_sao_value_filters(); this._render_filter_logic_panel(); + this._render_active_filter_chips(); this._schedule_state_save(); }, _trace_sao_value_filter_change: function(reason, col_id, selected, all, @@ -25599,6 +25913,208 @@ function eval_pyson(value){ .find('.ag-grid-value-filter-mode-label') .text(' ' + Sao.i18n.gettext(any ? 'Any filter' : 'All filters')); }, + _get_column_label: function(col_id) { + var column = this._find_column_by_id(col_id); + return column && column.attributes ? + column.attributes.string || column.attributes.name || col_id : + col_id; + }, + _describe_grid_filter_model: function(model) { + if (!model) { + return { + operator: '', + value: '', + }; + } + var type = model.type || model.operator || ''; + var value = model.filter; + if (value === undefined || value === null || value === '') { + value = model.dateFrom || model.filterTo || ''; + } + if (Array.isArray(model.values)) { + return { + operator: '=', + value: model.values.length <= 2 ? + model.values.join(', ') : + Sao.i18n.gettext('%1 values').replace('%1', model.values.length), + }; + } + if (model.condition1 || model.condition2) { + return { + operator: '', + value: Sao.i18n.gettext('complex'), + }; + } + var relative_date_labels = { + saoToday: 'Today', + saoLast7Days: 'Last 7 Days', + saoLast30Days: 'Last 30 Days', + saoLastMonth: 'Last Month', + saoThisMonth: 'This Month', + saoYearToDate: 'Year to Date', + }; + if (relative_date_labels[type]) { + return { + operator: '', + value: Sao.i18n.gettext(relative_date_labels[type]), + }; + } + var operators = { + equals: '=', + notEqual: '!=', + contains: 'contains', + notContains: 'not contains', + startsWith: 'starts', + endsWith: 'ends', + lessThan: '<', + lessThanOrEqual: '<=', + greaterThan: '>', + greaterThanOrEqual: '>=', + inRange: 'between', + }; + return { + operator: operators[type] || type || '=', + value: value, + }; + }, + _get_active_filter_chip_entries: function() { + var entries = []; + Object.keys(this._sao_value_filters || {}).forEach(col_id => { + var filter = this._sao_value_filters[col_id]; + if (!this._is_sao_value_filter_active(filter)) { + return; + } + var selected = Array.from(filter.selected || []).sort(); + var all = Array.from(filter.all || []).sort(); + var selected_lookup = new Set(selected); + var excluded = all.filter(value => !selected_lookup.has(value)); + var display_values = excluded.length && + excluded.length < selected.length ? excluded : selected; + var operator = display_values === excluded ? '<>' : '='; + var detail = display_values.length <= 2 ? + display_values.join(', ') : + Sao.i18n.gettext('%1 values').replace( + '%1', display_values.length); + entries.push({ + type: 'sao', + colId: col_id, + label: this._get_column_label(col_id), + operator: operator, + detail: detail, + }); + }); + var filter_model = this.gridApi && this.gridApi.getFilterModel ? + this._strip_sao_value_set_filter_model( + this.gridApi.getFilterModel()) : {}; + Object.keys(filter_model || {}).forEach(col_id => { + var description = + this._describe_grid_filter_model(filter_model[col_id]); + entries.push({ + type: 'grid', + colId: col_id, + label: this._get_column_label(col_id), + operator: description.operator, + detail: description.value, + }); + }); + return entries; + }, + _open_filter_for_chip: function(entry) { + if (entry.type == 'sao') { + this._show_filter_logic_menu(); + return; + } + if (!this.gridApi) { + return; + } + if (this.gridApi.showColumnFilter) { + this.gridApi.showColumnFilter(entry.colId); + } else if (this.gridApi.showColumnMenu) { + this.gridApi.showColumnMenu(entry.colId); + } + }, + _remove_filter_chip: function(entry) { + if (entry.type == 'sao') { + delete this._sao_value_filters[entry.colId]; + this._forget_sao_value_filter_saved_state(entry.colId); + this._apply_sao_value_filters(); + this._render_filter_logic_panel(); + this._schedule_state_save(); + this._render_active_filter_chips(); + return; + } + if (!this.gridApi || !this.gridApi.getFilterModel || + !this.gridApi.setFilterModel) { + return; + } + var model = jQuery.extend(true, {}, this.gridApi.getFilterModel() || {}); + delete model[entry.colId]; + this.gridApi.setFilterModel(model); + if (this.gridApi.onFilterChanged) { + this.gridApi.onFilterChanged(); + } + }, + _render_active_filter_chips: function() { + if (!this.active_filter_chips) { + return; + } + var entries = this._get_active_filter_chip_entries(); + this.active_filter_chips.empty(); + if (!entries.length) { + this.active_filter_chips.hide(); + return; + } + var visible = entries.slice(0, 4); + visible.forEach(entry => { + var text = entry.label + (entry.operator ? ' ' + + entry.operator : '') + (entry.detail ? ' ' + + entry.detail : ''); + var chip = jQuery('', { + 'class': 'ag-grid-active-filter-chip', + 'type': 'button', + 'title': text, + }).click(evt => { + evt.preventDefault(); + this._open_filter_for_chip(entry); + }).appendTo(this.active_filter_chips); + jQuery('', { + 'class': 'ag-grid-active-filter-chip-name', + 'text': entry.label, + }).appendTo(chip); + var value = jQuery('', { + 'class': 'ag-grid-active-filter-chip-value', + }).appendTo(chip); + jQuery('', { + 'class': 'ag-grid-active-filter-chip-operator', + 'text': entry.operator || '=', + }).appendTo(value); + jQuery('', { + 'class': 'ag-grid-active-filter-chip-detail', + 'text': entry.detail || '', + }).appendTo(value); + jQuery('', { + 'class': 'ag-grid-active-filter-chip-remove', + 'text': 'x', + 'title': Sao.i18n.gettext('Remove filter'), + }).click(evt => { + evt.preventDefault(); + evt.stopPropagation(); + this._remove_filter_chip(entry); + }).appendTo(chip); + }); + if (entries.length > visible.length) { + jQuery('', { + 'class': 'ag-grid-active-filter-chip ag-grid-active-filter-chip-more', + 'type': 'button', + 'text': '+' + String(entries.length - visible.length), + 'title': Sao.i18n.gettext('Show filter logic'), + }).click(evt => { + evt.preventDefault(); + this._show_filter_logic_menu(); + }).appendTo(this.active_filter_chips); + } + this.active_filter_chips.css('display', 'inline-flex'); + }, _get_active_sao_value_filter_entries: function() { return Object.keys(this._sao_value_filters || {}) .filter(col_id => this._is_sao_value_filter_active( @@ -26229,6 +26745,47 @@ function eval_pyson(value){ return ''; } }, + _get_column_date_filter_value: function(column, record) { + if (!column || !record || !column.attributes.name) { + return null; + } + var name = column.attributes.name; + var value = null; + try { + var field = record.model && record.model.fields && + record.model.fields[name]; + if (field && field.get_client) { + value = field.get_client(record); + } + } catch (error) { + value = null; + } + if (!value && record._values && + Object.prototype.hasOwnProperty.call(record._values, name)) { + value = record._values[name]; + } + if (!value) { + return null; + } + if (value instanceof Date) { + return new Date( + value.getFullYear(), value.getMonth(), value.getDate()); + } + if (value.isDate || value.isDateTime) { + return new Date(value.year(), value.month(), value.date()); + } + if (typeof value == 'string') { + var date = moment(value, [ + 'YYYY-MM-DD', + Sao.common.date_format( + this.screen && this.screen.context && + this.screen.context.date_format), + ], true); + return date.isValid() ? + new Date(date.year(), date.month(), date.date()) : null; + } + return null; + }, _is_badge_column: function(column) { if (!column || !column.attributes) { return false; @@ -35527,7 +36084,8 @@ function eval_pyson(value){ Sao.View.BoardXMLViewParser = Sao.class_(Sao.View.FormXMLViewParser, { _parse_board: function(node, attributes) { var container = new Sao.View.Form.Container( - Number(node.getAttribute('col') || 4)); + Number(node.getAttribute('col') || 4), + attributes.col_widths); this.view.el.append(container.el); this.parse_child(node, container); if (this._containers.length > 0) {