-
Notifications
You must be signed in to change notification settings - Fork 69
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature: Global timezone selector(UTC vs Local) (#1854)
- Loading branch information
1 parent
5362dc7
commit c406564
Showing
76 changed files
with
821 additions
and
281 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export * from "./i18n-testing.module"; | ||
export * from "./telemetry-testing.module"; | ||
export * from "./timezone-testing.module"; | ||
export * from "./mock-control-value-accessor"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { NgModule } from "@angular/core"; | ||
import { DEFAULT_TIMEZONE, TimeZoneService } from "@batch-flask/core/timezone"; | ||
import { DateTime } from "luxon"; | ||
import { BehaviorSubject } from "rxjs"; | ||
|
||
export class TestTimeZoneService { | ||
public current = new BehaviorSubject({ | ||
name: "utc", | ||
offsetNameShort: "UTC", | ||
offsetNameLong: "UTC", | ||
}); | ||
public setTimezone = jasmine.createSpy("setTimezone").and.callFake(this._setTimezone.bind(this)); | ||
|
||
private _setTimezone(name: string) { | ||
name = name || "local"; | ||
const date = DateTime.local().setZone(name); | ||
if (date.isValid) { | ||
this.current.next({ | ||
name, | ||
offsetNameShort: date.offsetNameShort, | ||
offsetNameLong: date.offsetNameLong, | ||
}); | ||
} else { | ||
return this.current.next(DEFAULT_TIMEZONE); | ||
} | ||
} | ||
|
||
} | ||
|
||
@NgModule({ | ||
providers: [ | ||
{ provide: TimeZoneService, useClass: TestTimeZoneService }, | ||
], | ||
}) | ||
export class TimeZoneTestingModule { | ||
public static withZone() { | ||
const service = new TestTimeZoneService(); | ||
service.current.next({ | ||
name: "local", | ||
offsetNameShort: "local", | ||
offsetNameLong: "local", | ||
}); | ||
return { | ||
ngModule: TimeZoneTestingModule, | ||
providers: [ | ||
{ provide: TestTimeZoneService, useValue: service }, | ||
], | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./timezone.service"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { BehaviorSubject, Subscription } from "rxjs"; | ||
import { DEFAULT_TIMEZONE, TimeZone, TimeZoneService } from "./timezone.service"; | ||
|
||
describe("TimezoneService", () => { | ||
let service: TimeZoneService; | ||
let configSpy; | ||
let telemetryServiceSpy; | ||
let timezoneSetting: BehaviorSubject<string>; | ||
let current: TimeZone; | ||
let sub: Subscription; | ||
|
||
beforeEach(() => { | ||
timezoneSetting = new BehaviorSubject<string | null>(null); | ||
configSpy = { | ||
watch: jasmine.createSpy("watch").and.returnValue(timezoneSetting), | ||
set: jasmine.createSpy("set"), | ||
}; | ||
telemetryServiceSpy = { | ||
trackSetting: jasmine.createSpy("trackSetting"), | ||
}; | ||
service = new TimeZoneService(configSpy, telemetryServiceSpy); | ||
sub = service.current.subscribe(x => current = x); | ||
}); | ||
|
||
afterEach(() => { | ||
if (sub) { | ||
sub.unsubscribe(); | ||
} | ||
}); | ||
|
||
it("should watch the settings", () => { | ||
expect(configSpy.watch).toHaveBeenCalledOnce(); | ||
expect(configSpy.watch).toHaveBeenCalledWith("timezone"); | ||
}); | ||
|
||
it("default to local timezone when there is no settings", () => { | ||
timezoneSetting.next(null); | ||
expect(current).toEqual(DEFAULT_TIMEZONE); | ||
}); | ||
|
||
it("default to local timezone when timezone setting is invalid", () => { | ||
timezoneSetting.next("foobar"); | ||
expect(current).toEqual(DEFAULT_TIMEZONE); | ||
timezoneSetting.next("America/Unkown_City"); | ||
expect(current).toEqual(DEFAULT_TIMEZONE); | ||
}); | ||
|
||
it("picks UTC", () => { | ||
timezoneSetting.next("utc"); | ||
expect(current).toEqual({ | ||
name: "utc", | ||
offsetNameShort: "UTC", | ||
offsetNameLong: "UTC", | ||
}); | ||
}); | ||
|
||
it("updates the timezone", () => { | ||
service.setTimezone("utc"); | ||
expect(configSpy.set).toHaveBeenCalledOnce(); | ||
expect(configSpy.set).toHaveBeenCalledWith("timezone", "utc"); | ||
expect(telemetryServiceSpy.trackSetting).toHaveBeenCalledOnce(); | ||
expect(telemetryServiceSpy.trackSetting).toHaveBeenCalledWith("timezone", "utc"); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { Injectable } from "@angular/core"; | ||
import { DateTime } from "luxon"; | ||
import { Observable } from "rxjs"; | ||
import { map, publishReplay, refCount } from "rxjs/operators"; | ||
import { TelemetryService } from "../telemetry"; | ||
import { BatchFlaskUserConfiguration, UserConfigurationService } from "../user-configuration"; | ||
|
||
const localDate = DateTime.local(); | ||
export const DEFAULT_TIMEZONE: TimeZone = { | ||
name: "local", | ||
offsetNameShort: localDate.offsetNameShort, | ||
offsetNameLong: localDate.offsetNameLong, | ||
}; | ||
|
||
export interface TimeZone { | ||
name: string; | ||
offsetNameShort: string; | ||
offsetNameLong: string; | ||
} | ||
|
||
/** | ||
* Service to handle timezone for dates in the app. | ||
* By default use the local timezone | ||
*/ | ||
@Injectable({ providedIn: "root" }) | ||
export class TimeZoneService { | ||
public current: Observable<TimeZone>; | ||
|
||
constructor( | ||
private userConfiguration: UserConfigurationService<BatchFlaskUserConfiguration>, | ||
private telemetryService: TelemetryService) { | ||
this.current = this.userConfiguration.watch("timezone").pipe( | ||
map((name): TimeZone => { | ||
name = name || "local"; | ||
const date = DateTime.local().setZone(name); | ||
if (date.isValid) { | ||
return { | ||
name, | ||
offsetNameShort: date.offsetNameShort, | ||
offsetNameLong: date.offsetNameLong, | ||
}; | ||
} else { | ||
return DEFAULT_TIMEZONE; | ||
} | ||
}), | ||
publishReplay(1), | ||
refCount(), | ||
); | ||
} | ||
|
||
public async setTimezone(timezone: string) { | ||
this.telemetryService.trackSetting("timezone", timezone); | ||
return this.userConfiguration.set("timezone", timezone); | ||
} | ||
} |
4 changes: 2 additions & 2 deletions
4
src/@batch-flask/core/user-configuration/user-configuration.service.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { Component, DebugElement } from "@angular/core"; | ||
import { ComponentFixture, TestBed } from "@angular/core/testing"; | ||
import { By } from "@angular/platform-browser"; | ||
|
||
import { TimeZoneService } from "@batch-flask/core"; | ||
import { DateUtils } from "@batch-flask/utils"; | ||
import { DateTime } from "luxon"; | ||
import { BehaviorSubject } from "rxjs"; | ||
import { DateComponent } from "./date.component"; | ||
|
||
const date1 = new Date(Date.UTC(2017, 8, 3)); | ||
const date2 = new Date(Date.UTC(2015, 9, 4)); | ||
|
||
@Component({ | ||
template: `<bl-date [date]="date"></bl-date>`, | ||
}) | ||
class TestComponent { | ||
public date = date1; | ||
} | ||
|
||
describe("DateComponent", () => { | ||
let fixture: ComponentFixture<TestComponent>; | ||
let testComponent: TestComponent; | ||
let de: DebugElement; | ||
|
||
let timezoneServiceSpy; | ||
|
||
beforeEach(() => { | ||
timezoneServiceSpy = { | ||
current: new BehaviorSubject({ | ||
name: "utc", | ||
}), | ||
}; | ||
TestBed.configureTestingModule({ | ||
imports: [], | ||
declarations: [DateComponent, TestComponent], | ||
providers: [ | ||
{ provide: TimeZoneService, useValue: timezoneServiceSpy }, | ||
], | ||
}); | ||
fixture = TestBed.createComponent(TestComponent); | ||
testComponent = fixture.componentInstance; | ||
de = fixture.debugElement.query(By.css("bl-date")); | ||
fixture.detectChanges(); | ||
}); | ||
|
||
it("shows the pretty date in the current timezone", () => { | ||
const data = DateTime.fromJSDate(date1).setZone("utc"); | ||
expect(de.nativeElement.textContent).toContain(DateUtils.prettyDate(data)); | ||
}); | ||
|
||
it("updates the time in the given timezone", () => { | ||
timezoneServiceSpy.current.next({ | ||
name: "America/Los_Angeles", | ||
}); | ||
fixture.detectChanges(); | ||
|
||
const utcDate = DateTime.fromJSDate(date1).setZone("utc"); | ||
const data = DateTime.fromJSDate(date1).setZone("America/Los_Angeles"); | ||
expect(de.nativeElement.textContent).toContain(DateUtils.prettyDate(data)); | ||
expect(de.nativeElement.textContent).not.toContain(DateUtils.prettyDate(utcDate)); | ||
}); | ||
|
||
it("updates the date", () => { | ||
testComponent.date = date2; | ||
fixture.detectChanges(); | ||
const data = DateTime.fromJSDate(date2).setZone("utc"); | ||
expect(de.nativeElement.textContent).toContain(DateUtils.prettyDate(data)); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,51 @@ | ||
import { ChangeDetectionStrategy, Component, Input, OnChanges } from "@angular/core"; | ||
import { | ||
ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Input, OnChanges, OnDestroy, | ||
} from "@angular/core"; | ||
import { TimeZoneService } from "@batch-flask/core"; | ||
import { DateUtils } from "@batch-flask/utils"; | ||
import { DateTime } from "luxon"; | ||
import { BehaviorSubject, Subject, combineLatest } from "rxjs"; | ||
import { takeUntil } from "rxjs/operators"; | ||
|
||
@Component({ | ||
selector: "bl-date", | ||
template: "{{formatedDate}}", | ||
changeDetection: ChangeDetectionStrategy.OnPush, | ||
}) | ||
export class DateComponent implements OnChanges { | ||
export class DateComponent implements OnChanges, OnDestroy { | ||
@Input() public date: Date; | ||
|
||
@HostBinding("attr.title") public title: string; | ||
|
||
public formatedDate: string; | ||
|
||
private _date = new BehaviorSubject(null); | ||
private _destroy = new Subject(); | ||
|
||
constructor(private changeDetector: ChangeDetectorRef, private timezoneService: TimeZoneService) { | ||
combineLatest(this.timezoneService.current, this._date).pipe( | ||
takeUntil(this._destroy), | ||
).subscribe(([zone, jsDate]) => { | ||
if (jsDate) { | ||
const date = DateTime.fromJSDate(jsDate).setZone(zone.name); | ||
this.formatedDate = DateUtils.prettyDate(date); | ||
this.title = date.toISO(); | ||
} else { | ||
this.formatedDate = "-"; | ||
this.title = "-"; | ||
} | ||
this.changeDetector.markForCheck(); | ||
}); | ||
} | ||
|
||
public ngOnChanges(changes) { | ||
if (changes.date) { | ||
this.formatedDate = DateUtils.prettyDate(this.date); | ||
this._date.next(this.date); | ||
} | ||
} | ||
|
||
public ngOnDestroy() { | ||
this._destroy.next(); | ||
this._destroy.complete(); | ||
} | ||
} |
Oops, something went wrong.