Skip to content

Instantly share code, notes, and snippets.

@isaac-martin
Created November 18, 2020 21:41
Show Gist options
  • Save isaac-martin/4c1be17078d148057c75d7ddaf18750a to your computer and use it in GitHub Desktop.
Save isaac-martin/4c1be17078d148057c75d7ddaf18750a to your computer and use it in GitHub Desktop.
Sanity Custom Input component.
import * as React from "react";
import { useImmer } from "use-immer";
import Select from "part:@sanity/components/selects/default";
import Fieldset from "part:@sanity/components/fieldsets/default";
import PatchEvent, { setIfMissing, set } from "part:@sanity/form-builder/patch-event";
import { FormBuilderInput } from "part:@sanity/form-builder";
import { FIELD_NAMES } from "./consts";
import Variant from "./variant";
interface State {
selectedExperiment: string;
experiements: Array<any>;
rawOptimizely: Array<any>;
variations: Array<any>;
dataState: "LOADING" | "SUCCESS" | "ERROR";
}
const getExperiments = (rawOptimizely) => {
return rawOptimizely.map((experiment) => ({
title: experiment.name,
value: experiment.key,
}));
};
const getExperimentByKey = (experiments, key) => {
return experiments.find((exp) => exp.value === key);
};
const getVariations = (rawOptimizely, experimentId) => {
if (!experimentId) return []
const { variations } = rawOptimizely.find(
(exp) => exp.key === experimentId
);
return variations.map((vari) => ({
title: vari.key,
value: vari.key,
}));
};
const fetchOptimizelyData = () => {
const OPTIMIZELY_PROJECT = process.env.SANITY_STUDIO_OPTIMIZELY_PROJECT;
const OPTIMIZELY_API_TOKEN = process.env.SANITY_STUDIO_OPTIMIZELY_KEY;
return fetch(
`https://api.optimizely.com/v2/experiments?project_id=${OPTIMIZELY_PROJECT}`,
{
method: "GET",
headers: {
Authorization: `Bearer ${OPTIMIZELY_API_TOKEN}`,
},
redirect: "follow",
}
)
.then((response) => response.json())
.catch((error) => console.error("error", error));
};
const DynamicData: React.FC<any> = React.forwardRef((props, ref) => {
const [state, setState] = useImmer<State>({
selectedExperiment: "",
experiements: [],
rawOptimizely: [],
variations: [],
dataState: "LOADING",
});
const {
value: fieldValues,
onChange,
type,
focusPath,
onBlur,
onFocus,
} = props
// console.log({ props })
React.useEffect(() => {
fetchOptimizelyData().then((res) => {
// console.log(res)
const experiments = getExperiments(res);
const selectedExpID = fieldValues ? fieldValues[FIELD_NAMES.EXPERIMENT_ID] : null;
setState((draftState) => {
draftState.rawOptimizely = res;
draftState.experiements = experiments;
draftState.selectedExperiment = getExperimentByKey(
experiments,
selectedExpID
);
draftState.variations = getVariations(res, selectedExpID);
draftState.dataState = "SUCCESS";
});
});
}, []);
// React.useEffect(() => {
// console.log('data change')
// console.log({ data })
// }, [data]);
const handleFieldChange = (field, fieldPatchEvent) => {
onChange(
fieldPatchEvent
.prefixAll(field.name)
.prepend(setIfMissing({ _type: type.name }))
);
};
const handleCustomFieldChange = (field, value, onChange) => {
console.log({ field, fieldValues, value })
onChange(PatchEvent.from(set({ ...fieldValues, [field.name]: value, _type: "experiment", _key: `isaac` + Math.random().toString(36).substring(7) })));
};
const handleExperimentIdChange = (field, value, onChange) => {
handleCustomFieldChange(field, value, onChange);
setState((draftState) => {
draftState.selectedExperiment = getExperimentByKey(
state.experiements,
value
);
draftState.variations = getVariations(state.rawOptimizely, value);
});
};
const { title, description, level, fields } = type;
// if (state.dataState === "LOADING") return <>Loading</>;
// if (state.dataState === "ERROR") return <>Error, please refresh</>;
const [data, setData] = React.useState(false)
return (
<Fieldset level={level} legend={title} description={description}>
<Select
items={state.experiements}
// onChange={handleFieldChange(fields[0], onChange)}
onChange={(evt) => handleExperimentIdChange(fields[0], evt.value, onChange)}
value={state.selectedExperiment}
focusPath={focusPath}
onFocus={() => { console.log('onFocus') && onFocus }}
onBlur={onBlur}
ref={ref}
/>
{fields
.filter((field) => field.name !== FIELD_NAMES.EXPERIMENT_ID)
.map((field, i) => {
if (field.name === FIELD_NAMES.VARIANTS) {
return (
<Variant
onChange={onChange}
variations={state.variations}
level={level}
field={field}
value={fieldValues}
focusPath={focusPath}
onFocus={onFocus}
onBlur={onBlur}
type={field.type}
// ref={ref}
/>
);
}
if (field.name !== FIELD_NAMES.VARIANTS) {
// Delegate to the generic FormBuilderInput.
// It will resolve and insert the actual input component specified in the schema
// Currently this isnt necessary but its a nice to ahve if someone wants to add another field to the experiment objec
return (
<FormBuilderInput
level={level + 1}
key={field.name}
type={field.type}
// ref={ref}
value={fieldValues && fieldValues[field.name]}
onChange={(patchEvent) => handleFieldChange(field, patchEvent)}
path={[field.name]}
focusPath={focusPath}
onFocus={onFocus}
onBlur={onBlur}
/>
);
}
})}
</Fieldset>
);
});
export default DynamicData;
import ExperimentSelector from "../../../src/CustomInputComponents/Experiment";
import { FIELD_NAMES } from "../../../src/CustomInputComponents/Experiment/consts";
import { ObjectType } from "../../../src/types";
const experiment: ObjectType = {
name: "experiment",
type: "object",
title: "Experiment",
inputComponent: ExperimentSelector,
fields: [
{
name: FIELD_NAMES.EXPERIMENT_ID,
type: "string",
title: "Experiment ID",
},
{
name: FIELD_NAMES.VARIANTS,
type: "array",
of: [{ type: "variant" }],
validation: (Rule) => Rule.required().length(2), // This 2 will be dynamically overwritten based on the amount of options
}
],
};
const variant: ObjectType = {
name: "variant",
type: "object",
title: "Variant",
fields: [
{
name: "variantLabel",
type: "string",
title: "Variant Label",
description: "Not shown on frontend",
validation: (Rule) => Rule.required()
},
{
name: FIELD_NAMES.VARIANT_ID,
type: "string",
title: "Variant ID",
options: {
layout: 'dropdown'
},
validation: Rule => Rule.custom((field, context) => {
const options = context.type.options.list ? context.type.options.list.map(option => option.title) : null
console.log({ options, context })
if (!field || !options) return "VariantID is required - please choose from dropdown"
if (options.includes(field)) return true
return `VariantID must be selected from dropdown - current value for ${context.parent.variantLabel} is "${field}" - did you perhaps change experiment ID?`
})
},
{
type: "corePageBuilder",
name: "corePageBuilder",
description: "Variant Modules"
},
],
preview: {
select: {
title: "variantLabel",
id: FIELD_NAMES.VARIANT_ID,
},
prepare({ title, id }) {
return {
title,
subtitle: `Variant ID: ${id}`
};
},
},
};
import * as React from "react";
import { setIfMissing } from "part:@sanity/form-builder/patch-event";
import { FormBuilderInput } from "part:@sanity/form-builder";
const Variant: React.FC<any> = React.forwardRef((props, ref) => {
const { variations, focusPath, level, value, onFocus, onBlur, onChange, type, field: oldField } = props
const [field, setField] = React.useState(oldField)
const handleFieldChange = (field, fieldPatchEvent) => {
onChange(
fieldPatchEvent
.prefixAll(field.name)
.prepend(setIfMissing({ _type: type.name }))
);
};
React.useEffect(() => {
const fieldData = { ...field }
// This is so very ugly, but its better then rebuilding all of the page builder with custom input components
const validationArr = fieldData.type.validation[0]._rules
const validationLength = validationArr.findIndex(rule => rule.flag === "length")
// console.log(validationArr)
fieldData.type.of[0].fields[1].type.options.list = variations // set our dynamic variations to the pick list
validationArr[validationLength].constraint = variations.length // reset the validation count to be the amount of options we have
setField(fieldData)
}, [variations])
return (
<FormBuilderInput
level={level + 1}
key={field.name}
type={field.type}
value={value && value[field.name]}
onChange={(patchEvent) => handleFieldChange(field, patchEvent)}
path={[field.name]}
focusPath={focusPath}
onFocus={onFocus}
onBlur={onBlur}
ref={ref}
/>
);
});
export default Variant;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment