mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2025-12-13 01:36:22 +00:00
fix: date range is now pipe has issues around boundaries
This commit is contained in:
@@ -2,7 +2,7 @@ import {ChangeDetectionStrategy, Component, ElementRef, Input} from '@angular/co
|
|||||||
import {SCIdCard} from '@openstapps/core';
|
import {SCIdCard} from '@openstapps/core';
|
||||||
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
||||||
import {AsyncPipe, TitleCasePipe} from '@angular/common';
|
import {AsyncPipe, TitleCasePipe} from '@angular/common';
|
||||||
import {InRangeNowPipe, ToDateRangePipe} from '../../util/in-range.pipe';
|
import {IntervalIsNowPipe, ToDateIntervalPipe} from '../../util/in-range.pipe';
|
||||||
import {TranslateModule} from '@ngx-translate/core';
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
import {AnimationController, ModalController} from '@ionic/angular';
|
import {AnimationController, ModalController} from '@ionic/angular';
|
||||||
import {ScreenBrightness} from '@capacitor-community/screen-brightness';
|
import {ScreenBrightness} from '@capacitor-community/screen-brightness';
|
||||||
@@ -16,7 +16,14 @@ import {iosDuration, iosEasing, mdDuration, mdEasing} from 'src/app/animation/ea
|
|||||||
styleUrls: ['id-card.scss'],
|
styleUrls: ['id-card.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [ThingTranslateModule, InRangeNowPipe, ToDateRangePipe, AsyncPipe, TranslateModule, TitleCasePipe],
|
imports: [
|
||||||
|
ThingTranslateModule,
|
||||||
|
IntervalIsNowPipe,
|
||||||
|
ToDateIntervalPipe,
|
||||||
|
AsyncPipe,
|
||||||
|
TranslateModule,
|
||||||
|
TitleCasePipe,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class IdCardComponent {
|
export class IdCardComponent {
|
||||||
@Input({required: true}) item: SCIdCard;
|
@Input({required: true}) item: SCIdCard;
|
||||||
|
|||||||
@@ -4,6 +4,6 @@
|
|||||||
draggable="false"
|
draggable="false"
|
||||||
(click)="presentFullscreen()"
|
(click)="presentFullscreen()"
|
||||||
/>
|
/>
|
||||||
@if (item.validity && (item.validity | toDateRange | isInRangeNow | async) === false) {
|
@if (item.validity && (item.validity | rangeToDateInterval | dfnsIntervalIsNow | async) === false) {
|
||||||
<div class="expired">{{ 'profile.userInfo.expired' | translate | titlecase }}</div>
|
<div class="expired">{{ 'profile.userInfo.expired' | translate | titlecase }}</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {Pipe, PipeTransform} from '@angular/core';
|
import {Pipe, PipeTransform} from '@angular/core';
|
||||||
import {SCRange, isInRange, SCISO8601DateRange} from '@openstapps/core';
|
import {SCRange, isInRange, SCISO8601DateRange} from '@openstapps/core';
|
||||||
import {merge, Observable, timer} from 'rxjs';
|
import {NormalizedInterval, differenceInMilliseconds, interval, isEqual} from 'date-fns';
|
||||||
import {distinctUntilChanged, map, startWith} from 'rxjs/operators';
|
import {EMPTY, Observable, SchedulerLike, asyncScheduler, concat, defer, map, of, timer} from 'rxjs';
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'isInRange',
|
name: 'isInRange',
|
||||||
@@ -14,28 +14,49 @@ export class InRangePipe implements PipeTransform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const MIN_DATE = new Date(0);
|
||||||
|
export const MAX_DATE = new Date(1e15);
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'toDateRange',
|
name: 'rangeToDateInterval',
|
||||||
pure: true,
|
pure: true,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
})
|
})
|
||||||
export class ToDateRangePipe implements PipeTransform {
|
export class ToDateIntervalPipe implements PipeTransform {
|
||||||
transform(value: SCISO8601DateRange): SCRange<Date> {
|
transform(value: SCISO8601DateRange): NormalizedInterval {
|
||||||
return Object.fromEntries(Object.entries(value).map(([key, value]) => [key, new Date(value)]));
|
return interval(new Date(value.gte ?? value.gt ?? MIN_DATE), new Date(value.lte ?? value.lt ?? MAX_DATE));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an Observable that will change its value when the current date is within the given interval.
|
||||||
|
*/
|
||||||
|
export function isWithinIntervalObservable(
|
||||||
|
value: NormalizedInterval,
|
||||||
|
scheduler: SchedulerLike = asyncScheduler,
|
||||||
|
): Observable<boolean> {
|
||||||
|
return defer(() => {
|
||||||
|
const now = scheduler.now();
|
||||||
|
const activate = differenceInMilliseconds(value.start, now);
|
||||||
|
const deactivate = differenceInMilliseconds(value.end, now);
|
||||||
|
|
||||||
|
return concat(
|
||||||
|
of(activate <= 0 && deactivate > 0),
|
||||||
|
activate <= 0 ? EMPTY : timer(value.start, scheduler).pipe(map(() => true)),
|
||||||
|
isEqual(value.end, MAX_DATE) || deactivate <= 0
|
||||||
|
? EMPTY
|
||||||
|
: timer(value.end, scheduler).pipe(map(() => false)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'isInRangeNow',
|
name: 'dfnsIntervalIsNow',
|
||||||
pure: true,
|
pure: true,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
})
|
})
|
||||||
export class InRangeNowPipe implements PipeTransform {
|
export class IntervalIsNowPipe implements PipeTransform {
|
||||||
transform(value: SCRange<Date>): Observable<boolean> {
|
transform(value: NormalizedInterval): Observable<boolean> {
|
||||||
return merge(timer(value.lte || value.lt || 0), timer(value.gte || value.gt || 0)).pipe(
|
return isWithinIntervalObservable(value);
|
||||||
startWith(0),
|
|
||||||
map(() => isInRange(new Date(), value)),
|
|
||||||
distinctUntilChanged(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
42
frontend/app/src/app/util/in-range.spec.ts
Normal file
42
frontend/app/src/app/util/in-range.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import {TestScheduler} from 'rxjs/testing';
|
||||||
|
import {MAX_DATE, MIN_DATE, isWithinIntervalObservable} from './in-range.pipe';
|
||||||
|
import {interval} from 'date-fns';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test macro
|
||||||
|
*/
|
||||||
|
function test(range: [number | undefined, number | undefined], subscribe: string, expected: string) {
|
||||||
|
const testScheduler = new TestScheduler((actual, expected) => {
|
||||||
|
expect(actual).withContext(actual.map(JSON.stringify).join('\n')).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should emit "${expected}" when "${subscribe}" for range ${range[0] ?? ''}..${range[1] ?? ''}`, () => {
|
||||||
|
testScheduler.run(({expectObservable}) => {
|
||||||
|
expectObservable(
|
||||||
|
isWithinIntervalObservable(
|
||||||
|
interval(new Date(range[0] ?? MIN_DATE), new Date(range[1] ?? MAX_DATE)),
|
||||||
|
testScheduler,
|
||||||
|
),
|
||||||
|
subscribe,
|
||||||
|
).toBe(expected, {t: true, f: false});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('isWithinIntervalObservable', () => {
|
||||||
|
test([500, undefined], '1s ^', '1s (t|)');
|
||||||
|
test([1000, undefined], '500ms ^', '500ms f 499ms (t|)');
|
||||||
|
|
||||||
|
test([undefined, 500], '1s ^', '1s (f|)');
|
||||||
|
test([undefined, 1000], '500ms ^', '500ms t 499ms (f|)');
|
||||||
|
|
||||||
|
test([1000, 2000], '500ms ^', '500ms f 499ms t 999ms (f|)');
|
||||||
|
|
||||||
|
test([500, 1000], '1500ms ^', '1500ms (f|)');
|
||||||
|
test([500, 1000], '1s ^', '1000ms (f|)');
|
||||||
|
test([500, 1000], '999ms ^', '999ms t (f|)');
|
||||||
|
test([500, 1000], '500ms ^', '500ms t 499ms (f|)');
|
||||||
|
test([500, 1000], '499ms ^', '499ms f t 499ms (f|)');
|
||||||
|
|
||||||
|
test([500, 1000], '^ 750ms !', 'f 499ms t');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user