Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/multi content authoring #3366

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/netlify-cms-backend-bitbucket/src/API.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,8 +499,9 @@ export default class API {
return files;
}

async persistFiles(entry: Entry | null, mediaFiles: AssetProxy[], options: PersistOptions) {
const files = entry ? [entry, ...mediaFiles] : mediaFiles;
async persistFiles(entries: Entry[] | null, mediaFiles: AssetProxy[], options: PersistOptions) {
const files = entries ? [...entries, ...mediaFiles] : mediaFiles;
const entry = entries ? entries[0] : null;
if (options.useWorkflow) {
return this.editorialWorkflowGit(files, entry as Entry, options);
} else {
Expand Down
4 changes: 2 additions & 2 deletions packages/netlify-cms-backend-bitbucket/src/implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,14 +428,14 @@ export default class BitbucketBackend implements Implementation {
};
}

async persistEntry(entry: Entry, mediaFiles: AssetProxy[], options: PersistOptions) {
async persistEntry(entries: Entry[], mediaFiles: AssetProxy[], options: PersistOptions) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably rename persistEntry to persistEntries or entries to entryFiles.
Or better - pass a new entry type that has a files list (containing both data files and media files).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would go with persistEntries

Or better - pass a new entry type that has a files list (containing both data files and media files).

new entry type name ?

const client = await this.getLargeMediaClient();
// persistEntry is a transactional operation
return runWithLock(
this.lock,
async () =>
this.api!.persistFiles(
entry,
entries,
client.enabled ? await getLargeMediaFilteredMediaFiles(client, mediaFiles) : mediaFiles,
options,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -531,10 +531,10 @@ export default class GitGateway implements Implementation {
return this.backend!.getMediaFile(path);
}

async persistEntry(entry: Entry, mediaFiles: AssetProxy[], options: PersistOptions) {
async persistEntry(entries: Entry[], mediaFiles: AssetProxy[], options: PersistOptions) {
const client = await this.getLargeMediaClient();
return this.backend!.persistEntry(
entry,
entries,
client.enabled ? await getLargeMediaFilteredMediaFiles(client, mediaFiles) : mediaFiles,
options,
);
Expand Down
5 changes: 3 additions & 2 deletions packages/netlify-cms-backend-github/src/API.ts
Original file line number Diff line number Diff line change
Expand Up @@ -870,8 +870,9 @@ export default class API {
}));
}

async persistFiles(entry: Entry | null, mediaFiles: AssetProxy[], options: PersistOptions) {
const files = entry ? mediaFiles.concat(entry) : mediaFiles;
async persistFiles(entries: Entry[] | null, mediaFiles: AssetProxy[], options: PersistOptions) {
const files = entries ? mediaFiles.concat(entries) : mediaFiles;
const entry = entries ? entries[0] : null;
const uploadPromises = files.map(file => this.uploadBlob(file));
await Promise.all(uploadPromises);

Expand Down
4 changes: 2 additions & 2 deletions packages/netlify-cms-backend-github/src/__tests__/API.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -280,7 +280,7 @@ describe('github API', () => {
},
];

await api.persistFiles(entry, mediaFiles, { useWorkflow: true });
await api.persistFiles([entry], mediaFiles, { useWorkflow: true });

expect(api.uploadBlob).toHaveBeenCalledTimes(3);
expect(api.uploadBlob).toHaveBeenCalledWith(entry);
Expand Down
4 changes: 2 additions & 2 deletions packages/netlify-cms-backend-github/src/implementation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -470,11 +470,11 @@ export default class GitHub implements Implementation {
);
}

persistEntry(entry: Entry, mediaFiles: AssetProxy[] = [], options: PersistOptions) {
persistEntry(entries: Entry[], mediaFiles: AssetProxy[] = [], options: PersistOptions) {
// persistEntry is a transactional operation
return runWithLock(
this.lock,
() => this.api!.persistFiles(entry, mediaFiles, options),
() => this.api!.persistFiles(entries, mediaFiles, options),
'Failed to acquire persist entry lock',
);
}
Expand Down
5 changes: 3 additions & 2 deletions packages/netlify-cms-backend-gitlab/src/API.ts
Original file line number Diff line number Diff line change
Expand Up @@ -512,8 +512,9 @@ export default class API {
return items;
}

async persistFiles(entry: Entry | null, mediaFiles: AssetProxy[], options: PersistOptions) {
const files = entry ? [entry, ...mediaFiles] : mediaFiles;
async persistFiles(entries: Entry[] | null, mediaFiles: AssetProxy[], options: PersistOptions) {
const files = entries ? [...entries, ...mediaFiles] : mediaFiles;
const entry = entries ? entries[0] : null;
if (options.useWorkflow) {
return this.editorialWorkflowGit(files, entry as Entry, options);
} else {
Expand Down
4 changes: 2 additions & 2 deletions packages/netlify-cms-backend-gitlab/src/implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,11 +260,11 @@ export default class GitLab implements Implementation {
};
}

async persistEntry(entry: Entry, mediaFiles: AssetProxy[], options: PersistOptions) {
async persistEntry(entries: Entry[], mediaFiles: AssetProxy[], options: PersistOptions) {
// persistEntry is a transactional operation
return runWithLock(
this.lock,
() => this.api!.persistFiles(entry, mediaFiles, options),
() => this.api!.persistFiles(entries, mediaFiles, options),
'Failed to acquire persist entry lock',
);
}
Expand Down
5 changes: 3 additions & 2 deletions packages/netlify-cms-backend-proxy/src/implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,14 @@ export default class ProxyBackend implements Implementation {
});
}

async persistEntry(entry: Entry, assetProxies: AssetProxy[], options: PersistOptions) {
async persistEntry(entries: Entry[], assetProxies: AssetProxy[], options: PersistOptions) {
const assets = await Promise.all(assetProxies.map(serializeAsset));
return this.request({
action: 'persistEntry',
params: {
branch: this.branch,
entry,
entry: entries[0],
entries,
assets,
options: { ...options, status: options.status || this.options.initialWorkflowStatus },
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describe('test backend implementation', () => {
const backend = new TestBackend({});

const entry = { path: 'posts/some-post.md', raw: 'content', slug: 'some-post.md' };
await backend.persistEntry(entry, [], { newEntry: true });
await backend.persistEntry([entry], [], { newEntry: true });

expect(window.repoFiles).toEqual({
posts: {
Expand Down Expand Up @@ -81,7 +81,7 @@ describe('test backend implementation', () => {
const backend = new TestBackend({});

const entry = { path: 'posts/new-post.md', raw: 'content', slug: 'new-post.md' };
await backend.persistEntry(entry, [], { newEntry: true });
await backend.persistEntry([entry], [], { newEntry: true });

expect(window.repoFiles).toEqual({
pages: {
Expand Down Expand Up @@ -109,7 +109,7 @@ describe('test backend implementation', () => {
const slug = 'dir1/dir2/some-post.md';
const path = `posts/${slug}`;
const entry = { path, raw: 'content', slug };
await backend.persistEntry(entry, [], { newEntry: true });
await backend.persistEntry([entry], [], { newEntry: true });

expect(window.repoFiles).toEqual({
posts: {
Expand Down Expand Up @@ -144,7 +144,7 @@ describe('test backend implementation', () => {
const slug = 'dir1/dir2/some-post.md';
const path = `posts/${slug}`;
const entry = { path, raw: 'new content', slug };
await backend.persistEntry(entry, [], { newEntry: false });
await backend.persistEntry([entry], [], { newEntry: false });

expect(window.repoFiles).toEqual({
posts: {
Expand Down
7 changes: 2 additions & 5 deletions packages/netlify-cms-backend-test/src/implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
208 changes: 208 additions & 0 deletions packages/netlify-cms-core/src/__tests__/backend.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -940,4 +940,212 @@ describe('Backend', () => {
]);
});
});

describe('mergeMultipleContentEntries', () => {
const implementation = {
init: jest.fn(() => implementation),
};
const config = Map({});
const backend = new Backend(implementation, { config, backendName: 'github' });

it('should combine multiple content same folder entries', () => {
const entries = [
{
path: 'posts/post.en.md',
data: { title: 'Title en', content: 'Content en' },
i18nStructure: 'locale_file_extensions',
locale: 'en',
contentKey: 'posts/post',
},
{
path: 'posts/post.fr.md',
data: { title: 'Title fr', content: 'Content fr' },
i18nStructure: 'locale_file_extensions',
locale: 'fr',
contentKey: 'posts/post',
},
];

expect(backend.mergeMultipleContentEntries(entries)).toEqual([
{
path: 'posts/post.md',
multiContent: true,
raw: '',
data: {
en: { title: 'Title en', content: 'Content en' },
fr: { title: 'Title fr', content: 'Content fr' },
},
},
]);
});

it('should combine multiple content different folder entries', () => {
const entries = [
{
path: 'posts/en/post.md',
data: { title: 'Title en', content: 'Content en' },
i18nStructure: 'locale_folders',
locale: 'en',
contentKey: 'posts/post',
},
{
path: 'posts/fr/post.md',
data: { title: 'Title fr', content: 'Content fr' },
i18nStructure: 'locale_folders',
locale: 'fr',
contentKey: 'posts/post',
},
];

expect(backend.mergeMultipleContentEntries(entries)).toEqual([
{
path: 'posts/post.md',
multiContent: true,
raw: '',
data: {
en: { title: 'Title en', content: 'Content en' },
fr: { title: 'Title fr', content: 'Content fr' },
},
},
]);
});
});

describe('listAllMultipleEntires', () => {
beforeEach(() => {
jest.clearAllMocks();
});
const implementation = {
init: jest.fn(() => implementation),
};
const config = Map({});
const backend = new Backend(implementation, { config, backendName: 'github' });

it('should combine multiple content same folder entries', async () => {
const entries = [
{
slug: 'post.en',
path: 'posts/post.en.md',
data: { title: 'Title en', content: 'Content en' },
},
{
slug: 'post.fr',
path: 'posts/post.fr.md',
data: { title: 'Title fr', content: 'Content fr' },
},
];
const collection = fromJS({
i18n_structure: 'locale_file_extensions',
locales: ['en', 'fr'],
});

backend.listAllEntries = jest.fn().mockResolvedValue(entries);

await expect(backend.listAllMultipleEntires(collection)).resolves.toEqual([
{
slug: 'post',
path: 'posts/post.md',
multiContent: true,
raw: '',
data: {
en: { title: 'Title en', content: 'Content en' },
fr: { title: 'Title fr', content: 'Content fr' },
},
},
]);
});

it('should combine multiple content different folder entries', async () => {
const entries = [
{
slug: 'en/post',
path: 'posts/en/post.md',
data: { title: 'Title en', content: 'Content en' },
},
{
slug: 'fr/post',
path: 'posts/fr/post.md',
data: { title: 'Title fr', content: 'Content fr' },
},
];
const collection = fromJS({ i18n_structure: 'locale_folders', locales: ['en', 'fr'] });

backend.listAllEntries = jest.fn().mockResolvedValue(entries);

await expect(backend.listAllMultipleEntires(collection)).resolves.toEqual([
{
slug: 'post',
path: 'posts/post.md',
multiContent: true,
raw: '',
data: {
en: { title: 'Title en', content: 'Content en' },
fr: { title: 'Title fr', content: 'Content fr' },
},
},
]);
});
});

describe('getMultipleEntries', () => {
beforeEach(() => {
jest.clearAllMocks();
});
const implementation = {
init: jest.fn(() => implementation),
};
const config = Map({});
const backend = new Backend(implementation, { config, backendName: 'github' });
const entryDraft = fromJS({
entry: {
data: {
en: { title: 'post', content: 'Content en' },
fr: { title: 'publier', content: 'Content fr' },
},
},
});
const entryObj = { path: 'posts/post.md', slug: 'post' };

it('should split multiple content into different locale file entries', async () => {
const collection = fromJS({
i18n_structure: 'locale_file_extensions',
fields: [{ name: 'title' }, { name: 'content' }],
extension: 'md',
});

expect(backend.getMultipleEntries(collection, entryDraft, entryObj)).toEqual([
{
slug: 'post',
path: 'posts/post.en.md',
raw: '---\ntitle: post\ncontent: Content en\n---\n',
},
{
slug: 'post',
path: 'posts/post.fr.md',
raw: '---\ntitle: publier\ncontent: Content fr\n---\n',
},
]);
});

it('should split multiple content into different locale folder entries', async () => {
const collection = fromJS({
i18n_structure: 'locale_folders',
fields: [{ name: 'title' }, { name: 'content' }],
extension: 'md',
});

expect(backend.getMultipleEntries(collection, entryDraft, entryObj)).toEqual([
{
slug: 'post',
path: 'posts/en/post.md',
raw: '---\ntitle: post\ncontent: Content en\n---\n',
},
{
slug: 'post',
path: 'posts/fr/post.md',
raw: '---\ntitle: publier\ncontent: Content fr\n---\n',
},
]);
});
});
});
Loading