From 5c21dab441881a697dafbc1982804f6a331da789 Mon Sep 17 00:00:00 2001 From: Sazedul Haque Date: Sat, 31 Jan 2026 20:45:48 +0600 Subject: [PATCH 1/5] Add inline comment editing for discussions Implements inline editing for lesson comments: adds editCommentMutation, updateComment API call, editingId state and handleEditComment in discussions.ts; creates a reusable comment-form template for edit/reply forms; updates lesson-comment-card.php and lesson-comment-single.php to show an edit popover, swap comment display with an edit form, and update the comment text element (tutor-lesson-comment-text-) on success. Adds success/error toasts and preserves existing reply behavior. --- .../frontend/dashboard/pages/discussions.ts | 30 +++ .../dashboard/discussions/comment-form.php | 91 +++++++++ .../discussions/lesson-comment-card.php | 109 +++++++---- .../discussions/lesson-comment-single.php | 180 +++++++++++------- 4 files changed, 306 insertions(+), 104 deletions(-) create mode 100644 templates/dashboard/discussions/comment-form.php diff --git a/assets/src/js/frontend/dashboard/pages/discussions.ts b/assets/src/js/frontend/dashboard/pages/discussions.ts index 25b1227fbb..f07594c898 100644 --- a/assets/src/js/frontend/dashboard/pages/discussions.ts +++ b/assets/src/js/frontend/dashboard/pages/discussions.ts @@ -31,6 +31,7 @@ const discussionsPage = () => { query, deleteCommentMutation: null as MutationState | null, replyCommentMutation: null as MutationState | null, + editCommentMutation: null as MutationState | null, qnaSingleActionMutation: null as MutationState | null, deleteQnAMutation: null as MutationState | null, replyQnAMutation: null as MutationState | null, @@ -39,6 +40,7 @@ const discussionsPage = () => { isSolved: false, isImportant: false, isArchived: false, + editingId: null as number | null, init() { // Lesson comment delete mutation. @@ -64,6 +66,23 @@ const discussionsPage = () => { }, }); + // Lesson comment edit mutation. + this.editCommentMutation = this.query.useMutation(this.updateComment, { + onSuccess: (response, payload) => { + window.TutorCore.toast.success(__('Comment updated successfully.', 'tutor')); + + const element = document.getElementById(`tutor-lesson-comment-text-${payload.comment_id}`); + if (element) { + element.innerHTML = payload.comment; + } + + this.editingId = null; + }, + onError: (error: Error) => { + window.TutorCore.toast.error(error.message || __('Failed to update comment', 'tutor')); + }, + }); + // Q&A single action mutation (read, unread, solved, important, archived). this.qnaSingleActionMutation = this.query.useMutation(this.qnaSingleAction, { onSuccess: (response, payload: QnASingleActionPayload) => { @@ -119,6 +138,10 @@ const discussionsPage = () => { return wpAjaxInstance.post(endpoints.REPLY_LESSON_COMMENT, payload); }, + updateComment(payload: { comment_id: number; comment: string }) { + return wpAjaxInstance.post(endpoints.UPDATE_LESSON_COMMENT, payload); + }, + qnaSingleAction(payload: QnASingleActionPayload) { return wpAjaxInstance.post(endpoints.QNA_SINGLE_ACTION, payload); }, @@ -146,6 +169,13 @@ const discussionsPage = () => { } }, + handleEditComment(data: { comment: string }, commentId: number) { + this.editCommentMutation?.mutate({ + comment_id: commentId, + comment: data.comment, + }); + }, + handleKeydown(event: KeyboardEvent) { if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') { (event.target as HTMLFormElement).closest('form')?.requestSubmit(); diff --git a/templates/dashboard/discussions/comment-form.php b/templates/dashboard/discussions/comment-form.php new file mode 100644 index 0000000000..56db18d46a --- /dev/null +++ b/templates/dashboard/discussions/comment-form.php @@ -0,0 +1,91 @@ + + * @link https://themeum.com + * @since 4.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +use Tutor\Components\Button; +use Tutor\Components\Constants\InputType; +use Tutor\Components\Constants\Size; +use Tutor\Components\Constants\Variant; +use Tutor\Components\InputField; +use TUTOR\Icon; + +$label = $label ?? ''; +$submit_label = $submit_label ?? __( 'Update', 'tutor' ); +$form_class = $form_class ?? 'tutor-w-full'; +$show_shortcut_info = $show_shortcut_info ?? false; +$is_collapsible = $is_collapsible ?? false; +$default_value = $default_value ?? ''; +?> + +
+ type( InputType::TEXTAREA ) + ->name( 'comment' ) + ->placeholder( $placeholder ) + ->attr( 'x-bind', "register('comment', { required: '" . esc_js( __( 'Please enter a comment', 'tutor' ) ) . "' })" ) + ->attr( '@keydown', 'handleKeydown($event)' ); + + if ( $label ) { + $input->label( $label ); + } + + if ( $is_collapsible ) { + $input->attr( '@focus', 'focused = true' ); + } + + $input->render(); + ?> + +
+ x-cloak + :class="{ 'tutor-hidden': !focused }" + + > + +
+ render_svg_icon( Icon::COMMAND, 12, 12 ); ?> + + render_svg_icon( Icon::ENTER, 12, 12 ); ?> + +
+ + +
+ label( __( 'Cancel', 'tutor' ) ) + ->variant( Variant::GHOST ) + ->size( Size::X_SMALL ) + ->attr( 'type', 'button' ) + ->attr( '@click', $cancel_handler ) + ->render(); + + Button::make() + ->label( $submit_label ) + ->variant( Variant::PRIMARY_SOFT ) + ->size( Size::X_SMALL ) + ->attr( 'type', 'submit' ) + ->attr( ':disabled', $is_pending_prop ) + ->attr( ':class', "{ 'tutor-btn-loading': " . $is_pending_prop . ' }' ) + ->render(); + ?> +
+
+
diff --git a/templates/dashboard/discussions/lesson-comment-card.php b/templates/dashboard/discussions/lesson-comment-card.php index 05adda6a96..c4ecf37f85 100644 --- a/templates/dashboard/discussions/lesson-comment-card.php +++ b/templates/dashboard/discussions/lesson-comment-card.php @@ -16,6 +16,10 @@ use Tutor\Components\PreviewTrigger; use Tutor\Helpers\UrlHelper; use TUTOR\Lesson; +use Tutor\Components\Constants\InputType; +use Tutor\Components\Constants\Variant; +use Tutor\Components\InputField; +use Tutor\Components\Button; // Comment read-unread feature does not exist currently, will be added in future. $is_unread = 0; @@ -41,44 +45,79 @@ ); ?>
- user( $lesson_comment->user_id )->size( Size::SIZE_32 )->render(); ?> -
-
-
comment_author ); ?>
-
- - id( $lesson_comment->comment_post_ID )->render(); ?> - - id( $course->ID )->render(); ?> -
-
-
comment_content ); ?>
-
- -
- render_svg_icon( Icon::COMMENTS, 20, 20 ); ?> - +
+ user( $lesson_comment->user_id )->size( Size::SIZE_32 )->render(); ?> +
+
+
comment_author ); ?>
+
+ + id( $lesson_comment->comment_post_ID )->render(); ?> + + id( $course->ID )->render(); ?> +
+
comment_content ); ?>
+
+ +
+ render_svg_icon( Icon::COMMENTS, 20, 20 ); ?> + +
- -
- user( $last_reply->user_id )->size( Size::SIZE_20 )->render(); ?> -
comment_date_gmt ) ) ) ); //phpcs:ignore ?>
+ +
+ user( $last_reply->user_id )->size( Size::SIZE_20 )->render(); ?> +
comment_date_gmt ) ) ) ); //phpcs:ignore ?>
+
+ +
comment_date_gmt ) ) ) ); //phpcs:ignore ?>
+
- -
comment_date_gmt ) ) ) ); //phpcs:ignore ?>
- +
+
+ + + + user_id ) : ?> +
+ +
+
+ + +
+
+
+
-
- - - - -
+ + user_id ) : ?> +
+ 'lesson-comment-card-edit-' . (int) $lesson_comment->comment_ID, + 'default_value' => $lesson_comment->comment_content, + 'submit_handler' => '(data) => handleEditComment(data, ' . (int) $lesson_comment->comment_ID . ')', + 'cancel_handler' => 'reset(); editingId = null', + 'is_pending_prop' => 'editCommentMutation?.isPending', + 'placeholder' => __( 'Write your comment', 'tutor' ), + ) + ); + ?> +
+
diff --git a/templates/dashboard/discussions/lesson-comment-single.php b/templates/dashboard/discussions/lesson-comment-single.php index d3c7bce200..632b2c186f 100644 --- a/templates/dashboard/discussions/lesson-comment-single.php +++ b/templates/dashboard/discussions/lesson-comment-single.php @@ -32,6 +32,8 @@ return; } +$user_id = get_current_user_id(); + $replies_order = Input::get( 'order', 'DESC' ); $replies = Lesson::get_comment_replies( $discussion_id, $replies_order ); @@ -69,67 +71,69 @@ class="tutor-btn tutor-btn-secondary tutor-btn-small tutor-gap-2"
- + user_id ) : ?> +
+ +
+
+ + +
+
+
+
-
- comment_content ); ?> -
-
- comment_ID; ?> -
- type( InputType::TEXTAREA ) - ->name( 'comment' ) - ->label( __( 'Reply', 'tutor' ) ) - ->placeholder( __( 'Just drop your response here!', 'tutor' ) ) - ->attr( 'x-bind', "register('comment', { required: '" . esc_js( __( 'Please enter a response', 'tutor' ) ) . "' })" ) - ->attr( '@focus', 'focused = true' ) - ->attr( '@keydown', 'handleKeydown($event)' ) - ->render(); - ?> -
-
- render_svg_icon( Icon::COMMAND, 12, 12 ); ?> - - render_svg_icon( Icon::ENTER, 12, 12 ); ?> - +
+
+ comment_content ); ?>
-
- label( __( 'Cancel', 'tutor' ) ) - ->variant( Variant::GHOST ) - ->size( Size::X_SMALL ) - ->attr( 'type', 'button' ) - ->attr( '@click', 'reset(); focused = false' ) - ->attr( ':disabled', 'replyCommentMutation?.isPending' ) - ->render(); +
- Button::make() - ->label( __( 'Save', 'tutor' ) ) - ->variant( Variant::PRIMARY_SOFT ) - ->size( Size::X_SMALL ) - ->attr( 'type', 'submit' ) - ->attr( ':disabled', 'replyCommentMutation?.isPending' ) - ->attr( ':class', "{ 'tutor-btn-loading': replyCommentMutation?.isPending }" ) - ->render(); + user_id ) : ?> +
+ 'lesson-comment-edit-' . (int) $lesson_comment->comment_ID, + 'default_value' => $lesson_comment->comment_content, + 'submit_handler' => '(data) => handleEditComment(data, ' . (int) $lesson_comment->comment_ID . ')', + 'cancel_handler' => 'reset(); editingId = null', + 'is_pending_prop' => 'editCommentMutation?.isPending', + 'placeholder' => __( 'Write your comment', 'tutor' ), + ) + ); ?>
-
- + +
+ 'lesson-comment-reply-form-' . $lesson_comment->comment_ID, + 'submit_handler' => '(data) => replyCommentMutation?.mutate({ ...data, comment_post_ID: ' . (int) $course->ID . ', comment_parent: ' . (int) $lesson_comment->comment_ID . ' })', + 'cancel_handler' => 'reset(); focused = false', + 'is_pending_prop' => 'replyCommentMutation?.isPending', + 'placeholder' => __( 'Just drop your response here!', 'tutor' ), + 'label' => __( 'Reply', 'tutor' ), + 'submit_label' => __( 'Save', 'tutor' ), + 'form_class' => 'tutor-discussion-single-reply-form tutor-p-6 tutor-border-b', + 'show_shortcut_info' => true, + 'is_collapsible' => true, + ) + ); + ?>
@@ -141,23 +145,62 @@ class="tutor-discussion-single-reply-form tutor-p-6 tutor-border-b"
- user( $reply->user_id )->size( Size::SIZE_40 )->render(); ?> -
-
- - comment_author ); ?> - - - comment_date_gmt ) ) ) ); - ?> - +
+ user( $reply->user_id )->size( Size::SIZE_40 )->render(); ?> +
+
+ + comment_author ); ?> + + + comment_date_gmt ) ) ) ); + ?> + +
+
+ comment_content ); ?> +
-
- comment_content ); ?> + user_id ) : ?> +
+ +
+
+ + +
+
+
+ + user_id ) : ?> +
+ 'lesson-comment-edit-' . (int) $reply->comment_ID, + 'default_value' => $reply->comment_content, + 'submit_handler' => '(data) => handleEditComment(data, ' . (int) $reply->comment_ID . ')', + 'cancel_handler' => 'reset(); editingId = null', + 'is_pending_prop' => 'editCommentMutation?.isPending', + 'placeholder' => __( 'Write your comment', 'tutor' ), + ) + ); + ?> +
+
@@ -174,4 +217,3 @@ class="tutor-discussion-single-reply-form tutor-p-6 tutor-border-b" ->render(); ?>
- From f27d05655f323109869cb68ec39eb3fdeadb3c16 Mon Sep 17 00:00:00 2001 From: Sazedul Haque Date: Sun, 1 Feb 2026 15:24:22 +0600 Subject: [PATCH 2/5] Add discussion replies loading & UI updates Introduce loading and management of discussion replies: add new AJAX endpoint (tutor_load_discussion_replies) and Lesson::load_discussion_replies to return rendered replies HTML. Add templates/dashboard/discussions/comment-replies.php to render replies list and sorting UI. Update frontend JS to support reloadReplies (with order/url param handling), handleReplyComment, setEditing, centralized constants, and better mutation handling (use TutorCore.form/toast/modal, track editingFormId/loadingReplies). Add LOAD_DISCUSSION_REPLIES endpoint constant. Update comment form and comment templates to use consistent form IDs, adjusted props (is_pending, cancel behavior, focused handling), and wire reply/edit/delete flows (confirmation modal now sends is_reply). Overall fixes reply reloads, inline editing UX, and error/success feedback. --- .../frontend/dashboard/pages/discussions.ts | 151 +++++++++++++++--- assets/src/js/v3/shared/utils/endpoints.ts | 1 + classes/Lesson.php | 37 +++++ .../dashboard/discussions/comment-form.php | 70 ++++---- .../dashboard/discussions/comment-replies.php | 97 +++++++++++ .../discussions/lesson-comment-card.php | 18 +-- .../discussions/lesson-comment-single.php | 121 ++++---------- 7 files changed, 331 insertions(+), 164 deletions(-) create mode 100644 templates/dashboard/discussions/comment-replies.php diff --git a/assets/src/js/frontend/dashboard/pages/discussions.ts b/assets/src/js/frontend/dashboard/pages/discussions.ts index f07594c898..c3e3901289 100644 --- a/assets/src/js/frontend/dashboard/pages/discussions.ts +++ b/assets/src/js/frontend/dashboard/pages/discussions.ts @@ -8,6 +8,12 @@ interface ReplyCommentPayload { comment_parent: number; comment: string; } + +interface DeleteCommentPayload { + comment_id: number; + is_reply?: boolean; +} + interface QnASingleActionPayload { question_id: number; qna_action: string; @@ -20,16 +26,39 @@ interface ReplyQnAPayload { answer: string; } +const FORM_ID_PREFIXES = { + COMMENT_EDIT: 'lesson-comment-edit-', + COMMENT_REPLY: 'lesson-comment-reply-form-', +}; + +const MODALS = { + COMMENT_DELETE: 'tutor-comment-delete-modal', + QNA_DELETE: 'tutor-qna-delete-modal', +}; + +const ELEMENT_IDS = { + COMMENT_TEXT_PREFIX: 'tutor-lesson-comment-text-', + REPLIES_LIST_CONTAINER: 'tutor-discussion-replies-list', +}; + +const URL_PARAMS = { + ID: 'id', + ORDER: 'order', +}; + /** * Discussions Page Component * Handles Q&A, Lesson comments related actions */ const discussionsPage = () => { const query = window.TutorCore.query; + const form = window.TutorCore.form; + const toast = window.TutorCore.toast; + const modal = window.TutorCore.modal; return { query, - deleteCommentMutation: null as MutationState | null, + deleteCommentMutation: null as MutationState | null, replyCommentMutation: null as MutationState | null, editCommentMutation: null as MutationState | null, qnaSingleActionMutation: null as MutationState | null, @@ -41,45 +70,69 @@ const discussionsPage = () => { isImportant: false, isArchived: false, editingId: null as number | null, + editingFormId: null as string | null, + loadingReplies: false, + repliesOrder: 'DESC', + $nextTick: undefined as ((callback: () => void) => void) | undefined, init() { + // Set initial state from URL + const url = new URL(window.location.href); + this.repliesOrder = url.searchParams.get(URL_PARAMS.ORDER) || 'DESC'; + // Lesson comment delete mutation. this.deleteCommentMutation = this.query.useMutation(this.deleteComment, { - onSuccess: () => { - const url = new URL(window.location.href); - url.searchParams.delete('id'); - window.location.href = url.toString(); + onSuccess: (_, payload) => { + if (payload.is_reply) { + toast.success(__('Reply deleted successfully', 'tutor')); + modal.closeModal(MODALS.COMMENT_DELETE); + this.reloadReplies(); + } else { + const url = new URL(window.location.href); + url.searchParams.delete(URL_PARAMS.ID); + window.location.href = url.toString(); + } }, onError: (error: Error) => { - window.TutorCore.toast.error(error.message || __('Failed to delete Comment', 'tutor')); + toast.error(error.message || __('Failed to delete Comment', 'tutor')); }, }); // Lesson comment reply mutation this.replyCommentMutation = this.query.useMutation(this.replyComment, { - onSuccess: () => { - window.TutorCore.toast.success(__('Reply saved successfully', 'tutor')); - window.location.reload(); + onSuccess: (_, payload) => { + toast.success(__('Reply saved successfully', 'tutor')); + this.reloadReplies(); + + const formId = `${FORM_ID_PREFIXES.COMMENT_REPLY}${payload.comment_parent}`; + if (form.hasForm(formId)) { + form.reset(formId); + } }, onError: (error: Error) => { - window.TutorCore.toast.error(error.message || __('Failed to save reply', 'tutor')); + toast.error(error.message || __('Failed to save reply', 'tutor')); }, }); // Lesson comment edit mutation. this.editCommentMutation = this.query.useMutation(this.updateComment, { - onSuccess: (response, payload) => { - window.TutorCore.toast.success(__('Comment updated successfully.', 'tutor')); + onSuccess: (_, payload) => { + toast.success(__('Comment updated successfully.', 'tutor')); - const element = document.getElementById(`tutor-lesson-comment-text-${payload.comment_id}`); + const element = document.getElementById(`${ELEMENT_IDS.COMMENT_TEXT_PREFIX}${payload.comment_id}`); if (element) { element.innerHTML = payload.comment; } + if (this.editingFormId && form.hasForm(this.editingFormId)) { + form.reset(this.editingFormId); + this.editingFormId = null; + } + this.editingId = null; }, onError: (error: Error) => { - window.TutorCore.toast.error(error.message || __('Failed to update comment', 'tutor')); + toast.error(error.message || __('Failed to update comment', 'tutor')); }, }); @@ -102,7 +155,7 @@ const discussionsPage = () => { ); }, onError: (error: Error) => { - window.TutorCore.toast.error(error.message || __('Action failed', 'tutor')); + toast.error(error.message || __('Action failed', 'tutor')); }, }); @@ -110,27 +163,61 @@ const discussionsPage = () => { this.deleteQnAMutation = this.query.useMutation(this.deleteQnA, { onSuccess: () => { const url = new URL(window.location.href); - url.searchParams.delete('id'); + url.searchParams.delete(URL_PARAMS.ID); window.location.href = url.toString(); }, onError: (error: Error) => { - window.TutorCore.toast.error(error.message || __('Failed to delete Q&A', 'tutor')); + toast.error(error.message || __('Failed to delete Q&A', 'tutor')); }, }); // Q&A reply mutation. this.replyQnAMutation = this.query.useMutation(this.replyQnA, { onSuccess: () => { - window.TutorCore.toast.success(__('Reply saved successfully', 'tutor')); + toast.success(__('Reply saved successfully', 'tutor')); window.location.reload(); }, onError: (error: Error) => { - window.TutorCore.toast.error(error.message || __('Failed to save reply', 'tutor')); + toast.error(error.message || __('Failed to save reply', 'tutor')); }, }); }, - deleteComment(payload: { comment_id: number }) { + async reloadReplies(order?: string) { + if (order) { + this.repliesOrder = order; + } + + const url = new URL(window.location.href); + const commentId = parseInt(url.searchParams.get(URL_PARAMS.ID) || '0'); + + if (!commentId) return; + + this.loadingReplies = true; + try { + const response = await wpAjaxInstance.post(endpoints.LOAD_DISCUSSION_REPLIES, { + comment_id: commentId, + order: this.repliesOrder, + }); + + const container = document.getElementById(ELEMENT_IDS.REPLIES_LIST_CONTAINER); + if (container && typeof response.data?.html === 'string') { + container.innerHTML = response.data.html; + + // Update URL without reload + const url = new URL(window.location.href); + url.searchParams.set(URL_PARAMS.ORDER, this.repliesOrder); + window.history.pushState({}, '', url.toString()); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error('Failed to reload replies:', error); + } finally { + this.loadingReplies = false; + } + }, + + deleteComment(payload: DeleteCommentPayload) { return wpAjaxInstance.post(endpoints.DELETE_LESSON_COMMENT, payload); }, @@ -169,8 +256,16 @@ const discussionsPage = () => { } }, + handleReplyComment(data: { comment: string }, commentId: number, courseId: number) { + return this.replyCommentMutation?.mutate({ + comment: data.comment, + comment_parent: commentId, + comment_post_ID: courseId, + }); + }, + handleEditComment(data: { comment: string }, commentId: number) { - this.editCommentMutation?.mutate({ + return this.editCommentMutation?.mutate({ comment_id: commentId, comment: data.comment, }); @@ -181,6 +276,20 @@ const discussionsPage = () => { (event.target as HTMLFormElement).closest('form')?.requestSubmit(); } }, + + setEditing(id: number | null) { + this.editingId = id; + const formId = id ? `${FORM_ID_PREFIXES.COMMENT_EDIT}${id}` : null; + this.editingFormId = formId; + + if (id && formId) { + this.$nextTick?.(() => { + if (form.hasForm(formId)) { + form.setFocus(formId, 'comment'); + } + }); + } + }, }; }; diff --git a/assets/src/js/v3/shared/utils/endpoints.ts b/assets/src/js/v3/shared/utils/endpoints.ts index f8fd57a054..f01660523b 100644 --- a/assets/src/js/v3/shared/utils/endpoints.ts +++ b/assets/src/js/v3/shared/utils/endpoints.ts @@ -86,6 +86,7 @@ const endpoints = { UPDATE_LESSON_COMMENT: 'tutor_update_lesson_comment', DELETE_LESSON_COMMENT: 'tutor_delete_lesson_comment', REPLY_LESSON_COMMENT: 'tutor_reply_lesson_comment', + LOAD_DISCUSSION_REPLIES: 'tutor_load_discussion_replies', // Q&A QNA_SINGLE_ACTION: 'tutor_qna_single_action', diff --git a/classes/Lesson.php b/classes/Lesson.php index 70598e3168..297314de9b 100644 --- a/classes/Lesson.php +++ b/classes/Lesson.php @@ -112,6 +112,7 @@ public function __construct( $register_hooks = true ) { add_action( 'wp_ajax_tutor_update_lesson_comment', array( $this, 'ajax_update_lesson_comment' ) ); add_action( 'wp_ajax_tutor_reply_lesson_comment', array( $this, 'reply_lesson_comment' ) ); add_action( 'wp_ajax_tutor_load_lesson_comments', array( $this, 'load_lesson_comments' ) ); + add_action( 'wp_ajax_tutor_load_discussion_replies', array( $this, 'load_discussion_replies' ) ); // Add lesson title as nav item & render single content on the learning area. add_action( "tutor_learning_area_nav_item_{$this->post_type}", array( $this, 'render_nav_item' ), 10, 2 ); @@ -1092,4 +1093,40 @@ public function load_lesson_comments() { ) ); } + + /** + * Load discussion replies for dashboard. + * + * @since 4.0.0 + * + * @return void + */ + public function load_discussion_replies() { + tutor_utils()->check_nonce(); + + $discussion_id = Input::post( 'comment_id', 0, Input::TYPE_INT ); + $replies_order = Input::post( 'order', 'DESC' ); + + if ( ! $discussion_id ) { + $this->response_bad_request( __( 'Invalid discussion ID', 'tutor' ) ); + } + + $lesson_comment = get_comment( $discussion_id ); + if ( ! $lesson_comment ) { + $this->response_bad_request( __( 'Invalid discussion ID', 'tutor' ) ); + } + + $user_id = get_current_user_id(); + $replies = self::get_comment_replies( $discussion_id, $replies_order ); + $course = get_post( tutor_utils()->get_course_id_by( 'lesson', $lesson_comment->comment_post_ID ) ); + + ob_start(); + tutor_load_template( + 'dashboard.discussions.comment-replies', + compact( 'replies', 'replies_order', 'lesson_comment', 'user_id' ) + ); + $html = ob_get_clean(); + + wp_send_json_success( array( 'html' => $html ) ); + } } diff --git a/templates/dashboard/discussions/comment-form.php b/templates/dashboard/discussions/comment-form.php index 56db18d46a..b9cfa173ab 100644 --- a/templates/dashboard/discussions/comment-form.php +++ b/templates/dashboard/discussions/comment-form.php @@ -18,12 +18,11 @@ use Tutor\Components\InputField; use TUTOR\Icon; -$label = $label ?? ''; -$submit_label = $submit_label ?? __( 'Update', 'tutor' ); -$form_class = $form_class ?? 'tutor-w-full'; -$show_shortcut_info = $show_shortcut_info ?? false; -$is_collapsible = $is_collapsible ?? false; -$default_value = $default_value ?? ''; +$label = $label ?? ''; +$submit_label = $submit_label ?? __( 'Update', 'tutor' ); +$form_class = $form_class ?? 'tutor-w-full'; +$default_value = $default_value ?? ''; +$is_pending = $is_pending ?? 'false'; ?>
name( 'comment' ) ->placeholder( $placeholder ) ->attr( 'x-bind', "register('comment', { required: '" . esc_js( __( 'Please enter a comment', 'tutor' ) ) . "' })" ) - ->attr( '@keydown', 'handleKeydown($event)' ); + ->attr( '@keydown', 'handleKeydown($event)' ) + ->attr( '@focus', 'focused = true' ); if ( $label ) { $input->label( $label ); } - if ( $is_collapsible ) { - $input->attr( '@focus', 'focused = true' ); - } - $input->render(); ?>
- x-cloak - :class="{ 'tutor-hidden': !focused }" - + class="tutor-flex tutor-items-center tutor-mt-5 tutor-justify-between" + x-cloak + :class="{ 'tutor-hidden': !focused }" > - -
- render_svg_icon( Icon::COMMAND, 12, 12 ); ?> - - render_svg_icon( Icon::ENTER, 12, 12 ); ?> - -
- +
+ render_svg_icon( Icon::COMMAND, 12, 12 ); ?> + + render_svg_icon( Icon::ENTER, 12, 12 ); ?> + +
label( __( 'Cancel', 'tutor' ) ) - ->variant( Variant::GHOST ) - ->size( Size::X_SMALL ) - ->attr( 'type', 'button' ) - ->attr( '@click', $cancel_handler ) - ->render(); + Button::make() + ->label( __( 'Cancel', 'tutor' ) ) + ->variant( Variant::GHOST ) + ->size( Size::X_SMALL ) + ->attr( 'type', 'button' ) + ->attr( '@click', $cancel_handler ) + ->render(); - Button::make() - ->label( $submit_label ) - ->variant( Variant::PRIMARY_SOFT ) - ->size( Size::X_SMALL ) - ->attr( 'type', 'submit' ) - ->attr( ':disabled', $is_pending_prop ) - ->attr( ':class', "{ 'tutor-btn-loading': " . $is_pending_prop . ' }' ) - ->render(); + Button::make() + ->label( $submit_label ) + ->variant( Variant::PRIMARY_SOFT ) + ->size( Size::X_SMALL ) + ->attr( 'type', 'submit' ) + ->attr( ':disabled', $is_pending ) + ->attr( ':class', "{ 'tutor-btn-loading': " . $is_pending . ' }' ) + ->render(); ?>
diff --git a/templates/dashboard/discussions/comment-replies.php b/templates/dashboard/discussions/comment-replies.php new file mode 100644 index 0000000000..814f2a667b --- /dev/null +++ b/templates/dashboard/discussions/comment-replies.php @@ -0,0 +1,97 @@ + + * @link https://themeum.com + * @since 4.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +use Tutor\Components\Avatar; +use Tutor\Components\Constants\Size; +use Tutor\Components\Sorting; +use TUTOR\Icon; + +?> + +
+
+ + () +
+ order( $replies_order ) + ->on_change( 'reloadReplies' ) + ->bind_active_order( 'repliesOrder' ) + ->render(); + ?> +
+ +
+ +
+
+ user( $reply->user_id )->size( Size::SIZE_40 )->render(); ?> +
+
+ + comment_author ); ?> + + + comment_date_gmt ) ) ) ); + ?> + +
+
+ comment_content ); ?> +
+
+ user_id ) : ?> +
+ +
+
+ + +
+
+
+ +
+ + user_id ) : ?> +
+ 'lesson-comment-edit-' . (int) $reply->comment_ID, + 'default_value' => $reply->comment_content, + 'submit_handler' => '(data) => handleEditComment(data, ' . (int) $reply->comment_ID . ')', + 'cancel_handler' => 'reset(); editingId = null; focused = false', + 'is_pending' => 'editCommentMutation?.isPending', + 'placeholder' => __( 'Write your comment', 'tutor' ), + ) + ); + ?> +
+ +
+ +
+ diff --git a/templates/dashboard/discussions/lesson-comment-card.php b/templates/dashboard/discussions/lesson-comment-card.php index c4ecf37f85..184b5409af 100644 --- a/templates/dashboard/discussions/lesson-comment-card.php +++ b/templates/dashboard/discussions/lesson-comment-card.php @@ -16,10 +16,6 @@ use Tutor\Components\PreviewTrigger; use Tutor\Helpers\UrlHelper; use TUTOR\Lesson; -use Tutor\Components\Constants\InputType; -use Tutor\Components\Constants\Variant; -use Tutor\Components\InputField; -use Tutor\Components\Button; // Comment read-unread feature does not exist currently, will be added in future. $is_unread = 0; @@ -88,7 +84,7 @@
- @@ -109,12 +105,12 @@ tutor_load_template( 'dashboard.discussions.comment-form', array( - 'form_id' => 'lesson-comment-card-edit-' . (int) $lesson_comment->comment_ID, - 'default_value' => $lesson_comment->comment_content, - 'submit_handler' => '(data) => handleEditComment(data, ' . (int) $lesson_comment->comment_ID . ')', - 'cancel_handler' => 'reset(); editingId = null', - 'is_pending_prop' => 'editCommentMutation?.isPending', - 'placeholder' => __( 'Write your comment', 'tutor' ), + 'form_id' => 'lesson-comment-edit-' . (int) $lesson_comment->comment_ID, + 'default_value' => $lesson_comment->comment_content, + 'submit_handler' => '(data) => handleEditComment(data, ' . (int) $lesson_comment->comment_ID . ')', + 'cancel_handler' => 'reset(); editingId = null; focused = false', + 'is_pending' => 'editCommentMutation?.isPending', + 'placeholder' => __( 'Write your comment', 'tutor' ), ) ); ?> diff --git a/templates/dashboard/discussions/lesson-comment-single.php b/templates/dashboard/discussions/lesson-comment-single.php index 632b2c186f..17e958eac8 100644 --- a/templates/dashboard/discussions/lesson-comment-single.php +++ b/templates/dashboard/discussions/lesson-comment-single.php @@ -12,15 +12,10 @@ defined( 'ABSPATH' ) || exit; use Tutor\Components\Avatar; -use Tutor\Components\Button; use Tutor\Components\ConfirmationModal; -use Tutor\Components\Constants\InputType; use Tutor\Components\Constants\Size; -use Tutor\Components\Constants\Variant; use Tutor\Components\EmptyState; -use Tutor\Components\InputField; use Tutor\Components\PreviewTrigger; -use Tutor\Components\Sorting; use Tutor\Helpers\UrlHelper; use TUTOR\Icon; use TUTOR\Input; @@ -78,7 +73,7 @@ class="tutor-btn tutor-btn-secondary tutor-btn-small tutor-gap-2"
- @@ -105,12 +100,12 @@ class="tutor-btn tutor-btn-secondary tutor-btn-small tutor-gap-2" tutor_load_template( 'dashboard.discussions.comment-form', array( - 'form_id' => 'lesson-comment-edit-' . (int) $lesson_comment->comment_ID, - 'default_value' => $lesson_comment->comment_content, - 'submit_handler' => '(data) => handleEditComment(data, ' . (int) $lesson_comment->comment_ID . ')', - 'cancel_handler' => 'reset(); editingId = null', - 'is_pending_prop' => 'editCommentMutation?.isPending', - 'placeholder' => __( 'Write your comment', 'tutor' ), + 'form_id' => 'lesson-comment-edit-' . (int) $lesson_comment->comment_ID, + 'default_value' => $lesson_comment->comment_content, + 'submit_handler' => '(data) => handleEditComment(data, ' . (int) $lesson_comment->comment_ID . ')', + 'cancel_handler' => 'reset(); editingId = null; focused = false', + 'is_pending' => 'editCommentMutation?.isPending', + 'placeholder' => __( 'Write your comment', 'tutor' ), ) ); ?> @@ -121,90 +116,30 @@ class="tutor-btn tutor-btn-secondary tutor-btn-small tutor-gap-2" tutor_load_template( 'dashboard.discussions.comment-form', array( - 'form_id' => 'lesson-comment-reply-form-' . $lesson_comment->comment_ID, - 'submit_handler' => '(data) => replyCommentMutation?.mutate({ ...data, comment_post_ID: ' . (int) $course->ID . ', comment_parent: ' . (int) $lesson_comment->comment_ID . ' })', - 'cancel_handler' => 'reset(); focused = false', - 'is_pending_prop' => 'replyCommentMutation?.isPending', - 'placeholder' => __( 'Just drop your response here!', 'tutor' ), - 'label' => __( 'Reply', 'tutor' ), - 'submit_label' => __( 'Save', 'tutor' ), - 'form_class' => 'tutor-discussion-single-reply-form tutor-p-6 tutor-border-b', - 'show_shortcut_info' => true, - 'is_collapsible' => true, + 'form_id' => 'lesson-comment-reply-form-' . $lesson_comment->comment_ID, + 'submit_handler' => '(data) => handleReplyComment(data, ' . (int) $lesson_comment->comment_ID . ', ' . (int) $course->ID . ')', + 'cancel_handler' => 'reset(); focused = false', + 'is_pending' => 'replyCommentMutation?.isPending', + 'placeholder' => __( 'Just drop your response here!', 'tutor' ), + 'label' => __( 'Reply', 'tutor' ), + 'submit_label' => __( 'Save', 'tutor' ), + 'form_class' => 'tutor-discussion-single-reply-form tutor-p-6', ) ); ?> -
-
- - () -
- order( $replies_order )->render(); ?> -
- -
- -
-
- user( $reply->user_id )->size( Size::SIZE_40 )->render(); ?> -
-
- - comment_author ); ?> - - - comment_date_gmt ) ) ) ); - ?> - -
-
- comment_content ); ?> -
-
- user_id ) : ?> -
- -
-
- - -
-
-
- -
- - user_id ) : ?> -
- 'lesson-comment-edit-' . (int) $reply->comment_ID, - 'default_value' => $reply->comment_content, - 'submit_handler' => '(data) => handleEditComment(data, ' . (int) $reply->comment_ID . ')', - 'cancel_handler' => 'reset(); editingId = null', - 'is_pending_prop' => 'editCommentMutation?.isPending', - 'placeholder' => __( 'Write your comment', 'tutor' ), - ) - ); - ?> -
- -
- +
+ $replies, + 'replies_order' => $replies_order, + 'lesson_comment' => $lesson_comment, + 'user_id' => $user_id, + ) + ); + ?>
- title( __( 'Delete This Comment?', 'tutor' ) ) ->message( __( 'Are you sure you want to delete this comment permanently? Please confirm your choice.', 'tutor' ) ) ->confirm_text( __( 'Yes, Delete This', 'tutor' ) ) - ->confirm_handler( 'deleteCommentMutation?.mutate({ comment_id: payload?.commentId })' ) + ->confirm_handler( 'deleteCommentMutation?.mutate({ comment_id: payload?.commentId, is_reply: payload?.isReply })' ) ->mutation_state( 'deleteCommentMutation' ) ->render(); ?> From 511d7418c8def8f803b541a706b785fedf9e1b7f Mon Sep 17 00:00:00 2001 From: Sazedul Haque Date: Mon, 2 Feb 2026 15:36:50 +0600 Subject: [PATCH 3/5] Rename lesson-comment templates to comment Rename lesson-comment template files to a generic comment naming and update all references. Files renamed: lesson-comment-card.php -> comment-card.php, lesson-comment-list.php -> comment-list.php, lesson-comment-single.php -> comment-single.php. Updated templates/dashboard/discussions.php to load comment-list.php and comment-single.php when the current tab is 'lesson-comments', and adjusted the tutor_load_template call to use 'dashboard.discussions.comment-card'. This consolidates naming to be more generic and consistent. --- templates/dashboard/discussions.php | 4 ++-- .../discussions/{lesson-comment-card.php => comment-card.php} | 0 .../discussions/{lesson-comment-list.php => comment-list.php} | 2 +- .../{lesson-comment-single.php => comment-single.php} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename templates/dashboard/discussions/{lesson-comment-card.php => comment-card.php} (100%) rename templates/dashboard/discussions/{lesson-comment-list.php => comment-list.php} (98%) rename templates/dashboard/discussions/{lesson-comment-single.php => comment-single.php} (100%) diff --git a/templates/dashboard/discussions.php b/templates/dashboard/discussions.php index a6d179843f..f20745e8fa 100644 --- a/templates/dashboard/discussions.php +++ b/templates/dashboard/discussions.php @@ -45,7 +45,7 @@ if ( $discussion_id ) { $template = tutor()->path . 'templates/dashboard/discussions/qna-single.php'; if ( 'lesson-comments' === $current_tab ) { - $template = tutor()->path . 'templates/dashboard/discussions/lesson-comment-single.php'; + $template = tutor()->path . 'templates/dashboard/discussions/comment-single.php'; } require_once $template; } else { @@ -58,7 +58,7 @@ path . 'templates/dashboard/discussions/qna-list.php'; if ( 'lesson-comments' === $current_tab ) { - $template = tutor()->path . 'templates/dashboard/discussions/lesson-comment-list.php'; + $template = tutor()->path . 'templates/dashboard/discussions/comment-list.php'; } require_once $template; diff --git a/templates/dashboard/discussions/lesson-comment-card.php b/templates/dashboard/discussions/comment-card.php similarity index 100% rename from templates/dashboard/discussions/lesson-comment-card.php rename to templates/dashboard/discussions/comment-card.php diff --git a/templates/dashboard/discussions/lesson-comment-list.php b/templates/dashboard/discussions/comment-list.php similarity index 98% rename from templates/dashboard/discussions/lesson-comment-list.php rename to templates/dashboard/discussions/comment-list.php index 6580c0b7c4..0fd2a365f8 100644 --- a/templates/dashboard/discussions/lesson-comment-list.php +++ b/templates/dashboard/discussions/comment-list.php @@ -74,7 +74,7 @@ $lesson_comment, 'discussion_url' => $discussion_url, diff --git a/templates/dashboard/discussions/lesson-comment-single.php b/templates/dashboard/discussions/comment-single.php similarity index 100% rename from templates/dashboard/discussions/lesson-comment-single.php rename to templates/dashboard/discussions/comment-single.php From 55c1c93ab63a18c2c4c376883978ecb9f42174f2 Mon Sep 17 00:00:00 2001 From: Sazedul Haque Date: Tue, 3 Feb 2026 18:09:20 +0600 Subject: [PATCH 4/5] Rename discussion replies to comment replies Rename and standardize discussion-replies APIs and templates to use 'comment' terminology. Changes: update frontend endpoint constant (LOAD_DISCUSSION_REPLIES -> LOAD_COMMENT_REPLIES), change AJAX action hook and handler in Lesson (tutor_load_discussion_replies -> tutor_load_comment_replies and load_discussion_replies() -> load_comment_replies()), normalize parameter names and validation messages (discussion_id -> comment_id), adjust response data passed to templates (use explicit array keys and remove unused lesson_comment), and update templates to use the provided user_id variable for permission checks. These edits improve naming consistency and data handling for comment replies. --- assets/src/js/v3/shared/utils/endpoints.ts | 2 +- classes/Lesson.php | 26 +++++++++---------- .../dashboard/discussions/comment-replies.php | 4 +-- .../dashboard/discussions/comment-single.php | 7 +++-- 4 files changed, 18 insertions(+), 21 deletions(-) diff --git a/assets/src/js/v3/shared/utils/endpoints.ts b/assets/src/js/v3/shared/utils/endpoints.ts index c3a88ee686..8333474b25 100644 --- a/assets/src/js/v3/shared/utils/endpoints.ts +++ b/assets/src/js/v3/shared/utils/endpoints.ts @@ -86,7 +86,7 @@ const endpoints = { UPDATE_LESSON_COMMENT: 'tutor_update_lesson_comment', DELETE_LESSON_COMMENT: 'tutor_delete_lesson_comment', REPLY_LESSON_COMMENT: 'tutor_reply_lesson_comment', - LOAD_DISCUSSION_REPLIES: 'tutor_load_discussion_replies', + LOAD_COMMENT_REPLIES: 'tutor_load_comment_replies', // Q&A QNA_SINGLE_ACTION: 'tutor_qna_single_action', diff --git a/classes/Lesson.php b/classes/Lesson.php index 297314de9b..7d75b84223 100644 --- a/classes/Lesson.php +++ b/classes/Lesson.php @@ -112,7 +112,7 @@ public function __construct( $register_hooks = true ) { add_action( 'wp_ajax_tutor_update_lesson_comment', array( $this, 'ajax_update_lesson_comment' ) ); add_action( 'wp_ajax_tutor_reply_lesson_comment', array( $this, 'reply_lesson_comment' ) ); add_action( 'wp_ajax_tutor_load_lesson_comments', array( $this, 'load_lesson_comments' ) ); - add_action( 'wp_ajax_tutor_load_discussion_replies', array( $this, 'load_discussion_replies' ) ); + add_action( 'wp_ajax_tutor_load_comment_replies', array( $this, 'load_comment_replies' ) ); // Add lesson title as nav item & render single content on the learning area. add_action( "tutor_learning_area_nav_item_{$this->post_type}", array( $this, 'render_nav_item' ), 10, 2 ); @@ -1095,35 +1095,33 @@ public function load_lesson_comments() { } /** - * Load discussion replies for dashboard. + * Load comment replies for dashboard. * * @since 4.0.0 * * @return void */ - public function load_discussion_replies() { + public function load_comment_replies() { tutor_utils()->check_nonce(); - $discussion_id = Input::post( 'comment_id', 0, Input::TYPE_INT ); + $comment_id = Input::post( 'comment_id', 0, Input::TYPE_INT ); $replies_order = Input::post( 'order', 'DESC' ); - if ( ! $discussion_id ) { - $this->response_bad_request( __( 'Invalid discussion ID', 'tutor' ) ); - } - - $lesson_comment = get_comment( $discussion_id ); - if ( ! $lesson_comment ) { - $this->response_bad_request( __( 'Invalid discussion ID', 'tutor' ) ); + if ( ! $comment_id ) { + $this->response_bad_request( __( 'Invalid comment ID', 'tutor' ) ); } $user_id = get_current_user_id(); - $replies = self::get_comment_replies( $discussion_id, $replies_order ); - $course = get_post( tutor_utils()->get_course_id_by( 'lesson', $lesson_comment->comment_post_ID ) ); + $replies = self::get_comment_replies( $comment_id, $replies_order ); ob_start(); tutor_load_template( 'dashboard.discussions.comment-replies', - compact( 'replies', 'replies_order', 'lesson_comment', 'user_id' ) + array( + 'replies' => $replies, + 'replies_order' => $replies_order, + 'user_id' => $user_id, + ) ); $html = ob_get_clean(); diff --git a/templates/dashboard/discussions/comment-replies.php b/templates/dashboard/discussions/comment-replies.php index 814f2a667b..bd5a40ce71 100644 --- a/templates/dashboard/discussions/comment-replies.php +++ b/templates/dashboard/discussions/comment-replies.php @@ -53,7 +53,7 @@ comment_content ); ?>
- user_id ) : ?> + user_id ) : ?>
- user_id ) : ?> + user_id ) : ?>
$replies, - 'replies_order' => $replies_order, - 'lesson_comment' => $lesson_comment, - 'user_id' => $user_id, + 'replies' => $replies, + 'replies_order' => $replies_order, + 'user_id' => $user_id, ) ); ?> From 130f5e5548da6a84fa00064cade68b037d592607 Mon Sep 17 00:00:00 2001 From: Sazedul Haque Date: Thu, 5 Feb 2026 12:10:43 +0600 Subject: [PATCH 5/5] Refactor discussion UI and form behavior Update dashboard discussion templates to improve popover interactions and unify UI. Remove unused unread flag and add Alpine popover state to the comment card container; reply button now links to the single-comment view. Standardize popover menu items (added spacing class and explicit 20x20 icon sizes) across comment-card, comment-replies, and comment-single. Adjust popover wrapper markup for owner actions. Change comment form to use tutorForm mode 'onSubmit' and tweak layout (added tutor-sm-justify-end) for improved form behavior and alignment. --- .../dashboard/discussions/comment-card.php | 18 ++++++++---------- .../dashboard/discussions/comment-form.php | 4 ++-- .../dashboard/discussions/comment-replies.php | 8 ++++---- .../dashboard/discussions/comment-single.php | 8 ++++---- 4 files changed, 18 insertions(+), 20 deletions(-) diff --git a/templates/dashboard/discussions/comment-card.php b/templates/dashboard/discussions/comment-card.php index 184b5409af..16ea14bd8d 100644 --- a/templates/dashboard/discussions/comment-card.php +++ b/templates/dashboard/discussions/comment-card.php @@ -17,8 +17,6 @@ use Tutor\Helpers\UrlHelper; use TUTOR\Lesson; -// Comment read-unread feature does not exist currently, will be added in future. -$is_unread = 0; $course = get_post( tutor_utils()->get_course_id_by( 'lesson', $lesson_comment->comment_post_ID ) ); $last_reply = null; $replies = Lesson::get_comments( @@ -40,7 +38,7 @@ ) ); ?> -
+
user( $lesson_comment->user_id )->size( Size::SIZE_32 )->render(); ?>
@@ -55,9 +53,9 @@
comment_content ); ?>
- +
render_svg_icon( Icon::COMMENTS, 20, 20 ); ?> @@ -78,18 +76,18 @@ user_id ) : ?> -
+
- -
diff --git a/templates/dashboard/discussions/comment-form.php b/templates/dashboard/discussions/comment-form.php index b9cfa173ab..f18e2a183d 100644 --- a/templates/dashboard/discussions/comment-form.php +++ b/templates/dashboard/discussions/comment-form.php @@ -27,7 +27,7 @@ @@ -48,7 +48,7 @@ class="" ?>
diff --git a/templates/dashboard/discussions/comment-replies.php b/templates/dashboard/discussions/comment-replies.php index bd5a40ce71..8e3752202f 100644 --- a/templates/dashboard/discussions/comment-replies.php +++ b/templates/dashboard/discussions/comment-replies.php @@ -60,12 +60,12 @@
- -
diff --git a/templates/dashboard/discussions/comment-single.php b/templates/dashboard/discussions/comment-single.php index 84ded05a9b..1d9a6f9bd5 100644 --- a/templates/dashboard/discussions/comment-single.php +++ b/templates/dashboard/discussions/comment-single.php @@ -73,12 +73,12 @@ class="tutor-btn tutor-btn-secondary tutor-btn-small tutor-gap-2"
- -