Feat: multi content authoring (#4139)
This commit is contained in:
@ -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) {
|
||||
|
@ -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' },
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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) {
|
||||
|
Reference in New Issue
Block a user