get( $slot ); if ( ! $def || ! current_user_can( (string) $def['capability'] ) ) { wp_die( esc_html__( 'You are not allowed to change this slot.', 'ai-eshop-optimizer' ) ); } $action = isset( $_REQUEST['do'] ) ? sanitize_key( wp_unslash( (string) $_REQUEST['do'] ) ) : 'default'; if ( 'remove_rule' === $action ) { $registry->remove_rule( $slot, isset( $_REQUEST['index'] ) ? absint( wp_unslash( $_REQUEST['index'] ) ) : -1 ); } elseif ( 'add_rule' === $action ) { $match = [ 'kind' => isset( $_POST['kind'] ) ? sanitize_key( wp_unslash( (string) $_POST['kind'] ) ) : '' ]; $value = isset( $_POST['value'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['value'] ) ) : ''; switch ( $match['kind'] ) { case 'term': [ $tax, $id ] = array_pad( explode( ':', $value, 2 ), 2, '' ); $match['taxonomy'] = sanitize_key( $tax ); $match['id'] = (int) $id; break; case 'meta': [ $key, $val ] = array_pad( explode( '=', $value, 2 ), 2, '' ); $match['key'] = sanitize_key( $key ); $match['value'] = $val; break; case 'role': $match['role'] = sanitize_key( $value ); break; case 'lang': $match['lang'] = sanitize_key( $value ); break; case 'auth': $match['auth'] = sanitize_key( $value ); break; case 'always': break; default: $match['id'] = (int) $value; } $registry->add_rule( $slot, [ 'match' => $match, 'template' => isset( $_POST['template'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['template'] ) ) : '', 'priority' => isset( $_POST['priority'] ) ? absint( wp_unslash( $_POST['priority'] ) ) : 10 ] ); } else { $registry->set_default( $slot, isset( $_POST['template'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['template'] ) ) : '' ); } wp_safe_redirect( add_query_arg( [ 'page' => self::SLUG, 'saved' => 1 ], admin_url( 'themes.php' ) ) . '#slot=' . rawurlencode( $slot ) ); exit; } public static function render(): void { $registry = Slots::instance(); $pins = $registry->pins(); $theme = get_stylesheet(); $page = Chrome::ensure()?->get( Chrome::PRODUCT . '/tools/templates' ) ?? []; rooster_admin_page_open( (string) ( $page['title'] ?? __( 'Rooster Templates', 'ai-eshop-optimizer' ) ), [ 'icon' => (string) ( $page['icon'] ?? 'table-properties' ), 'description' => __( 'Each slot is a place a plugin or the store renders. Pick the template a slot uses by default, and add rules for specific objects, terms, organisations, roles or languages. Plugins show the same picker on their own screens. To offer a slot another template, add one in the Site Editor whose name starts with the slot\'s default (single-rooster_lesson-focus for the Lesson slot): it then appears in that slot\'s lists.', 'ai-eshop-optimizer' ), 'actions' => [ [ 'label' => __( 'Add a template', 'ai-eshop-optimizer' ), 'url' => admin_url( 'site-editor.php?p=%2Ftemplate' ), 'icon' => 'plus' ], [ 'label' => __( 'How it works', 'ai-eshop-optimizer' ), 'url' => Chrome::product()['links']['manual'], 'external' => true ], ], ] ); if ( ! empty( $_GET['saved'] ) ) { echo '
' . esc_html__( 'Pins saved.', 'ai-eshop-optimizer' ) . '
' . esc_html__( 'No slots are registered: activate WooCommerce or Rooster LMS, or a plugin that registers slots.', 'ai-eshop-optimizer' ) . '
'; rooster_admin_page_close(); return; } foreach ( $slots as $slot => $def ) { if ( ! current_user_can( (string) $def['capability'] ) ) { continue; } $candidates = $registry->candidates( $slot ); $entry = $pins[ $slot ] ?? [ 'default' => '', 'rules' => [] ]; $resolved = '' !== (string) $entry['default'] ? (string) $entry['default'] : (string) $def['default']; $edit_url = '' !== $resolved ? admin_url( 'site-editor.php?p=' . rawurlencode( '/wp_template/' . $theme . '//' . $resolved ) . '&canvas=edit' ) : ''; $preview = self::preview_url( $def, $registry ); rooster_admin_card_open( Chrome::PRODUCT . '/tools/templates/' . $slot, (string) $def['label'], [ 'dom_id' => 'slot=' . $slot, 'icon' => Chrome::slot_icon( (string) $slot, (array) $def ), 'meta' => '' . esc_html( $slot ) . '',
'description' => 'theme' === $def['source'] ? __( 'Registered by the theme until the plugin adopts C3.', 'ai-eshop-optimizer' ) : __( 'Registered by a plugin.', 'ai-eshop-optimizer' ),
]
);
echo '';
$rows = [];
foreach ( (array) $entry['rules'] as $i => $rule ) {
$rows[] = [
'_id' => (string) $i,
'_name' => sprintf( __( 'rule %1$d of %2$s', 'ai-eshop-optimizer' ), $i + 1, (string) $def['label'] ),
'priority' => (int) ( $rule['priority'] ?? 10 ),
'when' => self::describe_match( (array) ( $rule['match'] ?? [] ) ),
'template' => (string) ( $rule['template'] ?? '' ),
'actions' => [
[
'label' => __( 'Remove', 'ai-eshop-optimizer' ),
'destructive' => true,
'icon' => 'x',
'url' => wp_nonce_url( add_query_arg( [ 'action' => 'rooster_save_pins', 'slot' => $slot, 'do' => 'remove_rule', 'index' => (int) $i ], admin_url( 'admin-post.php' ) ), 'rooster_save_pins' ),
],
],
];
}
$add = '';
\Rooster\Admin\Markup::out( \Rooster\Admin\Ui::table(
[
[ 'key' => 'priority', 'label' => __( 'Priority', 'ai-eshop-optimizer' ), 'type' => 'num', 'width' => 'xs' ],
[ 'key' => 'when', 'label' => __( 'When', 'ai-eshop-optimizer' ) ],
[ 'key' => 'template', 'label' => __( 'Template', 'ai-eshop-optimizer' ), 'type' => 'code' ],
[ 'key' => 'actions', 'label' => __( 'Actions', 'ai-eshop-optimizer' ), 'type' => 'actions' ],
],
$rows,
[
'id' => 'rooster-rules-' . sanitize_html_class( str_replace( '/', '-', (string) $slot ) ),
'caption' => sprintf( __( 'Rules of the %1$s slot (%2$s)', 'ai-eshop-optimizer' ), (string) $def['label'], (string) $slot ),
'empty' => esc_html__( 'No rules: the default applies.', 'ai-eshop-optimizer' ),
'footer' => $add,
'cards' => true,
]
) );
rooster_admin_card_close();
}
rooster_admin_page_close();
}
private static function kind_label( string $kind ): string {
$labels = [
'object' => __( 'Object ID (post or term)', 'ai-eshop-optimizer' ),
'term' => __( 'Term (taxonomy:ID)', 'ai-eshop-optimizer' ),
'org' => __( 'Organisation ID', 'ai-eshop-optimizer' ),
'role' => __( 'User role', 'ai-eshop-optimizer' ),
'lang' => __( 'Language', 'ai-eshop-optimizer' ),
'meta' => __( 'Post meta (key=value)', 'ai-eshop-optimizer' ),
'page' => __( 'Page ID', 'ai-eshop-optimizer' ),
'auth' => __( 'Signed in or guest (signed-in, guest)', 'ai-eshop-optimizer' ),
'always' => __( 'Always', 'ai-eshop-optimizer' ),
];
return $labels[ $kind ] ?? $kind;
}
private static function describe_match( array $match ): string {
$kind = (string) ( $match['kind'] ?? '' );
switch ( $kind ) {
case 'always':
return __( 'Always', 'ai-eshop-optimizer' );
case 'term':
$term = get_term( (int) ( $match['id'] ?? 0 ), (string) ( $match['taxonomy'] ?? '' ) );
return sprintf( '%s: %s', (string) ( $match['taxonomy'] ?? '' ), $term instanceof \WP_Term ? $term->name : '#' . (int) ( $match['id'] ?? 0 ) );
case 'object':
case 'page':
$id = (int) ( $match['id'] ?? 0 );
return sprintf( '%s #%d %s', self::kind_label( $kind ), $id, get_the_title( $id ) );
case 'org':
return sprintf( '%s #%d', self::kind_label( $kind ), (int) ( $match['id'] ?? 0 ) );
case 'role':
return sprintf( '%s: %s', self::kind_label( $kind ), (string) ( $match['role'] ?? '' ) );
case 'lang':
return sprintf( '%s: %s', self::kind_label( $kind ), (string) ( $match['lang'] ?? '' ) );
case 'meta':
return sprintf( '%s %s=%s', self::kind_label( $kind ), (string) ( $match['key'] ?? '' ), (string) ( $match['value'] ?? '' ) );
case 'auth':
return 'guest' === ( $match['auth'] ?? '' ) ? __( 'Guests', 'ai-eshop-optimizer' ) : __( 'Signed-in visitors', 'ai-eshop-optimizer' );
}
return $kind;
}
private static function preview_url( array $def, Slots $registry ): string {
switch ( $def['kind'] ) {
case 'page':
$id = $registry->bound_page( $def );
return $id > 0 ? (string) get_permalink( $id ) : '';
case 'archive':
return $def['post_types'] ? (string) get_post_type_archive_link( (string) $def['post_types'][0] ) : '';
case 'single':
$posts = $def['post_types'] ? get_posts( [ 'post_type' => $def['post_types'], 'posts_per_page' => 1, 'fields' => 'ids', 'no_found_rows' => true ] ) : [];
return $posts ? (string) get_permalink( (int) $posts[0] ) : '';
case 'taxonomy':
$terms = $def['taxonomies'] ? get_terms( [ 'taxonomy' => $def['taxonomies'], 'number' => 1, 'hide_empty' => true ] ) : [];
if ( ! is_array( $terms ) || ! $terms ) {
return '';
}
$link = get_term_link( $terms[0] );
return is_string( $link ) ? $link : '';
}
return '';
}
}