| @@ -1,13 +1,18 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | |
| 3 | 3 | defined( 'ABSPATH' ) || die(); |
| 4 | 4 | |
| 5 | +use Elementor\Plugin; | |
| 6 | +use Sellkit\Admin\Funnel\Importer\Ajax_Handler; | |
| 7 | +use Sellkit\Global_Checkout\Checkout; | |
| 8 | + | |
| 5 | 9 | /** |
| 6 | 10 | * List Table component class. |
| 7 | 11 | * |
| 8 | 12 | * @since 1.1.0 |
| 9 | 13 | * @SuppressWarnings(PHPMD.UnusedPrivateMethod) |
| 14 | + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) | |
| 10 | 15 | */ |
| 11 | 16 | class Sellkit_Admin_List_Table { |
| 12 | 17 | |
| 13 | 18 | /** |
| @@ -55,8 +60,13 @@ | ||
| 55 | 60 | $post_type = sellkit_htmlspecialchars( INPUT_POST, 'post_type' ); |
| 56 | 61 | $paged = filter_input( INPUT_POST, 'paged', FILTER_SANITIZE_NUMBER_INT ); |
| 57 | 62 | $posts_per_page = filter_input( INPUT_POST, 'posts_per_page', FILTER_SANITIZE_NUMBER_INT ); |
| 58 | 63 | $search_args = filter_input( INPUT_POST, 'args', FILTER_DEFAULT, FILTER_FORCE_ARRAY ); |
| 64 | + $page = filter_input( INPUT_POST, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); | |
| 65 | + $list_status = isset( $_POST['list_status'] ) ? sanitize_text_field( wp_unslash( $_POST['list_status'] ) ) : 'all'; | |
| 66 | + if ( ! in_array( $list_status, [ 'all', 'publish', 'draft' ], true ) ) { | |
| 67 | + $list_status = 'all'; | |
| 68 | + } | |
| 59 | 69 | |
| 60 | 70 | /** |
| 61 | 71 | * Filter List Table query arguments. |
| 62 | 72 | * |
| @@ -77,8 +87,22 @@ | ||
| 77 | 87 | $args[ $search_arg['param'] ] = $search_arg['value']; |
| 78 | 88 | } |
| 79 | 89 | } |
| 80 | 90 | |
| 91 | + $not_in = $this->get_list_table_post__not_in( $post_type, $page ); | |
| 92 | + if ( ! empty( $not_in ) ) { | |
| 93 | + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- Exclude global checkout funnel from list. | |
| 94 | + $args['post__not_in'] = $not_in; | |
| 95 | + } | |
| 96 | + | |
| 97 | + if ( 'publish' === $list_status ) { | |
| 98 | + $args['post_status'] = 'publish'; | |
| 99 | + } elseif ( 'draft' === $list_status ) { | |
| 100 | + $args['post_status'] = 'draft'; | |
| 101 | + } else { | |
| 102 | + $args['post_status'] = [ 'publish', 'draft', 'pending', 'private', 'future' ]; | |
| 103 | + } | |
| 104 | + | |
| 81 | 105 | // Query. |
| 82 | 106 | $query = new \WP_Query( $args ); |
| 83 | 107 | |
| 84 | 108 | /** |
| @@ -88,8 +112,9 @@ | ||
| 88 | 112 | * |
| 89 | 113 | * @param array $args The taxonomy arguments. |
| 90 | 114 | */ |
| 91 | 115 | $posts = apply_filters( "sellkit_list_table_{$post_type}_posts", $query->posts ); |
| 116 | + $posts = $this->prepare_posts_for_list_table( $posts ); | |
| 92 | 117 | |
| 93 | 118 | /** |
| 94 | 119 | * Filter List Table columns. |
| 95 | 120 | * |
| @@ -101,17 +126,109 @@ | ||
| 101 | 126 | 'labels' => [], |
| 102 | 127 | 'values' => [], |
| 103 | 128 | ], $posts ); |
| 104 | 129 | |
| 130 | + $list_counts = $this->get_list_status_counts( $post_type, $page ); | |
| 131 | + | |
| 105 | 132 | // Send response. |
| 106 | 133 | wp_send_json_success( [ |
| 107 | 134 | 'posts' => $posts, |
| 108 | 135 | 'max_num_pages' => $query->max_num_pages, |
| 109 | 136 | 'columns' => $columns, |
| 137 | + 'list_counts' => $list_counts, | |
| 110 | 138 | ] ); |
| 111 | 139 | } |
| 112 | 140 | |
| 113 | 141 | /** |
| 142 | + * Post IDs excluded from list queries (e.g. global checkout funnel). | |
| 143 | + * | |
| 144 | + * @since 2.4.0 | |
| 145 | + * | |
| 146 | + * @param string $post_type Post type. | |
| 147 | + * @param string $page Admin page slug context. | |
| 148 | + * @return int[] | |
| 149 | + */ | |
| 150 | + private function get_list_table_post__not_in( $post_type, $page ) { | |
| 151 | + if ( 'sellkit-funnels' === $post_type && 'checkout' !== $page ) { | |
| 152 | + $global_checkout_id = (int) get_option( Checkout::SELLKIT_GLOBAL_CHECKOUT_OPTION, 0 ); | |
| 153 | + if ( $global_checkout_id > 0 ) { | |
| 154 | + return [ $global_checkout_id ]; | |
| 155 | + } | |
| 156 | + } | |
| 157 | + return []; | |
| 158 | + } | |
| 159 | + | |
| 160 | + /** | |
| 161 | + * Counts for All / Active / Inactive list filters (respects post__not_in). | |
| 162 | + * | |
| 163 | + * @since 2.4.0 | |
| 164 | + * | |
| 165 | + * @param string $post_type Post type. | |
| 166 | + * @param string $page Admin page slug context. | |
| 167 | + * @return array{all:int,publish:int,draft:int} | |
| 168 | + */ | |
| 169 | + private function get_list_status_counts( $post_type, $page ) { | |
| 170 | + $not_in = $this->get_list_table_post__not_in( $post_type, $page ); | |
| 171 | + $base = [ | |
| 172 | + 'post_type' => $post_type, | |
| 173 | + 'posts_per_page' => -1, | |
| 174 | + 'fields' => 'ids', | |
| 175 | + 'no_found_rows' => false, | |
| 176 | + ]; | |
| 177 | + if ( ! empty( $not_in ) ) { | |
| 178 | + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in | |
| 179 | + $base['post__not_in'] = $not_in; | |
| 180 | + } | |
| 181 | + | |
| 182 | + $statuses_all = [ 'publish', 'draft', 'pending', 'private', 'future' ]; | |
| 183 | + $q_all = new \WP_Query( array_merge( $base, [ 'post_status' => $statuses_all ] ) ); | |
| 184 | + $q_pub = new \WP_Query( array_merge( $base, [ 'post_status' => 'publish' ] ) ); | |
| 185 | + $q_dra = new \WP_Query( array_merge( $base, [ 'post_status' => 'draft' ] ) ); | |
| 186 | + | |
| 187 | + return [ | |
| 188 | + 'all' => (int) $q_all->found_posts, | |
| 189 | + 'publish' => (int) $q_pub->found_posts, | |
| 190 | + 'draft' => (int) $q_dra->found_posts, | |
| 191 | + ]; | |
| 192 | + } | |
| 193 | + | |
| 194 | + /** | |
| 195 | + * Add formatted published / modified dates for list table rows (JSON). | |
| 196 | + * | |
| 197 | + * @since 2.4.0 | |
| 198 | + * | |
| 199 | + * @param array $posts Posts from WP_Query. | |
| 200 | + * @return array | |
| 201 | + */ | |
| 202 | + private function prepare_posts_for_list_table( $posts ) { | |
| 203 | + if ( empty( $posts ) || ! is_array( $posts ) ) { | |
| 204 | + return $posts; | |
| 205 | + } | |
| 206 | + | |
| 207 | + $date_time_format = get_option( 'date_format' ) . ' ' . get_option( 'time_format' ); | |
| 208 | + | |
| 209 | + foreach ( $posts as $key => $post ) { | |
| 210 | + if ( ! $post instanceof \WP_Post ) { | |
| 211 | + continue; | |
| 212 | + } | |
| 213 | + | |
| 214 | + $row = array_merge( | |
| 215 | + $post->to_array(), | |
| 216 | + [ | |
| 217 | + 'custom_date' => get_the_date( 'M d, Y', $post ), | |
| 218 | + 'custom_date_with_time' => mysql2date( $date_time_format, $post->post_date ), | |
| 219 | + 'custom_modified_date' => get_the_modified_date( 'M d, Y', $post ), | |
| 220 | + 'custom_modified_date_with_time' => mysql2date( $date_time_format, $post->post_modified ), | |
| 221 | + ] | |
| 222 | + ); | |
| 223 | + | |
| 224 | + $posts[ $key ] = (object) $row; | |
| 225 | + } | |
| 226 | + | |
| 227 | + return $posts; | |
| 228 | + } | |
| 229 | + | |
| 230 | + /** | |
| 114 | 231 | * Handle post actions. |
| 115 | 232 | * |
| 116 | 233 | * @since 1.1.0 |
| 117 | 234 | */ |
| @@ -121,8 +238,12 @@ | ||
| 121 | 238 | // Sanitize. |
| 122 | 239 | $post = filter_input( INPUT_POST, 'post', FILTER_DEFAULT, FILTER_FORCE_ARRAY ); |
| 123 | 240 | $sub_action = sellkit_htmlspecialchars( INPUT_POST, 'sub_action' ); |
| 124 | 241 | |
| 242 | + if ( is_array( $post ) && count( $post ) < 3 ) { | |
| 243 | + $post = (array) get_post( $post['ID'] ); | |
| 244 | + } | |
| 245 | + | |
| 125 | 246 | call_user_func( [ $this, "handle_post_{$sub_action}" ], $post ); |
| 126 | 247 | } |
| 127 | 248 | |
| 128 | 249 | /** |
| @@ -189,8 +310,9 @@ | ||
| 189 | 310 | * @since 1.1.0 |
| 190 | 311 | * @param array $post Post array. |
| 191 | 312 | */ |
| 192 | 313 | private function handle_post_remove( $post ) { |
| 314 | + do_action( 'sellkit_funnels_after_delete_posts_' . $post['post_type'], $post ); | |
| 193 | 315 | // Delete post. |
| 194 | 316 | $result = wp_delete_post( $post['ID'] ); |
| 195 | 317 | |
| 196 | 318 | // Handle error. |
| @@ -199,8 +321,261 @@ | ||
| 199 | 321 | } |
| 200 | 322 | |
| 201 | 323 | // Send success response. |
| 202 | 324 | wp_send_json_success( $result ); |
| 325 | + } | |
| 326 | + | |
| 327 | + /** | |
| 328 | + * Duplicate funnel item. | |
| 329 | + * | |
| 330 | + * @since 1.5.0 | |
| 331 | + * @param array $post Post Array. | |
| 332 | + */ | |
| 333 | + private function handle_post_duplicate_single_funnel( $post ) { | |
| 334 | + $post_id = $post['ID']; | |
| 335 | + $parent_title = sprintf( '%1s - %2s', esc_html( $post['post_title'] ), esc_html__( 'Clone', 'sellkit' ) ); | |
| 336 | + $funnel_data = get_post_meta( $post_id, 'nodes', true ); | |
| 337 | + $nodes = true; | |
| 338 | + | |
| 339 | + if ( empty( $funnel_data ) ) { | |
| 340 | + $funnel_data = get_post_meta( $post_id, 'sellkit_steps', true ); | |
| 341 | + $nodes = false; | |
| 342 | + } | |
| 343 | + | |
| 344 | + $funnel_slug = get_post_meta( $post_id, 'sellkit_post_slug', true ); | |
| 345 | + | |
| 346 | + // Let's create parent funnel post first. | |
| 347 | + $new_funnel_id = wp_insert_post( [ | |
| 348 | + 'post_title' => $parent_title, | |
| 349 | + 'post_status' => 'draft', | |
| 350 | + 'post_type' => 'sellkit-funnels', | |
| 351 | + 'meta_input' => [ | |
| 352 | + 'sellkit_post_slug' => $funnel_slug, | |
| 353 | + ], | |
| 354 | + ] ); | |
| 355 | + | |
| 356 | + // Loop through old parent data and create steps posts of new funnel. | |
| 357 | + foreach ( $funnel_data as $key => $funnel_item ) { | |
| 358 | + $old_id = $funnel_item['page_id']; | |
| 359 | + | |
| 360 | + // Create steps. | |
| 361 | + $title = $funnel_item['title']; | |
| 362 | + $args = [ | |
| 363 | + 'post_type' => 'sellkit_step', | |
| 364 | + 'post_title' => esc_html( $title ), | |
| 365 | + 'post_status' => get_post_status( $old_id ), | |
| 366 | + ]; | |
| 367 | + | |
| 368 | + $args = $this->handle_post_content( $args, $old_id ); | |
| 369 | + | |
| 370 | + $new_id = wp_insert_post( $args ); | |
| 371 | + | |
| 372 | + // Update new parent funnel data with new steps id. | |
| 373 | + $funnel_data[ $key ]['page_id'] = $new_id; | |
| 374 | + $funnel_data[ $key ]['funnel_id'] = $new_funnel_id; | |
| 375 | + | |
| 376 | + // Add old steps meta data to new steps. | |
| 377 | + $old_data = get_post_meta( $old_id ); | |
| 378 | + | |
| 379 | + foreach ( $old_data as $key => $data ) { | |
| 380 | + if ( 'step_data' === $key ) { | |
| 381 | + $data[0] = str_replace( $post_id, $new_funnel_id, $data[0] ); | |
| 382 | + } | |
| 383 | + | |
| 384 | + if ( '_elementor_data' === $key && '[' === substr( $data[0], 0, 1 ) ) { | |
| 385 | + update_post_meta( $new_id, $key, maybe_unserialize( wp_slash( $data[0] ) ) ); | |
| 386 | + | |
| 387 | + continue; | |
| 388 | + } | |
| 389 | + | |
| 390 | + update_post_meta( $new_id, $key, maybe_unserialize( $data[0] ) ); | |
| 391 | + } | |
| 392 | + | |
| 393 | + if ( class_exists( 'Elementor\Plugin' ) ) { | |
| 394 | + // Also save post in elementor way. after all it's elementor post. | |
| 395 | + $document = Plugin::$instance->documents->get( $new_id ); | |
| 396 | + $document->save( [] ); | |
| 397 | + } | |
| 398 | + } | |
| 399 | + | |
| 400 | + // At the end update parent main meta. | |
| 401 | + if ( false === $nodes ) { | |
| 402 | + update_post_meta( $new_funnel_id, 'sellkit_steps', $funnel_data ); | |
| 403 | + } else { | |
| 404 | + update_post_meta( $new_funnel_id, 'nodes', $funnel_data ); | |
| 405 | + } | |
| 406 | + | |
| 407 | + wp_send_json_success(); | |
| 408 | + } | |
| 409 | + | |
| 410 | + /** | |
| 411 | + * Handle post content for templates without elementor. | |
| 412 | + * | |
| 413 | + * @param array $args funnel nodes arguments. | |
| 414 | + * @param integer $old_id node id. | |
| 415 | + * @since 1.5.0 | |
| 416 | + * @return array | |
| 417 | + */ | |
| 418 | + private function handle_post_content( $args, $old_id ) { | |
| 419 | + if ( empty( $old_id ) ) { | |
| 420 | + return $args; | |
| 421 | + } | |
| 422 | + | |
| 423 | + $is_elementor = get_post_meta( $old_id, '_elementor_data', true ); | |
| 424 | + | |
| 425 | + if ( ! empty( $is_elementor ) ) { | |
| 426 | + return $args; | |
| 427 | + } | |
| 428 | + | |
| 429 | + $post_content = get_post( $old_id ); | |
| 430 | + $content = $post_content->post_content; | |
| 431 | + | |
| 432 | + return array_merge( $args, [ 'post_content' => $content ] ); | |
| 433 | + } | |
| 434 | + | |
| 435 | + /** | |
| 436 | + * Export single funnel. | |
| 437 | + * | |
| 438 | + * @since 1.5.0 | |
| 439 | + * @param array $post Post Array. | |
| 440 | + */ | |
| 441 | + private function export_single_funnel( $post ) { | |
| 442 | + $post_id = $post['ID']; | |
| 443 | + $data['funnel']['id'] = $post_id; | |
| 444 | + $data['funnel']['name'] = $post['post_name']; | |
| 445 | + $data['funnel']['title'] = $post['post_title']; | |
| 446 | + $data['funnel']['post_type'] = $post['post_type']; | |
| 447 | + $data['funnel']['post_status'] = $post['post_status']; | |
| 448 | + $data['funnel']['sellkit_steps'] = get_post_meta( $post_id, 'nodes', true ); | |
| 449 | + | |
| 450 | + // Integration with flowchart. | |
| 451 | + if ( empty( $data['funnel']['sellkit_steps'] ) ) { | |
| 452 | + $data['funnel']['sellkit_steps'] = get_post_meta( $post_id, 'sellkit_steps', true ); | |
| 453 | + } | |
| 454 | + | |
| 455 | + // Check for steps. | |
| 456 | + if ( ! is_array( $data['funnel']['sellkit_steps'] ) ) { | |
| 457 | + return $data; | |
| 458 | + } | |
| 459 | + | |
| 460 | + foreach ( $data['funnel']['sellkit_steps'] as $step ) { | |
| 461 | + if ( ! array_key_exists( 'page_id', $step ) ) { | |
| 462 | + continue; | |
| 463 | + } | |
| 464 | + | |
| 465 | + $id = $step['page_id']; | |
| 466 | + $post = get_post( $id ); | |
| 467 | + $export_data = get_post_meta( $id ); | |
| 468 | + | |
| 469 | + $data['step'][ $id ]['ID'] = $post->ID; | |
| 470 | + $data['step'][ $id ]['name'] = $post->post_name; | |
| 471 | + $data['step'][ $id ]['title'] = $post->post_title; | |
| 472 | + $data['step'][ $id ]['post_type'] = $post->post_type; | |
| 473 | + $data['step'][ $id ]['post_status'] = $post->post_status; | |
| 474 | + $data['step'][ $id ]['post_content'] = $post->post_content; | |
| 475 | + $data['step'][ $id ]['page_id'] = $id; | |
| 476 | + | |
| 477 | + foreach ( $export_data as $key => $meta ) { | |
| 478 | + if ( '_elementor_data' === $key && defined( 'ELEMENTOR_VERSION' ) ) { | |
| 479 | + $document = Plugin::$instance->documents->get( $id ); | |
| 480 | + $template_data = $document->get_export_data(); | |
| 481 | + | |
| 482 | + $data['step'][ $id ]['meta'][ $key ] = wp_json_encode( $template_data['content'] ); | |
| 483 | + continue; | |
| 484 | + } | |
| 485 | + | |
| 486 | + $data['step'][ $id ]['meta'][ $key ] = $meta[0]; | |
| 487 | + } | |
| 488 | + } | |
| 489 | + | |
| 490 | + return $data; | |
| 491 | + } | |
| 492 | + | |
| 493 | + /** | |
| 494 | + * Handle exporting single funnel by ajax. | |
| 495 | + * | |
| 496 | + * @since 1.5.0 | |
| 497 | + * @param array $post Post Array. | |
| 498 | + */ | |
| 499 | + private function handle_post_export_single_funnel( $post ) { | |
| 500 | + $data = []; | |
| 501 | + $data['type'] = 'single'; | |
| 502 | + $data['data'] = $this->export_single_funnel( $post ); | |
| 503 | + | |
| 504 | + wp_send_json_success( $data ); | |
| 505 | + } | |
| 506 | + | |
| 507 | + /** | |
| 508 | + * Handle exporting single funnel by ajax. | |
| 509 | + * | |
| 510 | + * @since 1.5.0 | |
| 511 | + */ | |
| 512 | + private function handle_post_export_all_funnel() { | |
| 513 | + $data = []; | |
| 514 | + $data['type'] = 'multi'; | |
| 515 | + | |
| 516 | + $args = [ | |
| 517 | + 'post_type' => 'sellkit-funnels', | |
| 518 | + 'posts_per_page' => -1, | |
| 519 | + ]; | |
| 520 | + | |
| 521 | + $posts = new \WP_Query( $args ); | |
| 522 | + $posts = $posts->posts; | |
| 523 | + | |
| 524 | + if ( empty( $posts ) ) { | |
| 525 | + wp_send_json_error(); | |
| 526 | + } | |
| 527 | + | |
| 528 | + $global_checkout_id = intval( get_option( Checkout::SELLKIT_GLOBAL_CHECKOUT_OPTION, 0 ) ); | |
| 529 | + | |
| 530 | + foreach ( $posts as $post ) { | |
| 531 | + if ( $post->ID === $global_checkout_id ) { | |
| 532 | + continue; | |
| 533 | + } | |
| 534 | + | |
| 535 | + $post = (array) $post; | |
| 536 | + | |
| 537 | + $data['data'][] = $this->export_single_funnel( $post ); | |
| 538 | + } | |
| 539 | + | |
| 540 | + wp_send_json_success( $data ); | |
| 541 | + } | |
| 542 | + | |
| 543 | + /** | |
| 544 | + * Handle import funnel. | |
| 545 | + * | |
| 546 | + * @since 1.5.0 | |
| 547 | + */ | |
| 548 | + private function handle_post_import_funnel() { | |
| 549 | + $json = filter_input( INPUT_POST, 'funnel', FILTER_DEFAULT ); | |
| 550 | + $page = filter_input( INPUT_POST, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); | |
| 551 | + $data = json_decode( $json ); | |
| 552 | + | |
| 553 | + if ( 'multi' === $data->type && 'checkout' === $page ) { | |
| 554 | + wp_send_json_error( esc_html__( 'You can not import multiple funnels to the global checkout.', 'sellkit' ) ); | |
| 555 | + } | |
| 556 | + | |
| 557 | + $ajax_handler = new Ajax_Handler(); | |
| 558 | + | |
| 559 | + if ( 'single' === $data->type ) { | |
| 560 | + $funnel_data = $data->data->funnel; | |
| 561 | + $steps_data = $data->data->step; | |
| 562 | + $funnel_id = $ajax_handler->import_funnel_by_data( $funnel_data, $steps_data ); | |
| 563 | + } | |
| 564 | + | |
| 565 | + if ( 'multi' === $data->type ) { | |
| 566 | + foreach ( $data->data as $item ) { | |
| 567 | + $funnel_data = $item->funnel; | |
| 568 | + $steps_data = $item->step; | |
| 569 | + $funnel_id = $ajax_handler->import_funnel_by_data( $funnel_data, $steps_data ); | |
| 570 | + } | |
| 571 | + } | |
| 572 | + | |
| 573 | + if ( 'checkout' === $page && (int) $funnel_id > 0 ) { | |
| 574 | + update_option( Checkout::SELLKIT_GLOBAL_CHECKOUT_OPTION, $funnel_id ); | |
| 575 | + } | |
| 576 | + | |
| 577 | + wp_send_json_success(); | |
| 203 | 578 | } |
| 204 | 579 | } |
| 205 | 580 | |
| 206 | 581 | Sellkit_Admin_List_Table::get_instance(); |