Tabs Config

This commit is contained in:
Arunavo Ray
2025-06-11 21:43:43 +05:30
parent 8f62da4572
commit bbd49d7d52
8 changed files with 648 additions and 222 deletions

View File

@@ -18,7 +18,7 @@
"@radix-ui/react-select": "^2.2.4",
"@radix-ui/react-slot": "^1.2.2",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.11",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-tooltip": "^1.2.6",
"@tailwindcss/vite": "^4.1.7",
"@tanstack/react-virtual": "^3.13.8",

View File

@@ -16,7 +16,6 @@
"check-db": "bun scripts/manage-db.ts check",
"fix-db": "bun scripts/manage-db.ts fix",
"reset-users": "bun scripts/manage-db.ts reset-users",
"startup-recovery": "bun scripts/startup-recovery.ts",
"startup-recovery-force": "bun scripts/startup-recovery.ts --force",
"test-recovery": "bun scripts/test-recovery.ts",
@@ -46,7 +45,7 @@
"@radix-ui/react-select": "^2.2.4",
"@radix-ui/react-slot": "^1.2.2",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.11",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-tooltip": "^1.2.6",
"@tailwindcss/vite": "^4.1.7",
"@tanstack/react-virtual": "^3.13.8",

View File

@@ -0,0 +1,90 @@
import React from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "../ui/checkbox";
import type { AdvancedOptions } from "@/types/config";
import { RefreshCw } from "lucide-react";
interface AdvancedOptionsFormProps {
config: AdvancedOptions;
setConfig: React.Dispatch<React.SetStateAction<AdvancedOptions>>;
onAutoSave?: (config: AdvancedOptions) => Promise<void>;
isAutoSaving?: boolean;
}
export function AdvancedOptionsForm({
config,
setConfig,
onAutoSave,
isAutoSaving = false,
}: AdvancedOptionsFormProps) {
const handleChange = (name: string, checked: boolean) => {
const newConfig = {
...config,
[name]: checked,
};
setConfig(newConfig);
// Auto-save
if (onAutoSave) {
onAutoSave(newConfig);
}
};
return (
<Card>
<CardHeader>
<CardTitle className="text-lg font-semibold flex items-center justify-between">
Advanced Options
{isAutoSaving && (
<div className="flex items-center text-sm text-muted-foreground">
<RefreshCw className="h-3 w-3 animate-spin mr-1" />
<span className="text-xs">Auto-saving...</span>
</div>
)}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3">
<div className="flex items-center">
<Checkbox
id="skip-forks"
checked={config.skipForks}
onCheckedChange={(checked) =>
handleChange("skipForks", Boolean(checked))
}
/>
<label
htmlFor="skip-forks"
className="ml-2 text-sm select-none"
>
Skip forks
</label>
</div>
<p className="text-xs text-muted-foreground ml-6">
Don't mirror repositories that are forks of other repositories
</p>
<div className="flex items-center">
<Checkbox
id="skip-starred-issues"
checked={config.skipStarredIssues}
onCheckedChange={(checked) =>
handleChange("skipStarredIssues", Boolean(checked))
}
/>
<label
htmlFor="skip-starred-issues"
className="ml-2 text-sm select-none"
>
Don't fetch issues for starred repos
</label>
</div>
<p className="text-xs text-muted-foreground ml-6">
Skip mirroring issues and pull requests for starred repositories
</p>
</div>
</CardContent>
</Card>
);
}

View File

