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
220 changes: 42 additions & 178 deletions apps/web/src/components/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,15 @@ import {
getNewProjectGitHubTarget,
getNewProjectPathPreview,
normalizePastedCloneUrl,
addProjectRemoteSourceLabel,
addProjectRemoteSourcePathHint,
addProjectRemoteSourceProvider,
buildAddProjectRemoteSourceReadiness,
sortAddProjectProviderSources,
type AddProjectRemoteSource,
type AddProjectRemoteSourceReadiness,
} from "@t3tools/client-runtime/operations/projects";
import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients";
import { connectionStatusText } from "@t3tools/client-runtime/connection";
import { threadSearchMatchKey } from "@t3tools/client-runtime/state/thread-search";
import { resolveThreadReferenceCopyTarget } from "@t3tools/shared/threadReference";
Expand Down Expand Up @@ -187,14 +195,8 @@ import {
CommandPaletteVirtualizedResults,
scrollCommandPaletteRowIntoView,
} from "./CommandPaletteResults";
import {
AzureDevOpsIcon,
BitbucketIcon,
GitCafeIcon,
GitHubIcon,
GitLabIcon,
ForgejoIcon,
} from "./Icons";
import { GitHubIcon } from "./Icons";
import { sourceControlIcon } from "~/sourceControlPresentation";
import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon";
import { Checkbox } from "./ui/checkbox";
import { ProjectFavicon } from "./ProjectFavicon";
Expand Down Expand Up @@ -257,15 +259,6 @@ interface AddProjectEnvironmentOption {
readonly status: string;
}

type AddProjectRemoteProviderKind =
| "github"
| "gitlab"
| "forgejo"
| "bitbucket"
| "azure-devops"
| "gitcafe";
type AddProjectRemoteSource = AddProjectRemoteProviderKind | "url";

type AddProjectCloneFlow =
| {
readonly step: "repository";
Expand All @@ -281,83 +274,10 @@ type AddProjectCloneFlow =
readonly remoteUrl: string;
};

const REMOTE_PROJECT_SOURCES: ReadonlyArray<AddProjectRemoteSource> = [
"url",
"github",
"gitlab",
"forgejo",
"bitbucket",
"azure-devops",
"gitcafe",
];
const REMOTE_PROJECT_PROVIDER_SOURCES: ReadonlyArray<AddProjectRemoteProviderKind> = [
"github",
"gitlab",
"forgejo",
"bitbucket",
"azure-devops",
"gitcafe",
];

function remoteProjectSourceLabel(source: AddProjectRemoteSource): string {
switch (source) {
case "github":
return "GitHub";
case "forgejo":
return "Forgejo / Gitea";
case "gitlab":
return "GitLab";
case "bitbucket":
return "Bitbucket";
case "azure-devops":
return "Azure DevOps";
case "gitcafe":
return "GitCafe";
case "url":
return "Git URL";
}
}

function remoteProjectSourcePathHint(source: AddProjectRemoteSource): string {
switch (source) {
case "forgejo":
case "github":
case "gitcafe":
return "owner/repo";
case "gitlab":
return "group/project";
case "bitbucket":
return "workspace/repository";
case "azure-devops":
return "project/repository";
case "url":
return "URL";
}
}

function remoteProjectSourceProvider(
source: AddProjectRemoteSource,
): AddProjectRemoteProviderKind | null {
return source === "url" ? null : source;
}

function remoteProjectSourceIcon(source: AddProjectRemoteSource, className: string): ReactNode {
switch (source) {
case "github":
return <GitHubIcon className={className} />;
case "forgejo":
return <ForgejoIcon className={className} />;
case "gitlab":
return <GitLabIcon className={className} />;
case "bitbucket":
return <BitbucketIcon className={className} />;
case "azure-devops":
return <AzureDevOpsIcon className={className} />;
case "gitcafe":
return <GitCafeIcon className={className} />;
case "url":
return <LinkIcon className={className} />;
}
if (source === "url") return <LinkIcon className={className} />;
const Icon = sourceControlIcon(sourceControlClients.get(source));
return <Icon className={className} />;
}

