Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Create Product::Avatar component #394

Merged
merged 15 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions web/app/components/doc/thumbnail.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@
{{/if}}

{{#if this.productShortName}}
<div
data-test-doc-thumbnail-product-badge
class="product-badge {{this.productShortName}}"
>
<FlightIcon @size="16" @name={{this.productShortName}} />
<div class="absolute left-0 bottom-0">
<Product::Avatar
data-test-doc-thumbnail-product-badge
@product={{@product}}
@size={{this.productAvatarSize}}
/>
</div>
{{/if}}
</div>
Expand Down
13 changes: 13 additions & 0 deletions web/app/components/doc/thumbnail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import Component from "@glimmer/component";
import { dasherize } from "@ember/string";
import getProductId from "hermes/utils/get-product-id";

export enum DocThumbnailSize {
Small = "small",
Large = "large",
}

interface DocThumbnailComponentSignature {
Element: HTMLDivElement;
Args: {
Expand All @@ -20,6 +25,14 @@ export default class DocThumbnailComponent extends Component<DocThumbnailCompone
}
}

protected get productAvatarSize() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need sizeIsLarge() with this?

if (this.sizeIsLarge) {
return DocThumbnailSize.Large;
} else {
return DocThumbnailSize.Small;
}
}

protected get sizeIsLarge(): boolean {
return this.args.size === "large";
}
Expand Down
11 changes: 4 additions & 7 deletions web/app/components/document/sidebar.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,10 @@
>
{{#let (get-product-id this.product) as |productIcon|}}
{{#if productIcon}}
<div
class="product-badge
{{productIcon}}
{{if this.saveIsRunning 'opacity-50'}}"
>
<FlightIcon @name={{productIcon}} />
</div>
<Product::Avatar
@product={{productIcon}}
class="rounded-l-none {{if this.saveIsRunning 'opacity-50'}}"
/>
{{/if}}
{{/let}}

Expand Down
6 changes: 1 addition & 5 deletions web/app/components/person/avatar.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import Component from "@glimmer/component";

enum HermesAvatarSize {
Small = "small",
Medium = "medium",
}
import { HermesAvatarSize } from "hermes/types/avatar-size";

interface PersonAvatarComponentSignature {
Element: HTMLDivElement;
Expand Down
59 changes: 59 additions & 0 deletions web/app/components/product/avatar.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Component from "@glimmer/component";
import getProductID from "hermes/utils/get-product-id";
import { inject as service } from "@ember/service";
import FlightIcon from "@hashicorp/ember-flight-icons/components/flight-icon";
import ProductAreasService from "hermes/services/product-areas";
import { assert } from "@ember/debug";
import { HermesAvatarSize } from "hermes/types/avatar-size";

interface ProductAvatarComponentSignature {
Element: HTMLDivElement;
Args: {
product: string;
size?: `${HermesAvatarSize}`;
};
Blocks: {
default: [];
};
}

export default class ProductAvatarComponent extends Component<ProductAvatarComponentSignature> {
@service declare productAreas: ProductAreasService;

private get productID(): string {
const productID = getProductID(this.args.product);
assert("productID must edxist", productID);
return productID;
}

private get sizeIsMedium() {
return this.args.size === HermesAvatarSize.Medium;
}

private get sizeIsLarge() {
return this.args.size === HermesAvatarSize.Large;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious about this pattern vs. checking the value in the template using a more generic getter that just returns the size (like was added in the thumbnail component in this PR) - seems like that could be more scalable?

Copy link
Contributor Author

@jeffdaley jeffdaley Oct 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of it was the convenience of having everything in one file in the early phase. But it's worth noting that Tailwind's dynamic class rules limit our efficiency a bit.* Still, we're past the early phase, so we can be smarter here. I refactored to use CSS classes.

* Of course, there are workarounds. I think safe-listing the w- and h- classes would allow us to do stuff like class="h-{{this.size}}" if that were ever preferable to CSS.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, sounds good 👍


<template>
<div
data-test-product-avatar
class="product-badge relative flex shrink-0 shrink-0 items-center justify-center rounded-md
{{this.productID}}
{{if
this.sizeIsLarge
'h-8 w-8'
(if this.sizeIsMedium 'h-7 w-7' 'h-5 w-5')
}}
"
...attributes
>
<FlightIcon @name={{this.productID}} class="h-4 w-4" />
</div>
</template>
}

declare module "@glint/environment-ember-loose/registry" {
export default interface Registry {
"Product::Avatar": typeof ProductAvatarComponent;
}
}
24 changes: 11 additions & 13 deletions web/app/styles/components/doc/thumbnail.scss
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
.doc-thumbnail {
@apply relative w-12 shrink-0 flex overflow-hidden;
@apply relative flex w-12 shrink-0 overflow-hidden;

// Outer border / shadow
&::after {
content: "";
@apply absolute z-10 pointer-events-none;
@apply pointer-events-none absolute z-10;
// Make the element 1px smaller than its container
// so its shadow picks up colors from the elements below it.
@apply top-px right-px bottom-px left-px;
@apply shadow-surface-low rounded-sm;
@apply rounded-sm shadow-surface-low;
}

.status-icon {
@apply absolute opacity-75 mix-blend-multiply;
@apply w-9 h-9;
@apply h-9 w-9;

&.approved {
@apply fill-color-palette-green-200 -rotate-6;
@apply -rotate-6 fill-color-palette-green-200;
@apply -right-[7px] top-[5px];
}

Expand All @@ -27,8 +27,7 @@
}

.product-badge {
@apply bottom-0 left-0;
@apply w-[19px] h-[17px] rounded-bl rounded-tr;
@apply rounded-tl-none rounded-br-none rounded-bl rounded-tr;

.flight-icon {
@apply scale-75;
Expand All @@ -42,23 +41,22 @@
}

&.large {
// Match the width of the progress bars
@apply w-28;

&::after {
@apply shadow-surface-mid rounded;
@apply rounded shadow-surface-mid;
}

.product-badge {
@apply w-[36px] h-[32px] rounded-bl rounded-tr;
@apply rounded-bl rounded-tr;

.flight-icon {
@apply scale-100;
}
@apply scale-100;
}
}

.status-icon {
@apply w-[84px] h-[84px] top-[8px];
@apply top-[8px] h-[84px] w-[84px];

&.approved {
@apply -right-3.5;
Expand Down
5 changes: 5 additions & 0 deletions web/app/types/avatar-size.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum HermesAvatarSize {
Small = "small",
Medium = "medium",
Large = "large",
}
45 changes: 45 additions & 0 deletions web/tests/integration/components/product/avatar-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { TestContext, render } from "@ember/test-helpers";
import { hbs } from "ember-cli-htmlbars";
import { setupRenderingTest } from "ember-qunit";
import ProductAreasService from "hermes/services/product-areas";
import { module, test } from "qunit";

const AVATAR = "[data-test-product-avatar]";
const ICON = ".flight-icon";

interface ProductAvatarTestContext extends TestContext {
product: string;
}

module("Integration | Component | product/avatar", function (hooks) {
setupRenderingTest(hooks);

hooks.beforeEach(async function (this: ProductAvatarTestContext) {
const productAreasService = this.owner.lookup(
"service:product-areas",
) as ProductAreasService;
});

test("it renders the product icon", async function (this: ProductAvatarTestContext, assert) {
this.set("product", "Terraform");

await render<ProductAvatarTestContext>(hbs`
<Product::Avatar
@product={{this.product}}
/>
`);

assert.dom(AVATAR).hasClass("terraform");
assert.dom(ICON).hasAttribute("data-test-icon", "terraform");

this.set("product", "Vault");

assert.dom(AVATAR).hasClass("vault");
assert.dom(ICON).hasAttribute("data-test-icon", "vault");

// expect an error if the product is not found
assert.throws(() => {
this.set("product", "foo");
});
});
});