diff --git a/src/renderer/stores/authorTeamStore.ts b/src/renderer/stores/authorTeamStore.ts new file mode 100644 index 00000000..6632a97e --- /dev/null +++ b/src/renderer/stores/authorTeamStore.ts @@ -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>) => 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((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 }; + }); + }, +})); \ No newline at end of file diff --git a/src/renderer/views/PRListView.tsx b/src/renderer/views/PRListView.tsx index 965f1166..635e630c 100644 --- a/src/renderer/views/PRListView.tsx +++ b/src/renderer/views/PRListView.tsx @@ -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 @@ -50,6 +51,7 @@ export default function PRListView() { setPRListFilters, } = useUIStore(); const { token } = useAuthStore(); + const { teams } = useAuthorTeamStore(); const [showAuthorDropdown, setShowAuthorDropdown] = useState(false); const authorDropdownRef = useRef(null); const [showStatusDropdown, setShowStatusDropdown] = useState(false); @@ -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 @@ -851,6 +873,39 @@ export default function PRListView() { theme === "dark" ? "border-gray-700" : "border-gray-200" )} /> + {/* Individual authors */} + {/* Teams section */} + {teams.length > 0 && ( + <> +
+
Teams
+ {teams.map(team => { + const allSelected = team.members.every(m => selectedAuthors.has(m)); + return ( + + ); + })} + + )} {/* Individual authors */} {authors.map(author => (