Skip to content

Commit bb812fe

Browse files
committed
binary
1 parent 6cac9f9 commit bb812fe

13 files changed

Lines changed: 520 additions & 111 deletions

frontend/src/app/components/ui-components/filter-fields/date-time/date-time.component.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,9 @@
1919
grid-template-columns: 1fr 1fr;
2020
grid-column-gap: 16px;
2121
}
22+
23+
.between-fields {
24+
display: flex;
25+
flex-direction: column;
26+
gap: 8px;
27+
}

frontend/src/app/components/ui-components/filter-fields/date-time/date-time.component.html

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,55 @@
1111
<mat-option value="lt">before</mat-option>
1212
<mat-option value="gte">on or after</mat-option>
1313
<mat-option value="lte">on or before</mat-option>
14+
<mat-option value="between">between</mat-option>
15+
<mat-option value="in">in</mat-option>
1416
</mat-select>
1517
</mat-form-field>
1618

17-
@if (!isPresetMode()) {
19+
@if (filterMode === 'between') {
20+
<div class="value-field between-fields">
21+
<div class="field-couple">
22+
<mat-form-field appearance="outline">
23+
<mat-label>{{normalizedLabel()}} (from date)</mat-label>
24+
<input type="date" matInput name="{{label()}}-{{key()}}-lower-date"
25+
[required]="required()" [disabled]="disabled()"
26+
[(ngModel)]="lowerDate" (change)="onBetweenLowerChange()">
27+
</mat-form-field>
28+
29+
<mat-form-field appearance="outline">
30+
<mat-label>(from time)</mat-label>
31+
<input type="time" matInput name="{{label()}}-{{key()}}-lower-time"
32+
[required]="required()" [disabled]="disabled()"
33+
[(ngModel)]="lowerTime" (change)="onBetweenLowerChange()">
34+
</mat-form-field>
35+
</div>
36+
37+
<div class="field-couple">
38+
<mat-form-field appearance="outline">
39+
<mat-label>{{normalizedLabel()}} (to date)</mat-label>
40+
<input type="date" matInput name="{{label()}}-{{key()}}-upper-date"
41+
[required]="required()" [disabled]="disabled()"
42+
[(ngModel)]="upperDate" (change)="onBetweenUpperChange()">
43+
</mat-form-field>
44+
45+
<mat-form-field appearance="outline">
46+
<mat-label>(to time)</mat-label>
47+
<input type="time" matInput name="{{label()}}-{{key()}}-upper-time"
48+
[required]="required()" [disabled]="disabled()"
49+
[(ngModel)]="upperTime" (change)="onBetweenUpperChange()">
50+
</mat-form-field>
51+
</div>
52+
</div>
53+
} @else if (filterMode === 'in') {
54+
<mat-form-field class="value-field" appearance="outline">
55+
<mat-label>{{normalizedLabel()}} (comma-separated ISO datetimes)</mat-label>
56+
<input matInput name="{{label()}}-{{key()}}-in"
57+
#inputElement
58+
placeholder="2024-01-01T00:00:00Z, 2024-02-01T00:00:00Z"
59+
[required]="required()" [disabled]="disabled()"
60+
[(ngModel)]="inValueText" (ngModelChange)="onInTextChange($event)">
61+
</mat-form-field>
62+
} @else if (!isPresetMode()) {
1863
<div class="value-field field-couple">
1964
<mat-form-field appearance="outline">
2065
<mat-label>{{normalizedLabel()}} (date)</mat-label>

frontend/src/app/components/ui-components/filter-fields/date-time/date-time.component.spec.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,62 @@ describe('DateTimeFilterComponent', () => {
127127
expect(component.time).toEqual('00:00:00');
128128
expect(fieldEvent).toHaveBeenCalledWith('2021-08-26T00:00:00Z');
129129
});
130+
131+
it('should emit between comparator with [lower, upper] ISO strings when BETWEEN bounds change', () => {
132+
const fieldEvent = vi.spyOn(component.onFieldChange, 'emit');
133+
const comparatorEvent = vi.spyOn(component.onComparatorChange, 'emit');
134+
component.onFilterModeChange('between');
135+
136+
component.lowerDate = '2024-01-01';
137+
component.lowerTime = '00:00:00';
138+
component.onBetweenLowerChange();
139+
140+
expect(comparatorEvent).toHaveBeenCalledWith('between');
141+
expect(fieldEvent).toHaveBeenLastCalledWith(['2024-01-01T00:00:00Z', null]);
142+
143+
component.upperDate = '2024-01-31';
144+
component.upperTime = '23:59:59';
145+
component.onBetweenUpperChange();
146+
147+
expect(fieldEvent).toHaveBeenLastCalledWith(['2024-01-01T00:00:00Z', '2024-01-31T23:59:59Z']);
148+
});
149+
150+
it('should restore BETWEEN bounds from a two-element array on init', () => {
151+
component.value = ['2024-01-01T00:00:00Z', '2024-01-31T23:59:59Z'];
152+
component.filterMode = 'between';
153+
component.ngOnInit();
154+
155+
expect(component.lowerDate).toEqual('2024-01-01');
156+
expect(component.lowerTime).toEqual('00:00:00');
157+
expect(component.upperDate).toEqual('2024-01-31');
158+
expect(component.upperTime).toEqual('23:59:59');
159+
});
160+
161+
it('should parse comma-separated text into array on IN text change', () => {
162+
const fieldEvent = vi.spyOn(component.onFieldChange, 'emit');
163+
const comparatorEvent = vi.spyOn(component.onComparatorChange, 'emit');
164+
component.onFilterModeChange('in');
165+
166+
component.onInTextChange('2024-01-01T00:00:00Z, 2024-02-01T00:00:00Z');
167+
168+
expect(comparatorEvent).toHaveBeenCalledWith('in');
169+
expect(fieldEvent).toHaveBeenLastCalledWith(['2024-01-01T00:00:00Z', '2024-02-01T00:00:00Z']);
170+
});
171+
172+
it('should emit undefined when IN text is empty', () => {
173+
const fieldEvent = vi.spyOn(component.onFieldChange, 'emit');
174+
component.onFilterModeChange('in');
175+
176+
component.onInTextChange(' ');
177+
178+
expect(fieldEvent).toHaveBeenLastCalledWith(undefined);
179+
});
180+
181+
it('should restore IN text from array value on init', () => {
182+
component.value = ['2024-01-01T00:00:00Z', '2024-02-01T00:00:00Z'];
183+
component.filterMode = 'in';
184+
component.ngOnInit();
185+
186+
expect(component.inValueText).toBe('2024-01-01T00:00:00Z, 2024-02-01T00:00:00Z');
187+
});
130188
});

