From 75b793dede013d7e01b1544f4ca45e374b920907 Mon Sep 17 00:00:00 2001 From: Bilge Date: Tue, 6 Sep 2022 14:20:24 +0200 Subject: [PATCH 01/74] Bump version to 0.50.0-alpha --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 5c4503b704..1a162e7c05 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.49.0 +0.50.0-alpha From ef9c6992128f35ba0d95baf3b2b6a4be8167da54 Mon Sep 17 00:00:00 2001 From: Audrey Kadjar Date: Wed, 7 Sep 2022 10:42:25 +0200 Subject: [PATCH 02/74] [#3669] remove component naming types in the control center (#3676) * first clean-up - wip * wip * still wip: created useselector and use it in connectorconfig * fixes * removed both types * fix typing in inbox * added check * last fixes --- .../src/components/Sidebar/index.tsx | 25 +++--- .../src/components/TopBar/index.tsx | 4 +- .../Catalog/CatalogItemDetails/index.tsx | 2 +- .../Connectors/ConnectorConfig/index.tsx | 87 ++++++++----------- .../Dialogflow/DialogflowConnect.tsx | 15 ++-- .../Connectors/Providers/Rasa/RasaConnect.tsx | 11 +-- .../Salesforce/ConnectNewSalesforce.tsx | 13 ++- .../WhatsappBusinessCloudConnect.tsx | 13 ++- .../Providers/Zendesk/ConnectNewZendesk.tsx | 13 ++- .../src/pages/Connectors/index.tsx | 18 ++-- .../Status/ComponentListItem/ItemInfo.tsx | 4 +- .../control-center/src/pages/Status/index.tsx | 5 +- .../src/selectors/components.ts | 13 +++ .../src/selectors/connectors.ts | 12 +++ .../control-center/src/selectors/index.ts | 3 + .../inbox/src/components/Sidebar/index.tsx | 6 +- lib/typescript/model/Components.ts | 31 ------- lib/typescript/model/Source.ts | 1 + lib/typescript/translations/translations.ts | 4 + 19 files changed, 129 insertions(+), 151 deletions(-) create mode 100644 frontend/control-center/src/selectors/components.ts create mode 100644 frontend/control-center/src/selectors/connectors.ts create mode 100644 frontend/control-center/src/selectors/index.ts diff --git a/frontend/control-center/src/components/Sidebar/index.tsx b/frontend/control-center/src/components/Sidebar/index.tsx index bf6c02ff4b..c8949b400b 100644 --- a/frontend/control-center/src/components/Sidebar/index.tsx +++ b/frontend/control-center/src/components/Sidebar/index.tsx @@ -1,39 +1,40 @@ import React from 'react'; import {Link} from 'react-router-dom'; import {useMatch} from 'react-router'; - +import {connect, ConnectedProps} from 'react-redux'; +import {StateModel} from '../../reducers'; +import {useCurrentComponentForSource} from '../../selectors'; +import {Source} from 'model'; import {CATALOG_ROUTE, CONNECTORS_ROUTE, INBOX_ROUTE, STATUS_ROUTE, WEBHOOKS_ROUTE} from '../../routes/routes'; + import {ReactComponent as ConnectorsIcon} from 'assets/images/icons/gitMerge.svg'; import {ReactComponent as CatalogIcon} from 'assets/images/icons/catalogIcon.svg'; import {ReactComponent as WebhooksIcon} from 'assets/images/icons/webhooksIcon.svg'; import {ReactComponent as StatusIcon} from 'assets/images/icons/statusIcon.svg'; import {ReactComponent as InboxIcon} from 'assets/images/icons/inboxIcon.svg'; - import styles from './index.module.scss'; -import {StateModel} from '../../reducers'; -import {connect, ConnectedProps} from 'react-redux'; -import {ComponentName, ComponentRepository} from 'model'; type SideBarProps = {} & ConnectedProps; const mapStateToProps = (state: StateModel) => ({ version: state.data.config.clusterVersion, components: state.data.config.components, - catalog: state.data.catalog, }); const connector = connect(mapStateToProps); const Sidebar = (props: SideBarProps) => { + const {version, components} = props; + const componentInfo = useCurrentComponentForSource(Source.webhooks); + + const webhooksEnabled = componentInfo.installed; + const inboxEnabled = components[Source.frontendInbox]?.enabled || false; + const showLine = inboxEnabled || webhooksEnabled; + const isActive = (route: string) => { return useMatch(`${route}/*`); }; - const webhooksEnabled = - props.catalog[`${ComponentRepository.airyCore}/${ComponentName.integrationWebhook}`]?.installed; - const inboxEnabled = props.components[ComponentName.frontendInbox]?.enabled || false; - const showLine = inboxEnabled || webhooksEnabled; - return ( ); }; diff --git a/frontend/control-center/src/components/TopBar/index.tsx b/frontend/control-center/src/components/TopBar/index.tsx index a73a91653c..851674cc9f 100644 --- a/frontend/control-center/src/components/TopBar/index.tsx +++ b/frontend/control-center/src/components/TopBar/index.tsx @@ -17,7 +17,7 @@ import {env} from '../../env'; import {useAnimation} from 'render'; import {useTranslation} from 'react-i18next'; import i18next from 'i18next'; -import {Language, ComponentName} from 'model'; +import {Language, Source} from 'model'; interface TopBarProps { isAdmin: boolean; @@ -44,7 +44,7 @@ const TopBar = (props: TopBarProps & ConnectedProps) => { const [chevronLanguageAnim, setChevronLanguageAnim] = useState(false); const [currentLanguage, setCurrentLanguage] = useState(localStorage.getItem('language') || Language.english); const {t} = useTranslation(); - const inboxEnabled = props.components[ComponentName.frontendInbox]?.enabled || false; + const inboxEnabled = props.components[Source.frontendInbox]?.enabled || false; useLayoutEffect(() => { handleLanguage(localStorage.getItem('language')); diff --git a/frontend/control-center/src/pages/Catalog/CatalogItemDetails/index.tsx b/frontend/control-center/src/pages/Catalog/CatalogItemDetails/index.tsx index 9a8c95f550..cac673d9c1 100644 --- a/frontend/control-center/src/pages/Catalog/CatalogItemDetails/index.tsx +++ b/frontend/control-center/src/pages/Catalog/CatalogItemDetails/index.tsx @@ -36,7 +36,6 @@ const CatalogItemDetails = (props: ConnectedProps) => { const locationState = location.state as LocationState; const {componentInfo} = locationState; - const isInstalled = component[componentInfo?.name]?.installed; const [isModalVisible, setIsModalVisible] = useState(false); const [modal, setModal] = useState(null); const [isPending, setIsPending] = useState(false); @@ -45,6 +44,7 @@ const CatalogItemDetails = (props: ConnectedProps) => { const {t} = useTranslation(); const navigate = useNavigate(); const NEW_COMPONENT_INSTALL_ROUTE = getNewChannelRouteForComponent(componentInfo.source); + const isInstalled = component[componentInfo?.name]?.installed; const uninstallText = t('uninstall') + ` ${componentInfo.displayName}`; const installText = `${componentInfo.displayName} ` + t('installed'); diff --git a/frontend/control-center/src/pages/Connectors/ConnectorConfig/index.tsx b/frontend/control-center/src/pages/Connectors/ConnectorConfig/index.tsx index 0871ddc771..dcd2680467 100644 --- a/frontend/control-center/src/pages/Connectors/ConnectorConfig/index.tsx +++ b/frontend/control-center/src/pages/Connectors/ConnectorConfig/index.tsx @@ -3,6 +3,7 @@ import {connect, ConnectedProps, useSelector} from 'react-redux'; import {useTranslation} from 'react-i18next'; import {useParams} from 'react-router-dom'; import {StateModel} from '../../../reducers'; +import {useCurrentComponentForSource} from '../../../selectors'; import { connectChatPlugin, updateChannel, @@ -13,7 +14,7 @@ import { listComponents, } from '../../../actions'; import {UpdateComponentConfigurationRequestPayload} from 'httpclient/src'; -import {Source, ComponentInfo} from 'model'; +import {Source} from 'model'; import ChatPluginConnect from '../Providers/Airy/ChatPlugin/ChatPluginConnect'; import FacebookConnect from '../Providers/Facebook/Messenger/FacebookConnect'; @@ -57,8 +58,14 @@ const connector = connect(mapStateToProps, mapDispatchToProps); const ConnectorConfig = (props: ConnectedProps) => { const {components, catalog, updateConnectorConfiguration, getConnectorsConfiguration, listComponents} = props; + const params = useParams(); + const {channelId, source} = params; + const newChannel = params['*'] === 'new'; + const connectedParams = params['*'] === 'connected'; + const connectors = useSelector((state: StateModel) => state.data.connector); - const [connectorInfo, setConnectorInfo] = useState(null); + const connectorInfo = useCurrentComponentForSource(source as Source); + const [currentPage] = useState(Pages.createUpdate); const [isEnabled, setIsEnabled] = useState(null); const [isPending, setIsPending] = useState(false); @@ -70,11 +77,6 @@ const ConnectorConfig = (props: ConnectedProps) => { const {t} = useTranslation(); - const params = useParams(); - const {channelId, source} = params; - const newChannel = params['*'] === 'new'; - const connectedParams = params['*'] === 'connected'; - const isAiryInternalConnector = source === Source.chatPlugin; const isCatalogList = Object.entries(catalog).length > 0; @@ -83,41 +85,31 @@ const ConnectorConfig = (props: ConnectedProps) => { listComponents().catch((error: Error) => { console.error(error); }); + getConnectorsConfiguration().catch((error: Error) => { + console.error(error); + }); }, []); useEffect(() => { - if (connectorInfo && connectors) { - if ( - connectors[removePrefix(connectorInfo.name)] && - Object.keys(connectors[removePrefix(connectorInfo.name)]).length > 0 - ) { + if (connectorInfo) determineLineTitle(connectorInfo.isChannel); + }, [connectorInfo]); + + useEffect(() => { + if (connectors && connectorInfo && connectorInfo?.name) { + const connectorName = removePrefix(connectorInfo.name); + if (connectors[connectorName] && Object.keys(connectors[connectorName]).length > 0) { setIsConfigured(true); } } }, [connectorInfo, connectors]); useEffect(() => { - getConnectorsConfiguration().catch((error: Error) => { - console.error(error); - }); - - if (isCatalogList) { - isAiryInternalConnector && setIsConfigured(true); - - const connectorSourceInfo = Object.entries(catalog).filter(item => item[1].source === source); - - const connectorSourceInfoArr: [string, ComponentInfo] = connectorSourceInfo[0]; - const connectorSourceInfoFormatted = {name: connectorSourceInfoArr[0], ...connectorSourceInfoArr[1]}; - - const connectorHasChannels: undefined | string = connectorSourceInfoFormatted?.isChannel; - - determineLineTitle(connectorHasChannels); - setConnectorInfo(connectorSourceInfoFormatted); - } - }, [source, isCatalogList, params]); + if (isCatalogList) isAiryInternalConnector && setIsConfigured(true); + }, [isCatalogList]); useEffect(() => { - if (components && connectorInfo) setIsEnabled(components[removePrefix(connectorInfo.name)]?.enabled); + if (components && connectorInfo && connectorInfo?.name) + setIsEnabled(components[removePrefix(connectorInfo.name)]?.enabled); }, [connectorInfo, components]); const determineLineTitle = (connectorHasChannels: undefined | string) => { @@ -221,28 +213,19 @@ const ConnectorConfig = (props: ConnectedProps) => { ); } - if (source === Source.chatPlugin) { - return ; - } - if (source === Source.facebook) { - return ; - } - if (source === Source.instagram) { - return ; - } - if (source === Source.google) { - return ; - } - if (source === Source.twilioSMS) { - return ; - } - if (source === Source.twilioWhatsApp) { - return ; - } + if (source === Source.chatPlugin) return ; - if (source === Source.viber) { - return

configuration page under construction - coming soon!

; - } + if (source === Source.facebook) return ; + + if (source === Source.instagram) return ; + + if (source === Source.google) return ; + + if (source === Source.twilioSMS) return ; + + if (source === Source.twilioWhatsApp) return ; + + if (source === Source.viber) return

{t('pageUnderConstruction')}

