Skip to content

Add ability to side-panel preview course materials#14907

Open
marcellamaki wants to merge 1 commit into
learningequality:developfrom
marcellamaki:course-preview
Open

Add ability to side-panel preview course materials#14907
marcellamaki wants to merge 1 commit into
learningequality:developfrom
marcellamaki:course-preview

Conversation

@marcellamaki

@marcellamaki marcellamaki commented Jun 30, 2026

Copy link
Copy Markdown
Member

Summary

After some slack discussion with Richard and Alex, this implements a "course content previewing" option based on the existing side panel lesson and quiz content previewing/selection experience. Within a course, specific subsections of content cannot be selected, thus no checkboxes -- it's just about seeing the content and being able to access everything a learner will see. Pre-post test assessments are not included.

References

Fixes #14149

Screen.Recording.2026-07-14.at.6.56.02.PM.mov

Reviewer guidance

Assign course workflow AND Course details workflow should be tested (although they share components, so it's unlikely that there would be significant UI differences but there might be some data bugs I suppose!)

  1. Access preview from the side panel
  2. "accurate"/expected tree navigation including into folders and the back button within the side panel
  3. All content types are preview-able
  4. Edge cases such as direct navigation to a particular page (for example, bookmarking, closing the tab, and then going directly to that URL) -- can you access properly? Does the "back" button still move you back up the tree
  5. UX feedback request -- there currently isn't a navigation guard (mainly because there isn't a selection state that is persisting like for example in quiz creation, so there isn't anything to prompt the user to "save" before closing the side panel if they click on the backdrop). Does that feel too inconsistent? But also -- if we add it, what would it prompt 🙃

one extra manual QA note: I had one instance on load where flexbook content didn't load and I didn't have my console open for an error 🤦‍♀️ . I haven't been able to reproduce it. Worth seeing if this pops up.

AI usage

Implementation done collaboratively with Claude (with moments for human in the loop feedback on the workflow from both me and Richard via slack discussions to do some decision making). Two separate rounds of human/self-review of the code. 1 upon initial completion, and now 1 ~2 weeks later when marking issue as ready for review.

@github-actions github-actions Bot added APP: Coach Re: Coach App (lessons, quizzes, groups, reports, etc.) DEV: frontend SIZE: medium labels Jun 30, 2026
@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

@marcellamaki marcellamaki left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added just a couple of notes

// Side panel routes accessible from the course summary page
COURSE_SUMMARY_ASSIGN: 'COURSE_SUMMARY_ASSIGN',
COURSE_SUMMARY_ASSIGN_COURSE_DETAILS: 'COURSE_SUMMARY_ASSIGN_COURSE_DETAILS',
COURSE_SUMMARY_ASSIGN_COURSE_PREVIEW: 'COURSE_SUMMARY_ASSIGN_COURSE_PREVIEW',

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be a bit redundant? but following the existing pattern for course routing as I suspect there was good reason for it :)

if (previewContentId.value) {
router.push(overrideRoute(route, { query: { previewContentId: undefined } }));
} else if (currentTopicId.value !== route.params.courseId) {
// topic.value isn't reset when a new topic starts fetching, so while

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an edge case I wouldn't have caught on my own (Claude did). "We" redrafted the comment a few times. Not sure if it's still helpful and clear enough -- happy to revise

@marcellamaki
marcellamaki marked this pull request as ready for review July 14, 2026 23:06
@marcellamaki
marcellamaki requested a review from rtibblesbot July 14, 2026 23:06
@rtibblesbot

rtibblesbot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

🔵 Review posted

Last updated: 2026-07-21 20:26 UTC

@rtibblesbot rtibblesbot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #14907 adds a browsable course-resource preview side panel. Implementation is clean and matches the linked scope; the tree navigation (remount :key, _topicFetchId stale-guard, no-trust-of-topic.value-while-loading) is carefully reasoned. Findings below are all non-blocking.

CI passing. Manual QA was required for these UI changes but did not run in this pipeline, so the rendered UI has not been visually verified — not approving on UI grounds.

  • suggestion — New logic-bearing components ship with no tests; the goBack/route-derivation state machine is the part most likely to regress silently (inline).
  • suggestionCourseResourcePreview :label="contentNode.kind" is dead and would be an untranslated enum if rendered (inline).
  • suggestion — Shared SidePanelModal header tweak is unrelated to this feature and silently affects the search-filters back state (inline).
  • suggestion — Topic title (content text) lacks dir="auto" for RTL (inline).
  • nitpick — Back during an in-flight topic fetch exits the whole preview (inline).

@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?

Ran a phased review pipeline over the pull request diff:

  • Classified the diff to select review passes (core, frontend, backend) and whether manual QA was required
  • Core review pass checked correctness, design, architecture, testing, completeness, and DRY/SRP/Rule-of-Three principles
  • Specialized frontend/backend review passes applied framework-specific lenses where those files changed
  • For UI changes: manual QA and an accessibility audit against a live dev server, when available
  • Checked CI status and linked issue acceptance criteria
  • Synthesized one review from those passes and chose the verdict from the findings, CI status, and QA evidence

);
}

