Skip to content

[PER-10643] Add edtf date and time validation - #1056

Open
aasandei-vsp wants to merge 10 commits into
mainfrom
PER-10643-edtf-validation
Open

[PER-10643] Add edtf date and time validation#1056
aasandei-vsp wants to merge 10 commits into
mainfrom
PER-10643-edtf-validation

Conversation

@aasandei-vsp

@aasandei-vsp aasandei-vsp commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Manual test cases — EDTF date/time input

Setup: log in, upload a file, click it.
EXPECTED: sidebar with a Date section. Unless a test says "inline picker", open the edit modal via More options for each test. The modal footer shows a live preview of the resulting EDTF string when valid, or a red error message when invalid, and the Save button is disabled while invalid.

Warning

The date and time inputs are the same shared component in both the inline sidebar picker and the More options modal. Every test that only exercises those fields — Basic date entry, Single-digit month/day, Invalid characters, Out-of-range values, Impossible calendar days, and Time — must be run in both places, because their commit behavior differs: the modal disables Save while invalid, whereas the inline picker lets you attempt the save and surfaces the error as a toast. Qualifiers, Unknown, and Date ranges exist only in the modal.

Note

Field-specific messages (invalid characters, out-of-range month/day/hours/minutes/seconds, day-for-month) appear inline below the offending input, with that input highlighted red. The modal footer separately shows the generic The date entered is not valid. Please check the values and try again. whenever anything is invalid (with Save disabled) — the specific message is not duplicated there. Two exceptions with no inline field surface keep their specific text in the footer: A complete date is required when time is provided. and The date range is not valid. Please make sure the start date is before the end date.


Basic date entry

(run in both the inline picker and the modal)

Year only

  1. Type 2026 in the year, leave month and day empty.
    • EXPECTED: No error. Preview shows 2026. Save is enabled.

Year and month

  1. Type 2026 in the year, 05 in the month, leave day empty.
    • EXPECTED: No error. Preview shows 2026-05. Save is enabled.

Full date

  1. Type 2026, 05, 20.
    • EXPECTED: No error. Preview shows 2026-05-20. Save is enabled.

Auto-advance between segments

  1. Type 2026 in the year.
    • EXPECTED: Focus jumps to the month input once 4 digits are entered.
  2. Type 05 in the month.
    • EXPECTED: Focus jumps to the day input once 2 digits are entered.

Backspace navigation between segments

  1. Enter 2026 / 05 / 20, then place the cursor in the empty day input and press Backspace until day is empty, then Backspace again.
    • EXPECTED: Focus moves back to the month input and removes its last digit.
  2. Continue pressing Backspace with the month empty.
    • EXPECTED: Focus moves back to the year input and removes its last digit.

Calendar picker

  1. Click the calendar icon; select a date.
    • EXPECTED: The picker fills year/month/day, clears any field errors, and closes. Preview reflects the selected date.
  2. Focus the calendar icon and press Enter or Space.
    • EXPECTED: The calendar opens/closes (keyboard toggle works).

Single-digit month/day (entered without zero-padding)

(run in both the inline picker and the modal)

Single-digit month is accepted and zero-padded

  1. Enter year 2026, month 2 (single digit), day 15.
    • EXPECTED: No error. Preview shows 2026-02-15 — the month is zero-padded automatically even though you typed 2. Save is enabled.
  2. Save, then reopen the item.
    • EXPECTED: The date persists and the month field now shows 02.

Single-digit day is accepted and zero-padded

  1. Enter year 2026, month 05, day 5 (single digit).
    • EXPECTED: No error. Preview shows 2026-05-05. Save is enabled.

Both month and day single-digit

  1. Enter year 2026, month 3, day 7.
    • EXPECTED: No error. Preview shows 2026-03-07.

Single-digit month, year-month only (no day)

  1. Enter year 2026, month 4, leave day empty.
    • EXPECTED: No error. Preview shows 2026-04.

No premature error while a single digit is typed

  1. Type 2 in the month input and stop.
    • EXPECTED: No error appears — validation waits until the value is complete. Focus does not auto-advance to the day (auto-advance only fires after 2 digits are entered), so you must move to the day input yourself.

Single-digit month is used for day validation

  1. Enter year 2026, month 2 (single digit), day 30.
    • EXPECTED: That day does not exist in the selected month and year. below the day input — the day is validated against February, not January. (This is the regression that was fixed: an un-padded month must not be treated as 01.)
  2. Enter year 2021, month 2, day 29.
    • EXPECTED: That day does not exist in the selected month and year.

Single-digit 1 month is treated as January

  1. Enter year 2026, month 1 (single digit), day 31.
    • EXPECTED: No error (January has 31 days). Preview shows 2026-01-31.

Lone 0 month is rejected (inline, on the month field)

  1. Enter year 2026, month 0 (just a zero), day 15.
    • EXPECTED: Month must be between 1 and 12. appears below the month input and the month field is highlighted red. A lone 0 is treated as an unfinished value, not a valid month. In the modal the footer shows the generic error and Save is disabled; in the inline picker a save attempt surfaces the error.

