@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.
- package/LICENSE +1 -1
- package/README.md +570 -144
- package/dist/growthbook/index.d.ts +12 -15
- package/dist/growthbook/index.d.ts.map +1 -0
- package/dist/growthbook/index.js +104 -68
- package/dist/growthbook/index.js.map +1 -1
- package/dist/index.d.ts +212 -252
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +398 -301
- package/dist/index.js.map +1 -1
- package/dist/launchDarkly/index.d.ts +9 -12
- package/dist/launchDarkly/index.d.ts.map +1 -0
- package/dist/launchDarkly/index.js +74 -46
- package/dist/launchDarkly/index.js.map +1 -1
- package/package.json +35 -77
- package/dist/growthbook/index.d.mts +0 -15
- package/dist/growthbook/index.mjs +0 -124
- package/dist/growthbook/index.mjs.map +0 -1
- package/dist/index.d.mts +0 -267
- package/dist/index.mjs +0 -472
- package/dist/index.mjs.map +0 -1
- package/dist/launchDarkly/index.d.mts +0 -12
- package/dist/launchDarkly/index.mjs +0 -107
- package/dist/launchDarkly/index.mjs.map +0 -1
- package/sanity.json +0 -8
- package/src/components/Array.tsx +0 -68
- package/src/components/ExperimentContext.tsx +0 -65
- package/src/components/ExperimentField.tsx +0 -138
- package/src/components/ExperimentInput.tsx +0 -75
- package/src/components/ExperimentItem.tsx +0 -10
- package/src/components/Select.tsx +0 -43
- package/src/components/VariantInput.tsx +0 -19
- package/src/components/VariantPreview.tsx +0 -75
- package/src/fieldExperiments.tsx +0 -266
- package/src/growthbook/Components/GrowthbookContext.tsx +0 -38
- package/src/growthbook/Components/Secrets.tsx +0 -47
- package/src/growthbook/index.ts +0 -54
- package/src/growthbook/types.ts +0 -15
- package/src/growthbook/utils.ts +0 -94
- package/src/index.ts +0 -3
- package/src/launchDarkly/components/LaunchDarklyContext.tsx +0 -36
- package/src/launchDarkly/components/Secrets.tsx +0 -46
- package/src/launchDarkly/index.ts +0 -52
- package/src/launchDarkly/types.ts +0 -193
- package/src/launchDarkly/utils.ts +0 -54
- package/src/types.ts +0 -245
- package/src/utils/flattenSchemaType.ts +0 -47
- 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
package/src/components/Array.tsx
DELETED
|
@@ -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
|
-
}
|