feat: add configurable gmail import labels
Agent-Logs-Url: https://github.com/christianlouis/InboxConverge/sessions/4d504045-2740-4bab-b51e-960fe4dafe4b Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
@@ -16,8 +16,112 @@ import {
|
||||
AlertTriangle,
|
||||
XCircle,
|
||||
Bug,
|
||||
RotateCcw,
|
||||
Tags,
|
||||
} from 'lucide-react';
|
||||
|
||||
const DEFAULT_GMAIL_IMPORT_LABEL_TEMPLATES = ['{{source_email}}', 'imported'];
|
||||
|
||||
function parseImportLabelTemplates(input: string): string[] {
|
||||
return input
|
||||
.split('\n')
|
||||
.map((value) => value.trim())
|
||||
.filter((value, index, values) => value.length > 0 && values.indexOf(value) === index);
|
||||
}
|
||||
|
||||
function GmailImportLabelsForm({
|
||||
gmailCredential,
|
||||
onSave,
|
||||
isSaving,
|
||||
}: {
|
||||
gmailCredential: {
|
||||
gmail_email: string;
|
||||
import_label_templates: string[];
|
||||
default_import_label_templates: string[];
|
||||
};
|
||||
onSave: (labels: string[]) => void;
|
||||
isSaving: boolean;
|
||||
}) {
|
||||
const [labelsInput, setLabelsInput] = useState(
|
||||
gmailCredential.import_label_templates.join('\n')
|
||||
);
|
||||
|
||||
const defaultTemplates =
|
||||
gmailCredential.default_import_label_templates.length > 0
|
||||
? gmailCredential.default_import_label_templates
|
||||
: DEFAULT_GMAIL_IMPORT_LABEL_TEMPLATES;
|
||||
const parsedLabels = parseImportLabelTemplates(labelsInput);
|
||||
const isDefaultSelection =
|
||||
parsedLabels.length === defaultTemplates.length &&
|
||||
parsedLabels.every((value, index) => value === defaultTemplates[index]);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<Tags className="h-4 w-4 text-gray-500" />
|
||||
<h3 className="text-sm font-semibold text-gray-900">Import labels</h3>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">
|
||||
One label is created per line. We recommend keeping{' '}
|
||||
<code className="rounded bg-white px-1 py-0.5 text-xs text-gray-700">
|
||||
{'{{source_email}}'}
|
||||
</code>{' '}
|
||||
so each imported message is tagged with the mailbox it came from, plus a
|
||||
catch-all label like <strong>imported</strong>.
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-gray-500">
|
||||
Example: a mail pulled from <strong>billing@example.com</strong> will be
|
||||
labeled as <strong>billing@example.com</strong> when{' '}
|
||||
<code className="rounded bg-white px-1 py-0.5 text-xs text-gray-700">
|
||||
{'{{source_email}}'}
|
||||
</code>{' '}
|
||||
is present.
|
||||
</p>
|
||||
<textarea
|
||||
value={labelsInput}
|
||||
onChange={(e) => setLabelsInput(e.target.value)}
|
||||
rows={4}
|
||||
className="mt-4 w-full rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder={`{{source_email}}\nimported`}
|
||||
/>
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2 text-xs text-gray-500">
|
||||
<span>Suggested defaults:</span>
|
||||
{defaultTemplates.map((label) => (
|
||||
<span
|
||||
key={label}
|
||||
className="rounded-full border border-gray-200 bg-white px-2 py-1 text-gray-700"
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSave(parsedLabels)}
|
||||
disabled={isSaving}
|
||||
className="flex items-center gap-2 rounded-md bg-blue-600 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{isSaving ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
|
||||
Save label setup
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLabelsInput(defaultTemplates.join('\n'))}
|
||||
disabled={isSaving || isDefaultSelection}
|
||||
className="flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm text-gray-700 ring-1 ring-gray-300 transition-colors hover:bg-gray-50 disabled:opacity-50"
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
Reset defaults
|
||||
</button>
|
||||
</div>
|
||||
<p className="mt-3 text-xs text-gray-500">
|
||||
Connected Gmail target: <strong>{gmailCredential.gmail_email}</strong>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SettingsPage() {
|
||||
return (
|
||||
<AuthGuard>
|
||||
@@ -106,6 +210,7 @@ function SettingsContent() {
|
||||
});
|
||||
|
||||
const [debugEmailResult, setDebugEmailResult] = useState<string | null>(null);
|
||||
const [gmailLabelsSaved, setGmailLabelsSaved] = useState(false);
|
||||
const sendDebugEmailMutation = useMutation({
|
||||
mutationFn: gmailApi.sendDebugEmail,
|
||||
onSuccess: () => {
|
||||
@@ -118,6 +223,15 @@ function SettingsContent() {
|
||||
},
|
||||
});
|
||||
|
||||
const updateGmailLabelsMutation = useMutation({
|
||||
mutationFn: gmailApi.updateImportLabels,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['gmail-credential'] });
|
||||
setGmailLabelsSaved(true);
|
||||
setTimeout(() => setGmailLabelsSaved(false), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
const saveSmtpMutation = useMutation({
|
||||
mutationFn: smtpApi.save,
|
||||
onSuccess: () => {
|
||||
@@ -362,7 +476,9 @@ function SettingsContent() {
|
||||
{debugEmailResult === 'success' && (
|
||||
<div className="flex items-center gap-2 text-sm text-green-700 bg-green-50 border border-green-200 rounded-md px-3 py-2">
|
||||
<CheckCircle className="h-4 w-4 flex-shrink-0" />
|
||||
Debug email injected successfully. Check your Gmail inbox — it should be labelled <strong className="mx-1">test</strong> and <strong className="mx-1">imported</strong>.
|
||||
Debug email injected successfully. Check your Gmail inbox for the
|
||||
configured import labels plus a <strong className="mx-1">test</strong>{' '}
|
||||
label.
|
||||
</div>
|
||||
)}
|
||||
{debugEmailResult === 'error' && (
|
||||
@@ -371,6 +487,18 @@ function SettingsContent() {
|
||||
Failed to inject debug email. Check that Gmail API access is still valid.
|
||||
</div>
|
||||
)}
|
||||
<GmailImportLabelsForm
|
||||
key={gmailCredential.updated_at}
|
||||
gmailCredential={gmailCredential}
|
||||
isSaving={updateGmailLabelsMutation.isPending}
|
||||
onSave={(labels) => updateGmailLabelsMutation.mutate(labels)}
|
||||
/>
|
||||
{gmailLabelsSaved && (
|
||||
<div className="flex items-center gap-2 text-sm text-green-700 bg-green-50 border border-green-200 rounded-md px-3 py-2">
|
||||
<CheckCircle className="h-4 w-4 flex-shrink-0" />
|
||||
Gmail import labels saved.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -589,4 +717,3 @@ function SettingsContent() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -141,6 +141,8 @@ export interface GmailCredential {
|
||||
user_id: number;
|
||||
gmail_email: string;
|
||||
is_valid: boolean;
|
||||
import_label_templates: string[];
|
||||
default_import_label_templates: string[];
|
||||
last_verified_at?: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
@@ -336,6 +338,14 @@ export const gmailApi = {
|
||||
const response = await api.post<GmailDebugEmailResponse>('/providers/gmail/debug-email');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/** Update the labels applied to imported Gmail messages. */
|
||||
async updateImportLabels(importLabelTemplates: string[]): Promise<GmailCredential> {
|
||||
const response = await api.put<GmailCredential>('/providers/gmail-credential/labels', {
|
||||
import_label_templates: importLabelTemplates,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
// ── SMTP Config API ─────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user