2020-03-23 12:01:37 +02:00
|
|
|
import { attempt, isError, take, unset, isEmpty } from 'lodash';
|
2018-07-17 19:13:52 -04:00
|
|
|
import uuid from 'uuid/v4';
|
2019-12-18 18:16:02 +02:00
|
|
|
import {
|
|
|
|
EditorialWorkflowError,
|
|
|
|
Cursor,
|
|
|
|
CURSOR_COMPATIBILITY_SYMBOL,
|
|
|
|
basename,
|
2020-01-15 00:15:14 +02:00
|
|
|
Implementation,
|
|
|
|
Entry,
|
|
|
|
ImplementationEntry,
|
|
|
|
AssetProxy,
|
|
|
|
PersistOptions,
|
|
|
|
ImplementationMediaFile,
|
|
|
|
User,
|
|
|
|
Config,
|
|
|
|
ImplementationFile,
|
2019-12-18 18:16:02 +02:00
|
|
|
} from 'netlify-cms-lib-util';
|
2018-07-17 19:13:52 -04:00
|
|
|
import AuthenticationPage from './AuthenticationPage';
|
|
|
|
|
2020-01-15 00:15:14 +02:00
|
|
|
type RepoFile = { file?: { path: string }; content: string };
|
|
|
|
type RepoTree = { [key: string]: RepoFile | RepoTree };
|
|
|
|
|
|
|
|
declare global {
|
|
|
|
interface Window {
|
|
|
|
repoFiles: RepoTree;
|
|
|
|
repoFilesUnpublished: ImplementationEntry[];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-17 19:13:52 -04:00
|
|
|
window.repoFiles = window.repoFiles || {};
|
|
|
|
window.repoFilesUnpublished = window.repoFilesUnpublished || [];
|
|
|
|
|
2020-01-15 00:15:14 +02:00
|
|
|
function getFile(path: string) {
|
2018-07-17 19:13:52 -04:00
|
|
|
const segments = path.split('/');
|
2020-01-15 00:15:14 +02:00
|
|
|
let obj: RepoTree = window.repoFiles;
|
2018-07-17 19:13:52 -04:00
|
|
|
while (obj && segments.length) {
|
2020-01-15 00:15:14 +02:00
|
|
|
obj = obj[segments.shift() as string] as RepoTree;
|
2018-07-17 19:13:52 -04:00
|
|
|
}
|
2020-01-15 00:15:14 +02:00
|
|
|
return ((obj as unknown) as RepoFile) || {};
|
2018-07-17 19:13:52 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
const pageSize = 10;
|
|
|
|
|
2020-01-15 00:15:14 +02:00
|
|
|
const getCursor = (
|
|
|
|
folder: string,
|
|
|
|
extension: string,
|
|
|
|
entries: ImplementationEntry[],
|
|
|
|
index: number,
|
|
|
|
depth: number,
|
|
|
|
) => {
|
2018-07-17 19:13:52 -04:00
|
|
|
const count = entries.length;
|
|
|
|
const pageCount = Math.floor(count / pageSize);
|
|
|
|
return Cursor.create({
|
|
|
|
actions: [
|
2018-08-07 14:46:54 -06:00
|
|
|
...(index < pageCount ? ['next', 'last'] : []),
|
|
|
|
...(index > 0 ? ['prev', 'first'] : []),
|
2018-07-17 19:13:52 -04:00
|
|
|
],
|
|
|
|
meta: { index, count, pageSize, pageCount },
|
2020-01-15 00:15:14 +02:00
|
|
|
data: { folder, extension, index, pageCount, depth },
|
2018-07-17 19:13:52 -04:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2020-01-15 00:15:14 +02:00
|
|
|
export const getFolderEntries = (
|
|
|
|
tree: RepoTree,
|
|
|
|
folder: string,
|
|
|
|
extension: string,
|
|
|
|
depth: number,
|
|
|
|
files = [] as ImplementationEntry[],
|
|
|
|
path = folder,
|
|
|
|
) => {
|
2019-12-22 15:20:42 +02:00
|
|
|
if (depth <= 0) {
|
|
|
|
return files;
|
|
|
|
}
|
|
|
|
|
|
|
|
Object.keys(tree[folder] || {}).forEach(key => {
|
|
|
|
if (key.endsWith(`.${extension}`)) {
|
2020-01-15 00:15:14 +02:00
|
|
|
const file = (tree[folder] as RepoTree)[key] as RepoFile;
|
2019-12-22 15:20:42 +02:00
|
|
|
files.unshift({
|
2020-01-15 00:15:14 +02:00
|
|
|
file: { path: `${path}/${key}`, id: null },
|
2019-12-22 15:20:42 +02:00
|
|
|
data: file.content,
|
|
|
|
});
|
|
|
|
} else {
|
2020-01-15 00:15:14 +02:00
|
|
|
const subTree = tree[folder] as RepoTree;
|
2019-12-22 15:20:42 +02:00
|
|
|
return getFolderEntries(subTree, key, extension, depth - 1, files, `${path}/${key}`);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return files;
|
2018-07-17 19:13:52 -04:00
|
|
|
};
|
|
|
|
|
2020-01-15 00:15:14 +02:00
|
|
|
export default class TestBackend implements Implementation {
|
|
|
|
assets: ImplementationMediaFile[];
|
|
|
|
options: { initialWorkflowStatus?: string };
|
|
|
|
|
|
|
|
constructor(_config: Config, options = {}) {
|
2018-07-17 19:13:52 -04:00
|
|
|
this.assets = [];
|
2018-07-23 12:14:53 -04:00
|
|
|
this.options = options;
|
2018-07-17 19:13:52 -04:00
|
|
|
}
|
|
|
|
|
2020-04-01 06:13:27 +03:00
|
|
|
isGitBackend() {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-06-03 12:44:03 +03:00
|
|
|
status() {
|
2020-06-15 10:59:28 -04:00
|
|
|
return Promise.resolve({ auth: { status: true }, api: { status: true, statusPage: '' } });
|
2020-06-03 12:44:03 +03:00
|
|
|
}
|
|
|
|
|
2018-07-17 19:13:52 -04:00
|
|
|
authComponent() {
|
|
|
|
return AuthenticationPage;
|
|
|
|
}
|
|
|
|
|
2020-01-15 00:15:14 +02:00
|
|
|
restoreUser() {
|
|
|
|
return this.authenticate();
|
2018-07-17 19:13:52 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
authenticate() {
|
2020-01-15 00:15:14 +02:00
|
|
|
return (Promise.resolve() as unknown) as Promise<User>;
|
2018-07-17 19:13:52 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
logout() {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
getToken() {
|
|
|
|
return Promise.resolve('');
|
|
|
|
}
|
|
|
|
|
2020-01-15 00:15:14 +02:00
|
|
|
traverseCursor(cursor: Cursor, action: string) {
|
|
|
|
const { folder, extension, index, pageCount, depth } = cursor.data!.toObject() as {
|
|
|
|
folder: string;
|
|
|
|
extension: string;
|
|
|
|
index: number;
|
|
|
|
pageCount: number;
|
|
|
|
depth: number;
|
|
|
|
};
|
2018-07-17 19:13:52 -04:00
|
|
|
const newIndex = (() => {
|
2018-08-07 14:46:54 -06:00
|
|
|
if (action === 'next') {
|
2020-01-15 00:15:14 +02:00
|
|
|
return (index as number) + 1;
|
2018-08-07 14:46:54 -06:00
|
|
|
}
|
|
|
|
if (action === 'prev') {
|
2020-01-15 00:15:14 +02:00
|
|
|
return (index as number) - 1;
|
2018-08-07 14:46:54 -06:00
|
|
|
}
|
|
|
|
if (action === 'first') {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
if (action === 'last') {
|
|
|
|
return pageCount;
|
|
|
|
}
|
2020-01-15 00:15:14 +02:00
|
|
|
return 0;
|
2018-07-17 19:13:52 -04:00
|
|
|
})();
|
|
|
|
// TODO: stop assuming cursors are for collections
|
2020-01-15 00:15:14 +02:00
|
|
|
const allEntries = getFolderEntries(window.repoFiles, folder, extension, depth);
|
2018-08-07 14:46:54 -06:00
|
|
|
const entries = allEntries.slice(newIndex * pageSize, newIndex * pageSize + pageSize);
|
2020-01-15 00:15:14 +02:00
|
|
|
const newCursor = getCursor(folder, extension, allEntries, newIndex, depth);
|
2018-07-17 19:13:52 -04:00
|
|
|
return Promise.resolve({ entries, cursor: newCursor });
|
|
|
|
}
|
|
|
|
|
2020-01-15 00:15:14 +02:00
|
|
|
entriesByFolder(folder: string, extension: string, depth: number) {
|
2019-12-22 15:20:42 +02:00
|
|
|
const entries = folder ? getFolderEntries(window.repoFiles, folder, extension, depth) : [];
|
2020-01-15 00:15:14 +02:00
|
|
|
const cursor = getCursor(folder, extension, entries, 0, depth);
|
2018-07-17 19:13:52 -04:00
|
|
|
const ret = take(entries, pageSize);
|
2020-01-15 00:15:14 +02:00
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
|
|
|
// @ts-ignore
|
2018-07-17 19:13:52 -04:00
|
|
|
ret[CURSOR_COMPATIBILITY_SYMBOL] = cursor;
|
|
|
|
return Promise.resolve(ret);
|
|
|
|
}
|
|
|
|
|
2020-01-15 00:15:14 +02:00
|
|
|
entriesByFiles(files: ImplementationFile[]) {
|
2018-08-07 14:46:54 -06:00
|
|
|
return Promise.all(
|
|
|
|
files.map(file => ({
|
|
|
|
file,
|
|
|
|
data: getFile(file.path).content,
|
|
|
|
})),
|
|
|
|
);
|
2018-07-17 19:13:52 -04:00
|
|
|
}
|
|
|
|
|
2020-01-15 00:15:14 +02:00
|
|
|
getEntry(path: string) {
|
2018-07-17 19:13:52 -04:00
|
|
|
return Promise.resolve({
|
2020-01-15 00:15:14 +02:00
|
|
|
file: { path, id: null },
|
2018-07-17 19:13:52 -04:00
|
|
|
data: getFile(path).content,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
unpublishedEntries() {
|
|
|
|
return Promise.resolve(window.repoFilesUnpublished);
|
|
|
|
}
|
|
|
|
|
2020-01-15 00:15:14 +02:00
|
|
|
getMediaFiles(entry: ImplementationEntry) {
|
|
|
|
const mediaFiles = entry.mediaFiles!.map(file => ({
|
2019-11-17 11:51:50 +02:00
|
|
|
...file,
|
2020-01-15 00:15:14 +02:00
|
|
|
...this.normalizeAsset(file),
|
|
|
|
file: file.file as File,
|
2019-11-17 11:51:50 +02:00
|
|
|
}));
|
|
|
|
return mediaFiles;
|
|
|
|
}
|
|
|
|
|
2020-01-15 00:15:14 +02:00
|
|
|
unpublishedEntry(collection: string, slug: string) {
|
2018-08-07 14:46:54 -06:00
|
|
|
const entry = window.repoFilesUnpublished.find(
|
2020-01-15 00:15:14 +02:00
|
|
|
e => e.metaData!.collection === collection && e.slug === slug,
|
2018-08-07 14:46:54 -06:00
|
|
|
);
|
2018-07-17 19:13:52 -04:00
|
|
|
if (!entry) {
|
2018-08-07 14:46:54 -06:00
|
|
|
return Promise.reject(
|
|
|
|
new EditorialWorkflowError('content is not under editorial workflow', true),
|
|
|
|
);
|
2018-07-17 19:13:52 -04:00
|
|
|
}
|
2019-11-17 11:51:50 +02:00
|
|
|
entry.mediaFiles = this.getMediaFiles(entry);
|
|
|
|
|
2018-07-17 19:13:52 -04:00
|
|
|
return Promise.resolve(entry);
|
|
|
|
}
|
|
|
|
|
2020-01-15 00:15:14 +02:00
|
|
|
deleteUnpublishedEntry(collection: string, slug: string) {
|
2018-07-17 19:13:52 -04:00
|
|
|
const unpubStore = window.repoFilesUnpublished;
|
2018-08-07 14:46:54 -06:00
|
|
|
const existingEntryIndex = unpubStore.findIndex(
|
2020-01-15 00:15:14 +02:00
|
|
|
e => e.metaData!.collection === collection && e.slug === slug,
|
2018-08-07 14:46:54 -06:00
|
|
|
);
|
2018-07-17 19:13:52 -04:00
|
|
|
unpubStore.splice(existingEntryIndex, 1);
|
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
|
2020-01-15 00:15:14 +02:00
|
|
|
async persistEntry(
|
|
|
|
{ path, raw, slug }: Entry,
|
|
|
|
assetProxies: AssetProxy[],
|
|
|
|
options: PersistOptions,
|
|
|
|
) {
|
2018-08-01 15:06:22 -04:00
|
|
|
if (options.useWorkflow) {
|
2018-07-17 19:13:52 -04:00
|
|
|
const unpubStore = window.repoFilesUnpublished;
|
2019-11-17 11:51:50 +02:00
|
|
|
|
2018-07-17 19:13:52 -04:00
|
|
|
const existingEntryIndex = unpubStore.findIndex(e => e.file.path === path);
|
|
|
|
if (existingEntryIndex >= 0) {
|
2020-01-15 00:15:14 +02:00
|
|
|
const unpubEntry = {
|
|
|
|
...unpubStore[existingEntryIndex],
|
|
|
|
data: raw,
|
|
|
|
mediaFiles: assetProxies.map(this.normalizeAsset),
|
|
|
|
};
|
2019-11-17 11:51:50 +02:00
|
|
|
|
2018-07-17 19:13:52 -04:00
|
|
|
unpubStore.splice(existingEntryIndex, 1, unpubEntry);
|
|
|
|
} else {
|
|
|
|
const unpubEntry = {
|
|
|
|
data: raw,
|
|
|
|
file: {
|
|
|
|
path,
|
2020-01-15 00:15:14 +02:00
|
|
|
id: null,
|
2018-07-17 19:13:52 -04:00
|
|
|
},
|
|
|
|
metaData: {
|
2020-01-15 00:15:14 +02:00
|
|
|
collection: options.collectionName as string,
|
|
|
|
status: (options.status || this.options.initialWorkflowStatus) as string,
|
2018-07-17 19:13:52 -04:00
|
|
|
},
|
|
|
|
slug,
|
2020-01-15 00:15:14 +02:00
|
|
|
mediaFiles: assetProxies.map(this.normalizeAsset),
|
2020-03-23 12:01:37 +02:00
|
|
|
isModification: !isEmpty(getFile(path)),
|
2018-07-17 19:13:52 -04:00
|
|
|
};
|
|
|
|
unpubStore.push(unpubEntry);
|
|
|
|
}
|
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
|
|
|
|
const newEntry = options.newEntry || false;
|
2019-11-28 05:39:33 +02:00
|
|
|
|
|
|
|
const segments = path.split('/');
|
|
|
|
const entry = newEntry ? { content: raw } : { ...getFile(path), content: raw };
|
|
|
|
|
|
|
|
let obj = window.repoFiles;
|
|
|
|
while (segments.length > 1) {
|
2020-01-15 00:15:14 +02:00
|
|
|
const segment = segments.shift() as string;
|
2019-11-28 05:39:33 +02:00
|
|
|
obj[segment] = obj[segment] || {};
|
2020-01-15 00:15:14 +02:00
|
|
|
obj = obj[segment] as RepoTree;
|
2018-07-17 19:13:52 -04:00
|
|
|
}
|
2020-01-15 00:15:14 +02:00
|
|
|
(obj[segments.shift() as string] as RepoFile) = entry;
|
2019-11-28 05:39:33 +02:00
|
|
|
|
2020-01-15 00:15:14 +02:00
|
|
|
await Promise.all(assetProxies.map(file => this.persistMedia(file)));
|
2018-07-17 19:13:52 -04:00
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
|
2020-01-15 00:15:14 +02:00
|
|
|
updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) {
|
2018-07-17 19:13:52 -04:00
|
|
|
const unpubStore = window.repoFilesUnpublished;
|
2018-08-07 14:46:54 -06:00
|
|
|
const entryIndex = unpubStore.findIndex(
|
2020-01-15 00:15:14 +02:00
|
|
|
e => e.metaData!.collection === collection && e.slug === slug,
|
2018-08-07 14:46:54 -06:00
|
|
|
);
|
2020-01-15 00:15:14 +02:00
|
|
|
unpubStore[entryIndex]!.metaData!.status = newStatus;
|
2018-07-17 19:13:52 -04:00
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
|
2020-01-15 00:15:14 +02:00
|
|
|
async publishUnpublishedEntry(collection: string, slug: string) {
|
2018-07-17 19:13:52 -04:00
|
|
|
const unpubStore = window.repoFilesUnpublished;
|
2018-08-07 14:46:54 -06:00
|
|
|
const unpubEntryIndex = unpubStore.findIndex(
|
2020-01-15 00:15:14 +02:00
|
|
|
e => e.metaData!.collection === collection && e.slug === slug,
|
2018-08-07 14:46:54 -06:00
|
|
|
);
|
2018-07-17 19:13:52 -04:00
|
|
|
const unpubEntry = unpubStore[unpubEntryIndex];
|
2020-01-15 00:15:14 +02:00
|
|
|
const entry = {
|
|
|
|
raw: unpubEntry.data,
|
|
|
|
slug: unpubEntry.slug as string,
|
|
|
|
path: unpubEntry.file.path,
|
|
|
|
};
|
2018-07-17 19:13:52 -04:00
|
|
|
unpubStore.splice(unpubEntryIndex, 1);
|
2019-11-17 11:51:50 +02:00
|
|
|
|
2020-01-15 00:15:14 +02:00
|
|
|
await this.persistEntry(entry, unpubEntry.mediaFiles!, { commitMessage: '' });
|
2018-07-17 19:13:52 -04:00
|
|
|
}
|
2019-11-17 11:51:50 +02:00
|
|
|
|
2018-07-17 19:13:52 -04:00
|
|
|
getMedia() {
|
|
|
|
return Promise.resolve(this.assets);
|
|
|
|
}
|
|
|
|
|
2020-01-15 00:15:14 +02:00
|
|
|
async getMediaFile(path: string) {
|
|
|
|
const asset = this.assets.find(asset => asset.path === path) as ImplementationMediaFile;
|
2019-12-18 18:16:02 +02:00
|
|
|
|
2020-01-15 00:15:14 +02:00
|
|
|
const url = asset.url as string;
|
2019-12-18 18:16:02 +02:00
|
|
|
const name = basename(path);
|
2020-01-15 00:15:14 +02:00
|
|
|
const blob = await fetch(url).then(res => res.blob());
|
2019-12-18 18:16:02 +02:00
|
|
|
const fileObj = new File([blob], name);
|
|
|
|
|
|
|
|
return {
|
2020-01-15 00:15:14 +02:00
|
|
|
id: url,
|
|
|
|
displayURL: url,
|
2019-12-18 18:16:02 +02:00
|
|
|
path,
|
|
|
|
name,
|
|
|
|
size: fileObj.size,
|
|
|
|
file: fileObj,
|
2020-01-15 00:15:14 +02:00
|
|
|
url,
|
2019-12-18 18:16:02 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-01-15 00:15:14 +02:00
|
|
|
normalizeAsset(assetProxy: AssetProxy) {
|
|
|
|
const fileObj = assetProxy.fileObj as File;
|
2018-07-17 19:13:52 -04:00
|
|
|
const { name, size } = fileObj;
|
|
|
|
const objectUrl = attempt(window.URL.createObjectURL, fileObj);
|
|
|
|
const url = isError(objectUrl) ? '' : objectUrl;
|
2020-01-15 00:15:14 +02:00
|
|
|
const normalizedAsset = {
|
|
|
|
id: uuid(),
|
|
|
|
name,
|
|
|
|
size,
|
|
|
|
path: assetProxy.path,
|
|
|
|
url,
|
|
|
|
displayURL: url,
|
|
|
|
fileObj,
|
|
|
|
};
|
2019-11-17 11:51:50 +02:00
|
|
|
|
|
|
|
return normalizedAsset;
|
|
|
|
}
|
|
|
|
|
2020-01-15 00:15:14 +02:00
|
|
|
persistMedia(assetProxy: AssetProxy) {
|
|
|
|
const normalizedAsset = this.normalizeAsset(assetProxy);
|
2019-11-17 11:51:50 +02:00
|
|
|
|
2019-12-18 18:16:02 +02:00
|
|
|
this.assets.push(normalizedAsset);
|
2018-07-17 19:13:52 -04:00
|
|
|
|
|
|
|
return Promise.resolve(normalizedAsset);
|
|
|
|
}
|
|
|
|
|
2020-01-15 00:15:14 +02:00
|
|
|
deleteFile(path: string) {
|
2018-07-17 19:13:52 -04:00
|
|
|
const assetIndex = this.assets.findIndex(asset => asset.path === path);
|
|
|
|
if (assetIndex > -1) {
|
|
|
|
this.assets.splice(assetIndex, 1);
|
2018-08-07 14:46:54 -06:00
|
|
|
} else {
|
2019-11-29 17:10:39 +01:00
|
|
|
unset(window.repoFiles, path.split('/'));
|
2018-07-17 19:13:52 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
return Promise.resolve();
|
|
|
|
}
|
2020-01-15 00:15:14 +02:00
|
|
|
|
|
|
|
async getDeployPreview() {
|
|
|
|
return null;
|
|
|
|
}
|
2018-07-17 19:13:52 -04:00
|
|
|
}
|