Andy Fitzgerald
Information Architect & Content Strategist
Custom input component with a DIY webhook for connecting to APIs beyond publish, update, and delete events.
import React from 'react';
import { FormField } from '@sanity/base/components';
import { TextInput, Stack, Text, Flex, Button, Box, useToast } from '@sanity/ui';
import PatchEvent, {set, unset} from '@sanity/form-builder/PatchEvent';
import { useId } from "@reach/auto-id";
const LDHyperlink = React.forwardRef((props, ref) => {
const {
type, // Schema information
value, // Current field value
readOnly, // Boolean if field is not editable
placeholder, // Placeholder text from the schema
markers, // Markers including validation rules
presence, // Presence information for collaborative avatars
compareValue, // Value to check for "edited" functionality
onFocus, // Method to handle focus state
onBlur, // Method to handle blur state
onChange, // Method to handle patch events
parent, // Parent document data
} = props
// Webhook payload. If you need to specify particular keys—-such as `event-type` for GitHub Actions workflows--those can be added here.
const webHookData = {
link: value,
resourceId: parent._id
};
const inputId = useId();
const toast = useToast();
const apiBaseURL = process.env.SANITY_STUDIO_DEV_API_URL || 'https://api.uxmethods.org';
const webHook = () =>
fetch(
`${apiBaseURL}/ld`, // URL to which to POST the webhook
{
method: 'POST',
headers: {
'User-Agent': 'UXMethods'
},
body: JSON.stringify(webHookData)
}
).then(response => {
if (response.ok) {
console.log("Webhook successfully received.");
console.log(webHookData);
toast.push({
status: 'info',
title: 'Linked Data received',
closable: true
});
} else {
return Promise.reject(response);
}
}).catch(err => {
console.warn('There was a problem', err);
toast.push({
status: 'error',
title: 'There was a problem:',
description: 'The Linked Data request failed. Check the console for error messages.',
closable: true
});
});
// Creates a change handler for patching data
const handleChange = React.useCallback(
(event) => {
const inputValue = event.currentTarget.value // get current value
// if the value exists, set the data, if not, unset the data
onChange(PatchEvent.from(inputValue ? set(inputValue) : unset()))
},
[onChange]
)
const isURL = (str) => {
const pattern = new RegExp('^(https?:\\/\\/)?'+ // protocol
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // domain name
'((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR ip (v4) address
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // port and path
'(\\?[;&a-z\\d%_.~+=-]*)?'+ // query string
'(\\#[-a-z\\d_]*)?$','i'); // fragment locator
return !!pattern.test(str);
}
return (
<Stack space={1}>
<FormField
description={type.description}
title={type.title}
__unstable_markers={markers} // Handles all markers including validation
__unstable_presence={presence} // Handles presence avatars
compareValue={compareValue} // Handles "edited" status
inputId={inputId} // Allows the label to connect to the input field
>
<Flex>
<Box flex={[1]}>
<TextInput
id={inputId} // A unique ID for this input
onChange={handleChange} // A function to call when the input value changes
value={value} // Current field value
readOnly={readOnly} // If "readOnly" is defined make this field read only
placeholder={placeholder} // If placeholder is defined, display placeholder text
onFocus={onFocus} // Handles focus events
onBlur={onBlur} // Handles blur events
ref={ref}
/>
</Box>
<Box marginLeft={[1]}>
<Button
fontSize={[2]}
padding={[3]}
text="Get Linked Data"
mode="ghost"
disabled={!isURL(value)} // Button disables until a valid URL is entered
tone="default"
justify="flex-end"
onClick={() => {
toast.push({
status: 'info',
title: 'Linked Data request sent.',
closable: true
});
webHook();
}}
/>
</Box>
</Flex>
</FormField>
</Stack>
)
})
export default LDHyperlink
Sanity GROQ powered webhooks are awesome, but sometimes you may want to trigger a microservice outside of a publish, update, or delete event. This custom input component adds ad-hoc webhook functionality to trigger a webhook ready API with field data as part of its payload. In this case, I'm using a custom API running on a subdomain to crawl and fetch linked data from a URL, in order to more easily populate fields for a shareable resource.
The end result of this component (and its connected service) is similar to Espen Hovlandsdal's URL Metadata Input component, but I wanted a bit more control over which linked data I fetched and how, with the idea being that this can later fit into a larger Linked Data pipeline. If you're just looking to populate metadata, Espen's plugin may be simpler.
Security caveats: Since this webhook is entirely on the front end (and in the repo), be sure not to include any API keys or passwords. I initially set this up with a GitHub Action, but wasn't able to secure the access token in a way I was comfortable with, so I put my API on a subdomain where I could control CORS access. If you host on Netlify, you may have access to "secrets" through an environment variable that would allow you to connect to services more easily.
Information Architect & Content Strategist