@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
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArrayOfObjectsInputProps,
|
|
3
|
+
defineField,
|
|
4
|
+
definePlugin,
|
|
5
|
+
defineType,
|
|
6
|
+
FieldDefinition,
|
|
7
|
+
isObjectInputProps,
|
|
8
|
+
SanityClient,
|
|
9
|
+
} from 'sanity'
|
|
10
|
+
|
|
11
|
+
import {ArrayInput} from './components/Array'
|
|
12
|
+
import {CONFIG_DEFAULT, ExperimentProvider} from './components/ExperimentContext'
|
|
13
|
+
import {ExperimentField} from './components/ExperimentField'
|
|
14
|
+
import {ExperimentInput} from './components/ExperimentInput'
|
|
15
|
+
import {VariantPreview} from './components/VariantPreview'
|
|
16
|
+
import {ExperimentType, FieldPluginConfig} from './types'
|
|
17
|
+
import {flattenSchemaType} from './utils/flattenSchemaType'
|
|
18
|
+
|
|
19
|
+
const createFieldType = ({
|
|
20
|
+
field,
|
|
21
|
+
}: {
|
|
22
|
+
field: string | FieldDefinition
|
|
23
|
+
experiments: ExperimentType[] | ((client: SanityClient) => Promise<ExperimentType[]>)
|
|
24
|
+
}) => {
|
|
25
|
+
const typeName = typeof field === `string` ? field : field.name
|
|
26
|
+
const usedName = String(typeName[0]).toUpperCase() + String(typeName).slice(1)
|
|
27
|
+
const objectName = `variant${usedName}`
|
|
28
|
+
|
|
29
|
+
return defineType({
|
|
30
|
+
name: `experiment${usedName}`,
|
|
31
|
+
type: 'object',
|
|
32
|
+
components: {
|
|
33
|
+
field: ExperimentField,
|
|
34
|
+
},
|
|
35
|
+
fields: [
|
|
36
|
+
typeof field === `string`
|
|
37
|
+
? // Define a simple field if all we have is the name as a string
|
|
38
|
+
defineField({
|
|
39
|
+
name: 'default',
|
|
40
|
+
type: field,
|
|
41
|
+
})
|
|
42
|
+
: // Pass in the configured options, but overwrite the name
|
|
43
|
+
{
|
|
44
|
+
...field,
|
|
45
|
+
name: 'default',
|
|
46
|
+
},
|
|
47
|
+
defineField({
|
|
48
|
+
name: 'active',
|
|
49
|
+
type: 'boolean',
|
|
50
|
+
hidden: true,
|
|
51
|
+
}),
|
|
52
|
+
defineField({
|
|
53
|
+
title: 'Experiment',
|
|
54
|
+
name: 'experimentId',
|
|
55
|
+
type: 'string',
|
|
56
|
+
components: {
|
|
57
|
+
input: ExperimentInput,
|
|
58
|
+
},
|
|
59
|
+
hidden: ({parent}) => {
|
|
60
|
+
return !parent?.active
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
defineField({
|
|
64
|
+
name: 'variants',
|
|
65
|
+
type: 'array',
|
|
66
|
+
hidden: ({parent}) => {
|
|
67
|
+
return !parent?.experimentId
|
|
68
|
+
},
|
|
69
|
+
components: {
|
|
70
|
+
input: (props: ArrayOfObjectsInputProps) => (
|
|
71
|
+
<ArrayInput {...props} objectName={objectName} />
|
|
72
|
+
),
|
|
73
|
+
},
|
|
74
|
+
of: [
|
|
75
|
+
defineField({
|
|
76
|
+
name: objectName,
|
|
77
|
+
type: objectName,
|
|
78
|
+
}),
|
|
79
|
+
],
|
|
80
|
+
}),
|
|
81
|
+
],
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const createFieldObjectType = ({
|
|
86
|
+
field,
|
|
87
|
+
}: {
|
|
88
|
+
field: string | FieldDefinition
|
|
89
|
+
experiments: ExperimentType[] | ((client: SanityClient) => Promise<ExperimentType[]>)
|
|
90
|
+
}) => {
|
|
91
|
+
const typeName = typeof field === `string` ? field : field.name
|
|
92
|
+
const usedName = String(typeName[0]).toUpperCase() + String(typeName).slice(1)
|
|
93
|
+
return defineType({
|
|
94
|
+
name: `variant${usedName}`,
|
|
95
|
+
title: `Experiment array ${usedName}`,
|
|
96
|
+
type: 'object',
|
|
97
|
+
components: {
|
|
98
|
+
preview: VariantPreview,
|
|
99
|
+
},
|
|
100
|
+
fields: [
|
|
101
|
+
{
|
|
102
|
+
type: 'string',
|
|
103
|
+
name: 'variantId',
|
|
104
|
+
readOnly: true,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
type: 'string',
|
|
108
|
+
name: 'experimentId',
|
|
109
|
+
hidden: true,
|
|
110
|
+
},
|
|
111
|
+
typeof field === `string`
|
|
112
|
+
? // Define a simple field if all we have is the name as a string
|
|
113
|
+
defineField({
|
|
114
|
+
name: 'value',
|
|
115
|
+
type: field,
|
|
116
|
+
hidden: ({parent}) => !parent?.variantId,
|
|
117
|
+
})
|
|
118
|
+
: // Pass in the configured options, but overwrite the name
|
|
119
|
+
{
|
|
120
|
+
...field,
|
|
121
|
+
name: 'value',
|
|
122
|
+
hidden: ({parent}) => !parent?.variantId,
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
preview: {
|
|
126
|
+
select: {
|
|
127
|
+
variant: 'variantId',
|
|
128
|
+
experiment: 'experimentId',
|
|
129
|
+
value: 'value',
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const fieldSchema = ({fields, experiments}: FieldPluginConfig) => {
|
|
136
|
+
return [
|
|
137
|
+
...fields.map((field) => createFieldObjectType({field, experiments})),
|
|
138
|
+
...fields.map((field) => createFieldType({field, experiments})),
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const fieldLevelExperiments = definePlugin<FieldPluginConfig>((config) => {
|
|
143
|
+
const pluginConfig = {...CONFIG_DEFAULT, ...config}
|
|
144
|
+
const {fields, experiments} = pluginConfig
|
|
145
|
+
const fieldSchemaConfig = fieldSchema({fields, experiments})
|
|
146
|
+
return {
|
|
147
|
+
name: 'sanity-personalistaion-plugin-field-level-experiments',
|
|
148
|
+
schema: {
|
|
149
|
+
types: fieldSchemaConfig,
|
|
150
|
+
},
|
|
151
|
+
form: {
|
|
152
|
+
components: {
|
|
153
|
+
input: (props) => {
|
|
154
|
+
const isRootInput = props.id === 'root' && isObjectInputProps(props)
|
|
155
|
+
|
|
156
|
+
if (!isRootInput) {
|
|
157
|
+
return props.renderDefault(props)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const flatFieldTypeNames = flattenSchemaType(props.schemaType).map(
|
|
161
|
+
(field) => field.type.name,
|
|
162
|
+
)
|
|
163
|
+
const hasExperiment = flatFieldTypeNames.some((name) => name.startsWith('experiment'))
|
|
164
|
+
|
|
165
|
+
if (!hasExperiment) {
|
|
166
|
+
return props.renderDefault(props)
|
|
167
|
+
}
|
|
168
|
+
const providerProps = {...props, experimentFieldPluginConfig: pluginConfig}
|
|
169
|
+
return ExperimentProvider(providerProps)
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
})
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArrayOfObjectsInputProps,
|
|
3
|
+
FieldDefinition,
|
|
4
|
+
ObjectField,
|
|
5
|
+
Path,
|
|
6
|
+
PreviewProps,
|
|
7
|
+
SanityClient,
|
|
8
|
+
SchemaType,
|
|
9
|
+
} from 'sanity'
|
|
10
|
+
|
|
11
|
+
export type VariantType = {
|
|
12
|
+
id: string
|
|
13
|
+
label: string
|
|
14
|
+
}
|
|
15
|
+
export type ExperimentType = {
|
|
16
|
+
id: string
|
|
17
|
+
label: string
|
|
18
|
+
variants: VariantType[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type FieldPluginConfig = {
|
|
22
|
+
fields: (string | FieldDefinition)[]
|
|
23
|
+
experiments: ExperimentType[] | ((client: SanityClient) => Promise<ExperimentType[]>)
|
|
24
|
+
apiVersion?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type VariantPreviewProps = Omit<PreviewProps, 'SchemaType'> & {
|
|
28
|
+
experiment: string
|
|
29
|
+
variant: string
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
31
|
+
value: any
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type ExperimentContextProps = Required<FieldPluginConfig> & {
|
|
35
|
+
experiments: ExperimentType[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ArrayInputProps = ArrayOfObjectsInputProps & {
|
|
39
|
+
objectName: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type ObjectFieldWithPath = ObjectField<SchemaType> & {path: Path}
|
|
43
|
+
|
|
44
|
+
export type VariantGeneric<T> = {
|
|
45
|
+
_type: string
|
|
46
|
+
variantId?: string
|
|
47
|
+
experimentId?: string
|
|
48
|
+
value?: T
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type ExperimentGeneric<T> = {
|
|
52
|
+
_type: string
|
|
53
|
+
default?: T
|
|
54
|
+
experimentValue?: string
|
|
55
|
+
variants?: Array<
|
|
56
|
+
{
|
|
57
|
+
_key: string
|
|
58
|
+
} & VariantGeneric<T>
|
|
59
|
+
>
|
|
60
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {isDocumentSchemaType, type ObjectField, type Path, type SchemaType} from 'sanity'
|
|
2
|
+
|
|
3
|
+
import {ObjectFieldWithPath} from '../types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Flattens a document's schema type into a flat array of fields and includes their path
|
|
7
|
+
*/
|
|
8
|
+
export function flattenSchemaType(schemaType: SchemaType): ObjectFieldWithPath[] {
|
|
9
|
+
if (!isDocumentSchemaType(schemaType)) {
|
|
10
|
+
console.error(`Schema type is not a document`)
|
|
11
|
+
return []
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return extractInnerFields(schemaType.fields, [], 3)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function extractInnerFields(
|
|
18
|
+
fields: ObjectField<SchemaType>[],
|
|
19
|
+
path: Path,
|
|
20
|
+
maxDepth: number,
|
|
21
|
+
): ObjectFieldWithPath[] {
|
|
22
|
+
if (path.length >= maxDepth) {
|
|
23
|
+
return []
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return fields.reduce<ObjectFieldWithPath[]>((acc, field) => {
|
|
27
|
+
const thisFieldWithPath = {path: [...path, field.name], ...field}
|
|
28
|
+
|
|
29
|
+
if (field.type.jsonType === 'object') {
|
|
30
|
+
const innerFields = extractInnerFields(field.type.fields, [...path, field.name], maxDepth)
|
|
31
|
+
|
|
32
|
+
return [...acc, thisFieldWithPath, ...innerFields]
|
|
33
|
+
} else if (
|
|
34
|
+
field.type.jsonType === 'array' &&
|
|
35
|
+
field.type.of.length &&
|
|
36
|
+
field.type.of.some((item) => 'fields' in item)
|
|
37
|
+
) {
|
|
38
|
+
const innerFields = extractInnerFields(
|
|
39
|
+
// @ts-expect-error - Fix TS assertion for array fields
|
|
40
|
+
field.type.of[0].fields,
|
|
41
|
+
[...path, field.name],
|
|
42
|
+
maxDepth,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return [...acc, thisFieldWithPath, ...innerFields]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return [...acc, thisFieldWithPath]
|
|
49
|
+
}, [])
|
|
50
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const {showIncompatiblePluginDialog} = require('@sanity/incompatible-plugin')
|
|
2
|
+
const {name, version, sanityExchangeUrl} = require('./package.json')
|
|
3
|
+
|
|
4
|
+
export default showIncompatiblePluginDialog({
|
|
5
|
+
name: name,
|
|
6
|
+
versions: {
|
|
7
|
+
v3: version,
|
|
8
|
+
v2: undefined,
|
|
9
|
+
},
|
|
10
|
+
sanityExchangeUrl,
|
|
11
|
+
})
|