Skip to content

Commit adb0f3f

Browse files
authored
Merge pull request #1223 from nyaruka/component-templates
Edit component-based message templates
2 parents 6a56d06 + c4e9c13 commit adb0f3f

24 files changed

+241
-300
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
"@babel/core": "^7.4.4",
7171
"@babel/preset-env": "^7.4.4",
7272
"@babel/preset-react": "7.0.0",
73-
"@nyaruka/temba-components": "0.62.3",
73+
"@nyaruka/temba-components": "0.72.0",
7474
"@testing-library/jest-dom": "4.0.0",
7575
"@testing-library/react": "8.0.1",
7676
"@types/common-tags": "^1.8.0",

src/components/flow/actions/localization/MsgLocalizationForm.tsx

+52-58
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
13
import { react as bindCallbacks } from 'auto-bind';
24
import Dialog, { ButtonSet, Tab } from 'components/dialog/Dialog';
35
import styles from 'components/flow/actions/action/Action.module.scss';
@@ -16,16 +18,16 @@ import { MaxOfTenItems, validate } from 'store/validators';
1618
import { initializeLocalizedForm } from './helpers';
1719
import i18n from 'config/i18n';
1820
import { Trans } from 'react-i18next';
19-
import { range } from 'utils';
2021
import { renderIssues } from '../helpers';
2122
import { Attachment, renderAttachments } from '../sendmsg/attachments';
2223
import { AxiosError, AxiosResponse } from 'axios';
24+
import { TembaComponent } from 'temba/TembaComponent';
2325

2426
export interface MsgLocalizationFormState extends FormState {
2527
message: StringEntry;
2628
quickReplies: StringArrayEntry;
2729
audio: StringEntry;
28-
templateVariables: StringEntry[];
30+
params: any;
2931
templating: MsgTemplating;
3032
attachments: Attachment[];
3133
uploadInProgress: boolean;
@@ -91,7 +93,7 @@ export default class MsgLocalizationForm extends React.Component<
9193
}
9294

9395
private handleSave(): void {
94-
const { message: text, quickReplies, audio, templateVariables, attachments } = this.state;
96+
const { message: text, quickReplies, audio, attachments } = this.state;
9597

9698
// make sure we are valid for saving, only quick replies can be invalid
9799
const typeConfig = determineTypeConfig(this.props.nodeSettings);
@@ -125,17 +127,32 @@ export default class MsgLocalizationForm extends React.Component<
125127
}
126128
];
127129

128-
// if we have template variables, they show up on their own key
129-
const hasTemplateVariables = templateVariables.find(
130-
(entry: StringEntry) => entry.value.length > 0
131-
);
132-
if (hasTemplateVariables) {
133-
localizations.push({
134-
uuid: this.state.templating.uuid,
135-
translations: { variables: templateVariables.map((entry: StringEntry) => entry.value) }
130+
// save our template components
131+
const templating = (this.props.nodeSettings.originalAction as SendMsg).templating;
132+
if (this.state.params && templating) {
133+
const components = templating.components;
134+
135+
// find the matching component for our params
136+
Object.keys(this.state.params).forEach((key: any) => {
137+
const component = components.find((c: any) => c.name === key);
138+
if (component) {
139+
const params = this.state.params[key];
140+
141+
// if each string in params is empty string, set params to null
142+
if (params.every((p: string) => p.trim() === '')) {
143+
localizations.push({
144+
uuid: component.uuid,
145+
translations: null
146+
});
147+
} else {
148+
localizations.push({
149+
uuid: component.uuid,
150+
translations: { params }
151+
});
152+
}
153+
}
136154
});
137155
}
138-
139156
this.props.updateLocalizations(this.props.language.id, localizations);
140157

141158
// notify our modal we are done
@@ -157,26 +174,20 @@ export default class MsgLocalizationForm extends React.Component<
157174
this.handleUpdate({ quickReplies });
158175
}
159176

160-
private handleTemplateVariableChanged(updatedText: string, num: number): void {
161-
const entry = validate(`Variable ${num + 1}`, updatedText, []);
162-
163-
const templateVariables = mutate(this.state.templateVariables, {
164-
$merge: { [num]: entry }
165-
}) as StringEntry[];
166-
167-
this.setState({ templateVariables });
177+
private handleTemplateVariableChanged(event: any): void {
178+
this.setState({ params: event.detail.params });
168179
}
169180

170181
private handleAttachmentUploading(isUploading: boolean) {
171-
const uploadError: string = '';
182+
const uploadError = '';
172183
console.log(uploadError);
173184
this.setState({ uploadError });
174185

175186
if (isUploading) {
176-
const uploadInProgress: boolean = true;
187+
const uploadInProgress = true;
177188
this.setState({ uploadInProgress });
178189
} else {
179-
const uploadInProgress: boolean = false;
190+
const uploadInProgress = false;
180191
this.setState({ uploadInProgress });
181192
}
182193
}
@@ -193,18 +204,18 @@ export default class MsgLocalizationForm extends React.Component<
193204
});
194205
this.setState({ attachments });
195206

196-
const uploadError: string = '';
207+
const uploadError = '';
197208
console.log(uploadError);
198209
this.setState({ uploadError });
199210
}
200211

