2 Commits

Author SHA1 Message Date
Bilal
118ab329d9 Revert "Bilal step3" 2017-12-03 18:49:50 +01:00
Bilal
c179e5e876 Merge pull request #1 from GotPPay/bilal-step3
Bilal step3
2017-12-03 18:48:42 +01:00
69 changed files with 712 additions and 22805 deletions

104
README.md
View File

@@ -1,104 +0,0 @@
To obtain client ID and secret :
https://developer.amazon.com/lwa/sp/overview.html
Click create new security profile, and add whitelist redirect uri : https://layla.amazon.com/api/skill/link/M2ODJY6EXOY6KO
To obtain new Auth Code :
https://www.amazon.com/ap/oa?client_id=amzn1.application-oa2-client.8c183daec15c488c9126b62bda9f7832&scope=alexa::ask:skills:readwrite alexa::ask:models:readwrite alexa::ask:skills:test&response_type=code&redirect_uri=https://layla.amazon.com/api/skill/link/M2ODJY6EXOY6KO
Response URL (Decoded) :
https://layla.amazon.com/api/skill/link/M2ODJY6EXOY6KO?code=ANCgZUfEFdlRRkpSNFuA&scope=alexa::ask:skills:readwrite alexa::ask:models:readwrite alexa::ask:skills:test
Code : ANCgZUfEFdlRRkpSNFuA
=======================================
Now to get Access Token :
Send a POST request to https://api.amazon.com/auth/o2/token with the following parameters
- HTTP Header Parameters
Content-Type: application/x-www-form-urlencoded
- HTTP Body Parameters
grant_type: authorization_code
code: The authorization code that was returned in the response.
client_id: The products client ID. To access this information, navigate to Amazons Developer Console. After youve logged in, click Get Started > under Alexa Voice Service, then click Edit next to a registered product (or create a new one). From the left navigation select Security Profile. This page contains both Client ID and Client Secret for your product.
client_secret: The products client secret. To access this information, navigate to Amazons Developer Console. After youve logged in, click Get Started > under Alexa Voice Service, then click Edit next to a registered product (or create a new one). From the left navigation select Security Profile. This page contains both Client ID and Client Secret for your product.
redirect_uri: One of the return URIs that you added to your apps security profile when signing up.
Response :
{
"access_token": "Atza|IwEBIBe6gDqrrowEEav6N-_6s4NztYeP3oG8PGWmu8ZiZw6lbOh3wNla3TK6pY-VEpT1d8an-dVf_n3kXJzVFsNo_4xBfZyFHGoCTDTFjs3yBRul4PVdBOhwwiH3-sgRLcUofZbe2oE06GmTcbfYtaStfXpQI5dfpldfnsJg_CvhSA6AHb_snJT3F6lyXzbV076d_3cYUMJxFldJGnYcviNHHxjjmuQTD06hhGzCbAxxe9eBmkuopRsNfyedLT2UlKP_ublah9CUGA3AdIX_3Iuke82jMwGnNl9gv7pbaDNEjAbj7IQSl3B08uuREtJq-oTBOjALNXRvFxTJmQjZwXNf9eHC7fSHJDdEPdZQU0AcffRQObAyAkUuL6Jv39OHzhb3Q64-zzoyODqnJyLP5SQZ2JVF53Kc_cTBqjIc9pXljqe7yEVk6JDs7q1zKbBibx_AQm57TO79IzWyLBzBMlYL5HdTsqEfRzLeDw2tws-hGMgkx2HWfdbYnmf5Qb4SyIhzvmmdfPLg3MVKTxjIBu1rx0xf3n0PLZP1EO6jsJPoMRPg77Gm4oit5Zp6s37ek3A3Vxh-ntoASpkrkxGTG9kVtRNt",
"refresh_token": "Atzr|IwEBICA3kDhfSJxlwvnQp9AD1o115AC_KBbFd5GBg8oN_QHWn2or5xFQ09BruuK6a07tGHtTt_9q2Y21mnOMH4RDtYXTVG9ADgLE6mHWKZFxYVwt3kHMiUJdY5lJcsOtWLoblrS-bJ0BEXXK5nVDt_bSI5IB7NUf-9QVZxhovRH_ANSxdTjJT0_rMIAZY3WEj68FEap49q_pg72BhnxHVZD2TC3zvX96_DN65HE5SoSgT7OiMAeiJewB_SyemW_HxBwaB-_X-G1ifOtnrzZ4gXTpOrEUlHI2YPuvRMBMtmz1h-nXDZYv3vwU3RA57Qj_ZNVcScj8_RXf2xq8w48v0PzZFXYBSalfnqPq6eUvSSbAJUp6bB8y596JlvR5dFQe_Z--X0Gkfo85IcyrI9D44vK9sJhrGhCVi2FDDa8pHczmNSen99JYZvDif-zpYzgbcymAkOV0gC7JvYMxlZfETT3NTBy7eVA7fJI1SZaeA_qW49xRcBkZBu5gkqTpeGWUU1cGr2aXRVVmXGM22NfV5E7KzvEBsCeHml_tCfxZeKY8Msd8hJb0Cd59u-_hsuc8oNjsOpIdFF976dY3uTmAgHWpG2PH",
"token_type": "bearer",
"expires_in": 3600
}
================================
Now to get new Access token using refresh token :
Send a POST request to https://api.amazon.com/auth/o2/token with the following parameters:
- HTTP Header Parameters
Content-Type: application/x-www-form-urlencoded
- HTTP Body Parameters
grant_type: refresh_token
refresh_token: The URL-encoded refresh token returned with the last request for a new access token.
client_id: To access this information, navigate to Amazons Developer Console. After youve logged in, click Get Started > under Alexa Voice Service, then click Edit next to a registered product (or create a new one). From the left navigation select Security Profile. This page contains both Client ID and Client Secret for your product.
client_secret: The websites client secret. To access this information, navigate to Amazons Developer Console. After youve logged in, click Get Started > under Alexa Voice Service, then click Edit next to a registered product (or create a new one). From the left navigation select Security Profile. This page contains both Client ID and Client Secret for your product.
Response :
{
"access_token": "Atza|IwEBILtBe3hrHovrMx7Oivng-RB2EKzvCm_epXJE2HXPMQzXTFqK10Zrqt-Z8paeRoLQBqbLCmqWvcr5RTNgw9qjtfzOTsOrXC1VKqKmxpqHTrJyn2TLGsCzFjBDfADNjCyufWTf2ZlsSzjxW2GiqCHlwoPSd9pFrLavtRThrm1J-5KvnFrj-yD-tYTSwrgX5W5p2SrjQxoE3aP5b96z6p8GvCL9lM1pddafAxkHb22A3IzR-pYGmEijb4ksRuaIf4WCNwssWV6GBIB2oJA5CU-Dtd2mOZZ5-dYpSSeCHyGumTYecTxxMVSdiVjCqB8WT6AtvvutWFQQoldHjJmIwBsTZP-iQcl-UyajOZJ03GqRUym5Hp-49uByzVG-MfR_Z5qVmYjjsLQEOLCY9kPVnmRGnOTj6YPjrHXibd6P8TQOMh4VTcgFpg-afKKABP6EeDwok1t2ivuYh5OJju-B1A6gzhMi4vQJYKq107e0QMYBBhrf_OqCgMbfnQZ8j40qocVGID5YWv8uk5wKyI61LrbzrTltmzxzNemzqbSBzwAlfNS6GW-jVjg8svsi1lb_EVRbhyOoWJWX3mEd-5GDYyUcyInleiAR0aIHVP94pZxqdiCamA",
"refresh_token": "Atzr|IwEBICA3kDhfSJxlwvnQp9AD1o115AC_KBbFd5GBg8oN_QHWn2or5xFQ09BruuK6a07tGHtTt_9q2Y21mnOMH4RDtYXTVG9ADgLE6mHWKZFxYVwt3kHMiUJdY5lJcsOtWLoblrS-bJ0BEXXK5nVDt_bSI5IB7NUf-9QVZxhovRH_ANSxdTjJT0_rMIAZY3WEj68FEap49q_pg72BhnxHVZD2TC3zvX96_DN65HE5SoSgT7OiMAeiJewB_SyemW_HxBwaB-_X-G1ifOtnrzZ4gXTpOrEUlHI2YPuvRMBMtmz1h-nXDZYv3vwU3RA57Qj_ZNVcScj8_RXf2xq8w48v0PzZFXYBSalfnqPq6eUvSSbAJUp6bB8y596JlvR5dFQe_Z--X0Gkfo85IcyrI9D44vK9sJhrGhCVi2FDDa8pHczmNSen99JYZvDif-zpYzgbcymAkOV0gC7JvYMxlZfETT3NTBy7eVA7fJI1SZaeA_qW49xRcBkZBu5gkqTpeGWUU1cGr2aXRVVmXGM22NfV5E7KzvEBsCeHml_tCfxZeKY8Msd8hJb0Cd59u-_hsuc8oNjsOpIdFF976dY3uTmAgHWpG2PH",
"token_type": "bearer",
"expires_in": 3600
}
=======================
=======================
Prerequests for step 3 (run on server):
requires running mongodb service
Database (tellall) with collection (skill_list)
* Insert dummy skill with : db.skill_list.insert({"skillID" : "amzn1.ask.skill.2445552d-954d-4cd6-b77f-295368e02842", "intents" : [ { "intentName" : "GetFirstQuestion", "questionExplanation" : "","questions" : [ "tell me something about projects", "tell me all about projects" ], "answer" : "blablabla bla bla", "answerType":0, "externalAnswerSource":"" }, { "intentName" : "GetThirdQuestion", "questionExplanation" : "","questions" : [ "Give me third question" ], "answer" : "This is answer to the third question", "answerType":1, "externalAnswerSource":"http://sarajevotimes.com" } ], "invocationName" : "saburly", "invocationAnswer" : "We are Saburly team one", "contactEmail":"bilal@saburly.com" })
*obtain _id and change in web/src/App.js, and also skill_db_id in backend/config.js
*enter web/ dir and run "npm run build"
Database (tellall) with collection (token_list)
* Insert tokens with : db.token_list.insert({"id" : 1, "refresh_token" : "...", "access_token" : "...", "expires_in" : 1515173601.754 })
(Change refresh_token and access_token dots with real ones)
Set skill_id, client_id and client_secret to appropriate values in backend/config.js
Set base_url to "tellall.saburly.com" in web/src/config.
Start backend service from backend/ running "node express.js"
======
for local testing :
first terminal :
*cd web
*npm start
second terminal :
* cd backend
*npm install
*node express.js

View File

@@ -1,33 +0,0 @@
constants = require('./constants')
var config = {};
config.SKILL_STAGE = constants.skillStage.IN_DEVELOPMENT;
config.DB_URL = 'mongodb://localhost:27017/tellall';
config.PORT = 5000;
//Bilal TOKENS
//config.TOKEN = 'Atza|IwEBIBe6gDqrrowEEav6N-_6s4NztYeP3oG8PGWmu8ZiZw6lbOh3wNla3TK6pY-VEpT1d8an-dVf_n3kXJzVFsNo_4xBfZyFHGoCTDTFjs3yBRul4PVdBOhwwiH3-sgRLcUofZbe2oE06GmTcbfYtaStfXpQI5dfpldfnsJg_CvhSA6AHb_snJT3F6lyXzbV076d_3cYUMJxFldJGnYcviNHHxjjmuQTD06hhGzCbAxxe9eBmkuopRsNfyedLT2UlKP_ublah9CUGA3AdIX_3Iuke82jMwGnNl9gv7pbaDNEjAbj7IQSl3B08uuREtJq-oTBOjALNXRvFxTJmQjZwXNf9eHC7fSHJDdEPdZQU0AcffRQObAyAkUuL6Jv39OHzhb3Q64-zzoyODqnJyLP5SQZ2JVF53Kc_cTBqjIc9pXljqe7yEVk6JDs7q1zKbBibx_AQm57TO79IzWyLBzBMlYL5HdTsqEfRzLeDw2tws-hGMgkx2HWfdbYnmf5Qb4SyIhzvmmdfPLg3MVKTxjIBu1rx0xf3n0PLZP1EO6jsJPoMRPg77Gm4oit5Zp6s37ek3A3Vxh-ntoASpkrkxGTG9kVtRNt';
//config.REFRESH_TOKEN = 'Atzr|IwEBICA3kDhfSJxlwvnQp9AD1o115AC_KBbFd5GBg8oN_QHWn2or5xFQ09BruuK6a07tGHtTt_9q2Y21mnOMH4RDtYXTVG9ADgLE6mHWKZFxYVwt3kHMiUJdY5lJcsOtWLoblrS-bJ0BEXXK5nVDt_bSI5IB7NUf-9QVZxhovRH_ANSxdTjJT0_rMIAZY3WEj68FEap49q_pg72BhnxHVZD2TC3zvX96_DN65HE5SoSgT7OiMAeiJewB_SyemW_HxBwaB-_X-G1ifOtnrzZ4gXTpOrEUlHI2YPuvRMBMtmz1h-nXDZYv3vwU3RA57Qj_ZNVcScj8_RXf2xq8w48v0PzZFXYBSalfnqPq6eUvSSbAJUp6bB8y596JlvR5dFQe_Z--X0Gkfo85IcyrI9D44vK9sJhrGhCVi2FDDa8pHczmNSen99JYZvDif-zpYzgbcymAkOV0gC7JvYMxlZfETT3NTBy7eVA7fJI1SZaeA_qW49xRcBkZBu5gkqTpeGWUU1cGr2aXRVVmXGM22NfV5E7KzvEBsCeHml_tCfxZeKY8Msd8hJb0Cd59u-_hsuc8oNjsOpIdFF976dY3uTmAgHWpG2PH';
//Saburly TOKENS
config.TOKEN = 'Atza|IwEBIABS0RvlVshGGO64X0tabhUuzpJKjbWNgxpRiy7YTftFD_lWlp-cbeXuVjRAu8kior2W2C5swf_rEHYvELQXdw78gB9WJQh4ITncPgqNCxvEnwVpIXiyeC_O287DRErnTYbI3s34i4NcxrXzobB8fIpTZxOkg6BI6vQGvvaiNLsTim2ElDYgAEmdgTN77llcMai521aovDqEw_XFc6GspeXhiGKxRomCMIL2UaT649owapDQ3y3Ug9eHvEaBzqjYdOUAtRtv19BGkG8YPs3npHmP5AD0Oc3ByCfrofcGk7fdd_nq28pRX6h4LXk4ylM279qlneWh9EErsWh8vtWuGEGusnDxW17OzEzf7HuwNDqdCJ6gCrIEkZaHISrSQ-vTsYGhKbv0z4nNjf_W_aoc9UJr9LnISCXx424R--iGDKZXhYWlZRjaiHsXE33MpS_M-sdN6GXYQwIjanTUahVXh5h-IBam5uJzTejE6CkIh5iUJ6um2IlDelJRMGS-T_aaG3zUvEagvEd9V9Z5mVN_kmO8bH4H2VefZuFGHRsCPa6SoLrlN0rkXK5fMw-zXfV2MHvQFdkgqYqGKxiEwWJ-g4n1ZrcPtWQowHT2z2yWrfnM2A6g8GIdPT23znmRcrdz4EU';
config.REFRESH_TOKEN = 'Atzr|IwEBIENdBZntrzvJYesv8SGhnty4Nyk2ZySL09elw5N0wH8S1Brz1UgIYLqenw3sKKxnc-VrIUbNtl1Ka4GDKwcTr2fDU_AbKQ6YXzeRBrfRQVvNOeCtjZE8P6Kg1PxAeQoCsqo7WPxK8ZdUaLwPjt_xiZ1FXtr01g-211PJs4KEg5jyF5nY2S14jA_TbwDW6ihpNqWd6ZklTZSRaOeSGa1mXZCSZ5yTsZIQV1Pn0fKhCXtcVg2L833YqRmextmHij4-2NtBQdW3gif5MPdhYjTqDNwgxO3OOagK1uSFqXOnMcmEDnxZuQQApugfDzClfN6DiDALCKN4dVAX8-OU_L2xsUkKiFP9rQjvHWJoRFBT1FpXjBfoVyzM1AaJ6C83WX6SjOBE0hhikQKIaPSe1ikK8_MzOIs2wqLLPnLGnKj3kcKMkDmY6DMgxfWj2H0hwLy0oZ7_qykS7wUHMGjRO5yScuBWFIr0RFFu00GS7zrKjkhFc_E4ZBBKskn0gywS-5pogo0U1rQtLg8lQJsVbxXQwtq2TxkGFMBiVxtQcXtHM1qbCVXpZQbk7ezxvasj4yAIsF4H7KBiZSGmWHkk4yADykWJSntTjFcM2wG0wSiEYoYzJQ';
config.TOKEN_EXPIRES_IN = 1515100500;
//config.SKILL_ID = 'amzn1.ask.skill.efbf0564-a732-4ba9-958f-57939138adae'; //bilal
config.SKILL_ID = 'amzn1.ask.skill.2445552d-954d-4cd6-b77f-295368e02842'; //saburly
//config.SKILL_DB_ID = '5a5016e775becaef2015da10'; //for server
config.SKILL_DB_ID = '5abd461329f85e4ec728d945'; //for local
//Bilal
//config.CLIENT_ID = 'amzn1.application-oa2-client.c748ca56ded04a95b236979898585ff7';
//config.CLIENT_SECRET = '6dea8125cecd049d3c4cff7bb5bdfd3ff17bc6fed246c4c8f6b519d9ed08d0b3';
//Saburly
config.CLIENT_ID = 'amzn1.application-oa2-client.8c183daec15c488c9126b62bda9f7832';
config.CLIENT_SECRET = '3acaa0755291132ee11e1cceaa100feef96a0244662df712a52189199cc655de';
module.exports = config;

View File

@@ -1,84 +0,0 @@
const constants = {};
constants.skillStage = {
IN_DEVELOPMENT: 'development',
LIVE: 'live',
};
constants.amazonResultCodes = {
OK: 200,
ACCEPTED: 202,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
NOT_FOUND: 404,
CONFLICT: 409,
PAYLOAD_TOO_LARGE: 413,
};
constants.apiResultCodes = {
GENERIC_ERROR: -1,
OK: 0,
AMAZON_ERROR: 1, //amazon api works, but error is some of the amazonResultCodes
AMAZON_FAIL: 2, //amazon api doesn't work
DATABASE_ERROR: 3,
NO_SKILL: 4,
INCONSISTENT_STATE: 5,
INVALID_SKILL: 6,
};
constants.HTTPResultCodes = {
INTERNAL_SERVER_ERROR: 500,
};
constants.SKILL_ID_LENGTH = 24;
constants.voiceResponseStrings = {
QUESTION_NOT_FOUND: 'Sorry, I didnt understand',
GENERIC_CONTINUE: 'Say something to continue',
DIDNT_ASK_ANYTHING: 'There was no question to answer to',
ERROR_SUMMARIZING_CONTENT: 'Sorry, there was problem with summarizing news',
ERROR_FETCHING_CONTENT: 'Failed to get content',
};
//Timing is given in [ms]
constants.voiceResponseTimings = {
PAUSE_BETWEEN_QUESTIONS: 650,
PAUSE_AFTER_WELCOME_MESSAGE: 650,
PAUSE_BETWEEN_TITLES: 500,
PAUSE_BETWEEN_TITLE_AND_CONTENT: 500,
PAUSE_BETWEEN_NEWS: 800,
};
constants.stringConstraints = {
INTENT_EXPLANATION_MAX_LENGTH: 70,
INTENT_NAME_MAX_LENGTH: 30,
INTENT_NAME_MIN_LENGTH: 2,
QUESTION_MAX_LENGTH: 150,
QUESTION_MIN_LENGTH: 2,
ANSWER_MAX_LENGTH: 150,
ANSWER_MIN_LENGTH: 2,
INVOCATION_NAME_MAX_LENGTH: 50,
INVOCATION_NAME_MIN_LENGTH: 2,
INVOCATION_ANSWER_MAX_LENGTH: 100,
EMAIL_MAX_LENGTH: 100,
};
constants.answerType = {
PREDEFINED: 0,
EXTERNAL_SOURCE_WP_TITLES: 1,
EXTERNAL_SOURCE_WP_NEWS: 2,
};
constants.contentType = {
TITLES: 0,
NEWS: 1,
};
constants.FIXED_SUMMARY_LENGTH = 3;
module.exports = constants;

View File

