@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.
- package/dist/_chunks-cjs/fieldExperiments.js +507 -0
- package/dist/_chunks-cjs/fieldExperiments.js.map +1 -0
- package/dist/_chunks-es/fieldExperiments.mjs +511 -0
- package/dist/_chunks-es/fieldExperiments.mjs.map +1 -0
- package/dist/growthbook/index.js +3 -3
- package/dist/growthbook/index.js.map +1 -1
- package/dist/growthbook/index.mjs +1 -1
- package/dist/index.d.mts +33 -12
- package/dist/index.d.ts +33 -12
- package/dist/index.js +157 -280
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +159 -280
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/{ExperimentItem.tsx → ArrayItem.tsx} +1 -2
- package/src/components/Select.tsx +1 -1
- package/src/components/{Array.tsx → experiment/Array.tsx} +2 -2
- package/src/components/{ExperimentContext.tsx → experiment/Context.tsx} +2 -2
- package/src/components/{ExperimentField.tsx → experiment/Field.tsx} +11 -8
- package/src/components/{ExperimentInput.tsx → experiment/Input.tsx} +4 -4
- package/src/components/{VariantPreview.tsx → experiment/VariantPreview.tsx} +2 -2
- package/src/components/experiment/index.ts +6 -0
- package/src/components/personalization/Array.tsx +59 -0
- package/src/components/personalization/Context.tsx +61 -0
- package/src/components/personalization/Field.tsx +134 -0
- package/src/components/personalization/SegmentInput.tsx +19 -0
- package/src/components/personalization/SegmentPreview.tsx +71 -0
- package/src/components/personalization/index.ts +5 -0
- package/src/fieldExperiments.tsx +43 -13
- package/src/fieldPersonalization.tsx +254 -0
- package/src/index.ts +1 -0
- package/src/types.ts +20 -2
- package/src/utils/clearChildGroups.ts +33 -0
- /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
package/src/types.ts
CHANGED
|
@@ -18,7 +18,7 @@ export type ExperimentType = {
|
|
|
18
18
|
variants: VariantType[]
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export type
|
|
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<
|
|
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
|
+
}
|
|
File without changes
|