Skip to content

Commit

Permalink
Fixed logic and unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
hudson-newey committed Dec 6, 2023
1 parent 2099bd5 commit ab26495
Show file tree
Hide file tree
Showing 21 changed files with 226 additions and 73 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
"typeahead",
"Ecoacoustics",
"datetime"
]
],
"compile-hero.disable-compile-files-on-did-save-code": false
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,25 @@ <h1>DateTime Examples</h1>

<div class="input-group">
<input
#dateInput="ngbDatepicker"
ngbDatepicker
(keypress)="updateFakeDate($event)"
type="datetime"
name="datetime-input"
id="datetime-input"
class="form-control"
placeholder="yyyy-mm-dd hh:mm:ss"
placeholder="yyyy-mm-ddThh:mm:ss+hh:mm"
[(ngModel)]="fakeDate"
/>
<button
class="btn btn-outline-secondary"
(click)="dateInput.toggle()"
type="button"
>
<fa-icon [icon]="['fas', 'calendar']"></fa-icon>
</button>
</div>

<label for="users-fake-timezone">Fake User Timezone (used in baw-datetime)</label>
<input
type="text"
name="users-fake-timezone"
id="users-fake-timezone"
class="form-control"
placeholder="utc, local +08:00 UTC, etc."
[(ngModel)]="fakeUserTimezone"
/>

<label for="timezone-input">Timezone</label>
<input
type="text"
Expand Down Expand Up @@ -55,30 +56,30 @@ <h2>baw-datetime</h2>
<strong>Date & Time Instants:</strong><br />

<code>
<pre>&lt;baw-datetime [value]="fakeDate" /&gt;</pre>
<pre>&lt;baw-datetime [value]="fakeUserDate" /&gt;</pre>
</code>

<baw-datetime [value]="fakeDate" />
<baw-datetime [value]="fakeUserDate" />
</div>

<div class="mt-3">
<strong>Date Instants:</strong><br />

<code>
<pre>&lt;baw-datetime [value]="fakeDate" dateOnly /&gt;</pre>
<pre>&lt;baw-datetime [value]="fakeUserDate" dateOnly /&gt;</pre>
</code>

<baw-datetime [value]="fakeDate" dateOnly />
<baw-datetime [value]="fakeUserDate" dateOnly />
</div>

<div class="mt-3">
<strong>Time Instants:</strong><br />

<code>
<pre>&lt;baw-datetime [value]="fakeDate" timeOnly /&gt;</pre>
<pre>&lt;baw-datetime [value]="fakeUserDate" timeOnly /&gt;</pre>
</code>

<baw-datetime [value]="fakeDate" timeOnly />
<baw-datetime [value]="fakeUserDate" timeOnly />
</div>
</section>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class DateTimeExampleComponent extends PageComponent implements OnInit {
}

protected fakeTimezone = "Australia/Perth";
protected fakeUserTimezone = DateTime.local().zoneName;
protected fakeDate;
protected fakeDuration;

Expand All @@ -23,6 +24,11 @@ class DateTimeExampleComponent extends PageComponent implements OnInit {
this.fakeDuration = Duration.fromObject({ hours: 1, minutes: 30 });
}

protected get fakeUserDate(): DateTime {
const luxonDateTime = this.fakeDate instanceof DateTime ? this.fakeDate : DateTime.fromISO(this.fakeDate);
return luxonDateTime.setZone(this.fakeUserTimezone);
}

protected get fakeDateWithZone(): DateTime {
const luxonDateTime = this.fakeDate instanceof DateTime ? this.fakeDate : DateTime.fromISO(this.fakeDate);
return luxonDateTime.setZone(this.fakeTimezone);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
>
<ng-template let-row="row" let-value="value" ngx-datatable-cell-template>
<span [ngbTooltip]="asRecording(row).recordedDateTimezone ?? 'UTC'">
<baw-datetime [value]="value" />
{{ formatDate(value) }}
</span>
</ng-template>
</ngx-datatable-column>
Expand All @@ -39,7 +39,7 @@
[maxWidth]="170"
>
<ng-template let-value="value" ngx-datatable-cell-template>
<baw-time-since [value]="value" />
{{ formatDuration(value) }}
</ng-template>
</ngx-datatable-column>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Component, Input } from "@angular/core";
import { AudioRecordingsService } from "@baw-api/audio-recording/audio-recordings.service";
import { Filters } from "@baw-api/baw-api.service";
import { toRelative } from "@interfaces/apiInterfaces";
import { AudioRecording } from "@models/AudioRecording";
import { ColumnMode } from "@swimlane/ngx-datatable";
import { DateTime, Duration } from "luxon";
import { BehaviorSubject } from "rxjs";

