@sanity/personalization-plugin 2.1.0-growthbook.1 → 2.1.0

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.1.0",
4
4
  "description": "Plugin to help with personalization, a/b testing when using Sanity",
5
5
  "keywords": [
6
6
  "sanity",
@@ -45,7 +45,6 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@sanity/incompatible-plugin": "^1.0.4",
48
- "@sanity/studio-secrets": "^3.0.0",
49
48
  "@sanity/ui": "^2.8.19",
50
49
  "@sanity/uuid": "^3.0.2",
51
50
  "fast-deep-equal": "^3.1.3",
@@ -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)}
@@ -1,5 +1,5 @@
1
1
  import equal from 'fast-deep-equal'
2
- import {createContext, useContext, useMemo, useState} from 'react'
2
+ import {createContext, useContext, useMemo} from 'react'
3
3
  import {type ObjectInputProps, useClient, useWorkspace} from 'sanity'
4
4
  import {suspend} from 'suspend-react'
5
5
 
@@ -11,13 +11,16 @@ 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>({
17
22
  ...CONFIG_DEFAULT,
18
23
  experiments: [],
19
- setSecret: () => undefined,
20
- secret: undefined,
21
24
  })
22
25
 
23
26
  export function useExperimentContext() {
@@ -30,7 +33,6 @@ type ExperimentProps = ObjectInputProps & {
30
33
 
31
34
  export function ExperimentProvider(props: ExperimentProps) {
32
35
  const {experimentFieldPluginConfig} = props
33
- const [secret, setSecret] = useState<string | undefined>()
34
36
 
35
37
  const client = useClient({apiVersion: experimentFieldPluginConfig.apiVersion})
36
38
  const workspace = useWorkspace()
@@ -42,17 +44,17 @@ export function ExperimentProvider(props: ExperimentProps) {
42
44
  // eslint-disable-next-line require-await
43
45
  async () => {
44
46
  if (typeof experimentFieldPluginConfig.experiments === 'function') {
45
- return experimentFieldPluginConfig.experiments(client, secret)
47
+ return experimentFieldPluginConfig.experiments(client)
46
48
  }
47
49
  return experimentFieldPluginConfig.experiments
48
50
  },
49
- [workspace, secret],
51
+ [workspace],
50
52
  {equal},
51
53
  )
52
54
 
53
55
  const context = useMemo(
54
- () => ({...experimentFieldPluginConfig, experiments, secret, setSecret}),
55
- [experimentFieldPluginConfig, experiments, secret, setSecret],
56
+ () => ({...experimentFieldPluginConfig, experiments}),
57
+ [experimentFieldPluginConfig, experiments],
56
58
  )
57
59
 
58
60
  return (
@@ -1,5 +1,4 @@
1
1
  import {CloseIcon} from '@sanity/icons'
2
- import {useCallback, useMemo} from 'react'
3
2
  import {GiSoapExperiment} from 'react-icons/gi'
4
3
  import {
5
4
  defineDocumentFieldAction,
@@ -14,71 +13,104 @@ import {
14
13
  type PatchStuff = {onChange: (patch: FormPatch | FormPatch[] | PatchEvent) => void; inputId: string}
15
14
 
16
15
  const useAddExperimentAction = (
17
- props: DocumentFieldActionProps & PatchStuff,
16
+ props: DocumentFieldActionProps &
17
+ PatchStuff & {experimentNameOverride: string; experimentId: string; active: boolean},
18
18
  ): DocumentFieldActionItem => {
19
- const patchActiveEvent = useMemo(() => {
20
- return set(true, ['active'])
21
- }, [])
22
- const handleAction = useCallback(() => {
23
- props.onChange([patchActiveEvent])
24
- }, [patchActiveEvent, props])
19
+ const {onChange, active, experimentNameOverride} = props
20
+
21
+ const handleAddAction = () => {
22
+ onChange([set(!active, ['active'])])
23
+ }
25
24
 
26
25
  return {
27
- title: 'Add experiment',
26
+ title: `Add ${experimentNameOverride}`,
28
27
  type: 'action',
29
28
  icon: GiSoapExperiment,
30
- onAction: handleAction,
29
+ onAction: handleAddAction,
31
30
  renderAsButton: true,
32
31
  }
33
32
  }
34
33
 
35
34
  const useRemoveExperimentAction = (
36
- props: DocumentFieldActionProps & PatchStuff,
35
+ props: DocumentFieldActionProps &
36
+ PatchStuff & {experimentNameOverride: string; experimentId: string; active: boolean},
37
37
  ): DocumentFieldActionItem => {
38
- const patchActiveEvent = useMemo(() => {
38
+ const {onChange, active, experimentId, experimentNameOverride} = props
39
+ const patchActiveFalseEvent = () => {
39
40
  const activeId = ['active']
40
- return set(false, activeId)
41
- }, [])
42
-
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])
51
-
41
+ return set(!active, activeId)
42
+ }
43
+ const patchClearEvent = () => {
44
+ const experiment = [experimentId]
45
+ const variants = [experimentNameOverride]
46
+ return [unset(experiment), unset(variants)]
47
+ }
48
+ const handleClearAction = () => {
49
+ const clearEvents = patchClearEvent()
50
+ const activeEvent = patchActiveFalseEvent()
51
+ onChange([activeEvent, ...clearEvents])
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 newActions = ({
63
+ onChange,
64
+ inputId,
65
+ active,
66
+ experimentNameOverride,
67
+ experimentId,
68
+ }: PatchStuff & {active?: boolean; experimentNameOverride: string; experimentId: string}) => {
69
+ const removeAction = defineDocumentFieldAction({
70
+ name: `Remove ${experimentNameOverride}`,
71
+ useAction: (props) =>
72
+ useRemoveExperimentAction({
73
+ ...props,
74
+ active: true,
75
+ onChange,
76
+ inputId,
77
+ experimentNameOverride,
78
+ experimentId,
79
+ }),
80
+ })
81
+ const addAction = defineDocumentFieldAction({
82
+ name: `Add ${experimentNameOverride}`,
83
+ useAction: (props) =>
84
+ useAddExperimentAction({
85
+ ...props,
86
+ active: false,
87
+ onChange,
88
+ inputId,
89
+ experimentNameOverride,
90
+ experimentId,
91
+ }),
92
+ })
93
+ if (active) {
94
+ return removeAction
95
+ }
96
+ return addAction
97
+ }
71
98
 
72
- export const ExperimentField = (props: ObjectFieldProps) => {
99
+ export const ExperimentField = (
100
+ props: ObjectFieldProps & {experimentNameOverride: string; experimentId: string},
101
+ ) => {
73
102
  const {onChange} = props.inputProps
74
- const {inputId} = props
103
+ const {inputId, experimentNameOverride, experimentId} = props
75
104
  const active = props.value?.active as boolean | undefined
76
105
 
77
106
  const oldActions = props.actions || []
78
107
 
79
108
  const withActionProps = {
80
109
  ...props,
81
- actions: [newActions({onChange, inputId, active}), ...oldActions],
110
+ actions: [
111
+ newActions({onChange, inputId, active, experimentNameOverride, experimentId}),
112
+ ...oldActions,
113
+ ],
82
114
  }
83
115
  return props.renderDefault(withActionProps)
84
116
  }
@@ -20,11 +20,14 @@ 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])
27
+ const aditionalChangePath = useMemo(
28
+ () => [...props.path.slice(0, -1), props.variantNameOverride],
29
+ [props.variantNameOverride, props.path],
30
+ )
28
31
  const subValues = useFormValue(aditionalChangePath)
29
32
 
30
33
  const {patch} = useDocumentOperation(id.replace('drafts.', ''), props.schemaType.name)
@@ -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,47 @@ 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
+ />
48
+ ),
36
49
  },
37
50
  fields: [
38
51
  typeof field === `string`
@@ -52,31 +65,37 @@ const createFieldType = ({
52
65
  hidden: true,
53
66
  }),
54
67
  defineField({
55
- title: 'Experiment',
56
- name: 'experimentId',
68
+ name: experimentId,
57
69
  type: 'string',
58
70
  components: {
59
- input: ExperimentInput,
71
+ input: (props) => (
72
+ <ExperimentInput {...props} variantNameOverride={variantNameOverride} />
73
+ ),
60
74
  },
61
75
  hidden: ({parent}) => {
62
76
  return !parent?.active
63
77
  },
64
78
  }),
65
79
  defineField({
66
- name: 'variants',
80
+ name: variantArrayName,
67
81
  type: 'array',
68
82
  hidden: ({parent}) => {
69
- return !parent?.experimentId
83
+ return !parent?.[experimentId]
70
84
  },
71
85
  components: {
72
86
  input: (props: ArrayOfObjectsInputProps) => (
73
- <ArrayInput {...props} objectName={objectName} />
87
+ <ArrayInput
88
+ {...props}
89
+ variantName={variantName}
90
+ variantId={variantId}
91
+ experimentId={experimentId}
92
+ />
74
93
  ),
75
94
  },
76
95
  of: [
77
96
  defineField({
78
- name: objectName,
79
- type: objectName,
97
+ name: variantName,
98
+ type: variantName,
80
99
  }),
81
100
  ],
82
101
  }),
@@ -84,30 +103,36 @@ const createFieldType = ({
84
103
  })
85
104
  }
86
105
 
87
- const createFieldObjectType = ({
106
+ const createVariantType = ({
88
107
  field,
108
+ variantNameOverride,
109
+ variantId,
110
+ experimentId,
89
111
  }: {
90
112
  field: string | FieldDefinition
91
- experiments: ExperimentType[] | ((client: SanityClient) => Promise<ExperimentType[]>)
113
+ variantNameOverride: string
114
+ variantId: string
115
+ experimentId: string
92
116
  }) => {
93
117
  const typeName = typeof field === `string` ? field : field.name
94
118
  const usedName = String(typeName[0]).toUpperCase() + String(typeName).slice(1)
95
119
  return defineType({
96
- name: `variant${usedName}`,
97
- title: `Experiment array ${usedName}`,
120
+ name: `${variantNameOverride}${usedName}`,
121
+ title: `${variantNameOverride} array ${usedName}`,
98
122
  type: 'object',
99
123
  components: {
100
124
  preview: VariantPreview,
125
+ input: VariantInput,
101
126
  },
102
127
  fields: [
103
128
  {
104
129
  type: 'string',
105
- name: 'variantId',
130
+ name: variantId,
106
131
  readOnly: true,
107
132
  },
108
133
  {
109
134
  type: 'string',
110
- name: 'experimentId',
135
+ name: experimentId,
111
136
  hidden: true,
112
137
  },
113
138
  typeof field === `string`
@@ -115,36 +140,66 @@ const createFieldObjectType = ({
115
140
  defineField({
116
141
  name: 'value',
117
142
  type: field,
118
- hidden: ({parent}) => !parent?.variantId,
143
+ // hidden: ({parent}) => !parent?.[`${objectNameOverride}Id`],
119
144
  })
120
145
  : // Pass in the configured options, but overwrite the name
121
146
  {
122
147
  ...field,
123
148
  name: 'value',
124
- hidden: ({parent}) => !parent?.variantId,
149
+ // hidden: ({parent}) => !parent?.[`${objectNameOverride}Id`],
125
150
  },
126
151
  ],
127
152
  preview: {
128
153
  select: {
129
- variant: 'variantId',
130
- experiment: 'experimentId',
154
+ variant: variantId,
155
+ experiment: experimentId,
131
156
  value: 'value',
132
157
  },
133
158
  },
134
159
  })
135
160
  }
136
161
 
137
- const fieldSchema = ({fields, experiments}: FieldPluginConfig) => {
162
+ const fieldSchema = ({
163
+ fields,
164
+ experimentNameOverride,
165
+ variantNameOverride,
166
+ variantId,
167
+ variantArrayName,
168
+ experimentId,
169
+ }: Required<Omit<FieldPluginConfig, 'apiVersion' | 'experiments'>>) => {
138
170
  return [
139
- ...fields.map((field) => createFieldObjectType({field, experiments})),
140
- ...fields.map((field) => createFieldType({field, experiments})),
171
+ ...fields.map((field) =>
172
+ createVariantType({field, variantNameOverride, variantId, experimentId}),
173
+ ),
174
+ ...fields.map((field) =>
175
+ createExperimentType({
176
+ field,
177
+ experimentNameOverride,
178
+ variantNameOverride,
179
+ variantId,
180
+ variantArrayName,
181
+ experimentId,
182
+ }),
183
+ ),
141
184
  ]
142
185
  }
143
186
 
144
187
  export const fieldLevelExperiments = definePlugin<FieldPluginConfig>((config) => {
145
188
  const pluginConfig = {...CONFIG_DEFAULT, ...config}
146
- const {fields, experiments} = pluginConfig
147
- const fieldSchemaConfig = fieldSchema({fields, experiments})
189
+ const {fields, experimentNameOverride, variantNameOverride} = pluginConfig
190
+
191
+ const experimentId = `${experimentNameOverride}Id`
192
+ const variantArrayName = `${variantNameOverride}s`
193
+ const variantId = `${variantNameOverride}Id`
194
+
195
+ const fieldSchemaConfig = fieldSchema({
196
+ fields,
197
+ experimentNameOverride,
198
+ variantNameOverride,
199
+ variantId,
200
+ variantArrayName,
201
+ experimentId,
202
+ })
148
203
  return {
149
204
  name: 'sanity-personalistaion-plugin-field-level-experiments',
150
205
  schema: {
@@ -162,12 +217,22 @@ export const fieldLevelExperiments = definePlugin<FieldPluginConfig>((config) =>
162
217
  const flatFieldTypeNames = flattenSchemaType(props.schemaType).map(
163
218
  (field) => field.type.name,
164
219
  )
165
- const hasExperiment = flatFieldTypeNames.some((name) => name.startsWith('experiment'))
220
+ const hasExperiment = flatFieldTypeNames.some((name) =>
221
+ name.startsWith(experimentNameOverride),
222
+ )
166
223
 
167
224
  if (!hasExperiment) {
168
225
  return props.renderDefault(props)
169
226
  }
170
- const providerProps = {...props, experimentFieldPluginConfig: pluginConfig}
227
+ const providerProps = {
228
+ ...props,
229
+ experimentFieldPluginConfig: {
230
+ ...pluginConfig,
231
+ variantId,
232
+ variantArrayName,
233
+ experimentId,
234
+ },
235
+ }
171
236
  return ExperimentProvider(providerProps)
172
237
  },
173
238
  },
package/src/index.ts CHANGED
@@ -1,4 +1,3 @@
1
1
  export * from './fieldExperiments'
2
- export * from './growthbookFieldExperiments'
3
2
  export * from './types'
4
3
  export * from './utils/flattenSchemaType'