mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-31 19:15:17 +02:00
Merge branch 'dev' of github.com:rowboatlabs/rowboat into dev
This commit is contained in:
commit
728857da1d
15 changed files with 1292 additions and 206 deletions
|
|
@ -43,6 +43,7 @@
|
|||
"motion": "^12.23.26",
|
||||
"nanoid": "^5.1.6",
|
||||
"posthog-js": "^1.332.0",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"sonner": "^2.0.7",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import * as React from "react"
|
||||
import { useState, useEffect, useCallback } from "react"
|
||||
import { Loader2, Mic, Mail, MessageSquare } from "lucide-react"
|
||||
import { AlertTriangle, Loader2, Mic, Mail, MessageSquare } from "lucide-react"
|
||||
|
||||
import {
|
||||
Popover,
|
||||
|
|
@ -28,17 +28,28 @@ interface ProviderState {
|
|||
isConnecting: boolean
|
||||
}
|
||||
|
||||
interface ProviderStatus {
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface ConnectorsPopoverProps {
|
||||
children: React.ReactNode
|
||||
tooltip?: string
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
|
||||
export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenChange }: ConnectorsPopoverProps) {
|
||||
const [openInternal, setOpenInternal] = useState(false)
|
||||
const isControlled = typeof openProp === "boolean"
|
||||
const open = isControlled ? openProp : openInternal
|
||||
const setOpen = onOpenChange ?? setOpenInternal
|
||||
const [providers, setProviders] = useState<string[]>([])
|
||||
const [providersLoading, setProvidersLoading] = useState(true)
|
||||
const [providerStates, setProviderStates] = useState<Record<string, ProviderState>>({})
|
||||
const [providerStatus, setProviderStatus] = useState<Record<string, ProviderStatus>>({})
|
||||
const [googleClientIdOpen, setGoogleClientIdOpen] = useState(false)
|
||||
const [googleClientIdDescription, setGoogleClientIdDescription] = useState<string | undefined>(undefined)
|
||||
|
||||
// Granola state
|
||||
const [granolaEnabled, setGranolaEnabled] = useState(false)
|
||||
|
|
@ -184,25 +195,35 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
|
|||
|
||||
const newStates: Record<string, ProviderState> = {}
|
||||
|
||||
await Promise.all(
|
||||
providers.map(async (provider) => {
|
||||
try {
|
||||
const result = await window.ipc.invoke('oauth:is-connected', { provider })
|
||||
newStates[provider] = {
|
||||
isConnected: result.isConnected,
|
||||
isLoading: false,
|
||||
isConnecting: false,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to check connection status for ${provider}:`, error)
|
||||
newStates[provider] = {
|
||||
isConnected: false,
|
||||
isLoading: false,
|
||||
isConnecting: false,
|
||||
}
|
||||
try {
|
||||
const result = await window.ipc.invoke('oauth:getState', null)
|
||||
const config = result.config || {}
|
||||
const statusMap: Record<string, ProviderStatus> = {}
|
||||
|
||||
for (const provider of providers) {
|
||||
const providerConfig = config[provider]
|
||||
newStates[provider] = {
|
||||
isConnected: providerConfig?.connected ?? false,
|
||||
isLoading: false,
|
||||
isConnecting: false,
|
||||
}
|
||||
})
|
||||
)
|
||||
if (providerConfig?.error) {
|
||||
statusMap[provider] = { error: providerConfig.error }
|
||||
}
|
||||
}
|
||||
|
||||
setProviderStatus(statusMap)
|
||||
} catch (error) {
|
||||
console.error('Failed to check connection statuses:', error)
|
||||
for (const provider of providers) {
|
||||
newStates[provider] = {
|
||||
isConnected: false,
|
||||
isLoading: false,
|
||||
isConnecting: false,
|
||||
}
|
||||
}
|
||||
setProviderStatus({})
|
||||
}
|
||||
|
||||
setProviderStates(newStates)
|
||||
}, [providers, refreshGranolaConfig, refreshSlackStatus])
|
||||
|
|
@ -302,6 +323,7 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
|
|||
// Connect to a provider
|
||||
const handleConnect = useCallback(async (provider: string) => {
|
||||
if (provider === 'google') {
|
||||
setGoogleClientIdDescription(undefined)
|
||||
const existingClientId = getGoogleClientId()
|
||||
if (!existingClientId) {
|
||||
setGoogleClientIdOpen(true)
|
||||
|
|
@ -317,6 +339,7 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
|
|||
const handleGoogleClientIdSubmit = useCallback((clientId: string) => {
|
||||
setGoogleClientId(clientId)
|
||||
setGoogleClientIdOpen(false)
|
||||
setGoogleClientIdDescription(undefined)
|
||||
startConnect('google', clientId)
|
||||
}, [startConnect])
|
||||
|
||||
|
|
@ -361,6 +384,10 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
|
|||
}
|
||||
}, [])
|
||||
|
||||
const hasProviderError = Object.values(providerStatus).some(
|
||||
(status) => Boolean(status?.error)
|
||||
)
|
||||
|
||||
// Helper to render an OAuth provider row
|
||||
const renderOAuthProvider = (provider: string, displayName: string, icon: React.ReactNode, description: string) => {
|
||||
const state = providerStates[provider] || {
|
||||
|
|
@ -368,6 +395,7 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
|
|||
isLoading: true,
|
||||
isConnecting: false,
|
||||
}
|
||||
const needsReconnect = Boolean(providerStatus[provider]?.error)
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -382,6 +410,8 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
|
|||
<span className="text-sm font-medium truncate">{displayName}</span>
|
||||
{state.isLoading ? (
|
||||
<span className="text-xs text-muted-foreground">Checking...</span>
|
||||
) : needsReconnect ? (
|
||||
<span className="text-xs text-amber-600">Needs reconnect</span>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground truncate">{description}</span>
|
||||
)}
|
||||
|
|
@ -390,6 +420,24 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
|
|||
<div className="shrink-0">
|
||||
{state.isLoading ? (
|
||||
<Loader2 className="size-4 animate-spin text-muted-foreground" />
|
||||
) : needsReconnect ? (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (provider === 'google') {
|
||||
setGoogleClientIdDescription(
|
||||
"To keep your Google account connected, please re-enter your client ID. You only need to do this once."
|
||||
)
|
||||
setGoogleClientIdOpen(true)
|
||||
return
|
||||
}
|
||||
startConnect(provider)
|
||||
}}
|
||||
className="h-7 px-2 text-xs"
|
||||
>
|
||||
Reconnect
|
||||
</Button>
|
||||
) : state.isConnected ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
@ -423,9 +471,15 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
|
|||
<>
|
||||
<GoogleClientIdModal
|
||||
open={googleClientIdOpen}
|
||||
onOpenChange={setGoogleClientIdOpen}
|
||||
onOpenChange={(nextOpen) => {
|
||||
setGoogleClientIdOpen(nextOpen)
|
||||
if (!nextOpen) {
|
||||
setGoogleClientIdDescription(undefined)
|
||||
}
|
||||
}}
|
||||
onSubmit={handleGoogleClientIdSubmit}
|
||||
isSubmitting={providerStates.google?.isConnecting ?? false}
|
||||
description={googleClientIdDescription}
|
||||
/>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
{tooltip ? (
|
||||
|
|
@ -451,7 +505,12 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
|
|||
className="w-80 p-0"
|
||||
>
|
||||
<div className="p-4 border-b">
|
||||
<h4 className="font-semibold text-sm">Connectors</h4>
|
||||
<h4 className="font-semibold text-sm flex items-center gap-1.5">
|
||||
Connected accounts
|
||||
{hasProviderError && (
|
||||
<AlertTriangle className="size-3 text-amber-500/80 animate-pulse" />
|
||||
)}
|
||||
</h4>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Connect accounts to sync data
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ interface GoogleClientIdModalProps {
|
|||
onOpenChange: (open: boolean) => void
|
||||
onSubmit: (clientId: string) => void
|
||||
isSubmitting?: boolean
|
||||
description?: string
|
||||
}
|
||||
|
||||
export function GoogleClientIdModal({
|
||||
|
|
@ -26,6 +27,7 @@ export function GoogleClientIdModal({
|
|||
onOpenChange,
|
||||
onSubmit,
|
||||
isSubmitting = false,
|
||||
description,
|
||||
}: GoogleClientIdModalProps) {
|
||||
const [clientId, setClientId] = useState("")
|
||||
|
||||
|
|
@ -49,7 +51,7 @@ export function GoogleClientIdModal({
|
|||
<DialogHeader>
|
||||
<DialogTitle>Enter Google Client ID</DialogTitle>
|
||||
<DialogDescription>
|
||||
This app does not store the client ID. You will be prompted each session.
|
||||
{description ?? "Enter the client ID for your Google OAuth app to continue."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2">
|
||||
|
|
|
|||
|
|
@ -325,25 +325,26 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
|||
|
||||
const newStates: Record<string, ProviderState> = {}
|
||||
|
||||
await Promise.all(
|
||||
providers.map(async (provider) => {
|
||||
try {
|
||||
const result = await window.ipc.invoke('oauth:is-connected', { provider })
|
||||
newStates[provider] = {
|
||||
isConnected: result.isConnected,
|
||||
isLoading: false,
|
||||
isConnecting: false,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to check connection status for ${provider}:`, error)
|
||||
newStates[provider] = {
|
||||
isConnected: false,
|
||||
isLoading: false,
|
||||
isConnecting: false,
|
||||
}
|
||||
try {
|
||||
const result = await window.ipc.invoke('oauth:getState', null)
|
||||
const config = result.config || {}
|
||||
for (const provider of providers) {
|
||||
newStates[provider] = {
|
||||
isConnected: config[provider]?.connected ?? false,
|
||||
isLoading: false,
|
||||
isConnecting: false,
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check connection status for providers:', error)
|
||||
for (const provider of providers) {
|
||||
newStates[provider] = {
|
||||
isConnected: false,
|
||||
isLoading: false,
|
||||
isConnecting: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setProviderStates(newStates)
|
||||
}, [providers, refreshGranolaConfig, refreshSlackStatus])
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
Copy,
|
||||
FilePlus,
|
||||
FolderPlus,
|
||||
AlertTriangle,
|
||||
HelpCircle,
|
||||
Mic,
|
||||
Network,
|
||||
|
|
@ -34,6 +35,17 @@ import {
|
|||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Sidebar,
|
||||
|
|
@ -379,6 +391,45 @@ export function SidebarContentPanel({
|
|||
...props
|
||||
}: SidebarContentPanelProps) {
|
||||
const { activeSection, setActiveSection } = useSidebarSection()
|
||||
const [hasOauthError, setHasOauthError] = useState(false)
|
||||
const [showOauthAlert, setShowOauthAlert] = useState(true)
|
||||
const [connectorsOpen, setConnectorsOpen] = useState(false)
|
||||
const [openConnectorsAfterClose, setOpenConnectorsAfterClose] = useState(false)
|
||||
const connectorsButtonRef = useRef<HTMLButtonElement | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true
|
||||
|
||||
const refreshOauthError = async () => {
|
||||
try {
|
||||
const result = await window.ipc.invoke('oauth:getState', null)
|
||||
const config = result.config || {}
|
||||
const hasError = Object.values(config).some((entry) => Boolean(entry?.error))
|
||||
if (mounted) {
|
||||
setHasOauthError(hasError)
|
||||
if (!hasError) {
|
||||
setShowOauthAlert(true)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch OAuth state:', error)
|
||||
if (mounted) {
|
||||
setHasOauthError(false)
|
||||
setShowOauthAlert(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refreshOauthError()
|
||||
const cleanup = window.ipc.on('oauth:didConnect', () => {
|
||||
refreshOauthError()
|
||||
})
|
||||
|
||||
return () => {
|
||||
mounted = false
|
||||
cleanup()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Sidebar className="border-r-0" {...props}>
|
||||
|
|
@ -430,12 +481,69 @@ export function SidebarContentPanel({
|
|||
{/* Bottom actions */}
|
||||
<div className="border-t border-sidebar-border px-2 py-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<ConnectorsPopover>
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1 text-xs text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground transition-colors">
|
||||
<Plug className="size-4" />
|
||||
<span>Connectors</span>
|
||||
</button>
|
||||
</ConnectorsPopover>
|
||||
<div className="flex items-center gap-2">
|
||||
<ConnectorsPopover open={connectorsOpen} onOpenChange={setConnectorsOpen}>
|
||||
<button
|
||||
ref={connectorsButtonRef}
|
||||
className="flex w-full items-center gap-2 rounded-md px-2 py-1 text-xs text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground transition-colors"
|
||||
>
|
||||
<Plug className="size-4" />
|
||||
<span>Connected accounts</span>
|
||||
</button>
|
||||
</ConnectorsPopover>
|
||||
{hasOauthError && (
|
||||
<AlertDialog
|
||||
open={showOauthAlert}
|
||||
onOpenChange={setShowOauthAlert}
|
||||
>
|
||||
<AlertDialogTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center"
|
||||
aria-label="OAuth connection issues"
|
||||
>
|
||||
<AlertTriangle className="size-3 text-amber-500/90 animate-pulse" />
|
||||
</button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent
|
||||
onCloseAutoFocus={(event) => {
|
||||
event.preventDefault()
|
||||
if (openConnectorsAfterClose) {
|
||||
setOpenConnectorsAfterClose(false)
|
||||
setConnectorsOpen(true)
|
||||
}
|
||||
connectorsButtonRef.current?.focus()
|
||||
}}
|
||||
>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Reconnect your accounts</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
One or more connected accounts need attention. Open Connected accounts
|
||||
to review the status and reconnect if needed.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel
|
||||
onClick={() => {
|
||||
setOpenConnectorsAfterClose(false)
|
||||
setShowOauthAlert(false)
|
||||
}}
|
||||
>
|
||||
Dismiss
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => {
|
||||
setOpenConnectorsAfterClose(true)
|
||||
setShowOauthAlert(false)
|
||||
}}
|
||||
>
|
||||
View connected accounts
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
</div>
|
||||
<SettingsDialog>
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1 text-xs text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground transition-colors">
|
||||
<Settings className="size-4" />
|
||||
|
|
|
|||
194
apps/x/apps/renderer/src/components/ui/alert-dialog.tsx
Normal file
194
apps/x/apps/renderer/src/components/ui/alert-dialog.tsx
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
import * as React from "react"
|
||||
import { AlertDialog as AlertDialogPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
function AlertDialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
|
||||
}
|
||||
|
||||
function AlertDialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
data-slot="alert-dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogContent({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Content> & {
|
||||
size?: "default" | "sm"
|
||||
}) {
|
||||
return (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
data-slot="alert-dialog-content"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 group/alert-dialog-content fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogHeader({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-header"
|
||||
className={cn(
|
||||
"grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-6 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogFooter({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Title
|
||||
data-slot="alert-dialog-title"
|
||||
className={cn(
|
||||
"text-lg font-semibold sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Description
|
||||
data-slot="alert-dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogMedia({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-media"
|
||||
className={cn(
|
||||
"bg-muted mb-2 inline-flex size-16 items-center justify-center rounded-md sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogAction({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Action> &
|
||||
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
|
||||
return (
|
||||
<Button variant={variant} size={size} asChild>
|
||||
<AlertDialogPrimitive.Action
|
||||
data-slot="alert-dialog-action"
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogCancel({
|
||||
className,
|
||||
variant = "outline",
|
||||
size = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel> &
|
||||
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
|
||||
return (
|
||||
<Button variant={variant} size={size} asChild>
|
||||
<AlertDialogPrimitive.Cancel
|
||||
data-slot="alert-dialog-cancel"
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogMedia,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogPortal,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
|
|
@ -22,9 +22,11 @@ const buttonVariants = cva(
|
|||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
},
|
||||
|
|
@ -46,7 +48,7 @@ function Button({
|
|||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const Comp = asChild ? Slot.Root : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
|
|
|
|||
|
|
@ -12,8 +12,9 @@ export function useOAuth(provider: string) {
|
|||
const checkConnection = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const result = await window.ipc.invoke('oauth:is-connected', { provider });
|
||||
setIsConnected(result.isConnected);
|
||||
const result = await window.ipc.invoke('oauth:getState', null);
|
||||
const config = result.config || {};
|
||||
setIsConnected(config[provider]?.connected ?? false);
|
||||
} catch (error) {
|
||||
console.error('Failed to check connection status:', error);
|
||||
setIsConnected(false);
|
||||
|
|
@ -107,8 +108,12 @@ export function useConnectedProviders() {
|
|||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const result = await window.ipc.invoke('oauth:get-connected-providers', null);
|
||||
setProviders(result.providers);
|
||||
const result = await window.ipc.invoke('oauth:getState', null);
|
||||
const config = result.config || {};
|
||||
const connected = Object.entries(config)
|
||||
.filter(([, value]) => value?.connected)
|
||||
.map(([key]) => key);
|
||||
setProviders(connected);
|
||||
} catch (error) {
|
||||
console.error('Failed to get connected providers:', error);
|
||||
setProviders([]);
|
||||
|
|
@ -149,4 +154,3 @@ export function useAvailableProviders() {
|
|||
|
||||
return { providers, isLoading };
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue