Seth commited on
Commit
5eda54b
·
1 Parent(s): e9a187e
frontend/src/components/layout/AppShell.jsx CHANGED
@@ -34,14 +34,14 @@ export default function AppShell({ title, subtitle, rightContent, children }) {
34
  const [showSettingsNav, setShowSettingsNav] = useState(false);
35
 
36
  useEffect(() => {
37
- const syncAdminNav = () => {
38
  apiFetch('/api/auth/me')
39
  .then((r) => (r.ok ? r.json() : null))
40
- .then((u) => setShowSettingsNav(u?.current_role === 'admin'))
41
  .catch(() => setShowSettingsNav(false));
42
  };
43
- syncAdminNav();
44
- window.addEventListener('emailout-auth-changed', syncAdminNav);
45
  return () => window.removeEventListener('emailout-auth-changed', syncAdminNav);
46
  }, []);
47
 
 
34
  const [showSettingsNav, setShowSettingsNav] = useState(false);
35
 
36
  useEffect(() => {
37
+ const syncSettingsNav = () => {
38
  apiFetch('/api/auth/me')
39
  .then((r) => (r.ok ? r.json() : null))
40
+ .then((u) => setShowSettingsNav(u != null))
41
  .catch(() => setShowSettingsNav(false));
42
  };
43
+ syncSettingsNav();
44
+ window.addEventListener('emailout-auth-changed', syncSettingsNav);
45
  return () => window.removeEventListener('emailout-auth-changed', syncAdminNav);
46
  }, []);
47
 
frontend/src/components/layout/GoogleAuthBar.jsx CHANGED
@@ -1,19 +1,12 @@
1
  import React, { useCallback, useEffect, useState } from 'react';
2
  import { useSearchParams } from 'react-router-dom';
3
- import { LogOut, Building2 } from 'lucide-react';
4
  import { Button } from '@/components/ui/button';
5
- import {
6
- Select,
7
- SelectContent,
8
- SelectItem,
9
- SelectTrigger,
10
- SelectValue,
11
- } from '@/components/ui/select';
12
  import { cn } from '@/lib/utils';
13
  import { apiFetch } from '@/lib/api';
14
 
15
  /**
16
- * Sign-in (Google), workspace switcher; invitations live on Settings (admins). Session from OAuth callback.
17
  */
18
  export default function GoogleAuthBar() {
19
  const [searchParams, setSearchParams] = useSearchParams();
@@ -70,22 +63,6 @@ export default function GoogleAuthBar() {
70
  }
71
  };
72
 
73
- const switchTenant = async (tid) => {
74
- const id = parseInt(tid, 10);
75
- if (!id || id === user?.current_tenant_id) return;
76
- try {
77
- const res = await apiFetch('/api/auth/switch-tenant', {
78
- method: 'POST',
79
- headers: { 'Content-Type': 'application/json' },
80
- body: JSON.stringify({ tenant_id: id }),
81
- });
82
- if (!res.ok) return;
83
- window.location.reload();
84
- } catch (e) {
85
- console.error(e);
86
- }
87
- };
88
-
89
  const inviteParam = searchParams.get('invite');
90
  const googleHref = inviteParam
91
  ? `/api/auth/google?invite=${encodeURIComponent(inviteParam)}`
@@ -108,31 +85,8 @@ export default function GoogleAuthBar() {
108
  }
109
 
110
  if (user) {
111
- const tenants = user.tenants || [];
112
-
113
  return (
114
  <div className="flex max-w-[min(100vw-6rem,28rem)] flex-wrap items-center justify-end gap-2">
115
- {tenants.length > 0 ? (
116
- <div className="flex items-center gap-1.5 min-w-0">
117
- <Building2 className="h-4 w-4 shrink-0 text-slate-400 hidden sm:block" />
118
- <Select
119
- value={String(user.current_tenant_id ?? '')}
120
- onValueChange={switchTenant}
121
- >
122
- <SelectTrigger className="h-9 max-w-[11rem] sm:max-w-[14rem] border-slate-200 text-xs sm:text-sm">
123
- <SelectValue placeholder="Workspace" />
124
- </SelectTrigger>
125
- <SelectContent>
126
- {tenants.map((tn) => (
127
- <SelectItem key={tn.id} value={String(tn.id)}>
128
- {tn.name}
129
- {tn.role === 'admin' ? ' · admin' : ''}
130
- </SelectItem>
131
- ))}
132
- </SelectContent>
133
- </Select>
134
- </div>
135
- ) : null}
136
  {user.picture ? (
137
  <img
138
  src={user.picture}
 
1
  import React, { useCallback, useEffect, useState } from 'react';
2
  import { useSearchParams } from 'react-router-dom';
3
+ import { LogOut } from 'lucide-react';
4
  import { Button } from '@/components/ui/button';
 
 
 
 
 
 
 
5
  import { cn } from '@/lib/utils';
6
  import { apiFetch } from '@/lib/api';
7
 
8
  /**
9
+ * Sign-in (Google). Workspace details and switching context live in Settings / session. OAuth callback sets session.
10
  */
11
  export default function GoogleAuthBar() {
12
  const [searchParams, setSearchParams] = useSearchParams();
 
63
  }
64
  };
65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  const inviteParam = searchParams.get('invite');
67
  const googleHref = inviteParam
68
  ? `/api/auth/google?invite=${encodeURIComponent(inviteParam)}`
 
85
  }
86
 
87
  if (user) {
 
 
88
  return (
89
  <div className="flex max-w-[min(100vw-6rem,28rem)] flex-wrap items-center justify-end gap-2">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  {user.picture ? (
91
  <img
92
  src={user.picture}
frontend/src/pages/Leads.jsx CHANGED
@@ -373,7 +373,7 @@ export default function Leads() {
373
  return (
374
  <AppShell
375
  title="Leads"
376
- subtitle="Replies from Smartlead campaigns appear here. Configure the webhook URL under Settings (admins)."
377
  >
378
  <MainTableWorkspace
379
  primaryAction={{ label: 'New lead', to: '/' }}
 
373
  return (
374
  <AppShell
375
  title="Leads"
376
+ subtitle="Replies from Smartlead campaigns appear here. Configure the Smartlead webhook URL on Settings."
377
  >
378
  <MainTableWorkspace
379
  primaryAction={{ label: 'New lead', to: '/' }}
frontend/src/pages/Settings.jsx CHANGED
@@ -1,5 +1,4 @@
1
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
- import { Link } from 'react-router-dom';
3
  import {
4
  ClipboardCopy,
5
  Loader2,
@@ -12,6 +11,13 @@ import {
12
  import AppShell from '@/components/layout/AppShell';
13
  import { Button } from '@/components/ui/button';
14
  import { Input } from '@/components/ui/input';
 
 
 
 
 
 
 
15
  import { apiFetch } from '@/lib/api';
16
  import { cn } from '@/lib/utils';
17
 
@@ -91,6 +97,25 @@ export default function Settings() {
91
  return base;
92
  }, [me]);
93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  const sendInvite = async () => {
95
  const email = inviteEmail.trim().toLowerCase();
96
  if (!email || !email.includes('@')) return;
@@ -159,31 +184,14 @@ export default function Settings() {
159
  );
160
  }
161
 
162
- if (!isAdmin) {
163
- return (
164
- <AppShell title="Settings">
165
- <div className="rounded-2xl border border-amber-200 bg-amber-50 p-6 flex gap-4 items-start">
166
- <ShieldAlert className="h-8 w-8 shrink-0 text-amber-700" />
167
- <div>
168
- <h3 className="font-semibold text-amber-950">Admin access required</h3>
169
- <p className="text-sm text-amber-900/90 mt-1">
170
- Invitations, member management, and workspace integration URLs are only
171
- available to workspace admins. Ask an admin to grant you the admin role or
172
- use another workspace where you are an admin.
173
- </p>
174
- <Button asChild variant="outline" className="mt-4">
175
- <Link to="/">Back to Generator</Link>
176
- </Button>
177
- </div>
178
- </div>
179
- </AppShell>
180
- );
181
- }
182
-
183
  return (
184
  <AppShell
185
  title="Settings"
186
- subtitle="Workspace details, Smartlead webhook, invitations, and members (admins only)."
 
 
 
 
187
  >
188
  <div className="space-y-8 max-w-3xl">
189
  <section className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
@@ -191,6 +199,27 @@ export default function Settings() {
191
  <Building2 className="h-5 w-5 text-violet-600" />
192
  Workspace details
193
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  <dl className="grid gap-3 text-sm sm:grid-cols-[8rem_1fr] sm:gap-x-4">
195
  <dt className="text-slate-500">Name</dt>
196
  <dd className="text-slate-900 font-medium">
@@ -226,6 +255,17 @@ export default function Settings() {
226
  </div>
227
  </section>
228
 
 
 
 
 
 
 
 
 
 
 
 
229
  <section className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
230
  <div className="flex items-center gap-2 text-slate-800 font-semibold mb-2">
231
  <UserPlus className="h-5 w-5 text-violet-600" />
@@ -265,7 +305,9 @@ export default function Settings() {
265
  </div>
266
  ) : null}
267
  </section>
 
268
 
 
269
  <section className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
270
  <div className="flex items-center gap-2 text-slate-800 font-semibold mb-4">
271
  <Users className="h-5 w-5 text-violet-600" />
@@ -347,6 +389,7 @@ export default function Settings() {
347
  </div>
348
  )}
349
  </section>
 
350
  </div>
351
  </AppShell>
352
  );
 
1
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
 
2
  import {
3
  ClipboardCopy,
4
  Loader2,
 
11
  import AppShell from '@/components/layout/AppShell';
12
  import { Button } from '@/components/ui/button';
13
  import { Input } from '@/components/ui/input';
14
+ import {
15
+ Select,
16
+ SelectContent,
17
+ SelectItem,
18
+ SelectTrigger,
19
+ SelectValue,
20
+ } from '@/components/ui/select';
21
  import { apiFetch } from '@/lib/api';
22
  import { cn } from '@/lib/utils';
23
 
 
97
  return base;
98
  }, [me]);
99
 
100
+ const tenants = me?.tenants || [];
101
+ const multiWorkspace = tenants.length > 1;
102
+
103
+ const switchTenant = async (tid) => {
104
+ const id = parseInt(tid, 10);
105
+ if (!id || id === me?.current_tenant_id) return;
106
+ try {
107
+ const res = await apiFetch('/api/auth/switch-tenant', {
108
+ method: 'POST',
109
+ headers: { 'Content-Type': 'application/json' },
110
+ body: JSON.stringify({ tenant_id: id }),
111
+ });
112
+ if (!res.ok) return;
113
+ window.location.reload();
114
+ } catch (e) {
115
+ console.error(e);
116
+ }
117
+ };
118
+
119
  const sendInvite = async () => {
120
  const email = inviteEmail.trim().toLowerCase();
121
  if (!email || !email.includes('@')) return;
 
184
  );
185
  }
186
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  return (
188
  <AppShell
189
  title="Settings"
190
+ subtitle={
191
+ isAdmin
192
+ ? 'Workspace details, Smartlead webhook, invitations, and members.'
193
+ : 'Workspace details and Smartlead webhook. Invites and member management require admin.'
194
+ }
195
  >
196
  <div className="space-y-8 max-w-3xl">
197
  <section className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
 
199
  <Building2 className="h-5 w-5 text-violet-600" />
200
  Workspace details
201
  </div>
202
+ {multiWorkspace ? (
203
+ <div className="mb-6 pb-6 border-b border-slate-100">
204
+ <p className="text-xs font-medium text-slate-600 mb-2">Active workspace</p>
205
+ <Select
206
+ value={String(me.current_tenant_id ?? '')}
207
+ onValueChange={switchTenant}
208
+ >
209
+ <SelectTrigger className="h-10 max-w-md border-slate-200 text-sm">
210
+ <SelectValue placeholder="Choose workspace" />
211
+ </SelectTrigger>
212
+ <SelectContent>
213
+ {tenants.map((tn) => (
214
+ <SelectItem key={tn.id} value={String(tn.id)}>
215
+ {tn.name}
216
+ {tn.role === 'admin' ? ' · admin' : ''}
217
+ </SelectItem>
218
+ ))}
219
+ </SelectContent>
220
+ </Select>
221
+ </div>
222
+ ) : null}
223
  <dl className="grid gap-3 text-sm sm:grid-cols-[8rem_1fr] sm:gap-x-4">
224
  <dt className="text-slate-500">Name</dt>
225
  <dd className="text-slate-900 font-medium">
 
255
  </div>
256
  </section>
257
 
258
+ {!isAdmin ? (
259
+ <div className="rounded-2xl border border-slate-200 bg-slate-50/80 p-5 flex gap-3 items-start">
260
+ <ShieldAlert className="h-6 w-6 shrink-0 text-slate-500 mt-0.5" />
261
+ <p className="text-sm text-slate-600">
262
+ Inviting users and removing members is limited to workspace admins. Ask an
263
+ admin if you need help.
264
+ </p>
265
+ </div>
266
+ ) : null}
267
+
268
+ {isAdmin ? (
269
  <section className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
270
  <div className="flex items-center gap-2 text-slate-800 font-semibold mb-2">
271
  <UserPlus className="h-5 w-5 text-violet-600" />
 
305
  </div>
306
  ) : null}
307
  </section>
308
+ ) : null}
309
 
310
+ {isAdmin ? (
311
  <section className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
312
  <div className="flex items-center gap-2 text-slate-800 font-semibold mb-4">
313
  <Users className="h-5 w-5 text-violet-600" />
 
389
  </div>
390
  )}
391
  </section>
392
+ ) : null}
393
  </div>
394
  </AppShell>
395
  );