@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.
@@ -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
@@ -0,0 +1,3 @@
1
+ export * from './fieldExperiments'
2
+ export * from './types'
3
+ export * from './utils/flattenSchemaType'
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
+ })