softr-vibe-coding 1.1.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/LICENSE +21 -0
- package/README.md +262 -0
- package/SKILL.md +480 -0
- package/bin/cli.js +140 -0
- package/datasources/airtable.md +91 -0
- package/datasources/bigquery.md +36 -0
- package/datasources/clickup.md +82 -0
- package/datasources/coda.md +44 -0
- package/datasources/fields.md +203 -0
- package/datasources/google-sheets.md +46 -0
- package/datasources/hubspot.md +51 -0
- package/datasources/monday.md +52 -0
- package/datasources/notion.md +56 -0
- package/datasources/overview.md +41 -0
- package/datasources/reading.md +222 -0
- package/datasources/rest-api.md +206 -0
- package/datasources/shared-patterns.md +9 -0
- package/datasources/smartsuite.md +39 -0
- package/datasources/softr-database.md +47 -0
- package/datasources/sql-database.md +48 -0
- package/datasources/supabase.md +38 -0
- package/datasources/writing.md +256 -0
- package/datasources/xano.md +37 -0
- package/package.json +40 -0
- package/references/advanced-integrations.md +69 -0
- package/references/airtable-automations.md +350 -0
- package/references/anti-patterns.md +86 -0
- package/references/common-patterns.md +102 -0
- package/references/helper-blocks.md +370 -0
- package/references/quick-reference.md +207 -0
- package/ui-ux-guidelines.md +746 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
# Helper Blocks & Cross-Block Patterns
|
|
2
|
+
|
|
3
|
+
Cross-block communication via `window` globals, the invisible helper block pattern for multi-table access, breadcrumb navigation, and advanced production-hardened patterns.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [When You Need Helper Blocks](#when-you-need-helper-blocks)
|
|
8
|
+
- [Companion Field Helpers](#companion-field-helpers)
|
|
9
|
+
- [Breadcrumb / Back Navigation](#breadcrumb--back-navigation)
|
|
10
|
+
- [Multi-Table Access via Invisible Helper Blocks](#multi-table-access-via-invisible-helper-blocks)
|
|
11
|
+
- [Publisher Template](#publisher-template)
|
|
12
|
+
- [Consumer Pattern](#consumer-pattern)
|
|
13
|
+
- [useWindowData Custom Hook](#usewindowdata-custom-hook)
|
|
14
|
+
- [Triggering Actions in Other Blocks (Bi-Directional Events)](#triggering-actions-in-other-blocks-bi-directional-events)
|
|
15
|
+
- [Advanced Patterns](#advanced-patterns)
|
|
16
|
+
- [Anti-Patterns](#anti-patterns)
|
|
17
|
+
|
|
18
|
+
## When You Need Helper Blocks
|
|
19
|
+
|
|
20
|
+
A Vibe Coding block can only connect to **one source table**. When your main block needs data from a second table (e.g., a task detail block that also needs the Users table for a team picker), you cannot add another `useRecords` for a different table.
|
|
21
|
+
|
|
22
|
+
**Solution:** Drop an invisible helper block on the same Softr page, connected to the second table. It fetches records, publishes them to a `window` global, and dispatches a custom event. The main block reads the global and listens for updates. The helper returns `null` so it renders nothing in the published app.
|
|
23
|
+
|
|
24
|
+
`useLinkedRecords` always returns `{id, title}` only and silently ignores extra fields in `select`. This is the core reason the helper block pattern exists. If your task involves showing status, due dates, or any non-title field from a linked table, skip `useLinkedRecords` entirely and build a helper -- there is no other way to get those fields.
|
|
25
|
+
|
|
26
|
+
Key rules:
|
|
27
|
+
- The helper block must be on the **same Softr page** as the consumer -- `window` is page-scoped, globals don't cross pages.
|
|
28
|
+
- **One helper block per foreign table.** Multiple helpers with distinct namespaces coexist fine on the same page.
|
|
29
|
+
- **Read-only pattern.** Helpers expose foreign table data for lookups, pickers, and display only. Writes still happen from the main block via its own `useRecordUpdate` / `useRecordCreate`. If the main block needs to write to the helper's table, use a webhook or the Softr Database REST API (see [writing.md Cross-Table Operations](../datasources/writing.md#cross-table-operations)), not the helper.
|
|
30
|
+
|
|
31
|
+
## Companion Field Helpers
|
|
32
|
+
|
|
33
|
+
The main `getFieldValue()` function lives in [fields.md](../datasources/fields.md). These companion helpers handle specific shapes needed for helper block consumers:
|
|
34
|
+
|
|
35
|
+
```jsx
|
|
36
|
+
/* For multi-value linked records / multi-selects when you need an array */
|
|
37
|
+
function getLinkedNames(f) {
|
|
38
|
+
if (f == null) return [];
|
|
39
|
+
if (Array.isArray(f)) return f.map(function(x) {
|
|
40
|
+
return (x && typeof x === "object") ? (x.label || x.name || x.title || "") : String(x);
|
|
41
|
+
}).filter(Boolean);
|
|
42
|
+
if (typeof f === "object") {
|
|
43
|
+
var v = f.label || f.name || f.title || "";
|
|
44
|
+
return v ? [v] : [];
|
|
45
|
+
}
|
|
46
|
+
return [String(f)];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* When you need {id, title} pairs for filtering by ID */
|
|
50
|
+
function getLinkedItems(f) {
|
|
51
|
+
if (f == null) return [];
|
|
52
|
+
if (Array.isArray(f)) return f.map(function(x) {
|
|
53
|
+
if (x && typeof x === "object") return { id: x.id || "", title: x.label || x.name || x.title || "" };
|
|
54
|
+
return { id: "", title: String(x) };
|
|
55
|
+
}).filter(function(o) { return o.id || o.title; });
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Breadcrumb / Back Navigation
|
|
61
|
+
|
|
62
|
+
For detail pages with a "back" button that returns to the listing:
|
|
63
|
+
|
|
64
|
+
```jsx
|
|
65
|
+
import { ArrowLeft, ChevronRight } from "lucide-react";
|
|
66
|
+
|
|
67
|
+
var BRAND_PRIMARY = "#386AF5";
|
|
68
|
+
|
|
69
|
+
<div className="flex items-center gap-2 mb-4">
|
|
70
|
+
<button
|
|
71
|
+
className="flex items-center gap-1 text-xs"
|
|
72
|
+
style={{ color: "#595959" }}
|
|
73
|
+
onClick={function() {
|
|
74
|
+
if (window.history.length > 1) {
|
|
75
|
+
setTimeout(function() { window.history.back(); }, 0);
|
|
76
|
+
} else {
|
|
77
|
+
window.location.href = "/listing-page";
|
|
78
|
+
}
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
<ArrowLeft className="h-3.5 w-3.5" />
|
|
82
|
+
Back
|
|
83
|
+
</button>
|
|
84
|
+
{parentName && (
|
|
85
|
+
<>
|
|
86
|
+
<ChevronRight className="h-3 w-3" style={{ color: "#E6E6E6" }} />
|
|
87
|
+
<span className="text-xs" style={{ color: "#595959" }}>{parentName}</span>
|
|
88
|
+
</>
|
|
89
|
+
)}
|
|
90
|
+
<ChevronRight className="h-3 w-3" style={{ color: "#E6E6E6" }} />
|
|
91
|
+
<span className="text-xs font-medium" style={{ color: BRAND_PRIMARY }}>{currentItemName}</span>
|
|
92
|
+
</div>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Key points:
|
|
96
|
+
- `window.history.back()` uses the browser's native History API -- no routing library needed.
|
|
97
|
+
- The `window.history.length > 1` check is the fallback for users who land directly via shared URL (no history). Send them to a sensible default like the listing page.
|
|
98
|
+
- Wrap in `setTimeout(fn, 0)` to break out of Softr's event cycle (same as scroll commands).
|
|
99
|
+
- Use alongside `useNavigationSetting` when you also need a Studio-configurable destination -- they solve different problems (contextual back vs. fixed CTA).
|
|
100
|
+
- Works in any Vibe Coding block without extra dependencies.
|
|
101
|
+
|
|
102
|
+
## Multi-Table Access via Invisible Helper Blocks
|
|
103
|
+
|
|
104
|
+
### Naming Conventions
|
|
105
|
+
|
|
106
|
+
- Namespace: `window.__<app>_<resource>` -- e.g., `window.__myapp_team`, `window.__myapp_campaigns`
|
|
107
|
+
- Ready flag: `window.__<app>_<resource>_ready` (boolean) -- consumers check before reading
|
|
108
|
+
- Progress event: `<app>_<resource>_progress` -- dispatched on every paginated update
|
|
109
|
+
- Ready event: `<app>_<resource>_ready` -- dispatched once when all pages are loaded
|
|
110
|
+
|
|
111
|
+
### Publisher Template (the invisible helper block)
|
|
112
|
+
|
|
113
|
+
```jsx
|
|
114
|
+
import { useRecords, q } from "@/lib/datasource";
|
|
115
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
116
|
+
|
|
117
|
+
var select = q.select({ fullName: "fldXXX", isActive: "fldYYY" });
|
|
118
|
+
|
|
119
|
+
function toOption(record) {
|
|
120
|
+
return { id: record.id, title: record.fields.fullName || "" };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export default function Block() {
|
|
124
|
+
var result = useRecords({ select: select, count: 100 });
|
|
125
|
+
|
|
126
|
+
/* Auto-fetch all pages -- MUST be inside useEffect */
|
|
127
|
+
useEffect(function() {
|
|
128
|
+
if (result.hasNextPage && !result.isFetchingNextPage && result.status === "success") {
|
|
129
|
+
result.fetchNextPage();
|
|
130
|
+
}
|
|
131
|
+
}, [result.hasNextPage, result.isFetchingNextPage, result.status, result.fetchNextPage]);
|
|
132
|
+
|
|
133
|
+
var publishedPageCount = useRef(0);
|
|
134
|
+
|
|
135
|
+
var allRecords = useMemo(function() {
|
|
136
|
+
if (!result.data) return [];
|
|
137
|
+
return result.data.pages.flatMap(function(p) { return p.items; });
|
|
138
|
+
}, [result.data]);
|
|
139
|
+
|
|
140
|
+
var pageCount = result.data ? result.data.pages.length : 0;
|
|
141
|
+
|
|
142
|
+
/* Progressive publish -- push to window every time a new page arrives.
|
|
143
|
+
Critical when the source table has hundreds of records: the consumer
|
|
144
|
+
can render the first 100 options while later pages stream in. */
|
|
145
|
+
useEffect(function() {
|
|
146
|
+
if (result.status !== "success") return;
|
|
147
|
+
if (pageCount <= publishedPageCount.current) return;
|
|
148
|
+
|
|
149
|
+
var options = allRecords
|
|
150
|
+
.filter(function(r) { return r.fields.isActive; })
|
|
151
|
+
.map(toOption)
|
|
152
|
+
.filter(function(o) { return o.title; })
|
|
153
|
+
.sort(function(a, b) { return a.title.localeCompare(b.title); });
|
|
154
|
+
|
|
155
|
+
window.__myapp_users = options;
|
|
156
|
+
window.__myapp_users_ready = true;
|
|
157
|
+
|
|
158
|
+
window.dispatchEvent(new CustomEvent("myapp_users_progress", {
|
|
159
|
+
detail: { count: options.length, complete: !result.hasNextPage },
|
|
160
|
+
}));
|
|
161
|
+
|
|
162
|
+
if (!result.hasNextPage) {
|
|
163
|
+
window.dispatchEvent(new CustomEvent("myapp_users_ready", {
|
|
164
|
+
detail: { count: options.length },
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
publishedPageCount.current = pageCount;
|
|
169
|
+
}, [allRecords, pageCount, result.status, result.hasNextPage]);
|
|
170
|
+
|
|
171
|
+
return null; /* invisible in published app */
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Consumer Pattern (inside the main block)
|
|
176
|
+
|
|
177
|
+
```jsx
|
|
178
|
+
var [teamOptions, setTeamOptions] = useState([]);
|
|
179
|
+
|
|
180
|
+
useEffect(function() {
|
|
181
|
+
function readFromWindow() {
|
|
182
|
+
if (window.__myapp_users_ready && window.__myapp_users) {
|
|
183
|
+
setTeamOptions(window.__myapp_users);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* Read immediately if helper already published */
|
|
188
|
+
readFromWindow();
|
|
189
|
+
|
|
190
|
+
/* Poll briefly in case helper hasn't mounted yet */
|
|
191
|
+
var interval = setInterval(function() {
|
|
192
|
+
if (window.__myapp_users_ready) {
|
|
193
|
+
readFromWindow();
|
|
194
|
+
clearInterval(interval);
|
|
195
|
+
}
|
|
196
|
+
}, 200);
|
|
197
|
+
|
|
198
|
+
/* Listen for progressive updates */
|
|
199
|
+
function onUpdate() { readFromWindow(); }
|
|
200
|
+
window.addEventListener("myapp_users_progress", onUpdate);
|
|
201
|
+
|
|
202
|
+
return function() {
|
|
203
|
+
clearInterval(interval);
|
|
204
|
+
window.removeEventListener("myapp_users_progress", onUpdate);
|
|
205
|
+
};
|
|
206
|
+
}, []);
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### useWindowData Custom Hook (cleaner consumer)
|
|
210
|
+
|
|
211
|
+
Wrap the listener + polling boilerplate in a reusable hook:
|
|
212
|
+
|
|
213
|
+
```jsx
|
|
214
|
+
function useWindowData() {
|
|
215
|
+
var [tick, setTick] = useState(0);
|
|
216
|
+
useEffect(function() {
|
|
217
|
+
function bump() { setTick(function(t) { return t + 1; }); }
|
|
218
|
+
window.addEventListener("myapp_projects_ready", bump);
|
|
219
|
+
window.addEventListener("myapp_projects_progress", bump);
|
|
220
|
+
var poll = setInterval(bump, 1500);
|
|
221
|
+
return function() {
|
|
222
|
+
window.removeEventListener("myapp_projects_ready", bump);
|
|
223
|
+
window.removeEventListener("myapp_projects_progress", bump);
|
|
224
|
+
clearInterval(poll);
|
|
225
|
+
};
|
|
226
|
+
}, []);
|
|
227
|
+
return {
|
|
228
|
+
projects: window.__myapp_projects_all || [],
|
|
229
|
+
options: window.__myapp_projects_filter_options || null,
|
|
230
|
+
ready: window.__myapp_projects_ready === true,
|
|
231
|
+
tick: tick,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
The `tick` state forces re-renders on every event/poll, so consumers naturally pick up new data without managing local state of the globals.
|
|
237
|
+
|
|
238
|
+
## Triggering Actions in Other Blocks (Bi-Directional Events)
|
|
239
|
+
|
|
240
|
+
The publisher / consumer pattern above describes one direction of cross-block communication: a helper block fetches data and consumers read it. The same `window.dispatchEvent` + `addEventListener` mechanism also supports the reverse -- any block triggering UI behavior in any other block on the same page.
|
|
241
|
+
|
|
242
|
+
Use this when one block needs to open an editor, refresh a sibling, or otherwise control state in a different block. Example: a list block dispatches an event when the user clicks "Edit", and a separate editor block listens and opens with the right record loaded.
|
|
243
|
+
|
|
244
|
+
### Block A -- dispatch the event
|
|
245
|
+
|
|
246
|
+
```jsx
|
|
247
|
+
function handleEditClick(participantId) {
|
|
248
|
+
window.dispatchEvent(new CustomEvent("myapp_participant_open", {
|
|
249
|
+
detail: { id: participantId },
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Block B -- listen and react
|
|
255
|
+
|
|
256
|
+
```jsx
|
|
257
|
+
var [editingId, setEditingId] = useState(null);
|
|
258
|
+
|
|
259
|
+
useEffect(function() {
|
|
260
|
+
function onOpen(e) {
|
|
261
|
+
setEditingId(e.detail.id);
|
|
262
|
+
}
|
|
263
|
+
window.addEventListener("myapp_participant_open", onOpen);
|
|
264
|
+
return function() {
|
|
265
|
+
window.removeEventListener("myapp_participant_open", onOpen);
|
|
266
|
+
};
|
|
267
|
+
}, []);
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Naming convention
|
|
271
|
+
|
|
272
|
+
`<app>_<entity>_<verb>` -- e.g. `myapp_participant_open`, `myapp_invoice_refresh`, `myapp_filter_reset`. The `detail` payload should include only what the listener needs (typically a record ID).
|
|
273
|
+
|
|
274
|
+
### Constraints (same as helper blocks)
|
|
275
|
+
|
|
276
|
+
- Both blocks must live on the **same Softr page** -- `window` events do not cross pages.
|
|
277
|
+
- The dispatcher block must be mounted before the listener is invoked, or the listener simply doesn't hear early dispatches. If the dispatch could happen on initial page load, the listener should also check `window` for a "last value" published by the dispatcher (similar to how the consumer pattern reads from a global) so it can recover state on mount.
|
|
278
|
+
- This pattern is for triggering UI actions, not for shared data. For shared data across blocks, use the helper block publisher / consumer pattern above.
|
|
279
|
+
|
|
280
|
+
## Advanced Patterns
|
|
281
|
+
|
|
282
|
+
These patterns were hardened in production on real features where the main block needed filtered, enriched data across multiple dimensions with live preview. Apply them whenever a helper serves more than a simple picker.
|
|
283
|
+
|
|
284
|
+
### Publishing Rich Filter Options
|
|
285
|
+
|
|
286
|
+
When the consumer needs dynamic filter UIs, have the helper compute options inside its `useMemo` and publish as a separate global:
|
|
287
|
+
|
|
288
|
+
- `window.__myapp_projects_all` -- raw records
|
|
289
|
+
- `window.__myapp_projects_filter_options` -- computed `{ workType: [...], industries: [...], dateRange: { min, max } }`
|
|
290
|
+
|
|
291
|
+
This avoids recomputing options in every consumer.
|
|
292
|
+
|
|
293
|
+
### Visible Dev Badge (instead of `return null`)
|
|
294
|
+
|
|
295
|
+
During development, render a minimal badge to see loading status at a glance:
|
|
296
|
+
|
|
297
|
+
```jsx
|
|
298
|
+
return (
|
|
299
|
+
<div style={{
|
|
300
|
+
display: "flex", alignItems: "center", gap: "8px",
|
|
301
|
+
padding: "8px", fontSize: "12px", color: "#64748b",
|
|
302
|
+
}}>
|
|
303
|
+
Helper: {allRecords.length} loaded {result.hasNextPage ? "..." : "done"}
|
|
304
|
+
</div>
|
|
305
|
+
);
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Switch to `return null` only after the feature is stable in production.
|
|
309
|
+
|
|
310
|
+
### Two CustomEvents: `_progress` + `_ready`
|
|
311
|
+
|
|
312
|
+
Dispatch `_progress` on every paginated update AND `_ready` once when all pages are loaded. Consumers that need progressive data listen to both; consumers that need complete data listen only to `_ready`.
|
|
313
|
+
|
|
314
|
+
The `detail.complete` flag in `_progress` events lets consumers show a loading indicator while pages are still streaming, then switch to a final state when `complete === true`.
|
|
315
|
+
|
|
316
|
+
### Block Ordering Matters
|
|
317
|
+
|
|
318
|
+
If helper B depends on helper A's globals, helper A MUST be placed **above** helper B in Softr's page block order. Softr renders blocks top-to-bottom. A lower-ordered helper B will mount with A's globals still `undefined`, then catch up -- but this causes initial empty-state flashes.
|
|
319
|
+
|
|
320
|
+
Document the dependency at the top of helper B:
|
|
321
|
+
|
|
322
|
+
```jsx
|
|
323
|
+
/* Depends on: window.__myapp_accounts_ready */
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Shape Stability Is a Contract
|
|
327
|
+
|
|
328
|
+
When you change a helper's output shape (e.g., `advisorOffice` from array to string), ALL downstream consumers break silently. Two options:
|
|
329
|
+
|
|
330
|
+
1. Document the published shape as a comment at the top of the helper file and update all consumers in the same commit.
|
|
331
|
+
2. Version the namespace (`__myapp_projects_v2`) -- old consumers keep reading v1 until migrated.
|
|
332
|
+
|
|
333
|
+
Defensive consumers can use `Array.isArray(x) ? x.map(...) : x` when shape might vary, but don't lean on this -- it hides bugs.
|
|
334
|
+
|
|
335
|
+
### useState, Not useRef, for IDs Consumed by useMemo
|
|
336
|
+
|
|
337
|
+
IDs consumed by `useMemo` must live in `useState`. `useRef` mutations don't trigger re-renders, so `useMemo` never recomputes when the ID resolves asynchronously. This produces the confusing symptom of "data loads, but only after a manual refresh."
|
|
338
|
+
|
|
339
|
+
```jsx
|
|
340
|
+
/* CORRECT */
|
|
341
|
+
var [currentTeamMemberId, setCurrentTeamMemberId] = useState(null);
|
|
342
|
+
|
|
343
|
+
var filteredViews = useMemo(function() {
|
|
344
|
+
if (!currentTeamMemberId) return [];
|
|
345
|
+
return allViews.filter(function(v) { return v.createdBy === currentTeamMemberId; });
|
|
346
|
+
}, [allViews, currentTeamMemberId]);
|
|
347
|
+
|
|
348
|
+
/* WRONG -- useRef changes don't trigger useMemo recomputation */
|
|
349
|
+
var currentTeamMemberId = useRef(null);
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Saved Views Architecture (Two-Block Pattern)
|
|
353
|
+
|
|
354
|
+
For per-user saved filter views on a list page:
|
|
355
|
+
|
|
356
|
+
1. A hidden **Team Member Resolver** helper that maps `window.__softr_current_user.email` to a Team Member record ID (via `useRecords` + filter on email field).
|
|
357
|
+
2. A `viewScope` field on the SavedViews table to scope views per page (e.g., `"pursuits"`, `"client-work"`, `"my-projects"`).
|
|
358
|
+
3. The main list block reads the current Team Member ID from the resolver global and filters SavedViews where `createdBy === currentTeamMemberId AND viewScope === pageScope`.
|
|
359
|
+
|
|
360
|
+
## Anti-Patterns
|
|
361
|
+
|
|
362
|
+
| Anti-Pattern | Correct Approach |
|
|
363
|
+
|---|---|
|
|
364
|
+
| Helper block returning `null` during dev | Return a minimal visible badge until feature stable |
|
|
365
|
+
| Single CustomEvent on full load | Dispatch `_progress` per page AND `_ready` on completion |
|
|
366
|
+
| Helper publishes only raw records | Also publish computed `filterOptions` as separate globals |
|
|
367
|
+
| Refactoring helper shape without updating consumers | Version namespace OR update all consumers in same commit |
|
|
368
|
+
| Helper B placed above A when B depends on A | A must be above B -- Softr renders top-to-bottom |
|
|
369
|
+
| `useRef` for IDs consumed by `useMemo` | Use `useState` -- ref mutations don't trigger recomputation |
|
|
370
|
+
| Using `useLinkedRecords` for rich foreign data | It only returns `{id, title}` -- use a helper block instead |
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Quick Reference
|
|
2
|
+
|
|
3
|
+
Fast lookup for imports, hook signatures, field mapping syntax, and common patterns. Use when you already know what you need and just want the shape.
|
|
4
|
+
|
|
5
|
+
## Imports
|
|
6
|
+
|
|
7
|
+
```jsx
|
|
8
|
+
// DATASOURCE
|
|
9
|
+
import { useRecords, useRecord, useRecordCreate, useRecordUpdate, useRecordDelete,
|
|
10
|
+
useCurrentRecordId, useLinkedRecords, useUpload, useMetric, useChartData,
|
|
11
|
+
q, metric } from "@/lib/datasource";
|
|
12
|
+
|
|
13
|
+
// USER
|
|
14
|
+
import { useCurrentUser } from "@/lib/user";
|
|
15
|
+
|
|
16
|
+
// EDITABLE SETTINGS
|
|
17
|
+
import { useTextSetting, useImageSetting, useVideoSetting, useArraySetting,
|
|
18
|
+
useVibeCodingBlockIconSetting, useNavigationSetting,
|
|
19
|
+
useBooleanSetting } from "@/lib/editable-settings";
|
|
20
|
+
|
|
21
|
+
// REACT
|
|
22
|
+
import { useState, useEffect, useMemo, useCallback, useRef } from "react";
|
|
23
|
+
|
|
24
|
+
// UI
|
|
25
|
+
import { Card, CardContent } from "@/components/ui/card";
|
|
26
|
+
import { Button } from "@/components/ui/button";
|
|
27
|
+
import { Target } from "lucide-react";
|
|
28
|
+
import { DynamicIcon } from "@/components/dynamic-icon";
|
|
29
|
+
import { toast } from "sonner";
|
|
30
|
+
import { cn } from "@/lib/utils";
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Field Mapping (static, outside component)
|
|
34
|
+
|
|
35
|
+
```jsx
|
|
36
|
+
var select = q.select({ alias: "FIELD_ID" });
|
|
37
|
+
var updateFields = q.select({ alias: "FIELD_ID" }); // writable only
|
|
38
|
+
var createFields = q.select({ alias: "FIELD_ID" }); // writable only
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Read
|
|
42
|
+
|
|
43
|
+
```jsx
|
|
44
|
+
var result = useRecords({ select: select, count: 100 });
|
|
45
|
+
var records = (result.data && result.data.pages) ? result.data.pages.flatMap(function(p) { return p.items; }) : [];
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Filter + Sort
|
|
49
|
+
|
|
50
|
+
```jsx
|
|
51
|
+
useRecords({
|
|
52
|
+
select: select, count: 100,
|
|
53
|
+
where: q.and(q.text("status").is("Active"), q.date("due").lte("2025-12-31")),
|
|
54
|
+
orderBy: q.desc("createdAt"),
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Single Record (detail pages)
|
|
59
|
+
|
|
60
|
+
```jsx
|
|
61
|
+
var recordId = useCurrentRecordId();
|
|
62
|
+
var result = useRecord({ recordId: recordId, select: select });
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Current User
|
|
66
|
+
|
|
67
|
+
```jsx
|
|
68
|
+
var currentUser = useCurrentUser(); // { id, fullName, email, avatar }
|
|
69
|
+
var softrUser = window.__softr_current_user; // full object with userGroups
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Create
|
|
73
|
+
|
|
74
|
+
```jsx
|
|
75
|
+
var createRecord = useRecordCreate({
|
|
76
|
+
fields: createFields,
|
|
77
|
+
onSuccess: function(newRecord) { refetch(); },
|
|
78
|
+
onError: function(err) { toast.error(err.message); },
|
|
79
|
+
});
|
|
80
|
+
createRecord.mutate({ name: "Jane", email: "jane@example.com" });
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Update (THE CORRECT PATTERN)
|
|
84
|
+
|
|
85
|
+
```jsx
|
|
86
|
+
var updateRecord = useRecordUpdate({
|
|
87
|
+
fields: updateFields,
|
|
88
|
+
onSuccess: function(updatedRecord) { refetch(); },
|
|
89
|
+
onError: function(err) { toast.error(err.message); },
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Call .mutate() — NOT .mutateAsync() — and use the nested {recordId, fields:{}} shape.
|
|
93
|
+
// Per-call onSuccess/onError go in the second argument.
|
|
94
|
+
updateRecord.mutate(
|
|
95
|
+
{ recordId: record.id, fields: { name: "New" } },
|
|
96
|
+
{
|
|
97
|
+
onSuccess: function() { toast.success("Saved"); },
|
|
98
|
+
onError: function(err) { toast.error(err.message); },
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Two parser requirements both must hold or `enabled` stays `false`:**
|
|
104
|
+
1. `.mutate(...)` — NOT `.mutateAsync(...).then(...)` (parser ignores `mutateAsync`)
|
|
105
|
+
2. Payload is `{ recordId, fields: {...} }` — NOT flat `{ recordId, status: "..." }`
|
|
106
|
+
|
|
107
|
+
See [datasources/writing.md](../datasources/writing.md#critical-two-parser-requirements-for-userecordupdate) for the full debugging path.
|
|
108
|
+
|
|
109
|
+
## Delete
|
|
110
|
+
|
|
111
|
+
```jsx
|
|
112
|
+
var deleteRecord = useRecordDelete({
|
|
113
|
+
onSuccess: function(result) { refetch(); },
|
|
114
|
+
});
|
|
115
|
+
deleteRecord.mutate(record.id); // Just the ID string
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Mutation Hook Properties (all hooks)
|
|
119
|
+
|
|
120
|
+
`.enabled`, `.status`, `.error`, `.mutate()`, `.mutateAsync()`, `.reset()`
|
|
121
|
+
|
|
122
|
+
⚠ `.mutateAsync()` exists at runtime but is **invisible to Softr's Action parser** — using it on `useRecordUpdate` leaves `enabled` permanently `false`. Always call `.mutate(payload, { onSuccess, onError })` for updates.
|
|
123
|
+
|
|
124
|
+
## Linked Records Picker
|
|
125
|
+
|
|
126
|
+
```jsx
|
|
127
|
+
// field = alias from select, NOT raw field ID
|
|
128
|
+
var result = useLinkedRecords({ select: select, field: "aliasName", count: 100 });
|
|
129
|
+
var options = (result.data && result.data.pages) ? result.data.pages.flatMap(function(p) { return p.items; }) : [];
|
|
130
|
+
// items shaped as { id, title } -- use opt.title NOT opt.label
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Linked Records in Mutations
|
|
134
|
+
|
|
135
|
+
```jsx
|
|
136
|
+
teamMembers: [{ id: "MEMBER_ID" }]
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Formula Booleans
|
|
140
|
+
|
|
141
|
+
```jsx
|
|
142
|
+
item.fields.isOverdue === "1" // true
|
|
143
|
+
item.fields.isOverdue === "0" // false
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Metrics
|
|
147
|
+
|
|
148
|
+
```jsx
|
|
149
|
+
var result1 = useMetric({ select: select, metric: metric.sum("revenue") });
|
|
150
|
+
var result2 = useMetric({ select: select, metric: metric.count() });
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Editable Settings
|
|
154
|
+
|
|
155
|
+
```jsx
|
|
156
|
+
var title = useTextSetting({ name: "title", label: "Title", initialValue: "Hello" });
|
|
157
|
+
var show = useBooleanSetting({ name: "toggle", label: "Show header", initialValue: false });
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Field Value Helper (getFieldValue)
|
|
161
|
+
|
|
162
|
+
```jsx
|
|
163
|
+
var getFieldValue = function(f) {
|
|
164
|
+
if (f == null) return "";
|
|
165
|
+
if (Array.isArray(f)) {
|
|
166
|
+
return f.map(function(x) {
|
|
167
|
+
if (x && typeof x === "object") return x.label || x.name || x.title || "";
|
|
168
|
+
return String(x);
|
|
169
|
+
}).filter(Boolean).join(", ");
|
|
170
|
+
}
|
|
171
|
+
if (typeof f === "object") return f.label || f.name || f.title || "";
|
|
172
|
+
return String(f);
|
|
173
|
+
};
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Wrap every field value in `getFieldValue()` before rendering, parsing, or comparing.
|
|
177
|
+
|
|
178
|
+
## Pagination Auto-Fetch
|
|
179
|
+
|
|
180
|
+
```jsx
|
|
181
|
+
useEffect(function() {
|
|
182
|
+
if (result.hasNextPage && !result.isFetchingNextPage && result.status === "success") {
|
|
183
|
+
result.fetchNextPage();
|
|
184
|
+
}
|
|
185
|
+
}, [result.hasNextPage, result.isFetchingNextPage, result.status, result.fetchNextPage]);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Component Skeleton
|
|
189
|
+
|
|
190
|
+
```jsx
|
|
191
|
+
export default function Block() {
|
|
192
|
+
var result = useRecords({ select: select, count: 25 });
|
|
193
|
+
|
|
194
|
+
if (result.status === "pending") return <div className="container py-6"><div className="content">Loading...</div></div>;
|
|
195
|
+
if (result.status === "error") return <div className="container py-6"><div className="content">Error</div></div>;
|
|
196
|
+
|
|
197
|
+
var records = (result.data && result.data.pages) ? result.data.pages.flatMap(function(p) { return p.items; }) : [];
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<div className="container py-6">
|
|
201
|
+
<div className="content">
|
|
202
|
+
{/* UI */}
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
```
|