Feat: multi content authoring (#4139)

This commit is contained in:
Erez Rokah
2020-09-20 10:30:46 -07:00
committed by GitHub
parent 7968e01e29
commit cb2ad687ee
65 changed files with 4331 additions and 1521 deletions

View File

@ -9,7 +9,7 @@ import {
localForage,
basename,
AssetProxy,
Entry as LibEntry,
DataFile,
PersistOptions,
readFileMetadata,
CMS_BRANCH_PREFIX,
@ -62,10 +62,6 @@ interface TreeFile {
raw?: string;
}
export interface Entry extends LibEntry {
sha?: string;
}
type Override<T, U> = Pick<T, Exclude<keyof T, keyof U>> & U;
type TreeEntry = Override<GitCreateTreeParamsTree, { sha: string | null }>;
@ -877,8 +873,8 @@ export default class API {
}));
}
async persistFiles(entry: Entry | null, mediaFiles: AssetProxy[], options: PersistOptions) {
const files = entry ? mediaFiles.concat(entry) : mediaFiles;
async persistFiles(dataFiles: DataFile[], mediaFiles: AssetProxy[], options: PersistOptions) {
const files = mediaFiles.concat(dataFiles);
const uploadPromises = files.map(file => this.uploadBlob(file));
await Promise.all(uploadPromises);
@ -896,12 +892,8 @@ export default class API {
sha,
}),
);
return this.editorialWorkflowGit(
files as TreeFile[],
entry as Entry,
mediaFilesList,
options,
);
const slug = dataFiles[0].slug;
return this.editorialWorkflowGit(files as TreeFile[], slug, mediaFilesList, options);
}
}
@ -927,29 +919,16 @@ export default class API {
}
}
deleteFile(path: string, message: string) {
async deleteFiles(paths: string[], message: string) {
if (this.useOpenAuthoring) {
return Promise.reject('Cannot delete published entries as an Open Authoring user!');
}
const branch = this.branch;
return this.getFileSha(path, { branch }).then(sha => {
const params: { sha: string; message: string; branch: string; author?: { date: string } } = {
sha,
message,
branch,
};
const opts = { method: 'DELETE', params };
if (this.commitAuthor) {
opts.params.author = {
...this.commitAuthor,
date: new Date().toISOString(),
};
}
const fileURL = `${this.repoURL}/contents/${path}`;
return this.request(fileURL, opts);
});
const branchData = await this.getDefaultBranch();
const files = paths.map(path => ({ path, sha: null }));
const changeTree = await this.updateTree(branchData.commit.sha, files);
const commit = await this.commit(message, changeTree);
await this.patchBranch(this.branch, commit.sha);
}
async createBranchAndPullRequest(branchName: string, sha: string, commitMessage: string) {
@ -966,11 +945,11 @@ export default class API {
async editorialWorkflowGit(
files: TreeFile[],
entry: Entry,
slug: string,
mediaFilesList: MediaFile[],
options: PersistOptions,
) {
const contentKey = this.generateContentKey(options.collectionName as string, entry.slug);
const contentKey = this.generateContentKey(options.collectionName as string, slug);
const branch = branchFromContentKey(contentKey);
const unpublished = options.unpublished || false;
if (!unpublished) {

View File

@ -229,12 +229,17 @@ describe('github API', () => {
mockAPI(api, responses);
const entry = {
slug: 'entry',
sha: 'abc',
path: 'content/posts/new-post.md',
raw: 'content',
dataFiles: [
{
slug: 'entry',
sha: 'abc',
path: 'content/posts/new-post.md',
raw: 'content',
},
],
assets: [],
};
await api.persistFiles(entry, [], { commitMessage: 'commitMessage' });
await api.persistFiles(entry.dataFiles, entry.assets, { commitMessage: 'commitMessage' });
expect(api.request).toHaveBeenCalledTimes(5);
@ -242,7 +247,10 @@ describe('github API', () => {
'/repos/owner/repo/git/blobs',
{
method: 'POST',
body: JSON.stringify({ content: Base64.encode(entry.raw), encoding: 'base64' }),
body: JSON.stringify({
content: Base64.encode(entry.dataFiles[0].raw),
encoding: 'base64',
}),
},
]);
@ -297,35 +305,38 @@ describe('github API', () => {
api.editorialWorkflowGit = jest.fn();
const entry = {
slug: 'entry',
sha: 'abc',
path: 'content/posts/new-post.md',
raw: 'content',
dataFiles: [
{
slug: 'entry',
sha: 'abc',
path: 'content/posts/new-post.md',
raw: 'content',
},
],
assets: [
{
path: '/static/media/image-1.png',
sha: 'image-1.png',
},
{
path: '/static/media/image-2.png',
sha: 'image-2.png',
},
],
};
const mediaFiles = [
{
path: '/static/media/image-1.png',
sha: 'image-1.png',
},
{
path: '/static/media/image-2.png',
sha: 'image-2.png',
},
];
await api.persistFiles(entry, mediaFiles, { useWorkflow: true });
await api.persistFiles(entry.dataFiles, entry.assets, { useWorkflow: true });
expect(api.uploadBlob).toHaveBeenCalledTimes(3);
expect(api.uploadBlob).toHaveBeenCalledWith(entry);
expect(api.uploadBlob).toHaveBeenCalledWith(mediaFiles[0]);
expect(api.uploadBlob).toHaveBeenCalledWith(mediaFiles[1]);
expect(api.uploadBlob).toHaveBeenCalledWith(entry.dataFiles[0]);
expect(api.uploadBlob).toHaveBeenCalledWith(entry.assets[0]);
expect(api.uploadBlob).toHaveBeenCalledWith(entry.assets[1]);
expect(api.editorialWorkflowGit).toHaveBeenCalledTimes(1);
expect(api.editorialWorkflowGit).toHaveBeenCalledWith(
mediaFiles.concat(entry),
entry,
entry.assets.concat(entry.dataFiles),
entry.dataFiles[0].slug,
[
{ path: 'static/media/image-1.png', sha: 'image-1.png' },
{ path: 'static/media/image-2.png', sha: 'image-2.png' },

View File

@ -104,7 +104,7 @@ describe('github backend implementation', () => {
});
expect(persistFiles).toHaveBeenCalledTimes(1);
expect(persistFiles).toHaveBeenCalledWith(null, [mediaFile], {});
expect(persistFiles).toHaveBeenCalledWith([], [mediaFile], {});
expect(createObjectURL).toHaveBeenCalledTimes(1);
expect(createObjectURL).toHaveBeenCalledWith(mediaFile.fileObj);
});

View File

@ -30,10 +30,11 @@ import {
contentKeyFromBranch,
unsentRequest,
branchFromContentKey,
Entry,
} from 'netlify-cms-lib-util';
import AuthenticationPage from './AuthenticationPage';
import { Octokit } from '@octokit/rest';
import API, { Entry, API_NAME } from './API';
import API, { API_NAME } from './API';
import GraphQLAPI from './GraphQLAPI';
type GitHubUser = Octokit.UsersGetAuthenticatedResponse;
@ -473,18 +474,18 @@ export default class GitHub implements Implementation {
);
}
persistEntry(entry: Entry, mediaFiles: AssetProxy[] = [], options: PersistOptions) {
persistEntry(entry: Entry, options: PersistOptions) {
// persistEntry is a transactional operation
return runWithLock(
this.lock,
() => this.api!.persistFiles(entry, mediaFiles, options),
() => this.api!.persistFiles(entry.dataFiles, entry.assets, options),
'Failed to acquire persist entry lock',
);
}
async persistMedia(mediaFile: AssetProxy, options: PersistOptions) {
try {
await this.api!.persistFiles(null, [mediaFile], options);
await this.api!.persistFiles([], [mediaFile], options);
const { sha, path, fileObj } = mediaFile as AssetProxy & { sha: string };
const displayURL = URL.createObjectURL(fileObj);
return {
@ -500,8 +501,8 @@ export default class GitHub implements Implementation {
}
}
deleteFile(path: string, commitMessage: string) {
return this.api!.deleteFile(path, commitMessage);
deleteFiles(paths: string[], commitMessage: string) {
return this.api!.deleteFiles(paths, commitMessage);
}
async traverseCursor(cursor: Cursor, action: string) {