diff --git a/bun.lock b/bun.lock index c975194..3feb12d 100644 --- a/bun.lock +++ b/bun.lock @@ -51,6 +51,7 @@ "tw-animate-css": "^1.3.5", "typescript": "^5.8.3", "uuid": "^11.1.0", + "vaul": "^1.1.2", "zod": "^3.25.75", }, "devDependencies": { @@ -1453,6 +1454,8 @@ "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + "vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="], + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], diff --git a/package.json b/package.json index 7f92bee..05e5838 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "tw-animate-css": "^1.3.5", "typescript": "^5.8.3", "uuid": "^11.1.0", + "vaul": "^1.1.2", "zod": "^3.25.75" }, "devDependencies": { diff --git a/src/components/activity/ActivityLog.tsx b/src/components/activity/ActivityLog.tsx index c3e5431..d650d14 100644 --- a/src/components/activity/ActivityLog.tsx +++ b/src/components/activity/ActivityLog.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useState, useRef } from 'react'; import { Button } from '@/components/ui/button'; -import { ChevronDown, Download, RefreshCw, Search, Trash2 } from 'lucide-react'; +import { ChevronDown, Download, RefreshCw, Search, Trash2, Filter } from 'lucide-react'; import { DropdownMenu, DropdownMenuContent, @@ -36,6 +36,16 @@ import { toast } from 'sonner'; import { useLiveRefresh } from '@/hooks/useLiveRefresh'; import { useConfigStatus } from '@/hooks/useConfigStatus'; import { useNavigation } from '@/components/layout/MainLayout'; +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from '@/components/ui/drawer'; type MirrorJobWithKey = MirrorJob & { _rowKey: string }; @@ -343,18 +353,225 @@ export function ActivityLog() { setShowCleanupDialog(false); }; + // Check if any filters are active + const hasActiveFilters = !!(filter.status || filter.type || filter.name); + const activeFilterCount = [filter.status, filter.type, filter.name].filter(Boolean).length; + + // Clear all filters + const clearFilters = () => { + setFilter({ + searchTerm: filter.searchTerm, + status: '', + type: '', + name: '', + }); + }; + /* ------------------------------ UI ------------------------------ */ return (
-
+ {/* Mobile: Search bar with filter and action buttons */} +
+
+
+ + + setFilter((prev) => ({ ...prev, searchTerm: e.target.value })) + } + /> +
+ + {/* Mobile Filter Drawer */} + + + + + + + Filter Activities + + Narrow down your activity log + + + +
+ {/* Active filters summary */} + {hasActiveFilters && ( +
+ + {activeFilterCount} filter{activeFilterCount > 1 ? 's' : ''} active + + +
+ )} + + {/* Status Filter */} +
+ + +
+ + {/* Type Filter */} +
+ + +
+ + {/* Name Filter */} +
+ + setFilter((p) => ({ ...p, name }))} + /> +
+
+ + + + + + + + + +
+
+ + + + + +
+
+ + {/* Desktop: Original layout */} +
{/* search input */} -
- +
+ setFilter((prev) => ({ @@ -365,8 +582,8 @@ export function ActivityLog() { />
- {/* Filter controls row */} -
+ {/* Filter controls */} +
{/* status select */} - {/* type select - hidden on mobile */} + {/* type select */}
- {/* repo/org name combobox - hidden on mobile */} -
- setFilter((p) => ({ ...p, name }))} - /> -
+ {/* repo/org name combobox */} + setFilter((p) => ({ ...p, name }))} + /> - {/* Action buttons row */} -
- {/* export dropdown - text hidden on mobile */} + {/* Action buttons */} +
+ {/* export dropdown */} - - + Export as CSV @@ -443,11 +675,11 @@ export function ActivityLog() { {/* cleanup all activities */} @@ -456,9 +688,9 @@ export function ActivityLog() { size="icon" onClick={handleCleanupClick} title="Delete all activities" - className="text-destructive hover:text-destructive h-8 w-8 sm:h-9 sm:w-9" + className="text-destructive hover:text-destructive h-10 w-10" > - +
@@ -495,6 +727,26 @@ export function ActivityLog() { + + {/* Mobile FAB for Export - only visible on mobile */} + + + + + + + Export as CSV + + + Export as JSON + + +
); } diff --git a/src/components/activity/ActivityNameCombobox.tsx b/src/components/activity/ActivityNameCombobox.tsx index bdac6f7..77d24f2 100644 --- a/src/components/activity/ActivityNameCombobox.tsx +++ b/src/components/activity/ActivityNameCombobox.tsx @@ -41,9 +41,14 @@ export function ActivityNameCombobox({ activities, value, onChange }: ActivityNa variant="outline" role="combobox" aria-expanded={open} - className="w-[180px] justify-between" + className="w-full sm:w-[180px] justify-between h-10" > - {value ? value : "All Names"} + + {value || "All names"} + @@ -62,7 +67,7 @@ export function ActivityNameCombobox({ activities, value, onChange }: ActivityNa }} > - All Names + All names {names.map((name) => ( Automation & Maintenance + + + + + + +
+

Background Operations

+

+ These automated tasks run in the background to keep your mirrors up-to-date and maintain optimal database performance. + Choose intervals that match your workflow and repository update frequency. +

+
+
+
+
@@ -311,21 +330,6 @@ export function AutomationSettings({
- -
-
- -
-

- Background Operations -

-

- These automated tasks run in the background to keep your mirrors up-to-date and maintain optimal database performance. - Choose intervals that match your workflow and repository update frequency. -

-
-
-
); diff --git a/src/components/config/ConfigTabs.tsx b/src/components/config/ConfigTabs.tsx index c75eb5e..d6fdbbf 100644 --- a/src/components/config/ConfigTabs.tsx +++ b/src/components/config/ConfigTabs.tsx @@ -566,14 +566,14 @@ export function ConfigTabs() {

- Configuration Settings + Configuration

Configure your GitHub and Gitea connections, and set up automatic mirroring.

-
+
- +

Fine-tune Your Mirror Destinations

diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 3eea9c1..96e2ef2 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -125,42 +125,24 @@ export function Header({ currentPage, onNavigate, onMenuClick }: HeaderProps) { {isLoading ? ( ) : user ? ( - <> - {/* Desktop: Show avatar and logout button */} -
- - - - {user.username.charAt(0).toUpperCase()} - - - -
- - {/* Mobile: Avatar with dropdown */} -
- - - - - - - - Logout - - - -
- + + + + + Logout + + + ) : ( + + + + Filter Organizations + + Narrow down your organization list + + + +
+ {/* Active filters summary */} + {hasActiveFilters && ( +
+ + {activeFilterCount} filter{activeFilterCount > 1 ? 's' : ''} active + + +
+ )} - {/* Filter controls */} -
- {/* Membership Role Filter */} - + {/* Role Filter */} +
+ + +
- {/* Status Filter */} - -
- - {/* Action buttons */} -
+ {/* Status Filter */} +
+ + +
+
+ + + + + + + + + + + + - +
+ + {/* Desktop: Original layout */} +
+
+ + + setFilter((prev) => ({ ...prev, searchTerm: e.target.value })) + } + /> +
+ + {/* Filter controls */} +
+ {/* Membership Role Filter */} + + + {/* Status Filter */} + +
+ + {/* Action buttons */} +
+ + + +
+
{ + setFilter({ + searchTerm: filter.searchTerm, + status: "", + organization: "", + owner: "", + }); + }; + return (
{/* Search and filters */}
-
- - - setFilter((prev) => ({ ...prev, searchTerm: e.target.value })) - } - /> -
+ {/* Mobile: Search bar with filter button */} +
+
+ + + setFilter((prev) => ({ ...prev, searchTerm: e.target.value })) + } + /> +
+ + {/* Mobile Filter Drawer */} + + + + + + + Filter Repositories + + Narrow down your repository list + + + +
+ {/* Active filters summary */} + {hasActiveFilters && ( +
+ + {activeFilterCount} filter{activeFilterCount > 1 ? 's' : ''} active + + +
+ )} - {/* Owner Combobox */} - - setFilter((prev) => ({ ...prev, owner })) - } - /> + {/* Owner Filter */} +
+ + + setFilter((prev) => ({ ...prev, owner })) + } + /> +
- {/* Organization Combobox */} - - setFilter((prev) => ({ ...prev, organization })) - } - /> - - {/* Filter controls in a responsive row */} -
- + {/* Organization Filter */} +
+ + + setFilter((prev) => ({ ...prev, organization })) + } + /> +
+ {/* Status Filter */} +
+ + +
+
+ + + + + + + + + + + + + + {selectedRepoIds.size === 0 && ( + + )} +
+ + {/* Desktop: Original layout */} +
+
+ + + setFilter((prev) => ({ ...prev, searchTerm: e.target.value })) + } + /> +
+ + {/* Owner Combobox */} + + setFilter((prev) => ({ ...prev, owner })) + } + /> + + {/* Organization Combobox */} + + setFilter((prev) => ({ ...prev, organization })) + } + /> + + {/* Filter controls in a responsive row */} +
+ + + +
- {/* Action buttons - separate row on mobile */} -
+ {/* Action buttons - shows when items are selected or Mirror All on desktop */} +
{selectedRepoIds.size === 0 ? ( ) : ( - <> + <>
{selectedRepoIds.size} selected diff --git a/src/components/repositories/RepositoryComboboxes.tsx b/src/components/repositories/RepositoryComboboxes.tsx index 56f3ae4..c581e54 100644 --- a/src/components/repositories/RepositoryComboboxes.tsx +++ b/src/components/repositories/RepositoryComboboxes.tsx @@ -33,17 +33,22 @@ export function OwnerCombobox({ options, value, onChange, placeholder = "Owner" variant="outline" role="combobox" aria-expanded={open} - className="w-full sm:w-[160px] justify-between" + className="w-full sm:w-[160px] justify-between h-10" > - {value ? value : placeholder} + + {value || "All owners"} + - + - No {placeholder.toLowerCase()} found. + No owners found. - All + All owners {options.map((option) => ( - {value ? value : placeholder} + + {value || "All organizations"} + - + - No {placeholder.toLowerCase()} found. + No organizations found. - All + All organizations {options.map((option) => ( ) { + return +} + +function DrawerTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DrawerPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DrawerClose({ + ...props +}: React.ComponentProps) { + return +} + +function DrawerOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DrawerContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + +
+ {children} + + + ) +} + +function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DrawerTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DrawerDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, +}