Skip to content

Commit bc0a697

Browse files
Merge pull request #351 from ashmaroli/raw-config
Support editing raw config file via admin interface
2 parents cb6faf1 + 2889238 commit bc0a697

File tree

13 files changed

+152
-56
lines changed

13 files changed

+152
-56
lines changed

docs/_frontend/actions.md

+6-2
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@ description: Actions are payloads of information that send data from the applica
77

88
### > `fetchConfig`
99

10-
Async action for fetching Jekyll project configuration (`_config.yml`)
10+
Async action for fetching an object comprised of Jekyll project configuration (from `_config.yml` by default)
1111

1212
### > `putConfig(config)`
1313

14-
Async action for updating Jekyll project configuration
14+
Async action for updating Jekyll project configuration, after ensuring the content is not empty.
15+
16+
### > `validateConfig(config)`
17+
18+
Action for checking whether the YAML editor has content.
1519

1620
### > `onEditorChange`
1721

docs/_frontend/containers.md

+7-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ Container for displaying header which includes title and homepage link.
2525
```javascript
2626
{
2727
config: Object,
28-
fetchConfig: Function
28+
fetchConfig: Function,
29+
updated: Boolean \\ optional
2930
}
3031
```
3132

@@ -82,7 +83,11 @@ The button is activated when the editor changes.
8283
putConfig: Function,
8384
error: String,
8485
updated: Boolean,
85-
editorChanged: Boolean
86+
editorChanged: Boolean,
87+
errors: Array,
88+
clearErrors: Function,
89+
router: Object,
90+
route: Object
8691
}
8792
```
8893

docs/api.md

+10-4
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,18 @@ A standard JSON object of a directory looks like this:
7373

7474
#### Data files and the config file
7575

76-
Data files and the config file are direct JSON representations of the underlying YAML File.
76+
Data files are a direct JSON representations of the underlying YAML File.
77+
A JSON object from the config file has the data segregated into two representations:
78+
79+
* `content` - the parsed configuration data as read by Jekyll.
80+
* `raw_content` - the raw data as it sits on the disk.
7781

7882
#### Static files
7983

8084
Static files are non-Jekyll files and may be binary or text.
8185

86+
---
87+
8288
### Collections
8389

8490
#### Parameters
@@ -143,13 +149,13 @@ Delete the requested page from disk.
143149

144150
#### `GET /configuration`
145151

146-
Returns the parsed site configuration.
152+
Returns a hash object comprised of the parsed configuration from the file and the raw unparsed content of the file.
147153

148154
#### `PUT /configuration`
149155

150-
Create or update the site's `_config.yml` file with the requested contents.
156+
Create or update the site's `_config.yml` file with the requested raw file content string.
151157

152-
File will be written to disk in YAML. It will not necessarily preserve whitespace or inline comments.
158+
File will be written to disk verbatim, preserving whitespace and inline comments.
153159

154160
### Static files
155161

lib/jekyll-admin/server/configuration.rb

+20-5
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ module JekyllAdmin
22
class Server < Sinatra::Base
33
namespace "/configuration" do
44
get do
5-
json raw_configuration.to_liquid
5+
json({
6+
:content => parsed_configuration,
7+
:raw_content => raw_configuration,
8+
})
69
end
710

811
put do
912
write_file(configuration_path, configuration_body)
10-
json raw_configuration.to_liquid
13+
json request_payload
1114
end
1215

1316
private
@@ -23,19 +26,31 @@ def configuration
2326
@configuration ||= Jekyll.configuration(overrides)
2427
end
2528

26-
# Raw configuration, as it sits on disk
27-
def raw_configuration
29+
# Configuration data, as read by Jekyll
30+
def parsed_configuration
2831
configuration.read_config_file(configuration_path)
2932
end
3033

34+
# Raw configuration content, as it sits on disk
35+
def raw_configuration
36+
File.read(
37+
configuration_path,
38+
Jekyll::Utils.merged_file_read_opts(site, {})
39+
)
40+
end
41+
3142
# Returns the path to the *first* config file discovered
3243
def configuration_path
3344
sanitized_path configuration.config_files(overrides).first
3445
end
3546

3647
# The user's uploaded configuration for updates
48+
# Instead of extracting `raw_content` directly from the `request_payload`,
49+
# assign the data to a new variable and then extract the `raw_content`
50+
# from it to circumvent CORS violation in `development` mode.
3751
def configuration_body
38-
YAML.dump request_payload
52+
payload = request_payload
53+
payload["raw_content"]
3954
end
4055
end
4156
end

spec/jekyll-admin/server/configuration_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def app
88
it "returns the configuration" do
99
get "/configuration"
1010
expect(last_response).to be_ok
11-
expect(last_response_parsed["foo"]).to eql("bar")
11+
expect(last_response_parsed["content"]["foo"]).to eql("bar")
1212
expect(last_response_parsed.key?("source")).to eql(false)
1313
end
1414

src/actions/config.js

+20-9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import * as ActionTypes from '../constants/actionTypes';
22
import { getConfigurationUrl, putConfigurationUrl } from '../constants/api';
3-
import { getParserErrorMessage } from '../constants/lang';
3+
import { getContentRequiredMessage } from '../constants/lang';
44
import { addNotification } from './notifications';
55
import { get, put } from '../utils/fetch';
6-
import { toJSON } from '../utils/helpers';
6+
import { validator } from '../utils/validation';
7+
import { validationError } from './utils';
78

89
export function fetchConfig() {
910
return dispatch => {
@@ -18,23 +19,33 @@ export function fetchConfig() {
1819
}
1920

2021
export function putConfig(config) {
21-
return (dispatch) => {
22-
let json;
23-
try {
24-
json = toJSON(config);
25-
} catch (e) {
26-
return dispatch(addNotification(getParserErrorMessage(), e.message, 'error'));
22+
return (dispatch, getState) => {
23+
const errors = validateConfig(config);
24+
if (errors.length) {
25+
return dispatch(validationError(errors));
2726
}
27+
// clear errors
28+
dispatch({type: ActionTypes.CLEAR_ERRORS});
2829
return put(
2930
putConfigurationUrl(),
30-
JSON.stringify(json),
31+
JSON.stringify({ raw_content: config }),
3132
{ type: ActionTypes.PUT_CONFIG_SUCCESS, name: "config"},
3233
{ type: ActionTypes.PUT_CONFIG_FAILURE, name: "error"},
3334
dispatch
3435
);
3536
};
3637
}
3738

39+
function validateConfig(config) {
40+
return validator(
41+
{ config },
42+
{ 'config': 'required' },
43+
{
44+
'config.required': getContentRequiredMessage()
45+
}
46+
);
47+
}
48+
3849
export function onEditorChange() {
3950
return {
4051
type: ActionTypes.CONFIG_EDITOR_CHANGED

src/actions/tests/config.spec.js

+10-9
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,13 @@ describe('Actions::Config', () => {
3636

3737
it('updates the configuration', () => {
3838
nock(API)
39-
.put('/configuration', config)
39+
.put('/configuration')
4040
.reply(200, config);
4141

42-
const expectedAction = [{
43-
type: types.PUT_CONFIG_SUCCESS,
44-
config
45-
}];
42+
const expectedAction = [
43+
{ type: types.CLEAR_ERRORS },
44+
{ type: types.PUT_CONFIG_SUCCESS, config }
45+
];
4646

4747
const store = mockStore({ config: {} });
4848
return store.dispatch(actions.putConfig(config_yaml))
@@ -56,15 +56,16 @@ describe('Actions::Config', () => {
5656
.put('/configuration', config)
5757
.replyWithError('something awful happened');
5858

59-
const expectedAction = {
60-
type: types.PUT_CONFIG_FAILURE
61-
};
59+
const expectedAction = [
60+
{ type: types.CLEAR_ERRORS },
61+
{ type: types.PUT_CONFIG_FAILURE }
62+
];
6263

6364
const store = mockStore({ config: {} });
6465

6566
return store.dispatch(actions.putConfig(config_yaml))
6667
.then(() => {
67-
expect(store.getActions()[0].type).toEqual(expectedAction.type);
68+
expect(store.getActions()[1].type).toEqual(expectedAction[1].type);
6869
});
6970
});
7071
});

src/containers/Header.js

+15-2
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,25 @@ export class Header extends Component {
1212
fetchConfig();
1313
}
1414

15+
componentWillReceiveProps(nextProps) {
16+
if (this.props.updated !== nextProps.updated) {
17+
const { fetchConfig } = this.props;
18+
fetchConfig();
19+
}
20+
}
21+
1522
render() {
1623
const { config } = this.props;
24+
const configuration = config.content;
1725
return (
1826
<div className="header">
1927
<h3 className="title">
2028
<Link target="_blank" to={`/`}>
2129
<i className="fa fa-home" />
22-
<span>{config.title || 'You have no title!'}</span>
30+
{
31+
configuration &&
32+
<span>{configuration.title || 'You have no title!'}</span>
33+
}
2334
</Link>
2435
</h3>
2536
<span className="version">{VERSION}</span>
@@ -30,11 +41,13 @@ export class Header extends Component {
3041

3142
Header.propTypes = {
3243
fetchConfig: PropTypes.func.isRequired,
33-
config: PropTypes.object.isRequired
44+
config: PropTypes.object.isRequired,
45+
updated: PropTypes.bool
3446
};
3547

3648
const mapStateToProps = (state) => ({
3749
config: state.config.config,
50+
updated: state.config.updated,
3851
isFetching: state.config.isFetching
3952
});
4053

src/containers/tests/fixtures/index.js

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
export const config = {
2-
title: "Your awesome title",
3-
4-
description: "Write an awesome description for your new site here.",
5-
baseurl: "",
6-
url: "http://yourdomain.com"
2+
content: {
3+
title: "Your awesome title",
4+
5+
baseurl: "",
6+
url: "http://yourdomain.com"
7+
},
8+
raw_content: "title: Your awesome title\nemail: [email protected]\nbaseurl:\nurl: http://yourdomain.com"
79
};
810

911
export const collections = [

src/containers/tests/header.spec.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ function setup() {
2424
describe('Containers::Header', () => {
2525
it('should render correctly', () => {
2626
const { title, component } = setup();
27+
const { content } = config;
2728
const actual = title.text();
28-
const expected = config.title;
29+
const expected = content.title;
2930
expect(actual).toEqual(expected);
3031
});
3132

src/containers/views/Configuration.js

+28-10
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import { bindActionCreators } from 'redux';
44
import { withRouter } from 'react-router';
55
import { HotKeys } from 'react-hotkeys';
66
import Editor from '../../components/Editor';
7+
import Errors from '../../components/Errors';
78
import Button from '../../components/Button';
89
import { putConfig, onEditorChange } from '../../actions/config';
10+
import { clearErrors } from '../../actions/utils';
911
import { getLeaveMessage } from '../../constants/lang';
1012
import { toYAML, preventDefault } from '../../utils/helpers';
1113

@@ -22,6 +24,14 @@ export class Configuration extends Component {
2224
router.setRouteLeaveHook(route, this.routerWillLeave.bind(this));
2325
}
2426

27+
componentWillUnmount() {
28+
const { clearErrors, errors } = this.props;
29+
// clear errors if any
30+
if (errors.length) {
31+
clearErrors();
32+
}
33+
}
34+
2535
routerWillLeave(nextLocation) {
2636
if (this.props.editorChanged) {
2737
return getLeaveMessage();
@@ -40,14 +50,15 @@ export class Configuration extends Component {
4050
}
4151

4252
render() {
43-
const { editorChanged, onEditorChange, config, updated } = this.props;
44-
53+
const { editorChanged, onEditorChange, config, updated, errors } = this.props;
54+
const { raw_content } = config;
4555
const keyboardHandlers = {
4656
'save': this.handleClickSave,
4757
};
4858

4959
return (
50-
<HotKeys handlers={keyboardHandlers}>
60+
<HotKeys handlers={keyboardHandlers} className="single">
61+
{errors && errors.length > 0 && <Errors errors={errors} />}
5162
<div className="content-header">
5263
<h1>Configuration</h1>
5364
<div className="page-buttons">
@@ -58,11 +69,14 @@ export class Configuration extends Component {
5869
triggered={updated} />
5970
</div>
6071
</div>
61-
<Editor
62-
editorChanged={editorChanged}
63-
onEditorChange={onEditorChange}
64-
content={toYAML(config)}
65-
ref="editor" />
72+
{
73+
raw_content &&
74+
<Editor
75+
editorChanged={editorChanged}
76+
onEditorChange={onEditorChange}
77+
content={raw_content}
78+
ref="editor" />
79+
}
6680
</HotKeys>
6781
);
6882
}
@@ -74,19 +88,23 @@ Configuration.propTypes = {
7488
putConfig: PropTypes.func.isRequired,
7589
updated: PropTypes.bool.isRequired,
7690
editorChanged: PropTypes.bool.isRequired,
91+
errors: PropTypes.array.isRequired,
92+
clearErrors: PropTypes.func.isRequired,
7793
router: PropTypes.object.isRequired,
7894
route: PropTypes.object.isRequired
7995
};
8096

8197
const mapStateToProps = (state) => ({
8298
config: state.config.config,
8399
updated: state.config.updated,
84-
editorChanged: state.config.editorChanged
100+
editorChanged: state.config.editorChanged,
101+
errors: state.utils.errors
85102
});
86103

87104
const mapDispatchToProps = (dispatch) => bindActionCreators({
88105
putConfig,
89-
onEditorChange
106+
onEditorChange,
107+
clearErrors
90108
}, dispatch);
91109

92110
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Configuration));

0 commit comments

Comments
 (0)