makersclaw 0.5.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -7
- package/package.json +3 -2
- package/r/annotate.json +4 -4
- package/r/approval-request.json +3 -3
- package/r/audio-player.json +3 -3
- package/r/auth-form.json +2 -2
- package/r/board.json +4 -4
- package/r/calendar-view.json +4 -4
- package/r/cart.json +3 -3
- package/r/chart.json +3 -3
- package/r/chat.json +3 -3
- package/r/code-editor.json +4 -4
- package/r/comment-box.json +4 -4
- package/r/contact-section.json +3 -3
- package/r/core.json +3 -3
- package/r/csv-import.json +4 -4
- package/r/data-table.json +3 -3
- package/r/diagram.json +3 -3
- package/r/doc-editor.json +4 -4
- package/r/doc-page.json +4 -4
- package/r/email-render.json +2 -2
- package/r/faq.json +2 -2
- package/r/file-upload.json +3 -3
- package/r/flow-canvas.json +4 -4
- package/r/footer.json +3 -3
- package/r/gantt.json +4 -4
- package/r/job-panel.json +3 -3
- package/r/json-editor.json +4 -4
- package/r/map.json +5 -5
- package/r/menubar.json +2 -2
- package/r/mind-map.json +4 -4
- package/r/navigation-menu.json +2 -2
- package/r/order-detail.json +2 -2
- package/r/org-chart.json +4 -4
- package/r/page-builder.json +6 -6
- package/r/payment-methods.json +2 -2
- package/r/pdf-document.json +2 -2
- package/r/pdf-viewer.json +6 -6
- package/r/product-detail.json +3 -3
- package/r/product-grid.json +4 -4
- package/r/record-form.json +3 -3
- package/r/record-notes.json +5 -5
- package/r/registry.json +121 -121
- package/r/search-dialog.json +3 -3
- package/r/settings-list.json +3 -3
- package/r/setup-checklist.json +3 -3
- package/r/share-dialog.json +3 -3
- package/r/sidebar.json +2 -2
- package/r/signature-pad.json +3 -3
- package/r/sketch-canvas.json +5 -5
- package/r/spreadsheet.json +3 -3
- package/r/stat-cards.json +4 -4
- package/r/task-list.json +4 -4
- package/r/task-model.json +3 -3
- package/r/testimonials.json +2 -2
- package/r/transaction-list.json +3 -3
- package/r/whiteboard.json +4 -4
- package/registry/blocks/annotate/README.md +14 -5
- package/registry/blocks/approval-request/README.md +15 -8
- package/registry/blocks/audio-player/README.md +17 -10
- package/registry/blocks/board/README.md +13 -9
- package/registry/blocks/board/board.jsx +1 -1
- package/registry/blocks/calendar-view/README.md +28 -4
- package/registry/blocks/cart/README.md +14 -10
- package/registry/blocks/chart/README.md +14 -7
- package/registry/blocks/chat/README.md +43 -13
- package/registry/blocks/code-editor/README.md +4 -0
- package/registry/blocks/code-editor/code-editor.jsx +1 -1
- package/registry/blocks/comment-box/README.md +20 -13
- package/registry/blocks/contact-section/README.md +7 -6
- package/registry/blocks/csv-import/README.md +34 -12
- package/registry/blocks/data-table/README.md +41 -8
- package/registry/blocks/diagram/diagram.jsx +1 -1
- package/registry/blocks/doc-editor/README.md +18 -6
- package/registry/blocks/doc-editor/doc-editor.jsx +1 -1
- package/registry/blocks/doc-page/README.md +16 -6
- package/registry/blocks/file-upload/README.md +7 -1
- package/registry/blocks/flow-canvas/README.md +16 -5
- package/registry/blocks/flow-canvas/flow-canvas.jsx +1 -1
- package/registry/blocks/footer/README.md +4 -2
- package/registry/blocks/gantt/README.md +18 -12
- package/registry/blocks/job-panel/README.md +1 -1
- package/registry/blocks/json-editor/README.md +16 -4
- package/registry/blocks/map/README.md +19 -3
- package/registry/blocks/map/map-view.jsx +1 -1
- package/registry/blocks/map/maplibre.js +1 -1
- package/registry/blocks/mind-map/README.md +16 -5
- package/registry/blocks/org-chart/README.md +10 -7
- package/registry/blocks/page-builder/README.md +13 -6
- package/registry/blocks/page-builder/page-builder.jsx +1 -1
- package/registry/blocks/page-builder/page-view.jsx +1 -1
- package/registry/blocks/page-builder/page.js +1 -1
- package/registry/blocks/pdf-viewer/README.md +22 -20
- package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1 -1
- package/registry/blocks/pdf-viewer/pdfjs.js +1 -1
- package/registry/blocks/pdf-viewer/ssr-shim.js +1 -1
- package/registry/blocks/product-detail/README.md +13 -7
- package/registry/blocks/product-detail/demo.jsx +1 -1
- package/registry/blocks/product-grid/README.md +12 -10
- package/registry/blocks/record-form/README.md +26 -9
- package/registry/blocks/record-notes/README.md +42 -30
- package/registry/blocks/search-dialog/README.md +38 -7
- package/registry/blocks/settings-list/README.md +29 -17
- package/registry/blocks/setup-checklist/README.md +12 -14
- package/registry/blocks/share-dialog/README.md +1 -1
- package/registry/blocks/signature-pad/README.md +15 -4
- package/registry/blocks/sketch-canvas/README.md +13 -4
- package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1 -1
- package/registry/blocks/sketch-canvas/ssr-shim.js +3 -3
- package/registry/blocks/spreadsheet/README.md +17 -8
- package/registry/blocks/stat-cards/README.md +17 -8
- package/registry/blocks/task-list/README.md +14 -11
- package/registry/blocks/task-model/README.md +3 -3
- package/registry/blocks/transaction-list/README.md +15 -7
- package/registry/blocks/whiteboard/README.md +19 -13
- package/src/cli.mjs +65 -9
- package/src/install.mjs +360 -21
- package/src/lockfile.mjs +468 -0
- package/src/pm.mjs +473 -0
- package/src/registry.mjs +125 -1
- package/templates/next/browser/app/auto-refresh.jsx +67 -0
- package/templates/next/browser/app/layout.jsx +94 -0
- package/templates/next/browser/app/page.jsx +60 -0
- package/templates/next/browser/app/s/[key]/page.jsx +95 -0
- package/templates/next/browser/lib/actions.js +16 -0
- package/templates/next/browser/lib/data.js +43 -0
- package/templates/next/files/README.md +818 -82
- package/templates/next/files/app/claw-alive/route.js +2 -2
- package/templates/next/files/app/frame.js +134 -9
- package/templates/next/files/app/layout.jsx +2 -2
- package/templates/next/files/check/run.mjs +1491 -53
- package/templates/next/files/components/ui/item.jsx +2 -2
- package/templates/next/files/components/ui/notice.jsx +1 -1
- package/templates/next/files/components/ui/separator.jsx +1 -1
- package/templates/next/files/lib/actions.js +171 -52
- package/templates/next/files/lib/brain-server.js +96 -0
- package/templates/next/files/lib/brain.js +413 -0
- package/templates/next/files/lib/claw.js +11 -0
- package/templates/next/files/lib/data.js +120 -30
- package/templates/next/files/lib/db.js +97 -17
- package/templates/next/files/lib/door.js +680 -0
- package/templates/next/files/next.config.mjs +85 -9
- package/templates/next/files/package.json +3 -3
- package/templates/next/files/{middleware.js → proxy.js} +13 -6
- package/templates/next/files/surfaces.js +3 -2
- package/templates/next/pnpm-lock.yaml +5870 -0
- package/templates/next/template.json +13 -6
- package/templates/next/files/schema.sql +0 -22
|
@@ -84,37 +84,37 @@ quantities by `key`.
|
|
|
84
84
|
## Example
|
|
85
85
|
|
|
86
86
|
```jsx
|
|
87
|
-
// surfaces/shop.jsx —
|
|
88
|
-
|
|
87
|
+
// surfaces/shop.jsx — client: read, hand over
|
|
88
|
+
"use client";
|
|
89
|
+
import { useRows } from "../lib/data";
|
|
89
90
|
import Shop from "../components/Shop";
|
|
90
91
|
|
|
91
|
-
export default
|
|
92
|
-
const { rows, error } =
|
|
93
|
-
return <Shop products={rows} error={error} />;
|
|
92
|
+
export default function ShopSurface() {
|
|
93
|
+
const { rows, error, loading, reload } = useRows({ source: "products", orderBy: "created_at desc" });
|
|
94
|
+
return <Shop products={rows} loading={loading} error={error} onRetry={reload} />;
|
|
94
95
|
}
|
|
95
96
|
```
|
|
96
97
|
|
|
97
98
|
```jsx
|
|
98
99
|
// components/Shop.jsx — client: the grid and the cart write
|
|
99
100
|
"use client";
|
|
100
|
-
import { useRouter } from "next/navigation";
|
|
101
101
|
import { insertRow } from "../lib/actions";
|
|
102
102
|
import { ProductGrid } from "./blocks/product-grid";
|
|
103
103
|
|
|
104
|
-
export default function Shop({ products, error }) {
|
|
105
|
-
const router = useRouter();
|
|
104
|
+
export default function Shop({ products, loading, error, onRetry }) {
|
|
106
105
|
return (
|
|
107
106
|
<ProductGrid
|
|
108
107
|
products={products}
|
|
108
|
+
loading={loading}
|
|
109
109
|
error={error}
|
|
110
|
-
onRetry={
|
|
110
|
+
onRetry={onRetry}
|
|
111
111
|
filters={[
|
|
112
112
|
{ key: "category", label: "Category", options: ["Shirts", "Sweaters", "Jackets"] },
|
|
113
113
|
{ key: "size", label: "Size", options: ["S", "M", "L", "XL"] },
|
|
114
114
|
]}
|
|
115
115
|
sort
|
|
116
116
|
onAddToCart={(line) =>
|
|
117
|
-
insertRow({ source: "
|
|
117
|
+
insertRow({ source: "cart_items", values: { product_id: line.id, line_key: line.key, quantity: 1 }, detail: line.name })
|
|
118
118
|
}
|
|
119
119
|
label="Shop"
|
|
120
120
|
/>
|
|
@@ -122,6 +122,8 @@ export default function Shop({ products, error }) {
|
|
|
122
122
|
}
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
126
|
+
|
|
125
127
|
Collection tiles, and a lead picture beside four products:
|
|
126
128
|
|
|
127
129
|
```jsx
|
|
@@ -93,7 +93,7 @@ Validation, first broken rule wins, one message per field:
|
|
|
93
93
|
error?, errors? }` where `errors` is `{ fieldName: "message" }` — each
|
|
94
94
|
shows under its field and the first one is focused. A throw counts as
|
|
95
95
|
failed. In a Makersclaw app it is one line over `updateRow`:
|
|
96
|
-
`onSave={(v) => updateRow({ source: "
|
|
96
|
+
`onSave={(v) => updateRow({ source: "products", id: p.id, patch: v })}`.
|
|
97
97
|
- `container` — `"card"` (default: a Card with `title` and `description`)
|
|
98
98
|
· `"page"` (no box: a page heading, then the sections) · `"dialog"` (a
|
|
99
99
|
centred Sheet; on a phone a bottom sheet). A dialog needs `open` and
|
|
@@ -127,10 +127,12 @@ Validation, first broken rule wins, one message per field:
|
|
|
127
127
|
## Example
|
|
128
128
|
|
|
129
129
|
```jsx
|
|
130
|
-
//
|
|
130
|
+
// surfaces/product.jsx — client: one product, edited
|
|
131
131
|
"use client";
|
|
132
|
-
import {
|
|
133
|
-
import {
|
|
132
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
133
|
+
import { useRow } from "../lib/data";
|
|
134
|
+
import { deleteRow, updateRow } from "../lib/actions";
|
|
135
|
+
import { RecordForm } from "../components/blocks/record-form";
|
|
134
136
|
|
|
135
137
|
const FIELDS = [
|
|
136
138
|
{ name: "name", label: "Product name", required: true },
|
|
@@ -140,20 +142,35 @@ const FIELDS = [
|
|
|
140
142
|
{ name: "details", label: "Details", type: "textarea" },
|
|
141
143
|
];
|
|
142
144
|
|
|
143
|
-
export default function ProductEditor(
|
|
145
|
+
export default function ProductEditor() {
|
|
146
|
+
const router = useRouter();
|
|
147
|
+
const id = useSearchParams().get("row");
|
|
148
|
+
const { row: product, loading, error, reload } = useRow({ source: "products", id });
|
|
144
149
|
return (
|
|
145
150
|
<RecordForm
|
|
146
151
|
title="Edit product"
|
|
147
152
|
fields={FIELDS}
|
|
148
|
-
values={product}
|
|
149
|
-
|
|
150
|
-
|
|
153
|
+
values={product ?? {}}
|
|
154
|
+
loading={loading}
|
|
155
|
+
error={error}
|
|
156
|
+
onRetry={reload}
|
|
157
|
+
onSave={(values) => updateRow({ source: "products", id, patch: values })}
|
|
158
|
+
onDelete={async () => {
|
|
159
|
+
const res = await deleteRow({ source: "products", id });
|
|
160
|
+
if (res.ok) router.push("/s/products");
|
|
161
|
+
return res;
|
|
162
|
+
}}
|
|
151
163
|
deleteLabel="Remove product"
|
|
152
164
|
/>
|
|
153
165
|
);
|
|
154
166
|
}
|
|
155
167
|
```
|
|
156
168
|
|
|
169
|
+
After a save the row is read again and comes back as `values`: the
|
|
170
|
+
form starts over from what it just saved.
|
|
171
|
+
|
|
172
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
173
|
+
|
|
157
174
|
In a dialog, from a list:
|
|
158
175
|
|
|
159
176
|
```jsx
|
|
@@ -170,7 +187,7 @@ const [editing, setEditing] = useState(null);
|
|
|
170
187
|
{ title: "Access", fields: [{ name: "role", label: "Role", type: "radio", options: ["Admin", "Member", "Viewer"] }] },
|
|
171
188
|
]}
|
|
172
189
|
onSave={async (values) => {
|
|
173
|
-
const res = await updateRow({ source: "
|
|
190
|
+
const res = await updateRow({ source: "members", id: editing.id, patch: values });
|
|
174
191
|
return res.ok ? res : { ok: false, errors: res.error?.includes("email") ? { email: "Already in use." } : undefined, error: res.error };
|
|
175
192
|
}}
|
|
176
193
|
/>
|
|
@@ -73,45 +73,57 @@ One table, a row per note: `record_id`, `author_id`, `body jsonb`,
|
|
|
73
73
|
## Example
|
|
74
74
|
|
|
75
75
|
```jsx
|
|
76
|
-
//
|
|
76
|
+
// surfaces/deal-notes.jsx — client: one deal's notes
|
|
77
77
|
"use client";
|
|
78
|
-
import {
|
|
78
|
+
import { useSearchParams } from "next/navigation";
|
|
79
|
+
import { useArtifact } from "../lib/hooks";
|
|
80
|
+
import { useRows, whenLanded } from "../lib/data";
|
|
79
81
|
import { deleteRow, insertRow, updateRow } from "../lib/actions";
|
|
80
|
-
import { RecordNotes } from "
|
|
81
|
-
import { docToText } from "
|
|
82
|
+
import { RecordNotes } from "../components/blocks/record-notes";
|
|
83
|
+
import { docToText } from "../components/blocks/doc-editor";
|
|
82
84
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
const source = "deal_notes";
|
|
86
|
+
|
|
87
|
+
// A write answers once the notes are read again, so the list never shows
|
|
88
|
+
// the notes from before it.
|
|
89
|
+
async function landed(res) {
|
|
90
|
+
if (res.ok) await whenLanded();
|
|
91
|
+
return res;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default function DealNotes() {
|
|
95
|
+
const dealId = useSearchParams().get("row");
|
|
96
|
+
const { user } = useArtifact(); // the member signed in: { id, email }
|
|
97
|
+
const { rows, loading } = useRows({ source, filter: { deal_id: dealId }, orderBy: "created_at desc" });
|
|
98
|
+
const { rows: team } = useRows({ source: "people", orderBy: "name asc" });
|
|
99
|
+
const me = { id: user?.email, name: user?.email ?? "You" };
|
|
100
|
+
// created_by: a before-insert trigger sets it from the member, never the page (the app's README §5)
|
|
101
|
+
const notes = rows.map((n) => ({
|
|
102
|
+
id: n.id,
|
|
103
|
+
author: { id: n.created_by, name: n.created_by },
|
|
104
|
+
body: n.body,
|
|
105
|
+
createdAt: n.created_at,
|
|
106
|
+
updatedAt: n.updated_at,
|
|
107
|
+
}));
|
|
85
108
|
const findPeople = async (q) =>
|
|
86
109
|
team.filter((p) => p.name.toLowerCase().includes(q.toLowerCase())).map((p) => ({ id: p.id, label: p.name }));
|
|
87
110
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const updatedAt = new Date();
|
|
100
|
-
const res = await updateRow({ source: "public.deal_notes", id: note.id, patch: { body: doc, body_text: docToText(doc), updated_at: updatedAt } });
|
|
101
|
-
if (res.ok) setNotes((n) => n.map((x) => (x.id === note.id ? { ...x, body: doc, updatedAt } : x)));
|
|
102
|
-
return res;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async function remove(note) {
|
|
106
|
-
const res = await deleteRow({ source: "public.deal_notes", id: note.id });
|
|
107
|
-
if (res.ok) setNotes((n) => n.filter((x) => x.id !== note.id));
|
|
108
|
-
return res;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return <RecordNotes notes={notes} me={me} onAdd={add} onEdit={edit} onDelete={remove} mentions={findPeople} />;
|
|
111
|
+
if (loading) return null;
|
|
112
|
+
return (
|
|
113
|
+
<RecordNotes
|
|
114
|
+
notes={notes}
|
|
115
|
+
me={me}
|
|
116
|
+
onAdd={(doc) => insertRow({ source, values: { deal_id: dealId, body: doc, body_text: docToText(doc) } }).then(landed)}
|
|
117
|
+
onEdit={(note, doc) => updateRow({ source, id: note.id, patch: { body: doc, body_text: docToText(doc), updated_at: new Date() } }).then(landed)}
|
|
118
|
+
onDelete={(note) => deleteRow({ source, id: note.id }).then(landed)}
|
|
119
|
+
mentions={findPeople}
|
|
120
|
+
/>
|
|
121
|
+
);
|
|
112
122
|
}
|
|
113
123
|
```
|
|
114
124
|
|
|
125
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
126
|
+
|
|
115
127
|
Beside the form, on a record page:
|
|
116
128
|
|
|
117
129
|
```jsx
|
|
@@ -70,15 +70,29 @@ Both components take these.
|
|
|
70
70
|
## Example
|
|
71
71
|
|
|
72
72
|
```jsx
|
|
73
|
+
// components/GlobalSearch.jsx — client: search as the member types
|
|
73
74
|
"use client";
|
|
74
|
-
import { useRouter } from "next/navigation";
|
|
75
75
|
import { useState } from "react";
|
|
76
|
-
import {
|
|
77
|
-
import {
|
|
76
|
+
import { useRouter } from "next/navigation";
|
|
77
|
+
import { loadRows, useRows } from "../lib/data";
|
|
78
|
+
import { Button } from "./ui";
|
|
79
|
+
import { SearchDialog } from "./blocks/search-dialog";
|
|
80
|
+
|
|
81
|
+
const asResult = (d) => ({ id: d.id, kind: "deal", title: d.name, snippet: d.stage });
|
|
78
82
|
|
|
79
|
-
export function GlobalSearch(
|
|
83
|
+
export default function GlobalSearch() {
|
|
80
84
|
const router = useRouter();
|
|
81
85
|
const [open, setOpen] = useState(false);
|
|
86
|
+
const { rows: recent } = useRows({ source: "deals", orderBy: "updated_at desc", limit: 5 });
|
|
87
|
+
|
|
88
|
+
// The app's own table: names that start with what was typed, case ignored.
|
|
89
|
+
// The dialog aborts the older query through `signal`.
|
|
90
|
+
async function search(q, { signal }) {
|
|
91
|
+
const { rows, error } = await loadRows({ source: "deals", filter: { name: { prefix: q } }, orderBy: "name asc", limit: 20, signal });
|
|
92
|
+
if (error) throw new Error(error); // "Search failed"; an aborted read's answer is ignored
|
|
93
|
+
return rows.map(asResult);
|
|
94
|
+
}
|
|
95
|
+
|
|
82
96
|
return (
|
|
83
97
|
<>
|
|
84
98
|
<Button icon="search" onClick={() => setOpen(true)}>Search</Button>
|
|
@@ -86,15 +100,32 @@ export function GlobalSearch({ recent, search }) {
|
|
|
86
100
|
open={open}
|
|
87
101
|
onOpenChange={setOpen}
|
|
88
102
|
hotkey="k"
|
|
89
|
-
sections={[{ heading: "Recent", results: recent }]}
|
|
90
|
-
onSearch={
|
|
91
|
-
onSelect={(r) => router.push(
|
|
103
|
+
sections={[{ heading: "Recent", results: recent.map(asResult) }]}
|
|
104
|
+
onSearch={search}
|
|
105
|
+
onSelect={(r) => router.push(`/s/deals?row=${r.id}`)}
|
|
92
106
|
/>
|
|
93
107
|
</>
|
|
94
108
|
);
|
|
95
109
|
}
|
|
96
110
|
```
|
|
97
111
|
|
|
112
|
+
The company's records, through the brain (`lib/brain.js`), with
|
|
113
|
+
`meaning: false` while a person types: keywords only, nothing embedded
|
|
114
|
+
or charged. A search by meaning is charged per call; keep it for a
|
|
115
|
+
search page or an Enter.
|
|
116
|
+
|
|
117
|
+
```jsx
|
|
118
|
+
import { searchBrain } from "../lib/brain";
|
|
119
|
+
|
|
120
|
+
async function search(q, { signal }) {
|
|
121
|
+
const { results, error } = await searchBrain({ q, meaning: false, limit: 20, signal });
|
|
122
|
+
if (error) throw new Error(error);
|
|
123
|
+
return results.map(({ record, snippet }) => ({ id: record.id, kind: record.kind, title: record.label, snippet }));
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
128
|
+
|
|
98
129
|
## What it will not do
|
|
99
130
|
|
|
100
131
|
- Index anything. It searches the list you give it, or asks your server.
|
|
@@ -71,7 +71,7 @@ The matrix (`variant="matrix"`) rows are events:
|
|
|
71
71
|
false, error }`; a throw counts as failed. Without it, switches and
|
|
72
72
|
selects are shown and cannot be changed. In a Makersclaw app it is one
|
|
73
73
|
line over `saveSetting` (lib/actions.js):
|
|
74
|
-
`onChange={(row, value) => saveSetting({ source: "
|
|
74
|
+
`onChange={(row, value) => saveSetting({ source: "settings", key: row.key, value })}`.
|
|
75
75
|
- `onAction(row, actionKey)` — run a button, a menu item or a remove.
|
|
76
76
|
Return `{ ok }` like `onChange`. Remove the row from `rows` on success
|
|
77
77
|
when it is gone (a signed-out device).
|
|
@@ -95,31 +95,43 @@ The matrix (`variant="matrix"`) rows are events:
|
|
|
95
95
|
## Example
|
|
96
96
|
|
|
97
97
|
```jsx
|
|
98
|
-
//
|
|
98
|
+
// surfaces/preferences.jsx — client: settings, read and saved
|
|
99
99
|
"use client";
|
|
100
|
+
import { useMemo } from "react";
|
|
101
|
+
import { useRows } from "../lib/data";
|
|
100
102
|
import { saveSetting } from "../lib/actions";
|
|
101
|
-
import { SettingsList } from "
|
|
102
|
-
|
|
103
|
-
export default function Preferences(
|
|
104
|
-
const rows =
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
label: "
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
103
|
+
import { SettingsList } from "../components/blocks/settings-list";
|
|
104
|
+
|
|
105
|
+
export default function Preferences() {
|
|
106
|
+
const { rows: saved, loading, error, reload } = useRows({ source: "settings" });
|
|
107
|
+
// Built once per read: until `rows` changes, a row shows the value it saved.
|
|
108
|
+
const rows = useMemo(() => {
|
|
109
|
+
const settings = Object.fromEntries(saved.map((s) => [s.key, s.value]));
|
|
110
|
+
return [
|
|
111
|
+
{ key: "digest", icon: "mail", label: "Weekly digest", description: "Every Monday at 8:00.", control: { type: "switch", value: settings.digest === true } },
|
|
112
|
+
{
|
|
113
|
+
key: "currency",
|
|
114
|
+
icon: "dollar",
|
|
115
|
+
label: "Currency",
|
|
116
|
+
control: { type: "select", value: settings.currency ?? "usd", options: [{ value: "usd", label: "US dollar" }, { value: "eur", label: "Euro" }] },
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
}, [saved]);
|
|
113
120
|
return (
|
|
114
121
|
<SettingsList
|
|
115
122
|
title="Preferences"
|
|
116
123
|
rows={rows}
|
|
117
|
-
|
|
124
|
+
loading={loading}
|
|
125
|
+
error={error}
|
|
126
|
+
onRetry={reload}
|
|
127
|
+
onChange={(row, value) => saveSetting({ source: "settings", key: row.key, value })}
|
|
118
128
|
/>
|
|
119
129
|
);
|
|
120
130
|
}
|
|
121
131
|
```
|
|
122
132
|
|
|
133
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
134
|
+
|
|
123
135
|
Notifications per channel:
|
|
124
136
|
|
|
125
137
|
```jsx
|
|
@@ -131,7 +143,7 @@ Notifications per channel:
|
|
|
131
143
|
{ key: "mentions", group: "Conversations", label: "Mentions", values: { email: true, push: true } },
|
|
132
144
|
{ key: "signin", group: "Security", label: "New sign-in", values: { email: true, push: false }, locked: ["email"] },
|
|
133
145
|
]}
|
|
134
|
-
onChange={(row, on, channel) => saveSetting({ mode: "json", source: "
|
|
146
|
+
onChange={(row, on, channel) => saveSetting({ mode: "json", source: "members", column: "notify", id: me.id, key: `${row.key}.${channel}`, value: on })}
|
|
135
147
|
/>
|
|
136
148
|
```
|
|
137
149
|
|
|
@@ -149,7 +161,7 @@ Signed-in devices, each with sign-out:
|
|
|
149
161
|
meta: [{ icon: "globe", text: s.place }, { icon: "clock", text: s.lastSeen }],
|
|
150
162
|
control: s.current ? null : { type: "remove", label: `Sign out ${s.device}`, confirm: { title: "Sign out this device?", confirmLabel: "Sign out" } },
|
|
151
163
|
}))}
|
|
152
|
-
onAction={(row) => deleteRow({ source: "
|
|
164
|
+
onAction={(row) => deleteRow({ source: "sessions", id: row.key })}
|
|
153
165
|
/>
|
|
154
166
|
```
|
|
155
167
|
|
|
@@ -75,16 +75,15 @@ With no steps it says "Nothing to set up".
|
|
|
75
75
|
## Example
|
|
76
76
|
|
|
77
77
|
```jsx
|
|
78
|
-
// surfaces/home.jsx —
|
|
79
|
-
|
|
78
|
+
// surfaces/home.jsx — client: work out what is done
|
|
79
|
+
"use client";
|
|
80
|
+
import { useCount } from "../lib/data";
|
|
80
81
|
import GettingStarted from "../components/GettingStarted";
|
|
81
82
|
|
|
82
|
-
export default
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
]);
|
|
87
|
-
return <GettingStarted invited={members.length > 1} hasDeal={deals.length > 0} />;
|
|
83
|
+
export default function Home() {
|
|
84
|
+
const members = useCount({ source: "members" });
|
|
85
|
+
const deals = useCount({ source: "deals" });
|
|
86
|
+
return <GettingStarted loading={members.loading || deals.loading} invited={members.count > 1} hasDeal={deals.count > 0} />;
|
|
88
87
|
}
|
|
89
88
|
```
|
|
90
89
|
|
|
@@ -93,10 +92,11 @@ export default async function Home() {
|
|
|
93
92
|
"use client";
|
|
94
93
|
import { SetupChecklist } from "./blocks/setup-checklist";
|
|
95
94
|
|
|
96
|
-
export default function GettingStarted({ invited, hasDeal, onHide }) {
|
|
95
|
+
export default function GettingStarted({ loading, invited, hasDeal, onHide }) {
|
|
97
96
|
return (
|
|
98
97
|
<SetupChecklist
|
|
99
98
|
title="Get started"
|
|
99
|
+
loading={loading}
|
|
100
100
|
steps={[
|
|
101
101
|
{ key: "invite", title: "Invite your team", description: "Bring in the people who own accounts.", actionLabel: "Invite", href: "/team", done: invited },
|
|
102
102
|
{ key: "deal", title: "Add your first deal", description: "One deal is enough to see the pipeline work.", actionLabel: "Add a deal", href: "/deals?new=1", done: hasDeal },
|
|
@@ -107,6 +107,8 @@ export default function GettingStarted({ invited, hasDeal, onHide }) {
|
|
|
107
107
|
}
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
111
|
+
|
|
110
112
|
In order, with buttons that do the work in place:
|
|
111
113
|
|
|
112
114
|
```jsx
|
|
@@ -115,11 +117,7 @@ In order, with buttons that do the work in place:
|
|
|
115
117
|
sequential
|
|
116
118
|
title="Quick start"
|
|
117
119
|
steps={steps}
|
|
118
|
-
onAction={
|
|
119
|
-
const res = await runSetupStep(step.key); // your server action, { ok, error }
|
|
120
|
-
if (res.ok) router.refresh(); // the step comes back done
|
|
121
|
-
return res;
|
|
122
|
-
}}
|
|
120
|
+
onAction={(step) => runAction({ name: "run_setup_step", args: { key: step.key } })} // an action named in references/actions.md; its ok re-reads every hook, so the step comes back done
|
|
123
121
|
/>
|
|
124
122
|
```
|
|
125
123
|
|
|
@@ -89,7 +89,7 @@ export function ShareProject({ project, members, invite, setRole, remove }) {
|
|
|
89
89
|
onClose={() => setOpen(false)}
|
|
90
90
|
title={`Share “${project.name}”`}
|
|
91
91
|
people={members}
|
|
92
|
-
onInvite={(emails, role) => invite(project.id, emails, role)} //
|
|
92
|
+
onInvite={(emails, role) => invite(project.id, emails, role)} // each answers { ok } or { ok: false, error }, as lib/actions' writes do
|
|
93
93
|
onRoleChange={(person, role) => setRole(project.id, person.id, role)}
|
|
94
94
|
onRemove={(person) => remove(project.id, person.id)}
|
|
95
95
|
link={`https://app.example.com/p/${project.id}`}
|
|
@@ -109,25 +109,36 @@ under the paper says so. Undo and clear are buttons with names.
|
|
|
109
109
|
Accepting a quote in a Makersclaw app.
|
|
110
110
|
|
|
111
111
|
```jsx
|
|
112
|
+
// surfaces/quote.jsx — client: one quote, accepted and signed
|
|
112
113
|
"use client";
|
|
114
|
+
import { useSearchParams } from "next/navigation";
|
|
115
|
+
import { useArtifact } from "../lib/hooks";
|
|
116
|
+
import { useRow } from "../lib/data";
|
|
113
117
|
import { updateRow } from "../lib/actions";
|
|
114
|
-
import { SignaturePad } from "
|
|
118
|
+
import { SignaturePad } from "../components/blocks/signature-pad";
|
|
115
119
|
|
|
116
|
-
export default function AcceptQuote(
|
|
120
|
+
export default function AcceptQuote() {
|
|
121
|
+
const id = useSearchParams().get("row");
|
|
122
|
+
const { user } = useArtifact(); // the member signed in: { id, email }
|
|
123
|
+
const { row: quote, loading } = useRow({ source: "quotes", id });
|
|
124
|
+
if (loading || !quote) return null;
|
|
117
125
|
return (
|
|
118
126
|
<SignaturePad
|
|
119
|
-
signer={
|
|
127
|
+
signer={user?.email}
|
|
120
128
|
value={quote.signature ?? undefined}
|
|
121
129
|
submitLabel="Accept and sign"
|
|
122
130
|
note="Signing records that you accept this quote. It is not a certified e-signature."
|
|
131
|
+
// signed_by: a before-update trigger sets it from the member, never the page (the app's README §5)
|
|
123
132
|
onSubmit={(signature) =>
|
|
124
|
-
updateRow({ source: "
|
|
133
|
+
updateRow({ source: "quotes", id: quote.id, patch: { status: "accepted", signature }, detail: "accepted" })
|
|
125
134
|
}
|
|
126
135
|
/>
|
|
127
136
|
);
|
|
128
137
|
}
|
|
129
138
|
```
|
|
130
139
|
|
|
140
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
141
|
+
|
|
131
142
|
## What it will not do
|
|
132
143
|
|
|
133
144
|
- Verify who signed, send the document for signature, or keep an audit
|
|
@@ -215,22 +215,31 @@ on a partial one:
|
|
|
215
215
|
A whiteboard that saves itself:
|
|
216
216
|
|
|
217
217
|
```jsx
|
|
218
|
+
// surfaces/sketch.jsx — client: one board, saved as it is drawn
|
|
218
219
|
"use client";
|
|
219
|
-
import {
|
|
220
|
+
import { useSearchParams } from "next/navigation";
|
|
221
|
+
import { useRow } from "../lib/data";
|
|
220
222
|
import { updateRow } from "../lib/actions";
|
|
223
|
+
import { SketchCanvas } from "../components/blocks/sketch-canvas";
|
|
221
224
|
|
|
222
|
-
export default function Board(
|
|
225
|
+
export default function Board() {
|
|
226
|
+
const id = useSearchParams().get("row");
|
|
227
|
+
const { row: board, loading } = useRow({ source: "boards", id });
|
|
228
|
+
if (loading || !board) return null;
|
|
223
229
|
return (
|
|
224
230
|
<SketchCanvas
|
|
231
|
+
key={board.id}
|
|
225
232
|
label={board.title}
|
|
226
|
-
defaultValue={board.sketch} // jsonb, may be null
|
|
227
|
-
onSave={(sketch) => updateRow({ source: "
|
|
233
|
+
defaultValue={board.sketch} // jsonb, may be null; read once per board
|
|
234
|
+
onSave={(sketch) => updateRow({ source: "boards", id: board.id, patch: { sketch } })}
|
|
228
235
|
className="h-dvh rounded-none border-0"
|
|
229
236
|
/>
|
|
230
237
|
);
|
|
231
238
|
}
|
|
232
239
|
```
|
|
233
240
|
|
|
241
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
242
|
+
|
|
234
243
|
Marking up a screenshot, with numbered notes:
|
|
235
244
|
|
|
236
245
|
```jsx
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// SketchCanvas — Excalidraw, in the workspace theme, behind one set of props.
|
|
4
4
|
//
|
|
5
|
-
// Client only, one
|
|
5
|
+
// Client only, one bundle. Excalidraw needs the DOM, and the app's
|
|
6
6
|
// check refuses import() (so no next/dynamic): the modules load with the
|
|
7
7
|
// page (on the server too — see excalidraw.js and ssr-shim.js for how that
|
|
8
8
|
// is made safe), and the canvas itself mounts only after the first effect
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
// devicePixelRatio, document.createElement("img"), an Element.prototype
|
|
6
6
|
// polyfill), so evaluating it on the server throws "window is not defined"
|
|
7
7
|
// and the page fails to render — even though the component itself only
|
|
8
|
-
// ever renders in the browser (the mount guard). The app is one
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// ever renders in the browser (the mount guard). The app is one bundle
|
|
9
|
+
// (no import(), no next/dynamic), so the module cannot be kept off the
|
|
10
|
+
// server; instead it is let load against these stand-ins.
|
|
11
11
|
//
|
|
12
12
|
// The stand-ins live only for that load: excalidraw.js imports this file,
|
|
13
13
|
// then @excalidraw/excalidraw, then ssr-shim-end.js, which removes them —
|
|
@@ -241,10 +241,12 @@ Shift+F10 the row menu.
|
|
|
241
241
|
## Example
|
|
242
242
|
|
|
243
243
|
```jsx
|
|
244
|
+
// surfaces/deals.jsx — client: the deals as a grid
|
|
244
245
|
"use client";
|
|
245
246
|
import { useState } from "react";
|
|
246
|
-
import {
|
|
247
|
-
import {
|
|
247
|
+
import { useRows } from "../lib/data";
|
|
248
|
+
import { deleteRow, insertRow, updateRow } from "../lib/actions";
|
|
249
|
+
import { Spreadsheet } from "../components/blocks/spreadsheet";
|
|
248
250
|
|
|
249
251
|
const COLUMNS = [
|
|
250
252
|
{ key: "name", name: "Name", type: "text", required: true },
|
|
@@ -253,20 +255,25 @@ const COLUMNS = [
|
|
|
253
255
|
{ key: "close", name: "Close date", type: "date" },
|
|
254
256
|
];
|
|
255
257
|
|
|
256
|
-
export default function Deals(
|
|
258
|
+
export default function Deals() {
|
|
259
|
+
const deals = useRows({ source: "deals", orderBy: "created_at asc" });
|
|
260
|
+
const team = useRows({ source: "people", orderBy: "name asc" });
|
|
257
261
|
const [view, setView] = useState({ sort: [{ key: "amount", dir: "desc" }] });
|
|
262
|
+
// Each change below is one write, and the door takes 300 a minute: a
|
|
263
|
+
// paste larger than that belongs in one action (runAction).
|
|
258
264
|
return (
|
|
259
265
|
<Spreadsheet
|
|
260
266
|
label="Deals"
|
|
261
267
|
noun="deal"
|
|
262
|
-
rows={deals}
|
|
268
|
+
rows={deals.rows}
|
|
269
|
+
loading={deals.loading}
|
|
263
270
|
columns={COLUMNS}
|
|
264
|
-
people={team}
|
|
271
|
+
people={team.rows}
|
|
265
272
|
view={view}
|
|
266
273
|
onViewChange={setView}
|
|
267
274
|
onChange={async (changes) => {
|
|
268
275
|
for (const c of changes) {
|
|
269
|
-
const r = await updateRow({ source: "
|
|
276
|
+
const r = await updateRow({ source: "deals", id: c.rowId, patch: { [c.key]: c.value } });
|
|
270
277
|
if (!r.ok) return r;
|
|
271
278
|
}
|
|
272
279
|
return { ok: true };
|
|
@@ -274,7 +281,7 @@ export default function Deals({ deals, team }) {
|
|
|
274
281
|
onAddRows={async (rows) => {
|
|
275
282
|
const made = [];
|
|
276
283
|
for (const { id, ...values } of rows) {
|
|
277
|
-
const r = await insertRow({ source: "
|
|
284
|
+
const r = await insertRow({ source: "deals", values: { stage: "lead", ...values } });
|
|
278
285
|
if (!r.ok) return r;
|
|
279
286
|
made.push({ id: r.id }); // the database's id replaces the one the grid made up
|
|
280
287
|
}
|
|
@@ -282,7 +289,7 @@ export default function Deals({ deals, team }) {
|
|
|
282
289
|
}}
|
|
283
290
|
onDeleteRows={async (ids) => {
|
|
284
291
|
for (const id of ids) {
|
|
285
|
-
const r = await deleteRow({ source: "
|
|
292
|
+
const r = await deleteRow({ source: "deals", id });
|
|
286
293
|
if (!r.ok) return r;
|
|
287
294
|
}
|
|
288
295
|
return { ok: true };
|
|
@@ -292,6 +299,8 @@ export default function Deals({ deals, team }) {
|
|
|
292
299
|
}
|
|
293
300
|
```
|
|
294
301
|
|
|
302
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
303
|
+
|
|
295
304
|
## What it will not do
|
|
296
305
|
|
|
297
306
|
- No formulas. A value is what was typed; a derived column is computed by
|