frontend/src/app/components/ui-components/filter-fields/date-time/date-time.component.ts

Lines changed: 92 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,34 @@ import { BaseFilterFieldComponent } from '../base-filter-field/base-filter-field
1414
imports: [CommonModule, FormsModule, MatFormFieldModule, MatInputModule, MatSelectModule],
1515
})
1616
export class DateTimeFilterComponent extends BaseFilterFieldComponent implements OnInit, AfterViewInit {
17-
@Input() value: string;
17+
@Input() value: string | string[];
1818
@ViewChild('inputElement') inputElement: ElementRef<HTMLInputElement>;
1919

2020
public filterMode: string = 'last_day';
2121
public date: string;
2222
public time: string;
2323

24+
public lowerDate: string;
25+
public lowerTime: string;
26+
public upperDate: string;
27+
public upperTime: string;
28+
29+
public inValueText: string = '';
30+
2431
private _presetModes = ['last_hour', 'last_day', 'last_week', 'last_month', 'last_year'];
2532

2633
ngOnInit(): void {
27-
if (this.value) {
34+
if (this.filterMode === 'between' && Array.isArray(this.value)) {
35+
this._restoreBetween(this.value);
36+
return;
37+
}
38+
39+
if (this.filterMode === 'in' && Array.isArray(this.value)) {
40+
this.inValueText = this.value.join(', ');
41+
return;
42+
}
43+
44+
if (typeof this.value === 'string' && this.value) {
2845
const datetime = new Date(this.value);
2946
this.date = format(datetime, 'yyyy-MM-dd');
3047
this.time = format(datetime, 'HH:mm:ss');
@@ -33,7 +50,9 @@ export class DateTimeFilterComponent extends BaseFilterFieldComponent implements
3350
}
3451

3552
ngAfterViewInit(): void {
36-
if (this.value) {
53+
if (this.filterMode === 'between' || this.filterMode === 'in') {
54+
this.onComparatorChange.emit(this.filterMode);
55+
} else if (this.value) {
3756
this.onComparatorChange.emit(this.filterMode);
3857
} else {
3958
const value = this._computePresetValue(this.filterMode);
@@ -49,19 +68,34 @@ export class DateTimeFilterComponent extends BaseFilterFieldComponent implements
4968
}
5069

5170
onFilterModeChange(mode: string): void {
71+
const previous = this.filterMode;
5272
this.filterMode = mode;
5373

5474
if (this._presetModes.includes(mode)) {
5575
const value = this._computePresetValue(mode);
5676
this.onFieldChange.emit(value);
5777
this.onComparatorChange.emit('gte');
58-
} else {
59-
this.onComparatorChange.emit(mode);
60-
if (this.date) {
61-
const time = this.time || '00:00';
62-
const datetime = `${this.date}T${time}Z`;
63-
this.onFieldChange.emit(datetime);
78+
return;
79+
}
80+
81+
if (mode === 'in' || mode === 'between') {
82+
if (previous !== mode) {
83+
this.inValueText = '';
84+
this.lowerDate = undefined;
85+
this.lowerTime = undefined;
86+
this.upperDate = undefined;
87+
this.upperTime = undefined;
88+
this.onFieldChange.emit(undefined);
6489
}
90+
this.onComparatorChange.emit(mode);
91+
return;
92+
}
93+
94+
this.onComparatorChange.emit(mode);
95+
if (this.date) {
96+
const time = this.time || '00:00';
97+
const datetime = `${this.date}T${time}Z`;
98+
this.onFieldChange.emit(datetime);
6599
}
66100
}
67101

@@ -78,10 +112,59 @@ export class DateTimeFilterComponent extends BaseFilterFieldComponent implements
78112
this.onComparatorChange.emit(this.filterMode);
79113
}
80114

115+
onBetweenLowerChange(): void {
116+
const lower = this._composeDateTime(this.lowerDate, this.lowerTime);
117+
const upper = this._composeDateTime(this.upperDate, this.upperTime);
118+
this.onFieldChange.emit([lower, upper]);
119+
this.onComparatorChange.emit('between');
120+
}
121+
122+
onBetweenUpperChange(): void {
123+
const lower = this._composeDateTime(this.lowerDate, this.lowerTime);
124+
const upper = this._composeDateTime(this.upperDate, this.upperTime);
125+
this.onFieldChange.emit([lower, upper]);
126+
this.onComparatorChange.emit('between');
127+
}
128+
129+
onInTextChange(text: string): void {
130+
this.inValueText = text;
131+
const parts = text
132+
.split(',')
133+
.map((v) => v.trim())
134+
.filter((v) => v.length > 0);
135+
136+
if (parts.length === 0) {
137+
this.onFieldChange.emit(undefined);
138+
return;
139+
}
140+
141+
this.onFieldChange.emit(parts);
142+
this.onComparatorChange.emit('in');
143+
}
144+
81145
isPresetMode(): boolean {
82146
return this._presetModes.includes(this.filterMode);
83147
}
84148

149+
private _restoreBetween(value: string[]): void {
150+
if (value[0]) {
151+
const lower = new Date(value[0]);
152+
this.lowerDate = format(lower, 'yyyy-MM-dd');
153+
this.lowerTime = format(lower, 'HH:mm:ss');
154+
}
155+
if (value[1]) {
156+
const upper = new Date(value[1]);
157+
this.upperDate = format(upper, 'yyyy-MM-dd');
158+
this.upperTime = format(upper, 'HH:mm:ss');
159+
}
160+
}
161+
162+
private _composeDateTime(date: string, time: string): string | null {
163+
if (!date) return null;
164+
const t = time || '00:00:00';
165+
return `${date}T${t}Z`;
166+
}
167+
85168
private _computePresetValue(mode: string): string {
86169
const now = new Date();
87170
let targetDate: Date;

frontend/src/app/components/ui-components/filter-fields/default-filter/default-filter.component.css

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,18 @@
1414
flex: 1;
1515
min-width: 0;
1616
}
17+
18+
.between-fields {
19+
display: flex;
20+
gap: 8px;
21+
align-items: flex-start;
22+
}
23+
24+
.between-fields ::ng-deep > ndc-dynamic {
25+
flex: 1;
26+
min-width: 0;
27+
}
28+
29+
.between-fields ::ng-deep mat-form-field {
30+
width: 100%;
31+
}

frontend/src/app/components/ui-components/filter-fields/default-filter/default-filter.component.html

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,43 @@
77
</mat-select>
88
</mat-form-field>
99

10-
<div class="value-field">
11-
<ndc-dynamic
12-
[ndcDynamicComponent]="valueComponent"
13-
[ndcDynamicInputs]="innerInputs"
14-
[ndcDynamicOutputs]="{
15-
onFieldChange: { handler: onValueChange, args: ['$event'] }
16-
}"
17-
></ndc-dynamic>
18-
</div>
10+
@if (comparator === 'in') {
11+
<mat-form-field class="value-field" appearance="outline">
12+
<mat-label>{{ normalizedLabel() }} (comma-separated)</mat-label>
13+
<input matInput
14+
name="{{ label() }}-{{ key() }}-in"
15+
placeholder="value1, value2, value3"
16+
[required]="required()"
17+
[disabled]="disabled()"
18+
[(ngModel)]="inValueText"
19+
(ngModelChange)="onInTextChange($event)">
20+
</mat-form-field>
21+
} @else if (comparator === 'between') {
22+
<div class="value-field between-fields">
23+
<ndc-dynamic
24+
[ndcDynamicComponent]="valueComponent"
25+
[ndcDynamicInputs]="lowerInputs"
26+
[ndcDynamicOutputs]="{
27+
onFieldChange: { handler: onBetweenLowerChange, args: ['$event'] }
28+
}"
29+
></ndc-dynamic>
30+
<ndc-dynamic
31+
[ndcDynamicComponent]="valueComponent"
32+
[ndcDynamicInputs]="upperInputs"
33+
[ndcDynamicOutputs]="{
34+
onFieldChange: { handler: onBetweenUpperChange, args: ['$event'] }
35+
}"
36+
></ndc-dynamic>
37+
</div>
38+
} @else {
39+
<div class="value-field">
40+
<ndc-dynamic
41+
[ndcDynamicComponent]="valueComponent"
42+
[ndcDynamicInputs]="innerInputs"
43+
[ndcDynamicOutputs]="{
44+
onFieldChange: { handler: onValueChange, args: ['$event'] }
45+
}"
46+
></ndc-dynamic>
47+
</div>
48+
}
1949
</div>

0 commit comments

Comments
 (0)