diff --git a/assets/static/template.go b/assets/static/template.go index def66635c..23f66b12e 100644 --- a/assets/static/template.go +++ b/assets/static/template.go @@ -38,29 +38,40 @@ func (t *Template) Translations() []assets.TemplateTranslation { // TemplateTranslation represents a single template translation type TemplateTranslation struct { - Channel_ *assets.ChannelReference `json:"channel" validate:"required"` - Locale_ i18n.Locale `json:"locale" validate:"required"` + Channel_ *assets.ChannelReference `json:"channel" validate:"required"` + Locale_ i18n.Locale `json:"locale" validate:"required"` Namespace_ string `json:"namespace"` Components_ []*TemplateComponent `json:"components"` + Variables_ []*TemplateVariable `json:"variables"` } // NewTemplateTranslation creates a new template translation -func NewTemplateTranslation(channel *assets.ChannelReference, locale i18n.Locale, namespace string, components []*TemplateComponent) *TemplateTranslation { +func NewTemplateTranslation(channel *assets.ChannelReference, locale i18n.Locale, namespace string, components []*TemplateComponent, variables []*TemplateVariable) *TemplateTranslation { return &TemplateTranslation{ Channel_: channel, Namespace_: namespace, Locale_: locale, Components_: components, + Variables_: variables, } } -// Components returns the components structure for this template +// Components returns the components for this template translation func (t *TemplateTranslation) Components() []assets.TemplateComponent { - tcs := make([]assets.TemplateComponent, len(t.Components_)) + cs := make([]assets.TemplateComponent, len(t.Components_)) for k, tc := range t.Components_ { - tcs[k] = tc + cs[k] = tc } - return tcs + return cs +} + +// Variables returns the variables for this template translation +func (t *TemplateTranslation) Variables() []assets.TemplateVariable { + vs := make([]assets.TemplateVariable, len(t.Variables_)) + for i := range t.Variables_ { + vs[i] = t.Variables_[i] + } + return vs } // Namespace returns the namespace for this template @@ -73,11 +84,11 @@ func (t *TemplateTranslation) Locale() i18n.Locale { return t.Locale_ } func (t *TemplateTranslation) Channel() *assets.ChannelReference { return t.Channel_ } type TemplateComponent struct { - Type_ string `json:"type"` - Name_ string `json:"name"` - Content_ string `json:"content"` - Display_ string `json:"display"` - Params_ []*TemplateParam `json:"params"` + Type_ string `json:"type"` + Name_ string `json:"name"` + Content_ string `json:"content"` + Display_ string `json:"display"` + Variables_ map[string]int `json:"variables"` } // Type returns the type for this template component @@ -92,29 +103,23 @@ func (t *TemplateComponent) Content() string { return t.Content_ } // Display returns the display for this template component func (t *TemplateComponent) Display() string { return t.Display_ } -// Params returns the params for this template component -func (t *TemplateComponent) Params() []assets.TemplateParam { - tps := make([]assets.TemplateParam, len(t.Params_)) - for i := range t.Params_ { - tps[i] = t.Params_[i] - } - return tps -} +// Variables returns the variable mapping for this template component +func (t *TemplateComponent) Variables() map[string]int { return t.Variables_ } // NewTemplateComponent creates a new template param -func NewTemplateComponent(type_, name, content, display string, params []*TemplateParam) *TemplateComponent { - return &TemplateComponent{Type_: type_, Name_: name, Content_: content, Display_: display, Params_: params} +func NewTemplateComponent(type_, name, content, display string, variables map[string]int) *TemplateComponent { + return &TemplateComponent{Type_: type_, Name_: name, Content_: content, Display_: display, Variables_: variables} } -// TemplateParam represents a single parameter for a template translation -type TemplateParam struct { +// TemplateVariable represents a single variable for a template translation +type TemplateVariable struct { Type_ string `json:"type"` } // Type returns the type for this parameter -func (t *TemplateParam) Type() string { return t.Type_ } +func (t *TemplateVariable) Type() string { return t.Type_ } -// NewTemplateParam creates a new template param -func NewTemplateParam(paramType string) *TemplateParam { - return &TemplateParam{Type_: paramType} +// NewTemplateVariable creates a new template variable +func NewTemplateVariable(paramType string) *TemplateVariable { + return &TemplateVariable{Type_: paramType} } diff --git a/assets/static/template_test.go b/assets/static/template_test.go index 7d5ce8cd1..b8a247f9b 100644 --- a/assets/static/template_test.go +++ b/assets/static/template_test.go @@ -1,4 +1,4 @@ -package static +package static_test import ( "testing" @@ -6,34 +6,37 @@ import ( "github.com/nyaruka/gocommon/i18n" "github.com/nyaruka/gocommon/jsonx" "github.com/nyaruka/goflow/assets" + "github.com/nyaruka/goflow/assets/static" "github.com/stretchr/testify/assert" ) func TestTemplate(t *testing.T) { channel := assets.NewChannelReference("Test Channel", "ffffffff-9b24-92e1-ffff-ffffb207cdb4") - tp1 := NewTemplateParam("text") - assert.Equal(t, "text", tp1.Type()) + v1 := static.NewTemplateVariable("text") + v2 := static.NewTemplateVariable("text") + assert.Equal(t, "text", v1.Type()) + assert.Equal(t, "text", v2.Type()) - tc1 := NewTemplateComponent("body", "body", "Hello {{1}}", "", []*TemplateParam{tp1}) - tc2 := NewTemplateComponent("button/url", "button.0", "http://google.com?q={{1}}", "Go {{1}", []*TemplateParam{NewTemplateParam("text"), NewTemplateParam("text")}) + c1 := static.NewTemplateComponent("body", "body", "Hello {{1}}", "", map[string]int{"1": 0}) + c2 := static.NewTemplateComponent("button/url", "button.0", "http://google.com?q={{1}}", "Go", map[string]int{"1": 1}) - assert.Equal(t, "body", tc1.Type()) - assert.Equal(t, "body", tc1.Name()) - assert.Equal(t, "Hello {{1}}", tc1.Content()) - assert.Equal(t, "", tc1.Display()) - assert.Equal(t, []assets.TemplateParam{tp1}, tc1.Params()) + assert.Equal(t, "body", c1.Type()) + assert.Equal(t, "body", c1.Name()) + assert.Equal(t, "Hello {{1}}", c1.Content()) + assert.Equal(t, "", c1.Display()) + assert.Equal(t, map[string]int{"1": 0}, c1.Variables()) - translation := NewTemplateTranslation(channel, i18n.Locale("eng-US"), "0162a7f4_dfe4_4c96_be07_854d5dba3b2b", []*TemplateComponent{tc1, tc2}) + translation := static.NewTemplateTranslation(channel, i18n.Locale("eng-US"), "0162a7f4_dfe4_4c96_be07_854d5dba3b2b", []*static.TemplateComponent{c1, c2}, []*static.TemplateVariable{v1, v2}) assert.Equal(t, channel, translation.Channel()) assert.Equal(t, i18n.Locale("eng-US"), translation.Locale()) assert.Equal(t, "0162a7f4_dfe4_4c96_be07_854d5dba3b2b", translation.Namespace()) assert.Equal(t, []assets.TemplateComponent{ - (assets.TemplateComponent)(tc1), - (assets.TemplateComponent)(tc2), + (assets.TemplateComponent)(c1), + (assets.TemplateComponent)(c2), }, translation.Components()) - template := NewTemplate(assets.TemplateUUID("8a9c1f73-5059-46a0-ba4a-6390979c01d3"), "hello", []*TemplateTranslation{translation}) + template := static.NewTemplate(assets.TemplateUUID("8a9c1f73-5059-46a0-ba4a-6390979c01d3"), "hello", []*static.TemplateTranslation{translation}) assert.Equal(t, assets.TemplateUUID("8a9c1f73-5059-46a0-ba4a-6390979c01d3"), template.UUID()) assert.Equal(t, "hello", template.Name()) assert.Equal(t, 1, len(template.Translations())) @@ -42,8 +45,8 @@ func TestTemplate(t *testing.T) { asJSON, err := jsonx.Marshal(template) assert.NoError(t, err) - copy := Template{} - err = jsonx.Unmarshal(asJSON, ©) + copy := &static.Template{} + err = jsonx.Unmarshal(asJSON, copy) assert.NoError(t, err) assert.Equal(t, copy.Name(), template.Name()) diff --git a/assets/template.go b/assets/template.go index 9cdfe27ae..7f8ed9a9c 100644 --- a/assets/template.go +++ b/assets/template.go @@ -27,9 +27,10 @@ type TemplateUUID uuids.UUID // "type": "body", // "name": "body", // "content": "Hello {{1}}", -// "params": [{"type": "text"}] +// "variables": {"1": 0} // } -// ] +// ], +// "variables": [{"type": "text"}] // }, // { // "locale": "fra", @@ -42,9 +43,10 @@ type TemplateUUID uuids.UUID // "type": "body", // "name": "body", // "content": "Bonjour {{1}}", -// "params": [{"type": "text"}] +// "variables": {"1": 0} // } -// ] +// ], +// "variables": [{"type": "text"}] // } // ] // } @@ -56,8 +58,7 @@ type Template interface { Translations() []TemplateTranslation } -// TemplateParam is a parameter for template translation -type TemplateParam interface { +type TemplateVariable interface { Type() string } @@ -66,7 +67,7 @@ type TemplateComponent interface { Name() string Content() string Display() string - Params() []TemplateParam + Variables() map[string]int } // TemplateTranslation represents a single translation for a specific template and channel @@ -75,6 +76,7 @@ type TemplateTranslation interface { Namespace() string Channel() *ChannelReference Components() []TemplateComponent + Variables() []TemplateVariable } // TemplateReference is used to reference a Template diff --git a/flows/actions/send_msg.go b/flows/actions/send_msg.go index 484496864..e9617f070 100644 --- a/flows/actions/send_msg.go +++ b/flows/actions/send_msg.go @@ -10,6 +10,7 @@ import ( "github.com/nyaruka/goflow/assets" "github.com/nyaruka/goflow/flows" "github.com/nyaruka/goflow/flows/events" + "github.com/nyaruka/goflow/utils" ) func init() { @@ -161,15 +162,19 @@ func (a *SendMsgAction) getTemplateMsg(run flows.Run, urn urns.URN, channelRef * var previewParts []string var previewQRs []string + variables := translation.Variables() + for _, comp := range translation.Components() { + varMap := comp.Variables() paramValues := evaluatedParams[comp.Name()] - params := make([]flows.TemplatingParam, len(comp.Params())) + params := make([]flows.TemplatingParam, len(varMap)) - for i, p := range comp.Params() { + for i, varName := range utils.SortedKeys(varMap) { + v := variables[varMap[varName]] if i < len(paramValues) { - params[i] = flows.TemplatingParam{Type: p.Type(), Value: paramValues[i]} + params[i] = flows.TemplatingParam{Type: v.Type(), Value: paramValues[i]} } else { - params[i] = flows.TemplatingParam{Type: p.Type(), Value: ""} + params[i] = flows.TemplatingParam{Type: v.Type(), Value: ""} } } diff --git a/flows/actions/testdata/_assets.json b/flows/actions/testdata/_assets.json index b87f56907..78fe1a70b 100644 --- a/flows/actions/testdata/_assets.json +++ b/flows/actions/testdata/_assets.json @@ -262,14 +262,18 @@ "type": "body", "name": "body", "content": "Hi {{1}}, who's an excellent {{2}}?", - "params": [ - { - "type": "text" - }, - { - "type": "text" - } - ] + "variables": { + "1": 0, + "2": 1 + } + } + ], + "variables": [ + { + "type": "text" + }, + { + "type": "text" } ] }, @@ -284,14 +288,18 @@ "type": "body", "name": "body", "content": "Hola {{1}}, quien es un {{2}} excelente?", - "params": [ - { - "type": "text" - }, - { - "type": "text" - } - ] + "variables": { + "1": 0, + "2": 1 + } + } + ], + "variables": [ + { + "type": "text" + }, + { + "type": "text" } ] } @@ -312,9 +320,10 @@ "type": "body", "name": "body", "content": "Hi there, it's time to get up!", - "params": [] + "variables": {} } - ] + ], + "variables": [] } ] }, @@ -333,34 +342,35 @@ "type": "header", "name": "header", "content": "", - "params": [ - { - "type": "image" - } - ] + "variables": {} }, { "type": "body", "name": "body", "content": "Hey {{1}}, your gender is saved as {{2}}.", - "params": [ - { - "type": "text" - }, - { - "type": "text" - } - ] + "variables": { + "1": 0, + "2": 1 + } }, { "type": "button/quick_reply", "name": "button.1", "content": "{{1}}", - "params": [ - { - "type": "text" - } - ] + "variables": { + "1": 2 + } + } + ], + "variables": [ + { + "type": "text" + }, + { + "type": "text" + }, + { + "type": "text" } ] }, @@ -375,40 +385,41 @@ "type": "header", "name": "header", "content": "", - "params": [ - { - "type": "image" - } - ] + "variables": {} }, { "type": "body", "name": "body", "content": "Hola, {{1}}, tu género está guardado como {{2}}.", - "params": [ - { - "type": "text" - }, - { - "type": "text" - } - ] + "variables": { + "1": 0, + "2": 1 + } }, { "type": "button/quick_reply", "name": "button.0", "content": "{{1}}", - "params": [ - { - "type": "text" - } - ] + "variables": { + "1": 2 + } }, { "type": "button/quick_reply", "name": "button.1", "content": "No", - "params": [] + "variables": {} + } + ], + "variables": [ + { + "type": "text" + }, + { + "type": "text" + }, + { + "type": "text" } ] } diff --git a/flows/actions/testdata/send_msg.json b/flows/actions/testdata/send_msg.json index d24827d9b..9a34c5f3b 100644 --- a/flows/actions/testdata/send_msg.json +++ b/flows/actions/testdata/send_msg.json @@ -458,6 +458,7 @@ "uuid": "5722e1fd-fe32-4e74-ac78-3cf41a6adb7e", "name": "affirmation" }, + "namespace": "", "components": [ { "type": "body", @@ -473,8 +474,7 @@ } ] } - ], - "namespace": "" + ] }, "locale": "eng-US" } @@ -555,6 +555,7 @@ "uuid": "5722e1fd-fe32-4e74-ac78-3cf41a6adb7e", "name": "affirmation" }, + "namespace": "", "components": [ { "type": "body", @@ -570,8 +571,7 @@ } ] } - ], - "namespace": "" + ] }, "locale": "spa" } @@ -824,13 +824,6 @@ "name": "gender_update" }, "components": [ - { - "uuid": "128b97ff-a530-4ec8-87dd-04a9a778c3e0", - "name": "header", - "params": [ - "http://templates.com/red.jpg" - ] - }, { "uuid": "1067f8e2-82f0-4378-9214-0f019365ddb7", "name": "body", @@ -898,17 +891,8 @@ "uuid": "ce00c80e-991a-4c03-b373-3273c23ee042", "name": "gender_update" }, + "namespace": "", "components": [ - { - "type": "header", - "name": "header", - "params": [ - { - "type": "image", - "value": "http://templates.com/rojo.jpg" - } - ] - }, { "type": "body", "name": "body", @@ -933,8 +917,7 @@ } ] } - ], - "namespace": "" + ] }, "locale": "spa" } @@ -945,8 +928,6 @@ "http://example.com/red.jpg", "Yes", "No", - "http://templates.com/red.jpg", - "http://templates.com/rojo.jpg", "@contact.name", "boy", "@contact.name", @@ -960,7 +941,6 @@ "http://example.com/red.jpg", "Yes", "No", - "http://templates.com/red.jpg", "@contact.name", "boy", "Yeah", diff --git a/flows/msg_test.go b/flows/msg_test.go index 8dbf711fe..fe4588dca 100644 --- a/flows/msg_test.go +++ b/flows/msg_test.go @@ -205,27 +205,27 @@ func TestTemplatingComponentPreview(t *testing.T) { expected string }{ { // 0: no params - component: static.NewTemplateComponent("body", "body", "Hello", "", []*static.TemplateParam{}), + component: static.NewTemplateComponent("body", "body", "Hello", "", map[string]int{}), templating: &flows.TemplatingComponent{Type: "body", Params: []flows.TemplatingParam{}}, expected: "Hello", }, { // 1: two params on component and two params in templating - component: static.NewTemplateComponent("body", "body", "Hello {{1}} {{2}}", "", []*static.TemplateParam{{Type_: "text"}, {Type_: "text"}}), + component: static.NewTemplateComponent("body", "body", "Hello {{1}} {{2}}", "", map[string]int{"1": 0, "2": 1}), templating: &flows.TemplatingComponent{Type: "body", Params: []flows.TemplatingParam{{Type: "text", Value: "Dr"}, {Type: "text", Value: "Bob"}}}, expected: "Hello Dr Bob", }, { // 2: one less param in templating than on component - component: static.NewTemplateComponent("body", "body", "Hello {{1}} {{2}}", "", []*static.TemplateParam{{Type_: "text"}, {Type_: "text"}}), + component: static.NewTemplateComponent("body", "body", "Hello {{1}} {{2}}", "", map[string]int{"1": 0, "2": 1}), templating: &flows.TemplatingComponent{Type: "body", Params: []flows.TemplatingParam{{Type: "text", Value: "Dr"}}}, expected: "Hello Dr ", }, { // 3 - component: static.NewTemplateComponent("button/quick_reply", "button.0", "{{1}}", "", []*static.TemplateParam{{Type_: "text"}}), + component: static.NewTemplateComponent("button/quick_reply", "button.0", "{{1}}", "", map[string]int{"1": 0, "2": 1}), templating: &flows.TemplatingComponent{Type: "button/quick_reply", Params: []flows.TemplatingParam{{Type: "text", Value: "Yes"}}}, expected: "Yes", }, { // 4: one param for content, one for display - component: static.NewTemplateComponent("button/url", "button.0", "example.com?p={{1}}", "{{1}}", []*static.TemplateParam{{Type_: "text"}}), + component: static.NewTemplateComponent("button/url", "button.0", "example.com?p={{1}}", "{{1}}", map[string]int{"1": 0, "2": 1}), templating: &flows.TemplatingComponent{Type: "button/url", Params: []flows.TemplatingParam{{Type: "text", Value: "123"}, {Type: "text", Value: "Go"}}}, expected: "example.com?p=123", }, diff --git a/flows/template_test.go b/flows/template_test.go index 5f25d1c3e..da7c75fb6 100644 --- a/flows/template_test.go +++ b/flows/template_test.go @@ -19,10 +19,10 @@ func TestFindTranslation(t *testing.T) { channel1Ref := assets.NewChannelReference(channel1.UUID(), channel1.Name()) channel2Ref := assets.NewChannelReference(channel2.UUID(), channel2.Name()) - tt1 := static.NewTemplateTranslation(channel1Ref, i18n.Locale("eng"), "", []*static.TemplateComponent{}) - tt2 := static.NewTemplateTranslation(channel1Ref, i18n.Locale("spa-EC"), "", []*static.TemplateComponent{}) - tt3 := static.NewTemplateTranslation(channel1Ref, i18n.Locale("spa-ES"), "", []*static.TemplateComponent{}) - tt4 := static.NewTemplateTranslation(channel2Ref, i18n.Locale("kin"), "", []*static.TemplateComponent{}) + tt1 := static.NewTemplateTranslation(channel1Ref, i18n.Locale("eng"), "", []*static.TemplateComponent{}, []*static.TemplateVariable{}) + tt2 := static.NewTemplateTranslation(channel1Ref, i18n.Locale("spa-EC"), "", []*static.TemplateComponent{}, []*static.TemplateVariable{}) + tt3 := static.NewTemplateTranslation(channel1Ref, i18n.Locale("spa-ES"), "", []*static.TemplateComponent{}, []*static.TemplateVariable{}) + tt4 := static.NewTemplateTranslation(channel2Ref, i18n.Locale("kin"), "", []*static.TemplateComponent{}, []*static.TemplateVariable{}) template := flows.NewTemplate(static.NewTemplate("c520cbda-e118-440f-aaf6-c0485088384f", "greeting", []*static.TemplateTranslation{tt1, tt2, tt3, tt4})) tas := flows.NewTemplateAssets([]assets.Template{template})