Add schema-driven project task creation
This commit is contained in:
@@ -105,6 +105,40 @@ const requireRuntime = () => {
|
||||
return runtimeConfig;
|
||||
};
|
||||
|
||||
export const callObjectMethod = async <Result = unknown>(
|
||||
object: ObjectRef<string>,
|
||||
methodName: string,
|
||||
input: Record<string, unknown> = {},
|
||||
): Promise<Result> => {
|
||||
const runtime = requireRuntime();
|
||||
const [objectResponse, classesResponse] = await Promise.all([
|
||||
runtime.camino.getObject({ objectId: object }),
|
||||
runtime.camino.listClasses({}),
|
||||
]);
|
||||
const classId = objectResponse.object?.classId;
|
||||
if (!classId) {
|
||||
throw new Error(`Unknown Camino object ${object}`);
|
||||
}
|
||||
const schema = classesResponse.classes.find(
|
||||
(entry) => entry.schema?.id && symbolKey(entry.schema.id) === classId,
|
||||
)?.schema;
|
||||
const method = schema?.methods.find((entry) => entry.name === methodName);
|
||||
if (!method?.function) {
|
||||
throw new Error(`${classId} has no method ${methodName}`);
|
||||
}
|
||||
const response = await runtime.orch.invokeFunction({
|
||||
function: functionToRequest(method.function),
|
||||
objectId: object,
|
||||
input: Object.fromEntries(
|
||||
Object.entries(input).map(([name, value]) => [name, jsToProtoValue(value)]),
|
||||
),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(response.error || `${classId}.${methodName} failed`);
|
||||
}
|
||||
return valueToJs(response.result) as Result;
|
||||
};
|
||||
|
||||
const bytesToBase64 = (value: Uint8Array) => {
|
||||
let binary = "";
|
||||
for (const byte of value) {
|
||||
|
||||
@@ -612,6 +612,12 @@ const reactRuntimeDeclaration = `${generatedWarning}declare module "@quixos/cami
|
||||
forObject: ForObject;
|
||||
} & RenderProps & ReactComponentHostProps<Action>) => any;
|
||||
|
||||
export const callObjectMethod: <Result = unknown>(
|
||||
object: ObjectRef<string>,
|
||||
methodName: string,
|
||||
input?: Record<string, unknown>,
|
||||
) => Promise<Result>;
|
||||
|
||||
export const h: (...args: any[]) => any;
|
||||
export const useLiveField: <T>(
|
||||
field: LiveFieldProp<T>,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
callObjectMethod,
|
||||
h,
|
||||
type ObjectRef,
|
||||
type ReactComponentImplementationProps,
|
||||
@@ -30,11 +31,31 @@ export default function ProjectComponent({
|
||||
render,
|
||||
}: ProjectComponentProps) {
|
||||
const [selectedTask, setSelectedTask] = useState<TaskRef | null>(null);
|
||||
const [newTaskTitle, setNewTaskTitle] = useState("");
|
||||
const [addingTask, setAddingTask] = useState(false);
|
||||
const [addTaskError, setAddTaskError] = useState("");
|
||||
const handleTaskAction = (task: TaskRef, action: TodoReactComponentAction) => {
|
||||
if (action.type === "SELECT") {
|
||||
setSelectedTask(task);
|
||||
}
|
||||
};
|
||||
const addTask = async () => {
|
||||
if (addingTask) {
|
||||
return;
|
||||
}
|
||||
setAddingTask(true);
|
||||
setAddTaskError("");
|
||||
try {
|
||||
await callObjectMethod(camino.project_id, "addTask", {
|
||||
title: newTaskTitle,
|
||||
});
|
||||
setNewTaskTitle("");
|
||||
} catch (error) {
|
||||
setAddTaskError(error instanceof Error ? error.message : String(error));
|
||||
} finally {
|
||||
setAddingTask(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
@@ -51,6 +72,51 @@ export default function ProjectComponent({
|
||||
{camino.tasks.length} tasks
|
||||
</span>
|
||||
</header>
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
void addTask();
|
||||
}}
|
||||
style={{ display: "grid", gridTemplateColumns: "1fr auto", gap: "8px" }}
|
||||
>
|
||||
<input
|
||||
aria-label="New task title"
|
||||
value={newTaskTitle}
|
||||
onInput={(event) => setNewTaskTitle(event.currentTarget.value)}
|
||||
placeholder="New task"
|
||||
disabled={addingTask}
|
||||
style={{
|
||||
minWidth: 0,
|
||||
border: "1px solid #d4d4d8",
|
||||
borderRadius: "6px",
|
||||
fontSize: "14px",
|
||||
padding: "8px 10px",
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={addingTask || newTaskTitle.trim().length === 0}
|
||||
style={{
|
||||
border: "1px solid #18181b",
|
||||
borderRadius: "6px",
|
||||
background: "#18181b",
|
||||
color: "#ffffff",
|
||||
fontSize: "13px",
|
||||
fontWeight: 600,
|
||||
padding: "8px 12px",
|
||||
}}
|
||||
>
|
||||
{addingTask ? "Adding" : "Add task"}
|
||||
</button>
|
||||
{addTaskError ? (
|
||||
<div
|
||||
role="alert"
|
||||
style={{ gridColumn: "1 / -1", color: "#b91c1c", fontSize: "12px" }}
|
||||
>
|
||||
{addTaskError}
|
||||
</div>
|
||||
) : null}
|
||||
</form>
|
||||
<div style={{ display: "grid", gap: "10px" }}>
|
||||
{camino.tasks.map((task) => (
|
||||
<TodoComponent
|
||||
|
||||
@@ -7,6 +7,19 @@ source_repo: "path:."
|
||||
server_installable: "#server"
|
||||
runtime_protocol_version: "camino-orch-v0"
|
||||
|
||||
function_exports: {
|
||||
function: {
|
||||
package_namespace: "quixos.todo"
|
||||
package_name: "project-runtime"
|
||||
symbol: "addTask"
|
||||
}
|
||||
input_type: "quixos.todo.ProjectAddTaskInput"
|
||||
output_type: "quixos.todo.Task"
|
||||
interface_version: 1
|
||||
interface_compatible_back_to: 1
|
||||
capability_kind: METHOD
|
||||
}
|
||||
|
||||
function_exports: {
|
||||
function: {
|
||||
package_namespace: "quixos.todo"
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { derivedWithDeps, protoValueToJs } from "@quixos/camino-package-runtime";
|
||||
import {
|
||||
derivedWithDeps,
|
||||
jsToProtoValue,
|
||||
protoValueToJs,
|
||||
} from "@quixos/camino-package-runtime";
|
||||
import type { ProjectContext } from "./gen/quixos.todo.Project.runtime.js";
|
||||
|
||||
type TaskSummaryItem = {
|
||||
@@ -7,6 +11,31 @@ type TaskSummaryItem = {
|
||||
priority: string;
|
||||
};
|
||||
|
||||
export const addTask = async (context: ProjectContext) => {
|
||||
const requestedTitle = protoValueToJs(context.request.input.title);
|
||||
const title =
|
||||
typeof requestedTitle === "string" && requestedTitle.trim().length > 0
|
||||
? requestedTitle.trim()
|
||||
: "Untitled task";
|
||||
const created = await context.camino.createObject({
|
||||
classId: "quixos.todo:Task:1:",
|
||||
fields: {
|
||||
title: jsToProtoValue(title),
|
||||
status: jsToProtoValue("OPEN"),
|
||||
priority: jsToProtoValue("NORMAL"),
|
||||
},
|
||||
});
|
||||
if (!created.object) {
|
||||
throw new Error("Camino did not return the created Task");
|
||||
}
|
||||
await context.camino.addEdge({
|
||||
fromObjectId: context.object.id,
|
||||
sourceField: "tasks",
|
||||
toObjectId: created.object.id,
|
||||
});
|
||||
return { objectId: created.object.id };
|
||||
};
|
||||
|
||||
const stringField = (
|
||||
fields: Record<string, unknown>,
|
||||
fieldName: string,
|
||||
|
||||
@@ -20,6 +20,15 @@ message Project {
|
||||
}
|
||||
};
|
||||
|
||||
option (camino.method) = {
|
||||
name: "addTask"
|
||||
function: {
|
||||
package_namespace: "quixos.todo"
|
||||
package_name: "project-runtime"
|
||||
symbol: "addTask"
|
||||
}
|
||||
};
|
||||
|
||||
string name = 1 [
|
||||
(camino.display_label) = true,
|
||||
(camino.conflict) = PRESERVE_CONFLICTS
|
||||
|
||||
Reference in New Issue
Block a user