feat: route-centric rules, approve popup, emoji picker, 5-column sync table
- Schema: SyncRouteRuleMap junction table, ruleIntent on TenantRule, ForwardFormat/Frequency/ApprovalMode columns on SyncRoute, INTEREST match type - Worker: remove global AUTO_APPROVE forwarding; per-route rule evaluation in ingest processor — POSITIVE rules auto-forward that route, NEGATIVE rules block it, fallback to approvalMode - API: PATCH /routes/:id, rule assign/unassign endpoints, approve now accepts targetGroupIds, new GET /messages/:id/routes for popup data - Web: 5-column route table with settings + rules dialogs, ForwardTarget popup with rule-based pre-selection, ruleIntent toggle + emoji dropdown (👍/👎 only) + INTEREST type in rules manager Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
type ForwardFormat = 'THREADED' | 'DIGEST' | 'SUMMARY';
|
||||
type ForwardFrequency = 'INSTANT' | 'FIVE_MIN' | 'FIFTEEN_MIN' | 'ONE_HOUR' | 'ONE_DAY' | 'SEVEN_DAYS';
|
||||
type ApprovalMode = 'MANUAL' | 'AUTO';
|
||||
|
||||
interface Route {
|
||||
id: string;
|
||||
forwardFormat: ForwardFormat;
|
||||
withAttachment: boolean;
|
||||
frequency: ForwardFrequency;
|
||||
approvalMode: ApprovalMode;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
const FORMAT_OPTIONS: { value: ForwardFormat; label: string }[] = [
|
||||
{ value: 'THREADED', label: 'Threaded' },
|
||||
{ value: 'DIGEST', label: 'Digest' },
|
||||
{ value: 'SUMMARY', label: 'Summary' },
|
||||
];
|
||||
|
||||
const FREQ_OPTIONS: { value: ForwardFrequency; label: string }[] = [
|
||||
{ value: 'INSTANT', label: 'Instant' },
|
||||
{ value: 'FIVE_MIN', label: '5 min' },
|
||||
{ value: 'FIFTEEN_MIN', label: '15 min' },
|
||||
{ value: 'ONE_HOUR', label: '1 hour' },
|
||||
{ value: 'ONE_DAY', label: '1 day' },
|
||||
{ value: 'SEVEN_DAYS', label: '7 days' },
|
||||
];
|
||||
|
||||
export function RouteSettingsDialog({
|
||||
route,
|
||||
onSaved,
|
||||
onClose,
|
||||
}: {
|
||||
route: Route;
|
||||
onSaved: (updated: Partial<Route>) => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [forwardFormat, setForwardFormat] = useState<ForwardFormat>(route.forwardFormat);
|
||||
const [withAttachment, setWithAttachment] = useState(route.withAttachment);
|
||||
const [frequency, setFrequency] = useState<ForwardFrequency>(route.frequency);
|
||||
const [approvalMode, setApprovalMode] = useState<ApprovalMode>(route.approvalMode);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
async function save() {
|
||||
setBusy(true);
|
||||
setError('');
|
||||
try {
|
||||
const res = await fetch(`/api/routes/${route.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ forwardFormat, withAttachment, frequency, approvalMode }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
setError(err.message ?? 'Failed to save');
|
||||
return;
|
||||
}
|
||||
onSaved({ forwardFormat, withAttachment, frequency, approvalMode });
|
||||
onClose();
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-sm mx-4 p-5 flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-base font-semibold">Route settings</h2>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 text-lg leading-none">×</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs text-gray-500">Forward format</label>
|
||||
<select
|
||||
value={forwardFormat}
|
||||
onChange={(e) => setForwardFormat(e.target.value as ForwardFormat)}
|
||||
className="border border-gray-300 rounded px-3 py-2 text-sm"
|
||||
>
|
||||
{FORMAT_OPTIONS.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={withAttachment}
|
||||
onChange={(e) => setWithAttachment(e.target.checked)}
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
Include attachments
|
||||
</label>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs text-gray-500">Frequency</label>
|
||||
<select
|
||||
value={frequency}
|
||||
onChange={(e) => setFrequency(e.target.value as ForwardFrequency)}
|
||||
className="border border-gray-300 rounded px-3 py-2 text-sm"
|
||||
>
|
||||
{FREQ_OPTIONS.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs text-gray-500">Approval mode</label>
|
||||
<div className="flex gap-2">
|
||||
{(['MANUAL', 'AUTO'] as ApprovalMode[]).map((m) => (
|
||||
<button
|
||||
key={m}
|
||||
type="button"
|
||||
onClick={() => setApprovalMode(m)}
|
||||
className={`flex-1 rounded px-3 py-2 text-sm font-medium border transition-colors ${
|
||||
approvalMode === m
|
||||
? 'bg-blue-600 text-white border-blue-600'
|
||||
: 'border-gray-300 text-gray-600 hover:border-gray-400'
|
||||
}`}
|
||||
>
|
||||
{m === 'MANUAL' ? 'Manual' : 'Auto'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <p className="text-sm text-red-600">{error}</p>}
|
||||
|
||||
<div className="flex justify-end gap-2 pt-1">
|
||||
<button onClick={onClose} className="px-4 py-2 text-sm text-gray-600 hover:text-gray-800">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={() => void save()}
|
||||
disabled={busy}
|
||||
className="px-4 py-2 rounded bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{busy ? 'Saving…' : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user