Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[stable-3.10] Rewrite share expiration date field's date handling, fixing issues #6048

Merged
merged 26 commits into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7b8fb44
Eliminate wrapper SpinBox for expire date field
claucambra Aug 7, 2023
15c7e21
Extract date field into a separate file
claucambra Aug 7, 2023
e7343a1
Stop reducing date numbers
claucambra Aug 7, 2023
dc23615
Add starter datefieldbackend class
claucambra Aug 7, 2023
429aa89
Add dateTime property to datefieldbackend
claucambra Aug 7, 2023
f895173
Add datetime msecs property to datefieldbackend
claucambra Aug 7, 2023
e2a99c6
Add strig based representation and setting for date in datefieldbackend
claucambra Aug 7, 2023
8cbf8ef
Add minimum date properties to datefieldbackend
claucambra Aug 7, 2023
3761569
Add maximum date properties to datefieldbackend
claucambra Aug 7, 2023
df744cc
Add validDateTime property to datefieldbackend
claucambra Aug 8, 2023
183df89
Register DateFieldBackend in QML engine
claucambra Aug 8, 2023
29b568a
Vastly simplify NCInputDateField by using the DateFieldBackend
claucambra Aug 8, 2023
eb76c0b
Use NCInputDateField in ShareDetailsPage
claucambra Aug 8, 2023
48c49fc
Customise locale default date format to ensure we eliminate issues pa…
claucambra Aug 8, 2023
2093c92
Give each property unique signals, fixing QML bugs
claucambra Aug 8, 2023
c57852e
Do not use QDateTime, use only QDate for datefieldbackend
claucambra Aug 8, 2023
ff4e1a5
Add date field input method hint
claucambra Aug 8, 2023
7cfd4d8
Ensure leading zero dates are also correctly parsed
claucambra Aug 8, 2023
2b0662d
Ensure qdatetimes are UTC in datefieldbackend
claucambra Aug 8, 2023
ea44c0f
Add date field backend test file
claucambra Sep 12, 2023
8ac48c2
Add simple test for basic. default date properties of DateFieldBackend
claucambra Sep 12, 2023
a8a6474
Add test for date field backend boundaries
claucambra Sep 12, 2023
836b8c5
Check that signals were correctly emitted in testDateBoundaries
claucambra Sep 12, 2023
6a01bb7
Add a test for the various ways of setting a date on datefieldbackend
claucambra Sep 12, 2023
9201eaa
Do not present NCInputDateField backend as public property
claucambra Sep 12, 2023
dddb83b
More stringently check for min and max date validity in date field ba…
claucambra Sep 12, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions resources.qrc
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,6 @@
<file>src/gui/ResolveConflictsDialog.qml</file>
<file>src/gui/ConflictDelegate.qml</file>
<file>src/gui/ConflictItemFileInfo.qml</file>
<file>src/gui/filedetails/NCInputDateField.qml</file>
</qresource>
</RCC>
2 changes: 2 additions & 0 deletions src/gui/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ set(client_SRCS
syncconflictsmodel.cpp
fileactivitylistmodel.h
fileactivitylistmodel.cpp
filedetails/datefieldbackend.h
filedetails/datefieldbackend.cpp
filedetails/filedetails.h
filedetails/filedetails.cpp
filedetails/sharemodel.h
Expand Down
49 changes: 49 additions & 0 deletions src/gui/filedetails/NCInputDateField.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright (C) 2022 by Claudio Cambra <[email protected]>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/

import QtQuick 2.15
import QtQuick.Controls 2.15
import com.nextcloud.desktopclient 1.0

NCInputTextField {
id: root

signal userAcceptedDate

function updateText() {
text = backend.dateString;
}

DateFieldBackend {
id: backend
onDateStringChanged: if (!root.activeFocus) root.updateText()
}

property alias date: backend.date
property alias dateInMs: backend.dateMsecs
property alias minimumDate: backend.minimumDate
property alias minimumDateMs: backend.minimumDateMsecs
property alias maximumDate: backend.maximumDate
property alias maximumDateMs: backend.maximumDateMsecs

inputMethodHints: Qt.ImhDate
validInput: backend.validDate
text: backend.dateString
onTextChanged: backend.dateString = text

onAccepted: {
backend.dateString = text;
root.userAcceptedDate();
}
}
150 changes: 15 additions & 135 deletions src/gui/filedetails/ShareDetailsPage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Page {
readonly property string passwordPlaceholder: "●●●●●●●●●●"

readonly property var expireDate: shareModelData.expireDate // Don't use int as we are limited
readonly property var maximumExpireDate: shareModelData.enforcedMaximumExpireDate
readonly property var maximumExpireDate: shareModelData.enforcedMaximumExpireDate // msecs epoch

readonly property string linkShareLabel: shareModelData.linkShareLabel ?? ""

Expand Down Expand Up @@ -127,7 +127,7 @@ Page {
//
// So to ensure that the text of the spin box is correctly updated, force an update of the
// contents of the expire date text field.
expireDateSpinBox.updateText();
expireDateField.updateText();
waitingForExpireDateChange = false;
}

Expand Down Expand Up @@ -731,149 +731,29 @@ Page {
sourceSize.height: scrollContentsColumn.rowIconWidth
}

// QML dates are essentially JavaScript dates, which makes them very finicky and unreliable.
// Instead, we exclusively deal with msecs from epoch time to make things less painful when editing.
// We only use the QML Date when showing the nice string to the user.
SpinBox {
id: expireDateSpinBox

function updateText() {
expireDateSpinBoxTextField.text = textFromValue(value, locale);
}

// Work arounds the limitations of QML's 32 bit integer when handling msecs from epoch
// Instead, we handle everything as days since epoch
readonly property int dayInMSecs: 24 * 60 * 60 * 1000
readonly property int expireDateReduced: Math.floor(root.expireDate / dayInMSecs)
// Reset the model data after binding broken on user interact
onExpireDateReducedChanged: {
value = expireDateReduced;
updateText();
}

// We can't use JS's convenient Infinity or Number.MAX_VALUE as
// JS Number type is 64 bits, whereas QML's int type is only 32 bits
readonly property IntValidator intValidator: IntValidator {}
readonly property int maximumExpireDateReduced: root.expireDateEnforced ?
Math.floor(root.maximumExpireDate / dayInMSecs) :
intValidator.top
readonly property int minimumExpireDateReduced: {
const currentDate = new Date();
const minDateUTC = new Date(Date.UTC(currentDate.getFullYear(),
currentDate.getMonth(),
currentDate.getDate() + 1));
return Math.floor(minDateUTC / dayInMSecs) // Start of day at 00:00:0000 UTC
}

// Taken from Kalendar 22.08
// https://invent.kde.org/pim/kalendar/-/blob/release/22.08/src/contents/ui/KalendarUtils/dateutils.js
function parseDateString(dateString) {
function defaultParse() {
const defaultParsedDate = Date.fromLocaleDateString(Qt.locale(), dateString, Locale.NarrowFormat);
// JS always generates date in system locale, eliminate timezone difference to UTC
const msecsSinceEpoch = defaultParsedDate.getTime() - (defaultParsedDate.getTimezoneOffset() * 60 * 1000);
return new Date(msecsSinceEpoch);
}

const dateStringDelimiterMatches = dateString.match(/\D/);
if(dateStringDelimiterMatches.length === 0) {
// Let the date method figure out this weirdness
return defaultParse();
}

const dateStringDelimiter = dateStringDelimiterMatches[0];

const localisedDateFormatSplit = Qt.locale().dateFormat(Locale.NarrowFormat).split(dateStringDelimiter);
const localisedDateDayPosition = localisedDateFormatSplit.findIndex((x) => /d/gi.test(x));
const localisedDateMonthPosition = localisedDateFormatSplit.findIndex((x) => /m/gi.test(x));
const localisedDateYearPosition = localisedDateFormatSplit.findIndex((x) => /y/gi.test(x));

let splitDateString = dateString.split(dateStringDelimiter);
let userProvidedYear = splitDateString[localisedDateYearPosition]

const dateNow = new Date();
const stringifiedCurrentYear = dateNow.getFullYear().toString();

// If we have any input weirdness, or if we have a fully-written year
// (e.g. 2022 instead of 22) then use default parse
if(splitDateString.length === 0 ||
splitDateString.length > 3 ||
userProvidedYear.length >= stringifiedCurrentYear.length) {

return defaultParse();
}

let fullyWrittenYear = userProvidedYear.split("");
const digitsToAdd = stringifiedCurrentYear.length - fullyWrittenYear.length;
for(let i = 0; i < digitsToAdd; i++) {
fullyWrittenYear.splice(i, 0, stringifiedCurrentYear[i])
}
fullyWrittenYear = fullyWrittenYear.join("");

const fixedYearNum = Number(fullyWrittenYear);
const monthIndexNum = Number(splitDateString[localisedDateMonthPosition]) - 1;
const dayNum = Number(splitDateString[localisedDateDayPosition]);

console.log(dayNum, monthIndexNum, fixedYearNum);

// Modification: return date in UTC
return new Date(Date.UTC(fixedYearNum, monthIndexNum, dayNum));
}
NCInputDateField {
id: expireDateField

Layout.fillWidth: true
height: visible ? implicitHeight : 0

// We want all the internal benefits of the spinbox but don't actually want the
// buttons, so set an empty item as a dummy
up.indicator: Item {}
down.indicator: Item {}

padding: 0
background: null
contentItem: NCInputTextField {
id: expireDateSpinBoxTextField

validInput: {
const value = expireDateSpinBox.valueFromText(text);
return value >= expireDateSpinBox.from && value <= expireDateSpinBox.to;
}

text: expireDateSpinBox.textFromValue(expireDateSpinBox.value, expireDateSpinBox.locale)
readOnly: !expireDateSpinBox.editable
validator: expireDateSpinBox.validator
inputMethodHints: Qt.ImhFormattedNumbersOnly
onAccepted: {
expireDateSpinBox.value = expireDateSpinBox.valueFromText(text, expireDateSpinBox.locale);
expireDateSpinBox.valueModified();
}
}

value: expireDateReduced
from: minimumExpireDateReduced
to: maximumExpireDateReduced

textFromValue: (value, locale) => {
const dateFromValue = new Date(value * dayInMSecs);
return dateFromValue.toLocaleDateString(Qt.locale(), Locale.NarrowFormat);
}
valueFromText: (text, locale) => {
const dateFromText = parseDateString(text);
return Math.floor(dateFromText.getTime() / dayInMSecs);
dateInMs: root.expireDate
maximumDateMs: root.maximumExpireDate
minimumDateMs: {
const currentDate = new Date();
const currentYear = currentDate.getFullYear();
const currentMonth = currentDate.getMonth();
const currentMonthDay = currentDate.getDate();
// Start of day at 00:00:0000 UTC
return Date.UTC(currentYear, currentMonth, currentMonthDay + 1);
}

editable: true
inputMethodHints: Qt.ImhDate | Qt.ImhFormattedNumbersOnly

enabled: root.expireDateEnabled &&
!root.waitingForExpireDateChange &&
!root.waitingForExpireDateEnabledChange

onValueModified: {
if (!enabled || !activeFocus) {
return;
}

root.setExpireDate(value * dayInMSecs);
onUserAcceptedDate: {
root.setExpireDate(dateInMs);
root.waitingForExpireDateChange = true;
}

Expand Down
Loading
Loading