diff --git a/src/components/flow/actions/requestoptin/RequestOptIn.tsx b/src/components/flow/actions/requestoptin/RequestOptIn.tsx new file mode 100644 index 000000000..f175395bd --- /dev/null +++ b/src/components/flow/actions/requestoptin/RequestOptIn.tsx @@ -0,0 +1,8 @@ +import { RequestOptIn } from 'flowTypes'; +import * as React from 'react'; + +const RequestOptInComp: React.SFC = (action: RequestOptIn): JSX.Element => { + return
{action.optin.name}
; +}; + +export default RequestOptInComp; diff --git a/src/components/flow/actions/requestoptin/RequestOptInForm.tsx b/src/components/flow/actions/requestoptin/RequestOptInForm.tsx new file mode 100644 index 000000000..288389ea6 --- /dev/null +++ b/src/components/flow/actions/requestoptin/RequestOptInForm.tsx @@ -0,0 +1,109 @@ +import { react as bindCallbacks } from 'auto-bind'; +import Dialog, { ButtonSet } from 'components/dialog/Dialog'; +import { ActionFormProps } from 'components/flow/props'; +import AssetSelector from 'components/form/assetselector/AssetSelector'; +import TypeList from 'components/nodeeditor/TypeList'; +import { fakePropType } from 'config/ConfigProvider'; +import * as React from 'react'; +import { Asset } from 'store/flowContext'; +import { FormState, mergeForm } from 'store/nodeEditor'; +import { shouldRequireIf, validate } from 'store/validators'; + +import i18n from 'config/i18n'; +import { Trans } from 'react-i18next'; +import { renderIssues } from '../helpers'; +import { initializeForm, stateToAction } from './helpers'; + +export interface RequestOptInFormState extends FormState { + optin: any; +} + +export const controlLabelSpecId = 'label'; + +export default class AddLabelsForm extends React.PureComponent< + ActionFormProps, + RequestOptInFormState +> { + public static contextTypes = { + assetService: fakePropType + }; + + constructor(props: ActionFormProps) { + super(props); + + this.state = initializeForm(this.props.nodeSettings); + bindCallbacks(this, { + include: [/^on/, /^handle/] + }); + } + + public handleSave(): void { + const valid = this.handleOptInChanged(this.state.optin.value!, true); + + if (valid) { + const newAction = stateToAction(this.props.nodeSettings, this.state); + this.props.updateAction(newAction); + this.props.onClose(false); + } + } + + public handleOptInChanged(selected: Asset[], submitting: boolean = false): boolean { + const updates: Partial = { + optin: validate(i18n.t('forms.title', 'Name'), selected, [shouldRequireIf(submitting)]) + }; + + const updated = mergeForm(this.state, updates); + this.setState(updated); + return updated.valid; + } + + private getButtons(): ButtonSet { + return { + primary: { name: i18n.t('buttons.ok', 'Ok'), onClick: this.handleSave }, + secondary: { + name: i18n.t('buttons.cancel', 'Cancel'), + onClick: () => this.props.onClose(true) + } + }; + } + + public handleCreateAssetFromInput(input: string): any { + return { name: input }; + } + + public handleOptInCreated(optin: Asset): void { + // update our store with our new group + this.props.addAsset('optins', optin); + + this.handleOptInChanged(this.state.optin.value); + } + + public render(): JSX.Element { + const typeConfig = this.props.typeConfig; + return ( + + +

+ Select the Opt-In to request +

+ + + {renderIssues(this.props)} +
+ ); + } +} diff --git a/src/components/flow/actions/requestoptin/helpers.ts b/src/components/flow/actions/requestoptin/helpers.ts new file mode 100644 index 000000000..7db0f96c1 --- /dev/null +++ b/src/components/flow/actions/requestoptin/helpers.ts @@ -0,0 +1,32 @@ +import { getActionUUID } from 'components/flow/actions/helpers'; +import { Types } from 'config/interfaces'; +import { RequestOptIn } from 'flowTypes'; +import { NodeEditorSettings } from 'store/nodeEditor'; +import { RequestOptInFormState } from './RequestOptInForm'; + +export const initializeForm = (settings: NodeEditorSettings): RequestOptInFormState => { + if (settings.originalAction && settings.originalAction.type === Types.request_optin) { + const action = settings.originalAction as RequestOptIn; + return { + optin: { value: action.optin }, + valid: true + }; + } + + return { + optin: { value: null }, + valid: false + }; +}; + +export const stateToAction = ( + settings: NodeEditorSettings, + formState: RequestOptInFormState +): RequestOptIn => { + const result = { + type: Types.request_optin, + optin: formState.optin.value[0], + uuid: getActionUUID(settings, Types.add_input_labels) + }; + return result; +}; diff --git a/src/components/shared.module.scss b/src/components/shared.module.scss index b90082e25..85a610af7 100644 --- a/src/components/shared.module.scss +++ b/src/components/shared.module.scss @@ -18,6 +18,7 @@ border-top-right-radius: 3px; padding: 10px; color: $red; + .icon { display: inline-block; vertical-align: middle; @@ -38,6 +39,7 @@ .issue_help { cursor: pointer; + &:hover { color: lighten($red, 3%); text-decoration: underline; @@ -70,7 +72,8 @@ .msg, .say_msg, -.send_msg { +.send_msg, +.request_optin { background: $blue; } @@ -103,13 +106,11 @@ $color_1: tomato; $color_2: lighten(tomato, 3%); - background-image: repeating-linear-gradient( - 120deg, - $color_1, - $color_1 6px, - $color_2 6px, - $color_2 18px - ) !important; + background-image: repeating-linear-gradient(120deg, + $color_1, + $color_1 6px, + $color_2 6px, + $color_2 18px) !important; } .missing_asset { @@ -144,6 +145,7 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + .node_group { padding-right: 5px; } @@ -159,4 +161,4 @@ .alert { background: $red; -} +} \ No newline at end of file diff --git a/src/config/interfaces.ts b/src/config/interfaces.ts index f3ac6d5f9..bb47fbc00 100644 --- a/src/config/interfaces.ts +++ b/src/config/interfaces.ts @@ -56,6 +56,7 @@ export enum Types { wait_for_video = 'wait_for_video', wait_for_location = 'wait_for_location', wait_for_image = 'wait_for_image', + request_optin = 'request_optin', missing = 'missing', say_msg = 'say_msg', play_audio = 'play_audio' @@ -103,7 +104,8 @@ export enum FeatureFilter { HAS_CLASSIFIER = 'classifier', HAS_TICKETER = 'ticketer', HAS_FACEBOOK = 'facebook', - HAS_LOCATIONS = 'locations' + HAS_LOCATIONS = 'locations', + HAS_OPTINS = 'optins' } export interface FlowTypeVisibility { diff --git a/src/config/typeConfigs.ts b/src/config/typeConfigs.ts index db9b92f1b..4f2f44fda 100644 --- a/src/config/typeConfigs.ts +++ b/src/config/typeConfigs.ts @@ -63,6 +63,8 @@ import i18n from 'config/i18n'; import SchemeRouterForm from 'components/flow/routers/scheme/SchemeRouterForm'; import TicketRouterForm from 'components/flow/routers/ticket/TicketRouterForm'; import OpenTicketComp from 'components/flow/actions/openticket/OpenTicket'; +import RequestOptInForm from 'components/flow/actions/requestoptin/RequestOptInForm'; +import RequestOptInComp from 'components/flow/actions/requestoptin/RequestOptIn'; const dedupeTypeConfigs = (typeConfigs: Type[]) => { const map: any = {}; @@ -164,12 +166,12 @@ export const SCHEMES: Scheme[] = [ { scheme: 'vk', name: i18n.t('schemes.vk.name', 'VK'), - path: i18n.t('schemes.vk.path', 'VK ID'), + path: i18n.t('schemes.vk.path', 'VK ID') }, { scheme: 'discord', name: i18n.t('schemes.discord.name', 'Discord'), - path: i18n.t('schemes.discord.path', 'Discord ID'), + path: i18n.t('schemes.discord.path', 'Discord ID') }, { scheme: 'webchat', @@ -180,7 +182,7 @@ export const SCHEMES: Scheme[] = [ { scheme: 'rocketchat', name: i18n.t('schemes.rocketchat.name', 'RocketChat'), - path: i18n.t('schemes.rocketchat.path', 'RocketChat ID'), + path: i18n.t('schemes.rocketchat.path', 'RocketChat ID') }, { scheme: 'ext', @@ -514,6 +516,14 @@ export const typeConfigList: Type[] = [ localization: RouterLocalizationForm, localizeableKeys: ['exits'], form: SchemeRouterForm + }, + { + type: Types.request_optin, + name: i18n.t('actions.request_optin.name', 'Request Opt-In'), + description: i18n.t('actions.request_optin.description', 'Send an Opt-In request'), + form: RequestOptInForm, + component: RequestOptInComp, + filter: FeatureFilter.HAS_OPTINS } // {type: 'random', name: 'Random Split', description: 'Split them up randomly', form: RandomRouterForm} ]; diff --git a/src/external/index.ts b/src/external/index.ts index 0db3f90ee..77a25d73a 100644 --- a/src/external/index.ts +++ b/src/external/index.ts @@ -311,6 +311,11 @@ export const createAssetStore = (endpoints: Endpoints): Promise => { type: AssetType.Label, items: {} }, + optins: { + endpoint: getURL(endpoints.optins), + type: AssetType.OptIn, + items: {} + }, results: { type: AssetType.Result, items: {} diff --git a/src/flowTypes.ts b/src/flowTypes.ts index f485d271c..a10802534 100644 --- a/src/flowTypes.ts +++ b/src/flowTypes.ts @@ -34,6 +34,7 @@ export interface Endpoints { revisions: string; activity: string; labels: string; + optins: string; channels: string; classifiers: string; ticketers: string; @@ -59,6 +60,7 @@ export interface FlowEditorConfig { path?: string; headers?: any; brand: string; + onLoad?: () => void; onActivityClicked?: (uuid: string) => void; onChangeLanguage?: (code: string, name: string) => void; @@ -292,6 +294,11 @@ export interface Label { name_match?: string; } +export interface OptIn { + uuid: string; + name: string; +} + export interface Flow { uuid: string; name: string; @@ -395,6 +402,13 @@ export interface AddLabels extends Action { labels: Label[]; } +export interface RequestOptIn extends Action { + optin: { + uuid: string; + name: string; + }; +} + export interface AddURN extends Action { scheme: string; path: string; diff --git a/src/store/flowContext.ts b/src/store/flowContext.ts index f4646a49e..73130eed1 100644 --- a/src/store/flowContext.ts +++ b/src/store/flowContext.ts @@ -72,6 +72,7 @@ export enum AssetType { Label = 'label', Language = 'language', NameMatch = 'name_match', + OptIn = 'optin', Remove = 'remove', Resthook = 'resthook', Result = 'result',