diff --git a/eclipse-scout-chart/src/table/controls/ChartTableControl.ts b/eclipse-scout-chart/src/table/controls/ChartTableControl.ts index 7a624d8f9ec..ec0f22eb0cf 100644 --- a/eclipse-scout-chart/src/table/controls/ChartTableControl.ts +++ b/eclipse-scout-chart/src/table/controls/ChartTableControl.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2025 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -417,7 +417,7 @@ export class ChartTableControl extends TableControl implements ChartTableControl } protected _columns(): Column[] { - return new TableMatrix(this.table, this.session).columns(); + return new TableMatrix(this.table).columns(); } protected _axisCount(columnCount: (number | Column)[][], column: Column): number { @@ -517,7 +517,7 @@ export class ChartTableControl extends TableControl implements ChartTableControl this._chartGroup2Map = {}; // find best x- and y-axis: best is 9 different entries - let matrix = new TableMatrix(this.table, this.session), + let matrix = new TableMatrix(this.table), columnCount = matrix.columnCount(false); // filterNumberColumns false: number columns will be filtered below columnCount.sort((a, b) => { return Math.abs(a[1] as number - 8) - Math.abs(b[1] as number - 8); @@ -799,7 +799,7 @@ export class ChartTableControl extends TableControl implements ChartTableControl protected _calculateValues(): TableMatrixResult { // build matrix - let matrix = new TableMatrix(this.table, this.session); + let matrix = new TableMatrix(this.table); // aggregation (data axis) let tableData = this.chartAggregation.id ? this._aggregationMap[this.chartAggregation.id].data('column') : -1; diff --git a/eclipse-scout-chart/src/table/controls/ChartTableUserFilter.ts b/eclipse-scout-chart/src/table/controls/ChartTableUserFilter.ts index f25ea2f0407..6998a3a9e05 100644 --- a/eclipse-scout-chart/src/table/controls/ChartTableUserFilter.ts +++ b/eclipse-scout-chart/src/table/controls/ChartTableUserFilter.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -39,7 +39,7 @@ export class ChartTableUserFilter extends TableUserFilter implements ChartTableU } override createFilterAddedEventData(): ChartTableUserFilterAddedEventData { - let data: ChartTableUserFilterAddedEventData = super.createFilterAddedEventData(); + let data = super.createFilterAddedEventData() as ChartTableUserFilterAddedEventData; data.text = this.text; data.filters = this.filters; data.columnIdX = (this.xAxis && this.xAxis.column) ? this.xAxis.column.id : null; @@ -48,7 +48,7 @@ export class ChartTableUserFilter extends TableUserFilter implements ChartTableU } calculate() { - let matrix = new TableMatrix(this.table, this.session); + let matrix = new TableMatrix(this.table); let columnX = this.table.columnById(this.columnIdX); let axisGroupX = columnX.createFilter().axisGroup(); this.xAxis = matrix.addAxis(columnX, axisGroupX); @@ -77,8 +77,8 @@ export class ChartTableUserFilter extends TableUserFilter implements ChartTableU } } -export type ChartTableUserFilterAddedEventData = TableUserFilterAddedEventData & { +export interface ChartTableUserFilterAddedEventData extends TableUserFilterAddedEventData { filters?: { deterministicKey: TableControlDeterministicKey }[]; columnIdX?: string; columnIdY?: string; -}; +} diff --git a/eclipse-scout-core/src/data-objects.ts b/eclipse-scout-core/src/data-objects.ts index 90852b42313..8633971f23e 100644 --- a/eclipse-scout-core/src/data-objects.ts +++ b/eclipse-scout-core/src/data-objects.ts @@ -141,6 +141,7 @@ export class TableColumnClientUiPreferenceDo extends BaseDoEntity { groupingActive: boolean; aggregationFunctionId: string; backgroundEffectId: string; + dateGroupType: DateGroupType; } export interface IUserFilterStateDo extends BaseDoEntity { @@ -162,10 +163,23 @@ export class ColumnUserFilterStateDo extends BaseDoEntity implements IUserFilter export class DateColumnUserFilterStateDo extends BaseDoEntity implements IUserFilterStateDo { columnId: string; selectedValues: Set; + groupType: DateGroupType; dateFrom: Date; dateTo: Date; } +/** + * @see "DateGroupType.java" + */ +export enum DateGroupType { + YEAR = 'year', + MONTH = 'month', + MONTH_AND_YEAR = 'month-and-year', + CALENDAR_WEEK = 'calendar-week', + WEEKDAY = 'weekday', + DATE = 'date' +} + @typeName('scout.NumberColumnUserFilterState') export class NumberColumnUserFilterStateDo extends BaseDoEntity implements IUserFilterStateDo { columnId: string; diff --git a/eclipse-scout-core/src/index.less b/eclipse-scout-core/src/index.less index b98249669f0..0177e641701 100644 --- a/eclipse-scout-core/src/index.less +++ b/eclipse-scout-core/src/index.less @@ -136,6 +136,7 @@ @import "table/controls/TableControl"; @import "table/editor/CellEditorPopup"; @import "table/columns/CompactColumn"; +@import "table/columns/DateColumnTableHeaderMenu"; @import "table/columns/LookupEditor"; @import "table/organizer/ShowInvisibleColumnsForm"; @import "table/organizer/TableOrganizerMenu"; diff --git a/eclipse-scout-core/src/index.ts b/eclipse-scout-core/src/index.ts index 1217f2f3844..788aef6aa35 100644 --- a/eclipse-scout-core/src/index.ts +++ b/eclipse-scout-core/src/index.ts @@ -586,6 +586,7 @@ export * from './table/columns/CompactLineBlock'; export * from './table/columns/DateColumn'; export * from './table/columns/DateColumnModel'; export * from './table/columns/DateColumnEventMap'; +export * from './table/columns/DateColumnTableHeaderMenu'; export * from './table/columns/IconColumn'; export * from './table/columns/NumberColumn'; export * from './table/columns/NumberColumnModel'; diff --git a/eclipse-scout-core/src/menu/ContextMenuPopup.ts b/eclipse-scout-core/src/menu/ContextMenuPopup.ts index 136db320e64..150cba8ec21 100644 --- a/eclipse-scout-core/src/menu/ContextMenuPopup.ts +++ b/eclipse-scout-core/src/menu/ContextMenuPopup.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2025 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -43,7 +43,7 @@ export class ContextMenuPopup extends Popup implements ContextMenuPopupModel { options.focusableContainer = true; // In order to allow keyboard navigation, the popup must gain focus. Because menu-items are not focusable, make the container focusable instead. // If menu items are cloned, don't link the original menus with the popup, otherwise they would be removed when the context menu is removed - if (options.cloneMenuItems === false) { + if (scout.nvl(options.cloneMenuItems, this.cloneMenuItems) === false) { this._addWidgetProperties('menuItems'); } diff --git a/eclipse-scout-core/src/prefs/TableUiPreferences.ts b/eclipse-scout-core/src/prefs/TableUiPreferences.ts index 7f788c137c8..be672e2bee2 100644 --- a/eclipse-scout-core/src/prefs/TableUiPreferences.ts +++ b/eclipse-scout-core/src/prefs/TableUiPreferences.ts @@ -8,7 +8,7 @@ * SPDX-License-Identifier: EPL-2.0 */ import { - arrays, Column, ErrorHandler, Event, ITableCustomizerDo, IUserFilterStateDo, NumberColumn, NumberColumnAggregationFunction, NumberColumnBackgroundEffect, objects, ObjectWithType, PropertyChangeEvent, scout, strings, Table, + arrays, Column, DateColumn, ErrorHandler, Event, ITableCustomizerDo, IUserFilterStateDo, NumberColumn, NumberColumnAggregationFunction, NumberColumnBackgroundEffect, objects, ObjectWithType, PropertyChangeEvent, scout, strings, Table, TableClientUiPreferenceProfileDo, TableClientUiPreferencesDo, TableColumnClientUiPreferenceDo, TableUserFilter, UiPreferences, uiPreferences, UserFilterStateMappers } from '../index'; @@ -123,7 +123,16 @@ export class TableUiPreferences implements ObjectWithType { */ protected _installTableListener(table: Table) { this._uninstallTableListener(table); - table.on('columnMoved columnResized columnStructureChanged group sort aggregationFunctionChanged columnBackgroundEffectChanged', this._tableColumnListener); + table.on([ + 'columnMoved', + 'columnResized', + 'columnStructureChanged', + 'group', + 'sort', + 'aggregationFunctionChanged', + 'columnBackgroundEffectChanged', + 'columnDateGroupTypeChanged' + ].join(' '), this._tableColumnListener); table.on('propertyChange:tileMode', this._tableTileModeListener); } @@ -131,7 +140,16 @@ export class TableUiPreferences implements ObjectWithType { * Uninstalls the listener installed by {@link _installTableListener}. */ protected _uninstallTableListener(table: Table) { - table.off('columnMoved columnResized columnStructureChanged group sort aggregationFunctionChanged columnBackgroundEffectChanged', this._tableColumnListener); + table.off([ + 'columnMoved', + 'columnResized', + 'columnStructureChanged', + 'group', + 'sort', + 'aggregationFunctionChanged', + 'columnBackgroundEffectChanged', + 'columnDateGroupTypeChanged' + ].join(' '), this._tableColumnListener); table.off('propertyChange:tileMode', this._tableTileModeListener); } @@ -265,6 +283,11 @@ export class TableUiPreferences implements ObjectWithType { ? column.backgroundEffect : this.isColumnPreferencesColumn(column) ? column.getColumnPreferences()?.backgroundEffectId + : undefined, + dateGroupType: column instanceof DateColumn + ? (column.grouped ? column.groupType : null) // only save group type if grouping is active + : this.isColumnPreferencesColumn(column) + ? column.getColumnPreferences()?.dateGroupType : undefined }); }); @@ -500,24 +523,37 @@ export class TableUiPreferences implements ObjectWithType { } // Use setter for 'visible' property because it is a multidimensional property - column.setVisible(columnPreferences.visible, false); // parameter 'false' skips call of onColumnVisibilityChanged() + if (columnPreferences.visible !== undefined) { + column.setVisible(columnPreferences.visible, false); // parameter 'false' skips call of onColumnVisibilityChanged() + } // Don't use setter for 'width' property to prevent unnecessarily redrawing the table (will be done again later in setColumns anyway) - if (!column.fixedWidth) { + if (columnPreferences.width !== undefined && !column.fixedWidth) { column.width = columnPreferences.width; } // Properties without setter (changes will be applied later by _setColumns) - column.sortIndex = columnPreferences.sortOrder; - column.sortAscending = columnPreferences.sortAscending; - column.sortActive = column.sortIndex >= 0; - column.grouped = columnPreferences.groupingActive; + if (columnPreferences.sortOrder !== undefined) { + column.sortIndex = columnPreferences.sortOrder; + column.sortActive = column.sortIndex >= 0; + } + if (columnPreferences.sortAscending !== undefined) { + column.sortAscending = columnPreferences.sortAscending; + } + if (columnPreferences.groupingActive !== undefined) { + column.grouped = columnPreferences.groupingActive; + } - if (column instanceof NumberColumn) { - // Use setters to correctly update internal structures (e.g. aggrStart function) + // Use setters to correctly update internal structures (e.g. aggrStart function) + if (columnPreferences.aggregationFunctionId !== undefined && column instanceof NumberColumn) { column.setAggregationFunction(columnPreferences.aggregationFunctionId as NumberColumnAggregationFunction); + } + if (columnPreferences.backgroundEffectId !== undefined && column instanceof NumberColumn) { column.setBackgroundEffect(columnPreferences.backgroundEffectId as NumberColumnBackgroundEffect, false); // false = don't redraw } + if (columnPreferences.dateGroupType !== undefined && column instanceof DateColumn) { + column.setGroupType(columnPreferences.dateGroupType, false); // false = don't apply grouping + } if (this.isColumnPreferencesColumn(column)) { column.setColumnPreferences(columnPreferences); diff --git a/eclipse-scout-core/src/table/Table.ts b/eclipse-scout-core/src/table/Table.ts index 9836bf94e42..58097b4a6df 100644 --- a/eclipse-scout-core/src/table/Table.ts +++ b/eclipse-scout-core/src/table/Table.ts @@ -9,14 +9,14 @@ */ import { Action, AggregateTableControl, Alignment, AppLinkKeyStroke, aria, arrays, BooleanColumn, Cell, CellEditorPopup, clipboard, Column, ColumnModel, CompactColumn, Comparator, ContextMenuKeyStroke, ContextMenuPopup, dataObjects, Desktop, - DesktopPopupOpenEvent, Device, DisplayViewId, DoubleClickSupport, dragAndDrop, DragAndDropHandler, DropType, EnumObject, ErrorHandler, EventHandler, events, Filter, Filterable, FilterOrFunction, FilterResult, FilterSupport, FullModelOf, - graphics, GridAriaRules, HtmlComponent, IconColumn, InitModelOf, Insets, IUserFilterStateDo, keys, KeyStrokeContext, LimitedResultTableStatus, LoadingSupport, Menu, MenuBar, MenuDestinations, MenuItemsOrder, menus as menuUtil, menus, - NumberColumn, NumberColumnAggregationFunction, NumberColumnBackgroundEffect, ObjectOrChildModel, ObjectOrModel, objects, Predicate, PropertyChangeEvent, Range, scout, scrollbars, ScrollToAlignment, ScrollToOptions, Status, StatusOrModel, - strings, styles, TabbableCoordinator, TableClientUiPreferenceProfileDo, TableCompactHandler, TableControl, TableCopyKeyStroke, TableCustomizer, TableDefaultRowActionKeyStroke, TableEventMap, TableFooter, TableHeader, TableLayout, - TableModel, TableNavigationCollapseKeyStroke, TableNavigationDownKeyStroke, TableNavigationEndKeyStroke, TableNavigationExpandKeyStroke, TableNavigationHomeKeyStroke, TableNavigationPageDownKeyStroke, TableNavigationPageUpKeyStroke, - TableNavigationUpKeyStroke, TableOrganizer, TableRefreshKeyStroke, TableRow, TableRowModel, TableSelectAllKeyStroke, TableSelectionHandler, TableSelectKeyStroke, TableStartCellEditKeyStroke, TableTextUserFilter, TableTileGridMediator, - TableToggleRowKeyStroke, TableTooltip, TableUiPreferences, tableUiPreferences, TableUpdateBuffer, TableUserFilter, TableUserFilterModel, Tile, TileTableHeaderBox, tooltips, TooltipSupport, TreeGridAriaRules, UiPreferences, - UpdateFilteredElementsOptions, UserFilterStateMappers, ValueField, Widget + DesktopPopupOpenEvent, Device, DisplayViewId, DoubleClickSupport, dragAndDrop, DragAndDropHandler, DropType, EnumObject, ErrorHandler, EventHandler, EventModel, events, Filter, Filterable, FilterOrFunction, FilterResult, FilterSupport, + FullModelOf, graphics, GridAriaRules, HtmlComponent, IconColumn, InitModelOf, Insets, IUserFilterStateDo, keys, KeyStrokeContext, LimitedResultTableStatus, LoadingSupport, Menu, MenuBar, MenuDestinations, MenuItemsOrder, + menus as menuUtil, menus, NumberColumn, NumberColumnAggregationFunction, NumberColumnBackgroundEffect, ObjectOrChildModel, ObjectOrModel, objects, Predicate, PropertyChangeEvent, Range, scout, scrollbars, ScrollToAlignment, + ScrollToOptions, Status, StatusOrModel, strings, styles, TabbableCoordinator, TableClientUiPreferenceProfileDo, TableCompactHandler, TableControl, TableCopyKeyStroke, TableCustomizer, TableDefaultRowActionKeyStroke, TableEventMap, + TableFooter, TableGroupEvent, TableHeader, TableLayout, TableModel, TableNavigationCollapseKeyStroke, TableNavigationDownKeyStroke, TableNavigationEndKeyStroke, TableNavigationExpandKeyStroke, TableNavigationHomeKeyStroke, + TableNavigationPageDownKeyStroke, TableNavigationPageUpKeyStroke, TableNavigationUpKeyStroke, TableOrganizer, TableRefreshKeyStroke, TableRow, TableRowModel, TableSelectAllKeyStroke, TableSelectionHandler, TableSelectKeyStroke, + TableStartCellEditKeyStroke, TableTextUserFilter, TableTileGridMediator, TableToggleRowKeyStroke, TableTooltip, TableUiPreferences, tableUiPreferences, TableUpdateBuffer, TableUserFilter, TableUserFilterModel, Tile, TileTableHeaderBox, + tooltips, TooltipSupport, TreeGridAriaRules, UiPreferences, UpdateFilteredElementsOptions, UserFilterStateMappers, ValueField, Widget } from '../index'; import $ from 'jquery'; @@ -1648,30 +1648,36 @@ export class Table extends Widget implements TableModel, Filterable { } protected _addGroupColumn(column: Column, direction?: 'asc' | 'desc', multiGroup?: boolean) { - let sortIndex = -1; - if (!this.isGroupingPossible(column)) { return; } + if (column.grouped) { + return; // column is already grouped, nothing to do (otherwise, the sort index would be increased unnecessarily) + } direction = scout.nvl(direction, column.sortAscending ? 'asc' : 'desc'); multiGroup = scout.nvl(multiGroup, true); + + // do not update sort index for permanent head/tail sort columns, their order is fixed (see ColumnSet.java) if (!(column.initialAlwaysIncludeSortAtBegin || column.initialAlwaysIncludeSortAtEnd)) { - // do not update sort index for permanent head/tail sort columns, their order is fixed (see ColumnSet.java) + let sortedSiblingColumns = this.columns + .filter(c => c !== column) + .filter(c => c.sortActive) + .filter(c => !(c.initialAlwaysIncludeSortAtBegin || c.initialAlwaysIncludeSortAtEnd)); + if (multiGroup) { - sortIndex = Math.max(-1, arrays.max(this.columns.map(c => c.sortIndex === undefined || c.initialAlwaysIncludeSortAtEnd || !c.grouped ? -1 : c.sortIndex))); + // compute the largest sort index of any grouped column + let sortIndex = Math.max(-1, arrays.max(this.columns.map(c => c.sortIndex === undefined || c.initialAlwaysIncludeSortAtEnd || !c.grouped ? -1 : c.sortIndex))); if (!column.sortActive) { // column was not yet present: insert at determined position // and move all subsequent nodes by one. // add just after all other grouping columns in column set. - column.sortIndex = sortIndex + 1; - arrays.eachSibling(this.columns, column, siblingColumn => { - if (siblingColumn.sortActive && !(siblingColumn.initialAlwaysIncludeSortAtBegin || siblingColumn.initialAlwaysIncludeSortAtEnd) && siblingColumn.sortIndex > sortIndex) { + sortedSiblingColumns.forEach(siblingColumn => { + if (siblingColumn.sortIndex > sortIndex) { siblingColumn.sortIndex++; } }); - // increase sortIndex for all permanent tail columns (a column has been added in front of them) this._permanentTailSortColumns.forEach(c => { c.sortIndex++; @@ -1679,58 +1685,51 @@ export class Table extends Widget implements TableModel, Filterable { } else { // column already sorted, update position: // move all sort columns between the newly determined sort-index and the old sort-index by one. - arrays.eachSibling(this.columns, column, siblingColumn => { - if (siblingColumn.sortActive && !(siblingColumn.initialAlwaysIncludeSortAtBegin || siblingColumn.initialAlwaysIncludeSortAtEnd) && - siblingColumn.sortIndex > sortIndex && - siblingColumn.sortIndex < column.sortIndex) { + sortedSiblingColumns.forEach(siblingColumn => { + if (siblingColumn.sortIndex > sortIndex && siblingColumn.sortIndex < column.sortIndex) { siblingColumn.sortIndex++; } }); - column.sortIndex = sortIndex + 1; } + + column.sortIndex = sortIndex + 1; + } else { // no multi-group: - sortIndex = this._permanentHeadSortColumns.length; + let sortIndex = this._permanentHeadSortColumns.length; if (column.sortActive) { // column already sorted, update position: // move all sort columns between the newly determined sort-index and the old sort-index by one. - arrays.eachSibling(this.columns, column, siblingColumn => { - if (siblingColumn.sortActive && !(siblingColumn.initialAlwaysIncludeSortAtBegin || siblingColumn.initialAlwaysIncludeSortAtEnd) && - siblingColumn.sortIndex >= sortIndex && - siblingColumn.sortIndex < column.sortIndex) { + sortedSiblingColumns.forEach(siblingColumn => { + if (siblingColumn.sortIndex >= sortIndex && siblingColumn.sortIndex < column.sortIndex) { siblingColumn.sortIndex++; } }); - column.sortIndex = sortIndex; - } else { // not sorted yet - arrays.eachSibling(this.columns, column, siblingColumn => { - if (siblingColumn.sortActive && !(siblingColumn.initialAlwaysIncludeSortAtBegin || siblingColumn.initialAlwaysIncludeSortAtEnd) && siblingColumn.sortIndex >= sortIndex) { - siblingColumn.sortIndex++; - } + } else { + // not sorted yet, update position: + // move all existing sort columns by one. + sortedSiblingColumns.forEach(siblingColumn => { + siblingColumn.sortIndex++; }); - - column.sortIndex = sortIndex; - // increase sortIndex for all permanent tail columns (a column has been added in front of them) this._permanentTailSortColumns.forEach(c => { c.sortIndex++; }); } - // remove all other grouped properties: + column.sortIndex = sortIndex; + + // remove all other grouped columns arrays.eachSibling(this.columns, column, siblingColumn => { - if (siblingColumn.sortActive && !(siblingColumn.initialAlwaysIncludeSortAtBegin || siblingColumn.initialAlwaysIncludeSortAtEnd) && siblingColumn.sortIndex >= sortIndex) { - siblingColumn.grouped = false; + if (siblingColumn.grouped) { + this._removeGroupColumn(siblingColumn); } }); } column.sortAscending = direction === 'asc'; column.sortActive = true; - } else if (column.initialAlwaysIncludeSortAtBegin) { - // do not change order or direction. just set grouped to true. - column.grouped = true; } column.grouped = true; @@ -2879,11 +2878,12 @@ export class Table extends Widget implements TableModel, Filterable { * the column to group by. * @param direction * the sorting direction. Either 'asc' or 'desc'. + * Does not have an effect if `remove` is set to true. * If not specified, the direction specified by the column is used ({@link Column.sortAscending}). * @param multiGroup * true to add the column to the list of grouped columns. * False to use this column exclusively as group column (reset other columns). - * Does not have an effect is `remove` is set to true. + * Does not have an effect if `remove` is set to true. * Default is false. * @param remove * true to remove the column from the list of grouped columns. @@ -2901,13 +2901,12 @@ export class Table extends Widget implements TableModel, Filterable { if (!remove) { this._addGroupColumn(column, direction, multiGroup); } - if (this.header) { this.header.onSortingChanged(); } let sorted = this._sort(true); - let data: any = { + let data: EventModel = { column: column, groupAscending: column.sortAscending }; diff --git a/eclipse-scout-core/src/table/TableAdapter.ts b/eclipse-scout-core/src/table/TableAdapter.ts index 4e87726a158..fdc3087bb95 100644 --- a/eclipse-scout-core/src/table/TableAdapter.ts +++ b/eclipse-scout-core/src/table/TableAdapter.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2025 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -8,10 +8,10 @@ * SPDX-License-Identifier: EPL-2.0 */ import { - AdapterData, App, arrays, BooleanColumn, Cell, ChildModelOf, Column, ColumnModel, ColumnUserFilter, defaultValues, Event, Filter, ModelAdapter, NumberColumn, ObjectOrModel, objects, RemoteEvent, RemoteTableOrganizer, scout, Table, - TableAggregationFunctionChangedEvent, TableAppLinkActionEvent, TableCancelCellEditEvent, TableColumnBackgroundEffectChangedEvent, TableColumnMovedEvent, TableColumnResizedEvent, TableCompleteCellEditEvent, TableDropEvent, - TableFilterAddedEvent, TableFilterRemovedEvent, TableGroupEvent, TableModel, TablePrepareCellEditEvent, TableReloadEvent, TableRow, TableRowActionEvent, TableRowClickEvent, TableRowModel, TableRowsCheckedEvent, TableRowsExpandedEvent, - TableRowsSelectedEvent, TableSortEvent, TableUserFilter, ValueField + AdapterData, App, arrays, BooleanColumn, Cell, ChildModelOf, Column, ColumnModel, ColumnUserFilter, DateColumn, defaultValues, Event, Filter, ModelAdapter, NumberColumn, NumberColumnAggregationFunction, ObjectOrModel, objects, + RemoteEvent, RemoteTableOrganizer, scout, Table, TableAggregationFunctionChangedEvent, TableAppLinkActionEvent, TableCancelCellEditEvent, TableColumnBackgroundEffectChangedEvent, TableColumnDateGroupTypeChangedEvent, + TableColumnMovedEvent, TableColumnResizedEvent, TableCompleteCellEditEvent, TableDropEvent, TableFilterAddedEvent, TableFilterRemovedEvent, TableGroupEvent, TableModel, TablePrepareCellEditEvent, TableReloadEvent, TableRow, + TableRowActionEvent, TableRowClickEvent, TableRowModel, TableRowsCheckedEvent, TableRowsExpandedEvent, TableRowsSelectedEvent, TableSortEvent, TableUserFilter, ValueField } from '../index'; import $ from 'jquery'; @@ -150,6 +150,21 @@ export class TableAdapter extends ModelAdapter { this._send('columnBackgroundEffectChanged', data); } + protected _onWidgetColumnDateGroupTypeChanged(event: TableColumnDateGroupTypeChangedEvent) { + this._sendColumnDateGroupTypeChanged(event.column); + } + + protected _sendColumnDateGroupTypeChanged(column: DateColumn) { + if (column.guiOnly) { + return; + } + let data = { + columnId: column.id, + groupType: column.groupType + }; + this._send('columnDateGroupTypeChanged', data); + } + sendColumnOrganizeAction(column: Column, action: 'add' | 'remove' | 'modify') { this._send('columnOrganizeAction', { action: action, @@ -393,6 +408,8 @@ export class TableAdapter extends ModelAdapter { this._onWidgetColumnBackgroundEffectChanged(event as TableColumnBackgroundEffectChangedEvent); } else if (event.type === 'aggregationFunctionChanged') { this._onWidgetAggregationFunctionChanged(event as TableAggregationFunctionChangedEvent); + } else if (event.type === 'columnDateGroupTypeChanged') { + this._onWidgetColumnDateGroupTypeChanged(event as TableColumnDateGroupTypeChangedEvent); } else if (event.type === 'drop' && this.widget.dragAndDropHandler) { this.widget.dragAndDropHandler.uploadFiles(event as TableDropEvent); } else { @@ -517,21 +534,6 @@ export class TableAdapter extends ModelAdapter { this.widget.revealSelection(); } - protected _onColumnBackgroundEffectChanged(event: RemoteEvent) { - event.eventParts.forEach(function(eventPart) { - let column = this.widget.columnById(eventPart.columnId), - backgroundEffect = eventPart.backgroundEffect; - - this.addFilterForWidgetEvent(widgetEvent => { - return (widgetEvent.type === 'columnBackgroundEffectChanged' && - widgetEvent.column.id === column.id && - widgetEvent.column.backgroundEffect === backgroundEffect); - }); - - column.setBackgroundEffect(backgroundEffect); - }, this); - } - protected _onRequestFocusInCell(event: RemoteEvent) { let row = this.widget.rowById(event.rowId), column = this.widget.columnById(event.columnId); @@ -539,24 +541,60 @@ export class TableAdapter extends ModelAdapter { } protected _onAggregationFunctionChanged(event: RemoteEvent) { - let columns = [], - functions = []; + let columns: NumberColumn[] = []; + let aggregationFunctions: NumberColumnAggregationFunction[] = []; - event.eventParts.forEach(function(eventPart) { - let func = eventPart.aggregationFunction, - column = this.widget.columnById(eventPart.columnId); + event.eventParts.forEach(eventPart => { + let column = this.widget.columnById(eventPart.columnId); + if (!(column instanceof NumberColumn)) { + return; + } + let aggregationFunction = eventPart.aggregationFunction; - this.addFilterForWidgetEvent(widgetEvent => { - return (widgetEvent.type === 'aggregationFunctionChanged' && + this.addFilterForWidgetEvent((widgetEvent: TableAggregationFunctionChangedEvent) => { + return widgetEvent.type === 'aggregationFunctionChanged' && widgetEvent.column.id === column.id && - widgetEvent.column.aggregationFunction === func); + widgetEvent.column.aggregationFunction === aggregationFunction; }); - columns.push(column); - functions.push(func); - }, this); + aggregationFunctions.push(aggregationFunction); + }); + + this.widget.changeAggregations(columns, aggregationFunctions); + } + + protected _onColumnBackgroundEffectChanged(event: RemoteEvent) { + event.eventParts.forEach(eventPart => { + let column = this.widget.columnById(eventPart.columnId); + if (!(column instanceof NumberColumn)) { + return; + } + let backgroundEffect = eventPart.backgroundEffect; - this.widget.changeAggregations(columns, functions); + this.addFilterForWidgetEvent((widgetEvent: TableColumnBackgroundEffectChangedEvent) => { + return widgetEvent.type === 'columnBackgroundEffectChanged' && + widgetEvent.column.id === column.id && + widgetEvent.column.backgroundEffect === backgroundEffect; + }); + column.setBackgroundEffect(backgroundEffect); + }); + } + + protected _onColumnDateGroupTypeChanged(event: RemoteEvent) { + event.eventParts.forEach(eventPart => { + let column = this.widget.columnById(eventPart.columnId); + if (!(column instanceof DateColumn)) { + return; + } + let groupType = eventPart.groupType; + + this.addFilterForWidgetEvent((widgetEvent: TableColumnDateGroupTypeChangedEvent) => { + return widgetEvent.type === 'columnDateGroupTypeChanged' + && widgetEvent.column.id === column.id + && widgetEvent.column.groupType === groupType; + }); + column.setGroupType(groupType); + }); } protected _onFiltersChanged(filters: (ObjectOrModel | Filter)[]) { @@ -607,6 +645,8 @@ export class TableAdapter extends ModelAdapter { this._onAggregationFunctionChanged(event); } else if (event.type === 'columnBackgroundEffectChanged') { this._onColumnBackgroundEffectChanged(event); + } else if (event.type === 'columnDateGroupTypeChanged') { + this._onColumnDateGroupTypeChanged(event); } else if (event.type === 'requestFocusInCell') { this._onRequestFocusInCell(event); } else { diff --git a/eclipse-scout-core/src/table/TableEventMap.ts b/eclipse-scout-core/src/table/TableEventMap.ts index bc3eb9c45a4..3cad42d0f8c 100644 --- a/eclipse-scout-core/src/table/TableEventMap.ts +++ b/eclipse-scout-core/src/table/TableEventMap.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2025 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -8,8 +8,8 @@ * SPDX-License-Identifier: EPL-2.0 */ import { - AppLinkActionEvent, Cell, Column, DropType, Event, FileDropEvent, Filter, KeyStroke, Menu, NumberColumn, PropertyChangeEvent, Status, Table, TableCheckableStyle, TableControl, TableGroupingStyle, TableHierarchicalStyle, TableReloadReason, - TableRow, Tile, TileTableHeaderBox, ValueField, WidgetEventMap + AppLinkActionEvent, Cell, Column, DateColumn, DropType, Event, FileDropEvent, Filter, KeyStroke, Menu, NumberColumn, PropertyChangeEvent, Status, Table, TableCheckableStyle, TableControl, TableGroupingStyle, TableHierarchicalStyle, + TableReloadReason, TableRow, Tile, TileTableHeaderBox, ValueField, WidgetEventMap } from '../index'; export interface TableColumnBackgroundEffectChangedEvent extends Event { @@ -20,6 +20,10 @@ export interface TableAggregationFunctionChangedEvent extends Event extends Event { + column: DateColumn; +} + export interface TableAllRowsDeletedEvent extends Event { rows: TableRow[]; } @@ -194,6 +198,7 @@ export interface TableEventMap extends WidgetEventMap { 'startCellEdit': TableStartCellEditEvent; 'statusChanged': Event; 'columnBackgroundEffectChanged': TableColumnBackgroundEffectChangedEvent; + 'columnDateGroupTypeChanged': TableColumnDateGroupTypeChangedEvent; 'propertyChange:autoResizeColumns': PropertyChangeEvent; 'propertyChange:checkable': PropertyChangeEvent; 'propertyChange:checkableStyle': PropertyChangeEvent; diff --git a/eclipse-scout-core/src/table/TableHeaderMenu.less b/eclipse-scout-core/src/table/TableHeaderMenu.less index 72e8c1e9a49..0f0d1a5d2ee 100644 --- a/eclipse-scout-core/src/table/TableHeaderMenu.less +++ b/eclipse-scout-core/src/table/TableHeaderMenu.less @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2025 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -58,12 +58,16 @@ flex-wrap: wrap; & > .table-header-menu-group-text { - flex-grow: 1; + flex: 1; } & > .actions > .action { height: 22px; margin-top: -4px; + + &:not(:has(.text)) { + width: 26px; // use fixed width for icon-only actions to prevent "jumping" when the icon is changed + } } } @@ -302,10 +306,8 @@ vertical-align: middle; } -.table-header-menu-toggle-sort-order { - &::before { - #scout.font-icon(); - } +.table-header-menu-toggle-sort-order::before { + #scout.font-icon(); } .table-header-menu-toggle-sort-order-alphabetically::before { diff --git a/eclipse-scout-core/src/table/TableHeaderMenu.ts b/eclipse-scout-core/src/table/TableHeaderMenu.ts index c1e94c4c1fc..7f15cd7ab88 100644 --- a/eclipse-scout-core/src/table/TableHeaderMenu.ts +++ b/eclipse-scout-core/src/table/TableHeaderMenu.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2025 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -8,9 +8,9 @@ * SPDX-License-Identifier: EPL-2.0 */ import { - AbstractLayout, Action, aria, arrays, Cell, Column, ColumnUserFilter, ColumnUserFilterValues, Device, EnumObject, EventHandler, FilterFieldsGroupBox, graphics, HtmlComponent, InitModelOf, ListBoxAriaRules, NumberColumn, - NumberColumnAggregationFunction, NumberField, Popup, RowLayout, scout, SomeRequired, Table, TableHeader, TableHeaderMenuButton, TableHeaderMenuEventMap, TableHeaderMenuGroup, TableHeaderMenuGroupItem, TableHeaderMenuLayout, - TableHeaderMenuModel, TableRow, TableRowModel, TableRowsCheckedEvent, ValueField + AbstractLayout, Action, aria, arrays, Cell, Column, ColumnUserFilter, ColumnUserFilterValues, Device, EnumObject, Event, EventHandler, FilterFieldsGroupBox, graphics, HtmlComponent, InitModelOf, ListBoxAriaRules, NumberColumn, + NumberColumnAggregationFunction, NumberField, objects, Popup, RowLayout, scout, SomeRequired, Table, TableHeader, TableHeaderMenuButton, TableHeaderMenuEventMap, TableHeaderMenuGroup, TableHeaderMenuGroupItem, TableHeaderMenuLayout, + TableHeaderMenuModel, TableRow, TableRowModel, TableRowsCheckedEvent, tooltips, ValueField } from '../index'; export class TableHeaderMenu extends Popup implements TableHeaderMenuModel { @@ -160,11 +160,11 @@ export class TableHeaderMenu extends Popup implements TableHeaderMenuModel { // Filtering this.filter = this.table.getFilter(this.column.id) as ColumnUserFilter; if (!this.filter) { - this.filter = this.column.createFilter(); + this.filter = this._createFilter(); } // always recalculate available values to make sure new/updated/deleted rows are considered this.filter.calculate(); - this.filter.on('filterFieldsChanged', this._updateFilterTable.bind(this)); + this.filter.on('filterFieldsChanged', this._updateTableFilter.bind(this)); this._updateFilterTableCheckedMode(); this.hasFilterTable = this.filter.availableValues.length > 0; @@ -178,6 +178,10 @@ export class TableHeaderMenu extends Popup implements TableHeaderMenuModel { } } + protected _createFilter(): ColumnUserFilter { + return this.column.createFilter(); + } + protected override _createLayout(): AbstractLayout { return new TableHeaderMenuLayout(this); } @@ -320,7 +324,7 @@ export class TableHeaderMenu extends Popup implements TableHeaderMenuModel { this.moveGroup = scout.create(TableHeaderMenuGroup, { parent: this, - textKey: 'ui.Move', + text: '${textKey:ui.Move}', cssClass: 'first' }); this.toBeginButton = scout.create(TableHeaderMenuButton, { @@ -388,7 +392,7 @@ export class TableHeaderMenu extends Popup implements TableHeaderMenuModel { protected _renderColumnActionsGroup(): TableHeaderMenuGroup { this.columnActionsGroup = scout.create(TableHeaderMenuGroup, { parent: this, - textKey: 'ui.Column' + text: '${textKey:ui.Column}' }); this.addColumnButton = scout.create(TableHeaderMenuButton, { @@ -435,7 +439,7 @@ export class TableHeaderMenu extends Popup implements TableHeaderMenuModel { this.sortingGroup = scout.create(TableHeaderMenuGroup, { parent: this, - textKey: 'ColumnSorting' + text: '${textKey:ColumnSorting}' }); if (!table.hasPermanentHeadOrTailSortColumns()) { @@ -535,14 +539,9 @@ export class TableHeaderMenu extends Popup implements TableHeaderMenuModel { } protected _renderGroupingGroup(): TableHeaderMenuGroup { - let menuPopup = this, - table = this.table, - column = this.column, - groupCount = this._groupColumnCount(); - let group = scout.create(TableHeaderMenuGroup, { parent: this, - textKey: 'ui.Grouping' + text: '${textKey:ui.Grouping}' }); this.groupButton = scout.create(TableHeaderMenuButton, { @@ -552,7 +551,7 @@ export class TableHeaderMenu extends Popup implements TableHeaderMenuModel { additional: false, toggleAction: true }); - this.groupButton.on('action', groupColumn.bind(this.groupButton)); + this.groupButton.on('action', event => this._onGroupButtonAction(event)); this.groupAddButton = scout.create(TableHeaderMenuButton, { parent: group, @@ -561,8 +560,9 @@ export class TableHeaderMenu extends Popup implements TableHeaderMenuModel { additional: true, toggleAction: true }); - this.groupAddButton.on('action', groupColumn.bind(this.groupAddButton)); + this.groupAddButton.on('action', event => this._onGroupButtonAction(event)); + let groupCount = this._groupColumnCount(); if (groupCount === 0) { this.groupAddButton.setVisible(false); } else if (groupCount === 1 && this.column.grouped) { @@ -572,7 +572,7 @@ export class TableHeaderMenu extends Popup implements TableHeaderMenuModel { this.groupAddButton.setVisible(true); } - if (table.hasPermanentHeadOrTailSortColumns() && groupCount > 0) { + if (this.table.hasPermanentHeadOrTailSortColumns() && groupCount > 0) { // If table has permanent head columns, other columns may not be grouped exclusively -> only enable add button (equally done for sort buttons) this.groupButton.setVisible(false); this.groupAddButton.setVisible(true); @@ -589,19 +589,19 @@ export class TableHeaderMenu extends Popup implements TableHeaderMenuModel { group.render(this.$columnActions); return group; + } - function groupColumn() { - let direction: 'asc' | 'desc' = (column.sortIndex >= 0 && !column.sortAscending) ? 'desc' : 'asc'; - menuPopup.close(); - table.group(column, direction, this.additional, !this.selected); - } + protected _onGroupButtonAction(event: Event) { + const button = event.source; + this.table.group(this.column, undefined, button.additional, !button.selected); + this.close(); } protected _renderHierarchyGroup(): TableHeaderMenuGroup { let table = this.table, menuPopup = this; this.hierarchyGroup = scout.create(TableHeaderMenuGroup, { parent: this, - textKey: 'ui.Hierarchy', + text: '${textKey:ui.Hierarchy}', visible: this.table.isTableNodeColumn(this.column) }); @@ -634,7 +634,7 @@ export class TableHeaderMenu extends Popup implements TableHeaderMenuModel { protected _renderWidthGroup(): TableHeaderMenuGroup { let group = scout.create(TableHeaderMenuGroup, { parent: this, - textKey: 'Width' + text: '${textKey:Width}' }); let optimizeWidthButton = scout.create(TableHeaderMenuButton, { parent: group, @@ -682,7 +682,7 @@ export class TableHeaderMenu extends Popup implements TableHeaderMenuModel { menuPopup = this, group = scout.create(TableHeaderMenuGroup, { parent: this, - textKey: 'ui.Aggregation' + text: '${textKey:ui.Aggregation}' }), allowedAggregationFunctions = arrays.ensure(column.allowedAggregationFunctions), isAggregationNoneAllowed = allowedAggregationFunctions.indexOf('none') !== -1; @@ -724,7 +724,7 @@ export class TableHeaderMenu extends Popup implements TableHeaderMenuModel { backgroundEffect = column.backgroundEffect, group = scout.create(TableHeaderMenuGroup, { parent: this, - textKey: 'ui.Coloring' + text: '${textKey:ui.Coloring}' }); this.colorGradient1Button = scout.create(TableHeaderMenuButton, { @@ -775,6 +775,8 @@ export class TableHeaderMenu extends Popup implements TableHeaderMenuModel { .appendDiv('table-header-menu-group-text') .text(this._filterByText()); HtmlComponent.install(this.$filterTableGroupTitle, this.session); + // Text is dynamic and might not have enough space due to the actions on the right side + tooltips.installForEllipsis(this.$filterTableGroupTitle, {parent: this}); let $filterActions = this.$filterTableGroup.appendDiv('actions'); this.filterSortOrderAction = scout.create(Action, { @@ -797,31 +799,11 @@ export class TableHeaderMenu extends Popup implements TableHeaderMenuModel { this.filterTable = this._createFilterTable(); this.filterTable.ariaRules = new ListBoxAriaRules(); this.filterTable.on('rowsChecked', this._filterTableRowsCheckedHandler); - let tableRows: TableRowModel[] = []; - this.filter.availableValues.forEach(filterValue => { - let tableRow: TableRowModel = { - cells: [ - scout.create(Cell, { - text: (this.filter.column instanceof NumberColumn) ? filterValue.text : null, - value: (this.filter.column instanceof NumberColumn) ? filterValue.key : filterValue.text, - iconId: filterValue.iconId, - htmlEnabled: filterValue.htmlEnabled, - cssClass: filterValue.cssClass - }), - filterValue.count, - filterValue.key === null ? 1 : 0 // empty cell should always be at the bottom - ], - checked: this.filter.selectedValues.indexOf(filterValue.key) > -1, - dataMap: { - filterValue: filterValue - } - }; - tableRows.push(tableRow); - }); - this.filterTable.insertRows(tableRows); + this._reloadFilterTable(); this.filterTable.render(this.$filterTableGroup); aria.linkElementWithLabel(this.filterTable.get$Focusable(), this.$filterTableGroupTitle); // must do this in a setTimeout, since table/popup is not visible yet (same as Table#revealSelection). + // FIXME bsh Remove setTimeout() when #385599 is fixed setTimeout(this.filterTable.revealChecked.bind(this.filterTable)); return this.$filterTableGroup; @@ -841,21 +823,20 @@ export class TableHeaderMenu extends Popup implements TableHeaderMenuModel { checkable: true, cssClass: 'table-header-menu-filter-table', checkableStyle: Table.CheckableStyle.TABLE_ROW, - // column-texts are not visible since header is not visible columns: [{ + id: 'ValueColumn', objectType: objectType, - text: 'filter-value', width: 120, - sortIndex: 1, horizontalAlignment: -1 }, { + id: 'AmountColumn', objectType: NumberColumn, - text: 'aggregate-count', cssClass: 'table-header-menu-filter-number-column', width: 50, minWidth: 32, autoOptimizeWidth: true }, { + id: 'SortByColumn', objectType: NumberColumn, displayable: false, sortIndex: 0 @@ -863,6 +844,42 @@ export class TableHeaderMenu extends Popup implements TableHeaderMenuModel { }); } + protected _reloadFilterTable() { + let tableRows: TableRowModel[] = []; + this.filter.availableValues.forEach((filterValue, filterValueIndex) => { + let tableRow: TableRowModel = { + cells: [ + scout.create(Cell, { + value: filterValue.key, + text: filterValue.text, + iconId: filterValue.iconId, + htmlEnabled: filterValue.htmlEnabled, + cssClass: filterValue.cssClass + }), + filterValue.count, + scout.create(Cell, { + value: filterValueIndex, // inherit order calculated by TableMatrix, see ColumnUserFilter#calculate + sortCode: objects.isNullOrUndefined(filterValue.key) ? 1 : 0 // the special '-empty-' cell should always be at the end + }) + ], + checked: this.filter.selectedValues.includes(filterValue.key), + dataMap: { + filterValue: filterValue + } + }; + tableRows.push(tableRow); + }); + this.filterTable.replaceRows(tableRows); + if (this.filterTable.rendered) { + // Re-optimize column widths + this.filterTable.columns.forEach(column => { + column.autoOptimizeWidthRequired = column.autoOptimizeWidth; + }); + this.filterTable.columnLayoutDirty = true; + this.filterTable.invalidateLayoutTree(false); + } + } + /** * @returns the title-text used for the filter-table */ @@ -899,20 +916,20 @@ export class TableHeaderMenu extends Popup implements TableHeaderMenuModel { let sortMode = TableHeaderMenu.SortMode; if (this.filterSortMode === sortMode.ALPHABETICALLY) { // sort by amount - this.filterTable.sort(this.filterTable.columns[1], 'desc'); + this.filterTable.sort(this.filterTable.columnById('AmountColumn'), 'desc'); + this.filterTable.sort(this.filterTable.columnById('SortByColumn'), 'asc', true); // if two rows have the same amount, sort them by original order this.filterSortMode = sortMode.AMOUNT; this.filterSortOrderAction.setTooltipText(this.session.text('ui.SortAlphabetically')); } else { - // sort alphabetically (first by invisible column to make sure empty cells are always at the bottom) - this.filterTable.sort(this.filterTable.columns[2], 'asc'); - this.filterTable.sort(this.filterTable.columns[0], 'asc', true); - this.filterSortOrderAction.setTooltipText(this.session.text('ui.SortByNumber')); + // sort by original order, see _reloadFilterTable() + this.filterTable.sort(this.filterTable.columnById('SortByColumn'), 'asc'); this.filterSortMode = sortMode.ALPHABETICALLY; + this.filterSortOrderAction.setTooltipText(this.session.text('ui.SortByNumber')); } this._updateFilterTableActions(); } - protected _updateFilterTable() { + protected _updateTableFilter() { if (this.filter.filterActive()) { this.table.addFilter(this.filter); } else { @@ -991,7 +1008,7 @@ export class TableHeaderMenu extends Popup implements TableHeaderMenuModel { this.filter.selectedValues.push(row.dataMap.filterValue.key); } }); - this._updateFilterTable(); + this._updateTableFilter(); } protected _onFilterTableChanged() { diff --git a/eclipse-scout-core/src/table/TableHeaderMenuGroup.ts b/eclipse-scout-core/src/table/TableHeaderMenuGroup.ts index 64bee9f3b28..dc10eae84fb 100644 --- a/eclipse-scout-core/src/table/TableHeaderMenuGroup.ts +++ b/eclipse-scout-core/src/table/TableHeaderMenuGroup.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2025 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -7,7 +7,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -import {aria, AriaLabelledByInsertPosition, InitModelOf, scout, TabbableCoordinator, TableHeaderMenuButton, TableHeaderMenuGroupEventMap, TableHeaderMenuGroupModel, Widget, widgets} from '../index'; +import {aria, AriaLabelledByInsertPosition, InitModelOf, scout, strings, TabbableCoordinator, TableHeaderMenuButton, TableHeaderMenuGroupEventMap, TableHeaderMenuGroupModel, Widget, widgets} from '../index'; export class TableHeaderMenuGroup extends Widget implements TableHeaderMenuGroupModel { declare model: TableHeaderMenuGroupModel; @@ -15,22 +15,30 @@ export class TableHeaderMenuGroup extends Widget implements TableHeaderMenuGroup declare self: TableHeaderMenuGroup; text: string; - textKey: string; - last: boolean; $text: JQuery; + /** + * Specifies which of the "items" (buttons) is currently active and should be shown in the group title. + * If null, the initial text is shown instead. This property is automatically set by _updateCurrentGroupItem(). + */ + currentGroupItem: TableHeaderMenuGroupItem; tabbableCoordinator: TabbableCoordinator; + // Internal properties used by _updateCurrentGroupItem() to compute the actual "current item". + // - "hovered" = set when the user hovers an item with the mouse + // - "focused" = set when an item is focused with the keyboard + // - "active" = can be set when an item is neither hovered nor focused but should still be marked as the current item (e.g. while a context menu popup is open) + protected _hoveredGroupItem: TableHeaderMenuGroupItem; + protected _focusedGroupItem: TableHeaderMenuGroupItem; + protected _activeGroupItem: TableHeaderMenuGroupItem; + constructor() { super(); - this.text = null; - this.textKey = null; - this.last = false; this.tabbableCoordinator = scout.create(TabbableCoordinator, {parent: this}); } protected override _init(options: InitModelOf) { super._init(options); - this.text = scout.nvl(this.text, this.session.text(this.textKey)); + this.resolveTextKeys(['text']); } protected override _addChild(child: Widget) { @@ -41,10 +49,7 @@ export class TableHeaderMenuGroup extends Widget implements TableHeaderMenuGroup protected override _render() { this.$container = this.$parent.appendDiv('table-header-menu-group buttons'); this.$text = this.$container.appendDiv('table-header-menu-group-text'); - if (this.cssClass) { - this.$container.addClass(this.cssClass); - } - this._renderText(); + this.children.forEach(child => { child.render(); if (isGroupItem(child)) { @@ -54,50 +59,41 @@ export class TableHeaderMenuGroup extends Widget implements TableHeaderMenuGroup widgets.updateFirstLastMarker(this.children); } - appendText(text: string) { - this.text = this.session.text(this.textKey) + ' ' + text; - if (this.rendered) { - this._renderText(); - } - } - - resetText() { - let focusedItem = this._getFocusedGroupItem(); - if (focusedItem) { - this.appendText(focusedItem.computeGroupSuffix()); - } else { - this.setText(this.session.text(this.textKey)); - } + protected override _renderProperties() { + super._renderProperties(); + this._renderCurrentGroupItem(); } setText(text: string) { this.text = text; if (this.rendered) { - this._renderText(); + this._renderComputedText(); } } - protected _renderText() { - this.$text.text(this.text); + protected _renderComputedText() { + let computedText = this._computeText(); + this.$text.text(computedText); } - protected _getFocusedGroupItem(): TableHeaderMenuGroupItem { - if (!this.rendered) { - return null; + protected _computeText(): string { + if (this.currentGroupItem) { + return strings.join(' ', this.text, this.currentGroupItem.computeGroupSuffix()); } - let focusedWidget = scout.widget(this.$container.activeElement()); - if (this.has(focusedWidget) && isGroupItem(focusedWidget)) { - return focusedWidget; - } - return null; + return this.text; } - setLast(last: boolean) { - this.setProperty('last', last); + setCurrentGroupItem(currentGroupItem: TableHeaderMenuGroupItem) { + if (!currentGroupItem) { + this._hoveredGroupItem = null; + this._activeGroupItem = null; + this._focusedGroupItem = null; + } + this.setProperty('currentGroupItem', currentGroupItem); } - protected _renderLast() { - this.$container.toggleClass('last', this.last); + protected _renderCurrentGroupItem() { + this._renderComputedText(); } /** @@ -110,12 +106,32 @@ export class TableHeaderMenuGroup extends Widget implements TableHeaderMenuGroup aria.linkElementWithLabel(item.get$Focusable(), this.$text, AriaLabelledByInsertPosition.FRONT, true); item.$container - .on('focusin mouseenter', () => this.appendText(item.computeGroupSuffix())) - .on('focusout mouseleave', () => { - if (!item.isFocused()) { - this.resetText(); - } - }); + .on('focusin', () => this.setFocusedGroupItem(item)) + .on('focusout', () => this.setFocusedGroupItem(null)) + .on('mouseenter', () => this.setHoveredGroupItem(item)) + .on('mouseleave', () => this.setHoveredGroupItem(null)); + } + + /** @internal */ + setHoveredGroupItem(hoveredGroupItem: TableHeaderMenuGroupItem) { + this._hoveredGroupItem = hoveredGroupItem; + this._updateCurrentGroupItem(); + } + + /** @internal */ + setFocusedGroupItem(focusedGroupItem: TableHeaderMenuGroupItem) { + this._focusedGroupItem = focusedGroupItem; + this._updateCurrentGroupItem(); + } + + /** @internal */ + setActiveGroupItem(activeGroupItem: TableHeaderMenuGroupItem) { + this._activeGroupItem = activeGroupItem; + this._updateCurrentGroupItem(); + } + + protected _updateCurrentGroupItem() { + this.setCurrentGroupItem(this._hoveredGroupItem || this._focusedGroupItem || this._activeGroupItem); } } diff --git a/eclipse-scout-core/src/table/TableHeaderMenuGroupModel.ts b/eclipse-scout-core/src/table/TableHeaderMenuGroupModel.ts index a44b6d41bfd..4155b8c53ea 100644 --- a/eclipse-scout-core/src/table/TableHeaderMenuGroupModel.ts +++ b/eclipse-scout-core/src/table/TableHeaderMenuGroupModel.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -11,6 +11,4 @@ import {WidgetModel} from '../index'; export interface TableHeaderMenuGroupModel extends WidgetModel { text?: string; - textKey?: string; - last?: boolean; } diff --git a/eclipse-scout-core/src/table/TableHeaderMenuLayout.ts b/eclipse-scout-core/src/table/TableHeaderMenuLayout.ts index 4795c32aeeb..8b107896d28 100644 --- a/eclipse-scout-core/src/table/TableHeaderMenuLayout.ts +++ b/eclipse-scout-core/src/table/TableHeaderMenuLayout.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -86,10 +86,9 @@ export class TableHeaderMenuLayout extends PopupLayout { // fix width of actions column, so it doesn't become wider when user // hovers over a button and thus the text of the group changes. - this._setMaxWidth(); let htmlColumnActions = HtmlComponent.get(this.popup.$columnActions); let actionColumnSize = htmlColumnActions.size(); - this._setMaxWidth(actionColumnSize.width); + this.popup.$columnActions.cssWidth(actionColumnSize.width); htmlColumnActions.validateLayout(); // Ensure widgets that require layouting (e.g. NumberField for column width) are layouted. } @@ -127,13 +126,13 @@ export class TableHeaderMenuLayout extends PopupLayout { * + paddings of surrounding containers */ override preferredLayoutSize($container: JQuery, options?: HtmlCompPrefSizeOptions): Dimension { - let rightColumnHeight = 0, - leftColumnHeight = 0, - containerInsets = graphics.insets($container), - oldMaxWidth = this._getMaxWidth(); + // temp. remove width so we can determine pref. size + let origStyle = this.popup.$columnActions.attr('style'); + this.popup.$columnActions.cssWidth(null); - this._setMaxWidth(); // temp. remove max-width so we can determine pref. size - leftColumnHeight = graphics.size(this.popup.$columnActions, true).height; + let containerInsets = graphics.insets($container); + let leftColumnHeight = graphics.size(this.popup.$columnActions, true).height; + let rightColumnHeight = 0; // Filter table if (this.popup.hasFilterTable) { @@ -185,17 +184,9 @@ export class TableHeaderMenuLayout extends PopupLayout { prefSize.height = leftColumnHeight + rightColumnHeight + containerInsets.vertical(); } - // restore max-width - this._setMaxWidth(oldMaxWidth); + // restore style + this.popup.$columnActions.attrOrRemove('style', origStyle); return prefSize; } - - protected _getMaxWidth(): number { - return parseInt(this.popup.$columnActions.css('max-width'), 10); - } - - protected _setMaxWidth(maxWidth?: number) { - this.popup.$columnActions.css('max-width', maxWidth || ''); - } } diff --git a/eclipse-scout-core/src/table/TableMatrix.ts b/eclipse-scout-core/src/table/TableMatrix.ts index b5e56ffaef0..937b3fe166e 100644 --- a/eclipse-scout-core/src/table/TableMatrix.ts +++ b/eclipse-scout-core/src/table/TableMatrix.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -7,30 +7,28 @@ * * SPDX-License-Identifier: EPL-2.0 */ -import {arrays, BooleanColumn, Column, comparators, DateColumn, DateFormat, dates, EnumObject, IconColumn, Locale, NumberColumn, objects, scout, Session, Table, TableRow} from '../index'; +import {arrays, BooleanColumn, Column, comparators, DateColumn, DateFormat, DateGroupType, dates, EnumObject, IconColumn, Locale, NumberColumn, scout, Session, Table, TableRow} from '../index'; export class TableMatrix { - session: Session; - locale: Locale; + protected _table: Table; + protected _rows: TableRow[]; protected _allData: TableMatrixDataAxis[]; protected _allAxis: TableMatrixKeyAxis[]; - protected _rows: TableRow[]; - protected _table: Table; - constructor(table: Table, session: Session) { - this.session = session; - this.locale = session.locale; + constructor(table: Table) { + this._table = scout.assertParameter('table', table); + this._rows = table.rows; this._allData = []; this._allAxis = []; - this._rows = table.rows; - this._table = table; } static DateGroup = { NONE: 0, YEAR: 256, MONTH: 257, + MONTH_AND_YEAR: 260, + CALENDAR_WEEK: 261, WEEKDAY: 258, DATE: 259 } as const; @@ -41,13 +39,21 @@ export class TableMatrix { AVG: 2 } as const; + get session(): Session { + return this._table.session; + } + + get locale(): Locale { + return this.session.locale; + } + /** - * add data axis + * Adds a new data axis (value) that operates on the given table column. */ addData(data: Column, dataGroup: TableMatrixNumberGroup): TableMatrixDataAxis { // @ts-expect-error - let dataAxis: TableMatrixDataAxis = {}, - locale = this.locale; + const dataAxis: TableMatrixDataAxis = {}; + const locale = this.locale; // collect all axis this._allData.push(dataAxis); @@ -89,14 +95,15 @@ export class TableMatrix { return dataAxis; } - // add x or y Axis + /** + * Adds a new key axis (x or y) that operates on the data in the given table column. + */ addAxis(axis: Column, axisGroup: TableMatrixNumberGroup | TableMatrixDateGroup): TableMatrixKeyAxis { // @ts-expect-error - let keyAxis: TableMatrixKeyAxis = [], - locale = this.locale, - session = this.session, - getText = this.session.text.bind(this.session), - emptyCell = getText('ui.EmptyCell'); + const keyAxis: TableMatrixKeyAxis = []; + const locale = this.locale; + const session = this.session; + const emptyCell = session.text('ui.EmptyCell'); // collect all axis this._allAxis.push(keyAxis); @@ -114,25 +121,36 @@ export class TableMatrix { } }; - // default functions - keyAxis.reorder = () => { - keyAxis.sort((a, b) => { - // make sure -empty- is at the bottom + // Helper function to create a sort function that always sorts 'null' at the end. + // We use this to make sure '-empty-' is at the bottom. + const nullsLast = (compareFn: (a: any, b: any) => number) => { + return (a, b) => { + if (a === null && b === null) { + return 0; + } if (a === null) { return 1; } if (b === null) { return -1; } - let sortCodeA = keyAxis.sortCodeMap[a], - sortCodeB = keyAxis.sortCodeMap[b]; - if (!objects.isNullOrUndefined(sortCodeA) || !objects.isNullOrUndefined(sortCodeB)) { - return comparators.NUMERIC.compare(sortCodeA, sortCodeB); + return compareFn(a, b); + }; + }; + + // default functions + keyAxis.reorder = () => { + keyAxis.sort(nullsLast((a: number, b: number): number => { + let sortCodeA = keyAxis.sortCodeMap[a]; + let sortCodeB = keyAxis.sortCodeMap[b]; + let c = comparators.NUMERIC.compare(sortCodeA, sortCodeB); + if (c) { + return c; } - // sort others - return (a - b); - }); + return keyAxis.compareKeys(a, b); + })); }; + keyAxis.compareKeys = (a: number, b: number): number => a - b; keyAxis.norm = f => { if (f === null || f === '') { return null; @@ -149,35 +167,51 @@ export class TableMatrix { } return keyAxis.normTable[n]; }; - keyAxis.deterministicKeyToKey = deterministicKey => keyAxis.norm(deterministicKey); - keyAxis.keyToDeterministicKey = key => { - if (key === null) { + keyAxis.keyToDeterministicKey = n => { + if (n === null) { return null; } - return keyAxis.format(key); + return keyAxis.format(n); }; + keyAxis.deterministicKeyToKey = d => keyAxis.norm(d); keyAxis.normDeterministic = f => keyAxis.keyToDeterministicKey(keyAxis.norm(f)); - // norm and format depends of datatype and group functionality + // norm and format depends on datatype and group functionality if (axis instanceof DateColumn) { + // deterministic key is always a number + keyAxis.keyToDeterministicKey = (n: number): number => n; + keyAxis.deterministicKeyToKey = (d: number): number => d; + + // Milliseconds in a normal day + const DAY_MILLIS = 24 * 60 * 60 * 1000; + // Offset from "local midnight" to "UTC midnight" + const LOCAL_EPOCH_OFFSET_MILLIS = new Date(1970, 0, 1).getTime(); + if (axisGroup === TableMatrix.DateGroup.NONE) { + // Dates from the server are sent without timezone, i.e. they look the same for all users but don't represent the same + // point in time. To make sure all users get the same numeric value, we shift it according to the local timezone. + // + // Example: + // + // dates.parseJsonDate('2025-10-20') getTime() LOCAL_EPOCH_OFFSET_MILLIS norm() + // ------------------------------------------------------------------------------------------------------------------------------------- + // Mon Oct 20 2025 00:00:00 GMT+0000 (Coordinated Universal Time) 1760918400000 0 1760918400000 + // Mon Oct 20 2025 00:00:00 GMT+0100 (Central European Standard Time) 1760914800000 -3600000 1760918400000 + // Mon Oct 20 2025 00:00:00 GMT+0200 (Central European Summer Time) 1760911200000 -7200000 1760918400000 + // Mon Oct 20 2025 00:00:00 GMT-0800 (Pacific Standard Time) 1760947200000 28800000 1760918400000 + // Mon Oct 20 2025 00:00:00 GMT-0700 (Pacific Daylight Time) 1760943600000 25200000 1760918400000 keyAxis.norm = f => { if (f === null || f === '') { return null; } - return f.getTime(); + return f.getTime() - LOCAL_EPOCH_OFFSET_MILLIS; }; keyAxis.format = n => { if (n === null) { - return null; - } - let format = axis.format; - if (format) { - format = DateFormat.ensure(locale, format); - } else { - format = locale.dateFormat; + return emptyCell; } - return format.format(new Date(n)); + let format = DateFormat.ensure(locale, axis.format || locale.dateFormat); + return format.format(new Date(n + LOCAL_EPOCH_OFFSET_MILLIS)); }; } else if (axisGroup === TableMatrix.DateGroup.YEAR) { keyAxis.norm = f => { @@ -205,6 +239,34 @@ export class TableMatrix { } return locale.dateFormatSymbols.months[n]; }; + } else if (axisGroup === TableMatrix.DateGroup.MONTH_AND_YEAR) { + keyAxis.norm = f => { + if (f === null || f === '') { + return null; + } + // months since 1970-01-01 + return ((f.getFullYear() - 1970) * 12) + f.getMonth(); + }; + keyAxis.format = n => { + if (n === null) { + return emptyCell; + } + let date = dates.shift(new Date(1970, 0, 1), 0, n); + return dates.format(date, locale, 'MMMM yyyy'); + }; + } else if (axisGroup === TableMatrix.DateGroup.CALENDAR_WEEK) { + keyAxis.norm = f => { + if (f === null || f === '') { + return null; + } + return dates.weekInYear(f); + }; + keyAxis.format = n => { + if (n === null) { + return emptyCell; + } + return session.text('ui.CW', n); + }; } else if (axisGroup === TableMatrix.DateGroup.WEEKDAY) { keyAxis.norm = f => { if (f === null || f === '') { @@ -218,23 +280,38 @@ export class TableMatrix { } return locale.dateFormatSymbols.weekdaysOrdered[n]; }; + // Convert locale-dependent weekday (0 = firstDayOfWeek) to locale-independent weekday (0 = Sun) + keyAxis.keyToDeterministicKey = (n: number): number => { + if (n === null) { + return null; + } + return (n + locale.dateFormatSymbols.firstDayOfWeek) % 7; + }; + keyAxis.deterministicKeyToKey = (d: number): number => { + if (d === null) { + return null; + } + return (d + 7 - locale.dateFormatSymbols.firstDayOfWeek) % 7; + }; } else if (axisGroup === TableMatrix.DateGroup.DATE) { keyAxis.norm = f => { if (f === null || f === '') { return null; } - return dates.trunc(f).getTime(); + // Truncate to midnight in UTC, so that dividing by DAY_MILLIS will result in a whole number + let utcMillis = Date.UTC(f.getFullYear(), f.getMonth(), f.getDate()); + return utcMillis / DAY_MILLIS; }; keyAxis.format = n => { if (n === null) { return emptyCell; } - return dates.format(new Date(n), locale, locale.dateFormatPatternDefault); + let utcMillis = n * DAY_MILLIS; + // shift "UTC midnight" to "local midnight" + let date = new Date(utcMillis + LOCAL_EPOCH_OFFSET_MILLIS); + return dates.format(date, locale, locale.dateFormatPatternDefault); }; } - keyAxis.deterministicKeyToKey = (deterministicKey: number) => deterministicKey; - keyAxis.keyToDeterministicKey = key => key; - keyAxis.normDeterministic = f => keyAxis.norm(f); } else if (axis instanceof NumberColumn) { keyAxis.norm = f => { if (isNaN(f) || f === null || f === '') { @@ -248,9 +325,9 @@ export class TableMatrix { } return axis.decimalFormat.format(n); }; - keyAxis.deterministicKeyToKey = (deterministicKey: number) => deterministicKey; - keyAxis.keyToDeterministicKey = key => key; - keyAxis.normDeterministic = f => keyAxis.norm(f); + // deterministic key is always a number + keyAxis.keyToDeterministicKey = (n: number): number => n; + keyAxis.deterministicKeyToKey = (d: number): number => d; } else if (axis instanceof BooleanColumn) { keyAxis.norm = f => { if (axis.triStateEnabled && f === null) { @@ -263,43 +340,30 @@ export class TableMatrix { }; keyAxis.format = n => { if (n === -1) { - return getText('ui.BooleanColumnGroupingMixed'); + return session.text('ui.BooleanColumnGroupingMixed'); } if (n === 0) { - return getText('ui.BooleanColumnGroupingFalse'); + return session.text('ui.BooleanColumnGroupingFalse'); } if (n === 1) { - return getText('ui.BooleanColumnGroupingTrue'); + return session.text('ui.BooleanColumnGroupingTrue'); } + return ''; }; - keyAxis.deterministicKeyToKey = (deterministicKey: number) => deterministicKey; - keyAxis.keyToDeterministicKey = key => key; - keyAxis.normDeterministic = f => keyAxis.norm(f); + // use inverse order -> true, false, mixed + keyAxis.compareKeys = (a: number, b: number): number => b - a; + // deterministic key is always a number + keyAxis.keyToDeterministicKey = (n: number): number => n; + keyAxis.deterministicKeyToKey = (d: number): number => d; } else if (axis instanceof IconColumn) { keyAxis.isIcon = true; } else { - keyAxis.reorder = () => { - let comparator = comparators.TEXT; - comparator.install(session); - - keyAxis.sort((a, b) => { - // make sure -empty- is at the bottom - if (a === null) { - return 1; - } - if (b === null) { - return -1; - } - let sortCodeA = keyAxis.sortCodeMap[a], - sortCodeB = keyAxis.sortCodeMap[b]; - if (!objects.isNullOrUndefined(sortCodeA) || !objects.isNullOrUndefined(sortCodeB)) { - return comparators.NUMERIC.compare(sortCodeA, sortCodeB); - } - // sort others - return comparator.compare(keyAxis.format(a), keyAxis.format(b)); - }); + comparators.TEXT.install(session); + keyAxis.compareKeys = (a: number, b: number): number => { + return comparators.TEXT.compare(keyAxis.format(a), keyAxis.format(b)); }; } + return keyAxis; } @@ -307,12 +371,13 @@ export class TableMatrix { * @returns a cube containing the results */ calculate(): TableMatrixResult { - let cube: Record> & { length?: number; getValue?(keys: number[]): number[] } = {}, length = 0; + let cube: Record> & { length?: number; getValue?(keys: number[]): number[] } = {}; + let length = 0; // collect data from table for (let r = 0; r < this._rows.length; r++) { let row = this._rows[r]; - // collect keys of x, y axis from row + // collect keys of x- and y-axis from row let keys: number[] = []; for (let k = 0; k < this._allAxis.length; k++) { let column = this._allAxis[k].column; @@ -383,7 +448,7 @@ export class TableMatrix { } } - // To calculate correct y axis scale data.max must not be 0. If data.max===0-> log(data.max)=-infinity + // To calculate correct y-axis scale data.max must not be 0. If data.max===0-> log(data.max)=-infinity if (scout.nvl(data.max, 0) === 0) { data.max = 0.1; } @@ -423,7 +488,6 @@ export class TableMatrix { } /** - * * @returns Array holding an entry for each column. Each entry consists of an array with the column at index 0 and the count at index 1. */ columnCount(filterNumberColumns?: boolean): Array | number>> { @@ -475,6 +539,27 @@ export class TableMatrix { isMatrixValid(): boolean { return this._table.rows.length === 0 || this._table.filterColumns(() => true, false).length === this._table.rows[0].cells.length; } + + /** + * Converts the given {@link DateGroupType} enum to the corresponding {@link TableMatrixDateGroup}. + */ + static resolveDateGroup(groupType: DateGroupType): TableMatrixDateGroup { + switch (groupType) { + case DateGroupType.YEAR: + return TableMatrix.DateGroup.YEAR; + case DateGroupType.MONTH: + return TableMatrix.DateGroup.MONTH; + case DateGroupType.MONTH_AND_YEAR: + return TableMatrix.DateGroup.MONTH_AND_YEAR; + case DateGroupType.CALENDAR_WEEK: + return TableMatrix.DateGroup.CALENDAR_WEEK; + case DateGroupType.WEEKDAY: + return TableMatrix.DateGroup.WEEKDAY; + case DateGroupType.DATE: + return TableMatrix.DateGroup.DATE; + } + return null; + } } export type TableMatrixNumberGroup = EnumObject; @@ -486,15 +571,26 @@ export type TableMatrixKeyAxis = number[] & { sortCodeMap: Record; isIcon?: boolean; iconId?: string; + /** The smallest numeric key in this axis */ min: number; + /** The biggest numeric key in this axis */ max: number; + /** Converts any value to a numeric key */ + norm(f: any): number; + /** Formats the given numeric key ({@link norm}) for display */ format(n: number): string; + /** Adds the numeric key ({@link norm}) to this axis if it does not already exist */ + add(k: number); + /** Converts the given numeric key ({@link norm}) to a persistable representation (aka "deterministic key") */ keyToDeterministicKey(n: number): number | string; - deterministicKeyToKey(f: string | number): number; + /** Converts the given persistable key (aka "deterministic key") back to a numeric key ({@link norm}) */ + deterministicKeyToKey(d: string | number): number; + /** Same as {@link norm} + {@link keyToDeterministicKey} */ normDeterministic(f: any): string | number; - norm(f: any): number; - add(k: number); + /** Sorts the keys in this axis. the default implementation first considers {@link sortCodeMap}, then calls {@link compareKeys}. */ reorder(): void; + /** Compares the given numeric keys ({@link norm}). used by {@link reorder}, both keys are not null. */ + compareKeys(n1: number, n2: number): number; }; export type TableMatrixDataAxis = { @@ -502,8 +598,11 @@ export type TableMatrixDataAxis = { total: number; min: number; max: number; - format(n: number): string; + /** Converts any value to a numeric key */ norm(f: any): number; + /** Formats the given numeric key ({@link norm}) for display */ + format(n: number): string; + /** Aggregates the given values into a single value */ group(array: number[]): number; }; diff --git a/eclipse-scout-core/src/table/TableRow.ts b/eclipse-scout-core/src/table/TableRow.ts index 2745e13ca1d..ecd9bf77314 100644 --- a/eclipse-scout-core/src/table/TableRow.ts +++ b/eclipse-scout-core/src/table/TableRow.ts @@ -112,7 +112,6 @@ export class TableRow implements TableRowModel, ObjectWithType, FilterElement { return this.childRows.some(childRow => childRow.filterAccepted || childRow.hasFilterAcceptedChildren()); } - get table(): Table { return this.parent; } diff --git a/eclipse-scout-core/src/table/columns/Column.ts b/eclipse-scout-core/src/table/columns/Column.ts index 3c64b438572..564af61db85 100644 --- a/eclipse-scout-core/src/table/columns/Column.ts +++ b/eclipse-scout-core/src/table/columns/Column.ts @@ -1029,11 +1029,19 @@ export class Column extends PropertyEventEmitter implements Col } compare(row1: TableRow, row2: TableRow): number { - let cell1 = this.table.cell(this, row1), - cell2 = this.table.cell(this, row2); - - if (cell1.sortCode !== null || cell2.sortCode !== null) { - return comparators.NUMERIC.compare(cell1.sortCode, cell2.sortCode); + let cell1 = this.table.cell(this, row1); + let cell2 = this.table.cell(this, row2); + + // Sort by sort code. If only one row has a sort code, sort it first. + if (cell1.sortCode !== null && cell2.sortCode !== null) { + let c = comparators.NUMERIC.compare(cell1.sortCode, cell2.sortCode); + if (c) { // only return if sort codes are different, otherwise continue comparing by value + return c; + } + } else if (cell1.sortCode !== null) { + return -1; + } else if (cell2.sortCode !== null) { + return 1; } let valueA = this.cellValueOrText(row1); diff --git a/eclipse-scout-core/src/table/columns/DateColumn.ts b/eclipse-scout-core/src/table/columns/DateColumn.ts index 951d1ca2acc..9e1cd43800c 100644 --- a/eclipse-scout-core/src/table/columns/DateColumn.ts +++ b/eclipse-scout-core/src/table/columns/DateColumn.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -7,7 +7,10 @@ * * SPDX-License-Identifier: EPL-2.0 */ -import {Column, comparators, DateColumnEventMap, DateColumnModel, DateField, DateFormat, dates, InitModelOf, Locale, scout, TableRow} from '../../index'; +import { + Column, comparators, DateColumnEventMap, DateColumnModel, DateColumnTableHeaderMenu, DateColumnUserFilter, DateField, DateFormat, DateGroupType, dates, InitModelOf, Locale, scout, TableHeader, TableHeaderMenu, TableMatrix, + TableMatrixKeyAxis, TableRow +} from '../../index'; export class DateColumn extends Column implements DateColumnModel { declare model: DateColumnModel; @@ -16,14 +19,18 @@ export class DateColumn extends Column implements DateColumnModel { format: DateFormat; groupFormat: DateFormat; + groupType: DateGroupType; hasDate: boolean; hasTime: boolean; + protected _groupTypeAxis: TableMatrixKeyAxis; // set by _updateGroupTypeAxis() when groupType is changed + constructor() { super(); this.format = null; // @ts-expect-error this.groupFormat = 'yyyy'; + this.groupType = null; this.hasDate = true; this.hasTime = false; this.filterType = 'DateColumnUserFilter'; @@ -36,6 +43,16 @@ export class DateColumn extends Column implements DateColumnModel { this._setFormat(this.format); this._setGroupFormat(this.groupFormat); + this._setGroupType(this.groupType); + } + + override createTableHeaderMenu(tableHeader: TableHeader): TableHeaderMenu { + return scout.create(DateColumnTableHeaderMenu, { + parent: tableHeader, + column: this, + tableHeader: tableHeader, + $anchor: this.$header + }); } setFormat(format: DateFormat | string) { @@ -69,6 +86,37 @@ export class DateColumn extends Column implements DateColumnModel { } } + /** + * Changes the {@link groupType} to the given value. + * + * If the column is grouped, the optional argument `applyGrouping` specifies whether the table rows should + * be updated automatically. Otherwise, the grouping has to be applied manually ({@link group}). Default is true. + */ + setGroupType(groupType: DateGroupType, applyGrouping = true) { + let changed = this.setProperty('groupType', groupType); + if (changed && applyGrouping && this.grouped) { + // Adding an already grouped column does not change the index, but will sort the table correctly + this.table.addGroupColumn(this); + } + } + + protected _setGroupType(groupType: DateGroupType) { + this._setProperty('groupType', groupType); + this._updateGroupTypeAxis(); + // Trigger event to update ui preferences and sync to java model + this.table.trigger('columnDateGroupTypeChanged', {column: this}); + } + + protected _updateGroupTypeAxis() { + let group = TableMatrix.resolveDateGroup(this.groupType); + if (group) { + let matrix = new TableMatrix(this.table); + this._groupTypeAxis = matrix.addAxis(this, group); + } else { + this._groupTypeAxis = null; + } + } + protected override _formatValue(value: Date, row?: TableRow): string { return this.format.format(value); } @@ -89,6 +137,12 @@ export class DateColumn extends Column implements DateColumnModel { override cellTextForGrouping(row: TableRow): string { let val = this.table.cellValue(this, row); + if (!val) { + return ''; + } + if (this._groupTypeAxis) { + return this._groupTypeAxis.format(this._groupTypeAxis.norm(val)); + } return this.groupFormat.format(val); } @@ -99,4 +153,36 @@ export class DateColumn extends Column implements DateColumnModel { hasTime: this.hasTime }); } + + override compare(row1: TableRow, row2: TableRow): number { + // --------------------------------------------------------------------------- + // Keep implementation in sync with AbstractDateColumn.java#compareTableRows + // --------------------------------------------------------------------------- + + let value1 = this.cellValue(row1); + let value2 = this.cellValue(row2); + if (!value1 && !value2) { + return 0; + } + if (!value1) { + return -1; + } + if (!value2) { + return 1; + } + + if (this.grouped && this._groupTypeAxis) { + let c = this._groupTypeAxis.norm(value1) - this._groupTypeAxis.norm(value2); + if (c) { + return c; + } + // If we are here, the grouped values are the same. Only return 0 if this is _not_ the last sort column, + // or else the additional columns could not have an effect on the row order. However, if this _is_ the + // last sort column, sort the values normally (-> super call). + if (this.table.columns.some(c => c !== this && c.sortActive && (c.sortIndex > this.sortIndex || c.initialAlwaysIncludeSortAtEnd))) { + return 0; + } + } + return super.compare(row1, row2); + } } diff --git a/eclipse-scout-core/src/table/columns/DateColumnEventMap.ts b/eclipse-scout-core/src/table/columns/DateColumnEventMap.ts index dcc750d21a7..42819c672db 100644 --- a/eclipse-scout-core/src/table/columns/DateColumnEventMap.ts +++ b/eclipse-scout-core/src/table/columns/DateColumnEventMap.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -7,9 +7,10 @@ * * SPDX-License-Identifier: EPL-2.0 */ -import {ColumnEventMap, DateFormat, PropertyChangeEvent} from '../../index'; +import {ColumnEventMap, DateFormat, DateGroupType, PropertyChangeEvent} from '../../index'; export interface DateColumnEventMap extends ColumnEventMap { 'propertyChange:format': PropertyChangeEvent; 'propertyChange:groupFormat': PropertyChangeEvent; + 'propertyChange:groupType': PropertyChangeEvent; } diff --git a/eclipse-scout-core/src/table/columns/DateColumnModel.ts b/eclipse-scout-core/src/table/columns/DateColumnModel.ts index e2a0d84160d..f92249cc4ee 100644 --- a/eclipse-scout-core/src/table/columns/DateColumnModel.ts +++ b/eclipse-scout-core/src/table/columns/DateColumnModel.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2025 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -7,11 +7,40 @@ * * SPDX-License-Identifier: EPL-2.0 */ -import {ColumnModel, DateFormat} from '../../index'; +import {ColumnModel, DateFormat, DateGroupType} from '../../index'; export interface DateColumnModel extends ColumnModel { + /** + * Specifies the {@link DateFormat} to be used when formatting cell values. + * + * If omitted, the column creates a default format based on the current locale and {@link hasDate}/{@link hasTime}. + * When a string is supplied, it is treated as a pattern for a new {@link DateFormat} instance. + */ format?: DateFormat | string; + /** + * Specifies a custom format that can be used to group the cell values in this column. All values + * with the same formatted representation are grouped together, sorted by the underlying date value. + * + * This format only has an effect if {@link groupType} is not set explicitly. It does not affect the + * display of the individual cell values, which are always formatted using {@link format}. + * + * Typically, this only needs to be set if the desired format cannot be provided by one of the + * {@link DateGroupType} values. + * + * Default is `'yyyy'` (year). + */ groupFormat?: DateFormat | string; + /** + * Specifies how the values in this column are grouped when grouping is enabled ({@link grouped}). + * Groups are sorted naturally, e.g. months from January to December. If this property is `null`, + * the data is grouped by {@link groupFormat}. + * + * Supported values are defined in the enum {@link DateGroupType}. The user can always change the + * group type via the table header menu of this column. + * + * Default is `null`. + */ + groupType?: DateGroupType; /** * Configures whether the values of this column should show the date. * diff --git a/eclipse-scout-core/src/table/columns/DateColumnTableHeaderMenu.less b/eclipse-scout-core/src/table/columns/DateColumnTableHeaderMenu.less new file mode 100644 index 00000000000..ed20ebc404e --- /dev/null +++ b/eclipse-scout-core/src/table/columns/DateColumnTableHeaderMenu.less @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +.table-header-menu-date-group-type-action { + display: inline; +} + +.table-header-menu-filter-date-group-type { + width: 22px; + + &::before { + #scout.font-icon(); + content: @icon-calendar; + } +} + +.date-group-type-menu > .hint { + flex: 1; + display: inline-flex; + justify-content: flex-end; + white-space: nowrap; + font-size: @font-size-extra-small; + padding-left: 20px; + opacity: 0.5; +} diff --git a/eclipse-scout-core/src/table/columns/DateColumnTableHeaderMenu.ts b/eclipse-scout-core/src/table/columns/DateColumnTableHeaderMenu.ts new file mode 100644 index 00000000000..a701f6745b9 --- /dev/null +++ b/eclipse-scout-core/src/table/columns/DateColumnTableHeaderMenu.ts @@ -0,0 +1,388 @@ +/* + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +import { + Action, aria, ColumnUserFilter, ContextMenuPopup, DateColumn, DateColumnUserFilter, DateGroupType, dates, Event, icons, keys, Menu, MenuModel, scout, strings, TableHeaderMenu, TableHeaderMenuButton, TableHeaderMenuGroup, + TableHeaderMenuModel, TableMatrix +} from '../../index'; + +export class DateColumnTableHeaderMenu extends TableHeaderMenu implements DateColumnTableHeaderMenuModel { + declare model: DateColumnTableHeaderMenuModel; + declare column: DateColumn & DateColumnWithFilterType; + declare filter: DateColumnUserFilter; + + groupingGroupTypeAction: Action; + filterGroupTypeAction: Action; + + // ------------------------------------------------------------------ + // Grouping + // ------------------------------------------------------------------ + + protected override _renderGroupingGroup(): TableHeaderMenuGroup { + let group = super._renderGroupingGroup(); + + if (!this.column.hasDate) { + return group; // always use default grouping for time-only columns + } + + if (this.column.grouped) { + // Render an alternative group title in the form "Grouped by [group-type]", where the group type is a clickable link. + // The user can use this action to change the group type without having to ungroup and re-group the column. Clicking + // the group button again will remove the grouping. + let $textWithGroupType = group.$text.afterDiv('table-header-menu-group-text'); + $textWithGroupType.appendSpan().text(this.session.text('ui.GroupingBy') + ' '); + + let groupFormatPattern = this.column.groupFormat.pattern; + let groupType = this.column.groupType || this._getGroupTypeForFormatPattern(groupFormatPattern); + let groupTypeText = this._formatGroupType(groupType) ?? groupFormatPattern; + this.groupingGroupTypeAction = scout.create(Action, { + id: 'ChangeGroupTypeAction', + parent: this, + cssClass: 'table-header-menu-date-group-type-action app-link', + text: groupTypeText + }); + this.groupingGroupTypeAction.render($textWithGroupType); + aria.label(this.groupingGroupTypeAction.$container, strings.join(' ', this.session.text('ui.GroupingBy'), groupTypeText)); + this.groupingGroupTypeAction.on('action', event => this._onGroupButtonAction(event)); + + // Show either the normal $text and the $textWithAction, depending on whether there is a "current" item + const updateTextVisibility = () => { + group.$text.setVisible(!!group.currentGroupItem); + $textWithGroupType.setVisible(!group.currentGroupItem); + }; + updateTextVisibility(); + group.on('propertyChange:currentGroupItem', event => updateTextVisibility()); + + // Because the group type action is hidden when one of the buttons is focused, the normal backward + // tab traversal would skip it. Therefore, we intercept the shift-tab keystroke and make the action + // visible first, before manually focusing it. + group.$container.on('keydown', event => { + if (event.which === keys.TAB && event.shiftKey && !this.groupingGroupTypeAction.get$Focusable().is(event.target)) { + event.preventDefault(); + group.setCurrentGroupItem(null); + this.groupingGroupTypeAction.focus(); + } + }); + } + + // Because we want to open a context menu popup when the column is not yet grouped, we have to disable + // the default toggle action -> handle in _onGroupButtonAction(). + group.children.forEach(child => { + if (child instanceof TableHeaderMenuButton) { + child.setToggleAction(false); + child.addCssClass('togglable'); // needed for remove icon + } + }); + + return group; + } + + protected override _onGroupButtonAction(event: Event | Event) { + if (!this.column.hasDate) { + // always use default grouping for time-only columns + super._onGroupButtonAction(event as Event); + return; + } + + // This handler is either called from a TableHeaderMenuButton or the "change group type" action created in _renderGroupingGroup() + const anchor = event.source; + + let contextMenu = anchor.findChild(DateGroupTypeContextMenuPopup); + if (contextMenu) { + contextMenu.close(); + return; // toggle only -> done + } + + // If button was already selected, just ungroup the column (don't show the context menu) + if (anchor instanceof TableHeaderMenuButton && anchor.selected) { + this.table.removeGroupColumn(this.column); + this.close(); + return; + } + + // Create menus + let menus = this._createGroupTypeMenus(); + + let groupFormatPattern = this.column.groupFormat.pattern; + let groupFormatGroupType = this._getGroupTypeForFormatPattern(groupFormatPattern); + if (!groupFormatGroupType) { + // Show additional menu to change grouping according to a custom format + let specialGroupFormatMenu = scout.create(DateGroupTypeMenu, { + parent: this, + text: groupFormatPattern, + groupType: null, + hint: dates.format(new Date(), this.session.locale, groupFormatPattern) + }); + menus.unshift(specialGroupFormatMenu); + } + + const updateGrouping = (groupType: DateGroupType) => { + // Unless there is an active filter, reset the previously selected filter group type, so the next time + // we open the popup, the filter table will inherit this.column.groupType by default. + if (!this.filter.tableFilterActive()) { + this.column.__filterGroupType = null; + } + // Set group type and apply grouping + this.column.setGroupType(groupType, false); // false = don't apply grouping + this.table.group(this.column, undefined, anchor instanceof TableHeaderMenuButton ? anchor.additional : true); + this.close(); + }; + + let currentGroupType = this.column.groupType || groupFormatGroupType; + menus.forEach(menu => { + if (menu.groupType === currentGroupType) { + menu.setIconId(icons.CHECKED_BOLD); + } + menu.on('action', event => { + if (this.column.grouped && menu.groupType === currentGroupType) { + // already grouped by this type + this.close(); + return; + } + updateGrouping(menu.groupType); + }); + }); + + // Create context menu + contextMenu = scout.create(DateGroupTypeContextMenuPopup, { + parent: anchor, + menuItems: menus, + anchor: anchor + }); + if (anchor instanceof TableHeaderMenuButton) { + anchor.parent.setActiveGroupItem(anchor); + contextMenu.one('destroy', event => { + anchor.parent.setActiveGroupItem(null); + }); + } + contextMenu.open(); + } + + // ------------------------------------------------------------------ + // Filter + // ------------------------------------------------------------------ + + protected override _createFilter(): ColumnUserFilter { + let filter = super._createFilter() as DateColumnUserFilter; + + // Initialize the filter with the previously selected group type or the column group type + filter.groupType = this.column.__filterGroupType || this.column.groupType; + + return filter; + } + + protected override _renderFilterTable(): JQuery { + let $filterTable = super._renderFilterTable(); + + if (!this.column.hasDate) { + return $filterTable; // always use default grouping for time-only columns + } + + // Create an additional action to change the group type in the filter table (only if there are at least two menus to choose from) + this.filterGroupTypeAction = scout.create(Action, { + id: 'FilterChangeGroupTypeAction', + parent: this, + tooltipText: this.session.text('GroupBy'), + cssClass: 'button borderless table-header-menu-filter-date-group-type' + }); + this.filterGroupTypeAction.on('action', this._onFilterChangeGroupTypeAction.bind(this)); + + this.filterGroupTypeAction.render(this.filterSortOrderAction.$parent); + this.filterGroupTypeAction.$container.insertBefore(this.filterSortOrderAction.$container); + + return $filterTable; + } + + protected _onFilterChangeGroupTypeAction(event: Event) { + const anchor = event.source; + + let contextMenu = anchor.findChild(DateGroupTypeContextMenuPopup); + if (contextMenu) { + contextMenu.close(); + return; // toggle only -> done + } + + // Create menus + let menus = this._createGroupTypeMenus(); + + const updateFilter = (groupType: DateGroupType) => { + this.column.__filterGroupType = groupType; + this.filter.groupType = groupType; + this.filter.selectedValues = []; + this.filter.calculate(); + this._updateTableFilter(); + this._reloadFilterTable(); + }; + + let currentGroupType = this.filter.groupType ?? DateGroupType.YEAR; + menus.forEach(menu => { + if (menu.groupType === currentGroupType) { + menu.setIconId(icons.CHECKED_BOLD); + } + menu.on('action', event => { + if (menu.groupType === currentGroupType) { + // already grouped by this type + return; + } + updateFilter(menu.groupType); + }); + }); + + // Create context menu + contextMenu = scout.create(DateGroupTypeContextMenuPopup, { + parent: anchor, + menuItems: menus, + anchor: anchor + }); + anchor.$container?.addClass('selected has-popup'); + contextMenu.one('destroy', event => { + anchor.$container?.removeClass('selected has-popup'); + }); + contextMenu.open(); + } + + // ------------------------------------------------------------------ + // Helpers + // ------------------------------------------------------------------ + + protected _createGroupTypeMenus(): DateGroupTypeMenu[] { + const groupTypes = [ + DateGroupType.YEAR, + DateGroupType.MONTH, + DateGroupType.MONTH_AND_YEAR, + DateGroupType.CALENDAR_WEEK, + DateGroupType.WEEKDAY, + DateGroupType.DATE + ]; + return groupTypes.map(groupType => scout.create(DateGroupTypeMenu, { + parent: this, + text: this._formatGroupType(groupType), + groupType: groupType, + hint: this._formatGroupTypeHint(groupType) + })); + } + + /** + * @returns the display name for the given {@link DateGroupType}. + */ + protected _formatGroupType(groupType: DateGroupType): string { + switch (groupType) { + case DateGroupType.YEAR: + return this.session.text('DateGroupTypeYear'); + case DateGroupType.MONTH: + return this.session.text('DateGroupTypeMonth'); + case DateGroupType.MONTH_AND_YEAR: + return this.session.text('DateGroupTypeMonthAndYear'); + case DateGroupType.CALENDAR_WEEK: + return this.session.text('DateGroupTypeWeekOfYear'); + case DateGroupType.WEEKDAY: + return this.session.text('DateGroupTypeWeekday'); + case DateGroupType.DATE: + return this.session.text('DateGroupTypeDate'); + } + return groupType || null; + } + + /** + * Returns the corresponding {@link DateGroupType} value if its format exactly matches the given date format pattern. + * Otherwise, null is returned. + */ + protected _getGroupTypeForFormatPattern(formatPattern: string): DateGroupType { + switch (formatPattern) { + case 'yyyy': + return DateGroupType.YEAR; + case 'MMMM': + return DateGroupType.MONTH; + case 'MMMM yyyy': + return DateGroupType.MONTH_AND_YEAR; + case 'EEEE': + return DateGroupType.WEEKDAY; + case this.session.locale.dateFormatPatternDefault: + return DateGroupType.DATE; + } + return null; + } + + protected _formatGroupTypeHint(groupType: DateGroupType): string { + let group = TableMatrix.resolveDateGroup(groupType); + if (group) { + let matrix = new TableMatrix(this.table); + let axis = matrix.addAxis(this.column, group); + return axis.format(axis.norm(new Date())); + } + return null; + } +} + +export interface DateColumnTableHeaderMenuModel extends TableHeaderMenuModel { + column?: DateColumn; +} + +export interface DateGroupTypeMenuModel extends MenuModel { + groupType: DateGroupType; + hint?: string; +} + +export class DateGroupTypeMenu extends Menu { + declare model: DateGroupTypeMenuModel; + + groupType: DateGroupType; + hint: string; + + $hint: JQuery; + + protected override _render() { + super._render(); + this.$container.addClass('date-group-type-menu'); + } + + protected override _renderProperties() { + super._renderProperties(); + this._renderHint(); + } + + setHint(hint: string) { + this.setProperty('hint', hint); + } + + protected _renderHint() { + if (this.hint) { + this.$hint = this.$hint || this.$container.appendSpan('hint'); + this.$hint.text(this.hint); + } else { + this.$hint?.remove(); + this.$hint = null; + } + } + + protected override _renderText() { + super._renderText(); + // Ensure $hint is positioned after $text + if (this.$text && this.$hint) { + this.$hint.insertAfter(this.$text); + } + } +} + +interface DateColumnWithFilterType { + /** + * Temporarily holds the selected group type of the filter table when no filter is active. + * Allows restoring the previous group type when the header menu is opened again. + */ + __filterGroupType: DateGroupType; +} + +export class DateGroupTypeContextMenuPopup extends ContextMenuPopup { + + constructor() { + super(); + this.cloneMenuItems = false; + this.closeOnAnchorMouseDown = false; // we use our own toggle logic + } +} diff --git a/eclipse-scout-core/src/table/userfilter/ColumnUserFilter.ts b/eclipse-scout-core/src/table/userfilter/ColumnUserFilter.ts index 156782da191..e4df220cfff 100644 --- a/eclipse-scout-core/src/table/userfilter/ColumnUserFilter.ts +++ b/eclipse-scout-core/src/table/userfilter/ColumnUserFilter.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2025 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -42,7 +42,7 @@ export class ColumnUserFilter extends TableUserFilter implements ColumnUserFilte calculate() { let containsSelectedValue, reorderAxis; - this.matrix = new TableMatrix(this.table, this.session); + this.matrix = new TableMatrix(this.table); this.matrix.addData(this.column, TableMatrix.NumberGroup.COUNT); this.xAxis = this.matrix.addAxis(this.column, this.axisGroup()); let cube = this.matrix.calculate(); @@ -75,7 +75,7 @@ export class ColumnUserFilter extends TableUserFilter implements ColumnUserFilte if (key !== null && this.xAxis.isIcon) { // Only display icon if isIcon (still display empty text if key is null) iconId = text; - text = null; + text = ''; // not null! otherwise, the column would fall back to the value as cell text } let cubeValue = cube.getValue([key]); this.availableValues.push({ @@ -202,6 +202,7 @@ export class ColumnUserFilter extends TableUserFilter implements ColumnUserFilte } export type ColumnUserFilterValues = { + /** persistable key (aka "deterministic key") */ key?: string | number; text?: string; iconId?: string; diff --git a/eclipse-scout-core/src/table/userfilter/ColumnUserFilterModel.ts b/eclipse-scout-core/src/table/userfilter/ColumnUserFilterModel.ts index 26b1fed1a76..0a752b9a470 100644 --- a/eclipse-scout-core/src/table/userfilter/ColumnUserFilterModel.ts +++ b/eclipse-scout-core/src/table/userfilter/ColumnUserFilterModel.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -19,12 +19,12 @@ export interface ColumnUserFilterModel extends TableUserFilterModel { hasFilterFields?: boolean; /** - * array of (normalized) key, text composite + * array of key/text composites ({@link ColumnUserFilterValues)} */ availableValues?: ColumnUserFilterValues[]; /** - * array of (normalized) keys + * array of persistable keys (aka "deterministic keys") */ selectedValues?: (string | number)[]; } diff --git a/eclipse-scout-core/src/table/userfilter/DateColumnUserFilter.ts b/eclipse-scout-core/src/table/userfilter/DateColumnUserFilter.ts index 9d7ae7a2afc..856d89e8e3a 100644 --- a/eclipse-scout-core/src/table/userfilter/DateColumnUserFilter.ts +++ b/eclipse-scout-core/src/table/userfilter/DateColumnUserFilter.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2025 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -8,8 +8,8 @@ * SPDX-License-Identifier: EPL-2.0 */ import { - aria, ColumnUserFilter, ColumnUserFilterModel, DateColumn, DateColumnUserFilterModel, DateField, dates, FilterFieldsGroupBox, InitModelOf, PropertyChangeEvent, TableMatrix, TableMatrixDateGroup, TableMatrixNumberGroup, TableRow, - TableUserFilterAddedEventData + aria, ColumnUserFilter, ColumnUserFilterModel, DateColumn, DateColumnUserFilterModel, DateField, DateGroupType, dates, FilterFieldsGroupBox, InitModelOf, PropertyChangeEvent, TableMatrix, TableMatrixDateGroup, TableMatrixNumberGroup, + TableRow, TableUserFilterAddedEventData } from '../../index'; import $ from 'jquery'; @@ -17,19 +17,25 @@ export class DateColumnUserFilter extends ColumnUserFilter implements ColumnUser declare model: DateColumnUserFilterModel; declare column: DateColumn; + groupType: DateGroupType; + dateFrom: Date; dateTo: Date; + dateFromField: DateField; dateToField: DateField; constructor() { super(); + this.groupType = null; + this.dateFrom = null; - this.dateFromField = null; this.dateTo = null; - this.dateToField = null; + this.hasFilterFields = true; + this.dateFromField = null; + this.dateToField = null; } protected override _init(model: InitModelOf) { @@ -39,16 +45,16 @@ export class DateColumnUserFilter extends ColumnUserFilter implements ColumnUser } override axisGroup(): TableMatrixNumberGroup | TableMatrixDateGroup { - if (this.column.hasDate) { - // Default grouping for date columns is year - return TableMatrix.DateGroup.YEAR; + if (!this.column.hasDate) { + // No grouping for time columns + return TableMatrix.DateGroup.NONE; } - // No grouping for time columns - return TableMatrix.DateGroup.NONE; + return TableMatrix.resolveDateGroup(this.groupType) ?? TableMatrix.DateGroup.YEAR; } override createFilterAddedEventData(): TableUserFilterAddedEventData { - let data = super.createFilterAddedEventData(); + let data = super.createFilterAddedEventData() as DateColumnTableUserFilterAddedEventData; + data.groupType = this.groupType; data.dateFrom = dates.toJsonDate(this.dateFrom); data.dateTo = dates.toJsonDate(this.dateTo); return data; @@ -67,7 +73,7 @@ export class DateColumnUserFilter extends ColumnUserFilter implements ColumnUser let keyValue = key.valueOf(), fromValue = this.dateFrom ? this.dateFrom.valueOf() : null, - // Shift the toValue to 1ms before midnight/next day. Thus any time of the selected day is accepted. + // Shift the toValue to 1ms before midnight/next day. Thus, any time of the selected day is accepted. toValue = this.dateTo ? dates.shift(this.dateTo, 0, 0, 1).valueOf() - 1 : null; if (fromValue && toValue) { @@ -120,7 +126,7 @@ export class DateColumnUserFilter extends ColumnUserFilter implements ColumnUser protected _onInput(event: JQuery.TriggeredEvent) { if (!this.dateFromField.rendered) { - // popup has been closed in the mean time + // popup has been closed in the meantime return; } this.dateFrom = this.dateFromField.value; @@ -128,3 +134,9 @@ export class DateColumnUserFilter extends ColumnUserFilter implements ColumnUser this.triggerFilterFieldsChanged(); } } + +export interface DateColumnTableUserFilterAddedEventData extends TableUserFilterAddedEventData { + groupType?: DateGroupType; + dateFrom?: string; + dateTo?: string; +} diff --git a/eclipse-scout-core/src/table/userfilter/DateColumnUserFilterModel.ts b/eclipse-scout-core/src/table/userfilter/DateColumnUserFilterModel.ts index 34ab2a547f4..b7ec7f0140d 100644 --- a/eclipse-scout-core/src/table/userfilter/DateColumnUserFilterModel.ts +++ b/eclipse-scout-core/src/table/userfilter/DateColumnUserFilterModel.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2025 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -7,9 +7,10 @@ * * SPDX-License-Identifier: EPL-2.0 */ -import {ColumnUserFilterModel} from '../../index'; +import {ColumnUserFilterModel, DateGroupType} from '../../index'; export interface DateColumnUserFilterModel extends ColumnUserFilterModel { - dateFrom?: string | Date; - dateTo?: string | Date; + groupType?: DateGroupType; + dateFrom?: Date | string; + dateTo?: Date | string; } diff --git a/eclipse-scout-core/src/table/userfilter/DateColumnUserFilterStateMapper.ts b/eclipse-scout-core/src/table/userfilter/DateColumnUserFilterStateMapper.ts index e8ecc85bfc0..e4267b4d478 100644 --- a/eclipse-scout-core/src/table/userfilter/DateColumnUserFilterStateMapper.ts +++ b/eclipse-scout-core/src/table/userfilter/DateColumnUserFilterStateMapper.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2025 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -7,7 +7,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -import {DateColumnUserFilter, DateColumnUserFilterStateDo, dates, IUserFilterStateDo, scout, Table, TableUserFilter, UserFilterStateMapper, UserFilterStateMappers} from '../../index'; +import {DateColumnUserFilter, DateColumnUserFilterStateDo, IUserFilterStateDo, scout, Table, TableUserFilter, UserFilterStateMapper, UserFilterStateMappers} from '../../index'; export class DateColumnUserFilterStateMapper extends UserFilterStateMapper { @@ -23,6 +23,7 @@ export class DateColumnUserFilterStateMapper extends UserFilterStateMapper { model.backgroundJobPollingEnabled = false; model.suppressErrors = true; model.$entryPoint = $sandbox; + model.locale = options.locale || new LocaleSpecHelper().createLocale('de-CH'); let session = scout.create(Session, model) as SandboxSession; $sandbox.data('sandboxSession', session); @@ -88,7 +89,6 @@ window.sandboxSession = options => { // Simulate successful session initialization session.uiSessionId = '1.1'; session.modelAdapterRegistry[session.uiSessionId] = session; - session.locale = new LocaleSpecHelper().createLocale('de-CH'); let desktop = (options.desktop || {}) as InitModelOf; desktop.navigationVisible = scout.nvl(desktop.navigationVisible, false); diff --git a/eclipse-scout-core/src/testing/table/TableSpecHelper.ts b/eclipse-scout-core/src/testing/table/TableSpecHelper.ts index a245f14f5bf..fb3b0627b81 100644 --- a/eclipse-scout-core/src/testing/table/TableSpecHelper.ts +++ b/eclipse-scout-core/src/testing/table/TableSpecHelper.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2025 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -188,7 +188,7 @@ export class TableSpecHelper { return this.createModel(this.createModelColumns(1), rows); } - createModelSingleColumnByValues(values: any[], columnType: ObjectType): SpecTableModel { + createModelSingleColumnByValues(values: any[], columnType: ObjectType>): SpecTableModel { let rows = []; for (let i = 0; i < values.length; i++) { rows.push(this.createModelRowByValues(null, values[i])); diff --git a/eclipse-scout-core/test/menu/ContextMenuPopupSpec.ts b/eclipse-scout-core/test/menu/ContextMenuPopupSpec.ts index 468c27322d2..97f7376c739 100644 --- a/eclipse-scout-core/test/menu/ContextMenuPopupSpec.ts +++ b/eclipse-scout-core/test/menu/ContextMenuPopupSpec.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2025 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -95,6 +95,47 @@ describe('ContextMenuPopup', () => { expect(childMenu.destroyed).toBe(false); }); + it('does not change the parent of the original menus', () => { + expect(menu.owner).toBe(session.desktop); + expect(menu.parent).toBe(session.desktop); + popup = scout.create(ContextMenuPopup, { + parent: session.desktop, + session: session, + menuItems: [menu], + cloneMenuItems: true + }); + expect(menu.owner).toBe(session.desktop); + expect(menu.parent).toBe(session.desktop); + expect(popup.menuItems).toContain(menu); + expect(popup.children).not.toContain(menu); + expect(popup.findChild(Menu)).toBe(null); + popup.render(); + expect(popup.children).not.toContain(menu); + expect(popup.findChild(Menu)).toBeInstanceOf(Menu); + expect(popup.findChild(Menu).cloneOf).toBe(menu); + + class SpecContextMenuPopup extends ContextMenuPopup { + constructor() { + super(); + this.cloneMenuItems = true; + } + } + + let popup2 = scout.create(SpecContextMenuPopup, { + parent: session.desktop, + session: session, + menuItems: [menu] + }); + expect(menu.owner).toBe(session.desktop); + expect(menu.parent).toBe(session.desktop); + expect(popup2.menuItems).toContain(menu); + expect(popup2.children).not.toContain(menu); + expect(popup2.findChild(Menu)).toBe(null); + popup2.render(); + expect(popup2.children).not.toContain(menu); + expect(popup2.findChild(Menu)).toBeInstanceOf(Menu); + expect(popup2.findChild(Menu).cloneOf).toBe(menu); + }); }); describe('false', () => { @@ -151,6 +192,40 @@ describe('ContextMenuPopup', () => { popup.render(); popup.destroy(); }); + + it('changes the parent of the original menus', () => { + expect(menu.owner).toBe(session.desktop); + expect(menu.parent).toBe(session.desktop); + popup = scout.create(ContextMenuPopup, { + parent: session.desktop, + session: session, + menuItems: [menu], + cloneMenuItems: false + }); + expect(menu.owner).toBe(session.desktop); + expect(menu.parent).toBe(popup); + expect(popup.menuItems).toContain(menu); + expect(popup.children).toContain(menu); + expect(popup.findChild(Menu).cloneOf).toBe(null); + + class SpecContextMenuPopup extends ContextMenuPopup { + constructor() { + super(); + this.cloneMenuItems = false; + } + } + + let popup2 = scout.create(SpecContextMenuPopup, { + parent: session.desktop, + session: session, + menuItems: [menu] + }); + expect(menu.owner).toBe(session.desktop); + expect(menu.parent).toBe(popup2); + expect(popup2.menuItems).toContain(menu); + expect(popup2.children).toContain(menu); + expect(popup2.findChild(Menu).cloneOf).toBe(null); + }); }); }); diff --git a/eclipse-scout-core/test/table/DateColumnTableHeaderMenuSpec.ts b/eclipse-scout-core/test/table/DateColumnTableHeaderMenuSpec.ts new file mode 100644 index 00000000000..8286950da61 --- /dev/null +++ b/eclipse-scout-core/test/table/DateColumnTableHeaderMenuSpec.ts @@ -0,0 +1,1006 @@ +/* + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +import {Action, Column, ContextMenuPopup, DateColumn, DateColumnTableHeaderMenu, DateColumnUserFilter, DateGroupType, DateGroupTypeMenu, dates, icons, scout, Table, TableHeaderMenu, TableHeaderMenuGroup, TableRow} from '../../src/index'; +import {JQueryTesting, TableSpecHelper} from '../../src/testing/index'; + +describe('DateColumnTableHeaderMenu', () => { + let session: SandboxSession; + let helper: TableSpecHelper; + + beforeEach(() => { + setFixtures(sandbox()); + session = sandboxSession(); + helper = new TableSpecHelper(session); + + session.textMap.add('ColumnSorting', 'Sorting'); + session.textMap.add('ui.ascending', 'ascending'); + session.textMap.add('ui.EmptyCell', '-empty-'); + session.textMap.add('ui.CW', 'CW {0}'); + session.textMap.add('DateGroupTypeDate', 'Date'); + session.textMap.add('DateGroupTypeMonth', 'Month'); + session.textMap.add('DateGroupTypeMonthAndYear', 'Month and year'); + session.textMap.add('DateGroupTypeWeekOfYear', 'Week of year'); + session.textMap.add('DateGroupTypeWeekday', 'Weekday'); + session.textMap.add('DateGroupTypeYear', 'Year'); + session.textMap.add('ui.Grouping', 'Grouping'); + session.textMap.add('ui.GroupingBy', 'Grouping by'); + session.textMap.add('ui.remove', 'remove'); + session.textMap.add('ui.groupingApply', 'apply'); + }); + + afterEach(() => { + // Destroy all widgets, including still open popups and their global 'mouse down outside' listeners. + session.desktop.destroy(); + }); + + describe('filter', () => { + let table: Table; + let dateColumn: DateColumn; + + beforeEach(() => { + table = helper.createTable(helper.createModelSingleColumnByValues([ + dates.create('2028-03-26'), // Sun + null, + dates.create('2008-04-14'), // Mon + null, + dates.create('2008-02-12'), // Tue + dates.create('2016-02-16') // Tue + ], DateColumn)); + table.render(); + dateColumn = scout.assertInstance(table.columns[0], DateColumn); + }); + + it('shows the dates formatted with the select group type', () => { + table.header.openHeaderMenu(dateColumn); + let headerMenu = scout.assertInstance(table.header.tableHeaderMenu, DateColumnTableHeaderMenu); + headerMenu.animateRemoval = false; + let filterTable = headerMenu.filterTable; + + let filterItems = filterTable.rows.map(row => filterTable.columns[0].cellText(row)); + expect(filterItems).toEqual([ + '2008', + '2016', + '2028', + '-empty-' + ]); + + // -------- + + const checkCurrentGroupType = (expectedGroupType: DateGroupType) => { + expect(headerMenu.filterGroupTypeAction).toBeInstanceOf(Action); + headerMenu.filterGroupTypeAction.doAction(); + + let contextMenu = headerMenu.findChild(ContextMenuPopup); + expect(contextMenu).toBeInstanceOf(ContextMenuPopup); + contextMenu.animateRemoval = false; + + let menus = contextMenu.menuItems.filter(menu => menu instanceof DateGroupTypeMenu) as DateGroupTypeMenu[]; + expect(menus.map(menu => menu.groupType)).toEqual([ + DateGroupType.YEAR, + DateGroupType.MONTH, + DateGroupType.MONTH_AND_YEAR, + DateGroupType.CALENDAR_WEEK, + DateGroupType.WEEKDAY, + DateGroupType.DATE + ]); + let checkedMenus = menus.filter(menu => menu.iconId === icons.CHECKED_BOLD); + expect(checkedMenus.length).toBe(1); + expect(checkedMenus[0].groupType).toBe(expectedGroupType); + + // Toggle + headerMenu.filterGroupTypeAction.doAction(); + expect(contextMenu.destroyed).toBe(true); + }; + + const changeGroupTypeAndCheckFilterItems = (groupType: DateGroupType, expectedFilterItems: string[]) => { + expect(headerMenu.filterGroupTypeAction).toBeInstanceOf(Action); + headerMenu.filterGroupTypeAction.doAction(); + + let contextMenu = headerMenu.findChild(ContextMenuPopup); + expect(contextMenu).toBeInstanceOf(ContextMenuPopup); + contextMenu.animateRemoval = false; + + let menus = contextMenu.menuItems.filter(menu => menu instanceof DateGroupTypeMenu) as DateGroupTypeMenu[]; + + let targetMenu = menus.find(menu => menu.groupType === groupType); + expect(targetMenu).toBeInstanceOf(DateGroupTypeMenu); + targetMenu.doAction(); + expect(contextMenu.destroyed).toBe(true); + + let filterItems = filterTable.rows.map(row => filterTable.columns[0].cellText(row)); + expect(filterItems).toEqual(expectedFilterItems); + }; + + // -------- + + checkCurrentGroupType(DateGroupType.YEAR); + changeGroupTypeAndCheckFilterItems(DateGroupType.YEAR, ['2008', '2016', '2028', '-empty-']); + checkCurrentGroupType(DateGroupType.YEAR); + changeGroupTypeAndCheckFilterItems(DateGroupType.MONTH, ['Februar', 'März', 'April', '-empty-']); + checkCurrentGroupType(DateGroupType.MONTH); + changeGroupTypeAndCheckFilterItems(DateGroupType.MONTH_AND_YEAR, ['Februar 2008', 'April 2008', 'Februar 2016', 'März 2028', '-empty-']); + checkCurrentGroupType(DateGroupType.MONTH_AND_YEAR); + changeGroupTypeAndCheckFilterItems(DateGroupType.CALENDAR_WEEK, ['CW 7', 'CW 12', 'CW 16', '-empty-']); + checkCurrentGroupType(DateGroupType.CALENDAR_WEEK); + changeGroupTypeAndCheckFilterItems(DateGroupType.WEEKDAY, ['Montag', 'Dienstag', 'Sonntag', '-empty-']); + checkCurrentGroupType(DateGroupType.WEEKDAY); + changeGroupTypeAndCheckFilterItems(DateGroupType.DATE, ['12.02.2008', '14.04.2008', '16.02.2016', '26.03.2028', '-empty-']); + checkCurrentGroupType(DateGroupType.DATE); + }); + + it('reflects the state of the filter', () => { + let filter = dateColumn.createFilter() as DateColumnUserFilter; + filter.groupType = DateGroupType.MONTH; + filter.selectedValues = [1, 7, null]; + table.addFilter(filter); + expect(table.filteredRows().length).toBe(4); + + table.header.openHeaderMenu(dateColumn); + let headerMenu = scout.assertInstance(table.header.tableHeaderMenu, DateColumnTableHeaderMenu); + headerMenu.animateRemoval = false; + let filterTable = headerMenu.filterTable; + + // -------- + + let filterItems = filterTable.rows.map(row => filterTable.columns[0].cellText(row)); + expect(filterItems).toEqual([ + 'Februar', + 'März', + 'April', + 'August', + '-empty-' + ]); + let checkedItems = filterTable.checkedRows().map(row => filterTable.columns[0].cellText(row)); + expect(checkedItems).toEqual([ + 'Februar', + 'August', + '-empty-' + ]); + }); + + it('saves the selected group type in the filter', () => { + expect(dateColumn.filtered).toBe(false); + expect(table.getFilter(dateColumn.id)).toBe(null); + + table.header.openHeaderMenu(dateColumn); + let headerMenu = scout.assertInstance(table.header.tableHeaderMenu, DateColumnTableHeaderMenu); + headerMenu.animateRemoval = false; + let filterTable = headerMenu.filterTable; + + // -------- + + expect(headerMenu.filterGroupTypeAction).toBeInstanceOf(Action); + headerMenu.filterGroupTypeAction.doAction(); + + let contextMenu = headerMenu.findChild(ContextMenuPopup); + expect(contextMenu).toBeInstanceOf(ContextMenuPopup); + contextMenu.animateRemoval = false; + + let menus = contextMenu.menuItems.filter(menu => menu instanceof DateGroupTypeMenu) as DateGroupTypeMenu[]; + + let targetMenu = menus.find(menu => menu.groupType === DateGroupType.WEEKDAY); + expect(targetMenu).toBeInstanceOf(DateGroupTypeMenu); + targetMenu.doAction(); + expect(contextMenu.destroyed).toBe(true); + + // -------- + + expect(dateColumn.filtered).toBe(false); + expect(table.getFilter(dateColumn.id)).toBe(null); + let itemsToCheck = filterTable.rows.filter(row => scout.isOneOf(filterTable.columns[0].cellText(row), 'Dienstag', '-empty-')); + filterTable.checkRows(itemsToCheck); + + expect(dateColumn.filtered).toBe(true); + expect(table.filteredRows().length).toBe(4); + let filter = table.getFilter(dateColumn.id) as DateColumnUserFilter; + expect(filter).toBeInstanceOf(DateColumnUserFilter); + expect(filter.selectedValues).toEqual([2, null]); + expect(filter.groupType).toBe(DateGroupType.WEEKDAY); + + table.header.closeHeaderMenu(); + + // -------- + + // Open again + table.header.openHeaderMenu(dateColumn); + headerMenu = scout.assertInstance(table.header.tableHeaderMenu, DateColumnTableHeaderMenu); + headerMenu.animateRemoval = false; + filterTable = headerMenu.filterTable; + + let checkedItems = filterTable.checkedRows().map(row => filterTable.columns[0].cellText(row)); + expect(checkedItems).toEqual([ + 'Dienstag', + '-empty-' + ]); + }); + + it('preserves the last selected group type even if no filter is selected', () => { + table.header.openHeaderMenu(dateColumn); + let headerMenu = scout.assertInstance(table.header.tableHeaderMenu, DateColumnTableHeaderMenu); + headerMenu.animateRemoval = false; + let filterTable = headerMenu.filterTable; + + // -------- + + expect(headerMenu.filterGroupTypeAction).toBeInstanceOf(Action); + headerMenu.filterGroupTypeAction.doAction(); + + let contextMenu = headerMenu.findChild(ContextMenuPopup); + expect(contextMenu).toBeInstanceOf(ContextMenuPopup); + contextMenu.animateRemoval = false; + + let menus = contextMenu.menuItems.filter(menu => menu instanceof DateGroupTypeMenu) as DateGroupTypeMenu[]; + + let targetMenu = menus.find(menu => menu.groupType === DateGroupType.CALENDAR_WEEK); + expect(targetMenu).toBeInstanceOf(DateGroupTypeMenu); + targetMenu.doAction(); + expect(contextMenu.destroyed).toBe(true); + + // -------- + + let filterItems = filterTable.rows.map(row => filterTable.columns[0].cellText(row)); + expect(filterItems).toEqual([ + 'CW 7', + 'CW 12', + 'CW 16', + '-empty-' + ]); + + table.header.closeHeaderMenu(); + + // -------- + + // Open again + table.header.openHeaderMenu(dateColumn); + headerMenu = scout.assertInstance(table.header.tableHeaderMenu, DateColumnTableHeaderMenu); + headerMenu.animateRemoval = false; + filterTable = headerMenu.filterTable; + + filterItems = filterTable.rows.map(row => filterTable.columns[0].cellText(row)); + expect(filterItems).toEqual([ + 'CW 7', + 'CW 12', + 'CW 16', + '-empty-' + ]); + + // -------- + + expect(headerMenu.filterGroupTypeAction).toBeInstanceOf(Action); + headerMenu.filterGroupTypeAction.doAction(); + + contextMenu = headerMenu.findChild(ContextMenuPopup); + expect(contextMenu).toBeInstanceOf(ContextMenuPopup); + contextMenu.animateRemoval = false; + + menus = contextMenu.menuItems.filter(menu => menu instanceof DateGroupTypeMenu) as DateGroupTypeMenu[]; + let checkedMenus = menus.filter(menu => menu.iconId === icons.CHECKED_BOLD); + expect(checkedMenus.length).toBe(1); + expect(checkedMenus[0].groupType).toBe(DateGroupType.CALENDAR_WEEK); + }); + + it('does not allow changing the group type if column.hasDate=false', () => { + dateColumn.hasDate = false; + dateColumn.setFormat(null); // recreate default format + dateColumn.setGroupFormat('HH'); + table.deleteAllRows(); + table.insertRows([ + {id: 'row0', cells: [dates.create('2022-03-26 17:17:17.000')]}, + {id: 'row1', cells: [dates.create('2022-03-10 17:17:17.000')]}, + {id: 'row2', cells: [dates.create('2022-03-09 18:18:18.000')]}, + {id: 'row3', cells: [null]}, + {id: 'row4', cells: [dates.create('2022-03-10 17:17:33.000')]}, + {id: 'row5', cells: [dates.create('2028-01-01 10:01:00.000')]} + ]); + + // -------- + + table.header.openHeaderMenu(dateColumn); + let headerMenu = scout.assertInstance(table.header.tableHeaderMenu, DateColumnTableHeaderMenu); + headerMenu.animateRemoval = false; + let filterTable = headerMenu.filterTable; + + expect(headerMenu.filterGroupTypeAction).toBe(undefined); + + let filterItems = filterTable.rows.map(row => filterTable.columns[0].cellText(row)); + expect(filterItems).toEqual([ + '18:18', + '17:17', + '17:17', + '17:17', + '10:01', + '-empty-' + ]); + }); + }); + + describe('grouping / sorting', () => { + + let table: Table; + let dateColumn1: DateColumn; + let dateColumn2: DateColumn; + let stringColumn1: Column; + let stringColumn2: Column; + let row0: TableRow; + let row1: TableRow; + let row2: TableRow; + let row3: TableRow; + + beforeEach(() => { + table = helper.createTable(helper.createModel( + [ + helper.createModelColumn('col0', DateColumn), + helper.createModelColumn('col1', DateColumn), + helper.createModelColumn('col2', Column), + helper.createModelColumn('col3', Column) + ], + [ + // (Sun, CW 12) | (Mon, CW 8) + helper.createModelRowByValues('row0', [dates.create('2028-03-26'), dates.create('2028-02-21'), 'aaa', 'zzz']), + // (Mon, CW 16) | null + helper.createModelRowByValues('row1', [dates.create('2008-04-14'), null, 'aaa', 'zzz']), + // (Tue, CW 7) | (Wed, CW 8) + helper.createModelRowByValues('row2', [dates.create('2008-02-12'), dates.create('2008-02-20'), 'bbb', 'yyy']), + // (Tue, CW 7) | null + helper.createModelRowByValues('row3', [dates.create('2016-02-16'), null, 'aaa', 'xxx']) + ] + )); + table.render(); + dateColumn1 = scout.assertInstance(table.columns[0], DateColumn); + dateColumn2 = scout.assertInstance(table.columns[1], DateColumn); + stringColumn1 = scout.assertInstance(table.columns[2], Column); + stringColumn2 = scout.assertInstance(table.columns[3], Column); + row0 = scout.assertInstance(table.rows[0], TableRow); + row1 = scout.assertInstance(table.rows[1], TableRow); + row2 = scout.assertInstance(table.rows[2], TableRow); + row3 = scout.assertInstance(table.rows[3], TableRow); + }); + + it('shows a popup to define the date group type', () => { + table.header.openHeaderMenu(dateColumn1); + let headerMenu = scout.assertInstance(table.header.tableHeaderMenu, DateColumnTableHeaderMenu); + headerMenu.animateRemoval = false; + + expect(headerMenu.groupButton.visible).toBe(true); + expect(headerMenu.groupButton.selected).toBe(false); + expect(headerMenu.groupAddButton.visible).toBe(false); + + headerMenu.groupButton.doAction(); + let contextMenu = headerMenu.findChild(ContextMenuPopup); + expect(contextMenu).toBeInstanceOf(ContextMenuPopup); + contextMenu.animateRemoval = false; + + let menus = contextMenu.menuItems.filter(menu => menu instanceof DateGroupTypeMenu) as DateGroupTypeMenu[]; + expect(menus.map(menu => menu.groupType)).toEqual([ + DateGroupType.YEAR, + DateGroupType.MONTH, + DateGroupType.MONTH_AND_YEAR, + DateGroupType.CALENDAR_WEEK, + DateGroupType.WEEKDAY, + DateGroupType.DATE + ]); + let checkedMenus = menus.filter(menu => menu.iconId === icons.CHECKED_BOLD); + expect(checkedMenus.length).toBe(1); + expect(checkedMenus[0].groupType).toBe(DateGroupType.YEAR); + + // Toggle + headerMenu.groupButton.doAction(); + expect(contextMenu.destroyed).toBe(true); + }); + + it('shows a popup to define the date group type for additional columns', () => { + table.group(dateColumn1); + + table.header.openHeaderMenu(dateColumn2); + let headerMenu = scout.assertInstance(table.header.tableHeaderMenu, DateColumnTableHeaderMenu); + headerMenu.animateRemoval = false; + + expect(headerMenu.groupButton.visible).toBe(true); + expect(headerMenu.groupButton.selected).toBe(false); + expect(headerMenu.groupAddButton.visible).toBe(true); + expect(headerMenu.groupAddButton.selected).toBe(false); + + headerMenu.groupButton.doAction(); + let contextMenu1 = headerMenu.findChild(ContextMenuPopup); + expect(contextMenu1).toBeInstanceOf(ContextMenuPopup); + contextMenu1.animateRemoval = false; + + headerMenu.groupAddButton.doAction(); + let contextMenu2 = headerMenu.findChild(ContextMenuPopup); + expect(contextMenu2).toBeInstanceOf(ContextMenuPopup); + expect(contextMenu1.destroyed).toBe(true); + contextMenu2.animateRemoval = false; + + let menus = contextMenu2.menuItems.filter(menu => menu instanceof DateGroupTypeMenu) as DateGroupTypeMenu[]; + expect(menus.map(menu => menu.groupType)).toEqual([ + DateGroupType.YEAR, + DateGroupType.MONTH, + DateGroupType.MONTH_AND_YEAR, + DateGroupType.CALENDAR_WEEK, + DateGroupType.WEEKDAY, + DateGroupType.DATE + ]); + let checkedMenus = menus.filter(menu => menu.iconId === icons.CHECKED_BOLD); + expect(checkedMenus.length).toBe(1); + expect(checkedMenus[0].groupType).toBe(DateGroupType.YEAR); + + // Toggle + headerMenu.groupButton.doAction(); + expect(contextMenu2.destroyed).toBe(true); + }); + + it('groups immediately without popup for non-date columns', () => { + table.header.openHeaderMenu(stringColumn1); + let headerMenu = scout.assertInstance(table.header.tableHeaderMenu, TableHeaderMenu); + headerMenu.animateRemoval = false; + + expect(headerMenu.groupButton.visible).toBe(true); + expect(headerMenu.groupButton.selected).toBe(false); + expect(headerMenu.groupAddButton.visible).toBe(false); + + headerMenu.groupButton.doAction(); + let contextMenu = headerMenu.findChild(ContextMenuPopup); + expect(contextMenu).toBe(null); + expect(headerMenu.destroyed).toBe(true); + expect(stringColumn1.grouped).toBe(true); + }); + + it('clears the grouping when clicking a selected group button', () => { + table.group(stringColumn2); + table.group(dateColumn2, undefined, true); + + table.header.openHeaderMenu(dateColumn2); + let headerMenu = scout.assertInstance(table.header.tableHeaderMenu, TableHeaderMenu); + headerMenu.animateRemoval = false; + + expect(headerMenu.groupButton.visible).toBe(true); + expect(headerMenu.groupButton.selected).toBe(false); + expect(headerMenu.groupAddButton.visible).toBe(true); + expect(headerMenu.groupAddButton.selected).toBe(true); + + headerMenu.groupAddButton.doAction(); + let contextMenu = headerMenu.findChild(ContextMenuPopup); + expect(contextMenu).toBe(null); + expect(headerMenu.destroyed).toBe(true); + expect(dateColumn2.grouped).toBe(false); + expect(stringColumn2.grouped).toBe(true); + }); + + it('sorts table rows by selected group type', () => { + expect(table.rows).toEqual([row0, row1, row2, row3]); + + table.group(dateColumn1); // default is DateGroupType.YEAR + expect(table.rows).toEqual([row2, row1, row3, row0]); + + dateColumn1.setGroupType(DateGroupType.YEAR); + expect(table.rows).toEqual([row2, row1, row3, row0]); + + dateColumn1.setGroupType(DateGroupType.MONTH); + expect(table.rows).toEqual([row2, row3, row0, row1]); + + dateColumn1.setGroupType(DateGroupType.MONTH_AND_YEAR); + expect(table.rows).toEqual([row2, row1, row3, row0]); + + dateColumn1.setGroupType(DateGroupType.CALENDAR_WEEK); + expect(table.rows).toEqual([row2, row3, row0, row1]); + + dateColumn1.setGroupType(DateGroupType.WEEKDAY); + expect(table.rows).toEqual([row1, row2, row3, row0]); + + dateColumn1.setGroupType(DateGroupType.DATE); + expect(table.rows).toEqual([row2, row1, row3, row0]); + }); + + it('sorts table rows by selected group type and additional sort columns', () => { + dateColumn1.setGroupType(DateGroupType.WEEKDAY); + table.group(dateColumn1); + expect(table.rows).toEqual([row1, row2, row3, row0]); + + table.sort(stringColumn1, 'asc', true); + expect(table.rows).toEqual([row1, row3, row2, row0]); + + table.sort(stringColumn1, 'desc', true); + expect(table.rows).toEqual([row1, row2, row3, row0]); + + // -------- + // nulls first + + dateColumn2.setGroupType(DateGroupType.MONTH); + table.group(dateColumn2); + expect(table.rows).toEqual([row1, row3, row2, row0]); + + table.sort(stringColumn2, 'asc', true); + expect(table.rows).toEqual([row3, row1, row2, row0]); + + table.sort(stringColumn2, 'desc', true); + expect(table.rows).toEqual([row1, row3, row2, row0]); + + table.sort(dateColumn2, 'desc', true); + expect(table.rows).toEqual([row2, row0, row1, row3]); + }); + + it('allows changing the group type', () => { + dateColumn1.setGroupType(DateGroupType.WEEKDAY); + table.group(dateColumn1); + expect(table.rows).toEqual([row1, row2, row3, row0]); + + table.header.openHeaderMenu(dateColumn1); + let headerMenu = scout.assertInstance(table.header.tableHeaderMenu, DateColumnTableHeaderMenu); + headerMenu.animateRemoval = false; + + expect(headerMenu.groupButton.visible).toBe(true); + expect(headerMenu.groupButton.selected).toBe(true); + expect(headerMenu.groupAddButton.visible).toBe(false); + + expect(headerMenu.groupingGroupTypeAction).toBeInstanceOf(Action); + expect(headerMenu.groupingGroupTypeAction.text).toBe('Weekday'); + + // ------ + + headerMenu.groupingGroupTypeAction.doAction(); + let contextMenu = headerMenu.findChild(ContextMenuPopup); + expect(contextMenu).toBeInstanceOf(ContextMenuPopup); + contextMenu.animateRemoval = false; + + let menus = contextMenu.menuItems.filter(menu => menu instanceof DateGroupTypeMenu) as DateGroupTypeMenu[]; + expect(menus.map(menu => menu.groupType)).toEqual([ + DateGroupType.YEAR, + DateGroupType.MONTH, + DateGroupType.MONTH_AND_YEAR, + DateGroupType.CALENDAR_WEEK, + DateGroupType.WEEKDAY, + DateGroupType.DATE + ]); + let checkedMenus = menus.filter(menu => menu.iconId === icons.CHECKED_BOLD); + expect(checkedMenus.length).toBe(1); + expect(checkedMenus[0].groupType).toBe(DateGroupType.WEEKDAY); + + // ------ + + let targetMenu = menus.find(menu => menu.groupType === DateGroupType.CALENDAR_WEEK); + expect(targetMenu).toBeInstanceOf(DateGroupTypeMenu); + targetMenu.doAction(); + expect(contextMenu.destroyed).toBe(true); + expect(headerMenu.destroyed).toBe(true); + + expect(dateColumn1.grouped).toBe(true); + expect(dateColumn1.groupType).toBe(DateGroupType.CALENDAR_WEEK); + expect(table.rows).toEqual([row2, row3, row0, row1]); + + // ------ + + table.header.openHeaderMenu(dateColumn1); + headerMenu = scout.assertInstance(table.header.tableHeaderMenu, DateColumnTableHeaderMenu); + headerMenu.animateRemoval = false; + + expect(headerMenu.groupButton.visible).toBe(true); + expect(headerMenu.groupButton.selected).toBe(true); + expect(headerMenu.groupAddButton.visible).toBe(false); + + expect(headerMenu.groupingGroupTypeAction).toBeInstanceOf(Action); + expect(headerMenu.groupingGroupTypeAction.text).toBe('Week of year'); + }); + + it('shows an additional menu when the column specifies a non-standard groupFormat', () => { + expect(table.rows).toEqual([row0, row1, row2, row3]); + dateColumn1.setGroupFormat('MM-yy'); + + table.header.openHeaderMenu(dateColumn1); + let headerMenu = scout.assertInstance(table.header.tableHeaderMenu, DateColumnTableHeaderMenu); + headerMenu.animateRemoval = false; + + expect(headerMenu.groupButton.visible).toBe(true); + expect(headerMenu.groupButton.selected).toBe(false); + expect(headerMenu.groupAddButton.visible).toBe(false); + + // ------ + + headerMenu.groupButton.doAction(); + let contextMenu = headerMenu.findChild(ContextMenuPopup); + expect(contextMenu).toBeInstanceOf(ContextMenuPopup); + contextMenu.animateRemoval = false; + + let menus = contextMenu.menuItems.filter(menu => menu instanceof DateGroupTypeMenu) as DateGroupTypeMenu[]; + expect(menus.map(menu => menu.groupType)).toEqual([ + null, + DateGroupType.YEAR, + DateGroupType.MONTH, + DateGroupType.MONTH_AND_YEAR, + DateGroupType.CALENDAR_WEEK, + DateGroupType.WEEKDAY, + DateGroupType.DATE + ]); + let checkedMenus = menus.filter(menu => menu.iconId === icons.CHECKED_BOLD); + expect(checkedMenus.length).toBe(1); + expect(checkedMenus[0].groupType).toBe(null); + expect(checkedMenus[0].text).toBe('MM-yy'); + + // ------ + + checkedMenus[0].doAction(); + expect(contextMenu.destroyed).toBe(true); + expect(headerMenu.destroyed).toBe(true); + + expect(dateColumn1.grouped).toBe(true); + expect(dateColumn1.groupType).toBe(null); + expect(table.rows).toEqual([row2, row1, row3, row0]); + + // ------ + + table.header.openHeaderMenu(dateColumn1); + headerMenu = scout.assertInstance(table.header.tableHeaderMenu, DateColumnTableHeaderMenu); + headerMenu.animateRemoval = false; + + expect(headerMenu.groupButton.visible).toBe(true); + expect(headerMenu.groupButton.selected).toBe(true); + expect(headerMenu.groupAddButton.visible).toBe(false); + + expect(headerMenu.groupingGroupTypeAction).toBeInstanceOf(Action); + expect(headerMenu.groupingGroupTypeAction.text).toBe('MM-yy'); + + // ------ + + headerMenu.groupingGroupTypeAction.doAction(); + contextMenu = headerMenu.findChild(ContextMenuPopup); + expect(contextMenu).toBeInstanceOf(ContextMenuPopup); + contextMenu.animateRemoval = false; + + menus = contextMenu.menuItems.filter(menu => menu instanceof DateGroupTypeMenu) as DateGroupTypeMenu[]; + expect(menus.map(menu => menu.groupType)).toEqual([ + null, + DateGroupType.YEAR, + DateGroupType.MONTH, + DateGroupType.MONTH_AND_YEAR, + DateGroupType.CALENDAR_WEEK, + DateGroupType.WEEKDAY, + DateGroupType.DATE + ]); + checkedMenus = menus.filter(menu => menu.iconId === icons.CHECKED_BOLD); + expect(checkedMenus.length).toBe(1); + expect(checkedMenus[0].groupType).toBe(null); + + let targetMenu = menus.find(menu => menu.groupType === DateGroupType.MONTH_AND_YEAR); + expect(targetMenu).toBeInstanceOf(DateGroupTypeMenu); + targetMenu.doAction(); + expect(contextMenu.destroyed).toBe(true); + expect(headerMenu.destroyed).toBe(true); + + expect(dateColumn1.grouped).toBe(true); + expect(dateColumn1.groupType).toBe(DateGroupType.MONTH_AND_YEAR); + expect(table.rows).toEqual([row2, row1, row3, row0]); + + // ------ + + table.header.openHeaderMenu(dateColumn1); + headerMenu = scout.assertInstance(table.header.tableHeaderMenu, DateColumnTableHeaderMenu); + headerMenu.animateRemoval = false; + + expect(headerMenu.groupButton.visible).toBe(true); + expect(headerMenu.groupButton.selected).toBe(true); + expect(headerMenu.groupAddButton.visible).toBe(false); + + expect(headerMenu.groupingGroupTypeAction).toBeInstanceOf(Action); + expect(headerMenu.groupingGroupTypeAction.text).toBe('Month and year'); + + // ------ + + headerMenu.groupingGroupTypeAction.doAction(); + contextMenu = headerMenu.findChild(ContextMenuPopup); + expect(contextMenu).toBeInstanceOf(ContextMenuPopup); + contextMenu.animateRemoval = false; + + menus = contextMenu.menuItems.filter(menu => menu instanceof DateGroupTypeMenu) as DateGroupTypeMenu[]; + expect(menus.map(menu => menu.groupType)).toEqual([ + null, + DateGroupType.YEAR, + DateGroupType.MONTH, + DateGroupType.MONTH_AND_YEAR, + DateGroupType.CALENDAR_WEEK, + DateGroupType.WEEKDAY, + DateGroupType.DATE + ]); + checkedMenus = menus.filter(menu => menu.iconId === icons.CHECKED_BOLD); + expect(checkedMenus.length).toBe(1); + expect(checkedMenus[0].groupType).toBe(DateGroupType.MONTH_AND_YEAR); + }); + + it('does not shows an additional menu when the column specifies a groupFormat that matches a groupType', () => { + expect(table.rows).toEqual([row0, row1, row2, row3]); + dateColumn1.setGroupFormat('MMMM'); + + table.header.openHeaderMenu(dateColumn1); + let headerMenu = scout.assertInstance(table.header.tableHeaderMenu, DateColumnTableHeaderMenu); + headerMenu.animateRemoval = false; + + expect(headerMenu.groupButton.visible).toBe(true); + expect(headerMenu.groupButton.selected).toBe(false); + expect(headerMenu.groupAddButton.visible).toBe(false); + + // ------ + + headerMenu.groupButton.doAction(); + let contextMenu = headerMenu.findChild(ContextMenuPopup); + expect(contextMenu).toBeInstanceOf(ContextMenuPopup); + contextMenu.animateRemoval = false; + + let menus = contextMenu.menuItems.filter(menu => menu instanceof DateGroupTypeMenu) as DateGroupTypeMenu[]; + expect(menus.map(menu => menu.groupType)).toEqual([ + DateGroupType.YEAR, + DateGroupType.MONTH, + DateGroupType.MONTH_AND_YEAR, + DateGroupType.CALENDAR_WEEK, + DateGroupType.WEEKDAY, + DateGroupType.DATE + ]); + let checkedMenus = menus.filter(menu => menu.iconId === icons.CHECKED_BOLD); + expect(checkedMenus.length).toBe(1); + expect(checkedMenus[0].groupType).toBe(DateGroupType.MONTH); + + // ------ + + contextMenu.close(); + headerMenu.close(); + table.group(dateColumn1); + + // ------ + + table.header.openHeaderMenu(dateColumn1); + headerMenu = scout.assertInstance(table.header.tableHeaderMenu, DateColumnTableHeaderMenu); + headerMenu.animateRemoval = false; + + expect(headerMenu.groupButton.visible).toBe(true); + expect(headerMenu.groupButton.selected).toBe(true); + expect(headerMenu.groupAddButton.visible).toBe(false); + + expect(headerMenu.groupingGroupTypeAction).toBeInstanceOf(Action); + expect(headerMenu.groupingGroupTypeAction.text).toBe('Month'); + + // ------ + + headerMenu.groupingGroupTypeAction.doAction(); + contextMenu = headerMenu.findChild(ContextMenuPopup); + expect(contextMenu).toBeInstanceOf(ContextMenuPopup); + contextMenu.animateRemoval = false; + + menus = contextMenu.menuItems.filter(menu => menu instanceof DateGroupTypeMenu) as DateGroupTypeMenu[]; + expect(menus.map(menu => menu.groupType)).toEqual([ + DateGroupType.YEAR, + DateGroupType.MONTH, + DateGroupType.MONTH_AND_YEAR, + DateGroupType.CALENDAR_WEEK, + DateGroupType.WEEKDAY, + DateGroupType.DATE + ]); + checkedMenus = menus.filter(menu => menu.iconId === icons.CHECKED_BOLD); + expect(checkedMenus.length).toBe(1); + expect(checkedMenus[0].groupType).toBe(DateGroupType.MONTH); + + let targetMenu = menus.find(menu => menu.groupType === DateGroupType.CALENDAR_WEEK); + expect(targetMenu).toBeInstanceOf(DateGroupTypeMenu); + targetMenu.doAction(); + expect(contextMenu.destroyed).toBe(true); + expect(headerMenu.destroyed).toBe(true); + + expect(dateColumn1.grouped).toBe(true); + expect(dateColumn1.groupFormat.pattern).toBe('MMMM'); // unchanged + expect(dateColumn1.groupType).toBe(DateGroupType.CALENDAR_WEEK); + expect(table.rows).toEqual([row2, row3, row0, row1]); + + // ------ + + table.header.openHeaderMenu(dateColumn1); + headerMenu = scout.assertInstance(table.header.tableHeaderMenu, DateColumnTableHeaderMenu); + headerMenu.animateRemoval = false; + + expect(headerMenu.groupButton.visible).toBe(true); + expect(headerMenu.groupButton.selected).toBe(true); + expect(headerMenu.groupAddButton.visible).toBe(false); + + expect(headerMenu.groupingGroupTypeAction).toBeInstanceOf(Action); + expect(headerMenu.groupingGroupTypeAction.text).toBe('Week of year'); + + // ------ + + headerMenu.groupingGroupTypeAction.doAction(); + contextMenu = headerMenu.findChild(ContextMenuPopup); + expect(contextMenu).toBeInstanceOf(ContextMenuPopup); + contextMenu.animateRemoval = false; + + menus = contextMenu.menuItems.filter(menu => menu instanceof DateGroupTypeMenu) as DateGroupTypeMenu[]; + expect(menus.map(menu => menu.groupType)).toEqual([ + DateGroupType.YEAR, + DateGroupType.MONTH, + DateGroupType.MONTH_AND_YEAR, + DateGroupType.CALENDAR_WEEK, + DateGroupType.WEEKDAY, + DateGroupType.DATE + ]); + checkedMenus = menus.filter(menu => menu.iconId === icons.CHECKED_BOLD); + expect(checkedMenus.length).toBe(1); + expect(checkedMenus[0].groupType).toBe(DateGroupType.CALENDAR_WEEK); + }); + + it('does not shows an additional menu when the column specifies a groupFormat that matches a groupType', () => { + expect(table.rows).toEqual([row0, row1, row2, row3]); + dateColumn1.setGroupFormat('MMMM'); + + table.header.openHeaderMenu(dateColumn1); + let headerMenu = scout.assertInstance(table.header.tableHeaderMenu, DateColumnTableHeaderMenu); + headerMenu.animateRemoval = false; + + expect(headerMenu.groupButton.visible).toBe(true); + expect(headerMenu.groupButton.selected).toBe(false); + expect(headerMenu.groupAddButton.visible).toBe(false); + + // ------ + + headerMenu.groupButton.doAction(); + let contextMenu = headerMenu.findChild(ContextMenuPopup); + expect(contextMenu).toBeInstanceOf(ContextMenuPopup); + contextMenu.animateRemoval = false; + + let menus = contextMenu.menuItems.filter(menu => menu instanceof DateGroupTypeMenu) as DateGroupTypeMenu[]; + expect(menus.map(menu => menu.groupType)).toEqual([ + DateGroupType.YEAR, + DateGroupType.MONTH, + DateGroupType.MONTH_AND_YEAR, + DateGroupType.CALENDAR_WEEK, + DateGroupType.WEEKDAY, + DateGroupType.DATE + ]); + let checkedMenus = menus.filter(menu => menu.iconId === icons.CHECKED_BOLD); + expect(checkedMenus.length).toBe(1); + expect(checkedMenus[0].groupType).toBe(DateGroupType.MONTH); + }); + + it('changes the label if a group button is hovered', () => { + dateColumn1.setGroupType(DateGroupType.YEAR); + dateColumn2.setGroupType(DateGroupType.MONTH); + table.group(dateColumn1); + table.group(dateColumn2, undefined, true); + + table.header.openHeaderMenu(dateColumn1); + let headerMenu = scout.assertInstance(table.header.tableHeaderMenu, DateColumnTableHeaderMenu); + headerMenu.animateRemoval = false; + + expect(headerMenu.groupButton.visible).toBe(true); + expect(headerMenu.groupButton.selected).toBe(false); + expect(headerMenu.groupAddButton.visible).toBe(true); + expect(headerMenu.groupAddButton.selected).toBe(true); + let menuGroup = headerMenu.groupButton.findParent(TableHeaderMenuGroup); + expect(menuGroup).toBeInstanceOf(TableHeaderMenuGroup); + + expect(headerMenu.groupingGroupTypeAction).toBeInstanceOf(Action); + expect(headerMenu.groupingGroupTypeAction.text).toBe('Year'); + expect(headerMenu.groupingGroupTypeAction.$container.isEveryParentVisible()).toBe(true); + expect(menuGroup.$container.children('.table-header-menu-group-text:visible').text()).toBe('Grouping by Year'); + + // ------ + + // simulate focus, because 'focusin' event is not triggered in spec + menuGroup.setFocusedGroupItem(headerMenu.groupButton); + expect(headerMenu.groupingGroupTypeAction.$container.isEveryParentVisible()).toBe(false); + expect(menuGroup.$container.children('.table-header-menu-group-text:visible').text()).toBe('Grouping apply'); + + JQueryTesting.triggerMouseEnter(headerMenu.groupAddButton.$container); + expect(headerMenu.groupingGroupTypeAction.$container.isEveryParentVisible()).toBe(false); + expect(menuGroup.$container.children('.table-header-menu-group-text:visible').text()).toBe('Grouping remove'); + + JQueryTesting.triggerMouseLeave(headerMenu.groupAddButton.$container); + expect(headerMenu.groupingGroupTypeAction.$container.isEveryParentVisible()).toBe(false); + expect(menuGroup.$container.children('.table-header-menu-group-text:visible').text()).toBe('Grouping apply'); + + menuGroup.setFocusedGroupItem(null); + expect(headerMenu.groupingGroupTypeAction.$container.isEveryParentVisible()).toBe(true); + expect(menuGroup.$container.children('.table-header-menu-group-text:visible').text()).toBe('Grouping by Year'); + + headerMenu.groupButton.doAction(); + let contextMenu = headerMenu.findChild(ContextMenuPopup); + expect(contextMenu).toBeInstanceOf(ContextMenuPopup); + contextMenu.animateRemoval = false; + + expect(headerMenu.groupingGroupTypeAction.$container.isEveryParentVisible()).toBe(false); + expect(menuGroup.$container.children('.table-header-menu-group-text:visible').text()).toBe('Grouping apply'); + + JQueryTesting.triggerMouseEnter(headerMenu.groupAddButton.$container); + expect(headerMenu.groupingGroupTypeAction.$container.isEveryParentVisible()).toBe(false); + expect(menuGroup.$container.children('.table-header-menu-group-text:visible').text()).toBe('Grouping remove'); + + JQueryTesting.triggerMouseLeave(headerMenu.groupAddButton.$container); + contextMenu.close(); + expect(headerMenu.groupingGroupTypeAction.$container.isEveryParentVisible()).toBe(true); + expect(menuGroup.$container.children('.table-header-menu-group-text:visible').text()).toBe('Grouping by Year'); + }); + + it('applies grouping if applyGrouping is true', () => { + expect(table.rows).toEqual([row0, row1, row2, row3]); + + table.group(dateColumn1); // default is DateGroupType.YEAR + expect(table.rows).toEqual([row2, row1, row3, row0]); + + dateColumn1.setGroupType(DateGroupType.MONTH); + expect(table.rows).toEqual([row2, row3, row0, row1]); + + dateColumn1.setGroupType(DateGroupType.YEAR, false); + expect(table.rows).toEqual([row2, row3, row0, row1]); // unchanged (because applyGrouping=false) + + table.group(dateColumn1); + expect(table.rows).toEqual([row2, row1, row3, row0]); // changed after explicit grouping + + table.removeGroupColumn(dateColumn1); + expect(table.rows).toEqual([row2, row1, row3, row0]); // unchanged + + dateColumn1.setGroupType(DateGroupType.MONTH); + expect(table.rows).toEqual([row2, row1, row3, row0]); // unchanged (because column is not grouped) + + table.group(dateColumn1); + expect(table.rows).toEqual([row2, row3, row0, row1]); // changed after explicit grouping + }); + + it('does not allow changing the group type if column.hasDate=false', () => { + dateColumn1.hasDate = false; + dateColumn1.setFormat(null); // recreate default format + dateColumn1.setGroupFormat('HH'); + + table.deleteAllRows(); + table.insertRows([ + {id: 'row0', cells: [dates.create('2022-03-26 17:17:17.000')]}, + {id: 'row1', cells: [dates.create('2022-03-10 17:17:17.000')]}, + {id: 'row2', cells: [dates.create('2022-03-09 18:18:18.000')]}, + {id: 'row3', cells: [null]}, + {id: 'row4', cells: [dates.create('2022-03-10 17:17:33.000')]}, + {id: 'row5', cells: [dates.create('2028-01-01 10:01:00.000')]} + ]); + let row4, row5; + [row0, row1, row2, row3, row4, row5] = table.rows; + + // -------- + + table.header.openHeaderMenu(dateColumn1); + let headerMenu = scout.assertInstance(table.header.tableHeaderMenu, DateColumnTableHeaderMenu); + headerMenu.animateRemoval = false; + + expect(headerMenu.groupButton.visible).toBe(true); + expect(headerMenu.groupButton.selected).toBe(false); + expect(headerMenu.groupAddButton.visible).toBe(false); + + headerMenu.groupButton.doAction(); + let contextMenu = headerMenu.findChild(ContextMenuPopup); + expect(contextMenu).toBeFalsy(); + expect(headerMenu.destroyed).toBe(true); + + expect(dateColumn1.grouped).toBe(true); + expect(dateColumn1.groupType).toBe(null); + expect(table.rows).toEqual([row3, row2, row1, row4, row0, row5]); + + // ------ + + table.header.openHeaderMenu(dateColumn1); + headerMenu = scout.assertInstance(table.header.tableHeaderMenu, DateColumnTableHeaderMenu); + headerMenu.animateRemoval = false; + + expect(headerMenu.groupButton.visible).toBe(true); + expect(headerMenu.groupButton.selected).toBe(true); + expect(headerMenu.groupAddButton.visible).toBe(false); + + expect(headerMenu.groupingGroupTypeAction).toBeFalsy(); + + // ------ + + headerMenu.groupButton.doAction(); + contextMenu = headerMenu.findChild(ContextMenuPopup); + expect(contextMenu).toBeFalsy(); + expect(headerMenu.destroyed).toBe(true); + + expect(dateColumn1.grouped).toBe(false); + expect(dateColumn1.groupType).toBe(null); + }); + }); +}); diff --git a/eclipse-scout-core/test/table/TableGroupingSpec.ts b/eclipse-scout-core/test/table/TableGroupingSpec.ts index d9b4ee7f390..e787a2944b5 100644 --- a/eclipse-scout-core/test/table/TableGroupingSpec.ts +++ b/eclipse-scout-core/test/table/TableGroupingSpec.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2025 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -157,6 +157,13 @@ describe('Table Grouping', () => { }); } + function assertColumnState(column: Column, expectedState: { sortActive: boolean, sortIndex: number, sortAscending: boolean, grouped: boolean }) { + expect(column.sortActive).toBe(expectedState.sortActive); + expect(column.sortIndex).toBe(expectedState.sortIndex); + expect(column.sortAscending).toBe(expectedState.sortAscending); + expect(column.grouped).toBe(expectedState.grouped); + } + it('renders an aggregate row for each group', () => { prepareTable(); prepareContent(); @@ -634,6 +641,42 @@ describe('Table Grouping', () => { assertGroupingProperty(table); }); + it('clears other group columns when grouping with multiGroup=false"', () => { + prepareTable(); + + table.sort(table.columns[0], 'desc'); // normal sort column (should be preserved when adding or removing group columns) + + assertColumnState(table.columns[0], {sortActive: true, sortIndex: 0, sortAscending: false, grouped: false}); + assertColumnState(table.columns[1], {sortActive: false, sortIndex: -1, sortAscending: true, grouped: false}); + assertColumnState(table.columns[2], {sortActive: false, sortIndex: -1, sortAscending: true, grouped: false}); + assertColumnState(table.columns[3], {sortActive: false, sortIndex: -1, sortAscending: true, grouped: false}); + assertColumnState(table.columns[4], {sortActive: false, sortIndex: -1, sortAscending: true, grouped: false}); + + table.group(table.columns[1], 'desc', true); + + assertColumnState(table.columns[0], {sortActive: true, sortIndex: 1, sortAscending: false, grouped: false}); + assertColumnState(table.columns[1], {sortActive: true, sortIndex: 0, sortAscending: false, grouped: true}); + assertColumnState(table.columns[2], {sortActive: false, sortIndex: -1, sortAscending: true, grouped: false}); + assertColumnState(table.columns[3], {sortActive: false, sortIndex: -1, sortAscending: true, grouped: false}); + assertColumnState(table.columns[4], {sortActive: false, sortIndex: -1, sortAscending: true, grouped: false}); + + table.group(table.columns[2], 'desc', true); + + assertColumnState(table.columns[0], {sortActive: true, sortIndex: 2, sortAscending: false, grouped: false}); + assertColumnState(table.columns[1], {sortActive: true, sortIndex: 0, sortAscending: false, grouped: true}); + assertColumnState(table.columns[2], {sortActive: true, sortIndex: 1, sortAscending: false, grouped: true}); + assertColumnState(table.columns[3], {sortActive: false, sortIndex: -1, sortAscending: true, grouped: false}); + assertColumnState(table.columns[4], {sortActive: false, sortIndex: -1, sortAscending: true, grouped: false}); + + table.group(table.columns[3], 'desc', false); // <-- multiGroup=false + + assertColumnState(table.columns[0], {sortActive: true, sortIndex: 1, sortAscending: false, grouped: false}); + assertColumnState(table.columns[1], {sortActive: false, sortIndex: -1, sortAscending: false, grouped: false}); + assertColumnState(table.columns[2], {sortActive: false, sortIndex: -1, sortAscending: false, grouped: false}); + assertColumnState(table.columns[3], {sortActive: true, sortIndex: 0, sortAscending: false, grouped: true}); + assertColumnState(table.columns[4], {sortActive: false, sortIndex: -1, sortAscending: true, grouped: false}); + }); + describe('removeAllGroupColumns', () => { it('clears the sort columns', () => { prepareTable(); diff --git a/eclipse-scout-core/test/table/TableSpec.ts b/eclipse-scout-core/test/table/TableSpec.ts index a5b879311b7..2c8b268aa31 100644 --- a/eclipse-scout-core/test/table/TableSpec.ts +++ b/eclipse-scout-core/test/table/TableSpec.ts @@ -1881,11 +1881,11 @@ describe('Table', () => { }); it('sorts columns with sortcode', () => { - let model = helper.createModelSingleColumnByValues([0, 1, 2, 3, 4], 'NumberColumn'); + let model = helper.createModelSingleColumnByValues([0, 1, 2, 3, 4, 5], 'NumberColumn'); let table = helper.createTable(model); column0 = table.columns[0]; - let sortCodes = [13, 0, 42, 7, null]; + let sortCodes = [13, 0, 42, 7, null, null]; table.rows.forEach((row, index) => { column0.cell(row).sortCode = sortCodes[index]; }); @@ -1893,10 +1893,10 @@ describe('Table', () => { table.render(); table.sort(column0, 'desc'); - helper.assertValuesInCells(table.rows, 0, [2, 0, 3, 1, 4]); + helper.assertValuesInCells(table.rows, 0, [5, 4, 2, 0, 3, 1]); table.sort(column0, 'asc'); - helper.assertValuesInCells(table.rows, 0, [4, 1, 3, 0, 2]); + helper.assertValuesInCells(table.rows, 0, [1, 3, 0, 2, 4, 5]); }); it('sorts smart columns with sortcode', () => { @@ -1935,6 +1935,13 @@ describe('Table', () => { text: '4', value: 4 }) + ]), + helper.createModelRow(null, [ + scout.create(Cell, { + sortCode: null, + text: '5', + value: 5 + }) ]) ]; @@ -1945,10 +1952,10 @@ describe('Table', () => { table.render(); table.sort(column0, 'desc'); - helper.assertValuesInCells(table.rows, 0, [2, 0, 3, 1, 4]); + helper.assertValuesInCells(table.rows, 0, [5, 4, 2, 0, 3, 1]); table.sort(column0, 'asc'); - helper.assertValuesInCells(table.rows, 0, [4, 1, 3, 0, 2]); + helper.assertValuesInCells(table.rows, 0, [1, 3, 0, 2, 4, 5]); }); it('uses non sort columns as fallback', () => { diff --git a/eclipse-scout-core/test/table/TableUiPreferencesSpec.ts b/eclipse-scout-core/test/table/TableUiPreferencesSpec.ts index d0ba60be576..b001074fd4c 100644 --- a/eclipse-scout-core/test/table/TableUiPreferencesSpec.ts +++ b/eclipse-scout-core/test/table/TableUiPreferencesSpec.ts @@ -8,8 +8,8 @@ * SPDX-License-Identifier: EPL-2.0 */ import { - BooleanColumn, Column, DateColumn, NumberColumn, ObjectIdProvider, scout, Table, TableClientUiPreferenceProfileDo, TableClientUiPreferencesDo, TableColumnClientUiPreferenceDo, TableRow, TableTextUserFilter, tableUiPreferences, - TableUiPreferences, TextColumnUserFilter, Tile, uiPreferences, UiPreferencesDo, WidgetModel + BooleanColumn, Column, DateColumn, DateGroupType, NumberColumn, ObjectIdProvider, scout, Table, TableClientUiPreferenceProfileDo, TableClientUiPreferencesDo, TableColumnClientUiPreferenceDo, TableRow, TableTextUserFilter, + tableUiPreferences, TableUiPreferences, TextColumnUserFilter, Tile, uiPreferences, UiPreferencesDo, WidgetModel } from '../../src/index'; import {SpecUiPreferencesStore} from '../../src/testing'; @@ -315,11 +315,11 @@ describe('TableUiPreferences', () => { }); table.saveInitialUiPreferences(); - let c2 = table.columnById('c2'); - let c3 = table.columnById('c3'); - let c4 = table.columnById('c4'); - let c5 = table.columnById('c5'); - let c6 = table.columnById('c6'); + let c2 = table.columnById('c2') as Column; + let c3 = table.columnById('c3') as Column; + let c4 = table.columnById('c4') as NumberColumn; + let c5 = table.columnById('c5') as DateColumn; + let c6 = table.columnById('c6') as BooleanColumn; // ----- @@ -389,6 +389,51 @@ describe('TableUiPreferences', () => { tableUiPreferences.applyProfile(table, profile1); expect(table.filterCount()).toBe(0); + + // ----- + + c5.setVisible(true); + c5.setGroupType(DateGroupType.MONTH_AND_YEAR); + table.group(c5, 'asc'); + expect(table.columns.map(c => c.id)).toEqual(['c1', 'c2', 'c6', 'c3', 'c4', 'c5']); + expect(table.visibleColumns().map(c => c.id)).toEqual(['c2', 'c6', 'c3', 'c4', 'c5']); + expect(table.columns.map(c => c.width)).toEqual([60, 202, 106, 103, 104, 105]); + expect(table.columns.map(c => c.grouped)).toEqual([false, false, false, false, false, true]); + expect(table.columns.map(c => c.sortIndex)).toEqual([-1, -1, -1, -1, 1, 0]); + expect(table.columns.map(c => c.sortAscending)).toEqual([true, true, true, true, false, true]); + expect(table.columns.map(c => c.sortActive)).toEqual([false, false, false, false, true, true]); + expect(table.filterCount()).toBe(0); + expect(c5.groupType).toBe(DateGroupType.MONTH_AND_YEAR); + + let profile3 = tableUiPreferences.createProfile(table); + table.resetToInitialUiPreferences(); + + expect(table.columns.map(c => c.id)).toEqual(['c1', 'c2', 'c3', 'c4', 'c5', 'c6']); + expect(table.visibleColumns().map(c => c.id)).toEqual(['c2', 'c3', 'c4', 'c5', 'c6']); + expect(table.columns.map(c => c.width)).toEqual([60, 102, 103, 104, 105, 106]); + expect(table.columns.map(c => c.grouped)).toEqual([false, false, false, false, false, false]); + expect(table.columns.map(c => c.sortIndex)).toEqual([-1, -1, -1, -1, 0, -1]); + expect(table.columns.map(c => c.sortAscending)).toEqual([true, true, true, true, false, true]); + expect(table.columns.map(c => c.sortActive)).toEqual([false, false, false, false, true, false]); + expect(table.filterCount()).toBe(0); + expect(c5.groupType).toBe(null); + + c5.setGroupType(DateGroupType.CALENDAR_WEEK); + let columnDateGroupTypeChangedEvents = []; + table.on('columnDateGroupTypeChanged', event => columnDateGroupTypeChangedEvents.push(event)); + + tableUiPreferences.applyProfile(table, profile3); + + expect(columnDateGroupTypeChangedEvents.length).toBe(1); + expect(table.columns.map(c => c.id)).toEqual(['c1', 'c2', 'c6', 'c3', 'c4', 'c5']); + expect(table.visibleColumns().map(c => c.id)).toEqual(['c2', 'c6', 'c3', 'c4', 'c5']); + expect(table.columns.map(c => c.width)).toEqual([60, 202, 106, 103, 104, 105]); + expect(table.columns.map(c => c.grouped)).toEqual([false, false, false, false, false, true]); + expect(table.columns.map(c => c.sortIndex)).toEqual([-1, -1, -1, -1, 1, 0]); + expect(table.columns.map(c => c.sortAscending)).toEqual([true, true, true, true, false, true]); + expect(table.columns.map(c => c.sortActive)).toEqual([false, false, false, false, true, true]); + expect(table.filterCount()).toBe(0); + expect(c5.groupType).toBe(DateGroupType.MONTH_AND_YEAR); }); it('does not modify width of fixed width columns when applying a profile', () => { @@ -679,8 +724,8 @@ describe('TableUiPreferences', () => { }, { id: 'c5', objectType: DateColumn, - width: 105, sortIndex: 0, + width: 105, sortAscending: false }, { id: 'c6', diff --git a/eclipse-scout-core/test/table/columns/DateColumnSpec.ts b/eclipse-scout-core/test/table/columns/DateColumnSpec.ts index fe3c41054d4..1b7c9423dad 100644 --- a/eclipse-scout-core/test/table/columns/DateColumnSpec.ts +++ b/eclipse-scout-core/test/table/columns/DateColumnSpec.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -7,7 +7,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -import {DateColumn, dates} from '../../../src/index'; +import {Column, DateColumn, DateGroupType, dates} from '../../../src/index'; import {TableSpecHelper} from '../../../src/testing/index'; describe('DateColumn', () => { @@ -44,4 +44,47 @@ describe('DateColumn', () => { expect(column0.cell(table.rows[0]).text).toBe('2017-01-01'); }); }); + + describe('compare', () => { + it('sorts grouped date columns by group type and date, unless there are additional sort columns', () => { + let date0 = dates.create('2018-03-03'); // B + let date1 = dates.create('2017-03-03'); // C + let date2 = dates.create('2019-03-02'); // A + let date3 = dates.create('2018-01-01'); // D + + let model = helper.createModel([ + { + id: 'col0', + objectType: DateColumn + }, + { + id: 'col1', + objectType: Column + } + ], [ + {id: 'row0', cells: [date0, 'C']}, + {id: 'row1', cells: [date1, 'D']}, + {id: 'row_', cells: [null, null]}, + {id: 'row2', cells: [date2, 'B']}, + {id: 'row3', cells: [date3, 'A']} + ]); + let table = helper.createTable(model); + let dateColumn = table.columns[0] as DateColumn; + let stringColumn = table.columns[1] as Column; + let [row0, row1, row_, row2, row3] = table.rows; + table.render(); + + expect(table.rows).toEqual([row0, row1, row_, row2, row3]); // insertion order + expect(table.rows.map(row => dateColumn.cellValue(row))).toEqual([date0, date1, null, date2, date3]); + + table.group(dateColumn, 'asc'); + expect(table.rows).toEqual([row_, row1, row3, row0, row2]); // grouped by year, sorted by date + + dateColumn.setGroupType(DateGroupType.MONTH); + expect(table.rows).toEqual([row_, row3, row1, row0, row2]); // grouped by month, sorted by date + + table.group(stringColumn, 'asc', true); + expect(table.rows).toEqual([row_, row3, row2, row0, row1]); // grouped by month, sorted by string column + }); + }); }); diff --git a/org.eclipse.scout.rt.api.data/src/main/java/org/eclipse/scout/rt/api/data/prefs/userfilter/DateColumnUserFilterStateDo.java b/org.eclipse.scout.rt.api.data/src/main/java/org/eclipse/scout/rt/api/data/prefs/userfilter/DateColumnUserFilterStateDo.java index 10a11b1c6ce..fd0b67e93fd 100644 --- a/org.eclipse.scout.rt.api.data/src/main/java/org/eclipse/scout/rt/api/data/prefs/userfilter/DateColumnUserFilterStateDo.java +++ b/org.eclipse.scout.rt.api.data/src/main/java/org/eclipse/scout/rt/api/data/prefs/userfilter/DateColumnUserFilterStateDo.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2025 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -15,6 +15,7 @@ import jakarta.annotation.Generated; +import org.eclipse.scout.rt.api.data.table.DateGroupType; import org.eclipse.scout.rt.api.data.table.IUserFilterStateDo; import org.eclipse.scout.rt.api.data.table.TableColumnId; import org.eclipse.scout.rt.dataobject.DoEntity; @@ -32,10 +33,14 @@ public DoValue columnId() { return doValue("columnId"); } - public DoSet selectedValues() { + public DoSet selectedValues() { return doSet("selectedValues"); } + public DoValue groupType() { + return doValue("groupType"); + } + public DoValue dateFrom() { return doValue("dateFrom"); } @@ -60,22 +65,33 @@ public TableColumnId getColumnId() { } @Generated("DoConvenienceMethodsGenerator") - public DateColumnUserFilterStateDo withSelectedValues(Collection selectedValues) { + public DateColumnUserFilterStateDo withSelectedValues(Collection selectedValues) { selectedValues().updateAll(selectedValues); return this; } @Generated("DoConvenienceMethodsGenerator") - public DateColumnUserFilterStateDo withSelectedValues(Integer... selectedValues) { + public DateColumnUserFilterStateDo withSelectedValues(Long... selectedValues) { selectedValues().updateAll(selectedValues); return this; } @Generated("DoConvenienceMethodsGenerator") - public Set getSelectedValues() { + public Set getSelectedValues() { return selectedValues().get(); } + @Generated("DoConvenienceMethodsGenerator") + public DateColumnUserFilterStateDo withGroupType(DateGroupType groupType) { + groupType().set(groupType); + return this; + } + + @Generated("DoConvenienceMethodsGenerator") + public DateGroupType getGroupType() { + return groupType().get(); + } + @Generated("DoConvenienceMethodsGenerator") public DateColumnUserFilterStateDo withDateFrom(Date dateFrom) { dateFrom().set(dateFrom); diff --git a/org.eclipse.scout.rt.api.data/src/main/java/org/eclipse/scout/rt/api/data/table/DateGroupType.java b/org.eclipse.scout.rt.api.data/src/main/java/org/eclipse/scout/rt/api/data/table/DateGroupType.java new file mode 100644 index 00000000000..e36267c7540 --- /dev/null +++ b/org.eclipse.scout.rt.api.data/src/main/java/org/eclipse/scout/rt/api/data/table/DateGroupType.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.scout.rt.api.data.table; + +import java.time.LocalDate; +import java.time.temporal.IsoFields; +import java.time.temporal.WeekFields; +import java.util.Date; + +import org.eclipse.scout.rt.dataobject.enumeration.EnumName; +import org.eclipse.scout.rt.dataobject.enumeration.IEnum; +import org.eclipse.scout.rt.platform.nls.NlsLocale; +import org.eclipse.scout.rt.platform.util.date.DateUtility; + +@EnumName("scout.DateGroupType") +public enum DateGroupType implements IEnum { + YEAR("year") { + @Override + protected long toKey(LocalDate date) { + return date.getYear(); + } + }, + MONTH("month") { + @Override + protected long toKey(LocalDate date) { + return date.getMonthValue(); + } + }, + MONTH_AND_YEAR("month-and-year") { + @Override + protected long toKey(LocalDate date) { + return (date.getYear() * 100L) + date.getMonthValue(); // 2026-03-27 -> 202603 + } + }, + CALENDAR_WEEK("calendar-week") { + @Override + protected long toKey(LocalDate date) { + return date.get(IsoFields.WEEK_OF_WEEK_BASED_YEAR); + } + }, + WEEKDAY("weekday") { + @Override + protected long toKey(LocalDate date) { + return date.get(WeekFields.of(NlsLocale.get()).dayOfWeek()); + } + }, + DATE("date") { + @Override + protected long toKey(LocalDate date) { + return date.toEpochDay(); + } + }; + + private final String m_stringValue; + + DateGroupType(String stringValue) { + m_stringValue = stringValue; + } + + @Override + public String stringValue() { + return m_stringValue; + } + + /** + * Returns a numeric value that can be used to order dates according to this group type. + */ + protected long toKey(Date date) { + return toKey(DateUtility.toLocalDate(date)); + } + + /** + * Returns a numeric value that can be used to order dates according to this group type. + */ + protected abstract long toKey(LocalDate date); + + /** + * Compares the given dates with respect to this group type. For example, {@link #MONTH}, + * would sort 2017-08-01 _after_ 2026-03-27, because August comes after March. + */ + public int compare(Date d1, Date d2) { + if (d1 == null && d2 == null) { + return 0; + } + if (d1 == null) { + return -1; + } + if (d2 == null) { + return 1; + } + return Long.compare(toKey(d1), toKey(d2)); + } +} diff --git a/org.eclipse.scout.rt.api.data/src/main/java/org/eclipse/scout/rt/api/data/table/TableColumnClientUiPreferenceDo.java b/org.eclipse.scout.rt.api.data/src/main/java/org/eclipse/scout/rt/api/data/table/TableColumnClientUiPreferenceDo.java index 78c0a3a054b..2f923ed2951 100644 --- a/org.eclipse.scout.rt.api.data/src/main/java/org/eclipse/scout/rt/api/data/table/TableColumnClientUiPreferenceDo.java +++ b/org.eclipse.scout.rt.api.data/src/main/java/org/eclipse/scout/rt/api/data/table/TableColumnClientUiPreferenceDo.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2025 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -57,6 +57,10 @@ public DoValue backgroundEffectId() { return doValue("backgroundEffectId"); } + public DoValue dateGroupType() { + return doValue("dateGroupType"); + } + /* ************************************************************************** * GENERATED CONVENIENCE METHODS * *************************************************************************/ @@ -174,4 +178,15 @@ public TableColumnClientUiPreferenceDo withBackgroundEffectId(TableColumnBackgro public TableColumnBackgroundEffectId getBackgroundEffectId() { return backgroundEffectId().get(); } + + @Generated("DoConvenienceMethodsGenerator") + public TableColumnClientUiPreferenceDo withDateGroupType(DateGroupType dateGroupType) { + dateGroupType().set(dateGroupType); + return this; + } + + @Generated("DoConvenienceMethodsGenerator") + public DateGroupType getDateGroupType() { + return dateGroupType().get(); + } } diff --git a/org.eclipse.scout.rt.api.data/src/test/resources/org/eclipse/scout/rt/api/data/scout-api-data-dataobject-signature.json b/org.eclipse.scout.rt.api.data/src/test/resources/org/eclipse/scout/rt/api/data/scout-api-data-dataobject-signature.json index c294ab80708..c24c1729760 100644 --- a/org.eclipse.scout.rt.api.data/src/test/resources/org/eclipse/scout/rt/api/data/scout-api-data-dataobject-signature.json +++ b/org.eclipse.scout.rt.api.data/src/test/resources/org/eclipse/scout/rt/api/data/scout-api-data-dataobject-signature.json @@ -97,11 +97,15 @@ "_type" : "scout.AttributeDataObjectSignature", "name" : "dateTo", "valueType" : "CLASS[java.util.Date]" + }, { + "_type" : "scout.AttributeDataObjectSignature", + "name" : "groupType", + "valueType" : "ENUM[scout.DateGroupType]" }, { "_type" : "scout.AttributeDataObjectSignature", "list" : true, "name" : "selectedValues", - "valueType" : "CLASS[java.lang.Integer]" + "valueType" : "CLASS[java.lang.Long]" } ], "typeName" : "scout.DateColumnUserFilterState", "typeVersion" : "scout-25.2.001" @@ -281,6 +285,10 @@ "_type" : "scout.AttributeDataObjectSignature", "name" : "columnId", "valueType" : "ID[scout.TableColumnId]" + }, { + "_type" : "scout.AttributeDataObjectSignature", + "name" : "dateGroupType", + "valueType" : "ENUM[scout.DateGroupType]" }, { "_type" : "scout.AttributeDataObjectSignature", "name" : "groupingActive", @@ -346,5 +354,9 @@ "typeName" : "scout.UiPreferences", "typeVersion" : "scout-25.2.002" } ], - "enums" : [ ] + "enums" : [ { + "_type" : "scout.EnumApiSignature", + "enumName" : "scout.DateGroupType", + "values" : [ "calendar-week", "date", "month", "month-and-year", "weekday", "year" ] + } ] } diff --git a/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/table/TableEventBufferTest.java b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/table/TableEventBufferTest.java index 0e70c5c9f8b..e2eedcce668 100644 --- a/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/table/TableEventBufferTest.java +++ b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/table/TableEventBufferTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -275,7 +275,7 @@ public void testCombineOnlyConsecutiveUpdates() { assertEquals(1, events.get(2).getRowCount()); // row 2 was merged to insert } - ////// REPLACE + // -------------------- REPLACE -------------------- /** * If a row is inserted and later updated, only an insert event with the updated value needs to be kept. @@ -556,6 +556,43 @@ public void testCoalesceAndRemoveObsoleteColumnBackgroundEffectEvents() { assertTrue(bgEffectEvent.getColumns().containsAll(CollectionUtility.arrayList(c1, c2))); } + @Test + public void testCoalesceAndRemoveObsoleteColumnDateGroupTypeEvents() { + ITable table = mock(ITable.class); + final IColumn c1 = mockColumn(0); + final IColumn c2 = mockColumn(1); + final IColumn c3 = mockColumn(2); + + final TableEvent event0 = new TableEvent(table, TableEvent.TYPE_COLUMN_DATE_GROUP_TYPE_CHANGED); + event0.setColumns(CollectionUtility.arrayList(c3)); + final TableEvent event1 = new TableEvent(table, TableEvent.TYPE_COLUMN_DATE_GROUP_TYPE_CHANGED); + event1.setColumns(CollectionUtility.arrayList(c2)); + final TableEvent event2 = new TableEvent(table, TableEvent.TYPE_COLUMN_BACKGROUND_EFFECT_CHANGED); + event2.setColumns(CollectionUtility.arrayList(c2)); + final TableEvent event3 = new TableEvent(table, TableEvent.TYPE_COLUMN_AGGREGATION_CHANGED); + event3.setColumns(CollectionUtility.arrayList(c1)); + final TableEvent event4 = new TableEvent(table, TableEvent.TYPE_COLUMN_DATE_GROUP_TYPE_CHANGED); + event4.setColumns(CollectionUtility.arrayList(c3)); + final TableEvent event5 = new TableEvent(table, TableEvent.TYPE_COLUMN_DATE_GROUP_TYPE_CHANGED); + event5.setColumns(CollectionUtility.arrayList(c3)); + + m_testBuffer.add(event0); + m_testBuffer.add(event1); + m_testBuffer.add(event2); + m_testBuffer.add(event3); + m_testBuffer.add(event4); + m_testBuffer.add(event5); + + final List events = m_testBuffer.consumeAndCoalesceEvents(); + assertEquals(4, events.size()); + assertEquals(TableEvent.TYPE_COLUMN_DATE_GROUP_TYPE_CHANGED, events.get(0).getType()); + assertEquals(List.of(c3, c2), events.get(0).getColumns()); + assertEquals(TableEvent.TYPE_COLUMN_BACKGROUND_EFFECT_CHANGED, events.get(1).getType()); + assertEquals(TableEvent.TYPE_COLUMN_AGGREGATION_CHANGED, events.get(2).getType()); + assertEquals(TableEvent.TYPE_COLUMN_DATE_GROUP_TYPE_CHANGED, events.get(3).getType()); + assertEquals(List.of(c3), events.get(3).getColumns()); + } + @Test public void testPreserveRowsFromPreviousEventsWhenDeleted() { ITable table = mock(ITable.class); diff --git a/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/table/columns/AbstractDateColumnTest.java b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/table/columns/AbstractDateColumnTest.java index 9cd5afe6c40..d7d358b042e 100644 --- a/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/table/columns/AbstractDateColumnTest.java +++ b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/table/columns/AbstractDateColumnTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -13,8 +13,11 @@ import static org.mockito.Mockito.mock; import java.text.SimpleDateFormat; +import java.util.Arrays; import java.util.Date; +import java.util.Locale; +import org.eclipse.scout.rt.api.data.table.DateGroupType; import org.eclipse.scout.rt.client.ui.basic.cell.ICell; import org.eclipse.scout.rt.client.ui.basic.table.AbstractTable; import org.eclipse.scout.rt.client.ui.basic.table.ITableRow; @@ -23,7 +26,10 @@ import org.eclipse.scout.rt.client.ui.form.fields.datefield.AbstractDateField; import org.eclipse.scout.rt.client.ui.form.fields.datefield.IDateField; import org.eclipse.scout.rt.platform.Order; +import org.eclipse.scout.rt.platform.context.RunContexts; import org.eclipse.scout.rt.platform.nls.NlsLocale; +import org.eclipse.scout.rt.platform.util.collection.OrderedCollection; +import org.eclipse.scout.rt.platform.util.date.DateUtility; import org.eclipse.scout.rt.testing.platform.runner.PlatformTestRunner; import org.junit.Test; import org.junit.runner.RunWith; @@ -44,8 +50,8 @@ public void testPrepareEditInternal() { column.setHasTime(true); ITableRow row = mock(ITableRow.class); IDateField field = (IDateField) column.prepareEditInternal(row); - assertEquals("mandatory property to be progagated to field", column.isMandatory(), field.isMandatory()); - assertEquals("mandatory property to be progagated to field", column.isHasTime(), field.isHasTime()); + assertEquals("mandatory property to be propagated to field", column.isMandatory(), field.isMandatory()); + assertEquals("mandatory property to be propagated to field", column.isHasTime(), field.isHasTime()); } @Test @@ -60,7 +66,7 @@ public void testCompleteEdit_ParsingError() { ICell c = table.getCell(0, 0); assertEquals("invalid", c.getText()); assertEquals(date, c.getValue()); - assertNotNull(String.format("The invalid cell should have an error status: value '%s'", c.getValue(), c.getErrorStatus())); + assertNotNull(String.format("The invalid cell should have an error status: value '%s'", c.getValue()), c.getErrorStatus()); } private void setParseErrorInUI(ITableRow row, AbstractDateColumn column) { @@ -147,13 +153,72 @@ public void testHasDate() { assertTrue(dateTimeText.length() > timeOnlyText.length()); } - public class TestTable extends AbstractTable { + @Test + public void testDateGroupType() { + Date date1 = DateUtility.parse("2028-03-26", "yyyy-MM-dd"); // Sun + Date date2 = DateUtility.parse("2008-04-14", "yyyy-MM-dd"); // Mon + Date date3 = DateUtility.parse("2008-02-12", "yyyy-MM-dd"); // Tue + Date date4 = DateUtility.parse("2016-02-16", "yyyy-MM-dd"); // Tue + + IStringColumn stringColumn = new AbstractStringColumn() { + }; + TestTable table = new TestTable() { + @Override + protected void injectColumnsInternal(OrderedCollection> columns) { + columns.addLast(stringColumn); + } + }; + IDateColumn dateColumn = table.getTestDateColumn(); + table.addRowByArray(new Object[]{null, "Null"}); + table.addRowByArray(new Object[]{date1, "Foo"}); + table.addRowByArray(new Object[]{date2, "Bar"}); + table.addRowByArray(new Object[]{date3, "AAA"}); + table.addRowByArray(new Object[]{date4, "BBB"}); + + // ----- + + table.getColumnSet().addSortColumn(dateColumn, false); + table.sort(); + assertEquals(Arrays.asList(date1, date4, date2, date3, null), dateColumn.getValues()); + + table.getColumnSet().addGroupingColumn(dateColumn, true); + table.sort(); + assertEquals(Arrays.asList(null, date3, date2, date4, date1), dateColumn.getValues()); + + // ----- + + dateColumn.setGroupType(DateGroupType.MONTH); + table.sort(); + assertEquals(Arrays.asList(null, date3, date4, date1, date2), dateColumn.getValues()); + + table.getColumnSet().addSortColumn(stringColumn, false); // additional sorting + table.sort(); + assertEquals(Arrays.asList(null, date4, date3, date1, date2), dateColumn.getValues()); + + table.getColumnSet().removeSortColumn(stringColumn); // remove additional sorting + table.sort(); + assertEquals(Arrays.asList(null, date3, date4, date1, date2), dateColumn.getValues()); + + // ----- + + dateColumn.setGroupType(DateGroupType.WEEKDAY); + RunContexts.empty() + .withLocale(Locale.GERMANY) // first day of week = Monday + .run(() -> table.sort()); + assertEquals(Arrays.asList(null, date2, date3, date4, date1), dateColumn.getValues()); + RunContexts.empty() + .withLocale(Locale.US) // first day of week = Sunday + .run(() -> table.sort()); + assertEquals(Arrays.asList(null, date1, date2, date3, date4), dateColumn.getValues()); + } + + protected static class TestTable extends AbstractTable { public TestDateColumn getTestDateColumn() { return getColumnSet().getColumnByClass(TestDateColumn.class); } - @Order(70) + @Order(10) public class TestDateColumn extends AbstractDateColumn { @Override diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/services/common/bookmark/internal/BookmarkUtility.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/services/common/bookmark/internal/BookmarkUtility.java index 98c88ea0ff6..4245e990db7 100644 --- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/services/common/bookmark/internal/BookmarkUtility.java +++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/services/common/bookmark/internal/BookmarkUtility.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -30,6 +30,7 @@ import org.eclipse.scout.rt.client.ui.basic.table.ITable; import org.eclipse.scout.rt.client.ui.basic.table.ITableRow; import org.eclipse.scout.rt.client.ui.basic.table.columns.IColumn; +import org.eclipse.scout.rt.client.ui.basic.table.columns.IDateColumn; import org.eclipse.scout.rt.client.ui.basic.table.columns.INumberColumn; import org.eclipse.scout.rt.client.ui.basic.table.customizer.ITableCustomizer; import org.eclipse.scout.rt.client.ui.basic.table.userfilter.TableUserFilterManager; @@ -399,6 +400,9 @@ public static List backupTableColumns(ITable table) { colState.setSortOrder(sortOrder); colState.setSortAscending(c.isSortAscending()); colState.setGroupingActive(c.isGroupingActive()); + if (c.isGroupingActive() && c instanceof IDateColumn dateColumn) { + colState.setDateGroupType(dateColumn.getGroupType()); + } } else { colState.setSortOrder(-1); @@ -440,15 +444,19 @@ public static void restoreTableColumns(ITable table, List oldC columnSet.setVisibleColumns(visibleColumns); } - //aggregation functions and background effect: for (TableColumnState colState : oldColumns) { - + // aggregation functions and background effect IColumn col = resolveColumn(columnSet.getColumns(), colState.getClassName()); - if (col instanceof INumberColumn) { + if (col instanceof INumberColumn numberColumn) { if (colState.getAggregationFunction() != null) { - ((INumberColumn) col).setAggregationFunction(colState.getAggregationFunction()); + numberColumn.setAggregationFunction(colState.getAggregationFunction()); } - ((INumberColumn) col).setBackgroundEffect(colState.getBackgroundEffect()); + numberColumn.setBackgroundEffect(colState.getBackgroundEffect()); + } + + // date group type + if (col instanceof IDateColumn dateColumn) { + dateColumn.setGroupType(colState.getDateGroupType()); } } diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/ClientUIPreferences.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/ClientUIPreferences.java index 8df3860e0e6..58f1db57c07 100644 --- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/ClientUIPreferences.java +++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/ClientUIPreferences.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2025 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -17,16 +17,19 @@ import java.util.Optional; import java.util.Set; +import org.eclipse.scout.rt.api.data.table.DateGroupType; import org.eclipse.scout.rt.client.IClientSession; import org.eclipse.scout.rt.client.services.common.prefs.IPreferences; import org.eclipse.scout.rt.client.services.common.prefs.Preferences; import org.eclipse.scout.rt.client.session.ClientSessionProvider; import org.eclipse.scout.rt.client.ui.basic.table.ITable; import org.eclipse.scout.rt.client.ui.basic.table.columns.IColumn; +import org.eclipse.scout.rt.client.ui.basic.table.columns.IDateColumn; import org.eclipse.scout.rt.client.ui.basic.table.columns.INumberColumn; import org.eclipse.scout.rt.client.ui.basic.table.customizer.ITableCustomizer; import org.eclipse.scout.rt.client.ui.form.fields.splitbox.ISplitBox; import org.eclipse.scout.rt.dataobject.IDataObjectMapper; +import org.eclipse.scout.rt.dataobject.enumeration.EnumResolver; import org.eclipse.scout.rt.platform.BEANS; import org.eclipse.scout.rt.platform.Bean; import org.eclipse.scout.rt.platform.exception.PlatformError; @@ -81,6 +84,7 @@ public static ClientUIPreferences getInstance(IClientSession session) { protected static final String TABLE_COLUMN_GROUPED = "table.column.grouped."; protected static final String TABLE_COLUMN_AGGR_FUNCTION = "table.column.aggr.function."; protected static final String TABLE_COLUMN_BACKGROUND_EFFECT = "table.column.background.effect."; + protected static final String TABLE_COLUMN_DATE_GROUP_TYPE = "table.column.dateGroupType."; protected static final String TABLE_COLUMN_SORT_ASC = "table.column.sortAsc."; protected static final String TABLE_COLUMN_SORT_EXPLICIT = "table.column.sortExplicit."; protected static final String TABLE_TILE_MODE = "table.tile.mode"; @@ -452,6 +456,11 @@ public void setTableColumnPreferences(IColumn col, boolean flush, String configN else { m_prefs.put(key, "false"); } + if (grouped && col instanceof IDateColumn dateColumn) { + key = createColumnConfigKey(col, configName, TABLE_COLUMN_DATE_GROUP_TYPE); + DateGroupType dateGroupType = dateColumn.getGroupType(); + m_prefs.put(key, dateGroupType == null ? null : dateGroupType.stringValue()); + } // key = createColumnConfigKey(col, configName, TABLE_COLUMN_AGGR_FUNCTION); m_prefs.put(key, aggregationFunction); @@ -477,6 +486,7 @@ public void renameTableColumnPreferences(IColumn col, String oldConfigName, Stri renameEntry(createColumnConfigKey(col, oldConfigName, TABLE_COLUMN_AGGR_FUNCTION), createColumnConfigKey(col, newConfigName, TABLE_COLUMN_AGGR_FUNCTION)); renameEntry(createColumnConfigKey(col, oldConfigName, TABLE_COLUMN_SORT_EXPLICIT), createColumnConfigKey(col, newConfigName, TABLE_COLUMN_SORT_EXPLICIT)); renameEntry(createColumnConfigKey(col, oldConfigName, TABLE_COLUMN_BACKGROUND_EFFECT), createColumnConfigKey(col, newConfigName, TABLE_COLUMN_BACKGROUND_EFFECT)); + renameEntry(createColumnConfigKey(col, oldConfigName, TABLE_COLUMN_DATE_GROUP_TYPE), createColumnConfigKey(col, newConfigName, TABLE_COLUMN_DATE_GROUP_TYPE)); } protected String createColumnConfigKey(IColumn col, String configName, String propertyKey) { @@ -528,6 +538,8 @@ public void removeAllTableColumnPreferences(IColumn col, String configName, bool m_prefs.remove(getUserAgentPrefix() + createColumnConfigKey(col, configName, TABLE_COLUMN_WIDTH)); // background effect m_prefs.remove(createColumnConfigKey(col, configName, TABLE_COLUMN_BACKGROUND_EFFECT)); + // date group type + m_prefs.remove(createColumnConfigKey(col, configName, TABLE_COLUMN_DATE_GROUP_TYPE)); if (flush) { flush(); @@ -691,6 +703,23 @@ public String getTableColumnBackgroundEffect(IColumn col, String defaultValue, S return m_prefs.get(key, defaultValue); } + public DateGroupType getTableColumnDateGroupType(IColumn col, DateGroupType defaultValue, String configName) { + if (m_prefs == null) { + return defaultValue; + } + String key = createColumnConfigKey(col, configName, TABLE_COLUMN_DATE_GROUP_TYPE); + String value = m_prefs.get(key, null); + if (value != null) { + try { + return BEANS.get(EnumResolver.class).resolve(DateGroupType.class, value); + } + catch (Exception e) { + LOG.warn("could not get table column date group type for [{}]. Loaded value '{}'", col.getClass().getName(), value, e); + } + } + return defaultValue; + } + public boolean getTableColumnVisible(IColumn col, boolean defaultValue) { return getTableColumnVisible(col, defaultValue, null); } diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/AbstractTable.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/AbstractTable.java index 0bac09ceb2e..5d5913000a8 100644 --- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/AbstractTable.java +++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/AbstractTable.java @@ -29,6 +29,7 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; +import org.eclipse.scout.rt.api.data.table.DateGroupType; import org.eclipse.scout.rt.client.ModelContextProxy; import org.eclipse.scout.rt.client.ModelContextProxy.ModelContext; import org.eclipse.scout.rt.client.extension.ui.action.tree.MoveActionNodesHandler; @@ -71,6 +72,7 @@ import org.eclipse.scout.rt.client.ui.basic.table.columns.AbstractStringColumn; import org.eclipse.scout.rt.client.ui.basic.table.columns.IBooleanColumn; import org.eclipse.scout.rt.client.ui.basic.table.columns.IColumn; +import org.eclipse.scout.rt.client.ui.basic.table.columns.IDateColumn; import org.eclipse.scout.rt.client.ui.basic.table.columns.INumberColumn; import org.eclipse.scout.rt.client.ui.basic.table.controls.AbstractTableControl; import org.eclipse.scout.rt.client.ui.basic.table.controls.ITableControl; @@ -3980,6 +3982,11 @@ private void resetColumnsInternal(Set options) { if (options.contains(IResetColumnsOption.SORTING)) { getColumnSet().resetSortingAndGrouping(); + for (IColumn col : getColumns()) { + if (col instanceof IDateColumn dateColumn) { + dateColumn.setGroupType(dateColumn.getInitialGroupType()); + } + } } if (options.contains(IResetColumnsOption.WIDTHS)) { @@ -4774,6 +4781,18 @@ public void setColumnBackgroundEffect(INumberColumn column, String effect) { } } + @Override + public void setDateGroupTypeFromUI(IDateColumn column, DateGroupType groupType) { + try { + pushUIProcessor(); + column.setGroupType(groupType); + ClientUIPreferences.getInstance().setAllTableColumnPreferences(AbstractTable.this); + } + finally { + popUIProcessor(); + } + } + @Override public void setCheckedRowsFromUI(List rows, boolean checked) { if (!isEnabled()) { diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/ColumnSet.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/ColumnSet.java index 3169d4a1e9f..330d142adbd 100644 --- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/ColumnSet.java +++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/ColumnSet.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -20,9 +20,11 @@ import java.util.SortedMap; import java.util.TreeMap; +import org.eclipse.scout.rt.api.data.table.DateGroupType; import org.eclipse.scout.rt.client.ui.ClientUIPreferences; import org.eclipse.scout.rt.client.ui.basic.table.columns.AbstractColumn; import org.eclipse.scout.rt.client.ui.basic.table.columns.IColumn; +import org.eclipse.scout.rt.client.ui.basic.table.columns.IDateColumn; import org.eclipse.scout.rt.client.ui.basic.table.columns.INumberColumn; import org.eclipse.scout.rt.platform.Replace; import org.eclipse.scout.rt.platform.reflect.ConfigurationUtility; @@ -191,6 +193,7 @@ private static class P_SortingAndGroupingConfig { private int m_sortIndex; private boolean m_ascending; private boolean m_grouped; + private DateGroupType m_dateGroupType; private String m_aggregationFunction; public int getSortIndex() { @@ -217,6 +220,14 @@ public void setGrouped(boolean grouped) { m_grouped = grouped; } + public DateGroupType getDateGroupType() { + return m_dateGroupType; + } + + public void setDateGroupType(DateGroupType dateGroupType) { + m_dateGroupType = dateGroupType; + } + public String getAggregationFunction() { return m_aggregationFunction; } @@ -230,13 +241,17 @@ private P_SortingAndGroupingConfig createSortingAndGroupingConfig(IColumn col if (col.isInitialAlwaysIncludeSortAtBegin() || col.isInitialAlwaysIncludeSortAtEnd()) { return createSortingAndGroupingConfig(col); } + P_SortingAndGroupingConfig config = new P_SortingAndGroupingConfig(); ClientUIPreferences prefs = ClientUIPreferences.getInstance(); config.setSortIndex(prefs.getTableColumnSortIndex(col, col.getInitialSortIndex(), configName)); config.setAscending(prefs.getTableColumnSortAscending(col, col.isInitialSortAscending(), configName)); config.setGrouped(prefs.getTableColumnGrouped(col, col.isInitialGrouped(), configName)); - if (col instanceof INumberColumn) { - config.setAggregationFunction(prefs.getTableColumnAggregationFunction(col, ((INumberColumn) col).getInitialAggregationFunction(), configName)); + if (col instanceof INumberColumn numberColumn) { + config.setAggregationFunction(prefs.getTableColumnAggregationFunction(col, numberColumn.getInitialAggregationFunction(), configName)); + } + if (col instanceof IDateColumn dateColumn) { + config.setDateGroupType(prefs.getTableColumnDateGroupType(col, dateColumn.getInitialGroupType(), configName)); } return config; } @@ -246,8 +261,11 @@ private P_SortingAndGroupingConfig createSortingAndGroupingConfig(IColumn col config.setSortIndex(col.getInitialSortIndex()); config.setAscending(col.isInitialSortAscending()); config.setGrouped(col.isInitialGrouped()); - if (col instanceof INumberColumn) { - config.setAggregationFunction(((INumberColumn) col).getInitialAggregationFunction()); + if (col instanceof INumberColumn numberColumn) { + config.setAggregationFunction(numberColumn.getInitialAggregationFunction()); + } + if (col instanceof IDateColumn dateColumn) { + config.setDateGroupType(dateColumn.getGroupType()); } return config; } @@ -278,8 +296,12 @@ else if (col.isInitialAlwaysIncludeSortAtEnd()) { } //aggregation function: - if (col instanceof INumberColumn) { - ((INumberColumn) col).setAggregationFunction(columnConfigs.get(col).getAggregationFunction()); + if (col instanceof INumberColumn numberColumn) { + numberColumn.setAggregationFunction(columnConfigs.get(col).getAggregationFunction()); + } + // date group type + if (col instanceof IDateColumn dateColumn) { + dateColumn.setGroupType(columnConfigs.get(col).getDateGroupType()); } index++; @@ -1397,6 +1419,13 @@ private void fireColumnBackgroundEffectChanged(IColumn c) { m_table.fireTableEventInternal(e); } + private void fireColumnDateGroupTypeChanged(IColumn c) { + Assertions.assertInstance(c, IDateColumn.class, "DateGroupType is only supported on DateColumns."); + TableEvent e = new TableEvent(m_table, TableEvent.TYPE_COLUMN_DATE_GROUP_TYPE_CHANGED); + e.setColumns(CollectionUtility.arrayList(c)); + m_table.fireTableEventInternal(e); + } + private void fireColumnStructureChanged() { TableEvent e = new TableEvent(m_table, TableEvent.TYPE_COLUMN_STRUCTURE_CHANGED); m_table.fireTableEventInternal(e); @@ -1437,6 +1466,10 @@ public void propertyChange(PropertyChangeEvent e) { fireColumnBackgroundEffectChanged(c); return; } + if (IDateColumn.PROP_GROUP_TYPE.equals(e.getPropertyName())) { + fireColumnDateGroupTypeChanged(c); + return; + } if (c.isGroupingActive() && IColumn.PROP_VISIBLE.equals(e.getPropertyName())) { onGroupedColumnInvisible(c); //also notifies table and to invalidate sorting. } diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/ITableUIFacade.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/ITableUIFacade.java index 91acc512c93..eb904dbebb3 100644 --- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/ITableUIFacade.java +++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/ITableUIFacade.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -12,8 +12,10 @@ import java.util.Collection; import java.util.List; +import org.eclipse.scout.rt.api.data.table.DateGroupType; import org.eclipse.scout.rt.client.ui.MouseButton; import org.eclipse.scout.rt.client.ui.basic.table.columns.IColumn; +import org.eclipse.scout.rt.client.ui.basic.table.columns.IDateColumn; import org.eclipse.scout.rt.client.ui.basic.table.columns.INumberColumn; import org.eclipse.scout.rt.client.ui.basic.userfilter.IUserFilterState; import org.eclipse.scout.rt.client.ui.desktop.outline.pages.IReloadReason; @@ -128,4 +130,6 @@ public interface ITableUIFacade { void removeFilteredRowsFromUI(); void setColumnBackgroundEffect(INumberColumn column, String mode); + + void setDateGroupTypeFromUI(IDateColumn column, DateGroupType groupType); } diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/TableEvent.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/TableEvent.java index bc576185c7e..23519739f47 100644 --- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/TableEvent.java +++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/TableEvent.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -170,6 +170,7 @@ public class TableEvent extends EventObject implements IModelEvent { public static final int TYPE_COLUMN_AGGREGATION_CHANGED = 950; public static final int TYPE_COLUMN_BACKGROUND_EFFECT_CHANGED = 960; + public static final int TYPE_COLUMN_DATE_GROUP_TYPE_CHANGED = 970; private final int m_type; private List m_rows; diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/TableEventBuffer.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/TableEventBuffer.java index e702f1c9828..42556d1b70a 100644 --- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/TableEventBuffer.java +++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/TableEventBuffer.java @@ -137,6 +137,7 @@ else if (type == TableEvent.TYPE_COLUMN_STRUCTURE_CHANGED) { // ignore all previous aggregate function changes. typesToDelete.add(TableEvent.TYPE_COLUMN_AGGREGATION_CHANGED); typesToDelete.add(TableEvent.TYPE_COLUMN_BACKGROUND_EFFECT_CHANGED); + typesToDelete.add(TableEvent.TYPE_COLUMN_DATE_GROUP_TYPE_CHANGED); typesToDelete.add(TableEvent.TYPE_COLUMN_STRUCTURE_CHANGED); } else if (isIgnorePrevious(type)) { @@ -470,8 +471,8 @@ protected boolean isIgnorePrevious(int type) { */ protected boolean isCoalesceConsecutivePrevious(int type) { return switch (type) { - case TableEvent.TYPE_ROWS_UPDATED, TableEvent.TYPE_ROWS_INSERTED, TableEvent.TYPE_ROWS_DELETED, TableEvent.TYPE_ROWS_CHECKED, TableEvent.TYPE_COLUMN_AGGREGATION_CHANGED, TableEvent.TYPE_COLUMN_BACKGROUND_EFFECT_CHANGED, - TableEvent.TYPE_COLUMN_HEADERS_UPDATED -> true; + case TableEvent.TYPE_ROWS_UPDATED, org.eclipse.scout.rt.client.ui.basic.table.TableEvent.TYPE_ROWS_INSERTED, org.eclipse.scout.rt.client.ui.basic.table.TableEvent.TYPE_ROWS_DELETED, org.eclipse.scout.rt.client.ui.basic.table.TableEvent.TYPE_ROWS_CHECKED, org.eclipse.scout.rt.client.ui.basic.table.TableEvent.TYPE_COLUMN_AGGREGATION_CHANGED, org.eclipse.scout.rt.client.ui.basic.table.TableEvent.TYPE_COLUMN_BACKGROUND_EFFECT_CHANGED, + TableEvent.TYPE_COLUMN_HEADERS_UPDATED, TableEvent.TYPE_COLUMN_DATE_GROUP_TYPE_CHANGED -> true; default -> false; }; } diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/TableListeners.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/TableListeners.java index 0781ad8de05..38c77ef9695 100644 --- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/TableListeners.java +++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/TableListeners.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -41,7 +41,8 @@ public final class TableListeners extends AbstractGroupedListenerList implements private boolean m_hasDate; private Date m_autoDate; + private DateGroupType m_initialGroupType; + public AbstractDateColumn() { this(true); } @@ -118,11 +122,29 @@ protected String getConfiguredGroupFormat() { return "yyyy"; } + /** + * Configures the type used to group this column. If set, the {@link #getConfiguredGroupFormat() group format} is ignored. + *

+ * Subclasses can override this method. Default is {@code null}, i.e. the configured group format is used to group the data. + */ + @ConfigProperty(ConfigProperty.OBJECT) + @Order(154) + protected DateGroupType getConfiguredGroupType() { + return null; + } + @Override protected boolean getConfiguredUiSortPossible() { return true; } + @Override + public void initColumn() { + ClientUIPreferences prefs = ClientUIPreferences.getInstance(); + setGroupType(prefs.getTableColumnDateGroupType(this, getGroupType(), null)); + super.initColumn(); + } + @Override protected void initConfig() { super.initConfig(); @@ -131,6 +153,8 @@ protected void initConfig() { setHasTime(getConfiguredHasTime()); setAutoDate(getConfiguredAutoDate()); setGroupFormat(getConfiguredGroupFormat()); + setGroupType(getConfiguredGroupType()); + setInitialGroupType(getConfiguredGroupType()); } /* @@ -189,6 +213,26 @@ public void setGroupFormat(String groupFormat) { propertySupport.setPropertyString(PROP_GROUP_FORMAT, groupFormat); } + @Override + public DateGroupType getGroupType() { + return (DateGroupType) propertySupport.getProperty(PROP_GROUP_TYPE); + } + + @Override + public void setGroupType(DateGroupType groupType) { + propertySupport.setProperty(PROP_GROUP_TYPE, groupType); + } + + @Override + public DateGroupType getInitialGroupType() { + return m_initialGroupType; + } + + @Override + public void setInitialGroupType(DateGroupType initialGroupType) { + m_initialGroupType = initialGroupType; + } + @Override protected IFormField prepareEditInternal(ITableRow row) { IDateField f = (IDateField) getDefaultEditor(); @@ -246,6 +290,40 @@ else if (!isHasDate() && isHasTime()) { return df; } + @Override + public int compareTableRows(ITableRow r1, ITableRow r2) { + // --------------------------------------------------------------------------- + // Keep implementation in sync with DateColumn.ts#compare + // --------------------------------------------------------------------------- + + Date d1 = getValue(r1); + Date d2 = getValue(r2); + if (d1 == null && d2 == null) { + return 0; + } + if (d1 == null) { + return -1; + } + if (d2 == null) { + return 1; + } + + if (isGroupingActive() && getGroupType() != null) { + int c = getGroupType().compare(d1, d2); + if (c != 0) { + return c; + } + // If we are here, the grouped values are the same. Only return 0 if this is _not_ the last sort column, + // or else the additional columns could not have an effect on the row order. However, if this _is_ the + // last sort column, sort the values normally (-> super call). + if (getTable().getColumnSet().getSortColumns().stream().anyMatch(col -> col.getSortIndex() > getSortIndex())) { + return 0; + } + } + + return super.compareTableRows(r1, r2); + } + protected static class LocalDateColumnExtension extends LocalColumnExtension implements IDateColumnExtension { public LocalDateColumnExtension(OWNER owner) { diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/columns/AbstractSmartColumn.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/columns/AbstractSmartColumn.java index ac7d6bbc974..f705a5d9f0f 100644 --- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/columns/AbstractSmartColumn.java +++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/columns/AbstractSmartColumn.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -432,7 +432,6 @@ protected ISmartField createDefaultEditor() { @Override public int compareTableRows(ITableRow r1, ITableRow r2) { ICodeType codeType = getCodeTypeClass() != null ? BEANS.opt(getCodeTypeClass()) : null; - ILookupCall call = getLookupCall() != null ? getLookupCall() : null; if (codeType != null) { if (isSortCodesByDisplayText()) { String s1 = getDisplayText(r1); @@ -448,7 +447,7 @@ public int compareTableRows(ITableRow r1, ITableRow r2) { return c; } } - else if (call != null) { + else if (getLookupCall() != null) { String s1 = getDisplayText(r1); String s2 = getDisplayText(r2); return StringUtility.compareIgnoreCase(s1, s2); diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/columns/IDateColumn.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/columns/IDateColumn.java index d99de554a8f..8733e66fb59 100644 --- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/columns/IDateColumn.java +++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/columns/IDateColumn.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -11,10 +11,13 @@ import java.util.Date; +import org.eclipse.scout.rt.api.data.table.DateGroupType; + public interface IDateColumn extends IColumn { double MILLIS_PER_DAY = 1000.0 * 3600.0 * 24.0; String PROP_GROUP_FORMAT = "groupFormat"; + String PROP_GROUP_TYPE = "groupType"; void setFormat(String s); @@ -35,4 +38,12 @@ public interface IDateColumn extends IColumn { void setGroupFormat(String groupFormat); String getGroupFormat(); + + void setGroupType(DateGroupType groupType); + + DateGroupType getGroupType(); + + void setInitialGroupType(DateGroupType initialGroupType); + + DateGroupType getInitialGroupType(); } diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/organizer/OrganizeColumnsForm.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/organizer/OrganizeColumnsForm.java index bec28a7652f..1c4686ebe38 100644 --- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/organizer/OrganizeColumnsForm.java +++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/organizer/OrganizeColumnsForm.java @@ -1133,8 +1133,8 @@ protected void applyConfigImpl(String configName) { col.setVisible(prefs.getTableColumnVisible(col, col.isInitialVisible(), configName)); col.setWidth(prefs.getTableColumnWidth(col, col.getInitialWidth(), configName)); col.setVisibleColumnIndexHint(prefs.getTableColumnViewIndex(col, col.getInitialSortIndex(), configName)); - if (col instanceof INumberColumn) { - ((INumberColumn) col).setBackgroundEffect(prefs.getTableColumnBackgroundEffect(col, ((INumberColumn) col).getInitialBackgroundEffect(), configName)); + if (col instanceof INumberColumn numberColumn) { + numberColumn.setBackgroundEffect(prefs.getTableColumnBackgroundEffect(col, numberColumn.getInitialBackgroundEffect(), configName)); } } diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/userfilter/DateColumnUserFilterState.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/userfilter/DateColumnUserFilterState.java index 78772c95e7c..85134c71e63 100644 --- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/userfilter/DateColumnUserFilterState.java +++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/userfilter/DateColumnUserFilterState.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -12,12 +12,14 @@ import java.io.Serial; import java.util.Date; +import org.eclipse.scout.rt.api.data.table.DateGroupType; import org.eclipse.scout.rt.client.ui.basic.table.columns.IColumn; public class DateColumnUserFilterState extends ColumnUserFilterState { @Serial private static final long serialVersionUID = 1L; + private DateGroupType m_groupType; private Date m_dateFrom; private Date m_dateTo; @@ -25,6 +27,14 @@ public DateColumnUserFilterState(IColumn column) { super(column); } + public DateGroupType getGroupType() { + return m_groupType; + } + + public void setGroupType(DateGroupType groupType) { + m_groupType = groupType; + } + public Date getDateFrom() { return m_dateFrom; } diff --git a/org.eclipse.scout.rt.nls/src/main/resources/org/eclipse/scout/rt/nls/texts/ScoutTexts.properties b/org.eclipse.scout.rt.nls/src/main/resources/org/eclipse/scout/rt/nls/texts/ScoutTexts.properties index 2f0ac5b01a8..efb03edf3b4 100644 --- a/org.eclipse.scout.rt.nls/src/main/resources/org/eclipse/scout/rt/nls/texts/ScoutTexts.properties +++ b/org.eclipse.scout.rt.nls/src/main/resources/org/eclipse/scout/rt/nls/texts/ScoutTexts.properties @@ -67,6 +67,12 @@ CopyToClipboardFromFieldBelow=Please use the context menu or shortcut to copy fi CorrelationId=Correlation ID Criteria=Criteria CustomColumAbbreviation=C +DateGroupTypeDate=Date +DateGroupTypeMonth=Month +DateGroupTypeMonthAndYear=Month and year +DateGroupTypeWeekOfYear=Week of year +DateGroupTypeWeekday=Weekday +DateGroupTypeYear=Year DateIsNotAllowed=Date is not allowed Day=Day DeepLinkError=The page you requested is not available. The resource may have been removed or the URL in the address bar is incorrect. diff --git a/org.eclipse.scout.rt.nls/src/main/resources/org/eclipse/scout/rt/nls/texts/ScoutTexts_de.properties b/org.eclipse.scout.rt.nls/src/main/resources/org/eclipse/scout/rt/nls/texts/ScoutTexts_de.properties index 19963350275..86ae52f9bf1 100644 --- a/org.eclipse.scout.rt.nls/src/main/resources/org/eclipse/scout/rt/nls/texts/ScoutTexts_de.properties +++ b/org.eclipse.scout.rt.nls/src/main/resources/org/eclipse/scout/rt/nls/texts/ScoutTexts_de.properties @@ -67,6 +67,12 @@ CopyToClipboardFromFieldBelow=Bitte mithilfe des Kontextmenüs oder der Tastenko CorrelationId=Analyse-ID Criteria=Kriterien CustomColumAbbreviation=C +DateGroupTypeDate=Datum +DateGroupTypeMonth=Monat +DateGroupTypeMonthAndYear=Monat und Jahr +DateGroupTypeWeekOfYear=Kalenderwoche +DateGroupTypeWeekday=Wochentag +DateGroupTypeYear=Jahr DateIsNotAllowed=Unzulässiges Datum Day=Tag DeepLinkError=Die Seite kann nicht angezeigt werden. Möglicherweise ist die gewünschte Ressource nicht mehr verfügbar oder die URL in der Adresszeile ist nicht korrekt. diff --git a/org.eclipse.scout.rt.nls/src/main/resources/org/eclipse/scout/rt/nls/texts/ScoutTexts_fr.properties b/org.eclipse.scout.rt.nls/src/main/resources/org/eclipse/scout/rt/nls/texts/ScoutTexts_fr.properties index f8c45b241a1..086695a0302 100644 --- a/org.eclipse.scout.rt.nls/src/main/resources/org/eclipse/scout/rt/nls/texts/ScoutTexts_fr.properties +++ b/org.eclipse.scout.rt.nls/src/main/resources/org/eclipse/scout/rt/nls/texts/ScoutTexts_fr.properties @@ -67,6 +67,12 @@ CopyToClipboardFromFieldBelow=Veuillez copier le contenu du champ dans le presse CorrelationId=Analyse de l'ID Criteria=Critères de recherche CustomColumAbbreviation=C +DateGroupTypeDate=Date +DateGroupTypeMonth=Mois +DateGroupTypeMonthAndYear=Moise et annéé +DateGroupTypeWeekOfYear=Semaine calendrier +DateGroupTypeWeekday=Jour de la semaine +DateGroupTypeYear=Année DateIsNotAllowed=Date interdite Day=Jour DeepLinkError=La page demandée n'est pas disponible. Il se peut que la ressource ait été supprimée ou que l'URL de la ligne d'adresse soit incorrecte. diff --git a/org.eclipse.scout.rt.nls/src/main/resources/org/eclipse/scout/rt/nls/texts/ScoutTexts_it.properties b/org.eclipse.scout.rt.nls/src/main/resources/org/eclipse/scout/rt/nls/texts/ScoutTexts_it.properties index 7ef94dfc351..47a84fb8290 100644 --- a/org.eclipse.scout.rt.nls/src/main/resources/org/eclipse/scout/rt/nls/texts/ScoutTexts_it.properties +++ b/org.eclipse.scout.rt.nls/src/main/resources/org/eclipse/scout/rt/nls/texts/ScoutTexts_it.properties @@ -67,6 +67,12 @@ CopyToClipboardFromFieldBelow=Si prega di copiare il contenuto del campo mediant CorrelationId=ID analisi Criteria=Criteri CustomColumAbbreviation=C +DateGroupTypeDate=Data +DateGroupTypeMonth=Mese +DateGroupTypeMonthAndYear=Mese e anno +DateGroupTypeWeekOfYear=Settimana di calendario +DateGroupTypeWeekday=Giorno della settimana +DateGroupTypeYear=Anno DateIsNotAllowed=Data non valida Day=Giorno DeepLinkError=Impossibile visualizzare la pagina. Probabilmente la risorsa non è più disponibile oppure l'URL nella riga dell'indirizzo non è corretto. diff --git a/org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/services/common/bookmark/TableColumnState.java b/org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/services/common/bookmark/TableColumnState.java index a880b97e374..8ff3cd33956 100644 --- a/org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/services/common/bookmark/TableColumnState.java +++ b/org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/services/common/bookmark/TableColumnState.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -12,6 +12,8 @@ import java.io.Serial; import java.io.Serializable; +import org.eclipse.scout.rt.api.data.table.DateGroupType; + public class TableColumnState implements Serializable { @Serial private static final long serialVersionUID = 1L; @@ -26,6 +28,7 @@ public class TableColumnState implements Serializable { private boolean m_groupingActive; private String m_aggregationFunction; private String m_backgroundEffect; + private DateGroupType m_dateGroupType; public TableColumnState() { super(); @@ -42,6 +45,7 @@ protected TableColumnState(TableColumnState state) { this.m_groupingActive = state.m_groupingActive; this.m_aggregationFunction = state.m_aggregationFunction; this.m_backgroundEffect = state.m_backgroundEffect; + this.m_dateGroupType = state.m_dateGroupType; } public String getClassName() { @@ -131,4 +135,12 @@ public String getBackgroundEffect() { public void setBackgroundEffect(String backgroundEffect) { m_backgroundEffect = backgroundEffect; } + + public DateGroupType getDateGroupType() { + return m_dateGroupType; + } + + public void setDateGroupType(DateGroupType dateGroupType) { + m_dateGroupType = dateGroupType; + } } diff --git a/org.eclipse.scout.rt.ui.html/src/main/java/org/eclipse/scout/rt/ui/html/UiTextContributor.java b/org.eclipse.scout.rt.ui.html/src/main/java/org/eclipse/scout/rt/ui/html/UiTextContributor.java index 2f853914797..a2d349f72f9 100644 --- a/org.eclipse.scout.rt.ui.html/src/main/java/org/eclipse/scout/rt/ui/html/UiTextContributor.java +++ b/org.eclipse.scout.rt.ui.html/src/main/java/org/eclipse/scout/rt/ui/html/UiTextContributor.java @@ -116,7 +116,13 @@ public void contribute(Set textKeys) { "FormsCannotBeSaved", "NotAllCheckedFormsCanBeSaved", "FormValidationFailedTitle", - "Width")); + "Width", + "DateGroupTypeYear", + "DateGroupTypeMonth", + "DateGroupTypeMonthAndYear", + "DateGroupTypeWeekOfYear", + "DateGroupTypeWeekday", + "DateGroupTypeDate")); // Additional text keys from org.eclipse.scout.rt.security textKeys.addAll(List.of( diff --git a/org.eclipse.scout.rt.ui.html/src/main/java/org/eclipse/scout/rt/ui/html/json/table/JsonDateColumn.java b/org.eclipse.scout.rt.ui.html/src/main/java/org/eclipse/scout/rt/ui/html/json/table/JsonDateColumn.java index 749865b25af..01dff219cd9 100644 --- a/org.eclipse.scout.rt.ui.html/src/main/java/org/eclipse/scout/rt/ui/html/json/table/JsonDateColumn.java +++ b/org.eclipse.scout.rt.ui.html/src/main/java/org/eclipse/scout/rt/ui/html/json/table/JsonDateColumn.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -47,6 +47,7 @@ public JSONObject toJson() { json.put("hasDate", getColumn().isHasDate()); json.put("hasTime", getColumn().isHasTime()); json.put(IDateColumn.PROP_GROUP_FORMAT, getColumn().getGroupFormat()); + json.put(IDateColumn.PROP_GROUP_TYPE, getColumn().getGroupType() == null ? null : getColumn().getGroupType().stringValue()); // TODO [7.0] CGU: update IDateColumnInterface // getDateFormat uses NlsLocale. IMHO getDateFormat should not perform any logic because it just a getter-> refactor. same on AbstractDateField // Alternative would be to use a clientJob or set localethreadlocal in ui thread as well, as done in rap diff --git a/org.eclipse.scout.rt.ui.html/src/main/java/org/eclipse/scout/rt/ui/html/json/table/JsonTable.java b/org.eclipse.scout.rt.ui.html/src/main/java/org/eclipse/scout/rt/ui/html/json/table/JsonTable.java index 15f99a5743e..a0d2dd17454 100644 --- a/org.eclipse.scout.rt.ui.html/src/main/java/org/eclipse/scout/rt/ui/html/json/table/JsonTable.java +++ b/org.eclipse.scout.rt.ui.html/src/main/java/org/eclipse/scout/rt/ui/html/json/table/JsonTable.java @@ -19,6 +19,7 @@ import java.util.Set; import java.util.stream.Collectors; +import org.eclipse.scout.rt.api.data.table.DateGroupType; import org.eclipse.scout.rt.client.context.ClientRunContext; import org.eclipse.scout.rt.client.context.ClientRunContexts; import org.eclipse.scout.rt.client.job.ModelJobs; @@ -40,6 +41,7 @@ import org.eclipse.scout.rt.client.ui.basic.table.TableEvent; import org.eclipse.scout.rt.client.ui.basic.table.TableListener; import org.eclipse.scout.rt.client.ui.basic.table.columns.IColumn; +import org.eclipse.scout.rt.client.ui.basic.table.columns.IDateColumn; import org.eclipse.scout.rt.client.ui.basic.table.columns.INumberColumn; import org.eclipse.scout.rt.client.ui.basic.table.controls.ITableControl; import org.eclipse.scout.rt.client.ui.basic.userfilter.IUserFilterState; @@ -49,6 +51,7 @@ import org.eclipse.scout.rt.client.ui.dnd.TextTransferObject; import org.eclipse.scout.rt.client.ui.dnd.TransferObject; import org.eclipse.scout.rt.client.ui.form.fields.IFormField; +import org.eclipse.scout.rt.dataobject.enumeration.EnumResolver; import org.eclipse.scout.rt.platform.BEANS; import org.eclipse.scout.rt.platform.exception.ProcessingException; import org.eclipse.scout.rt.platform.resource.BinaryResource; @@ -113,6 +116,7 @@ public class JsonTable extends AbstractJsonWidget implement public static final String EVENT_COLUMN_STRUCTURE_CHANGED = "columnStructureChanged"; public static final String EVENT_COLUMN_HEADERS_UPDATED = "columnHeadersUpdated"; public static final String EVENT_COLUMN_BACKGROUND_EFFECT_CHANGED = "columnBackgroundEffectChanged"; + public static final String EVENT_COLUMN_DATE_GROUP_TYPE_CHANGED = "columnDateGroupTypeChanged"; public static final String EVENT_COLUMN_ORGANIZE_ACTION = "columnOrganizeAction"; public static final String EVENT_REQUEST_FOCUS_IN_CELL = "requestFocusInCell"; public static final String EVENT_START_CELL_EDIT = "startCellEdit"; @@ -657,6 +661,9 @@ else if (EVENT_COLUMN_AGGR_FUNC_CHANGED.equals(event.getType())) { else if (EVENT_COLUMN_BACKGROUND_EFFECT_CHANGED.equals(event.getType())) { handleColumnBackgroundEffectChanged(event); } + else if (EVENT_COLUMN_DATE_GROUP_TYPE_CHANGED.equals(event.getType())) { + handleUiColumnDateGroupTypeChanged(event); + } else if (EVENT_COLUMN_ORGANIZE_ACTION.equals(event.getType())) { handleUiColumnOrganizeAction(event); } @@ -842,6 +849,14 @@ protected void handleColumnBackgroundEffectChanged(JsonEvent event) { getModel().getUIFacade().setColumnBackgroundEffect((INumberColumn) column, event.getData().optString("backgroundEffect", null)); } + protected void handleUiColumnDateGroupTypeChanged(JsonEvent event) { + addTableEventFilterCondition(TableEvent.TYPE_COLUMN_DATE_GROUP_TYPE_CHANGED); + IColumn column = extractColumn(event.getData()); + IDateColumn dateColumn = Assertions.assertInstance(column, IDateColumn.class, "DateGroupType can only be specified on date columns"); + DateGroupType groupType = BEANS.get(EnumResolver.class).resolve(DateGroupType.class, event.getData().optString("groupType", null)); + getModel().getUIFacade().setDateGroupTypeFromUI(dateColumn, groupType); + } + protected void handleUiColumnMoved(JsonEvent event) { IColumn column = extractColumn(event.getData()); int viewIndex = event.getData().getInt("index"); @@ -1369,6 +1384,7 @@ protected void processEvent(TableEvent event) { case TableEvent.TYPE_USER_FILTER_ADDED, TableEvent.TYPE_USER_FILTER_REMOVED -> handleModelUserFilterChange(event); case TableEvent.TYPE_COLUMN_AGGREGATION_CHANGED -> handleModelColumnAggregationChanged(event); case TableEvent.TYPE_COLUMN_BACKGROUND_EFFECT_CHANGED -> handleModelColumnBackgroundEffectChanged(event); + case TableEvent.TYPE_COLUMN_DATE_GROUP_TYPE_CHANGED -> handleModelColumnDateGroupTypeChanged(event); case TableEvent.TYPE_REQUEST_FOCUS_IN_CELL -> handleModelRequestFocusInCell(event); case TableEvent.TYPE_START_CELL_EDIT -> handleModelStartCellEdit(event); case TableEvent.TYPE_END_CELL_EDIT -> handleModelEndCellEdit(event); @@ -1618,6 +1634,25 @@ protected void handleModelColumnBackgroundEffectChanged(TableEvent event) { addActionEvent(EVENT_COLUMN_BACKGROUND_EFFECT_CHANGED, jsonEvent); } + protected void handleModelColumnDateGroupTypeChanged(TableEvent event) { + JSONObject jsonEvent = new JSONObject(); + JSONArray eventParts = new JSONArray(); + Collection> columns = filterVisibleColumns(event.getColumns()); + if (columns.isEmpty()) { + return; + } + for (IColumn c : columns) { + IDateColumn dateColumn = Assertions.assertInstance(c, IDateColumn.class, "DateGroupType is only supported on DateColumns"); + DateGroupType groupType = dateColumn.getGroupType(); + JSONObject eventPart = new JSONObject(); + putProperty(eventPart, "columnId", getColumnId(c)); + putProperty(eventPart, "groupType", groupType == null ? null : groupType.stringValue()); + eventParts.put(eventPart); + } + putProperty(jsonEvent, "eventParts", eventParts); + addActionEvent(EVENT_COLUMN_DATE_GROUP_TYPE_CHANGED, jsonEvent); + } + protected void handleModelRequestFocusInCell(TableEvent event) { final ITableRow row = CollectionUtility.firstElement(event.getRows()); if (row == null || !isRowAccepted(row)) { diff --git a/org.eclipse.scout.rt.ui.html/src/main/java/org/eclipse/scout/rt/ui/html/json/table/userfilter/JsonDateColumnUserFilter.java b/org.eclipse.scout.rt.ui.html/src/main/java/org/eclipse/scout/rt/ui/html/json/table/userfilter/JsonDateColumnUserFilter.java index e17e42a53b7..f8ac41cde60 100644 --- a/org.eclipse.scout.rt.ui.html/src/main/java/org/eclipse/scout/rt/ui/html/json/table/userfilter/JsonDateColumnUserFilter.java +++ b/org.eclipse.scout.rt.ui.html/src/main/java/org/eclipse/scout/rt/ui/html/json/table/userfilter/JsonDateColumnUserFilter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -11,9 +11,12 @@ import java.util.Date; +import org.eclipse.scout.rt.api.data.table.DateGroupType; import org.eclipse.scout.rt.client.ui.basic.table.columns.IColumn; import org.eclipse.scout.rt.client.ui.basic.table.userfilter.ColumnUserFilterState; import org.eclipse.scout.rt.client.ui.basic.table.userfilter.DateColumnUserFilterState; +import org.eclipse.scout.rt.dataobject.enumeration.EnumResolver; +import org.eclipse.scout.rt.platform.BEANS; import org.eclipse.scout.rt.ui.html.json.JsonDate; import org.json.JSONObject; @@ -28,6 +31,14 @@ public String getObjectType() { return "DateColumnUserFilter"; } + protected String groupTypeToJson(DateGroupType groupType) { + return groupType == null ? null : groupType.stringValue(); + } + + protected DateGroupType toGroupType(String groupTypeString) { + return BEANS.get(EnumResolver.class).resolve(DateGroupType.class, groupTypeString); + } + protected String dateToJson(Date date) { return JsonDate.format(date, JsonDate.JSON_PATTERN_DATE_ONLY, false); } @@ -40,14 +51,17 @@ protected Date toDate(String dateString) { public ColumnUserFilterState createFilterStateFromJson(IColumn column, JSONObject json) { DateColumnUserFilterState filterState = new DateColumnUserFilterState(column); filterState.setSelectedValues(createSelectedValuesFromJson(json)); - filterState.setDateFrom(toDate(json.optString("dateFrom"))); - filterState.setDateTo(toDate(json.optString("dateTo"))); + // Properties from DateColumnTableUserFilterAddedEventData: + filterState.setGroupType(toGroupType(json.optString("groupType", null))); + filterState.setDateFrom(toDate(json.optString("dateFrom", null))); + filterState.setDateTo(toDate(json.optString("dateTo", null))); return filterState; } @Override public JSONObject toJson() { JSONObject json = super.toJson(); + json.put("groupType", groupTypeToJson(getFilterState().getGroupType())); json.put("dateFrom", dateToJson(getFilterState().getDateFrom())); json.put("dateTo", dateToJson(getFilterState().getDateTo())); return json; diff --git a/org.eclipse.scout.rt.ui.html/src/main/java/org/eclipse/scout/rt/ui/html/json/table/userfilter/JsonNumberColumnUserFilter.java b/org.eclipse.scout.rt.ui.html/src/main/java/org/eclipse/scout/rt/ui/html/json/table/userfilter/JsonNumberColumnUserFilter.java index 3e627f33962..730afb22700 100644 --- a/org.eclipse.scout.rt.ui.html/src/main/java/org/eclipse/scout/rt/ui/html/json/table/userfilter/JsonNumberColumnUserFilter.java +++ b/org.eclipse.scout.rt.ui.html/src/main/java/org/eclipse/scout/rt/ui/html/json/table/userfilter/JsonNumberColumnUserFilter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -46,6 +46,7 @@ protected BigDecimal toBigDecimal(String numberString) { public ColumnUserFilterState createFilterStateFromJson(IColumn column, JSONObject json) { NumberColumnUserFilterState filterState = new NumberColumnUserFilterState(column); filterState.setSelectedValues(createSelectedValuesFromJson(json)); + // Properties from NumberColumnTableUserFilterAddedEventData: filterState.setNumberFrom(toBigDecimal(json.optString("numberFrom"))); filterState.setNumberTo(toBigDecimal(json.optString("numberTo"))); return filterState; diff --git a/org.eclipse.scout.rt.ui.html/src/main/resources/org/eclipse/scout/rt/ui/html/texts/Texts.properties b/org.eclipse.scout.rt.ui.html/src/main/resources/org/eclipse/scout/rt/ui/html/texts/Texts.properties index cb6c3900f57..cc00988bf98 100644 --- a/org.eclipse.scout.rt.ui.html/src/main/resources/org/eclipse/scout/rt/ui/html/texts/Texts.properties +++ b/org.eclipse.scout.rt.ui.html/src/main/resources/org/eclipse/scout/rt/ui/html/texts/Texts.properties @@ -74,6 +74,7 @@ ui.FilterInfoXOfY=({0} of {1}) ui.FreeText=Free text ui.FromXToY=from {0} to {1} ui.Grouping=Grouping +ui.GroupingBy=Grouping by ui.HeaderArea=Header area ui.Hierarchy=Hierarchy ui.Ignore=Ignore diff --git a/org.eclipse.scout.rt.ui.html/src/main/resources/org/eclipse/scout/rt/ui/html/texts/Texts_de.properties b/org.eclipse.scout.rt.ui.html/src/main/resources/org/eclipse/scout/rt/ui/html/texts/Texts_de.properties index 69a4aac2cec..ad0b6e883a5 100644 --- a/org.eclipse.scout.rt.ui.html/src/main/resources/org/eclipse/scout/rt/ui/html/texts/Texts_de.properties +++ b/org.eclipse.scout.rt.ui.html/src/main/resources/org/eclipse/scout/rt/ui/html/texts/Texts_de.properties @@ -74,6 +74,7 @@ ui.FilterInfoXOfY=({0} von {1}) ui.FreeText=Freitext ui.FromXToY=von {0} bis {1} ui.Grouping=Gruppierung +ui.GroupingBy=Gruppierung nach ui.HeaderArea=Kopfbereich ui.Hierarchy=Hierarchie ui.Ignore=Ignorieren diff --git a/org.eclipse.scout.rt.ui.html/src/main/resources/org/eclipse/scout/rt/ui/html/texts/Texts_fr.properties b/org.eclipse.scout.rt.ui.html/src/main/resources/org/eclipse/scout/rt/ui/html/texts/Texts_fr.properties index 524333424d3..f8707aa4f0a 100644 --- a/org.eclipse.scout.rt.ui.html/src/main/resources/org/eclipse/scout/rt/ui/html/texts/Texts_fr.properties +++ b/org.eclipse.scout.rt.ui.html/src/main/resources/org/eclipse/scout/rt/ui/html/texts/Texts_fr.properties @@ -73,6 +73,7 @@ ui.FilterInfoXOfY=({0} de {1}) ui.FreeText=Texte libre ui.FromXToY=de {0} à {1} ui.Grouping=Groupement +ui.GroupingBy=Groupement par ui.HeaderArea=Partie en-tête ui.Hierarchy=Hiérarchie ui.Ignore=Ignorer diff --git a/org.eclipse.scout.rt.ui.html/src/main/resources/org/eclipse/scout/rt/ui/html/texts/Texts_it.properties b/org.eclipse.scout.rt.ui.html/src/main/resources/org/eclipse/scout/rt/ui/html/texts/Texts_it.properties index 07ecca11131..9a95a8206e8 100644 --- a/org.eclipse.scout.rt.ui.html/src/main/resources/org/eclipse/scout/rt/ui/html/texts/Texts_it.properties +++ b/org.eclipse.scout.rt.ui.html/src/main/resources/org/eclipse/scout/rt/ui/html/texts/Texts_it.properties @@ -73,6 +73,7 @@ ui.FilterInfoXOfY=({0} di {1}) ui.FreeText=Testo libero ui.FromXToY=da {0} fino a {1} ui.Grouping=Raggruppamento +ui.GroupingBy=Raggruppamento per ui.HeaderArea=Area di intestazione ui.Hierarchy=Gerarchia ui.Ignore=Ignora