function goBack() {

@rtibblesbot rtibblesbot Jul 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved — addressed in the current code.

suggestion: goBack and the getTopicRoute/getResourceRoute/exitPreview route derivation carry the branching logic most likely to regress silently, but no *.spec.js is touched in this branch. views/courses/__tests__/ already has the pattern (CourseSummaryPage.spec.js). A smoke test plus a focused test of goBack's three paths (resource→topic, topic→parent, root→exit) and the stale-loading fallback would be high value. AGENTS.md lists testing as required.

// loading it may still hold the previous topic's (stale) data —
// ignore it and fall back to exiting preview rather than navigating
// to the wrong parent
const parentId = loading.value ? undefined : topic.value?.parent;

@rtibblesbot rtibblesbot Jul 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved — addressed in the current code.

nitpick: When loading is true, parentId is forced to undefined, so a back-click during an in-flight topic fetch drops the user out of preview entirely rather than up one level. The comment documents this as deliberate and it's a rare race, so take-it-or-leave-it — but disabling the back button while loading would be more forgiving than repurposing it into "exit".

<KCircularLoader v-if="loading && !contentNode" />
<div v-else-if="contentNode">
<h2 class="resource-title">
<KLabeledIcon :label="contentNode.kind">

@rtibblesbot rtibblesbot Jul 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved — addressed in the current code.

suggestion: KLabeledIcon renders label only as the fallback of its default slot; since this usage supplies a default slot ({{ contentNode.title }}), :label is never rendered — it's dead. And contentNode.kind is a raw enum ('video', 'exercise'), an untranslated string were it ever shown. Drop the :label binding.

v-if="topic"
class="topic-header"
>
<h2 class="topic-title">

@rtibblesbot rtibblesbot Jul 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved — addressed in the current code.

suggestion: topic.title is channel/DB content, not an app string, so it isn't auto-flipped for RTL. Add dir="auto" so the browser picks direction from the text itself: <h2 class="topic-title" dir="auto">. (The resource title in CourseResourcePreview gets this for free via KLabeledIcon.)

class="header-content"
:style="{
flexDirection: closeButtonIconType === 'close' ? 'row' : 'row-reverse',
...(closeButtonIconType === 'back'

@rtibblesbot rtibblesbot Jul 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved — addressed in the current code.

suggestion: This shared-component tweak is unrelated to the course-preview flow, which uses courses/sidePanel/SidePanelLayout, not this modal. The only consumer setting closeButtonIconType='back' is SearchFiltersPanel (category back state), so this silently alters that panel's layout. If intentional, worth splitting out and confirming the search-filters back state still renders correctly; if it's leftover from an earlier iteration, drop it.

@pcenov

pcenov commented Jul 16, 2026

Copy link
Copy Markdown
Member

Hi @marcellamaki,

Noticed the following issues:

  1. If there's a video description it somehow impacts the back arrow button so that it's not clickable. I have to click the 'View more' button of the video description in order to enable the back arrow button. Happens in both Chrome and Firefox:
video.with.description.mp4
  1. The 'Save to bookmarks' button is not working at all for all resources:
bookmarks.mp4

Improvement suggestion:

  1. Since there is a lot of navigation going on from one folder to another I would recommend adding breadcrumb navigation so that users will know exactly where they are and will be able to quickly jump back. Personally felt lost several times... 🙃
whynobreadcrumb.mp4

@marcellamaki

Copy link
Copy Markdown
Member Author

Thank you @pcenov -- I'll address these! The video bit is particularly strange. Good find 😂

@rtibblesbot rtibblesbot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #14907 — delta re-review. 1 of 5 prior findings resolved (dir="auto" on the topic title); 4 still open (all suggestion/nitpick). No new findings. CI pending; manual QA was required but did not run, so the UI is not visually verified.

Still-open findings (see inline comments):

  • CoursePreviewSidePanel.vue:226 — no *.spec.js for goBack's three paths and the stale-loading fallback; views/courses/__tests__/ already has the pattern.
  • CourseResourcePreview.vue:7 — dead :label binding on a raw enum; drop it.
  • SidePanelModal/index.vue:33 — shared-component style change is unrelated to this flow; it only affects SearchFiltersPanel's back state.
  • CoursePreviewSidePanel.vue:234 — nitpick: back during in-flight fetch exits preview rather than going up a level (documented as deliberate).
Prior-finding status

RESOLVED — CoursePreviewSidePanel.vue:26 — topic.title needs dir="auto" for RTL
UNADDRESSED — CoursePreviewSidePanel.vue:226 — no spec for goBack/route derivation
UNADDRESSED — CoursePreviewSidePanel.vue:234 — loading fallback drops user out of preview
UNADDRESSED — CourseResourcePreview.vue:7 — dead :label binding on raw enum
UNADDRESSED — SidePanelModal/index.vue:33 — unrelated shared-component change


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?

Ran an automatic code-only delta review triggered by new commits on a previously reviewed PR:

  • Retrieved prior bot reviews via the GitHub API
  • Classified each prior finding as RESOLVED, UNADDRESSED, ACKNOWLEDGED, or CONTESTED
  • Only raised NEW findings for newly introduced code
  • Core review pass only — specialized frontend/backend lenses and manual QA run when a review is explicitly requested
  • Synthesized one review from the passes and chose the verdict from the findings, CI status, and QA evidence

);
}

