From b8c7cc6df6616e38bf264dfef28b5eb99eee0ad1 Mon Sep 17 00:00:00 2001 From: Kai Reinhard Date: Mon, 17 Feb 2025 00:36:58 +0100 Subject: [PATCH] Date and numeric filter in AG grid. --- ToDo.adoc | 2 +- gradle/libs.versions.toml | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- .../src/main/resources/i18nKeys.json | 6 +- .../common/props/PropertyType.kt} | 20 +++--- .../rest/HistoryEntryUserCommentModalRest.kt | 2 +- .../rest/orga/VisitorbookPagesRest.kt | 2 +- .../kotlin/org/projectforge/ui/UIAgGrid.kt | 4 ++ .../org/projectforge/ui/UIAgGridColumnDef.kt | 68 +++++++++++++++---- .../kotlin/org/projectforge/ui/UILayout.kt | 11 +-- .../components/table/DynamicAgGrid.jsx | 46 +++++++++---- 11 files changed, 119 insertions(+), 46 deletions(-) rename projectforge-common/src/main/{java/org/projectforge/common/props/PropertyType.java => kotlin/org/projectforge/common/props/PropertyType.kt} (74%) diff --git a/ToDo.adoc b/ToDo.adoc index b8ea073448..a1c53652c0 100644 --- a/ToDo.adoc +++ b/ToDo.adoc @@ -1,5 +1,5 @@ ==== Aktuell: -- I18n for AI grid. +- Doppelter E-Mailversand bei Urlaubseinträgen (deutsch und englisch). - JCR: Tool for removing or recovering orphaned nodes. - Favoriten bei Scriptausführung für Parameter. - Viewpage für user für non-admins. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a617558642..173ed9afa6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ com-google-zxing-javase = "3.5.3" com-googlecode-ez-vcard = "0.12.1" com-googlecode-gson = "2.11.0" com-googlecode-json-simple = "1.1.1" -com-googlecode-lanterna = "3.1.2" +com-googlecode-lanterna = "3.1.3" com-itextpdf = "5.5.13.4" com-thoughtworks-xstream = "1.4.21" com-webauthn4j-core = "0.28.2.RELEASE" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e2847c8200..d9fbee2e1d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/projectforge-application/src/main/resources/i18nKeys.json b/projectforge-application/src/main/resources/i18nKeys.json index 9c1a3dd9d7..0974a116f4 100644 --- a/projectforge-application/src/main/resources/i18nKeys.json +++ b/projectforge-application/src/main/resources/i18nKeys.json @@ -1725,12 +1725,12 @@ {"i18nKey":"ok","bundleName":"I18nResources","translation":"OK","translationDE":"OK","usedInClasses":["org.projectforge.framework.utils.ResultHolderStatus"],"usedInFiles":[]}, {"i18nKey":"onlyDeleted","bundleName":"I18nResources","translation":"only deleted","translationDE":"nur gelöschte","usedInClasses":["org.projectforge.web.humanresources.HRPlanningEditForm","org.projectforge.web.wicket.AbstractListForm"],"usedInFiles":[]}, {"i18nKey":"onlyDeleted.tooltip","bundleName":"I18nResources","translation":"Only deleted entries will be displayed (in general independent of any other filter settings).","translationDE":"Es werden nur gelöschte Datensätze angezeigt (i. d. R. unabhängig von anderen Filterangaben).","usedInClasses":["org.projectforge.web.wicket.AbstractListForm"],"usedInFiles":[]}, - {"i18nKey":"operation.deleted","bundleName":"I18nResources","translation":"deleted","translationDE":"gelöscht","usedInClasses":["org.projectforge.framework.persistence.history.HistoryFormatService"],"usedInFiles":[]}, - {"i18nKey":"operation.inserted","bundleName":"I18nResources","translation":"inserted","translationDE":"angelegt","usedInClasses":["org.projectforge.framework.persistence.history.HistoryFormatService"],"usedInFiles":[]}, + {"i18nKey":"operation.deleted","bundleName":"I18nResources","translation":"deleted","translationDE":"gelöscht","usedInClasses":["org.projectforge.framework.persistence.history.DisplayHistoryEntry","org.projectforge.framework.persistence.history.HistoryFormatService"],"usedInFiles":[]}, + {"i18nKey":"operation.inserted","bundleName":"I18nResources","translation":"inserted","translationDE":"angelegt","usedInClasses":["org.projectforge.framework.persistence.history.DisplayHistoryEntry","org.projectforge.framework.persistence.history.HistoryFormatService"],"usedInFiles":[]}, {"i18nKey":"operation.markAsDeleted","bundleName":"I18nResources","translation":"marked as deleted","translationDE":"als gelöscht markiert","usedInClasses":["org.projectforge.framework.persistence.history.HistoryFormatService"],"usedInFiles":[]}, {"i18nKey":"operation.undefined","bundleName":"I18nResources","translation":"undefined","translationDE":"undefiniert","usedInClasses":["org.projectforge.framework.persistence.history.HistoryFormatService"],"usedInFiles":[]}, {"i18nKey":"operation.undeleted","bundleName":"I18nResources","translation":"undeleted","translationDE":"wiederhergestellt","usedInClasses":["org.projectforge.framework.persistence.history.HistoryFormatService"],"usedInFiles":[]}, - {"i18nKey":"operation.updated","bundleName":"I18nResources","translation":"updated","translationDE":"geändert","usedInClasses":["org.projectforge.framework.persistence.history.HistoryFormatService","org.projectforge.rest.my2fa.My2FASetupPageRest"],"usedInFiles":[]}, + {"i18nKey":"operation.updated","bundleName":"I18nResources","translation":"updated","translationDE":"geändert","usedInClasses":["org.projectforge.framework.persistence.history.DisplayHistoryEntry","org.projectforge.framework.persistence.history.HistoryFormatService","org.projectforge.rest.my2fa.My2FASetupPageRest"],"usedInFiles":[]}, {"i18nKey":"orga.post.inhalt","bundleName":"I18nResources","translation":"Content","translationDE":"Inhalt","usedInClasses":["org.projectforge.business.orga.PostausgangDO","org.projectforge.business.orga.PosteingangDO"],"usedInFiles":[]}, {"i18nKey":"orga.post.type","bundleName":"I18nResources","translation":"Type","translationDE":"Art","usedInClasses":["org.projectforge.business.orga.PostausgangDO","org.projectforge.business.orga.PosteingangDO"],"usedInFiles":[]}, {"i18nKey":"orga.post.type.brief","bundleName":"I18nResources","translation":"Letter","translationDE":"Brief","usedInClasses":["org.projectforge.business.orga.PostType"],"usedInFiles":[]}, diff --git a/projectforge-common/src/main/java/org/projectforge/common/props/PropertyType.java b/projectforge-common/src/main/kotlin/org/projectforge/common/props/PropertyType.kt similarity index 74% rename from projectforge-common/src/main/java/org/projectforge/common/props/PropertyType.java rename to projectforge-common/src/main/kotlin/org/projectforge/common/props/PropertyType.kt index 7ea1f33e13..c04ce477ef 100644 --- a/projectforge-common/src/main/java/org/projectforge/common/props/PropertyType.java +++ b/projectforge-common/src/main/kotlin/org/projectforge/common/props/PropertyType.kt @@ -21,18 +21,22 @@ // ///////////////////////////////////////////////////////////////////////////// -package org.projectforge.common.props; +package org.projectforge.common.props /** * If the type of a field isn't represented by the Java type it may be defined in more detail by this enum. For example a BigDecimal may * represent a currency value. * @author Kai Reinhard (k.reinhard@micromata.de) */ -public enum PropertyType -{ - CURRENCY, DATE, DATE_TIME, DATE_TIME_SECONDS, DATE_TIME_MILLIS, - /** - * Use INPUT for long text fields if you wish to use input fields instead of text areas. - */ - INPUT, TIME, TIME_SECONDS, TIME_MILLIS, UNSPECIFIED; +enum class PropertyType { + CURRENCY, DATE, DATE_TIME, DATE_TIME_SECONDS, DATE_TIME_MILLIS, + + /** + * Use INPUT for long text fields if you wish to use input fields instead of text areas. + */ + INPUT, TIME, TIME_SECONDS, TIME_MILLIS, UNSPECIFIED; + + fun isIn(vararg types: PropertyType): Boolean { + return types.any { it == this } + } } diff --git a/projectforge-rest/src/main/kotlin/org/projectforge/rest/HistoryEntryUserCommentModalRest.kt b/projectforge-rest/src/main/kotlin/org/projectforge/rest/HistoryEntryUserCommentModalRest.kt index 62baa8a5a1..7f3402ca6a 100644 --- a/projectforge-rest/src/main/kotlin/org/projectforge/rest/HistoryEntryUserCommentModalRest.kt +++ b/projectforge-rest/src/main/kotlin/org/projectforge/rest/HistoryEntryUserCommentModalRest.kt @@ -109,7 +109,7 @@ class HistoryEntryUserCommentModalRest { val historyEntry = item.entry ?: return ResponseEntity(HttpStatus.NOT_FOUND) val dto = HistoryData(historyFormatService.convert(entity, historyEntry, HistoryLoadContext(item.baseDao))) val titleKey = "history.entry" - val ui = UILayout(titleKey, RestResolver.getRestUrl(this::class.java, withoutPrefix = true)) + val ui = UILayout(titleKey) ui.userAccess.update = item.writeAccess ui.userAccess.history = item.readAccess ui.add(UIReadOnlyField("timeAgo", label = "modified")) diff --git a/projectforge-rest/src/main/kotlin/org/projectforge/rest/orga/VisitorbookPagesRest.kt b/projectforge-rest/src/main/kotlin/org/projectforge/rest/orga/VisitorbookPagesRest.kt index 6791aa885f..3ef85749e5 100644 --- a/projectforge-rest/src/main/kotlin/org/projectforge/rest/orga/VisitorbookPagesRest.kt +++ b/projectforge-rest/src/main/kotlin/org/projectforge/rest/orga/VisitorbookPagesRest.kt @@ -115,7 +115,7 @@ class VisitorbookPagesRest : AbstractDTOPagesRest? = null + } var pinned: String? = null @@ -81,6 +88,8 @@ open class UIAgGridColumnDef( var suppressSizeToFit: Boolean? = null + var filterParams: FilterParams? = null + /** * https://www.ag-grid.com/react-data-grid/column-definitions/#right-aligned-and-numeric-columns */ @@ -149,6 +158,13 @@ open class UIAgGridColumnDef( return this } + fun setApplyAndResetButton(): FilterParams { + filterParams = filterParams ?: FilterParams() + return filterParams!!.also { + it.buttons = arrayOf("apply", "reset") + } + } + companion object { fun createCol( property: KProperty<*>, @@ -265,6 +281,7 @@ open class UIAgGridColumnDef( autoHeight: Boolean? = wrapText, valueIconMap: Map? = null, tooltipField: String? = null, + type: Type? = null, ): UIAgGridColumnDef { val col = UIAgGridColumnDef(field, sortable = sortable, wrapText = wrapText, autoHeight = autoHeight) lc?.idPrefix?.let { @@ -285,7 +302,7 @@ open class UIAgGridColumnDef( if (col.headerName == null) { col.headerName = translate(elementInfo.i18nKey) } - if (useFormatter == null) { + if (type == null && useFormatter == null) { // Try to determine formatter by type and propertyInfo (defined on DO-field): if (Number::class.java.isAssignableFrom(elementInfo.propertyClass)) { if (elementInfo.propertyType == PropertyType.CURRENCY) { @@ -294,7 +311,11 @@ open class UIAgGridColumnDef( useFormatter = Formatter.NUMBER } } else if (elementInfo.propertyClass == LocalDate::class.java) { - useFormatter = Formatter.DATE + if (width == null) { + col.width = DATE_WIDTH + } + col.filter = "agDateColumnFilter" + col.setApplyAndResetButton() } else if (java.util.Date::class.java == elementInfo.propertyClass) { if (field in arrayOf("created", "lastUpdate")) { useFormatter = Formatter.DATE @@ -326,6 +347,18 @@ open class UIAgGridColumnDef( col.valueGetter = "data?.${col.field}?.displayName" } } + if (type != null) { + when (type) { + Type.NUMBER -> { + if (width == null) { + col.width = NUMBER_WIDTH + } + col.type = AG_TYPE.NUMERIC_COLUMN.agType + col.filter = "agNumberColumnFilter" + col.setApplyAndResetButton() + } + } + } if (width != null) { col.width = width } @@ -334,28 +367,39 @@ open class UIAgGridColumnDef( Formatter.CURRENCY -> { if (width == null) { col.width = CURRENCY_WIDTH - col.type = AG_TYPE.NUMERIC_COLUMN.agType } + col.type = AG_TYPE.NUMERIC_COLUMN.agType + col.filter = "agNumberColumnFilter" } Formatter.NUMBER -> { if (width == null) { col.width = NUMBER_WIDTH - col.type = AG_TYPE.NUMERIC_COLUMN.agType } - } - - Formatter.DATE -> { - col.width = DATE_WIDTH + col.type = AG_TYPE.NUMERIC_COLUMN.agType + col.filter = "agNumberColumnFilter" + col.setApplyAndResetButton() } Formatter.CONSUMPTION -> { - col.width = 80 + if (width == null) { + col.width = 80 + } else { + } } - else -> {} + else -> { + } } } + if (useFormatter == null + && elementInfo?.propertyType?.isIn(PropertyType.INPUT, PropertyType.UNSPECIFIED) == true + && (elementInfo.maxLength ?: 0) > 256 + ) { + // Use text filter for long texts. + col.filter = "agTextColumnFilter" + col.setApplyAndResetButton() + } valueGetter?.let { col.valueGetter = it } var myParams: MutableMap? = null useFormatter?.let { diff --git a/projectforge-rest/src/main/kotlin/org/projectforge/ui/UILayout.kt b/projectforge-rest/src/main/kotlin/org/projectforge/ui/UILayout.kt index da1f85eaa3..17829a5a2f 100644 --- a/projectforge-rest/src/main/kotlin/org/projectforge/ui/UILayout.kt +++ b/projectforge-rest/src/main/kotlin/org/projectforge/ui/UILayout.kt @@ -32,6 +32,7 @@ import kotlin.reflect.KProperty class UILayout( title: String, /** restBaseUrl is needed, if [UIAttachmentList] is used. */ + @Suppress("unused") // Needed by React frontend. var restBaseUrl: String? = null, ) : IUIContainer { class UserAccess( @@ -39,11 +40,6 @@ class UILayout( * The user has access to the object's history, if given. */ var history: Boolean? = null, - /** - * Is the edit user-comment button visible? - * @see [org.projectforge.framework.persistence.api.BaseDao.supportsHistoryUserComments] - */ - var editHistoryComments: Boolean? = null, /** * The user has access to insert new objects. */ @@ -54,6 +50,11 @@ class UILayout( * Cancel button is visible for all users at default. */ var cancel: Boolean? = true, + /** + * Is the edit user-comment button visible? + * @see [org.projectforge.framework.persistence.api.BaseDao.supportsHistoryUserComments] + */ + var editHistoryComments: Boolean? = null, ) { fun copyFrom(userAccess: UserAccess?) { this.history = userAccess?.history diff --git a/projectforge-webapp/src/components/base/dynamicLayout/components/table/DynamicAgGrid.jsx b/projectforge-webapp/src/components/base/dynamicLayout/components/table/DynamicAgGrid.jsx index 3bb2142df7..d18cfed6d1 100644 --- a/projectforge-webapp/src/components/base/dynamicLayout/components/table/DynamicAgGrid.jsx +++ b/projectforge-webapp/src/components/base/dynamicLayout/components/table/DynamicAgGrid.jsx @@ -1,6 +1,6 @@ import AwesomeDebouncePromise from 'awesome-debounce-promise'; import PropTypes from 'prop-types'; -import React, { useMemo, useRef, useState } from 'react'; +import React, { useMemo, useRef, useEffect, useState } from 'react'; import { AgGridReact } from 'ag-grid-react'; import { LicenseManager, ModuleRegistry, AllEnterpriseModule, themeBalham } from 'ag-grid-enterprise'; import { connect } from 'react-redux'; @@ -64,6 +64,7 @@ function DynamicAgGrid(props) { const [gridApi, setGridApi] = useState(); const gridRef = useRef(); // const gridStyle = React.useMemo(() => ({ width: '100%' }), []); + const [processedColumnDefs, setProcessedColumnDefs] = useState([]); const rowData = entries || Object.getByString(data, id) || Object.getByString(variables, id) || ''; const { selectedEntityIds } = data; /* @@ -116,6 +117,35 @@ function DynamicAgGrid(props) { } }, [gridApi, data.highlightRowId, highlightId]); + useEffect(() => { + // Gehe durch alle columnDefs und setze den comparator für agDateColumnFilter + const updatedColumnDefs = columnDefs.map((col) => { + if (col.filter === 'agDateColumnFilter') { + return { + ...col, + filterParams: { + ...col.filterParams, + comparator: (filterLocalDateAtMidnight, cellValue) => { + if (!cellValue) return -1; + + // Wandelt "YYYY-MM-DD" in ein JS Date-Objekt um + const [year, month, day] = cellValue.split('-'); + const cellDate = new Date(Number(year), Number(month) - 1, Number(day)); + + if (filterLocalDateAtMidnight.getTime() === cellDate.getTime()) { + return 0; + } + return cellDate < filterLocalDateAtMidnight ? -1 : 1; + }, + }, + }; + } + return col; + }); + + setProcessedColumnDefs(updatedColumnDefs); + }, [columnDefs]); + /* React.useEffect(() => { showHighlightedRow(); @@ -157,10 +187,6 @@ function DynamicAgGrid(props) { } const redirectUrl = modifyRedirectUrl(rowClickRedirectUrl, event.data.id); if (rowClickOpenModal) { - // const historyState = { serverData: action.variables }; - // TODO: Fin, wie bekomme ich action.variables hier? Wenn das Modal geschlossen wird, - // kann ich nicht mehr das Formular ändern. - // Ich wollte das von hier kopieren: form.js:121 const historyState = { }; historyState.background = history.location; @@ -171,11 +197,6 @@ function DynamicAgGrid(props) { } }; - /* - const onFirstDataRendered = () => { - showHighlightedRow(); - }; */ - const onSelectionChanged = () => { if (!rowClickRedirectUrl) { // Do nothing @@ -293,7 +314,7 @@ function DynamicAgGrid(props) { ref={gridRef} rowData={rowData} components={allComponents} - columnDefs={columnDefs} + columnDefs={processedColumnDefs} selectionColumnDef={selectionColumnDef} rowSelection={rowSelection} onGridReady={onGridReady} @@ -342,7 +363,7 @@ function DynamicAgGrid(props) { ), [ - columnDefs, + processedColumnDefs, data, selectionColumnDef, sortModel, @@ -362,7 +383,6 @@ DynamicAgGrid.propTypes = { pinned: PropTypes.string, resizable: PropTypes.bool, sortable: PropTypes.bool, - filter: PropTypes.bool, })), id: PropTypes.string, entries: PropTypes.arrayOf(PropTypes.shape()),