; } return ; diff --git a/frontend/control-center/src/pages/Connectors/Providers/Dialogflow/DialogflowConnect.tsx b/frontend/control-center/src/pages/Connectors/Providers/Dialogflow/DialogflowConnect.tsx index fcd39c800d..f65f3c9602 100644 --- a/frontend/control-center/src/pages/Connectors/Providers/Dialogflow/DialogflowConnect.tsx +++ b/frontend/control-center/src/pages/Connectors/Providers/Dialogflow/DialogflowConnect.tsx @@ -1,11 +1,10 @@ import React, {useState} from 'react'; -import {useSelector} from 'react-redux'; -import {StateModel} from '../../../../reducers'; +import {useCurrentConnectorForSource, useCurrentComponentForSource} from '../../../../selectors'; import {Input} from 'components'; import {ConfigureConnector} from '../../ConfigureConnector'; import styles from './index.module.scss'; import {useTranslation} from 'react-i18next'; -import {ComponentName} from 'model'; +import {Source} from 'model'; interface ConnectParams { [key: string]: string; @@ -16,7 +15,6 @@ type DialogflowConnectProps = { isEnabled: boolean; isConfigured: boolean; isPending: boolean; - componentName?: string; }; export const DialogflowConnect = ({ @@ -25,9 +23,8 @@ export const DialogflowConnect = ({ isConfigured, isPending, }: DialogflowConnectProps) => { - const componentInfo = useSelector( - (state: StateModel) => state.data.connector[ComponentName.enterpriseDialogflowConnector] - ); + const componentInfo = useCurrentConnectorForSource(Source.dialogflow); + const componentName = useCurrentComponentForSource(Source.dialogflow)?.name; const [projectId, setProjectID] = useState(componentInfo?.projectId); const [dialogflowCredentials, setDialogflowCredentials] = useState(componentInfo?.dialogflowCredentials || ''); @@ -35,7 +32,6 @@ export const DialogflowConnect = ({ componentInfo?.suggestionConfidenceLevel || '' ); const [replyConfidenceLevel, setReplyConfidenceLevel] = useState(componentInfo?.replyConfidenceLevel || ''); - const [isUpdateModalVisible, setIsUpdateModalVisible] = useState(false); const [processorWaitingTime, setProcessorWaitingTime] = useState( componentInfo?.connectorStoreMessagesProcessorMaxWaitMillis || '5000' ); @@ -43,6 +39,7 @@ export const DialogflowConnect = ({ componentInfo?.connectorStoreMessagesProcessorCheckPeriodMillis || '2500' ); const [defaultLanguage, setDefaultLanguage] = useState(componentInfo?.connectorDefaultLanguage || 'en'); + const [isUpdateModalVisible, setIsUpdateModalVisible] = useState(false); const {t} = useTranslation(); @@ -71,7 +68,7 @@ export const DialogflowConnect = ({ return ( { - const componentInfo = useSelector((state: StateModel) => state.data.connector[ComponentName.rasaConnector]); + const componentInfo = useCurrentConnectorForSource(Source.rasa); + const componentName = useCurrentComponentForSource(Source.rasa)?.name; + const [webhookUrl, setWebhookUrl] = useState(componentInfo?.webhookUrl || ''); const [apiHost, setApiHost] = useState(componentInfo?.apiHost || ''); const [token, setToken] = useState(componentInfo?.token || ''); @@ -42,7 +43,7 @@ export const RasaConnect = ({createNewConnection, isEnabled, isConfigured, isPen return ( { - const componentInfo = useSelector( - (state: StateModel) => state.data.connector[ComponentName.enterpriseSalesforceContactsIngestion] - ); + const componentInfo = useCurrentConnectorForSource(Source.salesforce); + const componentName = useCurrentComponentForSource(Source.salesforce)?.name; + const [url, setUrl] = useState(componentInfo?.url || ''); const [username, setUsername] = useState(componentInfo?.username || ''); const [password, setPassword] = useState(componentInfo?.password || ''); @@ -50,7 +49,7 @@ export const ConnectNewSalesforce = ({ return ( { - const componentInfo = useSelector( - (state: StateModel) => state.data.connector[ConnectorName.sourcesWhatsappBusinessCloud] - ); + const componentInfo = useCurrentConnectorForSource(Source.whatsapp); + const componentName = useCurrentComponentForSource(Source.whatsapp)?.name; + const [appId, setAppId] = useState(componentInfo?.appId || ''); const [appSecret, setAppSecret] = useState(componentInfo?.appSecret || ''); const [phoneNumber, setPhoneNumber] = useState(componentInfo?.phoneNumber || ''); @@ -50,7 +49,7 @@ export const WhatsappBusinessCloudConnect = ({ return ( { - const componentInfo = useSelector( - (state: StateModel) => state.data.connector[ComponentName.enterpriseZendenkConnector] - ); + const componentInfo = useCurrentConnectorForSource(Source.zendesk); + const componentName = useCurrentComponentForSource(Source.zendesk)?.name; + const [domain, setDomain] = useState(componentInfo?.domain || ''); const [username, setUsername] = useState(componentInfo?.username || ''); const [token, setToken] = useState(componentInfo?.token || ''); @@ -48,7 +47,7 @@ export const ConnectNewZendesk = ({ return ( ) => { const catalogListArr = Object.entries(catalogList); const emptyCatalogList = catalogListArr.length === 0; + useEffect(() => { + listChannels().catch((error: Error) => { + console.error(error); + }); + setPageTitle(pageTitle); + }, []); + useEffect(() => { getConnectorsConfiguration(); if (emptyCatalogList) { @@ -58,7 +65,7 @@ const Connectors = (props: ConnectedProps) => { } else { const listArr = []; catalogListArr.map(component => { - if (component[1].installed === true && component[1].source !== 'webhooks') { + if (component[1]?.name && component[1].installed === true && component[1].source !== 'webhooks') { setHasInstalledComponents(true); listArr.push({ name: component[1].name, @@ -74,15 +81,6 @@ const Connectors = (props: ConnectedProps) => { } }, [catalogList]); - useEffect(() => { - if (channels.length === 0) { - listChannels().catch((error: Error) => { - console.error(error); - }); - } - setPageTitle(pageTitle); - }, [channels.length]); - return (
diff --git a/frontend/control-center/src/pages/Status/ComponentListItem/ItemInfo.tsx b/frontend/control-center/src/pages/Status/ComponentListItem/ItemInfo.tsx index c115d88f20..f775fdf2ad 100644 --- a/frontend/control-center/src/pages/Status/ComponentListItem/ItemInfo.tsx +++ b/frontend/control-center/src/pages/Status/ComponentListItem/ItemInfo.tsx @@ -5,7 +5,7 @@ import {ReactComponent as CheckmarkIcon} from 'assets/images/icons/checkmarkFill import {ReactComponent as UncheckedIcon} from 'assets/images/icons/uncheckIcon.svg'; import {ReactComponent as ArrowRight} from 'assets/images/icons/arrowRight.svg'; import {getChannelAvatar} from '../../../components/ChannelAvatar'; -import {ComponentName, Source} from 'model'; +import {Source} from 'model'; import {SettingsModal, Button, Toggle, Tooltip} from 'components'; import {connect, ConnectedProps, useSelector} from 'react-redux'; import {useTranslation} from 'react-i18next'; @@ -60,7 +60,7 @@ const ItemInfo = (props: ComponentInfoProps) => { healthy && isConfigurableConnector() && !isComponentConfigured && - itemName !== ComponentName.sourcesChatPlugin; + !itemName.includes(Source.chatPlugin); const isRunning = healthy && enabled; const isNotHealthy = !healthy && enabled; const isDisabled = !enabled; diff --git a/frontend/control-center/src/pages/Status/index.tsx b/frontend/control-center/src/pages/Status/index.tsx index 179115e26a..a12910cd4f 100644 --- a/frontend/control-center/src/pages/Status/index.tsx +++ b/frontend/control-center/src/pages/Status/index.tsx @@ -6,7 +6,6 @@ import {ComponentListItem} from './ComponentListItem'; import {ReactComponent as RefreshIcon} from 'assets/images/icons/refreshIcon.svg'; import {setPageTitle} from '../../services/pageTitle'; import {useTranslation} from 'react-i18next'; -import {ComponentRepository} from 'model'; import styles from './index.module.scss'; const mapDispatchToProps = { @@ -57,9 +56,9 @@ const Status = (props: ConnectedProps) => { const formatToComponentName = (name: string) => { let formattedName; if (name.includes('enterprise')) { - formattedName = `${ComponentRepository.airyEnterprise}/${name}`; + formattedName = `'airy-enterprise'/${name}`; } else { - formattedName = `${ComponentRepository.airyCore}/${name}`; + formattedName = `airy-core/${name}`; } return formattedName; diff --git a/frontend/control-center/src/selectors/components.ts b/frontend/control-center/src/selectors/components.ts new file mode 100644 index 0000000000..5acb7dce7b --- /dev/null +++ b/frontend/control-center/src/selectors/components.ts @@ -0,0 +1,13 @@ +import {StateModel} from '../reducers'; +import {useSelector} from 'react-redux'; +import {Source, ComponentInfo} from 'model'; + +export const useCurrentComponentForSource = (source: Source) => { + const catalog = useSelector((state: StateModel) => state.data.catalog); + const componentInfoArr: [string, ComponentInfo][] = Object.entries(catalog).filter(item => item[1].source === source); + + const componentInfoArrFlat = componentInfoArr.flat() as [string, ComponentInfo]; + const [name] = componentInfoArrFlat; + + return {name: name, ...componentInfoArrFlat[1]}; +}; diff --git a/frontend/control-center/src/selectors/connectors.ts b/frontend/control-center/src/selectors/connectors.ts new file mode 100644 index 0000000000..f0078b5956 --- /dev/null +++ b/frontend/control-center/src/selectors/connectors.ts @@ -0,0 +1,12 @@ +import {StateModel} from '../reducers'; +import {useSelector} from 'react-redux'; +import {Source} from 'model'; + +export const useCurrentConnectorForSource = (source: Source) => { + const connectors = useSelector((state: StateModel) => state.data.connector); + const connectorInfoArr = Object.entries(connectors).filter(item => item[0].includes(source)); + + const connectorInfoArrFlat = connectorInfoArr.flat() as [string, {[key: string]: string}]; + + return {...connectorInfoArrFlat[1]}; +}; diff --git a/frontend/control-center/src/selectors/index.ts b/frontend/control-center/src/selectors/index.ts new file mode 100644 index 0000000000..4611d61124 --- /dev/null +++ b/frontend/control-center/src/selectors/index.ts @@ -0,0 +1,3 @@ +export * from './channels'; +export * from './components'; +export * from './connectors'; diff --git a/frontend/inbox/src/components/Sidebar/index.tsx b/frontend/inbox/src/components/Sidebar/index.tsx index 5515356b8d..4b0f2fb8b6 100644 --- a/frontend/inbox/src/components/Sidebar/index.tsx +++ b/frontend/inbox/src/components/Sidebar/index.tsx @@ -10,7 +10,6 @@ import {CONTACTS_ROUTE, INBOX_ROUTE, TAGS_ROUTE} from '../../routes/routes'; import styles from './index.module.scss'; import {connect, ConnectedProps} from 'react-redux'; import {StateModel} from '../../reducers'; -import {ComponentName} from 'model'; type SideBarProps = {} & ConnectedProps; @@ -20,6 +19,7 @@ const mapStateToProps = (state: StateModel) => ({ }); const connector = connect(mapStateToProps); +const apiContactsComponent = 'api-contacts'; const Sidebar = (props: SideBarProps) => { const isActive = (route: string) => { @@ -28,10 +28,10 @@ const Sidebar = (props: SideBarProps) => { useEffect(() => { Object.entries(props.components).length > 0 && - setContactsEnabled(props.components[ComponentName.apiContacts]?.enabled || false); + setContactsEnabled(props.components[apiContactsComponent]?.enabled || false); }, [props.components]); - const [contactsEnabled, setContactsEnabled] = useState(props.components[ComponentName.apiContacts]?.enabled || false); + const [contactsEnabled, setContactsEnabled] = useState(props.components[apiContactsComponent]?.enabled || false); return (
- {componentStatus && } + {componentStatus && ( + + )}
); diff --git a/frontend/control-center/src/pages/Status/ComponentListItem/ItemInfo.tsx b/frontend/control-center/src/pages/Status/ComponentListItem/ItemInfo.tsx index f775fdf2ad..e522f71a08 100644 --- a/frontend/control-center/src/pages/Status/ComponentListItem/ItemInfo.tsx +++ b/frontend/control-center/src/pages/Status/ComponentListItem/ItemInfo.tsx @@ -114,6 +114,7 @@ const ItemInfo = (props: ComponentInfoProps) => { hoverElementHeight={20} hoverElementWidth={20} tooltipContent={t('needsConfiguration')} + direction="right" /> ) : isRunning ? ( { hoverElementHeight={20} hoverElementWidth={20} tooltipContent={t('enabled')} + direction="right" /> ) : isNotHealthy ? ( { hoverElementHeight={20} hoverElementWidth={20} tooltipContent={t('notHealthy')} + direction="right" /> ) : ( isDisabled && ( @@ -136,6 +139,7 @@ const ItemInfo = (props: ComponentInfoProps) => { hoverElementHeight={20} hoverElementWidth={20} tooltipContent={t('disabled')} + direction="right" /> ) )} diff --git a/frontend/control-center/src/services/getComponentStatus.ts b/frontend/control-center/src/services/getComponentStatus.ts index e597b50aa9..f491c2bf8d 100644 --- a/frontend/control-center/src/services/getComponentStatus.ts +++ b/frontend/control-center/src/services/getComponentStatus.ts @@ -7,7 +7,7 @@ export const getComponentStatus = ( isEnabled: boolean ) => { if (isInstalled && !isEnabled) return ComponentStatus.disabled; - if (!isHealthy) return ComponentStatus.notHealthy; if (isInstalled && !isConfigured) return ComponentStatus.notConfigured; + if (!isHealthy) return ComponentStatus.notHealthy; if (isInstalled && isConfigured && isEnabled) return ComponentStatus.enabled; }; diff --git a/lib/typescript/components/tooltip/index.module.scss b/lib/typescript/components/tooltip/index.module.scss index d7f4aba641..c8d6b41f09 100644 --- a/lib/typescript/components/tooltip/index.module.scss +++ b/lib/typescript/components/tooltip/index.module.scss @@ -1,11 +1,32 @@ @import 'assets/scss/fonts.scss'; @import 'assets/scss/colors.scss'; -.tooltipContainer { +.tooltipContainer.top { display: flex; + flex-direction: column-reverse; + align-items: center; + width: 100%; +} + +.tooltipContainer.right { + display: flex; + flex-direction: row; + align-items: center; + width: 100%; +} + +.tooltipContainer.bottom { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; +} + +.tooltipContainer.left { + display: flex; + flex-direction: row-reverse; align-items: center; width: 100%; - // margin-left: 50%; } .tooltipContent { @@ -13,23 +34,52 @@ display: none; background: var(--color-tooltip-gray); border-radius: 4px; - margin-left: 4px; padding: 4px 6px; color: var(--color-text-contrast); max-width: 180px; position: relative; - margin-left: 16px; } -.tooltip { - margin-left: 32px; +.hoverElement:hover ~ .tooltipContent.top { + display: flex; + flex-direction: column; + line-height: 14px; + white-space: nowrap; + margin-bottom: 12px; +} + +.hoverElement:hover ~ .tooltipContent.right { + display: flex; + line-height: 14px; + white-space: nowrap; + margin-left: 12px; +} + +.hoverElement:hover ~ .tooltipContent.bottom { + display: flex; + line-height: 14px; + white-space: nowrap; + margin-top: 12px; } -.hoverElement:hover ~ .tooltipContent { +.hoverElement:hover ~ .tooltipContent.left { display: flex; + line-height: 14px; + white-space: nowrap; + margin-right: 12px; } -.hoverElement ~ .tooltipContent::after { +.hoverElement ~ .tooltipContent.top::after { + content: ''; + position: absolute; + top: 100%; + right: calc(50% - 5px); + border-width: 5px; + border-style: solid; + border-color: var(--color-tooltip-gray) transparent transparent transparent; +} + +.hoverElement ~ .tooltipContent.right::after { content: ''; position: absolute; top: 50%; @@ -39,3 +89,24 @@ border-style: solid; border-color: transparent var(--color-tooltip-gray) transparent transparent; } + +.hoverElement ~ .tooltipContent.bottom::after { + content: ''; + position: absolute; + bottom: 100%; + right: calc(50% - 5px); + border-width: 5px; + border-style: solid; + border-color: transparent transparent var(--color-tooltip-gray) transparent; +} + +.hoverElement ~ .tooltipContent.left::after { + content: ''; + position: absolute; + top: 50%; + left: 100%; + margin-top: -5px; + border-width: 5px; + border-style: solid; + border-color: transparent transparent transparent var(--color-tooltip-gray); +} diff --git a/lib/typescript/components/tooltip/index.tsx b/lib/typescript/components/tooltip/index.tsx index 41079ef5fd..6debc20df6 100644 --- a/lib/typescript/components/tooltip/index.tsx +++ b/lib/typescript/components/tooltip/index.tsx @@ -6,17 +6,21 @@ type TooltipProps = { hoverElementHeight?: number; hoverElementWidth: number; tooltipContent: string; + direction: 'top' | 'right' | 'bottom' | 'left'; }; export const Tooltip = (props: TooltipProps) => { - const {hoverElement, hoverElementHeight, hoverElementWidth, tooltipContent} = props; + const {hoverElement, hoverElementHeight, hoverElementWidth, tooltipContent, direction} = props; return ( -
+
{hoverElement}
- {tooltipContent} + {tooltipContent}
); }; From 6e687ae3d3047c7bfe3df75f1860f1752765d530 Mon Sep 17 00:00:00 2001 From: Thorsten Date: Thu, 8 Sep 2022 17:24:36 +0200 Subject: [PATCH 08/74] [#3485] Add notify me catalog (#3705) --- .../Catalog/CatalogCard/index.module.scss | 5 + .../src/pages/Catalog/CatalogCard/index.tsx | 49 ++++++++- .../Catalog/CatalogItemDetails/index.tsx | 59 +++++++++-- .../Catalog/NotifyMeModal/index.module.scss | 87 +++++++++++++++ .../src/pages/Catalog/NotifyMeModal/index.tsx | 100 ++++++++++++++++++ lib/typescript/assets/scss/colors.scss | 1 + .../alerts/NotificationComponent/index.tsx | 13 ++- .../components/cta/Button/index.tsx | 11 +- .../components/cta/Button/style.module.scss | 51 +++++++++ .../cta/SmartButton/index.module.scss | 81 ++++++++++++++ .../components/cta/SmartButton/index.tsx | 11 +- .../components/inputs/Input/index.tsx | 4 +- .../components/inputs/Input/style.module.scss | 5 +- lib/typescript/model/Connectors.ts | 6 ++ lib/typescript/translations/translations.ts | 61 +++++++++++ 15 files changed, 528 insertions(+), 16 deletions(-) create mode 100644 frontend/control-center/src/pages/Catalog/NotifyMeModal/index.module.scss create mode 100644 frontend/control-center/src/pages/Catalog/NotifyMeModal/index.tsx diff --git a/frontend/control-center/src/pages/Catalog/CatalogCard/index.module.scss b/frontend/control-center/src/pages/Catalog/CatalogCard/index.module.scss index 3394acc0a2..0dc2a835f3 100644 --- a/frontend/control-center/src/pages/Catalog/CatalogCard/index.module.scss +++ b/frontend/control-center/src/pages/Catalog/CatalogCard/index.module.scss @@ -139,3 +139,8 @@ .headerModal { @include font-xl; } + +.notifyMeButton { + display: flex; + white-space: nowrap; +} diff --git a/frontend/control-center/src/pages/Catalog/CatalogCard/index.tsx b/frontend/control-center/src/pages/Catalog/CatalogCard/index.tsx index 3b76dba6d6..00461aa040 100644 --- a/frontend/control-center/src/pages/Catalog/CatalogCard/index.tsx +++ b/frontend/control-center/src/pages/Catalog/CatalogCard/index.tsx @@ -4,7 +4,7 @@ import {useTranslation} from 'react-i18next'; import {connect, ConnectedProps} from 'react-redux'; import {StateModel} from '../../../reducers'; import {installComponent} from '../../../actions/catalog'; -import {ComponentInfo, NotificationModel} from 'model'; +import {ComponentInfo, ConnectorPrice, NotificationModel} from 'model'; import {Button, NotificationComponent, SettingsModal, SmartButton} from 'components'; import {getChannelAvatar} from '../../../components/ChannelAvatar'; import { @@ -15,6 +15,7 @@ import { import {DescriptionComponent, getDescriptionSourceName} from '../../../components/Description'; import {ReactComponent as CheckmarkIcon} from 'assets/images/icons/checkmarkFilled.svg'; import styles from './index.module.scss'; +import NotifyMeModal from '../NotifyMeModal'; type CatalogCardProps = { componentInfo: ComponentInfo; @@ -36,12 +37,17 @@ const CatalogCard = (props: CatalogCardProps) => { const {component, componentInfo, installComponent} = props; const isInstalled = component[componentInfo?.name]?.installed; const [isModalVisible, setIsModalVisible] = useState(false); + const [isNotifyMeModalVisible, setIsNotifyMeModalVisible] = useState(false); const [isPending, setIsPending] = useState(false); const [notification, setNotification] = useState(null); + const [notifyMeNotification, setNotifyMeNotification] = useState(null); + const [forceClose, setForceClose] = useState(false); + const notified = localStorage.getItem(`notified.${componentInfo.source}`); const installButtonCard = useRef(null); const componentCard = useRef(null); const {t} = useTranslation(); const navigate = useNavigate(); + const notifiedEmail = t('infoNotifyMe') + ` ${notified}`; const isChannel = componentInfo?.isChannel; @@ -76,7 +82,26 @@ const CatalogCard = (props: CatalogCardProps) => { } }; + const handleNotifyMeClick = () => { + setIsNotifyMeModalVisible(true); + notified && setNotification({show: true, text: notifiedEmail, info: true}); + }; + const CatalogCardButton = () => { + if (componentInfo?.price === ConnectorPrice.requestAccess) { + return ( + + ); + } + if (isInstalled) { return ( )} + + {isNotifyMeModalVisible && ( + + )} {notification?.show && ( + )} + {notifyMeNotification?.show && ( + )} diff --git a/frontend/control-center/src/pages/Catalog/CatalogItemDetails/index.tsx b/frontend/control-center/src/pages/Catalog/CatalogItemDetails/index.tsx index cac673d9c1..7787369959 100644 --- a/frontend/control-center/src/pages/Catalog/CatalogItemDetails/index.tsx +++ b/frontend/control-center/src/pages/Catalog/CatalogItemDetails/index.tsx @@ -4,7 +4,7 @@ import {useTranslation} from 'react-i18next'; import {connect, ConnectedProps} from 'react-redux'; import {installComponent, uninstallComponent} from '../../../actions/catalog'; import {StateModel} from '../../../reducers'; -import {ComponentInfo, Modal, ModalType, NotificationModel} from 'model'; +import {ComponentInfo, ConnectorPrice, Modal, ModalType, NotificationModel} from 'model'; import {ContentWrapper, Button, LinkButton, SettingsModal, NotificationComponent, SmartButton} from 'components'; import {getChannelAvatar} from '../../../components/ChannelAvatar'; import {availabilityFormatted} from '../CatalogCard'; @@ -14,6 +14,7 @@ import {getNewChannelRouteForComponent} from '../../../services'; import {ReactComponent as ArrowLeftIcon} from 'assets/images/icons/leftArrowCircle.svg'; import {ReactComponent as CheckmarkIcon} from 'assets/images/icons/checkmarkFilled.svg'; import styles from './index.module.scss'; +import NotifyMeModal from '../NotifyMeModal'; const mapStateToProps = (state: StateModel) => ({ component: state.data.catalog, @@ -37,11 +38,15 @@ const CatalogItemDetails = (props: ConnectedProps) => { const {componentInfo} = locationState; const [isModalVisible, setIsModalVisible] = useState(false); + const [isNotifyMeModalVisible, setIsNotifyMeModalVisible] = useState(false); const [modal, setModal] = useState(null); const [isPending, setIsPending] = useState(false); const [notification, setNotification] = useState(null); - + const [notifyMeNotification, setNotifyMeNotification] = useState(null); + const [forceClose, setForceClose] = useState(false); + const notified = localStorage.getItem(`notified.${componentInfo.source}`); const {t} = useTranslation(); + const notifiedEmail = t('infoNotifyMe') + ` ${notified}`; const navigate = useNavigate(); const NEW_COMPONENT_INSTALL_ROUTE = getNewChannelRouteForComponent(componentInfo.source); const isInstalled = component[componentInfo?.name]?.installed; @@ -89,6 +94,11 @@ const CatalogItemDetails = (props: ConnectedProps) => { }); }; + const handleNotifyMeClick = () => { + setIsNotifyMeModalVisible(true); + notified && setNotification({show: true, text: notifiedEmail, info: true}); + }; + const HeaderContent = () => { return (
@@ -124,12 +134,28 @@ const CatalogItemDetails = (props: ConnectedProps) => {
{getChannelAvatar(componentInfo?.displayName)}
@@ -154,7 +180,7 @@ const CatalogItemDetails = (props: ConnectedProps) => {

{t('price')}:

+ + ); +}; + +export default connector(NotifyMeModal); diff --git a/lib/typescript/assets/scss/colors.scss b/lib/typescript/assets/scss/colors.scss index 1d4e7e3499..1030a9cc52 100644 --- a/lib/typescript/assets/scss/colors.scss +++ b/lib/typescript/assets/scss/colors.scss @@ -41,6 +41,7 @@ --color-red-alert: #d51548; --color-red-alert-pressed: #a2002b; --color-red-info: #ee336c; + --color-notify-me: #a30d8b; --color-accent-magenta: #f7147d; --color-error-background: #fae6e9; --color-highlight-yellow: #fbbd54; diff --git a/lib/typescript/components/alerts/NotificationComponent/index.tsx b/lib/typescript/components/alerts/NotificationComponent/index.tsx index 4791201282..41fdae05d1 100644 --- a/lib/typescript/components/alerts/NotificationComponent/index.tsx +++ b/lib/typescript/components/alerts/NotificationComponent/index.tsx @@ -15,11 +15,13 @@ type NotificationProps = { info?: boolean; text: string; setShowFalse: Dispatch>; + forceClose?: boolean; + setForceClose?: Dispatch>; duration?: number; //in ms }; export const NotificationComponent = (props: NotificationProps) => { - const {type, show, successful, info, text, setShowFalse, duration} = props; + const {type, show, successful, info, text, setShowFalse, forceClose, setForceClose, duration} = props; const defaultDuration = 5000; const [close, setClose] = useState(false); const [usedDuration, setUsedDuration] = useState(duration || defaultDuration); @@ -52,19 +54,20 @@ export const NotificationComponent = (props: NotificationProps) => { }, []); useEffect(() => { - close && + (close || forceClose) && setTimeout(() => { setShowFalse({show: false}); setClose(false); + forceClose && setForceClose(false); }, duration / 2 || defaultDuration / 2); - }, [close]); + }, [close, forceClose]); return (
{ }} >
- + {text} {type === NotificationType.sticky && ( diff --git a/lib/typescript/components/cta/Button/index.tsx b/lib/typescript/components/cta/Button/index.tsx index 9fca9c1c6b..6c9201938a 100644 --- a/lib/typescript/components/cta/Button/index.tsx +++ b/lib/typescript/components/cta/Button/index.tsx @@ -11,7 +11,10 @@ type styleVariantType = | 'warning' | 'link' | 'text' - | 'green'; + | 'green' + | 'greenOutline' + | 'purple' + | 'purpleOutline'; type ButtonProps = { children: ReactNode; @@ -44,6 +47,12 @@ export const Button = ({ return styles.extraSmallButton; case 'green': return styles.greenButton; + case 'greenOutline': + return styles.greenOutlineButton; + case 'purple': + return styles.purpleButton; + case 'purpleOutline': + return styles.purpleOutlineButton; case 'small': return styles.smallButton; case 'outline': diff --git a/lib/typescript/components/cta/Button/style.module.scss b/lib/typescript/components/cta/Button/style.module.scss index c9d5c1f68f..b00fa3a485 100644 --- a/lib/typescript/components/cta/Button/style.module.scss +++ b/lib/typescript/components/cta/Button/style.module.scss @@ -58,6 +58,57 @@ } } +.greenOutlineButton { + @extend .extraSmallButton; + background-color: transparent; + border: 1px solid var(--color-soft-green); + color: var(--color-soft-green); + + &:hover { + background-color: rgba($color: #0da36b, $alpha: 0.2); + border: 1px solid #0da36b; + } + + &:active { + background-color: var(--color-green-pressed); + color: var(--color-background-white); + } +} + +.purpleButton { + @extend .extraSmallButton; + background-color: rgba($color: #a30d8b, $alpha: 0.2); + border: 1px solid var(--color-notify-me); + color: var(--color-notify-me); + + &:hover { + background: var(--color-notify-me); + color: var(--color-background-white); + } + + &:active { + background-color: var(--color-notify-me); + color: var(--color-background-white); + } +} + +.purpleOutlineButton { + @extend .extraSmallButton; + background-color: transparent; + border: 1px solid var(--color-notify-me); + color: var(--color-notify-me); + + &:hover { + background: var(--color-notify-me); + color: var(--color-background-white); + } + + &:active { + background-color: var(--color-notify-me); + color: var(--color-background-white); + } +} + .smallButton { @extend .button; @include font-base; diff --git a/lib/typescript/components/cta/SmartButton/index.module.scss b/lib/typescript/components/cta/SmartButton/index.module.scss index e59ca5e34e..17fa988cb4 100644 --- a/lib/typescript/components/cta/SmartButton/index.module.scss +++ b/lib/typescript/components/cta/SmartButton/index.module.scss @@ -58,6 +58,23 @@ } } +.greenOutlineButton { + @extend .extraSmallButton; + background-color: transparent; + border: 1px solid var(--color-soft-green); + color: var(--color-soft-green); + + &:hover { + background-color: rgba($color: #0da36b, $alpha: 0.2); + border: 1px solid #0da36b; + } + + &:active { + background-color: var(--color-green-pressed); + color: var(--color-background-white); + } +} + .smallButton { @extend .button; @include font-base; @@ -98,6 +115,70 @@ padding: 3px 16px; } +.greenButton { + @extend .extraSmallButton; + background-color: var(--color-soft-green); + + &:hover { + background-color: var(--color-green-pressed); + } + + &:active { + background-color: var(--color-green-pressed); + } +} + +.greenOutlineButton { + @extend .extraSmallButton; + background-color: transparent; + border: 1px solid var(--color-soft-green); + color: var(--color-soft-green); + + &:hover { + background-color: rgba($color: #0da36b, $alpha: 0.2); + border: 1px solid #0da36b; + } + + &:active { + background-color: var(--color-green-pressed); + color: var(--color-background-white); + } +} + +.purpleButton { + @extend .extraSmallButton; + background-color: rgba($color: #a30d8b, $alpha: 0.2); + border: 1px solid var(--color-notify-me); + color: var(--color-notify-me); + + &:hover { + background: var(--color-notify-me); + color: var(--color-background-white); + } + + &:active { + background-color: var(--color-notify-me); + color: var(--color-background-white); + } +} + +.purpleOutlineButton { + @extend .extraSmallButton; + background-color: transparent; + border: 1px solid var(--color-notify-me); + color: var(--color-notify-me); + + &:hover { + background: var(--color-notify-me); + color: var(--color-background-white); + } + + &:active { + background-color: var(--color-notify-me); + color: var(--color-background-white); + } +} + .linkButton { @include font-base; display: flex; diff --git a/lib/typescript/components/cta/SmartButton/index.tsx b/lib/typescript/components/cta/SmartButton/index.tsx index 17f778443c..85e971b6a5 100644 --- a/lib/typescript/components/cta/SmartButton/index.tsx +++ b/lib/typescript/components/cta/SmartButton/index.tsx @@ -12,7 +12,10 @@ type styleVariantType = | 'warning' | 'link' | 'text' - | 'green'; + | 'green' + | 'greenOutline' + | 'purple' + | 'purpleOutline'; type ButtonProps = { title: string; @@ -58,6 +61,12 @@ export const SmartButton = ({ return styles.extraSmallButton; case 'green': return styles.greenButton; + case 'greenOutline': + return styles.greenOutlineButton; + case 'purple': + return styles.purpleButton; + case 'purpleOutline': + return styles.purpleOutlineButton; case 'small': return styles.smallButton; case 'outline': diff --git a/lib/typescript/components/inputs/Input/index.tsx b/lib/typescript/components/inputs/Input/index.tsx index 34b2890be1..759857697f 100644 --- a/lib/typescript/components/inputs/Input/index.tsx +++ b/lib/typescript/components/inputs/Input/index.tsx @@ -263,6 +263,7 @@ class InputComponent extends Component { label, showLabelIcon, tooltipText, + tooltipStyle, hideLabel, name, value, @@ -314,7 +315,7 @@ class InputComponent extends Component { {showLabelIcon && ( <> - {tooltipText && {tooltipText}} + {tooltipText && {tooltipText}} )}
@@ -423,6 +424,7 @@ export interface InputProps { label?: string; showLabelIcon?: boolean; tooltipText?: string; + tooltipStyle?: string; minWidth?: number; width?: number; diff --git a/lib/typescript/components/inputs/Input/style.module.scss b/lib/typescript/components/inputs/Input/style.module.scss index 030f41a7dc..86712ecb45 100644 --- a/lib/typescript/components/inputs/Input/style.module.scss +++ b/lib/typescript/components/inputs/Input/style.module.scss @@ -222,12 +222,14 @@ .infoCircleText { @include font-s-bold; + line-height: 14px; display: none; background: var(--color-tooltip-gray); border-radius: 4px; margin-left: 4px; padding: 4px 6px; color: var(--color-text-contrast); + border-color: transparent var(--color-tooltip-gray) transparent transparent; } .infoCircle:hover ~ .infoCircleText { @@ -242,7 +244,8 @@ top: 50%; right: 100%; margin-top: -5px; + margin-right: inherit; border-width: 5px; border-style: solid; - border-color: transparent var(--color-tooltip-gray) transparent transparent; + border-color: inherit; } diff --git a/lib/typescript/model/Connectors.ts b/lib/typescript/model/Connectors.ts index 27ee0cc017..94072e106f 100644 --- a/lib/typescript/model/Connectors.ts +++ b/lib/typescript/model/Connectors.ts @@ -22,3 +22,9 @@ export enum ConnectorName { rasaConnector = 'rasa-connector', zendenkConnector = 'zendesk-connector', } + +export enum ConnectorPrice { + free = 'Free', + paid = 'Paid', + requestAccess = 'REQUEST ACCESS', +} diff --git a/lib/typescript/translations/translations.ts b/lib/typescript/translations/translations.ts index 5b0188c9c5..7b137dc107 100644 --- a/lib/typescript/translations/translations.ts +++ b/lib/typescript/translations/translations.ts @@ -425,6 +425,21 @@ const resources = { optional: 'Optional', configuration: 'Configuration', + //Request Access + notifyMe: 'Notify Me', + notifyMeRequestSent: 'Requested', + infoNotifyMe: 'We will already send a notification to', + notifyMeTitle: 'To get back to you', + notifyMeSuccessful: 'We got your request. We will get back to you as soon as we process your request', + emailCapital: 'Email', + addEmail: 'Add email', + notifyMeEmailTooltip: 'The email will be used as the request email', + addName: 'Add name', + notifyMeNameTooltip: 'The name will be used as the request name', + addMessage: 'Add message', + message: 'Message', + send: 'Send', + //Rasa rasaWebhookPlaceholder: 'Your Rasa Webhook Url', rasaWebhookTooltip: 'Example: http://webhooks.rasa', @@ -912,6 +927,22 @@ const resources = { optional: 'Optional', configuration: 'Konfiguration', + //Request Access + + notifyMe: 'Informier mich', + notifyMeRequestSent: 'Angefordert', + infoNotifyMe: 'Wir senden bereits eine Benachrichtigung an', + notifyMeTitle: 'Um auf Sie zurück zu kommen', + notifyMeSuccessful: + 'Wir haben Ihre Anfrage erhalten. Wir werden uns mit Ihnen in Verbindung setzen, sobald wir Ihre Anfrage bearbeitet haben', + addEmail: 'E-Mail hinzufügen', + notifyMeEmailTooltip: 'Die E-Mail wird als Anfrage-E-Mail verwendet.', + addName: 'Name hinzufügen', + notifyMeNameTooltip: 'Der Name wird als Name der Anfrage verwendet.', + addMessage: 'Nachricht hinzufügen', + message: 'Nachricht', + send: 'Senden', + //Rasa rasaWebhookPlaceholder: 'Ihre Rasa Webhook Url', rasaWebhookTooltip: 'Beispiel: http://webhooks.rasa', @@ -1391,6 +1422,21 @@ const resources = { optional: 'Optionnel', configuration: 'Configuration', + //Request Access + notifyMe: 'Notifiez-moi', + notifyMeRequestSent: 'Demandé', + infoNotifyMe: 'Nous allons déjà envoyer une notification à', + notifyMeTitle: 'Pour vous recontacter', + notifyMeSuccessful: + 'Nous avons reçu votre demande. Nous vous contacterons dès que nous aurons traité votre demande', + addEmail: 'Ajouter un e-mail', + notifyMeEmailTooltip: `L'email sera utilisé comme email de demande`, + addName: 'Ajouter un nom', + notifyMeNameTooltip: 'Le nom sera utilisé comme nom de la demande', + addMessage: 'Ajouter un message', + message: 'Message', + send: 'Envoyer', + //Rasa rasaWebhookPlaceholder: 'URL Webhook de Rasa', rasaWebhookTooltip: 'Exemple : http://webhooks.rasa', @@ -1874,6 +1920,21 @@ const resources = { optional: 'Opcional', configuration: 'Configuración', + //Request Access + notifyMe: 'Notificarme', + notifyMeRequestSent: 'Solicitado', + infoNotifyMe: 'Ya enviaremos una notificación a', + notifyMeTitle: 'Para volver a llamarte', + notifyMeSuccessful: + 'Hemos recibido su solicitud. Nos pondremos en contacto con usted en cuanto procesemos su solicitud', + addEmail: 'Añadir correo electrónico', + notifyMeEmailTooltip: 'En este correo electrónico se notificará su solicitud', + addName: 'Añadir nombre', + notifyMeNameTooltip: 'El nombre se utilizará como nombre de la solicitud', + addMessage: 'Añadir mensaje', + message: 'Mensaje', + send: 'Enviar', + //Rasa rasaWebhookPlaceholder: 'Su Url de Rasa Webhook', rasaWebhookTooltip: 'Ejemplo: http://webhooks.rasa', From ccfb4c6a4ede56608dc7407fd194df23fe451163 Mon Sep 17 00:00:00 2001 From: Thorsten Date: Fri, 9 Sep 2022 13:43:37 +0200 Subject: [PATCH 09/74] [#3485] Enhanced Notify Me UI (#3706) --- .../Catalog/CatalogCard/index.module.scss | 19 +++++++++++++++++++ .../src/pages/Catalog/CatalogCard/index.tsx | 16 +++++++++++----- .../CatalogItemDetails/index.module.scss | 19 +++++++++++++++++++ .../Catalog/CatalogItemDetails/index.tsx | 7 ++++++- lib/typescript/assets/scss/fonts.scss | 7 +++++++ 5 files changed, 62 insertions(+), 6 deletions(-) diff --git a/frontend/control-center/src/pages/Catalog/CatalogCard/index.module.scss b/frontend/control-center/src/pages/Catalog/CatalogCard/index.module.scss index 0dc2a835f3..171e1dc3d4 100644 --- a/frontend/control-center/src/pages/Catalog/CatalogCard/index.module.scss +++ b/frontend/control-center/src/pages/Catalog/CatalogCard/index.module.scss @@ -144,3 +144,22 @@ display: flex; white-space: nowrap; } + +.availableForSoonContainer { + display: flex; + align-items: center; + justify-content: space-between; +} + +.soonTag { + @include font-xs-bold; + display: flex; + align-items: center; + justify-content: center; + margin-top: 8px; + height: 22px; + width: 50px; + border-radius: 5px; + background: var(--color-notify-me); + color: var(--color-background-white); +} diff --git a/frontend/control-center/src/pages/Catalog/CatalogCard/index.tsx b/frontend/control-center/src/pages/Catalog/CatalogCard/index.tsx index 00461aa040..5089af5501 100644 --- a/frontend/control-center/src/pages/Catalog/CatalogCard/index.tsx +++ b/frontend/control-center/src/pages/Catalog/CatalogCard/index.tsx @@ -141,7 +141,6 @@ const CatalogCard = (props: CatalogCardProps) => {

{componentInfo.displayName}

-

{' '} {t('categories')}: {componentInfo.category}{' '} @@ -160,10 +159,17 @@ const CatalogCard = (props: CatalogCardProps) => { {t('availableFor')}:

- {componentInfo?.availableFor && - availabilityFormatted(componentInfo.availableFor).map((service: string) => ( - - ))} +
+
+ {componentInfo?.availableFor && + availabilityFormatted(componentInfo.availableFor).map((service: string) => ( + + ))} +
+ {componentInfo?.price === ConnectorPrice.requestAccess && ( +
{t('soon').toUpperCase()}
+ )} +
{isModalVisible && ( diff --git a/frontend/control-center/src/pages/Catalog/CatalogItemDetails/index.module.scss b/frontend/control-center/src/pages/Catalog/CatalogItemDetails/index.module.scss index e22879de72..e3e9417cfe 100644 --- a/frontend/control-center/src/pages/Catalog/CatalogItemDetails/index.module.scss +++ b/frontend/control-center/src/pages/Catalog/CatalogItemDetails/index.module.scss @@ -187,3 +187,22 @@ .headerModal { @include font-xl; } + +.availabilitySoonContainer { + display: flex; + align-items: center; +} + +.soonTag { + @include font-xs-bold; + display: flex; + align-items: center; + justify-content: center; + margin-top: 20px; + margin-left: 4px; + height: 22px; + width: 50px; + border-radius: 5px; + background: var(--color-notify-me); + color: var(--color-background-white); +} diff --git a/frontend/control-center/src/pages/Catalog/CatalogItemDetails/index.tsx b/frontend/control-center/src/pages/Catalog/CatalogItemDetails/index.tsx index 7787369959..45d314bbbd 100644 --- a/frontend/control-center/src/pages/Catalog/CatalogItemDetails/index.tsx +++ b/frontend/control-center/src/pages/Catalog/CatalogItemDetails/index.tsx @@ -162,7 +162,12 @@ const CatalogItemDetails = (props: ConnectedProps) => {
-

{t('availableFor')}:

+
+

{t('availableFor')}:

+ {componentInfo?.price === ConnectorPrice.requestAccess && ( +
{t('soon').toUpperCase()}
+ )} +
{componentInfo?.availableFor && availabilityFormatted(componentInfo.availableFor).map((service: string) => ( diff --git a/lib/typescript/assets/scss/fonts.scss b/lib/typescript/assets/scss/fonts.scss index 69a1c8c0c9..cae1018c75 100644 --- a/lib/typescript/assets/scss/fonts.scss +++ b/lib/typescript/assets/scss/fonts.scss @@ -8,6 +8,13 @@ line-height: 13.2px; } +@mixin font-xs-bold { + font-family: 'Lato', sans-serif; + font-size: 11px; + line-height: 13.2px; + font-weight: bold; +} + @mixin font-s { font-family: 'Lato', sans-serif; font-size: 13px; From e1307bdc710968b4ecdec988ed86b6decf94d5da Mon Sep 17 00:00:00 2001 From: Thorsten Date: Fri, 9 Sep 2022 16:31:57 +0200 Subject: [PATCH 10/74] [#3667] Added IBM and Amazon S3 to catalog (#3707) --- .../src/components/ChannelAvatar/index.tsx | 11 ++++ .../Catalog/CatalogCard/index.module.scss | 19 +++++- .../src/pages/Catalog/CatalogCard/index.tsx | 45 ++++++++++---- .../CatalogItemDetails/index.module.scss | 15 +++++ .../Catalog/CatalogItemDetails/index.tsx | 59 +++++++++++-------- .../assets/images/icons/amazons3Logo.svg | 1 + .../images/icons/ibmWatsonAssistantLogo.svg | 1 + lib/typescript/model/Source.ts | 2 + lib/typescript/translations/translations.ts | 28 +++++++++ 9 files changed, 141 insertions(+), 40 deletions(-) create mode 100644 lib/typescript/assets/images/icons/amazons3Logo.svg create mode 100644 lib/typescript/assets/images/icons/ibmWatsonAssistantLogo.svg diff --git a/frontend/control-center/src/components/ChannelAvatar/index.tsx b/frontend/control-center/src/components/ChannelAvatar/index.tsx index 21e7371128..4da1eda0da 100644 --- a/frontend/control-center/src/components/ChannelAvatar/index.tsx +++ b/frontend/control-center/src/components/ChannelAvatar/index.tsx @@ -13,6 +13,8 @@ import {ReactComponent as SalesforceAvatar} from 'assets/images/icons/salesforce import {ReactComponent as CongnigyAvatar} from 'assets/images/icons/congnigyLogo.svg'; import {ReactComponent as RasaAvatar} from 'assets/images/icons/rasaLogo.svg'; import {ReactComponent as AmeliaAvatar} from 'assets/images/icons/ameliaLogo.svg'; +import {ReactComponent as AmazonS3Avatar} from 'assets/images/icons/amazons3Logo.svg'; +import {ReactComponent as IbmWatsonAssistantAvatar} from 'assets/images/icons/ibmWatsonAssistantLogo.svg'; import {Channel, Source} from 'model'; import styles from './index.module.scss'; @@ -65,12 +67,21 @@ export const getChannelAvatar = (source: string) => { case Source.salesforce: case 'Salesforce': return ; + case Source.congnigy: case 'Congnigy': return ; + case Source.rasa: case 'Rasa': return ; + case Source.amelia: case 'Amelia': return ; + case Source.amazons3: + case 'Amazon S3': + return ; + case Source.ibm: + case 'IBM Watson Assistant': + return ; default: return ; } diff --git a/frontend/control-center/src/pages/Catalog/CatalogCard/index.module.scss b/frontend/control-center/src/pages/Catalog/CatalogCard/index.module.scss index 171e1dc3d4..16347deeb9 100644 --- a/frontend/control-center/src/pages/Catalog/CatalogCard/index.module.scss +++ b/frontend/control-center/src/pages/Catalog/CatalogCard/index.module.scss @@ -152,7 +152,7 @@ } .soonTag { - @include font-xs-bold; + @include font-xs; display: flex; align-items: center; justify-content: center; @@ -163,3 +163,20 @@ background: var(--color-notify-me); color: var(--color-background-white); } + +.comingSoonTag { + @include font-xs; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + height: 24px; + width: 104px; + color: white; + background: var(--color-notify-me); + margin-top: 14px; + white-space: nowrap; + &:hover { + background: var(--color-notify-me); + } +} diff --git a/frontend/control-center/src/pages/Catalog/CatalogCard/index.tsx b/frontend/control-center/src/pages/Catalog/CatalogCard/index.tsx index 5089af5501..aa9d6fdf40 100644 --- a/frontend/control-center/src/pages/Catalog/CatalogCard/index.tsx +++ b/frontend/control-center/src/pages/Catalog/CatalogCard/index.tsx @@ -42,12 +42,15 @@ const CatalogCard = (props: CatalogCardProps) => { const [notification, setNotification] = useState(null); const [notifyMeNotification, setNotifyMeNotification] = useState(null); const [forceClose, setForceClose] = useState(false); - const notified = localStorage.getItem(`notified.${componentInfo.source}`); + //Commented until backend is ready for this!!! + // const notified = localStorage.getItem(`notified.${componentInfo.source}`); const installButtonCard = useRef(null); const componentCard = useRef(null); const {t} = useTranslation(); const navigate = useNavigate(); - const notifiedEmail = t('infoNotifyMe') + ` ${notified}`; + + //Commented until backend is ready for this!!! + // const notifiedEmail = t('infoNotifyMe') + ` ${notified}`; const isChannel = componentInfo?.isChannel; @@ -82,22 +85,36 @@ const CatalogCard = (props: CatalogCardProps) => { } }; - const handleNotifyMeClick = () => { - setIsNotifyMeModalVisible(true); - notified && setNotification({show: true, text: notifiedEmail, info: true}); - }; + //Commented until backend is ready for this!!! + // const handleNotifyMeClick = () => { + // setIsNotifyMeModalVisible(true); + // notified && setNotification({show: true, text: notifiedEmail, info: true}); + // }; const CatalogCardButton = () => { + //Commented until backend is ready for this!!! + + // if (componentInfo?.price === ConnectorPrice.requestAccess) { + // return ( + // + // ); + // } + if (componentInfo?.price === ConnectorPrice.requestAccess) { return ( ); } @@ -166,9 +183,11 @@ const CatalogCard = (props: CatalogCardProps) => { ))}
- {componentInfo?.price === ConnectorPrice.requestAccess && ( + {/* Commented until backend is ready for this!!! + + {componentInfo?.price === ConnectorPrice.requestAccess && (
{t('soon').toUpperCase()}
- )} + )} */} diff --git a/frontend/control-center/src/pages/Catalog/CatalogItemDetails/index.module.scss b/frontend/control-center/src/pages/Catalog/CatalogItemDetails/index.module.scss index e3e9417cfe..32a771eea8 100644 --- a/frontend/control-center/src/pages/Catalog/CatalogItemDetails/index.module.scss +++ b/frontend/control-center/src/pages/Catalog/CatalogItemDetails/index.module.scss @@ -206,3 +206,18 @@ background: var(--color-notify-me); color: var(--color-background-white); } + +//As long the backend is not ready!!! +.comingSoonTag { + @include font-base; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + height: 50px; + width: 180px; + color: var(--color-background-white); + background: var(--color-notify-me); + margin-top: 15px; + white-space: nowrap; +} diff --git a/frontend/control-center/src/pages/Catalog/CatalogItemDetails/index.tsx b/frontend/control-center/src/pages/Catalog/CatalogItemDetails/index.tsx index 45d314bbbd..3a910fcfac 100644 --- a/frontend/control-center/src/pages/Catalog/CatalogItemDetails/index.tsx +++ b/frontend/control-center/src/pages/Catalog/CatalogItemDetails/index.tsx @@ -133,40 +133,47 @@ const CatalogItemDetails = (props: ConnectedProps) => {
{getChannelAvatar(componentInfo?.displayName)}
- + {componentInfo?.price === ConnectorPrice.requestAccess ? ( +
{t('comingSoon').toUpperCase()}
+ ) : ( + + )}

{t('availableFor')}:

+ {/* + Commented until backend is ready!!! + {componentInfo?.price === ConnectorPrice.requestAccess && (
{t('soon').toUpperCase()}
- )} + )} */}
{componentInfo?.availableFor && availabilityFormatted(componentInfo.availableFor).map((service: string) => ( diff --git a/lib/typescript/assets/images/icons/amazons3Logo.svg b/lib/typescript/assets/images/icons/amazons3Logo.svg new file mode 100644 index 0000000000..e1328214b4 --- /dev/null +++ b/lib/typescript/assets/images/icons/amazons3Logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/typescript/assets/images/icons/ibmWatsonAssistantLogo.svg b/lib/typescript/assets/images/icons/ibmWatsonAssistantLogo.svg new file mode 100644 index 0000000000..0fd197572e --- /dev/null +++ b/lib/typescript/assets/images/icons/ibmWatsonAssistantLogo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/typescript/model/Source.ts b/lib/typescript/model/Source.ts index 781d0c6e0f..251905c89b 100644 --- a/lib/typescript/model/Source.ts +++ b/lib/typescript/model/Source.ts @@ -17,6 +17,8 @@ export enum Source { mobile = 'mobile', whatsapp = 'whatsapp', frontendInbox = 'frontend-inbox', + ibm = 'ibm', + amazons3 = 'amazons3', } export const prettifySource = (source: string) => diff --git a/lib/typescript/translations/translations.ts b/lib/typescript/translations/translations.ts index 7b137dc107..ea65829c8b 100644 --- a/lib/typescript/translations/translations.ts +++ b/lib/typescript/translations/translations.ts @@ -350,6 +350,12 @@ const resources = { googleConfigurationText3: 'for more information.', newGoogleConnection: 'You are about to connect a new channel', + //IBM Watson Assistant + ibmDescription: 'IBM Watson Assistant uses artificial intelligence that understands customers.', + + //Amazon S3 + amazons3Description: 'Amazon Simple Storage Service (Amazon S3) is an object storage service.', + //Instagram instagramAccount: 'Facebook Page ID connected to the Instagram account', instagramAccountPlaceholder: 'Add the Facebook Page ID', @@ -426,6 +432,7 @@ const resources = { configuration: 'Configuration', //Request Access + comingSoon: 'Coming Soon', notifyMe: 'Notify Me', notifyMeRequestSent: 'Requested', infoNotifyMe: 'We will already send a notification to', @@ -876,6 +883,12 @@ const resources = { googleConfigurationText3: 'für weitere Informationen.', newGoogleConnection: 'Sie sind dabei, einen neuen Kanal zu verbinden', + //IBM Watson Assistant + ibmDescription: 'IBM Watson Assistant verwendet künstliche Intelligenz, die den Kunden versteht.', + + //Amazon S3 + amazons3Description: 'Amazon Simple Storage Service (Amazon S3) ist ein Objektspeicherdienst.', + //Instagram instagramAccount: 'Facebook-Seiten-ID, die mit dem Instagram-Konto verbunden ist', instagramAccountPlaceholder: 'Hinzufügen der Facebook-Seiten-ID', @@ -929,6 +942,7 @@ const resources = { //Request Access + comingSoon: 'Bald verfügbar', notifyMe: 'Informier mich', notifyMeRequestSent: 'Angefordert', infoNotifyMe: 'Wir senden bereits eine Benachrichtigung an', @@ -1348,6 +1362,12 @@ const resources = { googleConfigurationText3: `pour plus d'informations.`, newGoogleConnection: 'Vous êtes sur le point de connecter un nouveau canal', + //IBM Watson Assistant + ibmDescription: `L'assistant IBM Watson utilise une intelligence artificielle qui comprend les clients.`, + + //Amazon S3 + amazons3Description: `Amazon Simple Storage Service (Amazon S3) est un service de stockage d'objets.`, + //Instagram instagramAccount: 'ID de la page Facebook connectée au compte Instagram', instagramAccountPlaceholder: `Ajoutez l'ID de la page Facebook`, @@ -1423,6 +1443,7 @@ const resources = { configuration: 'Configuration', //Request Access + comingSoon: 'Prochainement', notifyMe: 'Notifiez-moi', notifyMeRequestSent: 'Demandé', infoNotifyMe: 'Nous allons déjà envoyer une notification à', @@ -1845,6 +1866,12 @@ const resources = { googleConfigurationText3: 'para más información.', newGoogleConnection: 'Estás a punto de conectar un nuevo canal', + //IBM Watson Assistant + ibmDescription: 'El Asistente Watson de IBM utiliza inteligencia artificial que entiende a los clientes.', + + //Amazon S3 + amazons3Description: 'Amazon Simple Storage Service (Amazon S3) es un servicio de almacenamiento de objetos.', + //Instagram instagramAccount: 'ID de la página de Facebook conectada a la cuenta de Instagram', instagramAccountPlaceholder: 'Añade el ID de la página de Facebook', @@ -1921,6 +1948,7 @@ const resources = { configuration: 'Configuración', //Request Access + comingSoon: 'Próximamente', notifyMe: 'Notificarme', notifyMeRequestSent: 'Solicitado', infoNotifyMe: 'Ya enviaremos una notificación a', From 2d4a3cf85d05cd74d5bb97dd9caa17b308e674c2 Mon Sep 17 00:00:00 2001 From: Ljupco Vangelski Date: Tue, 13 Sep 2022 18:19:28 +0200 Subject: [PATCH 11/74] [#2757] Use Terraform for Minikube provider (#3555) --- cli/pkg/cmd/create/create.go | 66 +-------- cli/pkg/cmd/status/BUILD | 1 - cli/pkg/providers/aws/BUILD | 1 + cli/pkg/providers/aws/aws.go | 27 +--- cli/pkg/providers/minikube/BUILD | 4 +- cli/pkg/providers/minikube/minikube.go | 135 +++++------------- cli/pkg/providers/provider.go | 3 +- .../terraform/install/airy-core/outputs.tf | 9 +- .../terraform/install/minikube/main.tf | 4 + .../terraform/install/minikube/outputs.tf | 3 + .../terraform/install/minikube/state.tf | 5 + infrastructure/terraform/modules/core/main.tf | 27 ++-- .../terraform/modules/core/outputs.tf | 19 ++- .../terraform/modules/core/variables.tf | 9 +- .../terraform/modules/minikube/main.tf | 19 +++ .../terraform/modules/minikube/outputs.tf | 4 + .../terraform/modules/minikube/variables.tf | 28 ++++ lib/go/tools/BUILD | 12 ++ lib/go/tools/tools.go | 17 +++ 19 files changed, 181 insertions(+), 212 deletions(-) create mode 100644 infrastructure/terraform/install/minikube/main.tf create mode 100644 infrastructure/terraform/install/minikube/outputs.tf create mode 100644 infrastructure/terraform/install/minikube/state.tf create mode 100644 infrastructure/terraform/modules/minikube/main.tf create mode 100644 infrastructure/terraform/modules/minikube/outputs.tf create mode 100644 infrastructure/terraform/modules/minikube/variables.tf create mode 100644 lib/go/tools/BUILD create mode 100644 lib/go/tools/tools.go diff --git a/cli/pkg/cmd/create/create.go b/cli/pkg/cmd/create/create.go index 08df0aa44c..4ae4bcb5cb 100644 --- a/cli/pkg/cmd/create/create.go +++ b/cli/pkg/cmd/create/create.go @@ -2,18 +2,14 @@ package create import ( "cli/pkg/console" - "cli/pkg/helm" "cli/pkg/providers" "cli/pkg/workspace" "fmt" "os" "runtime" - "github.com/airyhq/airy/lib/go/k8s" - "github.com/TwinProduction/go-color" "github.com/spf13/cobra" - "github.com/spf13/viper" "gopkg.in/segmentio/analytics-go.v3" ) @@ -88,69 +84,9 @@ func create(cmd *cobra.Command, args []string) { fmt.Fprintln(w) fmt.Fprintln(w, providerName, "provider output:") fmt.Fprintln(w) - context, err := provider.Provision(providerConfig, workspace.ConfigDir{Path: installDir}) + err = provider.Provision(providerConfig, workspace.ConfigDir{Path: installDir}) fmt.Fprintln(w) if err != nil { console.Exit("could not install Airy: ", err) } - if providerName == "minikube" { // TEMP fix to keep minikube working - clientset, err := context.GetClientSet() - if err != nil { - console.Exit("could not get clientset: ", err) - } - - if err = context.Store(); err != nil { - console.Exit("could not store the kube context: ", err) - } - - helm := helm.New(clientset, version, namespace, dir.GetAiryYaml()) - if err := helm.Setup(); err != nil { - console.Exit("setting up Helm failed with err: ", err) - } - - fmt.Println("🚀 Starting core with default components") - - if err := helm.InstallCharts(); err != nil { - console.Exit("installing Helm charts failed with err: ", err) - } - - if err = provider.PostInstallation(providerConfig, namespace, dir); err != nil { - console.Exit("failed to run post installation hook: ", err) - } - - fmt.Println("🎉 Your Airy Core is ready") - - coreConfig, err := k8s.GetCmData("core-config", namespace, clientset) - if err != nil { - console.Exit("failed to get hosts from installation") - } - - fmt.Println("\t 👩‍🍳 Available hosts:") - for hostName, host := range coreConfig { - switch hostName { - case "HOST": - fmt.Printf("\t\t %s:\t %s", "Host", host) - fmt.Println() - case "API_HOST": - fmt.Printf("\t\t %s:\t %s", "API", host) - fmt.Println() - case "NGROK": - fmt.Printf("\t\t %s:\t %s", "NGROK", host) - fmt.Println() - } - } - - fmt.Println("✅ Airy Installed") - fmt.Println() - - viper.Set("provider", provider) - viper.Set("namespace", namespace) - viper.WriteConfig() - - airyAnalytics.Track(analytics.Track{ - UserId: coreConfig["CORE_ID"], - Event: "installation_succesful"}) - fmt.Printf("📚 For more information about the %s provider visit https://airy.co/docs/core/getting-started/installation/%s", providerName, providerName) - fmt.Println() - } } diff --git a/cli/pkg/cmd/status/BUILD b/cli/pkg/cmd/status/BUILD index 1e6e15a3c4..19f89229a3 100644 --- a/cli/pkg/cmd/status/BUILD +++ b/cli/pkg/cmd/status/BUILD @@ -11,7 +11,6 @@ go_library( "//lib/go/httpclient", "@com_github_spf13_cobra//:cobra", "@com_github_spf13_viper//:viper", - "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library", ], ) diff --git a/cli/pkg/providers/aws/BUILD b/cli/pkg/providers/aws/BUILD index 52c3173890..bba406d29b 100644 --- a/cli/pkg/providers/aws/BUILD +++ b/cli/pkg/providers/aws/BUILD @@ -17,6 +17,7 @@ go_library( "//cli/pkg/kube", "//cli/pkg/workspace", "//cli/pkg/workspace/template", + "//lib/go/tools", "@com_github_hashicorp_go_getter//:go-getter", "@in_gopkg_segmentio_analytics_go_v3//:analytics-go_v3", ], diff --git a/cli/pkg/providers/aws/aws.go b/cli/pkg/providers/aws/aws.go index ad90420bb3..8d817749ae 100644 --- a/cli/pkg/providers/aws/aws.go +++ b/cli/pkg/providers/aws/aws.go @@ -2,22 +2,19 @@ package aws import ( "cli/pkg/console" - "cli/pkg/kube" "cli/pkg/workspace" tmpl "cli/pkg/workspace/template" "io" - "math/rand" "os" "os/exec" "strings" - "time" + + "github.com/airyhq/airy/lib/go/tools" getter "github.com/hashicorp/go-getter" "gopkg.in/segmentio/analytics-go.v3" ) -var letters = []rune("abcdefghijklmnopqrstuvwxyz") - type provider struct { w io.Writer analytics console.AiryAnalytics @@ -68,15 +65,14 @@ type KubeConfig struct { CertificateData string } -func (p *provider) Provision(providerConfig map[string]string, dir workspace.ConfigDir) (kube.KubeCtx, error) { +func (p *provider) Provision(providerConfig map[string]string, dir workspace.ConfigDir) error { installPath := dir.GetPath(".") - id := RandString(8) + id := tools.RandString(8) p.analytics.Track(analytics.Identify{ AnonymousId: id, Traits: analytics.NewTraits(). Set("provider", "AWS"), }) - name := "Airy-" + id cmd := exec.Command("/bin/bash", "install.sh") cmd.Dir = installPath cmd.Stdin = os.Stdin @@ -87,18 +83,5 @@ func (p *provider) Provision(providerConfig map[string]string, dir workspace.Con if err != nil { console.Exit("Error with Terraform installation", err) } - ctx := kube.KubeCtx{ - KubeConfigPath: "./kube.conf", // change this into a CLI - ContextName: name, - } - return ctx, nil -} - -func RandString(n int) string { - rand.Seed(time.Now().UnixNano()) - b := make([]rune, n) - for i := range b { - b[i] = letters[rand.Intn(len(letters))] - } - return string(b) + return nil } diff --git a/cli/pkg/providers/minikube/BUILD b/cli/pkg/providers/minikube/BUILD index 79fd961ece..a6d281a6a9 100644 --- a/cli/pkg/providers/minikube/BUILD +++ b/cli/pkg/providers/minikube/BUILD @@ -10,8 +10,8 @@ go_library( "//cli/pkg/kube", "//cli/pkg/workspace", "//cli/pkg/workspace/template", + "//lib/go/tools", + "@com_github_hashicorp_go_getter//:go-getter", "@in_gopkg_segmentio_analytics_go_v3//:analytics-go_v3", - "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library", - "@io_k8s_client_go//util/homedir:go_default_library", ], ) diff --git a/cli/pkg/providers/minikube/minikube.go b/cli/pkg/providers/minikube/minikube.go index e08e9f4bea..1090272b03 100644 --- a/cli/pkg/providers/minikube/minikube.go +++ b/cli/pkg/providers/minikube/minikube.go @@ -5,17 +5,15 @@ import ( "cli/pkg/kube" "cli/pkg/workspace" "cli/pkg/workspace/template" - "context" - "fmt" "io" + "os" "os/exec" - "path/filepath" - "runtime" "strings" + "github.com/airyhq/airy/lib/go/tools" + + "github.com/hashicorp/go-getter" "gopkg.in/segmentio/analytics-go.v3" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/util/homedir" ) const ( @@ -45,114 +43,49 @@ func (p *provider) GetOverrides() template.Variables { } func (p *provider) CheckEnvironment() error { - return workspace.CheckBinaries([]string{"minikube"}) -} -func (p *provider) PreInstallation(workspace string) (string, error) { - return workspace, nil + return workspace.CheckBinaries([]string{"minikube", "terraform"}) } - -func (p *provider) Provision(providerConfig map[string]string, dir workspace.ConfigDir) (kube.KubeCtx, error) { - if err := p.startCluster(providerConfig); err != nil { - return kube.KubeCtx{}, err +func (p *provider) PreInstallation(workspacePath string) (string, error) { + remoteUrl := "github.com/airyhq/airy/infrastructure/terraform/install" + installDir := workspacePath + "/terraform" + installFlags := strings.Join([]string{"PROVIDER=minikube", "WORKSPACE=" + workspacePath}, "\n") + + var gitGetter = &getter.Client{ + Src: remoteUrl, + Dst: installDir, + Dir: true, } - - homeDir := homedir.HomeDir() - if homeDir == "" { - return kube.KubeCtx{}, fmt.Errorf("could not find the kubeconfig") + if err := gitGetter.Get(); err != nil { + return "", err } - - ctx := kube.New(filepath.Join(homeDir, ".kube", "config"), profile) - p.context = ctx - return ctx, nil -} - -func (p *provider) startCluster(providerConfig map[string]string) error { - minikubeDriver := getArg(providerConfig, "driver", "docker") - minikubeCpus := getArg(providerConfig, "cpus", "4") - minikubeMemory := getArg(providerConfig, "memory", "7168") - driverArg := "--driver=" + minikubeDriver - runtimeArg := "" - if minikubeDriver == "docker" { - runtimeArg = dockerRuntime - } - cpusArg := "--cpus=" + minikubeCpus - memoryArg := "--memory=" + minikubeMemory - args := []string{"start", "--extra-config=apiserver.service-node-port-range=1-65535", "--ports=80:80", driverArg, runtimeArg, cpusArg, memoryArg} - // Prevent minikube download progress bar from polluting the output by downloading silently first - _, err := runGetOutput(append(args, "--download-only")...) + err := os.WriteFile(installDir+"/install.flags", []byte(installFlags), 0666) if err != nil { - return fmt.Errorf("downloading minikube files err: %v", err) + return "", err } - return p.runPrintOutput(args...) + return installDir, nil } -func (p *provider) runPrintOutput(args ...string) error { - cmd := getCmd(args...) - fmt.Fprintf(p.w, "$ %s %s", cmd.Path, strings.Join(cmd.Args, " ")) - fmt.Fprintln(p.w) - fmt.Fprintln(p.w) - cmd.Stdout = p.w +func (p *provider) Provision(providerConfig map[string]string, dir workspace.ConfigDir) error { + installPath := dir.GetPath(".") + id := tools.RandString(8) + p.analytics.Track(analytics.Identify{ + AnonymousId: id, + Traits: analytics.NewTraits(). + Set("provider", "Minikube"), + }) + cmd := exec.Command("/bin/bash", "install.sh") + cmd.Dir = installPath + cmd.Stdin = os.Stdin cmd.Stderr = p.w - return cmd.Run() -} + cmd.Stdout = p.w + err := cmd.Run() -func runGetOutput(args ...string) (string, error) { - cmd := getCmd(args...) - out, err := cmd.CombinedOutput() if err != nil { - return string(out), fmt.Errorf("running minikube failed with err: %v\n%v", err, string(out)) + console.Exit("Error with Terraform installation", err) } - return string(out), nil -} - -func getCmd(args ...string) *exec.Cmd { - defaultArgs := []string{"--profile=" + profile} - return exec.Command(minikube, append(defaultArgs, args...)...) + return nil } func (p *provider) PostInstallation(providerConfig map[string]string, namespace string, dir workspace.ConfigDir) error { - clientset, err := p.context.GetClientSet() - if err != nil { - return err - } - - configMaps := clientset.CoreV1().ConfigMaps(namespace) - configMap, err := configMaps.Get(context.TODO(), "core-config", metav1.GetOptions{}) - if err != nil { - return err - } - - // Ensure that kubectl is downloaded so that the progressbar does not pollute the output - runGetOutput("kubectl", "version") - - coreId, err := runGetOutput("kubectl", "--", "get", "cm", "core-config", "-o", "jsonpath='{.data.CORE_ID}'") - if err != nil { - return err - } - coreId = strings.Trim(coreId, "'") - - p.analytics.Track(analytics.Identify{ - UserId: coreId, - Traits: analytics.NewTraits(). - Set("provider", "minikube"). - Set("numCpu", runtime.NumCPU()), - }, - ) - - ngrokEndpoint := fmt.Sprintf("https://%s.tunnel.airy.co", coreId) - - configMap.Data["NGROK"] = ngrokEndpoint - if _, err = configMaps.Update(context.TODO(), configMap, metav1.UpdateOptions{}); err != nil { - return err - } - return nil } - -func getArg(providerConfig map[string]string, key string, fallback string) string { - value := providerConfig[key] - if value == "" { - return fallback - } - return value -} diff --git a/cli/pkg/providers/provider.go b/cli/pkg/providers/provider.go index a50841b76a..56faf0e4ba 100644 --- a/cli/pkg/providers/provider.go +++ b/cli/pkg/providers/provider.go @@ -2,7 +2,6 @@ package providers import ( "cli/pkg/console" - "cli/pkg/kube" "cli/pkg/providers/aws" "cli/pkg/providers/minikube" "cli/pkg/workspace" @@ -19,7 +18,7 @@ const ( ) type Provider interface { - Provision(providerConfig map[string]string, dir workspace.ConfigDir) (kube.KubeCtx, error) + Provision(providerConfig map[string]string, dir workspace.ConfigDir) error GetOverrides() template.Variables CheckEnvironment() error PreInstallation(workspace string) (string, error) diff --git a/infrastructure/terraform/install/airy-core/outputs.tf b/infrastructure/terraform/install/airy-core/outputs.tf index 7480452749..06a5e7c851 100644 --- a/infrastructure/terraform/install/airy-core/outputs.tf +++ b/infrastructure/terraform/install/airy-core/outputs.tf @@ -1,9 +1,14 @@ output "API" { description = "The URL where the API and the UI can be reached" - value = module.airy_core.loadbalancer + value = module.airy_core.api_host +} + +output "NGROK" { + description = "The URL where the API and the UI can be reached" + value = "https://${module.airy_core.unique_id}.tunnel.airy.co" } output "Info" { description = "More information" - value = "For more information about the AWS provider visit https://airy.co/docs/core/getting-started/installation/aws" + value = "For more information visit https://airy.co/docs/core/getting-started/installation/introduction" } diff --git a/infrastructure/terraform/install/minikube/main.tf b/infrastructure/terraform/install/minikube/main.tf new file mode 100644 index 0000000000..0393465d6e --- /dev/null +++ b/infrastructure/terraform/install/minikube/main.tf @@ -0,0 +1,4 @@ +module "minikube" { + source = "github.com/airyhq/airy.git/infrastructure/terraform/modules/minikube" + kubeconfig_output_path = "../kube.conf" +} diff --git a/infrastructure/terraform/install/minikube/outputs.tf b/infrastructure/terraform/install/minikube/outputs.tf new file mode 100644 index 0000000000..cc73a462a8 --- /dev/null +++ b/infrastructure/terraform/install/minikube/outputs.tf @@ -0,0 +1,3 @@ +output "kubeconfig_path" { + value = module.minikube.kubeconfig_path +} diff --git a/infrastructure/terraform/install/minikube/state.tf b/infrastructure/terraform/install/minikube/state.tf new file mode 100644 index 0000000000..c89c917cc5 --- /dev/null +++ b/infrastructure/terraform/install/minikube/state.tf @@ -0,0 +1,5 @@ +terraform { + backend "local" { + path = "../kubernetes.tfstate" + } +} diff --git a/infrastructure/terraform/modules/core/main.tf b/infrastructure/terraform/modules/core/main.tf index 08b8ddc3fa..d2c9a4e734 100644 --- a/infrastructure/terraform/modules/core/main.tf +++ b/infrastructure/terraform/modules/core/main.tf @@ -1,9 +1,3 @@ -resource "kubernetes_namespace" "core" { - metadata { - name = var.core_id - } -} - data "http" "core_version" { url = "https://raw.githubusercontent.com/airyhq/airy/main/VERSION" } @@ -16,14 +10,16 @@ resource "helm_release" "airy_core" { name = "airy-release" chart = "https://helm.airy.co/charts/airy-${local.core_version}.tgz" - timeout = "600" + timeout = var.timeout values = [ var.values_yaml, var.resources_yaml, var.prerequisite_properties_yaml ] - namespace = var.namespace + namespace = var.namespace + create_namespace = true + wait = false set { name = "global.appImageTag" @@ -34,11 +30,6 @@ resource "helm_release" "airy_core" { name = "ingress-controller.enabled" value = var.ingress_controller_enabled } - - depends_on = [ - kubernetes_namespace.core - ] - } data "kubernetes_service" "ingress" { @@ -50,3 +41,13 @@ data "kubernetes_service" "ingress" { helm_release.airy_core ] } + +data "kubernetes_config_map" "core_config" { + metadata { + name = "core-config" + namespace = var.namespace + } + depends_on = [ + helm_release.airy_core + ] +} diff --git a/infrastructure/terraform/modules/core/outputs.tf b/infrastructure/terraform/modules/core/outputs.tf index 777d261b47..1c47774502 100644 --- a/infrastructure/terraform/modules/core/outputs.tf +++ b/infrastructure/terraform/modules/core/outputs.tf @@ -3,6 +3,21 @@ output "core_id" { } output "loadbalancer" { - description = "The URL for the load balancer of the cluster. Used to access the UI via the browser" - value = data.kubernetes_service.ingress.status.0.load_balancer.0.ingress.0.hostname + description = "The URL for the load balancer of the cluster. Used to access the UI via the browser" + value = try(data.kubernetes_service.ingress.status.0.load_balancer.0.ingress.0.hostname,data.kubernetes_config_map.core_config.data.HOST) +} + +output "api_host" { + description = "The URL of the API and the UI" + value = try(data.kubernetes_config_map.core_config.data.API_HOST,data.kubernetes_config_map.core_config.data.HOST) +} + +output "version" { + description = "The URL of the API and the UI" + value = data.kubernetes_config_map.core_config.data.APP_IMAGE_TAG +} + +output "unique_id" { + description = "Unique ID used for the NGrok public tunnel" + value = data.kubernetes_config_map.core_config.data.CORE_ID } diff --git a/infrastructure/terraform/modules/core/variables.tf b/infrastructure/terraform/modules/core/variables.tf index 60d2edb9a1..2b2762a258 100644 --- a/infrastructure/terraform/modules/core/variables.tf +++ b/infrastructure/terraform/modules/core/variables.tf @@ -15,10 +15,12 @@ variable "namespace" { variable "values_yaml" { description = "The helm values overrides" + type = string } variable "resources_yaml" { description = "Resource requests and limits for the components" + default = "" } variable "prerequisite_properties_yaml" { @@ -28,12 +30,15 @@ variable "prerequisite_properties_yaml" { variable "core_version" { description = "Version of the Airy Core instance" - type = string default = "" } variable "ingress_controller_enabled" { description = "Whether to create the NGinx ingress controller" - type = string default = "true" } + +variable "timeout" { + description = "Timeout for the Helm installation" + default = 600 +} diff --git a/infrastructure/terraform/modules/minikube/main.tf b/infrastructure/terraform/modules/minikube/main.tf new file mode 100644 index 0000000000..92a2234b51 --- /dev/null +++ b/infrastructure/terraform/modules/minikube/main.tf @@ -0,0 +1,19 @@ +resource "null_resource" "minikube" { + triggers = { + profile = var.profile + driver = var.driver + cpus = var.cpus + memory = var.memory + nodeport = var.nodeport + kubeconfig_output_path = var.kubeconfig_output_path + } + + provisioner "local-exec" { + command = "KUBECONFIG=${self.triggers.kubeconfig_output_path} minikube -p ${self.triggers.profile} start --driver=${self.triggers.driver} --cpus=${self.triggers.cpus} --memory=${self.triggers.memory} --container-runtime=containerd --ports=${self.triggers.nodeport}:${self.triggers.nodeport} --extra-config=apiserver.service-node-port-range=1-65535" + } + + provisioner "local-exec" { + when = destroy + command = "minikube -p ${self.triggers.profile} delete" + } +} diff --git a/infrastructure/terraform/modules/minikube/outputs.tf b/infrastructure/terraform/modules/minikube/outputs.tf new file mode 100644 index 0000000000..ede5fc5c10 --- /dev/null +++ b/infrastructure/terraform/modules/minikube/outputs.tf @@ -0,0 +1,4 @@ +output "kubeconfig_path" { + description = "The location of the KUBECONFIG file that was used" + value = var.kubeconfig_output_path +} diff --git a/infrastructure/terraform/modules/minikube/variables.tf b/infrastructure/terraform/modules/minikube/variables.tf new file mode 100644 index 0000000000..a52fa05767 --- /dev/null +++ b/infrastructure/terraform/modules/minikube/variables.tf @@ -0,0 +1,28 @@ +variable "profile" { + description = "The profile to be used for Minikube" + default = "airy-core" +} + +variable "driver" { + description = "The driver to be used for Minikube" + default = "docker" +} + +variable "cpus" { + description = "Number of CPUs Minikube should use" + default = 3 +} + +variable "memory" { + description = "Amount of memory Minikube should use" + default = 7168 +} + +variable "nodeport" { + description = "The port on which the ingress controller, the API and the web UI will be reached" + default = 80 +} + +variable "kubeconfig_output_path" { + description = "The path where the KUBECONFIG file will be written" +} diff --git a/lib/go/tools/BUILD b/lib/go/tools/BUILD new file mode 100644 index 0000000000..0e40b57219 --- /dev/null +++ b/lib/go/tools/BUILD @@ -0,0 +1,12 @@ +load("@com_github_airyhq_bazel_tools//lint:buildifier.bzl", "check_pkg") +load("@io_bazel_rules_go//go:def.bzl", "go_library") +# gazelle:prefix github.com/airyhq/airy/lib/go/tools + +go_library( + name = "tools", + srcs = ["tools.go"], + importpath = "github.com/airyhq/airy/lib/go/tools", + visibility = ["//visibility:public"], +) + +check_pkg(name = "buildifier") diff --git a/lib/go/tools/tools.go b/lib/go/tools/tools.go new file mode 100644 index 0000000000..28aa54e096 --- /dev/null +++ b/lib/go/tools/tools.go @@ -0,0 +1,17 @@ +package tools + +import ( + "math/rand" + "time" +) + +var letters = []rune("abcdefghijklmnopqrstuvwxyz") + +func RandString(n int) string { + rand.Seed(time.Now().UnixNano()) + b := make([]rune, n) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] + } + return string(b) +} From a1cf44e7f2a43861262f1ce0c498289ec6082fa5 Mon Sep 17 00:00:00 2001 From: Thorsten Date: Wed, 14 Sep 2022 09:23:15 +0200 Subject: [PATCH 12/74] [#3708] Sort status alphabetically (#3712) --- .../control-center/src/pages/Status/index.tsx | 21 ++++++++++++------- lib/typescript/model/Config.ts | 5 ++++- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/frontend/control-center/src/pages/Status/index.tsx b/frontend/control-center/src/pages/Status/index.tsx index a12910cd4f..a033f74d43 100644 --- a/frontend/control-center/src/pages/Status/index.tsx +++ b/frontend/control-center/src/pages/Status/index.tsx @@ -1,5 +1,5 @@ import React, {useEffect, useState} from 'react'; -import {connect, ConnectedProps, useSelector} from 'react-redux'; +import {connect, ConnectedProps} from 'react-redux'; import {getClientConfig, getConnectorsConfiguration, listComponents} from '../../actions'; import {StateModel} from '../../reducers'; import {ComponentListItem} from './ComponentListItem'; @@ -14,12 +14,17 @@ const mapDispatchToProps = { listComponents, }; -const connector = connect(null, mapDispatchToProps); +const mapStateToProps = (state: StateModel) => { + return { + components: Object.entries(state.data.config.components), + catalog: state.data.catalog, + }; +}; + +const connector = connect(mapStateToProps, mapDispatchToProps); const Status = (props: ConnectedProps) => { - const {getClientConfig, getConnectorsConfiguration, listComponents} = props; - const components = useSelector((state: StateModel) => Object.entries(state.data.config.components)); - const catalogList = useSelector((state: StateModel) => state.data.catalog); + const {components, catalog, getClientConfig, getConnectorsConfiguration, listComponents} = props; const [spinAnim, setSpinAnim] = useState(true); const [lastRefresh, setLastRefresh] = useState(new Date().toLocaleString()); const {t} = useTranslation(); @@ -56,7 +61,7 @@ const Status = (props: ConnectedProps) => { const formatToComponentName = (name: string) => { let formattedName; if (name.includes('enterprise')) { - formattedName = `'airy-enterprise'/${name}`; + formattedName = `airy-enterprise/${name}`; } else { formattedName = `airy-core/${name}`; } @@ -85,10 +90,10 @@ const Status = (props: ConnectedProps) => {
- {Object.entries(catalogList).length > 0 && + {Object.entries(catalog).length > 0 && components.map((component, index) => { const formattedName = formatToComponentName(component[0]); - const catalogItem = catalogList[formattedName]; + const catalogItem = catalog[formattedName]; return ( { const {services} = config; + const servicesSorted = Object.fromEntries( + Object.entries(services).sort((a, b) => a[1].component.localeCompare(b[1].component)) + ); - return Object.keys(services).reduce((agg, key) => { + return Object.keys(servicesSorted).reduce((agg, key) => { const {healthy, enabled, component} = services[key]; return { From c1e8a29bbc2b519fd04e45957af640050485e3e7 Mon Sep 17 00:00:00 2001 From: Thorsten Date: Wed, 14 Sep 2022 17:21:35 +0200 Subject: [PATCH 13/74] [#3469] Removed animation contacts and added installed check (#3716) --- frontend/inbox/handles/index.ts | 1 - frontend/inbox/src/actions/contacts/index.ts | 8 +++---- .../src/components/ContactDetails/index.tsx | 4 +++- .../DialogCustomizable/index.module.scss | 2 +- .../src/components/Sidebar/index.module.scss | 2 +- .../ContactInformation/index.module.scss | 18 --------------- .../Contacts/ContactInformation/index.tsx | 19 ++++----------- .../ContactListItem/index.module.scss | 3 +++ .../src/pages/Contacts/index.module.scss | 8 ------- frontend/inbox/src/pages/Contacts/index.tsx | 23 +++++++++++-------- .../Messenger/ConversationMetadata/index.tsx | 4 +++- integration/ui/list_contacts_page.spec.ts | 5 +--- 12 files changed, 33 insertions(+), 64 deletions(-) diff --git a/frontend/inbox/handles/index.ts b/frontend/inbox/handles/index.ts index 482cace97a..e4e8b3cd64 100644 --- a/frontend/inbox/handles/index.ts +++ b/frontend/inbox/handles/index.ts @@ -72,4 +72,3 @@ export const cyChannelsInstagramList = 'cyChannelsInstagramList'; export const cyConversationsListForContact = 'conversationsListForContact'; export const cyConversationForContactButton = 'conversationForContactButton'; export const cyContactItem = 'contactItem'; -export const cyContactsCollapseIcon = 'contactsCollapseIcon'; diff --git a/frontend/inbox/src/actions/contacts/index.ts b/frontend/inbox/src/actions/contacts/index.ts index 75e3857e78..1dd976a556 100644 --- a/frontend/inbox/src/actions/contacts/index.ts +++ b/frontend/inbox/src/actions/contacts/index.ts @@ -28,7 +28,7 @@ export const listContactsAction = createAction(CONTACT_LIST, (contacts: Contact[ export const deleteContactAction = createAction(CONTACT_DELETE, (id: string) => id)(); -export const listContacts = () => (dispatch: Dispatch, state: () => StateModel) => { +export const listContacts = () => async (dispatch: Dispatch, state: () => StateModel) => { const pageSize = 54; const cursor = state().data.contacts.all.paginationData.nextCursor; HttpClientInstance.listContacts({page_size: pageSize, cursor: cursor}).then( @@ -40,7 +40,7 @@ export const listContacts = () => (dispatch: Dispatch, state: () => StateMo }; //deleteContact is disabled in the Contacts page (temporarily) -export const deleteContact = (id: string) => (dispatch: Dispatch) => { +export const deleteContact = (id: string) => async (dispatch: Dispatch) => { HttpClientInstance.deleteContact(id).then(() => { dispatch(deleteContactAction(id)); return Promise.resolve(true); @@ -49,7 +49,7 @@ export const deleteContact = (id: string) => (dispatch: Dispatch) => { export const getContactDetails = ({id, conversationId}: GetContactDetailsRequestPayload) => - (dispatch: Dispatch) => { + async (dispatch: Dispatch) => { return HttpClientInstance.getContactDetails({id, conversationId}).then((response: Contact) => { dispatch(getContactDetailsAction(response.id, response)); return Promise.resolve(response.id); @@ -57,7 +57,7 @@ export const getContactDetails = }; export const updateContactDetails = - (updateContactDetailsRequestPayload: UpdateContactDetailsRequestPayload) => (dispatch: Dispatch) => { + (updateContactDetailsRequestPayload: UpdateContactDetailsRequestPayload) => async (dispatch: Dispatch) => { return HttpClientInstance.updateContactDetails(updateContactDetailsRequestPayload).then(() => { dispatch(updateContactDetailsAction(updateContactDetailsRequestPayload)); return Promise.resolve(true); diff --git a/frontend/inbox/src/components/ContactDetails/index.tsx b/frontend/inbox/src/components/ContactDetails/index.tsx index 4cc407cdf1..f5be1eaa44 100644 --- a/frontend/inbox/src/components/ContactDetails/index.tsx +++ b/frontend/inbox/src/components/ContactDetails/index.tsx @@ -186,7 +186,9 @@ const ContactDetails = (props: ContactDetailsProps) => { organization ); - updateContactDetails({...infoDetailsPayload}); + updateContactDetails({...infoDetailsPayload}).catch((error: Error) => { + console.error(error); + }); updateContactType(infoDetailsPayload); getUpdatedInfo(); fillContactInfo({...infoDetailsPayload}, setEmail, setPhone, setTitle, setAddress, setCity, setOrganization); diff --git a/frontend/inbox/src/components/DialogCustomizable/index.module.scss b/frontend/inbox/src/components/DialogCustomizable/index.module.scss index 36116c2794..93dfcce61b 100644 --- a/frontend/inbox/src/components/DialogCustomizable/index.module.scss +++ b/frontend/inbox/src/components/DialogCustomizable/index.module.scss @@ -4,7 +4,7 @@ position: absolute; background-color: var(--color-background-white); border: 1px solid var(--color-light-gray); - border-radius: 4px; + border-radius: 10px; z-index: $popup; } diff --git a/frontend/inbox/src/components/Sidebar/index.module.scss b/frontend/inbox/src/components/Sidebar/index.module.scss index 04a73d2567..9be06f4a81 100644 --- a/frontend/inbox/src/components/Sidebar/index.module.scss +++ b/frontend/inbox/src/components/Sidebar/index.module.scss @@ -79,7 +79,7 @@ background-color: var(--color-blue-white); } -.inaction { +.inactive { display: none; } diff --git a/frontend/inbox/src/pages/Contacts/ContactInformation/index.module.scss b/frontend/inbox/src/pages/Contacts/ContactInformation/index.module.scss index 7a2aa603c5..eb1b32662b 100644 --- a/frontend/inbox/src/pages/Contacts/ContactInformation/index.module.scss +++ b/frontend/inbox/src/pages/Contacts/ContactInformation/index.module.scss @@ -199,21 +199,3 @@ fill: var(--color-text-gray); } } - -.contactsDetailsCollapseIcon { - position: absolute; - left: 20px; - top: 20px; - width: 20px; - background: transparent; - border: none; - cursor: pointer; - transition: opacity 0.3s; - svg { - fill: var(--color-airy-blue); - } -} - -.contactsDetailsCollapseIcon:hover { - opacity: 0.5; -} diff --git a/frontend/inbox/src/pages/Contacts/ContactInformation/index.tsx b/frontend/inbox/src/pages/Contacts/ContactInformation/index.tsx index 82caaa5359..810d3a055f 100644 --- a/frontend/inbox/src/pages/Contacts/ContactInformation/index.tsx +++ b/frontend/inbox/src/pages/Contacts/ContactInformation/index.tsx @@ -13,7 +13,6 @@ import {ReactComponent as CloseIcon} from 'assets/images/icons/close.svg'; import {ReactComponent as CheckmarkCircleIcon} from 'assets/images/icons/checkmark.svg'; import {ReactComponent as EditIcon} from 'assets/images/icons/pen.svg'; import {ReactComponent as CancelIcon} from 'assets/images/icons/cancelCross.svg'; -import {ReactComponent as CollapseRightArrowsIcon} from 'assets/images/icons/collapseRightArrows.svg'; import {StateModel} from '../../../reducers'; import { cyCancelEditContactIcon, @@ -22,7 +21,6 @@ import { cyEditContactIcon, cyEditDisplayNameCheckmark, cyEditDisplayNameIcon, - cyContactsCollapseIcon, } from 'handles'; const mapStateToProps = (state: StateModel) => ({ @@ -42,8 +40,6 @@ type ContactInformationProps = { editModeOn: boolean; cancelEdit: boolean; setEditModeOn: (editing: boolean) => void; - setContactInformationVisible: React.Dispatch>; - contactInformationVisible: boolean; } & ConnectedProps; const ContactInformation = (props: ContactInformationProps) => { @@ -56,8 +52,6 @@ const ContactInformation = (props: ContactInformationProps) => { editModeOn, cancelEdit, setEditModeOn, - setContactInformationVisible, - contactInformationVisible, } = props; const {t} = useTranslation(); const [showEditDisplayName, setShowEditDisplayName] = useState(false); @@ -83,7 +77,9 @@ const ContactInformation = (props: ContactInformationProps) => { }, [editModeOn, isContactDetailsExpanded, cancelEdit]); const saveEditDisplayName = () => { - updateContactDetails({id: contact.id, displayName: displayName}); + updateContactDetails({id: contact.id, displayName: displayName}).catch((error: Error) => { + console.error(error); + }); if (contact?.conversations) { Object.keys(contact.conversations).forEach(conversationId => { @@ -125,15 +121,8 @@ const ContactInformation = (props: ContactInformationProps) => { return ( <> - {(contact || conversationId) && contactInformationVisible && ( + {(contact || conversationId) && (
- {!isEditing ? (
-
+
diff --git a/frontend/inbox/src/pages/Inbox/Messenger/ConversationMetadata/index.tsx b/frontend/inbox/src/pages/Inbox/Messenger/ConversationMetadata/index.tsx index 128271a113..4543b37f78 100644 --- a/frontend/inbox/src/pages/Inbox/Messenger/ConversationMetadata/index.tsx +++ b/frontend/inbox/src/pages/Inbox/Messenger/ConversationMetadata/index.tsx @@ -150,7 +150,9 @@ const ConversationMetadata = (props: ConnectedProps) => { updateConversationContactInfo(conversationId, displayName); }); } - updateContactDetails({id: contactIdConvMetadata, displayName: displayName}); + updateContactDetails({id: contactIdConvMetadata, displayName: displayName}).catch((error: Error) => { + console.error(error); + }); setShowEditDisplayName(!saveEditDisplayName); }; diff --git a/integration/ui/list_contacts_page.spec.ts b/integration/ui/list_contacts_page.spec.ts index 08199bc190..99d2c06144 100644 --- a/integration/ui/list_contacts_page.spec.ts +++ b/integration/ui/list_contacts_page.spec.ts @@ -1,4 +1,4 @@ -import {cyContactItem, cyContactEmail, cyContactsCollapseIcon} from 'handles'; +import {cyContactItem, cyContactEmail} from 'handles'; describe('Contacts page lists contacts and allow to edit contacts details and display name', () => { beforeEach(() => { @@ -26,9 +26,6 @@ describe('Contacts page lists contacts and allow to edit contacts details and di cy.get(`[data-cy=${cyContactEmail}]`).should('be.visible'); - cy.get(`[data-cy=${cyContactsCollapseIcon}]`).click(); - cy.wait(500); - cy.get(`[data-cy=${cyContactEmail}]`).should('not.exist'); }); }); From 2b507c757df07738e4fb8f6a1ceb93c1169f7d24 Mon Sep 17 00:00:00 2001 From: Thorsten Date: Fri, 16 Sep 2022 12:14:56 +0200 Subject: [PATCH 14/74] [#3723] Sort catalog (#3729) --- .../src/pages/Catalog/index.tsx | 39 ++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/frontend/control-center/src/pages/Catalog/index.tsx b/frontend/control-center/src/pages/Catalog/index.tsx index 0daa42dece..4e6f0ac4ef 100644 --- a/frontend/control-center/src/pages/Catalog/index.tsx +++ b/frontend/control-center/src/pages/Catalog/index.tsx @@ -1,25 +1,31 @@ -import React, {useState, useEffect} from 'react'; +import React, {useState, useEffect, useLayoutEffect} from 'react'; import {useTranslation} from 'react-i18next'; -import {connect, ConnectedProps, useSelector} from 'react-redux'; +import {connect, ConnectedProps} from 'react-redux'; import {listComponents} from '../../actions/catalog'; import {StateModel} from '../../reducers'; import {setPageTitle} from '../../services'; -import {ComponentInfo} from 'model'; +import {ComponentInfo, ConnectorPrice} from 'model'; import CatalogCard from './CatalogCard'; import styles from './index.module.scss'; +const mapStateToProps = (state: StateModel) => { + return { + catalogList: Object.values(state.data.catalog), + }; +}; + const mapDispatchToProps = { listComponents, }; -const connector = connect(null, mapDispatchToProps); +const connector = connect(mapStateToProps, mapDispatchToProps); const Catalog = (props: ConnectedProps) => { - const {listComponents} = props; - const [orderedCatalogList, setOrderedCatalogList] = useState([]); - const catalogList = useSelector((state: StateModel) => state.data.catalog); + const {catalogList, listComponents} = props; + const [orderedCatalogList, setOrderedCatalogList] = useState(catalogList); const {t} = useTranslation(); const catalogPageTitle = t('Catalog'); + const sortByName = (a: ComponentInfo, b: ComponentInfo) => a?.displayName?.localeCompare(b?.displayName); useEffect(() => { listComponents().catch((error: Error) => { @@ -28,14 +34,19 @@ const Catalog = (props: ConnectedProps) => { setPageTitle(catalogPageTitle); }, []); - useEffect(() => { - setOrderedCatalogList(Object.values(catalogList).sort(sortByInstall)); - }, [catalogList]); + useLayoutEffect(() => { + const sortedByInstalled = [...catalogList] + .filter((component: ComponentInfo) => component.installed && component.price !== ConnectorPrice.requestAccess) + .sort(sortByName); + const sortedByUninstalled = [...catalogList] + .filter((component: ComponentInfo) => !component.installed && component.price !== ConnectorPrice.requestAccess) + .sort(sortByName); + const sortedByAccess = [...catalogList] + .filter((component: ComponentInfo) => component.price === ConnectorPrice.requestAccess) + .sort(sortByName); - const sortByInstall = (a: ComponentInfo) => { - if (a.installed) return 1; - return -1; - }; + setOrderedCatalogList(sortedByInstalled.concat(sortedByUninstalled).concat(sortedByAccess)); + }, [catalogList]); return (
From 84814e7dee9861cc8485f2097bf735ec5ef1494a Mon Sep 17 00:00:00 2001 From: Audrey Kadjar Date: Mon, 19 Sep 2022 09:55:47 +0200 Subject: [PATCH 15/74] [#3725] Rendering of WhatsApp Cloud messages in the Conversation List Preview (#3726) * added preview for whatsapp * removed logs --- .../render/SourceMessagePreview.tsx | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lib/typescript/render/SourceMessagePreview.tsx b/lib/typescript/render/SourceMessagePreview.tsx index 33498a08c9..d1892694a9 100644 --- a/lib/typescript/render/SourceMessagePreview.tsx +++ b/lib/typescript/render/SourceMessagePreview.tsx @@ -52,6 +52,24 @@ export const SourceMessagePreview = (props: SourceMessagePreviewProps) => { return ; } + //Wa Location + if (lastMessageContent.type === 'location') { + return ( + <> + Location + + ); + } + + //Wa Contacts + if (lastMessageContent.type === 'contacts') { + return ( + <> + Contact shared + + ); + } + //google const googleLiveAgentRequest = lastMessageContent?.userStatus?.requestedLiveAgent; const googleSurveyResponse = lastMessageContent?.surveyResponse; @@ -173,6 +191,8 @@ export const SourceMessagePreview = (props: SourceMessagePreviewProps) => { } if ( + lastMessageContent?.type === 'image' || + lastMessageContent?.type === 'sticker' || lastMessageContent.message?.attachments?.[0].type === 'image' || lastMessageContent?.attachment?.type === 'image' || isImageFromGoogleSource(lastMessageContent.message?.text) || @@ -182,6 +202,7 @@ export const SourceMessagePreview = (props: SourceMessagePreviewProps) => { } if ( + lastMessageContent?.type === 'video' || lastMessageContent.message?.attachments?.[0].type === 'video' || lastMessageContent?.attachment?.type === 'video' || twilioWhatsAppInboundVideo @@ -190,6 +211,7 @@ export const SourceMessagePreview = (props: SourceMessagePreviewProps) => { } if ( + lastMessageContent?.type === 'audio' || lastMessageContent.message?.attachments?.[0].type === 'audio' || lastMessageContent?.attachment?.type === 'audio' || twilioWhatsAppInboundAudio @@ -198,6 +220,7 @@ export const SourceMessagePreview = (props: SourceMessagePreviewProps) => { } if ( + lastMessageContent?.type === 'document' || lastMessageContent.message?.attachments?.[0].type === 'file' || lastMessageContent?.attachment?.type === 'file' || twilioWhatsAppInboundFile From 948a00b3d9722f76094901747bc56f9c2d9a1894 Mon Sep 17 00:00:00 2001 From: Audrey Kadjar Date: Mon, 19 Sep 2022 09:56:04 +0200 Subject: [PATCH 16/74] [#3700] support the rendering of whatsapp cloud messages (#3721) * wa wip * refactored meta render lib and added types * rendering wip * templates added * added media messages for WA * WA location added * interactive wip * whatsapp interactive added * typing clean-up * finalized rendering * fixes * added key * fixes * last fixes * added check in case of wrong payloads * fixed imports * fix imports * import fixed * lint fiw * fixes * fix * fixed import * imports fix --- .../src/pages/Inbox/MessageInput/index.tsx | 3 + .../assets/images/icons/productList.svg | 5 + lib/typescript/assets/scss/colors.scss | 1 + .../CurrentLocation/index.module.scss | 21 ++ .../components/CurrentLocation/index.tsx | 40 ++++ .../render/components/Image/index.tsx | 2 +- .../render/components/Text/index.tsx | 1 + lib/typescript/render/components/index.ts | 1 + lib/typescript/render/outbound/facebook.ts | 2 +- lib/typescript/render/outbound/index.ts | 4 +- lib/typescript/render/outbound/whatsapp.ts | 4 +- .../facebookModel.ts => meta/MetaModel.ts} | 118 ++++++++++- .../MetaRender.tsx} | 190 ++++++++++++++++-- .../ButtonTemplate/index.module.scss | 0 .../components/ButtonTemplate/index.tsx | 2 +- .../components/Buttons/index.module.scss | 0 .../components/Buttons/index.tsx | 2 +- .../DeletedMessage/index.module.scss | 0 .../components/DeletedMessage/index.tsx | 0 .../FallbackAttachment/index.module.scss | 0 .../components/FallbackAttachment/index.tsx | 2 +- .../GenericTemplate/index.module.scss | 0 .../components/GenericTemplate/index.tsx | 4 +- .../InstagramShare/index.module.scss | 0 .../components/InstagramShare/index.tsx | 0 .../InstagramStoryMention/index.module.scss | 0 .../InstagramStoryMention/index.tsx | 0 .../InstagramStoryReplies/index.module.scss | 0 .../InstagramStoryReplies/index.tsx | 0 .../MediaTemplate/index.module.scss | 0 .../components/MediaTemplate/index.tsx | 2 +- .../components/QuickReplies/index.module.scss | 0 .../components/QuickReplies/index.tsx | 2 +- .../WhatsAppContacts/index.module.scss | 23 +++ .../components/WhatsAppContacts/index.tsx | 15 ++ .../WhatsAppInteractive/index.module.scss | 76 +++++++ .../components/WhatsAppInteractive/index.tsx | 81 ++++++++ .../WhatsAppMedia/index.module.scss | 35 ++++ .../meta/components/WhatsAppMedia/index.tsx | 66 ++++++ .../WhatsAppTemplate/index.module.scss | 80 ++++++++ .../components/WhatsAppTemplate/index.tsx | 85 ++++++++ .../render/providers/meta/components/index.ts | 14 ++ .../render/providers/twilio/TwilioRender.tsx | 2 +- .../components/CurrentLocation/index.tsx | 18 -- lib/typescript/render/renderProviders.ts | 7 +- .../render/services/mediaAttachments.ts | 3 + 46 files changed, 854 insertions(+), 57 deletions(-) create mode 100644 lib/typescript/assets/images/icons/productList.svg rename lib/typescript/render/{providers/twilio => }/components/CurrentLocation/index.module.scss (61%) create mode 100644 lib/typescript/render/components/CurrentLocation/index.tsx rename lib/typescript/render/providers/{facebook/facebookModel.ts => meta/MetaModel.ts} (63%) rename lib/typescript/render/providers/{facebook/FacebookRender.tsx => meta/MetaRender.tsx} (61%) rename lib/typescript/render/providers/{facebook => meta}/components/ButtonTemplate/index.module.scss (100%) rename lib/typescript/render/providers/{facebook => meta}/components/ButtonTemplate/index.tsx (86%) rename lib/typescript/render/providers/{facebook => meta}/components/Buttons/index.module.scss (100%) rename lib/typescript/render/providers/{facebook => meta}/components/Buttons/index.tsx (95%) rename lib/typescript/render/providers/{facebook => meta}/components/DeletedMessage/index.module.scss (100%) rename lib/typescript/render/providers/{facebook => meta}/components/DeletedMessage/index.tsx (100%) rename lib/typescript/render/providers/{facebook => meta}/components/FallbackAttachment/index.module.scss (100%) rename lib/typescript/render/providers/{facebook => meta}/components/FallbackAttachment/index.tsx (92%) rename lib/typescript/render/providers/{facebook => meta}/components/GenericTemplate/index.module.scss (100%) rename lib/typescript/render/providers/{facebook => meta}/components/GenericTemplate/index.tsx (86%) rename lib/typescript/render/providers/{facebook => meta}/components/InstagramShare/index.module.scss (100%) rename lib/typescript/render/providers/{facebook => meta}/components/InstagramShare/index.tsx (100%) rename lib/typescript/render/providers/{facebook => meta}/components/InstagramStoryMention/index.module.scss (100%) rename lib/typescript/render/providers/{facebook => meta}/components/InstagramStoryMention/index.tsx (100%) rename lib/typescript/render/providers/{facebook => meta}/components/InstagramStoryReplies/index.module.scss (100%) rename lib/typescript/render/providers/{facebook => meta}/components/InstagramStoryReplies/index.tsx (100%) rename lib/typescript/render/providers/{facebook => meta}/components/MediaTemplate/index.module.scss (100%) rename lib/typescript/render/providers/{facebook => meta}/components/MediaTemplate/index.tsx (91%) rename lib/typescript/render/providers/{facebook => meta}/components/QuickReplies/index.module.scss (100%) rename lib/typescript/render/providers/{facebook => meta}/components/QuickReplies/index.tsx (95%) create mode 100644 lib/typescript/render/providers/meta/components/WhatsAppContacts/index.module.scss create mode 100644 lib/typescript/render/providers/meta/components/WhatsAppContacts/index.tsx create mode 100644 lib/typescript/render/providers/meta/components/WhatsAppInteractive/index.module.scss create mode 100644 lib/typescript/render/providers/meta/components/WhatsAppInteractive/index.tsx create mode 100644 lib/typescript/render/providers/meta/components/WhatsAppMedia/index.module.scss create mode 100644 lib/typescript/render/providers/meta/components/WhatsAppMedia/index.tsx create mode 100644 lib/typescript/render/providers/meta/components/WhatsAppTemplate/index.module.scss create mode 100644 lib/typescript/render/providers/meta/components/WhatsAppTemplate/index.tsx create mode 100644 lib/typescript/render/providers/meta/components/index.ts delete mode 100644 lib/typescript/render/providers/twilio/components/CurrentLocation/index.tsx diff --git a/frontend/inbox/src/pages/Inbox/MessageInput/index.tsx b/frontend/inbox/src/pages/Inbox/MessageInput/index.tsx index 6fa9a28889..f17606e7bd 100644 --- a/frontend/inbox/src/pages/Inbox/MessageInput/index.tsx +++ b/frontend/inbox/src/pages/Inbox/MessageInput/index.tsx @@ -283,6 +283,9 @@ const MessageInput = (props: Props) => { case Source.viber: message.message = outboundMapper.getTextPayload(input); break; + case Source.whatsapp: + message.message = outboundMapper.getTextPayload(input); + break; } sendMessages(message).then(() => { diff --git a/lib/typescript/assets/images/icons/productList.svg b/lib/typescript/assets/images/icons/productList.svg new file mode 100644 index 0000000000..be185a9171 --- /dev/null +++ b/lib/typescript/assets/images/icons/productList.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/lib/typescript/assets/scss/colors.scss b/lib/typescript/assets/scss/colors.scss index 1030a9cc52..8489f3dc59 100644 --- a/lib/typescript/assets/scss/colors.scss +++ b/lib/typescript/assets/scss/colors.scss @@ -21,6 +21,7 @@ --color-airy-message-text-inbound: #000000; //colors that do not change with darkmode + --color-template-border-gray: #ccc; --color-disable-status-config: #737373; --color-dark-elements-gray: #98a4ab; --color-icons-gray: #a0abb2; diff --git a/lib/typescript/render/providers/twilio/components/CurrentLocation/index.module.scss b/lib/typescript/render/components/CurrentLocation/index.module.scss similarity index 61% rename from lib/typescript/render/providers/twilio/components/CurrentLocation/index.module.scss rename to lib/typescript/render/components/CurrentLocation/index.module.scss index 7c5701ae58..c78f2e8cc1 100644 --- a/lib/typescript/render/providers/twilio/components/CurrentLocation/index.module.scss +++ b/lib/typescript/render/components/CurrentLocation/index.module.scss @@ -19,12 +19,26 @@ color: var(--color-text-contrast); } +.contactContent a, +.contactContent a:hover, +.contactContent a:visited, +.contactContent a:active { + color: var(--color-text-contrast); +} + .memberContent { @extend .textMessage; background: var(--color-airy-blue); color: white; } +.memberContent a, +.memberContent a:hover, +.memberContent a:visited, +.memberContent a:active { + color: white; +} + .text { font-family: 'Lato', sans-serif; } @@ -32,3 +46,10 @@ .geolocation { @include font-s; } + +.geolocation a:link, +.geolocation a:active, +.geolocation a:visited, +.geolocation a:hover { + text-decoration: none; +} diff --git a/lib/typescript/render/components/CurrentLocation/index.tsx b/lib/typescript/render/components/CurrentLocation/index.tsx new file mode 100644 index 0000000000..8d0fa66b7e --- /dev/null +++ b/lib/typescript/render/components/CurrentLocation/index.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import {Emoji} from 'components'; +import styles from './index.module.scss'; + +interface CurrentLocationProps { + longitude: string; + latitude: string; + name?: string; + address?: string; + fromContact: boolean; +} + +export const CurrentLocation = ({longitude, latitude, name, address, fromContact}: CurrentLocationProps) => { + if (!longitude) longitude = 'N/A'; + if (!latitude) latitude = 'N/A'; + + return ( +
+

+ This user has shared its current location. +

+
+

+ + Latitude: {latitude}, Longitude: {longitude} + +

+ {name && ( +

+ {name}{' '} + {address && ( + + {address} + + )} +

+ )} +
+ ); +}; diff --git a/lib/typescript/render/components/Image/index.tsx b/lib/typescript/render/components/Image/index.tsx index 6221af0263..63faa2a2ae 100644 --- a/lib/typescript/render/components/Image/index.tsx +++ b/lib/typescript/render/components/Image/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {ImageWithFallback} from '../ImageWithFallback'; -import {ImageContent} from '../../providers/facebook/facebookModel'; +import {ImageContent} from '../../providers/meta/MetaModel'; import {Text} from '../../components/Text'; import styles from './index.module.scss'; diff --git a/lib/typescript/render/components/Text/index.tsx b/lib/typescript/render/components/Text/index.tsx index cbb37b5fff..6b8bac0691 100644 --- a/lib/typescript/render/components/Text/index.tsx +++ b/lib/typescript/render/components/Text/index.tsx @@ -13,6 +13,7 @@ export const Text = ({text, fromContact}: TextRenderProps) => ( className={`${fromContact ? styles.contactContent : styles.memberContent}`} options={{ defaultProtocol: 'https', + target: '_blank', className: `${styles.messageLink} ${fromContact ? styles.contactContent : styles.memberContent}`, }} > diff --git a/lib/typescript/render/components/index.ts b/lib/typescript/render/components/index.ts index a0325c8429..e1a21e581c 100644 --- a/lib/typescript/render/components/index.ts +++ b/lib/typescript/render/components/index.ts @@ -3,3 +3,4 @@ export * from './Image'; export * from './ImageWithFallback'; export * from './Text'; export * from './Video'; +export * from './CurrentLocation'; diff --git a/lib/typescript/render/outbound/facebook.ts b/lib/typescript/render/outbound/facebook.ts index 743e59ccf3..bc61daa6da 100644 --- a/lib/typescript/render/outbound/facebook.ts +++ b/lib/typescript/render/outbound/facebook.ts @@ -1,7 +1,7 @@ import {OutboundMapper} from './mapper'; import {getAttachmentType} from '../services'; -export class FacebookMapper extends OutboundMapper { +export class MetaMapper extends OutboundMapper { getTextPayload(text: string): any { return { text, diff --git a/lib/typescript/render/outbound/index.ts b/lib/typescript/render/outbound/index.ts index 7c1b0ed332..d23c11016b 100644 --- a/lib/typescript/render/outbound/index.ts +++ b/lib/typescript/render/outbound/index.ts @@ -1,4 +1,4 @@ -import {FacebookMapper} from './facebook'; +import {MetaMapper} from './facebook'; import {ChatpluginMapper} from './chatplugin'; import {GoogleMapper} from './google'; import {TwilioMapper} from './twilio'; @@ -9,7 +9,7 @@ export const getOutboundMapper = (source: string) => { switch (source) { case 'facebook': case 'instagram': - return new FacebookMapper(); + return new MetaMapper(); case 'google': return new GoogleMapper(); case 'chatplugin': diff --git a/lib/typescript/render/outbound/whatsapp.ts b/lib/typescript/render/outbound/whatsapp.ts index 4a39a52b19..82527e68ba 100644 --- a/lib/typescript/render/outbound/whatsapp.ts +++ b/lib/typescript/render/outbound/whatsapp.ts @@ -1,9 +1,9 @@ import {OutboundMapper} from './mapper'; export class WhatsAppMapper extends OutboundMapper { - getTextPayload(text: string): any { + getTextPayload(text: string): {text: string} { return { - Body: text, + text, }; } diff --git a/lib/typescript/render/providers/facebook/facebookModel.ts b/lib/typescript/render/providers/meta/MetaModel.ts similarity index 63% rename from lib/typescript/render/providers/facebook/facebookModel.ts rename to lib/typescript/render/providers/meta/MetaModel.ts index 4cb8c15e52..fbe4bef87a 100644 --- a/lib/typescript/render/providers/facebook/facebookModel.ts +++ b/lib/typescript/render/providers/meta/MetaModel.ts @@ -180,7 +180,116 @@ export interface DeletedMessageContent extends Content { type: 'deletedMessage'; } -// Add a new facebook content model here: +//WhatsApp Business Cloud + +//WA template +export interface WhatsAppTemplate extends Content { + type: 'whatsAppTemplate'; + components?: WhatsAppComponents[]; +} + +export interface WhatsAppComponents { + type: 'header' | 'body' | 'button'; + parameters: (WhatsAppParameter | WhatsAppButton)[]; + sub_type?: 'quick_reply' | 'url'; + index?: number; +} + +export interface WhatsAppParameter extends Content { + type: 'currency' | 'date_time' | 'document' | 'image' | 'text' | 'video'; + text?: string; + currency?: WhatsAppCurrency; + date_time?: WhatsAppDateTime; + document?: WhatsAppMedia; + image?: WhatsAppMedia; + video?: WhatsAppMedia; +} + +export interface WhatsAppCurrency { + fallback_value: string; + code: string; + amount_1000: string; +} + +export interface WhatsAppDateTime { + fallback_value: string; +} + +export interface WhatsAppButton extends Content { + type: 'button'; + parameters: [ + { + type: 'payload' | 'text'; + payload?: string; + text?: string; + } + ]; +} + +//WA Media +type WhatsAppMediaContent = { + link: string; + caption?: string; +}; + +export interface WhatsAppMediaInfo extends Content { + type: 'whatsAppMedia'; + mediaType: WhatsAppMediaType; +} + +type WhatsAppMedia = WhatsAppMediaInfo & WhatsAppMediaContent; + +export enum WhatsAppMediaType { + video = 'video', + image = 'image', + document = 'document', + audio = 'audio', + sticker = 'sticker', +} + +//WA Location +export interface WhatsAppLocation extends Content { + type: 'whatsAppLocation'; + longitude: string; + latitude: string; + name?: string; + address?: string; +} + +//WA Interactive +export interface WhatsAppInteractive extends Content { + type: 'whatsAppInteractive'; + action: WhatsAppInteractiveAction; + header?: WhatsAppInteractiveHeader; + body?: {text: string}; + footer?: {text: string}; +} + +export interface WhatsAppInteractiveAction { + button: string; + buttons: WhatsAppInteractiveButton[]; +} + +export interface WhatsAppInteractiveButton { + type: 'reply'; + reply: {title: string}; +} + +export interface WhatsAppInteractiveHeader { + type: 'text' | 'video' | 'image' | 'document'; + text?: string; + video?: WhatsAppMediaContent; + image?: WhatsAppMediaContent; + document?: WhatsAppMediaContent; +} + +//WA Contacts +export interface WhatsAppContacts extends Content { + type: 'whatsAppContacts'; + formattedName: string; +} + +// Add a new Meta content model here: export type ContentUnion = | TextContent | PostbackButton @@ -197,7 +306,12 @@ export type ContentUnion = | StoryRepliesContent | ShareContent | Fallback - | DeletedMessageContent; + | DeletedMessageContent + | WhatsAppTemplate + | WhatsAppMedia + | WhatsAppLocation + | WhatsAppInteractive + | WhatsAppContacts; export type AttachmentUnion = | TextContent diff --git a/lib/typescript/render/providers/facebook/FacebookRender.tsx b/lib/typescript/render/providers/meta/MetaRender.tsx similarity index 61% rename from lib/typescript/render/providers/facebook/FacebookRender.tsx rename to lib/typescript/render/providers/meta/MetaRender.tsx index 82ba5f4ba7..c35847f049 100644 --- a/lib/typescript/render/providers/facebook/FacebookRender.tsx +++ b/lib/typescript/render/providers/meta/MetaRender.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {RenderPropsUnion} from '../../props'; -import {Text, Image, Video, File} from '../../components'; +import {Text, Image, Video, File, CurrentLocation} from '../../components'; import {AudioClip} from 'components'; import {QuickReplies} from './components/QuickReplies'; import { @@ -10,19 +10,27 @@ import { ButtonAttachment, GenericAttachment, MediaAttachment, -} from './facebookModel'; -import {ButtonTemplate} from './components/ButtonTemplate'; -import {GenericTemplate} from './components/GenericTemplate'; -import {MediaTemplate} from './components/MediaTemplate'; -import {FallbackAttachment} from './components/FallbackAttachment'; -import {StoryMention} from './components/InstagramStoryMention'; -import {StoryReplies} from './components/InstagramStoryReplies'; -import {Share} from './components/InstagramShare'; -import {DeletedMessage} from './components/DeletedMessage'; - -export const FacebookRender = (props: RenderPropsUnion) => { + WhatsAppMediaType, +} from './MetaModel'; +import { + ButtonTemplate, + GenericTemplate, + MediaTemplate, + FallbackAttachment, + StoryMention, + StoryReplies, + Share, + DeletedMessage, + WhatsAppMedia, + WhatsAppTemplate, + WhatsAppInteractive, + WhatsAppContacts, +} from './components'; +import {Source} from 'model'; + +export const MetaRender = (props: RenderPropsUnion) => { const message = props.message; - const content = message.fromContact ? facebookInbound(message) : facebookOutbound(message); + const content = message.fromContact ? metaInbound(message) : metaOutbound(message); return render(content, props); }; @@ -93,6 +101,37 @@ function render(content: ContentUnion, props: RenderPropsUnion) { case 'deletedMessage': return ; + //Whatsapp Business Cloud + case 'whatsAppTemplate': + return ; + + case 'whatsAppMedia': + return ; + + case 'whatsAppLocation': + return ( + + ); + + case 'whatsAppInteractive': + return ( + + ); + + case 'whatsAppContacts': + return ; + default: return null; } @@ -174,11 +213,11 @@ const parseAttachment = ( return { type: 'text', - text: 'Unsupported message type', + text: JSON.stringify(attachment), }; }; -function facebookInbound(message): ContentUnion { +function metaInbound(message): ContentUnion { const messageJson = message.content.message ?? message.content; if (messageJson.attachment?.type === 'fallback' || messageJson.attachments?.[0].type === 'fallback') { @@ -236,17 +275,71 @@ function facebookInbound(message): ContentUnion { if (messageJson.text) { return { type: 'text', - text: messageJson.text, + text: messageJson?.text?.body ?? messageJson.text, }; } + //WhatsApp Business Cloud + if (message.source === Source.whatsapp) { + //Template + if (messageJson.type === 'template' && messageJson.template?.components) { + return { + type: 'whatsAppTemplate', + components: messageJson.template.components, + }; + } + + //Media + if (messageJson.type in WhatsAppMediaType) { + const media = messageJson.type; + return { + type: 'whatsAppMedia', + mediaType: media, + link: messageJson[media]?.link, + caption: messageJson[media]?.caption ?? null, + }; + } + + //Location + if (messageJson.type === 'location') { + return { + type: 'whatsAppLocation', + longitude: messageJson.location.longitude, + latitude: messageJson.location.latitude, + name: messageJson.location?.name, + address: messageJson.location?.address, + }; + } + + //Interactive + if (messageJson.type === 'interactive') { + const isActionRenderable = messageJson?.interactive?.action?.button || messageJson?.interactive?.action?.buttons; + const actionToBeRendered = isActionRenderable ? {...messageJson?.interactive?.action} : null; + return { + type: 'whatsAppInteractive', + action: actionToBeRendered, + header: messageJson.interactive?.header ?? null, + body: messageJson.interactive?.body ?? null, + footer: messageJson.interactive?.footer ?? null, + }; + } + + //Contacts + if (messageJson.type === 'contacts') { + return { + type: 'whatsAppContacts', + formattedName: messageJson?.contacts[0]?.name?.formatted_name, + }; + } + } + return { type: 'text', - text: 'Unsupported message type', + text: JSON.stringify(messageJson), }; } -function facebookOutbound(message): ContentUnion { +function metaOutbound(message): ContentUnion { const messageJson = message?.content?.message || message?.content || message; if (messageJson.quick_replies) { @@ -322,12 +415,69 @@ function facebookOutbound(message): ContentUnion { if (messageJson.text) { return { type: 'text', - text: messageJson.text, + text: messageJson?.text?.body ?? messageJson.text, }; } + //WhatsApp Business Cloud + if (message.source === Source.whatsapp) { + //Template + if (messageJson.type === 'template' && messageJson.template?.components) { + return { + type: 'whatsAppTemplate', + components: messageJson.template.components, + }; + } + + //Media + if (messageJson.type in WhatsAppMediaType) { + const media = messageJson.type; + + if (messageJson[media]?.link) { + return { + type: 'whatsAppMedia', + mediaType: media, + link: messageJson[media]?.link, + caption: messageJson[media]?.caption ?? null, + }; + } + } + + //Location + if (messageJson.type === 'location') { + return { + type: 'whatsAppLocation', + longitude: messageJson.location.longitude, + latitude: messageJson.location.latitude, + name: messageJson.location?.name, + address: messageJson.location?.address, + }; + } + + //Interactive + if (messageJson.type === 'interactive') { + const isActionRenderable = messageJson?.interactive?.action?.button || messageJson?.interactive?.action?.buttons; + const actionToBeRendered = isActionRenderable ? {...messageJson?.interactive?.action} : null; + return { + type: 'whatsAppInteractive', + action: actionToBeRendered, + header: messageJson.interactive?.header ?? null, + body: messageJson.interactive?.body ?? null, + footer: messageJson.interactive?.footer ?? null, + }; + } + + //Contacts + if (messageJson.type === 'contacts') { + return { + type: 'whatsAppContacts', + formattedName: messageJson?.contacts[0]?.name?.formatted_name, + }; + } + } + return { type: 'text', - text: 'Unsupported message type', + text: JSON.stringify(messageJson), }; } diff --git a/lib/typescript/render/providers/facebook/components/ButtonTemplate/index.module.scss b/lib/typescript/render/providers/meta/components/ButtonTemplate/index.module.scss similarity index 100% rename from lib/typescript/render/providers/facebook/components/ButtonTemplate/index.module.scss rename to lib/typescript/render/providers/meta/components/ButtonTemplate/index.module.scss diff --git a/lib/typescript/render/providers/facebook/components/ButtonTemplate/index.tsx b/lib/typescript/render/providers/meta/components/ButtonTemplate/index.tsx similarity index 86% rename from lib/typescript/render/providers/facebook/components/ButtonTemplate/index.tsx rename to lib/typescript/render/providers/meta/components/ButtonTemplate/index.tsx index 79f262b7f8..d347b4d5eb 100644 --- a/lib/typescript/render/providers/facebook/components/ButtonTemplate/index.tsx +++ b/lib/typescript/render/providers/meta/components/ButtonTemplate/index.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {Buttons} from '../Buttons'; -import {ButtonTemplate as ButtonTemplateModel} from '../../facebookModel'; +import {ButtonTemplate as ButtonTemplateModel} from '../../MetaModel'; import styles from './index.module.scss'; diff --git a/lib/typescript/render/providers/facebook/components/Buttons/index.module.scss b/lib/typescript/render/providers/meta/components/Buttons/index.module.scss similarity index 100% rename from lib/typescript/render/providers/facebook/components/Buttons/index.module.scss rename to lib/typescript/render/providers/meta/components/Buttons/index.module.scss diff --git a/lib/typescript/render/providers/facebook/components/Buttons/index.tsx b/lib/typescript/render/providers/meta/components/Buttons/index.tsx similarity index 95% rename from lib/typescript/render/providers/facebook/components/Buttons/index.tsx rename to lib/typescript/render/providers/meta/components/Buttons/index.tsx index 26e71a1a43..7c80e59a81 100644 --- a/lib/typescript/render/providers/facebook/components/Buttons/index.tsx +++ b/lib/typescript/render/providers/meta/components/Buttons/index.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {URLButton, PostbackButton, CallButton, LoginButton, LogoutButton, GamePlayButton} from '../../facebookModel'; +import {URLButton, PostbackButton, CallButton, LoginButton, LogoutButton, GamePlayButton} from '../../MetaModel'; import styles from './index.module.scss'; type ButtonsProps = { diff --git a/lib/typescript/render/providers/facebook/components/DeletedMessage/index.module.scss b/lib/typescript/render/providers/meta/components/DeletedMessage/index.module.scss similarity index 100% rename from lib/typescript/render/providers/facebook/components/DeletedMessage/index.module.scss rename to lib/typescript/render/providers/meta/components/DeletedMessage/index.module.scss diff --git a/lib/typescript/render/providers/facebook/components/DeletedMessage/index.tsx b/lib/typescript/render/providers/meta/components/DeletedMessage/index.tsx similarity index 100% rename from lib/typescript/render/providers/facebook/components/DeletedMessage/index.tsx rename to lib/typescript/render/providers/meta/components/DeletedMessage/index.tsx diff --git a/lib/typescript/render/providers/facebook/components/FallbackAttachment/index.module.scss b/lib/typescript/render/providers/meta/components/FallbackAttachment/index.module.scss similarity index 100% rename from lib/typescript/render/providers/facebook/components/FallbackAttachment/index.module.scss rename to lib/typescript/render/providers/meta/components/FallbackAttachment/index.module.scss diff --git a/lib/typescript/render/providers/facebook/components/FallbackAttachment/index.tsx b/lib/typescript/render/providers/meta/components/FallbackAttachment/index.tsx similarity index 92% rename from lib/typescript/render/providers/facebook/components/FallbackAttachment/index.tsx rename to lib/typescript/render/providers/meta/components/FallbackAttachment/index.tsx index 0bfdf6fffb..407ade5419 100644 --- a/lib/typescript/render/providers/facebook/components/FallbackAttachment/index.tsx +++ b/lib/typescript/render/providers/meta/components/FallbackAttachment/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {Text} from '../../../../components/Text'; -import {Fallback} from '../../facebookModel'; +import {Fallback} from '../../MetaModel'; import styles from './index.module.scss'; interface FallbackProps { diff --git a/lib/typescript/render/providers/facebook/components/GenericTemplate/index.module.scss b/lib/typescript/render/providers/meta/components/GenericTemplate/index.module.scss similarity index 100% rename from lib/typescript/render/providers/facebook/components/GenericTemplate/index.module.scss rename to lib/typescript/render/providers/meta/components/GenericTemplate/index.module.scss diff --git a/lib/typescript/render/providers/facebook/components/GenericTemplate/index.tsx b/lib/typescript/render/providers/meta/components/GenericTemplate/index.tsx similarity index 86% rename from lib/typescript/render/providers/facebook/components/GenericTemplate/index.tsx rename to lib/typescript/render/providers/meta/components/GenericTemplate/index.tsx index ea450ab60d..fa74c27871 100644 --- a/lib/typescript/render/providers/facebook/components/GenericTemplate/index.tsx +++ b/lib/typescript/render/providers/meta/components/GenericTemplate/index.tsx @@ -1,8 +1,8 @@ import React from 'react'; import {Carousel} from 'components'; -import {GenericTemplate as GenericTemplateModel} from '../../facebookModel'; -import {ImageWithFallback} from 'render/components/ImageWithFallback'; +import {GenericTemplate as GenericTemplateModel} from '../../MetaModel'; +import {ImageWithFallback} from '../../../../components'; import {Buttons} from '../Buttons'; import styles from './index.module.scss'; diff --git a/lib/typescript/render/providers/facebook/components/InstagramShare/index.module.scss b/lib/typescript/render/providers/meta/components/InstagramShare/index.module.scss similarity index 100% rename from lib/typescript/render/providers/facebook/components/InstagramShare/index.module.scss rename to lib/typescript/render/providers/meta/components/InstagramShare/index.module.scss diff --git a/lib/typescript/render/providers/facebook/components/InstagramShare/index.tsx b/lib/typescript/render/providers/meta/components/InstagramShare/index.tsx similarity index 100% rename from lib/typescript/render/providers/facebook/components/InstagramShare/index.tsx rename to lib/typescript/render/providers/meta/components/InstagramShare/index.tsx diff --git a/lib/typescript/render/providers/facebook/components/InstagramStoryMention/index.module.scss b/lib/typescript/render/providers/meta/components/InstagramStoryMention/index.module.scss similarity index 100% rename from lib/typescript/render/providers/facebook/components/InstagramStoryMention/index.module.scss rename to lib/typescript/render/providers/meta/components/InstagramStoryMention/index.module.scss diff --git a/lib/typescript/render/providers/facebook/components/InstagramStoryMention/index.tsx b/lib/typescript/render/providers/meta/components/InstagramStoryMention/index.tsx similarity index 100% rename from lib/typescript/render/providers/facebook/components/InstagramStoryMention/index.tsx rename to lib/typescript/render/providers/meta/components/InstagramStoryMention/index.tsx diff --git a/lib/typescript/render/providers/facebook/components/InstagramStoryReplies/index.module.scss b/lib/typescript/render/providers/meta/components/InstagramStoryReplies/index.module.scss similarity index 100% rename from lib/typescript/render/providers/facebook/components/InstagramStoryReplies/index.module.scss rename to lib/typescript/render/providers/meta/components/InstagramStoryReplies/index.module.scss diff --git a/lib/typescript/render/providers/facebook/components/InstagramStoryReplies/index.tsx b/lib/typescript/render/providers/meta/components/InstagramStoryReplies/index.tsx similarity index 100% rename from lib/typescript/render/providers/facebook/components/InstagramStoryReplies/index.tsx rename to lib/typescript/render/providers/meta/components/InstagramStoryReplies/index.tsx diff --git a/lib/typescript/render/providers/facebook/components/MediaTemplate/index.module.scss b/lib/typescript/render/providers/meta/components/MediaTemplate/index.module.scss similarity index 100% rename from lib/typescript/render/providers/facebook/components/MediaTemplate/index.module.scss rename to lib/typescript/render/providers/meta/components/MediaTemplate/index.module.scss diff --git a/lib/typescript/render/providers/facebook/components/MediaTemplate/index.tsx b/lib/typescript/render/providers/meta/components/MediaTemplate/index.tsx similarity index 91% rename from lib/typescript/render/providers/facebook/components/MediaTemplate/index.tsx rename to lib/typescript/render/providers/meta/components/MediaTemplate/index.tsx index 6f5c4a9cb0..541332f13a 100644 --- a/lib/typescript/render/providers/facebook/components/MediaTemplate/index.tsx +++ b/lib/typescript/render/providers/meta/components/MediaTemplate/index.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {MediaTemplate as MediaTemplateModel} from '../../facebookModel'; +import {MediaTemplate as MediaTemplateModel} from '../../MetaModel'; import {Buttons} from '../Buttons'; import styles from './index.module.scss'; diff --git a/lib/typescript/render/providers/facebook/components/QuickReplies/index.module.scss b/lib/typescript/render/providers/meta/components/QuickReplies/index.module.scss similarity index 100% rename from lib/typescript/render/providers/facebook/components/QuickReplies/index.module.scss rename to lib/typescript/render/providers/meta/components/QuickReplies/index.module.scss diff --git a/lib/typescript/render/providers/facebook/components/QuickReplies/index.tsx b/lib/typescript/render/providers/meta/components/QuickReplies/index.tsx similarity index 95% rename from lib/typescript/render/providers/facebook/components/QuickReplies/index.tsx rename to lib/typescript/render/providers/meta/components/QuickReplies/index.tsx index c9fb56d246..f3fc5e5ad6 100644 --- a/lib/typescript/render/providers/facebook/components/QuickReplies/index.tsx +++ b/lib/typescript/render/providers/meta/components/QuickReplies/index.tsx @@ -3,7 +3,7 @@ import React from 'react'; import {Text} from '../../../../components/Text'; import {Video} from '../../../../components/Video'; import {Image} from '../../../../components/Image'; -import {QuickReply, AttachmentUnion} from '../../facebookModel'; +import {QuickReply, AttachmentUnion} from '../../MetaModel'; import {ImageWithFallback} from 'render/components/ImageWithFallback'; import styles from './index.module.scss'; diff --git a/lib/typescript/render/providers/meta/components/WhatsAppContacts/index.module.scss b/lib/typescript/render/providers/meta/components/WhatsAppContacts/index.module.scss new file mode 100644 index 0000000000..b5e9d96179 --- /dev/null +++ b/lib/typescript/render/providers/meta/components/WhatsAppContacts/index.module.scss @@ -0,0 +1,23 @@ +@import 'assets/scss/fonts.scss'; +@import 'assets/scss/colors.scss'; + +.contactsWrapper { + max-width: 300px; + padding: 10px; + display: flex; + align-items: center; + border-radius: 12px; + background-color: var(--color-background-white); + border: 1px solid var(--color-template-border-gray); +} + +.contactsWrapper img { + width: 50px; +} + +.contactsWrapper h1 { + margin-left: 10px; + @include font-s-bold; + color: var(--color-text-contrast); + word-break: break-word; +} diff --git a/lib/typescript/render/providers/meta/components/WhatsAppContacts/index.tsx b/lib/typescript/render/providers/meta/components/WhatsAppContacts/index.tsx new file mode 100644 index 0000000000..8ccd0ade41 --- /dev/null +++ b/lib/typescript/render/providers/meta/components/WhatsAppContacts/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import styles from './index.module.scss'; + +interface WhatsAppContactsProps { + formattedName: string; +} + +export const WhatsAppContacts = ({formattedName}: WhatsAppContactsProps) => { + return ( +
+ shared contact +

{formattedName ?? 'N/A'}

+
+ ); +}; diff --git a/lib/typescript/render/providers/meta/components/WhatsAppInteractive/index.module.scss b/lib/typescript/render/providers/meta/components/WhatsAppInteractive/index.module.scss new file mode 100644 index 0000000000..0cd190f8fa --- /dev/null +++ b/lib/typescript/render/providers/meta/components/WhatsAppInteractive/index.module.scss @@ -0,0 +1,76 @@ +@import 'assets/scss/fonts.scss'; +@import 'assets/scss/colors.scss'; + +.interactiveContent { + width: 200px; + max-width: 500px; + border-radius: 12px; + background-color: var(--color-background-white); + border: 1px solid var(--color-template-border-gray); + @include font-base; +} + +.header { + width: 100%; + padding: 10px 20px 10px 10px; + color: var(--color-text-contrast); + border-top-right-radius: 12px; + border-top-left-radius: 12px; +} + +.header img, +.header video { + max-width: 150px; + border-radius: 12px; +} + +.header h1 { + font-weight: bold; +} + +.body { + color: var(--color-text-contrast); + padding: 5px 20px 5px 10px; +} + +.footer { + color: var(--color-text-gray); + padding: 5px 0px 5px 10px; +} + +.footerLink, +.footerLink:active, +.footerLink:visited, +.footerLink:hover { + color: var(--color-text-gray); + text-decoration: none; +} + +.actionButton { + width: 100%; + padding: 10px; + display: flex; + justify-content: center; + align-items: center; + border-top: 1px solid var(--color-template-border-gray); +} +.actionButton svg { + fill: var(--color-elements-blue); + margin-right: 4px; +} + +.actionButton h2, +.actionReplyButton h2 { + color: var(--color-elements-blue); +} + +.actionReplyButton { + max-width: 500px; + padding: 5px; + margin-top: 5px; + background-color: var(--color-background-white); + border-radius: 12px; + border: 1px solid var(--color-template-border-gray); + text-align: center; + @include font-base; +} diff --git a/lib/typescript/render/providers/meta/components/WhatsAppInteractive/index.tsx b/lib/typescript/render/providers/meta/components/WhatsAppInteractive/index.tsx new file mode 100644 index 0000000000..28a1b5c25a --- /dev/null +++ b/lib/typescript/render/providers/meta/components/WhatsAppInteractive/index.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import Linkify from 'linkify-react'; +import ReactMarkdown from 'react-markdown'; +import {WhatsAppInteractiveHeader, WhatsAppMediaType, WhatsAppInteractiveAction} from '../../MetaModel'; +import {WhatsAppMediaContent} from '../WhatsAppMedia'; +import {ReactComponent as ProductListIcon} from 'assets/images/icons/productList.svg'; +import styles from './index.module.scss'; + +type WhatsAppInteractiveProps = { + action: WhatsAppInteractiveAction; + header?: WhatsAppInteractiveHeader; + body?: {text: string}; + footer?: {text: string}; +}; + +export const WhatsAppInteractive = ({action, header, body, footer}: WhatsAppInteractiveProps) => { + return ( +
+
+ {header && ( +
+ {header && header.type in WhatsAppMediaType && ( + + )} + {header && header.type === 'text' &&

{header.text}

} +
+ )} + + {body && ( +
+ + {body.text} + +
+ )} + + {footer && ( +
+ {footer.text.startsWith('https') || footer.text.startsWith('http') ? ( + + {footer.text} + + ) : ( + + {footer.text} + + )} +
+ )} + + {action && action?.button && ( +
+ +

{action.button}

+
+ )} +
+ + {action && + action?.buttons && + action.buttons.map(replyButton => { + return ( +
+

{replyButton.reply.title}

+
+ ); + })} +
+ ); +}; diff --git a/lib/typescript/render/providers/meta/components/WhatsAppMedia/index.module.scss b/lib/typescript/render/providers/meta/components/WhatsAppMedia/index.module.scss new file mode 100644 index 0000000000..04b5c33af9 --- /dev/null +++ b/lib/typescript/render/providers/meta/components/WhatsAppMedia/index.module.scss @@ -0,0 +1,35 @@ +@import 'assets/scss/fonts.scss'; +@import 'assets/scss/colors.scss'; + +.mediaWrapper { + max-width: 500px; + border-radius: 12px; + padding: 10px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + background-color: var(--color-background-white); + border: 1px solid var(--color-template-border-gray); +} + +.mediaWrapper :first-child { + margin-top: 0; +} + +.mediaWrapper img, +.mediaWrapper video { + max-width: 200px; + border-radius: 12px; +} + +.caption { + @include font-xs; + margin-top: 5px; +} + +.imageMedia { + display: block; + border-radius: 12px; + max-width: 300px; +} diff --git a/lib/typescript/render/providers/meta/components/WhatsAppMedia/index.tsx b/lib/typescript/render/providers/meta/components/WhatsAppMedia/index.tsx new file mode 100644 index 0000000000..550c1c50f2 --- /dev/null +++ b/lib/typescript/render/providers/meta/components/WhatsAppMedia/index.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import {WhatsAppMediaType} from '../../MetaModel'; +import {ImageWithFallback, Video, File} from '../../../../components'; +import {AudioClip} from 'components'; +import styles from './index.module.scss'; + +interface WhatsAppMediaProps { + mediaType: WhatsAppMediaType; + link: string; + caption?: string; +} + +const defaultText = 'N/A'; + +//Caption: only use for document, image, or video media +//https://developers.facebook.com/docs/whatsapp/cloud-api/reference/messages#media-object + +export const WhatsAppMedia = ({mediaType, link, caption}: WhatsAppMediaProps) => { + if (caption && mediaType !== 'audio') { + return ( +
+ +
+ ); + } else { + return ; + } +}; + +export const WhatsAppMediaContent = ({mediaType, link, caption}: WhatsAppMediaProps) => { + let mediaContent =

{defaultText}

; + + if (mediaType === 'image' || mediaType === 'sticker') { + const isCaptionRenderable = caption && mediaType === 'image'; + mediaContent = ( + <> + {' '} + {isCaptionRenderable ?

{caption}

: null} + + ); + } + + if (mediaType === WhatsAppMediaType.video) { + mediaContent = ( + <> +
); }; @@ -227,7 +227,11 @@ const CatalogItemDetails = (props: ConnectedProps) => { {t('uninstall')} ) : ( - )} diff --git a/frontend/control-center/src/pages/Connectors/ChannelCard/index.tsx b/frontend/control-center/src/pages/Connectors/ChannelCard/index.tsx index 47aa626df3..c010e39436 100644 --- a/frontend/control-center/src/pages/Connectors/ChannelCard/index.tsx +++ b/frontend/control-center/src/pages/Connectors/ChannelCard/index.tsx @@ -3,11 +3,11 @@ import styles from './index.module.scss'; import {getChannelAvatar} from '../../../components/ChannelAvatar'; import {useNavigate} from 'react-router-dom'; import {ReactComponent as ArrowRightIcon} from 'assets/images/icons/arrowRight.svg'; -import {CONNECTORS_ROUTE} from '../../../routes/routes'; import {useTranslation} from 'react-i18next'; import {ConfigStatusButton} from '../ConfigStatusButton'; import {ComponentStatus, ConnectorCardComponentInfo} from '..'; import {cyAddChannelButton} from 'handles'; +import {getConnectedRouteForComponent, getNewChannelRouteForComponent} from '../../../services'; type ChannelCardProps = { componentInfo: ConnectorCardComponentInfo; @@ -19,11 +19,25 @@ export const ChannelCard = (props: ChannelCardProps) => { const {componentInfo, channelsToShow, componentStatus} = props; const {t} = useTranslation(); const navigate = useNavigate(); + const CONFIGURATION_ROUTE = getConnectedRouteForComponent( + componentInfo.source, + componentInfo.isChannel, + componentStatus !== ComponentStatus.notConfigured + ); + + const CONFIGURATION_ROUTE_NEW = getNewChannelRouteForComponent( + componentInfo.source, + componentInfo?.isChannel, + componentStatus !== ComponentStatus.notConfigured + ); return (
{ - event.stopPropagation(), navigate(CONNECTORS_ROUTE + '/' + componentInfo.source + '/connected'); + event.stopPropagation(), + channelsToShow > 0 + ? navigate(CONFIGURATION_ROUTE, {state: {from: 'connected'}}) + : navigate(CONFIGURATION_ROUTE_NEW, {state: {from: 'new'}}); }} className={styles.container} data-cy={cyAddChannelButton} @@ -34,7 +48,9 @@ export const ChannelCard = (props: ChannelCardProps) => { {componentInfo.displayName}
- {componentStatus && } + {componentStatus && ( + + )} {channelsToShow} {channelsToShow === 1 ? t('channel') : t('channels')} diff --git a/frontend/control-center/src/pages/Connectors/ConfigStatusButton/index.tsx b/frontend/control-center/src/pages/Connectors/ConfigStatusButton/index.tsx index 4ae53fd8f1..bd6e5c2dfb 100644 --- a/frontend/control-center/src/pages/Connectors/ConfigStatusButton/index.tsx +++ b/frontend/control-center/src/pages/Connectors/ConfigStatusButton/index.tsx @@ -25,7 +25,7 @@ export const ConfigStatusButton = (props: ConfigStatusButtonProps) => { break; case ComponentStatus.notConfigured: event.stopPropagation(); - navigate(configurationRoute); + configurationRoute && navigate(configurationRoute, {state: {from: 'connectors'}}); break; default: break; diff --git a/frontend/control-center/src/pages/Connectors/ConfigureConnector/SetConfigInputs/SetConfigInputs.module.scss b/frontend/control-center/src/pages/Connectors/ConfigureConnector/SetConfigInputs/SetConfigInputs.module.scss new file mode 100644 index 0000000000..8dd7cf3997 --- /dev/null +++ b/frontend/control-center/src/pages/Connectors/ConfigureConnector/SetConfigInputs/SetConfigInputs.module.scss @@ -0,0 +1,7 @@ +.addColumn { + display: flex; +} + +.input { + height: 80px; +} diff --git a/frontend/control-center/src/pages/Connectors/ConfigureConnector/SetConfigInputs/SetConfigInputs.tsx b/frontend/control-center/src/pages/Connectors/ConfigureConnector/SetConfigInputs/SetConfigInputs.tsx new file mode 100644 index 0000000000..0ac55c7bee --- /dev/null +++ b/frontend/control-center/src/pages/Connectors/ConfigureConnector/SetConfigInputs/SetConfigInputs.tsx @@ -0,0 +1,61 @@ +import {Input} from 'components'; +import {Source} from 'model'; +import React, {Dispatch, SetStateAction, useEffect, useState} from 'react'; +import {useTranslation} from 'react-i18next'; +import styles from './SetConfigInputs.module.scss'; + +type SetConfigInputsProps = { + configurationValues: {[key: string]: string}; + setConfig: Dispatch>; + storedConfig: {}; + source: string; +}; + +export const SetConfigInputs = (props: SetConfigInputsProps) => { + const {configurationValues, storedConfig, source} = props; + const [input, setInput] = useState(storedConfig || configurationValues); + const {t} = useTranslation(); + const inputArr: React.ReactElement[] = []; + + useEffect(() => { + props.setConfig(input); + }, [input]); + + source !== Source.chatPlugin && + Object.entries(configurationValues).forEach((item, index) => { + const key = item[0]; + const keyTyped = key as keyof typeof input; + const valueTyped = input[keyTyped] || ''; + const toolTip = key.charAt(0).toUpperCase() + key.slice(1); + const replacedKey = key.replace(/([A-Z])/g, ' $1'); + const label = replacedKey.charAt(0).toUpperCase() + replacedKey.slice(1); + const placeholder = `${replacedKey.charAt(0).toUpperCase() + replacedKey.slice(1)}`; + const capitalSource = source?.charAt(0).toUpperCase() + source?.slice(1).replace('.', ''); + const isUrl = label.includes('Url'); + const hasSteps = source === Source.dialogflow && replacedKey.includes('Level'); + const stepPlaceholder = `0.1 ${t('to')} 0.9`; + + inputArr.push( +
+ ) => setInput({...input, [keyTyped]: e.target.value})} + label={label} + placeholder={hasSteps ? stepPlaceholder : placeholder} + showLabelIcon + tooltipText={t(`inputTooltip${capitalSource}${toolTip}`)} + required={valueTyped !== 'optionalString'} + height={32} + fontClass="font-base" + /> +
+ ); + }); + + return <>{inputArr}; +}; diff --git a/frontend/control-center/src/pages/Connectors/ConfigureConnector/index.module.scss b/frontend/control-center/src/pages/Connectors/ConfigureConnector/index.module.scss index 1845feb843..b87f14cde2 100644 --- a/frontend/control-center/src/pages/Connectors/ConfigureConnector/index.module.scss +++ b/frontend/control-center/src/pages/Connectors/ConfigureConnector/index.module.scss @@ -1,3 +1,25 @@ +@import 'assets/scss/fonts.scss'; +@import 'assets/scss/colors.scss'; + +.formRow { + height: 80px; + margin-bottom: 16px; + + label { + @include font-s; + } + + &:hover { + .actionToolTip { + display: block; + } + } + + button { + margin-top: 16px; + } +} + .formWrapper { align-self: center; } diff --git a/frontend/control-center/src/pages/Connectors/ConfigureConnector/index.tsx b/frontend/control-center/src/pages/Connectors/ConfigureConnector/index.tsx index a615d5ea73..4345731464 100644 --- a/frontend/control-center/src/pages/Connectors/ConfigureConnector/index.tsx +++ b/frontend/control-center/src/pages/Connectors/ConfigureConnector/index.tsx @@ -1,51 +1,102 @@ -import React, {SetStateAction} from 'react'; +import React, {useState} from 'react'; import RestartPopUp from '../RestartPopUp'; import {SmartButton} from 'components'; import {cyConnectorAddButton} from 'handles'; import {useTranslation} from 'react-i18next'; import styles from './index.module.scss'; +import {connect, ConnectedProps} from 'react-redux'; +import {StateModel} from '../../../reducers'; +import {SetConfigInputs} from './SetConfigInputs/SetConfigInputs'; +import {removePrefix} from '../../../services'; +import {updateConnectorConfiguration} from '../../../actions'; +import {UpdateComponentConfigurationRequestPayload} from 'httpclient/src'; -interface ConfigureConnectorProps { - children: JSX.Element[] | JSX.Element; +const mapStateToProps = (state: StateModel) => { + return { + config: state.data.connector, + }; +}; + +const mapDispatchToProps = { + updateConnectorConfiguration, +}; + +const connector = connect(mapStateToProps, mapDispatchToProps); + +type ConfigureConnectorProps = { componentName: string; - isUpdateModalVisible: boolean; - setIsUpdateModalVisible: React.Dispatch>; - enableSubmitConfigData: () => void; - disabled: boolean; + isEnabled: boolean; isConfigured: boolean; - updateConfig: (e: React.FormEvent) => void; - isPending: boolean; -} - -export const ConfigureConnector = ({ - children, - componentName, - isUpdateModalVisible, - setIsUpdateModalVisible, - enableSubmitConfigData, - disabled, - isConfigured, - updateConfig, - isPending, -}: ConfigureConnectorProps) => { + configValues: {[key: string]: string}; + source: string; +} & ConnectedProps; + +const ConfigureConnector = (props: ConfigureConnectorProps) => { + const {componentName, isConfigured, configValues, isEnabled, updateConnectorConfiguration, source} = props; const {t} = useTranslation(); + const displayName = componentName && removePrefix(componentName); + const [config, setConfig] = useState(configValues); + const [isPending, setIsPending] = useState(false); + const [isUpdateModalVisible, setIsUpdateModalVisible] = useState(false); + + const updateConfig = (event: React.FormEvent) => { + event.preventDefault(); + if (isEnabled) { + setIsUpdateModalVisible(true); + } else { + enableSubmitConfigData(); + } + }; + + const enableSubmitConfigData = () => { + config && createNewConnection(config); + }; + + const createNewConnection = (configurationValues: {}) => { + setIsPending(true); + + const payload: UpdateComponentConfigurationRequestPayload = { + components: [ + { + name: componentName && removePrefix(componentName), + enabled: true, + data: configurationValues, + }, + ], + }; + + updateConnectorConfiguration(payload) + .catch((error: Error) => { + console.error(error); + }) + .finally(() => { + setIsPending(false); + }); + }; return (
- {children} - updateConfig(e)} - dataCy={cyConnectorAddButton} - /> +
+ + updateConfig(e)} + dataCy={cyConnectorAddButton} + /> +
{isUpdateModalVisible && ( @@ -58,3 +109,5 @@ export const ConfigureConnector = ({
); }; + +export default connector(ConfigureConnector); diff --git a/frontend/control-center/src/pages/Connectors/ConnectorConfig/index.module.scss b/frontend/control-center/src/pages/Connectors/ConnectorConfig/index.module.scss index 5e75717245..85410c51ac 100644 --- a/frontend/control-center/src/pages/Connectors/ConnectorConfig/index.module.scss +++ b/frontend/control-center/src/pages/Connectors/ConnectorConfig/index.module.scss @@ -12,6 +12,7 @@ .channelsLineItems { display: flex; + flex-direction: row; } .activeItem { diff --git a/frontend/control-center/src/pages/Connectors/ConnectorConfig/index.tsx b/frontend/control-center/src/pages/Connectors/ConnectorConfig/index.tsx index dcd2680467..87887dc9da 100644 --- a/frontend/control-center/src/pages/Connectors/ConnectorConfig/index.tsx +++ b/frontend/control-center/src/pages/Connectors/ConnectorConfig/index.tsx @@ -1,49 +1,24 @@ import React, {useState, useEffect, useRef, useLayoutEffect} from 'react'; -import {connect, ConnectedProps, useSelector} from 'react-redux'; +import {connect, ConnectedProps} from 'react-redux'; import {useTranslation} from 'react-i18next'; -import {useParams} from 'react-router-dom'; +import {useLocation, useNavigate, useParams} from 'react-router-dom'; import {StateModel} from '../../../reducers'; import {useCurrentComponentForSource} from '../../../selectors'; -import { - connectChatPlugin, - updateChannel, - disconnectChannel, - updateConnectorConfiguration, - enableDisableComponent, - getConnectorsConfiguration, - listComponents, -} from '../../../actions'; -import {UpdateComponentConfigurationRequestPayload} from 'httpclient/src'; +import {getConnectorsConfiguration, listComponents} from '../../../actions'; import {Source} from 'model'; - import ChatPluginConnect from '../Providers/Airy/ChatPlugin/ChatPluginConnect'; import FacebookConnect from '../Providers/Facebook/Messenger/FacebookConnect'; import InstagramConnect from '../Providers/Instagram/InstagramConnect'; import GoogleConnect from '../Providers/Google/GoogleConnect'; import TwilioSmsConnect from '../Providers/Twilio/SMS/TwilioSmsConnect'; import TwilioWhatsappConnect from '../Providers/Twilio/WhatsApp/TwilioWhatsappConnect'; -import {DialogflowConnect} from '../Providers/Dialogflow/DialogflowConnect'; -import {ConnectNewZendesk} from '../Providers/Zendesk/ConnectNewZendesk'; -import {ConnectNewSalesforce} from '../Providers/Salesforce/ConnectNewSalesforce'; -import {RasaConnect} from '../Providers/Rasa/RasaConnect'; -import {WhatsappBusinessCloudConnect} from '../Providers/WhatsappBusinessCloud/WhatsappBusinessCloudConnect'; - import ConnectedChannelsList from '../ConnectedChannelsList'; import {removePrefix} from '../../../services'; import styles from './index.module.scss'; - -export enum Pages { - createUpdate = 'create-update', - customization = 'customization', - install = 'install', -} +import ConfigureConnector from '../ConfigureConnector'; +import {CONNECTORS_ROUTE} from '../../../routes/routes'; const mapDispatchToProps = { - connectChatPlugin, - updateChannel, - disconnectChannel, - updateConnectorConfiguration, - enableDisableComponent, getConnectorsConfiguration, listComponents, }; @@ -51,34 +26,44 @@ const mapDispatchToProps = { const mapStateToProps = (state: StateModel) => ({ components: state.data.config.components, catalog: state.data.catalog, + connectors: state.data.connector, }); +type LocationState = { + from: string; +}; + const connector = connect(mapStateToProps, mapDispatchToProps); const ConnectorConfig = (props: ConnectedProps) => { - const {components, catalog, updateConnectorConfiguration, getConnectorsConfiguration, listComponents} = props; + const {components, catalog, connectors, getConnectorsConfiguration, listComponents} = props; const params = useParams(); + const navigate = useNavigate(); + const location = useLocation(); const {channelId, source} = params; const newChannel = params['*'] === 'new'; - const connectedParams = params['*'] === 'connected'; - - const connectors = useSelector((state: StateModel) => state.data.connector); + const connectedChannels = params['*'] === 'connected'; + const configurePath = params['*'] === 'configure'; const connectorInfo = useCurrentComponentForSource(source as Source); - - const [currentPage] = useState(Pages.createUpdate); const [isEnabled, setIsEnabled] = useState(null); - const [isPending, setIsPending] = useState(false); const [isConfigured, setIsConfigured] = useState(false); const [lineTitle, setLineTitle] = useState(''); - + const [lineTitleRoute, setLineTitleRoute] = useState(''); + const configValues = connectorInfo.source === source && connectorInfo.configurationValues; + const parsedConfigValues = configValues && JSON.parse(configValues); const pageContentRef = useRef(null); const [offset, setOffset] = useState(pageContentRef?.current?.offsetTop); - const {t} = useTranslation(); - const isAiryInternalConnector = source === Source.chatPlugin; const isCatalogList = Object.entries(catalog).length > 0; + const previousPath = (location.state as LocationState)?.from; + const currentPath = params['*']; + const navigateNew = `${CONNECTORS_ROUTE}/${source}/new`; + const navigateConnected = `${CONNECTORS_ROUTE}/${source}/connected`; + const navigateConfigure = `${CONNECTORS_ROUTE}/${source}/configure`; + const navigateChannelId = `${CONNECTORS_ROUTE}/${source}/${channelId || previousPath}`; + const notConfigured = previousPath === 'connectors' || previousPath === 'status' || previousPath === 'catalog'; useLayoutEffect(() => { setOffset(pageContentRef?.current?.offsetTop); @@ -91,7 +76,7 @@ const ConnectorConfig = (props: ConnectedProps) => { }, []); useEffect(() => { - if (connectorInfo) determineLineTitle(connectorInfo.isChannel); + if (connectorInfo) determineLineTitle(); }, [connectorInfo]); useEffect(() => { @@ -112,119 +97,58 @@ const ConnectorConfig = (props: ConnectedProps) => { setIsEnabled(components[removePrefix(connectorInfo.name)]?.enabled); }, [connectorInfo, components]); - const determineLineTitle = (connectorHasChannels: undefined | string) => { + const determineLineTitle = () => { const newAiryChatPluginPage = newChannel && source === Source.chatPlugin; - const newChannelPage = newChannel && connectorHasChannels; + + if (channelId || previousPath?.includes('-')) { + setLineTitle(t('update')); + setLineTitleRoute(navigateChannelId); + return; + } if (newAiryChatPluginPage) { setLineTitle(t('create')); return; } - if (newChannelPage) { + if (newChannel || previousPath === 'new') { setLineTitle(t('addChannel')); + setLineTitleRoute(navigateNew); return; } - if (connectedParams) { + if (connectedChannels || previousPath === 'connected') { setLineTitle(t('channelsCapital')); + setLineTitleRoute(navigateConnected); return; } - setLineTitle(t('configuration')); - }; - - const createNewConnection = (configurationValues: {[key: string]: string}) => { - setIsPending(true); - - const payload: UpdateComponentConfigurationRequestPayload = { - components: [ - { - name: connectorInfo && removePrefix(connectorInfo.name), - enabled: true, - data: configurationValues, - }, - ], - }; - - updateConnectorConfiguration(payload) - .catch((error: Error) => { - console.error(error); - }) - .finally(() => { - setIsPending(false); - }); + if (configurePath) { + setLineTitle(t('configuration')); + return; + } }; const PageContent = () => { - if (newChannel || channelId) { - if (source === Source.dialogflow) { - return ( - - ); - } - - if (source === Source.zendesk) { - return ( - - ); - } - - if (source === Source.salesforce) { - return ( - - ); - } - - if (source === Source.rasa) { - return ( - - ); - } - - if (source === Source.whatsapp) { - return ( - - ); - } + if (configurePath) { + return ( + + ); + } + if (channelId || newChannel) { if (source === Source.chatPlugin) return ; - if (source === Source.facebook) return ; - if (source === Source.instagram) return ; - if (source === Source.google) return ; - if (source === Source.twilioSMS) return ; - if (source === Source.twilioWhatsApp) return ; - if (source === Source.viber) return

{t('pageUnderConstruction')}

; } @@ -236,9 +160,26 @@ const ConnectorConfig = (props: ConnectedProps) => { {!(source === Source.chatPlugin && (newChannel || channelId)) && (
- - {lineTitle} - + {!notConfigured && ( + previousPath && navigate(lineTitleRoute, {state: {from: currentPath}})} + > + {lineTitle} + + )} + {((source !== Source.chatPlugin && connectorInfo.isChannel) || notConfigured) && ( + !configurePath && navigate(navigateConfigure, {state: {from: currentPath}})} + > + {t('configuration')} + + )}
diff --git a/frontend/control-center/src/pages/Connectors/ConnectorWrapper/index.tsx b/frontend/control-center/src/pages/Connectors/ConnectorWrapper/index.tsx index 8b383dcea9..88d9c71357 100644 --- a/frontend/control-center/src/pages/Connectors/ConnectorWrapper/index.tsx +++ b/frontend/control-center/src/pages/Connectors/ConnectorWrapper/index.tsx @@ -4,16 +4,9 @@ import {useTranslation} from 'react-i18next'; import {Link, useParams} from 'react-router-dom'; import {Button, NotificationComponent, SettingsModal, SmartButton} from 'components'; import {StateModel} from '../../../reducers'; -import { - connectChatPlugin, - updateChannel, - disconnectChannel, - enableDisableComponent, - getConnectorsConfiguration, - listComponents, -} from '../../../actions'; +import {enableDisableComponent, getConnectorsConfiguration, listComponents} from '../../../actions'; import {LinkButton, InfoButton} from 'components'; -import {NotificationModel, Source, ComponentInfo} from 'model'; +import {NotificationModel, Source, ComponentInfo, Channel} from 'model'; import {ConfigStatusButton} from '../ConfigStatusButton'; import {getComponentStatus, removePrefix} from '../../../services'; import {DescriptionComponent, getDescriptionSourceName, getChannelAvatar} from '../../../components'; @@ -21,11 +14,9 @@ import {CONNECTORS_ROUTE} from '../../../routes/routes'; import {ReactComponent as CheckmarkIcon} from 'assets/images/icons/checkmarkFilled.svg'; import {ReactComponent as ArrowLeftIcon} from 'assets/images/icons/leftArrowCircle.svg'; import styles from './index.module.scss'; +import {allChannelsConnected} from '../../../selectors'; const mapDispatchToProps = { - connectChatPlugin, - updateChannel, - disconnectChannel, enableDisableComponent, getConnectorsConfiguration, listComponents, @@ -35,6 +26,7 @@ const mapStateToProps = (state: StateModel) => ({ config: state.data.config, components: state.data.config.components, catalog: state.data.catalog, + connectors: state.data.connector, }); const connector = connect(mapStateToProps, mapDispatchToProps); @@ -44,12 +36,21 @@ type ConnectorWrapperProps = { } & ConnectedProps; const ConnectorWrapper = (props: ConnectorWrapperProps) => { - const {components, catalog, enableDisableComponent, getConnectorsConfiguration, listComponents, config, Outlet} = - props; + const { + components, + catalog, + connectors, + enableDisableComponent, + getConnectorsConfiguration, + listComponents, + config, + Outlet, + } = props; - const connectors = useSelector((state: StateModel) => state.data.connector); const [connectorInfo, setConnectorInfo] = useState(null); const componentName = connectorInfo && removePrefix(connectorInfo?.name); + const channels = useSelector((state: StateModel) => Object.values(allChannelsConnected(state))); + const channelsBySource = (Source: Source) => channels.filter((channel: Channel) => channel?.source === Source); const [configurationModal, setConfigurationModal] = useState(false); const [notification, setNotification] = useState(null); const [isEnabled, setIsEnabled] = useState(components[connectorInfo && componentName]?.enabled); @@ -115,7 +116,8 @@ const ConnectorWrapper = (props: ConnectorWrapperProps) => { }, [config, connectorInfo, components]); const determineBackRoute = (channelId: string, newChannel: boolean, connectorHasChannels: string | undefined) => { - const channelRoute = (channelId || newChannel) && connectorHasChannels; + const channelRoute = + (channelId || newChannel) && connectorHasChannels && channelsBySource(connectorInfo?.source).length > 0; if (channelRoute) { setBackRoute(CONNECTOR_CONNECTED_ROUTE); @@ -199,7 +201,6 @@ const ConnectorWrapper = (props: ConnectorWrapperProps) => { text={t('infoButtonText')} />
- {isConfigured && ( void; - isEnabled: boolean; - isConfigured: boolean; - isPending: boolean; -}; - -export const DialogflowConnect = ({ - createNewConnection, - isEnabled, - isConfigured, - isPending, -}: DialogflowConnectProps) => { - const componentInfo = useCurrentConnectorForSource(Source.dialogflow); - const componentName = useCurrentComponentForSource(Source.dialogflow)?.name; - - const [projectId, setProjectID] = useState(componentInfo?.projectId); - const [dialogflowCredentials, setDialogflowCredentials] = useState(componentInfo?.dialogflowCredentials || ''); - const [suggestionConfidenceLevel, setSuggestionConfidenceLevel] = useState( - componentInfo?.suggestionConfidenceLevel || '' - ); - const [replyConfidenceLevel, setReplyConfidenceLevel] = useState(componentInfo?.replyConfidenceLevel || ''); - const [processorWaitingTime, setProcessorWaitingTime] = useState( - componentInfo?.connectorStoreMessagesProcessorMaxWaitMillis || '5000' - ); - const [processorCheckPeriod, setProcessorCheckPeriod] = useState( - componentInfo?.connectorStoreMessagesProcessorCheckPeriodMillis || '2500' - ); - const [defaultLanguage, setDefaultLanguage] = useState(componentInfo?.connectorDefaultLanguage || 'en'); - const [isUpdateModalVisible, setIsUpdateModalVisible] = useState(false); - - const {t} = useTranslation(); - - const updateConfig = (event: React.FormEvent) => { - event.preventDefault(); - if (isEnabled) { - setIsUpdateModalVisible(true); - } else { - enableSubmitConfigData(); - } - }; - - const enableSubmitConfigData = () => { - const payload = { - projectId, - dialogflowCredentials, - suggestionConfidenceLevel, - replyConfidenceLevel, - processorWaitingTime, - processorCheckPeriod, - defaultLanguage, - }; - - createNewConnection(payload); - }; - - return ( - -
-
-
- ) => setProjectID(e.target.value)} - label={t('projectID')} - placeholder={t('AddProjectId')} - showLabelIcon - tooltipText={t('fromCloudConsole')} - required - height={32} - fontClass="font-base" - /> -
- -
- ) => setDialogflowCredentials(e.target.value)} - label={t('GoogleApplicationCredentials')} - placeholder={t('AddGoogleApplicationCredentials')} - showLabelIcon - tooltipText={t('fromCloudConsole')} - required - height={32} - fontClass="font-base" - /> -
-
- ) => setSuggestionConfidenceLevel(e.target.value)} - label={t('SuggestionConfidenceLevel')} - placeholder={'0.1' + ' ' + t('to') + ' ' + '0.9'} - showLabelIcon - tooltipText={t('amountSuggestions')} - required - height={32} - fontClass="font-base" - /> -
-
- ) => setReplyConfidenceLevel(e.target.value)} - label={t('ReplyConfidenceLevel')} - placeholder={'0.1' + ' ' + t('to') + ' ' + '0.9'} - showLabelIcon - tooltipText={t('amountReplies')} - required - height={32} - fontClass="font-base" - /> -
-
-
-
- ) => setProcessorWaitingTime(e.target.value)} - label={t('processorWaitingTime')} - placeholder={t('processorWaitingTime')} - showLabelIcon - tooltipText={t('waitingDefault')} - required - height={32} - fontClass="font-base" - /> -
-
- ) => setProcessorCheckPeriod(e.target.value)} - label={t('processorCheckPeriod')} - placeholder={t('processorCheckPeriod')} - showLabelIcon - tooltipText={t('checkDefault')} - required - height={32} - fontClass="font-base" - /> -
-
- ) => setDefaultLanguage(e.target.value)} - label={t('defaultLanguage')} - placeholder={t('defaultLanguage')} - showLabelIcon - tooltipText={t('defaultLanguageTooltip')} - required - height={32} - fontClass="font-base" - /> -
-
-
-
- ); -}; diff --git a/frontend/control-center/src/pages/Connectors/Providers/Dialogflow/index.module.scss b/frontend/control-center/src/pages/Connectors/Providers/Dialogflow/index.module.scss deleted file mode 100644 index a1bf543bd4..0000000000 --- a/frontend/control-center/src/pages/Connectors/Providers/Dialogflow/index.module.scss +++ /dev/null @@ -1,41 +0,0 @@ -@import 'assets/scss/fonts.scss'; -@import 'assets/scss/colors.scss'; - -.formWrapper { - align-self: center; -} - -.settings { - width: 29rem; -} - -.columnContainer { - display: flex; -} - -.firstColumnForm { - display: flex; - flex-direction: column; - margin-right: 24px; -} - -.secondColumnForm { - display: flex; - flex-direction: column; -} - -.formRow { - width: 464px; - height: 80px; - margin-bottom: 16px; - - label { - @include font-s; - } - - &:hover { - .actionToolTip { - display: block; - } - } -} diff --git a/frontend/control-center/src/pages/Connectors/Providers/Facebook/Messenger/FacebookConnect.tsx b/frontend/control-center/src/pages/Connectors/Providers/Facebook/Messenger/FacebookConnect.tsx index 9aa81f80d3..f520972d54 100644 --- a/frontend/control-center/src/pages/Connectors/Providers/Facebook/Messenger/FacebookConnect.tsx +++ b/frontend/control-center/src/pages/Connectors/Providers/Facebook/Messenger/FacebookConnect.tsx @@ -118,6 +118,7 @@ const FacebookConnect = (props: ConnectedProps) => { fontClass="font-base" /> ) => { fontClass="font-base" /> ) => { fontClass="font-base" /> void; - isEnabled: boolean; - isConfigured: boolean; - isPending: boolean; -}; - -export const RasaConnect = ({createNewConnection, isEnabled, isConfigured, isPending}: RasaConnectProps) => { - const componentInfo = useCurrentConnectorForSource(Source.rasa); - const componentName = useCurrentComponentForSource(Source.rasa)?.name; - - const [webhookUrl, setWebhookUrl] = useState(componentInfo?.webhookUrl || ''); - const [apiHost, setApiHost] = useState(componentInfo?.apiHost || ''); - const [token, setToken] = useState(componentInfo?.token || ''); - const [isUpdateModalVisible, setIsUpdateModalVisible] = useState(false); - const {t} = useTranslation(); - const isUrlValid = webhookUrl && (webhookUrl.startsWith('https') || webhookUrl.startsWith('http')); - - const updateConfig = (event: React.FormEvent) => { - event.preventDefault(); - if (isEnabled) { - setIsUpdateModalVisible(true); - } else { - enableSubmitConfigData(); - } - }; - - const enableSubmitConfigData = () => { - createNewConnection({webhookUrl, apiHost, token}); - }; - - return ( - -
- ) => setWebhookUrl(e.target.value)} - label="Rasa Webhook" - placeholder={t('rasaWebhookPlaceholder')} - showLabelIcon - tooltipText={t('rasaWebhookTooltip')} - required - height={32} - fontClass="font-base" - /> -
- -
- ) => setApiHost(e.target.value)} - label="Api Host" - placeholder={t('rasaApihostPlaceholder')} - showLabelIcon - tooltipText={t('rasaApihostTooltip')} - height={32} - fontClass="font-base" - /> -
-
- ) => setToken(e.target.value)} - label="Token" - placeholder={t('rasaTokenPlaceholder')} - showLabelIcon - tooltipText={t('rasaTokenTooltip')} - height={32} - fontClass="font-base" - /> -
-
- ); -}; diff --git a/frontend/control-center/src/pages/Connectors/Providers/Salesforce/ConnectNewSalesforce.tsx b/frontend/control-center/src/pages/Connectors/Providers/Salesforce/ConnectNewSalesforce.tsx deleted file mode 100644 index c928c0e94c..0000000000 --- a/frontend/control-center/src/pages/Connectors/Providers/Salesforce/ConnectNewSalesforce.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import React, {useState} from 'react'; -import {useCurrentConnectorForSource, useCurrentComponentForSource} from '../../../../selectors'; -import {Input} from 'components'; -import {ConfigureConnector} from '../../ConfigureConnector'; -import {useTranslation} from 'react-i18next'; -import styles from './index.module.scss'; -import {Source} from 'model'; - -interface ConnectParams { - [key: string]: string; -} - -type ConnectNewSalesforceProps = { - createNewConnection: (configValues: ConnectParams) => void; - isEnabled: boolean; - isConfigured: boolean; - isPending: boolean; -}; - -export const ConnectNewSalesforce = ({ - createNewConnection, - isEnabled, - isConfigured, - isPending, -}: ConnectNewSalesforceProps) => { - const componentInfo = useCurrentConnectorForSource(Source.salesforce); - const componentName = useCurrentComponentForSource(Source.salesforce)?.name; - - const [url, setUrl] = useState(componentInfo?.url || ''); - const [username, setUsername] = useState(componentInfo?.username || ''); - const [password, setPassword] = useState(componentInfo?.password || ''); - const [securityToken, setSecurityToken] = useState(componentInfo?.securityToken || ''); - const [isUpdateModalVisible, setIsUpdateModalVisible] = useState(false); - const {t} = useTranslation(); - const isUrlValid = url && (url.startsWith('https') || url.startsWith('http')); - - const updateConfig = (event: React.FormEvent) => { - event.preventDefault(); - if (isEnabled) { - setIsUpdateModalVisible(true); - } else { - enableSubmitConfigData(); - } - }; - - const enableSubmitConfigData = () => { - createNewConnection({url, username, password, securityToken}); - }; - - return ( - -
- ) => setUrl(e.target.value)} - label={t('salesforceOrgUrl')} - placeholder={t('yourSalesforceOrgUrl')} - showLabelIcon - tooltipText={t('salesforceOrgUrlExample')} - required - height={32} - fontClass="font-base" - /> -
- -
- ) => setUsername(e.target.value)} - label={t('Username')} - placeholder={t('yourSalesforceUsername')} - showLabelIcon - tooltipText={t('yourSalesforceUsername')} - required - height={32} - fontClass="font-base" - /> -
-
- ) => setPassword(e.target.value)} - label={t('Password')} - placeholder={t('yourSalesforcePassword')} - showLabelIcon - tooltipText={t('yourSalesforcePassword')} - required - height={32} - fontClass="font-base" - /> -
-
- ) => setSecurityToken(e.target.value)} - label={t('securityToken')} - placeholder={t('yourSalesforceSecurityToken')} - showLabelIcon - tooltipText={t('yourSalesforceSecurityToken')} - required - height={32} - fontClass="font-base" - /> -
-
- ); -}; diff --git a/frontend/control-center/src/pages/Connectors/Providers/Salesforce/index.module.scss b/frontend/control-center/src/pages/Connectors/Providers/Salesforce/index.module.scss deleted file mode 100644 index 1fdbd99211..0000000000 --- a/frontend/control-center/src/pages/Connectors/Providers/Salesforce/index.module.scss +++ /dev/null @@ -1,25 +0,0 @@ -@import 'assets/scss/fonts.scss'; -@import 'assets/scss/colors.scss'; - -.formWrapper { - align-self: center; -} - -.settings { - width: 29rem; -} - -.formRow { - height: 80px; - margin-bottom: 16px; - - label { - @include font-s; - } - - &:hover { - .actionToolTip { - display: block; - } - } -} diff --git a/frontend/control-center/src/pages/Connectors/Providers/WhatsappBusinessCloud/WhatsappBusinessCloudConnect.module.scss b/frontend/control-center/src/pages/Connectors/Providers/WhatsappBusinessCloud/WhatsappBusinessCloudConnect.module.scss deleted file mode 100644 index 1fdbd99211..0000000000 --- a/frontend/control-center/src/pages/Connectors/Providers/WhatsappBusinessCloud/WhatsappBusinessCloudConnect.module.scss +++ /dev/null @@ -1,25 +0,0 @@ -@import 'assets/scss/fonts.scss'; -@import 'assets/scss/colors.scss'; - -.formWrapper { - align-self: center; -} - -.settings { - width: 29rem; -} - -.formRow { - height: 80px; - margin-bottom: 16px; - - label { - @include font-s; - } - - &:hover { - .actionToolTip { - display: block; - } - } -} diff --git a/frontend/control-center/src/pages/Connectors/Providers/WhatsappBusinessCloud/WhatsappBusinessCloudConnect.tsx b/frontend/control-center/src/pages/Connectors/Providers/WhatsappBusinessCloud/WhatsappBusinessCloudConnect.tsx deleted file mode 100644 index d408550556..0000000000 --- a/frontend/control-center/src/pages/Connectors/Providers/WhatsappBusinessCloud/WhatsappBusinessCloudConnect.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import React, {useState} from 'react'; -import {useCurrentConnectorForSource, useCurrentComponentForSource} from '../../../../selectors'; -import {Input} from 'components'; -import {ConfigureConnector} from '../../ConfigureConnector'; -import {useTranslation} from 'react-i18next'; -import styles from './WhatsappBusinessCloudConnect.module.scss'; -import {Source} from 'model'; - -interface ConnectParams { - [key: string]: string; -} - -type WhatsappBusinessCloudConnectProps = { - createNewConnection: (configValues: ConnectParams) => void; - isEnabled: boolean; - isConfigured: boolean; - isPending: boolean; -}; - -export const WhatsappBusinessCloudConnect = ({ - createNewConnection, - isEnabled, - isConfigured, - isPending, -}: WhatsappBusinessCloudConnectProps) => { - const componentInfo = useCurrentConnectorForSource(Source.whatsapp); - const componentName = useCurrentComponentForSource(Source.whatsapp)?.name; - - const [appId, setAppId] = useState(componentInfo?.appId || ''); - const [appSecret, setAppSecret] = useState(componentInfo?.appSecret || ''); - const [phoneNumber, setPhoneNumber] = useState(componentInfo?.phoneNumber || ''); - const [name, setName] = useState(componentInfo?.name || ''); - const [avatarUrl, setAvatarUrl] = useState(componentInfo?.avatarUrl || ''); - const [isUpdateModalVisible, setIsUpdateModalVisible] = useState(false); - const {t} = useTranslation(); - - const updateConfig = (event: React.FormEvent) => { - event.preventDefault(); - if (isEnabled) { - setIsUpdateModalVisible(true); - } else { - enableSubmitConfigData(); - } - }; - - const enableSubmitConfigData = () => { - createNewConnection({appId, appSecret, phoneNumber, name, avatarUrl}); - }; - - return ( - -
- ) => setAppId(e.target.value)} - label="App ID" - placeholder={t('whatsappBusinessCloudAppIdPlaceholder')} - showLabelIcon - tooltipText={t('whatsappBusinessCloudAppIdTooltip')} - required - height={32} - fontClass="font-base" - /> -
-
- ) => setAppSecret(e.target.value)} - label="App Secret" - placeholder={t('whatsappBusinessCloudAppSecretPlaceholder')} - showLabelIcon - tooltipText={t('whatsappBusinessCloudAppSecretToolTip')} - required - height={32} - fontClass="font-base" - /> -
-
- ) => setPhoneNumber(e.target.value)} - label="Phone Number" - placeholder={t('whatsappBusinessCloudPhoneNumberPlaceholder')} - showLabelIcon - tooltipText={t('whatsappBusinessCloudPhoneNumberTooltip')} - required - height={32} - fontClass="font-base" - /> -
-
- ) => setName(e.target.value)} - label="Name" - placeholder={t('name')} - showLabelIcon - tooltipText={t('optional')} - height={32} - fontClass="font-base" - /> -
-
- ) => setAvatarUrl(e.target.value)} - label="Avatar Url" - placeholder="Avatar Url" - showLabelIcon - tooltipText={t('optional')} - height={32} - fontClass="font-base" - /> -
-
- ); -}; diff --git a/frontend/control-center/src/pages/Connectors/Providers/Zendesk/ConnectNewZendesk.tsx b/frontend/control-center/src/pages/Connectors/Providers/Zendesk/ConnectNewZendesk.tsx deleted file mode 100644 index 604970c430..0000000000 --- a/frontend/control-center/src/pages/Connectors/Providers/Zendesk/ConnectNewZendesk.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import React, {useState} from 'react'; -import {useCurrentConnectorForSource, useCurrentComponentForSource} from '../../../../selectors'; -import {Input} from 'components'; -import {ConfigureConnector} from '../../ConfigureConnector'; -import styles from './index.module.scss'; -import {useTranslation} from 'react-i18next'; -import {Source} from 'model'; - -interface ConnectParams { - [key: string]: string; -} - -type ConnectNewDialogflowProps = { - createNewConnection: (configValues: ConnectParams) => void; - isEnabled: boolean; - isConfigured: boolean; - isPending: boolean; -}; - -export const ConnectNewZendesk = ({ - createNewConnection, - isEnabled, - isConfigured, - isPending, -}: ConnectNewDialogflowProps) => { - const componentInfo = useCurrentConnectorForSource(Source.zendesk); - const componentName = useCurrentComponentForSource(Source.zendesk)?.name; - - const [domain, setDomain] = useState(componentInfo?.domain || ''); - const [username, setUsername] = useState(componentInfo?.username || ''); - const [token, setToken] = useState(componentInfo?.token || ''); - const [isUpdateModalVisible, setIsUpdateModalVisible] = useState(false); - const {t} = useTranslation(); - - const updateConfig = (event: React.FormEvent) => { - event.preventDefault(); - if (isEnabled) { - setIsUpdateModalVisible(true); - } else { - enableSubmitConfigData(); - } - }; - - const enableSubmitConfigData = () => { - createNewConnection({domain, token, username}); - }; - - return ( - -
- ) => setDomain(e.target.value)} - label={t('ZendeskSubDomain')} - placeholder={t('AddDomain')} - showLabelIcon - tooltipText={t('ZendeskDomain')} - required - height={32} - fontClass="font-base" - /> -
- -
- ) => setUsername(e.target.value)} - label={t('username')} - placeholder={t('AddUsername')} - showLabelIcon - tooltipText={t('ZendeskUsername')} - required - height={32} - fontClass="font-base" - /> -
-
- ) => setToken(e.target.value)} - label={t('APIToken')} - placeholder={t('APIToken')} - showLabelIcon - tooltipText={t('ZendeskToken')} - required - height={32} - fontClass="font-base" - /> -
-
- ); -}; diff --git a/frontend/control-center/src/pages/Connectors/Providers/Zendesk/index.module.scss b/frontend/control-center/src/pages/Connectors/Providers/Zendesk/index.module.scss deleted file mode 100644 index 422930afbf..0000000000 --- a/frontend/control-center/src/pages/Connectors/Providers/Zendesk/index.module.scss +++ /dev/null @@ -1,17 +0,0 @@ -@import 'assets/scss/fonts.scss'; -@import 'assets/scss/colors.scss'; - -.formRow { - height: 80px; - margin-bottom: 16px; - - label { - @include font-s; - } - - &:hover { - .actionToolTip { - display: block; - } - } -} diff --git a/frontend/control-center/src/pages/Connectors/index.tsx b/frontend/control-center/src/pages/Connectors/index.tsx index 7434752d6f..ec63b30581 100644 --- a/frontend/control-center/src/pages/Connectors/index.tsx +++ b/frontend/control-center/src/pages/Connectors/index.tsx @@ -1,6 +1,6 @@ import React, {useEffect, useState} from 'react'; import {connect, ConnectedProps, useSelector} from 'react-redux'; -import {Channel, Source} from 'model'; +import {Channel, ComponentInfo, Source} from 'model'; import InfoCard from './InfoCard'; import {StateModel} from '../../reducers'; import {allChannelsConnected} from '../../selectors/channels'; @@ -27,24 +27,30 @@ export interface ConnectorCardComponentInfo { source: Source; } +const mapStateToProps = (state: StateModel) => { + return { + components: state.data.config.components, + connectors: state.data.connector, + catalogList: state.data.catalog, + }; +}; + const mapDispatchToProps = { listChannels, getConnectorsConfiguration, listComponents, }; -const connector = connect(null, mapDispatchToProps); +const connector = connect(mapStateToProps, mapDispatchToProps); const Connectors = (props: ConnectedProps) => { - const {listChannels, getConnectorsConfiguration, listComponents} = props; + const {components, connectors, catalogList, listChannels, getConnectorsConfiguration, listComponents} = props; const [connectorsPageList, setConnectorsPageList] = useState<[] | ConnectorCardComponentInfo[]>([]); const channels = useSelector((state: StateModel) => Object.values(allChannelsConnected(state))); - const components = useSelector((state: StateModel) => state.data.config.components); - const connectors = useSelector((state: StateModel) => state.data.connector); - const catalogList = useSelector((state: StateModel) => state.data.catalog); const channelsBySource = (Source: Source) => channels.filter((channel: Channel) => channel.source === Source); const [hasInstalledComponents, setHasInstalledComponents] = useState(false); const pageTitle = 'Connectors'; + const sortByName = (a: ComponentInfo, b: ComponentInfo) => a?.displayName?.localeCompare(b?.displayName); const catalogListArr = Object.entries(catalogList); const emptyCatalogList = catalogListArr.length === 0; @@ -83,6 +89,7 @@ const Connectors = (props: ConnectedProps) => { } }); + listArr.sort(sortByName); setConnectorsPageList(listArr); } }, [catalogList]); diff --git a/frontend/control-center/src/pages/Status/ComponentListItem/ItemInfo.tsx b/frontend/control-center/src/pages/Status/ComponentListItem/ItemInfo.tsx index e522f71a08..cf8b97a43f 100644 --- a/frontend/control-center/src/pages/Status/ComponentListItem/ItemInfo.tsx +++ b/frontend/control-center/src/pages/Status/ComponentListItem/ItemInfo.tsx @@ -10,6 +10,7 @@ import {SettingsModal, Button, Toggle, Tooltip} from 'components'; import {connect, ConnectedProps, useSelector} from 'react-redux'; import {useTranslation} from 'react-i18next'; import styles from './index.module.scss'; +import {CONNECTORS_ROUTE} from '../../../routes/routes'; type ComponentInfoProps = { healthy: boolean; @@ -36,13 +37,15 @@ const ItemInfo = (props: ComponentInfoProps) => { const [componentEnabled, setComponentEnabled] = useState(enabled); const [enablePopupVisible, setEnablePopupVisible] = useState(false); const isVisible = isExpanded || isComponent; + const navigateConfigure = `${CONNECTORS_ROUTE}/${source}/configure`; const {t} = useTranslation(); const isConfigurableConnector = () => { let isConfigurable = false; Object.entries(catalogList).forEach(elem => { - if (elem[1] && elem[1].source && elem[1].source === source) isConfigurable = true; + if (elem[1] && elem[1].source && elem[1].source === source && elem[1].source !== Source.webhooks) + isConfigurable = true; }); return isConfigurable; @@ -56,8 +59,6 @@ const ItemInfo = (props: ComponentInfoProps) => { connector && connectors[itemName] && isComponent && - enabled && - healthy && isConfigurableConnector() && !isComponentConfigured && !itemName.includes(Source.chatPlugin); @@ -108,15 +109,7 @@ const ItemInfo = (props: ComponentInfoProps) => {
- {needsConfig ? ( - } - hoverElementHeight={20} - hoverElementWidth={20} - tooltipContent={t('needsConfiguration')} - direction="right" - /> - ) : isRunning ? ( + {isRunning ? ( } hoverElementHeight={20} @@ -144,12 +137,23 @@ const ItemInfo = (props: ComponentInfoProps) => { ) )}
- - {isComponent && !needsConfig && ( -
- -
- )} + {isComponent && + (!needsConfig ? ( +
+ +
+ ) : ( +
+ } + hoverElementHeight={20} + hoverElementWidth={20} + tooltipContent={t('needsConfiguration')} + direction="right" + navigateTo={navigateConfigure} + /> +
+ ))}
)} diff --git a/frontend/control-center/src/services/getComponentStatus.ts b/frontend/control-center/src/services/getComponentStatus.ts index f491c2bf8d..45bdc9532e 100644 --- a/frontend/control-center/src/services/getComponentStatus.ts +++ b/frontend/control-center/src/services/getComponentStatus.ts @@ -6,8 +6,8 @@ export const getComponentStatus = ( isConfigured: boolean, isEnabled: boolean ) => { - if (isInstalled && !isEnabled) return ComponentStatus.disabled; if (isInstalled && !isConfigured) return ComponentStatus.notConfigured; - if (!isHealthy) return ComponentStatus.notHealthy; + if (isInstalled && !isEnabled) return ComponentStatus.disabled; + if (isInstalled && isConfigured && isEnabled && !isHealthy) return ComponentStatus.notHealthy; if (isInstalled && isConfigured && isEnabled) return ComponentStatus.enabled; }; diff --git a/frontend/control-center/src/services/getRouteForCard.ts b/frontend/control-center/src/services/getRouteForCard.ts index b83cd36262..e75f1e84eb 100644 --- a/frontend/control-center/src/services/getRouteForCard.ts +++ b/frontend/control-center/src/services/getRouteForCard.ts @@ -1,16 +1,20 @@ import {CONNECTORS_ROUTE, CATALOG_ROUTE, WEBHOOKS_ROUTE} from '../routes/routes'; import {Source} from 'model'; -export const getConnectedRouteForComponent = (source: Source, isChannel: string) => { +export const getConnectedRouteForComponent = (source: Source, isChannel?: string, configured?: boolean) => { if (source === Source.webhooks) return WEBHOOKS_ROUTE; - if (isChannel) return `${CONNECTORS_ROUTE}/${source}/connected`; + if ((!configured || !isChannel) && source !== Source.chatPlugin) return `${CONNECTORS_ROUTE}/${source}/configure`; - return `${CONNECTORS_ROUTE}/${source}/new`; + return `${CONNECTORS_ROUTE}/${source}/connected`; }; -export const getNewChannelRouteForComponent = (source: Source) => { - return source === Source.webhooks ? WEBHOOKS_ROUTE : `${CONNECTORS_ROUTE}/${source}/new`; +export const getNewChannelRouteForComponent = (source: Source, isChannel?: string, configured?: boolean) => { + if (source === Source.webhooks) return WEBHOOKS_ROUTE; + + if ((!configured || !isChannel) && source !== Source.chatPlugin) return `${CONNECTORS_ROUTE}/${source}/configure`; + + return `${CONNECTORS_ROUTE}/${source}/new`; }; export const getCatalogProductRouteForComponent = (source: Source) => { diff --git a/lib/typescript/components/tooltip/index.tsx b/lib/typescript/components/tooltip/index.tsx index 6debc20df6..4c458ee668 100644 --- a/lib/typescript/components/tooltip/index.tsx +++ b/lib/typescript/components/tooltip/index.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import {useNavigate} from 'react-router-dom'; import styles from './index.module.scss'; type TooltipProps = { @@ -7,17 +8,28 @@ type TooltipProps = { hoverElementWidth: number; tooltipContent: string; direction: 'top' | 'right' | 'bottom' | 'left'; + navigateTo?: string; }; export const Tooltip = (props: TooltipProps) => { - const {hoverElement, hoverElementHeight, hoverElementWidth, tooltipContent, direction} = props; + const {hoverElement, hoverElementHeight, hoverElementWidth, tooltipContent, direction, navigateTo} = props; + const navigate = useNavigate(); + + const handleOnClick = (event: React.MouseEvent) => { + event.stopPropagation(); + navigateTo && navigate(navigateTo, {state: {from: 'status'}}); + }; return (
-
+
handleOnClick(event)} + > {hoverElement}
{tooltipContent} diff --git a/lib/typescript/translations/translations.ts b/lib/typescript/translations/translations.ts index d175e5a848..5b5016ad6e 100644 --- a/lib/typescript/translations/translations.ts +++ b/lib/typescript/translations/translations.ts @@ -194,6 +194,7 @@ const resources = { reset: 'Reset', preview: 'Preview', sample: 'Sample', + add: 'Add', undoStep: 'Undo', deleteChannel: 'Do you really want to delete this channel?', addChanne: 'Add channel', @@ -284,16 +285,39 @@ const resources = { installCodeNpm1: 'You can install your ', installCodeNpm2: ' library here:', + //Facebook Messenger + inputTooltipFacebookAppId: 'Your Facebook App Id', + inputTooltipFacebookAppSecret: 'Your Facebook App Secret', + inputTooltipFacebookWebhookSecret: 'Your Facebook Webhook Secret', + + //Google + inputTooltipGoogleSaFile: 'Your Google Sa File', + inputTooltipGooglePartnerKey: 'Your Google Partner Key', + + //Viber + inputTooltipViberAuthToken: 'Your Viber Auth Token', + + //Twilio SMS + inputTooltipTwiliosmsAuthToken: 'Your Twilio SMS Auth Token', + inputTooltipTwiliosmsAccountSid: 'Your Twilio SMS Account Sid', + + //Whatsapp Business Cloud + inputTooltipWhatsappAppId: 'Your Whatsapp App Id', + inputTooltipWhatsappAppSecret: 'Your Whatsapp App Secret', + inputTooltipWhatsappPhoneNumber: 'Your Phone Number', + inputTooltipWhatsappName: 'Your Name', + inputTooltipWhatsappAvatarUrl: 'Your Avatar Url', + //Zendesk zendeskDescription: 'Make customers happy via text, mobile, phone, email, live chat, social media.', ZendeskSubDomain: 'Zendesk Subdomain', AddDomain: 'Add domain', - ZendeskDomain: 'Your zendesk subdomain', username: 'Username', AddUsername: 'Add Username', - ZendeskUsername: 'Your zendesk username', - ZendeskToken: 'A zendesk API token associated to your user', APIToken: 'API Token', + inputTooltipZendeskDomain: 'Your Zendesk Subdomain', + inputTooltipZendeskUsername: 'Your Zendesk Username', + inputTooltipZendeskToken: 'A Zendesk API token associated to your user', //Dialogflow dialogflowDescription: 'Conversational AI with virtual agents', @@ -303,29 +327,35 @@ const resources = { AddGoogleApplicationCredentials: 'Add the Google Application Credentials', SuggestionConfidenceLevel: 'Suggestion Confidence Level', ReplyConfidenceLevel: 'Reply Confidence Level', - fromCloudConsole: 'given by the Cloud Console', - amountSuggestions: 'amount for suggestions', - amountReplies: 'amount for replies', + inputTooltipDialogflowProjectId: 'given by the Cloud Console', + inputTooltipDialogflowDialogflowCredentials: 'given by the Cloud Console', + inputTooltipDialogflowSuggestionConfidenceLevel: 'amount for suggestions', + inputTooltipDialogflowReplyConfidenceLevel: 'amount for replies', to: 'to', processorWaitingTime: 'Processor waiting time', processorCheckPeriod: 'Processor check period', - waitingDefault: 'Default value: 5000', - checkDefault: 'Default value: 2500', + inputTooltipDialogflowConnectorStoreMessagesProcessorMaxWaitMillis: 'Default value: 5000', + inputTooltipDialogflowConnectorStoreMessagesProcessorCheckPeriodMillis: 'Default value: 2500', defaultLanguage: 'Default language', - defaultLanguageTooltip: 'Default value: en', + inputTooltipDialogflowConnectorDefaultLanguage: 'Default value: en', //Salesforce salesforceDescription: "Increase sales performance with the world's No. 1 CRM platform for business of all sizes.", salesforceOrgUrl: 'Organization URL', yourSalesforceOrgUrl: 'Your Salesforce organization URL', - salesforceOrgUrlExample: 'Example: https://org.my.salesforce.com', Username: 'Username', - yourSalesforceUsername: 'Your Salesforce username', Password: 'Password', - yourSalesforcePassword: 'Your Salesforce password', securityToken: 'Security Token', - yourSalesforceSecurityToken: 'Your Salesforce Security Token', + inputTooltipSalesforceUrl: 'Example: https://org.my.salesforce.com', + inputTooltipSalesforceUsername: 'Your Salesforce Username', + inputTooltipSalesforcePassword: 'Your Salesforce Password', + inputTooltipSalesforceSecurityToken: 'Your Salesforce Security Token', + + //Rasa + inputTooltipRasaWebhookUrl: 'Example: http://webhooks.rasa', + inputTooltipRasaApiHost: 'Your Rasa Api Host', + inputTooltipRasaToken: 'Your Rasa Token', //Facebook Messenger connectMessenger: 'Connect Messenger', @@ -450,14 +480,6 @@ const resources = { message: 'Message', send: 'Send', - //Rasa - rasaWebhookPlaceholder: 'Your Rasa Webhook Url', - rasaWebhookTooltip: 'Example: http://webhooks.rasa', - rasaApihostPlaceholder: 'Your Rasa Api Host', - rasaApihostTooltip: 'Your Rasa Api Host', - rasaTokenPlaceholder: 'Your Rasa Token', - rasaTokenTooltip: 'Your Rasa Token', - //Whatsapp Business Cloud whatsappBusinessCloudAppIdPlaceholder: 'Your App ID', whatsappBusinessCloudAppIdTooltip: 'Example: 1116544774977108', @@ -707,6 +729,7 @@ const resources = { reset: 'Zurücksetzen', preview: 'Vorschau', sample: 'Muster', + add: 'Hinzufügen', deleteChannel: 'Wollen Sie diesen Kanal wirklich löschen?', addChanne: 'Kanal hinzufügen', infoButtonText: 'mehr Informationen', @@ -796,16 +819,39 @@ const resources = { installCodeNpm1: 'Sie können Ihre ', installCodeNpm2: '-Bibliothek hier installieren:', + //Facebook Messenger + inputTooltipFacebookAppId: 'Ihre Facebook App Id', + inputTooltipFacebookAppSecret: 'Ihr Facebook App Secret', + inputTooltipFacebookWebhookSecret: 'Ihr Facebook Webhook Secret', + + //Google + inputTooltipGoogleSaFile: 'Ihre Google Sa File', + inputTooltipGooglePartnerKey: 'Ihr Google Partner Schlüssel', + + //Viber + inputTooltipViberAuthToken: 'Ihr Viber Auth Token', + + //Twilio SMS + inputTooltipTwiliosmsAuthToken: 'Ihr Twilio SMS Auth Token', + inputTooltipTwiliosmsAccountSid: 'Ihr Twilio SMS Account Sid', + + //Whatsapp Business Cloud + inputTooltipWhatsappAppId: 'Ihre Whatsapp App Id', + inputTooltipWhatsappAppSecret: 'Ihr Whatsapp App Secret', + inputTooltipWhatsappPhoneNumber: 'Ihre Handynummer', + inputTooltipWhatsappName: 'Ihr Name', + inputTooltipWhatsappAvatarUrl: 'Ihre Avatar Url', + //Zendesk zendeskDescription: 'Machen Sie Kunden glücklich per SMS, E-Mail, Live-Chat, Social Media.', ZendeskSubDomain: 'Zendesk Subdomäne', AddDomain: 'Domäne hinzufügen', - ZendeskDomain: 'Ihre Zendesk-Subdomäne', username: 'Benutzername', AddUsername: 'Benutzername hinzufügen', - ZendeskUsername: 'Ihr Zendesk Benutzername', - ZendeskToken: 'Ihr Zendesk-API-Token', APIToken: 'API-Token', + inputTooltipZendeskDomain: 'Ihre Zendesk-Subdomäne', + inputTooltipZendeskUsername: 'Ihr Zendesk Benutzername', + inputTooltipZendeskToken: 'Ihr Zendesk-API-Token', //Dialogflow dialogflowDescription: 'Conversational AI mit virtuellen Agenten', @@ -831,13 +877,18 @@ const resources = { 'Steigern Sie die Vertriebsleistung mit der weltweit führenden CRM-Plattform für Unternehmen.', salesforceOrgUrl: 'Organisations-URL', yourSalesforceOrgUrl: 'Ihre Salesforce-Organisations-URL', - salesforceOrgUrlExample: 'Beispiel: https://org.my.salesforce.com', Username: 'Benutzername', - yourSalesforceUsername: 'Ihr Salesforce-Benutzername', Password: 'Passwort', - yourSalesforcePassword: 'Ihr Salesforce-Passwort', securityToken: 'Sicherheitstoken', - yourSalesforceSecurityToken: 'Ihr Salesforce-Sicherheitstoken', + inputTooltipSalesforceUrl: 'Beispiel: https://org.my.salesforce.com', + inputTooltipSalesforceUsername: 'Ihr Salesforce-Benutzername', + inputTooltipSalesforcePassword: 'Ihr Salesforce-Passwort', + inputTooltipSalesforceSecurityToken: 'Ihr Salesforce-Sicherheitstoken', + + //Rasa + inputTooltipRasaWebhookUrl: 'Beispiel: http://webhooks.rasa', + inputTooltipRasaApiHost: 'Ihr Rasa Api Host', + inputTooltipRasaToken: 'Ihr Rasa-Token', //WhatsApp Business Cloud whatsappDescription: 'Weltweite Chat-App Nr. 1', @@ -963,14 +1014,6 @@ const resources = { message: 'Nachricht', send: 'Senden', - //Rasa - rasaWebhookPlaceholder: 'Ihre Rasa Webhook Url', - rasaWebhookTooltip: 'Beispiel: http://webhooks.rasa', - rasaApihostPlaceholder: 'Ihr Rasa Api Host', - rasaApihostTooltip: 'Ihr Rasa Api-Host', - rasaTokenPlaceholder: 'Ihr Rasa Token', - rasaTokenTooltip: 'Ihr Rasa-Token', - //Whatsapp Business Cloud whatsappBusinessCloudAppIdPlaceholder: 'Ihre App ID', whatsappBusinessCloudAppIdTooltip: 'Beispiel: 1116544774977108', @@ -1212,6 +1255,7 @@ const resources = { reset: 'Réinitialiser', preview: 'Aperçu', sample: 'Echantillon', + add: 'Ajouter', deleteChannel: 'Voulez-vous vraiment supprimer ce canal?', addChanne: 'Ajouter un canal', infoButtonText: `plus d'informations`, @@ -1285,16 +1329,39 @@ const resources = { installCustomize: 'Installation et personnalisation', addLiveChatToWebsite: 'Ajoutez Airy Live Chat à votre site web et à votre application.', + //Facebook Messenger + inputTooltipFacebookAppId: 'Ton Facebook App Id', + inputTooltipFacebookAppSecret: 'Ton Facebook App Secret', + inputTooltipFacebookWebhookSecret: 'Ton Facebook Webhook Secret', + + //Google + inputTooltipGoogleSaFile: 'Ton Google Sa File', + inputTooltipGooglePartnerKey: 'Ton Google Partner Key', + + //Viber + inputTooltipViberAuthToken: 'Ton Viber Auth Token', + + //Twilio SMS + inputTooltipTwiliosmsAuthToken: 'Ton Twilio SMS Auth Token', + inputTooltipTwiliosmsAccountSid: 'Ton Twilio SMS Account Sid', + + //Whatsapp Business Cloud + inputTooltipWhatsappAppId: 'Votre Whatsapp App Id', + inputTooltipWhatsappAppSecret: 'Votre Whatsapp App Secret', + inputTooltipWhatsappPhoneNumber: 'Votre numéro de téléphone', + inputTooltipWhatsappName: 'Votre nom', + inputTooltipWhatsappAvatarUrl: 'Votre Avatar Url', + //Zendesk zendeskDescription: "Un service client d'excellence par SMS, e-mail, chat, réseaux sociaux.", ZendeskSubDomain: 'Sous-domaine Zendesk', AddDomain: 'Ajouter un domaine', - ZendeskDomain: 'Ton sous-domaine Zendesk', username: "Nom d'utilisateur", AddUsername: "Ajouter un nom d'utilisateur", - ZendeskUsername: "Ton nom d'utilisateur Zendesk", - ZendeskToken: 'Un token API Zendesk associée à ton utilisateur', APIToken: 'Token API', + inputTooltipZendeskDomain: 'Ton sous-domaine Zendesk', + inputTooltipZendeskUsername: `Ton nom d'utilisateur Zendesk`, + inputTooltipZendeskToken: 'Un token API Zendesk associée à ton utilisateur', //Dialogflow dialogflowDescription: "Des conversations d'IA avec des agents virtuels", @@ -1320,13 +1387,13 @@ const resources = { salesforceDescription: 'Augmentez vos performances commerciales avec la plateforme CRM n° 1 au monde.', salesforceOrgUrl: 'URL', yourSalesforceOrgUrl: 'URL Salesforce de votre organisation', - salesforceOrgUrlExample: 'Exemple : https://org.my.salesforce.com', Username: "Nom d'utilisateur", - yourSalesforceUsername: "Nom d'utilisateur Salesforce", Password: 'Mot de passe', - yourSalesforcePassword: 'Mot de passe Salesforce', securityToken: 'Jeton de sécurité', - yourSalesforceSecurityToken: 'Jeton de sécurité Salesforce', + inputTooltipSalesforceUrl: 'Exemple : https://org.my.salesforce.com', + inputTooltipSalesforceUsername: "Nom d'utilisateur Salesforce", + inputTooltipSalesforcePassword: 'Mot de passe Salesforce', + inputTooltipSalesforceSecurityToken: 'Jeton de sécurité Salesforce', //Facebook Messenger facebookPageId: 'ID de la page Facebook', @@ -1720,6 +1787,7 @@ const resources = { reset: 'Restablecer', preview: 'Vista previa', sample: 'Muestra', + add: 'Añadir', deleteChannel: '¿Realmente quieres borrar este canal?', addChanne: 'Añadir canal', infoButtonText: 'más información', @@ -1809,17 +1877,40 @@ const resources = { installCodeNpm1: 'Puede instalar su biblioteca ', installCodeNpm2: ' aquí:', + //Facebook Messenger + inputTooltipFacebookAppId: 'Tu Facebook App Id', + inputTooltipFacebookAppSecret: 'Tu Facebook App Secret', + inputTooltipFacebookWebhookSecret: 'Tu Facebook Webhook Secret', + + //Google + inputTooltipGoogleSaFile: 'Tu Google Sa File', + inputTooltipGooglePartnerKey: 'Tu Google Partner Key', + + //Viber + inputTooltipViberAuthToken: 'Tu Viber Auth Token', + + //Twilio SMS + inputTooltipTwiliosmsAuthToken: 'Tu Twilio SMS Auth Token', + inputTooltipTwiliosmsAccountSid: 'Tu Twilio SMS Account Sid', + + //Whatsapp Business Cloud + inputTooltipWhatsappAppId: 'Su Id. de aplicación de Whatsapp', + inputTooltipWhatsappAppSecret: 'El secreto de tu aplicación de Whatsapp', + inputTooltipWhatsappPhoneNumber: 'Su número de teléfono', + inputTooltipWhatsappName: 'Su nombre', + inputTooltipWhatsappAvatarUrl: 'La url de su avatar', + //Zendesk zendeskDescription: 'Mantén a tus clientes satisfechos a través de mensajes de texto, correos electrónicos, chat en vivo.', ZendeskSubDomain: 'subdominio Zendesk', AddDomain: 'Añadir el subdominio', - ZendeskDomain: 'Tu subdominio Zendesk', username: 'nombre de usuario', AddUsername: 'Añadir el nombre de usuario', - ZendeskUsername: 'Tu nombre de usuario Zendesk', - ZendeskToken: 'Un token de API Zendesk asociado a tu usuario', APIToken: 'Token de API', + inputTooltipZendeskDomain: 'Tu subdominio Zendesk', + inputTooltipZendeskUsername: `Tu nombre de usuario Zendesk`, + inputTooltipZendeskToken: 'Un token de API Zendesk asociado a tu usuario', //Dialogflow dialogflowDescription: 'IA conversacional con agentes virtuales', @@ -1844,13 +1935,13 @@ const resources = { salesforceDescription: 'Aumente sus resultados de ventas con la plataforma de CRM n.º 1 del mundo.', salesforceOrgUrl: 'URL de la organización', yourSalesforceOrgUrl: 'La URL de su organización de Salesforce', - salesforceOrgUrlExample: 'Ejemplo: https://org.my.salesforce.com', Username: 'Nombre de usuario', - yourSalesforceUsername: 'Su nombre de usuario de Salesforce', Password: 'Contraseña', - yourSalesforcePassword: 'Su contraseña de Salesforce', securityToken: 'Token de seguridad', - yourSalesforceSecurityToken: 'Su token de seguridad de Salesforce', + inputTooltipSalesforceUrl: 'Ejemplo: https://org.my.salesforce.com', + inputTooltipSalesforceUsername: 'Su nombre de usuario de Salesforce', + inputTooltipSalesforcePassword: 'Su contraseña de Salesforce', + inputTooltipSalesforceSecurityToken: 'Su token de seguridad de Salesforce', //Facebook Messenger connectMessenger: 'Conectar con Messenger', From 954ca329f5bf84f548f47a5252a6416dfcf3d8bb Mon Sep 17 00:00:00 2001 From: Thorsten Date: Thu, 29 Sep 2022 16:12:18 +0200 Subject: [PATCH 55/74] [#3648] Added getMergedConnectors as useSelector (#3780) --- .../src/pages/Catalog/CatalogCard/index.tsx | 18 +- .../src/pages/Catalog/index.tsx | 12 +- .../pages/Connectors/ChannelCard/index.tsx | 5 +- .../Connectors/ConfigStatusButton/index.tsx | 8 +- .../ConnectedChannelsList/index.tsx | 8 +- .../Connectors/ConnectorConfig/index.tsx | 50 ++---- .../EmptyStateConnectors/index.module.scss | 2 + .../src/pages/Connectors/InfoCard/index.tsx | 6 +- .../src/pages/Connectors/index.tsx | 156 +++++++----------- .../control-center/src/pages/Status/index.tsx | 8 +- .../src/selectors/connectors.ts | 74 ++++++++- .../src/services/getComponentStatus.ts | 2 +- .../src/services/getRouteForCard.ts | 13 +- lib/typescript/model/Components.ts | 7 + lib/typescript/model/Connectors.ts | 20 +++ 15 files changed, 227 insertions(+), 162 deletions(-) diff --git a/frontend/control-center/src/pages/Catalog/CatalogCard/index.tsx b/frontend/control-center/src/pages/Catalog/CatalogCard/index.tsx index 63abd8d1bd..1f1e4bbc39 100644 --- a/frontend/control-center/src/pages/Catalog/CatalogCard/index.tsx +++ b/frontend/control-center/src/pages/Catalog/CatalogCard/index.tsx @@ -7,12 +7,13 @@ import {installComponent} from '../../../actions/catalog'; import {ComponentInfo, ConnectorPrice, NotificationModel} from 'model'; import {Button, NotificationComponent, SettingsModal, SmartButton} from 'components'; import {getChannelAvatar} from '../../../components/ChannelAvatar'; -import {getCatalogProductRouteForComponent} from '../../../services'; +import {getCatalogProductRouteForComponent, getConnectedRouteForComponent, removePrefix} from '../../../services'; import {DescriptionComponent, getDescriptionSourceName} from '../../../components/Description'; import {ReactComponent as CheckmarkIcon} from 'assets/images/icons/checkmarkFilled.svg'; import styles from './index.module.scss'; import NotifyMeModal from '../NotifyMeModal'; import {CONNECTORS_ROUTE} from '../../../routes/routes'; +import {getMergedConnectors} from '../../../selectors'; type CatalogCardProps = { componentInfo: ComponentInfo; @@ -20,6 +21,7 @@ type CatalogCardProps = { const mapStateToProps = (state: StateModel) => ({ component: state.data.catalog, + connectors: getMergedConnectors(state), }); const mapDispatchToProps = { @@ -31,7 +33,10 @@ const connector = connect(mapStateToProps, mapDispatchToProps); export const availabilityFormatted = (availability: string) => availability.split(','); const CatalogCard = (props: CatalogCardProps) => { - const {component, componentInfo, installComponent} = props; + const {component, connectors, componentInfo, installComponent} = props; + const hasConnectedChannels = connectors[removePrefix(componentInfo?.name)].connectedChannels > 0; + const isConfigured = connectors[removePrefix(componentInfo?.name)].isConfigured; + const isChannel = connectors[removePrefix(componentInfo?.name)].isChannel; const isInstalled = component[componentInfo?.name]?.installed; const [isModalVisible, setIsModalVisible] = useState(false); const [isNotifyMeModalVisible, setIsNotifyMeModalVisible] = useState(false); @@ -46,13 +51,10 @@ const CatalogCard = (props: CatalogCardProps) => { const {t} = useTranslation(); const navigate = useNavigate(); const navigateConfigure = `${CONNECTORS_ROUTE}/${componentInfo?.source}/configure`; - const navigateConnected = `${CONNECTORS_ROUTE}/${componentInfo?.source}/connected`; //Commented until backend is ready for this!!! // const notifiedEmail = t('infoNotifyMe') + ` ${notified}`; - const isChannel = componentInfo?.isChannel; - const openInstallModal = () => { setIsPending(true); installComponent({name: componentInfo.name}) @@ -120,7 +122,11 @@ const CatalogCard = (props: CatalogCardProps) => { + + + +
+
+
+ + +
+
+ ); +}; diff --git a/frontend/control-center/src/pages/Catalog/index.module.scss b/frontend/control-center/src/pages/Catalog/index.module.scss index 2ee93ce47b..278b13a7b0 100644 --- a/frontend/control-center/src/pages/Catalog/index.module.scss +++ b/frontend/control-center/src/pages/Catalog/index.module.scss @@ -13,6 +13,12 @@ width: 100%; } +.headlineSearchBarContainer { + display: flex; + justify-content: space-between; + align-items: center; +} + .catalogListContainer { display: flex; flex-wrap: wrap; @@ -25,6 +31,18 @@ color: var(--color-text-contrast); } +.notFoundContainer { + display: flex; + flex-direction: column; + h1 { + @include font-l; + font-weight: bold; + } + span { + @include font-m; + } +} + .notificationAnimation { display: flex; align-items: center; diff --git a/frontend/control-center/src/pages/Catalog/index.tsx b/frontend/control-center/src/pages/Catalog/index.tsx index 3c9eb556d7..ca3016fc7e 100644 --- a/frontend/control-center/src/pages/Catalog/index.tsx +++ b/frontend/control-center/src/pages/Catalog/index.tsx @@ -7,6 +7,8 @@ import {ComponentInfo, ConnectorPrice} from 'model'; import CatalogCard from './CatalogCard'; import styles from './index.module.scss'; import {listComponents, getConnectorsConfiguration, listChannels} from '../../actions'; +import {CatalogSearchBar} from './CatalogSearchBar/CatalogSearchBar'; +import {FilterTypes} from './CatalogSearchBar/FilterCatalogModal/FilterCatalogModal'; const mapStateToProps = (state: StateModel) => { return { @@ -27,6 +29,10 @@ const Catalog = (props: ConnectedProps) => { const [orderedCatalogList, setOrderedCatalogList] = useState(catalogList); const {t} = useTranslation(); const catalogPageTitle = t('Catalog'); + const [currentFilter, setCurrentFilter] = useState( + (localStorage.getItem('catalogCurrentTypeFilter') as FilterTypes) || FilterTypes.all + ); + const [query, setQuery] = useState(''); const sortByName = (a: ComponentInfo, b: ComponentInfo) => a?.displayName?.localeCompare(b?.displayName); useEffect(() => { @@ -43,30 +49,77 @@ const Catalog = (props: ConnectedProps) => { }, []); useLayoutEffect(() => { - const sortedByInstalled = [...catalogList] - .filter((component: ComponentInfo) => component.installed && component.price !== ConnectorPrice.requestAccess) - .sort(sortByName); - const sortedByUninstalled = [...catalogList] - .filter((component: ComponentInfo) => !component.installed && component.price !== ConnectorPrice.requestAccess) - .sort(sortByName); - const sortedByAccess = [...catalogList] - .filter((component: ComponentInfo) => component.price === ConnectorPrice.requestAccess) - .sort(sortByName); + if (query && currentFilter === FilterTypes.all) { + const filteredCatalogByName = [...catalogList].filter((component: ComponentInfo) => + component?.displayName?.toLowerCase().includes(query) + ); + setOrderedCatalogList(filteredCatalogByName); + } else { + const sortedByInstalled = [...catalogList] + .filter((component: ComponentInfo) => component.installed && component.price !== ConnectorPrice.requestAccess) + .sort(sortByName); + const sortedByUninstalled = [...catalogList] + .filter((component: ComponentInfo) => !component.installed && component.price !== ConnectorPrice.requestAccess) + .sort(sortByName); + const sortedByAccess = [...catalogList] + .filter((component: ComponentInfo) => component.price === ConnectorPrice.requestAccess) + .sort(sortByName); - setOrderedCatalogList(sortedByInstalled.concat(sortedByUninstalled).concat(sortedByAccess)); - }, [catalogList]); + switch (currentFilter) { + case FilterTypes.all: + setOrderedCatalogList(sortedByInstalled.concat(sortedByUninstalled).concat(sortedByAccess)); + break; + case FilterTypes.installed: + query !== '' + ? setOrderedCatalogList( + sortedByInstalled.filter((component: ComponentInfo) => + component?.displayName?.toLowerCase().includes(query) + ) + ) + : setOrderedCatalogList(sortedByInstalled); + break; + case FilterTypes.comingSoon: + query !== '' + ? setOrderedCatalogList( + sortedByAccess.filter((component: ComponentInfo) => + component?.displayName?.toLowerCase().includes(query) + ) + ) + : setOrderedCatalogList(sortedByAccess); + break; + case FilterTypes.notInstalled: + query !== '' + ? setOrderedCatalogList( + sortedByUninstalled.filter((component: ComponentInfo) => + component?.displayName?.toLowerCase().includes(query) + ) + ) + : setOrderedCatalogList(sortedByUninstalled); + break; + } + } + }, [catalogList, currentFilter, query]); return (
-

{catalogPageTitle}

+
+

{catalogPageTitle}

+ +
- {orderedCatalogList && + {orderedCatalogList && orderedCatalogList.length > 0 ? ( orderedCatalogList.map((infoItem: ComponentInfo) => { if (infoItem?.name && infoItem?.displayName) { return ; } - })} + }) + ) : ( +
+

{t('nothingFound')}

+ {t('noMatchingCatalogs')} +
+ )}
); diff --git a/frontend/inbox/src/pages/Contacts/ContactInformation/index.tsx b/frontend/inbox/src/pages/Contacts/ContactInformation/index.tsx index 810d3a055f..577fdfa0be 100644 --- a/frontend/inbox/src/pages/Contacts/ContactInformation/index.tsx +++ b/frontend/inbox/src/pages/Contacts/ContactInformation/index.tsx @@ -1,5 +1,5 @@ import {updateContactDetails, updateConversationContactInfo} from '../../../actions'; -import React, {useEffect, useState} from 'react'; +import React, {useCallback, useEffect, useState} from 'react'; import {useTranslation} from 'react-i18next'; import {connect, ConnectedProps} from 'react-redux'; import styles from './index.module.scss'; @@ -87,16 +87,16 @@ const ContactInformation = (props: ContactInformationProps) => { }); } - setShowEditDisplayName(!saveEditDisplayName); + toggleEditDisplayName(); }; - const cancelEditDisplayName = () => { + const toggleEditDisplayName = useCallback(() => { setDisplayName(contact?.displayName); useAnimation(showEditDisplayName, setShowEditDisplayName, setFade, 400); - }; + }, [showEditDisplayName, setShowEditDisplayName]); const editDisplayName = () => { - setShowEditDisplayName(!showEditDisplayName); + toggleEditDisplayName(); }; const getUpdatedInfo = () => { @@ -163,7 +163,7 @@ const ContactInformation = (props: ContactInformationProps) => { dataCy={cyDisplayNameInput} />
-
)}
+ {showNewConnectionModal && ( + setShowNewConnectionModal(false)} + headerClassName={styles.connectChannelModalHeader} + > + + + )} {areConnectedChannels && ( ) => { if (source === Source.facebook) return ; if (source === Source.instagram) return ; if (source === Source.google) return ; + if (source === Source.whatsapp) return ; if (source === Source.twilioSMS) return ; if (source === Source.twilioWhatsApp) return ; - if (source === Source.viber) return

{t('pageUnderConstruction')}

; + if (source === Source.viber) return ; } return ; diff --git a/frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/ChatPluginConnect.tsx b/frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/ChatPluginConnect.tsx index 3a79078634..ea96a41e1a 100644 --- a/frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/ChatPluginConnect.tsx +++ b/frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/ChatPluginConnect.tsx @@ -12,7 +12,7 @@ import {cyChannelCreatedChatPluginCloseButton} from 'handles'; import {Button, LinkButton, NotificationComponent, SettingsModal} from 'components'; import {Channel, NotificationModel, Source, ChatpluginConfig, DefaultConfig} from 'model'; -import {ConnectNewChatPlugin} from './sections/ConnectNewChatPlugin'; +import ConnectNewChatPlugin from './sections/ConnectNewChatPlugin'; import {ReactComponent as AiryAvatarIcon} from 'assets/images/icons/airyAvatar.svg'; import {ReactComponent as CheckmarkIcon} from 'assets/images/icons/checkmarkFilled.svg'; @@ -61,23 +61,6 @@ const ChatPluginConnect = (props: ConnectedProps) => { const {t} = useTranslation(); const CHAT_PLUGIN_ROUTE = `${CONNECTORS_ROUTE}/chatplugin`; - const createNewConnection = (displayName: string, imageUrl?: string) => { - props - .connectChatPlugin({ - name: displayName, - ...(imageUrl.length > 0 && { - imageUrl: imageUrl, - }), - }) - .then((id: string) => { - setCurrentChannelId(id); - setShowCreatedModal(true); - }) - .catch((error: Error) => { - console.error(error); - }); - }; - const disconnectChannel = (channel: Channel) => { if (window.confirm(t('deleteChannel'))) { props.disconnectChannel({source: 'chatplugin', channelId: channel.id}).catch((error: Error) => { @@ -111,11 +94,16 @@ const ChatPluginConnect = (props: ConnectedProps) => { navigate(`${CONNECTORS_ROUTE}/${Source.chatPlugin}/connected`); }; + const handleNewConnection = (id: string, showModal: boolean) => { + setCurrentChannelId(id); + setShowCreatedModal(showModal); + }; + const PageContent = () => { switch (currentPage) { case Pages.createUpdate: if (newChannel) { - return ; + return ; } if (channelId?.length > 0) { return ( diff --git a/frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/sections/ConnectNewChatPlugin.module.scss b/frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/sections/ConnectNewChatPlugin.module.scss index 3c4f80aa7e..2dc45d65b1 100644 --- a/frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/sections/ConnectNewChatPlugin.module.scss +++ b/frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/sections/ConnectNewChatPlugin.module.scss @@ -1,23 +1,20 @@ -.newPageParagraph { - margin: 24px 0; - color: var(--color-text-gray); -} - .formWrapper { - width: 340px; - align-self: center; + display: flex; + flex-direction: column; + width: 100%; + align-items: flex-start; + button { + margin-top: 32px; + } } -.formRow { - margin-bottom: 16px; - - &:hover { - .actionToolTip { - display: block; - } - } +.formWrapperModal { + @extend .formWrapper; + align-items: center; } -.settings { - width: 20rem; +.formRow { + height: 82px; + width: 75%; + max-width: 20rem; } diff --git a/frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/sections/ConnectNewChatPlugin.tsx b/frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/sections/ConnectNewChatPlugin.tsx index b30560d2ee..d6c9f6a088 100644 --- a/frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/sections/ConnectNewChatPlugin.tsx +++ b/frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/sections/ConnectNewChatPlugin.tsx @@ -3,71 +3,94 @@ import {useTranslation} from 'react-i18next'; import {Button, Input} from 'components'; import {cyChannelsChatPluginFormNameInput, cyChannelsChatPluginFormSubmitButton} from 'handles'; import styles from './ConnectNewChatPlugin.module.scss'; +import {connectChatPlugin} from '../../../../../../actions/channel'; +import {connect, ConnectedProps} from 'react-redux'; +import {useNavigate} from 'react-router-dom'; +import {CONNECTORS_ROUTE} from '../../../../../../routes/routes'; -interface ConnectNewChatPluginProps { - createNewConnection: (displayName: string, imageUrl?: string) => void; -} +const mapDispatchToProps = { + connectChatPlugin, +}; + +const connector = connect(null, mapDispatchToProps); -export const ConnectNewChatPlugin = (props: ConnectNewChatPluginProps) => { - const {createNewConnection} = props; +type ConnectNewChatPluginProps = { + modal?: boolean; + connectNew?: (id: string, showModal: boolean) => void; +} & ConnectedProps; + +const ConnectNewChatPlugin = (props: ConnectNewChatPluginProps) => { + const {modal, connectNew, connectChatPlugin} = props; const [displayName, setDisplayName] = useState(''); const [imageUrl, setImageUrl] = useState(''); const {t} = useTranslation(); + const navigate = useNavigate(); + const CHAT_PLUGIN_ROUTE = `${CONNECTORS_ROUTE}/chatplugin`; - return ( -
-
-
- -
- ) => { - setDisplayName(e.target.value); - }} - label={t('displayName')} - placeholder={t('addDisplayName')} - required - height={32} - fontClass="font-base" - dataCy={cyChannelsChatPluginFormNameInput} - /> -
+ const createNewConnection = (displayName: string, imageUrl?: string) => { + connectChatPlugin({ + name: displayName, + ...(imageUrl.length > 0 && { + imageUrl: imageUrl, + }), + }) + .then((id: string) => { + modal ? navigate(`${CHAT_PLUGIN_ROUTE}/${id}`) : connectNew(id, true); + }) + .catch((error: Error) => { + console.error(error); + }); + }; -
- ) => { - setImageUrl(e.target.value); - }} - label={t('imageUrl')} - showLabelIcon - tooltipText={t('imageUrlHint')} - placeholder={t('imageUrlPlaceholder')} - height={32} - fontClass="font-base" - /> -
- - -
+ return ( +
+
+ ) => { + setDisplayName(e.target.value); + }} + label={t('displayName')} + placeholder={t('addDisplayName')} + required + height={32} + fontClass="font-base" + dataCy={cyChannelsChatPluginFormNameInput} + /> +
+
+ ) => { + setImageUrl(e.target.value); + }} + label={t('imageUrl')} + showLabelIcon + tooltipText={t('imageUrlHint')} + placeholder={t('imageUrlPlaceholder')} + height={32} + fontClass="font-base" + />
-
+ + ); }; + +export default connector(ConnectNewChatPlugin); diff --git a/frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/sections/CreateUpdateSection/CreateUpdateSection.module.scss b/frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/sections/CreateUpdateSection/CreateUpdateSection.module.scss index 8627ca430b..cd8a2a4971 100644 --- a/frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/sections/CreateUpdateSection/CreateUpdateSection.module.scss +++ b/frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/sections/CreateUpdateSection/CreateUpdateSection.module.scss @@ -1,8 +1,33 @@ .formWrapper { align-self: center; display: flex; + flex-direction: column; + width: 100%; } -.settings { +.inputContainer { width: 20rem; + .input { + height: 82px; + width: 75%; + } +} + +.inputContainerModal { + @extend .inputContainer; + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + .input { + height: 82px; + width: 75%; + } +} + +.smartButtonContainer { + display: flex; + width: 100%; + align-items: center; + margin-top: 32px; } diff --git a/frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/sections/CreateUpdateSection/CreateUpdateSection.tsx b/frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/sections/CreateUpdateSection/CreateUpdateSection.tsx index 0e8d608e20..d0bd7870f6 100644 --- a/frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/sections/CreateUpdateSection/CreateUpdateSection.tsx +++ b/frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/sections/CreateUpdateSection/CreateUpdateSection.tsx @@ -14,25 +14,26 @@ const mapDispatchToProps = { const connector = connect(null, mapDispatchToProps); type InstallUpdateSectionProps = { - channel: Channel; - displayName: string; - imageUrl: string; - setNotification: Dispatch>; + channel?: Channel; + displayName?: string; + imageUrl?: string; + setNotification?: Dispatch>; + modal?: boolean; } & ConnectedProps; const CreateUpdateSection = (props: InstallUpdateSectionProps) => { - const {channel, displayName, imageUrl, setNotification} = props; + const {channel, displayName, imageUrl, setNotification, modal} = props; const [submit, setSubmit] = useState(false); - const [newDisplayName, setNewDisplayName] = useState(displayName || channel?.metadata?.name); - const [newImageUrl, setNewImageUrl] = useState(imageUrl || channel?.metadata?.imageUrl); + const [newDisplayName, setNewDisplayName] = useState(displayName || channel?.metadata?.name || ''); + const [newImageUrl, setNewImageUrl] = useState(imageUrl || channel?.metadata?.imageUrl || ''); const [isPending, setIsPending] = useState(false); const {t} = useTranslation(); const updateConnection = (displayName: string, imageUrl?: string) => { setIsPending(true); props - .updateChannel({channelId: channel.id, name: displayName, imageUrl: imageUrl}) + .updateChannel({channelId: channel?.id, name: displayName, imageUrl: imageUrl}) .then(() => { setNotification({show: true, text: t('updateSuccessful'), successful: true}); }) @@ -46,60 +47,60 @@ const CreateUpdateSection = (props: InstallUpdateSectionProps) => { }; return ( -
-
-
) => { - event.preventDefault(); - submit && updateConnection(newDisplayName, newImageUrl); - }} - > -
- ) => { - setNewDisplayName(e.target.value); - }} - label={t('displayName')} - placeholder={t('addAName')} - required - height={32} - fontClass="font-base" - dataCy={cyChannelsChatPluginFormNameInput} - /> -
- -
- ) => { - setNewImageUrl(e.target.value); - }} - label={t('imageUrl')} - placeholder={t('imageUrlPlaceholder')} - showLabelIcon - tooltipText={t('imageUrlHint')} - height={32} - fontClass="font-base" - /> -
- setSubmit(true)} - pending={isPending} - type="submit" - styleVariant="small" - disabled={newDisplayName === '' || newDisplayName === displayName || isPending} + ) => { + event.preventDefault(); + submit && updateConnection(newDisplayName, newImageUrl); + }} + > +
+
+ ) => { + setNewDisplayName(e.target.value); + }} + label={t('displayName')} + placeholder={t('addAName')} + required + height={32} + fontClass="font-base" + dataCy={cyChannelsChatPluginFormNameInput} + /> +
+
+ ) => { + setNewImageUrl(e.target.value); + }} + label={t('imageUrl')} + placeholder={t('imageUrlPlaceholder')} + showLabelIcon + tooltipText={t('imageUrlHint')} + height={32} + fontClass="font-base" /> - +
+
+
+ setSubmit(true)} + pending={isPending} + type="submit" + styleVariant="small" + disabled={newDisplayName === '' || newDisplayName === displayName || isPending} + />
-
+ ); }; diff --git a/frontend/control-center/src/pages/Connectors/Providers/Facebook/Messenger/FacebookConnect.module.scss b/frontend/control-center/src/pages/Connectors/Providers/Facebook/Messenger/FacebookConnect.module.scss index 09a67df6f1..c5bc0efc88 100644 --- a/frontend/control-center/src/pages/Connectors/Providers/Facebook/Messenger/FacebookConnect.module.scss +++ b/frontend/control-center/src/pages/Connectors/Providers/Facebook/Messenger/FacebookConnect.module.scss @@ -50,6 +50,16 @@ } } +.inputContainerModal { + @extend .inputContainer; + align-items: center; + width: 100%; + + label { + width: 75%; + } +} + .subtitle { display: flex; flex-direction: column; @@ -60,3 +70,25 @@ height: 13px; } } + +.smartButtonContainer { + display: flex; + width: 100%; + align-items: flex-start; + flex-direction: column; + span { + @include font-base; + color: var(--color-red-alert); + opacity: 0; + transition: all 1s ease; + } + .errorMessage { + opacity: 1; + transform: translateY(50%); + } +} + +.smartButtonModalContainer { + @extend .smartButtonContainer; + align-items: center; +} diff --git a/frontend/control-center/src/pages/Connectors/Providers/Facebook/Messenger/FacebookConnect.tsx b/frontend/control-center/src/pages/Connectors/Providers/Facebook/Messenger/FacebookConnect.tsx index f520972d54..133b68d03e 100644 --- a/frontend/control-center/src/pages/Connectors/Providers/Facebook/Messenger/FacebookConnect.tsx +++ b/frontend/control-center/src/pages/Connectors/Providers/Facebook/Messenger/FacebookConnect.tsx @@ -16,8 +16,12 @@ const mapDispatchToProps = { const connector = connect(null, mapDispatchToProps); -const FacebookConnect = (props: ConnectedProps) => { - const {connectFacebookChannel} = props; +type FacebookConnectProps = { + modal?: boolean; +} & ConnectedProps; + +const FacebookConnect = (props: FacebookConnectProps) => { + const {connectFacebookChannel, modal} = props; const channel = useCurrentChannel(); const navigate = useNavigate(); const {t} = useTranslation(); @@ -25,12 +29,17 @@ const FacebookConnect = (props: ConnectedProps) => { const [token, setToken] = useState(channel?.metadata?.pageToken || ''); const [name, setName] = useState(channel?.metadata?.name || ''); const [image, setImage] = useState(channel?.metadata?.imageUrl || ''); + const [error, setError] = useState(false); + const connectError = t('connectFailed'); const buttonTitle = channel ? t('updatePage') : t('connectPage') || ''; const [newButtonTitle, setNewButtonTitle] = useState(''); - const [errorMessage, setErrorMessage] = useState(''); const [notification, setNotification] = useState(null); const [isPending, setIsPending] = useState(false); + useEffect(() => { + modal && setError(false); + }, [id, token]); + useEffect(() => { if (channel?.sourceChannelId !== id && !!channel) { setNotification({show: true, text: t('newChannelInfo'), info: true}); @@ -72,8 +81,8 @@ const FacebookConnect = (props: ConnectedProps) => { navigate(CONNECTORS_ROUTE + '/facebook/connected', {replace: true}); }) .catch((error: Error) => { - setNotification({show: true, text: t('updateFailed'), successful: false}); - setErrorMessage(t('errorMessage')); + setNotification({show: true, text: channel ? t('updateFailed') : t('connectFailed'), successful: false}); + modal && setError(true); console.error(error); }) .finally(() => { @@ -83,17 +92,18 @@ const FacebookConnect = (props: ConnectedProps) => { return (
-
+
) => setId(event.target.value)} minLength={6} required={true} height={32} - hint={errorMessage} fontClass="font-base" /> ) => { label={t('token')} placeholder={t('tokenPlaceholder')} value={token} + showLabelIcon + tooltipText={t('token')} onChange={(event: React.ChangeEvent) => setToken(event.target.value)} required={true} height={32} - hint={errorMessage} fontClass="font-base" /> ) => setName(event.target.value)} height={32} @@ -120,8 +133,10 @@ const FacebookConnect = (props: ConnectedProps) => { ) => setImage(event.target.value)} @@ -129,18 +144,20 @@ const FacebookConnect = (props: ConnectedProps) => { fontClass="font-base" />
- connectNewChannel()} - /> - {notification?.show && ( +
+ connectNewChannel()} + /> + {modal && {connectError}} +
+ {notification?.show && !modal && ( ) => { - const {connectGoogleChannel} = props; +type GoogleConnectProps = { + modal?: boolean; +} & ConnectedProps; + +const GoogleConnect = (props: GoogleConnectProps) => { + const {connectGoogleChannel, modal} = props; const channel = useCurrentChannel(); const navigate = useNavigate(); const {t} = useTranslation(); const [id, setId] = useState(channel?.sourceChannelId || ''); const [name, setName] = useState(channel?.metadata?.name || ''); const [image, setImage] = useState(channel?.metadata?.imageUrl || ''); + const [error, setError] = useState(false); + const connectError = t('connectFailed'); const buttonTitle = channel ? t('updatePage') : t('connectPage') || ''; - const [errorMessage, setErrorMessage] = useState(''); const [notification, setNotification] = useState(null); const [isPending, setIsPending] = useState(false); const [newButtonTitle, setNewButtonTitle] = useState(''); + useEffect(() => { + modal && setError(false); + }, [id, name]); + const buttonStatus = () => { return ( !(id.length > 5 && name.length > 0) || @@ -76,11 +85,11 @@ const GoogleConnect = (props: ConnectedProps) => { .catch((error: Error) => { setNotification({ show: true, - text: buttonTitle === t('connectPage') ? t('createFailed') : 'updateFailed', + text: buttonTitle === t('connectPage') ? t('connectFailed') : 'updateFailed', successful: false, info: false, }); - setErrorMessage(t('errorMessage')); + modal && setError(true); console.error(error); }) .finally(() => { @@ -90,23 +99,26 @@ const GoogleConnect = (props: ConnectedProps) => { return (
-
+
) => setId(event.target.value)} minLength={6} required={true} height={32} - hint={errorMessage} fontClass="font-base" /> ) => setName(event.target.value)} required={true} @@ -117,26 +129,30 @@ const GoogleConnect = (props: ConnectedProps) => { ) => setImage(event.target.value)} height={32} fontClass="font-base" />
- connectNewChannel()} - /> - {notification?.show && ( +
+ connectNewChannel()} + /> + {modal && {connectError}} +
+ {notification?.show && !modal && ( ; const mapDispatchToProps = {connectTwilioWhatsapp, connectTwilioSms}; @@ -25,17 +26,24 @@ const mapDispatchToProps = {connectTwilioWhatsapp, connectTwilioSms}; const connector = connect(null, mapDispatchToProps); const TwilioConnect = (props: TwilioConnectProps) => { - const {channel, source, buttonText, connectTwilioWhatsapp, connectTwilioSms} = props; + const {channel, buttonText, connectTwilioWhatsapp, connectTwilioSms, modal} = props; const {t} = useTranslation(); - + const params = useParams(); + const {source} = params; const navigate = useNavigate(); const [numberInput, setNumberInput] = useState(channel?.sourceChannelId || ''); const [nameInput, setNameInput] = useState(channel?.metadata?.name || ''); const [imageUrlInput, setImageUrlInput] = useState(channel?.metadata?.imageUrl || ''); + const [error, setError] = useState(false); + const connectError = t('connectFailed'); const [notification, setNotification] = useState(null); const [newButtonText, setNewButtonText] = useState(''); const [isPending, setIsPending] = useState(false); + useEffect(() => { + modal && setError(false); + }, [numberInput]); + useEffect(() => { if (channel?.sourceChannelId !== numberInput && !!channel) { setNotification({show: true, text: t('newChannelInfo'), info: true}); @@ -76,7 +84,7 @@ const TwilioConnect = (props: TwilioConnectProps) => { imageUrl: imageUrlInput, }; - if (source === Source.twilioWhatsApp) { + if (source === Source.twilioWhatsApp || props.source === Source.twilioWhatsApp) { connectTwilioWhatsapp(connectPayload) .then(() => { navigate(CONNECTORS_ROUTE + `/twilio.whatsapp/connected`, { @@ -91,13 +99,14 @@ const TwilioConnect = (props: TwilioConnectProps) => { successful: false, info: false, }); + modal && setError(true); console.error(error); }) .finally(() => { setIsPending(false); }); } - if (source === Source.twilioSMS) { + if (source === Source.twilioSMS || props.source === Source.twilioSMS) { connectTwilioSms(connectPayload) .then(() => { navigate(CONNECTORS_ROUTE + `/twilio.sms/connected`, {replace: true, state: {source: 'twilio.sms'}}); @@ -109,6 +118,7 @@ const TwilioConnect = (props: TwilioConnectProps) => { successful: false, info: false, }); + modal && setError(true); console.error(error); }) .finally(() => { @@ -118,66 +128,65 @@ const TwilioConnect = (props: TwilioConnectProps) => { }; return ( -
-
-
-
- -
-
- -
-
- -
- ) => connectTwilioChannel(e)} - /> -
- {notification?.show && ( - - )} - -
+
+
+ +
+
+ +
+
+ +
+
+ ) => connectTwilioChannel(e)} + /> + {modal && {connectError}} +
+ {notification?.show && !modal && ( + + )} + ); }; diff --git a/frontend/control-center/src/pages/Connectors/Providers/Viber/ViberConnect.module.scss b/frontend/control-center/src/pages/Connectors/Providers/Viber/ViberConnect.module.scss new file mode 100644 index 0000000000..c5bc0efc88 --- /dev/null +++ b/frontend/control-center/src/pages/Connectors/Providers/Viber/ViberConnect.module.scss @@ -0,0 +1,94 @@ +@import 'assets/scss/fonts.scss'; +@import 'assets/scss/colors.scss'; + +.wrapper { + background: var(--color-background-white); + display: block; + border-radius: 10px; + width: 100%; + height: 100%; +} + +.headline { + @include font-xl; + font-weight: bold; + color: var(--color-text-contrast); + margin-bottom: 8px; +} + +.backButton { + display: block; + margin-bottom: 16px; + cursor: pointer; + text-decoration: none; + &:hover { + text-decoration: underline; + } +} + +.backIcon { + height: 13px; + width: 17px; + path { + fill: var(--color-airy-blue); + } + margin-right: 8px; +} + +.inputContainer { + display: flex; + flex-direction: column; + margin-bottom: 32px; + width: 474px; + + label { + height: 82px; + } + + input { + @include font-base; + } +} + +.inputContainerModal { + @extend .inputContainer; + align-items: center; + width: 100%; + + label { + width: 75%; + } +} + +.subtitle { + display: flex; + flex-direction: column; + @include font-s; + color: var(--color-airy-blue); + img { + width: 17px; + height: 13px; + } +} + +.smartButtonContainer { + display: flex; + width: 100%; + align-items: flex-start; + flex-direction: column; + span { + @include font-base; + color: var(--color-red-alert); + opacity: 0; + transition: all 1s ease; + } + .errorMessage { + opacity: 1; + transform: translateY(50%); + } +} + +.smartButtonModalContainer { + @extend .smartButtonContainer; + align-items: center; +} diff --git a/frontend/control-center/src/pages/Connectors/Providers/Viber/ViberConnect.tsx b/frontend/control-center/src/pages/Connectors/Providers/Viber/ViberConnect.tsx new file mode 100644 index 0000000000..ae37ac8879 --- /dev/null +++ b/frontend/control-center/src/pages/Connectors/Providers/Viber/ViberConnect.tsx @@ -0,0 +1,129 @@ +import React, {useEffect, useState} from 'react'; +import {useTranslation} from 'react-i18next'; +import {connect, ConnectedProps} from 'react-redux'; +import {connectViberChannel} from '../../../../actions/channel'; +import {Input, NotificationComponent, SmartButton} from 'components'; +import {ConnectViberRequestPayload} from 'httpclient/src'; +import styles from './ViberConnect.module.scss'; +import {useCurrentChannel} from '../../../../selectors/channels'; +import {NotificationModel} from 'model'; +import {useNavigate} from 'react-router-dom'; +import {CONNECTORS_ROUTE} from '../../../../routes/routes'; + +const mapDispatchToProps = { + connectViberChannel, +}; + +const connector = connect(null, mapDispatchToProps); + +type ViberConnectProps = { + modal?: boolean; +} & ConnectedProps; + +const ViberConnect = (props: ViberConnectProps) => { + const {connectViberChannel, modal} = props; + const channel = useCurrentChannel(); + const navigate = useNavigate(); + const {t} = useTranslation(); + const [name, setName] = useState(channel?.metadata?.name || ''); + const [image, setImage] = useState(channel?.metadata?.imageUrl || ''); + const [error, setError] = useState(false); + const connectError = t('connectFailed'); + const buttonTitle = channel ? t('updatePage') : t('connectPage') || ''; + const [notification, setNotification] = useState(null); + const [isPending, setIsPending] = useState(false); + + useEffect(() => { + modal && setError(false); + }, [name, image]); + + const buttonStatus = () => { + return ( + (channel?.metadata?.name === name && channel?.metadata?.imageUrl === image) || + (!!channel?.metadata?.imageUrl && image === '') + ); + }; + + const connectNewChannel = () => { + const connectPayload: ConnectViberRequestPayload = { + ...(name && + name !== '' && { + name, + }), + ...(image && + image !== '' && { + imageUrl: image, + }), + }; + + setIsPending(true); + + connectViberChannel(connectPayload) + .then(() => { + navigate(CONNECTORS_ROUTE + '/viber/connected', {replace: true}); + }) + .catch((error: Error) => { + setNotification({show: true, text: t('updateFailed'), successful: false}); + modal && setError(true); + console.error(error); + }) + .finally(() => { + setIsPending(false); + }); + }; + + return ( +
+
+ ) => setName(event.target.value)} + height={32} + fontClass="font-base" + /> + ) => setImage(event.target.value)} + height={32} + fontClass="font-base" + /> +
+
+ connectNewChannel()} + /> + {modal && {connectError}} +
+ {notification?.show && !modal && ( + + )} +
+ ); +}; + +export default connector(ViberConnect); diff --git a/frontend/control-center/src/pages/Connectors/Providers/WhatsappBusinessCloud/WhatsappConnect.module.scss b/frontend/control-center/src/pages/Connectors/Providers/WhatsappBusinessCloud/WhatsappConnect.module.scss new file mode 100644 index 0000000000..c5bc0efc88 --- /dev/null +++ b/frontend/control-center/src/pages/Connectors/Providers/WhatsappBusinessCloud/WhatsappConnect.module.scss @@ -0,0 +1,94 @@ +@import 'assets/scss/fonts.scss'; +@import 'assets/scss/colors.scss'; + +.wrapper { + background: var(--color-background-white); + display: block; + border-radius: 10px; + width: 100%; + height: 100%; +} + +.headline { + @include font-xl; + font-weight: bold; + color: var(--color-text-contrast); + margin-bottom: 8px; +} + +.backButton { + display: block; + margin-bottom: 16px; + cursor: pointer; + text-decoration: none; + &:hover { + text-decoration: underline; + } +} + +.backIcon { + height: 13px; + width: 17px; + path { + fill: var(--color-airy-blue); + } + margin-right: 8px; +} + +.inputContainer { + display: flex; + flex-direction: column; + margin-bottom: 32px; + width: 474px; + + label { + height: 82px; + } + + input { + @include font-base; + } +} + +.inputContainerModal { + @extend .inputContainer; + align-items: center; + width: 100%; + + label { + width: 75%; + } +} + +.subtitle { + display: flex; + flex-direction: column; + @include font-s; + color: var(--color-airy-blue); + img { + width: 17px; + height: 13px; + } +} + +.smartButtonContainer { + display: flex; + width: 100%; + align-items: flex-start; + flex-direction: column; + span { + @include font-base; + color: var(--color-red-alert); + opacity: 0; + transition: all 1s ease; + } + .errorMessage { + opacity: 1; + transform: translateY(50%); + } +} + +.smartButtonModalContainer { + @extend .smartButtonContainer; + align-items: center; +} diff --git a/frontend/control-center/src/pages/Connectors/Providers/WhatsappBusinessCloud/WhatsappConnect.tsx b/frontend/control-center/src/pages/Connectors/Providers/WhatsappBusinessCloud/WhatsappConnect.tsx new file mode 100644 index 0000000000..017d57c287 --- /dev/null +++ b/frontend/control-center/src/pages/Connectors/Providers/WhatsappBusinessCloud/WhatsappConnect.tsx @@ -0,0 +1,170 @@ +import React, {useEffect, useState} from 'react'; +import {useTranslation} from 'react-i18next'; +import {connect, ConnectedProps} from 'react-redux'; +import {connectWhatsappChannel} from '../../../../actions/channel'; +import {Input, NotificationComponent, SmartButton} from 'components'; +import {ConnectWhatsappRequestPayload} from 'httpclient/src'; +import styles from './WhatsappConnect.module.scss'; +import {useCurrentChannel} from '../../../../selectors/channels'; +import {NotificationModel} from 'model'; +import {useNavigate} from 'react-router-dom'; +import {CONNECTORS_ROUTE} from '../../../../routes/routes'; + +const mapDispatchToProps = { + connectWhatsappChannel, +}; + +const connector = connect(null, mapDispatchToProps); + +type WhatsappConnectProps = { + modal?: boolean; +} & ConnectedProps; + +const WhatsappConnect = (props: WhatsappConnectProps) => { + const {connectWhatsappChannel, modal} = props; + const channel = useCurrentChannel(); + const navigate = useNavigate(); + const {t} = useTranslation(); + const [phoneNumberId, setPhoneNumberId] = useState(channel?.sourceChannelId || ''); + const [userToken, setUserToken] = useState(channel?.metadata?.userToken || ''); + const [name, setName] = useState(channel?.metadata?.name || ''); + const [image, setImage] = useState(channel?.metadata?.imageUrl || ''); + const [error, setError] = useState(false); + const connectError = t('connectFailed'); + const buttonTitle = channel ? t('updatePage') : t('connectPage') || ''; + const [newButtonTitle, setNewButtonTitle] = useState(''); + const [notification, setNotification] = useState(null); + const [isPending, setIsPending] = useState(false); + + useEffect(() => { + modal && setError(false); + }, [phoneNumberId, userToken, name]); + + useEffect(() => { + if (channel?.sourceChannelId !== phoneNumberId && !!channel) { + setNotification({show: true, text: t('newChannelInfo'), info: true}); + setNewButtonTitle(t('connect')); + } else { + setNewButtonTitle(buttonTitle); + } + }, [phoneNumberId]); + + const buttonStatus = () => { + return ( + !(phoneNumberId.length > 5 && userToken !== '' && name !== '') || + (channel?.sourceChannelId === phoneNumberId && + channel?.metadata?.pageToken === userToken && + channel?.metadata?.name === name && + channel?.metadata?.imageUrl === image) || + (!!channel?.metadata?.imageUrl && image === '') + ); + }; + + const connectNewChannel = () => { + const connectPayload: ConnectWhatsappRequestPayload = { + phoneNumberId: phoneNumberId, + userToken: userToken, + name: name, + ...(image && + image !== '' && { + imageUrl: image, + }), + }; + + setIsPending(true); + + connectWhatsappChannel(connectPayload) + .then(() => { + navigate(CONNECTORS_ROUTE + '/whatsapp/connected', {replace: true}); + }) + .catch((error: Error) => { + setNotification({show: true, text: t('updateFailed'), successful: false}); + modal && setError(true); + console.error(error); + }) + .finally(() => { + setIsPending(false); + }); + }; + + return ( +
+
+ ) => setPhoneNumberId(event.target.value)} + minLength={6} + required + height={32} + fontClass="font-base" + /> + ) => setUserToken(event.target.value)} + required + height={32} + fontClass="font-base" + /> + ) => setName(event.target.value)} + required + height={32} + fontClass="font-base" + /> + ) => setImage(event.target.value)} + height={32} + fontClass="font-base" + /> +
+
+ connectNewChannel()} + /> + {modal && {connectError}} +
+ {notification?.show && !modal && ( + + )} +
+ ); +}; + +export default connector(WhatsappConnect); diff --git a/lib/typescript/httpclient/src/client.ts b/lib/typescript/httpclient/src/client.ts index 469fad0874..a115ef81dc 100644 --- a/lib/typescript/httpclient/src/client.ts +++ b/lib/typescript/httpclient/src/client.ts @@ -12,6 +12,7 @@ import { ConnectChatPluginRequestPayload, ConnectTwilioSmsRequestPayload, ConnectTwilioWhatsappRequestPayload, + ConnectWhatsappRequestPayload, ConnectChannelGoogleRequestPayload, UpdateChannelRequestPayload, ListTemplatesRequestPayload, @@ -31,6 +32,7 @@ import { EnableDisableComponentRequestPayload, UpdateComponentConfigurationRequestPayload, InstallUninstallComponentRequestPayload, + ConnectViberRequestPayload, } from './payload'; import { listChannelsDef, @@ -41,6 +43,7 @@ import { connectChatPluginChannelDef, connectTwilioSmsChannelDef, connectTwilioWhatsappChannelDef, + connectWhatsappChannelDef, connectGoogleChannelDef, updateChannelDef, disconnectChannelDef, @@ -75,6 +78,7 @@ import { installComponentDef, uninstallComponentDef, componentsListDef, + connectViberChannelDef, } from './endpoints'; import 'isomorphic-fetch'; import FormData from 'form-data'; @@ -193,6 +197,10 @@ export class HttpClient { connectTwilioWhatsappChannelDef ); + public connectWhatsappChannel = this.getRequest(connectWhatsappChannelDef); + + public connectViberChannel = this.getRequest(connectViberChannelDef); + public connectGoogleChannel = this.getRequest(connectGoogleChannelDef); public connectInstagramChannel = this.getRequest( diff --git a/lib/typescript/httpclient/src/endpoints/connectViberChannel.ts b/lib/typescript/httpclient/src/endpoints/connectViberChannel.ts new file mode 100644 index 0000000000..f341bc3b46 --- /dev/null +++ b/lib/typescript/httpclient/src/endpoints/connectViberChannel.ts @@ -0,0 +1,10 @@ +import camelcaseKeys from 'camelcase-keys'; + +export const connectViberChannelDef = { + endpoint: 'channels.viber.connect', + mapRequest: ({name, imageUrl}) => ({ + name, + image_url: imageUrl, + }), + mapResponse: response => camelcaseKeys(response, {deep: true, stopPaths: ['metadata.user_data']}), +}; diff --git a/lib/typescript/httpclient/src/endpoints/connectWhatsappChannel.ts b/lib/typescript/httpclient/src/endpoints/connectWhatsappChannel.ts new file mode 100644 index 0000000000..69cdf1b0e7 --- /dev/null +++ b/lib/typescript/httpclient/src/endpoints/connectWhatsappChannel.ts @@ -0,0 +1,12 @@ +import camelcaseKeys from 'camelcase-keys'; + +export const connectWhatsappChannelDef = { + endpoint: 'channels.whatsapp.connect', + mapRequest: ({phoneNumberId, userToken, name, imageUrl}) => ({ + phone_number_id: phoneNumberId, + user_token: userToken, + name, + image_url: imageUrl, + }), + mapResponse: response => camelcaseKeys(response, {deep: true, stopPaths: ['metadata.user_data']}), +}; diff --git a/lib/typescript/httpclient/src/endpoints/index.ts b/lib/typescript/httpclient/src/endpoints/index.ts index 16d3de853f..f9110f1742 100644 --- a/lib/typescript/httpclient/src/endpoints/index.ts +++ b/lib/typescript/httpclient/src/endpoints/index.ts @@ -4,6 +4,8 @@ export * from './connectFacebookChannel'; export * from './connectChatPluginChannel'; export * from './connectTwilioSmsChannel'; export * from './connectTwilioWhatsappChannel'; +export * from './connectWhatsappChannel'; +export * from './connectViberChannel'; export * from './connectGoogleChannel'; export * from './connectInstagramChannel'; export * from './disconnectChannel'; diff --git a/lib/typescript/httpclient/src/payload/ConnectViberRequestPayload.ts b/lib/typescript/httpclient/src/payload/ConnectViberRequestPayload.ts new file mode 100644 index 0000000000..4cfe4120d4 --- /dev/null +++ b/lib/typescript/httpclient/src/payload/ConnectViberRequestPayload.ts @@ -0,0 +1,4 @@ +export interface ConnectViberRequestPayload { + name?: string; + imageUrl?: string; +} diff --git a/lib/typescript/httpclient/src/payload/ConnectWhatsappRequestPayload.ts b/lib/typescript/httpclient/src/payload/ConnectWhatsappRequestPayload.ts new file mode 100644 index 0000000000..79d8c6f1ca --- /dev/null +++ b/lib/typescript/httpclient/src/payload/ConnectWhatsappRequestPayload.ts @@ -0,0 +1,6 @@ +export interface ConnectWhatsappRequestPayload { + phoneNumberId?: string; + userToken: string; + name?: string; + imageUrl?: string; +} diff --git a/lib/typescript/httpclient/src/payload/index.ts b/lib/typescript/httpclient/src/payload/index.ts index a2cacc1a0f..cc82270b26 100644 --- a/lib/typescript/httpclient/src/payload/index.ts +++ b/lib/typescript/httpclient/src/payload/index.ts @@ -2,6 +2,8 @@ export * from './ConnectChannelRequestPayload'; export * from './ConnectChatPluginRequestPayload'; export * from './ConnectTwilioSmsRequestPayload'; export * from './ConnectTwilioWhatsappRequestPayload'; +export * from './ConnectWhatsappRequestPayload'; +export * from './ConnectViberRequestPayload'; export * from './ConnectChannelGoogleRequestPayload'; export * from './ConnectChannelInstagramRequestPayload'; export * from './CreateTagRequestPayload'; diff --git a/lib/typescript/translations/translations.ts b/lib/typescript/translations/translations.ts index e23ef0e374..1f55353de6 100644 --- a/lib/typescript/translations/translations.ts +++ b/lib/typescript/translations/translations.ts @@ -370,6 +370,7 @@ const resources = { //Google Business Messages agentId: 'Agent ID', + addAgentId: 'Add Agent ID', googleAgentPlaceholder: 'Add the agent ID provided by your Google Partner', connectGoogle: 'Connect Google Business Messages', googleConfigurationText: @@ -423,6 +424,9 @@ const resources = { //WhatsApp Business Cloud whatsappDescription: 'World #1 chat app.', + whatsappPhoneNumberId: 'Phone Number Id', + whatsappPhoneNumberIdPlaceholder: 'Add your Phone Number Id', + whatsappPhoneNumberIdTooltip: 'Add your Phone Number Id', //Congnigy congnigyDescription: 'A low-code UI for conversational AI.', @@ -463,6 +467,7 @@ const resources = { noChannelsConnected: 'This connector does not have any connected channels yet.', optional: 'Optional', configuration: 'Configuration', + createChannel: 'Create Channel', //Request Access comingSoon: 'Coming Soon', @@ -895,6 +900,9 @@ const resources = { //WhatsApp Business Cloud whatsappDescription: 'Weltweite Chat-App Nr. 1', + whatsappPhoneNumberId: 'Telefonnummer Id', + whatsappPhoneNumberIdPlaceholder: 'Telefonnummer Id hinzufügen', + whatsappPhoneNumberIdTooltip: 'Telefonnummer Id hinzufügen', //Congnigy congnigyDescription: 'Eine Low-Code-Benutzeroberfläche für Konversations-KI.', @@ -933,6 +941,7 @@ const resources = { //Google Business Messages agentId: 'Agent ID', + addAgentId: 'Agent ID hinzufügen', googleAgentPlaceholder: 'Fügen Sie die von Ihrem Google-Partner bereitgestellte Agent-ID hinzu', connectGoogle: 'Google Business-Nachrichten verbinden', googleConfigurationText: @@ -999,6 +1008,7 @@ const resources = { noChannelsConnected: 'Mit diesem Anschluss sind noch keine Kanäle verbunden.', optional: 'Optional', configuration: 'Konfiguration', + createChannel: 'Kanal erstellen', //Request Access @@ -1431,6 +1441,7 @@ const resources = { //Google Business Messages agentId: `ID de l'agent`, + addAgentId: `Ajouter l'ID de l'agent`, googleAgentPlaceholder: `Ajoutez l'identifiant de l'agent fourni par votre partenaire Google`, connectGoogle: 'Connecter les messages Google Business', googleConfigurationText: @@ -1483,6 +1494,9 @@ const resources = { //WhatsApp Business Cloud whatsappDescription: 'Première application de chat au monde', + whatsappPhoneNumberId: 'Numéro de téléphone', + whatsappPhoneNumberIdPlaceholder: 'Ajoutez votre numéro de téléphone', + whatsappPhoneNumberIdTooltip: 'Ajoutez votre numéro de téléphone', //Congnigy congnigyDescription: "Une interface utilisateur low-code pour l'IA conversationnelle.", @@ -1523,6 +1537,7 @@ const resources = { noChannelsConnected: "Ce connecteur n'a pas encore de canaux connectés.", optional: 'Optionnel', configuration: 'Configuration', + createChannel: 'Créer un canal', //Request Access comingSoon: 'Prochainement', @@ -1965,6 +1980,7 @@ const resources = { //Google Business Messages agentId: 'Identificación del agente', + addAgentId: 'Añadir ID de agente', googleAgentPlaceholder: 'Añade el ID de agente proporcionado por tu Google Partner', connectGoogle: 'Conectar los mensajes de Google Business', googleConfigurationText: @@ -2018,6 +2034,9 @@ const resources = { //WhatsApp Business Cloud whatsappDescription: 'La aplicación de mensajería número 1 del mundo', + whatsappPhoneNumberId: 'Número de teléfono Id', + whatsappPhoneNumberIdPlaceholder: 'Añada su número de teléfono', + whatsappPhoneNumberIdTooltip: 'Añada su número de teléfono', //Congnigy congnigyDescription: 'Una interfaz de usuario de código bajo para IA conversacional.', @@ -2058,6 +2077,7 @@ const resources = { noChannelsConnected: 'Este conector aún no tiene ningún canal conectado.', optional: 'Opcional', configuration: 'Configuración', + createChannel: 'Crear canal', //Request Access comingSoon: 'Próximamente', From ad4ac95261aa363d0e0e80e30cfd914f4a53fb8a Mon Sep 17 00:00:00 2001 From: Christoph Proeschel Date: Thu, 6 Oct 2022 14:57:02 +0200 Subject: [PATCH 72/74] [#3701] Contact messages endpoint (#3789) * [#3701] Contact messages endpoint * refactor iterator use * Refactor to use auto-closeable and finish messages endpoint implementation * implement pagination * remove unneeded topics * fix BUILD file --- .../main/java/co/airy/core/admin/Stores.java | 34 ++--- .../co/airy/core/admin/TagsController.java | 9 +- .../co/airy/core/communication/Stores.java | 8 +- backend/components/contacts/BUILD | 3 +- .../core/contacts/ContactsController.java | 9 +- .../contacts/ConversationsController.java | 81 ++++++++++++ .../java/co/airy/core/contacts/Stores.java | 48 ++++++- .../co/airy/core/contacts/dto/Messages.java | 26 ++++ .../payload/ContactResponsePayload.java | 2 +- ...ontactWithMergeHistoryResponsePayload.java | 2 +- .../payload/CreateContactPayload.java | 2 +- .../payload/RecentMessagesRequestPayload.java | 24 ++++ .../RecentMessagesResponsePayload.java | 18 +++ .../payload/UpdateContactPayload.java | 2 +- .../airy/core/contacts/DeleteContactTest.java | 4 +- .../airy/core/contacts/ListContactsTest.java | 2 +- .../core/contacts/RecentMessagesTest.java | 119 ++++++++++++++++++ .../core/contacts/UpdateContactsTest.java | 2 +- .../airy/core/contacts/util/TestContact.java | 2 +- .../core/contacts/util/TestConversation.java | 67 ++++++++++ .../sources/facebook/ChannelsController.java | 9 +- .../rasa_connector/models/MessageSend.java | 1 - .../java/co/airy/core/sources/api/Stores.java | 9 +- .../airy/core/webhook/publisher/Stores.java | 5 +- backend/model/{contacts => contact}/BUILD | 4 +- .../java/co/airy/model/contact}/Contact.java | 28 ++--- .../model/contact}/ConversationContact.java | 2 +- .../model/contact}/MetadataRepository.java | 2 +- docs/docs/api/endpoints/contacts.md | 56 +++++++-- docs/docs/api/webhook.md | 4 +- 30 files changed, 504 insertions(+), 80 deletions(-) create mode 100644 backend/components/contacts/src/main/java/co/airy/core/contacts/ConversationsController.java create mode 100644 backend/components/contacts/src/main/java/co/airy/core/contacts/dto/Messages.java create mode 100644 backend/components/contacts/src/main/java/co/airy/core/contacts/payload/RecentMessagesRequestPayload.java create mode 100644 backend/components/contacts/src/main/java/co/airy/core/contacts/payload/RecentMessagesResponsePayload.java create mode 100644 backend/components/contacts/src/test/java/co/airy/core/contacts/RecentMessagesTest.java create mode 100644 backend/components/contacts/src/test/java/co/airy/core/contacts/util/TestConversation.java rename backend/model/{contacts => contact}/BUILD (81%) rename backend/model/{contacts/src/main/java/co/airy/model/contacts => contact/src/main/java/co/airy/model/contact}/Contact.java (93%) rename backend/model/{contacts/src/main/java/co/airy/model/contacts => contact/src/main/java/co/airy/model/contact}/ConversationContact.java (92%) rename backend/model/{contacts/src/main/java/co/airy/model/contacts => contact/src/main/java/co/airy/model/contact}/MetadataRepository.java (94%) diff --git a/backend/components/admin/src/main/java/co/airy/core/admin/Stores.java b/backend/components/admin/src/main/java/co/airy/core/admin/Stores.java index c1b02809b6..2525dee6b4 100644 --- a/backend/components/admin/src/main/java/co/airy/core/admin/Stores.java +++ b/backend/components/admin/src/main/java/co/airy/core/admin/Stores.java @@ -189,19 +189,21 @@ public Template getTemplate(String templateId) { public List getChannels() { final ReadOnlyKeyValueStore store = getConnectedChannelsStore(); - final KeyValueIterator iterator = store.all(); - - List channels = new ArrayList<>(); - iterator.forEachRemaining(kv -> channels.add(kv.value)); + List channels; + try (KeyValueIterator iterator = store.all()) { + channels = new ArrayList<>(); + iterator.forEachRemaining(kv -> channels.add(kv.value)); + } return channels; } public List