Skip to content

Commit b22fd7a

Browse files
authored
Merge pull request #2201 from cardstack/cs-7965-add-edit-mode-for-code-refs
Add edit mode to code ref field
2 parents 6eb8bb0 + 60857d7 commit b22fd7a

File tree

4 files changed

+302
-10
lines changed

4 files changed

+302
-10
lines changed

packages/base/code-ref.gts

+76-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type Owner from '@ember/owner';
2+
import { tracked } from '@glimmer/tracking';
13
import {
24
Component,
35
primitive,
@@ -13,7 +15,14 @@ import {
1315
type SerializeOpts,
1416
type JSONAPISingleResourceDocument,
1517
} from './card-api';
16-
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+
CardURLContextName,
23+
} from '@cardstack/runtime-common';
24+
import { not } from '@cardstack/boxel-ui/helpers';
25+
import { BoxelInput } from '@cardstack/boxel-ui/components';
1726
import CodeIcon from '@cardstack/boxel-icons/code';
1827

1928
function moduleIsUrlLike(module: string) {
@@ -35,6 +44,70 @@ class BaseView extends Component<typeof CodeRefField> {
3544
</template>
3645
}
3746

47+
class EditView extends Component<typeof CodeRefField> {
48+
@consume(CardURLContextName) declare cardURL: string | undefined;
49+
@tracked validationState: 'initial' | 'valid' | 'invalid' = 'initial';
50+
@tracked private maybeCodeRef: string | undefined = maybeSerializeCodeRef(
51+
this.args.model ?? undefined,
52+
);
53+
54+
<template>
55+
<BoxelInput
56+
data-test-hasValidated={{this.setIfValid.isIdle}}
57+
@value={{this.maybeCodeRef}}
58+
@state={{this.validationState}}
59+
@onInput={{this.onInput}}
60+
@disabled={{not @canEdit}}
61+
/>
62+
</template>
63+
64+
constructor(owner: Owner, args: any) {
65+
super(owner, args);
66+
if (this.maybeCodeRef != null) {
67+
this.setIfValid.perform(this.maybeCodeRef, { checkOnly: true });
68+
}
69+
}
70+
71+
private onInput = (inputVal: string) => {
72+
this.maybeCodeRef = inputVal;
73+
this.setIfValid.perform(this.maybeCodeRef);
74+
};
75+
76+
private setIfValid = restartableTask(
77+
async (maybeCodeRef: string, opts?: { checkOnly?: true }) => {
78+
this.validationState = 'initial';
79+
if (maybeCodeRef.length === 0) {
80+
if (!opts?.checkOnly) {
81+
this.args.set(undefined);
82+
}
83+
return;
84+
}
85+
86+
let parts = maybeCodeRef.split('/');
87+
if (parts.length < 2) {
88+
this.validationState = 'invalid';
89+
return;
90+
}
91+
92+
let name = parts.pop()!;
93+
let module = parts.join('/');
94+
try {
95+
let code = (await import(module))[name];
96+
if (code) {
97+
this.validationState = 'valid';
98+
if (!opts?.checkOnly) {
99+
this.args.set({ module, name });
100+
}
101+
} else {
102+
this.validationState = 'invalid';
103+
}
104+
} catch (err) {
105+
this.validationState = 'invalid';
106+
}
107+
},
108+
);
109+
}
110+
38111
export default class CodeRefField extends FieldDef {
39112
static icon = CodeIcon;
40113
static [primitive]: ResolvedCodeRef;
@@ -71,8 +144,8 @@ export default class CodeRefField extends FieldDef {
71144
}
72145

73146
static embedded = class Embedded extends BaseView {};
74-
// The edit template is meant to be read-only, this field card is not mutable
75-
static edit = class Edit extends BaseView {};
147+
148+
static edit = EditView;
76149
}
77150

78151
function maybeSerializeCodeRef(

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>

packages/host/tests/integration/components/card-basics-test.gts

+215-7
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import { renderCard } from '../../helpers/render-component';
7575
import { setupRenderingTest } from '../../helpers/setup';
7676

7777
let loader: Loader;
78+
const testModuleRealm = 'http://localhost:4202/test/';
7879

7980
module('Integration | card-basics', function (hooks) {
8081
setupRenderingTest(hooks);
@@ -644,13 +645,13 @@ module('Integration | card-basics', function (hooks) {
644645
};
645646
}
646647

647-
let ref = { module: `http://localhost:4202/test/person`, name: 'Person' };
648+
let ref = { module: `${testRealmURL}person`, name: 'Person' };
648649
let driver = new DriverCard({ ref });
649650

650651
await renderCard(loader, driver, 'embedded');
651652
assert
652653
.dom('[data-test-ref]')
653-
.containsText(`Module: http://localhost:4202/test/person Name: Person`);
654+
.containsText(`Module: ${testRealmURL}person Name: Person`);
654655

655656
// is this worth an assertion? or is it just obvious?
656657
assert.strictEqual(
@@ -660,7 +661,7 @@ module('Integration | card-basics', function (hooks) {
660661
);
661662
});
662663

663-
test('render codeRef fields are not editable', async function (assert) {
664+
test('can render a CodeRef field in edit mode with an initial value', async function (assert) {
664665
class DriverCard extends CardDef {
665666
@field ref = contains(CodeRefField);
666667
static edit = class Edit extends Component<typeof this> {
@@ -670,14 +671,221 @@ module('Integration | card-basics', function (hooks) {
670671
};
671672
}
672673

673-
let ref = { module: `http://localhost:4202/test/person`, name: 'Person' };
674+
let ref = { module: `${testModuleRealm}person`, name: 'Person' };
674675
let driver = new DriverCard({ ref });
676+
await renderCard(loader, driver, 'edit');
677+
await waitFor('[data-test-hasValidated]');
678+
679+
assert
680+
.dom('[data-test-ref] input')
681+
.hasValue(`${testModuleRealm}person/Person`, 'input field is correct');
682+
assert
683+
.dom('[data-test-ref] [data-test-boxel-input-validation-state="valid"]')
684+
.exists('code ref is valid');
685+
});
686+
687+
test('can edit a CodeRef field with a valid URL-like code ref', async function (assert) {
688+
class DriverCard extends CardDef {
689+
@field ref = contains(CodeRefField);
690+
static edit = class Edit extends Component<typeof this> {
691+
<template>
692+
<div data-test-ref><@fields.ref /></div>
693+
</template>
694+
};
695+
}
696+
697+
let driver = new DriverCard();
675698

676699
await renderCard(loader, driver, 'edit');
677-
assert.dom('input').doesNotExist('no input fields exist');
700+
await fillIn('[data-test-ref] input', `${testModuleRealm}person/Person`);
701+
await waitFor('[data-test-hasValidated]');
678702
assert
679-
.dom('[data-test-ref]')
680-
.containsText(`Module: http://localhost:4202/test/person Name: Person`);
703+
.dom('[data-test-ref] input')
704+
.hasValue(`${testModuleRealm}person/Person`, 'input field is correct');
705+
assert
706+
.dom('[data-test-ref] [data-test-boxel-input-validation-state="valid"]')
707+
.exists('code ref is valid');
708+
assert.deepEqual(
709+
driver.ref,
710+
{
711+
module: `${testModuleRealm}person`,
712+
name: 'Person',
713+
},
714+
'code ref field value is correct',
715+
);
716+
});
717+
718+
test('can edit a CodeRef field with a valid non-URL code ref', async function (assert) {
719+
class DriverCard extends CardDef {
720+
@field ref = contains(CodeRefField);
721+
static edit = class Edit extends Component<typeof this> {
722+
<template>
723+
<div data-test-ref><@fields.ref /></div>
724+
</template>
725+
};
726+
}
727+
728+
let driver = new DriverCard();
729+
730+
await renderCard(loader, driver, 'edit');
731+
await fillIn(
732+
'[data-test-ref] input',
733+
`@cardstack/boxel-host/commands/save-card/default`,
734+
);
735+
await waitFor('[data-test-hasValidated]');
736+
assert
737+
.dom('[data-test-ref] input')
738+
.hasValue(
739+
`@cardstack/boxel-host/commands/save-card/default`,
740+
'input field is correct',
741+
);
742+
assert
743+
.dom('[data-test-ref] [data-test-boxel-input-validation-state="valid"]')
744+
.exists('code ref is valid');
745+
746+
assert.deepEqual(
747+
driver.ref,
748+
{
749+
module: `@cardstack/boxel-host/commands/save-card`,
750+
name: `default`,
751+
},
752+
'code ref field value is correct',
753+
);
754+
});
755+
756+
test('can edit a CodeRef field with an invalid non-URL code ref', async function (assert) {
757+
class DriverCard extends CardDef {
758+
@field ref = contains(CodeRefField);
759+
static edit = class Edit extends Component<typeof this> {
760+
<template>
761+
<div data-test-ref><@fields.ref /></div>
762+
</template>
763+
};
764+
}
765+
766+
let ref = { module: `${testModuleRealm}person`, name: 'Person' };
767+
let driver = new DriverCard({ ref });
768+
await renderCard(loader, driver, 'edit');
769+
await waitFor('[data-test-hasValidated]');
770+
await fillIn(
771+
'[data-test-ref] input',
772+
`@cardstack/boxel-host/commands/save-card/doesNotExist`,
773+
);
774+
await waitFor('[data-test-hasValidated]');
775+
assert
776+
.dom('[data-test-ref] input')
777+
.hasValue(
778+
`@cardstack/boxel-host/commands/save-card/doesNotExist`,
779+
'input field is correct',
780+
);
781+
assert
782+
.dom(
783+
'[data-test-ref] [data-test-boxel-input-validation-state="invalid"]',
784+
)
785+
.exists('code ref is invalid');
786+
787+
assert.deepEqual(
788+
driver.ref,
789+
{
790+
module: `${testModuleRealm}person`,
791+
name: 'Person',
792+
},
793+
'code ref field value is correct',
794+
);
795+
});
796+
797+
test('can edit a CodeRef field with an invalid URL-like code ref', async function (assert) {
798+
class DriverCard extends CardDef {
799+
@field ref = contains(CodeRefField);
800+
static edit = class Edit extends Component<typeof this> {
801+
<template>
802+
<div data-test-ref><@fields.ref /></div>
803+
</template>
804+
};
805+
}
806+
let ref = { module: `${testModuleRealm}person`, name: 'Person' };
807+
let driver = new DriverCard({ ref });
808+
await renderCard(loader, driver, 'edit');
809+
await waitFor('[data-test-hasValidated]');
810+
811+
await fillIn(
812+
'[data-test-ref] input',
813+
`${testModuleRealm}doesNotExist/Nothing`,
814+
);
815+
await waitFor('[data-test-hasValidated]');
816+
assert
817+
.dom('[data-test-ref] input')
818+
.hasValue(
819+
`${testModuleRealm}doesNotExist/Nothing`,
820+
'input field is correct',
821+
);
822+
assert
823+
.dom(
824+
'[data-test-ref] [data-test-boxel-input-validation-state="invalid"]',
825+
)
826+
.exists('code ref is invalid');
827+
assert.deepEqual(
828+
driver.ref,
829+
{
830+
module: `${testModuleRealm}person`,
831+
name: 'Person',
832+
},
833+
'code ref field value is correct',
834+
);
835+
});
836+
837+
test('can edit a CodeRef field with a code ref that is invalid because its too short', async function (assert) {
838+
class DriverCard extends CardDef {
839+
@field ref = contains(CodeRefField);
840+
static edit = class Edit extends Component<typeof this> {
841+
<template>
842+
<div data-test-ref><@fields.ref /></div>
843+
</template>
844+
};
845+
}
846+
let ref = { module: `${testModuleRealm}person`, name: 'Person' };
847+
let driver = new DriverCard({ ref });
848+
await renderCard(loader, driver, 'edit');
849+
await waitFor('[data-test-hasValidated]');
850+
851+
await fillIn('[data-test-ref] input', `@cardstack`);
852+
await waitFor('[data-test-hasValidated]');
853+
assert
854+
.dom('[data-test-ref] input')
855+
.hasValue(`@cardstack`, 'input field is correct');
856+
assert
857+
.dom(
858+
'[data-test-ref] [data-test-boxel-input-validation-state="invalid"]',
859+
)
860+
.exists('code ref is invalid');
861+
assert.deepEqual(
862+
driver.ref,
863+
{
864+
module: `${testModuleRealm}person`,
865+
name: 'Person',
866+
},
867+
'code ref field value is correct',
868+
);
869+
});
870+
871+
test('can clear a CodeRef field in edit mode', async function (assert) {
872+
class DriverCard extends CardDef {
873+
@field ref = contains(CodeRefField);
874+
static edit = class Edit extends Component<typeof this> {
875+
<template>
876+
<div data-test-ref><@fields.ref /></div>
877+
</template>
878+
};
879+
}
880+
let ref = { module: `${testModuleRealm}person`, name: 'Person' };
881+
let driver = new DriverCard({ ref });
882+
await renderCard(loader, driver, 'edit');
883+
await waitFor('[data-test-hasValidated]');
884+
await fillIn('[data-test-ref] input', '');
885+
assert
886+
.dom('[data-test-ref] input')
887+
.hasValue('', 'input field is correct');
888+
assert.deepEqual(driver.ref, undefined, 'code ref can be unset');
681889
});
682890

683891
test('render base64 image card', async function (assert) {

packages/runtime-common/constants.ts

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export const DefaultFormatsContextName = 'default-format-context';
3030

3131
export const PermissionsContextName = 'permissions-context';
3232

33+
export const CardURLContextName = 'card-url-context';
34+
3335
export const RealmURLContextName = 'realm-url-context';
3436

3537
export interface Permissions {

0 commit comments

Comments
 (0)