@sanity/personalization-plugin 2.5.0 → 3.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.
Files changed (48) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +570 -144
  3. package/dist/growthbook/index.d.ts +12 -15
  4. package/dist/growthbook/index.d.ts.map +1 -0
  5. package/dist/growthbook/index.js +104 -68
  6. package/dist/growthbook/index.js.map +1 -1
  7. package/dist/index.d.ts +212 -252
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +398 -301
  10. package/dist/index.js.map +1 -1
  11. package/dist/launchDarkly/index.d.ts +9 -12
  12. package/dist/launchDarkly/index.d.ts.map +1 -0
  13. package/dist/launchDarkly/index.js +74 -46
  14. package/dist/launchDarkly/index.js.map +1 -1
  15. package/package.json +35 -77
  16. package/dist/growthbook/index.d.mts +0 -15
  17. package/dist/growthbook/index.mjs +0 -124
  18. package/dist/growthbook/index.mjs.map +0 -1
  19. package/dist/index.d.mts +0 -267
  20. package/dist/index.mjs +0 -472
  21. package/dist/index.mjs.map +0 -1
  22. package/dist/launchDarkly/index.d.mts +0 -12
  23. package/dist/launchDarkly/index.mjs +0 -107
  24. package/dist/launchDarkly/index.mjs.map +0 -1
  25. package/sanity.json +0 -8
  26. package/src/components/Array.tsx +0 -68
  27. package/src/components/ExperimentContext.tsx +0 -65
  28. package/src/components/ExperimentField.tsx +0 -138
  29. package/src/components/ExperimentInput.tsx +0 -75
  30. package/src/components/ExperimentItem.tsx +0 -10
  31. package/src/components/Select.tsx +0 -43
  32. package/src/components/VariantInput.tsx +0 -19
  33. package/src/components/VariantPreview.tsx +0 -75
  34. package/src/fieldExperiments.tsx +0 -266
  35. package/src/growthbook/Components/GrowthbookContext.tsx +0 -38
  36. package/src/growthbook/Components/Secrets.tsx +0 -47
  37. package/src/growthbook/index.ts +0 -54
  38. package/src/growthbook/types.ts +0 -15
  39. package/src/growthbook/utils.ts +0 -94
  40. package/src/index.ts +0 -3
  41. package/src/launchDarkly/components/LaunchDarklyContext.tsx +0 -36
  42. package/src/launchDarkly/components/Secrets.tsx +0 -46
  43. package/src/launchDarkly/index.ts +0 -52
  44. package/src/launchDarkly/types.ts +0 -193
  45. package/src/launchDarkly/utils.ts +0 -54
  46. package/src/types.ts +0 -245
  47. package/src/utils/flattenSchemaType.ts +0 -47
  48. package/v2-incompatible.js +0 -11
