@lotics/app-sdk 0.100.0 → 0.101.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 +32 -47
- package/dist/agent_stream.d.ts +131 -0
- package/dist/ask_ai.d.ts +27 -0
- package/dist/attachments.d.ts +58 -0
- package/dist/chunk-ARV5FAU5.js +1132 -0
- package/dist/comments.d.ts +89 -0
- package/dist/error_report.d.ts +9 -0
- package/dist/folder_pick.d.ts +8 -0
- package/dist/geolocation.d.ts +42 -0
- package/dist/hooks.d.ts +251 -0
- package/dist/{src/index.d.ts → index.d.ts} +13 -22
- package/dist/index.js +31309 -0
- package/dist/index.js.LEGAL.txt +11 -0
- package/dist/members.d.ts +32 -0
- package/dist/mock.d.ts +37 -0
- package/dist/mount.d.ts +19 -0
- package/dist/new_record.d.ts +37 -0
- package/dist/open_app.d.ts +12 -0
- package/dist/open_external.d.ts +10 -0
- package/dist/overlay.d.ts +25 -0
- package/dist/queries.d.ts +231 -0
- package/dist/recording.d.ts +47 -0
- package/dist/recording_state.d.ts +43 -0
- package/dist/rename_file.d.ts +13 -0
- package/dist/router.d.ts +10 -0
- package/dist/router.js +97 -0
- package/dist/row.d.ts +87 -0
- package/dist/rpc.d.ts +114 -0
- package/dist/select.d.ts +24 -0
- package/dist/shared_types.d.ts +8 -0
- package/dist/store.d.ts +43 -0
- package/dist/types.d.ts +36 -0
- package/dist/upload/optimize.d.ts +30 -0
- package/dist/upload/pipeline.d.ts +36 -0
- package/dist/upload/transport.d.ts +19 -0
- package/dist/url_params.d.ts +55 -0
- package/dist/use_recents.d.ts +15 -0
- package/dist/{src/use_url_state.d.ts → use_url_state.d.ts} +0 -2
- package/dist/viewer.d.ts +41 -0
- package/dist/written.d.ts +77 -0
- package/docs/ai.md +74 -133
- package/docs/data_fetching.md +209 -290
- package/docs/files.md +61 -51
- package/docs/members_and_options.md +93 -63
- package/docs/mutations.md +135 -205
- package/docs/navigation_and_state.md +26 -35
- package/docs/queries.md +144 -207
- package/docs/recipes.md +21 -45
- package/docs/runtime.md +74 -137
- package/docs/security.md +8 -11
- package/docs/workflows.md +189 -174
- package/package.json +27 -28
- package/dist/src/agent_stream.d.ts +0 -200
- package/dist/src/agent_stream.js +0 -314
- package/dist/src/ask_ai.d.ts +0 -40
- package/dist/src/ask_ai.js +0 -35
- package/dist/src/attachments.d.ts +0 -68
- package/dist/src/attachments.js +0 -93
- package/dist/src/comments.d.ts +0 -127
- package/dist/src/comments.js +0 -192
- package/dist/src/download.js +0 -54
- package/dist/src/geolocation.d.ts +0 -64
- package/dist/src/geolocation.js +0 -96
- package/dist/src/hooks.d.ts +0 -781
- package/dist/src/hooks.js +0 -860
- package/dist/src/index.js +0 -34
- package/dist/src/members.d.ts +0 -105
- package/dist/src/members.js +0 -62
- package/dist/src/mock.d.ts +0 -118
- package/dist/src/mock.js +0 -124
- package/dist/src/mount.d.ts +0 -47
- package/dist/src/mount.js +0 -34
- package/dist/src/new_record.d.ts +0 -74
- package/dist/src/new_record.js +0 -117
- package/dist/src/open_app.d.ts +0 -15
- package/dist/src/open_app.js +0 -18
- package/dist/src/open_external.d.ts +0 -16
- package/dist/src/open_external.js +0 -19
- package/dist/src/recording.d.ts +0 -59
- package/dist/src/recording.js +0 -30
- package/dist/src/recording_state.d.ts +0 -59
- package/dist/src/recording_state.js +0 -94
- package/dist/src/router.d.ts +0 -17
- package/dist/src/router.js +0 -144
- package/dist/src/row.d.ts +0 -159
- package/dist/src/row.js +0 -254
- package/dist/src/rpc.d.ts +0 -207
- package/dist/src/rpc.js +0 -904
- package/dist/src/select.d.ts +0 -34
- package/dist/src/select.js +0 -40
- package/dist/src/types.d.ts +0 -115
- package/dist/src/types.js +0 -1
- package/dist/src/upload/optimize.d.ts +0 -54
- package/dist/src/upload/optimize.js +0 -207
- package/dist/src/upload/pipeline.d.ts +0 -55
- package/dist/src/upload/pipeline.js +0 -52
- package/dist/src/upload/transport.d.ts +0 -42
- package/dist/src/upload/transport.js +0 -128
- package/dist/src/url_params.d.ts +0 -93
- package/dist/src/url_params.js +0 -215
- package/dist/src/use_optimistic.d.ts +0 -27
- package/dist/src/use_optimistic.js +0 -27
- package/dist/src/use_recents.d.ts +0 -19
- package/dist/src/use_recents.js +0 -71
- package/dist/src/use_url_state.js +0 -73
- package/dist/src/viewer.d.ts +0 -26
- package/dist/src/viewer.js +0 -47
- /package/dist/{src/download.d.ts → download.d.ts} +0 -0
package/dist/src/ask_ai.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { rpc } from "./rpc.js";
|
|
2
|
-
/**
|
|
3
|
-
* Hand off to the Lotics chat agent — opens the messenger on a FRESH chat
|
|
4
|
-
* seeded with the given files, records, and prompt.
|
|
5
|
-
*
|
|
6
|
-
* Use this for dialogue-shaped work: iterating on a document ("edit this
|
|
7
|
-
* invoice"), drafting from record context, open-ended questions. For
|
|
8
|
-
* structured judgment that commits back into the app's own data
|
|
9
|
-
* (extract/check/match/rank), use `useAgentRun` with a review surface
|
|
10
|
-
* instead — the outcome of `askAi` lands in chat, not in your workflows.
|
|
11
|
-
*
|
|
12
|
-
* The user stays in control: the prompt is only prefilled, attachments are
|
|
13
|
-
* visible in the composer, and nothing is sent until they press send.
|
|
14
|
-
*
|
|
15
|
-
* ```tsx
|
|
16
|
-
* import { askAi } from "@lotics/app-sdk";
|
|
17
|
-
* await askAi({
|
|
18
|
-
* file_ids: [file.id],
|
|
19
|
-
* record_ids: [row.id],
|
|
20
|
-
* prompt: "Update the header of this invoice to match our letterhead.",
|
|
21
|
-
* });
|
|
22
|
-
* ```
|
|
23
|
-
*
|
|
24
|
-
* Only available embedded in Lotics — rejects in standalone/dev mode.
|
|
25
|
-
*/
|
|
26
|
-
export function askAi(args) {
|
|
27
|
-
const hasPayload = (args.prompt ?? "") !== "" ||
|
|
28
|
-
(args.file_ids?.length ?? 0) > 0 ||
|
|
29
|
-
(args.record_ids?.length ?? 0) > 0 ||
|
|
30
|
-
(args.context ?? "") !== "";
|
|
31
|
-
if (!hasPayload) {
|
|
32
|
-
return Promise.reject(new Error("askAi requires at least one of prompt, file_ids, record_ids, context"));
|
|
33
|
-
}
|
|
34
|
-
return rpc("askAi", args);
|
|
35
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import type { ImageFidelity } from "./upload/optimize.js";
|
|
2
|
-
/** A file attached to a surface — its local preview is available immediately,
|
|
3
|
-
* its stored `file_id` once the background upload completes. */
|
|
4
|
-
export interface AttachedFile {
|
|
5
|
-
/** Stable local id — the React key and the `remove(id)` handle. */
|
|
6
|
-
id: string;
|
|
7
|
-
filename: string;
|
|
8
|
-
mime_type: string;
|
|
9
|
-
/** Local object-URL preview, available the INSTANT the file is added (before upload). */
|
|
10
|
-
preview_url: string;
|
|
11
|
-
status: "uploading" | "ready" | "error";
|
|
12
|
-
/** The stored file id, set once `status` is `"ready"` — pass to a workflow/agent. */
|
|
13
|
-
file_id?: string;
|
|
14
|
-
}
|
|
15
|
-
export interface AttachmentsOptions {
|
|
16
|
-
/**
|
|
17
|
-
* The stored ids the DESTINATION now holds. Every queued entry among them has
|
|
18
|
-
* arrived, so it leaves the queue and its preview URL is revoked.
|
|
19
|
-
*
|
|
20
|
-
* This is what makes the queue a record's rather than a composer's: pass what
|
|
21
|
-
* the row came back with and the queue hands over to the pile exactly as the
|
|
22
|
-
* pile takes it, with no window where neither is on screen. Pass nothing and
|
|
23
|
-
* the queue accumulates until `clear`.
|
|
24
|
-
*/
|
|
25
|
-
landed?: readonly string[];
|
|
26
|
-
}
|
|
27
|
-
export interface AttachmentsState {
|
|
28
|
-
/** The current attachments, in the order added. */
|
|
29
|
-
files: AttachedFile[];
|
|
30
|
-
/**
|
|
31
|
-
* Add picked/pasted/dropped files — each shows its local preview at once and
|
|
32
|
-
* uploads in the background. Picking is the app's choice: wire a button to
|
|
33
|
-
* `@lotics/ui` `pickFiles`, or a paste/drop handler, then call this.
|
|
34
|
-
*/
|
|
35
|
-
add: (files: File[], options?: {
|
|
36
|
-
fidelity?: ImageFidelity;
|
|
37
|
-
}) => void;
|
|
38
|
-
/**
|
|
39
|
-
* The same add, ANSWERED WHEN EVERY ONE OF THEM IS STORED — the ids, in the
|
|
40
|
-
* order given. What a write waits on: a record told it holds files whose
|
|
41
|
-
* bytes are still going up names ids the workspace does not have.
|
|
42
|
-
*
|
|
43
|
-
* Rejects with the first refusal instead of answering short, and the queue
|
|
44
|
-
* keeps every entry with its own status, so the reader sees which one failed
|
|
45
|
-
* and the ones that succeeded are not silently written without it.
|
|
46
|
-
*/
|
|
47
|
-
attach: (files: File[], options?: {
|
|
48
|
-
fidelity?: ImageFidelity;
|
|
49
|
-
}) => Promise<string[]>;
|
|
50
|
-
/** Remove one attachment and revoke its preview URL. */
|
|
51
|
-
remove: (id: string) => void;
|
|
52
|
-
/** Remove all attachments and revoke their preview URLs. */
|
|
53
|
-
clear: () => void;
|
|
54
|
-
/** True while any attachment is still uploading — gate Send on it. */
|
|
55
|
-
uploading: boolean;
|
|
56
|
-
/** Stored file ids of the completed uploads — the workflow/agent payload. */
|
|
57
|
-
fileIds: string[];
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* The queue itself, over whichever upload the caller runs through. `upload` is
|
|
61
|
-
* typed by what this needs of it — a stored id — so the transport is the
|
|
62
|
-
* caller's to bring.
|
|
63
|
-
*/
|
|
64
|
-
export declare function useAttachmentQueue(upload: (file: File, options?: {
|
|
65
|
-
fidelity?: ImageFidelity;
|
|
66
|
-
}) => Promise<{
|
|
67
|
-
id: string;
|
|
68
|
-
}>, options?: AttachmentsOptions): AttachmentsState;
|
package/dist/src/attachments.js
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* THE ADD-QUEUE — one owner of the preview → upload → id → revoke lifecycle,
|
|
3
|
-
* apart from the transport that carries the bytes.
|
|
4
|
-
*
|
|
5
|
-
* A surface that takes files has the same four moments wherever it stands: a
|
|
6
|
-
* local preview the instant one is picked, an upload in the background, the
|
|
7
|
-
* stored id when it lands, and the object URL revoked when the entry is done
|
|
8
|
-
* with. What differs is only WHEN an entry is done with — a composer keeps its
|
|
9
|
-
* pile until it sends, a record's section keeps one until the row comes back
|
|
10
|
-
* holding it — so the lifecycle is stated once and the caller says which.
|
|
11
|
-
*
|
|
12
|
-
* Parameterised by the upload rather than reaching for it, so the queue is the
|
|
13
|
-
* same queue wherever the bytes go.
|
|
14
|
-
*/
|
|
15
|
-
import { useCallback, useEffect, useState } from "react";
|
|
16
|
-
let attachSeq = 0;
|
|
17
|
-
/**
|
|
18
|
-
* What the landed ids are joined by to make the effect's one dependency, and
|
|
19
|
-
* split back on. A stored id is an opaque token with no punctuation in it; one
|
|
20
|
-
* that carried this would simply not match, leaving its own tile standing —
|
|
21
|
-
* never taking a different one away.
|
|
22
|
-
*/
|
|
23
|
-
const LANDED_SEPARATOR = ",";
|
|
24
|
-
/**
|
|
25
|
-
* The queue itself, over whichever upload the caller runs through. `upload` is
|
|
26
|
-
* typed by what this needs of it — a stored id — so the transport is the
|
|
27
|
-
* caller's to bring.
|
|
28
|
-
*/
|
|
29
|
-
export function useAttachmentQueue(upload, options) {
|
|
30
|
-
const [files, setFiles] = useState([]);
|
|
31
|
-
const start = useCallback((file, uploadOptions) => {
|
|
32
|
-
const id = `att_${(attachSeq += 1)}`;
|
|
33
|
-
const previewUrl = URL.createObjectURL(file);
|
|
34
|
-
setFiles((prev) => [
|
|
35
|
-
...prev,
|
|
36
|
-
{ id, filename: file.name, mime_type: file.type, preview_url: previewUrl, status: "uploading" },
|
|
37
|
-
]);
|
|
38
|
-
// SETTLES, NEVER REJECTS: the queue entry is where a failure is recorded,
|
|
39
|
-
// so a caller that only wanted the preview holds no rejected promise.
|
|
40
|
-
return upload(file, uploadOptions).then((uploaded) => {
|
|
41
|
-
setFiles((prev) => prev.map((f) => (f.id === id ? { ...f, status: "ready", file_id: uploaded.id } : f)));
|
|
42
|
-
return { kind: "stored", id: uploaded.id };
|
|
43
|
-
}, (cause) => {
|
|
44
|
-
setFiles((prev) => prev.map((f) => (f.id === id ? { ...f, status: "error" } : f)));
|
|
45
|
-
return { kind: "failed", reason: cause instanceof Error ? cause : new Error(String(cause)) };
|
|
46
|
-
});
|
|
47
|
-
}, [upload]);
|
|
48
|
-
const add = useCallback((incoming, uploadOptions) => {
|
|
49
|
-
for (const file of incoming)
|
|
50
|
-
void start(file, uploadOptions);
|
|
51
|
-
}, [start]);
|
|
52
|
-
const attach = useCallback(async (incoming, uploadOptions) => {
|
|
53
|
-
const settled = await Promise.all(incoming.map((file) => start(file, uploadOptions)));
|
|
54
|
-
const refused = settled.find((one) => one.kind === "failed");
|
|
55
|
-
if (refused?.kind === "failed")
|
|
56
|
-
throw refused.reason;
|
|
57
|
-
return settled.flatMap((one) => (one.kind === "stored" ? [one.id] : []));
|
|
58
|
-
}, [start]);
|
|
59
|
-
const remove = useCallback((id) => {
|
|
60
|
-
setFiles((prev) => {
|
|
61
|
-
const target = prev.find((f) => f.id === id);
|
|
62
|
-
if (target)
|
|
63
|
-
URL.revokeObjectURL(target.preview_url);
|
|
64
|
-
return prev.filter((f) => f.id !== id);
|
|
65
|
-
});
|
|
66
|
-
}, []);
|
|
67
|
-
const clear = useCallback(() => {
|
|
68
|
-
setFiles((prev) => {
|
|
69
|
-
for (const f of prev)
|
|
70
|
-
URL.revokeObjectURL(f.preview_url);
|
|
71
|
-
return [];
|
|
72
|
-
});
|
|
73
|
-
}, []);
|
|
74
|
-
// The IDS are the dependency, not the array holding them: a caller reading
|
|
75
|
-
// them off a row builds a fresh array every render.
|
|
76
|
-
const landedKey = options?.landed === undefined ? null : options.landed.join(LANDED_SEPARATOR);
|
|
77
|
-
useEffect(() => {
|
|
78
|
-
if (landedKey === null)
|
|
79
|
-
return;
|
|
80
|
-
const taken = new Set(landedKey === "" ? [] : landedKey.split(LANDED_SEPARATOR));
|
|
81
|
-
setFiles((prev) => {
|
|
82
|
-
const arrived = prev.filter((f) => f.file_id !== undefined && taken.has(f.file_id));
|
|
83
|
-
if (arrived.length === 0)
|
|
84
|
-
return prev;
|
|
85
|
-
for (const f of arrived)
|
|
86
|
-
URL.revokeObjectURL(f.preview_url);
|
|
87
|
-
return prev.filter((f) => !arrived.includes(f));
|
|
88
|
-
});
|
|
89
|
-
}, [landedKey]);
|
|
90
|
-
const uploading = files.some((f) => f.status === "uploading");
|
|
91
|
-
const fileIds = files.flatMap((f) => (f.status === "ready" && f.file_id ? [f.file_id] : []));
|
|
92
|
-
return { files, add, attach, remove, clear, uploading, fileIds };
|
|
93
|
-
}
|
package/dist/src/comments.d.ts
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
/** A file attached to a comment, in the resolved (serving) shape the list returns. */
|
|
2
|
-
export interface AppCommentFile {
|
|
3
|
-
id: string;
|
|
4
|
-
filename: string;
|
|
5
|
-
mime_type: string;
|
|
6
|
-
url?: string;
|
|
7
|
-
thumbnail_url?: string;
|
|
8
|
-
preview_url?: string;
|
|
9
|
-
}
|
|
10
|
-
/** A comment author's display identity, resolved server-side. */
|
|
11
|
-
export interface AppCommentAuthor {
|
|
12
|
-
/** The member's display name. Null when the id no longer resolves in the org
|
|
13
|
-
* (a removed member) — fall back explicitly, as with `readMembers`. */
|
|
14
|
-
name: string | null;
|
|
15
|
-
/** Presigned avatar URL, when the member has one. */
|
|
16
|
-
image?: string | null;
|
|
17
|
-
}
|
|
18
|
-
/** One record comment, as returned by the list op. */
|
|
19
|
-
export interface AppComment {
|
|
20
|
-
id: string;
|
|
21
|
-
record_id: string;
|
|
22
|
-
table_id: string;
|
|
23
|
-
/** The author's member id. */
|
|
24
|
-
member_id: string;
|
|
25
|
-
/**
|
|
26
|
-
* The author's display identity, resolved server-side on EVERY comment the
|
|
27
|
-
* server returns — list, create and update alike — including one written from
|
|
28
|
-
* another app by someone the record references nowhere. A thread crossing a
|
|
29
|
-
* role boundary is legible without the app declaring member access it does not
|
|
30
|
-
* otherwise need.
|
|
31
|
-
*
|
|
32
|
-
* Optional for one reason only: the optimistic row `createComment` renders is
|
|
33
|
-
* built HERE, from a context that carries the viewer's id and not their name,
|
|
34
|
-
* and inventing a display identity for it would render as somebody. It is
|
|
35
|
-
* replaced by the server's row when the refetch lands.
|
|
36
|
-
*/
|
|
37
|
-
author?: AppCommentAuthor;
|
|
38
|
-
content: string;
|
|
39
|
-
files: AppCommentFile[] | null;
|
|
40
|
-
workspace_id: string;
|
|
41
|
-
created_at: string;
|
|
42
|
-
updated_at: string;
|
|
43
|
-
}
|
|
44
|
-
export interface CommentsState {
|
|
45
|
-
/** Comments on the record, oldest first. Empty while loading or unavailable. */
|
|
46
|
-
comments: AppComment[];
|
|
47
|
-
/** True on the initial load only — false during background revalidation. */
|
|
48
|
-
loading: boolean;
|
|
49
|
-
error: string | null;
|
|
50
|
-
/**
|
|
51
|
-
* True only when a member is signed in AND the app declared the `comments`
|
|
52
|
-
* capability in its manifest. False in a standalone / public (anonymous) app,
|
|
53
|
-
* and for any app that didn't opt in. Gate the composer on this; mutations
|
|
54
|
-
* reject (and the backend re-checks the capability) when false.
|
|
55
|
-
*/
|
|
56
|
-
available: boolean;
|
|
57
|
-
/**
|
|
58
|
-
* Post a new comment as the viewing member. `file_ids` are ids from
|
|
59
|
-
* `useFileUpload().upload()`. No-op for empty content with no files.
|
|
60
|
-
*/
|
|
61
|
-
createComment: (input: {
|
|
62
|
-
content: string;
|
|
63
|
-
file_ids?: string[];
|
|
64
|
-
}) => Promise<void>;
|
|
65
|
-
/**
|
|
66
|
-
* Edit a comment (author-only, enforced server-side). Pass `files` to replace
|
|
67
|
-
* the attachment set; omit it to keep the comment's current files — a
|
|
68
|
-
* text-only edit never silently drops attachments.
|
|
69
|
-
*/
|
|
70
|
-
updateComment: (id: string, input: {
|
|
71
|
-
content: string;
|
|
72
|
-
files?: AppCommentFile[];
|
|
73
|
-
}) => Promise<void>;
|
|
74
|
-
/** Delete a comment (author-only, enforced server-side). */
|
|
75
|
-
deleteComment: (id: string) => Promise<void>;
|
|
76
|
-
/** Re-pull the thread from the server (e.g. after an external change). */
|
|
77
|
-
refetch: () => void;
|
|
78
|
-
}
|
|
79
|
-
export interface UseCommentsArgs {
|
|
80
|
-
/** The record whose comments to read and write. */
|
|
81
|
-
record_id: string;
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* `record_id` must be a REAL id. The fetch keys on `available` alone, so an
|
|
85
|
-
* empty string is not skipped — it fetches comments for nothing. When the id
|
|
86
|
-
* comes off a row's `__source_record_id` addressing column, narrow it first: a
|
|
87
|
-
* grouped query carries none, and a hook cannot be called conditionally, so the
|
|
88
|
-
* guard belongs at the component that renders the panel.
|
|
89
|
-
*
|
|
90
|
-
* ```tsx
|
|
91
|
-
* // In a record screen, where an id definitionally exists:
|
|
92
|
-
* const { comments, available, createComment } = useComments({ record_id: recordId });
|
|
93
|
-
*
|
|
94
|
-
* // From a row, gate the whole panel rather than passing a placeholder:
|
|
95
|
-
* {row.__source_record_id && <CommentsPanel recordId={row.__source_record_id} />}
|
|
96
|
-
* ```
|
|
97
|
-
*/
|
|
98
|
-
export declare function useComments(args: UseCommentsArgs): CommentsState;
|
|
99
|
-
export interface CommentCountsState {
|
|
100
|
-
/** `{ record_id: count }`. Empty while loading or unavailable. */
|
|
101
|
-
counts: Record<string, number>;
|
|
102
|
-
/** True on the initial load only — false during background revalidation. */
|
|
103
|
-
loading: boolean;
|
|
104
|
-
error: string | null;
|
|
105
|
-
/** Same gate as `useComments` — member signed in AND the app declared `comments`. */
|
|
106
|
-
available: boolean;
|
|
107
|
-
/** Re-pull the counts (e.g. after posting a comment). */
|
|
108
|
-
refetch: () => void;
|
|
109
|
-
}
|
|
110
|
-
export interface UseCommentCountsArgs {
|
|
111
|
-
/** The table whose per-record comment counts to read. Read it off a row
|
|
112
|
-
* (`__source_table_id`) rather than pasting a `tbl_` id — a copy of the app
|
|
113
|
-
* runs over other tables. `undefined` (no row on screen yet) sends no
|
|
114
|
-
* request and answers `{}`. */
|
|
115
|
-
table_id: string | undefined;
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Per-record comment counts for a whole table, as `{ record_id: count }` — for
|
|
119
|
-
* row badges (a count beside each row) without loading any comment content. One
|
|
120
|
-
* server-side `GROUP BY`. App-authority like {@link useComments}.
|
|
121
|
-
*
|
|
122
|
-
* ```tsx
|
|
123
|
-
* const { counts } = useCommentCounts({ table_id: rows[0]?.__source_table_id });
|
|
124
|
-
* // counts[row.__source_record_id] ?? 0
|
|
125
|
-
* ```
|
|
126
|
-
*/
|
|
127
|
-
export declare function useCommentCounts(args: UseCommentCountsArgs): CommentCountsState;
|
package/dist/src/comments.js
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `useComments` — read and write the comments on a record from a custom-code app.
|
|
3
|
-
*
|
|
4
|
-
* Comments are a *members-only* surface but, like `useQuery` / `useWorkflow`,
|
|
5
|
-
* run under the app's authority — NOT the member's table IAM. An app user with
|
|
6
|
-
* the app open (and the app declaring `comments`) may read/write comments on any
|
|
7
|
-
* record the app reaches, regardless of their own table access:
|
|
8
|
-
*
|
|
9
|
-
* - read / write gated by `app:use` + the `comments` capability (no table grant)
|
|
10
|
-
* - the comment's author is still the real viewing member (correct attribution)
|
|
11
|
-
* - edit / delete are author-only, checked against the viewer
|
|
12
|
-
*
|
|
13
|
-
* `useCommentCounts` returns per-record counts for a table (row badges) — a
|
|
14
|
-
* single server-side `GROUP BY`, no comment content shipped.
|
|
15
|
-
*
|
|
16
|
-
* The bridged host (`app_iframe_host`) proxies each op to the app-scoped
|
|
17
|
-
* `/v1/apps/{id}/…/comments` endpoints. A standalone (public) app has no
|
|
18
|
-
* signed-in member: `available` is false, lists stay empty, and a mutation
|
|
19
|
-
* rejects. Check `available` before rendering a composer.
|
|
20
|
-
*
|
|
21
|
-
* Freshness: SWR-cached, revalidate on focus / reconnect, and an explicit
|
|
22
|
-
* refetch after every mutation — so another viewer's comment appears on the
|
|
23
|
-
* next focus or `refetch()`, not on its own.
|
|
24
|
-
*
|
|
25
|
-
* This is the one read hook that does NOT keep itself current, and the reason
|
|
26
|
-
* is worth knowing rather than guessing: the realtime channel carries **record**
|
|
27
|
-
* changes, and a comment is its own entity that emits no record event. Your
|
|
28
|
-
* QUERIES do get pushed (`docs/data_fetching.md`) — do not read this as "apps
|
|
29
|
-
* have no realtime" and go build a poll.
|
|
30
|
-
*/
|
|
31
|
-
import { useCallback } from "react";
|
|
32
|
-
import useSWR from "swr";
|
|
33
|
-
import { rpc } from "./rpc.js";
|
|
34
|
-
import { useAppContext } from "./viewer.js";
|
|
35
|
-
/**
|
|
36
|
-
* The attachment set as ids — what the update endpoint wants.
|
|
37
|
-
*
|
|
38
|
-
* Sending whole file objects instead would be a full-snapshot write of metadata
|
|
39
|
-
* this client does not own: a filename or a storage key echoed back from a read
|
|
40
|
-
* lands in the row verbatim, so a stale copy overwrites what is stored. An id
|
|
41
|
-
* the server resolves for itself cannot do that.
|
|
42
|
-
*/
|
|
43
|
-
function toFileIds(files) {
|
|
44
|
-
return (files ?? []).map((f) => f.id);
|
|
45
|
-
}
|
|
46
|
-
let optimisticCounter = 0;
|
|
47
|
-
/**
|
|
48
|
-
* `record_id` must be a REAL id. The fetch keys on `available` alone, so an
|
|
49
|
-
* empty string is not skipped — it fetches comments for nothing. When the id
|
|
50
|
-
* comes off a row's `__source_record_id` addressing column, narrow it first: a
|
|
51
|
-
* grouped query carries none, and a hook cannot be called conditionally, so the
|
|
52
|
-
* guard belongs at the component that renders the panel.
|
|
53
|
-
*
|
|
54
|
-
* ```tsx
|
|
55
|
-
* // In a record screen, where an id definitionally exists:
|
|
56
|
-
* const { comments, available, createComment } = useComments({ record_id: recordId });
|
|
57
|
-
*
|
|
58
|
-
* // From a row, gate the whole panel rather than passing a placeholder:
|
|
59
|
-
* {row.__source_record_id && <CommentsPanel recordId={row.__source_record_id} />}
|
|
60
|
-
* ```
|
|
61
|
-
*/
|
|
62
|
-
export function useComments(args) {
|
|
63
|
-
const { record_id } = args;
|
|
64
|
-
const { memberId, commentsEnabled, resolved } = useAppContext();
|
|
65
|
-
const available = memberId != null && commentsEnabled;
|
|
66
|
-
const swr = useSWR(available ? ["app-comments", record_id] : null, () => rpc("comments.list", { record_id }), {
|
|
67
|
-
shouldRetryOnError: false,
|
|
68
|
-
});
|
|
69
|
-
const comments = swr.data?.comments ?? [];
|
|
70
|
-
const refetch = useCallback(() => {
|
|
71
|
-
void swr.mutate();
|
|
72
|
-
}, [swr]);
|
|
73
|
-
const createComment = useCallback(async (input) => {
|
|
74
|
-
if (!available || memberId == null) {
|
|
75
|
-
throw new Error("Comments are not available in this app.");
|
|
76
|
-
}
|
|
77
|
-
const content = input.content.trim();
|
|
78
|
-
const fileIds = input.file_ids ?? [];
|
|
79
|
-
if (!content && fileIds.length === 0)
|
|
80
|
-
return;
|
|
81
|
-
const now = new Date().toISOString();
|
|
82
|
-
const optimistic = {
|
|
83
|
-
id: `cmt_optimistic_${Date.now()}_${optimisticCounter++}`,
|
|
84
|
-
record_id,
|
|
85
|
-
// table_id / workspace_id resolve on the refetch (server-owned); the
|
|
86
|
-
// optimistic row only needs to render until then.
|
|
87
|
-
table_id: "",
|
|
88
|
-
member_id: memberId,
|
|
89
|
-
content,
|
|
90
|
-
// The real attachments resolve on the refetch; show none meanwhile.
|
|
91
|
-
files: null,
|
|
92
|
-
workspace_id: "",
|
|
93
|
-
created_at: now,
|
|
94
|
-
updated_at: now,
|
|
95
|
-
};
|
|
96
|
-
await swr.mutate(async () => {
|
|
97
|
-
await rpc("comments.create", {
|
|
98
|
-
record_id,
|
|
99
|
-
content,
|
|
100
|
-
file_ids: fileIds.length > 0 ? fileIds : undefined,
|
|
101
|
-
});
|
|
102
|
-
return rpc("comments.list", { record_id });
|
|
103
|
-
}, {
|
|
104
|
-
optimisticData: (current) => ({
|
|
105
|
-
comments: [...(current?.comments ?? []), optimistic],
|
|
106
|
-
}),
|
|
107
|
-
rollbackOnError: true,
|
|
108
|
-
revalidate: false,
|
|
109
|
-
populateCache: true,
|
|
110
|
-
});
|
|
111
|
-
}, [available, memberId, record_id, swr]);
|
|
112
|
-
const updateComment = useCallback(async (id, input) => {
|
|
113
|
-
if (!available)
|
|
114
|
-
throw new Error("Comments are not available in this app.");
|
|
115
|
-
const content = input.content.trim();
|
|
116
|
-
// Omitted files → keep the comment's current attachments (never drop them
|
|
117
|
-
// on a text-only edit). The update endpoint replaces the whole set.
|
|
118
|
-
const currentFiles = input.files !== undefined
|
|
119
|
-
? input.files
|
|
120
|
-
: (swr.data?.comments.find((c) => c.id === id)?.files ?? null);
|
|
121
|
-
const now = new Date().toISOString();
|
|
122
|
-
await swr.mutate(async () => {
|
|
123
|
-
await rpc("comments.update", {
|
|
124
|
-
record_id,
|
|
125
|
-
comment_id: id,
|
|
126
|
-
content,
|
|
127
|
-
file_ids: toFileIds(currentFiles),
|
|
128
|
-
});
|
|
129
|
-
return rpc("comments.list", { record_id });
|
|
130
|
-
}, {
|
|
131
|
-
optimisticData: (current) => ({
|
|
132
|
-
comments: (current?.comments ?? []).map((c) => c.id === id ? { ...c, content, files: currentFiles, updated_at: now } : c),
|
|
133
|
-
}),
|
|
134
|
-
rollbackOnError: true,
|
|
135
|
-
revalidate: false,
|
|
136
|
-
populateCache: true,
|
|
137
|
-
});
|
|
138
|
-
}, [available, record_id, swr]);
|
|
139
|
-
const deleteComment = useCallback(async (id) => {
|
|
140
|
-
if (!available)
|
|
141
|
-
throw new Error("Comments are not available in this app.");
|
|
142
|
-
await swr.mutate(async () => {
|
|
143
|
-
await rpc("comments.delete", { record_id, comment_id: id });
|
|
144
|
-
return rpc("comments.list", { record_id });
|
|
145
|
-
}, {
|
|
146
|
-
optimisticData: (current) => ({
|
|
147
|
-
comments: (current?.comments ?? []).filter((c) => c.id !== id),
|
|
148
|
-
}),
|
|
149
|
-
rollbackOnError: true,
|
|
150
|
-
revalidate: false,
|
|
151
|
-
populateCache: true,
|
|
152
|
-
});
|
|
153
|
-
}, [available, record_id, swr]);
|
|
154
|
-
return {
|
|
155
|
-
comments,
|
|
156
|
-
// Unavailable resolves instantly (no fetch); available shows the initial
|
|
157
|
-
// load until the list lands. Waiting on context counts as loading.
|
|
158
|
-
loading: available ? swr.isLoading : !resolved,
|
|
159
|
-
error: swr.error ? swr.error.message : null,
|
|
160
|
-
available,
|
|
161
|
-
createComment,
|
|
162
|
-
updateComment,
|
|
163
|
-
deleteComment,
|
|
164
|
-
refetch,
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Per-record comment counts for a whole table, as `{ record_id: count }` — for
|
|
169
|
-
* row badges (a count beside each row) without loading any comment content. One
|
|
170
|
-
* server-side `GROUP BY`. App-authority like {@link useComments}.
|
|
171
|
-
*
|
|
172
|
-
* ```tsx
|
|
173
|
-
* const { counts } = useCommentCounts({ table_id: rows[0]?.__source_table_id });
|
|
174
|
-
* // counts[row.__source_record_id] ?? 0
|
|
175
|
-
* ```
|
|
176
|
-
*/
|
|
177
|
-
export function useCommentCounts(args) {
|
|
178
|
-
const { table_id } = args;
|
|
179
|
-
const { memberId, commentsEnabled, resolved } = useAppContext();
|
|
180
|
-
const available = memberId != null && commentsEnabled;
|
|
181
|
-
const swr = useSWR(available && table_id !== undefined ? ["app-comment-counts", table_id] : null, () => rpc("comments.counts", { table_id }), { shouldRetryOnError: false });
|
|
182
|
-
const refetch = useCallback(() => {
|
|
183
|
-
void swr.mutate();
|
|
184
|
-
}, [swr]);
|
|
185
|
-
return {
|
|
186
|
-
counts: swr.data ?? {},
|
|
187
|
-
loading: available ? swr.isLoading : !resolved,
|
|
188
|
-
error: swr.error ? swr.error.message : null,
|
|
189
|
-
available,
|
|
190
|
-
refetch,
|
|
191
|
-
};
|
|
192
|
-
}
|
package/dist/src/download.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Save bytes the app generated in the browser to the visitor's device.
|
|
3
|
-
*
|
|
4
|
-
* This is the client-side counterpart to `openExternal`. The platform's other
|
|
5
|
-
* download path is server-side: a workflow generates a file, it comes back in
|
|
6
|
-
* `WorkflowResult.files[]` with a `.url`, and the app opens that URL with
|
|
7
|
-
* `openExternal`. But data an app builds *in the browser* (an .xlsx exported
|
|
8
|
-
* from a query result, a CSV, a generated PDF) never gets a server URL — there
|
|
9
|
-
* was no primitive to save it. This is that primitive.
|
|
10
|
-
*
|
|
11
|
-
* It is pure DOM, not an RPC: the embed iframe is sandboxed with
|
|
12
|
-
* `allow-downloads` (see `app_iframe_host`), so a same-origin blob download
|
|
13
|
-
* triggered from a user gesture is permitted without host mediation. In
|
|
14
|
-
* standalone (public) mode the app is a normal top-level page, where downloads
|
|
15
|
-
* always work. Call it synchronously from the click handler that produced the
|
|
16
|
-
* bytes so the browser attributes the download to the user gesture.
|
|
17
|
-
*
|
|
18
|
-
* ```tsx
|
|
19
|
-
* import { downloadFile } from "@lotics/app-sdk";
|
|
20
|
-
* import { buildDataWorkbook, exportWorkbook } from "@lotics/xlsx";
|
|
21
|
-
*
|
|
22
|
-
* const wb = buildDataWorkbook({ columns, rows });
|
|
23
|
-
* downloadFile("report.xlsx", exportWorkbook(wb), XLSX_MIME);
|
|
24
|
-
* ```
|
|
25
|
-
*/
|
|
26
|
-
const DEFAULT_MIME = "application/octet-stream";
|
|
27
|
-
function toBlob(data, mimeType) {
|
|
28
|
-
if (data instanceof Blob)
|
|
29
|
-
return data;
|
|
30
|
-
if (typeof data === "string")
|
|
31
|
-
return new Blob([data], { type: mimeType });
|
|
32
|
-
// Re-wrap the bytes: the DOM lib types a generic `Uint8Array<ArrayBufferLike>`
|
|
33
|
-
// as incompatible with `BlobPart`, and `new Uint8Array(data)` narrows it to a
|
|
34
|
-
// fresh `ArrayBuffer`-backed view (the pattern `save_workbook_version` uses).
|
|
35
|
-
return new Blob([new Uint8Array(data)], { type: mimeType });
|
|
36
|
-
}
|
|
37
|
-
export function downloadFile(filename, data, mimeType) {
|
|
38
|
-
const url = URL.createObjectURL(toBlob(data, mimeType ?? DEFAULT_MIME));
|
|
39
|
-
try {
|
|
40
|
-
const a = document.createElement("a");
|
|
41
|
-
a.href = url;
|
|
42
|
-
a.download = filename;
|
|
43
|
-
a.rel = "noopener";
|
|
44
|
-
a.style.display = "none";
|
|
45
|
-
document.body.appendChild(a);
|
|
46
|
-
a.click();
|
|
47
|
-
a.remove();
|
|
48
|
-
}
|
|
49
|
-
finally {
|
|
50
|
-
// Revoke after the current task so the navigation to the blob URL has
|
|
51
|
-
// started — revoking synchronously can cancel the download in some browsers.
|
|
52
|
-
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Device location for geofenced app actions.
|
|
3
|
-
*
|
|
4
|
-
* The host grants the app iframe the `geolocation` Permissions-Policy, so app
|
|
5
|
-
* code reads the device position directly through the browser — no RPC bridge.
|
|
6
|
-
* This module wraps that with permission handling, a timeout, and a geofence
|
|
7
|
-
* check, and returns a STRUCTURED outcome (`denied` / `unavailable` / `outside`)
|
|
8
|
-
* so the app renders its own guidance. A terse, untranslated "permission denied"
|
|
9
|
-
* dead-end is the single biggest reason a geofenced check-in fails in the field,
|
|
10
|
-
* so the messaging belongs to the app, not buried in here.
|
|
11
|
-
*
|
|
12
|
-
* Self-contained on purpose: `@lotics/app-sdk` is published to npm with a minimal
|
|
13
|
-
* dependency set and cannot pull in the private `@lotics/shared`, so the haversine
|
|
14
|
-
* lives here rather than being imported.
|
|
15
|
-
*/
|
|
16
|
-
/** One allowed circular zone: center `[latitude, longitude]` and a radius in meters. */
|
|
17
|
-
export interface GeofenceZone {
|
|
18
|
-
coordinates: [number, number];
|
|
19
|
-
radius: number;
|
|
20
|
-
}
|
|
21
|
-
/** The resolved device position handed back on a successful check. */
|
|
22
|
-
export interface GeoCoords {
|
|
23
|
-
latitude: number;
|
|
24
|
-
longitude: number;
|
|
25
|
-
/** Accuracy radius of the fix, in meters (browser-reported). */
|
|
26
|
-
accuracy: number;
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Result of `requestGeofencedLocation`. On failure, `reason` is a stable code the
|
|
30
|
-
* app maps to its own (localized) message and recovery UI:
|
|
31
|
-
* - `denied` — the user/browser has not granted location permission.
|
|
32
|
-
* - `unavailable` — no fix: location services off, no GPS, or the request timed out.
|
|
33
|
-
* - `outside` — a fix was obtained but it is not within any allowed zone.
|
|
34
|
-
*/
|
|
35
|
-
export type GeofenceOutcome = {
|
|
36
|
-
ok: true;
|
|
37
|
-
coords: GeoCoords;
|
|
38
|
-
} | {
|
|
39
|
-
ok: false;
|
|
40
|
-
reason: "denied" | "unavailable" | "outside";
|
|
41
|
-
};
|
|
42
|
-
/** Options for `requestGeofencedLocation`. */
|
|
43
|
-
export interface GeofenceOptions {
|
|
44
|
-
/** Max wait for a position fix before resolving `unavailable`. Default 15000ms. */
|
|
45
|
-
timeoutMs?: number;
|
|
46
|
-
}
|
|
47
|
-
/** True when `[latitude, longitude]` falls within `zone`'s radius of its center. */
|
|
48
|
-
export declare function isWithinZone(latitude: number, longitude: number, zone: GeofenceZone): boolean;
|
|
49
|
-
/**
|
|
50
|
-
* Get the device position and check it against the allowed zones.
|
|
51
|
-
*
|
|
52
|
-
* Returns `{ ok: true, coords }` when a fix lands inside any zone — the app can
|
|
53
|
-
* forward `coords` to a workflow to record where the action happened. Otherwise
|
|
54
|
-
* `{ ok: false, reason }`; render guidance per `reason`. An empty `zones` array
|
|
55
|
-
* means "no geofence" and resolves `ok` with the raw position (use it for a
|
|
56
|
-
* plain location read).
|
|
57
|
-
*
|
|
58
|
-
* ```tsx
|
|
59
|
-
* const r = await requestGeofencedLocation(DEPOTS);
|
|
60
|
-
* if (!r.ok) return showGeofenceError(r.reason);
|
|
61
|
-
* await chamCong();
|
|
62
|
-
* ```
|
|
63
|
-
*/
|
|
64
|
-
export declare function requestGeofencedLocation(zones: GeofenceZone[], opts?: GeofenceOptions): Promise<GeofenceOutcome>;
|