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
2 changes: 2 additions & 0 deletions libs/pandares/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1929,6 +1929,8 @@
<string name="gradePreferencesHeaderSortBy">Sort By</string>
<string name="gradesEmptyTitle">No Assignments</string>
<string name="gradesEmptyMessage">It looks like assignments haven\'t been created in this space yet.</string>
<string name="noMatchingAssignments">No Matching Assignments</string>
<string name="noMatchingAssignmentsDescription">No assignments match your search. Try a different search term.</string>
<string name="errorLoadingGrades">We\'re having trouble loading your student\'s grades. Please try reloading the page or check back later.</string>
<string name="errorLoadingCourse">We\'re having trouble loading your student\'s course details. Please try reloading the page or check back later.</string>
<string name="gradesFilterContentDescription">Filter</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Expand All @@ -45,6 +46,7 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.instructure.pandautils.R
Expand All @@ -61,15 +63,18 @@ fun SearchBar(
searchQuery: String = "",
collapsable: Boolean = true,
@DrawableRes hintIcon: Int? = null,
collapseOnSearch: Boolean = false
collapseOnSearch: Boolean = false,
onQueryChange: ((String) -> Unit)? = null
) {
Row(
modifier = modifier
.height(56.dp),
verticalAlignment = Alignment.CenterVertically
) {
var expanded by remember { mutableStateOf(!collapsable) }
var query by remember { mutableStateOf(searchQuery) }
var query by rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue(searchQuery))
}
val keyboardController = LocalSoftwareKeyboardController.current
val focusRequester = remember { FocusRequester() }

Expand Down Expand Up @@ -103,15 +108,18 @@ fun SearchBar(
.focusRequester(focusRequester),
placeholder = { Text(placeholder) },
value = query,
onValueChange = { query = it },
onValueChange = {
query = it
onQueryChange?.invoke(it.text)
},
singleLine = true,
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Search
),
keyboardActions = KeyboardActions(
onSearch = {
keyboardController?.hide()
onSearch(query)
onSearch(query.text)
if (collapseOnSearch) {
expanded = false
onExpand?.invoke(false)
Expand Down Expand Up @@ -142,11 +150,11 @@ fun SearchBar(
)
},
trailingIcon = {
if (query.isNotEmpty()) {
if (query.text.isNotEmpty()) {
IconButton(
modifier = Modifier.testTag("clearButton"),
onClick = {
query = ""
query = TextFieldValue("")
onClear?.invoke()
}) {
Icon(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
Expand Down Expand Up @@ -73,6 +75,8 @@ import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
Expand Down Expand Up @@ -100,6 +104,7 @@ import com.instructure.pandautils.compose.composables.ErrorContent
import com.instructure.pandautils.compose.composables.FullScreenDialog
import com.instructure.pandautils.compose.composables.GroupHeader
import com.instructure.pandautils.compose.composables.Loading
import com.instructure.pandautils.compose.composables.SearchBar
import com.instructure.pandautils.compose.composables.SubmissionState
import com.instructure.pandautils.features.grades.gradepreferences.GradePreferencesScreen
import com.instructure.pandautils.utils.DisplayGrade
Expand Down Expand Up @@ -296,8 +301,46 @@ private fun GradesScreenContent(
)
}

AnimatedVisibility(
visible = uiState.isSearchExpanded,
enter = slideInVertically() + fadeIn(),
exit = slideOutVertically() + fadeOut()
) {
val focusRequester = remember { FocusRequester() }

LaunchedEffect(uiState.isSearchExpanded) {
if (uiState.isSearchExpanded) {
focusRequester.requestFocus()
}
}

SearchBar(
icon = R.drawable.ic_search_white_24dp,
searchQuery = uiState.searchQuery,
tintColor = colorResource(R.color.textDarkest),
placeholder = stringResource(R.string.search),
collapsable = false,
onSearch = {
actionHandler(GradesAction.SearchQueryChanged(it))
},
onClear = {
actionHandler(GradesAction.SearchQueryChanged(""))
},
onQueryChange = {
actionHandler(GradesAction.SearchQueryChanged(it))
},
modifier = Modifier
.testTag("searchField")
.focusRequester(focusRequester)
)
}

if (uiState.items.isEmpty()) {
EmptyContent()
if (uiState.searchQuery.length >= 3) {
EmptySearchContent()
} else {
EmptyContent()
}
}
}

Expand Down Expand Up @@ -422,6 +465,27 @@ private fun GradesCard(
modifier = Modifier.size(24.dp)
)
}
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.clickable {
actionHandler(GradesAction.ToggleSearch)
}
.semantics {
role = Role.Button
},
contentAlignment = Alignment.Center
) {
Icon(
painter = painterResource(id = R.drawable.ic_search_white_24dp),
contentDescription = stringResource(id = R.string.search),
tint = Color(userColor),
modifier = Modifier
.size(24.dp)
.testTag("searchIcon")
)
}
}
}

Expand All @@ -437,6 +501,18 @@ private fun EmptyContent() {
)
}

