Skip to content

Commit

Permalink
Merge pull request #819 from Datawheel/refactor-redux
Browse files Browse the repository at this point in the history
Redux Refactor and Horizontal Navigation
  • Loading branch information
davelandry committed Nov 15, 2019
2 parents 01dbfdf + 78c9e3e commit 92cc52a
Show file tree
Hide file tree
Showing 62 changed files with 3,630 additions and 3,126 deletions.
2 changes: 1 addition & 1 deletion app/style.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ type-error: "#DB64A6"
type-error-dark: "#75224D"

# measurements
cms-nav-height: "3.125rem"
cms-nav-height: "2.75rem"
sidebar-width: "17rem"
toolbox-width: "22.5rem"

Expand Down
2 changes: 1 addition & 1 deletion bin/scaffold/app/style.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ type-error: "#DB64A6"
type-error-dark: "#75224D"

# measurements
cms-nav-height: "3.125rem"
cms-nav-height: "2.75rem"
sidebar-width: "17rem"
toolbox-width: "22.5rem"

Expand Down
16 changes: 14 additions & 2 deletions packages/cms/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,24 @@ import {Builder} from "@datawheel/canon-cms";
<Route path="/cms" component={Builder} />
```

#### 5) Start your dev server
#### 5) Configure Redux

The CMS state state is managed from the site-wide redux state. In `app/reducers/index.js`, import the reducer function and assign it to the `cms` key:

```js
import {cmsReducer} from "@datawheel/canon-cms";

export default {
cms: cmsReducer
};
```

#### 6) Start your dev server
```sh
npm run dev
```

#### 6) Navigate to the CMS panel
#### 7) Navigate to the CMS panel

`http://localhost:3300/cms`

Expand Down
6 changes: 5 additions & 1 deletion packages/cms/app/reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@
combined with the internal default canon reducers.
*/

export default {};
import cmsReducer from "../../src/reducers/index.js";

export default {
cms: cmsReducer
};
2 changes: 1 addition & 1 deletion packages/cms/app/style.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ type-error: "#DB64A6"
type-error-dark: "#75224D"

# measurements
cms-nav-height: "3.125rem"
cms-nav-height: "2.75rem"
sidebar-width: "17rem"
toolbox-width: "22.5rem"

Expand Down
214 changes: 53 additions & 161 deletions packages/cms/src/Builder.jsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import funcifyFormatterByLocale from "./utils/funcifyFormatterByLocale";
import React, {Component} from "react";
import {connect} from "react-redux";
import PropTypes from "prop-types";
import yn from "yn";
import {Icon} from "@blueprintjs/core";

import {fetchData} from "@datawheel/canon-core";
import {isAuthenticated} from "@datawheel/canon-core";

import funcifyFormatterByLocale from "./utils/funcifyFormatterByLocale";

import ProfileBuilder from "./profile/ProfileBuilder";
import StoryBuilder from "./story/StoryBuilder";
import MetaEditor from "./member/MetaEditor";
import Select from "./components/fields/Select";
import Button from "./components/fields/Button";
import AuthForm from "./components/interface/AuthForm";
import Navbar from "./components/interface/Navbar";

import {setStatus} from "./actions/status";

import AceWrapper from "./components/editors/AceWrapper";