@@ -3,6 +3,9 @@ import { GitHubConfigForm } from './GitHubConfigForm';
import { GiteaConfigForm } from './GiteaConfigForm';
import { ScheduleConfigForm } from './ScheduleConfigForm';
import { DatabaseCleanupConfigForm } from './DatabaseCleanupConfigForm';
import { MirrorOptionsForm } from './MirrorOptionsForm';
import { AdvancedOptionsForm } from './AdvancedOptionsForm';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
import type {
ConfigApiResponse,
GiteaConfig,
@@ -11,6 +14,8 @@ import type {
SaveConfigApiResponse,
ScheduleConfig,
DatabaseCleanupConfig,
MirrorOptions,
AdvancedOptions,
} from '@/types/config';
import { Button } from '../ui/button';
import { useAuth } from '@/hooks/useAuth';
@@ -25,6 +30,8 @@ type ConfigState = {
giteaConfig: GiteaConfig;
scheduleConfig: ScheduleConfig;
cleanupConfig: DatabaseCleanupConfig;
mirrorOptions: MirrorOptions;
advancedOptions: AdvancedOptions;
};
export function ConfigTabs() {
@@ -32,13 +39,8 @@ export function ConfigTabs() {
githubConfig: {
username: '',
token: '',
skipForks: false,
privateRepositories: false,
mirrorIssues: false,
mirrorWiki: false,
mirrorStarred: false,
preserveOrgStructure: false,
skipStarredIssues: false,
},
giteaConfig: {
url: '',
@@ -47,6 +49,7 @@ export function ConfigTabs() {
organization: 'github-mirrors',
visibility: 'public',
starredReposOrg: 'github',
preserveOrgStructure: false,
},
scheduleConfig: {
enabled: false,
@@ -56,6 +59,21 @@ export function ConfigTabs() {
enabled: false,
retentionDays: 604800, // 7 days in seconds
},
mirrorOptions: {
mirrorReleases: false,
mirrorMetadata: false,
metadataComponents: {
issues: false,
pullRequests: false,
labels: false,
milestones: false,
wiki: false,
},
},
advancedOptions: {
skipForks: false,
skipStarredIssues: false,
},
});
const { user } = useAuth();
const [isLoading, setIsLoading] = useState(true);
@@ -65,10 +83,14 @@ export function ConfigTabs() {
const [isAutoSavingCleanup, setIsAutoSavingCleanup] = useState<boolean>(false);
const [isAutoSavingGitHub, setIsAutoSavingGitHub] = useState<boolean>(false);
const [isAutoSavingGitea, setIsAutoSavingGitea] = useState<boolean>(false);
const [isAutoSavingMirrorOptions, setIsAutoSavingMirrorOptions] = useState<boolean>(false);
const [isAutoSavingAdvancedOptions, setIsAutoSavingAdvancedOptions] = useState<boolean>(false);
const autoSaveScheduleTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const autoSaveCleanupTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const autoSaveGitHubTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const autoSaveGiteaTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const autoSaveMirrorOptionsTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const autoSaveAdvancedOptionsTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const isConfigFormValid = (): boolean => {
const { githubConfig, giteaConfig } = config;
@@ -133,6 +155,8 @@ export function ConfigTabs() {
giteaConfig: config.giteaConfig,
scheduleConfig: scheduleConfig,
cleanupConfig: config.cleanupConfig,
mirrorOptions: config.mirrorOptions,
advancedOptions: config.advancedOptions,
};
try {
@@ -197,6 +221,8 @@ export function ConfigTabs() {
giteaConfig: config.giteaConfig,
scheduleConfig: config.scheduleConfig,
cleanupConfig: cleanupConfig,
mirrorOptions: config.mirrorOptions,
advancedOptions: config.advancedOptions,
};
try {
@@ -260,6 +286,8 @@ export function ConfigTabs() {
giteaConfig: config.giteaConfig,
scheduleConfig: config.scheduleConfig,
cleanupConfig: config.cleanupConfig,
mirrorOptions: config.mirrorOptions,
advancedOptions: config.advancedOptions,
};
try {
@@ -307,6 +335,8 @@ export function ConfigTabs() {
giteaConfig: giteaConfig,
scheduleConfig: config.scheduleConfig,
cleanupConfig: config.cleanupConfig,
mirrorOptions: config.mirrorOptions,
advancedOptions: config.advancedOptions,
};
try {
@@ -335,6 +365,104 @@ export function ConfigTabs() {
}, 500); // 500ms debounce
}, [user?.id, config.githubConfig, config.scheduleConfig, config.cleanupConfig]);
// Auto-save function specifically for mirror options changes
const autoSaveMirrorOptions = useCallback(async (mirrorOptions: MirrorOptions) => {
if (!user?.id) return;
// Clear any existing timeout
if (autoSaveMirrorOptionsTimeoutRef.current) {
clearTimeout(autoSaveMirrorOptionsTimeoutRef.current);
}
// Debounce the auto-save to prevent excessive API calls
autoSaveMirrorOptionsTimeoutRef.current = setTimeout(async () => {
setIsAutoSavingMirrorOptions(true);
const reqPayload: SaveConfigApiRequest = {
userId: user.id!,
githubConfig: config.githubConfig,
giteaConfig: config.giteaConfig,
scheduleConfig: config.scheduleConfig,
cleanupConfig: config.cleanupConfig,
mirrorOptions: mirrorOptions,
advancedOptions: config.advancedOptions,
};
try {
const response = await fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(reqPayload),
});
const result: SaveConfigApiResponse = await response.json();
if (result.success) {
// Silent success - no toast for auto-save
// Invalidate config cache so other components get fresh data
invalidateConfigCache();
} else {
showErrorToast(
`Auto-save failed: ${result.message || 'Unknown error'}`,
toast
);
}
} catch (error) {
showErrorToast(error, toast);
} finally {
setIsAutoSavingMirrorOptions(false);
}
}, 500); // 500ms debounce
}, [user?.id, config.githubConfig, config.giteaConfig, config.scheduleConfig, config.cleanupConfig, config.advancedOptions]);
// Auto-save function specifically for advanced options changes
const autoSaveAdvancedOptions = useCallback(async (advancedOptions: AdvancedOptions) => {
if (!user?.id) return;
// Clear any existing timeout
if (autoSaveAdvancedOptionsTimeoutRef.current) {
clearTimeout(autoSaveAdvancedOptionsTimeoutRef.current);
}
// Debounce the auto-save to prevent excessive API calls
autoSaveAdvancedOptionsTimeoutRef.current = setTimeout(async () => {
setIsAutoSavingAdvancedOptions(true);
const reqPayload: SaveConfigApiRequest = {
userId: user.id!,
githubConfig: config.githubConfig,
giteaConfig: config.giteaConfig,
scheduleConfig: config.scheduleConfig,
cleanupConfig: config.cleanupConfig,
mirrorOptions: config.mirrorOptions,
advancedOptions: advancedOptions,
};
try {
const response = await fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(reqPayload),
});
const result: SaveConfigApiResponse = await response.json();
if (result.success) {
// Silent success - no toast for auto-save
// Invalidate config cache so other components get fresh data
invalidateConfigCache();
} else {
showErrorToast(
`Auto-save failed: ${result.message || 'Unknown error'}`,
toast
);
}
} catch (error) {
showErrorToast(error, toast);
} finally {
setIsAutoSavingAdvancedOptions(false);
}
}, 500); // 500ms debounce
}, [user?.id, config.githubConfig, config.giteaConfig, config.scheduleConfig, config.cleanupConfig, config.mirrorOptions]);
// Cleanup timeouts on unmount
useEffect(() => {
return () => {
@@ -350,6 +478,12 @@ export function ConfigTabs() {
if (autoSaveGiteaTimeoutRef.current) {
clearTimeout(autoSaveGiteaTimeoutRef.current);
}
if (autoSaveMirrorOptionsTimeoutRef.current) {
clearTimeout(autoSaveMirrorOptionsTimeoutRef.current);
}
if (autoSaveAdvancedOptionsTimeoutRef.current) {
clearTimeout(autoSaveAdvancedOptionsTimeoutRef.current);
}
};
}, []);
@@ -373,6 +507,10 @@ export function ConfigTabs() {
response.scheduleConfig || config.scheduleConfig,
cleanupConfig:
response.cleanupConfig || config.cleanupConfig,
mirrorOptions:
response.mirrorOptions || config.mirrorOptions,
advancedOptions:
response.advancedOptions || config.advancedOptions,
});
}
@@ -496,72 +634,118 @@ export function ConfigTabs() {
</div>
{/* Content section */}
<div className="flex flex-col gap-y-4">
<div className="flex gap-x-4">
<GitHubConfigForm
config={config.githubConfig}
setConfig={update =>
setConfig(prev => ({
...prev,
githubConfig:
typeof update === 'function'
? update(prev.githubConfig)
: update,
}))
}
onAutoSave={autoSaveGitHubConfig}
isAutoSaving={isAutoSavingGitHub}
/>
<GiteaConfigForm
config={config.giteaConfig}
setConfig={update =>
setConfig(prev => ({
...prev,
giteaConfig:
typeof update === 'function'
? update(prev.giteaConfig)
: update,
}))
}
onAutoSave={autoSaveGiteaConfig}
isAutoSaving={isAutoSavingGitea}
/>
</div>
<div className="flex gap-x-4">
<div className="w-1/2">
<ScheduleConfigForm
config={config.scheduleConfig}
<Tabs defaultValue="connections" className="w-full">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="connections">Connections</TabsTrigger>
<TabsTrigger value="mirror">Mirror Options</TabsTrigger>
<TabsTrigger value="schedule">Schedule & Cleanup</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<TabsContent value="connections" className="mt-6">
<div className="flex gap-x-4">
<GitHubConfigForm
config={config.githubConfig}
setConfig={update =>
setConfig(prev => ({
...prev,
scheduleConfig:
githubConfig:
typeof update === 'function'
? update(prev.scheduleConfig)
? update(prev.githubConfig)
: update,
}))
}
onAutoSave={autoSaveScheduleConfig}
isAutoSaving={isAutoSavingSchedule}
onAutoSave={autoSaveGitHubConfig}
isAutoSaving={isAutoSavingGitHub}
/>
</div>
<div className="w-1/2">
<DatabaseCleanupConfigForm
config={config.cleanupConfig}
<GiteaConfigForm
config={config.giteaConfig}
setConfig={update =>
setConfig(prev => ({
...prev,
cleanupConfig:
giteaConfig:
typeof update === 'function'
? update(prev.cleanupConfig)
? update(prev.giteaConfig)
: update,
}))
}
onAutoSave={autoSaveCleanupConfig}
isAutoSaving={isAutoSavingCleanup}
onAutoSave={autoSaveGiteaConfig}
isAutoSaving={isAutoSavingGitea}
/>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="mirror" className="mt-6">
<MirrorOptionsForm
config={config.mirrorOptions}
setConfig={update =>
setConfig(prev => ({
...prev,
mirrorOptions:
typeof update === 'function'
? update(prev.mirrorOptions)
: update,
}))
}
onAutoSave={autoSaveMirrorOptions}
isAutoSaving={isAutoSavingMirrorOptions}
/>
</TabsContent>
<TabsContent value="schedule" className="mt-6">
<div className="flex gap-x-4">
<div className="w-1/2">
<ScheduleConfigForm
config={config.scheduleConfig}
setConfig={update =>
setConfig(prev => ({
...prev,
scheduleConfig:
typeof update === 'function'
? update(prev.scheduleConfig)
: update,
}))
}
onAutoSave={autoSaveScheduleConfig}
isAutoSaving={isAutoSavingSchedule}
/>
</div>
<div className="w-1/2">
<DatabaseCleanupConfigForm
config={config.cleanupConfig}
setConfig={update =>
setConfig(prev => ({
...prev,
cleanupConfig:
typeof update === 'function'
? update(prev.cleanupConfig)
: update,
}))
}
onAutoSave={autoSaveCleanupConfig}
isAutoSaving={isAutoSavingCleanup}
/>
</div>
</div>
</TabsContent>
<TabsContent value="advanced" className="mt-6">
<AdvancedOptionsForm
config={config.advancedOptions}
setConfig={update =>
setConfig(prev => ({
...prev,
advancedOptions:
typeof update === 'function'
? update(prev.advancedOptions)
: update,
}))
}
onAutoSave={autoSaveAdvancedOptions}
isAutoSaving={isAutoSavingAdvancedOptions}
/>
</TabsContent>
</Tabs>
</div>
);
}

