diff --git a/dev-test/config.yml b/dev-test/config.yml
index 2dd82bcac504..0361bf29e9bd 100644
--- a/dev-test/config.yml
+++ b/dev-test/config.yml
@@ -1,11 +1,16 @@
backend:
- name: test-repo
+ name: github
+ repo: bastiaan02/jekyll-base
site_url: https://example.com
publish_mode: editorial_workflow
media_folder: assets/uploads
+locales:
+ - en
+ - fr
+
collections: # A list of collections the CMS should be able to edit
- name: 'posts' # Used in routes, ie.: /admin/collections/:slug/edit
label: 'Posts' # Used in the UI
@@ -45,7 +50,22 @@ collections: # A list of collections the CMS should be able to edit
tagname: ''
- { label: 'Body', name: 'body', widget: 'markdown', hint: 'Main content goes here.' }
-
+ - name: 'blog' # Used in routes, ie.: /admin/collections/:slug/edit
+ label: 'BLOG' # Used in the UI
+ folder: '_blogs'
+ i18n_structure: locale_file_extensions
+ create: true # Allow users to create new documents in this collection
+ fields: # The fields each document in this collection have
+ - { label: 'Title', name: 'title', widget: 'string', translatable: true, tagname: 'h1' }
+ - { label: 'Answer', name: 'body', widget: 'markdown', translatable: true }
+ - name: 'product' # Used in routes, ie.: /admin/collections/:slug/edit
+ label: 'PRODUCT' # Used in the UI
+ folder: '_products'
+ i18n_structure: locale_folders
+ create: true # Allow users to create new documents in this collection
+ fields: # The fields each document in this collection have
+ - { label: 'Title', name: 'title', widget: 'string', translatable: true, tagname: 'h1' }
+ - { label: 'Description', name: 'body', widget: 'markdown', translatable: true }
- name: 'faq' # Used in routes, ie.: /admin/collections/:slug/edit
label: 'FAQ' # Used in the UI
folder: '_faqs'
diff --git a/packages/netlify-cms-backend-github/src/__tests__/API.spec.js b/packages/netlify-cms-backend-github/src/__tests__/API.spec.js
index 8eed28041d35..fa84c1521454 100644
--- a/packages/netlify-cms-backend-github/src/__tests__/API.spec.js
+++ b/packages/netlify-cms-backend-github/src/__tests__/API.spec.js
@@ -200,7 +200,7 @@ describe('github API', () => {
path: 'content/posts/new-post.md',
raw: 'content',
};
- await api.persistFiles(entry, [], { commitMessage: 'commitMessage' });
+ await api.persistFiles([entry], [], { commitMessage: 'commitMessage' });
expect(api.request).toHaveBeenCalledTimes(5);
diff --git a/packages/netlify-cms-backend-test/src/implementation.ts b/packages/netlify-cms-backend-test/src/implementation.ts
index c3127fb7ae27..b6b14b687438 100644
--- a/packages/netlify-cms-backend-test/src/implementation.ts
+++ b/packages/netlify-cms-backend-test/src/implementation.ts
@@ -295,11 +295,8 @@ export default class TestBackend implements Implementation {
};
}
- async persistEntry(
- { path, raw, slug, newPath }: Entry,
- assetProxies: AssetProxy[],
- options: PersistOptions,
- ) {
+ async persistEntry(entries: Entry, assetProxies: AssetProxy[], options: PersistOptions) {
+ const { path, raw, slug, newPath } = entries[0];
if (options.useWorkflow) {
const key = `${options.collectionName}/${slug}`;
const currentEntry = window.repoFilesUnpublished[key];
diff --git a/packages/netlify-cms-core/src/__tests__/backend.spec.js b/packages/netlify-cms-core/src/__tests__/backend.spec.js
index 9e8322766a28..653ef95a41d1 100644
--- a/packages/netlify-cms-core/src/__tests__/backend.spec.js
+++ b/packages/netlify-cms-core/src/__tests__/backend.spec.js
@@ -790,7 +790,7 @@ describe('Backend', () => {
},
],
});
- });
+ });
});
describe('getMultipleEntries', () => {
diff --git a/packages/netlify-cms-core/src/actions/__tests__/config.spec.js b/packages/netlify-cms-core/src/actions/__tests__/config.spec.js
index f7b5595c1129..9af4573f0c67 100644
--- a/packages/netlify-cms-core/src/actions/__tests__/config.spec.js
+++ b/packages/netlify-cms-core/src/actions/__tests__/config.spec.js
@@ -1,10 +1,12 @@
import { fromJS } from 'immutable';
-<<<<<<< HEAD
import { stripIndent } from 'common-tags';
-import { parseConfig, applyDefaults, detectProxyServer, handleLocalBackend } from '../config';
-=======
-import { applyDefaults, detectProxyServer, handleLocalBackend, addLocaleFields } from '../config';
->>>>>>> 6f4b539c... fix: add unit tests
+import {
+ parseConfig,
+ applyDefaults,
+ detectProxyServer,
+ handleLocalBackend,
+ addLocaleFields,
+} from '../config';
jest.spyOn(console, 'log').mockImplementation(() => {});
jest.mock('coreSrc/backend', () => {
@@ -582,9 +584,9 @@ describe('config', () => {
describe('addLocaleFields', () => {
it('should add locale fields', () => {
const fields = fromJS([
- { name: 'title', widget: 'string' },
+ { name: 'title', widget: 'string', translatable: true },
{ name: 'date', widget: 'date' },
- { name: 'content', widget: 'markdown' },
+ { name: 'content', widget: 'markdown', translatable: true },
]);
const actual = addLocaleFields(fields, ['en', 'fr']);
@@ -596,9 +598,9 @@ describe('config', () => {
widget: 'object',
multiContentId: Symbol.for('multiContentId'),
fields: [
- { name: 'title', widget: 'string' },
+ { name: 'title', widget: 'string', translatable: true },
{ name: 'date', widget: 'date' },
- { name: 'content', widget: 'markdown' },
+ { name: 'content', widget: 'markdown', translatable: true },
],
},
{
@@ -607,8 +609,8 @@ describe('config', () => {
widget: 'object',
multiContentId: Symbol.for('multiContentId'),
fields: [
- { name: 'title', widget: 'string' },
- { name: 'content', widget: 'markdown' },
+ { name: 'title', widget: 'string', translatable: true },
+ { name: 'content', widget: 'markdown', translatable: true },
],
},
]),
diff --git a/packages/netlify-cms-core/src/actions/config.js b/packages/netlify-cms-core/src/actions/config.js
index 0e2cdfd9678a..272d3343dcb2 100644
--- a/packages/netlify-cms-core/src/actions/config.js
+++ b/packages/netlify-cms-core/src/actions/config.js
@@ -1,11 +1,16 @@
import yaml from 'yaml';
-import { Map, fromJS } from 'immutable';
-import { trimStart, trim, get, isPlainObject } from 'lodash';
+import { Map, List, fromJS } from 'immutable';
+import { trimStart, trim, get, isPlainObject, uniq, isEmpty } from 'lodash';
import { authenticateUser } from 'Actions/auth';
import * as publishModes from 'Constants/publishModes';
import { validateConfig } from 'Constants/configSchema';
-import { selectDefaultSortableFields, traverseFields, selectIdentifier } from '../reducers/collections';
-import { NON_TRANSLATABLE_FIELDS } from 'Constants/multiContentTypes';
+import {
+ selectDefaultSortableFields,
+ traverseFields,
+ selectIdentifier,
+} from '../reducers/collections';
+import { resolveBackend } from 'coreSrc/backend';
+import { DIFF_FILE_TYPES } from 'Constants/multiContentTypes';
export const CONFIG_REQUEST = 'CONFIG_REQUEST';
export const CONFIG_SUCCESS = 'CONFIG_SUCCESS';
@@ -124,6 +129,9 @@ export function applyDefaults(config) {
if (backend === 'test-repo') {
collection = collection.set('i18n_structure', 'single_file');
}
+ if (DIFF_FILE_TYPES.includes(collection.get('i18n_structure'))) {
+ collection = collection.set('multi_content_diff_files', true);
+ }
}
}
@@ -190,7 +198,6 @@ export function addLocaleFields(fields, locales) {
function stripNonTranslatableFields(fields) {
return fields.reduce((acc, item) => {
const subfields = item.get('field') || item.get('fields');
- const widget = item.get('widget');
if (List.isList(subfields)) {
return acc.push(item.set('fields', stripNonTranslatableFields(subfields)));
@@ -200,7 +207,7 @@ function stripNonTranslatableFields(fields) {
return acc.push(item.set('field', stripNonTranslatableFields([subfields])));
}
- if (!NON_TRANSLATABLE_FIELDS.includes(widget)) {
+ if (item.get('translatable')) {
return acc.push(item);
}
diff --git a/packages/netlify-cms-core/src/actions/editorialWorkflow.ts b/packages/netlify-cms-core/src/actions/editorialWorkflow.ts
index cc30bb223fbb..7704fbd83ecc 100644
--- a/packages/netlify-cms-core/src/actions/editorialWorkflow.ts
+++ b/packages/netlify-cms-core/src/actions/editorialWorkflow.ts
@@ -23,7 +23,6 @@ import {
createDraftFromEntry,
loadEntries,
} from './entries';
-import { DIFF_FILE_TYPES } from 'Constants/multiContentTypes';
import { createAssetProxy } from '../valueObjects/AssetProxy';
import { addAssets } from './media';
import { loadMedia } from './mediaLibrary';
@@ -282,7 +281,7 @@ export function loadUnpublishedEntry(collection: Collection, slug: string) {
dispatch(unpublishedEntryLoading(collection, slug));
try {
- const entry = (await backend.unpublishedEntry(state, collection, slug)) as EntryValue;
+ let entry = (await backend.unpublishedEntry(state, collection, slug)) as EntryValue;
const assetProxies = await Promise.all(
entry.mediaFiles
.filter(file => file.draft)
@@ -296,7 +295,7 @@ export function loadUnpublishedEntry(collection: Collection, slug: string) {
);
dispatch(addAssets(assetProxies));
- if (multiContent && DIFF_FILE_TYPES.includes(i18nStructure)) {
+ if (collection.get('multi_content_diff_files')) {
const publishedEntries = get(
getState().entries.toJS(),
`pages.${collection.get('name')}.ids`,
diff --git a/packages/netlify-cms-core/src/actions/entries.ts b/packages/netlify-cms-core/src/actions/entries.ts
index 472135a24a70..8515e1854a2a 100644
--- a/packages/netlify-cms-core/src/actions/entries.ts
+++ b/packages/netlify-cms-core/src/actions/entries.ts
@@ -11,7 +11,6 @@ import { Cursor, ImplementationMediaFile } from 'netlify-cms-lib-util';
import { createEntry, EntryValue } from '../valueObjects/Entry';
import AssetProxy, { createAssetProxy } from '../valueObjects/AssetProxy';
import ValidationErrorTypes from '../constants/validationErrorTypes';
-import { DIFF_FILE_TYPES } from 'Constants/multiContentTypes';
import { addAssets, getAsset } from './media';
import {
Collection,
@@ -461,7 +460,7 @@ export function loadEntry(collection: Collection, slug: string) {
dispatch(entryLoading(collection, slug));
try {
- const loadedEntry = await tryLoadEntry(getState(), collection, slug, locales, multiContent);
+ const loadedEntry = await tryLoadEntry(getState(), collection, slug);
dispatch(entryLoaded(collection, loadedEntry));
dispatch(createDraftFromEntry(loadedEntry));
} catch (error) {
@@ -481,21 +480,9 @@ export function loadEntry(collection: Collection, slug: string) {
};
}
-export async function tryLoadEntry(
- state: State,
- collection: Collection,
- slug: string,
- locales,
- multiContent
-) {
+export async function tryLoadEntry(state: State, collection: Collection, slug: string) {
const backend = currentBackend(state.config);
- const loadedEntry = await backend.getEntry(
- state,
- collection,
- slug,
- locales,
- multiContent
- );
+ const loadedEntry = await backend.getEntry(state, collection, slug);
return loadedEntry;
}
@@ -526,8 +513,6 @@ export function loadEntries(collection: Collection, page = 0) {
}
const backend = currentBackend(state.config);
- const multiContent = collection.get('multi_content');
- const i18nStructure = collection.get('i18n_structure');
const integration = selectIntegration(state, collection.get('name'), 'listEntries');
const provider = integration
? getIntegrationProvider(state.integrations, backend.getToken, integration)
@@ -543,8 +528,8 @@ export function loadEntries(collection: Collection, page = 0) {
} = await (collection.has('nested')
? // nested collections require all entries to construct the tree
provider.listAllEntries(collection).then((entries: EntryValue[]) => ({ entries }))
- : locales && DIFF_FILE_TYPES.includes(multiContent)
- ? provider.listAllMultipleEntires(collection, locales)
+ : collection.get('multi_content_diff_files')
+ ? provider.listAllMultipleEntires(collection)
: provider.listEntries(collection, page));
response = {
...response,
diff --git a/packages/netlify-cms-core/src/backend.ts b/packages/netlify-cms-core/src/backend.ts
index a70ac72e8560..fdde8ed80ce7 100644
--- a/packages/netlify-cms-core/src/backend.ts
+++ b/packages/netlify-cms-core/src/backend.ts
@@ -54,17 +54,13 @@ import {
} from './types/redux';
import AssetProxy from './valueObjects/AssetProxy';
import { FOLDER, FILES } from './constants/collectionTypes';
-<<<<<<< HEAD
import { selectCustomPath } from './reducers/entryDraft';
import { UnpublishedEntry } from 'netlify-cms-lib-util/src/implementation';
-import { SAME_FOLDER, DIFF_FOLDER, DIFF_FILE_TYPES } from 'Constants/multiContentTypes';
-=======
import {
LOCALE_FILE_EXTENSIONS,
LOCALE_FOLDERS,
DIFF_FILE_TYPES,
} from 'Constants/multiContentTypes';
->>>>>>> 99de665d... fix: feedback changes
const { extractTemplateVars, dateParsers } = stringTemplate;
@@ -425,7 +421,7 @@ export class Backend {
// for local searches and queries.
async listAllEntries(collection: Collection, depth: number) {
const selectedDepth =
- depth ||
+ depth ||
collection.get('nested')?.get('depth') ||
getPathDepth(collection.get('path', '') as string);
@@ -480,6 +476,7 @@ export class Backend {
};
});
}
+
return { entries: this.combineMultipleContentEntries(multiEntries) };
}
@@ -683,35 +680,19 @@ export class Backend {
raw: loadedEntry.data,
label,
i18nStructure,
+ slugWithLocale: selectEntrySlug(collection, loadedEntry.file.path),
mediaFiles: [],
meta: { path: prepareMetaPath(loadedEntry.file.path, collection) },
});
-<<<<<<< HEAD
entry = this.entryWithFormat(collection)(entry);
entry = await this.processEntry(state, collection, entry);
-=======
- const entryWithFormat = this.entryWithFormat(collection)(entry);
- if (!mediaFiles) {
- const mediaFolders = selectMediaFolders(state, collection, fromJS(entryWithFormat));
- if (mediaFolders.length > 0 && !integration) {
- mediaFiles = [];
- for (const folder of mediaFolders) {
- mediaFiles = [...entry.mediaFiles, ...(await this.implementation.getMedia(folder))];
- }
- } else {
- mediaFiles = state.mediaLibrary.get('files') || [];
- }
- }
- entry.mediaFiles = mediaFiles;
- entry.slugWithLocale = selectEntrySlug(collection, entry.path);
->>>>>>> 99de665d... fix: feedback changes
return entry;
}),
);
- if (multiContent && DIFF_FILE_TYPES.includes(i18nStructure)) {
+ if (collection.get('multi_content_diff_files')) {
return this.combineEntries(entries);
}
@@ -762,20 +743,53 @@ export class Backend {
} else {
extension = selectFolderEntryExtension(collection);
}
+
+ const mediaFiles: MediaFile[] = [];
+ if (withMediaFiles) {
+ const nonDataFiles = entryData.diffs.filter(d => !d.path.endsWith(extension));
+ const files = await Promise.all(
+ nonDataFiles.map(f =>
+ this.implementation!.unpublishedEntryMediaFile(
+ collection.get('name'),
+ slug,
+ f.path,
+ f.id,
+ ),
+ ),
+ );
+ mediaFiles.push(...files.map(f => ({ ...f, draft: true })));
+ }
+
const dataFiles = sortBy(
entryData.diffs.filter(d => d.path.endsWith(extension)),
f => f.path.length,
);
- // if the unpublished entry has no diffs, return the original
+
let data = '';
+ let entryWithFormat;
let newFile = false;
let path = slug;
+ // if the unpublished entry has no diffs, return the original
if (dataFiles.length <= 0) {
const loadedEntry = await this.implementation.getEntry(
selectEntryPath(collection, slug) as string,
);
data = loadedEntry.data;
path = loadedEntry.file.path;
+ } else if (collection.get('multi_content_diff_files')) {
+ data = await Promise.all(
+ dataFiles.map(async file => {
+ const data = await this.implementation.unpublishedEntryDataFile(
+ collection.get('name'),
+ entryData.slug,
+ file.path,
+ file.id,
+ );
+ return { data, path: file.path };
+ }),
+ );
+ newFile = dataFiles[0].newFile;
+ path = dataFiles[0].path;
} else {
const entryFile = dataFiles[0];
data = await this.implementation.unpublishedEntryDataFile(
@@ -788,32 +802,38 @@ export class Backend {
path = entryFile.path;
}
- const mediaFiles: MediaFile[] = [];
- if (withMediaFiles) {
- const nonDataFiles = entryData.diffs.filter(d => !d.path.endsWith(extension));
- const files = await Promise.all(
- nonDataFiles.map(f =>
- this.implementation!.unpublishedEntryMediaFile(
- collection.get('name'),
- slug,
- f.path,
- f.id,
- ),
- ),
- );
- mediaFiles.push(...files.map(f => ({ ...f, draft: true })));
+ if (Array.isArray(data)) {
+ const multipleEntries = data.map(d => {
+ return this.entryWithFormat(collection)(
+ createEntry(collection.get('name'), slug, path, {
+ raw: d.data,
+ isModification: !newFile,
+ label: collection && selectFileEntryLabel(collection, slug),
+ mediaFiles,
+ updatedOn: entryData.updatedAt,
+ status: entryData.status,
+ meta: { path: prepareMetaPath(path, collection) },
+ i18nStructure: collection.get('i18n_structure'),
+ slugWithLocale: selectEntrySlug(collection, d.path),
+ }),
+ );
+ });
+
+ entryWithFormat = this.combineEntries(multipleEntries);
+ } else {
+ const entry = createEntry(collection.get('name'), slug, path, {
+ raw: data,
+ isModification: !newFile,
+ label: collection && selectFileEntryLabel(collection, slug),
+ mediaFiles,
+ updatedOn: entryData.updatedAt,
+ status: entryData.status,
+ meta: { path: prepareMetaPath(path, collection) },
+ });
+
+ entryWithFormat = this.entryWithFormat(collection)(entry);
}
- const entry = createEntry(collection.get('name'), slug, path, {
- raw: data,
- isModification: !newFile,
- label: collection && selectFileEntryLabel(collection, slug),
- mediaFiles,
- updatedOn: entryData.updatedAt,
- status: entryData.status,
- meta: { path: prepareMetaPath(path, collection) },
- });
- const entryWithFormat = this.entryWithFormat(collection)(entry);
return entryWithFormat;
}
@@ -978,8 +998,6 @@ export class Backend {
const entryDraft = (modifiedData && draft.setIn(['entry', 'data'], modifiedData)) || draft;
const newEntry = entryDraft.getIn(['entry', 'newRecord']) || false;
- const MultiContentDiffFiles =
- DIFF_FILE_TYPES.includes(collection.get('i18n_structure')) && collection.get('multi_content');
const useWorkflow = selectUseWorkflow(config);
@@ -1034,7 +1052,7 @@ export class Backend {
}
let entriesObj = [entryObj];
- if (MultiContentDiffFiles) {
+ if (collection.get('multi_content_diff_files')) {
entriesObj = this.getMultipleEntries(collection, entryDraft, entryObj);
}
@@ -1161,8 +1179,6 @@ export class Backend {
const config = state.config;
const path = selectEntryPath(collection, slug) as string;
const extension = selectFolderEntryExtension(collection) as string;
- const MultiContentDiffFiles =
- DIFF_FILE_TYPES.includes(collection.get('i18n_structure')) && collection.get('multi_content');
const locales = collection.get('locales');
if (!selectAllowDeletion(collection)) {
@@ -1185,7 +1201,7 @@ export class Backend {
const entry = selectEntry(state.entries, collection.get('name'), slug);
await this.invokePreUnpublishEvent(entry);
- if (MultiContentDiffFiles) {
+ if (collection.get('multi_content_diff_files')) {
const i18nStructure = collection.get('i18n_structure');
if (i18nStructure === LOCALE_FILE_EXTENSIONS) {
for (const l of locales) {
diff --git a/packages/netlify-cms-core/src/components/Editor/EditorControlPane/EditorControl.js b/packages/netlify-cms-core/src/components/Editor/EditorControlPane/EditorControl.js
index 558b7d7651c5..ed2ee887f6c3 100644
--- a/packages/netlify-cms-core/src/components/Editor/EditorControlPane/EditorControl.js
+++ b/packages/netlify-cms-core/src/components/Editor/EditorControlPane/EditorControl.js
@@ -111,6 +111,20 @@ export const ControlHint = styled.p`
transition: color ${transitions.main};
`;
+const LocaleDropdown = ({ locales, selectedLocale, onLocaleChange }) => {
+ return (
+ {selectedLocale}}
+ dropdownTopOverlap="30px"
+ dropdownWidth="100px"
+ >
+ {locales.map(l => (
+ onLocaleChange(l)} />
+ ))}
+
+ );
+};
+
class EditorControl extends React.Component {
static propTypes = {
value: PropTypes.oneOfType([
@@ -197,7 +211,6 @@ class EditorControl extends React.Component {
isEditorComponent,
isNewEditorComponent,
parentIds,
- locales,
selectedLocale,
onLocaleChange,
t,
@@ -216,23 +229,16 @@ class EditorControl extends React.Component {
const hasErrors = !!errors || childErrors;
const multiContentWidgetId = field.get('multiContentId') === Symbol.for('multiContentId');
const locales = this.props.collection.get('locales');
- const label = (
- <>
- {locales && multiContentWidgetId ? (
- {selectedLocale}}
- dropdownTopOverlap="30px"
- dropdownWidth="100px"
- >
- {locales.map(l => (
- onLocaleChange(l)} />
- ))}
-
- ) : (
- `${field.get('label', field.get('name'))}`
- )}
- >
- );
+ const label =
+ locales && multiContentWidgetId ? (
+
+ ) : (
+ `${field.get('label', field.get('name'))}`
+ );
return (
diff --git a/packages/netlify-cms-core/src/constants/multiContentTypes.js b/packages/netlify-cms-core/src/constants/multiContentTypes.js
index 4759f30bf50f..b4f6e49ec250 100644
--- a/packages/netlify-cms-core/src/constants/multiContentTypes.js
+++ b/packages/netlify-cms-core/src/constants/multiContentTypes.js
@@ -2,7 +2,6 @@ export const SINGLE_FILE = 'single_file';
export const LOCALE_FILE_EXTENSIONS = 'locale_file_extensions';
export const LOCALE_FOLDERS = 'locale_folders';
export const DIFF_FILE_TYPES = [LOCALE_FILE_EXTENSIONS, LOCALE_FOLDERS];
-export const NON_TRANSLATABLE_FIELDS = ['date', 'datetime', 'map', 'code', 'number'];
export const locales = [
'aa',
diff --git a/packages/netlify-cms-core/src/valueObjects/Entry.ts b/packages/netlify-cms-core/src/valueObjects/Entry.ts
index 056baa03c3ae..b50ea6e4dc34 100644
--- a/packages/netlify-cms-core/src/valueObjects/Entry.ts
+++ b/packages/netlify-cms-core/src/valueObjects/Entry.ts
@@ -32,6 +32,7 @@ export interface EntryValue {
meta: { path?: string };
i18nStructure?: string;
contentKey?: string;
+ slugWithLocale?: string;
}
export function createEntry(collection: string, slug = '', path = '', options: Options = {}) {
@@ -55,6 +56,9 @@ export function createEntry(collection: string, slug = '', path = '', options: O
...(options.i18nStructure && {
i18nStructure: options.i18nStructure,
}),
+ ...(options.slugWithLocale && {
+ slugWithLocale: options.slugWithLocale,
+ }),
};
return returnObj;