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