@Composable
private fun EmptySearchContent() {
EmptyContent(
emptyTitle = stringResource(id = R.string.noMatchingAssignments),
emptyMessage = stringResource(id = R.string.noMatchingAssignmentsDescription),
imageRes = R.drawable.ic_panda_space,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 32.dp, horizontal = 16.dp)
)
}

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun AssignmentItem(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ data class GradesUiState(
val onlyGradedAssignmentsSwitchEnabled: Boolean = true,
val gradeText: String = "",
val isGradeLocked: Boolean = false,
val snackbarMessage: String? = null
val snackbarMessage: String? = null,
val searchQuery: String = "",
val isSearchExpanded: Boolean = false
)

data class AssignmentGroupUiState(
Expand Down Expand Up @@ -97,6 +99,8 @@ sealed class GradesAction {
data class AssignmentClick(val id: Long) : GradesAction()
data object SnackbarDismissed : GradesAction()
data class ToggleCheckpointsExpanded(val assignmentId: Long) : GradesAction()
data object ToggleSearch : GradesAction()
data class SearchQueryChanged(val query: String) : GradesAction()
}

sealed class GradesViewModelAction {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import com.instructure.pandautils.R
import com.instructure.pandautils.compose.composables.DiscussionCheckpointUiState
import com.instructure.pandautils.features.grades.gradepreferences.SortBy
import com.instructure.pandautils.utils.Const
import com.instructure.pandautils.utils.debounce
import com.instructure.pandautils.utils.filterHiddenAssignments
import com.instructure.pandautils.utils.getAssignmentIcon
import com.instructure.pandautils.utils.getGrade
Expand Down Expand Up @@ -78,6 +79,16 @@ class GradesViewModel @Inject constructor(
private var courseGrade: CourseGrade? = null

private var customStatuses = listOf<CustomGradeStatusesQuery.Node>()
private var allItems = emptyList<AssignmentGroupUiState>()

private val debouncedSearch = debounce<String>(
coroutineScope = viewModelScope
) { query ->
val filteredItems = filterItems(allItems, query)
_uiState.update {
it.copy(items = filteredItems)
}
}

init {
loadGrades(
Expand Down Expand Up @@ -121,16 +132,18 @@ class GradesViewModel @Inject constructor(

courseGrade = repository.getCourseGrade(course, repository.studentId, enrollments, selectedGradingPeriod?.id)

val items = when (sortBy) {
allItems = when (sortBy) {
SortBy.GROUP -> groupByAssignmentGroup(assignmentGroups)
SortBy.DUE_DATE -> groupByDueDate(assignmentGroups)
}.filter {
it.assignments.isNotEmpty()
}

val filteredItems = filterItems(allItems, _uiState.value.searchQuery)

_uiState.update {
it.copy(
items = items,
items = filteredItems,
isLoading = false,
isRefreshing = false,
gradePreferencesUiState = it.gradePreferencesUiState.copy(
Expand Down Expand Up @@ -280,6 +293,21 @@ class GradesViewModel @Inject constructor(
context.getString(R.string.due, "$dateText $timeText")
} ?: context.getString(R.string.gradesNoDueDate)

private fun filterItems(items: List<AssignmentGroupUiState>, query: String): List<AssignmentGroupUiState> {
if (query.length < 3) return items

return items.mapNotNull { group ->
val filteredAssignments = group.assignments.filter { assignment ->
assignment.name.contains(query, ignoreCase = true)
}
if (filteredAssignments.isEmpty()) {
null
} else {
group.copy(assignments = filteredAssignments)
}
}
}

fun handleAction(action: GradesAction) {
when (action) {
is GradesAction.Refresh -> {
Expand Down Expand Up @@ -351,6 +379,24 @@ class GradesViewModel @Inject constructor(
}
_uiState.update { it.copy(items = items) }
}

is GradesAction.ToggleSearch -> {
val isExpanding = !uiState.value.isSearchExpanded
_uiState.update {
it.copy(
isSearchExpanded = isExpanding,
searchQuery = if (!isExpanding) "" else it.searchQuery,
items = if (!isExpanding) allItems else it.items
)
}
}

is GradesAction.SearchQueryChanged -> {
_uiState.update {
it.copy(searchQuery = action.query)
}
debouncedSearch(action.query)
}
}
}
}
Loading