Skip to content

Commit 4e68dff

Browse files
committed
Add code ref validation to code ref field
1 parent 93529a0 commit 4e68dff

File tree

4 files changed

+280
-34
lines changed

4 files changed

+280
-34
lines changed

packages/base/code-ref.gts

+82-17
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type Owner from '@ember/owner';
12
import { tracked } from '@glimmer/tracking';
23
import {
34
Component,
@@ -14,7 +15,14 @@ import {
1415
type SerializeOpts,
1516
type JSONAPISingleResourceDocument,
1617
} from './card-api';
17-
import { ResolvedCodeRef } from '@cardstack/runtime-common';
18+
import { restartableTask } from 'ember-concurrency';
19+
import { consume } from 'ember-provide-consume-context';
20+
import {
21+
type ResolvedCodeRef,
22+
type Loader,
23+
loadCard,
24+
CardURLContextName,
25+
} from '@cardstack/runtime-common';
1826
import { not } from '@cardstack/boxel-ui/helpers';
1927
import { BoxelInput } from '@cardstack/boxel-ui/components';
2028
import CodeIcon from '@cardstack/boxel-icons/code';
@@ -39,35 +47,81 @@ class BaseView extends Component<typeof CodeRefField> {
3947
}
4048

4149
class EditView extends Component<typeof CodeRefField> {
42-
@tracked private rawInput: string | undefined = maybeSerializeCodeRef(
50+
@consume(CardURLContextName) declare cardURL: string | undefined;
51+
@tracked validationState: 'initial' | 'valid' | 'invalid' = 'initial';
52+
@tracked private maybeCodeRef: string | undefined = maybeSerializeCodeRef(
4353
this.args.model ?? undefined,
4454
);
4555

4656
<template>
4757
<BoxelInput
48-
@value={{this.rawInput}}
58+
data-test-hasValidated={{this.setIfValid.isIdle}}
59+
@value={{this.maybeCodeRef}}
60+
@state={{this.validationState}}
4961
@onInput={{this.onInput}}
5062
@disabled={{not @canEdit}}
5163
/>
5264
</template>
5365

54-
private onInput = (inputVal: string) => {
55-
this.rawInput = inputVal;
56-
if (this.rawInput.length === 0) {
57-
this.args.set(undefined);
58-
return;
59-
}
60-
61-
let parts = this.rawInput.split('/');
62-
if (parts.length < 2) {
63-
this.args.set(undefined);
64-
return;
66+
constructor(owner: Owner, args: any) {
67+
super(owner, args);
68+
if (this.maybeCodeRef != null) {
69+
this.setIfValid.perform(this.maybeCodeRef, true);
6570
}
71+
}
6672

67-
let name = parts.pop();
68-
let module = parts.join('/');
69-
this.args.set({ module, name });
73+
private onInput = (inputVal: string) => {
74+
this.maybeCodeRef = inputVal;
75+
this.setIfValid.perform(this.maybeCodeRef);
7076
};
77+
78+
private setIfValid = restartableTask(
79+
async (maybeCodeRef: string, checkOnly?: true) => {
80+
this.validationState = 'initial';
81+
if (maybeCodeRef.length === 0) {
82+
if (!checkOnly) {
83+
this.args.set(undefined);
84+
}
85+
return;
86+
}
87+
88+
let parts = maybeCodeRef.split('/');
89+
if (parts.length < 2) {
90+
this.validationState = 'invalid';
91+
return;
92+
}
93+
94+
let name = parts.pop()!;
95+
let module = parts.join('/');
96+
try {
97+
if (moduleIsUrlLike(module)) {
98+
await loadCard(
99+
{ module, name },
100+
{
101+
loader: myLoader(),
102+
relativeTo: this.cardURL ? new URL(this.cardURL) : undefined,
103+
},
104+
);
105+
this.validationState = 'valid';
106+
if (!checkOnly) {
107+
this.args.set({ module, name });
108+
}
109+
} else {
110+
let code = (await import(module))[name];
111+
if (code) {
112+
this.validationState = 'valid';
113+
if (!checkOnly) {
114+
this.args.set({ module, name });
115+
}
116+
} else {
117+
this.validationState = 'invalid';
118+
}
119+
}
120+
} catch (err) {
121+
this.validationState = 'invalid';
122+
}
123+
},
124+
);
71125
}
72126

73127
export default class CodeRefField extends FieldDef {
@@ -128,3 +182,14 @@ function maybeSerializeCodeRef(
128182
}
129183
return undefined;
130184
}
185+
186+
function myLoader(): Loader {
187+
// we know this code is always loaded by an instance of our Loader, which sets
188+
// import.meta.loader.
189+
190+
// When type-checking realm-server, tsc sees this file and thinks
191+
// it will be transpiled to CommonJS and so it complains about this line. But
192+
// this file is always loaded through our loader and always has access to import.meta.
193+
// @ts-ignore
194+
return (import.meta as any).loader;
195+
}

packages/host/app/components/preview.gts

+9
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { provide } from 'ember-provide-consume-context';
55
import {
66
CardContextName,
77
DefaultFormatsContextName,
8+
CardURLContextName,
89
ResolvedCodeRef,
910
} from '@cardstack/runtime-common';
1011

@@ -46,6 +47,14 @@ export default class Preview extends Component<Signature> {
4647
};
4748
}
4849

50+
@provide(CardURLContextName)
51+
// @ts-ignore "cardURL is declared but not used"
52+
private get cardURL() {
53+
return 'id' in this.args.card
54+
? (this.args.card?.id as string | undefined)
55+
: undefined;
56+
}
57+
4958
<template>
5059
<this.renderedCard @displayContainer={{@displayContainer}} ...attributes />
5160
</template>

0 commit comments

Comments
 (0)