@@ -251,51 +208,39 @@ export default class SendMsgForm extends React.Component
-
- {this.state.templateTranslation ? (
- <>
- {this.state.templateTranslation.content}
- {range(0, this.state.templateTranslation.variable_count).map((num: number) => {
- return (
-
- {
- this.handleTemplateVariableChanged(updatedText, num);
- }}
- entry={this.state.templateVariables[num]}
- autocomplete={true}
- />
-
- );
- })}
- >
- ) : null}
+
>
);
}
private handleAttachmentUploading(isUploading: boolean) {
- const uploadError: string = '';
+ const uploadError = '';
console.log(uploadError);
this.setState({ uploadError });
if (isUploading) {
- const uploadInProgress: boolean = true;
+ const uploadInProgress = true;
this.setState({ uploadInProgress });
} else {
- const uploadInProgress: boolean = false;
+ const uploadInProgress = false;
this.setState({ uploadInProgress });
}
}
@@ -310,21 +255,19 @@ export default class SendMsgForm extends React.Component= 500) {
uploadError = i18n.t('file_upload_failed_generic', 'File upload failed, please try again');
@@ -335,7 +278,7 @@ export default class SendMsgForm extends React.Component hasErrors(entry))
+ checked: this.state.template.value != null
+ // hasErrors: !!this.state.templateVariables.find((entry: StringEntry) => hasErrors(entry))
};
tabs.splice(0, 0, templates);
}
diff --git a/src/components/flow/actions/sendmsg/__snapshots__/SendMsgForm.test.ts.snap b/src/components/flow/actions/sendmsg/__snapshots__/SendMsgForm.test.ts.snap
index c5f01c14c..2ba45971d 100644
--- a/src/components/flow/actions/sendmsg/__snapshots__/SendMsgForm.test.ts.snap
+++ b/src/components/flow/actions/sendmsg/__snapshots__/SendMsgForm.test.ts.snap
@@ -125,22 +125,20 @@ exports[`SendMsgForm render should render 1`] = `
Sending messages over a WhatsApp channel requires that a template be used if you have not received a message from a contact in the last 24 hours. Setting a template to use over WhatsApp is especially important for the first message in your flow.
-
,
"checked": false,
- "hasErrors": false,
"name": "WhatsApp",
},
]
@@ -158,7 +156,6 @@ exports[`SendMsgForm render should render 1`] = `
"localizeableKeys": Array [
"text",
"quick_replies",
- "templating.variables",
],
"massageForDisplay": [Function],
"name": "Send Message",
@@ -207,6 +204,7 @@ Object {
"validationFailures": Array [],
"value": "What is your favorite color?",
},
+ "paramsByTemplate": Object {},
"quickReplies": Object {
"validationFailures": Array [],
"value": Array [
@@ -222,7 +220,6 @@ Object {
"template": Object {
"value": null,
},
- "templateVariables": Array [],
"topic": Object {
"value": undefined,
},
diff --git a/src/components/flow/actions/sendmsg/helpers.ts b/src/components/flow/actions/sendmsg/helpers.ts
index 50b988df7..73dcfc529 100644
--- a/src/components/flow/actions/sendmsg/helpers.ts
+++ b/src/components/flow/actions/sendmsg/helpers.ts
@@ -1,13 +1,13 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { getActionUUID } from 'components/flow/actions/helpers';
import { SendMsgFormState } from 'components/flow/actions/sendmsg/SendMsgForm';
import { Types } from 'config/interfaces';
import { MsgTemplating, SendMsg } from 'flowTypes';
-import { AssetStore } from 'store/flowContext';
-import { FormEntry, NodeEditorSettings, StringEntry } from 'store/nodeEditor';
+import { FormEntry, NodeEditorSettings } from 'store/nodeEditor';
import { SelectOption } from 'components/form/select/SelectElement';
-import { createUUID } from 'utils';
import { Attachment } from './attachments';
+import { createUUID } from 'utils';
export const TOPIC_OPTIONS: SelectOption[] = [
{ value: 'event', name: 'Event' },
@@ -16,12 +16,8 @@ export const TOPIC_OPTIONS: SelectOption[] = [
{ value: 'agent', name: 'Agent' }
];
-export const initializeForm = (
- settings: NodeEditorSettings,
- assetStore: AssetStore
-): SendMsgFormState => {
+export const initializeForm = (settings: NodeEditorSettings): SendMsgFormState => {
let template: FormEntry = { value: null };
- let templateVariables: StringEntry[] = [];
if (settings.originalAction && settings.originalAction.type === Types.send_msg) {
const action = settings.originalAction as SendMsg;
@@ -39,6 +35,7 @@ export const initializeForm = (
attachments.push(attachment);
});
+ let paramsByTemplate: any = {};
if (action.templating) {
const msgTemplate = action.templating.template;
template = {
@@ -47,17 +44,20 @@ export const initializeForm = (
name: msgTemplate.name
}
};
- templateVariables = action.templating.variables.map((value: string) => {
- return {
- value
+
+ if (action.templating.components && action.templating.components.length > 0) {
+ paramsByTemplate = {
+ [action.templating.template.uuid]: {}
};
- });
+ action.templating.components.forEach((component: any) => {
+ paramsByTemplate[action.templating.template.uuid][component.name] = component.params;
+ });
+ }
}
return {
topic: { value: TOPIC_OPTIONS.find(option => option.value === action.topic) },
template,
- templateVariables,
attachments,
uploadInProgress: false,
uploadError: '',
@@ -65,6 +65,7 @@ export const initializeForm = (
quickReplies: { value: action.quick_replies || [] },
quickReplyEntry: { value: '' },
sendAll: action.all_urns,
+ paramsByTemplate,
valid: true
};
}
@@ -72,13 +73,13 @@ export const initializeForm = (
return {
topic: { value: null },
template,
- templateVariables: [],
attachments: [],
uploadInProgress: false,
uploadError: '',
message: { value: '' },
quickReplies: { value: [] },
quickReplyEntry: { value: '' },
+ paramsByTemplate: {},
sendAll: false,
valid: false
};
@@ -90,27 +91,36 @@ export const stateToAction = (settings: NodeEditorSettings, state: SendMsgFormSt
.map((attachment: Attachment) => `${attachment.type}:${attachment.url}`);
let templating: MsgTemplating = null;
-
if (state.template && state.template.value) {
- let templatingUUID = createUUID();
- if (settings.originalAction && settings.originalAction.type === Types.send_msg) {
- const action = settings.originalAction as SendMsg;
- if (
- action.templating &&
- action.templating.template &&
- action.templating.template.uuid === state.template.value.id
- ) {
- templatingUUID = action.templating.uuid;
+ const components = Object.keys(state.templateTranslation.components).map((key: string) => {
+ let uuid = createUUID();
+
+ // try looking up the uuid from the original action
+ if (settings.originalAction && settings.originalAction.type === Types.send_msg) {
+ const originalAction = settings.originalAction as SendMsg;
+ if (originalAction.templating) {
+ const originalComponent = originalAction.templating.components.find(
+ (component: any) => component.name === key
+ );
+ if (originalComponent) {
+ uuid = originalComponent.uuid;
+ }
+ }
}
- }
+
+ return {
+ uuid,
+ name: key,
+ params: state.paramsByTemplate[state.template.value.uuid][key]
+ };
+ });
templating = {
- uuid: templatingUUID,
template: {
uuid: state.template.value.uuid,
name: state.template.value.name
},
- variables: state.templateVariables.map((variable: StringEntry) => variable.value)
+ components
};
}
diff --git a/src/components/flow/props.ts b/src/components/flow/props.ts
index 022f42726..f2226f70f 100644
--- a/src/components/flow/props.ts
+++ b/src/components/flow/props.ts
@@ -15,6 +15,7 @@ export interface ActionFormProps extends IssueProps {
nodeSettings: NodeEditorSettings;
typeConfig: Type;
assetStore: AssetStore;
+ language: Asset;
addAsset(assetType: string, asset: Asset): void;
@@ -49,6 +50,7 @@ export interface LocalizationFormProps extends IssueProps {
updateLocalizations(languageCode: string, localizations: any[]): void;
onClose(canceled: boolean): void;
helpArticles: { [key: string]: string };
+ assetStore: AssetStore;
}
export const NAME_PROPERTY: Asset = {
diff --git a/src/components/form/assetselector/helpers.ts b/src/components/form/assetselector/helpers.ts
index ca4e93da2..44706d4a0 100644
--- a/src/components/form/assetselector/helpers.ts
+++ b/src/components/form/assetselector/helpers.ts
@@ -1,4 +1,3 @@
-import { Template } from 'flowTypes';
import { Asset, REMOVE_VALUE_ASSET } from 'store/flowContext';
/**
@@ -22,13 +21,3 @@ export const sortByName = (a: Asset, b: Asset): number => {
}
return 0;
};
-
-export const hasPendingTranslation = (template: Template) => {
- return !!template.translations.find(translation => translation.status === 'pending');
-};
-
-export const hasUseableTranslation = (template: Template) => {
- return !!template.translations.find(
- translation => translation.status === 'pending' || translation.status === 'approved'
- );
-};
diff --git a/src/components/form/assetselector/widgets.tsx b/src/components/form/assetselector/widgets.tsx
index 062748932..e2c30d561 100644
--- a/src/components/form/assetselector/widgets.tsx
+++ b/src/components/form/assetselector/widgets.tsx
@@ -1,6 +1,4 @@
import * as React from 'react';
-import { hasPendingTranslation } from 'components/form/assetselector/helpers';
-import { Template } from 'flowTypes';
import { Asset, AssetType } from 'store/flowContext';
export const getIconForAssetType = (asset: Asset): JSX.Element => {
@@ -13,12 +11,6 @@ export const getIconForAssetType = (asset: Asset): JSX.Element => {
return ;
case AssetType.Scheme:
return ;
- case AssetType.Template:
- if (hasPendingTranslation(asset.content as Template)) {
- return ;
- } else {
- return ;
- }
case AssetType.Remove:
return (
<>
diff --git a/src/components/nodeeditor/NodeEditor.tsx b/src/components/nodeeditor/NodeEditor.tsx
index 502e21e7b..af8d80a8d 100644
--- a/src/components/nodeeditor/NodeEditor.tsx
+++ b/src/components/nodeeditor/NodeEditor.tsx
@@ -68,6 +68,7 @@ export interface FormProps {
addAsset(assetType: string, asset: Asset): void;
+ language: Asset;
assetStore: AssetStore;
issues: FlowIssue[];
helpArticles: { [key: string]: string };
@@ -151,6 +152,7 @@ export class NodeEditor extends React.Component {
onClose: this.close,
language: this.props.language,
helpArticles: this.props.helpArticles,
+ assetStore: this.props.assetStore,
issues: this.props.issues.filter(
(issue: FlowIssue) => issue.language === this.props.language.id
)
@@ -176,7 +178,8 @@ export class NodeEditor extends React.Component {
issues: this.props.issues.filter((issue: FlowIssue) => !issue.language),
typeConfig: this.props.typeConfig,
onTypeChange: this.props.handleTypeConfigChange,
- onClose: this.close
+ onClose: this.close,
+ language: this.props.language
};
return (
diff --git a/src/components/translator/TranslatorTab.test.tsx b/src/components/translator/TranslatorTab.test.tsx
index 9ef99ae9d..ffa711d91 100644
--- a/src/components/translator/TranslatorTab.test.tsx
+++ b/src/components/translator/TranslatorTab.test.tsx
@@ -49,9 +49,14 @@ const createMessageNode = (
if (variables.length > 0) {
sendMsg.templating = {
- uuid: createUUID(),
template: { uuid: createUUID(), name: 'My Template' },
- variables
+ components: [
+ {
+ uuid: createUUID(),
+ name: 'body',
+ params: variables
+ }
+ ]
};
}
@@ -99,13 +104,12 @@ describe(TranslatorTab.name, () => {
it('finds message translations', () => {
const { baseElement, getByText, rerender } = render();
- const updates = createMessageNode('Hello World!', ['yes', 'no'], ['var1', 'var2']);
+ const updates = createMessageNode('Hello World!', ['yes', 'no']);
rerender();
// we pulled out all the localizable bits
getByText('Hello World!');
getByText('Quick Replies');
- getByText('Template Variables');
getByText('0%');
expect(baseElement).toMatchSnapshot();
diff --git a/src/components/translator/__snapshots__/TranslatorTab.test.tsx.snap b/src/components/translator/__snapshots__/TranslatorTab.test.tsx.snap
index 206bd8dbc..1448b4064 100644
--- a/src/components/translator/__snapshots__/TranslatorTab.test.tsx.snap
+++ b/src/components/translator/__snapshots__/TranslatorTab.test.tsx.snap
@@ -261,20 +261,6 @@ exports[`TranslatorTab finds message translations 1`] = `
Quick Replies
-
-
- var1, var2
-
-
- Template Variables
-
-
diff --git a/src/components/translator/helpers.ts b/src/components/translator/helpers.ts
index fa3b9a974..8ca5490d7 100644
--- a/src/components/translator/helpers.ts
+++ b/src/components/translator/helpers.ts
@@ -101,7 +101,7 @@ export const getFriendlyAttribute = (attribute: string) => {
return i18next.t('translation.attributes.quick_replies', 'Quick Replies');
}
- if (attribute === 'templating.variables') {
+ if (attribute === 'templating.components') {
return i18next.t('translation.attributes.templates', 'Template Variables');
}
diff --git a/src/config/__snapshots__/typeConfigs.test.ts.snap b/src/config/__snapshots__/typeConfigs.test.ts.snap
index ae0557388..91bd44bb2 100644
--- a/src/config/__snapshots__/typeConfigs.test.ts.snap
+++ b/src/config/__snapshots__/typeConfigs.test.ts.snap
@@ -84,7 +84,6 @@ Array [
"localizeableKeys": Array [
"text",
"quick_replies",
- "templating.variables",
],
"massageForDisplay": [Function],
"name": "Send Message",
@@ -648,7 +647,6 @@ Object {
"localizeableKeys": Array [
"text",
"quick_replies",
- "templating.variables",
],
"massageForDisplay": [Function],
"name": "Send Message",
diff --git a/src/config/typeConfigs.ts b/src/config/typeConfigs.ts
index a52fb57ba..fe07c818a 100644
--- a/src/config/typeConfigs.ts
+++ b/src/config/typeConfigs.ts
@@ -256,7 +256,7 @@ export const typeConfigList: Type[] = [
description: i18n.t('actions.send_msg.description', 'Send the contact a message'),
form: SendMsgForm,
localization: MsgLocalizationForm,
- localizeableKeys: ['text', 'quick_replies', 'templating.variables'],
+ localizeableKeys: ['text', 'quick_replies'],
component: SendMsgComp,
massageForDisplay: (action: SendMsg) => {
// quick replies are optional in the definition, make sure we have
diff --git a/src/flowTypes.ts b/src/flowTypes.ts
index 68b263da8..8bbd454c9 100644
--- a/src/flowTypes.ts
+++ b/src/flowTypes.ts
@@ -1,9 +1,10 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
import { Methods } from 'components/flow/routers/webhook/helpers';
import { FlowTypes, Operators, Types, ContactStatus } from 'config/interfaces';
import { ExclusionsCheckboxEntry } from 'store/nodeEditor';
// we don't concern ourselves with patch versions
-export const SPEC_VERSION = '13.2';
+export const SPEC_VERSION = '13.4';
export interface Languages {
[iso: string]: string;
@@ -204,18 +205,6 @@ export interface Category {
exit_uuid: string;
}
-export interface TemplateTranslation {
- language: string;
- status: string;
- content: string;
-}
-
-export interface Template {
- created_on: Date;
- modified_on: Date;
- translations: TemplateTranslation[];
-}
-
export interface SwitchRouter extends Router {
cases: Case[];
operand: string;
@@ -356,6 +345,7 @@ export interface TemplateTranslation {
language: string;
status: string;
variable_count: number;
+ components: { [key: string]: { params: string[]; content: string } };
}
export interface TemplateOptions {
@@ -367,10 +357,15 @@ export interface MsgTemplate {
uuid: string;
}
-export interface MsgTemplating {
+export interface MsgTemplateComponent {
uuid: string;
+ name: string;
+ params: string[];
+}
+
+export interface MsgTemplating {
template: MsgTemplate;
- variables: string[];
+ components?: MsgTemplateComponent[];
}
export interface SendMsg extends Action {
diff --git a/src/services/Localization.ts b/src/services/Localization.ts
index e917de07d..76f44e004 100644
--- a/src/services/Localization.ts
+++ b/src/services/Localization.ts
@@ -1,8 +1,8 @@
-import { Action, Case, Category, Language, MsgTemplating } from 'flowTypes';
+import { Action, Case, Category, Language, MsgTemplateComponent } from 'flowTypes';
import { Asset } from 'store/flowContext';
// list of keys that should always be treated as an array
-const ARRAY_KEYS = ['attachments'];
+const ARRAY_KEYS = ['attachments', 'components'];
export class LocalizedObject {
public localizedKeys: { [key: string]: boolean } = {};
@@ -13,7 +13,7 @@ export class LocalizedObject {
private name: string;
private language: Language;
- constructor(object: Action | Category | Case | MsgTemplating, { id, name }: Asset) {
+ constructor(object: Action | Category | Case | MsgTemplateComponent, { id, name }: Asset) {
this.localizedObject = object;
this.iso = id;
this.language = { iso: this.iso, name };
@@ -58,14 +58,14 @@ export class LocalizedObject {
return this.localized;
}
- public getObject(): Action | Case | Category | MsgTemplating {
+ public getObject(): Action | Case | Category {
return this.localizedObject;
}
}
export default class Localization {
public static translate(
- object: Action | Category | Case | MsgTemplating,
+ object: Action | Category | Case | MsgTemplateComponent,
language: Asset,
translations?: { [uuid: string]: any }
): LocalizedObject {
diff --git a/src/store/helpers.ts b/src/store/helpers.ts
index c7273b129..0570eeee9 100644
--- a/src/store/helpers.ts
+++ b/src/store/helpers.ts
@@ -187,9 +187,9 @@ export const getLocalizations = (
if (action.type === Types.send_msg) {
const sendMsgAction = action as SendMsg;
if (sendMsgAction.templating) {
- localizations.push(
- Localization.translate(sendMsgAction.templating, language, translations)
- );
+ sendMsgAction.templating.components.forEach((component: any) => {
+ localizations.push(Localization.translate(component, language, translations));
+ });
}
}
}
diff --git a/src/temba/TembaComponent.jsx b/src/temba/TembaComponent.jsx
new file mode 100644
index 000000000..43e6fdbcf
--- /dev/null
+++ b/src/temba/TembaComponent.jsx
@@ -0,0 +1,33 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import * as React from 'react';
+
+export class TembaComponent extends React.Component {
+ eventCallbacks = {};
+ componentRef = React.createRef();
+ onEvent = eventType => {
+ return event => {
+ // we will get the latest callback when the event happens
+ // eslint-disable-next-line react/prop-types
+ this.props.eventHandlers[eventType](event);
+ };
+ };
+
+ componentDidMount() {
+ // eslint-disable-next-line react/prop-types
+ const { eventHandlers } = this.props;
+ Object.keys(eventHandlers || {}).forEach(eventType => {
+ this.eventCallbacks[eventType] = this.onEvent(eventType);
+ this.componentRef.current.addEventListener(eventType, this.eventCallbacks[eventType]);
+ });
+ }
+ componentWillUnmount() {
+ Object.keys(this.eventCallbacks).forEach(eventType => {
+ this.componentRef.current.removeEventListener(eventType, this.eventCallbacks[eventType]);
+ });
+ }
+ render() {
+ // eslint-disable-next-line react/prop-types
+ const { tag: Component, ...restProps } = this.props;
+ return ;
+ }
+}
diff --git a/src/testUtils/assetCreators.ts b/src/testUtils/assetCreators.ts
index c9737307f..5f6fa73f1 100644
--- a/src/testUtils/assetCreators.ts
+++ b/src/testUtils/assetCreators.ts
@@ -467,12 +467,14 @@ export const getLocalizationFormProps = (
}),
originalAction: action,
localizations: [Localization.translate(action, language, translations)]
- }
+ },
+ assetStore: {}
};
};
export const getActionFormProps = (action: AnyAction): ActionFormProps => ({
assetStore: {
+ templates: { items: {}, type: AssetType.Template, endpoint: 'assets/templates.json' },
channels: { items: {}, type: AssetType.Channel },
fields: { items: {}, type: AssetType.Field },
languages: { items: {}, type: AssetType.Language },
@@ -488,6 +490,7 @@ export const getActionFormProps = (action: AnyAction): ActionFormProps => ({
onTypeChange: jest.fn(),
issues: [],
typeConfig: getTypeConfig(action.type),
+ language: null,
nodeSettings: {
originalNode: createRenderNode({
actions: [action],
diff --git a/src/untyped.d.ts b/src/untyped.d.ts
index acf8d2dab..e0b1e8fa6 100644
--- a/src/untyped.d.ts
+++ b/src/untyped.d.ts
@@ -14,5 +14,6 @@ declare namespace JSX {
'temba-compose': any;
'temba-icon': any;
'temba-sortable-list': any;
+ 'temba-template-editor': any;
}
}