Skip to content
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

new arg to filter by subclass ref #2105

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
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
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 @@ -1092,7 +1093,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;
subclassType?: ResolvedCodeRef;
};
};
Blocks: {};
}> {
<template>
Expand All @@ -1101,6 +1108,7 @@ class LinksTo<CardT extends CardDefConstructor> implements Field<CardT> {
<LinksToEditor
@model={{(getInnerModel)}}
@field={{linksToField}}
@subclassType={{@subclassType}}
...attributes
/>
{{else}}
Expand Down Expand Up @@ -1816,6 +1824,7 @@ export type BaseDefComponent = ComponentLike<{
fieldName: string | undefined;
context?: CardContext;
canEdit?: boolean;
subclassType?: 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;
subclassType?: 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;
subclassType?: ResolvedCodeRef;
};
}> = <template>
<CardContextConsumer as |context|>
<PermissionsConsumer as |permissions|>
Expand Down Expand Up @@ -238,6 +249,7 @@ export function getBoxComponent(
(not field.computeVia)
permissions.canWrite
}}
@subclassType={{@subclassType}}
/>
</CardContainer>
</DefaultFormatsProvider>
Expand Down
16 changes: 16 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>;
subclassType?: ResolvedCodeRef;
};
}

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

private chooseCard = restartableTask(async () => {
let type = identifyCard(this.args.field.card) ?? baseCardRef;
type = await getNarrowestType(this.args.subclassType, type, myLoader());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, you can only call this function if a subclassType arg was provided

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually do you even need to check if the type is the base CardDef? We know that all cards descend from it, so maybe in that case you can skip loading the whole chain and just use the provided subtype.

let chosenCard: CardDef | undefined = await chooseCard(
{ filter: { type } },
{
Expand All @@ -160,3 +165,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;
}
21 changes: 20 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';
subclassType?: ResolvedCodeRef;
};
}

Expand Down Expand Up @@ -95,7 +99,10 @@ 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] };
type = await getNarrowestType(this.args.subclassType, type, myLoader());
let filter = {
every: [{ type }, ...selectedCardsQuery],
};
let chosenCard: CardDef | undefined = await chooseCard(
{ filter },
{
Expand Down Expand Up @@ -405,6 +412,7 @@ export function getLinksToManyComponent({
defaultFormats.cardDef
model
}}
@subclassType={{@subclassType}}
...attributes
/>
{{else}}
Expand Down Expand Up @@ -481,3 +489,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 @subclassType={{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 @@ -326,6 +326,33 @@ module('Spec preview', function (hooks) {
await click('[data-test-create-spec-instance]');
assert.dom('[data-test-exported-type]').hasText('card');
});

test('can add linked examples to spec instance narrows the chooser by code ref inside of the instance', 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
56 changes: 55 additions & 1 deletion packages/runtime-common/code-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
type FieldDef,
} from 'https://cardstack.com/base/card-api';
import { Loader } from './loader';
import { isField } from './constants';
import { baseCardRef, isField } from './constants';
import { CardError } from './error';
import { trimExecutableExtension } from './index';

Expand Down Expand Up @@ -227,3 +227,57 @@ export function humanReadable(ref: CodeRef): string {
function assertNever(value: never) {
return new Error(`should never happen ${value}`);
}

function refEquals(ref1: CodeRef, ref2: CodeRef): boolean {
// For now, let's only handle for resolved code refs
if (!isResolvedCodeRef(ref1) || !isResolvedCodeRef(ref2)) {
return false;
}
return ref1.name === ref2.name && ref1.module === ref2.module;
}

async function getAncestorRef(codeRef: CodeRef, loader: Loader) {
let card = await loadCard(codeRef, { loader: loader });
let ancestor = getAncestor(card);
return identifyCard(ancestor);
}

//This function identifies the code ref identity of the card and verifies
//that it is a child of the ancestor
async function isInsideAncestorChain(
codeRef: CodeRef,
codeRefAncestor: CodeRef,
loader: Loader,
): Promise<boolean | undefined> {
if (
refEquals(codeRef, baseCardRef) &&
!refEquals(codeRefAncestor, baseCardRef)
) {
return false;
}
if (refEquals(codeRef, codeRefAncestor)) {
return true;
} else {
let newAncestorRef = await getAncestorRef(codeRef, loader);
if (newAncestorRef) {
return isInsideAncestorChain(newAncestorRef, codeRefAncestor, loader);
} else {
return false;
}
}
}

// utility to return subclassType when it exists and is part of the ancestor chain of type
export async function getNarrowestType(
subclassType: CodeRef | undefined,
type: CodeRef,
loader: Loader,
) {
let narrowTypeExists = false;
if (subclassType) {
narrowTypeExists =
(await isInsideAncestorChain(subclassType, type, loader)) ?? false;
}
let narrowestType = narrowTypeExists && subclassType ? subclassType : type;
return narrowestType;
}