@echovisionlab/geul-common 0.1.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/LICENSE.md +6 -0
- package/README.md +37 -0
- package/package.json +89 -0
- package/src/collaboration/artist.ts +63 -0
- package/src/collaboration/block-room-codec/ai-document-applicator.ts +319 -0
- package/src/collaboration/block-room-codec/ai-document-field-mutations.ts +557 -0
- package/src/collaboration/block-room-codec/ai-document-page-structure-mutations.ts +449 -0
- package/src/collaboration/block-room-codec/ai-document-values.ts +368 -0
- package/src/collaboration/block-room-codec/hydration.ts +257 -0
- package/src/collaboration/block-room-codec/internal.ts +432 -0
- package/src/collaboration/block-room-codec/locale-change-validation.ts +237 -0
- package/src/collaboration/block-room-codec/locale-presence.ts +827 -0
- package/src/collaboration/block-room-codec/materialization.ts +450 -0
- package/src/collaboration/block-room-codec/observation.ts +518 -0
- package/src/collaboration/block-room-codec/payload-mutations.ts +347 -0
- package/src/collaboration/block-room-codec/room-access.ts +154 -0
- package/src/collaboration/block-room-codec/structure-mutations.ts +456 -0
- package/src/collaboration/block-room-codec.ts +86 -0
- package/src/collaboration/campaign.ts +37 -0
- package/src/collaboration/document-layout.ts +75 -0
- package/src/collaboration/document.ts +185 -0
- package/src/collaboration/email-layout.ts +150 -0
- package/src/collaboration/form.ts +513 -0
- package/src/collaboration/label.ts +49 -0
- package/src/collaboration/map-theme.ts +157 -0
- package/src/collaboration/member-id.ts +9 -0
- package/src/collaboration/menu.ts +258 -0
- package/src/collaboration/metadata-ai.ts +99 -0
- package/src/collaboration/page.ts +483 -0
- package/src/collaboration/post-series.ts +96 -0
- package/src/collaboration/post.ts +59 -0
- package/src/collaboration/release.ts +221 -0
- package/src/collaboration/runtime-events.ts +547 -0
- package/src/collaboration/work.ts +107 -0
- package/src/editor/link-normalization.ts +212 -0
- package/src/editor/materialized-blocks.ts +58 -0
- package/src/index.ts +22 -0
- package/src/media/block-schemas.ts +78 -0
- package/src/media/hydration.ts +226 -0
- package/src/page/block-fixtures.ts +586 -0
- package/src/page/index.ts +3 -0
- package/src/page/types.ts +57 -0
- package/src/post/index.ts +1 -0
- package/src/post/types.ts +13 -0
- package/src/test/random-id.ts +9 -0
- package/src/translation/release.ts +88 -0
- package/src/types.ts +14 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
import { toJson } from "@bufbuild/protobuf";
|
|
2
|
+
import {
|
|
3
|
+
PageSectionLocaleSchema,
|
|
4
|
+
PageSectionNodeSchema,
|
|
5
|
+
RichTextBlockLocaleSchema,
|
|
6
|
+
RichTextBlockNodeSchema,
|
|
7
|
+
type PageSectionLocale,
|
|
8
|
+
type PageSectionNode,
|
|
9
|
+
type RichTextBlockLocale,
|
|
10
|
+
type RichTextBlockNode,
|
|
11
|
+
} from "@echovisionlab/geul-proto/content/block_content_pb.ts";
|
|
12
|
+
import * as Y from "yjs";
|
|
13
|
+
import {
|
|
14
|
+
fail,
|
|
15
|
+
integerValue,
|
|
16
|
+
jsonObject,
|
|
17
|
+
oneofPayload,
|
|
18
|
+
orderContainerKey,
|
|
19
|
+
pageSectionPayload,
|
|
20
|
+
pageSectionSlot,
|
|
21
|
+
richTextSlot,
|
|
22
|
+
setNodePayload,
|
|
23
|
+
stringValue,
|
|
24
|
+
withoutField,
|
|
25
|
+
type BlockRoomNodeFamily,
|
|
26
|
+
} from "./internal.ts";
|
|
27
|
+
import {
|
|
28
|
+
blockRoomBaseNodes,
|
|
29
|
+
blockRoomBaseOrder,
|
|
30
|
+
blockRoomLocaleOverlay,
|
|
31
|
+
decodeBaseNodes,
|
|
32
|
+
type BlockRoomBaseNodeSnapshot,
|
|
33
|
+
} from "./materialization.ts";
|
|
34
|
+
import {
|
|
35
|
+
roomDocumentType,
|
|
36
|
+
roomLocaleRole,
|
|
37
|
+
roomNode,
|
|
38
|
+
type BlockRoomAnchoredMutationOptions,
|
|
39
|
+
type BlockRoomMutationOptions,
|
|
40
|
+
} from "./room-access.ts";
|
|
41
|
+
import {
|
|
42
|
+
deleteBlockRoomLocalePresenceForBlocks,
|
|
43
|
+
markAllBlockRoomLocaleValuesPresentForBlock,
|
|
44
|
+
} from "./locale-presence.ts";
|
|
45
|
+
|
|
46
|
+
export interface BlockRoomContainerRef {
|
|
47
|
+
parentId: string | null;
|
|
48
|
+
containerSlot: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function orderArray(
|
|
52
|
+
yDocument: Y.Doc,
|
|
53
|
+
container: BlockRoomContainerRef,
|
|
54
|
+
create: boolean,
|
|
55
|
+
): Y.Array<string> {
|
|
56
|
+
const orders = blockRoomBaseOrder(yDocument);
|
|
57
|
+
const key = orderContainerKey(container.parentId, container.containerSlot);
|
|
58
|
+
const existing = orders.get(key);
|
|
59
|
+
if (existing !== undefined)
|
|
60
|
+
return existing instanceof Y.Array
|
|
61
|
+
? existing
|
|
62
|
+
: fail(`base_order:${key}:not_array`);
|
|
63
|
+
if (!create) return fail(`base_order:${key}:missing`);
|
|
64
|
+
const order = new Y.Array<string>();
|
|
65
|
+
orders.set(key, order);
|
|
66
|
+
return order;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createBlockRoomInsertionAnchor(
|
|
70
|
+
yDocument: Y.Doc,
|
|
71
|
+
container: BlockRoomContainerRef,
|
|
72
|
+
index: number,
|
|
73
|
+
): Y.RelativePosition {
|
|
74
|
+
const order = orderArray(yDocument, container, false);
|
|
75
|
+
if (!Number.isSafeInteger(index) || index < 0 || index > order.length) {
|
|
76
|
+
return fail("base_order:anchor_index");
|
|
77
|
+
}
|
|
78
|
+
return Y.createRelativePositionFromTypeIndex(order, index);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function insertionIndex(
|
|
82
|
+
yDocument: Y.Doc,
|
|
83
|
+
order: Y.Array<string>,
|
|
84
|
+
fallbackIndex: number,
|
|
85
|
+
anchor?: Y.RelativePosition,
|
|
86
|
+
): number {
|
|
87
|
+
if (anchor) {
|
|
88
|
+
const absolute = Y.createAbsolutePositionFromRelativePosition(
|
|
89
|
+
anchor,
|
|
90
|
+
yDocument,
|
|
91
|
+
);
|
|
92
|
+
if (!absolute || absolute.type !== order)
|
|
93
|
+
return fail("base_order:stale_anchor");
|
|
94
|
+
return absolute.index;
|
|
95
|
+
}
|
|
96
|
+
if (
|
|
97
|
+
!Number.isSafeInteger(fallbackIndex) ||
|
|
98
|
+
fallbackIndex < 0 ||
|
|
99
|
+
fallbackIndex > order.length
|
|
100
|
+
) {
|
|
101
|
+
return fail("base_order:index");
|
|
102
|
+
}
|
|
103
|
+
return fallbackIndex;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function insertBaseNode(
|
|
107
|
+
yDocument: Y.Doc,
|
|
108
|
+
node: Omit<BlockRoomBaseNodeSnapshot, "position">,
|
|
109
|
+
position: number,
|
|
110
|
+
anchor?: Y.RelativePosition,
|
|
111
|
+
): void {
|
|
112
|
+
const nodes = blockRoomBaseNodes(yDocument);
|
|
113
|
+
if (nodes.has(node.id)) fail(`base_node:${node.id}:exists`);
|
|
114
|
+
const value = new Y.Map<unknown>();
|
|
115
|
+
setNodePayload(value, node.family, node.kind, node.payload);
|
|
116
|
+
value.set("parentId", node.parentId);
|
|
117
|
+
value.set("containerSlot", node.containerSlot);
|
|
118
|
+
if (node.columnId) value.set("columnId", node.columnId);
|
|
119
|
+
const order = orderArray(yDocument, node, true);
|
|
120
|
+
const index = insertionIndex(yDocument, order, position, anchor);
|
|
121
|
+
nodes.set(node.id, value);
|
|
122
|
+
order.insert(index, [node.id]);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function insertRichTextNode(
|
|
126
|
+
yDocument: Y.Doc,
|
|
127
|
+
typedNode: RichTextBlockNode,
|
|
128
|
+
pageSectionId: string | undefined,
|
|
129
|
+
anchor?: Y.RelativePosition,
|
|
130
|
+
): void {
|
|
131
|
+
const json = jsonObject(
|
|
132
|
+
toJson(RichTextBlockNodeSchema, typedNode),
|
|
133
|
+
"rich_node",
|
|
134
|
+
);
|
|
135
|
+
const block = oneofPayload(
|
|
136
|
+
jsonObject(json.block, "rich_node:block"),
|
|
137
|
+
"id",
|
|
138
|
+
"rich_node:block",
|
|
139
|
+
);
|
|
140
|
+
if (!typedNode.placement) fail("rich_node:placement");
|
|
141
|
+
const documentType = roomDocumentType(yDocument);
|
|
142
|
+
const parentBlockId = typedNode.placement.parentBlockId;
|
|
143
|
+
if (documentType === "page" && !parentBlockId && !pageSectionId)
|
|
144
|
+
fail("rich_node:missing_section_parent");
|
|
145
|
+
if (documentType !== "page" && pageSectionId)
|
|
146
|
+
fail("rich_node:unexpected_section_parent");
|
|
147
|
+
insertBaseNode(
|
|
148
|
+
yDocument,
|
|
149
|
+
{
|
|
150
|
+
id: block.id,
|
|
151
|
+
family: "rich_text",
|
|
152
|
+
kind: block.kind,
|
|
153
|
+
payload: block.payload,
|
|
154
|
+
parentId: parentBlockId ?? pageSectionId ?? null,
|
|
155
|
+
containerSlot: richTextSlot(),
|
|
156
|
+
},
|
|
157
|
+
integerValue(typedNode.placement.index, "rich_node:index"),
|
|
158
|
+
anchor,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface InsertRichTextBlockNodeOptions extends BlockRoomAnchoredMutationOptions {
|
|
163
|
+
pageSectionId?: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function insertRichTextBlockNode(
|
|
167
|
+
yDocument: Y.Doc,
|
|
168
|
+
node: RichTextBlockNode,
|
|
169
|
+
options: InsertRichTextBlockNodeOptions = {},
|
|
170
|
+
): void {
|
|
171
|
+
yDocument.transact(() => {
|
|
172
|
+
insertRichTextNode(yDocument, node, options.pageSectionId, options.anchor);
|
|
173
|
+
}, options.origin);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function insertPageSectionNode(
|
|
177
|
+
yDocument: Y.Doc,
|
|
178
|
+
typedNode: PageSectionNode,
|
|
179
|
+
options: BlockRoomAnchoredMutationOptions = {},
|
|
180
|
+
): void {
|
|
181
|
+
yDocument.transact(() => {
|
|
182
|
+
if (roomDocumentType(yDocument) !== "page") fail("document_type");
|
|
183
|
+
const json = jsonObject(
|
|
184
|
+
toJson(PageSectionNodeSchema, typedNode),
|
|
185
|
+
"page_node",
|
|
186
|
+
);
|
|
187
|
+
const section = pageSectionPayload(
|
|
188
|
+
jsonObject(json.section, "page_node:section"),
|
|
189
|
+
"page_node:section",
|
|
190
|
+
);
|
|
191
|
+
if (!typedNode.placement) fail("page_node:placement");
|
|
192
|
+
const parentId = typedNode.placement.parentSectionId ?? null;
|
|
193
|
+
const columnId = typedNode.placement.columnId;
|
|
194
|
+
insertBaseNode(
|
|
195
|
+
yDocument,
|
|
196
|
+
{
|
|
197
|
+
id: section.id,
|
|
198
|
+
family: "page_section",
|
|
199
|
+
kind: section.kind,
|
|
200
|
+
payload:
|
|
201
|
+
section.kind === "richText"
|
|
202
|
+
? withoutField(section.payload, "blocks", "page_node:rich_text")
|
|
203
|
+
: section.payload,
|
|
204
|
+
parentId,
|
|
205
|
+
containerSlot: pageSectionSlot(parentId ?? undefined, columnId),
|
|
206
|
+
...(columnId ? { columnId } : {}),
|
|
207
|
+
},
|
|
208
|
+
integerValue(typedNode.placement.index, "page_node:index"),
|
|
209
|
+
options.anchor,
|
|
210
|
+
);
|
|
211
|
+
if (typedNode.section?.value.case === "richText") {
|
|
212
|
+
for (const block of typedNode.section.value.value.blocks?.nodes ?? []) {
|
|
213
|
+
insertRichTextNode(yDocument, block, section.id);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}, options.origin);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function insertLocaleNode(
|
|
220
|
+
yDocument: Y.Doc,
|
|
221
|
+
node: {
|
|
222
|
+
id: string;
|
|
223
|
+
family: BlockRoomNodeFamily;
|
|
224
|
+
kind: string;
|
|
225
|
+
payload: BlockRoomBaseNodeSnapshot["payload"];
|
|
226
|
+
},
|
|
227
|
+
): void {
|
|
228
|
+
const nodes = blockRoomLocaleOverlay(yDocument);
|
|
229
|
+
if (nodes.has(node.id)) fail(`locale:${node.id}:exists`);
|
|
230
|
+
const value = new Y.Map<unknown>();
|
|
231
|
+
setNodePayload(value, node.family, node.kind, node.payload);
|
|
232
|
+
nodes.set(node.id, value);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function insertRichTextLocaleNode(
|
|
236
|
+
yDocument: Y.Doc,
|
|
237
|
+
block: RichTextBlockLocale,
|
|
238
|
+
): void {
|
|
239
|
+
const parsed = oneofPayload(
|
|
240
|
+
jsonObject(toJson(RichTextBlockLocaleSchema, block), "rich_locale"),
|
|
241
|
+
"blockId",
|
|
242
|
+
"rich_locale",
|
|
243
|
+
);
|
|
244
|
+
insertLocaleNode(yDocument, {
|
|
245
|
+
id: parsed.id,
|
|
246
|
+
family: "rich_text",
|
|
247
|
+
kind: parsed.kind,
|
|
248
|
+
payload: parsed.payload,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function insertRichTextBlockLocale(
|
|
253
|
+
yDocument: Y.Doc,
|
|
254
|
+
block: RichTextBlockLocale,
|
|
255
|
+
options: BlockRoomMutationOptions = {},
|
|
256
|
+
): void {
|
|
257
|
+
yDocument.transact(() => {
|
|
258
|
+
insertRichTextLocaleNode(yDocument, block);
|
|
259
|
+
if (roomLocaleRole(yDocument) === "source")
|
|
260
|
+
markAllBlockRoomLocaleValuesPresentForBlock(yDocument, block.blockId);
|
|
261
|
+
}, options.origin);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function insertPageSectionLocale(
|
|
265
|
+
yDocument: Y.Doc,
|
|
266
|
+
section: PageSectionLocale,
|
|
267
|
+
options: BlockRoomMutationOptions = {},
|
|
268
|
+
): void {
|
|
269
|
+
yDocument.transact(() => {
|
|
270
|
+
if (roomDocumentType(yDocument) !== "page") fail("document_type");
|
|
271
|
+
const parsed = oneofPayload(
|
|
272
|
+
jsonObject(toJson(PageSectionLocaleSchema, section), "page_locale"),
|
|
273
|
+
"sectionId",
|
|
274
|
+
"page_locale",
|
|
275
|
+
);
|
|
276
|
+
const payload =
|
|
277
|
+
parsed.kind === "richText"
|
|
278
|
+
? withoutField(parsed.payload, "blocks", "page_locale:rich_text")
|
|
279
|
+
: parsed.payload;
|
|
280
|
+
const insertedIds = new Set<string>();
|
|
281
|
+
jsonObject(payload, "page_locale:payload");
|
|
282
|
+
insertLocaleNode(yDocument, {
|
|
283
|
+
id: parsed.id,
|
|
284
|
+
family: "page_section",
|
|
285
|
+
kind: parsed.kind,
|
|
286
|
+
payload,
|
|
287
|
+
});
|
|
288
|
+
insertedIds.add(parsed.id);
|
|
289
|
+
if (section.value.case === "richText") {
|
|
290
|
+
for (const block of section.value.value.blocks?.blocks ?? []) {
|
|
291
|
+
insertRichTextLocaleNode(yDocument, block);
|
|
292
|
+
insertedIds.add(block.blockId);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (roomLocaleRole(yDocument) === "source")
|
|
296
|
+
for (const id of insertedIds)
|
|
297
|
+
markAllBlockRoomLocaleValuesPresentForBlock(yDocument, id);
|
|
298
|
+
}, options.origin);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function moveBaseNode(
|
|
302
|
+
yDocument: Y.Doc,
|
|
303
|
+
id: string,
|
|
304
|
+
family: BlockRoomNodeFamily,
|
|
305
|
+
container: BlockRoomContainerRef & { columnId?: string },
|
|
306
|
+
fallbackIndex: number,
|
|
307
|
+
anchor?: Y.RelativePosition,
|
|
308
|
+
): void {
|
|
309
|
+
const node = roomNode(yDocument, { id, family });
|
|
310
|
+
const oldContainer: BlockRoomContainerRef = {
|
|
311
|
+
parentId:
|
|
312
|
+
node.get("parentId") === null
|
|
313
|
+
? null
|
|
314
|
+
: stringValue(node.get("parentId"), `base_node:${id}:parent`),
|
|
315
|
+
containerSlot: stringValue(
|
|
316
|
+
node.get("containerSlot"),
|
|
317
|
+
`base_node:${id}:slot`,
|
|
318
|
+
),
|
|
319
|
+
};
|
|
320
|
+
const oldOrder = orderArray(yDocument, oldContainer, false);
|
|
321
|
+
const oldIndex = oldOrder.toArray().indexOf(id);
|
|
322
|
+
if (oldIndex < 0) fail(`base_order:missing:${id}`);
|
|
323
|
+
const newOrder = orderArray(yDocument, container, true);
|
|
324
|
+
let index: number;
|
|
325
|
+
if (anchor) {
|
|
326
|
+
index = insertionIndex(yDocument, newOrder, fallbackIndex, anchor);
|
|
327
|
+
if (oldOrder === newOrder && oldIndex < index) index -= 1;
|
|
328
|
+
} else {
|
|
329
|
+
const maximum = newOrder.length - (oldOrder === newOrder ? 1 : 0);
|
|
330
|
+
if (
|
|
331
|
+
!Number.isSafeInteger(fallbackIndex) ||
|
|
332
|
+
fallbackIndex < 0 ||
|
|
333
|
+
fallbackIndex > maximum
|
|
334
|
+
) {
|
|
335
|
+
return fail("base_order:index");
|
|
336
|
+
}
|
|
337
|
+
index = fallbackIndex;
|
|
338
|
+
}
|
|
339
|
+
oldOrder.delete(oldIndex, 1);
|
|
340
|
+
node.set("parentId", container.parentId);
|
|
341
|
+
node.set("containerSlot", container.containerSlot);
|
|
342
|
+
if (container.columnId) node.set("columnId", container.columnId);
|
|
343
|
+
else node.delete("columnId");
|
|
344
|
+
newOrder.insert(index, [id]);
|
|
345
|
+
if (oldOrder.length === 0) {
|
|
346
|
+
blockRoomBaseOrder(yDocument).delete(
|
|
347
|
+
orderContainerKey(oldContainer.parentId, oldContainer.containerSlot),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function moveRichTextBlockNode(
|
|
353
|
+
yDocument: Y.Doc,
|
|
354
|
+
id: string,
|
|
355
|
+
placement: { parentBlockId?: string; index: number },
|
|
356
|
+
options: InsertRichTextBlockNodeOptions = {},
|
|
357
|
+
): void {
|
|
358
|
+
yDocument.transact(() => {
|
|
359
|
+
const documentType = roomDocumentType(yDocument);
|
|
360
|
+
const parentId = placement.parentBlockId ?? options.pageSectionId ?? null;
|
|
361
|
+
if (documentType === "page" && parentId === null)
|
|
362
|
+
fail("rich_node:missing_section_parent");
|
|
363
|
+
if (documentType !== "page" && options.pageSectionId)
|
|
364
|
+
fail("rich_node:unexpected_section_parent");
|
|
365
|
+
moveBaseNode(
|
|
366
|
+
yDocument,
|
|
367
|
+
id,
|
|
368
|
+
"rich_text",
|
|
369
|
+
{ parentId, containerSlot: richTextSlot() },
|
|
370
|
+
placement.index,
|
|
371
|
+
options.anchor,
|
|
372
|
+
);
|
|
373
|
+
}, options.origin);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function movePageSectionNode(
|
|
377
|
+
yDocument: Y.Doc,
|
|
378
|
+
id: string,
|
|
379
|
+
placement: { parentSectionId?: string; columnId?: string; index: number },
|
|
380
|
+
options: BlockRoomAnchoredMutationOptions = {},
|
|
381
|
+
): void {
|
|
382
|
+
yDocument.transact(() => {
|
|
383
|
+
if (roomDocumentType(yDocument) !== "page") fail("document_type");
|
|
384
|
+
const parentId = placement.parentSectionId ?? null;
|
|
385
|
+
moveBaseNode(
|
|
386
|
+
yDocument,
|
|
387
|
+
id,
|
|
388
|
+
"page_section",
|
|
389
|
+
{
|
|
390
|
+
parentId,
|
|
391
|
+
containerSlot: pageSectionSlot(
|
|
392
|
+
parentId ?? undefined,
|
|
393
|
+
placement.columnId,
|
|
394
|
+
),
|
|
395
|
+
...(placement.columnId ? { columnId: placement.columnId } : {}),
|
|
396
|
+
},
|
|
397
|
+
placement.index,
|
|
398
|
+
options.anchor,
|
|
399
|
+
);
|
|
400
|
+
}, options.origin);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export function deleteBlockRoomBaseNode(
|
|
404
|
+
yDocument: Y.Doc,
|
|
405
|
+
id: string,
|
|
406
|
+
options: BlockRoomMutationOptions = {},
|
|
407
|
+
): void {
|
|
408
|
+
yDocument.transact(() => {
|
|
409
|
+
const decoded = decodeBaseNodes(
|
|
410
|
+
blockRoomBaseNodes(yDocument),
|
|
411
|
+
blockRoomBaseOrder(yDocument),
|
|
412
|
+
);
|
|
413
|
+
if (!decoded.has(id)) fail(`base_node:${id}:missing`);
|
|
414
|
+
const deleted = new Set([id]);
|
|
415
|
+
let changed = true;
|
|
416
|
+
while (changed) {
|
|
417
|
+
changed = false;
|
|
418
|
+
for (const node of decoded.values()) {
|
|
419
|
+
if (
|
|
420
|
+
node.parentId !== null &&
|
|
421
|
+
deleted.has(node.parentId) &&
|
|
422
|
+
!deleted.has(node.id)
|
|
423
|
+
) {
|
|
424
|
+
deleted.add(node.id);
|
|
425
|
+
changed = true;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const nodes = blockRoomBaseNodes(yDocument);
|
|
430
|
+
const orders = blockRoomBaseOrder(yDocument);
|
|
431
|
+
for (const deletedId of deleted) {
|
|
432
|
+
const node = decoded.get(deletedId)!;
|
|
433
|
+
const key = orderContainerKey(node.parentId, node.containerSlot);
|
|
434
|
+
const order = orderArray(yDocument, node, false);
|
|
435
|
+
const index = order.toArray().indexOf(deletedId);
|
|
436
|
+
order.delete(index, 1);
|
|
437
|
+
if (order.length === 0) orders.delete(key);
|
|
438
|
+
nodes.delete(deletedId);
|
|
439
|
+
blockRoomLocaleOverlay(yDocument).delete(deletedId);
|
|
440
|
+
}
|
|
441
|
+
deleteBlockRoomLocalePresenceForBlocks(yDocument, deleted);
|
|
442
|
+
}, options.origin);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export function deleteBlockRoomLocaleNode(
|
|
446
|
+
yDocument: Y.Doc,
|
|
447
|
+
id: string,
|
|
448
|
+
options: BlockRoomMutationOptions = {},
|
|
449
|
+
): void {
|
|
450
|
+
yDocument.transact(() => {
|
|
451
|
+
const nodes = blockRoomLocaleOverlay(yDocument);
|
|
452
|
+
if (!nodes.has(id)) fail(`locale:${id}:missing`);
|
|
453
|
+
nodes.delete(id);
|
|
454
|
+
deleteBlockRoomLocalePresenceForBlocks(yDocument, new Set([id]));
|
|
455
|
+
}, options.origin);
|
|
456
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export {
|
|
2
|
+
canonicalBlockRoomDocumentBytes,
|
|
3
|
+
type BlockRoomDocumentType,
|
|
4
|
+
type BlockRoomLocaleRole,
|
|
5
|
+
type BlockRoomNodeFamily,
|
|
6
|
+
type BlockRoomTypedDocument,
|
|
7
|
+
type RichTextBlockRoomDocumentType,
|
|
8
|
+
} from "./block-room-codec/internal.ts";
|
|
9
|
+
export { hydrateCanonicalBlockRoom } from "./block-room-codec/hydration.ts";
|
|
10
|
+
export {
|
|
11
|
+
applyAIDocumentOperationsToBlockRoom,
|
|
12
|
+
type ApplyAIDocumentOperationsOptions,
|
|
13
|
+
} from "./block-room-codec/ai-document-applicator.ts";
|
|
14
|
+
export {
|
|
15
|
+
assertCanonicalBlockRoomParity,
|
|
16
|
+
decodeCanonicalBlockRoom,
|
|
17
|
+
materializeCanonicalBlockRoom,
|
|
18
|
+
type BlockRoomBaseNodeSnapshot,
|
|
19
|
+
type BlockRoomLocaleNodeSnapshot,
|
|
20
|
+
type CanonicalBlockRoomSnapshot,
|
|
21
|
+
} from "./block-room-codec/materialization.ts";
|
|
22
|
+
export {
|
|
23
|
+
deleteBlockRoomAtomicValue,
|
|
24
|
+
deleteBlockRoomPayloadArrayItem,
|
|
25
|
+
getBlockRoomAtomicValue,
|
|
26
|
+
getBlockRoomCollaborativeText,
|
|
27
|
+
insertBlockRoomPayloadArrayItem,
|
|
28
|
+
moveBlockRoomPayloadArrayItem,
|
|
29
|
+
replaceBlockRoomCollaborativeText,
|
|
30
|
+
replaceBlockRoomPayloadArray,
|
|
31
|
+
replaceRichTextBlockData,
|
|
32
|
+
setBlockRoomAtomicValue,
|
|
33
|
+
type BlockRoomAtomicValue,
|
|
34
|
+
type ReplaceRichTextBlockDataOptions,
|
|
35
|
+
} from "./block-room-codec/payload-mutations.ts";
|
|
36
|
+
export {
|
|
37
|
+
roomDocumentType,
|
|
38
|
+
roomLocale,
|
|
39
|
+
roomLocaleRole,
|
|
40
|
+
roomSourceLocale,
|
|
41
|
+
type BlockRoomAnchoredMutationOptions,
|
|
42
|
+
type BlockRoomMutationOptions,
|
|
43
|
+
type BlockRoomNodeRef,
|
|
44
|
+
type BlockRoomPayloadRef,
|
|
45
|
+
} from "./block-room-codec/room-access.ts";
|
|
46
|
+
export {
|
|
47
|
+
assertBlockRoomLocaleChangeAllowed,
|
|
48
|
+
BlockRoomLocaleChangeError,
|
|
49
|
+
BlockRoomLocaleChangeRejectionReason,
|
|
50
|
+
type BlockRoomLocaleChangeRejectionReason as BlockRoomLocaleChangeRejectionReasonType,
|
|
51
|
+
} from "./block-room-codec/locale-change-validation.ts";
|
|
52
|
+
export {
|
|
53
|
+
blockRoomPresentLocaleValues,
|
|
54
|
+
blockRoomLocaleValueTargetIdentity,
|
|
55
|
+
blockRoomLocaleValueTargetBlockId,
|
|
56
|
+
canonicalBlockRoomLocaleValueTarget,
|
|
57
|
+
canonicalBlockRoomLocaleValueTargetKey,
|
|
58
|
+
canonicalBlockRoomLocaleValueTargets,
|
|
59
|
+
markBlockRoomLocaleValuePresent,
|
|
60
|
+
} from "./block-room-codec/locale-presence.ts";
|
|
61
|
+
export {
|
|
62
|
+
createBlockRoomInsertionAnchor,
|
|
63
|
+
deleteBlockRoomBaseNode,
|
|
64
|
+
deleteBlockRoomLocaleNode,
|
|
65
|
+
insertPageSectionLocale,
|
|
66
|
+
insertPageSectionNode,
|
|
67
|
+
insertRichTextBlockLocale,
|
|
68
|
+
insertRichTextBlockNode,
|
|
69
|
+
movePageSectionNode,
|
|
70
|
+
moveRichTextBlockNode,
|
|
71
|
+
type BlockRoomContainerRef,
|
|
72
|
+
type InsertRichTextBlockNodeOptions,
|
|
73
|
+
} from "./block-room-codec/structure-mutations.ts";
|
|
74
|
+
export {
|
|
75
|
+
decodeCanonicalBlockRoomAffectedNodes,
|
|
76
|
+
mergeBlockRoomChangeSets,
|
|
77
|
+
observeBlockRoomChanges,
|
|
78
|
+
observeCanonicalBlockRoom,
|
|
79
|
+
transactBlockRoom,
|
|
80
|
+
type BlockRoomAffectedBaseNodeSnapshot,
|
|
81
|
+
type BlockRoomChangeSet,
|
|
82
|
+
type BlockRoomTransactionOriginKind,
|
|
83
|
+
type CanonicalBlockRoomAffectedSnapshot,
|
|
84
|
+
type CanonicalBlockRoomTransaction,
|
|
85
|
+
type ObservedBlockRoomChange,
|
|
86
|
+
} from "./block-room-codec/observation.ts";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const campaignRecipientScopeSchema = z.enum([
|
|
4
|
+
"SUBSCRIBED_USERS",
|
|
5
|
+
"ALL_MATCHING_USERS",
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
export type CampaignRecipientScope = z.infer<
|
|
9
|
+
typeof campaignRecipientScopeSchema
|
|
10
|
+
>;
|
|
11
|
+
|
|
12
|
+
export const campaignCollabFieldsSchema = z
|
|
13
|
+
.object({
|
|
14
|
+
segmentId: z.string().nullable().optional(),
|
|
15
|
+
layoutId: z.string().nullable().optional(),
|
|
16
|
+
recipientScope: campaignRecipientScopeSchema.default("SUBSCRIBED_USERS"),
|
|
17
|
+
})
|
|
18
|
+
.strict();
|
|
19
|
+
|
|
20
|
+
export type CampaignCollabFields = z.infer<typeof campaignCollabFieldsSchema>;
|
|
21
|
+
export type CampaignFieldValue = string | null;
|
|
22
|
+
|
|
23
|
+
export function extractCampaignFields(fieldsMap: {
|
|
24
|
+
get(key: string): CampaignFieldValue | undefined;
|
|
25
|
+
}): CampaignCollabFields {
|
|
26
|
+
const raw: Record<string, unknown> = {};
|
|
27
|
+
|
|
28
|
+
for (const key of Object.keys(campaignCollabFieldsSchema.shape)) {
|
|
29
|
+
const value = fieldsMap.get(key);
|
|
30
|
+
|
|
31
|
+
if (value !== undefined) {
|
|
32
|
+
raw[key] = value;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return campaignCollabFieldsSchema.parse(raw);
|
|
37
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type * as Y from "yjs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const DOCUMENT_LAYOUT_FIELD_KEYS = [
|
|
5
|
+
"contentHeight",
|
|
6
|
+
"pageChrome",
|
|
7
|
+
"footer",
|
|
8
|
+
] as const;
|
|
9
|
+
|
|
10
|
+
export const documentLayoutSchema = z
|
|
11
|
+
.object({
|
|
12
|
+
contentHeight: z.enum(["content", "viewport"]),
|
|
13
|
+
pageChrome: z.enum(["flow", "pinned"]),
|
|
14
|
+
footer: z.enum(["flow", "pinned"]),
|
|
15
|
+
})
|
|
16
|
+
.strict();
|
|
17
|
+
|
|
18
|
+
export type DocumentLayout = z.infer<typeof documentLayoutSchema>;
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_DOCUMENT_LAYOUT: DocumentLayout = {
|
|
21
|
+
contentHeight: "content",
|
|
22
|
+
pageChrome: "flow",
|
|
23
|
+
footer: "flow",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function readDocumentLayoutValue(
|
|
27
|
+
settingsMap: Y.Map<unknown>,
|
|
28
|
+
key: keyof DocumentLayout,
|
|
29
|
+
): unknown {
|
|
30
|
+
const value = settingsMap.get(key);
|
|
31
|
+
return value === undefined ? DEFAULT_DOCUMENT_LAYOUT[key] : value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function readDocumentLayout(
|
|
35
|
+
settingsMap: Y.Map<unknown>,
|
|
36
|
+
): DocumentLayout {
|
|
37
|
+
return documentLayoutSchema.parse({
|
|
38
|
+
contentHeight: readDocumentLayoutValue(settingsMap, "contentHeight"),
|
|
39
|
+
pageChrome: readDocumentLayoutValue(settingsMap, "pageChrome"),
|
|
40
|
+
footer: readDocumentLayoutValue(settingsMap, "footer"),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function hasExplicitDocumentLayout(
|
|
45
|
+
settingsMap: Y.Map<unknown>,
|
|
46
|
+
): boolean {
|
|
47
|
+
if (!DOCUMENT_LAYOUT_FIELD_KEYS.every((key) => settingsMap.has(key))) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return documentLayoutSchema.safeParse({
|
|
52
|
+
contentHeight: settingsMap.get("contentHeight"),
|
|
53
|
+
pageChrome: settingsMap.get("pageChrome"),
|
|
54
|
+
footer: settingsMap.get("footer"),
|
|
55
|
+
}).success;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function writeDocumentLayout(
|
|
59
|
+
settingsMap: Y.Map<unknown>,
|
|
60
|
+
layout: DocumentLayout,
|
|
61
|
+
): void {
|
|
62
|
+
const parsed = documentLayoutSchema.parse(layout);
|
|
63
|
+
const document = settingsMap.doc;
|
|
64
|
+
if (!document) {
|
|
65
|
+
throw new Error("Document settings map must be attached to a Y.Doc");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const write = () => {
|
|
69
|
+
settingsMap.set("contentHeight", parsed.contentHeight);
|
|
70
|
+
settingsMap.set("pageChrome", parsed.pageChrome);
|
|
71
|
+
settingsMap.set("footer", parsed.footer);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
document.transact(write);
|
|
75
|
+
}
|