Skip to content

Commit d33f9fc

Browse files
refactor(mobile): reuse repository aggregation
Generated-By: PostHog Code Task-Id: c1bbe3cf-742b-4b24-bf96-d11a18b4cf22
1 parent 994b567 commit d33f9fc

13 files changed

Lines changed: 274 additions & 241 deletions

File tree

apps/mobile/src/app/task/index.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
11
import { Text } from "@components/text";
2-
import {
3-
findRepositoryOption,
4-
isRepositorySelectionComplete,
5-
type RepositorySelection,
6-
toRepositorySelection,
7-
} from "@posthog/core/integrations/repositories";
82
import {
93
DEFAULT_CLAUDE_EXECUTION_MODE,
104
getAvailableModes,
@@ -79,7 +73,15 @@ import {
7973
pendingTaskPromptStoreApi,
8074
} from "@/features/tasks/stores/pendingTaskPromptStore";
8175
import { useTaskStore } from "@/features/tasks/stores/taskStore";
82-
import type { CreateTaskOptions } from "@/features/tasks/types";
76+
import type {
77+
CreateTaskOptions,
78+
RepositorySelection,
79+
} from "@/features/tasks/types";
80+
import {
81+
findRepositoryOption,
82+
isRepositorySelectionComplete,
83+
toRepositorySelection,
84+
} from "@/features/tasks/utils/repositorySelection";
8385
import { useScreenInsets } from "@/hooks/useScreenInsets";
8486
import { logger } from "@/lib/logger";
8587
import { getPostHogApiClient } from "@/lib/posthogApiClient";

apps/mobile/src/features/inbox/components/TinderView.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
formatSignalReportSummaryMarkdown,
44
inboxStatusLabel,
55
} from "@posthog/core/inbox/reportPresentation";
6-
import type { RepositoryOption } from "@posthog/core/integrations/repositories";
76
import type {
87
SignalReport,
98
SignalReportPriority,
@@ -29,7 +28,10 @@ import { MarkdownText } from "@/features/chat/components/MarkdownText";
2928
import { usePreferencesStore } from "@/features/preferences/stores/preferencesStore";
3029
import { getModelConfigOption } from "@/features/tasks/composer/options";
3130
import { useCloudTaskConfigOptions } from "@/features/tasks/hooks/useCloudTaskConfigOptions";
32-
import type { CreateTaskOptions } from "@/features/tasks/types";
31+
import type {
32+
CreateTaskOptions,
33+
RepositoryOption,
34+
} from "@/features/tasks/types";
3335
import {
3436
ANALYTICS_EVENTS,
3537
computeReportAgeHours,

apps/mobile/src/features/tasks/components/AutomationForm.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,6 @@ import {
77
deriveAutomationName,
88
parseCronExpression,
99
} from "@posthog/core/automations/automationSchedule";
10-
import {
11-
findRepositoryOption,
12-
isRepositorySelectionComplete,
13-
type RepositorySelection,
14-
toRepositorySelection,
15-
} from "@posthog/core/integrations/repositories";
1610
import { CaretDown, GithubLogo } from "phosphor-react-native";
1711
import { type MutableRefObject, useEffect, useMemo, useState } from "react";
1812
import {
@@ -26,6 +20,12 @@ import { MarkdownText } from "@/features/chat/components/MarkdownText";
2620
import { useThemeColors } from "@/lib/theme";
2721
import { RepositoryPickerInline } from "../composer/RepositoryPickerInline";
2822
import { useIntegrations } from "../hooks/useIntegrations";
23+
import type { RepositorySelection } from "../types";
24+
import {
25+
findRepositoryOption,
26+
isRepositorySelectionComplete,
27+
toRepositorySelection,
28+
} from "../utils/repositorySelection";
2929
import { GitHubConnectionPrompt } from "./GitHubConnectionPrompt";
3030
import { GitHubLoadNotice } from "./GitHubLoadNotice";
3131
import { ScheduleEditor } from "./ScheduleEditor";

apps/mobile/src/features/tasks/composer/RepositoryPickerInline.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Text } from "@components/text";
2-
import type { RepositoryOption } from "@posthog/core/integrations/repositories";
32
import { ArrowsClockwise, Check, MagnifyingGlass } from "phosphor-react-native";
43
import { useEffect, useMemo, useRef, useState } from "react";
54
import {
@@ -17,6 +16,7 @@ import Animated, {
1716
withTiming,
1817
} from "react-native-reanimated";
1918
import { useThemeColors } from "@/lib/theme";
19+
import type { RepositoryOption } from "../types";
2020

2121
// Tuning for the nested (ScrollView) path's progressive mount. The first
2222
// chunk needs to cover the rows the user can actually see (~5 with the

apps/mobile/src/features/tasks/hooks/useIntegrations.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import {
2-
buildTeamRepositoryOptions,
3-
normalizeRepositoryNames,
4-
repositoryLoadWarning,
5-
repositoryOptionsEqual,
6-
} from "@posthog/core/integrations/repositories";
1+
import { combineGithubRepositories } from "@posthog/core/integrations/repositories";
72
import { useQuery } from "@tanstack/react-query";
83
import { useEffect, useMemo } from "react";
94
import { useAuthStore } from "@/features/auth";
105
import { getPostHogApiClient } from "@/lib/posthogApiClient";
116
import { useRepositoryCacheStore } from "../stores/repositoryCacheStore";
7+
import {
8+
buildRepositoryOptions,
9+
repositoryLoadWarning,
10+
repositoryOptionsEqual,
11+
} from "../utils/repositorySelection";
1212

1313
export const integrationKeys = {
1414
all: ["integrations"] as const,
@@ -58,9 +58,11 @@ export function useIntegrations(options: UseIntegrationsOptions = {}) {
5858
const results = await Promise.allSettled(
5959
githubIntegrations.map(async (integration) => ({
6060
integrationId: integration.id,
61-
repositories: normalizeRepositoryNames(
62-
await getPostHogApiClient().getGithubRepositories(integration.id),
63-
),
61+
repositories: (
62+
await getPostHogApiClient().getGithubRepositories(integration.id)
63+
)
64+
.map((repository) => repository.toLowerCase())
65+
.filter(Boolean),
6466
})),
6567
);
6668

@@ -89,16 +91,27 @@ export function useIntegrations(options: UseIntegrationsOptions = {}) {
8991

9092
const repositoriesByIntegration =
9193
repositoriesQuery.data?.repositoriesByIntegration ?? {};
92-
const repositories = Object.values(repositoriesByIntegration).flat().sort();
94+
const repositories = Object.keys(
95+
combineGithubRepositories(
96+
githubIntegrations.map((integration) => ({
97+
data: {
98+
integrationId: integration.id,
99+
repos: repositoriesByIntegration[integration.id] ?? [],
100+
},
101+
isPending: repositoriesQuery.isPending,
102+
isError: false,
103+
isRefetching: repositoriesQuery.isRefetching,
104+
})),
105+
).repositoryMap,
106+
).sort();
93107

94108
// Memoize the derived options list keyed on the underlying query data so
95109
// its reference is stable across renders when the data hasn't actually
96110
// changed. Without this, every render produces a fresh array — which both
97111
// churns the cache-write effect below AND defeats downstream React.memo /
98112
// useMemo callers that depend on `repositoryOptions`.
99113
const liveRepositoryOptions = useMemo(
100-
() =>
101-
buildTeamRepositoryOptions(githubIntegrations, repositoriesByIntegration),
114+
() => buildRepositoryOptions(githubIntegrations, repositoriesByIntegration),
102115
[githubIntegrations, repositoriesByIntegration],
103116
);
104117

apps/mobile/src/features/tasks/hooks/useUserIntegrations.ts

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import {
2-
buildUserRepositoryOptions,
3-
normalizeRepositoryNames,
4-
type RepositoryOption,
5-
repositoryLoadWarning,
6-
} from "@posthog/core/integrations/repositories";
1+
import { combineUserGithubRepositories } from "@posthog/core/integrations/repositories";
72
import { useQuery } from "@tanstack/react-query";
83
import { useCallback, useMemo } from "react";
94
import { useAuthStore } from "@/features/auth";
105
import { getPostHogApiClient } from "@/lib/posthogApiClient";
6+
import type { RepositoryOption } from "../types";
7+
import {
8+
buildUserRepositoryOptions,
9+
repositoryLoadWarning,
10+
} from "../utils/repositorySelection";
1111

1212
/**
1313
* User-scoped sibling of {@link useIntegrations}. Reads the authenticated
@@ -63,31 +63,47 @@ export function useUserIntegrations(options: UseUserIntegrationsOptions = {}) {
6363
integrations.map((i) => i.installation_id),
6464
),
6565
queryFn: async () => {
66-
const byInstallation: Record<string, string[]> = {};
6766
const results = await Promise.allSettled(
6867
integrations.map(async (integration) => ({
6968
installationId: integration.installation_id,
70-
repositories: normalizeRepositoryNames(
69+
repositories: (
7170
await getPostHogApiClient().getGithubUserRepositories(
7271
integration.installation_id,
73-
),
74-
),
72+
)
73+
)
74+
.map((repository) => repository.toLowerCase())
75+
.filter(Boolean),
7576
})),
7677
);
7778

78-
let failedCount = 0;
79-
for (const result of results) {
80-
if (result.status === "fulfilled") {
81-
byInstallation[result.value.installationId] =
82-
result.value.repositories;
83-
} else {
84-
failedCount += 1;
85-
}
86-
}
79+
const combined = combineUserGithubRepositories(
80+
results.map((result) => ({
81+
data:
82+
result.status === "fulfilled"
83+
? {
84+
userIntegrationId:
85+
integrations.find(
86+
(integration) =>
87+
integration.installation_id ===
88+
result.value.installationId,
89+
)?.id ?? "",
90+
installationId: result.value.installationId,
91+
repos: result.value.repositories,
92+
}
93+
: undefined,
94+
isPending: false,
95+
isError: result.status === "rejected",
96+
isRefetching: false,
97+
})),
98+
integrations.map((integration) => integration.installation_id),
99+
);
87100

88101
return {
89-
byInstallation,
90-
partialError: repositoryLoadWarning(failedCount, integrations.length),
102+
byInstallation: combined.reposByInstallationId,
103+
partialError: repositoryLoadWarning(
104+
combined.failedInstallationIds.length,
105+
integrations.length,
106+
),
91107
};
92108
},
93109
enabled: enabled && integrations.length > 0,

apps/mobile/src/features/tasks/stores/repositoryCacheStore.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import type { RepositoryOption } from "@posthog/core/integrations/repositories";
21
import AsyncStorage from "@react-native-async-storage/async-storage";
32
import { create } from "zustand";
43
import { createJSONStorage, persist } from "zustand/middleware";
4+
import type { RepositoryOption } from "../types";
55

66
interface RepositoryCacheState {
77
/** Last successfully fetched, sorted list of repository options across all

apps/mobile/src/features/tasks/stores/taskStore.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import type { RepositorySelection } from "@posthog/core/integrations/repositories";
21
import type { TaskActivitySortMode } from "@posthog/core/tasks/taskActivity";
32
import type { ExecutionMode, SupportedReasoningEffort } from "@posthog/shared";
43
import AsyncStorage from "@react-native-async-storage/async-storage";
54
import { create } from "zustand";
65
import { createJSONStorage, persist } from "zustand/middleware";
6+
import type { RepositorySelection } from "../types";
77

88
export type OrganizeMode = "by-project" | "chronological";
99
export type SortMode = TaskActivitySortMode;

apps/mobile/src/features/tasks/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,17 @@ export interface UserGithubIntegration {
5757
};
5858
}
5959

60+
export interface RepositoryOption {
61+
integrationId: number;
62+
integrationLabel: string;
63+
repository: string;
64+
}
65+
66+
export interface RepositorySelection {
67+
integrationId: number | null;
68+
repository: string | null;
69+
}
70+
6071
export interface CreateTaskOptions {
6172
description: string;
6273
title?: string;
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
buildRepositoryOptions,
4+
findRepositoryOption,
5+
isRepositorySelectionComplete,
6+
toRepositorySelection,
7+
} from "./repositorySelection";
8+
9+
describe("repositorySelection", () => {
10+
const integrations = [
11+
{ id: 7, kind: "github", display_name: "Personal GitHub" },
12+
{
13+
id: 11,
14+
kind: "github",
15+
config: { account: { login: "posthog" } },
16+
},
17+
];
18+
19+
it("preserves integration identity for each repository option", () => {
20+
expect(
21+
buildRepositoryOptions(integrations, {
22+
7: ["annika/mobile-app"],
23+
11: ["posthog/posthog", "posthog/code"],
24+
}),
25+
).toEqual([
26+
{
27+
integrationId: 7,
28+
integrationLabel: "Personal GitHub",
29+
repository: "annika/mobile-app",
30+
},
31+
{
32+
integrationId: 11,
33+
integrationLabel: "posthog",
34+
repository: "posthog/code",
35+
},
36+
{
37+
integrationId: 11,
38+
integrationLabel: "posthog",
39+
repository: "posthog/posthog",
40+
},
41+
]);
42+
});
43+
44+
it("finds an exact repository option", () => {
45+
const options = buildRepositoryOptions(integrations, {
46+
7: ["posthog/posthog"],
47+
11: ["posthog/posthog"],
48+
});
49+
50+
expect(
51+
findRepositoryOption(options, {
52+
integrationId: 11,
53+
repository: "posthog/posthog",
54+
}),
55+
).toEqual({
56+
integrationId: 11,
57+
integrationLabel: "posthog",
58+
repository: "posthog/posthog",
59+
});
60+
});
61+
62+
it("converts an option into a repository selection", () => {
63+
const selection = toRepositorySelection({
64+
integrationId: 11,
65+
integrationLabel: "posthog",
66+
repository: "posthog/code",
67+
});
68+
69+
expect(selection).toEqual({
70+
integrationId: 11,
71+
repository: "posthog/code",
72+
});
73+
expect(isRepositorySelectionComplete(selection)).toBe(true);
74+
});
75+
});

0 commit comments

Comments
 (0)