[PER-10416] Add date and time selection modal#924
Conversation
77aad83 to
adb771a
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #924 +/- ##
==========================================
+ Coverage 49.88% 51.03% +1.15%
==========================================
Files 348 354 +6
Lines 11501 11978 +477
Branches 1974 2131 +157
==========================================
+ Hits 5737 6113 +376
- Misses 5576 5649 +73
- Partials 188 216 +28 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
e5ac965 to
f4748a1
Compare
cecilia-donnelly
left a comment
There was a problem hiding this comment.
I'm sorry, I forgot to finish and submit this!
| day: string; | ||
| } | ||
|
|
||
| const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; |
There was a problem hiding this comment.
It seems wrong for us to keep track of this ourselves - is there a way to use a library?
| if (!m || m < 1 || m > 12) return 31; | ||
|
|
||
| if (m === 2 && y) { | ||
| return this.isLeapYear(y) ? 29 : 28; |
There was a problem hiding this comment.
I see you handle February here, but it does seem strange to have to write this ourselves.
|
|
||
| endTime = signal<TimeObject>({ ...DEFAULT_TIME }); | ||
|
|
||
| edtfValue = computed(() => { |
There was a problem hiding this comment.
EDTF can get complicated - I would use something like https://www.npmjs.com/package/edtf?activeTab=readme for parsing
There was a problem hiding this comment.
That one might be better suited to transforming a stored EDTF date back into something that makes sense on the client? Generally just saying that we don't need to write all the logic ourselves!
400b7dc to
db36d72
Compare
2a00482 to
fbae542
Compare
b81c5dc to
f4a9326
Compare
cecilia-donnelly
left a comment
There was a problem hiding this comment.
Can you base this on PER-10475-add-patch-folder, so we're not reviewing the same commits in two different places? Once that one is merged this one should automatically switch to being based on main.
cecilia-donnelly
left a comment
There was a problem hiding this comment.
This is a lot! It looks fantastic. I am definitely wondering about doing less of our own date building (as I was asking in my first encounter with this PR a while ago), but I love how you're using the EDTF library to validate.
| { offset: 'GMT+11:00', name: 'Solomon Islands Time' }, | ||
| { offset: 'GMT+12:00', name: 'New Zealand Standard Time' }, | ||
| { offset: 'GMT+13:00', name: 'Tonga Standard Time' }, | ||
| ]; |
There was a problem hiding this comment.
Ideally we would not have a list of timezones hardcoded here, though I understand why you did it this way. Where did this list come from?
| static to24Hour(hour12: number, amPm: Meridian): number { | ||
| if (amPm === Meridian.AM) return hour12 === 12 ? 0 : hour12; | ||
| return hour12 === 12 ? 12 : hour12 + 12; | ||
| } |
There was a problem hiding this comment.
I'm going commit by commit and I think I've seen another to24Hour along the way, but maybe it moved here?
| 'GMT+11:00': 'SBT', | ||
| 'GMT+12:00': 'NZST', | ||
| 'GMT+13:00': 'TOT', | ||
| }; |
There was a problem hiding this comment.
Same comment about not keeping our own list of timezones or, here, abbreviations.
2417ede to
db9ea7e
Compare
db9ea7e to
322ddd0
Compare
322ddd0 to
7cb4e73
Compare
a7e65a3 to
3ce186a
Compare
…splay dates Add new getStelaFolderVOs method to retrieve updated folder information from the v2 API endpoint. This method handles both single and multiple folder requests with optional share token support. Add updateStelaFolder method to update folder date ranges by combining start and end dates into EDTF interval format before sending to the server. These changes enable proper synchronization of folder date information between the client and server when users edit date ranges in the UI. ISSUE: PER-10475
Add displayTime and displayEndTime getters to parse EDTF interval strings from the server. These getters split interval values like "1985-05-20/1990-06-15" into separate start and end dates for display in the UI. Update template to use the new getters instead of directly accessing displayDT and displayEndDT properties. Add fallback logic to use legacy properties when the displayTime interval is not available. ISSUE: PER-10475
This new component will be presentational, it will get a date input and it will also return a selected date. It includes a calendar. Issue: PER-10416
The time picker is a presentational component, its input si a time object and it returns a time object as well. It contains a time selector. It works with meridian. Issue: PER-10416
The edit date and time component is smart, because it manages its own state, but also the states of the time and date pickers. But still the state is isolated to this specific component, it gets as input a date and time object and returns back a date and time object. It handles the time picker, the date picker, the date qualifiers, start and end time and also shows an edtf calculated value. Issue: PER-10416
The service has the resposability to open the modal for the edit date time component and provide the initial data. Issue: PER-10416
Because we are using the timezone dropdown component in three places now, extracting it into a presentational component for reusability seems the right approach. It will be a generic isolated component that will provide an input and a dropdown for choosing the timezone. Issue: PER-10416
This component will be the smaller version to set the date and time, it will open the modal editor for more comporehensive choices. Issue: PER-10416
Issue: PER-10416
3ce186a to
ccb543e
Compare
Moved all logic related to date and time types and validations, including EDTF mappings into a single point of truth, in the edtf service. This way the components will only be presentational and only manipulate their own internal state. ISSUE: PER-10416
ccb543e to
c435a8d
Compare
There was a problem hiding this comment.
Pull request overview
Adds a new EDTF-focused date/time editor UX (inline sidebar dropdown + full modal) and extends the EDTF service to support timezones, qualifiers, ranges, and additional validation/formatting needed by the UI.
Changes:
- Introduces new shared UI components for date, time, and timezone input, plus a new sidebar date picker and “Edit date and time” modal.
- Expands
EdtfServiceto parse/format EDTF with ranges, qualifiers, unknown values, and timezone handling, with updated tests. - Updates sidebar integration and save/error handling, and extends record mapping to include
displayTime.
Reviewed changes
Copilot reviewed 36 out of 36 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| src/styles/_mixins.scss | Adds reusable mixins for the new date/time picker UI. |
| src/styles/_dialog.scss | Adds dialog positioning styles for the new modal. |
| src/styles/_colors.scss | Expands the color palette and adds aliases for backward compatibility. |
| src/app/shared/services/edtf-service/edtf.service.ts | Major EDTF parsing/formatting enhancements (qualifiers, ranges, timezones, validation helpers). |
| src/app/shared/services/edtf-service/edtf.service.spec.ts | Updates and extends EDTF service unit tests for new behavior. |
| src/app/shared/services/data/data.service.spec.ts | Adjusts mocking for lean item fetching in tests. |
| src/app/shared/services/api/record.repo.ts | Adds displayTime mapping into record conversion. |
| src/app/shared/services/api/record.repo.spec.ts | Adds/updates tests for displayTime mapping. |
| src/app/shared/components/timezone-dropdown/timezone-dropdown.component.ts | New standalone timezone dropdown component logic. |
| src/app/shared/components/timezone-dropdown/timezone-dropdown.component.spec.ts | Unit tests for timezone dropdown behavior. |
| src/app/shared/components/timezone-dropdown/timezone-dropdown.component.scss | Styles for timezone dropdown. |
| src/app/shared/components/timezone-dropdown/timezone-dropdown.component.html | Template for timezone dropdown UI. |
| src/app/shared/components/timepicker-input/timepicker-input.component.ts | New standalone time input + ngb-timepicker popover logic. |
| src/app/shared/components/timepicker-input/timepicker-input.component.spec.ts | Unit tests for timepicker input behavior/validation. |
| src/app/shared/components/timepicker-input/timepicker-input.component.scss | Styles for time input + ngb-timepicker popover. |
| src/app/shared/components/timepicker-input/timepicker-input.component.html | Template for time input and popover. |
| src/app/shared/components/datepicker-input/datepicker-input.component.ts | New standalone date input + ngb-datepicker popover logic. |
| src/app/shared/components/datepicker-input/datepicker-input.component.spec.ts | Unit tests for datepicker input behavior/validation. |
| src/app/shared/components/datepicker-input/datepicker-input.component.scss | Styles for date input + ngb-datepicker popover. |
| src/app/shared/components/datepicker-input/datepicker-input.component.html | Template for date input and popover. |
| src/app/file-browser/file-browser-components.module.ts | Registers the new sidebar date picker in the file-browser module. |
| src/app/file-browser/components/sidebar/sidebar.component.ts | Integrates sidebar date picker + modal open/save flow. |
| src/app/file-browser/components/sidebar/sidebar.component.spec.ts | Updates sidebar tests for modal integration and saving. |
| src/app/file-browser/components/sidebar/sidebar.component.html | Replaces inline date edit fields with the new sidebar date picker component. |
| src/app/file-browser/components/sidebar-date-picker/sidebar-date-picker.component.ts | New inline sidebar date/time editor wrapper with “More options” escalation. |
| src/app/file-browser/components/sidebar-date-picker/sidebar-date-picker.component.spec.ts | Unit tests for sidebar date picker display and interactions. |
| src/app/file-browser/components/sidebar-date-picker/sidebar-date-picker.component.scss | Styles for sidebar date picker and inline panel. |
| src/app/file-browser/components/sidebar-date-picker/sidebar-date-picker.component.html | Template for sidebar date picker and inline panel. |
| src/app/file-browser/components/edit-date-time-modal/edit-date-time-modal.service.ts | Service wrapper to open the edit date/time modal via CDK dialog. |
| src/app/file-browser/components/edit-date-time-modal/edit-date-time-modal.service.spec.ts | Unit tests for the modal service open() config. |
| src/app/file-browser/components/edit-date-time-modal/edit-date-time-modal.component.ts | New modal component for full EDTF editing (ranges, qualifiers, timezones). |
| src/app/file-browser/components/edit-date-time-modal/edit-date-time-modal.component.spec.ts | Unit tests for modal state/validation/save behavior. |
| src/app/file-browser/components/edit-date-time-modal/edit-date-time-modal.component.scss | Modal styling. |
| src/app/file-browser/components/edit-date-time-modal/edit-date-time-modal.component.html | Modal template. |
| src/app/core/services/edit/edit.service.ts | Improves save error handling by reverting state and showing errors. |
| src/app/core/services/edit/edit.service.spec.ts | Adds tests for new edit-service error handling behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
cecilia-donnelly
left a comment
There was a problem hiding this comment.
This is such a major undertaking! I think it would be worth writing a blog post about it afterward, so perhaps someone else could benefit from your research and work.
At this point my overall feedback is to rely more on moment (since it's already in the project) or to choose another datetime library. The most urgent requirement for a library is timezone management, in my opinion, but it could also simplify the other code you've written for parsing dates. Obviously it won't work for everything, since EDTF dates aren't necessarily "real" dates from the perspective of a date library. It does seem like it could help with am/pm parsing, though, for example, and much of the validation you've written. Specific comments inline!
|
I did go through all the comments, but changes are still in progress. I will let you guys know when it can be reviewed again! |
Migrated the date and time validators and sidebar formatting onto date-fns (parse, isValid, format, setYear), and removed dead methods (isValidDate, isValidTime, isLeapYear, getMaxDaysInMonth) along with the DAYS_IN_MONTH constant. Renamed to24HourTime to parseTimeAs24Hour and folded it into buildTimeString to drop the duplicated 12->24-hour conversion. Also buildDateString now zero-pads partial years. Issue: PER-1041
Backspace in an empty segment (day, month, seconds, minutes) now deletes the last character of the previous segment and focuses it. Auto-focus to the next field still waits for 2 digits. Issue: PER-10416
Bellow is information about what has been developed on this task and how to test it. This summary and test cases have been generated using Claude Code.
============================================================
EDTF DATE EDITING - COVERAGE & MANUAL TEST PLAN (PER-10590)
This document captures three things:
What part of the EDTF standard the new UI lets users produce -
the things QA should check work end-to-end.
What it doesn't (and why) - so we don't end up filing
intentional limitations as bugs.
A structured manual-test checklist QA can walk through.
============================================================
PART 1 - WHAT WE SUPPORT, AND WHAT WE DON'T
The EDTF standard is layered. Level 0 covers everyday calendar
dates. Level 1 adds qualifiers ("approximately 1985") and intervals
(date ranges with open ends). Level 2 adds more exotic features
like seasons, sets of dates, and BCE years. The new editor is
focused on the parts our archivists actually use.
For every construct below, we mark it as one of:
Supported The user can produce this from the UI today,
and it round-trips back when they reopen the
record.
Implicit only The underlying parser can read it (so old data
is preserved correctly), but the UI doesn't
give users a way to create it from scratch. If
we want users to be able to enter these values,
we'll need follow-up tickets.
Not supported Neither the UI nor the underlying service
handles this construct at all.
Level 0 - everyday calendar dates
Just a year (e.g. 1985)
Status: Supported
Year and month (e.g. 1985-04)
Status: Supported
Full date (e.g. 1985-04-12)
Status: Supported
Date with UTC time (e.g. 1985-04-12T23:20:30Z)
Status: Supported
Date with offset time (e.g. 1985-04-12T23:20:30-04:00)
Status: Supported
Closed date range (e.g. 1964/2008)
Status: Supported
Level 1 - qualifiers and partial dates
Approximate date, "around" (e.g. 1984~)
Status: Supported
Uncertain date, "we think but we're not sure" (e.g. 1984?)
Status: Supported
Both at once, "around and unsure" (e.g. 1984%)
Status: Supported
Year with unknown digits, e.g. some time in the 20th century (19XX)
Status: Implicit only
Month with unknown value (e.g. 1985-XX)
Status: Implicit only
Day with unknown value (e.g. 1985-04-XX)
Status: Implicit only
Whole date is unknown (XXXX-XX-XX)
Status: Supported
Range with open start (e.g. ../1985-04-12)
Status: Supported
Range with open end (e.g. 1985-04-12/..)
Status: Supported
Range bound marked as uncertain instead of open (e.g. ?/1985)
Status: Not supported
Season - spring, summer, autumn, winter (e.g. 1985-22)
Status: Not supported
Level 2 - advanced features we intentionally don't handle
Sub-year groupings - quarters, semesters, etc. (e.g. 1985-26)
Status: Not supported
"One of these dates" (e.g. [1667,1668,1670..1672])
Status: Not supported
"All of these dates" (e.g. {1667,1668})
Status: Not supported
Per-component qualifiers, e.g. only the day is uncertain
(e.g. ?2004-06-~01)
Status: Not supported - a qualifier always applies to the
whole date
BCE / negative years (e.g. -0533)
Status: Not supported
Long-form years, more than 4 digits (e.g. Y170000002)
Status: Not supported
Things to know about time and timezones
The time fields use a 12-hour clock with AM/PM. Internally we
convert this to 24-hour format before saving, and convert back
when reading.
You can only attach a time to a complete date (year + month +
day). If you have only a year, or year + month, the time is
silently dropped on save. This is intentional - partial dates
with times don't really make sense - but it can look like a
bug if you don't know.
The timezone is picked from a fixed list of 32 GMT offsets
(e.g. "Eastern Standard Time / GMT-05:00"). We don't model
daylight savings, region-specific zones (like
"America/New_York"), or arbitrary offsets.
If you fill in seconds but leave minutes blank, the time is
rejected as invalid.
If you fill in minutes but leave seconds blank, seconds
default to 00 on save.
Validation messages users will see
Both Approximate and Uncertain are on, but the underlying
date is invalid:
"The date is not valid as both approximate and uncertain.
Please check the date values."
Approximate is on, the underlying date is invalid:
"The date is not valid as approximate. Please check the
date values."
Uncertain is on, the underlying date is invalid:
"The date is not valid as uncertain. Please check the
date values."
A date range with the end before the start, or otherwise
unparseable as an interval:
"The date range is not valid. Please make sure the start
date is before the end date."
Anything else the parser doesn't accept:
"The date entered is not valid. Please check the values
and try again."
============================================================
PART 2 - MANUAL TEST CASES
Section A - Entering single dates (Sidebar)
A1. Enter year 1985 only and save.
Saved EDTF: 1985
Sidebar: "1985"
A2. Year 1985, month 04, then save.
Saved EDTF: 1985-04
Sidebar: "April 1985"
A3. Year 1985, month 04, day 12, then save.
Saved EDTF: 1985-04-12
Sidebar: "April 12, 1985"
A4. Continue from A3, add 02:30:00 AM with timezone "Eastern
Standard Time", then save.
Saved EDTF: 1985-04-12T02:30:00-05:00
Sidebar: "02:30:00 AM EST"
A5. Continue from A3, add 12:00 AM (midnight), then save.
Saved EDTF: ends in T00:00:00
Sidebar: "12:00:00 AM" preserved on reload
A6. Continue from A3, add 12:00 PM (noon), then save.
Saved EDTF: ends in T12:00:00
Sidebar: "12:00:00 PM" preserved on reload
A7. Continue from A3, add 01:15 PM with timezone "Greenwich
Mean Time", then save.
Saved EDTF: 1985-04-12T13:15:00+00:00
Sidebar: "01:15:00 PM GMT"
A8. After saving any date above, reopen the dropdown and click
the Clear date and time (trash) button, then save.
Saved EDTF: empty string
Sidebar: "Click to add date"
Section B - Date ranges (Modal)
B1. Open the modal, toggle "Use a date range", enter 1985 then
2000, save.
Saved EDTF: 1985/2000
B2. Range 1985-04-12 to 1985-04-20, save.
Saved EDTF: 1985-04-12/1985-04-20
B3. Same start and end date, but start time 09:00 AM EST and
end time 05:00 PM EST, save.
Saved EDTF:
1985-04-12T09:00:00-05:00/1985-04-12T17:00:00-05:00
B4. Open-end: enter only the start date 1985-04-12, leave the
end blank, save.
Saved EDTF: 1985-04-12/..
B5. Open-start: leave the start blank, enter only the end date
2000-01-01, save.
Saved EDTF: ../2000-01-01
B6. Range toggle is on but both start and end are blank, save.
Saved EDTF: empty value (no date saved)
Section C - Qualifiers (Modal)
C1. Year 1985, toggle Approximate on, save.
Saved EDTF: 1985~
UI: sidebar shows a blue dot next to the date and
the word "Approximate"
C2. Date 1985-04-12, toggle Uncertain on, save.
Saved EDTF: 1985-04-12?
UI: sidebar shows the word "Uncertain"
C3. Date 1985-04-12, toggle both Approximate and Uncertain on,
save.
Saved EDTF: 1985-04-12%
UI: sidebar shows "Approximate, Uncertain"
C4. Toggle Unknown on, save.
Saved EDTF: XXXX-XX-XX
UI: all date and time fields go grey and become
uneditable; sidebar shows "Unknown"
C5. Enter a full date and time, toggle Unknown on, then
immediately toggle it off again.
Expected: the original date and time should come back
unchanged. Verifies that turning Unknown on
doesn't permanently destroy the user's
previous input.
C6. With Unknown on, try to toggle Approximate or Uncertain.
Expected: those toggles should be disabled and
unclickable.
C7. Date 1985-04-12T02:30:00-05:00, toggle Approximate on, save.
Saved EDTF: 1985-04-12T02:30:00-05:00~
UI: the qualifier marker (~) appears AFTER the
timezone, not in the middle of the string.
Section D - Reading existing values
D1. Stored: 1985
Modal: year 1985; month, day, time and qualifiers all empty
D2. Stored: 1985-04-12T23:20:30Z
Modal: date 1985-04-12, time 11:20:30 PM, timezone
"Greenwich Mean Time"
D3. Stored: 1985-04-12T23:20:30+05:30
Modal: date populated, time populated, timezone
"India Standard Time"
D4. Stored: 1985~
Modal: year 1985, Approximate toggle on
D5. Stored: 1985%
Modal: year 1985, Approximate on, Uncertain on
D6. Stored: XXXX-XX-XX
Modal: Unknown toggle on, all other fields blank
and disabled
D7. Stored: 1985/2000
Modal: range toggle on, start year 1985, end year 2000
D8. Stored: 1985-04-12/..
Modal: range toggle on, start date filled in, end date
blank or showing ".."
D9. Stored: ../2000-01-01
Modal: range toggle on, start blank or showing "..",
end date filled in
Section E - Field-level validation while typing
E1. Year, type "abcd".
Expected: letters are rejected; the field stays empty.
E2. Year, type "0985".
Expected: the leading zero is rejected - years must not
start with 0.
E3. Year, type "19" then click away.
Expected: accepted as in-progress; nothing is saved
until 4 digits are entered.
E4. Year, type "1985".
Expected: after the 4th digit, focus jumps automatically
to the month field.
E5. Month, type "13".
Expected: the second digit is rejected; only 01-12 are
allowed.
E6. Month, type "2" as the first digit.
Expected: rejected; the first digit of a month must be
0 or 1.
E7. Month, type "02".
Expected: focus jumps automatically to the day field.
E8. Day, in February of a leap year (e.g. 2000), type "29".
Expected: accepted.
E9. Day, in February of a non-leap year (e.g. 1985), type "29".
Expected: rejected - that year's February only has 28
days.
E10. Day, in April, type "31".
Expected: rejected - April only has 30 days.
E11. Day, type "00".
Expected: rejected.
E12. Hour, type "13".
Expected: rejected - the input is a 12-hour clock.
E13. Hour, type "00".
Expected: the single digit 0 is allowed in-progress, but
00 as a complete value is rejected.
E14. Minutes, type "60".
Expected: rejected.
E15. Minutes, type "9" as the first digit.
Expected: rejected - the first digit of minutes must be
0-5.
E16. Seconds, enter seconds while the minutes field is still
blank.
Expected: the Save button stays disabled and the modal
shows the generic "date entered is not valid"
error.
Section F - Whole-date validation in the modal
F1. Range with start year 2000 and end year 1985 (end before
start).
Error: "The date range is not valid. Please make sure
the start date is before the end date."
Save: disabled
F2. Date 2024-02-30 (a day that doesn't exist).
Error: "The date entered is not valid. Please check
the values and try again."
Save: disabled
F3. Approximate on with otherwise-invalid data.
Error: "The date is not valid as approximate. Please
check the date values."
Save: disabled
F4. Uncertain on with otherwise-invalid data.
Error: "The date is not valid as uncertain. Please
check the date values."
Save: disabled
F5. Both qualifiers on with otherwise-invalid data.
Error: "The date is not valid as both approximate and
uncertain. Please check the date values."
Save: disabled
F6. Year only (1985) and try to add a time.
Error: none, but the time is silently dropped on save
(intentional - time needs a full year/month/day).
Save: enabled
F7. Open-start range: blank start, end 2000-01-01.
Error: none.
Save: enabled - saves as ../2000-01-01
Section G - Timezone dropdown
G1. Open the dropdown, type "Eastern".
Expected: the list filters to "Eastern Standard Time"
and "Eastern European Standard Time".
G2. Type "+05:30".
Expected: the list filters to "India Standard Time".
G3. Type "xyzzy".
Expected: the list is empty (apart from the "Select
timezone" reset row at the top).
G4. Pick "India Standard Time", save, reopen the record.
Expected: sidebar shows the abbreviation "IST"; modal
preselects "India Standard Time".
G5. Pick "International Date Line West" (GMT-12:00), save.
Expected: sidebar shows the abbreviation "IDLW".
G6. After picking a timezone, reopen the dropdown and click the
"Select timezone" row at the top.
Expected: the timezone clears; the saved EDTF no longer
has an offset suffix.
G7. Range toggle on; set the start timezone to "Eastern Standard
Time" and the end timezone to "Japan Standard Time", save.
Expected: each side of the saved range carries its own
offset - they don't have to match.
Section H - UI integration and regressions
H1. Open the sidebar on a record where the user only has
VIEWER permission.
Expected: the date row shows the current value (or
"No date") with no edit icon, and nothing
happens when clicked.
H2. Open the sidebar's date dropdown, then click More options.
Expected: the dropdown closes and the full modal opens,
pre-populated with whatever was in the dropdown.
H3. Inside the modal, change something, then click Cancel.
Expected: the modal closes; the sidebar still shows the
original (pre-edit) value.
H4. Inside the modal, enter valid data, then click Save.
Expected: the modal closes; the sidebar reflects the new
value; reloading the page keeps it.
H5. Click the date row of a record that already has qualifiers,
a range, or "unknown" set.
Expected: the modal opens directly - the lightweight
inline dropdown is skipped because the existing
data is too complex for it.
H6. Inside the modal, click Clear date and time (the trash
icon), then Save.
Expected: the date is wiped; the sidebar shows the empty
state and reloading confirms the value is
actually gone.
H7. Save the modal while disconnected from the network.
Expected: a toast error appears (no silent failure).