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
39 changes: 39 additions & 0 deletions docs/frontend-ui-audit-2026-08-06/GitHubIssueAssigneeEditing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Frontend UI Audit — GitHub Issue Assignee Editing

**Files:** `src/engines/ChatPanel/panels/GitHubIssuePanelView.tsx`, `src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/content/SourceControlMainContent/index.tsx`, `src/modules/WorkStation/CodeEditor/Panels/EditorMainPane/content/SourceControlMainPane.tsx`, `src/modules/WorkStation/TabContent/renderers/githubIssueDetail.tsx`
**Date:** 2026-08-06
**Auditor:** Codex PR audit

## D1 — Raw HTML vs Design System

| Line | Element | Verdict | Reason | Suggested change |
| ---- | ----------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- |
| — | Assignee control wiring | keep with reason | The changed hosts add no new raw interactive markup; each passes the same `assigneeConfig` into the existing shared issue-detail/property components. | — |

## D2 — Arbitrary Tailwind Value vs Token

| Line | Value | Verdict | Reason | Suggested change |
| ---- | ----- | ---------------- | ---------------------------------------------------------------------------------- | ---------------- |
| — | — | keep with reason | No Tailwind class or inline visual token is added by the changed production hosts. | — |

## D3 — Hardcoded Sizes / Colors

| Line | Value | Verdict | Reason | Suggested change |
| ---- | ----- | ---------------- | --------------------------------------- | ---------------- |
| — | — | keep with reason | No size or color literal is introduced. | — |

## D4 — Accessibility

| Line | Element | Verdict | Reason | Suggested change |
| ---- | ------------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------- |
| — | Shared assignee selector | keep with reason | Permission and mutation state flow through the shared selector's native disabled state; all four hosts expose identical behavior without a parallel control. | — |

## D5 — Visual Patterns Observed

- The issue-detail hosts reuse one `WorkItemExternalAssigneeConfig` boundary; no duplicated visual implementation was added.

## Summary

