@sanity/personalization-plugin 2.1.0-growthbook.1 → 2.2.0-growthbook.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/personalization-plugin",
3
- "version": "2.1.0-growthbook.1",
3
+ "version": "2.2.0-growthbook.1",
4
4
  "description": "Plugin to help with personalization, a/b testing when using Sanity",
5
5
  "keywords": [
6
6
  "sanity",
@@ -8,44 +8,43 @@ import {useExperimentContext} from './ExperimentContext'
8
8
 
9
9
  export const ArrayInput = (props: ArrayInputProps) => {
10
10
  const fieldPath = props.path.slice(0, -1)
11
- const experimentId = useFormValue([...fieldPath, 'experimentId'])
11
+ const {onItemAppend, variantName, variantId, experimentId} = props
12
+ const experimentValue = useFormValue([...fieldPath, experimentId])
12
13
 
13
14
  const {experiments} = useExperimentContext()
14
15
 
15
- const {onItemAppend, objectName} = props
16
-
17
16
  const handleClick = useCallback(
18
17
  async (variant: VariantType) => {
19
18
  const item = {
20
19
  _key: uuid(),
21
- variantId: variant.id,
22
- experimentId: experimentId,
23
- _type: objectName,
20
+ [variantId]: variant.id,
21
+ [experimentId]: experimentValue,
22
+ _type: variantName,
24
23
  }
25
24
 
26
25
  // Patch the document
27
26
  onItemAppend(item)
28
27
  },
29
- [experimentId, objectName, onItemAppend],
28
+ [variantId, experimentId, experimentValue, variantName, onItemAppend],
30
29
  )
31
30
 
32
31
  const filteredVariants =
33
32
  experiments.find((option) => {
34
- return option.id === experimentId
33
+ return option.id === experimentValue
35
34
  })?.variants || []
36
35
 
37
36
  type Value = {
38
- experimentId: string
39
37
  value?: unknown
38
+ [key: string]: string | unknown
40
39
  variantId: string
41
40
  _key: string
42
41
  _type: string
43
42
  }
44
43
 
45
44
  // there is probably some better was of getting the type of this?
46
- const values = props.value as Value[] | []
45
+ const values = (props.value as Value[]) || []
47
46
 
48
- const usedVariants = values?.map((variant) => variant.variantId)
47
+ const usedVariants = values?.map((variant) => variant[variantId])
49
48
 
50
49
  return (
51
50
  <Stack space={3}>
@@ -55,7 +54,7 @@ export const ArrayInput = (props: ArrayInputProps) => {
55
54
  {filteredVariants.map((variant) => {
56
55
  return (
57
56
  <Button
58
- key={`${experimentId}-${variant.id}`}
57
+ key={`${experimentValue}-${variant.id}`}
59
58
  text={`Add ${variant.label}`}
60
59
  mode="ghost"
61
60
  disabled={usedVariants?.includes(variant.id)}
@@ -11,6 +11,11 @@ import {ExperimentContextProps, FieldPluginConfig} from '../types'
11
11
  export const CONFIG_DEFAULT = {
12
12
  fields: [],
13
13
  apiVersion: '2024-11-07',
14
+ experimentNameOverride: 'experiment',
15
+ variantNameOverride: 'variant',
16
+ variantId: 'variantId',
17
+ variantArrayName: 'variants',
18
+ experimentId: 'experimentId',
14
19
  }
15
20
 
16
21
  export const ExperimentContext = createContext<ExperimentContextProps>({
@@ -14,71 +14,127 @@ import {
14
14
  type PatchStuff = {onChange: (patch: FormPatch | FormPatch[] | PatchEvent) => void; inputId: string}
15
15
 
16
16
  const useAddExperimentAction = (
17
- props: DocumentFieldActionProps & PatchStuff,
17
+ props: DocumentFieldActionProps &
18
+ PatchStuff & {experimentNameOverride: string; experimentId: string; active: boolean},
18
19
  ): DocumentFieldActionItem => {
19
- const patchActiveEvent = useMemo(() => {
20
- return set(true, ['active'])
21
- }, [])
22
- const handleAction = useCallback(() => {
23
- props.onChange([patchActiveEvent])
24
- }, [patchActiveEvent, props])
20
+ const {onChange, active, experimentNameOverride} = props
21
+
22
+ const handleAddAction = useCallback(() => {
23
+ onChange([set(!active, ['active'])])
24
+ }, [onChange, active])
25
25
 
26
26
  return {
27
- title: 'Add experiment',
27
+ title: `Add ${experimentNameOverride}`,
28
28
  type: 'action',
29
29
  icon: GiSoapExperiment,
30
- onAction: handleAction,
30
+ onAction: handleAddAction,
31
31
  renderAsButton: true,
32
32
  }
33
33
  }
34
34
 
35
35
  const useRemoveExperimentAction = (
36
- props: DocumentFieldActionProps & PatchStuff,
36
+ props: DocumentFieldActionProps &
37
+ PatchStuff & {
38
+ experimentNameOverride: string
39
+ experimentId: string
40
+ active: boolean
41
+ variantNameOverride: string
42
+ },
37
43
  ): DocumentFieldActionItem => {
38
- const patchActiveEvent = useMemo(() => {
39
- const activeId = ['active']
40
- return set(false, activeId)
41
- }, [])
44
+ const {onChange, active, experimentId, experimentNameOverride, variantNameOverride} = props
42
45
 
43
- const patchClearEvent = useMemo(() => {
44
- const experimentId = ['experimentId'] // `${props.inputId}.experimentId`
45
- const variants = ['variants'] //`${props.inputId}.variants`
46
- return [unset(experimentId), unset(variants)]
47
- }, [])
48
- const handleAction = useCallback(() => {
49
- props.onChange([patchActiveEvent, ...patchClearEvent])
50
- }, [patchActiveEvent, patchClearEvent, props])
46
+ const handleClearAction = useCallback(() => {
47
+ const activeId = ['active']
48
+ const experiment = [experimentId]
49
+ const variants = [`${variantNameOverride}s`]
50
+ onChange([set(!active, activeId), unset(experiment), unset(variants)])
51
+ }, [onChange, active, experimentId, variantNameOverride])
51
52
 
52
53
  return {
53
- title: 'Remove experiment',
54
+ title: `Remove ${experimentNameOverride}`,
54
55
  type: 'action',
55
56
  icon: CloseIcon,
56
- onAction: handleAction,
57
+ onAction: handleClearAction,
57
58
  renderAsButton: true,
58
59
  }
59
60
  }
60
61
 
61
- const newActions = ({onChange, inputId, active}: PatchStuff & {active?: boolean}) =>
62
- active
63
- ? defineDocumentFieldAction({
64
- name: 'Experiment',
65
- useAction: (props) => useRemoveExperimentAction({...props, onChange, inputId}),
66
- })
67
- : defineDocumentFieldAction({
68
- name: 'Experiment',
69
- useAction: (props) => useAddExperimentAction({...props, onChange, inputId}),
70
- })
62
+ const createActions = ({
63
+ onChange,
64
+ inputId,
65
+ active,
66
+ experimentNameOverride,
67
+ experimentId,
68
+ variantNameOverride,
69
+ }: PatchStuff & {
70
+ active?: boolean
71
+ experimentNameOverride: string
72
+ experimentId: string
73
+ variantNameOverride: string
74
+ }) => {
75
+ const removeAction = defineDocumentFieldAction({
76
+ name: `Remove ${experimentNameOverride}`,
77
+ useAction: (props) =>
78
+ useRemoveExperimentAction({
79
+ ...props,
80
+ active: true,
81
+ onChange,
82
+ inputId,
83
+ experimentNameOverride,
84
+ experimentId,
85
+ variantNameOverride,
86
+ }),
87
+ })
88
+ const addAction = defineDocumentFieldAction({
89
+ name: `Add ${experimentNameOverride}`,
90
+ useAction: (props) =>
91
+ useAddExperimentAction({
92
+ ...props,
93
+ active: false,
94
+ onChange,
95
+ inputId,
96
+ experimentNameOverride,
97
+ experimentId,
98
+ }),
99
+ })
100
+ return active ? removeAction : addAction
101
+ }
71
102
 
72
- export const ExperimentField = (props: ObjectFieldProps) => {
103
+ export const ExperimentField = (
104
+ props: ObjectFieldProps & {
105
+ experimentNameOverride: string
106
+ experimentId: string
107
+ variantNameOverride: string
108
+ },
109
+ ) => {
73
110
  const {onChange} = props.inputProps
74
- const {inputId} = props
111
+ const {inputId, experimentNameOverride, experimentId, variantNameOverride} = props
75
112
  const active = props.value?.active as boolean | undefined
76
113
 
77
- const oldActions = props.actions || []
114
+ const actionProps = useMemo(
115
+ () => ({
116
+ onChange,
117
+ inputId,
118
+ active,
119
+ experimentNameOverride,
120
+ experimentId,
121
+ variantNameOverride,
122
+ }),
123
+ [onChange, inputId, active, experimentNameOverride, experimentId, variantNameOverride],
124
+ )
125
+
126
+ const memoizedActions = useMemo(() => {
127
+ const oldActions = props.actions || []
128
+ return [createActions(actionProps), ...oldActions]
129
+ }, [actionProps, props.actions])
130
+
131
+ const withActionProps = useMemo(
132
+ () => ({
133
+ ...props,
134
+ actions: memoizedActions,
135
+ }),
136
+ [props, memoizedActions],
137
+ )
78
138
 
79
- const withActionProps = {
80
- ...props,
81
- actions: [newActions({onChange, inputId, active}), ...oldActions],
82
- }
83
139
  return props.renderDefault(withActionProps)
84
140
  }
@@ -20,13 +20,16 @@ const formatlistOptions = (experiments: ExperimentType[]): SelectOption[] =>
20
20
  value: experiment.id,
21
21
  }))
22
22
 
23
- export const ExperimentInput = (props: StringInputProps) => {
23
+ export const ExperimentInput = (props: StringInputProps & {variantNameOverride: string}) => {
24
24
  const {experiments} = useExperimentContext()
25
25
 
26
26
  const id = useFormValue(['_id']) as string
27
- const aditionalChangePath = useMemo(() => [...props.path.slice(0, -1), 'variants'], [props.path])
28
- const subValues = useFormValue(aditionalChangePath)
27
+ const aditionalChangePath = useMemo(
28
+ () => [...props.path.slice(0, -1), `${props.variantNameOverride}s`],
29
+ [props.variantNameOverride, props.path],
30
+ )
29
31
 
32
+ const subValues = useFormValue(aditionalChangePath)
30
33
  const {patch} = useDocumentOperation(id.replace('drafts.', ''), props.schemaType.name)
31
34
 
32
35
  const handleChange = useCallback(
@@ -1,71 +1,18 @@
1
- import {FormEvent, useCallback} from 'react'
2
- import {
3
- FormPatch,
4
- PatchEvent,
5
- set,
6
- StringInputProps,
7
- unset,
8
- useDocumentOperation,
9
- useFormValue,
10
- } from 'sanity'
11
-
12
- import {VariantType} from '../types'
13
- import {useExperimentContext} from './ExperimentContext'
14
- import {Select} from './Select'
15
-
16
- const formatlistOptions = (varants: VariantType[]) =>
17
- varants.map((variant) => ({
18
- title: variant.label,
19
- value: variant.id,
20
- }))
21
-
22
- export const VariantInput = (props: StringInputProps) => {
23
- const experimentPath = props.path.slice(0, -3)
24
-
25
- const experimentValue = useFormValue([...experimentPath, 'experimentValue'])
26
-
27
- const id = useFormValue(['_id']) as string
28
-
29
- const {patch} = useDocumentOperation(id.replace('drafts.', ''), props.schemaType.name)
30
-
31
- const {experiments} = useExperimentContext()
32
-
33
- const handleChange = useCallback(
34
- (
35
- event: FormEvent<Element>,
36
- onChange: (patchchange: FormPatch | FormPatch[] | PatchEvent) => void,
37
- ) => {
38
- const target = event.currentTarget as HTMLSelectElement
39
- const inputValue = target.value
40
- const variantExperimentId = props.id.replace('variantId', 'experimentId')
41
-
42
- if (inputValue) {
43
- onChange(set(inputValue))
44
- const patchEvent = {
45
- set: {[variantExperimentId]: experimentValue},
46
- }
47
- patch.execute([patchEvent])
48
- } else {
49
- onChange(unset())
50
- const patchEvent = {
51
- unset: [variantExperimentId],
52
- }
53
- patch.execute([patchEvent])
54
- }
55
- },
56
- [experimentValue, patch, props.id],
57
- )
58
-
59
- const filteredVariants =
60
- experiments.find((option) => {
61
- return option.id === experimentValue
62
- })?.variants || []
63
-
1
+ import {Button, Inline, Stack} from '@sanity/ui'
2
+ import {ObjectInputProps, set, useFormValue} from 'sanity'
3
+
4
+ export const VariantInput = (props: ObjectInputProps) => {
5
+ const defaultValue = useFormValue([props.path[0], 'default'])
6
+ const handleClick = () => {
7
+ props.onChange(set(defaultValue, ['value']))
8
+ }
64
9
  return (
65
- <Select
66
- {...props}
67
- listOptions={formatlistOptions(filteredVariants)}
68
- handleChange={handleChange}
69
- />
10
+ <Stack space={3}>
11
+ {props.renderDefault(props)}
12
+
13
+ <Inline space={1}>
14
+ <Button text="Copy default" mode="ghost" onClick={() => handleClick()} />
15
+ </Inline>
16
+ </Stack>
70
17
  )
71
18
  }
@@ -5,34 +5,48 @@ import {
5
5
  defineType,
6
6
  FieldDefinition,
7
7
  isObjectInputProps,
8
- SanityClient,
9
8
  } from 'sanity'
10
9
 
11
10
  import {ArrayInput} from './components/Array'
12
11
  import {CONFIG_DEFAULT, ExperimentProvider} from './components/ExperimentContext'
13
12
  import {ExperimentField} from './components/ExperimentField'
14
13
  import {ExperimentInput} from './components/ExperimentInput'
14
+ import {VariantInput} from './components/VariantInput'
15
15
  import {VariantPreview} from './components/VariantPreview'
16
- import {ExperimentType, FieldPluginConfig} from './types'
16
+ import {FieldPluginConfig} from './types'
17
17
  import {flattenSchemaType} from './utils/flattenSchemaType'
18
18
 
19
- const createFieldType = ({
19
+ const createExperimentType = ({
20
20
  field,
21
+ experimentNameOverride,
22
+ variantNameOverride,
23
+ variantId,
24
+ variantArrayName,
25
+ experimentId,
21
26
  }: {
22
27
  field: string | FieldDefinition
23
- experiments:
24
- | ExperimentType[]
25
- | ((client: SanityClient, secret?: string) => Promise<ExperimentType[]>)
28
+ experimentNameOverride: string
29
+ variantNameOverride: string
30
+ variantId: string
31
+ variantArrayName: string
32
+ experimentId: string
26
33
  }) => {
27
34
  const typeName = typeof field === `string` ? field : field.name
28
35
  const usedName = String(typeName[0]).toUpperCase() + String(typeName).slice(1)
29
- const objectName = `variant${usedName}`
36
+ const variantName = `${variantNameOverride}${usedName}`
30
37
 
31
38
  return defineType({
32
- name: `experiment${usedName}`,
39
+ name: `${experimentNameOverride}${usedName}`,
33
40
  type: 'object',
34
41
  components: {
35
- field: ExperimentField,
42
+ field: (props) => (
43
+ <ExperimentField
44
+ {...props}
45
+ experimentId={experimentId}
46
+ experimentNameOverride={experimentNameOverride}
47
+ variantNameOverride={variantNameOverride}
48
+ />
49
+ ),
36
50
  },
37
51
  fields: [
38
52
  typeof field === `string`
@@ -52,31 +66,37 @@ const createFieldType = ({
52
66
  hidden: true,
53
67
  }),
54
68
  defineField({
55
- title: 'Experiment',
56
- name: 'experimentId',
69
+ name: experimentId,
57
70
  type: 'string',
58
71
  components: {
59
- input: ExperimentInput,
72
+ input: (props) => (
73
+ <ExperimentInput {...props} variantNameOverride={variantNameOverride} />
74
+ ),
60
75
  },
61
76
  hidden: ({parent}) => {
62
77
  return !parent?.active
63
78
  },
64
79
  }),
65
80
  defineField({
66
- name: 'variants',
81
+ name: variantArrayName,
67
82
  type: 'array',
68
83
  hidden: ({parent}) => {
69
- return !parent?.experimentId
84
+ return !parent?.[experimentId]
70
85
  },
71
86
  components: {
72
87
  input: (props: ArrayOfObjectsInputProps) => (
73
- <ArrayInput {...props} objectName={objectName} />
88
+ <ArrayInput
89
+ {...props}
90
+ variantName={variantName}
91
+ variantId={variantId}
92
+ experimentId={experimentId}
93
+ />
74
94
  ),
75
95
  },
76
96
  of: [
77
97
  defineField({
78
- name: objectName,
79
- type: objectName,
98
+ name: variantName,
99
+ type: variantName,
80
100
  }),
81
101
  ],
82
102
  }),
@@ -84,30 +104,36 @@ const createFieldType = ({
84
104
  })
85
105
  }
86
106
 
87
- const createFieldObjectType = ({
107
+ const createVariantType = ({
88
108
  field,
109
+ variantNameOverride,
110
+ variantId,
111
+ experimentId,
89
112
  }: {
90
113
  field: string | FieldDefinition
91
- experiments: ExperimentType[] | ((client: SanityClient) => Promise<ExperimentType[]>)
114
+ variantNameOverride: string
115
+ variantId: string
116
+ experimentId: string
92
117
  }) => {
93
118
  const typeName = typeof field === `string` ? field : field.name
94
119
  const usedName = String(typeName[0]).toUpperCase() + String(typeName).slice(1)
95
120
  return defineType({
96
- name: `variant${usedName}`,
97
- title: `Experiment array ${usedName}`,
121
+ name: `${variantNameOverride}${usedName}`,
122
+ title: `${variantNameOverride} array ${usedName}`,
98
123
  type: 'object',
99
124
  components: {
100
125
  preview: VariantPreview,
126
+ input: VariantInput,
101
127
  },
102
128
  fields: [
103
129
  {
104
130
  type: 'string',
105
- name: 'variantId',
131
+ name: variantId,
106
132
  readOnly: true,
107
133
  },
108
134
  {
109
135
  type: 'string',
110
- name: 'experimentId',
136
+ name: experimentId,
111
137
  hidden: true,
112
138
  },
113
139
  typeof field === `string`
@@ -115,36 +141,66 @@ const createFieldObjectType = ({
115
141
  defineField({
116
142
  name: 'value',
117
143
  type: field,
118
- hidden: ({parent}) => !parent?.variantId,
144
+ // hidden: ({parent}) => !parent?.[`${objectNameOverride}Id`],
119
145
  })
120
146
  : // Pass in the configured options, but overwrite the name
121
147
  {
122
148
  ...field,
123
149
  name: 'value',
124
- hidden: ({parent}) => !parent?.variantId,
150
+ // hidden: ({parent}) => !parent?.[`${objectNameOverride}Id`],
125
151
  },
126
152
  ],
127
153
  preview: {
128
154
  select: {
129
- variant: 'variantId',
130
- experiment: 'experimentId',
155
+ variant: variantId,
156
+ experiment: experimentId,
131
157
  value: 'value',
132
158
  },
133
159
  },
134
160
  })
135
161
  }
136
162
 
137
- const fieldSchema = ({fields, experiments}: FieldPluginConfig) => {
163
+ const fieldSchema = ({
164
+ fields,
165
+ experimentNameOverride,
166
+ variantNameOverride,
167
+ variantId,
168
+ variantArrayName,
169
+ experimentId,
170
+ }: Required<Omit<FieldPluginConfig, 'apiVersion' | 'experiments'>>) => {
138
171
  return [
139
- ...fields.map((field) => createFieldObjectType({field, experiments})),
140
- ...fields.map((field) => createFieldType({field, experiments})),
172
+ ...fields.map((field) =>
173
+ createVariantType({field, variantNameOverride, variantId, experimentId}),
174
+ ),
175
+ ...fields.map((field) =>
176
+ createExperimentType({
177
+ field,
178
+ experimentNameOverride,
179
+ variantNameOverride,
180
+ variantId,
181
+ variantArrayName,
182
+ experimentId,
183
+ }),
184
+ ),
141
185
  ]
142
186
  }
143
187
 
144
188
  export const fieldLevelExperiments = definePlugin<FieldPluginConfig>((config) => {
145
189
  const pluginConfig = {...CONFIG_DEFAULT, ...config}
146
- const {fields, experiments} = pluginConfig
147
- const fieldSchemaConfig = fieldSchema({fields, experiments})
190
+ const {fields, experimentNameOverride, variantNameOverride} = pluginConfig
191
+
192
+ const experimentId = `${experimentNameOverride}Id`
193
+ const variantArrayName = `${variantNameOverride}s`
194
+ const variantId = `${variantNameOverride}Id`
195
+
196
+ const fieldSchemaConfig = fieldSchema({
197
+ fields,
198
+ experimentNameOverride,
199
+ variantNameOverride,
200
+ variantId,
201
+ variantArrayName,
202
+ experimentId,
203
+ })
148
204
  return {
149
205
  name: 'sanity-personalistaion-plugin-field-level-experiments',
150
206
  schema: {
@@ -162,12 +218,22 @@ export const fieldLevelExperiments = definePlugin<FieldPluginConfig>((config) =>
162
218
  const flatFieldTypeNames = flattenSchemaType(props.schemaType).map(
163
219
  (field) => field.type.name,
164
220
  )
165
- const hasExperiment = flatFieldTypeNames.some((name) => name.startsWith('experiment'))
221
+ const hasExperiment = flatFieldTypeNames.some((name) =>
222
+ name.startsWith(experimentNameOverride),
223
+ )
166
224
 
167
225
  if (!hasExperiment) {
168
226
  return props.renderDefault(props)
169
227
  }
170
- const providerProps = {...props, experimentFieldPluginConfig: pluginConfig}
228
+ const providerProps = {
229
+ ...props,
230
+ experimentFieldPluginConfig: {
231
+ ...pluginConfig,
232
+ variantId,
233
+ variantArrayName,
234
+ experimentId,
235
+ },
236
+ }
171
237
  return ExperimentProvider(providerProps)
172
238
  },
173
239
  },
package/src/types.ts CHANGED
@@ -25,11 +25,15 @@ export type FieldPluginConfig = {
25
25
  | ExperimentType[]
26
26
  | ((client: SanityClient, secret?: string) => Promise<ExperimentType[]>)
27
27
  apiVersion?: string
28
+ experimentNameOverride?: string
29
+ variantNameOverride?: string
30
+ variantId?: string
31
+ variantArrayName?: string
32
+ experimentId?: string
28
33
  }
29
34
 
30
35
  export type VariantPreviewProps = Omit<PreviewProps, 'SchemaType'> & {
31
- experiment: string
32
- variant: string
36
+ [key: string]: string
33
37
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
34
38
  value: any
35
39
  }
@@ -41,15 +45,16 @@ export type ExperimentContextProps = Required<FieldPluginConfig> & {
41
45
  }
42
46
 
43
47
  export type ArrayInputProps = ArrayOfObjectsInputProps & {
44
- objectName: string
48
+ variantName: string
49
+ variantId: string
50
+ experimentId: string
45
51
  }
46
52
 
47
53
  export type ObjectFieldWithPath = ObjectField<SchemaType> & {path: Path}
48
54
 
49
55
  export type VariantGeneric<T> = {
56
+ [key: string]: string | T | undefined
50
57
  _type: string
51
- variantId?: string
52
- experimentId?: string
53
58
  value?: T
54
59
  }
55
60
 
@@ -57,11 +62,15 @@ export type ExperimentGeneric<T> = {
57
62
  _type: string
58
63
  default?: T
59
64
  experimentValue?: string
60
- variants?: Array<
61
- {
62
- _key: string
63
- } & VariantGeneric<T>
64
- >
65
+ [key: string]:
66
+ | Array<
67
+ {
68
+ _key: string
69
+ } & VariantGeneric<T>
70
+ >
71
+ | string
72
+ | T
73
+ | undefined
65
74
  }
66
75
 
67
76
  export type GrowthbookExperiment = {