@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.
- package/README.md +107 -5
- 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 +158 -277
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +160 -277
- package/dist/index.mjs.map +1 -1
- package/package.json +20 -20
- package/src/components/ArrayItem.tsx +9 -0
- 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/{VariantInput.tsx → experiment/VariantInput.tsx} +2 -1
- 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 +44 -12
- 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/fieldExperiments.tsx
CHANGED
|
@@ -7,13 +7,17 @@ import {
|
|
|
7
7
|
isObjectInputProps,
|
|
8
8
|
} from 'sanity'
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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<
|
|
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<
|
|
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
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
|
+
}
|