Tutorial – Images using Cloudinary

Goal for this step 🏁: Add support for a new component type and uploading images using Cloudinary.

Dossier doesn't store images itself, instead you can integrate with external services. For this tutorial we will use Cloudinary.

After signing up for a Cloudinary project, go to Setting, Upload and click on "Add upload preset". We change "Signing Mode" to "Unsigned" and set the folder to "Tutorial". Click on "Save" and copy the "Upload preset name".

Configuration of the new upload preset in Cloudinary

Add the new configuration to .env (if you want to use your own Cloudinary project, override the configuration in .env.local):

VITE_CLOUDINARY_CLOUD_NAME=dossierhq
VITE_CLOUDINARY_UPLOAD_PRESET=ncdju1vk

Next, add the dependency that includes a Cloudinary field editor:

  • npm install @dossierhq/cloudinary

In order to support Cloudinary in the schema we want to add a component type called CloudinaryImage and an image field in Message were we can add an image. We could do it through the web interface, but instead we'll do it in backend/server.ts, since @dossierhq/cloudinary provides a definition of the type with all the fields:

import { CLOUDINARY_IMAGE_COMPONENT_TYPE } from '@dossierhq/cloudinary';

async function updateSchema(client: AppDossierClient) {
const schemaResult = await client.updateSchemaSpecification({
entityTypes: [
{
name: 'Message',
nameField: 'message',
fields: [
{ name: 'message', type: FieldType.String, required: true },
{ name: 'image', type: FieldType.Component, componentTypes: ['CloudinaryImage'] },
],
},
],
componentTypes: [CLOUDINARY_IMAGE_COMPONENT_TYPE],
});
return schemaResult;
}

When the backend is restarted we can see that the component type is set up:

[be] info: Loading schema
[be] info: Loaded schema with 1 entity types, 0 component types, 0 patterns, 0 indexes
[be] info: Updated schema, new schema has 1 entity types, 1 component types, 0 patterns, 0 indexes
[be] Listening on http://localhost:3000

In the frontend we expose the configuration in src/CloudinaryConfig.ts:

export const CLOUDINARY_CLOUD_NAME = import.meta.env.VITE_CLOUDINARY_CLOUD_NAME;
export const CLOUDINARY_UPLOAD_PRESET = import.meta.env.VITE_CLOUDINARY_UPLOAD_PRESET;

To enable the field editor we update src/AppAdminProvider.tsx:

import {
CloudinaryImageFieldEditor,
CloudinaryImageFieldEditorWithoutClear,
} from '@dossierhq/cloudinary';
import { FieldType, isComponentItemField } from '@dossierhq/core';
import { CLOUDINARY_CLOUD_NAME, CLOUDINARY_UPLOAD_PRESET } from './CloudinaryConfig.js';
import { isAdminCloudinaryImage } from './SchemaTypes.js';

class AdminAdapter implements DossierContextAdapter {
renderFieldEditor(props: FieldEditorProps): JSX.Element | null {
const { fieldSpec, value } = props;
if (
fieldSpec.type === FieldType.Component &&
isComponentItemField(fieldSpec, value) &&
value &&
isAdminCloudinaryImage(value)
) {
return CloudinaryImageFieldEditor({
...props,
cloudName: CLOUDINARY_CLOUD_NAME,
uploadPreset: CLOUDINARY_UPLOAD_PRESET,
fieldSpec,
value,
});
}

return null;
}

renderRichTextComponentEditor(props: RichTextComponentEditorProps): JSX.Element | null {
const { value, validationIssues, onChange } = props;
if (isAdminCloudinaryImage(value)) {
return CloudinaryImageFieldEditorWithoutClear({
cloudName: CLOUDINARY_CLOUD_NAME,
uploadPreset: CLOUDINARY_UPLOAD_PRESET,
validationIssues,
value,
onChange,
});
}
return null;
}
}

And similar for src/AppPublishedProvider.tsx:

import { CloudinaryImageFieldDisplay } from '@dossierhq/cloudinary';
import { isComponentItemField } from '@dossierhq/core';
import { CLOUDINARY_CLOUD_NAME } from './CloudinaryConfig.js';
import { isPublishedCloudinaryImage } from './SchemaTypes.js';

class PublishedAdapter implements PublishedDossierContextAdapter {
renderPublishedFieldDisplay(props: FieldDisplayProps): JSX.Element | null {
const { fieldSpec, value } = props;
if (isComponentItemField(fieldSpec, value) && value && isPublishedCloudinaryImage(value)) {
return CloudinaryImageFieldDisplay({
cloudName: CLOUDINARY_CLOUD_NAME,
value,
});
}
return null;
}

renderPublishedRichTextComponentDisplay({
value,
}: RichTextComponentDisplayProps): JSX.Element | null {
if (value && isPublishedCloudinaryImage(value)) {
return CloudinaryImageFieldDisplay({
cloudName: CLOUDINARY_CLOUD_NAME,
value,
});
}
return null;
}
}

And that's it. Now we can add a component of type CloudinaryImage to the image field.

Screenshot of web interface with no image

Which will show the Cloudinary upload widget.

Screenshot of web interface with the Cloudinary upload widget

When the image has been uploaded, a preview of the image is shown, and it's possible to add alt text.

 

Screenshot of web interface with thumbnail of the uploaded image