@agent-native/core 0.119.6 → 0.120.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +19 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/index.ts +64 -3
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +45 -0
- package/corpus/core/src/client/use-chat-models.ts +45 -0
- package/corpus/core/src/server/analytics.ts +15 -0
- package/corpus/core/src/server/auth.ts +17 -13
- package/corpus/core/src/server/better-auth-instance.ts +13 -1
- package/corpus/core/src/templates/default/package.json +1 -1
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/assets/changelog/2026-07-23-new-chat-aligns-with-sidebar-controls.md +6 -0
- package/corpus/templates/assets/package.json +1 -1
- package/corpus/templates/brain/package.json +1 -1
- package/corpus/templates/calendar/package.json +1 -1
- package/corpus/templates/clips/package.json +1 -1
- package/corpus/templates/content/actions/add-database-item.ts +1 -0
- package/corpus/templates/content/actions/set-document-property.ts +0 -4
- package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +10 -1
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +10 -4
- package/corpus/templates/content/app/components/editor/DocumentInfoPanel.tsx +5 -1
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +6 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +52 -33
- package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +16 -4
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +16 -7
- package/corpus/templates/content/app/hooks/use-create-page.ts +8 -3
- package/corpus/templates/content/app/hooks/use-document-properties.ts +1 -0
- package/corpus/templates/content/app/hooks/use-documents.ts +3 -1
- package/corpus/templates/content/app/lib/optimistic-document.ts +29 -0
- package/corpus/templates/content/changelog/2026-07-21-editing-a-database-property-no-longer-refreshes-unrelated-co.md +6 -0
- package/corpus/templates/content/changelog/2026-07-23-new-database-ready.md +6 -0
- package/corpus/templates/content/package.json +1 -1
- package/corpus/templates/content/shared/api.ts +1 -0
- package/corpus/templates/design/package.json +1 -1
- package/corpus/templates/dispatch/package.json +1 -1
- package/corpus/templates/forms/.agents/skills/form-publishing/SKILL.md +9 -3
- package/corpus/templates/forms/AGENTS.md +11 -1
- package/corpus/templates/forms/README.md +24 -1
- package/corpus/templates/forms/actions/update-form.ts +12 -5
- package/corpus/templates/forms/app/components/builder/FieldPropertiesPanel.tsx +199 -1
- package/corpus/templates/forms/app/i18n/en-US.ts +13 -1
- package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +35 -4
- package/corpus/templates/forms/app/pages/FormFillPage.tsx +7 -16
- package/corpus/templates/forms/changelog/2026-07-23-forms-can-branch-into-follow-up-questions-and-route-response.md +6 -0
- package/corpus/templates/forms/package.json +1 -1
- package/corpus/templates/forms/server/handlers/submissions.ts +22 -31
- package/corpus/templates/forms/server/lib/integrations.ts +11 -2
- package/corpus/templates/forms/server/lib/public-form-ssr.ts +26 -5
- package/corpus/templates/forms/server/lib/validate-fields.ts +48 -11
- package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -0
- package/corpus/templates/forms/shared/conditional.ts +67 -0
- package/corpus/templates/macros/package.json +1 -1
- package/corpus/templates/mail/package.json +1 -1
- package/corpus/templates/slides/package.json +1 -1
- package/dist/cli/index.js +53 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +35 -0
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/use-chat-models.d.ts +1 -0
- package/dist/client/use-chat-models.d.ts.map +1 -1
- package/dist/client/use-chat-models.js +33 -0
- package/dist/client/use-chat-models.js.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
- package/dist/provider-api/actions/provider-api.d.ts +9 -9
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/analytics.d.ts +8 -0
- package/dist/server/analytics.d.ts.map +1 -1
- package/dist/server/analytics.js +16 -0
- package/dist/server/analytics.js.map +1 -1
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +5 -4
- package/dist/server/auth.js.map +1 -1
- package/dist/server/better-auth-instance.d.ts +8 -0
- package/dist/server/better-auth-instance.d.ts.map +1 -1
- package/dist/server/better-auth-instance.js +10 -1
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/templates/default/package.json +1 -1
- package/package.json +1 -1
- package/src/cli/index.ts +64 -3
- package/src/client/MultiTabAssistantChat.tsx +45 -0
- package/src/client/use-chat-models.ts +45 -0
- package/src/server/analytics.ts +15 -0
- package/src/server/auth.ts +17 -13
- package/src/server/better-auth-instance.ts +13 -1
- package/src/templates/default/package.json +1 -1
|
@@ -197,6 +197,7 @@ import {
|
|
|
197
197
|
} from "@/hooks/use-document-properties";
|
|
198
198
|
import {
|
|
199
199
|
isDocumentUpdateConflict,
|
|
200
|
+
type DocumentUpdateResult,
|
|
200
201
|
useDeleteDocument,
|
|
201
202
|
useDocument,
|
|
202
203
|
seedDatabaseItemDocumentCaches,
|
|
@@ -254,7 +255,10 @@ import { EmojiPicker } from "../EmojiPicker";
|
|
|
254
255
|
import {
|
|
255
256
|
createPreviewDocumentSaveController,
|
|
256
257
|
deferredPreviewDocumentSave,
|
|
258
|
+
type PreviewDocumentPayload,
|
|
257
259
|
type PreviewDocumentSaveAdapter,
|
|
260
|
+
type PreviewDocumentSaveDeferred,
|
|
261
|
+
type PreviewDocumentSaveSuccess,
|
|
258
262
|
} from "../previewDocumentSaveController";
|
|
259
263
|
import {
|
|
260
264
|
acquirePreviewDocumentSaveController,
|
|
@@ -436,7 +440,8 @@ export function databaseCreatedItemForImmediatePreview(
|
|
|
436
440
|
if (returnedItem) return returnedItem;
|
|
437
441
|
if (!response.createdItemId || !response.createdDocumentId) return null;
|
|
438
442
|
|
|
439
|
-
const now =
|
|
443
|
+
const now =
|
|
444
|
+
response.createdDocumentUpdatedAt ?? args.now ?? new Date().toISOString();
|
|
440
445
|
const position = Math.max(
|
|
441
446
|
0,
|
|
442
447
|
(response.pagination?.totalItems ?? response.items.length + 1) - 1,
|
|
@@ -473,6 +478,42 @@ export function databaseCreatedItemForImmediatePreview(
|
|
|
473
478
|
};
|
|
474
479
|
}
|
|
475
480
|
|
|
481
|
+
export function previewDocumentSaveResult(args: {
|
|
482
|
+
result: DocumentUpdateResult;
|
|
483
|
+
payload: PreviewDocumentPayload;
|
|
484
|
+
baseline?: PreviewDocumentPayload;
|
|
485
|
+
contentChanged: boolean;
|
|
486
|
+
}): PreviewDocumentSaveDeferred | PreviewDocumentSaveSuccess {
|
|
487
|
+
const serverDocument = isDocumentUpdateConflict(args.result)
|
|
488
|
+
? args.result.document
|
|
489
|
+
: args.result;
|
|
490
|
+
const titleSaveObservedExternalBody =
|
|
491
|
+
!args.contentChanged && serverDocument.content !== args.payload.content;
|
|
492
|
+
|
|
493
|
+
if (isDocumentUpdateConflict(args.result) || titleSaveObservedExternalBody) {
|
|
494
|
+
return deferredPreviewDocumentSave("conflict", {
|
|
495
|
+
lastSaved: args.baseline ?? args.payload,
|
|
496
|
+
pending: {
|
|
497
|
+
title: serverDocument.title,
|
|
498
|
+
content: serverDocument.content,
|
|
499
|
+
loadedUpdatedAt: serverDocument.updatedAt,
|
|
500
|
+
loadedContentWasEmpty: isEffectivelyEmptyDocumentContent(
|
|
501
|
+
serverDocument.content,
|
|
502
|
+
),
|
|
503
|
+
},
|
|
504
|
+
deferredReason: "conflict",
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
outcome: "saved",
|
|
510
|
+
loadedUpdatedAt: args.result.updatedAt,
|
|
511
|
+
loadedContentWasEmpty: isEffectivelyEmptyDocumentContent(
|
|
512
|
+
args.result.content,
|
|
513
|
+
),
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
476
517
|
export function databaseBuilderHydrationSourceForItem(
|
|
477
518
|
item: ContentDatabaseItem,
|
|
478
519
|
sources: ContentDatabaseSource[],
|
|
@@ -4098,8 +4139,6 @@ function DatabaseItemPreview({
|
|
|
4098
4139
|
// ever touch its own row's state. See previewDocumentSaveRegistry.
|
|
4099
4140
|
const updateDocumentRef = useRef(updateDocument);
|
|
4100
4141
|
updateDocumentRef.current = updateDocument;
|
|
4101
|
-
const queryClientRef = useRef(queryClient);
|
|
4102
|
-
queryClientRef.current = queryClient;
|
|
4103
4142
|
const bodyHydrationPendingRef = useRef(bodyHydrationPending);
|
|
4104
4143
|
bodyHydrationPendingRef.current = bodyHydrationPending;
|
|
4105
4144
|
const draftVersionsRef = useRef<Map<string, number | null>>(new Map());
|
|
@@ -4244,30 +4283,14 @@ function DatabaseItemPreview({
|
|
|
4244
4283
|
},
|
|
4245
4284
|
{
|
|
4246
4285
|
onSuccess: (result) => {
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
loadedContentWasEmpty: isEffectivelyEmptyDocumentContent(
|
|
4256
|
-
result.document.content,
|
|
4257
|
-
),
|
|
4258
|
-
},
|
|
4259
|
-
deferredReason: "conflict",
|
|
4260
|
-
}),
|
|
4261
|
-
);
|
|
4262
|
-
return;
|
|
4263
|
-
}
|
|
4264
|
-
resolve({
|
|
4265
|
-
outcome: "saved" as const,
|
|
4266
|
-
loadedUpdatedAt: result.updatedAt,
|
|
4267
|
-
loadedContentWasEmpty: isEffectivelyEmptyDocumentContent(
|
|
4268
|
-
result.content,
|
|
4269
|
-
),
|
|
4270
|
-
});
|
|
4286
|
+
resolve(
|
|
4287
|
+
previewDocumentSaveResult({
|
|
4288
|
+
result,
|
|
4289
|
+
payload,
|
|
4290
|
+
baseline,
|
|
4291
|
+
contentChanged,
|
|
4292
|
+
}),
|
|
4293
|
+
);
|
|
4271
4294
|
},
|
|
4272
4295
|
onError: reject,
|
|
4273
4296
|
},
|
|
@@ -4276,12 +4299,6 @@ function DatabaseItemPreview({
|
|
|
4276
4299
|
onSaved: (persistedPayload) => {
|
|
4277
4300
|
const controller = peekPreviewDocumentSaveController(documentId);
|
|
4278
4301
|
if (controller) enqueueDraftWrite(controller, "delete", persistedPayload);
|
|
4279
|
-
void queryClientRef.current.invalidateQueries({
|
|
4280
|
-
queryKey: contentDatabaseQueryKey(databaseDocumentId),
|
|
4281
|
-
});
|
|
4282
|
-
void queryClientRef.current.invalidateQueries({
|
|
4283
|
-
queryKey: ["action", "list-documents"],
|
|
4284
|
-
});
|
|
4285
4302
|
},
|
|
4286
4303
|
onError: (err) => {
|
|
4287
4304
|
toast.error(dbText("failedToSavePagePreview"), {
|
|
@@ -4876,6 +4893,7 @@ function DatabaseItemPreview({
|
|
|
4876
4893
|
{previewDocument.databaseMembership ? (
|
|
4877
4894
|
<DocumentProperties
|
|
4878
4895
|
documentId={previewDocument.id}
|
|
4896
|
+
databaseDocumentId={databaseDocumentId}
|
|
4879
4897
|
canEdit={previewCanEdit}
|
|
4880
4898
|
popoversPortalled={false}
|
|
4881
4899
|
/>
|
|
@@ -4913,6 +4931,7 @@ function DatabaseItemPreview({
|
|
|
4913
4931
|
const editor = previewDocument.databaseMembership ? (
|
|
4914
4932
|
<DocumentBlockFields
|
|
4915
4933
|
documentId={previewDocument.id}
|
|
4934
|
+
databaseDocumentId={databaseDocumentId}
|
|
4916
4935
|
canEdit={previewCanEdit}
|
|
4917
4936
|
primaryEditor={primaryEditor}
|
|
4918
4937
|
/>
|
|
@@ -130,8 +130,8 @@ export interface PreviewDocumentSaveController {
|
|
|
130
130
|
/** Adopt `payload` as the confirmed-saved baseline (no save scheduled). */
|
|
131
131
|
mark(payload: PreviewDocumentPayload): void;
|
|
132
132
|
/**
|
|
133
|
-
* Adopt a fresher server baseline while retaining the user
|
|
134
|
-
*
|
|
133
|
+
* Adopt a fresher server baseline while retaining only fields the user
|
|
134
|
+
* changed locally. Used only after an explicit "keep local draft" choice.
|
|
135
135
|
*/
|
|
136
136
|
rebasePending(payload: PreviewDocumentPayload): void;
|
|
137
137
|
/** Replace callbacks captured by an older preview mount. */
|
|
@@ -261,8 +261,7 @@ export function createPreviewDocumentSaveController(
|
|
|
261
261
|
// the controller was created/last marked — e.g. "empty" for a
|
|
262
262
|
// brand-new page — forever, even after real content has been saved.
|
|
263
263
|
const success = asSaveSuccess(result);
|
|
264
|
-
|
|
265
|
-
...attempted,
|
|
264
|
+
const savedMetadata = {
|
|
266
265
|
...(success?.loadedUpdatedAt !== undefined
|
|
267
266
|
? { loadedUpdatedAt: success.loadedUpdatedAt }
|
|
268
267
|
: {}),
|
|
@@ -270,6 +269,15 @@ export function createPreviewDocumentSaveController(
|
|
|
270
269
|
? { loadedContentWasEmpty: success.loadedContentWasEmpty }
|
|
271
270
|
: {}),
|
|
272
271
|
};
|
|
272
|
+
lastSaved = {
|
|
273
|
+
...attempted,
|
|
274
|
+
...savedMetadata,
|
|
275
|
+
};
|
|
276
|
+
// A later keystroke starts from `pending`, including while this save is
|
|
277
|
+
// in flight. Rebase that trailing payload onto our own successful write
|
|
278
|
+
// so its next CAS does not mistake the preceding save for an external
|
|
279
|
+
// change.
|
|
280
|
+
pending = { ...pending, ...savedMetadata };
|
|
273
281
|
hasSavedLocally = true;
|
|
274
282
|
deferredReason = null;
|
|
275
283
|
inFlight = null;
|
|
@@ -336,9 +344,13 @@ export function createPreviewDocumentSaveController(
|
|
|
336
344
|
},
|
|
337
345
|
rebasePending(payload: PreviewDocumentPayload) {
|
|
338
346
|
clearTimer();
|
|
347
|
+
const titleChangedLocally = pending.title !== lastSaved.title;
|
|
348
|
+
const contentChangedLocally = pending.content !== lastSaved.content;
|
|
339
349
|
lastSaved = { ...payload };
|
|
340
350
|
pending = {
|
|
341
351
|
...pending,
|
|
352
|
+
title: titleChangedLocally ? pending.title : payload.title,
|
|
353
|
+
content: contentChangedLocally ? pending.content : payload.content,
|
|
342
354
|
loadedUpdatedAt: payload.loadedUpdatedAt,
|
|
343
355
|
loadedContentWasEmpty: payload.loadedContentWasEmpty,
|
|
344
356
|
};
|
|
@@ -121,6 +121,10 @@ import {
|
|
|
121
121
|
filterDocumentTreeDocuments,
|
|
122
122
|
} from "@/hooks/use-documents";
|
|
123
123
|
import { useLocalStorage } from "@/hooks/use-local-storage";
|
|
124
|
+
import {
|
|
125
|
+
markDocumentCreationPending,
|
|
126
|
+
shouldCreateDocumentOptimistically,
|
|
127
|
+
} from "@/lib/optimistic-document";
|
|
124
128
|
import { cn } from "@/lib/utils";
|
|
125
129
|
|
|
126
130
|
import {
|
|
@@ -747,7 +751,12 @@ export function DocumentSidebar({
|
|
|
747
751
|
optimisticId?: string,
|
|
748
752
|
rootFilesDatabaseId?: string,
|
|
749
753
|
) => {
|
|
750
|
-
if (
|
|
754
|
+
if (
|
|
755
|
+
!shouldCreateDocumentOptimistically({
|
|
756
|
+
localFileMode,
|
|
757
|
+
filesDatabaseId: rootFilesDatabaseId,
|
|
758
|
+
})
|
|
759
|
+
) {
|
|
751
760
|
try {
|
|
752
761
|
const created = await createDocument.mutateAsync({
|
|
753
762
|
title: "",
|
|
@@ -774,7 +783,7 @@ export function DocumentSidebar({
|
|
|
774
783
|
|
|
775
784
|
const id = optimisticId ?? nanoid();
|
|
776
785
|
const now = new Date().toISOString();
|
|
777
|
-
const tempDoc
|
|
786
|
+
const tempDoc = markDocumentCreationPending({
|
|
778
787
|
id,
|
|
779
788
|
parentId: parentId ?? null,
|
|
780
789
|
title: "",
|
|
@@ -789,7 +798,7 @@ export function DocumentSidebar({
|
|
|
789
798
|
canManage: true,
|
|
790
799
|
createdAt: now,
|
|
791
800
|
updatedAt: now,
|
|
792
|
-
};
|
|
801
|
+
});
|
|
793
802
|
|
|
794
803
|
// Optimistically inject into caches so UI updates immediately
|
|
795
804
|
queryClient.setQueryData(LIST_DOCUMENTS_QUERY_KEY, (old: any) => {
|
|
@@ -824,14 +833,14 @@ export function DocumentSidebar({
|
|
|
824
833
|
spaceId: parentId ? undefined : rootSpaceId,
|
|
825
834
|
});
|
|
826
835
|
const nextId = created?.id || id;
|
|
836
|
+
queryClient.setQueryData(
|
|
837
|
+
["action", "get-document", { id: nextId }],
|
|
838
|
+
created,
|
|
839
|
+
);
|
|
827
840
|
if (nextId !== id) {
|
|
828
841
|
queryClient.removeQueries({
|
|
829
842
|
queryKey: ["action", "get-document", { id }],
|
|
830
843
|
});
|
|
831
|
-
queryClient.setQueryData(
|
|
832
|
-
["action", "get-document", { id: nextId }],
|
|
833
|
-
created,
|
|
834
|
-
);
|
|
835
844
|
navigateToDocument(nextId);
|
|
836
845
|
}
|
|
837
846
|
// Replace optimistic doc with real server doc + clear any 404 error
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { useContentSpaces } from "@/hooks/use-content-spaces";
|
|
13
13
|
import { useCreateDocument } from "@/hooks/use-documents";
|
|
14
14
|
import { useLocalStorage } from "@/hooks/use-local-storage";
|
|
15
|
+
import { markDocumentCreationPending } from "@/lib/optimistic-document";
|
|
15
16
|
|
|
16
17
|
const LIST_DOCUMENTS_QUERY_KEY = [
|
|
17
18
|
"action",
|
|
@@ -63,7 +64,7 @@ export function useCreatePage(opts?: {
|
|
|
63
64
|
}
|
|
64
65
|
const id = nanoid();
|
|
65
66
|
const now = new Date().toISOString();
|
|
66
|
-
const tempDoc
|
|
67
|
+
const tempDoc = markDocumentCreationPending({
|
|
67
68
|
id,
|
|
68
69
|
parentId: parentId ?? null,
|
|
69
70
|
title: "",
|
|
@@ -75,7 +76,7 @@ export function useCreatePage(opts?: {
|
|
|
75
76
|
visibility: "private",
|
|
76
77
|
createdAt: now,
|
|
77
78
|
updatedAt: now,
|
|
78
|
-
};
|
|
79
|
+
});
|
|
79
80
|
|
|
80
81
|
queryClient.setQueryData(LIST_DOCUMENTS_QUERY_KEY, (old: any) => {
|
|
81
82
|
const docs: Document[] =
|
|
@@ -90,12 +91,16 @@ export function useCreatePage(opts?: {
|
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
const persist = async () => {
|
|
93
|
-
await createDocument.mutateAsync({
|
|
94
|
+
const created = await createDocument.mutateAsync({
|
|
94
95
|
id,
|
|
95
96
|
title: "",
|
|
96
97
|
parentId: parentId ?? undefined,
|
|
97
98
|
spaceId,
|
|
98
99
|
});
|
|
100
|
+
queryClient.setQueryData(
|
|
101
|
+
["action", "get-document", { id: created.id }],
|
|
102
|
+
created,
|
|
103
|
+
);
|
|
99
104
|
// Replace optimistic doc with real server doc + clear any 404 error
|
|
100
105
|
// state from the in-flight fetch that ran before create completed.
|
|
101
106
|
queryClient.invalidateQueries({
|
|
@@ -70,6 +70,7 @@ export function useSetDocumentProperty(
|
|
|
70
70
|
DocumentPropertiesResponse,
|
|
71
71
|
SetDocumentPropertyRequest
|
|
72
72
|
>("set-document-property", {
|
|
73
|
+
skipActionQueryInvalidation: true,
|
|
73
74
|
onMutate: async (variables) => {
|
|
74
75
|
await queryClient.cancelQueries(
|
|
75
76
|
contentDatabaseQueryFilter(databaseDocumentId),
|
|
@@ -342,7 +342,9 @@ export function useUpdatePreviewDocumentDraft() {
|
|
|
342
342
|
expectedTitle: string;
|
|
343
343
|
expectedContent: string;
|
|
344
344
|
}
|
|
345
|
-
>("update-preview-document-draft"
|
|
345
|
+
>("update-preview-document-draft", {
|
|
346
|
+
skipActionQueryInvalidation: true,
|
|
347
|
+
});
|
|
346
348
|
}
|
|
347
349
|
|
|
348
350
|
export function useCreateDocument() {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Document } from "@shared/api";
|
|
2
|
+
|
|
3
|
+
const pendingDocumentCreation = Symbol("pendingDocumentCreation");
|
|
4
|
+
|
|
5
|
+
type PendingDocument = Document & {
|
|
6
|
+
[pendingDocumentCreation]?: true;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function markDocumentCreationPending(document: Document): Document {
|
|
10
|
+
return Object.assign(document, { [pendingDocumentCreation]: true as const });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isDocumentCreationPending(document: Document): boolean {
|
|
14
|
+
return (document as PendingDocument)[pendingDocumentCreation] === true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isDatabaseChoicePending(
|
|
18
|
+
document: Document,
|
|
19
|
+
databaseCreationPending: boolean,
|
|
20
|
+
): boolean {
|
|
21
|
+
return databaseCreationPending || isDocumentCreationPending(document);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function shouldCreateDocumentOptimistically(args: {
|
|
25
|
+
localFileMode: boolean;
|
|
26
|
+
filesDatabaseId?: string;
|
|
27
|
+
}): boolean {
|
|
28
|
+
return !args.localFileMode || Boolean(args.filesDatabaseId);
|
|
29
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"dev": "agent-native dev",
|
|
7
|
+
"dev": "agent-native dev --inspect",
|
|
8
8
|
"dev:database": "node scripts/check-native-deps.mjs && node scripts/dev-database.mjs",
|
|
9
9
|
"build": "agent-native build",
|
|
10
10
|
"start": "agent-native start",
|
|
@@ -77,7 +77,7 @@ Each form has a `settings` JSON object:
|
|
|
77
77
|
| `showProgressBar` | boolean | Show progress bar for multi-section forms |
|
|
78
78
|
| `emailOnNewResponses` | boolean | Email the form owner's account when someone submits a response |
|
|
79
79
|
| `anonymous` | boolean | Suppress IP, submitter identity, chat/run ids, page URL, and client-surface metadata for every response |
|
|
80
|
-
| `integrations` | array | Webhook/Slack/Discord notification configs |
|
|
80
|
+
| `integrations` | array | Webhook/Slack/Discord/Google Sheets notification configs |
|
|
81
81
|
|
|
82
82
|
For a genuinely anonymous form, set `anonymous: true` when creating the form.
|
|
83
83
|
Do not describe an ordinary published form as anonymous: published forms accept
|
|
@@ -91,9 +91,15 @@ Forms can notify external services on submission:
|
|
|
91
91
|
| Type | Description |
|
|
92
92
|
| --------------- | ------------------------------- |
|
|
93
93
|
| `webhook` | POST JSON to any URL |
|
|
94
|
-
| `slack` | Send to a Slack channel
|
|
94
|
+
| `slack` | Send to a Slack channel through an Incoming Webhook URL |
|
|
95
95
|
| `discord` | Send to a Discord webhook |
|
|
96
|
-
| `google-sheets` |
|
|
96
|
+
| `google-sheets` | Send response JSON to a deployed Apps Script `/exec` URL |
|
|
97
|
+
|
|
98
|
+
These are outbound form destinations configured in the form builder's
|
|
99
|
+
**Integrations** tab. They are separate from the managed Slack/Messaging
|
|
100
|
+
connection. Google Sheets Apps Script handlers should parse
|
|
101
|
+
`JSON.parse(e.postData.contents)`; a spreadsheet URL or `/dev` URL will not
|
|
102
|
+
receive submissions.
|
|
97
103
|
|
|
98
104
|
## Related Skills
|
|
99
105
|
|
|
@@ -40,7 +40,17 @@ ladder.
|
|
|
40
40
|
- To email the form owner when someone submits a response, set
|
|
41
41
|
`settings.emailOnNewResponses: true` through `create-form` or `update-form`.
|
|
42
42
|
Delivery uses the configured framework email provider (`RESEND_API_KEY` or
|
|
43
|
-
`SENDGRID_API_KEY`)
|
|
43
|
+
`SENDGRID_API_KEY`) in the form owner's request context and sends to the form
|
|
44
|
+
owner's account email. A public submission can succeed even when delivery
|
|
45
|
+
fails, so check the server logs and provider configuration when debugging.
|
|
46
|
+
- Conditional fields use `conditional: { fieldId, operator, value }`. The
|
|
47
|
+
`fieldId` must reference an earlier field; supported operators are `equals`,
|
|
48
|
+
`not_equals`, and `contains`. Hidden fields and their stale values are not
|
|
49
|
+
persisted or delivered to integrations.
|
|
50
|
+
- Form integrations are outbound webhooks. Slack requires an Incoming Webhook
|
|
51
|
+
URL, and Google Sheets requires a deployed Google Apps Script `/exec` URL that
|
|
52
|
+
parses `JSON.parse(e.postData.contents)` and appends the received values.
|
|
53
|
+
They are separate from the managed Slack/Messaging connection.
|
|
44
54
|
- Form UX should stay focused: clear labels, sensible validation, minimal
|
|
45
55
|
required fields, and progressive disclosure for advanced settings.
|
|
46
56
|
- Public form submission endpoints must be intentionally public; keep management
|
|
@@ -17,7 +17,8 @@ them.
|
|
|
17
17
|
- Shipped field types: text, email, number, long text, select, multi-select,
|
|
18
18
|
checkbox, radio, date, rating, and scale.
|
|
19
19
|
- Submissions stored in SQL with a per-response detail view and dashboard.
|
|
20
|
-
- Route submissions to webhooks, Slack, Discord, or Google Sheets
|
|
20
|
+
- Route submissions to webhooks, Slack, Discord, or Google Sheets via Apps
|
|
21
|
+
Script.
|
|
21
22
|
- Publish public form URLs with a thank-you message.
|
|
22
23
|
|
|
23
24
|
## Develop locally
|
|
@@ -32,3 +33,25 @@ pnpm dev
|
|
|
32
33
|
```
|
|
33
34
|
|
|
34
35
|
Full docs: [agent-native.com/docs/template-forms](https://agent-native.com/docs/template-forms).
|
|
36
|
+
|
|
37
|
+
## Submission destinations
|
|
38
|
+
|
|
39
|
+
Open a form's **Integrations** tab and save a destination before publishing.
|
|
40
|
+
Slack uses an Incoming Webhook URL. Google Sheets uses a deployed Apps Script
|
|
41
|
+
web app URL ending in `/exec` (not a spreadsheet URL or `/dev` URL). The script
|
|
42
|
+
should parse the JSON body and append the values, for example:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
function doPost(e) {
|
|
46
|
+
const payload = JSON.parse(e.postData.contents);
|
|
47
|
+
SpreadsheetApp.getActiveSpreadsheet()
|
|
48
|
+
.getSheets()[0]
|
|
49
|
+
.appendRow([payload.submittedAt, payload.formTitle, payload.responseId]);
|
|
50
|
+
return ContentService.createTextOutput(JSON.stringify({ ok: true }));
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Deploy it as a web app that executes as you and allows anyone with the URL to
|
|
55
|
+
access it, then paste the deployed `/exec` URL into Forms. The destination
|
|
56
|
+
receives the form metadata plus one property per field label; duplicate labels
|
|
57
|
+
are disambiguated with the field ID.
|
|
@@ -85,22 +85,29 @@ export default defineAction({
|
|
|
85
85
|
updates.fields = JSON.stringify(parsedFields);
|
|
86
86
|
}
|
|
87
87
|
if (args.settings !== undefined) {
|
|
88
|
-
let
|
|
88
|
+
let incomingSettings: FormSettings;
|
|
89
89
|
if (typeof args.settings === "string") {
|
|
90
90
|
try {
|
|
91
|
-
|
|
92
|
-
updates.settings = args.settings;
|
|
91
|
+
incomingSettings = JSON.parse(args.settings) as FormSettings;
|
|
93
92
|
} catch {
|
|
94
93
|
throw new Error("--settings must be valid JSON");
|
|
95
94
|
}
|
|
96
95
|
} else {
|
|
97
|
-
|
|
98
|
-
updates.settings = JSON.stringify(args.settings);
|
|
96
|
+
incomingSettings = args.settings as unknown as FormSettings;
|
|
99
97
|
}
|
|
98
|
+
let existingSettings: FormSettings = {};
|
|
99
|
+
try {
|
|
100
|
+
existingSettings = JSON.parse(existing.settings) as FormSettings;
|
|
101
|
+
} catch {
|
|
102
|
+
// Keep malformed legacy settings recoverable by replacing them with
|
|
103
|
+
// the valid settings supplied by this update.
|
|
104
|
+
}
|
|
105
|
+
const parsedSettings = { ...existingSettings, ...incomingSettings };
|
|
100
106
|
// Reject blocked integration URLs at save time (private IPs,
|
|
101
107
|
// cloud-metadata, non-http(s) schemes). fireIntegrations also
|
|
102
108
|
// re-checks at runtime as defense-in-depth.
|
|
103
109
|
assertIntegrationUrlsAllowed(parsedSettings);
|
|
110
|
+
updates.settings = JSON.stringify(parsedSettings);
|
|
104
111
|
}
|
|
105
112
|
if (args.status !== undefined) updates.status = args.status;
|
|
106
113
|
|