Skip to content

UI cleanup#14533

Open
marcellamaki wants to merge 10 commits intolearningequality:release-v0.19.xfrom
marcellamaki:UI-cleanup
Open

UI cleanup#14533
marcellamaki wants to merge 10 commits intolearningequality:release-v0.19.xfrom
marcellamaki:UI-cleanup

Conversation

@marcellamaki
Copy link
Copy Markdown
Member

@marcellamaki marcellamaki commented Apr 3, 2026

Summary

Fairly comprehensive UI cleanup that prioritized:

  • UI consistency
  • proper mobile responsiveness
  • a11y improvements via semantic html approaches when possible

Before:

Screenshot 2026-04-03 at 1 41 08 PM Screenshot 2026-04-03 at 1 40 58 PM

comments have more details about particular technical decisions. there is still some deviation from the spec, but it feels more aligned in "intention" at least, and there are some things that we just don't have in our existing components to really be able to do 100% to spec without major changes.

Reviewer guidance

These changes focus on the coach main course page experience. Please test:

  • mobile responsiveness across all 3 tabs
  • the "options" menu should now be functional, and each of these actions should match with the dropdown menu on the main courses list page
Screen.Recording.2026-04-03.at.1.16.42.PM.mov
Screen.Recording.2026-04-03.at.1.19.13.PM.mov

AI usage

Just me n claude cleaning up the UI together. (I was very opinionated about the how of the UI cleanup. Claude had other suggestion re: performance improvements which i have noted in the comments and feel more ambivalent about)

@github-actions github-actions bot added APP: Coach Re: Coach App (lessons, quizzes, groups, reports, etc.) DEV: frontend SIZE: very large labels Apr 3, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 3, 2026

.then(session => {
courseSession.value = session;
return ContentNodeResource.fetchTree({ id: session.course });
return Promise.all([
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.

I had claude make this cleanup because i noticed when my vue devtools was open that we were definitely hitting red with loadtimes. this doesn't 100% solve the issue, but definitely brings them down and reduces the number of requests. open to other suggestions about how to improve, but this seemed like a small but somewhat helpful incremental improvement

COURSES_ASSIGN_COURSE_DETAILS: 'COURSES_ASSIGN_COURSE_DETAILS',
COURSES_ASSIGN_SELECT_RECIPIENTS: 'COURSES_ASSIGN_SELECT_RECIPIENTS',
COURSES_ASSIGN_PREVIEW_LEARNERS: 'COURSES_ASSIGN_PREVIEW_LEARNERS',
// Side panel routes accessible from the course summary page
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 so the options menu on the course page works

meta: {
titleParts: ['COURSE_NAME', 'CLASS_NAME'],
},
children: [
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.

likewise, so that the options menu works (while retaining the same background context state)

{{ numberOfResources$({ value: course?.on_device_resources }) }}
<CoachAppBarPage :pageTitle="coachPageTitle">
<KCircularLoader v-if="pageLoading" />
<KGrid v-else>
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 looks like a very large diff, but almost all of the content is the same. the main change here is just reformatting to use our grid/containers so this is 1) consistent with lessons and quizzes, and 2) so that we do not lose mobile responsiveness (which we had)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I was initially thinking, "Is the deviation from the Figma specs expected?" and then recalled the PR author is Marcella, so it definitely is 😆.

<span class="status-plain-message">{{
unitStatusMessages.plainMessage
}}</span>
<span
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.

I reverted the bold + plain message concat because it is going to give us problems in internationalization to have these strings be separate and then concatenated, so now it is a single string, non-bold, that doesn't assume the placement of the two parts of the phrase

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is something very common and very easy to miss during reviews. It'd be great if we instruct rtibblesbot to look for errors like this.

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.

Good suggestion — I'll add a check for translatable string concatenation (split strings that should be a single i18n key) to my review heuristics going forward.

</AccordionContainer>
</template>
<template #[TABS.LEARNERS]>
<div
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.

I added this at the learners tabs because there was no context on the learners tab about the current unit. this might not be the best approach, and can remove

unitReportInfo.value = {
...unitReportInfo.value,
[unit.id]: {
Promise.all(
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.

again, switch to Promise.all

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Feels like we should have a by_ids filter on the backend so that we don't fire many requests to the backend at once.

padding: '12px 16px',
padding: '0px 16px',
fontWeight: 'bold',
textAlign: isRtl(currentLanguage) ? 'right' : 'left',
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.

i checked this if i removed it, and things still aligned properly without

const { preTestCompleted, postTestCompleted, total } = activeUnitLearnerStats.value;
switch (unitPhase.value) {
case UnitPhase.PRE_TEST_PENDING:
return {
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.

again, cleaning up the bold/concat messages


const { getRecipientNamesForCourseSession } = useClassSummary();

const store = getCurrentInstance().proxy.$store;
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 all feels very verbose and i am going to revisit on monday to see if it can be cleaned up a bit

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could it be?


<!-- Content: learner has scores -->
<template v-else>
<!-- Stats row -->
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 was in the spec but it's confusing, i think we should just remove it

<div
role="table"
:aria-label="learnerReportLabel$()"
<table
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.

make a proper table - for a11y and for layout improvement including on mobile


<div>
<KCircularLoader v-if="loading" />
<template v-else-if="activeTestStatus === 'not_activated'">
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.

always show the table, although we still need to decide what to show for the leaner progress during course lessons (as opposed to during the assessments. but out of scope for now

&mdash;
</span>
</template>
<template v-else-if="colIndex === 2">
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.

I switched the order here so that the risk/evaluation level took precedence by default on mobile

Image

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

"Risk level" "On track" really doesn't make sense to me, conceptually - could we change this to "Unit progress"?

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.

yes i agree. i think there are a few places where we have language like this that i'd like to update comprehensively, but i'm fine also making this change here in this PR

Copy link
Copy Markdown
Member Author

@marcellamaki marcellamaki left a comment

Choose a reason for hiding this comment

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

added some comments about the changes and why

@marcellamaki marcellamaki force-pushed the UI-cleanup branch 2 times, most recently from e059da8 to 30f238f Compare April 3, 2026 17:44
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.

I deleted this because there were too many updates based on the changes i made, and the existing format no longer made sense and was rather brittle. i'd like to approach revisiting these tests after this and jacob's similar side panel PR are merged and have one general strategy and implementation that is consistent and concise

@marcellamaki marcellamaki marked this pull request as ready for review April 3, 2026 17:49
@marcellamaki marcellamaki requested a review from rtibblesbot April 3, 2026 17:49
Copy link
Copy Markdown
Contributor

@rtibblesbot rtibblesbot left a comment

Choose a reason for hiding this comment

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

UI cleanup for the coach course summary page — grid layout, mobile responsiveness, functional options menu, a11y improvements via semantic HTML.

CI passing. Visual inspection of PR screenshots and video frames confirms the layout looks clean on both desktop and mobile viewports — sidebar stacks properly, tabs work, active unit area is well-structured.

Findings:

  • blocking: Wrong error message in "Edit Recipients" handler (inline comment)
  • blocking: New i18n strings on release branch (inline comment)
  • blocking: Entire LearnersReport test suite deleted without replacement (inline comment)
  • suggestion: Hardcoded route name string in watcher (inline comment)
  • suggestion: Unused progressLabel$ import in test (inline comment)
  • praise: Semantic HTML upgrade in LearnerSidePanel (inline comment)
  • praise: Good use of Promise.all for parallel unit report fetching (inline comment)

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

How was this generated?

Reviewed the pull request diff checking for:

  • Correctness: bugs, edge cases, undocumented behavior, resource leaks, hardcoded values
  • Design: unnecessary complexity, naming, readability, comment accuracy, redundant state
  • Architecture: duplicated concerns, minimal interfaces, composition over inheritance
  • Testing: behavior-based assertions, mocks only at hard boundaries, accurate coverage
  • Completeness: missing dependencies, unupdated usages, i18n, accessibility, security
  • Principles: DRY (same reason to change), SRP, Rule of Three (no premature abstraction)
  • Checked CI status and linked issue acceptance criteria
  • For UI changes: inspected screenshots for layout, visual completeness, and consistency

}),
);
} catch (e) {
createSnackbar(courseDeleteError$());
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.

blocking: courseDeleteError$() is the wrong snackbar message here — this catch block handles a failure to fetch course content during "Edit Recipients", not a course deletion error. Use a generic error message like defaultErrorMessage$() (from coreStrings, already imported by useCourseSession) or add a dedicated string. Showing "There was an error deleting the course assignment" when the user clicked "Edit Recipients" would be confusing.

// is already mounted and courseSession hasn't changed, so the watcher above won't
// fire. This catches that case and clears the loading state set by the global guard.
'$route.name'(name) {
if (name === 'COURSE_SUMMARY' && this.courseSession) {
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: This uses the hardcoded string 'COURSE_SUMMARY' instead of the PageNames.COURSE_SUMMARY constant. The value happens to match, but using the constant would be more maintainable and consistent with the rest of the file. Since this is in the Options API watch block (not setup()), you'd need to import PageNames and reference it, or move this logic into a setup() watcher.

<div
role="table"
:aria-label="learnerReportLabel$()"
<table
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.

praise: Good upgrade from ARIA-role div table to semantic <table>/<thead>/<tbody>/<th scope="col"> elements. This is much better for screen reader navigation and requires less manual ARIA wiring.

};
});
}
}))
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.

praise: Refactoring the per-unit report fetching from a fire-and-forget loop to Promise.all with a single reactive update is a nice improvement — it avoids N intermediate re-renders and makes error handling cleaner.

@@ -12,7 +12,6 @@ const {
onTrackWithObjectivesSuffixLabel$,
xOfYCorrectLabel$,
progressLabel$,
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: progressLabel$ is imported here but only used in the "does not show PROGRESS row in empty state" test (line 99), which checks that a removed element doesn't appear. Since the stats row was removed from the component entirely, this test is trivially true and the import is effectively dead. Consider removing both the import and the test to avoid confusion.

Copy link
Copy Markdown
Contributor

@rtibblesbot rtibblesbot left a comment

Choose a reason for hiding this comment

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

Delta review of the latest commit (9e80aa2) addressing prior feedback.

Prior findings

Resolved:

  • Wrong error message in "Edit Recipients" handler (blocking) — code changed at CourseSummaryPage.vue:558, now uses a different string key (but see new finding below)
  • Hardcoded route name string in watcher (suggestion) — watchers moved to setup() using PageNames.COURSE_SUMMARY
  • Unused progressLabel$ import in test (suggestion) — import and associated test removed

Acknowledged (not re-raised):

  • Deleted LearnersReport.spec.js test suite (blocking) — author replied: tracked in #14534
  • New i18n strings on release branch (blocking) — author replied: "intentional for this release"

5/7 prior findings resolved or acknowledged. 0 re-raised. 1 new finding below.

CI passing. Visual inspection of PR screenshots and video frames confirms the layout is clean on desktop and mobile — sidebar stacks properly, tabs functional, learner report table renders correctly with risk level and groups columns, learning objectives accordion works.

New findings:

  • blocking: Non-existent string key courseAssignmentDeleteError$ — see inline comment
  • suggestion: Semantic mismatch in CoursesRootPage.vue delete error message — see inline comment
  • praise: Clean migration of Options API watchers into setup() — see inline comment

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

How was this generated?

Compared the current PR state against findings from a prior review:

  • Retrieved prior bot reviews deterministically via the GitHub API
  • Classified each prior finding as RESOLVED, UNADDRESSED, ACKNOWLEDGED, or CONTESTED
  • Only raised NEW findings for newly introduced code
  • Reviewed the pull request diff checking for correctness, design, architecture, testing, completeness, and adherence to DRY/SRP principles
  • Checked CI status and linked issue acceptance criteria

}),
);
} catch (e) {
createSnackbar(courseAssignmentDeleteError$());
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.

blocking: courseAssignmentDeleteError$ does not exist in coursesStrings.js. The available keys are courseDeleteError and courseAssignmentUpdateError. Since createTranslator only generates key$ methods for keys present in the definition object (see packages/kolibri/utils/i18n.js:158), this destructures as undefined and calling undefined() here will throw a TypeError at runtime when the "Edit Recipients" fetch fails.

This is likely meant to be courseAssignmentUpdateError$ ("There was an error updating the course assignment"), which fits the context of a failed recipient edit. Alternatively, if you want a delete-specific message, add a courseAssignmentDeleteError key to the translator definition in coursesStrings.js.

courseToDelete.value = null;
} catch (error) {
createSnackbar(courseDeleteError$());
createSnackbar(courseAssignmentUpdateError$());
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: The delete error handler uses courseAssignmentUpdateError$() ("There was an error updating the course assignment"), but this catch block handles a failed deleteModel call. The user just confirmed they want to delete — showing "updating" in the error message could be confusing. Consider either using a delete-specific message or making the courseAssignmentUpdateError message more generic (e.g., "There was an error with the course assignment").

watch(
() => route.name,
name => {
if (name === PageNames.COURSE_SUMMARY && courseSession.value) {
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.

praise: Clean migration of the Options API watchers into setup(), now using the PageNames.COURSE_SUMMARY constant instead of the hardcoded string. The comment explaining why the route-name watcher is needed alongside the courseSession watcher is helpful.

@pcenov
Copy link
Copy Markdown
Member

pcenov commented Apr 6, 2026

Hi @marcellamaki - I confirm that it looks much better now. Since there are too many moving parts and things in progress, I'll list the issues I noticed and we can then file follow-up issues for the ones not in scope for this PR:

  1. When I delete a course there's a brief display of the empty courses table and then I see all of the available courses with no snackbar confirmation message for the deletion:
delete.course.mp4
  1. After I edit the recipients the change gets reflected in the UI only if I manually refresh the page:
changed.recipients.mp4

A note that in this case there's missing snackbar confirmation message.

  1. There shouldn't be a confirmation modal when I'm trying to close the 'Select recipients' sidebar without having made any changes:
what.unsaved.edits.mp4
  1. In the 'Options' drop-down the 'Edit Recipients' label should be changed to 'Edit recipients'
  2. In mobile view the 'Options' drop-down should be positioned to the right for consistency with what we have elsewhere:
options.position.mp4
  1. In mobile view the Learning objectives tab shouldn't go to the next line if possible:
LO.tab.in.mobile.mp4
  1. The course info panel in general and specifically the 'Visible to learners' toggle are not implemented as specified in Figma. At least the distance between the 'Visible to learners' label and the toggle control should be removed:
visible.to.learners.mobile.mp4

@marcellamaki
Copy link
Copy Markdown
Member Author

Thanks, @pcenov for the helpful notes. I'll do my best to clean all of these up here, although (6) is a limitation of our tabs component at the moment unfortunately.

For (7)

The course info panel in general

i decided to go with consistency to the lessons and quizzes pages (which also helps with our mobile responsiveness), but I do see your point regarding the visibility toggle and will definitely update that.

@marcellamaki
Copy link
Copy Markdown
Member Author

Hi @pcenov - following items are fixed:

(1) no table flash and added snackbar

Screen.Recording.2026-04-06.at.9.25.28.PM.mov

(2) assignment updates without refresh, and snackbar
https://github.com/user-attachments/assets/be850e68-ded2-4e08-841a-4ced56b84536

(3) No confirmation modal if no changes

Screen.Recording.2026-04-06.at.9.27.07.PM.mov

(4) 'Edit recipients'
Screenshot 2026-04-06 at 9 24 40 PM

(5) and (7) options button placement and KSwitch/toggle alignment on mobile
Screenshot 2026-04-06 at 9 24 48 PM

(6) we will have to skip for now (ask Misha, Radina, or Alex why if you are feeling brave or bored 😂 )

Copy link
Copy Markdown
Contributor

@rtibblesbot rtibblesbot left a comment

Choose a reason for hiding this comment

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

Fourth-round review of commit b0dce071b2. One new blocking issue.

Prior findings

All 9 prior findings remain resolved or acknowledged — no changes since last review.

CI failing — see blocking finding below.

Blocking:

  • CI failure: LearningObjectiveSidePanel.vue calls nOfMLearners$() which was removed from coursesStrings.js in this PR — see displaced comment below

Praise:

  • Clean hasRecipientChanges unsaved-changes guard (inline comment)

LearningObjectiveSidePanel.vue:23,139,256 — blocking: nOfMLearners$ was removed from coursesStrings.js in this PR and replaced with nOfMLearnersworkingOnLessons and nOfMLearnersCompleted, but LearningObjectiveSidePanel.vue (added to the base branch by #14454) still uses nOfMLearners$ in three places. This causes TypeError: _vm.nOfMLearners$ is not a function and is what's failing CI right now.

Fix: replace all three uses of nOfMLearners$ in LearningObjectiveSidePanel.vue with nOfMLearnersCompleted$ (the string for "N of M learners completed" matches the completion-count context on line 23).


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

How was this generated?

Compared the current PR state against findings from a prior review:

  • Retrieved prior bot reviews deterministically via the GitHub API
  • Classified each prior finding as RESOLVED, UNADDRESSED, ACKNOWLEDGED, or CONTESTED
  • Only raised NEW findings for newly introduced code
  • Reviewed the pull request diff checking for correctness, design, architecture, testing, completeness, and adherence to DRY/SRP principles
  • Checked CI status and linked issue acceptance criteria


const isLoading = computed(() => coursesFetch.loading.value);

const hasRecipientChanges = computed(() => {
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.

praise: The originalGroupIds/originalLearnerIds snapshot approach combined with this set-equality check is a clean pattern for the unsaved-changes guard — it correctly ignores ordering differences in the selection and is zeroed out on setExistingAssignment so there are no false positives on page load.

@pcenov
Copy link
Copy Markdown
Member

pcenov commented Apr 7, 2026

Thanks @marcellamaki,

For point one the snackbar message is displayed only if I delete a course from the courses table, but if I delete the course from within the course itself then the snackbar message is not displayed:

deleted.course.mp4

I confirm that the rest of the points are fixed and I'll take your word on (6). :)

I will further revisit this for additional testing once other related PRs get merged.

@marcellamaki marcellamaki force-pushed the UI-cleanup branch 2 times, most recently from 4d8d217 to 6caa856 Compare April 7, 2026 13:56
Copy link
Copy Markdown
Contributor

@rtibblesbot rtibblesbot left a comment

Choose a reason for hiding this comment

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

Fifth-round review of commit 6caa856202. CI passing.

Prior findings

9 prior findings unchanged (stable from prior reviews).

Newly resolved:

  • nOfMLearners$ CI failure (blocking) ← was UNADDRESSED — completion count section removed from LearningObjectiveSidePanel.vue

10/10 prior findings resolved or acknowledged. 0 re-raised.


No new issues in the latest commits. The backRoute cleanup and nextTick() addition in CourseSummaryPage.vue look correct.


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

How was this generated?

Compared the current PR state against findings from a prior review:

  • Retrieved prior bot reviews deterministically via the GitHub API
  • Classified each prior finding as RESOLVED, UNADDRESSED, ACKNOWLEDGED, or CONTESTED
  • Only raised NEW findings for newly introduced code
  • Reviewed the pull request diff checking for correctness, design, architecture, testing, completeness, and adherence to DRY/SRP principles
  • Checked CI status and linked issue acceptance criteria

return { ...route, name: PageNames.COURSES_ROOT };
});
const { createSnackbar } = useSnackbar();
const backRoute = computed(() => ({
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.

praise: Replacing { ...route, name: PageNames.COURSES_ROOT } with an explicit { name, params } object prevents unintended leakage of query, hash, matched, and other route properties into the navigation target. Cleaner and more correct.

@rtibbles rtibbles self-assigned this Apr 7, 2026
@marcellamaki
Copy link
Copy Markdown
Member Author

if I delete the course from within the course itself then the snackbar message is not displayed

this is updated, and this should be ready for code review @rtibbles

@github-actions github-actions bot added DEV: backend Python, databases, networking, filesystem... APP: Learn Re: Learn App (content, quizzes, lessons, etc.) labels Apr 7, 2026
marcellamaki and others added 9 commits April 8, 2026 08:49
- Implement CourseSummaryPage with tabbed layout (Units, Learning Objectives,
  Learners) and coach-action header with dropdown menu
- Add units accordion showing active unit banner with phase-aware status
  messages, pre/post test controls, and learner count stats
- Add LearningObjectivesReport component with KTable and SparklineBar
- Add SparklineBar component with tooltip showing learner mastery distribution
- Use unified nOfMLearnersCompleted/nOfMLearnersworkingOnLessons strings
  (translation-safe, no string concatenation)
- Fix KTable headers validation (add columnId to each header)
- Fix page title to display "[course name] - [class name]" via coachPageTitle
  computed, avoiding missing CommonCoachStrings.COURSE_NAME key error
- Add mobile-responsive layouts using useKResponsiveWindow (flex direction
  flips to column on small screens, full-width buttons)
- Fix KDropdownMenu overflow with constrainToScrollParent=false
- Wire up coursesRoutes and PageNames constants for COURSE_SUMMARY routes
- Add eagerly-fetched unit report data shared between status display and tabs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Resolve duplicate store and activeUnitReport declarations introduced
  during rebase of UI-cleanup onto PR 14497
- Unify activeUnitLearnerStats to read directly from unitReportInfo
  so it shares the same data source as the PR modal counts
- Add flex-wrap: wrap to LearnerSidePanel warning/success banners so
  text wraps correctly on narrow screens

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Member

@rtibbles rtibbles left a comment

Choose a reason for hiding this comment

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

The global state feels like a blocker to me - the others are questions that need answers if not changes.


// Module-level ref so useCoreCoach can resolve the COURSE_NAME title part
// without needing to pass it through component props.
export const currentCourseName = ref(null);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is a code smell - it's global page specific state.

If the only concern here is to avoid prop drilling, please use provide/inject and inject it into useCoreCoach.

If that's not the concern, let's find another way.


const { getRecipientNamesForCourseSession } = useClassSummary();

const store = getCurrentInstance().proxy.$store;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could it be?


const coachPageTitle = computed(() => {
const parts = [course.value?.title, store.state.classSummary.name].filter(Boolean);
if (isRtl(currentLanguage)) parts.reverse();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is this actually the right way to do this? Surely it's dependent on the content of the parts?

const store = getCurrentInstance().proxy.$store;

const coachPageTitle = computed(() => {
const parts = [course.value?.title, store.state.classSummary.name].filter(Boolean);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Don't we have a composable wrapper for this to avoid directly accessing the store?

&mdash;
</span>
</template>
<template v-else-if="colIndex === 2">
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

"Risk level" "On track" really doesn't make sense to me, conceptually - could we change this to "Unit progress"?

Copy link
Copy Markdown
Member

@AlexVelezLl AlexVelezLl left a comment

Choose a reason for hiding this comment

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

Alex is too picky, I know, please ignore anything that doesn't feel too relevant :)

// Reset to avoid stale data
courseSession.value = null;
course.value = null;
currentCourseName.value = null;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Feels like we should have this at the same level as pageLoading.value = true to always reset the current course name when fetching a new course session? In case of an error, we wouldn't have the name of a previous course.

Comment on lines +30 to +41
children: [
{
name: PageNames.COURSE_SUMMARY_ASSIGN_COURSE_DETAILS,
path: ':courseId/course-details',
component: CourseDetailsSubpage,
},
{
name: PageNames.COURSE_SUMMARY_ASSIGN_SELECT_RECIPIENTS,
path: 'select-recipients',
component: SelectRecipientsSubpage,
},
],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just learned that we never implemented the preview learners view 😅. Was this intentionally left out of scope? Should we remove it from the courses assign side panel routes on the courses root page?

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.

hmmm i don't think it was intentionally left out of scope I think it was just an oversight... good catch 😓

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This composable has grown in a rather different direction from its initial purpose of serving as a composable that saves a temporary editing session that is created when the side panel is opened and destroyed when it is closed. It is now more like holding a global state, maintaining it in an updated state, and having a resetAssignment to reset the global state.

Given this deviation, it'd be great if we could update the description of this side panel here to match the current purpose.

Comment on lines 73 to +78
const setExistingAssignment = courseSession => {
courseSessionId.value = courseSession.id;
selectedGroupIds.value = [...(courseSession.assignments || [])];
selectedLearnerIds.value = [...(courseSession.learner_ids || [])];
originalGroupIds.value = [...(courseSession.assignments || [])];
originalLearnerIds.value = [...(courseSession.learner_ids || [])];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should also clean up these new originalGroupIds and originalLearnerIds in the resetAssignment method, just to prevent any inconsistent state.

selectedLearnerIds: inject('assignCourseSelectedLearnerIds'),
selectCourse: inject('assignCourseSelectCourse'),
courseSessionId: inject('assignCourseCourseSessionId'),
hasRecipientChanges: inject('assignCourseHasRecipientChanges'),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Now that we are updating this, could we also update the JS Docs with all the new changes?

@@ -175,9 +194,6 @@
:text="unit.numberedTitle"
:to="{}"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Some other empty to's objects. Just checking in, not sure if there is another issue for populating these.

Comment on lines +534 to +549
const confirmDeleteCourse = async () => {
if (!courseSession.value) return;
const courseId = courseSession.value.id;
try {
await CourseSessionResource.deleteModel({ id: courseId });
// Remove from module-level state so the list page shows correct data immediately
removeCourse(courseId);
router.push(backRoute.value, async () => {
await nextTick();
createSnackbar(courseDeleted$());
});
} catch {
createSnackbar(courseDeleteError$());
}
showDeleteModal.value = false;
};
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we move the api request to the useCourses composable so that we can share this logic with the courses root?

unitReportInfo.value = {
...unitReportInfo.value,
[unit.id]: {
Promise.all(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Feels like we should have a by_ids filter on the backend so that we don't fire many requests to the backend at once.

Comment on lines +642 to +648
).then(entries => {
const newInfo = {};
for (const { unitId, result } of entries) {
newInfo[unitId] = result;
}
unitReportInfo.value = newInfo;
});
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Now that we are here, could we change this to use async/await to prevent the callback hell?

Comment on lines +932 to +946
watch(courseSession, () => {
store.dispatch('notLoading');
});

// When the side panel closes and the route returns to COURSE_SUMMARY, the page
// is already mounted and courseSession hasn't changed, so the watcher above won't
// fire. This catches that case and clears the loading state set by the global guard.
watch(
() => route.name,
name => {
if (name === PageNames.COURSE_SUMMARY && courseSession.value) {
store.dispatch('notLoading');
}
},
);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is a common problem with subroutes, and in theory, this should not happen if we added all new pages into the skipLoading array on the app.js file, right?

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.) APP: Learn Re: Learn App (content, quizzes, lessons, etc.) DEV: backend Python, databases, networking, filesystem... DEV: frontend SIZE: very large

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants