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
36 changes: 33 additions & 3 deletions apps/mobile/src/features/pull-requests/PullRequestListFilters.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { sortProjectsByGroupOrder } from "@t3tools/client-runtime/state/project-grouping";
import { useMemo } from "react";
import { ScrollView } from "react-native";
import type { ControlPillMenuProps } from "../../components/ControlPillMenu.types";
import { ListFilterMenu } from "../workspace/ListFilterMenu";
import { useProjects, useThreadShells } from "../../state/entities";
import { useMobileProjectGroupingSettings } from "../../state/project-grouping";
import { buildHomeProjectScopes, sortHomeProjectScopes } from "../home/homeThreadList";
import { DEFAULT_PR_LIST_CONTROLS, type PullRequestListControls } from "./pullRequestList";

const STATES = { open: "Open", closed: "Closed", merged: "Merged", all: "All states" } as const;
Expand Down Expand Up @@ -28,6 +33,30 @@ function choices(field: string, options: Readonly<Record<string, string>>, selec
}));
}

/** The project list's own order, which is the order its sidebar and new-task picker show. */
function useProjectsInSidebarOrder<Project extends { id: string; environmentId: string }>(
projects: readonly Project[],
) {
const allProjects = useProjects();
const threads = useThreadShells();
const { sidebarProjectGroupingMode } = useMobileProjectGroupingSettings();
const scopes = useMemo(
() =>
sortHomeProjectScopes({
scopes: buildHomeProjectScopes({
projects: allProjects,
environmentId: null,
projectGroupingMode: sidebarProjectGroupingMode,
}),
threads,
pendingTasks: [],
projectSortOrder: "updated_at",
}).map((scope) => ({ memberProjectRefs: scope.projectRefs })),
[allProjects, sidebarProjectGroupingMode, threads],
);
return useMemo(() => sortProjectsByGroupOrder(projects, scopes), [projects, scopes]);
}

