Skip to content

Commit 8fbc5fe

Browse files
Format metadata object (#378)
* format metadata object
1 parent f766f54 commit 8fbc5fe

File tree

9 files changed

+140
-80
lines changed

9 files changed

+140
-80
lines changed

lib/jekyll-admin/server.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ def front_matter
5959

6060
def document_body
6161
body = if front_matter && !front_matter.empty?
62-
YAML.dump(front_matter).strip
62+
YAML.dump(restored_front_matter).strip
63+
.gsub(": 'null'", ": null") # restore null values
6364
else
6465
"---"
6566
end
@@ -81,6 +82,14 @@ def namespace
8182
namespace = request.path_info.split("/")[1].to_s.downcase
8283
namespace if ROUTES.include?(namespace)
8384
end
85+
86+
# verbose 'null' values in front matter
87+
def restored_front_matter
88+
front_matter.map do |key, value|
89+
value = "null" if value.nil?
90+
[key, value]
91+
end.to_h
92+
end
8493
end
8594
end
8695

src/actions/collections.js

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import moment from 'moment';
44
import { validationError } from '../actions/utils';
55
import { get, put } from '../utils/fetch';
66
import { validator } from '../utils/validation';
7-
import { slugify } from '../utils/helpers';
7+
import { slugify, trimObject } from '../utils/helpers';
88
import {
99
getTitleRequiredMessage,
1010
getFilenameRequiredMessage,
@@ -76,7 +76,7 @@ export function createDocument(collection, directory) {
7676
return put(
7777
// create or update document according to filename existence
7878
documentAPIUrl(collection, directory, path),
79-
JSON.stringify({ raw_content, front_matter }),
79+
preparePayload({ raw_content, front_matter }),
8080
{ type: ActionTypes.PUT_DOCUMENT_SUCCESS, name: "doc"},
8181
{ type: ActionTypes.PUT_DOCUMENT_FAILURE, name: "error"},
8282
dispatch
@@ -111,14 +111,30 @@ export function putDocument(collection, directory, filename) {
111111
return put(
112112
// create or update document according to filename existence
113113
documentAPIUrl(collection, directory, filename),
114-
JSON.stringify({ path: relative_path, raw_content, front_matter }),
114+
preparePayload({ path: relative_path, raw_content, front_matter }),
115115
{ type: ActionTypes.PUT_DOCUMENT_SUCCESS, name: "doc"},
116116
{ type: ActionTypes.PUT_DOCUMENT_FAILURE, name: "error"},
117117
dispatch
118118
);
119119
};
120120
}
121121

122+
export function deleteDocument(collection, directory, filename) {
123+
return (dispatch) => {
124+
return fetch(documentAPIUrl(collection, directory, filename), {
125+
method: 'DELETE'
126+
})
127+
.then(data => {
128+
dispatch({ type: ActionTypes.DELETE_DOCUMENT_SUCCESS });
129+
dispatch(fetchCollection(collection, directory));
130+
})
131+
.catch(error => dispatch({
132+
type: ActionTypes.DELETE_DOCUMENT_FAILURE,
133+
error
134+
}));
135+
};
136+
}
137+
122138
const generateFilenameFromTitle = (metadata, collection) => {
123139
if (collection == 'posts') {
124140
// if date is provided, use it, otherwise generate it with today's date
@@ -151,18 +167,4 @@ const validateDocument = (metadata, collection) => {
151167
return validator(metadata, validations, messages);
152168
};
153169

154-
export function deleteDocument(collection, directory, filename) {
155-
return (dispatch) => {
156-
return fetch(documentAPIUrl(collection, directory, filename), {
157-
method: 'DELETE'
158-
})
159-
.then(data => {
160-
dispatch({ type: ActionTypes.DELETE_DOCUMENT_SUCCESS });
161-
dispatch(fetchCollection(collection, directory));
162-
})
163-
.catch(error => dispatch({
164-
type: ActionTypes.DELETE_DOCUMENT_FAILURE,
165-
error
166-
}));
167-
};
168-
}
170+
const preparePayload = (obj) => JSON.stringify(trimObject(obj));

src/actions/datafiles.js

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,10 @@
11
import * as ActionTypes from '../constants/actionTypes';
22
import { validationError } from './utils';
33
import { get, put } from '../utils/fetch';
4-
import { toYAML, toJSON, getExtensionFromPath } from '../utils/helpers';
4+
import { toYAML, toJSON, getExtensionFromPath, trimObject } from '../utils/helpers';
55
import { validator } from '../utils/validation';
6-
import {
7-
getContentRequiredMessage,
8-
getFilenameRequiredMessage
9-
} from '../constants/lang';
10-
import {
11-
datafilesAPIUrl,
12-
datafileAPIUrl
13-
} from '../constants/api';
6+
import { getContentRequiredMessage, getFilenameRequiredMessage } from '../constants/lang';
7+
import { datafilesAPIUrl, datafileAPIUrl } from '../constants/api';
148

159
export function fetchDataFiles(directory = '') {
1610
return (dispatch) => {
@@ -52,10 +46,13 @@ export function putDataFile(directory, filename, data, new_path = '', source = '
5246
if (source == "gui") {
5347
const json = /json/i.test(ext);
5448
let metadata = getState().metadata.metadata;
49+
metadata = trimObject(metadata);
5550
data = json ? (JSON.stringify(metadata, null, 2)) : (toYAML(metadata));
5651
}
5752

58-
const payload = new_path ? { path: new_path, raw_content: data } : { raw_content: data };
53+
const payload = new_path ?
54+
{ path: new_path, raw_content: data } :
55+
{ raw_content: data };
5956

6057
// handle errors
6158
const errors = validateDatafile(filename, data);
@@ -74,17 +71,6 @@ export function putDataFile(directory, filename, data, new_path = '', source = '
7471
};
7572
}
7673

77-
function validateDatafile(filename, data) {
78-
return validator(
79-
{ filename, data },
80-
{ 'filename': 'required', 'data': 'required' },
81-
{
82-
'filename.required': getFilenameRequiredMessage(),
83-
'data.required': getContentRequiredMessage()
84-
}
85-
);
86-
}
87-
8874
export function deleteDataFile(directory, filename) {
8975
return (dispatch) => {
9076
return fetch(datafileAPIUrl(directory, filename), {
@@ -106,3 +92,14 @@ export function onDataFileChanged() {
10692
type: ActionTypes.DATAFILE_CHANGED
10793
};
10894
}
95+
96+
const validateDatafile = (filename, data) => {
97+
return validator(
98+
{ filename, data },
99+
{ 'filename': 'required', 'data': 'required' },
100+
{
101+
'filename.required': getFilenameRequiredMessage(),
102+
'data.required': getContentRequiredMessage()
103+
}
104+
);
105+
};

src/actions/pages.js

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,9 @@ import _ from 'underscore';
33
import { validationError } from '../actions/utils';
44
import { get, put } from '../utils/fetch';
55
import { validator } from '../utils/validation';
6-
import { slugify } from '../utils/helpers';
7-
import {
8-
getTitleRequiredMessage,
9-
getFilenameNotValidMessage
10-
} from '../constants/lang';
11-
import {
12-
pagesAPIUrl,
13-
pageAPIUrl
14-
} from '../constants/api';
6+
import { slugify, trimObject } from '../utils/helpers';
7+
import { getTitleRequiredMessage, getFilenameNotValidMessage } from '../constants/lang';
8+
import { pagesAPIUrl, pageAPIUrl } from '../constants/api';
159

1610
export function fetchPages(directory = '') {
1711
return (dispatch) => {
@@ -55,12 +49,12 @@ export function createPage(directory) {
5549
dispatch({type: ActionTypes.CLEAR_ERRORS});
5650
// omit raw_content, path and empty-value keys in metadata state from front_matter
5751
const front_matter = _.omit(metadata, (value, key, object) => {
58-
return key == 'raw_content' || key == 'path' || value == '';
52+
return key == 'raw_content' || key == 'path' || value === '';
5953
});
6054
//send the put request
6155
return put(
6256
pageAPIUrl(directory, path),
63-
JSON.stringify({ front_matter, raw_content }),
57+
preparePayload({ front_matter, raw_content }),
6458
{ type: ActionTypes.PUT_PAGE_SUCCESS, name: "page"},
6559
{ type: ActionTypes.PUT_PAGE_FAILURE, name: "error"},
6660
dispatch
@@ -86,33 +80,21 @@ export function putPage(directory, filename) {
8680
dispatch({type: ActionTypes.CLEAR_ERRORS});
8781
// omit raw_content, path and empty-value keys in metadata state from front_matter
8882
const front_matter = _.omit(metadata, (value, key, object) => {
89-
return key == 'raw_content' || key == 'path' || value == '';
83+
return key == 'raw_content' || key == 'path' || value === '';
9084
});
91-
const relative_path = directory ?
92-
`${directory}/${path}` : `${path}`;
85+
const relative_path = directory ? `${directory}/${path}` : `${path}`;
9386
//send the put request
9487
return put(
9588
// create or update page according to filename existence
9689
pageAPIUrl(directory, filename),
97-
JSON.stringify({ path: relative_path, front_matter, raw_content }),
90+
preparePayload({ path: relative_path, front_matter, raw_content }),
9891
{ type: ActionTypes.PUT_PAGE_SUCCESS, name: "page"},
9992
{ type: ActionTypes.PUT_PAGE_FAILURE, name: "error"},
10093
dispatch
10194
);
10295
};
10396
}
10497

105-
function validatePage(metadata) {
106-
return validator(
107-
metadata,
108-
{ 'path': 'required|filename' },
109-
{
110-
'path.required': getTitleRequiredMessage(),
111-
'path.filename': getFilenameNotValidMessage()
112-
}
113-
);
114-
}
115-
11698
export function deletePage(directory, filename) {
11799
return (dispatch) => {
118100
return fetch(pageAPIUrl(directory, filename), {
@@ -128,3 +110,16 @@ export function deletePage(directory, filename) {
128110
}));
129111
};
130112
}
113+
114+
const validatePage = (metadata) => {
115+
return validator(
116+
metadata,
117+
{ 'path': 'required|filename' },
118+
{
119+
'path.required': getTitleRequiredMessage(),
120+
'path.filename': getFilenameNotValidMessage()
121+
}
122+
);
123+
};
124+
125+
const preparePayload = (obj) => JSON.stringify(trimObject(obj));

src/components/metadata/MetaField.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ MetaField.propTypes = {
8989
moveArrayItem: PropTypes.func.isRequired,
9090
convertField: PropTypes.func.isRequired,
9191
fieldKey: PropTypes.string.isRequired,
92-
fieldValue: PropTypes.any.isRequired,
92+
fieldValue: PropTypes.any,
9393
nameAttr: PropTypes.string.isRequired,
9494
namePrefix: PropTypes.string.isRequired,
9595
key_prefix: PropTypes.string.isRequired

src/components/metadata/MetaSimple.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,17 @@ export class MetaSimple extends Component {
3434
updateFieldValue(nameAttr, e.target.value);
3535
}
3636

37-
handleEditableBlur(e) {
38-
const { nameAttr, updateFieldValue } = this.props;
39-
updateFieldValue(nameAttr, e.target.value.trim());
40-
}
41-
4237
handleDatepickerChange(date, dateStr) {
4338
const { nameAttr, updateFieldValue } = this.props;
4439
let formatted = moment(date).format("YYYY-MM-DD HH:mm:ss");
4540
updateFieldValue(nameAttr, formatted);
4641
}
4742

43+
handleEditableBlur(e) {
44+
const { nameAttr, updateFieldValue } = this.props;
45+
updateFieldValue(nameAttr, e.target.value.trim());
46+
}
47+
4848
renderEditable() {
4949
const { fieldValue } = this.props;
5050
return (
@@ -133,7 +133,7 @@ export class MetaSimple extends Component {
133133
MetaSimple.propTypes = {
134134
parentType: PropTypes.string.isRequired,
135135
fieldKey: PropTypes.string.isRequired,
136-
fieldValue: PropTypes.any.isRequired,
136+
fieldValue: PropTypes.any,
137137
updateFieldValue: PropTypes.func.isRequired,
138138
nameAttr: PropTypes.any.isRequired
139139
};

src/components/metadata/tests/metasimple.spec.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,4 @@ describe('Components::MetaSimple', () => {
4848
editable.simulate('change');
4949
expect(actions.updateFieldValue).toHaveBeenCalled();
5050
});
51-
52-
it('should call updateFieldValue when the input field is blurred', () => {
53-
const { actions, editable } = setup();
54-
editable.simulate('blur');
55-
expect(actions.updateFieldValue).toHaveBeenCalled();
56-
});
5751
});

src/utils/helpers.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,24 @@ export const preventDefault = (event) => {
113113
event.preventDefault();
114114
}
115115
};
116+
117+
/**
118+
* Given an object, trims every keys and values recursively.
119+
* @param {Object} object
120+
* @return {Object} trimmedObject
121+
*/
122+
export const trimObject = (object) => {
123+
if (!_.isObject(object)) return object;
124+
return _.keys(object).reduce((acc, key) => {
125+
if (typeof object[key] == 'string') {
126+
try {
127+
acc[key.trim()] = JSON.parse(object[key].trim());
128+
} catch (e) {
129+
acc[key.trim()] = object[key].trim();
130+
}
131+
} else {
132+
acc[key.trim()] = trimObject(object[key]);
133+
}
134+
return acc;
135+
}, Array.isArray(object) ? [] : {});
136+
};

src/utils/tests/helpers.spec.js

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {
22
toYAML, toJSON, capitalize, toTitleCase, slugify,
3-
existingUploadedFilenames, getFilenameFromPath, getExtensionFromPath
3+
existingUploadedFilenames, getFilenameFromPath, getExtensionFromPath, trimObject
44
} from '../helpers';
55

66
describe('Helper functions', () => {
@@ -155,4 +155,46 @@ describe('Helper functions', () => {
155155
expected = "";
156156
expect(getExtensionFromPath(path)).toEqual(expected);
157157
});
158+
159+
it('should trim whitespaces in object keys and values', () => {
160+
let obj = {};
161+
let expected = {};
162+
expect(trimObject(obj)).toEqual(expected);
163+
164+
obj = { ' foo ': ' bar ' };
165+
expected = { foo: 'bar' };
166+
expect(trimObject(obj)).toEqual(expected);
167+
168+
obj = {
169+
foo: ' 10 ',
170+
bar: ' false ',
171+
baz: {
172+
jekyll: 'true '
173+
}
174+
};
175+
expected = {
176+
foo: 10,
177+
bar: false,
178+
baz: {
179+
jekyll: true
180+
}
181+
};
182+
expect(trimObject(obj)).toEqual(expected);
183+
184+
obj = {
185+
foo: ' a "test" string ',
186+
'baz ': {
187+
foo: "this is 'also ' a test string "
188+
},
189+
bar: 10
190+
};
191+
expected = {
192+
foo: 'a "test" string',
193+
baz: {
194+
foo: "this is 'also ' a test string"
195+
},
196+
bar: 10
197+
};
198+
expect(trimObject(obj)).toEqual(expected);
199+
});
158200
});

0 commit comments

Comments
 (0)