Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions backend/Actions/WeDocs/RecordApiHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php

namespace BitApps\Integrations\Actions\WeDocs;

use BitApps\Integrations\Config;
use BitApps\Integrations\Core\Util\Common;
use BitApps\Integrations\Core\Util\Hooks;
use BitApps\Integrations\Log\LogHandler;

/**
* Provide functionality for weDocs record operations.
*/
class RecordApiHelper
{
private $_integrationID;

private $_integrationDetails;

public function __construct($integrationDetails, $integId)
{
$this->_integrationDetails = $integrationDetails;
$this->_integrationID = $integId;
}
Comment on lines +15 to +23
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better code clarity, consistency, and type safety, consider the following improvements:

  1. Add property and parameter types: Specify types for class properties and constructor parameters (available in PHP 7.4+).
  2. Use consistent naming: Rename $_integrationID to $_integrationId and the constructor parameter $integId to $integrationId to follow the camelCase convention used elsewhere.

This change should also be propagated to where this property is used (lines 33 and 78).

    private int $_integrationId;

    private object $_integrationDetails;

    public function __construct(object $integrationDetails, int $integrationId)
    {
        $this->_integrationDetails = $integrationDetails;
        $this->_integrationId = $integrationId;
    }


public function execute($fieldValues, $fieldMap)
{
if (!class_exists('WeDocs')) {
$response = [
'success' => false,
'message' => __('weDocs is not installed or activated', 'bit-integrations'),
];

LogHandler::save($this->_integrationID, ['type' => 'weDocs', 'type_name' => 'check'], 'error', $response);

return $response;
}

$mainAction = $this->_integrationDetails->mainAction ?? '';
$fieldData = $this->generateReqDataFromFieldMap($fieldMap, $fieldValues);
$payload = $this->buildPayload($fieldData);

$defaultResponse = [
'success' => false,
// translators: %s is the plugin name.
'message' => wp_sprintf(__('%s plugin is not installed or activated', 'bit-integrations'), 'Bit Integrations Pro'),
];

switch ($mainAction) {
case 'create_documentation':
$response = Hooks::apply(Config::withPrefix('wedocs_create_documentation'), $defaultResponse, $payload);
$actionType = 'create_documentation';

break;

case 'create_section':
$response = Hooks::apply(Config::withPrefix('wedocs_create_section'), $defaultResponse, $payload);
$actionType = 'create_section';

break;

case 'create_article':
$response = Hooks::apply(Config::withPrefix('wedocs_create_article'), $defaultResponse, $payload);
$actionType = 'create_article';

break;

default:
$response = [
'success' => false,
'message' => __('Invalid action', 'bit-integrations'),
];
$actionType = 'unknown';

break;
}

$responseType = !empty($response['success']) ? 'success' : 'error';
LogHandler::save($this->_integrationID, ['type' => 'weDocs', 'type_name' => $actionType], $responseType, $response);

return $response;
}

private function buildPayload($payload)
{
if (!isset($payload['documentation_id']) && isset($this->_integrationDetails->selectedDocumentationId)) {
$payload['documentation_id'] = $this->_integrationDetails->selectedDocumentationId;
}

if (!isset($payload['section_id']) && isset($this->_integrationDetails->selectedSectionId)) {
$payload['section_id'] = $this->_integrationDetails->selectedSectionId;
Comment on lines +85 to +90
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildPayload() adds documentation_id / section_id whenever the config property exists, even if it’s an empty string (these properties are always present in the frontend config). That can result in payloads containing documentation_id: '' / section_id: '', which downstream handlers may treat as invalid. Only set these keys when the selected IDs are non-empty (and ideally normalize to int via absint).

Suggested change
if (!isset($payload['documentation_id']) && isset($this->_integrationDetails->selectedDocumentationId)) {
$payload['documentation_id'] = $this->_integrationDetails->selectedDocumentationId;
}
if (!isset($payload['section_id']) && isset($this->_integrationDetails->selectedSectionId)) {
$payload['section_id'] = $this->_integrationDetails->selectedSectionId;
$selectedDocumentationId = $this->_integrationDetails->selectedDocumentationId ?? '';
if (!isset($payload['documentation_id']) && '' !== \trim((string) $selectedDocumentationId)) {
$payload['documentation_id'] = absint($selectedDocumentationId);
}
$selectedSectionId = $this->_integrationDetails->selectedSectionId ?? '';
if (!isset($payload['section_id']) && '' !== \trim((string) $selectedSectionId)) {
$payload['section_id'] = absint($selectedSectionId);

Copilot uses AI. Check for mistakes.
}

return $payload;
}

private function generateReqDataFromFieldMap($fieldMap, $fieldValues)
{
$dataFinal = [];

if (!\is_array($fieldMap)) {
return $dataFinal;
}

foreach ($fieldMap as $item) {
$triggerValue = $item->formField ?? '';
$actionValue = $item->weDocsField ?? '';

if (empty($actionValue)) {
continue;
}

$dataFinal[$actionValue] = $triggerValue === 'custom' && isset($item->customValue)
? Common::replaceFieldWithValue($item->customValue, $fieldValues)
: ($fieldValues[$triggerValue] ?? '');
}

return $dataFinal;
}
}
12 changes: 12 additions & 0 deletions backend/Actions/WeDocs/Routes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

if (!defined('ABSPATH')) {
exit;
}

use BitApps\Integrations\Actions\WeDocs\WeDocsController;
use BitApps\Integrations\Core\Util\Route;

Route::post('wedocs_authorize', [WeDocsController::class, 'weDocsAuthorize']);
Route::post('wedocs_get_documentations', [WeDocsController::class, 'getDocumentations']);
Route::post('wedocs_get_sections', [WeDocsController::class, 'getSections']);
145 changes: 145 additions & 0 deletions backend/Actions/WeDocs/WeDocsController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php

namespace BitApps\Integrations\Actions\WeDocs;

use WP_Post;

/**
* Provide functionality for weDocs integration.
*/
class WeDocsController
{
private const DOC_POST_TYPE = 'docs';

private const ALLOWED_POST_STATUSES = ['publish', 'draft', 'pending', 'private'];

public static function weDocsAuthorize()
{
self::checkPluginExists();
wp_send_json_success(true);
}

public static function getDocumentations()
{
self::checkPluginExists();

$allDocumentations = get_posts(
[
'post_type' => self::DOC_POST_TYPE,
'post_status' => self::ALLOWED_POST_STATUSES,
'posts_per_page' => -1,
'post_parent' => 0,
'orderby' => 'title',
'order' => 'ASC',
]
);

$documentations = array_map(
function ($doc) {
return (object) [
'value' => (string) $doc->ID,
'label' => $doc->post_title,
];
},
$allDocumentations
);

wp_send_json_success(['documentations' => $documentations], 200);
}

public static function getSections($request)
{
self::checkPluginExists();

$documentationId = self::sanitizeId($request->documentation_id ?? '');

if ($documentationId > 0) {
$documentation = get_post($documentationId);

if (!self::isValidDocumentation($documentation)) {
wp_send_json_error(__('Selected documentation is invalid.', 'bit-integrations'), 400);
}
}

$queryArgs = [
'post_type' => self::DOC_POST_TYPE,
'post_status' => self::ALLOWED_POST_STATUSES,
'posts_per_page' => -1,
'orderby' => 'title',
'order' => 'ASC',
];

if ($documentationId > 0) {
$queryArgs['post_parent'] = $documentationId;
}

$sections = get_posts($queryArgs);

if ($documentationId <= 0) {
$sections = array_values(array_filter($sections, [__CLASS__, 'isValidSection']));
}
Comment on lines +72 to +80
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation to fetch all sections when no documentation ID is provided can be inefficient. It fetches all 'docs' posts and then filters them in PHP using isValidSection, which in turn makes an additional database query (wp_get_post_parent_id) for each post. This can lead to performance issues if there are many documentation posts.

A more efficient approach is to first fetch all top-level documentation IDs and then use post_parent__in to get all their direct children (sections) in a single query. This avoids fetching all docs posts into memory and eliminates the N+1 query problem from isValidSection.

With this change, the isValidSection method might no longer be necessary and could be removed if not used elsewhere.

        if ($documentationId > 0) {
            $queryArgs['post_parent'] = $documentationId;
            $sections = get_posts($queryArgs);
        } else {
            $documentationIds = get_posts([
                'post_type'      => self::DOC_POST_TYPE,
                'post_status'    => self::ALLOWED_POST_STATUSES,
                'posts_per_page' => -1,
                'post_parent'    => 0,
                'fields'         => 'ids',
            ]);

            if (empty($documentationIds)) {
                $sections = [];
            } else {
                $queryArgs['post_parent__in'] = $documentationIds;
                $sections = get_posts($queryArgs);
            }
        }


$options = array_map(
function ($section) {
return (object) [
'value' => (string) $section->ID,
'label' => $section->post_title,
];
},
$sections
);

wp_send_json_success(['sections' => $options], 200);
}

public function execute($integrationData, $fieldValues)
{
$integrationDetails = $integrationData->flow_details;
$fieldMap = $integrationDetails->field_map ?? [];

$recordApiHelper = new RecordApiHelper($integrationDetails, $integrationData->id);

return $recordApiHelper->execute($fieldValues, $fieldMap);
}
Comment on lines +95 to +103
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

execute() doesn’t validate that field_map is present/non-empty before delegating to RecordApiHelper. Other action controllers return a WP_Error when field_map is empty, which prevents executing an action with an empty payload and produces a clearer user-facing error. Add the same empty($fieldMap) guard here.

Copilot uses AI. Check for mistakes.

private static function checkPluginExists()
{
if (!class_exists('WeDocs')) {
wp_send_json_error(__('weDocs is not activated or not installed', 'bit-integrations'), 400);
}
}

private static function sanitizeId($value)
{
if (empty($value)) {
return 0;
}

return absint($value);
}

private static function isValidDocsPost($post)
{
return $post instanceof WP_Post && $post->post_type === self::DOC_POST_TYPE;
}

private static function isValidDocumentation($post)
{
return self::isValidDocsPost($post) && (int) $post->post_parent === 0;
}

private static function isValidSection($post)
{
if (!self::isValidDocsPost($post)) {
return false;
}

$parentId = (int) $post->post_parent;

if ($parentId <= 0) {
return false;
}

return (int) wp_get_post_parent_id($parentId) === 0;
}
}
1 change: 1 addition & 0 deletions backend/Core/Util/AllTriggersName.php
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ public static function allTriggersName()
'WCSubscriptions' => ['name' => 'WooCommerce Subscriptions', 'isPro' => true, 'is_active' => false],
'Webhook' => ['name' => 'Webhook', 'isPro' => true, 'is_active' => false],
'WeForms' => ['name' => 'WeForms', 'isPro' => true, 'is_active' => false],
'WeDocs' => ['name' => 'weDocs', 'isPro' => true, 'is_active' => false],
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AllTriggersName::allTriggersName() is merged into the trigger list (see backend/Triggers/TriggerController.php). Adding WeDocs here will surface weDocs as a (Pro) trigger even though this PR only adds a WeDocs action integration (and there is no backend/Triggers/WeDocs). Remove this entry unless there is an actual WeDocs trigger implementation to back it.

Suggested change
'WeDocs' => ['name' => 'weDocs', 'isPro' => true, 'is_active' => false],

Copilot uses AI. Check for mistakes.
'WPCourseware' => ['name' => 'WP Courseware', 'isPro' => true, 'is_active' => false],
'WPEF' => ['name' => 'eForm', 'isPro' => true, 'is_active' => false],
'WPForo' => ['name' => 'wpForo Forum', 'isPro' => true, 'is_active' => false],
Expand Down
1 change: 1 addition & 0 deletions frontend/src/Utils/StaticData/webhookIntegrations.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export const customFormIntegrations = [
'SeoPress',
'ThriveLeads',
'NotificationX',
'WeDocs',
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

customFormIntegrations is used by SetEditIntegComponents to decide which trigger entity editing UI to show. Adding WeDocs here will route flows triggered by an entity named WeDocs into the custom-form edit path, but there’s no corresponding trigger implementation for WeDocs in this PR. Remove this entry unless WeDocs is intended to be a trigger entity.

Suggested change
'WeDocs',

Copilot uses AI. Check for mistakes.
'UserRegistrationMembership',
'UltimateAffiliatePro',
]
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/components/AllIntegrations/EditInteg.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,9 @@ const EditFluentCart = lazy(() => import('./FluentCart/EditFluentCart'))
const EditWCAffiliate = lazy(() => import('./WCAffiliate/EditWCAffiliate'))
const EditWPCafe = lazy(() => import('./WPCafe/EditWPCafe'))
const EditNotificationX = lazy(() => import('./NotificationX/EditNotificationX'))
const EditTeamsForWooCommerceMemberships = lazy(() =>
import('./TeamsForWooCommerceMemberships/EditTeamsForWooCommerceMemberships')
const EditWeDocs = lazy(() => import('./WeDocs/EditWeDocs'))
const EditTeamsForWooCommerceMemberships = lazy(
() => import('./TeamsForWooCommerceMemberships/EditTeamsForWooCommerceMemberships')
)
const EditAsgarosForum = lazy(() => import('./AsgarosForum/EditAsgarosForum'))
const EditSeoPress = lazy(() => import('./SeoPress/EditSeoPress'))
Expand Down Expand Up @@ -597,6 +598,9 @@ const IntegType = memo(({ allIntegURL, flow }) => {
return <EditWPCafe allIntegURL={allIntegURL} />
case 'NotificationX':
return <EditNotificationX allIntegURL={allIntegURL} />
case 'weDocs':
case 'WeDocs':
return <EditWeDocs allIntegURL={allIntegURL} />
case 'Asgaros Forum':
case 'AsgarosForum':
return <EditAsgarosForum allIntegURL={allIntegURL} />
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/components/AllIntegrations/IntegInfo.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ const TeamsForWooCommerceMembershipsAuthorization = lazy(
)
const SeoPressAuthorization = lazy(() => import('./SeoPress/SeoPressAuthorization'))
const NotificationXAuthorization = lazy(() => import('./NotificationX/NotificationXAuthorization'))
const WeDocsAuthorization = lazy(() => import('./WeDocs/WeDocsAuthorization'))
const AsgarosForumAuthorization = lazy(() => import('./AsgarosForum/AsgarosForumAuthorization'))
const UserRegistrationMembershipAuthorization = lazy(
() => import('./UserRegistrationMembership/UserRegistrationMembershipAuthorization')
Expand Down Expand Up @@ -650,6 +651,9 @@ export default function IntegInfo() {
return <SeoPressAuthorization seoPressConf={integrationConf} step={1} isInfo />
case 'NotificationX':
return <NotificationXAuthorization notificationXConf={integrationConf} step={1} isInfo />
case 'weDocs':
case 'WeDocs':
return <WeDocsAuthorization weDocsConf={integrationConf} step={1} isInfo />
case 'Asgaros Forum':
case 'AsgarosForum':
return <AsgarosForumAuthorization asgarosForumConf={integrationConf} step={1} isInfo />
Expand Down
15 changes: 13 additions & 2 deletions frontend/src/components/AllIntegrations/NewInteg.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,9 @@ const FluentCart = lazy(() => import('./FluentCart/FluentCart'))
const WCAffiliate = lazy(() => import('./WCAffiliate/WCAffiliate'))
const WPCafe = lazy(() => import('./WPCafe/WPCafe'))
const NotificationX = lazy(() => import('./NotificationX/NotificationX'))
const TeamsForWooCommerceMemberships = lazy(() =>
import('./TeamsForWooCommerceMemberships/TeamsForWooCommerceMemberships')
const WeDocs = lazy(() => import('./WeDocs/WeDocs'))
const TeamsForWooCommerceMemberships = lazy(
() => import('./TeamsForWooCommerceMemberships/TeamsForWooCommerceMemberships')
)
const AsgarosForum = lazy(() => import('./AsgarosForum/AsgarosForum'))
const SeoPress = lazy(() => import('./SeoPress/SeoPress'))
Expand Down Expand Up @@ -1693,6 +1694,16 @@ export default function NewInteg({ allIntegURL }) {
setFlow={setFlow}
/>
)
case 'weDocs':
case 'WeDocs':
return (
<WeDocs
allIntegURL={allIntegURL}
formFields={flow?.triggerData?.fields}
flow={flow}
setFlow={setFlow}
/>
)
case 'Asgaros Forum':
case 'AsgarosForum':
return (
Expand Down
Loading
Loading