From 1fc4b00b5fbaa02fc146057e38cd9020dca5bdd1 Mon Sep 17 00:00:00 2001 From: Aina Vendrell Date: Tue, 15 Jun 2021 11:50:38 +0200 Subject: [PATCH] wip-handoff --- .../src/cms/callback.ts | 2 + .../src/cms/cms-dummy.ts | 13 ++++ .../src/cms/cms-log.ts | 6 ++ .../src/cms/cms-multilocale.ts | 5 ++ .../src/cms/contents.ts | 4 +- .../src/cms/factories/content-factories.ts | 37 +++++++++++ .../src/cms/test-helpers/builders.ts | 18 ++++++ .../src/cms/transform/cms-filter.ts | 5 ++ .../src/contentful/cms-contentful.ts | 10 +++ .../src/contentful/contents/button.ts | 37 ++--------- .../contentful/contents/callback-delivery.ts | 59 ++++++++++++++++++ .../src/contentful/contents/handoff.ts | 62 ++++++++++++------- .../src/contentful/manage/manage-entry.ts | 6 +- .../src/manage-cms/fields.ts | 37 +++++++++++ .../tests/contentful/contents/handoff.test.ts | 31 ++++++++++ 15 files changed, 275 insertions(+), 57 deletions(-) create mode 100644 packages/botonic-plugin-contentful/src/contentful/contents/callback-delivery.ts create mode 100644 packages/botonic-plugin-contentful/tests/contentful/contents/handoff.test.ts diff --git a/packages/botonic-plugin-contentful/src/cms/callback.ts b/packages/botonic-plugin-contentful/src/cms/callback.ts index 95ae7d0775..523a40d0b6 100644 --- a/packages/botonic-plugin-contentful/src/cms/callback.ts +++ b/packages/botonic-plugin-contentful/src/cms/callback.ts @@ -190,6 +190,8 @@ export class TopContentId extends ContentId { return cms.url(this.id, context) case ContentType.SCHEDULE: return cms.schedule(this.id) + case ContentType.HANDOFF: + return cms.handoff(this.id, context) default: throw new Error( // eslint-disable-next-line @typescript-eslint/restrict-template-expressions diff --git a/packages/botonic-plugin-contentful/src/cms/cms-dummy.ts b/packages/botonic-plugin-contentful/src/cms/cms-dummy.ts index 5dbb0953ed..68ff33be1e 100644 --- a/packages/botonic-plugin-contentful/src/cms/cms-dummy.ts +++ b/packages/botonic-plugin-contentful/src/cms/cms-dummy.ts @@ -12,6 +12,8 @@ import { DateRangeContent, Document, Element, + Handoff, + HandoffQueue, Image, Queue, ScheduleContent, @@ -63,6 +65,17 @@ export class DummyCMS implements CMS { ) } + async handoff(id: string, {} = DEFAULT_CONTEXT): Promise { + return Promise.resolve( + new Handoff( + new CommonFields(id, id), + 'Dummy text for ' + id, + this.buttonCallbacks[0], + new HandoffQueue('queue') + ) + ) + } + chitchat(id: string, context = DEFAULT_CONTEXT): Promise { return this.text(id, context) } diff --git a/packages/botonic-plugin-contentful/src/cms/cms-log.ts b/packages/botonic-plugin-contentful/src/cms/cms-log.ts index 73a19c45f2..b75f5f39d6 100644 --- a/packages/botonic-plugin-contentful/src/cms/cms-log.ts +++ b/packages/botonic-plugin-contentful/src/cms/cms-log.ts @@ -11,6 +11,7 @@ import { DateRangeContent, Document, Element, + Handoff, Image, Queue, ScheduleContent, @@ -80,6 +81,11 @@ export class LogCMS implements CMS { return this.cms.element(id, context) } + handoff(id: string, context?: Context): Promise { + this.logContentDelivery(ContentType.HANDOFF, id, context) + return this.cms.handoff(id, context) + } + content(id: string, context?: Context): Promise { this.logContentDelivery('content' as ContentType, id, context) return this.cms.content(id, context) diff --git a/packages/botonic-plugin-contentful/src/cms/cms-multilocale.ts b/packages/botonic-plugin-contentful/src/cms/cms-multilocale.ts index 114fa7b1a7..6cc73b1d40 100644 --- a/packages/botonic-plugin-contentful/src/cms/cms-multilocale.ts +++ b/packages/botonic-plugin-contentful/src/cms/cms-multilocale.ts @@ -10,6 +10,7 @@ import { DateRangeContent, Document, Element, + Handoff, Image, Queue, ScheduleContent, @@ -95,6 +96,10 @@ export class MultiContextCms implements CMS { return this.cmsFromContext(context).text(id, context) } + handoff(id: string, context?: Context): Promise { + return this.cmsFromContext(context).handoff(id, context) + } + topContents( model: TopContentType, context?: Context, diff --git a/packages/botonic-plugin-contentful/src/cms/contents.ts b/packages/botonic-plugin-contentful/src/cms/contents.ts index a8bbc3f0b3..1e56df18a8 100644 --- a/packages/botonic-plugin-contentful/src/cms/contents.ts +++ b/packages/botonic-plugin-contentful/src/cms/contents.ts @@ -438,7 +438,7 @@ export class OnFinishPath { constructor(readonly path: string) {} } -export type OnFinish = OnFinishPath | OnFinishPayload +export type OnFinish = Callback export class HandoffAgentEmail { readonly type = 'AGENT_EMAIL' @@ -472,7 +472,7 @@ export class Handoff extends TopContent { constructor( readonly common: CommonFields, readonly text: string, - readonly onFinish: OnFinish, + readonly onFinish?: OnFinish, readonly destination?: HandoffDestination, readonly shadowing?: boolean ) { diff --git a/packages/botonic-plugin-contentful/src/cms/factories/content-factories.ts b/packages/botonic-plugin-contentful/src/cms/factories/content-factories.ts index a99d661f16..8ecfd02ad9 100644 --- a/packages/botonic-plugin-contentful/src/cms/factories/content-factories.ts +++ b/packages/botonic-plugin-contentful/src/cms/factories/content-factories.ts @@ -7,7 +7,10 @@ import { Document, Element, FollowUp, + Handoff, + HandoffDestination, Image, + OnFinish, StartUp, Text, } from '../contents' @@ -219,3 +222,37 @@ export class DocumentBuilder extends MessageContentBuilder { return new Document(this.buildCommonFields(), this.docUrl) } } + +export class HandoffBuilder extends MessageContentBuilder { + onFinish?: OnFinish + destination?: HandoffDestination + shadowing?: boolean + constructor(id: string, name: string, public text: string) { + super(id, name) + } + + withOnFinish(onFinish?: OnFinish): this { + this.onFinish = onFinish + return this + } + + withDestination(destination: HandoffDestination): this { + this.destination = destination + return this + } + + withShadowing(shadowing: boolean): this { + this.shadowing = shadowing + return this + } + + build(): Handoff { + return new Handoff( + this.buildCommonFields(), + this.text, + this.onFinish, + this.destination, + this.shadowing + ) + } +} diff --git a/packages/botonic-plugin-contentful/src/cms/test-helpers/builders.ts b/packages/botonic-plugin-contentful/src/cms/test-helpers/builders.ts index f7e0535acf..78919cc2c1 100644 --- a/packages/botonic-plugin-contentful/src/cms/test-helpers/builders.ts +++ b/packages/botonic-plugin-contentful/src/cms/test-helpers/builders.ts @@ -1,8 +1,10 @@ import { ContentType } from '../cms' +import { HandoffQueue } from '../contents' import { CarouselBuilder, DocumentBuilder, ElementBuilder, + HandoffBuilder, ImageBuilder, StartUpBuilder, TextBuilder, @@ -234,3 +236,19 @@ export class RndDocumentBuilder extends DocumentBuilder { return this } } + +export class RndHandoffBuilder extends HandoffBuilder { + readonly topComponentBuilder = new RndTopContentBuilder() + + constructor(name: string = rndStr(), text: string = rndStr()) { + super(rndStr(), name, text) + } + + withRandomFields(): this { + this.onFinish = new ContentCallbackBuilder().build() + this.destination = new HandoffQueue(rndStr()) + this.shadowing = rndBool() + this.topComponentBuilder.withRandomFields(this) + return this + } +} diff --git a/packages/botonic-plugin-contentful/src/cms/transform/cms-filter.ts b/packages/botonic-plugin-contentful/src/cms/transform/cms-filter.ts index a867369769..3d9508cb31 100644 --- a/packages/botonic-plugin-contentful/src/cms/transform/cms-filter.ts +++ b/packages/botonic-plugin-contentful/src/cms/transform/cms-filter.ts @@ -13,6 +13,7 @@ import { DateRangeContent, Document, Element, + Handoff, Image, MessageContent, PagingOptions, @@ -92,6 +93,10 @@ export class FilteredCMS implements CMS { return this.filterContent(content, Image, context) } + handoff(id: string, context?: Context): Promise { + return this.cms.handoff(id, context) + } + url(id: string, context?: Context): Promise { return this.cms.url(id, context) } diff --git a/packages/botonic-plugin-contentful/src/contentful/cms-contentful.ts b/packages/botonic-plugin-contentful/src/contentful/cms-contentful.ts index be811db4a7..e25d9d7d21 100644 --- a/packages/botonic-plugin-contentful/src/contentful/cms-contentful.ts +++ b/packages/botonic-plugin-contentful/src/contentful/cms-contentful.ts @@ -23,6 +23,7 @@ import { ContentsDelivery } from './contents/contents' import { DateRangeDelivery } from './contents/date-range' import { DocumentDelivery } from './contents/document' import { FollowUpDelivery } from './contents/follow-up' +import { HandoffDelivery } from './contents/handoff' import { ImageDelivery } from './contents/image' import { QueueDelivery } from './contents/queue' import { ScheduleDelivery } from './contents/schedule' @@ -50,6 +51,7 @@ export class Contentful implements cms.CMS { private readonly _schedule: ScheduleDelivery private readonly _dateRange: DateRangeDelivery private readonly _image: ImageDelivery + private readonly _handoff: HandoffDelivery private readonly _asset: AssetDelivery private readonly _queue: QueueDelivery private readonly _button: ButtonDelivery @@ -81,6 +83,7 @@ export class Contentful implements cms.CMS { this._startUp = new StartUpDelivery(delivery, this._button, resumeErrors) this._url = new UrlDelivery(delivery, resumeErrors) this._image = new ImageDelivery(delivery, resumeErrors) + this._handoff = new HandoffDelivery(delivery, resumeErrors) this._asset = new AssetDelivery(delivery, resumeErrors) this._schedule = new ScheduleDelivery(delivery, resumeErrors) this._queue = new QueueDelivery(delivery, this._schedule, resumeErrors) @@ -98,6 +101,7 @@ export class Contentful implements cms.CMS { this._carousel, this._image, this._startUp, + this._handoff, ].forEach(d => d.setFollowUp(followUp)) this._keywords = new KeywordsDelivery(delivery) this._dateRange = new DateRangeDelivery(delivery, resumeErrors) @@ -146,6 +150,10 @@ export class Contentful implements cms.CMS { return this._text.text(id, context) } + async handoff(id: string, context = DEFAULT_CONTEXT): Promise { + return this._handoff.handoff(id, context) + } + topContents( model: TopContentType, context = DEFAULT_CONTEXT, @@ -197,6 +205,8 @@ export class Contentful implements cms.CMS { return retype(await this._text.fromEntry(entry, context)) case ContentType.IMAGE: return retype(await this._image.fromEntry(entry, context)) + case ContentType.HANDOFF: + return retype(this._handoff.fromEntry(entry, context)) case ContentType.URL: return retype(await this._url.fromEntry(entry, context)) case ContentType.STARTUP: diff --git a/packages/botonic-plugin-contentful/src/contentful/contents/button.ts b/packages/botonic-plugin-contentful/src/contentful/contents/button.ts index c0ae9447cd..3272c31ec9 100644 --- a/packages/botonic-plugin-contentful/src/contentful/contents/button.ts +++ b/packages/botonic-plugin-contentful/src/contentful/contents/button.ts @@ -11,7 +11,9 @@ import { ContentWithNameFields, } from '../delivery-utils' import { DeliveryApi } from '../index' +import { getTargetCallback } from './callback-delivery' import { CarouselFields } from './carousel' +import { HandoffFields } from './handoff' import { QueueFields } from './queue' import { HourRangeFields, ScheduleFields } from './schedule' import { StartUpFields } from './startup' @@ -20,7 +22,6 @@ import { UrlFields } from './url' export class ButtonDelivery extends ContentDelivery { public static BUTTON_CONTENT_TYPE = 'button' - private static PAYLOAD_CONTENT_TYPE = 'payload' constructor(delivery: DeliveryApi, resumeErrors: boolean) { super(cms.ContentType.BUTTON, delivery, resumeErrors) @@ -82,7 +83,7 @@ export class ButtonDelivery extends ContentDelivery { ) } // target may be empty if we got it from a reference (delivery does not provide infinite recursive references) - const callback = this.getTargetCallback(buttonEntry.fields.target, context) + const callback = getTargetCallback(buttonEntry.fields.target, context) return new cms.Button( buttonEntry.sys.id, buttonEntry.fields.name, @@ -115,37 +116,6 @@ export class ButtonDelivery extends ContentDelivery { } return new cms.ContentCallback(modelType, entry.sys.id) } - - private getTargetCallback( - target: ButtonTarget, - context: cms.Context - ): cms.Callback { - const model = ContentfulEntryUtils.getContentModel(target) as string - try { - switch (model) { - case ContentType.URL: { - const urlFields = target as contentful.Entry - if (!urlFields.fields.url && context.ignoreFallbackLocale) { - return cms.Callback.empty() - } - return cms.Callback.ofUrl(urlFields.fields.url || '') - } - case ButtonDelivery.PAYLOAD_CONTENT_TYPE: { - const payloadFields = target as contentful.Entry - return cms.Callback.ofPayload(payloadFields.fields.payload) - } - } - if (isOfType(model, TopContentType)) { - return new cms.ContentCallback(model, target.sys.id) - } - throw new Error('Unexpected type: ' + model) - } catch (e) { - throw new CmsException( - `Error delivering button with id '${target.sys.id}'`, - e - ) - } - } } export interface PayloadFields { @@ -161,6 +131,7 @@ type ButtonTarget = contentful.Entry< | QueueFields | HourRangeFields | ScheduleFields + | HandoffFields > export interface ButtonFields extends ContentWithNameFields { diff --git a/packages/botonic-plugin-contentful/src/contentful/contents/callback-delivery.ts b/packages/botonic-plugin-contentful/src/contentful/contents/callback-delivery.ts new file mode 100644 index 0000000000..61746fb962 --- /dev/null +++ b/packages/botonic-plugin-contentful/src/contentful/contents/callback-delivery.ts @@ -0,0 +1,59 @@ +import * as contentful from 'contentful' + +import * as cms from '../../cms' +import { CmsException, ContentType } from '../../cms' +import { TopContentType } from '../../cms/cms' +import { isOfType } from '../../util/enums' +import { ContentfulEntryUtils } from '../delivery-utils' +import { PayloadFields } from './button' +import { CarouselFields } from './carousel' +import { HandoffFields } from './handoff' +import { QueueFields } from './queue' +import { HourRangeFields, ScheduleFields } from './schedule' +import { StartUpFields } from './startup' +import { TextFields } from './text' +import { UrlFields } from './url' + +export type CallbackTarget = contentful.Entry< + | CarouselFields + | TextFields + | UrlFields + | PayloadFields + | StartUpFields + | QueueFields + | HourRangeFields + | ScheduleFields + | HandoffFields +> + +export function getTargetCallback( + target: CallbackTarget, + context: cms.Context +): cms.Callback { + const PAYLOAD_CONTENT_TYPE = 'payload' + const model = ContentfulEntryUtils.getContentModel(target) as string + try { + switch (model) { + case ContentType.URL: { + const urlFields = target as contentful.Entry + if (!urlFields.fields.url && context.ignoreFallbackLocale) { + return cms.Callback.empty() + } + return cms.Callback.ofUrl(urlFields.fields.url || '') + } + case PAYLOAD_CONTENT_TYPE: { + const payloadFields = target as contentful.Entry + return cms.Callback.ofPayload(payloadFields.fields.payload) + } + } + if (isOfType(model, TopContentType)) { + return new cms.ContentCallback(model, target.sys.id) + } + throw new Error('Unexpected type: ' + model) + } catch (e) { + throw new CmsException( + `Error delivering button with id '${target.sys.id}'`, + e + ) + } +} diff --git a/packages/botonic-plugin-contentful/src/contentful/contents/handoff.ts b/packages/botonic-plugin-contentful/src/contentful/contents/handoff.ts index 592d7a252e..05c835ab34 100644 --- a/packages/botonic-plugin-contentful/src/contentful/contents/handoff.ts +++ b/packages/botonic-plugin-contentful/src/contentful/contents/handoff.ts @@ -1,13 +1,22 @@ import * as contentful from 'contentful' import * as cms from '../../cms' -import { CmsException, ContentType, OnFinish, OnFinishPayload } from '../../cms' +import { + CmsException, + ContentType, + HandoffAgentEmail, + HandoffAgentId, + HandoffDestination, + HandoffQueue, + OnFinish, +} from '../../cms' import { DeliveryApi } from '../delivery-api' import { addCustomFields, CommonEntryFields, ContentfulEntryUtils, } from '../delivery-utils' +import { CallbackTarget, getTargetCallback } from './callback-delivery' import { DeliveryWithFollowUp } from './follow-up' export class HandoffDelivery extends DeliveryWithFollowUp { @@ -23,51 +32,62 @@ export class HandoffDelivery extends DeliveryWithFollowUp { return this.fromEntry(entry, context) } - onFinish(entry: contentful.Entry): OnFinish { - if (entry.fields.onFinishPayload && entry.fields.onFinishPath) { + private onFinish( + entry: contentful.Entry, + context: cms.Context + ): OnFinish | undefined { + if (entry.fields.onFinish) { + return getTargetCallback(entry.fields.onFinish, context) + } + return undefined + } + + private destination( + entry: contentful.Entry + ): HandoffDestination | undefined { + if ( + (entry.fields.queue && entry.fields.agentEmail) || + (entry.fields.queue && entry.fields.agentId) || + (entry.fields.agentEmail && entry.fields.agentId) + ) { throw new CmsException( - `${this.getContentIdForLogs(entry)} has both path and payload` + `${this.getContentIdForLogs(entry)} has more than one destination` ) - } else if (entry.fields.onFinishPayload) { - return new OnFinishPayload(entry.fields.onFinishPayload) - } else if (entry.fields.onFinishPath) { - return new OnFinishPayload(entry.fields.onFinishPath) + } else if (entry.fields.queue) { + return new HandoffQueue(entry.fields.queue) + } else if (entry.fields.agentEmail) { + return new HandoffAgentEmail(entry.fields.agentEmail) + } else if (entry.fields.agentId) { + return new HandoffAgentId(entry.fields.agentId) } - throw new CmsException( - `${this.getContentIdForLogs(entry)} has neither path nor payload` - ) + return undefined } - async fromEntry( + fromEntry( entry: contentful.Entry, context: cms.Context - ): Promise { + ): cms.Handoff { const fields = entry.fields const common = ContentfulEntryUtils.commonFieldsFromEntry(entry) return addCustomFields( new cms.Handoff( common, fields.text, - this.onFinish(entry), + this.onFinish(entry, context), this.destination(entry), fields.shadowing ), fields, - ['onFinishPath', 'onFinishPayload', 'queue', 'agentEmail', 'agendId'] + ['onFinish', 'queue', 'agentEmail', 'agentId'] ) } } -export interface HandoffFields extends CommonEntryFields { - handoff: contentful.Asset -} - export interface HandoffFields extends CommonEntryFields { text: string queue?: string agentEmail?: string agentId?: string - onFinishPath?: string - onFinishPayload?: string + onFinish?: CallbackTarget shadowing?: boolean } diff --git a/packages/botonic-plugin-contentful/src/contentful/manage/manage-entry.ts b/packages/botonic-plugin-contentful/src/contentful/manage/manage-entry.ts index 51bbc339a8..02f5272d3c 100644 --- a/packages/botonic-plugin-contentful/src/contentful/manage/manage-entry.ts +++ b/packages/botonic-plugin-contentful/src/contentful/manage/manage-entry.ts @@ -181,7 +181,11 @@ export class ManageContentfulEntry { return field === ButtonStyle.QUICK_REPLY ? QUICK_REPLY : BUTTON } if (field === undefined) return field - if (key === ContentFieldType.FOLLOW_UP || key === ContentFieldType.TARGET) { + if ( + key === ContentFieldType.FOLLOW_UP || + key === ContentFieldType.TARGET || + key === ContentFieldType.ON_FINISH + ) { return this.getEntryLink(field, 'Entry') } if (key === ContentFieldType.IMAGE || key === ContentFieldType.PIC) { diff --git a/packages/botonic-plugin-contentful/src/manage-cms/fields.ts b/packages/botonic-plugin-contentful/src/manage-cms/fields.ts index 5943189796..d44e7f1b50 100644 --- a/packages/botonic-plugin-contentful/src/manage-cms/fields.ts +++ b/packages/botonic-plugin-contentful/src/manage-cms/fields.ts @@ -22,6 +22,11 @@ export enum ContentFieldType { BUTTONS_STYLE = 'Buttons Style', FOLLOW_UP = 'FollowUp', TARGET = 'Target', + QUEUE = 'Queue', + AGENT_EMAIL = 'Agent Email', + AGENT_ID = 'Agent Id', + ON_FINISH = 'On Finish', + SHADOWING = 'shadowing', } export enum ContentFieldValueType { @@ -30,6 +35,7 @@ export enum ContentFieldValueType { REFERENCE = 'reference', REFERENCE_ARRAY = 'reference[]', ASSET = 'asset', + BOOLEAN = 'boolean', } export class ContentField { @@ -166,6 +172,36 @@ export const CONTENT_FIELDS = new Map( ContentFieldValueType.REFERENCE, true ), + new ContentField( + ContentFieldType.QUEUE, + 'queue', + ContentFieldValueType.STRING, + true + ), + new ContentField( + ContentFieldType.AGENT_EMAIL, + 'agentEmail', + ContentFieldValueType.STRING, + true + ), + new ContentField( + ContentFieldType.AGENT_ID, + 'agentId', + ContentFieldValueType.STRING, + true + ), + new ContentField( + ContentFieldType.ON_FINISH, + 'onFinish', + ContentFieldValueType.REFERENCE, + true + ), + new ContentField( + ContentFieldType.SHADOWING, + 'shadowing', + ContentFieldValueType.BOOLEAN, + true + ), ]) ) @@ -197,6 +233,7 @@ const FIELDS_PER_CONTENT_TYPE: { [type: string]: ContentFieldType[] } = { [ContentType.STARTUP]: [ContentFieldType.TEXT, ContentFieldType.BUTTONS], [ContentType.TEXT]: [ContentFieldType.TEXT, ContentFieldType.BUTTONS], [ContentType.URL]: [ContentFieldType.URL], + [ContentType.HANDOFF]: [ContentFieldType.TEXT], } export function getFieldsForContentType( diff --git a/packages/botonic-plugin-contentful/tests/contentful/contents/handoff.test.ts b/packages/botonic-plugin-contentful/tests/contentful/contents/handoff.test.ts new file mode 100644 index 0000000000..541700b8c3 --- /dev/null +++ b/packages/botonic-plugin-contentful/tests/contentful/contents/handoff.test.ts @@ -0,0 +1,31 @@ +import { + ContentCallback, + ContentType, + HandoffQueue, + SPANISH, +} from '../../../src' +import { testContentful, testContext } from '../contentful.helper' + +export const TEST_HANDOFF_MAIN_ID = '6I5SudoItjGmxrVZPr0qeG' + +test('TEST: contentful handoff', async () => { + const sut = testContentful() + + // act + const handoff = await sut.handoff( + TEST_HANDOFF_MAIN_ID, + testContext([{ locale: SPANISH }]) + ) + + // assert + expect(handoff.text).toEqual('En breve un agente le atenderĂ¡') + expect(handoff.common.name).toEqual('HANDOFF') + expect(handoff.common.shortText).toEqual('Agent Handoff') + expect(handoff.destination).toEqual(new HandoffQueue('agent queue')) + expect(handoff.onFinish).toEqual( + new ContentCallback(ContentType.TEXT, 'C39lEROUgJl9hHSXKOEXS') + ) + expect(handoff.common.customFields).toEqual({ + customFieldText: 'custom Text', + }) +})