improve gmail sync: email input, permanent errors, auto-dock
- add isync to setup.sh - ask for gmail address alongside app password in integrations - add PermanentError to job queue (skips retries for non-recoverable failures) - use PermanentError for missing credentials, missing executable, auth failures - auto-add /email to dock after successful gmail sync - invalidate dock cache on sync completion for seamless UI update Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+61
-23
@@ -3,6 +3,8 @@ import { toast } from 'sonner';
|
||||
import { RefreshCw, Loader2, CheckCircle2, XCircle } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { useAuth } from 'hooks/useAuth';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useJobs } from 'hooks/useJobs';
|
||||
|
||||
type GoogleStatus = {
|
||||
@@ -20,8 +22,17 @@ const formatTime = (ts: number | string) => {
|
||||
|
||||
export const GoogleAccount = () => {
|
||||
const client = useClient();
|
||||
const { user } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [status, setStatus] = useState<GoogleStatus>({ connected: false, email: null, picture: null, configured: false, hasAppPassword: false });
|
||||
const [status, setStatus] = useState<GoogleStatus>({
|
||||
connected: false,
|
||||
email: null,
|
||||
picture: null,
|
||||
configured: false,
|
||||
hasAppPassword: false,
|
||||
});
|
||||
const [gmailEmail, setGmailEmail] = useState('');
|
||||
const [appPassword, setAppPassword] = useState('');
|
||||
const [showPasswordInput, setShowPasswordInput] = useState(false);
|
||||
const [savingPassword, setSavingPassword] = useState(false);
|
||||
@@ -34,7 +45,10 @@ export const GoogleAccount = () => {
|
||||
const fetchStatus = () => {
|
||||
client
|
||||
.get<GoogleStatus>('/integrations/google/status')
|
||||
.then(setStatus)
|
||||
.then((s) => {
|
||||
setStatus(s);
|
||||
setGmailEmail(s.email ?? user?.email ?? '');
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setIsLoading(false));
|
||||
client
|
||||
@@ -57,10 +71,14 @@ export const GoogleAccount = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Refresh sync status from DB when a job finishes
|
||||
// Refresh sync status and dock when a job finishes
|
||||
useEffect(() => {
|
||||
if (!activeJob && lastJob?.status === 'completed') {
|
||||
client.get<{ lastSyncAt: string | null }>('/email/sync-status').then((res) => setLastSyncAt(res.lastSyncAt)).catch(() => {});
|
||||
client
|
||||
.get<{ lastSyncAt: string | null }>('/email/sync-status')
|
||||
.then((res) => setLastSyncAt(res.lastSyncAt))
|
||||
.catch(() => {});
|
||||
queryClient.invalidateQueries({ queryKey: ['DOCK'] });
|
||||
}
|
||||
}, [activeJob, lastJob?.status]);
|
||||
|
||||
@@ -93,11 +111,14 @@ export const GoogleAccount = () => {
|
||||
};
|
||||
|
||||
const handleSaveAppPassword = async () => {
|
||||
if (!appPassword.trim()) return;
|
||||
if (!appPassword.trim() || !gmailEmail.trim()) return;
|
||||
setSavingPassword(true);
|
||||
try {
|
||||
await client.put('/integrations/google/app-password', { appPassword: appPassword.trim() });
|
||||
setStatus({ ...status, hasAppPassword: true });
|
||||
await client.put('/integrations/google/app-password', {
|
||||
appPassword: appPassword.trim(),
|
||||
email: gmailEmail.trim(),
|
||||
});
|
||||
setStatus({ ...status, hasAppPassword: true, email: gmailEmail.trim() });
|
||||
setAppPassword('');
|
||||
setShowPasswordInput(false);
|
||||
toast.success('App password saved');
|
||||
@@ -130,6 +151,9 @@ export const GoogleAccount = () => {
|
||||
{status.hasAppPassword && !showPasswordInput ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-green-600 dark:text-green-400">Configured</span>
|
||||
{status.email && (
|
||||
<span className="text-xs text-duck-dark/50 dark:text-foreground/50">({status.email})</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setShowPasswordInput(true)}
|
||||
className="text-xs text-duck-dark/50 dark:text-foreground/50 underline hover:opacity-70 cursor-pointer"
|
||||
@@ -158,23 +182,32 @@ export const GoogleAccount = () => {
|
||||
<li>Copy the 16-character password and paste it below</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="grid gap-2">
|
||||
<input
|
||||
type="password"
|
||||
value={appPassword}
|
||||
onChange={(ev) => setAppPassword(ev.target.value)}
|
||||
placeholder="xxxx xxxx xxxx xxxx"
|
||||
className="flex-1 h-9 rounded-md border border-duck-dark/10 dark:border-foreground/10 bg-transparent px-3 text-sm"
|
||||
type="email"
|
||||
value={gmailEmail}
|
||||
onChange={(ev) => setGmailEmail(ev.target.value)}
|
||||
placeholder="your@gmail.com"
|
||||
className="h-9 rounded-md border border-duck-dark/10 dark:border-foreground/10 bg-transparent px-3 text-sm"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={!appPassword.trim() || savingPassword}
|
||||
onClick={handleSaveAppPassword}
|
||||
className="h-9 cursor-pointer"
|
||||
>
|
||||
{savingPassword ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Save'}
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="password"
|
||||
value={appPassword}
|
||||
onChange={(ev) => setAppPassword(ev.target.value)}
|
||||
placeholder="xxxx xxxx xxxx xxxx"
|
||||
className="flex-1 h-9 rounded-md border border-duck-dark/10 dark:border-foreground/10 bg-transparent px-3 text-sm"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={!appPassword.trim() || !gmailEmail.trim() || savingPassword}
|
||||
onClick={handleSaveAppPassword}
|
||||
className="h-9 cursor-pointer"
|
||||
>
|
||||
{savingPassword ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Save'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -250,7 +283,12 @@ export const GoogleAccount = () => {
|
||||
<p className="text-xs text-duck-dark/50 dark:text-foreground/50 truncate">{status.email}</p>
|
||||
</div>
|
||||
{status.picture && (
|
||||
<img src={status.picture} alt="" className="h-9 w-9 rounded-full shrink-0" referrerPolicy="no-referrer" />
|
||||
<img
|
||||
src={status.picture}
|
||||
alt=""
|
||||
className="h-9 w-9 rounded-full shrink-0"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Button type="button" variant="outline" onClick={handleDisconnect} className="w-full h-11 cursor-pointer">
|
||||
|
||||
Reference in New Issue
Block a user