@Component({
Expand All @@ -23,4 +25,15 @@ export class DownloadTableComponent {
public asRecording(model: any): AudioRecording {
return model;
}

public formatDuration(duration: Duration): string {
return toRelative(duration, {
largest: 2,
round: true,
});
}

public formatDate(date: DateTime): string {
return date.toFormat("yyyy-MM-dd HH:mm:ss");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ <h1>Audio Recordings</h1>
>
<ngx-datatable-column name="Recorded">
<ng-template let-row="row" let-value="value" ngx-datatable-cell-template>
<baw-datetime [value]="value.recordedDate" />
<span [ngbTooltip]="value.recordedDateTimezone" placement="right">
{{ getRecordingDate(value) }}
</span>
</ng-template>
</ngx-datatable-column>
<ngx-datatable-column name="Duration"> </ngx-datatable-column>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ class AudioRecordingsListComponent
}
}

public getRecordingDate(recording: AudioRecording): string {
return recording.recordedDate.toFormat("yyyy-LL-dd HH:mm");
}

public updateFilters(incomingFilters: Filters<AudioRecording>): void {
// since sorting is handled by the pagination table, when the filter is updated, it would remove the sorting order
// to ensure that sorting is retained throughout filtering, retain the previous sorting information
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/harvest/pages/list/list.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ <h1>
Created
</ng-template>
<ng-template let-value="value" ngx-datatable-cell-template>
<baw-zoned-datetime [value]="value" timezone="local" />
<span>{{ formatDate(value) }}</span>
</ng-template>
</ngx-datatable-column>

Expand Down
5 changes: 5 additions & 0 deletions src/app/components/harvest/pages/list/list.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { PageComponent } from "@helpers/page/pageComponent";
import { Harvest } from "@models/Harvest";
import { Project } from "@models/Project";
import { List } from "immutable";
import { DateTime } from "luxon";
import { NgbModal } from "@ng-bootstrap/ng-bootstrap";
import { BawApiError } from "@helpers/custom-errors/baw-api-error";
import { BehaviorSubject, catchError, takeUntil, throwError } from "rxjs";
Expand Down Expand Up @@ -108,6 +109,10 @@ class ListComponent extends PageComponent implements OnInit {
public asHarvest(model: any): Harvest {
return model;
}

public formatDate(date: DateTime): string {
return date.toLocal().toFormat("yyyy-MM-dd HH:mm:ss");
}
}

ListComponent.linkToRoute({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<!--
This is a shared template used by datetime, zoned datetime, duration, and time since components
and should be used in conjunction with the AbstractDatetimeComponent.
-->
<time [dateTime]="rawDateTime" [ngbTooltip]="tooltipValue()">
{{ formattedValue() }}{{ suffix() }}
</time>
28 changes: 28 additions & 0 deletions src/app/components/shared/datetime/abstract-datetime.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export interface IAbstractDatetimeComponent {
formattedValue: () => string;
tooltipValue: () => string;
rawDateTime: () => string;
suffix: () => string;
}

/**
* An abstract component that should be used in conjunction with the abstract-datetime.component.html template
*
* @requires formattedValue
* @requires tooltipValue
* @requires rawDateTime
*/
export abstract class AbstractDatetimeComponent
implements IAbstractDatetimeComponent
{
public constructor() {}

public abstract formattedValue(): string;
public abstract tooltipValue(): string;
public abstract rawDateTime(): string;

// I have implemented this method so that each datetime component will default to having no suffix
public suffix(): string {
return "";
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe("DatetimeComponent", () => {
spectator.query<HTMLTimeElement>("time");

beforeEach(() => setup());

it("should create", () => {
expect(spectator.component).toBeInstanceOf(DatetimeComponent);
});
Expand Down Expand Up @@ -69,6 +69,18 @@ describe("DatetimeComponent", () => {
expect(spectator.element).toHaveExactTrimmedText(expectedDateTime);
});

it("should display full datetime if both dateOnly and timeOnly are set", () => {
const fakeDateTime = DateTime.fromISO("2020-01-01T12:10:11.000Z");
const expectedDateTime = "2020-01-01 20:10:11";

spectator.component.value = fakeDateTime;
spectator.component.dateOnly = true;
spectator.component.timeOnly = true;
spectator.detectChanges();

expect(spectator.element).toHaveExactTrimmedText(expectedDateTime);
});

it("should show a full utc date time in the uesrs timezone", () => {
const fakeDateTime = DateTime.fromISO("2020-01-01T12:10:11.000Z");
const expectedDateTime = "2020-01-01 20:10:11";
Expand All @@ -81,7 +93,7 @@ describe("DatetimeComponent", () => {
});

it("should have a tooltip that displays the full date, time and utc offset for a JavaScript date object", () => {
const expectedTooltip = "2020-01-01T20:10:11.000+08:00 (Australia/Perth UTC+8)";
const expectedTooltip = "2020-01-01 20:10:11 (Australia/Perth UTC+08:00)";
const mockDate = new Date("2020-01-01T12:10:11.000Z");

spectator.component.value = mockDate;
Expand All @@ -91,7 +103,7 @@ describe("DatetimeComponent", () => {
});

it("should have a tooltip that displays the full utc date, utc time, and utc offset for a Luxon DateTime object", () => {
const expectedTooltip = "2020-01-01T20:10:11.000+08:00 (Australia/Perth UTC+8)";
const expectedTooltip = "2020-01-01 20:10:11 (Australia/Perth UTC+08:00)";
const mockDateTime = DateTime.fromISO("2020-01-01T12:10:11.000Z");

spectator.component.value = mockDateTime;
Expand Down
55 changes: 37 additions & 18 deletions src/app/components/shared/datetime/datetime/datetime.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,25 @@ import { Component, Input } from "@angular/core";
import { isInstantiated } from "@helpers/isInstantiated/isInstantiated";
import { TimezoneInformation } from "@interfaces/apiInterfaces";
import { DateTime, Zone } from "luxon";
import { AbstractDatetimeComponent } from "../abstract-datetime.component";

type BawTimezoneUnion = Zone | TimezoneInformation | string;

/**
* Displays a datetime in the users locale and timezone
* Commonly used for displaying client side timestamps e.g. Project created at: xyz
*
* This component does not assume that all dates input are utc, and will localise using timezone information if possible
*/
@Component({
selector: "baw-datetime",
templateUrl: "datetime.component.html",
templateUrl: "../abstract-datetime.component.html",
styleUrls: ["datetime.component.scss"],
})
export class DatetimeComponent {
public constructor() {}
export class DatetimeComponent extends AbstractDatetimeComponent {
public constructor() {
super();
}

@Input({ required: true }) public value!: DateTime | Date;
@Input() public dateOnly: string | boolean;
Expand All @@ -28,23 +33,22 @@ export class DatetimeComponent {
const luxonDate: DateTime =
this.value instanceof Date ? DateTime.fromJSDate(this.value) : this.value;

if (!isInstantiated(this.timezone)) {
console.log("here");
return luxonDate.toUTC();
}

return luxonDate;
}

protected formattedDateTime(): string {
public formattedValue(): string {
return this.luxonDateTime.toFormat(this.dateTimeFormat());
}

protected tooltipValue(): string {
const fullDateTime: string = this.luxonDateTime.toFormat(
"yyyy-MM-dd HH:mm:ss"
);
return `${fullDateTime} (${this.timezoneName()})`;
public tooltipValue(): string {
const fullDateTime: string = this.luxonDateTime.toFormat("yyyy-MM-dd HH:mm:ss");
const timezoneName = this.timezoneName();

return `${fullDateTime} (${timezoneName})`;
}

public rawDateTime(): string {
return this.luxonDateTime.toISO();
}

protected timezoneName(): string {
Expand All @@ -53,7 +57,7 @@ export class DatetimeComponent {
// we should therefore return the users timezone name
const userDateTime = DateTime.local();

const userTimezoneName: string = userDateTime.offsetNameShort;
const userTimezoneName: string = userDateTime.zoneName;
const userTimezoneValue: string = userDateTime.zone.formatOffset(
userDateTime.offset,
"short"
Expand All @@ -63,7 +67,7 @@ export class DatetimeComponent {
} else if (typeof this.timezone === "string") {
const dateTime = this.luxonDateTime.setZone(this.timezone);

const offsetName = dateTime.offsetNameShort;
const offsetName = dateTime.zoneName;
const offsetValue = dateTime.zone.formatOffset(dateTime.offset, "short");

return `${offsetName} UTC${offsetValue}`;
Expand All @@ -82,8 +86,23 @@ export class DatetimeComponent {
return this.timezone.identifier;
}

private dateTimeFormat(): string {
// we use isInstantiated() to check if the humanized attribute is set (empty)
protected dateTimeFormat(): string {
//! we should prefer not to use this functionality
// however, by allowing the programmer to specify both dateOnly and timeOnly to be true
// we can do reactive formats such as <baw-datetime [dateOnly]="isDateOnly" [timeOnly]="isTimeOnly">
// it also exhaustively fills all possible combinations of dateOnly and timeOnly, reducing the obfuscation of the code
// eg. without this condition, setting both dateOnly and timeOnly to true would result in the dateOnly format being used
// which is not obvious to the programmer without looking at the code
if (
isInstantiated(this.dateOnly) &&
this.dateOnly !== false &&
isInstantiated(this.timeOnly) &&
this.timeOnly !== false
) {
return "yyyy-MM-dd HH:mm:ss";
}

// we use isInstantiated() to check if the humanized attribute is set
// and we also check that it is not set to false
// by allowing boolean values, we support reactive formats (e.g. [dateOnly]="isDateOnly")
if (isInstantiated(this.dateOnly) && this.dateOnly !== false) {
Expand Down

This file was deleted.

Loading

0 comments on commit ab26495

Please sign in to comment.