workflow = $workflow; $this->audit = $audit; } /** * Register hooks. * * @return void */ public function register_hooks() { add_action( 'add_meta_boxes', array( $this, 'add_meta_box' ), 10, 2 ); add_action( 'admin_menu', array( $this, 'add_submenu' ), 11 ); } /** * The admin screen's URL, optionally for one post. * * @param int $post_id Post ID, or 0 for the site-wide view. * @return string */ public static function screen_url( $post_id = 0 ) { $args = array( 'page' => self::MENU_SLUG ); if ( $post_id ) { $args['post'] = (int) $post_id; } return add_query_arg( $args, admin_url( 'admin.php' ) ); } /** * Add the History screen under the Editorial Workflow menu. * * @return void */ public function add_submenu() { add_submenu_page( Settings::MENU_SLUG, __( 'Editorial History', 'ost-editorial-workflow' ), __( 'History', 'ost-editorial-workflow' ), 'edit_others_posts', self::MENU_SLUG, array( $this, 'render_screen' ) ); } /** * Render the History screen: one post's trail, or the latest site-wide. * * @return void */ public function render_screen() { if ( ! current_user_can( 'edit_others_posts' ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen. $post_id = isset( $_GET['post'] ) ? absint( wp_unslash( $_GET['post'] ) ) : 0; echo '
'; if ( $post_id ) { $post = get_post( $post_id ); if ( ! $post || ! current_user_can( 'edit_post', $post_id ) ) { echo '

' . esc_html__( 'Editorial History', 'ost-editorial-workflow' ) . '

'; echo '

' . esc_html__( 'That item does not exist, or you cannot view it.', 'ost-editorial-workflow' ) . '

'; return; } printf( '

%s

%s  |  %s

', esc_html( sprintf( /* translators: %s: post title. */ __( 'Editorial History: %s', 'ost-editorial-workflow' ), get_the_title( $post ) ) ), esc_url( (string) get_edit_post_link( $post_id ) ), esc_html__( 'Edit this item', 'ost-editorial-workflow' ), esc_url( self::screen_url() ), esc_html__( 'All recent activity', 'ost-editorial-workflow' ) ); $this->render_table( $this->audit->for_post( $post_id, 200 ), $post, false ); echo ''; return; } echo '

' . esc_html__( 'Editorial History', 'ost-editorial-workflow' ) . '

'; echo '

' . esc_html__( 'The most recent workflow activity across the site. Open an item to see its own trail.', 'ost-editorial-workflow' ) . '

'; $this->render_table( $this->audit->recent( 200 ), null, true ); echo '

' . esc_html__( 'Export the full trail as CSV', 'ost-editorial-workflow' ) . '

'; echo ''; } /** * Add the history meta box on managed post types. * * @param string $post_type Post type. * @param \WP_Post $post Post. * @return void */ public function add_meta_box( $post_type, $post ) { if ( ! $post instanceof \WP_Post || ! $this->workflow->for_post( $post ) ) { return; } if ( ! current_user_can( 'edit_post', $post->ID ) ) { return; } add_meta_box( 'ostew_history', __( 'Editorial history', 'ost-editorial-workflow' ), array( $this, 'render' ), $post_type, 'normal', 'low' ); } /** * Render the trail. * * @param \WP_Post $post Post. * @return void */ public function render( $post ) { $rows = $this->audit->for_post( $post->ID, 100 ); $this->render_table( $rows, $post, false ); if ( ! empty( $rows ) ) { printf( '

%s

', esc_url( self::screen_url( $post->ID ) ), esc_html__( 'Open the full history for this item', 'ost-editorial-workflow' ) ); } } /** * Render a set of audit rows as a table. * * @param array $rows Audit rows. * @param \WP_Post|null $post The post these rows belong to, when they all do. * @param bool $with_post Whether to include an item column. * @return void */ private function render_table( $rows, $post = null, $with_post = false ) { if ( empty( $rows ) ) { echo '

' . esc_html__( 'Nothing recorded yet. Entries appear here as items move through the workflow.', 'ost-editorial-workflow' ) . '

'; return; } echo ''; echo ''; if ( $with_post ) { echo ''; } echo ''; echo ''; echo ''; echo ''; echo ''; foreach ( $rows as $row ) { $row_post = $post; if ( $with_post ) { $row_post = get_post( (int) $row->post_id ); } $workflow = $row_post ? $this->workflow->for_post( $row_post ) : null; $user = $row->user_id ? get_user_by( 'id', (int) $row->user_id ) : null; echo ''; echo ''; if ( $with_post ) { if ( $row_post ) { printf( '', esc_url( self::screen_url( (int) $row->post_id ) ), esc_html( get_the_title( $row_post ) ) ); } else { echo ''; } } $to_label = $this->status_label( $workflow, $row->to_status ); $from_label = $this->status_label( $workflow, $row->from_status ); // A post's first entry has no meaningful "from" — showing the entry status // on both sides reads as "Draft → Draft", which tells the reader nothing. $is_creation = in_array( (string) $row->from_status, array( '', 'new', 'auto-draft' ), true ); echo ''; echo ''; echo ''; echo ''; echo ''; } echo '
' . esc_html__( 'When', 'ost-editorial-workflow' ) . '' . esc_html__( 'Item', 'ost-editorial-workflow' ) . '' . esc_html__( 'Who', 'ost-editorial-workflow' ) . '' . esc_html__( 'Action', 'ost-editorial-workflow' ) . '' . esc_html__( 'Moved', 'ost-editorial-workflow' ) . '' . esc_html__( 'Note', 'ost-editorial-workflow' ) . '
' . esc_html( mysql2date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $row->created_at ) ) . '%2$s' . esc_html__( 'deleted', 'ost-editorial-workflow' ) . '' . esc_html( $user ? $user->display_name : __( 'System', 'ost-editorial-workflow' ) ) . '' . esc_html( $is_creation ? __( 'Created', 'ost-editorial-workflow' ) : $this->action_label( isset( $row->action ) ? (string) $row->action : '' ) ) . '' . esc_html( $is_creation ? $to_label : sprintf( /* translators: 1: previous status, 2: new status. */ __( '%1$s → %2$s', 'ost-editorial-workflow' ), $from_label, $to_label ) ) . '' . esc_html( (string) $row->note ) . '
'; } /** * Human label for a status, with a fallback for rows whose post has been * deleted and so has no workflow to read labels from. * * @param array|null $workflow Workflow definition, or null. * @param string $status Status key. * @return string */ private function status_label( $workflow, $status ) { if ( is_array( $workflow ) ) { return $this->workflow->status_label( $workflow, $status ); } $object = get_post_status_object( (string) $status ); if ( $object && ! empty( $object->label ) ) { return $object->label; } return ucwords( str_replace( array( 'ostew_', '_' ), array( '', ' ' ), (string) $status ) ); } /** * Human label for a recorded action key. * * @param string $action Action key. * @return string */ private function action_label( $action ) { $labels = array( 'submit' => __( 'Submitted', 'ost-editorial-workflow' ), 'approve' => __( 'Approved', 'ost-editorial-workflow' ), 'reject' => __( 'Sent back', 'ost-editorial-workflow' ), ); if ( isset( $labels[ $action ] ) ) { return $labels[ $action ]; } return __( 'Status changed directly', 'ost-editorial-workflow' ); } }