diff --git a/screenshots/truth/templates/default.png b/screenshots/truth/templates/default.png index 86ef734f..6eaef2b6 100644 Binary files a/screenshots/truth/templates/default.png and b/screenshots/truth/templates/default.png differ diff --git a/screenshots/truth/templates/french.png b/screenshots/truth/templates/french.png index 9e9fd2ff..d7fd282f 100644 Binary files a/screenshots/truth/templates/french.png and b/screenshots/truth/templates/french.png differ diff --git a/screenshots/truth/templates/unapproved.png b/screenshots/truth/templates/unapproved.png new file mode 100644 index 00000000..21a9dca9 Binary files /dev/null and b/screenshots/truth/templates/unapproved.png differ diff --git a/src/compose/Compose.ts b/src/compose/Compose.ts index ae686b5c..a730413c 100644 --- a/src/compose/Compose.ts +++ b/src/compose/Compose.ts @@ -2,18 +2,29 @@ import { TemplateResult, html, css } from 'lit'; import { FormElement } from '../FormElement'; import { property } from 'lit/decorators.js'; import { Attachment, CustomEventType, Language } from '../interfaces'; -import { DEFAULT_MEDIA_ENDPOINT } from '../utils'; +import { DEFAULT_MEDIA_ENDPOINT, getClasses } from '../utils'; import { Completion } from '../completion/Completion'; import { Select } from '../select/Select'; import { TabPane } from '../tabpane/TabPane'; -import { EventHandler } from '../RapidElement'; import { MediaPicker } from '../mediapicker/MediaPicker'; +import { Tab } from '../tabpane/Tab'; export class Compose extends FormElement { static get styles() { return css` :host { --textarea-min-height: var(--textarea-min-height, 4em); + overflow: hidden; + border-top-right-radius: var(--curvature); + border-top-left-radius: var(--curvature); + } + + .active-template .chatbox { + display: none; + } + + .active-template .actions { + border: none; } .container { @@ -105,6 +116,10 @@ export class Compose extends FormElement { .attachments { } + + temba-template-editor { + padding: 1em; + } `; } @@ -132,6 +147,9 @@ export class Compose extends FormElement { @property({ type: Boolean }) optIns: boolean; + @property({ type: Boolean }) + templates: boolean; + @property({ type: Boolean }) counter: boolean; @@ -168,9 +186,25 @@ export class Compose extends FormElement { @property({ type: Array }) currentOptin: { name: string; uuid: string }[] = []; + @property({ type: Array }) + variables: string[] = []; + + @property({ type: String }) + template: string; + + @property({ type: Object }) + currentTemplate: { name: string; uuid: string }; + + // locale for the template + @property({ type: String }) + locale: string; + @property({ type: String }) optinEndpoint = '/api/v2/optins.json'; + @property({ type: String }) + templateEndpoint = '/api/internal/templates.json'; + @property({ type: String }) buttonName = 'Send'; @@ -193,12 +227,18 @@ export class Compose extends FormElement { attachments: Attachment[]; quick_replies: string[]; optin?: { name: string; uuid: string }; + template?: string; + variables?: string[]; + locale?: string; }; } = {}; @property({ type: String }) currentLanguage = 'und'; + @property({ type: Object }) + currentTab: Tab; + public constructor() { super(); } @@ -212,19 +252,7 @@ export class Compose extends FormElement { private handleTabChanged() { const tabs = this.shadowRoot.querySelector('temba-tabs') as TabPane; - const tab = tabs.getCurrentTab(); - if (tab) { - // check we are going for the first attachment - if (tab.icon == 'attachment') { - // show the media picker? - } - } - } - - public getEventHandlers(): EventHandler[] { - return [ - { event: CustomEventType.ContextChanged, method: this.handleTabChanged } - ]; + this.currentTab = tabs.getCurrentTab(); } public firstUpdated(changes: Map): void { @@ -236,6 +264,8 @@ export class Compose extends FormElement { if (changes.has('value')) { this.langValues = this.getDeserializedValue() || {}; + this.variables = this.langValues[this.currentLanguage]?.variables || []; + this.template = this.langValues[this.currentLanguage]?.template || null; } this.setFocusOnChatbox(); } @@ -279,21 +309,27 @@ export class Compose extends FormElement { (changes.has('currentText') || changes.has('currentAttachments') || changes.has('currentQuickReplies'))) || - changes.has('currentOptin') + changes.has('currentOptin') || + changes.has('currentTemplate') || + changes.has('variables') ) { this.toggleButton(); const trimmed = this.currentText ? this.currentText.trim() : ''; if ( trimmed || - this.currentAttachments.length > 0 || - this.currentQuickReplies.length > 0 + (this.currentAttachments || []).length > 0 || + this.currentQuickReplies.length > 0 || + this.variables.length > 0 ) { this.langValues[this.currentLanguage] = { text: trimmed, attachments: this.currentAttachments, quick_replies: this.currentQuickReplies.map((option) => option.value), - optin: this.currentOptin.length > 0 ? this.currentOptin[0] : null + optin: this.currentOptin.length > 0 ? this.currentOptin[0] : null, + template: this.currentTemplate ? this.currentTemplate.uuid : null, + variables: this.variables, + locale: this.locale }; } else { delete this.langValues[this.currentLanguage]; @@ -409,6 +445,12 @@ export class Compose extends FormElement { .errors=${this.errors} .widgetOnly=${this.widgetOnly} .value=${this.value} + class=${getClasses({ + 'active-template': + !!this.currentTemplate && + this.currentTab && + this.currentTab.name === 'Template' + })} > ${this.languages.length > 1 ? html` ${this.attachments ? html`
@@ -520,6 +575,25 @@ export class Compose extends FormElement { >
+ error.includes('template'))} + ?hidden=${!showTemplates} + ?checked=${this.currentTemplate} + > + + + +
${this.buttonError ? html`
${this.buttonError}
` diff --git a/src/tabpane/Tab.ts b/src/tabpane/Tab.ts index f01c355c..f2d94225 100644 --- a/src/tabpane/Tab.ts +++ b/src/tabpane/Tab.ts @@ -37,6 +37,9 @@ export class Tab extends RapidElement { @property({ type: Boolean }) notify = false; + @property({ type: Boolean }) + alert = false; + @property({ type: Boolean }) hidden = false; diff --git a/src/tabpane/TabPane.ts b/src/tabpane/TabPane.ts index 76c12837..e36ce9b9 100644 --- a/src/tabpane/TabPane.ts +++ b/src/tabpane/TabPane.ts @@ -168,6 +168,11 @@ export class TabPane extends RapidElement { color: #fff; } + .alert { + color: var(--color-alert); + --icon-color: var(--color-alert); + } + .bottom.tabs .tab { border-radius: 0em; } @@ -365,7 +370,8 @@ export class TabPane extends RapidElement { first: index == 0, selected: index == this.index, hidden: tab.hidden, - notify: tab.notify + notify: tab.notify, + alert: tab.alert })}" style="${tab.selectionColor && index == this.index ? `color:${tab.selectionColor};--icon-color:${tab.selectionColor};` @@ -386,7 +392,7 @@ export class TabPane extends RapidElement {
` : null} - ${tab.checked + ${tab.checked && !tab.alert ? html`` : null} diff --git a/src/templates/TemplateEditor.ts b/src/templates/TemplateEditor.ts index e97d9d1c..a51ed09f 100644 --- a/src/templates/TemplateEditor.ts +++ b/src/templates/TemplateEditor.ts @@ -45,7 +45,6 @@ export class TemplateEditor extends FormElement { } .content { - margin-bottom: 1em; } .picker { @@ -67,6 +66,7 @@ export class TemplateEditor extends FormElement { .error-message { padding-left: 0.5em; + padding-bottom: 1em; } .variable { @@ -157,9 +157,13 @@ export class TemplateEditor extends FormElement { @property({ type: String }) lang = 'eng-US'; + // initial variables, not reflected back @property({ type: Array }) variables: string[]; + @property({ type: Array }) + currentVariables: string[]; + @property({ type: Object, attribute: false }) translation: Translation; @@ -172,47 +176,54 @@ export class TemplateEditor extends FormElement { changes: PropertyValueMap | Map ): void { super.firstUpdated(changes); + if (changes.has('variables') && this.variables) { + this.currentVariables = this.variables.slice(); + } } - public updated(changedProperties: Map): void { - super.updated(changedProperties); + public updated(changes: Map): void { + super.updated(changes); + + if (changes.has('template')) { + this.currentVariables = this.variables; + } } private handleTemplateChanged(event: CustomEvent) { const prev = this.selectedTemplate; this.selectedTemplate = (event.target as any).values[0] as Template; + + if (prev) { + this.currentVariables = []; + } + const [lang, loc] = this.lang.split('-'); if (this.selectedTemplate) { - this.selectedTemplate.translations.forEach((translation) => { - if ( - translation.locale === this.lang || - (!loc && translation.locale.split('-')[0] === lang) - ) { - this.translation = translation; - // initialize our variables array - const newVariables = new Array( - (translation.variables || []).length - ).fill(''); - - if (!prev) { - // copy our previous variables into newVariables - if (this.variables) { - this.variables.forEach((variable, index) => { - newVariables[index] = variable; - }); - } - } - this.variables = newVariables; + this.translation = this.selectedTemplate.translations.find( + (translation) => { + return ( + translation.locale === this.lang || + (!loc && translation.locale.split('-')[0] === lang) + ); } - }); + ); + + if (this.translation) { + this.variables = new Array( + (this.translation.variables || []).length + ).fill(''); + } else { + this.variables = []; + } } else { this.translation = null; + this.variables = []; } this.fireCustomEvent(CustomEventType.ContextChanged, { template: this.selectedTemplate, translation: this.translation, - variables: this.variables + variables: this.currentVariables }); } @@ -228,22 +239,25 @@ export class TemplateEditor extends FormElement { const index = parseInt(media.getAttribute('index')); if (media.attachments.length === 0) { - this.variables[index] = ''; + this.currentVariables[index] = ''; } else { const attachment = media.attachments[0]; if (attachment.url && attachment.content_type) { - this.variables[index] = `${attachment.content_type}:${attachment.url}`; + this.currentVariables[ + index + ] = `${attachment.content_type}:${attachment.url}`; } else { - this.variables[index] = ``; + this.currentVariables[index] = ``; } } this.fireContentChange(); + this.requestUpdate('currentVariables'); } private handleVariableChanged(event: CustomEvent) { const target = event.target as HTMLInputElement; const variableIndex = parseInt(target.getAttribute('index')); - this.variables[variableIndex] = target.value; + this.currentVariables[variableIndex] = target.value; this.fireContentChange(); } @@ -251,7 +265,7 @@ export class TemplateEditor extends FormElement { this.fireCustomEvent(CustomEventType.ContentChanged, { template: this.selectedTemplate, translation: this.translation, - variables: this.variables + variables: this.currentVariables }); } @@ -275,11 +289,12 @@ export class TemplateEditor extends FormElement { return html`${part}`; } const variableIndex = component.variables[part]; + const currVariables = this.currentVariables || []; return html` @@ -387,7 +403,7 @@ export class TemplateEditor extends FormElement { let content = null; if (this.translation) { content = this.renderComponents(this.translation.components); - } else { + } else if (this.selectedTemplate) { content = html`
No approved translation was found for current language.
`; @@ -409,8 +425,7 @@ export class TemplateEditor extends FormElement { @change=${this.handleTemplateChanged} > - - ${this.template ? html`
${content}
` : null} + ${content ? html`
${content}
` : null} `; } diff --git a/test/temba-compose.test.ts b/test/temba-compose.test.ts index 9eadc534..eb1d1a8f 100644 --- a/test/temba-compose.test.ts +++ b/test/temba-compose.test.ts @@ -39,7 +39,9 @@ const getInitialValue = ( text: text ? text : '', attachments: attachments ? attachments : [], quick_replies: quick_replies ? quick_replies : [], - optin: null + optin: null, + template: null, + variables: [] } }; return composeValue; diff --git a/test/temba-template-editor.test.ts b/test/temba-template-editor.test.ts index 9e60a2ce..43944f52 100644 --- a/test/temba-template-editor.test.ts +++ b/test/temba-template-editor.test.ts @@ -18,7 +18,6 @@ describe('TemplateEditor', () => { `); @@ -57,6 +56,12 @@ describe('TemplateEditor', () => { `); + const clip = getClip(templateEditor); + clip.height = 200; + clip.bottom = clip.top + clip.height; + + await assertScreenshot('templates/unapproved', clip); + const errorMessage = ( templateEditor.shadowRoot.querySelector( '.error-message'