@strapi-community/plugin-io 5.0.4 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,389 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const jsxRuntime = require("react/jsx-runtime");
4
+ const React = require("react");
5
+ const index = require("./index-DkTxsEqL.js");
6
+ const designSystem = require("@strapi/design-system");
7
+ const admin = require("@strapi/strapi/admin");
8
+ const styled = require("styled-components");
9
+ const socket_ioClient = require("socket.io-client");
10
+ const index$1 = require("./index--2NeIKGR.js");
11
+ const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
12
+ const styled__default = /* @__PURE__ */ _interopDefault(styled);
13
+ const pulse = styled.keyframes`
14
+ 0%, 100% {
15
+ box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.2);
16
+ transform: scale(1);
17
+ }
18
+ 50% {
19
+ box-shadow: 0 0 0 6px rgba(34, 197, 94, 0.1);
20
+ transform: scale(1.1);
21
+ }
22
+ `;
23
+ const StatusCard = styled__default.default.div`
24
+ background: ${(props) => props.theme.colors.neutral0};
25
+ border: 1px solid ${({ $status, theme }) => $status === "connected" ? "rgba(34, 197, 94, 0.3)" : $status === "error" ? "rgba(239, 68, 68, 0.3)" : theme.colors.neutral200};
26
+ border-radius: 10px;
27
+ padding: 14px 16px;
28
+ display: flex;
29
+ align-items: center;
30
+ gap: 12px;
31
+ `;
32
+ const StatusDot = styled__default.default.div`
33
+ width: 12px;
34
+ height: 12px;
35
+ border-radius: 50%;
36
+ flex-shrink: 0;
37
+ background: ${({ $status }) => $status === "connected" ? "#22c55e" : $status === "connecting" ? "#f59e0b" : $status === "error" ? "#ef4444" : "#94a3b8"};
38
+
39
+ ${({ $status }) => $status === "connected" && styled.css`
40
+ animation: ${pulse} 2s ease-in-out infinite;
41
+ `}
42
+ `;
43
+ const StatusText = styled__default.default.div`
44
+ display: flex;
45
+ flex-direction: column;
46
+ gap: 2px;
47
+ `;
48
+ const StatusLabel = styled__default.default.span`
49
+ font-size: 14px;
50
+ font-weight: 600;
51
+ color: ${({ $status, theme }) => $status === "connected" ? theme.colors.success600 : $status === "connecting" ? theme.colors.warning600 : $status === "error" ? theme.colors.danger600 : theme.colors.neutral600};
52
+ `;
53
+ const StatusSubtext = styled__default.default.span`
54
+ font-size: 12px;
55
+ color: ${(props) => props.theme.colors.neutral500};
56
+ `;
57
+ const SectionTitle = styled__default.default.div`
58
+ font-size: 11px;
59
+ font-weight: 600;
60
+ color: ${(props) => props.theme.colors.neutral600};
61
+ text-transform: uppercase;
62
+ letter-spacing: 0.5px;
63
+ margin-bottom: 10px;
64
+ `;
65
+ const EditorItem = styled__default.default.div`
66
+ display: flex;
67
+ align-items: center;
68
+ gap: 12px;
69
+ padding: 12px 14px;
70
+ background: ${(props) => props.theme.colors.neutral0};
71
+ border-radius: 10px;
72
+ border: 1px solid ${(props) => props.theme.colors.neutral150};
73
+ transition: all 0.2s ease;
74
+
75
+ &:hover {
76
+ border-color: ${(props) => props.theme.colors.primary200};
77
+ box-shadow: 0 2px 8px rgba(73, 69, 255, 0.08);
78
+ transform: translateY(-1px);
79
+ }
80
+ `;
81
+ const EDITOR_COLORS = [
82
+ "linear-gradient(135deg, #4945ff 0%, #7b79ff 100%)",
83
+ "linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%)",
84
+ "linear-gradient(135deg, #10b981 0%, #34d399 100%)",
85
+ "linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%)",
86
+ "linear-gradient(135deg, #ef4444 0%, #f87171 100%)",
87
+ "linear-gradient(135deg, #ec4899 0%, #f472b6 100%)"
88
+ ];
89
+ const EditorAvatar = styled__default.default.div`
90
+ width: 36px;
91
+ height: 36px;
92
+ border-radius: 50%;
93
+ background: ${({ $color }) => $color || EDITOR_COLORS[0]};
94
+ color: white;
95
+ font-size: 12px;
96
+ font-weight: 700;
97
+ display: flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ flex-shrink: 0;
101
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
102
+ `;
103
+ const EditorInfo = styled__default.default.div`
104
+ flex: 1;
105
+ min-width: 0;
106
+ display: flex;
107
+ flex-direction: column;
108
+ gap: 2px;
109
+ `;
110
+ const EditorName = styled__default.default.span`
111
+ font-size: 13px;
112
+ font-weight: 600;
113
+ color: ${(props) => props.theme.colors.neutral800};
114
+ white-space: nowrap;
115
+ overflow: hidden;
116
+ text-overflow: ellipsis;
117
+ `;
118
+ const EditorEmail = styled__default.default.span`
119
+ font-size: 11px;
120
+ color: ${(props) => props.theme.colors.neutral500};
121
+ white-space: nowrap;
122
+ overflow: hidden;
123
+ text-overflow: ellipsis;
124
+ `;
125
+ const EditingBadge = styled__default.default.span`
126
+ font-size: 10px;
127
+ font-weight: 600;
128
+ color: #166534;
129
+ background: #dcfce7;
130
+ padding: 4px 8px;
131
+ border-radius: 12px;
132
+ flex-shrink: 0;
133
+ `;
134
+ const TypingBadge = styled__default.default.span`
135
+ font-size: 10px;
136
+ font-weight: 600;
137
+ color: #92400e;
138
+ background: #fef3c7;
139
+ padding: 4px 8px;
140
+ border-radius: 12px;
141
+ flex-shrink: 0;
142
+ `;
143
+ const EmptyState = styled__default.default.div`
144
+ text-align: center;
145
+ padding: 16px;
146
+ background: ${(props) => props.theme.colors.neutral100};
147
+ border-radius: 10px;
148
+ border: 1px dashed ${(props) => props.theme.colors.neutral300};
149
+ `;
150
+ const EmptyText = styled__default.default.span`
151
+ font-size: 13px;
152
+ color: ${(props) => props.theme.colors.neutral500};
153
+ `;
154
+ const getEditorInitials = (user = {}) => {
155
+ const first = (user.firstname?.[0] || user.username?.[0] || user.email?.[0] || "?").toUpperCase();
156
+ const last = (user.lastname?.[0] || "").toUpperCase();
157
+ return `${first}${last}`.trim();
158
+ };
159
+ const getEditorName = (user = {}) => {
160
+ if (user.firstname) {
161
+ return `${user.firstname} ${user.lastname || ""}`.trim();
162
+ }
163
+ return user.username || user.email || "Unknown";
164
+ };
165
+ const LivePresencePanel = ({ documentId, model, document }) => {
166
+ const { formatMessage } = index.useIntl();
167
+ const { post } = admin.useFetchClient();
168
+ const t = (id, defaultMessage, values) => formatMessage({ id: `${index$1.PLUGIN_ID}.${id}`, defaultMessage }, values);
169
+ const socketRef = React.useRef(null);
170
+ const [sessionData, setSessionData] = React.useState(null);
171
+ const [presenceState, setPresenceState] = React.useState({
172
+ status: "initializing",
173
+ editors: [],
174
+ typingUsers: [],
175
+ error: null
176
+ });
177
+ const uid = model?.uid || model;
178
+ React.useEffect(() => {
179
+ if (!uid || !documentId) {
180
+ setPresenceState((prev) => ({ ...prev, status: "disconnected", error: "No content" }));
181
+ return;
182
+ }
183
+ let cancelled = false;
184
+ const getSession = async () => {
185
+ try {
186
+ setPresenceState((prev) => ({ ...prev, status: "requesting" }));
187
+ const { data } = await post(`/${index$1.PLUGIN_ID}/presence/session`, {});
188
+ if (cancelled) return;
189
+ if (!data || !data.token) {
190
+ throw new Error("Invalid session response");
191
+ }
192
+ console.log(`[${index$1.PLUGIN_ID}] Presence session obtained for:`, data.user?.email);
193
+ setSessionData(data);
194
+ setPresenceState((prev) => ({ ...prev, status: "connecting" }));
195
+ } catch (error2) {
196
+ if (cancelled) return;
197
+ console.error(`[${index$1.PLUGIN_ID}] Failed to get presence session:`, error2);
198
+ setPresenceState((prev) => ({
199
+ ...prev,
200
+ status: "error",
201
+ error: error2.message || "Failed to get session"
202
+ }));
203
+ }
204
+ };
205
+ getSession();
206
+ return () => {
207
+ cancelled = true;
208
+ };
209
+ }, [uid, documentId, post]);
210
+ React.useEffect(() => {
211
+ if (!sessionData?.token || !uid || !documentId) {
212
+ return;
213
+ }
214
+ const socketUrl = sessionData.wsUrl || `${window.location.protocol}//${window.location.host}`;
215
+ const socket = socket_ioClient.io(socketUrl, {
216
+ path: sessionData.wsPath || "/socket.io",
217
+ transports: ["websocket", "polling"],
218
+ auth: {
219
+ token: sessionData.token,
220
+ strategy: "admin-jwt",
221
+ isAdmin: true
222
+ },
223
+ reconnection: true,
224
+ reconnectionAttempts: 3
225
+ });
226
+ socketRef.current = socket;
227
+ let lastTypingEmit = 0;
228
+ const TYPING_THROTTLE = 2e3;
229
+ const getFieldName = (element) => {
230
+ const name = element.name || element.id || "";
231
+ const label = element.closest("label") || document.querySelector(`label[for="${element.id}"]`);
232
+ if (label) {
233
+ return label.textContent?.trim() || name;
234
+ }
235
+ const fieldWrapper = element.closest('[class*="Field"]');
236
+ if (fieldWrapper) {
237
+ const labelEl = fieldWrapper.querySelector('label, [class*="Label"]');
238
+ if (labelEl) {
239
+ return labelEl.textContent?.trim() || name;
240
+ }
241
+ }
242
+ return name || "unknown field";
243
+ };
244
+ const handleInput = (event) => {
245
+ const target = event.target;
246
+ if (!["INPUT", "TEXTAREA"].includes(target.tagName)) return;
247
+ const isInContentManager = target.closest('[class*="ContentLayout"]') || target.closest("main");
248
+ if (!isInContentManager) return;
249
+ const now = Date.now();
250
+ if (now - lastTypingEmit < TYPING_THROTTLE) return;
251
+ lastTypingEmit = now;
252
+ const fieldName = getFieldName(target);
253
+ if (socket.connected) {
254
+ socket.emit("presence:typing", { uid, documentId, fieldName });
255
+ console.log(`[${index$1.PLUGIN_ID}] Typing in field: ${fieldName}`);
256
+ }
257
+ };
258
+ if (typeof document !== "undefined" && typeof document.addEventListener === "function") {
259
+ document.addEventListener("input", handleInput, true);
260
+ }
261
+ socket.on("connect", () => {
262
+ console.log(`[${index$1.PLUGIN_ID}] Presence socket connected`);
263
+ setPresenceState((prev) => ({ ...prev, status: "connected", error: null }));
264
+ socket.emit("presence:join", { uid, documentId }, (response) => {
265
+ if (response?.success) {
266
+ setPresenceState((prev) => ({
267
+ ...prev,
268
+ editors: (response.editors || []).map((e) => ({
269
+ ...e,
270
+ isCurrentUser: e.socketId === socket.id
271
+ }))
272
+ }));
273
+ }
274
+ });
275
+ });
276
+ socket.on("disconnect", () => {
277
+ setPresenceState((prev) => ({ ...prev, status: "disconnected" }));
278
+ });
279
+ socket.on("connect_error", (err) => {
280
+ console.warn(`[${index$1.PLUGIN_ID}] Presence socket error:`, err.message);
281
+ setPresenceState((prev) => ({ ...prev, status: "error", error: err.message }));
282
+ });
283
+ socket.on("presence:update", (data) => {
284
+ if (data.uid === uid && data.documentId === documentId) {
285
+ setPresenceState((prev) => ({
286
+ ...prev,
287
+ editors: (data.editors || []).map((e) => ({
288
+ ...e,
289
+ isCurrentUser: e.socketId === socket.id
290
+ }))
291
+ }));
292
+ }
293
+ });
294
+ socket.on("presence:typing", (data) => {
295
+ if (data.uid === uid && data.documentId === documentId) {
296
+ setPresenceState((prev) => {
297
+ const newTyping = [...prev.typingUsers.filter((t2) => t2.user?.id !== data.user?.id)];
298
+ newTyping.push({ user: data.user, fieldName: data.fieldName, timestamp: Date.now() });
299
+ return { ...prev, typingUsers: newTyping };
300
+ });
301
+ setTimeout(() => {
302
+ setPresenceState((prev) => ({
303
+ ...prev,
304
+ typingUsers: prev.typingUsers.filter((t2) => t2.user?.id !== data.user?.id)
305
+ }));
306
+ }, 3e3);
307
+ }
308
+ });
309
+ const heartbeat = setInterval(() => {
310
+ if (socket.connected) {
311
+ socket.emit("presence:heartbeat");
312
+ }
313
+ }, 3e4);
314
+ return () => {
315
+ clearInterval(heartbeat);
316
+ if (typeof document !== "undefined" && typeof document.removeEventListener === "function") {
317
+ document.removeEventListener("input", handleInput, true);
318
+ }
319
+ if (socket.connected) {
320
+ socket.emit("presence:leave", { uid, documentId });
321
+ }
322
+ socket.disconnect();
323
+ socketRef.current = null;
324
+ };
325
+ }, [sessionData, uid, documentId]);
326
+ const { status, editors, typingUsers, error } = presenceState;
327
+ const otherEditors = React.useMemo(() => {
328
+ return editors.filter((e) => !e.isCurrentUser);
329
+ }, [editors]);
330
+ const getUserTypingInfo = React.useCallback((userId) => {
331
+ const typing = typingUsers.find((t2) => t2.user?.id === userId);
332
+ return typing || null;
333
+ }, [typingUsers]);
334
+ React.useCallback((userId) => {
335
+ return typingUsers.some((t2) => t2.user?.id === userId);
336
+ }, [typingUsers]);
337
+ const statusLabel = React.useMemo(() => {
338
+ switch (status) {
339
+ case "connected":
340
+ return t("presence.live", "Live");
341
+ case "connecting":
342
+ return t("presence.connecting", "Connecting...");
343
+ case "requesting":
344
+ return t("presence.requesting", "Authenticating...");
345
+ case "initializing":
346
+ return t("presence.initializing", "Initializing...");
347
+ case "error":
348
+ return t("presence.error", "Connection Error");
349
+ case "disconnected":
350
+ return t("presence.disconnected", "Disconnected");
351
+ default:
352
+ return t("presence.offline", "Offline");
353
+ }
354
+ }, [status, t]);
355
+ const isConnected = status === "connected";
356
+ console.log(`[${index$1.PLUGIN_ID}] LivePresencePanel render:`, { uid, documentId, status, editors: otherEditors.length });
357
+ return {
358
+ title: t("presence.title", "Live Presence"),
359
+ content: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 4, alignItems: "stretch", style: { width: "100%" }, children: [
360
+ /* @__PURE__ */ jsxRuntime.jsxs(StatusCard, { $status: status, children: [
361
+ /* @__PURE__ */ jsxRuntime.jsx(StatusDot, { $status: status }),
362
+ /* @__PURE__ */ jsxRuntime.jsxs(StatusText, { children: [
363
+ /* @__PURE__ */ jsxRuntime.jsx(StatusLabel, { $status: status, children: statusLabel }),
364
+ /* @__PURE__ */ jsxRuntime.jsx(StatusSubtext, { children: isConnected ? t("presence.realtimeActive", "Real-time sync active") : error || t("presence.establishing", "Establishing connection...") })
365
+ ] })
366
+ ] }),
367
+ isConnected && otherEditors.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
368
+ /* @__PURE__ */ jsxRuntime.jsx(SectionTitle, { children: t("presence.activeEditors", "Also Editing ({count})", { count: otherEditors.length }) }),
369
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", gap: 2, alignItems: "stretch", children: otherEditors.map((editor, idx) => {
370
+ const user = editor.user || {};
371
+ const typingInfo = getUserTypingInfo(user.id);
372
+ return /* @__PURE__ */ jsxRuntime.jsxs(EditorItem, { children: [
373
+ /* @__PURE__ */ jsxRuntime.jsx(EditorAvatar, { $color: EDITOR_COLORS[idx % EDITOR_COLORS.length], children: getEditorInitials(user) }),
374
+ /* @__PURE__ */ jsxRuntime.jsxs(EditorInfo, { children: [
375
+ /* @__PURE__ */ jsxRuntime.jsx(EditorName, { children: getEditorName(user) }),
376
+ typingInfo?.fieldName ? /* @__PURE__ */ jsxRuntime.jsxs(EditorEmail, { children: [
377
+ "Typing in: ",
378
+ typingInfo.fieldName
379
+ ] }) : user.email && user.firstname ? /* @__PURE__ */ jsxRuntime.jsx(EditorEmail, { children: user.email }) : null
380
+ ] }),
381
+ typingInfo ? /* @__PURE__ */ jsxRuntime.jsx(TypingBadge, { children: t("presence.typing", "Typing...") }) : /* @__PURE__ */ jsxRuntime.jsx(EditingBadge, { children: t("presence.editing", "Editing") })
382
+ ] }, editor.socketId || idx);
383
+ }) })
384
+ ] }),
385
+ isConnected && otherEditors.length === 0 && /* @__PURE__ */ jsxRuntime.jsx(EmptyState, { children: /* @__PURE__ */ jsxRuntime.jsx(EmptyText, { children: t("presence.workingAlone", "You are the only editor") }) })
386
+ ] })
387
+ };
388
+ };
389
+ exports.default = LivePresencePanel;
@@ -4,7 +4,7 @@ import { useFetchClient, useNotification } from "@strapi/strapi/admin";
4
4
  import styled, { css, keyframes } from "styled-components";
