2017-08-14 09:00:47 -04:00
|
|
|
import trimStart from 'lodash/trimStart';
|
2016-12-23 16:59:48 -02:00
|
|
|
import semaphore from "semaphore";
|
|
|
|
import AuthenticationPage from "./AuthenticationPage";
|
|
|
|
import API from "./API";
|
2016-06-05 01:52:18 -07:00
|
|
|
|
2016-09-04 19:55:14 +02:00
|
|
|
const MAX_CONCURRENT_DOWNLOADS = 10;
|
|
|
|
|
2016-06-05 01:52:18 -07:00
|
|
|
export default class GitHub {
|
2016-12-23 16:59:48 -02:00
|
|
|
constructor(config, proxied = false) {
|
2016-06-05 01:52:18 -07:00
|
|
|
this.config = config;
|
2016-12-23 16:59:48 -02:00
|
|
|
|
|
|
|
if (!proxied && config.getIn(["backend", "repo"]) == null) {
|
|
|
|
throw new Error("The GitHub backend needs a \"repo\" in the backend configuration.");
|
2016-06-05 01:52:18 -07:00
|
|
|
}
|
2016-12-23 16:59:48 -02:00
|
|
|
|
|
|
|
this.repo = config.getIn(["backend", "repo"], "");
|
WIP - Global UI (#785)
* update top bar and collections sidebar UI
* update collection entries UI
* improve global layout
* merge search page into collection page
* enable new entry button
* search fixup
* wip -initial editor update
* update editor scrolling and markdown toolbar position
* wip
* editor toolbar progress
* editor toolbar wip
* finished basic editor toolbar
* add standalone toggle component
* improve markdown toolbar spacing
* add user avatar placeholder
* finish markdown toggle styling
* refactor icon setup, add new icons
* add new icons to markdown editor toolbar
* remove extra app container
* add markdown active mark style
* relation and text widget styling
* widget design updates, basic list/object design update
* widget style updates, image widget improvements
* refactor widget directory, fix file removal
* widget focus styles
* finish editor widget focus styles
* migrate media library modal to react-modal
* wip - migrate editor component form to modal
* wip - move editor component form to modal
* wip - embed plugin forms in the editor
* inline shortcode forms working
* disable react hot loading, its breaking things
* improve shortcode form styles
* make shortcode form collapsible, improve styling
* add close functionality to shortcode blocks
* improve base media library styling
* fix shortcode label
* migrate unstyled workflow to new UI
* wip - reorganizing everything
* more work moving everything
* finish more moving and eliminating stuff
* restructure, remove react-toolbox
* wip - removing old stuff, more restructure
* finish restructure
* wip - css arch
* switch back to test repo
* update react-datetime to ^2.11.0
* remove leftover react-toolbox button
* more restructuring clean-up
* fix UI component directory case
* wip -css editor control style
* wip - consolidate widget styles
* wip - use a single control renderer
* fixed object values breaking
* wip - editor control active styles
* pass control wrapper to widgets
* ensure branch name is trimmed
* wip - improve widget authoring support
* import Map to Widget component
* refactor toolbar buttons
* wip - more widget active styles
* break out editor toggle component
* add local scroll sync back
* update editor toggle icons
* limit editor control pane content width
* fix editor control spacing
* migrate markdown toolbar stickiness to css
* fix markdown toolbar border radius
* temporarily use test backend
* stop markdown toolbar from going to bottom
* restore disabled markdown toolbar buttons for raw
* test markdown widget without focus styles
* more widget updates
* remove card visuals from editor
* disable dragging editor split off screen
* use editorControl component for shortcode fields
* make header site link configurable
* add configurable collection descriptions
* temporarily add example assets
* add basic list view
* remove outdated css mixins
* add and implement search icon
* activate quick add menu
* visualize usable space in editor view
* fix entry close, other improvements
* wip - editorial workflow updates
* some dropshadow and other CSS tweaks
* workflow ui updates
* add worfklow card buttons
* fix workflow card button handlers
* some dropshadow and other CSS tweaks
* make workflow board wider
* center workflow and collection views
* add basic responsiveness
* a bunch of fun UI fixes! a BUNCH! (#875)
* give `.nc-entryEditor-toolbar-mainSection` left and right child divs
* a bunch of fun UI fixes! a BUNCH!
* remove obscure --buttonShadow
* revert to test repo
* fix not found page styling
* allow workflow publishing from any column
* disallow publishing from all columns, with feedback
* fix new entry button
* fix markdown state persisting across entries
* enable simple workflow save and new from editor
* update slug in address bar when saving new entry
* wip - workflow updates, deletion working
* add status change functionality to editor
* wip - improving status change from editor
* editor toolbar back button improvements, loading improvements, cleanup
* progress on the media library UI cleanup
* remove font smothing css
* a quick fix for these buttons
* tweaks
* progress on media library modal— broken FYI
* fix media library functionality, finish migrating footer
* remove media library footer files
* remove leftover css import
* fix media library
* editor publishing functionality complete (unstyled)
* remove leftover loader var from media library
* wip - editor publishing styles
* add status dropdown styling
* editor toolbar style updates
* editor toolbar state improvements
* progress on the media library UI cleanup, style improvements
* finish editorial workflow editor styling
* finish media library styling
* fix config
* add what-input to optimize focus styling
* fix button
* fix navigation blocking for simple workflow
* improve simple workflow publishing
* add avatar dropdown to editor top bar
* style github and test-repo auth pages
* add git gateway auth page styles
* improve editor error styling
2017-12-07 12:37:10 -05:00
|
|
|
this.branch = config.getIn(["backend", "branch"], "master").trim();
|
2017-08-01 20:28:03 -07:00
|
|
|
this.api_root = config.getIn(["backend", "api_root"], "https://api.github.com");
|
2017-01-10 22:23:22 -02:00
|
|
|
this.token = '';
|
2016-06-05 01:52:18 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
authComponent() {
|
|
|
|
return AuthenticationPage;
|
|
|
|
}
|
|
|
|
|
2017-08-29 13:45:05 -06:00
|
|
|
restoreUser(user) {
|
2017-08-27 20:12:54 -06:00
|
|
|
return this.authenticate(user);
|
2016-06-05 01:52:18 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
authenticate(state) {
|
2017-01-10 22:23:22 -02:00
|
|
|
this.token = state.token;
|
2017-08-01 20:28:03 -07:00
|
|
|
this.api = new API({ token: this.token, branch: this.branch, repo: this.repo, api_root: this.api_root });
|
|
|
|
return this.api.user().then(user =>
|
2017-08-20 16:02:57 -04:00
|
|
|
this.api.hasWriteAccess().then((isCollab) => {
|
2017-08-01 20:28:03 -07:00
|
|
|
// Unauthorized user
|
|
|
|
if (!isCollab) throw new Error("Your GitHub user account does not have access to this repo.");
|
|
|
|
// Authorized user
|
|
|
|
user.token = state.token;
|
|
|
|
return user;
|
|
|
|
})
|
|
|
|
);
|
2016-06-05 01:52:18 -07:00
|
|
|
}
|
|
|
|
|
2017-09-05 19:30:03 -07:00
|
|
|
logout() {
|
|
|
|
this.token = null;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-01-10 22:23:22 -02:00
|
|
|
getToken() {
|
|
|
|
return Promise.resolve(this.token);
|
|
|
|
}
|
|
|
|
|
2017-04-14 19:19:45 +01:00
|
|
|
entriesByFolder(collection, extension) {
|
2016-12-23 16:59:48 -02:00
|
|
|
return this.api.listFiles(collection.get("folder"))
|
2018-02-27 15:23:20 -07:00
|
|
|
.then(files => files.filter(file => file.name.endsWith('.' + extension)))
|
2016-10-27 13:12:18 -02:00
|
|
|
.then(this.fetchFiles);
|
2016-06-05 01:52:18 -07:00
|
|
|
}
|
|
|
|
|
2016-10-27 14:23:36 +02:00
|
|
|
entriesByFiles(collection) {
|
2016-12-23 16:59:48 -02:00
|
|
|
const files = collection.get("files").map(collectionFile => ({
|
|
|
|
path: collectionFile.get("file"),
|
|
|
|
label: collectionFile.get("label"),
|
2016-10-27 14:23:36 +02:00
|
|
|
}));
|
2016-10-27 13:12:18 -02:00
|
|
|
return this.fetchFiles(files);
|
|
|
|
}
|
|
|
|
|
|
|
|
fetchFiles = (files) => {
|
2016-10-21 20:42:14 -02:00
|
|
|
const sem = semaphore(MAX_CONCURRENT_DOWNLOADS);
|
|
|
|
const promises = [];
|
|
|
|
files.forEach((file) => {
|
2016-10-27 13:12:18 -02:00
|
|
|
promises.push(new Promise((resolve, reject) => (
|
|
|
|
sem.take(() => this.api.readFile(file.path, file.sha).then((data) => {
|
|
|
|
resolve({ file, data });
|
2016-10-21 20:42:14 -02:00
|
|
|
sem.leave();
|
2018-03-05 16:31:05 -07:00
|
|
|
}).catch((err = true) => {
|
2016-10-21 20:42:14 -02:00
|
|
|
sem.leave();
|
2018-03-05 16:31:05 -07:00
|
|
|
console.error(`failed to load file from GitHub: ${file.path}`);
|
|
|
|
resolve({ error: err });
|
2016-10-27 13:12:18 -02:00
|
|
|
}))
|
|
|
|
)));
|
2016-10-21 20:42:14 -02:00
|
|
|
});
|
2018-03-05 16:31:05 -07:00
|
|
|
return Promise.all(promises)
|
|
|
|
.then(loadedEntries => loadedEntries.filter(loadedEntry => !loadedEntry.error));
|
2016-10-27 13:12:18 -02:00
|
|
|
};
|
2016-07-19 17:11:22 -03:00
|
|
|
|
2016-10-10 15:34:21 -03:00
|
|
|
// Fetches a single entry.
|
|
|
|
getEntry(collection, slug, path) {
|
2016-10-27 14:23:36 +02:00
|
|
|
return this.api.readFile(path).then(data => ({
|
|
|
|
file: { path },
|
|
|
|
data,
|
|
|
|
}));
|
2016-10-10 15:34:21 -03:00
|
|
|
}
|
|
|
|
|
2017-08-14 09:00:47 -04:00
|
|
|
getMedia() {
|
|
|
|
return this.api.listFiles(this.config.get('media_folder'))
|
|
|
|
.then(files => files.map(({ sha, name, size, download_url, path }) => {
|
2018-01-05 05:17:52 +10:00
|
|
|
const url = new URL(download_url);
|
|
|
|
if (url.pathname.match(/.svg$/)) {
|
|
|
|
url.search += (url.search.slice(1) === '' ? '?' : '&') + 'sanitize=true';
|
|
|
|
}
|
|
|
|
return { id: sha, name, size, url: url.href, path };
|
2017-08-14 09:00:47 -04:00
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
2016-08-29 17:09:04 -03:00
|
|
|
persistEntry(entry, mediaFiles = [], options = {}) {
|
|
|
|
return this.api.persistFiles(entry, mediaFiles, options);
|
2016-07-19 17:11:22 -03:00
|
|
|
}
|
2016-09-06 13:04:17 -03:00
|
|
|
|
2017-11-09 15:55:23 -05:00
|
|
|
/**
|
|
|
|
* Pulls repo info from a `repos` response url property.
|
|
|
|
*
|
|
|
|
* Turns this:
|
|
|
|
* '<api_root>/repo/<username>/<repo>/...'
|
|
|
|
*
|
|
|
|
* Into this:
|
|
|
|
* '<username>/<repo>'
|
|
|
|
*/
|
|
|
|
getRepoFromResponseUrl(url) {
|
|
|
|
return url
|
|
|
|
|
|
|
|
// -> '/repo/<username>/<repo>/...'
|
|
|
|
.slice(this.api_root.length)
|
|
|
|
|
|
|
|
// -> [ '', 'repo', '<username>', '<repo>', ... ]
|
|
|
|
.split('/')
|
|
|
|
|
|
|
|
// -> [ '<username>', '<repo>' ]
|
|
|
|
.slice(2, 4)
|
|
|
|
|
|
|
|
// -> '<username>/<repo>'
|
|
|
|
.join('/');
|
|
|
|
}
|
|
|
|
|
2017-08-14 09:00:47 -04:00
|
|
|
async persistMedia(mediaFile, options = {}) {
|
|
|
|
try {
|
|
|
|
const response = await this.api.persistFiles(null, [mediaFile], options);
|
2017-11-09 15:55:23 -05:00
|
|
|
const repo = this.repo || this.getRepoFromResponseUrl(response.url);
|
2017-08-14 09:00:47 -04:00
|
|
|
const { value, size, path, fileObj } = mediaFile;
|
2018-03-23 14:51:15 +00:00
|
|
|
let url = `https://raw.githubusercontent.com/${repo}/${this.branch}${path}`;
|
|
|
|
|
|
|
|
// Assets uploaded to private repos will need valid access tokens.
|
|
|
|
const isPrivateRepo = await this.api.isPrivateRepo();
|
|
|
|
if (isPrivateRepo) {
|
|
|
|
const files = await this.api.listFiles(this.config.get('media_folder'));
|
|
|
|
const file = files.find(f => (f.sha === mediaFile.sha));
|
|
|
|
if (file) {
|
|
|
|
url = file.download_url;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-23 15:21:07 +00:00
|
|
|
return { id: mediaFile.sha, name: value, size: fileObj.size, url, path: trimStart(path, '/') };
|
2017-08-14 09:00:47 -04:00
|
|
|
}
|
|
|
|
catch(error) {
|
|
|
|
console.error(error);
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-21 23:40:33 -07:00
|
|
|
deleteFile(path, commitMessage, options) {
|
|
|
|
return this.api.deleteFile(path, commitMessage, options);
|
|
|
|
}
|
|
|
|
|
2016-09-06 13:04:17 -03:00
|
|
|
unpublishedEntries() {
|
2016-09-06 17:18:27 -03:00
|
|
|
return this.api.listUnpublishedBranches().then((branches) => {
|
|
|
|
const sem = semaphore(MAX_CONCURRENT_DOWNLOADS);
|
|
|
|
const promises = [];
|
|
|
|
branches.map((branch) => {
|
|
|
|
promises.push(new Promise((resolve, reject) => {
|
2016-12-23 16:59:48 -02:00
|
|
|
const slug = branch.ref.split("refs/heads/cms/").pop();
|
2016-10-27 15:27:39 -02:00
|
|
|
return sem.take(() => this.api.readUnpublishedBranchFile(slug).then((data) => {
|
2016-10-10 18:33:49 -03:00
|
|
|
if (data === null || data === undefined) {
|
|
|
|
resolve(null);
|
|
|
|
sem.leave();
|
|
|
|
} else {
|
2017-01-11 20:58:15 -02:00
|
|
|
const path = data.metaData.objects.entry.path;
|
2016-10-27 15:27:39 -02:00
|
|
|
resolve({
|
|
|
|
slug,
|
|
|
|
file: { path },
|
|
|
|
data: data.fileData,
|
|
|
|
metaData: data.metaData,
|
2017-03-15 18:47:18 -07:00
|
|
|
isModification: data.isModification,
|
2016-10-27 15:27:39 -02:00
|
|
|
});
|
2016-10-10 18:33:49 -03:00
|
|
|
sem.leave();
|
|
|
|
}
|
2016-09-06 17:18:27 -03:00
|
|
|
}).catch((err) => {
|
|
|
|
sem.leave();
|
2017-01-11 20:58:15 -02:00
|
|
|
resolve(null);
|
2016-09-06 17:18:27 -03:00
|
|
|
}));
|
|
|
|
}));
|
|
|
|
});
|
|
|
|
return Promise.all(promises);
|
2016-12-01 19:59:29 -02:00
|
|
|
})
|
|
|
|
.catch((error) => {
|
2016-12-23 16:59:48 -02:00
|
|
|
if (error.message === "Not Found") {
|
2016-12-01 19:59:29 -02:00
|
|
|
return Promise.resolve([]);
|
|
|
|
}
|
|
|
|
return error;
|
2016-10-28 11:42:31 -02:00
|
|
|
});
|
2016-09-06 13:04:17 -03:00
|
|
|
}
|
2016-09-13 03:59:48 -03:00
|
|
|
|
|
|
|
unpublishedEntry(collection, slug) {
|
2016-10-28 11:42:31 -02:00
|
|
|
return this.api.readUnpublishedBranchFile(slug)
|
2017-01-11 20:58:15 -02:00
|
|
|
.then((data) => {
|
|
|
|
if (!data) return null;
|
|
|
|
return {
|
|
|
|
slug,
|
|
|
|
file: { path: data.metaData.objects.entry.path },
|
|
|
|
data: data.fileData,
|
|
|
|
metaData: data.metaData,
|
2017-03-15 18:47:18 -07:00
|
|
|
isModification: data.isModification,
|
2017-01-11 20:58:15 -02:00
|
|
|
};
|
|
|
|
});
|
2016-09-13 03:59:48 -03:00
|
|
|
}
|
2016-09-13 16:00:24 -03:00
|
|
|
|
|
|
|
updateUnpublishedEntryStatus(collection, slug, newStatus) {
|
|
|
|
return this.api.updateUnpublishedEntryStatus(collection, slug, newStatus);
|
|
|
|
}
|
2016-09-14 18:25:45 -03:00
|
|
|
|
2017-03-11 13:47:36 -05:00
|
|
|
deleteUnpublishedEntry(collection, slug) {
|
|
|
|
return this.api.deleteUnpublishedEntry(collection, slug);
|
|
|
|
}
|
2017-01-11 20:58:15 -02:00
|
|
|
publishUnpublishedEntry(collection, slug) {
|
|
|
|
return this.api.publishUnpublishedEntry(collection, slug);
|
2016-09-14 18:25:45 -03:00
|
|
|
}
|
2016-06-05 01:52:18 -07:00
|
|
|
}
|