View File

@@ -30,21 +30,6 @@ export function GitHubConfigForm({ config, setConfig, onAutoSave, isAutoSaving }
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value, type, checked } = e.target;
// Special handling for preserveOrgStructure changes
if (
name === "preserveOrgStructure" &&
config.preserveOrgStructure !== checked
) {
toast.info(
"Changing this setting may affect how repositories are accessed in Gitea. " +
"Existing mirrored repositories will still be accessible during sync operations.",
{
duration: 6000,
position: "top-center",
}
);
}
const newConfig = {
...config,
[name]: type === "checkbox" ? checked : value,
@@ -140,32 +125,10 @@ export function GitHubConfigForm({ config, setConfig, onAutoSave, isAutoSaving }
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-3">
<div className="flex items-center">
<Checkbox
id="skip-forks"
name="skipForks"
checked={config.skipForks}
onCheckedChange={(checked) =>
handleChange({
target: {
name: "skipForks",
type: "checkbox",
checked: Boolean(checked),
value: "",
},
} as React.ChangeEvent<HTMLInputElement>)
}
/>
<label
htmlFor="skip-forks"
className="ml-2 block text-sm select-none"
>
Skip Forks
</label>
</div>
<div className="space-y-4">
<h4 className="text-sm font-medium text-foreground">Repository Access</h4>
<div className="space-y-3">
<div className="flex items-center">
<Checkbox
id="private-repositories"
@@ -186,7 +149,7 @@ export function GitHubConfigForm({ config, setConfig, onAutoSave, isAutoSaving }
htmlFor="private-repositories"
className="ml-2 block text-sm select-none"
>
Mirror Private Repos
Include private repos
</label>
</div>
@@ -210,121 +173,7 @@ export function GitHubConfigForm({ config, setConfig, onAutoSave, isAutoSaving }
htmlFor="mirror-starred"
className="ml-2 block text-sm select-none"
>
Mirror Starred Repos
</label>
</div>
</div>
<div className="space-y-3">
<div className="flex items-center">
<Checkbox
id="mirror-issues"
name="mirrorIssues"
checked={config.mirrorIssues}
onCheckedChange={(checked) =>
handleChange({
target: {
name: "mirrorIssues",
type: "checkbox",
checked: Boolean(checked),
value: "",
},
} as React.ChangeEvent<HTMLInputElement>)
}
/>
<label
htmlFor="mirror-issues"
className="ml-2 block text-sm select-none"
>
Mirror Issues
</label>
</div>
<div className="flex items-center">
<Checkbox
id="mirror-wiki"
name="mirrorWiki"
checked={config.mirrorWiki}
onCheckedChange={(checked) =>
handleChange({
target: {
name: "mirrorWiki",
type: "checkbox",
checked: Boolean(checked),
value: "",
},
} as React.ChangeEvent<HTMLInputElement>)
}
/>
<label
htmlFor="mirror-wiki"
className="ml-2 block text-sm select-none"
>
Mirror Wiki
</label>
</div>
<div className="flex items-center">
<Checkbox
id="preserve-org-structure"
name="preserveOrgStructure"
checked={config.preserveOrgStructure}
onCheckedChange={(checked) =>
handleChange({
target: {
name: "preserveOrgStructure",
type: "checkbox",
checked: Boolean(checked),
value: "",
},
} as React.ChangeEvent<HTMLInputElement>)
}
/>
<label
htmlFor="preserve-org-structure"
className="ml-2 text-sm select-none flex items-center"
>
Preserve Org Structure
<Tooltip>
<TooltipTrigger asChild>
<span
className="ml-1 cursor-pointer align-middle text-muted-foreground"
role="button"
tabIndex={0}
>
<Info size={16} />
</span>
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs text-xs">
When enabled, organization repositories will be mirrored to
the same organization structure in Gitea. When disabled, all
repositories will be mirrored under your Gitea username.
</TooltipContent>
</Tooltip>
</label>
</div>
<div className="flex items-center">
<Checkbox
id="skip-starred-issues"
name="skipStarredIssues"
checked={config.skipStarredIssues}
onCheckedChange={(checked) =>
handleChange({
target: {
name: "skipStarredIssues",
type: "checkbox",
checked: Boolean(checked),
value: "",
},
} as React.ChangeEvent<HTMLInputElement>)
}
/>
<label
htmlFor="skip-starred-issues"
className="ml-2 block text-sm select-none"
>
Skip Issues for Starred Repos
Mirror starred repos
</label>
</div>
</div>

View File

@@ -14,9 +14,12 @@ import {
SelectTrigger,
SelectValue,
} from "../ui/select";
import { Checkbox } from "../ui/checkbox";
import { giteaApi } from "@/lib/api";
import type { GiteaConfig, GiteaOrgVisibility } from "@/types/config";
import { toast } from "sonner";
import { Info } from "lucide-react";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
interface GiteaConfigFormProps {
config: GiteaConfig;
@@ -31,10 +34,27 @@ export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving }:
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const { name, value } = e.target;
const { name, value, type } = e.target;
const checked = type === "checkbox" ? (e.target as HTMLInputElement).checked : undefined;
// Special handling for preserveOrgStructure changes
if (
name === "preserveOrgStructure" &&
config.preserveOrgStructure !== checked
) {
toast.info(
"Changing this setting may affect how repositories are accessed in Gitea. " +
"Existing mirrored repositories will still be accessible during sync operations.",
{
duration: 6000,
position: "top-center",
}
);
}
const newConfig = {
...config,
[name]: value,
[name]: type === "checkbox" ? checked : value,
};
setConfig(newConfig);
@@ -153,7 +173,7 @@ export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving }:
htmlFor="organization"
className="block text-sm font-medium mb-1.5"
>
Default Organization (Optional)
Destination organisation (optional)
</label>
<input
id="organization"
@@ -165,10 +185,51 @@ export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving }:
placeholder="Organization name"
/>
<p className="text-xs text-muted-foreground mt-1">
If specified, repositories will be mirrored to this organization.
Repos are created here if no per-repo org is set.
</p>
</div>
<div className="flex items-center">
<Checkbox
id="preserve-org-structure"
name="preserveOrgStructure"
checked={config.preserveOrgStructure}
onCheckedChange={(checked) =>
handleChange({
target: {
name: "preserveOrgStructure",
type: "checkbox",
checked: Boolean(checked),
value: "",
},
} as React.ChangeEvent<HTMLInputElement>)
}
/>
<label
htmlFor="preserve-org-structure"
className="ml-2 text-sm select-none flex items-center"
>
Mirror GitHub org / team hierarchy
<Tooltip>
<TooltipTrigger asChild>
<span
className="ml-1 cursor-pointer align-middle text-muted-foreground"
role="button"
tabIndex={0}
>
<Info size={16} />
</span>
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs text-xs">
Creates nested orgs or prefixes in Gitea so the layout matches GitHub.
When enabled, organization repositories will be mirrored to
the same organization structure in Gitea. When disabled, all
repositories will be mirrored under your Gitea username.
</TooltipContent>
</Tooltip>
</label>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label
@@ -222,7 +283,7 @@ export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving }:
placeholder="github"
/>
<p className="text-xs text-muted-foreground mt-1">
Organization for starred repositories (default: github)
Leave blank to use 'github'.
</p>
</div>
</div>

View File

@@ -0,0 +1,226 @@
import React from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "../ui/checkbox";
import type { MirrorOptions } from "@/types/config";
import { RefreshCw, Info } from "lucide-react";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
interface MirrorOptionsFormProps {
config: MirrorOptions;
setConfig: React.Dispatch<React.SetStateAction<MirrorOptions>>;
onAutoSave?: (config: MirrorOptions) => Promise<void>;
isAutoSaving?: boolean;
}
export function MirrorOptionsForm({
config,
setConfig,
onAutoSave,
isAutoSaving = false,
}: MirrorOptionsFormProps) {
const handleChange = (name: string, checked: boolean) => {
let newConfig = { ...config };
if (name === "mirrorMetadata") {
newConfig.mirrorMetadata = checked;
// If disabling metadata, also disable all components
if (!checked) {
newConfig.metadataComponents = {
issues: false,
pullRequests: false,
labels: false,
milestones: false,
wiki: false,
};
}
} else if (name.startsWith("metadataComponents.")) {
const componentName = name.split(".")[1] as keyof typeof config.metadataComponents;
newConfig.metadataComponents = {
...config.metadataComponents,
[componentName]: checked,
};
} else {
newConfig = {
...config,
[name]: checked,
};
}
setConfig(newConfig);
// Auto-save
if (onAutoSave) {
onAutoSave(newConfig);
}
};
return (
<Card>
<CardHeader>
<CardTitle className="text-lg font-semibold flex items-center justify-between">
Mirror Options
{isAutoSaving && (
<div className="flex items-center text-sm text-muted-foreground">
<RefreshCw className="h-3 w-3 animate-spin mr-1" />
<span className="text-xs">Auto-saving...</span>
</div>
)}
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Repository Content */}
<div className="space-y-4">
<h4 className="text-sm font-medium text-foreground">Repository Content</h4>
<div className="flex items-center">
<Checkbox
id="mirror-releases"
checked={config.mirrorReleases}
onCheckedChange={(checked) =>
handleChange("mirrorReleases", Boolean(checked))
}
/>
<label
htmlFor="mirror-releases"
className="ml-2 text-sm select-none flex items-center"
>
Mirror releases
<Tooltip>
<TooltipTrigger asChild>
<span className="ml-1 cursor-pointer text-muted-foreground">
<Info size={14} />
</span>
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs text-xs">
Include GitHub releases and tags in the mirror
</TooltipContent>
</Tooltip>
</label>
</div>
<div className="flex items-center">
<Checkbox
id="mirror-metadata"
checked={config.mirrorMetadata}
onCheckedChange={(checked) =>
handleChange("mirrorMetadata", Boolean(checked))
}
/>
<label
htmlFor="mirror-metadata"
className="ml-2 text-sm select-none flex items-center"
>
Mirror metadata
<Tooltip>
<TooltipTrigger asChild>
<span className="ml-1 cursor-pointer text-muted-foreground">
<Info size={14} />
</span>
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs text-xs">
Include issues, pull requests, labels, milestones, and wiki
</TooltipContent>
</Tooltip>
</label>
</div>
{/* Metadata Components */}
{config.mirrorMetadata && (
<div className="ml-6 space-y-3 border-l-2 border-muted pl-4">
<h5 className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
Metadata Components
</h5>
<div className="grid grid-cols-1 gap-2">
<div className="flex items-center">
<Checkbox
id="metadata-issues"
checked={config.metadataComponents.issues}
onCheckedChange={(checked) =>
handleChange("metadataComponents.issues", Boolean(checked))
}
disabled={!config.mirrorMetadata}
/>
<label
htmlFor="metadata-issues"
className="ml-2 text-sm select-none"
>
Issues
</label>
</div>
<div className="flex items-center">
<Checkbox
id="metadata-pullRequests"
checked={config.metadataComponents.pullRequests}
onCheckedChange={(checked) =>
handleChange("metadataComponents.pullRequests", Boolean(checked))
}
disabled={!config.mirrorMetadata}
/>
<label
htmlFor="metadata-pullRequests"
className="ml-2 text-sm select-none"
>
Pull requests
</label>
</div>
<div className="flex items-center">
<Checkbox
id="metadata-labels"
checked={config.metadataComponents.labels}
onCheckedChange={(checked) =>
handleChange("metadataComponents.labels", Boolean(checked))
}
disabled={!config.mirrorMetadata}
/>
<label
htmlFor="metadata-labels"
className="ml-2 text-sm select-none"
>
Labels
</label>
</div>
<div className="flex items-center">
<Checkbox
id="metadata-milestones"
checked={config.metadataComponents.milestones}
onCheckedChange={(checked) =>
handleChange("metadataComponents.milestones", Boolean(checked))
}
disabled={!config.mirrorMetadata}
/>
<label
htmlFor="metadata-milestones"
className="ml-2 text-sm select-none"
>
Milestones
</label>
</div>
<div className="flex items-center">
<Checkbox
id="metadata-wiki"
checked={config.metadataComponents.wiki}
onCheckedChange={(checked) =>
handleChange("metadataComponents.wiki", Boolean(checked))
}
disabled={!config.mirrorMetadata}
/>
<label
htmlFor="metadata-wiki"
className="ml-2 text-sm select-none"
>
Wiki
</label>
</div>
</div>
</div>
)}
</div>
</CardContent>
</Card>
);
}

