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
This commit is contained in:
Darren Thornton
2026-05-20 14:19:26 +05:30
committed by Anamika Dey
parent fe403dce39
commit 62512bde49
9 changed files with 1002 additions and 66 deletions
@@ -10,66 +10,78 @@
>
{{ property.label | translate }}
</mat-label>
<div class="adf-dateitem-editable-controls">
<span
class="adf-datepicker-span-button"
[attr.data-automation-id]="'datepicker-label-toggle-' + property.key"
(click)="showDatePicker()"
(dblclick)="copyToClipboard(property.displayValue)"
tabindex="0"
role="button"
(keyup.enter)="showDatePicker()"
>
<span [attr.data-automation-id]="'card-' + property.type + '-value-' + property.key">
@if (showProperty) {
{{ property.displayValue }}
} @else {
{{ property.default | translate }}
}
@if (property.allowManualInput) {
<input
matInput
[matDatetimepicker]="datetimePicker"
[formControl]="cardViewDateTimeControl"
(dateChange)="onDateChanged($event)"
[attr.id]="'card-view-dateitem-' + property.key"
[attr.data-automation-id]="'datepicker-manual-input-' + property.key"
[placeholder]="property.default ? (property.default | translate) : ''"
/>
} @else {
<div class="adf-dateitem-editable-controls">
<span
class="adf-datepicker-span-button"
[attr.data-automation-id]="'datepicker-label-toggle-' + property.key"
(click)="showDatePicker()"
(dblclick)="copyToClipboard(property.displayValue)"
tabindex="0"
role="button"
(keyup.enter)="showDatePicker()"
>
<span [attr.data-automation-id]="'card-' + property.type + '-value-' + property.key">
@if (showProperty) {
{{ property.displayValue }}
} @else {
{{ property.default | translate }}
}
</span>
</span>
</span>
</div>
<input
matInput
[disabled]="isReadonlyProperty || !editable"
class="adf-invisible-date-input"
[attr.tabIndex]="-1"
[matDatetimepicker]="datetimePicker"
[value]="valueDate"
(dateChange)="onDateChanged($event)"
[attr.id]="'card-view-dateitem-' + property.key"
/>
@if (showClearAction) {
<mat-icon
matIconSuffix
class="adf-date-reset-icon"
[ngClass]="{ 'cdk-visually-hidden': !editable || isReadonlyProperty}"
(click)="onDateClear()"
[attr.title]="'CORE.METADATA.ACTIONS.CLEAR' | translate"
[attr.data-automation-id]="'datepicker-date-clear-' + property.key"
adf-icon="clear"
/>
}
<mat-datetimepicker-toggle
matIconSuffix
[attr.tabindex]="-1"
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.data-automation-id]="'datepickertoggle-' + property.key"
[for]="datetimePicker"
class="adf-dateitem-picker-toggle"
[ngClass]="{ 'cdk-visually-hidden': !editable || isReadonlyProperty}"
/>
<mat-datetimepicker
#datetimePicker
[type]="$any(property).type"
[timeInterval]="5"
</div>
<input
matInput
[disabled]="isReadonlyProperty || !editable"
[attr.data-automation-id]="'datepicker-' + property.key"
[startAt]="valueDate"
[ngClass]="{ 'cdk-visually-hidden': !editable || isReadonlyProperty}"
class="adf-invisible-date-input"
[attr.tabIndex]="-1"
[matDatetimepicker]="datetimePicker"
[value]="valueDate"
(dateChange)="onDateChanged($event)"
[attr.id]="'card-view-dateitem-' + property.key"
/>
}
@if (showClearAction) {
<mat-icon
matIconSuffix
class="adf-date-reset-icon"
[ngClass]="{ 'cdk-visually-hidden': !editable || isReadonlyProperty}"
(click)="onDateClear()"
[attr.title]="'CORE.METADATA.ACTIONS.CLEAR' | translate"
[attr.data-automation-id]="'datepicker-date-clear-' + property.key"
adf-icon="clear"
/>
}
<mat-datetimepicker-toggle
matIconSuffix
[attr.tabindex]="-1"
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.data-automation-id]="'datepickertoggle-' + property.key"
[for]="datetimePicker"
class="adf-dateitem-picker-toggle"
[ngClass]="{ 'cdk-visually-hidden': !editable || isReadonlyProperty}"
/>
<mat-datetimepicker
#datetimePicker
[type]="$any(property).type"
[timeInterval]="5"
[disabled]="isReadonlyProperty || !editable"
[attr.data-automation-id]="'datepicker-' + property.key"
[startAt]="valueDate"
[ngClass]="{ 'cdk-visually-hidden': !editable || isReadonlyProperty}"
/>
</mat-form-field>
} @else {
<mat-form-field class="adf-property-field adf-dateitem-editable" [floatLabel]="property.default ? 'always' : null">
@@ -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<Date>);
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<Date>);
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({
@@ -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<CardViewDateItemModel> implements OnInit {
export class CardViewDateItemComponent extends BaseCardView<CardViewDateItemModel> implements OnInit, OnChanges {
private readonly dateAdapter = inject<DateAdapter<Date>>(DateAdapter);
private readonly datetimeAdapter = inject<DatetimeAdapter<Date>>(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<CardViewDateItemMode
cardViewDateTimeControl: FormControl<Date> = new FormControl<Date>(null);
private readonly property$ = new BehaviorSubject<CardViewDateItemModel | undefined>(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<CardViewDateItemMode
});
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.property) {
this.property$.next(this.property);
if (!changes.property.firstChange) {
this.handleFormatChange();
this.syncControlDisabledState();
if (!this.property.multivalued) {
this.initSingleValueProperty();
} else {
this.initMultivaluedProperty();
}
}
}
if (changes.editable && !changes.editable.firstChange) {
this.syncControlDisabledState();
}
}
ngOnInit() {
(this.dateAdapter as AdfDateFnsAdapter).displayFormat = 'MMM DD';
this.property$.next(this.property);
this.property$
.pipe(
switchMap((prop) => 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<CardViewDateItemMode
}
}
private applyFormat(): void {
if (this.property.allowManualInput && this.property.format) {
(this.dateAdapter as AdfDateFnsAdapter).displayFormat = this.property.format;
(this.datetimeAdapter as AdfDateTimeFnsAdapter).displayFormat = this.property.format;
} else {
(this.dateAdapter as AdfDateFnsAdapter).displayFormat = DEFAULT_DATE_FORMAT;
(this.datetimeAdapter as AdfDateTimeFnsAdapter).displayFormat = null;
}
}
get showProperty(): boolean {
return this.displayEmpty || !this.property.isEmpty();
}
@@ -126,6 +185,9 @@ export class CardViewDateItemComponent extends BaseCardView<CardViewDateItemMode
this.property.value = DateFnsUtils.forceUtc(event.value);
this.valueDate = DateFnsUtils.forceLocal(event.value);
}
if (this.property.allowManualInput) {
this.cardViewDateTimeControl.setValue(this.valueDate, { emitEvent: false });
}
this.update();
}
}
@@ -133,6 +195,9 @@ export class CardViewDateItemComponent extends BaseCardView<CardViewDateItemMode
onDateClear() {
this.valueDate = null;
if (this.property.allowManualInput) {
this.cardViewDateTimeControl.setValue(null, { emitEvent: false });
}
this.cardViewUpdateService.update({ ...this.property } as CardViewDateItemModel, null);
this.property.value = null;
this.property.default = null;
@@ -169,12 +234,36 @@ export class CardViewDateItemComponent extends BaseCardView<CardViewDateItemMode
this.cardViewUpdateService.update({ ...this.property } as CardViewDateItemModel, this.property.value);
}
private syncControlDisabledState(): void {
if (!this.property.allowManualInput) {
return;
}
if (!this.isEditable) {
this.cardViewDateTimeControl.disable({ emitEvent: false });
} else {
this.cardViewDateTimeControl.enable({ emitEvent: false });
}
}
private handleFormatChange(): void {
this.applyFormat();
if (this.property.allowManualInput && this.cardViewDateTimeControl.value !== null) {
this.cardViewDateTimeControl.setValue(this.cardViewDateTimeControl.value, { emitEvent: false });
}
}
private initSingleValueProperty() {
this.valueDate = null;
if (this.property.value && !Array.isArray(this.property.value)) {
const date = new Date(this.property.value);
this.property.value = date;
this.valueDate = this.property.type === 'date' ? DateFnsUtils.forceLocal(date) : date;
}
if (this.property.allowManualInput) {
this.cardViewDateTimeControl.setValue(this.valueDate ?? null);
this.syncControlDisabledState();
}
}
private initMultivaluedProperty() {
@@ -20,4 +20,5 @@ import { CardViewItemProperties } from './card-view-item-properties.interface';
export interface CardViewDateItemProperties extends CardViewItemProperties {
format?: string;
locale?: string;
allowManualInput?: boolean;
}
@@ -0,0 +1,167 @@
/*!
* @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 { DatePipe, registerLocaleData } from '@angular/common';
import localeFr from '@angular/common/locales/fr';
import { CardViewDateItemModel } from './card-view-dateitem.model';
import { CardViewDateItemProperties } from '../interfaces/card-view.interfaces';
import { DateFnsUtils } from '../../common/utils/date-fns-utils';
registerLocaleData(localeFr);
describe('CardViewDateItemModel', () => {
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();
});
});
});
@@ -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<DateItemType> implements CardViewItem, DynamicComponentModel {
type = 'date';
format: string;
locale: string;
allowManualInput = false;
private readonly formatChangesSubject = new Subject<string>();
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<DateItemType> i
if (cardViewDateItemProperties.locale) {
this.locale = cardViewDateItemProperties.locale;
}
if (cardViewDateItemProperties.allowManualInput !== undefined) {
this.allowManualInput = cardViewDateItemProperties.allowManualInput;
}
}
get displayValue(): string | string[] {
@@ -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);
});
});
@@ -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';
}
}
}
@@ -134,7 +134,7 @@ export class AdfDateTimeFnsAdapter extends DatetimeAdapter<Date> {
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);
}