TASK-042: colored status badges for issue/PR state (열림/닫힘/병합됨)
Replaces the plain-text 열림/닫힘/병합됨 label on issue/PR list rows and detail headers with a compact colored badge, reusing the VisibilityBadge pattern TASK-039 established for 공개/비공개. OPEN reuses the app's green primaryContainer, CLOSED reuses the baseline errorContainer, MERGED reuses the teal tertiaryContainer -- all existing theme roles, so dark mode is handled automatically with no new hardcoded colors. Found while verifying issue/PR body rendering for real content (confirmed correct, no bug) during the design-review pass. Live-verified on-device in both light and dark mode. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
39cbad522e
commit
471456258b
@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.width
|
|||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
@ -26,6 +27,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Tab
|
import androidx.compose.material3.Tab
|
||||||
import androidx.compose.material3.TabRow
|
import androidx.compose.material3.TabRow
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@ -37,6 +39,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
@ -155,7 +158,7 @@ private fun ModeTabs(tab: WorkItemsTab, onShowIssues: () -> Unit, onShowPullRequ
|
|||||||
private fun IssueRow(issue: IssueSummary, onSelected: () -> Unit) {
|
private fun IssueRow(issue: IssueSummary, onSelected: () -> Unit) {
|
||||||
Column(Modifier.fillMaxWidth().clickable(role = Role.Button, onClick = onSelected).padding(horizontal = 16.dp, vertical = 12.dp)) {
|
Column(Modifier.fillMaxWidth().clickable(role = Role.Button, onClick = onSelected).padding(horizontal = 16.dp, vertical = 12.dp)) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Text(stringResource(issue.state.labelResource), style = MaterialTheme.typography.bodySmall)
|
StatusBadge(issue.state)
|
||||||
Text("#${issue.number}", style = MaterialTheme.typography.bodySmall)
|
Text("#${issue.number}", style = MaterialTheme.typography.bodySmall)
|
||||||
}
|
}
|
||||||
Text(issue.title, style = MaterialTheme.typography.titleMedium, maxLines = 2)
|
Text(issue.title, style = MaterialTheme.typography.titleMedium, maxLines = 2)
|
||||||
@ -168,7 +171,7 @@ private fun IssueRow(issue: IssueSummary, onSelected: () -> Unit) {
|
|||||||
private fun PullRequestRow(pr: PullRequestSummary, onSelected: () -> Unit) {
|
private fun PullRequestRow(pr: PullRequestSummary, onSelected: () -> Unit) {
|
||||||
Column(Modifier.fillMaxWidth().clickable(role = Role.Button, onClick = onSelected).padding(horizontal = 16.dp, vertical = 12.dp)) {
|
Column(Modifier.fillMaxWidth().clickable(role = Role.Button, onClick = onSelected).padding(horizontal = 16.dp, vertical = 12.dp)) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Text(stringResource(pr.state.labelResource), style = MaterialTheme.typography.bodySmall)
|
StatusBadge(pr.state)
|
||||||
Text("#${pr.number}", style = MaterialTheme.typography.bodySmall)
|
Text("#${pr.number}", style = MaterialTheme.typography.bodySmall)
|
||||||
}
|
}
|
||||||
Text(pr.title, style = MaterialTheme.typography.titleMedium, maxLines = 2)
|
Text(pr.title, style = MaterialTheme.typography.titleMedium, maxLines = 2)
|
||||||
@ -182,7 +185,7 @@ private fun IssueDetailView(state: WorkItemsUiState.IssueDetailContent) {
|
|||||||
LazyColumn(Modifier.fillMaxSize()) {
|
LazyColumn(Modifier.fillMaxSize()) {
|
||||||
item {
|
item {
|
||||||
Column(Modifier.fillMaxWidth().padding(16.dp)) {
|
Column(Modifier.fillMaxWidth().padding(16.dp)) {
|
||||||
Text(stringResource(state.detail.state.labelResource), style = MaterialTheme.typography.bodySmall)
|
StatusBadge(state.detail.state, Modifier.padding(bottom = 4.dp))
|
||||||
Text(state.detail.title, style = MaterialTheme.typography.titleLarge)
|
Text(state.detail.title, style = MaterialTheme.typography.titleLarge)
|
||||||
Text(stringResource(R.string.workitems_author, state.detail.author.label()), style = MaterialTheme.typography.bodySmall)
|
Text(stringResource(R.string.workitems_author, state.detail.author.label()), style = MaterialTheme.typography.bodySmall)
|
||||||
if (state.detail.labels.isNotEmpty()) {
|
if (state.detail.labels.isNotEmpty()) {
|
||||||
@ -208,7 +211,7 @@ private fun PullRequestDetailView(state: WorkItemsUiState.PullRequestDetailConte
|
|||||||
LazyColumn(Modifier.fillMaxSize()) {
|
LazyColumn(Modifier.fillMaxSize()) {
|
||||||
item {
|
item {
|
||||||
Column(Modifier.fillMaxWidth().padding(16.dp)) {
|
Column(Modifier.fillMaxWidth().padding(16.dp)) {
|
||||||
Text(stringResource(state.detail.state.labelResource), style = MaterialTheme.typography.bodySmall)
|
StatusBadge(state.detail.state, Modifier.padding(bottom = 4.dp))
|
||||||
Text(state.detail.title, style = MaterialTheme.typography.titleLarge)
|
Text(state.detail.title, style = MaterialTheme.typography.titleLarge)
|
||||||
Text(stringResource(R.string.workitems_author, state.detail.author.label()), style = MaterialTheme.typography.bodySmall)
|
Text(stringResource(R.string.workitems_author, state.detail.author.label()), style = MaterialTheme.typography.bodySmall)
|
||||||
Text(stringResource(R.string.workitems_pr_refs, state.detail.baseRef, state.detail.headRef), style = MaterialTheme.typography.bodySmall)
|
Text(stringResource(R.string.workitems_pr_refs, state.detail.baseRef, state.detail.headRef), style = MaterialTheme.typography.bodySmall)
|
||||||
@ -416,6 +419,50 @@ private val PullRequestState.labelResource: Int
|
|||||||
PullRequestState.MERGED -> R.string.workitems_pr_merged
|
PullRequestState.MERGED -> R.string.workitems_pr_merged
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 열림/닫힘/병합됨 used to render as a plain bodySmall line indistinguishable in weight from the
|
||||||
|
// "#12" issue number next to it -- easy to miss when scanning a list. This reuses the same compact
|
||||||
|
// colored-Surface badge treatment RepositoryListScreen's VisibilityBadge already established for
|
||||||
|
// 공개/비공개, borrowing existing theme roles rather than inventing new colors: OPEN gets the app's
|
||||||
|
// green primaryContainer (this app's one "affirmative" accent, same one VisibilityBadge uses for a
|
||||||
|
// public repository), CLOSED gets the baseline Material3 errorContainer (matches how "closed"/
|
||||||
|
// "removed" states are treated elsewhere per Theme.kt's own doc comment), and MERGED -- a third,
|
||||||
|
// visually distinct state GitHub/Gitea conventionally colors purple, which this theme has no role
|
||||||
|
// for -- gets the teal tertiaryContainer instead of introducing an unthemed one-off color.
|
||||||
|
@Composable
|
||||||
|
private fun StatusBadge(state: IssueState, modifier: Modifier = Modifier) {
|
||||||
|
val (container, content) = when (state) {
|
||||||
|
IssueState.OPEN -> MaterialTheme.colorScheme.primaryContainer to MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
IssueState.CLOSED -> MaterialTheme.colorScheme.errorContainer to MaterialTheme.colorScheme.onErrorContainer
|
||||||
|
}
|
||||||
|
StatusBadge(stringResource(state.labelResource), container, content, modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun StatusBadge(state: PullRequestState, modifier: Modifier = Modifier) {
|
||||||
|
val (container, content) = when (state) {
|
||||||
|
PullRequestState.OPEN -> MaterialTheme.colorScheme.primaryContainer to MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
PullRequestState.CLOSED -> MaterialTheme.colorScheme.errorContainer to MaterialTheme.colorScheme.onErrorContainer
|
||||||
|
PullRequestState.MERGED -> MaterialTheme.colorScheme.tertiaryContainer to MaterialTheme.colorScheme.onTertiaryContainer
|
||||||
|
}
|
||||||
|
StatusBadge(stringResource(state.labelResource), container, content, modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun StatusBadge(label: String, containerColor: Color, contentColor: Color, modifier: Modifier = Modifier) {
|
||||||
|
Surface(
|
||||||
|
color = containerColor,
|
||||||
|
contentColor = contentColor,
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val ReviewState.labelResource: Int
|
private val ReviewState.labelResource: Int
|
||||||
get() = when (this) {
|
get() = when (this) {
|
||||||
ReviewState.PENDING -> R.string.workitems_review_pending
|
ReviewState.PENDING -> R.string.workitems_review_pending
|
||||||
|
|||||||
@ -845,3 +845,45 @@ This is the append-only implementation and handoff record shared by Codex and Cl
|
|||||||
detail views with real content, file viewers, extended dark-mode coverage) from where it was paused
|
detail views with real content, file viewers, extended dark-mode coverage) from where it was paused
|
||||||
for this bug fix.
|
for this bug fix.
|
||||||
- Next owner: Claude, continuing per the standing direction.
|
- Next owner: Claude, continuing per the standing direction.
|
||||||
|
|
||||||
|
## 2026-08-29 (continued) — TASK-042: colored status badges for issue/PR state
|
||||||
|
|
||||||
|
- Agent and role: Claude, direct continuation of the same live session, per "쭉 이어서 개발이랑 디자인
|
||||||
|
진행해줄래?" — resuming the paused design-review pass named as next work in TASK-041's log entry.
|
||||||
|
- Investigation: first re-verified the review item flagged as unresolved at the pause point — whether
|
||||||
|
issue/PR body text renders correctly for real content (the one PR viewed earlier happened to have no
|
||||||
|
body). Opened the real "PostActivated 관련 추가 필요" issue (multi-paragraph Korean body plus an
|
||||||
|
embedded C# code block) and confirmed it renders correctly end to end — no bug found there. While
|
||||||
|
doing that, noticed the 열림/닫힘/병합됨 state label next to it (and next to every issue/PR, in both
|
||||||
|
list rows and detail headers) was a plain, uncolored `bodySmall` line, no more visually prominent
|
||||||
|
than the `#8` issue number beside it — inconsistent with the colored `VisibilityBadge` treatment
|
||||||
|
TASK-039 already gave 공개/비공개 on the repository list.
|
||||||
|
- Commit/diff: on `claude/task-015-notifications`; `feature/workitems/WorkItemsScreen.kt` gained a
|
||||||
|
`StatusBadge` composable (two thin state-typed overloads sharing one rendering implementation)
|
||||||
|
replacing the four plain-text state-label call sites (`IssueRow`, `PullRequestRow`,
|
||||||
|
`IssueDetailView`, `PullRequestDetailView`); `tasks/042-workitem-status-badges.md`.
|
||||||
|
- How it was implemented: reused existing Material3 theme roles rather than inventing new colors —
|
||||||
|
OPEN gets `primaryContainer` (the same green `VisibilityBadge` uses for a public repository), CLOSED
|
||||||
|
gets the baseline `errorContainer` `Theme.kt` already reserves for closed/removed states, and MERGED
|
||||||
|
(a third state GitHub/Gitea conventionally color purple, which this theme has no role for) gets the
|
||||||
|
teal `tertiaryContainer` instead. Both light and dark mode are handled automatically since no new
|
||||||
|
hardcoded color was introduced.
|
||||||
|
- Files changed: `feature/workitems/WorkItemsScreen.kt`, `tasks/042-workitem-status-badges.md`.
|
||||||
|
- Checks and results: `./gradlew compileDebugKotlin compileDebugUnitTestKotlin testDebugUnitTest assembleDebug lintDebug`
|
||||||
|
BUILD SUCCESSFUL, all existing unit tests passing unmodified (a pure presentation change with no new
|
||||||
|
branching logic beyond the enums' own exhaustive `when`s), 0 lint errors.
|
||||||
|
- Live verification: installed over wireless `adb`. RobotAutomationDevelopment-Dept/OC5-PilotLine-
|
||||||
|
RectangleTypeWelder PR tab: confirmed a teal "병합됨" badge on two merged PRs and a green "열림" badge
|
||||||
|
on the third, side by side (screenshot); opened the merged PR's detail view and confirmed the same
|
||||||
|
teal badge in the header (screenshot). Repeated with `adb shell cmd uimode night yes` — both badge
|
||||||
|
colors keep good contrast against the dark surface (screenshot) — then restored `night no`.
|
||||||
|
- Review findings and resolutions: None from a separate review pass.
|
||||||
|
- Known limitations: `NotificationsScreen.kt` renders its own separate PR/이슈 상태 column as plain
|
||||||
|
text — the same visual gap, in a different screen, not touched in this pass to keep it scoped to
|
||||||
|
what was actually being reviewed. Did not visually confirm the red CLOSED badge with this fix
|
||||||
|
installed (no real closed issue/PR was opened live this session) — low risk, since the color mapping
|
||||||
|
reuses the same `errorContainer` role already used unconditionally elsewhere in the app.
|
||||||
|
- Next recommended task: continue the design-review pass — the text/image/CSV/PDF file viewers (not
|
||||||
|
yet reviewed this session), and consider extending today's status-badge fix to
|
||||||
|
`NotificationsScreen.kt` for consistency.
|
||||||
|
- Next owner: Claude, continuing per the standing direction.
|
||||||
|
|||||||
97
tasks/042-workitem-status-badges.md
Normal file
97
tasks/042-workitem-status-badges.md
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
# TASK-042: Colored status badges for issue/PR state (열림/닫힘/병합됨)
|
||||||
|
|
||||||
|
Status: implemented; all required checks pass; live-confirmed on the human product owner's device
|
||||||
|
Owner: Claude
|
||||||
|
Reviewer: Codex
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
Design-review follow-up (continuing the "앱의 아이콘부터 해서 전체적으로 확인해보고 부족한 부분 고쳐줘"
|
||||||
|
pass from TASK-039). While verifying that issue/PR body text renders correctly for real content
|
||||||
|
(it does — confirmed against a real issue with a multi-paragraph body and an embedded code block),
|
||||||
|
noticed the 열림/닫힘/병합됨 state label next to each issue/PR — in both the list row and the detail
|
||||||
|
header — rendered as a plain, uncolored `bodySmall` line, visually no more prominent than the
|
||||||
|
`#12` issue number sitting right next to it. Gitea's own web UI (and GitHub's, which established the
|
||||||
|
convention) colors this label so it reads at a glance while scanning a list. Replaced the plain text
|
||||||
|
with a compact colored badge, reusing the `VisibilityBadge` pattern TASK-039 already established for
|
||||||
|
공개/비공개 on the repository list.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Included: `feature/workitems/WorkItemsScreen.kt` — new private `StatusBadge` composable (two
|
||||||
|
overloads, one per state enum, sharing one color-agnostic rendering implementation) replacing the
|
||||||
|
four plain-`Text` state-label call sites: `IssueRow`, `PullRequestRow`, `IssueDetailView`,
|
||||||
|
`PullRequestDetailView`.
|
||||||
|
- Excluded: `NotificationsScreen.kt`'s own PR/이슈 state labels (a separate screen/composable, out of
|
||||||
|
scope for this pass — noted below as a known limitation, not silently left inconsistent).
|
||||||
|
`ReviewState` labels (승인/변경 요청/etc. on `ReviewRow`) — these are a different kind of status
|
||||||
|
with no obvious two/three-color GitHub/Gitea convention to borrow, left as plain text.
|
||||||
|
|
||||||
|
## Design notes
|
||||||
|
|
||||||
|
- **Reused existing theme roles instead of introducing new colors.** OPEN gets
|
||||||
|
`primaryContainer`/`onPrimaryContainer` — the same green `VisibilityBadge` already uses for a
|
||||||
|
public repository's affirmative state. CLOSED gets `errorContainer`/`onErrorContainer` — the
|
||||||
|
baseline Material3 red `Theme.kt` deliberately left untouched for exactly this kind of
|
||||||
|
closed/removed state. MERGED — a third state GitHub/Gitea conventionally color purple, which this
|
||||||
|
app's green/teal-accented theme has no role for — gets `tertiaryContainer`/`onTertiaryContainer`
|
||||||
|
(teal) rather than a one-off hardcoded color with no theme backing (and no dark-mode variant of its
|
||||||
|
own to get wrong).
|
||||||
|
- **One rendering composable, two small color-mapping overloads.** `IssueState` and `PullRequestState`
|
||||||
|
are different enums (`PullRequestState` has the extra MERGED case), so a single `when` couldn't
|
||||||
|
cover both — instead each enum gets its own thin `StatusBadge(state, modifier)` overload that maps
|
||||||
|
to a container/content color pair and delegates to the same private `StatusBadge(label, container,
|
||||||
|
content, modifier)` for the actual `Surface` + `Text` rendering, keeping the visual treatment
|
||||||
|
(rounded 4dp corner, `labelSmall`, 6dp/2dp padding) defined in exactly one place.
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- [x] Issue list rows show a colored badge (not plain text) for 열림/닫힘.
|
||||||
|
- [x] PR list rows show a colored badge for 열림/닫힘/병합됨.
|
||||||
|
- [x] Issue detail header shows the same colored badge as its list row.
|
||||||
|
- [x] PR detail header shows the same colored badge as its list row.
|
||||||
|
- [x] Colors are pulled from the existing Material3 theme (no new hardcoded colors), so dark mode is
|
||||||
|
handled automatically.
|
||||||
|
- [x] Build, unit tests, and lint pass.
|
||||||
|
- [x] Live-confirmed on the human product owner's device: PR list showing 병합됨 (teal) ×2 and 열림
|
||||||
|
(green) ×1 side by side; PR detail header showing the same teal 병합됨 badge; both re-confirmed
|
||||||
|
unchanged under forced dark mode.
|
||||||
|
|
||||||
|
## Required checks
|
||||||
|
|
||||||
|
```text
|
||||||
|
./gradlew compileDebugKotlin compileDebugUnitTestKotlin testDebugUnitTest assembleDebug lintDebug
|
||||||
|
```
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- `feature/workitems/WorkItemsScreen.kt`: `StatusBadge` composable (two state-typed overloads +
|
||||||
|
one shared rendering implementation) added; `IssueRow`, `PullRequestRow`, `IssueDetailView`,
|
||||||
|
`PullRequestDetailView` each call it in place of a plain `Text(stringResource(state.labelResource))`.
|
||||||
|
- `./gradlew compileDebugKotlin compileDebugUnitTestKotlin testDebugUnitTest assembleDebug lintDebug`:
|
||||||
|
BUILD SUCCESSFUL, all unit tests passing (no test changes needed — this is a pure presentation
|
||||||
|
change with no new branching logic to cover beyond what the enum `when`s already exhaustively
|
||||||
|
check at compile time), 0 lint errors.
|
||||||
|
- Live device: installed over wireless `adb`. Notifications → PostActivated 관련 추가 필요 이슈 detail:
|
||||||
|
confirmed the issue body (multi-paragraph Korean text plus an indented C# code block) renders
|
||||||
|
correctly end to end — this was the original investigation trigger, and it surfaced no bug on its
|
||||||
|
own. RobotAutomationDevelopment-Dept/OC5-PilotLine-RectangleTypeWelder PR tab: #6/#5 both show a
|
||||||
|
teal "병합됨" badge, #4 shows a green "열림" badge (screenshot); opened #6's detail view and confirmed
|
||||||
|
the same teal badge appears in the header (screenshot). Forced dark mode
|
||||||
|
(`adb shell cmd uimode night yes`) and reconfirmed the same PR list — both badge colors keep good
|
||||||
|
contrast against the dark surface (screenshot) — then restored `night no`.
|
||||||
|
|
||||||
|
## Known limitations
|
||||||
|
|
||||||
|
- `NotificationsScreen.kt` still renders its own PR/이슈 상태 column as plain text — a second,
|
||||||
|
separate location with the same visual gap this task fixed on the work-items screens. Not touched
|
||||||
|
here to keep this change scoped to what was actually being reviewed; worth a follow-up pass.
|
||||||
|
- Did not find a real CLOSED (닫힘) issue/PR with this fix installed to visually confirm the red badge
|
||||||
|
live — the CLOSED branch was exercised only via the existing 닫힘 rows seen in `NotificationsScreen`
|
||||||
|
(a different, unfixed screen) prior to this change. The color mapping itself
|
||||||
|
(`errorContainer`/`onErrorContainer`) is the same theme role used unconditionally elsewhere in the
|
||||||
|
app, so this is a low-risk gap, not left silently assumed away.
|
||||||
|
|
||||||
|
## Review findings
|
||||||
|
|
||||||
|
None yet — pending Codex/human review.
|
||||||
Loading…
x
Reference in New Issue
Block a user