@zachariaz/strapi-plugin-content-variants 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +600 -0
  2. package/dist/_chunks/Segments-BREqC60L.js +330 -0
  3. package/dist/_chunks/Segments-BgxnvvtR.mjs +330 -0
  4. package/dist/_chunks/en-Bnfrhhim.js +62 -0
  5. package/dist/_chunks/en-e_966kWj.mjs +62 -0
  6. package/dist/_chunks/index-DVoZM8JU.js +1036 -0
  7. package/dist/_chunks/index-Dj2sexmk.mjs +1020 -0
  8. package/dist/admin/index.js +3 -0
  9. package/dist/admin/index.mjs +4 -0
  10. package/dist/admin/src/components/Initializer.d.ts +5 -0
  11. package/dist/admin/src/components/SegmentPickerAction.d.ts +7 -0
  12. package/dist/admin/src/components/VariantInfoAction.d.ts +8 -0
  13. package/dist/admin/src/components/VariantPanel.d.ts +13 -0
  14. package/dist/admin/src/components/VariantPickerAction.d.ts +20 -0
  15. package/dist/admin/src/contentManagerHooks/editView.d.ts +6 -0
  16. package/dist/admin/src/contentManagerHooks/listView.d.ts +22 -0
  17. package/dist/admin/src/hooks/useSegments.d.ts +17 -0
  18. package/dist/admin/src/hooks/useVariantFamily.d.ts +19 -0
  19. package/dist/admin/src/hooks/useVariantLinks.d.ts +44 -0
  20. package/dist/admin/src/index.d.ts +11 -0
  21. package/dist/admin/src/pages/Settings/Segments.d.ts +2 -0
  22. package/dist/admin/src/pluginId.d.ts +2 -0
  23. package/dist/admin/src/utils/batchLinkFetcher.d.ts +11 -0
  24. package/dist/admin/src/utils/variants.d.ts +13 -0
  25. package/dist/server/index.js +895 -0
  26. package/dist/server/index.mjs +896 -0
  27. package/dist/server/src/bootstrap.d.ts +17 -0
  28. package/dist/server/src/config/index.d.ts +5 -0
  29. package/dist/server/src/content-types/index.d.ts +121 -0
  30. package/dist/server/src/controllers/index.d.ts +24 -0
  31. package/dist/server/src/controllers/segment.d.ts +11 -0
  32. package/dist/server/src/controllers/variant-link.d.ts +18 -0
  33. package/dist/server/src/destroy.d.ts +5 -0
  34. package/dist/server/src/index.d.ts +244 -0
  35. package/dist/server/src/register.d.ts +5 -0
  36. package/dist/server/src/routes/admin.d.ts +12 -0
  37. package/dist/server/src/routes/content-api.d.ts +19 -0
  38. package/dist/server/src/routes/index.d.ts +25 -0
  39. package/dist/server/src/services/index.d.ts +62 -0
  40. package/dist/server/src/services/segment.d.ts +14 -0
  41. package/dist/server/src/services/variant-link.d.ts +60 -0
  42. package/dist/server/src/services/variant-resolver.d.ts +23 -0
  43. package/package.json +104 -0
