@agent-native/core 0.176.5-nightly-20260903175350 → 0.176.5

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.
Files changed (29) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/calendar/CHANGELOG.md +18 -0
  3. package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +13 -7
  4. package/corpus/templates/content/CHANGELOG.md +1 -0
  5. package/corpus/templates/content/actions/_collection-export.ts +925 -0
  6. package/corpus/templates/content/actions/export-document.ts +79 -255
  7. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +39 -38
  8. package/corpus/templates/content/app/components/editor/database/DatabaseExportDialog.tsx +444 -113
  9. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +4 -0
  10. package/corpus/templates/content/app/i18n-data.ts +99 -0
  11. package/corpus/templates/content/docs/product/capabilities/content.portability.collection-export.md +139 -0
  12. package/corpus/templates/content/docs/product/encyclopedia.md +12 -8
  13. package/corpus/templates/content/docs/product/features/content.feature.take-the-whole-vault-with-you.md +5 -1
  14. package/corpus/templates/content/docs/product/roadmap.md +1 -1
  15. package/corpus/templates/content/shared/database-collection-export.ts +161 -0
  16. package/corpus/templates/content/shared/database-csv-export.ts +6 -3
  17. package/corpus/templates/design/CHANGELOG.md +1 -0
  18. package/corpus/templates/factory/CHANGELOG.md +20 -0
  19. package/corpus/templates/mail/CHANGELOG.md +12 -0
  20. package/corpus/templates/plan/CHANGELOG.md +6 -0
  21. package/corpus/templates/slides/CHANGELOG.md +9 -0
  22. package/dist/collab/awareness.d.ts +2 -2
  23. package/dist/collab/struct-routes.d.ts +1 -1
  24. package/dist/notifications/routes.d.ts +1 -1
  25. package/dist/observability/routes.d.ts +5 -5
  26. package/dist/progress/routes.d.ts +1 -1
  27. package/dist/secrets/routes.d.ts +9 -9
  28. package/dist/server/realtime-token.d.ts +1 -1
  29. package/package.json +3 -3
@@ -1,4 +1,7 @@
1
- import { useActionMutation } from "@agent-native/core/client/hooks";
1
+ import {
2
+ actionErrorMessage,
3
+ useActionMutation,
4
+ } from "@agent-native/core/client/hooks";
2
5
  import { useT } from "@agent-native/core/client/i18n";
3
6
  import type {
4
7
  ContentDatabaseFilter,
@@ -7,7 +10,6 @@ import type {
7
10
  } from "@shared/api";
8
11
  import { IconDownload, IconLoader2 } from "@tabler/icons-react";
9
12
  import { useEffect, useMemo, useRef, useState } from "react";
10
- import { toast } from "sonner";
11
13
 
12
14
  import { Button } from "@/components/ui/button";
13
15
  import { Checkbox } from "@/components/ui/checkbox";
@@ -20,6 +22,7 @@ import {
20
22
  } from "@/components/ui/dialog";
21
23
  import { cn } from "@/lib/utils";
22
24
 
25
+ export type DatabaseExportFormat = "csv" | "markdown" | "html" | "pdf";
23
26
  export type DatabaseExportScopeKind = "all_members" | "current_view";
24
27
 
25
28
  export interface DatabaseExportProperty {
@@ -27,6 +30,7 @@ export interface DatabaseExportProperty {
27
30
  name: string;
28
31
  type: string;
29
32
  visible: boolean;
33
+ primaryBody?: boolean;
30
34
  }
31
35
 
32
36
  export interface DatabaseExportContext {
@@ -41,13 +45,39 @@ export interface DatabaseExportContext {
41
45
  properties: DatabaseExportProperty[];
42
46
  }
43
47
 
44
- interface DatabaseCsvResult {
48
+ export interface DatabaseExportSelection {
49
+ propertyIds: string[];
50
+ includePrimaryBody: boolean;
51
+ blockPropertyIds: string[];
52
+ }
53
+
54
+ type DatabaseExportSelections = Record<
55
+ DatabaseExportFormat,
56
+ DatabaseExportSelection
57
+ >;
58
+
59
+ export interface CollectionExportArchiveFile {
60
+ path: string;
61
+ content: string;
62
+ }
63
+
64
+ export interface CollectionExportResult {
45
65
  filename: string;
46
66
  mimeType: string;
47
67
  content: string;
68
+ format: DatabaseExportFormat;
69
+ print: boolean;
70
+ archiveFiles?: CollectionExportArchiveFile[];
48
71
  }
49
72
 
50
- export function defaultDatabaseCsvPropertyIds(
73
+ const EXPORT_FORMATS: DatabaseExportFormat[] = [
74
+ "csv",
75
+ "markdown",
76
+ "html",
77
+ "pdf",
78
+ ];
79
+
80
+ export function defaultDatabaseExportPropertyIds(
51
81
  properties: DatabaseExportProperty[],
52
82
  ) {
53
83
  return properties
@@ -55,6 +85,31 @@ export function defaultDatabaseCsvPropertyIds(
55
85
  .map((property) => property.id);
56
86
  }
57
87
 
88
+ export function additionalDatabaseExportBlockProperties(
89
+ properties: DatabaseExportProperty[],
90
+ ) {
91
+ return properties.filter(
92
+ (property) => property.type === "blocks" && !property.primaryBody,
93
+ );
94
+ }
95
+
96
+ export function defaultDatabaseExportSelections(
97
+ properties: DatabaseExportProperty[],
98
+ ): DatabaseExportSelections {
99
+ const propertyIds = defaultDatabaseExportPropertyIds(properties);
100
+ const selection = (includePrimaryBody: boolean) => ({
101
+ propertyIds: [...propertyIds],
102
+ includePrimaryBody,
103
+ blockPropertyIds: [],
104
+ });
105
+ return {
106
+ csv: selection(false),
107
+ markdown: selection(true),
108
+ html: selection(true),
109
+ pdf: selection(true),
110
+ };
111
+ }
112
+
58
113
  export function shouldInitializeDatabaseExportDialog(
59
114
  wasOpen: boolean,
60
115
  open: boolean,
@@ -62,15 +117,27 @@ export function shouldInitializeDatabaseExportDialog(
62
117
  return open && !wasOpen;
63
118
  }
64
119
 
65
- export function databaseCsvRequest(args: {
120
+ export function updateDatabaseExportSelection(
121
+ selections: DatabaseExportSelections,
122
+ format: DatabaseExportFormat,
123
+ patch: Partial<DatabaseExportSelection>,
124
+ ) {
125
+ return {
126
+ ...selections,
127
+ [format]: { ...selections[format], ...patch },
128
+ };
129
+ }
130
+
131
+ export function databaseExportRequest(args: {
66
132
  id: string;
133
+ format: DatabaseExportFormat;
67
134
  context: DatabaseExportContext;
68
135
  scope: DatabaseExportScopeKind;
69
- propertyIds: string[];
136
+ selection: DatabaseExportSelection;
70
137
  }) {
71
138
  return {
72
139
  id: args.id,
73
- format: "csv" as const,
140
+ format: args.format,
74
141
  collection: {
75
142
  scope:
76
143
  args.scope === "current_view"
@@ -80,137 +147,363 @@ export function databaseCsvRequest(args: {
80
147
  query: args.context.query,
81
148
  }
82
149
  : { kind: "all_members" as const },
83
- propertyIds: args.propertyIds,
150
+ propertyIds: args.selection.propertyIds,
151
+ includePrimaryBody: args.selection.includePrimaryBody,
152
+ blockPropertyIds: args.selection.blockPropertyIds,
84
153
  },
85
154
  };
86
155
  }
87
156
 
88
- function downloadCsv(result: DatabaseCsvResult) {
89
- const blob = new Blob([result.content], { type: result.mimeType });
157
+ function crc32(bytes: Uint8Array) {
158
+ let crc = 0xffffffff;
159
+ for (const byte of bytes) {
160
+ crc ^= byte;
161
+ for (let bit = 0; bit < 8; bit += 1) {
162
+ crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
163
+ }
164
+ }
165
+ return (crc ^ 0xffffffff) >>> 0;
166
+ }
167
+
168
+ export function buildStoreOnlyZip(files: CollectionExportArchiveFile[]) {
169
+ const encoder = new TextEncoder();
170
+ const localParts: Uint8Array[] = [];
171
+ const centralParts: Uint8Array[] = [];
172
+ let offset = 0;
173
+
174
+ for (const file of files) {
175
+ const name = encoder.encode(file.path);
176
+ const content = encoder.encode(file.content);
177
+ const checksum = crc32(content);
178
+ const local = new Uint8Array(30);
179
+ const localView = new DataView(local.buffer);
180
+ localView.setUint32(0, 0x04034b50, true);
181
+ localView.setUint16(4, 20, true);
182
+ localView.setUint16(6, 0x0800, true);
183
+ localView.setUint16(12, 0x0021, true);
184
+ localView.setUint32(14, checksum, true);
185
+ localView.setUint32(18, content.length, true);
186
+ localView.setUint32(22, content.length, true);
187
+ localView.setUint16(26, name.length, true);
188
+ localParts.push(local, name, content);
189
+
190
+ const central = new Uint8Array(46);
191
+ const centralView = new DataView(central.buffer);
192
+ centralView.setUint32(0, 0x02014b50, true);
193
+ centralView.setUint16(4, 20, true);
194
+ centralView.setUint16(6, 20, true);
195
+ centralView.setUint16(8, 0x0800, true);
196
+ centralView.setUint16(14, 0x0021, true);
197
+ centralView.setUint32(16, checksum, true);
198
+ centralView.setUint32(20, content.length, true);
199
+ centralView.setUint32(24, content.length, true);
200
+ centralView.setUint16(28, name.length, true);
201
+ centralView.setUint32(42, offset, true);
202
+ centralParts.push(central, name);
203
+ offset += local.length + name.length + content.length;
204
+ }
205
+
206
+ const centralOffset = offset;
207
+ const centralSize = centralParts.reduce((sum, part) => sum + part.length, 0);
208
+ const end = new Uint8Array(22);
209
+ const endView = new DataView(end.buffer);
210
+ endView.setUint32(0, 0x06054b50, true);
211
+ endView.setUint16(8, files.length, true);
212
+ endView.setUint16(10, files.length, true);
213
+ endView.setUint32(12, centralSize, true);
214
+ endView.setUint32(16, centralOffset, true);
215
+
216
+ const output = new Uint8Array(centralOffset + centralSize + end.length);
217
+ let writeOffset = 0;
218
+ for (const part of [...localParts, ...centralParts, end]) {
219
+ output.set(part, writeOffset);
220
+ writeOffset += part.length;
221
+ }
222
+ return output;
223
+ }
224
+
225
+ function downloadBlob(blob: Blob, filename: string) {
90
226
  const url = URL.createObjectURL(blob);
91
227
  const link = window.document.createElement("a");
92
228
  link.href = url;
93
- link.download = result.filename;
229
+ link.download = filename;
94
230
  window.document.body.appendChild(link);
95
231
  link.click();
96
232
  link.remove();
97
233
  URL.revokeObjectURL(url);
98
234
  }
99
235
 
236
+ function printExportHtml(result: CollectionExportResult) {
237
+ const iframe = window.document.createElement("iframe");
238
+ const url = URL.createObjectURL(
239
+ new Blob([result.content], { type: "text/html;charset=utf-8" }),
240
+ );
241
+ iframe.setAttribute("aria-hidden", "true");
242
+ iframe.style.position = "fixed";
243
+ iframe.style.insetInlineEnd = "0";
244
+ iframe.style.bottom = "0";
245
+ iframe.style.width = "0";
246
+ iframe.style.height = "0";
247
+ iframe.style.border = "0";
248
+ let cleanedUp = false;
249
+ const cleanup = () => {
250
+ if (cleanedUp) return;
251
+ cleanedUp = true;
252
+ URL.revokeObjectURL(url);
253
+ iframe.remove();
254
+ };
255
+ iframe.addEventListener("load", () => {
256
+ const frameWindow = iframe.contentWindow;
257
+ if (!frameWindow) {
258
+ cleanup();
259
+ return;
260
+ }
261
+ frameWindow.addEventListener(
262
+ "afterprint",
263
+ () => window.setTimeout(cleanup, 500),
264
+ { once: true },
265
+ );
266
+ frameWindow.focus();
267
+ frameWindow.print();
268
+ });
269
+ iframe.src = url;
270
+ window.document.body.appendChild(iframe);
271
+ window.setTimeout(cleanup, 60_000);
272
+ }
273
+
274
+ export function deliverCollectionExport(result: CollectionExportResult) {
275
+ if (result.format === "pdf" && result.print) {
276
+ printExportHtml(result);
277
+ return;
278
+ }
279
+ if (result.format === "markdown") {
280
+ if (!result.archiveFiles?.length) {
281
+ throw new Error("Markdown export did not include package files.");
282
+ }
283
+ const archive = buildStoreOnlyZip(result.archiveFiles);
284
+ const archiveBuffer = archive.buffer.slice(
285
+ archive.byteOffset,
286
+ archive.byteOffset + archive.byteLength,
287
+ ) as ArrayBuffer;
288
+ downloadBlob(
289
+ new Blob([archiveBuffer], { type: "application/zip" }),
290
+ result.filename,
291
+ );
292
+ return;
293
+ }
294
+ downloadBlob(
295
+ new Blob([result.content], { type: result.mimeType }),
296
+ result.filename,
297
+ );
298
+ }
299
+
100
300
  export function DatabaseExportDialog({
101
301
  documentId,
102
302
  context,
103
- defaultScope,
104
303
  open,
105
304
  onOpenChange,
106
305
  }: {
107
306
  documentId: string;
108
307
  context: DatabaseExportContext | null;
109
- defaultScope: DatabaseExportScopeKind;
110
308
  open: boolean;
111
309
  onOpenChange: (open: boolean) => void;
112
310
  }) {
113
311
  const t = useT();
114
312
  const exportDocument = useActionMutation("export-document");
115
- const [scope, setScope] = useState<DatabaseExportScopeKind>(defaultScope);
116
- const [propertyIds, setPropertyIds] = useState<string[]>([]);
313
+ const [format, setFormat] = useState<DatabaseExportFormat>("csv");
314
+ const [scope, setScope] = useState<DatabaseExportScopeKind>("current_view");
315
+ const [selections, setSelections] = useState(() =>
316
+ defaultDatabaseExportSelections(context?.properties ?? []),
317
+ );
318
+ const [error, setError] = useState<string | null>(null);
117
319
  const wasOpenRef = useRef(false);
118
- const defaultPropertyIds = useMemo(
119
- () => defaultDatabaseCsvPropertyIds(context?.properties ?? []),
320
+ const hasCurrentView = Boolean(context?.viewId.trim());
321
+ const defaults = useMemo(
322
+ () => defaultDatabaseExportSelections(context?.properties ?? []),
120
323
  [context],
121
324
  );
325
+ const selection = selections[format];
326
+ const scalarProperties =
327
+ context?.properties.filter((property) => property.type !== "blocks") ?? [];
328
+ const blockProperties = additionalDatabaseExportBlockProperties(
329
+ context?.properties ?? [],
330
+ );
331
+ const formatLabels: Record<DatabaseExportFormat, string> = {
332
+ csv: t("editor.toolbar.exportFormatCsv"),
333
+ markdown: t("editor.toolbar.exportFormatMarkdown"),
334
+ html: t("editor.toolbar.exportFormatHtml"),
335
+ pdf: t("editor.toolbar.exportFormatPdf"),
336
+ };
122
337
 
123
338
  useEffect(() => {
124
339
  if (shouldInitializeDatabaseExportDialog(wasOpenRef.current, open)) {
125
- setScope(defaultScope);
126
- setPropertyIds(defaultPropertyIds);
340
+ setFormat("csv");
341
+ setScope(hasCurrentView ? "current_view" : "all_members");
342
+ setSelections(defaults);
343
+ setError(null);
127
344
  }
128
345
  wasOpenRef.current = open;
129
- }, [defaultPropertyIds, defaultScope, open]);
346
+ }, [defaults, hasCurrentView, open]);
130
347
 
131
- const toggleProperty = (id: string, checked: boolean) => {
132
- setPropertyIds((current) =>
133
- checked
134
- ? [...new Set([...current, id])]
135
- : current.filter((x) => x !== id),
348
+ const updateSelection = (patch: Partial<DatabaseExportSelection>) => {
349
+ setError(null);
350
+ setSelections((current) =>
351
+ updateDatabaseExportSelection(current, format, patch),
136
352
  );
137
353
  };
138
354
 
355
+ const toggleId = (
356
+ key: "propertyIds" | "blockPropertyIds",
357
+ id: string,
358
+ checked: boolean,
359
+ ) => {
360
+ const current = selection[key];
361
+ updateSelection({
362
+ [key]: checked
363
+ ? [...new Set([...current, id])]
364
+ : current.filter((candidate) => candidate !== id),
365
+ });
366
+ };
367
+
139
368
  const handleExport = async () => {
140
369
  if (!context || exportDocument.isPending) return;
370
+ setError(null);
141
371
  try {
142
372
  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"));
373
+ databaseExportRequest({
374
+ id: documentId,
375
+ format,
376
+ context,
377
+ scope,
378
+ selection,
379
+ }),
380
+ )) as CollectionExportResult;
381
+ deliverCollectionExport(result);
147
382
  onOpenChange(false);
148
- } catch (error) {
149
- toast.error(t("editor.toolbar.exportFailed"), {
150
- description:
151
- error instanceof Error ? error.message : t("empty.genericError"),
152
- });
383
+ } catch (caught) {
384
+ setError(
385
+ actionErrorMessage(caught) ??
386
+ (caught instanceof Error
387
+ ? caught.message
388
+ : t("editor.toolbar.exportFailed")),
389
+ );
153
390
  }
154
391
  };
155
392
 
156
393
  if (!context) return null;
157
394
 
395
+ const setDialogOpen = (nextOpen: boolean) => {
396
+ if (!nextOpen && exportDocument.isPending) return;
397
+ onOpenChange(nextOpen);
398
+ };
399
+
158
400
  return (
159
- <Dialog open={open} onOpenChange={onOpenChange}>
160
- <DialogContent className="gap-5 sm:max-w-md" data-database-export-dialog>
401
+ <Dialog open={open} onOpenChange={setDialogOpen}>
402
+ <DialogContent
403
+ className="max-h-[min(42rem,calc(100vh-2rem))] gap-5 overflow-y-auto sm:max-w-lg"
404
+ data-database-export-dialog
405
+ >
161
406
  <DialogHeader>
162
407
  <DialogTitle>{t("editor.toolbar.exportDatabase")}</DialogTitle>
163
408
  </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
- )}
409
+ <div className="grid gap-5">
410
+ <fieldset disabled={exportDocument.isPending}>
411
+ <legend className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
412
+ {t("editor.toolbar.exportFormat")}
413
+ </legend>
414
+ <div
415
+ className="grid grid-cols-2 gap-2 sm:grid-cols-4"
416
+ role="radiogroup"
200
417
  >
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")}
418
+ {EXPORT_FORMATS.map((candidate) => (
419
+ <button
420
+ key={candidate}
421
+ type="button"
422
+ role="radio"
423
+ aria-checked={format === candidate}
424
+ onClick={() => {
425
+ setError(null);
426
+ setFormat(candidate);
427
+ }}
428
+ className={cn(
429
+ "rounded-md border px-3 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
430
+ format === candidate
431
+ ? "border-foreground bg-muted"
432
+ : "border-border hover:bg-muted/60",
433
+ )}
434
+ >
435
+ {formatLabels[candidate]}
436
+ </button>
437
+ ))}
212
438
  </div>
213
- <div className="max-h-56 space-y-1 overflow-y-auto pe-1">
439
+ </fieldset>
440
+
441
+ <fieldset disabled={exportDocument.isPending}>
442
+ <legend className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
443
+ {t("editor.toolbar.exportScope")}
444
+ </legend>
445
+ {hasCurrentView ? (
446
+ <div
447
+ className="grid grid-cols-1 gap-2 sm:grid-cols-2"
448
+ role="radiogroup"
449
+ >
450
+ <button
451
+ type="button"
452
+ role="radio"
453
+ aria-checked={scope === "current_view"}
454
+ onClick={() => {
455
+ setError(null);
456
+ setScope("current_view");
457
+ }}
458
+ className={cn(
459
+ "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 disabled:cursor-not-allowed disabled:opacity-50",
460
+ scope === "current_view"
461
+ ? "border-foreground bg-muted"
462
+ : "border-border hover:bg-muted/60",
463
+ )}
464
+ >
465
+ <span className="block font-medium">
466
+ {t("editor.toolbar.currentView")}
467
+ </span>
468
+ <span className="block truncate text-xs text-muted-foreground">
469
+ {context.viewName}
470
+ </span>
471
+ </button>
472
+ <button
473
+ type="button"
474
+ role="radio"
475
+ aria-checked={scope === "all_members"}
476
+ onClick={() => {
477
+ setError(null);
478
+ setScope("all_members");
479
+ }}
480
+ className={cn(
481
+ "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 disabled:cursor-not-allowed disabled:opacity-50",
482
+ scope === "all_members"
483
+ ? "border-foreground bg-muted"
484
+ : "border-border hover:bg-muted/60",
485
+ )}
486
+ >
487
+ <span className="block font-medium">
488
+ {t("editor.toolbar.allDatabaseMembers")}
489
+ </span>
490
+ <span className="block text-xs text-muted-foreground">
491
+ {t("editor.toolbar.allDatabaseMembersDetail")}
492
+ </span>
493
+ </button>
494
+ </div>
495
+ ) : (
496
+ <div className="rounded-md border border-foreground bg-muted px-3 py-2 text-sm font-medium">
497
+ {t("editor.toolbar.allDatabaseMembers")}
498
+ </div>
499
+ )}
500
+ </fieldset>
501
+
502
+ <fieldset disabled={exportDocument.isPending}>
503
+ <legend className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
504
+ {t("editor.toolbar.exportProperties")}
505
+ </legend>
506
+ <div className="max-h-44 overflow-y-auto pe-1">
214
507
  <label className="flex items-center gap-3 rounded-md px-2 py-1.5 text-sm">
215
508
  <Checkbox
216
509
  checked
@@ -219,39 +512,75 @@ export function DatabaseExportDialog({
219
512
  />
220
513
  <span>{t("editor.toolbar.titleColumn")}</span>
221
514
  </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
- })}
515
+ {scalarProperties.map((property) => (
516
+ <label
517
+ key={property.id}
518
+ className="flex cursor-pointer items-center gap-3 rounded-md px-2 py-1.5 text-sm hover:bg-muted/60"
519
+ >
520
+ <Checkbox
521
+ checked={selection.propertyIds.includes(property.id)}
522
+ onCheckedChange={(value) =>
523
+ toggleId("propertyIds", property.id, value === true)
524
+ }
525
+ />
526
+ <span className="min-w-0 flex-1 truncate">
527
+ {property.name}
528
+ </span>
529
+ </label>
530
+ ))}
531
+ </div>
532
+ </fieldset>
533
+
534
+ <fieldset disabled={exportDocument.isPending}>
535
+ <legend className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
536
+ {t("editor.toolbar.exportPageContent")}
537
+ </legend>
538
+ <div className="max-h-44 overflow-y-auto pe-1">
539
+ <label className="flex cursor-pointer items-center gap-3 rounded-md px-2 py-1.5 text-sm hover:bg-muted/60">
540
+ <Checkbox
541
+ checked={selection.includePrimaryBody}
542
+ onCheckedChange={(value) =>
543
+ updateSelection({ includePrimaryBody: value === true })
544
+ }
545
+ />
546
+ <span>{t("editor.toolbar.primaryPageBody")}</span>
547
+ </label>
548
+ {blockProperties.map((property) => (
549
+ <label
550
+ key={property.id}
551
+ className="flex cursor-pointer items-center gap-3 rounded-md px-2 py-1.5 text-sm hover:bg-muted/60"
552
+ >
553
+ <Checkbox
554
+ checked={selection.blockPropertyIds.includes(property.id)}
555
+ onCheckedChange={(value) =>
556
+ toggleId("blockPropertyIds", property.id, value === true)
557
+ }
558
+ />
559
+ <span className="min-w-0 flex-1 truncate">
560
+ {property.name}
561
+ </span>
562
+ <span className="text-xs text-muted-foreground">
563
+ {t("editor.toolbar.blocksColumn")}
564
+ </span>
565
+ </label>
566
+ ))}
567
+ </div>
568
+ </fieldset>
569
+
570
+ {error ? (
571
+ <div
572
+ role="alert"
573
+ className="rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive"
574
+ >
575
+ {error}
247
576
  </div>
248
- </div>
577
+ ) : null}
249
578
  </div>
250
579
  <DialogFooter>
251
580
  <Button
252
581
  type="button"
253
582
  variant="outline"
254
- onClick={() => onOpenChange(false)}
583
+ onClick={() => setDialogOpen(false)}
255
584
  disabled={exportDocument.isPending}
256
585
  >
257
586
  {t("comments.cancel")}
@@ -267,8 +596,10 @@ export function DatabaseExportDialog({
267
596
  <IconDownload className="me-2 size-4" />
268
597
  )}
269
598
  {exportDocument.isPending
270
- ? t("editor.toolbar.exporting")
271
- : t("editor.toolbar.exportCsv")}
599
+ ? t("editor.toolbar.preparingExport", {
600
+ format: formatLabels[format],
601
+ })
602
+ : t("editor.toolbar.export")}
272
603
  </Button>
273
604
  </DialogFooter>
274
605
  </DialogContent>