Seth commited on
Commit ·
51ee8a8
1
Parent(s): 42babfe
update
Browse files
frontend/src/components/campaigns/CampaignSequenceBuilder.jsx
CHANGED
|
@@ -11,13 +11,6 @@ import {
|
|
| 11 |
X,
|
| 12 |
} from 'lucide-react';
|
| 13 |
import { cn } from '@/lib/utils';
|
| 14 |
-
import {
|
| 15 |
-
Select,
|
| 16 |
-
SelectContent,
|
| 17 |
-
SelectItem,
|
| 18 |
-
SelectTrigger,
|
| 19 |
-
SelectValue,
|
| 20 |
-
} from '@/components/ui/select';
|
| 21 |
|
| 22 |
function uid() {
|
| 23 |
return typeof crypto !== 'undefined' && crypto.randomUUID
|
|
@@ -209,9 +202,8 @@ function ActionCard({
|
|
| 209 |
senderHint,
|
| 210 |
accountHint,
|
| 211 |
emailAccountHint,
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
onEmailMailboxChange,
|
| 215 |
}) {
|
| 216 |
const sender = (senderHint || '').trim();
|
| 217 |
const account = (accountHint || '').trim();
|
|
@@ -219,9 +211,7 @@ function ActionCard({
|
|
| 219 |
const showLiMeta = step.channel === 'linkedin' && (showSender || account);
|
| 220 |
const mailbox = (emailAccountHint || '').trim();
|
| 221 |
const showEmailMeta = step.channel === 'gmail' && !!mailbox;
|
| 222 |
-
const
|
| 223 |
-
const showEmailMailboxSelect =
|
| 224 |
-
step.channel === 'gmail' && mailAccounts.length > 1 && typeof onEmailMailboxChange === 'function';
|
| 225 |
return (
|
| 226 |
<div className="relative w-full max-w-md">
|
| 227 |
<div className="flex w-full items-center gap-3 rounded-xl border border-slate-200 bg-white px-4 py-3 text-left shadow-sm">
|
|
@@ -243,39 +233,26 @@ function ActionCard({
|
|
| 243 |
{account ? <span>Profile: {account}</span> : null}
|
| 244 |
</p>
|
| 245 |
) : null}
|
| 246 |
-
{
|
| 247 |
-
<div
|
| 248 |
-
className="mt-2 space-y-1"
|
| 249 |
-
onClick={(e) => e.stopPropagation()}
|
| 250 |
-
onPointerDown={(e) => e.stopPropagation()}
|
| 251 |
-
>
|
| 252 |
-
<p className="text-[10px] font-medium uppercase tracking-wide text-slate-500">
|
| 253 |
-
Mailbox
|
| 254 |
-
</p>
|
| 255 |
-
<Select
|
| 256 |
-
value={String(emailMailboxSelectValue ?? mailAccounts[0]?.id ?? '')}
|
| 257 |
-
onValueChange={(v) => onEmailMailboxChange(Number(v))}
|
| 258 |
-
>
|
| 259 |
-
<SelectTrigger className="h-9 w-full max-w-[240px] border-slate-200 text-xs">
|
| 260 |
-
<SelectValue placeholder="Choose mailbox" />
|
| 261 |
-
</SelectTrigger>
|
| 262 |
-
<SelectContent>
|
| 263 |
-
{mailAccounts.map((a) => {
|
| 264 |
-
const label = (a.display_name || a.label || 'Mailbox').trim();
|
| 265 |
-
return (
|
| 266 |
-
<SelectItem key={a.id} value={String(a.id)}>
|
| 267 |
-
{label}
|
| 268 |
-
</SelectItem>
|
| 269 |
-
);
|
| 270 |
-
})}
|
| 271 |
-
</SelectContent>
|
| 272 |
-
</Select>
|
| 273 |
-
</div>
|
| 274 |
-
) : showEmailMeta ? (
|
| 275 |
<p className="mt-1 text-[11px] leading-snug text-violet-700">Mailbox: {mailbox}</p>
|
| 276 |
) : null}
|
| 277 |
</div>
|
| 278 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
</div>
|
| 280 |
{onRemove ? (
|
| 281 |
<button
|
|
@@ -439,12 +416,26 @@ export default function CampaignSequenceBuilder({ value, onChange, linkedinDefau
|
|
| 439 |
? step.unipile_account_ref_id ?? defaultMailRef
|
| 440 |
: null;
|
| 441 |
const mailIdSet = new Set(mailAccts.map((a) => Number(a.id)));
|
| 442 |
-
const
|
| 443 |
effMailRef != null && mailIdSet.has(Number(effMailRef))
|
| 444 |
? Number(effMailRef)
|
| 445 |
: mailAccts[0]?.id != null
|
| 446 |
? Number(mailAccts[0].id)
|
| 447 |
: null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
return (
|
| 449 |
<React.Fragment key={step.id}>
|
| 450 |
{step.type === 'wait' ? (
|
|
@@ -487,15 +478,10 @@ export default function CampaignSequenceBuilder({ value, onChange, linkedinDefau
|
|
| 487 |
)
|
| 488 |
: ''
|
| 489 |
}
|
| 490 |
-
|
| 491 |
-
emailMailboxSelectValue={
|
| 492 |
-
step.channel === 'gmail' ? emailSelectValue : null
|
| 493 |
-
}
|
| 494 |
-
onEmailMailboxChange={
|
| 495 |
step.channel === 'gmail' && mailAccts.length > 1
|
| 496 |
-
? (id) => updateStepMailbox(index, id)
|
| 497 |
-
: undefined
|
| 498 |
}
|
|
|
|
| 499 |
onRemove={() => removeAt(index)}
|
| 500 |
/>
|
| 501 |
)}
|
|
|
|
| 11 |
X,
|
| 12 |
} from 'lucide-react';
|
| 13 |
import { cn } from '@/lib/utils';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
function uid() {
|
| 16 |
return typeof crypto !== 'undefined' && crypto.randomUUID
|
|
|
|
| 202 |
senderHint,
|
| 203 |
accountHint,
|
| 204 |
emailAccountHint,
|
| 205 |
+
mailboxChevronCycles,
|
| 206 |
+
onMailboxChevronClick,
|
|
|
|
| 207 |
}) {
|
| 208 |
const sender = (senderHint || '').trim();
|
| 209 |
const account = (accountHint || '').trim();
|
|
|
|
| 211 |
const showLiMeta = step.channel === 'linkedin' && (showSender || account);
|
| 212 |
const mailbox = (emailAccountHint || '').trim();
|
| 213 |
const showEmailMeta = step.channel === 'gmail' && !!mailbox;
|
| 214 |
+
const chevronSwitchable = mailboxChevronCycles && typeof onMailboxChevronClick === 'function';
|
|
|
|
|
|
|
| 215 |
return (
|
| 216 |
<div className="relative w-full max-w-md">
|
| 217 |
<div className="flex w-full items-center gap-3 rounded-xl border border-slate-200 bg-white px-4 py-3 text-left shadow-sm">
|
|
|
|
| 233 |
{account ? <span>Profile: {account}</span> : null}
|
| 234 |
</p>
|
| 235 |
) : null}
|
| 236 |
+
{showEmailMeta ? (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
<p className="mt-1 text-[11px] leading-snug text-violet-700">Mailbox: {mailbox}</p>
|
| 238 |
) : null}
|
| 239 |
</div>
|
| 240 |
+
{chevronSwitchable ? (
|
| 241 |
+
<button
|
| 242 |
+
type="button"
|
| 243 |
+
className="shrink-0 rounded-lg p-1 text-slate-400 transition hover:bg-violet-50 hover:text-violet-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-300"
|
| 244 |
+
aria-label="Switch mailbox"
|
| 245 |
+
title="Switch mailbox"
|
| 246 |
+
onClick={(e) => {
|
| 247 |
+
e.stopPropagation();
|
| 248 |
+
onMailboxChevronClick();
|
| 249 |
+
}}
|
| 250 |
+
>
|
| 251 |
+
<ChevronRight className="h-5 w-5" aria-hidden />
|
| 252 |
+
</button>
|
| 253 |
+
) : (
|
| 254 |
+
<ChevronRight className="h-5 w-5 shrink-0 text-slate-300" aria-hidden />
|
| 255 |
+
)}
|
| 256 |
</div>
|
| 257 |
{onRemove ? (
|
| 258 |
<button
|
|
|
|
| 416 |
? step.unipile_account_ref_id ?? defaultMailRef
|
| 417 |
: null;
|
| 418 |
const mailIdSet = new Set(mailAccts.map((a) => Number(a.id)));
|
| 419 |
+
const effectiveMailboxId =
|
| 420 |
effMailRef != null && mailIdSet.has(Number(effMailRef))
|
| 421 |
? Number(effMailRef)
|
| 422 |
: mailAccts[0]?.id != null
|
| 423 |
? Number(mailAccts[0].id)
|
| 424 |
: null;
|
| 425 |
+
const cycleMailbox =
|
| 426 |
+
step.type === 'action' &&
|
| 427 |
+
step.channel === 'gmail' &&
|
| 428 |
+
mailAccts.length > 1 &&
|
| 429 |
+
effectiveMailboxId != null
|
| 430 |
+
? () => {
|
| 431 |
+
const ids = mailAccts.map((a) => Number(a.id));
|
| 432 |
+
const cur = effectiveMailboxId;
|
| 433 |
+
const idx = ids.indexOf(cur);
|
| 434 |
+
const from = idx >= 0 ? idx : 0;
|
| 435 |
+
const nextId = ids[(from + 1) % ids.length];
|
| 436 |
+
updateStepMailbox(index, nextId);
|
| 437 |
+
}
|
| 438 |
+
: undefined;
|
| 439 |
return (
|
| 440 |
<React.Fragment key={step.id}>
|
| 441 |
{step.type === 'wait' ? (
|
|
|
|
| 478 |
)
|
| 479 |
: ''
|
| 480 |
}
|
| 481 |
+
mailboxChevronCycles={
|
|
|
|
|
|
|
|
|
|
|
|
|
| 482 |
step.channel === 'gmail' && mailAccts.length > 1
|
|
|
|
|
|
|
| 483 |
}
|
| 484 |
+
onMailboxChevronClick={cycleMailbox}
|
| 485 |
onRemove={() => removeAt(index)}
|
| 486 |
/>
|
| 487 |
)}
|
frontend/src/components/layout/AppShell.jsx
CHANGED
|
@@ -34,9 +34,12 @@ export default function AppShell({ title, subtitle, rightContent, children }) {
|
|
| 34 |
|
| 35 |
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
|
| 36 |
try {
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
| 38 |
} catch {
|
| 39 |
-
return
|
| 40 |
}
|
| 41 |
});
|
| 42 |
|
|
@@ -49,9 +52,9 @@ export default function AppShell({ title, subtitle, rightContent, children }) {
|
|
| 49 |
}, [sidebarCollapsed]);
|
| 50 |
|
| 51 |
return (
|
| 52 |
-
<div className="flex
|
| 53 |
{/* Single full-width rule under branding + page title */}
|
| 54 |
-
<header className="
|
| 55 |
<div className="flex min-h-[4.25rem] items-stretch">
|
| 56 |
<div
|
| 57 |
className={cn(
|
|
@@ -109,10 +112,10 @@ export default function AppShell({ title, subtitle, rightContent, children }) {
|
|
| 109 |
</nav>
|
| 110 |
</header>
|
| 111 |
|
| 112 |
-
<div className="flex min-h-0 flex-1">
|
| 113 |
<aside
|
| 114 |
className={cn(
|
| 115 |
-
'hidden md:flex min-h-0 shrink-0 flex-col
|
| 116 |
sidebarCollapsed ? 'w-16 items-stretch px-2' : 'w-72 px-4'
|
| 117 |
)}
|
| 118 |
>
|
|
@@ -125,6 +128,7 @@ export default function AppShell({ title, subtitle, rightContent, children }) {
|
|
| 125 |
{NAV_ITEMS.map((item) => {
|
| 126 |
const Icon = item.icon;
|
| 127 |
const active = pathMatches(location.pathname, item.href);
|
|
|
|
| 128 |
return (
|
| 129 |
<Link
|
| 130 |
to={item.href}
|
|
@@ -135,7 +139,7 @@ export default function AppShell({ title, subtitle, rightContent, children }) {
|
|
| 135 |
sidebarCollapsed
|
| 136 |
? 'w-12 justify-center px-0'
|
| 137 |
: 'items-center gap-3 px-3',
|
| 138 |
-
|
| 139 |
? 'bg-violet-100 text-violet-700'
|
| 140 |
: 'text-slate-700 hover:bg-slate-50'
|
| 141 |
)}
|
|
@@ -143,7 +147,7 @@ export default function AppShell({ title, subtitle, rightContent, children }) {
|
|
| 143 |
<div
|
| 144 |
className={cn(
|
| 145 |
'flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border',
|
| 146 |
-
|
| 147 |
? 'border-violet-200 bg-white text-violet-600'
|
| 148 |
: 'border-slate-200 bg-white text-slate-500'
|
| 149 |
)}
|
|
@@ -154,7 +158,7 @@ export default function AppShell({ title, subtitle, rightContent, children }) {
|
|
| 154 |
<span
|
| 155 |
className={cn(
|
| 156 |
'text-base font-medium',
|
| 157 |
-
|
| 158 |
)}
|
| 159 |
>
|
| 160 |
{item.label}
|
|
@@ -187,7 +191,7 @@ export default function AppShell({ title, subtitle, rightContent, children }) {
|
|
| 187 |
</div>
|
| 188 |
</aside>
|
| 189 |
|
| 190 |
-
<div className="min-w-0 flex-1 overflow-auto">
|
| 191 |
<main className="mx-auto w-full min-w-0 max-w-none px-4 sm:px-5 md:px-6 lg:px-8 xl:px-10 2xl:px-12 py-6 md:py-8">
|
| 192 |
{children}
|
| 193 |
</main>
|
|
|
|
| 34 |
|
| 35 |
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
|
| 36 |
try {
|
| 37 |
+
if (typeof window === 'undefined') return true;
|
| 38 |
+
const v = localStorage.getItem(SIDEBAR_COLLAPSED_KEY);
|
| 39 |
+
if (v === null || v === '') return true;
|
| 40 |
+
return v === '1';
|
| 41 |
} catch {
|
| 42 |
+
return true;
|
| 43 |
}
|
| 44 |
});
|
| 45 |
|
|
|
|
| 52 |
}, [sidebarCollapsed]);
|
| 53 |
|
| 54 |
return (
|
| 55 |
+
<div className="flex h-[100dvh] max-h-[100dvh] flex-col overflow-hidden bg-gradient-to-br from-slate-50 via-white to-violet-50">
|
| 56 |
{/* Single full-width rule under branding + page title */}
|
| 57 |
+
<header className="z-40 flex shrink-0 flex-col border-b border-slate-200 bg-white/80 backdrop-blur-sm">
|
| 58 |
<div className="flex min-h-[4.25rem] items-stretch">
|
| 59 |
<div
|
| 60 |
className={cn(
|
|
|
|
| 112 |
</nav>
|
| 113 |
</header>
|
| 114 |
|
| 115 |
+
<div className="flex min-h-0 flex-1 overflow-hidden">
|
| 116 |
<aside
|
| 117 |
className={cn(
|
| 118 |
+
'hidden md:flex h-full min-h-0 shrink-0 flex-col border-r border-slate-200 bg-white py-4 transition-[width] duration-200 ease-out',
|
| 119 |
sidebarCollapsed ? 'w-16 items-stretch px-2' : 'w-72 px-4'
|
| 120 |
)}
|
| 121 |
>
|
|
|
|
| 128 |
{NAV_ITEMS.map((item) => {
|
| 129 |
const Icon = item.icon;
|
| 130 |
const active = pathMatches(location.pathname, item.href);
|
| 131 |
+
const activeHighlight = active && !sidebarCollapsed;
|
| 132 |
return (
|
| 133 |
<Link
|
| 134 |
to={item.href}
|
|
|
|
| 139 |
sidebarCollapsed
|
| 140 |
? 'w-12 justify-center px-0'
|
| 141 |
: 'items-center gap-3 px-3',
|
| 142 |
+
activeHighlight
|
| 143 |
? 'bg-violet-100 text-violet-700'
|
| 144 |
: 'text-slate-700 hover:bg-slate-50'
|
| 145 |
)}
|
|
|
|
| 147 |
<div
|
| 148 |
className={cn(
|
| 149 |
'flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border',
|
| 150 |
+
activeHighlight
|
| 151 |
? 'border-violet-200 bg-white text-violet-600'
|
| 152 |
: 'border-slate-200 bg-white text-slate-500'
|
| 153 |
)}
|
|
|
|
| 158 |
<span
|
| 159 |
className={cn(
|
| 160 |
'text-base font-medium',
|
| 161 |
+
activeHighlight ? 'text-violet-700' : 'text-slate-700'
|
| 162 |
)}
|
| 163 |
>
|
| 164 |
{item.label}
|
|
|
|
| 191 |
</div>
|
| 192 |
</aside>
|
| 193 |
|
| 194 |
+
<div className="min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-hidden">
|
| 195 |
<main className="mx-auto w-full min-w-0 max-w-none px-4 sm:px-5 md:px-6 lg:px-8 xl:px-10 2xl:px-12 py-6 md:py-8">
|
| 196 |
{children}
|
| 197 |
</main>
|