diff --git a/README.md b/README.md index 5c27221..8a9762c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ > This combines the sanity-plugin-cloudinary AND sanity-plugin-asset-source-cloudinary plugins previously for V2, > into a single plugin for V3. > -> For the v2 versions of these, please refer to the +> For the v2 versions of these, please refer to the > [v2-branch for sanity-plugin-cloudinary](https://github.com/sanity-io/sanity-plugin-cloudinary/tree/studio-v2) and > [sanity-plugin-asset-source-cloudinary](https://github.com/sanity-io/sanity-plugin-asset-source-cloudinary). @@ -22,10 +22,11 @@ yarn add sanity-plugin-cloudinary ## Usage -There are two plugins in this package: +There are three plugins in this package: - `cloudinaryAssetSourcePlugin` - use this if you intend to serve Cloudinary images from the Sanity CDN - `cloudinarySchemaPlugin` - use this if you intend to serve Cloudinary images from the Cloudinary CDN +- `cloudinaryReferencePlugin` - use this if you want to reference Cloudinary assets as document references Also see notes below on how Cloudinary config should be provided. @@ -92,6 +93,40 @@ Now you can declare a field to be `cloudinary.asset` in your schema } ``` +## Cloudinary Reference Assets + +This plugin mode allows you to store Cloudinary assets as document references, which can be useful for reusing the same asset across multiple documents. + +```js +import {defineConfig} from 'sanity' +import {cloudinaryReferencePlugin} from 'sanity-plugin-cloudinary' + +export default defineConfig({ + /*...*/ + plugins: [cloudinaryReferencePlugin()], +}) +``` + +Now you can declare a field to be a reference to a Cloudinary asset: + +```javascript +{ + type: "cloudinaryAssetReference", + name: "image", + description: "This is a reference to a Cloudinary asset document", +} +``` + +The plugin creates and maintains document references automatically. When you select an asset through the Cloudinary Media Library, it will: +1. Create a `cloudinaryAssetDocument` if the asset doesn't exist yet in your dataset +2. Update the asset if it already exists +3. Store a reference to the asset document in your current document + +This approach is particularly useful for: +- Reusing the same assets across multiple documents +- Updating assets in one place and having the changes reflected everywhere +- Managing assets separately from the content that uses them + ## Config Includes easy configuration of your cloudname and api key, stored safely in your dataset as a private document. diff --git a/src/components/CloudinaryReferenceInput.tsx b/src/components/CloudinaryReferenceInput.tsx new file mode 100644 index 0000000..a612b00 --- /dev/null +++ b/src/components/CloudinaryReferenceInput.tsx @@ -0,0 +1,184 @@ +/* eslint-disable no-nested-ternary */ +/* eslint-disable react/jsx-no-bind */ +import React, {useCallback, useState, useMemo} from 'react' +import {Button, Stack, Flex, Grid} from '@sanity/ui' +import {useClient} from 'sanity' +import {PatchEvent, set, unset} from 'sanity' +import {nanoid} from 'nanoid' +import {useSecrets} from '@sanity/studio-secrets' +import {PlugIcon} from '@sanity/icons' +import {ReferencePreview} from './ReferencePreview' +import {openMediaSelector} from '../utils' +import SecretsConfigView, {namespace, Secrets} from './SecretsConfigView' +import {InsertHandlerParams} from '../types' + +export function CloudinaryReferenceInput(props: any) { + const {onChange, value, schemaType} = props + const [showSettings, setShowSettings] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const {secrets} = useSecrets(namespace) + const client = useClient({apiVersion: '2023-01-01'}) + + const cloudName = secrets?.cloudName + const apiKey = secrets?.apiKey + const hasConfig = apiKey && cloudName + + const folderOption = useMemo(() => { + if (schemaType?.options.folder) { + return { + path: schemaType.options.folder.path, + // eslint-disable-next-line camelcase + resource_type: schemaType.options.folder.resource_type, + } + } + + return {} + }, [schemaType?.options.folder]) + + // Handle selecting an asset from Cloudinary + const handleSelect = useCallback( + async (payload: InsertHandlerParams) => { + const [asset] = payload.assets + + if (!asset) { + return + } + + try { + // Check if this asset already exists in Sanity + const existingAsset = await client.fetch( + `*[_type == "cloudinaryAssetDocument" && asset.id == $id][0]`, + {id: asset.id} + ) + + if (existingAsset) { + // Update the existing asset + await client.patch(existingAsset._id).set({asset: asset}).commit() + + // Set reference to the existing asset + onChange( + PatchEvent.from( + set({ + _type: 'reference', + _ref: existingAsset._id, + _weak: true, + }) + ) + ) + } else { + // Create a new asset document with a proper UUID + const newAsset = await client.create({ + _id: nanoid(), + _type: 'cloudinaryAssetDocument', + asset: { + ...asset, + _type: 'cloudinaryAssetReference', + _key: nanoid(), + }, + }) + + // Set reference to the new asset + onChange( + PatchEvent.from( + set({ + _type: 'reference', + _ref: newAsset._id, + _weak: true, + }) + ) + ) + } + } catch (err) { + console.error('Error creating/updating Cloudinary asset:', err) + } + }, + [client, onChange] + ) + + // Action to open the media selector + const handleOpenSelector = useCallback(() => { + if (!hasConfig) { + setShowSettings(true) + return + } + + setIsLoading(true) + + // Open the media selector + try { + openMediaSelector( + cloudName!, + apiKey!, + false, // single selection + (payload) => { + handleSelect(payload) + setIsLoading(false) + }, + undefined, + () => { + setIsLoading(false) + }, + folderOption + ) + + // Set a timeout to reset loading state if modal takes too long + setTimeout(() => { + setIsLoading(false) + }, 30000) // 30 seconds timeout as fallback + } catch (error) { + console.error('Error opening Cloudinary media selector:', error) + setIsLoading(false) + } + }, [cloudName, apiKey, hasConfig, handleSelect, folderOption]) + + return ( + + {showSettings && setShowSettings(false)} />} + + +