@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 +1 -1
- package/corpus/templates/content/actions/export-document.ts +251 -7
- package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +8 -1
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +19 -1
- package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +23 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseExportDialog.tsx +277 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +45 -0
- package/corpus/templates/content/app/i18n-data.ts +121 -0
- package/corpus/templates/content/changelog/2026-08-18-database-exports-can-be-configured-and-downloaded-as-csv.md +6 -0
- package/corpus/templates/content/shared/database-csv-export.ts +59 -0
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +1 -1
- package/package.json +3 -3
package/corpus/README.md
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core/action";
|
|
2
2
|
import { buildDeepLink } from "@agent-native/core/server";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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 {
|
|
20
|
-
import {
|
|
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
|
|
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({
|
|
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
|
|
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,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
|
+
}
|
package/dist/collab/routes.d.ts
CHANGED
|
@@ -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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.173.1
|
|
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
|
|
433
|
-
"@agent-native/toolkit": "^0.16.
|
|
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",
|