function projectFaviconIcon(project: Project): ReactNode {
Expand All @@ -370,82 +290,26 @@ function remoteProjectInputPlaceholder(flow: AddProjectCloneFlow | null): string
if (flow.source === "url") {
return "Enter Git clone URL";
}
return `Enter ${remoteProjectSourceLabel(flow.source)} repository (${remoteProjectSourcePathHint(flow.source)})`;
}

function sourceProviderKind(source: AddProjectRemoteSource): AddProjectRemoteProviderKind | null {
return source === "url" ? null : source;
}

function sortAddProjectProviderSources(
readinessBySource: AddProjectRemoteSourceReadiness,
): ReadonlyArray<AddProjectRemoteProviderKind> {
return REMOTE_PROJECT_PROVIDER_SOURCES.toSorted((left, right) => {
const leftReady = readinessBySource[left].ready;
const rightReady = readinessBySource[right].ready;
if (leftReady !== rightReady) {
return leftReady ? -1 : 1;
}
return remoteProjectSourceLabel(left).localeCompare(remoteProjectSourceLabel(right));
});
return `Enter ${addProjectRemoteSourceLabel(flow.source)} repository (${addProjectRemoteSourcePathHint(flow.source)})`;
}

type AddProjectRemoteSourceReadiness = Record<
AddProjectRemoteSource,
{ readonly ready: boolean; readonly hint: string | null }
>;

