@templatical/core 0.26.3 → 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/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SdkError, createBlock, createDefaultTemplateContent, generateId, safeClone } from "@templatical/types";
|
|
2
2
|
import { computed, reactive, readonly, ref, watch } from "@vue/reactivity";
|
|
3
|
-
import { computed as computed$1, ref as ref$1 } from "vue";
|
|
3
|
+
import { computed as computed$1, onScopeDispose, ref as ref$1, watch as watch$1 } from "vue";
|
|
4
4
|
//#region src/editor.ts
|
|
5
5
|
function getColumnCount(layout) {
|
|
6
6
|
if (layout === "1") return 1;
|
|
@@ -9,19 +9,33 @@ function getColumnCount(layout) {
|
|
|
9
9
|
}
|
|
10
10
|
function useEditor(options) {
|
|
11
11
|
const state = reactive({
|
|
12
|
+
template: null,
|
|
12
13
|
content: options.content ?? createDefaultTemplateContent(options.defaultFontFamily, options.templateDefaults),
|
|
13
14
|
selectedBlockId: null,
|
|
14
15
|
viewport: "desktop",
|
|
15
16
|
darkMode: false,
|
|
16
17
|
previewMode: false,
|
|
17
18
|
isDirty: false,
|
|
19
|
+
isSaving: false,
|
|
20
|
+
isLoading: false,
|
|
18
21
|
uiTheme: "auto"
|
|
19
22
|
});
|
|
23
|
+
/**
|
|
24
|
+
* Bumped by every content mutation. `save()` captures it before awaiting the
|
|
25
|
+
* provider and only clears `isDirty` if it is unchanged afterwards, so an edit
|
|
26
|
+
* made while a save is in flight is not reported as persisted — which would
|
|
27
|
+
* also make autosave skip it, since that decides dirtiness at debounce time.
|
|
28
|
+
*/
|
|
29
|
+
let revision = 0;
|
|
30
|
+
function touch() {
|
|
31
|
+
state.isDirty = true;
|
|
32
|
+
revision++;
|
|
33
|
+
}
|
|
20
34
|
const content = computed({
|
|
21
35
|
get: () => state.content,
|
|
22
36
|
set: (value) => {
|
|
23
37
|
state.content = value;
|
|
24
|
-
|
|
38
|
+
touch();
|
|
25
39
|
}
|
|
26
40
|
});
|
|
27
41
|
const selectedBlock = computed(() => {
|
|
@@ -73,7 +87,7 @@ function useEditor(options) {
|
|
|
73
87
|
}
|
|
74
88
|
function setContent(newContent, markDirty = true) {
|
|
75
89
|
state.content = newContent;
|
|
76
|
-
if (markDirty)
|
|
90
|
+
if (markDirty) touch();
|
|
77
91
|
}
|
|
78
92
|
function selectBlock(blockId) {
|
|
79
93
|
if (blockId && isBlockLocked(blockId)) return;
|
|
@@ -97,7 +111,7 @@ function useEditor(options) {
|
|
|
97
111
|
const block = findBlockById(state.content.blocks, blockId);
|
|
98
112
|
if (block) {
|
|
99
113
|
Object.assign(block, updates);
|
|
100
|
-
|
|
114
|
+
touch();
|
|
101
115
|
}
|
|
102
116
|
}
|
|
103
117
|
function updateSettings(updates) {
|
|
@@ -105,7 +119,7 @@ function useEditor(options) {
|
|
|
105
119
|
...state.content.settings,
|
|
106
120
|
...updates
|
|
107
121
|
};
|
|
108
|
-
|
|
122
|
+
touch();
|
|
109
123
|
}
|
|
110
124
|
function addBlock(block, targetSectionId, columnIndex = 0, index) {
|
|
111
125
|
if (targetSectionId && block.type === "section") return;
|
|
@@ -121,7 +135,7 @@ function useEditor(options) {
|
|
|
121
135
|
}
|
|
122
136
|
} else if (index !== void 0 && index < state.content.blocks.length) state.content.blocks.splice(index, 0, block);
|
|
123
137
|
else state.content.blocks.push(block);
|
|
124
|
-
|
|
138
|
+
touch();
|
|
125
139
|
}
|
|
126
140
|
function removeBlock(blockId) {
|
|
127
141
|
if (isBlockLocked(blockId)) return;
|
|
@@ -135,7 +149,7 @@ function useEditor(options) {
|
|
|
135
149
|
collectBlockIds(removed, removedIds);
|
|
136
150
|
if (removedIds.has(state.selectedBlockId)) state.selectedBlockId = null;
|
|
137
151
|
}
|
|
138
|
-
|
|
152
|
+
touch();
|
|
139
153
|
}
|
|
140
154
|
}
|
|
141
155
|
}
|
|
@@ -157,10 +171,105 @@ function useEditor(options) {
|
|
|
157
171
|
} else targetArray = state.content.blocks;
|
|
158
172
|
const [block] = parent.blocks.splice(oldIndex, 1);
|
|
159
173
|
targetArray.splice(newIndex, 0, block);
|
|
160
|
-
|
|
174
|
+
touch();
|
|
161
175
|
}
|
|
162
176
|
function markDirty() {
|
|
163
|
-
|
|
177
|
+
touch();
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* A refused call throws rather than resolving, because a resolved promise
|
|
181
|
+
* would read as "saved" to whoever awaited it.
|
|
182
|
+
*
|
|
183
|
+
* The message names the provider key the consumer set, not an internal
|
|
184
|
+
* capability flag: `EditorCapabilities` is type-only and its injection key is
|
|
185
|
+
* unexported, so neither a core nor an editor consumer can read one. Note the
|
|
186
|
+
* editor hides its own save controls when `save` is withheld but has no
|
|
187
|
+
* create affordance to hide at all, so `create()` is reachable by definition.
|
|
188
|
+
*/
|
|
189
|
+
function refuse(action, reason) {
|
|
190
|
+
throw new SdkError(`[Templatical] Templates: ${action} ${reason}`);
|
|
191
|
+
}
|
|
192
|
+
function requireProvider(action) {
|
|
193
|
+
const { templates } = options;
|
|
194
|
+
if (!templates) refuse(action, "needs a templates provider. Pass one as `init({ templates })` to enable saving and loading.");
|
|
195
|
+
return templates;
|
|
196
|
+
}
|
|
197
|
+
function setName(name) {
|
|
198
|
+
if (!state.template) return;
|
|
199
|
+
state.template = {
|
|
200
|
+
...state.template,
|
|
201
|
+
name
|
|
202
|
+
};
|
|
203
|
+
touch();
|
|
204
|
+
}
|
|
205
|
+
async function create(input) {
|
|
206
|
+
const { create: providerCreate } = requireProvider("create()");
|
|
207
|
+
if (typeof providerCreate !== "function") refuse("create()", "is disabled by the provider — its `create` is `false`.");
|
|
208
|
+
state.isLoading = true;
|
|
209
|
+
try {
|
|
210
|
+
if (input?.content) state.content = input.content;
|
|
211
|
+
const revisionAtRequest = revision;
|
|
212
|
+
const template = await providerCreate(input?.name !== void 0 ? {
|
|
213
|
+
name: input.name,
|
|
214
|
+
content: state.content
|
|
215
|
+
} : { content: state.content });
|
|
216
|
+
state.template = template;
|
|
217
|
+
if (revision === revisionAtRequest) state.isDirty = false;
|
|
218
|
+
return template;
|
|
219
|
+
} catch (error) {
|
|
220
|
+
options.onError?.(error);
|
|
221
|
+
throw error;
|
|
222
|
+
} finally {
|
|
223
|
+
state.isLoading = false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async function load(templateId) {
|
|
227
|
+
const provider = requireProvider("load()");
|
|
228
|
+
state.isLoading = true;
|
|
229
|
+
try {
|
|
230
|
+
const template = await provider.load(templateId);
|
|
231
|
+
state.template = template;
|
|
232
|
+
state.content = template.content;
|
|
233
|
+
state.isDirty = false;
|
|
234
|
+
return template;
|
|
235
|
+
} catch (error) {
|
|
236
|
+
options.onError?.(error);
|
|
237
|
+
throw error;
|
|
238
|
+
} finally {
|
|
239
|
+
state.isLoading = false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
async function save() {
|
|
243
|
+
const { save: providerSave } = requireProvider("save()");
|
|
244
|
+
if (typeof providerSave !== "function") refuse("save()", "is disabled by the provider — its `save` is `false`.");
|
|
245
|
+
const current = state.template;
|
|
246
|
+
if (!current) refuse("save()", "has no template loaded. Call create() or load() first.");
|
|
247
|
+
const patch = current.name !== void 0 ? {
|
|
248
|
+
name: current.name,
|
|
249
|
+
content: state.content
|
|
250
|
+
} : { content: state.content };
|
|
251
|
+
state.isSaving = true;
|
|
252
|
+
try {
|
|
253
|
+
const revisionAtRequest = revision;
|
|
254
|
+
const template = await providerSave(current.id, patch);
|
|
255
|
+
if (state.template?.id === current.id) {
|
|
256
|
+
const localName = state.template.name;
|
|
257
|
+
state.template = localName !== current.name ? {
|
|
258
|
+
...template,
|
|
259
|
+
name: localName
|
|
260
|
+
} : template;
|
|
261
|
+
}
|
|
262
|
+
if (revision === revisionAtRequest) state.isDirty = false;
|
|
263
|
+
return template;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
options.onError?.(error);
|
|
266
|
+
throw error;
|
|
267
|
+
} finally {
|
|
268
|
+
state.isSaving = false;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function hasTemplate() {
|
|
272
|
+
return state.template?.id !== void 0;
|
|
164
273
|
}
|
|
165
274
|
return {
|
|
166
275
|
state: readonly(state),
|
|
@@ -179,7 +288,12 @@ function useEditor(options) {
|
|
|
179
288
|
removeBlock,
|
|
180
289
|
moveBlock,
|
|
181
290
|
markDirty,
|
|
182
|
-
findBlockLocation
|
|
291
|
+
findBlockLocation,
|
|
292
|
+
setName,
|
|
293
|
+
create,
|
|
294
|
+
load,
|
|
295
|
+
save,
|
|
296
|
+
hasTemplate
|
|
183
297
|
};
|
|
184
298
|
}
|
|
185
299
|
//#endregion
|
|
@@ -344,8 +458,23 @@ function useBlockActions(options) {
|
|
|
344
458
|
}
|
|
345
459
|
//#endregion
|
|
346
460
|
//#region src/auto-save.ts
|
|
461
|
+
/**
|
|
462
|
+
* Trailing debounce, in ms, measured from the *last* content mutation.
|
|
463
|
+
*
|
|
464
|
+
* Typing is not debounced upstream — TipTap's `onUpdate` calls `updateBlock` per
|
|
465
|
+
* keystroke — so this is the only thing between a keypress and a whole-document
|
|
466
|
+
* write. 1000 was too eager: ordinary prose pauses for a second constantly
|
|
467
|
+
* (word choice, re-reading, reaching for the mouse), so a single paragraph could
|
|
468
|
+
* produce dozens of full-content saves. 2000 roughly halves that while still
|
|
469
|
+
* landing well before a user wonders whether their work was kept.
|
|
470
|
+
*
|
|
471
|
+
* **The single default for both entry points.** `initCloud()` used to carry its
|
|
472
|
+
* own copy at 5000 in the editor package; two constants for one setting drifted
|
|
473
|
+
* silently and nothing linked them. Cloud imports this one now.
|
|
474
|
+
*/
|
|
475
|
+
const DEFAULT_AUTO_SAVE_DEBOUNCE_MS = 2e3;
|
|
347
476
|
function useAutoSave(options) {
|
|
348
|
-
const { content, isDirty, onChange, debounce =
|
|
477
|
+
const { content, isDirty, onChange, debounce = DEFAULT_AUTO_SAVE_DEBOUNCE_MS, enabled = true } = options;
|
|
349
478
|
let timeoutId = null;
|
|
350
479
|
let paused = false;
|
|
351
480
|
function isEnabled() {
|
|
@@ -685,6 +814,389 @@ function createLocalStorageSavedBlocksProvider(options = {}) {
|
|
|
685
814
|
};
|
|
686
815
|
}
|
|
687
816
|
//#endregion
|
|
688
|
-
|
|
817
|
+
//#region src/version-history.ts
|
|
818
|
+
/**
|
|
819
|
+
* Reactive state over a {@link VersionHistoryProvider}.
|
|
820
|
+
*
|
|
821
|
+
* Owns the list, the loading flags and the per-version content cache that keeps
|
|
822
|
+
* scrubbing synchronous. Errors are reported through `onError` and re-thrown,
|
|
823
|
+
* leaving the list untouched on failure.
|
|
824
|
+
*
|
|
825
|
+
* It deliberately does **not** create versions of its own accord. The editor
|
|
826
|
+
* never records a version on a save — whoever implements `TemplatesProvider.save`
|
|
827
|
+
* decides that, because they are the side that pays for the storage.
|
|
828
|
+
*/
|
|
829
|
+
function useVersionHistory(options) {
|
|
830
|
+
const { provider, getTemplateId } = options;
|
|
831
|
+
const versions = ref$1([]);
|
|
832
|
+
const isLoading = ref$1(false);
|
|
833
|
+
const nextCursor = ref$1(void 0);
|
|
834
|
+
const isRestoring = ref$1(false);
|
|
835
|
+
/**
|
|
836
|
+
* Content fetched through `get`, keyed by version id. A version that carried
|
|
837
|
+
* a `content` hint never lands here — the hint is read straight off the entry,
|
|
838
|
+
* so a provider that eager-loads pays no second copy.
|
|
839
|
+
*/
|
|
840
|
+
const fetched = /* @__PURE__ */ new Map();
|
|
841
|
+
const canCreate = computed$1(() => typeof provider.create === "function");
|
|
842
|
+
const canRestore = computed$1(() => typeof provider.restore === "function");
|
|
843
|
+
/**
|
|
844
|
+
* The UI hides disabled actions and renders nothing before a template exists,
|
|
845
|
+
* so reaching one of these means a programmatic caller went around it. Fail
|
|
846
|
+
* loudly rather than silently no-op: a resolved promise reads as "done".
|
|
847
|
+
*/
|
|
848
|
+
function requireTemplateId(action) {
|
|
849
|
+
const templateId = getTemplateId();
|
|
850
|
+
if (!templateId) throw new SdkError(`[Templatical] Version history: ${action} needs a template. Call create() or load() first.`);
|
|
851
|
+
return templateId;
|
|
852
|
+
}
|
|
853
|
+
function refuse(action) {
|
|
854
|
+
throw new SdkError(`[Templatical] Version history: ${action} is disabled by the provider. Check the capability before calling — the editor's own UI hides the action.`);
|
|
855
|
+
}
|
|
856
|
+
async function load(params) {
|
|
857
|
+
const templateId = requireTemplateId("list");
|
|
858
|
+
isLoading.value = true;
|
|
859
|
+
try {
|
|
860
|
+
const page = await provider.list(templateId, params);
|
|
861
|
+
versions.value = page.versions;
|
|
862
|
+
nextCursor.value = page.nextCursor;
|
|
863
|
+
} catch (error) {
|
|
864
|
+
options.onError?.(error);
|
|
865
|
+
throw error;
|
|
866
|
+
} finally {
|
|
867
|
+
isLoading.value = false;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
function peekContent(version) {
|
|
871
|
+
return version.content ?? fetched.get(version.id) ?? null;
|
|
872
|
+
}
|
|
873
|
+
async function resolveContent(version) {
|
|
874
|
+
const known = peekContent(version);
|
|
875
|
+
if (known) return known;
|
|
876
|
+
const templateId = requireTemplateId("get");
|
|
877
|
+
try {
|
|
878
|
+
const content = await provider.get(templateId, version.id);
|
|
879
|
+
fetched.set(version.id, content);
|
|
880
|
+
return content;
|
|
881
|
+
} catch (error) {
|
|
882
|
+
options.onError?.(error);
|
|
883
|
+
throw error;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
async function create(content, meta) {
|
|
887
|
+
const { create: providerCreate } = provider;
|
|
888
|
+
if (typeof providerCreate !== "function") refuse("create");
|
|
889
|
+
const templateId = requireTemplateId("create");
|
|
890
|
+
try {
|
|
891
|
+
const created = await providerCreate(templateId, content, meta);
|
|
892
|
+
versions.value = [created, ...versions.value];
|
|
893
|
+
return created;
|
|
894
|
+
} catch (error) {
|
|
895
|
+
options.onError?.(error);
|
|
896
|
+
throw error;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
async function restore(versionId) {
|
|
900
|
+
const { restore: providerRestore } = provider;
|
|
901
|
+
if (typeof providerRestore !== "function") refuse("restore");
|
|
902
|
+
const templateId = requireTemplateId("restore");
|
|
903
|
+
isRestoring.value = true;
|
|
904
|
+
try {
|
|
905
|
+
return await providerRestore(templateId, versionId);
|
|
906
|
+
} catch (error) {
|
|
907
|
+
options.onError?.(error);
|
|
908
|
+
throw error;
|
|
909
|
+
} finally {
|
|
910
|
+
isRestoring.value = false;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return {
|
|
914
|
+
nextCursor,
|
|
915
|
+
versions,
|
|
916
|
+
isLoading,
|
|
917
|
+
isRestoring,
|
|
918
|
+
canCreate,
|
|
919
|
+
canRestore,
|
|
920
|
+
load,
|
|
921
|
+
peekContent,
|
|
922
|
+
resolveContent,
|
|
923
|
+
create,
|
|
924
|
+
restore
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
//#endregion
|
|
928
|
+
//#region src/comments.ts
|
|
929
|
+
/**
|
|
930
|
+
* Reactive state over a {@link CommentsProvider}.
|
|
931
|
+
*
|
|
932
|
+
* Owns the thread list, the loading flags, the local mutations that keep the list
|
|
933
|
+
* consistent, and the derived counts the chrome renders. Errors are reported
|
|
934
|
+
* through `onError` and **re-thrown**, leaving the list untouched on failure —
|
|
935
|
+
* the same discipline as `useSavedBlocks` and `useVersionHistory`.
|
|
936
|
+
*
|
|
937
|
+
* Mutations **reject** when the provider withheld them rather than resolving to
|
|
938
|
+
* `null`: a resolved promise reads as "saved" to whoever awaited it. Ask through
|
|
939
|
+
* `canCreate` / `canUpdate` / `canDelete` / `canResolve` first — which is what the
|
|
940
|
+
* editor's own UI does, hiding each action rather than disabling it.
|
|
941
|
+
*/
|
|
942
|
+
function useComments(options) {
|
|
943
|
+
const { provider, getTemplateId, getUser } = options;
|
|
944
|
+
const comments = ref$1([]);
|
|
945
|
+
const isLoading = ref$1(false);
|
|
946
|
+
const isSubmitting = ref$1(false);
|
|
947
|
+
const canCreate = computed$1(() => typeof provider.create === "function");
|
|
948
|
+
const canUpdate = computed$1(() => typeof provider.update === "function");
|
|
949
|
+
const canDelete = computed$1(() => typeof provider.delete === "function");
|
|
950
|
+
const canResolve = computed$1(() => typeof provider.setResolved === "function");
|
|
951
|
+
const totalCount = computed$1(() => comments.value.reduce((sum, thread) => sum + 1 + (thread.replies?.length ?? 0), 0));
|
|
952
|
+
const unresolvedCount = computed$1(() => comments.value.filter((thread) => !thread.resolvedAt).length);
|
|
953
|
+
const commentCountByBlock = computed$1(() => {
|
|
954
|
+
const map = /* @__PURE__ */ new Map();
|
|
955
|
+
for (const thread of comments.value) {
|
|
956
|
+
if (!thread.blockId) continue;
|
|
957
|
+
map.set(thread.blockId, (map.get(thread.blockId) ?? 0) + 1 + (thread.replies?.length ?? 0));
|
|
958
|
+
}
|
|
959
|
+
return map;
|
|
960
|
+
});
|
|
961
|
+
/**
|
|
962
|
+
* The UI hides disabled actions and renders nothing before a template exists,
|
|
963
|
+
* so reaching one of these means a programmatic caller went around it. Fail
|
|
964
|
+
* loudly rather than silently no-op.
|
|
965
|
+
*/
|
|
966
|
+
function requireTemplateId(action) {
|
|
967
|
+
const templateId = getTemplateId();
|
|
968
|
+
if (!templateId) throw new SdkError(`[Templatical] Comments: ${action} needs a template. Call create() or load() first.`);
|
|
969
|
+
return templateId;
|
|
970
|
+
}
|
|
971
|
+
function requireUser(action) {
|
|
972
|
+
const user = getUser();
|
|
973
|
+
if (!user) throw new SdkError(`[Templatical] Comments: ${action} needs to know who is commenting. Pass init({ user: { id, name } }) — an unattributed comment is not written.`);
|
|
974
|
+
return user;
|
|
975
|
+
}
|
|
976
|
+
function refuse(action) {
|
|
977
|
+
throw new SdkError(`[Templatical] Comments: ${action} is disabled by the provider. Check the capability before calling — the editor's own UI hides the action.`);
|
|
978
|
+
}
|
|
979
|
+
function find(commentId) {
|
|
980
|
+
for (const thread of comments.value) {
|
|
981
|
+
if (thread.id === commentId) return thread;
|
|
982
|
+
for (const reply of thread.replies ?? []) if (reply.id === commentId) return reply;
|
|
983
|
+
}
|
|
984
|
+
return null;
|
|
985
|
+
}
|
|
986
|
+
function isOwn(comment) {
|
|
987
|
+
const user = getUser();
|
|
988
|
+
return user !== null && comment.author.id === user.id;
|
|
989
|
+
}
|
|
990
|
+
function emit(type, comment) {
|
|
991
|
+
options.onComment?.({
|
|
992
|
+
type,
|
|
993
|
+
comment
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
function report(error) {
|
|
997
|
+
const wrapped = error instanceof Error ? error : new Error(String(error), { cause: error });
|
|
998
|
+
options.onError?.(wrapped);
|
|
999
|
+
throw wrapped;
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Insert a comment, replacing an entry with the same id rather than duplicating
|
|
1003
|
+
* it. `create` and a subscribe echo of that same create both land here, which is
|
|
1004
|
+
* why a provider does not have to de-duplicate its own broadcasts.
|
|
1005
|
+
*/
|
|
1006
|
+
function replaceAt(list, index, next) {
|
|
1007
|
+
return [
|
|
1008
|
+
...list.slice(0, index),
|
|
1009
|
+
next,
|
|
1010
|
+
...list.slice(index + 1)
|
|
1011
|
+
];
|
|
1012
|
+
}
|
|
1013
|
+
function upsert(comment) {
|
|
1014
|
+
if (comment.parentId) {
|
|
1015
|
+
comments.value = comments.value.map((thread) => {
|
|
1016
|
+
if (thread.id !== comment.parentId) return thread;
|
|
1017
|
+
const replies = thread.replies ?? [];
|
|
1018
|
+
const at = replies.findIndex((reply) => reply.id === comment.id);
|
|
1019
|
+
return {
|
|
1020
|
+
...thread,
|
|
1021
|
+
replies: at === -1 ? [...replies, comment] : replaceAt(replies, at, comment)
|
|
1022
|
+
};
|
|
1023
|
+
});
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
const at = comments.value.findIndex((thread) => thread.id === comment.id);
|
|
1027
|
+
if (at === -1) {
|
|
1028
|
+
comments.value = [...comments.value, comment];
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
comments.value = replaceAt(comments.value, at, {
|
|
1032
|
+
...comment,
|
|
1033
|
+
replies: comment.replies ?? comments.value[at].replies
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
function drop(commentId, parentId) {
|
|
1037
|
+
const existing = find(commentId);
|
|
1038
|
+
const parent = parentId ?? existing?.parentId ?? null;
|
|
1039
|
+
if (parent) comments.value = comments.value.map((thread) => thread.id === parent ? {
|
|
1040
|
+
...thread,
|
|
1041
|
+
replies: (thread.replies ?? []).filter((r) => r.id !== commentId)
|
|
1042
|
+
} : thread);
|
|
1043
|
+
else comments.value = comments.value.filter((c) => c.id !== commentId);
|
|
1044
|
+
return existing;
|
|
1045
|
+
}
|
|
1046
|
+
async function load(params) {
|
|
1047
|
+
const templateId = requireTemplateId("list");
|
|
1048
|
+
isLoading.value = true;
|
|
1049
|
+
try {
|
|
1050
|
+
comments.value = await provider.list(templateId, params);
|
|
1051
|
+
} catch (error) {
|
|
1052
|
+
report(error);
|
|
1053
|
+
} finally {
|
|
1054
|
+
isLoading.value = false;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
async function create(input) {
|
|
1058
|
+
const { create: providerCreate } = provider;
|
|
1059
|
+
if (typeof providerCreate !== "function") refuse("create");
|
|
1060
|
+
const templateId = requireTemplateId("create");
|
|
1061
|
+
requireUser("create");
|
|
1062
|
+
isSubmitting.value = true;
|
|
1063
|
+
try {
|
|
1064
|
+
const created = await providerCreate(templateId, input);
|
|
1065
|
+
upsert(created);
|
|
1066
|
+
emit("created", created);
|
|
1067
|
+
return created;
|
|
1068
|
+
} catch (error) {
|
|
1069
|
+
report(error);
|
|
1070
|
+
} finally {
|
|
1071
|
+
isSubmitting.value = false;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
async function update(commentId, patch) {
|
|
1075
|
+
const { update: providerUpdate } = provider;
|
|
1076
|
+
if (typeof providerUpdate !== "function") refuse("update");
|
|
1077
|
+
const templateId = requireTemplateId("update");
|
|
1078
|
+
requireUser("update");
|
|
1079
|
+
isSubmitting.value = true;
|
|
1080
|
+
try {
|
|
1081
|
+
const updated = await providerUpdate(templateId, commentId, patch);
|
|
1082
|
+
upsert(updated);
|
|
1083
|
+
emit("updated", updated);
|
|
1084
|
+
return updated;
|
|
1085
|
+
} catch (error) {
|
|
1086
|
+
report(error);
|
|
1087
|
+
} finally {
|
|
1088
|
+
isSubmitting.value = false;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
async function remove(commentId) {
|
|
1092
|
+
const { delete: providerDelete } = provider;
|
|
1093
|
+
if (typeof providerDelete !== "function") refuse("delete");
|
|
1094
|
+
const templateId = requireTemplateId("delete");
|
|
1095
|
+
requireUser("delete");
|
|
1096
|
+
const existing = find(commentId);
|
|
1097
|
+
isSubmitting.value = true;
|
|
1098
|
+
try {
|
|
1099
|
+
await providerDelete(templateId, commentId);
|
|
1100
|
+
drop(commentId);
|
|
1101
|
+
if (existing) emit("deleted", existing);
|
|
1102
|
+
} catch (error) {
|
|
1103
|
+
report(error);
|
|
1104
|
+
} finally {
|
|
1105
|
+
isSubmitting.value = false;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
async function setResolved(commentId, resolved) {
|
|
1109
|
+
const { setResolved: providerSetResolved } = provider;
|
|
1110
|
+
if (typeof providerSetResolved !== "function") refuse("setResolved");
|
|
1111
|
+
const templateId = requireTemplateId("setResolved");
|
|
1112
|
+
requireUser("setResolved");
|
|
1113
|
+
isSubmitting.value = true;
|
|
1114
|
+
try {
|
|
1115
|
+
const updated = await providerSetResolved(templateId, commentId, resolved);
|
|
1116
|
+
upsert(updated);
|
|
1117
|
+
emit(updated.resolvedAt ? "resolved" : "unresolved", updated);
|
|
1118
|
+
return updated;
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
report(error);
|
|
1121
|
+
} finally {
|
|
1122
|
+
isSubmitting.value = false;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
function applyRemoteCreate(comment) {
|
|
1126
|
+
upsert(comment);
|
|
1127
|
+
emit("created", comment);
|
|
1128
|
+
}
|
|
1129
|
+
function applyRemoteUpdate(comment) {
|
|
1130
|
+
upsert(comment);
|
|
1131
|
+
emit(comment.resolvedAt ? "resolved" : "updated", comment);
|
|
1132
|
+
}
|
|
1133
|
+
function applyRemoteDelete(commentId, parentId) {
|
|
1134
|
+
const existing = drop(commentId, parentId);
|
|
1135
|
+
if (existing) emit("deleted", existing);
|
|
1136
|
+
}
|
|
1137
|
+
return {
|
|
1138
|
+
comments,
|
|
1139
|
+
isLoading,
|
|
1140
|
+
isSubmitting,
|
|
1141
|
+
canCreate,
|
|
1142
|
+
canUpdate,
|
|
1143
|
+
canDelete,
|
|
1144
|
+
canResolve,
|
|
1145
|
+
totalCount,
|
|
1146
|
+
unresolvedCount,
|
|
1147
|
+
commentCountByBlock,
|
|
1148
|
+
load,
|
|
1149
|
+
create,
|
|
1150
|
+
update,
|
|
1151
|
+
remove,
|
|
1152
|
+
setResolved,
|
|
1153
|
+
find,
|
|
1154
|
+
isOwn,
|
|
1155
|
+
applyRemoteCreate,
|
|
1156
|
+
applyRemoteUpdate,
|
|
1157
|
+
applyRemoteDelete
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Wire {@link CommentsProvider.subscribe} into the three `applyRemote*` paths.
|
|
1162
|
+
*
|
|
1163
|
+
* A no-op when the provider has no `subscribe` — realtime is optional, and
|
|
1164
|
+
* comments without it work exactly the same except that a colleague's comment
|
|
1165
|
+
* only appears on the next read. Nothing here knows about a transport: Cloud's
|
|
1166
|
+
* Pusher channel lives inside `createCloudCommentsProvider`, and an SSE or
|
|
1167
|
+
* long-poll implementation is the same three lines on the consumer's side.
|
|
1168
|
+
*
|
|
1169
|
+
* Re-subscribes when the template id changes, and unsubscribes on scope dispose —
|
|
1170
|
+
* so a session that loads a second template does not keep receiving the first
|
|
1171
|
+
* one's comments.
|
|
1172
|
+
*/
|
|
1173
|
+
function useCommentListener(options) {
|
|
1174
|
+
const { comments, provider, getTemplateId } = options;
|
|
1175
|
+
const { subscribe } = provider;
|
|
1176
|
+
if (typeof subscribe !== "function") return;
|
|
1177
|
+
let unsubscribe = null;
|
|
1178
|
+
function stop() {
|
|
1179
|
+
unsubscribe?.();
|
|
1180
|
+
unsubscribe = null;
|
|
1181
|
+
}
|
|
1182
|
+
watch$1(() => getTemplateId(), (templateId) => {
|
|
1183
|
+
stop();
|
|
1184
|
+
if (!templateId) return;
|
|
1185
|
+
unsubscribe = subscribe(templateId, (change) => {
|
|
1186
|
+
switch (change.type) {
|
|
1187
|
+
case "created":
|
|
1188
|
+
comments.applyRemoteCreate(change.comment);
|
|
1189
|
+
break;
|
|
1190
|
+
case "updated":
|
|
1191
|
+
comments.applyRemoteUpdate(change.comment);
|
|
1192
|
+
break;
|
|
1193
|
+
case "deleted": comments.applyRemoteDelete(change.commentId, change.parentId);
|
|
1194
|
+
}
|
|
1195
|
+
});
|
|
1196
|
+
}, { immediate: true });
|
|
1197
|
+
onScopeDispose(stop);
|
|
1198
|
+
}
|
|
1199
|
+
//#endregion
|
|
1200
|
+
export { DEFAULT_AUTO_SAVE_DEBOUNCE_MS, createLocalStorageSavedBlocksProvider, useAutoSave, useBlockActions, useCommentListener, useComments, useConditionPreview, useDataSourceFetch, useEditor, useHistory, useHistoryInterceptor, useSavedBlocks, useVersionHistory };
|
|
689
1201
|
|
|
690
1202
|
//# sourceMappingURL=index.js.map
|