@@ -0,0 +1,1020 @@
1
+ import * as React from "react";
2
+ import { useRef, useEffect, useState, useCallback } from "react";
3
+ import { jsxs, jsx } from "react/jsx-runtime";
4
+ import { unstable_useDocument } from "@strapi/content-manager/strapi-admin";
5
+ import { Flex, Typography, Status, SingleSelect, SingleSelectOption, Box, Button, Menu, IconButton, Tooltip } from "@strapi/design-system";
6
+ import { Plus, More, Sparkle } from "@strapi/icons";
7
+ import { useIntl, RawIntlProvider } from "react-intl";
8
+ import { useNavigate } from "react-router-dom";
9
+ import { useFetchClient } from "@strapi/strapi/admin";
10
+ import { createRoot } from "react-dom/client";
11
+ import { useTheme, ThemeProvider } from "styled-components";
12
+ const pluginId = "content-variants";
13
+ const Initializer = ({ setPlugin }) => {
14
+ const ref = useRef(setPlugin);
15
+ useEffect(() => {
16
+ ref.current(pluginId);
17
+ }, []);
18
+ return null;
19
+ };
20
+ function useVariantFamily({ contentType, documentId }) {
21
+ const { get } = useFetchClient();
22
+ const [family, setFamily] = useState(null);
23
+ const [isLoading, setIsLoading] = useState(false);
24
+ const [error, setError] = useState(null);
25
+ const fetchFamily = useCallback(async () => {
26
+ if (!contentType || !documentId) {
27
+ setFamily(null);
28
+ return;
29
+ }
30
+ setIsLoading(true);
31
+ setError(null);
32
+ try {
33
+ const { data } = await get(`/${pluginId}/links/family`, {
34
+ params: { contentType, documentId }
35
+ });
36
+ setFamily(data);
37
+ } catch (err) {
38
+ setError(err?.message ?? "Failed to fetch variant family");
39
+ } finally {
40
+ setIsLoading(false);
41
+ }
42
+ }, [get, contentType, documentId]);
43
+ useEffect(() => {
44
+ fetchFamily();
45
+ }, [fetchFamily]);
46
+ return { family, isLoading, error, refetch: fetchFamily };
47
+ }
48
+ function useSegments() {
49
+ const [segments, setSegments] = useState([]);
50
+ const [isLoading, setIsLoading] = useState(true);
51
+ const [error, setError] = useState(null);
52
+ const { get, post, put, del } = useFetchClient();
53
+ const fetchSegments = useCallback(async () => {
54
+ setIsLoading(true);
55
+ setError(null);
56
+ try {
57
+ const { data } = await get(`/${pluginId}/segments`);
58
+ setSegments(Array.isArray(data) ? data : []);
59
+ } catch (err) {
60
+ setError(err?.message ?? "Failed to fetch segments");
61
+ } finally {
62
+ setIsLoading(false);
63
+ }
64
+ }, [get]);
65
+ const createSegment = useCallback(
66
+ async (payload) => {
67
+ const { data } = await post(`/${pluginId}/segments`, payload);
68
+ await fetchSegments();
69
+ return data;
70
+ },
71
+ [post, fetchSegments]
72
+ );
73
+ const updateSegment = useCallback(
74
+ async (documentId, payload) => {
75
+ const { data } = await put(`/${pluginId}/segments/${documentId}`, payload);
76
+ await fetchSegments();
77
+ return data;
78
+ },
79
+ [put, fetchSegments]
80
+ );
81
+ const deleteSegment = useCallback(
82
+ async (documentId) => {
83
+ await del(`/${pluginId}/segments/${documentId}`);
84
+ await fetchSegments();
85
+ },
86
+ [del, fetchSegments]
87
+ );
88
+ useEffect(() => {
89
+ fetchSegments();
90
+ }, [fetchSegments]);
91
+ return { segments, isLoading, error, fetchSegments, createSegment, updateSegment, deleteSegment };
92
+ }
93
+ function useVariantLinks({ baseContentType, baseDocumentId, variantDocumentId }) {
94
+ const { get, post, put, del } = useFetchClient();
95
+ const [links, setLinks] = useState([]);
96
+ const [isLoading, setIsLoading] = useState(false);
97
+ const [error, setError] = useState(null);
98
+ const fetchLinks = useCallback(async () => {
99
+ if (!baseContentType || !baseDocumentId && !variantDocumentId) {
100
+ setLinks([]);
101
+ return;
102
+ }
103
+ setIsLoading(true);
104
+ setError(null);
105
+ try {
106
+ const params = { baseContentType };
107
+ if (baseDocumentId) {
108
+ params.baseDocumentId = baseDocumentId;
109
+ }
110
+ if (variantDocumentId) {
111
+ params.variantDocumentId = variantDocumentId;
112
+ }
113
+ const { data } = await get(`/${pluginId}/links`, { params });
114
+ setLinks(Array.isArray(data) ? data : []);
115
+ } catch (err) {
116
+ setError(err?.message ?? "Failed to fetch variant links");
117
+ } finally {
118
+ setIsLoading(false);
119
+ }
120
+ }, [get, baseContentType, baseDocumentId, variantDocumentId]);
121
+ const createLink = useCallback(
122
+ async (payload) => {
123
+ const { data } = await post(`/${pluginId}/links`, payload);
124
+ await fetchLinks();
125
+ return data;
126
+ },
127
+ [post, fetchLinks]
128
+ );
129
+ const createVariantWithSegments = useCallback(
130
+ async (payload) => {
131
+ const { data } = await post(`/${pluginId}/links/with-variant`, payload);
132
+ await fetchLinks();
133
+ return data;
134
+ },
135
+ [post, fetchLinks]
136
+ );
137
+ const updateLink = useCallback(
138
+ async (documentId, payload) => {
139
+ const { data } = await put(`/${pluginId}/links/${documentId}`, payload);
140
+ await fetchLinks();
141
+ return data;
142
+ },
143
+ [put, fetchLinks]
144
+ );
145
+ const deleteLink = useCallback(
146
+ async (documentId) => {
147
+ await del(`/${pluginId}/links/${documentId}`);
148
+ await fetchLinks();
149
+ },
150
+ [del, fetchLinks]
151
+ );
152
+ useEffect(() => {
153
+ fetchLinks();
154
+ }, [fetchLinks]);
155
+ return { links, isLoading, error, fetchLinks, createLink, createVariantWithSegments, updateLink, deleteLink };
156
+ }
157
+ const PLUGIN_ID = "content-variants";
158
+ function isVariantEnabledContentType(options) {
159
+ return !!options?.[PLUGIN_ID]?.enabled;
160
+ }
161
+ function isVariantField(attribute) {
162
+ return !!attribute?.pluginOptions?.[PLUGIN_ID]?.variant;
163
+ }
164
+ const STATUS_VARIANTS = {
165
+ draft: "secondary",
166
+ published: "success",
167
+ modified: "alternative"
168
+ };
169
+ const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
170
+ const StatusBadge = ({ status }) => {
171
+ if (!status) return null;
172
+ return /* @__PURE__ */ jsx(
173
+ Status,
174
+ {
175
+ display: "flex",
176
+ paddingLeft: "6px",
177
+ paddingRight: "6px",
178
+ paddingTop: "2px",
179
+ paddingBottom: "2px",
180
+ size: "S",
181
+ variant: STATUS_VARIANTS[status] || "secondary",
182
+ children: /* @__PURE__ */ jsx(Typography, { tag: "span", variant: "pi", fontWeight: "bold", children: capitalize(status) })
183
+ }
184
+ );
185
+ };
186
+ const VariantPickerAction = ({ model, documentId, collectionType }) => {
187
+ const { formatMessage } = useIntl();
188
+ const navigate = useNavigate();
189
+ const { schema } = unstable_useDocument({
190
+ model,
191
+ documentId,
192
+ collectionType
193
+ });
194
+ const isEnabled = isVariantEnabledContentType(schema?.pluginOptions);
195
+ const isDraftAndPublish = !!schema?.options?.draftAndPublish;
196
+ const { family, isLoading: familyLoading } = useVariantFamily({
197
+ contentType: model,
198
+ documentId
199
+ });
200
+ const { segments } = useSegments();
201
+ const { createVariantWithSegments } = useVariantLinks({
202
+ baseContentType: model,
203
+ baseDocumentId: family?.baseDocumentId
204
+ });
205
+ if (!isEnabled || !documentId || familyLoading || !family) {
206
+ return null;
207
+ }
208
+ const currentLocale = (() => {
209
+ try {
210
+ const params = new URLSearchParams(window.location.search);
211
+ return params.get("plugins[i18n][locale]") || void 0;
212
+ } catch {
213
+ return void 0;
214
+ }
215
+ })();
216
+ const navigateTo = (targetId) => {
217
+ const localeQuery = currentLocale ? `?plugins[i18n][locale]=${encodeURIComponent(currentLocale)}` : "";
218
+ navigate(
219
+ `/content-manager/collection-types/${model}/${targetId}${localeQuery}`
220
+ );
221
+ };
222
+ const assignedSegmentIds = /* @__PURE__ */ new Set();
223
+ for (const link of family.links) {
224
+ for (const assignment of link.assignments || []) {
225
+ if (assignment.segment?.documentId) {
226
+ assignedSegmentIds.add(assignment.segment.documentId);
227
+ }
228
+ }
229
+ }
230
+ const unassignedSegments = segments.filter(
231
+ (s) => !assignedSegmentIds.has(s.documentId)
232
+ );
233
+ const options = [];
234
+ options.push({
235
+ label: /* @__PURE__ */ jsxs(Flex, { width: "100%", justifyContent: "space-between", gap: 2, children: [
236
+ /* @__PURE__ */ jsx(Typography, { children: formatMessage({
237
+ id: `${PLUGIN_ID}.variant-picker.base`,
238
+ defaultMessage: "Base (default)"
239
+ }) }),
240
+ isDraftAndPublish && /* @__PURE__ */ jsx(StatusBadge, { status: family.baseStatus })
241
+ ] }),
242
+ value: "base"
243
+ });
244
+ for (const link of family.links) {
245
+ const segmentNames = (link.assignments || []).map((a) => a.segment?.name).filter(Boolean);
246
+ const name = segmentNames.length > 0 ? segmentNames.join(", ") : link.label || formatMessage({
247
+ id: `${PLUGIN_ID}.variant-picker.variant-fallback`,
248
+ defaultMessage: "Variant"
249
+ });
250
+ options.push({
251
+ label: /* @__PURE__ */ jsxs(Flex, { width: "100%", justifyContent: "space-between", gap: 2, children: [
252
+ /* @__PURE__ */ jsx(Typography, { children: name }),
253
+ isDraftAndPublish && /* @__PURE__ */ jsx(StatusBadge, { status: link.variantStatus })
254
+ ] }),
255
+ value: `variant:${link.variantDocumentId}`
256
+ });
257
+ }
258
+ for (const seg of unassignedSegments) {
259
+ options.push({
260
+ label: formatMessage(
261
+ {
262
+ id: `${PLUGIN_ID}.variant-picker.create-segment`,
263
+ defaultMessage: "Create <bold>{segment}</bold> variant"
264
+ },
265
+ { bold: (chunks) => /* @__PURE__ */ jsx("b", { children: chunks }), segment: seg.name }
266
+ ),
267
+ value: `create:${seg.documentId}`,
268
+ startIcon: /* @__PURE__ */ jsx(Plus, {})
269
+ });
270
+ }
271
+ let currentValue = "base";
272
+ if (family.isVariant) {
273
+ currentValue = `variant:${documentId}`;
274
+ }
275
+ const displayText = (() => {
276
+ if (family.isVariant) {
277
+ const currentLink = family.links.find(
278
+ (l) => l.variantDocumentId === documentId
279
+ );
280
+ if (currentLink) {
281
+ const names = (currentLink.assignments || []).map((a) => a.segment?.name).filter(Boolean);
282
+ return names.length > 0 ? names.join(", ") : currentLink.label || formatMessage({
283
+ id: `${PLUGIN_ID}.variant-picker.variant-fallback`,
284
+ defaultMessage: "Variant"
285
+ });
286
+ }
287
+ return formatMessage({
288
+ id: `${PLUGIN_ID}.variant-picker.variant-fallback`,
289
+ defaultMessage: "Variant"
290
+ });
291
+ }
292
+ return formatMessage({
293
+ id: `${PLUGIN_ID}.variant-picker.base`,
294
+ defaultMessage: "Base (default)"
295
+ });
296
+ })();
297
+ const handleSelect = async (value) => {
298
+ if (value === currentValue) return;
299
+ if (value === "base") {
300
+ navigateTo(family.baseDocumentId);
301
+ return;
302
+ }
303
+ if (value.startsWith("variant:")) {
304
+ const variantDocId = value.slice("variant:".length);
305
+ navigateTo(variantDocId);
306
+ return;
307
+ }
308
+ if (value.startsWith("create:")) {
309
+ const segDocId = value.slice("create:".length);
310
+ const seg = segments.find((s) => s.documentId === segDocId);
311
+ if (!seg) return;
312
+ try {
313
+ const res = await createVariantWithSegments({
314
+ baseContentType: model,
315
+ baseDocumentId: family.baseDocumentId,
316
+ locale: currentLocale,
317
+ segments: [{ documentId: seg.documentId, name: seg.name }]
318
+ });
319
+ if (res?.variantDocumentId) {
320
+ navigateTo(res.variantDocumentId);
321
+ }
322
+ } catch (err) {
323
+ window.alert(
324
+ err?.response?.data?.error?.message || err?.message || formatMessage({
325
+ id: `${PLUGIN_ID}.variant-picker.create-error`,
326
+ defaultMessage: "Failed to create variant. Please try again."
327
+ })
328
+ );
329
+ }
330
+ }
331
+ };
332
+ return {
333
+ label: formatMessage({
334
+ id: `${PLUGIN_ID}.variant-picker.label`,
335
+ defaultMessage: "Segments"
336
+ }),
337
+ options,
338
+ value: currentValue,
339
+ customizeContent: () => displayText,
340
+ onSelect: handleSelect
341
+ };
342
+ };
343
+ const VariantModalSelect = ({ family, model, currentDocumentId }) => {
344
+ const { formatMessage } = useIntl();
345
+ const baseLabel = formatMessage({
346
+ id: `${PLUGIN_ID}.variant-picker.base`,
347
+ defaultMessage: "Base (default)"
348
+ });
349
+ const getSegmentLabel = (link) => {
350
+ const names = (link.assignments || []).map((a) => a.segment?.name).filter(Boolean);
351
+ return names.length > 0 ? names.join(", ") : link.label || formatMessage({
352
+ id: `${PLUGIN_ID}.variant-picker.variant-fallback`,
353
+ defaultMessage: "Variant"
354
+ });
355
+ };
356
+ let currentValue = "base";
357
+ if (family.isVariant && currentDocumentId) {
358
+ const match = family.links.find((l) => l.variantDocumentId === currentDocumentId);
359
+ if (match?.variantDocumentId) currentValue = match.variantDocumentId;
360
+ }
361
+ return /* @__PURE__ */ jsxs(
362
+ SingleSelect,
363
+ {
364
+ size: "S",
365
+ value: currentValue,
366
+ onChange: (newValue) => {
367
+ const val = String(newValue);
368
+ if (val === currentValue) return;
369
+ const targetId = val === "base" ? family.baseDocumentId : val;
370
+ if (targetId) {
371
+ window.open(
372
+ `/admin/content-manager/collection-types/${model}/${targetId}`,
373
+ "_blank"
374
+ );
375
+ }
376
+ },
377
+ children: [
378
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "base", children: baseLabel }),
379
+ family.links.map((link) => /* @__PURE__ */ jsx(
380
+ SingleSelectOption,
381
+ {
382
+ value: link.variantDocumentId,
383
+ children: getSegmentLabel(link)
384
+ },
385
+ link.variantDocumentId
386
+ ))
387
+ ]
388
+ }
389
+ );
390
+ };
391
+ const VariantInfoAction = ({
392
+ model,
393
+ documentId,
394
+ collectionType
395
+ }) => {
396
+ const intl = useIntl();
397
+ const theme = useTheme();
398
+ const { schema } = unstable_useDocument({
399
+ model,
400
+ documentId,
401
+ collectionType
402
+ });
403
+ const isEnabled = isVariantEnabledContentType(schema?.pluginOptions);
404
+ const { family, isLoading } = useVariantFamily({
405
+ contentType: model,
406
+ documentId
407
+ });
408
+ React.useEffect(() => {
409
+ const shouldShow = isEnabled && !!documentId && !isLoading && !!family && family.links.length > 0;
410
+ const dialogs = document.querySelectorAll('[role="dialog"]');
411
+ if (dialogs.length === 0) return;
412
+ const modal = dialogs[dialogs.length - 1];
413
+ const existing = modal.querySelector("[data-variant-info]");
414
+ if (existing) {
415
+ const existingRoot = existing.__reactRoot;
416
+ if (existingRoot) {
417
+ try {
418
+ existingRoot.unmount();
419
+ } catch {
420
+ }
421
+ }
422
+ existing.remove();
423
+ }
424
+ if (!shouldShow || !family) return;
425
+ const buttons = modal.querySelectorAll("button");
426
+ let btnContainer = null;
427
+ for (const btn of buttons) {
428
+ const text = btn.textContent?.trim().toLowerCase();
429
+ if (text === "save" || text === "publish") {
430
+ btnContainer = btn.parentElement;
431
+ break;
432
+ }
433
+ }
434
+ if (!btnContainer) return;
435
+ const container = document.createElement("div");
436
+ container.setAttribute("data-variant-info", "true");
437
+ container.style.display = "contents";
438
+ btnContainer.insertBefore(container, btnContainer.firstChild);
439
+ const root = createRoot(container);
440
+ container.__reactRoot = root;
441
+ root.render(
442
+ /* @__PURE__ */ jsx(RawIntlProvider, { value: intl, children: /* @__PURE__ */ jsx(ThemeProvider, { theme, children: /* @__PURE__ */ jsx(
443
+ VariantModalSelect,
444
+ {
445
+ family,
446
+ model,
447
+ currentDocumentId: documentId
448
+ }
449
+ ) }) })
450
+ );
451
+ return () => {
452
+ try {
453
+ root.unmount();
454
+ } catch {
455
+ }
456
+ container.remove();
457
+ };
458
+ }, [isEnabled, documentId, isLoading, family, model, intl, theme]);
459
+ return null;
460
+ };
461
+ const VariantPanel = ({ documentId, model, collectionType }) => {
462
+ const { formatMessage } = useIntl();
463
+ const navigate = useNavigate();
464
+ const currentLocale = React.useMemo(() => {
465
+ try {
466
+ const params = new URLSearchParams(window.location.search);
467
+ return params.get("plugins[i18n][locale]") || void 0;
468
+ } catch {
469
+ return void 0;
470
+ }
471
+ }, []);
472
+ const { schema } = unstable_useDocument({
473
+ model,
474
+ documentId,
475
+ collectionType
476
+ });
477
+ const isEnabled = isVariantEnabledContentType(schema?.pluginOptions);
478
+ const { family, isLoading: familyLoading, refetch } = useVariantFamily({
479
+ contentType: model || "",
480
+ documentId
481
+ });
482
+ const { deleteLink } = useVariantLinks({
483
+ baseContentType: model || "",
484
+ baseDocumentId: family?.baseDocumentId
485
+ });
486
+ if (!model || !documentId || !isEnabled) {
487
+ return null;
488
+ }
489
+ const handleNavigateTo = (targetId) => {
490
+ const localeQuery = currentLocale ? `?plugins[i18n][locale]=${encodeURIComponent(currentLocale)}` : "";
491
+ navigate(`/content-manager/collection-types/${model}/${targetId}${localeQuery}`);
492
+ };
493
+ const handleDelete = async (linkDocumentId, redirectToBase = false) => {
494
+ await deleteLink(linkDocumentId);
495
+ if (redirectToBase && family?.baseDocumentId) {
496
+ handleNavigateTo(family.baseDocumentId);
497
+ } else {
498
+ refetch();
499
+ }
500
+ };
501
+ const isVariant = family?.isVariant ?? false;
502
+ return {
503
+ title: formatMessage({
504
+ id: `${PLUGIN_ID}.variant-panel.title`,
505
+ defaultMessage: "Variants"
506
+ }),
507
+ content: /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 4, alignItems: "stretch", width: "100%", children: familyLoading ? /* @__PURE__ */ jsx(Typography, { variant: "pi", children: formatMessage({
508
+ id: `${PLUGIN_ID}.variant-panel.loading`,
509
+ defaultMessage: "Loading variants…"
510
+ }) }) : isVariant && family ? (
511
+ /* ---- Variant document view ---- */
512
+ /* @__PURE__ */ jsxs(Box, { children: [
513
+ /* @__PURE__ */ jsx(Box, { paddingBottom: 2, children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: formatMessage({
514
+ id: `${PLUGIN_ID}.variant-panel.is-variant`,
515
+ defaultMessage: "This is a variant document."
516
+ }) }) }),
517
+ (() => {
518
+ const currentLink = family.links.find(
519
+ (l) => l.variantDocumentId === documentId
520
+ );
521
+ if (!currentLink) return null;
522
+ const segmentNames = (currentLink.assignments || []).map((a) => a.segment?.name).filter(Boolean);
523
+ const label = segmentNames.length > 0 ? segmentNames.join(", ") : currentLink.label || formatMessage({
524
+ id: `${PLUGIN_ID}.variant-panel.no-segments`,
525
+ defaultMessage: "No segments"
526
+ });
527
+ return /* @__PURE__ */ jsxs(Flex, { gap: 2, marginBottom: 1, children: [
528
+ /* @__PURE__ */ jsx(
529
+ Button,
530
+ {
531
+ variant: "secondary",
532
+ size: "S",
533
+ style: { flex: 1, minWidth: 0 },
534
+ onClick: () => handleNavigateTo(family.baseDocumentId),
535
+ children: label
536
+ }
537
+ ),
538
+ /* @__PURE__ */ jsxs(Menu.Root, { children: [
539
+ /* @__PURE__ */ jsx(
540
+ Menu.Trigger,
541
+ {
542
+ tag: IconButton,
543
+ icon: /* @__PURE__ */ jsx(More, {}),
544
+ variant: "secondary",
545
+ size: "S",
546
+ label: formatMessage({
547
+ id: `${PLUGIN_ID}.variant-panel.actions`,
548
+ defaultMessage: "Variant actions"
549
+ })
550
+ }
551
+ ),
552
+ /* @__PURE__ */ jsxs(Menu.Content, { popoverPlacement: "bottom-end", children: [
553
+ /* @__PURE__ */ jsx(
554
+ Menu.Item,
555
+ {
556
+ onSelect: () => handleNavigateTo(family.baseDocumentId),
557
+ children: formatMessage({
558
+ id: `${PLUGIN_ID}.variant-panel.go-to-base`,
559
+ defaultMessage: "Go to base document"
560
+ })
561
+ }
562
+ ),
563
+ /* @__PURE__ */ jsx(
564
+ Menu.Item,
565
+ {
566
+ variant: "danger",
567
+ onSelect: () => handleDelete(
568
+ currentLink.documentId || String(currentLink.id),
569
+ true
570
+ ),
571
+ children: formatMessage({
572
+ id: `${PLUGIN_ID}.variant-panel.delete-link`,
573
+ defaultMessage: "Delete variant"
574
+ })
575
+ }
576
+ )
577
+ ] })
578
+ ] })
579
+ ] });
580
+ })()
581
+ ] })
582
+ ) : family ? (
583
+ /* ---- Base document view ---- */
584
+ /* @__PURE__ */ jsxs(Box, { children: [
585
+ /* @__PURE__ */ jsx(Box, { paddingBottom: 2, children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: formatMessage({
586
+ id: `${PLUGIN_ID}.variant-panel.base-help`,
587
+ defaultMessage: "Use the header dropdown to create new variants. Existing variants are listed below."
588
+ }) }) }),
589
+ family.links.length === 0 ? /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: formatMessage({
590
+ id: `${PLUGIN_ID}.variant-panel.no-variants`,
591
+ defaultMessage: "No variants yet. Use the segment dropdown in the header to create one."
592
+ }) }) : family.links.map((link) => {
593
+ const segmentNames = (link.assignments || []).map((a) => a.segment?.name).filter(Boolean);
594
+ const label = segmentNames.length > 0 ? segmentNames.join(", ") : link.label || formatMessage({
595
+ id: `${PLUGIN_ID}.variant-panel.no-segments`,
596
+ defaultMessage: "No segments"
597
+ });
598
+ return /* @__PURE__ */ jsxs(
599
+ Flex,
600
+ {
601
+ gap: 2,
602
+ marginBottom: 1,
603
+ children: [
604
+ /* @__PURE__ */ jsx(
605
+ Button,
606
+ {
607
+ variant: "secondary",
608
+ size: "S",
609
+ style: { flex: 1, minWidth: 0 },
610
+ onClick: () => link.variantDocumentId ? handleNavigateTo(link.variantDocumentId) : void 0,
611
+ children: label
612
+ }
613
+ ),
614
+ /* @__PURE__ */ jsxs(Menu.Root, { children: [
615
+ /* @__PURE__ */ jsx(
616
+ Menu.Trigger,
617
+ {
618
+ tag: IconButton,
619
+ icon: /* @__PURE__ */ jsx(More, {}),
620
+ variant: "secondary",
621
+ size: "S",
622
+ label: formatMessage({
623
+ id: `${PLUGIN_ID}.variant-panel.actions`,
624
+ defaultMessage: "Variant actions"
625
+ })
626
+ }
627
+ ),
628
+ /* @__PURE__ */ jsxs(Menu.Content, { popoverPlacement: "bottom-end", children: [
629
+ link.variantDocumentId && /* @__PURE__ */ jsx(
630
+ Menu.Item,
631
+ {
632
+ onSelect: () => handleNavigateTo(link.variantDocumentId),
633
+ children: formatMessage({
634
+ id: `${PLUGIN_ID}.variant-panel.open-variant`,
635
+ defaultMessage: "Open variant"
636
+ })
637
+ }
638
+ ),
639
+ /* @__PURE__ */ jsx(
640
+ Menu.Item,
641
+ {
642
+ variant: "danger",
643
+ onSelect: () => handleDelete(
644
+ link.documentId || String(link.id)
645
+ ),
646
+ children: formatMessage({
647
+ id: `${PLUGIN_ID}.variant-panel.delete-link`,
648
+ defaultMessage: "Delete variant"
649
+ })
650
+ }
651
+ )
652
+ ] })
653
+ ] })
654
+ ]
655
+ },
656
+ link.documentId || link.id
657
+ );
658
+ })
659
+ ] })
660
+ ) : null })
661
+ };
662
+ };
663
+ const VariantFieldIndicator = () => {
664
+ const { formatMessage } = useIntl();
665
+ const title = formatMessage({
666
+ id: `${PLUGIN_ID}.edit-view.variant-field`,
667
+ defaultMessage: "This field has per-segment variants"
668
+ });
669
+ return /* @__PURE__ */ jsx(Tooltip, { label: title, children: /* @__PURE__ */ jsx(
670
+ Flex,
671
+ {
672
+ tag: "span",
673
+ "aria-label": title,
674
+ role: "img",
675
+ justifyContent: "center",
676
+ alignItems: "center",
677
+ style: {
678
+ cursor: "help",
679
+ width: "1.5rem",
680
+ height: "1.5rem",
681
+ borderRadius: 4,
682
+ background: "#fef3c7",
683
+ color: "#d97706"
684
+ },
685
+ children: /* @__PURE__ */ jsx(Sparkle, { width: "1rem", height: "1rem" })
686
+ }
687
+ ) });
688
+ };
689
+ const addLabelActionToField = (field) => {
690
+ const attribute = field.attribute;
691
+ if (!attribute || !isVariantField(attribute)) {
692
+ return field;
693
+ }
694
+ return {
695
+ ...field,
696
+ labelAction: /* @__PURE__ */ jsx(VariantFieldIndicator, {})
697
+ };
698
+ };
699
+ const mutateEditViewHook = ({ layout }) => {
700
+ const options = layout.options || {};
701
+ if (!options?.[PLUGIN_ID]?.enabled) {
702
+ return { layout };
703
+ }
704
+ const decorateField = (field) => addLabelActionToField(field);
705
+ const components = Object.entries(layout.components).reduce(
706
+ (acc, [key, componentLayout]) => {
707
+ return {
708
+ ...acc,
709
+ [key]: {
710
+ ...componentLayout,
711
+ layout: componentLayout.layout.map((row) => row.map(decorateField))
712
+ }
713
+ };
714
+ },
715
+ {}
716
+ );
717
+ return {
718
+ layout: {
719
+ ...layout,
720
+ components,
721
+ layout: layout.layout.map((panel) => panel.map((row) => row.map(decorateField)))
722
+ }
723
+ };
724
+ };
725
+ const batches = /* @__PURE__ */ new Map();
726
+ function fetchLinksForDocument(postFn, contentType, documentId) {
727
+ return new Promise((resolve, reject) => {
728
+ let batch = batches.get(contentType);
729
+ if (!batch) {
730
+ batch = { pending: [], timer: null, postFn };
731
+ batches.set(contentType, batch);
732
+ }
733
+ batch.pending.push({ documentId, resolve, reject });
734
+ if (batch.timer) {
735
+ clearTimeout(batch.timer);
736
+ }
737
+ const currentBatch = batch;
738
+ currentBatch.timer = setTimeout(() => {
739
+ flushBatch(contentType, currentBatch);
740
+ }, 50);
741
+ });
742
+ }
743
+ async function flushBatch(contentType, batch) {
744
+ const requests = batch.pending.splice(0);
745
+ batch.timer = null;
746
+ batches.delete(contentType);
747
+ if (requests.length === 0) return;
748
+ const uniqueIds = [...new Set(requests.map((r) => r.documentId))];
749
+ try {
750
+ const pluginId2 = "content-variants";
751
+ const { data } = await batch.postFn(`/${pluginId2}/links/batch`, {
752
+ baseContentType: contentType,
753
+ documentIds: uniqueIds
754
+ });
755
+ const grouped = data || {};
756
+ for (const req of requests) {
757
+ req.resolve(grouped[req.documentId] || []);
758
+ }
759
+ } catch (err) {
760
+ for (const req of requests) {
761
+ req.reject(err);
762
+ }
763
+ }
764
+ }
765
+ const VariantSegmentCell = ({
766
+ documentId,
767
+ contentType
768
+ }) => {
769
+ const { post } = useFetchClient();
770
+ const { formatMessage } = useIntl();
771
+ const navigate = useNavigate();
772
+ const [links, setLinks] = React.useState(null);
773
+ React.useEffect(() => {
774
+ if (!documentId || !contentType) return;
775
+ let cancelled = false;
776
+ fetchLinksForDocument(
777
+ (url, config) => post(url, config),
778
+ contentType,
779
+ documentId
780
+ ).then((result) => {
781
+ if (!cancelled) setLinks(result);
782
+ }).catch(() => {
783
+ if (!cancelled) setLinks([]);
784
+ });
785
+ return () => {
786
+ cancelled = true;
787
+ };
788
+ }, [post, documentId, contentType]);
789
+ if (links === null) {
790
+ return /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "…" });
791
+ }
792
+ if (links.length === 0) {
793
+ return /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "--" });
794
+ }
795
+ const isVariant = links.some((l) => l.variantDocumentId === documentId);
796
+ const entries = [];
797
+ for (const link of links) {
798
+ for (const assignment of link.assignments || []) {
799
+ if (assignment.segment?.name) {
800
+ entries.push({
801
+ name: assignment.segment.name,
802
+ variantDocumentId: link.variantDocumentId,
803
+ baseDocumentId: link.baseDocumentId
804
+ });
805
+ }
806
+ }
807
+ }
808
+ if (entries.length === 0) {
809
+ return /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "--" });
810
+ }
811
+ const MAX_VISIBLE = 2;
812
+ const visibleNames = entries.slice(0, MAX_VISIBLE).map((e) => e.name);
813
+ const remaining = entries.length - MAX_VISIBLE;
814
+ const displayText = remaining > 0 ? formatMessage(
815
+ {
816
+ id: `${PLUGIN_ID}.list-view.more-segments`,
817
+ defaultMessage: "{names} +{count} more"
818
+ },
819
+ { names: visibleNames.join(", "), count: remaining }
820
+ ) : visibleNames.join(", ");
821
+ const currentLocale = (() => {
822
+ try {
823
+ const params = new URLSearchParams(window.location.search);
824
+ return params.get("plugins[i18n][locale]") || void 0;
825
+ } catch {
826
+ return void 0;
827
+ }
828
+ })();
829
+ const buildPath = (targetDocumentId) => {
830
+ const localeQuery = currentLocale ? `?plugins[i18n][locale]=${encodeURIComponent(currentLocale)}` : "";
831
+ return `/content-manager/collection-types/${contentType}/${targetDocumentId}${localeQuery}`;
832
+ };
833
+ return (
834
+ // Prevent clicks from propagating to the table row (which navigates to edit view)
835
+ /* @__PURE__ */ jsx("span", { onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxs(Menu.Root, { children: [
836
+ /* @__PURE__ */ jsx(Menu.Trigger, { children: /* @__PURE__ */ jsx(
837
+ Flex,
838
+ {
839
+ minWidth: "100%",
840
+ alignItems: "center",
841
+ justifyContent: "center",
842
+ gap: 1,
843
+ children: /* @__PURE__ */ jsx(
844
+ Typography,
845
+ {
846
+ textColor: "primary600",
847
+ variant: "pi",
848
+ children: isVariant ? formatMessage(
849
+ {
850
+ id: `${PLUGIN_ID}.list-view.variant-suffix`,
851
+ defaultMessage: "{segments} (variant)"
852
+ },
853
+ { segments: displayText }
854
+ ) : displayText
855
+ }
856
+ )
857
+ }
858
+ ) }),
859
+ /* @__PURE__ */ jsxs(Menu.Content, { children: [
860
+ isVariant && entries.length > 0 && /* @__PURE__ */ jsx(
861
+ Menu.Item,
862
+ {
863
+ onClick: (e) => {
864
+ e.stopPropagation();
865
+ navigate(buildPath(entries[0].baseDocumentId));
866
+ },
867
+ children: /* @__PURE__ */ jsx(Typography, { children: formatMessage({
868
+ id: `${PLUGIN_ID}.variant-picker.base`,
869
+ defaultMessage: "Base (default)"
870
+ }) })
871
+ }
872
+ ),
873
+ entries.map((entry, i) => /* @__PURE__ */ jsx(
874
+ Menu.Item,
875
+ {
876
+ onClick: (e) => {
877
+ e.stopPropagation();
878
+ navigate(buildPath(entry.variantDocumentId));
879
+ },
880
+ children: /* @__PURE__ */ jsx(Typography, { children: entry.name })
881
+ },
882
+ i
883
+ ))
884
+ ] })
885
+ ] }) })
886
+ );
887
+ };
888
+ const addVariantColumnHook = ({
889
+ displayedHeaders,
890
+ layout
891
+ }) => {
892
+ const options = layout.options || {};
893
+ const isEnabled = options?.[PLUGIN_ID]?.enabled;
894
+ if (!isEnabled) {
895
+ return { displayedHeaders, layout };
896
+ }
897
+ const match = window.location.pathname.match(/collection-types\/([^/]+)/);
898
+ const contentType = match ? decodeURIComponent(match[1]) : "";
899
+ if (!contentType || !/^[a-zA-Z0-9:._-]+$/.test(contentType)) {
900
+ return { displayedHeaders, layout };
901
+ }
902
+ return {
903
+ displayedHeaders: [
904
+ ...displayedHeaders,
905
+ {
906
+ attribute: { type: "string" },
907
+ label: {
908
+ id: `${PLUGIN_ID}.list-view.segments-column`,
909
+ defaultMessage: "Segments"
910
+ },
911
+ searchable: false,
912
+ sortable: false,
913
+ name: "variants",
914
+ cellFormatter: (props, _header, _meta) => /* @__PURE__ */ jsx(
915
+ VariantSegmentCell,
916
+ {
917
+ documentId: props.documentId,
918
+ contentType
919
+ }
920
+ )
921
+ }
922
+ ],
923
+ layout
924
+ };
925
+ };
926
+ const VARIANT_FIELD_TYPES = ["string", "text", "richtext", "media", "blocks"];
927
+ const index = {
928
+ register(app) {
929
+ app.registerPlugin({
930
+ id: pluginId,
931
+ initializer: Initializer,
932
+ isReady: false,
933
+ name: pluginId
934
+ });
935
+ },
936
+ bootstrap(app) {
937
+ app.addSettingsLink("global", {
938
+ intlLabel: {
939
+ id: `${pluginId}.settings.link`,
940
+ defaultMessage: "Content Variants"
941
+ },
942
+ id: pluginId,
943
+ to: `/settings/${pluginId}`,
944
+ Component: () => import("./Segments-BgxnvvtR.mjs")
945
+ });
946
+ const ctbPlugin = app.getPlugin("content-type-builder");
947
+ if (ctbPlugin) {
948
+ const ctbFormsAPI = ctbPlugin.apis.forms;
949
+ ctbFormsAPI.extendContentType({
950
+ form: {
951
+ advanced() {
952
+ return [
953
+ {
954
+ name: "pluginOptions.content-variants.enabled",
955
+ type: "checkbox",
956
+ intlLabel: {
957
+ id: `${pluginId}.contentType.enabled`,
958
+ defaultMessage: "Enable content variants"
959
+ },
960
+ description: {
961
+ id: `${pluginId}.contentType.enabled.description`,
962
+ defaultMessage: "Allow segment-based content variants for this content type"
963
+ }
964
+ }
965
+ ];
966
+ }
967
+ }
968
+ });
969
+ ctbFormsAPI.extendFields(VARIANT_FIELD_TYPES, {
970
+ form: {
971
+ advanced({ contentTypeSchema, forTarget }) {
972
+ if (forTarget !== "contentType") {
973
+ return [];
974
+ }
975
+ return [
976
+ {
977
+ name: "pluginOptions.content-variants.variant",
978
+ type: "checkbox",
979
+ intlLabel: {
980
+ id: `${pluginId}.field.variant`,
981
+ defaultMessage: "Enable variants for this field"
982
+ },
983
+ description: {
984
+ id: `${pluginId}.field.variant.description`,
985
+ defaultMessage: "When enabled, this field can have different values per segment"
986
+ }
987
+ }
988
+ ];
989
+ }
990
+ }
991
+ });
992
+ }
993
+ const contentManager = app.getPlugin("content-manager");
994
+ if (contentManager) {
995
+ contentManager.apis.addDocumentHeaderAction([VariantPickerAction]);
996
+ contentManager.apis.addDocumentAction([VariantInfoAction]);
997
+ contentManager.apis.addEditViewSidePanel([VariantPanel]);
998
+ }
999
+ app.registerHook(
1000
+ "Admin/CM/pages/EditView/mutate-edit-view-layout",
1001
+ mutateEditViewHook
1002
+ );
1003
+ app.registerHook(
1004
+ "Admin/CM/pages/ListView/inject-column-in-table",
1005
+ addVariantColumnHook
1006
+ );
1007
+ },
1008
+ async registerTrads({ locales }) {
1009
+ const enTranslations = (await import("./en-e_966kWj.mjs")).default;
1010
+ return locales.map((locale) => ({
1011
+ locale,
1012
+ data: locale === "en" ? enTranslations : {}
1013
+ }));
1014
+ }
1015
+ };
1016
+ export {
1017
+ PLUGIN_ID as P,
1018
+ index as i,
1019
+ useSegments as u
1020
+ };