From 0f218aa6fff33205c050e13db152c95699a69372 Mon Sep 17 00:00:00 2001 From: Canopix Date: Fri, 13 Jun 2025 16:26:44 -0300 Subject: [PATCH] Add Cloudinary Media Library --- .env | 6 +- custom-application-config.js | 16 ++++ .../asset-input/asset-input.tsx | 96 +++++++++++++++++-- src/globals.d.ts | 17 ++++ src/hooks/use-cloudinary.ts | 34 +++++++ 5 files changed, 159 insertions(+), 10 deletions(-) create mode 100644 src/hooks/use-cloudinary.ts diff --git a/.env b/.env index ea651dc..253206d 100644 --- a/.env +++ b/.env @@ -6,4 +6,8 @@ CLOUD_IDENTIFIER=gcp-us APPLICATION_ID=cldoqtyfj009nzk01wfj8r99o APPLICATION_URL=https://custom-objects-editor.pages.dev INITIAL_PROJECT_KEY=aries_dev-1 -ENTRY_POINT_URI_PATH=custom-objs \ No newline at end of file +ENTRY_POINT_URI_PATH=custom-objs + +MC_APP_CLOUDINARY_CLOUD_NAME= +MC_APP_CLOUDINARY_UPLOAD_PRESET=media-library +MC_APP_CLOUDINARY_ENABLED=true \ No newline at end of file diff --git a/custom-application-config.js b/custom-application-config.js index f854f4a..22eeb2c 100644 --- a/custom-application-config.js +++ b/custom-application-config.js @@ -37,8 +37,24 @@ const config = { labelAllLocales: [], permissions: [PERMISSIONS.View], }, + headers: { + csp: { + 'script-src': ['https://upload-widget.cloudinary.com'], + 'connect-src': ['https://api.cloudinary.com'], + 'img-src': ['res.cloudinary.com'], + 'frame-src': [ + 'https://widget.cloudinary.com', + 'https://upload-widget.cloudinary.com', + 'https://www.facebook.com', + 'https://www.instagram.com', + ], + }, + }, additionalEnv: { logoMustBeVisible: '${env:LOGO_MUST_BE_VISIBLE}', + cloudinaryCloudName: '${env:MC_APP_CLOUDINARY_CLOUD_NAME}', + cloudinaryUploadPreset: '${env:MC_APP_CLOUDINARY_UPLOAD_PRESET}', + cloudinaryEnabled: '${env:MC_APP_CLOUDINARY_ENABLED}', }, submenuLinks: [ // { diff --git a/src/components/custom-object-form/asset-input/asset-input.tsx b/src/components/custom-object-form/asset-input/asset-input.tsx index 8d6aa78..5a49971 100644 --- a/src/components/custom-object-form/asset-input/asset-input.tsx +++ b/src/components/custom-object-form/asset-input/asset-input.tsx @@ -1,11 +1,12 @@ -// src/components/attribute-input/asset-input.js - import Spacings from '@commercetools-uikit/spacings'; import TextInput from '@commercetools-uikit/text-input'; import LocalizedTextInput from '@commercetools-uikit/localized-text-input'; import { useApplicationContext } from '@commercetools-frontend/application-shell-connectors'; import Text from '@commercetools-uikit/text'; -import { FC } from 'react'; +import { FC, useState } from 'react'; +import PrimaryButton from '@commercetools-uikit/primary-button'; +import LoadingSpinner from '@commercetools-uikit/loading-spinner'; +import useCloudinary from '../../../hooks/use-cloudinary'; import SourceArrayInput from './source-array-input'; import { Asset, LocalizedString, Source } from './types'; @@ -25,14 +26,81 @@ const AssetInput: FC = ({ touched, errors, }) => { - const { dataLocale } = useApplicationContext((context) => ({ - dataLocale: context.dataLocale ?? '', - })); + const { dataLocale, environment } = useApplicationContext<{ + cloudinaryEnabled?: string | boolean; + cloudinaryCloudName?: string; + cloudinaryUploadPreset?: string; + }>(); + + const [isWidgetOpening, setIsWidgetOpening] = useState(false); + const isCloudinaryEnabled = + environment.cloudinaryEnabled === true && + !!environment.cloudinaryCloudName && + !!environment.cloudinaryUploadPreset; + + const cloudinaryLoaded = useCloudinary(); const triggerChange = (updatedValue: Partial) => { onChange({ target: { name, value: updatedValue } }); }; + const handleOpenMediaLibrary = () => { + if (cloudinaryLoaded && (window as any).cloudinary) { + setIsWidgetOpening(true); + (window as any).cloudinary.openUploadWidget( + { + cloudName: environment.cloudinaryCloudName || '', + uploadPreset: environment.cloudinaryUploadPreset || '', + sources: [ + 'local', + 'url', + 'camera', + 'image_search', + 'google_drive', + 'facebook', + 'dropbox', + 'instagram', + 'shutterstock', + 'getty', + 'istock', + 'unsplash', + ], + multiple: false, + }, + (error: any, result: any) => { + if ( + result && + (result.event === 'success' || + result.event === 'close' || + result.event === 'abort') + ) { + setIsWidgetOpening(false); + } + + if (!error && result && result.event === 'success') { + const assetData = result.info; + triggerChange({ + key: assetData.public_id, + name: { [dataLocale || '']: assetData.original_filename || '' }, + description: { [dataLocale || '']: '' }, + tags: assetData.tags, + folder: assetData.asset_folder, + sources: [ + { + uri: assetData.secure_url, + key: assetData.public_id, + contentType: assetData.format, + width: assetData.width, + height: assetData.height, + }, + ], + }); + } + } + ); + } + }; + const handleChange = (e: React.ChangeEvent) => { const { name: fieldName, value: fieldValue } = e.target; triggerChange({ ...value, [fieldName]: fieldValue }); @@ -57,7 +125,17 @@ const AssetInput: FC = ({ }; return ( - + + {isCloudinaryEnabled && ( + : undefined} + /> + )} = ({ name="name" placeholder={'Asset Name' as unknown as Record} value={value?.name || {}} - selectedLanguage={dataLocale} + selectedLanguage={dataLocale || ''} onChange={(event) => handleLocalizedChange( event.target.value as unknown as LocalizedString, @@ -87,7 +165,7 @@ const AssetInput: FC = ({ name="description" placeholder={'Asset Description' as unknown as Record} value={value?.description || {}} - selectedLanguage={dataLocale} + selectedLanguage={dataLocale || ''} onChange={(event) => handleLocalizedChange( event.target.value as unknown as LocalizedString, diff --git a/src/globals.d.ts b/src/globals.d.ts index 108cefb..1d3903c 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -1,3 +1,18 @@ +declare module '@commercetools-frontend/application-shell-connectors' { + export interface ApplicationRuntimeEnvironment { + cloudinaryEnabled?: string; + cloudinaryCloudName?: string; + cloudinaryUploadPreset?: string; + } +} + +declare global { + interface Window { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + cloudinary?: any; + } +} + declare module '*.graphql' { // eslint-disable-next-line @typescript-eslint/no-explicit-any const content: any; @@ -11,3 +26,5 @@ declare module '*.module.css' { declare module '*.png'; declare module '*.svg'; + +export {}; diff --git a/src/hooks/use-cloudinary.ts b/src/hooks/use-cloudinary.ts new file mode 100644 index 0000000..2d05047 --- /dev/null +++ b/src/hooks/use-cloudinary.ts @@ -0,0 +1,34 @@ +import { useState, useEffect } from 'react'; + +const useCloudinary = () => { + const [loaded, setLoaded] = useState(false); + + useEffect(() => { + const scriptId = 'cloudinary-upload-widget-script'; + if (document.getElementById(scriptId)) { + setLoaded(true); + return; + } + + const script = document.createElement('script'); + script.id = scriptId; + script.src = 'https://upload-widget.cloudinary.com/global/all.js'; + script.async = true; + script.onload = () => { + setLoaded(true); + }; + + document.body.appendChild(script); + + return () => { + const scriptElement = document.getElementById(scriptId); + if (scriptElement) { + document.body.removeChild(scriptElement); + } + }; + }, []); + + return loaded; +}; + +export default useCloudinary; \ No newline at end of file