diff --git a/backend/Actions/Instasent/InstasentController.php b/backend/Actions/Instasent/InstasentController.php new file mode 100644 index 000000000..3317f1e03 --- /dev/null +++ b/backend/Actions/Instasent/InstasentController.php @@ -0,0 +1,130 @@ +auth_token)) { + wp_send_json_error( + __( + 'Requested parameter is empty', + 'bit-integrations' + ), + 400 + ); + } + + $endpoint = self::$_baseUrl . '/organization/account'; + $header = [ + 'Authorization' => 'Bearer ' . $refreshFieldsRequestParams->auth_token, + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ]; + + $response = HttpHelper::get($endpoint, null, $header); + + if (is_wp_error($response)) { + wp_send_json_error($response->get_error_message(), 400); + } + + if (HttpHelper::$responseCode == 200) { + wp_send_json_success('Authorization Successful', 200); + + return; + } + + wp_send_json_error( + $response->message ?? $response ?? 'Authorization Failed', + 400 + ); + } + + public function refreshDatasources($refreshFieldsRequestParams) + { + if (empty($refreshFieldsRequestParams) || empty($refreshFieldsRequestParams->auth_token) || empty($refreshFieldsRequestParams->projectId)) { + wp_send_json_error( + __('Requested parameter is empty', 'bit-integrations'), + 400 + ); + } + + $project = rawurlencode($refreshFieldsRequestParams->projectId); + $endpoint = self::$_baseUrl . "/v1/project/{$project}/datasource"; + $header = [ + 'Authorization' => 'Bearer ' . $refreshFieldsRequestParams->auth_token, + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ]; + + $response = HttpHelper::get($endpoint, null, $header); + + if (is_wp_error($response)) { + wp_send_json_error($response->get_error_message(), 400); + } + + if (HttpHelper::$responseCode == 200) { + $entities = $response->entities ?? []; + $formattedResponse = []; + foreach ($entities as $entity) { + $formattedResponse[] = [ + 'id' => $entity->id ?? '', + 'name' => $entity->name ?? ($entity->id ?? ''), + ]; + } + + wp_send_json_success($formattedResponse, 200); + + return; + } + + wp_send_json_error( + $response->message ?? $response ?? __('Failed to fetch data sources', 'bit-integrations'), + 400 + ); + } + + public function execute($integrationData, $fieldValues) + { + $integrationDetails = $integrationData->flow_details; + $integId = $integrationData->id; + $auth_token = $integrationDetails->auth_token ?? ''; + $fieldMap = $integrationDetails->field_map ?? ''; + $action = $integrationDetails->action ?? ''; + $actions = isset($integrationDetails->actions) ? $integrationDetails->actions : (object) []; + + if ( + empty($fieldMap) + || empty($auth_token) + || empty($action) + ) { + // translators: %s: Placeholder value + return new WP_Error('REQ_FIELD_EMPTY', wp_sprintf(__('module, fields are required for %s api', 'bit-integrations'), 'Instasent')); + } + + $recordApiHelper = new RecordApiHelper($auth_token, $integrationDetails, $integId); + $instasentApiResponse = $recordApiHelper->execute( + $integrationDetails, + $fieldValues, + $fieldMap, + $auth_token, + $action, + $actions + ); + + return $instasentApiResponse; + } +} diff --git a/backend/Actions/Instasent/RecordApiHelper.php b/backend/Actions/Instasent/RecordApiHelper.php new file mode 100644 index 000000000..bdfdf3f08 --- /dev/null +++ b/backend/Actions/Instasent/RecordApiHelper.php @@ -0,0 +1,142 @@ +_authToken = $auth_token; + $this->_integrationDetails = $integrationDetails; + $this->_integrationID = $integId; + } + + public function generateReqDataFromFieldMap($data, $fieldMap) + { + $dataFinal = []; + + foreach ($fieldMap as $key => $value) { + $triggerValue = $value->formField; + $actionValue = $value->instasentFormField; + + if (empty($actionValue)) { + continue; + } + + if ($triggerValue === 'custom' && isset($value->customValue)) { + $dataFinal[$actionValue] = Common::replaceFieldWithValue($value->customValue, $data); + } elseif (isset($data[$triggerValue])) { + $dataFinal[$actionValue] = $data[$triggerValue]; + } + } + + return $dataFinal; + } + + public function execute( + $integrationDetails, + $fieldValues, + $fieldMap, + $auth_token, + $action, + $utilities = null + ) { + $fieldData = $this->generateReqDataFromFieldMap($fieldValues, $fieldMap); + + if ($action === 'send_sms' && isset($utilities->allowUnicode)) { + $fieldData['allowUnicode'] = $utilities->allowUnicode; + } + + $default = [ + 'success' => false, + 'message' => __('Bit Integrations Pro is required.', 'bit-integrations'), + ]; + + switch ($action) { + case 'send_sms': + $apiResponse = Hooks::apply(Config::withPrefix('instasent_send_sms'), $default, $fieldData, $auth_token); + $typeName = 'send-sms'; + + break; + + case 'create_lookup': + $apiResponse = Hooks::apply(Config::withPrefix('instasent_create_lookup'), $default, $fieldData, $auth_token); + $typeName = 'create-lookup'; + + break; + + case 'create_datasource': + $apiResponse = Hooks::apply(Config::withPrefix('instasent_create_datasource'), $default, $fieldData, $integrationDetails, $auth_token); + $typeName = 'create-datasource'; + + break; + + case 'create_or_update_contact': + $apiResponse = Hooks::apply(Config::withPrefix('instasent_create_or_update_contact'), $default, $fieldData, $integrationDetails, $auth_token); + $typeName = 'create-or-update-contact'; + + break; + + case 'delete_contact': + $apiResponse = Hooks::apply(Config::withPrefix('instasent_delete_contact'), $default, $fieldData, $integrationDetails, $auth_token); + $typeName = 'delete-contact'; + + break; + + case 'create_contact_event': + $apiResponse = Hooks::apply(Config::withPrefix('instasent_create_contact_event'), $default, $fieldData, $integrationDetails, $auth_token); + $typeName = 'create-contact-event'; + + break; + + default: + $apiResponse = $default; + $typeName = $action; + + break; + } + + if (is_wp_error($apiResponse)) { + LogHandler::save( + $this->_integrationID, + wp_json_encode(['type' => 'action', 'type_name' => $typeName]), + 'error', + wp_json_encode(['message' => $apiResponse->get_error_message()]) + ); + + return $apiResponse; + } + + $apiResponse = \is_array($apiResponse) ? (object) $apiResponse : $apiResponse; + + if ( + (isset($apiResponse->success) && $apiResponse->success) + || isset($apiResponse->id) + || isset($apiResponse->data) + ) { + LogHandler::save($this->_integrationID, wp_json_encode(['type' => 'action', 'type_name' => $typeName]), 'success', wp_json_encode($apiResponse)); + } else { + LogHandler::save($this->_integrationID, wp_json_encode(['type' => 'action', 'type_name' => $typeName]), 'error', wp_json_encode($apiResponse)); + } + + return $apiResponse; + } +} diff --git a/backend/Actions/Instasent/Routes.php b/backend/Actions/Instasent/Routes.php new file mode 100644 index 000000000..a1378dab5 --- /dev/null +++ b/backend/Actions/Instasent/Routes.php @@ -0,0 +1,11 @@ + import('./Acumbamail/EditAcumbamail')) const EditGroundhogg = lazy(() => import('./Groundhogg/EditGroundhogg')) const EditSendFox = lazy(() => import('./SendFox/EditSendFox')) const EditMailerLite = lazy(() => import('./MailerLite/EditMailerLite')) +const EditInstasent = lazy(() => import('./Instasent/EditInstasent')) const EditVbout = lazy(() => import('./Vbout/EditVbout')) const EditWhatsApp = lazy(() => import('./WhatsApp/EditWhatsApp')) const EditLearnDash = lazy(() => import('./LearnDash/EditLearnDash')) @@ -397,6 +398,8 @@ const IntegType = memo(({ allIntegURL, flow }) => { return case 'MailerLite': return + case 'Instasent': + return case 'Vbout': return case 'WhatsApp': diff --git a/frontend/src/components/AllIntegrations/Instasent/EditInstasent.jsx b/frontend/src/components/AllIntegrations/Instasent/EditInstasent.jsx new file mode 100644 index 000000000..f5b2cedbc --- /dev/null +++ b/frontend/src/components/AllIntegrations/Instasent/EditInstasent.jsx @@ -0,0 +1,113 @@ +/* eslint-disable no-param-reassign */ + +import { create } from 'mutative' +import { useEffect, useState } from 'react' +import { useNavigate, useParams } from 'react-router' +import { useRecoilState, useRecoilValue } from 'recoil' +import { $actionConf, $formFields, $newFlow } from '../../../GlobalStates' +import { __ } from '../../../Utils/i18nwrap' +import SnackMsg from '../../Utilities/SnackMsg' +import { saveActionConf } from '../IntegrationHelpers/IntegrationHelpers' +import IntegrationStepThree from '../IntegrationHelpers/IntegrationStepThree' +import SetEditIntegComponents from '../IntegrationHelpers/SetEditIntegComponents' +import { checkMappedFields, handleInput, InstasentStaticData } from './InstasentCommonFunc' +import InstasentIntegLayout from './InstasentIntegLayout' + +function EditInstasent({ allIntegURL }) { + const navigate = useNavigate() + const { id } = useParams() + const [flow, setFlow] = useRecoilState($newFlow) + const [instasentConf, setInstasentConf] = useRecoilState($actionConf) + const [isLoading, setIsLoading] = useState(false) + const [name, setName] = useState(instasentConf?.name || '') + const [loading, setLoading] = useState({ + list: false, + field: false, + auth: false + }) + const [snack, setSnackbar] = useState({ show: false }) + const formField = useRecoilValue($formFields) + + const saveConfig = () => { + if (!checkMappedFields(instasentConf)) { + setSnackbar({ show: true, msg: __('Please map mandatory fields', 'bit-integrations') }) + return + } + + saveActionConf({ + flow, + allIntegURL, + conf: instasentConf, + navigate, + edit: 1, + setLoading, + setSnackbar + }) + } + + useEffect(() => { + setInstasentConf(prev => + create(prev, draftConf => { + const action = draftConf.action || 'send_sms' + draftConf.action = action + // instasentFields isn't persisted; rebuild it from the saved action so the + // field map can render its options on edit (without touching the saved field_map). + draftConf.instasentFields = InstasentStaticData[action] || [] + }) + ) + }, []) + + const handleEditIntegName = e => { + setName(e.target.value) + + setInstasentConf(prevConf => + create(prevConf, draftConF => { + draftConF.name = e.target.value + }) + ) + } + + return ( +
+ + +
+ {__('Integration Name:', 'bit-integrations')} + +
+
+ + + handleInput(e, instasentConf, setInstasentConf, loading, setLoading)} + instasentConf={instasentConf} + setInstasentConf={setInstasentConf} + loading={loading} + setLoading={setLoading} + setSnackbar={setSnackbar} + /> + + +
+
+ ) +} + +export default EditInstasent diff --git a/frontend/src/components/AllIntegrations/Instasent/Instasent.jsx b/frontend/src/components/AllIntegrations/Instasent/Instasent.jsx new file mode 100644 index 000000000..2e0be1b11 --- /dev/null +++ b/frontend/src/components/AllIntegrations/Instasent/Instasent.jsx @@ -0,0 +1,133 @@ +/* eslint-disable no-unused-expressions */ +import { useState } from 'react' +import 'react-multiple-select-dropdown-lite/dist/index.css' +import toast from 'react-hot-toast' +import { useNavigate } from 'react-router' +import { __ } from '../../../Utils/i18nwrap' +import SnackMsg from '../../Utilities/SnackMsg' +import Steps from '../../Utilities/Steps' +import { saveIntegConfig } from '../IntegrationHelpers/IntegrationHelpers' +import IntegrationStepThree from '../IntegrationHelpers/IntegrationStepThree' +import InstasentAuthorization from './InstasentAuthorization' +import { checkMappedFields, handleInput } from './InstasentCommonFunc' +import InstasentIntegLayout from './InstasentIntegLayout' + +function Instasent({ formFields, setFlow, flow, allIntegURL }) { + const navigate = useNavigate() + const [isLoading, setIsLoading] = useState(false) + const [loading, setLoading] = useState({ + list: false, + field: false, + auth: false, + datasource: false + }) + + const [step, setstep] = useState(1) + const [snack, setSnackbar] = useState({ show: false }) + + const [instasentConf, setInstasentConf] = useState({ + name: 'Instasent', + type: 'Instasent', + auth_token: '', + action: '', + projectId: '', + datasourceId: '', + instasentFields: [], + field_map: [{ formField: '', instasentFormField: '' }], + actions: {}, + default: { datasources: [] } + }) + + const saveConfig = () => { + setIsLoading(true) + const resp = saveIntegConfig( + flow, + setFlow, + allIntegURL, + instasentConf, + navigate, + '', + '', + setIsLoading + ) + resp.then(res => { + if (res.success) { + toast.success(res.data?.msg) + navigate(allIntegURL) + } else { + toast.error(res.data || res) + } + }) + } + + const nextPage = pageNo => { + setTimeout(() => { + document.getElementById('btcd-settings-wrp').scrollTop = 0 + }, 300) + + if (!checkMappedFields(instasentConf)) { + toast.error(__('Please map mandatory fields', 'bit-integrations')) + return + } + instasentConf.field_map.length > 0 && setstep(pageNo) + } + + return ( +
+ +
+ +
+ + {/* STEP 1 */} + + + + {/* STEP 2 */} +
+ handleInput(e, instasentConf, setInstasentConf, loading, setLoading)} + instasentConf={instasentConf} + setInstasentConf={setInstasentConf} + loading={loading} + setLoading={setLoading} + setSnackbar={setSnackbar} + /> + + {instasentConf?.action && ( + + )} +
+ + {/* STEP 3 */} + saveConfig()} + isLoading={isLoading} + dataConf={instasentConf} + setDataConf={setInstasentConf} + formFields={formFields} + /> +
+ ) +} + +export default Instasent diff --git a/frontend/src/components/AllIntegrations/Instasent/InstasentActions.jsx b/frontend/src/components/AllIntegrations/Instasent/InstasentActions.jsx new file mode 100644 index 000000000..6cfe19c73 --- /dev/null +++ b/frontend/src/components/AllIntegrations/Instasent/InstasentActions.jsx @@ -0,0 +1,31 @@ +import { __ } from '../../../Utils/i18nwrap' +import TableCheckBox from '../../Utilities/TableCheckBox' + +export default function InstasentActions({ instasentConf, setInstasentConf }) { + const actionHandler = (e, type) => { + const newConf = { ...instasentConf, actions: { ...instasentConf.actions } } + if (e.target.checked) { + newConf.actions[type] = true + } else { + delete newConf.actions[type] + } + + setInstasentConf({ ...newConf }) + } + + return ( +
+ actionHandler(e, 'allowUnicode')} + className="wdt-200 mt-4 mr-2" + value="allow_unicode" + title={__('Allow Unicode', 'bit-integrations')} + subTitle={__( + 'Enable Unicode support for SMS messages (e.g. emojis, special characters)', + 'bit-integrations' + )} + /> +
+ ) +} diff --git a/frontend/src/components/AllIntegrations/Instasent/InstasentAuthorization.jsx b/frontend/src/components/AllIntegrations/Instasent/InstasentAuthorization.jsx new file mode 100644 index 000000000..3e1b99749 --- /dev/null +++ b/frontend/src/components/AllIntegrations/Instasent/InstasentAuthorization.jsx @@ -0,0 +1,118 @@ +import { useState } from 'react' +import { __ } from '../../../Utils/i18nwrap' +import LoaderSm from '../../Loaders/LoaderSm' +import Note from '../../Utilities/Note' +import TutorialLink from '../../Utilities/TutorialLink' +import { authorization } from './InstasentCommonFunc' + +export default function InstasentAuthorization({ + instasentConf, + setInstasentConf, + step, + setstep, + loading, + setLoading, + setSnackbar, + isInfo +}) { + const [isAuthorized, setIsAuthorized] = useState(false) + const [error, setError] = useState({ name: '', auth_token: '' }) + const url = 'https://app.instasent.com/' + + const nextPage = () => { + setTimeout(() => { + document.getElementById('btcd-settings-wrp').scrollTop = 0 + }, 300) + + setstep(2) + } + + const handleInput = e => { + const newConf = { ...instasentConf } + const rmError = { ...error } + rmError[e.target.name] = '' + newConf[e.target.name] = e.target.value + setError(rmError) + setInstasentConf(newConf) + } + + const note = ` +

${__('Step of generate API token:', 'bit-integrations')}

+
    +
  • ${__('Goto', 'bit-integrations')} Instasent Dashboard
  • +
  • ${__( + 'Copy the API Token and paste into API Token field of your authorization form.', + 'bit-integrations' + )}
  • +
  • ${__('Finally, click Authorize button.', 'bit-integrations')}
  • +
+ ` + + return ( +
+ + +
+ {__('Integration Name:', 'bit-integrations')} +
+ + +
+ {__('API Token:', 'bit-integrations')} +
+ +
{error.auth_token}
+ + + {__('To Get API Token, Please Visit', 'bit-integrations')} +   + + {__('Instasent Dashboard', 'bit-integrations')} + + +
+
+ + {!isInfo && ( +
+ +
+ +
+ )} + +
+ ) +} diff --git a/frontend/src/components/AllIntegrations/Instasent/InstasentCommonFunc.js b/frontend/src/components/AllIntegrations/Instasent/InstasentCommonFunc.js new file mode 100644 index 000000000..90b7e16a8 --- /dev/null +++ b/frontend/src/components/AllIntegrations/Instasent/InstasentCommonFunc.js @@ -0,0 +1,198 @@ +/* eslint-disable no-else-return */ +import { create } from 'mutative' +import toast from 'react-hot-toast' +import bitsFetch from '../../../Utils/bitsFetch' +import { __ } from '../../../Utils/i18nwrap' + +// Actions that need a Project / Data Source. These ids are selected at config time +// (Project Id as text, Data Source as a fetched dropdown) instead of in the field map. +export const ACTIONS_WITH_PROJECT = [ + 'create_datasource', + 'create_or_update_contact', + 'delete_contact', + 'create_contact_event' +] + +export const ACTIONS_WITH_DATASOURCE = [ + 'create_or_update_contact', + 'delete_contact', + 'create_contact_event' +] + +// Actions whose field map may carry arbitrary keys (contact custom attributes / +// event parameters), entered via the "Custom Field..." option in the field map. +export const ACTIONS_WITH_CUSTOM_FIELDS = ['create_or_update_contact', 'create_contact_event'] + +// Sentinel chosen in the Instasent-field select to switch a row to a free-text key. +export const CUSTOM_FIELD_KEY = '__custom_field__' + +export const InstasentStaticData = { + send_sms: [ + { key: 'from', label: 'From', required: true }, + { key: 'to', label: 'To', required: true }, + { key: 'text', label: 'Text', required: true }, + { key: 'clientId', label: 'Client Id', required: false } + ], + create_lookup: [{ key: 'to', label: 'To', required: true }], + create_datasource: [ + { key: 'name', label: 'Name', required: true }, + { key: 'description', label: 'Description', required: false }, + { key: 'defaultCountry', label: 'Default Country', required: false }, + { key: 'locale', label: 'Locale', required: false }, + { key: 'timezone', label: 'Timezone', required: false } + ], + create_or_update_contact: [ + { key: 'userId', label: 'User Id', required: true }, + { key: 'firstName', label: 'First Name', required: false }, + { key: 'lastName', label: 'Last Name', required: false }, + { key: 'email', label: 'Email', required: false }, + { key: 'phoneMobile', label: 'Phone Mobile', required: false } + ], + delete_contact: [{ key: 'userId', label: 'User Id', required: true }], + create_contact_event: [ + { key: 'userId', label: 'User Id', required: true }, + { key: 'eventId', label: 'Event Id', required: true }, + { key: 'eventType', label: 'Event Type', required: true } + ] +} + +export const handleInput = (e, instasentConf, setInstasentConf, loading, setLoading) => { + const { name, value } = e.target + + const updatedConf = create(instasentConf, draftConf => { + draftConf[name] = value + + // Changing the project invalidates the fetched data sources + the selection. + if (name === 'projectId') { + draftConf.datasourceId = '' + if (draftConf.default) { + draftConf.default.datasources = [] + } + } + }) + + setInstasentConf(updatedConf) + + if (name === 'action' && value !== '') { + instasentRefreshFields(updatedConf, setInstasentConf, loading, setLoading) + } +} + +export const generateMappedField = instasentConf => { + const requiredFlds = instasentConf?.instasentFields.filter(fld => fld.required === true) + return requiredFlds.length > 0 + ? requiredFlds.map(field => ({ + formField: '', + instasentFormField: field.key + })) + : [{ formField: '', instasentFormField: '' }] +} + +export const checkMappedFields = instasentConf => { + if (ACTIONS_WITH_PROJECT.includes(instasentConf?.action) && !instasentConf?.projectId) { + return false + } + + if (ACTIONS_WITH_DATASOURCE.includes(instasentConf?.action) && !instasentConf?.datasourceId) { + return false + } + + const mappedFields = instasentConf?.field_map + ? instasentConf.field_map.filter( + mappedField => + !mappedField.formField || + !mappedField.instasentFormField || + mappedField.instasentFormField === CUSTOM_FIELD_KEY || + (mappedField.formField === 'custom' && !mappedField.customValue) + ) + : [] + if (mappedFields.length > 0) { + return false + } + return true +} + +export const authorization = (confTmp, setIsAuthorized, loading, setLoading) => { + if (!confTmp.auth_token) { + toast.error(__("API Token can't be empty", 'bit-integrations')) + + return + } + + setLoading({ ...loading, auth: true }) + + const requestParams = { + auth_token: confTmp.auth_token + } + + bitsFetch(requestParams, 'instasent_authorize').then(result => { + setLoading({ ...loading, auth: false }) + + if (result && result.success) { + setIsAuthorized(true) + + toast.success(__('Authorized Successfully', 'bit-integrations')) + + return + } + + toast.error(__('Authorized failed', 'bit-integrations')) + }) +} + +export const instasentRefreshFields = (confTmp, setConf, loading, setLoading) => { + setLoading({ ...loading, field: true }) + + const staticFields = InstasentStaticData[confTmp?.action] || [] + + setConf(prev => + create(prev, draftConf => { + draftConf.instasentFields = staticFields + draftConf.field_map = generateMappedField(draftConf) + }) + ) + + setLoading({ ...loading, field: false }) + + toast.success(__('Fields refresh successfully', 'bit-integrations')) +} + +export const refreshDatasources = (confTmp, setConf, loading, setLoading) => { + if (!confTmp.auth_token) { + toast.error(__("API Token can't be empty", 'bit-integrations')) + + return + } + + if (!confTmp.projectId) { + toast.error(__('Please enter a Project Id first', 'bit-integrations')) + + return + } + + setLoading({ ...loading, datasource: true }) + + const requestParams = { + auth_token: confTmp.auth_token, + projectId: confTmp.projectId + } + + bitsFetch(requestParams, 'refresh_instasent_datasources').then(result => { + setLoading({ ...loading, datasource: false }) + + if (result && result.success) { + setConf(prev => + create(prev, draftConf => { + draftConf.default = draftConf.default || {} + draftConf.default.datasources = result.data || [] + }) + ) + + toast.success(__('Data sources fetched successfully', 'bit-integrations')) + + return + } + + toast.error(__('Data sources fetch failed', 'bit-integrations')) + }) +} diff --git a/frontend/src/components/AllIntegrations/Instasent/InstasentFieldMap.jsx b/frontend/src/components/AllIntegrations/Instasent/InstasentFieldMap.jsx new file mode 100644 index 000000000..958061df9 --- /dev/null +++ b/frontend/src/components/AllIntegrations/Instasent/InstasentFieldMap.jsx @@ -0,0 +1,128 @@ +import { useRecoilValue } from 'recoil' +import { $appConfigState } from '../../../GlobalStates' +import { SmartTagField } from '../../../Utils/StaticData/SmartTagField' +import { __, sprintf } from '../../../Utils/i18nwrap' +import { handleCustomValue } from '../IntegrationHelpers/IntegrationHelpers' +import TagifyInput from '../../Utilities/TagifyInput' +import { addFieldMap, delFieldMap, handleFieldMapping } from './IntegrationHelpers' +import { ACTIONS_WITH_CUSTOM_FIELDS, CUSTOM_FIELD_KEY } from './InstasentCommonFunc' + +export default function InstasentFieldMap({ i, formFields, field, instasentConf, setInstasentConf }) { + const btcbi = useRecoilValue($appConfigState) + const { isPro } = btcbi + + const requiredFlds = instasentConf?.instasentFields?.filter(fld => fld.required === true) || [] + const nonRequiredFlds = instasentConf?.instasentFields?.filter(fld => fld.required === false) || [] + + const isExtraRow = i >= requiredFlds.length + const customCapable = ACTIONS_WITH_CUSTOM_FIELDS.includes(instasentConf?.action) + const predefinedKeys = (instasentConf?.instasentFields || []).map(fld => fld.key) + const isCustomKey = + isExtraRow && + customCapable && + !!field.instasentFormField && + field.instasentFormField !== CUSTOM_FIELD_KEY && + !predefinedKeys.includes(field.instasentFormField) + const showCustomKeyInput = field.instasentFormField === CUSTOM_FIELD_KEY || isCustomKey + + return ( +
+
+
+ + + {field.formField === 'custom' && ( + handleCustomValue(e, i, instasentConf, setInstasentConf)} + label={__('Custom Value', 'bit-integrations')} + className="mr-2" + type="text" + value={field.customValue} + placeholder={__('Custom Value', 'bit-integrations')} + formFields={formFields} + /> + )} + + {showCustomKeyInput ? ( + handleFieldMapping(ev, i, instasentConf, setInstasentConf)} + /> + ) : ( + + )} +
+ {i >= requiredFlds.length && ( + <> + + + + )} +
+
+ ) +} diff --git a/frontend/src/components/AllIntegrations/Instasent/InstasentIntegLayout.jsx b/frontend/src/components/AllIntegrations/Instasent/InstasentIntegLayout.jsx new file mode 100644 index 000000000..7330a7486 --- /dev/null +++ b/frontend/src/components/AllIntegrations/Instasent/InstasentIntegLayout.jsx @@ -0,0 +1,227 @@ +import { create } from 'mutative' +import MultiSelect from 'react-multiple-select-dropdown-lite' +import { useRecoilValue } from 'recoil' +import { $appConfigState } from '../../../GlobalStates' +import { __ } from '../../../Utils/i18nwrap' +import Loader from '../../Loaders/Loader' +import { checkIsPro, getProLabel } from '../../Utilities/ProUtilHelpers' +import { addFieldMap } from './IntegrationHelpers' +import { + ACTIONS_WITH_DATASOURCE, + ACTIONS_WITH_PROJECT, + instasentRefreshFields, + refreshDatasources +} from './InstasentCommonFunc' +import InstasentActions from './InstasentActions' +import InstasentFieldMap from './InstasentFieldMap' + +const actionOptions = [ + { + value: 'send_sms', + label: __('Send SMS', 'bit-integrations'), + isPro: true + }, + { + value: 'create_lookup', + label: __('Create Lookup', 'bit-integrations'), + isPro: true + }, + { + value: 'create_datasource', + label: __('Create Datasource', 'bit-integrations'), + isPro: true + }, + { + value: 'create_or_update_contact', + label: __('Create or Update Contact', 'bit-integrations'), + isPro: true + }, + { + value: 'delete_contact', + label: __('Delete Contact', 'bit-integrations'), + isPro: true + }, + { + value: 'create_contact_event', + label: __('Create Contact Event', 'bit-integrations'), + isPro: true + } +] + +export default function InstasentIntegLayout({ + formFields, + handleInput, + instasentConf, + setInstasentConf, + loading, + setLoading, + setSnackbar +}) { + const btcbi = useRecoilValue($appConfigState) + const { isPro } = btcbi + + const handleMainAction = value => { + const updatedConf = create(instasentConf, draftConf => { + draftConf.action = value + }) + + setInstasentConf(updatedConf) + + if (value !== '') { + instasentRefreshFields(updatedConf, setInstasentConf, loading, setLoading) + } + } + + const setDatasource = value => { + setInstasentConf(prev => + create(prev, draftConf => { + draftConf.datasourceId = value + }) + ) + } + + return ( + <> +
+
+ {__('Action:', 'bit-integrations')} + handleMainAction(value)} + options={actionOptions.map(action => ({ + label: checkIsPro(isPro, action.isPro) ? action.label : getProLabel(action.label), + value: action.value, + disabled: !checkIsPro(isPro, action.isPro) + }))} + singleSelect + closeOnSelect + /> +
+
+ + {ACTIONS_WITH_PROJECT.includes(instasentConf?.action) && ( +
+ {__('Project Id:', 'bit-integrations')} + +
+ )} + + {ACTIONS_WITH_DATASOURCE.includes(instasentConf?.action) && ( + <> +
+
+ {__('Data Source:', 'bit-integrations')} + setDatasource(val)} + options={(instasentConf?.default?.datasources || []).map(ds => ({ + label: ds?.name || ds?.id || '', + value: String(ds?.id || '') + }))} + singleSelect + closeOnSelect + /> + +
+ + )} + + {loading.field && ( + + )} + + {instasentConf?.action && !loading?.field && ( + <> +
+ + {__('Field Map', 'bit-integrations')} + + +
+
+
+
+
+ {__('Form Fields', 'bit-integrations')} +
+
+ {__('Instasent Fields', 'bit-integrations')} +
+
+ + {instasentConf?.field_map.map((itm, i) => ( + + ))} +
+ +
+
+
+ + {instasentConf?.action === 'send_sms' && ( + <> +
+ {__('Utilities', 'bit-integrations')} +
+
+ + + )} + + )} + + ) +} diff --git a/frontend/src/components/AllIntegrations/Instasent/IntegrationHelpers.jsx b/frontend/src/components/AllIntegrations/Instasent/IntegrationHelpers.jsx new file mode 100644 index 000000000..9a0438351 --- /dev/null +++ b/frontend/src/components/AllIntegrations/Instasent/IntegrationHelpers.jsx @@ -0,0 +1,24 @@ +export const addFieldMap = (i, confTmp, setConf) => { + const newConf = { ...confTmp } + newConf.field_map.splice(i, 0, {}) + setConf({ ...newConf }) +} + +export const delFieldMap = (i, confTmp, setConf) => { + const newConf = { ...confTmp } + if (newConf.field_map.length > 1) { + newConf.field_map.splice(i, 1) + } + + setConf({ ...newConf }) +} + +export const handleFieldMapping = (event, index, conftTmp, setConf) => { + const newConf = { ...conftTmp } + newConf.field_map[index][event.target.name] = event.target.value + + if (event.target.value === 'custom') { + newConf.field_map[index].customValue = '' + } + setConf({ ...newConf }) +} diff --git a/frontend/src/components/AllIntegrations/IntegInfo.jsx b/frontend/src/components/AllIntegrations/IntegInfo.jsx index d64be1fe2..2a7f15131 100644 --- a/frontend/src/components/AllIntegrations/IntegInfo.jsx +++ b/frontend/src/components/AllIntegrations/IntegInfo.jsx @@ -68,6 +68,7 @@ const GroundhoggAuthorization = lazy(() => import('./Groundhogg/GroundhoggAuthor const SendFoxAuthorization = lazy(() => import('./SendFox/SendFoxAuthorization')) const TwilioAuthorization = lazy(() => import('./Twilio/TwilioAuthorization')) const MailerLiteAuthorization = lazy(() => import('./MailerLite/MailerLiteAuthorization')) +const InstasentAuthorization = lazy(() => import('./Instasent/InstasentAuthorization')) const VboutAuthorization = lazy(() => import('./Vbout/VboutAuthorization')) const FreshdeskAuthorization = lazy(() => import('./Freshdesk/FreshdeskAuthorization')) const GoogleContactsAuthorization = lazy(() => import('./GoogleContacts/GoogleContactsAuthorization')) @@ -429,6 +430,8 @@ export default function IntegInfo() { return case 'MailerLite': return + case 'Instasent': + return case 'Vbout': return case 'Freshdesk': diff --git a/frontend/src/components/AllIntegrations/NewInteg.jsx b/frontend/src/components/AllIntegrations/NewInteg.jsx index 842ed6ac0..b6d931852 100644 --- a/frontend/src/components/AllIntegrations/NewInteg.jsx +++ b/frontend/src/components/AllIntegrations/NewInteg.jsx @@ -70,6 +70,7 @@ const Acumbamail = lazy(() => import('./Acumbamail/Acumbamail')) const Groundhogg = lazy(() => import('./Groundhogg/Groundhogg')) const SendFox = lazy(() => import('./SendFox/SendFox')) const MailerLite = lazy(() => import('./MailerLite/MailerLite')) +const Instasent = lazy(() => import('./Instasent/Instasent')) const Vbout = lazy(() => import('./Vbout/Vbout')) const WhatsApp = lazy(() => import('./WhatsApp/WhatsApp')) const LearnDesh = lazy(() => import('./LearnDash/LearnDash')) @@ -736,6 +737,15 @@ export default function NewInteg({ allIntegURL }) { setFlow={setFlow} /> ) + case 'Instasent': + return ( + + ) case 'Vbout': return ( !inte.disable && (isPro || !inte.is_pro) && setAction(inte.type)} role="button" tabIndex="0" - className={`btcd-inte-card inte-sm mr-4 mt-3 ${ - inte.disable && (isPro || !inte.is_pro) && 'btcd-inte-dis' - } ${inte.is_pro && !isPro && 'btcd-inte-pro'}`}> + className={`btcd-inte-card inte-sm mr-4 mt-3 ${inte.disable && (isPro || !inte.is_pro) && 'btcd-inte-dis' + } ${inte.is_pro && !isPro && 'btcd-inte-pro'}`}> {inte.is_pro && !isPro && (