Lone 0 day is rejected (inline, on the day field)

  1. Enter year 2026, month 05, day 0 (just a zero).
    • EXPECTED: Day must be between 1 and 31. appears below the day input and the day field is highlighted red (same behavior as above).

Lone 0 month is attributed to the month field, not the day

  1. Enter year 2024, month 0, day 31.
    • EXPECTED: Only Month must be between 1 and 12. appears (on the month field). The day field does not show That day does not exist… — an invalid month is not mislabeled as a day error.

Invalid characters

(run in both the inline picker and the modal)

Invalid characters in date and time

  1. Type 19ab in the year, then ab in the hours.
    • EXPECTED: The values stay visible (not silently reverted), with The date contains invalid characters. / The time contains invalid characters. below the inputs and red styling.
  2. Clear the inputs.
    • EXPECTED: Errors disappear.

Out-of-range values

(run in both the inline picker and the modal)

Out-of-range month

  1. Type 13 in the month input.
    • EXPECTED: The value stays visible and Month must be between 1 and 12. appears below the input.
  2. Change the month to 12.
    • EXPECTED: The error disappears.

Out-of-range day

  1. Enter year 2026, month 01, then type 32 in the day input.
    • EXPECTED: Day must be between 1 and 31. appears below the input.
  2. Change the day to 31.
    • EXPECTED: The error disappears.

Out-of-range hours (AM/PM mode)

  1. With the format toggle on AM or PM, type 13 in the hours input.
    • EXPECTED: The value stays visible and Hour must be between 1 and 12. appears below the time input.
  2. Change the hours to 10.
    • EXPECTED: The error disappears.
  3. Type 13 again (error reappears), then toggle the format until it shows 24H (leave the hours as 13).
    • EXPECTED: The error disappears — 13 is valid in 24-hour mode — without editing the hours value. Toggling back to AM/PM makes Hour must be between 1 and 12. reappear.

Out-of-range hours (24H mode)

  1. Toggle the format until it shows 24H, type 24 in the hours input.
    • EXPECTED: Hour must be between 0 and 23. appears below the time input.
  2. Change the hours to 23.
    • EXPECTED: The error disappears.
  3. Type 24 again (error reappears), then toggle the format to AM (leave the hours as 24).
    • EXPECTED: The message changes to Hour must be between 1 and 12.24 is invalid in both modes, but the message reflects the active format's range. (A 12-hour clock's valid hours are 112; midnight is 12 AM and noon is 12 PM, so there is no 0 and the range is intentionally 1–12, not 0–12.)

Out-of-range minutes

  1. Type 60 in the minutes input.
    • EXPECTED: Minutes must be between 0 and 59. appears below the time input.
  2. Change the minutes to 59.
    • EXPECTED: The error disappears.

Out-of-range seconds

  1. Type 60 in the seconds input.
    • EXPECTED: Seconds must be between 0 and 59. appears below the time input.
  2. Change the seconds to 59.
    • EXPECTED: The error disappears.

Impossible calendar days

(run in both the inline picker and the modal)

Feb 29 in a non-leap year

  1. Enter year 2021, month 02, then type 29 in the day input.
    • EXPECTED: That day does not exist in the selected month and year. appears below the day input (the value is not silently autocorrected to 2021-03-01). Save is disabled.
  2. Change the year to 2024.
    • EXPECTED: The error disappears (2024 is a leap year). Preview shows 2024-02-29.

Feb 29 with a single-digit month

  1. Enter year 2021, month 2 (single digit), then type 29 in the day input.
    • EXPECTED: Same as above — That day does not exist in the selected month and year. (this previously slipped through with a single-digit month).

Re-validation when month changes

  1. Enter year 2026, month 01, day 31 (valid).
    • EXPECTED: No error.
  2. Change the month to 04.
    • EXPECTED: That day does not exist in the selected month and year. appears (the day is re-checked against the new month).

Time

(run in both the inline picker and the modal)

Format toggle (AM / PM / 24H)

  1. Enter a time, then click the format toggle repeatedly.
    • EXPECTED: It cycles AMPM24HAM. The hour is re-validated against the new format (e.g. 13 is invalid in AM/PM but valid in 24H).

Midnight and noon

  1. Enter time 12:00:00 with AM.
    • EXPECTED: Preview stores midnight (T00:00:00…).
  2. Enter time 12:00:00 with PM.
    • EXPECTED: Preview stores noon (T12:00:00…).

Seconds optional

  1. Enter hours and minutes only, leave seconds empty.
    • EXPECTED: No error; seconds default to 00 in the saved value.

Time requires a complete date

  1. Enter year 2026 only (no month/day), then enter a time.
    • EXPECTED: A complete date is required when time is provided. in the footer and Save is disabled.
  2. Fill in month and day.
    • EXPECTED: The error disappears.

Unspecified digits (X)

(modal only)

Unknown month with a known day

  1. Enter year 2026, leave month empty, enter day 11.
    • EXPECTED: No error. Preview shows 2026-XX-11. Save enabled.

