Skip to content
Merged
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
6 changes: 5 additions & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -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
ENTRY_POINT_URI_PATH=custom-objs

MC_APP_CLOUDINARY_CLOUD_NAME=
MC_APP_CLOUDINARY_UPLOAD_PRESET=media-library
MC_APP_CLOUDINARY_ENABLED=true
16 changes: 16 additions & 0 deletions custom-application-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
// {
Expand Down
96 changes: 87 additions & 9 deletions src/components/custom-object-form/asset-input/asset-input.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -25,14 +26,81 @@ const AssetInput: FC<Props> = ({
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<Asset>) => {
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<HTMLInputElement>) => {
const { name: fieldName, value: fieldValue } = e.target;
triggerChange({ ...value, [fieldName]: fieldValue });
Expand All @@ -57,7 +125,17 @@ const AssetInput: FC<Props> = ({
};

return (
<Spacings.Stack scale="l">
<Spacings.Stack scale="l" alignItems="flex-start">
{isCloudinaryEnabled && (
<PrimaryButton
label={
isWidgetOpening ? 'Loading...' : 'Open Cloudinary Media Library'
}
onClick={handleOpenMediaLibrary}
isDisabled={!cloudinaryLoaded || isWidgetOpening}
iconLeft={isWidgetOpening ? <LoadingSpinner /> : undefined}
/>
)}
<TextInput
name="key"
placeholder="Key (publicId)"
Expand All @@ -73,7 +151,7 @@ const AssetInput: FC<Props> = ({
name="name"
placeholder={'Asset Name' as unknown as Record<string, string>}
value={value?.name || {}}
selectedLanguage={dataLocale}
selectedLanguage={dataLocale || ''}
onChange={(event) =>
handleLocalizedChange(
event.target.value as unknown as LocalizedString,
Expand All @@ -87,7 +165,7 @@ const AssetInput: FC<Props> = ({
name="description"
placeholder={'Asset Description' as unknown as Record<string, string>}
value={value?.description || {}}
selectedLanguage={dataLocale}
selectedLanguage={dataLocale || ''}
onChange={(event) =>
handleLocalizedChange(
event.target.value as unknown as LocalizedString,
Expand Down
17 changes: 17 additions & 0 deletions src/globals.d.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -11,3 +26,5 @@ declare module '*.module.css' {

declare module '*.png';
declare module '*.svg';

export {};
34 changes: 34 additions & 0 deletions src/hooks/use-cloudinary.ts
Original file line number Diff line number Diff line change
@@ -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;
Loading