diff --git a/package.json b/package.json index 910ee4b64..06083b932 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "private": false, "dependencies": { "react": "^16.8.6", - "react-dom": "^16.8.6" + "react-dom": "^16.8.6", + "react-notifications": "1.7.2" }, "scripts": { "lambda:serve": "netlify-lambda serve lambda -p 6000", diff --git a/public/index.html b/public/index.html index 7238ec205..35a52dd6a 100644 --- a/public/index.html +++ b/public/index.html @@ -118,105 +118,104 @@ To begin the development, run `npm start` or `yarn start`. To create a production bundle, use `npm run build` or `yarn build`. --> - - - - - \ No newline at end of file + + + + diff --git a/src/components/flow/actions/sendmsg/SendMsgForm.tsx b/src/components/flow/actions/sendmsg/SendMsgForm.tsx index 67ca009f0..1d49f7d68 100644 --- a/src/components/flow/actions/sendmsg/SendMsgForm.tsx +++ b/src/components/flow/actions/sendmsg/SendMsgForm.tsx @@ -4,6 +4,8 @@ import { react as bindCallbacks } from 'auto-bind'; import axios from 'axios'; import Dialog, { ButtonSet, Tab } from 'components/dialog/Dialog'; import { hasErrors, renderIssues } from 'components/flow/actions/helpers'; +import 'react-notifications/lib/notifications.css'; +import { NotificationManager } from 'react-notifications'; import { initializeForm as stateToForm, stateToAction, @@ -23,7 +25,7 @@ import { fetchAsset, getCookie } from 'external'; import { Template, TemplateTranslation } from 'flowTypes'; import mutate from 'immutability-helper'; import * as React from 'react'; -import { Asset } from 'store/flowContext'; +import flowContext, { Asset } from 'store/flowContext'; import { FormState, mergeForm, @@ -42,6 +44,7 @@ import { FeatureFilter } from 'config/interfaces'; import i18n from 'config/i18n'; import { Trans } from 'react-i18next'; import { TembaSelectStyle } from 'temba/TembaSelect'; +import Modal from 'components/modal/Modal'; const MAX_ATTACHMENTS = 1; @@ -52,8 +55,6 @@ const TYPE_OPTIONS: SelectOption[] = [ { value: 'application', name: i18n.t('forms.pdf_url', 'PDF Document URL') } ]; -const NEW_TYPE_OPTIONS = TYPE_OPTIONS.concat([{ value: 'upload', name: 'Upload Attachment' }]); - const getAttachmentTypeOption = (type: string): SelectOption => { return TYPE_OPTIONS.find((option: SelectOption) => option.value === type); }; @@ -74,6 +75,7 @@ export interface SendMsgFormState extends FormState { topic: SelectOptionEntry; templateVariables: StringEntry[]; templateTranslation?: TemplateTranslation; + validAttachment: any; } export default class SendMsgForm extends React.Component { @@ -151,33 +153,103 @@ export default class SendMsgForm extends React.Component { + if (response.headers['content-type'].startsWith(type)) { + this.setState({ validAttachment: false }); + NotificationManager.success(`The attachment url is valid`, 'Valid URL', 3000); + if (hasErrors(this.state.message)) { + return; + } + + // make sure we validate untouched text fields and contact fields + let valid = this.handleMessageUpdate(this.state.message.value, null, true); + + let templateVariables = this.state.templateVariables; + // make sure we don't have untouched template variables + this.state.templateVariables.forEach((variable: StringEntry, num: number) => { + const updated = validate(`Variable ${num + 1}`, variable.value, [Required]); + templateVariables = mutate(templateVariables, { + [num]: { $merge: updated } + }) as StringEntry[]; + valid = valid && !hasErrors(updated); + }); + + valid = valid && !hasErrors(this.state.quickReplyEntry); + + if (valid) { + this.props.updateAction(stateToAction(this.props.nodeSettings, this.state)); + // notify our modal we are done + this.props.onClose(false); + } else { + this.setState({ templateVariables, valid }); + } + } else { + NotificationManager.error( + errorMessage ? errorMessage : `Not a valid ${type} url`, + 'Invalid URL', + 3000 + ); + } + }) + .catch(error => { + NotificationManager.error(error.toString(), 'Invalid attachment URL', 5000); + }); + } + private handleSave(): void { - // don't continue if our message already has errors - if (hasErrors(this.state.message)) { - return; - } + if (this.state.attachments.length > 0) { + const type = this.state.attachments[0].type; + const url = 'https://cors-anywhere.herokuapp.com/' + this.state.attachments[0].url; + + switch (type) { + case 'image': + this.handleAxios(url, 'image', null); + break; + + case 'video': + this.handleAxios(url, 'video', null); + break; + + case 'audio': + this.handleAxios(url, 'audio', null); + break; + case 'application': + this.handleAxios(url, 'application', 'Not a valid pdf url'); + break; + } + this.setState({ validAttachment: true }); + } else { + // don't continue if our message already has errors + if (hasErrors(this.state.message)) { + return; + } - // make sure we validate untouched text fields and contact fields - let valid = this.handleMessageUpdate(this.state.message.value, null, true); - - let templateVariables = this.state.templateVariables; - // make sure we don't have untouched template variables - this.state.templateVariables.forEach((variable: StringEntry, num: number) => { - const updated = validate(`Variable ${num + 1}`, variable.value, [Required]); - templateVariables = mutate(templateVariables, { - [num]: { $merge: updated } - }) as StringEntry[]; - valid = valid && !hasErrors(updated); - }); + // make sure we validate untouched text fields and contact fields + let valid = this.handleMessageUpdate(this.state.message.value, null, true); + + let templateVariables = this.state.templateVariables; + // make sure we don't have untouched template variables + this.state.templateVariables.forEach((variable: StringEntry, num: number) => { + const updated = validate(`Variable ${num + 1}`, variable.value, [Required]); + templateVariables = mutate(templateVariables, { + [num]: { $merge: updated } + }) as StringEntry[]; + valid = valid && !hasErrors(updated); + }); - valid = valid && !hasErrors(this.state.quickReplyEntry); + valid = valid && !hasErrors(this.state.quickReplyEntry); - if (valid) { - this.props.updateAction(stateToAction(this.props.nodeSettings, this.state)); - // notify our modal we are done - this.props.onClose(false); - } else { - this.setState({ templateVariables, valid }); + if (valid) { + this.props.updateAction(stateToAction(this.props.nodeSettings, this.state)); + // notify our modal we are done + this.props.onClose(false); + } else { + this.setState({ templateVariables, valid }); + } } } @@ -304,7 +376,7 @@ export default class SendMsgForm extends React.Component -1 ? TYPE_OPTIONS : NEW_TYPE_OPTIONS} + options={TYPE_OPTIONS} /> {index > -1 ? ( @@ -523,7 +595,8 @@ export default class SendMsgForm extends React.Component 0 + checked: this.state.attachments.length > 0, + hasErrors: this.state.validAttachment }; const advanced: Tab = { @@ -567,26 +640,28 @@ export default class SendMsgForm extends React.Component - - - - {renderIssues(this.props)} - + <> + + + + + {renderIssues(this.props)} + + ); } } diff --git a/src/components/flow/actions/sendmsg/helpers.ts b/src/components/flow/actions/sendmsg/helpers.ts index 0751208d9..4a7515d29 100644 --- a/src/components/flow/actions/sendmsg/helpers.ts +++ b/src/components/flow/actions/sendmsg/helpers.ts @@ -62,7 +62,8 @@ export const initializeForm = ( quickReplies: { value: action.quick_replies || [] }, quickReplyEntry: { value: '' }, sendAll: action.all_urns, - valid: true + valid: true, + validAttachment: false }; } @@ -75,7 +76,8 @@ export const initializeForm = ( quickReplies: { value: [] }, quickReplyEntry: { value: '' }, sendAll: false, - valid: false + valid: false, + validAttachment: false }; }; diff --git a/src/components/index.tsx b/src/components/index.tsx index 665edbc40..682b005f9 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -6,6 +6,7 @@ import ConnectedFlow from 'components/flow/Flow'; import styles from 'components/index.module.scss'; import ConnectedLanguageSelector from 'components/languageselector/LanguageSelector'; import Loading from 'components/loading/Loading'; +import { NotificationContainer } from 'react-notifications'; import Modal from 'components/modal/Modal'; import { RevisionExplorer } from 'components/revisions/RevisionExplorer'; import { IssuesTab, IssueDetail } from 'components/issues/IssuesTab'; @@ -311,6 +312,7 @@ export class FlowEditor extends React.Component { popped={this.props.popped} /> )} +
diff --git a/src/untyped.d.ts b/src/untyped.d.ts index 64206d78c..e88cfe5b6 100644 --- a/src/untyped.d.ts +++ b/src/untyped.d.ts @@ -1,7 +1,7 @@ declare module 'classnames/bind'; declare module 'http-proxy-middleware'; declare module 'http-headers-validation'; - +declare module 'react-notifications'; declare namespace JSX { interface IntrinsicElements { 'temba-textinput': any; diff --git a/yarn.lock b/yarn.lock index 8064763dc..62adc62c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1141,6 +1141,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.8.7": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" + integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.10.4", "@babel/template@^7.4.0", "@babel/template@^7.8.6": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" @@ -2450,16 +2457,16 @@ acorn-walk@^6.0.0, acorn-walk@^6.0.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c" integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA== +acorn@6.4.1, acorn@^6.0.0, acorn@^6.0.1, acorn@^6.0.4, acorn@^6.2.1, acorn@^6.4.1: + version "6.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" + integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== + acorn@^5.5.3: version "5.7.4" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e" integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg== -acorn@^6.0.0, acorn@^6.0.1, acorn@^6.0.4, acorn@^6.2.1, acorn@^6.4.1: - version "6.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" - integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== - acorn@^7.1.1: version "7.4.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c" @@ -3695,7 +3702,7 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -classnames@2.2.6: +classnames@2.2.6, classnames@^2.1.1: version "2.2.6" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== @@ -4757,6 +4764,14 @@ dom-helpers@^3.4.0: dependencies: "@babel/runtime" "^7.1.2" +dom-helpers@^5.0.1: + version "5.2.0" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.0.tgz#57fd054c5f8f34c52a3eeffdb7e7e93cd357d95b" + integrity sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + dom-serializer@0: version "0.2.2" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" @@ -11096,6 +11111,16 @@ react-modal@3.8.1: react-lifecycles-compat "^3.0.0" warning "^3.0.0" +react-notifications@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/react-notifications/-/react-notifications-1.7.2.tgz#e70e2c053f86321c0a7fa81ff638922e2fdd572f" + integrity sha512-3mlMiNLDQtp64IP+EnYx3xgmbdpzrLQiSO8AP+8o4LiYQC6HcgdVB+MMdGuYZ1ttfKeLgTgol2ESFQJhmz0O3Q== + dependencies: + acorn "6.4.1" + classnames "^2.1.1" + prop-types "^15.5.10" + react-transition-group "^4.4.1" + react-page-visibility@3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/react-page-visibility/-/react-page-visibility-3.2.1.tgz#4c7e4bad8e047ba8279fdee0c9ba1ce9a0db6212" @@ -11227,6 +11252,16 @@ react-transition-group@^2.2.1: prop-types "^15.6.2" react-lifecycles-compat "^3.0.4" +react-transition-group@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9" + integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react@^16.8.6: version "16.13.1" resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"