201-
const uploadInProgress: boolean = false;
212+
const uploadInProgress = false;
202213
this.setState({ uploadInProgress });
203214
}
204215

205216
private handleAttachmentUploadFailed(error: AxiosError) {
206217
//nginx returns a 300+ if there's an error
207-
let uploadError: string = '';
218+
let uploadError = '';
208219
const status = error.response.status;
209220
if (status >= 500) {
210221
uploadError = i18n.t('file_upload_failed_generic', 'File upload failed, please try again');
@@ -215,7 +226,7 @@ export default class MsgLocalizationForm extends React.Component<
215226
}
216227
this.setState({ uploadError });
217228

218-
const uploadInProgress: boolean = false;
229+
const uploadInProgress = false;
219230
this.setState({ uploadInProgress });
220231
}
221232

@@ -249,16 +260,7 @@ export default class MsgLocalizationForm extends React.Component<
249260
const typeConfig = determineTypeConfig(this.props.nodeSettings);
250261
const tabs: Tab[] = [];
251262

252-
if (
253-
this.state.templating &&
254-
typeConfig.localizeableKeys!.indexOf('templating.variables') > -1
255-
) {
256-
const hasLocalizedValue = !!this.state.templateVariables.find(
257-
(entry: StringEntry) => entry.value.length > 0
258-
);
259-
260-
const variable = i18n.t('forms.variable', 'Variable');
261-
263+
if (this.state.templating) {
262264
tabs.push({
263265
name: 'WhatsApp',
264266
body: (
@@ -269,30 +271,22 @@ export default class MsgLocalizationForm extends React.Component<
269271
'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.'
270272
)}
271273
</p>
272-
{this.state.templating && this.state.templating.variables.length > 0 ? (
273-
<>
274-
{range(0, this.state.templating.variables.length).map((num: number) => {
275-
const entry = this.state.templateVariables[num] || { value: '' };
276-
return (
277-
<div className={styles.variable} key={'tr_arg_' + num}>
278-
<TextInputElement
279-
name={`${i18n.t('forms.variable', 'Variable')} ${num + 1}`}
280-
showLabel={false}
281-
placeholder={`${this.props.language.name} ${variable} ${num + 1}`}
282-
onChange={(updatedText: string) => {
283-
this.handleTemplateVariableChanged(updatedText, num);
284-
}}
285-
entry={entry}
286-
autocomplete={true}
287-
/>
288-
</div>
289-
);
290-
})}
291-
</>
274+
{this.state.templating ? (
275+
<TembaComponent
276+
tag="temba-template-editor"
277+
eventHandlers={{
278+
'temba-content-changed': this.handleTemplateVariableChanged
279+
}}
280+
template={this.state.templating.template.uuid}
281+
url={this.props.assetStore.templates.endpoint}
282+
lang={this.props.language.id}
283+
params={JSON.stringify(this.state.params)}
284+
translating={true}
285+
></TembaComponent>
292286
) : null}
293287
</>
294288
),
295-
checked: hasLocalizedValue
289+
checked: true //hasLocalizedValue
296290
});
297291
}
298292

src/components/flow/actions/localization/__snapshots__/MsgLocalizationForm.test.ts.snap

+2-2
Original file line numberDiff line numberDiff line change
@@ -281,11 +281,11 @@ Object {
281281
"message": Object {
282282
"value": "",
283283
},
284+
"params": Object {},
284285
"quickReplies": Object {
285286
"validationFailures": Array [],
286287
"value": Array [],
287288
},
288-
"templateVariables": Array [],
289289
"templating": null,
290290
"uploadError": "",
291291
"uploadInProgress": false,
@@ -317,6 +317,7 @@ Object {
317317
"validationFailures": Array [],
318318
"value": "What is your favorite color?",
319319
},
320+
"params": Object {},
320321
"quickReplies": Object {
321322
"validationFailures": Array [],
322323
"value": Array [
@@ -325,7 +326,6 @@ Object {
325326
"blue",
326327
],
327328
},
328-
"templateVariables": Array [],
329329
"templating": null,
330330
"uploadError": "",
331331
"uploadInProgress": false,

src/components/flow/actions/localization/helpers.ts

+4-16
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { MsgLocalizationFormState } from 'components/flow/actions/localization/M
33
import { Types } from 'config/interfaces';
44
import { getTypeConfig } from 'config/typeConfigs';
55
import { NodeEditorSettings, StringEntry } from 'store/nodeEditor';
6-
import { SendMsg, MsgTemplating, SayMsg } from 'flowTypes';
6+
import { SendMsg, SayMsg } from 'flowTypes';
77
import { Attachment } from '../sendmsg/attachments';
88

99
export const initializeLocalizedKeyForm = (
@@ -30,7 +30,7 @@ export const initializeLocalizedForm = (settings: NodeEditorSettings): MsgLocali
3030
const state: MsgLocalizationFormState = {
3131
message: { value: '' },
3232
quickReplies: { value: [] },
33-
templateVariables: [],
33+
params: {},
3434
templating: null,
3535
audio: { value: null },
3636
valid: true,
@@ -49,17 +49,11 @@ export const initializeLocalizedForm = (settings: NodeEditorSettings): MsgLocali
4949
) {
5050
if (settings.originalAction && (settings.originalAction as any).templating) {
5151
state.templating = (settings.originalAction as any).templating;
52-
state.templateVariables = state.templating.variables.map((value: string) => {
53-
return {
54-
value: ''
55-
};
56-
});
5752
}
5853

5954
for (const localized of settings.localizations) {
6055
if (localized.isLocalized()) {
6156
const localizedObject = localized.getObject() as any;
62-
6357
if (localizedObject.text) {
6458
const action = localizedObject as (SendMsg & SayMsg);
6559
state.message.value = 'text' in localized.localizedKeys ? action.text : '';
@@ -88,14 +82,8 @@ export const initializeLocalizedForm = (settings: NodeEditorSettings): MsgLocali
8882
state.valid = true;
8983
}
9084

91-
if (localizedObject.variables) {
92-
const templating = localizedObject as MsgTemplating;
93-
state.templateVariables = templating.variables.map((value: string) => {
94-
return {
95-
value: 'variables' in localized.localizedKeys ? value : ''
96-
};
97-
});
98-
state.valid = true;
85+
if (localized.localizedKeys.params) {
86+
state.params[localizedObject.name] = localizedObject.params;
9987
}
10088
}
10189
}

src/components/flow/actions/saymsg/__snapshots__/SayMsgForm.test.ts.snap

-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ exports[`SayMsgForm render should render 1`] = `
2828
"localizeableKeys": Array [
2929
"text",
3030
"quick_replies",
31-
"templating.variables",
3231
],
3332
"massageForDisplay": [Function],
3433
"name": "Send Message",

0 commit comments

Comments
 (0)