@templatical/core 0.26.2 → 0.27.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/dist/cloud/index.d.ts +196 -110
- package/dist/cloud/index.js +380 -563
- package/dist/cloud/index.js.map +1 -1
- package/dist/editor-BCujEIye.d.ts +99 -0
- package/dist/index.d.ts +209 -5
- package/dist/index.js +524 -12
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/editor-BIIsaIoN.d.ts +0 -45
package/dist/cloud/index.js
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
|
-
import { SdkError
|
|
2
|
-
import { computed, onScopeDispose,
|
|
1
|
+
import { SdkError } from "@templatical/types";
|
|
2
|
+
import { computed, onScopeDispose, ref, watch } from "vue";
|
|
3
3
|
//#region src/cloud/auth.ts
|
|
4
|
+
/**
|
|
5
|
+
* Whether a token-refresh failure is worth interrupting the user over.
|
|
6
|
+
*
|
|
7
|
+
* **Fatal** means the endpoint actively rejected the credentials — a `4xx`. No
|
|
8
|
+
* amount of retrying fixes that: every subsequent request will fail the same
|
|
9
|
+
* way, so the session really is over and the editor should say so.
|
|
10
|
+
*
|
|
11
|
+
* **Everything else is transient**: a network blip, a `5xx`, a timeout, a
|
|
12
|
+
* malformed response. Those resolve on their own, and blanking the editor over
|
|
13
|
+
* one interrupts someone mid-edit on a template that is very likely unsaved.
|
|
14
|
+
* `AuthManager` reports every failure to `onError` and re-throws regardless, so
|
|
15
|
+
* a caller that genuinely cannot proceed still finds out; this only decides
|
|
16
|
+
* whether the *overlay* goes up.
|
|
17
|
+
*
|
|
18
|
+
* `404` is treated as fatal deliberately: a refresh URL that does not exist is a
|
|
19
|
+
* misconfiguration, and retrying a typo forever is worse than saying so.
|
|
20
|
+
*/
|
|
21
|
+
function isFatalAuthError(error) {
|
|
22
|
+
const status = error instanceof SdkError ? error.statusCode : void 0;
|
|
23
|
+
return status !== void 0 && status >= 400 && status < 500;
|
|
24
|
+
}
|
|
4
25
|
var AuthManager = class AuthManager {
|
|
5
26
|
static DEFAULT_BASE_URL = "https://templatical.com";
|
|
6
27
|
accessToken = null;
|
|
@@ -172,10 +193,10 @@ const API_ROUTES = {
|
|
|
172
193
|
"templates.export": `${TEMPLATE}/export`,
|
|
173
194
|
"templates.importFromBeefree": `${BASE}/templates/import/from-beefree`,
|
|
174
195
|
"templates.sendTestEmail": `${TEMPLATE}/send-test-email`,
|
|
175
|
-
"
|
|
176
|
-
"
|
|
177
|
-
"
|
|
178
|
-
"
|
|
196
|
+
"versions.index": `${TEMPLATE}/versions`,
|
|
197
|
+
"versions.store": `${TEMPLATE}/versions`,
|
|
198
|
+
"versions.show": `${TEMPLATE}/versions/{version}`,
|
|
199
|
+
"versions.restore": `${TEMPLATE}/versions/{version}/restore`,
|
|
179
200
|
"comments.index": `${TEMPLATE}/comments`,
|
|
180
201
|
"comments.store": `${TEMPLATE}/comments`,
|
|
181
202
|
"comments.update": `${TEMPLATE}/comments/{comment}`,
|
|
@@ -249,10 +270,13 @@ var ApiClient = class {
|
|
|
249
270
|
}
|
|
250
271
|
return error.message;
|
|
251
272
|
}
|
|
252
|
-
async createTemplate(content) {
|
|
273
|
+
async createTemplate(content, name) {
|
|
253
274
|
return this.request(buildUrl(API_ROUTES["templates.store"], this.baseParams), {
|
|
254
275
|
method: "POST",
|
|
255
|
-
body: JSON.stringify(
|
|
276
|
+
body: JSON.stringify(name !== void 0 ? {
|
|
277
|
+
content,
|
|
278
|
+
name
|
|
279
|
+
} : { content })
|
|
256
280
|
});
|
|
257
281
|
}
|
|
258
282
|
async getTemplate(id) {
|
|
@@ -261,22 +285,31 @@ var ApiClient = class {
|
|
|
261
285
|
template: id
|
|
262
286
|
}));
|
|
263
287
|
}
|
|
264
|
-
|
|
288
|
+
/**
|
|
289
|
+
* Apply a partial update. Takes a patch rather than bare content so a rename
|
|
290
|
+
* can travel without content and vice versa — the shape `TemplatePatch`
|
|
291
|
+
* defines, which is what `createCloudTemplatesProvider` forwards verbatim. Only
|
|
292
|
+
* the keys present are sent.
|
|
293
|
+
*/
|
|
294
|
+
async updateTemplate(id, patch) {
|
|
265
295
|
return this.request(buildUrl(API_ROUTES["templates.update"], {
|
|
266
296
|
...this.baseParams,
|
|
267
297
|
template: id
|
|
268
298
|
}), {
|
|
269
299
|
method: "PUT",
|
|
270
|
-
body: JSON.stringify(
|
|
300
|
+
body: JSON.stringify(patch)
|
|
271
301
|
});
|
|
272
302
|
}
|
|
273
|
-
async
|
|
274
|
-
return this.request(buildUrl(API_ROUTES["
|
|
303
|
+
async createVersion(templateId, content, label) {
|
|
304
|
+
return this.request(buildUrl(API_ROUTES["versions.store"], {
|
|
275
305
|
...this.baseParams,
|
|
276
306
|
template: templateId
|
|
277
307
|
}), {
|
|
278
308
|
method: "POST",
|
|
279
|
-
body: JSON.stringify(
|
|
309
|
+
body: JSON.stringify(label !== void 0 ? {
|
|
310
|
+
content,
|
|
311
|
+
label
|
|
312
|
+
} : { content })
|
|
280
313
|
});
|
|
281
314
|
}
|
|
282
315
|
async deleteTemplate(id) {
|
|
@@ -285,17 +318,24 @@ var ApiClient = class {
|
|
|
285
318
|
template: id
|
|
286
319
|
}), { method: "DELETE" });
|
|
287
320
|
}
|
|
288
|
-
async
|
|
289
|
-
return this.request(buildUrl(API_ROUTES["
|
|
321
|
+
async getVersions(templateId) {
|
|
322
|
+
return this.request(buildUrl(API_ROUTES["versions.index"], {
|
|
290
323
|
...this.baseParams,
|
|
291
324
|
template: templateId
|
|
292
325
|
}));
|
|
293
326
|
}
|
|
294
|
-
async
|
|
295
|
-
return this.request(buildUrl(API_ROUTES["
|
|
327
|
+
async getVersion(templateId, versionId) {
|
|
328
|
+
return this.request(buildUrl(API_ROUTES["versions.show"], {
|
|
296
329
|
...this.baseParams,
|
|
297
330
|
template: templateId,
|
|
298
|
-
|
|
331
|
+
version: versionId
|
|
332
|
+
}));
|
|
333
|
+
}
|
|
334
|
+
async restoreVersion(templateId, versionId) {
|
|
335
|
+
return this.request(buildUrl(API_ROUTES["versions.restore"], {
|
|
336
|
+
...this.baseParams,
|
|
337
|
+
template: templateId,
|
|
338
|
+
version: versionId
|
|
299
339
|
}), { method: "POST" });
|
|
300
340
|
}
|
|
301
341
|
async exportTemplate(templateId, fontsPayload) {
|
|
@@ -424,7 +464,7 @@ var WebSocketClient = class {
|
|
|
424
464
|
try {
|
|
425
465
|
({default: Pusher} = await import("pusher-js"));
|
|
426
466
|
} catch {
|
|
427
|
-
throw new Error("Cloud features require the optional peer dependency 'pusher-js'.
|
|
467
|
+
throw new Error("[Templatical] Cloud features require the optional peer dependency 'pusher-js'. Please install it.");
|
|
428
468
|
}
|
|
429
469
|
const { host, port, appKey } = this.config;
|
|
430
470
|
const authEndpoint = this.authManager.resolveUrl(buildUrl(API_ROUTES["broadcasting.auth"], {
|
|
@@ -507,251 +547,6 @@ function handleOperation(editor, payload) {
|
|
|
507
547
|
}
|
|
508
548
|
}
|
|
509
549
|
//#endregion
|
|
510
|
-
//#region src/cloud/editor.ts
|
|
511
|
-
function getColumnCount(layout) {
|
|
512
|
-
if (layout === "1") return 1;
|
|
513
|
-
if (layout === "3") return 3;
|
|
514
|
-
return 2;
|
|
515
|
-
}
|
|
516
|
-
function useEditor(options) {
|
|
517
|
-
const api = new ApiClient(options.authManager);
|
|
518
|
-
const state = reactive({
|
|
519
|
-
template: null,
|
|
520
|
-
content: createDefaultTemplateContent(options.defaultFontFamily, options.templateDefaults),
|
|
521
|
-
selectedBlockId: null,
|
|
522
|
-
viewport: "desktop",
|
|
523
|
-
darkMode: false,
|
|
524
|
-
previewMode: false,
|
|
525
|
-
isDirty: false,
|
|
526
|
-
isSaving: false,
|
|
527
|
-
isLoading: false,
|
|
528
|
-
uiTheme: "auto"
|
|
529
|
-
});
|
|
530
|
-
const content = computed({
|
|
531
|
-
get: () => state.content,
|
|
532
|
-
set: (value) => {
|
|
533
|
-
state.content = value;
|
|
534
|
-
state.isDirty = true;
|
|
535
|
-
}
|
|
536
|
-
});
|
|
537
|
-
const selectedBlock = computed(() => {
|
|
538
|
-
if (!state.selectedBlockId) return null;
|
|
539
|
-
return findBlockById(state.content.blocks, state.selectedBlockId);
|
|
540
|
-
});
|
|
541
|
-
const savedBlockIds = computed(() => {
|
|
542
|
-
const ids = /* @__PURE__ */ new Set();
|
|
543
|
-
const blocks = state.template?.content?.blocks;
|
|
544
|
-
if (!blocks) return ids;
|
|
545
|
-
for (const block of blocks) {
|
|
546
|
-
ids.add(block.id);
|
|
547
|
-
if (block.type === "section") for (const column of block.children) for (const child of column) ids.add(child.id);
|
|
548
|
-
}
|
|
549
|
-
return ids;
|
|
550
|
-
});
|
|
551
|
-
function findBlockById(blocks, id) {
|
|
552
|
-
for (const block of blocks) {
|
|
553
|
-
if (block.id === id) return block;
|
|
554
|
-
if (block.type === "section") for (const column of block.children) {
|
|
555
|
-
const found = findBlockById(column, id);
|
|
556
|
-
if (found) return found;
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
return null;
|
|
560
|
-
}
|
|
561
|
-
function findBlockParent(blocks, id, parent = { blocks }) {
|
|
562
|
-
for (let i = 0; i < blocks.length; i++) {
|
|
563
|
-
const block = blocks[i];
|
|
564
|
-
if (block.id === id) return parent;
|
|
565
|
-
if (block.type === "section") for (let colIdx = 0; colIdx < block.children.length; colIdx++) {
|
|
566
|
-
const result = findBlockParent(block.children[colIdx], id, {
|
|
567
|
-
blocks: block.children[colIdx],
|
|
568
|
-
sectionId: block.id,
|
|
569
|
-
columnIndex: colIdx
|
|
570
|
-
});
|
|
571
|
-
if (result) return result;
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
return null;
|
|
575
|
-
}
|
|
576
|
-
function isBlockLocked(blockId) {
|
|
577
|
-
return options.lockedBlocks?.value.has(blockId) ?? false;
|
|
578
|
-
}
|
|
579
|
-
function setContent(newContent, markDirty = true) {
|
|
580
|
-
state.content = newContent;
|
|
581
|
-
if (markDirty) state.isDirty = true;
|
|
582
|
-
}
|
|
583
|
-
function selectBlock(blockId) {
|
|
584
|
-
if (blockId && isBlockLocked(blockId)) return;
|
|
585
|
-
state.selectedBlockId = blockId;
|
|
586
|
-
}
|
|
587
|
-
function setViewport(viewport) {
|
|
588
|
-
state.viewport = viewport;
|
|
589
|
-
}
|
|
590
|
-
function setDarkMode(darkMode) {
|
|
591
|
-
state.darkMode = darkMode;
|
|
592
|
-
}
|
|
593
|
-
function setUiTheme(theme) {
|
|
594
|
-
state.uiTheme = theme;
|
|
595
|
-
}
|
|
596
|
-
function setPreviewMode(previewMode) {
|
|
597
|
-
state.previewMode = previewMode;
|
|
598
|
-
if (previewMode) state.selectedBlockId = null;
|
|
599
|
-
}
|
|
600
|
-
function updateBlock(blockId, updates) {
|
|
601
|
-
if (isBlockLocked(blockId)) return;
|
|
602
|
-
const block = findBlockById(state.content.blocks, blockId);
|
|
603
|
-
if (block) {
|
|
604
|
-
Object.assign(block, updates);
|
|
605
|
-
state.isDirty = true;
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
function updateSettings(updates) {
|
|
609
|
-
state.content.settings = {
|
|
610
|
-
...state.content.settings,
|
|
611
|
-
...updates
|
|
612
|
-
};
|
|
613
|
-
state.isDirty = true;
|
|
614
|
-
}
|
|
615
|
-
function addBlock(block, targetSectionId, columnIndex = 0, index) {
|
|
616
|
-
if (targetSectionId) {
|
|
617
|
-
const section = findBlockById(state.content.blocks, targetSectionId);
|
|
618
|
-
if (section && section.type === "section") {
|
|
619
|
-
section.children[columnIndex] = section.children[columnIndex] || [];
|
|
620
|
-
const targetArray = section.children[columnIndex];
|
|
621
|
-
if (index !== void 0 && index < targetArray.length) targetArray.splice(index, 0, block);
|
|
622
|
-
else targetArray.push(block);
|
|
623
|
-
}
|
|
624
|
-
} else if (index !== void 0 && index < state.content.blocks.length) state.content.blocks.splice(index, 0, block);
|
|
625
|
-
else state.content.blocks.push(block);
|
|
626
|
-
state.isDirty = true;
|
|
627
|
-
}
|
|
628
|
-
function removeBlock(blockId) {
|
|
629
|
-
if (isBlockLocked(blockId)) return;
|
|
630
|
-
const parent = findBlockParent(state.content.blocks, blockId);
|
|
631
|
-
if (parent) {
|
|
632
|
-
const index = parent.blocks.findIndex((b) => b.id === blockId);
|
|
633
|
-
if (index !== -1) {
|
|
634
|
-
parent.blocks.splice(index, 1);
|
|
635
|
-
if (state.selectedBlockId === blockId) state.selectedBlockId = null;
|
|
636
|
-
state.isDirty = true;
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
function moveBlock(blockId, newIndex, targetSectionId, columnIndex = 0) {
|
|
641
|
-
const parent = findBlockParent(state.content.blocks, blockId);
|
|
642
|
-
if (!parent) return;
|
|
643
|
-
const oldIndex = parent.blocks.findIndex((b) => b.id === blockId);
|
|
644
|
-
if (oldIndex === -1) return;
|
|
645
|
-
let targetArray;
|
|
646
|
-
if (targetSectionId) {
|
|
647
|
-
const section = findBlockById(state.content.blocks, targetSectionId);
|
|
648
|
-
if (!section || section.type !== "section") return;
|
|
649
|
-
if (columnIndex < 0 || columnIndex >= getColumnCount(section.columns)) return;
|
|
650
|
-
section.children[columnIndex] = section.children[columnIndex] || [];
|
|
651
|
-
targetArray = section.children[columnIndex];
|
|
652
|
-
} else targetArray = state.content.blocks;
|
|
653
|
-
const [block] = parent.blocks.splice(oldIndex, 1);
|
|
654
|
-
targetArray.splice(newIndex, 0, block);
|
|
655
|
-
state.isDirty = true;
|
|
656
|
-
}
|
|
657
|
-
async function create(content) {
|
|
658
|
-
state.isLoading = true;
|
|
659
|
-
try {
|
|
660
|
-
if (content) state.content = content;
|
|
661
|
-
const template = await api.createTemplate(state.content);
|
|
662
|
-
state.template = template;
|
|
663
|
-
state.isDirty = false;
|
|
664
|
-
return template;
|
|
665
|
-
} catch (error) {
|
|
666
|
-
options.onError?.(error);
|
|
667
|
-
throw error;
|
|
668
|
-
} finally {
|
|
669
|
-
state.isLoading = false;
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
async function load(templateId) {
|
|
673
|
-
state.isLoading = true;
|
|
674
|
-
try {
|
|
675
|
-
const template = await api.getTemplate(templateId);
|
|
676
|
-
state.template = template;
|
|
677
|
-
state.content = template.content;
|
|
678
|
-
state.isDirty = false;
|
|
679
|
-
return template;
|
|
680
|
-
} catch (error) {
|
|
681
|
-
options.onError?.(error);
|
|
682
|
-
throw error;
|
|
683
|
-
} finally {
|
|
684
|
-
state.isLoading = false;
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
async function save() {
|
|
688
|
-
if (!state.template?.id) throw new SdkError("No template loaded. Call create() or load() before saving.");
|
|
689
|
-
state.isSaving = true;
|
|
690
|
-
try {
|
|
691
|
-
const template = await api.updateTemplate(state.template.id, state.content);
|
|
692
|
-
state.template = template;
|
|
693
|
-
state.isDirty = false;
|
|
694
|
-
return template;
|
|
695
|
-
} catch (error) {
|
|
696
|
-
options.onError?.(error);
|
|
697
|
-
throw error;
|
|
698
|
-
} finally {
|
|
699
|
-
state.isSaving = false;
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
async function createSnapshot() {
|
|
703
|
-
if (!state.template?.id) return;
|
|
704
|
-
try {
|
|
705
|
-
await api.createSnapshot(state.template.id, state.content);
|
|
706
|
-
} catch (error) {
|
|
707
|
-
options.onError?.(error);
|
|
708
|
-
throw error;
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
function hasTemplate() {
|
|
712
|
-
return state.template?.id !== void 0;
|
|
713
|
-
}
|
|
714
|
-
function markDirty() {
|
|
715
|
-
state.isDirty = true;
|
|
716
|
-
}
|
|
717
|
-
function findBlockLocation(blockId) {
|
|
718
|
-
const parent = findBlockParent(state.content.blocks, blockId);
|
|
719
|
-
if (!parent) return null;
|
|
720
|
-
const index = parent.blocks.findIndex((b) => b.id === blockId);
|
|
721
|
-
if (index === -1) return null;
|
|
722
|
-
return {
|
|
723
|
-
targetSectionId: parent.sectionId,
|
|
724
|
-
columnIndex: parent.columnIndex,
|
|
725
|
-
index
|
|
726
|
-
};
|
|
727
|
-
}
|
|
728
|
-
return {
|
|
729
|
-
state: readonly(state),
|
|
730
|
-
content,
|
|
731
|
-
selectedBlock,
|
|
732
|
-
savedBlockIds,
|
|
733
|
-
isBlockLocked,
|
|
734
|
-
findBlockLocation,
|
|
735
|
-
setContent,
|
|
736
|
-
selectBlock,
|
|
737
|
-
setViewport,
|
|
738
|
-
setDarkMode,
|
|
739
|
-
setUiTheme,
|
|
740
|
-
setPreviewMode,
|
|
741
|
-
updateBlock,
|
|
742
|
-
updateSettings,
|
|
743
|
-
addBlock,
|
|
744
|
-
removeBlock,
|
|
745
|
-
moveBlock,
|
|
746
|
-
create,
|
|
747
|
-
load,
|
|
748
|
-
save,
|
|
749
|
-
createSnapshot,
|
|
750
|
-
hasTemplate,
|
|
751
|
-
markDirty
|
|
752
|
-
};
|
|
753
|
-
}
|
|
754
|
-
//#endregion
|
|
755
550
|
//#region src/cloud/ai-chat.ts
|
|
756
551
|
let messageIdCounter = 0;
|
|
757
552
|
function generateMessageId() {
|
|
@@ -1370,32 +1165,63 @@ function useDesignReference(options) {
|
|
|
1370
1165
|
};
|
|
1371
1166
|
}
|
|
1372
1167
|
//#endregion
|
|
1373
|
-
//#region src/cloud/comments.ts
|
|
1374
|
-
|
|
1375
|
-
|
|
1168
|
+
//#region src/cloud/comments-provider.ts
|
|
1169
|
+
/** The Pusher event Cloud's backend fans comment writes out on. */
|
|
1170
|
+
const BROADCAST_EVENT = "comment-broadcast";
|
|
1171
|
+
/**
|
|
1172
|
+
* Cloud's wire shape → the contract shape. The whole job of this adapter, plus
|
|
1173
|
+
* auth: snake_case to camelCase, and the two flat `author_*` / `resolved_by_*`
|
|
1174
|
+
* pairs into {@link CommentAuthor} objects.
|
|
1175
|
+
*/
|
|
1176
|
+
function toComment(record) {
|
|
1177
|
+
const comment = {
|
|
1178
|
+
id: record.id,
|
|
1179
|
+
body: record.body,
|
|
1180
|
+
author: {
|
|
1181
|
+
id: record.author_identifier,
|
|
1182
|
+
name: record.author_name
|
|
1183
|
+
},
|
|
1184
|
+
createdAt: record.created_at,
|
|
1185
|
+
blockId: record.block_id,
|
|
1186
|
+
parentId: record.parent_id,
|
|
1187
|
+
resolvedAt: record.resolved_at
|
|
1188
|
+
};
|
|
1189
|
+
if (record.updated_at !== record.created_at) comment.updatedAt = record.updated_at;
|
|
1190
|
+
if (record.resolved_by_identifier) comment.resolvedBy = {
|
|
1191
|
+
id: record.resolved_by_identifier,
|
|
1192
|
+
name: record.resolved_by_name ?? ""
|
|
1193
|
+
};
|
|
1194
|
+
if (record.replies?.length) comment.replies = record.replies.map(toComment);
|
|
1195
|
+
return comment;
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Cloud-backed {@link CommentsProvider} — the Templatical Cloud adapter for the
|
|
1199
|
+
* same review contract consumers implement themselves.
|
|
1200
|
+
*
|
|
1201
|
+
* All four mutations are enabled: comment storage and its realtime fan-out are
|
|
1202
|
+
* what the `commenting` plan feature pays for, so there is no Cloud tier that can
|
|
1203
|
+
* read a thread but not reply to it. A consumer who wants a read-only review
|
|
1204
|
+
* supplies their own provider with the mutations set to `false`.
|
|
1205
|
+
*
|
|
1206
|
+
* Two things about the write payloads are Cloud's alone and stay on this side of
|
|
1207
|
+
* the seam:
|
|
1208
|
+
*
|
|
1209
|
+
* - **The author is signed.** `user_id` / `user_name` / `user_signature` come from
|
|
1210
|
+
* the JWT, not from the editor's `user` config, so a browser cannot attribute a
|
|
1211
|
+
* comment to someone else. The editor's `user` key still gates the feature and
|
|
1212
|
+
* drives "you wrote this" in the UI; the two agree because `initCloud()` fills
|
|
1213
|
+
* `user` from the same JWT.
|
|
1214
|
+
* - **`X-Socket-ID`.** Cloud excludes the originating socket from the broadcast,
|
|
1215
|
+
* which is why a local write and its echo can't double-post. Every implementation
|
|
1216
|
+
* is nonetheless echo-safe (`upsert` replaces by id), so a backend without that
|
|
1217
|
+
* header is fine too.
|
|
1218
|
+
*/
|
|
1219
|
+
function createCloudCommentsProvider(options) {
|
|
1220
|
+
const { authManager, channel, getSocketId } = options;
|
|
1376
1221
|
const api = new ApiClient(authManager);
|
|
1377
|
-
|
|
1378
|
-
const isLoading = ref(false);
|
|
1379
|
-
const isSubmitting = ref(false);
|
|
1380
|
-
const isEnabled = computed(() => {
|
|
1381
|
-
return (hasCommentingFeature?.() ?? false) && authManager.userConfig !== null;
|
|
1382
|
-
});
|
|
1383
|
-
const totalCount = computed(() => {
|
|
1384
|
-
let count = 0;
|
|
1385
|
-
for (const thread of comments.value) count += 1 + (thread.replies?.length ?? 0);
|
|
1386
|
-
return count;
|
|
1387
|
-
});
|
|
1388
|
-
const unresolvedCount = computed(() => {
|
|
1389
|
-
return comments.value.filter((c) => !c.resolved_at).length;
|
|
1390
|
-
});
|
|
1391
|
-
const commentCountByBlock = computed(() => {
|
|
1392
|
-
const map = /* @__PURE__ */ new Map();
|
|
1393
|
-
for (const thread of comments.value) if (thread.block_id) map.set(thread.block_id, (map.get(thread.block_id) ?? 0) + 1 + (thread.replies?.length ?? 0));
|
|
1394
|
-
return map;
|
|
1395
|
-
});
|
|
1396
|
-
function getUserPayload() {
|
|
1222
|
+
function userPayload() {
|
|
1397
1223
|
const user = authManager.userConfig;
|
|
1398
|
-
if (!user) throw new Error("
|
|
1224
|
+
if (!user) throw new Error("[Templatical] Cloud comments need a signed user in the auth token. The project's token endpoint must include a `user` claim.");
|
|
1399
1225
|
return {
|
|
1400
1226
|
user_id: user.id,
|
|
1401
1227
|
user_name: user.name,
|
|
@@ -1404,226 +1230,70 @@ function useComments(options) {
|
|
|
1404
1230
|
}
|
|
1405
1231
|
function socketHeaders() {
|
|
1406
1232
|
const socketId = getSocketId?.();
|
|
1407
|
-
|
|
1408
|
-
return { "X-Socket-ID": socketId };
|
|
1409
|
-
}
|
|
1410
|
-
function emitEvent(type, comment) {
|
|
1411
|
-
onComment?.({
|
|
1412
|
-
type,
|
|
1413
|
-
comment
|
|
1414
|
-
});
|
|
1415
|
-
}
|
|
1416
|
-
function findComment(commentId) {
|
|
1417
|
-
for (const thread of comments.value) {
|
|
1418
|
-
if (thread.id === commentId) return thread;
|
|
1419
|
-
for (const reply of thread.replies ?? []) if (reply.id === commentId) return reply;
|
|
1420
|
-
}
|
|
1421
|
-
return null;
|
|
1422
|
-
}
|
|
1423
|
-
async function loadComments() {
|
|
1424
|
-
const templateId = getTemplateId();
|
|
1425
|
-
if (!templateId) return;
|
|
1426
|
-
isLoading.value = true;
|
|
1427
|
-
try {
|
|
1428
|
-
comments.value = await api.getComments(templateId);
|
|
1429
|
-
} catch (err) {
|
|
1430
|
-
const wrappedError = err instanceof Error ? err : new Error("Failed to load comments", { cause: err });
|
|
1431
|
-
onError?.(wrappedError);
|
|
1432
|
-
} finally {
|
|
1433
|
-
isLoading.value = false;
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
async function addComment(body, blockId, parentId) {
|
|
1437
|
-
const templateId = getTemplateId();
|
|
1438
|
-
if (!templateId) return null;
|
|
1439
|
-
isSubmitting.value = true;
|
|
1440
|
-
try {
|
|
1441
|
-
const comment = await api.createComment(templateId, {
|
|
1442
|
-
body,
|
|
1443
|
-
block_id: blockId,
|
|
1444
|
-
parent_id: parentId,
|
|
1445
|
-
...getUserPayload()
|
|
1446
|
-
}, socketHeaders());
|
|
1447
|
-
if (parentId) {
|
|
1448
|
-
const parent = findComment(parentId);
|
|
1449
|
-
if (parent) parent.replies = [...parent.replies ?? [], comment];
|
|
1450
|
-
} else comments.value = [...comments.value, comment];
|
|
1451
|
-
emitEvent("created", comment);
|
|
1452
|
-
return comment;
|
|
1453
|
-
} catch (err) {
|
|
1454
|
-
const wrappedError = err instanceof Error ? err : new Error("Failed to create comment", { cause: err });
|
|
1455
|
-
onError?.(wrappedError);
|
|
1456
|
-
return null;
|
|
1457
|
-
} finally {
|
|
1458
|
-
isSubmitting.value = false;
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
async function editComment(commentId, body) {
|
|
1462
|
-
const templateId = getTemplateId();
|
|
1463
|
-
if (!templateId) return null;
|
|
1464
|
-
isSubmitting.value = true;
|
|
1465
|
-
try {
|
|
1466
|
-
const updated = await api.updateComment(templateId, commentId, {
|
|
1467
|
-
body,
|
|
1468
|
-
...getUserPayload()
|
|
1469
|
-
}, socketHeaders());
|
|
1470
|
-
updateCommentInState(commentId, updated);
|
|
1471
|
-
emitEvent("updated", updated);
|
|
1472
|
-
return updated;
|
|
1473
|
-
} catch (err) {
|
|
1474
|
-
const wrappedError = err instanceof Error ? err : new Error("Failed to update comment", { cause: err });
|
|
1475
|
-
onError?.(wrappedError);
|
|
1476
|
-
return null;
|
|
1477
|
-
} finally {
|
|
1478
|
-
isSubmitting.value = false;
|
|
1479
|
-
}
|
|
1480
|
-
}
|
|
1481
|
-
async function removeComment(commentId) {
|
|
1482
|
-
const templateId = getTemplateId();
|
|
1483
|
-
if (!templateId) return false;
|
|
1484
|
-
const comment = findComment(commentId);
|
|
1485
|
-
if (!comment) return false;
|
|
1486
|
-
const commentSnapshot = {
|
|
1487
|
-
...comment,
|
|
1488
|
-
replies: [...comment.replies ?? []]
|
|
1489
|
-
};
|
|
1490
|
-
isSubmitting.value = true;
|
|
1491
|
-
try {
|
|
1492
|
-
await api.deleteComment(templateId, commentId, getUserPayload(), socketHeaders());
|
|
1493
|
-
if (comment.parent_id) {
|
|
1494
|
-
const parent = findComment(comment.parent_id);
|
|
1495
|
-
if (parent) parent.replies = (parent.replies ?? []).filter((r) => r.id !== commentId);
|
|
1496
|
-
} else comments.value = comments.value.filter((c) => c.id !== commentId);
|
|
1497
|
-
emitEvent("deleted", commentSnapshot);
|
|
1498
|
-
return true;
|
|
1499
|
-
} catch (err) {
|
|
1500
|
-
const wrappedError = err instanceof Error ? err : new Error("Failed to delete comment", { cause: err });
|
|
1501
|
-
onError?.(wrappedError);
|
|
1502
|
-
return false;
|
|
1503
|
-
} finally {
|
|
1504
|
-
isSubmitting.value = false;
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
|
-
async function toggleResolve(commentId) {
|
|
1508
|
-
const templateId = getTemplateId();
|
|
1509
|
-
if (!templateId) return null;
|
|
1510
|
-
isSubmitting.value = true;
|
|
1511
|
-
try {
|
|
1512
|
-
const updated = await api.resolveComment(templateId, commentId, getUserPayload(), socketHeaders());
|
|
1513
|
-
updateCommentInState(commentId, updated);
|
|
1514
|
-
emitEvent(updated.resolved_at ? "resolved" : "unresolved", updated);
|
|
1515
|
-
return updated;
|
|
1516
|
-
} catch (err) {
|
|
1517
|
-
const wrappedError = err instanceof Error ? err : new Error("Failed to toggle comment resolution", { cause: err });
|
|
1518
|
-
onError?.(wrappedError);
|
|
1519
|
-
return null;
|
|
1520
|
-
} finally {
|
|
1521
|
-
isSubmitting.value = false;
|
|
1522
|
-
}
|
|
1233
|
+
return socketId ? { "X-Socket-ID": socketId } : void 0;
|
|
1523
1234
|
}
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
...comments.value.slice(i + 1)
|
|
1574
|
-
];
|
|
1575
|
-
return;
|
|
1235
|
+
return {
|
|
1236
|
+
async list(templateId) {
|
|
1237
|
+
return (await api.getComments(templateId)).map(toComment);
|
|
1238
|
+
},
|
|
1239
|
+
async create(templateId, input) {
|
|
1240
|
+
return toComment(await api.createComment(templateId, {
|
|
1241
|
+
body: input.body,
|
|
1242
|
+
block_id: input.blockId,
|
|
1243
|
+
parent_id: input.parentId,
|
|
1244
|
+
...userPayload()
|
|
1245
|
+
}, socketHeaders()));
|
|
1246
|
+
},
|
|
1247
|
+
async update(templateId, commentId, patch) {
|
|
1248
|
+
if (patch.body === void 0) throw new Error("[Templatical] Cloud comments: update() needs a `body` in the patch.");
|
|
1249
|
+
return toComment(await api.updateComment(templateId, commentId, {
|
|
1250
|
+
body: patch.body,
|
|
1251
|
+
...userPayload()
|
|
1252
|
+
}, socketHeaders()));
|
|
1253
|
+
},
|
|
1254
|
+
async delete(templateId, commentId) {
|
|
1255
|
+
await api.deleteComment(templateId, commentId, userPayload(), socketHeaders());
|
|
1256
|
+
},
|
|
1257
|
+
async setResolved(templateId, commentId, _resolved) {
|
|
1258
|
+
return toComment(await api.resolveComment(templateId, commentId, userPayload(), socketHeaders()));
|
|
1259
|
+
},
|
|
1260
|
+
subscribe(_templateId, onChange) {
|
|
1261
|
+
function handle(payload) {
|
|
1262
|
+
const comment = toComment(payload.comment);
|
|
1263
|
+
switch (payload.action) {
|
|
1264
|
+
case "comment_created":
|
|
1265
|
+
onChange({
|
|
1266
|
+
type: "created",
|
|
1267
|
+
comment
|
|
1268
|
+
});
|
|
1269
|
+
break;
|
|
1270
|
+
case "comment_deleted":
|
|
1271
|
+
onChange({
|
|
1272
|
+
type: "deleted",
|
|
1273
|
+
commentId: comment.id,
|
|
1274
|
+
parentId: comment.parentId
|
|
1275
|
+
});
|
|
1276
|
+
break;
|
|
1277
|
+
case "comment_updated":
|
|
1278
|
+
case "comment_resolved":
|
|
1279
|
+
case "comment_unresolved": onChange({
|
|
1280
|
+
type: "updated",
|
|
1281
|
+
comment
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1576
1284
|
}
|
|
1285
|
+
const stopWatching = watch(channel, (next, previous) => {
|
|
1286
|
+
previous?.unbind(BROADCAST_EVENT, handle);
|
|
1287
|
+
next?.bind(BROADCAST_EVENT, handle);
|
|
1288
|
+
}, { immediate: true });
|
|
1289
|
+
return () => {
|
|
1290
|
+
stopWatching();
|
|
1291
|
+
channel.value?.unbind(BROADCAST_EVENT, handle);
|
|
1292
|
+
};
|
|
1577
1293
|
}
|
|
1578
|
-
}
|
|
1579
|
-
return {
|
|
1580
|
-
comments,
|
|
1581
|
-
isLoading,
|
|
1582
|
-
isSubmitting,
|
|
1583
|
-
isEnabled,
|
|
1584
|
-
commentCountByBlock,
|
|
1585
|
-
totalCount,
|
|
1586
|
-
unresolvedCount,
|
|
1587
|
-
loadComments,
|
|
1588
|
-
addComment,
|
|
1589
|
-
editComment,
|
|
1590
|
-
removeComment,
|
|
1591
|
-
toggleResolve,
|
|
1592
|
-
applyRemoteCreate,
|
|
1593
|
-
applyRemoteUpdate,
|
|
1594
|
-
applyRemoteDelete
|
|
1595
1294
|
};
|
|
1596
1295
|
}
|
|
1597
1296
|
//#endregion
|
|
1598
|
-
//#region src/cloud/comment-listener.ts
|
|
1599
|
-
function useCommentListener(options) {
|
|
1600
|
-
const { comments, channel } = options;
|
|
1601
|
-
watch(channel, (newChannel, oldChannel) => {
|
|
1602
|
-
if (oldChannel) oldChannel.unbind("comment-broadcast");
|
|
1603
|
-
if (newChannel) newChannel.bind("comment-broadcast", (payload) => {
|
|
1604
|
-
handleCommentBroadcast(comments, payload);
|
|
1605
|
-
});
|
|
1606
|
-
});
|
|
1607
|
-
onScopeDispose(() => {
|
|
1608
|
-
channel.value?.unbind("comment-broadcast");
|
|
1609
|
-
});
|
|
1610
|
-
}
|
|
1611
|
-
function handleCommentBroadcast(comments, payload) {
|
|
1612
|
-
switch (payload.action) {
|
|
1613
|
-
case "comment_created":
|
|
1614
|
-
comments.applyRemoteCreate(payload.comment);
|
|
1615
|
-
break;
|
|
1616
|
-
case "comment_updated":
|
|
1617
|
-
comments.applyRemoteUpdate(payload.comment);
|
|
1618
|
-
break;
|
|
1619
|
-
case "comment_deleted":
|
|
1620
|
-
comments.applyRemoteDelete(payload.comment.id, payload.comment.parent_id);
|
|
1621
|
-
break;
|
|
1622
|
-
case "comment_resolved":
|
|
1623
|
-
case "comment_unresolved": comments.applyRemoteUpdate(payload.comment);
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
//#endregion
|
|
1627
1297
|
//#region src/cloud/collaboration.ts
|
|
1628
1298
|
const COLLAB_EVENTS = [
|
|
1629
1299
|
"pusher:member_added",
|
|
@@ -1950,42 +1620,218 @@ function createCloudSavedBlocksProvider(authManager) {
|
|
|
1950
1620
|
};
|
|
1951
1621
|
}
|
|
1952
1622
|
//#endregion
|
|
1953
|
-
//#region src/cloud/
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1623
|
+
//#region src/cloud/templates-provider.ts
|
|
1624
|
+
/**
|
|
1625
|
+
* How often a save may also record an automatic version. Cloud's storage, Cloud's
|
|
1626
|
+
* number — an editor-side default would be guessing on someone else's behalf.
|
|
1627
|
+
*/
|
|
1628
|
+
const AUTO_VERSION_INTERVAL_MS = 6e4;
|
|
1629
|
+
/**
|
|
1630
|
+
* Cloud-backed {@link TemplatesProvider} — the Templatical Cloud adapter for the
|
|
1631
|
+
* same save/load contract consumers implement themselves.
|
|
1632
|
+
*
|
|
1633
|
+
* Auth (JWT via {@link AuthManager}) and project/tenant scoping live entirely on
|
|
1634
|
+
* this side of the seam; the editor's header, Cmd+S, autosave and
|
|
1635
|
+
* unsaved-changes guard never see them.
|
|
1636
|
+
*
|
|
1637
|
+
* All three methods are enabled: Cloud's template storage is what the plan pays
|
|
1638
|
+
* for, so there is no tier in which a Cloud session can load but not save. A
|
|
1639
|
+
* consumer who wants read-only supplies their own provider with `save: false`.
|
|
1640
|
+
*
|
|
1641
|
+
* **This is also where automatic versions are recorded.** The editor never
|
|
1642
|
+
* creates one on its own — whoever implements `save` decides whether a save also
|
|
1643
|
+
* records a version, which keeps throttling and retention with the side that
|
|
1644
|
+
* pays for the storage. Cloud throttles to one automatic version per
|
|
1645
|
+
* {@link AUTO_VERSION_INTERVAL_MS}, so an autosave firing every few seconds does
|
|
1646
|
+
* not turn history into a keystroke log. This replaced an editor-side
|
|
1647
|
+
* `createSnapshot()` on a timer, which put Cloud's retention policy in the
|
|
1648
|
+
* editor.
|
|
1649
|
+
*
|
|
1650
|
+
* Note `initCloud()` **rejects** a consumer-supplied templates provider, unlike
|
|
1651
|
+
* `savedBlocks` and `testEmail`. Those are inert — nothing keys off them. The
|
|
1652
|
+
* template id is the join key for collaboration, version history, comments, AI
|
|
1653
|
+
* rewrite, scoring and the server-side export, so an id Cloud never issued would
|
|
1654
|
+
* degrade all six silently.
|
|
1655
|
+
*/
|
|
1656
|
+
function createCloudTemplatesProvider(authManager) {
|
|
1657
|
+
const api = new ApiClient(authManager);
|
|
1658
|
+
let lastAutoVersionAt = 0;
|
|
1659
|
+
/**
|
|
1660
|
+
* Fire-and-forget: a version is a convenience on top of a save that already
|
|
1661
|
+
* succeeded, so a failing history endpoint must not turn a successful save
|
|
1662
|
+
* into a failed one. The clock advances before the request, so a slow endpoint
|
|
1663
|
+
* can't let a burst of saves queue several at once.
|
|
1664
|
+
*
|
|
1665
|
+
* Logged rather than swallowed: history quietly not filling up is exactly the
|
|
1666
|
+
* kind of failure nobody notices until they need a version that was never
|
|
1667
|
+
* recorded.
|
|
1668
|
+
*/
|
|
1669
|
+
function maybeRecordVersion(templateId, content) {
|
|
1670
|
+
const now = Date.now();
|
|
1671
|
+
if (now - lastAutoVersionAt < AUTO_VERSION_INTERVAL_MS) return;
|
|
1672
|
+
lastAutoVersionAt = now;
|
|
1673
|
+
api.createVersion(templateId, content).catch((error) => {
|
|
1674
|
+
console.warn("[Templatical] Automatic version not recorded — the save itself succeeded:", error);
|
|
1675
|
+
});
|
|
1969
1676
|
}
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1677
|
+
return {
|
|
1678
|
+
load(id) {
|
|
1679
|
+
return api.getTemplate(id);
|
|
1680
|
+
},
|
|
1681
|
+
create(input) {
|
|
1682
|
+
return api.createTemplate(input.content, input.name);
|
|
1683
|
+
},
|
|
1684
|
+
async save(id, patch) {
|
|
1685
|
+
const template = await api.updateTemplate(id, patch);
|
|
1686
|
+
if (patch.content) maybeRecordVersion(id, patch.content);
|
|
1975
1687
|
return template;
|
|
1976
|
-
} catch (error) {
|
|
1977
|
-
options.onError?.(error);
|
|
1978
|
-
throw error;
|
|
1979
|
-
} finally {
|
|
1980
|
-
isRestoring.value = false;
|
|
1981
1688
|
}
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
//#endregion
|
|
1692
|
+
//#region src/cloud/export.ts
|
|
1693
|
+
/**
|
|
1694
|
+
* Flatten a {@link FontsConfig} into the export payload.
|
|
1695
|
+
*
|
|
1696
|
+
* Unconditional: gating fonts by entitlement meters no resource Cloud buys, and
|
|
1697
|
+
* would only make the paid tier render fewer fonts than the free editor.
|
|
1698
|
+
*/
|
|
1699
|
+
function resolveExportFonts(fonts) {
|
|
1700
|
+
return {
|
|
1701
|
+
customFonts: fonts?.customFonts ?? [],
|
|
1702
|
+
defaultFallback: fonts?.defaultFallback ?? "Arial, sans-serif"
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
/**
|
|
1706
|
+
* Cloud's server-side export endpoint, as a plain API wrapper.
|
|
1707
|
+
*
|
|
1708
|
+
* Both calls render from the **stored** template, so callers save first when the
|
|
1709
|
+
* canvas may have moved on — see `createCloudRenderProvider`, which is where that
|
|
1710
|
+
* policy lives.
|
|
1711
|
+
*/
|
|
1712
|
+
function useExport(options) {
|
|
1713
|
+
const api = new ApiClient(options.authManager);
|
|
1714
|
+
async function exportHtml(templateId, fonts) {
|
|
1715
|
+
const result = await api.exportTemplate(templateId, fonts);
|
|
1716
|
+
return {
|
|
1717
|
+
html: result.html,
|
|
1718
|
+
mjml: result.mjml
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
async function getMjmlSource(templateId, fonts) {
|
|
1722
|
+
return (await api.exportTemplate(templateId, fonts)).mjml;
|
|
1982
1723
|
}
|
|
1983
1724
|
return {
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1725
|
+
exportHtml,
|
|
1726
|
+
getMjmlSource
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
//#endregion
|
|
1730
|
+
//#region src/cloud/render-provider.ts
|
|
1731
|
+
/**
|
|
1732
|
+
* Templatical Cloud's renderer, shaped as a {@link RenderProvider} so it plugs
|
|
1733
|
+
* into the same editor seam a consumer's own backend would.
|
|
1734
|
+
*
|
|
1735
|
+
* **Why Cloud renders server-side at all**, rather than running the bundled
|
|
1736
|
+
* renderer like an OSS consumer: its output is a deliberate *superset* that a
|
|
1737
|
+
* browser cannot produce. A countdown block resolves to a URL serving a live,
|
|
1738
|
+
* on-demand animated GIF; a video block gets a composited play button. Both are
|
|
1739
|
+
* injected into the published renderer through `blockRenderers` in Cloud's Node
|
|
1740
|
+
* sidecar, so the delta is two functions and parity on the other twelve block
|
|
1741
|
+
* types holds by construction.
|
|
1742
|
+
*
|
|
1743
|
+
* Two consequences of the endpoint rendering the **stored** template:
|
|
1744
|
+
*
|
|
1745
|
+
* - **Every render saves first.** The same trade the test-email adapter makes, for
|
|
1746
|
+
* the same reason: exporting a stale version of what is on screen is worse than
|
|
1747
|
+
* a write the caller did not ask for. It also means `toMjml()` needs a template
|
|
1748
|
+
* to exist — a Cloud session that never created one gets a clear rejection
|
|
1749
|
+
* rather than an export of nothing.
|
|
1750
|
+
* - **`payload.content` is ignored**, along with the custom-block `renderedHtml`
|
|
1751
|
+
* the editor pre-rendered into it. Cloud's `save()` persists that same content
|
|
1752
|
+
* immediately before, so the server reads it from storage rather than trusting an
|
|
1753
|
+
* echo. `payload.fonts` *is* read, because it is the only place the editor's
|
|
1754
|
+
* effective font set is expressed. Don't "fix" the content branch.
|
|
1755
|
+
*
|
|
1756
|
+
* `compileMjml` is deliberately absent: `toMjml` and `toHtml` are both whole-
|
|
1757
|
+
* pipeline calls here, so there is no MJML the editor would hand back for
|
|
1758
|
+
* compiling.
|
|
1759
|
+
*/
|
|
1760
|
+
function createCloudRenderProvider(options) {
|
|
1761
|
+
const exporter = useExport({ authManager: options.authManager });
|
|
1762
|
+
function exportFonts(payload) {
|
|
1763
|
+
return {
|
|
1764
|
+
customFonts: payload.fonts?.customFonts ?? [],
|
|
1765
|
+
defaultFallback: payload.fonts?.defaultFallback ?? "Arial, sans-serif"
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
async function saveThenResolveId() {
|
|
1769
|
+
if (options.getTemplateId() === null) throw new Error("[Templatical] Cloud renders from the saved template, so one must exist first. Call create() or load() before toMjml() / toHtml().");
|
|
1770
|
+
return (await options.save()).id;
|
|
1771
|
+
}
|
|
1772
|
+
return {
|
|
1773
|
+
async toMjml(payload) {
|
|
1774
|
+
const fonts = exportFonts(payload);
|
|
1775
|
+
const templateId = await saveThenResolveId();
|
|
1776
|
+
return exporter.getMjmlSource(templateId, fonts);
|
|
1777
|
+
},
|
|
1778
|
+
async toHtml(payload) {
|
|
1779
|
+
const fonts = exportFonts(payload);
|
|
1780
|
+
const templateId = await saveThenResolveId();
|
|
1781
|
+
const { html } = await exporter.exportHtml(templateId, fonts);
|
|
1782
|
+
return html;
|
|
1783
|
+
}
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
//#endregion
|
|
1787
|
+
//#region src/cloud/version-history-provider.ts
|
|
1788
|
+
/**
|
|
1789
|
+
* Cloud's wire shape → the contract shape. The only real work this adapter does:
|
|
1790
|
+
* snake_case to camelCase, and `is_autosave` to `isAutomatic`.
|
|
1791
|
+
*
|
|
1792
|
+
* `content` is carried through deliberately — Cloud's index endpoint already
|
|
1793
|
+
* returns it, so every listed version arrives hydrated and the editor never
|
|
1794
|
+
* calls `get` while scrubbing. See {@link TemplateVersion.content}: that is the
|
|
1795
|
+
* hint working as intended, not a shortcut. `get` is implemented anyway, because
|
|
1796
|
+
* it is the contract's operation and Cloud is one implementation of it like any
|
|
1797
|
+
* other.
|
|
1798
|
+
*/
|
|
1799
|
+
function toVersion(record) {
|
|
1800
|
+
return {
|
|
1801
|
+
id: record.id,
|
|
1802
|
+
createdAt: record.created_at,
|
|
1803
|
+
isAutomatic: record.is_autosave,
|
|
1804
|
+
content: record.content
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* Cloud-backed {@link VersionHistoryProvider} — the Templatical Cloud adapter for
|
|
1809
|
+
* the same contract consumers implement themselves.
|
|
1810
|
+
*
|
|
1811
|
+
* Both mutations are enabled: version storage is what the plan pays for, so
|
|
1812
|
+
* there is no Cloud tier that can list history but not restore it. A consumer
|
|
1813
|
+
* who wants read-only history supplies their own provider with `restore: false`.
|
|
1814
|
+
*
|
|
1815
|
+
* **Automatic versions are not created here.** They are recorded by
|
|
1816
|
+
* `createCloudTemplatesProvider`'s `save`, because the contract puts that
|
|
1817
|
+
* decision on whoever implements `save` — the side that knows the storage cost.
|
|
1818
|
+
* `create` here is for a version a person asked for.
|
|
1819
|
+
*/
|
|
1820
|
+
function createCloudVersionHistoryProvider(authManager) {
|
|
1821
|
+
const api = new ApiClient(authManager);
|
|
1822
|
+
return {
|
|
1823
|
+
async list(templateId) {
|
|
1824
|
+
return { versions: (await api.getVersions(templateId)).map(toVersion) };
|
|
1825
|
+
},
|
|
1826
|
+
async get(templateId, versionId) {
|
|
1827
|
+
return (await api.getVersion(templateId, versionId)).content;
|
|
1828
|
+
},
|
|
1829
|
+
async create(templateId, content, meta) {
|
|
1830
|
+
return toVersion(await api.createVersion(templateId, content, meta?.label));
|
|
1831
|
+
},
|
|
1832
|
+
restore(templateId, versionId) {
|
|
1833
|
+
return api.restoreVersion(templateId, versionId);
|
|
1834
|
+
}
|
|
1989
1835
|
};
|
|
1990
1836
|
}
|
|
1991
1837
|
//#endregion
|
|
@@ -2055,35 +1901,6 @@ function createCloudTestEmailProvider(options) {
|
|
|
2055
1901
|
};
|
|
2056
1902
|
}
|
|
2057
1903
|
//#endregion
|
|
2058
|
-
//#region src/cloud/export.ts
|
|
2059
|
-
function useExport(options) {
|
|
2060
|
-
const { authManager, getFontsConfig, canUseCustomFonts } = options;
|
|
2061
|
-
const api = new ApiClient(authManager);
|
|
2062
|
-
function getExportFontsPayload() {
|
|
2063
|
-
const fontsConfig = getFontsConfig?.();
|
|
2064
|
-
return {
|
|
2065
|
-
customFonts: (canUseCustomFonts?.() ?? true) && fontsConfig?.customFonts ? fontsConfig.customFonts : [],
|
|
2066
|
-
defaultFallback: fontsConfig?.defaultFallback ?? "Arial, sans-serif"
|
|
2067
|
-
};
|
|
2068
|
-
}
|
|
2069
|
-
async function exportHtml(templateId) {
|
|
2070
|
-
const fontsPayload = getExportFontsPayload();
|
|
2071
|
-
const result = await api.exportTemplate(templateId, fontsPayload);
|
|
2072
|
-
return {
|
|
2073
|
-
html: result.html,
|
|
2074
|
-
mjml: result.mjml
|
|
2075
|
-
};
|
|
2076
|
-
}
|
|
2077
|
-
async function getMjmlSource(templateId) {
|
|
2078
|
-
const fontsPayload = getExportFontsPayload();
|
|
2079
|
-
return (await api.exportTemplate(templateId, fontsPayload)).mjml;
|
|
2080
|
-
}
|
|
2081
|
-
return {
|
|
2082
|
-
exportHtml,
|
|
2083
|
-
getMjmlSource
|
|
2084
|
-
};
|
|
2085
|
-
}
|
|
2086
|
-
//#endregion
|
|
2087
1904
|
//#region src/cloud/plan-config.ts
|
|
2088
1905
|
function usePlanConfig(options) {
|
|
2089
1906
|
const { authManager, onError } = options;
|
|
@@ -2239,6 +2056,6 @@ function useMcpListener(options) {
|
|
|
2239
2056
|
});
|
|
2240
2057
|
}
|
|
2241
2058
|
//#endregion
|
|
2242
|
-
export { API_ROUTES, ApiClient, AuthManager, WebSocketClient, buildUrl, createCloudSavedBlocksProvider, createCloudTestEmailProvider, createSdkAuthManager, handleOperation, performHealthCheck, resolveWebSocketConfig, useAiChat, useAiConfig, useAiRewrite, useCollaboration, useCollaborationBroadcast,
|
|
2059
|
+
export { API_ROUTES, ApiClient, AuthManager, WebSocketClient, buildUrl, createCloudCommentsProvider, createCloudRenderProvider, createCloudSavedBlocksProvider, createCloudTemplatesProvider, createCloudTestEmailProvider, createCloudVersionHistoryProvider, createSdkAuthManager, handleOperation, isFatalAuthError, performHealthCheck, resolveExportFonts, resolveWebSocketConfig, useAiChat, useAiConfig, useAiRewrite, useCollaboration, useCollaborationBroadcast, useDesignReference, useExport, useMcpListener, usePlanConfig, useTemplateScoring, useTestEmail, useWebSocket };
|
|
2243
2060
|
|
|
2244
2061
|
//# sourceMappingURL=index.js.map
|