From 2649cdea2d1344c242f2ee098aa2d957f354991e Mon Sep 17 00:00:00 2001 From: Darren Thornton <6361057+dthornton-hyl@users.noreply.github.com> Date: Tue, 5 May 2026 10:14:15 -0500 Subject: [PATCH] AAE-44003 Allow manual input for date field (#11830) * AAE-44003 Allow manual input for date field * copilot suggestions * code review update * code review update --- .../card-view-dateitem.component.html | 124 ++-- .../card-view-dateitem.component.spec.ts | 592 +++++++++++++++++- .../card-view-dateitem.component.ts | 95 ++- ...card-view-dateitem-properties.interface.ts | 1 + .../models/card-view-dateitem.model.spec.ts | 167 +++++ .../models/card-view-dateitem.model.ts | 22 +- .../card-view-datetimeitem.model.spec.ts | 59 ++ .../models/card-view-datetimeitem.model.ts | 6 +- .../lib/common/utils/datetime-fns-adapter.ts | 2 +- 9 files changed, 1002 insertions(+), 66 deletions(-) create mode 100644 lib/core/src/lib/card-view/models/card-view-dateitem.model.spec.ts create mode 100644 lib/core/src/lib/card-view/models/card-view-datetimeitem.model.spec.ts diff --git a/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.html b/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.html index 23217f01ab..1d49c59a9b 100644 --- a/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.html +++ b/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.html @@ -10,66 +10,78 @@ > {{ property.label | translate }} -
- - - @if (showProperty) { - {{ property.displayValue }} - } @else { - {{ property.default | translate }} - } + @if (property.allowManualInput) { + + } @else { +
+ + + @if (showProperty) { + {{ property.displayValue }} + } @else { + {{ property.default | translate }} + } + - -
- - @if (showClearAction) { - - } - - - + + } + @if (showClearAction) { + + } + + + } @else { diff --git a/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.spec.ts b/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.spec.ts index 7406d16108..08f130bb58 100644 --- a/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.spec.ts +++ b/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.spec.ts @@ -22,12 +22,15 @@ import { CardViewDateItemComponent } from './card-view-dateitem.component'; import { ClipboardService } from '../../../clipboard/clipboard.service'; import { CardViewDatetimeItemModel } from '../../models/card-view-datetimeitem.model'; import { AppConfigService } from '../../../app-config/app-config.service'; -import { MatDatetimepickerInputEvent } from '@mat-datetimepicker/core'; +import { DatetimeAdapter, MatDatetimepickerInputEvent } from '@mat-datetimepicker/core'; +import { DateAdapter } from '@angular/material/core'; import { HarnessLoader } from '@angular/cdk/testing'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { addMinutes } from 'date-fns'; import { UnitTestingUtils } from '../../../testing/unit-testing-utils'; import { MatFormField } from '@angular/material/form-field'; +import { AdfDateFnsAdapter } from '../../../common/utils/date-fns-adapter'; +import { AdfDateTimeFnsAdapter } from '../../../common/utils/datetime-fns-adapter'; describe('CardViewDateItemComponent', () => { let loader: HarnessLoader; @@ -302,6 +305,18 @@ describe('CardViewDateItemComponent', () => { expect(component.property.value).toBeNull(); expect(component.property.default).toBeNull(); }); + + it('should not touch the form control when onDateClear is called and allowManualInput is false', () => { + component.editable = true; + component.property.editable = true; + component.property.value = new Date('Jul 10 2017'); + fixture.detectChanges(); + + const spy = spyOn(component.cardViewDateTimeControl, 'setValue'); + component.onDateClear(); + + expect(spy).not.toHaveBeenCalled(); + }); }); it('should be possible update a date-time', async () => { @@ -360,6 +375,581 @@ describe('CardViewDateItemComponent', () => { expect(await chips[2].getText()).toBe('Jul 12, 2017, 0:01'); }); + describe('allowManualInput', () => { + beforeEach(() => { + fixture.componentRef.setInput( + 'property', + new CardViewDateItemModel({ + label: 'Date label', + value: new Date('07/10/2017'), + key: 'dateKey', + default: '', + format: 'yyyy-MM-dd', + editable: true, + allowManualInput: true + }) + ); + fixture.componentRef.setInput('editable', true); + }); + + it('should render a visible typeable input when allowManualInput is true', () => { + fixture.detectChanges(); + + const manualInput = testingUtils.getByDataAutomationId('datepicker-manual-input-dateKey'); + expect(manualInput).not.toBeNull(); + const invisibleInput = fixture.nativeElement.querySelector('.adf-invisible-date-input'); + expect(invisibleInput).toBeNull(); + }); + + it('should NOT render the span-button when allowManualInput is true', () => { + fixture.detectChanges(); + + const spanButton = testingUtils.getByDataAutomationId('datepicker-label-toggle-dateKey'); + expect(spanButton).toBeNull(); + }); + + it('should render the invisible input and span-button when allowManualInput is false', () => { + component.property = new CardViewDateItemModel({ + label: 'Date label', + value: new Date('07/10/2017'), + key: 'dateKey', + default: '', + format: '', + editable: true, + allowManualInput: false + }); + fixture.detectChanges(); + + const invisibleInput = fixture.nativeElement.querySelector('.adf-invisible-date-input'); + expect(invisibleInput).not.toBeNull(); + const spanButton = testingUtils.getByDataAutomationId('datepicker-label-toggle-dateKey'); + expect(spanButton).not.toBeNull(); + }); + + it('should still render the datepicker toggle and picker when allowManualInput is true', () => { + fixture.detectChanges(); + + const datePickerToggle = testingUtils.getByDataAutomationId(`datepickertoggle-${component.property.key}`); + const datePicker = testingUtils.getByDataAutomationId(`datepicker-${component.property.key}`); + expect(datePickerToggle).not.toBeNull(); + expect(datePicker).not.toBeNull(); + }); + + it('should still render the clear icon when allowManualInput is true and value exists', () => { + fixture.detectChanges(); + + const clearIcon = testingUtils.getByDataAutomationId(`datepicker-date-clear-${component.property.key}`); + expect(clearIcon).not.toBeNull(); + }); + + it('should update the property when a date is changed via the picker', () => { + const cardViewUpdateService = TestBed.inject(CardViewUpdateService); + const itemUpdatedSpy = spyOn(cardViewUpdateService.itemUpdated$, 'next'); + fixture.detectChanges(); + + const expectedDate = new Date('Jul 10 2018'); + component.onDateChanged({ value: addMinutes(expectedDate, expectedDate.getTimezoneOffset()) } as MatDatetimepickerInputEvent); + + expect(itemUpdatedSpy).toHaveBeenCalled(); + }); + + it('should sync the form control value on init', () => { + fixture.detectChanges(); + + expect(component.cardViewDateTimeControl.value).not.toBeNull(); + }); + + it('should clear the form control value when onDateClear is called', () => { + fixture.detectChanges(); + + component.onDateClear(); + expect(component.cardViewDateTimeControl.value).toBeNull(); + }); + + it('should disable the form control when property.editable is false', () => { + component.property = new CardViewDateItemModel({ + label: 'Date label', + value: new Date('07/10/2017'), + key: 'dateKey', + default: '', + format: 'yyyy-MM-dd', + editable: false, + allowManualInput: true + }); + component.editable = true; + fixture.detectChanges(); + + expect(component.cardViewDateTimeControl.disabled).toBeTrue(); + }); + + it('should disable the form control when component editable is false', () => { + component.editable = false; + fixture.detectChanges(); + + expect(component.cardViewDateTimeControl.disabled).toBeTrue(); + }); + + it('should disable the form control when editable input changes to false after init', () => { + fixture.detectChanges(); + expect(component.cardViewDateTimeControl.disabled).toBeFalse(); + + fixture.componentRef.setInput('editable', false); + fixture.detectChanges(); + + expect(component.cardViewDateTimeControl.disabled).toBeTrue(); + }); + + it('should re-enable the form control when editable input changes back to true after being disabled', () => { + fixture.componentRef.setInput('editable', false); + fixture.detectChanges(); + expect(component.cardViewDateTimeControl.disabled).toBeTrue(); + + fixture.componentRef.setInput('editable', true); + fixture.detectChanges(); + + expect(component.cardViewDateTimeControl.disabled).toBeFalse(); + }); + + it('should disable the form control when property changes to non-editable after init', () => { + fixture.detectChanges(); + expect(component.cardViewDateTimeControl.disabled).toBeFalse(); + + fixture.componentRef.setInput( + 'property', + new CardViewDateItemModel({ + label: 'Date label', + value: new Date('07/10/2017'), + key: 'dateKey', + default: '', + format: 'yyyy-MM-dd', + editable: false, + allowManualInput: true + }) + ); + fixture.detectChanges(); + + expect(component.cardViewDateTimeControl.disabled).toBeTrue(); + }); + + it('should have null form control value when property has no value', () => { + component.property = new CardViewDateItemModel({ + label: 'Date label', + value: null, + key: 'dateKey', + default: '', + format: 'yyyy-MM-dd', + editable: true, + allowManualInput: true + }); + component.editable = true; + fixture.detectChanges(); + + expect(component.cardViewDateTimeControl.value).toBeNull(); + }); + + it('should render placeholder with default value when property has default', () => { + component.property = new CardViewDateItemModel({ + label: 'Date label', + value: null, + key: 'dateKey', + default: 'Select a date', + format: 'yyyy-MM-dd', + editable: true, + allowManualInput: true + }); + component.editable = true; + fixture.detectChanges(); + + const manualInput = testingUtils.getByDataAutomationId('datepicker-manual-input-dateKey'); + expect(manualInput.nativeElement.placeholder).toContain('Select a date'); + }); + + it('should render empty placeholder when property has no default', () => { + fixture.detectChanges(); + + const manualInput = testingUtils.getByDataAutomationId('datepicker-manual-input-dateKey'); + expect(manualInput.nativeElement.placeholder).toBe(''); + }); + + it('should sync form control value with valueDate when date is changed via picker', () => { + fixture.detectChanges(); + + const expectedDate = new Date('Jul 10 2018'); + component.onDateChanged({ value: addMinutes(expectedDate, expectedDate.getTimezoneOffset()) } as MatDatetimepickerInputEvent); + + expect(component.cardViewDateTimeControl.value).not.toBeNull(); + expect(component.cardViewDateTimeControl.value instanceof Date).toBeTrue(); + }); + + it('should re-sync form control value when property input is replaced after init', () => { + fixture.detectChanges(); + + const newDate = new Date('2020-03-15'); + fixture.componentRef.setInput( + 'property', + new CardViewDateItemModel({ + label: 'Date label', + value: newDate, + key: 'dateKey', + format: 'yyyy-MM-dd', + editable: true, + allowManualInput: true + }) + ); + fixture.detectChanges(); + + expect(component.cardViewDateTimeControl.value).not.toBeNull(); + expect(component.cardViewDateTimeControl.value.toDateString()).toBe(newDate.toDateString()); + }); + + it('should set form control to null when replacement property has no value', () => { + fixture.detectChanges(); + + fixture.componentRef.setInput( + 'property', + new CardViewDateItemModel({ + label: 'Date label', + value: null, + key: 'dateKey', + format: 'yyyy-MM-dd', + editable: true, + allowManualInput: true + }) + ); + fixture.detectChanges(); + + expect(component.cardViewDateTimeControl.value).toBeNull(); + }); + + it('should re-sync form control and valueDate when updateItem$ emits for matching key', () => { + fixture.detectChanges(); + const cardViewUpdateService = TestBed.inject(CardViewUpdateService); + const newDate = new Date('2021-06-01'); + + cardViewUpdateService.updateItem$.next({ key: 'dateKey', value: newDate } as any); + fixture.detectChanges(); + + expect(component.valueDate).not.toBeNull(); + expect(component.cardViewDateTimeControl.value).not.toBeNull(); + }); + + it('should not re-sync form control when updateItem$ emits for a different key', () => { + fixture.detectChanges(); + const originalControlValue = component.cardViewDateTimeControl.value; + const cardViewUpdateService = TestBed.inject(CardViewUpdateService); + + cardViewUpdateService.updateItem$.next({ key: 'otherKey', value: new Date('2021-06-01') } as any); + fixture.detectChanges(); + + expect(component.cardViewDateTimeControl.value).toEqual(originalControlValue); + }); + }); + + describe('format changes', () => { + it('should re-apply format when property.format changes with allowManualInput', () => { + component.property = new CardViewDateItemModel({ + label: 'Date label', + value: new Date('07/10/2017'), + key: 'dateKey', + format: 'dd/MM/yyyy', + editable: true, + allowManualInput: true + }); + component.editable = true; + fixture.detectChanges(); + + const datetimeAdapter = fixture.debugElement.injector.get(DatetimeAdapter) as AdfDateTimeFnsAdapter; + expect(datetimeAdapter.displayFormat).toBe('dd/MM/yyyy'); + + component.property.format = 'yyyy-MM-dd'; + fixture.detectChanges(); + + expect(datetimeAdapter.displayFormat).toBe('yyyy-MM-dd'); + }); + + it('should not re-apply format when property.format has not changed', () => { + component.property = new CardViewDateItemModel({ + label: 'Date label', + value: new Date('07/10/2017'), + key: 'dateKey', + format: 'dd/MM/yyyy', + editable: true, + allowManualInput: true + }); + component.editable = true; + fixture.detectChanges(); + + const datetimeAdapter = fixture.debugElement.injector.get(DatetimeAdapter) as AdfDateTimeFnsAdapter; + const formatBefore = datetimeAdapter.displayFormat; + fixture.detectChanges(); + + expect(datetimeAdapter.displayFormat).toBe(formatBefore); + }); + + it('should reset adapters to defaults when custom format is removed in manual input mode', () => { + component.property = new CardViewDateItemModel({ + label: 'Date label', + value: new Date('07/10/2017'), + key: 'dateKey', + format: 'dd/MM/yyyy', + editable: true, + allowManualInput: true + }); + component.editable = true; + fixture.detectChanges(); + + const datetimeAdapter = fixture.debugElement.injector.get(DatetimeAdapter) as AdfDateTimeFnsAdapter; + const dateAdapter = fixture.debugElement.injector.get(DateAdapter) as AdfDateFnsAdapter; + expect(datetimeAdapter.displayFormat).toBe('dd/MM/yyyy'); + expect(dateAdapter.displayFormat).toBe('dd/MM/yyyy'); + + component.property.format = ''; + fixture.detectChanges(); + + expect(datetimeAdapter.displayFormat).toBeNull(); + expect(dateAdapter.displayFormat).toBe('MMM dd'); + }); + + it('should reset datetime adapter when custom format is removed for datetime property in manual input mode', () => { + component.property = new CardViewDatetimeItemModel({ + label: 'Datetime label', + value: new Date('07/10/2017 10:15'), + key: 'datetimeKey', + format: 'yyyy-MM-dd HH:mm', + editable: true, + allowManualInput: true + }); + component.editable = true; + fixture.detectChanges(); + + const datetimeAdapter = fixture.debugElement.injector.get(DatetimeAdapter) as AdfDateTimeFnsAdapter; + expect(datetimeAdapter.displayFormat).toBe('yyyy-MM-dd HH:mm'); + + component.property.format = ''; + fixture.detectChanges(); + + expect(datetimeAdapter.displayFormat).toBeNull(); + }); + + it('should fallback to adapter defaults when allowManualInput is false', () => { + component.property = new CardViewDateItemModel({ + label: 'Date label', + value: new Date('07/10/2017'), + key: 'dateKey', + format: 'YYYY-MM-dd', + editable: true, + allowManualInput: false + }); + component.editable = true; + fixture.detectChanges(); + + const dateAdapter = fixture.debugElement.injector.get(DateAdapter) as AdfDateFnsAdapter; + const datetimeAdapter = fixture.debugElement.injector.get(DatetimeAdapter) as AdfDateTimeFnsAdapter; + + expect(dateAdapter.displayFormat).toBe('MMM dd'); + expect(datetimeAdapter.displayFormat).toBeNull(); + }); + + it('should re-set form control value when format changes and control has value', () => { + component.property = new CardViewDateItemModel({ + label: 'Date label', + value: new Date('07/10/2017'), + key: 'dateKey', + format: 'dd/MM/yyyy', + editable: true, + allowManualInput: true + }); + component.editable = true; + fixture.detectChanges(); + + const spy = spyOn(component.cardViewDateTimeControl, 'setValue'); + component.property.format = 'yyyy-MM-dd'; + fixture.detectChanges(); + + expect(spy).toHaveBeenCalled(); + }); + + it('should not re-set form control value when format changes but allowManualInput is false', () => { + component.property = new CardViewDateItemModel({ + label: 'Date label', + value: new Date('07/10/2017'), + key: 'dateKey', + format: 'dd/MM/yyyy', + editable: true, + allowManualInput: false + }); + component.editable = true; + fixture.detectChanges(); + + const spy = spyOn(component.cardViewDateTimeControl, 'setValue'); + component.property.format = 'yyyy-MM-dd'; + fixture.detectChanges(); + + expect(spy).not.toHaveBeenCalled(); + }); + }); + + describe('CardViewDateItemModel allowManualInput', () => { + it('should default allowManualInput to false when not specified', () => { + const model = new CardViewDateItemModel({ + label: 'Date label', + value: new Date(), + key: 'dateKey' + }); + expect(model.allowManualInput).toBeFalse(); + }); + + it('should set allowManualInput to true when specified', () => { + const model = new CardViewDateItemModel({ + label: 'Date label', + value: new Date(), + key: 'dateKey', + allowManualInput: true + }); + expect(model.allowManualInput).toBeTrue(); + }); + + it('should keep allowManualInput false when explicitly set to false', () => { + const model = new CardViewDateItemModel({ + label: 'Date label', + value: new Date(), + key: 'dateKey', + allowManualInput: false + }); + expect(model.allowManualInput).toBeFalse(); + }); + + it('should emit when format changes', () => { + const model = new CardViewDateItemModel({ + label: 'Date label', + value: new Date(), + key: 'dateKey', + format: 'dd/MM/yyyy' + }); + const formatChangeSpy = jasmine.createSpy('formatChangeSpy'); + model.formatChanges$.subscribe(formatChangeSpy); + + model.format = 'yyyy-MM-dd'; + + expect(formatChangeSpy).toHaveBeenCalledOnceWith('yyyy-MM-dd'); + }); + + it('should not emit when format is assigned the same value', () => { + const model = new CardViewDateItemModel({ + label: 'Date label', + value: new Date(), + key: 'dateKey', + format: 'dd/MM/yyyy' + }); + const formatChangeSpy = jasmine.createSpy('formatChangeSpy'); + model.formatChanges$.subscribe(formatChangeSpy); + + model.format = 'dd/MM/yyyy'; + + expect(formatChangeSpy).not.toHaveBeenCalled(); + }); + }); + + describe('property input replacement', () => { + it('should re-sync valueDate when property input is replaced after init', () => { + fixture.componentRef.setInput( + 'property', + new CardViewDateItemModel({ + label: 'Date label', + value: new Date('07/10/2017'), + key: 'dateKey', + format: '', + editable: true + }) + ); + fixture.detectChanges(); + + const newDate = new Date('2022-11-20'); + fixture.componentRef.setInput( + 'property', + new CardViewDateItemModel({ + label: 'Date label', + value: newDate, + key: 'dateKey', + format: '', + editable: true + }) + ); + fixture.detectChanges(); + + expect(component.valueDate).not.toBeNull(); + expect(component.valueDate instanceof Date).toBeTrue(); + }); + + it('should set valueDate to null when replacement property has no value', () => { + fixture.componentRef.setInput( + 'property', + new CardViewDateItemModel({ + label: 'Date label', + value: new Date('07/10/2017'), + key: 'dateKey', + format: '', + editable: true + }) + ); + fixture.detectChanges(); + + fixture.componentRef.setInput( + 'property', + new CardViewDateItemModel({ + label: 'Date label', + value: null, + key: 'dateKey', + format: '', + editable: true + }) + ); + fixture.detectChanges(); + + expect(component.valueDate).toBeNull(); + }); + }); + + describe('updateItem$ re-sync for multivalued', () => { + it('should re-sync valueDate when updateItem$ emits for a multivalued property', () => { + component.property = new CardViewDateItemModel({ + label: 'Date label', + value: ['2020-01-01', '2020-02-01'], + key: 'dateKey', + editable: true, + multivalued: true + }); + fixture.detectChanges(); + const cardViewUpdateService = TestBed.inject(CardViewUpdateService); + const newDates = [new Date('2021-06-01'), new Date('2021-07-01')]; + + component.property.value = newDates; + cardViewUpdateService.updateItem$.next({ key: 'dateKey', value: newDates } as any); + fixture.detectChanges(); + + expect(component.valueDate).not.toBeNull(); + expect(component.valueDate instanceof Date).toBeTrue(); + }); + + it('should not update valueDate when updateItem$ emits for a different key on multivalued property', () => { + component.property = new CardViewDateItemModel({ + label: 'Date label', + value: ['2020-01-01'], + key: 'dateKey', + editable: true, + multivalued: true + }); + fixture.detectChanges(); + const originalValueDate = component.valueDate; + const cardViewUpdateService = TestBed.inject(CardViewUpdateService); + + cardViewUpdateService.updateItem$.next({ key: 'otherKey', value: [new Date('2021-06-01')] } as any); + fixture.detectChanges(); + + expect(component.valueDate).toEqual(originalValueDate); + }); + }); + describe('FloatLabel behavior', () => { it('should set floatLabel to "always" when property has default value and is editable', async () => { component.property = new CardViewDateItemModel({ diff --git a/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.ts b/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.ts index 4f77c1e698..3a3fb6cab4 100644 --- a/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.ts +++ b/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { Component, effect, Input, OnInit, ViewChild, ViewEncapsulation, inject } from '@angular/core'; +import { Component, OnChanges, effect, Input, OnInit, SimpleChanges, ViewChild, ViewEncapsulation, inject, DestroyRef } from '@angular/core'; import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core'; import { DatetimeAdapter, @@ -42,6 +42,10 @@ import { MatSnackBarModule } from '@angular/material/snack-bar'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { MatInputModule } from '@angular/material/input'; import { IconModule } from '../../../icon/icon.module'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { BehaviorSubject, EMPTY, filter, switchMap } from 'rxjs'; + +const DEFAULT_DATE_FORMAT = 'MMM DD'; @Component({ providers: [ @@ -68,11 +72,13 @@ import { IconModule } from '../../../icon/icon.module'; encapsulation: ViewEncapsulation.None, host: { class: 'adf-card-view-dateitem' } }) -export class CardViewDateItemComponent extends BaseCardView implements OnInit { +export class CardViewDateItemComponent extends BaseCardView implements OnInit, OnChanges { private readonly dateAdapter = inject>(DateAdapter); + private readonly datetimeAdapter = inject>(DatetimeAdapter); private readonly userPreferencesService = inject(UserPreferencesService); private readonly clipboardService = inject(ClipboardService); private readonly translateService = inject(TranslationService); + private readonly destroyRef = inject(DestroyRef); @Input() displayEmpty = true; @@ -87,6 +93,8 @@ export class CardViewDateItemComponent extends BaseCardView = new FormControl(null); + private readonly property$ = new BehaviorSubject(undefined); + constructor() { super(); // Use effect to react to locale signal changes (must be in injection context) @@ -95,8 +103,49 @@ export class CardViewDateItemComponent extends BaseCardView prop?.formatChanges$ ?? EMPTY), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(() => this.handleFormatChange()); + + this.cardViewUpdateService.updateItem$ + .pipe( + filter((itemModel) => itemModel.key === this.property.key), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(() => { + if (!this.property.multivalued) { + this.initSingleValueProperty(); + } else { + this.initMultivaluedProperty(); + } + }); + + this.applyFormat(); if (this.property.multivalued) { this.initMultivaluedProperty(); @@ -105,6 +154,16 @@ export class CardViewDateItemComponent extends BaseCardView { + let properties: CardViewDateItemProperties; + + beforeEach(() => { + properties = { + label: 'Date label', + value: new Date('2025-03-07T15:30:00.000Z'), + key: 'dateKey' + }; + }); + + it('should initialize with date type and default allowManualInput', () => { + const itemModel = new CardViewDateItemModel(properties); + + expect(itemModel.type).toBe('date'); + expect(itemModel.allowManualInput).toBeFalse(); + }); + + it('should initialize format, locale and allowManualInput from properties', () => { + const itemModel = new CardViewDateItemModel({ + ...properties, + format: 'fullDate', + locale: 'fr', + allowManualInput: true + }); + + expect(itemModel.format).toBe('fullDate'); + expect(itemModel.locale).toBe('fr'); + expect(itemModel.allowManualInput).toBeTrue(); + }); + + describe('displayValue', () => { + it('should prepare and transform a single date value', () => { + const itemModel = new CardViewDateItemModel(properties); + const preparedDate = new Date('2025-03-07T00:00:00.000'); + + spyOn(DateFnsUtils, 'forceLocal').and.returnValue(preparedDate); + spyOn(itemModel, 'transformDate').and.returnValue('formatted-date'); + + expect(itemModel.displayValue).toBe('formatted-date'); + expect(DateFnsUtils.forceLocal).toHaveBeenCalledOnceWith(properties.value as Date); + expect(itemModel.transformDate).toHaveBeenCalledOnceWith(preparedDate); + }); + + it('should prepare and transform each value when multivalued', () => { + const firstDate = new Date('2025-03-07T15:30:00.000Z'); + const secondDate = new Date('2025-03-08T15:30:00.000Z'); + const itemModel = new CardViewDateItemModel({ + ...properties, + multivalued: true, + value: [firstDate, secondDate] + }); + + spyOn(DateFnsUtils, 'forceLocal').and.callFake((date) => date as Date); + spyOn(itemModel, 'transformDate').and.callFake((date) => `${(date as Date).toISOString()}-formatted`); + + expect(itemModel.displayValue).toEqual([`${firstDate.toISOString()}-formatted`, `${secondDate.toISOString()}-formatted`]); + expect(DateFnsUtils.forceLocal).toHaveBeenCalledTimes(2); + }); + + it('should return the default value when the single value is not present', () => { + const itemModel = new CardViewDateItemModel({ + ...properties, + value: null, + default: 'No date selected' + }); + + expect(itemModel.displayValue).toBe('No date selected'); + }); + + it('should return the default value as an array when the multivalued value is not present', () => { + const itemModel = new CardViewDateItemModel({ + ...properties, + multivalued: true, + value: null, + default: 'No dates selected' + }); + + expect(itemModel.displayValue).toEqual(['No dates selected']); + }); + + it('should return an empty array when the multivalued value and default are not present', () => { + const itemModel = new CardViewDateItemModel({ + ...properties, + multivalued: true, + value: null + }); + + expect(itemModel.displayValue).toEqual([]); + }); + }); + + describe('transformDate', () => { + it('should use the provided format and locale', () => { + const value = new Date('2025-03-07T15:30:00.000Z'); + const itemModel = new CardViewDateItemModel({ + ...properties, + format: 'fullDate', + locale: 'fr' + }); + const expectedValue = new DatePipe('fr').transform(value, 'fullDate'); + + expect(itemModel.transformDate(value)).toBe(expectedValue as string); + }); + + it('should use mediumDate and en-US when format and locale are not provided', () => { + const value = new Date('2025-03-07T15:30:00.000Z'); + const itemModel = new CardViewDateItemModel(properties); + const expectedValue = new DatePipe('en-US').transform(value, 'mediumDate'); + + expect(itemModel.transformDate(value)).toBe(expectedValue as string); + }); + }); + + describe('formatChanges$', () => { + it('should emit when format changes', () => { + const itemModel = new CardViewDateItemModel({ + ...properties, + format: 'dd/MM/yyyy' + }); + const formatChangeSpy = jasmine.createSpy('formatChangeSpy'); + + itemModel.formatChanges$.subscribe(formatChangeSpy); + itemModel.format = 'yyyy-MM-dd'; + + expect(formatChangeSpy).toHaveBeenCalledOnceWith('yyyy-MM-dd'); + }); + + it('should not emit when the same format is assigned', () => { + const itemModel = new CardViewDateItemModel({ + ...properties, + format: 'dd/MM/yyyy' + }); + const formatChangeSpy = jasmine.createSpy('formatChangeSpy'); + + itemModel.formatChanges$.subscribe(formatChangeSpy); + itemModel.format = 'dd/MM/yyyy'; + + expect(formatChangeSpy).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/lib/core/src/lib/card-view/models/card-view-dateitem.model.ts b/lib/core/src/lib/card-view/models/card-view-dateitem.model.ts index 398626a0dd..e1d7566e88 100644 --- a/lib/core/src/lib/card-view/models/card-view-dateitem.model.ts +++ b/lib/core/src/lib/card-view/models/card-view-dateitem.model.ts @@ -21,13 +21,29 @@ import { CardViewBaseItemModel } from './card-view-baseitem.model'; import { CardViewDateItemProperties } from '../interfaces/card-view.interfaces'; import { DatePipe } from '@angular/common'; import { DateFnsUtils } from '../../common/utils/date-fns-utils'; +import { Subject } from 'rxjs'; type DateItemType = Date | Date[] | null; export class CardViewDateItemModel extends CardViewBaseItemModel implements CardViewItem, DynamicComponentModel { type = 'date'; - format: string; locale: string; + allowManualInput = false; + + private readonly formatChangesSubject = new Subject(); + readonly formatChanges$ = this.formatChangesSubject.asObservable(); + private _format: string; + + get format(): string { + return this._format; + } + + set format(value: string) { + if (this._format !== value) { + this._format = value; + this.formatChangesSubject.next(value); + } + } constructor(cardViewDateItemProperties: CardViewDateItemProperties) { super(cardViewDateItemProperties); @@ -39,6 +55,10 @@ export class CardViewDateItemModel extends CardViewBaseItemModel i if (cardViewDateItemProperties.locale) { this.locale = cardViewDateItemProperties.locale; } + + if (cardViewDateItemProperties.allowManualInput !== undefined) { + this.allowManualInput = cardViewDateItemProperties.allowManualInput; + } } get displayValue(): string | string[] { diff --git a/lib/core/src/lib/card-view/models/card-view-datetimeitem.model.spec.ts b/lib/core/src/lib/card-view/models/card-view-datetimeitem.model.spec.ts new file mode 100644 index 0000000000..b29637a65f --- /dev/null +++ b/lib/core/src/lib/card-view/models/card-view-datetimeitem.model.spec.ts @@ -0,0 +1,59 @@ +/*! + * @license + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CardViewDatetimeItemModel } from './card-view-datetimeitem.model'; +import { CardViewDateItemProperties } from '../interfaces/card-view.interfaces'; +import { DateFnsUtils } from '../../common/utils/date-fns-utils'; + +describe('CardViewDatetimeItemModel', () => { + let properties: CardViewDateItemProperties; + + beforeEach(() => { + properties = { + label: 'Datetime label', + value: new Date('2025-03-07T15:30:00.000Z'), + key: 'datetimeKey' + }; + }); + + it('should initialize with datetime type and default datetime format', () => { + const itemModel = new CardViewDatetimeItemModel(properties); + + expect(itemModel.type).toBe('datetime'); + expect(itemModel.format).toBe('MMM d, y, H:mm'); + }); + + it('should preserve the provided format', () => { + const itemModel = new CardViewDatetimeItemModel({ + ...properties, + format: 'yyyy-MM-dd HH:mm' + }); + + expect(itemModel.format).toBe('yyyy-MM-dd HH:mm'); + }); + + it('should transform the original date value without forcing it to local midnight', () => { + const itemModel = new CardViewDatetimeItemModel(properties); + + spyOn(DateFnsUtils, 'forceLocal'); + spyOn(itemModel, 'transformDate').and.returnValue('formatted-datetime'); + + expect(itemModel.displayValue).toBe('formatted-datetime'); + expect(DateFnsUtils.forceLocal).not.toHaveBeenCalled(); + expect(itemModel.transformDate).toHaveBeenCalledOnceWith(properties.value as Date); + }); +}); diff --git a/lib/core/src/lib/card-view/models/card-view-datetimeitem.model.ts b/lib/core/src/lib/card-view/models/card-view-datetimeitem.model.ts index 9f9a3b7511..fa04db2551 100644 --- a/lib/core/src/lib/card-view/models/card-view-datetimeitem.model.ts +++ b/lib/core/src/lib/card-view/models/card-view-datetimeitem.model.ts @@ -22,13 +22,11 @@ import { CardViewDateItemProperties } from '../interfaces/card-view.interfaces'; export class CardViewDatetimeItemModel extends CardViewDateItemModel implements CardViewItem, DynamicComponentModel { type = 'datetime'; - format = 'MMM d, y, H:mm'; constructor(cardViewDateItemProperties: CardViewDateItemProperties) { super(cardViewDateItemProperties); - - if (cardViewDateItemProperties.format) { - this.format = cardViewDateItemProperties.format; + if (!cardViewDateItemProperties.format) { + this.format = 'MMM d, y, H:mm'; } } } diff --git a/lib/core/src/lib/common/utils/datetime-fns-adapter.ts b/lib/core/src/lib/common/utils/datetime-fns-adapter.ts index a9e7610d5d..0afaebed0f 100644 --- a/lib/core/src/lib/common/utils/datetime-fns-adapter.ts +++ b/lib/core/src/lib/common/utils/datetime-fns-adapter.ts @@ -134,7 +134,7 @@ export class AdfDateTimeFnsAdapter extends DatetimeAdapter { override format(date: Date, displayFormat: any): string { displayFormat = DateFnsUtils.convertMomentToDateFnsFormat(displayFormat); - if (this.displayFormat && displayFormat === this.formats?.display?.datetimeInput) { + if (this.displayFormat && (displayFormat === this.formats?.display?.datetimeInput || displayFormat === this.formats?.display?.dateInput)) { return this._delegate.format(date, this.displayFormat || displayFormat); }