Skip to content
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions packages/decap-cms-core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ declare module 'decap-cms-core' {
i18n?: boolean | 'translate' | 'duplicate' | 'none';
media_folder?: string;
public_folder?: string;
media_processing?: CmsMediaProcessing;
comment?: string;
}

Expand Down Expand Up @@ -408,6 +409,21 @@ declare module 'decap-cms-core' {
url?: string;
}

export type CmsMediaProcessingFormat = 'jpeg' | 'webp';

export interface CmsMediaProcessing {
enabled: boolean;
format?: {
enabled: boolean;
default: CmsMediaProcessingFormat;
};
quality?: number;
strip_metadata?: boolean;
width?: number | null;
height?: number | null;
aspect_ratio?: number | string | null;
}

export interface CmsConfig {
backend: CmsBackend;
collections: CmsCollection[];
Expand All @@ -423,6 +439,7 @@ declare module 'decap-cms-core' {
media_folder?: string;
public_folder?: string;
media_folder_relative?: boolean;
media_processing?: CmsMediaProcessing;
media_library?: CmsMediaLibrary;
publish_mode?: CmsPublishMode;
issue_reports?: CmsIssueReports;
Expand Down
1 change: 1 addition & 0 deletions packages/decap-cms-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"license": "MIT",
"dependencies": {
"@iarna/toml": "2.2.5",
"@jsquash/webp": "1.5.0",
"@vercel/stega": "^0.1.2",
"ajv": "^8.17.1",
"ajv-errors": "^3.0.0",
Expand Down
130 changes: 129 additions & 1 deletion packages/decap-cms-core/src/actions/__tests__/mediaLibrary.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ import { insertMedia, persistMedia, deleteMedia } from '../mediaLibrary';

jest.mock('../../backend');
jest.mock('../waitUntil');
jest.mock('../../lib/imageTransformations', () => {
const actual = jest.requireActual('../../lib/imageTransformations');
return {
...actual,
transformImage: jest.fn(),
};
});
jest.mock('decap-cms-lib-util', () => {
const lib = jest.requireActual('decap-cms-lib-util');
return {
Expand Down Expand Up @@ -74,6 +81,7 @@ describe('mediaLibrary', () => {

beforeEach(() => {
jest.clearAllMocks();
window.confirm = jest.fn(() => true);
});

it('should not persist media when editing draft', () => {
Expand Down Expand Up @@ -146,7 +154,7 @@ describe('mediaLibrary', () => {
}),
integrations: Map(),
mediaLibrary: Map({
files: List(),
files: List([{ name: 'kittens.jpg' }]),
}),
entryDraft: Map({
entry: Map(),
Expand Down Expand Up @@ -239,6 +247,126 @@ describe('mediaLibrary', () => {
);
});
});

it('should persist a processed image as the selected media', () => {
const { transformImage } = require('../../lib/imageTransformations');
backend.persistMedia.mockImplementation((_config, assetProxy) => ({
id: assetProxy.path,
path: assetProxy.path,
}));

const store = mockStore({
config: {
media_folder: 'static/media',
media_processing: {
enabled: true,
format: { enabled: true, default: 'webp' },
quality: 80,
strip_metadata: true,
width: 400,
height: null,
aspect_ratio: '16_9',
},
slug: {
encoding: 'unicode',
clean_accents: false,
sanitize_replacement: '-',
},
},
collections: Map({
posts: Map({ name: 'posts' }),
}),
integrations: Map(),
mediaLibrary: Map({
files: List(),
}),
entryDraft: Map({
entry: Map(),
}),
});

const file = new File(['original'], 'kittens.jpg', { type: 'image/jpeg' });
const processed = new File(['processed'], 'kittens.webp', { type: 'image/webp' });

transformImage.mockResolvedValue([{ file: processed, path: 'static/media/kittens.webp' }]);

return store.dispatch(persistMedia(file)).then(() => {
const actions = store.getActions();
const addAssetActions = actions.filter(action => action.type === 'ADD_ASSET');
const persistedActions = actions.filter(action => action.type === 'MEDIA_PERSIST_SUCCESS');

expect(transformImage).toHaveBeenCalledWith(file, 'static/media/kittens.jpg', {
format: 'webp',
quality: 0.8,
width: 400,
height: null,
aspectRatio: 16 / 9,
});
expect(addAssetActions.map(action => action.payload.path)).toEqual([
'static/media/kittens.webp',
]);
expect(persistedActions.map(action => action.payload.file.path)).toEqual([
'static/media/kittens.webp',
]);
});
});

it('should confirm before replacing the original output file when format conversion is disabled', () => {
const { transformImage } = require('../../lib/imageTransformations');
backend.persistMedia.mockImplementation((_config, assetProxy) => ({
id: assetProxy.path,
path: assetProxy.path,
}));

const store = mockStore({
config: {
media_folder: 'static/media',
media_processing: {
enabled: true,
format: { enabled: false, default: 'webp' },
},
slug: {
encoding: 'unicode',
clean_accents: false,
sanitize_replacement: '-',
},
},
collections: Map({
posts: Map({ name: 'posts' }),
}),
integrations: Map(),
mediaLibrary: Map({
files: List([{ name: 'kittens.jpg', path: 'static/media/kittens.jpg' }]),
}),
entryDraft: Map({
entry: Map(),
}),
});

const file = new File(['original'], 'kittens.jpg', { type: 'image/jpeg' });
const processed = new File(['processed'], 'kittens.jpg', { type: 'image/jpeg' });

transformImage.mockResolvedValue([{ file: processed, path: 'static/media/kittens.jpg' }]);

return store.dispatch(persistMedia(file)).then(() => {
const addAssetActions = store.getActions().filter(action => action.type === 'ADD_ASSET');

expect(transformImage).toHaveBeenCalledWith(file, 'static/media/kittens.jpg', {
format: undefined,
quality: undefined,
width: null,
height: null,
aspectRatio: null,
});
expect(addAssetActions.map(action => action.payload.path)).toEqual([
'static/media/kittens.jpg',
]);
expect(window.confirm).toHaveBeenCalledWith(
'kittens.jpg already exists. Do you want to replace it?',
);
expect(backend.persistMedia).toHaveBeenCalledTimes(1);
});
});
});

describe('deleteMedia', () => {
Expand Down
Loading
Loading