diff --git a/.psalm/psalm-baseline.xml b/.psalm/psalm-baseline.xml index fbc28baec..7eaa926fc 100644 --- a/.psalm/psalm-baseline.xml +++ b/.psalm/psalm-baseline.xml @@ -204,9 +204,6 @@ getMessage() )]]> - - - diff --git a/assets/css/admin-notices.scss b/assets/css/admin-notices.scss index 4ccb73f9b..14a381452 100644 --- a/assets/css/admin-notices.scss +++ b/assets/css/admin-notices.scss @@ -51,8 +51,19 @@ $notice-success: #43af99; aspect-ratio: 1; } + &__label { + font-weight: 600; + background: var(--wpjm-brand-color-shade-4); + color: var(--wpjm-brand-color-primary); + border-radius: 40px; + padding: 6px 12px; + display: inline-block; + text-transform: uppercase; + align-self: flex-start; + font-size: 12px; + } + &__heading { - margin: 0 0 8px 0; font-weight: 700; font-size: 16px; letter-spacing: -0.36px; @@ -62,12 +73,26 @@ $notice-success: #43af99; &__message { flex: 1 1 min-content; align-self: center; + display: flex; + flex-direction: column; + gap: 10px; min-width: min(100%, 400px); + line-height: 1.5; &, p { font-size: 14px; } + ul { + margin: 0; + padding-left: 10px; + list-style-type: '•'; + } + li { + padding-left: 10px; + } + + } &__actions { @@ -108,6 +133,35 @@ $notice-success: #43af99; } } + &--landing { + padding: 40px 40px; + align-items: flex-start; + flex-direction: row; + justify-content: space-between; + } + &--landing &__top { + flex-direction: column; + gap: 20px; + align-items: flex-start; + } + + &--landing &__message { + gap: 20px; + } + + &--landing &__heading { + font-size: 40px; + letter-spacing: -0.8px; + } + + &--landing &__image { + max-width: 50%; + } + &--landing &__image img { + max-width: 100%; + max-height: 400px; + } + } .wpjm-addon-update-notice-info { diff --git a/includes/admin/class-release-notice.php b/includes/admin/class-release-notice.php new file mode 100644 index 000000000..8cdacc550 --- /dev/null +++ b/includes/admin/class-release-notice.php @@ -0,0 +1,98 @@ +set_setting( \WP_Job_Manager\Stats::OPTION_ENABLE_STATS, '1' ); + } + + /** + * Add a release notice for the 2.3.0 release. + * + * @param array $notices + * + * @return array + */ + public static function add_release_notice( $notices ) { + + // Make sure to update the version number in the notice ID when changing this notice for a new release. + $action_url = \WP_Job_Manager_Admin_Notices::get_action_url( 'enable_stats', self::NOTICE_ID ); + $notices[ self::NOTICE_ID ] = [ + 'type' => 'site-wide', + 'label' => 'New', + 'heading' => 'Job Statistics', + 'message' => '
' . __( + ' +

Collect analytics about site visitors for each job listing. Display the detailed statistics in the refreshed jobs dashboard.

+
    +
  • Tracks page views and unique visitors, search impressions and apply button clicks.
  • +
  • Adds a new overlay to the employer dashboard with aggregated statistics and a daily breakdown chart.
  • +
  • Integrates with Job Alerts, Applications, and Bookmarks add-ons.
  • +
  • GDPR-compliant, with no personal user information collected.
  • +
