diff --git a/src/app/components/dashboard/ActionsTab.tsx b/src/app/components/dashboard/ActionsTab.tsx index 99925d42..19784de8 100644 --- a/src/app/components/dashboard/ActionsTab.tsx +++ b/src/app/components/dashboard/ActionsTab.tsx @@ -291,7 +291,7 @@ export default function ActionsTab(props: ActionsTabProps) { }); return ( -
+
{/* Repo header */}
+
+ + + + 0} + fallback={ +

+ {props.q + ? "No repos match your filter." + : "No repositories found."} +

+ } + > +
+
    + + {(repo) => ( +
  • +
    + + + + + + +
    +
  • + )} +
    +
+
+
+
+ + ); +} + export default function RepoSelector(props: RepoSelectorProps) { const [filter, setFilter] = createSignal(""); const [orgStates, setOrgStates] = createSignal([]); @@ -407,6 +518,22 @@ export default function RepoSelector(props: RepoSelectorProps) { ); }); + // ── Accordion state ─────────────────────────────────────────────────────── + + const isAccordion = createMemo(() => props.selectedOrgs.length >= 6); + // Stable default: props.selectedOrgs[0] avoids the mid-load shift that + // occurs when sortedOrgStates switches from insertion-order to alphabetical + const [expandedOrg, setExpandedOrg] = createSignal( + props.selectedOrgs[0] ?? "" + ); + const safeExpandedOrg = createMemo(() => { + const states = sortedOrgStates(); + const current = expandedOrg(); + if (states.some(s => s.org === current)) return current; + const stateOrgs = new Set(states.map(s => s.org)); + return props.selectedOrgs.find(o => stateOrgs.has(o)) ?? (states.length > 0 ? states[0].org : ""); + }); + // ── Status ──────────────────────────────────────────────────────────────── const totalOrgs = () => props.selectedOrgs.length; @@ -450,149 +577,130 @@ export default function RepoSelector(props: RepoSelectorProps) { {/* Per-org repo lists — Index (not For) avoids tearing down every org's DOM subtree when a single org's state updates via setOrgStates(prev.map(...)) */} - - {(state) => { - const visible = createMemo(() => filteredReposForOrg(state())); - - return ( -
- {/* Org header */} -
- - {state().org} - - -
- - · - + + {(state) => { + const visible = createMemo(() => filteredReposForOrg(state())); + return ( +
+
+ + {state().org} + + +
+ + · + +
+
- -
- - {/* Loading state for this org */} - -
- -
-
- - {/* Error state for this org */} - -
- - {state().error} - - +
-
- - {/* Repo list */} - - 0} - fallback={ -

- {q() - ? "No repos match your filter." - : "No repositories found."} -

- } - > -
-
    - - {(repo) => { - return ( -
  • -
    - - - - - - -
    -
  • - ); - }} -
    -
+ ); + }} + + } + > + { + if (vals.length > 0) setExpandedOrg(vals[0]); + }} + > + + {(state) => { + const visible = createMemo(() => filteredReposForOrg(state())); + // Count against ALL repos in the org (unfiltered) so the badge + // doesn't mislead users into thinking selections were lost when + // a text filter is active. + const selectedCount = createMemo(() => + state().repos.filter((r) => isSelected(r.fullName)).length + ); + const isExpanded = () => safeExpandedOrg() === state().org; + return ( + +
+ + + + + {state().org} + + } + > + {visible().length} {visible().length === 1 ? "repo" : "repos"} + 0}> + {selectedCount()} selected + + + + + +
+ + · + +
+
- - -
- ); - }} - + + + + + ); + }} + + +
{/* Upstream Repositories section */} diff --git a/src/app/components/shared/RepoLockControls.tsx b/src/app/components/shared/RepoLockControls.tsx index 521414f9..aea82998 100644 --- a/src/app/components/shared/RepoLockControls.tsx +++ b/src/app/components/shared/RepoLockControls.tsx @@ -1,6 +1,7 @@ import { Show, createMemo } from "solid-js"; import { viewState, lockRepo, unlockRepo, moveLockedRepo, type LockedReposTab } from "../../stores/view"; import { Tooltip } from "./Tooltip"; +import { withFlipAnimation } from "../../lib/scroll"; interface RepoLockControlsProps { tab: LockedReposTab; @@ -26,7 +27,7 @@ export default function RepoLockControls(props: RepoLockControlsProps) {