export function PullRequestListFilters(props: {
readonly environments: readonly { id: string; label: string }[];
readonly environmentId: string | null;
Expand All @@ -39,6 +68,7 @@ export function PullRequestListFilters(props: {
readonly projects: readonly { id: string; title: string; environmentId: string }[];
}) {
const { value } = props;
const projects = useProjectsInSidebarOrder(props.projects);
const selectedLabels = value.filters.labels?.flat() ?? [];
const selectedLabelSet = new Set(selectedLabels);
const author = value.filters.author;
Expand Down Expand Up @@ -76,7 +106,7 @@ export function PullRequestListFilters(props: {
props.onChange({ ...value, sort: next as PullRequestListControls["sort"] });
break;
case "project": {
const project = props.projects.find(
const project = projects.find(
(project) => JSON.stringify([project.environmentId, project.id]) === next,
);
props.onChange({
Expand Down Expand Up @@ -165,15 +195,15 @@ export function PullRequestListFilters(props: {
{
id: "projects",
title:
props.projects.find(
projects.find(
(project) =>
project.id === value.projectId && project.environmentId === value.projectEnvironmentId,
)?.title ?? "Project",
subactions: choices(
"project",
Object.fromEntries([
["", "All projects"],
...props.projects.map((project) => [
...projects.map((project) => [
JSON.stringify([project.environmentId, project.id]),
project.title,
]),
Expand Down
13 changes: 10 additions & 3 deletions apps/mobile/src/features/pull-requests/PullRequestsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
scopedPullRequestListKey as pullRequestKey,
type EnvironmentPullRequestListEntry,
PR_LIST_PAGE_SIZE,
withoutScratchProjectScope,
} from "./pullRequestList";
import { useDebouncedValue } from "../../state/queries";
import { useWorkspaceTabs } from "../workspace/WorkspaceTabsContext";
Expand Down Expand Up @@ -102,7 +103,12 @@ export function PullRequestsScreen(
},
[navigation, selectAndroidWorkspace],
);
const controls = listControls.controlsFor(environmentId);
const allProjects = useProjects();
const savedControls = listControls.controlsFor(environmentId);
const controls = useMemo(
() => withoutScratchProjectScope(savedControls, allProjects),
[allProjects, savedControls],
);
const targets = useMemo(
() =>
environments
Expand Down Expand Up @@ -175,8 +181,9 @@ export function PullRequestsScreen(
].sort(),
}));
}
const projects = useProjects().filter(
(project) => environmentId === null || project.environmentId === environmentId,
const projects = allProjects.filter(
(project) =>
!project.isScratch && (environmentId === null || project.environmentId === environmentId),
);
const renderPullRequest = useCallback(
({ item }: { readonly item: EnvironmentPullRequestListEntry }) => (
Expand Down
37 changes: 37 additions & 0 deletions apps/mobile/src/features/pull-requests/pullRequestList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
pullRequestLabelColor,
scopedPullRequestListKey,
pullRequestListRowsAreEqual,
withoutScratchProjectScope,
} from "./pullRequestList";

function entry(number: number, patch: Partial<PullRequestListEntry> = {}): PullRequestListEntry {
Expand Down Expand Up @@ -373,3 +374,39 @@ describe("mobile PR scan and filters", () => {
expect(pullRequestLabelColor("not-a-color")).toBeNull();
});
});

describe("withoutScratchProjectScope", () => {
const projects = [
{ id: "repo", environmentId: "env-a" },
{ id: "scratch", environmentId: "env-a", isScratch: true as const },
];

it("reads a scope saved on the Scratch folder as all projects", () => {
const controls = withoutScratchProjectScope(
{ ...DEFAULT_PR_LIST_CONTROLS, projectId: "scratch", projectEnvironmentId: "env-a" },
projects,
);
expect(controls.projectId).toBeUndefined();
expect(controls.projectEnvironmentId).toBeUndefined();
});

it("keeps a repository scope, and one whose projects have not arrived yet", () => {
const repository = {
...DEFAULT_PR_LIST_CONTROLS,
projectId: "repo",
projectEnvironmentId: "env-a",
};
expect(withoutScratchProjectScope(repository, projects)).toBe(repository);
const pending = { ...repository, projectId: "scratch" };
expect(withoutScratchProjectScope(pending, [])).toBe(pending);
});

it("keeps another server's project that shares the Scratch folder's id", () => {
const elsewhere = {
...DEFAULT_PR_LIST_CONTROLS,
projectId: "scratch",
projectEnvironmentId: "env-b",
};
expect(withoutScratchProjectScope(elsewhere, projects)).toBe(elsewhere);
});
});
15 changes: 15 additions & 0 deletions apps/mobile/src/features/pull-requests/pullRequestList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,21 @@ export const DEFAULT_PR_LIST_CONTROLS: PullRequestListControls = {
sort: "updated",
};

/** A scope saved on the Scratch folder is no longer offered, so it reads as "All projects". */
export function withoutScratchProjectScope(
controls: PullRequestListControls,
projects: readonly { id: string; environmentId: string; isScratch?: true }[],
): PullRequestListControls {
return projects.some(
(project) =>
project.isScratch &&
project.id === controls.projectId &&
project.environmentId === controls.projectEnvironmentId,
)
? { ...controls, projectId: undefined, projectEnvironmentId: undefined }
: controls;
}

export function pullRequestListKey(
item: Pick<PullRequestListEntry, "host" | "repository" | "number">,
) {
Expand Down
45 changes: 4 additions & 41 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,21 +128,21 @@ import { isMacPlatform } from "~/lib/utils";
import { useOpenPrLink } from "../lib/openPullRequestLink";
import { readLocalApi } from "../localApi";
import { useSidebarPendingFileDropStore } from "../sidebarPendingFileDropStore";
import { getProjectOrderKey, selectProjectGroupingSettings } from "../logicalProject";
import { selectProjectGroupingSettings } from "../logicalProject";
import {
buildSidebarProjectSnapshots,
projectGroupsSpanEnvironments,
resolveSidebarProjectScopeKey,
getSidebarProjectSettingsKey,
type SidebarProjectSnapshot,
} from "../sidebarProjectGrouping";
import { legacyProjectCwdPreferenceKey, useUiStateStore } from "../uiStateStore";
import { useUiStateStore } from "../uiStateStore";
import {
getThreadKeysToDeselectAfterDelete,
useThreadSelectionStore,
} from "../threadSelectionStore";
import { useAcknowledgeThreadWoke, useThreadActions } from "../hooks/useThreadActions";
import { useHandleNewThread } from "../hooks/useHandleNewThread";
import { useSidebarProjectGroups } from "../hooks/useSidebarProjectGroups";
import { useTerminalFocus } from "../hooks/useTerminalFocus";
import { isCommandPaletteOpen, openCommandPalette } from "../commandPaletteBus";
import { startNewThreadFromContext } from "../lib/chatThreadActions";
Expand Down Expand Up @@ -223,7 +223,6 @@ import {
sidebarThreadKeyAtY,
sortInboxThreadsByReturn,
sortPinnedThreadsForSidebar,
sortSidebarV2ProjectGroups,
sortThreadsForSidebar,
sortWorkingThreadsBySend,
useThreadJumpHintVisibility,
Expand Down Expand Up @@ -2419,14 +2418,12 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {

export default function Sidebar() {
const projects = useProjects();
const projectOrder = useUiStateStore((store) => store.projectOrder);
const threads = useThreadShells();
const router = useRouter();
const { isMobile, setOpenMobile } = useSidebar();
const keybindings = useAtomValue(primaryServerKeybindingsAtom);
const confirmThreadDelete = useClientSettings((s) => s.confirmThreadDelete);
const confirmThreadArchive = useClientSettings((s) => s.confirmThreadArchive);
const sidebarProjectSortOrder = useClientSettings((s) => s.sidebarProjectSortOrder);
const timestampFormat = useClientSettings((s) => s.timestampFormat);
const workingShelfEnabled = useClientSettings((s) => s.sidebarWorkingShelfEnabled);
const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings);
Expand Down Expand Up @@ -2544,41 +2541,7 @@ export default function Sidebar() {
[environments],
);
const environmentMachineById = useEnvironmentMachines();
const orderedProjects = useMemo(
() =>
orderItemsByPreferredIds({
items: projects,
preferredIds: projectOrder,
getId: getProjectOrderKey,
getPreferenceIds: (project) => [
getProjectOrderKey(project),
legacyProjectCwdPreferenceKey(project.workspaceRoot),
],
}),
[projectOrder, projects],
);
const unsortedProjectGroups = useMemo(
() =>
buildSidebarProjectSnapshots({
projects: sidebarProjectSortOrder === "manual" ? orderedProjects : projects,
groupScratchProjects: true,
settings: projectGroupingSettings,
primaryEnvironmentId,
resolveEnvironmentLabel: (environmentId) => environmentLabelById.get(environmentId) ?? null,
}),
[
environmentLabelById,
orderedProjects,
primaryEnvironmentId,
projectGroupingSettings,
projects,
sidebarProjectSortOrder,
],
);
const projectGroups = useMemo(
() => sortSidebarV2ProjectGroups(unsortedProjectGroups, threads, sidebarProjectSortOrder),
[sidebarProjectSortOrder, threads, unsortedProjectGroups],
);
const projectGroups = useSidebarProjectGroups();
const projectGroupsRef = useRef(projectGroups);
projectGroupsRef.current = projectGroups;
// Threads on non-primary environments (T3 Connect, hosted) resolve their
Expand Down
49 changes: 49 additions & 0 deletions apps/web/src/hooks/useSidebarProjectGroups.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useMemo } from "react";

import { orderItemsByPreferredIds, sortSidebarV2ProjectGroups } from "../components/Sidebar.logic";
import { getProjectOrderKey, selectProjectGroupingSettings } from "../logicalProject";
import { buildSidebarProjectSnapshots } from "../sidebarProjectGrouping";
import { useEnvironmentIdentities, usePrimaryEnvironmentId } from "../state/environments";
import { useProjects, useThreadShells } from "../state/entities";
import { legacyProjectCwdPreferenceKey, useUiStateStore } from "../uiStateStore";
import { useClientSettings } from "./useSettings";

/** The sidebar's project groups, in the order the sidebar shows them. */
export function useSidebarProjectGroups() {
const projects = useProjects();
const threads = useThreadShells();
const projectOrder = useUiStateStore((store) => store.projectOrder);
const sortOrder = useClientSettings((s) => s.sidebarProjectSortOrder);
const groupingSettings = useClientSettings(selectProjectGroupingSettings);
const environments = useEnvironmentIdentities();
const primaryEnvironmentId = usePrimaryEnvironmentId();
const orderedProjects = useMemo(
() =>
orderItemsByPreferredIds({
items: projects,
preferredIds: projectOrder,
getId: getProjectOrderKey,
getPreferenceIds: (project) => [
getProjectOrderKey(project),
legacyProjectCwdPreferenceKey(project.workspaceRoot),
],
}),
[projectOrder, projects],
);
const unsortedGroups = useMemo(() => {
const labels = new Map(
environments.map((environment) => [environment.environmentId, environment.label] as const),
);
return buildSidebarProjectSnapshots({
projects: sortOrder === "manual" ? orderedProjects : projects,
groupScratchProjects: true,
settings: groupingSettings,
primaryEnvironmentId,
resolveEnvironmentLabel: (environmentId) => labels.get(environmentId) ?? null,
});
}, [environments, groupingSettings, orderedProjects, primaryEnvironmentId, projects, sortOrder]);
return useMemo(
() => sortSidebarV2ProjectGroups(unsortedGroups, threads, sortOrder),
[sortOrder, threads, unsortedGroups],
);
}
24 changes: 21 additions & 3 deletions apps/web/src/routes/_chat.pull-requests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { PullRequestSpeedActionResult } from "~/components/pullRequest/Pull
import { usePullRequestCloseBatch } from "~/components/pullRequest/usePullRequestActions";
import { SidebarPointerSensor } from "~/components/Sidebar.pointer";
import { resolveSidebarSweepKeys } from "~/components/Sidebar.logic";
import { sortProjectsByGroupOrder } from "@t3tools/client-runtime/state/project-grouping";
import { pullRequestHostOf, resolveEnvironmentMachineKind } from "@t3tools/contracts";
import type {
EnvironmentId,
Expand Down Expand Up @@ -45,6 +46,7 @@ import {
useMemo,
useRef,
useState,
type ComponentProps,
type ReactNode,
type RefObject,
} from "react";
Expand Down Expand Up @@ -154,6 +156,7 @@ import {
} from "../rightPanelStore";
import { useDebouncedValue } from "../state/queries";
import { useAllEnvironmentShellsBootstrapped, useProjects } from "../state/entities";
import { useSidebarProjectGroups } from "../hooks/useSidebarProjectGroups";
import { useEnvironments } from "../state/environments";
import {
pullRequestEnvironment,
Expand Down Expand Up @@ -411,9 +414,12 @@ function PullRequestsRouteView() {
// the one wrong answer the empty state can give.
const projectsKnown = useAllEnvironmentShellsBootstrapped();
// Only the projects the page can actually read: one on an environment that cannot list pull
// requests could neither be listed nor acted on.
// requests could neither be listed nor acted on, and the Scratch folder is never a repository.
const projects = useMemo(
() => allProjects.filter((project) => environmentIds.includes(project.environmentId)),
() =>
allProjects.filter(
(project) => !project.isScratch && environmentIds.includes(project.environmentId),
),
[allProjects, environmentIds],
);
const environmentLabels = useMemo(
Expand Down Expand Up @@ -2058,7 +2064,7 @@ function PullRequestsRouteView() {
/>
);
const filtersMenu = (
<PullRequestFiltersMenu
<PullRequestFiltersMenuInSidebarOrder
onOpenChange={setFiltersOpen}
state={search.state}
stateOptions={STATE_TABS}
Expand Down Expand Up @@ -2386,6 +2392,18 @@ function PullRequestsRouteView() {
}

/** A compact stand-in for one pill group when the header is narrow. */
/** Lists the projects in the sidebar's order; it re-renders with thread activity, so the page does not. */
function PullRequestFiltersMenuInSidebarOrder(
props: ComponentProps<typeof PullRequestFiltersMenu>,
) {
const sidebarProjectGroups = useSidebarProjectGroups();
const projects = useMemo(
() => sortProjectsByGroupOrder(props.projects, sidebarProjectGroups),
[props.projects, sidebarProjectGroups],
);
return <PullRequestFiltersMenu {...props} projects={projects} />;
}

function CompactFilterMenu<Value extends string>({
label,
triggerIcon,
Expand Down
Loading
Loading