From 88a8e76c0a590df032116866011e9cde437b2e36 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Fri, 10 Apr 2026 11:52:31 +0200 Subject: [PATCH 1/3] Fix number filtering input --- app/filtercontroller.cpp | 12 ++--- .../filters/components/MMFilterRangeInput.qml | 49 ++++++++++--------- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/app/filtercontroller.cpp b/app/filtercontroller.cpp index 49addf083..13f0093ed 100644 --- a/app/filtercontroller.cpp +++ b/app/filtercontroller.cpp @@ -154,14 +154,10 @@ QString FilterController::buildFieldExpression( const FieldFilter &filter ) cons } case FieldFilter::NumberFilter: { - const QString valueFrom = filter.value.toList().at( 0 ).toString(); - const QString valueTo = filter.value.toList().at( 1 ).toString(); - - if ( valueFrom.isEmpty() || valueTo.isEmpty() ) - { - expressionCopy = {}; - break; - } + const QVariant &variantFrom = filter.value.toList().at( 0 ); + const QString valueFrom = variantFrom.isValid() ? variantFrom.toString() : QString::number( std::numeric_limits::min() ); + const QVariant &variantTo = filter.value.toList().at( 1 ); + const QString valueTo = variantTo.isValid() ? variantTo.toString() : QString::number( std::numeric_limits::max() ); expressionCopy.replace( QStringLiteral( "%%value_from%%" ), valueFrom ); expressionCopy.replace( QStringLiteral( "%%value_to%%" ), valueTo ); diff --git a/app/qml/filters/components/MMFilterRangeInput.qml b/app/qml/filters/components/MMFilterRangeInput.qml index eac9bea6b..1c76444d0 100644 --- a/app/qml/filters/components/MMFilterRangeInput.qml +++ b/app/qml/filters/components/MMFilterRangeInput.qml @@ -17,30 +17,15 @@ Column { width: parent.width spacing: __style.margin8 - required property string fieldDisplayName + required property string filterName + required property string filterId required property var currentValue - required property var currentValueTo - required property string fieldLayerId - required property string fieldName - - property string initialFrom: { - let v = root.currentValue - return ( v !== null && v !== undefined ) ? String( v ) : "" - } - property string initialTo: { - let v = root.currentValueTo - return ( v !== null && v !== undefined ) ? String( v ) : "" - } - - property bool _initialized: false - Component.onCompleted: _initialized = true MMText { width: parent.width - text: root.fieldDisplayName + text: root.filterName font: __style.p6 color: __style.nightColor - visible: root.fieldDisplayName !== "" } Row { @@ -61,11 +46,10 @@ Column { width: ( parent.width - __style.margin12 ) / 2 type: MMFilterTextInput.InputType.Number placeholderText: qsTr( "Min" ) - text: root.initialFrom + text: root.currentValue && root.currentValue[0] ? root.currentValue[0] : "" errorMsg: rangeRow.rangeInvalid ? qsTr( "\"Min\" must be less than \"Max\"" ) : "" onTextChanged: { - if ( !root._initialized ) return debounceTimer.restart() } } @@ -76,11 +60,10 @@ Column { width: ( parent.width - __style.margin12 ) / 2 type: MMFilterTextInput.InputType.Number placeholderText: qsTr( "Max" ) - text: root.initialTo + text: root.currentValue && root.currentValue[1] ? root.currentValue[1] : "" errorMsg: rangeRow.rangeInvalid ? qsTr( "\"Min\" must be less than \"Max\"" ) : "" onTextChanged: { - if ( !root._initialized ) return debounceTimer.restart() } } @@ -91,8 +74,26 @@ Column { interval: 300 repeat: false onTriggered: { - if ( root.fieldLayerId && root.fieldName ) - __activeProject.filterController.setNumberFilter( root.fieldLayerId, root.fieldName, fromInput.text, toInput.text ) + let newValues = [] + const valueFrom = parseFloat(fromInput.text) + if ( !isNaN(valueFrom) ) { + newValues[0] = valueFrom + } else { + newValues[0] = undefined + } + + const valueTo = parseFloat(toInput.text) + if ( !isNaN(valueTo) ) { + newValues[1] = valueTo + } else { + newValues[1] = undefined + } + + if ( newValues[0] === undefined && newValues[1] === undefined ) { + root.currentValue = undefined + } else { + root.currentValue = newValues + } } } } From 6242142cdae67d00827338c88d86a16b456f5829 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Mon, 13 Apr 2026 15:34:27 +0200 Subject: [PATCH 2/3] Fix Date filters --- app/filtercontroller.cpp | 29 ++-- app/filtercontroller.h | 5 + .../filters/components/MMFilterDateRange.qml | 151 +++++++++++------- 3 files changed, 122 insertions(+), 63 deletions(-) diff --git a/app/filtercontroller.cpp b/app/filtercontroller.cpp index 13f0093ed..21ca2f058 100644 --- a/app/filtercontroller.cpp +++ b/app/filtercontroller.cpp @@ -169,14 +169,13 @@ QString FilterController::buildFieldExpression( const FieldFilter &filter ) cons // so we must convert local datetimes to UTC before comparing. // Use a custom format to avoid the 'Z' suffix that Qt::ISODate adds for UTC. const QString isoFormat = QStringLiteral( "yyyy-MM-ddTHH:mm:ss" ); - const QString dateFrom = filter.value.toList().at( 0 ).toDateTime().toUTC().toString( isoFormat ); - const QString dateTo = filter.value.toList().at( 1 ).toDateTime().toUTC().toString( isoFormat ); + const QString minimumDateTime = QStringLiteral( "0001-01-01T00:00:00" ); + const QString maximumDateTime = QStringLiteral( "9999-12-31T23:59:59" ); - if ( dateFrom.isEmpty() || dateTo.isEmpty() ) - { - expressionCopy = {}; - break; - } + const QVariant &variantFrom = filter.value.toList().at( 0 ); + const QString dateFrom = variantFrom.isValid() ? variantFrom.toDateTime().toString( isoFormat ) : minimumDateTime; + const QVariant &variantTo = filter.value.toList().at( 1 ); + const QString dateTo = variantTo.isValid() ? variantTo.toDateTime().toString( isoFormat ) : maximumDateTime; expressionCopy.replace( QStringLiteral( "%%value_from%%" ), QgsExpression::quotedString( dateFrom ) ); expressionCopy.replace( QStringLiteral( "%%value_to%%" ), QgsExpression::quotedString( dateTo ) ); @@ -311,8 +310,6 @@ void FilterController::processFilters( const QVariantMap &newFilters ) { if ( newFilters.contains( filter.filterId ) ) { - //TODO: we need to have both upper and lower bounds for numbers and dates, - //if user didn't supply use numeric_limits for numbers and year 1 to 9999 for dates filter.value = newFilters.value( filter.filterId ); } } @@ -330,6 +327,20 @@ bool FilterController::hasActiveFilterOnLayer( const QString &layerId ) return !layer->subsetString().isEmpty(); } +bool FilterController::isDateFilterDateTime(const QString& filterId) +{ + for ( FieldFilter &filter : mFieldFilters ) + { + if ( filter.filterId == filterId ) + { + const QgsVectorLayer *layer = qobject_cast( QgsProject::instance()->mapLayer(filter.layerId) ); + const QMetaType::Type fieldType = layer->fields().field(filter.fieldName).type(); + return fieldType == QMetaType::QDateTime; + } + } + return false; +} + QVariantMap FilterController::getDropdownConfiguration( const QString &filterId ) { if ( filterId.isEmpty() ) return {}; diff --git a/app/filtercontroller.h b/app/filtercontroller.h index baef4b467..d1ed2327d 100644 --- a/app/filtercontroller.h +++ b/app/filtercontroller.h @@ -115,6 +115,11 @@ class FilterController : public QObject */ Q_INVOKABLE bool hasActiveFilterOnLayer( const QString &layerId ); + /** + * Returns whether the date filter is datetime or just date field. Used to show date or date & time UI for users. + */ + Q_INVOKABLE bool isDateFilterDateTime( const QString &filterId ); + bool hasFiltersAvailable() const; bool hasFiltersEnabled() const; diff --git a/app/qml/filters/components/MMFilterDateRange.qml b/app/qml/filters/components/MMFilterDateRange.qml index e7bc825c3..3e9ddd08a 100644 --- a/app/qml/filters/components/MMFilterDateRange.qml +++ b/app/qml/filters/components/MMFilterDateRange.qml @@ -6,6 +6,7 @@ * (at your option) any later version. * * * ***************************************************************************/ +pragma ComponentBehavior: Bound import QtQuick @@ -18,48 +19,23 @@ Column { width: parent.width spacing: __style.margin8 - required property string fieldDisplayName - required property bool hasTime + required property string filterName + required property string filterId required property var currentValue - required property var currentValueTo - required property string fieldLayerId - required property string fieldName - - property var initialFromDate: { - let v = root.currentValue - if ( v === null || v === undefined ) return null - let d = new Date( v ) - return isNaN( d.getTime() ) ? null : d - } - property var initialToDate: { - let v = root.currentValueTo - if ( v === null || v === undefined ) return null - let d = new Date( v ) - return isNaN( d.getTime() ) ? null : d - } - - property var fromDate: initialFromDate - property var toDate: initialToDate - - property bool rangeInvalid: fromDate !== null && toDate !== null && fromDate.getTime() > toDate.getTime() + readonly property bool hasTime: __activeProject.filterController.isDateFilterDateTime(filterId) - property bool _initialized: false - Component.onCompleted: _initialized = true - - function applyDateFilter() { - if ( !_initialized || !fieldLayerId || !fieldName ) return - __activeProject.filterController.setDateFilter( fieldLayerId, fieldName, fromDate, toDate, hasTime ) + property bool rangeInvalid: { + if ( !currentValue || !currentValue[0] || !currentValue[1] ){ + return false + } + return currentValue[0] > currentValue[1] } - onFromDateChanged: applyDateFilter() - onToDateChanged: applyDateFilter() - MMText { width: parent.width - text: root.fieldDisplayName + text: root.filterName font: __style.p6 color: __style.nightColor - visible: root.fieldDisplayName !== "" } Row { @@ -75,21 +51,43 @@ Column { width: parent.width type: MMFilterTextInput.InputType.Date - checked: root.fromDate !== null && !root.rangeInvalid placeholderText: qsTr( "From" ) - errorMsg: root.rangeInvalid ? qsTr( "\"From\" must be less than \"To\"" ) : "" + errorMsg: root.rangeInvalid ? qsTr( "\"From\" must be sooner than \"To\"" ) : "" text: { - if ( !root.fromDate ) return "" - if ( root.hasTime ) return Qt.formatDateTime( root.fromDate, Qt.DefaultLocaleShortDate ) - return Qt.formatDate( root.fromDate, Qt.DefaultLocaleShortDate ) + if ( !root.currentValue || !root.currentValue[0] ) return "" + if ( root.hasTime ) return Qt.formatDateTime( root.currentValue[0] ) + return Qt.formatDate( root.currentValue[0] ) } onTextClicked: fromCalendarLoader.active = true onRightContentClicked: { - if ( root.fromDate ) { - root.fromDate = null + if (checked) { + textField.clear() + checked = false + if ( root.currentValue[1] ){ + root.currentValue = [undefined, root.currentValue[1]] + } else { + root.currentValue = undefined + } + root.currentValueChanged() } else { - fromCalendarLoader.active = true + let currentTimestamp = new Date() + + if (root.hasTime) { + text = Qt.formatDateTime(currentTimestamp) + } else { + text = Qt.formatDate(currentTimestamp) + } + + if (!root.hasTime) { + currentTimestamp.setHours(0, 0, 0, 0) + } + if (!root.currentValue) { + root.currentValue = [currentTimestamp, undefined] + } else { + root.currentValue[0] = currentTimestamp + } + root.currentValueChanged() } } } @@ -106,10 +104,21 @@ Column { MMFormComponents.MMCalendarDrawer { hasDatePicker: true hasTimePicker: root.hasTime - dateTime: root.fromDate ? root.fromDate : new Date() + dateTime: root.currentValue && root.currentValue[0] ? root.currentValue[0] : new Date() onPrimaryButtonClicked: { - root.fromDate = dateTime + let currentTimestamp = dateTime + if (!root.hasTime) { + currentTimestamp.setHours(0, 0, 0, 0) + } + if (!root.currentValue){ + root.currentValue = [currentTimestamp, undefined] + } else { + root.currentValue[0] = currentTimestamp + root.currentValueChanged() + } + + fromDateInput.text = root.hasTime ? Qt.formatDateTime(dateTime) : Qt.formatDate(dateTime) } onClosed: fromCalendarLoader.active = false Component.onCompleted: open() @@ -126,21 +135,44 @@ Column { width: parent.width type: MMFilterTextInput.InputType.Date - checked: root.toDate !== null && !root.rangeInvalid placeholderText: qsTr( "To" ) - errorMsg: root.rangeInvalid ? qsTr( "\"From\" must be less than \"To\"" ) : "" + errorMsg: root.rangeInvalid ? qsTr( "\"From\" must be sooner than \"To\"" ) : "" text: { - if ( !root.toDate ) return "" - if ( root.hasTime ) return Qt.formatDateTime( root.toDate, Qt.DefaultLocaleShortDate ) - return Qt.formatDate( root.toDate, Qt.DefaultLocaleShortDate ) + if ( !root.currentValue || !root.currentValue[1] ) return "" + if ( root.hasTime ) return Qt.formatDateTime( root.currentValue[1] ) + return Qt.formatDate( root.currentValue[1] ) } onTextClicked: toCalendarLoader.active = true onRightContentClicked: { - if ( root.toDate ) { - root.toDate = null + if (checked) { + textField.clear() + checked = false + if ( root.currentValue[0] ){ + root.currentValue = [root.currentValue[0], undefined] + } else { + root.currentValue = undefined + } + root.currentValueChanged() + } else { - toCalendarLoader.active = true + let currentTimestamp = new Date() + + if (root.hasTime) { + text = Qt.formatDateTime(currentTimestamp) + } else { + text = Qt.formatDate(currentTimestamp) + } + + if (!root.hasTime) { + currentTimestamp.setHours(0, 0, 0, 0) + } + if (!root.currentValue) { + root.currentValue = [undefined, currentTimestamp] + } else { + root.currentValue[1] = currentTimestamp + root.currentValueChanged() + } } } } @@ -157,10 +189,21 @@ Column { MMFormComponents.MMCalendarDrawer { hasDatePicker: true hasTimePicker: root.hasTime - dateTime: root.toDate ? root.toDate : new Date() + dateTime: root.currentValue && root.currentValue[1] ? root.currentValue[1] : new Date() onPrimaryButtonClicked: { - root.toDate = dateTime + let currentTimestamp = dateTime + if (!root.hasTime) { + currentTimestamp.setHours(0, 0, 0, 0) + } + if (!root.currentValue){ + root.currentValue = [undefined, currentTimestamp] + } else { + root.currentValue[1] = currentTimestamp + } + root.currentValueChanged() + + toDateInput.text = root.hasTime ? Qt.formatDateTime(dateTime) : Qt.formatDate(dateTime) } onClosed: toCalendarLoader.active = false Component.onCompleted: open() From 278a8abbe948ca6caaa659f4e86817ba320aa4e3 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Mon, 13 Apr 2026 15:47:37 +0200 Subject: [PATCH 3/3] Fix formatting --- app/filtercontroller.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/filtercontroller.cpp b/app/filtercontroller.cpp index 21ca2f058..04be85a88 100644 --- a/app/filtercontroller.cpp +++ b/app/filtercontroller.cpp @@ -327,14 +327,14 @@ bool FilterController::hasActiveFilterOnLayer( const QString &layerId ) return !layer->subsetString().isEmpty(); } -bool FilterController::isDateFilterDateTime(const QString& filterId) +bool FilterController::isDateFilterDateTime( const QString &filterId ) { for ( FieldFilter &filter : mFieldFilters ) { if ( filter.filterId == filterId ) { - const QgsVectorLayer *layer = qobject_cast( QgsProject::instance()->mapLayer(filter.layerId) ); - const QMetaType::Type fieldType = layer->fields().field(filter.fieldName).type(); + const QgsVectorLayer *layer = qobject_cast( QgsProject::instance()->mapLayer( filter.layerId ) ); + const QMetaType::Type fieldType = layer->fields().field( filter.fieldName ).type(); return fieldType == QMetaType::QDateTime; } }