lemma-sdk 0.2.31 → 0.2.33
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 +138 -52
- package/dist/browser/lemma-client.js +23 -8
- package/dist/namespaces/desks.d.ts +5 -2
- package/dist/namespaces/desks.js +5 -2
- package/dist/namespaces/files.d.ts +11 -0
- package/dist/namespaces/files.js +12 -0
- package/dist/openapi_client/models/DeskBundleUploadRequest.d.ts +1 -1
- package/dist/openapi_client/services/DesksService.d.ts +4 -4
- package/dist/openapi_client/services/DesksService.js +6 -6
- package/dist/openapi_client/services/PublicDesksService.d.ts +2 -2
- package/dist/openapi_client/services/PublicDesksService.js +3 -3
- package/dist/react/index.d.ts +13 -1
- package/dist/react/index.js +6 -0
- package/dist/react/useAssistantController.js +82 -37
- package/dist/react/useAssistantRuntime.js +8 -4
- package/dist/react/useAssistantSession.js +44 -2
- package/dist/react/useConversationMessages.js +19 -2
- package/dist/react/useCreateRecord.d.ts +33 -3
- package/dist/react/useCreateRecord.js +12 -2
- package/dist/react/useFile.d.ts +18 -0
- package/dist/react/useFile.js +58 -0
- package/dist/react/useFilePreview.d.ts +23 -0
- package/dist/react/useFilePreview.js +76 -0
- package/dist/react/useFileSearch.d.ts +26 -0
- package/dist/react/useFileSearch.js +64 -0
- package/dist/react/useFileTree.d.ts +21 -0
- package/dist/react/useFileTree.js +59 -0
- package/dist/react/useFiles.d.ts +29 -0
- package/dist/react/useFiles.js +90 -0
- package/dist/react/useForeignKeyOptions.d.ts +18 -0
- package/dist/react/useFunctionRun.d.ts +17 -0
- package/dist/react/useJoinedRecords.d.ts +57 -2
- package/dist/react/useJoinedRecords.js +54 -5
- package/dist/react/useRecord.d.ts +16 -0
- package/dist/react/useRecordForm.d.ts +42 -3
- package/dist/react/useRecordForm.js +43 -6
- package/dist/react/useRecords.js +8 -5
- 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/useReverseRelatedRecords.d.ts +21 -0
- package/dist/react/useUpdateRecord.d.ts +34 -3
- package/dist/react/useUpdateRecord.js +13 -2
- package/dist/types.d.ts +6 -1
- package/package.json +2 -1
|
@@ -3,7 +3,7 @@ import { buildRecordFormValues, buildRecordPayload, } from "../record-form.js";
|
|
|
3
3
|
import { normalizeError, resolvePodClient, stringifyComparable } from "./utils.js";
|
|
4
4
|
import { useRecordSchema, } from "./useRecordSchema.js";
|
|
5
5
|
const EMPTY_VALUES = {};
|
|
6
|
-
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, }) {
|
|
7
7
|
const schema = useRecordSchema({
|
|
8
8
|
client,
|
|
9
9
|
podId,
|
|
@@ -11,6 +11,30 @@ export function useRecordForm({ client, podId, tableName, recordId = null, initi
|
|
|
11
11
|
enabled,
|
|
12
12
|
autoLoad,
|
|
13
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]);
|
|
14
38
|
const [record, setRecord] = useState(null);
|
|
15
39
|
const [values, setValuesState] = useState({});
|
|
16
40
|
const [baselineValues, setBaselineValues] = useState({});
|
|
@@ -147,6 +171,19 @@ export function useRecordForm({ client, podId, tableName, recordId = null, initi
|
|
|
147
171
|
setRecordError(null);
|
|
148
172
|
try {
|
|
149
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
|
+
}
|
|
150
187
|
const response = resolvedMode === "update" && recordId
|
|
151
188
|
? await scopedClient.records.update(tableName, recordId, payload.data)
|
|
152
189
|
: await scopedClient.records.create(tableName, payload.data);
|
|
@@ -168,14 +205,14 @@ export function useRecordForm({ client, podId, tableName, recordId = null, initi
|
|
|
168
205
|
finally {
|
|
169
206
|
setIsSubmitting(false);
|
|
170
207
|
}
|
|
171
|
-
}, [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]);
|
|
172
209
|
const isDirty = useMemo(() => {
|
|
173
210
|
return stringifyComparable(values) !== stringifyComparable(baselineValues);
|
|
174
211
|
}, [baselineValues, values]);
|
|
175
212
|
return useMemo(() => ({
|
|
176
213
|
table: schema.table,
|
|
177
|
-
fields:
|
|
178
|
-
editableFields:
|
|
214
|
+
fields: filteredFields,
|
|
215
|
+
editableFields: filteredEditableFields,
|
|
179
216
|
defaults: schema.defaults,
|
|
180
217
|
values,
|
|
181
218
|
baselineValues,
|
|
@@ -197,6 +234,8 @@ export function useRecordForm({ client, podId, tableName, recordId = null, initi
|
|
|
197
234
|
}), [
|
|
198
235
|
baselineValues,
|
|
199
236
|
fieldErrors,
|
|
237
|
+
filteredEditableFields,
|
|
238
|
+
filteredFields,
|
|
200
239
|
isDirty,
|
|
201
240
|
isLoadingRecord,
|
|
202
241
|
isSubmitting,
|
|
@@ -206,9 +245,7 @@ export function useRecordForm({ client, podId, tableName, recordId = null, initi
|
|
|
206
245
|
refreshRecord,
|
|
207
246
|
reset,
|
|
208
247
|
schema.defaults,
|
|
209
|
-
schema.editableFields,
|
|
210
248
|
schema.error,
|
|
211
|
-
schema.fields,
|
|
212
249
|
schema.isLoading,
|
|
213
250
|
schema.refresh,
|
|
214
251
|
schema.table,
|
package/dist/react/useRecords.js
CHANGED
|
@@ -72,7 +72,10 @@ export function useRecords({ client, podId, tableName, filters, sort, limit = 20
|
|
|
72
72
|
trimmedTableName,
|
|
73
73
|
]);
|
|
74
74
|
const loadMore = useCallback(async (overrides = {}) => {
|
|
75
|
-
|
|
75
|
+
const loadedCount = records.length;
|
|
76
|
+
const canLoadWithCursor = Boolean(nextPageToken);
|
|
77
|
+
const canLoadWithOffset = loadedCount < total;
|
|
78
|
+
if (!isEnabled || isLoading || isLoadingMore || (!canLoadWithCursor && !canLoadWithOffset)) {
|
|
76
79
|
return [];
|
|
77
80
|
}
|
|
78
81
|
setIsLoadingMore(true);
|
|
@@ -83,15 +86,15 @@ export function useRecords({ client, podId, tableName, filters, sort, limit = 20
|
|
|
83
86
|
filters: stableFilters,
|
|
84
87
|
sort: stableSort,
|
|
85
88
|
limit: overrides.limit ?? limit,
|
|
86
|
-
pageToken: nextPageToken,
|
|
87
|
-
offset: overrides.offset,
|
|
89
|
+
pageToken: overrides.pageToken ?? nextPageToken ?? undefined,
|
|
90
|
+
offset: overrides.offset ?? (nextPageToken ? undefined : (offset ?? 0) + loadedCount),
|
|
88
91
|
sortBy: overrides.sortBy ?? sortBy,
|
|
89
92
|
order: overrides.order ?? order,
|
|
90
93
|
params: overrides.params ?? stableParams,
|
|
91
94
|
});
|
|
92
95
|
const moreRecords = (response.items ?? []);
|
|
93
96
|
setRecords((previous) => [...previous, ...moreRecords]);
|
|
94
|
-
setTotal(response.total ??
|
|
97
|
+
setTotal(response.total ?? loadedCount + moreRecords.length);
|
|
95
98
|
setNextPageToken(response.next_page_token ?? null);
|
|
96
99
|
return moreRecords;
|
|
97
100
|
}
|
|
@@ -103,7 +106,7 @@ export function useRecords({ client, podId, tableName, filters, sort, limit = 20
|
|
|
103
106
|
finally {
|
|
104
107
|
setIsLoadingMore(false);
|
|
105
108
|
}
|
|
106
|
-
}, [client, isEnabled, isLoading, isLoadingMore, limit, nextPageToken, offset, order, podId, records.length, sortBy, stableFilters, stableParams, stableSort, trimmedTableName]);
|
|
109
|
+
}, [client, isEnabled, isLoading, isLoadingMore, limit, nextPageToken, offset, order, podId, records.length, sortBy, stableFilters, stableParams, stableSort, total, trimmedTableName]);
|
|
107
110
|
useEffect(() => {
|
|
108
111
|
if (!isEnabled) {
|
|
109
112
|
setRecords([]);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { LemmaClient } from "../client.js";
|
|
2
|
+
import type { Table } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* React hook for fetching records from a referencing table that point back
|
|
5
|
+
* to a specific record via a foreign key.
|
|
6
|
+
*
|
|
7
|
+
* Unlike `useReverseRelatedRecords` which starts from a parent table and
|
|
8
|
+
* discovers what references it, this hook starts from the child table and
|
|
9
|
+
* says "give me all rows in this table where this FK column equals this ID."
|
|
10
|
+
*
|
|
11
|
+
* @example Fetch all comments for an issue
|
|
12
|
+
* ```tsx
|
|
13
|
+
* const { records, isLoading } = useReferencingRecords({
|
|
14
|
+
* client,
|
|
15
|
+
* table: "comments",
|
|
16
|
+
* foreignKey: "issue_id",
|
|
17
|
+
* recordId: "issue_123",
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @example Fetch history entries
|
|
22
|
+
* ```tsx
|
|
23
|
+
* const { records, columns, isLoading } = useReferencingRecords({
|
|
24
|
+
* client,
|
|
25
|
+
* table: "issue_history",
|
|
26
|
+
* foreignKey: "issue_id",
|
|
27
|
+
* recordId: selectedIssueId,
|
|
28
|
+
* sortBy: "created_at",
|
|
29
|
+
* order: "desc",
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export interface ReferencingRecordsColumn {
|
|
34
|
+
key: string;
|
|
35
|
+
field: string;
|
|
36
|
+
label: string;
|
|
37
|
+
}
|
|
38
|
+
export interface UseReferencingRecordsOptions {
|
|
39
|
+
client: LemmaClient;
|
|
40
|
+
podId?: string;
|
|
41
|
+
/** The referencing (child) table, e.g. "comments". */
|
|
42
|
+
table: string;
|
|
43
|
+
/** The foreign-key column in the referencing table, e.g. "issue_id". */
|
|
44
|
+
foreignKey: string;
|
|
45
|
+
/** The record ID value to match against the foreign-key column. */
|
|
46
|
+
recordId?: string | null;
|
|
47
|
+
/** Fields to select. Auto-resolved from the table schema if omitted. */
|
|
48
|
+
fields?: string[];
|
|
49
|
+
limit?: number;
|
|
50
|
+
offset?: number;
|
|
51
|
+
sortBy?: string;
|
|
52
|
+
order?: "asc" | "desc" | string;
|
|
53
|
+
enabled?: boolean;
|
|
54
|
+
autoLoad?: boolean;
|
|
55
|
+
}
|
|
56
|
+
export interface UseReferencingRecordsResult<TRow extends Record<string, unknown> = Record<string, unknown>> {
|
|
57
|
+
referencedTable: Table | null;
|
|
58
|
+
columns: ReferencingRecordsColumn[];
|
|
59
|
+
records: TRow[];
|
|
60
|
+
total: number;
|
|
61
|
+
nextPageToken: string | null;
|
|
62
|
+
isLoading: boolean;
|
|
63
|
+
error: Error | null;
|
|
64
|
+
refresh: () => Promise<TRow[]>;
|
|
65
|
+
}
|
|
66
|
+
export declare function useReferencingRecords<TRow extends Record<string, unknown> = Record<string, unknown>>({ client, podId, table, foreignKey, recordId, fields, limit, offset, sortBy, order, enabled, autoLoad, }: UseReferencingRecordsOptions): UseReferencingRecordsResult<TRow>;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { normalizeError, resolvePodClient, stringifyComparable } from "./utils.js";
|
|
3
|
+
function sentenceCase(value) {
|
|
4
|
+
return value
|
|
5
|
+
.replace(/[_\.]/g, " ")
|
|
6
|
+
.replace(/\s+/g, " ")
|
|
7
|
+
.trim()
|
|
8
|
+
.replace(/\b\w/g, (match) => match.toUpperCase());
|
|
9
|
+
}
|
|
10
|
+
function pickDefaultFields(table, foreignKey) {
|
|
11
|
+
const names = table.columns
|
|
12
|
+
.map((column) => column.name)
|
|
13
|
+
.filter((name) => name !== "created_at" && name !== "updated_at");
|
|
14
|
+
const prioritized = ["id", "name", "title", "label", "status", foreignKey];
|
|
15
|
+
const next = [];
|
|
16
|
+
prioritized.forEach((name) => {
|
|
17
|
+
if (names.includes(name) && !next.includes(name)) {
|
|
18
|
+
next.push(name);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
names.forEach((name) => {
|
|
22
|
+
if (!next.includes(name)) {
|
|
23
|
+
next.push(name);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
return next.slice(0, 6);
|
|
27
|
+
}
|
|
28
|
+
export function useReferencingRecords({ client, podId, table, foreignKey, recordId = null, fields, limit = 20, offset, sortBy, order, enabled = true, autoLoad = true, }) {
|
|
29
|
+
const [referencedTable, setReferencedTable] = useState(null);
|
|
30
|
+
const [columns, setColumns] = useState([]);
|
|
31
|
+
const [records, setRecords] = useState([]);
|
|
32
|
+
const [total, setTotal] = useState(0);
|
|
33
|
+
const [nextPageToken, setNextPageToken] = useState(null);
|
|
34
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
35
|
+
const [error, setError] = useState(null);
|
|
36
|
+
const trimmedTable = table.trim();
|
|
37
|
+
const trimmedRecordId = typeof recordId === "string" ? recordId.trim() : "";
|
|
38
|
+
const fieldsKey = stringifyComparable(fields);
|
|
39
|
+
const stableFields = useMemo(() => fields, [fieldsKey]);
|
|
40
|
+
const isEnabled = enabled && trimmedTable.length > 0 && trimmedRecordId.length > 0;
|
|
41
|
+
const refresh = useCallback(async (signal) => {
|
|
42
|
+
if (!isEnabled) {
|
|
43
|
+
setReferencedTable(null);
|
|
44
|
+
setColumns([]);
|
|
45
|
+
setRecords([]);
|
|
46
|
+
setTotal(0);
|
|
47
|
+
setNextPageToken(null);
|
|
48
|
+
setError(null);
|
|
49
|
+
setIsLoading(false);
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
setIsLoading(true);
|
|
53
|
+
setError(null);
|
|
54
|
+
try {
|
|
55
|
+
const scopedClient = resolvePodClient(client, podId);
|
|
56
|
+
const tableResponse = await scopedClient.tables.get(trimmedTable);
|
|
57
|
+
if (signal?.aborted)
|
|
58
|
+
return [];
|
|
59
|
+
setReferencedTable(tableResponse);
|
|
60
|
+
const resolvedFields = (stableFields?.length ? stableFields : pickDefaultFields(tableResponse, foreignKey))
|
|
61
|
+
.filter((field, index, allFields) => field.trim().length > 0 && allFields.indexOf(field) === index);
|
|
62
|
+
const response = await scopedClient.records.list(trimmedTable, {
|
|
63
|
+
filters: [{
|
|
64
|
+
field: foreignKey,
|
|
65
|
+
op: "eq",
|
|
66
|
+
value: trimmedRecordId,
|
|
67
|
+
}],
|
|
68
|
+
limit,
|
|
69
|
+
offset,
|
|
70
|
+
sortBy,
|
|
71
|
+
order,
|
|
72
|
+
});
|
|
73
|
+
if (signal?.aborted)
|
|
74
|
+
return [];
|
|
75
|
+
const nextRecords = (response.items ?? []);
|
|
76
|
+
setColumns(resolvedFields.map((field) => ({
|
|
77
|
+
key: field,
|
|
78
|
+
field,
|
|
79
|
+
label: sentenceCase(field),
|
|
80
|
+
})));
|
|
81
|
+
setRecords(nextRecords);
|
|
82
|
+
setTotal(response.total ?? nextRecords.length);
|
|
83
|
+
setNextPageToken(response.next_page_token ?? null);
|
|
84
|
+
return nextRecords;
|
|
85
|
+
}
|
|
86
|
+
catch (refreshError) {
|
|
87
|
+
if (signal?.aborted)
|
|
88
|
+
return [];
|
|
89
|
+
const normalized = normalizeError(refreshError, "Failed to load referencing records.");
|
|
90
|
+
setError(normalized);
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
if (!signal?.aborted)
|
|
95
|
+
setIsLoading(false);
|
|
96
|
+
}
|
|
97
|
+
}, [
|
|
98
|
+
client,
|
|
99
|
+
foreignKey,
|
|
100
|
+
isEnabled,
|
|
101
|
+
limit,
|
|
102
|
+
offset,
|
|
103
|
+
order,
|
|
104
|
+
podId,
|
|
105
|
+
sortBy,
|
|
106
|
+
stableFields,
|
|
107
|
+
trimmedRecordId,
|
|
108
|
+
trimmedTable,
|
|
109
|
+
]);
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (!isEnabled) {
|
|
112
|
+
setReferencedTable(null);
|
|
113
|
+
setColumns([]);
|
|
114
|
+
setRecords([]);
|
|
115
|
+
setTotal(0);
|
|
116
|
+
setNextPageToken(null);
|
|
117
|
+
setError(null);
|
|
118
|
+
setIsLoading(false);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (!autoLoad)
|
|
122
|
+
return;
|
|
123
|
+
const controller = new AbortController();
|
|
124
|
+
let cancelled = false;
|
|
125
|
+
(async () => {
|
|
126
|
+
try {
|
|
127
|
+
await refresh(controller.signal);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
if (!cancelled) {
|
|
131
|
+
setError(normalizeError(new Error("Failed to load referencing records."), "Failed to load referencing records."));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
})();
|
|
135
|
+
return () => {
|
|
136
|
+
cancelled = true;
|
|
137
|
+
controller.abort();
|
|
138
|
+
};
|
|
139
|
+
}, [autoLoad, isEnabled, refresh]);
|
|
140
|
+
return useMemo(() => ({
|
|
141
|
+
referencedTable,
|
|
142
|
+
columns,
|
|
143
|
+
records,
|
|
144
|
+
total,
|
|
145
|
+
nextPageToken,
|
|
146
|
+
isLoading,
|
|
147
|
+
error,
|
|
148
|
+
refresh,
|
|
149
|
+
}), [
|
|
150
|
+
columns,
|
|
151
|
+
error,
|
|
152
|
+
isLoading,
|
|
153
|
+
nextPageToken,
|
|
154
|
+
records,
|
|
155
|
+
refresh,
|
|
156
|
+
referencedTable,
|
|
157
|
+
total,
|
|
158
|
+
]);
|
|
159
|
+
}
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import type { LemmaClient } from "../client.js";
|
|
2
2
|
import type { Table } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* React hook for fetching base-table rows with their FK-related data
|
|
5
|
+
* joined in a single query. You specify which FK columns to include and
|
|
6
|
+
* the hook auto-resolves the referenced table and join columns.
|
|
7
|
+
*
|
|
8
|
+
* The result has nested objects: `{ id, name, team: { id, name } }`.
|
|
9
|
+
*
|
|
10
|
+
* @example Issues with their team
|
|
11
|
+
* ```tsx
|
|
12
|
+
* const { records, isLoading } = useRelatedRecords({
|
|
13
|
+
* client,
|
|
14
|
+
* tableName: "issues",
|
|
15
|
+
* include: [{ foreignKey: "team_id" }],
|
|
16
|
+
* });
|
|
17
|
+
* // records[0] = { id: "1", title: "Bug", team: { id: "t1", name: "Eng" } }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
3
20
|
export interface RelatedRecordsInclude {
|
|
4
21
|
foreignKey: string;
|
|
5
22
|
as?: string;
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import type { LemmaClient } from "../client.js";
|
|
2
2
|
import type { Table } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* React hook for finding records in *other* tables that reference a given
|
|
5
|
+
* record. Starts from the parent table, discovers all tables with FK
|
|
6
|
+
* columns pointing back to it, and fetches the referencing rows.
|
|
7
|
+
*
|
|
8
|
+
* For the simpler case where you already know the referencing table and
|
|
9
|
+
* FK column, prefer `useReferencingRecords` — it has a more intuitive API.
|
|
10
|
+
*
|
|
11
|
+
* @example All comments and history entries for an issue
|
|
12
|
+
* ```tsx
|
|
13
|
+
* const { relations, records, isLoading } = useReverseRelatedRecords({
|
|
14
|
+
* client,
|
|
15
|
+
* tableName: "issues",
|
|
16
|
+
* recordId: "issue_123",
|
|
17
|
+
* relation: { tableName: "comments", foreignKey: "issue_id" },
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @see useReferencingRecords — flipped-perspective alias for the common
|
|
22
|
+
* "show me all rows in table X where FK = Y" pattern.
|
|
23
|
+
*/
|
|
3
24
|
export interface ReverseRelationSelector {
|
|
4
25
|
tableName: string;
|
|
5
26
|
foreignKey: string;
|
|
@@ -1,12 +1,43 @@
|
|
|
1
1
|
import type { LemmaClient } from "../client.js";
|
|
2
|
-
import type { RecordResponse } from "../types.js";
|
|
2
|
+
import type { FunctionRun, RecordResponse } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* React hook for updating a single record. Manages loading/error state and
|
|
5
|
+
* exposes an `update` function you can call from event handlers.
|
|
6
|
+
*
|
|
7
|
+
* Supports two modes:
|
|
8
|
+
* - `"direct"` (default): calls `records.update` directly.
|
|
9
|
+
* - `"function"`: calls `functions.runs.create`, routing the update through
|
|
10
|
+
* a pod function (e.g. for status transitions that log history).
|
|
11
|
+
*
|
|
12
|
+
* @example Direct update
|
|
13
|
+
* ```tsx
|
|
14
|
+
* const { update, isSubmitting } = useUpdateRecord({ client, tableName: "issues", recordId: "123" });
|
|
15
|
+
* await update({ status: "closed" });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @example Function-backed update
|
|
19
|
+
* ```tsx
|
|
20
|
+
* const { update, isSubmitting } = useUpdateRecord({
|
|
21
|
+
* client,
|
|
22
|
+
* tableName: "issues",
|
|
23
|
+
* recordId: "123",
|
|
24
|
+
* updateVia: "function",
|
|
25
|
+
* updateFunctionName: "update-issue-status",
|
|
26
|
+
* });
|
|
27
|
+
* await update({ status: "in_progress" });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
3
30
|
export interface UseUpdateRecordOptions {
|
|
4
31
|
client: LemmaClient;
|
|
5
32
|
podId?: string;
|
|
6
33
|
tableName: string;
|
|
7
34
|
recordId?: string | null;
|
|
8
35
|
enabled?: boolean;
|
|
9
|
-
|
|
36
|
+
/** How the record is updated. `"direct"` calls `records.update`. `"function"` calls `functions.runs.create`. */
|
|
37
|
+
updateVia?: "direct" | "function";
|
|
38
|
+
/** Function name to run when `updateVia` is `"function"`. Falls back to `tableName` if omitted. */
|
|
39
|
+
updateFunctionName?: string;
|
|
40
|
+
onSuccess?: (record: Record<string, unknown>, response: RecordResponse | FunctionRun) => void;
|
|
10
41
|
onError?: (error: unknown) => void;
|
|
11
42
|
}
|
|
12
43
|
export interface UseUpdateRecordResult<TRecord extends Record<string, unknown> = Record<string, unknown>> {
|
|
@@ -18,4 +49,4 @@ export interface UseUpdateRecordResult<TRecord extends Record<string, unknown> =
|
|
|
18
49
|
}) => Promise<TRecord | null>;
|
|
19
50
|
reset: () => void;
|
|
20
51
|
}
|
|
21
|
-
export declare function useUpdateRecord<TRecord extends Record<string, unknown> = Record<string, unknown>>({ client, podId, tableName, recordId, enabled, onSuccess, onError, }: UseUpdateRecordOptions): UseUpdateRecordResult<TRecord>;
|
|
52
|
+
export declare function useUpdateRecord<TRecord extends Record<string, unknown> = Record<string, unknown>>({ client, podId, tableName, recordId, enabled, updateVia, updateFunctionName, onSuccess, onError, }: UseUpdateRecordOptions): UseUpdateRecordResult<TRecord>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { normalizeError, resolvePodClient } from "./utils.js";
|
|
3
|
-
export function useUpdateRecord({ client, podId, tableName, recordId = null, enabled = true, onSuccess, onError, }) {
|
|
3
|
+
export function useUpdateRecord({ client, podId, tableName, recordId = null, enabled = true, updateVia = "direct", updateFunctionName, onSuccess, onError, }) {
|
|
4
4
|
const [updatedRecord, setUpdatedRecord] = useState(null);
|
|
5
5
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
6
6
|
const [error, setError] = useState(null);
|
|
@@ -22,6 +22,17 @@ export function useUpdateRecord({ client, podId, tableName, recordId = null, ena
|
|
|
22
22
|
setError(null);
|
|
23
23
|
try {
|
|
24
24
|
const scopedClient = resolvePodClient(client, podId);
|
|
25
|
+
if (updateVia === "function") {
|
|
26
|
+
const functionName = updateFunctionName ?? trimmedTableName;
|
|
27
|
+
const input = { ...data, id: nextRecordId, record_id: nextRecordId };
|
|
28
|
+
const run = await scopedClient.functions.runs.create(functionName, { input });
|
|
29
|
+
const nextRecord = (run.output_data ?? { id: nextRecordId, ...data });
|
|
30
|
+
setUpdatedRecord(nextRecord);
|
|
31
|
+
if (nextRecord) {
|
|
32
|
+
onSuccessRef.current?.(nextRecord, run);
|
|
33
|
+
}
|
|
34
|
+
return nextRecord;
|
|
35
|
+
}
|
|
25
36
|
const response = await scopedClient.records.update(trimmedTableName, nextRecordId, data);
|
|
26
37
|
const nextRecord = (response.data ?? null);
|
|
27
38
|
setUpdatedRecord(nextRecord);
|
|
@@ -39,7 +50,7 @@ export function useUpdateRecord({ client, podId, tableName, recordId = null, ena
|
|
|
39
50
|
finally {
|
|
40
51
|
setIsSubmitting(false);
|
|
41
52
|
}
|
|
42
|
-
}, [client, isEnabled, podId, trimmedRecordId, trimmedTableName]);
|
|
53
|
+
}, [client, isEnabled, podId, trimmedRecordId, trimmedTableName, updateFunctionName, updateVia]);
|
|
43
54
|
const reset = useCallback(() => {
|
|
44
55
|
setUpdatedRecord(null);
|
|
45
56
|
setError(null);
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentResponse, AssistantResponse, AvailableModels, ColumnSchema, ConversationMessageResponse, ConversationResponse, CreateAgentRequest, CreateAssistantRequest, CreateTaskRequest, DatastoreQueryResponse, FlowRunEntity, FlowResponse, FunctionRunResponse, IconUploadResponse, OrganizationInvitationResponse, OrganizationMemberResponse, OrganizationResponse, PodConfigResponse, PodJoinRequestCreateResponse, PodMemberResponse, PodResponse, TableResponse, TaskMessageResponse, TaskResponse, UpdateAgentRequest, UpdateAssistantRequest, UserResponse } from "./openapi_client/index.js";
|
|
1
|
+
import type { AgentResponse, AssistantResponse, AvailableModels, ColumnSchema, ConversationMessageResponse, ConversationResponse, CreateAgentRequest, CreateAssistantRequest, CreateTaskRequest, DatastoreQueryResponse, DirectoryTreeNode, DirectoryTreeResponse, FileResponse, FileSearchResponse, FileSearchResultSchema, FlowRunEntity, FlowResponse, FunctionRunResponse, IconUploadResponse, OrganizationInvitationResponse, OrganizationMemberResponse, OrganizationResponse, PodConfigResponse, PodJoinRequestCreateResponse, PodMemberResponse, PodResponse, TableResponse, TaskMessageResponse, TaskResponse, UpdateAgentRequest, UpdateAssistantRequest, UserResponse } from "./openapi_client/index.js";
|
|
2
2
|
/** Public ergonomic types. */
|
|
3
3
|
export interface PageResult<T> {
|
|
4
4
|
items: T[];
|
|
@@ -55,6 +55,11 @@ export type Workflow = FlowResponse;
|
|
|
55
55
|
export type Table = TableResponse;
|
|
56
56
|
export type TableColumn = ColumnSchema;
|
|
57
57
|
export type DatastoreQueryResult = DatastoreQueryResponse;
|
|
58
|
+
export type DatastoreFile = FileResponse;
|
|
59
|
+
export type DatastoreFileSearchResponse = FileSearchResponse;
|
|
60
|
+
export type DatastoreFileSearchResult = FileSearchResultSchema;
|
|
61
|
+
export type DatastoreDirectoryTree = DirectoryTreeResponse;
|
|
62
|
+
export type DatastoreDirectoryTreeNode = DirectoryTreeNode;
|
|
58
63
|
export type Pod = PodResponse;
|
|
59
64
|
export type PodConfig = PodConfigResponse;
|
|
60
65
|
export type PodMember = PodMemberResponse;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lemma-sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.33",
|
|
4
4
|
"description": "Official TypeScript SDK for Lemma pod-scoped APIs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"build:bundle": "tsc -p tsconfig.bundle.json && node ./scripts/build_browser_bundle.mjs",
|
|
46
46
|
"clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true, maxRetries: 10, retryDelay: 100 })\"",
|
|
47
47
|
"registry:build": "shadcn build ./registry.json -o ./public/r",
|
|
48
|
+
"registry:check": "npm run registry:build && node ./scripts/check_registry_blocks.mjs",
|
|
48
49
|
"prepublishOnly": "npm run build",
|
|
49
50
|
"release:check": "npm run build && npm_config_cache=.npm-cache npm pack --dry-run"
|
|
50
51
|
},
|