Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand All @@ -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.

Expand Down Expand Up @@ -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.
Expand Down
184 changes: 184 additions & 0 deletions src/components/CloudinaryReferenceInput.tsx
Original file line number Diff line number Diff line change
@@ -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<Secrets>(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 (
<Stack space={3}>
{showSettings && <SecretsConfigView onClose={() => setShowSettings(false)} />}

<Flex justify="flex-end">
<Button
color="primary"
icon={PlugIcon}
mode="bleed"
title="Configure"
onClick={() => setShowSettings(true)}
tabIndex={1}
text={hasConfig ? undefined : 'Configure Cloudinary plugin'}
/>
</Flex>

<Flex style={{textAlign: 'center', width: '100%'}} marginBottom={2}>
<ReferencePreview value={value} />
</Flex>

<Stack space={2}>
<Grid gap={1} style={{gridTemplateColumns: 'repeat(auto-fit, minmax(100px, 1fr))'}}>
<Button
text={
hasConfig
? isLoading
? 'Opening Media Library...'
: 'Select Asset'
: 'Configure Cloudinary to Select Assets'
}
onClick={handleOpenSelector}
tone="primary"
mode="ghost"
disabled={(!hasConfig && !showSettings) || isLoading}
loading={isLoading}
/>

{value && (
<Button
text="Remove"
tone="critical"
mode="ghost"
onClick={() => onChange(PatchEvent.from(unset()))}
disabled={isLoading}
/>
)}
</Grid>
</Stack>
</Stack>
)
}
44 changes: 44 additions & 0 deletions src/components/ReferencePreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, {useEffect, useState} from 'react'
import {useClient} from 'sanity'
import AssetPreview from './AssetPreview'

interface ReferencePreviewProps {
value: {_ref: string}
}

export function ReferencePreview(props: ReferencePreviewProps) {
const {value} = props
const client = useClient({apiVersion: '2023-01-01'})
const [asset, setAsset] = useState<any>(null)
const [loading, setLoading] = useState(true)

useEffect(() => {
if (!value?._ref) {
setAsset(null)
setLoading(false)
return
}

setLoading(true)
client
.getDocument(value._ref)
.then((document) => {
setAsset(document?.asset || null)
setLoading(false)
})
.catch((err) => {
console.error('Error fetching referenced asset:', err)
setLoading(false)
})
}, [value, client])

if (loading) {
return <div>Loading asset...</div>
}

if (!asset) {
return null
}

return <AssetPreview value={asset} layout="block" />
}
20 changes: 19 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {CloudinaryAssetSource} from './components/asset-source/CloudinaryAssetSo
import {cloudinaryAssetContext} from './schema/cloudinaryAssetContext'
import {cloudinaryAssetContextCustom} from './schema/cloudinaryAssetContextCustom'
import {AssetListFunctions} from './components/AssetListFunctions'
import {cloudinaryAssetDocument} from './schema/cloudinaryAssetDocument'
import {cloudinaryAssetReference} from './schema/cloudinaryAssetReference'

export {type CloudinaryAssetContext} from './schema/cloudinaryAssetContext'
export {type CloudinaryAssetDerived} from './schema/cloudinaryAssetDerived'
Expand All @@ -23,8 +25,24 @@ export {
cloudinaryAssetDerivedSchema,
cloudinaryAssetContext,
cloudinaryAssetContextCustom,
cloudinaryAssetDocument,
cloudinaryAssetReference,
}

export const cloudinaryReferencePlugin = definePlugin({
name: 'cloudinary-reference',
schema: {
types: [
cloudinaryAssetDocument,
cloudinaryAssetReference,
cloudinaryAssetSchema,
cloudinaryAssetDerivedSchema,
cloudinaryAssetContext,
cloudinaryAssetContextCustom,
],
},
})

export const cloudinarySchemaPlugin = definePlugin({
name: 'cloudinary-schema',
form: {
Expand Down Expand Up @@ -62,7 +80,7 @@ export const cloudinaryImageSource: AssetSource = {
}

export const cloudinaryAssetSourcePlugin = definePlugin({
name: 'cloudinart-asset-source',
name: 'cloudinary-asset-source',
form: {
image: {
assetSources: [cloudinaryImageSource],
Expand Down
40 changes: 40 additions & 0 deletions src/schema/cloudinaryAssetDocument.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {defineType} from 'sanity'
import {cloudinaryAssetSchema} from './cloudinaryAsset'

export const cloudinaryAssetDocument = defineType({
name: 'cloudinaryAssetDocument',
title: 'Cloudinary Asset',
type: 'document',
fields: [
{
name: 'asset',
type: cloudinaryAssetSchema.name,
title: 'Cloudinary Asset',
},
],
preview: {
select: {
caption: 'asset.metadata.caption',
alt: 'asset.metadata.alt_text',
displayName: 'asset.display_name',
resourceType: 'asset.resource_type',
format: 'asset.format',
media: 'asset',
},
prepare({caption, alt, displayName, resourceType, format, media}) {
// Use caption or alt text as the title, fall back to display name
const title = caption || alt || displayName || 'Untitled Asset'

// Create a descriptive subtitle
const type = resourceType || 'image'
const formatInfo = format ? `(${format})` : ''
const subtitle = `${type} ${formatInfo}`

return {
title,
subtitle,
media,
}
},
},
})
19 changes: 19 additions & 0 deletions src/schema/cloudinaryAssetReference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {defineType} from 'sanity'
import {CloudinaryReferenceInput} from '../components/CloudinaryReferenceInput'

export const cloudinaryAssetReference = defineType({
name: 'cloudinaryAssetReference',
title: 'Cloudinary Asset Reference',
type: 'object',
fields: [
{
name: 'asset',
type: 'reference',
to: [{type: 'cloudinaryAssetDocument'}],
weak: true,
},
],
components: {
input: CloudinaryReferenceInput,
},
})
Loading