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

Rewrite share expiration date field's date handling, fixing issues #5961

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
265396e
Eliminate wrapper SpinBox for expire date field
claucambra Aug 7, 2023
27e560d
Extract date field into a separate file
claucambra Aug 7, 2023
ea93ca0
Stop reducing date numbers
claucambra Aug 7, 2023
d6b2829
Add starter datefieldbackend class
claucambra Aug 7, 2023
3717558
Add dateTime property to datefieldbackend
claucambra Aug 7, 2023
eb66f51
Add datetime msecs property to datefieldbackend
claucambra Aug 7, 2023
d4f8b4f
Add strig based representation and setting for date in datefieldbackend
claucambra Aug 7, 2023
858aa9f
Add minimum date properties to datefieldbackend
claucambra Aug 7, 2023
7e26bbd
Add maximum date properties to datefieldbackend
claucambra Aug 7, 2023
1f8ecf4
Add validDateTime property to datefieldbackend
claucambra Aug 8, 2023
460d3dc
Register DateFieldBackend in QML engine
claucambra Aug 8, 2023
56b87c7
Vastly simplify NCInputDateField by using the DateFieldBackend
claucambra Aug 8, 2023
d2900ba
Use NCInputDateField in ShareDetailsPage
claucambra Aug 8, 2023
7ad97cf
Customise locale default date format to ensure we eliminate issues pa…
claucambra Aug 8, 2023
ce059ed
Give each property unique signals, fixing QML bugs
claucambra Aug 8, 2023
bbf920d
Do not use QDateTime, use only QDate for datefieldbackend
claucambra Aug 8, 2023
19f8d80
Add date field input method hint
claucambra Aug 8, 2023
32cea09
Ensure leading zero dates are also correctly parsed
claucambra Aug 8, 2023
bbbab01
Ensure qdatetimes are UTC in datefieldbackend
claucambra Aug 8, 2023
797663e
Add date field backend test file
claucambra Sep 12, 2023
a4767a1
Add simple test for basic. default date properties of DateFieldBackend
claucambra Sep 12, 2023
ebc3522
Add test for date field backend boundaries
claucambra Sep 12, 2023
e254dfc
Check that signals were correctly emitted in testDateBoundaries
claucambra Sep 12, 2023
b642c2e
Add a test for the various ways of setting a date on datefieldbackend
claucambra Sep 12, 2023
97e7ec3
Do not present NCInputDateField backend as public property
claucambra Sep 12, 2023
774918c
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