@agent-native/core 0.173.1-nightly-20260825150004 → 0.173.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 CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 8606
34
+ - template files: 8609
@@ -1,11 +1,18 @@
1
1
  import { defineAction } from "@agent-native/core/action";
2
2
  import { buildDeepLink } from "@agent-native/core/server";
3
- import { resolveAccess } from "@agent-native/core/sharing";
4
- import { asc, eq } from "drizzle-orm";
3
+ import { getRequestUserEmail } from "@agent-native/core/server/request-context";
4
+ import { accessFilter, resolveAccess } from "@agent-native/core/sharing";
5
+ import { and, asc, eq, isNull, or } from "drizzle-orm";
5
6
  import { z } from "zod";
6
7
 
7
8
  import { getDb, schema } from "../server/db/index.js";
9
+ import type {
10
+ ContentDatabaseItem,
11
+ ContentDatabaseTableQuery,
12
+ } from "../shared/api.js";
8
13
  import { blocksContentHash } from "../shared/blocks-field-identity.js";
14
+ import { renderDatabaseCsv } from "../shared/database-csv-export.js";
15
+ import { applyContentDatabaseTableQuery } from "../shared/database-query.js";
9
16
  import {
10
17
  buildDocumentExport,
11
18
  collectionItemsMarkdown,
@@ -16,11 +23,221 @@ import {
16
23
  isPrimaryBlocksField,
17
24
  } from "../shared/properties.js";
18
25
  import { resolveContentDocumentAccess } from "./_content-document-access.js";
19
- import { getDatabaseByDocumentId } from "./_database-utils.js";
20
- import { listPropertiesForAllDocumentDatabases } from "./_property-utils.js";
26
+ import { listContentOrganizationMemberships } from "./_content-space-access.js";
27
+ import {
28
+ CONTENT_DATABASE_MAX_READ_LIMIT,
29
+ getDatabaseByDocumentId,
30
+ } from "./_database-utils.js";
31
+ import {
32
+ listPropertiesForAllDocumentDatabases,
33
+ listPropertiesForDatabase,
34
+ listPropertiesForDatabaseDocuments,
35
+ parseDatabaseViewConfig,
36
+ } from "./_property-utils.js";
21
37
 
22
38
  const COLLECTION_EXPORT_ACCESS_CONCURRENCY = 8;
23
39
 
40
+ const collectionSchema = z.object({
41
+ scope: z.discriminatedUnion("kind", [
42
+ z.object({ kind: z.literal("all_members") }),
43
+ z.object({
44
+ kind: z.literal("current_view"),
45
+ viewId: z.string().min(1),
46
+ query: z.object({
47
+ search: z.string().max(500),
48
+ filters: z
49
+ .array(
50
+ z.object({
51
+ key: z.string(),
52
+ label: z.string(),
53
+ operator: z.enum([
54
+ "contains",
55
+ "equals",
56
+ "does_not_equal",
57
+ "greater_than",
58
+ "less_than",
59
+ "before",
60
+ "after",
61
+ "between",
62
+ "is_checked",
63
+ "is_unchecked",
64
+ "is_empty",
65
+ "is_not_empty",
66
+ ]),
67
+ value: z.string(),
68
+ filterGroupId: z.string().optional(),
69
+ parentFilterGroupId: z.string().optional(),
70
+ }),
71
+ )
72
+ .max(50),
73
+ sorts: z
74
+ .array(
75
+ z.object({
76
+ key: z.string(),
77
+ label: z.string(),
78
+ direction: z.enum(["asc", "desc"]),
79
+ }),
80
+ )
81
+ .max(20),
82
+ filterMode: z.enum(["and", "or"]),
83
+ }),
84
+ }),
85
+ ]),
86
+ propertyIds: z.array(z.string().min(1)).max(200),
87
+ });
88
+
89
+ type CollectionExport = z.infer<typeof collectionSchema>;
90
+
91
+ function assertValidQuery(
92
+ query: ContentDatabaseTableQuery,
93
+ propertyIds: ReadonlySet<string>,
94
+ ) {
95
+ for (const key of [...query.filters, ...query.sorts].map(({ key }) => key)) {
96
+ if (key !== "name" && !propertyIds.has(key)) {
97
+ throw new Error(`Unknown database property "${key}" in export query`);
98
+ }
99
+ }
100
+ }
101
+
102
+ async function databaseCsvContent(
103
+ documentId: string,
104
+ collection: CollectionExport,
105
+ ) {
106
+ const database = await getDatabaseByDocumentId(documentId);
107
+ if (!database) throw new Error("CSV export requires a database document");
108
+
109
+ const properties = await listPropertiesForDatabase(database.id);
110
+ const propertyById = new Map(
111
+ properties.map((property) => [property.definition.id, property]),
112
+ );
113
+ if (new Set(collection.propertyIds).size !== collection.propertyIds.length) {
114
+ throw new Error("CSV export property IDs must be unique");
115
+ }
116
+ const selectedProperties = collection.propertyIds.map((propertyId) => {
117
+ const property = propertyById.get(propertyId);
118
+ if (!property) throw new Error(`Unknown database property "${propertyId}"`);
119
+ return property;
120
+ });
121
+
122
+ const query =
123
+ collection.scope.kind === "current_view" ? collection.scope.query : null;
124
+ const scope = collection.scope;
125
+ if (scope.kind === "current_view") {
126
+ const view = parseDatabaseViewConfig(database.viewConfigJson).views.find(
127
+ (candidate) => candidate.id === scope.viewId,
128
+ );
129
+ if (!view) throw new Error(`Database view "${scope.viewId}" not found`);
130
+ assertValidQuery(query!, new Set(propertyById.keys()));
131
+ }
132
+ const blocksAreNeeded =
133
+ selectedProperties.some((property) =>
134
+ isBlocksPropertyType(property.definition.type),
135
+ ) ||
136
+ (!!query &&
137
+ (query.search.trim().length > 0 ||
138
+ [...query.filters, ...query.sorts].some((constraint) => {
139
+ const property = propertyById.get(constraint.key);
140
+ return !!property && isBlocksPropertyType(property.definition.type);
141
+ })));
142
+
143
+ const userEmail = getRequestUserEmail();
144
+ const memberships = userEmail
145
+ ? await listContentOrganizationMemberships(userEmail)
146
+ : [];
147
+ const accessClauses = [accessFilter(schema.documents, schema.documentShares)];
148
+ for (const membership of memberships) {
149
+ accessClauses.push(
150
+ accessFilter(schema.documents, schema.documentShares, {
151
+ userEmail: userEmail!,
152
+ orgId: membership.orgId,
153
+ }),
154
+ );
155
+ }
156
+ const rows = await getDb()
157
+ .select({
158
+ item: schema.contentDatabaseItems,
159
+ document: schema.documents,
160
+ })
161
+ .from(schema.contentDatabaseItems)
162
+ .innerJoin(
163
+ schema.documents,
164
+ eq(schema.documents.id, schema.contentDatabaseItems.documentId),
165
+ )
166
+ .where(
167
+ and(
168
+ eq(schema.contentDatabaseItems.databaseId, database.id),
169
+ isNull(schema.documents.trashedAt),
170
+ or(...accessClauses),
171
+ ),
172
+ )
173
+ .orderBy(
174
+ asc(schema.contentDatabaseItems.position),
175
+ asc(schema.contentDatabaseItems.createdAt),
176
+ asc(schema.contentDatabaseItems.id),
177
+ )
178
+ .limit(CONTENT_DATABASE_MAX_READ_LIMIT + 1);
179
+ if (rows.length > CONTENT_DATABASE_MAX_READ_LIMIT) {
180
+ throw new Error(
181
+ `CSV export supports up to ${CONTENT_DATABASE_MAX_READ_LIMIT} accessible rows.`,
182
+ );
183
+ }
184
+ if (blocksAreNeeded) {
185
+ for (const { item } of rows) {
186
+ if (
187
+ item.bodyHydrationStatus !== "hydrated" &&
188
+ item.bodyHydrationStatus !== "unavailable"
189
+ ) {
190
+ throw new Error(
191
+ `Database item "${item.documentId}" is not ready for export`,
192
+ );
193
+ }
194
+ }
195
+ }
196
+ const documents = rows.map((row) => row.document);
197
+ const propertiesByDocumentId = await listPropertiesForDatabaseDocuments(
198
+ database.id,
199
+ documents,
200
+ );
201
+ const queryItems: ContentDatabaseItem[] = rows.map((row) => ({
202
+ id: row.item.id,
203
+ databaseId: row.item.databaseId,
204
+ document: {
205
+ id: row.document.id,
206
+ parentId: row.document.parentId,
207
+ title: row.document.title,
208
+ content: row.document.content,
209
+ description: row.document.description ?? undefined,
210
+ icon: row.document.icon,
211
+ position: row.document.position,
212
+ isFavorite: row.document.isFavorite === 1,
213
+ hideFromSearch: row.document.hideFromSearch === 1,
214
+ createdAt: row.document.createdAt,
215
+ updatedAt: row.document.updatedAt,
216
+ },
217
+ position: row.item.position,
218
+ properties: propertiesByDocumentId.get(row.document.id) ?? [],
219
+ }));
220
+ const selectedRows = query
221
+ ? applyContentDatabaseTableQuery(queryItems, properties, query)
222
+ : queryItems;
223
+ return renderDatabaseCsv(
224
+ selectedProperties.map((property) => ({
225
+ id: property.definition.id,
226
+ name: property.definition.name,
227
+ property,
228
+ })),
229
+ selectedRows.map((row) => ({
230
+ title: row.document.title,
231
+ values: new Map(
232
+ row.properties.map((property) => [
233
+ property.definition.id,
234
+ property.value,
235
+ ]),
236
+ ),
237
+ })),
238
+ );
239
+ }
240
+
24
241
  async function databaseExportContent(documentId: string) {
25
242
  const database = await getDatabaseByDocumentId(documentId);
26
243
  if (!database) return null;
@@ -82,9 +299,12 @@ export default defineAction({
82
299
  schema: z.object({
83
300
  id: z.string().describe("Document ID (required)"),
84
301
  format: z
85
- .enum(["pdf", "markdown", "html"])
302
+ .enum(["pdf", "markdown", "html", "csv"])
86
303
  .default("pdf")
87
- .describe("Export format: pdf, markdown, or html."),
304
+ .describe("Export format: pdf, markdown, html, or csv."),
305
+ collection: collectionSchema
306
+ .optional()
307
+ .describe("Database CSV export scope and selected property IDs."),
88
308
  title: z
89
309
  .string()
90
310
  .max(500)
@@ -98,11 +318,35 @@ export default defineAction({
98
318
  }),
99
319
  readOnly: true,
100
320
  publicAgent: { expose: true, readOnly: true, requiresAuth: true },
101
- run: async ({ id, format, title, content }) => {
321
+ run: async ({ id, format, title, content, collection }) => {
102
322
  const access = await resolveAccess("document", id);
103
323
  if (!access) throw new Error(`Document "${id}" not found`);
104
324
 
105
325
  const doc = access.resource;
326
+ if (format === "csv") {
327
+ if (!collection)
328
+ throw new Error("CSV export requires collection options");
329
+ const content = await databaseCsvContent(doc.id, collection);
330
+ return {
331
+ id: doc.id,
332
+ title: doc.title || "Untitled",
333
+ format,
334
+ filename: `${
335
+ (doc.title || "untitled")
336
+ .replace(/[^a-z0-9]+/gi, "-")
337
+ .replace(/^-+|-+$/g, "")
338
+ .toLowerCase() || "untitled"
339
+ }.csv`,
340
+ mimeType: "text/csv;charset=utf-8",
341
+ content,
342
+ print: false,
343
+ deepLink: buildDeepLink({
344
+ app: "content",
345
+ view: "editor",
346
+ params: { documentId: doc.id },
347
+ }),
348
+ };
349
+ }
106
350
  const properties = await listPropertiesForAllDocumentDatabases(doc);
107
351
  const blocksFields = properties
108
352
  .filter((property) => isBlocksPropertyType(property.definition.type))
@@ -1,5 +1,6 @@
1
1
  import type { Document } from "@shared/api";
2
2
 
3
+ import type { DatabaseExportContext } from "./database/DatabaseExportDialog";
3
4
  import { DatabaseView } from "./database/DatabaseView";
4
5
 
5
6
  export * from "./database/DatabaseView";
@@ -7,9 +8,14 @@ export * from "./database/DatabaseView";
7
8
  interface DocumentDatabaseProps {
8
9
  document: Document;
9
10
  canEdit: boolean;
11
+ onExportContextChange?: (context: DatabaseExportContext | null) => void;
10
12
  }
11
13
 
12
- export function DocumentDatabase({ document, canEdit }: DocumentDatabaseProps) {
14
+ export function DocumentDatabase({
15
+ document,
16
+ canEdit,
17
+ onExportContextChange,
18
+ }: DocumentDatabaseProps) {
13
19
  const databaseId = document.database?.id;
14
20
  if (!databaseId) return null;
15
21
 
@@ -18,6 +24,7 @@ export function DocumentDatabase({ document, canEdit }: DocumentDatabaseProps) {
18
24
  databaseId={databaseId}
19
25
  databaseDocumentId={document.id}
20
26
  canEdit={canEdit}
27
+ onExportContextChange={onExportContextChange}
21
28
  />
22
29
  );
23
30
  }
@@ -98,6 +98,7 @@ import {
98
98
  import { BuilderBodySyncingNotice } from "./BuilderBodySyncingNotice";
99
99
  import type { CommentTextAnchor } from "./comment-anchors";
100
100
  import { CommentsSidebar } from "./CommentsSidebar";
101
+ import type { DatabaseExportContext } from "./database/DatabaseExportDialog";
101
102
  import { DocumentBlockFields } from "./DocumentBlockFields";
102
103
  import { DocumentDatabase } from "./DocumentDatabase";
103
104
  import { DocumentEditorSkeleton } from "./DocumentEditorSkeleton";
@@ -636,6 +637,18 @@ function DocumentEditorBody({
636
637
  const pushDocumentToNotion = usePushDocumentToNotion(documentId);
637
638
  const [localTitle, setLocalTitle] = useState("");
638
639
  const [localContent, setLocalContent] = useState("");
640
+ const [databaseExportContext, setDatabaseExportContext] =
641
+ useState<DatabaseExportContext | null>(null);
642
+ const databaseExportContextFingerprintRef = useRef("null");
643
+ const handleDatabaseExportContextChange = useCallback(
644
+ (context: DatabaseExportContext | null) => {
645
+ const fingerprint = JSON.stringify(context);
646
+ if (databaseExportContextFingerprintRef.current === fingerprint) return;
647
+ databaseExportContextFingerprintRef.current = fingerprint;
648
+ setDatabaseExportContext(context);
649
+ },
650
+ [],
651
+ );
639
652
  const [newDocumentTypeChosen, setNewDocumentTypeChosen] = useState(false);
640
653
  const [localContentUpdatedAt, setLocalContentUpdatedAt] = useState<
641
654
  string | null
@@ -2029,6 +2042,7 @@ function DocumentEditorBody({
2029
2042
  documentId={documentId}
2030
2043
  documentTitle={exportTitle}
2031
2044
  documentContent={exportContent}
2045
+ databaseExportContext={databaseExportContext}
2032
2046
  breadcrumbItems={toolbarBreadcrumbItems.map((item) =>
2033
2047
  item.id === documentId ? { ...item, title: exportTitle } : item,
2034
2048
  )}
@@ -2230,7 +2244,11 @@ function DocumentEditorBody({
2230
2244
  </div>
2231
2245
  {document.database ? (
2232
2246
  <div className={documentEditorDatabaseRegionClassName()}>
2233
- <DocumentDatabase document={document} canEdit={canEdit} />
2247
+ <DocumentDatabase
2248
+ document={document}
2249
+ canEdit={canEdit}
2250
+ onExportContextChange={handleDatabaseExportContextChange}
2251
+ />
2234
2252
  </div>
2235
2253
  ) : null}
2236
2254
 
@@ -99,6 +99,10 @@ import {
99
99
  } from "@/lib/local-content-source-files";
100
100
  import { cn } from "@/lib/utils";
101
101
 
102
+ import {
103
+ DatabaseExportDialog,
104
+ type DatabaseExportContext,
105
+ } from "./database/DatabaseExportDialog";
102
106
  import { VersionHistoryPanel } from "./VersionHistoryPanel";
103
107
 
104
108
  type ExportFormat = "pdf" | "markdown" | "html";
@@ -473,6 +477,7 @@ interface DocumentToolbarProps {
473
477
  utilityPanel: "info" | "comments" | null;
474
478
  onUtilityPanelChange: (panel: "info" | "comments" | null) => void;
475
479
  showCommentsControl?: boolean;
480
+ databaseExportContext?: DatabaseExportContext | null;
476
481
  onOpenBreadcrumbItem?: (id: string) => void;
477
482
  canUndo?: boolean;
478
483
  canRedo?: boolean;
@@ -499,6 +504,7 @@ export function DocumentToolbar({
499
504
  utilityPanel,
500
505
  onUtilityPanelChange,
501
506
  showCommentsControl = true,
507
+ databaseExportContext,
502
508
  onOpenBreadcrumbItem,
503
509
  canUndo = false,
504
510
  canRedo = false,
@@ -540,6 +546,7 @@ export function DocumentToolbar({
540
546
  boolean | null
541
547
  >(null);
542
548
  const [historyOpen, setHistoryOpen] = useState(false);
549
+ const [databaseExportOpen, setDatabaseExportOpen] = useState(false);
543
550
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
544
551
  const [searchQuery, setSearchQuery] = useState("");
545
552
  const [debouncedQuery, setDebouncedQuery] = useState("");
@@ -1076,6 +1083,15 @@ export function DocumentToolbar({
1076
1083
  {t("editor.toolbar.export")}
1077
1084
  </DropdownMenuSubTrigger>
1078
1085
  <DropdownMenuSubContent className="w-44">
1086
+ {databaseExportContext ? (
1087
+ <DropdownMenuItem
1088
+ disabled={exportDocument.isPending}
1089
+ onSelect={() => setDatabaseExportOpen(true)}
1090
+ >
1091
+ <IconDownload className="me-2 h-4 w-4" />
1092
+ CSV
1093
+ </DropdownMenuItem>
1094
+ ) : null}
1079
1095
  <DropdownMenuItem
1080
1096
  disabled={exportDocument.isPending}
1081
1097
  onSelect={() => void handleExport("pdf")}
@@ -1500,6 +1516,13 @@ export function DocumentToolbar({
1500
1516
  </AlertDialogFooter>
1501
1517
  </AlertDialogContent>
1502
1518
  </AlertDialog>
1519
+ <DatabaseExportDialog
1520
+ documentId={documentId}
1521
+ context={databaseExportContext ?? null}
1522
+ defaultScope="current_view"
1523
+ open={databaseExportOpen}
1524
+ onOpenChange={setDatabaseExportOpen}
1525
+ />
1503
1526
  </>
1504
1527
  );
1505
1528
  }
@@ -0,0 +1,277 @@
1
+ import { useActionMutation } from "@agent-native/core/client/hooks";
2
+ import { useT } from "@agent-native/core/client/i18n";
3
+ import type {
4
+ ContentDatabaseFilter,
5
+ ContentDatabaseFilterMode,
6
+ ContentDatabaseSort,
7
+ } from "@shared/api";
8
+ import { IconDownload, IconLoader2 } from "@tabler/icons-react";
9
+ import { useEffect, useMemo, useRef, useState } from "react";
10
+ import { toast } from "sonner";
11
+
12
+ import { Button } from "@/components/ui/button";
13
+ import { Checkbox } from "@/components/ui/checkbox";
14
+ import {
15
+ Dialog,
16
+ DialogContent,
17
+ DialogFooter,
18
+ DialogHeader,
19
+ DialogTitle,
20
+ } from "@/components/ui/dialog";
21
+ import { cn } from "@/lib/utils";
22
+
23
+ export type DatabaseExportScopeKind = "all_members" | "current_view";
24
+
25
+ export interface DatabaseExportProperty {
26
+ id: string;
27
+ name: string;
28
+ type: string;
29
+ visible: boolean;
30
+ }
31
+
32
+ export interface DatabaseExportContext {
33
+ viewId: string;
34
+ viewName: string;
35
+ query: {
36
+ search: string;
37
+ filters: ContentDatabaseFilter[];
38
+ sorts: ContentDatabaseSort[];
39
+ filterMode: ContentDatabaseFilterMode;
40
+ };
41
+ properties: DatabaseExportProperty[];
42
+ }
43
+
44
+ interface DatabaseCsvResult {
45
+ filename: string;
46
+ mimeType: string;
47
+ content: string;
48
+ }
49
+
50
+ export function defaultDatabaseCsvPropertyIds(
51
+ properties: DatabaseExportProperty[],
52
+ ) {
53
+ return properties
54
+ .filter((property) => property.visible && property.type !== "blocks")
55
+ .map((property) => property.id);
56
+ }
57
+
58
+ export function shouldInitializeDatabaseExportDialog(
59
+ wasOpen: boolean,
60
+ open: boolean,
61
+ ) {
62
+ return open && !wasOpen;
63
+ }
64
+
65
+ export function databaseCsvRequest(args: {
66
+ id: string;
67
+ context: DatabaseExportContext;
68
+ scope: DatabaseExportScopeKind;
69
+ propertyIds: string[];
70
+ }) {
71
+ return {
72
+ id: args.id,
73
+ format: "csv" as const,
74
+ collection: {
75
+ scope:
76
+ args.scope === "current_view"
77
+ ? {
78
+ kind: "current_view" as const,
79
+ viewId: args.context.viewId,
80
+ query: args.context.query,
81
+ }
82
+ : { kind: "all_members" as const },
83
+ propertyIds: args.propertyIds,
84
+ },
85
+ };
86
+ }
87
+
88
+ function downloadCsv(result: DatabaseCsvResult) {
89
+ const blob = new Blob([result.content], { type: result.mimeType });
90
+ const url = URL.createObjectURL(blob);
91
+ const link = window.document.createElement("a");
92
+ link.href = url;
93
+ link.download = result.filename;
94
+ window.document.body.appendChild(link);
95
+ link.click();
96
+ link.remove();
97
+ URL.revokeObjectURL(url);
98
+ }
99
+
100
+ export function DatabaseExportDialog({
101
+ documentId,
102
+ context,
103
+ defaultScope,
104
+ open,
105
+ onOpenChange,
106
+ }: {
107
+ documentId: string;
108
+ context: DatabaseExportContext | null;
109
+ defaultScope: DatabaseExportScopeKind;
110
+ open: boolean;
111
+ onOpenChange: (open: boolean) => void;
112
+ }) {
113
+ const t = useT();
114
+ const exportDocument = useActionMutation("export-document");
115
+ const [scope, setScope] = useState<DatabaseExportScopeKind>(defaultScope);
116
+ const [propertyIds, setPropertyIds] = useState<string[]>([]);
117
+ const wasOpenRef = useRef(false);
118
+ const defaultPropertyIds = useMemo(
119
+ () => defaultDatabaseCsvPropertyIds(context?.properties ?? []),
120
+ [context],
121
+ );
122
+
123
+ useEffect(() => {
124
+ if (shouldInitializeDatabaseExportDialog(wasOpenRef.current, open)) {
125
+ setScope(defaultScope);
126
+ setPropertyIds(defaultPropertyIds);
127
+ }
128
+ wasOpenRef.current = open;
129
+ }, [defaultPropertyIds, defaultScope, open]);
130
+
131
+ const toggleProperty = (id: string, checked: boolean) => {
132
+ setPropertyIds((current) =>
133
+ checked
134
+ ? [...new Set([...current, id])]
135
+ : current.filter((x) => x !== id),
136
+ );
137
+ };
138
+
139
+ const handleExport = async () => {
140
+ if (!context || exportDocument.isPending) return;
141
+ try {
142
+ const result = (await exportDocument.mutateAsync(
143
+ databaseCsvRequest({ id: documentId, context, scope, propertyIds }),
144
+ )) as DatabaseCsvResult;
145
+ downloadCsv(result);
146
+ toast.success(t("editor.toolbar.exportedCsv"));
147
+ onOpenChange(false);
148
+ } catch (error) {
149
+ toast.error(t("editor.toolbar.exportFailed"), {
150
+ description:
151
+ error instanceof Error ? error.message : t("empty.genericError"),
152
+ });
153
+ }
154
+ };
155
+
156
+ if (!context) return null;
157
+
158
+ return (
159
+ <Dialog open={open} onOpenChange={onOpenChange}>
160
+ <DialogContent className="gap-5 sm:max-w-md" data-database-export-dialog>
161
+ <DialogHeader>
162
+ <DialogTitle>{t("editor.toolbar.exportDatabase")}</DialogTitle>
163
+ </DialogHeader>
164
+ <div className="space-y-4">
165
+ <div
166
+ className="grid grid-cols-2 gap-2"
167
+ role="radiogroup"
168
+ aria-label={t("editor.toolbar.exportScope")}
169
+ >
170
+ <button
171
+ type="button"
172
+ role="radio"
173
+ aria-checked={scope === "current_view"}
174
+ onClick={() => setScope("current_view")}
175
+ className={cn(
176
+ "rounded-md border px-3 py-2 text-start text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
177
+ scope === "current_view"
178
+ ? "border-foreground bg-muted"
179
+ : "border-border hover:bg-muted/60",
180
+ )}
181
+ >
182
+ <span className="block font-medium">
183
+ {t("editor.toolbar.currentView")}
184
+ </span>
185
+ <span className="block truncate text-xs text-muted-foreground">
186
+ {context.viewName}
187
+ </span>
188
+ </button>
189
+ <button
190
+ type="button"
191
+ role="radio"
192
+ aria-checked={scope === "all_members"}
193
+ onClick={() => setScope("all_members")}
194
+ className={cn(
195
+ "rounded-md border px-3 py-2 text-start text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
196
+ scope === "all_members"
197
+ ? "border-foreground bg-muted"
198
+ : "border-border hover:bg-muted/60",
199
+ )}
200
+ >
201
+ <span className="block font-medium">
202
+ {t("editor.toolbar.allDatabaseMembers")}
203
+ </span>
204
+ <span className="block text-xs text-muted-foreground">
205
+ {t("editor.toolbar.allDatabaseMembersDetail")}
206
+ </span>
207
+ </button>
208
+ </div>
209
+ <div>
210
+ <div className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
211
+ {t("editor.toolbar.exportColumns")}
212
+ </div>
213
+ <div className="max-h-56 space-y-1 overflow-y-auto pe-1">
214
+ <label className="flex items-center gap-3 rounded-md px-2 py-1.5 text-sm">
215
+ <Checkbox
216
+ checked
217
+ disabled
218
+ aria-label={t("editor.toolbar.titleColumn")}
219
+ />
220
+ <span>{t("editor.toolbar.titleColumn")}</span>
221
+ </label>
222
+ {context.properties.map((property) => {
223
+ const checked = propertyIds.includes(property.id);
224
+ const isBlocks = property.type === "blocks";
225
+ return (
226
+ <label
227
+ key={property.id}
228
+ className="flex cursor-pointer items-center gap-3 rounded-md px-2 py-1.5 text-sm hover:bg-muted/60"
229
+ >
230
+ <Checkbox
231
+ checked={checked}
232
+ onCheckedChange={(value) =>
233
+ toggleProperty(property.id, value === true)
234
+ }
235
+ />
236
+ <span className="min-w-0 flex-1 truncate">
237
+ {property.name}
238
+ </span>
239
+ {isBlocks ? (
240
+ <span className="text-xs text-muted-foreground">
241
+ {t("editor.toolbar.blocksColumn")}
242
+ </span>
243
+ ) : null}
244
+ </label>
245
+ );
246
+ })}
247
+ </div>
248
+ </div>
249
+ </div>
250
+ <DialogFooter>
251
+ <Button
252
+ type="button"
253
+ variant="outline"
254
+ onClick={() => onOpenChange(false)}
255
+ disabled={exportDocument.isPending}
256
+ >
257
+ {t("comments.cancel")}
258
+ </Button>
259
+ <Button
260
+ type="button"
261
+ onClick={() => void handleExport()}
262
+ disabled={exportDocument.isPending}
263
+ >
264
+ {exportDocument.isPending ? (
265
+ <IconLoader2 className="me-2 size-4 animate-spin" />
266
+ ) : (
267
+ <IconDownload className="me-2 size-4" />
268
+ )}
269
+ {exportDocument.isPending
270
+ ? t("editor.toolbar.exporting")
271
+ : t("editor.toolbar.exportCsv")}
272
+ </Button>
273
+ </DialogFooter>
274
+ </DialogContent>
275
+ </Dialog>
276
+ );
277
+ }
@@ -275,6 +275,7 @@ import {
275
275
  releasePreviewDocumentSaveController,
276
276
  } from "../previewDocumentSaveRegistry";
277
277
  import { VisualEditor } from "../VisualEditor";
278
+ import type { DatabaseExportContext } from "./DatabaseExportDialog";
278
279
  import { DatabaseFormView } from "./FormView";
279
280
  import { DatabaseGalleryView } from "./GalleryView";
280
281
  import { DatabaseListView } from "./ListView";
@@ -293,6 +294,7 @@ export interface DatabaseViewProps {
293
294
  renderMode?: "page" | "inline";
294
295
  canEdit?: boolean;
295
296
  isActive?: boolean;
297
+ onExportContextChange?: (context: DatabaseExportContext | null) => void;
296
298
  }
297
299
 
298
300
  const CONTENT_DATABASE_PAGE_SIZE = 100;
@@ -740,6 +742,7 @@ export function DatabaseView({
740
742
  renderMode = "page",
741
743
  canEdit = true,
742
744
  isActive,
745
+ onExportContextChange,
743
746
  }: DatabaseViewProps) {
744
747
  const { data: document } = useDocument(databaseDocumentId);
745
748
 
@@ -755,6 +758,7 @@ export function DatabaseView({
755
758
  renderMode={renderMode}
756
759
  canEdit={effectiveCanEdit}
757
760
  isActive={isActive ?? renderMode === "page"}
761
+ onExportContextChange={onExportContextChange}
758
762
  />
759
763
  );
760
764
  }
@@ -767,6 +771,7 @@ function DatabaseTable({
767
771
  renderMode,
768
772
  canEdit,
769
773
  isActive,
774
+ onExportContextChange,
770
775
  }: {
771
776
  document: Document;
772
777
  databaseId: string;
@@ -775,6 +780,7 @@ function DatabaseTable({
775
780
  renderMode: "page" | "inline";
776
781
  canEdit: boolean;
777
782
  isActive: boolean;
783
+ onExportContextChange?: (context: DatabaseExportContext | null) => void;
778
784
  }) {
779
785
  const t = useT();
780
786
  const navigate = useNavigate();
@@ -1061,6 +1067,45 @@ function DatabaseTable({
1061
1067
  ),
1062
1068
  [orderedProperties, items, activeView],
1063
1069
  );
1070
+ const exportContext = useMemo<DatabaseExportContext | null>(
1071
+ () =>
1072
+ data
1073
+ ? {
1074
+ viewId: activeView.id,
1075
+ viewName: activeView.name,
1076
+ query: { search: searchQuery, filters, sorts, filterMode },
1077
+ properties: orderedProperties.map((property) => ({
1078
+ id: property.definition.id,
1079
+ name: property.definition.name,
1080
+ type: property.definition.type,
1081
+ visible: isDatabasePropertyVisibleInView(
1082
+ property,
1083
+ items,
1084
+ activeView,
1085
+ ),
1086
+ })),
1087
+ }
1088
+ : null,
1089
+ [
1090
+ activeView,
1091
+ data,
1092
+ filterMode,
1093
+ filters,
1094
+ items,
1095
+ orderedProperties,
1096
+ searchQuery,
1097
+ sorts,
1098
+ ],
1099
+ );
1100
+ useEffect(() => {
1101
+ // Inline blocks share this component but cannot replace the page snapshot.
1102
+ if (renderMode === "page") onExportContextChange?.(exportContext);
1103
+ }, [exportContext, onExportContextChange, renderMode]);
1104
+ useEffect(() => {
1105
+ return () => {
1106
+ if (renderMode === "page") onExportContextChange?.(null);
1107
+ };
1108
+ }, [onExportContextChange, renderMode]);
1064
1109
  const visibleItems = useMemo(
1065
1110
  () =>
1066
1111
  applyDatabaseView(
@@ -2830,6 +2830,17 @@ const editorToolbarMessages = {
2830
2830
  "Clipboard access is not available in this browser.",
2831
2831
  edited: "Edited",
2832
2832
  export: "Export",
2833
+ exportCsv: "Export CSV",
2834
+ exportDatabase: "Export database",
2835
+ exportScope: "Export scope",
2836
+ currentView: "Current view",
2837
+ allDatabaseMembers: "All database members",
2838
+ allDatabaseMembersDetail: "Every page in this database",
2839
+ exportColumns: "Columns",
2840
+ titleColumn: "Title",
2841
+ blocksColumn: "Blocks",
2842
+ exporting: "Exporting...",
2843
+ exportedCsv: "Exported CSV",
2833
2844
  exportFailed: "Export failed",
2834
2845
  exportedHtml: "Exported HTML",
2835
2846
  exportedMarkdown: "Exported Markdown",
@@ -11475,6 +11486,17 @@ const contentExactEnglishTranslations = {
11475
11486
  toolbar: {
11476
11487
  info: "資訊",
11477
11488
  closeUtilityPanel: "關閉面板",
11489
+ exportCsv: "匯出 CSV",
11490
+ exportDatabase: "匯出資料庫",
11491
+ exportScope: "匯出範圍",
11492
+ currentView: "目前檢視",
11493
+ allDatabaseMembers: "所有資料庫項目",
11494
+ allDatabaseMembersDetail: "此資料庫中的每個頁面",
11495
+ exportColumns: "匯出欄位",
11496
+ titleColumn: "標題",
11497
+ blocksColumn: "區塊",
11498
+ exporting: "正在匯出...",
11499
+ exportedCsv: "已匯出 CSV",
11478
11500
  },
11479
11501
  },
11480
11502
  comments: {
@@ -11514,6 +11536,17 @@ const contentExactEnglishTranslations = {
11514
11536
  toolbar: {
11515
11537
  info: "信息",
11516
11538
  closeUtilityPanel: "关闭面板",
11539
+ exportCsv: "导出 CSV",
11540
+ exportDatabase: "导出数据库",
11541
+ exportScope: "导出范围",
11542
+ currentView: "当前视图",
11543
+ allDatabaseMembers: "所有数据库项目",
11544
+ allDatabaseMembersDetail: "此数据库中的每个页面",
11545
+ exportColumns: "导出列",
11546
+ titleColumn: "标题",
11547
+ blocksColumn: "内容块",
11548
+ exporting: "正在导出...",
11549
+ exportedCsv: "已导出 CSV",
11517
11550
  copiedPageLink: "已复制页面链接",
11518
11551
  copyPageLink: "复制页面链接",
11519
11552
  couldNotCopyLink: "无法复制链接",
@@ -11561,6 +11594,17 @@ const contentExactEnglishTranslations = {
11561
11594
  toolbar: {
11562
11595
  info: "Información",
11563
11596
  closeUtilityPanel: "Cerrar panel",
11597
+ exportCsv: "Exportar CSV",
11598
+ exportDatabase: "Exportar base de datos",
11599
+ exportScope: "Ámbito de exportación",
11600
+ currentView: "Vista actual",
11601
+ allDatabaseMembers: "Todos los elementos de la base de datos",
11602
+ allDatabaseMembersDetail: "Todas las páginas de esta base de datos",
11603
+ exportColumns: "Columnas para exportar",
11604
+ titleColumn: "Título",
11605
+ blocksColumn: "Bloques",
11606
+ exporting: "Exportando...",
11607
+ exportedCsv: "CSV exportado",
11564
11608
  copiedPageLink: "Enlace de página copiado",
11565
11609
  copyPageLink: "Copiar enlace de página",
11566
11610
  couldNotCopyLink: "No se pudo copiar el enlace",
@@ -11610,6 +11654,17 @@ const contentExactEnglishTranslations = {
11610
11654
  toolbar: {
11611
11655
  info: "Informations",
11612
11656
  closeUtilityPanel: "Fermer le panneau",
11657
+ exportCsv: "Exporter en CSV",
11658
+ exportDatabase: "Exporter la base de données",
11659
+ exportScope: "Périmètre d’exportation",
11660
+ currentView: "Vue actuelle",
11661
+ allDatabaseMembers: "Tous les éléments de la base de données",
11662
+ allDatabaseMembersDetail: "Toutes les pages de cette base de données",
11663
+ exportColumns: "Colonnes à exporter",
11664
+ titleColumn: "Titre",
11665
+ blocksColumn: "Blocs",
11666
+ exporting: "Exportation...",
11667
+ exportedCsv: "CSV exporté",
11613
11668
  copiedPageLink: "Lien de la page copié",
11614
11669
  copyPageLink: "Copier le lien de la page",
11615
11670
  couldNotCopyLink: "Impossible de copier le lien",
@@ -11660,6 +11715,17 @@ const contentExactEnglishTranslations = {
11660
11715
  toolbar: {
11661
11716
  info: "Informationen",
11662
11717
  closeUtilityPanel: "Bereich schließen",
11718
+ exportCsv: "Als CSV exportieren",
11719
+ exportDatabase: "Datenbank exportieren",
11720
+ exportScope: "Exportumfang",
11721
+ currentView: "Aktuelle Ansicht",
11722
+ allDatabaseMembers: "Alle Datenbankeinträge",
11723
+ allDatabaseMembersDetail: "Alle Seiten in dieser Datenbank",
11724
+ exportColumns: "Zu exportierende Spalten",
11725
+ titleColumn: "Titel",
11726
+ blocksColumn: "Blöcke",
11727
+ exporting: "Wird exportiert...",
11728
+ exportedCsv: "CSV exportiert",
11663
11729
  copiedPageLink: "Seitenlink kopiert",
11664
11730
  copyPageLink: "Seitenlink kopieren",
11665
11731
  couldNotCopyLink: "Link konnte nicht kopiert werden",
@@ -11709,6 +11775,17 @@ const contentExactEnglishTranslations = {
11709
11775
  toolbar: {
11710
11776
  info: "情報",
11711
11777
  closeUtilityPanel: "パネルを閉じる",
11778
+ exportCsv: "CSV をエクスポート",
11779
+ exportDatabase: "データベースをエクスポート",
11780
+ exportScope: "エクスポート範囲",
11781
+ currentView: "現在のビュー",
11782
+ allDatabaseMembers: "データベースの全項目",
11783
+ allDatabaseMembersDetail: "このデータベース内のすべてのページ",
11784
+ exportColumns: "エクスポートする列",
11785
+ titleColumn: "タイトル",
11786
+ blocksColumn: "ブロック",
11787
+ exporting: "エクスポート中...",
11788
+ exportedCsv: "CSV をエクスポートしました",
11712
11789
  copiedPageLink: "ページリンクをコピーしました",
11713
11790
  copyPageLink: "ページリンクをコピー",
11714
11791
  couldNotCopyLink: "リンクをコピーできませんでした",
@@ -11757,6 +11834,17 @@ const contentExactEnglishTranslations = {
11757
11834
  toolbar: {
11758
11835
  info: "정보",
11759
11836
  closeUtilityPanel: "패널 닫기",
11837
+ exportCsv: "CSV 내보내기",
11838
+ exportDatabase: "데이터베이스 내보내기",
11839
+ exportScope: "내보내기 범위",
11840
+ currentView: "현재 보기",
11841
+ allDatabaseMembers: "모든 데이터베이스 항목",
11842
+ allDatabaseMembersDetail: "이 데이터베이스의 모든 페이지",
11843
+ exportColumns: "내보낼 열",
11844
+ titleColumn: "제목",
11845
+ blocksColumn: "블록",
11846
+ exporting: "내보내는 중...",
11847
+ exportedCsv: "CSV를 내보냈습니다",
11760
11848
  copiedPageLink: "페이지 링크를 복사했습니다",
11761
11849
  copyPageLink: "페이지 링크 복사",
11762
11850
  couldNotCopyLink: "링크를 복사하지 못했습니다",
@@ -11806,6 +11894,17 @@ const contentExactEnglishTranslations = {
11806
11894
  toolbar: {
11807
11895
  info: "Informações",
11808
11896
  closeUtilityPanel: "Fechar painel",
11897
+ exportCsv: "Exportar CSV",
11898
+ exportDatabase: "Exportar banco de dados",
11899
+ exportScope: "Escopo da exportação",
11900
+ currentView: "Visualização atual",
11901
+ allDatabaseMembers: "Todos os itens do banco de dados",
11902
+ allDatabaseMembersDetail: "Todas as páginas deste banco de dados",
11903
+ exportColumns: "Colunas para exportar",
11904
+ titleColumn: "Título",
11905
+ blocksColumn: "Blocos",
11906
+ exporting: "Exportando...",
11907
+ exportedCsv: "CSV exportado",
11809
11908
  copiedPageLink: "Link da página copiado",
11810
11909
  copyPageLink: "Copiar link da página",
11811
11910
  couldNotCopyLink: "Não foi possível copiar o link",
@@ -11854,6 +11953,17 @@ const contentExactEnglishTranslations = {
11854
11953
  toolbar: {
11855
11954
  info: "जानकारी",
11856
11955
  closeUtilityPanel: "पैनल बंद करें",
11956
+ exportCsv: "CSV निर्यात करें",
11957
+ exportDatabase: "डेटाबेस निर्यात करें",
11958
+ exportScope: "निर्यात का दायरा",
11959
+ currentView: "वर्तमान दृश्य",
11960
+ allDatabaseMembers: "डेटाबेस के सभी आइटम",
11961
+ allDatabaseMembersDetail: "इस डेटाबेस के सभी पेज",
11962
+ exportColumns: "निर्यात किए जाने वाले कॉलम",
11963
+ titleColumn: "शीर्षक",
11964
+ blocksColumn: "ब्लॉक",
11965
+ exporting: "निर्यात हो रहा है...",
11966
+ exportedCsv: "CSV निर्यात किया गया",
11857
11967
  copiedPageLink: "पेज लिंक कॉपी किया गया",
11858
11968
  copyPageLink: "पेज लिंक कॉपी करें",
11859
11969
  couldNotCopyLink: "लिंक कॉपी नहीं किया जा सका",
@@ -11900,6 +12010,17 @@ const contentExactEnglishTranslations = {
11900
12010
  toolbar: {
11901
12011
  info: "معلومات",
11902
12012
  closeUtilityPanel: "إغلاق اللوحة",
12013
+ exportCsv: "تصدير CSV",
12014
+ exportDatabase: "تصدير قاعدة البيانات",
12015
+ exportScope: "نطاق التصدير",
12016
+ currentView: "العرض الحالي",
12017
+ allDatabaseMembers: "كل عناصر قاعدة البيانات",
12018
+ allDatabaseMembersDetail: "كل الصفحات في قاعدة البيانات هذه",
12019
+ exportColumns: "الأعمدة المراد تصديرها",
12020
+ titleColumn: "العنوان",
12021
+ blocksColumn: "الكتل",
12022
+ exporting: "جارٍ التصدير...",
12023
+ exportedCsv: "تم تصدير CSV",
11903
12024
  copiedPageLink: "تم نسخ رابط الصفحة",
11904
12025
  copyPageLink: "نسخ رابط الصفحة",
11905
12026
  couldNotCopyLink: "تعذر نسخ الرابط",
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-08-18
4
+ ---
5
+
6
+ Database exports can be configured and downloaded as CSV
@@ -0,0 +1,59 @@
1
+ import type { DocumentProperty } from "./api.js";
2
+ import { formulaValueText, type DocumentPropertyValue } from "./properties.js";
3
+
4
+ export interface DatabaseCsvColumn {
5
+ id: string;
6
+ name: string;
7
+ property: {
8
+ definition: Pick<DocumentProperty["definition"], "type" | "options">;
9
+ };
10
+ }
11
+
12
+ function propertyValueText(
13
+ property: DatabaseCsvColumn["property"],
14
+ value: DocumentPropertyValue | undefined,
15
+ ): string {
16
+ if (value == null) return "";
17
+ const optionName = (entry: string) =>
18
+ property.definition.options.options?.find((option) => option.id === entry)
19
+ ?.name ?? entry;
20
+ if (Array.isArray(value)) return value.map(optionName).join(", ");
21
+ if (
22
+ property.definition.type === "select" ||
23
+ property.definition.type === "status"
24
+ ) {
25
+ return optionName(String(value));
26
+ }
27
+ if (property.definition.type === "checkbox") {
28
+ return value ? "TRUE" : "FALSE";
29
+ }
30
+ return formulaValueText(value);
31
+ }
32
+
33
+ export interface DatabaseCsvRow {
34
+ title: string | null | undefined;
35
+ values: ReadonlyMap<string, DocumentPropertyValue>;
36
+ }
37
+
38
+ function csvCell(value: string): string {
39
+ const safe = /^[\t\r\n ]*[=+\-@]/.test(value) ? `'${value}` : value;
40
+ return /[",\r\n]/.test(safe) ? `"${safe.replace(/"/g, '""')}"` : safe;
41
+ }
42
+
43
+ /** Render an already-authorized, ordered database projection as RFC 4180 CSV. */
44
+ export function renderDatabaseCsv(
45
+ columns: readonly DatabaseCsvColumn[],
46
+ rows: readonly DatabaseCsvRow[],
47
+ ): string {
48
+ const header = ["Title", ...columns.map((column) => column.name)];
49
+ const data = rows.map((row) => [
50
+ row.title ?? "",
51
+ ...columns.map((column) => {
52
+ return propertyValueText(column.property, row.values.get(column.id));
53
+ }),
54
+ ]);
55
+ return [...[header], ...data]
56
+ .map((row) => row.map(csvCell).join(","))
57
+ .join("\r\n")
58
+ .concat("\r\n");
59
+ }
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
26
26
  * Body: { update: string (base64), requestSource?: string }
27
27
  */
28
28
  export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
29
- error: string;
30
29
  ok?: undefined;
30
+ error: string;
31
31
  } | {
32
32
  error?: undefined;
33
33
  ok: boolean;
@@ -13,8 +13,8 @@
13
13
  * Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
14
14
  */
15
15
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
16
- ok?: undefined;
17
16
  error: string;
17
+ ok?: undefined;
18
18
  } | {
19
19
  error?: undefined;
20
20
  ok: boolean;
@@ -62,6 +62,6 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
62
62
  summary?: undefined;
63
63
  spans?: undefined;
64
64
  id?: undefined;
65
- error?: undefined;
66
65
  ok: boolean;
66
+ error?: undefined;
67
67
  }>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.173.1-nightly-20260825150004",
3
+ "version": "0.173.1",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -429,8 +429,8 @@
429
429
  "y-protocols": "^1.0.7",
430
430
  "yjs": "^13.6.32",
431
431
  "zod": "^4.3.6",
432
- "@agent-native/recap-cli": "0.5.10-nightly-20260825150004",
433
- "@agent-native/toolkit": "^0.16.12"
432
+ "@agent-native/recap-cli": "0.5.10",
433
+ "@agent-native/toolkit": "^0.16.13"
434
434
  },
435
435
  "devDependencies": {
436
436
  "@ai-sdk/anthropic": "^3.0.71",