Skip to content

new arg to filter by subclass ref #2105

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Feb 19, 2025
Merged
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
11 changes: 10 additions & 1 deletion packages/base/card-api.gts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
type CardResourceMeta,
CodeRef,
CommandContext,
type ResolvedCodeRef,
} from '@cardstack/runtime-common';
import type { ComponentLike } from '@glint/template';
import { initSharedState } from './shared-state';
Expand Down Expand Up @@ -1088,7 +1089,13 @@ class LinksTo<CardT extends CardDefConstructor> implements Field<CardT> {
}
return class LinksToComponent extends GlimmerComponent<{
Element: HTMLElement;
Args: { Named: { format?: Format; displayContainer?: boolean } };
Args: {
Named: {
format?: Format;
displayContainer?: boolean;
typeConstraint?: ResolvedCodeRef;
};
};
Blocks: {};
}> {
<template>
Expand All @@ -1097,6 +1104,7 @@ class LinksTo<CardT extends CardDefConstructor> implements Field<CardT> {
<LinksToEditor
@model={{(getInnerModel)}}
@field={{linksToField}}
@typeConstraint={{@typeConstraint}}
...attributes
/>
{{else}}
Expand Down Expand Up @@ -1812,6 +1820,7 @@ export type BaseDefComponent = ComponentLike<{
fieldName: string | undefined;
context?: CardContext;
canEdit?: boolean;
typeConstraint?: ResolvedCodeRef;
};
}>;

Expand Down
16 changes: 14 additions & 2 deletions packages/base/field-component.gts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
Loader,
type CodeRef,
type Permissions,
ResolvedCodeRef,
} from '@cardstack/runtime-common';
import type { ComponentLike } from '@glint/template';
import { CardContainer } from '@cardstack/boxel-ui/components';
Expand All @@ -33,7 +34,13 @@ import Component from '@glimmer/component';

export interface BoxComponentSignature {
Element: HTMLElement; // This may not be true for some field components, but it's true more often than not
Args: { Named: { format?: Format; displayContainer?: boolean } };
Args: {
Named: {
format?: Format;
displayContainer?: boolean;
typeConstraint?: ResolvedCodeRef;
};
};
Blocks: {};
}

Expand Down Expand Up @@ -185,7 +192,11 @@ export function getBoxComponent(

let component: TemplateOnlyComponent<{
Element: HTMLElement;
Args: { format?: Format; displayContainer?: boolean };
Args: {
format?: Format;
displayContainer?: boolean;
typeConstraint?: ResolvedCodeRef;
};
}> = <template>
<CardContextConsumer as |context|>
<PermissionsConsumer as |permissions|>
Expand Down Expand Up @@ -238,6 +249,7 @@ export function getBoxComponent(
(not field.computeVia)
permissions.canWrite
}}
@typeConstraint={{@typeConstraint}}
/>
</CardContainer>
</DefaultFormatsProvider>
Expand Down
18 changes: 18 additions & 0 deletions packages/base/links-to-editor.gts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import {
identifyCard,
CardContextName,
RealmURLContextName,
type ResolvedCodeRef,
getNarrowestType,
Loader,
} from '@cardstack/runtime-common';
import { AddButton, IconButton } from '@cardstack/boxel-ui/components';
import { IconMinusCircle } from '@cardstack/boxel-ui/icons';
Expand All @@ -33,6 +36,7 @@ interface Signature {
Args: {
model: Box<CardDef | null>;
field: Field<typeof CardDef>;
typeConstraint?: ResolvedCodeRef;
};
}

Expand Down Expand Up @@ -143,6 +147,9 @@ export class LinksToEditor extends GlimmerComponent<Signature> {

private chooseCard = restartableTask(async () => {
let type = identifyCard(this.args.field.card) ?? baseCardRef;
if (this.args.typeConstraint) {
type = await getNarrowestType(this.args.typeConstraint, type, myLoader());
}
let chosenCard: CardDef | undefined = await chooseCard(
{ filter: { type } },
{
Expand All @@ -160,3 +167,14 @@ export class LinksToEditor extends GlimmerComponent<Signature> {
}
});
}

function myLoader(): Loader {
// we know this code is always loaded by an instance of our Loader, which sets
// import.meta.loader.

// When type-checking realm-server, tsc sees this file and thinks
// it will be transpiled to CommonJS and so it complains about this line. But
// this file is always loaded through our loader and always has access to import.meta.
// @ts-ignore
return (import.meta as any).loader;
}
23 changes: 22 additions & 1 deletion packages/base/links-to-many-component.gts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ import {
getPlural,
CardContextName,
RealmURLContextName,
type ResolvedCodeRef,
getNarrowestType,
Loader,
} from '@cardstack/runtime-common';
import { IconMinusCircle, IconX, FourLines } from '@cardstack/boxel-ui/icons';
import { eq } from '@cardstack/boxel-ui/helpers';
Expand All @@ -52,6 +55,7 @@ interface Signature {
boxedElement: Box<BaseDef>,
): typeof BaseDef;
childFormat: 'atom' | 'fitted';
typeConstraint?: ResolvedCodeRef;
};
}