Expand All @@ -27,13 +29,9 @@ class Builder extends Component {
constructor(props) {
super(props);
this.state = {
currentTab: null,
locales: false,
localeDefault: false,
secondaryLocale: false,
pathObj: {},
formatters: {},
userInit: false
userInit: false,
outlineOpen: true
};
}

Expand All @@ -47,7 +45,7 @@ class Builder extends Component {

// The CMS is only accessible on localhost/dev. Redirect the user to root otherwise.
if (!isEnabled && typeof window !== "undefined" && window.location.pathname !== "/") window.location = "/";

let currentTab;
if (tab) {
currentTab = tab;
Expand All @@ -68,84 +66,67 @@ class Builder extends Component {
if (env.CANON_LANGUAGES && env.CANON_LANGUAGES.includes(",")) {
const locales = env.CANON_LANGUAGES.split(",").filter(l => l !== localeDefault);
// Default to no secondary language
const secondaryLocale = null;
const localeSecondary = null;
locales.forEach(locale => {
formatters[locale] = funcifyFormatterByLocale(this.props.formatters, locale);
});
this.setState({locales, formatters, secondaryLocale, localeDefault, pathObj, currentTab});
this.props.setStatus({locales, localeSecondary, localeDefault, pathObj});
this.setState({formatters});
}
else {
this.setState({localeDefault, formatters, pathObj, currentTab});
this.props.setStatus({localeDefault, pathObj});
this.setState({formatters});
}

}

componentDidUpdate(prevProps) {
if (prevProps.auth.loading && !this.props.auth.loading) {
this.setState({userInit: true});
}
// if location queries change, create new pathobj & set that
if (JSON.stringify(prevProps.status.pathObj) !== JSON.stringify(this.props.status.pathObj)) {
this.setPath.bind(this)();
}
}

getChildContext() {
const {formatters} = this.state;
const setPath = this.setPath.bind(this);
return {
formatters,
setPath
formatters
};
}

handleTabChange(newTab) {
const {currentTab} = this.state;
if (newTab !== currentTab) {
const newPathObj = {tab: newTab};
this.setState({currentTab: newTab, pathObj: newPathObj}, this.setPath.bind(this, newPathObj));
}
}

handleLocaleSelect(e) {
const val = e.target.value;
this.setState({
secondaryLocale: val === "none" ? null : val
});
}

toggleSettings() {
this.setState({settingsOpen: !this.state.settingsOpen});
}

setPath(pathObj) {
const {currentTab} = this.state;
// The underlying Editors don't know about tabs, so they will send pathObjs that don't have a tab in them.
// Always trust Builder.jsx's current tab, and assign it into whatever the Editors send up.
pathObj = Object.assign({}, pathObj, {tab: currentTab});
setPath() {
const {pathObj} = this.props.status;
const {router} = this.props;
const {pathname} = router.location;
let url = `${pathname}?tab=${pathObj.tab}`;
// Profile
if (pathObj.profile) url += `&profile=${pathObj.profile}`;
if (pathObj.section) url += `&section=${pathObj.section}`;
// previews may come in as a string (from the URL) or an array (from the app).
// Set the url correctly either way.
if (pathObj.previews) {
const previews = pathObj.previews.map(d => d.id).join();
const previews = typeof pathObj.previews === "string" ? pathObj.previews : pathObj.previews.map(d => d.id).join();
url += `&previews=${previews}`;
}
// Story
// Story
if (pathObj.story) url += `&story=${pathObj.story}`;
if (pathObj.storysection) url += `&storysection=${pathObj.storysection}`;
router.replace(url);
this.setState({pathObj});
}

render() {
const {currentTab, secondaryLocale, locales, localeDefault, pathObj, settingsOpen, userInit} = this.state;
const {userInit} = this.state;
const {isEnabled, env, auth, router} = this.props;
const {pathObj} = this.props.status;
const currentTab = pathObj.tab;
let {pathname} = router.location;
if (pathname.charAt(0) !== "/") pathname = `/${pathname}`;
const navLinks = ["profiles", "stories", "metadata"];

const waitingForUser = yn(env.CANON_LOGINS) && !userInit;

if (!isEnabled || waitingForUser) return null;
if (!isEnabled || waitingForUser || !currentTab) return null;

if (yn(env.CANON_LOGINS) && !auth.user) return <AuthForm redirect={pathname}/>;

Expand All @@ -155,121 +136,32 @@ class Builder extends Component {
);
}

return (
<div className={`cms cms-${currentTab}-page`}>
<div className={`cms-nav${settingsOpen ? " settings-visible" : ""}`}>
<div className="cms-nav-main">
{navLinks.map(navLink =>
<button
key={navLink}
className={`cms-nav-link u-font-xs${navLink === currentTab ? " is-active" : ""}`}
onClick={this.handleTabChange.bind(this, navLink)}>
{navLink}
</button>
)}
</div>
{(locales || auth.user) && <React.Fragment>
<div className="cms-nav-settings-button-container">
<Button
className="cms-nav-settings-button"
namespace="cms"
icon="cog"
fontSize="xs"
active={settingsOpen}
onClick={this.toggleSettings.bind(this)}
>
settings
</Button>
</div>
// Define component to render as editor
let Builder;
if (currentTab === "metadata") Builder = MetaEditor;
if (currentTab === "profiles") Builder = ProfileBuilder;
if (currentTab === "stories") Builder = StoryBuilder;

if (!Builder) return null;

<div className={`cms-nav-settings ${settingsOpen ? "is-visible" : "is-hidden"}`}>
{/* locale select */}
{locales &&
<React.Fragment>
<h2 className="cms-nav-settings-heading u-font-sm">
Languages
</h2>
{/* primary locale */}
{/* NOTE: currently just shows the primary locale in a dropdown */}
<Select
label="Primary"
fontSize="xs"
namespace="cms"
inline
options={[localeDefault]}
tabIndex={settingsOpen ? null : "-1"}
/>
{/* secondary locale */}
<Select
label="Secondary"
fontSize="xs"
namespace="cms"
inline
value={secondaryLocale ? secondaryLocale : "none"}
options={locales.map(loc => loc)}
onChange={this.handleLocaleSelect.bind(this)}
tabIndex={settingsOpen ? null : "-1"}
>
<option value="none">none</option>
</Select>
</React.Fragment>
}
{auth.user &&
<React.Fragment>
<h2 className="cms-nav-settings-heading u-font-sm u-margin-top-md">
Account
</h2>
<a className="cms-button cms-fill-button u-margin-bottom-xs" href="/auth/logout">
<Icon className="cms-button-icon" icon="log-out" />
<span className="cms-button-text">Log Out</span>
</a>
</React.Fragment>
}
</div>
<button
className={`cms-nav-settings-overlay ${settingsOpen ? "is-visible" : "is-hidden"}`}
onClick={this.toggleSettings.bind(this)}
tabIndex={settingsOpen ? null : "-1"}
/>
</React.Fragment> }
</div>
// This invisible AceWrapper is necessary, because running the require function in the render cycle of AceWrapper
// can cause components to remount (notably the toolbox, hitting all generators). By putting this dummy AceWrapper in
// the top-level Builder.jsx, we run the require function "once and for all", so future instantiations of AceWrapper
// do not cause any components to jigger and unmount/remount.
const HiddenAce = <div className="u-visually-hidden"><AceWrapper /></div>;

{currentTab === "profiles" &&
<ProfileBuilder
pathObj={pathObj}
localeDefault={localeDefault}
locale={secondaryLocale}
/>
}
{currentTab === "stories" &&
<StoryBuilder
pathObj={pathObj}
localeDefault={localeDefault}
locale={secondaryLocale}
/>
}
{currentTab === "metadata" &&
<MetaEditor
pathObj={pathObj}
localeDefault={localeDefault}
locale={secondaryLocale}
/>
}
{/*
This invisible AceWrapper is necessary, because running the require function in the render cycle of AceWrapper
can cause components to remount (notably the toolbox, hitting all generators). By putting this dummy AceWrapper in
the top-level Builder.jsx, we run the require function "once and for all", so future instantiations of AceWrapper
do not cause any components to jigger and unmount/remount.
*/}
<div style={{display: "none"}}><AceWrapper /></div>
return (
<div className={`cms cms-${currentTab}-page`}>
<Navbar key="navbar" />
<Builder key="editor" />
{HiddenAce}
</div>
);
}
}

Builder.childContextTypes = {
formatters: PropTypes.object,
setPath: PropTypes.func
formatters: PropTypes.object
};

Builder.need = [
Expand All @@ -278,17 +170,17 @@ Builder.need = [
];

const mapStateToProps = state => ({
formatters: state.data.formatters,
env: state.env,
isEnabled: state.data.isEnabled,
auth: state.auth
auth: state.auth,
status: state.cms.status,
formatters: state.data.formatters,
isEnabled: state.data.isEnabled
});

const mapDispatchToProps = dispatch => ({
dispatch: action => dispatch(action),
isAuthenticated: () => {
dispatch(isAuthenticated());
}
setStatus: status => dispatch(setStatus(status)),
isAuthenticated: () => dispatch(isAuthenticated())
});

export default connect(mapStateToProps, mapDispatchToProps)(Builder);
11 changes: 11 additions & 0 deletions packages/cms/src/actions/cubeData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import axios from "axios";

/** */
export function getCubeData() {
return function(dispatch, getStore) {
return axios.get(`${getStore().env.CANON_API}/api/cubeData`)
.then(({data}) => {
dispatch({type: "CUBEDATA_GET", data});
});
};
}
Loading

0 comments on commit 92cc52a

Please sign in to comment.