Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
26a035e
[MBL-19461] Add domain layer for My Courses dashboard widget
hermannakos Dec 1, 2025
906ad9b
[MBL-19461] Add UI layer for My Courses dashboard widget
hermannakos Dec 1, 2025
dba679a
[MBL-19461] Add color overlay switch support to My Courses widget
hermannakos Dec 1, 2025
f2e4c52
[MBL-19461] Update CourseCard design to match Figma specifications
hermannakos Dec 1, 2025
90f60e9
[MBL-19461] Add navigation and grid layout to My Courses widget
hermannakos Dec 3, 2025
587ad12
[MBL-19461] Fix card ripple and add DashboardScreen previews
hermannakos Dec 4, 2025
7b33653
[MBL-19461] Add unit tests for My Courses widget
hermannakos Dec 4, 2025
f343542
[MBL-19461] Add interaction tests for My Courses widget
hermannakos Dec 4, 2025
cda23f7
Update EnsureDefaultWidgetsUseCaseTest to include courses widget
hermannakos Dec 4, 2025
7281e1e
Add CoursesWidgetBehavior stub to Horizon test module
hermannakos Dec 4, 2025
b2f0a87
fix card shadows + all courses button placement
hermannakos Dec 9, 2025
b02752b
[MBL-19461] Fix CoursesWidgetTest assertions after UI changes
hermannakos Dec 10, 2025
3a7be1b
[MBL-19461] Fix grade calculations and course filtering in My Courses…
hermannakos Dec 10, 2025
3dcd0d6
[MBL-19461] Fix group colors to use parent course color for course name
hermannakos Dec 10, 2025
3274d93
Merge remote-tracking branch 'origin/master' into MBL-19461-dashboard…
hermannakos Dec 10, 2025
e6ff87c
CRs
hermannakos Dec 11, 2025
4858b90
use group color on group card
hermannakos Dec 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import com.instructure.canvasapi2.utils.pageview.PandataInfo
import com.instructure.pandautils.features.dashboard.edit.EditDashboardRepository
import com.instructure.pandautils.features.dashboard.edit.EditDashboardRouter
import com.instructure.pandautils.features.dashboard.notifications.DashboardRouter
import com.instructure.pandautils.features.dashboard.widget.courses.CoursesWidgetBehavior
import com.instructure.pandautils.features.dashboard.widget.courses.CoursesWidgetRouter
import com.instructure.pandautils.features.discussion.details.DiscussionDetailsWebViewFragmentBehavior
import com.instructure.pandautils.features.discussion.router.DiscussionRouteHelperRepository
import com.instructure.pandautils.features.discussion.router.DiscussionRouter
Expand Down Expand Up @@ -128,4 +130,14 @@ class DefaultBindingsModule {
fun provideSpeedGraderPostPolicyRouter(): SpeedGraderPostPolicyRouter {
throw NotImplementedError()
}

@Provides
fun provideCoursesWidgetRouter(): CoursesWidgetRouter {
throw NotImplementedError()
}

@Provides
fun provideCoursesWidgetBehavior(): CoursesWidgetBehavior {
throw NotImplementedError()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright (C) 2025 - present Instructure, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.instructure.student.di.feature

import com.instructure.pandautils.features.dashboard.widget.courses.CoursesWidgetBehavior
import com.instructure.pandautils.features.dashboard.widget.courses.CoursesWidgetRouter
import com.instructure.student.features.dashboard.widget.courses.StudentCoursesWidgetBehavior
import com.instructure.student.features.dashboard.widget.courses.StudentCoursesWidgetRouter
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent

@Module
@InstallIn(ViewModelComponent::class)
class CoursesWidgetModule {

@Provides
fun provideCoursesWidgetRouter(): CoursesWidgetRouter {
return StudentCoursesWidgetRouter()
}

@Provides
fun provideCoursesWidgetBehavior(
studentCoursesWidgetBehavior: StudentCoursesWidgetBehavior
): CoursesWidgetBehavior {
return studentCoursesWidgetBehavior
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.interactions.router.Route
import com.instructure.pandautils.compose.CanvasTheme
import com.instructure.pandautils.features.dashboard.notifications.DashboardRouter
import com.instructure.pandautils.utils.ThemePrefs
import com.instructure.pandautils.utils.ViewStyler
import com.instructure.student.fragment.ParentFragment
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
Expand All @@ -40,7 +42,6 @@ class DashboardFragment : ParentFragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
applyTheme()
return ComposeView(requireContext()).apply {
setContent {
CanvasTheme {
Expand All @@ -50,10 +51,16 @@ class DashboardFragment : ParentFragment() {
}
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
applyTheme()
}

override fun title(): String = ""

override fun applyTheme() {
navigation?.attachNavigationDrawer(this, null)
ViewStyler.setStatusBarDark(requireActivity(), ThemePrefs.primaryColor)
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ import com.instructure.pandautils.compose.composables.Loading
import com.instructure.pandautils.features.dashboard.notifications.DashboardRouter
import com.instructure.pandautils.features.dashboard.widget.WidgetMetadata
import com.instructure.pandautils.features.dashboard.widget.courseinvitation.CourseInvitationsWidget
import com.instructure.pandautils.features.dashboard.widget.welcome.WelcomeWidget
import com.instructure.pandautils.features.dashboard.widget.courses.CoursesWidget
import com.instructure.pandautils.features.dashboard.widget.institutionalannouncements.InstitutionalAnnouncementsWidget
import com.instructure.pandautils.features.dashboard.widget.welcome.WelcomeWidget
import com.instructure.student.R
import com.instructure.student.activity.NavigationActivity
import kotlinx.coroutines.flow.SharedFlow
Expand Down Expand Up @@ -110,7 +111,7 @@ fun DashboardScreenContent(
}

Scaffold(
modifier = Modifier.background(colorResource(R.color.backgroundLightest)),
modifier = Modifier.background(colorResource(R.color.backgroundLight)),
topBar = {
CanvasThemedAppBar(
title = stringResource(id = R.string.dashboard),
Expand All @@ -125,7 +126,7 @@ fun DashboardScreenContent(
) { paddingValues ->
Box(
modifier = Modifier
.background(colorResource(R.color.backgroundLightest))
.background(colorResource(R.color.backgroundLight))
.padding(paddingValues)
.pullRefresh(pullRefreshState)
.fillMaxSize()
Expand Down Expand Up @@ -230,6 +231,7 @@ private fun GetWidgetComposable(
) {
return when (widgetId) {
WidgetMetadata.WIDGET_ID_WELCOME -> WelcomeWidget(refreshSignal = refreshSignal)
WidgetMetadata.WIDGET_ID_COURSES -> CoursesWidget(refreshSignal = refreshSignal, columns = columns)
WidgetMetadata.WIDGET_ID_COURSE_INVITATIONS -> CourseInvitationsWidget(
refreshSignal = refreshSignal,
columns = columns,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright (C) 2025 - present Instructure, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.instructure.student.features.dashboard.widget.courses

import android.content.Context
import android.content.SharedPreferences
import com.instructure.pandautils.domain.usecase.BaseFlowUseCase
import com.instructure.student.util.StudentPrefs
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import javax.inject.Inject

class ObserveColorOverlayUseCase @Inject constructor(
@ApplicationContext private val context: Context
) : BaseFlowUseCase<Unit, Boolean>() {

override fun execute(params: Unit): Flow<Boolean> = callbackFlow {
val sharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)

val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
if (key == KEY_HIDE_COURSE_COLOR_OVERLAY) {
trySend(!StudentPrefs.hideCourseColorOverlay)
}
}

sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
send(!StudentPrefs.hideCourseColorOverlay)

awaitClose {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener)
}
}

companion object {
private const val PREFS_NAME = "candroidSP"
private const val KEY_HIDE_COURSE_COLOR_OVERLAY = "hideCourseColorOverlay"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright (C) 2025 - present Instructure, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.instructure.student.features.dashboard.widget.courses

import android.content.Context
import android.content.SharedPreferences
import com.instructure.pandautils.domain.usecase.BaseFlowUseCase
import com.instructure.student.util.StudentPrefs
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import javax.inject.Inject

class ObserveGradeVisibilityUseCase @Inject constructor(
@ApplicationContext private val context: Context
) : BaseFlowUseCase<Unit, Boolean>() {

override fun execute(params: Unit): Flow<Boolean> = callbackFlow {
val sharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)

val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
if (key == KEY_SHOW_GRADES_ON_CARD) {
trySend(StudentPrefs.showGradesOnCard)
}
}

sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
send(StudentPrefs.showGradesOnCard)

awaitClose {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener)
}
}

companion object {
private const val PREFS_NAME = "candroidSP"
private const val KEY_SHOW_GRADES_ON_CARD = "showGradesOnCard"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright (C) 2025 - present Instructure, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.instructure.student.features.dashboard.widget.courses

import androidx.fragment.app.FragmentActivity
import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.models.Group
import com.instructure.pandautils.features.dashboard.widget.courses.CoursesWidgetBehavior
import com.instructure.pandautils.features.dashboard.widget.courses.CoursesWidgetRouter
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject

class StudentCoursesWidgetBehavior @Inject constructor(
private val observeGradeVisibilityUseCase: ObserveGradeVisibilityUseCase,
private val observeColorOverlayUseCase: ObserveColorOverlayUseCase,
private val router: CoursesWidgetRouter
) : CoursesWidgetBehavior {

override fun observeGradeVisibility(): Flow<Boolean> {
return observeGradeVisibilityUseCase(Unit)
}

override fun observeColorOverlay(): Flow<Boolean> {
return observeColorOverlayUseCase(Unit)
}

override fun onCourseClick(activity: FragmentActivity, course: Course) {
router.routeToCourse(activity, course)
}

override fun onGroupClick(activity: FragmentActivity, group: Group) {
router.routeToGroup(activity, group)
}

override fun onManageOfflineContent(activity: FragmentActivity, course: Course) {
router.routeToManageOfflineContent(activity, course)
}

override fun onCustomizeCourse(activity: FragmentActivity, course: Course) {
router.routeToCustomizeCourse(activity, course)
}

override fun onAllCoursesClicked(activity: FragmentActivity) {
router.routeToAllCourses(activity)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright (C) 2025 - present Instructure, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.instructure.student.features.dashboard.widget.courses

import androidx.fragment.app.FragmentActivity
import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.models.Group
import com.instructure.pandautils.features.dashboard.edit.EditDashboardFragment
import com.instructure.pandautils.features.dashboard.widget.courses.CoursesWidgetRouter
import com.instructure.student.features.coursebrowser.CourseBrowserFragment
import com.instructure.student.router.RouteMatcher

class StudentCoursesWidgetRouter : CoursesWidgetRouter {

override fun routeToCourse(activity: FragmentActivity, course: Course) {
RouteMatcher.route(activity, CourseBrowserFragment.makeRoute(course))
}

override fun routeToGroup(activity: FragmentActivity, group: Group) {
RouteMatcher.route(activity, CourseBrowserFragment.makeRoute(group))
}

override fun routeToManageOfflineContent(activity: FragmentActivity, course: Course) {
// TODO: Navigate to manage offline content screen
}

override fun routeToCustomizeCourse(activity: FragmentActivity, course: Course) {
// TODO: Navigate to customize course screen (color/nickname)
}

override fun routeToAllCourses(activity: FragmentActivity) {
RouteMatcher.route(activity, EditDashboardFragment.makeRoute())
}
}
Loading
Loading