mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
Merge pull request #177 from ArnasDon/feat/account-sharing-ui-gating
feat(ui): role-aware gating sweep + account-name in sidebar
This commit is contained in:
commit
bf2addc7c9
15
CHANGELOG.md
15
CHANGELOG.md
@ -38,6 +38,21 @@ as the sole owner of their own account and sees identical data.
|
|||||||
- **The signup trigger (`handle_new_user`) now also creates a
|
- **The signup trigger (`handle_new_user`) now also creates a
|
||||||
personal account** and links the new profile to it as `owner`.
|
personal account** and links the new profile to it as `owner`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Role-aware UI gating across the app.** The inbox composer's
|
||||||
|
send button + textarea, the "New broadcast / automation / flow"
|
||||||
|
buttons, the "Add pipeline / deal" buttons, and the "Add /
|
||||||
|
Import contact" buttons are now disabled-with-tooltip for
|
||||||
|
viewers (and for agents on settings-class actions). Choice:
|
||||||
|
show-but-disable rather than hide, so the UI never feels
|
||||||
|
silently broken to a teammate looking at a feature they don't
|
||||||
|
yet have permission for.
|
||||||
|
- **Sidebar surfaces the active account** above the user info
|
||||||
|
when the `account_sharing` beta flag is on. Solo users keep
|
||||||
|
the original layout (their account is named after them, so
|
||||||
|
duplicating it would just add visual noise).
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- **Inbound WhatsApp messages now land in the shared inbox.** The
|
- **Inbound WhatsApp messages now land in the shared inbox.** The
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import {
|
|||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
|
|
||||||
import { createClient } from "@/lib/supabase/client"
|
import { createClient } from "@/lib/supabase/client"
|
||||||
|
import { useCan } from "@/hooks/use-can"
|
||||||
import type { Automation } from "@/types"
|
import type { Automation } from "@/types"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Switch } from "@/components/ui/switch"
|
import { Switch } from "@/components/ui/switch"
|
||||||
@ -55,8 +56,12 @@ const TEMPLATE_ICON: Record<TemplateSlug, typeof Zap> = {
|
|||||||
follow_up_reminder: PhoneCall,
|
follow_up_reminder: PhoneCall,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const READ_ONLY_TITLE =
|
||||||
|
"Read-only — your role can't create automations"
|
||||||
|
|
||||||
export default function AutomationsPage() {
|
export default function AutomationsPage() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const canCreate = useCan("send-messages")
|
||||||
const [automations, setAutomations] = useState<Automation[] | null>(null)
|
const [automations, setAutomations] = useState<Automation[] | null>(null)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [pendingDelete, setPendingDelete] = useState<Automation | null>(null)
|
const [pendingDelete, setPendingDelete] = useState<Automation | null>(null)
|
||||||
@ -164,6 +169,8 @@ export default function AutomationsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => router.push("/automations/new")}
|
onClick={() => router.push("/automations/new")}
|
||||||
|
disabled={!canCreate}
|
||||||
|
title={canCreate ? undefined : READ_ONLY_TITLE}
|
||||||
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
|
|||||||
@ -14,8 +14,15 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import { Radio, Plus, Loader2 } from 'lucide-react';
|
import { Radio, Plus, Loader2 } from 'lucide-react';
|
||||||
|
import { useCan } from '@/hooks/use-can';
|
||||||
import { getBroadcastStatus } from '@/lib/broadcast-status';
|
import { getBroadcastStatus } from '@/lib/broadcast-status';
|
||||||
|
|
||||||
|
// Tooltip copy reused across both "New broadcast" buttons on this
|
||||||
|
// page. Centralised so a wording change is a one-line diff and the
|
||||||
|
// two CTAs can't drift apart.
|
||||||
|
const READ_ONLY_TITLE =
|
||||||
|
"Read-only — your role can't create broadcasts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Poll cadence while any broadcast is sending. Kept modest so we don't
|
* Poll cadence while any broadcast is sending. Kept modest so we don't
|
||||||
* beat on Supabase — the aggregate trigger in migration 003 keeps
|
* beat on Supabase — the aggregate trigger in migration 003 keeps
|
||||||
@ -56,6 +63,7 @@ function RateCell({
|
|||||||
|
|
||||||
export default function BroadcastsPage() {
|
export default function BroadcastsPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const canCreate = useCan('send-messages');
|
||||||
const [broadcasts, setBroadcasts] = useState<Broadcast[]>([]);
|
const [broadcasts, setBroadcasts] = useState<Broadcast[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@ -183,6 +191,8 @@ export default function BroadcastsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => router.push('/broadcasts/new')}
|
onClick={() => router.push('/broadcasts/new')}
|
||||||
|
disabled={!canCreate}
|
||||||
|
title={canCreate ? undefined : READ_ONLY_TITLE}
|
||||||
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
@ -199,6 +209,8 @@ export default function BroadcastsPage() {
|
|||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => router.push('/broadcasts/new')}
|
onClick={() => router.push('/broadcasts/new')}
|
||||||
|
disabled={!canCreate}
|
||||||
|
title={canCreate ? undefined : READ_ONLY_TITLE}
|
||||||
className="mt-4 bg-primary text-primary-foreground hover:bg-primary/90"
|
className="mt-4 bg-primary text-primary-foreground hover:bg-primary/90"
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
|
|||||||
@ -44,8 +44,11 @@ import {
|
|||||||
import { ContactForm } from '@/components/contacts/contact-form';
|
import { ContactForm } from '@/components/contacts/contact-form';
|
||||||
import { ContactDetailView } from '@/components/contacts/contact-detail-view';
|
import { ContactDetailView } from '@/components/contacts/contact-detail-view';
|
||||||
import { ImportModal } from '@/components/contacts/import-modal';
|
import { ImportModal } from '@/components/contacts/import-modal';
|
||||||
|
import { useCan } from '@/hooks/use-can';
|
||||||
|
|
||||||
const PAGE_SIZE = 25;
|
const PAGE_SIZE = 25;
|
||||||
|
const READ_ONLY_TITLE =
|
||||||
|
"Read-only — your role can't add or import contacts";
|
||||||
|
|
||||||
interface ContactWithTags extends Contact {
|
interface ContactWithTags extends Contact {
|
||||||
tags?: Tag[];
|
tags?: Tag[];
|
||||||
@ -53,6 +56,7 @@ interface ContactWithTags extends Contact {
|
|||||||
|
|
||||||
export default function ContactsPage() {
|
export default function ContactsPage() {
|
||||||
const supabase = createClient();
|
const supabase = createClient();
|
||||||
|
const canEdit = useCan('send-messages');
|
||||||
|
|
||||||
const [contacts, setContacts] = useState<ContactWithTags[]>([]);
|
const [contacts, setContacts] = useState<ContactWithTags[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@ -219,6 +223,8 @@ export default function ContactsPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setImportOpen(true)}
|
onClick={() => setImportOpen(true)}
|
||||||
|
disabled={!canEdit}
|
||||||
|
title={canEdit ? undefined : READ_ONLY_TITLE}
|
||||||
className="border-slate-700 text-slate-300 hover:bg-slate-800"
|
className="border-slate-700 text-slate-300 hover:bg-slate-800"
|
||||||
>
|
>
|
||||||
<Upload className="size-4" />
|
<Upload className="size-4" />
|
||||||
@ -226,6 +232,8 @@ export default function ContactsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={openAddForm}
|
onClick={openAddForm}
|
||||||
|
disabled={!canEdit}
|
||||||
|
title={canEdit ? undefined : READ_ONLY_TITLE}
|
||||||
className="bg-primary hover:bg-primary/90 text-primary-foreground"
|
className="bg-primary hover:bg-primary/90 text-primary-foreground"
|
||||||
>
|
>
|
||||||
<Plus className="size-4" />
|
<Plus className="size-4" />
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import {
|
|||||||
FileText,
|
FileText,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
|
import { useCan } from "@/hooks/use-can";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@ -79,8 +80,11 @@ const TEMPLATE_ICONS = {
|
|||||||
UserPlus,
|
UserPlus,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
const READ_ONLY_TITLE = "Read-only — your role can't create flows";
|
||||||
|
|
||||||
export default function FlowsPage() {
|
export default function FlowsPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const canCreate = useCan("send-messages");
|
||||||
const [flows, setFlows] = useState<FlowRow[]>([]);
|
const [flows, setFlows] = useState<FlowRow[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
@ -211,14 +215,21 @@ export default function FlowsPage() {
|
|||||||
menus, FAQs, and triage before a human steps in.
|
menus, FAQs, and triage before a human steps in.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={() => setCreateOpen(true)}>
|
<Button
|
||||||
|
onClick={() => setCreateOpen(true)}
|
||||||
|
disabled={!canCreate}
|
||||||
|
title={canCreate ? undefined : READ_ONLY_TITLE}
|
||||||
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
New flow
|
New flow
|
||||||
</Button>
|
</Button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{flows.length === 0 ? (
|
{flows.length === 0 ? (
|
||||||
<EmptyState onCreate={() => setCreateOpen(true)} />
|
<EmptyState
|
||||||
|
onCreate={() => setCreateOpen(true)}
|
||||||
|
canCreate={canCreate}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||||
{flows.map((flow) => (
|
{flows.map((flow) => (
|
||||||
@ -312,7 +323,13 @@ export default function FlowsPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmptyState({ onCreate }: { onCreate: () => void }) {
|
function EmptyState({
|
||||||
|
onCreate,
|
||||||
|
canCreate,
|
||||||
|
}: {
|
||||||
|
onCreate: () => void;
|
||||||
|
canCreate: boolean;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed border-slate-700 bg-slate-900/50 px-6 py-16 text-center">
|
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed border-slate-700 bg-slate-900/50 px-6 py-16 text-center">
|
||||||
<div className="flex h-14 w-14 items-center justify-center rounded-full bg-slate-800">
|
<div className="flex h-14 w-14 items-center justify-center rounded-full bg-slate-800">
|
||||||
@ -326,7 +343,12 @@ function EmptyState({ onCreate }: { onCreate: () => void }) {
|
|||||||
bot. Customers tap buttons; the bot routes them to the right answer (or
|
bot. Customers tap buttons; the bot routes them to the right answer (or
|
||||||
the right agent).
|
the right agent).
|
||||||
</p>
|
</p>
|
||||||
<Button onClick={onCreate} className="mt-5">
|
<Button
|
||||||
|
onClick={onCreate}
|
||||||
|
disabled={!canCreate}
|
||||||
|
title={canCreate ? undefined : READ_ONLY_TITLE}
|
||||||
|
className="mt-5"
|
||||||
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
Create your first flow
|
Create your first flow
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -26,6 +26,14 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { GitBranch, Plus, ChevronDown, Settings } from "lucide-react";
|
import { GitBranch, Plus, ChevronDown, Settings } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { useCan } from "@/hooks/use-can";
|
||||||
|
|
||||||
|
// Pipeline creation is admin-class (it's a settings-tier write
|
||||||
|
// under the new RLS), but deal creation is operational and only
|
||||||
|
// requires agent+. The two CTAs gate on different capabilities.
|
||||||
|
const PIPELINE_READ_ONLY_TITLE =
|
||||||
|
"Read-only — your role can't create pipelines";
|
||||||
|
const DEAL_READ_ONLY_TITLE = "Read-only — your role can't create deals";
|
||||||
|
|
||||||
// Spec-defined seed — name and color per the product spec.
|
// Spec-defined seed — name and color per the product spec.
|
||||||
const SPEC_DEFAULT_STAGES = [
|
const SPEC_DEFAULT_STAGES = [
|
||||||
@ -38,6 +46,8 @@ const SPEC_DEFAULT_STAGES = [
|
|||||||
|
|
||||||
export default function PipelinesPage() {
|
export default function PipelinesPage() {
|
||||||
const supabase = createClient();
|
const supabase = createClient();
|
||||||
|
const canEditSettings = useCan("edit-settings");
|
||||||
|
const canCreateDeals = useCan("send-messages");
|
||||||
|
|
||||||
const [pipelines, setPipelines] = useState<Pipeline[]>([]);
|
const [pipelines, setPipelines] = useState<Pipeline[]>([]);
|
||||||
const [selectedPipelineId, setSelectedPipelineId] = useState<string>("");
|
const [selectedPipelineId, setSelectedPipelineId] = useState<string>("");
|
||||||
@ -349,6 +359,8 @@ export default function PipelinesPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setNewPipelineOpen(true)}
|
onClick={() => setNewPipelineOpen(true)}
|
||||||
|
disabled={!canEditSettings}
|
||||||
|
title={canEditSettings ? undefined : PIPELINE_READ_ONLY_TITLE}
|
||||||
className="border-slate-700 bg-slate-900 text-slate-200 hover:bg-slate-800"
|
className="border-slate-700 bg-slate-900 text-slate-200 hover:bg-slate-800"
|
||||||
>
|
>
|
||||||
<Plus className="mr-1 h-4 w-4" />
|
<Plus className="mr-1 h-4 w-4" />
|
||||||
@ -356,7 +368,10 @@ export default function PipelinesPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleAddDeal()}
|
onClick={() => handleAddDeal()}
|
||||||
disabled={!selectedPipelineId || stages.length === 0}
|
disabled={
|
||||||
|
!canCreateDeals || !selectedPipelineId || stages.length === 0
|
||||||
|
}
|
||||||
|
title={canCreateDeals ? undefined : DEAL_READ_ONLY_TITLE}
|
||||||
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
||||||
>
|
>
|
||||||
<Plus className="mr-1 h-4 w-4" />
|
<Plus className="mr-1 h-4 w-4" />
|
||||||
@ -377,6 +392,8 @@ export default function PipelinesPage() {
|
|||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setNewPipelineOpen(true)}
|
onClick={() => setNewPipelineOpen(true)}
|
||||||
|
disabled={!canEditSettings}
|
||||||
|
title={canEditSettings ? undefined : PIPELINE_READ_ONLY_TITLE}
|
||||||
className="mt-4 bg-primary text-primary-foreground hover:bg-primary/90"
|
className="mt-4 bg-primary text-primary-foreground hover:bg-primary/90"
|
||||||
>
|
>
|
||||||
<Plus className="mr-1 h-4 w-4" />
|
<Plus className="mr-1 h-4 w-4" />
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
import { useState, useRef, useCallback, KeyboardEvent } from "react";
|
import { useState, useRef, useCallback, KeyboardEvent } from "react";
|
||||||
import { Send, LayoutTemplate } from "lucide-react";
|
import { Send, LayoutTemplate } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useCan } from "@/hooks/use-can";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { ReplyQuote } from "./reply-quote";
|
import { ReplyQuote } from "./reply-quote";
|
||||||
|
|
||||||
@ -33,6 +34,11 @@ export function MessageComposer({
|
|||||||
const [text, setText] = useState("");
|
const [text, setText] = useState("");
|
||||||
const [sending, setSending] = useState(false);
|
const [sending, setSending] = useState(false);
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
// Viewers (read-only role) can browse the inbox but never send.
|
||||||
|
// For solo users this is always true — single-owner accounts pass
|
||||||
|
// every capability — so the disabled branch is a no-op there.
|
||||||
|
const canSend = useCan("send-messages");
|
||||||
|
const readOnly = !canSend;
|
||||||
|
|
||||||
const adjustHeight = useCallback(() => {
|
const adjustHeight = useCallback(() => {
|
||||||
const el = textareaRef.current;
|
const el = textareaRef.current;
|
||||||
@ -110,7 +116,8 @@ export function MessageComposer({
|
|||||||
size="sm"
|
size="sm"
|
||||||
className="h-9 w-9 shrink-0 p-0 text-slate-400 hover:text-white"
|
className="h-9 w-9 shrink-0 p-0 text-slate-400 hover:text-white"
|
||||||
onClick={onOpenTemplates}
|
onClick={onOpenTemplates}
|
||||||
title="Send template"
|
disabled={readOnly}
|
||||||
|
title={readOnly ? "Read-only — your role can't send messages" : "Send template"}
|
||||||
>
|
>
|
||||||
<LayoutTemplate className="h-4 w-4" />
|
<LayoutTemplate className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -121,23 +128,27 @@ export function MessageComposer({
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder={
|
placeholder={
|
||||||
sessionExpired
|
readOnly
|
||||||
|
? "Read-only — viewers can browse but not reply"
|
||||||
|
: sessionExpired
|
||||||
? "Session expired - use a template"
|
? "Session expired - use a template"
|
||||||
: "Type a message... (Shift+Enter for new line)"
|
: "Type a message... (Shift+Enter for new line)"
|
||||||
}
|
}
|
||||||
disabled={sessionExpired}
|
disabled={sessionExpired || readOnly}
|
||||||
rows={1}
|
rows={1}
|
||||||
|
title={readOnly ? "Read-only — your role can't send messages" : undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-1 resize-none rounded-xl border border-slate-700 bg-slate-800 px-4 py-2.5 text-sm text-white placeholder-slate-500 outline-none transition-colors focus:border-primary/50",
|
"flex-1 resize-none rounded-xl border border-slate-700 bg-slate-800 px-4 py-2.5 text-sm text-white placeholder-slate-500 outline-none transition-colors focus:border-primary/50",
|
||||||
sessionExpired && "cursor-not-allowed opacity-50"
|
(sessionExpired || readOnly) && "cursor-not-allowed opacity-50"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-9 w-9 shrink-0 bg-primary p-0 hover:bg-primary/90 disabled:opacity-40"
|
className="h-9 w-9 shrink-0 bg-primary p-0 hover:bg-primary/90 disabled:opacity-40"
|
||||||
disabled={!text.trim() || sessionExpired || sending}
|
disabled={!text.trim() || sessionExpired || sending || readOnly}
|
||||||
onClick={handleSend}
|
onClick={handleSend}
|
||||||
|
title={readOnly ? "Read-only — your role can't send messages" : undefined}
|
||||||
>
|
>
|
||||||
<Send className="h-4 w-4" />
|
<Send className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import {
|
|||||||
Settings,
|
Settings,
|
||||||
LogOut,
|
LogOut,
|
||||||
User,
|
User,
|
||||||
|
UsersRound,
|
||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
@ -63,10 +64,17 @@ interface SidebarProps {
|
|||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Same flag key the Members tab and Settings page use. Flip per
|
||||||
|
// profile via Supabase Studio to dogfood the multi-user surface.
|
||||||
|
const ACCOUNT_SHARING_FLAG = "account_sharing";
|
||||||
|
|
||||||
export function Sidebar({ open = false, onClose }: SidebarProps) {
|
export function Sidebar({ open = false, onClose }: SidebarProps) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { profile, signOut } = useAuth();
|
const { profile, account, accountRole, signOut } = useAuth();
|
||||||
const totalUnread = useTotalUnread();
|
const totalUnread = useTotalUnread();
|
||||||
|
const accountSharingEnabled = !!profile?.beta_features?.includes(
|
||||||
|
ACCOUNT_SHARING_FLAG,
|
||||||
|
);
|
||||||
|
|
||||||
// Close the drawer when route changes — users opened it to navigate,
|
// Close the drawer when route changes — users opened it to navigate,
|
||||||
// so once they pick a destination the drawer should get out of the way.
|
// so once they pick a destination the drawer should get out of the way.
|
||||||
@ -216,6 +224,23 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
|
|||||||
|
|
||||||
{/* User section */}
|
{/* User section */}
|
||||||
<div className="shrink-0 border-t border-slate-800 p-3">
|
<div className="shrink-0 border-t border-slate-800 p-3">
|
||||||
|
{/* Account name display — only surfaced when the user is
|
||||||
|
opted into the account_sharing beta flag. For solo
|
||||||
|
users (the default) the account is named after them,
|
||||||
|
so showing it here would just duplicate the user name
|
||||||
|
below. Once the flag is on the user is at minimum
|
||||||
|
aware of which shared account they're acting in. */}
|
||||||
|
{accountSharingEnabled && account?.name ? (
|
||||||
|
<div className="mb-2 flex items-center gap-2 px-3 text-xs text-slate-500">
|
||||||
|
<UsersRound className="size-3.5" />
|
||||||
|
<span className="truncate">{account.name}</span>
|
||||||
|
{accountRole && accountRole !== "owner" ? (
|
||||||
|
<span className="ml-auto rounded-full border border-slate-700 bg-slate-800 px-1.5 py-0.5 text-[10px] uppercase tracking-wider text-slate-400">
|
||||||
|
{accountRole}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger className="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left transition-colors hover:bg-slate-800/60 focus:bg-slate-800/60 focus:outline-none data-popup-open:bg-slate-800/60">
|
<DropdownMenuTrigger className="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left transition-colors hover:bg-slate-800/60 focus:bg-slate-800/60 focus:outline-none data-popup-open:bg-slate-800/60">
|
||||||
<Avatar className="size-8 shrink-0">
|
<Avatar className="size-8 shrink-0">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user