Add GitHub repository support to integrations and update related tests. Implement server-side GitHub API calls and normalize GitHub base URLs. Improve the repository browser UI with compact tabs and updated icons. Add a custom, accessible scrollbar with pointer and keyboard support. - Add paging, auth headers, and error handling when listing GitHub repos. - Default GitHub token username to "x-access-token" when saving credentials. - Introduce compact repo tab styles, new icons, and a draggable scrollbar.
170 lines
5.6 KiB
TypeScript
170 lines
5.6 KiB
TypeScript
import type {
|
|
AzureDevOpsOrganization,
|
|
GitIntegrationConfig,
|
|
GitIntegrationProvider,
|
|
GitIntegrationSource,
|
|
GitIntegrationSettings,
|
|
} from "./types";
|
|
|
|
export const gitIntegrationProviders: GitIntegrationProvider[] = [
|
|
"github",
|
|
"gitlab",
|
|
"gitlab-self-hosted",
|
|
"azure-devops",
|
|
"gitea",
|
|
];
|
|
|
|
const defaults: Record<GitIntegrationProvider, Omit<GitIntegrationConfig, "provider">> = {
|
|
github: {
|
|
enabled: false,
|
|
baseUrl: "https://github.com",
|
|
username: "",
|
|
tokenStored: false,
|
|
},
|
|
gitlab: {
|
|
enabled: false,
|
|
baseUrl: "https://gitlab.com",
|
|
username: "",
|
|
tokenStored: false,
|
|
},
|
|
"gitlab-self-hosted": {
|
|
enabled: false,
|
|
baseUrl: "",
|
|
username: "",
|
|
tokenStored: false,
|
|
},
|
|
"azure-devops": {
|
|
enabled: false,
|
|
baseUrl: "https://dev.azure.com/",
|
|
username: "",
|
|
tokenStored: false,
|
|
},
|
|
gitea: {
|
|
enabled: false,
|
|
baseUrl: "",
|
|
username: "",
|
|
tokenStored: false,
|
|
},
|
|
};
|
|
|
|
export function defaultGitIntegrationSettings(): GitIntegrationSettings {
|
|
return {
|
|
providers: Object.fromEntries(
|
|
gitIntegrationProviders.map((provider) => [provider, { provider, ...defaults[provider] }]),
|
|
) as GitIntegrationSettings["providers"],
|
|
azureDevOpsOrganizations: [],
|
|
};
|
|
}
|
|
|
|
function normaliseAzureOrganization(value: unknown): AzureDevOpsOrganization | null {
|
|
if (!value || typeof value !== "object") return null;
|
|
const stored = value as Partial<AzureDevOpsOrganization>;
|
|
const id = typeof stored.id === "string" && /^[a-zA-Z0-9_-]{1,80}$/.test(stored.id) ? stored.id : "";
|
|
if (!id) return null;
|
|
return {
|
|
id,
|
|
name: typeof stored.name === "string" ? stored.name : "",
|
|
enabled: stored.enabled === true,
|
|
baseUrl: typeof stored.baseUrl === "string" ? stored.baseUrl : "",
|
|
username: typeof stored.username === "string" ? stored.username : "",
|
|
tokenStored: stored.tokenStored === true,
|
|
};
|
|
}
|
|
|
|
export function normaliseGitIntegrationSettings(value: unknown): GitIntegrationSettings {
|
|
const fallback = defaultGitIntegrationSettings();
|
|
if (!value || typeof value !== "object") return fallback;
|
|
const storedProviders = (value as Partial<GitIntegrationSettings>).providers;
|
|
if (!storedProviders || typeof storedProviders !== "object") return fallback;
|
|
|
|
for (const provider of gitIntegrationProviders) {
|
|
const stored = storedProviders[provider] as Partial<GitIntegrationConfig> | undefined;
|
|
if (!stored || typeof stored !== "object") continue;
|
|
fallback.providers[provider] = {
|
|
provider,
|
|
enabled: stored.enabled === true,
|
|
baseUrl: typeof stored.baseUrl === "string" ? stored.baseUrl : fallback.providers[provider].baseUrl,
|
|
username: typeof stored.username === "string" ? stored.username : "",
|
|
tokenStored: stored.tokenStored === true,
|
|
};
|
|
}
|
|
|
|
const storedOrganizations = (value as Partial<GitIntegrationSettings>).azureDevOpsOrganizations;
|
|
if (Array.isArray(storedOrganizations)) {
|
|
const seen = new Set<string>();
|
|
fallback.azureDevOpsOrganizations = storedOrganizations
|
|
.map(normaliseAzureOrganization)
|
|
.filter((organization): organization is AzureDevOpsOrganization => {
|
|
if (!organization || seen.has(organization.id)) return false;
|
|
seen.add(organization.id);
|
|
return true;
|
|
});
|
|
} else {
|
|
const legacy = fallback.providers["azure-devops"];
|
|
const hasLegacyConfiguration = legacy.enabled || legacy.tokenStored || legacy.username.trim().length > 0 || !/^https:\/\/dev\.azure\.com\/?$/i.test(legacy.baseUrl.trim());
|
|
if (hasLegacyConfiguration) {
|
|
fallback.azureDevOpsOrganizations = [{
|
|
id: "default",
|
|
name: "Azure DevOps",
|
|
enabled: legacy.enabled,
|
|
baseUrl: legacy.baseUrl,
|
|
username: legacy.username,
|
|
tokenStored: legacy.tokenStored,
|
|
}];
|
|
}
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
export function integrationCredentialKey(provider: GitIntegrationProvider, accountId?: string): string {
|
|
if (provider === "azure-devops" && accountId && accountId !== "default") {
|
|
return `integration:${provider}:${accountId}`;
|
|
}
|
|
return `integration:${provider}`;
|
|
}
|
|
|
|
export function configuredIntegrationSources(settings: GitIntegrationSettings): GitIntegrationSource[] {
|
|
const sources: GitIntegrationSource[] = [];
|
|
for (const provider of gitIntegrationProviders) {
|
|
if (provider === "azure-devops") continue;
|
|
const config = settings.providers[provider];
|
|
if (config.enabled && config.tokenStored && config.baseUrl.trim()) {
|
|
sources.push({ id: provider, provider, label: providerLabel(provider), baseUrl: config.baseUrl });
|
|
}
|
|
}
|
|
for (const organization of settings.azureDevOpsOrganizations) {
|
|
if (!organization.enabled || !organization.tokenStored || !organization.baseUrl.trim()) continue;
|
|
sources.push({
|
|
id: `azure-devops:${organization.id}`,
|
|
provider: "azure-devops",
|
|
accountId: organization.id,
|
|
label: organization.name.trim() || organizationNameFromUrl(organization.baseUrl) || "Azure DevOps",
|
|
baseUrl: organization.baseUrl,
|
|
});
|
|
}
|
|
return sources;
|
|
}
|
|
|
|
export function configuredIntegrationCount(settings: GitIntegrationSettings): number {
|
|
return configuredIntegrationSources(settings).length;
|
|
}
|
|
|
|
export function providerLabel(provider: GitIntegrationProvider): string {
|
|
return {
|
|
github: "GitHub",
|
|
gitlab: "GitLab.com",
|
|
"gitlab-self-hosted": "GitLab Self-Managed",
|
|
"azure-devops": "Azure DevOps",
|
|
gitea: "Gitea",
|
|
}[provider];
|
|
}
|
|
|
|
export function organizationNameFromUrl(value: string): string {
|
|
try {
|
|
const url = new URL(value);
|
|
return url.pathname.split("/").filter(Boolean)[0] ?? "";
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|