From 08692178f8cf83aad389e51e8602a2097a4f6e78 Mon Sep 17 00:00:00 2001 From: Peter Kiss Date: Wed, 14 Feb 2024 15:14:14 +0100 Subject: [PATCH 1/4] Update job dashboard template (#2754) --- assets/css/job-dashboard.scss | 98 +++++++++++++ assets/css/ui.dialog.scss | 5 +- assets/css/ui.elements.scss | 81 ++++++++++- assets/css/ui.neutral.scss | 21 ++- assets/css/ui.notice.scss | 24 +--- includes/class-wp-job-manager-shortcodes.php | 68 +++++++-- includes/ui/class-notice.php | 2 +- includes/ui/class-ui-elements.php | 69 ++++++++- phpcs.xml.dist | 1 + templates/job-dashboard.php | 144 +++++++++---------- templates/notice.php | 2 +- webpack.config.js | 1 + 12 files changed, 396 insertions(+), 120 deletions(-) create mode 100644 assets/css/job-dashboard.scss diff --git a/assets/css/job-dashboard.scss b/assets/css/job-dashboard.scss new file mode 100644 index 000000000..ac9dd77d3 --- /dev/null +++ b/assets/css/job-dashboard.scss @@ -0,0 +1,98 @@ + +@import "mixins"; + +#job-manager-job-dashboard { + +} + +.jm-dashboard-job, .jm-dashboard-header { + display: flex; + align-items: center; + padding: var(--jm-ui-space-sm); + gap: var(--jm-ui-space-sm); + margin: var(--jm-ui-space-sm) 0; + font-size: var(--jm-ui-font-size-m); +} + +.jm-dashboard-header { + color: fadeCurrentColor(85%); + margin-bottom: unset; + padding-bottom: unset; +} + +.jm-dashboard__intro { + display: flex; + justify-content: space-between; + align-items: baseline; + flex-wrap: wrap; + gap: var(--jm-ui-space-sm); +} + +.jm-dashboard-job { + border: var(--jm-ui-border-size) solid var(--jm-ui-border-light); +} + +.jm-dashboard-job-column { + flex: 1 1 calc(50% - var(--jm-ui-space-sm)); + min-width: 0; +} + +.jm-dashboard small { + font-size: var(--jm-ui-font-size-s); +} + +.jm-dashboard-job-column.job_title { + flex: 1 1 200%; + + .job-status { + text-transform: uppercase; + font-weight: 500; + font-size: var(--jm-ui-font-size-s); + line-height: var(--jm-ui-icon-size-s); + color: fadeCurrentColor( 80% ); + + .jm-ui-icon { + width: var(--jm-ui-icon-size-s); + height: var(--jm-ui-icon-size-s); + } + } +} + + +.jm-dashboard-job-column.actions { + flex: 0.5 1 100%; + text-align: right; +} + +.jm-dashboard-job-column a.job-title { + font-weight: 600; + font-size: var(--jm-ui-font-size); + text-decoration: unset; +} + +.jm-dashboard-job-column a.job-title:hover { + text-decoration: underline; +} + +.jm-dashboard-action { + display: block; + text-decoration: none; + +} +.jm-dashboard-action:where(:not(:hover):not(:focus)) { + color: inherit; +} + +.job-dashboard-action-delete { + color: var(--jm-ui-danger-color); +} + +@media (max-width: 600px) { + .jm-dashboard-job { + flex-wrap: wrap; + } + + .jm-dashboard-header { + display: none; + } +} diff --git a/assets/css/ui.dialog.scss b/assets/css/ui.dialog.scss index 0e66c911e..6ba949e3f 100644 --- a/assets/css/ui.dialog.scss +++ b/assets/css/ui.dialog.scss @@ -80,10 +80,7 @@ opacity: 0.7; .jm-ui-button__icon { - background-color: currentColor; - mask: var(--jm-ui-svg-close) no-repeat center center; - width: var(--jm-ui-icon-size); - height: var(--jm-ui-icon-size); + mask-image: var(--jm-ui-svg-close); } } diff --git a/assets/css/ui.elements.scss b/assets/css/ui.elements.scss index 326c5f4e9..ac3903b94 100644 --- a/assets/css/ui.elements.scss +++ b/assets/css/ui.elements.scss @@ -90,6 +90,13 @@ } } +.jm-ui-button__icon { + background-color: currentColor; + mask: var(--jm-ui-svg-close) no-repeat center center; + width: var(--jm-ui-icon-size); + height: var(--jm-ui-icon-size); +} + .jm-ui-link { border-radius: 2px; @@ -102,7 +109,7 @@ } } -.jm-ui-actions { +.jm-ui-actions-row { display: flex; flex-direction: row; gap: var(--jm-ui-space-s); @@ -115,3 +122,75 @@ margin-left: calc(-1 * var(--jm-ui-space-s)); } } + +.jm-ui-icon { + display: inline-block; + flex: 0 0 auto; + width: var(--jm-ui-icon-size); + height: var(--jm-ui-icon-size); + mask-size: 100% 100%; + + &[data-icon=check] { + background-color: currentColor; + mask-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' fill=\'none\' viewBox=\'0 0 24 24\'%3e%3cg stroke-width=\'1.5\'%3e%3cpath stroke=\'%231E1E1E\' d=\'M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z\'/%3e%3cpath stroke=\'black\' d=\'m15.96 8.18-5.34 7.18-3.1-2.3\'/%3e%3c/g%3e%3c/svg%3e'); + } + + &[data-icon=check-circle] { + background-color: currentColor; + mask-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' fill=\'none\' viewBox=\'0 0 24 24\'%3e%3cg stroke-width=\'1.5\'%3e%3cpath stroke=\'%231E1E1E\' d=\'M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z\'/%3e%3cpath stroke=\'black\' d=\'m15.96 8.18-5.34 7.18-3.1-2.3\'/%3e%3c/g%3e%3c/svg%3e'); + } + + &[data-icon=check] { + background-color: currentColor; + mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'%3e%3cpath fill='black' fill-rule='evenodd' d='m17.93 7.98-7.2 9.68-4.52-3.36.9-1.2 3.31 2.46 6.3-8.48 1.2.9Z' clip-rule='evenodd'/%3e%3c/svg%3e"); + } + + &[data-icon=star] { + background-color: currentColor; + mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'%3e%3cpath fill='black' d='M11.78 4.45a.25.25 0 0 1 .44 0l2.07 4.2c.04.07.11.12.2.13l4.62.68c.2.02.28.28.14.42l-3.35 3.26a.25.25 0 0 0-.07.23l.79 4.6c.03.2-.18.36-.37.27l-4.13-2.18a.25.25 0 0 0-.24 0l-4.13 2.18a.25.25 0 0 1-.37-.27l.8-4.6a.25.25 0 0 0-.08-.23L4.75 9.88a.25.25 0 0 1 .14-.42l4.63-.68a.25.25 0 0 0 .19-.13l2.07-4.2Z'/%3e%3c/svg%3e"); + } + + &[data-icon=info] { + background-color: currentColor; + mask-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' fill=\'none\' viewBox=\'0 0 24 24\'%3e%3ccircle cx=\'12\' cy=\'12\' r=\'8\' stroke=\'black\' stroke-width=\'1.5\'/%3e%3cpath fill=\'black\' d=\'M11 11h2v6h-2zm0-4h2v2h-2z\'/%3e%3c/svg%3e'); + } + + &[data-icon=alert] { + background-color: currentColor; + mask-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' fill=\'none\' viewBox=\'0 0 24 24\'%3e%3cpath stroke=\'%231E1E1E\' stroke-width=\'1.5\' d=\'M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z\'/%3e%3cpath fill=\'%231E1E1E\' d=\'M13 7h-2v6h2V7Zm0 8h-2v2h2v-2Z\'/%3e%3c/svg%3e'); + } +} + +/* + * Action menu. + */ +.jm-ui-actions-menu { + position: relative; + display: inline-block; +} + +.jm-ui-action-menu__open-button { + cursor: pointer; + + .jm-ui-button__icon { + mask-image: var(--jm-ui-svg-ellipsis-v); + } +} + +.jm-ui-action-menu__content { + position: absolute; + right: 0; + top: calc(100% + 2px); + border: var(--jm-ui-border-size) solid var(--jm-ui-border-strong); + border-radius: var(--jm-ui-radius); + background-color: var(--jm-ui-background-color, #fff); + box-shadow: var(--jm-ui-shadow-popover); + padding: var(--jm-ui-space-s); + z-index: 10; + font-size: var(--jm-ui-font-size-s); + text-align: left; + white-space: nowrap; + + display: flex; + flex-direction: column; +} diff --git a/assets/css/ui.neutral.scss b/assets/css/ui.neutral.scss index 34500055a..5076c739b 100644 --- a/assets/css/ui.neutral.scss +++ b/assets/css/ui.neutral.scss @@ -13,15 +13,19 @@ --jm-ui-radius-4x: calc(4 * var(--jm-ui-radius)); --jm-ui-accent-color: inherit; + --jm-ui-danger-color: #cc1818; + --jm-ui-error-color: #cc1818; + --jm-ui-info-color: #4e71ec; + --jm-ui-success-color: #4ab866; --jm-ui-accent-color-contrast: #ffffff; --jm-ui-button-color: var(--jm-ui-accent-color); --jm-ui-button-contrast: var(--jm-ui-accent-color-contrast, #ffffff); --jm-ui-link-color: var(--jm-ui-accent-color, inherit); - --jm-ui-notice-error: #cc1818; - --jm-ui-notice-info: #4e71ec; - --jm-ui-notice-success: #4ab866; + --jm-ui-notice-error: var(--jm-ui-danger-color); + --jm-ui-notice-info: var(--jm-ui-info-color); + --jm-ui-notice-success: var(--jm-ui-success-color); --jm-ui-notice-strong: var(--jm-ui-border-strong); --jm-ui-notice-shadow: none; @@ -43,17 +47,24 @@ --jm-ui-space-xxl: calc(24 * var(--jm-ui-space-base)); // 96px --jm-ui-font-size: 16px; + --jm-ui-font-size-m: 14px; + --jm-ui-font-size-s: 12px; --jm-ui-heading-font-size: 20px; --jm-ui-large-font-size: 24px; --jm-ui-button-font-size: 14px; --jm-ui-icon-size: 24px; + --jm-ui-icon-size-m: 20px; + --jm-ui-icon-size-s: 18px; --jm-ui-shadow-modal: 0 0.7px 1px 0 rgba(0, 0, 0, 0.15), 0 2.7px 3.8px -0.2px rgba(0, 0, 0, 0.15), 0px 5.5px 7.8px -0.3px rgba(0, 0, 0, 0.15), 0.1px 11.5px 16.4px -0.5px rgba(0, 0, 0, 0.15); + --jm-ui-shadow-popover: 0px 2px 6px 0px rgba(0, 0, 0, 0.05); + --jm-ui-svg-close: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24'%3e%3cpath fill='black' d='m12 13.06 3.71 3.71 1.06-1.06-3.7-3.71 3.7-3.71-1.06-1.06-3.71 3.7-3.71-3.7-1.06 1.06 3.7 3.71-3.7 3.71 1.06 1.06 3.71-3.7Z'/%3e%3c/svg%3e"); - --jm-ui-svg-arrow-down: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='32' height='6' viewBox='0 0 16 10'%3e%3cpath fill='currentColor' fill-rule='evenodd' d='M0 1.6 1.53 0 8 6.95 14.5 0 16 1.6 8 10 0 1.6Z' clip-rule='evenodd'/%3e%3c/svg%3e"); - --jm-ui-svg-check: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'%3e%3cpath stroke='%231E1E1E' stroke-width='1.5' d='m18.93 6-8.9 11.97-5.16-3.84'/%3e%3c/svg%3e"); + --jm-ui-svg-arrow-down: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='32' height='6' viewBox='0 0 16 10'%3e%3cpath fill='black' fill-rule='evenodd' d='M0 1.6 1.53 0 8 6.95 14.5 0 16 1.6 8 10 0 1.6Z' clip-rule='evenodd'/%3e%3c/svg%3e"); + --jm-ui-svg-check: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'%3e%3cpath stroke='black' stroke-width='1.5' d='m18.93 6-8.9 11.97-5.16-3.84'/%3e%3c/svg%3e"); + --jm-ui-svg-ellipsis-v: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'%3e%3cpath fill='black' fill-rule='evenodd' d='M11 19v-2h2v2h-2Zm0-6v-2h2v2h-2Zm0-6V5h2v2h-2Z' clip-rule='evenodd'/%3e%3c/svg%3e"); } diff --git a/assets/css/ui.notice.scss b/assets/css/ui.notice.scss index 204e3e0b7..4817d82bd 100644 --- a/assets/css/ui.notice.scss +++ b/assets/css/ui.notice.scss @@ -33,7 +33,7 @@ &.color-#{$color} { border-color: var(--jm-ui-notice-#{$color}); background-color: color-mix(in srgb, transparent, var(--jm-ui-notice-#{$color}) var(--jm-ui-background-opacity)); - .jm-notice__icon { + .jm-ui-icon { color: var(--jm-ui-notice-#{$color}); } } @@ -91,28 +91,6 @@ } } -.jm-notice__icon { - flex: 0 0 auto; - width: var(--jm-ui-icon-size); - height: var(--jm-ui-icon-size); - mask-size: 100% 100%; - - &[data-icon=check] { - background-color: currentColor; - mask-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' fill=\'none\' viewBox=\'0 0 24 24\'%3e%3cg stroke-width=\'1.5\'%3e%3cpath stroke=\'%231E1E1E\' d=\'M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z\'/%3e%3cpath stroke=\'black\' d=\'m15.96 8.18-5.34 7.18-3.1-2.3\'/%3e%3c/g%3e%3c/svg%3e'); - } - - &[data-icon=info] { - background-color: currentColor; - mask-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' fill=\'none\' viewBox=\'0 0 24 24\'%3e%3ccircle cx=\'12\' cy=\'12\' r=\'8\' stroke=\'black\' stroke-width=\'1.5\'/%3e%3cpath fill=\'black\' d=\'M11 11h2v6h-2zm0-4h2v2h-2z\'/%3e%3c/svg%3e'); - } - - &[data-icon=alert] { - background-color: currentColor; - mask-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' fill=\'none\' viewBox=\'0 0 24 24\'%3e%3cpath stroke=\'%231E1E1E\' stroke-width=\'1.5\' d=\'M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z\'/%3e%3cpath fill=\'%231E1E1E\' d=\'M13 7h-2v6h2V7Zm0 8h-2v2h2v-2Z\'/%3e%3c/svg%3e'); - } -} - .jm-notice__header { display: flex; flex-direction: row; diff --git a/includes/class-wp-job-manager-shortcodes.php b/includes/class-wp-job-manager-shortcodes.php index a2eef4b79..4d701e0c5 100644 --- a/includes/class-wp-job-manager-shortcodes.php +++ b/includes/class-wp-job-manager-shortcodes.php @@ -5,6 +5,9 @@ * @package wp-job-manager */ +use WP_Job_Manager\UI\Notice; +use WP_Job_Manager\UI\UI_Elements; + if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -71,6 +74,9 @@ public function __construct() { add_shortcode( 'job_apply', [ $this, 'output_job_apply' ] ); add_filter( 'paginate_links', [ $this, 'filter_paginate_links' ], 10, 1 ); + + add_action( 'job_manager_job_dashboard_column_date', [ $this, 'job_dashboard_date_column_expires' ] ); + add_action( 'job_manager_job_dashboard_column_job_title', [ $this, 'job_dashboard_title_column_status' ] ); } /** @@ -214,7 +220,7 @@ public function job_dashboard_handler() { // Message. // translators: Placeholder %s is the job listing title. - $this->job_dashboard_message = '
' . wp_kses_post( sprintf( __( '%s has been filled', 'wp-job-manager' ), wpjm_get_the_job_title( $job ) ) ) . '
'; + $this->job_dashboard_message = Notice::success( sprintf( __( '%s has been filled', 'wp-job-manager' ), wpjm_get_the_job_title( $job ) ) ); break; case 'mark_not_filled': // Check status. @@ -227,7 +233,7 @@ public function job_dashboard_handler() { // Message. // translators: Placeholder %s is the job listing title. - $this->job_dashboard_message = '
' . wp_kses_post( sprintf( __( '%s has been marked as not filled', 'wp-job-manager' ), wpjm_get_the_job_title( $job ) ) ) . '
'; + $this->job_dashboard_message = Notice::success( sprintf( __( '%s has been marked as not filled', 'wp-job-manager' ), wpjm_get_the_job_title( $job ) ) ); break; case 'delete': // Trash it. @@ -235,7 +241,7 @@ public function job_dashboard_handler() { // Message. // translators: Placeholder %s is the job listing title. - $this->job_dashboard_message = '
' . wp_kses_post( sprintf( __( '%s has been deleted', 'wp-job-manager' ), wpjm_get_the_job_title( $job ) ) ) . '
'; + $this->job_dashboard_message = Notice::success( sprintf( __( '%s has been deleted', 'wp-job-manager' ), wpjm_get_the_job_title( $job ) ) ); break; case 'duplicate': @@ -288,10 +294,10 @@ public function job_dashboard_handler() { */ $success_message = apply_filters( 'job_manager_job_dashboard_success_message', '', $action, $job_id ); if ( $success_message ) { - $this->job_dashboard_message = '
' . $success_message . '
'; + $this->job_dashboard_message = Notice::success( $success_message ); } } catch ( Exception $e ) { - $this->job_dashboard_message = '
' . wp_kses_post( $e->getMessage() ) . '
'; + $this->job_dashboard_message = Notice::error( $e->getMessage() ); } } } @@ -375,6 +381,8 @@ public function job_dashboard( $atts ) { ); $posts_per_page = $new_atts['posts_per_page']; + WP_Job_Manager::register_style( 'wp-job-manager-job-dashboard', 'css/job-dashboard.css', [ 'wp-job-manager-ui' ] ); + wp_enqueue_style( 'wp-job-manager-job-dashboard' ); wp_enqueue_script( 'wp-job-manager-job-dashboard' ); ob_start(); @@ -397,15 +405,13 @@ public function job_dashboard( $atts ) { // Cache IDs for access check later on. $this->job_dashboard_job_ids = wp_list_pluck( $jobs->posts, 'ID' ); - echo wp_kses_post( $this->job_dashboard_message ); + echo '
' . wp_kses_post( $this->job_dashboard_message ) . '
'; $job_dashboard_columns = apply_filters( 'job_manager_job_dashboard_columns', [ 'job_title' => __( 'Title', 'wp-job-manager' ), - 'filled' => __( 'Filled?', 'wp-job-manager' ), 'date' => __( 'Date', 'wp-job-manager' ), - 'expires' => __( 'Listing Expires', 'wp-job-manager' ), ] ); @@ -990,6 +996,52 @@ public function output_job_apply( $atts ) { return ob_get_clean(); } + + /** + * Add expiration details to the job dashboard date column. + * + * @param \WP_Post $job + * + * @output string + */ + public function job_dashboard_date_column_expires( $job ) { + $expiration = WP_Job_Manager_Post_Types::instance()->get_job_expiration( $job ); + + if ( 'publish' === $job->post_status && ! empty( $expiration ) ) { + + // translators: Placeholder is the expiration date of the job listing. + echo '
' . UI_Elements::rel_time( $expiration, __( 'Expires in %s', 'wp-job-manager' ) ) . '
'; + } + } + + /** + * Add job status to the job dashboard title column. + * + * @param \WP_Post $job + * + * @output string + */ + public function job_dashboard_title_column_status( $job ) { + + echo '
'; + + $status = []; + + if ( is_position_filled( $job ) ) { + $status[] = '' . esc_html__( 'Filled', 'wp-job-manager' ) . ''; + } + + if ( is_position_featured( $job ) && 'publish' === $job->post_status ) { + $status[] = '' . esc_html__( 'Featured', 'wp-job-manager' ) . ''; + } + + $status[] = '' . esc_html( get_the_job_status( $job ) ) . ''; + + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped above. + echo implode( ', ', $status ); + + echo '
'; + } } WP_Job_Manager_Shortcodes::instance(); diff --git a/includes/ui/class-notice.php b/includes/ui/class-notice.php index 116e7d121..690927c00 100644 --- a/includes/ui/class-notice.php +++ b/includes/ui/class-notice.php @@ -32,7 +32,7 @@ public static function success( $args ) { return self::render( array_merge( - [ 'icon' => 'check' ], + [ 'icon' => 'check-circle' ], $args ) ); diff --git a/includes/ui/class-ui-elements.php b/includes/ui/class-ui-elements.php index 7fe0c4bc2..5c2bdb7bb 100644 --- a/includes/ui/class-ui-elements.php +++ b/includes/ui/class-ui-elements.php @@ -26,10 +26,11 @@ class UI_Elements { * Generate HTML for a notice icon. * * @param string|null $icon Icon name or a safe SVG code. + * @param string $label Accessible icon label. * * @return string Icon HTML. */ - public static function icon( $icon ) { + public static function icon( $icon, $label = '' ) { if ( empty( $icon ) ) { return ''; @@ -39,19 +40,23 @@ public static function icon( $icon ) { $is_classname = preg_match( '/^[\w-]+$/i', $icon ); - $html = '
' . implode( '', $actions_html ) . '
'; + return '
' . implode( '', $actions_html ) . '
'; + + } + + /** + * Generate an actions button with a dropdown menu. + * + * @param string $content Escaped HTML content. + * + * @return string Actions menu HTML. + */ + public static function actions_menu( $content ) { + $label = __( 'Actions', 'wp-job-manager' ); + return << + + + +
+ {$content} +
+ +HTML; + + } + + /** + * Generate HTML for a relative time string. + * + * @param string|\DateTimeInterface|int $time Time string, DateTime object, or timestamp. + * @param string $format_string Sprintf-compatible format string. Should contain a %s placeholder for the time. + * + * @return string + */ + public static function rel_time( $time, $format_string = '%s' ) { + + if ( is_string( $time ) ) { + $timestamp = strtotime( $time ); + } + + if ( $time instanceof \DateTimeInterface ) { + $timestamp = $time->getTimestamp(); + } + + if ( is_numeric( $time ) ) { + $timestamp = $time; + } + + if ( empty( $timestamp ) ) { + return ''; + } + + $abs_time = date_i18n( get_option( 'date_format' ), $timestamp ); + $rel_time = human_time_diff( $timestamp ); + + return ''; } diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 7045b0717..370520b85 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -67,6 +67,7 @@ + diff --git a/templates/job-dashboard.php b/templates/job-dashboard.php index cba5b6230..614d1320d 100644 --- a/templates/job-dashboard.php +++ b/templates/job-dashboard.php @@ -8,97 +8,95 @@ * @author Automattic * @package wp-job-manager * @category Template - * @version 1.41.0 + * @version $$next-version$$ * + * @since $$next-version$$ Switched to a responsive layout. job_manager_job_dashboard_column_{$key} action is called for all columns. * @since 1.34.4 Available job actions are passed in an array (`$job_actions`, keyed by job ID) and not generated in the template. * @since 1.35.0 Switched to new date functions. * * @var array $job_dashboard_columns Array of the columns to show on the job dashboard page. - * @var int $max_num_pages Maximum number of pages - * @var WP_Post[] $jobs Array of job post results. - * @var array $job_actions Array of actions available for each job. + * @var int $max_num_pages Maximum number of pages + * @var WP_Post[] $jobs Array of job post results. + * @var array $job_actions Array of actions available for each job. */ +use WP_Job_Manager\UI\UI_Elements; + if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } -$submission_limit = ! empty( get_option( 'job_manager_submission_limit' ) ) ? absint( get_option( 'job_manager_submission_limit' ) ) : false; -$submit_job_form_page_id = get_option( 'job_manager_submit_job_form_page_id' ); +$submit_job_form_page_id = get_option( 'job_manager_submit_job_form_page_id' ); ?> -
-

