From c8cbbe6750f3ddc4416d8c0ca3c9936790bbb750 Mon Sep 17 00:00:00 2001 From: Ashwin Maroli Date: Fri, 27 Dec 2019 17:59:21 +0530 Subject: [PATCH 01/16] Refactor ducks --- src/containers/views/DraftEdit.js | 2 +- src/containers/views/DraftNew.js | 2 +- src/ducks/action_tests/drafts.spec.js | 12 +-- src/ducks/collections.js | 94 ++++++++++++------------ src/ducks/drafts.js | 102 +++++++++++++------------- src/ducks/pages.js | 66 +++++------------ src/utils/helpers.js | 10 +++ src/utils/validation.js | 48 +++++++++--- 8 files changed, 174 insertions(+), 162 deletions(-) diff --git a/src/containers/views/DraftEdit.js b/src/containers/views/DraftEdit.js index 542a7ec65..b1f3580ed 100644 --- a/src/containers/views/DraftEdit.js +++ b/src/containers/views/DraftEdit.js @@ -75,7 +75,7 @@ export class DraftEdit extends Component { if (fieldChanged) { const [directory, ...rest] = params.splat; const filename = rest.join('.'); - putDraft('edit', directory, filename); + putDraft(directory, filename); } }; diff --git a/src/containers/views/DraftNew.js b/src/containers/views/DraftNew.js index 6262e58f3..cb215df9e 100644 --- a/src/containers/views/DraftNew.js +++ b/src/containers/views/DraftNew.js @@ -42,7 +42,7 @@ export class DraftNew extends Component { handleClickSave = e => { preventDefault(e); const { fieldChanged, putDraft, params } = this.props; - fieldChanged && putDraft('create', params.splat); + fieldChanged && putDraft(params.splat); }; render() { diff --git a/src/ducks/action_tests/drafts.spec.js b/src/ducks/action_tests/drafts.spec.js index 14bcc8f1a..e7434c411 100644 --- a/src/ducks/action_tests/drafts.spec.js +++ b/src/ducks/action_tests/drafts.spec.js @@ -100,7 +100,7 @@ describe('Actions::Drafts', () => { const store = mockStore({ metadata: { metadata: draft } }); return store - .dispatch(draftsDuck.putDraft('edit', null, 'draft-post.md')) + .dispatch(draftsDuck.putDraft(null, 'draft-post.md')) .then(() => { expect(store.getActions()).toEqual(expectedActions); }); @@ -118,14 +118,14 @@ describe('Actions::Drafts', () => { const store = mockStore({ metadata: { metadata: new_draft } }); - return store.dispatch(draftsDuck.putDraft('create')).then(() => { + return store.dispatch(draftsDuck.createDraft('')).then(() => { expect(store.getActions()).toEqual(expectedActions); }); }); it('creates the draft with autogenerated filename', () => { nock(API) - .put('/drafts/draft-post.md') + .put(`/drafts/${new_draft.relative_path}`) .reply(200, draft); const expectedActions = [ @@ -137,7 +137,7 @@ describe('Actions::Drafts', () => { metadata: { metadata: { ...new_draft, path: '' } }, }); - return store.dispatch(draftsDuck.putDraft('create')).then(() => { + return store.dispatch(draftsDuck.createDraft('')).then(() => { expect(store.getActions()).toEqual(expectedActions); }); }); @@ -154,7 +154,7 @@ describe('Actions::Drafts', () => { const store = mockStore({ metadata: { metadata: draft } }); - return store.dispatch(draftsDuck.putDraft('edit')).then(() => { + return store.dispatch(draftsDuck.putDraft(draft.relative_path)).then(() => { expect(store.getActions()[1].type).toEqual(expectedActions[1].type); }); }); @@ -171,7 +171,7 @@ describe('Actions::Drafts', () => { metadata: { metadata: { path: '', title: '' } }, }); - store.dispatch(draftsDuck.putDraft('create')); + store.dispatch(draftsDuck.putDraft('')); expect(store.getActions()).toEqual(expectedActions); }); }); diff --git a/src/ducks/collections.js b/src/ducks/collections.js index 6b1622396..4af15f2ad 100644 --- a/src/ducks/collections.js +++ b/src/ducks/collections.js @@ -3,7 +3,11 @@ import moment from 'moment'; import { CLEAR_ERRORS, validationError } from './utils'; import { get, put } from '../utils/fetch'; import { validator } from '../utils/validation'; -import { slugify, trimObject } from '../utils/helpers'; +import { + slugify, + preparePayload, + getFrontMatterFromMetdata, +} from '../utils/helpers'; import { collectionsAPIUrl, collectionAPIUrl, @@ -78,23 +82,17 @@ export const createDocument = (collection, directory) => ( ) => { // get edited fields from metadata state const metadata = getState().metadata.metadata; - let { path, raw_content, title } = metadata; - // if path is not given or equals to directory, generate filename from the title - if ((!path || `${path}/` == directory) && title) { - path = generateFilenameFromTitle(metadata, collection); // override empty path - } else { - // validate otherwise - const errors = validateDocument(metadata, collection); - if (errors.length) { - return dispatch(validationError(errors)); - } - } + + // get path or return if metadata doesn't validate + const { path, errors } = validateMetadata(metadata, collection, directory); + if (errors.length) return dispatch(validationError(errors)); + // clear errors dispatch({ type: CLEAR_ERRORS }); - // omit raw_content, path and empty-value keys in metadata state from front_matter - const front_matter = _.omit(metadata, (value, key, object) => { - return key == 'raw_content' || key == 'path' || value == ''; - }); + + const front_matter = getFrontMatterFromMetdata(metadata); + const raw_content = metadata.raw_content; + // send the put request return put( // create or update document according to filename existence @@ -112,27 +110,22 @@ export const putDocument = (collection, directory, filename) => ( ) => { // get edited fields from metadata state const metadata = getState().metadata.metadata; - let { path, raw_content, title } = metadata; - // if path is not given or equals to directory, generate filename from the title - if ((!path || `${path}/` == directory) && title) { - path = generateFilenameFromTitle(metadata, collection); // override empty path - } else { - // validate otherwise - const errors = validateDocument(metadata, collection); - if (errors.length) { - return dispatch(validationError(errors)); - } - } + + // get path or abort if metadata doesn't validate + const { path, errors } = validateMetadata(metadata, collection, directory); + if (errors.length) return dispatch(validationError(errors)); + // clear errors dispatch({ type: CLEAR_ERRORS }); - // omit raw_content, path and empty-value keys in metadata state from front_matter - const front_matter = _.omit(metadata, (value, key, object) => { - return key == 'raw_content' || key == 'path' || value == ''; - }); + + const raw_content = metadata.raw_content; + const front_matter = getFrontMatterFromMetdata(metadata); + // add collection type prefix to relative path const relative_path = directory ? `_${collection}/${directory}/${path}` : `_${collection}/${path}`; + // send the put request return put( // create or update document according to filename existence @@ -161,18 +154,28 @@ export const deleteDocument = (collection, directory, filename) => dispatch => { ); }; -const generateFilenameFromTitle = (metadata, collection) => { +const getFilenameFromTitle = (title, collection, date) => { + const slugifiedTitle = slugify(title); if (collection == 'posts') { // if date is provided, use it, otherwise generate it with today's date - let date; - if (metadata.date) { - date = metadata.date.split(' ')[0]; - } else { - date = moment().format('YYYY-MM-DD'); - } - return `${date}-${slugify(metadata.title)}.md`; + const docDate = date ? date.split(' ')[0] : moment().format('YYYY-MM-DD'); + return `${docDate}-${slugifiedTitle}.md`; } - return `${slugify(metadata.title)}.md`; + return `${slugifiedTitle}.md`; +}; + +const validateMetadata = (metadata, collection, directory) => { + let { path, title, date } = metadata; + let errors = []; + + // if path is not given or equals to directory, generate filename from the title + if ((!path || `${path}/` == directory) && title) { + path = getFilenameFromTitle(title, collection, date); // override empty path + } else { + // validate otherwise + errors = validateDocument(metadata, collection); + } + return { path, errors }; }; const validateDocument = (metadata, collection) => { @@ -193,8 +196,6 @@ const validateDocument = (metadata, collection) => { return validator(metadata, validations, messages); }; -const preparePayload = obj => JSON.stringify(trimObject(obj)); - // Reducer export default function collections( state = { @@ -266,17 +267,14 @@ export default function collections( // Selectors export const filterBySearchInput = (list, input) => { - if (!list) { - return []; - } if (input) { - return _.filter(list, item => { - if (item.type) { + return list.filter(item => { + if (item.name) { return item.name.toLowerCase().includes(input.toLowerCase()); } else { return item.title.toLowerCase().includes(input.toLowerCase()); } }); } - return list; + return list || []; }; diff --git a/src/ducks/drafts.js b/src/ducks/drafts.js index 02aa2548f..9e39c41c5 100644 --- a/src/ducks/drafts.js +++ b/src/ducks/drafts.js @@ -6,29 +6,28 @@ import { PUT_DOCUMENT_FAILURE, } from './collections'; import { get, put } from '../utils/fetch'; -import { validator } from '../utils/validation'; -import { slugify, trimObject } from '../utils/helpers'; +import { validateMetadata } from '../utils/validation'; +import { preparePayload, getFrontMatterFromMetdata } from '../utils/helpers'; import { draftsAPIUrl, draftAPIUrl, documentAPIUrl } from '../constants/api'; import translations from '../translations'; const { getTitleRequiredMessage, getFilenameNotValidMessage } = translations; +// Action Types export const FETCH_DRAFTS_REQUEST = 'FETCH_DRAFTS_REQUEST'; export const FETCH_DRAFTS_SUCCESS = 'FETCH_DRAFTS_SUCCESS'; export const FETCH_DRAFTS_FAILURE = 'FETCH_DRAFTS_FAILURE'; - export const FETCH_DRAFT_REQUEST = 'FETCH_DRAFT_REQUEST'; export const FETCH_DRAFT_SUCCESS = 'FETCH_DRAFT_SUCCESS'; export const FETCH_DRAFT_FAILURE = 'FETCH_DRAFT_FAILURE'; - export const PUT_DRAFT_REQUEST = 'PUT_DRAFT_REQUEST'; export const PUT_DRAFT_SUCCESS = 'PUT_DRAFT_SUCCESS'; export const PUT_DRAFT_FAILURE = 'PUT_DRAFT_FAILURE'; - export const DELETE_DRAFT_REQUEST = 'DELETE_DRAFT_REQUEST'; export const DELETE_DRAFT_SUCCESS = 'DELETE_DRAFT_SUCCESS'; export const DELETE_DRAFT_FAILURE = 'DELETE_DRAFT_FAILURE'; +// Actions export const fetchDrafts = (directory = '') => dispatch => { dispatch({ type: FETCH_DRAFTS_REQUEST }); return get( @@ -49,44 +48,54 @@ export const fetchDraft = (directory, filename) => dispatch => { ); }; -export const putDraft = (mode, directory, filename = '') => ( - dispatch, - getState -) => { +export const createDraft = directory => (dispatch, getState) => { // get edited fields from metadata state const metadata = getState().metadata.metadata; - let { path, raw_content, title } = metadata; - // if path is not given or equals to directory, generate filename from the title - if (!path && title) { - path = `${slugify(title)}.md`; - } else if (!path && !title) { - return dispatch(validationError(validateDraft(metadata))); - } + // get path or return if metadata doesn't validate + const { path, errors } = validateMetadata(metadata, directory); + if (errors.length) return dispatch(validationError(errors)); + // clear errors dispatch({ type: CLEAR_ERRORS }); - // omit raw_content, path and empty-value keys in metadata state from front_matter - const front_matter = _.omit(metadata, (value, key, object) => { - return key == 'raw_content' || key == 'path' || value === ''; - }); - - let payload; - if (mode == 'create') { - // strip '_drafts/' from path when provided - filename = path.replace('_drafts/', ''); - payload = { front_matter, raw_content }; - } else { - let writePath = directory - ? `_drafts/${directory}/${path}` - : `_drafts/${path}`; - payload = { path: writePath, front_matter, raw_content }; - } + const raw_content = metadata.raw_content; + const front_matter = getFrontMatterFromMetdata(metadata); + + // strip '_drafts/' from path when provided + const filename = path.replace('_drafts/', ''); //send the put request return put( draftAPIUrl(directory, filename), - preparePayload(payload), + preparePayload({ front_matter, raw_content }), + { type: PUT_DRAFT_SUCCESS, name: 'draft' }, + { type: PUT_DRAFT_FAILURE, name: 'error' }, + dispatch + ); +}; + +export const putDraft = (directory, filename) => (dispatch, getState) => { + // get edited fields from metadata state + const metadata = getState().metadata.metadata; + + // get path or return if metadata doesn't validate + const { path, errors } = validateMetadata(metadata, directory); + if (errors.length) return dispatch(validationError(errors)); + + // clear errors + dispatch({ type: CLEAR_ERRORS }); + + const raw_content = metadata.raw_content; + const front_matter = getFrontMatterFromMetdata(metadata); + const relative_path = directory + ? `_drafts/${directory}/${path}` + : `_drafts/${path}`; + + //send the put request + return put( + draftAPIUrl(directory, filename), + preparePayload({ path: relative_path, front_matter, raw_content }), { type: PUT_DRAFT_SUCCESS, name: 'draft' }, { type: PUT_DRAFT_FAILURE, name: 'error' }, dispatch @@ -112,12 +121,16 @@ export const deleteDraft = (directory, filename) => dispatch => { export const publishDraft = (directory, filename) => (dispatch, getState) => { const metadata = getState().metadata.metadata; - const { raw_content } = metadata; - // omit raw_content, path and empty-value keys in metadata state from front_matter - const front_matter = _.omit(metadata, (value, key, object) => { - return key == 'raw_content' || key == 'path' || value == ''; - }); + // return if metadata doesn't validate + const { errors } = validateMetadata(metadata, directory); + if (errors.length) return dispatch(validationError(errors)); + + // clear errors + dispatch({ type: CLEAR_ERRORS }); + + const raw_content = metadata.raw_content; + const front_matter = getFrontMatterFromMetdata(metadata); return put( documentAPIUrl('posts', directory, filename), @@ -128,18 +141,7 @@ export const publishDraft = (directory, filename) => (dispatch, getState) => { ); }; -const validateDraft = metadata => - validator( - metadata, - { path: 'required|filename' }, - { - 'path.required': getTitleRequiredMessage(), - 'path.filename': getFilenameNotValidMessage(), - } - ); - -const preparePayload = obj => JSON.stringify(trimObject(obj)); - +// Reducer export default function drafts( state = { drafts: [], diff --git a/src/ducks/pages.js b/src/ducks/pages.js index cbd7ca0b2..496aef24b 100644 --- a/src/ducks/pages.js +++ b/src/ducks/pages.js @@ -1,13 +1,10 @@ import _ from 'underscore'; import { CLEAR_ERRORS, validationError } from './utils'; import { get, put } from '../utils/fetch'; -import { validator } from '../utils/validation'; -import { slugify, trimObject } from '../utils/helpers'; +import { validateMetadata } from '../utils/validation'; +import { preparePayload, getFrontMatterFromMetdata } from '../utils/helpers'; import { pagesAPIUrl, pageAPIUrl } from '../constants/api'; -import translations from '../translations'; -const { getTitleRequiredMessage, getFilenameNotValidMessage } = translations; - // Action Types export const FETCH_PAGES_REQUEST = 'FETCH_PAGES_REQUEST'; export const FETCH_PAGES_SUCCESS = 'FETCH_PAGES_SUCCESS'; @@ -46,22 +43,17 @@ export const fetchPage = (directory, filename) => dispatch => { export const createPage = directory => (dispatch, getState) => { // get edited fields from metadata state const metadata = getState().metadata.metadata; - let { path, raw_content, title } = metadata; - // if path is not given or equals to directory, generate filename from the title - if (!path && title) { - path = `${slugify(title)}.md`; - } else { - const errors = validatePage(metadata); - if (errors.length) { - return dispatch(validationError(errors)); - } - } + + // get path or return if metadata doesn't validate + const { path, errors } = validateMetadata(metadata, directory); + if (errors.length) return dispatch(validationError(errors)); + // clear errors dispatch({ type: CLEAR_ERRORS }); - // omit raw_content, path and empty-value keys in metadata state from front_matter - const front_matter = _.omit(metadata, (value, key, object) => { - return key == 'raw_content' || key == 'path' || value === ''; - }); + + const raw_content = metadata.raw_content; + const front_matter = getFrontMatterFromMetdata(metadata); + //send the put request return put( pageAPIUrl(directory, path), @@ -75,26 +67,20 @@ export const createPage = directory => (dispatch, getState) => { export const putPage = (directory, filename) => (dispatch, getState) => { // get edited fields from metadata state const metadata = getState().metadata.metadata; - let { path, raw_content, title } = metadata; - // if path is not given or equals to directory, generate filename from the title - if (!path && title) { - path = `${slugify(title)}.md`; - } else { - const errors = validatePage(metadata); - if (errors.length) { - return dispatch(validationError(errors)); - } - } + + // get path or abort if metadata doesn't validate + const { path, errors } = validateMetadata(metadata, directory); + if (errors.length) return dispatch(validationError(errors)); + // clear errors dispatch({ type: CLEAR_ERRORS }); - // omit raw_content, path and empty-value keys in metadata state from front_matter - const front_matter = _.omit(metadata, (value, key, object) => { - return key == 'raw_content' || key == 'path' || value === ''; - }); + + const raw_content = metadata.raw_content; + const front_matter = getFrontMatterFromMetdata(metadata); const relative_path = directory ? `${directory}/${path}` : `${path}`; + //send the put request return put( - // create or update page according to filename existence pageAPIUrl(directory, filename), preparePayload({ path: relative_path, front_matter, raw_content }), { type: PUT_PAGE_SUCCESS, name: 'page' }, @@ -120,18 +106,6 @@ export const deletePage = (directory, filename) => dispatch => { ); }; -const validatePage = metadata => - validator( - metadata, - { path: 'required|filename' }, - { - 'path.required': getTitleRequiredMessage(), - 'path.filename': getFilenameNotValidMessage(), - } - ); - -const preparePayload = obj => JSON.stringify(trimObject(obj)); - // Reducer export default function pages( state = { diff --git a/src/utils/helpers.js b/src/utils/helpers.js index bf66b8e18..c3d4a816d 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -155,3 +155,13 @@ export const getDocumentTitle = (type, splat, prefix = '') => { const label = toTitleCase(type.toString()); return [prefix, splat, label].filter(Boolean).join(' | '); }; + +// omit raw_content, path and empty-value keys in metadata state from front_matter +export const getFrontMatterFromMetdata = metadata => { + return _.omit(metadata, (value, key, object) => { + return key == 'raw_content' || key == 'path' || value == ''; + }); +}; + +export const preparePayload = obj => JSON.stringify(trimObject(obj)); +export const getFilenameFromTitle = title => `${slugify(title)}.md`; diff --git a/src/utils/validation.js b/src/utils/validation.js index 9655bfabb..dd31781df 100644 --- a/src/utils/validation.js +++ b/src/utils/validation.js @@ -1,8 +1,35 @@ import _ from 'underscore'; +import { getFilenameFromTitle } from '../utils/helpers'; + +import translations from '../translations'; +const { getTitleRequiredMessage, getFilenameNotValidMessage } = translations; const DATE_FILENAME_MATCHER = /^(?:.+\/)*(\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1]))-(.*)(\.[^.]+)$/; const FILENAME_MATCHER = /^(.*)(\.[^.]+)$/; +const validated = (field, single) => { + switch (single) { + case 'required': + return !!field; + case 'date': + return DATE_FILENAME_MATCHER.test(field); + case 'filename': + return FILENAME_MATCHER.test(field); + default: + return false; + } +}; + +const validatePage = metadata => + validator( + metadata, + { path: 'required|filename' }, + { + 'path.required': getTitleRequiredMessage(), + 'path.filename': getFilenameNotValidMessage(), + } + ); + /** * Returns error messages if the given values does not pass the provided validations. * @param {Object} values @@ -23,15 +50,16 @@ export const validator = (values, validations, messages) => { return errorMessages; }; -const validated = (field, single) => { - switch (single) { - case 'required': - return !!field; - case 'date': - return DATE_FILENAME_MATCHER.test(field); - case 'filename': - return FILENAME_MATCHER.test(field); - default: - return false; +export const validateMetadata = (metadata, directory) => { + let { path, title } = metadata; + let errors = []; + + // if path is not given or equals to directory, generate filename from the title + if ((!path || `${path}/` == directory) && title) { + path = getFilenameFromTitle(title); // override empty path + } else { + // validate otherwise + errors = validatePage(metadata); } + return { path, errors }; }; From 52ace1b1f5568d0a86b49364fb95881305b47ca0 Mon Sep 17 00:00:00 2001 From: Ashwin Maroli Date: Fri, 27 Dec 2019 19:52:55 +0530 Subject: [PATCH 02/16] Update DraftNew component --- src/containers/views/DraftNew.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/containers/views/DraftNew.js b/src/containers/views/DraftNew.js index cb215df9e..8c9801210 100644 --- a/src/containers/views/DraftNew.js +++ b/src/containers/views/DraftNew.js @@ -6,7 +6,7 @@ import { browserHistory, withRouter } from 'react-router'; import DocumentTitle from 'react-document-title'; import CreateMarkdownPage from '../../components/CreateMarkdownPage'; import { updateTitle, updateBody, updatePath } from '../../ducks/metadata'; -import { putDraft } from '../../ducks/drafts'; +import { createDraft } from '../../ducks/drafts'; import { clearErrors } from '../../ducks/utils'; import { preventDefault, getDocumentTitle } from '../../utils/helpers'; import { ADMIN_PREFIX } from '../../constants'; @@ -41,8 +41,8 @@ export class DraftNew extends Component { handleClickSave = e => { preventDefault(e); - const { fieldChanged, putDraft, params } = this.props; - fieldChanged && putDraft(params.splat); + const { fieldChanged, createDraft, params } = this.props; + fieldChanged && createDraft(params.splat); }; render() { @@ -79,7 +79,7 @@ export class DraftNew extends Component { } DraftNew.propTypes = { - putDraft: PropTypes.func.isRequired, + createDraft: PropTypes.func.isRequired, updateTitle: PropTypes.func.isRequired, updateBody: PropTypes.func.isRequired, updatePath: PropTypes.func.isRequired, @@ -108,7 +108,7 @@ const mapDispatchToProps = dispatch => updateTitle, updateBody, updatePath, - putDraft, + createDraft, clearErrors, }, dispatch From 2acfb5a5bc82c7e8ad57776213396aac5c115531 Mon Sep 17 00:00:00 2001 From: Ashwin Maroli Date: Fri, 27 Dec 2019 19:53:41 +0530 Subject: [PATCH 03/16] Update tests for draftsDuck --- src/ducks/action_tests/drafts.spec.js | 40 ++++++++++++++++++++++-- src/ducks/action_tests/fixtures/index.js | 13 ++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/ducks/action_tests/drafts.spec.js b/src/ducks/action_tests/drafts.spec.js index e7434c411..78e03f5d6 100644 --- a/src/ducks/action_tests/drafts.spec.js +++ b/src/ducks/action_tests/drafts.spec.js @@ -1,11 +1,13 @@ import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import moment from 'moment'; import * as draftsDuck from '../drafts'; +import * as collectionsDuck from '../collections'; import * as utilsDuck from '../utils'; import { API } from '../../constants/api'; import nock from 'nock'; -import { draft, new_draft } from './fixtures'; +import { draft, new_draft, publishedDraft } from './fixtures'; const middlewares = [thunk]; const mockStore = configureMockStore(middlewares); @@ -159,6 +161,32 @@ describe('Actions::Drafts', () => { }); }); + it('publishes the draft as a post successfully', () => { + const today = moment().format('YYYY-MM-DD'); + const datedfilename = `${today}-draft-post.md`; + const doc = { + ...publishedDraft, + date: `${today} 00:00:00 +0200`, + }; + + nock(API) + .put(`/collections/posts/${datedfilename}`) + .reply(200, doc); + + const expectedActions = [ + { type: utilsDuck.CLEAR_ERRORS }, + { type: collectionsDuck.PUT_DOCUMENT_SUCCESS, doc }, + ]; + + const store = mockStore({ metadata: { metadata: publishedDraft } }); + + return store + .dispatch(draftsDuck.publishDraft(null, datedfilename)) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + it('creates VALIDATION_ERROR if required fields are not provided.', () => { const expectedActions = [ { @@ -171,7 +199,15 @@ describe('Actions::Drafts', () => { metadata: { metadata: { path: '', title: '' } }, }); - store.dispatch(draftsDuck.putDraft('')); + store.dispatch(draftsDuck.createDraft('')); expect(store.getActions()).toEqual(expectedActions); + + store.dispatch(draftsDuck.putDraft(null, 'draft-post.md')); + expect(store.getActions()).toEqual(expectedActions.concat(expectedActions)); + + store.dispatch(draftsDuck.publishDraft(null, 'draft-post.md')); + expect(store.getActions()).toEqual( + expectedActions.concat(expectedActions).concat(expectedActions) + ); }); }); diff --git a/src/ducks/action_tests/fixtures/index.js b/src/ducks/action_tests/fixtures/index.js index bf1f1d958..913b5222a 100644 --- a/src/ducks/action_tests/fixtures/index.js +++ b/src/ducks/action_tests/fixtures/index.js @@ -126,6 +126,19 @@ export const draft = { } }; +export const publishedDraft = { + raw_content: "# Test Draft\n", + name: "draft-post.md", + relative_path: "draft-post.md", + title: "Draft Post", + slug: "draft-post", + collection: "posts", + draft: true, + front_matter: { + title: "Draft Post" + } +}; + export const new_draft = { raw_content: "# Test Draft\n", name: "draft-post.md", From 26188d0505693cf0f629775684dc9fa57c30ab7b Mon Sep 17 00:00:00 2001 From: Ashwin Maroli Date: Fri, 27 Dec 2019 22:41:47 +0530 Subject: [PATCH 04/16] Use { del } from 'utils/fetch' to dispatch DELETE --- src/ducks/collections.js | 26 +++++++++++--------------- src/ducks/datafiles.js | 30 ++++++++++++------------------ src/ducks/drafts.js | 26 +++++++++++--------------- src/ducks/pages.js | 22 +++++++--------------- src/ducks/staticfiles.js | 26 +++++++++++--------------- src/utils/fetch.js | 14 ++++++-------- 6 files changed, 58 insertions(+), 86 deletions(-) diff --git a/src/ducks/collections.js b/src/ducks/collections.js index 4af15f2ad..70b7ea3ad 100644 --- a/src/ducks/collections.js +++ b/src/ducks/collections.js @@ -1,7 +1,7 @@ import _ from 'underscore'; import moment from 'moment'; import { CLEAR_ERRORS, validationError } from './utils'; -import { get, put } from '../utils/fetch'; +import { get, put, del } from '../utils/fetch'; import { validator } from '../utils/validation'; import { slugify, @@ -138,20 +138,16 @@ export const putDocument = (collection, directory, filename) => ( }; export const deleteDocument = (collection, directory, filename) => dispatch => { - return fetch(documentAPIUrl(collection, directory, filename), { - method: 'DELETE', - credentials: 'same-origin', - }) - .then(data => { - dispatch({ type: DELETE_DOCUMENT_SUCCESS }); - dispatch(fetchCollection(collection, directory)); - }) - .catch(error => - dispatch({ - type: DELETE_DOCUMENT_FAILURE, - error, - }) - ); + return del( + documentAPIUrl(collection, directory, filename), + { + type: DELETE_DOCUMENT_SUCCESS, + name: 'doc', + update: fetchCollection(collection, directory), + }, + { type: DELETE_DOCUMENT_FAILURE, name: 'error' }, + dispatch + ); }; const getFilenameFromTitle = (title, collection, date) => { diff --git a/src/ducks/datafiles.js b/src/ducks/datafiles.js index 5a4f6f7e7..51df2671b 100644 --- a/src/ducks/datafiles.js +++ b/src/ducks/datafiles.js @@ -1,6 +1,6 @@ import _ from 'underscore'; import { CLEAR_ERRORS, validationError } from './utils'; -import { get, put } from '../utils/fetch'; +import { get, put, del } from '../utils/fetch'; import { datafilesAPIUrl, datafileAPIUrl } from '../constants/api'; import { toYAML, @@ -95,25 +95,19 @@ export const putDataFile = ( }; export const deleteDataFile = (directory, filename) => dispatch => { - return fetch(datafileAPIUrl(directory, filename), { - method: 'DELETE', - credentials: 'same-origin', - }) - .then(data => { - dispatch({ type: DELETE_DATAFILE_SUCCESS }); - dispatch(fetchDataFiles(directory)); - }) - .catch(error => - dispatch({ - type: DELETE_DATAFILE_FAILURE, - error, - }) - ); + return del( + datafileAPIUrl(directory, filename), + { + type: DELETE_DATAFILE_SUCCESS, + name: 'file', + update: fetchDataFiles(directory), + }, + { type: DELETE_DATAFILE_FAILURE, name: 'error' }, + dispatch + ); }; -export const onDataFileChanged = () => ({ - type: DATAFILE_CHANGED, -}); +export const onDataFileChanged = () => ({ type: DATAFILE_CHANGED }); const validateDatafile = (filename, data) => validator( diff --git a/src/ducks/drafts.js b/src/ducks/drafts.js index 9e39c41c5..43a744e6e 100644 --- a/src/ducks/drafts.js +++ b/src/ducks/drafts.js @@ -5,7 +5,7 @@ import { PUT_DOCUMENT_SUCCESS, PUT_DOCUMENT_FAILURE, } from './collections'; -import { get, put } from '../utils/fetch'; +import { get, put, del } from '../utils/fetch'; import { validateMetadata } from '../utils/validation'; import { preparePayload, getFrontMatterFromMetdata } from '../utils/helpers'; import { draftsAPIUrl, draftAPIUrl, documentAPIUrl } from '../constants/api'; @@ -103,20 +103,16 @@ export const putDraft = (directory, filename) => (dispatch, getState) => { }; export const deleteDraft = (directory, filename) => dispatch => { - return fetch(draftAPIUrl(directory, filename), { - method: 'DELETE', - credentials: 'same-origin', - }) - .then(data => { - dispatch({ type: DELETE_DRAFT_SUCCESS }); - dispatch(fetchDrafts(directory)); - }) - .catch(error => - dispatch({ - type: DELETE_DRAFT_FAILURE, - error, - }) - ); + return del( + draftAPIUrl(directory, filename), + { + type: DELETE_DRAFT_SUCCESS, + name: 'draft', + update: fetchDrafts(directory), + }, + { type: DELETE_DRAFT_FAILURE, name: 'error' }, + dispatch + ); }; export const publishDraft = (directory, filename) => (dispatch, getState) => { diff --git a/src/ducks/pages.js b/src/ducks/pages.js index 496aef24b..8b6121154 100644 --- a/src/ducks/pages.js +++ b/src/ducks/pages.js @@ -1,6 +1,6 @@ import _ from 'underscore'; import { CLEAR_ERRORS, validationError } from './utils'; -import { get, put } from '../utils/fetch'; +import { get, put, del } from '../utils/fetch'; import { validateMetadata } from '../utils/validation'; import { preparePayload, getFrontMatterFromMetdata } from '../utils/helpers'; import { pagesAPIUrl, pageAPIUrl } from '../constants/api'; @@ -90,20 +90,12 @@ export const putPage = (directory, filename) => (dispatch, getState) => { }; export const deletePage = (directory, filename) => dispatch => { - return fetch(pageAPIUrl(directory, filename), { - method: 'DELETE', - credentials: 'same-origin', - }) - .then(data => { - dispatch({ type: DELETE_PAGE_SUCCESS }); - dispatch(fetchPages(directory)); - }) - .catch(error => - dispatch({ - type: DELETE_PAGE_FAILURE, - error, - }) - ); + return del( + pageAPIUrl(directory, filename), + { type: DELETE_PAGE_SUCCESS, name: 'page', update: fetchPages(directory) }, + { type: DELETE_PAGE_FAILURE, name: 'error' }, + dispatch + ); }; // Reducer diff --git a/src/ducks/staticfiles.js b/src/ducks/staticfiles.js index f3445dd83..1ea897995 100644 --- a/src/ducks/staticfiles.js +++ b/src/ducks/staticfiles.js @@ -1,5 +1,5 @@ import _ from 'underscore'; -import { get } from '../utils/fetch'; +import { get, del } from '../utils/fetch'; import { addNotification } from './notifications'; import { staticfilesAPIUrl, staticfileAPIUrl } from '../constants/api'; @@ -75,20 +75,16 @@ export const uploadStaticFiles = (directory, files) => dispatch => { }; export const deleteStaticFile = (directory, filename) => dispatch => { - return fetch(staticfileAPIUrl(directory, filename), { - method: 'DELETE', - credentials: 'same-origin', - }) - .then(data => { - dispatch({ type: DELETE_STATICFILE_SUCCESS }); - dispatch(fetchStaticFiles(directory)); - }) - .catch(error => - dispatch({ - type: DELETE_STATICFILE_FAILURE, - error, - }) - ); + return del( + staticfileAPIUrl(directory, filename), + { + type: DELETE_STATICFILE_SUCCESS, + name: 'file', + update: fetchStaticFiles(directory), + }, + { type: DELETE_STATICFILE_FAILURE, name: 'error' }, + dispatch + ); }; // Reducer diff --git a/src/utils/fetch.js b/src/utils/fetch.js index 65075673a..2cda502c9 100644 --- a/src/utils/fetch.js +++ b/src/utils/fetch.js @@ -7,7 +7,7 @@ const { getErrorMessage, getFetchErrorMessage, getUpdateErrorMessage, - getDeleteMessage, + getDeleteErrorMessage, } = translations; /** @@ -93,12 +93,10 @@ export const del = (url, action_success, action_failure, dispatch) => { method: 'DELETE', credentials: 'same-origin', }) - .then(data => - dispatch({ - type: action_success.type, - id: action_success.id, - }) - ) + .then(data => { + dispatch({ type: action_success.type }); + dispatch(action_success.update); + }) .catch(error => { dispatch({ type: action_failure.type, @@ -107,7 +105,7 @@ export const del = (url, action_success, action_failure, dispatch) => { dispatch( addNotification( getErrorMessage(), - getDeleteMessage(action_success.name), + getDeleteErrorMessage(action_success.name), 'error' ) ); From 78a62b2431e13e7627ce02307f731a33ff6dec32 Mon Sep 17 00:00:00 2001 From: Ashwin Maroli Date: Sun, 29 Dec 2019 19:03:20 +0530 Subject: [PATCH 05/16] Add documentation for validateMetadata arrow fn. --- src/utils/validation.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/utils/validation.js b/src/utils/validation.js index 1a421a295..31897fb5f 100644 --- a/src/utils/validation.js +++ b/src/utils/validation.js @@ -50,15 +50,24 @@ export const validator = (values, validations, messages) => { return errorMessages; }; +/** + * if `path` is a falsy value or if appending a slash to it equals to + * `directory`, generate filename from `title` available in the `metadata`. + * + * Set up `errors` otherwise. + * Returns an object with two keys -- `path` and `errors`. + * + * @param {Object} metadata + * @param {String} directory + * @return {Object} with keys 'path' and 'errors' + */ export const validateMetadata = (metadata, directory) => { let { path, title } = metadata; let errors = []; - // if path is not given or equals to directory, generate filename from the title if ((!path || `${path}/` === directory) && title) { - path = getFilenameFromTitle(title); // override empty path + path = getFilenameFromTitle(title); } else { - // validate otherwise errors = validatePage(metadata); } return { path, errors }; From de3bde41c1fab7329f2a6b5200416e335863ade4 Mon Sep 17 00:00:00 2001 From: Ashwin Maroli Date: Mon, 30 Dec 2019 19:34:23 +0530 Subject: [PATCH 06/16] Fix comments --- src/ducks/collections.js | 2 -- src/ducks/drafts.js | 4 ++-- src/ducks/pages.js | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/ducks/collections.js b/src/ducks/collections.js index d58425131..7273be5de 100644 --- a/src/ducks/collections.js +++ b/src/ducks/collections.js @@ -94,7 +94,6 @@ export const createDocument = (collection, directory) => ( // send the put request return put( - // create or update document according to filename existence documentAPIUrl(collection, directory, path), preparePayload({ raw_content, front_matter }), { type: PUT_DOCUMENT_SUCCESS, name: 'doc' }, @@ -127,7 +126,6 @@ export const putDocument = (collection, directory, filename) => ( // send the put request return put( - // create or update document according to filename existence documentAPIUrl(collection, directory, filename), preparePayload({ path: relative_path, raw_content, front_matter }), { type: PUT_DOCUMENT_SUCCESS, name: 'doc' }, diff --git a/src/ducks/drafts.js b/src/ducks/drafts.js index 84fa5196c..ebc408f40 100644 --- a/src/ducks/drafts.js +++ b/src/ducks/drafts.js @@ -57,7 +57,7 @@ export const createDraft = directory => (dispatch, getState) => { // strip '_drafts/' from path when provided const filename = path.replace('_drafts/', ''); - //send the put request + // send the put request return put( draftAPIUrl(directory, filename), preparePayload({ front_matter, raw_content }), @@ -84,7 +84,7 @@ export const putDraft = (directory, filename) => (dispatch, getState) => { ? `_drafts/${directory}/${path}` : `_drafts/${path}`; - //send the put request + // send the put request return put( draftAPIUrl(directory, filename), preparePayload({ path: relative_path, front_matter, raw_content }), diff --git a/src/ducks/pages.js b/src/ducks/pages.js index 31ef6fc28..692209284 100644 --- a/src/ducks/pages.js +++ b/src/ducks/pages.js @@ -53,7 +53,7 @@ export const createPage = directory => (dispatch, getState) => { const raw_content = metadata.raw_content; const front_matter = getFrontMatterFromMetdata(metadata); - //send the put request + // send the put request return put( pageAPIUrl(directory, path), preparePayload({ front_matter, raw_content }), @@ -78,7 +78,7 @@ export const putPage = (directory, filename) => (dispatch, getState) => { const front_matter = getFrontMatterFromMetdata(metadata); const relative_path = directory ? `${directory}/${path}` : `${path}`; - //send the put request + // send the put request return put( pageAPIUrl(directory, filename), preparePayload({ path: relative_path, front_matter, raw_content }), From 70ce632ee64799a6d71a706ac88173b0a4c12e3a Mon Sep 17 00:00:00 2001 From: Ashwin Maroli Date: Mon, 30 Dec 2019 19:56:00 +0530 Subject: [PATCH 07/16] Dispatch clearErrors from utilsDucks --- src/ducks/collections.js | 6 +++--- src/ducks/config.js | 4 ++-- src/ducks/datafiles.js | 4 ++-- src/ducks/drafts.js | 8 ++++---- src/ducks/pages.js | 6 +++--- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/ducks/collections.js b/src/ducks/collections.js index 7273be5de..645b460e0 100644 --- a/src/ducks/collections.js +++ b/src/ducks/collections.js @@ -1,5 +1,5 @@ import moment from 'moment'; -import { CLEAR_ERRORS, validationError } from './utils'; +import { clearErrors, validationError } from './utils'; import { get, put, del } from '../utils/fetch'; import { validator } from '../utils/validation'; import { @@ -87,7 +87,7 @@ export const createDocument = (collection, directory) => ( if (errors.length) return dispatch(validationError(errors)); // clear errors - dispatch({ type: CLEAR_ERRORS }); + dispatch(clearErrors()); const front_matter = getFrontMatterFromMetdata(metadata); const raw_content = metadata.raw_content; @@ -114,7 +114,7 @@ export const putDocument = (collection, directory, filename) => ( if (errors.length) return dispatch(validationError(errors)); // clear errors - dispatch({ type: CLEAR_ERRORS }); + dispatch(clearErrors()); const raw_content = metadata.raw_content; const front_matter = getFrontMatterFromMetdata(metadata); diff --git a/src/ducks/config.js b/src/ducks/config.js index 88cc218ee..e99be8502 100644 --- a/src/ducks/config.js +++ b/src/ducks/config.js @@ -1,7 +1,7 @@ import { getConfigurationUrl, putConfigurationUrl } from '../constants/api'; import { get, put } from '../utils/fetch'; import { validator } from '../utils/validation'; -import { CLEAR_ERRORS, validationError } from './utils'; +import { clearErrors, validationError } from './utils'; import { toYAML } from '../utils/helpers'; import translations from '../translations'; @@ -45,7 +45,7 @@ export const putConfig = (config, source = 'editor') => ( if (errors.length) { return dispatch(validationError(errors)); } - dispatch({ type: CLEAR_ERRORS }); + dispatch(clearErrors()); return put( putConfigurationUrl(), diff --git a/src/ducks/datafiles.js b/src/ducks/datafiles.js index e392e9f9c..f7691cb50 100644 --- a/src/ducks/datafiles.js +++ b/src/ducks/datafiles.js @@ -1,4 +1,4 @@ -import { CLEAR_ERRORS, validationError } from './utils'; +import { clearErrors, validationError } from './utils'; import { get, put, del } from '../utils/fetch'; import { datafilesAPIUrl, datafileAPIUrl } from '../constants/api'; import { toYAML, getExtensionFromPath, trimObject } from '../utils/helpers'; @@ -77,7 +77,7 @@ export const putDataFile = ( if (errors.length) { return dispatch(validationError(errors)); } - dispatch({ type: CLEAR_ERRORS }); + dispatch(clearErrors()); return put( datafileAPIUrl(directory, filename), diff --git a/src/ducks/drafts.js b/src/ducks/drafts.js index ebc408f40..04d84890a 100644 --- a/src/ducks/drafts.js +++ b/src/ducks/drafts.js @@ -1,4 +1,4 @@ -import { CLEAR_ERRORS, validationError } from './utils'; +import { clearErrors, validationError } from './utils'; import { PUT_DOCUMENT_SUCCESS, PUT_DOCUMENT_FAILURE } from './collections'; import { get, put, del } from '../utils/fetch'; import { validateMetadata } from '../utils/validation'; @@ -49,7 +49,7 @@ export const createDraft = directory => (dispatch, getState) => { if (errors.length) return dispatch(validationError(errors)); // clear errors - dispatch({ type: CLEAR_ERRORS }); + dispatch(clearErrors()); const raw_content = metadata.raw_content; const front_matter = getFrontMatterFromMetdata(metadata); @@ -76,7 +76,7 @@ export const putDraft = (directory, filename) => (dispatch, getState) => { if (errors.length) return dispatch(validationError(errors)); // clear errors - dispatch({ type: CLEAR_ERRORS }); + dispatch(clearErrors()); const raw_content = metadata.raw_content; const front_matter = getFrontMatterFromMetdata(metadata); @@ -115,7 +115,7 @@ export const publishDraft = (directory, filename) => (dispatch, getState) => { if (errors.length) return dispatch(validationError(errors)); // clear errors - dispatch({ type: CLEAR_ERRORS }); + dispatch(clearErrors()); const raw_content = metadata.raw_content; const front_matter = getFrontMatterFromMetdata(metadata); diff --git a/src/ducks/pages.js b/src/ducks/pages.js index 692209284..86b1b9eb3 100644 --- a/src/ducks/pages.js +++ b/src/ducks/pages.js @@ -1,4 +1,4 @@ -import { CLEAR_ERRORS, validationError } from './utils'; +import { clearErrors, validationError } from './utils'; import { get, put, del } from '../utils/fetch'; import { validateMetadata } from '../utils/validation'; import { preparePayload, getFrontMatterFromMetdata } from '../utils/helpers'; @@ -48,7 +48,7 @@ export const createPage = directory => (dispatch, getState) => { if (errors.length) return dispatch(validationError(errors)); // clear errors - dispatch({ type: CLEAR_ERRORS }); + dispatch(clearErrors()); const raw_content = metadata.raw_content; const front_matter = getFrontMatterFromMetdata(metadata); @@ -72,7 +72,7 @@ export const putPage = (directory, filename) => (dispatch, getState) => { if (errors.length) return dispatch(validationError(errors)); // clear errors - dispatch({ type: CLEAR_ERRORS }); + dispatch(clearErrors()); const raw_content = metadata.raw_content; const front_matter = getFrontMatterFromMetdata(metadata); From d0845f31fc0e6f0fd95d904c9596e14decab48eb Mon Sep 17 00:00:00 2001 From: Ashwin Maroli Date: Wed, 1 Jan 2020 15:10:49 +0530 Subject: [PATCH 08/16] Compact expressions without affecting readability --- src/ducks/collections.js | 39 +++++++++++--------------------------- src/ducks/config.js | 8 ++------ src/ducks/datafiles.js | 4 +--- src/ducks/drafts.js | 8 ++++---- src/ducks/metadata.js | 24 ++++------------------- src/ducks/notifications.js | 6 +----- src/ducks/pages.js | 8 ++++---- src/ducks/staticfiles.js | 15 ++++----------- src/ducks/utils.js | 22 +++++---------------- src/utils/helpers.js | 2 +- 10 files changed, 37 insertions(+), 99 deletions(-) diff --git a/src/ducks/collections.js b/src/ducks/collections.js index 645b460e0..27f66db01 100644 --- a/src/ducks/collections.js +++ b/src/ducks/collections.js @@ -2,11 +2,7 @@ import moment from 'moment'; import { clearErrors, validationError } from './utils'; import { get, put, del } from '../utils/fetch'; import { validator } from '../utils/validation'; -import { - slugify, - preparePayload, - getFrontMatterFromMetdata, -} from '../utils/helpers'; +import { slugify, preparePayload, sanitizeFrontMatter } from '../utils/helpers'; import { collectionsAPIUrl, collectionAPIUrl, @@ -48,27 +44,20 @@ export const fetchCollections = () => dispatch => { ); }; -export const fetchCollection = ( - collection_name, - directory = '' -) => dispatch => { +export const fetchCollection = (collection, directory = '') => dispatch => { dispatch({ type: FETCH_COLLECTION_REQUEST }); return get( - collectionAPIUrl(collection_name, directory), + collectionAPIUrl(collection, directory), { type: FETCH_COLLECTION_SUCCESS, name: 'entries' }, { type: FETCH_COLLECTION_FAILURE, name: 'error' }, dispatch ); }; -export const fetchDocument = ( - collection_name, - directory, - filename -) => dispatch => { +export const fetchDocument = (collection, directory, filename) => dispatch => { dispatch({ type: FETCH_DOCUMENT_REQUEST }); return get( - documentAPIUrl(collection_name, directory, filename), + documentAPIUrl(collection, directory, filename), { type: FETCH_DOCUMENT_SUCCESS, name: 'doc' }, { type: FETCH_DOCUMENT_FAILURE, name: 'error' }, dispatch @@ -89,7 +78,7 @@ export const createDocument = (collection, directory) => ( // clear errors dispatch(clearErrors()); - const front_matter = getFrontMatterFromMetdata(metadata); + const front_matter = sanitizeFrontMatter(metadata); const raw_content = metadata.raw_content; // send the put request @@ -117,7 +106,7 @@ export const putDocument = (collection, directory, filename) => ( dispatch(clearErrors()); const raw_content = metadata.raw_content; - const front_matter = getFrontMatterFromMetdata(metadata); + const front_matter = sanitizeFrontMatter(metadata); // add collection type prefix to relative path const relative_path = directory @@ -260,14 +249,8 @@ export default function collections( // Selectors export const filterBySearchInput = (list, input) => { - if (input) { - return list.filter(item => { - if (item.name) { - return item.name.toLowerCase().includes(input.toLowerCase()); - } else { - return item.title.toLowerCase().includes(input.toLowerCase()); - } - }); - } - return list || []; + if (!input) return list; + return list.filter(f => + (f.title || f.name).toLowerCase().includes(input.toLowerCase()) + ); }; diff --git a/src/ducks/config.js b/src/ducks/config.js index e99be8502..535421c5e 100644 --- a/src/ducks/config.js +++ b/src/ducks/config.js @@ -42,9 +42,7 @@ export const putConfig = (config, source = 'editor') => ( // handle errors const errors = validateConfig(config); - if (errors.length) { - return dispatch(validationError(errors)); - } + if (errors.length) return dispatch(validationError(errors)); dispatch(clearErrors()); return put( @@ -65,9 +63,7 @@ const validateConfig = config => } ); -export const onEditorChange = () => ({ - type: CONFIG_EDITOR_CHANGED, -}); +export const onEditorChange = () => ({ type: CONFIG_EDITOR_CHANGED }); // Reducer export default function config( diff --git a/src/ducks/datafiles.js b/src/ducks/datafiles.js index f7691cb50..ac1166acb 100644 --- a/src/ducks/datafiles.js +++ b/src/ducks/datafiles.js @@ -74,9 +74,7 @@ export const putDataFile = ( // handle errors const errors = validateDatafile(filename, data); - if (errors.length) { - return dispatch(validationError(errors)); - } + if (errors.length) return dispatch(validationError(errors)); dispatch(clearErrors()); return put( diff --git a/src/ducks/drafts.js b/src/ducks/drafts.js index 04d84890a..c6f861f91 100644 --- a/src/ducks/drafts.js +++ b/src/ducks/drafts.js @@ -2,7 +2,7 @@ import { clearErrors, validationError } from './utils'; import { PUT_DOCUMENT_SUCCESS, PUT_DOCUMENT_FAILURE } from './collections'; import { get, put, del } from '../utils/fetch'; import { validateMetadata } from '../utils/validation'; -import { preparePayload, getFrontMatterFromMetdata } from '../utils/helpers'; +import { preparePayload, sanitizeFrontMatter } from '../utils/helpers'; import { draftsAPIUrl, draftAPIUrl, documentAPIUrl } from '../constants/api'; // Action Types @@ -52,7 +52,7 @@ export const createDraft = directory => (dispatch, getState) => { dispatch(clearErrors()); const raw_content = metadata.raw_content; - const front_matter = getFrontMatterFromMetdata(metadata); + const front_matter = sanitizeFrontMatter(metadata); // strip '_drafts/' from path when provided const filename = path.replace('_drafts/', ''); @@ -79,7 +79,7 @@ export const putDraft = (directory, filename) => (dispatch, getState) => { dispatch(clearErrors()); const raw_content = metadata.raw_content; - const front_matter = getFrontMatterFromMetdata(metadata); + const front_matter = sanitizeFrontMatter(metadata); const relative_path = directory ? `_drafts/${directory}/${path}` : `_drafts/${path}`; @@ -118,7 +118,7 @@ export const publishDraft = (directory, filename) => (dispatch, getState) => { dispatch(clearErrors()); const raw_content = metadata.raw_content; - const front_matter = getFrontMatterFromMetdata(metadata); + const front_matter = sanitizeFrontMatter(metadata); return put( documentAPIUrl('posts', directory, filename), diff --git a/src/ducks/metadata.js b/src/ducks/metadata.js index db5fed11c..f0efbd907 100644 --- a/src/ducks/metadata.js +++ b/src/ducks/metadata.js @@ -24,12 +24,7 @@ export const storeContentFields = content => ({ type: STORE_CONTENT_FIELDS, content, }); - -export const addField = namePrefix => ({ - type: ADD_METAFIELD, - namePrefix, -}); - +export const addField = namePrefix => ({ type: ADD_METAFIELD, namePrefix }); export const removeField = (namePrefix, key) => ({ type: REMOVE_METAFIELD, namePrefix, @@ -62,20 +57,9 @@ export const convertField = (nameAttr, convertType) => ({ convertType, }); -export const updateTitle = title => ({ - type: UPDATE_TITLE, - title, -}); - -export const updateBody = body => ({ - type: UPDATE_BODY, - body, -}); - -export const updatePath = path => ({ - type: UPDATE_PATH, - path, -}); +export const updateTitle = title => ({ type: UPDATE_TITLE, title }); +export const updateBody = body => ({ type: UPDATE_BODY, body }); +export const updatePath = path => ({ type: UPDATE_PATH, path }); // Reducer export default function metadata( // TODO normalize the metadata diff --git a/src/ducks/notifications.js b/src/ducks/notifications.js index 2b6d1bbb8..b4c79f239 100644 --- a/src/ducks/notifications.js +++ b/src/ducks/notifications.js @@ -4,11 +4,7 @@ export const ADD_NOTIFICATION = 'ADD_NOTIFICATION'; // Actions export const addNotification = (title, message, level) => ({ type: ADD_NOTIFICATION, - notification: { - title, - message, - level, - }, + notification: { title, message, level }, }); // Reducer diff --git a/src/ducks/pages.js b/src/ducks/pages.js index 86b1b9eb3..3e6f4d808 100644 --- a/src/ducks/pages.js +++ b/src/ducks/pages.js @@ -1,7 +1,7 @@ import { clearErrors, validationError } from './utils'; import { get, put, del } from '../utils/fetch'; import { validateMetadata } from '../utils/validation'; -import { preparePayload, getFrontMatterFromMetdata } from '../utils/helpers'; +import { preparePayload, sanitizeFrontMatter } from '../utils/helpers'; import { pagesAPIUrl, pageAPIUrl } from '../constants/api'; // Action Types @@ -51,7 +51,7 @@ export const createPage = directory => (dispatch, getState) => { dispatch(clearErrors()); const raw_content = metadata.raw_content; - const front_matter = getFrontMatterFromMetdata(metadata); + const front_matter = sanitizeFrontMatter(metadata); // send the put request return put( @@ -75,8 +75,8 @@ export const putPage = (directory, filename) => (dispatch, getState) => { dispatch(clearErrors()); const raw_content = metadata.raw_content; - const front_matter = getFrontMatterFromMetdata(metadata); - const relative_path = directory ? `${directory}/${path}` : `${path}`; + const front_matter = sanitizeFrontMatter(metadata); + const relative_path = directory ? `${directory}/${path}` : path; // send the put request return put( diff --git a/src/ducks/staticfiles.js b/src/ducks/staticfiles.js index 1ea897995..a14ec4c9b 100644 --- a/src/ducks/staticfiles.js +++ b/src/ducks/staticfiles.js @@ -62,10 +62,7 @@ export const uploadStaticFiles = (directory, files) => dispatch => { ); }) .catch(error => { - dispatch({ - type: PUT_STATICFILE_FAILURE, - error, - }); + dispatch({ type: PUT_STATICFILE_FAILURE, error }); dispatch( addNotification(getErrorMessage(), getUploadErrorMessage(), 'error') ); @@ -134,11 +131,7 @@ export default function staticfiles( } // Selectors -export const filterByFilename = (staticfiles, input) => { - if (input) { - return staticfiles.filter(sf => - sf.path.toLowerCase().includes(input.toLowerCase()) - ); - } - return staticfiles; +export const filterByFilename = (files, input) => { + if (!input) return files; + return files.filter(f => f.path.toLowerCase().includes(input.toLowerCase())); }; diff --git a/src/ducks/utils.js b/src/ducks/utils.js index 2e47bf90f..2563911ac 100644 --- a/src/ducks/utils.js +++ b/src/ducks/utils.js @@ -1,9 +1,7 @@ // Selectors export const filterBySearchInput = (list, input) => { - if (input) { - return list.filter(p => p.name.toLowerCase().includes(input.toLowerCase())); - } - return list; + if (!input) return list; + return list.filter(p => p.name.toLowerCase().includes(input.toLowerCase())); }; // Action Types @@ -12,19 +10,9 @@ export const CLEAR_ERRORS = 'CLEAR_ERRORS'; export const VALIDATION_ERROR = 'VALIDATION_ERROR'; // Actions -export const search = input => ({ - type: SEARCH_CONTENT, - input, -}); - -export const clearErrors = () => ({ - type: CLEAR_ERRORS, -}); - -export const validationError = errors => ({ - type: VALIDATION_ERROR, - errors, -}); +export const search = input => ({ type: SEARCH_CONTENT, input }); +export const clearErrors = () => ({ type: CLEAR_ERRORS }); +export const validationError = errors => ({ type: VALIDATION_ERROR, errors }); // Reducer export default function utils( diff --git a/src/utils/helpers.js b/src/utils/helpers.js index efeccb30e..d160295e8 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -157,7 +157,7 @@ export const getDocumentTitle = (type, splat, prefix = '') => { }; // omit raw_content, path and empty-value keys in metadata state from front_matter -export const getFrontMatterFromMetdata = metadata => { +export const sanitizeFrontMatter = metadata => { return _.omit(metadata, (value, key, object) => { return key === 'raw_content' || key === 'path' || value === ''; }); From 7e150d479b16e0650968e99f13cd539a3b3cc4d1 Mon Sep 17 00:00:00 2001 From: Ashwin Maroli Date: Wed, 1 Jan 2020 19:34:06 +0530 Subject: [PATCH 09/16] Improve test coverage of staticfilesDuck --- src/ducks/reducer_tests/collections.spec.js | 3 +++ src/ducks/reducer_tests/staticfiles.spec.js | 11 ++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/ducks/reducer_tests/collections.spec.js b/src/ducks/reducer_tests/collections.spec.js index 6582d7e2d..ccd615c5f 100644 --- a/src/ducks/reducer_tests/collections.spec.js +++ b/src/ducks/reducer_tests/collections.spec.js @@ -108,5 +108,8 @@ describe('Reducers::Collections', () => { expect( collectionsDuck.filterBySearchInput(collection_entries, 'dir').length ).toBe(1); + expect( + collectionsDuck.filterBySearchInput(collection_entries, null).length + ).toBe(2); }); }); diff --git a/src/ducks/reducer_tests/staticfiles.spec.js b/src/ducks/reducer_tests/staticfiles.spec.js index c6432ed13..e90872be1 100644 --- a/src/ducks/reducer_tests/staticfiles.spec.js +++ b/src/ducks/reducer_tests/staticfiles.spec.js @@ -1,5 +1,5 @@ import * as staticfilesDuck from '../staticfiles'; -import { staticfile } from './fixtures'; +import { staticfile, staticfile_entries } from './fixtures'; const reducer = staticfilesDuck.default; @@ -80,4 +80,13 @@ describe('Reducers::StaticFiles', () => { uploading: false, }); }); + + it('should filter static files and directories', () => { + expect( + staticfilesDuck.filterByFilename(staticfile_entries, 'dir').length + ).toBe(1); + expect( + staticfilesDuck.filterByFilename(staticfile_entries, null).length + ).toBe(2); + }); }); From 2b2ab1ee9ed06ed0561a83a156d916efbda042b1 Mon Sep 17 00:00:00 2001 From: Ashwin Maroli Date: Wed, 1 Jan 2020 19:40:11 +0530 Subject: [PATCH 10/16] Add a fixture array of staticfile and directory --- src/ducks/reducer_tests/fixtures/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ducks/reducer_tests/fixtures/index.js b/src/ducks/reducer_tests/fixtures/index.js index 489075047..edc24895b 100644 --- a/src/ducks/reducer_tests/fixtures/index.js +++ b/src/ducks/reducer_tests/fixtures/index.js @@ -136,6 +136,8 @@ export const staticfile = { encoded_content: "PGh0bWw+CiAgPGJvZHk+CiAgICBZb3UncmUgcHJvYmFibHkgbG9va2luZyBm" }; +export const staticfile_entries = [staticfile, directory]; + export const notification = { title: 'Test', message: 'Testing notifications', From c06e5eacd80ab6d1d4eb152e3a98dc1c2d60f1d4 Mon Sep 17 00:00:00 2001 From: Ashwin Maroli Date: Thu, 2 Jan 2020 15:23:18 +0530 Subject: [PATCH 11/16] Avoid using one-liner expressions --- src/ducks/collections.js | 14 +++++++++----- src/ducks/config.js | 8 ++++++-- src/ducks/datafiles.js | 8 ++++++-- src/ducks/drafts.js | 15 +++++++++------ src/ducks/metadata.js | 24 ++++++++++++++++++++---- src/ducks/notifications.js | 6 +++++- src/ducks/pages.js | 10 ++++++---- src/ducks/staticfiles.js | 4 +++- src/ducks/utils.js | 4 +++- 9 files changed, 67 insertions(+), 26 deletions(-) diff --git a/src/ducks/collections.js b/src/ducks/collections.js index 27f66db01..004b560ce 100644 --- a/src/ducks/collections.js +++ b/src/ducks/collections.js @@ -73,8 +73,9 @@ export const createDocument = (collection, directory) => ( // get path or return if metadata doesn't validate const { path, errors } = validateMetadata(metadata, collection, directory); - if (errors.length) return dispatch(validationError(errors)); - + if (errors.length) { + return dispatch(validationError(errors)); + } // clear errors dispatch(clearErrors()); @@ -100,8 +101,9 @@ export const putDocument = (collection, directory, filename) => ( // get path or abort if metadata doesn't validate const { path, errors } = validateMetadata(metadata, collection, directory); - if (errors.length) return dispatch(validationError(errors)); - + if (errors.length) { + return dispatch(validationError(errors)); + } // clear errors dispatch(clearErrors()); @@ -249,7 +251,9 @@ export default function collections( // Selectors export const filterBySearchInput = (list, input) => { - if (!input) return list; + if (!input) { + return list; + } return list.filter(f => (f.title || f.name).toLowerCase().includes(input.toLowerCase()) ); diff --git a/src/ducks/config.js b/src/ducks/config.js index 535421c5e..e99be8502 100644 --- a/src/ducks/config.js +++ b/src/ducks/config.js @@ -42,7 +42,9 @@ export const putConfig = (config, source = 'editor') => ( // handle errors const errors = validateConfig(config); - if (errors.length) return dispatch(validationError(errors)); + if (errors.length) { + return dispatch(validationError(errors)); + } dispatch(clearErrors()); return put( @@ -63,7 +65,9 @@ const validateConfig = config => } ); -export const onEditorChange = () => ({ type: CONFIG_EDITOR_CHANGED }); +export const onEditorChange = () => ({ + type: CONFIG_EDITOR_CHANGED, +}); // Reducer export default function config( diff --git a/src/ducks/datafiles.js b/src/ducks/datafiles.js index ac1166acb..445ffaa6e 100644 --- a/src/ducks/datafiles.js +++ b/src/ducks/datafiles.js @@ -74,7 +74,9 @@ export const putDataFile = ( // handle errors const errors = validateDatafile(filename, data); - if (errors.length) return dispatch(validationError(errors)); + if (errors.length) { + return dispatch(validationError(errors)); + } dispatch(clearErrors()); return put( @@ -99,7 +101,9 @@ export const deleteDataFile = (directory, filename) => dispatch => { ); }; -export const onDataFileChanged = () => ({ type: DATAFILE_CHANGED }); +export const onDataFileChanged = () => ({ + type: DATAFILE_CHANGED, +}); const validateDatafile = (filename, data) => validator( diff --git a/src/ducks/drafts.js b/src/ducks/drafts.js index c6f861f91..ff83d7824 100644 --- a/src/ducks/drafts.js +++ b/src/ducks/drafts.js @@ -46,8 +46,9 @@ export const createDraft = directory => (dispatch, getState) => { // get path or return if metadata doesn't validate const { path, errors } = validateMetadata(metadata, directory); - if (errors.length) return dispatch(validationError(errors)); - + if (errors.length) { + return dispatch(validationError(errors)); + } // clear errors dispatch(clearErrors()); @@ -73,8 +74,9 @@ export const putDraft = (directory, filename) => (dispatch, getState) => { // get path or return if metadata doesn't validate const { path, errors } = validateMetadata(metadata, directory); - if (errors.length) return dispatch(validationError(errors)); - + if (errors.length) { + return dispatch(validationError(errors)); + } // clear errors dispatch(clearErrors()); @@ -112,8 +114,9 @@ export const publishDraft = (directory, filename) => (dispatch, getState) => { // return if metadata doesn't validate const { errors } = validateMetadata(metadata, directory); - if (errors.length) return dispatch(validationError(errors)); - + if (errors.length) { + return dispatch(validationError(errors)); + } // clear errors dispatch(clearErrors()); diff --git a/src/ducks/metadata.js b/src/ducks/metadata.js index f0efbd907..db5fed11c 100644 --- a/src/ducks/metadata.js +++ b/src/ducks/metadata.js @@ -24,7 +24,12 @@ export const storeContentFields = content => ({ type: STORE_CONTENT_FIELDS, content, }); -export const addField = namePrefix => ({ type: ADD_METAFIELD, namePrefix }); + +export const addField = namePrefix => ({ + type: ADD_METAFIELD, + namePrefix, +}); + export const removeField = (namePrefix, key) => ({ type: REMOVE_METAFIELD, namePrefix, @@ -57,9 +62,20 @@ export const convertField = (nameAttr, convertType) => ({ convertType, }); -export const updateTitle = title => ({ type: UPDATE_TITLE, title }); -export const updateBody = body => ({ type: UPDATE_BODY, body }); -export const updatePath = path => ({ type: UPDATE_PATH, path }); +export const updateTitle = title => ({ + type: UPDATE_TITLE, + title, +}); + +export const updateBody = body => ({ + type: UPDATE_BODY, + body, +}); + +export const updatePath = path => ({ + type: UPDATE_PATH, + path, +}); // Reducer export default function metadata( // TODO normalize the metadata diff --git a/src/ducks/notifications.js b/src/ducks/notifications.js index b4c79f239..2b6d1bbb8 100644 --- a/src/ducks/notifications.js +++ b/src/ducks/notifications.js @@ -4,7 +4,11 @@ export const ADD_NOTIFICATION = 'ADD_NOTIFICATION'; // Actions export const addNotification = (title, message, level) => ({ type: ADD_NOTIFICATION, - notification: { title, message, level }, + notification: { + title, + message, + level, + }, }); // Reducer diff --git a/src/ducks/pages.js b/src/ducks/pages.js index 3e6f4d808..40dc62f5a 100644 --- a/src/ducks/pages.js +++ b/src/ducks/pages.js @@ -45,8 +45,9 @@ export const createPage = directory => (dispatch, getState) => { // get path or return if metadata doesn't validate const { path, errors } = validateMetadata(metadata, directory); - if (errors.length) return dispatch(validationError(errors)); - + if (errors.length) { + return dispatch(validationError(errors)); + } // clear errors dispatch(clearErrors()); @@ -69,8 +70,9 @@ export const putPage = (directory, filename) => (dispatch, getState) => { // get path or abort if metadata doesn't validate const { path, errors } = validateMetadata(metadata, directory); - if (errors.length) return dispatch(validationError(errors)); - + if (errors.length) { + return dispatch(validationError(errors)); + } // clear errors dispatch(clearErrors()); diff --git a/src/ducks/staticfiles.js b/src/ducks/staticfiles.js index a14ec4c9b..27f6ea192 100644 --- a/src/ducks/staticfiles.js +++ b/src/ducks/staticfiles.js @@ -132,6 +132,8 @@ export default function staticfiles( // Selectors export const filterByFilename = (files, input) => { - if (!input) return files; + if (!input) { + return files; + } return files.filter(f => f.path.toLowerCase().includes(input.toLowerCase())); }; diff --git a/src/ducks/utils.js b/src/ducks/utils.js index 2563911ac..62668d3e2 100644 --- a/src/ducks/utils.js +++ b/src/ducks/utils.js @@ -1,6 +1,8 @@ // Selectors export const filterBySearchInput = (list, input) => { - if (!input) return list; + if (!input) { + return list; + } return list.filter(p => p.name.toLowerCase().includes(input.toLowerCase())); }; From bf43ff748ff1721cf7c77685a5783f35a5ad4796 Mon Sep 17 00:00:00 2001 From: Ashwin Maroli Date: Thu, 9 Jan 2020 16:33:42 +0530 Subject: [PATCH 12/16] Avoid using one-liner expressions --- src/ducks/utils.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/ducks/utils.js b/src/ducks/utils.js index 62668d3e2..ebd393fa1 100644 --- a/src/ducks/utils.js +++ b/src/ducks/utils.js @@ -12,9 +12,19 @@ export const CLEAR_ERRORS = 'CLEAR_ERRORS'; export const VALIDATION_ERROR = 'VALIDATION_ERROR'; // Actions -export const search = input => ({ type: SEARCH_CONTENT, input }); -export const clearErrors = () => ({ type: CLEAR_ERRORS }); -export const validationError = errors => ({ type: VALIDATION_ERROR, errors }); +export const search = input => ({ + type: SEARCH_CONTENT, + input, +}); + +export const clearErrors = () => ({ + type: CLEAR_ERRORS, +}); + +export const validationError = errors => ({ + type: VALIDATION_ERROR, + errors, +}); // Reducer export default function utils( From 3a89d3663a682556d4ae02a8d685a73894596c6f Mon Sep 17 00:00:00 2001 From: Ashwin Maroli Date: Thu, 9 Jan 2020 16:35:08 +0530 Subject: [PATCH 13/16] Remove helper with code-smell --- src/ducks/collections.js | 48 +++++++++++++++++-------------------- src/ducks/drafts.js | 51 ++++++++++++++++++++++++---------------- src/ducks/pages.js | 36 +++++++++++++++++----------- src/utils/validation.js | 40 +++++++------------------------ 4 files changed, 84 insertions(+), 91 deletions(-) diff --git a/src/ducks/collections.js b/src/ducks/collections.js index 004b560ce..dfe3704f3 100644 --- a/src/ducks/collections.js +++ b/src/ducks/collections.js @@ -70,17 +70,22 @@ export const createDocument = (collection, directory) => ( ) => { // get edited fields from metadata state const metadata = getState().metadata.metadata; - - // get path or return if metadata doesn't validate - const { path, errors } = validateMetadata(metadata, collection, directory); - if (errors.length) { - return dispatch(validationError(errors)); + let { path, raw_content, title, date } = metadata; + // if `path` is a falsy value or if appending a slash to it equals to + // `directory`, generate filename from `title`. + if ((!path || `${path}/` === directory) && title) { + path = getFilenameFromTitle(title, collection, date); // override empty path + } else { + // validate otherwise + const errors = validateDocument(metadata, collection); + if (errors.length) { + return dispatch(validationError(errors)); + } } // clear errors dispatch(clearErrors()); const front_matter = sanitizeFrontMatter(metadata); - const raw_content = metadata.raw_content; // send the put request return put( @@ -98,16 +103,21 @@ export const putDocument = (collection, directory, filename) => ( ) => { // get edited fields from metadata state const metadata = getState().metadata.metadata; - - // get path or abort if metadata doesn't validate - const { path, errors } = validateMetadata(metadata, collection, directory); - if (errors.length) { - return dispatch(validationError(errors)); + let { path, raw_content, title, date } = metadata; + // if `path` is a falsy value or if appending a slash to it equals to + // `directory`, generate filename from `title`. + if ((!path || `${path}/` === directory) && title) { + path = getFilenameFromTitle(title, collection, date); // override empty path + } else { + // validate otherwise + const errors = validateDocument(metadata, collection); + if (errors.length) { + return dispatch(validationError(errors)); + } } // clear errors dispatch(clearErrors()); - const raw_content = metadata.raw_content; const front_matter = sanitizeFrontMatter(metadata); // add collection type prefix to relative path @@ -148,20 +158,6 @@ const getFilenameFromTitle = (title, collection, date) => { return `${slugifiedTitle}.md`; }; -const validateMetadata = (metadata, collection, directory) => { - let { path, title, date } = metadata; - let errors = []; - - // if path is not given or equals to directory, generate filename from the title - if ((!path || `${path}/` === directory) && title) { - path = getFilenameFromTitle(title, collection, date); // override empty path - } else { - // validate otherwise - errors = validateDocument(metadata, collection); - } - return { path, errors }; -}; - const validateDocument = (metadata, collection) => { let validations = { title: 'required' }; // base validations // base messages diff --git a/src/ducks/drafts.js b/src/ducks/drafts.js index ff83d7824..be2815581 100644 --- a/src/ducks/drafts.js +++ b/src/ducks/drafts.js @@ -1,8 +1,8 @@ import { clearErrors, validationError } from './utils'; import { PUT_DOCUMENT_SUCCESS, PUT_DOCUMENT_FAILURE } from './collections'; import { get, put, del } from '../utils/fetch'; -import { validateMetadata } from '../utils/validation'; -import { preparePayload, sanitizeFrontMatter } from '../utils/helpers'; +import { validatePage } from '../utils/validation'; +import { slugify, preparePayload, sanitizeFrontMatter } from '../utils/helpers'; import { draftsAPIUrl, draftAPIUrl, documentAPIUrl } from '../constants/api'; // Action Types @@ -43,16 +43,20 @@ export const fetchDraft = (directory, filename) => dispatch => { export const createDraft = directory => (dispatch, getState) => { // get edited fields from metadata state const metadata = getState().metadata.metadata; - - // get path or return if metadata doesn't validate - const { path, errors } = validateMetadata(metadata, directory); - if (errors.length) { - return dispatch(validationError(errors)); + let { path, raw_content, title } = metadata; + // if `path` is a falsy value or if appending a slash to it equals to + // `directory`, generate filename from `title`. + if ((!path || `${path}/` === directory) && title) { + path = `${slugify(title)}.md`; + } else { + const errors = validatePage(metadata); + if (errors.length) { + return dispatch(validationError(errors)); + } } // clear errors dispatch(clearErrors()); - const raw_content = metadata.raw_content; const front_matter = sanitizeFrontMatter(metadata); // strip '_drafts/' from path when provided @@ -71,16 +75,20 @@ export const createDraft = directory => (dispatch, getState) => { export const putDraft = (directory, filename) => (dispatch, getState) => { // get edited fields from metadata state const metadata = getState().metadata.metadata; - - // get path or return if metadata doesn't validate - const { path, errors } = validateMetadata(metadata, directory); - if (errors.length) { - return dispatch(validationError(errors)); + let { path, raw_content, title } = metadata; + // if `path` is a falsy value or if appending a slash to it equals to + // `directory`, generate filename from `title`. + if ((!path || `${path}/` === directory) && title) { + path = `${slugify(title)}.md`; + } else { + const errors = validatePage(metadata); + if (errors.length) { + return dispatch(validationError(errors)); + } } // clear errors dispatch(clearErrors()); - const raw_content = metadata.raw_content; const front_matter = sanitizeFrontMatter(metadata); const relative_path = directory ? `_drafts/${directory}/${path}` @@ -111,16 +119,19 @@ export const deleteDraft = (directory, filename) => dispatch => { export const publishDraft = (directory, filename) => (dispatch, getState) => { const metadata = getState().metadata.metadata; - - // return if metadata doesn't validate - const { errors } = validateMetadata(metadata, directory); - if (errors.length) { - return dispatch(validationError(errors)); + let { path, raw_content, title } = metadata; + // if path is not given or equals to directory, generate filename from the title + if (!path && title) { + path = `${slugify(title)}.md`; + } else { + const errors = validatePage(metadata); + if (errors.length) { + return dispatch(validationError(errors)); + } } // clear errors dispatch(clearErrors()); - const raw_content = metadata.raw_content; const front_matter = sanitizeFrontMatter(metadata); return put( diff --git a/src/ducks/pages.js b/src/ducks/pages.js index 40dc62f5a..4da32805a 100644 --- a/src/ducks/pages.js +++ b/src/ducks/pages.js @@ -1,7 +1,7 @@ import { clearErrors, validationError } from './utils'; import { get, put, del } from '../utils/fetch'; -import { validateMetadata } from '../utils/validation'; -import { preparePayload, sanitizeFrontMatter } from '../utils/helpers'; +import { validatePage } from '../utils/validation'; +import { slugify, preparePayload, sanitizeFrontMatter } from '../utils/helpers'; import { pagesAPIUrl, pageAPIUrl } from '../constants/api'; // Action Types @@ -42,16 +42,20 @@ export const fetchPage = (directory, filename) => dispatch => { export const createPage = directory => (dispatch, getState) => { // get edited fields from metadata state const metadata = getState().metadata.metadata; - - // get path or return if metadata doesn't validate - const { path, errors } = validateMetadata(metadata, directory); - if (errors.length) { - return dispatch(validationError(errors)); + let { path, raw_content, title } = metadata; + // if `path` is a falsy value or if appending a slash to it equals to + // `directory`, generate filename from `title`. + if ((!path || `${path}/` === directory) && title) { + path = `${slugify(title)}.md`; + } else { + const errors = validatePage(metadata); + if (errors.length) { + return dispatch(validationError(errors)); + } } // clear errors dispatch(clearErrors()); - const raw_content = metadata.raw_content; const front_matter = sanitizeFrontMatter(metadata); // send the put request @@ -67,16 +71,20 @@ export const createPage = directory => (dispatch, getState) => { export const putPage = (directory, filename) => (dispatch, getState) => { // get edited fields from metadata state const metadata = getState().metadata.metadata; - - // get path or abort if metadata doesn't validate - const { path, errors } = validateMetadata(metadata, directory); - if (errors.length) { - return dispatch(validationError(errors)); + let { path, raw_content, title } = metadata; + // if `path` is a falsy value or if appending a slash to it equals to + // `directory`, generate filename from `title`. + if ((!path || `${path}/` === directory) && title) { + path = `${slugify(title)}.md`; + } else { + const errors = validatePage(metadata); + if (errors.length) { + return dispatch(validationError(errors)); + } } // clear errors dispatch(clearErrors()); - const raw_content = metadata.raw_content; const front_matter = sanitizeFrontMatter(metadata); const relative_path = directory ? `${directory}/${path}` : path; diff --git a/src/utils/validation.js b/src/utils/validation.js index 31897fb5f..c75a85f9f 100644 --- a/src/utils/validation.js +++ b/src/utils/validation.js @@ -20,16 +20,6 @@ const validated = (field, single) => { } }; -const validatePage = metadata => - validator( - metadata, - { path: 'required|filename' }, - { - 'path.required': getTitleRequiredMessage(), - 'path.filename': getFilenameNotValidMessage(), - } - ); - /** * Returns error messages if the given values does not pass the provided validations. * @param {Object} values @@ -50,25 +40,13 @@ export const validator = (values, validations, messages) => { return errorMessages; }; -/** - * if `path` is a falsy value or if appending a slash to it equals to - * `directory`, generate filename from `title` available in the `metadata`. - * - * Set up `errors` otherwise. - * Returns an object with two keys -- `path` and `errors`. - * - * @param {Object} metadata - * @param {String} directory - * @return {Object} with keys 'path' and 'errors' - */ -export const validateMetadata = (metadata, directory) => { - let { path, title } = metadata; - let errors = []; - - if ((!path || `${path}/` === directory) && title) { - path = getFilenameFromTitle(title); - } else { - errors = validatePage(metadata); - } - return { path, errors }; +export const validatePage = metadata => { + return validator( + metadata, + { path: 'required|filename' }, + { + 'path.required': getTitleRequiredMessage(), + 'path.filename': getFilenameNotValidMessage(), + } + ); }; From 016e3f9c51f6ae12bb0758a351ef8d087e30ca4e Mon Sep 17 00:00:00 2001 From: Ashwin Maroli Date: Thu, 9 Jan 2020 16:50:24 +0530 Subject: [PATCH 14/16] Revert internal function name change --- src/ducks/collections.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ducks/collections.js b/src/ducks/collections.js index dfe3704f3..72d99c384 100644 --- a/src/ducks/collections.js +++ b/src/ducks/collections.js @@ -74,7 +74,7 @@ export const createDocument = (collection, directory) => ( // if `path` is a falsy value or if appending a slash to it equals to // `directory`, generate filename from `title`. if ((!path || `${path}/` === directory) && title) { - path = getFilenameFromTitle(title, collection, date); // override empty path + path = generateFilenameFromTitle(title, collection, date); } else { // validate otherwise const errors = validateDocument(metadata, collection); @@ -107,7 +107,7 @@ export const putDocument = (collection, directory, filename) => ( // if `path` is a falsy value or if appending a slash to it equals to // `directory`, generate filename from `title`. if ((!path || `${path}/` === directory) && title) { - path = getFilenameFromTitle(title, collection, date); // override empty path + path = generateFilenameFromTitle(title, collection, date); } else { // validate otherwise const errors = validateDocument(metadata, collection); @@ -148,7 +148,7 @@ export const deleteDocument = (collection, directory, filename) => dispatch => { ); }; -const getFilenameFromTitle = (title, collection, date) => { +const generateFilenameFromTitle = (title, collection, date) => { const slugifiedTitle = slugify(title); if (collection === 'posts') { // if date is provided, use it, otherwise generate it with today's date From 6e6eaed076904c00150cc54ac9f3fdd8e7fabe04 Mon Sep 17 00:00:00 2001 From: Ashwin Maroli Date: Thu, 16 Jan 2020 19:00:18 +0530 Subject: [PATCH 15/16] Fix double dispatch in DELETE request wrapper --- src/ducks/action_tests/collections.spec.js | 3 +-- src/ducks/action_tests/datafiles.spec.js | 3 +-- src/ducks/action_tests/drafts.spec.js | 3 +-- src/ducks/action_tests/pages.spec.js | 7 +++---- src/ducks/action_tests/staticfiles.spec.js | 3 +-- src/ducks/collections.js | 21 ++++++++++++++------- src/ducks/datafiles.js | 21 ++++++++++++++------- src/ducks/drafts.js | 21 ++++++++++++++------- src/ducks/pages.js | 17 ++++++++++++++--- src/ducks/staticfiles.js | 14 +++++++++----- src/ducks/utils.js | 4 ++++ src/utils/fetch.js | 10 ++++++---- src/utils/helpers.js | 9 +++++++++ src/utils/validation.js | 22 +++++++++++----------- 14 files changed, 102 insertions(+), 56 deletions(-) diff --git a/src/ducks/action_tests/collections.spec.js b/src/ducks/action_tests/collections.spec.js index 0fb167cd9..728954319 100644 --- a/src/ducks/action_tests/collections.spec.js +++ b/src/ducks/action_tests/collections.spec.js @@ -87,8 +87,7 @@ describe('Actions::Collections', () => { .reply(200); const expectedActions = [ - { type: collectionsDuck.DELETE_DOCUMENT_SUCCESS }, - { type: collectionsDuck.FETCH_COLLECTION_REQUEST }, + { type: collectionsDuck.DELETE_DOCUMENT_SUCCESS, id: filename }, ]; const store = mockStore({}); diff --git a/src/ducks/action_tests/datafiles.spec.js b/src/ducks/action_tests/datafiles.spec.js index 1e1af8137..06b026d11 100644 --- a/src/ducks/action_tests/datafiles.spec.js +++ b/src/ducks/action_tests/datafiles.spec.js @@ -249,8 +249,7 @@ describe('Actions::Datafiles', () => { .reply(200); const expectedAction = [ - { type: datafilesDuck.DELETE_DATAFILE_SUCCESS }, - { type: datafilesDuck.FETCH_DATAFILES_REQUEST }, + { type: datafilesDuck.DELETE_DATAFILE_SUCCESS, id: 'data_file.yml' }, ]; const store = mockStore({ files: [] }); diff --git a/src/ducks/action_tests/drafts.spec.js b/src/ducks/action_tests/drafts.spec.js index 78e03f5d6..2cd2c6478 100644 --- a/src/ducks/action_tests/drafts.spec.js +++ b/src/ducks/action_tests/drafts.spec.js @@ -59,8 +59,7 @@ describe('Actions::Drafts', () => { .reply(200); const expectedActions = [ - { type: draftsDuck.DELETE_DRAFT_SUCCESS }, - { type: draftsDuck.FETCH_DRAFTS_REQUEST }, + { type: draftsDuck.DELETE_DRAFT_SUCCESS, id: 'draft-dir/test.md' }, ]; const store = mockStore({}); diff --git a/src/ducks/action_tests/pages.spec.js b/src/ducks/action_tests/pages.spec.js index b4b7d60e2..a12446061 100644 --- a/src/ducks/action_tests/pages.spec.js +++ b/src/ducks/action_tests/pages.spec.js @@ -51,12 +51,11 @@ describe('Actions::Pages', () => { it('deletes the page successfully', () => { nock(API) - .delete(`/pages/page-dir/test/test.md`) + .delete('/pages/page-dir/test/test.md') .reply(200); const expectedActions = [ - { type: pagesDuck.DELETE_PAGE_SUCCESS }, - { type: pagesDuck.FETCH_PAGES_REQUEST }, + { type: pagesDuck.DELETE_PAGE_SUCCESS, id: 'page-dir/test/test.md' }, ]; const store = mockStore({}); @@ -70,7 +69,7 @@ describe('Actions::Pages', () => { it('creates DELETE_PAGE_FAILURE when deleting a page failed', () => { nock(API) - .delete(`/pages/page.md`) + .delete('/pages/page.md') .replyWithError('something awful happened'); const expectedAction = { diff --git a/src/ducks/action_tests/staticfiles.spec.js b/src/ducks/action_tests/staticfiles.spec.js index 29ed37d2e..13da9cb71 100644 --- a/src/ducks/action_tests/staticfiles.spec.js +++ b/src/ducks/action_tests/staticfiles.spec.js @@ -95,8 +95,7 @@ describe('Actions::StaticFiles', () => { .reply(200); const expectedActions = [ - { type: staticfilesDuck.DELETE_STATICFILE_SUCCESS }, - { type: staticfilesDuck.FETCH_STATICFILES_REQUEST }, + { type: staticfilesDuck.DELETE_STATICFILE_SUCCESS, id: 'index.html' }, ]; const store = mockStore({ files: [] }); diff --git a/src/ducks/collections.js b/src/ducks/collections.js index 72d99c384..39b255e37 100644 --- a/src/ducks/collections.js +++ b/src/ducks/collections.js @@ -1,8 +1,13 @@ import moment from 'moment'; -import { clearErrors, validationError } from './utils'; +import { clearErrors, validationError, filterDeleted } from './utils'; import { get, put, del } from '../utils/fetch'; import { validator } from '../utils/validation'; -import { slugify, preparePayload, sanitizeFrontMatter } from '../utils/helpers'; +import { + slugify, + preparePayload, + sanitizeFrontMatter, + computeRelativePath, +} from '../utils/helpers'; import { collectionsAPIUrl, collectionAPIUrl, @@ -136,13 +141,10 @@ export const putDocument = (collection, directory, filename) => ( }; export const deleteDocument = (collection, directory, filename) => dispatch => { + const relative_path = computeRelativePath(directory, filename); return del( documentAPIUrl(collection, directory, filename), - { - type: DELETE_DOCUMENT_SUCCESS, - name: 'doc', - update: fetchCollection(collection, directory), - }, + { type: DELETE_DOCUMENT_SUCCESS, name: 'doc', id: relative_path }, { type: DELETE_DOCUMENT_FAILURE, name: 'error' }, dispatch ); @@ -237,6 +239,11 @@ export default function collections( currentDocument: action.doc, updated: true, }; + case DELETE_DOCUMENT_SUCCESS: + return { + ...state, + entries: filterDeleted(state.entries, action.id), + }; default: return { ...state, diff --git a/src/ducks/datafiles.js b/src/ducks/datafiles.js index 445ffaa6e..99fffbf96 100644 --- a/src/ducks/datafiles.js +++ b/src/ducks/datafiles.js @@ -1,7 +1,12 @@ -import { clearErrors, validationError } from './utils'; +import { clearErrors, validationError, filterDeleted } from './utils'; import { get, put, del } from '../utils/fetch'; import { datafilesAPIUrl, datafileAPIUrl } from '../constants/api'; -import { toYAML, getExtensionFromPath, trimObject } from '../utils/helpers'; +import { + toYAML, + getExtensionFromPath, + trimObject, + computeRelativePath, +} from '../utils/helpers'; import { validator } from '../utils/validation'; import translations from '../translations'; @@ -89,13 +94,10 @@ export const putDataFile = ( }; export const deleteDataFile = (directory, filename) => dispatch => { + const relative_path = computeRelativePath(directory, filename); return del( datafileAPIUrl(directory, filename), - { - type: DELETE_DATAFILE_SUCCESS, - name: 'file', - update: fetchDataFiles(directory), - }, + { type: DELETE_DATAFILE_SUCCESS, name: 'file', id: relative_path }, { type: DELETE_DATAFILE_FAILURE, name: 'error' }, dispatch ); @@ -171,6 +173,11 @@ export default function datafiles( ...state, datafileChanged: false, }; + case DELETE_DATAFILE_SUCCESS: + return { + ...state, + files: filterDeleted(state.files, action.id), + }; case DATAFILE_CHANGED: return { ...state, diff --git a/src/ducks/drafts.js b/src/ducks/drafts.js index be2815581..066d6cc9a 100644 --- a/src/ducks/drafts.js +++ b/src/ducks/drafts.js @@ -1,8 +1,13 @@ -import { clearErrors, validationError } from './utils'; +import { clearErrors, validationError, filterDeleted } from './utils'; import { PUT_DOCUMENT_SUCCESS, PUT_DOCUMENT_FAILURE } from './collections'; import { get, put, del } from '../utils/fetch'; import { validatePage } from '../utils/validation'; -import { slugify, preparePayload, sanitizeFrontMatter } from '../utils/helpers'; +import { + slugify, + preparePayload, + sanitizeFrontMatter, + computeRelativePath, +} from '../utils/helpers'; import { draftsAPIUrl, draftAPIUrl, documentAPIUrl } from '../constants/api'; // Action Types @@ -105,13 +110,10 @@ export const putDraft = (directory, filename) => (dispatch, getState) => { }; export const deleteDraft = (directory, filename) => dispatch => { + const relative_path = computeRelativePath(directory, filename); return del( draftAPIUrl(directory, filename), - { - type: DELETE_DRAFT_SUCCESS, - name: 'draft', - update: fetchDrafts(directory), - }, + { type: DELETE_DRAFT_SUCCESS, name: 'draft', id: relative_path }, { type: DELETE_DRAFT_FAILURE, name: 'error' }, dispatch ); @@ -191,6 +193,11 @@ export default function drafts( draft: action.draft, updated: true, }; + case DELETE_DRAFT_SUCCESS: + return { + ...state, + drafts: filterDeleted(state.drafts, action.id), + }; default: return { ...state, diff --git a/src/ducks/pages.js b/src/ducks/pages.js index 4da32805a..a45e01c8b 100644 --- a/src/ducks/pages.js +++ b/src/ducks/pages.js @@ -1,7 +1,12 @@ -import { clearErrors, validationError } from './utils'; +import { clearErrors, validationError, filterDeleted } from './utils'; import { get, put, del } from '../utils/fetch'; import { validatePage } from '../utils/validation'; -import { slugify, preparePayload, sanitizeFrontMatter } from '../utils/helpers'; +import { + slugify, + preparePayload, + sanitizeFrontMatter, + computeRelativePath, +} from '../utils/helpers'; import { pagesAPIUrl, pageAPIUrl } from '../constants/api'; // Action Types @@ -99,9 +104,10 @@ export const putPage = (directory, filename) => (dispatch, getState) => { }; export const deletePage = (directory, filename) => dispatch => { + const relative_path = computeRelativePath(directory, filename); return del( pageAPIUrl(directory, filename), - { type: DELETE_PAGE_SUCCESS, name: 'page', update: fetchPages(directory) }, + { type: DELETE_PAGE_SUCCESS, name: 'page', id: relative_path }, { type: DELETE_PAGE_FAILURE, name: 'error' }, dispatch ); @@ -155,6 +161,11 @@ export default function pages( page: action.page, updated: true, }; + case DELETE_PAGE_SUCCESS: + return { + ...state, + pages: filterDeleted(state.pages, action.id), + }; default: return { ...state, diff --git a/src/ducks/staticfiles.js b/src/ducks/staticfiles.js index 27f6ea192..5aba640b7 100644 --- a/src/ducks/staticfiles.js +++ b/src/ducks/staticfiles.js @@ -1,5 +1,7 @@ import _ from 'underscore'; +import { filterDeleted } from './utils'; import { get, del } from '../utils/fetch'; +import { computeRelativePath } from '../utils/helpers'; import { addNotification } from './notifications'; import { staticfilesAPIUrl, staticfileAPIUrl } from '../constants/api'; @@ -72,13 +74,10 @@ export const uploadStaticFiles = (directory, files) => dispatch => { }; export const deleteStaticFile = (directory, filename) => dispatch => { + const relative_path = computeRelativePath(directory, filename); return del( staticfileAPIUrl(directory, filename), - { - type: DELETE_STATICFILE_SUCCESS, - name: 'file', - update: fetchStaticFiles(directory), - }, + { type: DELETE_STATICFILE_SUCCESS, name: 'file', id: relative_path }, { type: DELETE_STATICFILE_FAILURE, name: 'error' }, dispatch ); @@ -125,6 +124,11 @@ export default function staticfiles( ...state, uploading: false, }; + case DELETE_STATICFILE_SUCCESS: + return { + ...state, + files: filterDeleted(state.files, action.id), + }; default: return state; } diff --git a/src/ducks/utils.js b/src/ducks/utils.js index ebd393fa1..9f5f2109c 100644 --- a/src/ducks/utils.js +++ b/src/ducks/utils.js @@ -6,6 +6,10 @@ export const filterBySearchInput = (list, input) => { return list.filter(p => p.name.toLowerCase().includes(input.toLowerCase())); }; +export const filterDeleted = (list, id) => { + return list.filter(item => item.relative_path !== id); +}; + // Action Types export const SEARCH_CONTENT = 'SEARCH_CONTENT'; export const CLEAR_ERRORS = 'CLEAR_ERRORS'; diff --git a/src/utils/fetch.js b/src/utils/fetch.js index 2cda502c9..acb5da32d 100644 --- a/src/utils/fetch.js +++ b/src/utils/fetch.js @@ -93,10 +93,12 @@ export const del = (url, action_success, action_failure, dispatch) => { method: 'DELETE', credentials: 'same-origin', }) - .then(data => { - dispatch({ type: action_success.type }); - dispatch(action_success.update); - }) + .then(data => + dispatch({ + type: action_success.type, + id: action_success.id, + }) + ) .catch(error => { dispatch({ type: action_failure.type, diff --git a/src/utils/helpers.js b/src/utils/helpers.js index d160295e8..33ae55002 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -156,6 +156,15 @@ export const getDocumentTitle = (type, splat, prefix = '') => { return [prefix, splat, label].filter(Boolean).join(' | '); }; +/** + * @param {String} directory - Directory splat for current resource. + * @param {String} filename - Basename of current resource. + * @return {String} Filename or directory splat joined to the filename. + */ +export const computeRelativePath = (directory, filename) => { + return directory ? `${directory}/${filename}` : `${filename}`; +}; + // omit raw_content, path and empty-value keys in metadata state from front_matter export const sanitizeFrontMatter = metadata => { return _.omit(metadata, (value, key, object) => { diff --git a/src/utils/validation.js b/src/utils/validation.js index c75a85f9f..49126b027 100644 --- a/src/utils/validation.js +++ b/src/utils/validation.js @@ -20,6 +20,17 @@ const validated = (field, single) => { } }; +export const validatePage = metadata => { + return validator( + metadata, + { path: 'required|filename' }, + { + 'path.required': getTitleRequiredMessage(), + 'path.filename': getFilenameNotValidMessage(), + } + ); +}; + /** * Returns error messages if the given values does not pass the provided validations. * @param {Object} values @@ -39,14 +50,3 @@ export const validator = (values, validations, messages) => { }); return errorMessages; }; - -export const validatePage = metadata => { - return validator( - metadata, - { path: 'required|filename' }, - { - 'path.required': getTitleRequiredMessage(), - 'path.filename': getFilenameNotValidMessage(), - } - ); -}; From 4661521cac6f8bba8a9415b60bf2fa5aa9619c31 Mon Sep 17 00:00:00 2001 From: Ashwin Maroli Date: Mon, 27 Jan 2020 14:40:59 +0530 Subject: [PATCH 16/16] Remove unused import --- src/utils/validation.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/validation.js b/src/utils/validation.js index 49126b027..dab838e42 100644 --- a/src/utils/validation.js +++ b/src/utils/validation.js @@ -1,5 +1,4 @@ import _ from 'underscore'; -import { getFilenameFromTitle } from '../utils/helpers'; import translations from '../translations'; const { getTitleRequiredMessage, getFilenameNotValidMessage } = translations;