Skip to content
Closed
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
1 change: 1 addition & 0 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const clientSettings: ClientSettings = {
sidebarProjectGroupingOverrides: {
"environment-1:/tmp/project-a": "separate",
},
sidebarProjectNamesUsePath: false,
sidebarProjectSortOrder: "manual",
sidebarThreadSortOrder: "created_at",
sidebarThreadPreviewCount: 6,
Expand Down
6 changes: 3 additions & 3 deletions apps/mobile/src/features/home/HomeRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,12 @@ export function HomeRouteScreen() {
buildHomeProjectScopes({
projects,
environmentId: selectedEnvironmentId,
projectGroupingMode: listOptions.projectGroupingMode,
projectGroupingSettings: listOptions.projectGroupingSettings,
}).map((scope) => ({
key: scope.key,
label: scope.title,
})),
[listOptions.projectGroupingMode, projects, selectedEnvironmentId],
[listOptions.projectGroupingSettings, projects, selectedEnvironmentId],
);
useEffect(() => {
if (
Expand Down Expand Up @@ -229,7 +229,7 @@ export function HomeRouteScreen() {
onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })}
onThreadSortOrderChange={setThreadSortOrder}
pendingTasks={pendingTasks}
projectGroupingMode={listOptions.projectGroupingMode}
projectGroupingSettings={listOptions.projectGroupingSettings}
projects={projects}
projectSortOrder={listOptions.projectSortOrder}
savedConnectionsById={savedConnectionsById}
Expand Down
17 changes: 7 additions & 10 deletions apps/mobile/src/features/home/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
type LegendListRef,
type LegendListRenderItemProps,
} from "@legendapp/list/react-native";
import type { ProjectGroupingSettings } from "@t3tools/client-runtime/state/project-grouping";
import {
type EnvironmentProject,
type EnvironmentThreadShell,
Expand All @@ -12,11 +13,7 @@ import {
type EnvironmentThreadSearchMatch,
} from "@t3tools/client-runtime/state/thread-search";
import { sortPinnedThreadsByOrderKey } from "@t3tools/client-runtime/state/thread-sort";
import type {
EnvironmentId,
SidebarProjectGroupingMode,
SidebarThreadSortOrder,
} from "@t3tools/contracts";
import type { EnvironmentId, SidebarThreadSortOrder } from "@t3tools/contracts";
import { useAtomSet, useAtomValue } from "@effect/atom-react";
import { AsyncResult } from "effect/unstable/reactivity";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
Expand Down Expand Up @@ -91,7 +88,7 @@ interface HomeScreenProps {
readonly selectedProjectKey: string | null;
readonly projectSortOrder: HomeProjectSortOrder;
readonly threadSortOrder: SidebarThreadSortOrder;
readonly projectGroupingMode: SidebarProjectGroupingMode;
readonly projectGroupingSettings: ProjectGroupingSettings;
readonly onSearchQueryChange: (query: string) => void;
readonly onEnvironmentChange: (environmentId: EnvironmentId | null) => void;
readonly onProjectChange: (projectKey: string | null) => void;
Expand Down Expand Up @@ -311,9 +308,9 @@ export function HomeScreen(props: HomeScreenProps) {
buildHomeProjectScopes({
projects: props.projects,
environmentId: props.selectedEnvironmentId,
projectGroupingMode: props.projectGroupingMode,
projectGroupingSettings: props.projectGroupingSettings,
}),
[props.projectGroupingMode, props.projects, props.selectedEnvironmentId],
[props.projectGroupingSettings, props.projects, props.selectedEnvironmentId],
);
const selectedProjectScope = useMemo(
() =>
Expand Down Expand Up @@ -382,10 +379,10 @@ export function HomeScreen(props: HomeScreenProps) {
matchedThreadKeys,
projectSortOrder: props.projectSortOrder,
threadSortOrder: props.threadSortOrder,
projectGroupingMode: props.projectGroupingMode,
projectGroupingSettings: props.projectGroupingSettings,
}),
[
props.projectGroupingMode,
props.projectGroupingSettings,
props.projectSortOrder,
props.searchQuery,
props.selectedEnvironmentId,
Expand Down
23 changes: 11 additions & 12 deletions apps/mobile/src/features/home/home-list-options.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import type {
EnvironmentId,
SidebarProjectGroupingMode,
SidebarThreadSortOrder,
} from "@t3tools/contracts";
import type { ProjectGroupingSettings } from "@t3tools/client-runtime/state/project-grouping";
import type { EnvironmentId, SidebarThreadSortOrder } from "@t3tools/contracts";
import {
DEFAULT_SIDEBAR_PROJECT_SORT_ORDER,
DEFAULT_SIDEBAR_THREAD_SORT_ORDER,
Expand All @@ -19,6 +16,7 @@ import {
type SetStateAction,
} from "react";

import { DEFAULT_MOBILE_PROJECT_GROUPING_SETTINGS } from "../../state/project-grouping.logic";
import type { HomeProjectSortOrder } from "./homeThreadList";

export interface HomeListOptions {
Expand All @@ -28,7 +26,7 @@ export interface HomeListOptions {
}

export interface ResolvedHomeListOptions extends HomeListOptions {
readonly projectGroupingMode: SidebarProjectGroupingMode;
readonly projectGroupingSettings: ProjectGroupingSettings;
}

export const PROJECT_SORT_OPTIONS: ReadonlyArray<{
Expand Down Expand Up @@ -61,22 +59,22 @@ function defaultHomeListOptions(): HomeListOptions {
interface HomeListOptionsContextValue {
readonly options: HomeListOptions;
readonly setOptions: Dispatch<SetStateAction<HomeListOptions>>;
readonly projectGroupingMode: SidebarProjectGroupingMode;
readonly projectGroupingSettings: ProjectGroupingSettings;
}

const HomeListOptionsContext = createContext<HomeListOptionsContextValue | null>(null);

/** Keeps list preferences stable while the app moves between compact and split shells. */
export function HomeListOptionsProvider({
children,
projectGroupingMode,
projectGroupingSettings,
}: PropsWithChildren<{
readonly projectGroupingMode: SidebarProjectGroupingMode;
readonly projectGroupingSettings: ProjectGroupingSettings;
}>) {
const [options, setOptions] = useState<HomeListOptions>(defaultHomeListOptions);
const value = useMemo(
() => ({ options, setOptions, projectGroupingMode }),
[options, projectGroupingMode],
() => ({ options, setOptions, projectGroupingSettings }),
[options, projectGroupingSettings],
);
return createElement(HomeListOptionsContext, { value }, children);
}
Expand Down Expand Up @@ -114,7 +112,8 @@ export function useHomeListOptions(availableEnvironmentIds: ReadonlySet<Environm
: { ...options, selectedEnvironmentId };
const resolvedOptions: ResolvedHomeListOptions = {
...availableOptions,
projectGroupingMode: shared?.projectGroupingMode ?? "repository",
projectGroupingSettings:
shared?.projectGroupingSettings ?? DEFAULT_MOBILE_PROJECT_GROUPING_SETTINGS,
};

const setSelectedEnvironmentId = useCallback((value: EnvironmentId | null) => {
Expand Down
103 changes: 89 additions & 14 deletions apps/mobile/src/features/home/homeThreadList.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ProjectGroupingSettings } from "@t3tools/client-runtime/state/project-grouping";
import type {
EnvironmentProject,
EnvironmentThreadShell,
Expand All @@ -12,6 +13,17 @@ import {
sortHomeProjectScopes,
} from "./homeThreadList";

function groupingSettings(
mode: ProjectGroupingSettings["sidebarProjectGroupingMode"],
namesUsePath = false,
): ProjectGroupingSettings {
return {
sidebarProjectGroupingMode: mode,
sidebarProjectGroupingOverrides: {},
sidebarProjectNamesUsePath: namesUsePath,
};
}

function makeProject(
input: Partial<EnvironmentProject> & Pick<EnvironmentProject, "environmentId" | "id" | "title">,
): EnvironmentProject {
Expand Down Expand Up @@ -65,7 +77,7 @@ function buildGroups(
searchQuery: "",
projectSortOrder: "updated_at",
threadSortOrder: "updated_at",
projectGroupingMode: "repository",
projectGroupingSettings: groupingSettings("repository"),
now: NOW,
...overrides,
});
Expand Down Expand Up @@ -101,7 +113,7 @@ describe("buildHomeThreadGroups", () => {
const scopes = buildHomeProjectScopes({
projects,
environmentId: null,
projectGroupingMode: "repository",
projectGroupingSettings: groupingSettings("repository"),
});

expect(scopes).toHaveLength(1);
Expand Down Expand Up @@ -160,7 +172,7 @@ describe("buildHomeThreadGroups", () => {
const scopes = buildHomeProjectScopes({
projects,
environmentId: null,
projectGroupingMode: "repository",
projectGroupingSettings: groupingSettings("repository"),
});
const groups = buildGroups(projects, [staleThread]);

Expand Down Expand Up @@ -217,7 +229,7 @@ describe("buildHomeThreadGroups", () => {
const scopes = buildHomeProjectScopes({
projects,
environmentId: null,
projectGroupingMode: "repository",
projectGroupingSettings: groupingSettings("repository"),
});

expect(scopes).toHaveLength(1);
Expand All @@ -243,7 +255,7 @@ describe("buildHomeThreadGroups", () => {
const scopes = buildHomeProjectScopes({
projects: [newerProject, olderProject],
environmentId: null,
projectGroupingMode: "separate",
projectGroupingSettings: groupingSettings("separate"),
});

expect(
Expand Down Expand Up @@ -288,7 +300,7 @@ describe("buildHomeThreadGroups", () => {
const scopes = buildHomeProjectScopes({
projects: [invalidProject, validProject],
environmentId: null,
projectGroupingMode: "separate",
projectGroupingSettings: groupingSettings("separate"),
});

expect(
Expand Down Expand Up @@ -335,7 +347,7 @@ describe("buildHomeThreadGroups", () => {
const scopes = buildHomeProjectScopes({
projects: [olderMember, newerMember, otherProject],
environmentId: null,
projectGroupingMode: "repository",
projectGroupingSettings: groupingSettings("repository"),
});

expect(
Expand Down Expand Up @@ -370,7 +382,7 @@ describe("buildHomeThreadGroups", () => {
buildHomeProjectScopes({
projects,
environmentId: null,
projectGroupingMode: "repository",
projectGroupingSettings: groupingSettings("repository"),
}),
).toHaveLength(2);
});
Expand All @@ -394,7 +406,7 @@ describe("buildHomeThreadGroups", () => {
const scopes = buildHomeProjectScopes({
projects: [project],
environmentId: null,
projectGroupingMode: "repository",
projectGroupingSettings: groupingSettings("repository"),
});
const groups = buildGroups(
[project],
Expand Down Expand Up @@ -483,7 +495,7 @@ describe("buildHomeThreadGroups", () => {
const groups = buildGroups([olderProject, newerProject], threads, {
projectSortOrder: "created_at",
threadSortOrder: "created_at",
projectGroupingMode: "separate",
projectGroupingSettings: groupingSettings("separate"),
});

expect(groups.map((group) => group.representative.id)).toEqual([
Expand Down Expand Up @@ -564,17 +576,80 @@ describe("buildHomeThreadGroups", () => {
}),
);

expect(buildGroups(projects, threads, { projectGroupingMode: "repository" })).toHaveLength(1);
// Nested workspaces of one repo are separate projects in every mode.
expect(
buildGroups(projects, threads, {
projectGroupingSettings: groupingSettings("repository"),
}).map((group) => group.title),
).toEqual(["Mobile", "Web"]);
expect(
buildGroups(projects, threads, {
projectGroupingSettings: groupingSettings("repository", true),
}).map((group) => group.title),
).toEqual(["apps/mobile", "apps/web"]);
expect(
buildGroups(projects, threads, { projectGroupingMode: "repository_path" }).map(
buildGroups(projects, threads, {
projectGroupingSettings: groupingSettings("repository_path"),
}).map((group) => group.title),
).toEqual(["Mobile", "Web"]);
expect(
buildGroups(projects, threads, { projectGroupingSettings: groupingSettings("separate") }).map(
(group) => group.title,
),
).toEqual(["Mobile", "Web"]);
});

it("finds a nested workspace by its own path segment", () => {
const environmentId = EnvironmentId.make("environment-1");
const repositoryIdentity = {
canonicalKey: "github.com/kosyanmedia/delta",
locator: {
source: "git-remote" as const,
remoteName: "origin",
remoteUrl: "git@github.com:kosyanmedia/delta.git",
},
provider: "github",
owner: "kosyanmedia",
name: "delta",
displayName: "kosyanmedia/delta",
rootPath: "/home/dev/delta",
};
const projects = [
makeProject({
environmentId,
id: ProjectId.make("project-root"),
title: "kosyanmedia/delta",
workspaceRoot: "/home/dev/delta",
repositoryIdentity,
}),
makeProject({
environmentId,
id: ProjectId.make("project-nested"),
title: "kosyanmedia/delta",
workspaceRoot: "/home/dev/delta/commerce-pricing",
repositoryIdentity,
}),
];
const threads = projects.map((project) =>
makeThread({
environmentId,
id: ThreadId.make(`thread-${project.id}`),
projectId: project.id,
title: project.title,
}),
);

// Same git name on both rows, so the path disambiguates them.
expect(
buildGroups(projects, threads, { projectGroupingMode: "separate" }).map(
buildGroups(projects, threads)
.map((group) => group.title)
.sort(),
).toEqual(["kosyanmedia/delta · .", "kosyanmedia/delta · commerce-pricing"]);
expect(
buildGroups(projects, threads, { searchQuery: "commerce-pricing" }).map(
(group) => group.title,
),
).toEqual(["Mobile", "Web"]);
).toEqual(["kosyanmedia/delta · commerce-pricing"]);
});

it("default view shows only threads from the last 5 days", () => {
Expand Down
Loading
Loading