| 1 |
<?php |
| 2 |
/** |
| 3 |
* Standalone (theme_id 990) — "Featured" on the Properties admin list (issue #288). |
| 4 |
* |
| 5 |
* The property edit screen already has a "Featured listing" checkbox (post meta |
| 6 |
* 'mlsimport_featured' = '1'). This file adds the two list-screen conveniences: |
| 7 |
* |
| 8 |
* 1. Bulk actions "Mark featured" / "Remove featured", so an owner can flag many |
| 9 |
* listings without opening each one. |
| 10 |
* 2. A "Featured" column, so they can see at a glance which listings are flagged. |
| 11 |
* |
| 12 |
* The front end reads the flag from the fast listings table, not from post meta, so |
| 13 |
* every change made here re-indexes the post — Mlsimport_Standalone_Row::upsert() |
| 14 |
* then copies the meta into the row's `featured` column. Writing the meta alone |
| 15 |
* would change nothing a visitor sees. |
| 16 |
* |
| 17 |
* Kept apart from Mlsimport_Property_Columns (which owns the column SET) so neither |
| 18 |
* file outgrows the 300-line limit; this class only appends its one column. |
| 19 |
* |
| 20 |
* @package Mlsimport |
| 21 |
*/ |
| 22 |
|
| 23 |
if ( ! defined( 'ABSPATH' ) ) { |
| 24 |
exit; |
| 25 |
} |
| 26 |
|
| 27 |
/** |
| 28 |
* Bulk actions + list column for the "Featured listing" flag. |
| 29 |
*/ |
| 30 |
class Mlsimport_Property_Featured_Admin { |
| 31 |
|
| 32 |
/** Post meta the "Featured listing" checkbox saves ('1' when ticked). */ |
| 33 |
private const META = 'mlsimport_featured'; |
| 34 |
|
| 35 |
/** |
| 36 |
* Register the admin hooks. Hooked on init; only wires up in the admin. |
| 37 |
* |
| 38 |
* @return void |
| 39 |
*/ |
| 40 |
public static function register(): void { |
| 41 |
if ( ! is_admin() ) { |
| 42 |
return; |
| 43 |
} |
| 44 |
add_filter( 'bulk_actions-edit-mlsimport_property', array( __CLASS__, 'bulk_actions' ) ); |
| 45 |
add_filter( 'handle_bulk_actions-edit-mlsimport_property', array( __CLASS__, 'handle_bulk' ), 10, 3 ); |
| 46 |
add_action( 'admin_notices', array( __CLASS__, 'notice' ) ); |
| 47 |
// Priority 20: Mlsimport_Property_Columns::columns() (priority 10) rebuilds the |
| 48 |
// whole column set, so the Featured column is appended after it has run. |
| 49 |
add_filter( 'manage_mlsimport_property_posts_columns', array( __CLASS__, 'columns' ), 20 ); |
| 50 |
add_action( 'manage_mlsimport_property_posts_custom_column', array( __CLASS__, 'render' ), 10, 2 ); |
| 51 |
// Priority 20, after Mlsimport_Property_Columns::sortable_columns() has run. |
| 52 |
add_filter( 'manage_edit-mlsimport_property_sortable_columns', array( __CLASS__, 'sortable' ), 20 ); |
| 53 |
add_filter( 'posts_clauses', array( __CLASS__, 'orderby_featured' ), 10, 2 ); |
| 54 |
} |
| 55 |
|
| 56 |
/** |
| 57 |
* Make the Featured header clickable. `true` = the first click sorts DESC, so |
| 58 |
* featured listings land on top straight away. |
| 59 |
* |
| 60 |
* @param array $columns Sortable columns. |
| 61 |
* @return array |
| 62 |
*/ |
| 63 |
public static function sortable( $columns ): array { |
| 64 |
$columns = (array) $columns; |
| 65 |
$columns['mlsimport_featured'] = array( 'mlsimport_featured', true ); |
| 66 |
return $columns; |
| 67 |
} |
| 68 |
|
| 69 |
/** |
| 70 |
* Sort by the featured flag when the header is clicked. A LEFT JOIN, not a |
| 71 |
* meta_key orderby: most listings have no meta row at all, and meta_key would |
| 72 |
* drop every one of them from the list. Newest first inside each group. |
| 73 |
* |
| 74 |
* @param array $clauses SQL clause fragments. |
| 75 |
* @param WP_Query $query Current query. |
| 76 |
* @return array |
| 77 |
*/ |
| 78 |
public static function orderby_featured( $clauses, $query ): array { |
| 79 |
if ( ! is_admin() || ! $query->is_main_query() ) { |
| 80 |
return $clauses; |
| 81 |
} |
| 82 |
if ( 'mlsimport_property' !== $query->get( 'post_type' ) || 'mlsimport_featured' !== $query->get( 'orderby' ) ) { |
| 83 |
return $clauses; |
| 84 |
} |
| 85 |
global $wpdb; |
| 86 |
$clauses['join'] .= $wpdb->prepare( " LEFT JOIN {$wpdb->postmeta} mlsimport_featured_pm ON ( {$wpdb->posts}.ID = mlsimport_featured_pm.post_id AND mlsimport_featured_pm.meta_key = %s )", self::META ); |
| 87 |
$order = ( 'ASC' === strtoupper( (string) $query->get( 'order' ) ) ) ? 'ASC' : 'DESC'; |
| 88 |
$clauses['orderby'] = "( mlsimport_featured_pm.meta_value = '1' ) $order, {$wpdb->posts}.post_date DESC"; |
| 89 |
return $clauses; |
| 90 |
} |
| 91 |
|
| 92 |
/** |
| 93 |
* Offer the two bulk actions in the Properties list dropdown. |
| 94 |
* |
| 95 |
* @param array $actions Bulk actions (key => label). |
| 96 |
* @return array |
| 97 |
*/ |
| 98 |
public static function bulk_actions( $actions ): array { |
| 99 |
$actions = (array) $actions; |
| 100 |
$actions['mlsimport_mark_featured'] = __( 'Mark featured', 'mlsimport' ); |
| 101 |
$actions['mlsimport_remove_featured'] = __( 'Remove featured', 'mlsimport' ); |
| 102 |
return $actions; |
| 103 |
} |
| 104 |
|
| 105 |
/** |
| 106 |
* Apply "Mark featured" / "Remove featured" to the ticked listings. |
| 107 |
* |
| 108 |
* WordPress has already verified the bulk nonce and that the user may edit |
| 109 |
* this post type before calling the filter; each post is still checked |
| 110 |
* individually so a listing the user cannot edit is skipped. |
| 111 |
* |
| 112 |
* @param string $redirect Where WordPress sends the browser afterwards. |
| 113 |
* @param string $action The chosen bulk action key. |
| 114 |
* @param array $post_ids The ticked post IDs. |
| 115 |
* @return string The redirect URL, carrying the changed count for notice(). |
| 116 |
*/ |
| 117 |
public static function handle_bulk( $redirect, $action, $post_ids ) { |
| 118 |
// Step 1: leave every bulk action that is not ours untouched. |
| 119 |
if ( 'mlsimport_mark_featured' !== $action && 'mlsimport_remove_featured' !== $action ) { |
| 120 |
return $redirect; |
| 121 |
} |
| 122 |
$value = 'mlsimport_mark_featured' === $action ? '1' : ''; |
| 123 |
$changed = 0; |
| 124 |
|
| 125 |
foreach ( (array) $post_ids as $post_id ) { |
| 126 |
$post_id = (int) $post_id; |
| 127 |
// Step 2: only our post type, and only listings this user may edit. The |
| 128 |
// capability check is skipped under WP-CLI, which has no current user. |
| 129 |
if ( 'mlsimport_property' !== get_post_type( $post_id ) ) { |
| 130 |
continue; |
| 131 |
} |
| 132 |
if ( ! ( defined( 'WP_CLI' ) && WP_CLI ) && ! current_user_can( 'edit_post', $post_id ) ) { |
| 133 |
continue; |
| 134 |
} |
| 135 |
|
| 136 |
// Step 3: write the same meta the edit-screen checkbox writes... |
| 137 |
update_post_meta( $post_id, self::META, $value ); |
| 138 |
// Step 4: ...then re-index, which is what moves the flag into the fast |
| 139 |
// listings table the front end sorts and badges from. |
| 140 |
if ( class_exists( 'Mlsimport_Standalone_Reindex' ) ) { |
| 141 |
Mlsimport_Standalone_Reindex::rebuild_post( $post_id ); |
| 142 |
} |
| 143 |
++$changed; |
| 144 |
|
| 145 |
/** Fires after a listing's featured flag changed from the list screen. @since 7.2.2 */ |
| 146 |
do_action( 'mlsimport_property_featured_changed', $post_id, '1' === $value ); |
| 147 |
} |
| 148 |
|
| 149 |
// Step 5: hand the count to notice() through the redirect URL. |
| 150 |
return add_query_arg( 'mlsimport_featured_changed', $changed, (string) $redirect ); |
| 151 |
} |
| 152 |
|
| 153 |
/** |
| 154 |
* Confirm a finished bulk action on the Properties list. |
| 155 |
* |
| 156 |
* @return void |
| 157 |
*/ |
| 158 |
public static function notice(): void { |
| 159 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only count for a notice. |
| 160 |
if ( ! isset( $_GET['mlsimport_featured_changed'] ) ) { |
| 161 |
return; |
| 162 |
} |
| 163 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 164 |
$count = absint( wp_unslash( $_GET['mlsimport_featured_changed'] ) ); |
| 165 |
/* translators: %s: number of listings. */ |
| 166 |
$text = sprintf( _n( '%s listing updated.', '%s listings updated.', $count, 'mlsimport' ), number_format_i18n( $count ) ); |
| 167 |
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html( $text ) . '</p></div>'; |
| 168 |
} |
| 169 |
|
| 170 |
/** |
| 171 |
* Add the Featured column just before the native Date column. |
| 172 |
* |
| 173 |
* @param array $columns Columns (key => label). |
| 174 |
* @return array |
| 175 |
*/ |
| 176 |
public static function columns( $columns ): array { |
| 177 |
$out = array(); |
| 178 |
foreach ( (array) $columns as $key => $label ) { |
| 179 |
// Slot ours in immediately ahead of Date. |
| 180 |
if ( 'date' === $key ) { |
| 181 |
$out['mlsimport_featured'] = esc_html__( 'Featured', 'mlsimport' ); |
| 182 |
} |
| 183 |
$out[ $key ] = $label; |
| 184 |
} |
| 185 |
// No Date column on this screen (hidden by another plugin): append at the end. |
| 186 |
if ( ! isset( $out['mlsimport_featured'] ) ) { |
| 187 |
$out['mlsimport_featured'] = esc_html__( 'Featured', 'mlsimport' ); |
| 188 |
} |
| 189 |
return $out; |
| 190 |
} |
| 191 |
|
| 192 |
/** |
| 193 |
* Render the Featured cell: a star + "Featured" for a flagged listing, a dash |
| 194 |
* (WordPress's usual "nothing here") otherwise. |
| 195 |
* |
| 196 |
* @param string $column Column key. |
| 197 |
* @param int $post_id Property post ID. |
| 198 |
* @return void |
| 199 |
*/ |
| 200 |
public static function render( $column, $post_id ): void { |
| 201 |
if ( 'mlsimport_featured' !== $column ) { |
| 202 |
return; |
| 203 |
} |
| 204 |
if ( '1' === (string) get_post_meta( $post_id, self::META, true ) ) { |
| 205 |
echo '<span class="dashicons dashicons-star-filled" style="color:#dba617" aria-hidden="true"></span> ' . esc_html__( 'Featured', 'mlsimport' ); |
| 206 |
return; |
| 207 |
} |
| 208 |
echo '<span aria-hidden="true">—</span>'; |
| 209 |
} |
| 210 |
} |
| 211 |
|