diff --git a/includes/abstract-integration.php b/includes/abstract-integration.php index 069db6ef441653442e1a383bc40eed3c3d5eb41f..bc576462b1d23996e037ca2d40f71286a1bc1dd2 100644 --- a/includes/abstract-integration.php +++ b/includes/abstract-integration.php @@ -8,29 +8,108 @@ use Exception; abstract class Integration extends Singleton { + /** + * Retrive the current form. + * + * @return array $form_data Form data array representation. + */ + abstract public function get_form(); + + /** + * Retrive form by ID. + * + * @since 3.0.0 + * + * @return array $form_data Form data array representation. + */ + abstract public function get_form_by_id($form_id); + + /** + * Retrive available integration forms. + * + * @since 3.0.0 + * + * @return array $forms Collection of form data array representations. + */ + abstract public function get_forms(); + + /** + * Retrive the current form submission. + * + * @since 3.0.0 + * + * @return array $submission Submission data array representation. + */ + abstract public function get_submission(); + + /** + * Retrive the current submission uploaded files. + * + * @since 3.0.0 + * + * @return array $files Collection of file array representations. + */ + abstract public function get_uploads(); + + /** + * Serialize the current form submission data. + * + * @since 1.0.0 + * + * @param any $submission Pair plugin submission handle. + * @param array $form_data Source form data. + * @return array $submission_data Submission data. + */ abstract public function serialize_submission($submission, $form); + + /** + * Serialize the current form data. + * + * @since 1.0.0 + * + * @param any $form Pair plugin form handle. + * @return array $form_data Form data. + */ abstract public function serialize_form($form); - abstract protected function get_uploads($submission, $form_data); - abstract protected function init(); - private $submission = []; - private $uploads = []; + /** + * Get uploads from pair submission handle. + * + * @since 1.0.0 + * + * @param any $submission Pair plugin submission handle. + * @param array $form_data Current form data. + * @return array $uploads Collection of file array representations. + */ + abstract protected function submission_uploads($submission, $form_data); + + /** + * Integration initializer to be fired on wp init. + * + * @since 0.0.1 + */ + abstract protected function init(); + /** + * Bind integration initializer to wp init hook. + * + * @since 0.0.1 + */ protected function __construct() { add_action('init', function () { $this->init(); }); - - add_filter('wpct_erp_forms_submission', function ($null) { - return $this->submission; - }); - - add_filter('wpct_erp_forms_uploads', function ($null) { - return $this->uploads; - }); } + /** + * Submit many requests with Wpct_Http_Client. + * + * @since 1.0.0 + * + * @param array $requests Array of requests. + * @return boolean $success Submit resolution. + */ private function submit($requests) { $success = true; @@ -43,14 +122,29 @@ abstract class Integration extends Singleton if (empty($attachments)) { $response = Wpct_Http_Client::post($endpoint, $payload); } else { - $response = Wpct_Http_Client::post_multipart($endpoint, $payload, $attachments); + $response = Wpct_Http_Client::post_multipart( + $endpoint, + $payload, + $attachments + ); } - $success = $success && !is_wp_error($response) && apply_filters('_wpct_erp_forms_validate_rpc_response', true, $response); + $success = + $success && + !is_wp_error($response) && + apply_filters( + '_wpct_erp_forms_validate_rpc_response', + true, + $response + ); } if (!$success) { - $email = Settings::get_setting('wpct-erp-forms', 'general', 'notification_receiver'); + $email = Settings::get_setting( + 'wpct-erp-forms', + 'general', + 'notification_receiver' + ); if (empty($email)) { return; } @@ -60,22 +154,36 @@ abstract class Integration extends Singleton $body = "Form ID: {$form_data['id']}\n"; $body .= "Form title: {$form_data['title']}\n"; $body .= 'Submission: ' . print_r($payload, true) . "\n"; - $body .= 'Error: ' . print_r($response->get_error_data(), true) . "\n"; + $body .= + 'Error: ' . print_r($response->get_error_data(), true) . "\n"; $success = wp_mail($to, $subject, $body); if (!$success) { - throw new Exception('Error while submitting form ' . $form_data['id']); + throw new Exception( + 'Error while submitting form ' . $form_data['id'] + ); } } return $success; } + /** + * Submit RPC form hooks. + * + * @since 2.0.0 + * + * @param array $models Array of target models. + * @param array $payload Submission data. + * @param array $attachments Collection of attachment files. + * @param array $form_data Source form data. + * @return boolean $result Submit result. + */ private function submit_rpc($models, $payload, $attachments, $form_data) { $setting = Settings::get_setting('wpct-erp-forms', 'rpc-api'); try { - [ $session_id, $user_id ] = $this->rpc_login($setting['endpoint']); + [$session_id, $user_id] = $this->rpc_login($setting['endpoint']); } catch (Exception) { return false; } @@ -86,25 +194,31 @@ abstract class Integration extends Singleton $attachments, $form_data, $session_id, - $user_id, + $user_id ) { + $pipes = []; + foreach ($setting['hooks'] as $hook) { + if ( + $hook['form_id'] == $form_data['id'] && + $hook['endpoint'] === $endpoint + ) { + $pipes = $hook['pipes']; + break; + } + } + $payload = apply_filters( 'wpct_erp_forms_rpc_payload', - $this->rpc_payload( - $session_id, - 'object', - 'execute', - [ - $setting['database'], - $user_id, - $setting['password'], - $model, - 'create', - $payload - ], - ), + $this->rpc_payload($session_id, 'object', 'execute', [ + $setting['database'], + $user_id, + $setting['password'], + $model, + 'create', + $this->apply_pipes($payload, $pipes), + ]), $attachments, - $form_data, + $form_data ); return [ @@ -115,21 +229,57 @@ abstract class Integration extends Singleton ]; }, $models); - $validation = fn ($result, $res) => $this->rpc_response_validation($res); + $validation = fn($result, $res) => $this->rpc_response_validation($res); add_filter('_wpct_erp_forms_validate_rpc_response', $validation, 2, 10); $result = $this->submit($requests); - remove_filter('_wpct_erp_forms_validate_rpc_response', $validation, 2, 10); + remove_filter( + '_wpct_erp_forms_validate_rpc_response', + $validation, + 2, + 10 + ); return $result; } + /** + * Submit REST from hooks. + * + * @since 2.0.0 + * + * @param array $endpoints Array of target endpoints. + * @param array $payload Submission data. + * @param array $attachments Collection of attachment files. + * @param array $form_data Source form data. + * @return boolean $result Submit result. + */ private function submit_rest($endpoints, $payload, $attachments, $form_data) { - $requests = array_map(function ($endpoint) use ($payload, $attachments, $form_data) { + ['form_hooks' => $hooks] = Settings::get_setting( + 'wpct-erp-forms', + 'rest-api' + ); + $requests = array_map(function ($endpoint) use ( + $payload, + $attachments, + $form_data, + $hooks + ) { + $pipes = []; + foreach ($hooks as $hook) { + if ( + $hook['form_id'] == $form_data['id'] && + $hook['endpoint'] === $endpoint + ) { + $pipes = $hook['pipes']; + break; + } + } + return [ 'endpoint' => $endpoint, - 'payload' => $payload, + 'payload' => $this->apply_pipes($payload, $pipes), 'attachments' => $attachments, 'form_data' => $form_data, ]; @@ -138,6 +288,14 @@ abstract class Integration extends Singleton return $this->submit($requests); } + /** + * Form hooks submission subroutine. + * + * @since 1.0.0 + * + * @param any $submission Pair plugin submission handle. + * @param any $form Pair plugin form handle. + */ public function do_submission($submission, $form) { $form_data = $this->serialize_form($form); @@ -145,86 +303,209 @@ abstract class Integration extends Singleton return; } - $uploads = $this->get_uploads($submission, $form_data); - $this->uploads = array_reduce(array_keys($uploads), function ($carry, $name) use ($uploads) { - if ($uploads[$name]['is_multi']) { - for ($i = 1; $i <= count($uploads[$name]['path']); $i++) { - $carry[$name . '_' . $i] = $uploads[$name]['path'][$i - 1]; + $uploads = $this->submission_uploads($submission, $form_data); + $uploads = array_reduce( + array_keys($uploads), + function ($carry, $name) use ($uploads) { + if ($uploads[$name]['is_multi']) { + for ($i = 1; $i <= count($uploads[$name]['path']); $i++) { + $carry[$name . '_' . $i] = + $uploads[$name]['path'][$i - 1]; + } + } else { + $carry[$name] = $uploads[$name]['path']; } - } else { - $carry[$name] = $uploads[$name]['path']; - } - return $carry; - }, []); + return $carry; + }, + [] + ); - $attachments = apply_filters('wpct_erp_forms_attachments', $uploads, $form_data); + $attachments = apply_filters( + 'wpct_erp_forms_attachments', + $uploads, + $form_data + ); - $this->submission = $this->serialize_submission($submission, $form_data); + $submission = $this->serialize_submission($submission, $form_data); $this->cleanup_empties($submission); - $payload = apply_filters('wpct_erp_forms_payload', $this->submission, $attachments, $form_data); + $payload = apply_filters( + 'wpct_erp_forms_payload', + $submission, + $attachments, + $form_data + ); - $endpoints = apply_filters('wpct_erp_forms_endpoints', $this->get_form_endpoints($form_data['id']), $payload, $attachments, $form_data); - $models = apply_filters('wpct_erp_forms_models', $this->get_form_models($form_data['id']), $payload, $attachments, $form_data); + $endpoints = apply_filters( + 'wpct_erp_forms_endpoints', + $this->get_form_endpoints($form_data['id']), + $payload, + $attachments, + $form_data + ); + $models = apply_filters( + 'wpct_erp_forms_models', + $this->get_form_models($form_data['id']), + $payload, + $attachments, + $form_data + ); - do_action('wpct_erp_forms_before_submission', $payload, $attachments, $form_data); + do_action( + 'wpct_erp_forms_before_submission', + $payload, + $attachments, + $form_data + ); - $success = $this->submit_rest($endpoints, $payload, $attachments, $form_data); - $success = $success && $this->submit_rpc($models, $payload, $attachments, $form_data); + $success = $this->submit_rest( + $endpoints, + $payload, + $attachments, + $form_data + ); + $success = + $success && + $this->submit_rpc($models, $payload, $attachments, $form_data); if ($success) { - do_action('wpct_erp_forms_after_submission', $payload, $attachments, $form_data); + do_action( + 'wpct_erp_forms_after_submission', + $payload, + $attachments, + $form_data + ); } else { - do_action('wpct_erp_forms_on_failure', $payload, $attachments, $form_data); + do_action( + 'wpct_erp_forms_on_failure', + $payload, + $attachments, + $form_data + ); + } + } + + /** + * Apply cast pipes to the submission data. + * + * @since 3.0.0 + * + * @param array $payload Submission data. + * @param array $form_data Form data. + */ + private function apply_pipes($payload, $form_data) + { + ['form_hooks' => $rest_hooks] = Settings::get_setting( + 'wpct-erp-forms', + 'rest-api' + ); + ['form_hooks' => $rpc_hooks] = Settings::get_setting( + 'wpct-erp-forms', + 'rpc-api' + ); + + foreach (array_merge($rest_hooks, $rpc_hooks) as $hook) { + if ($hook['form_id'] == $form_data['id']) { + } } } - private function cleanup_empties(&$submission) + /** + * Clean up submission empty fields. + * + * @since 1.0.0 + * + * @param array $submission_data Submission data. + * @return array $submission_data Submission data without empty fields. + */ + private function cleanup_empties(&$submission_data) { - foreach ($submission as $key => $val) { + foreach ($submission_data as $key => $val) { if (empty($val)) { - unset($submission[$key]); + unset($submission_data[$key]); } } - return $submission; + return $submission_data; } + /** + * Get form RPC bounded models. + * + * @since 2.0.0 + * + * @param int $form_id Form ID. + * @return array $models Array of model names. + */ private function get_form_models($form_id) { - $rpc_forms = Settings::get_setting('wpct-erp-forms', 'rpc-api', 'forms'); - return array_unique(array_map(function ($form) { - return $form['model']; - }, array_filter($rpc_forms, function ($form) use ($form_id) { - return (string) $form['form_id'] === (string) $form_id && !empty($form['model']); - }))); + $rpc_forms = Settings::get_setting( + 'wpct-erp-forms', + 'rpc-api', + 'forms' + ); + return array_unique( + array_map( + function ($form) { + return $form['model']; + }, + array_filter($rpc_forms, function ($form) use ($form_id) { + return (string) $form['form_id'] === (string) $form_id && + !empty($form['model']); + }) + ) + ); } + /** + * Get form REST bounded endpoints. + * + * @since 2.0.0 + * + * @param int $form_id Form ID. + * @return array $endpoints Array of endpoints. + */ private function get_form_endpoints($form_id) { - $rest_forms = Settings::get_setting('wpct-erp-forms', 'rest-api', 'forms'); - return array_unique(array_map(function ($form) { - return $form['endpoint']; - }, array_filter($rest_forms, function ($form) use ($form_id) { - return (string) $form['form_id'] === (string) $form_id && !empty($form['endpoint']); - }))); + $rest_forms = Settings::get_setting( + 'wpct-erp-forms', + 'rest-api', + 'forms' + ); + return array_unique( + array_map( + function ($form) { + return $form['endpoint']; + }, + array_filter($rest_forms, function ($form) use ($form_id) { + return (string) $form['form_id'] === (string) $form_id && + !empty($form['endpoint']); + }) + ) + ); } + /** + * JSON RPC login request. + * + * @since 2.0.0 + * + * @param string $endpoint Target endpoint. + * @return array $credentials Tuple with $session_id and $user_id. + */ private function rpc_login($endpoint) { $session_id = time(); $setting = Settings::get_setting('wpct-erp-forms', 'rpc-api'); - $payload = apply_filters('wpct_erp_forms_rpc_login', $this->rpc_payload( - $session_id, - 'common', - 'login', - [ + $payload = apply_filters( + 'wpct_erp_forms_rpc_login', + $this->rpc_payload($session_id, 'common', 'login', [ $setting['database'], $setting['user'], $setting['password'], - ], - )); + ]) + ); $res = Wpct_Http_Client::post($endpoint, $payload); @@ -240,6 +521,17 @@ abstract class Integration extends Singleton return [$session_id, $user_id]; } + /** + * RPC payload decorator. + * + * @since 2.0.0 + * + * @param int $session_id RPC session ID. + * @param string $service RPC service name. + * @param string $method RPC method name. + * @param array $args RPC request arguments. + * @return array $payload RPC conformant payload. + */ private function rpc_payload($session_id, $service, $method, $args) { return [ @@ -254,6 +546,15 @@ abstract class Integration extends Singleton ]; } + /** + * RPC does not work on HTTP standard. This method validate RPC responses + * on the application layer. + * + * @since 2.0.0 + * + * @param array $res RPC response. + * @return boolean $result RPC response result. + */ private function rpc_response_validation($res) { $payload = (array) json_decode($res['body'], true); diff --git a/includes/class-rest-controller.php b/includes/class-rest-controller.php index 962733399288183f189a27ff6878413ebb0d22d6..aeebade98da8e4744ba51d841dd95d040e9edeae 100644 --- a/includes/class-rest-controller.php +++ b/includes/class-rest-controller.php @@ -7,11 +7,48 @@ use WP_REST_Server; class REST_Controller { + /** + * @var string $namespace Handle wp rest api plugin namespace. + * + * @since 3.0.0 + */ private $namespace = 'wpct'; + + /** + * @var int $version Handle the API version. + * + * @since 3.0.0 + */ private $version = 1; + /** + * @var array $settings Handle the plugin settings names list. + * + * @since 3.0.0 + */ private static $settings = ['general', 'rest-api', 'rpc-api']; + /** + * Setup a new rest api controller. + * + * @since 3.0.0 + * + * @return object $controller Instance of REST_Controller. + */ + public static function setup() + { + return new REST_Controller(); + } + + /** + * Internal WP_Error proxy. + * + * @since 3.0.0 + * + * @param string $code + * @param string $message + * @param int $status + */ private static function error($code, $message, $status) { return new WP_Error($code, __($message, 'wpct-erp-forms'), [ @@ -19,6 +56,11 @@ class REST_Controller ]); } + /** + * Binds class initializer to the rest_api_init hook + * + * @since 3.0.0 + */ public function __construct() { add_action('rest_api_init', function () { @@ -26,6 +68,11 @@ class REST_Controller }); } + /** + * REST_Controller initializer. + * + * @since 3.0.0 + */ private function init() { register_rest_route( @@ -42,20 +89,6 @@ class REST_Controller ] ); - register_rest_route( - "{$this->namespace}/v{$this->version}", - '/erp-forms/form/(?P<id>[\d]+)', - [ - 'methods' => WP_REST_Server::READABLE, - 'callback' => function ($req) { - return $this->form_fields($req); - }, - 'permission_callback' => function () { - return $this->permission_callback(); - }, - ] - ); - register_rest_route( "{$this->namespace}/v{$this->version}", '/erp-forms/settings/', @@ -82,37 +115,25 @@ class REST_Controller ); } + /** + * GET requests forms endpoint callback. + * + * @since 3.0.0 + * + * @return array $forms Collection of array form representations. + */ private function forms() { - $forms = Settings::get_forms(); - $response = []; - foreach ($forms as $form) { - $response[] = $form; - } - - return $response; - } - - private function form_fields($req) - { - $target = null; - $form_id = $req->get_url_params()['id']; - $forms = Settings::get_forms(); - foreach ($forms as $form) { - if ($form->id === $form_id) { - $target = $form; - break; - } - } - - if (!$target) { - throw new Exception('Unkown form'); - } - - $fields = apply_filters('wpct_erp_forms_form_fields', [], $form_id); - return $fields; + return apply_filters('wpct_erp_forms_forms', []); } + /** + * GET requests settings endpoint callback. + * + * @since 3.0.0 + * + * @return array $settings Associative array with settings data. + */ private function get_settings() { $settings = []; @@ -125,6 +146,13 @@ class REST_Controller return $settings; } + /** + * POST requests settings endpoint callback. Store settings on the options table. + * + * @since 3.0.0 + * + * @return array $response New settings state. + */ private function set_settings() { $data = (array) json_decode(file_get_contents('php://input'), true); @@ -146,18 +174,21 @@ class REST_Controller return $response; } + /** + * Check if current user can manage options + * + * @since 3.0.0 + * + * @return boolean $allowed + */ private function permission_callback() { - // $nonce = $_REQUEST['_wpctnonce']; - if (!current_user_can('manage_options')) { - // if (!wp_verify_nonce($nonce, 'wpct-erp-forms')) { - return self::error( + return current_user_can('manage_options') + ? true + : self::error( 'rest_unauthorized', 'You can\'t manage wp options', 403 ); - } - - return true; } } diff --git a/includes/class-settings.php b/includes/class-settings.php index 083af89db3b2ec0721315f5ea4e8fc2323358043..148d09ba3c722091f9877da0e8cbc0dce7c12cd5 100644 --- a/includes/class-settings.php +++ b/includes/class-settings.php @@ -6,20 +6,13 @@ use WPCT_ABSTRACT\Settings as BaseSettings; class Settings extends BaseSettings { - public function __construct($group_name) - { - parent::__construct($group_name); - - add_action( - 'load-settings_page_wpct-erp-forms', - function () { - echo '<style>.wpct-erp-forms_general__backends table tr { display: flex; flex-direction: column; }</style>'; - }, - 10, - 0 - ); - } - + /** + * Return registered backends. + * + * @since 3.0.0 + * + * @return array $backends Collection of backend array representations. + */ public static function get_backends() { $setting = Settings::get_setting('wpct-erp-forms', 'general'); @@ -28,6 +21,13 @@ class Settings extends BaseSettings }, $setting['backends']); } + /** + * Get form instances from database. + * + * @since 2.0.0 + * + * @return array $forms Database record objects from form posts. + */ public static function get_forms() { global $wpdb; @@ -54,6 +54,11 @@ class Settings extends BaseSettings } } + /** + * Register plugin settings. + * + * @since 2.0.0 + */ public function register() { $host = parse_url(get_bloginfo('url'))['host']; @@ -92,7 +97,7 @@ class Settings extends BaseSettings 'base_url' => 'https://erp.' . $host, 'headers' => [ [ - 'name' => 'Auhtorization', + 'name' => 'Authorization', 'value' => 'Bearer <erp-backend-token>', ], ], @@ -104,15 +109,15 @@ class Settings extends BaseSettings $this->register_setting( 'rest-api', [ - 'forms' => [ + 'form_hooks' => [ 'type' => 'array', 'items' => [ 'type' => 'object', 'properties' => [ + 'name' => ['type' => 'string'], 'backend' => ['type' => 'string'], 'form_id' => ['type' => 'string'], 'endpoint' => ['type' => 'string'], - 'ref' => ['type' => 'string'], 'pipes' => [ 'type' => 'array', 'items' => [ @@ -120,6 +125,16 @@ class Settings extends BaseSettings 'properties' => [ 'from' => ['type' => 'string'], 'to' => ['type' => 'string'], + 'cast' => [ + 'type' => 'string', + 'enum' => [ + 'boolean', + 'string', + 'integer', + 'float', + 'json', + ], + ], ], ], ], @@ -128,15 +143,7 @@ class Settings extends BaseSettings ], ], [ - 'forms' => [ - [ - 'backend' => 'ERP', - 'form_id' => null, - 'endpoint' => '/api/crm-lead', - 'ref' => null, - 'pipes' => [], - ], - ], + 'form_hooks' => [], ] ); @@ -155,15 +162,15 @@ class Settings extends BaseSettings 'database' => [ 'type' => 'string', ], - 'forms' => [ + 'form_hooks' => [ 'type' => 'array', 'items' => [ 'type' => 'object', 'properties' => [ + 'name' => ['type' => 'string'], 'backend' => ['type' => 'string'], 'form_id' => ['type' => 'string'], 'model' => ['type' => 'string'], - 'ref' => ['type' => 'string'], 'pipes' => [ 'type' => 'array', 'items' => [ @@ -171,6 +178,16 @@ class Settings extends BaseSettings 'properties' => [ 'from' => ['type' => 'string'], 'to' => ['type' => 'string'], + 'cast' => [ + 'type' => 'string', + 'enum' => [ + 'boolean', + 'string', + 'integer', + 'float', + 'json', + ], + ], ], ], ], @@ -183,67 +200,8 @@ class Settings extends BaseSettings 'user' => 'admin', 'password' => 'admin', 'database' => 'erp', - 'forms' => [ - [ - 'backend' => 'ERP', - 'form_id' => 0, - 'model' => 'crm.lead', - 'ref' => null, - 'pipes' => [], - ], - ], + 'form_hooks' => [], ] ); } - - protected function input_render($setting, $field, $value, $is_root = false) - { - if (preg_match('/^forms.*form_id$/', $field)) { - return $this->render_forms_dropdown($setting, $field, $value); - } elseif (preg_match('/^forms.*backend$/', $field)) { - return $this->render_backends_dropdown($setting, $field, $value); - } elseif (preg_match('/password$/', $field)) { - return $this->password_input_render($setting, $field, $value); - } - - return parent::input_render($setting, $field, $value); - } - - private function render_backends_dropdown($setting, $field, $value) - { - $setting_name = $this->setting_name($setting); - $backends = self::get_backends(); - $options = array_merge( - ['<option value=""></option>'], - array_map(function ($backend) use ($value) { - $selected = $backend == $value ? 'selected' : ''; - return "<option value='{$backend}' {$selected}>{$backend}</option>"; - }, $backends) - ); - return "<select name='{$setting_name}[{$field}]'>" . - implode('', $options) . - '</select>'; - } - - private function render_forms_dropdown($setting, $field, $value) - { - $setting_name = $this->setting_name($setting); - $forms = self::get_forms(); - $options = array_merge( - ['<option value=""></option>'], - array_map(function ($form) use ($value) { - $selected = $form->id == $value ? 'selected' : ''; - return "<option value='{$form->id}' {$selected}>{$form->title}</option>"; - }, $forms) - ); - return "<select name='{$setting_name}[{$field}]'>" . - implode('', $options) . - '</select>'; - } - - private function password_input_render($setting, $field, $value) - { - $setting_name = $this->setting_name($setting); - return "<input type='password' name='{$setting_name}[{$field}]' value='{$value}' />"; - } } diff --git a/includes/integrations/gf/class-integration.php b/includes/integrations/gf/class-integration.php index 0c334dc641fd583a920bebd1720513a5c038ad40..14f4a8c34b9e7f193f1827dfad66f7bc6f245408 100644 --- a/includes/integrations/gf/class-integration.php +++ b/includes/integrations/gf/class-integration.php @@ -5,31 +5,142 @@ namespace WPCT_ERP_FORMS\GF; use Exception; use TypeError; use WPCT_ERP_FORMS\Integration as BaseIntegration; +use GFCommon; +use GFAPI; +use GFFormDisplay; require_once 'attachments.php'; require_once 'fields-population.php'; class Integration extends BaseIntegration { + /** + * Inherit prent constructor and hooks submissions to gform_after_submission + * + * @since 0.0.1 + */ protected function __construct() { - add_action('gform_after_submission', function ($entry, $form) { - $this->do_submission($entry, $form); - }, 10, 2); + add_action( + 'gform_after_submission', + function ($entry, $form) { + $this->do_submission($entry, $form); + }, + 10, + 2 + ); parent::__construct(); } - protected function init() - { - } + /** + * Integration initializer to be fired on wp init. + * + * @since 0.0.1 + */ + protected function init() + { + } + + /** + * Retrive the current form data. + * + * @return array $form_data Form data. + */ + public function get_form() + { + $form_id = null; + if (!isset($_POST['gform_submit'])) { + require_once GFCommon::get_base_path() . '/form_display.php'; + $form_id = GFFormDisplay::is_submit_form_id_valid(); + } + + $form = GFAPI::get_submission_form($form_id); + if (is_wp_error($form)) { + return null; + } + return $this->serialize_form($form); + } + + /** + * Retrive form data by ID. + * + * @since 3.0.0 + * + * @param int $form_id Form ID. + * @return array $form_data Form data. + */ + public function get_form_by_id($form_id) + { + $form = GFAPI::get_form($form_id); + if (!$form) { + return null; + } + + return $this->serialize_form($form); + } + + /** + * Retrive available forms data. + * + * @since 3.0.0 + * + * @return array $forms Collection of form data array representations. + */ + public function get_forms() + { + $forms = GFAPI::get_forms(); + return array_map( + function ($form) { + return $this->serialize_form($form); + }, + array_filter($forms, function ($form) { + return $form['is_active'] && !$form['is_trash']; + }) + ); + } + + /** + * Retrive the current submission data. + * + * @since 3.0.0 + * + * @return array $submission Submission data. + */ + public function get_submission() + { + $form = $this->get_form(); + if (!$form) { + return null; + } + + $submission = GFAPI::get_submission(); + $lead_id = gf_apply_filters( + ['gform_entry_id_pre_save_lead', $form_id], + null, + $form + ); + } + + /** + * Serialize gf form data. + * + * @since 1.0.0 + * + * @param array $form GF form data. + * @return array $form_data Form data. + */ public function serialize_form($form) { return [ 'id' => $form['id'], 'title' => $form['title'], - 'ref' => apply_filters('wpct_erp_forms_form_ref', null, $form['id']), + 'hooks' => apply_filters( + 'wpct_erp_forms_form_hooks', + null, + $form['id'] + ), 'description' => $form['description'], 'fields' => array_map(function ($field) { return $this->serialize_field($field); @@ -38,7 +149,16 @@ class Integration extends BaseIntegration return $form; } - private function serialize_field($field) + /** + * Serialize GF form data field. + * + * @since 1.0.0 + * + * @param object GFField instance. + * @param array From data. + * @return array $field_data Field data. + */ + private function serialize_field($field, $form_data) { switch ($field->type) { case 'fileupload': @@ -58,7 +178,11 @@ class Integration extends BaseIntegration $inputs = $field->get_entry_inputs(); if (is_array($inputs)) { $inputs = array_map(function ($input) { - return ['name' => $input['name'], 'label' => $input['label'], 'id' => $input['id']]; + return [ + 'name' => $input['name'], + 'label' => $input['label'], + 'id' => $input['id'], + ]; }, $inputs); } else { $inputs = []; @@ -79,23 +203,33 @@ class Integration extends BaseIntegration 'required' => $field->isRequired, 'options' => $options, 'inputs' => $inputs, - 'conditional' => is_array($field->conditionalLogic) && $field->conditionalLogic['enabled'], + 'conditional' => + is_array($field->conditionalLogic) && + $field->conditionalLogic['enabled'], ]; } - + /** + * Serialize current form submission data. + * + * @since 1.0.0 + * + * @param array $submission GF form lead. + * @param array @form Form data. + * @return array $submission_data Submission data. + */ public function serialize_submission($submission, $form_data) { $data = [ - 'submission_id' => $submission['id'] + 'submission_id' => $submission['id'], ]; foreach ($form_data['fields'] as $field) { if ( - $field['type'] === 'section' - || $field['type'] === 'file' - || $field['type'] === 'files' - || $field['type'] === 'html' + $field['type'] === 'section' || + $field['type'] === 'file' || + $field['type'] === 'files' || + $field['type'] === 'html' ) { continue; } @@ -114,7 +248,10 @@ class Integration extends BaseIntegration if (empty($names[$i])) { continue; } - $data[$names[$i]] = rgar($submission, (string) $inputs[$i]['id']); + $data[$names[$i]] = rgar( + $submission, + (string) $inputs[$i]['id'] + ); } } else { // Plain composed @@ -122,7 +259,11 @@ class Integration extends BaseIntegration foreach ($inputs as $input) { $value = rgar($submission, (string) $input['id']); if ($input_name && $value) { - $value = $this->format_value($value, $field, $input); + $value = $this->format_value( + $value, + $field, + $input + ); if ($value !== null) { $values[] = $value; } @@ -139,7 +280,10 @@ class Integration extends BaseIntegration } else { $raw_value = rgar($submission, (string) $field['id']); } - $data[$input_name] = $this->format_value($raw_value, $field); + $data[$input_name] = $this->format_value( + $raw_value, + $field + ); } } } @@ -147,6 +291,16 @@ class Integration extends BaseIntegration return $data; } + /** + * Format field values with noop fallback. + * + * @since 1.0.0 + * + * @param any $value Field value. + * @param object $field GFField instance. + * @param array $input GFField input data. + * @return any $value Formatted value. + */ private function format_value($value, $field, $input = null) { try { @@ -165,35 +319,51 @@ class Integration extends BaseIntegration return $value; } - protected function get_uploads($submission, $form_data) + /** + * Get current submission uploaded files. + * + * @since 1.0.0 + * + * @param array $submission GF lead data. + * @param array $form_data Form data. + * @return array $uploads Uploaded files data. + */ + protected function submission_uploads($submission, $form_data) { $private_upload = wpct_erp_forms_private_upload($form_data['id']); - return array_reduce(array_filter($form_data['fields'], function ($field) { - return $field['type'] === 'file' || $field['type'] === 'files'; - }), function ($carry, $field) use ($submission, $private_upload) { - $paths = rgar($submission, (string) $field['id']); - if (empty($paths)) { - return $carry; - } - - $paths = $field['type'] === 'files' ? json_decode($paths) : [$paths]; - $paths = array_map(function ($path) use ($private_upload) { - if ($private_upload) { - $url = parse_url($path); - parse_str($url['query'], $query); - $path = wpct_erp_forms_attachment_fullpath($query['erp-forms-attachment']); + return array_reduce( + array_filter($form_data['fields'], function ($field) { + return $field['type'] === 'file' || $field['type'] === 'files'; + }), + function ($carry, $field) use ($submission, $private_upload) { + $paths = rgar($submission, (string) $field['id']); + if (empty($paths)) { + return $carry; } - return $path; - }, $paths); + $paths = + $field['type'] === 'files' ? json_decode($paths) : [$paths]; + $paths = array_map(function ($path) use ($private_upload) { + if ($private_upload) { + $url = parse_url($path); + parse_str($url['query'], $query); + $path = wpct_erp_forms_attachment_fullpath( + $query['erp-forms-attachment'] + ); + } + + return $path; + }, $paths); - $carry[$field['name']] = [ - 'path' => $field['type'] === 'files' ? $paths : $paths[0], - 'is_multi' => $field['type'] === 'files' - ]; + $carry[$field['name']] = [ + 'path' => $field['type'] === 'files' ? $paths : $paths[0], + 'is_multi' => $field['type'] === 'files', + ]; - return $carry; - }, []); + return $carry; + }, + [] + ); } } diff --git a/includes/integrations/wpcf7/class-integration.php b/includes/integrations/wpcf7/class-integration.php index ab43d944505c4e5bdc492bfc1e545c2d340ccc32..6a19dfe262b7a4057bb50260276967fc4c358346 100644 --- a/includes/integrations/wpcf7/class-integration.php +++ b/includes/integrations/wpcf7/class-integration.php @@ -3,22 +3,155 @@ namespace WPCT_ERP_FORMS\WPCF7; use WPCT_ERP_FORMS\Integration as BaseIntegration; +use WPCF7_ContactForm; +use WPCF7_Submission; class Integration extends BaseIntegration { + /** + * Inherit parent constructor and hooks submissions to wpcf7_before_send_mail + * + * @since 0.0.1 + */ protected function __construct() { parent::__construct(); - add_filter('wpcf7_before_send_mail', function ($form, &$abort, $submission) { - $this->do_submission($submission, $form); - }, 10, 3); + add_filter( + 'wpcf7_before_send_mail', + function ($form, &$abort, $submission) { + $this->do_submission($submission, $form); + }, + 10, + 3 + ); } + /** + * Integration initializer to be fired on wp init. + * + * @since 0.0.1 + */ protected function init() { } + /** + * Retrive the current WPCF7_ContactForm data. + * + * @return array $form_data Form data array representation. + */ + public function get_form() + { + $form = WPCF7_ContactForm::get_current(); + if (!$form) { + return null; + } + + return $this->serialize_form($form); + } + + /** + * Retrive form data by ID. + * + * @since 3.0.0 + * + * @param int $form_id Form ID. + * @return array $form_data Form data. + */ + public function get_form_by_id($form_id) + { + $form = WPCF7_ContactForm::get_instance($form_id); + if (!$form) { + return null; + } + + return $this->serialize_form($form); + } + + /** + * Retrive available integration forms data. + * + * @since 3.0.0 + * + * @return array $forms Collection of form data. + */ + public function get_forms() + { + $forms = WPCF7_ContactForm::find(['post_status', 'publish']); + return array_map(function ($form) { + return $this->serialize_form($form); + }, $forms); + } + + /** + * Retrive the current WPCF7_Submission data. + * + * @since 3.0.0 + * + * @return array $submission Submission data. + */ + public function get_submission() + { + $submission = WPCF7_Submission::get_instance(); + if (!$submission) { + return null; + } + + return $this->serialize_submission($submission, $this->get_form()); + } + + /** + * Retrive the current WPCF7_Submission uploaded files. + * + * @since 3.0.0 + * + * @return array $files Uploaded files data. + */ + public function get_uploads() + { + $submission = WPCF7_Submission::get_instance(); + if (!$submission) { + return null; + } + + return $this->submission_uploads($submission, $this->get_form()); + } + + /** + * Serialize WPCF7_ContactForm data. + * + * @since 1.0.0 + * + * @param object $form WPCF7_ContactForm instance. + * @return array $form_data Form data. + */ + public function serialize_form($form) + { + $form_id = $form->id(); + return [ + 'id' => $form_id, + 'title' => $form->title(), + 'hooks' => apply_filters( + 'wpct_erp_forms_form_hooks', + null, + $form_id + ), + 'fields' => array_map(function ($field) use ($form) { + return $this->serialize_field($field, $form); + }, $form->scan_form_tags()), + ]; + } + + /** + * Serialize WPCF7_FormTag to array. + * + * @since 1.0.0 + * + * @param object $field WPCF7_FormTag instance. + * @param array $form_data Form data. + * @return array $field_data Field data. + */ private function serialize_field($field, $form_data) { $type = $field->basetype; @@ -44,10 +177,21 @@ class Integration extends BaseIntegration 'label' => $field->name, 'required' => $field->is_required(), 'options' => $options, - 'conditional' => $field->basetype === 'conditional' || $field->basetype === 'fileconditional', + 'conditional' => + $field->basetype === 'conditional' || + $field->basetype === 'fileconditional', ]; } + /** + * Serialize the WPCF7_Submission data. + * + * @since 1.0.0 + * + * @param object $submission WPCF7_Submission instance. + * @param array $form Form data. + * @return array $submission_data Submission data. + */ public function serialize_submission($submission, $form_data) { $data = $submission->get_posted_data(); @@ -63,7 +207,10 @@ class Integration extends BaseIntegration } } elseif ($field['type'] === 'number') { $data[$key] = (float) $val; - } elseif ($field['type'] === 'file' || $field['type'] === 'submit') { + } elseif ( + $field['type'] === 'file' || + $field['type'] === 'submit' + ) { unset($data[$key]); } } @@ -71,20 +218,16 @@ class Integration extends BaseIntegration return $data; } - public function serialize_form($form) - { - $form_id = $form->id(); - return [ - 'id' => $form_id, - 'title' => $form->title(), - 'ref' => apply_filters('wpct_erp_forms_form_ref', null, $form_id), - 'fields' => array_map(function ($field) use ($form) { - return $this->serialize_field($field, $form); - }, $form->scan_form_tags()), - ]; - } - - protected function get_uploads($submission, $form_data) + /** + * Get WPCF7_Submission uploaded files. + * + * @since 1.0.0 + * + * @param object $submission WPCF7_Submission instance. + * @param array $form_data Form data. + * @return array $uploads Uploaded files data. + */ + protected function submission_uploads($submission, $form_data) { $uploads = []; $uploads = $submission->uploaded_files(); @@ -96,7 +239,7 @@ class Integration extends BaseIntegration 'is_multi' => $is_multi, ]; } - }; + } return $uploads; } diff --git a/src/FormPipes/Table.jsx b/src/FormPipes/Table.jsx index 49ac9697fd130e0a03024cdd01c33b39c975dc2c..65a20423b1768309e50a40ea7edd4c1de0b88283 100644 --- a/src/FormPipes/Table.jsx +++ b/src/FormPipes/Table.jsx @@ -12,8 +12,31 @@ import { useEffect } from "@wordpress/element"; // vendor import useFormFields from "../hooks/useFormFields"; +const castOptions = [ + { + value: "string", + label: __("String", "wpct-erp-forms"), + }, + { + value: "int", + label: __("Integer", "wpct-erp-forms"), + }, + { + value: "float", + label: __("Decimal", "wpct-erp-forms"), + }, + { + value: "boolean", + label: __("Boolean", "wpct-erp-forms"), + }, + { + value: "json", + label: __("JSON", "wpct-erp-forms"), + }, +]; + export default function PipesTable({ formId, pipes, setPipes }) { - const { fields, loading } = useFormFields({ formId }); + const fields = useFormFields({ formId }); const fromOptions = fields.map((field) => ({ label: field.label, value: field.name, @@ -29,7 +52,7 @@ export default function PipesTable({ formId, pipes, setPipes }) { }; const addPipe = () => { - const newPipes = pipes.concat([{ from: "", to: "" }]); + const newPipes = pipes.concat([{ from: "", to: "", cast: "string" }]); setPipes(newPipes); }; @@ -42,8 +65,6 @@ export default function PipesTable({ formId, pipes, setPipes }) { if (!pipes.length) addPipe(); }, [pipes]); - if (loading) return <p>Loading...</p>; - return ( <div className="components-base-control__label"> <label @@ -59,11 +80,11 @@ export default function PipesTable({ formId, pipes, setPipes }) { </label> <table style={{ width: "100%" }}> <tbody> - {pipes.map(({ from, to }, i) => ( + {pipes.map(({ from, to, cast }, i) => ( <tr key={i}> <td> <SelectControl - label={__("From", "wpct-erp-forms")} + placeholder={__("From", "wpct-erp-forms")} value={from} onChange={(value) => setPipe("from", i, value)} options={fromOptions} @@ -78,6 +99,15 @@ export default function PipesTable({ formId, pipes, setPipes }) { __nextHasNoMarginBottom /> </td> + <td style={{ borderLeft: "1rem solid transparent" }}> + <SelectControl + placeholder={__("Cast as", "wpct-erp-forms")} + value={cast || "string"} + onChange={(value) => setPipe("cast", i, value)} + options={castOptions} + __nextHasNoMarginBottom + /> + </td> <td style={{ borderLeft: "1rem solid transparent" }}> <Button isDestructive diff --git a/src/RestApiSettings/Forms/Form.jsx b/src/RestApiSettings/FormHooks/FormHook.jsx similarity index 78% rename from src/RestApiSettings/Forms/Form.jsx rename to src/RestApiSettings/FormHooks/FormHook.jsx index 49d51bad39d5d2bd34e77c98959afeacc6509031..ba371c83a1c4748f77b79f43800de6cc4af620df 100644 --- a/src/RestApiSettings/Forms/Form.jsx +++ b/src/RestApiSettings/FormHooks/FormHook.jsx @@ -7,9 +7,10 @@ import { useState, useRef, useEffect } from "@wordpress/element"; // source import { useForms } from "../../providers/Forms"; import { useGeneral } from "../../providers/Settings"; +import useHookNames from "../../hooks/useHookNames"; import FormPipes from "../../FormPipes"; -function NewForm({ add }) { +function NewFormHook({ add }) { const [{ backends }] = useGeneral(); const backendOptions = backends.map(({ name, base_url }) => ({ label: name, @@ -21,14 +22,22 @@ function NewForm({ add }) { value: id, })); + const hookNames = useHookNames(); + const [name, setName] = useState(""); const [backend, setBackend] = useState(""); const [endpoint, setEndpoint] = useState(""); const [formId, setFormId] = useState(""); + const [nameConflict, setNameConflict] = useState(false); + + const handleSetName = (name) => { + setNameConflict(hookNames.has(name)); + setName(name.trim()); + }; const onClick = () => add({ name, backend, endpoint, form_id: formId }); - const disabled = !(name && backend && endpoint && formId); + const disabled = !(name && backend && endpoint && formId && !nameConflict); return ( <div @@ -45,9 +54,14 @@ function NewForm({ add }) { }} > <TextControl - label={__("Bound ID", "wpct-erp-forms")} + label={__("Name", "wpct-erp-forms")} + help={ + nameConflict + ? __("This name is already in use", "wpct-erp-forms") + : "" + } value={name} - onChange={setName} + onChange={handleSetName} __nextHasNoMarginBottom /> <SelectControl @@ -83,8 +97,8 @@ function NewForm({ add }) { ); } let focus; -export default function Form({ update, remove, ...data }) { - if (data.name === "add") return <NewForm add={update} />; +export default function FormHook({ update, remove, ...data }) { + if (data.name === "add") return <NewFormHook add={update} />; const [{ backends }] = useGeneral(); const backendOptions = backends.map(({ name, base_url }) => ({ @@ -98,29 +112,30 @@ export default function Form({ update, remove, ...data }) { })); const [name, setName] = useState(data.name); + const initialName = useRef(data.name); const nameInput = useRef(); + const hookNames = useHookNames(); + const [nameConflict, setNameConflict] = useState(false); + const handleSetName = (name) => { + setNameConflict(name !== initialName.current && hookNames.has(name)); + setName(name.trim()); + }; + useEffect(() => { if (focus) { nameInput.current.focus(); } }, []); - const timeout = useRef(false); + const timeout = useRef(); useEffect(() => { - if (timeout.current === false) { - timeout.current = 0; - return; - } - clearTimeout(timeout.current); + if (!name || nameConflict) return; timeout.current = setTimeout(() => update({ ...data, name }), 500); }, [name]); - useEffect(() => { - timeout.current = false; - setName(data.name); - }, [data.name]); + useEffect(() => setName(data.name), [data.name]); return ( <div @@ -138,9 +153,14 @@ export default function Form({ update, remove, ...data }) { > <TextControl ref={nameInput} - label={__("Bound ID", "wpct-erp-forms")} + label={__("Name", "wpct-erp-forms")} + help={ + nameConflict + ? __("This name is already in use", "wpct-erp-forms") + : "" + } value={name} - onChange={setName} + onChange={handleSetName} onFocus={() => (focus = true)} onBlur={() => (focus = false)} __nextHasNoMarginBottom @@ -179,7 +199,7 @@ export default function Form({ update, remove, ...data }) { </label> <FormPipes formId={data.form_id} - pipes={data.pipes} + pipes={data.pipes || []} setPipes={(pipes) => update({ ...data, pipes })} /> </div> diff --git a/src/RestApiSettings/FormHooks/index.jsx b/src/RestApiSettings/FormHooks/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..7407c26adebf376b80edc98fe4e540d372e2f49d --- /dev/null +++ b/src/RestApiSettings/FormHooks/index.jsx @@ -0,0 +1,70 @@ +// vendor +import React from "react"; +import { __ } from "@wordpress/i18n"; +import { TabPanel } from "@wordpress/components"; + +// source +import FormHook from "./FormHook"; + +export default function FormHooks({ hooks, setHooks }) { + const tabs = hooks + .map(({ backend, endpoint, form_id, name, pipes }) => ({ + name, + title: name, + form_id, + endpoint, + backend, + pipes, + })) + .concat([ + { + title: __("Add Form", "wpct-erp-forms"), + name: "add", + }, + ]); + + const updateHook = (index, data) => { + if (index === -1) index = forms.length; + const newHooks = hooks + .slice(0, index) + .concat([data]) + .concat(hooks.slice(index + 1, hooks.length)); + setHooks(newHooks); + }; + + const removeHook = ({ name }) => { + const index = hooks.findIndex((h) => h.name === name); + const newHooks = hooks.slice(0, index).concat(hooks.slice(index + 2)); + setHooks(newHooks); + }; + + return ( + <div style={{ width: "100%" }}> + <label + className="components-base-control__label" + style={{ + fontSize: "11px", + textTransform: "uppercase", + fontWeight: 500, + marginBottom: "calc(8px)", + }} + > + {__("Form Hooks", "wpct-erp-forms")} + </label> + <TabPanel tabs={tabs}> + {(hook) => ( + <FormHook + {...hook} + remove={removeHook} + update={(data) => + updateHook( + hooks.findIndex(({ name }) => name === hook.name), + data + ) + } + /> + )} + </TabPanel> + </div> + ); +} diff --git a/src/RestApiSettings/Forms/index.jsx b/src/RestApiSettings/Forms/index.jsx deleted file mode 100644 index d113d28649576572d4f38df927f0ac354ddfafe7..0000000000000000000000000000000000000000 --- a/src/RestApiSettings/Forms/index.jsx +++ /dev/null @@ -1,73 +0,0 @@ -// vendor -import React from "react"; -import { __ } from "@wordpress/i18n"; -import { TabPanel } from "@wordpress/components"; - -// source -import Form from "./Form"; - -export default function Forms({ forms, setForms }) { - const tabs = forms - .map(({ backend, endpoint, form_id, ref, pipes }) => ({ - name: ref, - title: ref, - form_id, - endpoint, - backend, - pipes, - })) - .concat([ - { - title: __("Add Form", "wpct-erp-forms"), - name: "add", - }, - ]); - - const updateForm = (index, data) => { - data = { ...data, ref: data.name }; - delete data.name; - - if (index === -1) index = forms.length; - const newForms = forms - .slice(0, index) - .concat([data]) - .concat(forms.slice(index + 1, forms.length)); - setForms(newForms); - }; - - const removeForm = ({ name }) => { - const index = forms.findIndex((f) => f.ref === name); - const newForms = forms.slice(0, index).concat(forms.slice(index + 2)); - setForms(newForms); - }; - - return ( - <div style={{ width: "100%" }}> - <label - className="components-base-control__label" - style={{ - fontSize: "11px", - textTransform: "uppercase", - fontWeight: 500, - marginBottom: "calc(8px)", - }} - > - {__("Forms", "wpct-erp-forms")} - </label> - <TabPanel tabs={tabs}> - {(form) => ( - <Form - {...form} - remove={removeForm} - update={(newForm) => - updateForm( - forms.findIndex(({ ref }) => ref === form.name), - newForm - ) - } - /> - )} - </TabPanel> - </div> - ); -} diff --git a/src/RestApiSettings/index.jsx b/src/RestApiSettings/index.jsx index 9a74ac85377da911dfa59b034bbccd5fd8c2a6d5..b27d1f06a477e9a068e4cbc6e0bd6a9c6071dfec 100644 --- a/src/RestApiSettings/index.jsx +++ b/src/RestApiSettings/index.jsx @@ -11,10 +11,10 @@ import { // source import { useRestApi } from "../providers/Settings"; -import Forms from "./Forms"; +import FormHooks from "./FormHooks"; export default function RestApiSettings() { - const [{ forms }, save] = useRestApi(); + const [{ form_hooks: hooks }, save] = useRestApi(); return ( <Card size="large" style={{ height: "fit-content" }}> <CardHeader> @@ -22,7 +22,10 @@ export default function RestApiSettings() { </CardHeader> <CardBody> <PanelRow> - <Forms forms={forms} setForms={(forms) => save({ forms })} /> + <FormHooks + hooks={hooks} + setHooks={(form_hooks) => save({ form_hooks })} + /> </PanelRow> </CardBody> </Card> diff --git a/src/RpcApiSettings/Forms/Form.jsx b/src/RpcApiSettings/FormHooks/FormHook.jsx similarity index 77% rename from src/RpcApiSettings/Forms/Form.jsx rename to src/RpcApiSettings/FormHooks/FormHook.jsx index 0e51fce2e1a7e24e4f9ba9e18604b85eff359d3f..e60a76bb355b8c28ad511ae34c92c78ccd7252c1 100644 --- a/src/RpcApiSettings/Forms/Form.jsx +++ b/src/RpcApiSettings/FormHooks/FormHook.jsx @@ -7,9 +7,10 @@ import { useState, useRef, useEffect } from "@wordpress/element"; // source import { useForms } from "../../providers/Forms"; import { useGeneral } from "../../providers/Settings"; +import useHookNames from "../../hooks/useHookNames"; import FormPipes from "../../FormPipes"; -function NewForm({ add }) { +function NewFormHook({ add }) { const [{ backends }] = useGeneral(); const backendOptions = backends.map(({ name, base_url }) => ({ label: name, @@ -21,14 +22,22 @@ function NewForm({ add }) { value: id, })); + const hookNames = useHookNames(); + const [name, setName] = useState(""); const [backend, setBackend] = useState(""); const [model, setModel] = useState(""); const [formId, setFormId] = useState(""); + const [nameConflict, setNameConflict] = useState(false); + + const handleSetName = (name) => { + setNameConflict(hookNames.has(name)); + setName(name.trim()); + }; const onClick = () => add({ name, backend, model, form_id: formId }); - const disabled = !(name, backend, model, formId); + const disabled = !(name && backend && model && formId && !nameConflict); return ( <div @@ -45,9 +54,14 @@ function NewForm({ add }) { }} > <TextControl - label={__("Bound ID", "wpct-erp-forms")} + label={__("Name", "wpct-erp-forms")} + help={ + nameConflict + ? __("This name is already in use", "wpct-erp-forms") + : "" + } value={name} - onChange={setName} + onChange={handleSetName} __nextHasNoMarginBottom /> <SelectControl @@ -84,8 +98,8 @@ function NewForm({ add }) { } let focus; -export default function Form({ update, remove, ...data }) { - if (data.name === "add") return <NewForm add={update} />; +export default function FormHook({ update, remove, ...data }) { + if (data.name === "add") return <NewFormHook add={update} />; const [{ backends }] = useGeneral(); const backendOptions = backends.map(({ name, base_url }) => ({ @@ -99,29 +113,30 @@ export default function Form({ update, remove, ...data }) { })); const [name, setName] = useState(data.name); + const initialName = useRef(data.name); const nameInput = useRef(); + const hookNames = useHookNames(); + const [nameConflict, setNameConflict] = useState(false); + const handleSetName = (name) => { + setNameConflict(name !== initialName.current && hookNames.has(name)); + setName(name.trim()); + }; + useEffect(() => { if (focus) { nameInput.current.focus(); } }, []); - const timeout = useRef(false); + const timeout = useRef(); useEffect(() => { - if (timeout.current === false) { - timeout.current = 0; - return; - } - clearTimeout(timeout.current); + if (!name || nameConflict) return; timeout.current = setTimeout(() => update({ ...data, name }), 500); }, [name]); - useEffect(() => { - timeout.current = false; - setName(data.name); - }, [data.name]); + useEffect(() => setName(data.name), [data.name]); return ( <div @@ -139,12 +154,17 @@ export default function Form({ update, remove, ...data }) { > <TextControl ref={nameInput} - label={__("Bound ID", "wpct-erp-forms")} + label={__("Name", "wpct-erp-forms")} + help={ + nameConflict + ? __("This name is already in use", "wpct-erp-forms") + : "" + } value={name} - onChange={setName} + onChange={handleSetName} onFocus={() => (focus = true)} onBlur={() => (focus = false)} - __nextHasNoMarginBottom={true} + __nextHasNoMarginBottom /> <SelectControl label={__("Backend", "wpct-erp-forms")} @@ -157,7 +177,7 @@ export default function Form({ update, remove, ...data }) { label={__("Model", "wpct-erp-forms")} value={data.model} onChange={(model) => update({ ...data, model })} - __nextHasNoMarginBottom={true} + __nextHasNoMarginBottom /> <SelectControl label={__("Form", "wpct-erp-forms")} @@ -180,7 +200,7 @@ export default function Form({ update, remove, ...data }) { </label> <FormPipes formId={data.form_id} - pipes={data.pipes} + pipes={data.pipes || []} setPipes={(pipes) => update({ ...data, pipes })} /> </div> diff --git a/src/RpcApiSettings/FormHooks/index.jsx b/src/RpcApiSettings/FormHooks/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..2d199c395d6693a6342d347752e5dd9fc2727c29 --- /dev/null +++ b/src/RpcApiSettings/FormHooks/index.jsx @@ -0,0 +1,70 @@ +// vendor +import React from "react"; +import { __ } from "@wordpress/i18n"; +import { TabPanel } from "@wordpress/components"; + +// source +import FormHook from "./FormHook"; + +export default function FormHooks({ hooks, setHooks }) { + const tabs = hooks + .map(({ backend, model, form_id, name, pipes }) => ({ + name, + title: name, + form_id, + model, + backend, + pipes, + })) + .concat([ + { + title: __("Add Form", "wpct-erp-forms"), + name: "add", + }, + ]); + + const updateHook = (index, data) => { + if (index === -1) index = hooks.length; + const newHooks = hooks + .slice(0, index) + .concat([data]) + .concat(hooks.slice(index + 1, hooks.length)); + setHooks(newHooks); + }; + + const removeHook = ({ name }) => { + const index = hooks.findIndex((h) => h.name === name); + const newHooks = hooks.slice(0, index).concat(hooks.slice(index + 2)); + setHooks(newHooks); + }; + + return ( + <div style={{ width: "100%" }}> + <label + className="components-base-control__label" + style={{ + fontSize: "11px", + textTransform: "uppercase", + fontWeight: 500, + marginBottom: "calc(8px)", + }} + > + {__("Form Hooks", "wpct-erp-forms")} + </label> + <TabPanel tabs={tabs}> + {(hook) => ( + <FormHook + {...hook} + remove={removeHook} + update={(data) => + updateHook( + hooks.findIndex(({ name }) => name === hook.name), + data + ) + } + /> + )} + </TabPanel> + </div> + ); +} diff --git a/src/RpcApiSettings/Forms/index.jsx b/src/RpcApiSettings/Forms/index.jsx deleted file mode 100644 index fb1baff861ecf556e55dee9518a3ee671b4f0bde..0000000000000000000000000000000000000000 --- a/src/RpcApiSettings/Forms/index.jsx +++ /dev/null @@ -1,73 +0,0 @@ -// vendor -import React from "react"; -import { __ } from "@wordpress/i18n"; -import { TabPanel } from "@wordpress/components"; - -// source -import Form from "./Form"; - -export default function Forms({ forms, setForms }) { - const tabs = forms - .map(({ backend, model, form_id, ref, pipes }) => ({ - name: ref, - title: ref, - form_id, - model, - backend, - pipes, - })) - .concat([ - { - title: __("Add Form", "wpct-erp-forms"), - name: "add", - }, - ]); - - const updateForm = (index, data) => { - data = { ...data, ref: data.name }; - delete data.name; - - if (index === -1) index = forms.length; - const newForms = forms - .slice(0, index) - .concat([data]) - .concat(forms.slice(index + 1, forms.length)); - setForms(newForms); - }; - - const removeForm = ({ name }) => { - const index = forms.findIndex((f) => f.ref === name); - const newForms = forms.slice(0, index).concat(forms.slice(index + 2)); - setForms(newForms); - }; - - return ( - <div style={{ width: "100%" }}> - <label - className="components-base-control__label" - style={{ - fontSize: "11px", - textTransform: "uppercase", - fontWeight: 500, - marginBottom: "calc(8px)", - }} - > - {__("Forms", "wpct-erp-forms")} - </label> - <TabPanel tabs={tabs}> - {(form) => ( - <Form - {...form} - remove={removeForm} - update={(newForm) => - updateForm( - forms.findIndex(({ ref }) => ref === form.name), - newForm - ) - } - /> - )} - </TabPanel> - </div> - ); -} diff --git a/src/RpcApiSettings/index.jsx b/src/RpcApiSettings/index.jsx index b59ff6749d3aacea1af49aa43e9af3d60268971a..9bc69f3c9d5dc43f9f1f7f2a6ba06e579053fb9d 100644 --- a/src/RpcApiSettings/index.jsx +++ b/src/RpcApiSettings/index.jsx @@ -13,13 +13,14 @@ import { // source import { useRpcApi } from "../providers/Settings"; -import Forms from "./Forms"; +import FormHooks from "./FormHooks"; export default function RpcApiSettings() { - const [{ endpoint, user, password, database, forms }, save] = useRpcApi(); + const [{ endpoint, user, password, database, form_hooks: hooks }, save] = + useRpcApi(); const update = (field) => - save({ endpoint, user, password, database, forms, ...field }); + save({ endpoint, user, password, database, form_hooks: hooks, ...field }); return ( <Card size="large" style={{ height: "fit-content" }}> @@ -61,7 +62,10 @@ export default function RpcApiSettings() { </PanelRow> <Spacer paddingY="calc(8px)" /> <PanelRow> - <Forms forms={forms} setForms={(forms) => update({ forms })} /> + <FormHooks + hooks={hooks} + setHooks={(form_hooks) => update({ form_hooks })} + /> </PanelRow> </CardBody> </Card> diff --git a/src/hooks/useFormFields.js b/src/hooks/useFormFields.js index dfe491aee7dfde6915365d6c622e0d60c8618d6d..f1d966fac7f133f7b7bf0052618c7d01c2b53d3e 100644 --- a/src/hooks/useFormFields.js +++ b/src/hooks/useFormFields.js @@ -1,22 +1,15 @@ // vendor -import { __ } from "@wordpress/i18n"; -import apiFetch from "@wordpress/api-fetch"; -import { useState, useEffect } from "@wordpress/element"; +import { useMemo } from "@wordpress/element"; -export default function useFormFields({ formId }) { - const [loading, setLoading] = useState(); - const [fields, setFields] = useState([]); +// source +import { useForms } from "../providers/Forms"; - useEffect(() => { - apiFetch({ - path: `${window.wpApiSettings.root}wpct/v1/erp-forms/form/${formId}`, - headers: { - "X-WP-Nonce": wpApiSettings.nonce, - }, - }) - .then((fields) => setFields(fields)) - .finally(() => setLoading(false)); - }, []); +export default function useFormFields({ formId }) { + const forms = useForms(); - return { loading, fields }; + return useMemo(() => { + const form = forms.find(({ id }) => id == formId); + if (!form) return []; + return form.fields.map(({ name, label }) => ({ name, label })); + }, [forms]); } diff --git a/src/hooks/useHookNames.js b/src/hooks/useHookNames.js new file mode 100644 index 0000000000000000000000000000000000000000..97bfb5a762b9460d01f2b057e879d20e4e57debc --- /dev/null +++ b/src/hooks/useHookNames.js @@ -0,0 +1,14 @@ +// vendor +import { useMemo } from "@wordpress/element"; + +// source +import { useRestApi, useRpcApi } from "../providers/Settings"; + +export default function useHookNames() { + const [{ form_hooks: restHooks }] = useRestApi(); + const [{ form_hooks: rpcHooks }] = useRpcApi(); + + return useMemo(() => { + return new Set(restHooks.concat(rpcHooks).map(({ name }) => name)); + }, [restHooks, rpcHooks]); +} diff --git a/src/providers/Settings.jsx b/src/providers/Settings.jsx index c0864147ef317bcd85c7b472671d9e90952440c4..049e09702297677748963b45c78d103f74088792 100644 --- a/src/providers/Settings.jsx +++ b/src/providers/Settings.jsx @@ -20,14 +20,14 @@ const defaultSettings = { backends: [], }, "rest-api": { - forms: [], + form_hooks: [], }, "rpc-api": { endpoint: "/jsonrpc", database: "crm.lead", user: "admin", password: "admin", - forms: [], + form_hooks: [], }, }; diff --git a/wpct-erp-forms.php b/wpct-erp-forms.php index 13fa46471397050b8e2f39452a98e1a0db98b516..570ce44c16f1db13b6a66f166d6522a14e0f9d74 100755 --- a/wpct-erp-forms.php +++ b/wpct-erp-forms.php @@ -21,6 +21,13 @@ if (!defined('ABSPATH')) { exit(); } +/** + * Handle plugin version + * + * @since 0.0.1 + * + * @var string WPCT_ERP_FORMS_VERSION Current plugin versio. + */ define('WPCT_ERP_FORMS_VERSION', '2.0.3'); require_once 'abstracts/class-singleton.php'; @@ -38,18 +45,74 @@ require_once 'includes/class-rest-controller.php'; class Wpct_Erp_Forms extends BasePlugin { - private $_integrations = []; - private $_refs = null; - + /** + * Handle plugin active integrations. + * + * @since 1.0.0 + * + * @var array $_integrations + */ + private $_integrations = null; + + /** + * Handle plugin name. + * + * @since 1.0.0 + * + * @var string $name Plugin name. + */ public static $name = 'Wpct ERP Forms'; + + /** + * Handle plugin textdomain. + * + * @since 1.0.0 + * + * @var string $textdomain Plugin text domain. + */ public static $textdomain = 'wpct-erp-forms'; + /** + * Handle plugin menu class name. + * + * @since 1.0.0 + * + * @var string $menu_class Plugin menu class name. + */ protected static $menu_class = '\WPCT_ERP_FORMS\Menu'; + /** + * Starts the plugin. + * + * @since 3.0.0 + */ + public static function start() + { + return self::get_instance(); + } + + /** + * Initialize integrations, REST Controller and setup plugin hooks. + * + * @since 1.0.0 + */ protected function __construct() { parent::__construct(); + REST_Controller::setup(); + $this->load_integrations(); + $this->wp_hooks(); + $this->custom_hooks(); + } + + /** + * Load plugin integrations. + * + * @since 3.0.0 + */ + private function load_integrations() + { if ( apply_filters( 'wpct_is_plugin_active', @@ -69,7 +132,16 @@ class Wpct_Erp_Forms extends BasePlugin require_once 'includes/integrations/gf/class-integration.php'; $this->_integrations['gf'] = GFIntegration::get_instance(); } + } + /** + * Bound plugin to wp hooks. + * + * @since 3.0.0 + */ + private function wp_hooks() + { + // Add link to submenu page on plugins page add_filter( 'plugin_action_links', function ($links, $file) { @@ -87,6 +159,7 @@ class Wpct_Erp_Forms extends BasePlugin 2 ); + // Patch http bridge settings to erp forms settings add_filter('option_wpct-erp-forms_general', function ($value) { $http_setting = Settings::get_setting( 'wpct-http-bridge', @@ -99,6 +172,7 @@ class Wpct_Erp_Forms extends BasePlugin return $value; }); + // Syncronize erp form settings with http bridge settings add_action( 'updated_option', function ($option, $from, $to) { @@ -117,139 +191,151 @@ class Wpct_Erp_Forms extends BasePlugin 3 ); + // Enqueue plugin admin client scripts + add_action('admin_enqueue_scripts', function ($admin_page) { + $this->admin_enqueue_scripts($admin_page); + }); + } + + /** + * Add plugin custom filters. + * + * @since 3.0.0 + */ + private function custom_hooks() + { + // Return registerd form hooks add_filter( - 'wpct_erp_forms_form_ref', + 'wpct_erp_forms_form_hooks', function ($null, $form_id) { - return $this->get_form_ref($form_id); + return $this->get_form_hooks($form_id); }, 10, 2 ); - add_filter( - 'option_wpct-erp-forms_rest-api', - function ($setting) { - return $this->populate_refs($setting); - }, - 10, - 1 - ); - - add_filter( - 'option_wpct-erp-forms_rpc-api', - function ($setting) { - return $this->populate_refs($setting); - }, - 10, - 1 - ); - - add_action( - 'updated_option', - function ($option, $from, $to) { - $this->on_option_updated($option, $to); - }, - 90, - 3 - ); - - add_action( - 'add_option', - function ($option, $value) { - $this->on_option_updated($option, $value); - }, - 90, - 3 - ); + // Return pair plugin registered forms datums + add_filter('wpct_erp_forms_forms', function ($null) { + $integration = $this->get_integration(); + if (!$integration) { + return $null; + } - add_action('admin_enqueue_scripts', function ($admin_page) { - $this->enqueue_scripts($admin_page); + return $integration->get_forms(); }); - new REST_Controller(); - } + // Return current pair plugin form representation + // If $form_id is passed, retrives form by ID. + add_filter('wpct_erp_forms_form', function ($null, $form_id = null) { + $integration = $this->get_integration(); + if (!$integration) { + return $null; + } - public function init() - { - } + if ($form_id) { + return $integration->get_form_by_id($form_id); + } else { + return $integration->get_form(); + } + }); - public static function activate() - { - } + // Return the current submission data + add_filter('wpct_erp_forms_submission', function ($null) { + $integration = $this->get_integration(); + if (!$integration) { + return $null; + } - public static function deactivate() - { - } + return $integration->get_submission(); + }); - private function get_form_refs() - { - if (empty($this->_refs)) { - $this->_refs = get_option('wpct-erp-forms_refs', []); - if (!is_array($this->_refs)) { - $this->_refs = []; + // Return the current submission uploaded files + add_filter('wpct_erp_forms_uploads', function ($null) { + $integration = $this->get_integration(); + if (!$integration) { + return $null; } - } - return $this->_refs; + return $integration->get_uploads(); + }); } - private function set_form_refs($refs) + /** + * Initialize the plugin on wp init. + * + * @since 1.0.0 + */ + public function init() { - $this->_refs = $refs; - update_option('wpct-erp-forms_refs', $refs); } - public function get_form_ref($form_id) + /** + * Callback to activation hook. + * + * @since 1.0.0 + */ + public static function activate() { - $refs = $this->get_form_refs(); - foreach ($refs as $ref_id => $ref) { - if ((string) $ref_id === (string) $form_id) { - return $ref; - } - } - - return null; } - public function set_form_ref($form_id, $ref) + /** + * Callback to deactivation hook. + * + * @since 1.0.0 + */ + public static function deactivate() { - $refs = $this->get_form_refs(); - $refs[$form_id] = $ref; - $this->set_form_refs($refs); } - private function populate_refs($setting) + /** + * Return the current integration. + * + * @since 3.0.0 + * + * @return object $integration + */ + private function get_integration() { - $refs = $this->get_form_refs(); - for ($i = 0; $i < count($setting['forms']); $i++) { - $form = $setting['forms'][$i]; - if (!isset($refs[$form['form_id']])) { - continue; + foreach ($this->_integrations as $key => $integration) { + if ($integration) { + return $integration; } - $form['ref'] = $refs[$form['form_id']]; - $setting['forms'][$i] = $form; } - - return $setting; } - private function on_option_updated($option, $value) + /** + * Return form API hooks. + * + * @since 3.0.0 + * + * @return array $hooks Array with hooks. + */ + private function get_form_hooks($form_id) { - $settings = ['wpct-erp-forms_rest-api', 'wpct-erp-forms_rpc-api']; - if (in_array($option, $settings)) { - $refs = $this->get_form_refs(); - foreach ($value['forms'] as $form) { - if (empty($form['form_id'])) { - continue; + $rest_api = Settings::get_setting('wpct-erp-forms', 'rest-api'); + $rpc_api = Settings::get_setting('wpct-erp-forms', 'rpc-api'); + + return array_reduce( + array_merge($rest_api['form_hooks'], $rpc_api['form_hooks']), + function ($hooks, $hook) use ($form_id) { + if ((int) $hook['form_id'] === (int) $form_id) { + $hooks[$hook['name']] = $hook; } - $refs[$form['form_id']] = $form['ref']; - } - $this->set_form_refs($refs); - } + return $hooks; + }, + [] + ); } - private function enqueue_scripts($admin_page) + /** + * Enqueue admin client scripts + * + * @since 3.0.0 + * + * @param string $admin_page Current admin page. + */ + private function admin_enqueue_scripts($admin_page) { if ('settings_page_wpct-erp-forms' !== $admin_page) { return; @@ -276,10 +362,5 @@ class Wpct_Erp_Forms extends BasePlugin } } -add_action( - 'plugins_loaded', - function () { - $plugin = Wpct_Erp_Forms::get_instance(); - }, - 9 -); +// Setup plugin on wp plugins_loaded hook +add_action('plugins_loaded', ['\WPCT_ERP_FORMS\Wpct_Erp_Forms', 'start'], 9); diff --git a/wpct-http-bridge b/wpct-http-bridge index 3ba30c173cbdc7f25265142e214a3a1cdb2b4030..758f1d2f4e114c9c43c46ffa3aca850659fb2c04 160000 --- a/wpct-http-bridge +++ b/wpct-http-bridge @@ -1 +1 @@ -Subproject commit 3ba30c173cbdc7f25265142e214a3a1cdb2b4030 +Subproject commit 758f1d2f4e114c9c43c46ffa3aca850659fb2c04