+', + 'wp-job-manager' + ) . '
', + 'actions' => [ + [ + 'label' => __( 'Enable', 'wp-job-manager' ), + 'url' => $action_url, + 'primary' => true, + ], + [ + 'label' => __( 'Dismiss', 'wp-job-manager' ), + 'url' => \WP_Job_Manager_Admin_Notices::get_dismiss_url( self::NOTICE_ID ), + 'primary' => false, + ], + [ + 'label' => __( 'See what\'s new in 2.3', 'wp-job-manager' ), + 'url' => 'https://wpjobmanager.com/2024/03/27/new-in-2-3-job-statistics/', + 'class' => 'is-link', + ], + ], + 'icon' => false, + 'level' => 'landing', + 'image' => 'https://wpjobmanager.com/wp-content/uploads/2024/03/jm-230-release.png', + 'dismissible' => false, + 'extra_details' => '', + 'conditions' => [ + [ + 'type' => 'screens', + 'screens' => [ 'edit-job_listing' ], + ], + ], + ]; + + return $notices; + } + +} diff --git a/includes/admin/class-wp-job-manager-admin-notices.php b/includes/admin/class-wp-job-manager-admin-notices.php index d00c23ab3..bfcc63302 100644 --- a/includes/admin/class-wp-job-manager-admin-notices.php +++ b/includes/admin/class-wp-job-manager-admin-notices.php @@ -10,6 +10,7 @@ } use WP_Job_Manager\Admin\Notices_Conditions_Checker; +use WP_Job_Manager\Admin\Release_Notice; use WP_Job_Manager\WP_Job_Manager_Com_API; /** @@ -46,6 +47,9 @@ class WP_Job_Manager_Admin_Notices { 'class' => [], ], 'em' => [], + 'ul' => [], + 'ol' => [], + 'li' => [], 'p' => [], 'strong' => [], ]; @@ -62,12 +66,15 @@ class WP_Job_Manager_Admin_Notices { */ public static function init() { add_action( 'admin_notices', [ __CLASS__, 'display_notices' ] ); - add_action( 'wp_loaded', [ __CLASS__, 'dismiss_notices' ] ); + add_action( 'wp_loaded', [ __CLASS__, 'handle_notice_action' ], 10 ); + add_action( 'wp_loaded', [ __CLASS__, 'dismiss_notices' ], 11 ); add_action( 'wp_ajax_wp_job_manager_dismiss_notice', [ __CLASS__, 'handle_notice_dismiss' ] ); - add_filter( 'wpjm_admin_notices', [ __CLASS__, 'maybe_add_addon_update_available_notice' ], 10, 1 ); - add_filter( 'wpjm_admin_notices', [ __CLASS__, 'paid_listings_renewal_notice' ], 10, 1 ); - add_filter( 'wpjm_admin_notices', [ __CLASS__, 'we_have_addons_notice' ], 10, 1 ); - add_filter( 'wpjm_admin_notices', [ __CLASS__, 'maybe_add_core_setup_notice' ], 10, 1 ); + add_filter( 'wpjm_admin_notices', [ __CLASS__, 'maybe_add_addon_update_available_notice' ] ); + add_filter( 'wpjm_admin_notices', [ __CLASS__, 'paid_listings_renewal_notice' ] ); + add_filter( 'wpjm_admin_notices', [ __CLASS__, 'we_have_addons_notice' ] ); + add_filter( 'wpjm_admin_notices', [ __CLASS__, 'maybe_add_core_setup_notice' ] ); + + Release_Notice::init(); } /** @@ -161,11 +168,11 @@ public static function init_core_notices() { } /** - * Dismiss notices as requested by user. Inspired by WooCommerce's approach. + * Dismiss a notice. */ public static function dismiss_notices() { if ( isset( $_GET['wpjm_hide_notice'] ) && isset( $_GET['_wpjm_notice_nonce'] ) ) { - if ( ! wp_verify_nonce( wp_unslash( $_GET['_wpjm_notice_nonce'] ), 'job_manager_hide_notices_nonce' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce should not be modified. + if ( ! wp_verify_nonce( wp_unslash( $_GET['_wpjm_notice_nonce'] ), 'job_manager_notices_nonce' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce should not be modified. wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'wp-job-manager' ) ); } @@ -173,15 +180,36 @@ public static function dismiss_notices() { wp_die( esc_html__( 'You don’t have permission to do this.', 'wp-job-manager' ) ); } - $hide_notice = sanitize_key( wp_unslash( $_GET['wpjm_hide_notice'] ) ); + $notice_id = sanitize_key( wp_unslash( $_GET['wpjm_hide_notice'] ) ); - self::remove_notice( $hide_notice ); + self::dismiss_notice( $notice_id ); - wp_safe_redirect( remove_query_arg( [ 'wpjm_hide_notice', '_wpjm_notice_nonce' ] ) ); + wp_safe_redirect( remove_query_arg( [ 'wpjm_hide_notice', '_wpjm_notice_nonce', 'wpjm_notice_action' ] ) ); exit; } } + /** + * Call an action when a notice button is clicked. + */ + public static function handle_notice_action() { + + if ( isset( $_GET['wpjm_notice_action'] ) && isset( $_GET['_wpjm_notice_nonce'] ) ) { + + $action = sanitize_key( wp_unslash( $_GET['wpjm_notice_action'] ) ); + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce should not be modified. + if ( ! wp_verify_nonce( wp_unslash( $_GET['_wpjm_notice_nonce'] ), 'job_manager_notices_nonce' ) ) { + wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'wp-job-manager' ) ); + } + + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( esc_html__( 'You don’t have permission to do this.', 'wp-job-manager' ) ); + } + + do_action( 'job_manager_action_' . $action ); + } + } + /** * Returns all the registered notices. * This includes notices coming from wpjobmanager.com and notices added by other plugins using the `job_manager_admin_notices` filter. @@ -208,6 +236,25 @@ public static function get_notices() { return $all_notices; } + /** + * Get a notice by ID. + * + * @param string $notice_id Notice ID. + * + * @return array|false + */ + public static function get_notice( $notice_id ) { + $notices = self::get_notices(); + + if ( ! isset( $notices[ $notice_id ] ) ) { + return false; + } + + $notice = self::normalize_notice( $notices[ $notice_id ] ); + + return $notice; + } + /** * Check if a notice was dismissed. * @@ -226,7 +273,6 @@ public static function is_dismissed( $notice_id, $is_user_notification ) { * Note: For internal use only. Do not call manually. */ public static function display_notices() { - /** * Allows WPJM related plugins to set up their notice hooks. * @@ -332,6 +378,7 @@ private static function get_dismissed_notices( $is_user_notification ) { * @param bool $is_user_notification True if we are setting user notifications (vs site-wide notifications). */ private static function save_dismissed_notices( $dismissed_notices, $is_user_notification ) { + $dismissed_notices = array_unique( $dismissed_notices ); if ( $is_user_notification ) { update_user_meta( get_current_user_id(), self::DISMISSED_NOTICES_USER_META, $dismissed_notices ); @@ -348,31 +395,43 @@ private static function save_dismissed_notices( $dismissed_notices, $is_user_not public static function handle_notice_dismiss() { check_ajax_referer( self::DISMISS_NOTICE_ACTION, 'nonce' ); - $notices = self::get_notices(); $notice_id = isset( $_POST['notice'] ) ? sanitize_text_field( wp_unslash( $_POST['notice'] ) ) : false; - if ( ! $notice_id || ! isset( $notices[ $notice_id ] ) ) { + if ( ! $notice_id ) { return; } - $notice = self::normalize_notice( $notices[ $notice_id ] ); + $notice = self::get_notice( $notice_id ); - $is_dismissible = $notice['dismissible']; - $is_user_notification = 'user' === $notice['type']; - if ( - ! $is_dismissible - || ( ! $is_user_notification && ! current_user_can( 'manage_options' ) ) - ) { + if ( ! $notice || ! $notice['dismissible'] || ( 'user' !== $notice['type'] && ! current_user_can( 'manage_options' ) ) ) { wp_die( '', '', 403 ); } + self::dismiss_notice( $notice_id ); + exit; + } + + /** + * Save notice state as dismissed. + * + * @param string $notice_id Notice ID. + * + * @return void + */ + public static function dismiss_notice( $notice_id ) { + + $notice = self::get_notice( $notice_id ); + if ( ! $notice ) { + return; + } + + $is_user_notification = 'user' === $notice['type']; + $dismissed_notices = self::get_dismissed_notices( $is_user_notification ); $dismissed_notices[] = $notice_id; self::save_dismissed_notices( $dismissed_notices, $is_user_notification ); - do_action( 'wp_job_manager_notice_dismissed', $notices[ $notice_id ], $notice_id, $is_user_notification ); - - exit; + do_action( 'wp_job_manager_notice_dismissed', $notice, $notice_id, $is_user_notification ); } /** @@ -453,7 +512,7 @@ public static function maybe_add_core_setup_notice( $notices ) { ], [ 'primary' => false, - 'url' => esc_url( wp_nonce_url( add_query_arg( 'wpjm_hide_notice', self::NOTICE_CORE_SETUP ), 'job_manager_hide_notices_nonce', '_wpjm_notice_nonce' ) ), + 'url' => self::get_dismiss_url( self::NOTICE_CORE_SETUP ), 'label' => __( 'Skip Setup*', 'wp-job-manager' ), ], ], @@ -500,6 +559,34 @@ public static function maybe_add_addon_update_available_notice( $notices ) { return $notices; } + /** + * Get URL for dismiss action for a notice. + * + * @param string $notice_name + * @return string + */ + public static function get_dismiss_url( $notice_name ) { + return wp_nonce_url( add_query_arg( [ 'wpjm_hide_notice' => $notice_name ] ), 'job_manager_notices_nonce', '_wpjm_notice_nonce' ); + } + + /** + * Get URL for a custom action for a notice. + * + * @param string $action_name Name of action. + * @param string $dismiss_notice Optional notice ID, if this action should also dismiss the notice. + * @return string + */ + public static function get_action_url( $action_name, $dismiss_notice = null ) { + + $base_url = ''; + + if ( $dismiss_notice ) { + $base_url = self::get_dismiss_url( $dismiss_notice ); + } + + return wp_nonce_url( add_query_arg( [ 'wpjm_notice_action' => $action_name ], $base_url ), 'job_manager_notices_nonce', '_wpjm_notice_nonce' ); + } + /** * Gets the current admin notices to be displayed. * @@ -595,13 +682,8 @@ private static function render_notice( $notice_id, $notice ) { $notice['actions'] = []; } - $notice_class = []; - $notice_levels = [ 'error', 'warning', 'success', 'info', 'upsell' ]; - if ( isset( $notice['level'] ) && in_array( $notice['level'], $notice_levels, true ) ) { - $notice_class[] = 'wpjm-admin-notice--' . $notice['level']; - } else { - $notice_class[] = 'wpjm-admin-notice--info'; - } + $notice_class = []; + $notice_class[] = 'wpjm-admin-notice--' . ( $notice['level'] ?? 'info' ); $is_dismissible = $notice['dismissible'] ?? true; $notice_wrapper_extra = ''; @@ -625,6 +707,11 @@ private static function render_notice( $notice_id, $notice ) { echo 'WP Job Manager Icon'; } echo '
'; + if ( ! empty( $notice['label'] ) ) { + echo '
'; + echo wp_kses( $notice['label'], self::ALLOWED_HTML ); + echo '
'; + } if ( ! empty( $notice['heading'] ) ) { echo '
'; echo wp_kses( $notice['heading'], self::ALLOWED_HTML ); @@ -639,7 +726,7 @@ private static function render_notice( $notice_id, $notice ) { continue; } - $button_class = ! isset( $action['primary'] ) || $action['primary'] ? 'is-primary' : 'is-outline'; + $button_class = $action['class'] ?? ( ! isset( $action['primary'] ) || $action['primary'] ? 'is-primary' : 'is-outline' ); echo ''; echo esc_html( $action['label'] ); @@ -653,6 +740,12 @@ private static function render_notice( $notice_id, $notice ) { echo '
'; echo '
'; + if ( ! empty( $notice['image'] ) ) { + echo '
'; + echo ''; + echo '
'; + } + if ( ! empty( $notice['extra_details'] ) ) { echo '
'; echo wp_kses( $notice['extra_details'], self::ALLOWED_HTML ); diff --git a/includes/class-wp-job-manager-install.php b/includes/class-wp-job-manager-install.php index c289376ab..31630809b 100644 --- a/includes/class-wp-job-manager-install.php +++ b/includes/class-wp-job-manager-install.php @@ -5,6 +5,8 @@ * @package wp-job-manager */ +use WP_Job_Manager\Admin\Release_Notice; + if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -74,6 +76,9 @@ public static function install() { $permalink_options = (array) json_decode( get_option( 'job_manager_permalinks', '[]' ), true ); $permalink_options['jobs_archive'] = ''; update_option( 'job_manager_permalinks', wp_json_encode( $permalink_options ) ); + + update_option( \WP_Job_Manager\Stats::OPTION_ENABLE_STATS, true ); + \WP_Job_Manager_Admin_Notices::dismiss_notice( Release_Notice::NOTICE_ID ); } \WP_Job_Manager\Stats::instance()->migrate_db();