Partial year

  1. Enter 198 in the year (3 digits), leave month/day empty.
    • EXPECTED: Preview shows 198X.
  2. Enter 19 in the year (2 digits).
    • EXPECTED: Preview shows 19XX.

Unknown year with a known month

  1. Leave year empty, enter month 05.
    • EXPECTED: Preview shows XXXX-05.

Qualifiers

(modal only)

Approximate

  1. Enter a full date 2026/05/20, toggle Approximate on.
    • EXPECTED: Preview shows 2026-05-20~. Save enabled.

Uncertain

  1. Enter a full date, toggle Uncertain on.
    • EXPECTED: Preview shows 2026-05-20?.

Approximate + Uncertain (combined)

  1. Enter a full date, toggle both Approximate and Uncertain on.
    • EXPECTED: Preview shows 2026-05-20%.

Qualifier with unspecified digits

  1. Enter year 2026, leave month empty, enter day 11, toggle Approximate on.
    • EXPECTED: No "not valid" error. Preview shows 2026-XX-11~. Save enabled.
  2. Save, then reopen the item.
    • EXPECTED: Month is blank, day is 11, and Approximate is still on (the value round-trips).

Qualifier combined with a time is rejected

  1. Enter a full date 2026/05/20, enter a time (e.g. 10:30), then toggle Approximate (or Uncertain) on.
    • EXPECTED: The footer shows The date entered is not valid. Please check the values and try again. and Save is disabled (a qualifier with a time is unsupported).

Unknown

  1. Toggle Unknown on.
    • EXPECTED: The date/time fields become disabled/cleared. Preview shows XXXX-XX-XX. Save enabled.
  2. Toggle Unknown off.
    • EXPECTED: The previous field values are restored.

Unknown is mutually exclusive with Approximate/Uncertain

  1. Toggle Unknown on.
    • EXPECTED: Approximate/Uncertain are disabled.

Date ranges

(modal only)

Basic range

  1. Toggle Use a date range on. Enter start 2026/05 and end 2027/06.
    • EXPECTED: Preview shows 2026-05/2027-06. Save enabled.

Range with time on both sides

  1. Toggle Use a date range on. Enter start 2026/05/20 with time 10:30:00, and end 2027/06/15 with time 14:00:00.
    • EXPECTED: No error. Preview shows the full interval with both times and their timezone offsets, e.g. 2026-05-20T10:30:00+HH:MM/2027-06-15T14:00:00+HH:MM. Save enabled.
  2. Save, then reopen the item.
    • EXPECTED: Both sides round-trip — each side shows its own date, wall-clock time, AM/PM (or 24H), and offset unchanged (not shifted to UTC).

Range with time on only one side

  1. With a range, enter start 2026/05/20 with time 10:30:00, and end 2027/06/15 with no time.
    • EXPECTED: No error. Preview shows 2026-05-20T10:30:00+HH:MM/2027-06-15 (a timed start with a date-only end is allowed). Save enabled.
  2. Repeat with the time on the end side only (start date-only, end timed).
    • EXPECTED: Preview shows 2026-05-20/2027-06-15T14:00:00+HH:MM. Save enabled.

A range side with a time still requires a complete date on that side

  1. With a range, enter start year 2026 only (no month/day) but add a time to the start, and a valid end date.
    • EXPECTED: The footer shows A complete date is required when time is provided. and Save is disabled (the requirement applies per side).
  2. Fill in the start month and day.
    • EXPECTED: The error disappears.

Range validation (start after end)

  1. With a range, enter start 2027 and end 2026.
    • EXPECTED: The date range is not valid. Please make sure the start date is before the end date. in the footer and Save is disabled.

Open-ended range — "sometime after"

  1. With a range, enter a start date and leave the end empty.
    • EXPECTED: Preview shows 2026-05/... In the sidebar it reads Sometime after.

Open-ended range — "sometime before"

  1. With a range, leave the start empty and enter an end date.
    • EXPECTED: Preview shows ../2027-06. In the sidebar it reads Sometime before.

Per-side qualifiers on a range

  1. With a range, toggle Approximate on the start side and Uncertain on the end side.
    • EXPECTED: Preview shows the qualifier on each side, e.g. 2026-05~/2027-06?.

Unspecified digits on one side of a range

  1. With a range, enter start 2026/blank month/11 with Approximate, and end 2027/01.
    • EXPECTED: Preview shows 2026-XX-11~/2027-01. Round-trips on reopen.

Unknown on one side of a range

  1. With a range, toggle Unknown on the start side and enter an end date.
    • EXPECTED: The start side shows as Unknown; Save enabled.

Clearing

Clear date and time (inline picker)

  1. On an item that already has a date, open the inline sidebar picker and click Clear date and time, then save.
    • EXPECTED: The date is removed from the item (stored value becomes empty/null).

Clear start / clear end (modal)

  1. In the modal with a range, click Clear start date and time.
    • EXPECTED: Only the start side is cleared.
  2. Click Clear end date and time.
    • EXPECTED: Only the end side is cleared.

Save gating & persistence

Save disabled while invalid (modal)

  1. Enter any invalid value (e.g. month 13).
    • EXPECTED: The Save button is disabled, the field shows its specific message inline, and the footer shows the generic error.
  2. Correct the value.
    • EXPECTED: Save becomes enabled and the footer shows the valid preview.

