From 378a874c828c50a6f65e110e6363e86796c3a49f Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Fri, 13 Feb 2026 16:00:40 +0700 Subject: [PATCH 1/2] Remove index card to avoid flash of empty cards in host mode --- packages/base/cards-grid.gts | 20 +- packages/base/cards-grid.json | 12 - packages/base/index.gts | 76 +---- packages/base/index.json | 26 +- packages/experiments-realm/cards-grid.json | 12 - packages/experiments-realm/index.json | 26 +- .../host/tests/acceptance/index-home-test.gts | 314 ------------------ .../scripts/setup-submission-realm.sh | 22 -- packages/realm-server/server.ts | 18 - scripts/migrate-index-to-cards-grid.js | 101 ++++++ 10 files changed, 113 insertions(+), 514 deletions(-) delete mode 100644 packages/base/cards-grid.json delete mode 100644 packages/experiments-realm/cards-grid.json delete mode 100644 packages/host/tests/acceptance/index-home-test.gts create mode 100755 scripts/migrate-index-to-cards-grid.js diff --git a/packages/base/cards-grid.gts b/packages/base/cards-grid.gts index 5e4c622a71c..3687bb46398 100644 --- a/packages/base/cards-grid.gts +++ b/packages/base/cards-grid.gts @@ -161,22 +161,11 @@ class Isolated extends Component { icon: AllCardsIcon, query: { filter: { - every: [ - { - not: { - eq: { - _cardType: 'Cards Grid', - }, - }, + not: { + eq: { + _cardType: 'Cards Grid', }, - { - not: { - eq: { - _cardType: 'Index', - }, - }, - }, - ], + }, }, }, filters: this.cardTypeFilters, @@ -308,7 +297,6 @@ class Isolated extends Component { let excludedCardTypeIds = [ `${baseRealm.url}card-api/CardDef`, `${baseRealm.url}cards-grid/CardsGrid`, - `${baseRealm.url}index/IndexCard`, ]; this.cardTypeFilters.splice(0, this.cardTypeFilters.length); diff --git a/packages/base/cards-grid.json b/packages/base/cards-grid.json deleted file mode 100644 index f692959b90e..00000000000 --- a/packages/base/cards-grid.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "data": { - "type": "card", - "attributes": {}, - "meta": { - "adoptsFrom": { - "module": "https://cardstack.com/base/cards-grid", - "name": "CardsGrid" - } - } - } -} diff --git a/packages/base/index.gts b/packages/base/index.gts index 3e127af223b..5935ce87067 100644 --- a/packages/base/index.gts +++ b/packages/base/index.gts @@ -1,74 +1,2 @@ -import { CardsGrid } from './cards-grid'; -import { - CardDef, - Component, - contains, - field, - linksTo, - realmInfo, - StringField, -} from './card-api'; - -export class IndexCard extends CardDef { - static displayName = 'Index'; - static prefersWideFormat = true; - - @field realmName = contains(StringField, { - computeVia: function (this: IndexCard) { - return this[realmInfo]?.name; - }, - }); - @field cardTitle = contains(StringField, { - computeVia: function (this: IndexCard) { - return this.realmName; - }, - }); - @field cardsGrid = linksTo(CardsGrid); - @field interactHome = linksTo(CardDef); - @field hostHome = linksTo(CardDef); - - static isolated = class Isolated extends Component { - private get prefersInteractHome() { - let submode = this.args.context?.submode; - return !!( - (submode === 'interact' || submode === 'code') && - this.args.model.interactHome - ); - } - - private get prefersHostHome() { - let mode = this.args.context?.mode; - let submode = this.args.context?.submode; - return !!( - (mode === 'host' || submode === 'host') && - this.args.model.hostHome - ); - } - - - }; -} +// Backward compatibility: IndexCard is now CardsGrid +export { CardsGrid as IndexCard } from './cards-grid'; diff --git a/packages/base/index.json b/packages/base/index.json index 9a8ae22e99b..58b088af4cb 100644 --- a/packages/base/index.json +++ b/packages/base/index.json @@ -1,30 +1,10 @@ { "data": { + "type": "card", "meta": { "adoptsFrom": { - "name": "IndexCard", - "module": "./index" - } - }, - "type": "card", - "attributes": { - "cardInfo": { - "notes": null, - "name": null, - "summary": null, - "cardThumbnailURL": null - } - }, - "relationships": { - "cardsGrid": { - "links": { - "self": "./cards-grid" - } - }, - "cardInfo.theme": { - "links": { - "self": null - } + "module": "./cards-grid", + "name": "CardsGrid" } } } diff --git a/packages/experiments-realm/cards-grid.json b/packages/experiments-realm/cards-grid.json deleted file mode 100644 index f692959b90e..00000000000 --- a/packages/experiments-realm/cards-grid.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "data": { - "type": "card", - "attributes": {}, - "meta": { - "adoptsFrom": { - "module": "https://cardstack.com/base/cards-grid", - "name": "CardsGrid" - } - } - } -} diff --git a/packages/experiments-realm/index.json b/packages/experiments-realm/index.json index bca0c486272..b1ab639953c 100644 --- a/packages/experiments-realm/index.json +++ b/packages/experiments-realm/index.json @@ -1,30 +1,10 @@ { "data": { + "type": "card", "meta": { "adoptsFrom": { - "name": "IndexCard", - "module": "https://cardstack.com/base/index" - } - }, - "type": "card", - "attributes": { - "cardInfo": { - "notes": null, - "name": null, - "summary": null, - "cardThumbnailURL": null - } - }, - "relationships": { - "cardsGrid": { - "links": { - "self": "./cards-grid" - } - }, - "cardInfo.theme": { - "links": { - "self": null - } + "module": "https://cardstack.com/base/cards-grid", + "name": "CardsGrid" } } } diff --git a/packages/host/tests/acceptance/index-home-test.gts b/packages/host/tests/acceptance/index-home-test.gts deleted file mode 100644 index 66f55206e61..00000000000 --- a/packages/host/tests/acceptance/index-home-test.gts +++ /dev/null @@ -1,314 +0,0 @@ -import { getOwner } from '@ember/owner'; -import { click, visit, waitFor, waitUntil } from '@ember/test-helpers'; - -import { fillIn } from '@ember/test-helpers'; - -import { getService } from '@universal-ember/test-support'; -import { module, test } from 'qunit'; - -import { baseRealm } from '@cardstack/runtime-common'; -import { - testHostModeRealmURL, - testRealmURL, -} from '@cardstack/runtime-common/helpers/const'; - -import HostModeService from '@cardstack/host/services/host-mode-service'; - -import { - setupAcceptanceTestRealm, - setupAuthEndpoints, - setupLocalIndexing, - setupOnSave, - setupUserSubscription, - SYSTEM_CARD_FIXTURE_CONTENTS, -} from '../helpers'; -import { setupBaseRealm } from '../helpers/base-realm'; -import { setupMockMatrix } from '../helpers/mock-matrix'; -import { setupApplicationTest } from '../helpers/setup'; -import visitOperatorMode from '../helpers/visit-operator-mode'; - -let testHostModeRealmURLWithoutRealm = testHostModeRealmURL.replace( - '/user/test/', - '', -); - -class StubHostModeService extends HostModeService { - get isActive() { - return true; - } - - get hostModeOrigin() { - return removeTrailingSlash(testHostModeRealmURLWithoutRealm); - } -} - -function removeTrailingSlash(url: string): string { - return url.endsWith('/') ? url.slice(0, -1) : url; -} - -module('Acceptance | index card home resolution', function (hooks) { - setupApplicationTest(hooks); - setupLocalIndexing(hooks); - setupOnSave(hooks); - - let mockMatrixUtils = setupMockMatrix(hooks, { - loggedInAs: '@testuser:localhost', - activeRealms: [baseRealm.url, testRealmURL], - }); - - setupBaseRealm(hooks); - let realmContents: any; - - hooks.beforeEach(async function () { - let loader = getService('loader-service').loader; - let { field, contains, CardDef, Component } = await loader.import< - typeof import('https://cardstack.com/base/card-api') - >(`${baseRealm.url}card-api`); - let { default: StringField } = await loader.import< - typeof import('https://cardstack.com/base/string') - >(`${baseRealm.url}string`); - - class Pet extends CardDef { - static displayName = 'Pet'; - @field name = contains(StringField); - @field cardTitle = contains(StringField, { - computeVia(this: Pet) { - return this.name; - }, - }); - static isolated = class Isolated extends Component { - - }; - } - - realmContents = { - ...SYSTEM_CARD_FIXTURE_CONTENTS, - 'pet.gts': { Pet }, - 'cards-grid.json': { - data: { - type: 'card', - meta: { - adoptsFrom: { - module: 'https://cardstack.com/base/cards-grid', - name: 'CardsGrid', - }, - }, - }, - }, - 'Pet/mango.json': { - data: { - attributes: { - name: 'Mango', - }, - meta: { - adoptsFrom: { - module: `../pet`, - name: 'Pet', - }, - }, - }, - }, - 'Pet/peanut.json': { - data: { - attributes: { - name: 'Peanut', - }, - meta: { - adoptsFrom: { - module: `../pet`, - name: 'Pet', - }, - }, - }, - }, - 'index.json': { - data: { - type: 'card', - meta: { - adoptsFrom: { - module: 'https://cardstack.com/base/index', - name: 'IndexCard', - }, - }, - relationships: { - cardsGrid: { - links: { - self: './cards-grid', - }, - }, - interactHome: { - links: { - self: './Pet/mango', - }, - }, - hostHome: { - links: { - self: './Pet/peanut', - }, - }, - }, - }, - }, - '.realm.json': { - publishable: true, - name: 'Index Home Workspace', - hostHome: `${testRealmURL}site`, - }, - }; - }); - - module('host mode', function (hooks) { - let { setActiveRealms, setExpiresInSec, createAndJoinRoom } = - mockMatrixUtils; - - hooks.beforeEach(async function () { - createAndJoinRoom({ - sender: '@testuser:localhost', - name: 'room-test', - }); - setupUserSubscription(); - setupAuthEndpoints(); - - let contents = { - ...realmContents, - '.realm.json': { - ...(realmContents['.realm.json'] as any), - }, - }; - setExpiresInSec(60 * 60); - await setupAcceptanceTestRealm({ - realmURL: testHostModeRealmURL, - mockMatrixUtils, - contents, - permissions: { - '*': ['read'], - }, - }); - - setActiveRealms([testHostModeRealmURL]); - }); - - hooks.beforeEach(function (this) { - getOwner(this)!.register( - 'service:host-mode-service', - StubHostModeService, - ); - }); - - test('host mode renders hostHome', async function (assert) { - await visit(`/user/test/`); - - assert - .dom(`[data-test-host-mode-card="${testHostModeRealmURL}index"]`) - .exists(); - assert - .dom(`[data-test-card="${testHostModeRealmURL}Pet/peanut"]`) - .exists(); - }); - }); - - module('operator submodes', function (hooks) { - hooks.beforeEach(async function () { - await setupAcceptanceTestRealm({ - mockMatrixUtils, - contents: realmContents, - }); - }); - - test('interact submode uses interactHome', async function (assert) { - await visitOperatorMode({ - submode: 'interact', - stacks: [ - [ - { - id: `${testRealmURL}index`, - format: 'isolated', - }, - ], - ], - }); - - assert.dom(`[data-test-stack-card="${testRealmURL}index"]`).exists(); - assert - .dom(`[data-test-card="http://test-realm/test/Pet/mango"]`) - .exists(); - - // default to cardsGrid if interactHome is not set - await click( - `[data-test-stack-card="${testRealmURL}index"] [data-test-edit-button]`, - ); - await click( - `[data-test-links-to-editor="interactHome"] [data-test-remove-card]`, - ); - await click( - `[data-test-stack-card="${testRealmURL}index"] [data-test-edit-button]`, - ); - assert.dom(`[data-test-card="${testRealmURL}cards-grid"]`).exists(); - assert - .dom(`[data-cards-grid-item="${testRealmURL}index"]`) - .doesNotExist(); - assert - .dom(`[data-cards-grid-item="${testRealmURL}cards-grid"]`) - .doesNotExist(); - }); - - test('host submode uses hostHome and updates after edit', async function (assert) { - await visitOperatorMode({ - submode: 'host', - trail: [`${testRealmURL}index`], - stacks: [ - [ - { - id: `${testRealmURL}index`, - format: 'isolated', - }, - ], - ], - }); - - assert.dom(`[data-test-host-mode-card="${testRealmURL}index"]`).exists(); - assert.dom(`[data-test-card="${testRealmURL}Pet/peanut"]`).exists(); - - // Switch to interact to edit index card - await click('[data-test-submode-switcher] > [data-test-boxel-button]'); - await click('[data-test-boxel-menu-item-text="Interact"]'); - await waitFor(`[data-test-stack-card="${testRealmURL}index"]`); - await click( - `[data-test-stack-card="${testRealmURL}index"] [data-test-edit-button]`, - ); - - await click( - `[data-test-links-to-editor="hostHome"] [data-test-remove-card]`, - ); - await waitFor( - `[data-test-links-to-editor="hostHome"] [data-test-add-new]`, - ); - - await click(`[data-test-links-to-editor="hostHome"] [data-test-add-new]`); - await waitFor('[data-test-card-catalog-modal]'); - await fillIn('[data-test-search-field]', 'Mango'); - await click(`[data-test-card-catalog-item="${testRealmURL}Pet/mango"]`); - await click('[data-test-card-catalog-go-button]'); - await waitUntil( - () => !document.querySelector('[data-test-card-catalog-modal]'), - ); - await waitFor( - `[data-test-links-to-editor="hostHome"] [data-test-card="${testRealmURL}Pet/mango"]`, - ); - - await click( - `[data-test-stack-card="${testRealmURL}index"] [data-test-edit-button]`, - ); - - await click('[data-test-submode-switcher] > [data-test-boxel-button]'); - await click('[data-test-boxel-menu-item-text="Host"]'); - await waitFor('[data-test-submode-switcher="host"]'); - assert.dom(`[data-test-host-mode-card="${testRealmURL}index"]`).exists(); - assert.dom(`[data-test-card="${testRealmURL}Pet/mango"]`).exists(); - }); - }); -}); diff --git a/packages/realm-server/scripts/setup-submission-realm.sh b/packages/realm-server/scripts/setup-submission-realm.sh index 1164ea4d78f..e04022f3c63 100644 --- a/packages/realm-server/scripts/setup-submission-realm.sh +++ b/packages/realm-server/scripts/setup-submission-realm.sh @@ -23,28 +23,6 @@ EOF fi if [ ! -f "$SUBMISSION_REALM_PATH/index.json" ]; then cat > "$SUBMISSION_REALM_PATH/index.json" << 'EOF' -{ - "data": { - "type": "card", - "meta": { - "adoptsFrom": { - "module": "https://cardstack.com/base/index", - "name": "IndexCard" - } - }, - "relationships": { - "cardsGrid": { - "links": { - "self": "./cards-grid" - } - } - } - } -} -EOF -fi -if [ ! -f "$SUBMISSION_REALM_PATH/cards-grid.json" ]; then - cat > "$SUBMISSION_REALM_PATH/cards-grid.json" << 'EOF' { "data": { "type": "card", diff --git a/packages/realm-server/server.ts b/packages/realm-server/server.ts index 31595b30120..1a55dd218ec 100644 --- a/packages/realm-server/server.ts +++ b/packages/realm-server/server.ts @@ -719,24 +719,6 @@ export class RealmServer { }; writeJSONSync(join(realmPath, '.realm.json'), info); writeJSONSync(join(realmPath, 'index.json'), { - data: { - type: 'card', - meta: { - adoptsFrom: { - module: 'https://cardstack.com/base/index', - name: 'IndexCard', - }, - }, - relationships: { - cardsGrid: { - links: { - self: './cards-grid', - }, - }, - }, - }, - }); - writeJSONSync(join(realmPath, 'cards-grid.json'), { data: { type: 'card', meta: { diff --git a/scripts/migrate-index-to-cards-grid.js b/scripts/migrate-index-to-cards-grid.js new file mode 100755 index 00000000000..35378d99914 --- /dev/null +++ b/scripts/migrate-index-to-cards-grid.js @@ -0,0 +1,101 @@ +#!/usr/bin/env node +'use strict'; + +const fs = require('fs/promises'); +const path = require('path'); + +const ROOT = process.argv[2] || '.'; +const MODE = process.argv[3] || 'dry-run'; // "apply" to write changes +const BACKUP = (process.argv[4] || 'yes') === 'yes'; +const EXCLUDE = new Set( + (process.argv[5] || 'node_modules,.git') + .split(',') + .map((s) => s.trim()) + .filter(Boolean), +); + +async function* walk(dir) { + let entries; + try { + entries = await fs.readdir(dir, { withFileTypes: true }); + } catch { + return; + } + + for (const ent of entries) { + const full = path.join(dir, ent.name); + if (ent.isDirectory()) { + if (EXCLUDE.has(ent.name)) continue; + yield* walk(full); + } else if (ent.isFile() && ent.name === 'index.json') { + yield full; + } + } +} + +async function processFile(filePath) { + let raw; + let data; + try { + raw = await fs.readFile(filePath, 'utf8'); + data = JSON.parse(raw); + } catch { + return { changed: false }; + } + + // Only process files that adopt from IndexCard + const adoptsFrom = data?.data?.meta?.adoptsFrom; + if (!adoptsFrom || adoptsFrom.name !== 'IndexCard') { + return { changed: false }; + } + + // Replace with CardsGrid adoption, strip relationships and attributes + const updated = { + data: { + type: 'card', + meta: { + adoptsFrom: { + module: 'https://cardstack.com/base/cards-grid', + name: 'CardsGrid', + }, + }, + }, + }; + + if (MODE === 'apply') { + if (BACKUP) { + await fs.writeFile(filePath + '.bak', raw, 'utf8'); + } + await fs.writeFile( + filePath, + JSON.stringify(updated, null, 2) + '\n', + 'utf8', + ); + } + + return { changed: true }; +} + +(async function main() { + const changedFiles = []; + + for await (const file of walk(ROOT)) { + const res = await processFile(file); + if (res.changed) changedFiles.push(file); + } + + console.log(`Mode: ${MODE}`); + console.log(`Scanned root: ${ROOT}`); + console.log(`Files to migrate: ${changedFiles.length}`); + if (changedFiles.length) { + for (const p of changedFiles) console.log(` ${p}`); + } + console.log(`Excluded dirs: ${[...EXCLUDE].join(', ')}`); + if (MODE === 'dry-run' && changedFiles.length) { + console.log('\nRe-run with "apply" to write changes:'); + console.log(` node ${path.basename(__filename)} ${ROOT} apply`); + } +})().catch((err) => { + console.error(err?.stack || String(err)); + process.exit(1); +}); From 780aadfd9b5fb8a80a77ce23c296dba44a20c344 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Fri, 13 Feb 2026 16:52:34 +0700 Subject: [PATCH 2/2] Fix test --- .../tests/server-endpoints/maintenance-endpoints-test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/realm-server/tests/server-endpoints/maintenance-endpoints-test.ts b/packages/realm-server/tests/server-endpoints/maintenance-endpoints-test.ts index 591519ea568..f041368cd99 100644 --- a/packages/realm-server/tests/server-endpoints/maintenance-endpoints-test.ts +++ b/packages/realm-server/tests/server-endpoints/maintenance-endpoints-test.ts @@ -418,12 +418,12 @@ module(`server-endpoints/${basename(__filename)}`, function () { .set('Content-Type', 'application/json'); assert.deepEqual(response.body, { fileErrors: 0, - filesIndexed: 2, + filesIndexed: 1, moduleErrors: 0, instanceErrors: 0, modulesIndexed: 0, - instancesIndexed: 2, - totalIndexEntries: 4, + instancesIndexed: 1, + totalIndexEntries: 2, }); } let finalJobs = await context.dbAdapter.execute('select * from jobs');