@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
@@ -7,13 +7,17 @@ import {
7
7
  isObjectInputProps,
8
8
  } from 'sanity'
9
9
 
10
- import {ArrayInput} from './components/Array'
11
- import {CONFIG_DEFAULT, ExperimentProvider} from './components/ExperimentContext'
12
- import {ExperimentField} from './components/ExperimentField'
13
- import {ExperimentInput} from './components/ExperimentInput'
14
- import {VariantInput} from './components/VariantInput'
15
- import {VariantPreview} from './components/VariantPreview'
16
- import {FieldPluginConfig} from './types'
10
+ import {ArrayItem} from './components/ArrayItem'
11
+ import {
12
+ ArrayInput,
13
+ CONFIG_DEFAULT,
14
+ ExperimentProvider,
15
+ Field,
16
+ Input,
17
+ VariantInput,
18
+ VariantPreview,
19
+ } from './components/experiment'
20
+ import {ExperimentFieldPluginConfig} from './types'
17
21
  import {flattenSchemaType} from './utils/flattenSchemaType'
18
22
 
19
23
  const createExperimentType = ({
@@ -38,15 +42,39 @@ const createExperimentType = ({
38
42
  return defineType({
39
43
  name: `${experimentNameOverride}${usedName}`,
40
44
  type: 'object',
45
+ groups: [
46
+ {
47
+ name: 'default',
48
+ title: 'Default',
49
+ hidden: ({parent}) => {
50
+ return !Array.isArray(parent)
51
+ },
52
+ },
53
+ {
54
+ name: 'experiments',
55
+ title: 'Experiments',
56
+ hidden: ({parent}) => {
57
+ return !Array.isArray(parent)
58
+ },
59
+ },
60
+ {
61
+ name: 'all-fields',
62
+ title: 'All fields',
63
+ hidden: ({parent}) => {
64
+ return Array.isArray(parent)
65
+ },
66
+ },
67
+ ],
41
68
  components: {
42
69
  field: (props) => (
43
- <ExperimentField
70
+ <Field
44
71
  {...props}
45
72
  experimentId={experimentId}
46
73
  experimentNameOverride={experimentNameOverride}
47
74
  variantNameOverride={variantNameOverride}
48
75
  />
49
76
  ),
77
+ item: ArrayItem,
50
78
  },
51
79
  fields: [
52
80
  typeof field === `string`
@@ -54,11 +82,13 @@ const createExperimentType = ({
54
82
  defineField({
55
83
  name: 'default',
56
84
  type: field,
85
+ group: 'default',
57
86
  })
58
87
  : // Pass in the configured options, but overwrite the name
59
88
  {
60
89
  ...field,
61
90
  name: 'default',
91
+ group: 'default',
62
92
  },
63
93
  defineField({
64
94
  name: 'active',
@@ -69,9 +99,10 @@ const createExperimentType = ({
69
99
  defineField({
70
100
  name: experimentId,
71
101
  type: 'string',
102
+ group: 'experiments',
72
103
  components: {
73
104
  input: (props) => (
74
- <ExperimentInput
105
+ <Input
75
106
  {...props}
76
107
  experimentNameOverride={experimentNameOverride}
77
108
  variantNameOverride={variantNameOverride}
@@ -85,6 +116,7 @@ const createExperimentType = ({
85
116
  defineField({
86
117
  name: variantArrayName,
87
118
  type: 'array',
119
+ group: 'experiments',
88
120
  hidden: ({parent}) => {
89
121
  return !parent?.[experimentId]
90
122
  },
@@ -112,7 +144,7 @@ const createExperimentType = ({
112
144
  experiment: experimentId,
113
145
  },
114
146
  prepare: ({base, experiment}) => {
115
- const title = base?.title || base?.name || ''
147
+ const title = base?.title || base?.name || typeof base === 'string' ? base : ''
116
148
  const experimentTitle = experiment ? `Experiment: ${experiment}` : ''
117
149
  const media = base?.image || base?.photo || base?.media || ''
118
150
  return {
@@ -188,7 +220,7 @@ const fieldSchema = ({
188
220
  variantId,
189
221
  variantArrayName,
190
222
  experimentId,
191
- }: Required<Omit<FieldPluginConfig, 'apiVersion' | 'experiments'>>) => {
223
+ }: Required<Omit<ExperimentFieldPluginConfig, 'apiVersion' | 'experiments'>>) => {
192
224
  return [
193
225
  ...fields.map((field) =>
194
226
  createVariantType({field, variantNameOverride, variantId, experimentId}),
@@ -206,7 +238,7 @@ const fieldSchema = ({
206
238
  ]
207
239
  }
208
240
 
209
- export const fieldLevelExperiments = definePlugin<FieldPluginConfig>((config) => {
241
+ export const fieldLevelExperiments = definePlugin<ExperimentFieldPluginConfig>((config) => {
210
242
  const pluginConfig = {...CONFIG_DEFAULT, ...config}
211
243
  const {fields, experimentNameOverride, variantNameOverride} = pluginConfig
212
244
 
@@ -0,0 +1,254 @@
1
+ import {
2
+ ArrayOfObjectsInputProps,
3
+ defineField,
4
+ definePlugin,
5
+ defineType,
6
+ FieldDefinition,
7
+ isObjectInputProps,
8
+ } from 'sanity'
9
+
10
+ import {ArrayItem} from './components/ArrayItem'
11
+ import {
12
+ ArrayInput,
13
+ CONFIG_DEFAULT,
14
+ Field,
15
+ PersonalizationProvider,
16
+ SegmentInput,
17
+ SegmentPreview,
18
+ } from './components/personalization'
19
+ import {PersonalizationFieldPluginConfig} from './types'
20
+ import {flattenSchemaType} from './utils/flattenSchemaType'
21
+
22
+ const createPersonalizationType = ({
23
+ field,
24
+ personalizationNameOverride,
25
+ segmentNameOverride,
26
+ segmentId,
27
+ segmentArrayName,
28
+ }: {
29
+ field: string | FieldDefinition
30
+ personalizationNameOverride: string
31
+ segmentNameOverride: string
32
+ segmentId: string
33
+ segmentArrayName: string
34
+ }) => {
35
+ const typeName = typeof field === `string` ? field : field.name
36
+ const usedName = String(typeName[0]).toUpperCase() + String(typeName).slice(1)
37
+ const segmentName = `${segmentNameOverride}${usedName}`
38
+
39
+ return defineType({
40
+ name: `${personalizationNameOverride}${usedName}`,
41
+ type: 'object',
42
+ groups: [
43
+ {
44
+ name: 'default',
45
+ title: 'Default',
46
+ hidden: ({parent}) => {
47
+ return !Array.isArray(parent)
48
+ },
49
+ },
50
+ {
51
+ name: 'personalization',
52
+ title: 'Personalization options',
53
+ hidden: ({parent}) => {
54
+ return !Array.isArray(parent)
55
+ },
56
+ },
57
+ {
58
+ name: 'all-fields',
59
+ title: 'All fields',
60
+ hidden: ({parent}) => {
61
+ return Array.isArray(parent)
62
+ },
63
+ },
64
+ ],
65
+ components: {
66
+ field: (props) => (
67
+ <Field
68
+ {...props}
69
+ personalizationNameOverride={personalizationNameOverride}
70
+ segmentNameOverride={segmentNameOverride}
71
+ />
72
+ ),
73
+ item: ArrayItem,
74
+ },
75
+ fields: [
76
+ typeof field === `string`
77
+ ? // Define a simple field if all we have is the name as a string
78
+ defineField({
79
+ name: 'default',
80
+ type: field,
81
+ group: 'default',
82
+ })
83
+ : // Pass in the configured options, but overwrite the name
84
+ {
85
+ ...field,
86
+ name: 'default',
87
+ group: 'default',
88
+ },
89
+ defineField({
90
+ name: 'active',
91
+ type: 'boolean',
92
+ hidden: true,
93
+ initialValue: false,
94
+ }),
95
+ defineField({
96
+ name: segmentArrayName,
97
+ type: 'array',
98
+ hidden: ({parent}) => {
99
+ return !parent?.active
100
+ },
101
+ group: 'personalization',
102
+ components: {
103
+ input: (props: ArrayOfObjectsInputProps) => (
104
+ <ArrayInput {...props} segmentName={segmentName} segmentId={segmentId} />
105
+ ),
106
+ },
107
+ of: [
108
+ defineField({
109
+ name: segmentName,
110
+ type: segmentName,
111
+ }),
112
+ ],
113
+ }),
114
+ ],
115
+ preview: {
116
+ select: {
117
+ base: 'default',
118
+ },
119
+ prepare: ({base}) => {
120
+ const title = base?.title || base?.name || typeof base === 'string' ? base : ''
121
+ const media = base?.image || base?.photo || base?.media || ''
122
+ return {
123
+ title: title,
124
+ media,
125
+ }
126
+ },
127
+ },
128
+ })
129
+ }
130
+
131
+ const createSegmentType = ({
132
+ field,
133
+ segmentNameOverride,
134
+ segmentId,
135
+ }: {
136
+ field: string | FieldDefinition
137
+ segmentNameOverride: string
138
+ segmentId: string
139
+ }) => {
140
+ const typeName = typeof field === `string` ? field : field.name
141
+ const usedName = String(typeName[0]).toUpperCase() + String(typeName).slice(1)
142
+ return defineType({
143
+ name: `${segmentNameOverride}${usedName}`,
144
+ title: `${segmentNameOverride} array ${usedName}`,
145
+ type: 'object',
146
+ components: {
147
+ preview: SegmentPreview,
148
+ input: SegmentInput,
149
+ },
150
+ fields: [
151
+ {
152
+ type: 'string',
153
+ name: segmentId,
154
+ readOnly: true,
155
+ },
156
+ typeof field === `string`
157
+ ? // Define a simple field if all we have is the name as a string
158
+ defineField({
159
+ name: 'value',
160
+ type: field,
161
+ // hidden: ({parent}) => !parent?.[`${objectNameOverride}Id`],
162
+ })
163
+ : // Pass in the configured options, but overwrite the name
164
+ {
165
+ ...field,
166
+ name: 'value',
167
+ // hidden: ({parent}) => !parent?.[`${objectNameOverride}Id`],
168
+ },
169
+ ],
170
+ preview: {
171
+ select: {
172
+ segment: segmentId,
173
+ value: 'value',
174
+ },
175
+ },
176
+ })
177
+ }
178
+
179
+ const fieldSchema = ({
180
+ fields,
181
+ personalizationNameOverride,
182
+ segmentNameOverride,
183
+ segmentId,
184
+ segmentArrayName,
185
+ }: Required<Omit<PersonalizationFieldPluginConfig, 'apiVersion' | 'segments'>>) => {
186
+ return [
187
+ ...fields.map((field) => createSegmentType({field, segmentNameOverride, segmentId})),
188
+ ...fields.map((field) =>
189
+ createPersonalizationType({
190
+ field,
191
+ personalizationNameOverride,
192
+ segmentNameOverride,
193
+ segmentId,
194
+ segmentArrayName,
195
+ }),
196
+ ),
197
+ ]
198
+ }
199
+
200
+ export const fieldLevelPersonalization = definePlugin<PersonalizationFieldPluginConfig>(
201
+ (config) => {
202
+ const pluginConfig = {...CONFIG_DEFAULT, ...config}
203
+ const {fields, personalizationNameOverride, segmentNameOverride} = pluginConfig
204
+
205
+ const segmentArrayName = `${segmentNameOverride}s`
206
+ const segmentId = `${segmentNameOverride}Id`
207
+
208
+ const fieldSchemaConfig = fieldSchema({
209
+ fields,
210
+ personalizationNameOverride,
211
+ segmentNameOverride,
212
+ segmentId,
213
+ segmentArrayName,
214
+ })
215
+ return {
216
+ name: 'sanity-personalistaion-plugin-field-level-personalization',
217
+ schema: {
218
+ types: fieldSchemaConfig,
219
+ },
220
+ form: {
221
+ components: {
222
+ input: (props) => {
223
+ const isRootInput = props.id === 'root' && isObjectInputProps(props)
224
+
225
+ if (!isRootInput) {
226
+ return props.renderDefault(props)
227
+ }
228
+
229
+ const flatFields = flattenSchemaType(props.schemaType)
230
+ const hasPersonalization = flatFields.some(
231
+ (field) =>
232
+ field.type.name.startsWith(personalizationNameOverride) ||
233
+ field.name.startsWith(personalizationNameOverride),
234
+ )
235
+
236
+ if (!hasPersonalization) {
237
+ return props.renderDefault(props)
238
+ }
239
+
240
+ const providerProps = {
241
+ ...props,
242
+ personalizationFieldPluginConfig: {
243
+ ...pluginConfig,
244
+ segmentId,
245
+ segmentArrayName,
246
+ },
247
+ }
248
+ return PersonalizationProvider(providerProps)
249
+ },
250
+ },
251
+ },
252
+ }
253
+ },
254
+ )
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './fieldExperiments'
2
+ export * from './fieldPersonalization'
2
3
  export * from './types'
3
4
  export * from './utils/flattenSchemaType'
package/src/types.ts CHANGED
@@ -18,7 +18,7 @@ export type ExperimentType = {
18
18
  variants: VariantType[]
19
19
  }
20
20
 
21
- export type FieldPluginConfig = {
21
+ export type ExperimentFieldPluginConfig = {
22
22
  fields: (string | FieldDefinition)[]
23
23
  experiments: ExperimentType[] | ((client: SanityClient) => Promise<ExperimentType[]>)
24
24
  apiVersion?: string
@@ -29,21 +29,39 @@ export type FieldPluginConfig = {
29
29
  experimentId?: string
30
30
  }
31
31
 
32
+ export type PersonalizationFieldPluginConfig = {
33
+ fields: (string | FieldDefinition)[]
34
+ segments: VariantType[] | ((client: SanityClient) => Promise<VariantType[]>)
35
+ apiVersion?: string
36
+ personalizationNameOverride?: string
37
+ segmentNameOverride?: string
38
+ segmentId?: string
39
+ segmentArrayName?: string
40
+ }
41
+
32
42
  export type VariantPreviewProps = Omit<PreviewProps, 'SchemaType'> & {
33
43
  [key: string]: string
34
44
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
45
  value: any
36
46
  }
37
47
 
38
- export type ExperimentContextProps = Required<FieldPluginConfig> & {
48
+ export type ExperimentContextProps = Required<ExperimentFieldPluginConfig> & {
39
49
  experiments: ExperimentType[]
40
50
  }
41
51
 
52
+ export type PersonalizationContextProps = Required<PersonalizationFieldPluginConfig> & {
53
+ segments: VariantType[]
54
+ }
55
+
42
56
  export type ArrayInputProps = ArrayOfObjectsInputProps & {
43
57
  variantName: string
44
58
  variantId: string
45
59
  experimentId: string
46
60
  }
61
+ export type PersonalizationArrayInputProps = ArrayOfObjectsInputProps & {
62
+ segmentName: string
63
+ segmentId: string
64
+ }
47
65
 
48
66
  export type ObjectFieldWithPath = ObjectField<SchemaType> & {path: Path}
49
67
 
@@ -0,0 +1,33 @@
1
+ import {ObjectFieldProps} from 'sanity'
2
+
3
+ /**
4
+ * Safely updates deeply nested children props to clear groups array
5
+ * This prevents field grouping UI conflicts in personalization mode
6
+ */
7
+ export const clearChildrenGroups = (props: ObjectFieldProps): ObjectFieldProps => {
8
+ // Type assertion is needed here because Sanity's ObjectFieldProps children
9
+ // typing doesn't account for the nested structure we need to manipulate
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ const children = props.children as any
12
+
13
+ if (!children || typeof children !== 'object' || !children.props) {
14
+ return props
15
+ }
16
+
17
+ return {
18
+ ...props,
19
+ children: {
20
+ ...children,
21
+ props: {
22
+ ...children.props,
23
+ children: {
24
+ ...children.props.children,
25
+ props: {
26
+ ...children.props.children?.props,
27
+ groups: [],
28
+ },
29
+ },
30
+ },
31
+ },
32
+ }
33
+ }