@sanity/personalization-plugin 2.5.0-field-level-personalization.1 → 2.5.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.
Files changed (47) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +2 -1
  3. package/dist/growthbook/index.js +3 -3
  4. package/dist/growthbook/index.js.map +1 -1
  5. package/dist/growthbook/index.mjs +1 -1
  6. package/dist/index.d.mts +12 -33
  7. package/dist/index.d.ts +12 -33
  8. package/dist/index.js +280 -157
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +280 -159
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/launchDarkly/index.d.mts +12 -0
  13. package/dist/launchDarkly/index.d.ts +12 -0
  14. package/dist/launchDarkly/index.js +103 -0
  15. package/dist/launchDarkly/index.js.map +1 -0
  16. package/dist/launchDarkly/index.mjs +107 -0
  17. package/dist/launchDarkly/index.mjs.map +1 -0
  18. package/package.json +10 -5
  19. package/src/components/{experiment/Array.tsx → Array.tsx} +2 -2
  20. package/src/components/{experiment/Context.tsx → ExperimentContext.tsx} +2 -2
  21. package/src/components/{experiment/Field.tsx → ExperimentField.tsx} +8 -11
  22. package/src/components/{experiment/Input.tsx → ExperimentInput.tsx} +4 -4
  23. package/src/components/{ArrayItem.tsx → ExperimentItem.tsx} +2 -1
  24. package/src/components/Select.tsx +1 -1
  25. package/src/components/{experiment/VariantPreview.tsx → VariantPreview.tsx} +2 -2
  26. package/src/fieldExperiments.tsx +13 -43
  27. package/src/index.ts +0 -1
  28. package/src/launchDarkly/components/LaunchDarklyContext.tsx +36 -0
  29. package/src/launchDarkly/components/Secrets.tsx +46 -0
  30. package/src/launchDarkly/index.ts +52 -0
  31. package/src/launchDarkly/types.ts +193 -0
  32. package/src/launchDarkly/utils.ts +54 -0
  33. package/src/types.ts +2 -20
  34. package/dist/_chunks-cjs/fieldExperiments.js +0 -507
  35. package/dist/_chunks-cjs/fieldExperiments.js.map +0 -1
  36. package/dist/_chunks-es/fieldExperiments.mjs +0 -511
  37. package/dist/_chunks-es/fieldExperiments.mjs.map +0 -1
  38. package/src/components/experiment/index.ts +0 -6
  39. package/src/components/personalization/Array.tsx +0 -59
  40. package/src/components/personalization/Context.tsx +0 -61
  41. package/src/components/personalization/Field.tsx +0 -134
  42. package/src/components/personalization/SegmentInput.tsx +0 -19
  43. package/src/components/personalization/SegmentPreview.tsx +0 -71
  44. package/src/components/personalization/index.ts +0 -5
  45. package/src/fieldPersonalization.tsx +0 -254
  46. package/src/utils/clearChildGroups.ts +0 -33
  47. /package/src/components/{experiment/VariantInput.tsx → VariantInput.tsx} +0 -0
