diff options
| author | Jules Laplace <julescarbon@gmail.com> | 2021-03-06 17:10:15 +0100 |
|---|---|---|
| committer | Jules Laplace <julescarbon@gmail.com> | 2021-03-06 17:10:15 +0100 |
| commit | b5ceb782f40fc1e402d1e58bc1ced2e4038fd787 (patch) | |
| tree | 33c5e0f9fb50456e58f726d7d584c0699903105d /animism-align/frontend/app | |
| parent | 9acd74cf1c09a03f832869988c7d14597b26e4c7 (diff) | |
adding project CRUD editor
Diffstat (limited to 'animism-align/frontend/app')
12 files changed, 517 insertions, 0 deletions
diff --git a/animism-align/frontend/app/actions.js b/animism-align/frontend/app/actions.js index 67cbf85..4b75fce 100644 --- a/animism-align/frontend/app/actions.js +++ b/animism-align/frontend/app/actions.js @@ -15,6 +15,7 @@ const crudActions = [ 'annotation', 'upload', 'media', + 'project', 'episode', 'venue', 'user', diff --git a/animism-align/frontend/app/store.js b/animism-align/frontend/app/store.js index 3f30abd..980437a 100644 --- a/animism-align/frontend/app/store.js +++ b/animism-align/frontend/app/store.js @@ -10,6 +10,7 @@ import annotationReducer from 'app/views/annotation/annotation.reducer' import siteReducer from 'app/views/site/site.reducer' import mediaReducer from 'app/views/media/media.reducer' import episodeReducer from 'app/views/episode/episode.reducer' +import projectReducer from 'app/views/project/project.reducer' import venueReducer from 'app/views/venue/venue.reducer' import authReducer from 'app/views/auth/auth.reducer' import userReducer from 'app/views/user/user.reducer' @@ -31,6 +32,7 @@ const createRootReducer = history => ( annotation: annotationReducer, media: mediaReducer, episode: episodeReducer, + project: projectReducer, venue: venueReducer, user: userReducer, auth: authReducer, diff --git a/animism-align/frontend/app/types.js b/animism-align/frontend/app/types.js index d4d701a..5f625d1 100644 --- a/animism-align/frontend/app/types.js +++ b/animism-align/frontend/app/types.js @@ -7,6 +7,7 @@ export const peaks = crud_type('peaks', []) export const text = crud_type('text', []) export const annotation = crud_type('annotation', []) export const episode = crud_type('episode', []) +export const project = crud_type('project', []) export const venue = crud_type('venue', []) export const user = crud_type('user', []) export const paragraph = crud_type('paragraph', [ diff --git a/animism-align/frontend/app/views/index.js b/animism-align/frontend/app/views/index.js index ba16ae1..6f86af0 100644 --- a/animism-align/frontend/app/views/index.js +++ b/animism-align/frontend/app/views/index.js @@ -4,5 +4,6 @@ export { default as upload } from './upload/upload.container' export { default as media } from './media/media.container' export { default as viewer } from './viewer/viewer.container' export { default as episode } from './episode/episode.container' +export { default as project } from './project/project.container' export { default as venue } from './venue/venue.container' export { default as users } from './user/user.container' diff --git a/animism-align/frontend/app/views/project/components/project.form.js b/animism-align/frontend/app/views/project/components/project.form.js new file mode 100644 index 0000000..7a2ef2e --- /dev/null +++ b/animism-align/frontend/app/views/project/components/project.form.js @@ -0,0 +1,216 @@ +import React, { Component } from 'react' +import { Link } from 'react-router-dom' + +import { capitalize } from 'app/utils' + +import { TextInput, TextArea, NumberInput, LabelDescription, Select, Checkbox, SubmitButton, Loader } from 'app/common' + +const newProject = () => ({ + title: '', + is_live: false, + settings: { + description: "", + base_href: "", + ftp_url: "", + ftp_base_path: "", + }, +}) + +export default class ProjectForm extends Component { + state = { + title: "", + submitTitle: "", + data: { ...newProject() }, + errorFields: new Set([]), + } + + constructor(props) { + super(props) + this.handleKeyDown = this.handleKeyDown.bind(this) + this.handleSelect = this.handleSelect.bind(this) + this.handleChange = this.handleChange.bind(this) + this.handleSettingsChange = this.handleSettingsChange.bind(this) + this.handleSettingsChangeEvent = this.handleSettingsChangeEvent.bind(this) + this.handleSubmit = this.handleSubmit.bind(this) + } + + componentDidMount() { + const { data, isNew } = this.props + const title = isNew ? 'New project' : 'Editing ' + data.title + const submitTitle = isNew ? "Add Project" : "Save Changes" + this.setState({ + title, + submitTitle, + errorFields: new Set([]), + data: { + ...newProject(), + ...data + }, + }) + window.addEventListener('keydown', this.handleKeyDown) + } + + componentWillUnmount() { + window.removeEventListener('keydown', this.handleKeyDown) + } + + handleKeyDown(e) { + // console.log(e, e.keyCode) + if ((e.ctrlKey || e.metaKey) && e.keyCode === 83) { + if (e) { + e.preventDefault() + } + this.handleSubmit() + } + } + + handleChange(e) { + const { name, value } = e.target + this.handleSelect(name, value) + } + + handleSelect(name, value) { + const { errorFields } = this.state + if (errorFields.has(name)) { + errorFields.delete(name) + } + this.setState({ + errorFields, + data: { + ...this.state.data, + [name]: value, + } + }) + } + + handleSettingsChangeEvent(e) { + const { name, value } = e.target + this.handleSettingsChange(name, value) + } + + handleSettingsChange(name, value) { + // console.log(name, value) + if (name !== 'multiple') { + value = { [name]: value } + } + this.setState({ + data: { + ...this.state.data, + settings: { + ...this.state.data.settings, + ...value, + } + } + }) + } + + handleSubmit(e) { + if (e) { + e.preventDefault() + } + const { isNew, onSubmit } = this.props + const { data } = this.state + const requiredKeys = "title is_live".split(" ") + const validKeys = "title is_live settings".split(" ") + const validData = validKeys.reduce((a,b) => { a[b] = data[b]; return a }, {}) + if (!data.title) { + data.title = "TBD" + } + const errorFields = requiredKeys.filter(key => !validData[key]) + if (errorFields.length) { + console.log('error', errorFields, validData) + this.setState({ errorFields: new Set(errorFields) }) + } else { + if (isNew) { + // + } else { + validData.id = data.id + } + console.log('submit', validData) + onSubmit(validData) + } + } + + render() { + const { isNew } = this.props + const { title, submitTitle, errorFields, data } = this.state + // console.log(data) + return ( + <div className='form project-form'> + <h1>{title}</h1> + <form onSubmit={this.handleSubmit}> + <SubmitButton + title={submitTitle} + onClick={this.handleSubmit} + /> + <TextInput + title="Title" + name="title" + required + data={data} + onChange={this.handleChange} + autoComplete="off" + /> + <Checkbox + label="Project is live" + name="is_live" + checked={data.is_live} + onChange={this.handleSelect} + /> + + <TextArea + title="Description" + name="description" + required + data={data.settings} + onChange={this.handleSettingsChange} + autoComplete="off" + /> + + <h3>Deployment settings</h3> + + <TextInput + title="Base URL" + name="base_href" + required + data={data.settings} + onChange={this.handleSettingsChange} + autoComplete="off" + /> + <TextInput + title="FTP URL" + name="ftp_url" + required + data={data.settings} + onChange={this.handleSettingsChange} + autoComplete="off" + /> + + <TextInput + title="FTP Base Path" + name="ftp_base_path" + required + data={data.settings} + onChange={this.handleSettingsChange} + autoComplete="off" + /> + + <SubmitButton + title={submitTitle} + onClick={this.handleSubmit} + /> + {!!errorFields.size && + <label> + <span></span> + <span>Please complete the required fields</span> + </label> + } + </form> + </div> + ) + } +} +/* + open ftp://animism:Agkp8#48@93.114.86.205; + lcd ./data_store/exports/animism; +*/
\ No newline at end of file diff --git a/animism-align/frontend/app/views/project/components/project.menu.js b/animism-align/frontend/app/views/project/components/project.menu.js new file mode 100644 index 0000000..a29a451 --- /dev/null +++ b/animism-align/frontend/app/views/project/components/project.menu.js @@ -0,0 +1,57 @@ +import React, { Component } from 'react' +import { Route } from 'react-router-dom' +import { connect } from 'react-redux' + +import { history } from 'app/store' +import actions from 'app/actions' +import { MenuButton } from 'app/common' + +const mapStateToProps = state => ({ + project: state.project, +}) + +export default class ProjectMenu extends Component { + render() { + return ( + <div className='menuButtons'> + <Route exact path='/project/:id/show/' component={ProjectShowMenu} /> + <Route exact path='/project/:id/edit/' component={ProjectEditMenu} /> + <Route exact path='/project/new/' component={ProjectNewMenu} /> + <Route exact path='/project/' component={ProjectIndexMenu} /> + </div> + ) + } +} + +const ProjectIndexMenu = () => ([ + <MenuButton key='new' name="new" href="/project/new/" />, +]) + +const ProjectShowMenu = connect(mapStateToProps)((props) => ([ + <MenuButton key='back' name="back" href="/project/" />, + <MenuButton key='edit' name="edit" href={"/project/" + props.match.params.id + "/edit/"} />, + <MenuButton key='delete' name="delete" onClick={() => { + const { res: project } = props.project.show + if (confirm("Really delete this project?")) { + actions.project.destroy(project).then(() => { + history.push('/project/') + }) + } + }} />, +])) + +const ProjectNewMenu = (props) => ([ + <MenuButton key='back' name="back" href="/project/" />, +]) + +const ProjectEditMenu = connect(mapStateToProps)((props) => ([ + <MenuButton key='back' name="back" href="/project/" />, + <MenuButton key='delete' name="delete" onClick={() => { + const { res: project } = props.project.show + if (confirm("Really delete this project?")) { + actions.project.destroy(project).then(() => { + history.push('/project/') + }) + } + }} />, +])) diff --git a/animism-align/frontend/app/views/project/containers/project.edit.js b/animism-align/frontend/app/views/project/containers/project.edit.js new file mode 100644 index 0000000..dbb384d --- /dev/null +++ b/animism-align/frontend/app/views/project/containers/project.edit.js @@ -0,0 +1,52 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' + +import { history } from 'app/store' +import actions from 'app/actions' + +import { Loader } from 'app/common' + +import ProjectForm from '../components/project.form' +import ProjectMenu from '../components/project.menu' + +class ProjectEdit extends Component { + componentDidMount() { + console.log(this.props.match.params.id) + actions.project.show(this.props.match.params.id) + } + + handleSubmit(data) { + actions.project.update(data) + .then(response => { + // response + console.log(response) + history.push('/project/') + }) + } + + render() { + const { show } = this.props.project + if (show.loading || !show.res) { + return ( + <div className='form'> + <Loader /> + </div> + ) + } + return ( + <div className='row formContainer'> + <ProjectMenu projectActions={this.props.projectActions} /> + <ProjectForm + data={show.res} + onSubmit={this.handleSubmit.bind(this)} + /> + </div> + ) + } +} + +const mapStateToProps = state => ({ + project: state.project, +}) + +export default connect(mapStateToProps)(ProjectEdit) diff --git a/animism-align/frontend/app/views/project/containers/project.index.js b/animism-align/frontend/app/views/project/containers/project.index.js new file mode 100644 index 0000000..cf36970 --- /dev/null +++ b/animism-align/frontend/app/views/project/containers/project.index.js @@ -0,0 +1,71 @@ +import React, { Component } from 'react' +import { Link } from 'react-router-dom' +import { connect } from 'react-redux' + +import { Loader } from 'app/common' +import actions from 'app/actions' + +import ProjectMenu from '../components/project.menu' + +// const { result, collectionLookup } = this.props + +class ProjectIndex extends Component { + componentDidMount() { + this.fetch() + } + + fetch() { + actions.project.index() + } + + render() { + const { loading, lookup, order } = this.props.project.index + if (loading) { + return ( + <section> + <Loader /> + </section> + ) + } + if (!lookup || !order.length) { + return ( + <section> + <div className="row project-index"> + <ProjectMenu /> + <div> + <h1>Projects</h1> + <p className='gray'> + {"No projects"} + </p> + </div> + </div> + </section> + ) + } + return ( + <section> + <div className="row project-index"> + <ProjectMenu /> + <div className="project-list"> + <h1>Projects</h1> + {order.map(id => ( + <div key={id}> + {lookup[id].title}{': '} + <Link to={"/project/" + id + "/edit/"}> + {lookup[id].title} + </Link> + </div> + ))} + </div> + </div> + {order.length >= 50 && <button className='loadMore' onClick={() => this.fetch(true)}>Load More</button>} + </section> + ) + } +} + +const mapStateToProps = state => ({ + project: state.project, +}) + +export default connect(mapStateToProps)(ProjectIndex) diff --git a/animism-align/frontend/app/views/project/containers/project.new.js b/animism-align/frontend/app/views/project/containers/project.new.js new file mode 100644 index 0000000..c8a2152 --- /dev/null +++ b/animism-align/frontend/app/views/project/containers/project.new.js @@ -0,0 +1,62 @@ +import React, { Component } from 'react' +import { Link } from 'react-router-dom' +import { connect } from 'react-redux' + +import { history } from 'app/store' +import actions from 'app/actions' + +import ProjectForm from '../components/project.form' +import ProjectMenu from '../components/project.menu' + +class ProjectNew extends Component { + state = { + loading: true, + initialData: {}, + } + + componentDidMount() { + this.setState({ loading: false }) + } + + handleSubmit(data) { + console.log(data) + actions.project.create(data) + .then(res => { + console.log(res) + if (res.res && res.res.id) { + history.push('/project/') + } + }) + .catch(err => { + console.error('error') + }) + } + + render() { + if (this.state.loading) { + return ( + <div className='row formContainer' /> + ) + } + return ( + <div className='row formContainer'> + <ProjectMenu /> + <ProjectForm + isNew + data={this.state.initialData} + onSubmit={this.handleSubmit.bind(this)} + /> + </div> + ) + } +} + +const mapStateToProps = state => ({ + project: state.project, +}) + +const mapDispatchToProps = dispatch => ({ + // uploadActions: bindActionCreators({ ...uploadActions }, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(ProjectNew) diff --git a/animism-align/frontend/app/views/project/project.container.js b/animism-align/frontend/app/views/project/project.container.js new file mode 100644 index 0000000..0fba8c9 --- /dev/null +++ b/animism-align/frontend/app/views/project/project.container.js @@ -0,0 +1,23 @@ +import React, { Component } from 'react' +import { Route } from 'react-router-dom' + +import './project.css' + +import ProjectIndex from './containers/project.index' +import ProjectNew from './containers/project.new' +import ProjectEdit from './containers/project.edit' + +export default class Container extends Component { + render() { + return ( + <div className='projectContainer'> + <Route exact path='/project/:id/edit/' component={ProjectEdit} /> + <Route exact path='/project/new/' component={ProjectNew} /> + <Route exact path='/project/' component={ProjectIndex} /> + </div> + ) + } +} +/* + <Route exact path='/project/:id/show/' component={ProjectShow} /> + */
\ No newline at end of file diff --git a/animism-align/frontend/app/views/project/project.css b/animism-align/frontend/app/views/project/project.css new file mode 100644 index 0000000..36ed597 --- /dev/null +++ b/animism-align/frontend/app/views/project/project.css @@ -0,0 +1,13 @@ +.app > .projectContainer { + width: 100%; + height: calc(100% - 3.125rem); + overflow: scroll; +} + +.project-index { + margin-top: 1rem; +} + +.project-form h3 { + margin-top: 3rem; +}
\ No newline at end of file diff --git a/animism-align/frontend/app/views/project/project.reducer.js b/animism-align/frontend/app/views/project/project.reducer.js new file mode 100644 index 0000000..8d38b72 --- /dev/null +++ b/animism-align/frontend/app/views/project/project.reducer.js @@ -0,0 +1,18 @@ +// import * as types from 'app/types' + +import { crudState, crudReducer } from 'app/api/crud.reducer' + +const initialState = crudState('project', { + options: {}, +}) + +const reducer = crudReducer('project') + +export default function projectReducer(state = initialState, action) { + // console.log(action.type, action) + state = reducer(state, action) + switch (action.type) { + default: + return state + } +} |
