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