diff --git a/packages/host/app/components/operator-mode/preview-panel/index.gts b/packages/host/app/components/operator-mode/preview-panel/index.gts index a2089b278d2..5ad553f2113 100644 --- a/packages/host/app/components/operator-mode/preview-panel/index.gts +++ b/packages/host/app/components/operator-mode/preview-panel/index.gts @@ -32,7 +32,10 @@ import { type ResolvedCodeRef, } from '@cardstack/runtime-common'; -import { CardContextName } from '@cardstack/runtime-common'; +import { + CardContextName, + CardCrudFunctionsContextName, +} from '@cardstack/runtime-common'; import CardRenderer from '@cardstack/host/components/card-renderer'; import Overlays from '@cardstack/host/components/operator-mode/overlays'; @@ -54,6 +57,7 @@ import MetadataPanel from './metadata-panel'; import type { BaseDef, CardContext, + CardCrudFunctions, CardDef, Format, ViewCardFn, @@ -211,6 +215,26 @@ export default class PreviewPanel extends Component { }; } + /** + * Card templates receive their crud functions through this context rather + * than through args (see `field-component.gts`), and the two providers of + * it are the interact-mode stack and host mode. Without one here, a button + * inside a previewed card that calls `viewCard` — the Workspace card's + * pinned tiles, a Library tile, any card that navigates — silently does + * nothing, because `viewCard` is undefined and every call site guards with + * `?.`. The pane's own `@viewCard` only ever reached the hover overlays. + * + * Only `viewCard` is provided: it navigates the code path, which is what + * the overlay buttons in this same pane already do. The rest stay + * undefined, so an in-card create/edit/delete button remains inert here + * instead of doing something a code-mode preview cannot honor. + */ + @provide(CardCrudFunctionsContextName) + // @ts-ignore context is used via provider + private get cardCrudFunctions(): Partial { + return { viewCard: this.args.viewCard }; + } + private get renderedCardsForOverlayActions(): | RenderedCardForOverlayActions[] | undefined { diff --git a/packages/host/tests/acceptance/code-submode/inspector-test.ts b/packages/host/tests/acceptance/code-submode/inspector-test.ts index 93b25941a54..03441263f61 100644 --- a/packages/host/tests/acceptance/code-submode/inspector-test.ts +++ b/packages/host/tests/acceptance/code-submode/inspector-test.ts @@ -306,6 +306,39 @@ const friendCardSource = ` } `; +// A card whose template navigates through `viewCard`, the way the Workspace +// card's pinned tiles and Library tiles do. Card templates receive that +// function from the card-crud-functions context, so this fixture is what +// proves the code-mode preview pane provides it. +const navigatorCardSource = ` + import { contains, field, linksTo, CardDef, Component } from "@cardstack/base/card-api"; + import { on } from '@ember/modifier'; + import StringField from "@cardstack/base/string"; + + export class Navigator extends CardDef { + static displayName = 'Navigator'; + @field name = contains(StringField); + @field target = linksTo(() => Navigator); + static isolated = class Isolated extends Component { + openTarget = () => { + let { target } = this.args.model; + if (target) { + this.args.viewCard?.(target); + } + }; + + }; + } +`; + const exportsSource = ` import { contains, @@ -519,6 +552,26 @@ module('Acceptance | code submode | inspector tests', function (hooks) { 'person.gts': personCardSource, 'pet.gts': petCardSource, 'friend.gts': friendCardSource, + 'navigator.gts': navigatorCardSource, + 'Navigator/parent.json': { + data: { + attributes: { name: 'Parent' }, + relationships: { + target: { links: { self: './child' } }, + }, + meta: { + adoptsFrom: { module: '../navigator', name: 'Navigator' }, + }, + }, + }, + 'Navigator/child.json': { + data: { + attributes: { name: 'Child' }, + meta: { + adoptsFrom: { module: '../navigator', name: 'Navigator' }, + }, + }, + }, 'employee.gts': employeeCardSource, 'in-this-file.gts': inThisFileSource, 'exports.gts': exportsSource, @@ -993,6 +1046,26 @@ module('Acceptance | code submode | inspector tests', function (hooks) { assert.dom('[data-test-card-url-bar-input]').hasValue(`${id}.json`); }); + test('a card in the preview pane can navigate through viewCard', async function (assert) { + await visitOperatorMode({ + submode: 'code', + codePath: `${testRealmURL}Navigator/parent.json`, + }); + + await waitFor('[data-test-open-target]'); + await click('[data-test-open-target]'); + + await waitFor( + `[data-test-code-mode-card-renderer-header="${testRealmURL}Navigator/child"]`, + ); + assert + .dom('[data-test-card-url-bar-input]') + .hasValue( + `${testRealmURL}Navigator/child.json`, + 'the in-card button opened the linked card, like the hover overlays do', + ); + }); + test('can duplicate an instance in different realm', async function (assert) { assert.expect(8); let operatorModeStateParam = stringify({