@@ -1,6 +0,0 @@
1
- export * from './Array'
2
- export * from './Context'
3
- export * from './Field'
4
- export * from './Input'
5
- export * from './VariantInput'
6
- export * from './VariantPreview'
@@ -1,59 +0,0 @@
1
- import {Button, Inline, Stack} from '@sanity/ui'
2
- import {uuid} from '@sanity/uuid'
3
- import {useCallback} from 'react'
4
-
5
- import {PersonalizationArrayInputProps, VariantType} from '../../types'
6
- import {usePersonalizationContext} from './Context'
7
-
8
- export const ArrayInput = (props: PersonalizationArrayInputProps) => {
9
- const {onItemAppend, segmentName, segmentId} = props
10
-
11
- const {segments} = usePersonalizationContext()
12
-
13
- const handleClick = useCallback(
14
- async (segment: VariantType) => {
15
- const item = {
16
- _key: uuid(),
17
- [segmentId]: segment.id,
18
- _type: segmentName,
19
- }
20
-
21
- // Patch the document
22
- onItemAppend(item)
23
- },
24
- [segmentId, segmentName, onItemAppend],
25
- )
26
-
27
- type Value = {
28
- value?: unknown
29
- [key: string]: string | unknown
30
- segmentId: string
31
- _key: string
32
- _type: string
33
- }
34
-
35
- // there is probably some better was of getting the type of this?
36
- const values = (props.value as Value[]) || []
37
-
38
- const usedSegments = values?.map((segment) => segment[segmentId])
39
-
40
- return (
41
- <Stack space={3}>
42
- {props.renderDefault({...props, arrayFunctions: () => null})}
43
-
44
- <Inline space={1}>
45
- {segments.map((segment) => {
46
- return (
47
- <Button
48
- key={`${segment.id}`}
49
- text={`Add ${segment.label}`}
50
- mode="ghost"
51
- disabled={usedSegments?.includes(segment.id)}
52
- onClick={() => handleClick(segment)}
53
- />
54
- )
55
- })}
56
- </Inline>
57
- </Stack>
58
- )
59
- }
@@ -1,61 +0,0 @@
1
- import equal from 'fast-deep-equal'
2
- import {createContext, useContext, useMemo} from 'react'
3
- import {ObjectInputProps, useClient, useWorkspace} from 'sanity'
4
- import {suspend} from 'suspend-react'
5
-
6
- import {PersonalizationContextProps, PersonalizationFieldPluginConfig} from '../../types'
7
-
8
- export const CONFIG_DEFAULT = {
9
- fields: [],
10
- apiVersion: '2024-11-07',
11
- personalizationNameOverride: 'personalization',
12
- segmentNameOverride: 'segment',
13
- segmentId: 'segmentId',
14
- segmentArrayName: 'segments',
15
- }
16
-
17
- export const PersonalizationContext = createContext<PersonalizationContextProps>({
18
- ...CONFIG_DEFAULT,
19
- segments: [],
20
- })
21
-
22
- export function usePersonalizationContext() {
23
- return useContext(PersonalizationContext)
24
- }
25
-
26
- type PersonalizationProps = ObjectInputProps & {
27
- personalizationFieldPluginConfig: Required<PersonalizationFieldPluginConfig>
28
- }
29
-
30
- export function PersonalizationProvider(props: PersonalizationProps) {
31
- const {personalizationFieldPluginConfig} = props
32
-
33
- const client = useClient({apiVersion: personalizationFieldPluginConfig.apiVersion})
34
- const workspace = useWorkspace()
35
-
36
- // Fetch or return experiments
37
- const segments = Array.isArray(personalizationFieldPluginConfig.segments)
38
- ? personalizationFieldPluginConfig.segments
39
- : suspend(
40
- // eslint-disable-next-line require-await
41
- async () => {
42
- if (typeof personalizationFieldPluginConfig.segments === 'function') {
43
- return personalizationFieldPluginConfig.segments(client)
44
- }
45
- return personalizationFieldPluginConfig.segments
46
- },
47
- [workspace],
48
- {equal},
49
- )
50
-
51
- const context = useMemo(
52
- () => ({...personalizationFieldPluginConfig, segments}),
53
- [personalizationFieldPluginConfig, segments],
54
- )
55
-
56
- return (
57
- <PersonalizationContext.Provider value={context}>
58
- {props.renderDefault(props)}
59
- </PersonalizationContext.Provider>
60
- )
61
- }
@@ -1,134 +0,0 @@
1
- import {CloseIcon} from '@sanity/icons'
2
- import React, {useCallback, useMemo} from 'react'
3
- import {IoMdPeople} from 'react-icons/io'
4
- import {
5
- defineDocumentFieldAction,
6
- DocumentFieldActionItem,
7
- DocumentFieldActionProps,
8
- FormPatch,
9
- ObjectFieldProps,
10
- PatchEvent,
11
- set,
12
- unset,
13
- } from 'sanity'
14
-
15
- import {clearChildrenGroups} from '../../utils/clearChildGroups'
16
-
17
- type PatchStuff = {onChange: (patch: FormPatch | FormPatch[] | PatchEvent) => void; inputId: string}
18
-
19
- const useAddExperimentAction = (
20
- props: DocumentFieldActionProps &
21
- PatchStuff & {personalizationNameOverride: string; active: boolean},
22
- ): DocumentFieldActionItem => {
23
- const {onChange, active, personalizationNameOverride} = props
24
-
25
- const handleAddAction = useCallback(() => {
26
- onChange([set(!active, ['active'])])
27
- }, [onChange, active])
28
-
29
- return {
30
- title: `Add ${personalizationNameOverride}`,
31
- type: 'action',
32
- icon: IoMdPeople,
33
- onAction: handleAddAction,
34
- renderAsButton: true,
35
- }
36
- }
37
-
38
- const useRemoveExperimentAction = (
39
- props: DocumentFieldActionProps &
40
- PatchStuff & {
41
- personalizationNameOverride: string
42
- active: boolean
43
- segmentNameOverride: string
44
- },
45
- ): DocumentFieldActionItem => {
46
- const {onChange, active, personalizationNameOverride, segmentNameOverride} = props
47
- const handleClearAction = useCallback(() => {
48
- const activeId = ['active']
49
- const segments = [`${segmentNameOverride}s`]
50
- onChange([set(!active, activeId), unset(segments)])
51
- }, [onChange, active, segmentNameOverride])
52
-
53
- return {
54
- title: `Remove ${personalizationNameOverride}`,
55
- type: 'action',
56
- icon: CloseIcon,
57
- onAction: handleClearAction,
58
- renderAsButton: true,
59
- }
60
- }
61
-
62
- const createActions = ({
63
- onChange,
64
- inputId,
65
- active,
66
- personalizationNameOverride,
67
- segmentNameOverride,
68
- }: PatchStuff & {
69
- active?: boolean
70
- personalizationNameOverride: string
71
- segmentNameOverride: string
72
- }) => {
73
- const removeAction = defineDocumentFieldAction({
74
- name: `Remove ${personalizationNameOverride}`,
75
- useAction: (props) =>
76
- useRemoveExperimentAction({
77
- ...props,
78
- active: true,
79
- onChange,
80
- inputId,
81
- personalizationNameOverride,
82
- segmentNameOverride,
83
- }),
84
- })
85
- const addAction = defineDocumentFieldAction({
86
- name: `Add ${personalizationNameOverride}`,
87
- useAction: (props) =>
88
- useAddExperimentAction({
89
- ...props,
90
- active: false,
91
- onChange,
92
- inputId,
93
- personalizationNameOverride,
94
- }),
95
- })
96
- return active ? removeAction : addAction
97
- }
98
-
99
- export const Field = (
100
- props: ObjectFieldProps & {
101
- personalizationNameOverride: string
102
- segmentNameOverride: string
103
- },
104
- ): React.ReactElement => {
105
- const {onChange} = props.inputProps
106
- const {inputId, personalizationNameOverride, segmentNameOverride} = props
107
- const active = props.value?.active as boolean | undefined
108
-
109
- const actionProps = useMemo(
110
- () => ({
111
- onChange,
112
- inputId,
113
- active,
114
- personalizationNameOverride,
115
- segmentNameOverride,
116
- }),
117
- [onChange, inputId, active, personalizationNameOverride, segmentNameOverride],
118
- )
119
-
120
- const memoizedActions = useMemo(() => {
121
- const oldActions = props.actions || []
122
- return [createActions(actionProps), ...oldActions]
123
- }, [actionProps, props.actions])
124
-
125
- const enhancedProps = useMemo(() => {
126
- const propsWithClearedGroups = clearChildrenGroups(props)
127
- return {
128
- ...propsWithClearedGroups,
129
- actions: memoizedActions,
130
- }
131
- }, [props, memoizedActions])
132
-
133
- return props.renderDefault(enhancedProps)
134
- }
@@ -1,19 +0,0 @@
1
- import {Button, Inline, Stack} from '@sanity/ui'
2
- import {ObjectInputProps, set, useFormValue} from 'sanity'
3
-
4
- export const SegmentInput = (props: ObjectInputProps) => {
5
- const personalizationPath = props.path.slice(0, -2)
6
- const defaultValue = useFormValue([...personalizationPath, 'default'])
7
- const handleClick = () => {
8
- props.onChange(set(defaultValue, ['value']))
9
- }
10
- return (
11
- <Stack space={3}>
12
- {props.renderDefault(props)}
13
-
14
- <Inline space={1}>
15
- <Button text="Copy default" mode="ghost" onClick={() => handleClick()} />
16
- </Inline>
17
- </Stack>
18
- )
19
- }
@@ -1,71 +0,0 @@
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 {usePersonalizationContext} from './Context'
13
-
14
- export const SegmentPreview = (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 {segments} = usePersonalizationContext()
21
-
22
- const {segment, value} = props as VariantPreviewProps
23
-
24
- const selectedSegment = segments.find((segmentItem) => {
25
- return segmentItem.id === segment
26
- })
27
-
28
- useEffect(() => {
29
- const getSubtitle = async () => {
30
- setTitle(`${selectedSegment?.label}`)
31
- if (typeof value === 'string') {
32
- return setSubtitle(value)
33
- }
34
- if (isReference(value)) {
35
- const doc = await client.getDocument(value._ref)
36
- const type = props.schemaType as ObjectSchemaType
37
- const valueField = type.fields.find((field) => field.name === 'value') as ObjectSchemaType
38
- const referenceField = valueField?.type as ReferenceSchemaType
39
- const referenceType = referenceField.to.find((field) => field.type?.name === doc?._type)
40
-
41
- const selectFields = {} as Record<string, unknown>
42
- const previewFields = referenceType?.preview?.select || {}
43
- Object.keys(previewFields).forEach((key) => {
44
- const valueKey = referenceType?.preview?.select?.[key]
45
- selectFields[key] =
46
- valueKey && doc
47
- ? valueKey?.split('.').reduce((acc, index) => acc[index], doc)
48
- : undefined
49
- })
50
-
51
- const previewContent = referenceType?.preview?.prepare?.(selectFields)
52
- setMedia(previewContent?.media || selectFields.media)
53
- return setSubtitle(previewContent?.title || (selectFields?.title as string))
54
- }
55
- if (isImage(value)) {
56
- setMedia(value)
57
- }
58
- return ''
59
- }
60
- getSubtitle()
61
- }, [value, client, selectedSegment?.label, props.schemaType])
62
-
63
- const previewProps = {
64
- ...props,
65
- title: title,
66
- subtitle: subtitle,
67
- media: media,
68
- }
69
-
70
- return props.renderDefault(previewProps)
71
- }
@@ -1,5 +0,0 @@
1
- export * from './Array'
2
- export * from './Context'
3
- export * from './Field'
4
- export * from './SegmentInput'
5
- export * from './SegmentPreview'
@@ -1,254 +0,0 @@
1
- import {
2
- ArrayOfObjectsInputProps,
3
- defineField,
4
- definePlugin,
5
- defineType,
6
- FieldDefinition,
7
- isObjectInputProps,
8
- } from 'sanity'
9
-
10
- import {ArrayItem} from './components/ArrayItem'
11
- import {
12
- ArrayInput,
13
- CONFIG_DEFAULT,
14
- Field,
15
- PersonalizationProvider,
16
- SegmentInput,
17
- SegmentPreview,
18
- } from './components/personalization'
19
- import {PersonalizationFieldPluginConfig} from './types'
20
- import {flattenSchemaType} from './utils/flattenSchemaType'
21
-
22
- const createPersonalizationType = ({
23
- field,
24
- personalizationNameOverride,
25
- segmentNameOverride,
26
- segmentId,
27
- segmentArrayName,
28
- }: {
29
- field: string | FieldDefinition
30
- personalizationNameOverride: string
31
- segmentNameOverride: string
32
- segmentId: string
33
- segmentArrayName: string
34
- }) => {
35
- const typeName = typeof field === `string` ? field : field.name
36
- const usedName = String(typeName[0]).toUpperCase() + String(typeName).slice(1)
37
- const segmentName = `${segmentNameOverride}${usedName}`
38
-
39
- return defineType({
40
- name: `${personalizationNameOverride}${usedName}`,
41
- type: 'object',
42
- groups: [
43
- {
44
- name: 'default',
45
- title: 'Default',
46
- hidden: ({parent}) => {
47
- return !Array.isArray(parent)
48
- },
49
- },
50
- {
51
- name: 'personalization',
52
- title: 'Personalization options',
53
- hidden: ({parent}) => {
54
- return !Array.isArray(parent)
55
- },
56
- },
57
- {
58
- name: 'all-fields',
59
- title: 'All fields',
60
- hidden: ({parent}) => {
61
- return Array.isArray(parent)
62
- },
63
- },
64
- ],
65
- components: {
66
- field: (props) => (
67
- <Field
68
- {...props}
69
- personalizationNameOverride={personalizationNameOverride}
70
- segmentNameOverride={segmentNameOverride}
71
- />
72
- ),
73
- item: ArrayItem,
74
- },
75
- fields: [
76
- typeof field === `string`
77
- ? // Define a simple field if all we have is the name as a string
78
- defineField({
79
- name: 'default',
80
- type: field,
81
- group: 'default',
82
- })
83
- : // Pass in the configured options, but overwrite the name
84
- {
85
- ...field,
86
- name: 'default',
87
- group: 'default',
88
- },
89
- defineField({
90
- name: 'active',
91
- type: 'boolean',
92
- hidden: true,
93
- initialValue: false,
94
- }),
95
- defineField({
96
- name: segmentArrayName,
97
- type: 'array',
98
- hidden: ({parent}) => {
99
- return !parent?.active
100
- },
101
- group: 'personalization',
102
- components: {
103
- input: (props: ArrayOfObjectsInputProps) => (
104
- <ArrayInput {...props} segmentName={segmentName} segmentId={segmentId} />
105
- ),
106
- },
107
- of: [
108
- defineField({
109
- name: segmentName,
110
- type: segmentName,
111
- }),
112
- ],
113
- }),
114
- ],
115
- preview: {
116
- select: {
117
- base: 'default',
118
- },
119
- prepare: ({base}) => {
120
- const title = base?.title || base?.name || typeof base === 'string' ? base : ''
121
- const media = base?.image || base?.photo || base?.media || ''
122
- return {
123
- title: title,
124
- media,
125
- }
126
- },
127
- },
128
- })
129
- }
130
-
131
- const createSegmentType = ({
132
- field,
133
- segmentNameOverride,
134
- segmentId,
135
- }: {
136
- field: string | FieldDefinition
137
- segmentNameOverride: string
138
- segmentId: string
139
- }) => {
140
- const typeName = typeof field === `string` ? field : field.name
141
- const usedName = String(typeName[0]).toUpperCase() + String(typeName).slice(1)
142
- return defineType({
143
- name: `${segmentNameOverride}${usedName}`,
144
- title: `${segmentNameOverride} array ${usedName}`,
145
- type: 'object',
146
- components: {
147
- preview: SegmentPreview,
148
- input: SegmentInput,
149
- },
150
- fields: [
151
- {
152
- type: 'string',
153
- name: segmentId,
154
- readOnly: true,
155
- },
156
- typeof field === `string`
157
- ? // Define a simple field if all we have is the name as a string
158
- defineField({
159
- name: 'value',
160
- type: field,
161
- // hidden: ({parent}) => !parent?.[`${objectNameOverride}Id`],
162
- })
163
- : // Pass in the configured options, but overwrite the name
164
- {
165
- ...field,
166
- name: 'value',
167
- // hidden: ({parent}) => !parent?.[`${objectNameOverride}Id`],
168
- },
169
- ],
170
- preview: {
171
- select: {
172
- segment: segmentId,
173
- value: 'value',
174
- },
175
- },
176
- })
177
- }
178
-
179
- const fieldSchema = ({
180
- fields,
181
- personalizationNameOverride,
182
- segmentNameOverride,
183
- segmentId,
184
- segmentArrayName,
185
- }: Required<Omit<PersonalizationFieldPluginConfig, 'apiVersion' | 'segments'>>) => {
186
- return [
187
- ...fields.map((field) => createSegmentType({field, segmentNameOverride, segmentId})),
188
- ...fields.map((field) =>
189
- createPersonalizationType({
190
- field,
191
- personalizationNameOverride,
192
- segmentNameOverride,
193
- segmentId,
194
- segmentArrayName,
195
- }),
196
- ),
197
- ]
198
- }
199
-
200
- export const fieldLevelPersonalization = definePlugin<PersonalizationFieldPluginConfig>(
201
- (config) => {
202
- const pluginConfig = {...CONFIG_DEFAULT, ...config}
203
- const {fields, personalizationNameOverride, segmentNameOverride} = pluginConfig
204
-
205
- const segmentArrayName = `${segmentNameOverride}s`
206
- const segmentId = `${segmentNameOverride}Id`
207
-
208
- const fieldSchemaConfig = fieldSchema({
209
- fields,
210
- personalizationNameOverride,
211
- segmentNameOverride,
212
- segmentId,
213
- segmentArrayName,
214
- })
215
- return {
216
- name: 'sanity-personalistaion-plugin-field-level-personalization',
217
- schema: {
218
- types: fieldSchemaConfig,
219
- },
220
- form: {
221
- components: {
222
- input: (props) => {
223
- const isRootInput = props.id === 'root' && isObjectInputProps(props)
224
-
225
- if (!isRootInput) {
226
- return props.renderDefault(props)
227
- }
228
-
229
- const flatFields = flattenSchemaType(props.schemaType)
230
- const hasPersonalization = flatFields.some(
231
- (field) =>
232
- field.type.name.startsWith(personalizationNameOverride) ||
233
- field.name.startsWith(personalizationNameOverride),
234
- )
235
-
236
- if (!hasPersonalization) {
237
- return props.renderDefault(props)
238
- }
239
-
240
- const providerProps = {
241
- ...props,
242
- personalizationFieldPluginConfig: {
243
- ...pluginConfig,
244
- segmentId,
245
- segmentArrayName,
246
- },
247
- }
248
- return PersonalizationProvider(providerProps)
249
- },
250
- },
251
- },
252
- }
253
- },
254
- )
@@ -1,33 +0,0 @@
1
- import {ObjectFieldProps} from 'sanity'
2
-
3
- /**
4
- * Safely updates deeply nested children props to clear groups array
5
- * This prevents field grouping UI conflicts in personalization mode
6
- */
7
- export const clearChildrenGroups = (props: ObjectFieldProps): ObjectFieldProps => {
8
- // Type assertion is needed here because Sanity's ObjectFieldProps children
9
- // typing doesn't account for the nested structure we need to manipulate
10
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
- const children = props.children as any
12
-
13
- if (!children || typeof children !== 'object' || !children.props) {
14
- return props
15
- }
16
-
17
- return {
18
- ...props,
19
- children: {
20
- ...children,
21
- props: {
22
- ...children.props,
23
- children: {
24
- ...children.props.children,
25
- props: {
26
- ...children.props.children?.props,
27
- groups: [],
28
- },
29
- },
30
- },
31
- },
32
- }
33
- }