function buildAddProjectRemoteSourceReadiness(
/** The palette's readiness, pointing at Settings by its menu path. */
function buildPaletteRemoteSourceReadiness(
discovery: SourceControlDiscoveryResult | null,
): AddProjectRemoteSourceReadiness {
const unavailable = {
ready: false,
hint: "Provider status unavailable. Open Settings -> Source Control and rescan.",
} as const;
const defaultReadiness: AddProjectRemoteSourceReadiness = {
url: { ready: true, hint: null },
github: unavailable,
gitlab: unavailable,
forgejo: unavailable,
bitbucket: unavailable,
"azure-devops": unavailable,
gitcafe: unavailable,
const readiness = buildAddProjectRemoteSourceReadiness(discovery);
return (source) => {
const entry = readiness(source);
return entry.hint === null
? entry
: {
...entry,
hint: entry.hint.replace(
"Open Source Control settings",
"Open Settings -> Source Control",
),
};
};

if (!discovery) {
return defaultReadiness;
}

const providerByKind = new Map(
discovery.sourceControlProviders.map((provider) => [provider.kind, provider]),
);
const readiness = { ...defaultReadiness };

for (const source of REMOTE_PROJECT_SOURCES) {
const kind = sourceProviderKind(source);
if (!kind) continue;
const provider = providerByKind.get(SourceControlProviderKind.make(kind));
if (!provider) {
readiness[source] = unavailable;
continue;
}
if (provider.status !== "available") {
readiness[source] = { ready: false, hint: provider.installHint };
continue;
}
if (provider.auth.status === "unauthenticated") {
readiness[source] = {
ready: false,
hint:
Option.getOrNull(provider.auth.detail) ??
`${provider.label} is not authenticated. Open Settings -> Source Control for setup guidance.`,
};
continue;
}
readiness[source] = { ready: true, hint: null };
}

return readiness;
}

function errorMessage(error: unknown): string {
Expand Down Expand Up @@ -1673,13 +1537,13 @@ function OpenCommandPaletteDialog(props: {
];

for (const source of orderedSources) {
const label = remoteProjectSourceLabel(source);
const label = addProjectRemoteSourceLabel(source);
const title = source === "url" ? "Git URL" : `${label} repository`;
const description =
source === "url"
? "Clone from a remote URL"
: `Clone ${label} ${remoteProjectSourcePathHint(source)}`;
const readiness = readinessBySource[source];
: `Clone ${label} ${addProjectRemoteSourcePathHint(source)}`;
const readiness = readinessBySource(source);
const disabledHint = readiness.hint;

const titleTrailingContent = readiness.ready ? undefined : (
Expand Down Expand Up @@ -1767,7 +1631,7 @@ function OpenCommandPaletteDialog(props: {
addonIcon: <FolderPlusIcon className={ADDON_ICON_CLASS} />,
groups: buildAddProjectSourceGroups(
environmentId,
buildAddProjectRemoteSourceReadiness(
buildPaletteRemoteSourceReadiness(
browseEnvironmentId === environmentId ? sourceControlDiscovery.data : null,
),
),
Expand Down Expand Up @@ -2105,13 +1969,13 @@ function OpenCommandPaletteDialog(props: {
"repository",
"repo",
"git",
"github",
"gitlab",
"forgejo",
"bitbucket",
"azure",
"devops",
"gitcafe",
// Every host's name, spelled as a word: "azure devops" searches as `azure` and `devops`.
...sourceControlClients.definitions.flatMap((definition) =>
definition.pickerLabel
.toLowerCase()
.split(/[^a-z0-9]+/u)
.filter(Boolean),
),
"url",
"environment",
],
Expand Down Expand Up @@ -2356,7 +2220,7 @@ function OpenCommandPaletteDialog(props: {
currentView.groups[0]?.value === sourceSelectionViewValue
? buildAddProjectSourceGroups(
addProjectEnvironmentId,
buildAddProjectRemoteSourceReadiness(sourceControlDiscovery.data),
buildPaletteRemoteSourceReadiness(sourceControlDiscovery.data),
)
: currentView?.groups[0]?.value === "themes"
? changeThemeItem.groups
Expand Down Expand Up @@ -2606,7 +2470,7 @@ function OpenCommandPaletteDialog(props: {
return;
}

const provider = remoteProjectSourceProvider(addProjectCloneFlow.source);
const provider = addProjectRemoteSourceProvider(addProjectCloneFlow.source);
if (!provider) {
const destinationPath = getCloneDestinationPath(
getDefaultCloneParentPath(addProjectCloneFlow.environmentId),
Expand All @@ -2631,7 +2495,7 @@ function OpenCommandPaletteDialog(props: {
const lookupResult = await lookupRepository({
environmentId: addProjectCloneFlow.environmentId,
input: {
provider: SourceControlProviderKind.make(provider),
provider,
repository: rawRepository,
},
});
Expand Down
15 changes: 5 additions & 10 deletions apps/web/src/components/GitActionsControl.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,8 @@ import type {
VcsStatusResult,
} from "@t3tools/contracts";
import { isTemporaryWorktreeBranch } from "@t3tools/shared/git";
import {
DEFAULT_CHANGE_REQUEST_TERMINOLOGY,
getChangeRequestTerminology,
type ChangeRequestTerminology,
} from "../sourceControlPresentation";
import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients";
import type { ChangeRequestTerminology } from "@t3tools/client-runtime/source-control-clients";

export type GitActionIconName = "commit" | "push" | "pr";

Expand Down Expand Up @@ -68,9 +65,7 @@ export function resolveGitActionResultToastTiming(
function resolveChangeRequestTerminology(
gitStatus: VcsStatusResult | null,
): ChangeRequestTerminology {
return gitStatus?.sourceControlProvider
? getChangeRequestTerminology(gitStatus.sourceControlProvider)
: DEFAULT_CHANGE_REQUEST_TERMINOLOGY;
return sourceControlClients.get(gitStatus?.sourceControlProvider?.kind).changeRequest;
}

export function resolveGitActionProgressPresentation(input: {
Expand Down Expand Up @@ -129,7 +124,7 @@ export function buildGitActionProgressStages(input: {
shouldPushBeforePr?: boolean;
terminology?: ChangeRequestTerminology;
}): string[] {
const terminology = input.terminology ?? DEFAULT_CHANGE_REQUEST_TERMINOLOGY;
const terminology = input.terminology ?? sourceControlClients.get(undefined).changeRequest;
const branchStages = input.featureBranch ? ["Preparing feature ref..."] : [];
const pushStage = input.pushTarget ? `Pushing to ${input.pushTarget}...` : "Pushing...";
const prStages = [
Expand Down Expand Up @@ -406,7 +401,7 @@ export function resolveDefaultBranchActionDialogCopy(input: {
}): DefaultBranchActionDialogCopy {
const branchLabel = input.branchName;
const suffix = ` on "${branchLabel}". You can continue on this ref or create a feature ref and run the same action there.`;
const terminology = input.terminology ?? DEFAULT_CHANGE_REQUEST_TERMINOLOGY;
const terminology = input.terminology ?? sourceControlClients.get(undefined).changeRequest;

if (input.action === "push" || input.action === "commit_push") {
if (input.includesCommit) {
Expand Down
Loading
Loading