From e229a1a4b21f629753143836d346ee5e5ae1d29f Mon Sep 17 00:00:00 2001 From: Rishad Alam <101513331+RishadAlam@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:37:41 +0600 Subject: [PATCH 01/18] feat: update ProFeatureSubtitle prop names and add send_confirmation_email option in MailPoetActions --- backend/Actions/MailPoet/RecordApiHelper.php | 2 ++ .../ActionProFeatureLabels.jsx | 18 +++++++-------- .../Klaviyo/KlaviyoActions.jsx | 2 +- .../MailPoet/MailPoetActions.jsx | 23 ++++++++++++++----- .../Salesforce/SalesforceActions.jsx | 10 ++++---- 5 files changed, 34 insertions(+), 21 deletions(-) diff --git a/backend/Actions/MailPoet/RecordApiHelper.php b/backend/Actions/MailPoet/RecordApiHelper.php index 530370f2a..9611b336c 100644 --- a/backend/Actions/MailPoet/RecordApiHelper.php +++ b/backend/Actions/MailPoet/RecordApiHelper.php @@ -91,6 +91,8 @@ public function execute($fieldValues, $fieldMap, $lists, $actions) } $fieldData = static::setFieldMap($fieldMap, $fieldValues); + $fieldMap['send_confirmation_email'] = isset($actions->send_confirmation_email) ? (bool) $actions->send_confirmation_email : false; + $recordApiResponse = $this->insertRecord($fieldData, $lists, $actions); if ($recordApiResponse['success']) { diff --git a/frontend/src/components/AllIntegrations/IntegrationHelpers/ActionProFeatureLabels.jsx b/frontend/src/components/AllIntegrations/IntegrationHelpers/ActionProFeatureLabels.jsx index b1168477a..8ca4adc40 100644 --- a/frontend/src/components/AllIntegrations/IntegrationHelpers/ActionProFeatureLabels.jsx +++ b/frontend/src/components/AllIntegrations/IntegrationHelpers/ActionProFeatureLabels.jsx @@ -14,22 +14,22 @@ const ProFeatureTitle = ({ title }) => { ) } -const ProFeatureSubtitle = ({ title, subtitle, proVersion }) => { +const ProFeatureSubtitle = ({ title, subTitle, proVersion }) => { const btcbi = useRecoilValue($appConfigState) const { isPro } = btcbi return ( {isPro - ? subtitle + ? subTitle : sprintf( - __( - 'The Bit Integrations Pro v(%s) plugin needs to be installed and activated to enable the %s feature', - 'bit-integrations' - ), - proVersion, - title - )} + __( + 'The Bit Integrations Pro v(%s) plugin needs to be installed and activated to enable the %s feature', + 'bit-integrations' + ), + proVersion, + title + )} ) } diff --git a/frontend/src/components/AllIntegrations/Klaviyo/KlaviyoActions.jsx b/frontend/src/components/AllIntegrations/Klaviyo/KlaviyoActions.jsx index 3f7cbbd42..b636f68ba 100644 --- a/frontend/src/components/AllIntegrations/Klaviyo/KlaviyoActions.jsx +++ b/frontend/src/components/AllIntegrations/Klaviyo/KlaviyoActions.jsx @@ -34,7 +34,7 @@ export default function KlaviyoActions({ klaviyoConf, setKlaviyoConf, loading, s subTitle={ { const newConf = { ...mailPoetConf } - if (type === 'update') { - if (e.target.checked) { - newConf.actions.update = true - } else { - delete newConf.actions.update - } + if (e.target.checked) { + newConf.actions[type] = true + } else { + delete newConf.actions[type] } + setMailPoetConf({ ...newConf }) } @@ -42,6 +41,18 @@ export default function MailPoetActions({ mailPoetConf, setMailPoetConf }) { /> } /> + actionHandler(e, 'send_confirmation_email')} + className="wdt-200 mt-4 mr-2" + value="user_share" + isInfo={!isPro} + title={__('Send Confirmation Email', 'bit-integrations')} + subTitle={__( + 'Can be used to disable a confirmation email. Otherwise, a confirmation email is sent as described above. It is strongly recommended to keep this option set to checked so that MailPoet settings for sign-up confirmation are respected. Turning it to unchecked might lead that subscriber to be added as unconfirmed.', + 'bit-integrations' + )} + /> ) } diff --git a/frontend/src/components/AllIntegrations/Salesforce/SalesforceActions.jsx b/frontend/src/components/AllIntegrations/Salesforce/SalesforceActions.jsx index fb80be012..81b12eb39 100644 --- a/frontend/src/components/AllIntegrations/Salesforce/SalesforceActions.jsx +++ b/frontend/src/components/AllIntegrations/Salesforce/SalesforceActions.jsx @@ -121,7 +121,7 @@ export default function SalesforceActions({ subTitle={ } @@ -330,7 +330,7 @@ export default function SalesforceActions({ subTitle={ } @@ -347,7 +347,7 @@ export default function SalesforceActions({ subTitle={ } @@ -364,7 +364,7 @@ export default function SalesforceActions({ subTitle={ } @@ -381,7 +381,7 @@ export default function SalesforceActions({ subTitle={ } From 2858ee20fadb33640e31d830a258d47ff96ddb7f Mon Sep 17 00:00:00 2001 From: Rishad Alam <101513331+RishadAlam@users.noreply.github.com> Date: Mon, 27 Apr 2026 11:13:59 +0600 Subject: [PATCH 02/18] feat: enhance integration failure notification email template with improved styling and content clarity --- .../integration-failure-notification.php | 273 +++++++++++------- 1 file changed, 167 insertions(+), 106 deletions(-) diff --git a/views/emails/integration-failure-notification.php b/views/emails/integration-failure-notification.php index 8cdd05545..319c3a035 100644 --- a/views/emails/integration-failure-notification.php +++ b/views/emails/integration-failure-notification.php @@ -14,7 +14,6 @@ * @var string $logUrl URL to view integration logs * @var string $timestamp Current timestamp */ - if (! defined('ABSPATH')) { exit; } @@ -26,18 +25,18 @@ '' . esc_html($siteName) . '' ); $bit_integrations_details_title = esc_html__('Failure Details', 'bit-integrations'); -$bit_integrations_flow_label = esc_html__('Integration ID:', 'bit-integrations'); -$bit_integrations_action_name_label = esc_html__('Action Name:', 'bit-integrations'); -$bit_integrations_trigger_name_label = esc_html__('Trigger Name:', 'bit-integrations'); -$bit_integrations_record_type_label = esc_html__('Record Type:', 'bit-integrations'); -$bit_integrations_time_label = esc_html__('Time:', 'bit-integrations'); -$bit_integrations_error_label = esc_html__('Error Message:', 'bit-integrations'); -$bit_integrations_resolve_text = esc_html__('To resolve this issue, please check the integration settings and logs:', 'bit-integrations'); +$bit_integrations_flow_label = esc_html__('Integration ID', 'bit-integrations'); +$bit_integrations_action_name_label = esc_html__('Action Name', 'bit-integrations'); +$bit_integrations_trigger_name_label = esc_html__('Trigger Name', 'bit-integrations'); +$bit_integrations_record_type_label = esc_html__('Record Type', 'bit-integrations'); +$bit_integrations_time_label = esc_html__('Time', 'bit-integrations'); +$bit_integrations_error_label = esc_html__('Error Message', 'bit-integrations'); +$bit_integrations_resolve_text = esc_html__('To resolve this issue, review the integration settings and check the logs for more context.', 'bit-integrations'); $bit_integrations_view_integration = esc_html__('View Integration', 'bit-integrations'); $bit_integrations_view_logs = esc_html__('View Logs', 'bit-integrations'); $bit_integrations_footer_text = sprintf( // translators: %s: Placeholder value - esc_html__('You are receiving this email because failure notifications are enabled in %s. You can disable these notifications in the plugin settings.', 'bit-integrations'), + esc_html__('You received this email because failure notifications are enabled in %s. You can disable these notifications in the plugin settings.', 'bit-integrations'), 'Bit Integrations' ); ?> @@ -48,117 +47,179 @@ <?php echo esc_html($bit_integrations_title); ?> - - + +
From 1fb057b717311d3f8dcb5f42f97cb833fe6de705 Mon Sep 17 00:00:00 2001 From: Rishad Alam <101513331+RishadAlam@users.noreply.github.com> Date: Tue, 28 Apr 2026 10:58:41 +0600 Subject: [PATCH 03/18] feat: add integration management features with tag support - Implemented `integrationsColumns.jsx` to define table columns for integrations, including a tags column with management capabilities. - Created utility functions in `tagUtils.js` for parsing tag input, building tag options, and filtering integrations by tags. - Added `useCompactBreakpoint.js` hook to manage responsive design for compact views. - Developed `useIntegrationActions.js` hook to handle integration actions such as status toggling, deletion, and cloning. - Introduced `useIntegrationTags.js` hook for managing tags and their associations with integrations. - Created `useTagPickerSubmit.js` hook to handle tag creation and assignment logic in the tag picker modal. --- .../AllIntegrations/integrationsColumns.jsx | 145 +++ .../components/AllIntegrations/tagUtils.js | 57 ++ frontend/src/hooks/useCompactBreakpoint.js | 20 + frontend/src/hooks/useIntegrationActions.js | 127 +++ frontend/src/hooks/useIntegrationTags.js | 60 ++ frontend/src/hooks/useTagPickerSubmit.js | 151 +++ frontend/src/pages/AllIntegrations.jsx | 898 +++++------------- 7 files changed, 774 insertions(+), 684 deletions(-) create mode 100644 frontend/src/components/AllIntegrations/integrationsColumns.jsx create mode 100644 frontend/src/components/AllIntegrations/tagUtils.js create mode 100644 frontend/src/hooks/useCompactBreakpoint.js create mode 100644 frontend/src/hooks/useIntegrationActions.js create mode 100644 frontend/src/hooks/useIntegrationTags.js create mode 100644 frontend/src/hooks/useTagPickerSubmit.js diff --git a/frontend/src/components/AllIntegrations/integrationsColumns.jsx b/frontend/src/components/AllIntegrations/integrationsColumns.jsx new file mode 100644 index 000000000..642692cf5 --- /dev/null +++ b/frontend/src/components/AllIntegrations/integrationsColumns.jsx @@ -0,0 +1,145 @@ +/* eslint-disable react/no-unstable-nested-components */ +import MenuBtn from '../Utilities/MenuBtn' +import SingleToggle2 from '../Utilities/SingleToggle2' +import { __ } from '../../Utils/i18nwrap' + +export const initialIntegrationsCols = [ + { + width: 250, + minWidth: 80, + Header: __('Trigger', 'bit-integrations'), + accessor: 'triggered_entity' + }, + { width: 250, minWidth: 80, Header: __('Action Name', 'bit-integrations'), accessor: 'name' }, + { + width: 200, + minWidth: 200, + Header: __('Created At', 'bit-integrations'), + accessor: 'created_at' + } +] + +const TagsCell = ({ + value, + tags, + integrationTags, + isCompactTagColumn, + onRemoveTag, + onOpenTagPicker +}) => { + const integrationId = String(value.row.original.id) + const assignedTagIds = integrationTags[integrationId] || [] + const assignedTags = assignedTagIds + .map(tagId => tags.find(currentTag => String(currentTag.id) === String(tagId))) + .filter(Boolean) + const visibleAssignedTags = assignedTags.slice(0, isCompactTagColumn ? 1 : 2) + const hiddenAssignedTagsCount = Math.max(assignedTags.length - visibleAssignedTags.length, 0) + + return ( +
+ {visibleAssignedTags.map(tag => ( + + {tag.name} + + + ))} + {hiddenAssignedTagsCount > 0 && ( + + +{hiddenAssignedTagsCount} + + )} + +
+ ) +} + +export const buildIntegrationsColumns = ({ + prevCols, + isCompactTagColumn, + tags, + integrationTags, + isValidUser, + onRemoveTag, + onOpenTagPicker, + onToggleStatus, + onShowDelModal, + onShowDupMdl +}) => { + const ncols = (prevCols || initialIntegrationsCols).filter( + itm => itm.accessor !== 't_action' && itm.accessor !== 'status' && itm.accessor !== 'tags' + ) + + ncols.push({ + width: isCompactTagColumn ? 170 : 220, + minWidth: isCompactTagColumn ? 140 : 180, + Header: __('Tags', 'bit-integrations'), + accessor: 'tags', + className: 'table-tags-cell', + Cell: value => ( + + ) + }) + + ncols.push({ + width: 70, + minWidth: 60, + Header: __('Status', 'bit-integrations'), + accessor: 'status', + Cell: value => ( + onToggleStatus(e, value.row.original.id)} + checked={Number(value.row.original.status) === 1} + /> + ) + }) + + ncols.push({ + sticky: 'right', + width: 64, + minWidth: 52, + Header: '', + accessor: 't_action', + Cell: val => ( +
+ onShowDelModal(val.row.original.id, val.row.index)} + dup={() => onShowDupMdl(val.row.original.id, val.row.index)} + /> +
+ ) + }) + + return ncols +} diff --git a/frontend/src/components/AllIntegrations/tagUtils.js b/frontend/src/components/AllIntegrations/tagUtils.js new file mode 100644 index 000000000..840768764 --- /dev/null +++ b/frontend/src/components/AllIntegrations/tagUtils.js @@ -0,0 +1,57 @@ +export const parseTagPickerInput = input => { + const seen = new Set() + const out = [] + String(input || '') + .split(',') + .map(t => t.trim().replace(/\s+/g, ' ')) + .filter(Boolean) + .forEach(name => { + const key = name.toLowerCase() + if (seen.has(key)) return + seen.add(key) + out.push(name) + }) + return out +} + +export const buildTagPickerOptions = tags => { + const seen = new Set() + const opts = [] + tags.forEach(tag => { + const name = tag?.name?.trim() + if (!name) return + const key = name.toLowerCase() + if (seen.has(key)) return + seen.add(key) + opts.push({ label: name, value: name }) + }) + return opts +} + +export const dedupeIds = ids => { + const seen = new Set() + const out = [] + ids.forEach(id => { + const key = String(id) + if (seen.has(key)) return + seen.add(key) + out.push(id) + }) + return out +} + +export const findTagByName = (tags, name) => { + const target = name.toLowerCase() + return tags.find(tag => tag.name.trim().toLowerCase() === target) +} + +export const filterIntegrationsByTags = (integrations, integrationTags, selectedTags) => { + if (!selectedTags.length) return integrations + return integrations.filter(integration => { + const assigned = integrationTags[String(integration.id)] || [] + return selectedTags.some(tagId => assigned.includes(tagId)) + }) +} + +export const hasCustomNamesInPicker = (pickerNames, tags) => + pickerNames.some(name => !findTagByName(tags, name)) diff --git a/frontend/src/hooks/useCompactBreakpoint.js b/frontend/src/hooks/useCompactBreakpoint.js new file mode 100644 index 000000000..4c1471e69 --- /dev/null +++ b/frontend/src/hooks/useCompactBreakpoint.js @@ -0,0 +1,20 @@ +import { useEffect, useState } from 'react' + +export default function useCompactBreakpoint(breakpoint = 1100) { + const [isCompact, setIsCompact] = useState( + typeof window !== 'undefined' ? window.innerWidth <= breakpoint : false + ) + + useEffect(() => { + const update = () => { + setIsCompact(prev => { + const next = window.innerWidth <= breakpoint + return next === prev ? prev : next + }) + } + window.addEventListener('resize', update) + return () => window.removeEventListener('resize', update) + }, [breakpoint]) + + return isCompact +} diff --git a/frontend/src/hooks/useIntegrationActions.js b/frontend/src/hooks/useIntegrationActions.js new file mode 100644 index 000000000..637f5c98f --- /dev/null +++ b/frontend/src/hooks/useIntegrationActions.js @@ -0,0 +1,127 @@ +import { useCallback } from 'react' +import toast from 'react-hot-toast' +import bitsFetch from '../Utils/bitsFetch' +import { __ } from '../Utils/i18nwrap' + +export default function useIntegrationActions({ + integrations, + setIntegrations, + mutate, + integrationTags, + setIntegrationTags, + tags, + persistTagData +}) { + const handleStatus = useCallback( + (e, id) => { + const status = e.target.checked + setIntegrations(prev => + prev.map(int => (int.id === id ? { ...int, status: status ? '1' : '0' } : int)) + ) + bitsFetch({ id, status }, 'flow/toggleStatus') + .then(res => toast.success(__(res.data, 'bit-integrations'))) + .catch(() => toast.error(__('Something went wrong', 'bit-integrations'))) + }, + [setIntegrations] + ) + + const handleDelete = useCallback( + (id, index) => { + const deleteLoad = bitsFetch({ id }, 'flow/delete').then(response => { + if (!response.success) return response.data + const next = [...integrations] + next.splice(index, 1) + mutate(next) + setIntegrations(next) + + const key = String(id) + if (integrationTags[key]) { + const updatedMapping = { ...integrationTags } + delete updatedMapping[key] + setIntegrationTags(updatedMapping) + persistTagData(tags, updatedMapping).catch(() => {}) + } + return __('Integration deleted successfully', 'bit-integrations') + }) + + toast.promise(deleteLoad, { + success: msg => msg, + error: __('Error Occurred', 'bit-integrations'), + loading: __('delete...') + }) + }, + [integrations, mutate, setIntegrations, integrationTags, setIntegrationTags, tags, persistTagData] + ) + + const handleClone = useCallback( + id => { + const loadClone = bitsFetch({ id }, 'flow/clone').then(response => { + if (!response.success) return response.data + const newInteg = response.data + const exist = integrations.find(item => item.id === id) + const cpyInteg = { + id: newInteg.id, + name: `duplicate of ${exist.name}`, + triggered_entity: exist.triggered_entity, + status: exist.status, + created_at: newInteg.created_at + } + setIntegrations([...integrations, cpyInteg]) + return __('Integration clone successfully', 'bit-integrations') + }) + + toast.promise(loadClone, { + success: msg => msg, + error: __('Error Occurred', 'bit-integrations'), + loading: __('cloning...') + }) + }, + [integrations, setIntegrations] + ) + + const setBulkDelete = useCallback( + rows => { + const rowID = [] + const flowID = [] + rows.forEach(r => { + rowID.push(r.id) + flowID.push(r.original.id) + }) + + const bulkDeleteLoading = bitsFetch({ flowID }, 'flow/bulk-delete').then(response => { + if (!response.success) return response.data + + const newData = [...integrations] + for (let i = rowID.length - 1; i >= 0; i -= 1) { + newData.splice(Number(rowID[i]), 1) + } + setIntegrations(newData) + + const updatedMapping = { ...integrationTags } + let isMappingUpdated = false + flowID.forEach(deletedIntegId => { + const key = String(deletedIntegId) + if (updatedMapping[key]) { + delete updatedMapping[key] + isMappingUpdated = true + } + }) + if (isMappingUpdated) { + setIntegrationTags(updatedMapping) + persistTagData(tags, updatedMapping).catch(() => {}) + } + + return __('Integration Deleted Successfully', 'bit-integrations') + }) + + toast.promise(bulkDeleteLoading, { + success: msg => msg, + error: __('Error Occurred', 'bit-integrations'), + loading: __('delete...') + }) + }, + [integrations, integrationTags, tags, persistTagData, setIntegrations, setIntegrationTags] + ) + + return { handleStatus, handleDelete, handleClone, setBulkDelete } +} diff --git a/frontend/src/hooks/useIntegrationTags.js b/frontend/src/hooks/useIntegrationTags.js new file mode 100644 index 000000000..3dbee6976 --- /dev/null +++ b/frontend/src/hooks/useIntegrationTags.js @@ -0,0 +1,60 @@ +import { useCallback, useEffect, useState } from 'react' +import toast from 'react-hot-toast' +import bitsFetch from '../Utils/bitsFetch' +import { __ } from '../Utils/i18nwrap' + +const isPlainObject = value => value && typeof value === 'object' && !Array.isArray(value) + +export default function useIntegrationTags() { + const [tags, setTags] = useState([]) + const [integrationTags, setIntegrationTags] = useState({}) + + const fetchTagData = useCallback( + (showErrorMsg = true) => + bitsFetch({}, 'integration-tags/get', null, 'GET') + .then(res => { + if (!res?.success) throw new Error('tag_load_failed') + setTags(Array.isArray(res?.data?.tags) ? res.data.tags : []) + setIntegrationTags(isPlainObject(res?.data?.integrationTags) ? res.data.integrationTags : {}) + }) + .catch(() => { + if (showErrorMsg) toast.error(__('Failed to load tags', 'bit-integrations')) + }), + [] + ) + + useEffect(() => { + fetchTagData() + }, [fetchTagData]) + + const persistTagData = useCallback( + (nextTags, nextIntegrationTags, successMsg = '') => + bitsFetch( + { tags: nextTags, integrationTags: nextIntegrationTags }, + 'integration-tags/save' + ) + .then(res => { + if (!res?.success) throw new Error('tag_save_failed') + setTags(Array.isArray(res?.data?.tags) ? res.data.tags : nextTags) + setIntegrationTags( + isPlainObject(res?.data?.integrationTags) ? res.data.integrationTags : nextIntegrationTags + ) + if (successMsg) toast.success(successMsg) + }) + .catch(() => { + fetchTagData(false) + toast.error(__('Failed to save tags', 'bit-integrations')) + throw new Error('tag_save_failed') + }), + [fetchTagData] + ) + + return { + tags, + integrationTags, + setTags, + setIntegrationTags, + fetchTagData, + persistTagData + } +} diff --git a/frontend/src/hooks/useTagPickerSubmit.js b/frontend/src/hooks/useTagPickerSubmit.js new file mode 100644 index 000000000..483a31890 --- /dev/null +++ b/frontend/src/hooks/useTagPickerSubmit.js @@ -0,0 +1,151 @@ +import { useCallback } from 'react' +import toast from 'react-hot-toast' +import { + dedupeIds, + findTagByName, + parseTagPickerInput +} from '../components/AllIntegrations/tagUtils' +import { __ } from '../Utils/i18nwrap' + +const TAG_NAME_LIMIT = 20 +const NEW_TAG_COLOR = '#6f42c1' + +export default function useTagPickerSubmit({ + tags, + integrationTags, + persistTagData, + editingIntegrationId, + bulkTagIntegrationIds, + tagPickerInput, + setSelectedTags, + closeTagPickerModal +}) { + return useCallback(() => { + const normalizedTagNames = parseTagPickerInput(tagPickerInput) + + if (!normalizedTagNames.length && !editingIntegrationId) { + toast.error(__('Please select or create at least one tag', 'bit-integrations')) + return Promise.resolve(false) + } + + if (normalizedTagNames.find(t => t.length > TAG_NAME_LIMIT)) { + toast.error(__('Tag name must be 20 characters or less', 'bit-integrations')) + return Promise.resolve(false) + } + + const updatedTags = [...tags] + const resolvedTagIds = [] + let createdTagCount = 0 + + normalizedTagNames.forEach((tagName, index) => { + const existing = findTagByName(updatedTags, tagName) + if (existing) { + resolvedTagIds.push(existing.id) + return + } + const newTag = { id: `${Date.now()}-${index}`, name: tagName, color: NEW_TAG_COLOR } + updatedTags.push(newTag) + resolvedTagIds.push(newTag.id) + createdTagCount += 1 + }) + + const uniqueResolvedTagIds = dedupeIds(resolvedTagIds) + const successMsg = + createdTagCount > 0 + ? __('Tags created and assigned successfully', 'bit-integrations') + : __('Tags assigned successfully', 'bit-integrations') + + if (editingIntegrationId) { + const key = String(editingIntegrationId) + const updatedMapping = { ...integrationTags } + const currentTagIds = (updatedMapping[key] || []).map(String) + const nextTagIds = uniqueResolvedTagIds.map(String) + const isAssignmentChanged = + currentTagIds.length !== nextTagIds.length || + currentTagIds.some(id => !nextTagIds.includes(id)) + + if (!isAssignmentChanged && createdTagCount === 0) { + toast.success(__('No changes found', 'bit-integrations')) + closeTagPickerModal() + return Promise.resolve(true) + } + + if (uniqueResolvedTagIds.length) updatedMapping[key] = uniqueResolvedTagIds + else delete updatedMapping[key] + + return persistTagData(updatedTags, updatedMapping, successMsg) + .then(() => { + closeTagPickerModal() + return true + }) + .catch(() => false) + } + + if (bulkTagIntegrationIds.length > 0) { + const updatedMapping = { ...integrationTags } + let hasAssignmentChange = false + + bulkTagIntegrationIds.forEach(integrationId => { + const key = String(integrationId) + const currentTagIds = updatedMapping[key] || [] + const merged = [...currentTagIds] + + uniqueResolvedTagIds.forEach(tagId => { + if (!merged.some(c => String(c) === String(tagId))) merged.push(tagId) + }) + + if (merged.length !== currentTagIds.length) hasAssignmentChange = true + if (merged.length) updatedMapping[key] = merged + }) + + if (!hasAssignmentChange && createdTagCount === 0) { + toast.success(__('No changes found', 'bit-integrations')) + closeTagPickerModal() + return Promise.resolve(true) + } + + return persistTagData(updatedTags, updatedMapping, successMsg) + .then(() => { + closeTagPickerModal() + return true + }) + .catch(() => false) + } + + const mergeSelected = prev => { + const next = [...prev] + uniqueResolvedTagIds.forEach(tagId => { + if (!next.some(s => String(s) === String(tagId))) next.push(tagId) + }) + return next + } + + if (createdTagCount === 0) { + setSelectedTags(mergeSelected) + toast.success(__('Tag selected successfully', 'bit-integrations')) + closeTagPickerModal() + return Promise.resolve(true) + } + + return persistTagData( + updatedTags, + integrationTags, + __('Tags created successfully', 'bit-integrations') + ) + .then(() => { + setSelectedTags(mergeSelected) + closeTagPickerModal() + return true + }) + .catch(() => false) + }, [ + tags, + integrationTags, + persistTagData, + editingIntegrationId, + bulkTagIntegrationIds, + tagPickerInput, + setSelectedTags, + closeTagPickerModal + ]) +} diff --git a/frontend/src/pages/AllIntegrations.jsx b/frontend/src/pages/AllIntegrations.jsx index 83e1f11c6..1c6f5b49a 100644 --- a/frontend/src/pages/AllIntegrations.jsx +++ b/frontend/src/pages/AllIntegrations.jsx @@ -1,38 +1,53 @@ -/* eslint-disable react/no-unstable-nested-components */ -/* eslint-disable no-unused-expressions */ -/* eslint-disable max-len */ -import { lazy, memo, useCallback, useEffect, useState } from 'react' +import { lazy, memo, useCallback, useEffect, useMemo, useState } from 'react' import toast from 'react-hot-toast' -import { useRecoilState } from 'recoil' +import { useSetRecoilState } from 'recoil' import { $flowStep, $newFlow } from '../GlobalStates' import EditTagModal from '../components/AllIntegrations/EditTagModal' import IntegrationsTableView from '../components/AllIntegrations/IntegrationsTableView' import TagPickerModal from '../components/AllIntegrations/TagPickerModal' +import { + buildIntegrationsColumns, + initialIntegrationsCols +} from '../components/AllIntegrations/integrationsColumns' +import { + buildTagPickerOptions, + filterIntegrationsByTags, + hasCustomNamesInPicker, + parseTagPickerInput +} from '../components/AllIntegrations/tagUtils' import Loader from '../components/Loaders/Loader' import ConfirmModal from '../components/Utilities/ConfirmModal' -import MenuBtn from '../components/Utilities/MenuBtn' -import SingleToggle2 from '../components/Utilities/SingleToggle2' import SnackMsg from '../components/Utilities/SnackMsg' +import useCompactBreakpoint from '../hooks/useCompactBreakpoint' import useFetch from '../hooks/useFetch' -import bitsFetch from '../Utils/bitsFetch' +import useIntegrationActions from '../hooks/useIntegrationActions' +import useIntegrationTags from '../hooks/useIntegrationTags' +import useTagPickerSubmit from '../hooks/useTagPickerSubmit' import { __ } from '../Utils/i18nwrap' const Welcome = lazy(() => import('./Welcome')) const preloadFlowBuilder = () => import('./FlowBuilder') +const TAG_NAME_LIMIT = 20 +const LOADER_STYLE = { + display: 'flex', + height: '82vh', + justifyContent: 'center', + alignItems: 'center' +} + function AllIntegrations({ isValidUser }) { const { data, isLoading, mutate } = useFetch({ payload: {}, action: 'flow/list', method: 'get' }) - const [integrations, setIntegrations] = useState( - !isLoading && data.success && data?.data?.integrations ? data.data.integrations : [] - ) + + const [integrations, setIntegrations] = useState([]) const [snack, setSnackbar] = useState({ show: false }) const [confMdl, setconfMdl] = useState({ show: false, btnTxt: '' }) - const [, setNewFlow] = useRecoilState($newFlow) - const [, setFlowStep] = useRecoilState($flowStep) + const setNewFlow = useSetRecoilState($newFlow) + const setFlowStep = useSetRecoilState($flowStep) + + const { tags, integrationTags, setIntegrationTags, persistTagData } = useIntegrationTags() - const [tags, setTags] = useState([]) - const [integrationTags, setIntegrationTags] = useState({}) const [selectedTags, setSelectedTags] = useState([]) const [showTagPickerModal, setShowTagPickerModal] = useState(false) const [tagPickerInput, setTagPickerInput] = useState('') @@ -42,356 +57,126 @@ function AllIntegrations({ isValidUser }) { const [editingIntegrationId, setEditingIntegrationId] = useState(null) const [bulkTagIntegrationIds, setBulkTagIntegrationIds] = useState([]) const [tagToDelete, setTagToDelete] = useState(null) - const [isCompactTagColumn, setIsCompactTagColumn] = useState( - typeof window !== 'undefined' ? window.innerWidth <= 1100 : false - ) + + const isCompactTagColumn = useCompactBreakpoint(1100) useEffect(() => { setFlowStep(1) setNewFlow({}) - }, []) - - const fetchTagData = useCallback( - (showErrorMsg = true) => - bitsFetch({}, 'integration-tags/get', null, 'GET') - .then(res => { - if (!res?.success) { - throw new Error('tag_load_failed') - } - - const fetchedTags = Array.isArray(res?.data?.tags) ? res.data.tags : [] - const fetchedIntegrationTags = - res?.data?.integrationTags && typeof res.data.integrationTags === 'object' - ? res.data.integrationTags - : {} - - setTags(fetchedTags) - setIntegrationTags(fetchedIntegrationTags) - }) - .catch(() => { - if (showErrorMsg) { - toast.error(__('Failed to load tags', 'bit-integrations')) - } - }), - [] - ) - - useEffect(() => { - fetchTagData() - }, [fetchTagData]) + }, [setFlowStep, setNewFlow]) useEffect(() => { - const updateTagColumnMode = () => { - setIsCompactTagColumn(window.innerWidth <= 1100) - } - - updateTagColumnMode() - window.addEventListener('resize', updateTagColumnMode) + if (!isLoading) setIntegrations(data?.success ? data.data.integrations : []) + }, [data, isLoading]) + + const { handleStatus, handleDelete, handleClone, setBulkDelete } = useIntegrationActions({ + integrations, + setIntegrations, + mutate, + integrationTags, + setIntegrationTags, + tags, + persistTagData + }) - return () => { - window.removeEventListener('resize', updateTagColumnMode) - } + const closeConfMdl = useCallback(() => { + setconfMdl(prev => ({ ...prev, show: false })) }, []) - const persistTagData = useCallback( - (nextTags, nextIntegrationTags, successMsg = '') => - bitsFetch( - { - tags: nextTags, - integrationTags: nextIntegrationTags + const showDelModal = useCallback( + (id, index) => { + setconfMdl({ + show: true, + action: () => { + handleDelete(id, index) + closeConfMdl() }, - 'integration-tags/save' - ) - .then(res => { - if (!res?.success) { - throw new Error('tag_save_failed') - } - - const savedTags = Array.isArray(res?.data?.tags) ? res.data.tags : nextTags - const savedIntegrationTags = - res?.data?.integrationTags && typeof res.data.integrationTags === 'object' - ? res.data.integrationTags - : nextIntegrationTags - - setTags(savedTags) - setIntegrationTags(savedIntegrationTags) - - if (successMsg) { - toast.success(successMsg) - } - }) - .catch(() => { - fetchTagData(false) - toast.error(__('Failed to save tags', 'bit-integrations')) - throw new Error('tag_save_failed') - }), - [fetchTagData] + btnTxt: __('Delete', 'bit-integrations'), + btn2Txt: null, + btnClass: '', + body: __('Are you sure to delete this Integration?', 'bit-integrations') + }) + }, + [handleDelete, closeConfMdl] ) - const [cols, setCols] = useState([ - { - width: 250, - minWidth: 80, - Header: __('Trigger', 'bit-integrations'), - accessor: 'triggered_entity' + const showDupMdl = useCallback( + formID => { + setconfMdl({ + show: true, + action: () => { + handleClone(formID) + closeConfMdl() + }, + btnTxt: __('Clone', 'bit-integration'), + btn2Txt: null, + btnClass: 'purple', + body: __('Are you sure to clone this Integration ?', 'bitform') + }) }, - { width: 250, minWidth: 80, Header: __('Action Name', 'bit-integrations'), accessor: 'name' }, - { - width: 200, - minWidth: 200, - Header: __('Created At', 'bit-integrations'), - accessor: 'created_at' - } - ]) - - useEffect(() => { - !isLoading && setIntegrations(data.success ? data.data.integrations : []) - }, [data]) - - useEffect(() => { - const ncols = cols.filter( - itm => itm.accessor !== 't_action' && itm.accessor !== 'status' && itm.accessor !== 'tags' - ) - - ncols.push({ - width: isCompactTagColumn ? 170 : 220, - minWidth: isCompactTagColumn ? 140 : 180, - Header: __('Tags', 'bit-integrations'), - accessor: 'tags', - className: 'table-tags-cell', - Cell: value => { - const integrationId = String(value.row.original.id) - const assignedTagIds = integrationTags[integrationId] || [] - const assignedTags = assignedTagIds - .map(tagId => tags.find(currentTag => String(currentTag.id) === String(tagId))) - .filter(Boolean) - const visibleAssignedTags = assignedTags.slice(0, isCompactTagColumn ? 1 : 2) - const hiddenAssignedTagsCount = Math.max(assignedTags.length - visibleAssignedTags.length, 0) - - return ( -
- {visibleAssignedTags.map(tag => ( - - {tag.name} - - - ))} - {hiddenAssignedTagsCount > 0 && ( - - +{hiddenAssignedTagsCount} - - )} - -
- ) - } - }) + [handleClone, closeConfMdl] + ) - ncols.push({ - width: 70, - minWidth: 60, - Header: __('Status', 'bit-integrations'), - accessor: 'status', - Cell: value => ( - handleStatus(e, value.row.original.id)} - checked={Number(value.row.original.status) === 1} - /> - ) - }) - ncols.push({ - sticky: 'right', - width: 64, - minWidth: 52, - Header: '', - accessor: 't_action', - Cell: val => ( -
- showDelModal(val.row.original.id, val.row.index)} - dup={() => showDupMdl(val.row.original.id, val.row.index)} - /> -
- ) - }) - setCols([...ncols]) - }, [integrations, tags, integrationTags, isCompactTagColumn]) - - const handleStatus = (e, id) => { - const status = e.target.checked - const tmp = [...integrations] - const integ = tmp.find(int => int.id === id) - integ.status = status === true ? '1' : '0' - setIntegrations(tmp) - - const param = { id, status } - bitsFetch(param, 'flow/toggleStatus') - .then(res => { - toast.success(__(res.data, 'bit-integrations')) - }) - .catch(() => { - toast.error(__('Something went wrong', 'bit-integrations')) - }) - } - - const handleDelete = (id, index) => { - const tmpIntegrations = [...integrations] - const deleteLoad = bitsFetch({ id }, 'flow/delete').then(response => { - if (response.success) { - tmpIntegrations.splice(index, 1) - mutate(tmpIntegrations) - setIntegrations(tmpIntegrations) - - const integrationKey = String(id) - if (integrationTags[integrationKey]) { - const updatedMapping = { ...integrationTags } - delete updatedMapping[integrationKey] - setIntegrationTags(updatedMapping) - persistTagData(tags, updatedMapping).catch(() => { }) - } - - return __('Integration deleted successfully', 'bit-integrations') - } - return response.data - }) + const closeTagPickerModal = useCallback(() => { + setShowTagPickerModal(false) + setTagPickerInput('') + setEditingIntegrationId(null) + setBulkTagIntegrationIds([]) + }, []) - toast.promise(deleteLoad, { - success: msg => msg, - error: __('Error Occurred', 'bit-integrations'), - loading: __('delete...') - }) - } - - const handleClone = id => { - const loadClone = bitsFetch({ id }, 'flow/clone').then(response => { - if (response.success) { - const newInteg = response.data - const tmpIntegrations = [...integrations] - const exist = tmpIntegrations.find(item => item.id === id) - const cpyInteg = { - id: newInteg.id, - name: `duplicate of ${exist.name}`, - triggered_entity: exist.triggered_entity, - status: exist.status, - created_at: newInteg.created_at - } - tmpIntegrations.push(cpyInteg) - setIntegrations(tmpIntegrations) - return __('Integration clone successfully', 'bit-integrations') - } - return response.data - }) + const openTagPickerModal = useCallback(() => { + setEditingIntegrationId(null) + setBulkTagIntegrationIds([]) + setTagPickerInput('') + setShowTagPickerModal(true) + }, []) - toast.promise(loadClone, { - success: msg => msg, - error: __('Error Occurred', 'bit-integrations'), - loading: __('cloning...') - }) - } - - const setBulkDelete = useCallback( - rows => { - const rowID = [] - const flowID = [] - for (let i = 0; i < rows.length; i += 1) { - rowID.push(rows[i].id) - flowID.push(rows[i].original.id) - } - const bulkDeleteLoading = bitsFetch({ flowID }, 'flow/bulk-delete').then(response => { - if (response.success) { - const newData = [...integrations] - for (let i = rowID.length - 1; i >= 0; i -= 1) { - newData.splice(Number(rowID[i]), 1) - } - setIntegrations(newData) - - const updatedMapping = { ...integrationTags } - let isMappingUpdated = false - flowID.forEach(deletedIntegId => { - const integrationKey = String(deletedIntegId) - if (updatedMapping[integrationKey]) { - delete updatedMapping[integrationKey] - isMappingUpdated = true - } - }) - - if (isMappingUpdated) { - setIntegrationTags(updatedMapping) - persistTagData(tags, updatedMapping).catch(() => { }) - } - - return __('Integration Deleted Successfully', 'bit-integrations') - } - return response.data - }) + const openTagPickerForIntegration = useCallback((integrationId, prefillInput) => { + setEditingIntegrationId(integrationId) + setBulkTagIntegrationIds([]) + setTagPickerInput(prefillInput || '') + setShowTagPickerModal(true) + }, []) - toast.promise(bulkDeleteLoading, { - success: msg => msg, - error: __('Error Occurred', 'bit-integrations'), - loading: __('delete...') - }) - // eslint-disable-next-line react-hooks/exhaustive-deps + const removeTagFromIntegration = useCallback( + (integrationId, tagId) => { + const integrationKey = String(integrationId) + const updatedMapping = { ...integrationTags } + const currentTags = updatedMapping[integrationKey] || [] + const nextTags = currentTags.filter(c => String(c) !== String(tagId)) + + if (nextTags.length) updatedMapping[integrationKey] = nextTags + else delete updatedMapping[integrationKey] + + setIntegrationTags(updatedMapping) + persistTagData( + tags, + updatedMapping, + __('Tag removed successfully', 'bit-integrations') + ).catch(() => {}) }, - [integrations, integrationTags, tags, persistTagData] + [integrationTags, setIntegrationTags, persistTagData, tags] ) - const setTableCols = useCallback(newCols => { - setCols(newCols) - }, []) - const setBulkTagAssign = useCallback(rows => { - const selectedIntegrationIds = [] - const selectedIntegrationIdsSet = new Set() + const seen = new Set() + const ids = [] rows.forEach(row => { const integrationId = row?.original?.id - if (integrationId === undefined || integrationId === null) { - return - } - - const integrationIdKey = String(integrationId) - if (selectedIntegrationIdsSet.has(integrationIdKey)) { - return - } - - selectedIntegrationIdsSet.add(integrationIdKey) - selectedIntegrationIds.push(integrationId) + if (integrationId === undefined || integrationId === null) return + const key = String(integrationId) + if (seen.has(key)) return + seen.add(key) + ids.push(integrationId) }) - if (!selectedIntegrationIds.length) { + if (!ids.length) { toast.error(__('Please select at least one integration', 'bit-integrations')) return } - setBulkTagIntegrationIds(selectedIntegrationIds) + setBulkTagIntegrationIds(ids) setEditingIntegrationId(null) setTagPickerInput('') setShowTagPickerModal(true) @@ -401,290 +186,99 @@ function AllIntegrations({ isValidUser }) { void preloadFlowBuilder() }, []) - const closeConfMdl = () => { - confMdl.show = false - setconfMdl({ ...confMdl }) - } + const [cols, setCols] = useState(initialIntegrationsCols) - const showDelModal = (id, index) => { - confMdl.action = () => { - handleDelete(id, index) - closeConfMdl() - } - confMdl.btnTxt = __('Delete', 'bit-integrations') - confMdl.btn2Txt = null - confMdl.btnClass = '' - confMdl.body = __('Are you sure to delete this Integration?', 'bit-integrations') - confMdl.show = true - setconfMdl({ ...confMdl }) - } - - const showDupMdl = formID => { - confMdl.action = () => { - handleClone(formID) - closeConfMdl() - } - confMdl.btnTxt = __('Clone', 'bit-integration') - confMdl.btn2Txt = null - confMdl.btnClass = 'purple' - confMdl.body = __('Are you sure to clone this Integration ?', 'bitform') - confMdl.show = true - setconfMdl({ ...confMdl }) - } - - const closeTagPickerModal = () => { - setShowTagPickerModal(false) - setTagPickerInput('') - setEditingIntegrationId(null) - setBulkTagIntegrationIds([]) - } - - const openTagPickerModal = useCallback(() => { - setEditingIntegrationId(null) - setBulkTagIntegrationIds([]) - setTagPickerInput('') - setShowTagPickerModal(true) - }, []) - - const saveTagFromPicker = () => { - const normalizedTagNames = [] - const seenTagNames = new Set() - - tagPickerInput - .split(',') - .map(tagName => tagName.trim().replace(/\s+/g, ' ')) - .filter(Boolean) - .forEach(tagName => { - const normalizedNameKey = tagName.toLowerCase() - if (seenTagNames.has(normalizedNameKey)) { - return - } - seenTagNames.add(normalizedNameKey) - normalizedTagNames.push(tagName) + useEffect(() => { + setCols(prevCols => + buildIntegrationsColumns({ + prevCols, + isCompactTagColumn, + tags, + integrationTags, + isValidUser, + onRemoveTag: removeTagFromIntegration, + onOpenTagPicker: openTagPickerForIntegration, + onToggleStatus: handleStatus, + onShowDelModal: showDelModal, + onShowDupMdl: showDupMdl }) + ) + }, [ + isCompactTagColumn, + tags, + integrationTags, + isValidUser, + removeTagFromIntegration, + openTagPickerForIntegration, + handleStatus, + showDelModal, + showDupMdl + ]) - if (!normalizedTagNames.length && !editingIntegrationId) { - toast.error(__('Please select or create at least one tag', 'bit-integrations')) - return Promise.resolve(false) - } - - const overLimitTag = normalizedTagNames.find(tagName => tagName.length > 20) - if (overLimitTag) { - toast.error(__('Tag name must be 20 characters or less', 'bit-integrations')) - return Promise.resolve(false) - } - - const updatedTags = [...tags] - const resolvedTagIds = [] - let createdTagCount = 0 - - normalizedTagNames.forEach((tagName, index) => { - const existingTag = updatedTags.find( - tag => tag.name.trim().toLowerCase() === tagName.toLowerCase() - ) - - if (existingTag) { - resolvedTagIds.push(existingTag.id) - return - } - - const newTag = { - id: `${Date.now()}-${index}`, - name: tagName, - color: '#6f42c1' - } - - updatedTags.push(newTag) - resolvedTagIds.push(newTag.id) - createdTagCount += 1 - }) - - const uniqueResolvedTagIds = [] - const seenTagIds = new Set() - resolvedTagIds.forEach(tagId => { - const tagIdKey = String(tagId) - if (!seenTagIds.has(tagIdKey)) { - seenTagIds.add(tagIdKey) - uniqueResolvedTagIds.push(tagId) - } - }) - - if (editingIntegrationId) { - const integrationKey = String(editingIntegrationId) - const updatedMapping = { ...integrationTags } - const currentTagIds = (updatedMapping[integrationKey] || []).map(tagId => String(tagId)) - const nextTagIds = uniqueResolvedTagIds.map(tagId => String(tagId)) - const isAssignmentChanged = - currentTagIds.length !== nextTagIds.length || - currentTagIds.some(tagId => !nextTagIds.includes(tagId)) - - if (!isAssignmentChanged && createdTagCount === 0) { - toast.success(__('No changes found', 'bit-integrations')) - closeTagPickerModal() - return Promise.resolve(true) - } - - if (uniqueResolvedTagIds.length > 0) { - updatedMapping[integrationKey] = uniqueResolvedTagIds - } else { - delete updatedMapping[integrationKey] - } - - const successMessage = - createdTagCount > 0 - ? __('Tags created and assigned successfully', 'bit-integrations') - : __('Tags assigned successfully', 'bit-integrations') - - return persistTagData(updatedTags, updatedMapping, successMessage) - .then(() => { - closeTagPickerModal() - return true - }) - .catch(() => false) - } + const saveTagFromPicker = useTagPickerSubmit({ + tags, + integrationTags, + persistTagData, + editingIntegrationId, + bulkTagIntegrationIds, + tagPickerInput, + setSelectedTags, + closeTagPickerModal + }) - if (bulkTagIntegrationIds.length > 0) { + const deleteTag = useCallback( + tagId => { + const updatedTags = tags.filter(tag => String(tag.id) !== String(tagId)) const updatedMapping = { ...integrationTags } - let hasAssignmentChange = false - - bulkTagIntegrationIds.forEach(integrationId => { - const integrationKey = String(integrationId) - const currentTagIds = updatedMapping[integrationKey] || [] - const mergedTagIds = [...currentTagIds] - - uniqueResolvedTagIds.forEach(tagId => { - if (!mergedTagIds.some(currentTagId => String(currentTagId) === String(tagId))) { - mergedTagIds.push(tagId) - } - }) - - if ( - mergedTagIds.length !== currentTagIds.length || - mergedTagIds.some( - mergedTagId => - !currentTagIds.some(currentTagId => String(currentTagId) === String(mergedTagId)) - ) - ) { - hasAssignmentChange = true - } - - if (mergedTagIds.length > 0) { - updatedMapping[integrationKey] = mergedTagIds - } - }) - - if (!hasAssignmentChange && createdTagCount === 0) { - toast.success(__('No changes found', 'bit-integrations')) - closeTagPickerModal() - return Promise.resolve(true) - } - - const successMessage = - createdTagCount > 0 - ? __('Tags created and assigned successfully', 'bit-integrations') - : __('Tags assigned successfully', 'bit-integrations') - - return persistTagData(updatedTags, updatedMapping, successMessage) - .then(() => { - closeTagPickerModal() - return true - }) - .catch(() => false) - } - if (createdTagCount === 0) { - setSelectedTags(prevSelectedTags => { - const nextSelectedTags = [...prevSelectedTags] - uniqueResolvedTagIds.forEach(tagId => { - if (!nextSelectedTags.some(selectedTagId => String(selectedTagId) === String(tagId))) { - nextSelectedTags.push(tagId) - } - }) - return nextSelectedTags + Object.keys(updatedMapping).forEach(integrationId => { + const remaining = updatedMapping[integrationId].filter(c => c !== tagId) + if (remaining.length) updatedMapping[integrationId] = remaining + else delete updatedMapping[integrationId] }) - toast.success(__('Tag selected successfully', 'bit-integrations')) - closeTagPickerModal() - return Promise.resolve(true) - } - return persistTagData( - updatedTags, - integrationTags, - __('Tags created successfully', 'bit-integrations') - ) - .then(() => { - setSelectedTags(prevSelectedTags => { - const nextSelectedTags = [...prevSelectedTags] - uniqueResolvedTagIds.forEach(tagId => { - if (!nextSelectedTags.some(selectedTagId => String(selectedTagId) === String(tagId))) { - nextSelectedTags.push(tagId) - } - }) - return nextSelectedTags - }) - - closeTagPickerModal() - return true - }) - .catch(() => false) - } - - const deleteTag = tagId => { - const updatedTags = tags.filter(tag => String(tag.id) !== String(tagId)) - const updatedMapping = { ...integrationTags } - - Object.keys(updatedMapping).forEach(integrationId => { - const tagIds = updatedMapping[integrationId].filter(currentTagId => currentTagId !== tagId) - if (tagIds.length) { - updatedMapping[integrationId] = tagIds - } else { - delete updatedMapping[integrationId] - } - }) + setSelectedTags(prev => prev.filter(s => s !== tagId)) + setTagToDelete(null) - setSelectedTags(prev => prev.filter(selectedTagId => selectedTagId !== tagId)) - setTagToDelete(null) - - persistTagData( - updatedTags, - updatedMapping, - __('Tag deleted successfully', 'bit-integrations') - ).catch(() => { }) - } + persistTagData( + updatedTags, + updatedMapping, + __('Tag deleted successfully', 'bit-integrations') + ).catch(() => {}) + }, + [tags, integrationTags, persistTagData] + ) - const confirmDeleteTag = tagId => { - setTagToDelete(tagId) - } + const confirmDeleteTag = useCallback(tagId => setTagToDelete(tagId), []) - const openEditTagModal = tag => { + const openEditTagModal = useCallback(tag => { setTagToEdit(tag.id) setEditTagName(tag.name) setShowEditTagModal(true) - } + }, []) - const closeEditTagModal = () => { + const closeEditTagModal = useCallback(() => { setShowEditTagModal(false) setTagToEdit(null) setEditTagName('') - } + }, []) - const updateTag = () => { - const trimmedTagName = editTagName.trim() + const updateTag = useCallback(() => { + const trimmed = editTagName.trim() - if (!trimmedTagName) { + if (!trimmed) { toast.error(__('Please enter a tag name', 'bit-integrations')) return Promise.resolve(false) } - - if (trimmedTagName.length > 20) { + if (trimmed.length > TAG_NAME_LIMIT) { toast.error(__('Tag name must be 20 characters or less', 'bit-integrations')) return Promise.resolve(false) } + const lower = trimmed.toLowerCase() if ( tags.some( - tag => - String(tag.id) !== String(tagToEdit) && tag.name.toLowerCase() === trimmedTagName.toLowerCase() + tag => String(tag.id) !== String(tagToEdit) && tag.name.toLowerCase() === lower ) ) { toast.error(__('Tag already exists', 'bit-integrations')) @@ -692,7 +286,7 @@ function AllIntegrations({ isValidUser }) { } const updatedTags = tags.map(tag => - String(tag.id) === String(tagToEdit) ? { ...tag, name: trimmedTagName } : tag + String(tag.id) === String(tagToEdit) ? { ...tag, name: trimmed } : tag ) return persistTagData( @@ -705,110 +299,46 @@ function AllIntegrations({ isValidUser }) { return true }) .catch(() => false) - } - - const removeTagFromIntegration = (integrationId, tagId) => { - const integrationKey = String(integrationId) - const updatedMapping = { ...integrationTags } - const currentTags = updatedMapping[integrationKey] || [] - const nextTags = currentTags.filter(currentTagId => String(currentTagId) !== String(tagId)) - - if (nextTags.length > 0) { - updatedMapping[integrationKey] = nextTags - } else { - delete updatedMapping[integrationKey] - } + }, [editTagName, tags, tagToEdit, integrationTags, persistTagData, closeEditTagModal]) - setIntegrationTags(updatedMapping) - persistTagData(tags, updatedMapping, __('Tag removed successfully', 'bit-integrations')).catch( - () => { } - ) - } - - const toggleTagFilter = tagId => { + const toggleTagFilter = useCallback(tagId => { if (tagId === 'ALL') { setSelectedTags([]) return } + setSelectedTags(prev => + prev.includes(tagId) ? prev.filter(id => id !== tagId) : [...prev, tagId] + ) + }, []) - if (selectedTags.includes(tagId)) { - setSelectedTags(selectedTags.filter(id => id !== tagId)) - } else { - setSelectedTags([...selectedTags, tagId]) - } - } + const clearTagFilters = useCallback(() => setSelectedTags([]), []) - const clearTagFilters = () => { - setSelectedTags([]) - } + const filteredIntegrations = useMemo( + () => filterIntegrationsByTags(integrations, integrationTags, selectedTags), + [integrations, integrationTags, selectedTags] + ) - const filteredIntegrations = - selectedTags.length > 0 - ? integrations.filter(integration => { - const assignedTagIds = integrationTags[String(integration.id)] || [] - return selectedTags.some(tagId => assignedTagIds.includes(tagId)) - }) - : integrations - - const selectedTagNamesFromPicker = [] - const selectedTagNamesSet = new Set() - tagPickerInput - .split(',') - .map(tagName => tagName.trim().replace(/\s+/g, ' ')) - .filter(Boolean) - .forEach(tagName => { - const normalizedTagName = tagName.toLowerCase() - if (!selectedTagNamesSet.has(normalizedTagName)) { - selectedTagNamesSet.add(normalizedTagName) - selectedTagNamesFromPicker.push(tagName) - } - }) + const tagPickerOptions = useMemo(() => buildTagPickerOptions(tags), [tags]) - const hasCustomTagInPicker = selectedTagNamesFromPicker.some( - tagName => !tags.some(tag => tag.name.trim().toLowerCase() === tagName.toLowerCase()) - ) + const tagPickerPrimaryBtnLabel = useMemo(() => { + const pickerNames = parseTagPickerInput(tagPickerInput) + const hasCustom = hasCustomNamesInPicker(pickerNames, tags) - const tagPickerOptions = [] - const tagOptionNames = new Set() - tags.forEach(tag => { - const tagName = tag?.name?.trim() - if (!tagName) { - return + if (bulkTagIntegrationIds.length > 0) { + return hasCustom + ? __('Create & Assign', 'bit-integrations') + : __('Assign Tags', 'bit-integrations') } - const tagNameKey = tagName.toLowerCase() - if (tagOptionNames.has(tagNameKey)) { - return + if (editingIntegrationId) { + return hasCustom + ? __('Create & Assign', 'bit-integrations') + : __('Save Tags', 'bit-integrations') } - tagOptionNames.add(tagNameKey) - tagPickerOptions.push({ - label: tagName, - value: tagName - }) - }) + if (hasCustom) return __('Create Tag', 'bit-integrations') + return __('Select Tag', 'bit-integrations') + }, [tagPickerInput, tags, bulkTagIntegrationIds, editingIntegrationId]) - let tagPickerPrimaryBtnLabel = __('Select Tag', 'bit-integrations') - if (bulkTagIntegrationIds.length > 0) { - tagPickerPrimaryBtnLabel = hasCustomTagInPicker - ? __('Create & Assign', 'bit-integrations') - : __('Assign Tags', 'bit-integrations') - } else if (editingIntegrationId) { - tagPickerPrimaryBtnLabel = hasCustomTagInPicker - ? __('Create & Assign', 'bit-integrations') - : __('Save Tags', 'bit-integrations') - } else if (hasCustomTagInPicker) { - tagPickerPrimaryBtnLabel = __('Create Tag', 'bit-integrations') - } - - const loaderStyle = { - display: 'flex', - height: '82vh', - justifyContent: 'center', - alignItems: 'center' - } - - if (isLoading) { - return - } + if (isLoading) return return (
@@ -858,11 +388,11 @@ function AllIntegrations({ isValidUser }) { onSubmit={updateTag} /> - {integrations && integrations?.length ? ( + {integrations?.length ? ( Date: Tue, 28 Apr 2026 12:42:43 +0600 Subject: [PATCH 04/18] feat: enhance subscriber addition by including actions and improving error handling --- backend/Actions/MailPoet/RecordApiHelper.php | 47 +++++++++++++++----- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/backend/Actions/MailPoet/RecordApiHelper.php b/backend/Actions/MailPoet/RecordApiHelper.php index 9611b336c..0003b3523 100644 --- a/backend/Actions/MailPoet/RecordApiHelper.php +++ b/backend/Actions/MailPoet/RecordApiHelper.php @@ -38,7 +38,7 @@ public function insertRecord($subscriber, $lists, $actions) $existingSubscriber = static::$mailPoet_api->getSubscriber($subscriber['email']); if (!$existingSubscriber) { - return static::addSubscriber($subscriber, $lists); + return static::addSubscriber($subscriber, $lists, $actions); } if (!empty($actions->update)) { @@ -54,19 +54,25 @@ public function insertRecord($subscriber, $lists, $actions) // translators: %s: Plugin name $errorMessages = wp_sprintf(__('%s is not active or not installed', 'bit-integrations'), 'Bit Integrations Pro'); } elseif (!$response['success']) { - $errorMessages = $response('message'); + $errorMessages = $response['message']; } if (isset($errorMessages)) { - LogHandler::save($this->_integrationID, ['type' => 'record', 'type_name' => 'update'], 'error', $errorMessages); + return [ + 'success' => false, + 'message' => $errorMessages, + ]; } - } - return static::addSubscribeToLists($existingSubscriber['id'], $lists); + $newLists = static::getFilteredList($lists, $existingSubscriber['subscriptions']); + if (!empty($newLists)) { + return static::addSubscribeToLists($existingSubscriber['id'], $newLists); + } + } } catch (\MailPoet\API\MP\v1\APIException $e) { if ($e->getCode() == 4) { // Handle the case where the subscriber doesn't exist - return static::addSubscriber($subscriber, $lists); + return static::addSubscriber($subscriber, $lists, $actions); } return [ @@ -121,14 +127,23 @@ private static function setFieldMap($fieldMap, $fieldValues) return $fieldData; } - private static function addSubscriber($subscriber, $lists) + private static function addSubscriber($subscriber, $lists, $actions) { try { - $subscriber = static::$mailPoet_api->addSubscriber($subscriber, $lists); + $options = []; + if (isset($actions->send_confirmation_email)) { + $options['send_confirmation_email'] = (bool) $actions->send_confirmation_email; + } + + $subscriber = static::$mailPoet_api->addSubscriber($subscriber, $lists, $options); + + if (isset($subscriber['id']) && !empty($lists)) { + return static::addSubscribeToLists($subscriber['id'], $lists); + } return [ - 'success' => true, - 'id' => $subscriber['id'], + 'success' => false, + 'data' => $subscriber, ]; } catch (\MailPoet\API\MP\v1\APIException $e) { return [ @@ -156,4 +171,16 @@ private static function addSubscribeToLists($subscriber_id, $lists) ]; } } + + private static function getFilteredList($listIds, $subscriptions) + { + $segmentIds = array_column($subscriptions, 'segment_id'); + + return array_filter( + $listIds, + function ($listId) use ($segmentIds) { + return !\in_array($listId, $segmentIds); + } + ); + } } From 83aed088ffb9fdc98a2ac632702f3e6a29f55191 Mon Sep 17 00:00:00 2001 From: Rishad Alam <101513331+RishadAlam@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:07:50 +0600 Subject: [PATCH 05/18] feat: enhance order creation by adding line item subtotal and tax details --- backend/Actions/WooCommerce/RecordApiHelper.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/backend/Actions/WooCommerce/RecordApiHelper.php b/backend/Actions/WooCommerce/RecordApiHelper.php index bdfab5fc7..34bacf78a 100644 --- a/backend/Actions/WooCommerce/RecordApiHelper.php +++ b/backend/Actions/WooCommerce/RecordApiHelper.php @@ -749,7 +749,19 @@ private function product_added_to_order($fieldDataLine, $module, $customer_id) $order = wc_create_order(['customer_id' => $customer_id]); $product = wc_get_product($product_id); - $order->add_product($product, (int) $lineItem->quantity); + + $lineArgs = []; + if (isset($lineItem->subtotal) && $lineItem->subtotal !== '') { + $lineArgs['subtotal'] = (float) $lineItem->subtotal; + } + if (isset($lineItem->total) && $lineItem->total !== '') { + $lineArgs['total'] = (float) $lineItem->total; + } + if (isset($lineItem->line_subtotal_tax) && $lineItem->line_subtotal_tax !== '') { + $lineArgs['subtotal_tax'] = (float) $lineItem->line_subtotal_tax; + } + + $order->add_product($product, (int) $lineItem->quantity, $lineArgs); return $order; } From ffe458046730e30dbb6969369f0fd43ad8f0497f Mon Sep 17 00:00:00 2001 From: Rishad Alam <101513331+RishadAlam@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:46:09 +0600 Subject: [PATCH 06/18] feat: enhance insertRecord and addSubscriber methods to support options parameter --- backend/Actions/MailPoet/RecordApiHelper.php | 25 ++++++++------------ 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/backend/Actions/MailPoet/RecordApiHelper.php b/backend/Actions/MailPoet/RecordApiHelper.php index 0003b3523..e24a41dbb 100644 --- a/backend/Actions/MailPoet/RecordApiHelper.php +++ b/backend/Actions/MailPoet/RecordApiHelper.php @@ -31,14 +31,14 @@ public function __construct($integId) static::$mailPoet_api = \MailPoet\API\API::MP('v1'); } - public function insertRecord($subscriber, $lists, $actions) + public function insertRecord($subscriber, $lists, $actions, $options = []) { try { // try to find if user is already a subscriber $existingSubscriber = static::$mailPoet_api->getSubscriber($subscriber['email']); if (!$existingSubscriber) { - return static::addSubscriber($subscriber, $lists, $actions); + return static::addSubscriber($subscriber, $lists, $options); } if (!empty($actions->update)) { @@ -66,13 +66,13 @@ public function insertRecord($subscriber, $lists, $actions) $newLists = static::getFilteredList($lists, $existingSubscriber['subscriptions']); if (!empty($newLists)) { - return static::addSubscribeToLists($existingSubscriber['id'], $newLists); + return static::addSubscribeToLists($existingSubscriber['id'], $newLists, $options); } } } catch (\MailPoet\API\MP\v1\APIException $e) { if ($e->getCode() == 4) { // Handle the case where the subscriber doesn't exist - return static::addSubscriber($subscriber, $lists, $actions); + return static::addSubscriber($subscriber, $lists, $options); } return [ @@ -97,9 +97,9 @@ public function execute($fieldValues, $fieldMap, $lists, $actions) } $fieldData = static::setFieldMap($fieldMap, $fieldValues); - $fieldMap['send_confirmation_email'] = isset($actions->send_confirmation_email) ? (bool) $actions->send_confirmation_email : false; + $options = ['send_confirmation_email' => isset($actions->send_confirmation_email)]; - $recordApiResponse = $this->insertRecord($fieldData, $lists, $actions); + $recordApiResponse = $this->insertRecord($fieldData, $lists, $actions, $options); if ($recordApiResponse['success']) { LogHandler::save($this->_integrationID, ['type' => 'record', 'type_name' => 'insert'], 'success', $recordApiResponse); @@ -127,18 +127,13 @@ private static function setFieldMap($fieldMap, $fieldValues) return $fieldData; } - private static function addSubscriber($subscriber, $lists, $actions) + private static function addSubscriber($subscriber, $lists, $options = []) { try { - $options = []; - if (isset($actions->send_confirmation_email)) { - $options['send_confirmation_email'] = (bool) $actions->send_confirmation_email; - } - $subscriber = static::$mailPoet_api->addSubscriber($subscriber, $lists, $options); if (isset($subscriber['id']) && !empty($lists)) { - return static::addSubscribeToLists($subscriber['id'], $lists); + return static::addSubscribeToLists($subscriber['id'], $lists, $options); } return [ @@ -154,10 +149,10 @@ private static function addSubscriber($subscriber, $lists, $actions) } } - private static function addSubscribeToLists($subscriber_id, $lists) + private static function addSubscribeToLists($subscriber_id, $lists, $options = []) { try { - $subscriber = static::$mailPoet_api->subscribeToLists($subscriber_id, $lists); + $subscriber = static::$mailPoet_api->subscribeToLists($subscriber_id, $lists, $options); return [ 'success' => true, From 4fa2084fa0c6c89940f624c6895355852265a82c Mon Sep 17 00:00:00 2001 From: Rishad Alam <101513331+RishadAlam@users.noreply.github.com> Date: Wed, 29 Apr 2026 13:24:06 +0600 Subject: [PATCH 07/18] feat: implement unassign subscriber from group functionality and enhance error handling --- .../MailerLite/MailerLiteController.php | 3 +- .../Actions/MailerLite/RecordApiHelper.php | 48 +++++++++++++++++-- .../MailerLite/EditMailerLite.jsx | 6 +++ .../AllIntegrations/MailerLite/MailerLite.jsx | 14 +++++- .../MailerLite/MailerLiteCommonFunc.js | 4 ++ .../MailerLite/MailerLiteIntegLayout.jsx | 43 ++++++++++++++--- 6 files changed, 106 insertions(+), 12 deletions(-) diff --git a/backend/Actions/MailerLite/MailerLiteController.php b/backend/Actions/MailerLite/MailerLiteController.php index 5f1f815c3..636195a1e 100644 --- a/backend/Actions/MailerLite/MailerLiteController.php +++ b/backend/Actions/MailerLite/MailerLiteController.php @@ -193,7 +193,6 @@ public function execute($integrationData, $fieldValues) $integId = $integrationData->id; $auth_token = $integrationDetails->auth_token; $version = $integrationDetails->version; - $groupIds = $integrationDetails->group_ids ?? ''; $fieldMap = $integrationDetails->field_map ?? ''; $type = $integrationDetails->mailer_lite_type ?? ''; $actions = $integrationDetails->actions ?? ''; @@ -208,7 +207,7 @@ public function execute($integrationData, $fieldValues) } $recordApiHelper = new RecordApiHelper($auth_token, $integrationDetails, $integId, $actions, $version); $mailerliteApiResponse = $recordApiHelper->execute( - $groupIds, + $integrationDetails, $type, $fieldValues, $fieldMap, diff --git a/backend/Actions/MailerLite/RecordApiHelper.php b/backend/Actions/MailerLite/RecordApiHelper.php index d0d41fc11..ae2faeeaf 100644 --- a/backend/Actions/MailerLite/RecordApiHelper.php +++ b/backend/Actions/MailerLite/RecordApiHelper.php @@ -8,8 +8,8 @@ use BitApps\Integrations\Config; use BitApps\Integrations\Core\Util\Common; -use BitApps\Integrations\Core\Util\HttpHelper; use BitApps\Integrations\Core\Util\Hooks; +use BitApps\Integrations\Core\Util\HttpHelper; use BitApps\Integrations\Log\LogHandler; /** @@ -170,6 +170,38 @@ public function deleteSubscriber($auth_token, $finalData, $forget = false) return $response ? $response : (object) ['success' => false, 'message' => __('Bit Integrations Pro is required.', 'bit-integrations'), 'code' => 400]; } + public function unassignSubscriberFromGroup($auth_token, $groupId, $finalData) + { + if (!$this->_isMailerLiteV2) { + return [ + 'success' => false, + 'message' => __('This action is not supported for Classic accounts.', 'bit-integrations'), + 'code' => 400 + ]; + } + + if (empty($finalData['email'])) { + return [ + 'success' => false, + 'message' => __('Required field Email is empty', 'bit-integrations'), + 'code' => 400 + ]; + } + + $subscriberId = $this->existSubscriber($auth_token, $finalData['email']); + if (empty($subscriberId)) { + return [ + 'success' => false, + 'message' => __('Subscriber not exist', 'bit-integrations'), + 'code' => 400 + ]; + } + + $response = Hooks::apply(Config::withPrefix('mailerlite_unassign_subscriber_from_group'), false, $subscriberId, $groupId, $this->_baseUrl, $this->_defaultHeader); + + return $response ? $response : (object) ['success' => false, 'message' => __('Bit Integrations Pro is required.', 'bit-integrations'), 'code' => 400]; + } + public function generateReqDataFromFieldMap($data, $fieldMap) { $dataFinal = []; @@ -188,7 +220,7 @@ public function generateReqDataFromFieldMap($data, $fieldMap) } public function execute( - $groupId, + $integrationDetails, $type, $fieldValues, $fieldMap, @@ -212,7 +244,17 @@ public function execute( break; + case 'unassign_subscriber_from_group': + $groupId = $integrationDetails->selected_group_id ?? ''; + $apiResponse = $this->unassignSubscriberFromGroup($auth_token, $groupId, $finalData); + error_log(print_r(['apiResponse' => $apiResponse], true)); + $typeName = 'unassign-subscriber-from-group'; + $res = ['success' => true, 'message' => __('Subscriber unassigned from group successfully', 'bit-integrations'), 'code' => 200]; + + break; + default: + $groupId = $integrationDetails->group_ids ?? ''; $apiResponse = $this->addSubscriber($auth_token, $groupId, $type, $finalData); $typeName = 'add-subscriber'; $res = ['success' => true, 'message' => isset($apiResponse->update) ? __('Subscriber updated successfully', 'bit-integrations') : __('Subscriber created successfully', 'bit-integrations'), 'code' => 200]; @@ -220,7 +262,7 @@ public function execute( break; } - if (isset($apiResponse->data->id) || isset($apiResponse->id) || str_starts_with((string) HttpHelper::$responseCode, '20')) { + if (isset($apiResponse->data->id) || isset($apiResponse->id) || strpos((string) HttpHelper::$responseCode, '20') === 0) { LogHandler::save($this->_integrationID, wp_json_encode(['type' => 'subscriber', 'type_name' => $typeName]), 'success', wp_json_encode($res)); } else { LogHandler::save($this->_integrationID, wp_json_encode(['type' => 'subscriber', 'type_name' => $typeName]), 'error', wp_json_encode($apiResponse)); diff --git a/frontend/src/components/AllIntegrations/MailerLite/EditMailerLite.jsx b/frontend/src/components/AllIntegrations/MailerLite/EditMailerLite.jsx index 1a981a6d8..1c04745f1 100644 --- a/frontend/src/components/AllIntegrations/MailerLite/EditMailerLite.jsx +++ b/frontend/src/components/AllIntegrations/MailerLite/EditMailerLite.jsx @@ -36,6 +36,12 @@ function EditMailerLite({ allIntegURL }) { setSnackbar({ show: true, msg: __('Please map mandatory fields', 'bit-integrations') }) return } + + if (mailerLiteConf?.action === 'unassign_subscriber_from_group' && !mailerLiteConf?.selected_group_id) { + setSnackbar({ show: true, msg: __('Please select a group', 'bit-integrations') }) + return + } + saveActionConf({ flow, allIntegURL, diff --git a/frontend/src/components/AllIntegrations/MailerLite/MailerLite.jsx b/frontend/src/components/AllIntegrations/MailerLite/MailerLite.jsx index 367f00ac4..1a903397b 100644 --- a/frontend/src/components/AllIntegrations/MailerLite/MailerLite.jsx +++ b/frontend/src/components/AllIntegrations/MailerLite/MailerLite.jsx @@ -40,6 +40,11 @@ function MailerLite({ formFields, setFlow, flow, allIntegURL }) { }) const saveConfig = () => { + if (mailerLiteConf?.action === 'unassign_subscriber_from_group' && !mailerLiteConf?.selected_group_id) { + toast.error(__('Please select a group', 'bit-integrations')) + return + } + setIsLoading(true) const resp = saveIntegConfig( flow, @@ -69,6 +74,10 @@ function MailerLite({ formFields, setFlow, flow, allIntegURL }) { toast.error(__('Please map mandatory fields', 'bit-integrations')) return } + if (mailerLiteConf?.action === 'unassign_subscriber_from_group' && !mailerLiteConf?.selected_group_id) { + toast.error(__('Please select a group', 'bit-integrations')) + return + } mailerLiteConf.field_map.length > 0 && setstep(pageNo) } @@ -108,7 +117,10 @@ function MailerLite({ formFields, setFlow, flow, allIntegURL }) { {mailerLiteConf?.action && ( +
+ )} + + {(loading.field || loading.group) && ( ${__( - 'This action requires a MailerLite New account. It isn’t supported with Classic accounts.', - 'bit-integrations' - )}

+ 'This action requires a MailerLite New account. It isn’t supported with Classic accounts.', + 'bit-integrations' +)}

` From 4d43c7f4e6d684d17ad6b818af0ae63047d8dccd Mon Sep 17 00:00:00 2001 From: Rishad Alam <101513331+RishadAlam@users.noreply.github.com> Date: Wed, 29 Apr 2026 13:29:11 +0600 Subject: [PATCH 08/18] feat: refactor action selection to use MultiSelect component and update handling for unassign action --- .../MailerLite/MailerLiteCommonFunc.js | 4 - .../MailerLite/MailerLiteIntegLayout.jsx | 104 +++++++++++------- 2 files changed, 67 insertions(+), 41 deletions(-) diff --git a/frontend/src/components/AllIntegrations/MailerLite/MailerLiteCommonFunc.js b/frontend/src/components/AllIntegrations/MailerLite/MailerLiteCommonFunc.js index f560411ae..bd1a7d848 100644 --- a/frontend/src/components/AllIntegrations/MailerLite/MailerLiteCommonFunc.js +++ b/frontend/src/components/AllIntegrations/MailerLite/MailerLiteCommonFunc.js @@ -16,10 +16,6 @@ export const handleInput = (e, mailerLiteConf, setMailerLiteConf, loading, setLo if (name === 'action' && value !== '') { mailerliteRefreshFields(updatedConf, setMailerLiteConf, loading, setLoading) } - - if(name === 'action' && value === 'unassign_subscriber_from_group') { - getAllGroups(updatedConf, setMailerLiteConf, loading, setLoading) - } } export const generateMappedField = mailerLiteConf => { diff --git a/frontend/src/components/AllIntegrations/MailerLite/MailerLiteIntegLayout.jsx b/frontend/src/components/AllIntegrations/MailerLite/MailerLiteIntegLayout.jsx index 6533011c9..d22a062e0 100644 --- a/frontend/src/components/AllIntegrations/MailerLite/MailerLiteIntegLayout.jsx +++ b/frontend/src/components/AllIntegrations/MailerLite/MailerLiteIntegLayout.jsx @@ -1,4 +1,5 @@ import { __ } from '../../../Utils/i18nwrap' +import MultiSelect from 'react-multiple-select-dropdown-lite' import Loader from '../../Loaders/Loader' import { addFieldMap } from './IntegrationHelpers' import MailerLiteFieldMap from './MailerLiteFieldMap' @@ -8,9 +9,36 @@ import { useEffect } from 'react' import Note from '../../Utilities/Note' import { useRecoilValue } from 'recoil' import { $appConfigState } from '../../../GlobalStates' -import { getProLabel } from '../../Utilities/ProUtilHelpers' +import { checkIsPro, getProLabel } from '../../Utilities/ProUtilHelpers' import { create } from 'mutative' +const actionOptions = [ + { + value: 'add_subscriber', + label: __('Add Subscriber', 'bit-integrations'), + isPro: false, + isV2Only: false + }, + { + value: 'delete_subscriber', + label: __('Delete subscriber', 'bit-integrations'), + isPro: true, + isV2Only: true + }, + { + value: 'forget_subscriber', + label: __('Forget subscriber', 'bit-integrations'), + isPro: true, + isV2Only: true + }, + { + value: 'unassign_subscriber_from_group', + label: __('Unassign subscriber from a group', 'bit-integrations'), + isPro: true, + isV2Only: true + } +] + export default function MailerLiteIntegLayout({ formFields, handleInput, @@ -23,49 +51,51 @@ export default function MailerLiteIntegLayout({ const btcbi = useRecoilValue($appConfigState) const { isPro } = btcbi + const handleMainAction = value => { + const updatedConf = create(mailerLiteConf, draftConf => { + draftConf.action = value + }) + + setMailerLiteConf(updatedConf) + + if (value !== '') { + mailerliteRefreshFields(updatedConf, setMailerLiteConf, loading, setLoading) + } + + if (value === 'unassign_subscriber_from_group') { + getAllGroups(updatedConf, setMailerLiteConf, loading, setLoading) + } + } + return ( <>
- {__('Select Action:', 'bit-integrations')} - +
+ {__('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) || (action.isV2Only && mailerLiteConf.version === 'v1') + }))} + singleSelect + closeOnSelect + /> +
{mailerLiteConf?.action === 'unassign_subscriber_from_group' && (
{__('Select Group:', 'bit-integrations')} - {mailerLiteConf?.groups?.map(group => (
- - +
+ + - - - + + - - + - +
-

- ⚠️ -

+
+ + + + +
+ Bit Integrations +
-

- -

- - -
-

- -

- - - - - - - - - - - - - - - - - - - - - - -
- - - # -
- - - -
- - - -
- - - -
- - - -
- -
-

- -

-
- -
-
-
- - -
-

- -

- - - - - -
- - - - - - - -
-
+
+ + + + + + +
+ + + + + +
+ + + + + + +
+
!
+
+

+

+ +

+
+
+ FAILED +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ +

+
+

+ +

+ + + + + + + + + + + + + + + + + + + + + +
+ + + # +
+ + + +
+ + + +
+ + + +
+ + + +
+
+

+ +

+
+

+ +

+
+
+
+
+

+ +

+ + + + + + + +
+ + + +
+ + + +
+
-

+

+