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:
copilot-swe-agent[bot]
2026-03-26 18:20:09 +00:00
parent 5bf4cbc2e9
commit 5711a56569
12 changed files with 405 additions and 12 deletions
+129 -2
View File
@@ -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>
);
}
+10
View File
@@ -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 ─────────────────────────────────────────────────────