diff --git a/web/app/components/project/tile.hbs b/web/app/components/project/tile.hbs new file mode 100644 index 000000000..464f441fd --- /dev/null +++ b/web/app/components/project/tile.hbs @@ -0,0 +1,54 @@ + +
+

+ {{@project.title}} +

+ {{#if @project.description}} +

+ {{@project.description}} +

+ {{/if}} + +
+ {{#if this.productAreas}} +
    + {{#each this.productAreas as |productArea|}} +
  • + {{productArea}} +
  • + {{/each}} +
+ {{/if}} + {{#if this.jiraObject}} +
+ {{#if @project.jiraObject.type}} + + {{@project.jiraObject.type}} + + {{/if}} + + {{this.jiraObject.key}} + +
+ {{/if}} +
+
+
diff --git a/web/app/components/project/tile.ts b/web/app/components/project/tile.ts new file mode 100644 index 000000000..369a8ade9 --- /dev/null +++ b/web/app/components/project/tile.ts @@ -0,0 +1,29 @@ +import Component from "@glimmer/component"; +import { HermesProject } from "hermes/types/project"; + +interface ProjectTileComponentSignature { + Element: HTMLDivElement; + Args: { + project: HermesProject; + }; +} + +export default class ProjectTileComponent extends Component { + protected get documents() { + return this.args.project.documents; + } + + protected get jiraObject() { + return this.args.project.jiraObject; + } + + protected get productAreas() { + return this.documents?.map((doc) => doc.product).uniq(); + } +} + +declare module "@glint/environment-ember-loose/registry" { + export default interface Registry { + "Project::Tile": typeof ProjectTileComponent; + } +} diff --git a/web/app/styles/app.scss b/web/app/styles/app.scss index 17a98355c..72627f700 100644 --- a/web/app/styles/app.scss +++ b/web/app/styles/app.scss @@ -29,7 +29,7 @@ @use "components/doc/tile-list"; @use "components/doc/thumbnail"; @use "components/doc/folder-affordance"; -@use "components/doc/tile"; +@use "components/doc/tile" as doc-tile; @use "components/doc/state"; @use "components/table/sortable-header"; @use "components/preview-card"; @@ -38,6 +38,7 @@ @use "components/document/related-resources"; @use "components/hds-badge"; @use "components/product-badge-link"; +@use "components/project/tile" as project-tile; @use "components/header/facet-dropdown"; @use "components/floating-u-i/content"; @use "components/settings/subscription-list-item"; diff --git a/web/app/styles/components/project/tile.scss b/web/app/styles/components/project/tile.scss new file mode 100644 index 000000000..ed2baa5a2 --- /dev/null +++ b/web/app/styles/components/project/tile.scss @@ -0,0 +1,18 @@ +.project-tile { + .title, + .description { + @apply overflow-hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + } + + .title { + -webkit-line-clamp: 2; + line-clamp: 2; + } + + .description { + -webkit-line-clamp: 3; + line-clamp: 3; + } +} diff --git a/web/app/templates/authenticated/projects/index.hbs b/web/app/templates/authenticated/projects/index.hbs index 7efd3ee37..9c0e551ce 100644 --- a/web/app/templates/authenticated/projects/index.hbs +++ b/web/app/templates/authenticated/projects/index.hbs @@ -1,9 +1,11 @@ {{page-title "All Projects"}} -
    +
      {{#each-in this.model as |_id project|}}
    1. - {{project.title}} + + +
    2. {{/each-in}}
    diff --git a/web/mirage/factories/jira-object.ts b/web/mirage/factories/jira-object.ts new file mode 100644 index 000000000..7a9e3c792 --- /dev/null +++ b/web/mirage/factories/jira-object.ts @@ -0,0 +1,12 @@ +import { Factory } from "miragejs"; + +export default Factory.extend({ + id: (i) => i, + key: (i) => `KEY-00${i}`, + url: "", + priority: "Medium", + status: "Open", + assignee: "Unassigned", + type: "Task", + summary: "This is a Jira object", +}); diff --git a/web/mirage/factories/project.ts b/web/mirage/factories/project.ts index 2d188f67b..3984437c5 100644 --- a/web/mirage/factories/project.ts +++ b/web/mirage/factories/project.ts @@ -1,4 +1,5 @@ -import { Factory } from "miragejs"; +import { Factory, ModelInstance, Server } from "miragejs"; +import { HermesProject } from "hermes/types/project"; export default Factory.extend({ id: (i: number) => i, @@ -6,4 +7,21 @@ export default Factory.extend({ dateCreated: 1, dateModified: 1, creator: "testuser@example.com", + + // @ts-ignore - Bug https://github.com/miragejs/miragejs/issues/1052 + afterCreate(project: ModelInstance, server: any): void { + server.createList("related-hermes-document", 1); + server.create("jira-object"); + + const relatedHermesDocuments = server.schema.relatedHermesDocument + .all() + .models.map((doc: ModelInstance) => doc.attrs); + + const jiraObject = server.schema.jiraObjects.first()?.attrs; + + project.update({ + documents: relatedHermesDocuments, + jiraObject, + }); + }, }); diff --git a/web/mirage/models/jira-object.ts b/web/mirage/models/jira-object.ts new file mode 100644 index 000000000..dea7fc045 --- /dev/null +++ b/web/mirage/models/jira-object.ts @@ -0,0 +1,5 @@ +import { Model } from "miragejs"; + +export default Model.extend({ + // Required for Mirage, even though it's empty +}); diff --git a/web/tests/acceptance/authenticated/projects-test.ts b/web/tests/acceptance/authenticated/projects-test.ts index 00b781b25..cb63bceb4 100644 --- a/web/tests/acceptance/authenticated/projects-test.ts +++ b/web/tests/acceptance/authenticated/projects-test.ts @@ -6,6 +6,13 @@ import { getPageTitle } from "ember-page-title/test-support"; import { setupApplicationTest } from "ember-qunit"; import { HermesProject } from "hermes/types/project"; +const PROJECT_TILE = "[data-test-project-tile]"; +const PROJECT_TITLE = `${PROJECT_TILE} [data-test-title]`; +const PROJECT_DESCRIPTION = `${PROJECT_TILE} [data-test-description]`; +const PROJECT_PRODUCT = `${PROJECT_TILE} [data-test-product]`; +const PROJECT_JIRA_TYPE = `${PROJECT_TILE} [data-test-jira-type]`; +const PROJECT_JIRA_KEY = `${PROJECT_TILE} [data-test-jira-key]`; + interface AuthenticatedProjectsRouteTestContext extends MirageTestContext {} module("Acceptance | authenticated/projects", function (hooks) { setupApplicationTest(hooks); @@ -27,14 +34,60 @@ module("Acceptance | authenticated/projects", function (hooks) { assert.dom("[data-test-project]").exists({ count: 3 }); - const expectedTitles = this.server.schema.projects + let expectedTitles: string[] = []; + let expectedDescriptions: string[] = []; + let expectedProducts: string[] = []; + let expectedKeys: string[] = []; + let expectedJiraTypes: string[] = []; + + this.server.schema.projects .all() - .models.map((project: HermesProject) => project.title); + .models.forEach((project: HermesProject) => { + expectedTitles.push(project.title); + + if (project.description) { + expectedDescriptions.push(project.description); + } + + if (project.jiraObject) { + expectedKeys.push(project.jiraObject.key); + if (project.jiraObject.type) { + expectedJiraTypes.push(project.jiraObject.type); + } + } + if (project.documents) { + project.documents.forEach((doc) => { + if (doc.product) { + expectedProducts.push(doc.product); + } + }); + } + }); + + const renderedTitles = findAll(PROJECT_TITLE).map( + (e) => e.textContent?.trim(), + ); + + const renderedDescriptions = findAll(PROJECT_DESCRIPTION).map( + (e) => e.textContent?.trim(), + ); + + const renderedProducts = findAll(PROJECT_PRODUCT).map( + (e) => e.textContent?.trim(), + ); + + const renderedKeys = findAll(PROJECT_JIRA_KEY).map( + (e) => e.textContent?.trim(), + ); - const renderedTitles = findAll("[data-test-project]").map( + const renderedJiraTypes = findAll(PROJECT_JIRA_TYPE).map( (e) => e.textContent?.trim(), ); assert.deepEqual(renderedTitles, expectedTitles); + assert.deepEqual(renderedDescriptions, expectedDescriptions); + assert.deepEqual(renderedProducts, expectedProducts); + assert.deepEqual(renderedKeys, expectedKeys); + assert.deepEqual(renderedJiraTypes, expectedJiraTypes); }); }); diff --git a/web/tests/integration/components/project/tile-test.ts b/web/tests/integration/components/project/tile-test.ts new file mode 100644 index 000000000..0d5249aac --- /dev/null +++ b/web/tests/integration/components/project/tile-test.ts @@ -0,0 +1,153 @@ +import { find, findAll, render } from "@ember/test-helpers"; +import { hbs } from "ember-cli-htmlbars"; +import { MirageTestContext, setupMirage } from "ember-cli-mirage/test-support"; +import { setupRenderingTest } from "ember-qunit"; +import { HermesProject } from "hermes/types/project"; +import { module, test } from "qunit"; +import { assert as emberAssert } from "@ember/debug"; +import htmlElement from "hermes/utils/html-element"; + +const PROJECT_TITLE = "[data-test-title]"; +const PROJECT_DESCRIPTION = "[data-test-description]"; +const PROJECT_PRODUCT = "[data-test-product]"; +const PROJECT_JIRA_TYPE = "[data-test-jira-type]"; +const PROJECT_JIRA_KEY = "[data-test-jira-key]"; + +interface ProjectTileComponentTestContext extends MirageTestContext { + project: HermesProject; + jiraStatus: string; +} + +module("Integration | Component | project/tile", function (hooks) { + setupRenderingTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(async function (this: ProjectTileComponentTestContext) { + this.project = this.server.create("project", { + title: "Test Title", + description: "Test Description", + documents: [ + { + product: "Foo", + }, + { + product: "Bar", + }, + ], + jiraObject: { + key: "TEST-123", + type: "Epic", + }, + }); + }); + + test("it renders as expected (complete model)", async function (this: ProjectTileComponentTestContext, assert) { + await render(hbs` + + `); + + const { title, description, documents, jiraObject } = this.project; + const documentProducts = documents + ?.map((doc) => doc.product as string) + .uniq(); + + assert.dom(PROJECT_TITLE).hasText(title); + + emberAssert("description must exist", description); + + assert.dom(PROJECT_DESCRIPTION).hasText(description); + + assert.deepEqual( + findAll(PROJECT_PRODUCT).map((el) => el.textContent?.trim()), + documentProducts, + ); + + emberAssert("jiraObject must exist", jiraObject); + + const { key, type } = jiraObject; + + emberAssert("jiraObject type must exist", type); + + assert.dom(PROJECT_JIRA_KEY).hasText(key); + assert.dom(PROJECT_JIRA_TYPE).hasText(type); + }); + + test("it renders as expected (incomplete model)", async function (this: ProjectTileComponentTestContext, assert) { + const project = this.server.schema.projects.first(); + + project.update({ + description: null, + documents: null, + jiraObject: null, + }); + + this.set("project", project); + + await render(hbs` + + `); + + assert.dom(PROJECT_DESCRIPTION).doesNotExist(); + assert.dom(PROJECT_PRODUCT).doesNotExist(); + assert.dom(PROJECT_JIRA_KEY).doesNotExist(); + assert.dom(PROJECT_JIRA_TYPE).doesNotExist(); + }); + + test('if the status of a jiraObject is "Done," the key is rendered with a line through it', async function (this: ProjectTileComponentTestContext, assert) { + await render(hbs` + + `); + + assert.dom(PROJECT_JIRA_KEY).doesNotHaveClass("line-through"); + + const project = this.server.schema.projects.first(); + + project.update({ + jiraObject: { + key: "TEST-123", + type: "Epic", + status: "Done", + }, + }); + + this.set("project", project); + + assert.dom(PROJECT_JIRA_KEY).hasClass("line-through"); + }); + + test("it truncates long titles and descriptions", async function (this: ProjectTileComponentTestContext, assert) { + this.set( + "project", + this.server.create("project", { + title: + "This is a long text string that should be truncated. It goes on and on and on, and then, wouldn't you know it, it goes on some more.", + description: + "This is a long text string that should be truncated. It goes on and on and on, and then, wouldn't you know it, it goes on some more.", + }), + ); + + await render(hbs` +
    + +
    + `); + + const titleHeight = htmlElement(PROJECT_TITLE).offsetHeight; + const descriptionHeight = htmlElement(PROJECT_DESCRIPTION).offsetHeight; + + const titleLineHeight = Math.ceil( + parseFloat( + window.getComputedStyle(htmlElement(PROJECT_TITLE)).lineHeight, + ), + ); + + const descriptionLineHeight = Math.ceil( + parseFloat( + window.getComputedStyle(htmlElement(PROJECT_DESCRIPTION)).lineHeight, + ), + ); + + assert.equal(titleHeight, titleLineHeight * 2); + assert.equal(descriptionHeight, descriptionLineHeight * 3); + }); +}); diff --git a/web/tests/integration/components/truncated-text-test.ts b/web/tests/integration/components/truncated-text-test.ts index b5241e046..ec4968bf0 100644 --- a/web/tests/integration/components/truncated-text-test.ts +++ b/web/tests/integration/components/truncated-text-test.ts @@ -27,7 +27,7 @@ module("Integration | Component | truncated-text", function (hooks) { emberAssert( "container must be an HTMLElement", - container instanceof HTMLElement + container instanceof HTMLElement, ); emberAssert("text must be an HTMLElement", text instanceof HTMLElement);