From f12d6956cc1e34f6772a7d95fc4275865379c667 Mon Sep 17 00:00:00 2001 From: FadhlanR Date: Wed, 8 Jan 2025 10:28:05 +0700 Subject: [PATCH] Show card type icon of CardsGird's filter list (#1971) --- packages/base/cards-grid.gts | 10 ++++-- .../src/components/filter-list/index.gts | 32 ++++++++++++++++--- .../host/app/components/card-prerender.gts | 6 ++-- packages/host/app/lib/current-run.ts | 9 ++++++ packages/host/app/lib/isolated-render.gts | 4 +-- packages/host/app/services/render-service.ts | 15 +++++++-- .../config/schema/1735668047598_schema.sql | 1 + ...734686216941_prerendered-card-type-icon.js | 13 ++++++++ .../realm-server/tests/realm-server-test.ts | 6 +++- packages/runtime-common/card-document.ts | 1 + packages/runtime-common/helpers/index.ts | 2 ++ packages/runtime-common/index-structure.ts | 2 ++ packages/runtime-common/index-writer.ts | 4 ++- .../runtime-common/tests/index-writer-test.ts | 28 ++++++++++++++-- 14 files changed, 115 insertions(+), 18 deletions(-) create mode 100644 packages/postgres/migrations/1734686216941_prerendered-card-type-icon.js diff --git a/packages/base/cards-grid.gts b/packages/base/cards-grid.gts index 10038734b0..c3ba4e64b8 100644 --- a/packages/base/cards-grid.gts +++ b/packages/base/cards-grid.gts @@ -306,7 +306,7 @@ class Isolated extends Component { - filters: { displayName: string; icon: IconComponent; query: any }[] = + filters: { displayName: string; icon: IconComponent | string; query: any }[] = new TrackedArray([ { displayName: 'All Cards', @@ -424,7 +424,11 @@ class Isolated extends Component { } let cardTypeSummaries = (await response.json()).data as { id: string; - attributes: { displayName: string; total: number }; + attributes: { + displayName: string; + total: number; + iconHTML: string | null; + }; }[]; let excludedCardTypeIds = [ `${baseRealm.url}card-api/CardDef`, @@ -441,7 +445,7 @@ class Isolated extends Component { const lastIndex = summary.id.lastIndexOf('/'); this.filters.push({ displayName: summary.attributes.displayName, - icon: Captions, + icon: summary.attributes.iconHTML ?? Captions, query: { filter: { type: { diff --git a/packages/boxel-ui/addon/src/components/filter-list/index.gts b/packages/boxel-ui/addon/src/components/filter-list/index.gts index 961a08047e..0345c44ced 100644 --- a/packages/boxel-ui/addon/src/components/filter-list/index.gts +++ b/packages/boxel-ui/addon/src/components/filter-list/index.gts @@ -10,11 +10,13 @@ export interface FilterListIconSignature { export type FilterListIcon = ComponentLike; +import { htmlSafe } from '@ember/template'; + import { cn, eq } from '../../helpers.ts'; export type Filter = { displayName: string; - icon: FilterListIcon; + icon: FilterListIcon | string; }; interface Signature { @@ -39,9 +41,16 @@ export default class FilterList extends Component { class={{cn 'filter-list__button' selected=(eq @activeFilter filter)}} {{on 'click' (fn this.onChanged filter)}} data-test-boxel-filter-list-button={{filter.displayName}} - >{{filter.displayName}} + > + {{#if (isIconString filter.icon)}} + {{htmlSafe + (addClassToSVG filter.icon 'filter-list__icon') + }}{{filter.displayName}} + {{else}} + {{filter.displayName}}{{/if}} + {{/each}} } + +function addClassToSVG(svgString: string, className: string) { + return svgString + .replace(/]*)\sclass="([^"]*)"/, `]*)>/, ``); +} + +function isIconString(icon: FilterListIcon | string): icon is string { + return typeof icon === 'string'; +} diff --git a/packages/host/app/components/card-prerender.gts b/packages/host/app/components/card-prerender.gts index cfd63c3a4d..c2c9fb1ed0 100644 --- a/packages/host/app/components/card-prerender.gts +++ b/packages/host/app/components/card-prerender.gts @@ -105,7 +105,8 @@ export default class CardPrerender extends Component { realmURL, reader, indexWriter, - renderCard: this.renderService.renderCard.bind(this.renderService), + renderCard: this.renderService.renderCard, + render: this.renderService.render, }); setOwner(currentRun, getOwner(this)!); @@ -127,7 +128,8 @@ export default class CardPrerender extends Component { reader, indexWriter, ignoreData: { ...ignoreData }, - renderCard: this.renderService.renderCard.bind(this.renderService), + renderCard: this.renderService.renderCard, + render: this.renderService.render, }); setOwner(currentRun, getOwner(this)!); let current = await CurrentRun.incremental(currentRun, { diff --git a/packages/host/app/lib/current-run.ts b/packages/host/app/lib/current-run.ts index e3d2d6f8c3..70522c33fd 100644 --- a/packages/host/app/lib/current-run.ts +++ b/packages/host/app/lib/current-run.ts @@ -41,6 +41,7 @@ import { serializableError, type SerializedError, } from '@cardstack/runtime-common/error'; +import { cardTypeIcon } from '@cardstack/runtime-common/helpers'; import { RealmPaths, LocalPath } from '@cardstack/runtime-common/paths'; import { isIgnored } from '@cardstack/runtime-common/realm-index-updater'; import { type Reader, type Stats } from '@cardstack/runtime-common/worker'; @@ -50,6 +51,7 @@ import type * as CardAPI from 'https://cardstack.com/base/card-api'; import { type RenderCard, + type Render, IdentityContextWithErrors, } from '../services/render-service'; @@ -82,6 +84,7 @@ export class CurrentRun { #realmPaths: RealmPaths; #ignoreData: Record; #renderCard: RenderCard; + #render: Render; #realmURL: URL; #realmInfo?: RealmInfo; readonly stats: Stats = { @@ -100,12 +103,14 @@ export class CurrentRun { indexWriter, ignoreData = {}, renderCard, + render, }: { realmURL: URL; reader: Reader; indexWriter: IndexWriter; ignoreData?: Record; renderCard: RenderCard; + render: Render; }) { this.#indexWriter = indexWriter; this.#realmPaths = new RealmPaths(realmURL); @@ -113,6 +118,7 @@ export class CurrentRun { this.#realmURL = realmURL; this.#ignoreData = ignoreData; this.#renderCard = renderCard; + this.#render = render; } static async fromScratch(current: CurrentRun): Promise { @@ -413,6 +419,7 @@ export class CurrentRun { let cardType: typeof CardDef | undefined; let isolatedHtml: string | undefined; let atomHtml: string | undefined; + let iconHTML: string | undefined; let card: CardDef | undefined; let embeddedHtml: Record | undefined; let fittedHtml: Record | undefined; @@ -473,6 +480,7 @@ export class CurrentRun { }), ), ); + iconHTML = unwrap(sanitizeHTML(this.#render(cardTypeIcon(card)))); cardType = Reflect.getPrototypeOf(card)?.constructor as typeof CardDef; let data = api.serializeCard(card, { includeComputeds: true }); // prepare the document for index serialization @@ -552,6 +560,7 @@ export class CurrentRun { atomHtml, embeddedHtml, fittedHtml, + iconHTML, lastModified, resourceCreatedAt, types: typesMaybeError.types.map(({ refURL }) => refURL), diff --git a/packages/host/app/lib/isolated-render.gts b/packages/host/app/lib/isolated-render.gts index 28ca079157..c612132512 100644 --- a/packages/host/app/lib/isolated-render.gts +++ b/packages/host/app/lib/isolated-render.gts @@ -16,7 +16,7 @@ import type { SimpleElement } from '@simple-dom/interface'; interface Signature { Args: { - format: Format; + format?: Format; }; } @@ -24,7 +24,7 @@ export function render( C: ComponentLike, element: SimpleElement, owner: Owner, - format: Format, + format?: Format, ): void { // this needs to be a template-only component because the way we're invoking it // just grabs the template and would drop any associated class. diff --git a/packages/host/app/services/render-service.ts b/packages/host/app/services/render-service.ts index 30d2539f87..f8d1872ccf 100644 --- a/packages/host/app/services/render-service.ts +++ b/packages/host/app/services/render-service.ts @@ -2,6 +2,7 @@ import { getOwner } from '@ember/application'; import type Owner from '@ember/owner'; import Service, { service } from '@ember/service'; +import { ComponentLike } from '@glint/template'; import Serializer from '@simple-dom/serializer'; import voidMap from '@simple-dom/void-map'; @@ -58,6 +59,7 @@ interface RenderCardParams { componentCodeRef?: CodeRef; } export type RenderCard = (params: RenderCardParams) => Promise; +export type Render = (component: ComponentLike) => string; const maxRenderThreshold = 10000; export default class RenderService extends Service { @@ -69,7 +71,7 @@ export default class RenderService extends Service { renderError: Error | undefined; owner: Owner = getOwner(this)!; - async renderCard(params: RenderCardParams): Promise { + renderCard = async (params: RenderCardParams): Promise => { let { card, visit, @@ -127,7 +129,16 @@ export default class RenderService extends Service { let serializer = new Serializer(voidMap); let html = serializer.serialize(element); return parseCardHtml(html); - } + }; + + render = (component: ComponentLike): string => { + let element = getIsolatedRenderElement(this.document); + render(component, element, this.owner); + + let serializer = new Serializer(voidMap); + let html = serializer.serialize(element); + return parseCardHtml(html); + }; // TODO delete me private async resolveField( diff --git a/packages/host/config/schema/1735668047598_schema.sql b/packages/host/config/schema/1735668047598_schema.sql index 5577b1a87b..8375812c17 100644 --- a/packages/host/config/schema/1735668047598_schema.sql +++ b/packages/host/config/schema/1735668047598_schema.sql @@ -23,6 +23,7 @@ fitted_html BLOB, display_names BLOB, resource_created_at, + icon_html TEXT, PRIMARY KEY ( url, realm_version, realm_url ) ); diff --git a/packages/postgres/migrations/1734686216941_prerendered-card-type-icon.js b/packages/postgres/migrations/1734686216941_prerendered-card-type-icon.js new file mode 100644 index 0000000000..83964f952e --- /dev/null +++ b/packages/postgres/migrations/1734686216941_prerendered-card-type-icon.js @@ -0,0 +1,13 @@ +/* eslint-disable camelcase */ + +exports.shorthands = undefined; + +exports.up = (pgm) => { + pgm.addColumns('boxel_index', { + icon_html: 'varchar', + }); +}; + +exports.down = (pgm) => { + pgm.dropColumns('boxel_index', ['icon_html']); +}; diff --git a/packages/realm-server/tests/realm-server-test.ts b/packages/realm-server/tests/realm-server-test.ts index f892a1f5ca..405058b1a1 100644 --- a/packages/realm-server/tests/realm-server-test.ts +++ b/packages/realm-server/tests/realm-server-test.ts @@ -4055,7 +4055,8 @@ module('Realm Server', function (hooks) { let response = await request .get('/_types') .set('Accept', 'application/json'); - + let iconHTML = + ''; assert.strictEqual(response.status, 200, 'HTTP 200 status'); assert.deepEqual(response.body, { data: [ @@ -4065,6 +4066,7 @@ module('Realm Server', function (hooks) { attributes: { displayName: 'Friend', total: 2, + iconHTML, }, }, { @@ -4073,6 +4075,7 @@ module('Realm Server', function (hooks) { attributes: { displayName: 'Home', total: 1, + iconHTML, }, }, { @@ -4081,6 +4084,7 @@ module('Realm Server', function (hooks) { attributes: { displayName: 'Person', total: 3, + iconHTML, }, }, ], diff --git a/packages/runtime-common/card-document.ts b/packages/runtime-common/card-document.ts index 8386d478af..c8f55767f7 100644 --- a/packages/runtime-common/card-document.ts +++ b/packages/runtime-common/card-document.ts @@ -356,6 +356,7 @@ export function makeCardTypeSummaryDoc(summaries: CardTypeSummary[]) { attributes: { displayName: summary.display_name, total: summary.total, + iconHTML: summary.icon_html, }, })); diff --git a/packages/runtime-common/helpers/index.ts b/packages/runtime-common/helpers/index.ts index f9d7199a4a..c00d4fd6d4 100644 --- a/packages/runtime-common/helpers/index.ts +++ b/packages/runtime-common/helpers/index.ts @@ -1,5 +1,7 @@ import { parse } from 'date-fns'; +export * from './card-type-display-name'; + export interface SharedTests { [testName: string]: (assert: Assert, args: T) => Promise; } diff --git a/packages/runtime-common/index-structure.ts b/packages/runtime-common/index-structure.ts index 097f45b321..e8986ec03d 100644 --- a/packages/runtime-common/index-structure.ts +++ b/packages/runtime-common/index-structure.ts @@ -25,6 +25,7 @@ export interface BoxelIndexTable { fitted_html: Record | null; isolated_html: string | null; atom_html: string | null; + icon_html: string | null; indexed_at: string | null; // pg represents big integers as strings in javascript last_modified: string | null; // pg represents big integers as strings in javascript resource_created_at: string | null; // pg represents big integers as strings in javascript @@ -40,6 +41,7 @@ export interface CardTypeSummary { code_ref: string; display_name: string; total: number; + icon_html: string; } export interface RealmMetaTable { diff --git a/packages/runtime-common/index-writer.ts b/packages/runtime-common/index-writer.ts index cd0a378271..96a430534b 100644 --- a/packages/runtime-common/index-writer.ts +++ b/packages/runtime-common/index-writer.ts @@ -72,6 +72,7 @@ export interface InstanceEntry { embeddedHtml?: Record; fittedHtml?: Record; atomHtml?: string; + iconHTML?: string; types: string[]; displayNames: string[]; deps: Set; @@ -168,6 +169,7 @@ export class Batch { embedded_html: entry.embeddedHtml, fitted_html: entry.fittedHtml, atom_html: entry.atomHtml, + icon_html: entry.iconHTML, deps: [...entry.deps], types: entry.types, display_names: entry.displayNames, @@ -295,7 +297,7 @@ export class Batch { private async updateRealmMeta() { let results = await this.#query([ - `SELECT CAST(count(i.url) AS INTEGER) as total, i.display_names->>0 as display_name, i.types->>0 as code_ref + `SELECT CAST(count(i.url) AS INTEGER) as total, i.display_names->>0 as display_name, i.types->>0 as code_ref, MAX(i.icon_html) as icon_html FROM boxel_index as i INNER JOIN realm_versions r ON i.realm_url = r.realm_url WHERE`, diff --git a/packages/runtime-common/tests/index-writer-test.ts b/packages/runtime-common/tests/index-writer-test.ts index 685c712038..b090782a1a 100644 --- a/packages/runtime-common/tests/index-writer-test.ts +++ b/packages/runtime-common/tests/index-writer-test.ts @@ -556,6 +556,7 @@ const tests = Object.freeze({ ), isolated_html: `
Isolated HTML
`, atom_html: `Atom HTML`, + icon_html: 'test icon', }, ], ); @@ -611,6 +612,7 @@ const tests = Object.freeze({ last_modified: String(modified), resource_created_at: String(modified), is_deleted: null, + icon_html: 'test icon', }, 'the error entry includes last known good state of instance', ); @@ -665,6 +667,7 @@ const tests = Object.freeze({ last_modified: null, resource_created_at: null, is_deleted: false, + icon_html: null, }, 'the error entry does not include last known good state of instance', ); @@ -1170,6 +1173,7 @@ const tests = Object.freeze({ assert, { indexWriter, adapter }, ) => { + let iconHTML = 'test icon'; await setupIndex( adapter, [{ realm_url: testRealmURL, current_version: 1 }], @@ -1197,6 +1201,7 @@ const tests = Object.freeze({ types: [{ module: `./person`, name: 'Person' }, baseCardRef].map( (i) => internalKeyFor(i, new URL(testRealmURL)), ), + icon_html: iconHTML, }, ], ); @@ -1230,6 +1235,7 @@ const tests = Object.freeze({ { module: `./person`, name: 'Person' }, baseCardRef, ].map((i) => internalKeyFor(i, new URL(testRealmURL))), + iconHTML, }); let results = await adapter.execute( @@ -1265,7 +1271,12 @@ const tests = Object.freeze({ 'correct length of query result after indexing is done', ); let value = results[0].value as [ - { code_ref: string; display_name: string; total: number }, + { + code_ref: string; + display_name: string; + icon_html: string; + total: number; + }, ]; assert.strictEqual( value.length, @@ -1279,11 +1290,13 @@ const tests = Object.freeze({ total: 1, code_ref: `${testRealmURL}fancy-person/FancyPerson`, display_name: 'Fancy Person', + icon_html: iconHTML, }, { total: 1, code_ref: `${testRealmURL}person/Person`, display_name: 'Person', + icon_html: iconHTML, }, ], 'correct card type summary after indexing is done', @@ -1318,6 +1331,7 @@ const tests = Object.freeze({ { module: `./person`, name: 'Person' }, baseCardRef, ].map((i) => internalKeyFor(i, new URL(testRealmURL))), + iconHTML, }); let resource4: CardResource = { id: `${testRealmURL}4`, @@ -1347,6 +1361,7 @@ const tests = Object.freeze({ { module: `./card-api`, name: 'CardDef' }, baseCardRef, ].map((i) => internalKeyFor(i, new URL(testRealmURL))), + iconHTML, }); await batch.done(); @@ -1365,13 +1380,19 @@ const tests = Object.freeze({ 'correct length of query result after indexing is done', ); value = results[0].value as [ - { code_ref: string; display_name: string; total: number }, + { + code_ref: string; + display_name: string; + total: number; + icon_html: string; + }, ]; assert.strictEqual( value.length, 3, 'correct length of card type summary after indexing is done', ); + assert.deepEqual( value, [ @@ -1379,16 +1400,19 @@ const tests = Object.freeze({ total: 2, code_ref: `${testRealmURL}fancy-person/FancyPerson`, display_name: 'Fancy Person', + icon_html: iconHTML, }, { total: 1, code_ref: `${testRealmURL}person/Person`, display_name: 'Person', + icon_html: iconHTML, }, { total: 1, code_ref: `${testRealmURL}pet/Pet`, display_name: 'Pet', + icon_html: iconHTML, }, ], 'correct card type summary after indexing is done',