- - - - $column ) : ?> - - - - - + +
+
+

+ +
+ +
+ +
+
+
+ $column ) : ?> +
+ +
+
+
-
- - +
- +
$column ) : ?> -
+
+ ID ) ) . '">' . ( wpjm_get_the_job_title( $job ) ?? $job->ID ) . ''; + break; + case 'date': + echo '
' . esc_html( wp_date( apply_filters( 'job_manager_get_dashboard_date_format', 'M d, Y' ), get_post_datetime( $job )->getTimestamp() ) ) . '
'; + + break; + } + + do_action( 'job_manager_job_dashboard_column_' . $key, $job ); + + ?> +
- +
+ ID ] ) ) { + foreach ( $job_actions[ $job->ID ] as $action => $value ) { + $action_url = add_query_arg( [ + 'action' => $action, + 'job_id' => $job->ID + ] ); + if ( $value['nonce'] ) { + $action_url = wp_nonce_url( $action_url, $value['nonce'] ); + } + $actions_html .= '' . esc_html( $value['label'] ) . '' . "\n"; + } + } + + echo UI_Elements::actions_menu( $actions_html ); + ?> +
+ - - - - - - - - -
- - post_status == 'publish' ) : ?> - - - () - - - () - - ' : ''; ?> -
    - ID ] ) ) { - foreach ( $job_actions[ $job->ID ] as $action => $value ) { - $action_url = add_query_arg( [ - 'action' => $action, - 'job_id' => $job->ID - ] ); - if ( $value['nonce'] ) { - $action_url = wp_nonce_url( $action_url, $value['nonce'] ); - } - echo '
  • ' . esc_html( $value['label'] ) . '
  • '; - } - } - ?> -
- - getTimestamp() ) ); ?> - - get_job_expiration( $job ); - echo esc_html( $job_expires ? wp_date( get_option( 'date_format' ), $job_expires->getTimestamp() ) : '–' ); - ?> - - - - - -
- -
+
+ $max_num_pages ] ); ?> diff --git a/templates/notice.php b/templates/notice.php index 105a1d77e..ca3906af4 100644 --- a/templates/notice.php +++ b/templates/notice.php @@ -42,7 +42,7 @@ ?> -
+
diff --git a/webpack.config.js b/webpack.config.js index fd64e8baa..b80bb7402 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -23,6 +23,7 @@ const files = { 'css/ui': 'css/ui.scss', 'css/job-listings': 'css/job-listings.scss', 'css/job-submission': 'css/job-submission.scss', + 'css/job-dashboard': 'css/job-dashboard.scss', 'css/setup': 'css/setup.scss', 'css/menu': 'css/menu.scss', }; From 3bf968f33ce11944d4a515dc0a3672c2ea305fdc Mon Sep 17 00:00:00 2001 From: Peter Kiss Date: Wed, 14 Feb 2024 15:23:02 +0100 Subject: [PATCH 2/4] Refactor shortcode class (#2759) --- includes/class-job-dashboard-shortcode.php | 534 +++++++++++++++++++ includes/class-wp-job-manager-shortcodes.php | 501 +---------------- 2 files changed, 557 insertions(+), 478 deletions(-) create mode 100644 includes/class-job-dashboard-shortcode.php diff --git a/includes/class-job-dashboard-shortcode.php b/includes/class-job-dashboard-shortcode.php new file mode 100644 index 000000000..f1fde82e1 --- /dev/null +++ b/includes/class-job-dashboard-shortcode.php @@ -0,0 +1,534 @@ + '25', + ], + $attrs + ); + $posts_per_page = $attrs['posts_per_page']; + + \WP_Job_Manager::register_style( 'wp-job-manager-job-dashboard', 'css/job-dashboard.css', [ 'wp-job-manager-ui' ] ); + wp_enqueue_style( 'wp-job-manager-job-dashboard' ); + wp_enqueue_script( 'wp-job-manager-job-dashboard' ); + + ob_start(); + + // If doing an action, show conditional content if needed.... + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Input is used safely. + $action = isset( $_REQUEST['action'] ) ? sanitize_title( wp_unslash( $_REQUEST['action'] ) ) : false; + if ( ! empty( $action ) ) { + // Show alternative content if a plugin wants to. + if ( has_action( 'job_manager_job_dashboard_content_' . $action ) ) { + do_action( 'job_manager_job_dashboard_content_' . $action, $attrs ); + + return ob_get_clean(); + } + } + + // ....If not show the job dashboard. + $jobs = new \WP_Query( $this->get_job_dashboard_query_args( $posts_per_page ) ); + + // Cache IDs for access check later on. + $this->job_dashboard_job_ids = wp_list_pluck( $jobs->posts, 'ID' ); + + echo '
' . wp_kses_post( $this->job_dashboard_message ) . '
'; + + $job_dashboard_columns = apply_filters( + 'job_manager_job_dashboard_columns', + [ + 'job_title' => __( 'Title', 'wp-job-manager' ), + 'date' => __( 'Date', 'wp-job-manager' ), + ] + ); + + $job_actions = []; + foreach ( $jobs->posts as $job ) { + $job_actions[ $job->ID ] = $this->get_job_actions( $job ); + } + + get_job_manager_template( + 'job-dashboard.php', + [ + 'jobs' => $jobs->posts, + 'job_actions' => $job_actions, + 'max_num_pages' => $jobs->max_num_pages, + 'job_dashboard_columns' => $job_dashboard_columns, + ] + ); + + return ob_get_clean(); + } + + /** + * Get the actions available to the user for a job listing on the job dashboard page. + * + * @param \WP_Post $job The job post object. + * + * @return array + */ + public function get_job_actions( $job ) { + if ( + ! get_current_user_id() + || ! $job instanceof \WP_Post + || \WP_Job_Manager_Post_Types::PT_LISTING !== $job->post_type + || ! $this->is_job_available_on_dashboard( $job ) + ) { + return []; + } + + $base_nonce_action_name = 'job_manager_my_job_actions'; + + $actions = []; + switch ( $job->post_status ) { + case 'publish': + if ( \WP_Job_Manager_Post_Types::job_is_editable( $job->ID ) ) { + $actions['edit'] = [ + 'label' => __( 'Edit', 'wp-job-manager' ), + 'nonce' => false, + ]; + } + if ( is_position_filled( $job ) ) { + $actions['mark_not_filled'] = [ + 'label' => __( 'Mark not filled', 'wp-job-manager' ), + 'nonce' => $base_nonce_action_name, + ]; + } else { + $actions['mark_filled'] = [ + 'label' => __( 'Mark filled', 'wp-job-manager' ), + 'nonce' => $base_nonce_action_name, + ]; + } + if ( + get_option( 'job_manager_renewal_days' ) > 0 + && \WP_Job_Manager_Helper_Renewals::job_can_be_renewed( $job ) + && \WP_Job_Manager_Helper_Renewals::is_wcpl_renew_compatible() + && \WP_Job_Manager_Helper_Renewals::is_spl_renew_compatible() + ) { + $actions['renew'] = [ + 'label' => __( 'Renew', 'wp-job-manager' ), + 'nonce' => $base_nonce_action_name, + ]; + } + + $actions['duplicate'] = [ + 'label' => __( 'Duplicate', 'wp-job-manager' ), + 'nonce' => $base_nonce_action_name, + ]; + break; + case 'expired': + if ( job_manager_get_permalink( 'submit_job_form' ) ) { + $actions['relist'] = [ + 'label' => __( 'Relist', 'wp-job-manager' ), + 'nonce' => $base_nonce_action_name, + ]; + } + break; + case 'pending_payment': + case 'pending': + if ( \WP_Job_Manager_Post_Types::job_is_editable( $job->ID ) ) { + $actions['edit'] = [ + 'label' => __( 'Edit', 'wp-job-manager' ), + 'nonce' => false, + ]; + } + break; + case 'draft': + case 'preview': + $actions['continue'] = [ + 'label' => __( 'Continue Submission', 'wp-job-manager' ), + 'nonce' => $base_nonce_action_name, + ]; + break; + } + + $actions['delete'] = [ + 'label' => __( 'Delete', 'wp-job-manager' ), + 'nonce' => $base_nonce_action_name, + ]; + + /** + * Filter the actions available to the current user for a job on the job dashboard page. + * + * @since 1.0.0 + * + * @param array $actions Actions to filter. + * @param \WP_Post $job Job post object. + */ + $actions = apply_filters( 'job_manager_my_job_actions', $actions, $job ); + + // For backwards compatibility, convert `nonce => true` to the nonce action name. + foreach ( $actions as $key => $action ) { + if ( true === $action['nonce'] ) { + $actions[ $key ]['nonce'] = $base_nonce_action_name; + } + } + + return $actions; + } + + /** + * Filters the url from paginate_links to avoid multiple calls for same action in job dashboard + * + * @param string $link + * + * @return string + */ + public function filter_paginate_links( $link ) { + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Input is used for comparison only. + if ( $this->is_job_dashboard_page() && isset( $_GET['action'] ) && in_array( + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Input is used for comparison only. + $_GET['action'], + [ + 'mark_filled', + 'mark_not_filled', + ], + true + ) ) { + return remove_query_arg( [ 'action', 'job_id', '_wpnonce' ], $link ); + } + + return $link; + } + + /** + * Displays edit job form. + */ + public function edit_job() { + global $job_manager; + + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output should be appropriately escaped in the form generator. + echo $job_manager->forms->get_form( 'edit-job' ); + } + + /** + * Helper function used to check if page is WPJM dashboard page. + * + * Checks if page has 'job_dashboard' shortcode. + * + * @access private + * @return bool True if page is dashboard page, false otherwise. + */ + private function is_job_dashboard_page() { + global $post; + + if ( is_page() && has_shortcode( $post->post_content, 'job_dashboard' ) ) { + return true; + } + + return false; + } + + /** + * Handles actions on job dashboard. + * + * @throws \Exception On action handling error. + */ + public function handle_actions() { + + /** + * Determine if the shortcode action handler should run. + * + * @since 1.35.0 + * + * @param bool $should_run_handler Should the handler run. + */ + $should_run_handler = apply_filters( 'job_manager_should_run_shortcode_action_handler', $this->is_job_dashboard_page() ); + + if ( ! $should_run_handler || + empty( $_REQUEST['action'] ) + || empty( $_REQUEST['job_id'] ) + || empty( $_REQUEST['_wpnonce'] ) + ) { + return; + } + + $job_id = absint( $_REQUEST['job_id'] ); + $action = sanitize_title( wp_unslash( $_REQUEST['action'] ) ); + + $job = get_post( $job_id ); + $job_actions = $this->get_job_actions( $job ); + + if ( ! isset( $job_actions[ $action ] ) + || empty( $job_actions[ $action ]['nonce'] ) + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce should not be modified. + || ! wp_verify_nonce( wp_unslash( $_REQUEST['_wpnonce'] ), $job_actions[ $action ]['nonce'] ) + ) { + return; + } + + try { + if ( empty( $job ) || \WP_Job_Manager_Post_Types::PT_LISTING !== $job->post_type || ! job_manager_user_can_edit_job( $job_id ) ) { + throw new \Exception( __( 'Invalid ID', 'wp-job-manager' ) ); + } + + switch ( $action ) { + case 'mark_filled': + // Check status. + if ( 1 === intval( $job->_filled ) ) { + throw new \Exception( __( 'This position has already been filled', 'wp-job-manager' ) ); + } + + // Update. + update_post_meta( $job_id, '_filled', 1 ); + + // Message. + // translators: Placeholder %s is the job listing title. + $this->job_dashboard_message = Notice::success( sprintf( __( '%s has been filled', 'wp-job-manager' ), wpjm_get_the_job_title( $job ) ) ); + break; + case 'mark_not_filled': + // Check status. + if ( 1 !== intval( $job->_filled ) ) { + throw new \Exception( __( 'This position is not filled', 'wp-job-manager' ) ); + } + + // Update. + update_post_meta( $job_id, '_filled', 0 ); + + // Message. + // translators: Placeholder %s is the job listing title. + $this->job_dashboard_message = Notice::success( sprintf( __( '%s has been marked as not filled', 'wp-job-manager' ), wpjm_get_the_job_title( $job ) ) ); + break; + case 'delete': + // Trash it. + wp_trash_post( $job_id ); + + // Message. + // translators: Placeholder %s is the job listing title. + $this->job_dashboard_message = Notice::success( sprintf( __( '%s has been deleted', 'wp-job-manager' ), wpjm_get_the_job_title( $job ) ) ); + + break; + case 'duplicate': + if ( ! job_manager_get_permalink( 'submit_job_form' ) ) { + throw new \Exception( __( 'Missing submission page.', 'wp-job-manager' ) ); + } + + $new_job_id = job_manager_duplicate_listing( $job_id ); + + if ( $new_job_id ) { + wp_safe_redirect( add_query_arg( [ 'job_id' => absint( $new_job_id ) ], job_manager_get_permalink( 'submit_job_form' ) ) ); + exit; + } + + break; + case 'relist': + case 'renew': + case 'continue': + if ( ! job_manager_get_permalink( 'submit_job_form' ) ) { + throw new \Exception( __( 'Missing submission page.', 'wp-job-manager' ) ); + } + + $query_args = [ + 'job_id' => absint( $job_id ), + 'action' => $action, + ]; + + if ( 'renew' === $action ) { + $query_args['nonce'] = wp_create_nonce( 'job_manager_renew_job_' . $job_id ); + } + wp_safe_redirect( add_query_arg( $query_args, job_manager_get_permalink( 'submit_job_form' ) ) ); + exit; + default: + do_action( 'job_manager_job_dashboard_do_action_' . $action, $job_id ); + break; + } + + do_action( 'job_manager_my_job_do_action', $action, $job_id ); + + /** + * Set a success message for a custom dashboard action handler. + * + * When left empty, no success message will be shown. + * + * @since 1.31.1 + * + * @param string $message Text for the success message. Default: empty string. + * @param string $action The name of the custom action. + * @param int $job_id The ID for the job that's been altered. + */ + $success_message = apply_filters( 'job_manager_job_dashboard_success_message', '', $action, $job_id ); + if ( $success_message ) { + $this->job_dashboard_message = Notice::success( $success_message ); + } + } catch ( \Exception $e ) { + $this->job_dashboard_message = Notice::error( $e->getMessage() ); + } + + } + + /** + * Add expiration details to the job dashboard date column. + * + * @param \WP_Post $job + * + * @output string + */ + public function job_dashboard_date_column_expires( $job ) { + $expiration = \WP_Job_Manager_Post_Types::instance()->get_job_expiration( $job ); + + if ( 'publish' === $job->post_status && ! empty( $expiration ) ) { + + // translators: Placeholder is the expiration date of the job listing. + echo '
' . UI_Elements::rel_time( $expiration, __( 'Expires in %s', 'wp-job-manager' ) ) . '
'; + } + } + + /** + * Add job status to the job dashboard title column. + * + * @param \WP_Post $job + * + * @output string + */ + public function job_dashboard_title_column_status( $job ) { + + echo '
'; + + $status = []; + + if ( is_position_filled( $job ) ) { + $status[] = '' . esc_html__( 'Filled', 'wp-job-manager' ) . ''; + } + + if ( is_position_featured( $job ) && 'publish' === $job->post_status ) { + $status[] = '' . esc_html__( 'Featured', 'wp-job-manager' ) . ''; + } + + $status[] = '' . esc_html( get_the_job_status( $job ) ) . ''; + + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped above. + echo implode( ', ', $status ); + + echo '
'; + } + + /** + * Check if a job is listed on the current user's job dashboard page. + * + * @param \WP_Post $job Job post object. + * + * @return bool + */ + private function is_job_available_on_dashboard( \WP_Post $job ) { + // Check cache of currently displayed job dashboard IDs first to avoid lots of queries. + if ( isset( $this->job_dashboard_job_ids ) && in_array( (int) $job->ID, $this->job_dashboard_job_ids, true ) ) { + return true; + } + + $args = $this->get_job_dashboard_query_args(); + $args['p'] = $job->ID; + $args['fields'] = 'ids'; + + $query = new \WP_Query( $args ); + + return (int) $query->post_count > 0; + } + + /** + * Helper that generates the job dashboard query args. + * + * @param int $posts_per_page Number of posts per page. + * + * @return array + */ + private function get_job_dashboard_query_args( $posts_per_page = -1 ) { + $job_dashboard_args = [ + 'post_type' => \WP_Job_Manager_Post_Types::PT_LISTING, + 'post_status' => [ 'publish', 'expired', 'pending', 'draft', 'preview' ], + 'ignore_sticky_posts' => 1, + 'posts_per_page' => $posts_per_page, + 'orderby' => 'date', + 'order' => 'desc', + 'author' => get_current_user_id(), + ]; + + if ( get_option( 'job_manager_enable_scheduled_listings' ) ) { + $job_dashboard_args['post_status'][] = 'future'; + } + + if ( $posts_per_page > 0 ) { + $job_dashboard_args['offset'] = ( max( 1, get_query_var( 'paged' ) ) - 1 ) * $posts_per_page; + } + + /** + * Customize the query that is used to get jobs on the job dashboard. + * + * @since 1.0.0 + * + * @param array $job_dashboard_args Arguments to pass to \WP_Query. + */ + return apply_filters( 'job_manager_get_dashboard_jobs_args', $job_dashboard_args ); + } + +} diff --git a/includes/class-wp-job-manager-shortcodes.php b/includes/class-wp-job-manager-shortcodes.php index 4d701e0c5..f1fc8b64e 100644 --- a/includes/class-wp-job-manager-shortcodes.php +++ b/includes/class-wp-job-manager-shortcodes.php @@ -5,13 +5,14 @@ * @package wp-job-manager */ -use WP_Job_Manager\UI\Notice; -use WP_Job_Manager\UI\UI_Elements; +use WP_Job_Manager\Job_Dashboard_Shortcode; if ( ! defined( 'ABSPATH' ) ) { exit; } +require_once __DIR__ . '/class-job-dashboard-shortcode.php'; + /** * Handles the shortcodes for WP Job Manager. * @@ -19,82 +20,25 @@ */ class WP_Job_Manager_Shortcodes { - /** - * Dashboard message. - * - * @access private - * @var string - */ - private $job_dashboard_message = ''; - - /** - * Cache of job post IDs currently displayed on job dashboard. - * - * @var int[] - */ - private $job_dashboard_job_ids; - - /** - * The single instance of the class. - * - * @var self - * @since 1.26.0 - */ - private static $instance = null; - - /** - * Allows for accessing single instance of class. Class should only be constructed once per call. - * - * @since 1.26.0 - * @static - * @return self Main instance. - */ - public static function instance() { - if ( is_null( self::$instance ) ) { - self::$instance = new self(); - } - return self::$instance; - } + use \WP_Job_Manager\Singleton; /** * Constructor. */ public function __construct() { add_action( 'wp', [ $this, 'handle_redirects' ] ); - add_action( 'wp', [ $this, 'shortcode_action_handler' ] ); - add_action( 'job_manager_job_dashboard_content_edit', [ $this, 'edit_job' ] ); + add_action( 'job_manager_job_filters_end', [ $this, 'job_filter_job_types' ], 20 ); add_action( 'job_manager_job_filters_end', [ $this, 'job_filter_results' ], 30 ); add_action( 'job_manager_output_jobs_no_results', [ $this, 'output_no_results' ] ); add_shortcode( 'submit_job_form', [ $this, 'submit_job_form' ] ); - add_shortcode( 'job_dashboard', [ $this, 'job_dashboard' ] ); + add_shortcode( 'jobs', [ $this, 'output_jobs' ] ); add_shortcode( 'job', [ $this, 'output_job' ] ); add_shortcode( 'job_summary', [ $this, 'output_job_summary' ] ); add_shortcode( 'job_apply', [ $this, 'output_job_apply' ] ); - add_filter( 'paginate_links', [ $this, 'filter_paginate_links' ], 10, 1 ); - - add_action( 'job_manager_job_dashboard_column_date', [ $this, 'job_dashboard_date_column_expires' ] ); - add_action( 'job_manager_job_dashboard_column_job_title', [ $this, 'job_dashboard_title_column_status' ] ); - } - - /** - * Helper function used to check if page is WPJM dashboard page. - * - * Checks if page has 'job_dashboard' shortcode. - * - * @access private - * @return bool True if page is dashboard page, false otherwise. - */ - private function is_job_dashboard_page() { - global $post; - - if ( is_page() && has_shortcode( $post->post_content, 'job_dashboard' ) ) { - return true; - } - - return false; + Job_Dashboard_Shortcode::instance(); } /** @@ -148,24 +92,6 @@ public function handle_redirects() { } } - /** - * Handles actions which need to be run before the shortcode e.g. post actions. - */ - public function shortcode_action_handler() { - /** - * Determine if the shortcode action handler should run. - * - * @since 1.35.0 - * - * @param bool $should_run_handler Should the handler run. - */ - $should_run_handler = apply_filters( 'job_manager_should_run_shortcode_action_handler', $this->is_job_dashboard_page() ); - - if ( $should_run_handler ) { - $this->job_dashboard_handler(); - } - } - /** * Shows the job submission form. * @@ -176,394 +102,58 @@ public function submit_job_form( $atts = [] ) { return $GLOBALS['job_manager']->forms->get_form( 'submit-job', $atts ); } - /** - * Handles actions on job dashboard. - * - * @throws Exception On action handling error. - */ - public function job_dashboard_handler() { - if ( - ! empty( $_REQUEST['action'] ) - && ! empty( $_REQUEST['job_id'] ) - && ! empty( $_REQUEST['_wpnonce'] ) - ) { - - $job_id = isset( $_REQUEST['job_id'] ) ? absint( $_REQUEST['job_id'] ) : 0; - $action = sanitize_title( wp_unslash( $_REQUEST['action'] ) ); - - $job = get_post( $job_id ); - $job_actions = $this->get_job_actions( $job ); - - if ( - ! isset( $job_actions[ $action ] ) - || empty( $job_actions[ $action ]['nonce'] ) - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce should not be modified. - || ! wp_verify_nonce( wp_unslash( $_REQUEST['_wpnonce'] ), $job_actions[ $action ]['nonce'] ) - ) { - return; - } - - try { - if ( empty( $job ) || \WP_Job_Manager_Post_Types::PT_LISTING !== $job->post_type || ! job_manager_user_can_edit_job( $job_id ) ) { - throw new Exception( __( 'Invalid ID', 'wp-job-manager' ) ); - } - - switch ( $action ) { - case 'mark_filled': - // Check status. - if ( 1 === intval( $job->_filled ) ) { - throw new Exception( __( 'This position has already been filled', 'wp-job-manager' ) ); - } - - // Update. - update_post_meta( $job_id, '_filled', 1 ); - - // Message. - // translators: Placeholder %s is the job listing title. - $this->job_dashboard_message = Notice::success( sprintf( __( '%s has been filled', 'wp-job-manager' ), wpjm_get_the_job_title( $job ) ) ); - break; - case 'mark_not_filled': - // Check status. - if ( 1 !== intval( $job->_filled ) ) { - throw new Exception( __( 'This position is not filled', 'wp-job-manager' ) ); - } - - // Update. - update_post_meta( $job_id, '_filled', 0 ); - - // Message. - // translators: Placeholder %s is the job listing title. - $this->job_dashboard_message = Notice::success( sprintf( __( '%s has been marked as not filled', 'wp-job-manager' ), wpjm_get_the_job_title( $job ) ) ); - break; - case 'delete': - // Trash it. - wp_trash_post( $job_id ); - - // Message. - // translators: Placeholder %s is the job listing title. - $this->job_dashboard_message = Notice::success( sprintf( __( '%s has been deleted', 'wp-job-manager' ), wpjm_get_the_job_title( $job ) ) ); - - break; - case 'duplicate': - if ( ! job_manager_get_permalink( 'submit_job_form' ) ) { - throw new Exception( __( 'Missing submission page.', 'wp-job-manager' ) ); - } - - $new_job_id = job_manager_duplicate_listing( $job_id ); - - if ( $new_job_id ) { - wp_safe_redirect( add_query_arg( [ 'job_id' => absint( $new_job_id ) ], job_manager_get_permalink( 'submit_job_form' ) ) ); - exit; - } - - break; - case 'relist': - case 'renew': - case 'continue': - if ( ! job_manager_get_permalink( 'submit_job_form' ) ) { - throw new Exception( __( 'Missing submission page.', 'wp-job-manager' ) ); - } - - $query_args = [ - 'job_id' => absint( $job_id ), - 'action' => $action, - ]; - - if ( 'renew' === $action ) { - $query_args['nonce'] = wp_create_nonce( 'job_manager_renew_job_' . $job_id ); - } - wp_safe_redirect( add_query_arg( $query_args, job_manager_get_permalink( 'submit_job_form' ) ) ); - exit; - default: - do_action( 'job_manager_job_dashboard_do_action_' . $action, $job_id ); - break; - } - - do_action( 'job_manager_my_job_do_action', $action, $job_id ); - - /** - * Set a success message for a custom dashboard action handler. - * - * When left empty, no success message will be shown. - * - * @since 1.31.1 - * - * @param string $message Text for the success message. Default: empty string. - * @param string $action The name of the custom action. - * @param int $job_id The ID for the job that's been altered. - */ - $success_message = apply_filters( 'job_manager_job_dashboard_success_message', '', $action, $job_id ); - if ( $success_message ) { - $this->job_dashboard_message = Notice::success( $success_message ); - } - } catch ( Exception $e ) { - $this->job_dashboard_message = Notice::error( $e->getMessage() ); - } - } - } - - /** - * Check if a job is listed on the current user's job dashboard page. - * - * @param WP_Post $job Job post object. - * - * @return bool - */ - private function is_job_available_on_dashboard( WP_Post $job ) { - // Check cache of currently displayed job dashboard IDs first to avoid lots of queries. - if ( isset( $this->job_dashboard_job_ids ) && in_array( (int) $job->ID, $this->job_dashboard_job_ids, true ) ) { - return true; - } - - $args = $this->get_job_dashboard_query_args(); - $args['p'] = $job->ID; - $args['fields'] = 'ids'; - - $query = new WP_Query( $args ); - - return (int) $query->post_count > 0; - } - - /** - * Helper that generates the job dashboard query args. - * - * @param int $posts_per_page Number of posts per page. - * - * @return array - */ - private function get_job_dashboard_query_args( $posts_per_page = -1 ) { - $job_dashboard_args = [ - 'post_type' => \WP_Job_Manager_Post_Types::PT_LISTING, - 'post_status' => [ 'publish', 'expired', 'pending', 'draft', 'preview' ], - 'ignore_sticky_posts' => 1, - 'posts_per_page' => $posts_per_page, - 'orderby' => 'date', - 'order' => 'desc', - 'author' => get_current_user_id(), - ]; - - if ( get_option( 'job_manager_enable_scheduled_listings' ) ) { - $job_dashboard_args['post_status'][] = 'future'; - } - - if ( $posts_per_page > 0 ) { - $job_dashboard_args['offset'] = ( max( 1, get_query_var( 'paged' ) ) - 1 ) * $posts_per_page; - } - - /** - * Customize the query that is used to get jobs on the job dashboard. - * - * @since 1.0.0 - * - * @param array $job_dashboard_args Arguments to pass to WP_Query. - */ - return apply_filters( 'job_manager_get_dashboard_jobs_args', $job_dashboard_args ); - } - /** * Handles shortcode which lists the logged in user's jobs. * + * @deprecated $$next-version$$ - Moved to Job_Dashboard_Shortcode. + * * @param array $atts * @return string */ public function job_dashboard( $atts ) { - if ( ! is_user_logged_in() ) { - ob_start(); - get_job_manager_template( 'job-dashboard-login.php' ); - return ob_get_clean(); - } - - $new_atts = shortcode_atts( - [ - 'posts_per_page' => '25', - ], - $atts - ); - $posts_per_page = $new_atts['posts_per_page']; - - WP_Job_Manager::register_style( 'wp-job-manager-job-dashboard', 'css/job-dashboard.css', [ 'wp-job-manager-ui' ] ); - wp_enqueue_style( 'wp-job-manager-job-dashboard' ); - wp_enqueue_script( 'wp-job-manager-job-dashboard' ); - - ob_start(); - - // If doing an action, show conditional content if needed.... - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Input is used safely. - $action = isset( $_REQUEST['action'] ) ? sanitize_title( wp_unslash( $_REQUEST['action'] ) ) : false; - if ( ! empty( $action ) ) { - // Show alternative content if a plugin wants to. - if ( has_action( 'job_manager_job_dashboard_content_' . $action ) ) { - do_action( 'job_manager_job_dashboard_content_' . $action, $atts ); - - return ob_get_clean(); - } - } - - // ....If not show the job dashboard. - $jobs = new WP_Query( $this->get_job_dashboard_query_args( $posts_per_page ) ); - - // Cache IDs for access check later on. - $this->job_dashboard_job_ids = wp_list_pluck( $jobs->posts, 'ID' ); - - echo '
' . wp_kses_post( $this->job_dashboard_message ) . '
'; - - $job_dashboard_columns = apply_filters( - 'job_manager_job_dashboard_columns', - [ - 'job_title' => __( 'Title', 'wp-job-manager' ), - 'date' => __( 'Date', 'wp-job-manager' ), - ] - ); + _deprecated_function( __METHOD__, '$$next-version$$', 'Job_Dashboard_Shortcode::output_job_dashboard_shortcode' ); - $job_actions = []; - foreach ( $jobs->posts as $job ) { - $job_actions[ $job->ID ] = $this->get_job_actions( $job ); - } - - get_job_manager_template( - 'job-dashboard.php', - [ - 'jobs' => $jobs->posts, - 'job_actions' => $job_actions, - 'max_num_pages' => $jobs->max_num_pages, - 'job_dashboard_columns' => $job_dashboard_columns, - ] - ); - - return ob_get_clean(); + return Job_Dashboard_Shortcode::instance()->output_job_dashboard( $atts ); } /** * Get the actions available to the user for a job listing on the job dashboard page. * + * @deprecated $$next-version$$ - Moved to Job_Dashboard_Shortcode. + * * @param WP_Post $job The job post object. * * @return array */ public function get_job_actions( $job ) { - if ( - ! get_current_user_id() - || ! $job instanceof WP_Post - || \WP_Job_Manager_Post_Types::PT_LISTING !== $job->post_type - || ! $this->is_job_available_on_dashboard( $job ) - ) { - return []; - } - - $base_nonce_action_name = 'job_manager_my_job_actions'; - - $actions = []; - switch ( $job->post_status ) { - case 'publish': - if ( WP_Job_Manager_Post_Types::job_is_editable( $job->ID ) ) { - $actions['edit'] = [ - 'label' => __( 'Edit', 'wp-job-manager' ), - 'nonce' => false, - ]; - } - if ( is_position_filled( $job ) ) { - $actions['mark_not_filled'] = [ - 'label' => __( 'Mark not filled', 'wp-job-manager' ), - 'nonce' => $base_nonce_action_name, - ]; - } else { - $actions['mark_filled'] = [ - 'label' => __( 'Mark filled', 'wp-job-manager' ), - 'nonce' => $base_nonce_action_name, - ]; - } - if ( - get_option( 'job_manager_renewal_days' ) > 0 - && WP_Job_Manager_Helper_Renewals::job_can_be_renewed( $job ) - && WP_Job_Manager_Helper_Renewals::is_wcpl_renew_compatible() - && WP_Job_Manager_Helper_Renewals::is_spl_renew_compatible() - ) { - $actions['renew'] = [ - 'label' => __( 'Renew', 'wp-job-manager' ), - 'nonce' => $base_nonce_action_name, - ]; - } - - $actions['duplicate'] = [ - 'label' => __( 'Duplicate', 'wp-job-manager' ), - 'nonce' => $base_nonce_action_name, - ]; - break; - case 'expired': - if ( job_manager_get_permalink( 'submit_job_form' ) ) { - $actions['relist'] = [ - 'label' => __( 'Relist', 'wp-job-manager' ), - 'nonce' => $base_nonce_action_name, - ]; - } - break; - case 'pending_payment': - case 'pending': - if ( WP_Job_Manager_Post_Types::job_is_editable( $job->ID ) ) { - $actions['edit'] = [ - 'label' => __( 'Edit', 'wp-job-manager' ), - 'nonce' => false, - ]; - } - break; - case 'draft': - case 'preview': - $actions['continue'] = [ - 'label' => __( 'Continue Submission', 'wp-job-manager' ), - 'nonce' => $base_nonce_action_name, - ]; - break; - } + _deprecated_function( __METHOD__, '$$next-version$$', 'Job_Dashboard_Shortcode::get_job_actions' ); - $actions['delete'] = [ - 'label' => __( 'Delete', 'wp-job-manager' ), - 'nonce' => $base_nonce_action_name, - ]; - - /** - * Filter the actions available to the current user for a job on the job dashboard page. - * - * @since 1.0.0 - * - * @param array $actions Actions to filter. - * @param WP_Post $job Job post object. - */ - $actions = apply_filters( 'job_manager_my_job_actions', $actions, $job ); - - // For backwards compatibility, convert `nonce => true` to the nonce action name. - foreach ( $actions as $key => $action ) { - if ( true === $action['nonce'] ) { - $actions[ $key ]['nonce'] = $base_nonce_action_name; - } - } - - return $actions; + return Job_Dashboard_Shortcode::instance()->get_job_actions( $job ); } /** * Filters the url from paginate_links to avoid multiple calls for same action in job dashboard * + * @deprecated $$next-version$$ - Moved to Job_Dashboard_Shortcode. + * * @param string $link * @return string */ public function filter_paginate_links( $link ) { + _deprecated_function( __METHOD__, '$$next-version$$', 'Job_Dashboard_Shortcode::filter_paginate_links' ); - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Input is used for comparison only. - if ( $this->is_job_dashboard_page() && isset( $_GET['action'] ) && in_array( $_GET['action'], [ 'mark_filled', 'mark_not_filled' ], true ) ) { - return remove_query_arg( [ 'action', 'job_id', '_wpnonce' ], $link ); - } - - return $link; + return Job_Dashboard_Shortcode::instance()->filter_paginate_links( $link ); } /** * Displays edit job form. + * + * @deprecated $$next-version$$ - Moved to Job_Dashboard_Shortcode. */ public function edit_job() { - global $job_manager; + _deprecated_function( __METHOD__, '$$next-version$$', 'Job_Dashboard_Shortcode::edit_job' ); - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output should be appropriately escaped in the form generator. - echo $job_manager->forms->get_form( 'edit-job' ); + Job_Dashboard_Shortcode::instance()->edit_job(); } /** @@ -997,51 +587,6 @@ public function output_job_apply( $atts ) { return ob_get_clean(); } - /** - * Add expiration details to the job dashboard date column. - * - * @param \WP_Post $job - * - * @output string - */ - public function job_dashboard_date_column_expires( $job ) { - $expiration = WP_Job_Manager_Post_Types::instance()->get_job_expiration( $job ); - - if ( 'publish' === $job->post_status && ! empty( $expiration ) ) { - - // translators: Placeholder is the expiration date of the job listing. - echo '
' . UI_Elements::rel_time( $expiration, __( 'Expires in %s', 'wp-job-manager' ) ) . '
'; - } - } - - /** - * Add job status to the job dashboard title column. - * - * @param \WP_Post $job - * - * @output string - */ - public function job_dashboard_title_column_status( $job ) { - - echo '
'; - - $status = []; - - if ( is_position_filled( $job ) ) { - $status[] = '' . esc_html__( 'Filled', 'wp-job-manager' ) . ''; - } - - if ( is_position_featured( $job ) && 'publish' === $job->post_status ) { - $status[] = '' . esc_html__( 'Featured', 'wp-job-manager' ) . ''; - } - - $status[] = '' . esc_html( get_the_job_status( $job ) ) . ''; - - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped above. - echo implode( ', ', $status ); - - echo '
'; - } } WP_Job_Manager_Shortcodes::instance(); From d74d5eccc3e37f54715603e056fbf799ecc76087 Mon Sep 17 00:00:00 2001 From: Peter Kiss Date: Wed, 14 Feb 2024 15:38:38 +0100 Subject: [PATCH 3/4] Add search to job dashboard (#2760) --- assets/css/ui.elements.scss | 4 ++ assets/css/ui.form.scss | 7 +++ assets/css/ui.neutral.scss | 1 + includes/class-job-dashboard-shortcode.php | 59 ++++++++++++-------- templates/job-dashboard.php | 62 ++++++++++++++-------- 5 files changed, 89 insertions(+), 44 deletions(-) diff --git a/assets/css/ui.elements.scss b/assets/css/ui.elements.scss index ac3903b94..bdd3b9ebb 100644 --- a/assets/css/ui.elements.scss +++ b/assets/css/ui.elements.scss @@ -28,6 +28,10 @@ transition: color 0.2s ease-out, background 0.2s ease-out; + &:focus:not(:focus-visible) { + outline: unset; + } + &:focus-visible { outline: 1.5px solid fadeCurrentColor(85%); outline-offset: 1.5px; diff --git a/assets/css/ui.form.scss b/assets/css/ui.form.scss index 4a55b2c41..c431b9499 100644 --- a/assets/css/ui.form.scss +++ b/assets/css/ui.form.scss @@ -177,6 +177,13 @@ padding-right: var(--jm-ui-space-ml); } + input[type].jm-ui-input--search-icon { + background-image: var(--jm-ui-svg-search); + background-position: var(--jm-ui-space-xs) center; + background-repeat: no-repeat; + padding-left: calc(var(--jm-ui-space-ml) + var(--jm-ui-space-xs)); + } + .select2-container.select2-container.select2-container { input { diff --git a/assets/css/ui.neutral.scss b/assets/css/ui.neutral.scss index 5076c739b..3cb3fafcc 100644 --- a/assets/css/ui.neutral.scss +++ b/assets/css/ui.neutral.scss @@ -67,4 +67,5 @@ --jm-ui-svg-arrow-down: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='32' height='6' viewBox='0 0 16 10'%3e%3cpath fill='black' fill-rule='evenodd' d='M0 1.6 1.53 0 8 6.95 14.5 0 16 1.6 8 10 0 1.6Z' clip-rule='evenodd'/%3e%3c/svg%3e"); --jm-ui-svg-check: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'%3e%3cpath stroke='black' stroke-width='1.5' d='m18.93 6-8.9 11.97-5.16-3.84'/%3e%3c/svg%3e"); --jm-ui-svg-ellipsis-v: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'%3e%3cpath fill='black' fill-rule='evenodd' d='M11 19v-2h2v2h-2Zm0-6v-2h2v2h-2Zm0-6V5h2v2h-2Z' clip-rule='evenodd'/%3e%3c/svg%3e"); + --jm-ui-svg-search: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'%3e%3cpath fill='black' fill-rule='evenodd' d='M19 11a6 6 0 0 1-9.68 4.74l-3.79 3.79-1.06-1.06 3.79-3.8A6 6 0 1 1 19 11Zm-1.5 0a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0Z' clip-rule='evenodd'/%3e%3c/svg%3e"); } diff --git a/includes/class-job-dashboard-shortcode.php b/includes/class-job-dashboard-shortcode.php index f1fde82e1..bb3bbb38c 100644 --- a/includes/class-job-dashboard-shortcode.php +++ b/includes/class-job-dashboard-shortcode.php @@ -96,8 +96,18 @@ public function output_job_dashboard( $attrs ) { } } + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $search = isset( $_GET['search'] ) ? sanitize_text_field( wp_unslash( $_GET['search'] ) ) : ''; + // ....If not show the job dashboard. - $jobs = new \WP_Query( $this->get_job_dashboard_query_args( $posts_per_page ) ); + $jobs = new \WP_Query( + $this->get_job_dashboard_query_args( + [ + 'posts_per_page' => $posts_per_page, + 's' => $search, + ] + ), + ); // Cache IDs for access check later on. $this->job_dashboard_job_ids = wp_list_pluck( $jobs->posts, 'ID' ); @@ -124,6 +134,7 @@ public function output_job_dashboard( $attrs ) { 'job_actions' => $job_actions, 'max_num_pages' => $jobs->max_num_pages, 'job_dashboard_columns' => $job_dashboard_columns, + 'search_input' => $search, ] ); @@ -222,7 +233,7 @@ public function get_job_actions( $job ) { * * @since 1.0.0 * - * @param array $actions Actions to filter. + * @param array $actions Actions to filter. * @param \WP_Post $job Job post object. */ $actions = apply_filters( 'job_manager_my_job_actions', $actions, $job ); @@ -306,8 +317,8 @@ public function handle_actions() { */ $should_run_handler = apply_filters( 'job_manager_should_run_shortcode_action_handler', $this->is_job_dashboard_page() ); - if ( ! $should_run_handler || - empty( $_REQUEST['action'] ) + if ( ! $should_run_handler + || empty( $_REQUEST['action'] ) || empty( $_REQUEST['job_id'] ) || empty( $_REQUEST['_wpnonce'] ) ) { @@ -413,9 +424,9 @@ public function handle_actions() { * * @since 1.31.1 * - * @param string $message Text for the success message. Default: empty string. - * @param string $action The name of the custom action. - * @param int $job_id The ID for the job that's been altered. + * @param string $message Text for the success message. Default: empty string. + * @param string $action The name of the custom action. + * @param int $job_id The ID for the job that's been altered. */ $success_message = apply_filters( 'job_manager_job_dashboard_success_message', '', $action, $job_id ); if ( $success_message ) { @@ -498,27 +509,29 @@ private function is_job_available_on_dashboard( \WP_Post $job ) { /** * Helper that generates the job dashboard query args. * - * @param int $posts_per_page Number of posts per page. + * @param array $args Additional query args. * * @return array */ - private function get_job_dashboard_query_args( $posts_per_page = -1 ) { - $job_dashboard_args = [ - 'post_type' => \WP_Job_Manager_Post_Types::PT_LISTING, - 'post_status' => [ 'publish', 'expired', 'pending', 'draft', 'preview' ], - 'ignore_sticky_posts' => 1, - 'posts_per_page' => $posts_per_page, - 'orderby' => 'date', - 'order' => 'desc', - 'author' => get_current_user_id(), - ]; + private function get_job_dashboard_query_args( $args = [] ) { + $args = wp_parse_args( + $args, + [ + 'post_type' => \WP_Job_Manager_Post_Types::PT_LISTING, + 'post_status' => [ 'publish', 'expired', 'pending', 'draft', 'preview' ], + 'ignore_sticky_posts' => 1, + 'orderby' => 'date', + 'order' => 'desc', + 'author' => get_current_user_id(), + ] + ); if ( get_option( 'job_manager_enable_scheduled_listings' ) ) { - $job_dashboard_args['post_status'][] = 'future'; + $args['post_status'][] = 'future'; } - if ( $posts_per_page > 0 ) { - $job_dashboard_args['offset'] = ( max( 1, get_query_var( 'paged' ) ) - 1 ) * $posts_per_page; + if ( $args['posts_per_page'] > 0 ) { + $args['offset'] = ( max( 1, get_query_var( 'paged' ) ) - 1 ) * $args['posts_per_page']; } /** @@ -526,9 +539,9 @@ private function get_job_dashboard_query_args( $posts_per_page = -1 ) { * * @since 1.0.0 * - * @param array $job_dashboard_args Arguments to pass to \WP_Query. + * @param array $args Arguments to pass to \WP_Query. */ - return apply_filters( 'job_manager_get_dashboard_jobs_args', $job_dashboard_args ); + return apply_filters( 'job_manager_get_dashboard_jobs_args', $args ); } } diff --git a/templates/job-dashboard.php b/templates/job-dashboard.php index 614d1320d..77918ebcb 100644 --- a/templates/job-dashboard.php +++ b/templates/job-dashboard.php @@ -18,8 +18,10 @@ * @var int $max_num_pages Maximum number of pages * @var WP_Post[] $jobs Array of job post results. * @var array $job_actions Array of actions available for each job. + * @var string $search_input Search input. */ +use WP_Job_Manager\UI\Notice; use WP_Job_Manager\UI\UI_Elements; if ( ! defined( 'ABSPATH' ) ) { @@ -31,27 +33,45 @@
-

- -
- -
- +
+
+
+ +
+
+
+
+ + + +
-
- $column ) : ?> -
- -
-
-
- -
- + +
+ $search_input + // translators: Placeholder is the search term. + ? sprintf( __( 'No results found for "%s".', 'wp-job-manager' ), $search_input ) + : __( 'You do not have any active listings.', 'wp-job-manager' ) + ] + ); ?> +
+ +
+ $column ) : ?> +
+ +
+
+
$column ) : ?> @@ -95,8 +115,8 @@ class="jm-dashboard-empty">
- -
+
+
$max_num_pages ] ); ?>
From 109e94a02e0874fef9abf934b4f9208136639ec6 Mon Sep 17 00:00:00 2001 From: Peter Kiss Date: Mon, 19 Feb 2024 17:51:42 +0100 Subject: [PATCH 4/4] Update `feature/stats` branch (#2764) Co-authored-by: Mikey Arce Co-authored-by: WPJM Bot --- assets/js/datepicker.js | 2 + changelog.txt | 5 + ...ass-wp-job-manager-email-notifications.php | 92 +++++++------------ includes/class-wp-job-manager.php | 9 +- includes/ui/class-ui-settings.php | 4 +- includes/ui/class-ui.php | 7 -- languages/wp-job-manager.pot | 78 ++++++++-------- package-lock.json | 4 +- package.json | 2 +- readme.txt | 21 ++--- wp-job-manager-autoload.php | 86 +++++++++++++++++ wp-job-manager.php | 8 +- 12 files changed, 180 insertions(+), 138 deletions(-) create mode 100644 wp-job-manager-autoload.php diff --git a/assets/js/datepicker.js b/assets/js/datepicker.js index 891c62399..1d8e248a5 100644 --- a/assets/js/datepicker.js +++ b/assets/js/datepicker.js @@ -1,7 +1,9 @@ /* global job_manager_datepicker */ jQuery(document).ready( function() { + var $date_today = new Date(); var datePickerOptions = { altFormat : 'yy-mm-dd', + minDate : $date_today, }; if ( typeof job_manager_datepicker !== 'undefined' ) { diff --git a/changelog.txt b/changelog.txt index b1362b92f..849e17fa1 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,10 @@ # WP Job Manager +## 2.2.2 - 2024-02-15 +* Fix issue with rich e-mails on some e-mail providers (#2753) +* Fix: 'featured_first' argument now works when 'show_filters' is set to false. +* Improve checkbox and radio inputs for styled forms + ## 2.2.1 - 2024-01-31 * Fix PHP 7.x error for mixed returned type (#2726) diff --git a/includes/class-wp-job-manager-email-notifications.php b/includes/class-wp-job-manager-email-notifications.php index 16d2396ee..10d2404ea 100644 --- a/includes/class-wp-job-manager-email-notifications.php +++ b/includes/class-wp-job-manager-email-notifications.php @@ -18,7 +18,6 @@ final class WP_Job_Manager_Email_Notifications { const EMAIL_SETTING_PREFIX = 'job_manager_email_'; const EMAIL_SETTING_ENABLED = 'enabled'; const EMAIL_SETTING_PLAIN_TEXT = 'plain_text'; - const MULTIPART_BOUNDARY = 'jm-boundary'; /** * Notifications to be scheduled. @@ -767,7 +766,7 @@ private static function is_email_notification_valid( $email_class ) { * @return bool */ private static function send_email( $email_notification_key, WP_Job_Manager_Email $email ) { - add_filter( 'wp_mail_content_type', [ __CLASS__, 'mail_content_type' ] ); + global $job_manager_doing_email; $job_manager_doing_email = true; @@ -804,10 +803,11 @@ private static function send_email( $email_notification_key, WP_Job_Manager_Emai $is_plain_text_only = self::send_as_plain_text( $email_notification_key, $args ); $content_plain = self::get_email_content( $email_notification_key, $args, true ); - $content_html = null; - if ( ! $is_plain_text_only ) { - $content_html = self::get_email_content( $email_notification_key, $args, false ); + if ( $is_plain_text_only ) { + $body = $content_plain; + } else { + $body = self::get_email_content( $email_notification_key, $args, false ); } /** @@ -829,8 +829,6 @@ private static function send_email( $email_notification_key, WP_Job_Manager_Emai $headers[] = 'CC: ' . $args['cc']; } - $multipart_body = self::get_multipart_body( $content_html, $content_plain ); - /** * Allows for short-circuiting the actual sending of email notifications. * @@ -842,34 +840,45 @@ private static function send_email( $email_notification_key, WP_Job_Manager_Emai * @param string $content Email content. * @param array $headers Email headers. */ - if ( ! apply_filters( 'job_manager_email_do_send_notification', true, $email, $args, $multipart_body, $headers ) ) { + if ( ! apply_filters( 'job_manager_email_do_send_notification', true, $email, $args, $body, $headers ) ) { continue; } - if ( wp_mail( $to_email, $args['subject'], $multipart_body, $headers, $args['attachments'] ) ) { + $set_alt_body = function( $mailer ) use ( $content_plain ) { + // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + $mailer->AltBody = $content_plain; + + return $mailer; + }; + + $set_content_type = fn() => 'multipart/alternative'; + + if ( ! $is_plain_text_only ) { + add_filter( 'wp_mail_content_type', $set_content_type ); + add_filter( 'phpmailer_init', $set_alt_body ); + } + + if ( wp_mail( $to_email, $args['subject'], $body, $headers, $args['attachments'] ) ) { $sent_count++; } + + remove_filter( 'wp_mail_content_type', $set_content_type ); + remove_filter( 'phpmailer_init', $set_alt_body ); + + // Make sure AltBody is not sticking around for a different email. + global $phpmailer; + + if ( $phpmailer instanceof \PHPMailer\PHPMailer\PHPMailer ) { + // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + $phpmailer->AltBody = ''; + } } - remove_filter( 'wp_mail_content_type', [ __CLASS__, 'mail_content_type' ] ); $job_manager_doing_email = false; return $sent_count > 0; } - /** - * Set the "Content Type" header of the e-mail to multipart/alternative. - * - * @access private - * - * @since 2.2.0 - * - * @return string - */ - public static function mail_content_type() { - return 'multipart/alternative; boundary="' . self::MULTIPART_BOUNDARY . '"'; - } - /** * Generates the content for an email. * @@ -965,41 +974,4 @@ private static function get_styles() { return ob_get_clean(); } - /** - * Assemble multipart e-mail body. - * - * @param string $content_html - * @param string $content_plain - * - * @return string - */ - private static function get_multipart_body( string $content_html, string $content_plain ): string { - $multipart_body = ''; - - if ( ! empty( $content_plain ) ) { - - $multipart_body .= ' ---' . self::MULTIPART_BOUNDARY . ' -Content-Type: text/plain; charset="utf-8" - -' . $content_plain; - } - - if ( ! empty( $content_html ) ) { - $multipart_body .= ' ---' . self::MULTIPART_BOUNDARY . ' -Content-Type: text/html; charset="utf-8" - -' . $content_html; - } - - if ( ! empty( $multipart_body ) ) { - $multipart_body .= ' ---' . self::MULTIPART_BOUNDARY . '-- -'; - } - - return $multipart_body; - } - } diff --git a/includes/class-wp-job-manager.php b/includes/class-wp-job-manager.php index 63910aa65..123cba7aa 100644 --- a/includes/class-wp-job-manager.php +++ b/includes/class-wp-job-manager.php @@ -86,17 +86,14 @@ public function __construct() { include_once JOB_MANAGER_PLUGIN_DIR . '/includes/class-wp-job-manager-data-exporter.php'; include_once JOB_MANAGER_PLUGIN_DIR . '/includes/class-wp-job-manager-com-api.php'; include_once JOB_MANAGER_PLUGIN_DIR . '/includes/promoted-jobs/class-wp-job-manager-promoted-jobs.php'; - include_once JOB_MANAGER_PLUGIN_DIR . '/includes/class-access-token.php'; - include_once JOB_MANAGER_PLUGIN_DIR . '/includes/class-guest-user.php'; - include_once JOB_MANAGER_PLUGIN_DIR . '/includes/class-guest-session.php'; - include_once JOB_MANAGER_PLUGIN_DIR . '/includes/class-stats.php'; - include_once JOB_MANAGER_PLUGIN_DIR . '/includes/ui/class-ui.php'; - include_once JOB_MANAGER_PLUGIN_DIR . '/includes/ui/class-ui-settings.php'; if ( is_admin() ) { include_once JOB_MANAGER_PLUGIN_DIR . '/includes/admin/class-wp-job-manager-admin.php'; } + \WP_Job_Manager\UI\UI::instance(); + \WP_Job_Manager\UI\UI_Settings::instance(); + // Load 3rd party customizations. include_once JOB_MANAGER_PLUGIN_DIR . '/includes/3rd-party/3rd-party.php'; diff --git a/includes/ui/class-ui-settings.php b/includes/ui/class-ui-settings.php index 5d79f1ac5..0fed89303 100644 --- a/includes/ui/class-ui-settings.php +++ b/includes/ui/class-ui-settings.php @@ -21,7 +21,7 @@ * * @internal */ -class UISettings { +class UI_Settings { use Singleton; @@ -333,5 +333,3 @@ public function preview_ui_elements() { } } - -UISettings::instance(); diff --git a/includes/ui/class-ui.php b/includes/ui/class-ui.php index 84cb29c1c..a899223c8 100644 --- a/includes/ui/class-ui.php +++ b/includes/ui/class-ui.php @@ -14,11 +14,6 @@ exit; // Exit if accessed directly. } -require_once JOB_MANAGER_PLUGIN_DIR . '/includes/ui/class-ui-elements.php'; -require_once JOB_MANAGER_PLUGIN_DIR . '/includes/ui/class-notice.php'; -require_once JOB_MANAGER_PLUGIN_DIR . '/includes/ui/class-modal-dialog.php'; -require_once JOB_MANAGER_PLUGIN_DIR . '/includes/ui/class-redirect-message.php'; - /** * Frontend UI elements of Job Manager. * @@ -103,5 +98,3 @@ private function generate_inline_css() { return $css; } } - -UI::instance(); diff --git a/languages/wp-job-manager.pot b/languages/wp-job-manager.pot index dae947cae..a06e33c51 100644 --- a/languages/wp-job-manager.pot +++ b/languages/wp-job-manager.pot @@ -2,16 +2,16 @@ # This file is distributed under the GPL2+. msgid "" msgstr "" -"Project-Id-Version: WP Job Manager 2.2.1\n" +"Project-Id-Version: WP Job Manager 2.2.2\n" "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/wp-job-manager/\n" "Last-Translator: \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2024-01-31T09:07:35+00:00\n" +"POT-Creation-Date: 2024-02-02T12:24:53+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"X-Generator: WP-CLI 2.5.0\n" +"X-Generator: WP-CLI 2.7.1\n" "X-Domain: wp-job-manager\n" #. Plugin Name of the plugin @@ -381,7 +381,6 @@ msgstr "" msgid "%1$s updated. View" msgstr "" -#. translators: %1$s is the singular name of the job listing post type; %2$s is the URL to view the listing. #: includes/admin/class-wp-job-manager-cpt.php:457 msgid "Custom field updated." msgstr "" @@ -479,7 +478,6 @@ msgstr "" msgid "View" msgstr "" -#. translators: Placeholder %s is the singular label of the job listing post type. #: includes/admin/class-wp-job-manager-cpt.php:567 #: includes/class-wp-job-manager-post-types.php:464 #: includes/class-wp-job-manager-shortcodes.php:454 @@ -1504,7 +1502,6 @@ msgstr[1] "" msgid "You must be logged in to upload files using this method." msgstr "" -#. translators: Placeholder %s is the singular label of the job listing post type. #: includes/class-wp-job-manager-data-exporter.php:51 #: includes/class-wp-job-manager-post-types.php:481 msgid "Company Logo" @@ -1717,7 +1714,6 @@ msgstr "" msgid "Jobs" msgstr "" -#. translators: Placeholder %s is the plural label of the job listing post type. #: includes/class-wp-job-manager-post-types.php:461 msgid "Add New" msgstr "" @@ -1766,7 +1762,7 @@ msgid "This is where you can create and manage %s." msgstr "" #: includes/class-wp-job-manager-post-types.php:522 -#: wp-job-manager-functions.php:377 +#: wp-job-manager-functions.php:381 msgctxt "post status" msgid "Expired" msgstr "" @@ -1779,7 +1775,7 @@ msgstr[0] "" msgstr[1] "" #: includes/class-wp-job-manager-post-types.php:535 -#: wp-job-manager-functions.php:378 +#: wp-job-manager-functions.php:382 msgctxt "post status" msgid "Preview" msgstr "" @@ -1935,7 +1931,6 @@ msgstr "" msgid "Missing submission page." msgstr "" -#. translators: Placeholder %s is the plural label for the job listing post type. #: includes/class-wp-job-manager-shortcodes.php:405 #: includes/widgets/class-wp-job-manager-widget-featured-jobs.php:36 #: includes/widgets/class-wp-job-manager-widget-featured-jobs.php:52 @@ -1972,7 +1967,7 @@ msgid "Continue Submission" msgstr "" #: includes/class-wp-job-manager-shortcodes.php:715 -#: includes/class-wp-job-manager-shortcodes.php:754 +#: includes/class-wp-job-manager-shortcodes.php:755 msgid "Load more listings" msgstr "" @@ -2204,7 +2199,7 @@ msgstr "" #. translators: Placeholder %1$s is field label; %2$s is the file mime type; %3$s is the allowed mime-types. #. translators: %1$s is the file field label; %2$s is the file type; %3$s is the list of allowed file types. #: includes/forms/class-wp-job-manager-form-submit-job.php:500 -#: wp-job-manager-functions.php:1412 +#: wp-job-manager-functions.php:1416 msgid "\"%1$s\" (filetype %2$s) needs to be one of the following file types: %3$s" msgstr "" @@ -2688,12 +2683,12 @@ msgid "Maximum file size: %s." msgstr "" #: templates/form-fields/multiselect-field.php:20 -#: wp-job-manager-functions.php:1179 +#: wp-job-manager-functions.php:1183 msgid "No results match" msgstr "" #: templates/form-fields/multiselect-field.php:20 -#: wp-job-manager-functions.php:1180 +#: wp-job-manager-functions.php:1184 msgid "Select Some Options" msgstr "" @@ -2798,7 +2793,6 @@ msgstr "" msgid "%s submitted successfully. Your listing will be visible once approved." msgstr "" -#. translators: %1$s is the URL to view the listing; %2$s is #: templates/job-submitted.php:61 msgid " View your %2$s" msgstr "" @@ -2816,117 +2810,117 @@ msgstr "" msgid "Requires Attention" msgstr "" -#: wp-job-manager-functions.php:376 +#: wp-job-manager-functions.php:380 msgctxt "post status" msgid "Draft" msgstr "" -#: wp-job-manager-functions.php:379 +#: wp-job-manager-functions.php:383 msgctxt "post status" msgid "Pending approval" msgstr "" -#: wp-job-manager-functions.php:380 +#: wp-job-manager-functions.php:384 msgctxt "post status" msgid "Pending payment" msgstr "" -#: wp-job-manager-functions.php:381 +#: wp-job-manager-functions.php:385 msgctxt "post status" msgid "Active" msgstr "" -#: wp-job-manager-functions.php:382 +#: wp-job-manager-functions.php:386 msgctxt "post status" msgid "Scheduled" msgstr "" -#: wp-job-manager-functions.php:503 +#: wp-job-manager-functions.php:507 msgid "Reset" msgstr "" -#: wp-job-manager-functions.php:507 +#: wp-job-manager-functions.php:511 msgid "RSS" msgstr "" -#: wp-job-manager-functions.php:616 +#: wp-job-manager-functions.php:620 msgid "Invalid email address." msgstr "" -#: wp-job-manager-functions.php:624 +#: wp-job-manager-functions.php:628 msgid "Your email address isn’t correct." msgstr "" -#: wp-job-manager-functions.php:628 +#: wp-job-manager-functions.php:632 msgid "This email is already registered, please choose another one." msgstr "" -#: wp-job-manager-functions.php:939 +#: wp-job-manager-functions.php:943 msgid "Full Time" msgstr "" -#: wp-job-manager-functions.php:940 +#: wp-job-manager-functions.php:944 msgid "Part Time" msgstr "" -#: wp-job-manager-functions.php:941 +#: wp-job-manager-functions.php:945 msgid "Contractor" msgstr "" -#: wp-job-manager-functions.php:942 +#: wp-job-manager-functions.php:946 msgid "Temporary" msgstr "" -#: wp-job-manager-functions.php:943 +#: wp-job-manager-functions.php:947 msgid "Intern" msgstr "" -#: wp-job-manager-functions.php:944 +#: wp-job-manager-functions.php:948 msgid "Volunteer" msgstr "" -#: wp-job-manager-functions.php:945 +#: wp-job-manager-functions.php:949 msgid "Per Diem" msgstr "" -#: wp-job-manager-functions.php:946 +#: wp-job-manager-functions.php:950 msgid "Other" msgstr "" -#: wp-job-manager-functions.php:1013 +#: wp-job-manager-functions.php:1017 msgid "Passwords must be at least 8 characters long." msgstr "" -#: wp-job-manager-functions.php:1178 +#: wp-job-manager-functions.php:1182 msgid "Choose a category…" msgstr "" #. translators: %s is the list of allowed file types. -#: wp-job-manager-functions.php:1415 +#: wp-job-manager-functions.php:1419 msgid "Uploaded files need to be one of the following file types: %s" msgstr "" -#: wp-job-manager-functions.php:1711 +#: wp-job-manager-functions.php:1715 msgid "--" msgstr "" -#: wp-job-manager-functions.php:1712 +#: wp-job-manager-functions.php:1716 msgid "Year" msgstr "" -#: wp-job-manager-functions.php:1713 +#: wp-job-manager-functions.php:1717 msgid "Month" msgstr "" -#: wp-job-manager-functions.php:1714 +#: wp-job-manager-functions.php:1718 msgid "Week" msgstr "" -#: wp-job-manager-functions.php:1715 +#: wp-job-manager-functions.php:1719 msgid "Day" msgstr "" -#: wp-job-manager-functions.php:1716 +#: wp-job-manager-functions.php:1720 msgid "Hour" msgstr "" diff --git a/package-lock.json b/package-lock.json index b55e234b3..aa805bdf9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wp-job-manager", - "version": "2.2.1", + "version": "2.2.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "wp-job-manager", - "version": "2.2.1", + "version": "2.2.2", "license": "GPL-2.0-or-later", "dependencies": { "select2": "4.0.13" diff --git a/package.json b/package.json index 175e31815..41520fa58 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wp-job-manager", - "version": "2.2.1", + "version": "2.2.2", "description": "WP Job Manager", "author": "Automattic", "license": "GPL-2.0-or-later", diff --git a/readme.txt b/readme.txt index 49c945f64..c1325dd31 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: job manager, job listing, job board, job management, job lists, job list, Requires at least: 6.2 Tested up to: 6.4 Requires PHP: 7.2 -Stable tag: 2.2.1 +Stable tag: 2.2.2 License: GPLv3 License URI: http://www.gnu.org/licenses/gpl-3.0.html @@ -147,6 +147,11 @@ You can view (and contribute) translations via the [translate.wordpress.org](htt == Changelog == +### 2.2.2 - 2024-02-15 +* Fix issue with rich e-mails on some e-mail providers (#2753) +* Fix: 'featured_first' argument now works when 'show_filters' is set to false. +* Improve checkbox and radio inputs for styled forms + ### 2.2.1 - 2024-01-31 * Fix PHP 7.x error for mixed returned type (#2726) @@ -182,17 +187,3 @@ Fixes: ### 2.1.0 - 2023-11-17 * Fix: Remove public update endpoint and add nonce check (#2642) -### 2.0.0 - 2023-11-17 -* Enhancement: Improve settings descriptions (#2639) -* Enhancement: Add directApply in Google job schema (#2635) -* Enhancement: Add 'Don't show this again' link to dismiss promote job modal in the editor (#2632) -* Enhancement: Add landing pages for Applications and Resumes extensions (#2621) -* Fix: Align actions in notices in the center (#2637) -* Fix: Safeguard array in WP_Job_Manager_Settings::input_capabilities (#2631) -* Fix: Escape menu titles and various admin labels (#2630) -* Fix: Incorrectly duplicated string in settings (#2628) -* Fix: Add array initialization to avoid warning (#2619) -* Fix: Do not check for plugin updates when there are no plugins (#2605) -* Change: Reorganize administration menu (#2621) -* Change: Update naming from Add-ons to Extensions, Marketplace (#2621) - diff --git a/wp-job-manager-autoload.php b/wp-job-manager-autoload.php new file mode 100644 index 000000000..2de528282 --- /dev/null +++ b/wp-job-manager-autoload.php @@ -0,0 +1,86 @@ + directory mappings. + * + * @var array + */ + private static $autoload_map = []; + + /** + * Add the autoloader. + */ + public static function init() { + spl_autoload_register( [ self::class, 'autoload' ] ); + } + + /** + * Register a new plugin with a class prefix and directory to autoload. + * + * @param string $namespace Root namespace. Should start with WP_Job_Manager_. + * @param string $dir Directory to autoload. + */ + public static function register( $namespace, $dir ) { + self::$autoload_map[ $namespace ] = $dir; + } + + /** + * Autoload plugin classes. + * + * @access private + * + * @param string $class_name Class name. + */ + public static function autoload( $class_name ) { + + if ( ! str_starts_with( $class_name, 'WP_Job_Manager' ) || ! str_contains( $class_name, '\\' ) ) { + return; + } + + [ $namespace, $file_name ] = explode( '\\', $class_name, 2 ); + + if ( empty( $namespace ) || empty( $file_name ) || empty( self::$autoload_map[ $namespace ] ) ) { + return; + } + + $root_dir = self::$autoload_map[ $namespace ]; + + $file_name = strtolower( $file_name ); + $dirs = explode( '\\', $file_name ); + $file_name = array_pop( $dirs ); + $file_name = str_replace( '_', '-', $file_name ); + + $file_dir = implode( '/', [ $root_dir, ...$dirs ] ); + + $file_paths = [ + 'class-' . $file_name . '.php', + 'trait-' . $file_name . '.php', + ]; + + foreach ( $file_paths as $file_path ) { + $file_path = $file_dir . '/' . $file_path; + if ( file_exists( $file_path ) ) { + require $file_path; + return; + } + } + + } + +} diff --git a/wp-job-manager.php b/wp-job-manager.php index 8f8539c9e..fbb25f095 100644 --- a/wp-job-manager.php +++ b/wp-job-manager.php @@ -3,7 +3,7 @@ * Plugin Name: WP Job Manager * Plugin URI: https://wpjobmanager.com/ * Description: Manage job listings from the WordPress admin panel, and allow users to post jobs directly to your site. - * Version: 2.2.1 + * Version: 2.2.2 * Author: Automattic * Author URI: https://wpjobmanager.com/ * Requires at least: 6.2 @@ -21,11 +21,15 @@ } // Define constants. -define( 'JOB_MANAGER_VERSION', '2.2.1' ); +define( 'JOB_MANAGER_VERSION', '2.2.2' ); define( 'JOB_MANAGER_PLUGIN_DIR', untrailingslashit( plugin_dir_path( __FILE__ ) ) ); define( 'JOB_MANAGER_PLUGIN_URL', untrailingslashit( plugins_url( basename( plugin_dir_path( __FILE__ ) ), basename( __FILE__ ) ) ) ); define( 'JOB_MANAGER_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); +require_once dirname( __FILE__ ) . '/wp-job-manager-autoload.php'; +WP_Job_Manager_Autoload::init(); +WP_Job_Manager_Autoload::register( 'WP_Job_Manager', JOB_MANAGER_PLUGIN_DIR . '/includes' ); + require_once dirname( __FILE__ ) . '/includes/class-wp-job-manager-dependency-checker.php'; if ( ! WP_Job_Manager_Dependency_Checker::check_dependencies() ) { return;