list( $label, $control, $help ) ) { printf( '
%1$s%2$s

%3$s

', self::label( $id, $label ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in label(). $control, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built from escaped values in fields(). esc_html( $help ) ); } } /** * Prints the fields on the form that edits a status. * * @param WP_Term $status Status being edited. */ public static function render_on_edit( WP_Term $status ): void { $fields = self::fields( StatusTaxonomy::color( $status ), (string) (int) get_term_meta( $status->term_id, StatusTaxonomy::META_ORDER, true ), StatusTaxonomy::is_closed( $status ) ); foreach ( $fields as $id => list( $label, $control, $help ) ) { printf( '%1$s%2$s

%3$s

', self::label( $id, $label ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in label(). $control, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built from escaped values in fields(). esc_html( $help ) ); } // Inside a table cell, the only place this form allows more fields. echo ''; wp_nonce_field( self::NONCE_ACTION, self::NONCE_FIELD ); echo ''; } /** * Saves the fields when a status is added or edited from its form. * * Statuses are also created and updated by the REST API, WP-CLI and other code. Those * send no fields, and must leave the status as it is. * * @param int $status_id Status term ID. */ public static function save( int $status_id ): void { $nonce = isset( $_POST[ self::NONCE_FIELD ] ) ? sanitize_text_field( wp_unslash( $_POST[ self::NONCE_FIELD ] ) ) : ''; if ( ! wp_verify_nonce( $nonce, self::NONCE_ACTION ) || ! current_user_can( 'edit_term', $status_id ) ) { return; } $sent = isset( $_POST[ self::FIELD ] ) && is_array( $_POST[ self::FIELD ] ) ? map_deep( wp_unslash( $_POST[ self::FIELD ] ), 'sanitize_text_field' ) : []; $color = sanitize_hex_color( (string) ( $sent['color'] ?? '' ) ); if ( $color ) { update_term_meta( $status_id, StatusTaxonomy::META_COLOR, $color ); } // An empty position keeps the one the status has. if ( isset( $sent['order'] ) && '' !== $sent['order'] ) { update_term_meta( $status_id, StatusTaxonomy::META_ORDER, absint( $sent['order'] ) ); } // An unticked box sends nothing. update_term_meta( $status_id, StatusTaxonomy::META_CLOSED, ! empty( $sent['closed'] ) ); } /** * Adds the colour and voting columns after the name. * * @param array $columns Columns of the statuses list. * @return array */ public static function columns( array $columns ): array { $ordered = []; foreach ( $columns as $key => $label ) { $ordered[ $key ] = $label; if ( 'name' === $key ) { $ordered[ self::COLUMN_COLOR ] = __( 'Colour', 'feature-request-system' ); $ordered[ self::COLUMN_VOTING ] = __( 'Voting', 'feature-request-system' ); } } return $ordered; } /** * Returns the content of the colour and voting columns. * * @param string $content Content so far. * @param string $column Column key. * @param int $status_id Status term ID. */ public static function render_column( string $content, string $column, int $status_id ): string { $status = get_term( $status_id, StatusTaxonomy::NAME ); if ( ! $status instanceof WP_Term ) { return $content; } if ( self::COLUMN_COLOR === $column ) { return sprintf( ' %1$s', esc_attr( StatusTaxonomy::color( $status ) ) ); } if ( self::COLUMN_VOTING === $column ) { return StatusTaxonomy::is_closed( $status ) ? esc_html__( 'Closed', 'feature-request-system' ) : esc_html__( 'Open', 'feature-request-system' ); } return $content; } /** * Lists statuses in workflow order on the Statuses screen, unless a column is sorted. * * @param WP_Term_Query $query Terms query about to run. */ public static function order_list( WP_Term_Query $query ): void { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; $vars = $query->query_vars; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only checks whether the user sorted a column. if ( ! $screen || 'edit-' . StatusTaxonomy::NAME !== $screen->id || [ StatusTaxonomy::NAME ] !== array_values( (array) $vars['taxonomy'] ) || isset( $_GET['orderby'] ) || 'count' === $vars['fields'] ) { return; } // Every status has a position from creation, so none drop out of the list. $query->query_vars['meta_key'] = StatusTaxonomy::META_ORDER; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- A handful of status terms. $query->query_vars['orderby'] = 'meta_value_num'; $query->query_vars['order'] = 'ASC'; // WordPress has read the meta arguments by now, and sorts by what it read. $query->meta_query->parse_query_vars( $query->query_vars ); } /** * The fields, by HTML id: their label, their control and their help text. * * @param string $color Colour to show. * @param string $order Position to show, or '' for a new status. * @param bool $closed Whether the status is closed for voting. * @return array */ private static function fields( string $color, string $order, bool $closed ): array { return [ 'mwtfrs-status-color' => [ __( 'Colour', 'feature-request-system' ), sprintf( '', esc_attr( self::FIELD . '[color]' ), esc_attr( $color ) ), __( 'Shown next to the status wherever a request is listed.', 'feature-request-system' ), ], 'mwtfrs-status-order' => [ __( 'Position', 'feature-request-system' ), sprintf( '', esc_attr( self::FIELD . '[order]' ), esc_attr( $order ) ), __( 'Statuses are listed from the lowest number to the highest. Leave empty to put a new status last.', 'feature-request-system' ), ], 'mwtfrs-status-closed' => [ __( 'Voting', 'feature-request-system' ), sprintf( '', esc_attr( self::FIELD . '[closed]' ), checked( $closed, true, false ), esc_html__( 'Closed for voting', 'feature-request-system' ) ), __( 'Requests in a closed status keep their votes, and nobody can add or remove one. Use it for statuses such as Shipped or Declined.', 'feature-request-system' ), ], ]; } /** * Returns a field's label. * * @param string $id HTML id of the field. * @param string $label Label text. */ private static function label( string $id, string $label ): string { return sprintf( '', esc_attr( $id ), esc_html( $label ) ); } }