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
15 changes: 13 additions & 2 deletions apps/dashboard/src/components/navigation/command-palette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import {
CommandShortcut,
} from "@diffkit/ui/components/command";
import { cn } from "@diffkit/ui/lib/utils";
import { useQuery } from "@tanstack/react-query";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { getRouteApi, useRouter } from "@tanstack/react-router";
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import type { CommandItem, CommandItemMeta } from "#/lib/command-palette/types";
import {
cacheSearchResults,
getCommandSearchItems,
useCommandItems,
} from "#/lib/command-palette/use-command-items";
Expand All @@ -26,7 +27,9 @@ const routeApi = getRouteApi("/_protected");
export function CommandPalette() {
const { open, setOpen, close } = useCommandPalette();
const router = useRouter();
const queryClient = useQueryClient();
const { user } = routeApi.useRouteContext();
const scope = useMemo(() => ({ userId: user.id }), [user.id]);
const [search, setSearch] = useState("");
const debouncedSearch = useDebouncedValue(search, 250);
const trimmedDebouncedSearch = debouncedSearch.trim();
Expand All @@ -48,6 +51,14 @@ export function CommandPalette() {
[items, searchItems],
);

const cachedSearchDataRef = useRef(githubSearchQuery.data);
useEffect(() => {
const data = githubSearchQuery.data;
if (!data || data === cachedSearchDataRef.current) return;
cachedSearchDataRef.current = data;
cacheSearchResults(queryClient, scope, data);
}, [githubSearchQuery.data, queryClient, scope]);

const groups = new Map<string, CommandItem[]>();
for (const item of allItems) {
const list = groups.get(item.group) ?? [];
Expand Down
96 changes: 46 additions & 50 deletions apps/dashboard/src/lib/command-palette/use-command-items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@ import {
GitPullRequestDraftIcon,
GitPullRequestIcon,
IssuesIcon,
UserAddIcon,
} from "@diffkit/icons";
import { useQueryClient } from "@tanstack/react-query";
import { type QueryClient, useQueryClient } from "@tanstack/react-query";
import { getRouteApi } from "@tanstack/react-router";
import { useSyncExternalStore } from "react";
import type { GitHubQueryScope } from "#/lib/github.query";
import { githubQueryKeys } from "#/lib/github.query";
import type {
CommandPaletteSearchResult,
GitHubAccountSummary,
IssueSummary,
MyIssuesResult,
MyPullsResult,
Expand Down Expand Up @@ -49,14 +48,6 @@ function getIssueIcon(issue: IssueSummary) {
return { icon: IssuesIcon, iconClassName: "text-green-500" };
}

function getGitHubAccountGroup(account: GitHubAccountSummary) {
return account.type === "Organization"
? "GitHub Organizations"
: "GitHub Users";
}

const noopCommandAction = () => {};

const routeApi = getRouteApi("/_protected");

export function useCommandItems(): CommandItem[] {
Expand Down Expand Up @@ -195,45 +186,12 @@ export function getCommandSearchItems(

const items: CommandItem[] = [];

for (const repo of result.repositories) {
items.push({
id: `repo:${repo.id}`,
label: repo.fullName,
group: "GitHub Repositories",
icon: CodeIcon,
keywords: [repo.name, repo.owner, repo.language ?? ""].filter(Boolean),
action: {
type: "execute",
fn: noopCommandAction,
},
meta: {
language: repo.language,
stars: repo.stars,
updatedAt: repo.updatedAt ?? undefined,
},
});
}

for (const user of result.users) {
items.push({
id: `github-account:${user.id}`,
label: user.login,
group: getGitHubAccountGroup(user),
icon: UserAddIcon,
keywords: [user.login, user.type],
action: {
type: "execute",
fn: noopCommandAction,
},
});
}

for (const pr of result.pulls) {
const prState = getPrIcon(pr);
items.push({
id: `pull:${pr.id}`,
label: `#${pr.number} ${pr.title}`,
group: "GitHub Pull Requests",
group: "Pull Requests",
icon: prState.icon,
iconClassName: prState.iconClassName,
keywords: [
Expand All @@ -243,8 +201,8 @@ export function getCommandSearchItems(
String(pr.number),
].filter(Boolean),
action: {
type: "execute",
fn: noopCommandAction,
type: "navigate",
to: `/${pr.repository.owner}/${pr.repository.name}/pull/${pr.number}`,
},
meta: {
repo: pr.repository.fullName,
Expand All @@ -259,7 +217,7 @@ export function getCommandSearchItems(
items.push({
id: `issue:${issue.id}`,
label: `#${issue.number} ${issue.title}`,
group: "GitHub Issues",
group: "Issues",
icon: issueState.icon,
iconClassName: issueState.iconClassName,
keywords: [
Expand All @@ -269,8 +227,8 @@ export function getCommandSearchItems(
String(issue.number),
].filter(Boolean),
action: {
type: "execute",
fn: noopCommandAction,
type: "navigate",
to: `/${issue.repository.owner}/${issue.repository.name}/issues/${issue.number}`,
},
meta: {
repo: issue.repository.fullName,
Expand All @@ -282,3 +240,41 @@ export function getCommandSearchItems(

return items;
}

export function cacheSearchResults(
queryClient: QueryClient,
scope: GitHubQueryScope,
result: CommandPaletteSearchResult,
) {
if (result.pulls.length > 0) {
queryClient.setQueryData<MyPullsResult>(
githubQueryKeys.pulls.mine(scope),
(prev) => {
if (!prev) return prev;
const existingIds = new Set(prev.involved.map((p) => p.id));
const newPulls = result.pulls.filter((p) => !existingIds.has(p.id));
if (newPulls.length === 0) return prev;
return {
...prev,
involved: [...prev.involved, ...newPulls],
};
},
);
}

if (result.issues.length > 0) {
queryClient.setQueryData<MyIssuesResult>(
githubQueryKeys.issues.mine(scope),
(prev) => {
if (!prev) return prev;
const existingIds = new Set(prev.mentioned.map((i) => i.id));
const newIssues = result.issues.filter((i) => !existingIds.has(i.id));
if (newIssues.length === 0) return prev;
return {
...prev,
mentioned: [...prev.mentioned, ...newIssues],
};
},
);
}
}
82 changes: 6 additions & 76 deletions apps/dashboard/src/lib/github.functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type {
CommandPaletteSearchResult,
CreateLabelInput,
CreateReviewCommentInput,
GitHubAccountSummary,
GitHubActor,
GitHubLabel,
IssueComment,
Expand Down Expand Up @@ -80,12 +79,6 @@ type SearchItem = Awaited<
type SearchResult = Awaited<
ReturnType<GitHubClient["rest"]["search"]["issuesAndPullRequests"]>
>["data"];
type RepositorySearchItem = Awaited<
ReturnType<GitHubClient["rest"]["search"]["repos"]>
>["data"]["items"][number];
type UserSearchItem = Awaited<
ReturnType<GitHubClient["rest"]["search"]["users"]>
>["data"]["items"][number];
type AuthenticatedUserRepo = Awaited<
ReturnType<GitHubClient["rest"]["repos"]["listForAuthenticatedUser"]>
>["data"][number];
Expand Down Expand Up @@ -406,8 +399,6 @@ function normalizeCommandPaletteSearchQuery(query: string) {

function emptyCommandPaletteSearchResult(): CommandPaletteSearchResult {
return {
repositories: [],
users: [],
pulls: [],
issues: [],
};
Expand Down Expand Up @@ -458,42 +449,6 @@ function mapActor(user: GitHubApiUser | null | undefined): GitHubActor | null {
};
}

function mapGitHubAccountSearchItem(
user: UserSearchItem,
): GitHubAccountSummary | null {
if (!user.login) {
return null;
}

return {
id: user.id,
login: user.login,
avatarUrl: user.avatar_url ?? "",
url: user.html_url ?? `https://github.com/${user.login}`,
type: user.type ?? "User",
};
}

function mapRepositorySearchItem(repo: RepositorySearchItem): UserRepoSummary {
const ownerLogin = repo.owner?.login ?? "";
const fullName =
repo.full_name ?? [ownerLogin, repo.name].filter(Boolean).join("/");
const fallbackOwner = fullName.split("/")[0] ?? "";

return {
id: repo.id,
name: repo.name,
fullName,
description: repo.description ?? null,
stars: repo.stargazers_count ?? 0,
language: repo.language ?? null,
updatedAt: repo.updated_at ?? null,
isPrivate: repo.private ?? false,
url: repo.html_url ?? `https://github.com/${fullName}`,
owner: ownerLogin || fallbackOwner,
};
}

function mapReviewerCandidate(user: GitHubApiUser): RepoCollaborator | null {
const actor = mapActor(user);
if (!actor) {
Expand Down Expand Up @@ -2790,41 +2745,18 @@ export const searchCommandPaletteGitHub = createServerFn({ method: "GET" })
return emptyCommandPaletteSearchResult();
}

const viewer = await getViewer(context);
const login = viewer.login;

const perPage = clampCommandSearchPerPage(data.perPage);
const [repositories, users, pullItems, issueItems] = await Promise.all([
safeCommandPaletteSearch({
label: "repositories",
fallback: [] as UserRepoSummary[],
task: async () => {
const response = await context.octokit.rest.search.repos({
q: `${query} in:name,description fork:true`,
per_page: perPage,
sort: "updated",
order: "desc",
});
return response.data.items.map(mapRepositorySearchItem);
},
}),
safeCommandPaletteSearch({
label: "users",
fallback: [] as GitHubAccountSummary[],
task: async () => {
const response = await context.octokit.rest.search.users({
q: `${query} in:login,fullname`,
per_page: perPage,
});
return response.data.items
.map((user) => mapGitHubAccountSearchItem(user))
.filter((user): user is GitHubAccountSummary => Boolean(user));
},
}),
const [pullItems, issueItems] = await Promise.all([
safeCommandPaletteSearch({
label: "pull requests",
fallback: [] as SearchItem[],
task: async () => {
const response =
await context.octokit.rest.search.issuesAndPullRequests({
q: `${query} is:pr in:title,body archived:false`,
q: `${query} is:pr involves:${login} archived:false`,
per_page: perPage,
sort: "updated",
order: "desc",
Expand All @@ -2838,7 +2770,7 @@ export const searchCommandPaletteGitHub = createServerFn({ method: "GET" })
task: async () => {
const response =
await context.octokit.rest.search.issuesAndPullRequests({
q: `${query} is:issue in:title,body archived:false`,
q: `${query} is:issue involves:${login} archived:false`,
per_page: perPage,
sort: "updated",
order: "desc",
Expand All @@ -2849,8 +2781,6 @@ export const searchCommandPaletteGitHub = createServerFn({ method: "GET" })
]);

return {
repositories,
users,
pulls: mapPullSearchItems(pullItems),
issues: mapIssueSearchItems(issueItems),
};
Expand Down
2 changes: 0 additions & 2 deletions apps/dashboard/src/lib/github.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,6 @@ export type MyIssuesResult = {
};

export type CommandPaletteSearchResult = {
repositories: UserRepoSummary[];
users: GitHubAccountSummary[];
pulls: PullSummary[];
issues: IssueSummary[];
};
Expand Down
Loading