// Copyright: © 2026 TWWIM UG. All rights reserved. (www.twwim.com) /** * PluginSettingsPage * * Tabbed widget configuration UI. Four tabs: * - Visibility: master + per-device mount gates * - Snippet: server-side snippet config + generated copy-paste HTML * (the latter replaces the old SnippetConstructor that * lived inside TenantDetail) * - Welcome: welcome bubble + EN/DE phrases * - Idle: idle bubbles + EN/DE phrases * * The attention master toggle sits ABOVE the tabs — it's a cross-cutting * on/off for both welcome and idle, so it doesn't belong inside either. * * Save is disabled until the form is valid AND dirty (changed from the * most-recently-loaded server state). One Save commits the entire form * atomically across all tabs. * * @layer Presentation */ import { useState } from 'react'; import { usePluginSettings, useUpdatePluginSettings, } from './hooks/usePluginSettings'; import { usePluginSettingsForm } from './hooks/usePluginSettingsForm'; import { useTenant } from '@/features/tenants/hooks'; import { useTranslation } from '@/i18n/TranslationProvider'; import { PluginSettingsLoadError } from './components/PluginSettingsLoadError'; import { PluginSettingsSkeleton } from './components/PluginSettingsSkeleton'; import { EnabledToggle } from './components/EnabledToggle'; import { IdleSection } from './components/IdleSection'; import { PageHeader } from './components/PageHeader'; import { SaveBar } from './components/SaveBar'; import { SaveErrorBanner } from './components/SaveErrorBanner'; import { ValidationSummary } from './components/ValidationSummary'; import { SnippetSection } from './components/SnippetSection'; import { VisibilitySection } from './components/VisibilitySection'; import { WelcomeSection } from './components/WelcomeSection'; import { InstallationSnippetButton } from './components/InstallationSnippetButton'; import { TabBar, type PluginSettingsTab } from './components/TabBar'; import { normaliseForWire } from './lib/wire'; interface PluginSettingsPageProps { tenantId: string; /** Render the built-in PageHeader (back arrow + tenant name + intro). * Default true; the top-level /dashboard/plugin-settings route opts * out so its tenant selector replaces the header. */ hasHeader?: boolean; } export function PluginSettingsPage({ tenantId, hasHeader = true }: PluginSettingsPageProps) { const { t } = useTranslation(); const tenantQuery = useTenant(tenantId); const settingsQuery = usePluginSettings(tenantId); const updateMutation = useUpdatePluginSettings(tenantId); const { form, setForm, isDirty, validation, resetBaseline } = usePluginSettingsForm(settingsQuery.data); const [showSaved, setShowSaved] = useState(false); const [activeTab, setActiveTab] = useState('visibility'); const canSave = validation.isValid && isDirty && !updateMutation.isPending; const handleSave = async () => { if (!canSave) return; try { const saved = await updateMutation.mutateAsync(normaliseForWire(form)); resetBaseline(saved); setShowSaved(true); window.setTimeout(() => setShowSaved(false), 2500); } catch { /* surfaced via updateMutation.error */ } }; if (settingsQuery.isLoading || tenantQuery.isLoading) { return ; } if (settingsQuery.error) { return ; } return (
{hasHeader && ( )} setForm({ ...form, attention: { ...form.attention, enabled } }) } t={t} /> {activeTab === 'visibility' && ( )} {activeTab === 'snippet' && ( )} {activeTab === 'welcome' && ( )} {activeTab === 'idle' && ( )} {!validation.isValid && ( )} {updateMutation.error && ( )}
); }