Skip to content

Commit 7c5ef05

Browse files
jsantelljakedahn
andauthored
Shell: Load and run default recipe in fresh spaces (#1423)
* Initial charm list recipe work * feat: Implement a default recipe view for spaces, toggling between charm lists and the default recipe. --------- Co-authored-by: jakedahn <jake@common.tools>
1 parent edf9e2d commit 7c5ef05

File tree

10 files changed

+283
-26
lines changed

10 files changed

+283
-26
lines changed

packages/charm/src/ops/charms-controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export class CharmsController {
1515
}
1616

1717
async create(
18-
program: RuntimeProgram,
18+
program: RuntimeProgram | string,
1919
input?: object,
2020
): Promise<CharmController> {
2121
const recipe = await compileProgram(this.#manager, program);

packages/shell/src/components/Body.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,15 @@ export class XBodyElement extends BaseView {
2121
activeCharmId?: string;
2222

2323
override render() {
24-
const connected = this.cc ? "Connected" : "Not Connected";
2524
const charmView = html`
2625
<x-charm .cc="${this.cc}" .charmId="${this.activeCharmId}"></x-charm>
2726
`;
28-
const charmList = html`
29-
<x-charm-list .cc="${this.cc}"></x-charm-list>
27+
const spaceView = html`
28+
<x-space .cc="${this.cc}"></x-space>
3029
`;
31-
const view = this.activeCharmId ? charmView : charmList;
30+
const view = this.activeCharmId ? charmView : spaceView;
3231
return html`
33-
<div>
34-
<h2>App!! (${connected})</h2>
35-
<div>${view}</div>
36-
</div>
32+
<div>${view}</div>
3733
`;
3834
}
3935
}

packages/shell/src/components/CharmList.ts

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { css, html } from "lit";
22
import { property } from "lit/decorators.js";
33
import { BaseView } from "../views/BaseView.ts";
4-
import { CharmsController } from "@commontools/charm/ops";
5-
import { Task } from "@lit/task";
4+
import { CharmController } from "@commontools/charm/ops";
65
import { getNavigationHref } from "../lib/navigate.ts";
76

87
export class XCharmListElement extends BaseView {
@@ -17,26 +16,20 @@ export class XCharmListElement extends BaseView {
1716
`;
1817

1918
@property({ attribute: false })
20-
cc?: CharmsController;
19+
charms?: CharmController[];
2120

22-
private _charmList = new Task(this, {
23-
task: ([cc]) => {
24-
return cc ? cc.getAllCharms() : undefined;
25-
},
26-
args: () => [this.cc],
27-
});
21+
@property({ attribute: false })
22+
spaceName?: string;
2823

2924
override render() {
30-
const spaceName = this.cc ? this.cc.manager().getSpaceName() : undefined;
31-
const charmList = this._charmList.value;
32-
33-
if (!spaceName || !charmList) {
25+
const { charms, spaceName } = this;
26+
if (!spaceName || !charms) {
3427
return html`
3528
<x-spinner></x-spinner>
3629
`;
3730
}
3831

39-
const list = (charmList ?? []).map((charm) => {
32+
const list = charms.map((charm) => {
4033
const name = charm.name();
4134
const id = charm.id;
4235
const href = getNavigationHref(spaceName, id);
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { css, html } from "lit";
2+
import { property, state } from "lit/decorators.js";
3+
import { BaseView } from "../views/BaseView.ts";
4+
import { CharmsController } from "@commontools/charm/ops";
5+
import { Task } from "@lit/task";
6+
import * as DefaultRecipe from "../lib/default-recipe.ts";
7+
8+
type CharmData = {
9+
name: string;
10+
id: string;
11+
href: string;
12+
};
13+
14+
export class XSpaceElement extends BaseView {
15+
static override styles = css`
16+
:host {
17+
display: block;
18+
width: 100%;
19+
height: 100%;
20+
background-color: white;
21+
padding: 1rem;
22+
}
23+
`;
24+
25+
@property({ attribute: false })
26+
cc?: CharmsController;
27+
28+
@state()
29+
showCharmList = false;
30+
31+
@state()
32+
creatingDefaultRecipe = false;
33+
34+
private _charms = new Task(this, {
35+
task: async ([cc]) => {
36+
if (!cc) return undefined;
37+
38+
// Ensure charms are synced before checking
39+
const manager = cc.manager();
40+
await manager.synced();
41+
return await cc.getAllCharms();
42+
},
43+
args: () => [this.cc],
44+
});
45+
46+
async onRequestDefaultRecipe(e: Event) {
47+
e.preventDefault();
48+
if (this.creatingDefaultRecipe) {
49+
return;
50+
}
51+
if (!this.cc) {
52+
throw new Error(
53+
"Cannot create default recipe without a charms controller.",
54+
);
55+
}
56+
this.creatingDefaultRecipe = true;
57+
try {
58+
await DefaultRecipe.create(this.cc);
59+
} catch (e) {
60+
console.error(`Could not create default recipe: ${e}`);
61+
} finally {
62+
this.creatingDefaultRecipe = false;
63+
this._charms.run();
64+
}
65+
}
66+
67+
onViewToggle(e: Event) {
68+
e.preventDefault();
69+
this.showCharmList = !this.showCharmList;
70+
}
71+
72+
override render() {
73+
const spaceName = this.cc ? this.cc.manager().getSpaceName() : undefined;
74+
const charms = this._charms.value;
75+
const defaultRecipe = charms
76+
? DefaultRecipe.getDefaultRecipe(charms)
77+
: undefined;
78+
79+
const inner = !charms
80+
? html`
81+
<x-spinner></x-spinner>
82+
`
83+
: this.showCharmList
84+
? html`
85+
<x-charm-list .charms="${charms}" .spaceName="${spaceName}"></x-charm-list>
86+
`
87+
: !defaultRecipe
88+
? (this.creatingDefaultRecipe
89+
? html`
90+
<div>
91+
<span>Creating default recipe...</span>
92+
<x-spinner></x-spinner>
93+
</div>
94+
`
95+
: html`
96+
<div>
97+
<span>Create default recipe?</span>
98+
<button @click="${this.onRequestDefaultRecipe}">Go!</button>
99+
</div>
100+
`)
101+
// TBD if we want to use x-charm or ct-render directly here
102+
: html`
103+
<x-charm .charmId="${defaultRecipe.id}" .cc="${this.cc}"></x-charm>
104+
`;
105+
106+
return html`
107+
<div>
108+
<button @click="${this.onViewToggle}">${this.showCharmList
109+
? "show default"
110+
: "show list"}</button>
111+
${inner}
112+
</div>
113+
`;
114+
}
115+
}
116+
117+
globalThis.customElements.define("x-space", XSpaceElement);

packages/shell/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ export * from "./Header.ts";
22
export * from "./Body.ts";
33
export * from "./Charm.ts";
44
export * from "./CharmList.ts";
5+
export * from "./Space.ts";
56
export * from "./CTLogo.ts";
67
export * from "./Spinner.ts";

packages/shell/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { XRootView } from "./views/RootView.ts";
1111
import "./components/index.ts";
1212
import "./views/index.ts";
1313
import { AppController } from "./lib/app/controller.ts";
14+
import { getNavigationHref } from "./lib/navigate.ts";
1415

1516
console.log(`ENVIRONMENT=${ENVIRONMENT}`);
1617
console.log(`API_URL=${API_URL}`);
@@ -60,4 +61,9 @@ globalThis.addEventListener("navigate-to-charm", (e) => {
6061
}
6162
app.setSpace(spaceName);
6263
app.setActiveCharmId(charmId);
64+
65+
// Update the browser URL to reflect the new location
66+
// (DefaultCharmList should not use this event, it sets activeCharmId directly)
67+
const href = getNavigationHref(spaceName, charmId);
68+
globalThis.history.pushState({}, "", href);
6369
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { CharmController, CharmsController } from "@commontools/charm/ops";
2+
import { processSchema } from "@commontools/charm";
3+
4+
const ALL_CHARMS_ID =
5+
"baedreiahv63wxwgaem4hzjkizl4qncfgvca7pj5cvdon7cukumfon3ioye";
6+
const DEFAULT_CHARM_NAME = "DefaultCharmList";
7+
8+
export async function create(cc: CharmsController): Promise<CharmController> {
9+
const manager = cc.manager();
10+
const runtime = manager.runtime;
11+
12+
const recipeContent = await runtime.staticCache.getText(
13+
"recipes/charm-list.tsx",
14+
);
15+
const charm = await cc.create(recipeContent);
16+
17+
const tx = runtime.edit();
18+
const charmCell = charm.getCell();
19+
const sourceCell = charmCell.getSourceCell(processSchema);
20+
21+
if (!sourceCell) {
22+
// Not sure how/when this happens
23+
throw new Error("Could not create and link default recipe.");
24+
}
25+
26+
// Get the well-known allCharms cell using its EntityId format
27+
const allCharmsCell = await manager.getCellById({ "/": ALL_CHARMS_ID });
28+
sourceCell.withTx(tx).key("argument").key("allCharms").set(
29+
allCharmsCell.withTx(tx),
30+
);
31+
await tx.commit();
32+
33+
// Wait for the link to be processed
34+
await runtime.idle();
35+
await manager.synced();
36+
37+
return charm;
38+
}
39+
40+
export function getDefaultRecipe(
41+
charms: CharmController[],
42+
): CharmController | undefined {
43+
return charms.find((c) => {
44+
const name = c.name();
45+
return name && name.startsWith(DEFAULT_CHARM_NAME);
46+
});
47+
}

packages/shell/src/lib/runtime.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ANYONE, Identity, Session } from "@commontools/identity";
22
import { Runtime } from "@commontools/runner";
3-
import { charmId, CharmManager } from "@commontools/charm";
3+
import { charmId, CharmManager, processSchema } from "@commontools/charm";
44
import { CharmsController } from "@commontools/charm/ops";
55
import { StorageManager } from "@commontools/runner/storage/cache";
66
import { API_URL } from "./env.ts";
@@ -72,6 +72,13 @@ export async function createCharmsController(
7272

7373
const staticAssetUrl = new URL(API_URL);
7474
staticAssetUrl.pathname = "/static";
75+
76+
// We're hoisting CharmManager so that
77+
// we can create it after the runtime, but still reference
78+
// its `getSpaceName` method in a runtime callback.
79+
// deno-lint-ignore prefer-const
80+
let charmManager: CharmManager;
81+
7582
const runtime = new Runtime({
7683
storageManager: StorageManager.open({
7784
as: session.as,
@@ -97,12 +104,19 @@ export async function createCharmsController(
97104
if (!id) {
98105
throw new Error(`Could not navigate to cell that is not a charm.`);
99106
}
100-
navigateToCharm(target.space, id);
107+
108+
// NOTE(jake): Eventually, once we're doing multi-space navigation, we will
109+
// need to replace this charmManager.getSpaceName() with a call to some
110+
// sort of address book / dns-style server, OR just navigate to the DID.
111+
112+
// Use the human-readable space name from CharmManager instead of the DID
113+
navigateToCharm(charmManager.getSpaceName(), id);
101114
},
102115
});
103116

104117
console.log("[createCharmsController] Creating CharmManager with session");
105-
const charmManager = new CharmManager(session, runtime);
118+
charmManager = new CharmManager(session, runtime);
119+
106120
await charmManager.synced();
107121
console.log("[createCharmsController] Creating CharmsController");
108122
return new CharmsController(charmManager);

packages/static/assets.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ export const assets: Readonly<string[]> = [
66
"types/es2023.d.ts",
77
"types/jsx.d.ts",
88
"types/turndown.d.ts",
9+
"recipes/charm-list.tsx",
910
];

0 commit comments

Comments
 (0)