diff --git a/src/components/ProjectContents.jsx b/src/components/ProjectContents.jsx index a93179da..ead8d0b6 100644 --- a/src/components/ProjectContents.jsx +++ b/src/components/ProjectContents.jsx @@ -14,7 +14,7 @@ class ProjectContents extends Component { render() { const { contents } = this.props; - const project_contents = contents.data.length ? contents.data[0] : {}; + const project_contents = contents.original.length ? contents.original[0] : {}; return (

Project Contents

diff --git a/src/components/ProjectDashboard.jsx b/src/components/ProjectDashboard.jsx new file mode 100644 index 00000000..38c2ad92 --- /dev/null +++ b/src/components/ProjectDashboard.jsx @@ -0,0 +1,54 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import fixIt from 'react-fix-it'; +import { Link } from 'react-router'; +import ProjectContentsContainer from '../containers/ProjectContentsContainer'; +import ProjectContents from './ProjectContents'; + +const propTypes = { + project: PropTypes.object.isRequired, + tutorials: PropTypes.array.isRequired, + workflows: PropTypes.array.isRequired, +}; + +function ProjectDashboard(props) { + const { project } = props; + return ( +
+ + + +

Workflows

+ +

Tutorials

+ +
+ ); +} + +ProjectDashboard.propTypes = propTypes; + +ProjectDashboard.defaultProps = { + project: { + tutorials: [], + workflows: [] + } +}; + +export default fixIt(ProjectDashboard); diff --git a/src/components/ProjectList.jsx b/src/components/ProjectList.jsx index e1ff54e5..3afacbd8 100644 --- a/src/components/ProjectList.jsx +++ b/src/components/ProjectList.jsx @@ -22,7 +22,7 @@ class ProjectList extends Component {

Projects

); diff --git a/src/components/Resource.jsx b/src/components/Resource.jsx new file mode 100644 index 00000000..a1551932 --- /dev/null +++ b/src/components/Resource.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import fixIt, { options } from 'react-fix-it'; +import Tutorial from './Tutorial'; +import WorkflowContents from './WorkflowContents'; + +const resources = { + tutorials: Tutorial, + workflows: WorkflowContents +}; + +const propTypes = { + contents: PropTypes.object.isRequired, + params: PropTypes.shape({ + resource_type: PropTypes.string + }) +}; + +options.log = (test) => { + console.warn(test); +}; + +function Resource(props) { + const { contents } = props; + const ResourceViewer = resources[props.params.resource_type]; + return (); +} + +Resource.propTypes = propTypes; +export default fixIt(Resource); diff --git a/src/components/Tutorial.jsx b/src/components/Tutorial.jsx new file mode 100644 index 00000000..7d835680 --- /dev/null +++ b/src/components/Tutorial.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import fixIt, { options } from 'react-fix-it'; + +const propTypes = { + contents: PropTypes.object.isRequired, +}; + +options.log = (test) => { + console.warn(test); +}; + +function Tutorial(props) { + const { contents } = props; + const tutorial = contents.original.length ? contents.original[0] : { steps: [] }; + const steps = []; + tutorial.steps && tutorial.steps.map((step, key) => { + steps.push(

{key} {step.content}

); + }); + return ( +
+

Tutorial

+ {steps} +
+ ); +} + +Tutorial.propTypes = propTypes; +export default fixIt(Tutorial); diff --git a/src/components/WorkflowContents.jsx b/src/components/WorkflowContents.jsx new file mode 100644 index 00000000..9027a0ce --- /dev/null +++ b/src/components/WorkflowContents.jsx @@ -0,0 +1,33 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import fixIt, { options } from 'react-fix-it'; + +const propTypes = { + contents: PropTypes.object.isRequired, +}; + +options.log = (test) => { + console.warn(test); +}; + +class WorkflowContents extends Component { + + render() { + const { contents } = this.props; + const workflow_contents = contents.original.length ? contents.original[0] : {strings: []}; + console.log(workflow_contents) + const strings = []; + for (const key in workflow_contents.strings) { + strings.push(

{key} {workflow_contents.strings[key]}

); + } + return ( +
+

Workflow Contents

+ {strings} +
+ ); + } +} + +WorkflowContents.propTypes = propTypes; +export default fixIt(WorkflowContents); diff --git a/src/containers/ProjectContentsContainer.jsx b/src/containers/ProjectContentsContainer.jsx index 5db3bf50..0b5e18eb 100644 --- a/src/containers/ProjectContentsContainer.jsx +++ b/src/containers/ProjectContentsContainer.jsx @@ -2,14 +2,18 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import * as contentsActions from '../ducks/contents'; -import ProjectContents from '../components/ProjectContents'; +import * as contentsActions from '../ducks/resource'; import { BaseModal, ModalBody, ModalFooter } from 'pui-react-modals'; import { DefaultButton } from 'pui-react-buttons'; const propTypes = { actions: PropTypes.object.isRequired, - contents: PropTypes.object.isRequired, + children: PropTypes.node, + params: PropTypes.shape({ + project_id: PropTypes.string, + resource_id: PropTypes.string, + resource_type: PropTypes.string + }) }; class ProjectContentsContainer extends Component { @@ -24,7 +28,9 @@ class ProjectContentsContainer extends Component { componentDidMount() { const { actions } = this.props; - return actions.fetchProjectContents(this.props.params.project_id); + const type = this.props.params.resource_type; + const id = type ? this.props.params.resource_id : this.props.params.project_id; + actions.fetchResource(id, type); } handleClick(event) { @@ -35,7 +41,7 @@ class ProjectContentsContainer extends Component { } render() { - const { contents } = this.props; + const { resource } = this.props; return (
- + {React.cloneElement(this.props.children, { contents: resource })}
); @@ -63,7 +69,7 @@ class ProjectContentsContainer extends Component { } const mapStateToProps = (state) => ({ - contents: state.contents, + resource: state.resource, }); const mapDispatchToProps = (dispatch) => ({ actions: bindActionCreators(contentsActions, dispatch), diff --git a/src/containers/ProjectDashboardContainer.jsx b/src/containers/ProjectDashboardContainer.jsx new file mode 100644 index 00000000..cb2b9879 --- /dev/null +++ b/src/containers/ProjectDashboardContainer.jsx @@ -0,0 +1,58 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import * as projectActions from '../ducks/project'; + +const propTypes = { + actions: PropTypes.object.isRequired, + children: PropTypes.node, + project: PropTypes.shape({ + data: PropTypes.object, + tutorials: PropTypes.array, + workflows: PropTypes.array + }), + params: PropTypes.shape({ + project_id: PropTypes.string + }) +}; + +class ProjectDashboardContainer extends Component { + + componentDidMount() { + const { actions } = this.props; + actions.fetchProject(this.props.params.project_id); + } + + render() { + const project = this.props.project.data; + const { workflows, tutorials } = this.props.project; + return ( +
+

Project Dashboard

+ {React.cloneElement(this.props.children, { project, workflows, tutorials })} +
+ ); + } +} + +const mapStateToProps = (state) => ({ + project: state.project +}); +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators(projectActions, dispatch), +}); + +ProjectDashboardContainer.propTypes = propTypes; +ProjectDashboardContainer.defaultProps = { + children: null, + project: { + data: null, + tutorials: [], + workflows: [] + } +}; +export default connect( + mapStateToProps, + mapDispatchToProps, +)(ProjectDashboardContainer); diff --git a/src/ducks/project.js b/src/ducks/project.js new file mode 100644 index 00000000..92a0f15a --- /dev/null +++ b/src/ducks/project.js @@ -0,0 +1,102 @@ +import apiClient from 'panoptes-client/lib/api-client'; + +// Action Types +export const FETCH_PROJECT = 'FETCH_PROJECT'; +export const FETCH_PROJECT_SUCCESS = 'FETCH_PROJECT_SUCCESS'; +export const FETCH_PROJECT_ERROR = 'FETCH_PROJECT_ERROR'; +export const FETCH_WORKFLOWS = 'FETCH_WORKFLOWS'; +export const FETCH_WORKFLOWS_SUCCESS = 'FETCH_WORKFLOWS_SUCCESS'; +export const FETCH_WORKFLOWS_ERROR = 'FETCH_WORKFLOWS_ERROR'; +export const FETCH_TUTORIALS = 'FETCH_TUTORIALS'; +export const FETCH_TUTORIALS_SUCCESS = 'FETCH_TUTORIALS_SUCCESS'; +export const FETCH_TUTORIALS_ERROR = 'FETCH_TUTORIALS_ERROR'; + +// Reducer +const initialState = { + data: {}, + tutorials: [], + workflows: [], + error: false, + loading: false, +}; + +const projectReducer = (state = initialState, action) => { + switch (action.type) { + case FETCH_PROJECT: + return Object.assign({}, initialState, { loading: true }); + case FETCH_PROJECT_SUCCESS: + return Object.assign({}, state, { data: action.payload, loading: false }); + case FETCH_PROJECT_ERROR: + return Object.assign({}, state, { error: action.payload, loading: false }); + case FETCH_WORKFLOWS: + return Object.assign({}, state, { loading: true }); + case FETCH_WORKFLOWS_SUCCESS: + return Object.assign({}, state, { workflows: action.payload, loading: false }); + case FETCH_TUTORIALS: + return Object.assign({}, state, { loading: true }); + case FETCH_TUTORIALS_SUCCESS: + return Object.assign({}, state, { tutorials: action.payload, loading: false }); + default: + return state; + } +}; + +// Action Creators +const fetchProject = (id) => { + return (dispatch) => { + dispatch({ + type: FETCH_PROJECT, + }); + const query = { + id, + current_user_roles: ['owner', 'translator'], + include: ['workflows'] + }; + apiClient.type('projects').get(query) + .then(([project]) => { + dispatch(fetchWorkflows(project)); + dispatch(fetchTutorials(project)); + dispatch({ + type: FETCH_PROJECT_SUCCESS, + payload: project, + }); + }); + }; +}; + +function fetchWorkflows(project) { + return (dispatch) => { + dispatch({ + type: FETCH_WORKFLOWS, + }); + apiClient.type('workflows').get(project.links.workflows) + .then((workflows) => { + dispatch({ + type: FETCH_WORKFLOWS_SUCCESS, + payload: workflows, + }); + }); + }; +} + +function fetchTutorials(project) { + return (dispatch) => { + dispatch({ + type: FETCH_TUTORIALS, + }); + apiClient.type('tutorials').get({ project_id: project.id }) + .then((tutorials) => { + dispatch({ + type: FETCH_TUTORIALS_SUCCESS, + payload: tutorials, + }); + }); + }; +} + +// Exports +export default projectReducer; + +export { + fetchProject +}; diff --git a/src/ducks/projects.js b/src/ducks/projects.js index 1c6f6215..3a23e892 100644 --- a/src/ducks/projects.js +++ b/src/ducks/projects.js @@ -33,7 +33,7 @@ const fetchProjects = () => { }); const query = { current_user_roles: ['owner', 'translator'] - } + }; apiClient.type('projects').get(query) .then((projects) => { dispatch({ diff --git a/src/ducks/reducer.js b/src/ducks/reducer.js index 47f6349e..2ee57a40 100644 --- a/src/ducks/reducer.js +++ b/src/ducks/reducer.js @@ -1,10 +1,14 @@ import { combineReducers } from 'redux'; import contents from './contents'; import login from './login'; +import project from './project'; import projects from './projects'; +import resource from './resource'; export default combineReducers({ contents, login, - projects + project, + projects, + resource }); diff --git a/src/ducks/resource.js b/src/ducks/resource.js new file mode 100644 index 00000000..78e5c444 --- /dev/null +++ b/src/ducks/resource.js @@ -0,0 +1,107 @@ +import apiClient from 'panoptes-client/lib/api-client'; + +// Action Types +export const FETCH_RESOURCE = 'FETCH_RESOURCE'; +export const FETCH_RESOURCE_SUCCESS = 'FETCH_RESOURCE_SUCCESS'; +export const FETCH_RESOURCE_ERROR = 'FETCH_RESOURCE_ERROR'; +export const CREATE_TRANSLATION_SUCCESS = 'CREATE_TRANSLATION_SUCCESS'; + +// Reducer +const initialState = { + original: [], + translation: null, + error: false, + loading: false, +}; + +const resourceReducer = (state = initialState, action) => { + switch (action.type) { + case FETCH_RESOURCE: + return Object.assign({}, initialState, { loading: true }); + case FETCH_RESOURCE_SUCCESS: + return Object.assign({}, state, { original: action.payload, loading: false }); + case FETCH_RESOURCE_ERROR: + return Object.assign({}, state, { error: action.payload, loading: false }); + case CREATE_TRANSLATION_SUCCESS: + return Object.assign({}, state, { translation: action.payload, loading: false }); + default: + return state; + } +}; + +// Action Creators +const fetchResource = (id, type) => { + type = type ? type : 'projects'; + return (dispatch) => { + switch (type) { + case 'projects': + case 'workflows': + dispatch(fetchResourceContents(id, type)); + break; + default: + dispatch({ + type: FETCH_RESOURCE, + }); + apiClient.type(type).get({ id }) + .then((resource) => { + dispatch({ + type: FETCH_RESOURCE_SUCCESS, + payload: resource, + }); + }); + } + }; +}; + +function fetchResourceContents(id, type) { + return (dispatch) => { + dispatch({ + type: FETCH_RESOURCE, + }); + let key = ''; + switch (type) { + case 'projects': + key = 'project_id'; + type = 'project_contents'; + break; + case 'workflows': + key = 'workflow_id'; + type = 'workflow_contents'; + break; + } + const query = {}; + query[key] = id; + apiClient.type(type).get(query) + .then((resource) => { + dispatch({ + type: FETCH_RESOURCE_SUCCESS, + payload: resource, + }); + }); + }; +} + +const createTranslation = (type, lang) => + (dispatch, getState) => { + const { original } = getState(); + const newResource = Object.assign({}, original); + newResource.lang = lang; + apiClient.type(type) + .create(newResource) + .save() + .then((translation) => { + dispatch({ + type: CREATE_TRANSLATION_SUCCESS, + payload: translation, + }); + }) + .catch(error => console.error(error)); + }; + +// Exports +export default resourceReducer; + +export { + createTranslation, + fetchResource, +}; diff --git a/src/index.jsx b/src/index.jsx index c7a47806..2f108778 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -8,8 +8,11 @@ import App from './components/App'; import About from './components/About'; import config from './config'; import configureStore from './store'; -import ProjectContents from './containers/ProjectContentsContainer'; +import ProjectContentsContainer from './containers/ProjectContentsContainer'; +import ProjectDashboardContainer from './containers/ProjectDashboardContainer'; +import ProjectDashboard from './components/ProjectDashboard'; import ProjectList from './containers/ProjectListContainer'; +import Resource from './components/Resource'; // Todo: let's find a better way to include Styles, // currently Styles looks like an unused var to eslint @@ -24,7 +27,12 @@ oauth.init(config.panoptesAppId) - + + + + + +