Cancel discards changes

  1. Change a value, then click Cancel (or click outside the inline picker).
    • EXPECTED: No change is saved; the previously stored value is restored.

Round-trip persistence

  1. For each of: full date, year-only, unspecified month, partial year, approximate/uncertain/combined, unknown, a date range, an open-ended range, and a full date + time — save it, then reopen the item.
    • EXPECTED: Each value displays correctly in the sidebar and reopens in the modal with the same fields, qualifiers, and time populated.

New folder persistence

  1. Create a new folder and add an edtf date to it.
  • EXPECTED: Value displays correctly in the sidebar.
  1. Change the date on the folder
  • EXPECTED: The new value displays correctly in the sidebar and modal, with no general error or BE error.

@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.42553% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 52.64%. Comparing base (3dbec72) to head (80e59ec).
⚠️ Report is 8 commits behind head on main.

Files with missing lines Patch % Lines
...c/app/shared/services/edtf-service/edtf.service.ts 92.85% 0 Missing and 8 partials ⚠️
...nts/datepicker-input/datepicker-input.component.ts 80.64% 3 Missing and 3 partials ⚠️
...nts/timepicker-input/timepicker-input.component.ts 87.87% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1056      +/-   ##
==========================================
+ Coverage   52.19%   52.64%   +0.44%     
==========================================
  Files         354      354              
  Lines       12089    12199     +110     
  Branches     2185     2220      +35     
==========================================
+ Hits         6310     6422     +112     
+ Misses       5555     5549       -6     
- Partials      224      228       +4     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@aasandei-vsp
aasandei-vsp force-pushed the PER-10642-create-edtf-date-modal-component branch from 2a3db6f to 215f0c0 Compare June 12, 2026 14:33
@aasandei-vsp
aasandei-vsp force-pushed the PER-10643-edtf-validation branch from f3c2c87 to 374ded6 Compare June 15, 2026 12:26
@aasandei-vsp aasandei-vsp self-assigned this Jun 15, 2026
@aasandei-vsp

Copy link
Copy Markdown
Contributor Author

@cecilia-donnelly @slifty I have managed to rebase this one on the branch that's been checked out from too :D

@aasandei-vsp
aasandei-vsp force-pushed the PER-10643-edtf-validation branch 2 times, most recently from 44786c5 to 6de3071 Compare June 15, 2026 12:57
@aasandei-vsp
aasandei-vsp force-pushed the PER-10642-create-edtf-date-modal-component branch from 215f0c0 to a71e024 Compare June 16, 2026 13:40
@aasandei-vsp
aasandei-vsp force-pushed the PER-10643-edtf-validation branch from 6de3071 to 1c74479 Compare June 16, 2026 14:04
@cecilia-donnelly

Copy link
Copy Markdown
Member

Thanks for the branch management! For others reading this (QA), this needs a feature flag to be turned on: edtf-date.

@cecilia-donnelly cecilia-donnelly left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love the way this is looking! The "sometime before"/"sometime after" language for an open ended range is very cool.

  • I did see that if I enter February 29 in a non-leap-year, the error is "Day must be between 1 and 31" which is not exactly what we want to express there. I'm not sure what this should be, but maybe @omnignorant knows.
  • If I leave both start and end dates empty and indicate that this is a range then I get "sometime before .." which isn't exactly right either
  • If I have an open range (no start or end dates), then remove the "range" indicator, the new value is an empty string which gets an error from the backend. Maybe that's fine, but maybe we should instead send "Unknown"? QA/design question.
  • the sidebar behaves slightly differently from the modal for invalid data. The sidebar allows you to click "save" with only a time filled (no date) while the modal blocks that path. The time-only value doesn't save, but in the sidebar the user gets a "We could not update" error in a toast instead of the nicer inline "A complete date is required when time is provided."

None of these are showstoppers, but if possible would be nice to fix!

@aasandei-vsp
aasandei-vsp force-pushed the PER-10642-create-edtf-date-modal-component branch from a71e024 to 3876198 Compare June 17, 2026 12:02
@aasandei-vsp
aasandei-vsp force-pushed the PER-10643-edtf-validation branch from 1c74479 to fe7dd07 Compare June 17, 2026 12:36
@aasandei-vsp
aasandei-vsp force-pushed the PER-10642-create-edtf-date-modal-component branch from 3876198 to b6b99b9 Compare June 18, 2026 08:50
Base automatically changed from PER-10642-create-edtf-date-modal-component to main June 19, 2026 13:30
@aasandei-vsp

aasandei-vsp commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

When the date and time fields are undefined, we send null to the BE PATCH for the displayTime property and then we show the date field empty in the sidebar.
The problem is, the displayTime is set to null even if it has never been set by the user, so we cannot rely on the displayDT fallback.
@cecilia-donnelly How should we deal with this situation? Right now, if we just remove the displayDT fallback for the sidebar, the list shows the displayDT and it looks inconsistent.

image

