Paul Welsh
Raised in Cumbria. Made in Manchester.
Paul is located at Manchester, UK
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.