Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6539111dc | ||
|
|
14aefcd80e |
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"monorepoCommit": "1227e3672a5a24b7331567fc3f431e422e1513c1",
|
||||
"sourcePath": "quixos-instance/packages/project-react-runtime"
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
# Generated from the checked package contract
|
||||
package_id: "package:project-react-runtime"
|
||||
package_revision_id: "package:project-react-runtime@1"
|
||||
runtime_protocol_version: "quixos-capabilities-v1"
|
||||
exports: { export_id: "export:project-react-runtime:create-component" runtime_symbol: "createComponent" }
|
||||
exports: { export_id: "export:project-react-runtime:source-get" runtime_symbol: "source.get" }
|
||||
exports: { export_id: "export:project-react-runtime:props-get" runtime_symbol: "props.get" }
|
||||
exports: { export_id: "export:project-react-runtime:props-watch" runtime_symbol: "props.watch" }
|
||||
exports: { export_id: "export:project-react-runtime:props-watch-stop" runtime_symbol: "props.watchStop" }
|
||||
exports: { export_id: "export:project-react-runtime:source-get" runtime_symbol: "sourceGet" }
|
||||
exports: { export_id: "export:project-react-runtime:props-get" runtime_symbol: "propsGet" }
|
||||
exports: { export_id: "export:project-react-runtime:props-watch" runtime_symbol: "propsWatch" }
|
||||
exports: { export_id: "export:project-react-runtime:props-watch-stop" runtime_symbol: "propsWatchStop" }
|
||||
|
||||
Generated
+7
-7
@@ -37,17 +37,17 @@
|
||||
"quixosNixHelpers": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1788543053,
|
||||
"narHash": "sha256-3YReATWEVxZinYVi/l3cwOz+oFoZUrwj24kUBQTmUpQ=",
|
||||
"ref": "refs/tags/quixos-reachability/56bbcd22d17f8c7f120b1e0b5bd1d5a86e91a80a",
|
||||
"rev": "56bbcd22d17f8c7f120b1e0b5bd1d5a86e91a80a",
|
||||
"revCount": 8,
|
||||
"lastModified": 1789602549,
|
||||
"narHash": "sha256-DD66dQ3dzEvG13Zju/4sVSV4pnktSUkJ/Rbr1KkHMdg=",
|
||||
"ref": "refs/tags/quixos-reachability/e2747066ca7d9d001dcb5dd936e17969ed88fc5f",
|
||||
"rev": "e2747066ca7d9d001dcb5dd936e17969ed88fc5f",
|
||||
"revCount": 32,
|
||||
"type": "git",
|
||||
"url": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-nix-helpers.git"
|
||||
},
|
||||
"original": {
|
||||
"ref": "refs/tags/quixos-reachability/56bbcd22d17f8c7f120b1e0b5bd1d5a86e91a80a",
|
||||
"rev": "56bbcd22d17f8c7f120b1e0b5bd1d5a86e91a80a",
|
||||
"ref": "refs/tags/quixos-reachability/e2747066ca7d9d001dcb5dd936e17969ed88fc5f",
|
||||
"rev": "e2747066ca7d9d001dcb5dd936e17969ed88fc5f",
|
||||
"type": "git",
|
||||
"url": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-nix-helpers.git"
|
||||
}
|
||||
|
||||
@@ -6,31 +6,33 @@
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
|
||||
quixosNixHelpers = {
|
||||
url = "git+https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-nix-helpers.git?ref=refs/tags/quixos-reachability/56bbcd22d17f8c7f120b1e0b5bd1d5a86e91a80a&rev=56bbcd22d17f8c7f120b1e0b5bd1d5a86e91a80a";
|
||||
url = "git+https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-nix-helpers.git?ref=refs/tags/quixos-reachability/e2747066ca7d9d001dcb5dd936e17969ed88fc5f&rev=e2747066ca7d9d001dcb5dd936e17969ed88fc5f";
|
||||
flake = false;
|
||||
};
|
||||
};
|
||||
|
||||
outputs = inputs@{ nixpkgs, flake-utils, quixosNixHelpers, ... }:
|
||||
outputs =
|
||||
inputs@{
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
quixosNixHelpers,
|
||||
...
|
||||
}:
|
||||
let
|
||||
quixosHelpers = import "${quixosNixHelpers}/quixos-package-helpers.nix";
|
||||
in
|
||||
quixosHelpers.mkCaminoTsYarnNixifyFlake {
|
||||
inherit inputs nixpkgs flake-utils;
|
||||
packageRoot = ./.;
|
||||
migrationEntrypoint = "dist/migrate.js";
|
||||
promptName = "project-react-runtime";
|
||||
bundle = {
|
||||
entry = "dist/server.js";
|
||||
entry = "src/server.ts";
|
||||
browserSources = true;
|
||||
};
|
||||
installServer = {
|
||||
libexecName = "project-react-runtime";
|
||||
descriptorPath = "descriptor.quixos-package.txtpb";
|
||||
extraFiles = [
|
||||
{
|
||||
source = "dist/project-component.mjs";
|
||||
target = "project-component.mjs";
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
+2
-2
@@ -8,11 +8,11 @@
|
||||
"server": "dist/server.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && tsc -p tsconfig.build.json && node scripts/build-component.mjs",
|
||||
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@quixos/camino-package-runtime": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-package-runtime.git#commit=a998690446ecf647e3af78e369492c1a3663b66d",
|
||||
"@quixos/camino-package-runtime": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-package-runtime.git#commit=86933aa97f3ea386a4c5cfeb28386fd8d2722075",
|
||||
"react": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+32
-2
@@ -1,18 +1,48 @@
|
||||
import interface TagData;
|
||||
import interface CompletionData;
|
||||
import interface TaskActions;
|
||||
import interface TaskData;
|
||||
import interface ProjectActions;
|
||||
external atom Task id "atom:task";
|
||||
external atom Tag id "atom:tag";
|
||||
external atom Completion id "atom:completion";
|
||||
import interface Named;
|
||||
import interface ProjectTasks;
|
||||
external atom ProjectReactComponent id "atom:project-react-component";
|
||||
external atom Project id "atom:project";
|
||||
type ProjectProps = record {named: interface-ref<Named>; tasks: interface-ref<ProjectTasks>;};
|
||||
|
||||
package ProjectReactRuntime id "package:project-react-runtime" revision "package:project-react-runtime@1" {
|
||||
constructor createComponent id "export:project-react-runtime:create-component" constructs ProjectReactComponent : atom-ref<Project>;
|
||||
operation sourceGet id "export:project-react-runtime:source-get" : unit -> string mode call receiver atom ProjectReactComponent;
|
||||
operation propsGet id "export:project-react-runtime:props-get" : unit -> message "org.quixos.web-studio.ReactProps" mode call receiver atom ProjectReactComponent requires {
|
||||
operation propsGet id "export:project-react-runtime:props-get" : unit -> ProjectProps mode call receiver atom ProjectReactComponent requires {
|
||||
interface named id "port:project-react:named" : Named;
|
||||
interface tasks id "port:project-react:tasks" : ProjectTasks;
|
||||
};
|
||||
operation propsWatch id "export:project-react-runtime:props-watch" : unit -> watch-handle mode watch-start emits message "org.quixos.web-studio.ReactProps" receiver atom ProjectReactComponent requires {
|
||||
operation propsWatch id "export:project-react-runtime:props-watch" : unit -> watch-handle mode watch-start emits ProjectProps receiver atom ProjectReactComponent requires {
|
||||
interface named id "port:project-react:named" : Named;
|
||||
interface tasks id "port:project-react:tasks" : ProjectTasks;
|
||||
};
|
||||
operation propsWatchStop id "export:project-react-runtime:props-watch-stop" : watch-handle -> unit mode watch-stop receiver atom ProjectReactComponent;
|
||||
query Tasks id "query:Tasks" root ProjectTasks document "queries/Tasks.graphql" operation "Tasks" {
|
||||
view Task as TaskData; view Tag as TagData; view Completion as CompletionData;
|
||||
max rows 100; max depth 8; max result-bytes 1048576; watch;
|
||||
}
|
||||
query TaggedTasks id "query:TaggedTasks" root ProjectTasks document "queries/TaggedTasks.graphql" operation "TaggedTasks" {
|
||||
view Task as TaskData; view Tag as TagData; view Completion as CompletionData;
|
||||
max rows 100; max depth 8; max result-bytes 1048576; watch;
|
||||
}
|
||||
query UntaggedTasks id "query:UntaggedTasks" root ProjectTasks document "queries/UntaggedTasks.graphql" operation "UntaggedTasks" {
|
||||
view Task as TaskData; view Tag as TagData; view Completion as CompletionData;
|
||||
max rows 100; max depth 8; max result-bytes 1048576; watch;
|
||||
}
|
||||
query Catalog id "query:Catalog" root ProjectTasks document "queries/Catalog.graphql" operation "Catalog" {
|
||||
view Task as TaskData; view Tag as TagData; view Completion as CompletionData;
|
||||
max rows 100; max depth 8; max result-bytes 1048576; watch;
|
||||
}
|
||||
query Summary id "query:Summary" root ProjectTasks document "queries/Summary.graphql" operation "Summary" {
|
||||
view Task as TaskData; view Tag as TagData; view Completion as CompletionData;
|
||||
max rows 100; max depth 8; max result-bytes 1048576; watch;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
query Catalog {root {tags(all: 100, orderBy: [{name: ASC}]) {entries {node {_qx {ref} name color}}}}}
|
||||
@@ -0,0 +1,8 @@
|
||||
query Summary {root {_qx {relations {tasks {
|
||||
aggregate {count sum {estimatedHours}}
|
||||
groups(by: {status: true}, first: 10) {entries {group {status} aggregate {count sum {estimatedHours}}}}
|
||||
filter(where: {status: {eq: "OPEN"}}) {expand {tags {
|
||||
groups(by: {target: {_qx: {ref: true}, name: true, color: true}}, first: 100,
|
||||
orderBy: [{count: DESC}]) {entries {group {target {_qx {ref} name color}} aggregate {count sum {source {estimatedHours}}}}}
|
||||
}}}
|
||||
}}}}}
|
||||
@@ -0,0 +1,4 @@
|
||||
query TaggedTasks($first: Int!, $after: Cursor, $statuses: [String!]!, $tags: [QxRef_TagData!]!) {root {tasks(first: $first, after: $after, where: {status: {in: $statuses}, _qx: {relations: {tags: {some: {_qx: {ref: {in: $tags}}}}}}}, orderBy: [{dueDate: ASC}, {title: ASC}]) {entries { key node {_qx {ref} title @live status priority dueDate repeatDays estimatedHours
|
||||
tags(all: 100) {entries {node {_qx {ref} name color}}}
|
||||
_qx {relations {completions {aggregate {count max {completedAt}}}}}
|
||||
}} pageInfo {hasNextPage endCursor}}}}
|
||||
@@ -0,0 +1,4 @@
|
||||
query Tasks($first: Int!, $after: Cursor, $statuses: [String!]!) {root {tasks(first: $first, after: $after, where: {status: {in: $statuses}}, orderBy: [{dueDate: ASC}, {title: ASC}]) {entries { key node {_qx {ref} title @live status priority dueDate repeatDays estimatedHours
|
||||
tags(all: 100) {entries {node {_qx {ref} name color}}}
|
||||
_qx {relations {completions {aggregate {count max {completedAt}}}}}
|
||||
}} pageInfo {hasNextPage endCursor}}}}
|
||||
@@ -0,0 +1,4 @@
|
||||
query UntaggedTasks($first: Int!, $after: Cursor, $statuses: [String!]!) {root {tasks(first: $first, after: $after, where: {status: {in: $statuses}, _qx: {relations: {tags: {none: {}}}}}, orderBy: [{dueDate: ASC}, {title: ASC}]) {entries { key node {_qx {ref} title @live status priority dueDate repeatDays estimatedHours
|
||||
tags(all: 100) {entries {node {_qx {ref} name color}}}
|
||||
_qx {relations {completions {aggregate {count max {completedAt}}}}}
|
||||
}} pageInfo {hasNextPage endCursor}}}}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"backend": "typescript",
|
||||
"bindingOutput": "src/gen/qx.ts",
|
||||
"options": {
|
||||
"react": {
|
||||
"propsExports": ["propsGet"],
|
||||
"components": [{ "module": "../project-component.js", "propsExport": "propsGet" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
-5
@@ -1,16 +1,34 @@
|
||||
quixos-lock version 1 {
|
||||
quixos source {
|
||||
repository "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos.git";
|
||||
commit "9c3ab572ecfbc83785038c3d9f174516df15d63f";
|
||||
commit "1227e3672a5a24b7331567fc3f431e422e1513c1";
|
||||
}
|
||||
|
||||
interface Named source {
|
||||
repository "https://repos.quixos.org/quixos-templates/interface-named.git";
|
||||
commit "14be607ed250f8c3ce1633558573fc1449f6f2a1";
|
||||
commit "1aa05d6b5b953126adcaa85c4a5f17d444246273";
|
||||
}
|
||||
|
||||
interface ProjectTasks source {
|
||||
repository "https://repos.quixos.org/quixos-templates/interface-project-tasks.git";
|
||||
commit "2590966c1f95da08126f1cb830be7dcc6dc715d9";
|
||||
commit "0b4f9fecf2a367b207575bae6ec5114ea28f408a";
|
||||
}
|
||||
interface TagData source {
|
||||
repository "https://repos.quixos.org/quixos-templates/interface-tag-data.git";
|
||||
commit "353491e12fd5ef16f4253817dc48178043e7cc40";
|
||||
}
|
||||
interface CompletionData source {
|
||||
repository "https://repos.quixos.org/quixos-templates/interface-completion-data.git";
|
||||
commit "79c5046dfe5d4172af77e39f19fb572afb7cf84c";
|
||||
}
|
||||
interface TaskData source {
|
||||
repository "https://repos.quixos.org/quixos-templates/interface-task-data.git";
|
||||
commit "67adb8a9f8d3c13126ed503a4ddc1bcf1153aef5";
|
||||
}
|
||||
interface TaskActions source {
|
||||
repository "https://repos.quixos.org/quixos-templates/interface-task-actions.git";
|
||||
commit "a129c7a65aa4027d36a43f87c1f2e65afe066d1a";
|
||||
}
|
||||
interface ProjectActions source {
|
||||
repository "https://repos.quixos.org/quixos-templates/interface-project-actions.git";
|
||||
commit "2336dd32216641fb94361962d015008a82d0776f";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"generatedBy": "qx-scaffold-v1",
|
||||
"schemaVersion": 1,
|
||||
"contracts": {},
|
||||
"migrations": []
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"generatedBy": "qx-scaffold-v1",
|
||||
"nixifyPluginUrl": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/yarn-plugin-nixify-patched/raw/commit/4528fdd20b30d869262443b3f044549810e75fb8/dist/yarn-plugin-nixify.js"
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { build } from "esbuild";
|
||||
|
||||
const platformModules = new Map([
|
||||
[
|
||||
"@quixos/web-studio-react-runtime",
|
||||
"/__quixos/platform/web-studio-react-runtime/v1.mjs",
|
||||
],
|
||||
["react", "/__quixos/platform/react/v18.mjs"],
|
||||
["react/jsx-runtime", "/__quixos/platform/react-jsx-runtime/v18.mjs"],
|
||||
["react/jsx-dev-runtime", "/__quixos/platform/react-jsx-dev-runtime/v18.mjs"],
|
||||
]);
|
||||
|
||||
await build({
|
||||
entryPoints: ["dist/project-component.js"],
|
||||
outfile: "dist/project-component.mjs",
|
||||
bundle: true,
|
||||
format: "esm",
|
||||
platform: "browser",
|
||||
target: "es2022",
|
||||
sourcemap: true,
|
||||
plugins: [
|
||||
{
|
||||
name: "quixos-platform-modules",
|
||||
setup(buildApi) {
|
||||
buildApi.onResolve({ filter: /.*/ }, (args) => {
|
||||
const platformPath = platformModules.get(args.path);
|
||||
return platformPath
|
||||
? { path: platformPath, external: true }
|
||||
: undefined;
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
declare module "*?browser-source" {
|
||||
const source: string;
|
||||
export default source;
|
||||
}
|
||||
declare module "*.css";
|
||||
+1186
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+41
-29
@@ -1,28 +1,42 @@
|
||||
// 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 LiveFieldProp<T> = {
|
||||
value: T;
|
||||
source: {
|
||||
objectId: string;
|
||||
slotId: string;
|
||||
valueType?: string;
|
||||
storagePolicy?: string;
|
||||
revision?: string | number | bigint;
|
||||
crdtSnapshot?: {
|
||||
type: string;
|
||||
encoding: string;
|
||||
payload: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
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?: unknown;
|
||||
fallback?: React.ReactNode;
|
||||
className?: string;
|
||||
style?: Record<string, string | number | undefined>;
|
||||
onError?: (error: Error) => void;
|
||||
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;
|
||||
@@ -39,16 +53,14 @@ declare module "@quixos/web-studio-react-runtime" {
|
||||
interfaceRevisionId: string,
|
||||
operationId: string,
|
||||
value?: unknown,
|
||||
options?: {clientMutationId?: string; signal?: AbortSignal},
|
||||
) => Promise<Result>;
|
||||
export const h: (...args: any[]) => any;
|
||||
export const useLiveField: <T>(
|
||||
field: LiveFieldProp<T>,
|
||||
options?: {
|
||||
reconcileRegister?: (state: {
|
||||
confirmed: T;
|
||||
optimistic: T;
|
||||
pending: boolean;
|
||||
}) => T;
|
||||
},
|
||||
) => [T, (value: T) => void | Promise<void>];
|
||||
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 "../project-react.impl.js";
|
||||
@@ -0,0 +1,2 @@
|
||||
// This adapter keeps the implementation file hand-authored.
|
||||
export { props as handler } from "../project-react.impl.js";
|
||||
@@ -0,0 +1,2 @@
|
||||
// This adapter keeps the implementation file hand-authored.
|
||||
export { props as handler } from "../project-react.impl.js";
|
||||
@@ -0,0 +1,2 @@
|
||||
// This adapter keeps the implementation file hand-authored.
|
||||
export { stopWatch as handler } from "../project-react.impl.js";
|
||||
@@ -0,0 +1,2 @@
|
||||
// This adapter keeps the implementation file hand-authored.
|
||||
export { source as handler } from "../project-react.impl.js";
|
||||
@@ -0,0 +1,3 @@
|
||||
// Generated by qx-scaffold-v1
|
||||
import { serveMigration } from "@quixos/camino-package-runtime";
|
||||
await serveMigration({});
|
||||
+335
-156
@@ -1,185 +1,364 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
h,
|
||||
invokeCapability,
|
||||
createWebStudioComponent,
|
||||
tryConform,
|
||||
useLiveField,
|
||||
type LiveFieldProp,
|
||||
useQuery,
|
||||
type ObjectRef,
|
||||
type QueryState,
|
||||
type ReactComponentImplementationProps,
|
||||
} from "@quixos/web-studio-react-runtime";
|
||||
import {
|
||||
TodoComponent,
|
||||
type TodoComponentProps,
|
||||
type TodoReactComponentAction,
|
||||
} from "./gen/quixos.todo.TodoReactComponent.react.js";
|
||||
import type { ProjectReactComponentRenderProps } from "./gen/quixos.todo.ProjectReactComponent.react.js";
|
||||
queries,
|
||||
reactInterfaces,
|
||||
type QueryResults,
|
||||
type QueryVariables,
|
||||
type ReactResults,
|
||||
} from "./gen/react-props.gen.js";
|
||||
import "./tracker.css";
|
||||
|
||||
type TaskRef = TodoComponentProps["forObject"];
|
||||
|
||||
type ProjectCaminoProps = {
|
||||
project_id: ObjectRef<"atom:project">;
|
||||
name: LiveFieldProp<string>;
|
||||
tasks: TaskRef[];
|
||||
type Tag = QueryResults["Catalog"]["root"]["tags"]["entries"][number]["node"];
|
||||
type TagChoice = { id: string; name: string; color: string };
|
||||
const TaskEditor = createWebStudioComponent<ObjectRef<"atom:task">, { tagChoices: TagChoice[] }, never>({
|
||||
expectedAtomId: "atom:task",
|
||||
});
|
||||
type Props = ReactComponentImplementationProps<ReactResults["propsGet"], { compact?: boolean }, never>;
|
||||
type ListProps = {
|
||||
root: string;
|
||||
variables: QueryVariables["Tasks"];
|
||||
selected: string | null;
|
||||
select(id: string): void;
|
||||
next(cursor: string | null): void;
|
||||
reset(): void;
|
||||
};
|
||||
|
||||
type ProjectComponentProps = ReactComponentImplementationProps<
|
||||
ProjectCaminoProps,
|
||||
ProjectReactComponentRenderProps,
|
||||
Record<string, never>
|
||||
>;
|
||||
|
||||
const colors = {
|
||||
ink: "#28362d",
|
||||
muted: "#72786f",
|
||||
cream: "#fffdf5",
|
||||
paper: "#f6f0df",
|
||||
line: "#d9d0ba",
|
||||
moss: "#5f7f63",
|
||||
honey: "#d99b3d",
|
||||
};
|
||||
|
||||
export default function ProjectComponent({ camino, render }: ProjectComponentProps) {
|
||||
const [name, setName] = useLiveField(camino.name);
|
||||
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 || !newTaskTitle.trim()) return;
|
||||
setAddingTask(true);
|
||||
setAddTaskError("");
|
||||
function TaskTitle({ field }: { field: QueryResults["Tasks"]["root"]["tasks"]["entries"][number]["node"]["title"] }) {
|
||||
const title = useLiveField(field);
|
||||
return <strong>{title.value ?? "Loading…"}</strong>;
|
||||
}
|
||||
function Rows({ result, selected, select, next, reset }: ListProps & { result: QueryState<QueryResults["Tasks"]> }) {
|
||||
if (result.status === "error")
|
||||
return (
|
||||
<div role="alert" className="tt-notice">
|
||||
{result.error.message}
|
||||
<button onClick={reset}>Return to first page</button>
|
||||
</div>
|
||||
);
|
||||
if (result.status !== "ready") return <p className="tt-notice">Loading tasks…</p>;
|
||||
const page = result.data.root.tasks;
|
||||
return (
|
||||
<>
|
||||
<div className="tt-rows" aria-label="Tasks">
|
||||
{page.entries.length === 0 && <p className="tt-notice">No tasks match this view.</p>}
|
||||
{page.entries.map(({ key, node: task }) => (
|
||||
<button
|
||||
className={"tt-row" + (selected === task._qx.ref.$quixosRef ? " is-selected" : "")}
|
||||
aria-pressed={selected === task._qx.ref.$quixosRef}
|
||||
key={key}
|
||||
onClick={() => select(task._qx.ref.$quixosRef)}
|
||||
>
|
||||
<span className={"tt-status status-" + task.status.toLowerCase()}>
|
||||
{task.status === "DONE" ? "✓" : task.status === "ARCHIVED" ? "—" : "○"}
|
||||
</span>
|
||||
<span className="tt-row-body">
|
||||
<TaskTitle field={task.title} />
|
||||
<span className="tt-row-meta">
|
||||
{task.dueDate || "No due date"}
|
||||
{task.repeatDays > 0 ? ` · Every ${task.repeatDays}d` : ""}
|
||||
{task.estimatedHours > 0 ? ` · ${task.estimatedHours}h` : ""}
|
||||
{task.priority === "HIGH" ? " · High priority" : ""}
|
||||
</span>
|
||||
<span className="tt-tag-line">
|
||||
{task.tags.entries.map(({ node: tag }) => (
|
||||
<span className="tt-tag" key={tag._qx.ref.$quixosRef}>
|
||||
<i style={{ background: tag.color }} />
|
||||
{tag.name}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
{task._qx.relations.completions.aggregate.count > 0n && (
|
||||
<span className="tt-row-meta">
|
||||
{String(task._qx.relations.completions.aggregate.count)} completions
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="tt-page">
|
||||
<button onClick={reset}>First page</button>
|
||||
{page.pageInfo.hasNextPage && <button onClick={() => next(page.pageInfo.endCursor)}>Next 50 tasks →</button>}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
function AllRows(props: ListProps) {
|
||||
return <Rows {...props} result={useQuery(queries.Tasks, { root: props.root, variables: props.variables })} />;
|
||||
}
|
||||
function UntaggedRows(props: ListProps) {
|
||||
return <Rows {...props} result={useQuery(queries.UntaggedTasks, { root: props.root, variables: props.variables })} />;
|
||||
}
|
||||
function TaggedRows(props: ListProps & { tags: Tag["_qx"]["ref"][] }) {
|
||||
return (
|
||||
<Rows
|
||||
{...props}
|
||||
result={useQuery(queries.TaggedTasks, { root: props.root, variables: { ...props.variables, tags: props.tags } })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export default function ProjectComponent({ camino }: Props) {
|
||||
const nameField = useLiveField(camino.named.fields.name);
|
||||
const name = nameField.value ?? "";
|
||||
const setName = nameField.set;
|
||||
const root = camino.tasks.$quixosRef;
|
||||
const catalog = useQuery(queries.Catalog, { root, variables: {} });
|
||||
const summary = useQuery(queries.Summary, { root, variables: {} });
|
||||
const [selected, setSelected] = useState<string | null>(null);
|
||||
const [status, setStatus] = useState("OPEN");
|
||||
const [tagIds, setTagIds] = useState<string[]>([]);
|
||||
const [untagged, setUntagged] = useState(false);
|
||||
const [after, setAfter] = useState<string | null>(null);
|
||||
const [newTitle, setNewTitle] = useState("");
|
||||
const [newTag, setNewTag] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const tags = catalog.status === "ready" ? catalog.data.root.tags.entries.map((e) => e.node) : [];
|
||||
const run = async (action: () => Promise<unknown>) => {
|
||||
if (busy) return;
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
await invokeCapability(
|
||||
camino.project_id,
|
||||
"interface:project-actions@1",
|
||||
"operation:project-actions:add-task",
|
||||
newTaskTitle,
|
||||
);
|
||||
setNewTaskTitle("");
|
||||
} catch (error) {
|
||||
setAddTaskError(error instanceof Error ? error.message : String(error));
|
||||
await action();
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e));
|
||||
} finally {
|
||||
setAddingTask(false);
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const actions = async () => {
|
||||
const view = await tryConform(root, reactInterfaces.ProjectActions);
|
||||
if (!view) throw new Error("This project no longer supports task actions");
|
||||
return view.call;
|
||||
};
|
||||
const addTask = () =>
|
||||
run(async () => {
|
||||
if (!newTitle.trim()) return;
|
||||
const task = await (await actions())["addTask.call"](newTitle.trim());
|
||||
setNewTitle("");
|
||||
setAfter(null);
|
||||
setStatus("OPEN");
|
||||
setTagIds([]);
|
||||
setUntagged(false);
|
||||
setSelected(task);
|
||||
});
|
||||
const editTag = (tag: Tag, field: "name" | "color", value: string) =>
|
||||
run(async () => {
|
||||
if (!value.trim()) throw new Error("A tag needs a name");
|
||||
const view = await tryConform(tag._qx.ref, reactInterfaces.TagData);
|
||||
if (!view) throw new Error("Tag is no longer available");
|
||||
await view.call[field === "name" ? "name.set" : "color.set"](value);
|
||||
});
|
||||
const choices = tags.map((tag) => ({ id: tag._qx.ref.$quixosRef, name: tag.name, color: tag.color }));
|
||||
const totals = summary.status === "ready" ? summary.data.root._qx.relations.tasks : undefined;
|
||||
const list: ListProps = {
|
||||
root,
|
||||
variables: { first: 50, after, statuses: status === "ALL" ? ["OPEN", "DONE", "ARCHIVED"] : [status] },
|
||||
selected,
|
||||
select: setSelected,
|
||||
next: setAfter,
|
||||
reset: () => {
|
||||
setAfter(null);
|
||||
},
|
||||
};
|
||||
const activeTags = tags.filter((t) => tagIds.includes(t._qx.ref.$quixosRef));
|
||||
return (
|
||||
<section style={{
|
||||
display: "grid",
|
||||
gridTemplateRows: "auto auto minmax(0, 1fr)",
|
||||
gap: "12px",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
minHeight: 0,
|
||||
padding: render.compact ? "11px" : "14px",
|
||||
color: colors.ink,
|
||||
fontFamily: "Avenir Next, Nunito, ui-sans-serif, system-ui, sans-serif",
|
||||
}}>
|
||||
<header style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "12px" }}>
|
||||
<div style={{ display: "grid", gap: "2px", minWidth: 0, flex: 1 }}>
|
||||
<input
|
||||
aria-label="Project name"
|
||||
value={name}
|
||||
onInput={(event) => void setName(event.currentTarget.value)}
|
||||
style={{
|
||||
minWidth: 0,
|
||||
width: "100%",
|
||||
border: "none",
|
||||
borderBottom: `1px solid ${colors.line}`,
|
||||
background: "transparent",
|
||||
color: colors.ink,
|
||||
fontFamily: "Georgia, ui-serif, serif",
|
||||
fontSize: render.compact ? "17px" : "22px",
|
||||
fontWeight: 700,
|
||||
padding: "3px 0 5px",
|
||||
}}
|
||||
/>
|
||||
<span style={{ color: colors.muted, fontSize: "11px" }}>A little clearing for useful things.</span>
|
||||
</div>
|
||||
<span style={{
|
||||
flex: "0 0 auto",
|
||||
border: `1px solid ${colors.line}`,
|
||||
borderRadius: "8px",
|
||||
background: colors.paper,
|
||||
color: colors.moss,
|
||||
fontSize: "11px",
|
||||
fontWeight: 800,
|
||||
padding: "5px 8px",
|
||||
}}>
|
||||
{camino.tasks.length} {camino.tasks.length === 1 ? "task" : "tasks"}
|
||||
</span>
|
||||
<section className="tt-board">
|
||||
<header className="tt-header">
|
||||
<input
|
||||
aria-label="Project name"
|
||||
value={name}
|
||||
onChange={(e) => {
|
||||
void setName(e.target.value).catch((err) => setError(String(err)));
|
||||
}}
|
||||
/>
|
||||
<span>{totals ? `${String(totals.aggregate.count)} tasks` : "Task tracker"}</span>
|
||||
</header>
|
||||
|
||||
{error && (
|
||||
<div role="alert" className="tt-error">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
className="tt-add"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
void addTask();
|
||||
}}
|
||||
style={{ display: "grid", gridTemplateColumns: "minmax(0, 1fr) auto", gap: "8px" }}
|
||||
>
|
||||
<input
|
||||
aria-label="New task title"
|
||||
value={newTaskTitle}
|
||||
onInput={(event) => setNewTaskTitle(event.currentTarget.value)}
|
||||
placeholder="What wants doing?"
|
||||
disabled={addingTask}
|
||||
style={{ minWidth: 0, border: `1px solid ${colors.line}`, borderRadius: "9px", background: "#fff", padding: "8px 10px" }}
|
||||
placeholder="Add a task"
|
||||
value={newTitle}
|
||||
onChange={(e) => setNewTitle(e.target.value)}
|
||||
disabled={busy}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={addingTask || newTaskTitle.trim().length === 0}
|
||||
style={{
|
||||
border: `1px solid ${colors.moss}`,
|
||||
borderRadius: "9px",
|
||||
background: colors.moss,
|
||||
color: "#fffdf5",
|
||||
cursor: "pointer",
|
||||
fontSize: "12px",
|
||||
fontWeight: 750,
|
||||
padding: "8px 12px",
|
||||
<button disabled={busy || !newTitle.trim()} type="submit">
|
||||
Add task
|
||||
</button>
|
||||
</form>
|
||||
<nav className="tt-filters" aria-label="Task filters">
|
||||
<select
|
||||
aria-label="Task status"
|
||||
value={status}
|
||||
onChange={(e) => {
|
||||
setStatus(e.target.value);
|
||||
setAfter(null);
|
||||
}}
|
||||
>
|
||||
{addingTask ? "Planting…" : "Add task"}
|
||||
<option value="OPEN">Open</option>
|
||||
<option value="DONE">Done</option>
|
||||
<option value="ARCHIVED">Archived</option>
|
||||
<option value="ALL">All statuses</option>
|
||||
</select>
|
||||
<button
|
||||
aria-pressed={!untagged && tagIds.length === 0}
|
||||
onClick={() => {
|
||||
setTagIds([]);
|
||||
setUntagged(false);
|
||||
setAfter(null);
|
||||
}}
|
||||
>
|
||||
All tags
|
||||
</button>
|
||||
{addTaskError ? <div role="alert" style={{ gridColumn: "1 / -1", color: "#9f4741", fontSize: "11px" }}>{addTaskError}</div> : null}
|
||||
</form>
|
||||
|
||||
<div style={{
|
||||
display: "grid",
|
||||
alignContent: "start",
|
||||
gap: "9px",
|
||||
minHeight: 0,
|
||||
overflow: "auto",
|
||||
paddingRight: "3px",
|
||||
}}>
|
||||
{camino.tasks.length === 0 ? (
|
||||
<div style={{
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
minHeight: render.compact ? "80px" : "140px",
|
||||
border: `1px dashed ${colors.line}`,
|
||||
borderRadius: "12px",
|
||||
background: colors.paper,
|
||||
color: colors.muted,
|
||||
fontSize: "12px",
|
||||
textAlign: "center",
|
||||
}}>
|
||||
Your project is quiet. Add the first task when it arrives.
|
||||
</div>
|
||||
) : camino.tasks.map((task) => (
|
||||
<TodoComponent
|
||||
key={task}
|
||||
forObject={task}
|
||||
selected={selectedTask === task}
|
||||
compact={render.compact ?? true}
|
||||
onAction={(action) => handleTaskAction(task, action)}
|
||||
fallback={<div style={{ color: colors.muted, fontSize: "11px" }}>Waking task…</div>}
|
||||
/>
|
||||
<button
|
||||
aria-pressed={untagged}
|
||||
onClick={() => {
|
||||
setTagIds([]);
|
||||
setUntagged(true);
|
||||
setAfter(null);
|
||||
}}
|
||||
>
|
||||
Untagged
|
||||
</button>
|
||||
{tags.map((tag) => (
|
||||
<button
|
||||
key={tag._qx.ref.$quixosRef}
|
||||
aria-pressed={tagIds.includes(tag._qx.ref.$quixosRef)}
|
||||
onClick={() => {
|
||||
setUntagged(false);
|
||||
setAfter(null);
|
||||
setTagIds((ids) =>
|
||||
ids.includes(tag._qx.ref.$quixosRef)
|
||||
? ids.filter((id) => id !== tag._qx.ref.$quixosRef)
|
||||
: [...ids, tag._qx.ref.$quixosRef],
|
||||
);
|
||||
}}
|
||||
>
|
||||
<i style={{ background: tag.color }} />
|
||||
{tag.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
{tagIds.length > 1 && <p className="tt-filter-note">Matching any selected tag</p>}
|
||||
{catalog.status === "error" && (
|
||||
<p role="alert" className="tt-error">
|
||||
Tags: {catalog.error.message}
|
||||
</p>
|
||||
)}
|
||||
<main className={"tt-body" + (selected ? " has-detail" : "")}>
|
||||
<section className="tt-list">
|
||||
{untagged ? (
|
||||
<UntaggedRows {...list} />
|
||||
) : activeTags.length ? (
|
||||
<TaggedRows {...list} tags={activeTags.map((t) => t._qx.ref)} />
|
||||
) : (
|
||||
<AllRows {...list} />
|
||||
)}
|
||||
</section>
|
||||
<section className="tt-detail">
|
||||
{selected ? (
|
||||
<>
|
||||
<div className="tt-detail-bar">
|
||||
<button onClick={() => setSelected(null)}>← Task list</button>
|
||||
<a href={`/entity/${encodeURIComponent(selected)}`}>Open separately ↗</a>
|
||||
</div>
|
||||
{/* The queried relation explicitly targets atom Task; query refs carry its TaskData view. */}
|
||||
<TaskEditor
|
||||
forObject={selected as ObjectRef<"atom:task">}
|
||||
tagChoices={choices}
|
||||
style={{ flex: 1, minHeight: 0, height: "100%" }}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="tt-empty">Select a task to edit it or view completion history.</div>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
<details className="tt-manage">
|
||||
<summary>Tags & project totals</summary>
|
||||
<div className="tt-management">
|
||||
<section>
|
||||
<h3>Tags</h3>
|
||||
{tags.map((tag) => (
|
||||
<div className="tt-tag-edit" key={tag._qx.ref.$quixosRef}>
|
||||
<input
|
||||
aria-label={`Color for ${tag.name}`}
|
||||
type="color"
|
||||
value={/^#[0-9a-f]{6}$/i.test(tag.color) ? tag.color : "#557a68"}
|
||||
disabled={busy}
|
||||
onChange={(e) => {
|
||||
void editTag(tag, "color", e.target.value);
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
aria-label={`Rename ${tag.name}`}
|
||||
defaultValue={tag.name}
|
||||
key={tag.name}
|
||||
disabled={busy}
|
||||
onBlur={(e) => {
|
||||
if (e.target.value !== tag.name) void editTag(tag, "name", e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<form
|
||||
className="tt-add"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
void run(async () => {
|
||||
await (await actions())["addTag.call"](newTag);
|
||||
setNewTag("");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<input
|
||||
aria-label="New tag name"
|
||||
placeholder="New tag"
|
||||
value={newTag}
|
||||
onChange={(e) => setNewTag(e.target.value)}
|
||||
/>
|
||||
<button disabled={busy || !newTag.trim()}>Add tag</button>
|
||||
</form>
|
||||
</section>
|
||||
<section>
|
||||
<h3>Project totals</h3>
|
||||
{summary.status === "error" && <p role="alert">{summary.error.message}</p>}
|
||||
{totals?.groups.entries.map((row) => (
|
||||
<p key={row.group.status}>
|
||||
{row.group.status.toLowerCase()}: <strong>{String(row.aggregate.count)}</strong> ·{" "}
|
||||
{row.aggregate.sum.estimatedHours ?? 0}h estimated
|
||||
</p>
|
||||
))}
|
||||
<h4>Open tasks by tag</h4>
|
||||
{totals?.filter.expand.tags.groups.entries.map((row) => (
|
||||
<p key={row.group.target._qx.ref.$quixosRef}>
|
||||
<i style={{ background: row.group.target.color }} />
|
||||
{row.group.target.name}: {String(row.aggregate.count)} · {row.aggregate.sum.source.estimatedHours ?? 0}h
|
||||
</p>
|
||||
))}
|
||||
<small>A task can contribute to more than one tag. Project totals count each task once.</small>
|
||||
</section>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
+11
-22
@@ -1,24 +1,13 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { derived, type RuntimeContext } from "@quixos/camino-package-runtime";
|
||||
|
||||
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const componentSourcePath = path.join(dirname, "project-component.mjs");
|
||||
|
||||
export const source = async () => await fs.readFile(componentSourcePath, "utf8");
|
||||
|
||||
export const createComponent = (context: RuntimeContext) => context.objectId;
|
||||
|
||||
export const props = derived(async (context: RuntimeContext) => {
|
||||
const named = context.interface("port:project-react:named");
|
||||
const tasks = context.interface("port:project-react:tasks");
|
||||
return {
|
||||
project_id: named.objectId,
|
||||
component_id: context.objectId,
|
||||
name: await named.live("operation:named:name:get"),
|
||||
tasks: await tasks.invoke("operation:project-tasks:tasks:resolve"),
|
||||
};
|
||||
});
|
||||
import componentSource from "./project-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"]> => ({
|
||||
named: ports.named.objectId,
|
||||
tasks: ports.tasks.objectId,
|
||||
}),
|
||||
};
|
||||
export const stopWatch = () => null;
|
||||
|
||||
+16
-12
@@ -1,13 +1,17 @@
|
||||
// Generated by qx-scaffold-v1
|
||||
import { servePackageRuntime } from "@quixos/camino-package-runtime";
|
||||
import { createComponent, props, source, stopWatch } from "./project-react.impl.js";
|
||||
|
||||
servePackageRuntime({
|
||||
packageRevisionId: "package:project-react-runtime@1",
|
||||
exports: {
|
||||
"export:project-react-runtime:create-component": createComponent,
|
||||
"export:project-react-runtime:source-get": source,
|
||||
"export:project-react-runtime:props-get": props,
|
||||
"export:project-react-runtime:props-watch": props,
|
||||
"export:project-react-runtime:props-watch-stop": stopWatch,
|
||||
},
|
||||
});
|
||||
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,
|
||||
}),
|
||||
);
|
||||
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
.tt-board {
|
||||
height: 100%;
|
||||
min-height: 360px;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: #fcfcf8;
|
||||
color: #27372d;
|
||||
font:
|
||||
14px/1.45 system-ui,
|
||||
sans-serif;
|
||||
container-type: inline-size;
|
||||
}
|
||||
.tt-board * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.tt-board button,
|
||||
.tt-board input,
|
||||
.tt-board select {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
border: 1px solid #ced8cb;
|
||||
border-radius: 7px;
|
||||
background: #fff;
|
||||
padding: 8px 11px;
|
||||
min-width: 0;
|
||||
}
|
||||
.tt-board button {
|
||||
cursor: pointer;
|
||||
}
|
||||
.tt-board button:hover {
|
||||
background: #eef3e9;
|
||||
}
|
||||
.tt-board button:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: wait;
|
||||
}
|
||||
.tt-board button[aria-pressed="true"] {
|
||||
background: #e2eedb;
|
||||
border-color: #789569;
|
||||
}
|
||||
.tt-board :focus-visible {
|
||||
outline: 2px solid #42765a;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.tt-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 18px 20px 8px;
|
||||
}
|
||||
.tt-header input {
|
||||
font-size: 22px;
|
||||
font-weight: 650;
|
||||
border-color: transparent;
|
||||
background: transparent;
|
||||
flex: 1;
|
||||
}
|
||||
.tt-header span,
|
||||
.tt-row-meta,
|
||||
.tt-filter-note,
|
||||
.tt-management small {
|
||||
color: #647064;
|
||||
font-size: 12px;
|
||||
}
|
||||
.tt-add {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
.tt-add input {
|
||||
flex: 1;
|
||||
}
|
||||
.tt-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
padding: 8px 20px 14px;
|
||||
border-bottom: 1px solid #dce2d7;
|
||||
max-height: 160px;
|
||||
overflow: auto;
|
||||
}
|
||||
.tt-filters button {
|
||||
font-size: 12px;
|
||||
padding: 5px 9px;
|
||||
}
|
||||
.tt-board i {
|
||||
display: inline-block;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
flex: none;
|
||||
}
|
||||
.tt-filter-note {
|
||||
margin: 5px 20px;
|
||||
}
|
||||
.tt-body {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 2fr) minmax(280px, 3fr);
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.tt-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
border-right: 1px solid #dce2d7;
|
||||
}
|
||||
.tt-rows {
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.tt-board .tt-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
text-align: left;
|
||||
flex: none;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border: 0;
|
||||
border-bottom: 1px solid #e4e9df;
|
||||
border-radius: 0;
|
||||
padding: 14px 18px;
|
||||
background: transparent;
|
||||
}
|
||||
.tt-board .tt-row.is-selected {
|
||||
background: #eaf1e4;
|
||||
box-shadow: inset 3px 0 #557a46;
|
||||
}
|
||||
.tt-row-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.tt-row strong {
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.tt-status {
|
||||
font-size: 20px;
|
||||
line-height: 1.2;
|
||||
color: #62804f;
|
||||
}
|
||||
.tt-tag-line {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.tt-tag {
|
||||
background: #f1f3ed;
|
||||
border: 1px solid #dce3d4;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
padding: 1px 5px;
|
||||
}
|
||||
.tt-page,
|
||||
.tt-detail-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 8px 12px;
|
||||
flex: none;
|
||||
border-top: 1px solid #dce2d7;
|
||||
font-size: 12px;
|
||||
}
|
||||
.tt-detail-bar {
|
||||
border-top: 0;
|
||||
border-bottom: 1px solid #dce2d7;
|
||||
align-items: center;
|
||||
}
|
||||
.tt-detail-bar a {
|
||||
color: #42694b;
|
||||
}
|
||||
.tt-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
overflow: auto;
|
||||
background: #fff;
|
||||
}
|
||||
.tt-empty,
|
||||
.tt-notice {
|
||||
padding: 24px;
|
||||
color: #697568;
|
||||
}
|
||||
.tt-empty {
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
max-width: 300px;
|
||||
}
|
||||
.tt-error {
|
||||
padding: 10px 20px;
|
||||
background: #fff1ec;
|
||||
color: #933d2f;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.tt-manage {
|
||||
border-top: 1px solid #dce2d7;
|
||||
flex: none;
|
||||
max-height: 40%;
|
||||
overflow: auto;
|
||||
background: #f6f8f1;
|
||||
}
|
||||
.tt-manage summary {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tt-management {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
padding: 0 20px 18px;
|
||||
}
|
||||
.tt-management h3 {
|
||||
font-size: 14px;
|
||||
}
|
||||
.tt-management h4 {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.tt-management p {
|
||||
margin: 5px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
.tt-management .tt-add {
|
||||
padding: 10px 0;
|
||||
}
|
||||
.tt-tag-edit {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin: 6px 0;
|
||||
}
|
||||
.tt-tag-edit input[type="color"] {
|
||||
width: 40px;
|
||||
padding: 3px;
|
||||
height: 35px;
|
||||
}
|
||||
.tt-tag-edit input:not([type="color"]) {
|
||||
flex: 1;
|
||||
}
|
||||
@container (max-width:650px) {
|
||||
.tt-body {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.tt-body:not(.has-detail) .tt-detail,
|
||||
.tt-body.has-detail .tt-list {
|
||||
display: none;
|
||||
}
|
||||
.tt-management {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.tt-header {
|
||||
padding: 10px;
|
||||
}
|
||||
.tt-header span {
|
||||
display: none;
|
||||
}
|
||||
.tt-add,
|
||||
.tt-filters {
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -12,8 +12,7 @@
|
||||
"types": ["node"],
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"jsx": "react",
|
||||
"jsxFactory": "h"
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"]
|
||||
}
|
||||
|
||||
+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=a998690446ecf647e3af78e369492c1a3663b66d" = { filename = "@quixos-camino-package-runtime-https-683bcbf33c-c90d5d5c53.zip"; hash = "sha512-yQ1dXFMR2k+xbkka2rrhsAcrQFZlJAZEYM0hqwGzdV5RJ+fhQEh3BEgoPiL2yxrxulU4zg/jsj8BTBFRp1feyg=="; };
|
||||
"@quixos/camino-package-runtime@https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-package-runtime.git#commit=86933aa97f3ea386a4c5cfeb28386fd8d2722075" = { filename = "@quixos-camino-package-runtime-https-af3a6c1cce-afad508367.zip"; hash = "sha512-r61Qg2cI8jRSzUs4m5vOZl92VMkd/eQB0KedVjGLAkpnhg5dt1FAVptjUOcs746empJUj/HEIau5rq0a+hyRhA=="; };
|
||||
"@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=a998690446ecf647e3af78e369492c1a3663b66d":
|
||||
"@quixos/camino-package-runtime@https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-package-runtime.git#commit=86933aa97f3ea386a4c5cfeb28386fd8d2722075":
|
||||
version: 0.1.0
|
||||
resolution: "@quixos/camino-package-runtime@https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-package-runtime.git#commit=a998690446ecf647e3af78e369492c1a3663b66d"
|
||||
resolution: "@quixos/camino-package-runtime@https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-package-runtime.git#commit=86933aa97f3ea386a4c5cfeb28386fd8d2722075"
|
||||
dependencies:
|
||||
"@bufbuild/protobuf": "npm:^2.12.1"
|
||||
"@connectrpc/connect": "npm:^2.1.2"
|
||||
"@connectrpc/connect-node": "npm:^2.1.2"
|
||||
checksum: 10c0/c90d5d5c5311da4fb16e491adabae1b0072b40566524064460cd21ab01b3755e5127e7e14048770448283e22f6cb1af1ba5538ce0fe3b23f014c1151a757deca
|
||||
checksum: 10c0/afad50836708f23452cd4b389b9bce665f7654c91dfde401d0a79d56318b024a67860e5db75140569b6350e72cef8e9e9a92548ff1c421abb9aead1afa1c9184
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -228,7 +228,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@quixos/project-react-runtime@workspace:."
|
||||
dependencies:
|
||||
"@quixos/camino-package-runtime": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-package-runtime.git#commit=a998690446ecf647e3af78e369492c1a3663b66d"
|
||||
"@quixos/camino-package-runtime": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-package-runtime.git#commit=86933aa97f3ea386a4c5cfeb28386fd8d2722075"
|
||||
"@types/node": "npm:^24"
|
||||
"@types/react": "npm:^18.3.12"
|
||||
esbuild: "npm:^0.25.12"
|
||||
|
||||
Reference in New Issue
Block a user