View File

@@ -9,6 +9,7 @@ export interface GiteaConfig {
organization: string;
visibility: GiteaOrgVisibility;
starredReposOrg: string;
preserveOrgStructure: boolean;
}
export interface ScheduleConfig {
@@ -28,12 +29,24 @@ export interface DatabaseCleanupConfig {
export interface GitHubConfig {
username: string;
token: string;
skipForks: boolean;
privateRepositories: boolean;
mirrorIssues: boolean;
mirrorWiki: boolean;
mirrorStarred: boolean;
preserveOrgStructure: boolean;
}
export interface MirrorOptions {
mirrorReleases: boolean;
mirrorMetadata: boolean;
metadataComponents: {
issues: boolean;
pullRequests: boolean;
labels: boolean;
milestones: boolean;
wiki: boolean;
};
}
export interface AdvancedOptions {
skipForks: boolean;
skipStarredIssues: boolean;
}
@@ -43,6 +56,8 @@ export interface SaveConfigApiRequest {
giteaConfig: GiteaConfig;
scheduleConfig: ScheduleConfig;
cleanupConfig: DatabaseCleanupConfig;
mirrorOptions?: MirrorOptions;
advancedOptions?: AdvancedOptions;
}
export interface SaveConfigApiResponse {
@@ -65,6 +80,8 @@ export interface ConfigApiResponse {
giteaConfig: GiteaConfig;
scheduleConfig: ScheduleConfig;
cleanupConfig: DatabaseCleanupConfig;
mirrorOptions?: MirrorOptions;
advancedOptions?: AdvancedOptions;
include: string[];
exclude: string[];
createdAt: Date;