From: Anna Koskinen Date: Fri, 22 Jan 2021 11:08:02 +0000 (+0200) Subject: DateField value should actively adjust to the set resolution. (#12183) X-Git-Tag: 7.7.23 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=b4f011230fd5c9d56a0dd7ad7c00c584e25ee990;p=vaadin-framework.git DateField value should actively adjust to the set resolution. (#12183) --- diff --git a/server/src/main/java/com/vaadin/ui/AbstractDateField.java b/server/src/main/java/com/vaadin/ui/AbstractDateField.java index 84a26436c0..dbbc19b673 100644 --- a/server/src/main/java/com/vaadin/ui/AbstractDateField.java +++ b/server/src/main/java/com/vaadin/ui/AbstractDateField.java @@ -38,9 +38,9 @@ import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; -import com.googlecode.gentyref.GenericTypeReflector; import org.jsoup.nodes.Element; +import com.googlecode.gentyref.GenericTypeReflector; import com.vaadin.data.Result; import com.vaadin.data.ValidationResult; import com.vaadin.data.Validator; @@ -179,7 +179,7 @@ public abstract class AbstractDateField * Note: Negative, i.e. BC dates are not supported. *

- * Note: It's usually recommended to use only one of the following at the same - * time: Range validator with Binder or DateField's setRangeStart check. + * Note: It's usually recommended to use only one of the following at the + * same time: Range validator with Binder or DateField's setRangeStart + * check. * * @param startDate * - the allowed range's start date @@ -377,8 +378,11 @@ public abstract class AbstractDateField - * Note: It's usually recommended to use only one of the following at the same - * time: Range validator with Binder or DateField's setRangeEnd check. + * Note: It's usually recommended to use only one of the following at the + * same time: Range validator with Binder or DateField's setRangeEnd check. * * @param endDate * the allowed range's end date (inclusive, based on the current @@ -545,8 +549,8 @@ public abstract class AbstractDateField validator = getRangeValidator(); - ValidationResult result = validator.apply(value, + ValidationResult result = validator.apply(adjusted, new ValueContext(this, this)); if (result.isError()) { @@ -718,25 +723,40 @@ public abstract class AbstractDateField getRangeValidator() { return new DateRangeValidator(getDateOutOfRangeMessage(), - getDate(getRangeStart(), getResolution()), - getDate(getRangeEnd(), getResolution())); + adjustToResolution(getRangeStart(), getResolution()), + adjustToResolution(getRangeEnd(), getResolution())); } @Override @@ -134,7 +134,9 @@ public abstract class AbstractLocalDateField return Date.from(date.atStartOfDay(ZoneOffset.UTC).toInstant()); } - private LocalDate getDate(LocalDate date, DateResolution forResolution) { + @Override + protected LocalDate adjustToResolution(LocalDate date, + DateResolution forResolution) { if (date == null) { return null; } @@ -171,19 +173,21 @@ public abstract class AbstractLocalDateField protected Result handleUnparsableDateString(String dateString) { // Handle possible week number, which cannot be parsed client side due // limitations in GWT - if (this.getDateFormat() != null && this.getDateFormat().contains("w")) { + if (getDateFormat() != null && getDateFormat().contains("w")) { Date parsedDate; - SimpleDateFormat df = new SimpleDateFormat(this.getDateFormat(),this.getLocale()); + SimpleDateFormat df = new SimpleDateFormat(getDateFormat(), + getLocale()); try { parsedDate = df.parse(dateString); } catch (ParseException e) { return super.handleUnparsableDateString(dateString); } - ZoneId zi = this.getZoneId(); - if (zi == null) { + ZoneId zi = getZoneId(); + if (zi == null) { zi = ZoneId.systemDefault(); } - LocalDate date = Instant.ofEpochMilli(parsedDate.getTime()).atZone(zi).toLocalDate(); + LocalDate date = Instant.ofEpochMilli(parsedDate.getTime()) + .atZone(zi).toLocalDate(); return Result.ok(date); } else { return super.handleUnparsableDateString(dateString); diff --git a/server/src/main/java/com/vaadin/ui/AbstractLocalDateTimeField.java b/server/src/main/java/com/vaadin/ui/AbstractLocalDateTimeField.java index 6f7037d732..39f16735a9 100644 --- a/server/src/main/java/com/vaadin/ui/AbstractLocalDateTimeField.java +++ b/server/src/main/java/com/vaadin/ui/AbstractLocalDateTimeField.java @@ -111,8 +111,8 @@ public abstract class AbstractLocalDateTimeField @Override protected RangeValidator getRangeValidator() { return new DateTimeRangeValidator(getDateOutOfRangeMessage(), - getDate(getRangeStart(), getResolution()), - getDate(getRangeEnd(), getResolution())); + adjustToResolution(getRangeStart(), getResolution()), + adjustToResolution(getRangeEnd(), getResolution())); } @Override @@ -143,7 +143,8 @@ public abstract class AbstractLocalDateTimeField return Date.from(date.toInstant(ZoneOffset.UTC)); } - private LocalDateTime getDate(LocalDateTime date, + @Override + protected LocalDateTime adjustToResolution(LocalDateTime date, DateTimeResolution forResolution) { if (date == null) { return null; diff --git a/server/src/test/java/com/vaadin/tests/data/converter/LocalDateTimeToDateConverterTest.java b/server/src/test/java/com/vaadin/tests/data/converter/LocalDateTimeToDateConverterTest.java index 6851761cfc..a9dca56c15 100644 --- a/server/src/test/java/com/vaadin/tests/data/converter/LocalDateTimeToDateConverterTest.java +++ b/server/src/test/java/com/vaadin/tests/data/converter/LocalDateTimeToDateConverterTest.java @@ -14,6 +14,7 @@ import com.vaadin.data.Binder; import com.vaadin.data.ValidationException; import com.vaadin.data.ValueContext; import com.vaadin.data.converter.LocalDateTimeToDateConverter; +import com.vaadin.shared.ui.datefield.DateTimeResolution; import com.vaadin.ui.DateTimeField; public class LocalDateTimeToDateConverterTest extends AbstractConverterTest { @@ -59,7 +60,15 @@ public class LocalDateTimeToDateConverterTest extends AbstractConverterTest { BeanWithDate bean = new BeanWithDate(); binder.writeBean(bean); - assertEquals(DATE, bean.getDate()); + assertEquals(DateTimeResolution.MINUTE, dateField.getResolution()); + + // create a comparison date that matches the resolution + Calendar calendar = Calendar + .getInstance(TimeZone.getTimeZone(ZoneOffset.UTC)); + calendar.clear(); + calendar.set(2017, Calendar.JANUARY, 1, 1, 1, 0); + Date date = calendar.getTime(); + assertEquals(date, bean.getDate()); } public static class BeanWithDate { diff --git a/server/src/test/java/com/vaadin/tests/server/component/abstractdatefield/AbstractLocalDateTimeFieldDeclarativeTest.java b/server/src/test/java/com/vaadin/tests/server/component/abstractdatefield/AbstractLocalDateTimeFieldDeclarativeTest.java index d4d2c397b7..de6dc322ad 100644 --- a/server/src/test/java/com/vaadin/tests/server/component/abstractdatefield/AbstractLocalDateTimeFieldDeclarativeTest.java +++ b/server/src/test/java/com/vaadin/tests/server/component/abstractdatefield/AbstractLocalDateTimeFieldDeclarativeTest.java @@ -25,15 +25,19 @@ import com.vaadin.ui.AbstractLocalDateTimeField; public abstract class AbstractLocalDateTimeFieldDeclarativeTest extends AbstractFieldDeclarativeTest { - protected DateTimeFormatter DATE_FORMATTER = DateTimeFormatter - .ofPattern("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH); + // field initialised with DateTimeResolution.MINUTE, seconds get truncated + protected DateTimeFormatter VALUE_DATE_FORMATTER = DateTimeFormatter + .ofPattern("yyyy-MM-dd HH:mm:00", Locale.ROOT); + // only field value conforms to resolution, range keeps the initial values + protected DateTimeFormatter RANGE_DATE_FORMATTER = DateTimeFormatter + .ofPattern("yyyy-MM-dd HH:mm:ss", Locale.ROOT); @Override public void valueDeserialization() throws InstantiationException, IllegalAccessException { LocalDateTime value = LocalDateTime.of(2003, 02, 27, 10, 37, 43); String design = String.format("<%s value='%s'/>", getComponentTag(), - DATE_FORMATTER.format(value)); + VALUE_DATE_FORMATTER.format(value)); T component = getComponentClass().newInstance(); component.setValue(value); @@ -57,8 +61,8 @@ public abstract class AbstractLocalDateTimeFieldDeclarativeTest", - getComponentTag(), DATE_FORMATTER.format(end), - DATE_FORMATTER.format(start), dateOutOfRange, + getComponentTag(), RANGE_DATE_FORMATTER.format(end), + RANGE_DATE_FORMATTER.format(start), dateOutOfRange, resolution.name().toLowerCase(Locale.ROOT), dateFormat, parseErrorMsg); @@ -82,7 +86,7 @@ public abstract class AbstractLocalDateTimeFieldDeclarativeTest", - getComponentTag(), DATE_FORMATTER.format(value)); + getComponentTag(), VALUE_DATE_FORMATTER.format(value)); T component = getComponentClass().newInstance(); component.setValue(value); diff --git a/server/src/test/java/com/vaadin/tests/server/component/datefield/DateFieldListenersTest.java b/server/src/test/java/com/vaadin/tests/server/component/datefield/DateFieldListenersTest.java index 91d7384421..da7306e025 100644 --- a/server/src/test/java/com/vaadin/tests/server/component/datefield/DateFieldListenersTest.java +++ b/server/src/test/java/com/vaadin/tests/server/component/datefield/DateFieldListenersTest.java @@ -1,12 +1,14 @@ package com.vaadin.tests.server.component.datefield; import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAccessor; import java.util.Date; import java.util.Map; import org.junit.Test; +import com.vaadin.data.validator.DateTimeRangeValidator; import com.vaadin.data.validator.RangeValidator; import com.vaadin.event.FieldEvents.BlurEvent; import com.vaadin.event.FieldEvents.BlurListener; @@ -39,7 +41,9 @@ public class DateFieldListenersTest extends AbstractListenerMethodsTestBase { @Override protected RangeValidator getRangeValidator() { - return null; + return new DateTimeRangeValidator(getDateOutOfRangeMessage(), + adjustToResolution(getRangeStart(), getResolution()), + adjustToResolution(getRangeEnd(), getResolution())); } @Override @@ -61,6 +65,32 @@ public class DateFieldListenersTest extends AbstractListenerMethodsTestBase { protected LocalDateTime toType(TemporalAccessor temporalAccessor) { return LocalDateTime.from(temporalAccessor); } + + @Override + protected LocalDateTime adjustToResolution(LocalDateTime date, + DateTimeResolution forResolution) { + if (date == null) { + return null; + } + switch (forResolution) { + case YEAR: + return date.withDayOfYear(1).toLocalDate().atStartOfDay(); + case MONTH: + return date.withDayOfMonth(1).toLocalDate().atStartOfDay(); + case DAY: + return date.toLocalDate().atStartOfDay(); + case HOUR: + return date.truncatedTo(ChronoUnit.HOURS); + case MINUTE: + return date.truncatedTo(ChronoUnit.MINUTES); + case SECOND: + return date.truncatedTo(ChronoUnit.SECONDS); + default: + assert false : "Unexpected resolution argument " + + forResolution; + return null; + } + } } @Test diff --git a/uitest/src/main/java/com/vaadin/tests/components/datefield/DateFieldResolutionChange.java b/uitest/src/main/java/com/vaadin/tests/components/datefield/DateFieldResolutionChange.java new file mode 100644 index 0000000000..a3635ea9a3 --- /dev/null +++ b/uitest/src/main/java/com/vaadin/tests/components/datefield/DateFieldResolutionChange.java @@ -0,0 +1,114 @@ +package com.vaadin.tests.components.datefield; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +import com.vaadin.data.Binder; +import com.vaadin.server.VaadinRequest; +import com.vaadin.shared.ui.datefield.DateResolution; +import com.vaadin.tests.components.AbstractTestUIWithLog; +import com.vaadin.ui.Button; +import com.vaadin.ui.DateField; +import com.vaadin.ui.HorizontalLayout; + +public class DateFieldResolutionChange extends AbstractTestUIWithLog { + + protected DateTimeFormatter DATE_FORMATTER = DateTimeFormatter + .ofPattern("yyyy-MM-dd", Locale.ROOT); + + @Override + protected void setup(VaadinRequest request) { + Binder binder = new Binder<>(Pojo.class); + + HorizontalLayout horizontalLayout = new HorizontalLayout(); + + final DateField monthField = new DateField() { + @Override + public void setValue(LocalDate value) { + if (value != null) { + log("MonthField set value " + DATE_FORMATTER.format(value)); + } + super.setValue(value); + } + }; + monthField.setResolution(DateResolution.MONTH); + monthField.setId("MonthField"); + monthField.addValueChangeListener( + event -> log("MonthField value change event: " + + DATE_FORMATTER.format(event.getValue()))); + binder.bind(monthField, "value1"); + + final DateField dayField = new DateField() { + @Override + public void setValue(LocalDate value) { + if (value != null) { + log("DayField set value " + DATE_FORMATTER.format(value)); + } + super.setValue(value); + } + }; + dayField.setResolution(DateResolution.DAY); + dayField.setId("DayField"); + dayField.addValueChangeListener( + event -> log("DayField value change event: " + + DATE_FORMATTER.format(event.getValue()))); + binder.bind(dayField, "value2"); + + Pojo pojo = new Pojo(); + binder.setBean(pojo); + + Button monthButton = new Button("month", e -> { + monthField.setResolution(DateResolution.MONTH); + dayField.setResolution(DateResolution.MONTH); + }); + + Button dayButton = new Button("day", e -> { + monthField.setResolution(DateResolution.DAY); + dayField.setResolution(DateResolution.DAY); + }); + + Button logButton = new Button("log", e -> { + log("MonthField current value: " + + DATE_FORMATTER.format(pojo.getValue1())); + log("DayField current value: " + + DATE_FORMATTER.format(pojo.getValue2())); + }); + + Button setButton = new Button("set", e -> { + LocalDate newDate = LocalDate.of(2021, 2, 14); + pojo.setValue1(newDate); + pojo.setValue2(newDate); + binder.setBean(pojo); + }); + + horizontalLayout.addComponents(monthField, dayField, monthButton, + dayButton, logButton, setButton); + addComponent(horizontalLayout); + } + + public class Pojo { + private LocalDate value1, value2 = null; + + public LocalDate getValue1() { + return value1; + } + + public void setValue1(LocalDate value1) { + this.value1 = value1; + } + + public LocalDate getValue2() { + return value2; + } + + public void setValue2(LocalDate value2) { + this.value2 = value2; + } + } + + @Override + protected String getTestDescription() { + return "Date field value should immediately update to match resolution."; + } +} diff --git a/uitest/src/main/java/com/vaadin/tests/components/datefield/DateTimeFieldResolutionChange.java b/uitest/src/main/java/com/vaadin/tests/components/datefield/DateTimeFieldResolutionChange.java new file mode 100644 index 0000000000..c8066af75b --- /dev/null +++ b/uitest/src/main/java/com/vaadin/tests/components/datefield/DateTimeFieldResolutionChange.java @@ -0,0 +1,114 @@ +package com.vaadin.tests.components.datefield; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +import com.vaadin.data.Binder; +import com.vaadin.server.VaadinRequest; +import com.vaadin.shared.ui.datefield.DateTimeResolution; +import com.vaadin.tests.components.AbstractTestUIWithLog; +import com.vaadin.ui.Button; +import com.vaadin.ui.DateTimeField; +import com.vaadin.ui.HorizontalLayout; + +public class DateTimeFieldResolutionChange extends AbstractTestUIWithLog { + + protected DateTimeFormatter DATE_FORMATTER = DateTimeFormatter + .ofPattern("yyyy-MM-dd HH:mm:ss", Locale.ROOT); + + @Override + protected void setup(VaadinRequest request) { + Binder binder = new Binder<>(Pojo.class); + + HorizontalLayout horizontalLayout = new HorizontalLayout(); + + final DateTimeField monthField = new DateTimeField() { + @Override + public void setValue(LocalDateTime value) { + if (value != null) { + log("MonthField set value " + DATE_FORMATTER.format(value)); + } + super.setValue(value); + } + }; + monthField.setResolution(DateTimeResolution.MONTH); + monthField.setId("MonthField"); + monthField.addValueChangeListener( + event -> log("MonthField value change event: " + + DATE_FORMATTER.format(event.getValue()))); + binder.bind(monthField, "value1"); + + final DateTimeField dayField = new DateTimeField() { + @Override + public void setValue(LocalDateTime value) { + if (value != null) { + log("DayField set value " + DATE_FORMATTER.format(value)); + } + super.setValue(value); + } + }; + dayField.setResolution(DateTimeResolution.DAY); + dayField.setId("DayField"); + dayField.addValueChangeListener( + event -> log("DayField value change event: " + + DATE_FORMATTER.format(event.getValue()))); + binder.bind(dayField, "value2"); + + Pojo pojo = new Pojo(); + binder.setBean(pojo); + + Button monthButton = new Button("month", e -> { + monthField.setResolution(DateTimeResolution.MONTH); + dayField.setResolution(DateTimeResolution.MONTH); + }); + + Button dayButton = new Button("day", e -> { + monthField.setResolution(DateTimeResolution.DAY); + dayField.setResolution(DateTimeResolution.DAY); + }); + + Button logButton = new Button("log", e -> { + log("MonthField current value: " + + DATE_FORMATTER.format(pojo.getValue1())); + log("DayField current value: " + + DATE_FORMATTER.format(pojo.getValue2())); + }); + + Button setButton = new Button("set", e -> { + LocalDateTime newDate = LocalDateTime.of(2021, 2, 14, 16, 17); + pojo.setValue1(newDate); + pojo.setValue2(newDate); + binder.setBean(pojo); + }); + + horizontalLayout.addComponents(monthField, dayField, monthButton, + dayButton, logButton, setButton); + addComponent(horizontalLayout); + } + + public class Pojo { + private LocalDateTime value1, value2 = null; + + public LocalDateTime getValue1() { + return value1; + } + + public void setValue1(LocalDateTime value1) { + this.value1 = value1; + } + + public LocalDateTime getValue2() { + return value2; + } + + public void setValue2(LocalDateTime value2) { + this.value2 = value2; + } + } + + @Override + protected String getTestDescription() { + return "Date field value should immediately update to match resolution."; + } +} diff --git a/uitest/src/test/java/com/vaadin/tests/components/datefield/DateFieldResolutionChangeTest.java b/uitest/src/test/java/com/vaadin/tests/components/datefield/DateFieldResolutionChangeTest.java new file mode 100644 index 0000000000..c2a6b8ad12 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/components/datefield/DateFieldResolutionChangeTest.java @@ -0,0 +1,50 @@ +package com.vaadin.tests.components.datefield; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import com.vaadin.testbench.elements.ButtonElement; +import com.vaadin.tests.tb3.MultiBrowserTest; + +public class DateFieldResolutionChangeTest extends MultiBrowserTest { + + @Test + public void testValueAndResolutionChange() { + openTestURL(); + + // set a fixed date + $(ButtonElement.class).caption("set").first().click(); + + // both fields should trigger a value change event but for MonthField + // the day value should be truncated to default + assertEquals("Unexpected log row", "1. MonthField set value 2021-02-14", + getLogRow(3)); + assertEquals("Unexpected log row", + "2. MonthField value change event: 2021-02-01", getLogRow(2)); + assertEquals("Unexpected log row", "3. DayField set value 2021-02-14", + getLogRow(1)); + assertEquals("Unexpected log row", + "4. DayField value change event: 2021-02-14", getLogRow(0)); + + // change both to day resolution + $(ButtonElement.class).caption("day").first().click(); + + // DayField shouldn't react, MonthField should check that the value + // matches resolution but not trigger a ValueChangeEvent + assertEquals("Unexpected log row", "5. MonthField set value 2021-02-01", + getLogRow(0)); + + // change both to month resolution + $(ButtonElement.class).caption("month").first().click(); + + // both fields should check that the value matches resolution but only + // DayField should trigger a ValueChangeEvent + assertEquals("Unexpected log row", "6. MonthField set value 2021-02-01", + getLogRow(2)); + assertEquals("Unexpected log row", "7. DayField set value 2021-02-01", + getLogRow(1)); + assertEquals("Unexpected log row", + "8. DayField value change event: 2021-02-01", getLogRow(0)); + } +} diff --git a/uitest/src/test/java/com/vaadin/tests/components/datefield/DateTimeFieldResolutionChangeTest.java b/uitest/src/test/java/com/vaadin/tests/components/datefield/DateTimeFieldResolutionChangeTest.java new file mode 100644 index 0000000000..4fdd0a5ae9 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/components/datefield/DateTimeFieldResolutionChangeTest.java @@ -0,0 +1,53 @@ +package com.vaadin.tests.components.datefield; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import com.vaadin.testbench.elements.ButtonElement; +import com.vaadin.tests.tb3.MultiBrowserTest; + +public class DateTimeFieldResolutionChangeTest extends MultiBrowserTest { + + @Test + public void testValueAndResolutionChange() { + openTestURL(); + + // set a fixed date + $(ButtonElement.class).caption("set").first().click(); + + // both fields should trigger a value change event but the value should + // be truncated according to each field's resolution + assertEquals("Unexpected log row", + "1. MonthField set value 2021-02-14 16:17:00", getLogRow(3)); + assertEquals("Unexpected log row", + "2. MonthField value change event: 2021-02-01 00:00:00", + getLogRow(2)); + assertEquals("Unexpected log row", + "3. DayField set value 2021-02-14 16:17:00", getLogRow(1)); + assertEquals("Unexpected log row", + "4. DayField value change event: 2021-02-14 00:00:00", + getLogRow(0)); + + // change both to day resolution + $(ButtonElement.class).caption("day").first().click(); + + // DayField shouldn't react, MonthField should check that the value + // matches resolution but not trigger a ValueChangeEvent + assertEquals("Unexpected log row", + "5. MonthField set value 2021-02-01 00:00:00", getLogRow(0)); + + // change both to month resolution + $(ButtonElement.class).caption("month").first().click(); + + // both fields should check that the value matches resolution but only + // DayField should trigger a ValueChangeEvent + assertEquals("Unexpected log row", + "6. MonthField set value 2021-02-01 00:00:00", getLogRow(2)); + assertEquals("Unexpected log row", + "7. DayField set value 2021-02-01 00:00:00", getLogRow(1)); + assertEquals("Unexpected log row", + "8. DayField value change event: 2021-02-01 00:00:00", + getLogRow(0)); + } +}