Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9523cfb79b | ||
|
|
cf33959ceb |
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"monorepoCommit": "4c88728f144add3457b4273d4b9a04ee53c53be3",
|
||||
"monorepoCommit": "99b2868b7e974441744657b35049f5baae4a499b",
|
||||
"sourcePath": "quixos-instance/packages/todo-react-runtime"
|
||||
}
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@quixos/camino-package-runtime": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-package-runtime.git#commit=446fd4e36e9700cf27ce20b472a8d512133e9370"
|
||||
"@quixos/camino-package-runtime": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-package-runtime.git#commit=cd2b04dc8c2daeea633bf8ecdd31d20fdc41f2ab"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24",
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
import interface TagData;
|
||||
import interface CompletionData;
|
||||
import interface TaskActions;
|
||||
external atom Tag id "atom:tag";
|
||||
external atom Completion id "atom:completion";
|
||||
import interface TaskData;
|
||||
external atom TaskReactComponent id "atom:task-react-component";
|
||||
external atom Task id "atom:task";
|
||||
@@ -18,8 +13,4 @@ package TodoReactRuntime id "package:todo-react-runtime" revision "package:todo-
|
||||
interface data id "port:todo-react-runtime:data" : TaskData;
|
||||
};
|
||||
operation propsWatchStop id "export:todo-react-runtime:props-watch-stop" : watch-handle -> unit mode watch-stop receiver atom TaskReactComponent;
|
||||
query History id "query:History" root TaskData document "queries/History.graphql" operation "History" {
|
||||
view Tag as TagData; view Completion as CompletionData;
|
||||
max rows 100; max depth 8; max result-bytes 1048576; watch;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
query History($first: Int!, $after: Cursor) {root {
|
||||
tags(all: 100) {entries {node {_qx {ref} name color}}}
|
||||
completions(first: $first, after: $after, orderBy: [{completedAt: DESC}]) {entries {key node {completedAt scheduledDate}} pageInfo {hasNextPage endCursor}}
|
||||
_qx {relations {completions {aggregate {count}}}}
|
||||
}}
|
||||
+2
-14
@@ -1,22 +1,10 @@
|
||||
quixos-lock version 1 {
|
||||
quixos source {
|
||||
repository "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos.git";
|
||||
commit "4c88728f144add3457b4273d4b9a04ee53c53be3";
|
||||
commit "99b2868b7e974441744657b35049f5baae4a499b";
|
||||
}
|
||||
interface TaskData source {
|
||||
repository "https://repos.quixos.org/quixos-templates/interface-task-data.git";
|
||||
commit "73d0b65861ab7d4194e431a6a38e28a288687fe9";
|
||||
}
|
||||
interface TagData source {
|
||||
repository "https://repos.quixos.org/quixos-templates/interface-tag-data.git";
|
||||
commit "944d1186ff82c2b00fe0db4fd839bbbfc66b0a72";
|
||||
}
|
||||
interface CompletionData source {
|
||||
repository "https://repos.quixos.org/quixos-templates/interface-completion-data.git";
|
||||
commit "6af9cc41f203378dce92f14b716708ba053d30fc";
|
||||
}
|
||||
interface TaskActions source {
|
||||
repository "https://repos.quixos.org/quixos-templates/interface-task-actions.git";
|
||||
commit "75c1f0cc6d201d34adf5d75695f7bab687157500";
|
||||
commit "ad2eac936b1d7f30eed9c0a74d1c220ec073b625";
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
-1
@@ -2,4 +2,3 @@ declare module "*?browser-source" {
|
||||
const source: string;
|
||||
export default source;
|
||||
}
|
||||
declare module "*.css";
|
||||
|
||||
+2
-424
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-14
@@ -13,20 +13,6 @@ declare module "@quixos/web-studio-react-runtime" {
|
||||
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 interface QueryDescriptor<Variables, Result> {readonly id: string; readonly definitionDigest: string; readonly rootInterfaceRevisionId: string; readonly variables: QueryValueType; readonly output: QueryValueType; readonly watch: boolean; readonly $types?: (variables: Variables, result: Result) => [Variables, Result]}
|
||||
export type QueryPartial<T> = T extends QueryReference ? 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;
|
||||
|
||||
-127
@@ -1,127 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
+203
-204
@@ -1,218 +1,217 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
tryConform,
|
||||
useLiveField,
|
||||
useQuery,
|
||||
type ObjectRef,
|
||||
type ReactComponentImplementationProps,
|
||||
} from "@quixos/web-studio-react-runtime";
|
||||
import { queries, reactInterfaces, type ReactResults } from "./gen/react-props.gen.js";
|
||||
import "./task.css";
|
||||
import { useLiveField, type ReactComponentImplementationProps } from "@quixos/web-studio-react-runtime";
|
||||
import type {
|
||||
TodoReactComponentAction,
|
||||
TodoReactComponentRenderProps,
|
||||
} from "./gen/quixos.todo.TodoReactComponent.react.js";
|
||||
import type { ReactResults } from "./gen/react-props.gen.js";
|
||||
|
||||
type Props = ReactComponentImplementationProps<
|
||||
ReactResults["propsGet"],
|
||||
{ tagChoices?: { id: string; name: string; color: string }[] },
|
||||
never
|
||||
type TodoReactComponentCaminoProps = ReactResults["propsGet"];
|
||||
|
||||
const palette = {
|
||||
ink: "#28362d",
|
||||
muted: "#6f776e",
|
||||
cream: "#fffdf5",
|
||||
line: "#d9d0ba",
|
||||
moss: "#5f7f63",
|
||||
honey: "#d99b3d",
|
||||
berry: "#a9544f",
|
||||
};
|
||||
|
||||
const statusTone = (status: string) =>
|
||||
status === "DONE" ? palette.moss : status === "ARCHIVED" ? palette.muted : "#52749a";
|
||||
|
||||
const fieldLabelStyle = {
|
||||
display: "grid",
|
||||
gap: "4px",
|
||||
color: palette.muted,
|
||||
fontSize: "10px",
|
||||
fontWeight: 750,
|
||||
letterSpacing: "0.08em",
|
||||
textTransform: "uppercase" as const,
|
||||
};
|
||||
|
||||
const controlStyle = {
|
||||
minWidth: 0,
|
||||
width: "100%",
|
||||
border: `1px solid ${palette.line}`,
|
||||
borderRadius: "7px",
|
||||
background: "#fff",
|
||||
color: palette.ink,
|
||||
fontSize: "11px",
|
||||
padding: "6px 8px",
|
||||
};
|
||||
|
||||
type TodoComponentImplementationProps = ReactComponentImplementationProps<
|
||||
TodoReactComponentCaminoProps,
|
||||
TodoReactComponentRenderProps,
|
||||
TodoReactComponentAction
|
||||
>;
|
||||
|
||||
export default function TodoComponent({ camino: props, render }: Props) {
|
||||
export default function TodoComponent({ camino: props, render, dispatch }: TodoComponentImplementationProps) {
|
||||
const [title, setTitle] = useLiveField(props.data.fields.title);
|
||||
const [status] = useLiveField(props.data.fields.status);
|
||||
const [status, setStatus] = useLiveField(props.data.fields.status);
|
||||
const [priority, setPriority] = useLiveField(props.data.fields.priority);
|
||||
const [notes, setNotes] = useLiveField(props.data.fields.notes);
|
||||
const [dueDate, setDueDate] = useLiveField(props.data.fields.dueDate);
|
||||
const [energy, setEnergy] = useLiveField(props.data.fields.energy);
|
||||
const [hours, setHours] = useLiveField(props.data.fields.estimatedHours);
|
||||
const [repeatDays, setRepeatDays] = useLiveField(props.data.fields.repeatDays);
|
||||
const [after, setAfter] = useState<string | null>(null);
|
||||
const history = useQuery(queries.History, { root: props.data.$quixosRef, variables: { first: 50, after } });
|
||||
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 data = history.status === "ready" ? history.data.root : undefined;
|
||||
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))];
|
||||
const done = status === "DONE";
|
||||
|
||||
return (
|
||||
<article className="task-editor">
|
||||
{error && (
|
||||
<div role="alert" className="task-error">
|
||||
{error}
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gap: render.compact ? "9px" : "13px",
|
||||
minWidth: 0,
|
||||
height: "100%",
|
||||
padding: render.compact ? "11px" : "15px",
|
||||
border: render.selected ? `2px solid ${palette.honey}` : `1px solid ${palette.line}`,
|
||||
borderRadius: "12px",
|
||||
background: palette.cream,
|
||||
boxShadow: render.selected ? "0 0 0 3px rgba(217,155,61,.14)" : "none",
|
||||
color: palette.ink,
|
||||
fontFamily: "Avenir Next, Nunito, ui-sans-serif, system-ui, sans-serif",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: "10px" }}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={done ? "Reopen task" : "Complete task"}
|
||||
onClick={() => void setStatus(done ? "OPEN" : "DONE")}
|
||||
style={{
|
||||
flex: "0 0 auto",
|
||||
width: "26px",
|
||||
height: "26px",
|
||||
border: `1px solid ${done ? palette.moss : palette.line}`,
|
||||
borderRadius: "8px",
|
||||
background: done ? palette.moss : "#fff",
|
||||
color: "#fff",
|
||||
fontWeight: 900,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
{done ? "✓" : ""}
|
||||
</button>
|
||||
<div style={{ display: "grid", flex: 1, gap: "3px", minWidth: 0 }}>
|
||||
<strong
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
fontSize: render.compact ? "14px" : "17px",
|
||||
lineHeight: 1.25,
|
||||
textDecoration: done ? "line-through" : "none",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{title || "Untitled task"}
|
||||
</strong>
|
||||
<span style={{ color: statusTone(status), fontSize: "10px", fontWeight: 800, letterSpacing: ".08em" }}>
|
||||
{status || "OPEN"} · {priority || "NORMAL"}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => dispatch({ type: "SELECT" })}
|
||||
style={{
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
color: palette.muted,
|
||||
cursor: "pointer",
|
||||
fontSize: "12px",
|
||||
}}
|
||||
>
|
||||
{render.selected ? "Pinned" : "Open"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{!render.compact ? (
|
||||
<div style={{ display: "contents" }}>
|
||||
<label style={fieldLabelStyle}>
|
||||
Title
|
||||
<input
|
||||
value={title}
|
||||
onInput={(event: { currentTarget: HTMLInputElement }) => void setTitle(event.currentTarget.value)}
|
||||
style={controlStyle}
|
||||
/>
|
||||
</label>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(112px, 1fr))", gap: "8px" }}>
|
||||
<label style={fieldLabelStyle}>
|
||||
Status
|
||||
<select
|
||||
style={controlStyle}
|
||||
value={status}
|
||||
onChange={(event: { currentTarget: HTMLSelectElement }) => void setStatus(event.currentTarget.value)}
|
||||
>
|
||||
<option value="OPEN">Open</option>
|
||||
<option value="DONE">Done</option>
|
||||
<option value="ARCHIVED">Archived</option>
|
||||
</select>
|
||||
</label>
|
||||
<label style={fieldLabelStyle}>
|
||||
Priority
|
||||
<select
|
||||
style={controlStyle}
|
||||
value={priority}
|
||||
onChange={(event: { currentTarget: HTMLSelectElement }) => void setPriority(event.currentTarget.value)}
|
||||
>
|
||||
<option value="LOW">Low</option>
|
||||
<option value="NORMAL">Normal</option>
|
||||
<option value="HIGH">High</option>
|
||||
</select>
|
||||
</label>
|
||||
<label style={fieldLabelStyle}>
|
||||
Energy
|
||||
<select
|
||||
style={controlStyle}
|
||||
value={energy}
|
||||
onChange={(event: { currentTarget: HTMLSelectElement }) => void setEnergy(event.currentTarget.value)}
|
||||
>
|
||||
<option value="LOW">Low</option>
|
||||
<option value="MEDIUM">Medium</option>
|
||||
<option value="HIGH">High</option>
|
||||
</select>
|
||||
</label>
|
||||
<label style={fieldLabelStyle}>
|
||||
Due
|
||||
<input
|
||||
style={controlStyle}
|
||||
type="date"
|
||||
value={dueDate}
|
||||
onInput={(event: { currentTarget: HTMLInputElement }) => void setDueDate(event.currentTarget.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<label style={fieldLabelStyle}>
|
||||
Notes
|
||||
<textarea
|
||||
aria-label="Notes"
|
||||
value={notes}
|
||||
onInput={(event: { currentTarget: HTMLTextAreaElement }) => void setNotes(event.currentTarget.value)}
|
||||
rows={4}
|
||||
placeholder="Leave a trail for your future self…"
|
||||
style={{ ...controlStyle, minHeight: "78px", resize: "vertical" }}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: "6px", color: palette.muted, fontSize: "11px" }}>
|
||||
{dueDate ? <span>Due {dueDate}</span> : <span>No due date</span>}
|
||||
<span>·</span>
|
||||
<span>{energy || "MEDIUM"} energy</span>
|
||||
</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
|
||||
style={{
|
||||
color: palette.muted,
|
||||
fontSize: "10px",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{props.data.$quixosRef}
|
||||
</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>
|
||||
{history.status === "error" && (
|
||||
<div role="alert">
|
||||
{history.error.message}
|
||||
<button onClick={() => setAfter(null)}>Return to latest</button>
|
||||
</div>
|
||||
)}
|
||||
{history.status === "loading" && <p>Loading history…</p>}
|
||||
{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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -161,7 +161,7 @@ cacheEntries = {
|
||||
"@connectrpc/connect@npm:2.1.2" = { filename = "@connectrpc-connect-npm-2.1.2-ab34825de3-4e15000890.zip"; hash = "sha512-ThUACJAjt1Lv1E+pQKWuHh+gDhvrxLoM7ScruTWSDYw7hbd0UOZErUQruMqaJ1sMFxNviWuo7/A2F4Ex2wHWFQ=="; };
|
||||
"@esbuild/linux-arm64@npm:0.25.12" = { filename = "@esbuild-linux-arm64-npm-0.25.12-a5aaa0e70e-10c0.zip"; hash = "sha512-tm9zhnNR4GZv4A+Sgbt9jdCJ0iGlzvYxGCaxaScbhFH14mX7QWJRYaCvFCZB1dseApmaq6mNwbilL0wJnbB8tw=="; };
|
||||
"@esbuild/linux-x64@npm:0.25.12" = { filename = "@esbuild-linux-x64-npm-0.25.12-07c7c22cce-10c0.zip"; hash = "sha512-U2QGT8ZIZN8q2WPSFDC32+sZGKgp49gvnHzIf/vKVR4xIzjcgBbjAIKI+FqfGBmGBWu8eCpS7lR9JDXdUHOFkw=="; };
|
||||
"@quixos/camino-package-runtime@https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-package-runtime.git#commit=446fd4e36e9700cf27ce20b472a8d512133e9370" = { filename = "@quixos-camino-package-runtime-https-84d8fd5109-6d173f9220.zip"; hash = "sha512-bRc/kiD/ODsJqBNX4+RTqdsczILibHLhB+X8MckDCoK+j32c4Hszh87obb1Rwjb5o35aI0fzArQqCwK270zzlw=="; };
|
||||
"@quixos/camino-package-runtime@https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-package-runtime.git#commit=cd2b04dc8c2daeea633bf8ecdd31d20fdc41f2ab" = { filename = "@quixos-camino-package-runtime-https-99aa94a9bf-7872db96f5.zip"; hash = "sha512-eHLblvUv7jzGJ45p9j7PY5UsK74OeyZp2Z36rC5ki0BlGUXgCHAzeJ6YvaWov64433kSLJT9NjkocOgUacVvZg=="; };
|
||||
"@types/node@npm:24.13.3" = { filename = "@types-node-npm-24.13.3-b512a0bbeb-a5bc08f49b.zip"; hash = "sha512-pbwI9JuVgdzcqQ4CzXcZejx5mECAdmSULlzVFhpVPU0q6AZPfjKUQQ10jX0Ji1wYSL5/pQwG8pxBk6sWvk5Z6w=="; };
|
||||
"@types/prop-types@npm:15.7.15" = { filename = "@types-prop-types-npm-15.7.15-cefe16a1fa-b59aad1ad1.zip"; hash = "sha512-tZqtGtGb8XM89ST9TmGBlsbHaQ9I7nCjJ+tFCkKquOigY/vlnKClcBrr4tktWCKSwPuEXqV0dPahX2mUsOJgsg=="; };
|
||||
"@types/react@npm:18.3.31" = { filename = "@types-react-npm-18.3.31-d376eb19a5-44180549dd.zip"; hash = "sha512-RBgFSd0EX1Nuzs0545qs34KOdq3BxKkLEy9FPiPMNwxGSNkQKuQBFy69j9ixl3qQGjniFOU+x3FxsnUUtYjBeQ=="; };
|
||||
|
||||
@@ -213,14 +213,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@quixos/camino-package-runtime@https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-package-runtime.git#commit=446fd4e36e9700cf27ce20b472a8d512133e9370":
|
||||
"@quixos/camino-package-runtime@https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-package-runtime.git#commit=cd2b04dc8c2daeea633bf8ecdd31d20fdc41f2ab":
|
||||
version: 0.1.0
|
||||
resolution: "@quixos/camino-package-runtime@https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-package-runtime.git#commit=446fd4e36e9700cf27ce20b472a8d512133e9370"
|
||||
resolution: "@quixos/camino-package-runtime@https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-package-runtime.git#commit=cd2b04dc8c2daeea633bf8ecdd31d20fdc41f2ab"
|
||||
dependencies:
|
||||
"@bufbuild/protobuf": "npm:^2.12.1"
|
||||
"@connectrpc/connect": "npm:^2.1.2"
|
||||
"@connectrpc/connect-node": "npm:^2.1.2"
|
||||
checksum: 10c0/6d173f9220ff383b09a81357e3e453a9db1ccc82e26c72e107e5fc31c9030a82be8f7d9ce07b3387cee86dbd51c236f9a37e5a2347f302b42a0b02b6ef4cf397
|
||||
checksum: 10c0/7872db96f52fee3cc6278e69f63ecf63952c2bbe0e7b2669d99dfaac2e648b40651945e0087033789e98bda5a8bfae38df79122c94fd36392870e81469c56f66
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -228,7 +228,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@quixos/todo-react-runtime@workspace:."
|
||||
dependencies:
|
||||
"@quixos/camino-package-runtime": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-package-runtime.git#commit=446fd4e36e9700cf27ce20b472a8d512133e9370"
|
||||
"@quixos/camino-package-runtime": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-package-runtime.git#commit=cd2b04dc8c2daeea633bf8ecdd31d20fdc41f2ab"
|
||||
"@types/node": "npm:^24"
|
||||
"@types/react": "npm:^18.3.12"
|
||||
esbuild: "npm:^0.25.12"
|
||||
|
||||
Reference in New Issue
Block a user