@aasandei-vsp
aasandei-vsp force-pushed the PER-10643-edtf-validation branch from fe7dd07 to 57c3de4 Compare June 29, 2026 10:27

@slifty slifty left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be coming at this with the wrong design philosophy, so please forgive me for possibly questioning defined specification!

I think right now we have code that is silently attempting to make inferences about user intent which may or may not actually be correct.

I'd encourage we reconsider this approach, and instead simply convey "this isn't correct, you need to fix it by X" (or possibly offering a suggested correction).


All that said, if we do decide to keep attempted inference we should make sure that the logic for save / serialization matches the logic for formatting / rendering.

Comment thread src/app/shared/services/edtf-service/edtf.service.ts
Comment thread src/app/shared/services/edtf-service/edtf.service.ts
Comment thread src/app/shared/services/edtf-service/edtf.service.ts
@cecilia-donnelly

Copy link
Copy Markdown
Member

@aasandei-vsp and I talked about the null problem in displayTime during a meeting. What we will do is remove the fallback on these branches and complete the migration where we copy displayDt values to null displayTime values before we flip the feature flag. In the old UI, there is no way for a user to clear a date, so the fallback will work as intended. This requires that these intermediate PRs also consider the feature flag in the file list, though, because we should keep the fallback in deployed code until the new UI is released (i.e., the feature flag is turned on).

More succinctly:

  • Behind the edtf-date feature flag, remove the fallback in the file list
  • Do the same on other branches working on this feature

@cecilia-donnelly cecilia-donnelly left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Officially requesting changes so this doesn't show up in my "review requested" list anymore. The specific change requested is to remove the fallback in the file list behind the feature flag.

@aasandei-vsp

Copy link
Copy Markdown
Contributor Author

Officially requesting changes so this doesn't show up in my "review requested" list anymore. The specific change requested is to remove the fallback in the file list behind the feature flag.

I have created a commit specifically for this situation, making it easier to review.

@aasandei-vsp

Copy link
Copy Markdown
Contributor Author

I might be coming at this with the wrong design philosophy, so please forgive me for possibly questioning defined specification!

I think right now we have code that is silently attempting to make inferences about user intent which may or may not actually be correct.

I'd encourage we reconsider this approach, and instead simply convey "this isn't correct, you need to fix it by X" (or possibly offering a suggested correction).

All that said, if we do decide to keep attempted inference we should make sure that the logic for save / serialization matches the logic for formatting / rendering.

There are indeed a lot of scenarios and dealing with each one of them proved to be slightly complex. Your suggestions are great and correct, but at some point it was decided that we need to silently make some decisions for the user, as not everyone is savvy when it comes to edtf dates. I'll be addressing the rest of the comments one by one.

@aasandei-vsp
aasandei-vsp force-pushed the PER-10643-edtf-validation branch from 57c3de4 to 67edbff Compare July 14, 2026 12:45

@cecilia-donnelly cecilia-donnelly left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code makes sense but there are a couple things I found while testing it out:

  • Clear date doesn't work because somewhere we're sending an empty string instead of explicit null
  • I can't enter a month value like "3" -- the sidebar simply doesn't show anything if I try. "03" works, but I should be able to enter a plain "3" (or any other single digit) and have it be padded with a zero afterward
  • It looks like the same is happening for hours
  • It lets me enter a value like "2026-XX-10" but then I can't set it to "uncertain" or "approximate." That might be an EDTF restriction, do you know?

I would go back through and do some testing again -- I know this has been waiting a while so maybe there are some odd interactions?

Also I see that you put the keyboard interactions back, disregard my inline comments! Thank you!

}

