From d5120a1ba237951ba843edc3cc7bb84b3876f1a4 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Sat, 2 Dec 2017 22:48:45 +0100 Subject: [PATCH] functional application without amazon update --- backend/express.js | 43 ++++--- backend/helpers/amazon.js | 7 ++ web/package.json | 6 +- web/src/App.js | 174 +++++++++++++++++++--------- web/src/components/IntentDetails.js | 48 +++++++- web/src/components/IntentItem.js | 17 +-- web/src/components/IntentList.js | 4 +- web/src/components/LaunchRequest.js | 64 ++++++++++ web/src/config.js | 12 +- web/src/css/Intent.css | 37 +++++- web/src/css/Intent.scss | 40 ++++++- web/src/lib/api.js | 21 ++-- 12 files changed, 375 insertions(+), 98 deletions(-) create mode 100644 backend/helpers/amazon.js create mode 100644 web/src/components/LaunchRequest.js diff --git a/backend/express.js b/backend/express.js index 252e564..e40491a 100644 --- a/backend/express.js +++ b/backend/express.js @@ -1,3 +1,5 @@ +import { sendUpdateToAmazon } from './helpers/amazon'; + var express = require('express'); var alexa = require('alexa-app'); @@ -12,13 +14,22 @@ var ObjectID = require ('mongodb').ObjectID; const router = express.Router (); -router.get ('/intents', async (req, res, next) => { +router.get ('/getSkill/:id', async (req, res, next) => { try { const id = req.params.id; - db.collection ('intent_list').find({}).toArray((err,result)=>{ - if (err) throw err; - res.json(result); + if (id.length !== 24){ + res.json([]); + throw("error"); + } + + db.collection ('skill_list').find({_id: ObjectID(id)}).toArray((err,result)=>{ + if (err){ + throw err; + res.json([]); + }else{ + res.json(result); + } }); } catch (e) { @@ -27,10 +38,11 @@ router.get ('/intents', async (req, res, next) => { } }); -router.get ('/deleteIntent/:id', async (req, res, next) => { + +router.get ('/deleteSkill/:skillID', async (req, res, next) => { try { let id = req.params.id; - let result = db.collection('intent_list').remove({_id: ObjectID(id)},(err,result)=>{ + let result = db.collection('skill_list').remove({_id: ObjectID(id)},(err,result)=>{ if (err) throw err; res.json(result); }); @@ -40,15 +52,20 @@ router.get ('/deleteIntent/:id', async (req, res, next) => { } }); -router.post ('/updateIntent/:id', async (req, res, next) => { +router.post ('/updateSkill/:id', async (req, res, next) => { try { let id = req.params.id; - let intent = req.body; - delete intent._id; - let result = db.collection('intent_list').update({_id: ObjectID(id)}, intent,{upsert:true}, (err, result)=>{ - if (err) throw(err); - res.json(result); - }); + let skill = req.body; + delete skill._id; + if (id !== '-1'){ + let result = db.collection('skill_list').update({_id: ObjectID(id)}, skill,{upsert:true}, (err, result)=>{ + if (err) throw(err); + res.json(result); + }); + }else{ + //no new skills for now + } + } catch (e) { console.log ('error:', e); next (e); diff --git a/backend/helpers/amazon.js b/backend/helpers/amazon.js new file mode 100644 index 0000000..7789579 --- /dev/null +++ b/backend/helpers/amazon.js @@ -0,0 +1,7 @@ + + + +export const sendUpdateToAmazon = (id)=>{ + //id - skill ID in database + +} \ No newline at end of file diff --git a/web/package.json b/web/package.json index fe7f721..0c49f3b 100644 --- a/web/package.json +++ b/web/package.json @@ -6,7 +6,8 @@ "react": "^16.2.0", "react-dom": "^16.2.0", "react-md": "^1.2.8", - "react-scripts": "1.0.17" + "react-scripts": "1.0.17", + "webfontloader": "^1.6.28" }, "scripts": { "build-css": "node-sass-chokidar --include-path ./node_modules src/ -o src/", @@ -20,6 +21,7 @@ }, "devDependencies": { "node-sass": "^4.7.2", - "nodemon": "^1.12.1" + "nodemon": "^1.12.1", + "npm-run-all": "^4.1.2" } } diff --git a/web/src/App.js b/web/src/App.js index 34e7b2b..7761b92 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -2,86 +2,152 @@ import React, { Component } from 'react'; import './css/App.css'; import IntentList from './components/IntentList'; import IntentDetails from './components/IntentDetails'; +import LaunchRequest from './components/LaunchRequest'; -import {getAllIntents, deleteIntent, updateIntent} from './lib/api' +import {getSkill, updateSkill} from './lib/api' class App extends Component { constructor(props){ super(props); - this.state={allIntents:[], selectedIntent: {questions:[''],answer:''}, selectedIndex:-1}; + this.state={_id:'5a22ffd36ce046c749739453', + skillID:'', + invocationName:'Saburly', + invocationAnswer:'We are saburly', + allIntents:[], + selectedIntent: {intentName:'',questions:[''],answer:''}, + selectedIndex:-1, + launchRequest:false}; - getAllIntents().then(l=> l.text()).then(result=>{ - this.setState({allIntents: JSON.parse(result), selectedIntent: this.state.selectedIntent}) + getSkill(this.state._id).then(l=> l.text()).then(result=>{ + let jResult = JSON.parse(result)[0]; + if (jResult===undefined) return; + this.setState({ skillID:jResult.skillID, invocationName: jResult.invocationName, + invocationAnswer: jResult.invocationAnswer, + allIntents: jResult.intents}) }) this.handleIntentClick = this.handleIntentClick.bind(this); + this.handleLaunchRequestClick = this.handleLaunchRequestClick.bind(this); this.handleDeleteIntentClick = this.handleDeleteIntentClick.bind(this); this.handleSaveIntentClick = this.handleSaveIntentClick.bind(this); this.handleAddIntentClick = this.handleAddIntentClick.bind(this); + this.handleSaveLaunchRequestClick = this.handleSaveLaunchRequestClick.bind(this); + this.createSkill = this.createSkill.bind(this); } render() { - return ( -
-
-

Tell All

+ if(this.state.launchRequest){ + return ( +
+
+

Tell All

+
+ + +
- - - - -
- ); - } - - handleIntentClick(selectedIntent, index){ - this.setState({selectedIntent:selectedIntent, selectedIndex: index}); - } - - handleDeleteIntentClick(selectedIntent){ - if (selectedIntent._id){ - deleteIntent(selectedIntent._id).then(l=>l.text()).then(result=>{ - if (JSON.parse(result).n===1){ - let id = this.state.allIntents.indexOf(selectedIntent); - if (id!==-1){ - this.state.allIntents.splice(id,1); - this.setState({allIntents: this.state.allIntents, selectedIntent: {questions:[''],answer:''}}); - }else{ - alert("Error"); - } - }else{ - alert("Error"); - } - }); + ); + }else{ + return ( +
+
+

Tell All

+
+ + + + +
+ ); } } - handleSaveIntentClick(selectedIntent){ - updateIntent(selectedIntent._id, selectedIntent).then(l=>l.text()).then(result=>{ - if (JSON.parse(result).nModified===1){ - this.state.allIntents.map((intent,index)=>{ - if (intent._id === selectedIntent._id){ - let newAllIntents = this.state.allIntents; - newAllIntents[index] = selectedIntent; - this.setState({allIntents: newAllIntents, selectedIntent: selectedIntent}); - return 1; - } + createSkill(intents, name, answer){ + return { + _id: this.state._id, + skillID: this.state.skillID, + intents: intents, + invocationName: (name===undefined) ? this.state.invocationName : name, + invocationAnswer: (answer===undefined)? this.state.invocationAnswer: answer + }; + } + + handleIntentClick(selectedIntent, index){ + this.setState({selectedIntent:selectedIntent, selectedIndex: index, launchRequest:false}); + } + + handleLaunchRequestClick(){ + this.setState({selectedIndex: -2, launchRequest:true}); + } + + handleSaveLaunchRequestClick(name, answer){ + this.setState({invocationName: name, invocationAnswer: answer}); + try{ + updateSkill(this.createSkill(this.state.allIntents,name,answer)).then(l=>l.text()).then(result=>{ + if (JSON.parse(result).nModified!==1) + alert("Error"); + }); + }catch(e){ + alert("exception"); + } + } + + handleDeleteIntentClick(selectedIntent){ + let id = this.state.allIntents.indexOf(selectedIntent); + if (id!==-1){ + try{ + //I don't like this, state in database is different than component state, for some time + //TODO : move database operation in componentWillUpdate or componentDidUpdate + let newAllIntents = this.state.allIntents; + newAllIntents.splice(id,1); + this.setState({allIntents: newAllIntents, selectedIntent: {intentName:'', questions:[''],answer:''}}); + updateSkill(this.createSkill(newAllIntents)).then(l=>l.text()).then(result=>{ + if (JSON.parse(result).nModified!==1) + alert("Error"); }); - }else{ - alert("error - update went wrong"); + }catch(e){ + alert("exception"); } - }); + } + } + + + handleSaveIntentClick(selectedIntent){ + let newAllIntents = this.state.allIntents; + if (this.state.selectedIndex === -1){ + //new intent + newAllIntents.push(selectedIntent); + this.setState({allIntents: newAllIntents, selectedIntent: selectedIntent, selectedIndex: newAllIntents.length-1}); + }else{ + newAllIntents[this.state.selectedIndex] = selectedIntent; + this.setState({allIntents: newAllIntents, selectedIntent: selectedIntent}); + } + try{ + updateSkill(this.createSkill(newAllIntents)).then(l=>l.text()).then(result=>{ + if (JSON.parse(result).nModified!==1) + alert("Error"); + }); + }catch(e){ + alert("exception"); + } } handleAddIntentClick(){ - this.setState({allIntents: this.state.allIntents, selectedIntent: {questions:[''], answer:''}}); + this.setState({allIntents: this.state.allIntents, selectedIndex: -1,launchRequest:false,selectedIntent: {intentName:'',questions:[''], answer:''}}); } } diff --git a/web/src/components/IntentDetails.js b/web/src/components/IntentDetails.js index 52af133..9eb0b0d 100644 --- a/web/src/components/IntentDetails.js +++ b/web/src/components/IntentDetails.js @@ -1,6 +1,7 @@ import React, { Component } from 'react'; import {Button, SVGIcon, TextField} from 'react-md'; import '../css/Intent.css' +import {QUESTION_MAX_LENGTH, ANSWER_MAX_LENGTH, INTENT_NAME_MAX_LENGTH} from '../config'; class IntentDetails extends Component { constructor(props){ @@ -10,6 +11,9 @@ class IntentDetails extends Component { this.addQuestion = this.addQuestion.bind(this); this.deleteQuestion = this.deleteQuestion.bind(this); + this.handleQuestionEdit = this.handleQuestionEdit.bind(this); + this.handleAnswerEdit = this.handleAnswerEdit.bind(this); + this.handleIntentNameEdit = this.handleIntentNameEdit.bind(this); } componentWillReceiveProps(props){ @@ -19,18 +23,32 @@ class IntentDetails extends Component { render() { return (
+
+ +
Questions
{ this.state.intent.questions.map((question, index)=>{ return (
{this.deleteQuestion(question)}}> } + onChange={(e)=>{this.handleQuestionEdit(e,index)}} value={question}/>
); @@ -40,12 +58,14 @@ class IntentDetails extends Component { {
} @@ -60,7 +80,7 @@ class IntentDetails extends Component { addQuestion(){ let newIntent = this.state.intent; - newIntent.questions.push('New question'); + newIntent.questions.push(''); this.setState({intent: newIntent}); } @@ -72,6 +92,28 @@ class IntentDetails extends Component { this.setState({intent: newIntent}); } + + handleQuestionEdit(e,index){ + if (e.length === QUESTION_MAX_LENGTH) return; + let newIntent = this.state.intent; + newIntent.questions[index] = e; + this.setState({intent: newIntent}); + } + + handleAnswerEdit(e){ + if (e.length === ANSWER_MAX_LENGTH) return; + let newIntent = this.state.intent; + newIntent.answer = e; + this.setState({intent: newIntent}); + } + + handleIntentNameEdit(e){ + if (e.length === INTENT_NAME_MAX_LENGTH) return; + //e.replace(/\s/g,''); + let newIntent = this.state.intent; + newIntent.intentName = e; + this.setState({intent: newIntent}); + } } export default IntentDetails; diff --git a/web/src/components/IntentItem.js b/web/src/components/IntentItem.js index 45f5185..a4fb43b 100644 --- a/web/src/components/IntentItem.js +++ b/web/src/components/IntentItem.js @@ -1,6 +1,7 @@ import React, { Component } from 'react'; import {Button} from 'react-md'; import '../css/Intent.css' +import {INTENT_TITLE_MAX_LENGTH, INTENT_TITLE_TOOLTIP_DELAY} from '../config' class IntentItem extends Component { @@ -11,18 +12,18 @@ class IntentItem extends Component { } render() { - /* -
{this.state.onClick(this.state.intent,this.state.index)}}> - -

{this.state.index+1}. {this.state.intent.questions[0]}

-
- */ + let buttonTitle = this.state.intent.intentName; + if (buttonTitle.length > INTENT_TITLE_MAX_LENGTH){ + buttonTitle = this.state.intent.intentName.substr(0,INTENT_TITLE_MAX_LENGTH-1) + '. . .'; + } return (


diff --git a/web/src/components/IntentList.js b/web/src/components/IntentList.js index 264c2f3..b97f990 100644 --- a/web/src/components/IntentList.js +++ b/web/src/components/IntentList.js @@ -17,6 +17,8 @@ class IntentList extends Component { render() { return (
+

Intents

@@ -30,7 +32,7 @@ class IntentList extends Component { }) }

- +
); } diff --git a/web/src/components/LaunchRequest.js b/web/src/components/LaunchRequest.js new file mode 100644 index 0000000..28d5099 --- /dev/null +++ b/web/src/components/LaunchRequest.js @@ -0,0 +1,64 @@ +import React, { Component } from 'react'; +import {Button, TextField} from 'react-md'; +import '../css/Intent.css' +import { INVOCATION_NAME_MAX_LENGTH, INVOCATION_ANSWER_MAX_LENGTH } from '../config'; + +class LaunchRequest extends Component { + constructor(props){ + super(props); + + this.state = {invocationName: props.invocationName, invocationAnswer: props.invocationAnswer}; + + this.handleNameEdit = this.handleNameEdit.bind(this); + this.handleAnswerEdit = this.handleAnswerEdit.bind(this); + } + + componentWillReceiveProps(props){ + this.setState({invocationName: props.invocationName, invocationAnswer: props.invocationAnswer}); + } + + render() { + return ( +
+
Invocation name customers use to activate the skill. For example "Open Saburly" or "Talk to Saburly"
+ +

+
Answer customers get from Alexa when they activate the skill.
+ +

+

+ +
+ ); + } + + handleNameEdit(e){ + if (e.length === INVOCATION_NAME_MAX_LENGTH) return; + this.setState({invocationName: e}); + } + + handleAnswerEdit(e){ + if (e.length === INVOCATION_ANSWER_MAX_LENGTH) return; + this.setState({invocationAnswer: e}); + } +} + +export default LaunchRequest; diff --git a/web/src/config.js b/web/src/config.js index 0fefd14..1a4de76 100644 --- a/web/src/config.js +++ b/web/src/config.js @@ -1 +1,11 @@ -export const BASE_URL = 'localhost:5000'; \ No newline at end of file +export const BASE_URL = 'localhost:5000'; + +export const INTENT_NAME_MAX_LENGTH = 30; +export const QUESTION_MAX_LENGTH = 150; +export const ANSWER_MAX_LENGTH = 150; + +export const INTENT_TITLE_MAX_LENGTH = 20; +export const INTENT_TITLE_TOOLTIP_DELAY = 700; + +export const INVOCATION_NAME_MAX_LENGTH = 15; +export const INVOCATION_ANSWER_MAX_LENGTH = 100; diff --git a/web/src/css/Intent.css b/web/src/css/Intent.css index 490da92..62b35e2 100644 --- a/web/src/css/Intent.css +++ b/web/src/css/Intent.css @@ -4,13 +4,26 @@ min-height: 9999px; height: 9999px; float: left; - background-color: #d5dae2; } + background-color: #eff0f0; } .IntentList-title { font-size: 1.5em; height: 70px; padding: 20px; - text-align: left; } + text-align: left; + background-color: #eff0f0; } + +.LaunchRequest { + text-align: left; + width: 100%; + height: 50px; + background-color: #d8d8d8; } + +.LaunchRequest-selected { + text-align: left; + width: 100%; + height: 50px; + background-color: #f5f5f5; } .AddIntent { float: right; @@ -22,7 +35,7 @@ height: 50px; width: 100%; text-align: left; - background-color: #adb0b5; } + background-color: #d8d8d8; } .IntentItem-selected { margin-top: 2px; @@ -30,7 +43,7 @@ height: 50px; width: 100%; text-align: left; - background-color: #e8ecf2; } + background-color: #f5f5f5; } /*IntentDetails CSS*/ .IntentDetails { @@ -38,7 +51,21 @@ width: 70%; min-height: 9999px; height: 9999px; - background-color: #e8ecf2; } + background-color: #f5f5f5; } .QuestionBox { margin: 25px; } + +/*LaunchRequest CSS*/ +.LaunchRequestBox { + float: left; + width: 70%; + min-height: 9999px; + height: 9999px; + background-color: #f5f5f5; } + +.ExplanationText { + float: left; + margin-top: 30px; + margin-left: 20px; + text-align: left; } diff --git a/web/src/css/Intent.scss b/web/src/css/Intent.scss index c954fe7..b35497e 100644 --- a/web/src/css/Intent.scss +++ b/web/src/css/Intent.scss @@ -5,7 +5,7 @@ min-height:9999px; height:9999px; float:left; - background-color: #d5dae2; + background-color: #eff0f0; } .IntentList-title{ @@ -13,6 +13,21 @@ height: 70px; padding: 20px; text-align:left; + background-color: #eff0f0; +} + +.LaunchRequest{ + text-align: left; + width: 100%; + height: 50px; + background-color: #d8d8d8 +} + +.LaunchRequest-selected{ + text-align: left; + width: 100%; + height: 50px; + background-color: #f5f5f5; } .AddIntent{ @@ -26,7 +41,7 @@ height: 50px; width:100%; text-align:left; - background-color: #adb0b5; + background-color: #d8d8d8; } .IntentItem-selected{ @@ -35,7 +50,7 @@ height: 50px; width:100%; text-align:left; - background-color:#e8ecf2; + background-color: #f5f5f5; } @@ -46,9 +61,26 @@ width: 70%; min-height:9999px; height:9999px; - background-color:#e8ecf2; + background-color: #f5f5f5; } .QuestionBox{ margin:25px; +} + +/*LaunchRequest CSS*/ + +.LaunchRequestBox{ + float: left; + width:70%; + min-height: 9999px; + height: 9999px; + background-color: #f5f5f5; +} + +.ExplanationText{ + float: left; + margin-top: 30px; + margin-left: 20px; + text-align: left; } \ No newline at end of file diff --git a/web/src/lib/api.js b/web/src/lib/api.js index 6891bef..06964bb 100644 --- a/web/src/lib/api.js +++ b/web/src/lib/api.js @@ -1,24 +1,31 @@ import fetch from 'isomorphic-fetch'; import {BASE_URL} from '../config'; -export const getAllIntents = ()=>{ - let url = `http://${BASE_URL}/intents` +export const getAllIntents = (id)=>{ + let url = `http://${BASE_URL}/intents/${id}` return fetch(url, {method: 'GET'}); } -export const deleteIntent = id=>{ - let url = `http://${BASE_URL}/deleteIntent/${id}` +export const getSkill = (id)=>{ + let url = `http://${BASE_URL}/getSkill/${id}` return fetch(url, {method: 'GET'}); } -export const updateIntent = (id,intent)=>{ - let url = `http://${BASE_URL}/updateIntent/${id}` +export const deleteSkill = (id)=>{ + let url = `http://${BASE_URL}/deleteSkill/${id}` + return fetch(url, {method: 'GET'}); +} + +export const updateSkill = (skill)=>{ + console.log(skill); + let id = (skill._id) ? skill._id : -1; + let url = `http://${BASE_URL}/updateSkill/${id}` return fetch(url, { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, - body: JSON.stringify(intent), + body: JSON.stringify(skill), }); } \ No newline at end of file