├── .npmrc ├── .prettierignore ├── .husky ├── pre-commit └── commit-msg ├── commitlint.config.js ├── .eslintignore ├── .prettierrc ├── lint-staged.config.js ├── .npmignore ├── sanity.json ├── .releaserc.json ├── .github ├── renovate.json └── workflows │ └── main.yml ├── tsconfig.json ├── .editorconfig ├── .eslintrc ├── v2-incompatible.js ├── src ├── schema │ ├── cloudinaryAssetContext.ts │ ├── cloudinaryAssetContextCustom.ts │ ├── cloudinaryAssetDerived.ts │ └── cloudinaryAsset.ts ├── components │ ├── VideoPlayer.tsx │ ├── SecretsConfigView.tsx │ ├── AssetDiff.tsx │ ├── AssetPreview.tsx │ ├── WidgetInput.tsx │ ├── AssetListFunctions.tsx │ ├── CloudinaryInput.tsx │ └── asset-source │ │ ├── CloudinaryAssetSource.tsx │ │ └── Icon.tsx ├── types.ts ├── index.ts └── utils.ts ├── tsconfig.lib.json ├── tsconfig.settings.json ├── package.config.ts ├── .gitignore ├── LICENSE ├── package.json ├── README.md └── CHANGELOG.md /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | lib 2 | pnpm-lock.yaml 3 | yarn.lock 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | } 4 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit "" 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.js 2 | .eslintrc.js 3 | commitlint.config.js 4 | lib 5 | lint-staged.config.js 6 | package.config.ts 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "printWidth": 100, 4 | "bracketSpacing": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '**/*.{js,jsx}': ['eslint'], 3 | '**/*.{ts,tsx}': ['eslint', () => 'tsc --noEmit'], 4 | } 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /test 2 | /coverage 3 | .editorconfig 4 | .eslintrc 5 | .gitignore 6 | .github 7 | .prettierrc 8 | .travis.yml 9 | .nyc_output 10 | -------------------------------------------------------------------------------- /sanity.json: -------------------------------------------------------------------------------- 1 | { 2 | "parts": [ 3 | { 4 | "implements": "part:@sanity/base/sanity-root", 5 | "path": "./v2-incompatible.js" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sanity/semantic-release-preset", 3 | "branches": ["main", {"name": "studio-v2", "channel": "studio-v2", "range": "0.x.x"}] 4 | } 5 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>sanity-io/renovate-config", 5 | "github>sanity-io/renovate-config:studio-v3" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.settings", 3 | "include": ["./src", "./package.config.ts"], 4 | "compilerOptions": { 5 | "rootDir": ".", 6 | "jsx": "react-jsx", 7 | "noEmit": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; editorconfig.org 2 | root = true 3 | charset= utf8 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "browser": true 6 | }, 7 | "extends": [ 8 | "sanity", 9 | "sanity/typescript", 10 | "sanity/react", 11 | "plugin:react-hooks/recommended", 12 | "plugin:prettier/recommended" 13 | ] 14 | } -------------------------------------------------------------------------------- /v2-incompatible.js: -------------------------------------------------------------------------------- 1 | const {showIncompatiblePluginDialog} = require('@sanity/incompatible-plugin') 2 | const {name, version, sanityExchangeUrl} = require('./package.json') 3 | 4 | export default showIncompatiblePluginDialog({ 5 | name: name, 6 | versions: { 7 | v3: version, 8 | v2: '^0.2.2', 9 | }, 10 | sanityExchangeUrl, 11 | }) 12 | -------------------------------------------------------------------------------- /src/schema/cloudinaryAssetContext.ts: -------------------------------------------------------------------------------- 1 | import {defineType} from 'sanity' 2 | 3 | export interface CloudinaryAssetContext { 4 | custom: object 5 | } 6 | 7 | export const cloudinaryAssetContext = defineType({ 8 | type: 'object', 9 | name: 'cloudinary.assetContext', 10 | fields: [ 11 | { 12 | type: 'cloudinary.assetContextCustom', 13 | name: 'custom', 14 | }, 15 | ], 16 | }) 17 | -------------------------------------------------------------------------------- /tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.settings", 3 | "include": ["./src"], 4 | "exclude": [ 5 | "./src/**/__fixtures__", 6 | "./src/**/__mocks__", 7 | "./src/**/*.test.ts", 8 | "./src/**/*.test.tsx" 9 | ], 10 | "compilerOptions": { 11 | "rootDir": ".", 12 | "outDir": "./lib", 13 | "jsx": "react-jsx", 14 | "emitDeclarationOnly": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "esnext", 5 | "module": "esnext", 6 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "downlevelIteration": true, 10 | "declaration": true, 11 | "allowSyntheticDefaultImports": true, 12 | "skipLibCheck": true, 13 | "isolatedModules": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/schema/cloudinaryAssetContextCustom.ts: -------------------------------------------------------------------------------- 1 | import {defineType} from 'sanity' 2 | 3 | export type CloudinaryAssetContextCustom = { 4 | alt: string 5 | caption: string 6 | } 7 | 8 | export const cloudinaryAssetContextCustom = defineType({ 9 | type: 'object', 10 | name: 'cloudinary.assetContextCustom', 11 | fields: [ 12 | { 13 | type: 'string', 14 | name: 'alt', 15 | }, 16 | { 17 | type: 'string', 18 | name: 'caption', 19 | }, 20 | ], 21 | }) 22 | -------------------------------------------------------------------------------- /package.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from '@sanity/pkg-utils' 2 | 3 | export default defineConfig({ 4 | legacyExports: true, 5 | dist: 'lib', 6 | tsconfig: 'tsconfig.lib.json', 7 | 8 | // Remove this block to enable strict export validation 9 | extract: { 10 | rules: { 11 | 'ae-forgotten-export': 'off', 12 | 'ae-incompatible-release-tags': 'off', 13 | 'ae-internal-missing-underscore': 'off', 14 | 'ae-missing-release-tag': 'off', 15 | }, 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /src/schema/cloudinaryAssetDerived.ts: -------------------------------------------------------------------------------- 1 | import {defineType} from 'sanity' 2 | 3 | export type CloudinaryAssetDerived = { 4 | raw_transformation: string 5 | url: string 6 | secure_url: string 7 | } 8 | 9 | export const cloudinaryAssetDerivedSchema = defineType({ 10 | type: 'object', 11 | name: 'cloudinary.assetDerived', 12 | fields: [ 13 | { 14 | type: 'string', 15 | name: 'raw_transformation', 16 | }, 17 | { 18 | type: 'url', 19 | name: 'url', 20 | }, 21 | { 22 | type: 'url', 23 | name: 'secure_url', 24 | }, 25 | ], 26 | }) 27 | -------------------------------------------------------------------------------- /src/components/VideoPlayer.tsx: -------------------------------------------------------------------------------- 1 | import React, {CSSProperties} from 'react' 2 | 3 | type PlayerKind = 'player' | 'diff' 4 | 5 | export type VideoPlayerProps = { 6 | src: string 7 | // eslint-disable-next-line react/no-unused-prop-types 8 | kind: PlayerKind 9 | } 10 | 11 | export default function VideoPlayer(props: VideoPlayerProps) { 12 | const {src} = props 13 | 14 | const style: CSSProperties = { 15 | width: '100%', 16 | height: 'auto', 17 | } 18 | 19 | return ( 20 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/components/SecretsConfigView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {SettingsView} from '@sanity/studio-secrets' 3 | 4 | export type Secrets = { 5 | cloudName: string 6 | apiKey: string 7 | } 8 | 9 | const pluginConfigKeys = [ 10 | { 11 | key: 'cloudName', 12 | title: 'Cloud name', 13 | description: '', 14 | }, 15 | { 16 | key: 'apiKey', 17 | title: 'API key', 18 | description: '', 19 | }, 20 | ] 21 | 22 | export const namespace = 'cloudinary' 23 | 24 | type Props = { 25 | onClose: () => void 26 | } 27 | 28 | const SecretsConfigView = (props: Props) => { 29 | return ( 30 | 36 | ) 37 | } 38 | 39 | export default SecretsConfigView 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # macOS finder cache file 40 | .DS_Store 41 | 42 | # VS Code settings 43 | .vscode 44 | 45 | # IntelliJ 46 | .idea 47 | *.iml 48 | 49 | # Cache 50 | .cache 51 | 52 | # Yalc 53 | .yalc 54 | yalc.lock 55 | 56 | ##npm package zips 57 | *.tgz 58 | 59 | # Compiled plugin 60 | lib 61 | dist 62 | 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Sanity.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/AssetDiff.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {DiffFromTo} from 'sanity' 3 | import VideoPlayer from './VideoPlayer' 4 | import {assetUrl} from '../utils' 5 | import {CloudinaryAsset} from '../types' 6 | 7 | type Props = { 8 | value: CloudinaryAsset | undefined 9 | } 10 | 11 | const CloudinaryDiffPreview = ({value}: Props) => { 12 | if (!value) { 13 | return null 14 | } 15 | 16 | const url = assetUrl(value) 17 | 18 | if (value.resource_type === 'video' && url) { 19 | return ( 20 |
27 | 28 |
29 | ) 30 | } 31 | 32 | return preview 33 | } 34 | 35 | type DiffProps = { 36 | diff: any 37 | schemaType: any 38 | } 39 | 40 | const AssetDiff = ({diff, schemaType}: DiffProps) => { 41 | return 42 | } 43 | 44 | export default AssetDiff 45 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import {CloudinaryAssetDerived} from './schema/cloudinaryAssetDerived' 2 | 3 | export type CloudinaryDerivative = { 4 | url: string 5 | secure_url: string 6 | raw_transformation: string 7 | } 8 | 9 | export type CloudinaryAssetResponse = { 10 | public_id: string 11 | resource_type: string 12 | type: string 13 | url: string 14 | tags: string[] 15 | secure_url: string 16 | format: string 17 | width: number 18 | height: number 19 | bytes: number 20 | context?: { 21 | custom: Record 22 | } 23 | derived?: CloudinaryDerivative[] 24 | } 25 | 26 | export type InsertHandlerParams = { 27 | assets: CloudinaryAssetResponse[] 28 | } 29 | 30 | export interface CloudinaryMediaLibrary { 31 | show: (config?: {asset: any; folder: any}) => void 32 | hide: () => void 33 | } 34 | 35 | export type CloudinaryAsset = { 36 | _type: string 37 | _key?: string 38 | _version: number 39 | public_id: string 40 | resource_type: string 41 | type: string 42 | format: string 43 | version: number 44 | url: string 45 | secure_url: string 46 | derived?: CloudinaryAssetDerived[] 47 | display_name?: string 48 | } 49 | 50 | export type AssetDocument = { 51 | _id: string 52 | label?: string 53 | title?: string 54 | description?: string 55 | source?: { 56 | id: string 57 | name: string 58 | url?: string 59 | } 60 | creditLine?: string 61 | originalFilename?: string 62 | } 63 | 64 | declare global { 65 | // eslint-disable-next-line no-unused-vars 66 | interface Window { 67 | cloudinary: { 68 | openMediaLibrary: (config: any, callbacks: any) => void 69 | createMediaLibrary: (config: any, callbacks?: any) => CloudinaryMediaLibrary 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/components/AssetPreview.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import VideoPlayer from './VideoPlayer' 3 | import {assetUrl} from '../utils' 4 | import {Flex, Text} from '@sanity/ui' 5 | import {CloudinaryAsset} from '../types' 6 | import {DocumentIcon} from '@sanity/icons' 7 | 8 | interface ComponentProps { 9 | layout?: 'default' | 'block' 10 | value: CloudinaryAsset | undefined 11 | } 12 | 13 | const AssetPreview = ({value, layout}: ComponentProps) => { 14 | const url = value && assetUrl(value) 15 | if (!value || !url) { 16 | return null 17 | } 18 | 19 | switch (value.resource_type) { 20 | case 'video': 21 | return ( 22 | 28 | 29 | 30 | ) 31 | case 'raw': 32 | return ( 33 | 34 | 35 | 36 | {value.display_name ?? 'Raw file'} 37 | 38 | 39 | ) 40 | default: 41 | return ( 42 | 43 | preview 61 | 62 | ) 63 | } 64 | } 65 | 66 | export default AssetPreview 67 | -------------------------------------------------------------------------------- /src/components/WidgetInput.tsx: -------------------------------------------------------------------------------- 1 | import React, {useCallback} from 'react' 2 | import {ObjectInputProps, PatchEvent, unset} from 'sanity' 3 | import {Button, Flex, Grid, Stack} from '@sanity/ui' 4 | import {PlugIcon} from '@sanity/icons' 5 | import {styled} from 'styled-components' 6 | import AssetPreview from './AssetPreview' 7 | import {CloudinaryAsset} from '../types' 8 | 9 | const SetupButtonContainer = styled.div` 10 | position: relative; 11 | display: block; 12 | font-size: 0.8em; 13 | transform: translate(0%, -10%); 14 | ` 15 | 16 | type WidgetInputProps = ObjectInputProps & {openMediaSelector: () => void; onSetup: () => void} 17 | 18 | const WidgetInput = (props: WidgetInputProps) => { 19 | const {onChange, readOnly, value, openMediaSelector} = props 20 | 21 | const removeValue = useCallback(() => { 22 | onChange(PatchEvent.from([unset()])) 23 | }, [onChange]) 24 | 25 | return ( 26 | 27 | 28 | 29 |