Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions assets/css/_forms.scss
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,19 @@ label {
}
}

.import-sample-loading {
width: 100%;
height: 16rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-evenly;

&:after {
@extend .blue-text, .lighten-1;
}
}

.audio-section {
position: relative;
padding-left: 10px;
Expand Down
29 changes: 29 additions & 0 deletions assets/js/actions/respondentGroups.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ export const CLEAR_INVALID_RESPONDENTS_FOR_GROUP =
export const CLEAR_INVALIDS = "RESPONDENT_GROUP_CLEAR_INVALIDS"
export const SELECT_CHANNELS = "RESPONDENT_GROUP_SELECT_CHANNELS"
export const UPLOAD_RESPONDENT_GROUP = "RESPONDENT_GROUP_UPLOAD"
export const IMPORT_RESPONDENTS = "RESPONDENT_GROUP_IMPORT"
export const INVALID_IMPORT = "RESPONDENT_GROUP_INVALID_IMPORT"
export const CLEAR_INVALID_IMPORT = "RESPONDENT_GROUP_CLEAR_INVALID_IMPORT"
export const UPLOAD_EXISTING_RESPONDENT_GROUP_ID = "RESPONDENT_GROUP_UPLOAD_EXISTING"
export const DONE_UPLOAD_EXISTING_RESPONDENT_GROUP_ID = "RESPONDENT_GROUP_DONE_UPLOAD_EXISTING"

Expand All @@ -38,6 +41,15 @@ export const receiveRespondentGroup = (respondentGroup) => ({
respondentGroup,
})

export const importInvalids = (importError) => ({
type: INVALID_IMPORT,
importError
})

export const clearInvalidImport = () => ({
type: CLEAR_INVALID_IMPORT,
})

export const receiveInvalids = (invalidRespondents) => ({
type: INVALID_RESPONDENTS,
invalidRespondents: invalidRespondents,
Expand All @@ -62,6 +74,18 @@ export const uploadRespondentGroup = (projectId, surveyId, files) => (dispatch,
handleRespondentGroupUpload(dispatch, api.uploadRespondentGroup(projectId, surveyId, files))
}

export const importUnusedSampleFromSurvey = (projectId, surveyId, sourceSurveyId) => (dispatch, getState) => {
dispatch(importingUnusedSample(sourceSurveyId))
api.importUnusedSampleFromSurvey(projectId, surveyId, sourceSurveyId).then((response) => {
const group = response.entities.respondentGroups[response.result]
dispatch(receiveRespondentGroup(group))
}, (e) => {
e.json().then((error) => {
dispatch(importInvalids(error))
})
})
}

export const addMoreRespondentsToGroup =
(projectId, surveyId, groupId, file) => (dispatch, getState) => {
dispatch(uploadingExistingRespondentGroup(groupId))
Expand Down Expand Up @@ -115,6 +139,11 @@ export const uploadingRespondentGroup = () => ({
type: UPLOAD_RESPONDENT_GROUP,
})

export const importingUnusedSample = (sourceSurveyId) => ({
type: IMPORT_RESPONDENTS,
sourceSurveyId
})

export const uploadingExistingRespondentGroup = (id) => ({
type: UPLOAD_EXISTING_RESPONDENT_GROUP_ID,
id,
Expand Down
18 changes: 18 additions & 0 deletions assets/js/actions/surveys.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import * as api from "../api"

export const RECEIVE = "RECEIVE_SURVEYS"
export const RECEIVE_UNUSED_SAMPLE_SURVEYS = "RECEIVE_UNUSED_SAMPLE_SURVEYS"
export const FETCHING_UNUSED_SAMPLE_SURVEYS = "FETCHING_UNUSED_SAMPLE_SURVEYS"
export const FETCH = "FETCH_SURVEYS"
export const NEXT_PAGE = "SURVEYS_NEXT_PAGE"
export const PREVIOUS_PAGE = "SURVEYS_PREVIOUS_PAGE"
Expand Down Expand Up @@ -31,6 +33,22 @@ export const fetchSurveys =
.then(() => getState().surveys.items)
}

export const fetchUnusedSample = (projectId: number) => (dispatch: Function, getState: () => Store) => {
dispatch(fetchingUnusedSampleSurveys())
return api.fetchUnusedSampleSurveys(projectId).then((surveys) => {
dispatch(receiveUnusedSampleSurveys(surveys))
})
}

export const fetchingUnusedSampleSurveys = () => ({
type: FETCHING_UNUSED_SAMPLE_SURVEYS
})

export const receiveUnusedSampleSurveys = (surveys: [UnusedSampleSurvey]) => ({
type: RECEIVE_UNUSED_SAMPLE_SURVEYS,
surveys
})

export const startFetchingSurveys = (projectId: number) => ({
type: FETCH,
projectId,
Expand Down
8 changes: 8 additions & 0 deletions assets/js/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,14 @@ export const uploadRespondentGroup = (projectId, surveyId, files) => {
)
}

export const fetchUnusedSampleSurveys = (projectId) => {
return apiFetchJSON(`projects/${projectId}/unused_sample`, null)
}

export const importUnusedSampleFromSurvey = (projectId, surveyId, sourceSurveyId) => {
return apiPostJSON(`projects/${projectId}/surveys/${surveyId}/respondent_groups/import_unused`, respondentGroupSchema, { sourceSurveyId })
}

export const addMoreRespondentsToGroup = (projectId, surveyId, groupId, file) => {
return apiPostFile(
`projects/${projectId}/surveys/${surveyId}/respondent_groups/${groupId}/add`,
Expand Down
97 changes: 97 additions & 0 deletions assets/js/components/surveys/ImportSampleModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React, { Component, PropTypes } from "react"
import { CardTable, Modal } from "../ui"
import { translate } from "react-i18next"
import { FormattedDate } from "react-intl"
import { Preloader } from "react-materialize"
import values from "lodash/values"

class ImportSampleModal extends Component {
static propTypes = {
t: PropTypes.func,
unusedSample: PropTypes.array,
onConfirm: PropTypes.func.isRequired,
modalId: PropTypes.string.isRequired,
style: PropTypes.object,
}

onSubmit(event, selectedSurveyId) {
event.preventDefault()
let { onConfirm, modalId } = this.props
$(`#${modalId}`).modal("close")
onConfirm(selectedSurveyId)
}

render() {
const { unusedSample, modalId, style, t } = this.props

let surveys = values(unusedSample || {})

let loadingDiv = <div className="import-sample-loading">
<div className="preloader-wrapper active center">
<Preloader />
</div>
</div>

let surveysTable = <CardTable>
<colgroup>
<col width="40%" />
<col width="20%" />
<col width="30%" />
<col width="10%" />
</colgroup>
<thead>
<tr>
<th>{t("Name")}</th>
<th>{t("Unused respondents")}</th>
<th>{t("Ended at")}</th>
<th />
</tr>
</thead>
<tbody>
{surveys.map((survey) => {
let name = survey.name ? `${survey.name} (#${survey.survey_id})` : <em>Untitled Survey #{survey.survey_id}</em>
const canBeImported = survey.respondents > 0
const importButton = canBeImported ?
(<a href="#" onClick={(e) => this.onSubmit(e, survey.survey_id)} className="blue-text btn-flat">
{t("Import")}
</a>) : (<a href="#" onClick={(e) => e.preventDefault()} className="btn-flat disabled">
{t("Import")}
</a>)
return <tr key={survey.survey_id}>
<td>{name}</td>
<td>{survey.respondents}</td>
<td>
<FormattedDate
value={Date.parse(survey.ended_at)}
day="numeric"
month="short"
year="numeric"
/>
</td>
<td>{importButton}</td>
</tr>})}
</tbody>
</CardTable>

return (
<div>
<Modal card id={modalId} style={style}>
<div className="modal-content">
<div className="card-title header">
<h5>{t("Import unused respondents")}</h5>
<p>{t("You can import the respondents that haven't been contacted in finished surveys of the project")}</p>
</div>
</div>
{ unusedSample ? surveysTable : loadingDiv }
<div className="card-action">
<a href="#!" className="modal-action modal-close grey-text btn-link">
{t("Cancel")}
</a>
</div>
</Modal>
</div>
)
}
}

export default translate()(ImportSampleModal)
4 changes: 3 additions & 1 deletion assets/js/components/surveys/RespondentsDropzone.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import React, { PropTypes } from "react"
import Dropzone from "react-dropzone"
import { Preloader } from "react-materialize"

export const RespondentsDropzone = ({ survey, uploading, onDrop, onDropRejected }) => {
export const RespondentsDropzone = ({ survey, uploading, importing, onDrop, onDropRejected }) => {
let className = "drop-text csv"
if (uploading) className += " uploading"
if (importing) className += " importing"

let icon = null
if (uploading) {
Expand Down Expand Up @@ -53,6 +54,7 @@ export const dropzoneProps = () => {
RespondentsDropzone.propTypes = {
survey: PropTypes.object,
uploading: PropTypes.bool,
importing: PropTypes.bool,
onDrop: PropTypes.func.isRequired,
onDropRejected: PropTypes.func.isRequired,
}
12 changes: 12 additions & 0 deletions assets/js/components/surveys/SurveyEdit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class SurveyEdit extends Component {
t: PropTypes.func,
dispatch: PropTypes.func,
projectId: PropTypes.any.isRequired,
unusedSample: PropTypes.array,
surveyId: PropTypes.any.isRequired,
router: PropTypes.object.isRequired,
survey: PropTypes.object.isRequired,
Expand All @@ -23,9 +24,11 @@ class SurveyEdit extends Component {
project: PropTypes.object,
respondentGroups: PropTypes.object,
respondentGroupsUploading: PropTypes.bool,
respondentGroupsImporting: PropTypes.bool,
respondentGroupsUploadingExisting: PropTypes.object,
invalidRespondents: PropTypes.object,
invalidGroup: PropTypes.bool,
invalidImport: PropTypes.object,
}

componentWillMount() {
Expand Down Expand Up @@ -55,14 +58,17 @@ class SurveyEdit extends Component {
survey,
projectId,
project,
unusedSample,
questionnaires,
dispatch,
channels,
respondentGroups,
respondentGroupsUploading,
respondentGroupsImporting,
respondentGroupsUploadingExisting,
invalidRespondents,
invalidGroup,
invalidImport,
t,
} = this.props
const activeQuestionnaires = Object.keys(questionnaires)
Expand Down Expand Up @@ -90,10 +96,13 @@ class SurveyEdit extends Component {
survey={survey}
respondentGroups={respondentGroups}
respondentGroupsUploading={respondentGroupsUploading}
respondentGroupsImporting={respondentGroupsImporting}
respondentGroupsUploadingExisting={respondentGroupsUploadingExisting}
invalidRespondents={invalidRespondents}
invalidGroup={invalidGroup}
invalidImport={invalidImport}
projectId={projectId}
unusedSample={unusedSample}
questionnaires={activeQuestionnaires}
channels={channels}
dispatch={dispatch}
Expand All @@ -108,14 +117,17 @@ class SurveyEdit extends Component {
const mapStateToProps = (state, ownProps) => ({
projectId: ownProps.params.projectId,
project: state.project.data,
unusedSample: state.unusedSample,
surveyId: ownProps.params.surveyId,
channels: state.channels.items,
questionnaires: state.questionnaires.items || {},
respondentGroups: state.respondentGroups.items || {},
respondentGroupsUploading: state.respondentGroups.uploading,
respondentGroupsImporting: state.respondentGroups.importing,
respondentGroupsUploadingExisting: state.respondentGroups.uploadingExisting,
invalidRespondents: state.respondentGroups.invalidRespondents,
invalidGroup: state.respondentGroups.invalidRespondentsForGroup,
invalidImport: state.respondentGroups.invalidImport,
survey: state.survey.data || {},
})

Expand Down
9 changes: 9 additions & 0 deletions assets/js/components/surveys/SurveyForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,19 @@ class SurveyForm extends Component {
t: PropTypes.func,
dispatch: PropTypes.func,
projectId: PropTypes.any.isRequired,
unusedSample: PropTypes.array,
survey: PropTypes.object.isRequired,
surveyId: PropTypes.any.isRequired,
router: PropTypes.object.isRequired,
questionnaires: PropTypes.object,
questionnaire: PropTypes.object,
respondentGroups: PropTypes.object,
respondentGroupsUploading: PropTypes.bool,
respondentGroupsImporting: PropTypes.bool,
respondentGroupsUploadingExisting: PropTypes.object,
invalidRespondents: PropTypes.object,
invalidGroup: PropTypes.bool,
invalidImport: PropTypes.object,
channels: PropTypes.object,
errors: PropTypes.object,
readOnly: PropTypes.bool.isRequired,
Expand Down Expand Up @@ -89,13 +92,16 @@ class SurveyForm extends Component {
const {
survey,
projectId,
unusedSample,
questionnaires,
channels,
respondentGroups,
respondentGroupsUploading,
respondentGroupsImporting,
respondentGroupsUploadingExisting,
invalidRespondents,
invalidGroup,
invalidImport,
errors,
questionnaire,
readOnly,
Expand Down Expand Up @@ -308,12 +314,15 @@ class SurveyForm extends Component {
<SurveyWizardRespondentsStep
projectId={projectId}
survey={survey}
unusedSample={unusedSample}
channels={channels}
respondentGroups={respondentGroups}
respondentGroupsUploading={respondentGroupsUploading}
respondentGroupsImporting={respondentGroupsImporting}
respondentGroupsUploadingExisting={respondentGroupsUploadingExisting}
invalidRespondents={invalidRespondents}
invalidGroup={invalidGroup}
invalidImport={invalidImport}
readOnly={readOnly}
surveyStarted={surveyStarted}
/>
Expand Down
Loading