decap-cms-core 3.11.0 → 3.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/decap-cms-core.js +18 -18
- package/dist/decap-cms-core.js.map +1 -1
- package/dist/esm/backend.js +8 -4
- package/dist/esm/bootstrap.js +2 -2
- package/dist/esm/components/Editor/EditorPreviewPane/EditorPreviewContent.js +1 -0
- package/dist/esm/components/Editor/EditorPreviewPane/EditorPreviewPane.js +6 -4
- package/dist/esm/components/MediaLibrary/MediaLibraryButtons.js +7 -8
- package/dist/esm/components/UI/ErrorBoundary.js +6 -7
- package/dist/esm/formats/yaml.js +11 -2
- package/dist/esm/lib/formatters.js +14 -3
- package/dist/esm/lib/i18n.js +8 -3
- package/index.d.ts +1 -0
- package/package.json +2 -3
- package/src/backend.ts +4 -0
- package/src/components/Editor/EditorPreviewPane/EditorPreviewContent.js +1 -0
- package/src/components/Editor/EditorPreviewPane/EditorPreviewPane.js +3 -0
- package/src/components/MediaLibrary/MediaLibraryButtons.js +2 -3
- package/src/components/UI/ErrorBoundary.js +1 -2
- package/src/formats/__tests__/frontmatter.spec.js +21 -0
- package/src/formats/__tests__/yaml.spec.js +27 -0
- package/src/formats/yaml.ts +16 -1
- package/src/lib/__tests__/formatters.spec.js +21 -0
- package/src/lib/formatters.ts +15 -3
- package/src/lib/i18n.ts +14 -6
- package/src/types/redux.ts +1 -0
package/src/backend.ts
CHANGED
|
@@ -1178,6 +1178,7 @@ export class Backend {
|
|
|
1178
1178
|
path,
|
|
1179
1179
|
authorLogin: user.login,
|
|
1180
1180
|
authorName: user.name,
|
|
1181
|
+
authorEmail: user.email,
|
|
1181
1182
|
},
|
|
1182
1183
|
user.useOpenAuthoring,
|
|
1183
1184
|
);
|
|
@@ -1253,6 +1254,7 @@ export class Backend {
|
|
|
1253
1254
|
path: file.path,
|
|
1254
1255
|
authorLogin: user.login,
|
|
1255
1256
|
authorName: user.name,
|
|
1257
|
+
authorEmail: user.email,
|
|
1256
1258
|
},
|
|
1257
1259
|
user.useOpenAuthoring,
|
|
1258
1260
|
),
|
|
@@ -1279,6 +1281,7 @@ export class Backend {
|
|
|
1279
1281
|
path,
|
|
1280
1282
|
authorLogin: user.login,
|
|
1281
1283
|
authorName: user.name,
|
|
1284
|
+
authorEmail: user.email,
|
|
1282
1285
|
},
|
|
1283
1286
|
user.useOpenAuthoring,
|
|
1284
1287
|
);
|
|
@@ -1303,6 +1306,7 @@ export class Backend {
|
|
|
1303
1306
|
path,
|
|
1304
1307
|
authorLogin: user.login,
|
|
1305
1308
|
authorName: user.name,
|
|
1309
|
+
authorEmail: user.email,
|
|
1306
1310
|
},
|
|
1307
1311
|
user.useOpenAuthoring,
|
|
1308
1312
|
);
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
getPreviewTemplate,
|
|
14
14
|
getPreviewStyles,
|
|
15
15
|
getRemarkPlugins,
|
|
16
|
+
getEditorComponents,
|
|
16
17
|
} from '../../../lib/registry';
|
|
17
18
|
import { getAllEntries, tryLoadEntry } from '../../../actions/entries';
|
|
18
19
|
import { ErrorBoundary } from '../../UI';
|
|
@@ -57,6 +58,7 @@ export class PreviewPane extends React.Component {
|
|
|
57
58
|
fieldsMetaData={metadata}
|
|
58
59
|
resolveWidget={resolveWidget}
|
|
59
60
|
getRemarkPlugins={getRemarkPlugins}
|
|
61
|
+
getEditorComponents={getEditorComponents}
|
|
60
62
|
/>
|
|
61
63
|
);
|
|
62
64
|
};
|
|
@@ -261,6 +263,7 @@ export class PreviewPane extends React.Component {
|
|
|
261
263
|
this.widgetFor(name, fields, values, fieldsMetaData),
|
|
262
264
|
widgetsFor: this.widgetsFor,
|
|
263
265
|
getCollection: this.getCollection,
|
|
266
|
+
getEditorComponents,
|
|
264
267
|
};
|
|
265
268
|
|
|
266
269
|
const styleEls = getPreviewStyles().map((style, i) => {
|
|
@@ -2,7 +2,6 @@ import React from 'react';
|
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import { css } from '@emotion/react';
|
|
4
4
|
import styled from '@emotion/styled';
|
|
5
|
-
import copyToClipboard from 'copy-text-to-clipboard';
|
|
6
5
|
import { isAbsolutePath } from 'decap-cms-lib-util';
|
|
7
6
|
import { buttons, shadows, zIndex } from 'decap-cms-ui-default';
|
|
8
7
|
|
|
@@ -87,10 +86,10 @@ export class CopyToClipBoardButton extends React.Component {
|
|
|
87
86
|
this.mounted = false;
|
|
88
87
|
}
|
|
89
88
|
|
|
90
|
-
handleCopy = () => {
|
|
89
|
+
handleCopy = async () => {
|
|
91
90
|
clearTimeout(this.timeout);
|
|
92
91
|
const { path, draft, name } = this.props;
|
|
93
|
-
|
|
92
|
+
await navigator.clipboard.writeText(isAbsolutePath(path) || !draft ? path : name);
|
|
94
93
|
this.setState({ copied: true });
|
|
95
94
|
this.timeout = setTimeout(() => this.mounted && this.setState({ copied: false }), 1500);
|
|
96
95
|
};
|
|
@@ -4,7 +4,6 @@ import { translate } from 'react-polyglot';
|
|
|
4
4
|
import styled from '@emotion/styled';
|
|
5
5
|
import yaml from 'yaml';
|
|
6
6
|
import truncate from 'lodash/truncate';
|
|
7
|
-
import copyToClipboard from 'copy-text-to-clipboard';
|
|
8
7
|
import { localForage } from 'decap-cms-lib-util';
|
|
9
8
|
import { buttons, colors } from 'decap-cms-ui-default';
|
|
10
9
|
|
|
@@ -117,7 +116,7 @@ function RecoveredEntry({ entry, t }) {
|
|
|
117
116
|
<hr />
|
|
118
117
|
<h2>{t('ui.errorBoundary.recoveredEntry.heading')}</h2>
|
|
119
118
|
<strong>{t('ui.errorBoundary.recoveredEntry.warning')}</strong>
|
|
120
|
-
<CopyButton onClick={() =>
|
|
119
|
+
<CopyButton onClick={() => navigator.clipboard.writeText(entry)}>
|
|
121
120
|
{t('ui.errorBoundary.recoveredEntry.copyButtonLabel')}
|
|
122
121
|
</CopyButton>
|
|
123
122
|
<pre>
|
|
@@ -72,6 +72,27 @@ describe('Frontmatter', () => {
|
|
|
72
72
|
});
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
+
it('should throw on duplicate frontmatter keys', () => {
|
|
76
|
+
expect(() =>
|
|
77
|
+
FrontmatterInfer.fromFile('---\ntitle: Hello\ntitle: World\n---\nContent'),
|
|
78
|
+
).toThrow(/Map keys must be unique/);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should throw on duplicate frontmatter keys with explicit YAML format', () => {
|
|
82
|
+
expect(() =>
|
|
83
|
+
frontmatterYAML().fromFile('---\ntitle: Hello\ntitle: World\n---\nContent'),
|
|
84
|
+
).toThrow(/Map keys must be unique/);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should not throw when body contains YAML-like patterns', () => {
|
|
88
|
+
expect(
|
|
89
|
+
FrontmatterInfer.fromFile('---\ntitle: Hello\n---\ntitle: this is not a duplicate'),
|
|
90
|
+
).toEqual({
|
|
91
|
+
title: 'Hello',
|
|
92
|
+
body: 'title: this is not a duplicate',
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
75
96
|
it('should stringify YAML with --- delimiters', () => {
|
|
76
97
|
expect(
|
|
77
98
|
FrontmatterInfer.toFile({
|
|
@@ -85,6 +85,33 @@ describe('yaml', () => {
|
|
|
85
85
|
time: '10:05',
|
|
86
86
|
});
|
|
87
87
|
});
|
|
88
|
+
|
|
89
|
+
test('throws on duplicate keys', () => {
|
|
90
|
+
expect(() => yaml.fromFile('title: Hello\ntitle: World')).toThrow(
|
|
91
|
+
/Map keys must be unique; "title" is repeated/,
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('throws on duplicate nested keys', () => {
|
|
96
|
+
expect(() => yaml.fromFile('nested:\n a: 1\n a: 2')).toThrow(
|
|
97
|
+
/Map keys must be unique; "a" is repeated/,
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('does not throw when same key appears in different nested objects', () => {
|
|
102
|
+
expect(yaml.fromFile('obj1:\n name: foo\nobj2:\n name: bar')).toEqual({
|
|
103
|
+
obj1: { name: 'foo' },
|
|
104
|
+
obj2: { name: 'bar' },
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('logs warnings to console.warn', () => {
|
|
109
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
110
|
+
// Valid YAML should produce no warnings
|
|
111
|
+
yaml.fromFile('title: Hello');
|
|
112
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
113
|
+
warnSpy.mockRestore();
|
|
114
|
+
});
|
|
88
115
|
});
|
|
89
116
|
describe('toFile', () => {
|
|
90
117
|
test('outputs valid yaml', () => {
|
package/src/formats/yaml.ts
CHANGED
|
@@ -41,7 +41,22 @@ export default {
|
|
|
41
41
|
if (content && content.trim().endsWith('---')) {
|
|
42
42
|
content = content.trim().slice(0, -3);
|
|
43
43
|
}
|
|
44
|
-
|
|
44
|
+
|
|
45
|
+
const doc = yaml.parseDocument(content, {
|
|
46
|
+
customTags: [timestampTag],
|
|
47
|
+
prettyErrors: true,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
for (const warn of doc.warnings) {
|
|
51
|
+
console.warn(`YAML warning: ${warn.message}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (doc.errors.length > 0) {
|
|
55
|
+
const messages = doc.errors.map(e => e.message).join('\n');
|
|
56
|
+
throw new Error(`YAML parsing error:\n${messages}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return doc.toJSON();
|
|
45
60
|
},
|
|
46
61
|
|
|
47
62
|
toFile(data: object, sortedKeys: string[] = [], comments: Record<string, string> = {}) {
|
|
@@ -247,6 +247,27 @@ describe('formatters', () => {
|
|
|
247
247
|
'Ignoring unknown variable “author-email” in open authoring message template.',
|
|
248
248
|
);
|
|
249
249
|
});
|
|
250
|
+
|
|
251
|
+
it('should return commit with trailer when signoff_commits is enabled', () => {
|
|
252
|
+
const collection = Map({ label_singular: 'Collection' });
|
|
253
|
+
const config = {
|
|
254
|
+
backend: {
|
|
255
|
+
signoff_commits: true,
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
expect(
|
|
260
|
+
commitMessageFormatter('create', config, {
|
|
261
|
+
slug: 'doc-slug',
|
|
262
|
+
path: 'file-path',
|
|
263
|
+
collection,
|
|
264
|
+
authorName: 'Test User',
|
|
265
|
+
authorEmail: 'test-user@example.org',
|
|
266
|
+
}),
|
|
267
|
+
).toEqual(
|
|
268
|
+
'Create Collection “doc-slug”\n\nSigned-off-by: Test User <test-user@example.org>\n',
|
|
269
|
+
);
|
|
270
|
+
});
|
|
250
271
|
});
|
|
251
272
|
|
|
252
273
|
describe('prepareSlug', () => {
|
package/src/lib/formatters.ts
CHANGED
|
@@ -44,16 +44,28 @@ type Options = {
|
|
|
44
44
|
collection?: Collection;
|
|
45
45
|
authorLogin?: string;
|
|
46
46
|
authorName?: string;
|
|
47
|
+
authorEmail?: string;
|
|
47
48
|
};
|
|
48
49
|
|
|
49
50
|
export function commitMessageFormatter(
|
|
50
51
|
type: keyof typeof commitMessageTemplates,
|
|
51
52
|
config: CmsConfig,
|
|
52
|
-
{ slug, path, collection, authorLogin, authorName }: Options,
|
|
53
|
+
{ slug, path, collection, authorLogin, authorName, authorEmail }: Options,
|
|
53
54
|
isOpenAuthoring?: boolean,
|
|
54
55
|
) {
|
|
55
56
|
const templates = { ...commitMessageTemplates, ...(config.backend.commit_messages || {}) };
|
|
56
57
|
|
|
58
|
+
let trailers = '';
|
|
59
|
+
if (config.backend.signoff_commits) {
|
|
60
|
+
if (!authorName) {
|
|
61
|
+
console.warn('Option signoff_commits is enabled, but author name is unknown');
|
|
62
|
+
} else if (!authorEmail) {
|
|
63
|
+
console.warn('Option signoff_commits is enabled, but author email is unknown');
|
|
64
|
+
} else {
|
|
65
|
+
trailers = `\n\nSigned-off-by: ${authorName} <${authorEmail}>\n`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
57
69
|
const commitMessage = templates[type].replace(variableRegex, (_, variable) => {
|
|
58
70
|
switch (variable) {
|
|
59
71
|
case 'slug':
|
|
@@ -73,7 +85,7 @@ export function commitMessageFormatter(
|
|
|
73
85
|
});
|
|
74
86
|
|
|
75
87
|
if (!isOpenAuthoring) {
|
|
76
|
-
return commitMessage;
|
|
88
|
+
return commitMessage + trailers;
|
|
77
89
|
}
|
|
78
90
|
|
|
79
91
|
const message = templates.openAuthoring.replace(variableRegex, (_, variable) => {
|
|
@@ -90,7 +102,7 @@ export function commitMessageFormatter(
|
|
|
90
102
|
}
|
|
91
103
|
});
|
|
92
104
|
|
|
93
|
-
return message;
|
|
105
|
+
return message + trailers;
|
|
94
106
|
}
|
|
95
107
|
|
|
96
108
|
export function prepareSlug(slug: string) {
|
package/src/lib/i18n.ts
CHANGED
|
@@ -308,18 +308,26 @@ export async function getI18nEntry(
|
|
|
308
308
|
if (structure === I18N_STRUCTURE.SINGLE_FILE) {
|
|
309
309
|
entryValue = mergeSingleFileValue(await getEntryValue(path), defaultLocale, locales);
|
|
310
310
|
} else {
|
|
311
|
-
const
|
|
311
|
+
const entryValuesResults = await Promise.allSettled(
|
|
312
312
|
locales.map(async locale => {
|
|
313
313
|
const entryPath = getFilePath(structure, extension, path, slug, locale);
|
|
314
|
-
const value = await getEntryValue(entryPath)
|
|
314
|
+
const value = await getEntryValue(entryPath);
|
|
315
315
|
return { value, locale };
|
|
316
316
|
}),
|
|
317
317
|
);
|
|
318
318
|
|
|
319
|
-
const nonNullValues =
|
|
320
|
-
value:
|
|
321
|
-
locale: string;
|
|
322
|
-
|
|
319
|
+
const nonNullValues = entryValuesResults
|
|
320
|
+
.map(e => (e.status === 'fulfilled' ? e.value : undefined))
|
|
321
|
+
.filter((e): e is { value: EntryValue; locale: string } => e !== undefined);
|
|
322
|
+
|
|
323
|
+
if (nonNullValues.length === 0) {
|
|
324
|
+
// mergeValues will throw on an empty list, and show the error messages.
|
|
325
|
+
const [error = new Error('No entry values found for any locale')] = entryValuesResults
|
|
326
|
+
.map(e => (e.status === 'rejected' ? e.reason : undefined))
|
|
327
|
+
.filter(e => e !== undefined);
|
|
328
|
+
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
323
331
|
|
|
324
332
|
entryValue = mergeValues(collection, structure, defaultLocale, nonNullValues);
|
|
325
333
|
}
|