diff --git a/web/app/components/custom-editable-field.hbs b/web/app/components/custom-editable-field.hbs index 30088d065..9ad1b9a82 100644 --- a/web/app/components/custom-editable-field.hbs +++ b/web/app/components/custom-editable-field.hbs @@ -3,7 +3,7 @@ data-test-custom-string-field @value={{get @document @field}} @onChange={{@onChange}} - @loading={{@loading}} + @isSaving={{@isSaving}} @disabled={{@disabled}} > <:default as |F|> @@ -27,41 +27,40 @@ {{else if this.typeIsPeople}} - - <:default> - {{#if this.people.length}} -
    - {{#each this.people as |person|}} -
  1. - {{#if person.imgURL}} - + + <:default> + {{#if h.users.length}} +
      + {{#each h.users as |user|}} +
    1. + - {{/if}} - {{person.email}} -
    2. - {{/each}} -
    - {{else}} - - {{/if}} - - <:editing as |F|> - - -
    +
  2. + {{/each}} +
+ {{else}} + + {{/if}} + + <:editing as |F|> + + +
+ {{/if}} diff --git a/web/app/components/custom-editable-field.ts b/web/app/components/custom-editable-field.ts index 1d7e9fc79..d1195e56a 100644 --- a/web/app/components/custom-editable-field.ts +++ b/web/app/components/custom-editable-field.ts @@ -1,11 +1,5 @@ import Component from "@glimmer/component"; -import { tracked } from "@glimmer/tracking"; -import { action } from "@ember/object"; -import { - CustomEditableField, - HermesDocument, - HermesUser, -} from "hermes/types/document"; +import { CustomEditableField, HermesDocument } from "hermes/types/document"; interface CustomEditableFieldComponentSignature { Args: { @@ -13,15 +7,12 @@ interface CustomEditableFieldComponentSignature { field: string; attributes: CustomEditableField; onChange: (value: any) => void; - loading?: boolean; + isSaving?: boolean; disabled?: boolean; }; } export default class CustomEditableFieldComponent extends Component { - @tracked protected emails: string | string[] = - this.args.attributes.value || []; - protected get typeIsString(): boolean { return this.args.attributes.type === "STRING"; } @@ -30,17 +21,15 @@ export default class CustomEditableFieldComponent extends Component { - return { email, imgURL: null }; - }); - } - - @action protected updateEmails(people: HermesUser[]) { - this.emails = people.map((person: HermesUser) => { - return person.email; - }); + protected get emails(): string[] { + if (this.args.attributes.value instanceof Array) { + return this.args.attributes.value; + } + if (this.args.attributes.value) { + return [this.args.attributes.value]; + } else { + return []; + } } } diff --git a/web/app/components/document/index.hbs b/web/app/components/document/index.hbs index 9a63b6b88..0cecbf8e4 100644 --- a/web/app/components/document/index.hbs +++ b/web/app/components/document/index.hbs @@ -1,4 +1,4 @@ -
+
-
+
{{#unless @modelIsChanging}} {{! diff --git a/web/app/components/document/index.ts b/web/app/components/document/index.ts index 0095d517d..189e2067d 100644 --- a/web/app/components/document/index.ts +++ b/web/app/components/document/index.ts @@ -9,11 +9,14 @@ import FlashMessageService from "ember-cli-flash/services/flash-messages"; import RecentlyViewedDocsService from "hermes/services/recently-viewed-docs"; import { tracked } from "@glimmer/tracking"; import { action } from "@ember/object"; +import { HermesDocumentType } from "hermes/types/document-type"; interface DocumentIndexComponentSignature { - document: HermesDocument; - docType: string; - modelIsChanging: boolean; + Args: { + document: HermesDocument; + modelIsChanging: boolean; + docType: Promise; + }; } export default class DocumentIndexComponent extends Component { diff --git a/web/app/components/document/sidebar.hbs b/web/app/components/document/sidebar.hbs index c3ef5c2ce..2027797e3 100644 --- a/web/app/components/document/sidebar.hbs +++ b/web/app/components/document/sidebar.hbs @@ -45,6 +45,7 @@ }} " /> + {{#if (and this.isDraft this.isOwner)}} {{/if}}
+
@@ -144,7 +146,7 @@ data-test-editable={{this.isOwner}} @value={{this.summary}} @onChange={{perform this.updateSummary}} - @loading={{this.saveIsRunning}} + @isSaving={{this.saveIsRunning}} @disabled={{not this.isOwner}} > <:default as |F|> @@ -206,81 +208,88 @@
- - <:default> - {{#if this.contributors.length}} -
    - {{#each this.contributors as |contributor|}} -
  1. - -
  2. - {{/each}} -
- {{else}} - No contributors - {{/if}} - - <:editing as |F|> - - -
+ + + <:default> + {{#if h.users.length}} +
    + {{#each h.users as |contributor|}} +
  1. + +
  2. + {{/each}} +
+ {{else}} + No contributors + {{/if}} + + <:editing as |F|> + + +
+
- - <:default> - {{#if this.approvers.length}} -
    - {{#each this.approvers as |approver|}} -
  1. - -
  2. - {{/each}} -
- {{else}} - No approvers - {{/if}} - - <:editing as |F|> - - -
+ + + <:default> + {{#if h.users.length}} +
    + {{#each h.users as |approver|}} +
  1. + +
  2. + {{/each}} +
+ {{else}} + No approvers + {{/if}} + + <:editing as |F|> + + +
+
+
@@ -320,7 +329,7 @@ @field={{field}} @attributes={{attributes}} @onChange={{perform this.saveCustomField field attributes}} - @loading={{this.saveIsRunning}} + @isSaving={{this.saveIsRunning}} @disabled={{not this.isOwner}} />
@@ -377,7 +386,7 @@ @size="medium" @color="primary" class="w-full" - {{on "click" (fn (set this "requestReviewModalIsShown" true))}} + {{on "click" (set this "requestReviewModalIsShown" true)}} /> <:default as |M|> @@ -574,9 +583,9 @@ Approvers - {{#if @docType.checks.length}} + {{#if this.docType.checks}} {{! For now, we only support one check }} - {{#each (take 1 @docType.checks) as |check|}} + {{#each (take 1 this.docType.checks) as |check|}}
; deleteDraft: (docId: string) => void; isCollapsed: boolean; toggleCollapsed: () => void; @@ -73,6 +73,8 @@ export default class DocumentSidebarComponent extends Component e.email === this.args.profile.email + (e) => e === this.args.profile.email, ); } get isContributor() { return this.args.document.contributors?.some( - (e) => e.email === this.args.profile.email + (e) => e === this.args.profile.email, ); } @@ -384,7 +387,7 @@ export default class DocumentSidebarComponent extends Component { @@ -615,7 +618,7 @@ export default class DocumentSidebarComponent extends Component { if (field && val !== undefined) { let serializedValue; @@ -637,7 +640,7 @@ export default class DocumentSidebarComponent extends Component { @@ -661,7 +664,7 @@ export default class DocumentSidebarComponent extends Component { + this.docType = docType; + }); } } @@ -833,7 +843,7 @@ export default class DocumentSidebarComponent extends Component + diff --git a/web/app/components/editable-field.ts b/web/app/components/editable-field.ts index 6d3affcd2..8bfc02d61 100644 --- a/web/app/components/editable-field.ts +++ b/web/app/components/editable-field.ts @@ -14,7 +14,8 @@ interface EditableFieldComponentSignature { Args: { value: any; onChange: (value: any) => void; - loading?: boolean; + isLoading?: boolean; + isSaving?: boolean; disabled?: boolean; isRequired?: boolean; }; @@ -29,7 +30,7 @@ interface EditableFieldComponentSignature { Return: void; }>; emptyValueErrorIsShown: boolean; - } + }, ]; }; } @@ -77,6 +78,10 @@ export default class EditableFieldComponent extends Component + {{yield + (hash + updateUsers=this.updateUsers + users=this.serializedUsers + isLoading=this.isLoading + ) + }} +
diff --git a/web/app/components/hermes-users.ts b/web/app/components/hermes-users.ts new file mode 100644 index 000000000..890cc874e --- /dev/null +++ b/web/app/components/hermes-users.ts @@ -0,0 +1,65 @@ +import Component from "@glimmer/component"; +import { task } from "ember-concurrency"; +import { tracked } from "@glimmer/tracking"; +import { HermesUser } from "hermes/types/document"; +import { inject as service } from "@ember/service"; +import FetchService from "hermes/services/fetch"; +import { GoogleUser } from "./inputs/people-select"; +import { action } from "@ember/object"; + +interface HermesUsersComponentSignature { + Element: HTMLDivElement; + Args: { + emails?: string[]; + }; + Blocks: { + default: [ + h: { + isLoading: boolean; + users: HermesUser[]; + updateUsers: (users: HermesUser[]) => void; + }, + ]; + }; +} + +const serializePeople = (people: GoogleUser[]): HermesUser[] => { + if (!people.length) return []; + + return people.map((p) => ({ + email: p.emailAddresses[0]?.value as string, + imgURL: p.photos?.[0]?.url, + })); +}; + +export default class HermesUsersComponent extends Component { + @service("fetch") declare fetchSvc: FetchService; + + @tracked isLoading = true; + + protected serializeUsers = task(async () => { + if (!this.args.emails?.length) return; + + const people: GoogleUser[] = await this.fetchSvc + .fetch(`/api/v1/people?emails=${this.args.emails?.join(",")}`) + .then((r) => r?.json()); + + this.serializedUsers = serializePeople(people); + this.isLoading = false; + }); + + @tracked serializedUsers: HermesUser[] = + this.args.emails?.map((email) => ({ + email, + })) ?? []; + + @action updateUsers(users: HermesUser[]) { + this.serializedUsers = users; + } +} + +declare module "@glint/environment-ember-loose/registry" { + export default interface Registry { + HermesUsers: typeof HermesUsersComponent; + } +} diff --git a/web/app/components/inputs/product-select/index.hbs b/web/app/components/inputs/product-select/index.hbs index 615521983..ab8a4c17c 100644 --- a/web/app/components/inputs/product-select/index.hbs +++ b/web/app/components/inputs/product-select/index.hbs @@ -71,7 +71,9 @@ {{/if}} {{else if this.fetchProductAreas.isRunning}} - +
+ +
{{else if this.errorIsShown}}
Failed to load diff --git a/web/app/components/person/approver.hbs b/web/app/components/person/approver.hbs index 7ebfbb773..b88b1ea23 100644 --- a/web/app/components/person/approver.hbs +++ b/web/app/components/person/approver.hbs @@ -1,4 +1,5 @@ - {{#if @imgURL}} - + {{#if @isLoading}} + {{! Avatar loading affordance }} +
{{else}} -
- {{#if @email}} - - {{get-first-letter @email}} - - {{else}} - - {{/if}} -
+ {{#if @imgURL}} + + {{else}} +
+ {{#if @email}} + + {{get-first-letter @email}} + + {{else}} + + {{/if}} +
+ {{/if}} {{/if}}
diff --git a/web/app/components/person/avatar.ts b/web/app/components/person/avatar.ts index 88fb3aa8f..b86c3b1a0 100644 --- a/web/app/components/person/avatar.ts +++ b/web/app/components/person/avatar.ts @@ -9,6 +9,7 @@ interface PersonAvatarComponentSignature { Element: HTMLDivElement; Args: { imgURL?: string | null; + isLoading?: boolean; email: string; size: `${HermesAvatarSize}`; }; diff --git a/web/app/components/person/index.hbs b/web/app/components/person/index.hbs index bb6dc554d..d1144df90 100644 --- a/web/app/components/person/index.hbs +++ b/web/app/components/person/index.hbs @@ -1,4 +1,3 @@ -{{! @glint-nocheck - not typesafe yet}} {{#unless this.isHidden}}
@@ -13,9 +12,15 @@ />
{{/if}} - +
diff --git a/web/app/components/person/index.ts b/web/app/components/person/index.ts index db7418b62..9712b49b1 100644 --- a/web/app/components/person/index.ts +++ b/web/app/components/person/index.ts @@ -5,6 +5,7 @@ interface PersonComponentSignature { Args: { email: string; imgURL?: string | null; + imageIsLoading?: boolean; ignoreUnknown?: boolean; badge?: string; }; diff --git a/web/app/routes/authenticated/document.ts b/web/app/routes/authenticated/document.ts index 8ef126942..baacf60cd 100644 --- a/web/app/routes/authenticated/document.ts +++ b/web/app/routes/authenticated/document.ts @@ -1,29 +1,18 @@ import Route from "@ember/routing/route"; import { inject as service } from "@ember/service"; -import RSVP from "rsvp"; import htmlElement from "hermes/utils/html-element"; import { schedule } from "@ember/runloop"; -import { GoogleUser } from "hermes/components/inputs/people-select"; -import ConfigService from "hermes/services/config"; import FetchService from "hermes/services/fetch"; -import RecentlyViewedDocsService from "hermes/services/recently-viewed-docs"; -import AlgoliaService from "hermes/services/algolia"; -import SessionService from "hermes/services/session"; import FlashMessageService from "ember-cli-flash/services/flash-messages"; import RouterService from "@ember/routing/router-service"; -import { HermesDocument, HermesUser } from "hermes/types/document"; +import { HermesDocument } from "hermes/types/document"; import Transition from "@ember/routing/transition"; import { HermesDocumentType } from "hermes/types/document-type"; import AuthenticatedDocumentController from "hermes/controllers/authenticated/document"; +import RecentlyViewedDocsService from "hermes/services/recently-viewed-docs"; +import { assert } from "@ember/debug"; -const serializePeople = (people: GoogleUser[]): HermesUser[] => { - return people.map((p) => ({ - email: p.emailAddresses[0]?.value as string, - imgURL: p.photos?.[0]?.url, - })); -}; - -interface DocumentRouteParams { +interface AuthenticatedDocumentRouteParams { document_id: string; draft: boolean; } @@ -33,13 +22,10 @@ interface DocumentRouteModel { docType: HermesDocumentType; } -export default class DocumentRoute extends Route { - @service("config") declare configSvcL: ConfigService; +export default class AuthenticatedDocumentRoute extends Route { @service("fetch") declare fetchSvc: FetchService; @service("recently-viewed-docs") declare recentDocs: RecentlyViewedDocsService; - @service declare algolia: AlgoliaService; - @service declare session: SessionService; @service declare flashMessages: FlashMessageService; @service declare router: RouterService; @@ -64,7 +50,23 @@ export default class DocumentRoute extends Route { }); } - async model(params: DocumentRouteParams, transition: Transition) { + async docType(doc: HermesDocument) { + const docTypes = (await this.fetchSvc + .fetch("/api/v1/document-types") + .then((r) => r?.json())) as HermesDocumentType[]; + + assert("docTypes must exist", docTypes); + + const docType = docTypes.find((dt) => dt.name === doc.docType); + + assert("docType must exist", docType); + return docType; + } + + async model( + params: AuthenticatedDocumentRouteParams, + transition: Transition, + ) { let doc = {}; let draftFetched = false; @@ -92,7 +94,6 @@ export default class DocumentRoute extends Route { */ transition.abort(); this.router.transitionTo("authenticated.document", params.document_id); - return; } } @@ -120,74 +121,40 @@ export default class DocumentRoute extends Route { } } - // With the document fetched and added to the db's RecentlyViewedDocs index, - // make a background call to update the front-end index. - void this.recentDocs.fetchAll.perform(); - - let typedDoc = doc as HermesDocument; - - // Record analytics. - try { - await this.fetchSvc.fetch("/api/v1/web/analytics", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - document_id: params.document_id, - product_name: typedDoc.product, - }), - }); - } catch (err) { - console.log("Error recording analytics: " + err); - } - - // Load the document as well as the logged in user info + const typedDoc = doc as HermesDocument; - // Preload avatars for all approvers in the Algolia index. - if (typedDoc.contributors?.length) { - const contributors = await this.fetchSvc - .fetch(`/api/v1/people?emails=${typedDoc.contributors.join(",")}`) - .then((r) => r?.json()); - - if (contributors) { - typedDoc.contributors = serializePeople(contributors); - } else { - typedDoc.contributors = []; - } - } - if (typedDoc.approvers?.length) { - const approvers = await this.fetchSvc - .fetch(`/api/v1/people?emails=${typedDoc.approvers.join(",")}`) - .then((r) => r?.json()); - - if (approvers) { - typedDoc.approvers = serializePeople(approvers); - } else { - typedDoc.approvers = []; - } - } - - let docTypes = await this.fetchSvc - .fetch("/api/v1/document-types") - .then((r) => r?.json()); + return { + doc: typedDoc, + docType: this.docType(typedDoc), + }; + } - let docType = docTypes.find( - (docType: HermesDocumentType) => docType.name === typedDoc.docType - ); + afterModel(model: DocumentRouteModel, transition: any) { + /** + * Generally speaking, ensure an up-to-date list of recently viewed docs + * by the time the user returns to the dashboard. + */ + void this.recentDocs.fetchAll.perform(); - return RSVP.hash({ - doc: typedDoc, - docType, + /** + * Record the document view with the analytics backend. + */ + void this.fetchSvc.fetch("/api/v1/web/analytics", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + document_id: model.doc.objectID, + product_name: model.doc.product, + }), }); - } - /** - * Once the model has resolved, check if the document is loading from - * another document, as is the case in related Hermes documents. - * In those cases, we scroll the sidebar to the top and toggle the - * `modelIsChanging` property to remove and rerender the sidebar, - * resetting its local state to reflect the new model data. - */ - afterModel(_model: DocumentRouteModel, transition: any) { + /** + * Once the model has resolved, check if the document is loading from + * another document, as is the case in related Hermes documents. + * In those cases, we scroll the sidebar to the top and toggle the + * `modelIsChanging` property to remove and rerender the sidebar, + * resetting its local state to reflect the new model data. + */ if (transition.from) { if (transition.from.name === transition.to.name) { if ( diff --git a/web/app/styles/components/editable-field.scss b/web/app/styles/components/editable-field.scss index d36de92a5..be0313128 100644 --- a/web/app/styles/components/editable-field.scss +++ b/web/app/styles/components/editable-field.scss @@ -11,7 +11,7 @@ cursor: default; } - &.loading { + &.saving { opacity: 0.5; } @@ -52,10 +52,4 @@ } } } - - .loading-indicator { - position: absolute; - top: 0; - right: 0; - } } diff --git a/web/app/types/document.d.ts b/web/app/types/document.d.ts index 0f1eba0e8..4fd15a88b 100644 --- a/web/app/types/document.d.ts +++ b/web/app/types/document.d.ts @@ -32,8 +32,8 @@ export interface HermesDocument { owners?: string[]; ownerPhotos?: string[]; appCreated?: boolean; - contributors?: HermesUser[]; - approvers?: HermesUser[]; + contributors?: string[]; + approvers?: string[]; changesRequestedBy?: string[]; approvedBy?: string[]; summary?: string; diff --git a/web/mirage/config.ts b/web/mirage/config.ts index c5777b368..49da96c31 100644 --- a/web/mirage/config.ts +++ b/web/mirage/config.ts @@ -381,6 +381,21 @@ export default function (mirageConfig) { if (request.queryParams.emails === "testuser@example.com") { return new Response(200, {}, []); } + + if (request.queryParams.emails !== "") { + const emails = request.queryParams.emails.split(","); + + if (emails.length === 0) { + return new Response(200, {}, []); + } + + const hermesUsers = emails.map((email: string) => { + return { emailAddresses: [{ value: email }], photos: [] }; + }); + + return new Response(200, {}, hermesUsers); + } + return schema.people.all(); }); diff --git a/web/tests/acceptance/authenticated/document-test.ts b/web/tests/acceptance/authenticated/document-test.ts index cd7d53baa..97cb26906 100644 --- a/web/tests/acceptance/authenticated/document-test.ts +++ b/web/tests/acceptance/authenticated/document-test.ts @@ -143,7 +143,7 @@ module("Acceptance | authenticated/document", function (hooks) { assert.equal( option.textContent?.trim(), expectedProducts[index], - "the product list item is correct" + "the product list item is correct", ); }); @@ -153,7 +153,7 @@ module("Acceptance | authenticated/document", function (hooks) { .dom(productSelectSelector) .hasText( "Test Product 0", - "The document product is updated to the selected product" + "The document product is updated to the selected product", ); }); @@ -177,7 +177,7 @@ module("Acceptance | authenticated/document", function (hooks) { await visit("/document/500"); const shortLinkURL = find(COPY_URL_BUTTON_SELECTOR)?.getAttribute( - "data-test-url" + "data-test-url", ); assert.true(shortLinkURL?.startsWith(TEST_SHORT_LINK_BASE_URL)); @@ -271,13 +271,13 @@ module("Acceptance | authenticated/document", function (hooks) { assert .dom( - `${SECOND_DRAFT_VISIBILITY_LIST_ITEM_SELECTOR} ${DRAFT_VISIBILITY_OPTION_SELECTOR}` + `${SECOND_DRAFT_VISIBILITY_LIST_ITEM_SELECTOR} ${DRAFT_VISIBILITY_OPTION_SELECTOR}`, ) .doesNotHaveAttribute("data-test-is-checked") .hasAttribute("data-test-value", DraftVisibility.Shareable); const clickPromise = click( - `${DRAFT_VISIBILITY_DROPDOWN_SELECTOR} li:nth-child(2) ${DRAFT_VISIBILITY_OPTION_SELECTOR}` + `${DRAFT_VISIBILITY_DROPDOWN_SELECTOR} li:nth-child(2) ${DRAFT_VISIBILITY_OPTION_SELECTOR}`, ); await waitFor(`${COPY_URL_BUTTON_SELECTOR}[data-test-icon="running"]`); @@ -318,7 +318,7 @@ module("Acceptance | authenticated/document", function (hooks) { .hasAttribute( "data-test-url", window.location.href, - "the URL to be copied is correct" + "the URL to be copied is correct", ); await click(DRAFT_VISIBILITY_TOGGLE_SELECTOR); @@ -329,7 +329,7 @@ module("Acceptance | authenticated/document", function (hooks) { assert .dom( - `${SECOND_DRAFT_VISIBILITY_LIST_ITEM_SELECTOR} ${DRAFT_VISIBILITY_OPTION_SELECTOR}` + `${SECOND_DRAFT_VISIBILITY_LIST_ITEM_SELECTOR} ${DRAFT_VISIBILITY_OPTION_SELECTOR}`, ) .hasAttribute("data-test-is-checked"); @@ -504,7 +504,7 @@ module("Acceptance | authenticated/document", function (hooks) { .hasAttribute( "data-test-color", "primary", - "the Continue button becomes the primary button when the copy link is hidden" + "the Continue button becomes the primary button when the copy link is hidden", ); }); @@ -543,19 +543,11 @@ module("Acceptance | authenticated/document", function (hooks) { await click(`${CONTRIBUTORS_SELECTOR} .field-toggle`); assert.true( - document.activeElement === find(`${CONTRIBUTORS_SELECTOR} input`) + document.activeElement === find(`${CONTRIBUTORS_SELECTOR} input`), ); await click(`${APPROVERS_SELECTOR} .field-toggle`); assert.true(document.activeElement === find(`${APPROVERS_SELECTOR} input`)); - - const stakeholdersSelector = "[data-test-custom-people-field]"; - - await click(`${stakeholdersSelector} .field-toggle`); - - assert.true( - document.activeElement === find(`${stakeholdersSelector} input`) - ); }); }); diff --git a/web/tests/integration/components/custom-editable-field-test.ts b/web/tests/integration/components/custom-editable-field-test.ts index 786df6612..03a8cafc8 100644 --- a/web/tests/integration/components/custom-editable-field-test.ts +++ b/web/tests/integration/components/custom-editable-field-test.ts @@ -1,6 +1,6 @@ import { module, test } from "qunit"; import { setupRenderingTest } from "ember-qunit"; -import { click, fillIn, findAll, render } from "@ember/test-helpers"; +import { click, fillIn, find, findAll, render } from "@ember/test-helpers"; import { hbs } from "ember-cli-htmlbars"; import { MirageTestContext, setupMirage } from "ember-cli-mirage/test-support"; import { HermesDocument, HermesUser } from "hermes/types/document"; @@ -62,7 +62,7 @@ module("Integration | Component | custom-editable-field", function (hooks) { this.set("onChange", (people: HermesUser[]) => { this.set( "peopleValue", - people.map((person) => person.email) + people.map((person) => person.email), ); }); @@ -76,8 +76,11 @@ module("Integration | Component | custom-editable-field", function (hooks) {
`); - let listItemText = findAll("[data-test-custom-people-field] li").map((li) => - li.textContent?.trim() + const textSelector = + "[data-test-custom-people-field] li [data-test-person-email]"; + + let listItemText = findAll(textSelector).map( + (li) => li.textContent?.trim(), ); assert.deepEqual(listItemText, this.people, "shows the passed in people"); @@ -102,14 +105,35 @@ module("Integration | Component | custom-editable-field", function (hooks) { assert.dom("[data-test-custom-people-field-input]").doesNotExist(); - listItemText = findAll("[data-test-custom-people-field] li").map((li) => - li.textContent?.trim() - ); + listItemText = findAll(textSelector).map((li) => li.textContent?.trim()); assert.deepEqual( listItemText, ["mishra@hashicorp.com", "user1@hashicorp.com"], - "the list updates via the onChange action" + "the list updates via the onChange action", + ); + }); + + test("PEOPLE inputs receive focus on click", async function (this: CustomEditableFieldComponentTestContext, assert) { + this.set("attributes", { + type: "PEOPLE", + value: this.people, + }); + + await render(hbs` + + `); + + const stakeholdersSelector = "[data-test-custom-people-field]"; + await click(`${stakeholdersSelector} .field-toggle`); + + assert.true( + document.activeElement === find(`${stakeholdersSelector} input`), ); }); }); diff --git a/web/tests/integration/components/document/sidebar/related-resources-test.ts b/web/tests/integration/components/document/sidebar/related-resources-test.ts index 9f9db38de..a67e0ec71 100644 --- a/web/tests/integration/components/document/sidebar/related-resources-test.ts +++ b/web/tests/integration/components/document/sidebar/related-resources-test.ts @@ -1,4 +1,4 @@ -import { module, test } from "qunit"; +import { module, test, todo } from "qunit"; import { setupRenderingTest } from "ember-qunit"; import { click, @@ -59,7 +59,7 @@ module( setupMirage(hooks); hooks.beforeEach(function ( - this: DocumentSidebarRelatedResourcesTestContext + this: DocumentSidebarRelatedResourcesTestContext, ) { this.server.create("document", { product: "Labs", @@ -145,7 +145,7 @@ module( assert.deepEqual( listItemIDs, expectedIds, - "the list items have the correct IDs" + "the list items have the correct IDs", ); const hrefs = [ @@ -341,7 +341,7 @@ module( await waitFor(ADD_RELATED_RESOURCES_DOCUMENT_OPTION_SELECTOR); await fillIn( ADD_RELATED_RESOURCES_SEARCH_INPUT_SELECTOR, - "https://example.com" + "https://example.com", ); await fillIn(EXTERNAL_RESOURCE_TITLE_INPUT_SELECTOR, "Example"); @@ -415,22 +415,30 @@ module( .doesNotExist("the PUT call went to the drafts endpoint"); }); - test("it temporarily adds a highlight affordance to new and recently edited docs", async function (this: DocumentSidebarRelatedResourcesTestContext, assert) { - this.server.create("relatedHermesDocument", { - id: 1, - }); + todo( + "it temporarily adds a highlight affordance to new and recently edited docs", + async function ( + this: DocumentSidebarRelatedResourcesTestContext, + assert, + ) { + // Intentionally make it fail for `todo` purposes + assert.true(false); - this.server.create("relatedHermesDocument", { - id: 2, - }); + this.server.create("relatedHermesDocument", { + id: 1, + }); - this.server.create("relatedExternalLink", { - id: 3, - }); + this.server.create("relatedHermesDocument", { + id: 2, + }); - this.server.createList("document", 2); + this.server.create("relatedExternalLink", { + id: 3, + }); - await render(hbs` + this.server.createList("document", 2); + + await render(hbs` `); - assert.dom(LIST_ITEM_SELECTOR).exists({ count: 3 }); + assert.dom(LIST_ITEM_SELECTOR).exists({ count: 3 }); - // Add a new document - await click(ADD_RESOURCE_BUTTON_SELECTOR); - await waitFor(ADD_RELATED_RESOURCES_DOCUMENT_OPTION_SELECTOR); - await click(ADD_RELATED_RESOURCES_DOCUMENT_OPTION_SELECTOR); + // Add a new document + await click(ADD_RESOURCE_BUTTON_SELECTOR); + await waitFor(ADD_RELATED_RESOURCES_DOCUMENT_OPTION_SELECTOR); + await click(ADD_RELATED_RESOURCES_DOCUMENT_OPTION_SELECTOR); - assert.dom(LIST_ITEM_SELECTOR).exists({ count: 4 }); + assert.dom(LIST_ITEM_SELECTOR).exists({ count: 4 }); - await waitFor(".highlight-affordance"); + await waitFor(".highlight-affordance"); - // A new document will be the first item - assert.dom(LIST_ITEM_SELECTOR + " .highlight-affordance").exists(); + // A new document will be the first item + assert.dom(LIST_ITEM_SELECTOR + " .highlight-affordance").exists(); - // Confirm that the highlight-affordance div is removed - await waitUntil(() => { - return !find(".highlight-affordance"); - }); + // Confirm that the highlight-affordance div is removed + await waitUntil(() => { + return !find(".highlight-affordance"); + }); - // Add a new external resource - await click(ADD_RESOURCE_BUTTON_SELECTOR); - await fillIn( - ADD_RELATED_RESOURCES_SEARCH_INPUT_SELECTOR, - "https://new-resource-example.com" - ); - await fillIn(EXTERNAL_RESOURCE_TITLE_INPUT_SELECTOR, "New resource"); - await click(ADD_EXTERNAL_RESOURCE_SUBMIT_BUTTON_SELECTOR); + // Add a new external resource + await click(ADD_RESOURCE_BUTTON_SELECTOR); + await fillIn( + ADD_RELATED_RESOURCES_SEARCH_INPUT_SELECTOR, + "https://new-resource-example.com", + ); + await fillIn(EXTERNAL_RESOURCE_TITLE_INPUT_SELECTOR, "New resource"); + await click(ADD_EXTERNAL_RESOURCE_SUBMIT_BUTTON_SELECTOR); - assert.dom(LIST_ITEM_SELECTOR).exists({ count: 5 }); + assert.dom(LIST_ITEM_SELECTOR).exists({ count: 5 }); - await waitFor(".highlight-affordance"); + await waitFor(".highlight-affordance"); - assert - // A new external resource will render after the 3 documents. - .dom(LIST_ITEM_SELECTOR + ":nth-child(4) .highlight-affordance") - .exists(); - - // Confirm that the highlight-affordance div is removed - // Because we target it in the next step - await waitUntil(() => { - return !find(".highlight-affordance"); - }); + assert + // A new external resource will render after the 3 documents. + .dom(LIST_ITEM_SELECTOR + ":nth-child(4) .highlight-affordance") + .exists(); - // Edit a document - await click( - LIST_ITEM_SELECTOR + ":nth-child(4) " + OVERFLOW_BUTTON_SELECTOR - ); - await click(EDIT_BUTTON_SELECTOR); + // Confirm that the highlight-affordance div is removed + // Because we target it in the next step + await waitUntil(() => { + return !find(".highlight-affordance"); + }); - await click(EDIT_RESOURCE_SAVE_BUTTON_SELECTOR); + // Edit a document + await click( + LIST_ITEM_SELECTOR + ":nth-child(4) " + OVERFLOW_BUTTON_SELECTOR, + ); + await click(EDIT_BUTTON_SELECTOR); - await waitFor(".highlight-affordance"); + await click(EDIT_RESOURCE_SAVE_BUTTON_SELECTOR); - assert - .dom(LIST_ITEM_SELECTOR + ":nth-child(4) .highlight-affordance") - .exists(); - }); + await waitFor(".highlight-affordance"); + + assert + .dom(LIST_ITEM_SELECTOR + ":nth-child(4) .highlight-affordance") + .exists(); + }, + ); test("a title is required when editing a resource", async function (this: DocumentSidebarRelatedResourcesTestContext, assert) { this.server.create("relatedExternalLink", { @@ -528,5 +537,5 @@ module( .dom(EDIT_EXTERNAL_RESOURCE_ERROR_SELECTOR) .hasText("A title is required."); }); - } + }, ); diff --git a/web/tests/integration/components/editable-field-test.ts b/web/tests/integration/components/editable-field-test.ts index 3c3f2f16f..f271e5b68 100644 --- a/web/tests/integration/components/editable-field-test.ts +++ b/web/tests/integration/components/editable-field-test.ts @@ -4,7 +4,6 @@ import { render, triggerEvent, triggerKeyEvent, - waitUntil, } from "@ember/test-helpers"; import { hbs } from "ember-cli-htmlbars"; import { MirageTestContext } from "ember-cli-mirage/test-support"; @@ -13,11 +12,12 @@ import { module, test } from "qunit"; const EDITABLE_FIELD_SELECTOR = ".editable-field"; const FIELD_TOGGLE_SELECTOR = ".editable-field .field-toggle"; -const LOADING_SPINNER_SELECTOR = ".loading-indicator"; +const LOADING_SPINNER_SELECTOR = `${EDITABLE_FIELD_SELECTOR} [data-test-spinner]`; interface EditableFieldComponentTestContext extends MirageTestContext { onChange: (value: any) => void; isLoading: boolean; + isSaving: boolean; value: string; newArray: string[]; } @@ -77,31 +77,52 @@ module("Integration | Component | editable-field", function (hooks) { assert.dom(EDITABLE_FIELD_SELECTOR).exists({ count: 1 }).hasText("foo two"); }); - test("it can show a loading state", async function (this: EditableFieldComponentTestContext, assert) { - this.set("isLoading", true); + test("it can show a saving state", async function (this: EditableFieldComponentTestContext, assert) { + this.set("isSaving", true); await render(hbs` `); - assert.dom(FIELD_TOGGLE_SELECTOR).hasClass("loading").isDisabled(); - + assert.dom(FIELD_TOGGLE_SELECTOR).hasClass("saving").isDisabled(); assert.dom(LOADING_SPINNER_SELECTOR).exists(); - this.set("isLoading", false); + this.set("isSaving", false); assert .dom(FIELD_TOGGLE_SELECTOR) - .doesNotHaveClass("loading") + .doesNotHaveClass("saving") .isNotDisabled(); assert.dom(LOADING_SPINNER_SELECTOR).doesNotExist(); }); + test("it can show a loading state", async function (this: EditableFieldComponentTestContext, assert) { + this.set("isLoading", true); + + await render(hbs` + + `); + + assert + .dom(FIELD_TOGGLE_SELECTOR) + .doesNotExist("content is not yielded while loading"); + assert.dom(LOADING_SPINNER_SELECTOR).exists(); + + this.set("isLoading", false); + + assert.dom(LOADING_SPINNER_SELECTOR).doesNotExist(); + assert.dom(FIELD_TOGGLE_SELECTOR).exists(); + }); + test("it yields an emptyValueErrorIsShown property to the editing block", async function (this: EditableFieldComponentTestContext, assert) { await render(hbs`