feat(web): Slack disconnect UI and connect-error toasts (#1375)

Part of ENG-1136
This commit is contained in:
MaheshtheDev 2026-07-31 07:11:26 +00:00
parent b21ea31ffc
commit e8ed80f768
2 changed files with 102 additions and 16 deletions

View file

@ -146,9 +146,28 @@ export function AppExperience() {
} | null>(null)
useEffect(() => {
const sp = new URLSearchParams(window.location.search)
if (sp.get("slack") !== "connected") return
const slackParam = sp.get("slack")
if (slackParam !== "connected" && slackParam !== "error") return
const team = sp.get("team")
if (isCompanyBrain) {
if (slackParam === "error") {
const reason = sp.get("reason")
const org = sp.get("linked_org")
const isOrgMember = sp.get("linked_member") === "1"
const conflictMessage =
org && isOrgMember
? `That Slack workspace is already connected to your "${org}" organization. Switch to it to manage the connection.`
: org
? `That Slack workspace is already connected to another Supermemory organization (${org}). Ask the teammate who set it up.`
: "That Slack workspace is already connected to a different Supermemory organization. Disconnect it there first, or switch to that organization."
toast.error(
reason === "workspace_conflict"
? conflictMessage
: reason === "expired"
? "That Slack connect link expired. Try connecting again."
: "Slack connection failed. Try again.",
{ duration: 10000 },
)
} else if (isCompanyBrain) {
setSlackHandoff({ team })
} else {
toast.success(
@ -159,6 +178,9 @@ export function AppExperience() {
}
sp.delete("slack")
sp.delete("team")
sp.delete("reason")
sp.delete("linked_org")
sp.delete("linked_member")
const qs = sp.toString()
window.history.replaceState(
null,

View file

@ -237,19 +237,28 @@ function SlackCard({
status,
isAdmin,
installHref,
onDisconnect,
}: {
status: SlackStatus | null
isAdmin: boolean
installHref: string
onDisconnect: () => Promise<void>
}) {
const connected = status?.connected ?? false
const [confirming, setConfirming] = useState(false)
const [disconnecting, setDisconnecting] = useState(false)
useEffect(() => {
if (!confirming) return
const timer = setTimeout(() => setConfirming(false), 4000)
return () => clearTimeout(timer)
}, [confirming])
return (
<div className="flex min-w-0 flex-col justify-between gap-3 rounded-xl bg-[#14161A] p-4 shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]">
<div className="flex min-w-0 items-start gap-3">
<div className="flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-[10px] bg-[#080B0F] shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.6)]">
<SlackMark className="size-5" />
</div>
<div className="min-w-0 pt-0.5">
<div className="min-w-0 flex-1 pt-0.5">
<p
className={cn(
dmSans125ClassName(),
@ -267,25 +276,59 @@ function SlackCard({
Messaging
</p>
</div>
{connected && status?.teamName ? (
<span
className={cn(
dmSans125ClassName(),
"max-w-[45%] shrink-0 truncate pt-0.5 text-[12px] font-medium text-[#737373]",
)}
>
{status.teamName}
</span>
) : null}
</div>
<div className="flex min-h-9 items-center justify-between gap-3 border-[#1E293B]/50 border-t pt-3">
<ScopeChip
label={
connected
? status?.teamName
? `Workspace · ${status.teamName}`
: "Workspace"
: "Not connected"
}
label={connected ? "Connected" : "Not connected"}
connected={connected}
/>
{isAdmin ? (
<a
href={installHref}
className={cn(dmSans125ClassName(), pillLinkClass)}
>
{connected ? "Reconnect" : "Connect"}
</a>
<div className="flex items-center gap-2">
{connected ? (
<button
type="button"
disabled={disconnecting}
onClick={() => {
if (!confirming) {
setConfirming(true)
return
}
setConfirming(false)
setDisconnecting(true)
void onDisconnect().finally(() => setDisconnecting(false))
}}
className={cn(
dmSans125ClassName(),
"cursor-pointer text-[12px] font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50",
confirming
? "text-red-400 hover:text-red-300"
: "text-[#6B6B6B] hover:text-[#FAFAFA]",
)}
>
{disconnecting
? "Disconnecting…"
: confirming
? "Confirm?"
: "Disconnect"}
</button>
) : null}
<a
href={installHref}
className={cn(dmSans125ClassName(), pillLinkClass)}
>
{connected ? "Reconnect" : "Connect"}
</a>
</div>
) : null}
</div>
</div>
@ -560,6 +603,27 @@ export default function CompanyBrainConnections() {
status={slackStatus}
isAdmin={isAdmin}
installHref={`${BACKEND}/brain/slack/oauth/install`}
onDisconnect={async () => {
try {
const res = await fetch(`${BACKEND}/brain/slack/workspace`, {
method: "DELETE",
credentials: "include",
})
if (res.status === 403) {
toast.error("Only admins can disconnect Slack.")
return
}
if (!res.ok) {
toast.error("Couldn't disconnect Slack.")
return
}
} catch {
toast.error("Couldn't disconnect Slack.")
return
}
toast.success("Slack disconnected.")
await load().catch(() => undefined)
}}
/>
{apps.map((entry) => (
<AppCard