@@ -1,14 +0,0 @@
var config = {};
config.PORT = 587;
config.SMTP_HOST = 'smtp.mail.com';
config.SECURE = false;
config.AUTH = {
user: 'saburly@mail.com',
pass: 'KeepSaburly',
};
config.FROM_EMAIL = 'saburly@mail.com';
config.SUBJECT = 'Message from Saburly service';
module.exports = config;

View File

@@ -1,6 +0,0 @@
var express = require ('express'), router = express.Router ();
router.use ('/skill', require ('./skill'));
router.use ('/saburly', require('./saburlyEntryPoint'));
module.exports = router;

View File

@@ -1,12 +0,0 @@
var express = require ('express'), router = express.Router ();
var bodyParser = require ('body-parser');
var alexa = require ('../models/alexa');
var verifier = require('alexa-verifier-middleware')
router.use(verifier);
router.post ('/', bodyParser.json (), async (req, res) => {
alexa.run (req, res);
});
module.exports = router;

View File

@@ -1,145 +0,0 @@
let express = require ('express'), router = express.Router ();
const constants = require ('../config/constants');
let databaseHelper = require ('../helpers/database');
let amazonHelper = require ('../helpers/amazon');
let skillValidator = require('../helpers/skillValidator');
let bodyParser = require ('body-parser');
let alexa = require ('../models/alexa');
router.get ('/:id', async (req, res, next) => {
const id = req.params.id;
if (id.length !== constants.SKILL_ID_LENGTH) {
res.json ([]);
} else {
databaseHelper
.getSkill (id)
.then (result => {
res.json (result);
})
.catch (err => {
res.json ([]);
});
}
});
router.put ('/:id', bodyParser.json (), async (req, res, next) => {
let id = req.params.id;
let dataFromWeb = JSON.stringify (req.body);
let skill = JSON.parse (dataFromWeb);
let updateOnAmazon = skill.updateOnAmazon;
delete skill.updateOnAmazon;
delete skill._id;
//Validate skill
if (!skillValidator.validateSkill(skill)){
//skill not valid
res
.status (
constants.HTTPResultCodes.INTERNAL_SERVER_ERROR
)
.json ({
result: constants.apiResultCodes.INVALID_SKILL,
message: '',
});
return;
}
//First get current skill from DB
databaseHelper
.getSkill (id)
.then (currentSkillState => {
//Now let's update skill in DB
databaseHelper
.updateSkill (id, skill)
.then (() => {
//Ok, done, now update skill on Amazon (if needed)
if (updateOnAmazon) {
//We need to update skill on Amazon
amazonHelper
.updateSkill (skill)
.then (amazonResult => {
if (
amazonResult === constants.amazonResultCodes.OK ||
amazonResult === constants.amazonResultCodes.ACCEPTED
) {
res.json ({result: constants.apiResultCodes.OK, message: ''});
alexa.updateModel ();
} else {
//Update on amazon failed, revert changes in database and send error to user
databaseHelper
.updateSkill (id, currentSkillState)
.then (() => {
res
.status (
constants.HTTPResultCodes.INTERNAL_SERVER_ERROR
)
.json ({
result: constants.apiResultCodes.AMAZON_ERROR,
message: amazonResult,
});
})
.catch (() => {
//This should never happen, something is seriously wrong, like no database connection
res
.status (
constants.HTTPResultCodes.INTERNAL_SERVER_ERROR
)
.json ({
result: constants.apiResultCodes.INCONSISTENT_STATE,
message: '',
});
});
}
})
.catch (e => {
//Update on amazon failed, revert changes in database and send error to user
databaseHelper
.updateSkill (id, currentSkillState)
.then (() => {
res
.status (constants.HTTPResultCodes.INTERNAL_SERVER_ERROR)
.json ({
result: constants.apiResultCodes.AMAZON_FAIL,
message: e,
});
})
.catch (() => {
//This should never happen, something is seriously wrong, like no database connection
res
.status (constants.HTTPResultCodes.INTERNAL_SERVER_ERROR)
.json ({
result: constants.apiResultCodes.INCONSISTENT_STATE,
message: '',
});
});
});
} else {
//No need to update on Amazon, tell to user it's ok
res.json ({result: constants.apiResultCodes.OK, message: ''});
alexa.updateModel ();
}
})
.catch (() => {
//Update in database didn't go well, no need to revert since it failed to write in the first place
//just send error to user
res
.status (
constants.HTTPResultCodes.INTERNAL_SERVER_ERROR
)
.json ({
result: constants.apiResultCodes.DATABASE_ERROR,
message: '',
});
});
})
.catch (e => {
//I don't know why, but something went wrong, possibly ID of skill is wrong, doesn't exist in DB
res
.status (constants.HTTPResultCodes.INTERNAL_SERVER_ERROR)
.json ({result: constants.apiResultCodes.NO_SKILL, message: ''});
});
});
module.exports = router;

View File

@@ -1,311 +0,0 @@
require ('isomorphic-fetch');
const config = require ('../config/config');
var request = require ('request');
var databaseHelper = require ('./database');
var getBuildStatus = function (skillID) {
fetch (
`https://api.amazonalexa.com/v0/skills/${skillID}/interactionModel/locales/en-US/status`,
{
method: 'GET',
headers: {
Authorization: config.TOKEN,
},
}
).then (result => {
return result.text ();
});
};
var refreshTokens = function () {
return new Promise ((resolve, reject) => {
var options = {
method: 'POST',
url: 'https://api.amazon.com/auth/o2/token',
headers: {
'cache-control': 'no-cache',
'content-type': 'application/x-www-form-urlencoded',
},
form: {
grant_type: 'refresh_token',
refresh_token: config.REFRESH_TOKEN,
client_id: config.CLIENT_ID,
client_secret: config.CLIENT_SECRET,
},
};
request (options, function (error, response, body) {
if (error) {
reject (error);
} else {
parsedResponse = JSON.parse (body);
if (parsedResponse.refresh_token) {
databaseHelper
.updateTokens (
parsedResponse.refresh_token,
parsedResponse.access_token,
parsedResponse.expires_in
)
.then (() => {
resolve ();
})
.catch (e => {
reject (e);
});
} else {
reject (body);
}
}
});
});
};
var generateInteractionModel = function (skill) {
let result = {};
let allIntents = [];
skill.intents.map (intent => {
allIntents.push ({name: intent.intentName, samples: intent.questions});
});
//Built-In like intents (Amazon built-in don't work, probably something related to existance of dialog intent
allIntents.push ({
name: 'HelpIntent',
samples: ['Help', 'Can you help me', 'I need help'],
slots: [],
},
{
name: 'CancelIntent',
samples: ['Cancel', 'Stop', 'Please stop'],
slots: [],
},
{
name: 'YesIntent',
samples: [
'Yes',
'Yes please',
'I would like that',
'Yes I would like that',
],
slots: [],
},
{
name: 'NoIntent',
samples: ['No', 'No thank you'],
slots: [],
});
//Special intent for sending message (Dialog)
allIntents.push ({
name: 'SendMessageIntent',
samples: [
'I would like to send a message',
'I want to send a message',
'Send message',
],
slots: [
{
name: 'Name',
type: 'AMAZON.US_FIRST_NAME',
samples: ['My name is {Name}', 'I am {Name}', '{Name}'],
},
{
name: 'Email',
type: 'EmailSlot',
samples: ['My email is {Email}', '{Email}'],
},
{
name: 'Message',
type: 'MessageSlot',
samples: ['{Message}'],
},
],
});
let customSlotTypes = [
{
name: 'EmailSlot',
values: [
{
id: null,
name: {
value: 'bla@bla.bla',
synonyms: [],
},
},
{
id: null,
name: {
value: 'bla.bla@bla.bla.bla',
synonyms: [],
},
},
{
id: null,
name: {
value: 'bla_bla@bla.bla',
synonyms: [],
},
},
],
},
{
name: 'MessageSlot',
values: [
{
id: null,
name: {
value: 'Quick brown fox jumps over lazy dog',
synonyms: [],
},
},
{
id: null,
name: {
value: 'Quick brown fox jumps over lazy dog. Quick brown fox jumps over lazy dog.',
synonyms: [],
},
},
{
id: null,
name: {
value: 'Quick brown fox jumps over lazy dog. Quick brown fox jumps over lazy dog. Quick brown fox jumps over lazy dog.',
synonyms: [],
},
},
],
},
];
let dialogPrompts = [
{
id: 'Elicit.Intent-SendMessageIntent.IntentSlot-Name',
variations: [
{
type: 'PlainText',
value: 'What is your name ?',
},
{
type: 'PlainText',
value: 'Tell me your name',
},
],
},
{
id: 'Elicit.Intent-SendMessageIntent.IntentSlot-Email',
variations: [
{
type: 'PlainText',
value: 'What is your email ?',
},
{
type: 'PlainText',
value: 'Tell me your email',
},
],
},
{
id: 'Elicit.Intent-SendMessageIntent.IntentSlot-Message',
variations: [
{
type: 'PlainText',
value: 'What is your message',
},
],
},
];
let dialogIntents = [
{
name: 'SendMessageIntent',
confirmationRequired: false,
prompts: {},
slots: [
{
name: 'Name',
type: 'AMAZON.US_FIRST_NAME',
elicitationRequired: true,
confirmationRequired: false,
prompts: {
elicitation: 'Elicit.Intent-SendMessageIntent.IntentSlot-Name',
},
},
{
name: 'Email',
type: 'EmailSlot',
elicitationRequired: true,
confirmationRequired: false,
prompts: {
elicitation: 'Elicit.Intent-SendMessageIntent.IntentSlot-Email',
},
},
{
name: 'Message',
type: 'MessageSlot',
elicitationRequired: true,
confirmationRequired: false,
prompts: {
elicitation: 'Elicit.Intent-SendMessageIntent.IntentSlot-Message',
},
},
],
},
];
result.interactionModel = {};
result.interactionModel.languageModel = {
invocationName: skill.invocationName,
types: customSlotTypes,
intents: allIntents,
};
result.interactionModel.prompts = dialogPrompts;
result.interactionModel.dialog = {};
result.interactionModel.dialog.intents = dialogIntents;
return JSON.stringify (result);
};
var uploadSkill = function (skill) {
let generatedInteractionModel = generateInteractionModel (skill);
console.log(skill.skillID);
return fetch (
`https://api.amazonalexa.com/v1/skills/${skill.skillID}/stages/development/interactionModel/locales/en-US`,
{
method: 'PUT',
headers: {
Authorization: config.TOKEN,
},
body: generatedInteractionModel,
}
);
};
module.exports = {
updateSkill: function (skill) {
return new Promise ((resolve, reject) => {
if (new Date () / 1000 > config.TOKEN_EXPIRES_IN) {
refreshTokens ()
.then (() => {
uploadSkill (skill).then (response => {
resolve (response.status);
});
})
.catch (e => {
reject (e);
});
} else {
uploadSkill (skill)
.then (response => {
resolve (response.status);
})
.catch (e => {
reject (e);
});
}
});
},
};

View File

@@ -1,99 +0,0 @@
const config = require ('../config/config');
var ObjectID = require ('mongodb').ObjectID;
var db = null;
module.exports = {
initModule: function (databaseObject) {
db = databaseObject;
db.collection ('intent_list');
},
loadTokens: function () {
db
.collection ('token_list')
.findOne ()
.then (tokens => {
if (tokens !== null) {
config.TOKEN = tokens.access_token;
config.REFRESH_TOKEN = tokens.refresh_token;
config.TOKEN_EXPIRES_IN = tokens.expires_in;
} else {
//Cannot continue without tokens
console.log ('Cannot continue without tokens in database');
process.exit (-1);
}
})
.catch (e => {
console.log (
'Error loading tokens ! Cannot continue without tokens in database'
);
process.exit (-1);
});
},
updateTokens: function (refresh_token, access_token, expires_in) {
return new Promise ((resolve, reject) => {
let newTokenDocument = {
id: 1,
refresh_token: refresh_token,
access_token: access_token,
expires_in: new Date () / 1000 + expires_in,
};
db
.collection ('token_list')
.update ({id: 1}, newTokenDocument, {upsert: true}, (err, result) => {
if (err) {
reject (err)
}else{
config.REFRESH_TOKEN = refresh_token;
config.TOKEN = access_token;
config.TOKEN_EXPIRES_IN = newTokenDocument.expires_in;
resolve ();
}
});
});
},
getSkill: function (skillDbID) {
return new Promise ((resolve, reject) => {
db
.collection ('skill_list')
.findOne ({_id: ObjectID (skillDbID)}, (err, skill) => {
if (skill) {
resolve (skill);
} else {
reject (err);
}
});
});
},
updateSkill: function (id, skill) {
return new Promise ((resolve, reject) => {
db
.collection ('skill_list')
.update ({_id: ObjectID (id)}, skill, {upsert: true}, (err, result) => {
if (err){
reject();
}else{
resolve();
}
});
});
},
deleteSkill: function (id) {
return new Promise ((resolve, reject) => {
db
.collection ('skill_list')
.remove ({_id: ObjectID (id)}, (err, result) => {
if (err){
reject (err);
}else{
resolve (result);
}
});
});
}
};

View File

@@ -1,75 +0,0 @@
const nodemailer = require ('nodemailer');
const emailConfig = require('../config/email');
module.exports = {
transformEmailFromAlexaResponse: function (email) {
//email from alexa response will contain words instead of symbols, like :
//at = @
//underscore = _
//dash = -
//dot = .
//TODO: This list should be longer
let transformedEmail = email
.replace (/\s/g, '') //remove all spaces
.replace (/at/gi, '@')
.replace (/underscore/gi, '_')
.replace (/dash/gi, '-')
.replace (/dot/gi, '.');
return transformedEmail;
},
isEmailValid: function (email) {
console.log ('Email to validate : ' + email);
let validEmailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return validEmailRegex.test (email);
},
sendEmail: function (name, fromEmail, message, toEmail) {
return new Promise ((resolve, reject) => {
fromEmail = this.transformEmailFromAlexaResponse(fromEmail);
let messageBody =
'Hello. User left you a message on Saburly service using Alexa skill. \r\nMessage : ' +
message +
'\r\nName : ' +
name +
'\r\nEmail : ' +
fromEmail +
'\r\nYour Saburly team';
let messageBodyHTML =
'<p>Hello. User left you a message on Saburly service using Alexa skill.</p><br/><b>Message : </b><br/><p>' +
message +
'</p><br/><b>Name : </b>' +
name +
'<br/><b>Email : </b>' +
fromEmail +
'<br/><br/><b>Your Saburly team</b>';
let transporter = nodemailer.createTransport ({
host: emailConfig.SMTP_HOST,
port: emailConfig.PORT,
secure: emailConfig.SECURE,
auth: emailConfig.AUTH,
});
var mailOptions = {
from: emailConfig.FROM_EMAIL,
replyTo: fromEmail,
to: toEmail,
subject: emailConfig.SUBJECT,
text: messageBody,
html: messageBodyHTML,
};
transporter.sendMail (mailOptions, (error, info) => {
if (error) {
reject (error);
} else {
resolve (info);
}
});
});
},
};

View File

@@ -1,112 +0,0 @@
let request = require ('request');
let Parser = require ('rss-parser');
let summarizer = require ('nodejs-text-summarizer');
var htmlToText = require ('html-to-text');
const constants = require ('../config/constants');
let parser = new Parser ();
getDataFromWPJSON = function (sourceUrl, page = 1, maxPosts = 10) {
return new Promise ((resolve, reject) => {
var options = {
method: 'GET',
url: `${sourceUrl}/wp-json/wp/v2/posts`,
qs: {
page: page,
per_page: maxPosts,
},
};
request (options, (error, response, body) => {
if (error) {
reject (error);
} else {
resolve (JSON.parse (body));
}
});
});
};
summarizeText = function (text, length, clearText = true) {
let preparedText = text;
if (clearText) {
preparedText = htmlToText.fromString (text, {
wordwrap: false,
ignoreHref: true,
ignoreImage: true,
});
}
return summarizer (preparedText, {n: length});
};
getTitlesFromWP = function (sourceUrl) {
return new Promise ((resolve, reject) => {
getDataFromWPJSON (sourceUrl)
.then (rawData => {
let result = '';
rawData.forEach (post => {
result +=
post.title.rendered +
`<break time="${constants.voiceResponseTimings.PAUSE_BETWEEN_TITLES}ms"/> `;
});
resolve (result);
})
.catch (err => {
reject (constants.voiceResponseStrings.ERROR_FETCHING_CONTENT);
});
});
};
getLatestNewsFromWP = function (
sourceUrl,
postCount = 10,
includeTitle = false
) {
return new Promise ((resolve, reject) => {
getDataFromWPJSON (sourceUrl, 1, postCount)
.then (rawData => {
let result = '';
let htmlToTextOptions = {
wordwrap: false,
ignoreHref: true,
ignoreImage: true,
};
try {
rawData.forEach (post => {
result += includeTitle ? post.title.rendered : '';
result += includeTitle
? `<break time="${constants.voiceResponseTimings.PAUSE_BETWEEN_TITLE_AND_CONTENT}ms"/>`
: '';
result += summarizeText (
post.content.rendered,
constants.FIXED_SUMMARY_LENGTH
);
result += `<break time="${constants.voiceResponseTimings.PAUSE_BETWEEN_NEWS}ms"/>`;
});
resolve (result);
} catch (err) {
reject (constants.voiceResponseStrings.ERROR_SUMMARIZING_CONTENT);
}
})
.catch (err => {
reject (constants.voiceResponseStrings.ERROR_FETCHING_CONTENT);
});
});
};
module.exports = {
getAnswerFromWP: function (sourceUrl, contentType) {
//This function will extract needed data from JSON, which we got from getDataFromWPJSON
switch (contentType) {
case constants.contentType.TITLES:
return getTitlesFromWP (sourceUrl);
break;
case constants.contentType.NEWS:
return getLatestNewsFromWP (sourceUrl);
break;
}
},
};

View File

