@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,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
+ }