- 0 fixes recommended
- 4 kept with documented reason
- 0 abstract candidates
4 changes: 3 additions & 1 deletion src/engines/ChatPanel/panels/GitHubIssuePanelView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export function GitHubIssuePanelView({
}: {
detail: GitHubIssueDetailTabData;
}): React.ReactNode {
const { selectedState, interaction } = useGitHubIssueDetailState(detail);
const { selectedState, interaction, assigneeConfig } =
useGitHubIssueDetailState(detail);

if (!selectedState.issue) {
return (
Expand All @@ -35,6 +36,7 @@ export function GitHubIssuePanelView({
timeline={selectedState.timeline}
timelineLoading={selectedState.timelineLoading}
interaction={interaction}
assigneeConfig={assigneeConfig}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { useCallback, useRef, useState } from "react";

import type { GitHubIssue, GitHubIssueUser } from "@src/api/tauri/github";
import Message from "@src/components/Message";
import {
issueHasAssigneeLogins,
resolveGitHubAssigneeUsers,
} from "@src/modules/shared/githubIssueAssignees";
import {
updateCachedClosedIssues,
updateCachedOpenIssues,
Expand Down Expand Up @@ -59,31 +63,14 @@ export function resolveIssueAssigneeUsers(
assignableUsers: GitHubIssueUser[],
assigneeLogins: string[]
): GitHubIssueUser[] {
const usersByLogin = new Map(
[...item.rawIssue.assignees, ...assignableUsers].map((user) => [
user.login.toLowerCase(),
user,
])
);
return assigneeLogins.map(
(login) =>
usersByLogin.get(login.toLowerCase()) ?? { login, avatar_url: "" }
return resolveGitHubAssigneeUsers(
item.rawIssue.assignees,
assignableUsers,
assigneeLogins
);
}

export function issueHasAssigneeLogins(
issue: GitHubIssue,
assigneeLogins: string[]
): boolean {
const actual = issue.assignees
.map((assignee) => assignee.login.toLowerCase())
.sort();
const expected = assigneeLogins.map((login) => login.toLowerCase()).sort();
return (
actual.length === expected.length &&
actual.every((login, index) => login === expected[index])
);
}
export { issueHasAssigneeLogins };

export function useGitHubIssueAssigneeMutations({
repoSources,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,15 @@ const SourceControlMainContent: React.FC<SourceControlMainContentProps> = ({
emptyFocusActions,
}) => {
const scopeKey = workstationRepoScopeKey(repoId, repoPath);
const { selectedState: selectedIssueState, interaction } =
useGitHubIssueDetailState({
repoPath: repoPath ?? "",
repoId,
stateScopeKey: scopeKey,
});
const {
selectedState: selectedIssueState,
interaction,
assigneeConfig,
} = useGitHubIssueDetailState({
repoPath: repoPath ?? "",
repoId,
stateScopeKey: scopeKey,
});

// `historySelection` keeps a stable reference across renders (it comes from
// the persisted tab payload), so memoizing on it directly gives a stable
Expand Down Expand Up @@ -144,6 +147,7 @@ const SourceControlMainContent: React.FC<SourceControlMainContentProps> = ({
timeline={selectedIssueState.timeline}
timelineLoading={selectedIssueState.timelineLoading}
interaction={interaction}
assigneeConfig={assigneeConfig}
/>
</Suspense>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,15 @@ const SourceControlMainPane: React.FC<SourceControlMainPaneProps> = ({
onGitDiffUnsavedChange,
}) => {
const scopeKey = workstationRepoScopeKey(repoId, repoPath);
const { selectedState: selectedIssueState, interaction } =
useGitHubIssueDetailState({
repoPath,
repoId: repoId ?? undefined,
stateScopeKey: scopeKey,
});
const {
selectedState: selectedIssueState,
interaction,
assigneeConfig,
} = useGitHubIssueDetailState({
repoPath,
repoId: repoId ?? undefined,
stateScopeKey: scopeKey,
});

const { mode, staged, focusPath, historySelection, allFiles, focusGitFile } =
deriveSourceControlMainProps({
Expand Down Expand Up @@ -99,6 +102,7 @@ const SourceControlMainPane: React.FC<SourceControlMainPaneProps> = ({
timeline={selectedIssueState.timeline}
timelineLoading={selectedIssueState.timelineLoading}
interaction={interaction}
assigneeConfig={assigneeConfig}
showHeader={false}
/>
</Suspense>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import type { UnifiedTabContentProps } from "../types";
const GitHubIssueDetailTabRenderer: React.FC<UnifiedTabContentProps> = memo(
({ tab }) => {
const tabData = tab.data as unknown as GitHubIssueDetailTabData;
const { selectedState, interaction } = useGitHubIssueDetailState(tabData);
const { selectedState, interaction, assigneeConfig } =
useGitHubIssueDetailState(tabData);

const headerContent = useMemo(
() => (
Expand Down Expand Up @@ -72,6 +73,7 @@ const GitHubIssueDetailTabRenderer: React.FC<UnifiedTabContentProps> = memo(
timeline={selectedState.timeline}
timelineLoading={selectedState.timelineLoading}
interaction={interaction}
assigneeConfig={assigneeConfig}
showHeader={false}
/>
);
Expand Down
34 changes: 34 additions & 0 deletions src/modules/shared/githubIssueAssignees.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { GitHubIssue, GitHubIssueUser } from "@src/api/tauri/github";

export function resolveGitHubAssigneeUsers(
currentAssignees: GitHubIssueUser[],
assignableUsers: GitHubIssueUser[],
assigneeLogins: string[]
): GitHubIssueUser[] {
const usersByLogin = new Map(
[...currentAssignees, ...assignableUsers].map((user) => [
user.login.toLowerCase(),
user,
])
);

return assigneeLogins.map(
(login) =>
usersByLogin.get(login.toLowerCase()) ?? { login, avatar_url: "" }
);
}

export function issueHasAssigneeLogins(
issue: GitHubIssue,
assigneeLogins: string[]
): boolean {
const actual = issue.assignees
.map((assignee) => assignee.login.toLowerCase())
.sort();
const expected = assigneeLogins.map((login) => login.toLowerCase()).sort();

return (
actual.length === expected.length &&
actual.every((login, index) => login === expected[index])
);
}
Loading
Loading