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".
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.
Which will show 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.