5
5
  import { Box, Loader, Flex, Field, TextInput, Badge, Typography } from "@strapi/design-system";
6
6
  import { u as useIntl } from "./index-CEh8vkxY.mjs";
7
- import { P as PLUGIN_ID } from "./index-DLXtrAtk.mjs";
7
+ import { P as PLUGIN_ID } from "./index-CzvX8YTe.mjs";
8
8
  const UsersIcon = () => /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", strokeWidth: 1.5, stroke: "currentColor", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" }) });
9
9
  const BoltIcon = () => /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", strokeWidth: 1.5, stroke: "currentColor", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "m3.75 13.5 10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75Z" }) });
10
10
  const ChartBarIcon = () => /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", strokeWidth: 1.5, stroke: "currentColor", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z" }) });
@@ -6,7 +6,7 @@ const admin = require("@strapi/strapi/admin");
6
6
  const styled = require("styled-components");
7
7
  const designSystem = require("@strapi/design-system");
8
8
  const index = require("./index-DkTxsEqL.js");
9
- const index$1 = require("./index-BVQ20t1c.js");
9
+ const index$1 = require("./index--2NeIKGR.js");
10
10
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
11
11
  const styled__default = /* @__PURE__ */ _interopDefault(styled);
12
12
  const UsersIcon = () => /* @__PURE__ */ jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", strokeWidth: 1.5, stroke: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" }) });