@mp-lb/mdkit 0.2.3-main.21.1 → 0.2.4-main.25.1
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/collaboration/useMdKitCollaboration.js +304 -18
- package/dist/document/MdKitDocumentToolbar.js +5 -7
- package/dist/document/documentTypes.d.ts +7 -0
- package/dist/markdown/TiptapMarkdownSurface.js +11 -1
- package/dist/versioning/useMdKitDocumentVersions.d.ts +1 -0
- package/dist/versioning/useMdKitDocumentVersions.js +6 -6
- package/docs/index.md +3 -2
- package/package.json +1 -1
|
@@ -1,6 +1,50 @@
|
|
|
1
|
-
import { useEffect, useMemo, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
2
|
import { HocuspocusProvider } from "@hocuspocus/provider";
|
|
3
3
|
import * as Y from "yjs";
|
|
4
|
+
const isCollaborationDebugEnabled = () => {
|
|
5
|
+
if (globalThis.__MDKIT_COLLAB_DEBUG__ ===
|
|
6
|
+
true) {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
if (typeof window === "undefined") {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
return window.localStorage.getItem("mdkit:collab-debug") === "true";
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const stringifyDebugDetails = (details) => {
|
|
20
|
+
const seen = new WeakSet();
|
|
21
|
+
try {
|
|
22
|
+
return JSON.stringify(details, (_key, value) => {
|
|
23
|
+
if (typeof value === "bigint") {
|
|
24
|
+
return value.toString();
|
|
25
|
+
}
|
|
26
|
+
if (!value || typeof value !== "object") {
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
if (seen.has(value)) {
|
|
30
|
+
return "[Circular]";
|
|
31
|
+
}
|
|
32
|
+
seen.add(value);
|
|
33
|
+
return value;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
return JSON.stringify({
|
|
38
|
+
serializationError: error instanceof Error ? error.message : "Unable to serialize details",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const debugCollaboration = (event, details) => {
|
|
43
|
+
if (!isCollaborationDebugEnabled()) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
console.info(`MDKIT_COLLAB_DEBUG ${event} ${stringifyDebugDetails(details)}`);
|
|
47
|
+
};
|
|
4
48
|
const createColorFromId = (id) => {
|
|
5
49
|
let hash = 0;
|
|
6
50
|
for (let index = 0; index < id.length; index += 1) {
|
|
@@ -9,13 +53,84 @@ const createColorFromId = (id) => {
|
|
|
9
53
|
}
|
|
10
54
|
return `hsl(${Math.abs(hash) % 360}, 85%, 55%)`;
|
|
11
55
|
};
|
|
56
|
+
const parsePresence = (clientId, state, localClientId) => {
|
|
57
|
+
if (!state || typeof state !== "object") {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const user = "user" in state ? state.user : state;
|
|
61
|
+
if (!user || typeof user !== "object") {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const { color, id, imageUrl, name } = user;
|
|
65
|
+
if (typeof name !== "string") {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
clientId,
|
|
70
|
+
color: typeof color === "string"
|
|
71
|
+
? color
|
|
72
|
+
: createColorFromId(typeof id === "string" ? id : String(clientId)),
|
|
73
|
+
id: typeof id === "string" ? id : String(clientId),
|
|
74
|
+
imageUrl: typeof imageUrl === "string" ? imageUrl : undefined,
|
|
75
|
+
isLocal: clientId === localClientId,
|
|
76
|
+
name,
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
const summarizeAwarenessState = (clientId, state) => {
|
|
80
|
+
if (!state || typeof state !== "object") {
|
|
81
|
+
return {
|
|
82
|
+
clientId,
|
|
83
|
+
hasState: false,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const user = "user" in state ? state.user : state;
|
|
87
|
+
return {
|
|
88
|
+
clientId,
|
|
89
|
+
hasCursor: "cursor" in state,
|
|
90
|
+
hasState: true,
|
|
91
|
+
hasUser: !!user,
|
|
92
|
+
user,
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
const summarizeParticipants = (participants) => participants.map((participant) => ({
|
|
96
|
+
clientId: participant.clientId,
|
|
97
|
+
id: participant.id,
|
|
98
|
+
isLocal: participant.isLocal,
|
|
99
|
+
name: participant.name,
|
|
100
|
+
}));
|
|
101
|
+
const areParticipantsEqual = (left, right) => left.length === right.length &&
|
|
102
|
+
left.every((leftParticipant, index) => {
|
|
103
|
+
const rightParticipant = right[index];
|
|
104
|
+
return (rightParticipant &&
|
|
105
|
+
leftParticipant.clientId === rightParticipant.clientId &&
|
|
106
|
+
leftParticipant.color === rightParticipant.color &&
|
|
107
|
+
leftParticipant.id === rightParticipant.id &&
|
|
108
|
+
leftParticipant.imageUrl === rightParticipant.imageUrl &&
|
|
109
|
+
leftParticipant.isLocal === rightParticipant.isLocal &&
|
|
110
|
+
leftParticipant.name === rightParticipant.name);
|
|
111
|
+
});
|
|
112
|
+
const setProviderUser = (provider, collaborator) => {
|
|
113
|
+
provider.setAwarenessField("user", {
|
|
114
|
+
color: collaborator.color,
|
|
115
|
+
id: collaborator.id,
|
|
116
|
+
imageUrl: collaborator.imageUrl || undefined,
|
|
117
|
+
name: collaborator.name,
|
|
118
|
+
});
|
|
119
|
+
};
|
|
12
120
|
export const useMdKitCollaboration = (options) => {
|
|
13
121
|
const { collaborator, documentId, enabled = true, endpoint, getToken, resolveRoomName, } = options;
|
|
14
122
|
const [status, setStatus] = useState("disconnected");
|
|
123
|
+
const [participants, setParticipants] = useState([]);
|
|
124
|
+
const [provider, setProvider] = useState(null);
|
|
15
125
|
const normalizedCollaborator = useMemo(() => ({
|
|
16
126
|
...collaborator,
|
|
17
127
|
color: collaborator.color || createColorFromId(collaborator.id),
|
|
18
|
-
}), [
|
|
128
|
+
}), [
|
|
129
|
+
collaborator.color,
|
|
130
|
+
collaborator.id,
|
|
131
|
+
collaborator.imageUrl,
|
|
132
|
+
collaborator.name,
|
|
133
|
+
]);
|
|
19
134
|
const roomName = useMemo(() => {
|
|
20
135
|
if (!documentId) {
|
|
21
136
|
return "";
|
|
@@ -26,53 +141,223 @@ export const useMdKitCollaboration = (options) => {
|
|
|
26
141
|
void roomName;
|
|
27
142
|
return new Y.Doc();
|
|
28
143
|
}, [roomName]);
|
|
29
|
-
const
|
|
144
|
+
const setNextParticipants = useCallback((nextParticipants, source) => {
|
|
145
|
+
setParticipants((currentParticipants) => areParticipantsEqual(currentParticipants, nextParticipants)
|
|
146
|
+
? currentParticipants
|
|
147
|
+
: (() => {
|
|
148
|
+
debugCollaboration("participants_update", {
|
|
149
|
+
nextParticipants: summarizeParticipants(nextParticipants),
|
|
150
|
+
previousParticipants: summarizeParticipants(currentParticipants),
|
|
151
|
+
roomName,
|
|
152
|
+
source,
|
|
153
|
+
});
|
|
154
|
+
return nextParticipants;
|
|
155
|
+
})());
|
|
156
|
+
}, [roomName]);
|
|
157
|
+
const updateParticipantsFromProvider = useCallback((nextProvider, source) => {
|
|
158
|
+
const awareness = nextProvider?.awareness;
|
|
159
|
+
if (!awareness) {
|
|
160
|
+
debugCollaboration("awareness_missing", {
|
|
161
|
+
roomName,
|
|
162
|
+
source,
|
|
163
|
+
});
|
|
164
|
+
setNextParticipants([], source);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const rawStates = Array.from(awareness.getStates());
|
|
168
|
+
const nextParticipants = rawStates
|
|
169
|
+
.map(([clientId, state]) => parsePresence(clientId, state, ydoc.clientID))
|
|
170
|
+
.filter((presence) => Boolean(presence));
|
|
171
|
+
debugCollaboration("awareness_map_read", {
|
|
172
|
+
localClientId: ydoc.clientID,
|
|
173
|
+
parsedParticipants: summarizeParticipants(nextParticipants),
|
|
174
|
+
rawStates: rawStates.map(([clientId, state]) => summarizeAwarenessState(clientId, state)),
|
|
175
|
+
roomName,
|
|
176
|
+
source,
|
|
177
|
+
});
|
|
178
|
+
setNextParticipants(nextParticipants, source);
|
|
179
|
+
}, [roomName, setNextParticipants, ydoc.clientID]);
|
|
180
|
+
useEffect(() => {
|
|
30
181
|
if (!enabled || !documentId || !endpoint) {
|
|
31
|
-
|
|
182
|
+
debugCollaboration("provider_skipped", {
|
|
183
|
+
documentId,
|
|
184
|
+
enabled,
|
|
185
|
+
endpoint,
|
|
186
|
+
roomName,
|
|
187
|
+
});
|
|
188
|
+
setProvider(null);
|
|
189
|
+
setStatus("disconnected");
|
|
190
|
+
setNextParticipants([], "provider_skipped");
|
|
191
|
+
return;
|
|
32
192
|
}
|
|
33
|
-
|
|
193
|
+
debugCollaboration("provider_create", {
|
|
194
|
+
collaborator: normalizedCollaborator,
|
|
195
|
+
documentId,
|
|
196
|
+
endpoint,
|
|
197
|
+
localClientId: ydoc.clientID,
|
|
198
|
+
roomName,
|
|
199
|
+
});
|
|
200
|
+
let nextProvider = null;
|
|
201
|
+
nextProvider = new HocuspocusProvider({
|
|
34
202
|
document: ydoc,
|
|
35
203
|
name: roomName,
|
|
36
|
-
onConnect: () =>
|
|
37
|
-
|
|
204
|
+
onConnect: () => {
|
|
205
|
+
if (nextProvider) {
|
|
206
|
+
setProviderUser(nextProvider, normalizedCollaborator);
|
|
207
|
+
}
|
|
208
|
+
debugCollaboration("provider_connect", {
|
|
209
|
+
localClientId: ydoc.clientID,
|
|
210
|
+
roomName,
|
|
211
|
+
});
|
|
212
|
+
setStatus("connected");
|
|
213
|
+
globalThis.setTimeout(() => {
|
|
214
|
+
updateParticipantsFromProvider(nextProvider, "provider_connect");
|
|
215
|
+
}, 0);
|
|
216
|
+
},
|
|
217
|
+
onDisconnect: () => {
|
|
218
|
+
debugCollaboration("provider_disconnect", {
|
|
219
|
+
localClientId: ydoc.clientID,
|
|
220
|
+
roomName,
|
|
221
|
+
});
|
|
222
|
+
setStatus("disconnected");
|
|
223
|
+
},
|
|
38
224
|
onStatus: ({ status: nextStatus }) => {
|
|
225
|
+
debugCollaboration("provider_status", {
|
|
226
|
+
localClientId: ydoc.clientID,
|
|
227
|
+
roomName,
|
|
228
|
+
status: nextStatus,
|
|
229
|
+
});
|
|
39
230
|
if (nextStatus === "connecting") {
|
|
40
231
|
setStatus("connecting");
|
|
41
232
|
}
|
|
42
233
|
},
|
|
234
|
+
onAwarenessChange: ({ states }) => {
|
|
235
|
+
debugCollaboration("provider_awareness_change", {
|
|
236
|
+
localClientId: ydoc.clientID,
|
|
237
|
+
rawStates: states,
|
|
238
|
+
roomName,
|
|
239
|
+
});
|
|
240
|
+
updateParticipantsFromProvider(nextProvider, "provider_awareness_change");
|
|
241
|
+
},
|
|
43
242
|
token: async () => {
|
|
44
243
|
const token = getToken ? await getToken() : null;
|
|
45
|
-
|
|
244
|
+
debugCollaboration("provider_token", {
|
|
245
|
+
hasToken: !!token,
|
|
246
|
+
roomName,
|
|
247
|
+
source: getToken ? "custom" : "default",
|
|
248
|
+
});
|
|
249
|
+
return token || "notoken";
|
|
46
250
|
},
|
|
47
251
|
url: endpoint,
|
|
48
252
|
});
|
|
49
|
-
|
|
253
|
+
setProvider(nextProvider);
|
|
254
|
+
setStatus("connecting");
|
|
255
|
+
return () => {
|
|
256
|
+
nextProvider?.destroy();
|
|
257
|
+
nextProvider = null;
|
|
258
|
+
setProvider(null);
|
|
259
|
+
setStatus("disconnected");
|
|
260
|
+
setNextParticipants([], "provider_destroy");
|
|
261
|
+
};
|
|
262
|
+
}, [
|
|
263
|
+
documentId,
|
|
264
|
+
enabled,
|
|
265
|
+
endpoint,
|
|
266
|
+
getToken,
|
|
267
|
+
normalizedCollaborator,
|
|
268
|
+
roomName,
|
|
269
|
+
setNextParticipants,
|
|
270
|
+
updateParticipantsFromProvider,
|
|
271
|
+
ydoc,
|
|
272
|
+
]);
|
|
273
|
+
useEffect(() => {
|
|
274
|
+
debugCollaboration("session_options", {
|
|
275
|
+
collaborator: normalizedCollaborator,
|
|
276
|
+
documentId,
|
|
277
|
+
enabled,
|
|
278
|
+
endpoint,
|
|
279
|
+
localClientId: ydoc.clientID,
|
|
280
|
+
roomName,
|
|
281
|
+
});
|
|
282
|
+
}, [
|
|
283
|
+
documentId,
|
|
284
|
+
enabled,
|
|
285
|
+
endpoint,
|
|
286
|
+
normalizedCollaborator,
|
|
287
|
+
roomName,
|
|
288
|
+
ydoc.clientID,
|
|
289
|
+
]);
|
|
290
|
+
useEffect(() => {
|
|
291
|
+
debugCollaboration("session_state", {
|
|
292
|
+
isCollaborating: participants.some((participant) => !participant.isLocal),
|
|
293
|
+
participants: summarizeParticipants(participants),
|
|
294
|
+
roomName,
|
|
295
|
+
status: provider ? status : "disconnected",
|
|
296
|
+
});
|
|
297
|
+
}, [participants, provider, roomName, status]);
|
|
50
298
|
useEffect(() => {
|
|
51
299
|
if (!provider) {
|
|
52
300
|
return;
|
|
53
301
|
}
|
|
302
|
+
debugCollaboration("awareness_set_user", {
|
|
303
|
+
localClientId: ydoc.clientID,
|
|
304
|
+
roomName,
|
|
305
|
+
user: {
|
|
306
|
+
color: normalizedCollaborator.color,
|
|
307
|
+
id: normalizedCollaborator.id,
|
|
308
|
+
imageUrl: normalizedCollaborator.imageUrl || undefined,
|
|
309
|
+
name: normalizedCollaborator.name,
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
setProviderUser(provider, normalizedCollaborator);
|
|
313
|
+
updateParticipantsFromProvider(provider, "awareness_set_user");
|
|
314
|
+
}, [
|
|
315
|
+
normalizedCollaborator,
|
|
316
|
+
provider,
|
|
317
|
+
roomName,
|
|
318
|
+
updateParticipantsFromProvider,
|
|
319
|
+
ydoc.clientID,
|
|
320
|
+
]);
|
|
321
|
+
useEffect(() => {
|
|
322
|
+
const awareness = provider?.awareness;
|
|
323
|
+
if (!awareness) {
|
|
324
|
+
debugCollaboration("awareness_effect_missing", {
|
|
325
|
+
roomName,
|
|
326
|
+
});
|
|
327
|
+
setNextParticipants([], "awareness_effect_missing");
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const handleAwarenessChange = () => updateParticipantsFromProvider(provider, "awareness_change_event");
|
|
331
|
+
awareness.on("change", handleAwarenessChange);
|
|
332
|
+
updateParticipantsFromProvider(provider, "awareness_change_subscribe");
|
|
54
333
|
return () => {
|
|
55
|
-
|
|
334
|
+
awareness.off("change", handleAwarenessChange);
|
|
56
335
|
};
|
|
57
|
-
}, [provider]);
|
|
336
|
+
}, [provider, roomName, setNextParticipants, updateParticipantsFromProvider]);
|
|
58
337
|
useEffect(() => {
|
|
59
338
|
if (!provider) {
|
|
60
339
|
return;
|
|
61
340
|
}
|
|
62
|
-
provider
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
341
|
+
const handleProviderAwareness = () => updateParticipantsFromProvider(provider, "provider_awareness_event");
|
|
342
|
+
provider.on("awarenessChange", handleProviderAwareness);
|
|
343
|
+
provider.on("awarenessUpdate", handleProviderAwareness);
|
|
344
|
+
updateParticipantsFromProvider(provider, "provider_awareness_subscribe");
|
|
345
|
+
return () => {
|
|
346
|
+
provider.off("awarenessChange", handleProviderAwareness);
|
|
347
|
+
provider.off("awarenessUpdate", handleProviderAwareness);
|
|
348
|
+
};
|
|
349
|
+
}, [provider, updateParticipantsFromProvider]);
|
|
69
350
|
return useMemo(() => {
|
|
70
351
|
if (!enabled || !documentId || !endpoint) {
|
|
71
352
|
return null;
|
|
72
353
|
}
|
|
354
|
+
const otherParticipants = participants.filter((participant) => !participant.isLocal);
|
|
73
355
|
return {
|
|
74
356
|
collaborator: normalizedCollaborator,
|
|
75
357
|
document: ydoc,
|
|
358
|
+
isCollaborating: otherParticipants.length > 0,
|
|
359
|
+
otherParticipants,
|
|
360
|
+
participants,
|
|
76
361
|
provider,
|
|
77
362
|
roomName,
|
|
78
363
|
status: provider ? status : "disconnected",
|
|
@@ -82,6 +367,7 @@ export const useMdKitCollaboration = (options) => {
|
|
|
82
367
|
enabled,
|
|
83
368
|
endpoint,
|
|
84
369
|
normalizedCollaborator,
|
|
370
|
+
participants,
|
|
85
371
|
provider,
|
|
86
372
|
roomName,
|
|
87
373
|
status,
|
|
@@ -9,6 +9,7 @@ const formatUpdatedAt = (updatedAt) => {
|
|
|
9
9
|
};
|
|
10
10
|
export const MdKitDocumentToolbar = ({ className, collaboration, document, onOpenConflict, onOpenVersionHistory, showConflictActions = false, versions, }) => {
|
|
11
11
|
const [pendingAction, setPendingAction] = useState(null);
|
|
12
|
+
const otherCollaboratorCount = collaboration?.otherParticipants.length ?? 0;
|
|
12
13
|
const runAction = async (name, action) => {
|
|
13
14
|
setPendingAction(name);
|
|
14
15
|
try {
|
|
@@ -19,6 +20,7 @@ export const MdKitDocumentToolbar = ({ className, collaboration, document, onOpe
|
|
|
19
20
|
}
|
|
20
21
|
};
|
|
21
22
|
const hasVersionHistory = versions?.hasVersioning ?? false;
|
|
23
|
+
const hasActiveCollaborators = collaboration?.isCollaborating ?? false;
|
|
22
24
|
const isBusy = pendingAction !== null || document.saveStatus === "saving";
|
|
23
25
|
const status = document.conflict
|
|
24
26
|
? "Conflict"
|
|
@@ -33,16 +35,12 @@ export const MdKitDocumentToolbar = ({ className, collaboration, document, onOpe
|
|
|
33
35
|
: document.saveStatus === "saved"
|
|
34
36
|
? "Saved"
|
|
35
37
|
: "Idle";
|
|
36
|
-
return (_jsxs("div", { className: joinClassNames("mp-lb-mdkit-document-toolbar", className), "data-conflict": document.conflict ? "true" : undefined, "data-dirty": document.isDirty ? "true" : undefined, "data-save-status": document.saveStatus, "data-status": status.toLowerCase().replace(/\s+/g, "-"), children: [_jsxs("div", { className: "mp-lb-mdkit-document-toolbar-status", children: [_jsx("strong", { children: status }), _jsx("span", { children: formatUpdatedAt(document.updatedAt) }), _jsxs("span", { children: [
|
|
37
|
-
document.conflict ||
|
|
38
|
-
!hasVersionHistory ||
|
|
39
|
-
versions?.isLoading ||
|
|
40
|
-
!onOpenVersionHistory, onClick: () => void runAction("versions", async () => {
|
|
38
|
+
return (_jsxs("div", { className: joinClassNames("mp-lb-mdkit-document-toolbar", className), "data-conflict": document.conflict ? "true" : undefined, "data-dirty": document.isDirty ? "true" : undefined, "data-save-status": document.saveStatus, "data-status": status.toLowerCase().replace(/\s+/g, "-"), children: [_jsxs("div", { className: "mp-lb-mdkit-document-toolbar-status", children: [_jsx("strong", { children: status }), _jsx("span", { children: formatUpdatedAt(document.updatedAt) }), hasActiveCollaborators ? (_jsxs("span", { children: [otherCollaboratorCount + 1, " collaborators"] })) : null] }), document.error && !document.conflict ? (_jsx("div", { className: "mp-lb-mdkit-document-toolbar-error", children: document.error })) : null, _jsxs("div", { className: "mp-lb-mdkit-document-toolbar-actions", children: [hasVersionHistory && onOpenVersionHistory ? (_jsx("button", { type: "button", disabled: isBusy || document.conflict || versions?.isLoading, onClick: () => void runAction("versions", async () => {
|
|
41
39
|
await versions?.refresh();
|
|
42
|
-
await onOpenVersionHistory
|
|
40
|
+
await onOpenVersionHistory();
|
|
43
41
|
}), children: versions?.isLoading
|
|
44
42
|
? "Loading versions..."
|
|
45
|
-
: `Version ${String(document.version ?? "none")}` }), document.conflict && onOpenConflict ? (_jsx("button", { type: "button", className: "mp-lb-mdkit-document-toolbar-conflict-trigger", disabled: isBusy, onClick: () => void runAction("conflict", async () => {
|
|
43
|
+
: `Version ${String(document.version ?? "none")}` })) : null, document.conflict && onOpenConflict ? (_jsx("button", { type: "button", className: "mp-lb-mdkit-document-toolbar-conflict-trigger", disabled: isBusy, onClick: () => void runAction("conflict", async () => {
|
|
46
44
|
await onOpenConflict();
|
|
47
45
|
}), children: "Resolve conflict" })) : null] }), document.conflict && showConflictActions ? (_jsxs("div", { className: "mp-lb-mdkit-document-toolbar-conflict", children: [_jsx("span", { children: "Remote changes conflict with local edits." }), _jsx("button", { type: "button", disabled: isBusy, onClick: () => void runAction("reload", document.resync), children: "Keep remote" }), _jsx("button", { type: "button", disabled: isBusy, onClick: () => void runAction("overwrite", document.forceSave), children: "Keep local" })] })) : null] }));
|
|
48
46
|
};
|
|
@@ -48,9 +48,16 @@ export type MdKitCollaborationParticipant = {
|
|
|
48
48
|
color?: string;
|
|
49
49
|
imageUrl?: string;
|
|
50
50
|
};
|
|
51
|
+
export type MdKitCollaborationPresence = MdKitCollaborationParticipant & {
|
|
52
|
+
clientId: number;
|
|
53
|
+
isLocal: boolean;
|
|
54
|
+
};
|
|
51
55
|
export type MdKitCollaborationSession = {
|
|
52
56
|
collaborator: MdKitCollaborationParticipant;
|
|
53
57
|
document: Y.Doc;
|
|
58
|
+
isCollaborating: boolean;
|
|
59
|
+
otherParticipants: MdKitCollaborationPresence[];
|
|
60
|
+
participants: MdKitCollaborationPresence[];
|
|
54
61
|
provider: HocuspocusProvider | null;
|
|
55
62
|
roomName: string;
|
|
56
63
|
status: MdKitCollaborationStatus;
|
|
@@ -72,6 +72,8 @@ export const TiptapMarkdownSurface = (props) => {
|
|
|
72
72
|
const collaborationDocument = collaboration?.document ?? null;
|
|
73
73
|
const collaborationProvider = collaboration?.provider ?? null;
|
|
74
74
|
const collaborationUserColor = collaboration?.collaborator.color ?? "";
|
|
75
|
+
const collaborationUserId = collaboration?.collaborator.id ?? "";
|
|
76
|
+
const collaborationUserImageUrl = collaboration?.collaborator.imageUrl ?? "";
|
|
75
77
|
const collaborationUserName = collaboration?.collaborator.name ?? "";
|
|
76
78
|
const hasCollaboration = !!collaborationDocument;
|
|
77
79
|
useEffect(() => {
|
|
@@ -103,11 +105,19 @@ export const TiptapMarkdownSurface = (props) => {
|
|
|
103
105
|
}),
|
|
104
106
|
user: {
|
|
105
107
|
color: collaborationUserColor,
|
|
108
|
+
id: collaborationUserId,
|
|
109
|
+
imageUrl: collaborationUserImageUrl || undefined,
|
|
106
110
|
name: collaborationUserName,
|
|
107
111
|
},
|
|
108
112
|
}),
|
|
109
113
|
]
|
|
110
|
-
: [], [
|
|
114
|
+
: [], [
|
|
115
|
+
collaborationProvider,
|
|
116
|
+
collaborationUserColor,
|
|
117
|
+
collaborationUserId,
|
|
118
|
+
collaborationUserImageUrl,
|
|
119
|
+
collaborationUserName,
|
|
120
|
+
]);
|
|
111
121
|
const editor = useEditor({
|
|
112
122
|
content: hasCollaboration
|
|
113
123
|
? undefined
|
|
@@ -2,6 +2,7 @@ import type { MdKitDocumentAdapter, MdKitDocumentVersionDetail, MdKitDocumentVer
|
|
|
2
2
|
export type UseMdKitDocumentVersionsOptions = {
|
|
3
3
|
adapter: Pick<MdKitDocumentAdapter, "listDocumentVersions" | "readDocumentVersion">;
|
|
4
4
|
documentId: string | null;
|
|
5
|
+
enabled?: boolean;
|
|
5
6
|
};
|
|
6
7
|
export type MdKitDocumentVersionsController = {
|
|
7
8
|
error: string | null;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
2
|
export const useMdKitDocumentVersions = (options) => {
|
|
3
|
-
const { adapter, documentId } = options;
|
|
3
|
+
const { adapter, documentId, enabled = true } = options;
|
|
4
4
|
const [versions, setVersions] = useState([]);
|
|
5
5
|
const [selectedVersionId, setSelectedVersionId] = useState(null);
|
|
6
6
|
const [selectedVersion, setSelectedVersion] = useState(null);
|
|
7
7
|
const [isLoading, setIsLoading] = useState(false);
|
|
8
8
|
const [error, setError] = useState(null);
|
|
9
|
-
const hasVersioning = !!adapter.listDocumentVersions && !!adapter.readDocumentVersion;
|
|
9
|
+
const hasVersioning = enabled && !!adapter.listDocumentVersions && !!adapter.readDocumentVersion;
|
|
10
10
|
const refresh = useCallback(async () => {
|
|
11
|
-
if (!documentId || !adapter.listDocumentVersions) {
|
|
11
|
+
if (!hasVersioning || !documentId || !adapter.listDocumentVersions) {
|
|
12
12
|
setVersions([]);
|
|
13
13
|
setSelectedVersionId(null);
|
|
14
14
|
setSelectedVersion(null);
|
|
@@ -34,9 +34,9 @@ export const useMdKitDocumentVersions = (options) => {
|
|
|
34
34
|
finally {
|
|
35
35
|
setIsLoading(false);
|
|
36
36
|
}
|
|
37
|
-
}, [adapter, documentId]);
|
|
37
|
+
}, [adapter, documentId, hasVersioning]);
|
|
38
38
|
const openVersion = useCallback(async (versionId) => {
|
|
39
|
-
if (!documentId || !adapter.readDocumentVersion) {
|
|
39
|
+
if (!hasVersioning || !documentId || !adapter.readDocumentVersion) {
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
42
|
setSelectedVersionId(versionId);
|
|
@@ -60,7 +60,7 @@ export const useMdKitDocumentVersions = (options) => {
|
|
|
60
60
|
finally {
|
|
61
61
|
setIsLoading(false);
|
|
62
62
|
}
|
|
63
|
-
}, [adapter, documentId]);
|
|
63
|
+
}, [adapter, documentId, hasVersioning]);
|
|
64
64
|
useEffect(() => {
|
|
65
65
|
setSelectedVersionId(null);
|
|
66
66
|
setSelectedVersion(null);
|
package/docs/index.md
CHANGED
|
@@ -177,8 +177,9 @@ and build your own workflow components.
|
|
|
177
177
|
|
|
178
178
|
The backend starts with a store object. Replace `createYourDocumentStore()` with
|
|
179
179
|
Postgres, MongoDB, Redis, files, or any other durable storage. Your store
|
|
180
|
-
implements
|
|
181
|
-
checkpoints, restore a checkpoint,
|
|
180
|
+
implements the [`MdKitBackendStore`](./api.md#mdkitbackendstore) interface:
|
|
181
|
+
read/write the current document, create/read checkpoints, restore a checkpoint,
|
|
182
|
+
and optionally persist collaboration state.
|
|
182
183
|
The mdkit backend helper applies checkpoint policy and turns those primitives
|
|
183
184
|
into tRPC or REST procedures. Application-owned metadata, auth, permissions,
|
|
184
185
|
tenancy, and durable Yjs storage stay in your code; see
|