@sanity/personalization-plugin 2.0.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/LICENSE +21 -0
- package/README.md +287 -0
- package/dist/index.d.mts +70 -0
- package/dist/index.d.ts +70 -0
- package/dist/index.js +6654 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +6657 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +93 -0
- package/sanity.json +8 -0
- package/src/components/Array.tsx +69 -0
- package/src/components/ExperimentContext.tsx +60 -0
- package/src/components/ExperimentField.tsx +84 -0
- package/src/components/ExperimentInput.tsx +61 -0
- package/src/components/Select.tsx +43 -0
- package/src/components/VariantInput.tsx +71 -0
- package/src/components/VariantPreview.tsx +75 -0
- package/src/fieldExperiments.tsx +174 -0
- package/src/index.ts +3 -0
- package/src/types.ts +60 -0
- package/src/utils/flattenSchemaType.ts +50 -0
- package/v2-incompatible.js +11 -0
package/package.json
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sanity/personalization-plugin",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Plugin to help with personalization, a/b testing when using Sanity",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"sanity",
|
|
7
|
+
"sanity-plugin"
|
|
8
|
+
],
|
|
9
|
+
"homepage": "https://github.com/sanity-io/sanity-plugin-personalization#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/sanity-io/sanity-plugin-personalization/issues"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git@github.com:sanity-io/sanity-plugin-personalization.git"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Sanity <hello@sanity.io>",
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"type": "commonjs",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"source": "./src/index.ts",
|
|
24
|
+
"import": "./dist/index.mjs",
|
|
25
|
+
"default": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./package.json": "./package.json"
|
|
28
|
+
},
|
|
29
|
+
"main": "./dist/index.js",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"sanity.json",
|
|
34
|
+
"src",
|
|
35
|
+
"v2-incompatible.js"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "plugin-kit verify-package --silent && pkg-utils build --strict --check --clean",
|
|
39
|
+
"format": "prettier --write --cache --ignore-unknown .",
|
|
40
|
+
"link-watch": "plugin-kit link-watch",
|
|
41
|
+
"lint": "eslint . --fix",
|
|
42
|
+
"prepublishOnly": "npm run build",
|
|
43
|
+
"watch": "pkg-utils watch --strict",
|
|
44
|
+
"prepare": "husky"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@sanity/incompatible-plugin": "^1.0.4",
|
|
48
|
+
"@sanity/ui": "^2.8.19",
|
|
49
|
+
"@sanity/uuid": "^3.0.2",
|
|
50
|
+
"fast-deep-equal": "^3.1.3",
|
|
51
|
+
"react-icons": "^5.4.0",
|
|
52
|
+
"suspend-react": "^0.1.3"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@commitlint/cli": "^19.7.1",
|
|
56
|
+
"@commitlint/config-conventional": "^19.7.1",
|
|
57
|
+
"@sanity/pkg-utils": "^6.13.4",
|
|
58
|
+
"@sanity/plugin-kit": "^4.0.19",
|
|
59
|
+
"@sanity/semantic-release-preset": "^5.0.0",
|
|
60
|
+
"@types/react": "^18.3.18",
|
|
61
|
+
"@typescript-eslint/eslint-plugin": "^8.23.0",
|
|
62
|
+
"@typescript-eslint/parser": "^8.23.0",
|
|
63
|
+
"eslint": "^8.57.1",
|
|
64
|
+
"eslint-config-prettier": "^9.1.0",
|
|
65
|
+
"eslint-config-sanity": "^7.1.4",
|
|
66
|
+
"eslint-plugin-prettier": "^5.2.3",
|
|
67
|
+
"eslint-plugin-react": "^7.37.4",
|
|
68
|
+
"eslint-plugin-react-hooks": "^5.1.0",
|
|
69
|
+
"husky": "^9.1.7",
|
|
70
|
+
"lint-staged": "^15.2.10",
|
|
71
|
+
"prettier": "^3.4.2",
|
|
72
|
+
"prettier-plugin-packagejson": "^2.5.8",
|
|
73
|
+
"react": "^18.3.1",
|
|
74
|
+
"react-dom": "^18.3.1",
|
|
75
|
+
"sanity": "^3.74.1",
|
|
76
|
+
"semantic-release": "^24.2.1",
|
|
77
|
+
"styled-components": "^6.1.15",
|
|
78
|
+
"typescript": "^5.7.3"
|
|
79
|
+
},
|
|
80
|
+
"peerDependencies": {
|
|
81
|
+
"react": "^18",
|
|
82
|
+
"sanity": "^3"
|
|
83
|
+
},
|
|
84
|
+
"engines": {
|
|
85
|
+
"node": ">=18"
|
|
86
|
+
},
|
|
87
|
+
"resolutions": {
|
|
88
|
+
"conventional-changelog-conventionalcommits": ">= 8.0.0"
|
|
89
|
+
},
|
|
90
|
+
"publishConfig": {
|
|
91
|
+
"access": "public"
|
|
92
|
+
}
|
|
93
|
+
}
|
package/sanity.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {Button, Inline, Stack} from '@sanity/ui'
|
|
2
|
+
import {uuid} from '@sanity/uuid'
|
|
3
|
+
import {useCallback} from 'react'
|
|
4
|
+
import {useFormValue} from 'sanity'
|
|
5
|
+
|
|
6
|
+
import {ArrayInputProps, VariantType} from '../types'
|
|
7
|
+
import {useExperimentContext} from './ExperimentContext'
|
|
8
|
+
|
|
9
|
+
export const ArrayInput = (props: ArrayInputProps) => {
|
|
10
|
+
const fieldPath = props.path.slice(0, -1)
|
|
11
|
+
const experimentId = useFormValue([...fieldPath, 'experimentId'])
|
|
12
|
+
|
|
13
|
+
const {experiments} = useExperimentContext()
|
|
14
|
+
|
|
15
|
+
const {onItemAppend, objectName} = props
|
|
16
|
+
|
|
17
|
+
const handleClick = useCallback(
|
|
18
|
+
async (variant: VariantType) => {
|
|
19
|
+
const item = {
|
|
20
|
+
_key: uuid(),
|
|
21
|
+
variantId: variant.id,
|
|
22
|
+
experimentId: experimentId,
|
|
23
|
+
_type: objectName,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Patch the document
|
|
27
|
+
onItemAppend(item)
|
|
28
|
+
},
|
|
29
|
+
[experimentId, objectName, onItemAppend],
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
const filteredVariants =
|
|
33
|
+
experiments.find((option) => {
|
|
34
|
+
return option.id === experimentId
|
|
35
|
+
})?.variants || []
|
|
36
|
+
|
|
37
|
+
type Value = {
|
|
38
|
+
experimentId: string
|
|
39
|
+
value?: unknown
|
|
40
|
+
variantId: string
|
|
41
|
+
_key: string
|
|
42
|
+
_type: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// there is probably some better was of getting the type of this?
|
|
46
|
+
const values = props.value as Value[] | []
|
|
47
|
+
|
|
48
|
+
const usedVariants = values?.map((variant) => variant.variantId)
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Stack space={3}>
|
|
52
|
+
{props.renderDefault({...props, arrayFunctions: () => null})}
|
|
53
|
+
|
|
54
|
+
<Inline space={1}>
|
|
55
|
+
{filteredVariants.map((variant) => {
|
|
56
|
+
return (
|
|
57
|
+
<Button
|
|
58
|
+
key={`${experimentId}-${variant.id}`}
|
|
59
|
+
text={`Add ${variant.label}`}
|
|
60
|
+
mode="ghost"
|
|
61
|
+
disabled={usedVariants?.includes(variant.id)}
|
|
62
|
+
onClick={() => handleClick(variant)}
|
|
63
|
+
/>
|
|
64
|
+
)
|
|
65
|
+
})}
|
|
66
|
+
</Inline>
|
|
67
|
+
</Stack>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import equal from 'fast-deep-equal'
|
|
2
|
+
import {createContext, useContext, useMemo} from 'react'
|
|
3
|
+
import {type ObjectInputProps, useClient, useWorkspace} from 'sanity'
|
|
4
|
+
import {suspend} from 'suspend-react'
|
|
5
|
+
|
|
6
|
+
import {ExperimentContextProps, FieldPluginConfig} from '../types'
|
|
7
|
+
|
|
8
|
+
// This provider makes the plugin config available to all components in the document form
|
|
9
|
+
// But with experiments resolved
|
|
10
|
+
|
|
11
|
+
export const CONFIG_DEFAULT = {
|
|
12
|
+
fields: [],
|
|
13
|
+
apiVersion: '2024-11-07',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const ExperimentContext = createContext<ExperimentContextProps>({
|
|
17
|
+
...CONFIG_DEFAULT,
|
|
18
|
+
experiments: [],
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
export function useExperimentContext() {
|
|
22
|
+
return useContext(ExperimentContext)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type ExperimentProps = ObjectInputProps & {
|
|
26
|
+
experimentFieldPluginConfig: Required<FieldPluginConfig>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function ExperimentProvider(props: ExperimentProps) {
|
|
30
|
+
const {experimentFieldPluginConfig} = props
|
|
31
|
+
|
|
32
|
+
const client = useClient({apiVersion: experimentFieldPluginConfig.apiVersion})
|
|
33
|
+
const workspace = useWorkspace()
|
|
34
|
+
|
|
35
|
+
// Fetch or return experiments
|
|
36
|
+
const experiments = Array.isArray(experimentFieldPluginConfig.experiments)
|
|
37
|
+
? experimentFieldPluginConfig.experiments
|
|
38
|
+
: suspend(
|
|
39
|
+
// eslint-disable-next-line require-await
|
|
40
|
+
async () => {
|
|
41
|
+
if (typeof experimentFieldPluginConfig.experiments === 'function') {
|
|
42
|
+
return experimentFieldPluginConfig.experiments(client)
|
|
43
|
+
}
|
|
44
|
+
return experimentFieldPluginConfig.experiments
|
|
45
|
+
},
|
|
46
|
+
[workspace],
|
|
47
|
+
{equal},
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
const context = useMemo(
|
|
51
|
+
() => ({...experimentFieldPluginConfig, experiments}),
|
|
52
|
+
[experimentFieldPluginConfig, experiments],
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<ExperimentContext.Provider value={context}>
|
|
57
|
+
{props.renderDefault(props)}
|
|
58
|
+
</ExperimentContext.Provider>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {CloseIcon} from '@sanity/icons'
|
|
2
|
+
import {useCallback, useMemo} from 'react'
|
|
3
|
+
import {GiSoapExperiment} from 'react-icons/gi'
|
|
4
|
+
import {
|
|
5
|
+
defineDocumentFieldAction,
|
|
6
|
+
DocumentFieldActionItem,
|
|
7
|
+
DocumentFieldActionProps,
|
|
8
|
+
FormPatch,
|
|
9
|
+
ObjectFieldProps,
|
|
10
|
+
PatchEvent,
|
|
11
|
+
set,
|
|
12
|
+
unset,
|
|
13
|
+
} from 'sanity'
|
|
14
|
+
type PatchStuff = {onChange: (patch: FormPatch | FormPatch[] | PatchEvent) => void; inputId: string}
|
|
15
|
+
|
|
16
|
+
const useAddExperimentAction = (
|
|
17
|
+
props: DocumentFieldActionProps & PatchStuff,
|
|
18
|
+
): DocumentFieldActionItem => {
|
|
19
|
+
const patchActiveEvent = useMemo(() => {
|
|
20
|
+
return set(true, ['active'])
|
|
21
|
+
}, [])
|
|
22
|
+
const handleAction = useCallback(() => {
|
|
23
|
+
props.onChange([patchActiveEvent])
|
|
24
|
+
}, [patchActiveEvent, props])
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
title: 'Add experiment',
|
|
28
|
+
type: 'action',
|
|
29
|
+
icon: GiSoapExperiment,
|
|
30
|
+
onAction: handleAction,
|
|
31
|
+
renderAsButton: true,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const useRemoveExperimentAction = (
|
|
36
|
+
props: DocumentFieldActionProps & PatchStuff,
|
|
37
|
+
): DocumentFieldActionItem => {
|
|
38
|
+
const patchActiveEvent = useMemo(() => {
|
|
39
|
+
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
|
+
|
|
52
|
+
return {
|
|
53
|
+
title: 'Remove experiment',
|
|
54
|
+
type: 'action',
|
|
55
|
+
icon: CloseIcon,
|
|
56
|
+
onAction: handleAction,
|
|
57
|
+
renderAsButton: true,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
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
|
+
})
|
|
71
|
+
|
|
72
|
+
export const ExperimentField = (props: ObjectFieldProps) => {
|
|
73
|
+
const {onChange} = props.inputProps
|
|
74
|
+
const {inputId} = props
|
|
75
|
+
const active = props.value?.active as boolean | undefined
|
|
76
|
+
|
|
77
|
+
const oldActions = props.actions || []
|
|
78
|
+
|
|
79
|
+
const withActionProps = {
|
|
80
|
+
...props,
|
|
81
|
+
actions: [newActions({onChange, inputId, active}), ...oldActions],
|
|
82
|
+
}
|
|
83
|
+
return props.renderDefault(withActionProps)
|
|
84
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {FormEvent, useCallback, useMemo} from 'react'
|
|
2
|
+
import {
|
|
3
|
+
FormPatch,
|
|
4
|
+
PatchEvent,
|
|
5
|
+
set,
|
|
6
|
+
StringInputProps,
|
|
7
|
+
unset,
|
|
8
|
+
useDocumentOperation,
|
|
9
|
+
useFormValue,
|
|
10
|
+
} from 'sanity'
|
|
11
|
+
|
|
12
|
+
import {ExperimentType} from '..'
|
|
13
|
+
import {useExperimentContext} from './ExperimentContext'
|
|
14
|
+
import {Select} from './Select'
|
|
15
|
+
|
|
16
|
+
export type SelectOption = {title: string; value: string}
|
|
17
|
+
const formatlistOptions = (experiments: ExperimentType[]): SelectOption[] =>
|
|
18
|
+
experiments.map((experiment) => ({
|
|
19
|
+
title: experiment.label,
|
|
20
|
+
value: experiment.id,
|
|
21
|
+
}))
|
|
22
|
+
|
|
23
|
+
export const ExperimentInput = (props: StringInputProps) => {
|
|
24
|
+
const {experiments} = useExperimentContext()
|
|
25
|
+
|
|
26
|
+
const id = useFormValue(['_id']) as string
|
|
27
|
+
const aditionalChangePath = useMemo(() => [...props.path.slice(0, -1), 'variants'], [props.path])
|
|
28
|
+
const subValues = useFormValue(aditionalChangePath)
|
|
29
|
+
|
|
30
|
+
const {patch} = useDocumentOperation(id.replace('drafts.', ''), props.schemaType.name)
|
|
31
|
+
|
|
32
|
+
const handleChange = useCallback(
|
|
33
|
+
(
|
|
34
|
+
event: FormEvent<Element>,
|
|
35
|
+
onChange: (patchchange: FormPatch | FormPatch[] | PatchEvent) => void,
|
|
36
|
+
) => {
|
|
37
|
+
const target = event.currentTarget as HTMLSelectElement
|
|
38
|
+
const inputValue = target.value
|
|
39
|
+
|
|
40
|
+
if (inputValue) {
|
|
41
|
+
onChange(set(inputValue))
|
|
42
|
+
} else {
|
|
43
|
+
onChange(unset())
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (subValues) {
|
|
47
|
+
const patchEvent = {
|
|
48
|
+
unset: [aditionalChangePath.join('.')],
|
|
49
|
+
}
|
|
50
|
+
patch.execute([patchEvent])
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
[patch, subValues, aditionalChangePath],
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if (!experiments.length) return <></>
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Select {...props} listOptions={formatlistOptions(experiments)} handleChange={handleChange} />
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {Select as SanitySelect} from '@sanity/ui'
|
|
2
|
+
import {FormEvent} from 'react'
|
|
3
|
+
import {FormPatch, PatchEvent, Path, StringInputProps} from 'sanity'
|
|
4
|
+
|
|
5
|
+
import {SelectOption} from './ExperimentInput'
|
|
6
|
+
|
|
7
|
+
export const Select = (
|
|
8
|
+
props: StringInputProps & {
|
|
9
|
+
listOptions: SelectOption[]
|
|
10
|
+
handleChange: (
|
|
11
|
+
event: FormEvent,
|
|
12
|
+
onChange: (patch: FormPatch | FormPatch[] | PatchEvent) => void,
|
|
13
|
+
) => void
|
|
14
|
+
aditionalChangePath?: Path
|
|
15
|
+
clearSubValueOnChange?: boolean
|
|
16
|
+
},
|
|
17
|
+
) => {
|
|
18
|
+
const {
|
|
19
|
+
value, // Current field value
|
|
20
|
+
onChange, // Method to handle patch events,
|
|
21
|
+
elementProps,
|
|
22
|
+
listOptions,
|
|
23
|
+
handleChange,
|
|
24
|
+
} = props
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<SanitySelect
|
|
28
|
+
{...elementProps}
|
|
29
|
+
fontSize={2}
|
|
30
|
+
padding={3}
|
|
31
|
+
space={[3, 3, 4]}
|
|
32
|
+
value={value || ''} // Current field value
|
|
33
|
+
onChange={(event) => handleChange(event, onChange)} // A function to call when the input value changes
|
|
34
|
+
>
|
|
35
|
+
<option value={''}>{'Select an option...'}</option>
|
|
36
|
+
{listOptions.map(({value: optionValue, title}) => (
|
|
37
|
+
<option key={optionValue} value={optionValue}>
|
|
38
|
+
{title}
|
|
39
|
+
</option>
|
|
40
|
+
))}
|
|
41
|
+
</SanitySelect>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Select
|
|
66
|
+
{...props}
|
|
67
|
+
listOptions={formatlistOptions(filteredVariants)}
|
|
68
|
+
handleChange={handleChange}
|
|
69
|
+
/>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {useEffect, useState} from 'react'
|
|
2
|
+
import {
|
|
3
|
+
isImage,
|
|
4
|
+
isReference,
|
|
5
|
+
ObjectSchemaType,
|
|
6
|
+
PreviewProps,
|
|
7
|
+
ReferenceSchemaType,
|
|
8
|
+
useClient,
|
|
9
|
+
} from 'sanity'
|
|
10
|
+
|
|
11
|
+
import {VariantPreviewProps} from '../types'
|
|
12
|
+
import {useExperimentContext} from './ExperimentContext'
|
|
13
|
+
|
|
14
|
+
export const VariantPreview = (props: PreviewProps) => {
|
|
15
|
+
const [subtitle, setSubtitle] = useState<string | undefined>(undefined)
|
|
16
|
+
const [title, setTitle] = useState<string | undefined>(undefined)
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
|
+
const [media, setMedia] = useState<any>(undefined)
|
|
19
|
+
const client = useClient({apiVersion: '2025-01-01'})
|
|
20
|
+
const {experiments} = useExperimentContext()
|
|
21
|
+
|
|
22
|
+
const {experiment, variant, value} = props as VariantPreviewProps
|
|
23
|
+
|
|
24
|
+
const selectedExperiment = experiments.find((experimentItem) => {
|
|
25
|
+
return experimentItem.id === experiment
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const selectedVariant = selectedExperiment?.variants.find((variantItem) => {
|
|
29
|
+
return variantItem.id === variant
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const getSubtitle = async () => {
|
|
34
|
+
setTitle(`${selectedExperiment?.label} - ${selectedVariant?.label}`)
|
|
35
|
+
if (typeof value === 'string') {
|
|
36
|
+
return setSubtitle(value)
|
|
37
|
+
}
|
|
38
|
+
if (isReference(value)) {
|
|
39
|
+
const doc = await client.getDocument(value._ref)
|
|
40
|
+
const type = props.schemaType as ObjectSchemaType
|
|
41
|
+
const valueField = type.fields.find((field) => field.name === 'value') as ObjectSchemaType
|
|
42
|
+
const referenceField = valueField?.type as ReferenceSchemaType
|
|
43
|
+
const referenceType = referenceField.to.find((field) => field.type?.name === doc?._type)
|
|
44
|
+
|
|
45
|
+
const selectFields = {} as Record<string, unknown>
|
|
46
|
+
const previewFields = referenceType?.preview?.select || {}
|
|
47
|
+
Object.keys(previewFields).forEach((key) => {
|
|
48
|
+
const valueKey = referenceType?.preview?.select?.[key]
|
|
49
|
+
selectFields[key] =
|
|
50
|
+
valueKey && doc
|
|
51
|
+
? valueKey?.split('.').reduce((acc, index) => acc[index], doc)
|
|
52
|
+
: undefined
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const previewContent = referenceType?.preview?.prepare?.(selectFields)
|
|
56
|
+
setMedia(previewContent?.media || selectFields.media)
|
|
57
|
+
return setSubtitle(previewContent?.title || (selectFields?.title as string))
|
|
58
|
+
}
|
|
59
|
+
if (isImage(value)) {
|
|
60
|
+
setMedia(value)
|
|
61
|
+
}
|
|
62
|
+
return ''
|
|
63
|
+
}
|
|
64
|
+
getSubtitle()
|
|
65
|
+
}, [value, client, selectedExperiment?.label, selectedVariant?.label, props.schemaType])
|
|
66
|
+
|
|
67
|
+
const previewProps = {
|
|
68
|
+
...props,
|
|
69
|
+
title: title,
|
|
70
|
+
subtitle: subtitle,
|
|
71
|
+
media: media,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return props.renderDefault(previewProps)
|
|
75
|
+
}
|