Skip to content

Commit 95ec682

Browse files
committed
feat: support browser media processing
1 parent 505b6a4 commit 95ec682

15 files changed

Lines changed: 916 additions & 33 deletions

File tree

package-lock.json

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/decap-cms-core/index.d.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ declare module 'decap-cms-core' {
6060
i18n?: boolean | 'translate' | 'duplicate' | 'none';
6161
media_folder?: string;
6262
public_folder?: string;
63+
media_processing?: CmsMediaProcessing;
6364
comment?: string;
6465
}
6566

@@ -408,6 +409,21 @@ declare module 'decap-cms-core' {
408409
url?: string;
409410
}
410411

412+
export type CmsMediaProcessingFormat = 'jpeg' | 'webp';
413+
414+
export interface CmsMediaProcessing {
415+
enabled: boolean;
416+
format?: {
417+
enabled: boolean;
418+
default: CmsMediaProcessingFormat;
419+
};
420+
quality?: number;
421+
strip_metadata?: boolean;
422+
width?: number | null;
423+
height?: number | null;
424+
aspect_ratio?: number | string | null;
425+
}
426+
411427
export interface CmsConfig {
412428
backend: CmsBackend;
413429
collections: CmsCollection[];
@@ -423,6 +439,7 @@ declare module 'decap-cms-core' {
423439
media_folder?: string;
424440
public_folder?: string;
425441
media_folder_relative?: boolean;
442+
media_processing?: CmsMediaProcessing;
426443
media_library?: CmsMediaLibrary;
427444
publish_mode?: CmsPublishMode;
428445
issue_reports?: CmsIssueReports;

packages/decap-cms-core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"license": "MIT",
2626
"dependencies": {
2727
"@iarna/toml": "2.2.5",
28+
"@jsquash/webp": "1.5.0",
2829
"@vercel/stega": "^0.1.2",
2930
"ajv": "^8.17.1",
3031
"ajv-errors": "^3.0.0",

packages/decap-cms-core/src/actions/__tests__/mediaLibrary.spec.js

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ import { insertMedia, persistMedia, deleteMedia } from '../mediaLibrary';
66

77
jest.mock('../../backend');
88
jest.mock('../waitUntil');
9+
jest.mock('../../lib/imageTransformations', () => {
10+
const actual = jest.requireActual('../../lib/imageTransformations');
11+
return {
12+
...actual,
13+
transformImage: jest.fn(),
14+
};
15+
});
916
jest.mock('decap-cms-lib-util', () => {
1017
const lib = jest.requireActual('decap-cms-lib-util');
1118
return {
@@ -74,6 +81,7 @@ describe('mediaLibrary', () => {
7481

7582
beforeEach(() => {
7683
jest.clearAllMocks();
84+
window.confirm = jest.fn(() => true);
7785
});
7886

7987
it('should not persist media when editing draft', () => {
@@ -146,7 +154,7 @@ describe('mediaLibrary', () => {
146154
}),
147155
integrations: Map(),
148156
mediaLibrary: Map({
149-
files: List(),
157+
files: List([{ name: 'kittens.jpg' }]),
150158
}),
151159
entryDraft: Map({
152160
entry: Map(),
@@ -239,6 +247,126 @@ describe('mediaLibrary', () => {
239247
);
240248
});
241249
});
250+
251+
it('should persist a processed image as the selected media', () => {
252+
const { transformImage } = require('../../lib/imageTransformations');
253+
backend.persistMedia.mockImplementation((_config, assetProxy) => ({
254+
id: assetProxy.path,
255+
path: assetProxy.path,
256+
}));
257+
258+
const store = mockStore({
259+
config: {
260+
media_folder: 'static/media',
261+
media_processing: {
262+
enabled: true,
263+
format: { enabled: true, default: 'webp' },
264+
quality: 80,
265+
strip_metadata: true,
266+
width: 400,
267+
height: null,
268+
aspect_ratio: '16_9',
269+
},
270+
slug: {
271+
encoding: 'unicode',
272+
clean_accents: false,
273+
sanitize_replacement: '-',
274+
},
275+
},
276+
collections: Map({
277+
posts: Map({ name: 'posts' }),
278+
}),
279+
integrations: Map(),
280+
mediaLibrary: Map({
281+
files: List(),
282+
}),
283+
entryDraft: Map({
284+
entry: Map(),
285+
}),
286+
});
287+
288+
const file = new File(['original'], 'kittens.jpg', { type: 'image/jpeg' });
289+
const processed = new File(['processed'], 'kittens.webp', { type: 'image/webp' });
290+
291+
transformImage.mockResolvedValue([{ file: processed, path: 'static/media/kittens.webp' }]);
292+
293+
return store.dispatch(persistMedia(file)).then(() => {
294+
const actions = store.getActions();
295+
const addAssetActions = actions.filter(action => action.type === 'ADD_ASSET');
296+
const persistedActions = actions.filter(action => action.type === 'MEDIA_PERSIST_SUCCESS');
297+
298+
expect(transformImage).toHaveBeenCalledWith(file, 'static/media/kittens.jpg', {
299+
format: 'webp',
300+
quality: 0.8,
301+
width: 400,
302+
height: null,
303+
aspectRatio: 16 / 9,
304+
});
305+
expect(addAssetActions.map(action => action.payload.path)).toEqual([
306+
'static/media/kittens.webp',
307+
]);
308+
expect(persistedActions.map(action => action.payload.file.path)).toEqual([
309+
'static/media/kittens.webp',
310+
]);
311+
});
312+
});
313+
314+
it('should confirm before replacing the original output file when format conversion is disabled', () => {
315+
const { transformImage } = require('../../lib/imageTransformations');
316+
backend.persistMedia.mockImplementation((_config, assetProxy) => ({
317+
id: assetProxy.path,
318+
path: assetProxy.path,
319+
}));
320+
321+
const store = mockStore({
322+
config: {
323+
media_folder: 'static/media',
324+
media_processing: {
325+
enabled: true,
326+
format: { enabled: false, default: 'webp' },
327+
},
328+
slug: {
329+
encoding: 'unicode',
330+
clean_accents: false,
331+
sanitize_replacement: '-',
332+
},
333+
},
334+
collections: Map({
335+
posts: Map({ name: 'posts' }),
336+
}),
337+
integrations: Map(),
338+
mediaLibrary: Map({
339+
files: List([{ name: 'kittens.jpg', path: 'static/media/kittens.jpg' }]),
340+
}),
341+
entryDraft: Map({
342+
entry: Map(),
343+
}),
344+
});
345+
346+
const file = new File(['original'], 'kittens.jpg', { type: 'image/jpeg' });
347+
const processed = new File(['processed'], 'kittens.jpg', { type: 'image/jpeg' });
348+
349+
transformImage.mockResolvedValue([{ file: processed, path: 'static/media/kittens.jpg' }]);
350+
351+
return store.dispatch(persistMedia(file)).then(() => {
352+
const addAssetActions = store.getActions().filter(action => action.type === 'ADD_ASSET');
353+
354+
expect(transformImage).toHaveBeenCalledWith(file, 'static/media/kittens.jpg', {
355+
format: undefined,
356+
quality: undefined,
357+
width: null,
358+
height: null,
359+
aspectRatio: null,
360+
});
361+
expect(addAssetActions.map(action => action.payload.path)).toEqual([
362+
'static/media/kittens.jpg',
363+
]);
364+
expect(window.confirm).toHaveBeenCalledWith(
365+
'kittens.jpg already exists. Do you want to replace it?',
366+
);
367+
expect(backend.persistMedia).toHaveBeenCalledTimes(1);
368+
});
369+
});
242370
});
243371

244372
describe('deleteMedia', () => {

0 commit comments

Comments
 (0)