From c7d990070733260c52a35fdc50e29845ba1bc3bd Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Tue, 4 Aug 2026 17:19:20 +0600 Subject: [PATCH 1/3] feat(nextcrm): add NextCRM integration Add the free side of the NextCRM integration: 16 write actions that fire bit_integrations_next_crm_* filters for Bit Integrations Pro, six dropdown endpoints (tags, lists, campaigns, contact fields, contact types and contact statuses) and the React wizard. Actions identify their target contact by email rather than by NextCRM's internal id, since an email is what trigger data actually carries. Read-only operations are not extracted as actions; the data they returned is exposed through the dropdowns instead. Register the trigger in customFormIntegrations so editing a NextCRM flow loads the custom-form-submission editor rather than falling through to the form-type one. --- backend/Actions/NextCrm/NextCrmController.php | 113 ++++++++ backend/Actions/NextCrm/RecordApiHelper.php | 189 ++++++++++++++ backend/Actions/NextCrm/Routes.php | 15 ++ backend/Core/Util/AllTriggersName.php | 1 + .../Utils/StaticData/webhookIntegrations.js | 3 +- .../components/AllIntegrations/EditInteg.jsx | 3 + .../components/AllIntegrations/IntegInfo.jsx | 3 + .../components/AllIntegrations/NewInteg.jsx | 10 + .../AllIntegrations/NextCrm/EditNextCrm.jsx | 76 ++++++ .../AllIntegrations/NextCrm/NextCrm.jsx | 130 ++++++++++ .../NextCrm/NextCrmActions.jsx | 244 ++++++++++++++++++ .../NextCrm/NextCrmAuthorization.jsx | 30 +++ .../NextCrm/NextCrmCommonFunc.js | 109 ++++++++ .../NextCrm/NextCrmFieldMap.jsx | 104 ++++++++ .../NextCrm/NextCrmIntegLayout.jsx | 208 +++++++++++++++ .../AllIntegrations/NextCrm/staticData.js | 176 +++++++++++++ .../src/components/Flow/New/SelectAction.jsx | 1 + 17 files changed, 1414 insertions(+), 1 deletion(-) create mode 100644 backend/Actions/NextCrm/NextCrmController.php create mode 100644 backend/Actions/NextCrm/RecordApiHelper.php create mode 100644 backend/Actions/NextCrm/Routes.php create mode 100644 frontend/src/components/AllIntegrations/NextCrm/EditNextCrm.jsx create mode 100644 frontend/src/components/AllIntegrations/NextCrm/NextCrm.jsx create mode 100644 frontend/src/components/AllIntegrations/NextCrm/NextCrmActions.jsx create mode 100644 frontend/src/components/AllIntegrations/NextCrm/NextCrmAuthorization.jsx create mode 100644 frontend/src/components/AllIntegrations/NextCrm/NextCrmCommonFunc.js create mode 100644 frontend/src/components/AllIntegrations/NextCrm/NextCrmFieldMap.jsx create mode 100644 frontend/src/components/AllIntegrations/NextCrm/NextCrmIntegLayout.jsx create mode 100644 frontend/src/components/AllIntegrations/NextCrm/staticData.js diff --git a/backend/Actions/NextCrm/NextCrmController.php b/backend/Actions/NextCrm/NextCrmController.php new file mode 100644 index 000000000..ef6136eaa --- /dev/null +++ b/backend/Actions/NextCrm/NextCrmController.php @@ -0,0 +1,113 @@ +contact->get_tags()); + wp_send_json_success($response, 200); + } + + public function refreshLists() + { + self::isExists(); + + $response['lists'] = self::toOptions(nextcrm_manager()->contact->get_lists()); + wp_send_json_success($response, 200); + } + + public function refreshCampaigns() + { + self::isExists(); + + $response['campaigns'] = self::toOptions(nextcrm_get_campaigns()); + wp_send_json_success($response, 200); + } + + public function refreshContactFields() + { + self::isExists(); + + $response['contactFields'] = self::toOptions(nextcrm_get_contact_properties()); + wp_send_json_success($response, 200); + } + + public function refreshContactTypes() + { + self::isExists(); + + $response['contactTypes'] = self::toOptions(nextcrm_contact_types()); + wp_send_json_success($response, 200); + } + + public function refreshContactStatuses() + { + self::isExists(); + + $response['contactStatuses'] = self::toOptions(nextcrm_contact_status()); + wp_send_json_success($response, 200); + } + + public function execute($integrationData, $fieldValues) + { + $integrationDetails = $integrationData->flow_details; + $integId = $integrationData->id; + $fieldMap = $integrationDetails->field_map; + $utilities = isset($integrationDetails->utilities) ? $integrationDetails->utilities : []; + + if (empty($fieldMap)) { + return new WP_Error('field_map_empty', __('Field map is empty', 'bit-integrations')); + } + + $recordApiHelper = new RecordApiHelper($integrationDetails, $integId); + + return $recordApiHelper->execute($fieldValues, $fieldMap, $utilities); + } + + /** + * NextCRM returns its dropdown sources as `[value => label]` maps. + * + * @param array $map + * + * @return array + */ + private static function toOptions($map) + { + $options = []; + + foreach ((array) $map as $value => $label) { + $options[] = (object) [ + 'value' => $value, + 'label' => \is_scalar($label) ? (string) $label : (string) $value, + ]; + } + + return $options; + } +} diff --git a/backend/Actions/NextCrm/RecordApiHelper.php b/backend/Actions/NextCrm/RecordApiHelper.php new file mode 100644 index 000000000..fc70785aa --- /dev/null +++ b/backend/Actions/NextCrm/RecordApiHelper.php @@ -0,0 +1,189 @@ +_integrationDetails = $integrationDetails; + $this->_integrationID = $integId; + } + + /** + * Execute the integration + * + * @param array $fieldValues Field values from trigger + * @param array $fieldMap Field mapping + * @param array $utilities Optional actions to perform + * + * @return array + */ + public function execute($fieldValues, $fieldMap, $utilities) + { + if (!\defined('NEXTCRM_VERSION')) { + return [ + 'success' => false, + 'message' => __('NextCRM is not installed or activated', 'bit-integrations') + ]; + } + + $fieldData = static::generateReqDataFromFieldMap($fieldMap, $fieldValues); + + $mainAction = $this->_integrationDetails->mainAction ?? 'create_contact'; + + $defaultResponse = [ + 'success' => false, + // translators: %s: Plugin name + 'message' => wp_sprintf(__('%s plugin is not installed or activate', 'bit-integrations'), 'Bit Integrations Pro') + ]; + + switch ($mainAction) { + case 'create_contact': + $response = Hooks::apply(Config::withPrefix('next_crm_create_contact'), $defaultResponse, $fieldData, $utilities); + $type = 'contact'; + + break; + + case 'update_contact': + $response = Hooks::apply(Config::withPrefix('next_crm_update_contact'), $defaultResponse, $fieldData, $utilities); + $type = 'contact'; + + break; + + case 'create_or_update_contact': + $response = Hooks::apply(Config::withPrefix('next_crm_create_or_update_contact'), $defaultResponse, $fieldData, $utilities); + $type = 'contact'; + + break; + + case 'delete_contact': + $response = Hooks::apply(Config::withPrefix('next_crm_delete_contact'), $defaultResponse, $fieldData); + $type = 'contact'; + + break; + + case 'change_contact_status': + $response = Hooks::apply(Config::withPrefix('next_crm_change_contact_status'), $defaultResponse, $fieldData, $this->_integrationDetails); + $type = 'contact'; + + break; + + case 'update_contact_field': + $response = Hooks::apply(Config::withPrefix('next_crm_update_contact_field'), $defaultResponse, $fieldData, $this->_integrationDetails); + $type = 'contact'; + + break; + + case 'set_contact_meta': + $response = Hooks::apply(Config::withPrefix('next_crm_set_contact_meta'), $defaultResponse, $fieldData); + $type = 'contact'; + + break; + + case 'delete_contact_meta': + $response = Hooks::apply(Config::withPrefix('next_crm_delete_contact_meta'), $defaultResponse, $fieldData); + $type = 'contact'; + + break; + + case 'add_contact_activity': + $response = Hooks::apply(Config::withPrefix('next_crm_add_contact_activity'), $defaultResponse, $fieldData, $utilities); + $type = 'activity'; + + break; + + case 'create_tag': + $response = Hooks::apply(Config::withPrefix('next_crm_create_tag'), $defaultResponse, $fieldData); + $type = 'tag'; + + break; + + case 'add_tag_to_contact': + $response = Hooks::apply(Config::withPrefix('next_crm_add_tag_to_contact'), $defaultResponse, $fieldData, $this->_integrationDetails); + $type = 'tag'; + + break; + + case 'remove_tag_from_contact': + $response = Hooks::apply(Config::withPrefix('next_crm_remove_tag_from_contact'), $defaultResponse, $fieldData, $this->_integrationDetails); + $type = 'tag'; + + break; + + case 'create_list': + $response = Hooks::apply(Config::withPrefix('next_crm_create_list'), $defaultResponse, $fieldData); + $type = 'list'; + + break; + + case 'add_contact_to_list': + $response = Hooks::apply(Config::withPrefix('next_crm_add_contact_to_list'), $defaultResponse, $fieldData, $this->_integrationDetails); + $type = 'list'; + + break; + + case 'remove_contact_from_list': + $response = Hooks::apply(Config::withPrefix('next_crm_remove_contact_from_list'), $defaultResponse, $fieldData, $this->_integrationDetails); + $type = 'list'; + + break; + + case 'send_campaign_email': + $response = Hooks::apply(Config::withPrefix('next_crm_send_campaign_email'), $defaultResponse, $fieldData, $utilities, $this->_integrationDetails); + $type = 'campaign'; + + break; + + default: + $response = [ + 'success' => false, + 'message' => __('Invalid action', 'bit-integrations') + ]; + $type = 'NextCrm'; + + break; + } + + $responseType = isset($response['success']) && $response['success'] ? 'success' : 'error'; + LogHandler::save($this->_integrationID, ['type' => $type, 'type_name' => $mainAction], $responseType, $response); + + return $response; + } + + private static function generateReqDataFromFieldMap($fieldMap, $fieldValues) + { + $dataFinal = []; + + foreach ($fieldMap as $item) { + $triggerValue = $item->formField; + $actionValue = $item->nextCrmField; + + if (empty($actionValue)) { + continue; + } + + $dataFinal[$actionValue] = $triggerValue === 'custom' && isset($item->customValue) + ? Common::replaceFieldWithValue($item->customValue, $fieldValues) + : $fieldValues[$triggerValue] ?? ''; + } + + return $dataFinal; + } +} diff --git a/backend/Actions/NextCrm/Routes.php b/backend/Actions/NextCrm/Routes.php new file mode 100644 index 000000000..6cce3cf48 --- /dev/null +++ b/backend/Actions/NextCrm/Routes.php @@ -0,0 +1,15 @@ + ['name' => 'MoreConvert Wishlist', 'isPro' => true, 'is_active' => false], 'Newsletter' => ['name' => 'Newsletter', 'isPro' => true, 'is_active' => false], 'NewUserApprove' => ['name' => 'New User Approve', 'isPro' => true, 'is_active' => false], + 'NextCrm' => ['name' => 'NextCRM', 'isPro' => true, 'is_active' => false], 'NexForms' => ['name' => 'NEX-Forms', 'isPro' => true, 'is_active' => false], 'NF' => ['name' => 'Ninja Forms', 'isPro' => true, 'is_active' => false], 'NinjaTables' => ['name' => 'Ninja Tables', 'isPro' => true, 'is_active' => false], diff --git a/frontend/src/Utils/StaticData/webhookIntegrations.js b/frontend/src/Utils/StaticData/webhookIntegrations.js index 1d4c63a90..96768e991 100644 --- a/frontend/src/Utils/StaticData/webhookIntegrations.js +++ b/frontend/src/Utils/StaticData/webhookIntegrations.js @@ -114,7 +114,8 @@ export const customFormIntegrations = [ 'GiveWp', 'SenseiLMS', 'FluentPlayer', - 'BitCrm' + 'BitCrm', + 'NextCrm' ] export const actionHookIntegrations = ['ActionHook'] diff --git a/frontend/src/components/AllIntegrations/EditInteg.jsx b/frontend/src/components/AllIntegrations/EditInteg.jsx index d4daa3176..71341b1ca 100644 --- a/frontend/src/components/AllIntegrations/EditInteg.jsx +++ b/frontend/src/components/AllIntegrations/EditInteg.jsx @@ -180,6 +180,7 @@ const EditCreatorLms = lazy(() => import('./CreatorLms/EditCreatorLms')) const EditUltimateAffiliatePro = lazy(() => import('./UltimateAffiliatePro/EditUltimateAffiliatePro')) const EditBookly = lazy(() => import('./Bookly/EditBookly')) const EditFluentCart = lazy(() => import('./FluentCart/EditFluentCart')) +const EditNextCrm = lazy(() => import('./NextCrm/EditNextCrm')) const EditFluentPlayer = lazy(() => import('./FluentPlayer/EditFluentPlayer')) const EditBitCrm = lazy(() => import('./BitCrm/EditBitCrm')) const EditWsms = lazy(() => import('./Wsms/EditWsms')) @@ -629,6 +630,8 @@ const IntegType = memo(({ allIntegURL, flow }) => { return case 'Bookly': return + case 'FluentCart': + return case 'FluentCart': return case 'FluentPlayer': diff --git a/frontend/src/components/AllIntegrations/IntegInfo.jsx b/frontend/src/components/AllIntegrations/IntegInfo.jsx index 8f1f95c41..841b1f7e0 100644 --- a/frontend/src/components/AllIntegrations/IntegInfo.jsx +++ b/frontend/src/components/AllIntegrations/IntegInfo.jsx @@ -185,6 +185,7 @@ const UltimateAffiliateProAuthorization = lazy( ) const BooklyAuthorization = lazy(() => import('./Bookly/BooklyAuthorization')) const FluentCartAuthorization = lazy(() => import('./FluentCart/FluentCartAuthorization')) +const NextCrmAuthorization = lazy(() => import('./NextCrm/NextCrmAuthorization')) const FluentPlayerAuthorization = lazy(() => import('./FluentPlayer/FluentPlayerAuthorization')) const BitCrmAuthorization = lazy(() => import('./BitCrm/BitCrmAuthorization')) const WsmsAuthorization = lazy(() => import('./Wsms/WsmsAuthorization')) @@ -670,6 +671,8 @@ const IntegrationInfo = memo(({ integrationConf, location, editUrl }) => { return case 'FluentCart': return + case 'NextCrm': + return case 'FluentPlayer': return case 'BitCrm': diff --git a/frontend/src/components/AllIntegrations/NewInteg.jsx b/frontend/src/components/AllIntegrations/NewInteg.jsx index ab927ddcc..329f304d6 100644 --- a/frontend/src/components/AllIntegrations/NewInteg.jsx +++ b/frontend/src/components/AllIntegrations/NewInteg.jsx @@ -179,6 +179,7 @@ const CreatorLms = lazy(() => import('./CreatorLms/CreatorLms')) const UltimateAffiliatePro = lazy(() => import('./UltimateAffiliatePro/UltimateAffiliatePro')) const Bookly = lazy(() => import('./Bookly/Bookly')) const FluentCart = lazy(() => import('./FluentCart/FluentCart')) +const NextCrm = lazy(() => import('./NextCrm/NextCrm')) const FluentPlayer = lazy(() => import('./FluentPlayer/FluentPlayer')) const BitCrm = lazy(() => import('./BitCrm/BitCrm')) const Wsms = lazy(() => import('./Wsms/Wsms')) @@ -1741,6 +1742,15 @@ const NewIntegs = memo(({ integUrlName, allIntegURL, flow, setFlow }) => { setFlow={setFlow} /> ) + case 'NextCrm': + return ( + + ) case 'FluentCart': return ( + + +
+ {__('Integration Name:', 'bit-integrations')} + handleInput(e, nextCrmConf, setNextCrmConf)} + name="name" + value={nextCrmConf.name} + type="text" + placeholder={__('Integration Name...', 'bit-integrations')} + /> +
+
+ + + + + + + saveActionConf({ + flow, + setFlow, + allIntegURL, + conf: nextCrmConf, + navigate, + id, + edit: 1, + setIsLoading, + setSnackbar + }) + } + disabled={!checkMappedFields(nextCrmConf)} + isLoading={isLoading} + dataConf={nextCrmConf} + setDataConf={setNextCrmConf} + formFields={formFields} + /> +
+ + ) +} diff --git a/frontend/src/components/AllIntegrations/NextCrm/NextCrm.jsx b/frontend/src/components/AllIntegrations/NextCrm/NextCrm.jsx new file mode 100644 index 000000000..3f3b16e53 --- /dev/null +++ b/frontend/src/components/AllIntegrations/NextCrm/NextCrm.jsx @@ -0,0 +1,130 @@ +import { useState } from 'react' +import 'react-multiple-select-dropdown-lite/dist/index.css' +import { useNavigate, useParams } from 'react-router' +import BackIcn from '../../../Icons/BackIcn' +import { __ } from '../../../Utils/i18nwrap' +import SnackMsg from '../../Utilities/SnackMsg' +import { saveIntegConfig } from '../IntegrationHelpers/IntegrationHelpers' +import IntegrationStepThree from '../IntegrationHelpers/IntegrationStepThree' +import NextCrmAuthorization from './NextCrmAuthorization' +import { checkMappedFields } from './NextCrmCommonFunc' +import NextCrmIntegLayout from './NextCrmIntegLayout' +import { needsCampaign, needsContactField, needsList, needsStatus, needsTag } from './staticData' + +const requiredSelect = { + selectedStatus: needsStatus, + selectedField: needsContactField, + selectedTag: needsTag, + selectedList: needsList, + selectedCampaign: needsCampaign +} + +export default function NextCrm({ formFields, setFlow, flow, allIntegURL }) { + const navigate = useNavigate() + const { formID } = useParams() + const [isLoading, setIsLoading] = useState(false) + const [step, setStep] = useState(1) + const [snack, setSnackbar] = useState({ show: false }) + const [nextCrmConf, setNextCrmConf] = useState({ + name: 'NextCRM', + type: 'NextCrm', + field_map: [{ formField: '', nextCrmField: '' }], + actions: {}, + mainAction: '' + }) + + const nextPage = val => { + setTimeout(() => { + document.getElementById('btcd-settings-wrp').scrollTop = 0 + }, 300) + + if (val === 3) { + const missingSelect = Object.keys(requiredSelect).find( + key => requiredSelect[key].includes(nextCrmConf.mainAction) && !nextCrmConf?.[key] + ) + + if (missingSelect) { + setSnackbar({ + show: true, + msg: __('Please complete all required selections to continue.', 'bit-integrations') + }) + return + } + + if (!checkMappedFields(nextCrmConf)) { + setSnackbar({ + show: true, + msg: __('Please map all required fields to continue.', 'bit-integrations') + }) + return + } + + if (nextCrmConf.name !== '' && nextCrmConf.field_map.length > 0) { + setStep(val) + } + } else { + setStep(val) + } + } + + return ( +
+ +
+ + {/* STEP 1 */} + + + {/* STEP 2 */} +
+ +
+
+
+ +
+ + {/* STEP 3 */} + + saveIntegConfig(flow, setFlow, allIntegURL, nextCrmConf, navigate, '', '', setIsLoading) + } + isLoading={isLoading} + dataConf={nextCrmConf} + setDataConf={setNextCrmConf} + formFields={formFields} + /> +
+ ) +} diff --git a/frontend/src/components/AllIntegrations/NextCrm/NextCrmActions.jsx b/frontend/src/components/AllIntegrations/NextCrm/NextCrmActions.jsx new file mode 100644 index 000000000..31122db6b --- /dev/null +++ b/frontend/src/components/AllIntegrations/NextCrm/NextCrmActions.jsx @@ -0,0 +1,244 @@ +/* eslint-disable no-param-reassign */ + +import { useState } from 'react' +import { create } from 'mutative' +import MultiSelect from 'react-multiple-select-dropdown-lite' +import { __ } from '../../../Utils/i18nwrap' +import Loader from '../../Loaders/Loader' +import ConfirmModal from '../../Utilities/ConfirmModal' +import TableCheckBox from '../../Utilities/TableCheckBox' +import 'react-multiple-select-dropdown-lite/dist/index.css' +import { + refreshNextCrmContactStatuses, + refreshNextCrmContactTypes, + refreshNextCrmLists, + refreshNextCrmTags +} from './NextCrmCommonFunc' +import { activityStatusOptions, yesNoOptions } from './staticData' + +export default function NextCrmActions({ nextCrmConf, setNextCrmConf, setSnackbar }) { + const [isLoading, setIsLoading] = useState(false) + const [actionMdl, setActionMdl] = useState({ show: false }) + const action = nextCrmConf?.mainAction + const isContactSave = ['create_contact', 'update_contact', 'create_or_update_contact'].includes(action) + + const actionHandler = type => { + setActionMdl({ show: type }) + + if (type === 'contact_type') refreshNextCrmContactTypes(setNextCrmConf, setIsLoading) + if (type === 'status') refreshNextCrmContactStatuses(setNextCrmConf, setIsLoading) + if (type === 'lists') refreshNextCrmLists(setNextCrmConf, setIsLoading) + if (type === 'tags') refreshNextCrmTags(setNextCrmConf, setIsLoading) + } + + const clsActionMdl = () => setActionMdl({ show: false }) + + const setAction = (val, name) => + setNextCrmConf(prevConf => + create(prevConf, draftConf => { + if (!draftConf.utilities) { + draftConf.utilities = {} + } + draftConf.utilities[name] = val + }) + ) + + const toOptions = list => (list ?? []).map(item => ({ label: item.label, value: String(item.value) })) + + return ( + <> +
+
+ {__('Utilities', 'bit-integrations')} +
+
+ + {isContactSave && ( + <> + actionHandler('contact_type')} + className="wdt-200 mt-4 mr-2" + value="contact_type" + title={__('Contact Type', 'bit-integrations')} + subTitle={__('Set the contact type', 'bit-integrations')} + /> + actionHandler('status')} + className="wdt-200 mt-4 mr-2" + value="status" + title={__('Contact Status', 'bit-integrations')} + subTitle={__('Set the contact status', 'bit-integrations')} + /> + actionHandler('lists')} + className="wdt-200 mt-4 mr-2" + value="lists" + title={__('Lists', 'bit-integrations')} + subTitle={__('Assign the contact to lists', 'bit-integrations')} + /> + actionHandler('tags')} + className="wdt-200 mt-4 mr-2" + value="tags" + title={__('Tags', 'bit-integrations')} + subTitle={__('Assign tags to the contact', 'bit-integrations')} + /> + + )} + + {action === 'add_contact_activity' && ( + actionHandler('activity_status')} + className="wdt-200 mt-4 mr-2" + value="activity_status" + title={__('Activity Status', 'bit-integrations')} + subTitle={__('Set the activity status', 'bit-integrations')} + /> + )} + + {action === 'send_campaign_email' && ( + actionHandler('skip_already_sent')} + className="wdt-200 mt-4 mr-2" + value="skip_already_sent" + title={__('Skip If Already Sent', 'bit-integrations')} + subTitle={__('Do not queue a contact twice', 'bit-integrations')} + /> + )} + + +
+ setAction(val, 'selected_contact_type')} + singleSelect + closeOnSelect + /> + {isLoading && ( + + )} + + + +
+ setAction(val, 'selected_status')} + singleSelect + closeOnSelect + /> + {isLoading && ( + + )} + + + +
+ setAction(val.split(','), 'selected_lists')} + /> + {isLoading && ( + + )} + + + +
+ setAction(val.split(','), 'selected_tags')} + /> + {isLoading && ( + + )} + + + +
+ setAction(val, 'selected_activity_status')} + singleSelect + closeOnSelect + /> + + + +
+ setAction(val, 'selected_skip_already_sent')} + singleSelect + closeOnSelect + /> + + + ) +} diff --git a/frontend/src/components/AllIntegrations/NextCrm/NextCrmAuthorization.jsx b/frontend/src/components/AllIntegrations/NextCrm/NextCrmAuthorization.jsx new file mode 100644 index 000000000..1b9bec68c --- /dev/null +++ b/frontend/src/components/AllIntegrations/NextCrm/NextCrmAuthorization.jsx @@ -0,0 +1,30 @@ +import { useCallback } from 'react' +import { AUTH_TYPES } from '../../../Utils/connectionAuth' +import { __ } from '../../../Utils/i18nwrap' +import tutorialLinks from '../../../Utils/StaticData/tutorialLinks' +import Authorization from '../../Connections/Authorization' + +export default function NextCrmAuthorization({ nextCrmConf, setNextCrmConf, step, nextPage, isInfo }) { + const setStep = useCallback(value => nextPage(value), [nextPage]) + return ( + + ) +} diff --git a/frontend/src/components/AllIntegrations/NextCrm/NextCrmCommonFunc.js b/frontend/src/components/AllIntegrations/NextCrm/NextCrmCommonFunc.js new file mode 100644 index 000000000..671818cd5 --- /dev/null +++ b/frontend/src/components/AllIntegrations/NextCrm/NextCrmCommonFunc.js @@ -0,0 +1,109 @@ +import { create } from 'mutative' +import toast from 'react-hot-toast' +import bitsFetch from '../../../Utils/bitsFetch' +import { __ } from '../../../Utils/i18nwrap' + +export const handleInput = (e, nextCrmConf, setNextCrmConf) => { + const { name, value } = e.target + + setNextCrmConf(prevConf => + create(prevConf, draftConf => { + draftConf[name] = value + }) + ) +} + +const refreshOptions = + (route, dataKey, confKey, successMsg, errorMsg) => (setNextCrmConf, setIsLoading) => { + setIsLoading(true) + bitsFetch(null, route) + .then(result => { + if (result && result?.success && result?.data?.[dataKey]) { + setNextCrmConf(prevConf => + create(prevConf, draftConf => { + draftConf[confKey] = result.data[dataKey] + }) + ) + + setIsLoading(false) + toast.success(successMsg) + return + } + setIsLoading(false) + toast.error(errorMsg) + }) + .catch(() => setIsLoading(false)) + } + +export const refreshNextCrmTags = refreshOptions( + 'refresh_next_crm_tags', + 'tags', + 'allTags', + __('All tags fetched successfully', 'bit-integrations'), + __('NextCRM tags fetch failed. Please try again', 'bit-integrations') +) + +export const refreshNextCrmLists = refreshOptions( + 'refresh_next_crm_lists', + 'lists', + 'allLists', + __('All lists fetched successfully', 'bit-integrations'), + __('NextCRM lists fetch failed. Please try again', 'bit-integrations') +) + +export const refreshNextCrmCampaigns = refreshOptions( + 'refresh_next_crm_campaigns', + 'campaigns', + 'allCampaigns', + __('All campaigns fetched successfully', 'bit-integrations'), + __('NextCRM campaigns fetch failed. Please try again', 'bit-integrations') +) + +export const refreshNextCrmContactFields = refreshOptions( + 'refresh_next_crm_contact_fields', + 'contactFields', + 'allContactFields', + __('All contact fields fetched successfully', 'bit-integrations'), + __('NextCRM contact fields fetch failed. Please try again', 'bit-integrations') +) + +export const refreshNextCrmContactTypes = refreshOptions( + 'refresh_next_crm_contact_types', + 'contactTypes', + 'allContactTypes', + __('All contact types fetched successfully', 'bit-integrations'), + __('NextCRM contact types fetch failed. Please try again', 'bit-integrations') +) + +export const refreshNextCrmContactStatuses = refreshOptions( + 'refresh_next_crm_contact_statuses', + 'contactStatuses', + 'allContactStatuses', + __('All contact statuses fetched successfully', 'bit-integrations'), + __('NextCRM contact statuses fetch failed. Please try again', 'bit-integrations') +) + +export const checkMappedFields = nextCrmConf => { + const mappedFields = nextCrmConf?.field_map + ? nextCrmConf.field_map.filter( + mappedField => + !mappedField.formField || + !mappedField.nextCrmField || + (mappedField.formField === 'custom' && !mappedField.customValue) + ) + : [] + if (mappedFields.length > 0) { + return false + } + return true +} + +export const generateMappedField = fields => { + const requiredFlds = fields.filter(fld => fld.required === true) + return requiredFlds.length > 0 + ? requiredFlds.map(field => ({ + formField: '', + nextCrmField: field.key + })) + : [{ formField: '', nextCrmField: '' }] +} diff --git a/frontend/src/components/AllIntegrations/NextCrm/NextCrmFieldMap.jsx b/frontend/src/components/AllIntegrations/NextCrm/NextCrmFieldMap.jsx new file mode 100644 index 000000000..3760b5477 --- /dev/null +++ b/frontend/src/components/AllIntegrations/NextCrm/NextCrmFieldMap.jsx @@ -0,0 +1,104 @@ +import { useRecoilValue } from 'recoil' +import { $appConfigState } from '../../../GlobalStates' +import { __, sprintf } from '../../../Utils/i18nwrap' +import { SmartTagField } from '../../../Utils/StaticData/SmartTagField' +import TagifyInput from '../../Utilities/TagifyInput' +import { + addFieldMap, + delFieldMap, + handleCustomValue, + handleFieldMapping +} from '../GlobalIntegrationHelper' + +export default function NextCrmFieldMap({ i, formFields, field, nextCrmConf, setNextCrmConf }) { + const btcbi = useRecoilValue($appConfigState) + const { isPro } = btcbi + + const requiredFlds = nextCrmConf?.nextCrmFields?.filter(fld => fld.required === true) || [] + const nonRequiredFlds = nextCrmConf?.nextCrmFields?.filter(fld => fld.required === false) || [] + + return ( +
+
+
+ + + {field.formField === 'custom' && ( + handleCustomValue(e, i, nextCrmConf, setNextCrmConf)} + label={__('Custom Value', 'bit-integrations')} + className="mr-2" + type="text" + value={field.customValue} + placeholder={__('Custom Value', 'bit-integrations')} + formFields={formFields} + /> + )} + + +
+ {i >= requiredFlds.length && ( + <> + + + + )} +
+
+ ) +} diff --git a/frontend/src/components/AllIntegrations/NextCrm/NextCrmIntegLayout.jsx b/frontend/src/components/AllIntegrations/NextCrm/NextCrmIntegLayout.jsx new file mode 100644 index 000000000..f8782ad3d --- /dev/null +++ b/frontend/src/components/AllIntegrations/NextCrm/NextCrmIntegLayout.jsx @@ -0,0 +1,208 @@ +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/IntegrationHelpers' +import NextCrmActions from './NextCrmActions' +import { + generateMappedField, + refreshNextCrmCampaigns, + refreshNextCrmContactFields, + refreshNextCrmContactStatuses, + refreshNextCrmLists, + refreshNextCrmTags +} from './NextCrmCommonFunc' +import NextCrmFieldMap from './NextCrmFieldMap' +import { + hasUtilities, + modules, + needsCampaign, + needsContactField, + needsList, + needsStatus, + needsTag, + nextCrmStaticData +} from './staticData' + +export default function NextCrmIntegLayout({ + formID, + formFields, + nextCrmConf, + setNextCrmConf, + isLoading, + setIsLoading, + setSnackbar +}) { + const btcbi = useRecoilValue($appConfigState) + const { isPro } = btcbi + const action = nextCrmConf?.mainAction + + const setField = (key, value) => + setNextCrmConf(prevConf => + create(prevConf, draftConf => { + draftConf[key] = value + }) + ) + + const handleMainAction = value => { + setNextCrmConf(prevConf => + create(prevConf, draftConf => { + draftConf.mainAction = value + draftConf.nextCrmFields = nextCrmStaticData[value] ?? [] + draftConf.field_map = generateMappedField(draftConf.nextCrmFields) + }) + ) + + if (needsStatus.includes(value)) refreshNextCrmContactStatuses(setNextCrmConf, setIsLoading) + if (needsContactField.includes(value)) refreshNextCrmContactFields(setNextCrmConf, setIsLoading) + if (needsTag.includes(value)) refreshNextCrmTags(setNextCrmConf, setIsLoading) + if (needsList.includes(value)) refreshNextCrmLists(setNextCrmConf, setIsLoading) + if (needsCampaign.includes(value)) refreshNextCrmCampaigns(setNextCrmConf, setIsLoading) + } + + const toOptions = list => (list ?? []).map(item => ({ label: item.label, value: String(item.value) })) + + const renderSelect = (title, label, confKey, optionsKey, refresher) => ( + <> +
+
+ {label} + setField(confKey, value)} + singleSelect + closeOnSelect + /> + +
+ + ) + + return ( + <> +
+
+ {__('Action:', 'bit-integrations')} + handleMainAction(value)} + options={modules?.map(module => ({ + label: checkIsPro(isPro, module.is_pro) ? module.label : getProLabel(module.label), + value: module.name, + disabled: !checkIsPro(isPro, module.is_pro) + }))} + singleSelect + closeOnSelect + /> +
+ + {needsStatus.includes(action) && + renderSelect( + 'selectedStatus', + __('Status:', 'bit-integrations'), + 'selectedStatus', + 'allContactStatuses', + refreshNextCrmContactStatuses + )} + + {needsContactField.includes(action) && + renderSelect( + 'selectedField', + __('Contact Field:', 'bit-integrations'), + 'selectedField', + 'allContactFields', + refreshNextCrmContactFields + )} + + {needsTag.includes(action) && + renderSelect( + 'selectedTag', + __('Tag:', 'bit-integrations'), + 'selectedTag', + 'allTags', + refreshNextCrmTags + )} + + {needsList.includes(action) && + renderSelect( + 'selectedList', + __('List:', 'bit-integrations'), + 'selectedList', + 'allLists', + refreshNextCrmLists + )} + + {needsCampaign.includes(action) && + renderSelect( + 'selectedCampaign', + __('Campaign:', 'bit-integrations'), + 'selectedCampaign', + 'allCampaigns', + refreshNextCrmCampaigns + )} + + {action && ( + <> +
+
+ {__('Field Map', 'bit-integrations')} +
+
+
+
{__('Form Field', 'bit-integrations')}
+
{__('NextCRM Field', 'bit-integrations')}
+
+ + {nextCrmConf?.field_map?.map((itm, i) => ( + + ))} + +
+ +
+ + )} + + {hasUtilities.includes(action) && ( + + )} + + {isLoading && ( + + )} + + ) +} diff --git a/frontend/src/components/AllIntegrations/NextCrm/staticData.js b/frontend/src/components/AllIntegrations/NextCrm/staticData.js new file mode 100644 index 000000000..7d2da8ba5 --- /dev/null +++ b/frontend/src/components/AllIntegrations/NextCrm/staticData.js @@ -0,0 +1,176 @@ +import { __ } from '../../../Utils/i18nwrap' + +export const modules = [ + { name: 'create_contact', label: __('Create Contact', 'bit-integrations'), is_pro: true }, + { name: 'update_contact', label: __('Update Contact', 'bit-integrations'), is_pro: true }, + { + name: 'create_or_update_contact', + label: __('Create or Update Contact', 'bit-integrations'), + is_pro: true + }, + { name: 'delete_contact', label: __('Delete Contact', 'bit-integrations'), is_pro: true }, + { + name: 'change_contact_status', + label: __('Change Contact Status', 'bit-integrations'), + is_pro: true + }, + { + name: 'update_contact_field', + label: __('Update Contact Field', 'bit-integrations'), + is_pro: true + }, + { name: 'set_contact_meta', label: __('Set Contact Meta', 'bit-integrations'), is_pro: true }, + { + name: 'delete_contact_meta', + label: __('Delete Contact Meta', 'bit-integrations'), + is_pro: true + }, + { + name: 'add_contact_activity', + label: __('Add Contact Activity', 'bit-integrations'), + is_pro: true + }, + { name: 'create_tag', label: __('Create Tag', 'bit-integrations'), is_pro: true }, + { name: 'add_tag_to_contact', label: __('Add Tag to Contact', 'bit-integrations'), is_pro: true }, + { + name: 'remove_tag_from_contact', + label: __('Remove Tag from Contact', 'bit-integrations'), + is_pro: true + }, + { name: 'create_list', label: __('Create List', 'bit-integrations'), is_pro: true }, + { + name: 'add_contact_to_list', + label: __('Add Contact to List', 'bit-integrations'), + is_pro: true + }, + { + name: 'remove_contact_from_list', + label: __('Remove Contact from List', 'bit-integrations'), + is_pro: true + }, + { + name: 'send_campaign_email', + label: __('Send Campaign Email to Contact', 'bit-integrations'), + is_pro: true + } +] + +const ContactEmailField = { + key: 'contact_email', + label: __('Contact Email', 'bit-integrations'), + required: true +} + +const AdditionalFields = [ + { key: 'mobile', label: __('Mobile', 'bit-integrations'), required: false }, + { key: 'source', label: __('Source', 'bit-integrations'), required: false }, + { key: 'date_of_birth', label: __('Date of Birth', 'bit-integrations'), required: false }, + { key: 'gender', label: __('Gender', 'bit-integrations'), required: false }, + { key: 'address', label: __('Address', 'bit-integrations'), required: false }, + { key: 'city', label: __('City', 'bit-integrations'), required: false }, + { key: 'state', label: __('State', 'bit-integrations'), required: false }, + { key: 'zip', label: __('Zip', 'bit-integrations'), required: false }, + { key: 'country', label: __('Country', 'bit-integrations'), required: false } +] + +const ContactProfileFields = [ + { key: 'first_name', label: __('First Name', 'bit-integrations'), required: false }, + { key: 'last_name', label: __('Last Name', 'bit-integrations'), required: false }, + { key: 'photo', label: __('Photo URL', 'bit-integrations'), required: false }, + { key: 'rating', label: __('Rating', 'bit-integrations'), required: false }, + ...AdditionalFields +] + +export const CreateContactFields = [ + { key: 'email_address', label: __('Email Address', 'bit-integrations'), required: true }, + ...ContactProfileFields +] + +export const UpdateContactFields = [ + ContactEmailField, + { key: 'new_email_address', label: __('New Email Address', 'bit-integrations'), required: false }, + ...ContactProfileFields +] + +export const ContactEmailOnlyFields = [ContactEmailField] + +export const UpdateContactFieldFields = [ + ContactEmailField, + { key: 'value', label: __('Value', 'bit-integrations'), required: true } +] + +export const SetContactMetaFields = [ + ContactEmailField, + { key: 'meta_key', label: __('Meta Key', 'bit-integrations'), required: true }, + { key: 'meta_value', label: __('Meta Value', 'bit-integrations'), required: true } +] + +export const DeleteContactMetaFields = [ + ContactEmailField, + { key: 'meta_key', label: __('Meta Key', 'bit-integrations'), required: true } +] + +export const ContactActivityFields = [ + ContactEmailField, + { key: 'title', label: __('Title', 'bit-integrations'), required: true }, + { key: 'description', label: __('Description', 'bit-integrations'), required: false } +] + +export const TaxonomyFields = [ + { key: 'title', label: __('Title', 'bit-integrations'), required: true }, + { key: 'name', label: __('Slug', 'bit-integrations'), required: false }, + { key: 'description', label: __('Description', 'bit-integrations'), required: false } +] + +export const CampaignEmailFields = [ + ContactEmailField, + { key: 'body', label: __('Email Body', 'bit-integrations'), required: true }, + { key: 'subject', label: __('Subject', 'bit-integrations'), required: false } +] + +export const nextCrmStaticData = { + create_contact: CreateContactFields, + create_or_update_contact: CreateContactFields, + update_contact: UpdateContactFields, + delete_contact: ContactEmailOnlyFields, + change_contact_status: ContactEmailOnlyFields, + update_contact_field: UpdateContactFieldFields, + set_contact_meta: SetContactMetaFields, + delete_contact_meta: DeleteContactMetaFields, + add_contact_activity: ContactActivityFields, + create_tag: TaxonomyFields, + create_list: TaxonomyFields, + add_tag_to_contact: ContactEmailOnlyFields, + remove_tag_from_contact: ContactEmailOnlyFields, + add_contact_to_list: ContactEmailOnlyFields, + remove_contact_from_list: ContactEmailOnlyFields, + send_campaign_email: CampaignEmailFields +} + +// Fixed option sets — rendered as selects, never mapped. +export const activityStatusOptions = [ + { label: __('Active', 'bit-integrations'), value: 'active' }, + { label: __('Deactive', 'bit-integrations'), value: 'deactive' }, + { label: __('Delete', 'bit-integrations'), value: 'delete' } +] + +export const yesNoOptions = [ + { label: __('No', 'bit-integrations'), value: 'no' }, + { label: __('Yes', 'bit-integrations'), value: 'yes' } +] + +// Required config selects → IntegLayout. +export const needsStatus = ['change_contact_status'] +export const needsContactField = ['update_contact_field'] +export const needsTag = ['add_tag_to_contact', 'remove_tag_from_contact'] +export const needsList = ['add_contact_to_list', 'remove_contact_from_list'] +export const needsCampaign = ['send_campaign_email'] + +// Optional config selects → Utilities. +export const hasUtilities = [ + 'create_contact', + 'update_contact', + 'create_or_update_contact', + 'add_contact_activity', + 'send_campaign_email' +] diff --git a/frontend/src/components/Flow/New/SelectAction.jsx b/frontend/src/components/Flow/New/SelectAction.jsx index f2efc8ff1..5cb8fa4cc 100644 --- a/frontend/src/components/Flow/New/SelectAction.jsx +++ b/frontend/src/components/Flow/New/SelectAction.jsx @@ -164,6 +164,7 @@ export default function SelectAction() { { type: 'ZagoMail', is_pro: false }, { type: 'Drip', is_pro: false }, { type: 'Newsletter', is_pro: false }, + { type: 'NextCrm', is_pro: true }, { type: 'SureDash', is_pro: true }, { type: 'SureMembers', is_pro: false }, { type: 'Mailster', is_pro: false }, From 3bd5a21411f1b16136ab08d38f8147479bf3cf88 Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Wed, 5 Aug 2026 10:52:57 +0600 Subject: [PATCH 2/3] feat(NextCrm): simplify NextCrmActions props and enhance action handling --- .../AllIntegrations/NextCrm/NextCrmActions.jsx | 6 +++--- .../NextCrm/NextCrmIntegLayout.jsx | 7 +------ .../src/components/Flow/New/SelectAction.jsx | 2 +- frontend/src/resource/img/integ/nextCrm.webp | Bin 0 -> 7396 bytes 4 files changed, 5 insertions(+), 10 deletions(-) create mode 100644 frontend/src/resource/img/integ/nextCrm.webp diff --git a/frontend/src/components/AllIntegrations/NextCrm/NextCrmActions.jsx b/frontend/src/components/AllIntegrations/NextCrm/NextCrmActions.jsx index 31122db6b..3f4332402 100644 --- a/frontend/src/components/AllIntegrations/NextCrm/NextCrmActions.jsx +++ b/frontend/src/components/AllIntegrations/NextCrm/NextCrmActions.jsx @@ -16,7 +16,7 @@ import { } from './NextCrmCommonFunc' import { activityStatusOptions, yesNoOptions } from './staticData' -export default function NextCrmActions({ nextCrmConf, setNextCrmConf, setSnackbar }) { +export default function NextCrmActions({ nextCrmConf, setNextCrmConf }) { const [isLoading, setIsLoading] = useState(false) const [actionMdl, setActionMdl] = useState({ show: false }) const action = nextCrmConf?.mainAction @@ -172,7 +172,7 @@ export default function NextCrmActions({ nextCrmConf, setNextCrmConf, setSnackba options={toOptions(nextCrmConf?.allLists)} className="msl-wrp-options" defaultValue={nextCrmConf?.utilities?.selected_lists} - onChange={val => setAction(val.split(','), 'selected_lists')} + onChange={val => setAction(val ? val.split(',') : [], 'selected_lists')} /> {isLoading && ( @@ -193,7 +193,7 @@ export default function NextCrmActions({ nextCrmConf, setNextCrmConf, setSnackba options={toOptions(nextCrmConf?.allTags)} className="msl-wrp-options" defaultValue={nextCrmConf?.utilities?.selected_tags} - onChange={val => setAction(val.split(','), 'selected_tags')} + onChange={val => setAction(val ? val.split(',') : [], 'selected_tags')} /> {isLoading && ( diff --git a/frontend/src/components/AllIntegrations/NextCrm/NextCrmIntegLayout.jsx b/frontend/src/components/AllIntegrations/NextCrm/NextCrmIntegLayout.jsx index f8782ad3d..8babe9f18 100644 --- a/frontend/src/components/AllIntegrations/NextCrm/NextCrmIntegLayout.jsx +++ b/frontend/src/components/AllIntegrations/NextCrm/NextCrmIntegLayout.jsx @@ -192,12 +192,7 @@ export default function NextCrmIntegLayout({ )} {hasUtilities.includes(action) && ( - + )} {isLoading && ( diff --git a/frontend/src/components/Flow/New/SelectAction.jsx b/frontend/src/components/Flow/New/SelectAction.jsx index 5cb8fa4cc..07424a2c1 100644 --- a/frontend/src/components/Flow/New/SelectAction.jsx +++ b/frontend/src/components/Flow/New/SelectAction.jsx @@ -164,7 +164,7 @@ export default function SelectAction() { { type: 'ZagoMail', is_pro: false }, { type: 'Drip', is_pro: false }, { type: 'Newsletter', is_pro: false }, - { type: 'NextCrm', is_pro: true }, + { type: 'NextCrm', name: 'NextCRM', is_pro: true }, { type: 'SureDash', is_pro: true }, { type: 'SureMembers', is_pro: false }, { type: 'Mailster', is_pro: false }, diff --git a/frontend/src/resource/img/integ/nextCrm.webp b/frontend/src/resource/img/integ/nextCrm.webp new file mode 100644 index 0000000000000000000000000000000000000000..54c00fabec6087b7c6b45838dbbf8a5f1f7443ac GIT binary patch literal 7396 zcma)9Wmr^exZT9i-7$nZq?8Cq%Fq%6NSA~(A|at7;1JRwB_Rli#L!X#k`mJC00Pq8 zA|N1f2k>}~C+@xb$A0#HzIwm))>;~h^77+g0MM0_R@YG%X6{3M7vrL*pmA1W_F+mM zpPW9euYX^1sXQoVOETEf;pk>ncDduWLhvhCprrX}>tSU>J=vbg;dhy_(*^5?rwPO= zlMRxmo2Smo`@}z(zB9J-h3MX>b;914Jf=-Odip?#45At+xmYSlQH}U2-5r>gdq->TPHzPW=^RaPNEcEx0nP)GFrL=w>BGjt==xN9>weR^E?a+VSZRXjl3EHk! z`OWCFlNNHhN~#&_Z+QJ>^dBhoe!~8DkpHph9{s<#;vcjA26LL1*8X<9_U#DK-|Per zB>mxE_$Znld&7JW5%XVg`qMLiIkP7H@7F&I4nqWQ)=ylZ`i-?$jTpg$zk~F*5Mmo% z-L*sdmna6*Ehx@^VPg*8IH-B!{~hmd!87x$eL)k2tuCRU=8UyI(-XIaU4}iG#}sv8 zQN9CT#s8O6N`|V7=PVxj1>3PbHMM=;DtWqN=3X?S!IBqD+D zZEZhox^@#>p0BDN^GPg;+-C4Frhx7w?YH>;?VhX+B-HV&r`z_YO$=W!zDC^;C?7{m z1o|&~fiamh#}QMgxXO0ZSjo~S(S-FOlsT?Lkuzri;|nFJ9>*F1hFManz>F!F%93T_EWY^qu#HI5ipiAQ`(O zsM(7{g=(Wyi^S|3w_lGxlFFw?lYOjq8}{d`u9RKXyi{n!qkf6)E!*(s@ofcb1r|65*DG;U=;( z=wI!!Tgdn?Qrk1_?dp}tn&^T~>wGrm5Glq*5r$1khX?d*7Jm5q_~G(uxEgN`l$T3) z(NIoMeLmo4o12*)e(*M{pVRp*Wc(RTC42P9zQ!{yv5^H1i!a-O}F_ukQqz({W_FjKP!-@-QjCR-=A>Hc|7 zZuYE>o+Xz4TeLwL%C47cV$xqqUX;|jE4CWY^@J85J|+Hmx}^YV;@0Xun->*Z{os{b z=mUQ=Jn6diMn;-tjBpR3 zeIrKR+D~bTMLD{s^gTH@#_dlJoRhH2P$hf`7oaDWl5CJYt^u)B8ozjpAB#P0m~YC0 zFGTjU=U6@-V|YxN6`-utEmtvX(eenJdf@UZephx?>sgyoLbG5dJUj{l#%;!gc5t%S zR^nS%(L>r{?ctZPRHI+Ap)LCkXT4h`f9;rXt(hPTWm?InD!mUSA;z~e zg0DS>j7#9-WTyZ{7V3#=kEkaZ1WlslO`CPB4f zwx&{tD;?00`(%9gpIO?aQqY@g*r0#=)r-j(!yImxVDvc{?I)UDyV)vd^$xf6rpt~& z@uLL+dkM^acYDhmd^a?tf?D-E%MIPs-(SX={+V zjHRiqblzVnOUu{b=NaPKw^_@m7vxq)Bl|*0ChF0Ggj%Rs=0IsG#CV$YoA<|BL?6VO(d~Q7Ga& zpTL?vTIFz8VIpV}viY2oX;3>l%+=O?%SNJc>8im}eb@k&Prm)eIaRdmVdjR{G_&&T85`r^MDSum-oFG^99lr81B2H1o{e9P2GL`C$X18D= zHIZtvT*#n3Mkldae#Yx#ZI9aF%EL&twVdh54eCX;$(v1N5qV$mRO_hrJ+qRt$6|k+ z5t)#g84R-}PD>4=Ca}FPKv6sTUDm?MCCIIbSOVjmyvC4RHPULV#^hWZ1IeWkbZC$^ zm9vk|(ewVKp_R6|iJZy_=1+HQ<5FI!=_v6LZFkSN(ZbFNP&cziqR6F$8h&}|Sr2AS zwL!zk);KqH_j8szH5s?uftBN=Y)7HIFmPIdaYd6=T; zpTxwNPl4RFBpm)2*Fq)B7g!-B8G+AOqAVy!4N-mu*iX)EA|1po)qisNQr3*OmO6Ox z1%e|YZU^}XACHlLJZA^4(rc-yo~yX4qNY@y{#T|xG)fsJ!#phk&UKFxtlgwvtt38a zy`Wm!+^AfQ7lxOA4Ste1+p74UPt<}=;Tq)RmtECG?p=rGhxgi|xdoXzhMPO#r)#z( zsB}HJsDFzjlmlk0)mV`Vu>_3ksTEgw81jE5L^AAql-754X+o|JSn@nO)qRe|Hp`mb zgEK(C9iT!tgiv>Rpl)K9y7QjYnnem`L*HK|fis6tWbUTBpmNugW1##Oi^~3)z}^L2 zr*mZ>b08K>`$!5SB)E47<6|f|ljK|0A56-&Ii~BJZ1lYR2eEp!m}twE)k=g0EguaO z7VnkPQM!l@^#Wey;x|MDDq>wAA*eOIg-o1td+jM?{1(2-t@N4;@?-%j@P}oxbl_3t zpgPuhlkL)Z+^=gm@8)QgEL&q2GnuhFgdcSRFWnn1{nni%p?i zxlqP)3YA*Rko->{X;9ySa&?dFS&S;lTCjp zhUT$?_LH9;=|1cG8g1v7_`4o$()*dtf5=(RUtT-22?fvpC8vH@ty=p@VNAbCB_jcp zXa1&RrfF{dmr^|kE_jIIsuC1-S;VdP?LQX(#Ogw#_3ll$SGvAE(L3jPZ! zOPt!3g>XE=)YjaS4NS(=K6zew=jz+E22Gt0|0zj_$kCL)drbi+$0ub|KI;qSjk=mz zCB~%`3gU4QW6H%lACG$HA0X|u{blc}g!$6bY@o^im_X=+#jsF%qmU1DkX}IyY#fq z22&mmU@>2_W)>$5W}t@yz^L9ev}0d5U|%?_L!N`U2M6Bob^xh-06_*%Qd-bVAG-Kn zV_f4Qxbs73IZwJg0o0Az-|&`zUKt+=g=>_RujTOt;h1UhQV1c# ztN^!Tn{_UBZ-RQG1Ir%R9SZ;{-!6m`Lwb)QFlntk!)=GZJH04|Pch+GDc|A7P+?7o zy*FX?(dg>9c0MAZun1{HYEKM#Ig7b&3IHfWw#Kq_C?lkC_M2HBEBD-;B>gx&>I79) zQW~Lm4ys&>lHnu~y;Tnb$0c(X5;jlmui39a(QN@DrE>Jfqq&v#{k@SD%c#3w3ME%4 zQ+F+kx27#KbNP7Vsu7JKNL+eA0Bki7pYP4w`q%zi$udkzfw4Tv!^A(u&1bKOna0?oSjyiz*g>TN0VB#u=rL?qSLl1-l^KC-eps3&?wpa%Qdg4Kw4caZ%gt9E^uS)a zj}t7-DT6&jiN#tYwr_+>!lijrxst%setZQw8}yW(r$foVpFUB6^HZZMxi>iY7Kq;r z?Fn-)x5!qKKpwJhjS|3W={mq3o#LW**^HcCzi-uVs&6>&F`RKDDkxl#eMnvHIV9J{ z*3gGb;HuZ^BNbknf?oXlQg?KEd{4B~4f|Gt!rD4|kgBF8EuD`_f{N)BecT9! zptt$3(@P}yi3=xQ+-I)ED&fLspXZiuFUD3`8iignNySJGo6!K`gzk)1`3r$|2Bsu! z!Et-M3)cO-#;=(tl#IKvmJl~WJ*?6teLQxvA3pQc~Mi^2`DD}b)l8( zHQWJ@8`y`m8c{4MS2phRc9SxcsBUr%$Z}L>+hWMdlYJ~x{mSL^L-?J5rE?Hm#oj}s ze7$Ff8h>HnTZ2+sS&mCVpFJR#h%4XrBUG{CVnp;|cr*YV5# z?JN8A2xCRQyR3!Us-4Hv3-)b$54jlK5F^QK=yKdxhMcRy}QK?1#5zM2U8KWz;ogf;%Y}=eX zL9X0PBB(cOZe!Z~$X_g3)a+@^2GDiMfb}EK#9iSN>I}-)nF&u3*Z9f|d(^4z(?>n~ zPTksck3>E5z}t9)?!v5*S{Vjx6EW)y>rKA2{pes2?M@uqP0_w94P-BYXyZ^-%i9mb zadLLWy+6&p4mRveagQs}HUD;bE|$WCVtlD$KUM%1rxQgo7ckRh*m)VN>$uz>Cd7-L_DdSFFv0d zX3iTtE^3q%Cf|kqe9F=!_trAn7VVT^iNumSuYkRGROFqY`SgsFg-tsJzVsT1m8f)_ zcZ3Wjr%5GrtLv*N9Uh2Epu1&KuA<|KN<_fP(NLVEiM5q%UA$oTr2ltC`;c@>u!@(U z$pfjc(OHFr*DK0^`O$@zFcn^q=liTsFPX{@f%`=_H*Ss=Hd>r!uSIcoY$vW5b!8aH zOZtS1%e)=uK8?Iix}o!Uq`NZ6SM`B8*JQsO$+*NrmRYg)xf{(OZ$Q%TF(cY|29^TSZXWbuuZ~qNc!RemIIe}5Fw0Jo)-gtD^NXixcb1%^iQhPF? zLB4TV{LGKCd~C}V_^Qch$fYOjW_O-?G`^N4BtR6_6+Yk<8U`<*@>xRdT&K@wd{(g{8s?XKM!AkWZ*d48=SzAih z9S6X9IXaD$SciVx?)%(PAFWOzqRAmLa-OecjNK_&*O<0QJ~&Ts)jn3y4HCvK>xkA$ zs{K~&7EYw}t=)UsujojhUVfvVw_hE#XKk+?O@_wCnX-i*a6>Al7C-g+CPALB&K73X z_+_(bZ`0~*r@$UeJU@T&F{~HENss}GN=KSOE3oJg)1^;t+@=cFMO=}l(uStAQDGn7 zcafYlvRi$kiYNA95U0uYexOu;I;kkfzghs=4Jgcn5VDMjoGJ!wOdEc`{^4#BkvXwe zH`UhjaNmr`)pc_UpA8&P3nI?7jb#5ca$DzESASDcWE5a6q>hOw-pt$ZMz`&EdRN?2 zkwv#-5LYyJzapA+?ASd}G62Jn9s7$`qUi$l@Ttng7NFIKMvp1{RtQn>X+$?ra@{0K z&$_$Zx+9KzD9_mml&ogz_#!(^p|Sfu#tPT>(US3g|W zshNsUyuggg4RR62VC(glx4wW248vYRF(+CkP;B+Xv9hDMIA5-Kzp-EZ2oJHE1{|P| zf+{CMc=HY)qfaqyoMOE$jd(CM_`VcV5i?yrgZZOVPb=26X%?ilBeWI~%M|QR$s~D8 zg}{8dW%DLIv}&FO)XTxn^H@rv3XTLOhc#uBnK3f1&h2KJ!2QIGdKSd0KoDGrkjjZsT zeVjk0TLA!IJWeP-j*Ft;a#CPGc`}+rcew#YPnW_V2lxzT!a~{3^HosPbUF-cxzs0w zRiEK1n`x+jEY_JDOLe2rvO=i4SBAPbjuK;)DF{3TU?3RWQ$y{{UF-WbAwuZ>n?uzN jXaXtGhR-%;aF` Date: Wed, 5 Aug 2026 12:18:20 +0600 Subject: [PATCH 3/3] feat(NextCrm): update field mapping and enhance loading states in NextCrm integration --- .../components/AllIntegrations/EditInteg.jsx | 2 +- .../NextCrm/NextCrmActions.jsx | 234 ++++++++---------- .../NextCrm/NextCrmIntegLayout.jsx | 56 +++-- 3 files changed, 132 insertions(+), 160 deletions(-) diff --git a/frontend/src/components/AllIntegrations/EditInteg.jsx b/frontend/src/components/AllIntegrations/EditInteg.jsx index 71341b1ca..6c8c05d75 100644 --- a/frontend/src/components/AllIntegrations/EditInteg.jsx +++ b/frontend/src/components/AllIntegrations/EditInteg.jsx @@ -630,7 +630,7 @@ const IntegType = memo(({ allIntegURL, flow }) => { return case 'Bookly': return - case 'FluentCart': + case 'NextCrm': return case 'FluentCart': return diff --git a/frontend/src/components/AllIntegrations/NextCrm/NextCrmActions.jsx b/frontend/src/components/AllIntegrations/NextCrm/NextCrmActions.jsx index 3f4332402..61058a518 100644 --- a/frontend/src/components/AllIntegrations/NextCrm/NextCrmActions.jsx +++ b/frontend/src/components/AllIntegrations/NextCrm/NextCrmActions.jsx @@ -45,14 +45,54 @@ export default function NextCrmActions({ nextCrmConf, setNextCrmConf }) { const toOptions = list => (list ?? []).map(item => ({ label: item.label, value: String(item.value) })) - return ( - <> -
-
- {__('Utilities', 'bit-integrations')} -
-
+ const renderModal = ({ type, title, options, valueName, isMulti = false, refresher }) => ( + +
+
{title}
+ {isLoading ? ( + + ) : ( +
+ setAction(isMulti ? (val ? val.split(',') : []) : val, valueName)} + singleSelect={!isMulti} + closeOnSelect={!isMulti} + /> + {refresher && ( + + )} +
+ )} + + ) + return ( +
{isContactSave && ( <> 0} onChange={() => actionHandler('lists')} className="wdt-200 mt-4 mr-2" value="lists" @@ -80,7 +120,7 @@ export default function NextCrmActions({ nextCrmConf, setNextCrmConf }) { subTitle={__('Assign the contact to lists', 'bit-integrations')} /> 0} onChange={() => actionHandler('tags')} className="wdt-200 mt-4 mr-2" value="tags" @@ -112,133 +152,53 @@ export default function NextCrmActions({ nextCrmConf, setNextCrmConf }) { /> )} - -
- setAction(val, 'selected_contact_type')} - singleSelect - closeOnSelect - /> - {isLoading && ( - - )} - - - -
- setAction(val, 'selected_status')} - singleSelect - closeOnSelect - /> - {isLoading && ( - - )} - - - -
- setAction(val ? val.split(',') : [], 'selected_lists')} - /> - {isLoading && ( - - )} - - - -
- setAction(val ? val.split(',') : [], 'selected_tags')} - /> - {isLoading && ( - - )} - - - -
- setAction(val, 'selected_activity_status')} - singleSelect - closeOnSelect - /> - - - -
- setAction(val, 'selected_skip_already_sent')} - singleSelect - closeOnSelect - /> - - + {renderModal({ + type: 'contact_type', + title: __('Contact Type', 'bit-integrations'), + options: toOptions(nextCrmConf?.allContactTypes), + valueName: 'selected_contact_type', + refresher: refreshNextCrmContactTypes + })} + + {renderModal({ + type: 'status', + title: __('Contact Status', 'bit-integrations'), + options: toOptions(nextCrmConf?.allContactStatuses), + valueName: 'selected_status', + refresher: refreshNextCrmContactStatuses + })} + + {renderModal({ + type: 'lists', + title: __('Lists', 'bit-integrations'), + options: toOptions(nextCrmConf?.allLists), + valueName: 'selected_lists', + isMulti: true, + refresher: refreshNextCrmLists + })} + + {renderModal({ + type: 'tags', + title: __('Tags', 'bit-integrations'), + options: toOptions(nextCrmConf?.allTags), + valueName: 'selected_tags', + isMulti: true, + refresher: refreshNextCrmTags + })} + + {renderModal({ + type: 'activity_status', + title: __('Activity Status', 'bit-integrations'), + options: activityStatusOptions, + valueName: 'selected_activity_status' + })} + + {renderModal({ + type: 'skip_already_sent', + title: __('Skip If Already Sent', 'bit-integrations'), + options: yesNoOptions, + valueName: 'selected_skip_already_sent' + })} +
) } diff --git a/frontend/src/components/AllIntegrations/NextCrm/NextCrmIntegLayout.jsx b/frontend/src/components/AllIntegrations/NextCrm/NextCrmIntegLayout.jsx index 8babe9f18..94414f635 100644 --- a/frontend/src/components/AllIntegrations/NextCrm/NextCrmIntegLayout.jsx +++ b/frontend/src/components/AllIntegrations/NextCrm/NextCrmIntegLayout.jsx @@ -155,48 +155,60 @@ export default function NextCrmIntegLayout({ refreshNextCrmCampaigns )} - {action && ( - <> -
-
- {__('Field Map', 'bit-integrations')} -
+ {isLoading && ( + + )} + + {action && nextCrmConf?.nextCrmFields && ( +
+ {__('Map Fields', 'bit-integrations')}
-
-
{__('Form Field', 'bit-integrations')}
-
{__('NextCRM Field', 'bit-integrations')}
+
+
+ {__('Form Fields', 'bit-integrations')} +
+
+ {__('NextCRM Fields', 'bit-integrations')} +
{nextCrmConf?.field_map?.map((itm, i) => ( ))} -
+
- - )} - - {hasUtilities.includes(action) && ( - +
+
)} - {isLoading && ( - + {action && nextCrmConf?.nextCrmFields && hasUtilities.includes(action) && ( +
+ {__('Utilities', 'bit-integrations')} +
+ +
)} )