From aacc1cb0be9dffac0f53c36448085fdb46f95f5a Mon Sep 17 00:00:00 2001 From: Shipra Verma Date: Mon, 1 Mar 2021 13:02:48 +0530 Subject: [PATCH] Add feature to fill, edit and view forms --- src/Routes.js | 4 +- src/actions/answer.js | 53 +++++++ src/actions/types.js | 3 + src/components/Preview.js | 268 +++++++++++++++++++++++++++++------- src/components/Questions.js | 15 +- src/components/Upload.js | 72 ---------- src/reducers/answer.js | 36 +++++ src/reducers/index.js | 2 + src/urls.js | 25 +++- 9 files changed, 340 insertions(+), 138 deletions(-) create mode 100644 src/actions/answer.js delete mode 100644 src/components/Upload.js create mode 100644 src/reducers/answer.js diff --git a/src/Routes.js b/src/Routes.js index 5f5b474..5ad5c91 100644 --- a/src/Routes.js +++ b/src/Routes.js @@ -7,8 +7,7 @@ import Forms from './components/Forms' import ErrorPage from './components/ErrorPage' import Submission from './components/Submission' import Questions from './components/Questions' -import { login, register, dashboard, forms, upload, submission, urlBaseFrontend } from './urls' -import Upload from './components/Upload' +import { login, register, dashboard, forms, submission, urlBaseFrontend } from './urls' import {PrivateRoute} from './PrivateRoute' import { AuthRoute } from './AuthRoute' @@ -23,7 +22,6 @@ export default class Routes extends Component { - diff --git a/src/actions/answer.js b/src/actions/answer.js new file mode 100644 index 0000000..67a38e6 --- /dev/null +++ b/src/actions/answer.js @@ -0,0 +1,53 @@ +import axios from 'axios' +import { + urlFormFeedback, + urlSubmissions +} from '../urls' +import { + GET_ANSWERS, + POST_ANSWERS, + ANSWER_ERROR +} from './types' + +export const getAnswers = (form_id) => async dispatch => { + try { + const config = { + headers: { + Authorization: `Bearer ${localStorage.token}`, + } + } + const res = await axios.get(urlSubmissions(undefined, form_id), config); + dispatch({ + type: GET_ANSWERS, + payload: res.data + }); + } + catch (err) { + dispatch({ + type: ANSWER_ERROR, + payload: err.response.data + }) + } +} + +export const postAnswers = (data, callback) => async dispatch => { + try { + const config = { + headers: { + Authorization: `Bearer ${localStorage.token}`, + } + } + const res = await axios.post(urlFormFeedback(),data, config); + dispatch({ + type: POST_ANSWERS, + payload: res.data + }); + callback() + } + catch (err) { + dispatch({ + type: ANSWER_ERROR, + payload: err.response.data + }) + } +} \ No newline at end of file diff --git a/src/actions/types.js b/src/actions/types.js index 1cef3c1..9ffe793 100644 --- a/src/actions/types.js +++ b/src/actions/types.js @@ -26,3 +26,6 @@ export const QUESTION_ERROR = 'QUESTION_ERROR'; export const GET_ZULIP_STAT = 'GET_ZULIP_STAT'; export const UPDATE_ZULIP_STAT = 'UPDATE_ZULIP_STAT'; export const ZULIP_STAT_ERROR = 'ZULIP_STAT_ERROR'; +export const GET_ANSWERS = 'GET_ANSWERS'; +export const POST_ANSWERS = 'POST_ANSWERS'; +export const ANSWER_ERROR = 'ANSWER_ERROR'; diff --git a/src/components/Preview.js b/src/components/Preview.js index 211b549..b845d21 100644 --- a/src/components/Preview.js +++ b/src/components/Preview.js @@ -3,8 +3,12 @@ import { connect } from 'react-redux' import { getQuestions } from '../actions/question' import PropTypes from 'prop-types' import '../styles/Questions.css' +import moment from 'moment' +import { postAnswers, getAnswers } from '../actions/answer' import { DateInput, TimeInput } from 'semantic-ui-calendar-react'; -import { Form, TextArea, Divider } from 'semantic-ui-react' +import { Form, TextArea, Divider, Button, Select } from 'semantic-ui-react' +import AWS from 'aws-sdk' +require('dotenv').config(); class Preview extends Component { constructor(props) { @@ -12,175 +16,337 @@ class Preview extends Component { this.state = { value: '', date: '', - time: '' + time: '', + answers: [], + error: null, + success: false, + file: '' } } async componentDidMount() { await this.props.getQuestions(this.props.id) + await this.props.getAnswers(this.props.id) + if(this.props.feedback[0]){ + for(var i=0; i { + this.setState({ + answers: this.state.answers.map((answer, index) => { + return id === index ? { + ...answer, + value: e.target.value, + } : answer + }) + }) + } + + handleChange = (e, id, {name, value}) => { + if(this.state.hasOwnProperty(name)) { + this.setState({ + answers: this.state.answers.map((answer, index) => { + return id === index ? { + ...answer, + value: value, + } : answer + }) + }) + } + } + + handleFileChange = (e) => { + this.setState({ + success: false, + url: '', + file: e.target.files[0].name + }) + } + + onSelect = (e, id, {value}) => { + this.setState({ + answers: this.state.answers.map((answer, index) => { + return id === index ? { + ...answer, + value: value + } : answer + }) + }) + } + + onDropdownChange = (e, id) => { + this.setState({ + answers: this.state.answers.map((answer, index) => { + return id === index ? { + ...answer, + value: e.target.value + } : answer + }) + }) } - handleChange = (event, {name, value}) => { - if (this.state.hasOwnProperty(name)) { - this.setState({ [name]: value }); + checkboxChange = (e, id, optionindex) => { + this.setState({ + answers: this.state.answers.map((answer, index) => { + return id === index ? { + ...answer, + value: e.target.checked ? [...this.state.answers[id].value.filter((value, idx) => idx !== optionindex), e.target.value] : [...this.state.answers[id].value.filter( + (value, idx) => idx !== optionindex + )] + } : answer + }) + }) + } + + uploadFile = (file, id) => { + AWS.config.update({ + region: 'ap-south-1', + accessKeyId: process.env.REACT_APP_AWSAccessKeyId, + secretAccessKey: process.env.REACT_APP_AWSSecretKey, + }) + const send_file = file + file = file.split('.') + const params = { + ACL: 'public-read', + Key: file[0], + ContentType: 'application/octet-stream', + Body: send_file, + Bucket: process.env.REACT_APP_Bucket + } + var myBucket = new AWS.S3() + myBucket.putObject(params) + .on('httpUploadProgress', (evt) => { + this.setState({ + answers: this.state.answers.map((answer, index) => { + return id === index ? { + ...answer, + value: `https://${process.env.REACT_APP_Bucket}.s3.amazonaws.com/${file[0]}`, + } : answer + }), + progress: Math.round((evt.loaded / evt.total) * 100), + }) + }) + .send((err) => { + if (err) { + console.log(err) + } + }) + + this.setState({ + answers: this.state.answers.map((answer, index) => { + return id === index ? { + ...answer, + value: this.state.url, + } : answer + }), + url: '' + }) + } + + submit = () => { + const data = { + form: this.props.id, + answers: this.state.answers } + this.props.postAnswers(data, this.callback) + } + + callback = () => { + this.setState({ + error: this.props.answererror?true:false + }) + setTimeout(() => { + this.setState({ + error: null + }) + }, 5000) } render () { const { questions } = this.props - const { value } = this.state + const { answers } = this.state return (
{/* question type view based on each data type */} { questions && questions.length !== 0 ? - questions.map(question => - <> + questions.map((question, index) => +
{ question.data_type === 'char' ? - <> +
- {question.description} - + value={answers[index] ? answers[index].value : ''} + onChange={(event) => this.onChange(event, index)} + /> + {question.description} +
: null } { question.data_type === 'text' ? - <> +
this.onChange(event, index)} /> {question.description} - +
: null } { question.data_type === 'choice' ? - <> +
{question.order + '. ' + question.label} { question.options.map(option => - <> this.onSelect(event, index, value)} /> - ) } {question.description} - +
: null } { question.data_type === 'dropdown' ? - <> +
this.onDropdownChange(event, index)} > { question.options.map(option => - <> + - ) } {question.description} - +
: null } { question.data_type === 'checkbox' ? - <> - {question.order + '. ' + question.label} +
+ {question.order + '. ' + question.label} { - question.options.map(option => - <> - - - ) + question.options.map((option, id) => + this.checkboxChange(event, index, id)} + /> + ) } {question.description} - +
: null } { question.data_type === 'date' ? - <> +
this.handleChange(event, index, name, value)} /> {question.description} - +
: null } { question.data_type === 'time' ? - <> +
this.handleChange(event, index, name, value)} /> {question.description} - +
: null } { question.data_type === 'file' ? - <> +
(this.fileInput = fileInput)} /> + + { + answers[index] ? + File URL + : null + } {question.description} - +
: null } - +
) : null } +
) } } Preview.propTypes = { - questions: PropTypes.array.isRequired + questions: PropTypes.array.isRequired, + feedback: PropTypes.array.isRequired }; const mapStateToProps = state => ({ questions: state.questions.questions, - questionerror: state.questions.questionerror + questionerror: state.questions.questionerror, + answererror: state.answer.answererror, + feedback: state.answer.answers }) export default connect( mapStateToProps, - { getQuestions } + { getQuestions, postAnswers, getAnswers } )(Preview) diff --git a/src/components/Questions.js b/src/components/Questions.js index 9e1ef70..8b515bd 100644 --- a/src/components/Questions.js +++ b/src/components/Questions.js @@ -33,7 +33,7 @@ class Questions extends Component { { label: '', description: '', - order: null, + order: undefined, required: false, data_type: '', options: [], @@ -149,7 +149,7 @@ class Questions extends Component { // function to add a new field createForm = () => { return this.state.newfields.map((object, index) => ( - <> +
- +
)) } @@ -216,7 +216,7 @@ class Questions extends Component { newfields: [...this.state.newfields, { label: '', description: '', - order: null, + order: undefined, required: false, data_type: '', options: [], @@ -291,6 +291,7 @@ class Questions extends Component { {form.description}
+ {/* check for the published status of the form */} { form.published_status === 'unpublished' ? @@ -327,6 +328,7 @@ class Questions extends Component { > Copy Link + {/* domain name of the site + pathname */} @@ -358,7 +360,7 @@ class Questions extends Component { ( questions && questions.length !== 0 ? this.state.fields.map((object, index) => - <> +
Updated {moment(new Date(this.state.fields[index].updated_on), "YYYYMMDD").fromNow()}
- +
) : null ) @@ -480,7 +482,6 @@ class Questions extends Component { Questions.propTypes = { questions: PropTypes.array.isRequired, - form: PropTypes.object.isRequired, userinfo: PropTypes.array.isRequired }; diff --git a/src/components/Upload.js b/src/components/Upload.js deleted file mode 100644 index ff5f2ee..0000000 --- a/src/components/Upload.js +++ /dev/null @@ -1,72 +0,0 @@ -import React, { Component } from 'react' -import AWS from 'aws-sdk' -require('dotenv').config(); - -export default class Upload extends Component { - // This component shows a demo of how the uploading feature will work with file uploads in form responses - constructor(props) { - super(props) - this.state = { - success: false, - url: '', - file: '' - } - } - - handleChange = (e) => { - this.setState({ - success: false, - url: '', - file: e.target.files[0].name - }) - } - - // the function which handles the file upload to the AWS S3 bucket - uploadFile = (file) => { - AWS.config.update({ - region: 'ap-south-1', - accessKeyId: process.env.REACT_APP_AWSAccessKeyId, - secretAccessKey: process.env.REACT_APP_AWSSecretKey, - }) - const send_file = file - file = file.split('.') - const params = { - ACL: 'public-read', - Key: file[0], - ContentType: 'application/octet-stream', - Body: send_file, - Bucket: process.env.REACT_APP_Bucket - } - var myBucket = new AWS.S3() - myBucket.putObject(params) - .on('httpUploadProgress', (evt) => { - this.setState({ - url: `https://${process.env.REACT_APP_Bucket}.s3.amazonaws.com/${file[0]}`, - progress: Math.round((evt.loaded / evt.total) * 100), - }) - }) - .send((err) => { - if (err) { - } - }) - } - -render() { - return ( -
-
-

UPLOAD A FILE

- (this.fileInput = fileInput)} - /> -
- - {this.state.url} -
-
- ); -} - -} diff --git a/src/reducers/answer.js b/src/reducers/answer.js new file mode 100644 index 0000000..4a89390 --- /dev/null +++ b/src/reducers/answer.js @@ -0,0 +1,36 @@ +import { + GET_ANSWERS, + POST_ANSWERS, + ANSWER_ERROR +} from '../actions/types'; + +const initialState = { + answers: [], + postanswers: [], + answererror: null +} + +const answerReducer = (state = initialState, action) => { + switch(action.type) { + case GET_ANSWERS: + return { + ...state, + answers: action.payload, + }; + case POST_ANSWERS: + return { + ...state, + postanswers: action.payload, + answers: action.payload + }; + case ANSWER_ERROR: + return { + ...state, + answererror: action.payload, + } + default: + return state + } +} + +export default answerReducer \ No newline at end of file diff --git a/src/reducers/index.js b/src/reducers/index.js index 444dfac..27ff51f 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -5,6 +5,7 @@ import userReducer from './user' import formReducer from './form' import questionReducer from './question' import zulipReducer from './zulip' +import answerReducer from './answer' const rootReducers = combineReducers ({ login: loginReducer, @@ -12,6 +13,7 @@ const rootReducers = combineReducers ({ user: userReducer, form: formReducer, questions: questionReducer, + answer: answerReducer, zulip: zulipReducer }) diff --git a/src/urls.js b/src/urls.js index f6d14ef..d2d8abf 100644 --- a/src/urls.js +++ b/src/urls.js @@ -24,11 +24,7 @@ export function form(id) { } export function submission() { - return `${urlBaseFrontend()}submissions` -} - -export function upload() { - return `${urlBaseFrontend()}upload` + return `${urlBaseFrontend()}submission` } // backend URLs @@ -78,3 +74,22 @@ export function urlPatchQuestions() { export function urlZulipStats() { return `${urlBaseBackend()}/zulip_stat/` } + +export function urlAnswers() { + return `${urlBaseBackend()}/answers/` +} + +export function urlFormFeedback() { + return `${urlBaseBackend()}/feedback/` +} + +export function urlSubmissions(user_name, form_id) { + if(user_name === undefined && form_id === undefined) + return `${urlBaseBackend()}/feedback/` + else if(user_name === undefined) + return `${urlBaseBackend()}/feedback/?form_id=${form_id}` + else if(form_id === undefined) + return `${urlBaseBackend()}/feedback/?user_name=${user_name}` + else + return `${urlBaseBackend()}/feedback/?user_name=${user_name}&form_id=${form_id}` +}