Export template source from 1227e3672a5a24b7331567fc3f431e422e1513c1:quixos-instance/packages/todo-react-runtime
This commit is contained in:
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
declare module "*?browser-source" {
|
||||
const source: string;
|
||||
export default source;
|
||||
}
|
||||
declare module "*.css";
|
||||
@@ -0,0 +1,29 @@
|
||||
// @generated by camino-codegen-ts-runtime. Do not edit.
|
||||
import {
|
||||
createWebStudioComponent,
|
||||
type ReactComponentHostProps,
|
||||
type ObjectRef,
|
||||
} from "@quixos/web-studio-react-runtime";
|
||||
|
||||
export type TodoReactComponentRenderProps = {
|
||||
"selected"?: boolean;
|
||||
"compact"?: boolean;
|
||||
};
|
||||
|
||||
export type TodoReactComponentAction = {
|
||||
"type"?: TodoReactComponentActionType;
|
||||
};
|
||||
|
||||
export type TodoReactComponentActionType = "TODO_REACT_COMPONENT_ACTION_TYPE_UNSPECIFIED" | "SELECT";
|
||||
|
||||
export type TodoComponentProps = {
|
||||
forObject: ObjectRef<"atom:task">;
|
||||
} & TodoReactComponentRenderProps & ReactComponentHostProps<TodoReactComponentAction>;
|
||||
|
||||
export const TodoComponent = createWebStudioComponent<
|
||||
ObjectRef<"atom:task">,
|
||||
TodoReactComponentRenderProps,
|
||||
TodoReactComponentAction
|
||||
>({
|
||||
expectedAtomId: "atom:task",
|
||||
});
|
||||
+835
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+66
@@ -0,0 +1,66 @@
|
||||
// Generated by the Quixos React platform contract. Do not edit.
|
||||
declare module "@quixos/web-studio-react-runtime" {
|
||||
import type * as React from "react";
|
||||
export function useComponentOverlayContainer(): HTMLElement;
|
||||
export function useComponentStyleRoot(): ShadowRoot;
|
||||
/** Aggregate transport counters only; contains no object identities or values. */
|
||||
export function getFieldTransportMetrics(): Partial<Record<"get" | "watch" | "register" | "crdt" | "setter", Readonly<{started: number; active: number; completed: number; failed: number; totalMs: number}>>>;
|
||||
export type ObjectRef<AtomId extends string> = string & {
|
||||
readonly $quixosAtom: AtomId;
|
||||
};
|
||||
export type FieldCapability = {objectId: string; interfaceRevisionId: string; getOperationId: string; watchOperationId?: string; setOperationId?: string; inputKind?: "fields" | "value"};
|
||||
export type ReadableField<T> = {readonly value?: T; readonly capability: FieldCapability};
|
||||
/** set(T) uses the resolved capability: native CRDT fields send incremental edits;
|
||||
* custom/manual setters receive semantic values. Storage provenance grants no authority. */
|
||||
export type WritableField<T> = ReadableField<T> & {readonly $writeType?: (value: T) => T; readonly writable: true; readonly capability: FieldCapability & {setOperationId: string}};
|
||||
export type LiveFieldProp<T> = ReadableField<T>;
|
||||
export type InterfaceReference<I extends string, Fields> = {readonly $quixosRef: string; readonly interfaceRevisionId: I; readonly fields: Fields};
|
||||
export type ReactInterfaceContract<View> = {readonly interfaceRevisionId: string; readonly $viewType?: (view: View) => View};
|
||||
export function defineReactInterfaceContract<View>(interfaceRevisionId: string, interfaces: unknown): ReactInterfaceContract<View>;
|
||||
export function tryConform<View>(object: string | {readonly $quixosRef: string}, contract: ReactInterfaceContract<View>, options?: {signal?: AbortSignal}): Promise<View | undefined>;
|
||||
export type ConformanceResult<View> = {status: "loading"} | {status: "absent"} | {status: "available"; view: View} | {status: "error"; error: Error};
|
||||
export function useTryConform<View>(object: string | {readonly $quixosRef: string}, contract: ReactInterfaceContract<View>): ConformanceResult<View>;
|
||||
export type QueryValueType = {kind: "builtin" | "scalar"; name: string} | {kind: "record"; fields: Record<string, QueryValueType>} | {kind: "optional" | "list"; value: QueryValueType} | {kind: "object-ref"; expectation: {kind: "atom"; atomId: string} | {kind: "interface"; interfaceRevisionId: string}};
|
||||
export type QueryReference<Contract extends string = string> = {readonly $quixosRef: string; readonly queryContract: Contract};
|
||||
export type QueryLiveProjection = {path: readonly string[]; selectionId: string; interfaceRevisionId: string; getOperationId: string; setOperationId?: string; watchOperationId?: string; inputKind: "fields" | "value"; valueType: QueryValueType; conditions: {include: boolean; defaultValue?: boolean; value: {kind: "variable"; name: string} | {kind: "literal"; value: unknown}}[]};
|
||||
export interface QueryDescriptor<Variables, Result> {readonly id: string; readonly definitionDigest: string; readonly rootInterfaceRevisionId: string; readonly variables: QueryValueType; readonly output: QueryValueType; readonly watch: boolean; readonly liveFields?: readonly QueryLiveProjection[]; readonly $types?: (variables: Variables, result: Result) => [Variables, Result]}
|
||||
export type QueryPartial<T> = T extends {readonly $quixosRef: string} | {readonly capability: {readonly getOperationId: string}} ? T : T extends readonly (infer Item)[] ? QueryPartial<Item>[] : T extends object ? {[Key in keyof T]?: QueryPartial<T[Key]>} : T;
|
||||
export type QueryFieldState = {path: readonly (string | number)[]; status: "pending" | "error"; error?: string};
|
||||
export type QueryState<T> = {refreshing: boolean; fields: QueryFieldState[]; runId?: string; sequence?: bigint; consistency?: string} & ({status: "loading"; data?: undefined; error?: undefined} | {status: "ready"; data: T; error?: undefined} | {status: "partial"; data: QueryPartial<T>; error?: undefined} | {status: "error"; data?: T | QueryPartial<T>; error: Error});
|
||||
export function useQuery<Variables, Result>(descriptor: QueryDescriptor<Variables, Result>, options: {root: string | {readonly $quixosRef: string}; variables: Variables}): QueryState<Result> & {refresh(): void};
|
||||
export interface QueryConnection<Row> {entries: {key: string; node: Row; cursor?: string | null}[]; pageInfo: {hasNextPage: boolean; endCursor?: string | null}}
|
||||
export function CollectionView<Result, Row>(props: {result: QueryState<Result> & {refresh(): void}; connection(data: QueryPartial<Result>): QueryPartial<QueryConnection<Row>> | undefined; renderItem(row: QueryPartial<Row>, entryKey: string): React.ReactNode; onCreate?: () => void; onLoadMore?: (cursor: string | null) => void; onReset?: () => void; loadingMore?: boolean; empty?: React.ReactNode; prefetch?: boolean}): React.ReactElement;
|
||||
export type ReactComponentHostProps<Action> = {
|
||||
onAction?: (action: Action) => void;
|
||||
fallback?: React.ReactNode;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
/** Optional extra action. The host still displays and reports errors by default. */
|
||||
extraErrorAction?: (error: Error) => void;
|
||||
};
|
||||
export type ReactComponentImplementationProps<CaminoProps, RenderProps, Action> = {
|
||||
camino: CaminoProps;
|
||||
render: RenderProps;
|
||||
dispatch: (action: Action) => void;
|
||||
};
|
||||
export const createWebStudioComponent: <
|
||||
ForObject extends ObjectRef<string>, RenderProps extends object, Action,
|
||||
>(config: { expectedAtomId: string }) => (props: {
|
||||
forObject: ForObject;
|
||||
} & RenderProps & ReactComponentHostProps<Action>) => any;
|
||||
export const invokeCapability: <Result = unknown>(
|
||||
objectId: string,
|
||||
interfaceRevisionId: string,
|
||||
operationId: string,
|
||||
value?: unknown,
|
||||
options?: {clientMutationId?: string; signal?: AbortSignal},
|
||||
) => Promise<Result>;
|
||||
export const h: typeof React.createElement;
|
||||
export type LiveFieldState<T> = {refreshing: boolean; stale: boolean; pendingWrites: number; writeError?: Error; refresh(): Promise<void>; discard(): void} & ({status: "loading"; value?: undefined; error?: undefined} | {status: "ready"; value: T; error?: Error} | {status: "error"; value?: undefined; error: Error});
|
||||
export type WritableLiveFieldState<T> = LiveFieldState<T> & {set(value: T): Promise<void>; retry(): Promise<void>};
|
||||
export function useLiveField<T>(field: ReadableField<T>, options: {id: string; write: (value: T) => Promise<void>}): WritableLiveFieldState<T>;
|
||||
export function useLiveField<T>(field: WritableField<T>): WritableLiveFieldState<T>;
|
||||
export function useLiveField<T>(field: ReadableField<T>): LiveFieldState<T>;
|
||||
export function useSuspenseLiveField<T>(field: WritableField<T>): WritableLiveFieldState<T> & {status: "ready"; value: T};
|
||||
export function useSuspenseLiveField<T>(field: ReadableField<T>): LiveFieldState<T> & {status: "ready"; value: T};
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// This adapter keeps the implementation file hand-authored.
|
||||
export { createComponent as handler } from "../todo-react.impl.js";
|
||||
@@ -0,0 +1,2 @@
|
||||
// This adapter keeps the implementation file hand-authored.
|
||||
export { props as handler } from "../todo-react.impl.js";
|
||||
@@ -0,0 +1,2 @@
|
||||
// This adapter keeps the implementation file hand-authored.
|
||||
export { props as handler } from "../todo-react.impl.js";
|
||||
@@ -0,0 +1,2 @@
|
||||
// This adapter keeps the implementation file hand-authored.
|
||||
export { stopWatch as handler } from "../todo-react.impl.js";
|
||||
@@ -0,0 +1,2 @@
|
||||
// This adapter keeps the implementation file hand-authored.
|
||||
export { source as handler } from "../todo-react.impl.js";
|
||||
@@ -0,0 +1,3 @@
|
||||
// Generated by qx-scaffold-v1
|
||||
import { serveMigration } from "@quixos/camino-package-runtime";
|
||||
await serveMigration({});
|
||||
@@ -0,0 +1,17 @@
|
||||
// Generated by qx-scaffold-v1
|
||||
import { servePackageRuntime } from "@quixos/camino-package-runtime";
|
||||
import { createRuntime } from "./gen/qx.js";
|
||||
import { handler as impl0 } from "./impl/createComponent.js";
|
||||
import { handler as impl1 } from "./impl/sourceGet.js";
|
||||
import { handler as impl2 } from "./impl/propsGet.js";
|
||||
import { handler as impl3 } from "./impl/propsWatch.js";
|
||||
import { handler as impl4 } from "./impl/propsWatchStop.js";
|
||||
servePackageRuntime(
|
||||
createRuntime({
|
||||
createComponent: impl0,
|
||||
sourceGet: impl1,
|
||||
propsGet: impl2,
|
||||
propsWatch: impl3,
|
||||
propsWatchStop: impl4,
|
||||
}),
|
||||
);
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
.task-editor {
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
color: #27372d;
|
||||
font:
|
||||
14px/1.5 system-ui,
|
||||
sans-serif;
|
||||
}
|
||||
.task-editor * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.task-editor label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: #63715f;
|
||||
}
|
||||
.task-editor input,
|
||||
.task-editor select,
|
||||
.task-editor textarea,
|
||||
.task-editor button {
|
||||
font: inherit;
|
||||
color: #27372d;
|
||||
border: 1px solid #d4dccf;
|
||||
border-radius: 6px;
|
||||
padding: 8px 10px;
|
||||
background: #fff;
|
||||
min-width: 0;
|
||||
}
|
||||
.task-editor input,
|
||||
.task-editor select,
|
||||
.task-editor textarea {
|
||||
width: 100%;
|
||||
}
|
||||
.task-editor textarea {
|
||||
resize: vertical;
|
||||
min-height: 120px;
|
||||
}
|
||||
.task-title input {
|
||||
font-size: 21px;
|
||||
font-weight: 650;
|
||||
}
|
||||
.task-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin: 12px 0;
|
||||
}
|
||||
.task-actions span,
|
||||
.task-hint {
|
||||
color: #6c7867;
|
||||
font-size: 12px;
|
||||
}
|
||||
.task-editor button {
|
||||
cursor: pointer;
|
||||
}
|
||||
.task-editor button:hover,
|
||||
.task-editor button[aria-pressed="true"] {
|
||||
background: #e9f1e1;
|
||||
}
|
||||
.task-editor button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: wait;
|
||||
}
|
||||
.task-editor :focus-visible {
|
||||
outline: 2px solid #42765a;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.task-fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.task-editor h3 {
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
margin: 20px 0 8px;
|
||||
}
|
||||
.task-tags {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.task-tags button {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.task-tags i {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.task-history {
|
||||
border-top: 1px solid #e3e8de;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.task-history ol {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
.task-history li {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #edf0e8;
|
||||
}
|
||||
.task-history li span {
|
||||
font-size: 12px;
|
||||
color: #6c7867;
|
||||
}
|
||||
.task-error {
|
||||
background: #fff1ec;
|
||||
color: #923c2c;
|
||||
padding: 10px;
|
||||
margin-bottom: 12px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
tryConform,
|
||||
useLiveField,
|
||||
useQuery,
|
||||
type ObjectRef,
|
||||
type ReactComponentImplementationProps,
|
||||
} from "@quixos/web-studio-react-runtime";
|
||||
import { queries, reactInterfaces, type ReactResults, type QueryResults } from "./gen/react-props.gen.js";
|
||||
import "./task.css";
|
||||
|
||||
type Props = ReactComponentImplementationProps<
|
||||
ReactResults["propsGet"],
|
||||
{ tagChoices?: { id: string; name: string; color: string }[] },
|
||||
never
|
||||
>;
|
||||
|
||||
export default function TodoComponent({ camino: props, render }: Props) {
|
||||
const [after, setAfter] = useState<string | null>(null);
|
||||
const history = useQuery(queries.History, { root: props.data.$quixosRef, variables: { first: 50, after } });
|
||||
if (history.status === "loading") return <p>Loading task…</p>;
|
||||
if (history.status !== "ready")
|
||||
return (
|
||||
<div role="alert">
|
||||
{history.status === "error" ? history.error.message : "Loading task fields…"}
|
||||
<button onClick={history.refresh}>Retry</button>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<Editor
|
||||
camino={props}
|
||||
render={render}
|
||||
history={history}
|
||||
data={history.data.root}
|
||||
after={after}
|
||||
setAfter={setAfter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
function Editor({
|
||||
camino: props,
|
||||
render,
|
||||
history,
|
||||
data,
|
||||
after,
|
||||
setAfter,
|
||||
}: Pick<Props, "camino" | "render"> & {
|
||||
after: string | null;
|
||||
history: { status: "ready"; refresh(): void };
|
||||
data: QueryResults["History"]["root"];
|
||||
setAfter(value: string | null): void;
|
||||
}) {
|
||||
const titleField = useLiveField(data.title);
|
||||
const statusField = useLiveField(data.status);
|
||||
const priorityField = useLiveField(data.priority);
|
||||
const notesField = useLiveField(data.notes);
|
||||
const dueDateField = useLiveField(data.dueDate);
|
||||
const energyField = useLiveField(data.energy);
|
||||
const estimatedHoursField = useLiveField(data.estimatedHours);
|
||||
const repeatDaysField = useLiveField(data.repeatDays);
|
||||
const title = titleField.value ?? "";
|
||||
const setTitle = titleField.set;
|
||||
const status = statusField.value ?? "";
|
||||
const priority = priorityField.value ?? "";
|
||||
const setPriority = priorityField.set;
|
||||
const notes = notesField.value ?? "";
|
||||
const setNotes = notesField.set;
|
||||
const dueDate = dueDateField.value ?? "";
|
||||
const setDueDate = dueDateField.set;
|
||||
const energy = energyField.value ?? "";
|
||||
const setEnergy = energyField.set;
|
||||
const hours = estimatedHoursField.value ?? 0;
|
||||
const setHours = estimatedHoursField.set;
|
||||
const repeatDays = repeatDaysField.value ?? 0;
|
||||
const setRepeatDays = repeatDaysField.set;
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
// Live setters retain the platform's optimistic/CRDT behavior; do not drop keystrokes while an action is pending.
|
||||
const save = (request: Promise<unknown>) => {
|
||||
void request.catch((e) => setError(e instanceof Error ? e.message : String(e)));
|
||||
};
|
||||
const run = async (action: () => Promise<unknown>) => {
|
||||
if (busy) return;
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
await action();
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
const action = (name: "complete.call" | "reopen.call" | "archive.call") =>
|
||||
run(async () => {
|
||||
const view = await tryConform(props.data.$quixosRef, reactInterfaces.TaskActions);
|
||||
if (!view) throw new Error("Task actions are unavailable");
|
||||
await view.call[name]();
|
||||
setAfter(null);
|
||||
});
|
||||
const toggleTag = (id: string, connected: boolean) =>
|
||||
run(async () => {
|
||||
const view = await tryConform(props.data.$quixosRef, reactInterfaces.TaskData);
|
||||
if (!view) throw new Error("Task is unavailable");
|
||||
// These IDs originate in atom-Tag relations (the project catalog or this task), not user-entered strings.
|
||||
await view.call[connected ? "tags.disconnect" : "tags.connect"](id as ObjectRef<"atom:tag">);
|
||||
});
|
||||
const assigned =
|
||||
data?.tags.entries.map((e) => ({ id: e.node._qx.ref.$quixosRef, name: e.node.name, color: e.node.color })) ?? [];
|
||||
const choices = [...assigned, ...(render.tagChoices ?? []).filter((t) => !assigned.some((a) => a.id === t.id))];
|
||||
return (
|
||||
<article className="task-editor">
|
||||
{error && (
|
||||
<div role="alert" className="task-error">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<label className="task-title">
|
||||
Task title
|
||||
<input value={title} onChange={(e) => save(setTitle(e.target.value))} />
|
||||
</label>
|
||||
<div className="task-actions">
|
||||
{status === "OPEN" ? (
|
||||
<button
|
||||
disabled={busy}
|
||||
onClick={() => {
|
||||
void action("complete.call");
|
||||
}}
|
||||
>
|
||||
{repeatDays > 0 ? "Record completion" : "Complete task"}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
disabled={busy}
|
||||
onClick={() => {
|
||||
void action("reopen.call");
|
||||
}}
|
||||
>
|
||||
Reopen task
|
||||
</button>
|
||||
)}
|
||||
{status !== "ARCHIVED" && (
|
||||
<button
|
||||
disabled={busy}
|
||||
onClick={() => {
|
||||
void action("archive.call");
|
||||
}}
|
||||
>
|
||||
Archive
|
||||
</button>
|
||||
)}
|
||||
<span>{status.toLowerCase()}</span>
|
||||
</div>
|
||||
<div className="task-fields">
|
||||
<label>
|
||||
Priority
|
||||
<select value={priority} onChange={(e) => save(setPriority(e.target.value))}>
|
||||
<option>LOW</option>
|
||||
<option>NORMAL</option>
|
||||
<option>HIGH</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Energy
|
||||
<select value={energy} onChange={(e) => save(setEnergy(e.target.value))}>
|
||||
<option>LOW</option>
|
||||
<option>MEDIUM</option>
|
||||
<option>HIGH</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Due date
|
||||
<input type="date" value={dueDate} onChange={(e) => save(setDueDate(e.target.value))} />
|
||||
</label>
|
||||
<label>
|
||||
Estimated hours
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.25"
|
||||
value={hours}
|
||||
onChange={(e) => {
|
||||
const n = Number(e.target.value);
|
||||
if (Number.isFinite(n) && n >= 0) save(setHours(n));
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Repeat after (days)
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="36500"
|
||||
step="1"
|
||||
value={repeatDays}
|
||||
onChange={(e) => {
|
||||
const n = Number(e.target.value);
|
||||
if (Number.isInteger(n) && n >= 0 && n <= 36500) save(setRepeatDays(n));
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<p className="task-hint">
|
||||
{repeatDays > 0
|
||||
? `After completion, stay open and set the next due date ${repeatDays} days later (UTC).`
|
||||
: "One-off task. Set repeat days above zero to repeat after completion."}
|
||||
</p>
|
||||
<section>
|
||||
<h3>Tags</h3>
|
||||
<div className="task-tags">
|
||||
{choices.map((t) => {
|
||||
const connected = assigned.some((a) => a.id === t.id);
|
||||
return (
|
||||
<button
|
||||
key={t.id}
|
||||
disabled={busy || !data}
|
||||
aria-pressed={connected}
|
||||
onClick={() => {
|
||||
void toggleTag(t.id, connected);
|
||||
}}
|
||||
>
|
||||
<i style={{ background: t.color }} />
|
||||
{t.name}
|
||||
{connected ? " ✓" : ""}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{choices.length === 0 && <p className="task-hint">Create and assign tags from the project's tag catalog.</p>}
|
||||
</section>
|
||||
<label>
|
||||
Notes
|
||||
<textarea value={notes} rows={8} onChange={(e) => save(setNotes(e.target.value))} />
|
||||
</label>
|
||||
<section className="task-history">
|
||||
<h3>Completion history{data ? ` · ${String(data._qx.relations.completions.aggregate.count)}` : ""}</h3>
|
||||
{data?.completions.entries.length === 0 && <p className="task-hint">No completions yet.</p>}
|
||||
<ol>
|
||||
{data?.completions.entries.map(({ key, node }) => (
|
||||
<li key={key}>
|
||||
<time dateTime={new Date(node.completedAt).toISOString()}>
|
||||
{new Date(node.completedAt).toLocaleString()}
|
||||
</time>
|
||||
<span>{node.scheduledDate ? `Scheduled for ${node.scheduledDate}` : "No scheduled date"}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<div className="task-actions">
|
||||
{after && <button onClick={() => setAfter(null)}>Latest</button>}
|
||||
{data?.completions.pageInfo.hasNextPage && (
|
||||
<button onClick={() => setAfter(data.completions.pageInfo.endCursor)}>Older completions →</button>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import componentSource from "./todo-component.js?browser-source";
|
||||
import type { Contexts, Results } from "./gen/qx.js";
|
||||
|
||||
export const source = () => componentSource;
|
||||
export const createComponent = ({ objectId }: Contexts["createComponent"]) => objectId;
|
||||
export const props = {
|
||||
kind: "derived" as const,
|
||||
get: async ({ ports }: Contexts["propsGet"]): Promise<Results["propsGet"]> => ({ data: ports.data.objectId }),
|
||||
};
|
||||
export const stopWatch = () => null;
|
||||
Reference in New Issue
Block a user