@@ -1,12 +0,0 @@
1
- import {FieldDefinition} from 'sanity'
2
- import {Plugin as Plugin_2} from 'sanity'
3
-
4
- export declare const fieldLevelExperiments: Plugin_2<LaunchDarklyFieldLevelConfig>
5
-
6
- declare type LaunchDarklyFieldLevelConfig = {
7
- fields: (string | FieldDefinition)[]
8
- projectKey: string
9
- tags?: string[]
10
- }
11
-
12
- export {}
@@ -1,107 +0,0 @@
1
- import { definePlugin, isObjectInputProps } from "sanity";
2
- import { fieldLevelExperiments as fieldLevelExperiments$1, flattenSchemaType } from "../index.mjs";
3
- import { jsxs, Fragment, jsx } from "react/jsx-runtime";
4
- import { useState, useEffect, createContext, useMemo, useContext } from "react";
5
- import { useSecrets, SettingsView } from "@sanity/studio-secrets";
6
- const namespace = "launchdarkly", pluginConfigKeys = [
7
- {
8
- key: "apiKey",
9
- title: "Your secret API key"
10
- }
11
- ], Secrets = (props) => {
12
- const { secrets, loading } = useSecrets(namespace), { setSecret } = useLaunchDarklyContext(), [showSettings, setShowSettings] = useState(!1);
13
- return useEffect(() => {
14
- if (!loading)
15
- return !secrets && !loading ? (setSecret(void 0), setShowSettings(!0)) : (setSecret(secrets.apiKey), setShowSettings(!1));
16
- }, [secrets, loading, setSecret]), showSettings ? /* @__PURE__ */ jsxs(Fragment, { children: [
17
- /* @__PURE__ */ jsx(
18
- SettingsView,
19
- {
20
- title: `${namespace} api key`,
21
- namespace,
22
- keys: pluginConfigKeys,
23
- onClose: () => {
24
- setShowSettings(!1);
25
- }
26
- }
27
- ),
28
- props.renderDefault(props)
29
- ] }) : props.renderDefault(props);
30
- }, LAUNCHDARKLY_CONFIG_DEFAULT = {}, LaunchDarklyContext = createContext({
31
- setSecret: () => {
32
- },
33
- secret: void 0
34
- });
35
- function useLaunchDarklyContext() {
36
- return useContext(LaunchDarklyContext);
37
- }
38
- function LaunchDarklyProvider(props) {
39
- const { launchDarklyFieldPluginConfig } = props, [secret, setSecret] = useState(), context = useMemo(
40
- () => ({ ...launchDarklyFieldPluginConfig, secret, setSecret }),
41
- [launchDarklyFieldPluginConfig, secret, setSecret]
42
- );
43
- return /* @__PURE__ */ jsx(LaunchDarklyContext.Provider, { value: context, children: /* @__PURE__ */ jsx(Secrets, { ...props }) });
44
- }
45
- const getExperiments = async ({
46
- client,
47
- projectKey,
48
- tags
49
- }) => {
50
- const secret = await client.fetch("*[_id == 'secrets.launchdarkly'][0].secrets.apiKey");
51
- if (!secret) return [];
52
- const url = new URL(`https://app.launchdarkly.com/api/v2/flags/${projectKey}`);
53
- tags && url.searchParams.set("filter", `tags:${tags.join("+")}`);
54
- const featureExperiments = [];
55
- let hasMore = !0;
56
- const offset = 0, limit = 10;
57
- for (; hasMore; ) {
58
- url.searchParams.set("offset", offset.toString()), url.searchParams.set("limit", limit.toString());
59
- const responseFlags = await fetch(url, {
60
- headers: {
61
- Authorization: secret
62
- }
63
- }), { items } = await responseFlags.json(), experiments = items.map((flag) => ({
64
- id: flag.key,
65
- label: flag.name,
66
- variants: flag.variations.map((variation) => ({
67
- id: variation.value.toString(),
68
- label: variation.name ?? variation.value.toString()
69
- }))
70
- }));
71
- featureExperiments.push(...experiments), items.length !== limit && (hasMore = !1);
72
- }
73
- return featureExperiments;
74
- }, fieldLevelExperiments = definePlugin((config) => {
75
- const pluginConfig = { ...LAUNCHDARKLY_CONFIG_DEFAULT, ...config }, { fields, projectKey, tags } = pluginConfig;
76
- return {
77
- name: "sanity-growthbook-personalistaion-plugin-field-level-experiments",
78
- plugins: [
79
- fieldLevelExperiments$1({
80
- fields,
81
- experiments: (client) => getExperiments({ client, projectKey, tags }),
82
- experimentNameOverride: "flag"
83
- })
84
- ],
85
- form: {
86
- components: {
87
- input: (props) => {
88
- if (!(props.id === "root" && isObjectInputProps(props)) || !flattenSchemaType(props.schemaType).map(
89
- (field) => field.type.name
90
- ).some((name) => name.startsWith("flag")))
91
- return props.renderDefault(props);
92
- const providerProps = {
93
- ...props,
94
- launchDarklyFieldPluginConfig: {
95
- ...pluginConfig
96
- }
97
- };
98
- return LaunchDarklyProvider(providerProps);
99
- }
100
- }
101
- }
102
- };
103
- });
104
- export {
105
- fieldLevelExperiments
106
- };
107
- //# sourceMappingURL=index.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.mjs","sources":["../../src/launchDarkly/components/Secrets.tsx","../../src/launchDarkly/components/LaunchDarklyContext.tsx","../../src/launchDarkly/utils.ts","../../src/launchDarkly/index.ts"],"sourcesContent":["import {SettingsView, useSecrets} from '@sanity/studio-secrets'\nimport {useEffect, useState} from 'react'\nimport {ObjectInputProps} from 'sanity'\n\nimport {useLaunchDarklyContext} from './LaunchDarklyContext'\n\nconst namespace = 'launchdarkly'\nconst pluginConfigKeys = [\n {\n key: 'apiKey',\n title: 'Your secret API key',\n },\n]\n\nexport const Secrets = (props: ObjectInputProps) => {\n const {secrets, loading} = useSecrets(namespace) as {secrets: {apiKey: string}; loading: boolean}\n const {setSecret} = useLaunchDarklyContext()\n const [showSettings, setShowSettings] = useState<boolean>(false)\n\n useEffect(() => {\n if (loading) return undefined\n if (!secrets && !loading) {\n setSecret(undefined)\n return setShowSettings(true)\n }\n setSecret(secrets.apiKey)\n return setShowSettings(false)\n }, [secrets, loading, setSecret])\n\n if (!showSettings) {\n return props.renderDefault(props)\n }\n return (\n <>\n <SettingsView\n title={`${namespace} api key`}\n namespace={namespace}\n keys={pluginConfigKeys}\n onClose={() => {\n setShowSettings(false)\n }}\n />\n {props.renderDefault(props)}\n </>\n )\n}\n","import {createContext, useContext, useMemo, useState} from 'react'\nimport {ObjectInputProps} from 'sanity'\n\nimport {LaunchDarklyContextProps, LaunchDarklyFieldLevelConfig} from '../types'\nimport {Secrets} from './Secrets'\n\nexport const LAUNCHDARKLY_CONFIG_DEFAULT = {}\n\nexport const LaunchDarklyContext = createContext<LaunchDarklyContextProps>({\n setSecret: () => undefined,\n secret: undefined,\n})\n\nexport function useLaunchDarklyContext() {\n return useContext(LaunchDarklyContext)\n}\n\ntype LaunchDarklyProps = ObjectInputProps & {\n launchDarklyFieldPluginConfig: LaunchDarklyFieldLevelConfig\n}\n\nexport function LaunchDarklyProvider(props: LaunchDarklyProps) {\n const {launchDarklyFieldPluginConfig} = props\n const [secret, setSecret] = useState<string | undefined>()\n\n const context = useMemo(\n () => ({...launchDarklyFieldPluginConfig, secret, setSecret}),\n [launchDarklyFieldPluginConfig, secret, setSecret],\n )\n\n return (\n <LaunchDarklyContext.Provider value={context}>\n <Secrets {...props} />\n </LaunchDarklyContext.Provider>\n )\n}\n","import {SanityClient} from 'sanity'\n\nimport {ExperimentType} from '../types'\nimport {LaunchDarklyFieldLevelConfig, LaunchDarklyFlagItem} from './types'\n\nexport const getExperiments = async ({\n client,\n projectKey,\n tags,\n}: Omit<LaunchDarklyFieldLevelConfig, 'fields'> & {client: SanityClient}): Promise<\n ExperimentType[]\n> => {\n const query = `*[_id == 'secrets.launchdarkly'][0].secrets.apiKey`\n\n const secret = await client.fetch(query) // secret is stored in the content lake using @sanity/studio-secrets\n if (!secret) return []\n\n const url = new URL(`https://app.launchdarkly.com/api/v2/flags/${projectKey}`)\n\n if (tags) {\n url.searchParams.set('filter', `tags:${tags.join('+')}`)\n }\n\n const featureExperiments: ExperimentType[] = []\n let hasMore = true\n const offset = 0\n const limit = 10\n\n while (hasMore) {\n url.searchParams.set('offset', offset.toString())\n url.searchParams.set('limit', limit.toString())\n const responseFlags = await fetch(url, {\n headers: {\n Authorization: secret,\n },\n })\n\n const {items} = await responseFlags.json()\n const experiments = items.map((flag: LaunchDarklyFlagItem) => ({\n id: flag.key,\n label: flag.name,\n variants: flag.variations.map((variation) => ({\n id: variation.value.toString(),\n label: variation.name ?? variation.value.toString(),\n })),\n }))\n featureExperiments.push(...experiments)\n if (items.length !== limit) {\n hasMore = false\n }\n }\n\n return featureExperiments\n}\n","import {definePlugin, isObjectInputProps} from 'sanity'\n\nimport {fieldLevelExperiments as baseFieldLevelExperiments} from '../fieldExperiments'\nimport {flattenSchemaType} from '../utils/flattenSchemaType'\nimport {LAUNCHDARKLY_CONFIG_DEFAULT, LaunchDarklyProvider} from './components/LaunchDarklyContext'\nimport {LaunchDarklyFieldLevelConfig} from './types'\nimport {getExperiments} from './utils'\n\nexport const fieldLevelExperiments = definePlugin<LaunchDarklyFieldLevelConfig>((config) => {\n const pluginConfig = {...LAUNCHDARKLY_CONFIG_DEFAULT, ...config}\n const {fields, projectKey, tags} = pluginConfig\n return {\n name: 'sanity-growthbook-personalistaion-plugin-field-level-experiments',\n plugins: [\n baseFieldLevelExperiments({\n fields,\n experiments: (client) => getExperiments({client, projectKey, tags}),\n experimentNameOverride: 'flag',\n }),\n ],\n\n form: {\n components: {\n input: (props) => {\n const isRootInput = props.id === 'root' && isObjectInputProps(props)\n\n if (!isRootInput) {\n return props.renderDefault(props)\n }\n\n const flatFieldTypeNames = flattenSchemaType(props.schemaType).map(\n (field) => field.type.name,\n )\n\n const hasExperiment = flatFieldTypeNames.some((name) => name.startsWith('flag'))\n\n if (!hasExperiment) {\n return props.renderDefault(props)\n }\n\n const providerProps = {\n ...props,\n launchDarklyFieldPluginConfig: {\n ...pluginConfig,\n },\n }\n return LaunchDarklyProvider(providerProps)\n },\n },\n },\n }\n})\n"],"names":["baseFieldLevelExperiments"],"mappings":"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"}
package/sanity.json DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "parts": [
3
- {
4
- "implements": "part:@sanity/base/sanity-root",
5
- "path": "./v2-incompatible.js"
6
- }
7
- ]
8
- }
@@ -1,68 +0,0 @@
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 {onItemAppend, variantName, variantId, experimentId} = props
12
- const experimentValue = useFormValue([...fieldPath, experimentId])
13
-
14
- const {experiments} = useExperimentContext()
15
-
16
- const handleClick = useCallback(
17
- async (variant: VariantType) => {
18
- const item = {
19
- _key: uuid(),
20
- [variantId]: variant.id,
21
- [experimentId]: experimentValue,
22
- _type: variantName,
23
- }
24
-
25
- // Patch the document
26
- onItemAppend(item)
27
- },
28
- [variantId, experimentId, experimentValue, variantName, onItemAppend],
29
- )
30
-
31
- const filteredVariants =
32
- experiments.find((option) => {
33
- return option.id === experimentValue
34
- })?.variants || []
35
-
36
- type Value = {
37
- value?: unknown
38
- [key: string]: string | unknown
39
- variantId: string
40
- _key: string
41
- _type: string
42
- }
43
-
44
- // there is probably some better was of getting the type of this?
45
- const values = (props.value as Value[]) || []
46
-
47
- const usedVariants = values?.map((variant) => variant[variantId])
48
-
49
- return (
50
- <Stack space={3}>
51
- {props.renderDefault({...props, arrayFunctions: () => null})}
52
-
53
- <Inline space={1}>
54
- {filteredVariants.map((variant) => {
55
- return (
56
- <Button
57
- key={`${experimentValue}-${variant.id}`}
58
- text={`Add ${variant.label}`}
59
- mode="ghost"
60
- disabled={usedVariants?.includes(variant.id)}
61
- onClick={() => handleClick(variant)}
62
- />
63
- )
64
- })}
65
- </Inline>
66
- </Stack>
67
- )
68
- }
@@ -1,65 +0,0 @@
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
- experimentNameOverride: 'experiment',
15
- variantNameOverride: 'variant',
16
- variantId: 'variantId',
17
- variantArrayName: 'variants',
18
- experimentId: 'experimentId',
19
- }
20
-
21
- export const ExperimentContext = createContext<ExperimentContextProps>({
22
- ...CONFIG_DEFAULT,
23
- experiments: [],
24
- })
25
-
26
- export function useExperimentContext() {
27
- return useContext(ExperimentContext)
28
- }
29
-
30
- type ExperimentProps = ObjectInputProps & {
31
- experimentFieldPluginConfig: Required<FieldPluginConfig>
32
- }
33
-
34
- export function ExperimentProvider(props: ExperimentProps) {
35
- const {experimentFieldPluginConfig} = props
36
-
37
- const client = useClient({apiVersion: experimentFieldPluginConfig.apiVersion})
38
- const workspace = useWorkspace()
39
-
40
- // Fetch or return experiments
41
- const experiments = Array.isArray(experimentFieldPluginConfig.experiments)
42
- ? experimentFieldPluginConfig.experiments
43
- : suspend(
44
- // eslint-disable-next-line require-await
45
- async () => {
46
- if (typeof experimentFieldPluginConfig.experiments === 'function') {
47
- return experimentFieldPluginConfig.experiments(client)
48
- }
49
- return experimentFieldPluginConfig.experiments
50
- },
51
- [workspace],
52
- {equal},
53
- )
54
-
55
- const context = useMemo(
56
- () => ({...experimentFieldPluginConfig, experiments}),
57
- [experimentFieldPluginConfig, experiments],
58
- )
59
-
60
- return (
61
- <ExperimentContext.Provider value={context}>
62
- {props.renderDefault(props)}
63
- </ExperimentContext.Provider>
64
- )
65
- }
@@ -1,138 +0,0 @@
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 &
18
- PatchStuff & {experimentNameOverride: string; experimentId: string; active: boolean},
19
- ): DocumentFieldActionItem => {
20
- const {onChange, active, experimentNameOverride} = props
21
-
22
- const handleAddAction = useCallback(() => {
23
- onChange([set(!active, ['active'])])
24
- }, [onChange, active])
25
-
26
- return {
27
- title: `Add ${experimentNameOverride}`,
28
- type: 'action',
29
- icon: GiSoapExperiment,
30
- onAction: handleAddAction,
31
- renderAsButton: true,
32
- }
33
- }
34
-
35
- const useRemoveExperimentAction = (
36
- props: DocumentFieldActionProps &
37
- PatchStuff & {
38
- experimentNameOverride: string
39
- experimentId: string
40
- active: boolean
41
- variantNameOverride: string
42
- },
43
- ): DocumentFieldActionItem => {
44
- const {onChange, active, experimentId, experimentNameOverride, variantNameOverride} = props
45
- const handleClearAction = useCallback(() => {
46
- const activeId = ['active']
47
- const experiment = [experimentId]
48
- const variants = [`${variantNameOverride}s`]
49
- onChange([set(!active, activeId), unset(experiment), unset(variants)])
50
- }, [onChange, active, experimentId, variantNameOverride])
51
-
52
- return {
53
- title: `Remove ${experimentNameOverride}`,
54
- type: 'action',
55
- icon: CloseIcon,
56
- onAction: handleClearAction,
57
- renderAsButton: true,
58
- }
59
- }
60
-
61
- const createActions = ({
62
- onChange,
63
- inputId,
64
- active,
65
- experimentNameOverride,
66
- experimentId,
67
- variantNameOverride,
68
- }: PatchStuff & {
69
- active?: boolean
70
- experimentNameOverride: string
71
- experimentId: string
72
- variantNameOverride: string
73
- }) => {
74
- const removeAction = defineDocumentFieldAction({
75
- name: `Remove ${experimentNameOverride}`,
76
- useAction: (props) =>
77
- useRemoveExperimentAction({
78
- ...props,
79
- active: true,
80
- onChange,
81
- inputId,
82
- experimentNameOverride,
83
- experimentId,
84
- variantNameOverride,
85
- }),
86
- })
87
- const addAction = defineDocumentFieldAction({
88
- name: `Add ${experimentNameOverride}`,
89
- useAction: (props) =>
90
- useAddExperimentAction({
91
- ...props,
92
- active: false,
93
- onChange,
94
- inputId,
95
- experimentNameOverride,
96
- experimentId,
97
- }),
98
- })
99
- return active ? removeAction : addAction
100
- }
101
-
102
- export const ExperimentField = (
103
- props: ObjectFieldProps & {
104
- experimentNameOverride: string
105
- experimentId: string
106
- variantNameOverride: string
107
- },
108
- ) => {
109
- const {onChange} = props.inputProps
110
- const {inputId, experimentNameOverride, experimentId, variantNameOverride} = props
111
- const active = props.value?.active as boolean | undefined
112
-
113
- const actionProps = useMemo(
114
- () => ({
115
- onChange,
116
- inputId,
117
- active,
118
- experimentNameOverride,
119
- experimentId,
120
- variantNameOverride,
121
- }),
122
- [onChange, inputId, active, experimentNameOverride, experimentId, variantNameOverride],
123
- )
124
-
125
- const memoizedActions = useMemo(() => {
126
- const oldActions = props.actions || []
127
- return [createActions(actionProps), ...oldActions]
128
- }, [actionProps, props.actions])
129
-
130
- const withActionProps = useMemo(
131
- () => ({
132
- ...props,
133
- actions: memoizedActions,
134
- }),
135
- [props, memoizedActions],
136
- )
137
- return props.renderDefault(withActionProps)
138
- }
@@ -1,75 +0,0 @@
1
- import {Card, Text} from '@sanity/ui'
2
- import {FormEvent, useCallback, useMemo} from 'react'
3
- import {
4
- FormPatch,
5
- getPublishedId,
6
- PatchEvent,
7
- set,
8
- StringInputProps,
9
- unset,
10
- useDocumentOperation,
11
- useFormValue,
12
- } from 'sanity'
13
-
14
- import {ExperimentType} from '..'
15
- import {useExperimentContext} from './ExperimentContext'
16
- import {Select} from './Select'
17
-
18
- export type SelectOption = {title: string; value: string}
19
- const formatlistOptions = (experiments: ExperimentType[]): SelectOption[] =>
20
- experiments.map((experiment) => ({
21
- title: experiment.label,
22
- value: experiment.id,
23
- }))
24
-
25
- export const ExperimentInput = (
26
- props: StringInputProps & {variantNameOverride: string; experimentNameOverride: string},
27
- ) => {
28
- const {experiments} = useExperimentContext()
29
-
30
- const id = useFormValue(['_id']) as string
31
- const additionalChangePath = useMemo(
32
- () => [...props.path.slice(0, -1), `${props.variantNameOverride}s`],
33
- [props.variantNameOverride, props.path],
34
- )
35
- const subValues = useFormValue(additionalChangePath)
36
-
37
- const {patch} = useDocumentOperation(getPublishedId(id), props.schemaType.name)
38
-
39
- const handleChange = useCallback(
40
- (
41
- event: FormEvent<Element>,
42
- onChange: (patchchange: FormPatch | FormPatch[] | PatchEvent) => void,
43
- ) => {
44
- const target = event.currentTarget as HTMLSelectElement
45
- const inputValue = target.value
46
-
47
- if (inputValue) {
48
- onChange(set(inputValue))
49
- } else {
50
- onChange(unset())
51
- }
52
-
53
- if (subValues) {
54
- const patchEvent = {
55
- unset: [additionalChangePath.join('.')],
56
- }
57
- patch.execute([patchEvent])
58
- }
59
- },
60
- [patch, subValues, additionalChangePath],
61
- )
62
-
63
- if (!experiments.length)
64
- return (
65
- <Card padding={[3, 3, 4]} radius={2} shadow={1} tone="caution">
66
- <Text align="center" size={[2, 2, 3]}>
67
- There are no defined {props.experimentNameOverride}s
68
- </Text>
69
- </Card>
70
- )
71
-
72
- return (
73
- <Select {...props} listOptions={formatlistOptions(experiments)} handleChange={handleChange} />
74
- )
75
- }
@@ -1,10 +0,0 @@
1
- import {ObjectItem, ObjectItemProps, set} from 'sanity'
2
-
3
- export const ExperimentItem = (props: ObjectItemProps) => {
4
- const {active} = props.value as ObjectItem & {active: boolean}
5
- if (!active) {
6
- props.inputProps.onChange(set(true, ['active']))
7
- }
8
-
9
- return props.renderDefault(props)
10
- }
@@ -1,43 +0,0 @@
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
- }
@@ -1,19 +0,0 @@
1
- import {Button, Inline, Stack} from '@sanity/ui'
2
- import {ObjectInputProps, set, useFormValue} from 'sanity'
3
-
4
- export const VariantInput = (props: ObjectInputProps) => {
5
- const experimentPath = props.path.slice(0, -2)
6
- const defaultValue = useFormValue([...experimentPath, '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,75 +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 {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
- }