@@ -1,107 +0,0 @@
const constants = require ('../config/constants');
validateEmail = function (email) {
if (email.length > constants.stringConstraints.EMAIL_MAX_LENGTH) return false;
let validEmailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return validEmailRegex.test (email);
};
validateIntentName = function (intentName) {
if (
intentName.length < constants.stringConstraints.INTENT_NAME_MIN_LENGTH ||
intentName.length > constants.stringConstraints.INTENT_NAME_MAX_LENGTH
)
return false;
let validIntentNameRegex = /^[a-z]*$/i;
return validIntentNameRegex.test (intentName);
};
validateQuestion = function (question) {
if (
question.length < constants.stringConstraints.QUESTION_MIN_LENGTH ||
question.length > constants.stringConstraints.QUESTION_MAX_LENGTH
)
return false;
let validQuestionNameRegex = /^[a-z,.' ]*$/i;
return validQuestionNameRegex.test (question);
};
validateAnswer = function (answer, answerType) {
if (answerType !== constants.answerType.PREDEFINED) return true;
if (
answer.length < constants.stringConstraints.ANSWER_MIN_LENGTH ||
answer.length > constants.stringConstraints.ANSWER_MAX_LENGTH
)
return false;
let validAnswerRegex = /^[a-z,.' ]*$/i;
return validAnswerRegex.test (answer);
};
validateExternalAnswerSource = function (externalAnswerSource, answerType){
// TODO: implement validation logic
return true;
}
validateInvocationName = function (invocationName) {
if (
invocationName.length < constants.stringConstraints.INVOCATION_NAME_MIN_LENGTH ||
invocationName.length > constants.stringConstraints.INVOCATION_NAME_MAX_LENGTH
)
return false;
let validInvocationNameRegex = /^[a-z,.' ]*$/;
return validInvocationNameRegex.test (invocationName);
};
validateInvocationAnswer = function (invocationAnswer) {
if (invocationAnswer.length > constants.stringConstraints.INVOCATION_ANSWER_MAX_LENGTH)
return false;
let validInvocationAnswerRegex = /^[a-z,.' ]*$/i;
return validInvocationAnswerRegex.test (invocationAnswer);
};
validateIntentExplanation = function (explanation) {
if (explanation.length > constants.stringConstraints.INTENT_EXPLANATION_MAX_LENGTH)
return false;
let validExplanationRegex = /^[a-z,.' ]*$/i;
return validExplanationRegex.test (explanation);
};
module.exports = {
validateSkill: function (skill) {
try {
if (
!validateEmail (skill.contactEmail) ||
!validateInvocationName (skill.invocationName) ||
!validateInvocationAnswer (skill.invocationAnswer)
)
return false;
for (let i = 0; i < skill.intents.length; i++) {
if (!validateIntentName (skill.intents[i].intentName)) return false;
if (!validateAnswer (skill.intents[i].answer, skill.intents[i].answerType)) return false;
if (!validateExternalAnswerSource(skill.intents[i].externalAnswerSource, skill.intents[i].answerType)) return false;
for (let j = 0; j < skill.intents.length; j++) {
if (i === j) continue;
if (skill.intents[i].intentName === skill.intents[j].intentName)
return false;
}
for (let j = 0; j < skill.intents[i].questions.length; j++) {
if (!validateQuestion (skill.intents[i].questions[j])) return false;
for (let k = 0; k < skill.intents[i].questions.length; k++) {
if (j === k) continue;
if (skill.intents[i].questions[j] === skill.intents[i].questions[k])
return false;
}
}
}
return true;
} catch (e) {
console.log ('Error : ' + e);
return false;
}
},
};

View File

@@ -1,7 +0,0 @@
module.exports = function (req, res, next) {
res.header ('Access-Control-Allow-Origin', '*');
res.header ('Access-Control-Allow-Headers', 'Origin, Content-Type');
res.header ('Access-Control-Allow-Methods', 'GET, POST, PUT');
res.header ('Access-Control-Allow-Credentials', 'true');
next ();
};

View File

@@ -1,291 +0,0 @@
var Alexa = require ('alexa-sdk');
const config = require ('../config/config');
var databaseHelper = require ('../helpers/database');
var emailHelper = require ('../helpers/email');
const constants = require ('../config/constants');
let predefinedSourceHelper = require ('../helpers/externalSource');
var handlers = {};
var destinationEmail;
let skillName;
module.exports = {
run: function (req, res) {
// Build the context manually, because Amazon Lambda is missing
var context = {
succeed: function (result) {
res.json (result);
},
fail: function (error) {
console.log (error);
//We could send error json from here
},
};
var alexa = Alexa.handler (req.body, context);
alexa.appId = config.SKILL_ID;
alexa.registerHandlers (handlers);
alexa.execute ();
},
updateModel: function () {
//Get info from database, and store it for faster response on intent
databaseHelper
.getSkill (config.SKILL_DB_ID)
.then (activeSkill => {
handlers = {};
destinationEmail = activeSkill.contactEmail;
skillName = activeSkill.invocationName;
let listOfPossibleQuestions = '';
activeSkill.intents.forEach (intent => {
if (intent.questions.length > 0 && intent.intentExplanation) {
listOfPossibleQuestions +=
intent.intentExplanation +
intent.questions[0] +
'<break time="' +
constants.voiceResponseTimings.PAUSE_BETWEEN_QUESTIONS +
'ms"/>';
}
});
listOfPossibleQuestions +=
'If you dont know what to do, just say help or stop';
//Handler for launch requestconsole.log()
handlers = {
LaunchRequest: function () {
this.response
.speak (
activeSkill.invocationAnswer +
'<break time="' +
constants.voiceResponseTimings.PAUSE_AFTER_WELCOME_MESSAGE +
'ms"/>' +
'Would you like to hear list of questions that you can ask me'
)
.listen (constants.voiceResponseStrings.GENERIC_CONTINUE);
this.attributes['LaunchRequestYesNo'] = true;
this.emit (':responseReady');
},
};
//Handlers for user defined questions
activeSkill.intents.map (intent => {
handlers[intent.intentName] = function () {
if (this.attributes['LaunchRequestYesNo']) {
this.attributes['LaunchRequestYesNo'] = false;
}
let answerPromiseProps = {
resolve: null,
reject: null,
};
let answerPromise = new Promise ((resolve, reject) => {
answerPromiseProps = {
resolve: resolve,
reject: reject,
};
});
switch (intent.answerType) {
case constants.answerType.PREDEFINED:
answerPromiseProps.resolve (intent.answer);
break;
case constants.answerType.EXTERNAL_SOURCE_WP_TITLES:
predefinedSourceHelper
.getAnswerFromWP (
intent.externalAnswerSource,
constants.contentType.TITLES
)
.then (answer => {
answerPromiseProps.resolve (answer);
})
.catch (error => {
answerPromiseProps.reject (error);
});
break;
case constants.answerType.EXTERNAL_SOURCE_WP_NEWS:
predefinedSourceHelper
.getAnswerFromWP (
intent.externalAnswerSource,
constants.contentType.NEWS
)
.then (answer => {
answerPromiseProps.resolve (answer);
})
.catch (error => {
answerPromiseProps.reject (error);
});
break;
}
answerPromise
.then (answer => {
this.response
.speak (answer);
this.emit (':responseReady');
})
.catch (error => {
this.response
.speak (error);
this.emit (':responseReady');
});
};
});
//Handler for sending message
handlers.SendMessageIntent = function () {
if (this.attributes['LaunchRequestYesNo']) {
this.attributes['LaunchRequestYesNo'] = false;
}
let intent = this.event.request.intent;
console.log ('Dialog state : ' + this.event.request.dialogState);
console.log (intent);
//STARTED, IN_PROGRESS
if (!intent.slots.Name.value) {
//Name not defined yet, ask user for name
const slotToElicit = 'Name';
const speechOutput = 'What is your name';
const repromptSpeech = speechOutput;
this.emit (
':elicitSlot',
slotToElicit,
speechOutput,
repromptSpeech
);
} else if (!intent.slots.Email.value) {
//Name not defined yet, ask user for email
const slotToElicit = 'Email';
const speechOutput =
'Ok ' + intent.slots.Name.value + '. What is your email';
const repromptSpeech = speechOutput;
this.emit (
':elicitSlot',
slotToElicit,
speechOutput,
repromptSpeech
);
} else if (!intent.slots.Message.value) {
intent.slots.Email.value = emailHelper.transformEmailFromAlexaResponse (
intent.slots.Email.value
);
if (!emailHelper.isEmailValid (intent.slots.Email.value)) {
//Email is not valid, ask again
const slotToElicit = 'Email';
const speechOutput =
'Sorry, that was not valid email. What is your email';
const repromptSpeech = speechOutput;
this.emit (
':elicitSlot',
slotToElicit,
speechOutput,
repromptSpeech
);
} else {
//Email is valid
const slotToElicit = 'Message';
const speechOutput = 'Great. What is your message';
const repromptSpeech = speechOutput;
this.emit (
':elicitSlot',
slotToElicit,
speechOutput,
repromptSpeech
);
}
} else {
//all slots are filled
console.log ('Name : ' + intent.slots.Name.value);
console.log ('Email : ' + intent.slots.Email.value);
console.log ('Message : ' + intent.slots.Message.value);
emailHelper
.sendEmail (
intent.slots.Name.value,
intent.slots.Email.value,
intent.slots.Message.value,
destinationEmail
)
.then (info => {
console.log (info);
this.response.speak (
'Ok. Message sent. Someone will contact you ASAP'
);
this.emit (':responseReady');
})
.catch (error => {
console.log (error);
this.response.speak (
'Sorry, there was a problem with sending message.'
);
this.emit (':responseReady');
});
}
};
//Built-In intents
handlers.CancelIntent = function () {
if (this.attributes['LaunchRequestYesNo']) {
this.attributes['LaunchRequestYesNo'] = false;
}
this.response.speak (`Thank you for using ${skillName}`);
this.emit (':responseReady');
};
handlers.HelpIntent = function () {
if (this.attributes['LaunchRequestYesNo']) {
this.attributes['LaunchRequestYesNo'] = false;
}
this.response
.speak (listOfPossibleQuestions)
.listen (constants.voiceResponseStrings.GENERIC_CONTINUE);
this.emit (':responseReady');
};
handlers.YesIntent = function () {
if (this.attributes['LaunchRequestYesNo']) {
this.attributes['LaunchRequestYesNo'] = false;
this.emit ('HelpIntent');
} else {
this.response
.speak (constants.voiceResponseStrings.DIDNT_ASK_ANYTHING);
this.emit (':responseReady');
}
};
handlers.NoIntent = function () {
if (this.attributes['LaunchRequestYesNo']) {
this.attributes['LaunchRequestYesNo'] = false;
this.response
.speak ('');
this.emit (':responseReady');
} else {
this.response
.speak (constants.voiceResponseStrings.DIDNT_ASK_ANYTHING);
this.emit (':responseReady');
}
};
//Default handler for unknown question
handlers.Unhandled = function () {
this.response
.speak (constants.voiceResponseStrings.QUESTION_NOT_FOUND);
this.emit (':responseReady');
};
//Session handlers
handlers.SessionEndedRequest = function () {
this.response.speak (`Thank you for using ${skillName}`);
this.emit (':responseReady');
};
})
.catch (e => {
//Something is wrong, skill is not ready, use catch-all intent to inform user
console.log ('Error. Skill doesnt exist');
});
},
};

View File

@@ -1,20 +0,0 @@
{
"name": "tellall",
"version": "1.0.0",
"description": "",
"main": "test.js",
"dependencies": {
"alexa-sdk": "^1.0.25",
"alexa-verifier-middleware": "^1.0.1",
"body-parser": "^1.13.1",
"ejs": "^2.5.7",
"express": "^4.13.0",
"html-to-text": "^4.0.0",
"isomorphic-fetch": "^2.2.1",
"mongodb": "^2.2.33",
"nodejs-text-summarizer": "GotPPay/nodejs-text-summarizer",
"nodemailer": "^4.4.1",
"request": "^2.83.0",
"rss-parser": "^3.1.1"
}
}

View File

@@ -1,28 +0,0 @@
var databaseHelper = require ('./helpers/database');
const config = require ('./config/config');
var express = require ('express');
var alexa = require ('./models/alexa');
var MongoClient = require ('mongodb').MongoClient;
var ObjectID = require ('mongodb').ObjectID;
const router = express.Router ();
var app = express ();
app.set ('view engine', 'ejs'); // Should be removed
app.use (require ('./middleware')); //common middleware for all requests
app.use (require ('./controllers')); //all routes
MongoClient.connect (config.DB_URL)
.then (database => {
databaseHelper.initModule (database);
app.listen (config.PORT, () => {
console.log ('Express server running on port ' + config.PORT);
alexa.updateModel ();
databaseHelper.loadTokens ();
});
})
.catch (e => {
console.log ('error : ' + e);
});

43
express.js Normal file
View File

@@ -0,0 +1,43 @@
var express = require("express");
var alexa = require("alexa-app");
var PORT = process.env.port || 5000;
var app = express();
// ALWAYS setup the alexa app and attach it to express before anything else.
var alexaApp = new alexa.app("step1");
alexaApp.express({
expressApp: app,
// verifies requests come from amazon alexa. Must be enabled for production.
// You can disable this if you're running a dev environment and want to POST
// things to test behavior. enabled by default.
checkCert: false,
// sets up a GET route when set to true. This is handy for testing in
// development, but not recommended for production. disabled by default
debug: true
});
// now POST calls to /test in express will be handled by the app.request() function
// from here on you can setup any other express routes or middlewares as normal
app.set("view engine", "ejs");
alexaApp.launch(function(request, response) {
response.say("You launched Saburly app!");
});
alexaApp.intent("GetProcessIntent", {
"utterances": [
"tell me about projects", "say something about your project", "what are your projects"
]
},
function(request, response) {
response.say("We collaborate closely with our clients at each step of the developmentprocess. From designing the UX to developing the front-end andarchitecting the back-end.");
}
);
app.listen(PORT);
console.log("Listening on port " + PORT + ", try http://localhost:" + PORT + "/step1");

14
lambda.js Normal file
View File

@@ -0,0 +1,14 @@
var alexa = require("alexa-app");
var find = require("find-my-iphone");
var app = new alexa.app();
app.launch(function(request, response) {
find("me@icloud.com", "mypassword", "iPhone", function() {
response.say("OK").send();
});
// because this is an async handler
return false;
});
// connect to lambda
exports.handler = app.lambda();

File diff suppressed because it is too large Load Diff

14
package.json Normal file
View File

@@ -0,0 +1,14 @@
{
"name": "example",
"version": "1.0.0",
"description": "",
"main": "test.js",
"dependencies": {
"body-parser": "^1.13.1",
"ejs": "^2.3.1",
"express": "^4.13.0",
"alexa-app": "4.2.0"
},
"author": "Matt Kruse <github@mattkruse.com> (http://mattkruse.com/)",
"license": "MIT"
}

116
template.js Normal file
View File

@@ -0,0 +1,116 @@
var template = {};
// LaunchRequest template
template.launch = {
"version": "1.0",
"session": {
"new": true,
"sessionId": "amzn1.echo-api.session.abeee1a7-aee0-41e6-8192-e6faaed9f5ef",
"attributes": {},
"application": {
"applicationId": "amzn1.ask.skill.7115bfc9-313e-4728-830b-ebd19ce96cb3"
},
"user": {
"userId": "amzn1.account.AM3B227HF3FAM1B261HK7FFM3A2"
}
},
"request": {
"type": "LaunchRequest",
"requestId": "amzn1.echo-api.request.9cdaa4db-f20e-4c58-8d01-c75322d6c423"
}
};
// IntentRequest template
template.intent = {
"version": "1.0",
"session": {
"new": false,
"sessionId": "amzn1.echo-api.session.abeee1a7-aee0-41e6-8192-e6faaed9f5ef",
"attributes": {},
"application": {
"applicationId": "amzn1.ask.skill.7115bfc9-313e-4728-830b-ebd19ce96cb3"
},
"user": {
"userId": "amzn1.account.AM3B227HF3FAM1B261HK7FFM3A2"
}
},
"request": {
"type": "IntentRequest",
"requestId": "amzn1.echo-api.request.6919844a-733e-4e89-893a-fdcb77e2ef0d",
"intent": {
"name": "sampleIntent",
"slots": {
"NAME": {
"name": "NAME",
"value": "Matt"
}
}
}
}
};
// errorIntent template
template.errorIntent = {
"version": "1.0",
"session": {
"new": false,
"sessionId": "amzn1.echo-api.session.abeee1a7-aee0-41e6-8192-e6faaed9f5ef",
"attributes": {},
"application": {
"applicationId": "amzn1.ask.skill.7115bfc9-313e-4728-830b-ebd19ce96cb3"
},
"user": {
"userId": "amzn1.account.AM3B227HF3FAM1B261HK7FFM3A2"
}
},
"request": {
"type": "IntentRequest",
"requestId": "amzn1.echo-api.request.6919844a-733e-4e89-893a-fdcb77e2ef0d",
"intent": {
"name": "errorIntent",
"slots": {}
}
}
};
// missingIntent template
template.missingIntent = {
"version": "1.0",
"session": {
"new": false,
"sessionId": "amzn1.echo-api.session.abeee1a7-aee0-41e6-8192-e6faaed9f5ef",
"attributes": {},
"application": {
"applicationId": "amzn1.ask.skill.7115bfc9-313e-4728-830b-ebd19ce96cb3"
},
"user": {
"userId": "amzn1.account.AM3B227HF3FAM1B261HK7FFM3A2"
}
},
"request": {
"type": "IntentRequest",
"requestId": "amzn1.echo-api.request.6919844a-733e-4e89-893a-fdcb77e2ef0d",
"intent": {
"name": "missingIntent",
"slots": {}
}
}
};
// SessionEndedRequest template
template.session_end = {
"version": "1.0",
"session": {
"new": false,
"sessionId": "amzn1.echo-api.session.abeee1a7-aee0-41e6-8192-e6faaed9f5ef",
"attributes": {},
"application": {
"applicationId": "amzn1.ask.skill.7115bfc9-313e-4728-830b-ebd19ce96cb3"
},
"user": {
"userId": "amzn1.account.AM3B227HF3FAM1B261HK7FFM3A2"
}
},
"request": {
"type": "SessionEndedRequest",
"requestId": "amzn1.echo-api.request.d8c37cd6-0e1c-458e-8877-5bb4160bf1e1",
"reason": "USER_INITIATED"
}
};
module.exports = template;

79
test.js Normal file
View File

@@ -0,0 +1,79 @@
var alexa = require("alexa-app");
var template = require("./template.js");
var app = new alexa.app("test");
app.dictionary = {
"names": ["Bob", "Jack", "Matt", "Mary", "Jane", "Bill"]
};
app.launch(function(request, response) {
response.say("App launched!");
});
app.intent("sampleIntent", {
"slots": { "NAME": "LITERAL", "AGE": "NUMBER" },
"utterances": ["my {name is|name's} {names|NAME} and {I am|I'm} {1-100|AGE}{ years old|}"]
},
function(request, response) {
setTimeout(function() {
response.say("After timeout!").say(" test ").reprompt("Reprompt");
response.send();
}, 1000);
// We are async!
return false;
}
);
app.intent("errorIntent", function(request, response) {
response.say(someVariableThatDoesntExist);
});
// output the schema
console.log("\n\nSCHEMA:\n\n" + app.schema() + "\n\n");
// output sample utterances
console.log("\n\nUTTERANCES:\n\n" + app.utterances() + "\n\n");
// test pre() and post() functions
app.pre = function(request, response, type) {
response.say("This part of the output is from pre(). ");
};
app.post = function(request, response, type, exception) {
if (exception) {
response.clear().say("An error occured: " + exception).send();
}
};
// error example
app.request(template.errorIntent)
.then(function(response) {
console.log(JSON.stringify(response, null, 3));
});
// async example
app.request(template.intent)
.then(function(response) {
console.log(JSON.stringify(response, null, 3));
});
// synchronous example
app.request(template.launch)
.then(function(response) {
console.log(JSON.stringify(response, null, 3));
});
// error example
app.messages.NO_INTENT_FOUND = "Why you called dat intent? I don't know bout dat";
app.request(template.missingIntent)
.then(function(response) {
console.log(JSON.stringify(response, null, 3));
});
// error handler example
app.error = function(e, request, response) {
response.say("I captured the exception! It was: " + e.message);
};
app.request(template.errorIntent)
.then(function(response) {
console.log(JSON.stringify(response, null, 3));
});

10
views/test.ejs Normal file
View File

@@ -0,0 +1,10 @@
<div style="white-space:pre;border:1px solid black;margin:5px;padding:5px;font-family:monospace;">
Schema:
<%=schema%>
Utterances:
<%=utterances%>
</div>

23
web/.gitignore vendored
View File

@@ -1,23 +0,0 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
src/**/*.css

12961
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +0,0 @@
{
"name": "web",
"version": "0.1.0",
"private": true,
"dependencies": {
"keymaster": "^1.6.2",
"node-sass-chokidar": "0.0.3",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"react-md": "^1.2.8",
"react-popup": "^0.9.1",
"react-scripts": "1.0.17",
"webfontloader": "^1.6.28",
"node-sass": "^4.7.2",
"npm-run-all": "^4.1.2"
},
"scripts": {
"build-css": "node-sass-chokidar --include-path ./node_modules src/ -o src/",
"watch-css-mine": "npm run build-css && npm run build-css --watch --recursive",
"watch-css": "nodemon -e scss -x \"npm run watch-css-mine\"",
"start-js": "react-scripts start",
"start": "npm-run-all -p watch-css start-js",
"react-build": "react-scripts build",
"build": "npm-run-all -p build-css react-build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"devDependencies": {
"babel-jest": "^22.4.3",
"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
"enzyme-to-json": "^3.3.3",
"jest": "^20.0.3",
"jest-enzyme": "^6.0.0",
"nodemon": "^1.12.1",
"react-test-renderer": "^16.3.0"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -1,40 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@@ -1,15 +0,0 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -1,372 +0,0 @@
import React, {Component} from 'react';
import './css/App.css';
import './css/popup.css';
import IntentList from './components/IntentList';
import IntentDetails from './components/IntentDetails';
import LaunchRequest from './components/LaunchRequest';
import Contact from './components/Contact';
import Popup from 'react-popup';
import {getSkill, updateSkill} from './lib/api';
import {isEmailValid} from './lib/helpers';
import {
NEW_INTENT_SELECTED_INDEX,
LAUNCH_REQUEST_SELECTED_INDEX,
CONTACT_SELECTED_INDEX,
RESULT_CODES,
INVOCATION_NAME_MIN_LENGTH,
INTENT_NAME_MIN_LENGTH,
QUESTION_MIN_LENGTH,
ANSWER_MIN_LENGTH,
ANSWER_TYPE,
} from './config/constants';
class App extends Component {
constructor (props) {
super (props);
this.state = {
_id: '5abd461329f85e4ec728d945',
skillID: '',
skillName: '',
invocationName: 'Saburly',
invocationAnswer: 'We are saburly',
allIntents: [],
selectedIntent: {
intentName: '',
intentExplanation: '',
questions: [''],
answer: '',
answerType: ANSWER_TYPE.PREDEFINED,
externalAnswerSource: '',
},
selectedIndex: NEW_INTENT_SELECTED_INDEX,
contactEmail: '',
waiting: false,
};
getSkill (this.state._id).then (l => l.json ()).then (result => {
if (result === undefined) return;
this.setState ({
skillID: result.skillID,
skillName: result.skillName,
invocationName: result.invocationName,
invocationAnswer: result.invocationAnswer,
allIntents: result.intents,
contactEmail: result.contactEmail,
});
});
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);
this.sendSkill = this.sendSkill.bind (this);
this.handleContactClick = this.handleContactClick.bind (this);
this.handleSaveEmailClick = this.handleSaveEmailClick.bind (this);
}
render () {
let rightPanel;
switch (this.state.selectedIndex) {
case LAUNCH_REQUEST_SELECTED_INDEX:
rightPanel = (
<LaunchRequest
invocationName={this.state.invocationName}
invocationAnswer={this.state.invocationAnswer}
onSaveClick={this.handleSaveLaunchRequestClick}
waiting={this.state.waiting}
/>
);
break;
case CONTACT_SELECTED_INDEX:
rightPanel = (
<Contact
contactEmail={this.state.contactEmail}
onSaveEmailClick={this.handleSaveEmailClick}
waiting={this.state.waiting}
/>
);
break;
default:
rightPanel = (
<IntentDetails
selectedIntent={this.state.selectedIntent}
onDeleteIntentClick={this.handleDeleteIntentClick}
onSaveIntentClick={this.handleSaveIntentClick}
waiting={this.state.waiting}
/>
);
}
return (
<div className="App">
<Popup />
<div className="App-header">
<h1> Tell All </h1>
</div>
<IntentList
allIntents={this.state.allIntents}
onLaunchRequestClick={this.handleLaunchRequestClick}
onContactClick={this.handleContactClick}
onIntentClick={this.handleIntentClick}
onAddIntentClick={this.handleAddIntentClick}
selectedIndex={this.state.selectedIndex}
waiting={this.state.waiting}
/>
{rightPanel}
</div>
);
}
createSkill (intents, name, answer, email, updateOnAmazon) {
return {
_id: this.state._id,
skillID: this.state.skillID,
intents: intents,
invocationName: name,
invocationAnswer: answer,
contactEmail: email,
updateOnAmazon: updateOnAmazon,
};
}
handleIntentClick (selectedIntent, index) {
this.setState ({
selectedIntent: selectedIntent,
selectedIndex: index,
});
}
handleLaunchRequestClick () {
this.setState ({selectedIndex: LAUNCH_REQUEST_SELECTED_INDEX});
}
handleContactClick () {
this.setState ({selectedIndex: CONTACT_SELECTED_INDEX});
}
handleSaveLaunchRequestClick (name, answer) {
if (name.length < INVOCATION_NAME_MIN_LENGTH) {
Popup.alert ('Invocation name should be at least 2 characters long');
return;
}
this.setState ({
waiting: true,
invocationName: name,
invocationAnswer: answer,
});
this.sendSkill (
this.state.allIntents,
true,
{waiting: false},
{waiting: false},
name,
answer,
this.state.contactEmail,
true
);
}
handleSaveEmailClick (email) {
if (isEmailValid (email)) {
this.setState ({waiting: true});
this.sendSkill (
this.state.allIntents,
true,
{contactEmail: email, waiting: false},
{waiting: false},
this.state.invocationName,
this.state.invocationAnswer,
email,
false
);
} else {
Popup.alert ('Please enter valid email');
}
}
handleDeleteIntentClick (selectedIntent) {
let id = -1;
//TODO : Change comparsion method ! Same object with different proeprty sorting will not be same string
this.state.allIntents.map ((intent, index) => {
if (
id === -1 &&
JSON.stringify (selectedIntent) === JSON.stringify (intent)
)
id = index;
});
if (id !== -1) {
try {
let newAllIntentsJSON = JSON.stringify (this.state.allIntents);
let newAllIntents = JSON.parse (newAllIntentsJSON);
newAllIntents.splice (id, 1);
this.setState ({waiting: true});
let newState = {
allIntents: newAllIntents,
selectedIntent: {intentName: '', questions: [''], answer: ''},
waiting: false,
};
this.sendSkill (
newAllIntents,
true,
newState,
{waiting: false},
this.state.invocationName,
this.state.invocationAnswer,
this.state.contactEmail,
true
);
} catch (e) {
console.log ('error : ' + e);
}
}
}
handleSaveIntentClick (selectedIntent) {
if (selectedIntent.intentName.length < INTENT_NAME_MIN_LENGTH) {
Popup.alert ('Question name should have at least 2 characters');
return;
}
if (
selectedIntent.answerType === ANSWER_TYPE.PREDEFINED &&
selectedIntent.answer.length < ANSWER_MIN_LENGTH
) {
Popup.alert ('Answer should have at least 2 characters');
return;
}
for (let i = 0; i < selectedIntent.questions.length; i++) {
if (selectedIntent.questions[i].length < QUESTION_MIN_LENGTH) {
Popup.alert ('Question variant should have at least 2 characters');
return;
}
}
//Check for same question variants and same question name in other intents
//all intents with the same intentName, or some of the questions are the same
//will be kept in filteredIntents. After filterring, there should be only one
//intent left, the selected one
let selectedIntentQuestionsForSearch = selectedIntent.questions.map(question=>
question.toLowerCase().trim());
let filteredIntents = this.state.allIntents.filter(intent=>{
let result = (selectedIntent.intentName.toLowerCase().trim() === intent.intentName.toLowerCase().trim());
let filteredQuestions = intent.questions.filter(question=>{
return (selectedIntentQuestionsForSearch.indexOf(question.toLowerCase().trim())!==-1);
});
return (result || filteredQuestions.length > 0);
});
if (filteredIntents.length > 1){
Popup.alert('Question name or question variant already exists');
return;
}
let newAllIntentsJSON = JSON.stringify (this.state.allIntents);
let newAllIntents = JSON.parse (newAllIntentsJSON);
let resolveState = null;
let rejectState = {waiting: false};
if (this.state.selectedIndex === NEW_INTENT_SELECTED_INDEX) {
//new intent
newAllIntents.push (selectedIntent);
resolveState = {
allIntents: newAllIntents,
selectedIntent: selectedIntent,
selectedIndex: newAllIntents.length - 1,
waiting: false,
};
} else {
newAllIntents[this.state.selectedIndex] = selectedIntent;
resolveState = {
allIntents: newAllIntents,
selectedIntent: selectedIntent,
waiting: false,
};
}
this.setState ({waiting: true});
this.sendSkill (
newAllIntents,
true,
resolveState,
rejectState,
this.state.invocationName,
this.state.invocationAnswer,
this.state.contactEmail,
true
);
}
handleAddIntentClick () {
this.setState ({
allIntents: this.state.allIntents,
selectedIndex: NEW_INTENT_SELECTED_INDEX,
launchRequest: false,
selectedIntent: {
intentName: '',
questions: [''],
answer: '',
intentExplanation: '',
answerType: ANSWER_TYPE.PREDEFINED,
externalAnswerSource: '',
},
});
}
sendSkill (
newAllIntents,
showPopUp,
resolveState,
rejectState,
newName,
newAnswer,
email,
updateOnAmazon
) {
return new Promise ((resolve, reject) => {
updateSkill (
this.createSkill (
newAllIntents,
newName,
newAnswer,
email,
updateOnAmazon
)
)
.then (l => l.json ())
.then (result => {
if (result.result !== RESULT_CODES.OK) {
console.log (result);
if (showPopUp)
Popup.alert ('Model was not saved. Please try again');
this.setState (rejectState);
//reject('Error code : ' + jResult.result);
} else {
if (showPopUp) Popup.alert ('Saved');
this.setState (resolveState);
resolve ();
}
})
.catch (e => {
console.log ('error : ' + e);
if (showPopUp) Popup.alert ('Model was not saved. Please try again');
this.setState (rejectState);
//reject(e);
});
});
}
}
export default App;

View File

@@ -1,60 +0,0 @@
import React, {Component} from 'react';
import {Button} from 'react-md';
import AnswerSourceForm from './helper/AnswerSourceForm';
import '../css/components/IntentDetails.css';
class AnswerSource extends Component {
constructor (props) {
super (props);
this.state = {
isModalOpen: false,
answerType: this.props.answerType
};
}
onOpen () {
this.setState ({
isModalOpen: true,
answerType: this.props.answerType
});
}
onClose () {
this.setState ({
isModalOpen: false,
});
}
onSave(){
this.onClose();
this.props.onSaveAnswerType(this.state.answerType);
}
onSourceChange(value, event){
this.setState({answerType:parseInt(value,10)});
}
render () {
let modal;
if (this.state.isModalOpen) {
modal = <AnswerSourceForm
isModalOpen={this.state.isModalOpen}
answerType={this.state.answerType}
onSave={this.onSave.bind(this)}
onClose={this.onClose.bind(this)}
onSourceChange={this.onSourceChange.bind(this)}
/>
}
return (
<div>
<Button flat primary onClick={this.onOpen.bind (this)}>
Answer type
</Button>
{modal}
</div>
);
}
}
export default AnswerSource;

View File

@@ -1,50 +0,0 @@
import React, { Component } from 'react';
import {Button, TextField} from 'react-md';
import '../css/Common.css';
import '../css/components/Contact.css';
import {EMAIL_MAX_LENGTH} from '../config/constants';
class Contact extends Component {
constructor(props){
super(props);
this.state = {contactEmail: props.contactEmail};
this.handleEmailEdit = this.handleEmailEdit.bind(this);
}
componentWillReceiveProps(props){
this.setState({contactEmail: props.contactEmail});
}
render() {
return (
<div className="RightPanelBox">
<h5 className="PanelSubTitle"> Contact address will be used for direct messaging through Alexa </h5>
<TextField
id="contact email"
lineDirection="center"
label="Contact email"
className="md-cell md-cell--bottom ContactEmailInput"
maxLength={EMAIL_MAX_LENGTH}
onChange={this.handleEmailEdit}
value={this.state.contactEmail}/>
<br></br>
<br></br>
<br></br>
<Button className="SaveButton" flat primary swapTheming
onClick={()=>{this.props.onSaveEmailClick(this.state.contactEmail)}}
disabled={this.props.waiting}>Save</Button>
</div>
);
}
handleEmailEdit(e){
const isEmailValid = e.length < EMAIL_MAX_LENGTH;
if (isEmailValid){
this.setState({contactEmail: e});
}
}
}
export default Contact;

View File

@@ -1,209 +0,0 @@
import React, {Component} from 'react';
import {Button, SVGIcon, TextField} from 'react-md';
import AnswerSource from './AnswerSource.js';
import '../css/components/IntentDetails.css';
import '../css/Common.css';
import {
QUESTION_MAX_LENGTH,
ANSWER_MAX_LENGTH,
INTENT_NAME_MAX_LENGTH,
INTENT_EXPLANATION_MAX_LENGTH,
ANSWER_TYPE,
} from '../config/constants';
import AnswerTextBox from './helper/AnswerTextBox.js';
class IntentDetails extends Component {
constructor (props) {
super (props);
this.state = {intent: props.selectedIntent};
this.addQuestion = this.addQuestion.bind (this);
this.deleteQuestion = this.deleteQuestion.bind (this);
this.handleQuestionEdit = this.handleQuestionEdit.bind (this);
this.handleIntentNameEdit = this.handleIntentNameEdit.bind (this);
this.handleIntentExplanationEdit = this.handleIntentExplanationEdit.bind (
this
);
}
componentWillReceiveProps (props) {
this.setState ({intent: props.selectedIntent});
}
render () {
return (
<div className="RightPanelBox">
<div className="QuestionBox">
<h5 className="PanelSubTitle">
{' '}
In introduction, Alexa will help users to ask her the right questions about your business. For Example, she will say : "To ask us about our services, say : What do you do ? ". What do you do ? is defined in question field. Alexa will use first variation of question in intro.
</h5>
<TextField
id="intent explanation"
lineDirection="center"
placeholder="To ask us about our services, say "
className="md-cell md-cell--bottom IntentDetailsInputBoxes"
onChange={this.handleIntentExplanationEdit}
maxLength={INTENT_EXPLANATION_MAX_LENGTH}
value={this.state.intent.intentExplanation}
/>
<br />
<TextField
id="intent name"
lineDirection="center"
label="Question name"
className="md-cell md-cell--bottom IntentDetailsInputBoxes"
onChange={this.handleIntentNameEdit}
maxLength={INTENT_NAME_MAX_LENGTH}
value={this.state.intent.intentName}
/>
</div>
<h5 className="QuestionTitle">Question variants</h5>
{this.state.intent.questions.map ((question, index) => {
return (
<div key={index} className="QuestionBox">
<TextField
id="intent question"
lineDirection="center"
placeholder="Question"
className="md-cell md-cell--bottom IntentDetailsInputBoxes"
maxLength={QUESTION_MAX_LENGTH}
rightIcon={
<SVGIcon
onClick={() => {
this.deleteQuestion (index);
}}
>
{' '}
<path
fill="#000000"
d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z"
/>
{' '}
</SVGIcon>
}
onChange={e => {
this.handleQuestionEdit (e, index);
}}
value={question}
/>
</div>
);
})}
<Button
className="AddQuestionVariantButton"
icon
primary
onClick={this.addQuestion}
disabled={this.props.waiting}
>
add
</Button>
<AnswerSource
className="AnswerTypeButton"
onSaveAnswerType={this.handleExternalSourceSave.bind(this)}
answerType={this.state.intent.answerType}
/>
<AnswerTextBox
answerType={this.state.intent.answerType}
externalAnswerSource={this.state.intent.externalAnswerSource}
handleAnswerSourceEdit={this.handleAnswerSourceEdit.bind(this)}
handleAnswerEdit={this.handleAnswerEdit.bind(this)}
answer={this.state.intent.answer}
/>
<Button
className="IntentDetailsButton-firstInRow"
flat
primary
swapTheming
onClick={() => {
this.props.onSaveIntentClick (this.state.intent);
}}
disabled={this.props.waiting}
>
Save
</Button>
<Button
className="IntentDetailsButton"
flat
primary
onClick={() => {
this.props.onDeleteIntentClick (this.state.intent);
}}
disabled={this.props.waiting}
>
Delete
</Button>
</div>
);
}
addQuestion () {
let newIntent = this.state.intent;
newIntent.questions.push ('');
this.setState ({intent: newIntent});
}
deleteQuestion (index) {
if (this.state.intent.questions.length > 1) {
let newIntent = this.state.intent;
if (index >= 0 && index < newIntent.questions.length) newIntent.questions.splice (index, 1);
this.setState ({intent: newIntent});
}
}
handleQuestionEdit (e, index) {
const isQuestionValid = e.length < QUESTION_MAX_LENGTH && /^[a-z,.' ]*$/i.test (e);
if (isQuestionValid){
let newIntent = this.state.intent;
newIntent.questions[index] = e;
this.setState ({intent: newIntent});
}
}
handleIntentExplanationEdit (e, index) {
const isExplanationValid = e.length < INTENT_EXPLANATION_MAX_LENGTH && /^[a-z,.' ]*$/i.test (e);
if (isExplanationValid){
let newIntent = this.state.intent;
newIntent.intentExplanation = e;
this.setState ({intent: newIntent});
}
}
handleAnswerEdit (e) {
const isAnswerValid = e.length < ANSWER_MAX_LENGTH && /^[a-z,.' ]*$/i.test (e);
if (isAnswerValid){
let newIntent = this.state.intent;
newIntent.answer = e;
this.setState ({intent: newIntent});
}
}
handleAnswerSourceEdit (e) {
const isAnswerSourceValid = e.length < ANSWER_MAX_LENGTH;
if (isAnswerSourceValid){
let newIntent = this.state.intent;
newIntent.externalAnswerSource = e;
this.setState ({intent: newIntent});
}
}
handleIntentNameEdit (e) {
const isIntentNameValid = e.length < INTENT_NAME_MAX_LENGTH && /^[a-z]*$/i.test (e);
if (isIntentNameValid){
let newIntent = this.state.intent;
newIntent.intentName = e;
this.setState ({intent: newIntent});
}
}
handleExternalSourceSave (answerType) {
let newIntent = this.state.intent;
newIntent.answerType = answerType;
this.setState ({intent: newIntent});
}
}
export default IntentDetails;

View File

@@ -1,35 +0,0 @@
import React, { Component } from 'react';
import {Button} from 'react-md';
import '../css/components/IntentItem.css'
import {INTENT_TITLE_MAX_LENGTH, INTENT_TITLE_TOOLTIP_DELAY} from '../config/constants'
class IntentItem extends Component {
constructor(props){
super(props);
this.state={intent: props.intent, index: props.index, onClick: props.onClick};
}
render() {
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 (
<div>
<Button className={this.props.selectedIndex===this.state.index ? 'IntentItem-selected' : 'IntentItem'}
onClick={()=>{this.state.onClick(this.state.intent,this.state.index)}}
flat
disabled={this.props.waiting}
tooltipDelay={INTENT_TITLE_TOOLTIP_DELAY}
tooltipLabel={this.state.intent.intentName.length>INTENT_TITLE_MAX_LENGTH ? this.state.intent.questions[0] : ''}>
{buttonTitle}
</Button>
<br></br>
</div>
);
}
}
export default IntentItem;

View File

@@ -1,54 +0,0 @@
import React, { Component } from 'react';
import {Button} from 'react-md';
import IntentItem from './IntentItem';
import '../css/components/IntentList.css';
import {
LAUNCH_REQUEST_SELECTED_INDEX,
CONTACT_SELECTED_INDEX} from '../config/constants'
class IntentList extends Component {
constructor (props){
super(props);
this.state = {intents: props.allIntents, selectedIndex:props.selectedIndex, onIntentClick:props.onIntentClick};
}
componentWillReceiveProps(props){
this.setState({intents: props.allIntents, selectedIndex: props.selectedIndex, onIntentClick: props.onIntentClick});
}
render() {
return (
<div className="IntentList">
<Button className={this.props.selectedIndex===LAUNCH_REQUEST_SELECTED_INDEX ? "LaunchRequestButton-selected" : "LaunchRequestButton"} flat primary
onClick={this.props.onLaunchRequestClick}
disabled={this.props.waiting} >Launch request</Button>
<Button className={this.props.selectedIndex===CONTACT_SELECTED_INDEX ? "ContactButton-selected" : "ContactButton"} flat primary
onClick={this.props.onContactClick}
disabled={this.props.waiting} >Contact</Button>
<div className="IntentList-title">
<h3>Questions</h3>
</div>
{
this.state.intents.map((intent,index)=>{
return <IntentItem
key={intent.intentName} intent={intent} index={index}
selectedIndex={this.props.selectedIndex}
onClick={this.state.onIntentClick}
waiting={this.props.waiting}>
</IntentItem>
})
}
<br></br>
<Button className="AddIntent" flat primary swapTheming
onClick={this.props.onAddIntentClick}
disabled={this.props.waiting}>Add question</Button>
</div>
);
}
}
export default IntentList;

View File

@@ -1,69 +0,0 @@
import React, { Component } from 'react';
import {Button, TextField} from 'react-md';
import '../css/Common.css';
import '../css/components/LaunchRequest.css';
import { INVOCATION_NAME_MAX_LENGTH, INVOCATION_ANSWER_MAX_LENGTH } from '../config/constants';
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 (
<div className="RightPanelBox">
<h5 className="PanelSubTitle"> Invocation name customers use to activate the skill. For example "Open Saburly" or "Talk to Saburly" </h5>
<TextField
id="invocation name"
lineDirection="center"
placeholder="saburly"
label="Invocation name"
className="md-cell md-cell--bottom InvocationInputBoxes"
maxLength={INVOCATION_NAME_MAX_LENGTH}
onChange={this.handleNameEdit}
value={this.state.invocationName}/>
<br></br>
<h5 className="PanelSubTitle" >Answer customers get from Alexa when they activate the skill.</h5>
<TextField
id="invocation answer"
lineDirection="center"
placeholder="We are Saburly, ask us something about us"
label="Answer"
className="md-cell md-cell--bottom InvocationInputBoxes"
maxLength={INVOCATION_ANSWER_MAX_LENGTH}
onChange={this.handleAnswerEdit}
value={this.state.invocationAnswer}/>
<br></br>
<br></br>
<Button className="SaveButton" flat primary swapTheming
onClick={()=>{this.props.onSaveClick(this.state.invocationName, this.state.invocationAnswer)}}
disabled={this.props.waiting}>Save</Button>
</div>
);
}
handleNameEdit(e){
const isInvocationNameValid = e.length < INVOCATION_NAME_MAX_LENGTH && (/^[a-z,.' ]*$/.test(e));
if (isInvocationNameValid) {
this.setState({invocationName: e});
}
}
handleAnswerEdit(e){
const isInvocationAnswerValid = e.length < INVOCATION_ANSWER_MAX_LENGTH && (/^[a-z,.' ]*$/i.test(e));
if (isInvocationAnswerValid){
this.setState({invocationAnswer: e});
}
}
}
export default LaunchRequest;

View File

@@ -1,61 +0,0 @@
import React from 'react';
import {Button} from 'react-md';
import {shallow, mount} from 'enzyme';
import AnswerSource from '../AnswerSource';
import {ANSWER_TYPE} from '../../config/constants';
it ('renders without crashing', () => {
shallow (<AnswerSource />);
});
describe ('functional tests', () => {
let wrapper;
beforeEach (() => {
const onSaveAnswerTypeFunction = jest.fn ();
wrapper = mount (
<AnswerSource onSaveAnswerType={onSaveAnswerTypeFunction} />
);
wrapper.setState ({
isModalOpen: false,
answerType: 0,
});
});
it ('snapshot', ()=>{
expect(wrapper).toMatchSnapshot();
});
it ('renders only a button', () => {
expect (wrapper.first ().text ()).toEqual ('Answer type');
expect (wrapper.find ('AnswerSourceForm').exists ()).toEqual (false);
});
it ('answer type button click opens modal form', () => {
expect (wrapper.find ('AnswerSourceFormA').exists ()).toEqual (false);
expect (wrapper.state ().isModalOpen).toEqual (false);
const AnswerTypeButton = wrapper.find ('button').first ();
AnswerTypeButton.simulate ('click');
expect (wrapper.state ().isModalOpen).toEqual (true);
expect (wrapper.find ('AnswerSourceForm').exists ()).toEqual (true);
expect (wrapper.find ('button').length).toBe (3);
expect (wrapper.find ('button').first ().text ()).toEqual ('Answer type');
});
it ('save button changes answerType value in state and closes the form ', () => {
const AnswerTypeButton = wrapper.find ('button').first ();
AnswerTypeButton.simulate ('click');
const saveButton = wrapper.find ('button').at (2);
const optionControl = wrapper.find ('SelectionControlGroup');
expect (saveButton.text ()).toEqual ('Save');
expect (optionControl.exists ()).toEqual (true);
optionControl.simulate ('change', {
target: {value: String (ANSWER_TYPE.EXTERNAL_SOURCE_WP_NEWS)},
});
saveButton.simulate ('click');
expect (wrapper.state ().answerType).toBe (
ANSWER_TYPE.EXTERNAL_SOURCE_WP_NEWS
);
expect (wrapper.state ().isModalOpen).toEqual (false);
expect (wrapper.find ('button').length).toBe (1);
});
});

View File

@@ -1,318 +0,0 @@
import React from 'react';
import {shallow, mount} from 'enzyme';
import IntentDetails from '../IntentDetails';
import {
QUESTION_MAX_LENGTH,
ANSWER_MAX_LENGTH,
INTENT_NAME_MAX_LENGTH,
INTENT_EXPLANATION_MAX_LENGTH,
ANSWER_TYPE,
} from '../../config/constants';
it ('renders without crashing', () => {
const dummyIntent = {
questions: ['q1', 'q2'],
};
shallow (<IntentDetails selectedIntent={dummyIntent} />);
});
describe('complete testing', () => {
let wrapper;
let newIntent;
beforeEach(()=>{
newIntent = {
intentName: '',
intentExplanation: '',
questions: [''],
answer: '',
answerType: ANSWER_TYPE.PREDEFINED,
externalAnswerSource: '',
};
wrapper = mount(<IntentDetails selectedIntent={newIntent} />);
expect(wrapper.state('intent')).toEqual(newIntent);
});
it ('snapshot', () =>{
expect(wrapper).toMatchSnapshot();
});
it ('renders correctly for new intent input when empty intent is sent', () => {
expect(wrapper.find('TextField').length).toBe(4);
expect(wrapper.find('Button').length).toBe(4);
expect(wrapper.find('TextField').at(0).props().id).toEqual('intent explanation');
expect(wrapper.find('TextField').at(1).props().id).toEqual('intent name');
expect(wrapper.find('TextField').at(2).props().id).toEqual('intent question');
expect(wrapper.find('TextField').at(3).props().id).toEqual('intent answer');
expect(wrapper.find('Button').at(0).props().children).toEqual('add');
expect(wrapper.find('Button').at(1).props().children).toEqual('Answer type');
expect(wrapper.find('Button').at(2).props().children).toEqual('Save');
expect(wrapper.find('Button').at(3).props().children).toEqual('Delete');
});
it ('receives correct props for non empty intent with predefined answer', () => {
newIntent = {
intentName: 'Dummy intent',
intentExplanation: 'Dummy explanation',
questions: ['Dummy question'],
answer: 'dummy answer',
answerType: ANSWER_TYPE.PREDEFINED,
externalAnswerSource: '',
};
wrapper = mount(<IntentDetails selectedIntent={newIntent} />);
expect(wrapper.state('intent')).toEqual(newIntent);
});
it ('receives correct props for non empty intent with external source for answer', () => {
newIntent = {
intentName: 'Dummy intent',
intentExplanation: 'Dummy explanation',
questions: ['Dummy question'],
answer: '',
answerType: ANSWER_TYPE.EXTERNAL_SOURCE_WP_NEWS,
externalAnswerSource: 'http://sarajevotimes.com',
};
wrapper = mount(<IntentDetails selectedIntent={newIntent} />);
expect(wrapper.state('intent')).toEqual(newIntent);
});
it ('adds text field when add button is clicked', () => {
const addButton = wrapper.find('button').at(0);
addButton.simulate('click');
expect(wrapper.find('TextField').length).toBe(5);
addButton.simulate('click');
expect(wrapper.find('TextField').length).toBe(6);
expect(wrapper.find('TextField').at(0).props().id).toEqual('intent explanation');
expect(wrapper.find('TextField').at(1).props().id).toEqual('intent name');
expect(wrapper.find('TextField').at(2).props().id).toEqual('intent question');
expect(wrapper.find('TextField').at(3).props().id).toEqual('intent question');
expect(wrapper.find('TextField').at(4).props().id).toEqual('intent question');
expect(wrapper.find('TextField').at(5).props().id).toEqual('intent answer');
expect(wrapper.state('intent').questions.length).toBe(3);
});
it ('removes correct text field when delete button on text field is clicked', () => {
const addButton = wrapper.find('button').at(0);
addButton.simulate('click');
addButton.simulate('click');
let firstQuestionTextField = wrapper.find('TextField').at(2);
let secondQuestionTextField = wrapper.find('TextField').at(3);
let thirdQuestionTextField = wrapper.find('TextField').at(4);
firstQuestionTextField.instance().props.onChange('first question');
secondQuestionTextField.instance().props.onChange('second question');
thirdQuestionTextField.instance().props.onChange('third question');
expect(firstQuestionTextField.instance().value).toEqual('first question');
expect(secondQuestionTextField.instance().value).toEqual('second question');
expect(thirdQuestionTextField.instance().value).toEqual('third question');
expect(wrapper.state('intent').questions.length).toBe(3);
const rightIcon = secondQuestionTextField.props().rightIcon;
rightIcon.props.onClick(secondQuestionTextField.props().key);
expect(wrapper.state('intent').questions.length).toBe(2);
expect(secondQuestionTextField.instance().value).toEqual('third question');
expect(thirdQuestionTextField.instance()._field._field).toBeNull();
});
it ('does not remove text field when it is only one left', () => {
let firstQuestionTextField = wrapper.find('TextField').at(2);
firstQuestionTextField.instance().props.onChange('first question');
expect(firstQuestionTextField.props().id).toEqual('intent question');
expect(wrapper.state('intent').questions.length).toBe(1);
const rightIcon = firstQuestionTextField.props().rightIcon;
rightIcon.props.onClick(firstQuestionTextField.props().key);
expect(wrapper.state('intent').questions.length).toBe(1);
expect(firstQuestionTextField.instance().value).toEqual('first question');
});
it ('accepts text without special characters for intent explanation', () => {
let explanationTextField = wrapper.find('TextField').at(0);
let validExplanationText = 'to get latest news, say ';
explanationTextField.instance().props.onChange(validExplanationText);
expect(wrapper.state('intent').intentExplanation).toEqual(validExplanationText);
expect(explanationTextField.instance().value).toEqual(validExplanationText);
});
it ('does not accept text with special characters for intent explanation', () => {
let explanationTextField = wrapper.find('TextField').at(0);
let invalidExplanationText = '554to get latest news, say #$ ';
explanationTextField.instance().props.onChange(invalidExplanationText);
expect(wrapper.state('intent').intentExplanation).toEqual('');
expect(explanationTextField.instance().value).toEqual('');
});
it ('does not accept too long text for intent explanation', () => {
let explanationTextField = wrapper.find('TextField').at(0);
let invalidExplanationText = new Array(INTENT_EXPLANATION_MAX_LENGTH + 10).join('A');
explanationTextField.instance().props.onChange(invalidExplanationText);
expect(wrapper.state('intent').intentExplanation).toEqual('');
expect(explanationTextField.instance().value).toEqual('');
});
it ('accepts text without special characters for intent name', () => {
let intentNameTextField = wrapper.find('TextField').at(1);
let validIntentNameText = 'intentName';
intentNameTextField.instance().props.onChange(validIntentNameText);
expect(wrapper.state('intent').intentName).toEqual(validIntentNameText);
expect(intentNameTextField.instance().value).toEqual(validIntentNameText);
});
it ('does not accept text with speces characters for intent name', () => {
let intentNameTextField = wrapper.find('TextField').at(1);
let invalidIntentNameText = 'intentName with space';
intentNameTextField.instance().props.onChange(invalidIntentNameText);
expect(wrapper.state('intent').intentName).toEqual('');
expect(intentNameTextField.instance().value).toEqual('');
});
it ('does not accept text with special characters for intent name', () => {
let intentNameTextField = wrapper.find('TextField').at(1);
let invalidIntentNameText = 'intentName23!';
intentNameTextField.instance().props.onChange(invalidIntentNameText);
expect(wrapper.state('intent').intentName).toEqual('');
expect(intentNameTextField.instance().value).toEqual('');
});
it ('does not accept too long text for intent name', () => {
let intentNameTextField = wrapper.find('TextField').at(1);
let invalidIntentNameText = new Array(INTENT_NAME_MAX_LENGTH + 10).join('A');
intentNameTextField.instance().props.onChange(invalidIntentNameText);
expect(wrapper.state('intent').intentName).toEqual('');
expect(intentNameTextField.instance().value).toEqual('');
});
it ('accepts text without special characters for question text', () => {
let questionTextField = wrapper.find('TextField').at(2);
let validQuestionText = 'read me latest news'
questionTextField.instance().props.onChange(validQuestionText);
expect(wrapper.state('intent').questions).toEqual([validQuestionText]);
expect(questionTextField.instance().value).toEqual(validQuestionText);
});
it ('does not accept text with special characters for question text', () => {
let questionTextField = wrapper.find('TextField').at(2);
let invalidQuestionText = 'read m3 1at35t news #'
questionTextField.instance().props.onChange(invalidQuestionText);
expect(wrapper.state('intent').questions).toEqual(['']);
expect(questionTextField.instance().value).toEqual('');
});
it ('does not accept too long text for question text', () => {
let questionTextField = wrapper.find('TextField').at(2);
let invalidQuestionText = new Array(QUESTION_MAX_LENGTH + 10).join('A');
questionTextField.instance().props.onChange(invalidQuestionText);
expect(wrapper.state('intent').questions).toEqual(['']);
expect(questionTextField.instance().value).toEqual('');
});
it ('accepts text without special characters for answer text', () => {
let answerTextField = wrapper.find('TextField').at(3);
let validAnswerText = 'this is valid answer.'
answerTextField.instance().props.onChange(validAnswerText);
expect(wrapper.state('intent').answer).toEqual(validAnswerText);
expect(answerTextField.instance().value).toEqual(validAnswerText);
});
it ('does not accept text with special characters for answer text', () => {
let answerTextField = wrapper.find('TextField').at(3);
let invalidAnswerText = 'this is invalid answer.0123'
answerTextField.instance().props.onChange(invalidAnswerText);
expect(wrapper.state('intent').answer).toEqual('');
expect(answerTextField.instance().value).toEqual('');
});
it ('does not accept too long text for answer text', () => {
let answerTextField = wrapper.find('TextField').at(3);
let invalidAnswerText = new Array(ANSWER_MAX_LENGTH + 10).join('A');
answerTextField.instance().props.onChange(invalidAnswerText);
expect(wrapper.state('intent').answer).toEqual('');
expect(answerTextField.instance().value).toEqual('');
});
it ('accepts text for external source as answer', () => {
newIntent = {
intentName: '',
intentExplanation: '',
questions: [''],
answer: '',
answerType: ANSWER_TYPE.EXTERNAL_SOURCE_WP_NEWS,
externalAnswerSource: '',
};
wrapper = mount(<IntentDetails selectedIntent={newIntent} />);
let answerTextField = wrapper.find('TextField').at(3);
let validAnswerText = 'http://sarajevotimes.com'
answerTextField.instance().props.onChange(validAnswerText);
expect(wrapper.state('intent').externalAnswerSource).toEqual(validAnswerText);
expect(answerTextField.instance().value).toEqual(validAnswerText);
});
it ('does not accept too long text for external source as answer', () => {
newIntent = {
intentName: '',
intentExplanation: '',
questions: [''],
answer: '',
answerType: ANSWER_TYPE.EXTERNAL_SOURCE_WP_NEWS,
externalAnswerSource: '',
};
wrapper = mount(<IntentDetails selectedIntent={newIntent} />);
let answerTextField = wrapper.find('TextField').at(3);
let invalidAnswerText = new Array(ANSWER_MAX_LENGTH + 10).join('A');
answerTextField.instance().props.onChange(invalidAnswerText);
expect(wrapper.state('intent').answer).toEqual('');
expect(answerTextField.instance().value).toEqual('');
});
it ('calls function with correct data on save button click', () => {
newIntent = {
intentName: 'Dummy intent',
intentExplanation: 'Dummy explanation',
questions: ['Dummy question'],
answer: 'Dummy answer',
answerType: ANSWER_TYPE.PREDEFINED,
externalAnswerSource: '',
};
const onSaveFunction = jest.fn();
wrapper = mount(<IntentDetails selectedIntent={newIntent} onSaveIntentClick={onSaveFunction} />);
wrapper.find('Button').at(2).simulate('click');
expect(onSaveFunction).toBeCalledWith(newIntent);
});
it ('calls function with correct data on delete button click', () => {
newIntent = {
intentName: 'Dummy intent',
intentExplanation: 'Dummy explanation',
questions: ['Dummy question'],
answer: 'Dummy answer',
answerType: ANSWER_TYPE.PREDEFINED,
externalAnswerSource: '',
};
const onSaveFunction = jest.fn();
wrapper = mount(<IntentDetails selectedIntent={newIntent} onDeleteIntentClick={onSaveFunction} />);
wrapper.find('Button').at(3).simulate('click');
expect(onSaveFunction).toBeCalledWith(newIntent);
});
});

View File

@@ -1,93 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`functional tests snapshot 1`] = `
<AnswerSource
onSaveAnswerType={[Function]}
>
<div>
<withInk(withTooltip(Button))
flat={true}
inkTransitionEnterTimeout={450}
inkTransitionLeaveTimeout={300}
inkTransitionOverlap={150}
onClick={[Function]}
primary={true}
>
<withTooltip(Button)
flat={true}
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
onClick={[Function]}
primary={true}
>
<Button
fixedPosition="br"
flat={true}
iconBefore={true}
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
onClick={[Function]}
primary={true}
type="button"
>
<button
className="md-btn md-btn--flat md-btn--text md-pointer--hover md-text--theme-primary md-ink--primary md-inline-block"
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
type="button"
>
<InkContainer
key="ink-container"
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
>
<TransitionGroup
childFactory={[Function]}
className="md-ink-container"
component="div"
>
<div
className="md-ink-container"
/>
</TransitionGroup>
</InkContainer>
Answer type
</button>
</Button>
</withTooltip(Button)>
</withInk(withTooltip(Button))>
</div>
</AnswerSource>
`;

View File

@@ -1,898 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`complete testing snapshot 1`] = `
<IntentDetails
selectedIntent={
Object {
"answer": "",
"answerType": 0,
"externalAnswerSource": "",
"intentExplanation": "",
"intentName": "",
"questions": Array [
"",
],
}
}
>
<div
className="RightPanelBox"
>
<div
className="QuestionBox"
>
<h5
className="PanelSubTitle"
>
In introduction, Alexa will help users to ask her the right questions about your business. For Example, she will say : "To ask us about our services, say : What do you do ? ". What do you do ? is defined in question field. Alexa will use first variation of question in intro.
</h5>
<TextField
className="md-cell md-cell--bottom IntentDetailsInputBoxes"
fullWidth={true}
id="intent explanation"
leftIconStateful={true}
lineDirection="center"
maxLength={70}
onChange={[Function]}
passwordIcon={
<FontIcon
iconClassName="material-icons"
>
remove_red_eye
</FontIcon>
}
placeholder="To ask us about our services, say "
rightIconStateful={true}
type="text"
value=""
>
<div
className="md-text-field-container md-full-width md-text-field-container--input md-cell md-cell--bottom IntentDetailsInputBoxes"
onClick={[Function]}
>
<FloatingLabel
active={false}
error={false}
floating={false}
htmlFor="intent explanation"
iconOffset={false}
key="label"
/>
<InputField
className=""
fullWidth={true}
id="intent explanation"
inlineIndicator={false}
key="field"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="To ask us about our services, say "
type="text"
value=""
>
<input
className="md-text-field md-text-field--margin md-full-width md-text"
id="intent explanation"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="To ask us about our services, say "
type="text"
value=""
/>
</InputField>
<TextFieldDivider
active={false}
error={false}
key="text-divider"
lineDirection="center"
>
<Divider
className="md-divider--text-field md-divider--expand-from-center"
>
<hr
className="md-divider md-divider--text-field md-divider--expand-from-center"
/>
</Divider>
</TextFieldDivider>
<TextFieldMessage
active={false}
currentLength={0}
error={false}
key="message"
leftIcon={false}
maxLength={70}
rightIcon={false}
>
<div
className="md-text-field-message-container md-text-field-message-container--count-only md-full-width md-text--disabled"
>
<Message
active={false}
key="message"
/>
<Message
active={false}
className="md-text-field-message--counter"
key="counter"
>
<div
aria-hidden={true}
className="md-text-field-message md-text-field-message--inactive md-text-field-message--counter"
>
0 / 70
</div>
</Message>
</div>
</TextFieldMessage>
</div>
</TextField>
<br />
<TextField
className="md-cell md-cell--bottom IntentDetailsInputBoxes"
fullWidth={true}
id="intent name"
label="Question name"
leftIconStateful={true}
lineDirection="center"
maxLength={30}
onChange={[Function]}
passwordIcon={
<FontIcon
iconClassName="material-icons"
>
remove_red_eye
</FontIcon>
}
rightIconStateful={true}
type="text"
value=""
>
<div
className="md-text-field-container md-full-width md-text-field-container--input md-cell md-cell--bottom IntentDetailsInputBoxes"
onClick={[Function]}
>
<FloatingLabel
active={false}
error={false}
floating={false}
htmlFor="intent name"
iconOffset={false}
key="label"
label="Question name"
>
<label
className="md-floating-label md-floating-label--inactive md-floating-label--inactive-sized md-text--secondary"
htmlFor="intent name"
>
Question name
</label>
</FloatingLabel>
<InputField
className=""
fullWidth={true}
id="intent name"
inlineIndicator={false}
key="field"
label="Question name"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder={null}
type="text"
value=""
>
<input
className="md-text-field md-text-field--floating-margin md-full-width md-text"
id="intent name"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder={null}
type="text"
value=""
/>
</InputField>
<TextFieldDivider
active={false}
error={false}
key="text-divider"
lineDirection="center"
>
<Divider
className="md-divider--text-field md-divider--expand-from-center"
>
<hr
className="md-divider md-divider--text-field md-divider--expand-from-center"
/>
</Divider>
</TextFieldDivider>
<TextFieldMessage
active={false}
currentLength={0}
error={false}
key="message"
leftIcon={false}
maxLength={30}
rightIcon={false}
>
<div
className="md-text-field-message-container md-text-field-message-container--count-only md-full-width md-text--disabled"
>
<Message
active={false}
key="message"
/>
<Message
active={false}
className="md-text-field-message--counter"
key="counter"
>
<div
aria-hidden={true}
className="md-text-field-message md-text-field-message--inactive md-text-field-message--counter"
>
0 / 30
</div>
</Message>
</div>
</TextFieldMessage>
</div>
</TextField>
</div>
<h5
className="QuestionTitle"
>
Question variants
</h5>
<div
className="QuestionBox"
key="0"
>
<TextField
className="md-cell md-cell--bottom IntentDetailsInputBoxes"
fullWidth={true}
id="intent question"
leftIconStateful={true}
lineDirection="center"
maxLength={150}
onChange={[Function]}
passwordIcon={
<FontIcon
iconClassName="material-icons"
>
remove_red_eye
</FontIcon>
}
placeholder="Question"
rightIcon={
<SVGIcon
focusable="false"
onClick={[Function]}
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z"
fill="#000000"
/>
</SVGIcon>
}
rightIconStateful={true}
type="text"
value=""
>
<div
className="md-text-field-container md-full-width md-text-field-container--input md-cell md-cell--bottom IntentDetailsInputBoxes"
onClick={[Function]}
>
<FloatingLabel
active={false}
error={false}
floating={false}
htmlFor="intent question"
iconOffset={false}
key="label"
/>
<div
className="md-text-field-icon-container"
key="icon-divider"
>
<div
className="md-text-field-divider-container md-text-field-divider-container--grow"
key="divider-container"
>
<InputField
className=""
fullWidth={true}
id="intent question"
inlineIndicator={false}
key="field"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="Question"
type="text"
value=""
>
<input
className="md-text-field md-text-field--margin md-full-width md-text"
id="intent question"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="Question"
type="text"
value=""
/>
</InputField>
<TextFieldDivider
active={false}
error={false}
key="text-divider"
lineDirection="center"
>
<Divider
className="md-divider--text-field md-divider--expand-from-center"
>
<hr
className="md-divider md-divider--text-field md-divider--expand-from-center"
/>
</Divider>
</TextFieldDivider>
</div>
<SVGIcon
className="md-text-field-icon md-text-field-icon--positioned"
error={false}
focusable="false"
key="icon-right"
onClick={[Function]}
primary={false}
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<svg
aria-labelledby={null}
className="md-icon md-text-field-icon md-text-field-icon--positioned"
focusable="false"
onClick={[Function]}
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z"
fill="#000000"
/>
</svg>
</SVGIcon>
</div>
<TextFieldMessage
active={false}
currentLength={0}
error={false}
key="message"
leftIcon={false}
maxLength={150}
rightIcon={true}
>
<div
className="md-text-field-message-container md-text-field-message-container--count-only md-text-field-message-container--right-icon-offset md-full-width md-text--disabled"
>
<Message
active={false}
key="message"
/>
<Message
active={false}
className="md-text-field-message--counter"
key="counter"
>
<div
aria-hidden={true}
className="md-text-field-message md-text-field-message--inactive md-text-field-message--counter"
>
0 / 150
</div>
</Message>
</div>
</TextFieldMessage>
</div>
</TextField>
</div>
<withInk(withTooltip(Button))
className="AddQuestionVariantButton"
icon={true}
inkTransitionEnterTimeout={450}
inkTransitionLeaveTimeout={300}
inkTransitionOverlap={150}
onClick={[Function]}
primary={true}
>
<withTooltip(Button)
className="AddQuestionVariantButton"
icon={true}
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
onClick={[Function]}
primary={true}
>
<Button
className="AddQuestionVariantButton"
fixedPosition="br"
icon={true}
iconBefore={true}
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
onClick={[Function]}
primary={true}
type="button"
>
<button
className="md-btn md-btn--icon md-pointer--hover md-text--theme-primary md-ink--primary md-inline-block AddQuestionVariantButton"
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
type="button"
>
<InkContainer
key="ink-container"
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
>
<TransitionGroup
childFactory={[Function]}
className="md-ink-container"
component="div"
>
<div
className="md-ink-container"
/>
</TransitionGroup>
</InkContainer>
<FontIcon
iconClassName="material-icons"
inherit={true}
>
<i
className="md-icon material-icons md-text--inherit"
>
add
</i>
</FontIcon>
</button>
</Button>
</withTooltip(Button)>
</withInk(withTooltip(Button))>
<AnswerSource
answerType={0}
className="AnswerTypeButton"
onSaveAnswerType={[Function]}
>
<div>
<withInk(withTooltip(Button))
flat={true}
inkTransitionEnterTimeout={450}
inkTransitionLeaveTimeout={300}
inkTransitionOverlap={150}
onClick={[Function]}
primary={true}
>
<withTooltip(Button)
flat={true}
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
onClick={[Function]}
primary={true}
>
<Button
fixedPosition="br"
flat={true}
iconBefore={true}
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
onClick={[Function]}
primary={true}
type="button"
>
<button
className="md-btn md-btn--flat md-btn--text md-pointer--hover md-text--theme-primary md-ink--primary md-inline-block"
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
type="button"
>
<InkContainer
key="ink-container"
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
>
<TransitionGroup
childFactory={[Function]}
className="md-ink-container"
component="div"
>
<div
className="md-ink-container"
/>
</TransitionGroup>
</InkContainer>
Answer type
</button>
</Button>
</withTooltip(Button)>
</withInk(withTooltip(Button))>
</div>
</AnswerSource>
<AnswerTextBox
answer=""
answerType={0}
externalAnswerSource=""
handleAnswerEdit={[Function]}
handleAnswerSourceEdit={[Function]}
>
<div
className="QuestionBox"
>
<TextField
className="md-cell md-cell--bottom IntentDetailsInputBoxes"
fullWidth={true}
id="intent answer"
label="Answer"
leftIconStateful={true}
lineDirection="center"
maxLength={150}
onChange={[Function]}
passwordIcon={
<FontIcon
iconClassName="material-icons"
>
remove_red_eye
</FontIcon>
}
placeholder="Answer"
rightIconStateful={true}
type="text"
value=""
>
<div
className="md-text-field-container md-full-width md-text-field-container--input md-cell md-cell--bottom IntentDetailsInputBoxes"
onClick={[Function]}
>
<FloatingLabel
active={false}
error={false}
floating={false}
htmlFor="intent answer"
iconOffset={false}
key="label"
label="Answer"
>
<label
className="md-floating-label md-floating-label--inactive md-floating-label--inactive-sized md-text--secondary"
htmlFor="intent answer"
>
Answer
</label>
</FloatingLabel>
<InputField
className=""
fullWidth={true}
id="intent answer"
inlineIndicator={false}
key="field"
label="Answer"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder={null}
type="text"
value=""
>
<input
className="md-text-field md-text-field--floating-margin md-full-width md-text"
id="intent answer"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder={null}
type="text"
value=""
/>
</InputField>
<TextFieldDivider
active={false}
error={false}
key="text-divider"
lineDirection="center"
>
<Divider
className="md-divider--text-field md-divider--expand-from-center"
>
<hr
className="md-divider md-divider--text-field md-divider--expand-from-center"
/>
</Divider>
</TextFieldDivider>
<TextFieldMessage
active={false}
currentLength={0}
error={false}
key="message"
leftIcon={false}
maxLength={150}
rightIcon={false}
>
<div
className="md-text-field-message-container md-text-field-message-container--count-only md-full-width md-text--disabled"
>
<Message
active={false}
key="message"
/>
<Message
active={false}
className="md-text-field-message--counter"
key="counter"
>
<div
aria-hidden={true}
className="md-text-field-message md-text-field-message--inactive md-text-field-message--counter"
>
0 / 150
</div>
</Message>
</div>
</TextFieldMessage>
</div>
</TextField>
</div>
</AnswerTextBox>
<withInk(withTooltip(Button))
className="IntentDetailsButton-firstInRow"
flat={true}
inkTransitionEnterTimeout={450}
inkTransitionLeaveTimeout={300}
inkTransitionOverlap={150}
onClick={[Function]}
primary={true}
swapTheming={true}
>
<withTooltip(Button)
className="IntentDetailsButton-firstInRow"
flat={true}
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
onClick={[Function]}
primary={true}
swapTheming={true}
>
<Button
className="IntentDetailsButton-firstInRow"
fixedPosition="br"
flat={true}
iconBefore={true}
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
onClick={[Function]}
primary={true}
swapTheming={true}
type="button"
>
<button
className="md-btn md-btn--flat md-btn--text md-pointer--hover md-background--primary md-background--primary-hover md-inline-block IntentDetailsButton-firstInRow"
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
type="button"
>
<InkContainer
key="ink-container"
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
>
<TransitionGroup
childFactory={[Function]}
className="md-ink-container"
component="div"
>
<div
className="md-ink-container"
/>
</TransitionGroup>
</InkContainer>
Save
</button>
</Button>
</withTooltip(Button)>
</withInk(withTooltip(Button))>
<withInk(withTooltip(Button))
className="IntentDetailsButton"
flat={true}
inkTransitionEnterTimeout={450}
inkTransitionLeaveTimeout={300}
inkTransitionOverlap={150}
onClick={[Function]}
primary={true}
>
<withTooltip(Button)
className="IntentDetailsButton"
flat={true}
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
onClick={[Function]}
primary={true}
>
<Button
className="IntentDetailsButton"
fixedPosition="br"
flat={true}
iconBefore={true}
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
onClick={[Function]}
primary={true}
type="button"
>
<button
className="md-btn md-btn--flat md-btn--text md-pointer--hover md-text--theme-primary md-ink--primary md-inline-block IntentDetailsButton"
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
type="button"
>
<InkContainer
key="ink-container"
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
>
<TransitionGroup
childFactory={[Function]}
className="md-ink-container"
component="div"
>
<div
className="md-ink-container"
/>
</TransitionGroup>
</InkContainer>
Delete
</button>
</Button>
</withTooltip(Button)>
</withInk(withTooltip(Button))>
</div>
</IntentDetails>
`;

View File

@@ -1,57 +0,0 @@
import React, {Component} from 'react';
import {Button, SelectionControlGroup} from 'react-md';
import Modal from './Modal';
class AnswerSourceForm extends Component{
render(){
return(
<Modal
title="Answer type"
actions={[
<Button
flat
swapTheming
onClick={this.props.onClose.bind (this)}
key="cancel"
>
Cancel
</Button>,
<Button
flat
primary
swapTheming
key="save"
onClick={this.props.onSave.bind(this)}
>
Save
</Button>,
]}
>
<SelectionControlGroup
id="answer-source"
name="answer-source"
type="radio"
label="Import answer from:"
onChange={this.props.onSourceChange.bind(this)}
controls={[
{
label: 'Predefined answer',
value: '0'
},
{
label: 'WordPress titles',
value: '1',
},
{
label: 'WordPress latest news',
value: '2',
},
]}
defaultValue={String(this.props.answerType)}
/>
</Modal>);
}
}
export default AnswerSourceForm;

View File

@@ -1,44 +0,0 @@
import React, {Component} from 'react';
import {TextField} from 'react-md';
import '../../css/components/IntentDetails.css';
import '../../css/Common.css';
import {
ANSWER_MAX_LENGTH,
ANSWER_TYPE,
} from '../../config/constants';
class AnswerTextBox extends Component {
render () {
//theese are defaults for ANSWER_TYPE.PREDEFINED
let labelText="Answer";
let valueText=this.props.answer;
let onChangeValue=this.props.handleAnswerEdit;
switch(this.props.answerType){
case ANSWER_TYPE.EXTERNAL_SOURCE_WP_TITLES:
case ANSWER_TYPE.EXTERNAL_SOURCE_WP_NEWS:
labelText="Answer source";
valueText=this.props.externalAnswerSource;
onChangeValue=this.props.handleAnswerSourceEdit
break;
}
return(
<div className="QuestionBox">
<TextField
id="intent answer"
lineDirection="center"
label={labelText}
placeholder={labelText}
maxLength={ANSWER_MAX_LENGTH}
className="md-cell md-cell--bottom IntentDetailsInputBoxes"
onChange={onChangeValue}
value={valueText}
/>
</div>
);
}
}
export default AnswerTextBox;

View File

@@ -1,23 +0,0 @@
import React, { Component } from 'react';
import '../../css/components/Modal.css';
class Modal extends Component {
render() {
const { title, children, actions } = this.props;
return (
<div className="modal">
<div className="modal-content">
<h2 className="header">
{title}
</h2>
{children}
<div className="actions">
{actions}
</div>
</div>
</div>
);
}
}
export default Modal;

View File

@@ -1,76 +0,0 @@
import React from 'react';
import { shallow, mount } from 'enzyme';
import AnswerSourceForm from '../AnswerSourceForm';
import {ANSWER_TYPE} from '../../../config/constants'
it('renders without crashing', () => {
shallow(<AnswerSourceForm onClose={()=>{}} onSave={()=>{}} onSourceChange={()=>{}} />);
});
it ('snapshot',()=>{
const wrapper = mount(<AnswerSourceForm onClose={()=>{}} onSave={()=>{}} onSourceChange={()=>{}} />);
expect(wrapper).toMatchSnapshot();
})
it('calls onClose when cancel is pressed', () => {
const onClose = jest.fn();
const wrapper = mount(<AnswerSourceForm onClose={onClose} onSave={()=>{}} onSourceChange={()=>{}} />);
const cancelButton = wrapper.find('button').at(0);
expect(cancelButton.text()).toEqual('Cancel');
cancelButton.simulate('click');
expect(onClose).toBeCalled();
});
it('calls onSave when save is pressed', () => {
const onSave = jest.fn();
const wrapper = mount(<AnswerSourceForm onClose={()=>{}} onSave={onSave} onSourceChange={()=>{}} />);
const saveButton = wrapper.find('button').at(1);
expect(saveButton.text()).toEqual('Save');
saveButton.simulate('click');
expect(onSave).toBeCalled();
});
it('sets PREDEFINED value when Predefined answer is selected', () => {
let selectedValue = null;
const wrapper = mount(<AnswerSourceForm
onClose={()=>{}}
onSave={()=>{}}
onSourceChange={(value)=>{selectedValue=value}} />);
const optionControl = wrapper.find('SelectionControlGroup');
expect(optionControl.exists()).toEqual(true);
optionControl.simulate('change',{target:{value:String(ANSWER_TYPE.PREDEFINED)}});
expect(selectedValue).toBe(String(ANSWER_TYPE.PREDEFINED));
});
it('sets EXTERNAL_SOURCE_WP_TITLES value when WordPress titles is selected', () => {
let selectedValue = null;
const wrapper = mount(<AnswerSourceForm
onClose={()=>{}}
onSave={()=>{}}
onSourceChange={(value)=>{selectedValue=value}} />);
const optionControl = wrapper.find('SelectionControlGroup');
expect(optionControl.exists()).toEqual(true);
optionControl.simulate('change',{target:{value:String(ANSWER_TYPE.EXTERNAL_SOURCE_WP_TITLES)}});
expect(selectedValue).toBe(String(ANSWER_TYPE.EXTERNAL_SOURCE_WP_TITLES));
});
it('sets EXTERNAL_SOURCE_WP_NEWS value when WordPress latest news is selected', () => {
let selectedValue = null;
const wrapper = mount(<AnswerSourceForm
onClose={()=>{}}
onSave={()=>{}}
onSourceChange={(value)=>{selectedValue=value}} />);
const optionControl = wrapper.find('SelectionControlGroup');
expect(optionControl.exists()).toEqual(true);
optionControl.simulate('change',{target:{value:String(ANSWER_TYPE.EXTERNAL_SOURCE_WP_NEWS)}});
expect(selectedValue).toBe(String(ANSWER_TYPE.EXTERNAL_SOURCE_WP_NEWS));
});

View File

@@ -1,93 +0,0 @@
import React from 'react';
import {shallow, mount} from 'enzyme';
import AnswerTextBox from '../AnswerTextBox';
import {ANSWER_TYPE} from '../../../config/constants';
it ('renders without crashing', () => {
shallow (<AnswerTextBox />);
});
describe ('predefined answer selected', () => {
let wrapper;
let textField;
beforeEach (() => {
const onChange = jest.fn();
wrapper = mount (
<AnswerTextBox
answerType={ANSWER_TYPE.PREDEFINED}
answer={'Dummy answer'}
handleAnswerEdit={onChange}
/>
);
textField = wrapper.find ('TextField').first ();
});
it ('snapshot', () =>{
expect(wrapper).toMatchSnapshot();
});
it ('renders text box for normal answer', () => {
expect (textField.props ().label).toEqual ('Answer');
});
it ('receives valid answer text', () => {
expect (textField.props ().value).toEqual ('Dummy answer');
});
});
describe ('WordPress titles selected', () => {
let wrapper;
let textField;
beforeEach (() => {
const onChange = jest.fn();
wrapper = mount (
<AnswerTextBox
answerType={ANSWER_TYPE.EXTERNAL_SOURCE_WP_TITLES}
externalAnswerSource={'Dummy answer'}
handleAnswerSourceEdit={onChange}
/>
);
textField = wrapper.find ('TextField').first ();
});
it ('snapshot', () =>{
expect(wrapper).toMatchSnapshot();
});
it ('renders text box for external source input', () => {
expect (textField.props ().label).toEqual ('Answer source');
});
it ('receives valid answer text', () => {
expect (textField.props ().value).toEqual ('Dummy answer');
});
});
describe ('WordPress latest news selected', () => {
let wrapper;
let textField;
beforeEach (() => {
const onChange = jest.fn();
wrapper = mount (
<AnswerTextBox
answerType={ANSWER_TYPE.EXTERNAL_SOURCE_WP_NEWS}
externalAnswerSource={'Dummy answer'}
handleAnswerSourceEdit={onChange}
/>
);
textField = wrapper.find ('TextField').first ();
});
it ('snapshot', () =>{
expect(wrapper).toMatchSnapshot();
});
it ('renders text box for external source input', () => {
expect (textField.props ().label).toEqual ('Answer source');
});
it ('receives valid answer text', () => {
expect (textField.props ().value).toEqual ('Dummy answer');
});
});

View File

@@ -1,27 +0,0 @@
import React from 'react';
import { shallow, mount } from 'enzyme';
import Modal from '../Modal';
it('renders without crashing', () => {
shallow(<Modal />);
});
let actionButton;
let childButton;
let wrapper;
beforeEach(()=>{
actionButton = <button key={0}>Dummy action button</button>;
childButton = <button key={1}>Child button</button>;
wrapper = mount(<Modal title={'Dummy title'} actions={[actionButton]}>{childButton}</Modal>);
});
it ('snapshot', () => {
expect(wrapper).toMatchSnapshot();
});
it('receives props as expected', () =>{
expect(wrapper.props().title).toEqual('Dummy title');
expect(wrapper.props().actions).toEqual([actionButton]);
expect(wrapper.props().children).toEqual(childButton);
});

View File

@@ -1,675 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`snapshot 1`] = `
<AnswerSourceForm
onClose={[Function]}
onSave={[Function]}
onSourceChange={[Function]}
>
<Modal
actions={
Array [
<withInk(withTooltip(Button))
flat={true}
inkTransitionEnterTimeout={450}
inkTransitionLeaveTimeout={300}
inkTransitionOverlap={150}
onClick={[Function]}
swapTheming={true}
>
Cancel
</withInk(withTooltip(Button))>,
<withInk(withTooltip(Button))
flat={true}
inkTransitionEnterTimeout={450}
inkTransitionLeaveTimeout={300}
inkTransitionOverlap={150}
onClick={[Function]}
primary={true}
swapTheming={true}
>
Save
</withInk(withTooltip(Button))>,
]
}
title="Answer type"
>
<div
className="modal"
>
<div
className="modal-content"
>
<h2
className="header"
>
Answer type
</h2>
<SelectionControlGroup
component="fieldset"
controls={
Array [
Object {
"label": "Predefined answer",
"value": "0",
},
Object {
"label": "WordPress titles",
"value": "1",
},
Object {
"label": "WordPress latest news",
"value": "2",
},
]
}
defaultValue="undefined"
id="answer-source"
label="Import answer from:"
labelClassName="md-subheading-1"
labelComponent="legend"
name="answer-source"
onChange={[Function]}
type="radio"
>
<fieldset
className="md-selection-control-group"
onChange={[Function]}
onKeyDown={[Function]}
>
<legend
className="md-subheading-1"
>
Import answer from:
</legend>
<SelectionControl
checked={false}
checkedCheckboxIcon={
<FontIcon
iconClassName="material-icons"
>
check_box
</FontIcon>
}
checkedRadioIcon={
<FontIcon
iconClassName="material-icons"
>
radio_button_checked
</FontIcon>
}
className=""
id="answer-source0"
key="control0"
label="Predefined answer"
name="answer-source"
type="radio"
uncheckedCheckboxIcon={
<FontIcon
iconClassName="material-icons"
>
check_box_outline_blank
</FontIcon>
}
uncheckedRadioIcon={
<FontIcon
iconClassName="material-icons"
>
radio_button_unchecked
</FontIcon>
}
value="0"
>
<div
className="md-selection-control-container"
onKeyDown={[Function]}
>
<input
aria-hidden={true}
checked={false}
className="md-selection-control-input"
id="answer-source0"
name="answer-source"
onChange={[Function]}
type="radio"
value="0"
/>
<label
className="md-selection-control-label md-pointer--hover md-text"
htmlFor="answer-source0"
>
<withInk(AccessibleFakeButton)
aria-checked={false}
className="md-selection-control-toggle md-btn md-btn--icon md-text--secondary"
inkTransitionEnterTimeout={450}
inkTransitionLeaveTimeout={300}
inkTransitionOverlap={150}
role="radio"
>
<AccessibleFakeButton
aria-checked={false}
className="md-selection-control-toggle md-btn md-btn--icon md-text--secondary"
component="div"
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
listenToEnter={true}
listenToSpace={true}
noFocusOutline={true}
role="radio"
tabIndex={0}
>
<div
aria-checked={false}
aria-pressed={false}
className="md-fake-btn md-pointer--hover md-fake-btn--no-outline md-selection-control-toggle md-btn md-btn--icon md-text--secondary"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
role="radio"
tabIndex={0}
>
<InkContainer
key="ink-container"
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
>
<TransitionGroup
childFactory={[Function]}
className="md-ink-container"
component="div"
>
<div
className="md-ink-container"
/>
</TransitionGroup>
</InkContainer>
<FontIcon
iconClassName="material-icons"
inherit={true}
key=".1"
>
<i
className="md-icon material-icons md-text--inherit"
>
radio_button_unchecked
</i>
</FontIcon>
</div>
</AccessibleFakeButton>
</withInk(AccessibleFakeButton)>
<span>
Predefined answer
</span>
</label>
</div>
</SelectionControl>
<SelectionControl
checked={false}
checkedCheckboxIcon={
<FontIcon
iconClassName="material-icons"
>
check_box
</FontIcon>
}
checkedRadioIcon={
<FontIcon
iconClassName="material-icons"
>
radio_button_checked
</FontIcon>
}
className=""
id="answer-source1"
key="control1"
label="WordPress titles"
name="answer-source"
tabIndex={-1}
type="radio"
uncheckedCheckboxIcon={
<FontIcon
iconClassName="material-icons"
>
check_box_outline_blank
</FontIcon>
}
uncheckedRadioIcon={
<FontIcon
iconClassName="material-icons"
>
radio_button_unchecked
</FontIcon>
}
value="1"
>
<div
className="md-selection-control-container"
onKeyDown={[Function]}
>
<input
aria-hidden={true}
checked={false}
className="md-selection-control-input"
id="answer-source1"
name="answer-source"
onChange={[Function]}
type="radio"
value="1"
/>
<label
className="md-selection-control-label md-pointer--hover md-text"
htmlFor="answer-source1"
>
<withInk(AccessibleFakeButton)
aria-checked={false}
className="md-selection-control-toggle md-btn md-btn--icon md-text--secondary"
inkTransitionEnterTimeout={450}
inkTransitionLeaveTimeout={300}
inkTransitionOverlap={150}
role="radio"
tabIndex={-1}
>
<AccessibleFakeButton
aria-checked={false}
className="md-selection-control-toggle md-btn md-btn--icon md-text--secondary"
component="div"
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
listenToEnter={true}
listenToSpace={true}
noFocusOutline={true}
role="radio"
tabIndex={-1}
>
<div
aria-checked={false}
aria-pressed={false}
className="md-fake-btn md-pointer--hover md-fake-btn--no-outline md-selection-control-toggle md-btn md-btn--icon md-text--secondary"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
role="radio"
tabIndex={-1}
>
<InkContainer
key="ink-container"
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
>
<TransitionGroup
childFactory={[Function]}
className="md-ink-container"
component="div"
>
<div
className="md-ink-container"
/>
</TransitionGroup>
</InkContainer>
<FontIcon
iconClassName="material-icons"
inherit={true}
key=".1"
>
<i
className="md-icon material-icons md-text--inherit"
>
radio_button_unchecked
</i>
</FontIcon>
</div>
</AccessibleFakeButton>
</withInk(AccessibleFakeButton)>
<span>
WordPress titles
</span>
</label>
</div>
</SelectionControl>
<SelectionControl
checked={false}
checkedCheckboxIcon={
<FontIcon
iconClassName="material-icons"
>
check_box
</FontIcon>
}
checkedRadioIcon={
<FontIcon
iconClassName="material-icons"
>
radio_button_checked
</FontIcon>
}
className=""
id="answer-source2"
key="control2"
label="WordPress latest news"
name="answer-source"
tabIndex={-1}
type="radio"
uncheckedCheckboxIcon={
<FontIcon
iconClassName="material-icons"
>
check_box_outline_blank
</FontIcon>
}
uncheckedRadioIcon={
<FontIcon
iconClassName="material-icons"
>
radio_button_unchecked
</FontIcon>
}
value="2"
>
<div
className="md-selection-control-container"
onKeyDown={[Function]}
>
<input
aria-hidden={true}
checked={false}
className="md-selection-control-input"
id="answer-source2"
name="answer-source"
onChange={[Function]}
type="radio"
value="2"
/>
<label
className="md-selection-control-label md-pointer--hover md-text"
htmlFor="answer-source2"
>
<withInk(AccessibleFakeButton)
aria-checked={false}
className="md-selection-control-toggle md-btn md-btn--icon md-text--secondary"
inkTransitionEnterTimeout={450}
inkTransitionLeaveTimeout={300}
inkTransitionOverlap={150}
role="radio"
tabIndex={-1}
>
<AccessibleFakeButton
aria-checked={false}
className="md-selection-control-toggle md-btn md-btn--icon md-text--secondary"
component="div"
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
listenToEnter={true}
listenToSpace={true}
noFocusOutline={true}
role="radio"
tabIndex={-1}
>
<div
aria-checked={false}
aria-pressed={false}
className="md-fake-btn md-pointer--hover md-fake-btn--no-outline md-selection-control-toggle md-btn md-btn--icon md-text--secondary"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
role="radio"
tabIndex={-1}
>
<InkContainer
key="ink-container"
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
>
<TransitionGroup
childFactory={[Function]}
className="md-ink-container"
component="div"
>
<div
className="md-ink-container"
/>
</TransitionGroup>
</InkContainer>
<FontIcon
iconClassName="material-icons"
inherit={true}
key=".1"
>
<i
className="md-icon material-icons md-text--inherit"
>
radio_button_unchecked
</i>
</FontIcon>
</div>
</AccessibleFakeButton>
</withInk(AccessibleFakeButton)>
<span>
WordPress latest news
</span>
</label>
</div>
</SelectionControl>
</fieldset>
</SelectionControlGroup>
<div
className="actions"
>
<withInk(withTooltip(Button))
flat={true}
inkTransitionEnterTimeout={450}
inkTransitionLeaveTimeout={300}
inkTransitionOverlap={150}
key="cancel"
onClick={[Function]}
swapTheming={true}
>
<withTooltip(Button)
flat={true}
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
onClick={[Function]}
swapTheming={true}
>
<Button
fixedPosition="br"
flat={true}
iconBefore={true}
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
onClick={[Function]}
swapTheming={true}
type="button"
>
<button
className="md-btn md-btn--flat md-btn--text md-pointer--hover md-text md-inline-block"
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
type="button"
>
<InkContainer
key="ink-container"
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
>
<TransitionGroup
childFactory={[Function]}
className="md-ink-container"
component="div"
>
<div
className="md-ink-container"
/>
</TransitionGroup>
</InkContainer>
Cancel
</button>
</Button>
</withTooltip(Button)>
</withInk(withTooltip(Button))>
<withInk(withTooltip(Button))
flat={true}
inkTransitionEnterTimeout={450}
inkTransitionLeaveTimeout={300}
inkTransitionOverlap={150}
key="save"
onClick={[Function]}
primary={true}
swapTheming={true}
>
<withTooltip(Button)
flat={true}
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
onClick={[Function]}
primary={true}
swapTheming={true}
>
<Button
fixedPosition="br"
flat={true}
iconBefore={true}
ink={
<InkContainer
className={undefined}
disabledInteractions={undefined}
inkClassName={undefined}
inkStyle={undefined}
pulse={undefined}
style={undefined}
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
waitForInkTransition={undefined}
/>
}
onClick={[Function]}
primary={true}
swapTheming={true}
type="button"
>
<button
className="md-btn md-btn--flat md-btn--text md-pointer--hover md-background--primary md-background--primary-hover md-inline-block"
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
type="button"
>
<InkContainer
key="ink-container"
transitionEnterTimeout={450}
transitionLeaveTimeout={300}
transitionOverlap={150}
>
<TransitionGroup
childFactory={[Function]}
className="md-ink-container"
component="div"
>
<div
className="md-ink-container"
/>
</TransitionGroup>
</InkContainer>
Save
</button>
</Button>
</withTooltip(Button)>
</withInk(withTooltip(Button))>
</div>
</div>
</div>
</Modal>
</AnswerSourceForm>
`;

View File

@@ -1,376 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`WordPress latest news selected snapshot 1`] = `
<AnswerTextBox
answerType={2}
externalAnswerSource="Dummy answer"
handleAnswerSourceEdit={[Function]}
>
<div
className="QuestionBox"
>
<TextField
className="md-cell md-cell--bottom IntentDetailsInputBoxes"
fullWidth={true}
id="intent answer"
label="Answer source"
leftIconStateful={true}
lineDirection="center"
maxLength={150}
onChange={[Function]}
passwordIcon={
<FontIcon
iconClassName="material-icons"
>
remove_red_eye
</FontIcon>
}
placeholder="Answer source"
rightIconStateful={true}
type="text"
value="Dummy answer"
>
<div
className="md-text-field-container md-full-width md-text-field-container--input md-cell md-cell--bottom IntentDetailsInputBoxes"
onClick={[Function]}
>
<FloatingLabel
active={false}
error={false}
floating={true}
htmlFor="intent answer"
iconOffset={false}
key="label"
label="Answer source"
>
<label
className="md-floating-label md-floating-label--floating md-text--secondary"
htmlFor="intent answer"
>
Answer source
</label>
</FloatingLabel>
<InputField
className=""
fullWidth={true}
id="intent answer"
inlineIndicator={false}
key="field"
label="Answer source"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="Answer source"
type="text"
value="Dummy answer"
>
<input
className="md-text-field md-text-field--floating-margin md-full-width md-text"
id="intent answer"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="Answer source"
type="text"
value="Dummy answer"
/>
</InputField>
<TextFieldDivider
active={false}
error={false}
key="text-divider"
lineDirection="center"
>
<Divider
className="md-divider--text-field md-divider--expand-from-center"
>
<hr
className="md-divider md-divider--text-field md-divider--expand-from-center"
/>
</Divider>
</TextFieldDivider>
<TextFieldMessage
active={false}
currentLength={12}
error={false}
key="message"
leftIcon={false}
maxLength={150}
rightIcon={false}
>
<div
className="md-text-field-message-container md-text-field-message-container--count-only md-full-width md-text--disabled"
>
<Message
active={false}
key="message"
/>
<Message
active={false}
className="md-text-field-message--counter"
key="counter"
>
<div
aria-hidden={true}
className="md-text-field-message md-text-field-message--inactive md-text-field-message--counter"
>
12 / 150
</div>
</Message>
</div>
</TextFieldMessage>
</div>
</TextField>
</div>
</AnswerTextBox>
`;
exports[`WordPress titles selected snapshot 1`] = `
<AnswerTextBox
answerType={1}
externalAnswerSource="Dummy answer"
handleAnswerSourceEdit={[Function]}
>
<div
className="QuestionBox"
>
<TextField
className="md-cell md-cell--bottom IntentDetailsInputBoxes"
fullWidth={true}
id="intent answer"
label="Answer source"
leftIconStateful={true}
lineDirection="center"
maxLength={150}
onChange={[Function]}
passwordIcon={
<FontIcon
iconClassName="material-icons"
>
remove_red_eye
</FontIcon>
}
placeholder="Answer source"
rightIconStateful={true}
type="text"
value="Dummy answer"
>
<div
className="md-text-field-container md-full-width md-text-field-container--input md-cell md-cell--bottom IntentDetailsInputBoxes"
onClick={[Function]}
>
<FloatingLabel
active={false}
error={false}
floating={true}
htmlFor="intent answer"
iconOffset={false}
key="label"
label="Answer source"
>
<label
className="md-floating-label md-floating-label--floating md-text--secondary"
htmlFor="intent answer"
>
Answer source
</label>
</FloatingLabel>
<InputField
className=""
fullWidth={true}
id="intent answer"
inlineIndicator={false}
key="field"
label="Answer source"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="Answer source"
type="text"
value="Dummy answer"
>
<input
className="md-text-field md-text-field--floating-margin md-full-width md-text"
id="intent answer"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="Answer source"
type="text"
value="Dummy answer"
/>
</InputField>
<TextFieldDivider
active={false}
error={false}
key="text-divider"
lineDirection="center"
>
<Divider
className="md-divider--text-field md-divider--expand-from-center"
>
<hr
className="md-divider md-divider--text-field md-divider--expand-from-center"
/>
</Divider>
</TextFieldDivider>
<TextFieldMessage
active={false}
currentLength={12}
error={false}
key="message"
leftIcon={false}
maxLength={150}
rightIcon={false}
>
<div
className="md-text-field-message-container md-text-field-message-container--count-only md-full-width md-text--disabled"
>
<Message
active={false}
key="message"
/>
<Message
active={false}
className="md-text-field-message--counter"
key="counter"
>
<div
aria-hidden={true}
className="md-text-field-message md-text-field-message--inactive md-text-field-message--counter"
>
12 / 150
</div>
</Message>
</div>
</TextFieldMessage>
</div>
</TextField>
</div>
</AnswerTextBox>
`;
exports[`predefined answer selected snapshot 1`] = `
<AnswerTextBox
answer="Dummy answer"
answerType={0}
handleAnswerEdit={[Function]}
>
<div
className="QuestionBox"
>
<TextField
className="md-cell md-cell--bottom IntentDetailsInputBoxes"
fullWidth={true}
id="intent answer"
label="Answer"
leftIconStateful={true}
lineDirection="center"
maxLength={150}
onChange={[Function]}
passwordIcon={
<FontIcon
iconClassName="material-icons"
>
remove_red_eye
</FontIcon>
}
placeholder="Answer"
rightIconStateful={true}
type="text"
value="Dummy answer"
>
<div
className="md-text-field-container md-full-width md-text-field-container--input md-cell md-cell--bottom IntentDetailsInputBoxes"
onClick={[Function]}
>
<FloatingLabel
active={false}
error={false}
floating={true}
htmlFor="intent answer"
iconOffset={false}
key="label"
label="Answer"
>
<label
className="md-floating-label md-floating-label--floating md-text--secondary"
htmlFor="intent answer"
>
Answer
</label>
</FloatingLabel>
<InputField
className=""
fullWidth={true}
id="intent answer"
inlineIndicator={false}
key="field"
label="Answer"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="Answer"
type="text"
value="Dummy answer"
>
<input
className="md-text-field md-text-field--floating-margin md-full-width md-text"
id="intent answer"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="Answer"
type="text"
value="Dummy answer"
/>
</InputField>
<TextFieldDivider
active={false}
error={false}
key="text-divider"
lineDirection="center"
>
<Divider
className="md-divider--text-field md-divider--expand-from-center"
>
<hr
className="md-divider md-divider--text-field md-divider--expand-from-center"
/>
</Divider>
</TextFieldDivider>
<TextFieldMessage
active={false}
currentLength={12}
error={false}
key="message"
leftIcon={false}
maxLength={150}
rightIcon={false}
>
<div
className="md-text-field-message-container md-text-field-message-container--count-only md-full-width md-text--disabled"
>
<Message
active={false}
key="message"
/>
<Message
active={false}
className="md-text-field-message--counter"
key="counter"
>
<div
aria-hidden={true}
className="md-text-field-message md-text-field-message--inactive md-text-field-message--counter"
>
12 / 150
</div>
</Message>
</div>
</TextFieldMessage>
</div>
</TextField>
</div>
</AnswerTextBox>
`;

View File

@@ -1,42 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`snapshot 1`] = `
<Modal
actions={
Array [
<button>
Dummy action button
</button>,
]
}
title="Dummy title"
>
<div
className="modal"
>
<div
className="modal-content"
>
<h2
className="header"
>
Dummy title
</h2>
<button
key="1"
>
Child button
</button>
<div
className="actions"
>
<button
key="0"
>
Dummy action button
</button>
</div>
</div>
</div>
</Modal>
`;

View File

@@ -1,4 +0,0 @@
//export const BASE_URL = 'tellall.saburly.com'; //for server
export const BASE_URL = 'localhost:5000'; //for local

View File

@@ -1,34 +0,0 @@
export const INTENT_EXPLANATION_MAX_LENGTH = 70;
export const INTENT_NAME_MAX_LENGTH = 30;
export const INTENT_NAME_MIN_LENGTH = 2;
export const QUESTION_MAX_LENGTH = 150;
export const QUESTION_MIN_LENGTH = 2;
export const ANSWER_MAX_LENGTH = 150;
export const ANSWER_MIN_LENGTH = 2;
export const INTENT_TITLE_MAX_LENGTH = 20;
export const INTENT_TITLE_TOOLTIP_DELAY = 700;
export const INVOCATION_NAME_MAX_LENGTH = 50;
export const INVOCATION_NAME_MIN_LENGTH = 2;
export const INVOCATION_ANSWER_MAX_LENGTH = 100;
export const EMAIL_MAX_LENGTH = 100;
export const NEW_INTENT_SELECTED_INDEX = -1;
export const LAUNCH_REQUEST_SELECTED_INDEX = -2;
export const CONTACT_SELECTED_INDEX = -3;
export const ANSWER_TYPE = {
PREDEFINED: 0,
EXTERNAL_SOURCE_WP_TITLES : 1,
EXTERNAL_SOURCE_WP_NEWS : 2
}
export const RESULT_CODES = {
OK: 0,
ERROR: -1,
};

View File

@@ -1,13 +0,0 @@
.App {
text-align: center; }
.App-header {
background-color: white;
height: 80px;
padding: 20px; }
.App-title {
font-size: 1.5em; }
.App-intro {
font-size: large; }

View File

@@ -1,17 +0,0 @@
.App {
text-align: center;
}
.App-header {
background-color:white;
height: 80px;
padding: 20px;
}
.App-title {
font-size: 1.5em;
}
.App-intro {
font-size: large;
}

View File

@@ -1,21 +0,0 @@
$minHeight : calc(100vh - 80px); //80px is height of the title div container
/* Common for right panel components */
.RightPanelBox{
float: left;
width: 70%;
min-height:$minHeight;
background-color: #f5f5f5;
}
.PanelSubTitle{
text-align: left;
margin-top: 30px;
margin-left: 20px;
}
.SaveButton{
float: right;
margin-right: 20px;
}

View File

@@ -1,5 +0,0 @@
@import 'react-md/src/scss/react-md';
// Any variable overrides. The following just changes the default theme to use teal and purple.
$md-primary-color: $md-teal-500;
$md-secondary-color: $md-purple-a-400;

View File

@@ -1,4 +0,0 @@
.ContactEmailInput{
width: 60%;
margin-left: 20px;
}

View File

@@ -1,33 +0,0 @@
.QuestionBox{
margin:25px;
}
.QuestionTitle{
margin-top:20px;
margin-left: 30px;
float: left;
}
.IntentDetailsInputBoxes{
width: 90%;
}
.IntentDetailsButton{
float: right;
margin-right: 25px;
}
.IntentDetailsButton-firstInRow{
float: right;
margin-right: 10%;
}
.AddQuestionVariantButton{
float: right;
margin-right: 10%;
}
.AnswerTypeButton{
float: right;
margin-right: 30px;
}

View File

@@ -1,18 +0,0 @@
.IntentItem{
margin-top: 2px;
margin-bottom: 2px;
height: 50px;
width:100%;
text-align:left;
background-color: #d8d8d8;
}
.IntentItem-selected{
margin-top: 2px;
margin-bottom: 2px;
height: 50px;
width:100%;
text-align:left;
background-color: #f5f5f5;
}

View File

@@ -1,52 +0,0 @@
$minHeight : calc(100vh - 80px); //80px is height of the title div container
.IntentList{
width: 30%;
min-height:$minHeight;
float:left;
background-color: #eff0f0;
}
.IntentList-title{
font-size: 1.5em;
height: 70px;
padding: 20px;
text-align:left;
background-color: #eff0f0;
}
.AddIntent{
float: right;
margin: 12px;
}
.LaunchRequestButton{
text-align: left;
width: 100%;
height: 50px;
background-color: #d8d8d8
}
.LaunchRequestButton-selected{
text-align: left;
width: 100%;
height: 50px;
background-color: #f5f5f5;
}
.ContactButton{
text-align: left;
color: #009b8a;
width: 100%;
height: 50px;
background-color: #d8d8d8
}
.ContactButton-selected{
text-align: left;
color: #009b8a;
width: 100%;
height: 50px;
background-color: #f5f5f5;
}

View File

@@ -1,11 +0,0 @@
.ExplanationText{
float: left;
margin-top: 30px;
margin-left: 20px;
text-align: left;
}
.InvocationInputBoxes{
width: 60%;
margin-left: 20px;
}

View File

@@ -1,32 +0,0 @@
.modal {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(37, 37, 37, .7);
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
.modal-content {
color: black;
padding: 20px;
padding-bottom: 0px;
display: flex;
flex-direction: column;
align-items: flex-start;
height: auto;
background-color: #FFF;
min-width: 500px;
.actions {
border-top: 1px solid #bebebe;
width: 100%;
display: flex;
justify-content: flex-end;
min-width: 500px;
margin: 0 -20px;
padding: 10px;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
@import 'globals';
@include react-md-everything;
body {
margin: 0;
padding: 0;
}

View File

@@ -1,174 +0,0 @@
.mm-popup {
display: none; }
.mm-popup--visible {
display: block; }
.mm-popup__overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
overflow: auto;
background: rgba(0, 0, 0, 0.1); }
.mm-popup__close {
position: absolute;
top: 15px;
right: 20px;
padding: 0;
width: 20px;
height: 20px;
cursor: pointer;
outline: none;
text-align: center;
border-radius: 10px;
border: none;
text-indent: -9999px;
background: transparent url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAABGdBTUEAALGPC/xhBQAAB8BJREFUWAnFWAtsU1UY/s+5XTcYYxgfvERQeQXxNeYLjVFxLVvb2xasKIgSVNQoREVI1GhmfC6ioijiNDo1vBxb19uVtRWUzAQ1+EowOkSQzTBAUJio27r2Hr9TLJTaa7vK4yTtvec///+f7/znf5xzGf2PZnVMKRHUczEJNpgYDSEdPzTB6GdG1EbE2sxk+qqxsW5rrtNAT+/aZLtrkiDdLYhUIcSwQ9KsA7DaAbKdEWOCQBckxwrkOGP0Lf7rTAqrW+vzbT4kk91/1gAB7BqdYlVC0KUAsQuANOKKjwYUNYfff//PdNNZ3O4zqEe/FguZykhUYFGFQKspnBYGNW1LOplUWkaANtvUc3pY5FUAKwewb4jzR0KaN8ikoXrRZs2aVbBr3/6bddKfhHUHAugys+j3eCCwYv9/qflPgFab83ps52ookxZ6OOT3regtsNTJHY45fSO05yGh6wsFsZ1cIVtI035M5Uv0DQFabY77BWOLsNrmQrPi8Xq9vyaEjsXT4pg6VuiRABZfzAVzhwK+T9Lp5emIFru6QCd6CXv4+sRLSizHGpycM+yvayng/S6Do7QIJtZZVXVyOiz/sqDV4XAKweoxsDjUqM1PJ3QsaeVz5+bHtrc2IjWVmky8tKmhYVuy/qMsWOZyXSR0Wo4IDVxRWrIgmfF4vTctWdINF7oJljwQ7dG9lpkzC5PnOgywsrKSU1R/Gz6xo7hPwXT0scsnpkkXEnncjTw6kvZ3vJI8q5Lo5BUV3YaAuFthyjStof6HBP1EPbe3tOweNWpMF0AuGHveuNqtLS375NxxC8rQB7inkOd8wcaGDScKVOo8/fvmLwWOPZFIrDIxFgcYEbtnA9wgk1lZmBgwetrtnqGTbapqNG5Et06ZMhhuYzIal/Ta2tpOlMVnEAOeCqfzfEmLA0SV8KB+bljr9Wbc2ijrujpGwmdxOB+SCrJpckGiu+enT7/85uZM/P375FcjDn6LxsRMycsrPJ5B2PerOLE1mYTleNDvX8k4W4xK8HyZ3XlvJpkym+qJEa1B1VjHRwz7IBM/rBjBNodhxXLJy6N/dbvlSz4nr3xm08J+7QHkyTdI6EssDsftRjJWh2smtmwlyrZ29tBBbplSjHiT6ZyxIHZ1vHQnVBlRArTfaZq2J5kp0zuS+D2w5Hs4/FWj8sxI5bfa1TuF0GtAX4W0Na26uronlceon89FSI5FRPf1HJY4C2e1HUbMRnR5aCguyIf1RC143oW1piZ44Z/zdCFgYXpnYmnJrdg27HL2LW4sxg7A9YYhqthwEmJ99uJHOOXEiMxbNm76qkAX+kps9xSUyXHwzyps02tBv29urqcfGG4fzgKnIYrFMHTajkzbuzcAjBb3zb8ROtajTHqx2Cq8L4IL3JcruEMIxF4cck/niK4IjlV5vYN1NLeMPATDd6DKPBclhfmP5sipdxBSRdKCe/E7PScVEMJxnllszlfgcw/CYk8g4X8OSwbKHY7Lc9Up5aB2MNxvN2eC7UUnJ4DYXm51ON/AqXsuVvpAuFGrVAYUVUD991HBmuStL1eQ2N7hkG1DfqY92J4ze6vI4/EoCI53YcE7EBD3hAL+xVJH0/Llv5tFkRUTtOoiGrbY3ONz0F2MAOnPGG8FQLYRCi7DhP2yVTRnzpy8A391r8TipqNYzkZALEuWlRchpU9BGfbpF8Fi6yar6pjk8UzvBzt7SuM8grbwPBMPwArm37u6JmUSlOPyBLyjfVcdttGNPDfjQ7+/Jp1cU23tXp6fNwkRfTCmi/XydpiOLx0tRvoNWPzOoN+7iQe83u/h2Dvgh7Z0zKk0/afWF+C8VsYVTzigrUodT+6H6ut3IaKvw0KiEYp8pKpqUfJ4unfp16C7meD1Mk3JDprwovbdaLNNP+VQ3/hfKGwFJ+WasL+hwZjryEjY5/vZTObrYJFmznHJzNA+2/S1dI2BsLysUBBDw8qGdOr0Ixz75XCj/2FJOxlNpiyrQ/0CuZmF/b4Jhy2I2ie/qywFqHkAO/BkgJNzWu3OW7GTJZzT/EQV+meL5Veewudg0FhnjJacDIAul2sATlZPw3gavjR8nMBwGCDOofuA+m74o0de3BMMJ+KJwDD9GY2twdGtH+7GDybPeZTTbvthy+aRo8cUYxWPjhw1duO2rVu2JzMfr3dzYZF0LzdTmCvk832RPM9hCyaIEy+ZsBBpoRnlqyGXy1FCTzbPeKm0q1WoGnch1c0La9qHqXLxKE4lyqrS0YlKQVTBhJifKGOpfP+nXz5jRv9Yx8HliFwbXOtR1PFn0+lLC1Ayylrb0dn1IqJqHmr1alL4ApnT0inpLa1MVa9kungLQYk7B90SDGiakQ5DgAkBi02djeiqgrJC3A8WiQHFVUZfVBMyRs9yp3McrpPPIhHjXs02m0zspiafT54jDVtGgFJSpoDOqP4YfOU+KO+Cco1xsYaPGBHMdFOTRaBbl9+zyYlcWwZ17Vjw41dOmPAefDDj95+sACaWV+5ynQsLzMZ104NAGoVo/0Oe/eDgrVDUhtl2gl7IOA2Of/FnYgSAXRBPuoI+JS5WDzn11DdramqwyOxarwAmq7Ta3RfqIqZCwWhYZjicHbdDGhoHLeTXfmrHUWwngDaTWWkMe72/JMtn+/43YTIL+pAwwhkAAAAASUVORK5CYII=") no-repeat center center;
background-size: 100%;
margin: 0; }
.mm-popup__input {
display: block;
width: 100%;
height: 30px;
border-radius: 3px;
background: #f5f5f5;
border: 1px solid #e9ebec;
outline: none;
-moz-box-sizing: border-box !important;
-webkit-box-sizing: border-box !important;
box-sizing: border-box !important;
font-size: 14px;
padding: 0 12px;
color: #808080; }
.mm-popup__btn {
border-radius: 3px;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
padding: 0 10px;
margin: 0;
line-height: 32px;
height: 32px;
border: 1px solid #666;
text-align: center;
display: inline-block;
font-size: 12px;
font-weight: 400;
color: #333;
background: transparent;
outline: none;
text-decoration: none;
cursor: pointer;
font-family: "Open Sans", sans-serif; }
.mm-popup__btn--success {
background-color: #27ae60;
border-color: #27ae60;
color: #fff; }
.mm-popup__btn--danger {
background-color: #c5545c;
border-color: #c5545c;
color: #fff; }
.mm-popup__box {
width: 350px;
position: fixed;
top: 10%;
left: 50%;
margin-left: -175px;
background: #fff;
box-shadow: 0px 5px 20px 0px rgba(126, 137, 140, 0.2);
border-radius: 5px;
border: 1px solid #B8C8CC;
overflow: hidden;
z-index: 1001; }
.mm-popup__box__header {
padding: 15px 20px;
background: #EDF5F7;
color: #454B4D; }
.mm-popup__box__header__title {
margin: 0;
font-size: 16px;
text-align: left;
font-weight: 600; }
.mm-popup__box__body {
padding: 20px;
line-height: 1.4;
font-size: 14px;
color: #454B4D;
background: #fff;
position: relative;
z-index: 2; }
.mm-popup__box__body p {
margin: 0 0 5px; }
.mm-popup__box__footer {
overflow: hidden;
padding: 40px 20px 20px; }
.mm-popup__box__footer__right-space {
float: right; }
.mm-popup__box__footer__right-space .mm-popup__btn {
margin-left: 5px; }
.mm-popup__box__footer__left-space {
float: left; }
.mm-popup__box__footer__left-space .mm-popup__btn {
margin-right: 5px; }
.mm-popup__box--popover {
width: 300px;
margin-left: -150px; }
.mm-popup__box--popover .mm-popup__close {
position: absolute;
top: 5px;
right: 5px;
padding: 0;
width: 20px;
height: 20px;
cursor: pointer;
outline: none;
text-align: center;
border-radius: 10px;
border: none;
text-indent: -9999px;
background: transparent url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAABGdBTUEAALGPC/xhBQAAB8BJREFUWAnFWAtsU1UY/s+5XTcYYxgfvERQeQXxNeYLjVFxLVvb2xasKIgSVNQoREVI1GhmfC6ioijiNDo1vBxb19uVtRWUzAQ1+EowOkSQzTBAUJio27r2Hr9TLJTaa7vK4yTtvec///+f7/znf5xzGf2PZnVMKRHUczEJNpgYDSEdPzTB6GdG1EbE2sxk+qqxsW5rrtNAT+/aZLtrkiDdLYhUIcSwQ9KsA7DaAbKdEWOCQBckxwrkOGP0Lf7rTAqrW+vzbT4kk91/1gAB7BqdYlVC0KUAsQuANOKKjwYUNYfff//PdNNZ3O4zqEe/FguZykhUYFGFQKspnBYGNW1LOplUWkaANtvUc3pY5FUAKwewb4jzR0KaN8ikoXrRZs2aVbBr3/6bddKfhHUHAugys+j3eCCwYv9/qflPgFab83ps52ookxZ6OOT3regtsNTJHY45fSO05yGh6wsFsZ1cIVtI035M5Uv0DQFabY77BWOLsNrmQrPi8Xq9vyaEjsXT4pg6VuiRABZfzAVzhwK+T9Lp5emIFru6QCd6CXv4+sRLSizHGpycM+yvayng/S6Do7QIJtZZVXVyOiz/sqDV4XAKweoxsDjUqM1PJ3QsaeVz5+bHtrc2IjWVmky8tKmhYVuy/qMsWOZyXSR0Wo4IDVxRWrIgmfF4vTctWdINF7oJljwQ7dG9lpkzC5PnOgywsrKSU1R/Gz6xo7hPwXT0scsnpkkXEnncjTw6kvZ3vJI8q5Lo5BUV3YaAuFthyjStof6HBP1EPbe3tOweNWpMF0AuGHveuNqtLS375NxxC8rQB7inkOd8wcaGDScKVOo8/fvmLwWOPZFIrDIxFgcYEbtnA9wgk1lZmBgwetrtnqGTbapqNG5Et06ZMhhuYzIal/Ta2tpOlMVnEAOeCqfzfEmLA0SV8KB+bljr9Wbc2ijrujpGwmdxOB+SCrJpckGiu+enT7/85uZM/P375FcjDn6LxsRMycsrPJ5B2PerOLE1mYTleNDvX8k4W4xK8HyZ3XlvJpkym+qJEa1B1VjHRwz7IBM/rBjBNodhxXLJy6N/dbvlSz4nr3xm08J+7QHkyTdI6EssDsftRjJWh2smtmwlyrZ29tBBbplSjHiT6ZyxIHZ1vHQnVBlRArTfaZq2J5kp0zuS+D2w5Hs4/FWj8sxI5bfa1TuF0GtAX4W0Na26uronlceon89FSI5FRPf1HJY4C2e1HUbMRnR5aCguyIf1RC143oW1piZ44Z/zdCFgYXpnYmnJrdg27HL2LW4sxg7A9YYhqthwEmJ99uJHOOXEiMxbNm76qkAX+kps9xSUyXHwzyps02tBv29urqcfGG4fzgKnIYrFMHTajkzbuzcAjBb3zb8ROtajTHqx2Cq8L4IL3JcruEMIxF4cck/niK4IjlV5vYN1NLeMPATDd6DKPBclhfmP5sipdxBSRdKCe/E7PScVEMJxnllszlfgcw/CYk8g4X8OSwbKHY7Lc9Up5aB2MNxvN2eC7UUnJ4DYXm51ON/AqXsuVvpAuFGrVAYUVUD991HBmuStL1eQ2N7hkG1DfqY92J4ze6vI4/EoCI53YcE7EBD3hAL+xVJH0/Llv5tFkRUTtOoiGrbY3ONz0F2MAOnPGG8FQLYRCi7DhP2yVTRnzpy8A391r8TipqNYzkZALEuWlRchpU9BGfbpF8Fi6yar6pjk8UzvBzt7SuM8grbwPBMPwArm37u6JmUSlOPyBLyjfVcdttGNPDfjQ7+/Jp1cU23tXp6fNwkRfTCmi/XydpiOLx0tRvoNWPzOoN+7iQe83u/h2Dvgh7Z0zKk0/afWF+C8VsYVTzigrUodT+6H6ut3IaKvw0KiEYp8pKpqUfJ4unfp16C7meD1Mk3JDprwovbdaLNNP+VQ3/hfKGwFJ+WasL+hwZjryEjY5/vZTObrYJFmznHJzNA+2/S1dI2BsLysUBBDw8qGdOr0Ixz75XCj/2FJOxlNpiyrQ/0CuZmF/b4Jhy2I2ie/qywFqHkAO/BkgJNzWu3OW7GTJZzT/EQV+meL5Veewudg0FhnjJacDIAul2sATlZPw3gavjR8nMBwGCDOofuA+m74o0de3BMMJ+KJwDD9GY2twdGtH+7GDybPeZTTbvthy+aRo8cUYxWPjhw1duO2rVu2JzMfr3dzYZF0LzdTmCvk832RPM9hCyaIEy+ZsBBpoRnlqyGXy1FCTzbPeKm0q1WoGnch1c0La9qHqXLxKE4lyqrS0YlKQVTBhJifKGOpfP+nXz5jRv9Yx8HliFwbXOtR1PFn0+lLC1Ayylrb0dn1IqJqHmr1alL4ApnT0inpLa1MVa9kungLQYk7B90SDGiakQ5DgAkBi02djeiqgrJC3A8WiQHFVUZfVBMyRs9yp3McrpPPIhHjXs02m0zspiafT54jDVtGgFJSpoDOqP4YfOU+KO+Cco1xsYaPGBHMdFOTRaBbl9+zyYlcWwZ17Vjw41dOmPAefDDj95+sACaWV+5ynQsLzMZ104NAGoVo/0Oe/eDgrVDUhtl2gl7IOA2Of/FnYgSAXRBPuoI+JS5WDzn11DdramqwyOxarwAmq7Ta3RfqIqZCwWhYZjicHbdDGhoHLeTXfmrHUWwngDaTWWkMe72/JMtn+/43YTIL+pAwwhkAAAAASUVORK5CYII=") no-repeat center center;
background-size: 100%;
margin: 0;
z-index: 3; }
.mm-popup__box--popover .mm-popup__box__body {
padding: 20px; }
@media (max-width: 420px) {
.mm-popup__box {
width: auto;
left: 10px;
right: 10px;
top: 10px;
margin-left: 0; }
.mm-popup__box__footer__left-space {
float: none; }
.mm-popup__box__footer__right-space {
float: none; }
.mm-popup__box__footer {
padding-top: 30px; }
.mm-popup__box__footer .mm-popup__btn {
display: block;
width: 100%;
text-align: center;
margin-top: 10px; } }

View File

@@ -1,16 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './css/index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import WebFontLoader from 'webfontloader';
WebFontLoader.load({
google: {
families: ['Roboto:300,400,500,700', 'Material Icons'],
},
});
ReactDOM.render(<App />, document.getElementById('root'));
registerServiceWorker();

View File

@@ -1,20 +0,0 @@
import fetch from 'isomorphic-fetch';
import {BASE_URL} from '../config/config';
export const getSkill = (id)=>{
let url = `http://${BASE_URL}/skill/${id}`
return fetch(url, {method: 'GET'});
}
export const updateSkill = (skill)=>{
let id = (skill._id) ? skill._id : -1;
let url = `http://${BASE_URL}/skill/${id}`
return fetch(url, {
method: 'PUT',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(skill),
});
}

View File

@@ -1,5 +0,0 @@
export const isEmailValid = email => {
let validEmailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return validEmailRegex.test (email);
};

View File

@@ -1,108 +0,0 @@
// In production, we register a service worker to serve assets from local cache.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on the "N+1" visit to a page, since previously
// cached resources are updated in the background.
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
// This link also includes instructions on opting out of this behavior.
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export default function register() {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl);
} else {
// Is not local host. Just register service worker
registerValidSW(swUrl);
}
});
}
}
function registerValidSW(swUrl) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log('New content is available; please refresh.');
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
if (
response.status === 404 ||
response.headers.get('content-type').indexOf('javascript') === -1
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}

View File

@@ -1,5 +0,0 @@
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import 'jest-enzyme';
configure({ adapter: new Adapter() });