Paul Welsh
Raised in Cumbria. Made in Manchester.
Add the ability to make your document conditionally `readOnly` using an input component, where Sanity React hooks are available
import { ReadOnlyIcon } from '@sanity/icons';
import { Box, Card, Flex, Stack, Text } from '@sanity/ui';
import { defineQuery } from 'groq';
import { Suspense, useEffect, useState } from 'react';
import { type ObjectInputProps, useClient } from 'sanity';
type MetadataResponse = {
state: string;
};
export const makeReadOnly = <T extends object | unknown[] | null>(
value: T,
keys: string[] = [
'item',
'field',
'fields',
'members',
'of',
'type',
'array',
'items',
],
seen = new WeakSet(),
): T => {
// Early return for primitives, null, undefined, or circular references
if (value == null || typeof value !== 'object' || seen.has(value as object)) {
return value;
}
seen.add(value as object);
// Handle arrays with direct return
if (Array.isArray(value)) {
return value.map((item) => makeReadOnly(item, keys, seen)) as T;
}
// Create result object with readOnly flag
const result = Object.assign({}, value as object, { readOnly: true });
const objectKeys = Object.keys(value as object);
const keysToProcess = objectKeys.filter((key) => keys.includes(key));
// Process each key that needs to be made readonly
for (const key of keysToProcess) {
const val = (value as Record<string, unknown>)[key];
const shouldMakeReadOnly = val && typeof val === 'object';
(result as Record<string, unknown>)[key] = shouldMakeReadOnly
? makeReadOnly(val, keys, seen)
: val;
}
// Return the result as the original type
return result as T;
};
const ConditionalReadOnlyDocumentInput = (props: ObjectInputProps) => {
const client = useClient({ apiVersion: '2024-01-17' });
const [isReadOnly, setIsReadOnly] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [readOnlyProps, setReadOnlyProps] = useState<ObjectInputProps | null>(
null,
);
// In this example, we're using a query to fetch the metadata associated with the document. This would be better as a listener but this demonstrates the async functionality enough
useEffect(() => {
const fetchMetadata = async () => {
try {
setIsLoading(true);
if (!props.value?._id) {
setIsReadOnly(false);
return;
}
const query = defineQuery(`*[type == $type && documentId == $id][0] { state }`);
const params = {
id: props.value._id,
type: 'metadata',
};
const response = await client.fetch<MetadataResponse>(query, params);
setIsReadOnly(!!(response?.state === 'approved'));
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
};
fetchMetadata();
}, [props.value?._id, client]);
// Update the readOnly props when the props change
useEffect(() => {
setReadOnlyProps(makeReadOnly<ObjectInputProps>(props));
}, [props]);
if (!isReadOnly || !readOnlyProps) {
return props.renderDefault(props);
}
return (
<Suspense fallback={<div>Loading...</div>}>
{!isLoading && (
<Stack space={5}>
<Card
padding={3}
border
tone="primary"
radius={2}
>
<Flex
gap={3}
align="center"
>
<Flex
style={{ minWidth: 24 }}
align="center"
justify="center"
>
<ReadOnlyIcon
width={24}
height={24}
/>
</Flex>
<Box>
<Text size={1}>
This document cannot be edited while in an approved state.
Either publish, discard changes or move the document back into
review.
</Text>
</Box>
</Flex>
</Card>
{props.renderDefault(readOnlyProps)}
</Stack>
)}
</Suspense>
);
};
export default ConditionalReadOnlyDocumentInput;
import { defineType } from 'sanity';
// Import the component from your project
import ConditionalReadOnlyDocumentInput from '../components/ConditionalReadOnlyDocumentInput';
export const exampleSchemaType = defineType({
// ...all other schema type definitions
type: 'document',
components: {
input: ConditionalReadOnlyDocumentInput,
},
});
I had the need to disable editing of a document based on the value of another document (essentially a Draft Lock). The `readOnly` resolver on a document schema type has no access to the document store or client, so I resorted to using an input component where react hooks were available.
This could be classed as a hack BUT it solves a need that may be introduced within the core functionality in the future.
Raised in Cumbria. Made in Manchester.
Content operations
Content backend


The only platform powering content operations
By Industry


Tecovas strengthens their customer connections
Build and Share

Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag store