Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/master' into cerealize
Browse files Browse the repository at this point in the history
  • Loading branch information
KyleAMathews committed Mar 18, 2017
2 parents 05976c8 + ef7f51c commit 4ac40dc
Show file tree
Hide file tree
Showing 40 changed files with 587 additions and 745 deletions.
3 changes: 3 additions & 0 deletions .github/ISSUE_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ include the BUG REPORT INFORMATION shown below. If you fail to provide this
information within 7 days, we cannot debug your issue and will close it. We
will, however, reopen it if you later provide the information.
If you have an issue that can be shown visually, please provide a screenshot or
gif of the problem as well.
---------------------------------------------------
BUG REPORT INFORMATION
---------------------------------------------------
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ It presents a clean UI for editing content stored in a Git repository.
You setup a YAML config to describe the content model of your site, and typically
tweak the main layout of the CMS a bit to fit your own site.

When a user navigates to `/admin` she'll be prompted to login, and once authenticated
she'll be able to create new content or edit existing content.
When a user navigates to `/admin` they'll be prompted to login, and once authenticated
they'll be able to create new content or edit existing content.

Read more about Netlify CMS [Core Concepts](docs/intro.md).

Expand Down
92 changes: 91 additions & 1 deletion docs/extending.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ Registers a template for a collection.
**React Component Props:**

* collection: The name of the collection which this preview component will be used for.
* react_component: A React component that renders the collection data. Three props will be passed to your component during render:
* react_component: A React component that renders the collection data. Four props will be passed to your component during render:
* entry: Immutable collection containing the entry data.
* widgetFor: Returns the appropriate widget preview component for a given field.
* [widgetsFor](#lists-and-objects): Returns an array of objects with widgets and associated field data. For use with list and object type entries.
* getAsset: Returns the correct filePath or in-memory preview for uploaded images.

**Example:**
Expand All @@ -63,6 +64,95 @@ CMS.registerPreviewTemplate("posts", PostPreview);
</script>
```

### Lists and Objects
The API for accessing the individual fields of list and object type entries is similar to the API
for accessing fields in standard entries, but there are a few key differences. Access to these
nested fields is facilitated through the `widgetsFor` function, which is passed to the preview
template component during render.

**Note**: as is often the case with the Netlify CMS API, arrays and objects are created with
Immutable.js. If some of the methods that we use are unfamiliar, such as `getIn`, check out
[their docs](https://facebook.github.io/immutable-js/docs/#/) to get a better understanding.

**List Example:**

```html
<script>
var AuthorsPreview = createClass({
// For list fields, the widgetFor function returns an array of objects
// which you can map over in your template. If our field is a list of
// authors containing two entries, with fields `name` and `description`,
// the return value of `widgetsFor` would look like this:
//
// [{
// data: { name: 'Mathias', description: 'Co-Founder'},
// widgets: { name: (<WidgetComponent>), description: (WidgetComponent>)}
// },
// {
// data: { name: 'Chris', description: 'Co-Founder'},
// widgets: { name: (<WidgetComponent>), description: (WidgetComponent>)}
// }]
//
// Templating would look something like this:
render: function() {
return h('div', {},
// This is a static header that would only be rendered once for the entire list
h('h1', {}, 'Authors'),
// Here we provide a simple mapping function that will be applied to each
// object in the array of authors
this.props.widgetsFor('authors').map(function(author, index) {
return h('div', {key: index},
h('hr', {}),
h('strong', {}, author.getIn(['data', 'name'])),
author.getIn(['widgets', 'description'])
);
})
);
}
});
CMS.registerPreviewTemplate("authors", AuthorsPreview);
</script>
```

**Object Example:**

```html
<script>
var GeneralPreview = createClass({
// Object fields are simpler than lists - instead of `widgetsFor` returning
// an array of objects, it returns a single object. Accessing the shape of
// that object is the same as the shape of objects returned for list fields:
//
// {
// data: { front_limit: 0, author: 'Chris' },
// widgets: { front_limit: (<WidgetComponent>), author: (WidgetComponent>)}
// }
render: function() {
var entry = this.props.entry;
var title = entry.getIn(['data', 'site_title']);
var posts = entry.getIn(['data', 'posts']);
return h('div', {},
h('h1', {}, title),
h('dl', {},
h('dt', {}, 'Posts on Frontpage'),
h('dd', {}, this.props.widgetsFor('posts').getIn(['widgets', 'front_limit']) || 0),
h('dt', {}, 'Default Author'),
h('dd', {}, this.props.widgetsFor('posts').getIn(['data', 'author']) || 'None'),
)
);
}
});
CMS.registerPreviewTemplate("general", GeneralPreview);
</script>
```

### Accessing Metadata
Preview Components also receive an additional prop: `fieldsMetaData`. It contains aditional information (besides the plain plain textual value of each field) that can be useful for preview purposes.

Expand Down
5 changes: 3 additions & 2 deletions docs/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Netlify CMS relies on the GitHub API for managing files, so you'll need to have

### Hosting with Netlify

In order to use Nelify's authentication provider service, you'll need to connect your site repo with Netlify. Netlify has published a general [Step-by-Step Guide](https://www.netlify.com/blog/2016/10/27/a-step-by-step-guide-deploying-a-static-site-or-single-page-app/) for this, along with detailed guides for many popular static site generators, including [Jekyll](https://www.netlify.com/blog/2015/10/28/a-step-by-step-guide-jekyll-3.0-on-netlify/), [Hugo](https://www.netlify.com/blog/2016/09/21/a-step-by-step-guide-hugo-on-netlify/), [Hexo](https://www.netlify.com/blog/2015/10/26/a-step-by-step-guide-hexo-on-netlify/), [Middleman](https://www.netlify.com/blog/2015/10/01/a-step-by-step-guide-middleman-on-netlify/), and more.
In order to use Netlify's authentication provider service, you'll need to connect your site repo with Netlify. Netlify has published a general [Step-by-Step Guide](https://www.netlify.com/blog/2016/10/27/a-step-by-step-guide-deploying-a-static-site-or-single-page-app/) for this, along with detailed guides for many popular static site generators, including [Jekyll](https://www.netlify.com/blog/2015/10/28/a-step-by-step-guide-jekyll-3.0-on-netlify/), [Hugo](https://www.netlify.com/blog/2016/09/21/a-step-by-step-guide-hugo-on-netlify/), [Hexo](https://www.netlify.com/blog/2015/10/26/a-step-by-step-guide-hexo-on-netlify/), [Middleman](https://www.netlify.com/blog/2015/10/01/a-step-by-step-guide-middleman-on-netlify/), and more.

### Authenticating with GitHub

Expand All @@ -33,6 +33,7 @@ These generators... | store static files in
Jekyll, GitBook | `/` (project root)
Hugo | `/static`
Hexo, Middleman | `/source`
Spike | `/views`

If your generator isn't listed here, you can check its documentation, or as a shortcut, look in your project for a `CSS` or `images` folder. They're usually processed as static files, so it's likely you can store your `admin` folder next to those. (When you've found the location, feel free to add it to these docs!).

Expand Down Expand Up @@ -196,4 +197,4 @@ Based on this example, you can go through the post types in your site and add th

With your configuration complete, it's time to try it out! Go to `yoursite.com/admin` and complete the login prompt to access the admin interface. To add users, simply add them as collaborators on the GitHub repo.

Happy posting!
Happy posting!
20 changes: 18 additions & 2 deletions example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,10 @@
h('h1', {}, title),
h('dl', {},
h('dt', {}, 'Posts on Frontpage'),
h('dd', {}, posts && posts.get('front_limit') || '0'),
h('dd', {}, this.props.widgetsFor('posts').getIn(['widgets', 'front_limit']) || 0),

h('dt', {}, 'Default Author'),
h('dd', {}, posts && posts.get('author') || 'None'),
h('dd', {}, this.props.widgetsFor('posts').getIn(['data', 'author']) || 'None'),

h('dt', {}, 'Default Thumbnail'),
h('dd', {}, thumb && h('img', {src: this.props.getAsset(thumb).toString()}))
Expand All @@ -110,8 +110,24 @@
}
});

var AuthorsPreview = createClass({
render: function() {
return h('div', {},
h('h1', {}, 'Authors'),
this.props.widgetsFor('authors').map(function(author, index) {
return h('div', {key: index},
h('hr', {}),
h('strong', {}, author.getIn(['data', 'name'])),
author.getIn(['widgets', 'description'])
);
})
);
}
});

CMS.registerPreviewTemplate("posts", PostPreview);
CMS.registerPreviewTemplate("general", GeneralPreview);
CMS.registerPreviewTemplate("authors", AuthorsPreview);
CMS.registerPreviewStyle("/example.css");
CMS.registerEditorComponent({
id: "youtube",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "netlify-cms",
"version": "0.3.12",
"version": "0.3.15",
"description": "Netlify CMS lets content editors work on structured content stored in git",
"main": "dist/cms.js",
"scripts": {
Expand Down
23 changes: 22 additions & 1 deletion src/actions/editorialWorkflow.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,8 +241,8 @@ export function persistUnpublishedEntry(collection, existingUnpublishedEntry) {
kind: 'success',
dismissAfter: 4000,
}));
dispatch(closeEntry());
dispatch(unpublishedEntryPersisted(collection, entry, transactionID));
dispatch(closeEntry());
})
.catch((error) => {
dispatch(notifSend({
Expand Down Expand Up @@ -271,6 +271,27 @@ export function updateUnpublishedEntryStatus(collection, slug, oldStatus, newSta
};
}

export function deleteUnpublishedEntry(collection, slug) {
return (dispatch, getState) => {
const state = getState();
const backend = currentBackend(state.config);
const transactionID = uuid.v4();
dispatch(unpublishedEntryPublishRequest(collection, slug, transactionID));
backend.deleteUnpublishedEntry(collection, slug)
.then(() => {
dispatch(unpublishedEntryPublished(collection, slug, transactionID));
})
.catch((error) => {
dispatch(notifSend({
message: `Failed to close PR: ${ error }`,
kind: 'danger',
dismissAfter: 8000,
}));
dispatch(unpublishedEntryPublishError(collection, slug, transactionID));
});
};
}

export function publishUnpublishedEntry(collection, slug) {
return (dispatch, getState) => {
const state = getState();
Expand Down
2 changes: 1 addition & 1 deletion src/actions/entries.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,8 @@ export function persistEntry(collection) {
kind: 'success',
dismissAfter: 4000,
}));
dispatch(closeEntry(collection));
dispatch(entryPersisted(collection, entry));
dispatch(closeEntry(collection));
})
.catch((error) => {
dispatch(notifSend({
Expand Down
37 changes: 18 additions & 19 deletions src/actions/findbar.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,38 @@
import history from '../routing/history';
import { SEARCH } from '../components/FindBar/FindBar';
import { getCollectionUrl, getNewEntryUrl } from '../lib/urlHelper';

export const RUN_COMMAND = 'RUN_COMMAND';
export const SHOW_COLLECTION = 'SHOW_COLLECTION';
export const CREATE_COLLECTION = 'CREATE_COLLECTION';
export const HELP = 'HELP';

export function run(commandName, payload) {
return { type: RUN_COMMAND, command: commandName, payload };
}

export function navigateToCollection(collectionName) {
return runCommand(SHOW_COLLECTION, { collectionName });
}

export function createNewEntryInCollection(collectionName) {
return runCommand(CREATE_COLLECTION, { collectionName });
}

export function runCommand(commandName, payload) {
return dispatch => {
switch (commandName) {
export function runCommand(command, payload) {
return (dispatch) => {
switch (command) {
case SHOW_COLLECTION:
history.push(`/collections/${payload.collectionName}`);
history.push(getCollectionUrl(payload.collectionName));
break;
case CREATE_COLLECTION:
history.push(`/collections/${payload.collectionName}/entries/new`);
history.push(getNewEntryUrl(payload.collectionName));
break;
case HELP:
window.alert('Find Bar Help (PLACEHOLDER)\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit.');
break;
case SEARCH:
history.push(`/search/${payload.searchTerm}`);
history.push(`/search/${ payload.searchTerm }`);
break;
default:
break;
}
dispatch(run(commandName, payload));
dispatch({ type: RUN_COMMAND, command, payload });
};
}

export function navigateToCollection(collectionName) {
return runCommand(SHOW_COLLECTION, { collectionName });
}

export function createNewEntryInCollection(collectionName) {
return runCommand(CREATE_COLLECTION, { collectionName });
}
7 changes: 5 additions & 2 deletions src/backends/backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const slugFormatter = (template = "{{slug}}", entryData) => {
case "day":
return (`0${ date.getDate() }`).slice(-2);
case "slug":
return identifier.trim().toLowerCase().replace(/[^a-z0-9\.\-_]+/gi, "-");
return identifier.trim().toLowerCase().replace(/[^a-z0-9\-_]+/gi, "-");
default:
return entryData.get(field, "").trim().toLowerCase().replace(/[^a-z0-9\.\-_]+/gi, "-");
}
Expand Down Expand Up @@ -176,7 +176,7 @@ class Backend {
};
}

const commitMessage = `${ (newEntry ? "Created " : "Updated ") +
const commitMessage = `${ (newEntry ? "Create " : "Update ") +
collection.get("label") }${ entryObj.slug }”`;

const mode = config.get("publish_mode");
Expand All @@ -200,6 +200,9 @@ class Backend {
return this.implementation.publishUnpublishedEntry(collection, slug);
}

deleteUnpublishedEntry(collection, slug) {
return this.implementation.deleteUnpublishedEntry(collection, slug);
}

entryToRaw(collection, entry) {
const format = resolveFormat(collection, entry.toJS());
Expand Down
32 changes: 31 additions & 1 deletion src/backends/github/API.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import LocalForage from "localforage";
import { Base64 } from "js-base64";
import _ from "lodash";
import { filterPromises } from "../../lib/promiseHelper";
import AssetProxy from "../../valueObjects/AssetProxy";
import { SIMPLE, EDITORIAL_WORKFLOW, status } from "../../constants/publishModes";
import { APIError, EditorialWorkflowError } from "../../valueObjects/errors";
Expand Down Expand Up @@ -141,7 +142,7 @@ export default class API {
return this.request(`${ this.repoURL }/contents/${ path }`, {
headers: { Accept: "application/vnd.github.VERSION.raw" },
params: { ref: branch },
cache: false,
cache: "no-store",
}).then((result) => {
if (sha) {
LocalForage.setItem(`gh.${ sha }`, result);
Expand Down Expand Up @@ -177,6 +178,15 @@ export default class API {
listUnpublishedBranches() {
console.log("%c Checking for Unpublished entries", "line-height: 30px;text-align: center;font-weight: bold"); // eslint-disable-line
return this.request(`${ this.repoURL }/git/refs/heads/cms`)
.then(branches => filterPromises(branches, (branch) => {
const branchName = branch.ref.substring("/refs/heads/".length - 1);

// Get PRs with a `head` of `branchName`. Note that this is a
// substring match, so we need to check that the `head.ref` of
// at least one of the returned objects matches `branchName`.
return this.request(`${ this.repoURL }/pulls?head=${ branchName }&state=open`)
.then(prs => prs.some(pr => pr.head.ref === branchName));
}))
.catch((error) => {
console.log("%c No Unpublished entries", "line-height: 30px;text-align: center;font-weight: bold"); // eslint-disable-line
throw error;
Expand Down Expand Up @@ -314,6 +324,14 @@ export default class API {
.then(updatedMetadata => this.storeMetadata(contentKey, updatedMetadata));
}

deleteUnpublishedEntry(collection, slug) {
const contentKey = slug;
let prNumber;
return this.retrieveMetadata(contentKey)
.then(metadata => this.closePR(metadata.pr, metadata.objects))
.then(() => this.deleteBranch(`cms/${ contentKey }`));
}

publishUnpublishedEntry(collection, slug) {
const contentKey = slug;
let prNumber;
Expand Down Expand Up @@ -367,6 +385,18 @@ export default class API {
});
}

closePR(pullrequest, objects) {
const headSha = pullrequest.head;
const prNumber = pullrequest.number;
console.log("%c Deleting PR", "line-height: 30px;text-align: center;font-weight: bold"); // eslint-disable-line
return this.request(`${ this.repoURL }/pulls/${ prNumber }`, {
method: "PATCH",
body: JSON.stringify({
state: closed,
}),
});
}

mergePR(pullrequest, objects) {
const headSha = pullrequest.head;
const prNumber = pullrequest.number;
Expand Down
Loading

0 comments on commit 4ac40dc

Please sign in to comment.