makersclaw 0.6.0 → 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 +48 -8
- package/package.json +1 -1
- 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 +31 -4
- package/src/install.mjs +263 -21
- package/src/registry.mjs +78 -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 +810 -79
- package/templates/next/files/app/frame.js +128 -4
- package/templates/next/files/check/run.mjs +1438 -41
- 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 +170 -51
- 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 +119 -29
- 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 +36 -2
- package/templates/next/files/surfaces.js +3 -2
- package/templates/next/template.json +9 -3
- package/templates/next/files/schema.sql +0 -22
|
@@ -157,22 +157,46 @@ const getSlots = useCallback(({ from, to }) =>
|
|
|
157
157
|
## Example
|
|
158
158
|
|
|
159
159
|
```jsx
|
|
160
|
+
// surfaces/schedule.jsx — client: the tasks on a calendar
|
|
160
161
|
"use client";
|
|
161
162
|
import { useState } from "react";
|
|
162
|
-
import {
|
|
163
|
-
import {
|
|
163
|
+
import { useRows } from "../lib/data";
|
|
164
|
+
import { deleteRow, insertRow, updateRow } from "../lib/actions";
|
|
165
|
+
import { Sheet } from "../components/ui";
|
|
166
|
+
import { CalendarView } from "../components/blocks/calendar-view";
|
|
167
|
+
import { changeWrites } from "../components/blocks/task-model";
|
|
168
|
+
|
|
169
|
+
const COLUMNS = ["id", "title", "status", "assignees", "start", "due", "project"];
|
|
170
|
+
|
|
171
|
+
// One change of the calendar's, as the rows it writes (task-list's README has the same).
|
|
172
|
+
async function save(change) {
|
|
173
|
+
for (const w of changeWrites(change, { columns: COLUMNS, keepNewIds: false })) {
|
|
174
|
+
const r =
|
|
175
|
+
w.op === "insert" ? await insertRow({ source: "tasks", values: w.values, detail: w.values.title })
|
|
176
|
+
: w.op === "update" ? await updateRow({ source: "tasks", id: w.id, patch: w.patch })
|
|
177
|
+
: await deleteRow({ source: "tasks", id: w.id });
|
|
178
|
+
if (r?.ok === false) return r;
|
|
179
|
+
}
|
|
180
|
+
return { ok: true };
|
|
181
|
+
}
|
|
164
182
|
|
|
165
|
-
export default function Schedule(
|
|
183
|
+
export default function Schedule() {
|
|
184
|
+
const tasks = useRows({ source: "tasks", orderBy: "start asc" });
|
|
185
|
+
const people = useRows({ source: "people" });
|
|
186
|
+
const projects = useRows({ source: "projects" });
|
|
166
187
|
const [open, setOpen] = useState(null);
|
|
167
188
|
return (
|
|
168
189
|
<>
|
|
169
|
-
<CalendarView tasks={tasks} people={people} projects={projects} onChange={
|
|
190
|
+
<CalendarView tasks={tasks.rows} people={people.rows} projects={projects.rows} onChange={save} onOpen={setOpen} defaultMode="week"
|
|
191
|
+
loading={tasks.loading || people.loading || projects.loading} error={tasks.error} />
|
|
170
192
|
<Sheet open={Boolean(open)} onClose={() => setOpen(null)} title={open?.title}>…</Sheet>
|
|
171
193
|
</>
|
|
172
194
|
);
|
|
173
195
|
}
|
|
174
196
|
```
|
|
175
197
|
|
|
198
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
199
|
+
|
|
176
200
|
## What it will not do
|
|
177
201
|
|
|
178
202
|
- Recurring events, time zones, or events that are not tasks: a repeat
|
|
@@ -94,33 +94,35 @@ The checkout step:
|
|
|
94
94
|
## Example
|
|
95
95
|
|
|
96
96
|
```jsx
|
|
97
|
-
//
|
|
97
|
+
// surfaces/basket.jsx — client: the cart, read and changed
|
|
98
98
|
"use client";
|
|
99
99
|
import { useState } from "react";
|
|
100
|
+
import { useRows } from "../lib/data";
|
|
100
101
|
import { deleteRow, updateRow } from "../lib/actions";
|
|
101
|
-
import { Cart } from "
|
|
102
|
+
import { Cart } from "../components/blocks/cart";
|
|
102
103
|
|
|
103
|
-
export default function Basket(
|
|
104
|
+
export default function Basket() {
|
|
105
|
+
const { rows: items, error, loading } = useRows({ source: "cart_items", orderBy: "created_at asc" });
|
|
104
106
|
const [step, setStep] = useState("cart");
|
|
105
107
|
return (
|
|
106
108
|
<Cart
|
|
107
109
|
items={items}
|
|
110
|
+
loading={loading}
|
|
108
111
|
error={error}
|
|
109
112
|
step={step}
|
|
110
113
|
shipping={0}
|
|
111
114
|
tax="Calculated at payment"
|
|
112
115
|
note="Free delivery on every order this month."
|
|
113
|
-
onQuantityChange={(item, quantity) => updateRow({ source: "
|
|
114
|
-
onRemove={(item) => deleteRow({ source: "
|
|
116
|
+
onQuantityChange={(item, quantity) => updateRow({ source: "cart_items", id: item.id, patch: { quantity } })}
|
|
117
|
+
onRemove={(item) => deleteRow({ source: "cart_items", id: item.id })}
|
|
115
118
|
onCheckout={() => setStep("checkout")}
|
|
116
119
|
onBack={() => setStep("cart")}
|
|
117
120
|
continueHref="/shop"
|
|
118
121
|
terms="I agree to the terms of sale"
|
|
119
|
-
onPay={async (
|
|
120
|
-
|
|
121
|
-
if (!
|
|
122
|
-
|
|
123
|
-
window.Paddle?.Checkout.open({ transactionId });
|
|
122
|
+
onPay={async () => {
|
|
123
|
+
// Paddle.js opens its checkout in the browser from price ids: no route of the app's own.
|
|
124
|
+
if (!window.Paddle) return { ok: false, error: "Payments have not loaded. Try again." };
|
|
125
|
+
window.Paddle.Checkout.open({ items: items.map((i) => ({ priceId: i.price_id, quantity: i.quantity })) });
|
|
124
126
|
return { ok: true };
|
|
125
127
|
}}
|
|
126
128
|
/>
|
|
@@ -128,6 +130,8 @@ export default function Basket({ items, error }) {
|
|
|
128
130
|
}
|
|
129
131
|
```
|
|
130
132
|
|
|
133
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
134
|
+
|
|
131
135
|
## What it will not do
|
|
132
136
|
|
|
133
137
|
- Take a card number, expiry or CVC. Payment is Paddle's overlay or page,
|
|
@@ -244,15 +244,20 @@ sharing one key, say).
|
|
|
244
244
|
## Example
|
|
245
245
|
|
|
246
246
|
```jsx
|
|
247
|
-
// surfaces/sales.jsx —
|
|
248
|
-
|
|
247
|
+
// surfaces/sales.jsx — client: read, hand over
|
|
248
|
+
"use client";
|
|
249
|
+
import { useRows } from "../lib/data";
|
|
249
250
|
import SalesCharts from "../components/SalesCharts";
|
|
250
251
|
|
|
251
|
-
export default
|
|
252
|
-
const
|
|
253
|
-
const
|
|
254
|
-
const
|
|
255
|
-
|
|
252
|
+
export default function SalesSurface() {
|
|
253
|
+
const months = useRows({ source: "revenue_by_month", orderBy: "month asc" });
|
|
254
|
+
const stages = useRows({ source: "deals_by_stage", orderBy: "position asc" });
|
|
255
|
+
const visits = useRows({ source: "visits_by_slot", orderBy: "position asc" });
|
|
256
|
+
if (months.loading || stages.loading || visits.loading) return null;
|
|
257
|
+
return (
|
|
258
|
+
<SalesCharts months={months.rows} stages={stages.rows} visits={visits.rows}
|
|
259
|
+
error={months.error ?? stages.error ?? visits.error} />
|
|
260
|
+
);
|
|
256
261
|
}
|
|
257
262
|
```
|
|
258
263
|
|
|
@@ -303,6 +308,8 @@ export default function SalesCharts({ months, stages, visits, error }) {
|
|
|
303
308
|
}
|
|
304
309
|
```
|
|
305
310
|
|
|
311
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
312
|
+
|
|
306
313
|
## What it will not do
|
|
307
314
|
|
|
308
315
|
- Sort, group or sum: the rows come shaped. Totals by month or by stage
|
|
@@ -214,29 +214,53 @@ and the hint in its bar, Send at the end. The picker leaves out files
|
|
|
214
214
|
## Example
|
|
215
215
|
|
|
216
216
|
```jsx
|
|
217
|
-
//
|
|
217
|
+
// surfaces/ticket.jsx — client: one ticket's conversation
|
|
218
218
|
"use client";
|
|
219
219
|
import { useState } from "react";
|
|
220
|
+
import { useSearchParams } from "next/navigation";
|
|
221
|
+
import { loadRows, useRow, useRows, whenLanded } from "../lib/data";
|
|
220
222
|
import { insertRow } from "../lib/actions";
|
|
221
|
-
import { ChatThread } from "
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
223
|
+
import { ChatThread } from "../components/blocks/chat";
|
|
224
|
+
|
|
225
|
+
const source = "ticket_messages";
|
|
226
|
+
const PAGE = 200;
|
|
227
|
+
const oldestFirst = (rows) => [...rows].reverse();
|
|
228
|
+
const asMessage = (r) => ({ id: r.id, role: r.from_agent ? "assistant" : "user", content: r.body, createdAt: r.created_at });
|
|
229
|
+
|
|
230
|
+
export default function SupportChat() {
|
|
231
|
+
const id = useSearchParams().get("row");
|
|
232
|
+
const { row: ticket } = useRow({ source: "tickets", id });
|
|
233
|
+
// The newest page, read again after every write.
|
|
234
|
+
const { rows, loading } = useRows({ source, filter: { ticket_id: id }, orderBy: "created_at desc", limit: PAGE });
|
|
235
|
+
const [earlier, setEarlier] = useState({ rows: [], more: true }); // what onLoadMore read, oldest first
|
|
236
|
+
const [sending, setSending] = useState([]); // sent, not read back yet
|
|
237
|
+
const seen = new Map([...earlier.rows, ...oldestFirst(rows)].map((r) => [r.id, r]));
|
|
238
|
+
const shown = [...seen.values()];
|
|
239
|
+
|
|
240
|
+
async function loadMore() {
|
|
241
|
+
const oldest = shown[0]?.created_at;
|
|
242
|
+
if (!oldest) return;
|
|
243
|
+
const r = await loadRows({ source, filter: { ticket_id: id, created_at: { lt: oldest } }, orderBy: "created_at desc", limit: PAGE });
|
|
244
|
+
// What is on screen is kept too: new messages push the oldest out of the page above.
|
|
245
|
+
if (!r.error) setEarlier({ rows: [...oldestFirst(r.rows), ...shown], more: r.rows.length === PAGE });
|
|
246
|
+
}
|
|
226
247
|
|
|
227
248
|
async function send(text) {
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
249
|
+
const mid = crypto.randomUUID();
|
|
250
|
+
setSending((all) => [...all, { id: mid, role: "user", content: text, createdAt: new Date().toISOString(), status: "sending" }]);
|
|
251
|
+
const r = await insertRow({ source, values: { id: mid, ticket_id: id, body: text }, detail: text });
|
|
252
|
+
if (r.ok) await whenLanded(); // read back: it is in `rows` now
|
|
253
|
+
setSending((all) => (r.ok ? all.filter((m) => m.id !== mid) : all.map((m) => (m.id === mid ? { ...m, status: "failed", error: r.error } : m))));
|
|
233
254
|
}
|
|
234
255
|
|
|
256
|
+
if (loading || !ticket) return null;
|
|
235
257
|
return (
|
|
236
258
|
<ChatThread
|
|
237
|
-
messages={
|
|
259
|
+
messages={[...shown.map(asMessage), ...sending.filter((m) => !seen.has(m.id))]}
|
|
238
260
|
onSend={send}
|
|
239
|
-
onRetry={(m) => {
|
|
261
|
+
onRetry={(m) => { setSending((all) => all.filter((x) => x.id !== m.id)); send(m.content); }}
|
|
262
|
+
onLoadMore={loadMore}
|
|
263
|
+
hasMore={rows.length === PAGE && earlier.more}
|
|
240
264
|
assistantName={ticket.agent_name}
|
|
241
265
|
userName="You"
|
|
242
266
|
label={`Ticket ${ticket.number}`}
|
|
@@ -245,6 +269,12 @@ export default function SupportChat({ ticket, initial }) {
|
|
|
245
269
|
}
|
|
246
270
|
```
|
|
247
271
|
|
|
272
|
+
`onLoadMore` reads the page before the oldest message shown:
|
|
273
|
+
`loadRows({ source, filter: { ticket_id, created_at: { lt: oldest } } })`,
|
|
274
|
+
the same filter in a server app's `loadRows` and the door.
|
|
275
|
+
|
|
276
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
277
|
+
|
|
248
278
|
With the AI SDK in a shadcn project, `useChat()` already has the shape:
|
|
249
279
|
`<ChatThread messages={messages} onSend={(text) => sendMessage({ text })}
|
|
250
280
|
streaming={status === "streaming" || status === "submitted"} onStop={stop}
|
|
@@ -193,6 +193,10 @@ export function QueryBox({ runQuery }) {
|
|
|
193
193
|
}
|
|
194
194
|
```
|
|
195
195
|
|
|
196
|
+
Running SQL a member typed is a server app's only: `runQuery` is a
|
|
197
|
+
Server Action of the app's own. A browser app has no such call, since no
|
|
198
|
+
route of the data door runs SQL a member typed.
|
|
199
|
+
|
|
196
200
|
JSON, read-only, fixed height:
|
|
197
201
|
|
|
198
202
|
```jsx
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// as you type) or plain text; line numbers, problems drawn on the text,
|
|
6
6
|
// find and replace, and a run hook on Cmd/Ctrl+Enter.
|
|
7
7
|
//
|
|
8
|
-
// Client only, one
|
|
8
|
+
// Client only, one bundle. CodeMirror needs the DOM, and the app's
|
|
9
9
|
// check refuses import() (so no next/dynamic): the modules load with the
|
|
10
10
|
// page, and the editor itself is created only after the first effect (the
|
|
11
11
|
// mount guard). Until then — on the server and in the first client paint —
|
|
@@ -79,32 +79,39 @@ The other exports, for a thread of your own:
|
|
|
79
79
|
## Example
|
|
80
80
|
|
|
81
81
|
```jsx
|
|
82
|
-
//
|
|
82
|
+
// surfaces/task.jsx — client: one task's comments
|
|
83
83
|
"use client";
|
|
84
|
-
import {
|
|
84
|
+
import { useSearchParams } from "next/navigation";
|
|
85
|
+
import { useArtifact } from "../lib/hooks";
|
|
86
|
+
import { useRows, whenLanded } from "../lib/data";
|
|
85
87
|
import { insertRow } from "../lib/actions";
|
|
86
|
-
import { CommentBox } from "
|
|
87
|
-
import { docToText } from "
|
|
88
|
-
|
|
89
|
-
export default function TaskComments(
|
|
90
|
-
const
|
|
88
|
+
import { CommentBox } from "../components/blocks/comment-box";
|
|
89
|
+
import { docToText } from "../components/blocks/doc-editor";
|
|
90
|
+
|
|
91
|
+
export default function TaskComments() {
|
|
92
|
+
const taskId = useSearchParams().get("row");
|
|
93
|
+
const { user } = useArtifact(); // the member signed in: { id, email }
|
|
94
|
+
const { rows, loading } = useRows({ source: "comments", filter: { task_id: taskId }, orderBy: "created_at asc" });
|
|
95
|
+
const { rows: team } = useRows({ source: "people", orderBy: "name asc" });
|
|
96
|
+
// created_by: a before-insert trigger sets it from the member, never the page (the app's README §5)
|
|
97
|
+
const comments = rows.map((c) => ({ id: c.id, author: c.created_by, body: c.body, createdAt: c.created_at }));
|
|
91
98
|
const findPeople = async (q) =>
|
|
92
99
|
team.filter((p) => p.name.toLowerCase().includes(q.toLowerCase())).map((p) => ({ id: p.id, label: p.name }));
|
|
93
100
|
|
|
94
101
|
async function post(doc) {
|
|
95
|
-
const res = await insertRow({
|
|
96
|
-
source: "public.comments",
|
|
97
|
-
values: { task_id: task.id, author_id: me.id, body: doc, body_text: docToText(doc) },
|
|
98
|
-
});
|
|
102
|
+
const res = await insertRow({ source: "comments", values: { task_id: taskId, body: doc, body_text: docToText(doc) } });
|
|
99
103
|
if (!res.ok) return { ok: false, error: res.error };
|
|
100
|
-
|
|
104
|
+
await whenLanded(); // the comment is read back before the box clears
|
|
101
105
|
return { ok: true };
|
|
102
106
|
}
|
|
103
107
|
|
|
104
|
-
|
|
108
|
+
if (loading) return null;
|
|
109
|
+
return <CommentBox comments={comments} onPost={post} author={user ? { name: user.email } : undefined} mentions={findPeople} />;
|
|
105
110
|
}
|
|
106
111
|
```
|
|
107
112
|
|
|
113
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
114
|
+
|
|
108
115
|
## What it will not do
|
|
109
116
|
|
|
110
117
|
- Edit or delete a posted comment, react, or reply in a nested thread: the
|
|
@@ -107,10 +107,10 @@ It is a client component: render it from a `"use client"` component.
|
|
|
107
107
|
## Example
|
|
108
108
|
|
|
109
109
|
```jsx
|
|
110
|
-
// components/Contact.jsx
|
|
110
|
+
// components/Contact.jsx — client: a message is a row
|
|
111
111
|
"use client";
|
|
112
|
+
import { insertRow } from "../lib/actions";
|
|
112
113
|
import { ContactSection } from "./blocks/contact-section";
|
|
113
|
-
import { sendContactMessage } from "../lib/actions"; // your server action
|
|
114
114
|
|
|
115
115
|
export default function Contact() {
|
|
116
116
|
return (
|
|
@@ -124,15 +124,16 @@ export default function Contact() {
|
|
|
124
124
|
]}
|
|
125
125
|
topics={{ options: ["Sales", "Support", "Partnerships"], required: true }}
|
|
126
126
|
consent="I agree to the privacy policy."
|
|
127
|
-
onSubmit={
|
|
128
|
-
const res = await sendContactMessage(values);
|
|
129
|
-
return res.ok ? { ok: true } : { ok: false, error: res.error };
|
|
130
|
-
}}
|
|
127
|
+
onSubmit={(values) => insertRow({ source: "contact_messages", values })} // { ok, id } or { ok: false, error }
|
|
131
128
|
/>
|
|
132
129
|
);
|
|
133
130
|
}
|
|
134
131
|
```
|
|
135
132
|
|
|
133
|
+
The row is the message: the app's agent, or a job, answers it.
|
|
134
|
+
|
|
135
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
136
|
+
|
|
136
137
|
## What it will not do
|
|
137
138
|
|
|
138
139
|
- Send anything: `onSubmit` is yours (save a row, email the team).
|
|
@@ -93,33 +93,55 @@ creates, fields }` (each row's `__problems` is `{ key: message }`) ·
|
|
|
93
93
|
## Example
|
|
94
94
|
|
|
95
95
|
```jsx
|
|
96
|
+
// surfaces/import.jsx — client: contacts from a spreadsheet
|
|
96
97
|
"use client";
|
|
97
98
|
import { useRouter } from "next/navigation";
|
|
98
|
-
import {
|
|
99
|
-
import {
|
|
99
|
+
import { useRows } from "../lib/data";
|
|
100
|
+
import { runAction } from "../lib/actions";
|
|
101
|
+
import { CsvImport } from "../components/blocks/csv-import";
|
|
100
102
|
|
|
101
|
-
|
|
103
|
+
const FIELDS = [
|
|
104
|
+
{ key: "name", name: "Name", type: "text", required: true },
|
|
105
|
+
{ key: "email", name: "Email", type: "text", required: true },
|
|
106
|
+
{ key: "owner", name: "Owner", type: "person" },
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
export default function ImportContacts() {
|
|
102
110
|
const router = useRouter();
|
|
111
|
+
const { rows: team } = useRows({ source: "people", orderBy: "name asc" });
|
|
103
112
|
return (
|
|
104
113
|
<CsvImport
|
|
105
|
-
fields={
|
|
114
|
+
fields={FIELDS}
|
|
106
115
|
people={team}
|
|
107
116
|
noun="contact"
|
|
108
117
|
onImport={async ({ rows }) => {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
imported++;
|
|
114
|
-
}
|
|
115
|
-
return { ok: true, imported };
|
|
118
|
+
// One action, one transaction: a write per row would stop halfway,
|
|
119
|
+
// and the door takes 300 writes a minute.
|
|
120
|
+
const r = await runAction({ name: "import_contacts", args: { rows: rows.map(({ id, ...values }) => values) } });
|
|
121
|
+
return r.ok ? { ok: true, imported: r.result?.imported } : r;
|
|
116
122
|
}}
|
|
117
|
-
onDone={() => router.push("/contacts")}
|
|
123
|
+
onDone={() => router.push("/s/contacts")}
|
|
118
124
|
/>
|
|
119
125
|
);
|
|
120
126
|
}
|
|
121
127
|
```
|
|
122
128
|
|
|
129
|
+
The action, named in `references/actions.md` and made with `sql`:
|
|
130
|
+
|
|
131
|
+
```sql
|
|
132
|
+
create or replace function import_contacts(args jsonb) returns jsonb
|
|
133
|
+
language sql as $$
|
|
134
|
+
with made as (
|
|
135
|
+
insert into contacts (name, email, owner)
|
|
136
|
+
select name, email, owner from jsonb_populate_recordset(null::contacts, args->'rows')
|
|
137
|
+
returning 1
|
|
138
|
+
)
|
|
139
|
+
select jsonb_build_object('imported', count(*)) from made
|
|
140
|
+
$$;
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
144
|
+
|
|
123
145
|
## What it will not do
|
|
124
146
|
|
|
125
147
|
- Excel files (.xlsx) are not read: save the sheet as CSV first (the
|
|
@@ -252,8 +252,19 @@ argument, `ids`, is every selected id, including rows not on this page.
|
|
|
252
252
|
|
|
253
253
|
```jsx
|
|
254
254
|
const [q, setQ] = useState({ search: "", filters: {}, sort: [], page: 1, pageSize: 25 });
|
|
255
|
-
//
|
|
256
|
-
|
|
255
|
+
// One page through the door: the filter grammar, an order, limit and offset.
|
|
256
|
+
const filter = {
|
|
257
|
+
...Object.fromEntries(Object.entries(q.filters).filter(([, values]) => values.length)),
|
|
258
|
+
customer: q.search ? { prefix: q.search } : undefined, // starts with, case ignored
|
|
259
|
+
};
|
|
260
|
+
const page = useRows({ source: "invoices", filter, orderBy: q.sort.map((s) => `${s.key} ${s.dir}`).join(", ") || undefined,
|
|
261
|
+
limit: q.pageSize, offset: (q.page - 1) * q.pageSize });
|
|
262
|
+
const total = useCount({ source: "invoices", filter });
|
|
263
|
+
const { status, ...others } = filter; // a facet counts over the other filters
|
|
264
|
+
const byStatus = useCountBy({ source: "invoices", column: "status", filter: others });
|
|
265
|
+
const statusCounts = Object.entries(byStatus.counts).map(([value, count]) => ({ value, label: value, count }));
|
|
266
|
+
|
|
267
|
+
<DataTable manual rows={page.rows} rowCount={total.count} loading={page.loading || total.loading}
|
|
257
268
|
search={q.search} onSearchChange={(search) => setQ((s) => ({ ...s, search }))}
|
|
258
269
|
filters={q.filters} onFiltersChange={(filters) => setQ((s) => ({ ...s, filters }))}
|
|
259
270
|
sort={q.sort} onSortChange={(sort) => setQ((s) => ({ ...s, sort }))}
|
|
@@ -262,6 +273,12 @@ const [q, setQ] = useState({ search: "", filters: {}, sort: [], page: 1, pageSiz
|
|
|
262
273
|
columns={COLUMNS} facets={[{ key: "status", options: statusCounts }]} />
|
|
263
274
|
```
|
|
264
275
|
|
|
276
|
+
`useRows`, `useCount` and `useCountBy` are `lib/data.js`'s in a browser
|
|
277
|
+
app. `offset` goes up to 10,000; past it, page with `cursor` (the last
|
|
278
|
+
answer's `next`, given when `orderBy` names one column). A server app's
|
|
279
|
+
own `loadRows` takes no `offset`: its `"use client"` component takes
|
|
280
|
+
these hooks from `lib/door.js`.
|
|
281
|
+
|
|
265
282
|
The pure helpers the table uses are exported too — `processRows`,
|
|
266
283
|
`searchRows`, `filterRows`, `sortRows`, `facetOptions`, `nextSort`,
|
|
267
284
|
`groupRows`, `groupValueOf` and `useControllable` — so a surface can compute the same answer the table
|
|
@@ -269,12 +286,25 @@ shows (a total over the filtered rows) without drawing it twice.
|
|
|
269
286
|
|
|
270
287
|
## Example
|
|
271
288
|
|
|
289
|
+
```jsx
|
|
290
|
+
// surfaces/invoices.jsx — client: read, hand over
|
|
291
|
+
"use client";
|
|
292
|
+
import { useRows } from "../lib/data";
|
|
293
|
+
import Invoices from "../components/Invoices";
|
|
294
|
+
|
|
295
|
+
export default function InvoicesSurface() {
|
|
296
|
+
const { rows, error, loading } = useRows({ source: "invoices", orderBy: "due asc" });
|
|
297
|
+
if (loading) return null; // rows only once read: to useFixture, a read in flight looks like an empty table
|
|
298
|
+
return <Invoices invoices={rows} error={error} />;
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
272
302
|
```jsx
|
|
273
303
|
// components/Invoices.jsx — client: the table, the row sheet
|
|
274
304
|
"use client";
|
|
275
305
|
import { useState } from "react";
|
|
276
306
|
import { updateRow } from "../lib/actions";
|
|
277
|
-
import { Badge, Button, Sheet } from "./ui";
|
|
307
|
+
import { Badge, Button, Notice, Sheet } from "./ui";
|
|
278
308
|
import { FixtureNotice, useFixture } from "./parts";
|
|
279
309
|
import { DataTable } from "./blocks/data-table";
|
|
280
310
|
|
|
@@ -283,7 +313,7 @@ const COLUMNS = [
|
|
|
283
313
|
{ key: "number", header: "Invoice", sortable: true, hideable: false },
|
|
284
314
|
{ key: "customer", header: "Customer", sortable: true },
|
|
285
315
|
{ key: "status", header: "Status", sortable: true, cell: (r) => <Badge tone={TONE[r.status]}>{r.status}</Badge> },
|
|
286
|
-
{ key: "amount", header: "Amount", sortable: true, align: "end", cell: (r) => `$${r.amount.toLocaleString()}` },
|
|
316
|
+
{ key: "amount", header: "Amount", sortable: true, align: "end", cell: (r) => `$${Number(r.amount).toLocaleString()}` }, // numeric arrives as text
|
|
287
317
|
{ key: "due", header: "Due", sortable: true },
|
|
288
318
|
];
|
|
289
319
|
const SAMPLE = [
|
|
@@ -291,14 +321,15 @@ const SAMPLE = [
|
|
|
291
321
|
{ number: "INV-1002", customer: "Acme Ltd", status: "overdue", amount: 450, due: "2026-09-01" },
|
|
292
322
|
];
|
|
293
323
|
|
|
294
|
-
export default function Invoices({ invoices }) {
|
|
324
|
+
export default function Invoices({ invoices, error }) {
|
|
295
325
|
const fixture = useFixture(invoices, SAMPLE);
|
|
296
326
|
const [open, setOpen] = useState(null);
|
|
297
327
|
const markPaid = (rows) =>
|
|
298
|
-
Promise.all(rows.map((r) => updateRow({ source: "
|
|
328
|
+
Promise.all(rows.map((r) => updateRow({ source: "invoices", id: r.id, patch: { status: "paid" } })))
|
|
299
329
|
.then((all) => all.find((r) => r.ok === false) ?? { ok: true });
|
|
300
330
|
return (
|
|
301
331
|
<div className="grid gap-4">
|
|
332
|
+
{error ? <Notice tone="danger">{error}</Notice> : null}
|
|
302
333
|
{fixture.isFixture ? <FixtureNotice onClear={fixture.clear} /> : null}
|
|
303
334
|
<DataTable
|
|
304
335
|
label="Invoices"
|
|
@@ -323,6 +354,8 @@ export default function Invoices({ invoices }) {
|
|
|
323
354
|
}
|
|
324
355
|
```
|
|
325
356
|
|
|
357
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
358
|
+
|
|
326
359
|
### Details, groups and totals
|
|
327
360
|
|
|
328
361
|
A budget: line items under departments, sorted within them, a subtotal
|
|
@@ -364,8 +397,8 @@ the row and the rest in the menu:
|
|
|
364
397
|
]} />
|
|
365
398
|
```
|
|
366
399
|
|
|
367
|
-
`setStatus` and `remove` return a promise of `{ ok }`, as
|
|
368
|
-
|
|
400
|
+
`setStatus` and `remove` return a promise of `{ ok }`, as `updateRow` and
|
|
401
|
+
`deleteRow` do; a failure is said above the table.
|
|
369
402
|
|
|
370
403
|
## What it will not do
|
|
371
404
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// Diagram — Mermaid text drawn in the workspace theme, with zoom and pan.
|
|
4
4
|
//
|
|
5
|
-
// Client only, one
|
|
5
|
+
// Client only, one bundle: Mermaid needs the DOM to measure text,
|
|
6
6
|
// and the app's check refuses import() (so no next/dynamic). The modules
|
|
7
7
|
// load with the page; the drawing happens after the first effect (the
|
|
8
8
|
// mount guard). Until then — on the server and in the first client paint —
|
|
@@ -144,20 +144,30 @@ on a partial one):
|
|
|
144
144
|
## Example
|
|
145
145
|
|
|
146
146
|
```jsx
|
|
147
|
-
//
|
|
147
|
+
// surfaces/deal.jsx — client: one deal's notes
|
|
148
148
|
"use client";
|
|
149
149
|
import { useState } from "react";
|
|
150
|
+
import { useSearchParams } from "next/navigation";
|
|
151
|
+
import { useRow, useRows } from "../lib/data";
|
|
150
152
|
import { updateRow } from "../lib/actions";
|
|
151
|
-
import { DocEditor } from "
|
|
153
|
+
import { DocEditor } from "../components/blocks/doc-editor";
|
|
154
|
+
|
|
155
|
+
export default function DealSurface() {
|
|
156
|
+
const id = useSearchParams().get("row");
|
|
157
|
+
const { row: deal, loading } = useRow({ source: "deals", id });
|
|
158
|
+
const { rows: team } = useRows({ source: "people", orderBy: "name asc" });
|
|
159
|
+
if (loading || !deal) return null;
|
|
160
|
+
return <DealNotes key={deal.id} deal={deal} team={team} />;
|
|
161
|
+
}
|
|
152
162
|
|
|
153
|
-
|
|
154
|
-
const [doc, setDoc] = useState(deal.notes); // jsonb, may be null
|
|
163
|
+
function DealNotes({ deal, team }) {
|
|
164
|
+
const [doc, setDoc] = useState(deal.notes); // jsonb, may be null; read once per deal, so a read never moves the cursor
|
|
155
165
|
return (
|
|
156
166
|
<DocEditor
|
|
157
167
|
value={doc}
|
|
158
168
|
onChange={(d) => {
|
|
159
169
|
setDoc(d);
|
|
160
|
-
updateRow({ source: "
|
|
170
|
+
updateRow({ source: "deals", id: deal.id, patch: { notes: d } }); // debounce this in a real app
|
|
161
171
|
}}
|
|
162
172
|
mentions={async (q) => team.filter((p) => p.name.toLowerCase().includes(q.toLowerCase())).map((p) => ({ id: p.id, label: p.name }))}
|
|
163
173
|
placeholder="Notes on this deal…"
|
|
@@ -167,13 +177,15 @@ export default function DealNotes({ deal, team }) {
|
|
|
167
177
|
}
|
|
168
178
|
```
|
|
169
179
|
|
|
180
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
181
|
+
|
|
170
182
|
A comment box that posts on Cmd/Ctrl+Enter and clears itself:
|
|
171
183
|
|
|
172
184
|
```jsx
|
|
173
185
|
const box = useDocEditor();
|
|
174
186
|
const post = async (doc) => {
|
|
175
187
|
if (isDocEmpty(doc)) return;
|
|
176
|
-
const res = await insertRow({ source: "
|
|
188
|
+
const res = await insertRow({ source: "comments", values: { body: doc, body_text: docToText(doc) } });
|
|
177
189
|
if (res.ok) box.current?.clear();
|
|
178
190
|
};
|
|
179
191
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// DocEditor — BlockNote, in the workspace theme, behind one set of props.
|
|
4
4
|
//
|
|
5
|
-
// Client only, one
|
|
5
|
+
// Client only, one bundle. BlockNote needs the DOM, and the app's
|
|
6
6
|
// check refuses import() (so no next/dynamic): the modules load with the
|
|
7
7
|
// page, and the editor itself is created only after the first effect
|
|
8
8
|
// (the mount guard). Until then — on the server and in the first client
|
|
@@ -75,22 +75,27 @@ out without waiting for the answer.
|
|
|
75
75
|
## Example
|
|
76
76
|
|
|
77
77
|
```jsx
|
|
78
|
-
//
|
|
78
|
+
// surfaces/wiki.jsx — client: one page, read and saved
|
|
79
79
|
"use client";
|
|
80
|
+
import { useSearchParams } from "next/navigation";
|
|
81
|
+
import { useRow, useRows } from "../lib/data";
|
|
80
82
|
import { updateRow } from "../lib/actions";
|
|
81
|
-
import { DocPage } from "
|
|
83
|
+
import { DocPage } from "../components/blocks/doc-page";
|
|
82
84
|
|
|
83
|
-
export default function WikiPage(
|
|
85
|
+
export default function WikiPage() {
|
|
86
|
+
const id = useSearchParams().get("row");
|
|
87
|
+
const { row: page, loading } = useRow({ source: "wiki_pages", id });
|
|
88
|
+
const { rows: team } = useRows({ source: "people", orderBy: "name asc" });
|
|
89
|
+
if (loading || !page) return null;
|
|
84
90
|
return (
|
|
85
91
|
<DocPage
|
|
86
92
|
id={page.id}
|
|
87
93
|
title={page.title}
|
|
88
94
|
icon={page.icon}
|
|
89
95
|
doc={page.body}
|
|
90
|
-
|
|
91
|
-
meta={`Edited by ${page.edited_by_name}`}
|
|
96
|
+
meta={`Edited by ${page.edited_by}`} // a before-update trigger sets edited_by (the app's README §5)
|
|
92
97
|
onSave={({ id, title, icon, doc }) =>
|
|
93
|
-
updateRow({ source: "
|
|
98
|
+
updateRow({ source: "wiki_pages", id, patch: { title, icon, body: doc } })
|
|
94
99
|
}
|
|
95
100
|
mentions={async (q) =>
|
|
96
101
|
team.filter((p) => p.name.toLowerCase().includes(q.toLowerCase())).map((p) => ({ id: p.id, label: p.name }))
|
|
@@ -100,6 +105,11 @@ export default function WikiPage({ page, team, canEdit }) {
|
|
|
100
105
|
}
|
|
101
106
|
```
|
|
102
107
|
|
|
108
|
+
The page reads `title`, `icon` and `doc` when `id` changes and owns the
|
|
109
|
+
draft in between, so the read after each save never moves the cursor.
|
|
110
|
+
|
|
111
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
112
|
+
|
|
103
113
|
For search or a preview, keep a plain-text copy beside the doc:
|
|
104
114
|
`docToText(doc)` from the doc-editor block, saved in the same `onSave`.
|
|
105
115
|
|