Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
19 changes: 17 additions & 2 deletions packages/base/card-api.gts
Original file line number Diff line number Diff line change
Expand Up @@ -2306,10 +2306,14 @@ export type ViewCardFn = (
stackIndex?: number;
fieldType?: 'linksTo' | 'contains' | 'containsMany' | 'linksToMany';
fieldName?: string;
useBaseTemplate?: boolean;
},
) => void;

export type EditCardFn = (card: CardDef) => void;
export type EditCardFn = (
card: CardDef,
opts?: { useBaseTemplate?: boolean },
) => void;

export type SaveCardFn = (id: string) => void;

Expand Down Expand Up @@ -2794,6 +2798,14 @@ export class CardDef extends BaseDef {
static atom: BaseDefComponent = DefaultAtomViewTemplate;
static head: BaseDefComponent = DefaultHeadTemplate;

static get hasCustomEditTemplate(): boolean {
return this.edit !== CardDef.edit;
}

static get hasCustomIsolatedTemplate(): boolean {
return this.isolated !== CardDef.isolated;
}

static prefersWideFormat = false; // whether the card is full-width in the stack
static headerColor: string | null = null; // set string color value if the stack-item header has a background color

Expand Down Expand Up @@ -3053,7 +3065,10 @@ function lazilyLoadLink(
inflightLoads = new Map();
inflightLinkLoads.set(instance, inflightLoads);
}
let reference = resolveCardReference(link, instance.id ?? instance[relativeTo]);
let reference = resolveCardReference(
link,
instance.id ?? instance[relativeTo],
);
let key = `${field.name}/${reference}`;
let promise = inflightLoads.get(key);
let store = getStore(instance);
Expand Down
5 changes: 4 additions & 1 deletion packages/base/default-templates/isolated-and-edit.gts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ export default class DefaultCardDefTemplate extends GlimmerComponent<{
}

<template>
<div class={{cn 'default-card-template' @format}}>
<div
class={{cn 'default-card-template' @format}}
data-test-base-template={{@format}}
>
<Header @hasBottomBorder={{true}} class='card-info-header'>
{{#if (eq @format 'isolated')}}
<CardInfoTemplates.view
Expand Down
46 changes: 46 additions & 0 deletions packages/base/menu-items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,23 @@ export type GetMenuItemParams = {
cardCrudFunctions: Partial<CardCrudFunctions>;
commandContext: CommandContext;
format?: Format;
useBaseTemplate?: boolean;
} & MenuContext;

function makeToggleTemplateItem(
card: CardDef,
useBaseTemplate: boolean | undefined,
action: (useBaseTemplate: boolean) => void,
): MenuItemOptions {
return {
label: 'Toggle Standard View',
action: () => action(!useBaseTemplate),
icon: Eye,
checked: !!useBaseTemplate,
disabled: !card.id,
};
}

export function getDefaultCardMenuItems(
card: CardDef,
params: GetMenuItemParams,
Expand All @@ -72,6 +87,37 @@ export function getDefaultCardMenuItems(
});
}
if (params.menuContext === 'interact') {
if (
cardId &&
params.canEdit &&
params.format === 'edit' &&
(card.constructor as typeof CardDef).hasCustomEditTemplate
) {
menuItems.push(
makeToggleTemplateItem(
card,
params.useBaseTemplate,
(useBaseTemplate) =>
params.cardCrudFunctions.editCard?.(card, { useBaseTemplate }),
),
);
}
if (
cardId &&
params.format === 'isolated' &&
(card.constructor as typeof CardDef).hasCustomIsolatedTemplate
) {
menuItems.push(
makeToggleTemplateItem(
card,
params.useBaseTemplate,
(useBaseTemplate) =>
params.cardCrudFunctions.viewCard?.(card, 'isolated', {
useBaseTemplate,
}),
),
);
}
if (
!isRealmIndexCard(card) && // workspace index card cannot be deleted
cardId &&
Expand Down
54 changes: 52 additions & 2 deletions packages/host/app/components/operator-mode/interact-submode.gts
Original file line number Diff line number Diff line change
Expand Up @@ -206,12 +206,38 @@ export default class InteractSubmode extends Component {
stackIndex?: number;
fieldType?: 'linksTo' | 'linksToMany' | 'contains' | 'containsMany';
fieldName?: string;
useBaseTemplate?: boolean;
},
): void => {
if (format instanceof Event) {
// common when invoked from template {{on}} modifier
format = 'isolated';
}
// When toggling the isolated template for a card already on the stack,
// replace the existing item in-place rather than pushing a new one.
// Two separate checks are needed:
// 1. CardDef instances (including local-id-only cards with no .id yet) —
// findCardInStackSafe matches by instance identity or local id.
// 2. String/URL inputs — matched below by resolved cardId after the
// CardDef branch is skipped.
if (
format === 'isolated' &&
!(typeof cardOrURL === 'string' || cardOrURL instanceof URL) &&
this.stacks[stackIndex]
) {
let item = this.operatorModeStateService.findCardInStackSafe(
cardOrURL,
stackIndex,
);
if (item) {
this.operatorModeStateService.viewCardOnStack(
stackIndex,
cardOrURL,
opts,
);
return;
}
}
let cardId =
typeof cardOrURL === 'string'
? cardOrURL
Expand All @@ -221,6 +247,18 @@ export default class InteractSubmode extends Component {
if (!cardId) {
return;
}
if (format === 'isolated') {
let stack = this.stacks[stackIndex];
let isOnStack = stack?.some((item) => item.id === cardId);
if (isOnStack) {
this.operatorModeStateService.viewCardOnStack(
stackIndex,
cardOrURL as CardDef,
opts,
);
return;
}
}
if (opts?.openCardInRightMostStack) {
stackIndex = this.stacks.length;
} else if (typeof opts?.stackIndex === 'number') {
Expand All @@ -244,6 +282,7 @@ export default class InteractSubmode extends Component {
format,
stackIndex,
type: stackItemType,
useBaseTemplate: opts?.useBaseTemplate,
relationshipContext: opts?.fieldName
? {
fieldName: opts.fieldName,
Expand All @@ -258,8 +297,19 @@ export default class InteractSubmode extends Component {
this.operatorModeStateService.closeWorkspaceChooser();
};

private editCard = (stackIndex: number, card: CardDef): void => {
this.operatorModeStateService.editCardOnStack(stackIndex, card);
private editCard = (
stackIndex: number,
card: CardDef,
opts?: { useBaseTemplate?: boolean },
): void => {
let item =
this.stacks[stackIndex] &&
this.operatorModeStateService.findCardInStackSafe(card, stackIndex);
if (item) {
this.operatorModeStateService.editCardOnStack(stackIndex, card, opts);
} else {
this.viewCard(stackIndex, card, 'edit', opts);
}
};

private getStackItemType(
Expand Down
8 changes: 8 additions & 0 deletions packages/host/app/components/operator-mode/stack-item.gts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import {
CardContextName,
CardCrudFunctionsContextName,
getMenuItems,
baseCardRef,
} from '@cardstack/runtime-common';

import {
Expand Down Expand Up @@ -488,6 +489,8 @@ export default class OperatorModeStackItem extends Component<Signature> {
cardCrudFunctions: this.cardCrudFunctions,
menuContext: 'interact',
commandContext: this.args.commandContext,
format: this.cardFormat,
useBaseTemplate: this.args.item.useBaseTemplate,
}) ?? [],
);
}
Expand Down Expand Up @@ -680,6 +683,10 @@ export default class OperatorModeStackItem extends Component<Signature> {
return this.isFileCard ? 'isolated' : this.args.item.format;
}

private get defaultCodeRef() {
return this.args.item.useBaseTemplate ? baseCardRef : undefined;
}

private get showError() {
// in edit format, prefer showing the stale card if possible so user can
// attempt to fix the card error
Expand Down Expand Up @@ -850,6 +857,7 @@ export default class OperatorModeStackItem extends Component<Signature> {
class='stack-item-preview'
@card={{this.card}}
@format={{this.cardFormat}}
@codeRef={{this.defaultCodeRef}}
/>
<OperatorModeOverlays
@renderedCardsForOverlayActions={{this.renderedCardsForOverlayActions}}
Expand Down
6 changes: 6 additions & 0 deletions packages/host/app/lib/stack-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface Args {
id: string;
type?: StackItemType;
closeAfterSaving?: boolean;
useBaseTemplate?: boolean;
relationshipContext?: {
fieldName?: string;
fieldType?: 'linksTo' | 'linksToMany';
Expand Down Expand Up @@ -55,6 +56,7 @@ export class StackItem {
request?: Deferred<string>;
stackIndex: number;
closeAfterSaving?: boolean;
useBaseTemplate?: boolean;
type: StackItemType;
#id: string;
relationshipContext?:
Expand All @@ -72,6 +74,7 @@ export class StackItem {
id,
type,
closeAfterSaving,
useBaseTemplate,
relationshipContext,
} = args;

Expand All @@ -81,6 +84,7 @@ export class StackItem {
this.stackIndex = stackIndex;
this.type = inferStackItemType(type);
this.closeAfterSaving = closeAfterSaving;
this.useBaseTemplate = useBaseTemplate;
this.relationshipContext = relationshipContext;
}

Expand All @@ -97,6 +101,7 @@ export class StackItem {
stackIndex,
relationshipContext,
type,
useBaseTemplate,
} = this;
return new StackItem({
format,
Expand All @@ -106,6 +111,7 @@ export class StackItem {
type,
stackIndex,
relationshipContext,
useBaseTemplate,
...args,
});
}
Expand Down
43 changes: 42 additions & 1 deletion packages/host/app/services/operator-mode-state-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ interface CardItem {
id: string;
format: 'isolated' | 'edit' | 'head';
type?: StackItemType;
useBaseTemplate?: boolean;
}

export type FileView = 'inspector' | 'browser';
Expand Down Expand Up @@ -414,6 +415,20 @@ export default class OperatorModeStateService extends Service {
this.schedulePersist();
}

findCardInStackSafe(
card: CardDef | string,
stackIndex: number,
): StackItem | undefined {
try {
return this.findCardInStack(card, stackIndex);
} catch (err) {
if (err instanceof Error && err.message.includes('Could not find card')) {
return undefined;
}
throw err;
}
}

findCardInStack(card: CardDef | string, stackIndex: number): StackItem {
let stack = this._state.stacks[stackIndex];
if (!stack) {
Expand All @@ -437,7 +452,11 @@ export default class OperatorModeStateService extends Service {
return item;
}

editCardOnStack(stackIndex: number, card: CardDef): void {
editCardOnStack(
stackIndex: number,
card: CardDef,
opts?: { useBaseTemplate?: boolean },
): void {
let item = this.findCardInStack(card, stackIndex);
if (item.type === 'file') {
return;
Expand All @@ -447,6 +466,26 @@ export default class OperatorModeStateService extends Service {
item.clone({
request: new Deferred(),
format: 'edit',
useBaseTemplate: opts?.useBaseTemplate,
}),
);
}

viewCardOnStack(
stackIndex: number,
card: CardDef,
opts?: { useBaseTemplate?: boolean },
): void {
let item = this.findCardInStack(card, stackIndex);
if (item.type === 'file') {
return;
}
this.replaceItemInStack(
item,
item.clone({
request: new Deferred(),
format: 'isolated',
useBaseTemplate: opts?.useBaseTemplate,
}),
);
}
Expand Down Expand Up @@ -907,6 +946,7 @@ export default class OperatorModeStateService extends Service {
id: instance?.id ?? item.id,
format: item.format,
type: item.type === 'card' ? undefined : item.type,
useBaseTemplate: item.useBaseTemplate ?? undefined,
});
}
}
Expand Down Expand Up @@ -991,6 +1031,7 @@ export default class OperatorModeStateService extends Service {
format,
stackIndex,
type: item.type,
useBaseTemplate: item.useBaseTemplate,
}),
);
}
Expand Down
Loading
Loading