@lotics/app-sdk 0.120.3 → 0.121.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/AGENTS.md +1 -1
- package/dist/activity.d.ts +15 -2
- package/dist/{chunk-ABDSJ25E.js → chunk-MK4DBY6R.js} +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +46 -3
- package/dist/router.js +1 -1
- package/dist/rpc.d.ts +1 -1
- package/dist/shared_types.d.ts +2 -1
- package/dist/store.d.ts +2 -0
- package/dist/viewer.d.ts +2 -0
- package/docs/members_and_options.md +21 -5
- package/docs/runtime.md +1 -0
- package/docs/security.md +1 -0
- package/docs/workflows.md +1 -1
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -20,7 +20,7 @@ This file is the index. The **exact type** of anything is its shipped declaratio
|
|
|
20
20
|
| [docs/workflows.md](./docs/workflows.md) | **The workflow-BODY reference** — the JS subset a body may use, opaque `fld_*`/`opt_*` keys, every step form, the accepted sugar, helpers, record-write surfaces, the traps and the verify loop. |
|
|
21
21
|
| [docs/recipes.md](./docs/recipes.md) | Task-shaped how-tos — returning a generated file, returning structured data, parameterized lookups, composable optional filters, cell decoding, testing an AI action without spending credits. |
|
|
22
22
|
| [docs/files.md](./docs/files.md) | Files end to end — `useFileUpload` and its `fidelity` and a photo's `place`, `renameFile` (a new file over the same bytes), `useAttachments`/`useAttachmentPiles`, `readFiles` and presigned URLs (a bearer credential — never logged or persisted), workflow-generated files, naming a zip's entries, the delivery bounds. |
|
|
23
|
-
| [docs/members_and_options.md](./docs/members_and_options.md) | People, select options and comments — `useMembers`, `useFieldOptions`, `useViewer`, `useWorkspaceTimezone`/`useWorkspaceCurrency`, `useAppContext`, the workspace's zone at the root, `useComments`, a record's activity (`useRecordActivity`). |
|
|
23
|
+
| [docs/members_and_options.md](./docs/members_and_options.md) | People, select options and comments — `useMembers`, `useFieldOptions`, `useViewer`, `useWorkspaceTimezone`/`useWorkspaceCurrency`, `useAppContext`, the workspace's zone at the root, `useComments`, a record's activity and the undo of one change (`useRecordActivity`, `undoRecordChange`). |
|
|
24
24
|
| [docs/navigation_and_state.md](./docs/navigation_and_state.md) | `AppRouter` (embedded/standalone URL model, the screens and `drawers` it reports to the host), `useUrlState` + `urlParam` codecs, `useRecents`, `useFolderPick`. |
|
|
25
25
|
| [docs/ai.md](./docs/ai.md) | `useAgentRun` (structured vs free-text, streaming parts, the agent's ask-back), `askAi`, `useAiContext`, and what the member's own chat agent can do with the app while it is open. |
|
|
26
26
|
| [docs/security.md](./docs/security.md) | **Read before shipping** — the owner-principal model, `is_current_member` scoping, write attribution, group gates, public-app bounds, why a per-input bound is a tenancy floor rather than an authorization check. |
|
package/dist/activity.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ActivityEntry } from "./shared_types.js";
|
|
1
|
+
import type { ActivityEntry, ActivityUndoResult } from "./shared_types.js";
|
|
2
2
|
export interface RecordActivityState {
|
|
3
3
|
/** Newest first. */
|
|
4
4
|
entries: ActivityEntry[];
|
|
@@ -6,7 +6,7 @@ export interface RecordActivityState {
|
|
|
6
6
|
next: string | undefined;
|
|
7
7
|
loading: boolean;
|
|
8
8
|
error: string | null;
|
|
9
|
-
/** A signed-in member;
|
|
9
|
+
/** A signed-in member of the app's organization; for anyone else nothing is read and `entries` stays empty. */
|
|
10
10
|
available: boolean;
|
|
11
11
|
refetch: () => void;
|
|
12
12
|
}
|
|
@@ -25,3 +25,16 @@ export interface RecordActivityOptions {
|
|
|
25
25
|
* ```
|
|
26
26
|
*/
|
|
27
27
|
export declare function useRecordActivity(entity: string, record_id: string, options?: RecordActivityOptions): RecordActivityState;
|
|
28
|
+
/**
|
|
29
|
+
* Put one change entry of the record's activity back, as the viewer's own save: each field the entry changed returns
|
|
30
|
+
* to what it held before it where the record still holds what the entry wrote. The answer names the fields put back
|
|
31
|
+
* (`reverted`) and those left as they stand (`kept`), each with its `reason` — `changed_since`, or `refused` where the
|
|
32
|
+
* table refused the write (a lock, a check) — and arrives once the record's reads are read anew. Refused for an entry
|
|
33
|
+
* whose `undoable` is false: the record's making, an undo, an entry already put back, and a viewer who may not write
|
|
34
|
+
* the record.
|
|
35
|
+
*
|
|
36
|
+
* ```tsx
|
|
37
|
+
* const { reverted, kept } = await undoRecordChange("job", recordId, entry.id);
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare function undoRecordChange(entity: string, record_id: string, entry_id: string): Promise<ActivityUndoResult>;
|
|
@@ -1656,6 +1656,7 @@ function rpcStandalone(op, payload) {
|
|
|
1656
1656
|
case "comments.counts":
|
|
1657
1657
|
return rejectCommentsStandalone();
|
|
1658
1658
|
case "activity.list":
|
|
1659
|
+
case "activity.undo":
|
|
1659
1660
|
return Promise.reject(new Error("A record's activity is available only in embedded apps \u2014 a signed-in member is required."));
|
|
1660
1661
|
case "recording.start":
|
|
1661
1662
|
case "recording.stop":
|
package/dist/index.d.ts
CHANGED
|
@@ -11,9 +11,9 @@ export type { ExportedFile } from "./shared_types.js";
|
|
|
11
11
|
export type { AttachedFile, AttachmentPiles, AttachmentPilesOptions, AttachmentsOptions, AttachmentsState } from "./attachments.js";
|
|
12
12
|
export { useComments, useCommentCounts } from "./comments.js";
|
|
13
13
|
export type { AppComment, AppCommentAuthor, AppCommentFile, CommentsState, UseCommentsArgs, CommentCountsState, UseCommentCountsArgs, } from "./comments.js";
|
|
14
|
-
export { useRecordActivity } from "./activity.js";
|
|
14
|
+
export { undoRecordChange, useRecordActivity } from "./activity.js";
|
|
15
15
|
export type { RecordActivityOptions, RecordActivityState } from "./activity.js";
|
|
16
|
-
export type { ActivityActor, ActivityActorKind, ActivityChange, ActivityComment, ActivityEntry, ActivityValue, RecordActivityPage, } from "./shared_types.js";
|
|
16
|
+
export type { ActivityActor, ActivityActorKind, ActivityChange, ActivityComment, ActivityEntry, ActivityUndoResult, ActivityValue, RecordActivityPage, UndoKeptReason, } from "./shared_types.js";
|
|
17
17
|
export { useAppContext, useViewer, useWorkspaceCurrency, useWorkspaceTimezone } from "./viewer.js";
|
|
18
18
|
export type { AppAbsence } from "./shared_types.js";
|
|
19
19
|
export { useRecording, useRecordings } from "./recording.js";
|
package/dist/index.js
CHANGED
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
sumDecimal,
|
|
41
41
|
tiersAscend,
|
|
42
42
|
urlParam
|
|
43
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-MK4DBY6R.js";
|
|
44
44
|
|
|
45
45
|
// src/mount.tsx
|
|
46
46
|
import { createRoot } from "react-dom/client";
|
|
@@ -611,6 +611,14 @@ function readNow(aliases) {
|
|
|
611
611
|
function refetchReads(aliases) {
|
|
612
612
|
for (const slot of readNow(aliases)) void ask(slot, true);
|
|
613
613
|
}
|
|
614
|
+
async function readsAnswered(aliases) {
|
|
615
|
+
await Promise.all(
|
|
616
|
+
readNow(aliases).map(async (slot) => {
|
|
617
|
+
await ask(slot, true);
|
|
618
|
+
while (slot.inflight !== null) await slot.inflight;
|
|
619
|
+
})
|
|
620
|
+
);
|
|
621
|
+
}
|
|
614
622
|
function askInOnePaint(read2) {
|
|
615
623
|
if (read2.length === 0) return;
|
|
616
624
|
const paint = { waiting: new Set(read2), held: [], drawn: false, cap: setTimeout(() => draw(paint), PAINT_CAP_MS) };
|
|
@@ -26395,6 +26403,9 @@ var appQueryDeclarationSchema = zod_default.object({
|
|
|
26395
26403
|
),
|
|
26396
26404
|
templates: zod_default.record(zod_default.string().regex(APP_ALIAS_REGEX), appExportTemplateSchema).optional().describe(
|
|
26397
26405
|
"Document templates an export of this query's rows may fill, by the name the export asks for. An export names one of these or none; a template it does not declare is refused."
|
|
26406
|
+
),
|
|
26407
|
+
screen_only: zod_default.literal(true).optional().describe(
|
|
26408
|
+
"The query feeds one control of the app's screen and answers no question a person asks. It runs like any other; a catalog of what an agent may ask leaves it out."
|
|
26398
26409
|
)
|
|
26399
26410
|
});
|
|
26400
26411
|
var appAgentWritesSchema = zod_default.object({
|
|
@@ -26736,6 +26747,15 @@ var TOOL_INPUT_SEMANTICS = {
|
|
|
26736
26747
|
// same position `query_records.field_keys` holds.
|
|
26737
26748
|
return_fields: { kind: "field_key_refs", table_from: "table_id" }
|
|
26738
26749
|
},
|
|
26750
|
+
import_rows: {
|
|
26751
|
+
table_id: { kind: "table_ref", rows: "change" },
|
|
26752
|
+
match: { kind: "field_key_refs", table_from: "table_id" },
|
|
26753
|
+
// Each row's `values` are keyed by field, a link cell possibly `{ lookup }`; the tool resolves both at execute.
|
|
26754
|
+
rows: { kind: "dynamic" },
|
|
26755
|
+
create: { kind: "scalar" },
|
|
26756
|
+
// `fields` is keyed by field; the tool resolves it at execute.
|
|
26757
|
+
conflicts: { kind: "dynamic" }
|
|
26758
|
+
},
|
|
26739
26759
|
delete_records: {
|
|
26740
26760
|
// THE ONE CALL THAT TAKES ROWS. Every column of the row goes with it, so
|
|
26741
26761
|
// nothing field-keyed can carry the fact and it rides the table position —
|
|
@@ -26771,6 +26791,22 @@ var TOOL_INPUT_SEMANTICS = {
|
|
|
26771
26791
|
changes: { kind: "dynamic" },
|
|
26772
26792
|
reason: { kind: "text" }
|
|
26773
26793
|
},
|
|
26794
|
+
// The record names its own table; an entry's id and a cursor are the trail's.
|
|
26795
|
+
get_record_activity: {
|
|
26796
|
+
record_id: { kind: "record_ref" },
|
|
26797
|
+
after: { kind: "scalar" }
|
|
26798
|
+
},
|
|
26799
|
+
undo_record_change: {
|
|
26800
|
+
record_id: { kind: "record_ref" },
|
|
26801
|
+
entry_id: { kind: "scalar" }
|
|
26802
|
+
},
|
|
26803
|
+
// `table_id` names the table whose rows the undo changes, moves to the trash and brings back; a run's id is the
|
|
26804
|
+
// trail's.
|
|
26805
|
+
undo_record_changes: {
|
|
26806
|
+
table_id: { kind: "table_ref", rows: "remove" },
|
|
26807
|
+
record_ids: { kind: "record_refs" },
|
|
26808
|
+
workflow_execution_id: { kind: "scalar" }
|
|
26809
|
+
},
|
|
26774
26810
|
// ─── Query surfaces ────────────────────────────────────────────────────
|
|
26775
26811
|
query_records: {
|
|
26776
26812
|
table_id: { kind: "table_ref", rows: "none" },
|
|
@@ -29032,6 +29068,7 @@ function useAppContext() {
|
|
|
29032
29068
|
const { data, error: error51, refetch } = useKey(APP_CONTEXT_KEY, readContext, { focus: false, arrival: false });
|
|
29033
29069
|
return {
|
|
29034
29070
|
memberId: data?.member_id ?? null,
|
|
29071
|
+
insider: data !== void 0 && data.member_id !== null && data.group_ids !== void 0,
|
|
29035
29072
|
groupIds: data?.group_ids ?? null,
|
|
29036
29073
|
absences: data?.absences ?? null,
|
|
29037
29074
|
openableApps: data?.openable_apps ?? null,
|
|
@@ -32717,8 +32754,7 @@ function oneQueryAlias(entity) {
|
|
|
32717
32754
|
var NO_ENTRIES = [];
|
|
32718
32755
|
function useRecordActivity(entity, record_id, options = {}) {
|
|
32719
32756
|
const { after } = options;
|
|
32720
|
-
const {
|
|
32721
|
-
const available = memberId !== null;
|
|
32757
|
+
const { insider: available, resolved } = useAppContext();
|
|
32722
32758
|
const page = useKey(
|
|
32723
32759
|
available ? JSON.stringify(["activity", entity, record_id, after ?? null]) : null,
|
|
32724
32760
|
() => rpc("activity.list", { entity, record_id, ...after === void 0 ? {} : { after } }),
|
|
@@ -32734,6 +32770,12 @@ function useRecordActivity(entity, record_id, options = {}) {
|
|
|
32734
32770
|
refetch: page.refetch
|
|
32735
32771
|
};
|
|
32736
32772
|
}
|
|
32773
|
+
async function undoRecordChange(entity, record_id, entry_id) {
|
|
32774
|
+
const result = await rpc("activity.undo", { entity, record_id, entry_id });
|
|
32775
|
+
refetchReads([listQueryAlias(entity)]);
|
|
32776
|
+
await readsAnswered([oneQueryAlias(entity)]);
|
|
32777
|
+
return result;
|
|
32778
|
+
}
|
|
32737
32779
|
|
|
32738
32780
|
// src/recording.ts
|
|
32739
32781
|
import { useCallback as useCallback7, useMemo as useMemo4, useSyncExternalStore as useSyncExternalStore3 } from "react";
|
|
@@ -33077,6 +33119,7 @@ export {
|
|
|
33077
33119
|
requestGeofencedLocation,
|
|
33078
33120
|
row,
|
|
33079
33121
|
rpc,
|
|
33122
|
+
undoRecordChange,
|
|
33080
33123
|
urlParam,
|
|
33081
33124
|
useAgentRun,
|
|
33082
33125
|
useAgentRuns,
|
package/dist/router.js
CHANGED
package/dist/rpc.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { type UrlParams, type UrlParamsPatch } from "./url_params.js";
|
|
|
9
9
|
* app → host: { id, op, payload }
|
|
10
10
|
* host → app: { id, type: "result", data } | { id, type: "error", message }
|
|
11
11
|
*/
|
|
12
|
-
export type RpcOp = "query" | "export" | "field_options" | "workflow" | "agentRuns" | "agentRun.get" | "agentRun.cancel" | "upload" | "file.rename" | "members" | "context" | "write_model" | "prediction_miss" | "openExternal" | "openApp" | "askAi" | "urlState.get" | "urlState.set" | "folder.get" | "folder.set" | "comments.list" | "comments.create" | "comments.update" | "comments.delete" | "comments.counts" | "activity.list" | "recording.start" | "recording.stop";
|
|
12
|
+
export type RpcOp = "query" | "export" | "field_options" | "workflow" | "agentRuns" | "agentRun.get" | "agentRun.cancel" | "upload" | "file.rename" | "members" | "context" | "write_model" | "prediction_miss" | "openExternal" | "openApp" | "askAi" | "urlState.get" | "urlState.set" | "folder.get" | "folder.set" | "comments.list" | "comments.create" | "comments.update" | "comments.delete" | "comments.counts" | "activity.list" | "activity.undo" | "recording.start" | "recording.stop";
|
|
13
13
|
export interface AgentRunPayload {
|
|
14
14
|
alias: string;
|
|
15
15
|
session_id: string;
|
package/dist/shared_types.d.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* `scripts/build_sdk.mjs`, because that package is private and ships in no
|
|
4
4
|
* tarball. Written at every build, never edited.
|
|
5
5
|
*/
|
|
6
|
-
export type ActivityEntry = { changes: { field_id: string; label: string; before: string | number | boolean | { key: string; label: string; }[] | { id: string; display: string; }[] | { id: string; name: string | null; image?: string | undefined; }[] | { id: string; filename: string; mime_type: string; file_storage_key?: string | undefined; url?: string | undefined; thumbnail_url?: string | undefined; preview_url?: string | undefined; size?: number | null | undefined; created_at?: string | undefined; document_template_id?: string | undefined; }[] | null; after: string | number | boolean | { key: string; label: string; }[] | { id: string; display: string; }[] | { id: string; name: string | null; image?: string | undefined; }[] | { id: string; filename: string; mime_type: string; file_storage_key?: string | undefined; url?: string | undefined; thumbnail_url?: string | undefined; preview_url?: string | undefined; size?: number | null | undefined; created_at?: string | undefined; document_template_id?: string | undefined; }[] | null; removed?: true | undefined; }[]; id: string; actor: { kind: "member" | "agent" | "workflow" | "import"; id: string; name: string; image?: string | undefined; }; at: string; until: string; kind: "change"; created?: true | undefined; } | { comment: { id: string; body: string; mentions: { id: string; name: string; }[]; files?: { id: string; filename: string; mime_type: string; file_storage_key?: string | undefined; url?: string | undefined; thumbnail_url?: string | undefined; preview_url?: string | undefined; size?: number | null | undefined; created_at?: string | undefined; document_template_id?: string | undefined; }[] | undefined; }; id: string; actor: { kind: "member" | "agent" | "workflow" | "import"; id: string; name: string; image?: string | undefined; }; at: string; until: string; kind: "comment"; };
|
|
6
|
+
export type ActivityEntry = { undoable: boolean; changes: { field_id: string; label: string; before: string | number | boolean | { key: string; label: string; }[] | { id: string; display: string; }[] | { id: string; name: string | null; image?: string | undefined; }[] | { id: string; filename: string; mime_type: string; file_storage_key?: string | undefined; url?: string | undefined; thumbnail_url?: string | undefined; preview_url?: string | undefined; size?: number | null | undefined; created_at?: string | undefined; document_template_id?: string | undefined; }[] | null; after: string | number | boolean | { key: string; label: string; }[] | { id: string; display: string; }[] | { id: string; name: string | null; image?: string | undefined; }[] | { id: string; filename: string; mime_type: string; file_storage_key?: string | undefined; url?: string | undefined; thumbnail_url?: string | undefined; preview_url?: string | undefined; size?: number | null | undefined; created_at?: string | undefined; document_template_id?: string | undefined; }[] | null; removed?: true | undefined; }[]; id: string; actor: { kind: "member" | "agent" | "workflow" | "import"; id: string; name: string; image?: string | undefined; }; at: string; until: string; kind: "change"; created?: true | undefined; undo?: true | undefined; undone?: { by: { kind: "member" | "agent" | "workflow" | "import"; id: string; name: string; image?: string | undefined; }; at: string; } | undefined; } | { comment: { id: string; body: string; mentions: { id: string; name: string; }[]; files?: { id: string; filename: string; mime_type: string; file_storage_key?: string | undefined; url?: string | undefined; thumbnail_url?: string | undefined; preview_url?: string | undefined; size?: number | null | undefined; created_at?: string | undefined; document_template_id?: string | undefined; }[] | undefined; }; id: string; actor: { kind: "member" | "agent" | "workflow" | "import"; id: string; name: string; image?: string | undefined; }; at: string; until: string; kind: "comment"; };
|
|
7
|
+
export type ActivityUndoResult = { reverted: { field_id: string; label: string; }[]; kept: { field_id: string; label: string; reason: "changed_since" | "refused"; }[]; };
|
|
7
8
|
export type AppAbsence = { member: { id: string; name: string; }; stand_in: { id: string; name: string; }; starts_on: string; ends_on: string; group_ids: string[]; };
|
|
8
9
|
/** The file an app export made, as a link to download it: no workspace file, so no id, and kept a day. */
|
|
9
10
|
export type ExportedFile = {
|
package/dist/store.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export type Fetcher<T> = (askedAt: number, current: T | undefined) => Promise<T>
|
|
|
4
4
|
export declare function tick(): number;
|
|
5
5
|
/** Ask again every key reading one of `aliases` — every query key where none is named; an unmounted one on its next mount. */
|
|
6
6
|
export declare function refetchReads(aliases: readonly string[] | undefined): void;
|
|
7
|
+
/** `refetchReads`, settled once every key a screen reads now holds an answer asked after this call — or its refusal. */
|
|
8
|
+
export declare function readsAnswered(aliases: readonly string[]): Promise<void>;
|
|
7
9
|
/** The host's push, naming the aliases whose tables moved: each key reading one asked again (`refetchReads`), in one
|
|
8
10
|
* paint. */
|
|
9
11
|
export declare function refetchPushed(aliases: readonly string[]): void;
|
package/dist/viewer.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export declare function readContext(): Promise<AppContext>;
|
|
|
10
10
|
*/
|
|
11
11
|
export declare function useAppContext(): {
|
|
12
12
|
memberId: string | null;
|
|
13
|
+
/** The viewer is a signed-in member of the app's organization: the host named them and told their groups. */
|
|
14
|
+
insider: boolean;
|
|
13
15
|
/** The viewer's member groups, by id; `null` where the host did not say. */
|
|
14
16
|
groupIds: readonly string[] | null;
|
|
15
17
|
/** Who in the organization is away, or will be, and who stands in; `null` where the host did not say. */
|
|
@@ -373,7 +373,7 @@ table's id: read it off a row of the query that renders the badges (`rows[0]?.__
|
|
|
373
373
|
rather than pasting a `tbl_` id, which a copy of the app cannot carry. Pass `undefined` while no
|
|
374
374
|
row is on screen — no request is sent and `counts` is `{}`.
|
|
375
375
|
|
|
376
|
-
## Record activity: `useRecordActivity`
|
|
376
|
+
## Record activity: `useRecordActivity`, `undoRecordChange`
|
|
377
377
|
|
|
378
378
|
Who changed which of a record's fields, and who commented on it — one entry per save
|
|
379
379
|
(`dist/activity.d.ts`). For a JSON app: `entity` is the alias the app's spec names the record's
|
|
@@ -383,6 +383,8 @@ entity under.
|
|
|
383
383
|
const { entries, next, loading, error, available, refetch } = useRecordActivity("job", recordId);
|
|
384
384
|
// The page past it, mounted where the reader asks for more:
|
|
385
385
|
const older = useRecordActivity("job", recordId, { after: next });
|
|
386
|
+
// One change entry put back, as the viewer's own save:
|
|
387
|
+
const { reverted, kept } = await undoRecordChange("job", recordId, entry.id);
|
|
386
388
|
```
|
|
387
389
|
|
|
388
390
|
- **Authority: the app's own read of the record.** The server reaches the record through the
|
|
@@ -392,8 +394,8 @@ const older = useRecordActivity("job", recordId, { after: next });
|
|
|
392
394
|
entry. A field taken out of the app while its table keeps it stays hidden; a field gone from its
|
|
393
395
|
table appears where the version serving when it was written read it, marked `removed`. Comments
|
|
394
396
|
appear only where the app declares `comments`.
|
|
395
|
-
- **Members of the app's organization only** — the trail names its people.
|
|
396
|
-
is `false` and nothing is
|
|
397
|
+
- **Members of the app's organization only** — the trail names its people. For a public visitor,
|
|
398
|
+
a member of another organization, and standalone, `available` is `false` and nothing is asked.
|
|
397
399
|
- **One entry per save.** Edits by one actor on one record less than ten minutes apart read as one
|
|
398
400
|
entry: each field from its value before the first edit to its value after the last, a field edited
|
|
399
401
|
back to where it began dropped (and an entry left with nothing). A comment, another actor or a
|
|
@@ -401,7 +403,9 @@ const older = useRecordActivity("job", recordId, { after: next });
|
|
|
401
403
|
its maker's edits after it, however soon, are entries of their own. `at` is the entry's first
|
|
402
404
|
write, `until` its last.
|
|
403
405
|
- **An entry** (`ActivityEntry`) is `kind: "change"` with `changes`, or `kind: "comment"` with
|
|
404
|
-
`comment: { id, body, mentions }`.
|
|
406
|
+
`comment: { id, body, mentions }`. A change entry also says `undoable` — the viewer may put it
|
|
407
|
+
back — `undone: { by, at }` once someone has, and `undo: true` where it is itself the save that
|
|
408
|
+
put an earlier one back. `actor`: `{ kind, id, name, image? }` — `member` (a person),
|
|
405
409
|
`agent` (an AI acting for a member: the chat, an app's agent, an MCP client — named by that
|
|
406
410
|
member), `workflow` (a workflow on its own, named by the workflow) or `import` (a script over the
|
|
407
411
|
API or the CLI, named by the member whose key ran it). `name` is empty where nothing resolves it.
|
|
@@ -411,6 +415,17 @@ const older = useRecordActivity("job", recordId, { after: next });
|
|
|
411
415
|
`[{ key, label }]` (`readSelect`; each option's label as it was when written), a member field
|
|
412
416
|
`[{ id, name, image? }]`, a link `[{ id, display }]` (`readLinks`), files presigned (`readFiles`),
|
|
413
417
|
`null` for empty.
|
|
418
|
+
- **`undoRecordChange(entity, record_id, entry_id)`** puts one change entry back: each field it
|
|
419
|
+
changed that the app reads returns to what it held before the entry, where the record still holds
|
|
420
|
+
what the entry wrote. It answers `{ reverted, kept }`: the fields put back (`{ field_id, label }`),
|
|
421
|
+
and those left as they stand with a `reason` — `changed_since` (someone changed it since) or
|
|
422
|
+
`refused` (the table refused the write: a lock, a check). A field already holding what it held
|
|
423
|
+
before is in neither. The write is the viewer's own save: they hold `write_data` on the table as
|
|
424
|
+
the call arrives, on a row their own row rule covers, and the record's lock, the table's checks
|
|
425
|
+
and its write rules answer as for any save of theirs. `undoable` is `false`, and the call
|
|
426
|
+
refused, for the entry that made the record, an entry that is an undo, one already put back, and
|
|
427
|
+
a viewer who may not write the record.
|
|
428
|
+
The undo is an entry of its own; the answer arrives once the record's reads are read anew.
|
|
414
429
|
- **Pages**: newest first, about thirty entries each; `next` is where the trail goes on —
|
|
415
430
|
`undefined` at its end. A page may hold fewer entries and still say `next` where the trail it read
|
|
416
431
|
held little the reader may see.
|
|
@@ -428,4 +443,5 @@ const older = useRecordActivity("job", recordId, { after: next });
|
|
|
428
443
|
| `useViewer` | member id (view-as target) | `null` |
|
|
429
444
|
| `useWorkspaceTimezone` / `useWorkspaceCurrency` | ✓ | ✓ |
|
|
430
445
|
| `useComments` / `useCommentCounts` | ✓ when capability declared | ✗ `available: false` |
|
|
431
|
-
| `useRecordActivity` | ✓ the app's organization's members | ✗ `available: false
|
|
446
|
+
| `useRecordActivity` | ✓ the app's organization's members | ✗ `available: false`, nothing asked |
|
|
447
|
+
| `undoRecordChange` | ✓ a member holding the table's write, on a row they may write | ✗ rejects |
|
package/docs/runtime.md
CHANGED
|
@@ -210,6 +210,7 @@ surfaces:
|
|
|
210
210
|
|---|---|---|
|
|
211
211
|
| `query`, `field_options`, `workflow`, `members`, `context`, `upload`, `urlState.get/set`, `openExternal` | yes | yes |
|
|
212
212
|
| `comments.*` | yes | rejects — `"Comments are available only in embedded apps — a signed-in member is required."` |
|
|
213
|
+
| `activity.list`, `activity.undo` | yes, for a member of the app's organization; `activity.undo` for one who may write the record. A host that does not carry the op answers `"Unknown RPC op: activity.undo"` | rejects — `"A record's activity is available only in embedded apps — a signed-in member is required."` |
|
|
213
214
|
| `agentRun` (streaming, internal to `useAgentRun`) | yes | yes |
|
|
214
215
|
| `agentRun.get`, `agentRun.cancel` | yes | yes |
|
|
215
216
|
| `agentRuns` (session history) | **no** — the product host doesn't implement it either (`"Unknown RPC op: agentRuns"`) | rejects — a standalone caller is anonymous, and a history is its member's (`"App agent runs require an authenticated member …"`) |
|
package/docs/security.md
CHANGED
|
@@ -103,6 +103,7 @@ A publicly-shared app (its own origin, or its public link) is reachable by **any
|
|
|
103
103
|
| Agent runs (`useAgentRun`) | **No** — rejected: agent runs require an authenticated member, and each member's run history is private to them (a guessed session id cannot read another member's thread) |
|
|
104
104
|
| Comments (`useComments`) | **No** — members-only |
|
|
105
105
|
| Record activity (`useRecordActivity`) | **No** — the app's organization's members only |
|
|
106
|
+
| Undo of a record's change (`undoRecordChange`) | **No** — a member of the app's organization holding `write_data` on the record's table, on a row their own row rule covers |
|
|
106
107
|
| Member roster (`useMembers`) | **No** — same-org members only (below) |
|
|
107
108
|
|
|
108
109
|
**Warning — `is_current_member` on a public app resolves to the app owner.** An anonymous request has no viewer to bind, so both `is_current_member` and `current_member in_any_group` fall back to the owner's identity and the owner's groups. A "my items" query shows every anonymous visitor the *owner's* items, and a manager-only read template gated on a group the owner belongs to **passes for every anonymous visitor**. Never rely on viewer-bound predicates in a public app.
|
package/docs/workflows.md
CHANGED
|
@@ -392,7 +392,7 @@ body does not look like what you typed.
|
|
|
392
392
|
| `arr.at(i)` | `at(arr, i)` — negative counts from the end |
|
|
393
393
|
| `arr.filter(Boolean)` | `compact(arr)` — drops every falsy element |
|
|
394
394
|
| `[...a, b]` | `concat(a, [b])` |
|
|
395
|
-
| `{ ...a, b: 1 }` | `merge(a, { b: 1 })` — later sources win, as in JS |
|
|
395
|
+
| `{ ...a, b: 1 }` | `merge(merge({}, a), { b: 1 })` — later sources win, as in JS — and reads back as the spread, typed as you wrote it |
|
|
396
396
|
| `{ table_id }` | `{ table_id: table_id }` — in expressions **and** tool inputs |
|
|
397
397
|
| `undefined` | `null` |
|
|
398
398
|
| `xs.push(a, b);` | `xs = concat(xs, [a, b])` — statement form |
|
package/package.json
CHANGED