if (message.includes('complete date is required')) {
return 'A complete date is required when time is provided.';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is smart.

[class.disabled]="disabled"
(click)="toggleDatepicker()"
(keydown.enter)="toggleDatepicker()"
(keydown.space)="toggleDatepicker()"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove these?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's true that at some point in the past Claude removed them for some reason and I didn't notice and I'm glad you did! Now it's all in there, thank you for having such a thorough look!

[class.disabled]="disabled"
(click)="toggleTimepicker()"
(keydown.enter)="toggleTimepicker()"
(keydown.space)="toggleTimepicker()"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question - why remove this? (Maybe this isn't acting as a button anymore?)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's true that at some point in the past Claude removed them for some reason and I didn't notice and I'm glad you did! Now it's all in there, thank you for having such a thorough look!

@aasandei-vsp

aasandei-vsp commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

The code makes sense but there are a couple things I found while testing it out:

  • Clear date doesn't work because somewhere we're sending an empty string instead of explicit null
  • I can't enter a month value like "3" -- the sidebar simply doesn't show anything if I try. "03" works, but I should be able to enter a plain "3" (or any other single digit) and have it be padded with a zero afterward
  • It looks like the same is happening for hours
  • It lets me enter a value like "2026-XX-10" but then I can't set it to "uncertain" or "approximate." That might be an EDTF restriction, do you know?

I would go back through and do some testing again -- I know this has been waiting a while so maybe there are some odd interactions?

Also I see that you put the keyboard interactions back, disregard my inline comments! Thank you!

Clear date+only one digit
I have checked the mentioned behaviors and the first three I couldn't reproduce. I have added a small video, from the branch deployed on dev, where things seem to be working as expected.
The only thing worth mentioning, when you only insert one digit, the next input won't be focused, but that makes sense.
Please let me know if I'm missing something:
https://www.loom.com/share/c0639bb8f5ba4275993d71afa584b61b

Unspecified digits+qualifiers
TL;DR Specs unclear to me(no example to actually confirm this behavior). BE supports it. Made workaround in FE.

The library's grammar treats "unspecified digits" (X) and "qualification" ( ~ / ? / % ) as separate, non-combinable features, so it throws a syntax error on anything like 2026-XX-10~ — regardless of where the qualifier is placed. That error was being caught and surfaced as the generic "date is not valid" message, which is why you could enter 2026-XX-10 but not mark it approximate/uncertain.

I verified the backend happily accepts and stores these strings (2026-XX-10~ and friends), so the restriction was purely FE. The fix works around the library by stripping the trailing qualifier before parsing/validating, then reattaching the flags to the model ourselves — so 2026-XX-10~ now serializes, saves, and parses back into the picker correctly, in both directions.

One intentional boundary: a qualifier combined with a time (e.g. 2026-01-01T10:00:00~) is still rejected — the library doesn't support that combination either, and since an unspecified date and a time can't coexist anyway, we kept that blocked rather than silently allowing it.

@aasandei-vsp
aasandei-vsp force-pushed the PER-10643-edtf-validation branch from bad07a3 to a3a7e5a Compare July 22, 2026 12:11
@aasandei-vsp
aasandei-vsp force-pushed the PER-10643-edtf-validation branch from a3a7e5a to 452da47 Compare July 22, 2026 13:33

@cecilia-donnelly cecilia-donnelly left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, @aasandei-vsp! I tested on dev this time to avoid any local environment oddities. I did find one edge case. When I put in "2021-2-29" (i.e., a leap day in a non-leap year) instead of erroring, the UI autocorrected to "2021-03-01" (no doubt a Date library fix). I think we want to give an error there. Final little things!

@aasandei-vsp
aasandei-vsp force-pushed the PER-10643-edtf-validation branch from 452da47 to 5381112 Compare July 24, 2026 13:27
@cecilia-donnelly

cecilia-donnelly commented Jul 24, 2026

Copy link
Copy Markdown
Member

So much here! So close! Thank you for this long list of tests.

I did find another weird case. I made a new folder on dev ("edtf-tests") for the testing. I intended to test on a record in that folder, but accidentally ran one on the folder itself. I tried to edit the date from "1990-09" to "1990-07." I got a 200 response from the PATCH call in devtools with the correct new value, but the main (top of screen) error toast said that it couldn't "update the item you selected" and in fact it did not. In the console I see Failed to save item property TypeError: can't access property "update" of undefined. I'm sorry, I really wanted to be done with edge cases! I am going through the manual tests you gave above and will edit this comment with anything else I find. I assume this is a problem with folders and records being treated differently somehow. I did test on dev so hopefully you can reproduce this problem on a folder.

  • Minor: range errors (e.g., seconds greater than 59) appear correctly inline, but don't block save (in the sidebar only, correct in modal)
  • Just a question and maybe a need for a design change: Do you know why this range is invalid? "displayTime" failed custom validation because 2020-11~/2024-08-31T04:30:00-05:00 is not valid Level 2 EDTF: Invalid interval start: Invalid date format: 2020-11~? (It's a backend error). If we can't have an approximate start date, which seems like it might be the case, then maybe we should remove the qualifier toggles from the start date on a range?

Some notes that aren't blockers for this PR, but would be improvements:

  • Side note/different ticket, but is there a way to re-select the same record after refresh?
  • Hitting "enter" should save if possible, except if focus is on one of the elements that has a different "enter" definition (datepicker, am/pm, etc)
  • dates with qualifiers get "Invalid date" in the file list -- will be addressed in PER-10655, just wanted to call it out as an explicit case

@aasandei-vsp
aasandei-vsp force-pushed the PER-10643-edtf-validation branch from 5381112 to 6458a27 Compare July 27, 2026 12:11
@aasandei-vsp

aasandei-vsp commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

So much here! So close! Thank you for this long list of tests.

I did find another weird case. I made a new folder on dev ("edtf-tests") for the testing. I intended to test on a record in that folder, but accidentally ran one on the folder itself. I tried to edit the date from "1990-09" to "1990-07." I got a 200 response from the PATCH call in devtools with the correct new value, but the main (top of screen) error toast said that it couldn't "update the item you selected" and in fact it did not. In the console I see Failed to save item property TypeError: can't access property "update" of undefined. I'm sorry, I really wanted to be done with edge cases! I am going through the manual tests you gave above and will edit this comment with anything else I find. I assume this is a problem with folders and records being treated differently somehow. I did test on dev so hopefully you can reproduce this problem on a folder.

  • Minor: range errors (e.g., seconds greater than 59) appear correctly inline, but don't block save (in the sidebar only, correct in modal)
  • Just a question and maybe a need for a design change: Do you know why this range is invalid? "displayTime" failed custom validation because 2020-11~/2024-08-31T04:30:00-05:00 is not valid Level 2 EDTF: Invalid interval start: Invalid date format: 2020-11~? (It's a backend error). If we can't have an approximate start date, which seems like it might be the case, then maybe we should remove the qualifier toggles from the start date on a range?

Some notes that aren't blockers for this PR, but would be improvements:

  • Side note/different ticket, but is there a way to re-select the same record after refresh?
  • Hitting "enter" should save if possible, except if focus is on one of the elements that has a different "enter" definition (datepicker, am/pm, etc)
  • dates with qualifiers get "Invalid date" in the file list -- will be addressed in PER-10655, just wanted to call it out as an explicit case

This is quite an edge case, thank you for looking into it. I couldn't get it to reproduce on my end, but the error you pasted helped me track it down. You were right that the save itself worked; the problem was in the code that syncs the server's response back onto the folder afterward. It matches the response to the folder by folderId, but the folder in memory holds that id as a number while the response comes back with it as a string, so the match failed and we ended up trying to update undefined — which is what reverted your change and threw that error.
I've fixed it by normalizing the id on both sides so it matches reliably, and added a guard so a miss like this can never blow up a save that already succeeded.

image image

We can definitely have a range with qualifiers. The difference is the FE accepts the start date to have a qualifier and the end date to have time attached to it, while the BE does not accept this combination. My take would be just to leave it like this, because the user does get an error shown. Otherwise, I can look into it and see how to sync with the BE. But I feel that there might still be validation differences between BE and FE and not sure we could address them all.

NOTES

  • Hm, there are ways to do this, none of them easy or straight forward. We would have to alter the route, save some data in the session storage or worst case scenario set a "selected" property on the record(FE+BE) changes.

  • No sure it's doable, because we would need to figure out the focus from multiple components. Also, the A11y way is focus the save button and hit enter, which atm works.

  • Yes! We definitely need to figure out dates in the list and how to make them look good and also be sortable.

@cecilia-donnelly cecilia-donnelly left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hurray! Let's go to QA on this one. Thanks for the detailed response to my last comment.

@cecilia-donnelly
cecilia-donnelly requested review from omnignorant and removed request for slifty July 27, 2026 20:58
@cecilia-donnelly cecilia-donnelly added the QA This issue is ready for QA / user acceptance testing label Jul 27, 2026
@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

QA Instructions

Summary

This pull request implements new Extended Date/Time Format (EDTF) validation logic for managing date and time inputs in both a sidebar inline picker and a modal. The changes include:

  1. Inline validation errors for invalid characters, out-of-range values, and logical inconsistencies (e.g., "invalid date ranges").
  2. Auto-padding for single-digit month and day entries.
  3. Consistent date error messaging for invalid inputs on both the inline picker and modal.
  4. Enhanced support for EDTF-specific features such as approximate dates, unspecified digits, and date ranges.

Test Environment Setup

  1. Deploy the updated branch with the relevant commits applied.
  2. Verify that the application has both the sidebar inline picker and the modal for editing dates.
  3. Ensure a test EDTF-enabled item is available to test the date entry settings.

Test Scenarios

Basic Date Entry

  1. Test year-only, year-month, full date, and auto-advance behavior in both inline picker and modal.
  2. Verify backspace navigation properly transitions between fields in reverse order.
  3. Validate date selection using the calendar picker.

Single-Digit Month/Day

  1. Verify single-digit month/day entries are padded (e.g., 202).
  2. Test edge cases: month: 0, day: 0 are rejected with proper inline feedback.

Invalid Characters

  1. Enter invalid characters (e.g., ab) in both the date and time fields to confirm error visibility and user notification.

Out-of-Range Values

  1. Validate appropriate error messages for:
    • Months greater than 12.
    • Days beyond the maximum in the selected month.
    • Hours exceeding 12 in AM/PM or 23 in 24-hour mode.
    • Minutes or seconds exceeding 59.
  2. Ensure these values are not auto-corrected and errors disappear when the values are corrected.

Impossible Calendar Days

  1. Verify dates like February 29 in a non-leap year surface the proper error.
  2. Test dynamic re-validation when the month or year changes.

Time Input

  1. Test all time formats (AM/PM/24-hour) to ensure they toggle correctly and validate input properly.
  2. Confirm midnight and noon (12:00) behavior follows EDTF guidelines.
  3. Verify that incomplete times are invalid unless a complete date is provided.

Unspecified Digits (Modal Only)

  1. Test entry of partial or unspecified dates (e.g., 19XX or 2026-XX-11) to ensure correct preview rendering and saving.
  2. Confirm that saved unspecified data persists upon reopening.

Qualifiers (Modal Only)

  1. Validate behaviors for Approximate, Uncertain, and combined qualifiers.
  2. Test qualifiers combined with unspecified digits and confirm correct preview generation.
  3. Ensure qualifiers with time cause Save to be disabled, with appropriate error messaging.
  4. Confirm toggle functionality and exclusivity when switching between Unknown and other qualifiers.

Date Ranges (Modal Only)

  1. Test precision date ranges (start-end dates), open-ended ranges, and ranges involving unspecified digits.
  2. Validate per-side qualifiers, e.g., Approximate on start and Uncertain on end.
  3. Verify error handling for invalid range scenarios (e.g., start date after end date).
  4. Ensure round-trip consistency for various range scenarios.

Clearing Dates and Times

  1. Verify the Clear Date and Time button in the inline picker correctly clears the value.
  2. Confirm that Clear start date and Clear end date buttons in the modal remove the appropriate fields.

Save Gating and Persistence

  1. Ensure the Save button is disabled while there are validation errors.
  2. Validate no changes are saved if Cancel is clicked in either the inline picker or modal after modifying values.
  3. Test end-to-end persistence for different valid values (single dates, date ranges, approximate dates, etc.) — save, then reopen to ensure the values are retained.

Regression Risks

  1. Shared Component Behavior:
    • The updates affect both the sidebar and modal, so inconsistencies or unintended regressions may manifest differently in these UIs.
  2. Parsing & Formatting Logic:
    • Changes to how single-digit months/days are padded and stored might disrupt integrations or downstream systems if the EDTF serialization rules are mismatched.
  3. Edge Cases:
    • Ensure leap years, unspecified digits, and out-of-bounds inputs follow the new rules universally.

Things to Watch For

  1. Keyboard Accessibility:
    • Verify that keyboard interactions for the calendar and time pickers work correctly after the restoration of the tab and keydown handlers.
  2. Date Clearing:
    • Ensure null values for cleared dates are properly sent to the backend and that previously cleared values are displayed correctly when the modal or sidebar is reopened.

By following these test instructions, you'll cover all functional and edge case scenarios introduced in this pull request.


Generated by QA Instructions Action

Left-pad single-digit month and day values with zero instead of
X-padding them, since a single 1-9 digit can only be a complete value
('5' -> '05', no longer '5X'). Require a complete date whenever a time
is provided and surface a dedicated friendly error message for that
case. Add a generic getSegmentError helper that the datepicker and
timepicker inputs use to build their inline field error messages
(invalid characters and out-of-range checks, skipped while the value
is still being typed).

Issue: PER-10643
Instead of silently rejecting invalid input, the year, month and day
segments now emit whatever the user typed and surface a per-field
error message below the input (invalid characters, out-of-range month
or day). The day is re-validated when the month changes, and picking
a date from the calendar clears all errors. The error styling lives
in new shared input-error-state and input-error-message mixins.

Issue: PER-10643
Instead of silently rejecting invalid input, the hours, minutes and
seconds segments now emit whatever the user typed and surface a
per-field error message below the input. The hour is re-validated
when the AM/PM/24H format changes.

Issue: PER-10643
The inline-error restructure dropped role="button", tabindex, and the
keydown.enter/space handlers from the calendar and clock toggle buttons,
leaving them mouse-only. Restore them and add keyboard-toggle tests.

Issue: PER-10643
Persist null instead of '' when a date is cleared, and render an
explicitly-null displayTime as empty ("Click to add date"). Widen
displayTime to string | null and add specs.

Issue: PER-10643
With the flag on, displayTime is authoritative (the migration backfills
it before the flip), so a missing value means the date was cleared and
nothing is shown. With the flag off, the file list keeps its fallback
so deployed code is unchanged.

Issue: PER-10643
Move the picker's display formatting into EdtfService so the preview
and the serialized EDTF are built from the same padding helpers — a
single-digit month now reads as the month itself ("1" -> January /
1985-01) in both. A lone "0" month or day is rejected with a clear
range error instead of being silently serialized to "0X".

Issue: PER-10643
The edtf library rejects a qualifier (~/?/%) combined with unspecified
(X) digits, so approximate/uncertain dates with a blank month or day
(e.g. 2026-XX-10~) failed validation, even though the backend accepts
them. Strip the qualifier before parsing/validating and reattach the
flags ourselves so the combination round-trips both ways.

Issue: PER-10643
An impossible day such as Feb 29 in a non-leap year was accepted and
silently autocorrected on save (the edtf library rolls it forward via a
JS Date overflow, e.g. 2021-02-29 → 2021-03-01). Validate the day against
the month and year and reject it instead, and pad a single-digit month so
the day is checked against the right month rather than January.

Show each segment's specific message inline on the offending field
(day-for-month, out-of-range, lone "0") while the footer keeps the generic
"not valid" message, and move the day/month segment validation into
EdtfService.

Issue: PER-10643
Normalize folderId to a string on both sides of the lookup and guard the
update so an unmatched response can never crash an otherwise-successful
save.

Issue: PER-10643
@aasandei-vsp
aasandei-vsp force-pushed the PER-10643-edtf-validation branch from 6458a27 to 80e59ec Compare July 28, 2026 09:58
@aasandei-vsp

Copy link
Copy Markdown
Contributor Author

@cecilia-donnelly Thank you for putting this up for QA. I have added a manual test for the edge case you found with the new folder.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

QA This issue is ready for QA / user acceptance testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants