Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 4 additions & 3 deletions apps/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,10 @@ Apps support multiple languages. Translation tags are scanned at build time via
When creating a pull request, use the template located at `/PULL_REQUEST_TEMPLATE` in the repository root. The template includes:
- Test plan description
- Issue references (refs:)
- Impact scope (affects:)
- Impact scope (affects: - only Student, Teacher, or Parent; can be multiple if affecting multiple apps)
- Release note
- Screenshots table (Before/After)
- Checklist (E2E tests, dark/light mode, landscape/tablet, accessibility, product approval)
- Checklist (dark/light mode, landscape/tablet, accessibility, product approval)

Note: Do not include E2E tests or screenshots sections in the PR description unless specifically needed.

Use `gh pr create` with the template to create PRs from the command line.
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.instructure.student.ui.rendertests

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.instructure.student.features.dashboard.compose.DashboardScreenContent
import com.instructure.student.features.dashboard.compose.DashboardUiState
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class DashboardScreenTest {

@get:Rule
val composeTestRule = createComposeRule()

@Test
fun testDashboardScreenShowsLoadingState() {
val mockUiState = DashboardUiState(
loading = true,
error = null,
refreshing = false,
onRefresh = {},
onRetry = {}
)

composeTestRule.setContent {
DashboardScreenContent(uiState = mockUiState)
}

composeTestRule.waitForIdle()
composeTestRule.onNodeWithTag("loading").assertIsDisplayed()
}

@Test
fun testDashboardScreenShowsErrorState() {
val mockUiState = DashboardUiState(
loading = false,
error = "An error occurred",
refreshing = false,
onRefresh = {},
onRetry = {}
)

composeTestRule.setContent {
DashboardScreenContent(uiState = mockUiState)
}

composeTestRule.waitForIdle()
composeTestRule.onNodeWithTag("errorContent").assertIsDisplayed()
}

@Test
fun testDashboardScreenShowsEmptyState() {
val mockUiState = DashboardUiState(
loading = false,
error = null,
refreshing = false,
onRefresh = {},
onRetry = {}
)

composeTestRule.setContent {
DashboardScreenContent(uiState = mockUiState)
}

composeTestRule.waitForIdle()
composeTestRule.onNodeWithTag("emptyContent").assertIsDisplayed()
}

@Test
fun testDashboardScreenShowsRefreshIndicator() {
val mockUiState = DashboardUiState(
loading = false,
error = null,
refreshing = true,
onRefresh = {},
onRetry = {}
)

composeTestRule.setContent {
DashboardScreenContent(uiState = mockUiState)
}

composeTestRule.waitForIdle()
composeTestRule.onNodeWithTag("dashboardPullRefreshIndicator").assertIsDisplayed()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ import com.instructure.student.features.modules.progression.CourseModuleProgress
import com.instructure.student.features.navigation.NavigationRepository
import com.instructure.student.features.todolist.ToDoListFragment
import com.instructure.student.fragment.BookmarksFragment
import com.instructure.student.fragment.DashboardFragment
import com.instructure.student.fragment.OldDashboardFragment
import com.instructure.student.fragment.NotificationListFragment
import com.instructure.student.fragment.OldToDoListFragment
import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionUploadEffectHandler
Expand Down Expand Up @@ -1012,7 +1012,7 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
private fun selectBottomNavFragment(fragmentClass: Class<out Fragment>) {
val selectedFragment = supportFragmentManager.findFragmentByTag(fragmentClass.name)

(topFragment as? DashboardFragment)?.cancelCardDrag()
(topFragment as? OldDashboardFragment)?.cancelCardDrag()

if (selectedFragment == null) {
val fragment = createBottomNavFragment(fragmentClass.name)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright (C) 2024 - 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.compose

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.interactions.router.Route
import com.instructure.pandautils.compose.CanvasTheme
import com.instructure.student.fragment.ParentFragment
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class DashboardFragment : ParentFragment() {

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
applyTheme()
return ComposeView(requireContext()).apply {
setContent {
CanvasTheme {
DashboardScreen()
}
}
}
}

override fun title(): String = ""

override fun applyTheme() {
navigation?.attachNavigationDrawer(this, null)
}

companion object {
fun makeRoute(canvasContext: CanvasContext?) =
Route(DashboardFragment::class.java, canvasContext)

fun newInstance(route: Route): DashboardFragment {
val fragment = DashboardFragment()
fragment.arguments = route.arguments
return fragment
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright (C) 2024 - 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.compose

import androidx.activity.compose.LocalActivity
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.instructure.pandautils.compose.composables.CanvasThemedAppBar
import com.instructure.pandautils.compose.composables.EmptyContent
import com.instructure.pandautils.compose.composables.ErrorContent
import com.instructure.pandautils.compose.composables.Loading
import com.instructure.student.R
import com.instructure.student.activity.NavigationActivity

@Composable
fun DashboardScreen() {
val viewModel: DashboardViewModel = hiltViewModel()
val uiState by viewModel.uiState.collectAsState()

DashboardScreenContent(uiState = uiState)
}

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun DashboardScreenContent(uiState: DashboardUiState) {
val activity = LocalActivity.current
val pullRefreshState = rememberPullRefreshState(
refreshing = uiState.refreshing,
onRefresh = uiState.onRefresh
)

Scaffold(
modifier = Modifier.background(colorResource(R.color.backgroundLightest)),
topBar = {
CanvasThemedAppBar(
title = stringResource(id = R.string.dashboard),
navIconRes = R.drawable.ic_hamburger,
navIconContentDescription = stringResource(id = R.string.navigation_drawer_open),
navigationActionClick = { (activity as? NavigationActivity)?.openNavigationDrawer() }
)
}
) { paddingValues ->
Box(
modifier = Modifier
.background(colorResource(R.color.backgroundLightest))
.padding(paddingValues)
.pullRefresh(pullRefreshState)
.fillMaxSize()
) {
when {
uiState.error != null -> {
ErrorContent(
errorMessage = uiState.error,
retryClick = uiState.onRetry,
modifier = Modifier
.fillMaxSize()
.testTag("errorContent")
)
}

uiState.loading -> {
Loading(modifier = Modifier
.fillMaxSize()
.testTag("loading"))
}

else -> {
EmptyContent(
emptyMessage = stringResource(id = R.string.noCoursesSubtext),
imageRes = R.drawable.ic_panda_nocourses,
modifier = Modifier
.fillMaxSize()
.testTag("emptyContent")
)
}
}

PullRefreshIndicator(
refreshing = uiState.refreshing,
state = pullRefreshState,
modifier = Modifier
.align(Alignment.TopCenter)
.testTag("dashboardPullRefreshIndicator")
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright (C) 2024 - 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.compose

data class DashboardUiState(
val loading: Boolean = true,
val error: String? = null,
val refreshing: Boolean = false,
val onRefresh: () -> Unit = {},
val onRetry: () -> Unit = {}
)
Loading
Loading