@agent-native/core 0.160.1 → 0.161.0
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/analytics/actions/bigquery.ts +57 -0
- package/corpus/templates/analytics/actions/restore-analysis-revision.ts +5 -2
- package/corpus/templates/analytics/actions/restore-dashboard-revision.ts +5 -2
- package/corpus/templates/analytics/app/components/analysis/AnalysisHistoryPanel.tsx +14 -3
- package/corpus/templates/analytics/app/components/dashboard/DashboardHistoryPanel.tsx +14 -3
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +3 -0
- package/corpus/templates/analytics/app/hooks/use-analysis-revisions.ts +1 -0
- package/corpus/templates/analytics/app/hooks/use-dashboard-revisions.ts +16 -1
- package/corpus/templates/analytics/app/lib/use-auto-focus-select.ts +19 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +0 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-sync.ts +14 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +19 -0
- package/corpus/templates/analytics/changelog/2026-08-17-fixed-dashboard-editing-focus-history-recovery-panel-setup-f.md +6 -0
- package/corpus/templates/calendar/actions/create-event.ts +7 -3
- package/corpus/templates/calendar/actions/manage-event-draft.ts +8 -0
- package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +64 -0
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +5 -1
- package/corpus/templates/calendar/app/hooks/use-events.ts +1 -0
- package/corpus/templates/calendar/changelog/2026-08-17-booking-links-now-respect-each-required-host-s-saved-availab.md +6 -0
- package/corpus/templates/calendar/server/lib/booking-host-availability.ts +76 -0
- package/corpus/templates/calendar/server/lib/google-calendar.ts +2 -0
- package/corpus/templates/calendar/shared/api.ts +1 -1
- package/corpus/templates/clips/app/components/player/signed-out-share-actions.tsx +47 -0
- package/corpus/templates/clips/app/components/player/transcript-panel.tsx +3 -1
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +27 -19
- package/corpus/templates/clips/changelog/2026-08-17-shared-clips-sign-in-comment-no-audio.md +6 -0
- package/corpus/templates/clips/server/routes/api/public-recording.get.ts +6 -5
- package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +13 -0
- package/corpus/templates/content/AGENTS.md +2 -0
- package/corpus/templates/content/actions/_blocks-field-identity.ts +17 -0
- package/corpus/templates/content/actions/_database-block-actions.ts +958 -0
- package/corpus/templates/content/actions/_database-row-mutation.ts +7 -7
- package/corpus/templates/content/actions/list-content-database-blocks.ts +31 -0
- package/corpus/templates/content/actions/mutate-content-database-block.ts +49 -0
- package/corpus/templates/content/changelog/2026-08-10-agents-can-safely-insert-update-upsert-delete-and-reorder-on.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.object.block.md +6 -2
- package/corpus/templates/content/docs/product/capabilities/content.object.blocks-field.md +2 -1
- package/corpus/templates/content/parity/matrix.md +30 -30
- package/corpus/templates/content/parity/matrix.ts +4 -1
- package/corpus/templates/content/shared/blocks-field-identity.ts +114 -37
- package/corpus/templates/content/shared/database-block-actions.ts +82 -0
- package/corpus/templates/content/shared/database-block-mutations.ts +366 -0
- package/corpus/templates/content/shared/nfm.ts +3 -2
- package/corpus/templates/mail/.agents/skills/email-drafts/SKILL.md +24 -0
- package/corpus/templates/mail/actions/get-mail-settings.ts +1 -1
- package/corpus/templates/mail/actions/update-mail-settings.ts +7 -3
- package/corpus/templates/mail/app/i18n/en-US.ts +3 -1
- package/corpus/templates/mail/app/pages/SettingsPage.tsx +63 -0
- package/corpus/templates/mail/changelog/2026-08-17-gmail-signatures-now-preserve-images-and-support-paste-or-up.md +6 -0
- package/corpus/templates/mail/changelog/2026-08-17-mail-now-updates-durable-drafting-preferences-without-openin.md +6 -0
- package/corpus/templates/mail/server/plugins/agent-chat.ts +19 -0
- package/corpus/templates/mail/shared/gmail-signature.ts +14 -2
- package/corpus/templates/mail/shared/signature.ts +1 -10
- package/corpus/templates/slides/actions/generate-slides-ai.ts +2 -0
- package/corpus/templates/slides/actions/patch-deck.ts +1 -1
- package/corpus/templates/slides/app/components/ThemeToggle.tsx +1 -0
- package/corpus/templates/slides/app/components/editor/SlideThumbnailContextMenu.tsx +59 -0
- package/corpus/templates/slides/app/global.css +1 -0
- package/corpus/templates/slides/app/lib/normalize-slide-padding.ts +40 -11
- package/corpus/templates/slides/changelog/2026-08-17-stable-layout-and-slide-editing.md +6 -0
- package/corpus/templates/slides/shared/deck-title.ts +12 -4
- package/dist/agent/production-agent.js +17 -4
- package/dist/agent/tool-approval-migrations.d.ts +1 -0
- package/dist/agent/tool-approval-migrations.js +10 -0
- package/dist/agent/tool-approval-store.d.ts +12 -0
- package/dist/agent/tool-approval-store.js +46 -1
- package/dist/agent/types.d.ts +3 -1
- package/dist/client/AssistantChat.js +3 -1
- package/dist/localization/core-messages/ar-SA.js +1 -0
- package/dist/localization/core-messages/de-DE.js +1 -0
- package/dist/localization/core-messages/en-US.d.ts +1 -0
- package/dist/localization/core-messages/en-US.js +1 -0
- package/dist/localization/core-messages/es-ES.js +1 -0
- package/dist/localization/core-messages/fr-FR.js +1 -0
- package/dist/localization/core-messages/hi-IN.js +1 -0
- package/dist/localization/core-messages/ja-JP.js +1 -0
- package/dist/localization/core-messages/ko-KR.js +1 -0
- package/dist/localization/core-messages/pt-BR.js +1 -0
- package/dist/localization/core-messages/zh-CN.js +1 -0
- package/dist/localization/core-messages/zh-TW.js +1 -0
- package/dist/localization/core-messages.d.ts +1 -0
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/scripts/resources/read.js +4 -4
- package/dist/scripts/resources/save-memory.js +15 -2
- package/dist/scripts/resources/write.js +3 -3
- package/dist/server/agent-chat/script-entries.d.ts +1 -0
- package/dist/server/agent-chat/script-entries.js +8 -0
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/docs/content/onboarding.mdx +13 -2
- package/package.json +3 -3
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { getUserSetting } from "@agent-native/core/settings";
|
|
2
|
+
|
|
3
|
+
import type { AvailabilityConfig, OverlayPerson } from "../../shared/api.js";
|
|
4
|
+
import { safeBookingTimeZone } from "./booking-timezone.js";
|
|
5
|
+
|
|
6
|
+
export interface EligibleHostAvailability {
|
|
7
|
+
email: string;
|
|
8
|
+
displayName?: string;
|
|
9
|
+
/** Present only when the host has saved a working-hours schedule. */
|
|
10
|
+
weeklySchedule?: AvailabilityConfig["weeklySchedule"];
|
|
11
|
+
/**
|
|
12
|
+
* Resolved from calendar-availability.timezone, falling back to
|
|
13
|
+
* calendar-settings.timezone. Present only when one of those is a
|
|
14
|
+
* resolvable IANA time zone — never defaulted, since guessing a peer's
|
|
15
|
+
* zone would be misleading.
|
|
16
|
+
*/
|
|
17
|
+
timezone?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Cross-references booking-link hosts against the owner's calendar overlay
|
|
22
|
+
* ("subscribed peer") list. Only hosts the owner has explicitly overlaid get
|
|
23
|
+
* their real working-hours schedule and time zone used for hard-filtering —
|
|
24
|
+
* everyone else keeps today's free/busy-only behavior.
|
|
25
|
+
*/
|
|
26
|
+
export async function getEligibleHostAvailability(
|
|
27
|
+
ownerEmail: string | undefined,
|
|
28
|
+
hostEmails: string[],
|
|
29
|
+
): Promise<EligibleHostAvailability[]> {
|
|
30
|
+
if (!ownerEmail || hostEmails.length === 0) return [];
|
|
31
|
+
|
|
32
|
+
const overlayData = (await getUserSetting(
|
|
33
|
+
ownerEmail,
|
|
34
|
+
"calendar-overlay-people",
|
|
35
|
+
)) as { people: OverlayPerson[] } | null;
|
|
36
|
+
const overlayEmails = new Set(
|
|
37
|
+
(overlayData?.people ?? []).map((person) => person.email.toLowerCase()),
|
|
38
|
+
);
|
|
39
|
+
if (overlayEmails.size === 0) return [];
|
|
40
|
+
|
|
41
|
+
const owner = ownerEmail.toLowerCase();
|
|
42
|
+
const eligibleEmails = Array.from(
|
|
43
|
+
new Set(
|
|
44
|
+
hostEmails
|
|
45
|
+
.map((email) => email.toLowerCase())
|
|
46
|
+
.filter((email) => email !== owner && overlayEmails.has(email)),
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
if (eligibleEmails.length === 0) return [];
|
|
50
|
+
|
|
51
|
+
return Promise.all(
|
|
52
|
+
eligibleEmails.map(async (email) => {
|
|
53
|
+
const [config, calendarSettings] = await Promise.all([
|
|
54
|
+
getUserSetting(
|
|
55
|
+
email,
|
|
56
|
+
"calendar-availability",
|
|
57
|
+
) as Promise<AvailabilityConfig | null>,
|
|
58
|
+
getUserSetting(email, "calendar-settings") as Promise<{
|
|
59
|
+
timezone?: string;
|
|
60
|
+
} | null>,
|
|
61
|
+
]);
|
|
62
|
+
const timezone =
|
|
63
|
+
safeBookingTimeZone(config?.timezone) ||
|
|
64
|
+
safeBookingTimeZone(calendarSettings?.timezone);
|
|
65
|
+
|
|
66
|
+
if (!config?.weeklySchedule) {
|
|
67
|
+
return { email, timezone };
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
email,
|
|
71
|
+
weeklySchedule: config.weeklySchedule,
|
|
72
|
+
timezone,
|
|
73
|
+
};
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -304,6 +304,8 @@ function applyEventOptions(body: any, event: CalendarEvent): void {
|
|
|
304
304
|
if (event.visibility !== undefined) body.visibility = event.visibility;
|
|
305
305
|
if (event.status !== undefined) body.status = event.status;
|
|
306
306
|
if (event.colorId !== undefined) body.colorId = event.colorId;
|
|
307
|
+
if (event.recurrence !== undefined) body.recurrence = event.recurrence;
|
|
308
|
+
if (event.recurrence !== undefined) body.recurrence = event.recurrence;
|
|
307
309
|
if (event.remindersUseDefault !== undefined) {
|
|
308
310
|
body.reminders = event.remindersUseDefault
|
|
309
311
|
? { useDefault: true }
|
|
@@ -146,9 +146,9 @@ export interface CalendarEventDraft {
|
|
|
146
146
|
transparency?: "opaque" | "transparent";
|
|
147
147
|
visibility?: "default" | "public" | "private" | "confidential";
|
|
148
148
|
colorId?: string;
|
|
149
|
-
recurrence?: string[];
|
|
150
149
|
reminders?: CalendarEvent["reminders"];
|
|
151
150
|
remindersUseDefault?: boolean;
|
|
151
|
+
recurrence?: CalendarEvent["recurrence"];
|
|
152
152
|
attachments?: CalendarEvent["attachments"];
|
|
153
153
|
attendees?: CalendarEvent["attendees"];
|
|
154
154
|
addGoogleMeet?: boolean;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { appPath } from "@agent-native/core/client/api-path";
|
|
2
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
3
|
+
import { buildSignInReturnHref } from "@agent-native/core/client/ui";
|
|
4
|
+
import { IconExternalLink, IconLogin2 } from "@tabler/icons-react";
|
|
5
|
+
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
|
|
8
|
+
export type SignedOutShareCta = "signin" | "try_clips";
|
|
9
|
+
|
|
10
|
+
export function buildShareSignInHref(recordingId: string): string {
|
|
11
|
+
return buildSignInReturnHref({ returnTo: `/share/${recordingId}` });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function SignedOutShareActions({
|
|
15
|
+
recordingId,
|
|
16
|
+
onCtaClick,
|
|
17
|
+
}: {
|
|
18
|
+
recordingId: string;
|
|
19
|
+
onCtaClick?: (cta: SignedOutShareCta) => void;
|
|
20
|
+
}) {
|
|
21
|
+
const t = useT();
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
<Button variant="outline" size="sm" asChild>
|
|
26
|
+
<a
|
|
27
|
+
href={buildShareSignInHref(recordingId)}
|
|
28
|
+
className="gap-1.5"
|
|
29
|
+
onClick={() => onCtaClick?.("signin")}
|
|
30
|
+
>
|
|
31
|
+
<IconLogin2 className="h-4 w-4 rtl:-scale-x-100" />
|
|
32
|
+
{t("sharePage.signIn")}
|
|
33
|
+
</a>
|
|
34
|
+
</Button>
|
|
35
|
+
<Button variant="ghost" size="sm" asChild>
|
|
36
|
+
<a
|
|
37
|
+
href={appPath("/")}
|
|
38
|
+
className="gap-1.5"
|
|
39
|
+
onClick={() => onCtaClick?.("try_clips")}
|
|
40
|
+
>
|
|
41
|
+
{t("sharePage.tryClips")}
|
|
42
|
+
<IconExternalLink className="h-3.5 w-3.5" />
|
|
43
|
+
</a>
|
|
44
|
+
</Button>
|
|
45
|
+
</>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -513,7 +513,9 @@ function isNoSpeechTranscriptFailure(
|
|
|
513
513
|
return (
|
|
514
514
|
normalized.includes("no speech") ||
|
|
515
515
|
normalized.includes("no native transcript was captured") ||
|
|
516
|
-
normalized.includes("no transcript was captured by native speech")
|
|
516
|
+
normalized.includes("no transcript was captured by native speech") ||
|
|
517
|
+
normalized.includes("no audio track") ||
|
|
518
|
+
normalized.includes("recording was saved without audio")
|
|
517
519
|
);
|
|
518
520
|
}
|
|
519
521
|
|
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
IconDeviceDesktop,
|
|
21
21
|
IconDownload,
|
|
22
22
|
IconDots,
|
|
23
|
-
IconExternalLink,
|
|
24
23
|
IconLock,
|
|
25
24
|
IconLogin2,
|
|
26
25
|
} from "@tabler/icons-react";
|
|
@@ -59,6 +58,8 @@ import { RecordingViewsBadge } from "@/components/player/recording-views-badge";
|
|
|
59
58
|
import { RequestAccessDialog } from "@/components/player/request-access-dialog";
|
|
60
59
|
import { ShareRecordingPopover } from "@/components/player/share-dialog";
|
|
61
60
|
import { SignInPromptDialog } from "@/components/player/sign-in-prompt-dialog";
|
|
61
|
+
import { SignedOutShareActions } from "@/components/player/signed-out-share-actions";
|
|
62
|
+
import { TimestampedCommentButton } from "@/components/player/timestamped-comment-button";
|
|
62
63
|
import { TranscriptPanel } from "@/components/player/transcript-panel";
|
|
63
64
|
import {
|
|
64
65
|
VideoPlayer,
|
|
@@ -555,7 +556,13 @@ export default function ShareRoute() {
|
|
|
555
556
|
);
|
|
556
557
|
|
|
557
558
|
const dataQ = useQuery({
|
|
558
|
-
queryKey: [
|
|
559
|
+
queryKey: [
|
|
560
|
+
"public-recording",
|
|
561
|
+
shareId,
|
|
562
|
+
password,
|
|
563
|
+
agentAccessToken,
|
|
564
|
+
session?.email ?? null,
|
|
565
|
+
],
|
|
559
566
|
queryFn: async () => {
|
|
560
567
|
const url = new URL(
|
|
561
568
|
`${appBasePath()}/api/public-recording`,
|
|
@@ -657,11 +664,7 @@ export default function ShareRoute() {
|
|
|
657
664
|
viewerRole === "owner" ||
|
|
658
665
|
viewerRole === "admin" ||
|
|
659
666
|
viewerRole === "editor";
|
|
660
|
-
const viewerCanComment =
|
|
661
|
-
viewerRole === "owner" ||
|
|
662
|
-
viewerRole === "admin" ||
|
|
663
|
-
viewerRole === "editor" ||
|
|
664
|
-
viewerRole === "commenter";
|
|
667
|
+
const viewerCanComment = Boolean(dataQ.data?.data?.viewer?.canComment);
|
|
665
668
|
const viewerIsOwner = Boolean(dataQ.data?.data?.viewer?.isOwner);
|
|
666
669
|
const canReshareLink =
|
|
667
670
|
(viewerRole === "viewer" || viewerRole === "commenter") &&
|
|
@@ -1138,18 +1141,10 @@ export default function ShareRoute() {
|
|
|
1138
1141
|
onOpenInsights={() => setPanel("insights")}
|
|
1139
1142
|
/>
|
|
1140
1143
|
{session ? null : (
|
|
1141
|
-
<
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
rel="noopener noreferrer"
|
|
1146
|
-
className="gap-1.5"
|
|
1147
|
-
onClick={() => fireShareCtaClick("try_clips")}
|
|
1148
|
-
>
|
|
1149
|
-
{t("sharePage.tryClips")}
|
|
1150
|
-
<IconExternalLink className="h-3.5 w-3.5" />
|
|
1151
|
-
</a>
|
|
1152
|
-
</Button>
|
|
1144
|
+
<SignedOutShareActions
|
|
1145
|
+
recordingId={recording.id}
|
|
1146
|
+
onCtaClick={fireShareCtaClick}
|
|
1147
|
+
/>
|
|
1153
1148
|
)}
|
|
1154
1149
|
{!viewerCanEdit && canDownloadRecording ? (
|
|
1155
1150
|
<DropdownMenu
|
|
@@ -1256,6 +1251,18 @@ export default function ShareRoute() {
|
|
|
1256
1251
|
) : null}
|
|
1257
1252
|
</div>
|
|
1258
1253
|
<div className="flex max-w-full flex-col items-stretch gap-2 sm:items-end">
|
|
1254
|
+
<TimestampedCommentButton
|
|
1255
|
+
enableComments={recording.enableComments}
|
|
1256
|
+
canComment={!session || viewerCanComment}
|
|
1257
|
+
className="shrink-0"
|
|
1258
|
+
onOpen={() => {
|
|
1259
|
+
if (!session) {
|
|
1260
|
+
requireSignIn("comment");
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
if (viewerCanComment) setPanel("comments");
|
|
1264
|
+
}}
|
|
1265
|
+
/>
|
|
1259
1266
|
{recording.enableReactions ? (
|
|
1260
1267
|
<ReactionsTray
|
|
1261
1268
|
disabled={!viewerCanComment}
|
|
@@ -1401,6 +1408,7 @@ export default function ShareRoute() {
|
|
|
1401
1408
|
shareId,
|
|
1402
1409
|
password,
|
|
1403
1410
|
agentAccessToken,
|
|
1411
|
+
session?.email ?? null,
|
|
1404
1412
|
]}
|
|
1405
1413
|
selectComments={(d: any) => d?.data?.comments}
|
|
1406
1414
|
applyComments={(d: any, next) =>
|
|
@@ -268,6 +268,11 @@ export default defineEventHandler(async (event) => {
|
|
|
268
268
|
return { error: "Not found" };
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
const viewerCanComment = Boolean(
|
|
272
|
+
session?.email &&
|
|
273
|
+
(rec.visibility === "public" || viewerIsOwner || viewerIsOrgMember),
|
|
274
|
+
);
|
|
275
|
+
|
|
271
276
|
// Expiry check
|
|
272
277
|
const recordingExpired = isRecordingExpired(rec.expiresAt);
|
|
273
278
|
if (recordingExpired) {
|
|
@@ -536,11 +541,7 @@ export default defineEventHandler(async (event) => {
|
|
|
536
541
|
viewerRole === "owner" ||
|
|
537
542
|
viewerRole === "admin" ||
|
|
538
543
|
viewerRole === "editor",
|
|
539
|
-
canComment:
|
|
540
|
-
viewerRole === "owner" ||
|
|
541
|
-
viewerRole === "admin" ||
|
|
542
|
-
viewerRole === "editor" ||
|
|
543
|
-
viewerRole === "commenter",
|
|
544
|
+
canComment: viewerCanComment,
|
|
544
545
|
isOwner: viewerRole === "owner",
|
|
545
546
|
role: viewerRole ?? "viewer",
|
|
546
547
|
canOpenDashboard,
|
|
@@ -96,6 +96,18 @@ its stored name). In table views a Blocks column shows a word count (e.g.
|
|
|
96
96
|
database view's column menu (not from the page body); deleting the last
|
|
97
97
|
Blocks field warns that it removes the body for every object of the type.
|
|
98
98
|
|
|
99
|
+
For one-block agent edits, call `list-content-database-blocks` with the exact
|
|
100
|
+
space, database, backing document, membership row, row document, and property
|
|
101
|
+
IDs. Preserve its schema, row, and field revisions. Each returned block names
|
|
102
|
+
the operations its kind supports and carries canonical Notion-flavored Markdown
|
|
103
|
+
(NFM) for that one block. Pass all three revisions to
|
|
104
|
+
`mutate-content-database-block`; its `insert`, `update`, `upsert`, `delete`, and
|
|
105
|
+
`reorder` variants preserve unmentioned fields and sibling blocks. A block
|
|
106
|
+
value must contain exactly one top-level block of the declared kind. Reuse an
|
|
107
|
+
idempotency key only for an exact retry. Unsupported kinds, kind conversion,
|
|
108
|
+
tombstone reuse, cross-parent reorder, schema drift, and stale row or field
|
|
109
|
+
revisions fail explicitly.
|
|
110
|
+
|
|
99
111
|
Formula properties store their expression in property options and support
|
|
100
112
|
`{Property name}` substitution plus simple numeric math such as `{MSV} * 2`.
|
|
101
113
|
|
|
@@ -236,6 +248,7 @@ Use `create-content-database`, `create-inline-content-database`,
|
|
|
236
248
|
`duplicate-database-items`, `remove-database-items`, `move-database-item`,
|
|
237
249
|
`update-content-database-view`, `list-document-properties`,
|
|
238
250
|
`configure-document-property`, `set-document-property`,
|
|
251
|
+
`list-content-database-blocks`, `mutate-content-database-block`,
|
|
239
252
|
`duplicate-document-property`, and `delete-document-property`; do not edit
|
|
240
253
|
property rows or view config via raw SQL when an action can do it.
|
|
241
254
|
|
|
@@ -79,6 +79,8 @@ Read the relevant skill before deeper work:
|
|
|
79
79
|
| `edit-document` | Find/replace edit — preferred for small changes |
|
|
80
80
|
| `update-document` | Full rewrite of title, content, or description |
|
|
81
81
|
| `delete-document` | Move a page and its children to Trash |
|
|
82
|
+
| `list-content-database-blocks` | List stable blocks and revisions in one exact database row/property |
|
|
83
|
+
| `mutate-content-database-block` | Insert, update, upsert, delete, or reorder one supported stable block |
|
|
82
84
|
| `migrate-content-database-rows` | Validate, atomically apply, verify, roll back, or finalize one bounded whole-database row migration |
|
|
83
85
|
|
|
84
86
|
Every action carries its own schema, and the rest of the app-specific surface
|
|
@@ -29,6 +29,17 @@ export class BlocksFieldRevisionConflictError extends Error {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
export class BlocksFieldIdCollisionError extends Error {
|
|
33
|
+
readonly blockId: string;
|
|
34
|
+
readonly statusCode = 409;
|
|
35
|
+
|
|
36
|
+
constructor(blockId: string) {
|
|
37
|
+
super(`Block ID is already owned by another Blocks field: ${blockId}`);
|
|
38
|
+
this.name = "BlocksFieldIdCollisionError";
|
|
39
|
+
this.blockId = blockId;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
32
43
|
function nanoid(size = 12): string {
|
|
33
44
|
const chars =
|
|
34
45
|
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
@@ -265,6 +276,8 @@ export async function persistBlocksFieldIdentity(args: {
|
|
|
265
276
|
previousMarkdown: string;
|
|
266
277
|
markdown: string;
|
|
267
278
|
expectedRevision?: number;
|
|
279
|
+
preferredIdsByPath?: Readonly<Record<string, string>>;
|
|
280
|
+
rejectCrossFieldIdRemapping?: boolean;
|
|
268
281
|
now: string;
|
|
269
282
|
}): Promise<StoredBlocksFieldIdentity> {
|
|
270
283
|
const fieldId = blocksFieldId(args.documentId, args.propertyId);
|
|
@@ -306,6 +319,7 @@ export async function persistBlocksFieldIdentity(args: {
|
|
|
306
319
|
previous,
|
|
307
320
|
markdown: args.markdown,
|
|
308
321
|
createId: () => `block_${nanoid(16)}`,
|
|
322
|
+
preferredIdsByPath: args.preferredIdsByPath,
|
|
309
323
|
});
|
|
310
324
|
|
|
311
325
|
if (next.blocks.length > 0) {
|
|
@@ -325,6 +339,9 @@ export async function persistBlocksFieldIdentity(args: {
|
|
|
325
339
|
const reserved = new Set(next.blocks.map((block) => block.id));
|
|
326
340
|
for (const existing of existingOwners) {
|
|
327
341
|
if (existing.fieldId === fieldId) continue;
|
|
342
|
+
if (args.rejectCrossFieldIdRemapping) {
|
|
343
|
+
throw new BlocksFieldIdCollisionError(existing.id);
|
|
344
|
+
}
|
|
328
345
|
let replacement = `block_${nanoid(16)}`;
|
|
329
346
|
while (reserved.has(replacement)) replacement = `block_${nanoid(16)}`;
|
|
330
347
|
reserved.add(replacement);
|