Skip to content
Draft
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
72 changes: 72 additions & 0 deletions src/renderer/stores/authorTeamStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { create } from "zustand";
import { nanoid } from "nanoid";

export interface AuthorTeam {
id: string;
name: string;
members: string[]; // GitHub logins
createdAt: string;
}

interface AuthorTeamState {
teams: AuthorTeam[];
addTeam: (name: string, members: string[]) => void;
updateTeam: (id: string, data: Partial<Omit<AuthorTeam, "id" | "createdAt">>) => void;
deleteTeam: (id: string) => void;
}

const STORAGE_KEY = "authorTeams";

function loadTeams(): AuthorTeam[] {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) return parsed as AuthorTeam[];
}
} catch (err) {
console.error("Failed to load author teams:", err);
}
return [];
}

function saveTeams(teams: AuthorTeam[]) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(teams));
} catch (err) {
console.error("Failed to save author teams:", err);
}
}

export const useAuthorTeamStore = create<AuthorTeamState>((set, get) => ({
teams: loadTeams(),
addTeam: (name, members) => {
const newTeam: AuthorTeam = {
id: nanoid(),
name,
members: Array.from(new Set(members)),
createdAt: new Date().toISOString(),
};
set((state) => {
const updated = [...state.teams, newTeam];
saveTeams(updated);
return { teams: updated };
});
},
updateTeam: (id, data) => {
set((state) => {
const updated = state.teams.map((team) =>
team.id === id ? { ...team, ...data } : team,
);
saveTeams(updated);
return { teams: updated };
});
},
deleteTeam: (id) => {
set((state) => {
const updated = state.teams.filter((team) => team.id !== id);
saveTeams(updated);
return { teams: updated };
});
},
}));
55 changes: 55 additions & 0 deletions src/renderer/views/PRListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import WelcomeView from "./WelcomeView";
import { GitHubAPI, PullRequest } from "../services/github";
import { PRTreeView } from "../components/PRTreeView";
import type { SortByType, PRWithMetadata } from "../types/prList";
import { useAuthorTeamStore } from "../stores/authorTeamStore";

type StatusType = PRStatusType; // Use the centralized type

Expand Down Expand Up @@ -50,6 +51,7 @@ export default function PRListView() {
setPRListFilters,
} = useUIStore();
const { token } = useAuthStore();
const { teams } = useAuthorTeamStore();
const [showAuthorDropdown, setShowAuthorDropdown] = useState(false);
const authorDropdownRef = useRef<HTMLDivElement>(null);
const [showStatusDropdown, setShowStatusDropdown] = useState(false);
Expand Down Expand Up @@ -226,6 +228,26 @@ export default function PRListView() {
[authors, setPRListFilters],
);

const handleTeamToggle = useCallback(
(teamMembers: string[]) => {
setPRListFilters(prev => {
const newSet = new Set(prev.selectedAuthors);
const allSelected = teamMembers.every(m => newSet.has(m));
if (allSelected) {
teamMembers.forEach(m => newSet.delete(m));
newSet.delete("all");
} else {
teamMembers.forEach(m => newSet.add(m));
if (authors.every(a => newSet.has(a.login))) {
newSet.add("all");
}
}
return { ...prev, selectedAuthors: Array.from(newSet) };
});
},
[authors, setPRListFilters],
);

// Use the centralized getPRStatus utility
// No need for useCallback since getPRStatus is a pure function

Expand Down Expand Up @@ -851,6 +873,39 @@ export default function PRListView() {
theme === "dark" ? "border-gray-700" : "border-gray-200"
)} />

{/* Individual authors */}
{/* Teams section */}
{teams.length > 0 && (
<>
<div className={cn(
"my-1 border-t",
theme === "dark" ? "border-gray-700" : "border-gray-200",
)} />
<div className="px-2 py-1 text-xs font-semibold opacity-70">Teams</div>
{teams.map(team => {
const allSelected = team.members.every(m => selectedAuthors.has(m));
return (
<label
key={team.id}
className={cn(
"flex items-center space-x-2 p-2 rounded cursor-pointer",
theme === "dark" ? "hover:bg-gray-700" : "hover:bg-gray-50",
)}
>
<input
type="checkbox"
checked={allSelected}
onChange={() => handleTeamToggle(team.members)}
className="rounded"
/>
<span className="text-sm font-medium truncate">
{team.name} ({team.members.length})
</span>
</label>
);
})}
</>
)}
{/* Individual authors */}
{authors.map(author => (
<label
Expand Down