lemma-sdk 0.2.30 → 0.2.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +213 -51
- package/dist/react/index.d.ts +23 -1
- package/dist/react/index.js +11 -0
- package/dist/react/useAgentInputSchema.d.ts +19 -0
- package/dist/react/useAgentInputSchema.js +73 -0
- package/dist/react/useAgentRun.js +18 -20
- package/dist/react/useAgentRuns.d.ts +33 -0
- package/dist/react/useAgentRuns.js +149 -0
- package/dist/react/useAssistantRun.js +10 -9
- package/dist/react/useAssistantSession.js +21 -25
- package/dist/react/useBulkRecords.js +9 -16
- package/dist/react/useConversation.js +24 -8
- package/dist/react/useConversations.d.ts +4 -0
- package/dist/react/useConversations.js +49 -3
- package/dist/react/useCreateRecord.d.ts +33 -3
- package/dist/react/useCreateRecord.js +20 -17
- package/dist/react/useCurrentUser.d.ts +14 -0
- package/dist/react/useCurrentUser.js +68 -0
- package/dist/react/useDeleteRecord.js +9 -16
- package/dist/react/useFlowRunHistory.js +1 -5
- package/dist/react/useFlowSession.js +41 -33
- package/dist/react/useForeignKeyOptions.d.ts +18 -0
- package/dist/react/useForeignKeyOptions.js +26 -15
- package/dist/react/useFunctionRun.d.ts +36 -0
- package/dist/react/useFunctionRun.js +30 -0
- package/dist/react/useFunctionRuns.d.ts +33 -0
- package/dist/react/useFunctionRuns.js +149 -0
- package/dist/react/useFunctionSession.js +37 -29
- package/dist/react/useJoinedRecords.d.ts +57 -2
- package/dist/react/useJoinedRecords.js +77 -27
- package/dist/react/useMembers.d.ts +4 -0
- package/dist/react/useMembers.js +55 -16
- package/dist/react/useOrganizationMembers.d.ts +26 -0
- package/dist/react/useOrganizationMembers.js +113 -0
- package/dist/react/usePodAccess.d.ts +22 -0
- package/dist/react/usePodAccess.js +128 -0
- package/dist/react/useRecord.d.ts +16 -0
- package/dist/react/useRecord.js +24 -13
- package/dist/react/useRecordForm.d.ts +42 -3
- package/dist/react/useRecordForm.js +44 -24
- package/dist/react/useRecords.d.ts +2 -0
- package/dist/react/useRecords.js +62 -22
- package/dist/react/useReferencingRecords.d.ts +66 -0
- package/dist/react/useReferencingRecords.js +159 -0
- package/dist/react/useRelatedRecords.d.ts +17 -0
- package/dist/react/useRelatedRecords.js +28 -21
- package/dist/react/useReverseRelatedRecords.d.ts +21 -0
- package/dist/react/useReverseRelatedRecords.js +30 -21
- package/dist/react/useSchemaForm.js +1 -13
- package/dist/react/useTable.js +24 -13
- package/dist/react/useTables.d.ts +4 -0
- package/dist/react/useTables.js +57 -15
- package/dist/react/useTaskSession.js +11 -22
- package/dist/react/useUpdateRecord.d.ts +34 -3
- package/dist/react/useUpdateRecord.js +21 -17
- package/dist/react/useWorkflowResume.d.ts +18 -0
- package/dist/react/useWorkflowResume.js +45 -0
- package/dist/react/useWorkflowRun.d.ts +21 -0
- package/dist/react/useWorkflowRun.js +49 -0
- package/dist/react/useWorkflowRuns.d.ts +33 -0
- package/dist/react/useWorkflowRuns.js +149 -0
- package/dist/react/useWorkflowStart.js +20 -27
- package/dist/react/utils.d.ts +5 -0
- package/dist/react/utils.js +25 -0
- package/dist/types.d.ts +1 -0
- package/package.json +2 -4
- package/dist/react/components/AssistantChrome.d.ts +0 -86
- package/dist/react/components/AssistantChrome.js +0 -48
- package/dist/react/components/AssistantEmbedded.d.ts +0 -10
- package/dist/react/components/AssistantEmbedded.js +0 -15
- package/dist/react/components/AssistantExperience.d.ts +0 -96
- package/dist/react/components/AssistantExperience.js +0 -1294
- package/dist/react/components/assistant-types.d.ts +0 -80
- package/dist/react/components/assistant-types.js +0 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { normalizeError } from "./utils.js";
|
|
3
|
+
export function useOrganizationMembers({ client, organizationId, enabled = true, autoLoad = true, limit = 100, pageToken, }) {
|
|
4
|
+
const [members, setMembers] = useState([]);
|
|
5
|
+
const [total, setTotal] = useState(0);
|
|
6
|
+
const [nextPageToken, setNextPageToken] = useState(null);
|
|
7
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
8
|
+
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
|
9
|
+
const [error, setError] = useState(null);
|
|
10
|
+
const trimmedOrganizationId = organizationId.trim();
|
|
11
|
+
const isEnabled = enabled && trimmedOrganizationId.length > 0;
|
|
12
|
+
const refresh = useCallback(async (overrides = {}, signal) => {
|
|
13
|
+
if (!isEnabled) {
|
|
14
|
+
setMembers([]);
|
|
15
|
+
setTotal(0);
|
|
16
|
+
setNextPageToken(null);
|
|
17
|
+
setError(null);
|
|
18
|
+
setIsLoading(false);
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
setIsLoading(true);
|
|
22
|
+
setError(null);
|
|
23
|
+
try {
|
|
24
|
+
const response = await client.organizations.members.list(trimmedOrganizationId, {
|
|
25
|
+
limit: overrides.limit ?? limit,
|
|
26
|
+
pageToken: overrides.pageToken ?? pageToken,
|
|
27
|
+
});
|
|
28
|
+
if (signal?.aborted)
|
|
29
|
+
return [];
|
|
30
|
+
const nextMembers = response.items ?? [];
|
|
31
|
+
setMembers(nextMembers);
|
|
32
|
+
setTotal(response.total ?? nextMembers.length);
|
|
33
|
+
setNextPageToken(response.next_page_token ?? null);
|
|
34
|
+
return nextMembers;
|
|
35
|
+
}
|
|
36
|
+
catch (refreshError) {
|
|
37
|
+
if (signal?.aborted)
|
|
38
|
+
return [];
|
|
39
|
+
const normalized = normalizeError(refreshError, "Failed to load organization members.");
|
|
40
|
+
setError(normalized);
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
if (!signal?.aborted)
|
|
45
|
+
setIsLoading(false);
|
|
46
|
+
}
|
|
47
|
+
}, [client, isEnabled, limit, pageToken, trimmedOrganizationId]);
|
|
48
|
+
const loadMore = useCallback(async (overrides = {}) => {
|
|
49
|
+
if (!isEnabled || !nextPageToken || isLoading || isLoadingMore) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
setIsLoadingMore(true);
|
|
53
|
+
setError(null);
|
|
54
|
+
try {
|
|
55
|
+
const response = await client.organizations.members.list(trimmedOrganizationId, {
|
|
56
|
+
limit: overrides.limit ?? limit,
|
|
57
|
+
pageToken: nextPageToken,
|
|
58
|
+
});
|
|
59
|
+
const moreMembers = response.items ?? [];
|
|
60
|
+
setMembers((previous) => [...previous, ...moreMembers]);
|
|
61
|
+
setTotal(response.total ?? members.length + moreMembers.length);
|
|
62
|
+
setNextPageToken(response.next_page_token ?? null);
|
|
63
|
+
return moreMembers;
|
|
64
|
+
}
|
|
65
|
+
catch (loadError) {
|
|
66
|
+
const normalized = normalizeError(loadError, "Failed to load more organization members.");
|
|
67
|
+
setError(normalized);
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
setIsLoadingMore(false);
|
|
72
|
+
}
|
|
73
|
+
}, [client, isEnabled, isLoading, isLoadingMore, limit, members.length, nextPageToken, trimmedOrganizationId]);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (!isEnabled) {
|
|
76
|
+
setMembers([]);
|
|
77
|
+
setTotal(0);
|
|
78
|
+
setNextPageToken(null);
|
|
79
|
+
setError(null);
|
|
80
|
+
setIsLoading(false);
|
|
81
|
+
setIsLoadingMore(false);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (!autoLoad)
|
|
85
|
+
return;
|
|
86
|
+
const controller = new AbortController();
|
|
87
|
+
let cancelled = false;
|
|
88
|
+
(async () => {
|
|
89
|
+
try {
|
|
90
|
+
await refresh({}, controller.signal);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
if (!cancelled) {
|
|
94
|
+
setError(normalizeError(new Error("Failed to load organization members."), "Failed to load organization members."));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
})();
|
|
98
|
+
return () => {
|
|
99
|
+
cancelled = true;
|
|
100
|
+
controller.abort();
|
|
101
|
+
};
|
|
102
|
+
}, [autoLoad, isEnabled, refresh]);
|
|
103
|
+
return useMemo(() => ({
|
|
104
|
+
members,
|
|
105
|
+
total,
|
|
106
|
+
nextPageToken,
|
|
107
|
+
isLoading,
|
|
108
|
+
isLoadingMore,
|
|
109
|
+
error,
|
|
110
|
+
refresh,
|
|
111
|
+
loadMore,
|
|
112
|
+
}), [error, isLoading, isLoadingMore, loadMore, members, nextPageToken, refresh, total]);
|
|
113
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { LemmaClient } from "../client.js";
|
|
2
|
+
import type { PodJoinRequest, PodMember, User } from "../types.js";
|
|
3
|
+
export type PodAccessStatus = "idle" | "checking" | "member" | "missing" | "pending" | "error";
|
|
4
|
+
export interface UsePodAccessOptions {
|
|
5
|
+
client: LemmaClient;
|
|
6
|
+
podId?: string;
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
autoLoad?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface UsePodAccessResult {
|
|
11
|
+
status: PodAccessStatus;
|
|
12
|
+
hasAccess: boolean;
|
|
13
|
+
user: User | null;
|
|
14
|
+
member: PodMember | null;
|
|
15
|
+
joinRequest: PodJoinRequest | null;
|
|
16
|
+
isLoading: boolean;
|
|
17
|
+
isRequestingAccess: boolean;
|
|
18
|
+
error: Error | null;
|
|
19
|
+
refresh: () => Promise<PodAccessStatus>;
|
|
20
|
+
requestAccess: () => Promise<PodJoinRequest>;
|
|
21
|
+
}
|
|
22
|
+
export declare function usePodAccess({ client, podId, enabled, autoLoad, }: UsePodAccessOptions): UsePodAccessResult;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { ApiError } from "../http.js";
|
|
3
|
+
import { normalizeError, resolvePodId } from "./utils.js";
|
|
4
|
+
function isMissingAccessError(error) {
|
|
5
|
+
return error instanceof ApiError && (error.statusCode === 403 || error.statusCode === 404);
|
|
6
|
+
}
|
|
7
|
+
export function usePodAccess({ client, podId, enabled = true, autoLoad = true, }) {
|
|
8
|
+
const [status, setStatus] = useState("idle");
|
|
9
|
+
const [user, setUser] = useState(null);
|
|
10
|
+
const [member, setMember] = useState(null);
|
|
11
|
+
const [joinRequest, setJoinRequest] = useState(null);
|
|
12
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
13
|
+
const [isRequestingAccess, setIsRequestingAccess] = useState(false);
|
|
14
|
+
const [error, setError] = useState(null);
|
|
15
|
+
const refresh = useCallback(async () => {
|
|
16
|
+
if (!enabled) {
|
|
17
|
+
setStatus("idle");
|
|
18
|
+
setUser(null);
|
|
19
|
+
setMember(null);
|
|
20
|
+
setJoinRequest(null);
|
|
21
|
+
setError(null);
|
|
22
|
+
setIsLoading(false);
|
|
23
|
+
return "idle";
|
|
24
|
+
}
|
|
25
|
+
setStatus("checking");
|
|
26
|
+
setIsLoading(true);
|
|
27
|
+
setError(null);
|
|
28
|
+
try {
|
|
29
|
+
const resolvedPodId = resolvePodId(client, podId);
|
|
30
|
+
const currentUser = await client.users.current();
|
|
31
|
+
setUser(currentUser);
|
|
32
|
+
try {
|
|
33
|
+
const nextMember = await client.podMembers.get(resolvedPodId, currentUser.id);
|
|
34
|
+
setMember(nextMember);
|
|
35
|
+
setJoinRequest(null);
|
|
36
|
+
setStatus("member");
|
|
37
|
+
return "member";
|
|
38
|
+
}
|
|
39
|
+
catch (membershipError) {
|
|
40
|
+
if (!isMissingAccessError(membershipError)) {
|
|
41
|
+
throw membershipError;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
setMember(null);
|
|
45
|
+
try {
|
|
46
|
+
const request = await client.podJoinRequests.me(resolvedPodId);
|
|
47
|
+
setJoinRequest(request);
|
|
48
|
+
const nextStatus = request?.status === "PENDING" ? "pending" : "missing";
|
|
49
|
+
setStatus(nextStatus);
|
|
50
|
+
return nextStatus;
|
|
51
|
+
}
|
|
52
|
+
catch (joinRequestError) {
|
|
53
|
+
if (!isMissingAccessError(joinRequestError)) {
|
|
54
|
+
throw joinRequestError;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
setJoinRequest(null);
|
|
58
|
+
setStatus("missing");
|
|
59
|
+
return "missing";
|
|
60
|
+
}
|
|
61
|
+
catch (refreshError) {
|
|
62
|
+
const normalized = normalizeError(refreshError, "Failed to check pod access.");
|
|
63
|
+
setError(normalized);
|
|
64
|
+
setStatus("error");
|
|
65
|
+
return "error";
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
setIsLoading(false);
|
|
69
|
+
}
|
|
70
|
+
}, [client, enabled, podId]);
|
|
71
|
+
const requestAccess = useCallback(async () => {
|
|
72
|
+
setIsRequestingAccess(true);
|
|
73
|
+
setError(null);
|
|
74
|
+
try {
|
|
75
|
+
const resolvedPodId = resolvePodId(client, podId);
|
|
76
|
+
const request = await client.podJoinRequests.create(resolvedPodId);
|
|
77
|
+
setJoinRequest(request);
|
|
78
|
+
setMember(null);
|
|
79
|
+
setStatus(request.status === "PENDING" ? "pending" : "missing");
|
|
80
|
+
return request;
|
|
81
|
+
}
|
|
82
|
+
catch (requestError) {
|
|
83
|
+
const normalized = normalizeError(requestError, "Failed to request pod access.");
|
|
84
|
+
setError(normalized);
|
|
85
|
+
setStatus("error");
|
|
86
|
+
throw normalized;
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
setIsRequestingAccess(false);
|
|
90
|
+
}
|
|
91
|
+
}, [client, podId]);
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (!enabled) {
|
|
94
|
+
setStatus("idle");
|
|
95
|
+
setUser(null);
|
|
96
|
+
setMember(null);
|
|
97
|
+
setJoinRequest(null);
|
|
98
|
+
setError(null);
|
|
99
|
+
setIsLoading(false);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (!autoLoad)
|
|
103
|
+
return;
|
|
104
|
+
void refresh();
|
|
105
|
+
}, [autoLoad, enabled, refresh]);
|
|
106
|
+
return useMemo(() => ({
|
|
107
|
+
status,
|
|
108
|
+
hasAccess: status === "member",
|
|
109
|
+
user,
|
|
110
|
+
member,
|
|
111
|
+
joinRequest,
|
|
112
|
+
isLoading,
|
|
113
|
+
isRequestingAccess,
|
|
114
|
+
error,
|
|
115
|
+
refresh,
|
|
116
|
+
requestAccess,
|
|
117
|
+
}), [
|
|
118
|
+
error,
|
|
119
|
+
isLoading,
|
|
120
|
+
isRequestingAccess,
|
|
121
|
+
joinRequest,
|
|
122
|
+
member,
|
|
123
|
+
refresh,
|
|
124
|
+
requestAccess,
|
|
125
|
+
status,
|
|
126
|
+
user,
|
|
127
|
+
]);
|
|
128
|
+
}
|
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
import type { LemmaClient } from "../client.js";
|
|
2
|
+
/**
|
|
3
|
+
* React hook for fetching a single record by ID. Unwraps `.data`
|
|
4
|
+
* automatically so `record` is the plain object, not the API envelope.
|
|
5
|
+
*
|
|
6
|
+
* Perfect for detail panels — pair with `useRecords` for the list.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* const { record, isLoading } = useRecord({
|
|
11
|
+
* client,
|
|
12
|
+
* tableName: "issues",
|
|
13
|
+
* recordId: selectedId,
|
|
14
|
+
* });
|
|
15
|
+
* // record is the issue object directly (or null)
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
2
18
|
export interface UseRecordOptions {
|
|
3
19
|
client: LemmaClient;
|
|
4
20
|
podId?: string;
|
package/dist/react/useRecord.js
CHANGED
|
@@ -1,14 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
-
|
|
3
|
-
if (!podId || podId === client.podId)
|
|
4
|
-
return client;
|
|
5
|
-
return client.withPod(podId);
|
|
6
|
-
}
|
|
7
|
-
function normalizeError(error, fallback) {
|
|
8
|
-
if (error instanceof Error)
|
|
9
|
-
return error;
|
|
10
|
-
return new Error(fallback);
|
|
11
|
-
}
|
|
2
|
+
import { normalizeError, resolvePodClient } from "./utils.js";
|
|
12
3
|
export function useRecord({ client, podId, tableName, recordId = null, enabled = true, autoLoad = true, }) {
|
|
13
4
|
const [record, setRecord] = useState(null);
|
|
14
5
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -16,7 +7,7 @@ export function useRecord({ client, podId, tableName, recordId = null, enabled =
|
|
|
16
7
|
const trimmedTableName = tableName.trim();
|
|
17
8
|
const trimmedRecordId = typeof recordId === "string" ? recordId.trim() : "";
|
|
18
9
|
const isEnabled = enabled && trimmedTableName.length > 0 && trimmedRecordId.length > 0;
|
|
19
|
-
const refresh = useCallback(async (overrides = {}) => {
|
|
10
|
+
const refresh = useCallback(async (overrides = {}, signal) => {
|
|
20
11
|
const nextRecordId = typeof overrides.recordId === "string"
|
|
21
12
|
? overrides.recordId.trim()
|
|
22
13
|
: trimmedRecordId;
|
|
@@ -31,17 +22,22 @@ export function useRecord({ client, podId, tableName, recordId = null, enabled =
|
|
|
31
22
|
try {
|
|
32
23
|
const scopedClient = resolvePodClient(client, podId);
|
|
33
24
|
const response = await scopedClient.records.get(trimmedTableName, nextRecordId);
|
|
25
|
+
if (signal?.aborted)
|
|
26
|
+
return null;
|
|
34
27
|
const nextRecord = (response.data ?? null);
|
|
35
28
|
setRecord(nextRecord);
|
|
36
29
|
return nextRecord;
|
|
37
30
|
}
|
|
38
31
|
catch (refreshError) {
|
|
32
|
+
if (signal?.aborted)
|
|
33
|
+
return null;
|
|
39
34
|
const normalized = normalizeError(refreshError, "Failed to load record.");
|
|
40
35
|
setError(normalized);
|
|
41
36
|
return null;
|
|
42
37
|
}
|
|
43
38
|
finally {
|
|
44
|
-
|
|
39
|
+
if (!signal?.aborted)
|
|
40
|
+
setIsLoading(false);
|
|
45
41
|
}
|
|
46
42
|
}, [client, enabled, podId, trimmedRecordId, trimmedTableName]);
|
|
47
43
|
useEffect(() => {
|
|
@@ -53,7 +49,22 @@ export function useRecord({ client, podId, tableName, recordId = null, enabled =
|
|
|
53
49
|
}
|
|
54
50
|
if (!autoLoad)
|
|
55
51
|
return;
|
|
56
|
-
|
|
52
|
+
const controller = new AbortController();
|
|
53
|
+
let cancelled = false;
|
|
54
|
+
(async () => {
|
|
55
|
+
try {
|
|
56
|
+
await refresh({}, controller.signal);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
if (!cancelled) {
|
|
60
|
+
setError(normalizeError(new Error("Failed to load record."), "Failed to load record."));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
})();
|
|
64
|
+
return () => {
|
|
65
|
+
cancelled = true;
|
|
66
|
+
controller.abort();
|
|
67
|
+
};
|
|
57
68
|
}, [autoLoad, isEnabled, refresh]);
|
|
58
69
|
return useMemo(() => ({
|
|
59
70
|
record,
|
|
@@ -1,7 +1,36 @@
|
|
|
1
1
|
import type { LemmaClient } from "../client.js";
|
|
2
2
|
import { type RecordSchemaField } from "../record-form.js";
|
|
3
|
-
import type { RecordResponse } from "../types.js";
|
|
3
|
+
import type { FunctionRun, RecordResponse } from "../types.js";
|
|
4
4
|
import { type UseRecordSchemaResult } from "./useRecordSchema.js";
|
|
5
|
+
/**
|
|
6
|
+
* React hook for schema-driven record forms with validation, dirty tracking,
|
|
7
|
+
* and create/update auto-detection.
|
|
8
|
+
*
|
|
9
|
+
* Supports two submit modes:
|
|
10
|
+
* - `"direct"` (default): persists via `records.create` / `records.update`.
|
|
11
|
+
* - `"function"`: persists via `functions.runs.create`, routing the form
|
|
12
|
+
* payload through a pod function that may enforce business logic
|
|
13
|
+
* (e.g. auto-generating identifiers, logging history).
|
|
14
|
+
*
|
|
15
|
+
* @example Direct create/update form
|
|
16
|
+
* ```tsx
|
|
17
|
+
* const form = useRecordForm({ client, tableName: "issues" });
|
|
18
|
+
* // form.fields → all schema fields, form.editableFields → user-fillable fields
|
|
19
|
+
* await form.submit(); // calls records.create or records.update
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @example Function-backed form
|
|
23
|
+
* ```tsx
|
|
24
|
+
* const form = useRecordForm({
|
|
25
|
+
* client,
|
|
26
|
+
* tableName: "issues",
|
|
27
|
+
* submitVia: "function",
|
|
28
|
+
* submitFunctionName: "create-issue",
|
|
29
|
+
* hiddenFields: ["identifier", "created_at"],
|
|
30
|
+
* });
|
|
31
|
+
* await form.submit(); // calls functions.runs.create("create-issue", { input: payload })
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
5
34
|
export interface UseRecordFormOptions {
|
|
6
35
|
client: LemmaClient;
|
|
7
36
|
podId?: string;
|
|
@@ -11,7 +40,17 @@ export interface UseRecordFormOptions {
|
|
|
11
40
|
mode?: "auto" | "create" | "update";
|
|
12
41
|
enabled?: boolean;
|
|
13
42
|
autoLoad?: boolean;
|
|
14
|
-
|
|
43
|
+
/** Field names to include in `fields` and `editableFields`. Takes precedence over `hiddenFields`. */
|
|
44
|
+
visibleFields?: string[];
|
|
45
|
+
/** Field names to exclude from `fields` and `editableFields`. Ignored for any field also listed in `visibleFields`. */
|
|
46
|
+
hiddenFields?: string[];
|
|
47
|
+
/** How the form persists data. `"direct"` calls `records.create`/`records.update`. `"function"` calls `functions.runs.create`. */
|
|
48
|
+
submitVia?: "direct" | "function";
|
|
49
|
+
/** Function name to run when `submitVia` is `"function"`. Required when `submitVia` is `"function"`. */
|
|
50
|
+
submitFunctionName?: string;
|
|
51
|
+
/** Transforms the form payload before passing it as function input. Receives the validated payload, returns the function input object. */
|
|
52
|
+
submitFunctionInput?: (payload: Record<string, unknown>) => Record<string, unknown>;
|
|
53
|
+
onSubmitSuccess?: (record: Record<string, unknown>, response: RecordResponse | FunctionRun) => void;
|
|
15
54
|
onError?: (error: unknown) => void;
|
|
16
55
|
}
|
|
17
56
|
export interface UseRecordFormResult {
|
|
@@ -39,4 +78,4 @@ export interface UseRecordFormResult {
|
|
|
39
78
|
mode?: "create" | "update";
|
|
40
79
|
}) => Promise<Record<string, unknown> | null>;
|
|
41
80
|
}
|
|
42
|
-
export declare function useRecordForm({ client, podId, tableName, recordId, initialValues, mode, enabled, autoLoad, onSubmitSuccess, onError, }: UseRecordFormOptions): UseRecordFormResult;
|
|
81
|
+
export declare function useRecordForm({ client, podId, tableName, recordId, initialValues, mode, enabled, autoLoad, visibleFields, hiddenFields, submitVia, submitFunctionName, submitFunctionInput, onSubmitSuccess, onError, }: UseRecordFormOptions): UseRecordFormResult;
|
|
@@ -1,26 +1,9 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
2
|
import { buildRecordFormValues, buildRecordPayload, } from "../record-form.js";
|
|
3
|
+
import { normalizeError, resolvePodClient, stringifyComparable } from "./utils.js";
|
|
3
4
|
import { useRecordSchema, } from "./useRecordSchema.js";
|
|
4
5
|
const EMPTY_VALUES = {};
|
|
5
|
-
function
|
|
6
|
-
if (error instanceof Error)
|
|
7
|
-
return error;
|
|
8
|
-
return new Error(fallback);
|
|
9
|
-
}
|
|
10
|
-
function resolvePodClient(client, podId) {
|
|
11
|
-
if (!podId || podId === client.podId)
|
|
12
|
-
return client;
|
|
13
|
-
return client.withPod(podId);
|
|
14
|
-
}
|
|
15
|
-
function stringifyComparable(value) {
|
|
16
|
-
try {
|
|
17
|
-
return JSON.stringify(value);
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
return String(value);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
export function useRecordForm({ client, podId, tableName, recordId = null, initialValues = EMPTY_VALUES, mode = "auto", enabled = true, autoLoad = true, onSubmitSuccess, onError, }) {
|
|
6
|
+
export function useRecordForm({ client, podId, tableName, recordId = null, initialValues = EMPTY_VALUES, mode = "auto", enabled = true, autoLoad = true, visibleFields, hiddenFields, submitVia = "direct", submitFunctionName, submitFunctionInput, onSubmitSuccess, onError, }) {
|
|
24
7
|
const schema = useRecordSchema({
|
|
25
8
|
client,
|
|
26
9
|
podId,
|
|
@@ -28,6 +11,30 @@ export function useRecordForm({ client, podId, tableName, recordId = null, initi
|
|
|
28
11
|
enabled,
|
|
29
12
|
autoLoad,
|
|
30
13
|
});
|
|
14
|
+
const visibleSet = useMemo(() => visibleFields ? new Set(visibleFields) : null, [visibleFields]);
|
|
15
|
+
const hiddenSet = useMemo(() => hiddenFields ? new Set(hiddenFields) : null, [hiddenFields]);
|
|
16
|
+
const filteredFields = useMemo(() => {
|
|
17
|
+
if (!visibleSet && !hiddenSet)
|
|
18
|
+
return schema.fields;
|
|
19
|
+
return schema.fields.filter((field) => {
|
|
20
|
+
if (visibleSet)
|
|
21
|
+
return visibleSet.has(field.name);
|
|
22
|
+
if (hiddenSet)
|
|
23
|
+
return !hiddenSet.has(field.name);
|
|
24
|
+
return true;
|
|
25
|
+
});
|
|
26
|
+
}, [hiddenSet, schema.fields, visibleSet]);
|
|
27
|
+
const filteredEditableFields = useMemo(() => {
|
|
28
|
+
if (!visibleSet && !hiddenSet)
|
|
29
|
+
return schema.editableFields;
|
|
30
|
+
return schema.editableFields.filter((field) => {
|
|
31
|
+
if (visibleSet)
|
|
32
|
+
return visibleSet.has(field.name);
|
|
33
|
+
if (hiddenSet)
|
|
34
|
+
return !hiddenSet.has(field.name);
|
|
35
|
+
return true;
|
|
36
|
+
});
|
|
37
|
+
}, [hiddenSet, schema.editableFields, visibleSet]);
|
|
31
38
|
const [record, setRecord] = useState(null);
|
|
32
39
|
const [values, setValuesState] = useState({});
|
|
33
40
|
const [baselineValues, setBaselineValues] = useState({});
|
|
@@ -164,6 +171,19 @@ export function useRecordForm({ client, podId, tableName, recordId = null, initi
|
|
|
164
171
|
setRecordError(null);
|
|
165
172
|
try {
|
|
166
173
|
const scopedClient = resolvePodClient(client, podId);
|
|
174
|
+
if (submitVia === "function") {
|
|
175
|
+
const functionName = submitFunctionName ?? tableName;
|
|
176
|
+
const functionInput = submitFunctionInput ? submitFunctionInput(payload.data) : payload.data;
|
|
177
|
+
const run = await scopedClient.functions.runs.create(functionName, { input: functionInput });
|
|
178
|
+
const nextRecord = run.output_data ?? { id: run.id, ...functionInput };
|
|
179
|
+
setRecord(nextRecord);
|
|
180
|
+
hydrateValues({
|
|
181
|
+
...(nextRecord ?? {}),
|
|
182
|
+
...stableInitialValues,
|
|
183
|
+
});
|
|
184
|
+
onSubmitSuccess?.(nextRecord ?? {}, run);
|
|
185
|
+
return nextRecord;
|
|
186
|
+
}
|
|
167
187
|
const response = resolvedMode === "update" && recordId
|
|
168
188
|
? await scopedClient.records.update(tableName, recordId, payload.data)
|
|
169
189
|
: await scopedClient.records.create(tableName, payload.data);
|
|
@@ -185,14 +205,14 @@ export function useRecordForm({ client, podId, tableName, recordId = null, initi
|
|
|
185
205
|
finally {
|
|
186
206
|
setIsSubmitting(false);
|
|
187
207
|
}
|
|
188
|
-
}, [client, hydrateValues, mode, onError, onSubmitSuccess, podId, recordId, schemaTable, stableInitialValues, tableName, values]);
|
|
208
|
+
}, [client, hydrateValues, mode, onError, onSubmitSuccess, podId, recordId, schemaTable, stableInitialValues, submitFunctionInput, submitFunctionName, submitVia, tableName, values]);
|
|
189
209
|
const isDirty = useMemo(() => {
|
|
190
210
|
return stringifyComparable(values) !== stringifyComparable(baselineValues);
|
|
191
211
|
}, [baselineValues, values]);
|
|
192
212
|
return useMemo(() => ({
|
|
193
213
|
table: schema.table,
|
|
194
|
-
fields:
|
|
195
|
-
editableFields:
|
|
214
|
+
fields: filteredFields,
|
|
215
|
+
editableFields: filteredEditableFields,
|
|
196
216
|
defaults: schema.defaults,
|
|
197
217
|
values,
|
|
198
218
|
baselineValues,
|
|
@@ -214,6 +234,8 @@ export function useRecordForm({ client, podId, tableName, recordId = null, initi
|
|
|
214
234
|
}), [
|
|
215
235
|
baselineValues,
|
|
216
236
|
fieldErrors,
|
|
237
|
+
filteredEditableFields,
|
|
238
|
+
filteredFields,
|
|
217
239
|
isDirty,
|
|
218
240
|
isLoadingRecord,
|
|
219
241
|
isSubmitting,
|
|
@@ -223,9 +245,7 @@ export function useRecordForm({ client, podId, tableName, recordId = null, initi
|
|
|
223
245
|
refreshRecord,
|
|
224
246
|
reset,
|
|
225
247
|
schema.defaults,
|
|
226
|
-
schema.editableFields,
|
|
227
248
|
schema.error,
|
|
228
|
-
schema.fields,
|
|
229
249
|
schema.isLoading,
|
|
230
250
|
schema.refresh,
|
|
231
251
|
schema.table,
|
|
@@ -12,7 +12,9 @@ export interface UseRecordsResult<TRecord extends Record<string, unknown> = Reco
|
|
|
12
12
|
total: number;
|
|
13
13
|
nextPageToken: string | null;
|
|
14
14
|
isLoading: boolean;
|
|
15
|
+
isLoadingMore: boolean;
|
|
15
16
|
error: Error | null;
|
|
16
17
|
refresh: (overrides?: Partial<ListRecordsOptions>) => Promise<TRecord[]>;
|
|
18
|
+
loadMore: (overrides?: Partial<ListRecordsOptions>) => Promise<TRecord[]>;
|
|
17
19
|
}
|
|
18
20
|
export declare function useRecords<TRecord extends Record<string, unknown> = Record<string, unknown>>({ client, podId, tableName, filters, sort, limit, pageToken, offset, sortBy, order, params, enabled, autoLoad, }: UseRecordsOptions): UseRecordsResult<TRecord>;
|