Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Harvest UI tweaks #2033

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 89 additions & 89 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
"zone.js": "~0.11.6"
},
"devDependencies": {
"@angular-devkit/build-angular": "14.2.6",
"@angular-devkit/build-angular": "^14.2.10",
"@angular-eslint/builder": "14.0.2",
"@angular-eslint/eslint-plugin": "14.0.2",
"@angular-eslint/eslint-plugin-template": "14.0.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ import { defaultDebounceTime } from "src/app/app.helper";
template: `
<!-- Show site name and link if exists -->
<div *ngIf="site" class="site-label">
<a [bawUrl]="site.getViewUrl(project)">{{ site.name }}</a>
<a
[bawUrl]="site.getViewUrl(project)"
[ngbTooltip]="site?.isPoint ? 'Point' : 'Site' + ' id: ' + site?.id.toString()"
[disableTooltip]="site === null"
>{{ site.name }}</a>

<div>
<button
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { createComponentFactory, Spectator, SpectatorOverrides } from "@ngneat/spectator";
import { generateSite } from "@test/fakes/Site";
import { Site } from "@models/Site";
import { TimezoneInformation } from "@interfaces/apiInterfaces";
import { UTCOffsetSelectorComponent } from "./utc-offset-selector.component";

describe("UTCOffsetSelectorComponent", () => {
let spectator: Spectator<UTCOffsetSelectorComponent>;

const createHost = createComponentFactory({
component: UTCOffsetSelectorComponent,
});

function setup(props?: SpectatorOverrides<UTCOffsetSelectorComponent>) {
spectator = createHost(props);
spectator.detectChanges();
}

function getOffsetInput(): HTMLSelectElement {
return spectator.query<HTMLSelectElement>("select");
}

function getEditOffsetButton(): HTMLButtonElement {
// since there is only one button in this component, it is possible to directly query the button
return spectator.query<HTMLButtonElement>("button");
}

function getDisplayedUTCOffset(): string {
return spectator.query<HTMLDivElement>(".utc-label").innerText;
}

it("should create", () => {
setup();
expect(spectator.component).toBeInstanceOf(UTCOffsetSelectorComponent);
});

it("should show the correct placeholder text if there is not UTC offset specified", () => {
setup();

const expectedPlaceholderText = "Select offset";
expect(getOffsetInput().value).toEqual(expectedPlaceholderText);
});

it("should display all offsets with from smallest to largest if there is no site location specified", () => {
setup();
// the offset selector should contain the list of all possible offsets, with the placeholder text at the beginning
const expectedPlaceholderText = "Select offset";

const allUTCOffsets = String(UTCOffsetSelectorComponent.offsets).replace(/,/g, "\n");
const expectedValue = `${expectedPlaceholderText}\n${allUTCOffsets}`;

expect(getOffsetInput().innerText).toEqual(expectedValue);
});

it("should display an edit button if there is a offset already specified", () => {
setup({
props: {
offset: "+10:00",
}
});

expect(getEditOffsetButton()).toBeInstanceOf(HTMLButtonElement);
});

it("should show the correct offset if there is a offset specified", () => {
const expectedOffset = "+10:00";

setup({
props: {
offset: "+10:00",
}
});

expect(getDisplayedUTCOffset()).toEqual(expectedOffset);
});

it("should show relevant UTC offsets at the top of the offset list if a site with a location is set", () => {
const mockSite = new Site(generateSite({
timezoneInformation: {
identifierAlt: "Brisbane",
identifier: "Australia/Brisbane",
friendlyIdentifier: "Australia - Brisbane",
utcOffset: 36000,
utcTotalOffset: 36000,
} as TimezoneInformation,
}));

setup({
props: {
site: mockSite,
}
});

const expectedPlaceholderText = "Select offset";
const expectedRelevantOffsets = "+10:00\n";
const allUTCOffsets = String(UTCOffsetSelectorComponent.offsets).replace(/,/g, "\n");

const expectedValue = `${expectedPlaceholderText}\n${expectedRelevantOffsets}---\n${allUTCOffsets}`;

expect(getOffsetInput().innerText).toEqual(expectedValue);
});

// since the timezone converter tests are unit tests and do not rely on the GUI components
// there is no need to generate the component through the component factory
describe("timezone converter", () => {
const convertHourToUnixOffset = (hours: number) => hours * 3600;

function assertConversion(decimalHourOffset: number, expectedResult: string) {
const unixOffset = convertHourToUnixOffset(decimalHourOffset);

expect(spectator.component.convertUnixOffsetToUTCOffset(unixOffset)).toEqual(expectedResult);
}

it("should correctly process utc offsets of < +1 hour", () => {
assertConversion(1, "+01:00");
});

it("should correctly process utc offsets of < -1 hour", () => {
assertConversion(-1, "-01:00");
});

it("should correctly process known times (Brisbane)", () => {
assertConversion(10, "+10:00");
});

it("should correctly process known times (New York)", () => {
assertConversion(-5, "-05:00");
});

it("should correctly process a utc offset of + and -00:00", () => {
assertConversion(0, "+00:00");
assertConversion(-0, "+00:00");
});

it("should throw an error if the utc offset is incorrect (>= +12)", () => {
setup();
const offset = convertHourToUnixOffset(13);

expect(() => spectator.component.convertUnixOffsetToUTCOffset(offset)).toThrow(
new Error("UTC Offset out of bounds.")
);
});

it("should throw an error if the utc offset is incorrect (<= -12)", () => {
setup();
const offset = convertHourToUnixOffset(-12.1);

expect(() => spectator.component.convertUnixOffsetToUTCOffset(offset)).toThrow(
new Error("UTC Offset out of bounds.")
);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { isInstantiated } from "@helpers/isInstantiated/isInstantiated";
import { Site } from "@models/Site";

@Component({
selector: "baw-harvest-utc-offset-selector",
Expand All @@ -24,7 +26,7 @@ import { Component, EventEmitter, Input, Output } from "@angular/core";
(change)="onSelection($any($event).target.value)"
>
<option selected disabled>Select offset</option>
<option *ngFor="let offset of offsets" [value]="offset">
<option *ngFor="let offset of offsets" [value]="offset" [disabled]="offset === relevantOffsetListSeparator">
{{ offset }}
</option>
</select>
Expand All @@ -45,6 +47,10 @@ import { Component, EventEmitter, Input, Output } from "@angular/core";
],
})
export class UTCOffsetSelectorComponent {
protected relevantOffsetListSeparator = "---";

// the UTC input component needs knowledge of the site so that it can suggest the relevant UTC offsets relative to the site location
@Input() public site: Site;
@Input() public offset: string;
@Output() public offsetChange = new EventEmitter<string>();

Expand All @@ -62,8 +68,65 @@ export class UTCOffsetSelectorComponent {
this.offsetChange.emit(this.offset);
}

/**
* Returns the UTC offsets that are relevant to the site location
*/
public get relevantUTCOffsets(): string[] {
const foundRelevantOffsets = Array<string>();

// if the site or timezone information is not set, it can be assumed that there are no relevant / suggested time zones
if (isInstantiated(this.site?.timezoneInformation)) {
const utcOffset = this.site.timezoneInformation.utcOffset;
const totalUtcOffset = this.site.timezoneInformation.utcTotalOffset;

foundRelevantOffsets.push(this.convertUnixOffsetToUTCOffset(utcOffset));

// if the total offset is not equal to the utc offset, this is an indicator of two potential time offsets.
// e.g. daylight savings time
if (utcOffset !== totalUtcOffset) {
foundRelevantOffsets.push(this.convertUnixOffsetToUTCOffset(totalUtcOffset));
}

// add a separator that the user can not select to distinguish between relevant and all utc offsets
foundRelevantOffsets.push(this.relevantOffsetListSeparator);
}

return foundRelevantOffsets;
}

/**
* Returns a list of UTC offsets with the relevant offsets appended to the top
*/
public get offsets(): string[] {
return UTCOffsetSelectorComponent.offsets;
return this.relevantUTCOffsets.concat(UTCOffsetSelectorComponent.offsets);
}

public convertUnixOffsetToUTCOffset(unixOffset: number): string {
const directionalIndicator = unixOffset >= 0 ? "+" : "-";

// unix offset is in relative seconds. Therefore, if we divide the number by 3600, we get the offset as an hour decimal
const secondsToHoursScalarMultiple = 3600;

// assert that the provided dates are within the legal range. If not, throw an error
if (unixOffset >= (12 * secondsToHoursScalarMultiple) || unixOffset <= (-12 * secondsToHoursScalarMultiple)) {
throw new Error("UTC Offset out of bounds.");
}

const utcOffsetTime = new Date(0);
utcOffsetTime.setHours(unixOffset / secondsToHoursScalarMultiple);

let hoursTimeFormat = utcOffsetTime.getHours();

// since -1 is the same as +23, it will be encoded as +23 at this point
// however, since the user is expecting -1, subtracting 24 hours (if greater than 12 hours) will return the result the user is expecting
if (hoursTimeFormat > 12) {
hoursTimeFormat -= 24; // hours
}

const hours: string = directionalIndicator + hoursTimeFormat.toString().replace("-", "").padStart(2, "0");
const minutes: string = utcOffsetTime.getMinutes().toString().padStart(2, "0");

return `${hours}:${minutes}`;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
flex-basis: 1.8em;
transition: 0.1s;
overflow: hidden;

& > small {
padding-right: 1rem;
}
}
}
}
Expand All @@ -32,6 +36,10 @@
}
}

.file-name {
width: min-content;
}

.badge {
height: min-content;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { createComponentFactory, Spectator } from "@ngneat/spectator";
import { MetaReviewFile } from "@components/harvest/screens/metadata-review/metadata-review.component";
import { modelData } from "@test/helpers/faker";
import { HarvestItem, HarvestItemReport } from "@models/HarvestItem";
import { generateHarvestItem, generateHarvestReport } from "@test/fakes/HarvestItem";
import { NgbModule } from "@ng-bootstrap/ng-bootstrap";
import { WhitespaceComponent } from "./whitespace.component";
import { FileRowComponent } from "./file-row.component";

describe("FileRowComponent", () => {
let spectator: Spectator<FileRowComponent>;
let mockRow: MetaReviewFile;

const createHost = createComponentFactory({
component: FileRowComponent,
declarations: [WhitespaceComponent],
imports: [NgbModule],
});

function setup() {
spectator = createHost({
props: {
row: mockRow,
}
});

spyOnProperty(spectator.component, "mapping", "get").and.callFake(() => mockRow.mapping);
spyOnProperty(spectator.component, "harvestItem", "get").and.callFake(() => mockRow.harvestItem);

spectator.detectChanges();
}

function constructMockRow(data?: Partial<MetaReviewFile>): MetaReviewFile {
const mockFileName = modelData.system.filePath();

return {
path: mockFileName,
showValidations: true,
harvestItem: new HarvestItem(generateHarvestItem()),
...data
} as MetaReviewFile
}

function getRowFilePath(): string {
return spectator.query<HTMLElement>(".file-name").innerText;
}

function getDropdownIcon(): HTMLDivElement {
return spectator.query<HTMLDivElement>(".dropdown-icon");
}

beforeEach(() => mockRow = constructMockRow());

it("should create", () => {
setup();
expect(spectator.component).toBeInstanceOf(FileRowComponent);
});

it("should display the correct file path", () => {
setup();
expect(getRowFilePath()).toEqual(mockRow.path);
});

it("should show a drop down chevron for files with multiple validations", () => {
mockRow = constructMockRow({
harvestItem: new HarvestItem(
generateHarvestItem({
report: new HarvestItemReport(generateHarvestReport({
itemsFailed: 3,
})),
})
),
});

setup();

expect(getDropdownIcon()).toBeTruthy();
});

it("should not show a chevron for files with one validation", () => {
mockRow = constructMockRow({
harvestItem: new HarvestItem(
generateHarvestItem({
report: new HarvestItemReport(
generateHarvestReport({
itemsErrored: 0,
itemsInvalidFixable: 0,
itemsInvalidNotFixable: 0,
itemsFailed: 1,
})
),
})
),
});

setup();

expect(getDropdownIcon()).toBeFalsy();
});
});
Loading