@agent-native/core 0.77.10 → 0.77.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +6 -0
- package/corpus/core/package.json +3 -3
- package/corpus/core/src/collab/awareness.ts +12 -9
- package/corpus/core/src/collab/client.ts +4 -2
- package/corpus/templates/clips/chrome-extension/package.json +1 -1
- package/corpus/templates/content/actions/list-trashed-content-databases.ts +36 -2
- package/corpus/templates/content/actions/move-document.ts +79 -33
- package/corpus/templates/content/actions/restore-content-database.ts +40 -3
- package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +84 -2
- package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +177 -49
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +393 -219
- package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +632 -467
- package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +114 -14
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +56 -15
- package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +6 -5
- package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +12 -4
- package/corpus/templates/content/app/global.css +9 -0
- package/corpus/templates/content/app/hooks/use-create-page.ts +36 -7
- package/corpus/templates/content/app/i18n-data.ts +140 -10
- package/corpus/templates/content/changelog/2026-06-25-comment-cards-now-track-their-highlighted-text-while-scrolli.md +6 -0
- package/corpus/templates/content/changelog/2026-06-25-comments-now-scroll-with-the-document-and-stay-below-the-pag.md +6 -0
- package/corpus/templates/content/changelog/2026-06-25-creating-a-database-from-the-slash-menu-no-longer-leaves-sta.md +6 -0
- package/corpus/templates/content/changelog/2026-06-25-sidebar-hover-controls-no-longer-leave-a-lingering-fade-when.md +6 -0
- package/corpus/templates/content/changelog/2026-06-25-sidebar-page-actions-are-now-easier-to-see-when-hovering-ina.md +6 -0
- package/corpus/templates/content/changelog/2026-06-25-text-selections-in-the-editor-no-longer-show-white-gaps-when.md +6 -0
- package/corpus/templates/content/changelog/2026-06-25-the-editor-toolbar-now-has-a-one-click-page-link-copy-button.md +6 -0
- package/corpus/templates/content/changelog/2026-06-25-the-editor-toolbar-now-shows-page-breadcrumbs-and-the-latest.md +6 -0
- package/corpus/templates/content/changelog/2026-06-25-the-page-command-now-leaves-a-notion-style-page-reference-an.md +6 -0
- package/corpus/templates/content/shared/nfm.ts +16 -0
- package/dist/collab/awareness.d.ts +1 -1
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/awareness.js +12 -8
- package/dist/collab/awareness.js.map +1 -1
- package/dist/collab/client.d.ts.map +1 -1
- package/dist/collab/client.js +4 -3
- package/dist/collab/client.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +7 -7
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +3 -3
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.77.
|
|
3
|
+
"version": "0.77.11",
|
|
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": {
|
|
@@ -211,8 +211,8 @@
|
|
|
211
211
|
"@react-router/dev": "^8.0.1",
|
|
212
212
|
"@react-router/fs-routes": "^8.0.1",
|
|
213
213
|
"@resvg/resvg-js": "^2.6.2",
|
|
214
|
-
"@sentry/browser": "
|
|
215
|
-
"@sentry/node": "
|
|
214
|
+
"@sentry/browser": "10.60.0",
|
|
215
|
+
"@sentry/node": "10.60.0",
|
|
216
216
|
"@standard-schema/spec": "^1.1.0",
|
|
217
217
|
"@tanstack/react-table": "^8.21.3",
|
|
218
218
|
"@tiptap/core": "3.27.1",
|
|
@@ -102,7 +102,7 @@ function pruneIfEmpty(docId: string, map: Map<number, AwarenessEntry>): void {
|
|
|
102
102
|
*
|
|
103
103
|
* Client sends its awareness state and receives other clients' states.
|
|
104
104
|
*
|
|
105
|
-
* Body: { clientId: number, state: string (
|
|
105
|
+
* Body: { clientId: number, state: string | null (JSON-encoded awareness state, or null to clear) }
|
|
106
106
|
* Response: { states: Array<{ clientId: number, state: string }> }
|
|
107
107
|
*/
|
|
108
108
|
export const postAwareness = defineEventHandler(async (event: H3Event) => {
|
|
@@ -115,28 +115,31 @@ export const postAwareness = defineEventHandler(async (event: H3Event) => {
|
|
|
115
115
|
const body = await readBody(event);
|
|
116
116
|
const { clientId, state } = body as {
|
|
117
117
|
clientId?: number;
|
|
118
|
-
state?: string;
|
|
118
|
+
state?: string | null;
|
|
119
119
|
};
|
|
120
120
|
|
|
121
|
-
if (clientId == null ||
|
|
121
|
+
if (clientId == null || state === undefined) {
|
|
122
122
|
// `!clientId` would wrongly reject clientId === 0, which is a valid
|
|
123
|
-
// (if rare) Yjs client id.
|
|
123
|
+
// (if rare) Yjs client id. A null state is valid: it clears this client.
|
|
124
124
|
setResponseStatus(event, 400);
|
|
125
125
|
return { error: "clientId and state required" };
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
const map = getDocAwareness(docId);
|
|
129
129
|
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
if (state === null) {
|
|
131
|
+
map.delete(clientId);
|
|
132
|
+
} else {
|
|
133
|
+
// Store this client's state
|
|
134
|
+
map.set(clientId, { clientId, state, lastSeen: Date.now() });
|
|
135
|
+
}
|
|
132
136
|
|
|
133
137
|
// Clean expired entries, then prune the outer-map entry if it becomes empty.
|
|
134
138
|
// Without pruning, a deployment with many transient docIds (e.g. one per
|
|
135
139
|
// session) would grow _awarenessMap without bound.
|
|
136
140
|
cleanExpired(map);
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
// that was immediately evicted by a concurrent cleanExpired run).
|
|
141
|
+
// Null-state clears and expiry can empty the map; prune the outer entry so
|
|
142
|
+
// transient document ids do not accumulate.
|
|
140
143
|
pruneIfEmpty(docId, map);
|
|
141
144
|
|
|
142
145
|
// Build the full list of current states (all clients including sender).
|
|
@@ -276,11 +276,13 @@ function scheduleAwarenessPush(
|
|
|
276
276
|
const timer = setTimeout(() => {
|
|
277
277
|
_awarenessThrottleTimers.delete(key);
|
|
278
278
|
const state = getState();
|
|
279
|
-
if (!state) return;
|
|
280
279
|
fetch(`${baseUrl}/${docId}/awareness`, {
|
|
281
280
|
method: "POST",
|
|
282
281
|
headers: { "Content-Type": "application/json" },
|
|
283
|
-
body: JSON.stringify({
|
|
282
|
+
body: JSON.stringify({
|
|
283
|
+
clientId,
|
|
284
|
+
state: state ? JSON.stringify(state) : null,
|
|
285
|
+
}),
|
|
284
286
|
}).catch(() => {}); // best-effort; poll cycle is the baseline fallback
|
|
285
287
|
}, 150);
|
|
286
288
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
2
|
import { accessFilter } from "@agent-native/core/sharing";
|
|
3
|
-
import { and, desc, eq, isNotNull } from "drizzle-orm";
|
|
3
|
+
import { and, desc, eq, isNotNull, isNull, ne, or } from "drizzle-orm";
|
|
4
|
+
import { alias } from "drizzle-orm/sqlite-core";
|
|
4
5
|
import { z } from "zod";
|
|
5
6
|
|
|
6
7
|
import { getDb, schema } from "../server/db/index.js";
|
|
@@ -14,6 +15,16 @@ export default defineAction({
|
|
|
14
15
|
readOnly: true,
|
|
15
16
|
run: async (): Promise<ListTrashedContentDatabasesResponse> => {
|
|
16
17
|
const db = getDb();
|
|
18
|
+
const hostDocuments = alias(schema.documents, "host_documents");
|
|
19
|
+
const isBlockOwned = and(
|
|
20
|
+
isNotNull(schema.contentDatabases.ownerDocumentId),
|
|
21
|
+
eq(schema.documents.parentId, schema.contentDatabases.ownerDocumentId),
|
|
22
|
+
);
|
|
23
|
+
const isNotBlockOwned = or(
|
|
24
|
+
isNull(schema.contentDatabases.ownerDocumentId),
|
|
25
|
+
isNull(schema.documents.parentId),
|
|
26
|
+
ne(schema.documents.parentId, schema.contentDatabases.ownerDocumentId),
|
|
27
|
+
);
|
|
17
28
|
const rows = await db
|
|
18
29
|
.select({
|
|
19
30
|
databaseId: schema.contentDatabases.id,
|
|
@@ -28,10 +39,33 @@ export default defineAction({
|
|
|
28
39
|
schema.documents,
|
|
29
40
|
eq(schema.documents.id, schema.contentDatabases.documentId),
|
|
30
41
|
)
|
|
42
|
+
.leftJoin(
|
|
43
|
+
hostDocuments,
|
|
44
|
+
eq(hostDocuments.id, schema.contentDatabases.ownerDocumentId),
|
|
45
|
+
)
|
|
31
46
|
.where(
|
|
32
47
|
and(
|
|
33
48
|
isNotNull(schema.contentDatabases.deletedAt),
|
|
34
|
-
|
|
49
|
+
or(
|
|
50
|
+
and(
|
|
51
|
+
isBlockOwned,
|
|
52
|
+
accessFilter(
|
|
53
|
+
hostDocuments,
|
|
54
|
+
schema.documentShares,
|
|
55
|
+
undefined,
|
|
56
|
+
"editor",
|
|
57
|
+
),
|
|
58
|
+
),
|
|
59
|
+
and(
|
|
60
|
+
isNotBlockOwned,
|
|
61
|
+
accessFilter(
|
|
62
|
+
schema.documents,
|
|
63
|
+
schema.documentShares,
|
|
64
|
+
undefined,
|
|
65
|
+
"admin",
|
|
66
|
+
),
|
|
67
|
+
),
|
|
68
|
+
),
|
|
35
69
|
),
|
|
36
70
|
)
|
|
37
71
|
.orderBy(desc(schema.contentDatabases.deletedAt));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
2
|
import { writeAppState } from "@agent-native/core/application-state";
|
|
3
|
-
import { assertAccess } from "@agent-native/core/sharing";
|
|
3
|
+
import { assertAccess, type Visibility } from "@agent-native/core/sharing";
|
|
4
4
|
import { and, eq, sql } from "drizzle-orm";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
|
|
@@ -54,19 +54,17 @@ async function assertParentIsNotDescendant({
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
async function
|
|
57
|
+
async function preflightBlockDatabaseOwnershipClearance({
|
|
58
58
|
db,
|
|
59
59
|
documentId,
|
|
60
60
|
ownerEmail,
|
|
61
61
|
parentId,
|
|
62
|
-
updatedAt,
|
|
63
62
|
}: {
|
|
64
63
|
db: ReturnType<typeof getDb>;
|
|
65
64
|
documentId: string;
|
|
66
65
|
ownerEmail: string;
|
|
67
66
|
parentId: string | null;
|
|
68
|
-
|
|
69
|
-
}) {
|
|
67
|
+
}): Promise<string | null> {
|
|
70
68
|
const [database] = await db
|
|
71
69
|
.select({
|
|
72
70
|
id: schema.contentDatabases.id,
|
|
@@ -81,11 +79,24 @@ async function clearBlockDatabaseOwnershipAfterParentChange({
|
|
|
81
79
|
);
|
|
82
80
|
|
|
83
81
|
if (!database?.ownerDocumentId || database.ownerDocumentId === parentId) {
|
|
84
|
-
return;
|
|
82
|
+
return null;
|
|
85
83
|
}
|
|
86
84
|
|
|
87
85
|
await assertAccess("document", database.ownerDocumentId, "editor");
|
|
86
|
+
return database.id;
|
|
87
|
+
}
|
|
88
88
|
|
|
89
|
+
async function clearBlockDatabaseOwnership({
|
|
90
|
+
db,
|
|
91
|
+
databaseId,
|
|
92
|
+
ownerEmail,
|
|
93
|
+
updatedAt,
|
|
94
|
+
}: {
|
|
95
|
+
db: Pick<ReturnType<typeof getDb>, "update">;
|
|
96
|
+
databaseId: string;
|
|
97
|
+
ownerEmail: string;
|
|
98
|
+
updatedAt: string;
|
|
99
|
+
}) {
|
|
89
100
|
await db
|
|
90
101
|
.update(schema.contentDatabases)
|
|
91
102
|
.set({
|
|
@@ -95,23 +106,44 @@ async function clearBlockDatabaseOwnershipAfterParentChange({
|
|
|
95
106
|
})
|
|
96
107
|
.where(
|
|
97
108
|
and(
|
|
98
|
-
eq(schema.contentDatabases.id,
|
|
109
|
+
eq(schema.contentDatabases.id, databaseId),
|
|
99
110
|
eq(schema.contentDatabases.ownerEmail, ownerEmail),
|
|
100
111
|
),
|
|
101
112
|
);
|
|
102
113
|
}
|
|
103
114
|
|
|
115
|
+
function sameRootSection(
|
|
116
|
+
left: { visibility: Visibility; orgId?: string | null },
|
|
117
|
+
right: { visibility: Visibility; orgId?: string | null },
|
|
118
|
+
) {
|
|
119
|
+
return left.visibility === right.visibility && left.orgId === right.orgId;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function rootSectionFilter(document: {
|
|
123
|
+
visibility: Visibility;
|
|
124
|
+
orgId?: string | null;
|
|
125
|
+
}) {
|
|
126
|
+
return and(
|
|
127
|
+
eq(schema.documents.visibility, document.visibility),
|
|
128
|
+
document.orgId
|
|
129
|
+
? eq(schema.documents.orgId, document.orgId)
|
|
130
|
+
: sql`${schema.documents.orgId} IS NULL`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
104
134
|
async function resolveSiblingPositionsAfterMove({
|
|
105
135
|
db,
|
|
106
136
|
ownerEmail,
|
|
107
137
|
id,
|
|
108
138
|
parentId,
|
|
139
|
+
rootSection,
|
|
109
140
|
position,
|
|
110
141
|
}: {
|
|
111
142
|
db: ReturnType<typeof getDb>;
|
|
112
143
|
ownerEmail: string;
|
|
113
144
|
id: string;
|
|
114
145
|
parentId: string | null;
|
|
146
|
+
rootSection: { visibility: Visibility; orgId?: string | null };
|
|
115
147
|
position: number;
|
|
116
148
|
}) {
|
|
117
149
|
const siblings = await db
|
|
@@ -129,6 +161,7 @@ async function resolveSiblingPositionsAfterMove({
|
|
|
129
161
|
)
|
|
130
162
|
: and(
|
|
131
163
|
eq(schema.documents.ownerEmail, ownerEmail),
|
|
164
|
+
rootSectionFilter(rootSection),
|
|
132
165
|
sql`parent_id IS NULL`,
|
|
133
166
|
),
|
|
134
167
|
);
|
|
@@ -209,6 +242,9 @@ export default defineAction({
|
|
|
209
242
|
if (parentAccess.resource.ownerEmail !== ownerEmail) {
|
|
210
243
|
throw new Error("Parent document must belong to the same owner");
|
|
211
244
|
}
|
|
245
|
+
if (!sameRootSection(parentAccess.resource, existing)) {
|
|
246
|
+
throw new Error("Parent document must be in the same section");
|
|
247
|
+
}
|
|
212
248
|
await assertParentIsNotDescendant({
|
|
213
249
|
db,
|
|
214
250
|
ownerEmail,
|
|
@@ -221,6 +257,15 @@ export default defineAction({
|
|
|
221
257
|
|
|
222
258
|
const targetParentId =
|
|
223
259
|
args.parentId !== undefined ? args.parentId : existing.parentId;
|
|
260
|
+
const blockDatabaseIdToDetach =
|
|
261
|
+
args.parentId !== undefined
|
|
262
|
+
? await preflightBlockDatabaseOwnershipClearance({
|
|
263
|
+
db,
|
|
264
|
+
documentId: id,
|
|
265
|
+
ownerEmail,
|
|
266
|
+
parentId: args.parentId,
|
|
267
|
+
})
|
|
268
|
+
: null;
|
|
224
269
|
const normalizedSiblingPositions =
|
|
225
270
|
args.position !== undefined
|
|
226
271
|
? await resolveSiblingPositionsAfterMove({
|
|
@@ -228,6 +273,7 @@ export default defineAction({
|
|
|
228
273
|
ownerEmail,
|
|
229
274
|
id,
|
|
230
275
|
parentId: targetParentId,
|
|
276
|
+
rootSection: existing,
|
|
231
277
|
position: args.position,
|
|
232
278
|
})
|
|
233
279
|
: null;
|
|
@@ -249,36 +295,36 @@ export default defineAction({
|
|
|
249
295
|
)
|
|
250
296
|
: and(
|
|
251
297
|
eq(schema.documents.ownerEmail, ownerEmail),
|
|
298
|
+
rootSectionFilter(existing),
|
|
252
299
|
sql`parent_id IS NULL`,
|
|
253
300
|
),
|
|
254
301
|
);
|
|
255
302
|
updates.position = (maxPos[0]?.max ?? -1) + 1;
|
|
256
303
|
}
|
|
257
304
|
|
|
258
|
-
await db
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
305
|
+
await db.transaction(async (tx) => {
|
|
306
|
+
await tx
|
|
307
|
+
.update(schema.documents)
|
|
308
|
+
.set(updates)
|
|
309
|
+
.where(
|
|
310
|
+
and(
|
|
311
|
+
eq(schema.documents.id, id),
|
|
312
|
+
eq(schema.documents.ownerEmail, ownerEmail),
|
|
313
|
+
),
|
|
314
|
+
);
|
|
267
315
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
-
}
|
|
316
|
+
if (args.parentId !== undefined && blockDatabaseIdToDetach) {
|
|
317
|
+
await clearBlockDatabaseOwnership({
|
|
318
|
+
db: tx,
|
|
319
|
+
databaseId: blockDatabaseIdToDetach,
|
|
320
|
+
ownerEmail,
|
|
321
|
+
updatedAt,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
277
324
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
db
|
|
325
|
+
if (normalizedSiblingPositions) {
|
|
326
|
+
for (const document of normalizedSiblingPositions) {
|
|
327
|
+
await tx
|
|
282
328
|
.update(schema.documents)
|
|
283
329
|
.set({ position: document.position })
|
|
284
330
|
.where(
|
|
@@ -286,10 +332,10 @@ export default defineAction({
|
|
|
286
332
|
eq(schema.documents.id, document.id),
|
|
287
333
|
eq(schema.documents.ownerEmail, ownerEmail),
|
|
288
334
|
),
|
|
289
|
-
)
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
});
|
|
293
339
|
|
|
294
340
|
const [doc] = await db
|
|
295
341
|
.select()
|
|
@@ -1,10 +1,37 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
2
|
import { writeAppState } from "@agent-native/core/application-state";
|
|
3
|
+
import { resolveAccess } from "@agent-native/core/sharing";
|
|
3
4
|
import { eq } from "drizzle-orm";
|
|
4
5
|
import { z } from "zod";
|
|
5
6
|
|
|
6
7
|
import { getDb, schema } from "../server/db/index.js";
|
|
7
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
assertContentDatabaseLifecycleAccess,
|
|
10
|
+
collectInlineDatabaseOwnerBlockIds,
|
|
11
|
+
} from "./_content-database-lifecycle.js";
|
|
12
|
+
import pullDocumentAction from "./pull-document.js";
|
|
13
|
+
|
|
14
|
+
async function shouldClearStaleInlineOwnership(args: {
|
|
15
|
+
ownerDocumentId: string | null;
|
|
16
|
+
ownerBlockId: string | null;
|
|
17
|
+
}) {
|
|
18
|
+
if (!args.ownerDocumentId || !args.ownerBlockId) return false;
|
|
19
|
+
let content: string | null = null;
|
|
20
|
+
try {
|
|
21
|
+
const host = await pullDocumentAction.run({
|
|
22
|
+
id: args.ownerDocumentId,
|
|
23
|
+
format: "markdown",
|
|
24
|
+
});
|
|
25
|
+
content = String(host.content ?? "");
|
|
26
|
+
} catch {
|
|
27
|
+
const hostAccess = await resolveAccess("document", args.ownerDocumentId);
|
|
28
|
+
if (!hostAccess) return false;
|
|
29
|
+
content = String(hostAccess.resource.content ?? "");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const parsed = await collectInlineDatabaseOwnerBlockIds(content);
|
|
33
|
+
return parsed.ok && !parsed.ownerBlockIds.has(args.ownerBlockId);
|
|
34
|
+
}
|
|
8
35
|
|
|
9
36
|
export default defineAction({
|
|
10
37
|
description: "Restore a soft-deleted content database.",
|
|
@@ -12,13 +39,23 @@ export default defineAction({
|
|
|
12
39
|
databaseId: z.string().describe("Content database ID"),
|
|
13
40
|
}),
|
|
14
41
|
run: async ({ databaseId }) => {
|
|
15
|
-
await assertContentDatabaseLifecycleAccess(databaseId);
|
|
42
|
+
const ownership = await assertContentDatabaseLifecycleAccess(databaseId);
|
|
16
43
|
const db = getDb();
|
|
17
44
|
const now = new Date().toISOString();
|
|
45
|
+
const clearInlineOwnership = await shouldClearStaleInlineOwnership({
|
|
46
|
+
ownerDocumentId: ownership.database.ownerDocumentId,
|
|
47
|
+
ownerBlockId: ownership.database.ownerBlockId,
|
|
48
|
+
});
|
|
18
49
|
|
|
19
50
|
await db
|
|
20
51
|
.update(schema.contentDatabases)
|
|
21
|
-
.set({
|
|
52
|
+
.set({
|
|
53
|
+
deletedAt: null,
|
|
54
|
+
updatedAt: now,
|
|
55
|
+
...(clearInlineOwnership
|
|
56
|
+
? { ownerDocumentId: null, ownerBlockId: null }
|
|
57
|
+
: {}),
|
|
58
|
+
})
|
|
22
59
|
.where(eq(schema.contentDatabases.id, databaseId));
|
|
23
60
|
|
|
24
61
|
await writeAppState("refresh-signal", { ts: Date.now() });
|
|
@@ -11,10 +11,16 @@ import {
|
|
|
11
11
|
IconH3,
|
|
12
12
|
IconH4,
|
|
13
13
|
} from "@tabler/icons-react";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
NodeSelection,
|
|
16
|
+
Plugin,
|
|
17
|
+
PluginKey,
|
|
18
|
+
type EditorState,
|
|
19
|
+
} from "@tiptap/pm/state";
|
|
20
|
+
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
15
21
|
import type { Editor } from "@tiptap/react";
|
|
16
22
|
import { BubbleMenu } from "@tiptap/react/menus";
|
|
17
|
-
import { useState } from "react";
|
|
23
|
+
import { useEffect, useState } from "react";
|
|
18
24
|
|
|
19
25
|
import {
|
|
20
26
|
Tooltip,
|
|
@@ -44,6 +50,15 @@ const BUBBLE_TOOLBAR_EXCLUDED_NODE_TYPES = new Set([
|
|
|
44
50
|
"localMdxComponent",
|
|
45
51
|
]);
|
|
46
52
|
|
|
53
|
+
type SelectionFillRange = {
|
|
54
|
+
from: number;
|
|
55
|
+
to: number;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const selectionFillPluginKey = new PluginKey<SelectionFillRange | null>(
|
|
59
|
+
"contentSelectionFill",
|
|
60
|
+
);
|
|
61
|
+
|
|
47
62
|
function selectionIncludesBubbleToolbarExcludedNode(
|
|
48
63
|
state: EditorState,
|
|
49
64
|
from: number,
|
|
@@ -72,6 +87,73 @@ export function BubbleToolbar({ editor, onComment }: BubbleToolbarProps) {
|
|
|
72
87
|
const [showLinkInput, setShowLinkInput] = useState(false);
|
|
73
88
|
const [linkUrl, setLinkUrl] = useState("");
|
|
74
89
|
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
const plugin = new Plugin<SelectionFillRange | null>({
|
|
92
|
+
key: selectionFillPluginKey,
|
|
93
|
+
state: {
|
|
94
|
+
init: () => null,
|
|
95
|
+
apply: (tr, value) => {
|
|
96
|
+
const meta = tr.getMeta(selectionFillPluginKey);
|
|
97
|
+
if (meta !== undefined) return meta;
|
|
98
|
+
return value
|
|
99
|
+
? {
|
|
100
|
+
from: tr.mapping.map(value.from),
|
|
101
|
+
to: tr.mapping.map(value.to),
|
|
102
|
+
}
|
|
103
|
+
: null;
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
props: {
|
|
107
|
+
decorations(state) {
|
|
108
|
+
const range = selectionFillPluginKey.getState(state);
|
|
109
|
+
if (!range || range.from === range.to) return DecorationSet.empty;
|
|
110
|
+
return DecorationSet.create(state.doc, [
|
|
111
|
+
Decoration.inline(range.from, range.to, {
|
|
112
|
+
class: "notion-selection-fill",
|
|
113
|
+
}),
|
|
114
|
+
]);
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
editor.registerPlugin(plugin);
|
|
120
|
+
|
|
121
|
+
const syncSelectionFill = () => {
|
|
122
|
+
const { state } = editor;
|
|
123
|
+
const { from, to } = state.selection;
|
|
124
|
+
const nextRange =
|
|
125
|
+
editor.isFocused &&
|
|
126
|
+
from !== to &&
|
|
127
|
+
!selectionIncludesBubbleToolbarExcludedNode(state, from, to)
|
|
128
|
+
? { from, to }
|
|
129
|
+
: null;
|
|
130
|
+
const currentRange = selectionFillPluginKey.getState(state);
|
|
131
|
+
if (
|
|
132
|
+
currentRange?.from === nextRange?.from &&
|
|
133
|
+
currentRange?.to === nextRange?.to
|
|
134
|
+
) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
editor.view.dispatch(
|
|
138
|
+
state.tr
|
|
139
|
+
.setMeta(selectionFillPluginKey, nextRange)
|
|
140
|
+
.setMeta("addToHistory", false),
|
|
141
|
+
);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
editor.on("selectionUpdate", syncSelectionFill);
|
|
145
|
+
editor.on("focus", syncSelectionFill);
|
|
146
|
+
editor.on("blur", syncSelectionFill);
|
|
147
|
+
syncSelectionFill();
|
|
148
|
+
|
|
149
|
+
return () => {
|
|
150
|
+
editor.off("selectionUpdate", syncSelectionFill);
|
|
151
|
+
editor.off("focus", syncSelectionFill);
|
|
152
|
+
editor.off("blur", syncSelectionFill);
|
|
153
|
+
editor.unregisterPlugin(selectionFillPluginKey);
|
|
154
|
+
};
|
|
155
|
+
}, [editor]);
|
|
156
|
+
|
|
75
157
|
const handleSetLink = () => {
|
|
76
158
|
if (linkUrl.trim()) {
|
|
77
159
|
editor
|