PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.8
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.8
4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.9.1 4.3.9 4.3.8 4.3.7 4.1.6.9 4.1.6.9.1 4.1.6.9.2 4.1.6.9.3 4.1.6.9.4 4.1.7 4.1.7.1 4.1.7.2 4.1.7.3 4.1.7.3.1 4.1.7.3.2 4.2.0 All 139 releases
← All changes | inc/TemplateHooks/CourseBuilder/Course/BuilderEditCourseTemplate.php +1428 -1419 4.3.94.4.8 View file →
@@ -1,1419 +1,1428 @@
1 -<?php
2 -/**
3 - * Template hooks Course Builder.
4 - *
5 - * @since 4.3.0
6 - * @version 1.0.0
7 - */
8 -
9 -namespace LearnPress\TemplateHooks\CourseBuilder\Course;
10 -
11 -use Exception;
12 -use LearnPress\CourseBuilder\CourseBuilder;
13 -use LearnPress\CourseBuilder\CourseBuilderAccessPolicy;
14 -use LearnPress\Helpers\Singleton;
15 -use LearnPress\Helpers\Template;
16 -use LearnPress\Models\CourseModel;
17 -use LearnPress\Models\CoursePostModel;
18 -use LearnPress\Models\PostModel;
19 -use LearnPress\Models\UserModel;
20 -use LearnPress\TemplateHooks\Admin\AdminTemplate;
21 -use LearnPress\TemplateHooks\Admin\AI\AdminEditCourseCurriculumWithAITemplate;
22 -use LearnPress\TemplateHooks\Admin\AI\AdminEditWithAITemplate;
23 -use LearnPress\TemplateHooks\Course\AdminEditCurriculumTemplate;
24 -use LearnPress\TemplateHooks\CourseBuilder\BuilderPopupTemplate;
25 -use LearnPress\TemplateHooks\TemplateAJAX;
26 -use LP_Settings;
27 -use LP_WP_Filesystem;
28 -use Throwable;
29 -use WP_User;
30 -
31 -class BuilderEditCourseTemplate {
32 - use Singleton;
33 -
34 - public function init() {
35 - add_filter( 'lp/rest/ajax/allow_callback', [ $this, 'allow_callback' ] );
36 -
37 - // Register filter for adding edit popup button in Course Builder curriculum
38 - add_filter( 'learn-press/admin/curriculum/section-item/actions', [ $this, 'add_edit_popup_button' ], 10, 5 );
39 - }
40 -
41 - /**
42 - * HTML edit/create course on Course Builder screen
43 - *
44 - * @param array $data
45 - *
46 - * @since 4.3.6
47 - * @version 1.0.1
48 - * @return string
49 - */
50 - public function layout( array $data = [] ): string {
51 - $html = '';
52 -
53 - try {
54 - // Check permission
55 - $userModel = $data['userModel'] ?? false;
56 - if ( ! $userModel || ! $userModel->is_instructor() ) {
57 - throw new Exception( __( 'You do not have permission to create or edit courses', 'learnpress' ) );
58 - }
59 -
60 - $userCoursePostModel = new CoursePostModel();
61 - if ( ! $userCoursePostModel->check_capabilities_create() ) {
62 - throw new Exception( __( 'You do not have permission to create or edit courses', 'learnpress' ) );
63 - }
64 -
65 - $item_id = $data['item_id'] ?? '';
66 - if ( empty( $item_id ) ) {
67 - throw new Exception( __( 'Invalid course ID', 'learnpress' ) );
68 - }
69 -
70 - if ( ! CourseBuilderAccessPolicy::can_access_tab_post( 'courses', $item_id ) ) {
71 - throw new Exception( __( "Sorry, you don't have permission to access this content", 'learnpress' ) );
72 - }
73 -
74 - $is_create_new = $item_id === CourseBuilder::POST_NEW;
75 - $courseModel = false;
76 -
77 - if ( ! $is_create_new ) {
78 - $courseModel = CourseModel::find( (int) $item_id, true );
79 - if ( ! $courseModel ) {
80 - throw new Exception( __( 'Course not found', 'learnpress' ) );
81 - }
82 -
83 - if ( $courseModel->get_status() === PostModel::STATUS_TRASH ) {
84 - throw new Exception(
85 - __(
86 - 'You cannot edit this item because it is in the Trash. Please restore it and try again.',
87 - 'learnpress'
88 - )
89 - );
90 - }
91 - }
92 -
93 - $data['courseModel'] = $courseModel;
94 -
95 - $section = [
96 - 'wrap' => sprintf(
97 - '<div class="lp-cb-content" data-post-id="%1$s">',
98 - esc_attr( $item_id ),
99 - ),
100 - 'header' => $this->html_header( $data ),
101 - 'tabs' => $this->html_tabs( $data ),
102 - 'wrap_end' => '</div>',
103 - ];
104 -
105 - $html = Template::combine_components( $section );
106 - } catch ( Throwable $e ) {
107 - $html = Template::print_message( $e->getMessage(), 'error', false );
108 - }
109 -
110 - return $html;
111 - }
112 -
113 - /**
114 - * HTML header
115 - *
116 - * @param array $data
117 - *
118 - * @return string
119 - */
120 - public function html_header( array $data = [] ): string {
121 - /** @var UserModel $userModel */
122 - $userModel = $data['userModel'] ?? false;
123 - if ( ! $userModel instanceof UserModel ) {
124 - return '';
125 - }
126 -
127 - /** @var CourseModel|false $courseModel */
128 - $courseModel = $data['courseModel'] ?? false;
129 - $hide_instructor_access_admin_screen = LP_Settings::is_hide_instructor_access_admin_screen();
130 - $more_actions_icon = LP_WP_Filesystem::get_icon_svg( 'ico-cb-more.svg' );
131 - $title = $courseModel ? $courseModel->get_title() : __( 'Add New Course', 'learnpress' );
132 - $status_badge = $courseModel ? $courseModel->get_status() : '';
133 - $status = '';
134 - if ( $courseModel ) {
135 - $status = $courseModel->get_status();
136 - }
137 - $main_action_status = in_array(
138 - $status,
139 - array(
140 - 'publish',
141 - 'draft',
142 - 'pending',
143 - 'future',
144 - 'private',
145 - ),
146 - true
147 - ) ? $status : 'publish';
148 - $wp_user = new WP_User( $userModel );
149 - $hide_wp_edit_link = $hide_instructor_access_admin_screen && user_can( $wp_user, UserModel::ROLE_INSTRUCTOR );
150 -
151 - $section = [
152 - 'header_wrap' => '<div class="lp-cb-header">',
153 - 'header_left' => '<div class="lp-cb-header__left">',
154 - 'title' => sprintf(
155 - '<h1 class="lp-cb-header__title">%s</h1>',
156 - esc_html( $title )
157 - ),
158 - 'status_badge' => $courseModel ? sprintf(
159 - '<span class="course-status %s">%s</span>',
160 - $status_badge,
161 - esc_html( $courseModel->get_post_model()->get_status_i18n() )
162 - ) : '',
163 - 'link_edit_on_wp' => $courseModel && ! $hide_wp_edit_link ? sprintf(
164 - '<a href="%1$s" class="lp-cb-admin-link" target="_blank" title="%2$s"%3$s>
165 - <span class="dashicons dashicons-wordpress"></span>
166 - <span>%2$s</span>
167 - </a>',
168 - esc_url( admin_url( "post.php?post={$courseModel->get_id()}&action=edit" ) ),
169 - esc_attr__( 'Edit with WordPress', 'learnpress' ),
170 - PostModel::STATUS_TRASH === $status ? ' style="display:none"' : ''
171 - ) : '',
172 - 'header_left_end' => '</div>',
173 - 'header_actions' => '<div class="lp-cb-header__actions">',
174 - 'preview_btn' => $courseModel && $courseModel->get_status() !== PostModel::STATUS_TRASH ? sprintf(
175 - '<a href="%1$s" class="cb-button cb-btn-preview cb-btn-secondary lp-button" target="_blank">%2$s</a>',
176 - esc_url( get_permalink( $courseModel->get_id() ) ),
177 - esc_html__( 'Preview', 'learnpress' )
178 - ) : '',
179 - 'dropdown_wrap' => '<div class="cb-header-actions-dropdown cb-header-actions-dropdown--single">',
180 - 'update_btn' => sprintf(
181 - '<div class="cb-btn-update cb-btn-primary cb-btn-main-action lp-button"
182 - data-status="%1$s"
183 - data-title-update="%2$s"
184 - data-title-publish="%3$s"
185 - data-title-draft="%4$s"
186 - data-title-submit-review="%5$s">%6$s</div>',
187 - esc_attr( $main_action_status ),
188 - esc_attr__( 'Update', 'learnpress' ),
189 - esc_attr__( 'Publish', 'learnpress' ),
190 - esc_attr__( 'Save Draft', 'learnpress' ),
191 - esc_attr__( 'Submit for Review', 'learnpress' ),
192 - esc_html__( 'Update', 'learnpress' )
193 - ),
194 - 'dropdown_wrap_end' => '</div>',
195 - 'expanded_actions' => $courseModel ? sprintf(
196 - '<div class="cb-header-action-expanded">
197 - <button type="button" class="course-action-expanded lp-button" aria-haspopup="true" aria-expanded="false" aria-label="%1$s">
198 - %2$s
199 - </button>
200 - <div class="cb-header-action-expanded__items">
201 - <div class="cb-header-action-expanded__duplicate cb-btn-duplicate-course lp-button"
202 - data-title="%3$s"
203 - data-content="%4$s">
204 - <span class="dashicons dashicons-admin-page"></span>
205 - %5$s
206 - </div>
207 - <div class="cb-header-action-expanded__trash cb-btn-trash lp-button">
208 - <span class="dashicons dashicons-trash"></span>
209 - %6$s
210 - </div>
211 - </div>
212 - </div>',
213 - esc_attr__( 'More actions', 'learnpress' ),
214 - $more_actions_icon,
215 - esc_attr__( 'Are you sure?', 'learnpress' ),
216 - esc_attr__( 'Are you sure you want to duplicate this course?', 'learnpress' ),
217 - esc_html__( 'Duplicate', 'learnpress' ),
218 - esc_html__( 'Move to Trash', 'learnpress' )
219 - ) : '',
220 - 'header_actions_end' => '</div>',
221 - 'header_end' => '</div>',
222 - ];
223 -
224 - return Template::combine_components( $section );
225 - }
226 -
227 - /**
228 - * Render tabs
229 - *
230 - * @param array $data
231 - *
232 - * @return string
233 - */
234 - public function html_tabs( array $data = [] ): string {
235 - $section_tab = [
236 - 'tabs_wrap' => '<div class="lp-cb-tabs">',
237 - 'tabs' => '',
238 - 'tabs_end' => '</div>',
239 - ];
240 -
241 - $section_content = [
242 - 'wrap' => '<div class="lp-cb-tab-content">',
243 - 'content' => '',
244 - 'wrap-end' => '</div>',
245 - ];
246 -
247 - $tabs = apply_filters(
248 - 'learn-press/course-builder/course/edit/tabs',
249 - [
250 - 'overview' => [
251 - 'title' => esc_html__( 'Overview', 'learnpress' ),
252 - 'html' => $this->html_tab_overview( $data ),
253 - ],
254 - 'curriculum' => [
255 - 'title' => esc_html__( 'Curriculum', 'learnpress' ),
256 - 'html' => $this->html_tab_curriculum( $data ),
257 - ],
258 - 'settings' => [
259 - 'title' => esc_html__( 'Settings', 'learnpress' ),
260 - 'html' => $this->html_tab_settings( $data ),
261 - ],
262 - ],
263 - $data
264 - );
265 -
266 - $tab_active = array_key_first( $tabs );
267 - if ( isset( $_GET['tab'] ) ) {
268 - $tab_active = $_GET['tab'];
269 - }
270 -
271 - foreach ( $tabs as $key => $tab ) {
272 - $is_active = $key === $tab_active;
273 -
274 - $section_tab['tabs'] .= sprintf(
275 - '<a href="#" class="lp-cb-tabs__item %s" data-tab-section="%s">%s</a>',
276 - $is_active ? 'is-active' : '',
277 - esc_attr( $tab['slug'] ?? $key ),
278 - esc_html( $tab['title'] ?? '' )
279 - );
280 -
281 - /**
282 - * @uses html_tab_overview
283 - * @uses html_tab_curriculum
284 - * @uses html_tab_settings
285 - */
286 - $section_content['content'] .= sprintf(
287 - '<div class="lp-cb-tab-panel %s" data-section="%s">%s</div>',
288 - $is_active ? '' : 'lp-hidden',
289 - esc_attr( $key ),
290 - $tab['html']
291 - );
292 - }
293 -
294 - $section = [
295 - 'tabs' => Template::combine_components( $section_tab ),
296 - 'contents' => Template::combine_components( $section_content ),
297 - ];
298 -
299 - return Template::combine_components( $section );
300 - }
301 -
302 - public function html_tab_overview( array $data = [] ) {
303 - wp_enqueue_script( 'lp-course-builder' );
304 -
305 - $courseModel = $data['courseModel'] ?? null;
306 - $course_id = 0;
307 - if ( $courseModel instanceof CourseModel ) {
308 - $course_id = $courseModel->get_id();
309 - }
310 -
311 - $html_edit_title = $this->edit_title( $courseModel );
312 - $html_edit_permalink = $this->edit_permalink( $courseModel );
313 - $html_edit_features = $this->edit_featured_image( $courseModel );
314 - $html_edit_publish = $this->edit_publish( $courseModel );
315 - $html_edit_desc = $this->edit_desc( $courseModel );
316 - $html_edit_cat = $this->edit_categories( $courseModel );
317 - $html_edit_tags = $this->edit_tags( $courseModel );
318 -
319 - $section = [
320 - 'wrapper' => sprintf( '<div class="cb-section__course-edit" data-course-id="%s">', $course_id ),
321 - 'content_wrapper' => '<div class="cb-course-edit-content">',
322 - // Left column
323 - 'left_column' => '<div class="cb-course-edit-column cb-course-edit-column--left">',
324 - 'edit_title' => $html_edit_title,
325 - 'edit_permalink' => $html_edit_permalink,
326 - 'edit_publish' => $html_edit_publish,
327 - 'edit_features' => $html_edit_features,
328 - 'left_column_end' => '</div>',
329 - // Right column
330 - 'right_column' => '<div class="cb-course-edit-column cb-course-edit-column--right">',
331 - 'edit_desc' => $html_edit_desc,
332 - 'edit_term_category' => '<div class="cb-course-edit-terms-categories-wrapper">',
333 - 'edit_cat' => $html_edit_cat,
334 - 'edit_term' => $html_edit_tags,
335 - 'edit_term_category_end' => '</div>',
336 - 'right_column_end' => '</div>',
337 - 'content_wrapper_end' => '</div>',
338 - 'ai_templates' => $this->html_overview_ai_templates(),
339 - 'wrapper_end' => '</div>',
340 - ];
341 -
342 - return Template::combine_components( $section );
343 - }
344 -
345 - /**
346 - * HTML Curriculum
347 - *
348 - * @param array $data
349 - *
350 - * @return string
351 - */
352 - public function html_tab_curriculum( array $data = [] ): string {
353 - wp_enqueue_script( 'lp-cb-edit-curriculum' );
354 - wp_enqueue_style( 'lp-cb-edit-curriculum' );
355 - wp_enqueue_script( 'lp-cb-admin-learnpress' );
356 -
357 - $course_id = CourseBuilder::get_item_id();
358 -
359 - if ( $course_id === CourseBuilder::POST_NEW ) {
360 - return Template::print_message( __( 'Please save Course before add Section', 'learnpress' ), 'info', false );
361 - }
362 -
363 - $courseModel = $data['courseModel'] ?? null;
364 - if ( ! $courseModel instanceof CourseModel ) {
365 - return Template::print_message( __( 'Course is invalid!', 'learnpress' ), 'error', false );
366 - }
367 -
368 - return $this->html_curriculum( $courseModel );
369 - }
370 -
371 - public function html_tab_settings( array $data = [] ) {
372 - wp_enqueue_script( 'lp-cb-edit-curriculum' );
373 - wp_enqueue_script( 'lp-tom-select' );
374 - wp_enqueue_style( 'lp-cb-edit-curriculum' );
375 - wp_enqueue_script( 'lp-cb-learnpress' );
376 -
377 - $course_id = CourseBuilder::get_item_id();
378 -
379 - if ( $course_id === CourseBuilder::POST_NEW ) {
380 - return Template::print_message( __( 'Please save Course before setting course', 'learnpress' ), 'info', false );
381 - }
382 -
383 - $courseModel = $data['courseModel'] ?? null;
384 - if ( ! $courseModel instanceof CourseModel ) {
385 - return Template::print_message( __( 'Course is invalid!', 'learnpress' ), 'error', false );
386 - }
387 -
388 - if ( ! class_exists( 'LP_Meta_Box_Course' ) ) {
389 - require_once LP_PLUGIN_PATH . 'inc/admin/views/meta-boxes/course/settings.php';
390 - }
391 -
392 - add_filter( 'learnpress/course/metabox/tabs', [ $this, 'filter_course_builder_settings_tabs' ], 999 );
393 - add_filter(
394 - 'learn-press/course/meta-box/assessment/final-quiz/edit-link',
395 - [
396 - $this,
397 - 'filter_course_builder_assessment_final_quiz_edit_link',
398 - ],
399 - 10,
400 - 2
401 - );
402 -
403 - $metabox = \LP_Meta_Box_Course::instance();
404 - ob_start();
405 - $metabox->output( $courseModel );
406 - $settings = ob_get_clean();
407 -
408 - remove_filter( 'learnpress/course/metabox/tabs', [ $this, 'filter_course_builder_settings_tabs' ], 999 );
409 - remove_filter(
410 - 'learn-press/course/meta-box/assessment/final-quiz/edit-link',
411 - [
412 - $this,
413 - 'filter_course_builder_assessment_final_quiz_edit_link',
414 - ],
415 - 10
416 - );
417 -
418 - $output = [
419 - 'wrapper' => sprintf( '<div class="cb-section__course-edit" data-course-id="%s">', $course_id ),
420 - 'form_setting' => '<form name="lp-form-setting-course" class="lp-form-setting-course" method="post" enctype="multipart/form-data">',
421 - 'settings' => $settings,
422 - 'form_setting_end' => '</form>',
423 - 'wrapper_end' => '</div>',
424 - ];
425 -
426 - return Template::combine_components( $output );
427 - }
428 -
429 - /**
430 - * Allow callback for AJAX.
431 - * @use self::render_edit_course_curriculum
432 - * @use self::render_html
433 - *
434 - * @param array $callbacks
435 - *
436 - * @return array
437 - */
438 - public function allow_callback( array $callbacks ): array {
439 - $callbacks[] = AdminEditCurriculumTemplate::class . ':render_edit_course_curriculum';
440 -
441 - return $callbacks;
442 - }
443 -
444 - /**
445 - * Render Course Builder course title field.
446 - *
447 - * Uses the raw post title for existing courses so the edit field does not receive
448 - * display filters. New courses start with an empty value and still render the
449 - * optional AI button when AI settings are enabled.
450 - *
451 - * @param CourseModel|false|null $courseModel Course model being edited, or empty when creating a new course.
452 - *
453 - * @return string HTML markup for the title input, character count, and optional AI button.
454 - * @since 4.3.0
455 - * @version 1.0.0
456 - */
457 - public function edit_title( $courseModel ) {
458 - $title = '';
459 - if ( ! empty( $courseModel ) ) {
460 - $post_id = absint( $courseModel->get_id() );
461 - $title = $post_id ? (string) get_post_field( 'post_title', $post_id, 'raw' ) : '';
462 - }
463 - $char_count = mb_strlen( wp_strip_all_tags( $title ) );
464 - $ai_button = $this->html_overview_ai_button( '#lp-tmpl-edit-title-ai' );
465 - $edit = [
466 - 'wrapper' => '<div class="cb-course-edit-title">',
467 - 'label_wrap' => '<div class="cb-course-edit-title__label-wrap">',
468 - 'label' => sprintf( '<label for="title" class="cb-course-edit-title__label">%s <span class="required">*</span></label>', __( 'Course Title', 'learnpress' ) ),
469 - 'char_count' => sprintf( '<span class="cb-course-edit-title__char-count">%s</span>', sprintf( __( '%d characters', 'learnpress' ), $char_count ) ),
470 - 'ai_button' => $ai_button,
471 - 'label_wrap_end' => '</div>',
472 - 'input' => sprintf( '<input type="text" name="course_title" size="30" value="%s" id="title" class="cb-course-edit-title__input" placeholder="%s">', esc_attr( $title ), esc_attr__( 'example', 'learnpress' ) ),
473 - 'wrapper_end' => '</div>',
474 - ];
475 -
476 - return Template::combine_components( $edit );
477 - }
478 -
479 - /**
480 - * Render Course Builder permalink editor.
481 - *
482 - * Permalink editing is hidden for unsaved courses. Existing courses use
483 - * get_sample_permalink() when available to display a publish-style URL for slug
484 - * editing, while the preview link keeps the current permalink as its href.
485 - *
486 - * @param CourseModel|false|null $courseModel Course model being edited, or empty when creating a new course.
487 - *
488 - * @return string HTML markup for the permalink display/editor, or an empty string for new courses.
489 - * @since 4.3.0
490 - * @version 1.0.0
491 - */
492 - public function edit_permalink( $courseModel ) {
493 - $post_id = ! empty( $courseModel ) ? $courseModel->get_id() : '';
494 - $post_name = '';
495 -
496 - // Hide permalink for new courses
497 - if ( empty( $post_id ) || $post_id === CourseBuilder::POST_NEW ) {
498 - return '';
499 - }
500 -
501 - if ( $post_id ) {
502 - $post = get_post( $post_id );
503 - $post_name = $post ? $post->post_name : '';
504 - }
505 -
506 - // Get base URL for courses
507 - $courses_page_id = learn_press_get_page_id( 'courses' );
508 - $base_url = '';
509 - if ( $courses_page_id ) {
510 - $base_url = trailingslashit( get_permalink( $courses_page_id ) );
511 - } else {
512 - $base_url = trailingslashit( home_url() ) . 'courses/';
513 - }
514 -
515 - $full_url = urldecode( (string) get_permalink( $post_id ) );
516 - $display_data = $this->get_course_display_permalink_data( (int) $post_id, (string) $post_name );
517 - $display_url = (string) ( $display_data['url'] ?? '' );
518 - $editor_slug = (string) ( $display_data['slug'] ?? '' );
519 -
520 - if ( empty( $display_url ) ) {
521 - $display_url = $full_url;
522 - }
523 -
524 - if ( empty( $full_url ) ) {
525 - $full_url = $display_url;
526 - }
527 -
528 - if ( empty( $editor_slug ) ) {
529 - $editor_slug = (string) $post_name;
530 - }
531 -
532 - if ( empty( $editor_slug ) && ! empty( $display_url ) ) {
533 - $display_path = parse_url( $display_url, PHP_URL_PATH );
534 - if ( is_string( $display_path ) && '' !== $display_path ) {
535 - $editor_slug = basename( untrailingslashit( $display_path ) );
536 - }
537 - }
538 -
539 - // Use publish-style permalink as editable base, but keep href as current permalink.
540 - if (
541 - ! empty( $editor_slug ) &&
542 - false === strpos( $display_url, '?p=' ) &&
543 - false === strpos( $display_url, '&p=' ) &&
544 - false === strpos( $display_url, '?lp_course=' ) &&
545 - false === strpos( $display_url, '&lp_course=' )
546 - ) {
547 - $base_url = trailingslashit( preg_replace( '/' . preg_quote( $editor_slug, '/' ) . '\/?$/', '', $display_url ) );
548 - }
549 -
550 - $state_a = sprintf(
551 - '<span class="cb-permalink-label">%s</span>
552 - <div class="cb-permalink-display">
553 - <a href="%s" target="_blank" class="cb-permalink-url">%s</a>
554 - <button type="button" class="cb-permalink-edit-btn" title="%s">
555 - <span class="dashicons dashicons-edit"></span>
556 - </button>
557 - </div>',
558 - __( 'Permalink', 'learnpress' ),
559 - esc_url( $full_url ),
560 - esc_html( $display_url ),
561 - __( 'Edit', 'learnpress' )
562 - );
563 -
564 - $state_b = sprintf(
565 - '<div class="cb-permalink-editor lp-hidden">
566 - <span class="cb-permalink-prefix">%s</span>
567 - <div class="cb-permalink-input-row">
568 - <input type="text" name="course_permalink" id="course_permalink" value="%s" class="cb-permalink-slug-input" placeholder="%s">
569 - <div class="cb-permalink-actions">
570 - <button type="button" class="cb-permalink-ok-btn">%s</button>
571 - <button type="button" class="cb-permalink-cancel-btn">%s</button>
572 - </div>
573 - </div>
574 - </div>',
575 - esc_html( $base_url ),
576 - esc_attr( $editor_slug ),
577 - esc_attr__( 'your-slug', 'learnpress' ),
578 - __( 'OK', 'learnpress' ),
579 - __( 'Cancel', 'learnpress' )
580 - );
581 -
582 - $hidden_base = sprintf(
583 - '<input type="hidden" id="cb-permalink-base-url" value="%s">',
584 - esc_attr( $base_url )
585 - );
586 -
587 - $edit = [
588 - 'wrapper' => '<div class="cb-course-edit-permalink">',
589 - 'state_a' => $state_a,
590 - 'state_b' => $state_b,
591 - 'hidden_base' => $hidden_base,
592 - 'wrapper_end' => '</div>',
593 - ];
594 -
595 - return Template::combine_components( $edit );
596 - }
597 -
598 - /**
599 - * Get permalink display URL in publish-style format for editing slug.
600 - *
601 - * @param int $post_id
602 - * @param string $post_name
603 - *
604 - * @return array<string, string>
605 - */
606 - private function get_course_display_permalink_data( int $post_id, string $post_name = '' ): array {
607 - $display_url = urldecode( (string) get_permalink( $post_id ) );
608 - $sample_slug = $post_name;
609 -
610 - if ( ! function_exists( 'get_sample_permalink' ) ) {
611 - require_once ABSPATH . 'wp-admin/includes/post.php';
612 - }
613 -
614 - if ( function_exists( 'get_sample_permalink' ) ) {
615 - $sample_permalink = get_sample_permalink( $post_id );
616 - if ( is_array( $sample_permalink ) && ! empty( $sample_permalink[0] ) ) {
617 - $sample_slug = ! empty( $sample_permalink[1] ) ? (string) $sample_permalink[1] : $sample_slug;
618 - $display_url = str_replace(
619 - [ '%postname%', '%pagename%' ],
620 - $sample_slug,
621 - (string) $sample_permalink[0]
622 - );
623 - }
624 - }
625 -
626 - $post = get_post( $post_id );
627 - if ( $post instanceof \WP_Post ) {
628 - $display_url = \LP_Helper::handle_lp_permalink_structure( $display_url, $post );
629 - }
630 -
631 - return [
632 - 'url' => urldecode( $display_url ),
633 - 'slug' => urldecode( (string) $sample_slug ),
634 - ];
635 - }
636 -
637 - /**
638 - * Render Course Builder description editor.
639 - *
640 - * Uses the current course post content when editing an existing CourseModel.
641 - * For new courses, the editor starts with an empty value. AI button rendering
642 - * is delegated to html_overview_ai_button().
643 - *
644 - * @param CourseModel|false|null $courseModel Course model being edited, or empty when creating a new course.
645 - *
646 - * @return string HTML markup for the course description editor.
647 - */
648 - public function edit_desc( $courseModel ) {
649 - $desc = $courseModel ? $courseModel->post_content : '';
650 -
651 - $editor_id = 'course_description_editor';
652 - $ai_button = $this->html_overview_ai_button( '#lp-tmpl-edit-description-ai' );
653 - $editor_settings = array(
654 - 'textarea_name' => 'course_description',
655 - 'textarea_rows' => 10,
656 - 'teeny' => false,
657 - 'media_buttons' => true,
658 - 'tinymce' => array(
659 - 'content_style' => "body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; font-size: 14px; line-height: 1.6; color: #1e1e1e; }",
660 - 'toolbar1' => 'formatselect,bold,italic,underline,bullist,numlist,blockquote,alignleft,aligncenter,alignright,link,unlink,spellchecker,wp_adv',
661 - 'toolbar2' => 'strikethrough,hr,forecolor,pastetext,removeformat,charmap,outdent,indent,undo,redo,wp_help',
662 - ),
663 - 'quicktags' => true,
664 - );
665 -
666 - $edit = [
667 - 'wrapper' => '<div class="cb-course-edit-desc">',
668 - 'label_wrap' => '<div class="cb-course-edit-desc__label-wrap">',
669 - 'label' => sprintf( '<label for="course_description" class="cb-course-edit-desc__label">%s</label>', __( 'Description', 'learnpress' ) ),
670 - 'ai_button' => $ai_button,
671 - 'label_wrap_end' => '</div>',
672 - 'edit' => AdminTemplate::editor_tinymce(
673 - $desc,
674 - $editor_id,
675 - $editor_settings
676 - ),
677 - 'wrapper_end' => '</div>',
678 - ];
679 -
680 - return Template::combine_components( $edit );
681 - }
682 -
683 - /**
684 - * Check if Overview AI button and templates should be shown.
685 - *
686 - * @return bool
687 - */
688 - protected function can_show_overview_ai_button(): bool {
689 - return \LP_Settings::get_option( 'enable_open_ai', 'no' ) === 'yes'
690 - && ! empty( \LP_Settings::get_option( 'open_ai_secret_key', '' ) );
691 - }
692 -
693 - /**
694 - * Render icon-only AI button in Course Builder overview.
695 - *
696 - * @param string $template_id
697 - *
698 - * @return string
699 - */
700 - protected function html_overview_ai_button( string $template_id ): string {
701 - if ( ! $this->can_show_overview_ai_button() ) {
702 - return '';
703 - }
704 -
705 - $button_label = esc_html__( 'Generate with AI', 'learnpress' );
706 -
707 - return sprintf(
708 - '<button type="button" class="cb-course-edit-ai-btn lp-btn-generate-with-ai" data-template="%1$s" title="%2$s" aria-label="%2$s"><i class="lp-ico-ai" aria-hidden="true"></i></button>',
709 - esc_attr( $template_id ),
710 - esc_attr( $button_label )
711 - );
712 - }
713 -
714 - /**
715 - * Render AI popup templates for Course Builder overview edit page.
716 - *
717 - * @return string
718 - */
719 - protected function html_overview_ai_templates(): string {
720 - if ( ! $this->can_show_overview_ai_button() ) {
721 - return '';
722 - }
723 -
724 - try {
725 - return AdminEditWithAITemplate::instance()->render_for_frontend( [ 'title', 'description', 'image' ] );
726 - } catch ( Throwable $e ) {
727 - error_log( __METHOD__ . ': ' . $e->getMessage() );
728 - }
729 -
730 - return '';
731 - }
732 -
733 - /**
734 - * Render AI popup template for curriculum edit in Course Builder.
735 - *
736 - * @return string
737 - */
738 - protected function html_curriculum_ai_templates(): string {
739 - if ( ! $this->can_show_overview_ai_button() ) {
740 - return '';
741 - }
742 -
743 - try {
744 - return AdminEditCourseCurriculumWithAITemplate::instance()->render_for_frontend();
745 - } catch ( Throwable $e ) {
746 - error_log( __METHOD__ . ': ' . $e->getMessage() );
747 - }
748 -
749 - return '';
750 - }
751 -
752 - /**
753 - * Render Course Builder course category selector.
754 - *
755 - * Reuses the WordPress post_categories_meta_box() output for course_category,
756 - * then adds Course Builder search and "add new category" controls. During render,
757 - * checked_ontop is forced off so the checklist keeps taxonomy order.
758 - *
759 - * @param CourseModel|false|null $courseModel Course model being edited, or empty when creating a new course.
760 - *
761 - * @return string HTML markup for the course category checklist, search toolbar, and add-new form.
762 - * @since 4.3.0
763 - * @version 1.0.0
764 - */
765 - public function edit_categories( $courseModel ) {
766 - if ( ! function_exists( 'post_categories_meta_box' ) ) {
767 - require_once ABSPATH . 'wp-admin/includes/meta-boxes.php';
768 - }
769 - if ( ! function_exists( 'wp_popular_terms_checklist' ) ) {
770 - require_once ABSPATH . 'wp-admin/includes/template.php';
771 - }
772 -
773 - $post_id = ! empty( $courseModel ) ? $courseModel->get_id() : get_the_ID();
774 - $post = get_post( $post_id );
775 -
776 - $force_checked_ontop_false = function ( $args ) {
777 - if ( isset( $args['taxonomy'] ) && 'course_category' === $args['taxonomy'] ) {
778 - $args['checked_ontop'] = false;
779 - }
780 -
781 - return $args;
782 - };
783 -
784 - ob_start();
785 -
786 - add_filter( 'wp_terms_checklist_args', $force_checked_ontop_false );
787 -
788 - if ( function_exists( 'post_categories_meta_box' ) ) {
789 - \post_categories_meta_box(
790 - $post,
791 - array(
792 - 'id' => 'course_categorydiv',
793 - 'title' => __( 'Categories', 'learnpress' ),
794 - 'callback' => 'post_categories_meta_box',
795 - 'args' => array(
796 - 'taxonomy' => 'course_category',
797 - 'checked_ontop' => false,
798 - ),
799 - )
800 - );
801 - }
802 -
803 - remove_filter( 'wp_terms_checklist_args', $force_checked_ontop_false );
804 - $html_meta_box = ob_get_clean();
805 -
806 - // Build add new category form (between header and content)
807 - $parent_terms = get_terms(
808 - [
809 - 'taxonomy' => 'course_category',
810 - 'hide_empty' => false,
811 - ]
812 - );
813 - $parent_options = sprintf( '<option value="0">— %s —</option>', __( 'Parent Category', 'learnpress' ) );
814 - if ( ! empty( $parent_terms ) && ! is_wp_error( $parent_terms ) ) {
815 - foreach ( $parent_terms as $term ) {
816 - $parent_options .= sprintf(
817 - '<option value="%d">%s</option>',
818 - $term->term_id,
819 - esc_html( $term->name )
820 - );
821 - }
822 - }
823 -
824 - $form_add_category = sprintf(
825 - '<div class="cb-course-edit-terms__form-add-category" style="display:none;">
826 - <input type="text" class="cb-course-edit-category__input" placeholder="%s" id="cb-newcourse_category" />
827 - <select class="cb-course-edit-category__select-parent" id="cb-newcourse_category_parent">%s</select>
828 - <button type="button" class="cb-course-edit-category__btn-cancel">%s</button>
829 - <button type="button" class="cb-course-edit-category__btn-save" id="cb-course_category-add-submit">%s</button>
830 - </div>',
831 - esc_attr__( 'Enter Category Name', 'learnpress' ),
832 - $parent_options,
833 - esc_html__( 'Cancel', 'learnpress' ),
834 - esc_html__( 'Add', 'learnpress' ),
835 - );
836 -
837 - $edit = [
838 - 'wrapper' => '<div class="cb-course-edit-categories__wrapper">',
839 - 'header' => '<div class="cb-terms-header">',
840 - 'label_wrap' => '<div class="cb-terms-header__label-wrap">',
841 - 'label' => sprintf( '<label class="cb-terms-header__label">%s</label>', __( 'Categories', 'learnpress' ) ),
842 - 'btn_search' => sprintf(
843 - '<button type="button" class="cb-terms-header__btn-search" data-toggle-target="#cb-course-edit-categories-search-toolbar" aria-expanded="false" aria-label="%s">
844 - <i class="lp-icon-search"></i>
845 - </button>',
846 - esc_attr__( 'Search categories', 'learnpress' )
847 - ),
848 - 'label_wrap_end' => '</div>',
849 - 'btn_add_new' => sprintf( '<button class="cb-course-edit-category__btn-add-new cb-terms-header__btn-add-new">%s</button>', __( 'Add New', 'learnpress' ) ),
850 - 'header_end' => '</div>',
851 - 'form_add_category' => $form_add_category,
852 - 'search' => sprintf(
853 - '<div class="cb-course-edit-categories__toolbar cb-terms-search-toolbar" id="cb-course-edit-categories-search-toolbar">
854 - <label class="cb-course-edit-categories__search-wrap">
855 - <span class="screen-reader-text">%1$s</span>
856 - <input type="search" class="cb-course-edit-category__search-input" placeholder="%2$s" />
857 - </label>
858 - </div>',
859 - esc_html__( 'Search categories', 'learnpress' ),
860 - esc_attr__( 'Search categories', 'learnpress' )
861 - ),
862 - 'content' => $html_meta_box,
863 - 'wrapper_end' => '</div>',
864 - ];
865 -
866 - return Template::combine_components( $edit );
867 - }
868 -
869 - /**
870 - * Render Course Builder course tag selector.
871 - *
872 - * Builds tag chips from all course tags, placing selected tags before available
873 - * tags so the current course state is visible first. The returned markup includes
874 - * search, empty-state text, and add-new controls consumed by the builder runtime.
875 - *
876 - * @param CourseModel|false|null $courseModel Course model being edited, or empty when creating a new course.
877 - *
878 - * @return string HTML markup for the selected/available tag chips, search toolbar, and add-new form.
879 - * @since 4.3.0
880 - * @version 1.0.0
881 - */
882 - public function edit_tags( $courseModel ) {
883 - $course_terms = ! empty( $courseModel ) ? $courseModel->get_tags() : [];
884 - $tags = get_terms(
885 - [
886 - 'taxonomy' => LP_COURSE_TAXONOMY_TAG,
887 - 'hide_empty' => false,
888 - ]
889 - );
890 -
891 - $selected_tag_ids = array_map(
892 - function ( $term ) {
893 - return (int) $term->term_id;
894 - },
895 - $course_terms
896 - );
897 -
898 - $html_selected_chips = '';
899 - $html_available_chips = '';
900 -
901 - if ( ! empty( $tags ) && ! is_wp_error( $tags ) ) {
902 - foreach ( $tags as $tag ) {
903 - $tag_id = $tag->term_id;
904 - $tag_name = $tag->name;
905 - $tag_count = $tag->count;
906 - $is_checked = in_array( (int) $tag_id, $selected_tag_ids, true );
907 - $html_chip = $this->input_checkbox_tag_item( $tag_id, $tag_name, $is_checked, $tag_count );
908 -
909 - if ( $is_checked ) {
910 - $html_selected_chips .= $html_chip;
911 - } else {
912 - $html_available_chips .= $html_chip;
913 - }
914 - }
915 - }
916 -
917 - $html_chips = $html_selected_chips . $html_available_chips;
918 - $count_all = substr_count( $html_chips, 'class="cb-tag-chip"' );
919 - $empty_default = esc_html__( 'No tags found.', 'learnpress' );
920 - $empty_search = esc_html__( 'No matching tags.', 'learnpress' );
921 -
922 - $toolbar = sprintf(
923 - '<div class="cb-course-edit-tags__toolbar cb-terms-search-toolbar" id="cb-course-edit-tags-search-toolbar">
924 - <label class="cb-course-edit-tags__search-wrap">
925 - <span class="screen-reader-text">%1$s</span>
926 - <input type="search" class="cb-course-edit-tags__search-input" placeholder="%2$s" />
927 - </label>
928 - </div>',
929 - esc_html__( 'Search tags', 'learnpress' ),
930 - esc_attr__( 'Search tags', 'learnpress' )
931 - );
932 -
933 - $edit = [
934 - 'wrapper' => '<div class="cb-course-edit-tags__wrapper">',
935 - 'header' => '<div class="cb-terms-header">',
936 - 'label_wrap' => '<div class="cb-terms-header__label-wrap">',
937 - 'label' => sprintf( '<label class="cb-terms-header__label">%s</label>', __( 'Tags', 'learnpress' ) ),
938 - 'btn_search' => sprintf(
939 - '<button type="button" class="cb-terms-header__btn-search" data-toggle-target="#cb-course-edit-tags-search-toolbar" aria-expanded="false" aria-label="%s">
940 - <i class="lp-icon-search"></i>
941 - </button>',
942 - esc_attr__( 'Search tags', 'learnpress' )
943 - ),
944 - 'label_wrap_end' => '</div>',
945 - 'btn_add_new' => sprintf( '<button class="cb-course-edit-tag__btn-add-new cb-terms-header__btn-add-new">%s</button>', __( 'Add New', 'learnpress' ) ),
946 - 'header_end' => '</div>',
947 - 'form_add_tag_wrapper' => '<div class="cb-course-edit-terms__form-add-tag" style="display:none;">',
948 - 'input' => '<input type="text" class="cb-course-edit-tags__input" placeholder="' . esc_attr__( 'Enter Tag Name', 'learnpress' ) . '"/>',
949 - 'btn_cancel' => sprintf( '<button type="button" class="cb-course-edit-tag__btn-cancel">%s</button>', __( 'Cancel', 'learnpress' ) ),
950 - 'button' => '<button type="button" class="cb-course-edit-tags__btn-save">' . esc_html__( 'Add', 'learnpress' ) . '</button>',
951 - 'form_add_tag_wrapper_end' => '</div>',
952 - 'toolbar' => $toolbar,
953 - 'wrapper_checkbox' => '<div class="cb-course-edit-tags__checkbox-wrapper">',
954 - 'checkbox' => $html_chips,
955 - 'wrapper_checkbox_end' => '</div>',
956 - 'empty' => sprintf(
957 - '<p class="cb-course-edit-tags__empty%1$s" data-empty-default="%2$s" data-empty-search="%3$s">%2$s</p>',
958 - $count_all > 0 ? ' lp-hidden' : '',
959 - esc_attr( $empty_default ),
960 - esc_attr( $empty_search )
961 - ),
962 - 'wrapper_end' => '</div>',
963 - ];
964 -
965 - return Template::combine_components( $edit );
966 - }
967 -
968 - public function input_checkbox_tag_item( $term_id, $term_name, $is_checked, $count = 0 ) {
969 - if ( 0 === $count ) {
970 - $tag_obj = get_term( $term_id, LP_COURSE_TAXONOMY_TAG );
971 - if ( $tag_obj && ! is_wp_error( $tag_obj ) ) {
972 - $count = $tag_obj->count;
973 - }
974 - }
975 -
976 - $tag_name_search = wp_strip_all_tags( $term_name );
977 - if ( function_exists( 'mb_strtolower' ) ) {
978 - $tag_name_search = mb_strtolower( $tag_name_search );
979 - } else {
980 - $tag_name_search = strtolower( $tag_name_search );
981 - }
982 -
983 - $html = sprintf(
984 - '<div class="cb-tag-chip" data-tag-name="%s" data-term-id="%d">',
985 - esc_attr( $tag_name_search ),
986 - (int) $term_id
987 - );
988 - $html .= sprintf(
989 - '<input type="checkbox" name="course_tags[]" value="%s" id="course_tag_%s" %s>',
990 - $term_id,
991 - $term_id,
992 - checked( $is_checked, true, false )
993 - );
994 - $html .= sprintf(
995 - '<label for="course_tag_%s"><span class="cb-tag-chip__name">%s</span><span class="cb-tag-chip__count">(%d)</span><span class="cb-tag-chip__remove">&times;</span></label>',
996 - $term_id,
997 - esc_html( $term_name ),
998 - $count
999 - );
1000 - $html .= '</div>';
1001 -
1002 - return $html;
1003 - }
1004 -
1005 - /**
1006 - * Render Course Builder featured image uploader.
1007 - *
1008 - * Existing courses render the current thumbnail preview with replace/remove
1009 - * controls. Courses without a thumbnail render the upload dropzone and hidden
1010 - * course_thumbnail_id field used by the builder save flow.
1011 - *
1012 - * @param CourseModel|false|null $courseModel Course model being edited, or empty when creating a new course.
1013 - *
1014 - * @return string HTML markup for the featured image dropzone, thumbnail hidden field, and optional AI button.
1015 - * @since 4.3.0
1016 - * @version 1.0.0
1017 - */
1018 - public function edit_featured_image( $courseModel ) {
1019 - $post_id = ! empty( $courseModel ) ? $courseModel->get_id() : '';
1020 - $ai_button = $this->html_overview_ai_button( '#lp-tmpl-edit-image-ai' );
1021 -
1022 - $thumbnail_id = ! empty( $post_id ) ? get_post_thumbnail_id( $post_id ) : '';
1023 - $thumbnail_url = '';
1024 - $thumbnail_alt = '';
1025 -
1026 - if ( $thumbnail_id ) {
1027 - $thumbnail_url = wp_get_attachment_image_url( $thumbnail_id, 'medium' );
1028 - $thumbnail_alt = get_post_meta( $thumbnail_id, '_wp_attachment_image_alt', true );
1029 - }
1030 -
1031 - $has_image = ! empty( $thumbnail_url );
1032 -
1033 - $featured_image_html = '<div class="cb-featured-image-container">';
1034 -
1035 - // Upload area
1036 - $featured_image_html .= sprintf(
1037 - '<div class="cb-featured-image-dropzone %s" data-post-id="%s">',
1038 - $has_image ? 'has-image' : '',
1039 - esc_attr( $post_id )
1040 - );
1041 -
1042 - if ( $has_image ) {
1043 - $featured_image_html .= sprintf(
1044 - '<img src="%s" alt="%s" class="cb-featured-image-preview__img">',
1045 - esc_url( $thumbnail_url ),
1046 - esc_attr( $thumbnail_alt )
1047 - );
1048 - } else {
1049 - $featured_image_html .= '<div class="cb-featured-image-upload-content">';
1050 - $featured_image_html .= '<span class="cb-featured-image-icon"><span class="cb-featured-image-icon__image" aria-hidden="true"></span></span>';
1051 - $featured_image_html .= sprintf(
1052 - '<p class="cb-featured-image-text"><a href="#" class="cb-featured-image-link">%s</a></p>',
1053 - __( 'Click to upload', 'learnpress' )
1054 - );
1055 - $featured_image_html .= sprintf(
1056 - '<p class="cb-featured-image-hint">%s</p>',
1057 - __( 'JPG, JPEG, PNG less than 1MB', 'learnpress' )
1058 - );
1059 - $featured_image_html .= '</div>';
1060 - }
1061 -
1062 - $featured_image_html .= '</div>';
1063 -
1064 - $featured_image_html .= sprintf(
1065 - '<input type="hidden" name="course_thumbnail_id" id="course_thumbnail_id" value="%s">',
1066 - esc_attr( $thumbnail_id )
1067 - );
1068 -
1069 - // Action buttons wrapper
1070 - $featured_image_html .= '<div class="cb-featured-image-actions">';
1071 -
1072 - // Remove button (only show when has image)
1073 - if ( $has_image ) {
1074 - $featured_image_html .= sprintf(
1075 - '<button type="button" class="cb-remove-featured-image">%s</button>',
1076 - '<span class="cb-remove-featured-image__icon" aria-hidden="true"></span>'
1077 - );
1078 -
1079 - $featured_image_html .= sprintf(
1080 - '<button type="button" class="cb-change-featured-image">%s</button>',
1081 - __( 'Replace', 'learnpress' )
1082 - );
1083 - }
1084 -
1085 - $featured_image_html .= '</div>'; // End actions wrapper
1086 - $featured_image_html .= '</div>'; // End container
1087 -
1088 - $edit = [
1089 - 'wrapper' => '<div class="cb-course-edit-featured-image">',
1090 - 'label_wrap' => '<div class="cb-course-edit-featured-image__label-wrap">',
1091 - 'label' => sprintf(
1092 - '<label class="cb-course-edit-featured-image__title">%s</label>',
1093 - __( 'Featured Image', 'learnpress' )
1094 - ),
1095 - 'ai_button' => $ai_button,
1096 - 'label_wrap_end' => '</div>',
1097 - 'edit' => $featured_image_html,
1098 - 'wrapper_end' => '</div>',
1099 - ];
1100 -
1101 - return Template::combine_components( $edit );
1102 - }
1103 -
1104 - /**
1105 - * Render publish panel (status, visibility, publish date, danger zone) for Course Builder overview.
1106 - *
1107 - * @param CourseModel|false|string|null $courseModel
1108 - *
1109 - * @return string
1110 - */
1111 - public function edit_publish( $courseModel ): string {
1112 - $post_id = ! empty( $courseModel ) ? absint( $courseModel->get_id() ) : 0;
1113 - $post = $post_id ? get_post( $post_id ) : null;
1114 -
1115 - $current_status = 'draft';
1116 - if ( $post && ! empty( $post->post_status ) ) {
1117 - $current_status = sanitize_key( $post->post_status );
1118 - }
1119 -
1120 - $status_for_select = in_array( $current_status, [ 'publish', 'draft', 'pending', 'future' ], true )
1121 - ? $current_status
1122 - : 'publish';
1123 - $current_password = $post ? (string) ( $post->post_password ?? '' ) : '';
1124 - $visibility = 'private' === $current_status
1125 - ? 'private'
1126 - : ( ! empty( $current_password ) ? 'password' : 'public' );
1127 - $published_on = '';
1128 -
1129 - if ( $post && ! empty( $post->post_date ) && '0000-00-00 00:00:00' !== $post->post_date ) {
1130 - $published_on = wp_date( 'Y-m-d\TH:i', strtotime( $post->post_date ), wp_timezone() );
1131 - }
1132 -
1133 - $has_future_publish_date = false;
1134 - if ( $post && ! empty( $post->post_date ) && '0000-00-00 00:00:00' !== $post->post_date ) {
1135 - $has_future_publish_date = strtotime( $post->post_date ) > current_time( 'timestamp' );
1136 - }
1137 -
1138 - $is_scheduled_status = 'future' === $status_for_select
1139 - || ( in_array( $status_for_select, [ 'draft', 'pending' ], true ) && $has_future_publish_date );
1140 - $primary_status_value = $is_scheduled_status ? 'future' : 'publish';
1141 - $primary_status_label = $is_scheduled_status
1142 - ? __( 'Scheduled', 'learnpress' )
1143 - : __( 'Published', 'learnpress' );
1144 - $selected_status_for_ui = in_array( $status_for_select, [ 'draft', 'pending' ], true )
1145 - ? $status_for_select
1146 - : $primary_status_value;
1147 - $status_options = [
1148 - $primary_status_value => $primary_status_label,
1149 - 'draft' => __( 'Draft', 'learnpress' ),
1150 - 'pending' => __( 'Pending Review', 'learnpress' ),
1151 - ];
1152 -
1153 - $publish_date_label = $has_future_publish_date
1154 - ? __( 'Scheduled for', 'learnpress' )
1155 - : __( 'Published on', 'learnpress' );
1156 -
1157 - $status_options_html = '';
1158 - foreach ( $status_options as $value => $label ) {
1159 - $status_options_html .= sprintf(
1160 - '<option value="%1$s" %2$s>%3$s</option>',
1161 - esc_attr( $value ),
1162 - selected( $selected_status_for_ui, $value, false ),
1163 - esc_html( $label )
1164 - );
1165 - }
1166 -
1167 - $visibility_options_html = sprintf(
1168 - '<option value="public" %1$s>%2$s</option><option value="private" %3$s>%4$s</option><option value="password" %5$s>%6$s</option>',
1169 - selected( $visibility, 'public', false ),
1170 - esc_html__( 'Public', 'learnpress' ),
1171 - selected( $visibility, 'private', false ),
1172 - esc_html__( 'Private', 'learnpress' ),
1173 - selected( $visibility, 'password', false ),
1174 - esc_html__( 'Password protected', 'learnpress' )
1175 - );
1176 -
1177 - $edit = [
1178 - 'wrapper' => '<div class="cb-course-edit-publish">',
1179 - 'title' => sprintf( '<h3 class="cb-course-edit-publish__title">%s</h3>', esc_html__( 'Publish', 'learnpress' ) ),
1180 - 'status_row' => sprintf(
1181 - '<div class="cb-course-edit-publish__row">
1182 - <label for="cb-course-publish-status" class="cb-course-edit-publish__label">%1$s</label>
1183 - <select id="cb-course-publish-status" name="cb_course_publish_status" class="cb-course-edit-publish__control" data-publish-label="%3$s" data-future-label="%4$s" data-primary-status="%5$s">%2$s</select>
1184 - </div>',
1185 - esc_html__( 'Status', 'learnpress' ),
1186 - $status_options_html,
1187 - esc_attr__( 'Published', 'learnpress' ),
1188 - esc_attr__( 'Scheduled', 'learnpress' ),
1189 - esc_attr( $primary_status_value )
1190 - ),
1191 - 'visibility_row' => sprintf(
1192 - '<div class="cb-course-edit-publish__row">
1193 - <label for="cb-course-publish-visibility" class="cb-course-edit-publish__label">%1$s</label>
1194 - <select id="cb-course-publish-visibility" name="cb_course_publish_visibility" class="cb-course-edit-publish__control">%2$s</select>
1195 - </div>',
1196 - esc_html__( 'Visibility', 'learnpress' ),
1197 - $visibility_options_html
1198 - ),
1199 - 'password_row' => sprintf(
1200 - '<div class="cb-course-edit-publish__row cb-course-edit-publish__password-row %1$s">
1201 - <label for="cb-course-publish-password" class="cb-course-edit-publish__label">%2$s</label>
1202 - <input type="text" id="cb-course-publish-password" name="cb_course_publish_password" class="cb-course-edit-publish__control" value="%3$s" autocomplete="new-password">
1203 - </div>',
1204 - 'password' === $visibility ? '' : 'lp-hidden',
1205 - esc_html__( 'Password', 'learnpress' ),
1206 - esc_attr( $current_password )
1207 - ),
1208 - 'published_on_row' => sprintf(
1209 - '<div class="cb-course-edit-publish__row">
1210 - <label for="cb-course-publish-date" id="cb-course-publish-date-label" class="cb-course-edit-publish__label">%1$s</label>
1211 - <input type="datetime-local" id="cb-course-publish-date" name="cb_course_publish_date" class="cb-course-edit-publish__control" value="%2$s">
1212 - </div>',
1213 - esc_html( $publish_date_label ),
1214 - esc_attr( $published_on )
1215 - ),
1216 - 'wrapper_end' => '</div>',
1217 - ];
1218 -
1219 - return Template::combine_components( $edit );
1220 - }
1221 -
1222 - /**
1223 - * Render Course Builder curriculum editor.
1224 - *
1225 - * Delegates the main curriculum layout to AdminEditCurriculumTemplate with the
1226 - * is_course_builder context flag, then appends popup templates and optional AI
1227 - * curriculum templates needed by the builder UI.
1228 - *
1229 - * @param CourseModel $courseModel Course model used to render curriculum sections and popup template IDs.
1230 - *
1231 - * @return string HTML markup for the curriculum editor plus Course Builder popup and AI templates.
1232 - * @since 4.3.0
1233 - * @version 1.0.0
1234 - */
1235 - protected function html_curriculum( CourseModel $courseModel ): string {
1236 - ob_start();
1237 - AdminEditCurriculumTemplate::instance()->edit_course_curriculum_layout(
1238 - $courseModel,
1239 - [ 'is_course_builder' => true ]
1240 - );
1241 - $html_curriculum = ob_get_clean();
1242 -
1243 - return $html_curriculum
1244 - . $this->html_curriculum_popup_templates( $courseModel )
1245 - . $this->html_curriculum_ai_templates();
1246 - }
1247 -
1248 - protected function html_curriculum_popup_templates( CourseModel $courseModel ): string {
1249 - $popup_templates = apply_filters(
1250 - 'learn-press/course-builder/courses/curriculum/popup-templates',
1251 - [
1252 - 'lesson' => sprintf(
1253 - '<script type="text/template" id="%1$s"><div class="lp-builder-popup-overlay"></div><div class="lp-builder-popup lp-builder-popup--loading">%2$s</div></script>',
1254 - esc_attr(
1255 - sprintf(
1256 - 'lp-tmpl-builder-popup-curriculum-lesson-course-%d',
1257 - $courseModel->get_id()
1258 - )
1259 - ),
1260 - TemplateAJAX::load_content_via_ajax(
1261 - [
1262 - 'id_url' => 'builder-popup-lesson-curriculum',
1263 - 'lesson_id' => 0,
1264 - 'html_no_load_ajax_first' => sprintf(
1265 - '<div class="lp-builder-popup__loader"><div class="lp-loading-circle"></div><span>%s</span></div>',
1266 - esc_html__( 'Loading...', 'learnpress' )
1267 - ),
1268 - ],
1269 - [
1270 - 'class' => BuilderPopupTemplate::class,
1271 - 'method' => 'render_lesson_popup',
1272 - ]
1273 - )
1274 - ),
1275 - 'quiz' => sprintf(
1276 - '<script type="text/template" id="%1$s"><div class="lp-builder-popup-overlay"></div><div class="lp-builder-popup lp-builder-popup--loading">%2$s</div></script>',
1277 - esc_attr(
1278 - sprintf(
1279 - 'lp-tmpl-builder-popup-curriculum-quiz-course-%d',
1280 - $courseModel->get_id()
1281 - )
1282 - ),
1283 - TemplateAJAX::load_content_via_ajax(
1284 - [
1285 - 'id_url' => 'builder-popup-quiz-curriculum',
1286 - 'quiz_id' => 0,
1287 - 'html_no_load_ajax_first' => sprintf(
1288 - '<div class="lp-builder-popup__loader"><div class="lp-loading-circle"></div><span>%s</span></div>',
1289 - esc_html__( 'Loading...', 'learnpress' )
1290 - ),
1291 - ],
1292 - [
1293 - 'class' => BuilderPopupTemplate::class,
1294 - 'method' => 'render_quiz_popup',
1295 - ]
1296 - )
1297 - ),
1298 - ],
1299 - $courseModel
1300 - );
1301 -
1302 - return Template::combine_components( $popup_templates );
1303 - }
1304 -
1305 -
1306 - /**
1307 - * Add edit popup button for lesson and quiz items in Course Builder curriculum.
1308 - * Replace the default edit button with popup button for lesson and quiz items.
1309 - *
1310 - * @param array $section_action Array of action buttons.
1311 - * @param object|null $item Item data.
1312 - * @param PostModel|null $itemModel Item model.
1313 - * @param CourseModel $courseModel .
1314 - * @param array $context_data Context data passed from AJAX.
1315 - *
1316 - * @return array
1317 - * @since 4.3.0
1318 - * @version 1.0.2
1319 - *
1320 - */
1321 - public function add_edit_popup_button( array $section_action, $item, $itemModel, $courseModel, $context_data = [] ): array {
1322 - // Check if we are in Course Builder context via the flag passed in AJAX args
1323 - $is_course_builder = ! empty( $context_data['is_course_builder'] );
1324 -
1325 - if ( ! $is_course_builder ) {
1326 - return $section_action;
1327 - }
1328 -
1329 - $item_id = $item->item_id ?? 0;
1330 - $item_type = $item->item_type ?? '';
1331 -
1332 - $popup_data_attr = '';
1333 - $popup_type = '';
1334 - if ( $item_type === LP_LESSON_CPT ) {
1335 - $popup_data_attr = sprintf( 'data-popup-lesson="%s"', $item_id );
1336 - $popup_type = 'lesson';
1337 - } elseif ( $item_type === LP_QUIZ_CPT ) {
1338 - $popup_data_attr = sprintf( 'data-popup-quiz="%s"', $item_id );
1339 - $popup_type = 'quiz';
1340 - }
1341 -
1342 - $popup_type = sanitize_key(
1343 - (string) apply_filters(
1344 - 'learn-press/course-builder/courses/curriculum/popup-item-type',
1345 - $popup_type,
1346 - $item_type,
1347 - $item,
1348 - $itemModel,
1349 - $courseModel,
1350 - $context_data
1351 - )
1352 - );
1353 - if ( empty( $popup_type ) ) {
1354 - return $section_action;
1355 - }
1356 -
1357 - $popup_template_id = sprintf(
1358 - 'lp-tmpl-builder-popup-curriculum-%1$s-course-%2$d',
1359 - $popup_type,
1360 - $courseModel->get_id()
1361 - );
1362 -
1363 - // Replace edit button with popup button - use lp-icon-edit-square instead of lp-icon-expand
1364 - $section_action['edit'] = sprintf(
1365 - '<li title="%s" class="lp-btn-edit-item-popup"
1366 - data-course-id="%s"
1367 - data-template="#%s"
1368 - data-popup-type="%s"
1369 - data-popup-id="%d"
1370 - %s>
1371 - <a class="lp-icon-edit-square edit-link edit-popup-link"></a>
1372 - </li>',
1373 - __( 'Edit in popup', 'learnpress' ),
1374 - $courseModel->get_id(),
1375 - esc_attr( $popup_template_id ),
1376 - esc_attr( $popup_type ),
1377 - $item_id,
1378 - $popup_data_attr
1379 - );
1380 -
1381 - return $section_action;
1382 - }
1383 -
1384 - /**
1385 - * Keep supported course settings tabs in Course Builder.
1386 - *
1387 - * @param array $tabs
1388 - *
1389 - * @return array
1390 - */
1391 - public function filter_course_builder_settings_tabs( array $tabs ): array {
1392 - $allowed_tabs = [ 'general', 'offline', 'price', 'extra', 'assessment', 'author', 'material' ];
1393 -
1394 - foreach ( array_keys( $tabs ) as $tab_key ) {
1395 - if ( ! in_array( $tab_key, $allowed_tabs, true ) ) {
1396 - unset( $tabs[ $tab_key ] );
1397 - }
1398 - }
1399 -
1400 - return apply_filters( 'learn-press/course-builder/edit-course/settings/tabs', $tabs );
1401 - }
1402 -
1403 - /**
1404 - * Convert final quiz edit link to Course Builder quiz settings URL.
1405 - *
1406 - * @param string $url
1407 - * @param int $final_quiz_id
1408 - *
1409 - * @return string
1410 - */
1411 - public function filter_course_builder_assessment_final_quiz_edit_link( string $url, int $final_quiz_id ): string {
1412 - $final_quiz_id = absint( $final_quiz_id );
1413 - if ( ! $final_quiz_id ) {
1414 - return $url;
1415 - }
1416 -
1417 - return CourseBuilder::get_tab_link( 'quizzes', $final_quiz_id, 'settings' ) . '#_lp_passing_grade';
1418 - }
1419 -}
1 +<?php
2 +/**
3 + * Template hooks Course Builder.
4 + *
5 + * @since 4.3.0
6 + * @version 1.0.0
7 + */
8 +
9 +namespace LearnPress\TemplateHooks\CourseBuilder\Course;
10 +
11 +use Exception;
12 +use LearnPress\CourseBuilder\CourseBuilder;
13 +use LearnPress\CourseBuilder\CourseBuilderAccessPolicy;
14 +use LearnPress\Helpers\Singleton;
15 +use LearnPress\Helpers\Template;
16 +use LearnPress\Models\CourseModel;
17 +use LearnPress\Models\CoursePostModel;
18 +use LearnPress\Models\PostModel;
19 +use LearnPress\Models\UserModel;
20 +use LearnPress\TemplateHooks\Admin\AdminTemplate;
21 +use LearnPress\TemplateHooks\Admin\AI\AdminEditCourseCurriculumWithAITemplate;
22 +use LearnPress\TemplateHooks\Admin\AI\AdminEditWithAITemplate;
23 +use LearnPress\TemplateHooks\Course\AdminEditCurriculumTemplate;
24 +use LearnPress\TemplateHooks\CourseBuilder\BuilderPopupTemplate;
25 +use LearnPress\TemplateHooks\TemplateAJAX;
26 +use LP_Settings;
27 +use LP_WP_Filesystem;
28 +use Throwable;
29 +use WP_User;
30 +
31 +class BuilderEditCourseTemplate {
32 + use Singleton;
33 +
34 + public function init() {
35 + add_filter( 'lp/rest/ajax/allow_callback', [ $this, 'allow_callback' ] );
36 +
37 + // Register filter for adding edit popup button in Course Builder curriculum
38 + add_filter( 'learn-press/admin/curriculum/section-item/actions', [ $this, 'add_edit_popup_button' ], 10, 5 );
39 + }
40 +
41 + /**
42 + * HTML edit/create course on Course Builder screen
43 + *
44 + * @param array $data
45 + *
46 + * @since 4.3.6
47 + * @version 1.0.1
48 + * @return string
49 + */
50 + public function layout( array $data = [] ): string {
51 + $html = '';
52 +
53 + try {
54 + // Check permission
55 + $userModel = $data['userModel'] ?? false;
56 + if ( ! $userModel || ! $userModel->is_instructor() ) {
57 + throw new Exception( __( 'You do not have permission to create or edit courses', 'learnpress' ) );
58 + }
59 +
60 + $userCoursePostModel = new CoursePostModel();
61 + if ( ! $userCoursePostModel->check_capabilities_create() ) {
62 + throw new Exception( __( 'You do not have permission to create or edit courses', 'learnpress' ) );
63 + }
64 +
65 + $item_id = $data['item_id'] ?? '';
66 + if ( empty( $item_id ) ) {
67 + throw new Exception( __( 'Invalid course ID', 'learnpress' ) );
68 + }
69 +
70 + /*if ( ! CourseBuilderAccessPolicy::can_access_tab_post( 'courses', $item_id ) ) {
71 + throw new Exception( __( "Sorry, you don't have permission to access this content", 'learnpress' ) );
72 + }*/
73 +
74 + $is_create_new = $item_id === CourseBuilder::POST_NEW;
75 + $courseModel = false;
76 +
77 + if ( ! $is_create_new ) {
78 + $courseModel = CourseModel::find( (int) $item_id, true );
79 + if ( ! $courseModel ) {
80 + throw new Exception( __( 'Course not found', 'learnpress' ) );
81 + }
82 +
83 + // Check permission
84 + $coursePostModel = $courseModel->get_post_model();
85 + if ( ! $coursePostModel->check_capabilities_update() ) {
86 + throw new Exception( __( 'You do not have permission to edit this course', 'learnpress' ) );
87 + }
88 +
89 + if ( $courseModel->get_status() === PostModel::STATUS_TRASH ) {
90 + throw new Exception(
91 + __(
92 + 'You cannot edit this item because it is in the Trash. Please restore it and try again.',
93 + 'learnpress'
94 + )
95 + );
96 + }
97 + }
98 +
99 + $data['courseModel'] = $courseModel;
100 +
101 + $section = [
102 + 'wrap' => sprintf(
103 + '<div class="lp-cb-content" data-post-id="%1$s">',
104 + esc_attr( $item_id ),
105 + ),
106 + 'header' => $this->html_header( $data ),
107 + 'tabs' => $this->html_tabs( $data ),
108 + 'wrap_end' => '</div>',
109 + ];
110 +
111 + $html = Template::combine_components( $section );
112 + } catch ( Throwable $e ) {
113 + $html = Template::print_message( $e->getMessage(), 'error', false );
114 + }
115 +
116 + return $html;
117 + }
118 +
119 + /**
120 + * HTML header
121 + *
122 + * @param array $data
123 + *
124 + * @return string
125 + */
126 + public function html_header( array $data = [] ): string {
127 + /** @var UserModel $userModel */
128 + $userModel = $data['userModel'] ?? false;
129 + if ( ! $userModel instanceof UserModel ) {
130 + return '';
131 + }
132 +
133 + /** @var CourseModel|false $courseModel */
134 + $courseModel = $data['courseModel'] ?? false;
135 + $hide_instructor_access_admin_screen = LP_Settings::is_hide_instructor_access_admin_screen();
136 + $more_actions_icon = LP_WP_Filesystem::get_icon_svg( 'ico-cb-more.svg' );
137 + $title = $courseModel ? $courseModel->get_title() : __( 'Add New Course', 'learnpress' );
138 + $status_badge = $courseModel ? $courseModel->get_status() : '';
139 + $status = '';
140 + if ( $courseModel ) {
141 + $status = $courseModel->get_status();
142 + }
143 + $main_action_status = in_array(
144 + $status,
145 + array(
146 + 'publish',
147 + 'draft',
148 + 'pending',
149 + 'future',
150 + 'private',
151 + ),
152 + true
153 + ) ? $status : 'publish';
154 + $wp_user = new WP_User( $userModel );
155 + $hide_wp_edit_link = $hide_instructor_access_admin_screen && user_can( $wp_user, UserModel::ROLE_INSTRUCTOR );
156 +
157 + $section = [
158 + 'header_wrap' => '<div class="lp-cb-header">',
159 + 'header_left' => '<div class="lp-cb-header__left">',
160 + 'title' => sprintf(
161 + '<h1 class="lp-cb-header__title">%s</h1>',
162 + esc_html( $title )
163 + ),
164 + 'status_badge' => $courseModel ? sprintf(
165 + '<span class="course-status %s">%s</span>',
166 + $status_badge,
167 + esc_html( $courseModel->get_post_model()->get_status_i18n() )
168 + ) : '',
169 + 'link_edit_on_wp' => $courseModel && ! $hide_wp_edit_link ? sprintf(
170 + '<a href="%1$s" class="lp-cb-admin-link" target="_blank" title="%2$s"%3$s>
171 + <span class="dashicons dashicons-wordpress"></span>
172 + <span>%2$s</span>
173 + </a>',
174 + esc_url( admin_url( "post.php?post={$courseModel->get_id()}&action=edit" ) ),
175 + esc_attr__( 'Edit with WordPress', 'learnpress' ),
176 + PostModel::STATUS_TRASH === $status ? ' style="display:none"' : ''
177 + ) : '',
178 + 'header_left_end' => '</div>',
179 + 'header_actions' => '<div class="lp-cb-header__actions">',
180 + 'preview_btn' => $courseModel && $courseModel->get_status() !== PostModel::STATUS_TRASH ? sprintf(
181 + '<a href="%1$s" class="cb-button cb-btn-preview cb-btn-secondary lp-button" target="_blank">%2$s</a>',
182 + esc_url( get_permalink( $courseModel->get_id() ) ),
183 + esc_html__( 'Preview', 'learnpress' )
184 + ) : '',
185 + 'dropdown_wrap' => '<div class="cb-header-actions-dropdown cb-header-actions-dropdown--single">',
186 + 'update_btn' => sprintf(
187 + '<div class="cb-btn-update cb-btn-primary cb-btn-main-action lp-button"
188 + data-status="%1$s"
189 + data-title-update="%2$s"
190 + data-title-publish="%3$s"
191 + data-title-draft="%4$s"
192 + data-title-submit-review="%5$s">%6$s</div>',
193 + esc_attr( $main_action_status ),
194 + esc_attr__( 'Update', 'learnpress' ),
195 + esc_attr__( 'Publish', 'learnpress' ),
196 + esc_attr__( 'Save Draft', 'learnpress' ),
197 + esc_attr__( 'Submit for Review', 'learnpress' ),
198 + esc_html__( 'Update', 'learnpress' )
199 + ),
200 + 'dropdown_wrap_end' => '</div>',
201 + 'expanded_actions' => $courseModel ? sprintf(
202 + '<div class="cb-header-action-expanded">
203 + <button type="button" class="course-action-expanded lp-button" aria-haspopup="true" aria-expanded="false" aria-label="%1$s">
204 + %2$s
205 + </button>
206 + <div class="cb-header-action-expanded__items">
207 + <div class="cb-header-action-expanded__duplicate cb-btn-duplicate-course lp-button"
208 + data-title="%3$s"
209 + data-content="%4$s">
210 + <span class="dashicons dashicons-admin-page"></span>
211 + %5$s
212 + </div>
213 + <div class="cb-header-action-expanded__trash cb-btn-trash lp-button">
214 + <span class="dashicons dashicons-trash"></span>
215 + %6$s
216 + </div>
217 + </div>
218 + </div>',
219 + esc_attr__( 'More actions', 'learnpress' ),
220 + $more_actions_icon,
221 + esc_attr__( 'Are you sure?', 'learnpress' ),
222 + esc_attr__( 'Are you sure you want to duplicate this course?', 'learnpress' ),
223 + esc_html__( 'Duplicate', 'learnpress' ),
224 + esc_html__( 'Move to Trash', 'learnpress' )
225 + ) : '',
226 + 'header_actions_end' => '</div>',
227 + 'header_end' => '</div>',
228 + ];
229 +
230 + return Template::combine_components( $section );
231 + }
232 +
233 + /**
234 + * Render tabs
235 + *
236 + * @param array $data
237 + *
238 + * @return string
239 + */
240 + public function html_tabs( array $data = [] ): string {
241 + $section_tab = [
242 + 'tabs_wrap' => '<div class="lp-cb-tabs">',
243 + 'tabs' => '',
244 + 'tabs_end' => '</div>',
245 + ];
246 +
247 + $section_content = [
248 + 'wrap' => '<div class="lp-cb-tab-content">',
249 + 'content' => '',
250 + 'wrap-end' => '</div>',
251 + ];
252 +
253 + $tabs = apply_filters(
254 + 'learn-press/course-builder/course/edit/tabs',
255 + [
256 + 'overview' => [
257 + 'title' => esc_html__( 'Overview', 'learnpress' ),
258 + 'html' => $this->html_tab_overview( $data ),
259 + ],
260 + 'curriculum' => [
261 + 'title' => esc_html__( 'Curriculum', 'learnpress' ),
262 + 'html' => $this->html_tab_curriculum( $data ),
263 + ],
264 + 'settings' => [
265 + 'title' => esc_html__( 'Settings', 'learnpress' ),
266 + 'html' => $this->html_tab_settings( $data ),
267 + ],
268 + ],
269 + $data
270 + );
271 +
272 + $tab_active = array_key_first( $tabs );
273 + if ( isset( $_GET['tab'] ) ) {
274 + $tab_active = $_GET['tab'];
275 + }
276 +
277 + foreach ( $tabs as $key => $tab ) {
278 + $is_active = $key === $tab_active;
279 +
280 + $section_tab['tabs'] .= sprintf(
281 + '<a href="#" class="lp-cb-tabs__item %s" data-tab-section="%s">%s</a>',
282 + $is_active ? 'is-active' : '',
283 + esc_attr( $tab['slug'] ?? $key ),
284 + esc_html( $tab['title'] ?? '' )
285 + );
286 +
287 + /**
288 + * @uses html_tab_overview
289 + * @uses html_tab_curriculum
290 + * @uses html_tab_settings
291 + */
292 + $section_content['content'] .= sprintf(
293 + '<div class="lp-cb-tab-panel %s" data-section="%s">%s</div>',
294 + $is_active ? '' : 'lp-hidden',
295 + esc_attr( $key ),
296 + $tab['html']
297 + );
298 + }
299 +
300 + $section = [
301 + 'tabs' => Template::combine_components( $section_tab ),
302 + 'contents' => Template::combine_components( $section_content ),
303 + ];
304 +
305 + return Template::combine_components( $section );
306 + }
307 +
308 + public function html_tab_overview( array $data = [] ) {
309 + wp_enqueue_script( 'lp-course-builder' );
310 +
311 + $courseModel = $data['courseModel'] ?? null;
312 + $course_id = 0;
313 + if ( $courseModel instanceof CourseModel ) {
314 + $course_id = $courseModel->get_id();
315 + }
316 +
317 + $html_edit_title = $this->edit_title( $courseModel );
318 + $html_edit_permalink = $this->edit_permalink( $courseModel );
319 + $html_edit_features = $this->edit_featured_image( $courseModel );
320 + $html_edit_publish = $this->edit_publish( $courseModel );
321 + $html_edit_desc = $this->edit_desc( $courseModel );
322 + $html_edit_cat = $this->edit_categories( $courseModel );
323 + $html_edit_tags = $this->edit_tags( $courseModel );
324 +
325 + $section = [
326 + 'wrapper' => sprintf( '<div class="cb-section__course-edit" data-course-id="%s">', $course_id ),
327 + 'content_wrapper' => '<div class="cb-course-edit-content">',
328 + // Left column
329 + 'left_column' => '<div class="cb-course-edit-column cb-course-edit-column--left">',
330 + 'edit_title' => $html_edit_title,
331 + 'edit_permalink' => $html_edit_permalink,
332 + 'edit_publish' => $html_edit_publish,
333 + 'edit_features' => $html_edit_features,
334 + 'left_column_end' => '</div>',
335 + // Right column
336 + 'right_column' => '<div class="cb-course-edit-column cb-course-edit-column--right">',
337 + 'edit_desc' => $html_edit_desc,
338 + 'edit_term_category' => '<div class="cb-course-edit-terms-categories-wrapper">',
339 + 'edit_cat' => $html_edit_cat,
340 + 'edit_term' => $html_edit_tags,
341 + 'edit_term_category_end' => '</div>',
342 + 'right_column_end' => '</div>',
343 + 'content_wrapper_end' => '</div>',
344 + 'ai_templates' => $this->html_overview_ai_templates(),
345 + 'wrapper_end' => '</div>',
346 + ];
347 +
348 + return Template::combine_components( $section );
349 + }
350 +
351 + /**
352 + * HTML Curriculum
353 + *
354 + * @param array $data
355 + *
356 + * @return string
357 + */
358 + public function html_tab_curriculum( array $data = [] ): string {
359 + wp_enqueue_script( 'lp-cb-edit-curriculum' );
360 + wp_enqueue_style( 'lp-cb-edit-curriculum' );
361 + wp_enqueue_script( 'lp-cb-admin-learnpress' );
362 +
363 + $course_id = CourseBuilder::get_item_id();
364 +
365 + if ( $course_id === CourseBuilder::POST_NEW ) {
366 + return Template::print_message( __( 'Please save Course before add Section', 'learnpress' ), 'info', false );
367 + }
368 +
369 + $courseModel = $data['courseModel'] ?? null;
370 + if ( ! $courseModel instanceof CourseModel ) {
371 + return Template::print_message( __( 'Course is invalid!', 'learnpress' ), 'error', false );
372 + }
373 +
374 + return $this->html_curriculum( $courseModel );
375 + }
376 +
377 + public function html_tab_settings( array $data = [] ) {
378 + wp_enqueue_script( 'lp-cb-edit-curriculum' );
379 + wp_enqueue_script( 'lp-tom-select' );
380 + wp_enqueue_style( 'lp-cb-edit-curriculum' );
381 + wp_enqueue_script( 'lp-cb-learnpress' );
382 +
383 + $course_id = CourseBuilder::get_item_id();
384 +
385 + if ( $course_id === CourseBuilder::POST_NEW ) {
386 + return Template::print_message( __( 'Please save Course before setting course', 'learnpress' ), 'info', false );
387 + }
388 +
389 + $courseModel = $data['courseModel'] ?? null;
390 + if ( ! $courseModel instanceof CourseModel ) {
391 + return Template::print_message( __( 'Course is invalid!', 'learnpress' ), 'error', false );
392 + }
393 +
394 + if ( ! class_exists( 'LP_Meta_Box_Course' ) ) {
395 + require_once LP_PLUGIN_PATH . 'inc/admin/views/meta-boxes/course/settings.php';
396 + }
397 +
398 + add_filter( 'learnpress/course/metabox/tabs', [ $this, 'filter_course_builder_settings_tabs' ], 999 );
399 + add_filter(
400 + 'learn-press/course/meta-box/assessment/final-quiz/edit-link',
401 + [
402 + $this,
403 + 'filter_course_builder_assessment_final_quiz_edit_link',
404 + ],
405 + 10,
406 + 2
407 + );
408 +
409 + $metabox = \LP_Meta_Box_Course::instance();
410 + ob_start();
411 + $metabox->output( $courseModel );
412 + $settings = ob_get_clean();
413 +
414 + remove_filter( 'learnpress/course/metabox/tabs', [ $this, 'filter_course_builder_settings_tabs' ], 999 );
415 + remove_filter(
416 + 'learn-press/course/meta-box/assessment/final-quiz/edit-link',
417 + [
418 + $this,
419 + 'filter_course_builder_assessment_final_quiz_edit_link',
420 + ],
421 + 10
422 + );
423 +
424 + $output = [
425 + 'wrapper' => sprintf( '<div class="cb-section__course-edit" data-course-id="%s">', $course_id ),
426 + 'form_setting' => '<form name="lp-form-setting-course" class="lp-form-setting-course" method="post" enctype="multipart/form-data">',
427 + 'settings' => $settings,
428 + 'form_setting_end' => '</form>',
429 + 'wrapper_end' => '</div>',
430 + ];
431 +
432 + return Template::combine_components( $output );
433 + }
434 +
435 + /**
436 + * Allow callback for AJAX.
437 + * @use self::render_edit_course_curriculum
438 + * @use self::render_html
439 + *
440 + * @param array $callbacks
441 + *
442 + * @return array
443 + */
444 + public function allow_callback( array $callbacks ): array {
445 + $callbacks[] = AdminEditCurriculumTemplate::class . ':render_edit_course_curriculum';
446 +
447 + return $callbacks;
448 + }
449 +
450 + /**
451 + * Render Course Builder course title field.
452 + *
453 + * Uses the raw post title for existing courses so the edit field does not receive
454 + * display filters. New courses start with an empty value and still render the
455 + * optional AI button when AI settings are enabled.
456 + *
457 + * @param CourseModel|false|null $courseModel Course model being edited, or empty when creating a new course.
458 + *
459 + * @return string HTML markup for the title input, character count, and optional AI button.
460 + * @since 4.3.0
461 + * @version 1.0.0
462 + */
463 + public function edit_title( $courseModel ) {
464 + $title = '';
465 + if ( ! empty( $courseModel ) ) {
466 + $post_id = absint( $courseModel->get_id() );
467 + $title = $post_id ? (string) get_post_field( 'post_title', $post_id, 'raw' ) : '';
468 + }
469 + $char_count = mb_strlen( wp_strip_all_tags( $title ) );
470 + $ai_button = $this->html_overview_ai_button( '#lp-tmpl-edit-title-ai' );
471 + $edit = [
472 + 'wrapper' => '<div class="cb-course-edit-title">',
473 + 'label_wrap' => '<div class="cb-course-edit-title__label-wrap">',
474 + 'label' => sprintf( '<label for="title" class="cb-course-edit-title__label">%s <span class="required">*</span></label>', __( 'Course Title', 'learnpress' ) ),
475 + 'char_count' => sprintf( '<span class="cb-course-edit-title__char-count">%s</span>', sprintf( __( '%d characters', 'learnpress' ), $char_count ) ),
476 + 'ai_button' => $ai_button,
477 + 'label_wrap_end' => '</div>',
478 + 'input' => sprintf( '<input type="text" name="course_title" size="30" value="%s" id="title" class="cb-course-edit-title__input" placeholder="%s">', esc_attr( $title ), esc_attr__( 'example', 'learnpress' ) ),
479 + 'wrapper_end' => '</div>',
480 + ];
481 +
482 + return Template::combine_components( $edit );
483 + }
484 +
485 + /**
486 + * Render Course Builder permalink editor.
487 + *
488 + * Permalink editing is hidden for unsaved courses. Existing courses use
489 + * get_sample_permalink() when available to display a publish-style URL for slug
490 + * editing, while the preview link keeps the current permalink as its href.
491 + *
492 + * @param CourseModel|false|null $courseModel Course model being edited, or empty when creating a new course.
493 + *
494 + * @return string HTML markup for the permalink display/editor, or an empty string for new courses.
495 + * @since 4.3.0
496 + * @version 1.0.0
497 + */
498 + public function edit_permalink( $courseModel ) {
499 + $post_id = ! empty( $courseModel ) ? $courseModel->get_id() : '';
500 + $post_name = '';
501 +
502 + // Hide permalink for new courses
503 + if ( empty( $post_id ) || $post_id === CourseBuilder::POST_NEW ) {
504 + return '';
505 + }
506 +
507 + if ( $post_id ) {
508 + $post = get_post( $post_id );
509 + $post_name = $post ? $post->post_name : '';
510 + }
511 +
512 + // Get base URL for courses
513 + $courses_page_id = learn_press_get_page_id( 'courses' );
514 + $base_url = '';
515 + if ( $courses_page_id ) {
516 + $base_url = trailingslashit( get_permalink( $courses_page_id ) );
517 + } else {
518 + $base_url = trailingslashit( home_url() ) . 'courses/';
519 + }
520 +
521 + $full_url = urldecode( (string) get_permalink( $post_id ) );
522 + $display_data = $this->get_course_display_permalink_data( (int) $post_id, (string) $post_name );
523 + $display_url = (string) ( $display_data['url'] ?? '' );
524 + $editor_slug = (string) ( $display_data['slug'] ?? '' );
525 +
526 + if ( empty( $display_url ) ) {
527 + $display_url = $full_url;
528 + }
529 +
530 + if ( empty( $full_url ) ) {
531 + $full_url = $display_url;
532 + }
533 +
534 + if ( empty( $editor_slug ) ) {
535 + $editor_slug = (string) $post_name;
536 + }
537 +
538 + if ( empty( $editor_slug ) && ! empty( $display_url ) ) {
539 + $display_path = parse_url( $display_url, PHP_URL_PATH );
540 + if ( is_string( $display_path ) && '' !== $display_path ) {
541 + $editor_slug = basename( untrailingslashit( $display_path ) );
542 + }
543 + }
544 +
545 + // Use publish-style permalink as editable base, but keep href as current permalink.
546 + if (
547 + ! empty( $editor_slug ) &&
548 + false === strpos( $display_url, '?p=' ) &&
549 + false === strpos( $display_url, '&p=' ) &&
550 + false === strpos( $display_url, '?lp_course=' ) &&
551 + false === strpos( $display_url, '&lp_course=' )
552 + ) {
553 + $base_url = trailingslashit( preg_replace( '/' . preg_quote( $editor_slug, '/' ) . '\/?$/', '', $display_url ) );
554 + }
555 +
556 + $state_a = sprintf(
557 + '<span class="cb-permalink-label">%s</span>
558 + <div class="cb-permalink-display">
559 + <a href="%s" target="_blank" class="cb-permalink-url">%s</a>
560 + <button type="button" class="cb-permalink-edit-btn" title="%s">
561 + <span class="dashicons dashicons-edit"></span>
562 + </button>
563 + </div>',
564 + __( 'Permalink', 'learnpress' ),
565 + esc_url( $full_url ),
566 + esc_html( $display_url ),
567 + __( 'Edit', 'learnpress' )
568 + );
569 +
570 + $state_b = sprintf(
571 + '<div class="cb-permalink-editor lp-hidden">
572 + <span class="cb-permalink-prefix">%s</span>
573 + <div class="cb-permalink-input-row">
574 + <input type="text" name="course_permalink" id="course_permalink" value="%s" class="cb-permalink-slug-input" placeholder="%s">
575 + <div class="cb-permalink-actions">
576 + <button type="button" class="cb-permalink-ok-btn">%s</button>
577 + <button type="button" class="cb-permalink-cancel-btn">%s</button>
578 + </div>
579 + </div>
580 + </div>',
581 + esc_html( $base_url ),
582 + esc_attr( $editor_slug ),
583 + esc_attr__( 'your-slug', 'learnpress' ),
584 + __( 'OK', 'learnpress' ),
585 + __( 'Cancel', 'learnpress' )
586 + );
587 +
588 + $hidden_base = sprintf(
589 + '<input type="hidden" id="cb-permalink-base-url" value="%s">',
590 + esc_attr( $base_url )
591 + );
592 +
593 + $edit = [
594 + 'wrapper' => '<div class="cb-course-edit-permalink">',
595 + 'state_a' => $state_a,
596 + 'state_b' => $state_b,
597 + 'hidden_base' => $hidden_base,
598 + 'wrapper_end' => '</div>',
599 + ];
600 +
601 + return Template::combine_components( $edit );
602 + }
603 +
604 + /**
605 + * Get permalink display URL in publish-style format for editing slug.
606 + *
607 + * @param int $post_id
608 + * @param string $post_name
609 + *
610 + * @return array<string, string>
611 + */
612 + private function get_course_display_permalink_data( int $post_id, string $post_name = '' ): array {
613 + $display_url = urldecode( (string) get_permalink( $post_id ) );
614 + $sample_slug = $post_name;
615 +
616 + if ( ! function_exists( 'get_sample_permalink' ) ) {
617 + require_once ABSPATH . 'wp-admin/includes/post.php';
618 + }
619 +
620 + if ( function_exists( 'get_sample_permalink' ) ) {
621 + $sample_permalink = get_sample_permalink( $post_id );
622 + if ( is_array( $sample_permalink ) && ! empty( $sample_permalink[0] ) ) {
623 + $sample_slug = ! empty( $sample_permalink[1] ) ? (string) $sample_permalink[1] : $sample_slug;
624 + $display_url = str_replace(
625 + [ '%postname%', '%pagename%' ],
626 + $sample_slug,
627 + (string) $sample_permalink[0]
628 + );
629 + }
630 + }
631 +
632 + $post = get_post( $post_id );
633 + if ( $post instanceof \WP_Post ) {
634 + $display_url = \LP_Helper::handle_lp_permalink_structure( $display_url, $post );
635 + }
636 +
637 + return [
638 + 'url' => urldecode( $display_url ),
639 + 'slug' => urldecode( (string) $sample_slug ),
640 + ];
641 + }
642 +
643 + /**
644 + * Render Course Builder description editor.
645 + *
646 + * Uses the current course post content when editing an existing CourseModel.
647 + * For new courses, the editor starts with an empty value. AI button rendering
648 + * is delegated to html_overview_ai_button().
649 + *
650 + * @param CourseModel|false|null $courseModel Course model being edited, or empty when creating a new course.
651 + *
652 + * @return string HTML markup for the course description editor.
653 + */
654 + public function edit_desc( $courseModel ) {
655 + $desc = $courseModel ? $courseModel->post_content : '';
656 +
657 + $editor_id = 'course_description_editor';
658 + $ai_button = $this->html_overview_ai_button( '#lp-tmpl-edit-description-ai' );
659 + $editor_settings = array(
660 + 'textarea_name' => 'course_description',
661 + 'textarea_rows' => 10,
662 + 'teeny' => false,
663 + 'media_buttons' => true,
664 + 'tinymce' => array(
665 + 'content_style' => "body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; font-size: 14px; line-height: 1.6; color: #1e1e1e; }",
666 + 'toolbar1' => 'formatselect,bold,italic,underline,bullist,numlist,blockquote,alignleft,aligncenter,alignright,link,unlink,spellchecker,wp_adv',
667 + 'toolbar2' => 'strikethrough,hr,forecolor,pastetext,removeformat,charmap,outdent,indent,undo,redo,wp_help',
668 + ),
669 + 'quicktags' => true,
670 + );
671 +
672 + $edit = [
673 + 'wrapper' => '<div class="cb-course-edit-desc">',
674 + 'label_wrap' => '<div class="cb-course-edit-desc__label-wrap">',
675 + 'label' => sprintf(
676 + '<label for="course_description" class="cb-course-edit-desc__label">%s</label>',
677 + __( 'Description', 'learnpress' )
678 + ),
679 + 'ai_button' => $ai_button,
680 + 'label_wrap_end' => '</div>',
681 + 'edit' => AdminTemplate::editor_tinymce(
682 + $desc,
683 + $editor_id,
684 + $editor_settings
685 + ),
686 + 'wrapper_end' => '</div>',
687 + ];
688 +
689 + return Template::combine_components( $edit );
690 + }
691 +
692 + /**
693 + * Check if Overview AI button and templates should be shown.
694 + *
695 + * @return bool
696 + */
697 + protected function can_show_overview_ai_button(): bool {
698 + return \LP_Settings::get_option( 'enable_open_ai', 'no' ) === 'yes'
699 + && ! empty( \LP_Settings::get_option( 'open_ai_secret_key', '' ) );
700 + }
701 +
702 + /**
703 + * Render icon-only AI button in Course Builder overview.
704 + *
705 + * @param string $template_id
706 + *
707 + * @return string
708 + */
709 + protected function html_overview_ai_button( string $template_id ): string {
710 + if ( ! $this->can_show_overview_ai_button() ) {
711 + return '';
712 + }
713 +
714 + $button_label = esc_html__( 'Generate with AI', 'learnpress' );
715 +
716 + return sprintf(
717 + '<button type="button" class="cb-course-edit-ai-btn lp-btn-generate-with-ai" data-template="%1$s" title="%2$s" aria-label="%2$s"><i class="lp-ico-ai" aria-hidden="true"></i></button>',
718 + esc_attr( $template_id ),
719 + esc_attr( $button_label )
720 + );
721 + }
722 +
723 + /**
724 + * Render AI popup templates for Course Builder overview edit page.
725 + *
726 + * @return string
727 + */
728 + protected function html_overview_ai_templates(): string {
729 + if ( ! $this->can_show_overview_ai_button() ) {
730 + return '';
731 + }
732 +
733 + try {
734 + return AdminEditWithAITemplate::instance()->render_for_frontend( [ 'title', 'description', 'image' ] );
735 + } catch ( Throwable $e ) {
736 + error_log( __METHOD__ . ': ' . $e->getMessage() );
737 + }
738 +
739 + return '';
740 + }
741 +
742 + /**
743 + * Render AI popup template for curriculum edit in Course Builder.
744 + *
745 + * @return string
746 + */
747 + protected function html_curriculum_ai_templates(): string {
748 + if ( ! $this->can_show_overview_ai_button() ) {
749 + return '';
750 + }
751 +
752 + try {
753 + return AdminEditCourseCurriculumWithAITemplate::instance()->render_for_frontend();
754 + } catch ( Throwable $e ) {
755 + error_log( __METHOD__ . ': ' . $e->getMessage() );
756 + }
757 +
758 + return '';
759 + }
760 +
761 + /**
762 + * Render Course Builder course category selector.
763 + *
764 + * Reuses the WordPress post_categories_meta_box() output for course_category,
765 + * then adds Course Builder search and "add new category" controls. During render,
766 + * checked_ontop is forced off so the checklist keeps taxonomy order.
767 + *
768 + * @param CourseModel|false|null $courseModel Course model being edited, or empty when creating a new course.
769 + *
770 + * @return string HTML markup for the course category checklist, search toolbar, and add-new form.
771 + * @since 4.3.0
772 + * @version 1.0.0
773 + */
774 + public function edit_categories( $courseModel ) {
775 + if ( ! function_exists( 'post_categories_meta_box' ) ) {
776 + require_once ABSPATH . 'wp-admin/includes/meta-boxes.php';
777 + }
778 + if ( ! function_exists( 'wp_popular_terms_checklist' ) ) {
779 + require_once ABSPATH . 'wp-admin/includes/template.php';
780 + }
781 +
782 + $post_id = ! empty( $courseModel ) ? $courseModel->get_id() : get_the_ID();
783 + $post = get_post( $post_id );
784 +
785 + $force_checked_ontop_false = function ( $args ) {
786 + if ( isset( $args['taxonomy'] ) && 'course_category' === $args['taxonomy'] ) {
787 + $args['checked_ontop'] = false;
788 + }
789 +
790 + return $args;
791 + };
792 +
793 + ob_start();
794 +
795 + add_filter( 'wp_terms_checklist_args', $force_checked_ontop_false );
796 +
797 + if ( function_exists( 'post_categories_meta_box' ) ) {
798 + \post_categories_meta_box(
799 + $post,
800 + array(
801 + 'id' => 'course_categorydiv',
802 + 'title' => __( 'Categories', 'learnpress' ),
803 + 'callback' => 'post_categories_meta_box',
804 + 'args' => array(
805 + 'taxonomy' => 'course_category',
806 + 'checked_ontop' => false,
807 + ),
808 + )
809 + );
810 + }
811 +
812 + remove_filter( 'wp_terms_checklist_args', $force_checked_ontop_false );
813 + $html_meta_box = ob_get_clean();
814 +
815 + // Build add new category form (between header and content)
816 + $parent_terms = get_terms(
817 + [
818 + 'taxonomy' => 'course_category',
819 + 'hide_empty' => false,
820 + ]
821 + );
822 + $parent_options = sprintf( '<option value="0">— %s —</option>', __( 'Parent Category', 'learnpress' ) );
823 + if ( ! empty( $parent_terms ) && ! is_wp_error( $parent_terms ) ) {
824 + foreach ( $parent_terms as $term ) {
825 + $parent_options .= sprintf(
826 + '<option value="%d">%s</option>',
827 + $term->term_id,
828 + esc_html( $term->name )
829 + );
830 + }
831 + }
832 +
833 + $form_add_category = sprintf(
834 + '<div class="cb-course-edit-terms__form-add-category" style="display:none;">
835 + <input type="text" class="cb-course-edit-category__input" placeholder="%s" id="cb-newcourse_category" />
836 + <select class="cb-course-edit-category__select-parent" id="cb-newcourse_category_parent">%s</select>
837 + <button type="button" class="cb-course-edit-category__btn-cancel">%s</button>
838 + <button type="button" class="cb-course-edit-category__btn-save" id="cb-course_category-add-submit">%s</button>
839 + </div>',
840 + esc_attr__( 'Enter Category Name', 'learnpress' ),
841 + $parent_options,
842 + esc_html__( 'Cancel', 'learnpress' ),
843 + esc_html__( 'Add', 'learnpress' ),
844 + );
845 +
846 + $edit = [
847 + 'wrapper' => '<div class="cb-course-edit-categories__wrapper">',
848 + 'header' => '<div class="cb-terms-header">',
849 + 'label_wrap' => '<div class="cb-terms-header__label-wrap">',
850 + 'label' => sprintf( '<label class="cb-terms-header__label">%s</label>', __( 'Categories', 'learnpress' ) ),
851 + 'btn_search' => sprintf(
852 + '<button type="button" class="cb-terms-header__btn-search" data-toggle-target="#cb-course-edit-categories-search-toolbar" aria-expanded="false" aria-label="%s">
853 + <i class="lp-icon-search"></i>
854 + </button>',
855 + esc_attr__( 'Search categories', 'learnpress' )
856 + ),
857 + 'label_wrap_end' => '</div>',
858 + 'btn_add_new' => sprintf( '<button class="cb-course-edit-category__btn-add-new cb-terms-header__btn-add-new">%s</button>', __( 'Add New', 'learnpress' ) ),
859 + 'header_end' => '</div>',
860 + 'form_add_category' => $form_add_category,
861 + 'search' => sprintf(
862 + '<div class="cb-course-edit-categories__toolbar cb-terms-search-toolbar" id="cb-course-edit-categories-search-toolbar">
863 + <label class="cb-course-edit-categories__search-wrap">
864 + <span class="screen-reader-text">%1$s</span>
865 + <input type="search" class="cb-course-edit-category__search-input" placeholder="%2$s" />
866 + </label>
867 + </div>',
868 + esc_html__( 'Search categories', 'learnpress' ),
869 + esc_attr__( 'Search categories', 'learnpress' )
870 + ),
871 + 'content' => $html_meta_box,
872 + 'wrapper_end' => '</div>',
873 + ];
874 +
875 + return Template::combine_components( $edit );
876 + }
877 +
878 + /**
879 + * Render Course Builder course tag selector.
880 + *
881 + * Builds tag chips from all course tags, placing selected tags before available
882 + * tags so the current course state is visible first. The returned markup includes
883 + * search, empty-state text, and add-new controls consumed by the builder runtime.
884 + *
885 + * @param CourseModel|false|null $courseModel Course model being edited, or empty when creating a new course.
886 + *
887 + * @return string HTML markup for the selected/available tag chips, search toolbar, and add-new form.
888 + * @since 4.3.0
889 + * @version 1.0.0
890 + */
891 + public function edit_tags( $courseModel ) {
892 + $course_terms = ! empty( $courseModel ) ? $courseModel->get_tags() : [];
893 + $tags = get_terms(
894 + [
895 + 'taxonomy' => LP_COURSE_TAXONOMY_TAG,
896 + 'hide_empty' => false,
897 + ]
898 + );
899 +
900 + $selected_tag_ids = array_map(
901 + function ( $term ) {
902 + return (int) $term->term_id;
903 + },
904 + $course_terms
905 + );
906 +
907 + $html_selected_chips = '';
908 + $html_available_chips = '';
909 +
910 + if ( ! empty( $tags ) && ! is_wp_error( $tags ) ) {
911 + foreach ( $tags as $tag ) {
912 + $tag_id = $tag->term_id;
913 + $tag_name = $tag->name;
914 + $tag_count = $tag->count;
915 + $is_checked = in_array( (int) $tag_id, $selected_tag_ids, true );
916 + $html_chip = $this->input_checkbox_tag_item( $tag_id, $tag_name, $is_checked, $tag_count );
917 +
918 + if ( $is_checked ) {
919 + $html_selected_chips .= $html_chip;
920 + } else {
921 + $html_available_chips .= $html_chip;
922 + }
923 + }
924 + }
925 +
926 + $html_chips = $html_selected_chips . $html_available_chips;
927 + $count_all = substr_count( $html_chips, 'class="cb-tag-chip"' );
928 + $empty_default = esc_html__( 'No tags found.', 'learnpress' );
929 + $empty_search = esc_html__( 'No matching tags.', 'learnpress' );
930 +
931 + $toolbar = sprintf(
932 + '<div class="cb-course-edit-tags__toolbar cb-terms-search-toolbar" id="cb-course-edit-tags-search-toolbar">
933 + <label class="cb-course-edit-tags__search-wrap">
934 + <span class="screen-reader-text">%1$s</span>
935 + <input type="search" class="cb-course-edit-tags__search-input" placeholder="%2$s" />
936 + </label>
937 + </div>',
938 + esc_html__( 'Search tags', 'learnpress' ),
939 + esc_attr__( 'Search tags', 'learnpress' )
940 + );
941 +
942 + $edit = [
943 + 'wrapper' => '<div class="cb-course-edit-tags__wrapper">',
944 + 'header' => '<div class="cb-terms-header">',
945 + 'label_wrap' => '<div class="cb-terms-header__label-wrap">',
946 + 'label' => sprintf( '<label class="cb-terms-header__label">%s</label>', __( 'Tags', 'learnpress' ) ),
947 + 'btn_search' => sprintf(
948 + '<button type="button" class="cb-terms-header__btn-search" data-toggle-target="#cb-course-edit-tags-search-toolbar" aria-expanded="false" aria-label="%s">
949 + <i class="lp-icon-search"></i>
950 + </button>',
951 + esc_attr__( 'Search tags', 'learnpress' )
952 + ),
953 + 'label_wrap_end' => '</div>',
954 + 'btn_add_new' => sprintf( '<button class="cb-course-edit-tag__btn-add-new cb-terms-header__btn-add-new">%s</button>', __( 'Add New', 'learnpress' ) ),
955 + 'header_end' => '</div>',
956 + 'form_add_tag_wrapper' => '<div class="cb-course-edit-terms__form-add-tag" style="display:none;">',
957 + 'input' => '<input type="text" class="cb-course-edit-tags__input" placeholder="' . esc_attr__( 'Enter Tag Name', 'learnpress' ) . '"/>',
958 + 'btn_cancel' => sprintf( '<button type="button" class="cb-course-edit-tag__btn-cancel">%s</button>', __( 'Cancel', 'learnpress' ) ),
959 + 'button' => '<button type="button" class="cb-course-edit-tags__btn-save">' . esc_html__( 'Add', 'learnpress' ) . '</button>',
960 + 'form_add_tag_wrapper_end' => '</div>',
961 + 'toolbar' => $toolbar,
962 + 'wrapper_checkbox' => '<div class="cb-course-edit-tags__checkbox-wrapper">',
963 + 'checkbox' => $html_chips,
964 + 'wrapper_checkbox_end' => '</div>',
965 + 'empty' => sprintf(
966 + '<p class="cb-course-edit-tags__empty%1$s" data-empty-default="%2$s" data-empty-search="%3$s">%2$s</p>',
967 + $count_all > 0 ? ' lp-hidden' : '',
968 + esc_attr( $empty_default ),
969 + esc_attr( $empty_search )
970 + ),
971 + 'wrapper_end' => '</div>',
972 + ];
973 +
974 + return Template::combine_components( $edit );
975 + }
976 +
977 + public function input_checkbox_tag_item( $term_id, $term_name, $is_checked, $count = 0 ) {
978 + if ( 0 === $count ) {
979 + $tag_obj = get_term( $term_id, LP_COURSE_TAXONOMY_TAG );
980 + if ( $tag_obj && ! is_wp_error( $tag_obj ) ) {
981 + $count = $tag_obj->count;
982 + }
983 + }
984 +
985 + $tag_name_search = wp_strip_all_tags( $term_name );
986 + if ( function_exists( 'mb_strtolower' ) ) {
987 + $tag_name_search = mb_strtolower( $tag_name_search );
988 + } else {
989 + $tag_name_search = strtolower( $tag_name_search );
990 + }
991 +
992 + $html = sprintf(
993 + '<div class="cb-tag-chip" data-tag-name="%s" data-term-id="%d">',
994 + esc_attr( $tag_name_search ),
995 + (int) $term_id
996 + );
997 + $html .= sprintf(
998 + '<input type="checkbox" name="course_tags[]" value="%s" id="course_tag_%s" %s>',
999 + $term_id,
1000 + $term_id,
1001 + checked( $is_checked, true, false )
1002 + );
1003 + $html .= sprintf(
1004 + '<label for="course_tag_%s"><span class="cb-tag-chip__name">%s</span><span class="cb-tag-chip__count">(%d)</span><span class="cb-tag-chip__remove">&times;</span></label>',
1005 + $term_id,
1006 + esc_html( $term_name ),
1007 + $count
1008 + );
1009 + $html .= '</div>';
1010 +
1011 + return $html;
1012 + }
1013 +
1014 + /**
1015 + * Render Course Builder featured image uploader.
1016 + *
1017 + * Existing courses render the current thumbnail preview with replace/remove
1018 + * controls. Courses without a thumbnail render the upload dropzone and hidden
1019 + * course_thumbnail_id field used by the builder save flow.
1020 + *
1021 + * @param CourseModel|false|null $courseModel Course model being edited, or empty when creating a new course.
1022 + *
1023 + * @return string HTML markup for the featured image dropzone, thumbnail hidden field, and optional AI button.
1024 + * @since 4.3.0
1025 + * @version 1.0.0
1026 + */
1027 + public function edit_featured_image( $courseModel ) {
1028 + $post_id = ! empty( $courseModel ) ? $courseModel->get_id() : '';
1029 + $ai_button = $this->html_overview_ai_button( '#lp-tmpl-edit-image-ai' );
1030 +
1031 + $thumbnail_id = ! empty( $post_id ) ? get_post_thumbnail_id( $post_id ) : '';
1032 + $thumbnail_url = '';
1033 + $thumbnail_alt = '';
1034 +
1035 + if ( $thumbnail_id ) {
1036 + $thumbnail_url = wp_get_attachment_image_url( $thumbnail_id, 'medium' );
1037 + $thumbnail_alt = get_post_meta( $thumbnail_id, '_wp_attachment_image_alt', true );
1038 + }
1039 +
1040 + $has_image = ! empty( $thumbnail_url );
1041 +
1042 + $featured_image_html = '<div class="cb-featured-image-container">';
1043 +
1044 + // Upload area
1045 + $featured_image_html .= sprintf(
1046 + '<div class="cb-featured-image-dropzone %s" data-post-id="%s">',
1047 + $has_image ? 'has-image' : '',
1048 + esc_attr( $post_id )
1049 + );
1050 +
1051 + if ( $has_image ) {
1052 + $featured_image_html .= sprintf(
1053 + '<img src="%s" alt="%s" class="cb-featured-image-preview__img">',
1054 + esc_url( $thumbnail_url ),
1055 + esc_attr( $thumbnail_alt )
1056 + );
1057 + } else {
1058 + $featured_image_html .= '<div class="cb-featured-image-upload-content">';
1059 + $featured_image_html .= '<span class="cb-featured-image-icon"><span class="cb-featured-image-icon__image" aria-hidden="true"></span></span>';
1060 + $featured_image_html .= sprintf(
1061 + '<p class="cb-featured-image-text"><a href="#" class="cb-featured-image-link">%s</a></p>',
1062 + __( 'Click to upload', 'learnpress' )
1063 + );
1064 + $featured_image_html .= sprintf(
1065 + '<p class="cb-featured-image-hint">%s</p>',
1066 + __( 'JPG, JPEG, PNG less than 1MB', 'learnpress' )
1067 + );
1068 + $featured_image_html .= '</div>';
1069 + }
1070 +
1071 + $featured_image_html .= '</div>';
1072 +
1073 + $featured_image_html .= sprintf(
1074 + '<input type="hidden" name="course_thumbnail_id" id="course_thumbnail_id" value="%s">',
1075 + esc_attr( $thumbnail_id )
1076 + );
1077 +
1078 + // Action buttons wrapper
1079 + $featured_image_html .= '<div class="cb-featured-image-actions">';
1080 +
1081 + // Remove button (only show when has image)
1082 + if ( $has_image ) {
1083 + $featured_image_html .= sprintf(
1084 + '<button type="button" class="cb-remove-featured-image">%s</button>',
1085 + '<span class="cb-remove-featured-image__icon" aria-hidden="true"></span>'
1086 + );
1087 +
1088 + $featured_image_html .= sprintf(
1089 + '<button type="button" class="cb-change-featured-image">%s</button>',
1090 + __( 'Replace', 'learnpress' )
1091 + );
1092 + }
1093 +
1094 + $featured_image_html .= '</div>'; // End actions wrapper
1095 + $featured_image_html .= '</div>'; // End container
1096 +
1097 + $edit = [
1098 + 'wrapper' => '<div class="cb-course-edit-featured-image">',
1099 + 'label_wrap' => '<div class="cb-course-edit-featured-image__label-wrap">',
1100 + 'label' => sprintf(
1101 + '<label class="cb-course-edit-featured-image__title">%s</label>',
1102 + __( 'Featured Image', 'learnpress' )
1103 + ),
1104 + 'ai_button' => $ai_button,
1105 + 'label_wrap_end' => '</div>',
1106 + 'edit' => $featured_image_html,
1107 + 'wrapper_end' => '</div>',
1108 + ];
1109 +
1110 + return Template::combine_components( $edit );
1111 + }
1112 +
1113 + /**
1114 + * Render publish panel (status, visibility, publish date, danger zone) for Course Builder overview.
1115 + *
1116 + * @param CourseModel|false|string|null $courseModel
1117 + *
1118 + * @return string
1119 + */
1120 + public function edit_publish( $courseModel ): string {
1121 + $post_id = ! empty( $courseModel ) ? absint( $courseModel->get_id() ) : 0;
1122 + $post = $post_id ? get_post( $post_id ) : null;
1123 +
1124 + $current_status = 'draft';
1125 + if ( $post && ! empty( $post->post_status ) ) {
1126 + $current_status = sanitize_key( $post->post_status );
1127 + }
1128 +
1129 + $status_for_select = in_array( $current_status, [ 'publish', 'draft', 'pending', 'future' ], true )
1130 + ? $current_status
1131 + : 'publish';
1132 + $current_password = $post ? (string) ( $post->post_password ?? '' ) : '';
1133 + $visibility = 'private' === $current_status
1134 + ? 'private'
1135 + : ( ! empty( $current_password ) ? 'password' : 'public' );
1136 + $published_on = '';
1137 +
1138 + if ( $post && ! empty( $post->post_date ) && '0000-00-00 00:00:00' !== $post->post_date ) {
1139 + $published_on = wp_date( 'Y-m-d\TH:i', strtotime( $post->post_date ), wp_timezone() );
1140 + }
1141 +
1142 + $has_future_publish_date = false;
1143 + if ( $post && ! empty( $post->post_date ) && '0000-00-00 00:00:00' !== $post->post_date ) {
1144 + $has_future_publish_date = strtotime( $post->post_date ) > current_time( 'timestamp' );
1145 + }
1146 +
1147 + $is_scheduled_status = 'future' === $status_for_select
1148 + || ( in_array( $status_for_select, [ 'draft', 'pending' ], true ) && $has_future_publish_date );
1149 + $primary_status_value = $is_scheduled_status ? 'future' : 'publish';
1150 + $primary_status_label = $is_scheduled_status
1151 + ? __( 'Scheduled', 'learnpress' )
1152 + : __( 'Published', 'learnpress' );
1153 + $selected_status_for_ui = in_array( $status_for_select, [ 'draft', 'pending' ], true )
1154 + ? $status_for_select
1155 + : $primary_status_value;
1156 + $status_options = [
1157 + $primary_status_value => $primary_status_label,
1158 + 'draft' => __( 'Draft', 'learnpress' ),
1159 + 'pending' => __( 'Pending Review', 'learnpress' ),
1160 + ];
1161 +
1162 + $publish_date_label = $has_future_publish_date
1163 + ? __( 'Scheduled for', 'learnpress' )
1164 + : __( 'Published on', 'learnpress' );
1165 +
1166 + $status_options_html = '';
1167 + foreach ( $status_options as $value => $label ) {
1168 + $status_options_html .= sprintf(
1169 + '<option value="%1$s" %2$s>%3$s</option>',
1170 + esc_attr( $value ),
1171 + selected( $selected_status_for_ui, $value, false ),
1172 + esc_html( $label )
1173 + );
1174 + }
1175 +
1176 + $visibility_options_html = sprintf(
1177 + '<option value="public" %1$s>%2$s</option><option value="private" %3$s>%4$s</option><option value="password" %5$s>%6$s</option>',
1178 + selected( $visibility, 'public', false ),
1179 + esc_html__( 'Public', 'learnpress' ),
1180 + selected( $visibility, 'private', false ),
1181 + esc_html__( 'Private', 'learnpress' ),
1182 + selected( $visibility, 'password', false ),
1183 + esc_html__( 'Password protected', 'learnpress' )
1184 + );
1185 +
1186 + $edit = [
1187 + 'wrapper' => '<div class="cb-course-edit-publish">',
1188 + 'title' => sprintf( '<h3 class="cb-course-edit-publish__title">%s</h3>', esc_html__( 'Publish', 'learnpress' ) ),
1189 + 'status_row' => sprintf(
1190 + '<div class="cb-course-edit-publish__row">
1191 + <label for="cb-course-publish-status" class="cb-course-edit-publish__label">%1$s</label>
1192 + <select id="cb-course-publish-status" name="cb_course_publish_status" class="cb-course-edit-publish__control" data-publish-label="%3$s" data-future-label="%4$s" data-primary-status="%5$s">%2$s</select>
1193 + </div>',
1194 + esc_html__( 'Status', 'learnpress' ),
1195 + $status_options_html,
1196 + esc_attr__( 'Published', 'learnpress' ),
1197 + esc_attr__( 'Scheduled', 'learnpress' ),
1198 + esc_attr( $primary_status_value )
1199 + ),
1200 + 'visibility_row' => sprintf(
1201 + '<div class="cb-course-edit-publish__row">
1202 + <label for="cb-course-publish-visibility" class="cb-course-edit-publish__label">%1$s</label>
1203 + <select id="cb-course-publish-visibility" name="cb_course_publish_visibility" class="cb-course-edit-publish__control">%2$s</select>
1204 + </div>',
1205 + esc_html__( 'Visibility', 'learnpress' ),
1206 + $visibility_options_html
1207 + ),
1208 + 'password_row' => sprintf(
1209 + '<div class="cb-course-edit-publish__row cb-course-edit-publish__password-row %1$s">
1210 + <label for="cb-course-publish-password" class="cb-course-edit-publish__label">%2$s</label>
1211 + <input type="text" id="cb-course-publish-password" name="cb_course_publish_password" class="cb-course-edit-publish__control" value="%3$s" autocomplete="new-password">
1212 + </div>',
1213 + 'password' === $visibility ? '' : 'lp-hidden',
1214 + esc_html__( 'Password', 'learnpress' ),
1215 + esc_attr( $current_password )
1216 + ),
1217 + 'published_on_row' => sprintf(
1218 + '<div class="cb-course-edit-publish__row">
1219 + <label for="cb-course-publish-date" id="cb-course-publish-date-label" class="cb-course-edit-publish__label">%1$s</label>
1220 + <input type="datetime-local" id="cb-course-publish-date" name="cb_course_publish_date" class="cb-course-edit-publish__control" value="%2$s">
1221 + </div>',
1222 + esc_html( $publish_date_label ),
1223 + esc_attr( $published_on )
1224 + ),
1225 + 'wrapper_end' => '</div>',
1226 + ];
1227 +
1228 + return Template::combine_components( $edit );
1229 + }
1230 +
1231 + /**
1232 + * Render Course Builder curriculum editor.
1233 + *
1234 + * Delegates the main curriculum layout to AdminEditCurriculumTemplate with the
1235 + * is_course_builder context flag, then appends popup templates and optional AI
1236 + * curriculum templates needed by the builder UI.
1237 + *
1238 + * @param CourseModel $courseModel Course model used to render curriculum sections and popup template IDs.
1239 + *
1240 + * @return string HTML markup for the curriculum editor plus Course Builder popup and AI templates.
1241 + * @since 4.3.0
1242 + * @version 1.0.0
1243 + */
1244 + protected function html_curriculum( CourseModel $courseModel ): string {
1245 + ob_start();
1246 + AdminEditCurriculumTemplate::instance()->edit_course_curriculum_layout(
1247 + $courseModel,
1248 + [ 'is_course_builder' => true ]
1249 + );
1250 + $html_curriculum = ob_get_clean();
1251 +
1252 + return $html_curriculum
1253 + . $this->html_curriculum_popup_templates( $courseModel )
1254 + . $this->html_curriculum_ai_templates();
1255 + }
1256 +
1257 + protected function html_curriculum_popup_templates( CourseModel $courseModel ): string {
1258 + $popup_templates = apply_filters(
1259 + 'learn-press/course-builder/courses/curriculum/popup-templates',
1260 + [
1261 + 'lesson' => sprintf(
1262 + '<script type="text/template" id="%1$s"><div class="lp-builder-popup-overlay"></div><div class="lp-builder-popup lp-builder-popup--loading">%2$s</div></script>',
1263 + esc_attr(
1264 + sprintf(
1265 + 'lp-tmpl-builder-popup-curriculum-lesson-course-%d',
1266 + $courseModel->get_id()
1267 + )
1268 + ),
1269 + TemplateAJAX::load_content_via_ajax(
1270 + [
1271 + 'id_url' => 'builder-popup-lesson-curriculum',
1272 + 'lesson_id' => 0,
1273 + 'html_no_load_ajax_first' => sprintf(
1274 + '<div class="lp-builder-popup__loader"><div class="lp-loading-circle"></div><span>%s</span></div>',
1275 + esc_html__( 'Loading...', 'learnpress' )
1276 + ),
1277 + ],
1278 + [
1279 + 'class' => BuilderPopupTemplate::class,
1280 + 'method' => 'render_lesson_popup',
1281 + ]
1282 + )
1283 + ),
1284 + 'quiz' => sprintf(
1285 + '<script type="text/template" id="%1$s"><div class="lp-builder-popup-overlay"></div><div class="lp-builder-popup lp-builder-popup--loading">%2$s</div></script>',
1286 + esc_attr(
1287 + sprintf(
1288 + 'lp-tmpl-builder-popup-curriculum-quiz-course-%d',
1289 + $courseModel->get_id()
1290 + )
1291 + ),
1292 + TemplateAJAX::load_content_via_ajax(
1293 + [
1294 + 'id_url' => 'builder-popup-quiz-curriculum',
1295 + 'quiz_id' => 0,
1296 + 'html_no_load_ajax_first' => sprintf(
1297 + '<div class="lp-builder-popup__loader"><div class="lp-loading-circle"></div><span>%s</span></div>',
1298 + esc_html__( 'Loading...', 'learnpress' )
1299 + ),
1300 + ],
1301 + [
1302 + 'class' => BuilderPopupTemplate::class,
1303 + 'method' => 'render_quiz_popup',
1304 + ]
1305 + )
1306 + ),
1307 + ],
1308 + $courseModel
1309 + );
1310 +
1311 + return Template::combine_components( $popup_templates );
1312 + }
1313 +
1314 +
1315 + /**
1316 + * Add edit popup button for lesson and quiz items in Course Builder curriculum.
1317 + * Replace the default edit button with popup button for lesson and quiz items.
1318 + *
1319 + * @param array $section_action Array of action buttons.
1320 + * @param object|null $item Item data.
1321 + * @param PostModel|null $itemModel Item model.
1322 + * @param CourseModel $courseModel .
1323 + * @param array $context_data Context data passed from AJAX.
1324 + *
1325 + * @return array
1326 + * @since 4.3.0
1327 + * @version 1.0.2
1328 + *
1329 + */
1330 + public function add_edit_popup_button( array $section_action, $item, $itemModel, $courseModel, $context_data = [] ): array {
1331 + // Check if we are in Course Builder context via the flag passed in AJAX args
1332 + $is_course_builder = ! empty( $context_data['is_course_builder'] );
1333 +
1334 + if ( ! $is_course_builder ) {
1335 + return $section_action;
1336 + }
1337 +
1338 + $item_id = $item->item_id ?? 0;
1339 + $item_type = $item->item_type ?? '';
1340 +
1341 + $popup_data_attr = '';
1342 + $popup_type = '';
1343 + if ( $item_type === LP_LESSON_CPT ) {
1344 + $popup_data_attr = sprintf( 'data-popup-lesson="%s"', $item_id );
1345 + $popup_type = 'lesson';
1346 + } elseif ( $item_type === LP_QUIZ_CPT ) {
1347 + $popup_data_attr = sprintf( 'data-popup-quiz="%s"', $item_id );
1348 + $popup_type = 'quiz';
1349 + }
1350 +
1351 + $popup_type = sanitize_key(
1352 + (string) apply_filters(
1353 + 'learn-press/course-builder/courses/curriculum/popup-item-type',
1354 + $popup_type,
1355 + $item_type,
1356 + $item,
1357 + $itemModel,
1358 + $courseModel,
1359 + $context_data
1360 + )
1361 + );
1362 + if ( empty( $popup_type ) ) {
1363 + return $section_action;
1364 + }
1365 +
1366 + $popup_template_id = sprintf(
1367 + 'lp-tmpl-builder-popup-curriculum-%1$s-course-%2$d',
1368 + $popup_type,
1369 + $courseModel->get_id()
1370 + );
1371 +
1372 + // Replace edit button with popup button - use lp-icon-edit-square instead of lp-icon-expand
1373 + $section_action['edit'] = sprintf(
1374 + '<li title="%s" class="lp-btn-edit-item-popup"
1375 + data-course-id="%s"
1376 + data-template="#%s"
1377 + data-popup-type="%s"
1378 + data-popup-id="%d"
1379 + %s>
1380 + <a class="lp-icon-edit-square edit-link edit-popup-link"></a>
1381 + </li>',
1382 + __( 'Edit in popup', 'learnpress' ),
1383 + $courseModel->get_id(),
1384 + esc_attr( $popup_template_id ),
1385 + esc_attr( $popup_type ),
1386 + $item_id,
1387 + $popup_data_attr
1388 + );
1389 +
1390 + return $section_action;
1391 + }
1392 +
1393 + /**
1394 + * Keep supported course settings tabs in Course Builder.
1395 + *
1396 + * @param array $tabs
1397 + *
1398 + * @return array
1399 + */
1400 + public function filter_course_builder_settings_tabs( array $tabs ): array {
1401 + $allowed_tabs = [ 'general', 'offline', 'price', 'extra', 'assessment', 'author', 'material' ];
1402 +
1403 + foreach ( array_keys( $tabs ) as $tab_key ) {
1404 + if ( ! in_array( $tab_key, $allowed_tabs, true ) ) {
1405 + unset( $tabs[ $tab_key ] );
1406 + }
1407 + }
1408 +
1409 + return apply_filters( 'learn-press/course-builder/edit-course/settings/tabs', $tabs );
1410 + }
1411 +
1412 + /**
1413 + * Convert final quiz edit link to Course Builder quiz settings URL.
1414 + *
1415 + * @param string $url
1416 + * @param int $final_quiz_id
1417 + *
1418 + * @return string
1419 + */
1420 + public function filter_course_builder_assessment_final_quiz_edit_link( string $url, int $final_quiz_id ): string {
1421 + $final_quiz_id = absint( $final_quiz_id );
1422 + if ( ! $final_quiz_id ) {
1423 + return $url;
1424 + }
1425 +
1426 + return CourseBuilder::get_tab_link( 'quizzes', $final_quiz_id, 'settings' ) . '#_lp_passing_grade';
1427 + }
1428 +}