-
Notifications
You must be signed in to change notification settings - Fork 1
feat(wptablebuilder): add WP Table Builder action integration #200
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
35fe35e
7456444
19a8bed
0e4efba
d71c2c2
d224f9e
dfbe910
5870825
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| <?php | ||
|
|
||
| /** | ||
| * WP Table Builder Record Api | ||
| */ | ||
|
|
||
| namespace BitApps\Integrations\Actions\WpTableBuilder; | ||
|
|
||
| use BitApps\Integrations\Config; | ||
| use BitApps\Integrations\Core\Util\Common; | ||
| use BitApps\Integrations\Core\Util\Hooks; | ||
| use BitApps\Integrations\Log\LogHandler; | ||
|
|
||
| /** | ||
| * Provide functionality for WP Table Builder table writes | ||
| */ | ||
| class RecordApiHelper | ||
| { | ||
| private $_integrationID; | ||
|
|
||
| private $_integrationDetails; | ||
|
|
||
| public function __construct($integrationDetails, $integId) | ||
| { | ||
| $this->_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 | ||
| * | ||
| * @return array | ||
| */ | ||
| public function execute($fieldValues, $fieldMap, $utilities) | ||
| { | ||
| if (!\defined('WPTB_PLUGIN_DIR')) { | ||
| return [ | ||
| 'success' => false, | ||
| 'message' => __('WP Table Builder is not installed or activated', 'bit-integrations') | ||
| ]; | ||
| } | ||
|
|
||
| $fieldData = static::generateReqDataFromFieldMap($fieldMap, $fieldValues); | ||
|
|
||
| // No fallback action: every action writes, and delete_table removes a table, so | ||
| // a flow that lost its mainAction should fail through the default branch. | ||
| $mainAction = $this->_integrationDetails->mainAction ?? ''; | ||
|
|
||
| $defaultResponse = [ | ||
| 'success' => false, | ||
| // translators: %s: Plugin name | ||
| 'message' => wp_sprintf(__('%s plugin is not installed or activated', 'bit-integrations'), 'Bit Integrations Pro') | ||
| ]; | ||
|
|
||
| switch ($mainAction) { | ||
| case 'create_table': | ||
| $response = Hooks::apply(Config::withPrefix('wptablebuilder_create_table'), $defaultResponse, $fieldData); | ||
| $type = 'table'; | ||
| $actionType = 'create_table'; | ||
|
|
||
| break; | ||
|
|
||
| case 'update_table': | ||
| $response = Hooks::apply(Config::withPrefix('wptablebuilder_update_table'), $defaultResponse, $fieldData); | ||
| $type = 'table'; | ||
| $actionType = 'update_table'; | ||
|
|
||
| break; | ||
|
|
||
| case 'delete_table': | ||
| $response = Hooks::apply(Config::withPrefix('wptablebuilder_delete_table'), $defaultResponse, $fieldData, $utilities); | ||
| $type = 'table'; | ||
| $actionType = 'delete_table'; | ||
|
|
||
| break; | ||
|
|
||
| case 'add_row': | ||
| $response = Hooks::apply(Config::withPrefix('wptablebuilder_add_row'), $defaultResponse, $fieldData, $this->_integrationDetails); | ||
| $type = 'row'; | ||
| $actionType = 'add_row'; | ||
|
|
||
| break; | ||
|
|
||
| default: | ||
| $response = [ | ||
| 'success' => false, | ||
| 'message' => __('Invalid action', 'bit-integrations') | ||
| ]; | ||
| $type = 'WpTableBuilder'; | ||
| $actionType = 'unknown'; | ||
|
|
||
| break; | ||
| } | ||
|
|
||
| $responseType = isset($response['success']) && $response['success'] ? 'success' : 'error'; | ||
| LogHandler::save($this->_integrationID, ['type' => $type, 'type_name' => $actionType], $responseType, $response); | ||
|
|
||
| return $response; | ||
| } | ||
|
|
||
| private static function generateReqDataFromFieldMap($fieldMap, $fieldValues) | ||
| { | ||
| $dataFinal = []; | ||
| foreach ($fieldMap as $item) { | ||
| $triggerValue = $item->formField; | ||
| $actionValue = $item->wpTableBuilderField; | ||
|
|
||
| if (empty($actionValue)) { | ||
| continue; | ||
| } | ||
|
|
||
| $dataFinal[$actionValue] = $triggerValue === 'custom' && isset($item->customValue) | ||
| ? Common::replaceFieldWithValue($item->customValue, $fieldValues) | ||
| : $fieldValues[$triggerValue] ?? ''; | ||
| } | ||
|
|
||
| return $dataFinal; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| <?php | ||
|
|
||
| if (!defined('ABSPATH')) { | ||
| exit; | ||
| } | ||
|
|
||
| use BitApps\Integrations\Actions\WpTableBuilder\WpTableBuilderController; | ||
| use BitApps\Integrations\Core\Util\Route; | ||
|
|
||
| Route::post('wptablebuilder_authorize', [WpTableBuilderController::class, 'wpTableBuilderAuthorize']); | ||
|
|
||
| // Create/Update/Delete take table_id through the field map, so a flow can target a | ||
| // different table per run. Add Row is the exception: its column list has to be known | ||
| // while the flow is being configured, which only a fixed table can provide. | ||
| Route::post('refresh_wptablebuilder_tables', [WpTableBuilderController::class, 'refreshTables']); | ||
| Route::post('refresh_wptablebuilder_columns', [WpTableBuilderController::class, 'refreshColumns']); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| <?php | ||
|
|
||
| /** | ||
| * WP Table Builder Integration | ||
| */ | ||
|
|
||
| namespace BitApps\Integrations\Actions\WpTableBuilder; | ||
|
|
||
| use DOMDocument; | ||
| use DOMXPath; | ||
| use WP_Error; | ||
|
|
||
| /** | ||
| * Provide functionality for WP Table Builder integration | ||
| */ | ||
| class WpTableBuilderController | ||
| { | ||
| /** | ||
| * Tables are a custom post type, and the table body lives in a single post meta. | ||
| * Mirrors WP Table Builder's Cpt::POST_TYPE. | ||
| */ | ||
| public const POST_TYPE = 'wptb-tables'; | ||
|
|
||
| public const CONTENT_META_KEY = '_wptb_content_'; | ||
|
|
||
| public static function isExists() | ||
| { | ||
| if (!\defined('WPTB_PLUGIN_DIR')) { | ||
| wp_send_json_error( | ||
| __( | ||
| 'WP Table Builder is not activated or not installed', | ||
| 'bit-integrations' | ||
| ), | ||
| 400 | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| public static function wpTableBuilderAuthorize() | ||
| { | ||
| self::isExists(); | ||
| wp_send_json_success(true); | ||
| } | ||
|
|
||
| /** | ||
| * List the tables an Add Row flow can append to. | ||
| * | ||
| * Trashed tables are excluded — appending to one would silently write into a table | ||
| * nobody can see. | ||
| */ | ||
| public static function refreshTables() | ||
| { | ||
| self::isExists(); | ||
|
|
||
| $tables = get_posts( | ||
| [ | ||
| 'post_type' => self::POST_TYPE, | ||
| 'post_status' => ['publish', 'draft', 'pending', 'private', 'future'], | ||
| 'numberposts' => -1, | ||
| 'orderby' => 'title', | ||
| 'order' => 'ASC', | ||
| 'suppress_filters' => true, | ||
| ] | ||
| ); | ||
|
|
||
| $response['tables'] = array_map( | ||
| function ($table) { | ||
| return [ | ||
| 'value' => (string) $table->ID, | ||
| // An untitled table is still selectable, so fall back to the id. | ||
| 'label' => $table->post_title === '' | ||
| // Translators: %d is the table's post ID. WP Table Builder tables are a custom post type, and the title is optional. | ||
| ? wp_sprintf(__('Table #%d', 'bit-integrations'), $table->ID) | ||
| : $table->post_title, | ||
| ]; | ||
| }, | ||
| $tables | ||
| ); | ||
|
|
||
| wp_send_json_success($response); | ||
| } | ||
|
|
||
| /** | ||
| * Read a table's column labels from its header row so the field map can be built | ||
| * with real names instead of positional placeholders. | ||
| * | ||
| * @param mixed $requestParams | ||
| */ | ||
| public static function refreshColumns($requestParams) | ||
| { | ||
| self::isExists(); | ||
|
|
||
| if (empty($requestParams->selectedTable)) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Declining —
Only a direct read warns. So The same review flagged a case where the read is direct — |
||
| wp_send_json_error(__('Select a table first', 'bit-integrations'), 400); | ||
| } | ||
|
|
||
| $table = get_post((int) $requestParams->selectedTable); | ||
|
|
||
| if (!$table || $table->post_type !== self::POST_TYPE) { | ||
| wp_send_json_error(__('Table not found', 'bit-integrations'), 400); | ||
| } | ||
|
|
||
| $columns = self::readColumnLabels(get_post_meta($table->ID, self::CONTENT_META_KEY, true)); | ||
|
|
||
| if (empty($columns)) { | ||
| wp_send_json_error( | ||
| __('No columns found. Open the table in WP Table Builder and add at least one row.', 'bit-integrations'), | ||
| 400 | ||
| ); | ||
| } | ||
|
|
||
| wp_send_json_success(['columns' => $columns]); | ||
| } | ||
|
|
||
| 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); | ||
| } | ||
|
|
||
| /** | ||
| * Extract one entry per column from the first row of the stored table markup. | ||
| * | ||
| * The stored value is rendered HTML, so the header labels are read from the DOM | ||
| * rather than from any structured source — WP Table Builder does not keep one. | ||
| * | ||
| * @param string $content | ||
| * | ||
| * @return array | ||
| */ | ||
| private static function readColumnLabels($content) | ||
| { | ||
| if (empty($content) || !\class_exists('DOMDocument')) { | ||
| return []; | ||
| } | ||
|
|
||
| $dom = new DOMDocument(); | ||
| $previous = libxml_use_internal_errors(true); | ||
| // The stored markup is a fragment, and it is authored content that routinely | ||
| // trips libxml — parse errors here are expected and must not surface. | ||
| $dom->loadHTML( | ||
| '<?xml encoding="utf-8" ?>' . $content, | ||
| LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD | ||
| ); | ||
| libxml_clear_errors(); | ||
| libxml_use_internal_errors($previous); | ||
|
|
||
| $xpath = new DOMXPath($dom); | ||
| $firstRow = $xpath->query('//tr')->item(0); | ||
|
|
||
| if (!$firstRow) { | ||
| return []; | ||
| } | ||
|
|
||
| $columns = []; | ||
|
|
||
| foreach ($xpath->query('.//th|.//td', $firstRow) as $index => $cell) { | ||
| $label = trim($cell->textContent); | ||
|
|
||
| $columns[] = [ | ||
| 'key' => 'cell_' . $index, | ||
| 'label' => $label === '' | ||
| ? wp_sprintf(__('Column %d', 'bit-integrations'), $index + 1) | ||
| : $label, | ||
| 'required' => false, | ||
| ]; | ||
| } | ||
|
|
||
| return $columns; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use the null coalescing operator (
??) when accessing properties on$itemto prevent 'Undefined property' notices if any expected fields are missing from the mapped item.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, applied:
These are direct property reads, which is what makes this one different from the
isset()/empty()cases elsewhere in this review — a direct read on a missing property does emitWarning: Undefined property, so the??is doing real work here.