@sanity/personalization-plugin 2.4.1 → 2.5.0-field-level-personalization.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +107 -5
  2. package/dist/_chunks-cjs/fieldExperiments.js +507 -0
  3. package/dist/_chunks-cjs/fieldExperiments.js.map +1 -0
  4. package/dist/_chunks-es/fieldExperiments.mjs +511 -0
  5. package/dist/_chunks-es/fieldExperiments.mjs.map +1 -0
  6. package/dist/growthbook/index.js +3 -3
  7. package/dist/growthbook/index.js.map +1 -1
  8. package/dist/growthbook/index.mjs +1 -1
  9. package/dist/index.d.mts +33 -12
  10. package/dist/index.d.ts +33 -12
  11. package/dist/index.js +158 -277
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.mjs +160 -277
  14. package/dist/index.mjs.map +1 -1
  15. package/package.json +20 -20
  16. package/src/components/ArrayItem.tsx +9 -0
  17. package/src/components/Select.tsx +1 -1
  18. package/src/components/{Array.tsx → experiment/Array.tsx} +2 -2
  19. package/src/components/{ExperimentContext.tsx → experiment/Context.tsx} +2 -2
  20. package/src/components/{ExperimentField.tsx → experiment/Field.tsx} +11 -8
  21. package/src/components/{ExperimentInput.tsx → experiment/Input.tsx} +4 -4
  22. package/src/components/{VariantInput.tsx → experiment/VariantInput.tsx} +2 -1
  23. package/src/components/{VariantPreview.tsx → experiment/VariantPreview.tsx} +2 -2
  24. package/src/components/experiment/index.ts +6 -0
  25. package/src/components/personalization/Array.tsx +59 -0
  26. package/src/components/personalization/Context.tsx +61 -0
  27. package/src/components/personalization/Field.tsx +134 -0
  28. package/src/components/personalization/SegmentInput.tsx +19 -0
  29. package/src/components/personalization/SegmentPreview.tsx +71 -0
  30. package/src/components/personalization/index.ts +5 -0
  31. package/src/fieldExperiments.tsx +44 -12
  32. package/src/fieldPersonalization.tsx +254 -0
  33. package/src/index.ts +1 -0
  34. package/src/types.ts +20 -2
  35. package/src/utils/clearChildGroups.ts +33 -0
