diff --git a/packages/base/card-api.gts b/packages/base/card-api.gts index e59854f2891..937af0030d5 100644 --- a/packages/base/card-api.gts +++ b/packages/base/card-api.gts @@ -2138,7 +2138,7 @@ function brokenSingularLink( // slot when it can't load); `head` / `metadata` / `markdown` / `form` have no // dedicated placeholder footprint and fall back to the general-purpose // `embedded` layout. -function brokenLinkFormat( +export function brokenLinkFormat( format: Format | undefined, defaultFormat: Format, ): BrokenLinkFormat { diff --git a/packages/base/default-templates/broken-link-template.gts b/packages/base/default-templates/broken-link-template.gts index ddcde0fe0f6..34228b255e4 100644 --- a/packages/base/default-templates/broken-link-template.gts +++ b/packages/base/default-templates/broken-link-template.gts @@ -36,6 +36,7 @@ function isSafeHttpUrl(url: string): boolean { } export default class BrokenLinkTemplate extends GlimmerComponent<{ + Element: HTMLDivElement; Args: BrokenLinkTemplateArgs; }> { private get isNotFound() { @@ -113,6 +114,7 @@ export default class BrokenLinkTemplate extends GlimmerComponent<{ class='broken-link-template {{@format}} {{@state}}' data-test-broken-link-template={{@format}} data-test-broken-link-state={{@state}} + ...attributes > {{#if (eq @format 'atom')}} diff --git a/packages/base/links-to-many-component.gts b/packages/base/links-to-many-component.gts index 5b24720c3f7..8719972afe8 100644 --- a/packages/base/links-to-many-component.gts +++ b/packages/base/links-to-many-component.gts @@ -1,6 +1,6 @@ import GlimmerComponent from '@glimmer/component'; import { on } from '@ember/modifier'; -import { fn } from '@ember/helper'; +import { fn, get } from '@ember/helper'; import { BaseDef, type CardContext, @@ -14,7 +14,10 @@ import { CreateCardFn, CardCrudFunctions, isFileDef, + brokenLinkFormat, } from './card-api'; +import BrokenLinkTemplate from './default-templates/broken-link-template'; +import { getRelationship, type RelationshipState } from './field-support'; import { BoxComponentSignature, DefaultFormatsConsumer, @@ -201,10 +204,18 @@ class LinksToManyStandardEditor extends GlimmerComponent) in sync. + // + // `broken` carries the per-slot terminal failure state (read once here via a + // pure `getRelationship`) so a broken element shows the placeholder + remove + // affordance instead of trying to render a sentinel as a card. The `{{#each}}` + // still keys on the stable index `key`, so adding this never changes block + // identity and an input elsewhere in the edit form keeps focus. + let broken = brokenSlotsFor(this.args.model, this.args.field.name); return this.args.arrayField.children.map((child, index) => ({ box: child, index, key: index, + broken: broken[index], })); } @@ -255,14 +266,24 @@ class LinksToManyStandardEditor extends GlimmerComponent {{/if}} - {{#let - (getBoxComponent - (@cardTypeFor @field entry.box) entry.box @field - ) - as |Item| - }} - - {{/let}} + {{#if entry.broken}} + + {{else}} + {{#let + (getBoxComponent + (@cardTypeFor @field entry.box) entry.box @field + ) + as |Item| + }} + + {{/let}} + {{/if}} {{/each}} @@ -384,30 +405,64 @@ interface LinksToManyCompactEditorSignature { class LinksToManyCompactEditor extends GlimmerComponent { @consume(CardContextName) declare cardContext: CardContext; + // Per-slot broken-link state, read once per render via a pure + // `getRelationship`. The `{{#each}}` keeps keying on the stable child box, so + // this only drives the inner branch that swaps a broken card for the + // placeholder and never destabilizes a sibling pill mid-edit. + get brokenSlots() { + return brokenSlotsFor(this.args.model, this.args.field.name); + } +