mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
feat(ui): role-aware gating sweep + account-name in sidebar
PR 9 of the multi-user accounts series — the final piece. Visual
polish so non-owners see consistent disabled states across the
app and so every user always knows which account they're acting in.
Sidebar
- New account-name strip above the user dropdown, gated behind
the `account_sharing` beta flag (same flag the Members tab
from PR 6 reads). For solo users (the default) the account is
named after them, so showing it would just duplicate the user
name below — flag-gating keeps the original layout untouched
for single-user installs.
- When the caller's role is admin/agent/viewer (not owner), the
role is surfaced as a small uppercase chip alongside the
account name so they have an unambiguous signal of their
permission level without opening Settings.
- Nav items stay always-visible per the planning doc's
"show-but-disable" decision — hiding nav rows by role would
make the app feel broken.
Inbox composer
- Send button + Templates button + textarea are disabled for
viewers with a "Read-only — viewers can browse but not reply"
placeholder and tooltip. The existing session-expired branch
still wins when it applies (session-expired is the more
specific condition and surfaces template-only sending).
List-page create buttons
- broadcasts/page.tsx: "New Broadcast" CTA (header + empty
state).
- automations/page.tsx: "Create Automation" CTA.
- flows/page.tsx: "New flow" CTA (header + empty-state).
- contacts/page.tsx: "Add Contact" + "Import" CTAs.
- pipelines/page.tsx: "Add Pipeline" (gates on edit-settings —
pipelines are settings-class under the 017 RLS) and "Add
Deal" (gates on send-messages).
All use `useCan(...)` from PR 5. Disabled buttons get a tooltip
explaining why (`title="Read-only — your role can't ..."`).
Solo users see no visual change — single-owner accounts pass
every capability check.
Why show-but-disable, not hide
- A viewer scanning the app should still see what's possible
in the product, both to understand what they can ask for an
upgrade for and to avoid the UI feeling silently broken
("the New button just disappeared?"). The tooltip carries
the explanation cheaply.
Verification
- `npm run lint` → 0 errors (same 19 pre-existing warnings)
- `npm run typecheck` → clean
- `npm test` → 366/366 pass
- `npm run build` → succeeds
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b1921af0ec
commit
14cdbf457b
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
|
||||||
? "Session expired - use a template"
|
? "Read-only — viewers can browse but not reply"
|
||||||
: "Type a message... (Shift+Enter for new line)"
|
: sessionExpired
|
||||||
|
? "Session expired - use a template"
|
||||||
|
: "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