Skip to content
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
2 changes: 1 addition & 1 deletion packages/charm/src/ops/charms-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class CharmsController {
}

async create(
program: RuntimeProgram,
program: RuntimeProgram | string,
input?: object,
): Promise<CharmController> {
const recipe = await compileProgram(this.#manager, program);
Expand Down
12 changes: 4 additions & 8 deletions packages/shell/src/components/Body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,15 @@ export class XBodyElement extends BaseView {
activeCharmId?: string;

override render() {
const connected = this.cc ? "Connected" : "Not Connected";
const charmView = html`
<x-charm .cc="${this.cc}" .charmId="${this.activeCharmId}"></x-charm>
`;
const charmList = html`
<x-charm-list .cc="${this.cc}"></x-charm-list>
const spaceView = html`
<x-space .cc="${this.cc}"></x-space>
`;
const view = this.activeCharmId ? charmView : charmList;
const view = this.activeCharmId ? charmView : spaceView;
return html`
<div>
<h2>App!! (${connected})</h2>
<div>${view}</div>
</div>
<div>${view}</div>
`;
}
}
Expand Down
21 changes: 7 additions & 14 deletions packages/shell/src/components/CharmList.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { css, html } from "lit";
import { property } from "lit/decorators.js";
import { BaseView } from "../views/BaseView.ts";
import { CharmsController } from "@commontools/charm/ops";
import { Task } from "@lit/task";
import { CharmController } from "@commontools/charm/ops";
import { getNavigationHref } from "../lib/navigate.ts";

export class XCharmListElement extends BaseView {
Expand All @@ -17,26 +16,20 @@ export class XCharmListElement extends BaseView {
`;

@property({ attribute: false })
cc?: CharmsController;
charms?: CharmController[];

private _charmList = new Task(this, {
task: ([cc]) => {
return cc ? cc.getAllCharms() : undefined;
},
args: () => [this.cc],
});
@property({ attribute: false })
spaceName?: string;

override render() {
const spaceName = this.cc ? this.cc.manager().getSpaceName() : undefined;
const charmList = this._charmList.value;

if (!spaceName || !charmList) {
const { charms, spaceName } = this;
if (!spaceName || !charms) {
return html`
<x-spinner></x-spinner>
`;
}

const list = (charmList ?? []).map((charm) => {
const list = charms.map((charm) => {
const name = charm.name();
const id = charm.id;
const href = getNavigationHref(spaceName, id);
Expand Down
117 changes: 117 additions & 0 deletions packages/shell/src/components/Space.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { css, html } from "lit";
import { property, state } from "lit/decorators.js";
import { BaseView } from "../views/BaseView.ts";
import { CharmsController } from "@commontools/charm/ops";
import { Task } from "@lit/task";
import * as DefaultRecipe from "../lib/default-recipe.ts";

type CharmData = {
name: string;
id: string;
href: string;
};

export class XSpaceElement extends BaseView {
static override styles = css`
:host {
display: block;
width: 100%;
height: 100%;
background-color: white;
padding: 1rem;
}
`;

@property({ attribute: false })
cc?: CharmsController;

@state()
showCharmList = false;

@state()
creatingDefaultRecipe = false;

private _charms = new Task(this, {
task: async ([cc]) => {
if (!cc) return undefined;

// Ensure charms are synced before checking
const manager = cc.manager();
await manager.synced();
return await cc.getAllCharms();
},
args: () => [this.cc],
});

async onRequestDefaultRecipe(e: Event) {
e.preventDefault();
if (this.creatingDefaultRecipe) {
return;
}
if (!this.cc) {
throw new Error(
"Cannot create default recipe without a charms controller.",
);
}
this.creatingDefaultRecipe = true;
try {
await DefaultRecipe.create(this.cc);
} catch (e) {
console.error(`Could not create default recipe: ${e}`);
} finally {
this.creatingDefaultRecipe = false;
this._charms.run();
}
}

onViewToggle(e: Event) {
e.preventDefault();
this.showCharmList = !this.showCharmList;
}

override render() {
const spaceName = this.cc ? this.cc.manager().getSpaceName() : undefined;
const charms = this._charms.value;
const defaultRecipe = charms
? DefaultRecipe.getDefaultRecipe(charms)
: undefined;

const inner = !charms
? html`
<x-spinner></x-spinner>
`
: this.showCharmList
? html`
<x-charm-list .charms="${charms}" .spaceName="${spaceName}"></x-charm-list>
`
: !defaultRecipe
? (this.creatingDefaultRecipe
? html`
<div>
<span>Creating default recipe...</span>
<x-spinner></x-spinner>
</div>
`
: html`
<div>
<span>Create default recipe?</span>
<button @click="${this.onRequestDefaultRecipe}">Go!</button>
</div>
`)
// TBD if we want to use x-charm or ct-render directly here
: html`
<x-charm .charmId="${defaultRecipe.id}" .cc="${this.cc}"></x-charm>
`;

return html`
<div>
<button @click="${this.onViewToggle}">${this.showCharmList
? "show default"
: "show list"}</button>
${inner}
</div>
`;
}
}

globalThis.customElements.define("x-space", XSpaceElement);
1 change: 1 addition & 0 deletions packages/shell/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export * from "./Header.ts";
export * from "./Body.ts";
export * from "./Charm.ts";
export * from "./CharmList.ts";
export * from "./Space.ts";
export * from "./CTLogo.ts";
export * from "./Spinner.ts";
6 changes: 6 additions & 0 deletions packages/shell/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { XRootView } from "./views/RootView.ts";
import "./components/index.ts";
import "./views/index.ts";
import { AppController } from "./lib/app/controller.ts";
import { getNavigationHref } from "./lib/navigate.ts";

console.log(`ENVIRONMENT=${ENVIRONMENT}`);
console.log(`API_URL=${API_URL}`);
Expand Down Expand Up @@ -60,4 +61,9 @@ globalThis.addEventListener("navigate-to-charm", (e) => {
}
app.setSpace(spaceName);
app.setActiveCharmId(charmId);

// Update the browser URL to reflect the new location
// (DefaultCharmList should not use this event, it sets activeCharmId directly)
const href = getNavigationHref(spaceName, charmId);
globalThis.history.pushState({}, "", href);
});
47 changes: 47 additions & 0 deletions packages/shell/src/lib/default-recipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { CharmController, CharmsController } from "@commontools/charm/ops";
import { processSchema } from "@commontools/charm";

const ALL_CHARMS_ID =
"baedreiahv63wxwgaem4hzjkizl4qncfgvca7pj5cvdon7cukumfon3ioye";
const DEFAULT_CHARM_NAME = "DefaultCharmList";

export async function create(cc: CharmsController): Promise<CharmController> {
const manager = cc.manager();
const runtime = manager.runtime;

const recipeContent = await runtime.staticCache.getText(
"recipes/charm-list.tsx",
);
const charm = await cc.create(recipeContent);

const tx = runtime.edit();
const charmCell = charm.getCell();
const sourceCell = charmCell.getSourceCell(processSchema);

if (!sourceCell) {
// Not sure how/when this happens
throw new Error("Could not create and link default recipe.");
}

// Get the well-known allCharms cell using its EntityId format
const allCharmsCell = await manager.getCellById({ "/": ALL_CHARMS_ID });
sourceCell.withTx(tx).key("argument").key("allCharms").set(
allCharmsCell.withTx(tx),
);
await tx.commit();

// Wait for the link to be processed
await runtime.idle();
await manager.synced();

return charm;
}

export function getDefaultRecipe(
charms: CharmController[],
): CharmController | undefined {
return charms.find((c) => {
const name = c.name();
return name && name.startsWith(DEFAULT_CHARM_NAME);
});
}
20 changes: 17 additions & 3 deletions packages/shell/src/lib/runtime.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ANYONE, Identity, Session } from "@commontools/identity";
import { Runtime } from "@commontools/runner";
import { charmId, CharmManager } from "@commontools/charm";
import { charmId, CharmManager, processSchema } from "@commontools/charm";
import { CharmsController } from "@commontools/charm/ops";
import { StorageManager } from "@commontools/runner/storage/cache";
import { API_URL } from "./env.ts";
Expand Down Expand Up @@ -72,6 +72,13 @@ export async function createCharmsController(

const staticAssetUrl = new URL(API_URL);
staticAssetUrl.pathname = "/static";

// We're hoisting CharmManager so that
// we can create it after the runtime, but still reference
// its `getSpaceName` method in a runtime callback.
// deno-lint-ignore prefer-const
let charmManager: CharmManager;

const runtime = new Runtime({
storageManager: StorageManager.open({
as: session.as,
Expand All @@ -97,12 +104,19 @@ export async function createCharmsController(
if (!id) {
throw new Error(`Could not navigate to cell that is not a charm.`);
}
navigateToCharm(target.space, id);

// NOTE(jake): Eventually, once we're doing multi-space navigation, we will
// need to replace this charmManager.getSpaceName() with a call to some
// sort of address book / dns-style server, OR just navigate to the DID.

// Use the human-readable space name from CharmManager instead of the DID
navigateToCharm(charmManager.getSpaceName(), id);
},
});

console.log("[createCharmsController] Creating CharmManager with session");
const charmManager = new CharmManager(session, runtime);
charmManager = new CharmManager(session, runtime);

await charmManager.synced();
console.log("[createCharmsController] Creating CharmsController");
return new CharmsController(charmManager);
Expand Down
1 change: 1 addition & 0 deletions packages/static/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export const assets: Readonly<string[]> = [
"types/es2023.d.ts",
"types/jsx.d.ts",
"types/turndown.d.ts",
"recipes/charm-list.tsx",
];
Loading