Expand Down Expand Up @@ -95,7 +99,12 @@ class LinksToManyEditor extends GlimmerComponent<Signature> {
selectedCards?.map((card: any) => ({ not: { eq: { id: card.id } } })) ??
[];
let type = identifyCard(this.args.field.card) ?? baseCardRef;
let filter = { every: [{ type }, ...selectedCardsQuery] };
if (this.args.typeConstraint) {
type = await getNarrowestType(this.args.typeConstraint, type, myLoader());
}
let filter = {
every: [{ type }, ...selectedCardsQuery],
};
let chosenCard: CardDef | undefined = await chooseCard(
{ filter },
{
Expand Down Expand Up @@ -405,6 +414,7 @@ export function getLinksToManyComponent({
defaultFormats.cardDef
model
}}
@typeConstraint={{@typeConstraint}}
...attributes
/>
{{else}}
Expand Down Expand Up @@ -481,3 +491,14 @@ export function getLinksToManyComponent({
},
});
}

function myLoader(): Loader {
// we know this code is always loaded by an instance of our Loader, which sets
// import.meta.loader.

// When type-checking realm-server, tsc sees this file and thinks
// it will be transpiled to CommonJS and so it complains about this line. But
// this file is always loaded through our loader and always has access to import.meta.
// @ts-ignore
return (import.meta as any).loader;
}
21 changes: 19 additions & 2 deletions packages/base/spec.gts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ import {
Pill,
RealmIcon,
} from '@cardstack/boxel-ui/components';
import { loadCard, Loader } from '@cardstack/runtime-common';
import {
codeRefWithAbsoluteURL,
Loader,
loadCard,
isResolvedCodeRef,
} from '@cardstack/runtime-common';
import { eq } from '@cardstack/boxel-ui/helpers';

import GlimmerComponent from '@glimmer/component';
Expand Down Expand Up @@ -102,6 +107,18 @@ export class Spec extends CardDef {
}
});

get absoluteRef() {
if (!this.args.model.ref || !this.args.model.id) {
return undefined;
}
let url = new URL(this.args.model.id);
let ref = codeRefWithAbsoluteURL(this.args.model.ref, url);
if (!isResolvedCodeRef(ref)) {
throw new Error('ref is not a resolved code ref');
}
return ref;
}

private get realmInfo() {
return getCardMeta(this.args.model as CardDef, 'realmInfo');
}
Expand Down Expand Up @@ -140,7 +157,7 @@ export class Spec extends CardDef {
{{#if (eq @model.specType 'field')}}
<@fields.containedExamples />
{{else}}
<@fields.linkedExamples />
<@fields.linkedExamples @typeConstraint={{this.absoluteRef}} />
{{/if}}
</section>
<section class='module section'>
Expand Down
27 changes: 27 additions & 0 deletions packages/host/tests/acceptance/code-submode/spec-test.gts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,33 @@ module('Acceptance | Spec preview', function (hooks) {
assert.dom('[data-test-exported-name]').hasText('NewSkill');
assert.dom('[data-test-module-href]').hasText(`${testRealmURL}new-skill`);
});

test('when adding linked examples, card chooser options are narrowed to this type', async function (assert) {
await visitOperatorMode({
stacks: [
[
{
id: `${testRealmURL}person-entry`,
format: 'edit',
},
],
],
submode: 'interact',
});
assert.dom('[data-test-links-to-many="linkedExamples"]').exists();
await click('[data-test-add-new]');
assert
.dom('[data-test-card-catalog-modal] [data-test-boxel-header-title]')
.containsText('Person');
assert.dom('[data-test-card-catalog-item]').exists({ count: 2 });
assert
.dom(`[data-test-card-catalog-item="${testRealmURL}Person/1"]`)
.exists();
assert
.dom(`[data-test-card-catalog-item="${testRealmURL}Person/fadhlan"]`)
.exists();
});

test('title does not default to "default"', async function (assert) {
await visitOperatorMode({
submode: 'code',
Expand Down
36 changes: 34 additions & 2 deletions packages/runtime-common/code-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,23 @@ export function isBaseDef(cardOrField: any): cardOrField is typeof BaseDef {
return typeof cardOrField === 'function' && 'baseDef' in cardOrField;
}

export function isCardDef(card: any): card is typeof CardDef {
return isBaseDef(card) && 'isCardDef' in card;
export function isCardDef(card: any): card is typeof CardDef;
export function isCardDef(codeRef: CodeRef, loader: Loader): Promise<boolean>;
export function isCardDef(
cardOrCodeRef: any,
loader?: Loader,
): boolean | Promise<boolean> {
if (isCodeRef(cardOrCodeRef)) {
if (!loader) {
throw new Error(
'Loader is required to check if a code ref is a card def',
);
}
return loadCard(cardOrCodeRef, { loader })
.then((card) => isCardDef(card))
.catch(() => false);
}
return isBaseDef(cardOrCodeRef) && 'isCardDef' in cardOrCodeRef;
}

export function isCardInstance(card: any): card is CardDef {
Expand Down Expand Up @@ -227,3 +242,20 @@ export function humanReadable(ref: CodeRef): string {
function assertNever(value: never) {
return new Error(`should never happen ${value}`);
}

// utility to return `typeConstraint` when it exists and is part of the ancestor chain of `type`
export async function getNarrowestType(
typeConstraint: CodeRef,
type: CodeRef,
loader: Loader,
) {
let narrowTypeExists = false;
// Since the only place this function is used is inside of the spec preview,
// We use isCardDef (a shortcut) because it's a faster check to determine if `typeConstraint` is in the same inheritance chain as `type`
// As `type` is always a card, checking that the typeConstraint isCardDef is a sufficient condition
// TODO: This will have to be made more generic in consideration of other scenarios. This commit shows a solution that was more generic https://github.com/cardstack/boxel/pull/2105/commits/02e8408b776f4dea179978271b6f1febc0246f9b
narrowTypeExists = (await isCardDef(typeConstraint, loader)) ?? false;
let narrowestType =
narrowTypeExists && typeConstraint ? typeConstraint : type;
return narrowestType;
}
Loading