*/ private static function get_optimize_fields(): array { return array( 'meta_title' => array( 'label' => __( 'Meta Title', 'ai-content-by-parallax' ), 'credits' => 1, ), 'meta_desc' => array( 'label' => __( 'Meta Description', 'ai-content-by-parallax' ), 'credits' => 2, ), 'short_desc' => array( 'label' => __( 'Short Description', 'ai-content-by-parallax' ), 'credits' => 3, ), 'long_desc' => array( 'label' => __( 'Description', 'ai-content-by-parallax' ), 'credits' => 5, ), ); } /** * Register hooks for the product category list screen. * * @return void */ public static function register(): void { add_action( 'current_screen', array( self::class, 'maybe_attach_hooks' ) ); // AJAX handlers must be registered unconditionally. add_action( 'wp_ajax_' . WCPAI_NAMESPACE . '_toggle_cat_excluded', array( self::class, 'ajax_toggle_cat_excluded' ) ); add_action( 'wp_ajax_' . WCPAI_NAMESPACE . '_bulk_toggle_cat_lock', array( self::class, 'ajax_bulk_toggle_cat_lock' ) ); } /** * Conditionally attach hooks when on the product category list screen. * * @param \WP_Screen $screen Current admin screen. * @return void */ public static function maybe_attach_hooks( $screen ): void { if ( ! is_object( $screen ) || 'edit-product_cat' !== ( $screen->id ?? '' ) ) { return; } add_filter( 'manage_edit-product_cat_columns', array( self::class, 'add_columns' ), 20 ); add_action( 'manage_product_cat_custom_column', array( self::class, 'render_column' ), 10, 3 ); add_action( 'admin_notices', array( self::class, 'render_optimize_form' ) ); add_filter( 'term_row_actions', array( self::class, 'add_row_actions' ), 10, 2 ); add_filter( 'bulk_actions-edit-product_cat', array( self::class, 'add_bulk_actions' ) ); add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_assets' ) ); } /** * Add the "Optimized" column to the category list table. * * @param array $columns Existing columns. * @return array */ public static function add_columns( array $columns ): array { $columns['parallax_optimized'] = __( 'Optimized', 'ai-content-by-parallax' ); return $columns; } /** * Render the "Optimized" column value for a category row. * * @param string $content Existing column content. * @param string $column Column key. * @param int $term_id Term ID. * @return void */ public static function render_column( string $content, string $column, int $term_id ): void { if ( 'parallax_optimized' !== $column ) { return; } $is_excluded = self::is_category_excluded( $term_id ); if ( $is_excluded ) { printf( '', esc_attr__( 'Locked — excluded from optimization', 'ai-content-by-parallax' ) ); return; } $optimized = AjaxRequestHandlers::is_term_optimized_for_optimize_all( $term_id ); if ( $optimized ) { printf( '', esc_attr__( 'Optimized', 'ai-content-by-parallax' ) ); } else { printf( '', esc_attr__( 'Not optimized', 'ai-content-by-parallax' ) ); } } /** * Add "Optimize" and "Lock/Unlock" row actions on category rows. * * @param array $actions Existing row actions. * @param \WP_Term $tag Current term object. * @return array */ public static function add_row_actions( array $actions, $tag ): array { if ( ! current_user_can( 'manage_options' ) ) { return $actions; } if ( ! is_object( $tag ) || 'product_cat' !== ( $tag->taxonomy ?? '' ) ) { return $actions; } $is_excluded = self::is_category_excluded( (int) $tag->term_id ); $actions['parallax_optimize'] = sprintf( '%s', (int) $tag->term_id, esc_html__( 'Optimize', 'ai-content-by-parallax' ) ); $actions['parallax_lock'] = sprintf( '%s', (int) $tag->term_id, $is_excluded ? '1' : '0', $is_excluded ? esc_html__( 'Unlock', 'ai-content-by-parallax' ) : esc_html__( 'Lock', 'ai-content-by-parallax' ) ); return $actions; } /** * Add "Optimize Selected" and "Toggle Lock" to the bulk actions dropdown. * * @param array $actions Existing bulk actions. * @return array */ public static function add_bulk_actions( array $actions ): array { if ( current_user_can( 'manage_options' ) ) { $actions['parallax_cat_optimize_selected'] = __( 'Optimize Selected', 'ai-content-by-parallax' ); $actions['parallax_cat_toggle_lock'] = __( 'Toggle Optimization Lock', 'ai-content-by-parallax' ); } return $actions; } /** * Render the collapsible optimization form above the category list. * * @return void */ public static function render_optimize_form(): void { if ( ! current_user_can( 'manage_options' ) ) { return; } $screen = get_current_screen(); if ( ! $screen || 'edit-product_cat' !== $screen->id ) { return; } $fields = self::get_optimize_fields(); $client = Client::load(); $credits = $client->get_credits(); $approval_import_url = ProductListIntegration::get_approval_import_url(); ?>
admin_url( 'admin-ajax.php' ), 'approval_import_url' => ProductListIntegration::get_approval_import_url(), 'optimize_all_nonce' => wp_create_nonce( AjaxRequestHandlers::get_nonce_action( 'optimize_all' ) ), 'namespace' => WCPAI_NAMESPACE, 'credits_available' => $client->get_credits(), 'is_authenticated' => $client->is_user_authenticated(), 'toggle_excluded_nonce' => wp_create_nonce( WCPAI_NAMESPACE . '_toggle_cat_excluded' ), 'bulk_toggle_lock_nonce' => wp_create_nonce( WCPAI_NAMESPACE . '_bulk_toggle_cat_lock' ), 'i18n' => array( 'select_field' => __( 'Please select at least one field to optimize.', 'ai-content-by-parallax' ), 'select_category' => __( 'Please select at least one category.', 'ai-content-by-parallax' ), /* translators: %s: formatted credit count */ 'confirm_optimize_all' => __( "Would you like to optimize all categories?\nTotal Credits Required: %s", 'ai-content-by-parallax' ), /* translators: 1: number of selected categories, 2: formatted credit count */ 'confirm_optimize_selected' => __( 'Optimize %1$s selected categories? Credits required: %2$s.', 'ai-content-by-parallax' ), /* translators: %d: number of categories (plural) */ 'submitted_count' => __( 'Optimization submitted for %d categories.', 'ai-content-by-parallax' ), /* translators: shown when exactly 1 category is submitted */ 'submitted_count_singular' => __( 'Optimization submitted for 1 category.', 'ai-content-by-parallax' ), 'submitted_all' => __( 'Optimization request submitted for all categories.', 'ai-content-by-parallax' ), 'request_failed' => __( 'Optimization request failed.', 'ai-content-by-parallax' ), 'error_occurred' => __( 'An error occurred while submitting the optimization request.', 'ai-content-by-parallax' ), 'optimizing' => __( 'Optimizing…', 'ai-content-by-parallax' ), 'submitted' => __( 'Submitted', 'ai-content-by-parallax' ), 'optimize' => __( 'Optimize', 'ai-content-by-parallax' ), 'optimization_failed' => __( 'Optimization failed.', 'ai-content-by-parallax' ), 'error_optimizing_category' => __( 'An error occurred while optimizing this category.', 'ai-content-by-parallax' ), 'saving' => __( 'Saving…', 'ai-content-by-parallax' ), 'unlock' => __( 'Unlock', 'ai-content-by-parallax' ), 'lock' => __( 'Lock', 'ai-content-by-parallax' ), 'lock_failed' => __( 'Failed to update lock status.', 'ai-content-by-parallax' ), 'error_lock' => __( 'An error occurred while updating lock status.', 'ai-content-by-parallax' ), 'all_categories' => __( 'All Categories', 'ai-content-by-parallax' ), /* translators: %d: number of selected categories */ 'optimize_selected' => __( 'Optimize Selected (%d)', 'ai-content-by-parallax' ), /* translators: %d: number of selected categories */ 'selected_count' => __( '%d Selected', 'ai-content-by-parallax' ), 'locked_excluded' => __( 'Locked — excluded from optimization', 'ai-content-by-parallax' ), 'optimized' => __( 'Optimized', 'ai-content-by-parallax' ), 'not_optimized' => __( 'Not optimized', 'ai-content-by-parallax' ), 'select_category_lock' => __( 'Please select at least one category.', 'ai-content-by-parallax' ), /* translators: %d: number of categories toggled (plural) */ 'toggle_lock_success' => __( 'Lock status toggled for %d categories.', 'ai-content-by-parallax' ), /* translators: shown when exactly 1 category lock is toggled */ 'toggle_lock_success_single' => __( 'Lock status toggled for 1 category.', 'ai-content-by-parallax' ), 'toggle_lock_failed' => __( 'Failed to toggle lock status.', 'ai-content-by-parallax' ), 'no_eligible' => __( 'No eligible categories to optimize. Locked categories are excluded.', 'ai-content-by-parallax' ), 'insufficient_credits' => __( 'Insufficient credits:', 'ai-content-by-parallax' ), /* translators: 1: required credits, 2: available credits */ 'credits_needed' => __( 'You need %1$s credits but only have %2$s available. Please purchase more credits to proceed.', 'ai-content-by-parallax' ), 'credits_sufficient' => __( 'You have enough credits to run this batch.', 'ai-content-by-parallax' ), 'authentication_required' => __( 'Cannot submit batch request: the configured Parallax API key could not be verified. Check your API credentials on the Connection tab and try again.', 'ai-content-by-parallax' ), 'not_enough_credits_btn' => __( 'Not enough credits to optimize the selected items.', 'ai-content-by-parallax' ), 'view_status_link' => __( 'View optimization status', 'ai-content-by-parallax' ), ), ) ); } /** * AJAX handler to toggle the lock (excluded) meta for a category. * * @return void */ public static function ajax_toggle_cat_excluded(): void { check_ajax_referer( WCPAI_NAMESPACE . '_toggle_cat_excluded' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'ai-content-by-parallax' ) ), 403 ); } $term_id = isset( $_POST['term_id'] ) ? absint( wp_unslash( $_POST['term_id'] ) ) : 0; if ( ! $term_id ) { wp_send_json_error( array( 'message' => __( 'Invalid category.', 'ai-content-by-parallax' ) ), 400 ); } $term = get_term( $term_id, 'product_cat' ); if ( ! $term || is_wp_error( $term ) ) { wp_send_json_error( array( 'message' => __( 'Invalid category.', 'ai-content-by-parallax' ) ), 400 ); } $currently_excluded = self::is_category_excluded( $term_id ); if ( $currently_excluded ) { delete_term_meta( $term_id, self::META_EXCLUDED ); } else { update_term_meta( $term_id, self::META_EXCLUDED, '1' ); } $now_excluded = ! $currently_excluded; wp_send_json_success( array( 'term_id' => $term_id, 'excluded' => $now_excluded, 'optimized' => ! $now_excluded ? AjaxRequestHandlers::is_term_optimized_for_optimize_all( $term_id ) : false, ) ); } /** * AJAX handler to bulk-toggle the lock meta for multiple categories. * * @return void */ public static function ajax_bulk_toggle_cat_lock(): void { check_ajax_referer( WCPAI_NAMESPACE . '_bulk_toggle_cat_lock' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'ai-content-by-parallax' ) ), 403 ); } $raw_ids = isset( $_POST['term_ids'] ) ? wp_unslash( $_POST['term_ids'] ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- each ID is sanitized with absint below. if ( ! is_array( $raw_ids ) || empty( $raw_ids ) ) { wp_send_json_error( array( 'message' => __( 'No categories selected.', 'ai-content-by-parallax' ) ), 400 ); } $results = array(); foreach ( $raw_ids as $raw_id ) { $term_id = absint( $raw_id ); if ( ! $term_id ) { continue; } $term = get_term( $term_id, 'product_cat' ); if ( ! $term || is_wp_error( $term ) ) { continue; } $currently_excluded = self::is_category_excluded( $term_id ); if ( $currently_excluded ) { delete_term_meta( $term_id, self::META_EXCLUDED ); } else { update_term_meta( $term_id, self::META_EXCLUDED, '1' ); } $now_excluded = ! $currently_excluded; $results[] = array( 'term_id' => $term_id, 'excluded' => $now_excluded, 'optimized' => ! $now_excluded ? AjaxRequestHandlers::is_term_optimized_for_optimize_all( $term_id ) : false, ); } wp_send_json_success( array( 'categories' => $results ) ); } /** * Check whether a category is excluded (locked) from optimization. * * @param int $term_id Term ID. * @return bool */ public static function is_category_excluded( int $term_id ): bool { return '1' === (string) get_term_meta( $term_id, self::META_EXCLUDED, true ); } }