@sanity/personalization-plugin 2.3.0-launch-darkly.1 → 2.3.0-launch-darkly.2

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,12 @@
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 {}
@@ -0,0 +1,12 @@
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 {}
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: !0 });
3
+ var sanity = require("sanity"), index = require("../index.js"), jsxRuntime = require("react/jsx-runtime"), react = require("react"), studioSecrets = require("@sanity/studio-secrets");
4
+ const namespace = "launchdarkly", pluginConfigKeys = [
5
+ {
6
+ key: "apiKey",
7
+ title: "Your secret API key"
8
+ }
9
+ ], Secrets = (props) => {
10
+ const { secrets, loading } = studioSecrets.useSecrets(namespace), { setSecret } = useLaunchDarklyContext(), [showSettings, setShowSettings] = react.useState(!1);
11
+ return react.useEffect(() => {
12
+ if (!loading)
13
+ return !secrets && !loading ? (setSecret(void 0), setShowSettings(!0)) : (setSecret(secrets.apiKey), setShowSettings(!1));
14
+ }, [secrets, loading, setSecret]), showSettings ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
15
+ /* @__PURE__ */ jsxRuntime.jsx(
16
+ studioSecrets.SettingsView,
17
+ {
18
+ title: `${namespace} api key`,
19
+ namespace,
20
+ keys: pluginConfigKeys,
21
+ onClose: () => {
22
+ setShowSettings(!1);
23
+ }
24
+ }
25
+ ),
26
+ props.renderDefault(props)
27
+ ] }) : props.renderDefault(props);
28
+ }, LAUNCHDARKLY_CONFIG_DEFAULT = {}, LaunchDarklyContext = react.createContext({
29
+ setSecret: () => {
30
+ },
31
+ secret: void 0
32
+ });
33
+ function useLaunchDarklyContext() {
34
+ return react.useContext(LaunchDarklyContext);
35
+ }
36
+ function LaunchDarklyProvider(props) {
37
+ const { launchDarklyFieldPluginConfig } = props, [secret, setSecret] = react.useState(), context = react.useMemo(
38
+ () => ({ ...launchDarklyFieldPluginConfig, secret, setSecret }),
39
+ [launchDarklyFieldPluginConfig, secret, setSecret]
40
+ );
41
+ return /* @__PURE__ */ jsxRuntime.jsx(LaunchDarklyContext.Provider, { value: context, children: /* @__PURE__ */ jsxRuntime.jsx(Secrets, { ...props }) });
42
+ }
43
+ const getExperiments = async ({
44
+ client,
45
+ projectKey,
46
+ tags
47
+ }) => {
48
+ const secret = await client.fetch("*[_id == 'secrets.launchdarkly'][0].secrets.apiKey");
49
+ if (!secret) return [];
50
+ const url = new URL(`https://app.launchdarkly.com/api/v2/flags/${projectKey}`);
51
+ tags && url.searchParams.set("filter", `tags:${tags.join("+")}`);
52
+ const featureExperiments = [];
53
+ let hasMore = !0;
54
+ const offset = 0, limit = 10;
55
+ for (; hasMore; ) {
56
+ url.searchParams.set("offset", offset.toString()), url.searchParams.set("limit", limit.toString());
57
+ const responseFlags = await fetch(url, {
58
+ headers: {
59
+ Authorization: secret
60
+ }
61
+ }), { items } = await responseFlags.json(), experiments = items.map((flag) => ({
62
+ id: flag.key,
63
+ label: flag.name,
64
+ variants: flag.variations.map((variation) => ({
65
+ id: variation.value,
66
+ label: variation.name ?? variation.value
67
+ }))
68
+ }));
69
+ featureExperiments.push(...experiments), items.length !== limit && (hasMore = !1);
70
+ }
71
+ return featureExperiments;
72
+ }, fieldLevelExperiments = sanity.definePlugin((config) => {
73
+ const pluginConfig = { ...LAUNCHDARKLY_CONFIG_DEFAULT, ...config }, { fields, projectKey, tags } = pluginConfig;
74
+ return {
75
+ name: "sanity-growthbook-personalistaion-plugin-field-level-experiments",
76
+ plugins: [
77
+ index.fieldLevelExperiments({
78
+ fields,
79
+ experiments: (client) => getExperiments({ client, projectKey, tags }),
80
+ experimentNameOverride: "flag"
81
+ })
82
+ ],
83
+ form: {
84
+ components: {
85
+ input: (props) => {
86
+ if (!(props.id === "root" && sanity.isObjectInputProps(props)) || !index.flattenSchemaType(props.schemaType).map(
87
+ (field) => field.type.name
88
+ ).some((name) => name.startsWith("flag")))
89
+ return props.renderDefault(props);
90
+ const providerProps = {
91
+ ...props,
92
+ launchDarklyFieldPluginConfig: {
93
+ ...pluginConfig
94
+ }
95
+ };
96
+ return LaunchDarklyProvider(providerProps);
97
+ }
98
+ }
99
+ }
100
+ };
101
+ });
102
+ exports.fieldLevelExperiments = fieldLevelExperiments;
103
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","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,\n label: variation.name ?? variation.value,\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":["useSecrets","useState","useEffect","jsxs","Fragment","jsx","SettingsView","createContext","useContext","useMemo","definePlugin","baseFieldLevelExperiments","isObjectInputProps","flattenSchemaType"],"mappings":"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"}
@@ -0,0 +1,107 @@
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,
68
+ label: variation.name ?? variation.value
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
@@ -0,0 +1 @@
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,\n label: variation.name ?? variation.value,\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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/personalization-plugin",
3
- "version": "2.3.0-launch-darkly.1",
3
+ "version": "2.3.0-launch-darkly.2",
4
4
  "description": "Plugin to help with personalization, a/b testing when using Sanity",
