Seth commited on
Commit
51ee8a8
·
1 Parent(s): 42babfe
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
- emailMailboxAccounts,
213
- emailMailboxSelectValue,
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 mailAccounts = emailMailboxAccounts || [];
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
- {showEmailMailboxSelect ? (
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
- <ChevronRight className="h-5 w-5 shrink-0 text-slate-300" aria-hidden />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 emailSelectValue =
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
- emailMailboxAccounts={step.channel === 'gmail' ? mailAccts : []}
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
- return typeof window !== 'undefined' && localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === '1';
 
 
 
38
  } catch {
39
- return false;
40
  }
41
  });
42
 
@@ -49,9 +52,9 @@ export default function AppShell({ title, subtitle, rightContent, children }) {
49
  }, [sidebarCollapsed]);
50
 
51
  return (
52
- <div className="flex min-h-screen flex-col bg-gradient-to-br from-slate-50 via-white to-violet-50">
53
  {/* Single full-width rule under branding + page title */}
54
- <header className="sticky top-0 z-40 flex flex-col border-b border-slate-200 bg-white/80 backdrop-blur-sm">
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 self-stretch border-r border-slate-200 bg-white py-4 transition-[width] duration-200 ease-out',
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
- active
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
- active
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
- active ? 'text-violet-700' : 'text-slate-700'
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>