From ea81df9f127ab722aacc57cc46d36d04d0e7c618 Mon Sep 17 00:00:00 2001 From: Ashwin Maroli Date: Sun, 1 Mar 2020 10:56:54 +0530 Subject: [PATCH] Import site metadata into admin interface (#543) To allow selecting existing categories, layouts and tags via the interface. The idea is to fetch the *data* related to above every time a page / draft / document is created / edited so that the `Metafields` component always reflects the latest status (i.e. a category added to one post will be available for the next post being edited). --- lib/jekyll-admin/server.rb | 3 ++ lib/jekyll-admin/server/site_meta.rb | 25 +++++++++++++ src/constants/api.js | 2 ++ src/containers/MetaFields.js | 7 +++- src/containers/tests/metafields.spec.js | 21 ++++++++--- src/ducks/index.js | 2 ++ src/ducks/siteMeta.js | 48 +++++++++++++++++++++++++ 7 files changed, 102 insertions(+), 6 deletions(-) create mode 100644 lib/jekyll-admin/server/site_meta.rb create mode 100644 src/ducks/siteMeta.js diff --git a/lib/jekyll-admin/server.rb b/lib/jekyll-admin/server.rb index 532d02d3c..36dd463c1 100644 --- a/lib/jekyll-admin/server.rb +++ b/lib/jekyll-admin/server.rb @@ -103,3 +103,6 @@ def restored_front_matter # load individual route configurations JekyllAdmin::Server::ROUTES.each { |name| require_relative File.join("server", name) } + +# load namespaces outside route configurations +require_relative "server/site_meta" diff --git a/lib/jekyll-admin/server/site_meta.rb b/lib/jekyll-admin/server/site_meta.rb new file mode 100644 index 000000000..0fdd8b7b1 --- /dev/null +++ b/lib/jekyll-admin/server/site_meta.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module JekyllAdmin + class Server < Sinatra::Base + # Jekyll::Site instance method names that return a Hash. + META_KEYS = [:categories, :layouts, :tags].freeze + private_constant :META_KEYS + + namespace "/site_meta" do + get "/?" do + json site_meta + end + + private + + # Stash a Hash generated with pre-determined keys. + def site_meta + @site_meta ||= META_KEYS.zip(META_KEYS.map { |k| site.send(k).keys }).to_h + end + + # Reset memoization of `#site_meta` when the site regenerates + Jekyll::Hooks.register(:site, :after_reset) { @site_meta = nil } + end + end +end diff --git a/src/constants/api.js b/src/constants/api.js index c9154b144..b762dc321 100644 --- a/src/constants/api.js +++ b/src/constants/api.js @@ -3,6 +3,8 @@ export const API = ? '/_api' : 'http://localhost:4000/_api'; +export const getSiteMetaUrl = () => `${API}/site_meta`; + export const getConfigurationUrl = () => `${API}/configuration`; export const putConfigurationUrl = () => `${API}/configuration`; diff --git a/src/containers/MetaFields.js b/src/containers/MetaFields.js index 1e448852e..d6fc10150 100644 --- a/src/containers/MetaFields.js +++ b/src/containers/MetaFields.js @@ -15,10 +15,12 @@ import { moveArrayItem, convertField, } from '../ducks/metadata'; +import { fetchSiteMeta } from '../ducks/siteMeta'; export class MetaFields extends Component { componentDidMount() { - const { storeContentFields, fields } = this.props; + const { storeContentFields, fields, fetchSiteMeta, dataview } = this.props; + if (!dataview) fetchSiteMeta(); storeContentFields(fields); } @@ -119,18 +121,21 @@ MetaFields.propTypes = { updateFieldValue: PropTypes.func.isRequired, moveArrayItem: PropTypes.func.isRequired, convertField: PropTypes.func.isRequired, + fetchSiteMeta: PropTypes.func.isRequired, dataview: PropTypes.bool, }; const mapStateToProps = state => ({ metadata: state.metadata.metadata, key_prefix: state.metadata.key_prefix, + site: state.meta.site, }); const mapDispatchToProps = dispatch => bindActionCreators( { storeContentFields, + fetchSiteMeta, addField, removeField, updateFieldKey, diff --git a/src/containers/tests/metafields.spec.js b/src/containers/tests/metafields.spec.js index 26158e08e..1325a1547 100644 --- a/src/containers/tests/metafields.spec.js +++ b/src/containers/tests/metafields.spec.js @@ -13,6 +13,7 @@ const defaultProps = { const actions = { storeContentFields: jest.fn(), + fetchSiteMeta: jest.fn(), addField: jest.fn(), removeField: jest.fn(), updateFieldKey: jest.fn(), @@ -34,6 +35,21 @@ function setup(props = defaultProps) { } describe('Containers::MetaFields', () => { + it('does not call fetchSiteMeta before mount in dataview', () => { + const { actions } = setup({ ...defaultProps, dataview: true }); + expect(actions.fetchSiteMeta).not.toHaveBeenCalled(); + }); + + it('calls fetchSiteMeta before mount', () => { + const { actions } = setup(); + expect(actions.fetchSiteMeta).toHaveBeenCalled(); + }); + + it('calls storeContentFields before mount', () => { + const { actions } = setup(); + expect(actions.storeContentFields).toHaveBeenCalled(); + }); + it('renders MetaFields correctly', () => { let { component, addFieldButton, addDataFieldButton } = setup(); @@ -63,11 +79,6 @@ describe('Containers::MetaFields', () => { expect(component.prop('metadata')).toEqual(content); }); - it('calls storeContentFields before mount', () => { - const { actions } = setup(); - expect(actions.storeContentFields).toHaveBeenCalled(); - }); - it('calls addField when the button is clicked', () => { const { actions, addFieldButton } = setup(); addFieldButton.simulate('click'); diff --git a/src/ducks/index.js b/src/ducks/index.js index 8cb443107..23484db16 100644 --- a/src/ducks/index.js +++ b/src/ducks/index.js @@ -9,10 +9,12 @@ import drafts from './drafts'; import datafiles from './datafiles'; import staticfiles from './staticfiles'; import utils from './utils'; +import siteMeta from './siteMeta'; import notifications from './notifications'; export default combineReducers({ routing: routerReducer, + meta: siteMeta, config, pages, collections, diff --git a/src/ducks/siteMeta.js b/src/ducks/siteMeta.js new file mode 100644 index 000000000..a8653a247 --- /dev/null +++ b/src/ducks/siteMeta.js @@ -0,0 +1,48 @@ +import { getSiteMetaUrl } from '../constants/api'; +import { get } from '../utils/fetch'; + +// Action Types +export const FETCH_SITE_META_REQUEST = 'FETCH_SITE_META_REQUEST'; +export const FETCH_SITE_META_SUCCESS = 'FETCH_SITE_META_SUCCESS'; +export const FETCH_SITE_META_FAILURE = 'FETCH_SITE_META_FAILURE'; + +// Actions +export const fetchSiteMeta = () => dispatch => { + dispatch({ type: FETCH_SITE_META_REQUEST }); + return get( + getSiteMetaUrl(), + { type: FETCH_SITE_META_SUCCESS, name: 'site' }, + { type: FETCH_SITE_META_FAILURE, name: 'error' }, + dispatch + ); +}; + +// Reducer +export default function siteMeta( + state = { + site: {}, + isFetching: false, + }, + action +) { + switch (action.type) { + case FETCH_SITE_META_REQUEST: + return { + ...state, + isFetching: true, + }; + case FETCH_SITE_META_SUCCESS: + return { + ...state, + site: action.site, + isFetching: false, + }; + case FETCH_SITE_META_FAILURE: + return { + ...state, + isFetching: false, + }; + default: + return state; + } +}