'/^[a-z][a-z0-9_-]{0,31}$/', 'section' => '/^[a-z][a-z0-9_-]{0,31}$/', 'kind' => '/^[a-z][a-z0-9_-]{0,31}$/', 'pane' => '/^(lesson|files|qa)$/', 'open' => '/^(picker|search)$/', 'lang' => '/^[a-z]{2,3}(-[a-z0-9]{2,8})?$/', ); public static function kept(): array { $out = array(); foreach ( self::KEEP as $name => $pattern ) { $value = 'lang' === $name ? strtolower( Request::text( 'lang', 20 ) ) : Request::key( $name ); if ( '' !== $value && 1 === preg_match( $pattern, $value ) ) { $out[ $name ] = $value; } } if ( isset( $out['tab'] ) ) { $tab = Tabs::canonical( $out['tab'] ); if ( '' === $tab ) { unset( $out['tab'] ); } else { $out['tab'] = $tab; } } return $out; } public static function open_url( int $course_id, array $kept ): string { return add_query_arg( array_merge( array( 'page' => BuilderPage::SLUG, 'course' => $course_id, ), $kept ), admin_url( 'admin.php' ) ); } public static function courses( int $user_id, string $search = '' ): array { if ( $user_id <= 0 ) { return array(); } $args = array( 'post_type' => PostTypes::COURSE, 'post_status' => array( 'publish', 'future', 'draft', 'pending', 'private' ), 'orderby' => 'modified', 'order' => 'DESC', 'posts_per_page' => self::LIMIT * 2, 'no_found_rows' => true, 'ignore_sticky_posts' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, 'suppress_filters' => false, ); $scope = InstructorCaps::admin_list_scope( PostTypes::COURSE, $user_id ); if ( null !== $scope ) { $args['post__in'] = array() === $scope ? array( 0 ) : $scope; } if ( '' !== $search ) { $args['s'] = $search; $args['search_columns'] = array( 'post_title' ); } $query = new \WP_Query( $args ); $posts = array_values( array_filter( $query->posts, static fn( $p ): bool => $p instanceof \WP_Post ) ); $map = array() === $posts ? array() : Services::course_repo()->ids_for_posts( array_map( static fn( \WP_Post $p ): int => (int) $p->ID, $posts ) ); $out = array(); $seen = array(); foreach ( $posts as $post ) { $course = (int) ( $map[ (int) $post->ID ] ?? 0 ); if ( $course <= 0 || isset( $seen[ $course ] ) || ! user_can( $user_id, 'edit_post', (int) $post->ID ) ) { continue; } $seen[ $course ] = true; $out[] = array( 'course' => $course, 'post' => (int) $post->ID, 'title' => trim( wp_strip_all_tags( (string) $post->post_title ) ), 'status' => (string) $post->post_status, 'modified' => (string) $post->post_modified_gmt, ); if ( count( $out ) >= self::LIMIT ) { break; } } return $out; } public static function view_label( array $kept ): string { $tab = (string) ( $kept['tab'] ?? '' ); if ( '' === $tab ) { return ''; } $entry = Registry::get( Registry::tab_id( BuilderPage::PAGE, $tab ) ); $label = null !== $entry ? (string) $entry['title'] : $tab; $part = (string) ( $kept['section'] ?? '' ); if ( '' !== $part ) { $sub = Registry::get( Registry::tab_id( BuilderPage::PAGE, $part, $tab ) ); if ( null !== $sub ) { $label .= ' › ' . (string) $sub['title']; } } return $label; } public static function html( int $user_id ): string { $kept = self::kept(); $search = Request::text( self::SEARCH_ARG, 100 ); $rows = self::courses( $user_id, $search ); $view = self::view_label( $kept ); $html = Card::open_html( BuilderPage::PAGE, self::CARD ); if ( '' !== $view ) { $html .= Card::desc_html( /* translators: %s: the Designer's view a link asked for ("Sequencing › Before this course"). */ sprintf( __( 'The course you choose opens on: %s.', 'rooster-lms' ), $view ) ); } $kit = Ui::kit_markup(); $submit = Ui::button( __( 'Search', 'rooster-lms' ), 'outline', array( 'type' => 'submit', 'size' => 'sm' ) ); $html .= ''; if ( array() === $rows ) { $html .= '' !== $search ? Card::empty_state_html( __( 'No course matches this search', 'rooster-lms' ), __( 'Try another word of the title, or clear the search.', 'rooster-lms' ) ) : Card::empty_state_html( __( 'No course to open yet', 'rooster-lms' ), __( 'Courses you may edit are listed here.', 'rooster-lms' ), current_user_can( 'edit_rooster_courses' ) ? Ui::button( // phpcs:ignore WordPress.WP.Capabilities.Unknown -- the course post type's primitive. __( 'Add new course', 'rooster-lms' ), 'primary', array( 'href' => admin_url( 'post-new.php?post_type=' . PostTypes::COURSE ), 'size' => 'sm', ) ) : '' ); return $html . Card::close_html(); } $tbody = ''; foreach ( $rows as $row ) { $url = self::open_url( $row['course'], $kept ); $title = '' !== $row['title'] ? $row['title'] : __( '(no title)', 'rooster-lms' ); $tbody .= '' . esc_html( $title ) . '' . '' . Ui::badge( CoursesTree::status_label( $row['status'] ), 'publish' === $row['status'] ? 'success' : 'secondary' ) . '' . ''; } $html .= \RoosterLMS\UI\DataTable::bare( array( __( 'Course', 'rooster-lms' ), __( 'Status', 'rooster-lms' ), __( 'Last changed', 'rooster-lms' ) ), $tbody, 'rl-cb-picker', __( 'Courses you may edit, changed last first', 'rooster-lms' ) ); if ( count( $rows ) >= self::LIMIT ) { $html .= Card::footer_html() . Card::note_html( /* translators: %d: number of courses listed. */ sprintf( __( 'The %d courses changed last are listed: search to find another one.', 'rooster-lms' ), self::LIMIT ) ); } return $html . Card::close_html(); } }