diff --git a/package-lock.json b/package-lock.json index a044a2a93..1065d2e60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9379,6 +9379,11 @@ "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==", "dev": true }, + "line-truncation": { + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/line-truncation/-/line-truncation-1.3.9.tgz", + "integrity": "sha512-YFlsU6Zos5bJvnrLYY5Rh+kdiSvGtG1akUICe1xaF2ZqN6dKVHsY/tiq1lRuqQ8iF5LtuePn54jnyKQf4NRZDA==" + }, "loader-runner": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", @@ -10177,6 +10182,15 @@ "integrity": "sha512-H2d2XDQvwy4/2eeIAucX5JH/zrU8tAQATuZCe//MN06BxPs6aoa0lav7w8sdJRIgnTEUyfMitje2QI7XxuliqA==", "dev": true }, + "ngx-line-truncation": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/ngx-line-truncation/-/ngx-line-truncation-1.6.6.tgz", + "integrity": "sha512-UtOhloj1U45zx/R382m9kiWRKkdHIFE2iJk0146ef1NpEgMKYUROmXJ1RtK3ctwS032uMoUj1BUrxX/jiO8jjg==", + "requires": { + "line-truncation": "^1.3.9", + "tslib": "^2.0.0" + } + }, "ngx-toastr": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/ngx-toastr/-/ngx-toastr-13.0.0.tgz", diff --git a/package.json b/package.json index d6f1a9a8e..f75314dcb 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "just-camel-case": "^4.0.2", "just-snake-case": "^1.1.0", "luxon": "^1.25.0", + "ngx-line-truncation": "^1.6.6", "ngx-toastr": "^13.0.0", "rxjs": "^6.6.2", "snazzy-info-window": "^1.1.1", diff --git a/src/app/components/home/home.component.spec.ts b/src/app/components/home/home.component.spec.ts index 7c70a29a6..f1c19493f 100644 --- a/src/app/components/home/home.component.spec.ts +++ b/src/app/components/home/home.component.spec.ts @@ -1,8 +1,3 @@ -import { - HttpClientTestingModule, - HttpTestingController, -} from "@angular/common/http/testing"; -import { ComponentFixture, TestBed } from "@angular/core/testing"; import { RouterTestingModule } from "@angular/router/testing"; import { ApiErrorDetails } from "@baw-api/api.interceptor.service"; import { Filters } from "@baw-api/baw-api.service"; @@ -10,210 +5,169 @@ import { MockBawApiModule } from "@baw-api/baw-apiMock.module"; import { ProjectsService } from "@baw-api/project/projects.service"; import { SecurityService } from "@baw-api/security/security.service"; import { Project } from "@models/Project"; -import { SpyObject } from "@ngneat/spectator"; +import { + createComponentFactory, + Spectator, + SpyObject, +} from "@ngneat/spectator"; import { AppConfigService } from "@services/app-config/app-config.service"; -import { cmsRoot } from "@services/app-config/app-config.service.spec"; -import { SharedModule } from "@shared/shared.module"; +import { CardImageComponent } from "@shared/cards/card-image/card-image.component"; +import { CardsComponent } from "@shared/cards/cards.component"; +import { CmsComponent } from "@shared/cms/cms.component"; import { generateApiErrorDetails } from "@test/fakes/ApiErrorDetails"; import { generateProject } from "@test/fakes/Project"; import { nStepObservable } from "@test/helpers/general"; import { assertRoute } from "@test/helpers/html"; +import { MockComponent } from "ng-mocks"; import { Subject } from "rxjs"; import { HomeComponent } from "./home.component"; describe("HomeComponent", () => { - let httpMock: HttpTestingController; let projectApi: SpyObject; let securityApi: SecurityService; - let component: HomeComponent; - let env: AppConfigService; - let fixture: ComponentFixture; - let cmsUrl: string; - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [HomeComponent], - imports: [ - SharedModule, - HttpClientTestingModule, - RouterTestingModule, - MockBawApiModule, - ], - }).compileComponents(); - - fixture = TestBed.createComponent(HomeComponent); - component = fixture.componentInstance; - httpMock = TestBed.inject(HttpTestingController); - projectApi = TestBed.inject(ProjectsService) as SpyObject; - securityApi = TestBed.inject(SecurityService); - env = TestBed.inject(AppConfigService); - - cmsUrl = `${cmsRoot}/home.html`; + let config: AppConfigService; + let spectator: Spectator; + const createComponent = createComponentFactory({ + component: HomeComponent, + declarations: [ + CardsComponent, + MockComponent(CardImageComponent), + MockComponent(CmsComponent), + ], + imports: [RouterTestingModule, MockBawApiModule], }); - afterEach(() => { - httpMock.verify(); - }); + async function interceptProjects( + projects: Project[] = [], + error?: ApiErrorDetails + ) { + const subject = new Subject(); + const promise = nStepObservable( + subject, + () => (error ? error : projects), + !projects + ); + projectApi.filter.and.callFake(() => subject); + spectator.detectChanges(); + await promise; + spectator.detectChanges(); + } - function interceptCmsRequest() { - const req = httpMock.expectOne(cmsUrl); - req.flush("

Test Header

Test Description

"); + function getCardImages() { + return spectator.queryAll(CardImageComponent); } - it("should load cms", async () => { - const subject = new Subject(); - const promise = nStepObservable(subject, () => []); - projectApi.filter.and.callFake(() => subject); + function getButton() { + return spectator.query("button"); + } - await promise; - fixture.detectChanges(); - interceptCmsRequest(); - fixture.detectChanges(); + beforeEach(() => { + spectator = createComponent({ detectChanges: false }); - const header = fixture.nativeElement.querySelector("h1"); - const body = fixture.nativeElement.querySelector("p"); + projectApi = spectator.inject(ProjectsService); + securityApi = spectator.inject(SecurityService); + config = spectator.inject(AppConfigService); + }); - expect(header).toBeTruthy(); - expect(header.innerText.trim()).toBe("Test Header"); - expect(body).toBeTruthy(); - expect(body.innerText.trim()).toBe("Test Description"); + it("should load cms", async () => { + await interceptProjects(); + const cms = spectator.query(CmsComponent); + expect(cms.page).toBe("/home.html"); }); - describe("page", () => { - async function setupComponent( - projects: Project[], - error?: ApiErrorDetails - ) { - const subject = new Subject(); - const promise = nStepObservable( - subject, - () => (projects ? projects : error), - !projects - ); - projectApi.filter.and.callFake(() => subject); - - fixture.detectChanges(); - await promise; - interceptCmsRequest(); - fixture.detectChanges(); - } - - function getCardImages() { - return fixture.nativeElement.querySelectorAll("baw-card-image"); - } - - function getCardTitle(card: HTMLElement): HTMLElement { - return card.querySelector(".card-title"); - } - - function getCardText(card: HTMLElement): HTMLElement { - return card.querySelector(".card-text"); - } - - function getButton() { - return fixture.nativeElement.querySelector("button"); - } - - it("should create", async () => { - await setupComponent([]); - expect(component).toBeTruthy(); - }); + it("should create", async () => { + await interceptProjects(); + expect(spectator.component).toBeTruthy(); + }); - it("should request 3 projects", async () => { - await setupComponent([]); - expect(projectApi.filter).toHaveBeenCalledWith({ - paging: { items: 3 }, - } as Filters); - }); + it("should request 3 projects", async () => { + await interceptProjects(); + expect(projectApi.filter).toHaveBeenCalledWith({ + paging: { items: 3 }, + } as Filters); + }); - it("should handle filter error", async () => { - await setupComponent(undefined, generateApiErrorDetails()); - expect(getCardImages().length).toBe(0); - expect(getButton()).toBeTruthy(); - }); + it("should handle filter error", async () => { + await interceptProjects(undefined, generateApiErrorDetails()); + expect(getCardImages().length).toBe(0); + expect(getButton()).toBeTruthy(); + }); - it("should display no projects", async () => { - await setupComponent([]); - expect(getCardImages().length).toBe(0); - expect(getButton()).toBeTruthy(); - }); + it("should display no projects", async () => { + await interceptProjects([]); + expect(getCardImages().length).toBe(0); + expect(getButton()).toBeTruthy(); + }); - it("should display single project", async () => { - await setupComponent([new Project(generateProject())]); + it("should display single project", async () => { + await interceptProjects([new Project(generateProject())]); + expect(getCardImages().length).toBe(1); + expect(getButton()).toBeTruthy(); + }); - const cards = getCardImages(); - expect(cards.length).toBe(1); - expect(getButton()).toBeTruthy(); - }); + it("should display project name", async () => { + await interceptProjects([ + new Project({ ...generateProject(), name: "Project" }), + ]); - it("should display project name", async () => { - await setupComponent([ - new Project({ ...generateProject(), name: "Project" }), - ]); + const cards = getCardImages(); + expect(cards[0].card.title).toBe("Project"); + }); - const cards = getCardImages(); - expect(getCardTitle(cards[0]).innerText.trim()).toBe("Project"); - }); + it("should display description", async () => { + await interceptProjects([ + new Project({ + ...generateProject(), + descriptionHtmlTagline: "Description", + }), + ]); - it("should display description", async () => { - await setupComponent([ - new Project({ - ...generateProject(), - description: "Description", - }), - ]); + const cards = getCardImages(); + expect(cards[0].card.description).toBe("Description"); + }); - const cards = getCardImages(); - expect(getCardText(cards[0]).innerText.trim()).toBe("Description"); - }); + it("should display missing description", async () => { + await interceptProjects([ + new Project({ + ...generateProject(), + descriptionHtmlTagline: undefined, + }), + ]); - it("should display missing description", async () => { - await setupComponent([ - new Project({ - ...generateProject(), - description: undefined, - }), - ]); - - const cards = getCardImages(); - expect(getCardText(cards[0]).innerText.trim()).toBe( - "No description given" - ); - }); + const cards = getCardImages(); + expect(cards[0].card.description).toBe(undefined); + }); - it("should display multiple projects", async () => { - const ids = [1, 2, 3]; - const names = ids.map((id) => `Project ${id}`); - const descriptions = ids.map((id) => `Description ${id}`); - await setupComponent( - ids.map( - (id, index) => - new Project({ - ...generateProject(id), - name: names[index], - description: descriptions[index], - }) - ) - ); - - const cards = getCardImages(); - expect(cards.length).toBe(ids.length); - expect(getButton()).toBeTruthy(); - ids.forEach((_, index) => { - expect(getCardTitle(cards[index]).innerText.trim()).toBe(names[index]); - expect(getCardText(cards[index]).innerText.trim()).toBe( - descriptions[index] - ); - }); + it("should display multiple projects", async () => { + const ids = [1, 2, 3]; + const names = ids.map((id) => `Project ${id}`); + const descriptions = ids.map((id) => `Description ${id}`); + await interceptProjects( + ids.map( + (id, index) => + new Project({ + ...generateProject(id), + name: names[index], + descriptionHtmlTagline: descriptions[index], + }) + ) + ); + + const cards = getCardImages(); + expect(cards.length).toBe(ids.length); + expect(getButton()).toBeTruthy(); + ids.forEach((_, index) => { + expect(cards[index].card.title).toBe(names[index]); + expect(cards[index].card.description).toBe(descriptions[index]); }); + }); - it("should link to project details page", async () => { - await setupComponent([]); + it("should link to project details page", async () => { + await interceptProjects([]); - const button = getButton(); - expect(button).toBeTruthy(); - expect(button.innerText.trim()).toBe("More Projects"); - assertRoute(button, "/projects"); - }); + const button = getButton(); + expect(button).toBeTruthy(); + expect(button.innerText.trim()).toBe("More Projects"); + assertRoute(button, "/projects"); }); }); diff --git a/src/app/components/projects/pages/assign/assign.component.ts b/src/app/components/projects/pages/assign/assign.component.ts index 7ec13bd54..87bdd51fb 100644 --- a/src/app/components/projects/pages/assign/assign.component.ts +++ b/src/app/components/projects/pages/assign/assign.component.ts @@ -33,7 +33,7 @@ class AssignComponent extends PagedTableTemplate { public sortKeys = { siteId: "id", name: "name", - description: "description", + description: "descriptionHtmlTagline", }; constructor(api: ShallowSitesService, route: ActivatedRoute) { @@ -43,7 +43,7 @@ class AssignComponent extends PagedTableTemplate { sites.map((site) => ({ siteId: site.id, name: site.name, - description: site.description, + description: site.descriptionHtmlTagline, })), route ); diff --git a/src/app/components/projects/pages/details/details.component.html b/src/app/components/projects/pages/details/details.component.html index ab414fa95..93035ff64 100644 --- a/src/app/components/projects/pages/details/details.component.html +++ b/src/app/components/projects/pages/details/details.component.html @@ -8,7 +8,7 @@

{{ project.name }}

-

{{ project.description }}

+

diff --git a/src/app/components/projects/pages/details/details.component.spec.ts b/src/app/components/projects/pages/details/details.component.spec.ts index 822fdff68..0d53430b3 100644 --- a/src/app/components/projects/pages/details/details.component.spec.ts +++ b/src/app/components/projects/pages/details/details.component.spec.ts @@ -141,10 +141,10 @@ describe("ProjectDetailsComponent", () => { assertImage(image, "http://brokenlink/", "Test Project image"); }); - it("should display description", () => { + it("should display description with html markup", () => { const project = new Project({ ...generateProject(), - description: "A test project", + descriptionHtml: "A test project", }); configureTestingModule(project, undefined, defaultSites, undefined); @@ -154,7 +154,7 @@ describe("ProjectDetailsComponent", () => { "p#project_description" ); expect(description).toBeTruthy(); - expect(description.innerText).toBe("A test project"); + expect(description.innerHTML).toBe("A test project"); }); }); diff --git a/src/app/components/projects/pages/list/list.component.spec.ts b/src/app/components/projects/pages/list/list.component.spec.ts index 904f6186e..a62dd0e48 100644 --- a/src/app/components/projects/pages/list/list.component.spec.ts +++ b/src/app/components/projects/pages/list/list.component.spec.ts @@ -47,7 +47,7 @@ describe("ProjectsListComponent", () => { } function assertCardDescription(card: any, description: string) { - expect(card.querySelector(".card-text").innerText.trim()).toBe(description); + expect(card.querySelector(".card-text").innerHTML.trim()).toBe(description); } it("should handle zero projects", () => { @@ -82,7 +82,10 @@ describe("ProjectsListComponent", () => { it("should display single project card with default description", () => { const projects = [ - new Project({ ...generateProject(), description: undefined }), + new Project({ + ...generateProject(), + descriptionHtmlTagline: undefined, + }), ]; configureTestingModule(projects, undefined); fixture.detectChanges(); @@ -92,12 +95,17 @@ describe("ProjectsListComponent", () => { }); it("should display single project card with custom description", () => { - const projects = [new Project(generateProject())]; + const projects = [ + new Project({ + ...generateProject(), + descriptionHtmlTagline: "Custom Description", + }), + ]; configureTestingModule(projects, undefined); fixture.detectChanges(); const cards = getCards(); - assertCardDescription(cards[0], projects[0].description); + assertCardDescription(cards[0], "Custom Description"); }); it("should display multiple project cards", () => { diff --git a/src/app/components/projects/project.schema.json b/src/app/components/projects/project.schema.json index 36f78585e..e97e068fc 100644 --- a/src/app/components/projects/project.schema.json +++ b/src/app/components/projects/project.schema.json @@ -15,7 +15,8 @@ "templateOptions": { "label": "Description", "required": false, - "rows": 8 + "rows": 8, + "description": "Description uses markdown formatting allowing you to apply some limited styling to the model. You can find a guide on the basics here: https://markdown-guide.readthedocs.io/en/latest/basics.html" } }, { diff --git a/src/app/components/shared/cards/card-image/card-image.component.html b/src/app/components/shared/cards/card-image/card-image.component.html deleted file mode 100644 index c23d1b743..000000000 --- a/src/app/components/shared/cards/card-image/card-image.component.html +++ /dev/null @@ -1,45 +0,0 @@ -
- - - - - - - - - - - - - - - - - - - - -
- -

- - - {{ card.title }} - - - - - {{ card.title }} - - - - - {{ card.title }} -

- - -

- {{ card.description ? card.description : "No description given" }} -

-
-
diff --git a/src/app/components/shared/cards/card-image/card-image.component.spec.ts b/src/app/components/shared/cards/card-image/card-image.component.spec.ts index 6ac3b3d32..923f187bc 100644 --- a/src/app/components/shared/cards/card-image/card-image.component.spec.ts +++ b/src/app/components/shared/cards/card-image/card-image.component.spec.ts @@ -7,8 +7,14 @@ import { AbstractModel } from "@models/AbstractModel"; import { createComponentFactory, Spectator } from "@ngneat/spectator"; import { assetRoot } from "@services/app-config/app-config.service"; import { modelData } from "@test/helpers/faker"; -import { assertHref, assertImage, assertRoute } from "@test/helpers/html"; +import { + assertHref, + assertImage, + assertRoute, + assertTruncation, +} from "@test/helpers/html"; import { websiteHttpUrl } from "@test/helpers/url"; +import { LineTruncationLibModule } from "ngx-line-truncation"; import { Card } from "../cards.component"; import { CardImageComponent } from "./card-image.component"; @@ -33,8 +39,9 @@ describe("CardImageComponent", () => { imports: [ HttpClientTestingModule, RouterTestingModule, - AuthenticatedImageModule, MockBawApiModule, + AuthenticatedImageModule, + LineTruncationLibModule, ], }); @@ -76,7 +83,7 @@ describe("CardImageComponent", () => { ); }); - it("should handle remote image", () => { + it("should display remote image", () => { const baseUrl = "https://broken_link/broken_link"; spectator.setInput("card", { title: "custom title", @@ -87,21 +94,41 @@ describe("CardImageComponent", () => { assertImage(image, baseUrl + "/300/300", "custom title image"); }); - it("should handle description", () => { + it("should have default description when none provided", () => { + spectator.setInput("card", { ...defaultCard, description: undefined }); + spectator.component.ngOnChanges(); + + const description = spectator.query(".card-text"); + expect(description.textContent).toContain("No description given"); + }); + + it("should have description when provided", () => { spectator.setInput("card", { ...defaultCard, description: "description" }); + spectator.component.ngOnChanges(); const description = spectator.query(".card-text"); expect(description.textContent).toContain("description"); }); - it("should have image href", () => { + it("should shorten description when description is long", () => { + spectator.setInput("card", { + ...defaultCard, + description: modelData.descriptionLong(), + }); + spectator.component.ngOnChanges(); + + const description = spectator.query(".card-text"); + assertTruncation(description, 4); + }); + + it("should have image href when link provided", () => { spectator.setInput("card", { ...defaultCard, link: "https://link/" }); const link = spectator.query("a img").parentElement as HTMLAnchorElement; assertHref(link, "https://link/"); }); - it("should have title href", () => { + it("should have title href when link provided", () => { spectator.setInput("card", { ...defaultCard, title: "title", @@ -112,14 +139,14 @@ describe("CardImageComponent", () => { assertHref(link, "https://link/"); }); - it("should have image route", () => { + it("should have image route when route provided", () => { spectator.setInput("card", { ...defaultCard, route: "/broken_link" }); const route = spectator.query("a img").parentElement as HTMLAnchorElement; assertRoute(route, "/broken_link"); }); - it("should have title route", () => { + it("should have title route when route provided", () => { spectator.setInput("card", { ...defaultCard, title: "title", diff --git a/src/app/components/shared/cards/card-image/card-image.component.ts b/src/app/components/shared/cards/card-image/card-image.component.ts index 0c0109ee5..de26f80eb 100644 --- a/src/app/components/shared/cards/card-image/card-image.component.ts +++ b/src/app/components/shared/cards/card-image/card-image.component.ts @@ -1,8 +1,9 @@ import { ChangeDetectionStrategy, + ChangeDetectorRef, Component, Input, - OnInit, + OnChanges, } from "@angular/core"; import { Card } from "../cards.component"; @@ -11,14 +12,69 @@ import { Card } from "../cards.component"; */ @Component({ selector: "baw-card-image", - templateUrl: "./card-image.component.html", styleUrls: ["./card-image.component.scss"], + template: ` +
+ + + + + + + + + + + + + + + + + + + + +
+ +

+ + + {{ card.title }} + + + + + {{ card.title }} + + + + + {{ card.title }} +

+ + +

+
+
+ `, changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CardImageComponent implements OnInit { +export class CardImageComponent implements OnChanges { @Input() public card: Card; + public description: string; - constructor() {} + constructor(private ref: ChangeDetectorRef) {} - public ngOnInit() {} + public ngOnChanges() { + this.description = this.card.description + ? this.card.description + : "No description given"; + this.ref.detectChanges(); + } } diff --git a/src/app/components/shared/cards/card/card.component.spec.ts b/src/app/components/shared/cards/card/card.component.spec.ts index 78f44714a..0ef36b11e 100644 --- a/src/app/components/shared/cards/card/card.component.spec.ts +++ b/src/app/components/shared/cards/card/card.component.spec.ts @@ -1,10 +1,15 @@ import { createRoutingFactory, SpectatorRouting } from "@ngneat/spectator"; -import { assertHref, assertRoute } from "@test/helpers/html"; +import { modelData } from "@test/helpers/faker"; +import { assertHref, assertRoute, assertTruncation } from "@test/helpers/html"; +import { LineTruncationLibModule } from "ngx-line-truncation"; import { CardComponent } from "./card.component"; describe("CardComponent", () => { let spectator: SpectatorRouting; - const createComponent = createRoutingFactory(CardComponent); + const createComponent = createRoutingFactory({ + component: CardComponent, + imports: [LineTruncationLibModule], + }); beforeEach(() => (spectator = createComponent({ detectChanges: false }))); @@ -29,6 +34,7 @@ describe("CardComponent", () => { it("should have default description when none provided", () => { spectator.setInput("card", { title: "title" }); + spectator.component.ngOnChanges(); const description = spectator.query("p"); expect(description.textContent).toContain("No description given"); @@ -36,11 +42,23 @@ describe("CardComponent", () => { it("should have description when provided", () => { spectator.setInput("card", { title: "title", description: "description" }); + spectator.component.ngOnChanges(); const description = spectator.query("p"); expect(description.textContent).toContain("description"); }); + it("should shorten description when description is long", () => { + spectator.setInput("card", { + title: "title", + description: modelData.descriptionLong(), + }); + spectator.component.ngOnChanges(); + + const description = spectator.query("p"); + assertTruncation(description, 4); + }); + it("should create href link", () => { spectator.setInput("card", { title: "title", link: "https://brokenlink/" }); diff --git a/src/app/components/shared/cards/card/card.component.ts b/src/app/components/shared/cards/card/card.component.ts index 2306875d4..ed76da5a1 100644 --- a/src/app/components/shared/cards/card/card.component.ts +++ b/src/app/components/shared/cards/card/card.component.ts @@ -1,8 +1,9 @@ import { ChangeDetectionStrategy, + ChangeDetectorRef, Component, Input, - OnInit, + OnChanges, } from "@angular/core"; import { Card } from "../cards.component"; @@ -30,18 +31,27 @@ import { Card } from "../cards.component"; {{ card.title }}
-

- {{ card.description ? card.description : "No description given" }} -

+

`, changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CardComponent implements OnInit { +export class CardComponent implements OnChanges { @Input() public card: Card; + public description: string; - constructor() {} + constructor(private ref: ChangeDetectorRef) {} - public ngOnInit() {} + public ngOnChanges() { + this.description = this.card.description + ? this.card.description + : "No description given"; + this.ref.detectChanges(); + } } diff --git a/src/app/components/shared/cards/cards.module.ts b/src/app/components/shared/cards/cards.module.ts index 763cd000d..25ca66b7c 100644 --- a/src/app/components/shared/cards/cards.module.ts +++ b/src/app/components/shared/cards/cards.module.ts @@ -2,6 +2,7 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { RouterModule } from "@angular/router"; import { AuthenticatedImageModule } from "@directives/image/image.module"; +import { LineTruncationLibModule } from "ngx-line-truncation"; import { CardImageComponent } from "./card-image/card-image.component"; import { CardComponent } from "./card/card.component"; import { CardsComponent } from "./cards.component"; @@ -11,7 +12,12 @@ import { CardsComponent } from "./cards.component"; */ @NgModule({ declarations: [CardsComponent, CardComponent, CardImageComponent], - imports: [CommonModule, RouterModule, AuthenticatedImageModule], + imports: [ + CommonModule, + RouterModule, + AuthenticatedImageModule, + LineTruncationLibModule, + ], exports: [CardsComponent], }) export class CardsModule {} diff --git a/src/app/components/shared/cms/cms.component.ts b/src/app/components/shared/cms/cms.component.ts index 2bb45a67c..72423a712 100644 --- a/src/app/components/shared/cms/cms.component.ts +++ b/src/app/components/shared/cms/cms.component.ts @@ -50,6 +50,8 @@ export class CmsComponent extends WithUnsubscribe() implements OnInit { .pipe(takeUntil(this.unsubscribe)) .subscribe( (data) => { + // TODO Validate if this is needed? + // https://www.intricatecloud.io/2019/10/using-angular-innerhtml-to-display-user-generated-content-without-sacrificing-security/ // This is a bit dangerous, however CMS should only load from trusted sources. // May need to revise this in future. this.blob = this.sanitizer.bypassSecurityTrustHtml(data); diff --git a/src/app/components/shared/header/header.component.spec.ts b/src/app/components/shared/header/header.component.spec.ts index 7709a3931..143a1c5ba 100644 --- a/src/app/components/shared/header/header.component.spec.ts +++ b/src/app/components/shared/header/header.component.spec.ts @@ -9,6 +9,7 @@ import { Router } from "@angular/router"; import { RouterTestingModule } from "@angular/router/testing"; import { MockBawApiModule } from "@baw-api/baw-apiMock.module"; import { SecurityService } from "@baw-api/security/security.service"; +import { ImageSizes } from "@interfaces/apiInterfaces"; import { SessionUser } from "@models/User"; import { AppConfigService, @@ -243,19 +244,19 @@ describe("HeaderComponent", () => { ...user, imageUrls: [ { - size: "medium", + size: ImageSizes.MEDIUM, url: modelData.image.imageUrl(140, 140), width: 140, height: 140, }, { - size: "small", + size: ImageSizes.SMALL, url, width: 60, height: 60, }, { - size: "tiny", + size: ImageSizes.TINY, url: modelData.image.imageUrl(30, 30), width: 30, height: 30, diff --git a/src/app/components/shared/shared.components.ts b/src/app/components/shared/shared.components.ts index 69d7fae7b..6031c4c9b 100644 --- a/src/app/components/shared/shared.components.ts +++ b/src/app/components/shared/shared.components.ts @@ -8,6 +8,7 @@ import { FormlyBootstrapModule } from "@ngx-formly/bootstrap"; import { FormlyModule } from "@ngx-formly/core"; import { LoadingBarHttpClientModule } from "@ngx-loading-bar/http-client"; import { NgxDatatableModule } from "@swimlane/ngx-datatable"; +import { LineTruncationLibModule } from "ngx-line-truncation"; import { ToastrModule } from "ngx-toastr"; import { DirectivesModule } from "src/app/directives/directives.module"; import { ActionMenuComponent } from "./action-menu/action-menu.component"; @@ -65,3 +66,5 @@ export const sharedModules = [ LoadingModule, IndicatorModule, ]; + +export const internalModules = [...sharedModules, LineTruncationLibModule]; diff --git a/src/app/components/shared/shared.module.ts b/src/app/components/shared/shared.module.ts index 2ae0e641c..fbb63fc77 100644 --- a/src/app/components/shared/shared.module.ts +++ b/src/app/components/shared/shared.module.ts @@ -1,8 +1,10 @@ import { NgModule } from "@angular/core"; import { FaIconLibrary } from "@fortawesome/angular-fontawesome"; +import { LineTruncationDirective } from "ngx-line-truncation"; import { fontAwesomeLibraries } from "src/app/app.helper"; import { internalComponents, + internalModules, sharedComponents, sharedModules, } from "./shared.components"; @@ -12,8 +14,8 @@ import { */ @NgModule({ declarations: internalComponents, - imports: sharedModules, - exports: [...sharedModules, ...sharedComponents], + imports: internalModules, + exports: [...sharedModules, ...sharedComponents, LineTruncationDirective], }) export class SharedModule { constructor(library: FaIconLibrary) { diff --git a/src/app/components/sites/pages/details/details.component.html b/src/app/components/sites/pages/details/details.component.html index 8804d5cbb..014232539 100644 --- a/src/app/components/sites/pages/details/details.component.html +++ b/src/app/components/sites/pages/details/details.component.html @@ -13,7 +13,7 @@

-

{{ site.description }}

+

diff --git a/src/app/components/sites/pages/details/details.component.spec.ts b/src/app/components/sites/pages/details/details.component.spec.ts index 2d7202a8c..ce01c928f 100644 --- a/src/app/components/sites/pages/details/details.component.spec.ts +++ b/src/app/components/sites/pages/details/details.component.spec.ts @@ -102,7 +102,7 @@ describe("SitesDetailsComponent", () => { describe("Project", () => { it("should display project name", () => { const project = new Project({ - id: 1, + ...generateSite(), name: "Custom Project", }); configureTestingModule(project, undefined, defaultSite, undefined); @@ -117,7 +117,7 @@ describe("SitesDetailsComponent", () => { describe("Site", () => { it("should display site name", () => { const site = new Site({ - id: 1, + ...generateSite(), name: "Custom Site", }); configureTestingModule(defaultProject, undefined, site, undefined); @@ -130,8 +130,9 @@ describe("SitesDetailsComponent", () => { it("should display default site image", () => { const site = new Site({ - id: 1, + ...generateSite(), name: "Site", + imageUrl: undefined, }); configureTestingModule(defaultProject, undefined, site, undefined); fixture.detectChanges(); @@ -146,7 +147,7 @@ describe("SitesDetailsComponent", () => { it("should display custom site image", () => { const site = new Site({ - id: 1, + ...generateSite(), name: "Site", imageUrl: "http://brokenlink/", }); @@ -157,11 +158,10 @@ describe("SitesDetailsComponent", () => { assertImage(image, "http://brokenlink/", "Site image"); }); - it("should display site description", () => { + it("should display site description with html markup", () => { const site = new Site({ - id: 1, - name: "Site", - description: "Custom Description", + ...generateSite(), + descriptionHtml: "Custom Description", }); configureTestingModule(defaultProject, undefined, site, undefined); fixture.detectChanges(); @@ -170,7 +170,7 @@ describe("SitesDetailsComponent", () => { "p#site_description" ); expect(description).toBeTruthy(); - expect(description.innerText).toContain("Custom Description"); + expect(description.innerHTML).toContain("Custom Description"); }); }); diff --git a/src/app/components/sites/site.base.json b/src/app/components/sites/site.base.json index 45550d626..1c71f34e5 100644 --- a/src/app/components/sites/site.base.json +++ b/src/app/components/sites/site.base.json @@ -22,7 +22,8 @@ "templateOptions": { "label": "Description", "required": false, - "rows": 8 + "rows": 8, + "description": "Description uses markdown formatting allowing you to apply some limited styling to the model. You can find a guide on the basics here: https://markdown-guide.readthedocs.io/en/latest/basics.html" } }, { diff --git a/src/app/interfaces/apiInterfaces.ts b/src/app/interfaces/apiInterfaces.ts index 5246a5822..d13760eb1 100644 --- a/src/app/interfaces/apiInterfaces.ts +++ b/src/app/interfaces/apiInterfaces.ts @@ -31,6 +31,11 @@ export type UserName = string; */ export type AuthToken = string; +/** + * BAW API Access Levels + */ +export type AccessLevel = "Reader" | "Writer" | "Owner"; + /** * BAW API Item Description */ @@ -88,14 +93,7 @@ export interface TimezoneInformation { * BAW API Image Details */ export interface ImageUrl { - size: - | "extralarge" - | "large" - | "medium" - | "small" - | "tiny" - | "default" - | "unknown"; + size: ImageSizes; url: string; height?: number; width?: number; @@ -124,3 +122,28 @@ export function isImageUrl(value: any): value is ImageUrl { keys.includes("url") ); } + +export interface HasCreator { + creatorId?: Id; + createdAt?: DateTimeTimezone | string; +} + +export interface HasUpdater { + updaterId?: Id; + updatedAt?: DateTimeTimezone | string; +} + +export interface HasDeleter { + deleterId?: Id; + deletedAt?: DateTimeTimezone | string; +} + +export interface HasDescription { + description?: Description; + descriptionHtml?: Description; + descriptionHtmlTagline?: Description; +} + +export type HasAllUsers = HasCreator & HasUpdater & HasDeleter; +export type HasCreatorAndUpdater = HasCreator & HasUpdater; +export type HasCreatorAndDeleter = HasCreator & HasDeleter; diff --git a/src/app/models/AnalysisJob.ts b/src/app/models/AnalysisJob.ts index 0ef715100..4f8c7e9ee 100644 --- a/src/app/models/AnalysisJob.ts +++ b/src/app/models/AnalysisJob.ts @@ -4,6 +4,8 @@ import { Duration } from "luxon"; import { DateTimeTimezone, Description, + HasAllUsers, + HasDescription, Id, Param, } from "../interfaces/apiInterfaces"; @@ -21,19 +23,12 @@ import type { User } from "./User"; /** * An analysis job model. */ -export interface IAnalysisJob { +export interface IAnalysisJob extends HasAllUsers, HasDescription { id?: Id; name?: Param; annotationName?: string; customSettings?: Blob | object; scriptId?: Id; - creatorId?: Id; - updaterId?: Id; - deleterId?: Id; - createdAt?: DateTimeTimezone | string; - updatedAt?: DateTimeTimezone | string; - deletedAt?: DateTimeTimezone | string; - description?: Description; savedSearchId?: Id; startedAt?: DateTimeTimezone | string; overallStatus?: AnalysisJobStatus; @@ -57,6 +52,8 @@ export class AnalysisJob extends AbstractModel implements IAnalysisJob { public readonly customSettings?: Blob; @BawPersistAttr public readonly description?: Description; + public readonly descriptionHtml?: Description; + public readonly descriptionHtmlTagline?: Description; public readonly scriptId?: Id; public readonly creatorId?: Id; public readonly updaterId?: Id; diff --git a/src/app/models/AudioEvent.ts b/src/app/models/AudioEvent.ts index 076fdf7bc..b35062321 100644 --- a/src/app/models/AudioEvent.ts +++ b/src/app/models/AudioEvent.ts @@ -1,13 +1,13 @@ import { Injector } from "@angular/core"; import { AUDIO_RECORDING } from "@baw-api/ServiceTokens"; -import { DateTimeTimezone, Id } from "@interfaces/apiInterfaces"; +import { DateTimeTimezone, HasAllUsers, Id } from "@interfaces/apiInterfaces"; import { AbstractModel } from "./AbstractModel"; import { Creator, Deleter, HasOne, Updater } from "./AssociationDecorators"; import { BawDateTime, BawPersistAttr } from "./AttributeDecorators"; import type { AudioRecording } from "./AudioRecording"; import type { User } from "./User"; -export interface IAudioEvent { +export interface IAudioEvent extends HasAllUsers { id?: Id; audioRecordingId?: Id; startTimeSeconds?: number; @@ -15,12 +15,6 @@ export interface IAudioEvent { lowFrequencyHertz?: number; highFrequencyHertz?: number; isReference?: boolean; - creatorId?: Id; - updaterId?: Id; - deleterId?: Id; - createdAt?: DateTimeTimezone | string; - updatedAt?: DateTimeTimezone | string; - deletedAt?: DateTimeTimezone | string; } export class AudioEvent extends AbstractModel implements IAudioEvent { diff --git a/src/app/models/AudioRecording.ts b/src/app/models/AudioRecording.ts index b746dd06e..9cbec3bdc 100644 --- a/src/app/models/AudioRecording.ts +++ b/src/app/models/AudioRecording.ts @@ -1,7 +1,12 @@ import { Injector } from "@angular/core"; import { ACCOUNT, SHALLOW_SITE } from "@baw-api/ServiceTokens"; import { Duration } from "luxon"; -import { DateTimeTimezone, Id, Uuid } from "../interfaces/apiInterfaces"; +import { + DateTimeTimezone, + HasAllUsers, + Id, + Uuid, +} from "../interfaces/apiInterfaces"; import { AbstractModel } from "./AbstractModel"; import { Creator, Deleter, HasOne, Updater } from "./AssociationDecorators"; import { BawDateTime, BawDuration } from "./AttributeDecorators"; @@ -11,7 +16,7 @@ import type { User } from "./User"; /** * An audio recording model */ -export interface IAudioRecording { +export interface IAudioRecording extends HasAllUsers { id?: Id; uuid?: Uuid; uploaderId?: Id; @@ -26,12 +31,6 @@ export interface IAudioRecording { fileHash?: string; status?: AudioRecordingStatus; notes?: Blob | any; - creatorId?: Id; - updaterId?: Id; - deleterId?: Id; - createdAt?: DateTimeTimezone | string; - updatedAt?: DateTimeTimezone | string; - deletedAt?: DateTimeTimezone | string; originalFileName?: string; recordedUtcOffset?: string; } diff --git a/src/app/models/Bookmark.ts b/src/app/models/Bookmark.ts index d56936152..bdfd88667 100644 --- a/src/app/models/Bookmark.ts +++ b/src/app/models/Bookmark.ts @@ -3,6 +3,8 @@ import { AUDIO_RECORDING } from "@baw-api/ServiceTokens"; import { DateTimeTimezone, Description, + HasCreatorAndUpdater, + HasDescription, Id, Param, } from "@interfaces/apiInterfaces"; @@ -15,16 +17,11 @@ import type { User } from "./User"; /** * A bookmark model. */ -export interface IBookmark { +export interface IBookmark extends HasCreatorAndUpdater, HasDescription { id?: Id; audioRecordingId?: Id; offsetSeconds?: number; name?: Param; - creatorId?: Id; - updaterId?: Id; - createdAt?: DateTimeTimezone | string; - updatedAt?: DateTimeTimezone | string; - description?: Description; category?: string; } @@ -46,6 +43,8 @@ export class Bookmark extends AbstractModel implements IBookmark { public readonly updatedAt?: DateTimeTimezone; @BawPersistAttr public readonly description?: Description; + public readonly descriptionHtml?: Description; + public readonly descriptionHtmlTagline?: Description; @BawPersistAttr public readonly category?: string; diff --git a/src/app/models/Dataset.ts b/src/app/models/Dataset.ts index 74f46be35..69f1da389 100644 --- a/src/app/models/Dataset.ts +++ b/src/app/models/Dataset.ts @@ -2,6 +2,8 @@ import { Injector } from "@angular/core"; import { DateTimeTimezone, Description, + HasCreatorAndUpdater, + HasDescription, Id, Param, } from "@interfaces/apiInterfaces"; @@ -10,14 +12,9 @@ import { Creator, Updater } from "./AssociationDecorators"; import { BawDateTime, BawPersistAttr } from "./AttributeDecorators"; import type { User } from "./User"; -export interface IDataset { +export interface IDataset extends HasCreatorAndUpdater, HasDescription { id?: Id; - creatorId?: Id; - updaterId?: Id; name?: Param; - description?: Description; - createdAt?: DateTimeTimezone | string; - updatedAt?: DateTimeTimezone | string; } export class Dataset extends AbstractModel implements IDataset { @@ -30,6 +27,8 @@ export class Dataset extends AbstractModel implements IDataset { public readonly name?: Param; @BawPersistAttr public readonly description?: Description; + public readonly descriptionHtml?: Description; + public readonly descriptionHtmlTagline?: Description; @BawDateTime() public readonly createdAt?: DateTimeTimezone; @BawDateTime() diff --git a/src/app/models/ProgressEvent.ts b/src/app/models/ProgressEvent.ts index 99ce8fd12..084c8c6a7 100644 --- a/src/app/models/ProgressEvent.ts +++ b/src/app/models/ProgressEvent.ts @@ -1,18 +1,16 @@ import { Injector } from "@angular/core"; import { DATASET_ITEM } from "@baw-api/ServiceTokens"; -import { DateTimeTimezone, Id } from "@interfaces/apiInterfaces"; +import { DateTimeTimezone, HasCreator, Id } from "@interfaces/apiInterfaces"; import { AbstractModel } from "./AbstractModel"; import { Creator, HasOne } from "./AssociationDecorators"; import { BawDateTime, BawPersistAttr } from "./AttributeDecorators"; import type { DatasetItem } from "./DatasetItem"; import type { User } from "./User"; -export interface IProgressEvent { +export interface IProgressEvent extends HasCreator { id?: Id; - creatorId?: Id; datasetItemId?: Id; activity?: string; - createdAt?: DateTimeTimezone | string; } export class ProgressEvent extends AbstractModel implements IProgressEvent { diff --git a/src/app/models/Project.ts b/src/app/models/Project.ts index 4d227dadc..b48104116 100644 --- a/src/app/models/Project.ts +++ b/src/app/models/Project.ts @@ -2,8 +2,11 @@ import { Injector } from "@angular/core"; import { SHALLOW_SITE } from "@baw-api/ServiceTokens"; import { projectMenuItem } from "@components/projects/projects.menus"; import { + AccessLevel, DateTimeTimezone, Description, + HasAllUsers, + HasDescription, Id, Ids, ImageUrl, @@ -25,15 +28,11 @@ import type { User } from "./User"; /** * A project model. */ -export interface IProject { +export interface IProject extends HasAllUsers, HasDescription { id?: Id; name?: Param; - description?: Description; imageUrl?: string; - creatorId?: Id; - createdAt?: DateTimeTimezone | string; - updaterId?: Id; - updatedAt?: DateTimeTimezone | string; + accessLevel?: AccessLevel; ownerId?: Id; siteIds?: Ids | Id[]; } @@ -49,17 +48,23 @@ export class Project extends AbstractModel implements IProject { public readonly name?: Param; @BawPersistAttr public readonly description?: Description; + public readonly descriptionHtml?: Description; + public readonly descriptionHtmlTagline?: Description; public readonly imageUrl?: string; @BawImage(`${assetRoot}/images/project/project_span4.png`, { key: "imageUrl", }) public readonly image: ImageUrl[]; + public readonly accessLevel?: AccessLevel; public readonly creatorId?: Id; + public readonly updaterId?: Id; + public readonly deleterId?: Id; @BawDateTime() public readonly createdAt?: DateTimeTimezone; - public readonly updaterId?: Id; @BawDateTime() public readonly updatedAt?: DateTimeTimezone; + @BawDateTime() + public readonly deletedAt?: DateTimeTimezone; public readonly ownerId?: Id; @BawCollection({ persist: true }) public readonly siteIds?: Ids; @@ -85,7 +90,7 @@ export class Project extends AbstractModel implements IProject { public getCard(): Card { return { title: this.name, - description: this.description, + description: this.descriptionHtmlTagline, model: this, route: this.viewUrl, }; diff --git a/src/app/models/SavedSearch.ts b/src/app/models/SavedSearch.ts index 9950e08af..0b4e7fde1 100644 --- a/src/app/models/SavedSearch.ts +++ b/src/app/models/SavedSearch.ts @@ -3,6 +3,8 @@ import { InnerFilter } from "@baw-api/baw-api.service"; import { DateTimeTimezone, Description, + HasCreatorAndDeleter, + HasDescription, Id, Param, } from "@interfaces/apiInterfaces"; @@ -12,15 +14,10 @@ import { BawDateTime, BawPersistAttr } from "./AttributeDecorators"; import type { AudioRecording } from "./AudioRecording"; import type { User } from "./User"; -export interface ISavedSearch { +export interface ISavedSearch extends HasCreatorAndDeleter, HasDescription { id?: Id; name?: Param; - description?: Description; storedQuery?: InnerFilter; - creatorId?: Id; - deleterId?: Id; - createdAt?: DateTimeTimezone | string; - deletedAt?: DateTimeTimezone | string; } export class SavedSearch extends AbstractModel implements ISavedSearch { @@ -31,6 +28,8 @@ export class SavedSearch extends AbstractModel implements ISavedSearch { public readonly name?: Param; @BawPersistAttr public readonly description?: Description; + public readonly descriptionHtml?: Description; + public readonly descriptionHtmlTagline?: Description; @BawPersistAttr public readonly storedQuery?: InnerFilter; public readonly creatorId?: Id; diff --git a/src/app/models/Script.ts b/src/app/models/Script.ts index 1e3788d3a..304e749b0 100644 --- a/src/app/models/Script.ts +++ b/src/app/models/Script.ts @@ -1,7 +1,14 @@ import { Injector } from "@angular/core"; import { SCRIPT } from "@baw-api/ServiceTokens"; import { adminScriptMenuItem } from "@components/admin/scripts/scripts.menus"; -import { DateTimeTimezone, Id, Param } from "../interfaces/apiInterfaces"; +import { + DateTimeTimezone, + Description, + HasCreator, + HasDescription, + Id, + Param, +} from "../interfaces/apiInterfaces"; import { AbstractModel } from "./AbstractModel"; import { Creator, HasOne } from "./AssociationDecorators"; import { BawDateTime, BawPersistAttr } from "./AttributeDecorators"; @@ -10,16 +17,13 @@ import type { User } from "./User"; /** * A script model */ -export interface IScript { +export interface IScript extends HasCreator, HasDescription { id?: Id; name?: Param; - description?: string; analysisIdentifier?: string; version?: number; verified?: boolean; groupId?: Id; - creatorId?: Id; - createdAt?: DateTimeTimezone | string; executableCommand?: string; executableSettings?: string; executableSettingsMediaType?: string; @@ -33,7 +37,9 @@ export class Script extends AbstractModel implements IScript { @BawPersistAttr public readonly name?: Param; @BawPersistAttr - public readonly description?: string; + public readonly description?: Description; + public readonly descriptionHtml?: Description; + public readonly descriptionHtmlTagline?: Description; @BawPersistAttr public readonly analysisIdentifier?: string; @BawPersistAttr diff --git a/src/app/models/Site.ts b/src/app/models/Site.ts index 6abc208b3..13d69a58b 100644 --- a/src/app/models/Site.ts +++ b/src/app/models/Site.ts @@ -8,6 +8,8 @@ import { siteMenuItem } from "../components/sites/sites.menus"; import { DateTimeTimezone, Description, + HasAllUsers, + HasDescription, Id, Ids, ImageUrl, @@ -28,16 +30,11 @@ import type { User } from "./User"; /** * A site model. */ -export interface ISite { +export interface ISite extends HasAllUsers, HasDescription { id?: Id; name?: Param; imageUrl?: string; - description?: Description; locationObfuscated?: boolean; - creatorId?: Id; - updaterId?: Id; - createdAt?: DateTimeTimezone | string; - updatedAt?: DateTimeTimezone | string; projectIds?: Ids | Id[]; latitude?: number; customLatitude?: number; @@ -64,13 +61,18 @@ export class Site extends AbstractModel implements ISite { public readonly image?: ImageUrl[]; @BawPersistAttr public readonly description?: Description; + public readonly descriptionHtml?: Description; + public readonly descriptionHtmlTagline?: Description; public readonly locationObfuscated?: boolean; public readonly creatorId?: Id; public readonly updaterId?: Id; + public readonly deleterId?: Id; @BawDateTime() public readonly createdAt?: DateTimeTimezone; @BawDateTime() public readonly updatedAt?: DateTimeTimezone; + @BawDateTime() + public readonly deletedAt?: DateTimeTimezone; @BawCollection({ persist: true }) public readonly projectIds?: Ids; @BawPersistAttr diff --git a/src/app/models/Study.ts b/src/app/models/Study.ts index 3ba522bac..9f1175647 100644 --- a/src/app/models/Study.ts +++ b/src/app/models/Study.ts @@ -1,20 +1,21 @@ import { Injector } from "@angular/core"; import { DATASET } from "@baw-api/ServiceTokens"; -import { DateTimeTimezone, Id, Param } from "@interfaces/apiInterfaces"; +import { + DateTimeTimezone, + HasCreatorAndUpdater, + Id, + Param, +} from "@interfaces/apiInterfaces"; import { AbstractModel } from "./AbstractModel"; import { Creator, HasOne, Updater } from "./AssociationDecorators"; import { BawDateTime, BawPersistAttr } from "./AttributeDecorators"; import type { Dataset } from "./Dataset"; import type { User } from "./User"; -export interface IStudy { +export interface IStudy extends HasCreatorAndUpdater { id?: Id; name?: Param; - creatorId?: Id; - updaterId?: Id; datasetId?: Id; - createdAt?: DateTimeTimezone | string; - updatedAt?: DateTimeTimezone | string; } export class Study extends AbstractModel implements IStudy { diff --git a/src/app/models/Tag.ts b/src/app/models/Tag.ts index 1d20b4243..14ac69257 100644 --- a/src/app/models/Tag.ts +++ b/src/app/models/Tag.ts @@ -1,5 +1,9 @@ import { Injector } from "@angular/core"; -import { DateTimeTimezone, Id } from "../interfaces/apiInterfaces"; +import { + DateTimeTimezone, + HasCreatorAndUpdater, + Id, +} from "../interfaces/apiInterfaces"; import { AbstractData } from "./AbstractData"; import { AbstractModel } from "./AbstractModel"; import { Creator, Updater } from "./AssociationDecorators"; @@ -9,17 +13,13 @@ import type { User } from "./User"; /** * Tag model interface */ -export interface ITag { +export interface ITag extends HasCreatorAndUpdater { id?: Id; text?: string; isTaxanomic?: boolean; typeOfTag?: string; retired?: boolean; notes?: Blob | object; - creatorId?: Id; - updaterId?: Id; - createdAt?: DateTimeTimezone | string; - updatedAt?: DateTimeTimezone | string; } /** diff --git a/src/app/models/TagGroup.ts b/src/app/models/TagGroup.ts index 285ec89e9..77db4d27f 100644 --- a/src/app/models/TagGroup.ts +++ b/src/app/models/TagGroup.ts @@ -1,7 +1,7 @@ import { Injector } from "@angular/core"; import { TAG } from "@baw-api/ServiceTokens"; import { adminTagGroupsMenuItem } from "@components/admin/tag-group/tag-group.menus"; -import { DateTimeTimezone, Id } from "@interfaces/apiInterfaces"; +import { DateTimeTimezone, HasCreator, Id } from "@interfaces/apiInterfaces"; import { AbstractModel } from "./AbstractModel"; import { Creator, HasOne } from "./AssociationDecorators"; import { BawDateTime, BawPersistAttr } from "./AttributeDecorators"; @@ -11,12 +11,10 @@ import type { User } from "./User"; /** * A tag group model */ -export interface ITagGroup { +export interface ITagGroup extends HasCreator { id?: Id; groupIdentifier?: string; tagId?: Id; - creatorId?: Id; - createdAt?: DateTimeTimezone | string; } /** diff --git a/src/app/models/Tagging.ts b/src/app/models/Tagging.ts index babe32a48..97c483295 100644 --- a/src/app/models/Tagging.ts +++ b/src/app/models/Tagging.ts @@ -1,6 +1,10 @@ import { Injector } from "@angular/core"; import { AUDIO_EVENT, TAG } from "@baw-api/ServiceTokens"; -import { DateTimeTimezone, Id } from "@interfaces/apiInterfaces"; +import { + DateTimeTimezone, + HasCreatorAndUpdater, + Id, +} from "@interfaces/apiInterfaces"; import { AbstractModel } from "./AbstractModel"; import { Creator, HasOne, Updater } from "./AssociationDecorators"; import { BawDateTime, BawPersistAttr } from "./AttributeDecorators"; @@ -8,14 +12,10 @@ import type { AudioEvent } from "./AudioEvent"; import type { Tag } from "./Tag"; import type { User } from "./User"; -export interface ITagging { +export interface ITagging extends HasCreatorAndUpdater { id?: Id; audioEventId?: Id; tagId?: Id; - creatorId?: Id; - updaterId?: Id; - createdAt?: DateTimeTimezone | string; - updatedAt?: DateTimeTimezone | string; } export class Tagging extends AbstractModel implements ITagging { diff --git a/src/app/services/baw-api/security/security.service.spec.ts b/src/app/services/baw-api/security/security.service.spec.ts index 3a8f85dff..7310f5638 100644 --- a/src/app/services/baw-api/security/security.service.spec.ts +++ b/src/app/services/baw-api/security/security.service.spec.ts @@ -9,6 +9,7 @@ import { BawApiInterceptor, } from "@baw-api/api.interceptor.service"; import { MockShowApiService } from "@baw-api/mock/apiMocks.service"; +import { ImageSizes } from "@interfaces/apiInterfaces"; import { ISessionUser, IUser, SessionUser, User } from "@models/User"; import { MockAppConfigModule } from "@services/app-config/app-configMock.module"; import { generateApiErrorDetails } from "@test/fakes/ApiErrorDetails"; @@ -185,7 +186,12 @@ describe("SecurityService", () => { preferences: {}, rolesMask: 1, imageUrls: [ - { size: "extralarge", url: "path.png", width: 300, height: 300 }, + { + size: ImageSizes.EXTRA_LARGE, + url: "path.png", + width: 300, + height: 300, + }, ], }) ); @@ -234,7 +240,12 @@ describe("SecurityService", () => { preferences: {}, authToken: "xxxxxxxxxxxxxxxx", imageUrls: [ - { size: "extralarge", url: "path.png", width: 300, height: 300 }, + { + size: ImageSizes.EXTRA_LARGE, + url: "path.png", + width: 300, + height: 300, + }, ], }) ); diff --git a/src/app/test/fakes/AnalysisJob.ts b/src/app/test/fakes/AnalysisJob.ts index c6d654d16..8ce9aa38e 100644 --- a/src/app/test/fakes/AnalysisJob.ts +++ b/src/app/test/fakes/AnalysisJob.ts @@ -2,7 +2,7 @@ import { Id } from "@interfaces/apiInterfaces"; import { AnalysisJobStatus, IAnalysisJob } from "@models/AnalysisJob"; import { modelData } from "@test/helpers/faker"; -export function generateAnalysisJob(id?: Id): IAnalysisJob { +export function generateAnalysisJob(id?: Id): Required { const overallDurationSeconds = modelData.random.number(3.154e7); // 1 year const overallDataLengthBytes = overallDurationSeconds * 22050 * 2; // duration seconds * sample rate * two bytes per sample const statuses: AnalysisJobStatus[] = [ @@ -18,14 +18,7 @@ export function generateAnalysisJob(id?: Id): IAnalysisJob { id: modelData.id(id), name: modelData.param(), annotationName: modelData.param(), - description: modelData.description(), scriptId: modelData.id(), - creatorId: modelData.id(), - updaterId: modelData.id(), - deleterId: modelData.id(), - createdAt: modelData.timestamp(), - updatedAt: modelData.timestamp(), - deletedAt: modelData.timestamp(), savedSearchId: modelData.id(), startedAt: modelData.timestamp(), overallStatus: modelData.random.arrayElement(statuses), @@ -42,5 +35,7 @@ export function generateAnalysisJob(id?: Id): IAnalysisJob { overallDurationSeconds, overallDataLengthBytes, customSettings: modelData.randomObject(0, 5), + ...modelData.model.generateDescription(), + ...modelData.model.generateAllUsers(), }; } diff --git a/src/app/test/fakes/AnalysisJobItem.ts b/src/app/test/fakes/AnalysisJobItem.ts index 46c6cb485..a386d0b06 100644 --- a/src/app/test/fakes/AnalysisJobItem.ts +++ b/src/app/test/fakes/AnalysisJobItem.ts @@ -5,7 +5,7 @@ import { } from "@models/AnalysisJobItem"; import { modelData } from "@test/helpers/faker"; -export function generateAnalysisJobItem(id?: Id): IAnalysisJobItem { +export function generateAnalysisJobItem(id?: Id): Required { const statuses: AnalysisJobItemStatus[] = [ "successful", "new", diff --git a/src/app/test/fakes/AudioEvent.ts b/src/app/test/fakes/AudioEvent.ts index c8641c6a4..641baca2d 100644 --- a/src/app/test/fakes/AudioEvent.ts +++ b/src/app/test/fakes/AudioEvent.ts @@ -2,7 +2,7 @@ import { Id } from "@interfaces/apiInterfaces"; import { IAudioEvent } from "@models/AudioEvent"; import { modelData } from "@test/helpers/faker"; -export function generateAudioEvent(id?: Id): IAudioEvent { +export function generateAudioEvent(id?: Id): Required { const [startTimeSeconds, endTimeSeconds] = modelData.startEndSeconds(); const [lowFrequencyHertz, highFrequencyHertz] = modelData.startEndArray( modelData.defaults.sampleRateHertz @@ -16,11 +16,6 @@ export function generateAudioEvent(id?: Id): IAudioEvent { lowFrequencyHertz, highFrequencyHertz, isReference: modelData.boolean(), - creatorId: modelData.id(), - updaterId: modelData.id(), - deleterId: modelData.id(), - createdAt: modelData.timestamp(), - updatedAt: modelData.timestamp(), - deletedAt: modelData.timestamp(), + ...modelData.model.generateAllUsers(), }; } diff --git a/src/app/test/fakes/AudioRecording.ts b/src/app/test/fakes/AudioRecording.ts index 2cb662d98..90a0be09e 100644 --- a/src/app/test/fakes/AudioRecording.ts +++ b/src/app/test/fakes/AudioRecording.ts @@ -2,7 +2,7 @@ import { Id } from "@interfaces/apiInterfaces"; import { AudioRecordingStatus, IAudioRecording } from "@models/AudioRecording"; import { modelData } from "@test/helpers/faker"; -export function generateAudioRecording(id?: Id): IAudioRecording { +export function generateAudioRecording(id?: Id): Required { const bitRateBps = modelData.random.arrayElement( modelData.defaults.bitRateBps ); @@ -41,13 +41,8 @@ export function generateAudioRecording(id?: Id): IAudioRecording { fileHash: modelData.hash(), status: modelData.random.arrayElement(statuses), notes: modelData.notes(), - creatorId: modelData.id(), - updaterId: modelData.id(), - deleterId: modelData.id(), - createdAt: modelData.timestamp(), - updatedAt: modelData.timestamp(), - deletedAt: modelData.timestamp(), originalFileName: modelData.system.fileName(".mpg", "audio"), recordedUtcOffset: modelData.offset(), + ...modelData.model.generateAllUsers(), }; } diff --git a/src/app/test/fakes/Bookmark.ts b/src/app/test/fakes/Bookmark.ts index c47051a42..72ab211f5 100644 --- a/src/app/test/fakes/Bookmark.ts +++ b/src/app/test/fakes/Bookmark.ts @@ -2,17 +2,14 @@ import { Id } from "@interfaces/apiInterfaces"; import { IBookmark } from "@models/Bookmark"; import { modelData } from "@test/helpers/faker"; -export function generateBookmark(id?: Id): IBookmark { +export function generateBookmark(id?: Id): Required { return { id: modelData.id(id), audioRecordingId: modelData.id(), offsetSeconds: modelData.seconds(), name: modelData.param(), - creatorId: modelData.id(), - updaterId: modelData.id(), - createdAt: modelData.timestamp(), - updatedAt: modelData.timestamp(), - description: modelData.description(), category: "<< application >>", // TODO Replace with list of possibilities + ...modelData.model.generateDescription(), + ...modelData.model.generateCreatorAndUpdater(), }; } diff --git a/src/app/test/fakes/Dataset.ts b/src/app/test/fakes/Dataset.ts index 38b3cd2bf..cbc184681 100644 --- a/src/app/test/fakes/Dataset.ts +++ b/src/app/test/fakes/Dataset.ts @@ -2,14 +2,11 @@ import { Id } from "@interfaces/apiInterfaces"; import { IDataset } from "@models/Dataset"; import { modelData } from "@test/helpers/faker"; -export function generateDataset(id?: Id): IDataset { +export function generateDataset(id?: Id): Required { return { id: modelData.id(id), name: modelData.param(), - description: modelData.description(), - creatorId: modelData.id(), - updaterId: modelData.id(), - createdAt: modelData.timestamp(), - updatedAt: modelData.timestamp(), + ...modelData.model.generateDescription(), + ...modelData.model.generateCreatorAndUpdater(), }; } diff --git a/src/app/test/fakes/DatasetItem.ts b/src/app/test/fakes/DatasetItem.ts index 5d7dc0c7e..7ceab2398 100644 --- a/src/app/test/fakes/DatasetItem.ts +++ b/src/app/test/fakes/DatasetItem.ts @@ -2,17 +2,16 @@ import { Id } from "@interfaces/apiInterfaces"; import { IDatasetItem } from "@models/DatasetItem"; import { modelData } from "@test/helpers/faker"; -export function generateDatasetItem(id?: Id): IDatasetItem { +export function generateDatasetItem(id?: Id): Required { const [startTimeSeconds, endTimeSeconds] = modelData.startEndSeconds(); return { id: modelData.id(id), datasetId: modelData.id(), audioRecordingId: modelData.id(), - creatorId: modelData.id(), - createdAt: modelData.timestamp(), startTimeSeconds, endTimeSeconds, order: modelData.random.number(100), + ...modelData.model.generateCreator(), }; } diff --git a/src/app/test/fakes/ProgressEvent.ts b/src/app/test/fakes/ProgressEvent.ts index d2838168b..39bfe17b0 100644 --- a/src/app/test/fakes/ProgressEvent.ts +++ b/src/app/test/fakes/ProgressEvent.ts @@ -2,12 +2,11 @@ import { Id } from "@interfaces/apiInterfaces"; import { IProgressEvent } from "@models/ProgressEvent"; import { modelData } from "@test/helpers/faker"; -export function generateProgressEvent(id?: Id): IProgressEvent { +export function generateProgressEvent(id?: Id): Required { return { id: modelData.id(id), - creatorId: modelData.id(), datasetItemId: modelData.id(), activity: modelData.random.arrayElement(["viewed", "played", "annotated"]), - createdAt: modelData.timestamp(), + ...modelData.model.generateCreator(), }; } diff --git a/src/app/test/fakes/Project.ts b/src/app/test/fakes/Project.ts index a5072bec7..e34ebe7e8 100644 --- a/src/app/test/fakes/Project.ts +++ b/src/app/test/fakes/Project.ts @@ -2,17 +2,15 @@ import { Id } from "@interfaces/apiInterfaces"; import { IProject } from "@models/Project"; import { modelData } from "@test/helpers/faker"; -export function generateProject(id?: Id): IProject { +export function generateProject(id?: Id): Required { return { id: modelData.id(id), name: modelData.param(), - description: modelData.description(), imageUrl: modelData.imageUrl(), - creatorId: modelData.id(), - updaterId: modelData.id(), + accessLevel: modelData.accessLevel(), ownerId: modelData.id(), - createdAt: modelData.timestamp(), - updatedAt: modelData.timestamp(), siteIds: modelData.ids(), + ...modelData.model.generateDescription(), + ...modelData.model.generateAllUsers(), }; } diff --git a/src/app/test/fakes/Question.ts b/src/app/test/fakes/Question.ts index ddb1ed3e0..c9476f985 100644 --- a/src/app/test/fakes/Question.ts +++ b/src/app/test/fakes/Question.ts @@ -2,14 +2,11 @@ import { Id } from "@interfaces/apiInterfaces"; import { IQuestion } from "@models/Question"; import { modelData } from "@test/helpers/faker"; -export function generateQuestion(id?: Id): IQuestion { +export function generateQuestion(id?: Id): Required { return { id: modelData.id(id), text: modelData.html(), data: modelData.notes(), - creatorId: modelData.id(), - updaterId: modelData.id(), - createdAt: modelData.timestamp(), - updatedAt: modelData.timestamp(), + ...modelData.model.generateCreatorAndUpdater(), }; } diff --git a/src/app/test/fakes/Response.ts b/src/app/test/fakes/Response.ts index 502d435b1..8ee6489ea 100644 --- a/src/app/test/fakes/Response.ts +++ b/src/app/test/fakes/Response.ts @@ -2,14 +2,13 @@ import { Id } from "@interfaces/apiInterfaces"; import { IResponse } from "@models/Response"; import { modelData } from "@test/helpers/faker"; -export function generateResponse(id?: Id): IResponse { +export function generateResponse(id?: Id): Required { return { id: modelData.id(id), data: modelData.notes(), datasetItemId: modelData.id(), questionId: modelData.id(), studyId: modelData.id(), - creatorId: modelData.id(), - createdAt: modelData.timestamp(), + ...modelData.model.generateCreator(), }; } diff --git a/src/app/test/fakes/SavedSearch.ts b/src/app/test/fakes/SavedSearch.ts index 5fc2eded0..8bfa1684f 100644 --- a/src/app/test/fakes/SavedSearch.ts +++ b/src/app/test/fakes/SavedSearch.ts @@ -2,15 +2,12 @@ import { Id } from "@interfaces/apiInterfaces"; import { ISavedSearch } from "@models/SavedSearch"; import { modelData } from "@test/helpers/faker"; -export function generateSavedSearch(id?: Id): ISavedSearch { +export function generateSavedSearch(id?: Id): Required { return { id: modelData.id(id), name: modelData.param(), - description: modelData.description(), storedQuery: { uuid: { eq: "blah blah" } }, // TODO Implement with random values - creatorId: modelData.id(), - deleterId: modelData.id(), - createdAt: modelData.timestamp(), - deletedAt: modelData.timestamp(), + ...modelData.model.generateDescription(), + ...modelData.model.generateCreatorAndDeleter(), }; } diff --git a/src/app/test/fakes/Script.ts b/src/app/test/fakes/Script.ts index ccf3597ac..7258f60df 100644 --- a/src/app/test/fakes/Script.ts +++ b/src/app/test/fakes/Script.ts @@ -2,17 +2,14 @@ import { Id } from "@interfaces/apiInterfaces"; import { IScript } from "@models/Script"; import { modelData } from "@test/helpers/faker"; -export function generateScript(id?: Id): IScript { +export function generateScript(id?: Id): Required { return { id: modelData.id(id), name: modelData.param(), - description: modelData.description(), analysisIdentifier: "script machine identifier", // TODO Implement with random values version: parseFloat(modelData.system.semver()), verified: modelData.boolean(), groupId: modelData.id(), - creatorId: modelData.id(), - createdAt: modelData.timestamp(), executableCommand: "executive command", // TODO Implement with random values executableSettings: "executive settings", // TODO Implement with random values executableSettingsMediaType: "text/plain", // TODO Implement with random values @@ -22,5 +19,7 @@ export function generateScript(id?: Id): IScript { subFolders: [], customSettings: {}, }, // TODO Implement with random values + ...modelData.model.generateDescription(), + ...modelData.model.generateCreator(), }; } diff --git a/src/app/test/fakes/Site.ts b/src/app/test/fakes/Site.ts index 7a9fa43d6..9be7c790f 100644 --- a/src/app/test/fakes/Site.ts +++ b/src/app/test/fakes/Site.ts @@ -2,25 +2,20 @@ import { Id } from "@interfaces/apiInterfaces"; import { ISite } from "@models/Site"; import { modelData } from "@test/helpers/faker"; -export function generateSite(id?: Id): ISite { - const site: ISite = { +export function generateSite(id?: Id): Required { + return { id: modelData.id(id), name: modelData.param(), imageUrl: modelData.imageUrl(), - description: modelData.description(), locationObfuscated: modelData.boolean(), - creatorId: modelData.id(), - updaterId: modelData.id(), - createdAt: modelData.timestamp(), - updatedAt: modelData.timestamp(), projectIds: modelData.ids(), latitude: modelData.latitude(), customLatitude: modelData.latitude(), longitude: modelData.longitude(), customLongitude: modelData.longitude(), timezoneInformation: modelData.timezone(), + tzinfoTz: modelData.tzInfoTz(), + ...modelData.model.generateDescription(), + ...modelData.model.generateAllUsers(), }; - site.tzinfoTz = site.timezoneInformation.identifier; - - return site; } diff --git a/src/app/test/fakes/Study.ts b/src/app/test/fakes/Study.ts index a3770429b..a6dfee77a 100644 --- a/src/app/test/fakes/Study.ts +++ b/src/app/test/fakes/Study.ts @@ -2,14 +2,11 @@ import { Id } from "@interfaces/apiInterfaces"; import { IStudy } from "@models/Study"; import { modelData } from "@test/helpers/faker"; -export function generateStudy(id?: Id): IStudy { +export function generateStudy(id?: Id): Required { return { id: modelData.id(id), name: modelData.param(), - creatorId: modelData.id(), - updaterId: modelData.id(), datasetId: modelData.id(), - createdAt: modelData.timestamp(), - updatedAt: modelData.timestamp(), + ...modelData.model.generateCreatorAndUpdater(), }; } diff --git a/src/app/test/fakes/Tag.ts b/src/app/test/fakes/Tag.ts index 8f8bf0f74..d984625fc 100644 --- a/src/app/test/fakes/Tag.ts +++ b/src/app/test/fakes/Tag.ts @@ -2,7 +2,7 @@ import { Id } from "@interfaces/apiInterfaces"; import { ITag } from "@models/Tag"; import { modelData } from "@test/helpers/faker"; -export function generateTag(id?: Id): ITag { +export function generateTag(id?: Id): Required { const tagTypes = [ "general", "common_name", @@ -18,9 +18,6 @@ export function generateTag(id?: Id): ITag { typeOfTag: modelData.random.arrayElement(tagTypes), retired: modelData.boolean(), notes: modelData.notes(), - creatorId: modelData.id(), - updaterId: modelData.id(), - createdAt: modelData.timestamp(), - updatedAt: modelData.timestamp(), + ...modelData.model.generateCreatorAndUpdater(), }; } diff --git a/src/app/test/fakes/TagGroup.ts b/src/app/test/fakes/TagGroup.ts index 47932c592..e6b0b4a86 100644 --- a/src/app/test/fakes/TagGroup.ts +++ b/src/app/test/fakes/TagGroup.ts @@ -2,12 +2,11 @@ import { Id } from "@interfaces/apiInterfaces"; import { ITagGroup } from "@models/TagGroup"; import { modelData } from "@test/helpers/faker"; -export function generateTagGroup(id?: Id): ITagGroup { +export function generateTagGroup(id?: Id): Required { return { id: modelData.id(id), groupIdentifier: modelData.param(), tagId: modelData.id(), - creatorId: modelData.id(), - createdAt: modelData.timestamp(), + ...modelData.model.generateCreator(), }; } diff --git a/src/app/test/fakes/Tagging.ts b/src/app/test/fakes/Tagging.ts index fdedabb33..823c6f163 100644 --- a/src/app/test/fakes/Tagging.ts +++ b/src/app/test/fakes/Tagging.ts @@ -2,14 +2,11 @@ import { Id } from "@interfaces/apiInterfaces"; import { ITagging } from "@models/Tagging"; import { modelData } from "@test/helpers/faker"; -export function generateTagging(id?: Id): ITagging { +export function generateTagging(id?: Id): Required { return { id: modelData.id(id), audioEventId: modelData.id(), tagId: modelData.id(), - creatorId: modelData.id(), - updaterId: modelData.id(), - createdAt: modelData.timestamp(), - updatedAt: modelData.timestamp(), + ...modelData.model.generateCreatorAndUpdater(), }; } diff --git a/src/app/test/fakes/User.ts b/src/app/test/fakes/User.ts index c7e949c3f..3735233c2 100644 --- a/src/app/test/fakes/User.ts +++ b/src/app/test/fakes/User.ts @@ -2,7 +2,7 @@ import { Id } from "@interfaces/apiInterfaces"; import { ISessionUser, IUser } from "@models/User"; import { modelData } from "@test/helpers/faker"; -export function generateUser(id?: Id, isAdmin?: boolean): IUser { +export function generateUser(id?: Id, isAdmin?: boolean): Required { return { id: modelData.id(id), email: modelData.internet.email(), @@ -29,6 +29,7 @@ export function generateUser(id?: Id, isAdmin?: boolean): IUser { { rolesMask: 1, rolesMaskNames: ["Admin"] }, { rolesMask: 2, rolesMaskNames: ["User"] }, ]), + tzinfoTz: modelData.tzInfoTz(), }; } diff --git a/src/app/test/helpers/faker.ts b/src/app/test/helpers/faker.ts index 7ef026e64..1d9cf0963 100644 --- a/src/app/test/helpers/faker.ts +++ b/src/app/test/helpers/faker.ts @@ -1,4 +1,5 @@ import { + AccessLevel, Id, ImageSizes, ImageUrl, @@ -7,8 +8,14 @@ import { import faker from "faker"; export const modelData = { + accessLevel: () => + faker.random.arrayElement(["Reader", "Writer", "Owner"]), boolean: () => faker.random.boolean(), description: () => faker.lorem.sentence(), + descriptionLong: () => + [0, 1, 2, 3, 4].map(() => faker.lorem.sentences()).join(" "), + descriptionHtml: () => + `${faker.commerce.productName()}

${modelData.description()}

`, defaults: { sampleRateHertz: [8000, 22050, 44100, 48000], bitRateBps: [ @@ -54,12 +61,50 @@ export const modelData = { return [arr[min], arr[min + inc]]; }, timestamp: () => faker.date.past().toISOString(), + tzInfoTz: () => + faker.random.arrayElement([ + "America/Costa_Rica", + "Australia/Brisbane", + "Asia/Makassar", + ]), uuid: () => faker.random.uuid(), hexaDecimal, randomArray, randomObject, shuffleArray, timezone, + model: { + generateDescription: () => ({ + description: modelData.description(), + descriptionHtml: modelData.descriptionHtml(), + descriptionHtmlTagline: modelData.descriptionHtml(), + }), + generateCreator: () => ({ + creatorId: modelData.id(), + createdAt: modelData.timestamp(), + }), + generateUpdater: () => ({ + updaterId: modelData.id(), + updatedAt: modelData.timestamp(), + }), + generateDeleter: () => ({ + deleterId: modelData.id(), + deletedAt: modelData.timestamp(), + }), + generateCreatorAndUpdater: () => ({ + ...modelData.model.generateCreator(), + ...modelData.model.generateUpdater(), + }), + generateCreatorAndDeleter: () => ({ + ...modelData.model.generateCreator(), + ...modelData.model.generateDeleter(), + }), + generateAllUsers: () => ({ + ...modelData.model.generateCreator(), + ...modelData.model.generateUpdater(), + ...modelData.model.generateDeleter(), + }), + }, ...faker, }; @@ -92,38 +137,18 @@ function shuffleArray(array: T[]): T[] { * @param url Base url for image urls. Do not end url with '/' ie /broken_links/. */ function imageUrls(url?: string): ImageUrl[] { - return new Array( - { - size: ImageSizes.EXTRA_LARGE, - url: url ? url + "/300/300" : faker.image.imageUrl(300, 300), - width: 300, - height: 300, - }, - { - size: ImageSizes.LARGE, - url: url ? url + "/220/220" : faker.image.imageUrl(220, 220), - width: 220, - height: 220, - }, - { - size: ImageSizes.MEDIUM, - url: url ? url + "/140/140" : faker.image.imageUrl(140, 140), - width: 140, - height: 140, - }, - { - size: ImageSizes.SMALL, - url: url ? url + "/60/60" : faker.image.imageUrl(60, 60), - width: 60, - height: 60, - }, - { - size: ImageSizes.TINY, - url: url ? url + "/30/30" : faker.image.imageUrl(30, 30), - width: 30, - height: 30, - } - ); + return [ + { image: ImageSizes.EXTRA_LARGE, size: 300 }, + { image: ImageSizes.LARGE, size: 220 }, + { image: ImageSizes.MEDIUM, size: 140 }, + { image: ImageSizes.SMALL, size: 60 }, + { image: ImageSizes.TINY, size: 30 }, + ].map(({ image, size }) => ({ + size: image, + url: url ? url + "/300/300" : faker.image.imageUrl(size, size), + width: size, + height: size, + })); } /** diff --git a/src/app/test/helpers/html.ts b/src/app/test/helpers/html.ts index 06d0e2281..1df51a178 100644 --- a/src/app/test/helpers/html.ts +++ b/src/app/test/helpers/html.ts @@ -2,6 +2,7 @@ import { DebugElement } from "@angular/core"; import { ComponentFixture, tick } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; import { AuthenticatedImageDirective } from "@directives/image/image.directive"; +import { LineTruncationDirective } from "ngx-line-truncation"; declare const ng: any; @@ -181,3 +182,15 @@ export function assertSpinner( expectation.toBeFalsy(); } } + +export function assertTruncation( + text: HTMLDivElement | HTMLParagraphElement, + lines: number +) { + const directive: LineTruncationDirective = ng + .getDirectives(text) + .find((_directive) => _directive instanceof LineTruncationDirective); + + expect(directive).toBeTruthy(); + expect(directive.lines).toBe(lines); +}