function goBack() {

@rtibblesbot rtibblesbot Jul 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved — addressed in the current code.

suggestion: Still no *.spec.js in the branch. goBack and the getTopicRoute/getResourceRoute/exitPreview derivation carry the branching most likely to regress silently. A smoke test plus a focused test of goBack's three paths (resource→topic, topic→parent, root→exit) and the stale-loading fallback would be high value. views/courses/__tests__/ already has the pattern (CourseSummaryPage.spec.js); AGENTS.md lists testing as required.

// loading it may still hold the previous topic's (stale) data —
// ignore it and fall back to exiting preview rather than navigating
// to the wrong parent
const parentId = loading.value ? undefined : topic.value?.parent;

@rtibblesbot rtibblesbot Jul 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved — addressed in the current code.

nitpick: When loading is true, parentId is forced to undefined, so a back-click during an in-flight topic fetch drops the user out of preview rather than up one level. Documented as deliberate and a rare race — take-it-or-leave-it. Disabling the back button while loading would be more forgiving.

<KCircularLoader v-if="loading && !contentNode" />
<div v-else-if="contentNode">
<h2 class="resource-title">
<KLabeledIcon :label="contentNode.kind">

@rtibblesbot rtibblesbot Jul 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved — addressed in the current code.

suggestion: KLabeledIcon renders label only as the fallback of its default slot; this usage supplies a default slot, so :label never renders. And contentNode.kind is a raw enum (video, exercise) — an untranslated string were it ever shown. Drop the :label binding.

class="header-content"
:style="{
flexDirection: closeButtonIconType === 'close' ? 'row' : 'row-reverse',
...(closeButtonIconType === 'back'

@rtibblesbot rtibblesbot Jul 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved — addressed in the current code.

suggestion: This shared-component tweak is unrelated to the course-preview flow, which renders via courses/sidePanel/SidePanelLayout and does not wrap SidePanelModal — so it has zero effect on this feature. The only consumer that sets closeButtonIconType='back' is SearchFiltersPanel, so this silently alters that panel's category back-state header (adds justify-content: flex-end; gap: 8px). If intentional, split it out and confirm search-filters still renders correctly; if leftover, drop it.

@github-actions github-actions Bot added DEV: renderers HTML5 apps, videos, exercises, etc. SIZE: very large and removed SIZE: medium labels Jul 21, 2026
Implement rtibblesbot cleanup fixes

@rtibblesbot rtibblesbot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #14907 — delta re-review. All five prior findings resolved (dir="auto" persists, spec covers the three goBack paths + stale-loading fallback, dead :label replaced by KLabeledIcon+title, the unrelated SidePanelModal change is gone, loading fallback now hides the back button rather than exiting preview). One new edge-case suggestion below on the error path; not blocking.

CI pending. Manual QA was required for these UI changes but did not run this pass — the rendered UI is not visually verified, so this is not an approval on UI grounds.

  • suggestiongoBackHandler can dereference a null topic on the fetch-error path (see inline).
Prior-finding status

RESOLVED — CoursePreviewSidePanel.vue:26 — topic.title needs dir="auto" for RTL
RESOLVED — CoursePreviewSidePanel.vue:226 — no spec for goBack/route derivation
RESOLVED — CoursePreviewSidePanel.vue:234 — loading fallback drops user out of preview
RESOLVED — CourseResourcePreview.vue:7 — dead :label binding on raw enum
RESOLVED — SidePanelModal/index.vue:33 — unrelated shared-component change


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?

Ran an automatic code-only delta review triggered by new commits on a previously reviewed PR:

  • Retrieved prior bot reviews via the GitHub API
  • Classified each prior finding as RESOLVED, UNADDRESSED, ACKNOWLEDGED, or CONTESTED
  • Only raised NEW findings for newly introduced code
  • Core review pass only — specialized frontend/backend lenses and manual QA run when a review is explicitly requested
  • Synthesized one review from the passes and chose the verdict from the findings, CI status, and QA evidence

if (previewContentId.value) {
router.push(overrideRoute(route, { query: { previewContentId: undefined } }));
} else if (currentTopicId.value !== route.params.courseId) {
router.push(getTopicRoute(topic.value.parent));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: goBack guards the loading window but not the error window. useFetch sets loading=false and leaves data/topic untouched when fetchMethod rejects (packages/kolibri-common/composables/useFetch.js), and error isn't surfaced here. So on direct navigation to a deep topic URL where fetchTree rejects (previewTopicId set, no prior successful fetch), topic.value stays null, loading is false, and the back button renders — clicking it hits getTopicRoute(topic.value.parent)Cannot read properties of null (reading 'parent'). Guard this branch on topic.value (falling back to exitPreview()), and consider rendering error so the panel doesn't silently show a blank body. Low-probability path, not blocking.

@radinamatic

Copy link
Copy Markdown
Member

Course details workflow:

  • access preview from the side panel ✔️
  • tree navigation including into folders and the back button within the side panel ✔️
  • all content types are preview-able ✔️

Potential issue with direct navigation to a bookmarked page (preview of a single resource), "back" button apparently moves well up the tree with one difference (screenshot below):

  1. Course contents in a side panel once it has been assigned to recipients (entire class).
  2. Second browser tab where I opened a bookmarked page of a single resource preview, and repeatedly used the ⬅️, until I arrived to what seems like the same course contents in a side panel as in 1., but it also has a back ⬅️ button...? 🤨
  3. If I press that ⬅️ I get to the Select course to assign side panel. Nothing above necessarily needs to be a blocker, a part from the fact that it allowed me to assign the same course for a second time, even though it is already assigned to the entire class. I could understand it should be displayed and offered to assign again if the first one has only part of class as recipients (single learners or a group), but if a course has already been assigned to the entire class, what would be the point of it being displayed here again?
Ubuntu22 04 2606 (start)  Running  - Oracle VirtualBox_015

there isn't anything to prompt the user to "save" before closing the side panel if they click on the backdrop). Does that feel too inconsistent? But also -- if we add it, what would it prompt 🙃

Maybe a question for @jtamiace?

one instance on load where flexbook content didn't load

I couldn't replicate this with any Flexbox resources included in the 2 courses. Will keep an 👁️

Of the 2 issues reported by @pcenov, bookmarking resources during preview is now working correctly ✔️, but it is still not possible to use the back ⬅️ button if the video has a description (one must first press View more, so weird 😳).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

APP: Coach Re: Coach App (lessons, quizzes, groups, reports, etc.) DEV: frontend DEV: renderers HTML5 apps, videos, exercises, etc. SIZE: large

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Course description - Missing 'Preview' button

4 participants