*/ private array $remote_maps; /** * Config posts bound to a map, keyed by map id. * * @var array */ private array $bound; /** * Whether the current user may open a given config post. * * @var callable(MapConfig): bool */ private $can_edit_config; /** * Map counts per view, after the search. * * @var array */ private array $counts = array(); /** * Constructor. * * @param array $remote_maps Remote maps keyed by id, as Client::get_remote_maps() returns them. * @param array $bound Config posts keyed by the map id they are bound to. * @param callable(MapConfig): bool $can_edit_config Whether the current user may open a config post. */ public function __construct( array $remote_maps, array $bound, callable $can_edit_config ) { $this->remote_maps = $remote_maps; $this->bound = $bound; $this->can_edit_config = $can_edit_config; $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; parent::__construct( array( 'singular' => 'map', 'plural' => 'maps', 'ajax' => false, // Outside a real admin request (tests) there is no current // screen; naming one keeps WP_Screen from reading $hook_suffix. 'screen' => $screen ?? DataEditorPage::PAGE_SLUG, ) ); } /** * The columns. No checkbox: there are no bulk actions. * * @return array */ public function get_columns() { return array( 'name' => __( 'Map', 'mapsemble' ), 'created' => __( 'Created', 'mapsemble' ), 'origin' => __( 'Data', 'mapsemble' ), 'action' => '' . esc_html__( 'Actions', 'mapsemble' ) . '', ); } /** * The search box term, sanitised. */ public static function search_term(): string { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display-only filter of a list. return isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : ''; } /** * The chosen origin view, or '' for all. */ public static function origin(): string { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display-only filter of a list. $origin = isset( $_GET['origin'] ) ? sanitize_key( wp_unslash( $_GET['origin'] ) ) : ''; return in_array( $origin, array( self::ORIGIN_HOSTED, self::ORIGIN_SITE ), true ) ? $origin : ''; } /** * The requested page number. * * Core's, but read from $_GET rather than $_REQUEST: the form is * GET-only. The clamp to the last page is core's, kept as is. * * @return int */ public function get_pagenum() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display-only pagination. $pagenum = isset( $_GET['paged'] ) ? absint( wp_unslash( $_GET['paged'] ) ) : 0; if ( isset( $this->_pagination_args['total_pages'] ) && $pagenum > $this->_pagination_args['total_pages'] ) { $pagenum = (int) $this->_pagination_args['total_pages']; } return max( 1, $pagenum ); } /** * Builds the rows: newest first, searched, filtered by origin, paged. * * The search matches what a row shows — the name and the id — not the * date-and-id suffix map_choices() adds to tell duplicate names apart. */ public function prepare_items() { $lower = static fn ( string $text ): string => function_exists( 'mb_strtolower' ) ? mb_strtolower( $text ) : strtolower( $text ); $needle = $lower( self::search_term() ); $rows = array(); $this->counts = array( 'all' => 0, self::ORIGIN_HOSTED => 0, self::ORIGIN_SITE => 0, ); foreach ( Plugin::map_choices( $this->remote_maps ) as $choice ) { if ( '' !== $needle && false === strpos( $lower( $choice['name'] ), $needle ) && false === strpos( $lower( $choice['value'] ), $needle ) ) { continue; } $config = $this->bound[ $choice['value'] ] ?? null; $origin = $config instanceof MapConfig && ! $config->is_hosted() ? self::ORIGIN_SITE : self::ORIGIN_HOSTED; ++$this->counts['all']; ++$this->counts[ $origin ]; $rows[] = array( 'id' => $choice['value'], 'name' => $choice['name'], 'origin' => $origin, 'config' => $config, ); } $origin = self::origin(); if ( '' !== $origin ) { $rows = array_values( array_filter( $rows, static fn ( array $row ): bool => $origin === $row['origin'] ) ); } $this->set_pagination_args( array( 'total_items' => count( $rows ), 'per_page' => self::PER_PAGE, ) ); $this->items = array_slice( $rows, ( $this->get_pagenum() - 1 ) * self::PER_PAGE, self::PER_PAGE ); $this->_column_headers = array( $this->get_columns(), array(), array(), 'name' ); } /** * All / Hosted on Mapsemble / Built from this site, with counts. * * @return array */ protected function get_views() { $labels = array( 'all' => __( 'All', 'mapsemble' ), self::ORIGIN_HOSTED => __( 'Hosted on Mapsemble', 'mapsemble' ), self::ORIGIN_SITE => _x( 'Built from this site', 'map origin', 'mapsemble' ), ); $current = self::origin(); $search = self::search_term(); $views = array(); foreach ( $labels as $key => $label ) { $args = array( 'page' => DataEditorPage::PAGE_SLUG ); if ( '' !== $search ) { $args['s'] = rawurlencode( $search ); } if ( 'all' !== $key ) { $args['origin'] = $key; } $is_current = ( 'all' === $key ? '' : $key ) === $current; $views[ $key ] = sprintf( '%3$s (%4$s)', esc_url( add_query_arg( $args, admin_url( 'admin.php' ) ) ), $is_current ? ' class="current" aria-current="page"' : '', esc_html( $label ), esc_html( number_format_i18n( $this->counts[ $key ] ?? 0 ) ) ); } return $views; } /** * The map's name, and its id shortened with the full one on hover. * * @param array $item Row. */ public function column_name( $item ): string { $id = (string) $item['id']; $short = MapChoices::short_id( $id ); return sprintf( '%1$s %3$s', esc_html( (string) $item['name'] ), esc_attr( $id ), esc_html( $short ) ); } /** * The creation date read from the UUIDv7 id, or a dash. * * @param array $item Row. */ public function column_created( $item ): string { $created = MapChoices::created_at( (string) $item['id'] ); if ( null === $created ) { return '' . esc_html__( 'Unknown', 'mapsemble' ) . ''; } return esc_html( (string) wp_date( (string) get_option( 'date_format' ), $created ) ); } /** * Where the map's locations live. * * @param array $item Row. */ public function column_origin( $item ): string { return self::ORIGIN_SITE === $item['origin'] ? esc_html_x( 'Built from this site', 'map origin', 'mapsemble' ) : esc_html__( 'Hosted on Mapsemble', 'mapsemble' ); } /** * The way into the editor, or why there is none. * * @param array $item Row. */ public function column_action( $item ): string { $config = $item['config']; if ( $config instanceof MapConfig && ! $config->is_hosted() ) { // Its locations are this site's posts; the Mapsemble editor // has nothing to edit. Administrators get the way to the // configuration; pure data editors just get the fact. $action = esc_html__( 'Built from this site — its locations are the posts.', 'mapsemble' ); if ( ( $this->can_edit_config )( $config ) ) { $edit_link = get_edit_post_link( $config->id, 'url' ); if ( is_string( $edit_link ) ) { $action .= sprintf( ' %s', esc_url( $edit_link ), esc_html__( 'Edit site-fed map', 'mapsemble' ) ); } } return $action; } return sprintf( '%s', esc_url( DataEditorPage::editor_url( (string) $item['id'] ) ), esc_html__( 'Edit locations', 'mapsemble' ) ); } /** * Shown when no map matches the search or the view. */ public function no_items() { if ( '' !== self::search_term() ) { esc_html_e( 'No maps match your search.', 'mapsemble' ); return; } esc_html_e( 'No maps found.', 'mapsemble' ); } }