diff --git a/packages/functionals/botpress-qna/src/index.js b/packages/functionals/botpress-qna/src/index.js index b2d2badc6a7..7151717f65b 100755 --- a/packages/functionals/botpress-qna/src/index.js +++ b/packages/functionals/botpress-qna/src/index.js @@ -48,6 +48,7 @@ module.exports = { const questionsToSave = typeof questions === 'string' ? parsers[`${format}Parse`](questions) : questions return Promise.each(questionsToSave, question => storage.saveQuestion({ ...question, enabled: true })) }, + /** * @async * Fetches questions and represents them as json @@ -56,15 +57,32 @@ module.exports = { * @returns {Array.<{questions: Array, question: String, action: String, answer: String}>} */ async export({ flat = false } = {}) { - return (await storage.getQuestions()).flatMap( - ({ data: { questions, answer: textAnswer, action, redirectFlow, redirectNode } }) => { - const answer = action === 'text' ? textAnswer : [redirectFlow, redirectNode].filter(Boolean).join('#') - if (!flat) { - return { questions, action, answer } + const qnas = await storage.getQuestions() + + return qnas.flatMap(question => { + const { data } = question + const { questions, answer: textAnswer, action, redirectNode, redirectFlow } = data + + let answer = textAnswer + let answer2 = null + + if (action === 'redirect') { + answer = redirectFlow + if (redirectNode) { + answer += '#' + redirectNode } - return questions.map(question => ({ question, action, answer })) + } else if (action === 'text_redirect') { + answer2 = redirectFlow + if (redirectNode) { + answer2 += '#' + redirectNode + } + } + + if (!flat) { + return { questions, action, answer, answer2 } } - ) + return questions.map(question => ({ question, action, answer, answer2 })) + }) } } @@ -112,7 +130,7 @@ module.exports = { router.get('/csv', async (req, res) => { res.setHeader('Content-Type', 'text/csv') res.setHeader('Content-disposition', `attachment; filename=qna_${moment().format('DD-MM-YYYY')}.csv`) - const json2csvParser = new Json2csvParser({ fields: ['question', 'action', 'answer'], header: false }) + const json2csvParser = new Json2csvParser({ fields: ['question', 'action', 'answer', 'answer2'], header: true }) res.end(json2csvParser.parse(await bp.qna.export({ flat: true }))) }) diff --git a/packages/functionals/botpress-qna/src/middleware.js b/packages/functionals/botpress-qna/src/middleware.js index 040bbf0795f..f2077e5eb42 100644 --- a/packages/functionals/botpress-qna/src/middleware.js +++ b/packages/functionals/botpress-qna/src/middleware.js @@ -17,12 +17,17 @@ export const processEvent = async (event, { bp, storage, logger, config }) => { return false } - if (data.action === 'text') { + if (data.action.includes('text')) { logger.debug('QnA: replying to recognized question with plain text answer', id) event.reply(config.textRenderer, { text: data.answer }) // return `true` to prevent further middlewares from capturing the message - return true - } else if (data.action === 'redirect') { + + if (data.action === 'text') { + return true + } + } + + if (data.action.includes('redirect')) { logger.debug('QnA: replying to recognized question with redirect', id) // TODO: This is used as the `stateId` by the bot template // Not sure if it's universal enough for every use-case but diff --git a/packages/functionals/botpress-qna/src/parsers.js b/packages/functionals/botpress-qna/src/parsers.js index 33f165c4f49..de3efadd4c4 100644 --- a/packages/functionals/botpress-qna/src/parsers.js +++ b/packages/functionals/botpress-qna/src/parsers.js @@ -1,3 +1,4 @@ +import get from 'lodash/get' import parseCsvToJson from 'csv-parse/lib/sync' const parseFlow = str => { @@ -6,22 +7,46 @@ const parseFlow = str => { } export const jsonParse = jsonContent => - jsonContent.map(({ questions, answer: instruction, action }) => { - if (!['text', 'redirect'].includes(action)) { - throw new Error('Failed to process CSV-row: action should be either "text" or "redirect"') + jsonContent.map(({ questions, answer: instruction, answer2, action }) => { + if (!['text', 'redirect', 'text_redirect'].includes(action)) { + throw new Error('Failed to process CSV-row: action should be either "text", "redirect" or "text_redirect"') } - const answer = action === 'text' ? instruction : '' - const flowParams = action === 'redirect' ? parseFlow(instruction) : { redirectFlow: '', redirectNode: '' } - return { questions, action, answer, ...flowParams } + + let redirectInstruction = null + let textAnswer = '' + + if (action === 'text') { + textAnswer = instruction + } else if (action === 'redirect') { + redirectInstruction = instruction + } else if (action === 'text_redirect') { + textAnswer = instruction + redirectInstruction = answer2 + } + + const flowParams = redirectInstruction ? parseFlow(redirectInstruction) : { redirectFlow: '', redirectNode: '' } + return { questions, action, answer: textAnswer, ...flowParams } }) export const csvParse = csvContent => { - const mergeRows = (acc, { question, answer, action }) => { + const mergeRows = (acc, { question, answer, answer2, action }) => { const [prevRow] = acc.slice(-1) - if (prevRow && prevRow.answer === answer) { + const isSameAnswer = prevRow && (prevRow.answer === answer && (!answer2 || answer2 === prevRow.answer2)) + if (isSameAnswer) { return [...acc.slice(0, acc.length - 1), { ...prevRow, questions: [...prevRow.questions, question] }] } - return [...acc, { answer, action, questions: [question] }] + return [...acc, { answer, answer2, action, questions: [question] }] } - return jsonParse(parseCsvToJson(csvContent, { columns: ['question', 'action', 'answer'] }).reduce(mergeRows, [])) + + const rows = parseCsvToJson(csvContent, { columns: ['question', 'action', 'answer', 'answer2'] }).reduce( + mergeRows, + [] + ) + + // We trim the header if detected in the first row + if (get(rows, '0.action') === 'action') { + rows.splice(0, 1) + } + + return jsonParse(rows) } diff --git a/packages/functionals/botpress-qna/src/views/index.js b/packages/functionals/botpress-qna/src/views/index.js index 764680fb990..38f99fe5807 100755 --- a/packages/functionals/botpress-qna/src/views/index.js +++ b/packages/functionals/botpress-qna/src/views/index.js @@ -23,6 +23,7 @@ import Select from 'react-select' import classnames from 'classnames' import find from 'lodash/find' import some from 'lodash/some' +import get from 'lodash/get' import ArrayEditor from './ArrayEditor' import QuestionsEditor from './QuestionsEditor' @@ -50,7 +51,8 @@ const cleanupQuestions = questions => questions.map(q => q.trim()).filter(Boolea const ACTIONS = { TEXT: 'text', - REDIRECT: 'redirect' + REDIRECT: 'redirect', + TEXT_REDIRECT: 'text_redirect' } export default class QnaAdmin extends Component { @@ -163,6 +165,32 @@ export default class QnaAdmin extends Component { !!cleanupQuestions(data.questions).length && (data.action === ACTIONS.TEXT ? !!data.answer : !!data.redirectFlow && !!data.redirectNode) + renderTextAndRedirectSelect(index, onChange) { + return ( +
+ {this.renderTextInput(index, onChange)} + {this.renderRedirectSelect(index, onChange)} +
+ ) + } + + renderTextInput = (index, onChange) => { + const item = index === null ? 'newItem' : `items.${index}` + const answer = get(this.state, `${item}.data.answer`, '') + + return ( + + Answer: + + + ) + } + renderRedirectSelect(index, onChange) { const { flows } = this.state if (!flows) { @@ -299,21 +327,20 @@ export default class QnaAdmin extends Component { > redirect to flow node + + text answer and redirect to flow node + - {data.action === ACTIONS.TEXT && ( - - Answer: - - - )} - + {data.action === ACTIONS.TEXT && this.renderTextInput(index, onChange)} {data.action === ACTIONS.REDIRECT && this.renderRedirectSelect(index, onChange)} + {data.action === ACTIONS.TEXT_REDIRECT && this.renderTextAndRedirectSelect(index, onChange)}