Skip to content

Commit 17ebf39

Browse files
authored
refactor(catalog): modularize CatalogScreen and add unit test suite
1 parent 15463c0 commit 17ebf39

12 files changed

Lines changed: 1843 additions & 989 deletions

File tree

‎ApiDemos/project/common-ui/build.gradle.kts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ dependencies {
8585
implementation(libs.lifecycle.runtime.ktx)
8686

8787
testImplementation(libs.junit)
88+
testImplementation(libs.truth)
8889
androidTestImplementation(libs.junit)
8990
androidTestImplementation(libs.espresso.core)
9091
}
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.common_ui.catalog.compose
18+
19+
import androidx.compose.foundation.layout.Arrangement
20+
import androidx.compose.foundation.layout.Column
21+
import androidx.compose.foundation.layout.Row
22+
import androidx.compose.foundation.layout.Spacer
23+
import androidx.compose.foundation.layout.fillMaxWidth
24+
import androidx.compose.foundation.layout.height
25+
import androidx.compose.foundation.shape.RoundedCornerShape
26+
import androidx.compose.material3.AlertDialog
27+
import androidx.compose.material3.Button
28+
import androidx.compose.material3.ButtonDefaults
29+
import androidx.compose.material3.Icon
30+
import androidx.compose.material3.MaterialTheme
31+
import androidx.compose.material3.OutlinedButton
32+
import androidx.compose.material3.OutlinedTextField
33+
import androidx.compose.material3.Text
34+
import androidx.compose.material3.TextButton
35+
import androidx.compose.runtime.Composable
36+
import androidx.compose.runtime.getValue
37+
import androidx.compose.runtime.mutableStateOf
38+
import androidx.compose.runtime.saveable.rememberSaveable
39+
import androidx.compose.runtime.setValue
40+
import androidx.compose.ui.Modifier
41+
import androidx.compose.ui.res.painterResource
42+
import androidx.compose.ui.text.font.FontWeight
43+
import androidx.compose.ui.unit.dp
44+
import androidx.compose.ui.unit.sp
45+
import com.example.common_ui.R
46+
import com.example.common_ui.catalog.Framework
47+
import com.example.common_ui.catalog.ReviewStatus
48+
import com.example.common_ui.catalog.SampleItem
49+
50+
/**
51+
* Host component managing the display of catalog dialogs (confirmation, detail viewer, quick grading).
52+
*/
53+
@Composable
54+
fun CatalogDialogHost(
55+
state: CatalogState,
56+
onSaveEvaluation: ((targetFqcn: String, status: ReviewStatus, notes: String, sample: SampleItem) -> Unit)?,
57+
onClearEvaluations: (() -> Unit)?,
58+
onLaunchSample: (SampleItem, Framework) -> Unit
59+
) {
60+
if (state.showClearConfirmDialog) {
61+
ClearEvaluationsDialog(
62+
onConfirm = {
63+
onClearEvaluations?.invoke()
64+
state.showClearConfirmDialog = false
65+
},
66+
onDismiss = { state.showClearConfirmDialog = false }
67+
)
68+
}
69+
70+
state.activeSampleForDetail?.let { sample ->
71+
val targetFqcn = sample.getTargetFqcn(state.selectedFramework)
72+
val existingEval = state.evaluations[targetFqcn] ?: state.evaluations[sample.id]
73+
SampleDetailFullScreenDialog(
74+
sample = sample,
75+
targetFqcn = targetFqcn,
76+
framework = state.selectedFramework,
77+
isReviewerMode = state.isReviewerMode,
78+
existingEvaluation = existingEval,
79+
onDismiss = { state.activeSampleDetailId = null },
80+
onSaveEvaluation = { status, notes ->
81+
onSaveEvaluation?.invoke(targetFqcn, status, notes, sample)
82+
state.activeSampleDetailId = null
83+
},
84+
onLaunch = { fw ->
85+
state.activeSampleDetailId = null
86+
onLaunchSample(sample, fw)
87+
}
88+
)
89+
}
90+
91+
state.activeQuickGrading?.let { (sample, gradeStatus) ->
92+
val targetFqcn = sample.getTargetFqcn(state.selectedFramework)
93+
val existingEval = state.evaluations[targetFqcn] ?: state.evaluations[sample.id]
94+
95+
QuickGradingDialog(
96+
sample = sample,
97+
targetFqcn = targetFqcn,
98+
gradeStatus = gradeStatus,
99+
initialNotes = existingEval?.notes.orEmpty(),
100+
onSave = { notes ->
101+
onSaveEvaluation?.invoke(targetFqcn, gradeStatus, notes, sample)
102+
state.activeQuickGradingSampleId = null
103+
state.activeQuickGradingStatus = null
104+
},
105+
onSaveAndNext = { notes ->
106+
onSaveEvaluation?.invoke(targetFqcn, gradeStatus, notes, sample)
107+
state.activeQuickGradingSampleId = null
108+
state.activeQuickGradingStatus = null
109+
val nextUnchecked = state.findNextUncheckedSample(currentSampleId = sample.id)
110+
if (nextUnchecked != null) {
111+
onLaunchSample(nextUnchecked, state.selectedFramework)
112+
}
113+
},
114+
onDismiss = {
115+
state.activeQuickGradingSampleId = null
116+
state.activeQuickGradingStatus = null
117+
}
118+
)
119+
}
120+
}
121+
122+
/**
123+
* Confirmation dialog for clearing / resetting all evaluations back to unchecked.
124+
*/
125+
@Composable
126+
fun ClearEvaluationsDialog(
127+
onConfirm: () -> Unit,
128+
onDismiss: () -> Unit
129+
) {
130+
AlertDialog(
131+
onDismissRequest = onDismiss,
132+
icon = {
133+
Icon(
134+
painter = painterResource(R.drawable.ic_undo),
135+
contentDescription = null,
136+
tint = MaterialTheme.colorScheme.error
137+
)
138+
},
139+
title = { Text("Reset All Review Evaluations?") },
140+
text = {
141+
Text("This will reset all ratings, status marks, and reviewer notes across all Kotlin and Java samples back to Unchecked.")
142+
},
143+
confirmButton = {
144+
Button(
145+
onClick = onConfirm,
146+
colors = ButtonDefaults.buttonColors(
147+
containerColor = MaterialTheme.colorScheme.error,
148+
contentColor = MaterialTheme.colorScheme.onError
149+
)
150+
) {
151+
Text("Reset All")
152+
}
153+
},
154+
dismissButton = {
155+
TextButton(onClick = onDismiss) {
156+
Text("Cancel")
157+
}
158+
}
159+
)
160+
}
161+
162+
/**
163+
* Quick grading dialog for entering optional notes and saving or saving & advancing to the next sample.
164+
*/
165+
@Composable
166+
fun QuickGradingDialog(
167+
sample: SampleItem,
168+
targetFqcn: String,
169+
gradeStatus: ReviewStatus,
170+
initialNotes: String,
171+
onSave: (notes: String) -> Unit,
172+
onSaveAndNext: (notes: String) -> Unit,
173+
onDismiss: () -> Unit
174+
) {
175+
var notes by rememberSaveable { mutableStateOf(initialNotes) }
176+
177+
AlertDialog(
178+
onDismissRequest = onDismiss,
179+
title = {
180+
Text(
181+
text = if (gradeStatus == ReviewStatus.PASSING) "👍 Good Job: ${sample.title}" else "⚠️ Something's Wrong: ${sample.title}",
182+
fontWeight = FontWeight.Bold,
183+
fontSize = 17.sp
184+
)
185+
},
186+
text = {
187+
Column(modifier = Modifier.fillMaxWidth()) {
188+
Text(
189+
text = "Target: ${targetFqcn.substringAfterLast('.')}",
190+
style = MaterialTheme.typography.labelSmall,
191+
color = MaterialTheme.colorScheme.outline
192+
)
193+
Spacer(modifier = Modifier.height(10.dp))
194+
OutlinedTextField(
195+
value = notes,
196+
onValueChange = { notes = it },
197+
label = { Text("Notes (optional for pass, describe issues if broken)") },
198+
modifier = Modifier.fillMaxWidth(),
199+
minLines = 3,
200+
maxLines = 5,
201+
shape = RoundedCornerShape(10.dp)
202+
)
203+
}
204+
},
205+
confirmButton = {
206+
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
207+
OutlinedButton(onClick = { onSave(notes) }) {
208+
Text(if (gradeStatus == ReviewStatus.PASSING) "Save Pass 👍" else "Save Issue ⚠️")
209+
}
210+
Button(
211+
onClick = { onSaveAndNext(notes) },
212+
colors = ButtonDefaults.buttonColors(
213+
containerColor = MaterialTheme.colorScheme.primary
214+
)
215+
) {
216+
Text("Save & Next ⏭️", fontWeight = FontWeight.Bold)
217+
}
218+
}
219+
},
220+
dismissButton = {
221+
TextButton(onClick = onDismiss) {
222+
Text("Cancel")
223+
}
224+
}
225+
)
226+
}

0 commit comments

Comments
 (0)