@@ -0,0 +1,511 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { set, useClient, useWorkspace, useFormValue, defineDocumentFieldAction, unset, useDocumentOperation, getPublishedId, isReference, isImage, isDocumentSchemaType, definePlugin, isObjectInputProps, defineType, defineField } from "sanity";
3
+ import { Stack, Inline, Button, Select as Select$1, Card, Text } from "@sanity/ui";
4
+ import { uuid } from "@sanity/uuid";
5
+ import { createContext, useMemo, useContext, useCallback, forwardRef, useState, useEffect } from "react";
6
+ import equal from "fast-deep-equal";
7
+ import { suspend } from "suspend-react";
8
+ import { GiSoapExperiment } from "react-icons/gi";
9
+ const ArrayItem = (props) => {
10
+ const { active } = props.value;
11
+ return active || props.inputProps.onChange(set(!0, ["active"])), props.renderDefault(props);
12
+ }, CONFIG_DEFAULT = {
13
+ fields: [],
14
+ apiVersion: "2024-11-07",
15
+ experimentNameOverride: "experiment",
16
+ variantNameOverride: "variant",
17
+ variantId: "variantId",
18
+ variantArrayName: "variants",
19
+ experimentId: "experimentId"
20
+ }, ExperimentContext = createContext({
21
+ ...CONFIG_DEFAULT,
22
+ experiments: []
23
+ });
24
+ function useExperimentContext() {
25
+ return useContext(ExperimentContext);
26
+ }
27
+ function ExperimentProvider(props) {
28
+ const { experimentFieldPluginConfig } = props, client = useClient({ apiVersion: experimentFieldPluginConfig.apiVersion }), workspace = useWorkspace(), experiments = Array.isArray(experimentFieldPluginConfig.experiments) ? experimentFieldPluginConfig.experiments : suspend(
29
+ // eslint-disable-next-line require-await
30
+ async () => typeof experimentFieldPluginConfig.experiments == "function" ? experimentFieldPluginConfig.experiments(client) : experimentFieldPluginConfig.experiments,
31
+ [workspace],
32
+ { equal }
33
+ ), context = useMemo(
34
+ () => ({ ...experimentFieldPluginConfig, experiments }),
35
+ [experimentFieldPluginConfig, experiments]
36
+ );
37
+ return /* @__PURE__ */ jsx(ExperimentContext.Provider, { value: context, children: props.renderDefault(props) });
38
+ }
39
+ const ArrayInput = (props) => {
40
+ const fieldPath = props.path.slice(0, -1), { onItemAppend, variantName, variantId, experimentId } = props, experimentValue = useFormValue([...fieldPath, experimentId]), { experiments } = useExperimentContext(), handleClick = useCallback(
41
+ async (variant) => {
42
+ const item = {
43
+ _key: uuid(),
44
+ [variantId]: variant.id,
45
+ [experimentId]: experimentValue,
46
+ _type: variantName
47
+ };
48
+ onItemAppend(item);
49
+ },
50
+ [variantId, experimentId, experimentValue, variantName, onItemAppend]
51
+ ), filteredVariants = experiments.find((option) => option.id === experimentValue)?.variants || [], usedVariants = (props.value || [])?.map((variant) => variant[variantId]);
52
+ return /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
53
+ props.renderDefault({ ...props, arrayFunctions: () => null }),
54
+ /* @__PURE__ */ jsx(Inline, { space: 1, children: filteredVariants.map((variant) => /* @__PURE__ */ jsx(
55
+ Button,
56
+ {
57
+ text: `Add ${variant.label}`,
58
+ mode: "ghost",
59
+ disabled: usedVariants?.includes(variant.id),
60
+ onClick: () => handleClick(variant)
61
+ },
62
+ `${experimentValue}-${variant.id}`
63
+ )) })
64
+ ] });
65
+ }, CloseIcon = /* @__PURE__ */ forwardRef(function(props, ref) {
66
+ return /* @__PURE__ */ jsx(
67
+ "svg",
68
+ {
69
+ "data-sanity-icon": "close",
70
+ width: "1em",
71
+ height: "1em",
72
+ viewBox: "0 0 25 25",
73
+ fill: "none",
74
+ xmlns: "http://www.w3.org/2000/svg",
75
+ ...props,
76
+ ref,
77
+ children: /* @__PURE__ */ jsx(
78
+ "path",
79
+ {
80
+ d: "M18 7L7 18M7 7L18 18",
81
+ stroke: "currentColor",
82
+ strokeWidth: 1.2,
83
+ strokeLinejoin: "round"
84
+ }
85
+ )
86
+ }
87
+ );
88
+ }), clearChildrenGroups = (props) => {
89
+ const children = props.children;
90
+ return !children || typeof children != "object" || !children.props ? props : {
91
+ ...props,
92
+ children: {
93
+ ...children,
94
+ props: {
95
+ ...children.props,
96
+ children: {
97
+ ...children.props.children,
98
+ props: {
99
+ ...children.props.children?.props,
100
+ groups: []
101
+ }
102
+ }
103
+ }
104
+ }
105
+ };
106
+ }, useAddExperimentAction = (props) => {
107
+ const { onChange, active, experimentNameOverride } = props, handleAddAction = useCallback(() => {
108
+ onChange([set(!active, ["active"])]);
109
+ }, [onChange, active]);
110
+ return {
111
+ title: `Add ${experimentNameOverride}`,
112
+ type: "action",
113
+ icon: GiSoapExperiment,
114
+ onAction: handleAddAction,
115
+ renderAsButton: !0
116
+ };
117
+ }, useRemoveExperimentAction = (props) => {
118
+ const { onChange, active, experimentId, experimentNameOverride, variantNameOverride } = props, handleClearAction = useCallback(() => {
119
+ const activeId = ["active"], experiment = [experimentId], variants = [`${variantNameOverride}s`];
120
+ onChange([set(!active, activeId), unset(experiment), unset(variants)]);
121
+ }, [onChange, active, experimentId, variantNameOverride]);
122
+ return {
123
+ title: `Remove ${experimentNameOverride}`,
124
+ type: "action",
125
+ icon: CloseIcon,
126
+ onAction: handleClearAction,
127
+ renderAsButton: !0
128
+ };
129
+ }, createActions = ({
130
+ onChange,
131
+ inputId,
132
+ active,
133
+ experimentNameOverride,
134
+ experimentId,
135
+ variantNameOverride
136
+ }) => {
137
+ const removeAction = defineDocumentFieldAction({
138
+ name: `Remove ${experimentNameOverride}`,
139
+ useAction: (props) => useRemoveExperimentAction({
140
+ active: !0,
141
+ onChange,
142
+ experimentNameOverride,
143
+ experimentId,
144
+ variantNameOverride
145
+ })
146
+ }), addAction = defineDocumentFieldAction({
147
+ name: `Add ${experimentNameOverride}`,
148
+ useAction: (props) => useAddExperimentAction({
149
+ active: !1,
150
+ onChange,
151
+ experimentNameOverride
152
+ })
153
+ });
154
+ return active ? removeAction : addAction;
155
+ }, Field = (props) => {
156
+ const { onChange } = props.inputProps, { inputId, experimentNameOverride, experimentId, variantNameOverride } = props, active = props.value?.active, actionProps = useMemo(
157
+ () => ({
158
+ onChange,
159
+ inputId,
160
+ active,
161
+ experimentNameOverride,
162
+ experimentId,
163
+ variantNameOverride
164
+ }),
165
+ [onChange, inputId, active, experimentNameOverride, experimentId, variantNameOverride]
166
+ ), memoizedActions = useMemo(() => {
167
+ const oldActions = props.actions || [];
168
+ return [createActions(actionProps), ...oldActions];
169
+ }, [actionProps, props.actions]), enhancedProps = useMemo(() => ({
170
+ ...clearChildrenGroups(props),
171
+ actions: memoizedActions
172
+ }), [props, memoizedActions]);
173
+ return props.renderDefault(enhancedProps);
174
+ }, Select = (props) => {
175
+ const {
176
+ value,
177
+ // Current field value
178
+ onChange,
179
+ // Method to handle patch events,
180
+ elementProps,
181
+ listOptions,
182
+ handleChange
183
+ } = props;
184
+ return /* @__PURE__ */ jsxs(
185
+ Select$1,
186
+ {
187
+ ...elementProps,
188
+ fontSize: 2,
189
+ padding: 3,
190
+ space: [3, 3, 4],
191
+ value: value || "",
192
+ onChange: (event) => handleChange(event, onChange),
193
+ children: [
194
+ /* @__PURE__ */ jsx("option", { value: "", children: "Select an option..." }),
195
+ listOptions.map(({ value: optionValue, title }) => /* @__PURE__ */ jsx("option", { value: optionValue, children: title }, optionValue))
196
+ ]
197
+ }
198
+ );
199
+ }, formatlistOptions = (experiments) => experiments.map((experiment) => ({
200
+ title: experiment.label,
201
+ value: experiment.id
202
+ })), Input = (props) => {
203
+ const { experiments } = useExperimentContext(), id = useFormValue(["_id"]), additionalChangePath = useMemo(
204
+ () => [...props.path.slice(0, -1), `${props.variantNameOverride}s`],
205
+ [props.variantNameOverride, props.path]
206
+ ), subValues = useFormValue(additionalChangePath), { patch } = useDocumentOperation(getPublishedId(id), props.schemaType.name), handleChange = useCallback(
207
+ (event, onChange) => {
208
+ const inputValue = event.currentTarget.value;
209
+ if (onChange(inputValue ? set(inputValue) : unset()), subValues) {
210
+ const patchEvent = {
211
+ unset: [additionalChangePath.join(".")]
212
+ };
213
+ patch.execute([patchEvent]);
214
+ }
215
+ },
216
+ [patch, subValues, additionalChangePath]
217
+ );
218
+ return experiments.length ? /* @__PURE__ */ jsx(Select, { ...props, listOptions: formatlistOptions(experiments), handleChange }) : /* @__PURE__ */ jsx(Card, { padding: [3, 3, 4], radius: 2, shadow: 1, tone: "caution", children: /* @__PURE__ */ jsxs(Text, { align: "center", size: [2, 2, 3], children: [
219
+ "There are no defined ",
220
+ props.experimentNameOverride,
221
+ "s"
222
+ ] }) });
223
+ }, VariantInput = (props) => {
224
+ const experimentPath = props.path.slice(0, -2), defaultValue = useFormValue([...experimentPath, "default"]), handleClick = () => {
225
+ props.onChange(set(defaultValue, ["value"]));
226
+ };
227
+ return /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
228
+ props.renderDefault(props),
229
+ /* @__PURE__ */ jsx(Inline, { space: 1, children: /* @__PURE__ */ jsx(Button, { text: "Copy default", mode: "ghost", onClick: () => handleClick() }) })
230
+ ] });
231
+ }, VariantPreview = (props) => {
232
+ const [subtitle, setSubtitle] = useState(void 0), [title, setTitle] = useState(void 0), [media, setMedia] = useState(void 0), client = useClient({ apiVersion: "2025-01-01" }), { experiments } = useExperimentContext(), { experiment, variant, value } = props, selectedExperiment = experiments.find((experimentItem) => experimentItem.id === experiment), selectedVariant = selectedExperiment?.variants.find((variantItem) => variantItem.id === variant);
233
+ useEffect(() => {
234
+ (async () => {
235
+ if (setTitle(`${selectedExperiment?.label} - ${selectedVariant?.label}`), typeof value == "string")
236
+ return setSubtitle(value);
237
+ if (isReference(value)) {
238
+ const doc = await client.getDocument(value._ref), referenceType = (props.schemaType.fields.find((field) => field.name === "value")?.type).to.find((field) => field.type?.name === doc?._type), selectFields = {}, previewFields = referenceType?.preview?.select || {};
239
+ Object.keys(previewFields).forEach((key) => {
240
+ const valueKey = referenceType?.preview?.select?.[key];
241
+ selectFields[key] = valueKey && doc ? valueKey?.split(".").reduce((acc, index) => acc[index], doc) : void 0;
242
+ });
243
+ const previewContent = referenceType?.preview?.prepare?.(selectFields);
244
+ return setMedia(previewContent?.media || selectFields.media), setSubtitle(previewContent?.title || selectFields?.title);
245
+ }
246
+ return isImage(value) && setMedia(value), "";
247
+ })();
248
+ }, [value, client, selectedExperiment?.label, selectedVariant?.label, props.schemaType]);
249
+ const previewProps = {
250
+ ...props,
251
+ title,
252
+ subtitle,
253
+ media
254
+ };
255
+ return props.renderDefault(previewProps);
256
+ };
257
+ function flattenSchemaType(schemaType) {
258
+ return isDocumentSchemaType(schemaType) ? extractInnerFields(schemaType.fields, [], 5) : (console.error("Schema type is not a document"), []);
259
+ }
260
+ function extractInnerFields(fields, path, maxDepth) {
261
+ return path.length >= maxDepth ? [] : fields.reduce((acc, field) => {
262
+ const thisFieldWithPath = { path: [...path, field.name], ...field };
263
+ if (field.type.jsonType === "object") {
264
+ const innerFields = extractInnerFields(field.type.fields, [...path, field.name], maxDepth);
265
+ return [...acc, thisFieldWithPath, ...innerFields];
266
+ } else if (field.type.jsonType === "array") {
267
+ const innerFields = (field.type.of || []).reduce((arrayAcc, arrayType) => {
268
+ if ("fields" in arrayType) {
269
+ const typeFields = extractInnerFields(arrayType.fields, [...path, field.name], maxDepth);
270
+ return [...arrayAcc, ...typeFields];
271
+ }
272
+ return arrayAcc;
273
+ }, []);
274
+ return [...acc, thisFieldWithPath, ...innerFields];
275
+ }
276
+ return [...acc, thisFieldWithPath];
277
+ }, []);
278
+ }
279
+ const createExperimentType = ({
280
+ field,
281
+ experimentNameOverride,
282
+ variantNameOverride,
283
+ variantId,
284
+ variantArrayName,
285
+ experimentId
286
+ }) => {
287
+ const typeName = typeof field == "string" ? field : field.name, usedName = String(typeName[0]).toUpperCase() + String(typeName).slice(1), variantName = `${variantNameOverride}${usedName}`;
288
+ return defineType({
289
+ name: `${experimentNameOverride}${usedName}`,
290
+ type: "object",
291
+ groups: [
292
+ {
293
+ name: "default",
294
+ title: "Default",
295
+ hidden: ({ parent }) => !Array.isArray(parent)
296
+ },
297
+ {
298
+ name: "experiments",
299
+ title: "Experiments",
300
+ hidden: ({ parent }) => !Array.isArray(parent)
301
+ },
302
+ {
303
+ name: "all-fields",
304
+ title: "All fields",
305
+ hidden: ({ parent }) => Array.isArray(parent)
306
+ }
307
+ ],
308
+ components: {
309
+ field: (props) => /* @__PURE__ */ jsx(
310
+ Field,
311
+ {
312
+ ...props,
313
+ experimentId,
314
+ experimentNameOverride,
315
+ variantNameOverride
316
+ }
317
+ ),
318
+ item: ArrayItem
319
+ },
320
+ fields: [
321
+ typeof field == "string" ? (
322
+ // Define a simple field if all we have is the name as a string
323
+ defineField({
324
+ name: "default",
325
+ type: field,
326
+ group: "default"
327
+ })
328
+ ) : (
329
+ // Pass in the configured options, but overwrite the name
330
+ {
331
+ ...field,
332
+ name: "default",
333
+ group: "default"
334
+ }
335
+ ),
336
+ defineField({
337
+ name: "active",
338
+ type: "boolean",
339
+ hidden: !0,
340
+ initialValue: !1
341
+ }),
342
+ defineField({
343
+ name: experimentId,
344
+ type: "string",
345
+ group: "experiments",
346
+ components: {
347
+ input: (props) => /* @__PURE__ */ jsx(
348
+ Input,
349
+ {
350
+ ...props,
351
+ experimentNameOverride,
352
+ variantNameOverride
353
+ }
354
+ )
355
+ },
356
+ hidden: ({ parent }) => !parent?.active
357
+ }),
358
+ defineField({
359
+ name: variantArrayName,
360
+ type: "array",
361
+ group: "experiments",
362
+ hidden: ({ parent }) => !parent?.[experimentId],
363
+ components: {
364
+ input: (props) => /* @__PURE__ */ jsx(
365
+ ArrayInput,
366
+ {
367
+ ...props,
368
+ variantName,
369
+ variantId,
370
+ experimentId
371
+ }
372
+ )
373
+ },
374
+ of: [
375
+ defineField({
376
+ name: variantName,
377
+ type: variantName
378
+ })
379
+ ]
380
+ })
381
+ ],
382
+ preview: {
383
+ select: {
384
+ base: "default",
385
+ experiment: experimentId
386
+ },
387
+ prepare: ({ base, experiment }) => {
388
+ const title = base?.title || base?.name || typeof base == "string" ? base : "", experimentTitle = experiment ? `Experiment: ${experiment}` : "", media = base?.image || base?.photo || base?.media || "";
389
+ return {
390
+ title: title || experimentTitle,
391
+ subtitle: title ? experimentTitle : "",
392
+ media
393
+ };
394
+ }
395
+ }
396
+ });
397
+ }, createVariantType = ({
398
+ field,
399
+ variantNameOverride,
400
+ variantId,
401
+ experimentId
402
+ }) => {
403
+ const typeName = typeof field == "string" ? field : field.name, usedName = String(typeName[0]).toUpperCase() + String(typeName).slice(1);
404
+ return defineType({
405
+ name: `${variantNameOverride}${usedName}`,
406
+ title: `${variantNameOverride} array ${usedName}`,
407
+ type: "object",
408
+ components: {
409
+ preview: VariantPreview,
410
+ input: VariantInput
411
+ },
412
+ fields: [
413
+ {
414
+ type: "string",
415
+ name: variantId,
416
+ readOnly: !0
417
+ },
418
+ {
419
+ type: "string",
420
+ name: experimentId,
421
+ hidden: !0
422
+ },
423
+ typeof field == "string" ? (
424
+ // Define a simple field if all we have is the name as a string
425
+ defineField({
426
+ name: "value",
427
+ type: field
428
+ // hidden: ({parent}) => !parent?.[`${objectNameOverride}Id`],
429
+ })
430
+ ) : (
431
+ // Pass in the configured options, but overwrite the name
432
+ {
433
+ ...field,
434
+ name: "value"
435
+ // hidden: ({parent}) => !parent?.[`${objectNameOverride}Id`],
436
+ }
437
+ )
438
+ ],
439
+ preview: {
440
+ select: {
441
+ variant: variantId,
442
+ experiment: experimentId,
443
+ value: "value"
444
+ }
445
+ }
446
+ });
447
+ }, fieldSchema = ({
448
+ fields,
449
+ experimentNameOverride,
450
+ variantNameOverride,
451
+ variantId,
452
+ variantArrayName,
453
+ experimentId
454
+ }) => [
455
+ ...fields.map(
456
+ (field) => createVariantType({ field, variantNameOverride, variantId, experimentId })
457
+ ),
458
+ ...fields.map(
459
+ (field) => createExperimentType({
460
+ field,
461
+ experimentNameOverride,
462
+ variantNameOverride,
463
+ variantId,
464
+ variantArrayName,
465
+ experimentId
466
+ })
467
+ )
468
+ ], fieldLevelExperiments = definePlugin((config) => {
469
+ const pluginConfig = { ...CONFIG_DEFAULT, ...config }, { fields, experimentNameOverride, variantNameOverride } = pluginConfig, experimentId = `${experimentNameOverride}Id`, variantArrayName = `${variantNameOverride}s`, variantId = `${variantNameOverride}Id`;
470
+ return {
471
+ name: "sanity-personalistaion-plugin-field-level-experiments",
472
+ schema: {
473
+ types: fieldSchema({
474
+ fields,
475
+ experimentNameOverride,
476
+ variantNameOverride,
477
+ variantId,
478
+ variantArrayName,
479
+ experimentId
480
+ })
481
+ },
482
+ form: {
483
+ components: {
484
+ input: (props) => {
485
+ if (!(props.id === "root" && isObjectInputProps(props)) || !flattenSchemaType(props.schemaType).some(
486
+ (field) => field.type.name.startsWith(experimentNameOverride) || field.name.startsWith(experimentNameOverride)
487
+ ))
488
+ return props.renderDefault(props);
489
+ const providerProps = {
490
+ ...props,
491
+ experimentFieldPluginConfig: {
492
+ ...pluginConfig,
493
+ variantId,
494
+ variantArrayName,
495
+ experimentId
496
+ }
497
+ };
498
+ return ExperimentProvider(providerProps);
499
+ }
500
+ }
501
+ }
502
+ };
503
+ });
504
+ export {
505
+ ArrayItem,
506
+ CloseIcon,
507
+ clearChildrenGroups,
508
+ fieldLevelExperiments,
509
+ flattenSchemaType
510
+ };
511
+ //# sourceMappingURL=fieldExperiments.mjs.map