feat(integrations): add Git hosting integrations and repo listing
Add support for integrating with external Git hosts (GitLab, Gitea, and Azure DevOps). The backend gains a client to fetch paginated repository lists, normalise base URLs, and surface provider errors. Credentials are loaded from the OS keychain and a Tauri command is exposed for the frontend to list integration repositories. - Implement integration client with pagination, deserialization, and provider-specific handling. - Centralise keychain credential loading and expose listing command. - Update UI to manage integration metadata, persist settings, and save/remove tokens to the OS keychain for cloning and operations.
This commit is contained in:
@@ -6,11 +6,13 @@
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
CircleDashed,
|
||||
CloudCog,
|
||||
Code2,
|
||||
FolderOpen,
|
||||
GitCompare,
|
||||
GitMerge,
|
||||
Languages,
|
||||
KeyRound,
|
||||
Palette,
|
||||
RefreshCw,
|
||||
RotateCw,
|
||||
@@ -29,6 +31,7 @@
|
||||
type ExternalToolKind,
|
||||
type ExternalToolPreset,
|
||||
} from "../externalTools";
|
||||
import { configuredIntegrationCount, defaultGitIntegrationSettings } from "../integrations";
|
||||
import type {
|
||||
AnalyticsSettings,
|
||||
AppAppearance,
|
||||
@@ -37,11 +40,14 @@
|
||||
CustomThemeColors,
|
||||
DetectedExternalTool,
|
||||
ExternalToolsSettings,
|
||||
GitIntegrationSecretUpdate,
|
||||
GitIntegrationSettings,
|
||||
ToolOpenMode,
|
||||
} from "../types";
|
||||
import IntegrationSettingsPage from "./IntegrationSettingsPage.svelte";
|
||||
import SelectMenu from "./SelectMenu.svelte";
|
||||
|
||||
type SettingsPage = "general" | "tools";
|
||||
type SettingsPage = "general" | "integrations" | "tools";
|
||||
|
||||
interface Props {
|
||||
analytics: AnalyticsSettings;
|
||||
@@ -51,11 +57,12 @@
|
||||
language: AppLanguage;
|
||||
autoRefresh: boolean;
|
||||
externalTools: ExternalToolsSettings;
|
||||
integrations: GitIntegrationSettings;
|
||||
detectedTools: DetectedExternalTool[];
|
||||
detectionPending: boolean;
|
||||
detectionUnavailable: boolean;
|
||||
onRefreshDetectedTools: () => void | Promise<void>;
|
||||
onSave: (settings: AnalyticsSettings, theme: AppTheme, appearance: AppAppearance, customTheme: CustomThemeColors, language: AppLanguage, autoRefresh: boolean, externalTools: ExternalToolsSettings) => void;
|
||||
onSave: (settings: AnalyticsSettings, theme: AppTheme, appearance: AppAppearance, customTheme: CustomThemeColors, language: AppLanguage, autoRefresh: boolean, externalTools: ExternalToolsSettings, integrations: GitIntegrationSettings, integrationSecrets: GitIntegrationSecretUpdate[]) => void | Promise<void>;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
@@ -67,6 +74,7 @@
|
||||
language = "en",
|
||||
autoRefresh = true,
|
||||
externalTools,
|
||||
integrations,
|
||||
detectedTools = [],
|
||||
detectionPending = false,
|
||||
detectionUnavailable = false,
|
||||
@@ -77,7 +85,7 @@
|
||||
|
||||
const toolKinds: ExternalToolKind[] = ["editor", "diff", "merge", "terminal", "fileManager"];
|
||||
|
||||
let activePage = $state<SettingsPage>("tools");
|
||||
let activePage = $state<SettingsPage>("integrations");
|
||||
let activeToolKind = $state<ExternalToolKind>("editor");
|
||||
let advancedOpen = $state(false);
|
||||
let analyticsEnabled = $state(true);
|
||||
@@ -87,6 +95,9 @@
|
||||
let selectedLanguage = $state<AppLanguage>("en");
|
||||
let autoRefreshEnabled = $state(true);
|
||||
let tools = $state<ExternalToolsSettings>(defaultExternalToolsSettings());
|
||||
let integrationDraft = $state<GitIntegrationSettings>(defaultGitIntegrationSettings());
|
||||
let integrationSecretUpdates = $state<GitIntegrationSecretUpdate[]>([]);
|
||||
let saving = $state(false);
|
||||
const isGerman = $derived(selectedLanguage === "de");
|
||||
|
||||
$effect(() => {
|
||||
@@ -97,14 +108,21 @@
|
||||
selectedLanguage = language;
|
||||
autoRefreshEnabled = autoRefresh;
|
||||
tools = structuredClone(externalTools);
|
||||
integrationDraft = structuredClone(integrations);
|
||||
});
|
||||
|
||||
function save() {
|
||||
onSave({
|
||||
...analytics,
|
||||
enabled: analyticsEnabled,
|
||||
noticeSeen: true,
|
||||
}, selectedTheme, selectedAppearance, $state.snapshot(customColors), selectedLanguage, autoRefreshEnabled, $state.snapshot(tools));
|
||||
async function save() {
|
||||
if (saving) return;
|
||||
saving = true;
|
||||
try {
|
||||
await onSave({
|
||||
...analytics,
|
||||
enabled: analyticsEnabled,
|
||||
noticeSeen: true,
|
||||
}, selectedTheme, selectedAppearance, $state.snapshot(customColors), selectedLanguage, autoRefreshEnabled, $state.snapshot(tools), $state.snapshot(integrationDraft), $state.snapshot(integrationSecretUpdates));
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
function resetCustomColors() {
|
||||
@@ -315,10 +333,22 @@
|
||||
</span>
|
||||
{#if !detectionUnavailable}<em>{detectedTools.length}</em>{/if}
|
||||
</button>
|
||||
<button type="button" class:active={activePage === "integrations"} onclick={() => { activePage = "integrations"; }}>
|
||||
<CloudCog size={16} aria-hidden="true" />
|
||||
<span>
|
||||
<strong>{isGerman ? "Integrationen" : "Integrations"}</strong>
|
||||
<small>GitLab, Azure DevOps & Gitea</small>
|
||||
</span>
|
||||
<em>{configuredIntegrationCount(integrationDraft)}</em>
|
||||
</button>
|
||||
|
||||
<div class="settings-nav-note">
|
||||
<ShieldCheck size={15} aria-hidden="true" />
|
||||
<p>{isGerman ? "Tool-Argumente werden direkt und ohne zusätzliche Shell übergeben." : "Tool arguments are passed directly without an extra shell."}</p>
|
||||
{#if activePage === "integrations"}<KeyRound size={15} aria-hidden="true" />{:else}<ShieldCheck size={15} aria-hidden="true" />{/if}
|
||||
<p>
|
||||
{activePage === "integrations"
|
||||
? (isGerman ? "Tokens werden sicher im Schlüsselbund des Betriebssystems gespeichert." : "Tokens are stored securely in the operating system keychain.")
|
||||
: (isGerman ? "Tool-Argumente werden direkt und ohne zusätzliche Shell übergeben." : "Tool arguments are passed directly without an extra shell.")}
|
||||
</p>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -399,7 +429,7 @@
|
||||
</label>
|
||||
</section>
|
||||
</div>
|
||||
{:else}
|
||||
{:else if activePage === "tools"}
|
||||
<div class="settings-page-head tools-page-head">
|
||||
<div>
|
||||
<h3>{isGerman ? "Externe Tools" : "External tools"}</h3>
|
||||
@@ -518,6 +548,19 @@
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
{:else}
|
||||
<div class="settings-page-head">
|
||||
<div>
|
||||
<h3>{isGerman ? "Integrationen" : "Integrations"}</h3>
|
||||
<p>{isGerman ? "Verbinde Gitty mit deinen Git-Hosting-Diensten." : "Connect Gitty to your Git hosting services."}</p>
|
||||
</div>
|
||||
</div>
|
||||
<IntegrationSettingsPage
|
||||
language={selectedLanguage}
|
||||
settings={integrationDraft}
|
||||
onChange={(next) => { integrationDraft = next; }}
|
||||
onSecretsChange={(updates) => { integrationSecretUpdates = updates; }}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -526,7 +569,7 @@
|
||||
<span>{isGerman ? "Änderungen werden erst beim Speichern übernommen." : "Changes are applied only after saving."}</span>
|
||||
<div>
|
||||
<button class="btn-secondary" type="button" onclick={onClose}>{isGerman ? "Abbrechen" : "Cancel"}</button>
|
||||
<button class="btn-primary" type="submit"><Check size={16} aria-hidden="true" />{isGerman ? "Änderungen speichern" : "Save changes"}</button>
|
||||
<button class="btn-primary" type="submit" disabled={saving}><Check size={16} aria-hidden="true" />{saving ? (isGerman ? "Wird gespeichert…" : "Saving…") : (isGerman ? "Änderungen speichern" : "Save changes")}</button>
|
||||
</div>
|
||||
</footer>
|
||||
</form>
|
||||
@@ -663,7 +706,8 @@
|
||||
.app-settings-head { min-height: 58px; padding: 10px 12px; }
|
||||
.app-settings-mark { width: 34px; height: 34px; }
|
||||
.settings-nav button small, .settings-nav button em { display: none; }
|
||||
.settings-nav > button { grid-template-columns: auto minmax(0, 1fr); }
|
||||
.settings-nav > button { grid-template-columns: auto minmax(0, 1fr); gap: 5px; padding-inline: 6px; }
|
||||
.settings-nav button strong { font-size: 10px; }
|
||||
.settings-page-head { align-items: stretch; flex-direction: column; }
|
||||
.tool-rescan-button { align-self: flex-start; }
|
||||
.general-settings-grid { grid-template-columns: 1fr; }
|
||||
|
||||
Reference in New Issue
Block a user