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