@lotics/app-sdk 0.97.0 → 0.98.1
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/AGENTS.md +1 -1
- package/dist/src/row.d.ts +9 -0
- package/dist/src/row.js +35 -21
- package/docs/data_fetching.md +6 -3
- package/docs/files.md +7 -0
- package/docs/members_and_options.md +5 -5
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -19,7 +19,7 @@ signature; open the file.**
|
|
|
19
19
|
| [docs/data_fetching.md](./docs/data_fetching.md) | The four read hooks (`useQuery`/`useInfiniteQuery`/`usePaginatedQuery`/`useCount` — the last for a number with no rows, sharing the `(alias, params, filter)` count key the paginated hook uses, so a list and a badge over one set buy one count; a count is a full scan and stays its OWN request so rows paint without waiting for it, and `rows.length` is never a count since rows truncate at 10,000 — `useQuery` says so with `truncated`), the ROW type (`RowOf` — the alias's projected columns and nothing else, values `unknown`; `__source_record_id`/`__source_table_id` typed but optional), runtime `sort`/`filter` keys AND the `useFieldOptions` map keyed against that same projection (`AppQueryColumns`, `ColumnKeyOf`), cell readers (`row.*` — `row.num` and `row.bool` answer `null` for an EMPTY cell, so none is distinguishable from zero and an unanswered checkbox from a "no" — `readSelect`, `readMembers`, `readLinks`, `readFiles`, `readLocked`, and `readCreatedAt`/`readUpdatedAt` for the record timestamps every row-level result carries), caching — **arrival revalidates** (a re-mount renders cache *and* refreshes it in the background, `loading` never flips) — **realtime push** (a table one of your queries reads changes and that query refetches within about a second, alias-precise, records-only, host-embedded apps only) — and the fourth source, **this app's own successful write**, which re-reads every mounted query immediately rather than waiting on that push (→ [mutations](./docs/mutations.md)), data discipline, the pagination count as a second full execution (and `total` to suppress it), the search-as-you-type + record-picker patterns. |
|
|
20
20
|
| [docs/mutations.md](./docs/mutations.md) | `useWorkflow` (the ONLY write path), the `WorkflowResult` resolve-never-throw contract (`field_errors` locates a refusal on the control it belongs to, where `message` can only say it at the dialog's scope), typed inputs, the automatic re-read a SUCCESSFUL write triggers over every mounted query (so a screen never waits on the host push to see its own write), diff-before-update, locked records, `useOptimistic`, `useNewRecord` (client-minted `rec_*` id so a new-record surface never remounts on its first save), read-after-write ordering (a re-read must not overtake an in-flight write). |
|
|
21
21
|
| [docs/workflows.md](./docs/workflows.md) | **The workflow-BODY authoring reference** — the JS subset a `src/workflows/<alias>.ts` body may use: the parse-at-save/never-execute model, opaque `fld_*`/`opt_*` keys, expression sources + explicit `linked()` descent, every step form (tool call, `agent`, waits, `validate`, `return`), the accepted sugar and its canonical lowering, helpers + callback rules, record-write surfaces, the traps, the bright line, and the verify loop — `check` (the only local gate: the app's own `npm run typecheck` never sees a body) → `dry_run_workflow` (static green is not a run) → `set`. |
|
|
22
|
-
| [docs/files.md](./docs/files.md) | Files end to end — `useFileUpload`, `useAttachments` (**the add-queue, and which lifecycle it keeps**: a composer clears, a record passes `landed` and each entry leaves as the stored pile takes it over), `readFiles`/presigned URLs (**a bearer credential for the bytes** — never logged, reported, or persisted), workflow-generated files, **naming a zip's entries** (`{ id, name }` per file — a file name, never a path), preview pairing, filter operators, the server-side delivery bounds. **Uploads declare a `fidelity`** (`standard` / `high` / `original`) — the app picks how much of the image survives storage; use `high` whenever text must stay legible. |
|
|
22
|
+
| [docs/files.md](./docs/files.md) | Files end to end — `useFileUpload`, `useAttachments` (**the add-queue, and which lifecycle it keeps**: a composer clears, a record passes `landed` and each entry leaves as the stored pile takes it over), `readFiles`/presigned URLs (**a bearer credential for the bytes** — never logged, reported, or persisted; and a LOOKUP of a files field is one list per linked row, which the reader opens itself), workflow-generated files, **naming a zip's entries** (`{ id, name }` per file — a file name, never a path), preview pairing, filter operators, the server-side delivery bounds. **Uploads declare a `fidelity`** (`standard` / `high` / `original`) — the app picks how much of the image survives storage; use `high` whenever text must stay legible. |
|
|
23
23
|
| [docs/members_and_options.md](./docs/members_and_options.md) | People + select options + comments — `useMembers`, `useFieldOptions` (keyed by the alias's OWN columns, every key optional), `useViewer`, `useComments` (each comment carries its own resolved `author`, so a thread crossing a role boundary is legible without declaring member access), and the `@lotics/ui` components they feed. |
|
|
24
24
|
| [docs/navigation_and_state.md](./docs/navigation_and_state.md) | `AppRouter` (embedded/standalone URL model), `useUrlState` + `urlParam` codecs, `useRecents`. |
|
|
25
25
|
| [docs/ai.md](./docs/ai.md) | `useAgentRun` (structured vs free-text, streaming ai-sdk `parts` → `AgentRun`, the agent's ask-back — `pendingChoice`/`answerChoice` over the parked `awaiting_input` state — and the `AgentRunLanding` every leg resolves), `askAi` — plus the fields-vs-file razor for choosing between them — and `useAiContext` (push the current screen's view state to the member's ambient chat agent; caps, push-only semantics; a chat mutation refetches your queries through the realtime channel, not a separate poke). **A `file` input carries its own content** — no reader tool to declare. **An agent reaches record DATA only through its declared `query_aliases` / `workflow_aliases`.** Also **what the member's own chat agent can do with your app while it is open** — the alias catalog it reads and how to shape a mutating alias for it. |
|
package/dist/src/row.d.ts
CHANGED
|
@@ -116,6 +116,15 @@ export interface AppFile {
|
|
|
116
116
|
* an unservable file. Pass one to `toDisplayFile` from `@lotics/ui/file_thumbnail`
|
|
117
117
|
* for FileThumbnail/Gallery — the kit owns that conversion, so don't re-declare
|
|
118
118
|
* it per app.
|
|
119
|
+
*
|
|
120
|
+
* A LOOKUP OF A FILES FIELD ARRIVES ONE LEVEL DEEPER, exactly as a lookup of a
|
|
121
|
+
* link field does (see {@link readLinks}) — the cell holds one entry per linked
|
|
122
|
+
* record and each entry is THAT record's whole files cell, an array — so read
|
|
123
|
+
* entry-by-entry it decoded to nothing while the pictures were plainly there,
|
|
124
|
+
* and every mark drawn off a looked-up photograph fell back to its glyph. The
|
|
125
|
+
* nesting is flattened here, at the boundary that owns the serialization, rather
|
|
126
|
+
* than at each caller: one level, in the order the rows link, and a file two
|
|
127
|
+
* linked rows both carry is one file, so ids repeat at most once.
|
|
119
128
|
*/
|
|
120
129
|
export declare function readFiles(v: unknown): AppFile[];
|
|
121
130
|
/**
|
package/dist/src/row.js
CHANGED
|
@@ -162,32 +162,46 @@ export function readLinks(v) {
|
|
|
162
162
|
* an unservable file. Pass one to `toDisplayFile` from `@lotics/ui/file_thumbnail`
|
|
163
163
|
* for FileThumbnail/Gallery — the kit owns that conversion, so don't re-declare
|
|
164
164
|
* it per app.
|
|
165
|
+
*
|
|
166
|
+
* A LOOKUP OF A FILES FIELD ARRIVES ONE LEVEL DEEPER, exactly as a lookup of a
|
|
167
|
+
* link field does (see {@link readLinks}) — the cell holds one entry per linked
|
|
168
|
+
* record and each entry is THAT record's whole files cell, an array — so read
|
|
169
|
+
* entry-by-entry it decoded to nothing while the pictures were plainly there,
|
|
170
|
+
* and every mark drawn off a looked-up photograph fell back to its glyph. The
|
|
171
|
+
* nesting is flattened here, at the boundary that owns the serialization, rather
|
|
172
|
+
* than at each caller: one level, in the order the rows link, and a file two
|
|
173
|
+
* linked rows both carry is one file, so ids repeat at most once.
|
|
165
174
|
*/
|
|
166
175
|
export function readFiles(v) {
|
|
167
176
|
if (!Array.isArray(v))
|
|
168
177
|
return [];
|
|
169
178
|
const out = [];
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
179
|
+
const seen = new Set();
|
|
180
|
+
for (const entry of v) {
|
|
181
|
+
const held = Array.isArray(entry) ? entry : [entry];
|
|
182
|
+
for (const f of held) {
|
|
183
|
+
if (!f || typeof f !== "object")
|
|
184
|
+
continue;
|
|
185
|
+
const id = f.id;
|
|
186
|
+
const url = f.url;
|
|
187
|
+
if (typeof id !== "string" || !id || typeof url !== "string" || !url || seen.has(id))
|
|
188
|
+
continue;
|
|
189
|
+
seen.add(id);
|
|
190
|
+
const filename = f.filename;
|
|
191
|
+
const mime = f.mime_type;
|
|
192
|
+
const thumb = f.thumbnail_url;
|
|
193
|
+
const size = f.size;
|
|
194
|
+
const created_at = f.created_at;
|
|
195
|
+
out.push({
|
|
196
|
+
id,
|
|
197
|
+
filename: typeof filename === "string" ? filename : "",
|
|
198
|
+
mime_type: typeof mime === "string" ? mime : "",
|
|
199
|
+
url,
|
|
200
|
+
thumbnail_url: typeof thumb === "string" ? thumb : undefined,
|
|
201
|
+
size: typeof size === "number" ? size : undefined,
|
|
202
|
+
created_at: typeof created_at === "string" ? created_at : undefined,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
191
205
|
}
|
|
192
206
|
return out;
|
|
193
207
|
}
|
package/docs/data_fetching.md
CHANGED
|
@@ -442,7 +442,7 @@ the server's answer is the only check — the rule the filter keys follow too.
|
|
|
442
442
|
| `readLinks(cell)` | link cell → `{ id, display }[]` | ALL linked records (`[]` when empty), a LOOKUP of a link field included |
|
|
443
443
|
| `readSelect(cell)` | select cell → `ResolvedOption[]` | all selected options as `{ key, label }` (`[]` when empty) |
|
|
444
444
|
| `readMembers(cell)` | member cell → `ResolvedMember[]` | `{ id, name, email?, image?, groups? }[]` (`[]` when empty) |
|
|
445
|
-
| `readFiles(cell)` | files cell → `AppFile[]` | attached files with presigned URLs (`[]` when empty) |
|
|
445
|
+
| `readFiles(cell)` | files cell → `AppFile[]` | attached files with presigned URLs (`[]` when empty), a LOOKUP of a files field included |
|
|
446
446
|
| `readLocked(rowObj)` | the whole **row** → `boolean` | the record's lock state from `__source_locked` |
|
|
447
447
|
| `readCreatedAt(rowObj)` | the whole **row** → `Date \| null` | when the record was created, from `__created_at` |
|
|
448
448
|
| `readUpdatedAt(rowObj)` | the whole **row** → `Date \| null` | when the record was last updated, from `__updated_at` |
|
|
@@ -498,8 +498,11 @@ anonymous public-app visitors; re-querying refreshes the TTL naturally. Entries
|
|
|
498
498
|
presign (no `url`) are skipped, so you never render an unservable file. `size` (bytes) and
|
|
499
499
|
`created_at` (ISO upload timestamp) are resolved at serving time; `size` is absent for older files
|
|
500
500
|
not yet backfilled — render it only when present, and surface both as dedicated sortable columns
|
|
501
|
-
over the raw values (a formatted "8.4 MB" string sorts wrong).
|
|
502
|
-
|
|
501
|
+
over the raw values (a formatted "8.4 MB" string sorts wrong). **A LOOKUP of a files field arrives
|
|
502
|
+
one level deeper**, exactly as a looked-up link does — one entry per linked record, each entry that
|
|
503
|
+
record's whole files cell — and `readFiles` opens it, in link order and with a file two linked rows
|
|
504
|
+
both carry named once, so a looked-up picture needs no unwrapping of its own. Previewing and
|
|
505
|
+
uploading files: [./files.md](./files.md).
|
|
503
506
|
|
|
504
507
|
**Warning — the presign ceiling:** signing file URLs is per-entry server work, so a response is
|
|
505
508
|
capped at **2,000 file entries** (server default). A query over the cap **fails** with an error
|
package/docs/files.md
CHANGED
|
@@ -241,6 +241,13 @@ Decode with `readFiles(cell)` → `AppFile[]` (`dist/src/row.d.ts`):
|
|
|
241
241
|
`readFiles` skips entries the server didn't presign (no `url`), so you never render an unservable
|
|
242
242
|
file. It is pure and never throws.
|
|
243
243
|
|
|
244
|
+
- **A LOOKUP of a files field is one list per linked row.** The cell holds one entry per linked
|
|
245
|
+
record and each entry is that record's whole files cell, so the value arrives one level deeper
|
|
246
|
+
than a stored column's — exactly as a looked-up link does. `readFiles` opens that level itself,
|
|
247
|
+
in link order, naming a file two linked rows both carry once, so a picture read through a link
|
|
248
|
+
needs no unwrapping at the call site. Read entry-by-entry it decodes to nothing, which is a mark
|
|
249
|
+
falling back to its glyph while the bytes are plainly there.
|
|
250
|
+
|
|
244
251
|
- **Thumbnails are optimistic.** `thumbnail_url` is emitted for every image without checking that
|
|
245
252
|
the variant exists; a not-yet-generated variant 404s on fetch. `@lotics/ui`'s `FileThumbnail`
|
|
246
253
|
falls back to `url` on image error — a hand-rolled `<img src={thumbnail_url}>` must do the same.
|
|
@@ -265,7 +265,7 @@ client-side up front, and re-checked server-side.
|
|
|
265
265
|
- **The author is always the real signed-in member** — correct attribution, enforced server-side.
|
|
266
266
|
**Warning:** under "View as", comments still author as the real member (the admin), not the
|
|
267
267
|
view-as target — unlike `useViewer` and `is_current_member` scoping, which follow the target.
|
|
268
|
-
- **Edit and delete are author-only**, checked server-side against the viewer. `
|
|
268
|
+
- **Edit and delete are author-only**, checked server-side against the viewer. `EntryList`'s
|
|
269
269
|
`currentMemberId` prop drives the matching affordance client-side.
|
|
270
270
|
|
|
271
271
|
### Reading & writing
|
|
@@ -286,13 +286,13 @@ renders the panel. State: `{ comments, loading, error, available, createComment,
|
|
|
286
286
|
updateComment, deleteComment, refetch }`.
|
|
287
287
|
|
|
288
288
|
- `comments` — newest first on the wire (server order). Pass the array as-is to `@lotics/ui`'s
|
|
289
|
-
`
|
|
289
|
+
`EntryList`, which re-sorts oldest-first for display. Each `AppComment`: `{ id, record_id,
|
|
290
290
|
table_id, member_id, author, content, files, workspace_id, created_at, updated_at }`. Attachments
|
|
291
291
|
(`AppCommentFile`) carry `id` / `filename` / `mime_type` — a file's identity is its `id`, and
|
|
292
292
|
the server re-reads every attachment from storage by that id, so nothing else you hold about a
|
|
293
293
|
file can affect what is stored. The `url` / `thumbnail_url` / `preview_url` fields exist on the type
|
|
294
294
|
but the server does not populate them today — render attachments by name and type (what
|
|
295
|
-
`
|
|
295
|
+
`EntryList`'s default file row does), never by counting on a fetchable URL.
|
|
296
296
|
- `createComment({ content, file_ids? })` — posts as the viewing member. `file_ids` come from
|
|
297
297
|
`useFileUpload` / `useAttachments` (see [files](./files.md)). A comment must have content or at
|
|
298
298
|
least one file (empty input is a client no-op; the server enforces the same rule). Content max
|
|
@@ -308,7 +308,7 @@ updateComment, deleteComment, refetch }`.
|
|
|
308
308
|
app by someone the record references nowhere. So a thread that crosses a role boundary is legible without the app declaring member
|
|
309
309
|
access it does not otherwise need, and a per-viewer app never widens its reach for a display
|
|
310
310
|
string. `name` is `null` when the id no longer resolves in the org (a removed member) — render a
|
|
311
|
-
fallback (`
|
|
311
|
+
fallback (`EntryList` has an `unknownMember` label for exactly this). The one comment with no
|
|
312
312
|
`author` is the optimistic row `createComment` renders locally: the SDK knows the viewer's id and
|
|
313
313
|
not their name, and it will not invent one. The server's row replaces it when the refetch lands.
|
|
314
314
|
- **Freshness:** SWR-cached, revalidates on focus/reconnect, so another viewer's comment appears on
|
|
@@ -336,7 +336,7 @@ directly (full props: `node_modules/@lotics/ui/AGENTS.md` and its `docs/`):
|
|
|
336
336
|
| A select value (stored or picker option) | `Status` | A `useFieldOptions` option, or `byKey(readSelect(cell)[0]?.key)`; accepts a single option, an array (multi → one badge each), or null (renders nothing). Missing/unknown color → neutral. |
|
|
337
337
|
| A person, inline | `MemberChip` | `name` / `image` from a roster or a cell — both carry it; no image → initials |
|
|
338
338
|
| A member picker | `MemberSelect` | `members={useMembers().members}` — renders each option as a `MemberChip`; `MEMBER_UNASSIGNED` marks its optional "unassigned" option |
|
|
339
|
-
| A comment thread | `
|
|
339
|
+
| A comment thread | `EntryList` + `CommentComposer` | `useComments` state; `resolveMember` reads each comment's own `author` (`{ name, image }`) — no roster needed |
|
|
340
340
|
|
|
341
341
|
The SDK never imports `@lotics/ui` — the app owns the (thin) data→UI adapter in each row above.
|
|
342
342
|
|