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
30 changes: 20 additions & 10 deletions src/app/components/dashboard/ActionsTab.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { createMemo, For, Show } from "solid-js";
import { createEffect, createMemo, For, Show } from "solid-js";
import { createStore } from "solid-js/store";
import type { WorkflowRun } from "../../services/api";
import { config } from "../../stores/config";
import { viewState, setViewState, setTabFilter, resetTabFilter, resetAllTabFilters, ignoreItem, unignoreItem, type ActionsFilterField } from "../../stores/view";
import { viewState, setViewState, setTabFilter, resetTabFilter, resetAllTabFilters, ignoreItem, unignoreItem, toggleExpandedRepo, setAllExpanded, pruneExpandedRepos, type ActionsFilterField } from "../../stores/view";
import WorkflowSummaryCard from "./WorkflowSummaryCard";
import IgnoreBadge from "./IgnoreBadge";
import SkeletonRows from "../shared/SkeletonRows";
import FilterChips from "../shared/FilterChips";
import type { FilterChipGroupDef } from "../shared/FilterChips";
import ChevronIcon from "../shared/ChevronIcon";
import ExpandCollapseButtons from "../shared/ExpandCollapseButtons";

interface ActionsTabProps {
workflowRuns: WorkflowRun[];
Expand Down Expand Up @@ -116,17 +117,22 @@ const actionsFilterGroups: FilterChipGroupDef[] = [
];

export default function ActionsTab(props: ActionsTabProps) {
const [expandedRepos, setExpandedRepos] = createStore<Record<string, boolean>>({});
const [expandedWorkflows, setExpandedWorkflows] = createStore<Record<string, boolean>>({});

function toggleRepo(repoFullName: string) {
setExpandedRepos(repoFullName, (v) => !v);
}

function toggleWorkflow(key: string) {
setExpandedWorkflows(key, (v) => !v);
}

const activeRepoNames = createMemo(() =>
[...new Set(props.workflowRuns.map((r) => r.repoFullName))]
);

createEffect(() => {
const names = activeRepoNames();
if (names.length === 0) return;
pruneExpandedRepos("actions", names);
});

function handleIgnore(run: WorkflowRun) {
ignoreItem({
id: String(run.id),
Expand Down Expand Up @@ -180,7 +186,7 @@ export default function ActionsTab(props: ActionsTabProps) {
return (
<div class="divide-y divide-base-300">
{/* Toolbar */}
<div class="flex flex-wrap items-center gap-3 px-4 py-2 bg-base-100">
<div class="flex flex-wrap items-center gap-3 px-4 py-2 border-b border-base-300 bg-base-100">
<label class="flex items-center gap-1.5 text-sm text-base-content/70 cursor-pointer select-none">
<input
type="checkbox"
Expand All @@ -198,6 +204,10 @@ export default function ActionsTab(props: ActionsTabProps) {
onResetAll={() => resetAllTabFilters("actions")}
/>
<div class="flex-1" />
<ExpandCollapseButtons
onExpandAll={() => setAllExpanded("actions", repoGroups().map((g) => g.repoFullName), true)}
onCollapseAll={() => setAllExpanded("actions", repoGroups().map((g) => g.repoFullName), false)}
/>
<IgnoreBadge
items={viewState.ignoredItems.filter((i) => i.type === "workflowRun")}
onUnignore={unignoreItem}
Expand All @@ -224,7 +234,7 @@ export default function ActionsTab(props: ActionsTabProps) {
<Show when={repoGroups().length > 0}>
<For each={repoGroups()}>
{(repoGroup) => {
const isExpanded = () => !!expandedRepos[repoGroup.repoFullName];
const isExpanded = () => !!viewState.expandedRepos.actions[repoGroup.repoFullName];

const sortedWorkflows = createMemo(() =>
sortWorkflowsByStatus(repoGroup.workflows)
Expand All @@ -249,7 +259,7 @@ export default function ActionsTab(props: ActionsTabProps) {
<div class="bg-base-100">
{/* Repo header */}
<button
onClick={() => toggleRepo(repoGroup.repoFullName)}
onClick={() => toggleExpandedRepo("actions", repoGroup.repoFullName)}
aria-expanded={isExpanded()}
class="w-full flex items-center gap-2 px-4 py-2.5 text-left text-sm font-semibold text-base-content bg-base-200/60 border-y border-base-300 hover:bg-base-200 transition-colors"
>
Expand Down
31 changes: 15 additions & 16 deletions src/app/components/dashboard/IssuesTab.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { createEffect, createMemo, createSignal, For, Show } from "solid-js";
import { createStore } from "solid-js/store";
import { config } from "../../stores/config";
import { viewState, setSortPreference, setTabFilter, resetTabFilter, resetAllTabFilters, ignoreItem, unignoreItem, type IssueFilterField } from "../../stores/view";
import { viewState, setSortPreference, setTabFilter, resetTabFilter, resetAllTabFilters, ignoreItem, unignoreItem, toggleExpandedRepo, setAllExpanded, pruneExpandedRepos, type IssueFilterField } from "../../stores/view";
import type { Issue } from "../../services/api";
import ItemRow from "./ItemRow";
import IgnoreBadge from "./IgnoreBadge";
Expand All @@ -13,6 +12,7 @@ import type { FilterChipGroupDef } from "../shared/FilterChips";
import RoleBadge from "../shared/RoleBadge";
import SkeletonRows from "../shared/SkeletonRows";
import ChevronIcon from "../shared/ChevronIcon";
import ExpandCollapseButtons from "../shared/ExpandCollapseButtons";
import { deriveInvolvementRoles } from "../../lib/format";
import { groupByRepo, computePageLayout, slicePageGroups } from "../../lib/grouping";

Expand Down Expand Up @@ -54,11 +54,6 @@ const sortOptions: SortOption[] = [

export default function IssuesTab(props: IssuesTabProps) {
const [page, setPage] = createSignal(0);
const [expandedRepos, setExpandedRepos] = createStore<Record<string, boolean>>({});

function toggleRepo(repoFullName: string) {
setExpandedRepos(repoFullName, (v) => !v);
}

const sortPref = createMemo(() => {
const pref = viewState.sortPreferences["issues"];
Expand Down Expand Up @@ -141,14 +136,14 @@ export default function IssuesTab(props: IssuesTabProps) {
if (page() > max) setPage(max);
});

// Auto-expand first group on initial mount
let hasAutoExpanded = false;
const activeRepoNames = createMemo(() =>
[...new Set(props.issues.map((i) => i.repoFullName))]
);

createEffect(() => {
const groups = pageGroups();
if (!hasAutoExpanded && groups.length > 0) {
hasAutoExpanded = true;
setExpandedRepos(groups[0].repoFullName, true);
}
const names = activeRepoNames();
if (names.length === 0) return;
pruneExpandedRepos("issues", names);
});

function handleSort(field: string, direction: "asc" | "desc") {
Expand Down Expand Up @@ -193,6 +188,10 @@ export default function IssuesTab(props: IssuesTabProps) {
}}
/>
<div class="flex-1" />
<ExpandCollapseButtons
onExpandAll={() => setAllExpanded("issues", repoGroups().map((g) => g.repoFullName), true)}
onCollapseAll={() => setAllExpanded("issues", repoGroups().map((g) => g.repoFullName), false)}
/>
<IgnoreBadge
items={viewState.ignoredItems.filter((i) => i.type === "issue")}
onUnignore={unignoreItem}
Expand Down Expand Up @@ -234,7 +233,7 @@ export default function IssuesTab(props: IssuesTabProps) {
<div class="divide-y divide-base-300">
<For each={pageGroups()}>
{(repoGroup) => {
const isExpanded = () => !!expandedRepos[repoGroup.repoFullName];
const isExpanded = () => !!viewState.expandedRepos.issues[repoGroup.repoFullName];

const roleSummary = createMemo(() => {
const counts: Record<string, number> = {};
Expand All @@ -252,7 +251,7 @@ export default function IssuesTab(props: IssuesTabProps) {
return (
<div class="bg-base-100">
<button
onClick={() => toggleRepo(repoGroup.repoFullName)}
onClick={() => toggleExpandedRepo("issues", repoGroup.repoFullName)}
aria-expanded={isExpanded()}
class="w-full flex items-center gap-2 px-4 py-2.5 text-left text-sm font-semibold text-base-content bg-base-200/60 border-y border-base-300 hover:bg-base-200 transition-colors"
>
Expand Down
46 changes: 21 additions & 25 deletions src/app/components/dashboard/ItemRow.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { For, JSX, Show } from "solid-js";
import { openGitHubUrl } from "../../lib/url";
import { isSafeGitHubUrl } from "../../lib/url";
import { relativeTime, labelTextColor, formatCount } from "../../lib/format";

export interface ItemRowProps {
Expand All @@ -19,29 +19,28 @@ export interface ItemRowProps {

export default function ItemRow(props: ItemRowProps) {
const isCompact = () => props.density === "compact";

function handleRowClick(e: MouseEvent) {
// Only open if click was not on the ignore button
if ((e.target as HTMLElement).closest("[data-ignore-btn]")) return;
openGitHubUrl(props.url);
}
const safeUrl = () => isSafeGitHubUrl(props.url) ? props.url : undefined;

return (
<div
role="row"
tabIndex={0}
onClick={handleRowClick}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
openGitHubUrl(props.url);
}
}}
class={`group relative flex items-start gap-3 cursor-pointer
class={`group relative flex items-start gap-3
hover:bg-base-200
transition-colors focus:outline-none focus:bg-base-200 focus-visible:ring-2 focus-visible:ring-primary
transition-colors
${isCompact() ? "px-4 py-2" : "px-4 py-3"}`}
>
{/* Overlay link — covers entire row; interactive children use relative z-10 */}
<Show when={safeUrl()}>
{(url) => (
<a
href={url()}
target="_blank"
rel="noopener noreferrer"
class="absolute inset-0 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-inset rounded"
aria-label={`${props.repo} #${props.number}: ${props.title}`}
/>
)}
</Show>

{/* Repo badge */}
<Show when={!props.hideRepo}>
<span
Expand Down Expand Up @@ -86,9 +85,9 @@ export default function ItemRow(props: ItemRowProps) {
</div>
</Show>

{/* Additional children slot */}
{/* Additional children slot — z-10 to sit above stretched link */}
<Show when={props.children !== undefined}>
<div class={isCompact() ? "mt-0.5" : "mt-1"}>{props.children}</div>
<div class={`relative z-10 ${isCompact() ? "mt-0.5" : "mt-1"}`}>{props.children}</div>
</Show>
</div>

Expand Down Expand Up @@ -119,11 +118,8 @@ export default function ItemRow(props: ItemRowProps) {
{/* Ignore button — visible on hover */}
<button
data-ignore-btn
onClick={(e) => {
e.stopPropagation();
props.onIgnore();
}}
class={`shrink-0 self-center rounded p-1
onClick={() => props.onIgnore()}
class={`relative z-10 shrink-0 self-center rounded p-1
text-base-content/30
hover:text-error
opacity-0 group-hover:opacity-100 focus:opacity-100
Expand Down
31 changes: 15 additions & 16 deletions src/app/components/dashboard/PullRequestsTab.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { createEffect, createMemo, createSignal, For, Show } from "solid-js";
import { createStore } from "solid-js/store";
import { config } from "../../stores/config";
import { viewState, setSortPreference, ignoreItem, unignoreItem, setTabFilter, resetTabFilter, resetAllTabFilters, type PullRequestFilterField } from "../../stores/view";
import { viewState, setSortPreference, ignoreItem, unignoreItem, setTabFilter, resetTabFilter, resetAllTabFilters, toggleExpandedRepo, setAllExpanded, pruneExpandedRepos, type PullRequestFilterField } from "../../stores/view";
import type { PullRequest } from "../../services/api";
import { deriveInvolvementRoles, prSizeCategory } from "../../lib/format";
import ExpandCollapseButtons from "../shared/ExpandCollapseButtons";
import ItemRow from "./ItemRow";
import StatusDot from "../shared/StatusDot";
import IgnoreBadge from "./IgnoreBadge";
Expand Down Expand Up @@ -119,11 +119,6 @@ const sortOptions: SortOption[] = [

export default function PullRequestsTab(props: PullRequestsTabProps) {
const [page, setPage] = createSignal(0);
const [expandedRepos, setExpandedRepos] = createStore<Record<string, boolean>>({});

function toggleRepo(repoFullName: string) {
setExpandedRepos(repoFullName, (v) => !v);
}

const sortPref = createMemo(() => {
const pref = viewState.sortPreferences["pullRequests"];
Expand Down Expand Up @@ -230,14 +225,14 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {
if (page() > max) setPage(max);
});

// Auto-expand first group on initial load
let hasAutoExpanded = false;
const activeRepoNames = createMemo(() =>
[...new Set(props.pullRequests.map((pr) => pr.repoFullName))]
);

createEffect(() => {
const groups = pageGroups();
if (!hasAutoExpanded && groups.length > 0) {
hasAutoExpanded = true;
setExpandedRepos(groups[0].repoFullName, true);
}
const names = activeRepoNames();
if (names.length === 0) return;
pruneExpandedRepos("pullRequests", names);
});

function handleSort(field: string, direction: "asc" | "desc") {
Expand Down Expand Up @@ -282,6 +277,10 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {
}}
/>
<div class="flex-1" />
<ExpandCollapseButtons
onExpandAll={() => setAllExpanded("pullRequests", repoGroups().map((g) => g.repoFullName), true)}
onCollapseAll={() => setAllExpanded("pullRequests", repoGroups().map((g) => g.repoFullName), false)}
/>
<IgnoreBadge
items={viewState.ignoredItems.filter((i) => i.type === "pullRequest")}
onUnignore={unignoreItem}
Expand Down Expand Up @@ -323,7 +322,7 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {
<div class="divide-y divide-base-300">
<For each={pageGroups()}>
{(repoGroup) => {
const isExpanded = () => !!expandedRepos[repoGroup.repoFullName];
const isExpanded = () => !!viewState.expandedRepos.pullRequests[repoGroup.repoFullName];

const summaryMeta = createMemo(() => {
const checks = { success: 0, failure: 0, pending: 0, conflict: 0 };
Expand Down Expand Up @@ -354,7 +353,7 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {
return (
<div class="bg-base-100">
<button
onClick={() => toggleRepo(repoGroup.repoFullName)}
onClick={() => toggleExpandedRepo("pullRequests", repoGroup.repoFullName)}
aria-expanded={isExpanded()}
class="w-full flex items-center gap-2 px-4 py-2.5 text-left text-sm font-semibold text-base-content bg-base-200/60 border-y border-base-300 hover:bg-base-200 transition-colors"
>
Expand Down
53 changes: 53 additions & 0 deletions src/app/components/shared/ExpandCollapseButtons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
export interface ExpandCollapseButtonsProps {
onExpandAll: () => void;
onCollapseAll: () => void;
}

export default function ExpandCollapseButtons(props: ExpandCollapseButtonsProps) {
return (
<div class="flex items-center gap-1">
<button
class="btn btn-ghost btn-xs"
title="Expand all"
aria-label="Expand all"
onClick={props.onExpandAll}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={1.5}
stroke="currentColor"
class="h-4 w-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m4.5 5.25 7.5 7.5 7.5-7.5m-15 6 7.5 7.5 7.5-7.5"
/>
</svg>
</button>
<button
class="btn btn-ghost btn-xs"
title="Collapse all"
aria-label="Collapse all"
onClick={props.onCollapseAll}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={1.5}
stroke="currentColor"
class="h-4 w-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m4.5 18.75 7.5-7.5 7.5 7.5m-15-6 7.5-7.5 7.5 7.5"
/>
</svg>
</button>
</div>
);
}
Loading
Loading