diff --git a/src/components/flow/actions/helpers.tsx b/src/components/flow/actions/helpers.tsx index e0b0aac47..0ea99b96d 100644 --- a/src/components/flow/actions/helpers.tsx +++ b/src/components/flow/actions/helpers.tsx @@ -5,16 +5,22 @@ import { RecipientsAction, FlowIssue, FlowIssueType, - BroadcastMsg + BroadcastMsg, + SendMsg, + ComposeAttachment } from 'flowTypes'; import * as React from 'react'; import { Asset, AssetType } from 'store/flowContext'; -import { FormEntry, NodeEditorSettings, ValidationFailure } from 'store/nodeEditor'; +import { FormEntry, NodeEditorSettings, StringEntry, ValidationFailure } from 'store/nodeEditor'; import { createUUID } from 'utils'; import { Trans } from 'react-i18next'; import shared from 'components/shared.module.scss'; import { showHelpArticle } from 'external'; import { IssueProps } from '../props'; +import { SendBroadcastFormState } from './sendbroadcast/SendBroadcastForm'; +import { SendMsgFormState } from './sendmsg/SendMsgForm'; +import { MaxOf640Chars, MaxOfThreeItems, shouldRequireIf, validate } from 'store/validators'; +import i18n from 'config/i18n'; export const renderIssues = (issueProps: IssueProps): JSX.Element => { const { issues, helpArticles } = issueProps; @@ -102,7 +108,11 @@ export const getActionUUID = (nodeSettings: NodeEditorSettings, currentType: str return createUUID(); }; -export const getCompose = (action: BroadcastMsg = null): string => { +export const getEmptyComposeValue = (): string => { + return JSON.stringify({ text: '', attachments: [] }); +}; + +export const getComposeActionToState = (action: SendMsg | BroadcastMsg = null): string => { if (!action) { return getEmptyComposeValue(); } @@ -112,8 +122,67 @@ export const getCompose = (action: BroadcastMsg = null): string => { return action.compose; }; -export const getEmptyComposeValue = (): string => { - return JSON.stringify({ text: '', attachments: [] }); +export const validateCompose = (composeValue: string, submitting: boolean = false): StringEntry => { + let composeUpdate: StringEntry; + + // validate empty compose value + if (composeValue === getEmptyComposeValue()) { + composeUpdate = validate(i18n.t('forms.compose', 'Compose'), '', [shouldRequireIf(submitting)]); + composeUpdate.value = composeValue; + if (composeUpdate.validationFailures.length > 0) { + let composeErrMsg = composeUpdate.validationFailures[0].message; + composeErrMsg = composeErrMsg.replace('Compose is', 'Text or attachments are'); + composeUpdate.validationFailures[0].message = composeErrMsg; + } + return composeUpdate; + } + + // validate populated compose value + composeUpdate = validate(i18n.t('forms.compose', 'Compose'), composeValue, [ + shouldRequireIf(submitting) + ]); + // validate inner text value + const composeTextValue = getComposeByAsset(composeValue, AssetType.ComposeText); + const composeTextResult = validate(i18n.t('forms.compose', 'Compose'), composeTextValue, [ + MaxOf640Chars + ]); + if (composeTextResult.validationFailures.length > 0) { + let textErrMsg = composeTextResult.validationFailures[0].message; + textErrMsg = textErrMsg.replace('Compose cannot be more than', 'Maximum allowed text is'); + composeTextResult.validationFailures[0].message = textErrMsg; + composeUpdate.validationFailures = [ + ...composeUpdate.validationFailures, + ...composeTextResult.validationFailures + ]; + } + // validate inner attachments value + const composeAttachmentsValue = getComposeByAsset(composeValue, AssetType.ComposeAttachments); + const composeAttachmentsResult = validate( + i18n.t('forms.compose', 'Compose'), + composeAttachmentsValue, + [MaxOfThreeItems] + ); + if (composeAttachmentsResult.validationFailures.length > 0) { + let attachmentsErrMsg = composeAttachmentsResult.validationFailures[0].message; + attachmentsErrMsg = attachmentsErrMsg + .replace('Compose cannot have more than', 'Maximum allowed attachments is') + .replace('entries', 'files'); + composeAttachmentsResult.validationFailures[0].message = attachmentsErrMsg; + composeUpdate.validationFailures = [ + ...composeUpdate.validationFailures, + ...composeAttachmentsResult.validationFailures + ]; + } + return composeUpdate; +}; + +export const getComposeStateToAction = (state: SendMsgFormState | SendBroadcastFormState): any => { + const compose = state.compose.value; + const text = getComposeByAsset(compose, AssetType.ComposeText); + const attachments = getComposeByAsset(compose, AssetType.ComposeAttachments).map( + (attachment: ComposeAttachment) => `${attachment.content_type}:${attachment.url}` + ); + return [compose, text, attachments]; }; export const getRecipients = (action: RecipientsAction): Asset[] => { @@ -140,6 +209,13 @@ export const getRecipients = (action: RecipientsAction): Asset[] => { return selected; }; +export const getRecipientsStateToAction = (state: SendBroadcastFormState): any => { + const legacy_vars = getExpressions(state.recipients.value); + const contacts = getRecipientsByAsset(state.recipients.value, AssetType.Contact); + const groups = getRecipientsByAsset(state.recipients.value, AssetType.Group); + return [legacy_vars, contacts, groups]; +}; + export const renderAssetList = ( assets: Asset[], max: number = 10, diff --git a/src/components/flow/actions/sendbroadcast/SendBroadcastForm.tsx b/src/components/flow/actions/sendbroadcast/SendBroadcastForm.tsx index b828ff24c..ab746d456 100644 --- a/src/components/flow/actions/sendbroadcast/SendBroadcastForm.tsx +++ b/src/components/flow/actions/sendbroadcast/SendBroadcastForm.tsx @@ -6,11 +6,11 @@ import AssetSelector from 'components/form/assetselector/AssetSelector'; import TypeList from 'components/nodeeditor/TypeList'; import { fakePropType } from 'config/ConfigProvider'; import * as React from 'react'; -import { Asset, AssetType } from 'store/flowContext'; +import { Asset } from 'store/flowContext'; import { AssetArrayEntry, FormState, mergeForm, StringEntry } from 'store/nodeEditor'; -import { MaxOf640Chars, MaxOfThreeItems, shouldRequireIf, validate } from 'store/validators'; +import { shouldRequireIf, validate } from 'store/validators'; import i18n from 'config/i18n'; -import { getComposeByAsset, getEmptyComposeValue, renderIssues } from '../helpers'; +import { renderIssues, validateCompose } from '../helpers'; import ComposeElement from 'components/form/compose/ComposeElement'; export interface SendBroadcastFormState extends FormState { @@ -51,57 +51,7 @@ export default class SendBroadcastForm extends React.Component< const updates: Partial = {}; if (keys.hasOwnProperty('compose')) { - // validate empty compose value - if (keys.compose === getEmptyComposeValue()) { - updates.compose = validate(i18n.t('forms.compose', 'Compose'), '', [ - shouldRequireIf(submitting) - ]); - updates.compose.value = keys.compose; - if (updates.compose.validationFailures.length > 0) { - let composeErrMsg = updates.compose.validationFailures[0].message; - composeErrMsg = composeErrMsg.replace('Compose is', 'Text or attachments are'); - updates.compose.validationFailures[0].message = composeErrMsg; - } - } else { - updates.compose = validate(i18n.t('forms.compose', 'Compose'), keys.compose, [ - shouldRequireIf(submitting) - ]); - // validate inner compose text value - const composeTextValue = getComposeByAsset(keys.compose, AssetType.ComposeText); - const composeTextResult = validate(i18n.t('forms.compose', 'Compose'), composeTextValue, [ - MaxOf640Chars - ]); - if (composeTextResult.validationFailures.length > 0) { - let textErrMsg = composeTextResult.validationFailures[0].message; - textErrMsg = textErrMsg.replace('Compose cannot be more than', 'Maximum allowed text is'); - composeTextResult.validationFailures[0].message = textErrMsg; - updates.compose.validationFailures = [ - ...updates.compose.validationFailures, - ...composeTextResult.validationFailures - ]; - } - // validate inner compose attachments value - const composeAttachmentsValue = getComposeByAsset( - keys.compose, - AssetType.ComposeAttachments - ); - const composeAttachmentsResult = validate( - i18n.t('forms.compose', 'Compose'), - composeAttachmentsValue, - [MaxOfThreeItems] - ); - if (composeAttachmentsResult.validationFailures.length > 0) { - let attachmentsErrMsg = composeAttachmentsResult.validationFailures[0].message; - attachmentsErrMsg = attachmentsErrMsg - .replace('Compose cannot have more than', 'Maximum allowed attachments is') - .replace('entries', 'files'); - composeAttachmentsResult.validationFailures[0].message = attachmentsErrMsg; - updates.compose.validationFailures = [ - ...updates.compose.validationFailures, - ...composeAttachmentsResult.validationFailures - ]; - } - } + updates.compose = validateCompose(keys.compose, submitting); } if (keys.hasOwnProperty('recipients')) { diff --git a/src/components/flow/actions/sendbroadcast/helpers.ts b/src/components/flow/actions/sendbroadcast/helpers.ts index 5855ef052..039ea2884 100644 --- a/src/components/flow/actions/sendbroadcast/helpers.ts +++ b/src/components/flow/actions/sendbroadcast/helpers.ts @@ -1,15 +1,13 @@ import { getActionUUID, - getCompose, - getComposeByAsset, - getExpressions, + getComposeActionToState, + getComposeStateToAction, getRecipients, - getRecipientsByAsset + getRecipientsStateToAction } from 'components/flow/actions/helpers'; import { SendBroadcastFormState } from 'components/flow/actions/sendbroadcast/SendBroadcastForm'; import { Types } from 'config/interfaces'; -import { BroadcastMsg, ComposeAttachment } from 'flowTypes'; -import { AssetType } from 'store/flowContext'; +import { BroadcastMsg } from 'flowTypes'; import { NodeEditorSettings } from 'store/nodeEditor'; export const initializeForm = (settings: NodeEditorSettings): SendBroadcastFormState => { @@ -23,7 +21,7 @@ export const initializeForm = (settings: NodeEditorSettings): SendBroadcastFormS action = settings.localizations[0].getObject() as BroadcastMsg; } else { return { - compose: { value: getCompose() }, + compose: { value: getComposeActionToState() }, recipients: { value: [] }, valid: true }; @@ -31,14 +29,14 @@ export const initializeForm = (settings: NodeEditorSettings): SendBroadcastFormS } return { - compose: { value: getCompose(action) }, + compose: { value: getComposeActionToState(action) }, recipients: { value: getRecipients(action) }, valid: true }; } return { - compose: { value: getCompose() }, + compose: { value: getComposeActionToState() }, recipients: { value: [] }, valid: false }; @@ -48,16 +46,13 @@ export const stateToAction = ( settings: NodeEditorSettings, formState: SendBroadcastFormState ): BroadcastMsg => { - const compose = formState.compose.value; - const text = getComposeByAsset(compose, AssetType.ComposeText); - const attachments = getComposeByAsset(compose, AssetType.ComposeAttachments).map( - (attachment: ComposeAttachment) => `${attachment.content_type}:${attachment.url}` - ); + const [compose, text, attachments] = getComposeStateToAction(formState); + const [legacy_vars, contacts, groups] = getRecipientsStateToAction(formState); return { - legacy_vars: getExpressions(formState.recipients.value), - contacts: getRecipientsByAsset(formState.recipients.value, AssetType.Contact), - groups: getRecipientsByAsset(formState.recipients.value, AssetType.Group), + legacy_vars: legacy_vars, + contacts: contacts, + groups: groups, compose: compose, text: text, attachments: attachments, diff --git a/src/components/flow/actions/sendmsg/SendMsg.tsx b/src/components/flow/actions/sendmsg/SendMsg.tsx index aa1503dce..d9c6f6ace 100644 --- a/src/components/flow/actions/sendmsg/SendMsg.tsx +++ b/src/components/flow/actions/sendmsg/SendMsg.tsx @@ -4,51 +4,53 @@ import * as React from 'react'; import styles from './SendMsg.module.scss'; import i18n from 'config/i18n'; +import { ellipsize } from 'utils'; export const PLACEHOLDER = i18n.t('actions.send_msg.placeholder', 'Send a message to the contact'); const SendMsgComp: React.SFC = (action: SendMsg): JSX.Element => { - if (action.text) { - let replies = null; + let replies = null; - let quickReplies = action.quick_replies || []; - if (quickReplies.length > 0) { - replies = ( -
- {quickReplies.map(reply => ( - - ))} -
- ); - } + let quickReplies = action.quick_replies || []; + if (quickReplies.length > 0) { + replies = ( +
+ {quickReplies.map(reply => ( + + ))} +
+ ); + } - return ( - <> -
- {action.text.split(/\r?\n/).map((line: string, idx: number) => ( + return ( + <> +
+ {action.text && action.text.length > 0 ? ( + action.text.split(/\r?\n/).map((line: string, idx: number) => (
- {line} + {ellipsize(line, 125)}
- ))} - {action.attachments && action.attachments.length > 0 ? ( -
- ) : null} - {action.templating && action.templating.template ? ( -
- ) : null} - {action.topic ?
: null} -
-
{replies}
- - ); - } - return
{PLACEHOLDER}
; + )) + ) : ( +
{PLACEHOLDER}
+ )} + {action.attachments && action.attachments.length > 0 ? ( +
+ ) : null} + {action.templating && action.templating.template ? ( +
+ ) : null} + {action.topic ?
: null} +
+
{replies}
+ + ); }; export default SendMsgComp; diff --git a/src/components/flow/actions/sendmsg/SendMsgForm.tsx b/src/components/flow/actions/sendmsg/SendMsgForm.tsx index da327273a..6ab0278ac 100644 --- a/src/components/flow/actions/sendmsg/SendMsgForm.tsx +++ b/src/components/flow/actions/sendmsg/SendMsgForm.tsx @@ -1,9 +1,8 @@ /* eslint-disable @typescript-eslint/explicit-member-accessibility */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import { react as bindCallbacks } from 'auto-bind'; -import { AxiosError, AxiosResponse } from 'axios'; import Dialog, { ButtonSet, Tab } from 'components/dialog/Dialog'; -import { hasErrors, renderIssues } from 'components/flow/actions/helpers'; +import { hasErrors, renderIssues, validateCompose } from 'components/flow/actions/helpers'; import { initializeForm as stateToForm, stateToAction, @@ -31,7 +30,7 @@ import { SelectOptionEntry, FormEntry } from 'store/nodeEditor'; -import { MaxOfTenItems, Required, shouldRequireIf, validate } from 'store/validators'; +import { MaxOfTenItems, Required, validate } from 'store/validators'; import { range } from 'utils'; import styles from './SendMsgForm.module.scss'; @@ -40,16 +39,13 @@ import { FeatureFilter } from 'config/interfaces'; import i18n from 'config/i18n'; import { Trans } from 'react-i18next'; -import { Attachment, renderAttachments } from './attachments'; +import ComposeElement from 'components/form/compose/ComposeElement'; export interface SendMsgFormState extends FormState { - message: StringEntry; + compose: StringEntry; quickReplies: StringArrayEntry; quickReplyEntry: StringEntry; sendAll: boolean; - attachments: Attachment[]; - uploadInProgress: boolean; - uploadError: string; template: FormEntry; topic: SelectOptionEntry; templateVariables: StringEntry[]; @@ -84,17 +80,16 @@ export default class SendMsgForm extends React.Component = {}; - if (keys.hasOwnProperty('text')) { - updates.message = validate(i18n.t('forms.message', 'Message'), keys.text, [ - shouldRequireIf(submitting) - ]); + + if (keys.hasOwnProperty('compose')) { + updates.compose = validateCompose(keys.compose, submitting); } if (keys.hasOwnProperty('sendAll')) { @@ -115,12 +110,8 @@ export default class SendMsgForm extends React.Component= 500) { - uploadError = i18n.t('file_upload_failed_generic', 'File upload failed, please try again'); - } else if (status === 413) { - uploadError = i18n.t('file_upload_failed_max_limit', 'Limit for file uploads is 25 MB'); - } else { - uploadError = error.response.statusText; - } - this.setState({ uploadError }); - - const uploadInProgress: boolean = false; - this.setState({ uploadInProgress }); - } - - private handleAttachmentChanged(index: number, type: string, url: string) { - this.handleAttachmentUploading(false); - - let attachments: any = this.state.attachments; - if (index === -1) { - attachments = mutate(attachments, { - $push: [{ type, url }] - }); - } else { - attachments = mutate(attachments, { - [index]: { - $set: { type, url } - } - }); - } - - this.setState({ attachments }); - } - - private handleAttachmentRemoved(index: number) { - const attachments: any = mutate(this.state.attachments, { - $splice: [[index, 1]] - }); - this.setState({ attachments }); - } - public render(): JSX.Element { const typeConfig = this.props.typeConfig; @@ -394,22 +306,6 @@ export default class SendMsgForm extends React.Component 0 - }; - const advanced: Tab = { name: i18n.t('forms.advanced', 'Advanced'), body: ( @@ -428,7 +324,7 @@ export default class SendMsgForm extends React.Component - - + {renderIssues(this.props)} ); diff --git a/src/components/flow/actions/sendmsg/helpers.ts b/src/components/flow/actions/sendmsg/helpers.ts index 50b988df7..64413f46c 100644 --- a/src/components/flow/actions/sendmsg/helpers.ts +++ b/src/components/flow/actions/sendmsg/helpers.ts @@ -1,5 +1,9 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { getActionUUID } from 'components/flow/actions/helpers'; +import { + getActionUUID, + getComposeActionToState, + getComposeStateToAction +} from 'components/flow/actions/helpers'; import { SendMsgFormState } from 'components/flow/actions/sendmsg/SendMsgForm'; import { Types } from 'config/interfaces'; import { MsgTemplating, SendMsg } from 'flowTypes'; @@ -7,7 +11,6 @@ import { AssetStore } from 'store/flowContext'; import { FormEntry, NodeEditorSettings, StringEntry } from 'store/nodeEditor'; import { SelectOption } from 'components/form/select/SelectElement'; import { createUUID } from 'utils'; -import { Attachment } from './attachments'; export const TOPIC_OPTIONS: SelectOption[] = [ { value: 'event', name: 'Event' }, @@ -25,19 +28,6 @@ export const initializeForm = ( if (settings.originalAction && settings.originalAction.type === Types.send_msg) { const action = settings.originalAction as SendMsg; - const attachments: Attachment[] = []; - (action.attachments || []).forEach((attachmentString: string) => { - const splitPoint = attachmentString.indexOf(':'); - - const type = attachmentString.substring(0, splitPoint); - const attachment = { - type, - url: attachmentString.substring(splitPoint + 1), - uploaded: type.indexOf('/') > -1 - }; - - attachments.push(attachment); - }); if (action.templating) { const msgTemplate = action.templating.template; @@ -55,13 +45,10 @@ export const initializeForm = ( } return { + compose: { value: getComposeActionToState(action) }, topic: { value: TOPIC_OPTIONS.find(option => option.value === action.topic) }, template, templateVariables, - attachments, - uploadInProgress: false, - uploadError: '', - message: { value: action.text }, quickReplies: { value: action.quick_replies || [] }, quickReplyEntry: { value: '' }, sendAll: action.all_urns, @@ -70,13 +57,10 @@ export const initializeForm = ( } return { + compose: { value: getComposeActionToState() }, topic: { value: null }, template, templateVariables: [], - attachments: [], - uploadInProgress: false, - uploadError: '', - message: { value: '' }, quickReplies: { value: [] }, quickReplyEntry: { value: '' }, sendAll: false, @@ -85,9 +69,7 @@ export const initializeForm = ( }; export const stateToAction = (settings: NodeEditorSettings, state: SendMsgFormState): SendMsg => { - const attachments = state.attachments - .filter((attachment: Attachment) => attachment.url.trim().length > 0) - .map((attachment: Attachment) => `${attachment.type}:${attachment.url}`); + const [compose, text, attachments] = getComposeStateToAction(state); let templating: MsgTemplating = null; @@ -115,8 +97,9 @@ export const stateToAction = (settings: NodeEditorSettings, state: SendMsgFormSt } const result: SendMsg = { - attachments, - text: state.message.value, + compose: compose, + text: text, + attachments: attachments, type: Types.send_msg, all_urns: state.sendAll, quick_replies: state.quickReplies.value, diff --git a/src/flowTypes.ts b/src/flowTypes.ts index f485d271c..65aa9d1e6 100644 --- a/src/flowTypes.ts +++ b/src/flowTypes.ts @@ -368,10 +368,11 @@ export interface MsgTemplating { } export interface SendMsg extends Action { - text: string; + compose: string; + text?: string; + attachments?: string[]; all_urns?: boolean; quick_replies?: string[]; - attachments?: string[]; topic?: string; templating?: MsgTemplating; } diff --git a/src/store/thunks.ts b/src/store/thunks.ts index d8eea9c95..fba93882c 100644 --- a/src/store/thunks.ts +++ b/src/store/thunks.ts @@ -69,6 +69,7 @@ import { createUUID, hasString, NODE_SPACING, timeEnd, timeStart, ACTIVITY_INTER import { AxiosError } from 'axios'; import i18n from 'config/i18n'; import { TembaStore } from 'temba-components'; +import { getEmptyComposeValue } from 'components/flow/actions/helpers'; // TODO: Remove use of Function // tslint:disable:ban-types @@ -810,7 +811,9 @@ export const onAddToNode = (node: FlowNode) => ( const newAction: SendMsg = { uuid: createUUID(), type: Types.send_msg, - text: '' + compose: getEmptyComposeValue(), + text: '', + attachments: [] }; dispatch( diff --git a/src/testUtils/assetCreators.ts b/src/testUtils/assetCreators.ts index 07b78d6ff..e21b186d0 100644 --- a/src/testUtils/assetCreators.ts +++ b/src/testUtils/assetCreators.ts @@ -1,4 +1,5 @@ import { AxiosError, AxiosResponse } from 'axios'; +import { getEmptyComposeValue } from 'components/flow/actions/helpers'; import { determineTypeConfig } from 'components/flow/helpers'; import { ActionFormProps, LocalizationFormProps, RouterFormProps } from 'components/flow/props'; import { CaseProps } from 'components/flow/routers/caselist/CaseList'; @@ -95,17 +96,23 @@ export const createPlayAudioAction = ({ export const createSendMsgAction = ({ uuid = utils.createUUID(), - text = 'Hey!', + compose = JSON.stringify({ text: 'Some message', attachments: [] }), + text = 'Some message', + attachments = [], all_urns = false }: { uuid?: string; + compose?: string; text?: string; + attachments?: string[]; // tslint:disable-next-line:variable-name all_urns?: boolean; } = {}): SendMsg => ({ type: Types.send_msg, uuid, + compose, text, + attachments, all_urns });