5
5
  "keywords": [
6
6
  "sanity",
@@ -24,6 +24,11 @@
24
24
  "import": "./dist/index.mjs",
25
25
  "default": "./dist/index.js"
26
26
  },
27
+ "./launchDarkly": {
28
+ "source": "./src/launchDarkly/index.ts",
29
+ "import": "./dist/launchDarkly/index.mjs",
30
+ "default": "./dist/launchDarkly/index.js"
31
+ },
27
32
  "./package.json": "./package.json"
28
33
  },
29
34
  "main": "./dist/index.js",
@@ -1,5 +1,5 @@
1
1
  import equal from 'fast-deep-equal'
2
- import {createContext, useContext, useMemo, useState} from 'react'
2
+ import {createContext, useContext, useMemo} from 'react'
3
3
  import {type ObjectInputProps, useClient, useWorkspace} from 'sanity'
4
4
  import {suspend} from 'suspend-react'
5
5
 
@@ -21,8 +21,6 @@ export const CONFIG_DEFAULT = {
21
21
  export const ExperimentContext = createContext<ExperimentContextProps>({
22
22
  ...CONFIG_DEFAULT,
23
23
  experiments: [],
24
- setSecret: () => undefined,
25
- secret: undefined,
26
24
  })
27
25
 
28
26
  export function useExperimentContext() {
@@ -35,7 +33,6 @@ type ExperimentProps = ObjectInputProps & {
35
33
 
36
34
  export function ExperimentProvider(props: ExperimentProps) {
37
35
  const {experimentFieldPluginConfig} = props
38
- const [secret, setSecret] = useState<string | undefined>()
39
36
 
40
37
  const client = useClient({apiVersion: experimentFieldPluginConfig.apiVersion})
41
38
  const workspace = useWorkspace()
@@ -51,13 +48,13 @@ export function ExperimentProvider(props: ExperimentProps) {
51
48
  }
52
49
  return experimentFieldPluginConfig.experiments
53
50
  },
54
- [workspace, secret],
51
+ [workspace],
55
52
  {equal},
56
53
  )
57
54
 
58
55
  const context = useMemo(
59
- () => ({...experimentFieldPluginConfig, experiments, secret, setSecret}),
60
- [experimentFieldPluginConfig, experiments, secret, setSecret],
56
+ () => ({...experimentFieldPluginConfig, experiments}),
57
+ [experimentFieldPluginConfig, experiments],
61
58
  )
62
59
 
63
60
  return (
@@ -1,4 +1,5 @@
1
1
  import {CloseIcon} from '@sanity/icons'
2
+ import {useCallback, useMemo} from 'react'
2
3
  import {GiSoapExperiment} from 'react-icons/gi'
3
4
  import {
4
5
  defineDocumentFieldAction,
@@ -18,9 +19,9 @@ const useAddExperimentAction = (
18
19
  ): DocumentFieldActionItem => {
19
20
  const {onChange, active, experimentNameOverride} = props
20
21
 
21
- const handleAddAction = () => {
22
+ const handleAddAction = useCallback(() => {
22
23
  onChange([set(!active, ['active'])])
23
- }
24
+ }, [onChange, active])
24
25
 
25
26
  return {
26
27
  title: `Add ${experimentNameOverride}`,
@@ -33,23 +34,22 @@ const useAddExperimentAction = (
33
34
 
34
35
  const useRemoveExperimentAction = (
35
36
  props: DocumentFieldActionProps &
36
- PatchStuff & {experimentNameOverride: string; experimentId: string; active: boolean},
37
+ PatchStuff & {
38
+ experimentNameOverride: string
39
+ experimentId: string
40
+ active: boolean
41
+ variantNameOverride: string
42
+ },
37
43
  ): DocumentFieldActionItem => {
38
- const {onChange, active, experimentId, experimentNameOverride} = props
39
- const patchActiveFalseEvent = () => {
44
+ const {onChange, active, experimentId, experimentNameOverride, variantNameOverride} = props
45
+
46
+ const handleClearAction = useCallback(() => {
40
47
  const activeId = ['active']
41
- return set(!active, activeId)
42
- }
43
- const patchClearEvent = () => {
44
48
  const experiment = [experimentId]
45
- const variants = [experimentNameOverride]
46
- return [unset(experiment), unset(variants)]
47
- }
48
- const handleClearAction = () => {
49
- const clearEvents = patchClearEvent()
50
- const activeEvent = patchActiveFalseEvent()
51
- onChange([activeEvent, ...clearEvents])
52
- }
49
+ const variants = [`${variantNameOverride}s`]
50
+ onChange([set(!active, activeId), unset(experiment), unset(variants)])
51
+ }, [onChange, active, experimentId, variantNameOverride])
52
+
53
53
  return {
54
54
  title: `Remove ${experimentNameOverride}`,
55
55
  type: 'action',
@@ -59,13 +59,19 @@ const useRemoveExperimentAction = (
59
59
  }
60
60
  }
61
61
 
62
- const newActions = ({
62
+ const createActions = ({
63
63
  onChange,
64
64
  inputId,
65
65
  active,
66
66
  experimentNameOverride,
67
67
  experimentId,
68
- }: PatchStuff & {active?: boolean; experimentNameOverride: string; experimentId: string}) => {
68
+ variantNameOverride,
69
+ }: PatchStuff & {
70
+ active?: boolean
71
+ experimentNameOverride: string
72
+ experimentId: string
73
+ variantNameOverride: string
74
+ }) => {
69
75
  const removeAction = defineDocumentFieldAction({
70
76
  name: `Remove ${experimentNameOverride}`,
71
77
  useAction: (props) =>
@@ -76,6 +82,7 @@ const newActions = ({
76
82
  inputId,
77
83
  experimentNameOverride,
78
84
  experimentId,
85
+ variantNameOverride,
79
86
  }),
80
87
  })
81
88
  const addAction = defineDocumentFieldAction({
@@ -97,20 +104,39 @@ const newActions = ({
97
104
  }
98
105
 
99
106
  export const ExperimentField = (
100
- props: ObjectFieldProps & {experimentNameOverride: string; experimentId: string},
107
+ props: ObjectFieldProps & {
108
+ experimentNameOverride: string
109
+ experimentId: string
110
+ variantNameOverride: string
111
+ },
101
112
  ) => {
102
113
  const {onChange} = props.inputProps
103
- const {inputId, experimentNameOverride, experimentId} = props
114
+ const {inputId, experimentNameOverride, experimentId, variantNameOverride} = props
104
115
  const active = props.value?.active as boolean | undefined
105
116
 
106
- const oldActions = props.actions || []
117
+ const actionProps = useMemo(
118
+ () => ({
119
+ onChange,
120
+ inputId,
121
+ active,
122
+ experimentNameOverride,
123
+ experimentId,
124
+ variantNameOverride,
125
+ }),
126
+ [onChange, inputId, active, experimentNameOverride, experimentId, variantNameOverride],
127
+ )
107
128
 
108
- const withActionProps = {
109
- ...props,
110
- actions: [
111
- newActions({onChange, inputId, active, experimentNameOverride, experimentId}),
112
- ...oldActions,
113
- ],
114
- }
129
+ const memoizedActions = useMemo(() => {
130
+ const oldActions = props.actions || []
131
+ return [createActions(actionProps), ...oldActions]
132
+ }, [actionProps, props.actions])
133
+
134
+ const withActionProps = useMemo(
135
+ () => ({
136
+ ...props,
137
+ actions: memoizedActions,
138
+ }),
139
+ [props, memoizedActions],
140
+ )
115
141
  return props.renderDefault(withActionProps)
116
142
  }
@@ -28,11 +28,11 @@ export const ExperimentInput = (
28
28
 
29
29
  const id = useFormValue(['_id']) as string
30
30
  const aditionalChangePath = useMemo(
31
- () => [...props.path.slice(0, -1), props.variantNameOverride],
31
+ () => [...props.path.slice(0, -1), `${props.variantNameOverride}s`],
32
32
  [props.variantNameOverride, props.path],
33
33
  )
34
- const subValues = useFormValue(aditionalChangePath)
35
34
 
35
+ const subValues = useFormValue(aditionalChangePath)
36
36
  const {patch} = useDocumentOperation(id.replace('drafts.', ''), props.schemaType.name)
37
37
 
38
38
  const handleChange = useCallback(
@@ -44,6 +44,7 @@ const createExperimentType = ({
44
44
  {...props}
45
45
  experimentId={experimentId}
46
46
  experimentNameOverride={experimentNameOverride}
47
+ variantNameOverride={variantNameOverride}
47
48
  />
48
49
  ),
49
50
  },
package/src/index.ts CHANGED
@@ -1,4 +1,3 @@
1
1
  export * from './fieldExperiments'
2
- export * from './launchDarklyExperiments'
3
2
  export * from './types'
4
3
  export * from './utils/flattenSchemaType'
@@ -0,0 +1,36 @@
1
+ import {createContext, useContext, useMemo, useState} from 'react'
2
+ import {ObjectInputProps} from 'sanity'
3
+
4
+ import {LaunchDarklyContextProps, LaunchDarklyFieldLevelConfig} from '../types'
5
+ import {Secrets} from './Secrets'
6
+
7
+ export const LAUNCHDARKLY_CONFIG_DEFAULT = {}
8
+
9
+ export const LaunchDarklyContext = createContext<LaunchDarklyContextProps>({
10
+ setSecret: () => undefined,
11
+ secret: undefined,
12
+ })
13
+
14
+ export function useLaunchDarklyContext() {
15
+ return useContext(LaunchDarklyContext)
16
+ }
17
+
18
+ type LaunchDarklyProps = ObjectInputProps & {
19
+ launchDarklyFieldPluginConfig: LaunchDarklyFieldLevelConfig
20
+ }
21
+
22
+ export function LaunchDarklyProvider(props: LaunchDarklyProps) {
23
+ const {launchDarklyFieldPluginConfig} = props
24
+ const [secret, setSecret] = useState<string | undefined>()
25
+
26
+ const context = useMemo(
27
+ () => ({...launchDarklyFieldPluginConfig, secret, setSecret}),
28
+ [launchDarklyFieldPluginConfig, secret, setSecret],
29
+ )
30
+
31
+ return (
32
+ <LaunchDarklyContext.Provider value={context}>
33
+ <Secrets {...props} />
34
+ </LaunchDarklyContext.Provider>
35
+ )
36
+ }
@@ -2,8 +2,9 @@ import {SettingsView, useSecrets} from '@sanity/studio-secrets'
2
2
  import {useEffect, useState} from 'react'
3
3
  import {ObjectInputProps} from 'sanity'
4
4
 
5
- import {useExperimentContext} from './ExperimentContext'
5
+ import {useLaunchDarklyContext} from './LaunchDarklyContext'
6
6
 
7
+ const namespace = 'launchdarkly'
7
8
  const pluginConfigKeys = [
8
9
  {
9
10
  key: 'apiKey',
@@ -11,9 +12,9 @@ const pluginConfigKeys = [
11
12
  },
12
13
  ]
13
14
 
14
- export const Secrets = (props: ObjectInputProps, namespace: string) => {
15
+ export const Secrets = (props: ObjectInputProps) => {
15
16
  const {secrets, loading} = useSecrets(namespace) as {secrets: {apiKey: string}; loading: boolean}
16
- const {setSecret} = useExperimentContext()
17
+ const {setSecret} = useLaunchDarklyContext()
17
18
  const [showSettings, setShowSettings] = useState<boolean>(false)
18
19
 
19
20
  useEffect(() => {
@@ -0,0 +1,52 @@
1
+ import {definePlugin, isObjectInputProps} from 'sanity'
2
+
3
+ import {fieldLevelExperiments as baseFieldLevelExperiments} from '../fieldExperiments'
4
+ import {flattenSchemaType} from '../utils/flattenSchemaType'
5
+ import {LAUNCHDARKLY_CONFIG_DEFAULT, LaunchDarklyProvider} from './components/LaunchDarklyContext'
6
+ import {LaunchDarklyFieldLevelConfig} from './types'
7
+ import {getExperiments} from './utils'
8
+
9
+ export const fieldLevelExperiments = definePlugin<LaunchDarklyFieldLevelConfig>((config) => {
10
+ const pluginConfig = {...LAUNCHDARKLY_CONFIG_DEFAULT, ...config}
11
+ const {fields, projectKey, tags} = pluginConfig
12
+ return {
13
+ name: 'sanity-growthbook-personalistaion-plugin-field-level-experiments',
14
+ plugins: [
15
+ baseFieldLevelExperiments({
16
+ fields,
17
+ experiments: (client) => getExperiments({client, projectKey, tags}),
18
+ experimentNameOverride: 'flag',
19
+ }),
20
+ ],
21
+
22
+ form: {
23
+ components: {
24
+ input: (props) => {
25
+ const isRootInput = props.id === 'root' && isObjectInputProps(props)
26
+
27
+ if (!isRootInput) {
28
+ return props.renderDefault(props)
29
+ }
30
+
31
+ const flatFieldTypeNames = flattenSchemaType(props.schemaType).map(
32
+ (field) => field.type.name,
33
+ )
34
+
35
+ const hasExperiment = flatFieldTypeNames.some((name) => name.startsWith('flag'))
36
+
37
+ if (!hasExperiment) {
38
+ return props.renderDefault(props)
39
+ }
40
+
41
+ const providerProps = {
42
+ ...props,
43
+ launchDarklyFieldPluginConfig: {
44
+ ...pluginConfig,
45
+ },
46
+ }
47
+ return LaunchDarklyProvider(providerProps)
48
+ },
49
+ },
50
+ },
51
+ }
52
+ })