feat(tenant): make connect panel dismissible after connecting
All checks were successful
Build and Push / build (push) Successful in 1m49s
All checks were successful
Build and Push / build (push) Successful in 1m49s
This commit is contained in:
@@ -225,6 +225,7 @@ export default async function TenantDetailPage({
|
|||||||
unreachable). */}
|
unreachable). */}
|
||||||
<section className="mb-8 animate-in animate-in-delay-1">
|
<section className="mb-8 animate-in animate-in-delay-1">
|
||||||
<ConnectPanel
|
<ConnectPanel
|
||||||
|
tenantName={name}
|
||||||
enabledChannels={enabledChannels}
|
enabledChannels={enabledChannels}
|
||||||
phase={tenant.status?.phase ?? "Pending"}
|
phase={tenant.status?.phase ?? "Pending"}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { THREEMA_GATEWAY } from "@/lib/threema-gateway-config";
|
import { THREEMA_GATEWAY } from "@/lib/threema-gateway-config";
|
||||||
|
|
||||||
@@ -16,11 +17,16 @@ import { THREEMA_GATEWAY } from "@/lib/threema-gateway-config";
|
|||||||
* NO channel is enabled it says so explicitly (a running assistant with
|
* NO channel is enabled it says so explicitly (a running assistant with
|
||||||
* no channel is unreachable).
|
* no channel is unreachable).
|
||||||
*
|
*
|
||||||
|
* Once a customer has connected they don't need the steps every visit,
|
||||||
|
* so the panel is dismissible: clicking "I've connected" collapses it
|
||||||
|
* to a slim row and remembers that per-tenant (localStorage). The slim
|
||||||
|
* row keeps a "Show connection details" toggle so it's never lost.
|
||||||
|
* The no-channel warning is NOT dismissible — it's an actionable alert,
|
||||||
|
* not reference material.
|
||||||
|
*
|
||||||
* It is intentionally complementary to ChannelUsers below it:
|
* It is intentionally complementary to ChannelUsers below it:
|
||||||
* - ConnectPanel → "how do *I* reach the assistant"
|
* - ConnectPanel → "how do *I* reach the assistant"
|
||||||
* - ChannelUsers → "*who* is allowed to reach it"
|
* - ChannelUsers → "*who* is allowed to reach it"
|
||||||
* The Threema/Telegram/Discord steps reference the authorised-users
|
|
||||||
* list rather than duplicating it.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Render order is fixed (not the order packages happen to appear in
|
// Render order is fixed (not the order packages happen to appear in
|
||||||
@@ -40,10 +46,15 @@ const CHANNEL_STEPS_KEY: Record<string, string> = {
|
|||||||
discord: "discordSteps",
|
discord: "discordSteps",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const dismissKey = (tenantName: string) =>
|
||||||
|
`pieced:connect-hidden:${tenantName}`;
|
||||||
|
|
||||||
export function ConnectPanel({
|
export function ConnectPanel({
|
||||||
|
tenantName,
|
||||||
enabledChannels,
|
enabledChannels,
|
||||||
phase,
|
phase,
|
||||||
}: {
|
}: {
|
||||||
|
tenantName: string;
|
||||||
enabledChannels: string[];
|
enabledChannels: string[];
|
||||||
/** Tenant phase — connection details only "work" once it's Ready. */
|
/** Tenant phase — connection details only "work" once it's Ready. */
|
||||||
phase: string;
|
phase: string;
|
||||||
@@ -53,7 +64,41 @@ export function ConnectPanel({
|
|||||||
const channels = CHANNEL_ORDER.filter((c) => enabledChannels.includes(c));
|
const channels = CHANNEL_ORDER.filter((c) => enabledChannels.includes(c));
|
||||||
const ready = phase === "Ready" || phase === "Running" || phase === "Active";
|
const ready = phase === "Ready" || phase === "Running" || phase === "Active";
|
||||||
|
|
||||||
// No channel at all → the assistant is unreachable. Make it loud.
|
// Dismissed state is read from localStorage after mount to avoid a
|
||||||
|
// hydration mismatch (server has no localStorage). `hydrated` gates
|
||||||
|
// the collapsed view so the first paint matches the server output.
|
||||||
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
const [hydrated, setHydrated] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
setCollapsed(localStorage.getItem(dismissKey(tenantName)) === "1");
|
||||||
|
} catch {
|
||||||
|
/* private mode / storage disabled — just stay expanded */
|
||||||
|
}
|
||||||
|
setHydrated(true);
|
||||||
|
}, [tenantName]);
|
||||||
|
|
||||||
|
const dismiss = () => {
|
||||||
|
setCollapsed(true);
|
||||||
|
try {
|
||||||
|
localStorage.setItem(dismissKey(tenantName), "1");
|
||||||
|
} catch {
|
||||||
|
/* no-op */
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const reopen = () => {
|
||||||
|
setCollapsed(false);
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(dismissKey(tenantName));
|
||||||
|
} catch {
|
||||||
|
/* no-op */
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// No channel at all → the assistant is unreachable. Make it loud and
|
||||||
|
// keep it non-dismissible (it's an alert, not reference material).
|
||||||
if (channels.length === 0) {
|
if (channels.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-amber-500/30 bg-amber-500/10 p-5">
|
<div className="rounded-xl border border-amber-500/30 bg-amber-500/10 p-5">
|
||||||
@@ -85,11 +130,51 @@ export function ConnectPanel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Collapsed: a slim, unobtrusive row with a toggle to bring the full
|
||||||
|
// panel back. Only shown once hydrated so SSR/CSR agree.
|
||||||
|
if (hydrated && collapsed) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between rounded-lg border border-border bg-surface-1 px-4 py-2">
|
||||||
|
<span className="text-xs text-text-muted">{t("title")}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={reopen}
|
||||||
|
className="text-xs font-medium text-accent hover:text-accent-dim transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
{t("show")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-accent/30 bg-accent/5 p-5">
|
<div className="rounded-xl border border-accent/30 bg-accent/5 p-5">
|
||||||
<h2 className="font-display text-base font-semibold text-text-primary mb-1">
|
<div className="flex items-start justify-between gap-3 mb-1">
|
||||||
{t("title")}
|
<h2 className="font-display text-base font-semibold text-text-primary">
|
||||||
</h2>
|
{t("title")}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={dismiss}
|
||||||
|
className="shrink-0 inline-flex items-center gap-1 text-xs font-medium text-text-muted hover:text-text-secondary transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="h-3.5 w-3.5"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M4.5 12.75l6 6 9-13.5"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{t("dismiss")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<p className="text-xs text-text-secondary mb-4 leading-relaxed">
|
<p className="text-xs text-text-secondary mb-4 leading-relaxed">
|
||||||
{t("description")}
|
{t("description")}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1005,6 +1005,8 @@
|
|||||||
"threemaBotIdLabel": "Threema-ID",
|
"threemaBotIdLabel": "Threema-ID",
|
||||||
"threemaSteps": "1. Öffnen Sie Threema und scannen Sie diesen QR-Code (oder fügen Sie die obige ID als Kontakt hinzu).\n2. Senden Sie eine Nachricht, um den Chat zu starten.\nStellen Sie sicher, dass Ihre eigene Threema-ID in der Liste der autorisierten Benutzer unten steht – nur gelistete IDs erhalten eine Antwort.",
|
"threemaSteps": "1. Öffnen Sie Threema und scannen Sie diesen QR-Code (oder fügen Sie die obige ID als Kontakt hinzu).\n2. Senden Sie eine Nachricht, um den Chat zu starten.\nStellen Sie sicher, dass Ihre eigene Threema-ID in der Liste der autorisierten Benutzer unten steht – nur gelistete IDs erhalten eine Antwort.",
|
||||||
"telegramSteps": "Öffnen Sie den verbundenen Telegram-Bot und senden Sie ihm eine Nachricht, um den Chat zu starten. Nur die Benutzer-IDs in der Liste der autorisierten Benutzer unten erhalten eine Antwort.",
|
"telegramSteps": "Öffnen Sie den verbundenen Telegram-Bot und senden Sie ihm eine Nachricht, um den Chat zu starten. Nur die Benutzer-IDs in der Liste der autorisierten Benutzer unten erhalten eine Antwort.",
|
||||||
"discordSteps": "Schreiben Sie dem verbundenen Discord-Bot oder erwähnen Sie ihn in einem Kanal, dem er beigetreten ist. Nur die Benutzer-IDs in der Liste der autorisierten Benutzer unten erhalten eine Antwort."
|
"discordSteps": "Schreiben Sie dem verbundenen Discord-Bot oder erwähnen Sie ihn in einem Kanal, dem er beigetreten ist. Nur die Benutzer-IDs in der Liste der autorisierten Benutzer unten erhalten eine Antwort.",
|
||||||
|
"dismiss": "Verbunden",
|
||||||
|
"show": "Verbindungsdetails anzeigen"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1005,6 +1005,8 @@
|
|||||||
"threemaBotIdLabel": "Threema ID",
|
"threemaBotIdLabel": "Threema ID",
|
||||||
"threemaSteps": "1. Open Threema and scan this QR code (or add the ID above as a contact).\n2. Send it a message to start chatting.\nMake sure your own Threema ID is on the authorised users list below — only listed IDs get a reply.",
|
"threemaSteps": "1. Open Threema and scan this QR code (or add the ID above as a contact).\n2. Send it a message to start chatting.\nMake sure your own Threema ID is on the authorised users list below — only listed IDs get a reply.",
|
||||||
"telegramSteps": "Open the Telegram bot you connected and send it a message to start chatting. Only the user IDs on the authorised users list below get a reply.",
|
"telegramSteps": "Open the Telegram bot you connected and send it a message to start chatting. Only the user IDs on the authorised users list below get a reply.",
|
||||||
"discordSteps": "Message the Discord bot you connected, or mention it in a channel it has joined. Only the user IDs on the authorised users list below get a reply."
|
"discordSteps": "Message the Discord bot you connected, or mention it in a channel it has joined. Only the user IDs on the authorised users list below get a reply.",
|
||||||
|
"dismiss": "I've connected",
|
||||||
|
"show": "Show connection details"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1005,6 +1005,8 @@
|
|||||||
"threemaBotIdLabel": "Identifiant Threema",
|
"threemaBotIdLabel": "Identifiant Threema",
|
||||||
"threemaSteps": "1. Ouvrez Threema et scannez ce QR code (ou ajoutez l'identifiant ci-dessus comme contact).\n2. Envoyez-lui un message pour commencer à discuter.\nAssurez-vous que votre propre identifiant Threema figure dans la liste des utilisateurs autorisés ci-dessous — seuls les identifiants listés reçoivent une réponse.",
|
"threemaSteps": "1. Ouvrez Threema et scannez ce QR code (ou ajoutez l'identifiant ci-dessus comme contact).\n2. Envoyez-lui un message pour commencer à discuter.\nAssurez-vous que votre propre identifiant Threema figure dans la liste des utilisateurs autorisés ci-dessous — seuls les identifiants listés reçoivent une réponse.",
|
||||||
"telegramSteps": "Ouvrez le bot Telegram que vous avez connecté et envoyez-lui un message pour commencer à discuter. Seuls les identifiants utilisateur de la liste des utilisateurs autorisés ci-dessous reçoivent une réponse.",
|
"telegramSteps": "Ouvrez le bot Telegram que vous avez connecté et envoyez-lui un message pour commencer à discuter. Seuls les identifiants utilisateur de la liste des utilisateurs autorisés ci-dessous reçoivent une réponse.",
|
||||||
"discordSteps": "Écrivez au bot Discord que vous avez connecté, ou mentionnez-le dans un salon qu'il a rejoint. Seuls les identifiants utilisateur de la liste des utilisateurs autorisés ci-dessous reçoivent une réponse."
|
"discordSteps": "Écrivez au bot Discord que vous avez connecté, ou mentionnez-le dans un salon qu'il a rejoint. Seuls les identifiants utilisateur de la liste des utilisateurs autorisés ci-dessous reçoivent une réponse.",
|
||||||
|
"dismiss": "Je suis connecté",
|
||||||
|
"show": "Afficher les détails de connexion"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1005,6 +1005,8 @@
|
|||||||
"threemaBotIdLabel": "ID Threema",
|
"threemaBotIdLabel": "ID Threema",
|
||||||
"threemaSteps": "1. Apri Threema e scansiona questo codice QR (oppure aggiungi l'ID sopra come contatto).\n2. Inviagli un messaggio per iniziare a chattare.\nAssicurati che il tuo ID Threema sia presente nell'elenco degli utenti autorizzati qui sotto: solo gli ID elencati ricevono una risposta.",
|
"threemaSteps": "1. Apri Threema e scansiona questo codice QR (oppure aggiungi l'ID sopra come contatto).\n2. Inviagli un messaggio per iniziare a chattare.\nAssicurati che il tuo ID Threema sia presente nell'elenco degli utenti autorizzati qui sotto: solo gli ID elencati ricevono una risposta.",
|
||||||
"telegramSteps": "Apri il bot Telegram che hai collegato e inviagli un messaggio per iniziare a chattare. Solo gli ID utente nell'elenco degli utenti autorizzati qui sotto ricevono una risposta.",
|
"telegramSteps": "Apri il bot Telegram che hai collegato e inviagli un messaggio per iniziare a chattare. Solo gli ID utente nell'elenco degli utenti autorizzati qui sotto ricevono una risposta.",
|
||||||
"discordSteps": "Scrivi al bot Discord che hai collegato, oppure menzionalo in un canale a cui si è unito. Solo gli ID utente nell'elenco degli utenti autorizzati qui sotto ricevono una risposta."
|
"discordSteps": "Scrivi al bot Discord che hai collegato, oppure menzionalo in un canale a cui si è unito. Solo gli ID utente nell'elenco degli utenti autorizzati qui sotto ricevono una risposta.",
|
||||||
|
"dismiss": "Mi sono collegato",
|
||||||
|
"show": "Mostra dettagli di connessione"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user