diff --git a/docs/INTERFACE_PATTERN_MIGRATION.md b/docs/INTERFACE_PATTERN_MIGRATION.md new file mode 100644 index 0000000..74c7773 --- /dev/null +++ b/docs/INTERFACE_PATTERN_MIGRATION.md @@ -0,0 +1,27 @@ +# Notifications interface pattern migration + +Notifications uses the platform inbox/list-detail pattern for recipient work and the governed-operation pattern for delivery. + +## Surfaces + +- `notifications.route.notifications` remains the route identifier so existing Views keep working. +- `notifications.page.inbox` owns status filtering and notification selection. +- `notifications.page.detail` owns message, source, recipient, and read/acknowledgement state. +- `notifications.page.delivery` owns sanitized delivery-attempt evidence. +- `notifications.action.mark-read`, `notifications.action.acknowledge`, and `notifications.action.cancel` describe recipient actions on the selected record. +- `notifications.action.dispatch` describes the privileged bounded delivery operation. +- `notifications.settings.preferences` and `notifications.widget.summary` remain composed Settings and Dashboard surfaces. + +The backend and WebUI manifests publish the same identifiers and hierarchy. Notifications does not introduce a navigation entry because the title-bar bell is the platform entry point. + +## Consequences and recovery + +Read and acknowledgement actions record durable recipient state. Cancellation is confirmed and is available only before an outcome becomes provider-accepted or otherwise terminal; it stops eligible local work and is not presented as remote recall. Dispatch is confirmed separately and processes at most 50 eligible tenant notifications per request. Accepted outcomes are not retried blindly. + +Unavailable actions remain visible with explicit selection, permission, busy, terminal-state, and optional-Mail explanations. Personal preference drafts use the shared unsaved-change guard. The settings surface explains that in-product notifications continue when Mail is absent and that production email depends on the optional Mail capability. + +Contextual help resolves through `govoplan-docs` when installed and through the hosted fallback otherwise. Delivery evidence remains sanitized and never exposes provider credentials or private sibling-module state. + +## Optional boundaries + +Producing modules continue to own notification meaning. Notifications owns durable recipient and delivery state and calls optional Mail, Portal, Tasks, Calendar, Scheduling, and Workflow Engine integrations only through declared capabilities and references. diff --git a/src/govoplan_notifications/backend/manifest.py b/src/govoplan_notifications/backend/manifest.py index bacaf05..ce5ab9a 100644 --- a/src/govoplan_notifications/backend/manifest.py +++ b/src/govoplan_notifications/backend/manifest.py @@ -103,7 +103,23 @@ manifest = ModuleManifest( documentation_types=("user",), audience=("user",), related_modules=("mail", "calendar", "scheduling", "workflow_engine"), - metadata={"kind": "reference"}, + metadata={ + "kind": "reference", + "help_contexts": [ + "notifications.route.notifications", + "notifications.page.inbox", + "notifications.page.detail", + "notifications.settings.preferences", + "notifications.widget.summary", + "notifications.state.read-only", + ], + "consequence_classes": { + "mark_read": "record the notification as read for the current recipient", + "acknowledge": "record explicit recipient acknowledgement in addition to read state", + "cancel": "stop an eligible notification before provider acceptance without claiming remote recall", + "update_preferences": "replace the current user's badge, channel, digest, and source-muting preferences", + }, + }, ), DocumentationTopic( id="notifications.delivery-operations", @@ -113,7 +129,18 @@ manifest = ModuleManifest( documentation_types=("admin",), audience=("tenant_admin", "operator", "module_admin"), related_modules=("mail", "audit", "ops"), - metadata={"kind": "reference"}, + metadata={ + "kind": "reference", + "help_contexts": [ + "notifications.page.delivery", + "notifications.action.dispatch", + "notifications.state.delivery-unavailable", + ], + "consequence_classes": { + "dispatch_pending": "attempt delivery for up to the requested number of eligible tenant notifications", + "inspect_attempts": "read sanitized provider, status, timing, and error evidence for the selected notification", + }, + }, ), ), frontend=FrontendModule( @@ -125,22 +152,79 @@ manifest = ModuleManifest( component="NotificationCenterPage", required_any=(READ_SCOPE,), order=59, + surface_id="notifications.route.notifications", ), ), view_surfaces=( + ViewSurface( + id="notifications.page.inbox", + module_id=MODULE_ID, + kind="section", + label="Notification inbox", + parent_id="notifications.route.notifications", + order=20, + ), + ViewSurface( + id="notifications.page.detail", + module_id=MODULE_ID, + kind="section", + label="Notification details", + parent_id="notifications.route.notifications", + order=30, + ), + ViewSurface( + id="notifications.page.delivery", + module_id=MODULE_ID, + kind="section", + label="Notification delivery evidence", + parent_id="notifications.page.detail", + order=40, + ), + ViewSurface( + id="notifications.action.mark-read", + module_id=MODULE_ID, + kind="action", + label="Mark notification as read", + parent_id="notifications.page.detail", + order=50, + ), + ViewSurface( + id="notifications.action.acknowledge", + module_id=MODULE_ID, + kind="action", + label="Acknowledge notification", + parent_id="notifications.page.detail", + order=60, + ), + ViewSurface( + id="notifications.action.cancel", + module_id=MODULE_ID, + kind="action", + label="Cancel notification delivery", + parent_id="notifications.page.delivery", + order=70, + ), + ViewSurface( + id="notifications.action.dispatch", + module_id=MODULE_ID, + kind="action", + label="Dispatch pending notifications", + parent_id="notifications.page.delivery", + order=80, + ), ViewSurface( id="notifications.widget.summary", module_id=MODULE_ID, kind="section", label="Notification summary widget", - order=30, + order=90, ), ViewSurface( id="notifications.settings.preferences", module_id=MODULE_ID, kind="section", label="Notification preferences", - order=40, + order=100, ), ), ), diff --git a/src/govoplan_notifications/backend/service.py b/src/govoplan_notifications/backend/service.py index fb13e45..64a0da2 100644 --- a/src/govoplan_notifications/backend/service.py +++ b/src/govoplan_notifications/backend/service.py @@ -322,6 +322,10 @@ def update_notification( notification.read_at = notification.read_at or now notification.acknowledged_at = notification.acknowledged_at or now elif payload.status == "cancelled": + if notification.status not in PENDING_STATUSES | {"paused"}: + raise NotificationError( + f"Notification cannot be cancelled from status {notification.status}" + ) notification.status = "cancelled" notification.cancelled_at = notification.cancelled_at or now if payload.metadata is not None: diff --git a/tests/test_interface_documentation_contract.py b/tests/test_interface_documentation_contract.py new file mode 100644 index 0000000..1694f75 --- /dev/null +++ b/tests/test_interface_documentation_contract.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +from pathlib import Path +import unittest + +from govoplan_notifications.backend.manifest import get_manifest + + +REPO_ROOT = Path(__file__).resolve().parents[1] + + +class NotificationsInterfaceDocumentationContractTests(unittest.TestCase): + def test_backend_surfaces_and_hierarchy_remain_declared(self) -> None: + frontend = get_manifest().frontend + self.assertIsNotNone(frontend) + surfaces = {item.id: item for item in frontend.view_surfaces} # type: ignore[union-attr] + expected = { + "notifications.page.inbox", + "notifications.page.detail", + "notifications.page.delivery", + "notifications.action.mark-read", + "notifications.action.acknowledge", + "notifications.action.cancel", + "notifications.action.dispatch", + "notifications.settings.preferences", + "notifications.widget.summary", + } + self.assertEqual(expected, set(surfaces)) + self.assertEqual( + "notifications.route.notifications", + frontend.routes[0].surface_id, # type: ignore[union-attr] + ) + self.assertEqual( + "notifications.route.notifications", + surfaces["notifications.page.inbox"].parent_id, + ) + self.assertEqual( + "notifications.route.notifications", + surfaces["notifications.page.detail"].parent_id, + ) + self.assertEqual( + "notifications.page.detail", + surfaces["notifications.page.delivery"].parent_id, + ) + self.assertEqual( + "notifications.page.delivery", + surfaces["notifications.action.dispatch"].parent_id, + ) + + def test_help_and_consequence_metadata_remain_published(self) -> None: + topics = {topic.id: topic for topic in get_manifest().documentation} + center = topics["notifications.center-and-preferences"] + delivery = topics["notifications.delivery-operations"] + + self.assertIn("notifications.page.detail", center.metadata["help_contexts"]) + self.assertIn("notifications.settings.preferences", center.metadata["help_contexts"]) + self.assertIn("acknowledge", center.metadata["consequence_classes"]) + self.assertIn("cancel", center.metadata["consequence_classes"]) + self.assertIn("notifications.action.dispatch", delivery.metadata["help_contexts"]) + self.assertIn("dispatch_pending", delivery.metadata["consequence_classes"]) + + def test_webui_uses_shared_consequence_and_draft_patterns(self) -> None: + center = (REPO_ROOT / "webui/src/features/notifications/NotificationCenterPage.tsx").read_text(encoding="utf-8") + settings = (REPO_ROOT / "webui/src/features/notifications/NotificationSettingsPanel.tsx").read_text(encoding="utf-8") + widget = (REPO_ROOT / "webui/src/features/notifications/NotificationSummaryWidget.tsx").read_text(encoding="utf-8") + + for component in ( + "ActionBlockerHint", + "ConfirmDialog", + "DocumentationHelpLink", + "SelectionList", + ): + self.assertIn(component, center) + for component in ( + "ActionBlockerHint", + "DocumentationHelpLink", + "ReferenceMultiSelect", + "useUnsavedDraftGuard", + ): + self.assertIn(component, settings) + self.assertIn("DocumentationHelpLink", widget) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_notifications.py b/tests/test_notifications.py index 29feaf5..4829995 100644 --- a/tests/test_notifications.py +++ b/tests/test_notifications.py @@ -21,12 +21,14 @@ from govoplan_notifications.backend.db.models import NotificationDeliveryAttempt from govoplan_notifications.backend.router import api_get_notification, api_list_notifications, api_notification_summary, api_update_notification from govoplan_notifications.backend.schemas import NotificationCreateRequest, NotificationPreferencesUpdateRequest, NotificationUpdateRequest from govoplan_notifications.backend.service import ( + NotificationError, create_notification, deliver_pending, notification_preferences_response, notification_response, notification_summary, update_notification_preferences, + update_notification, ) @@ -222,6 +224,37 @@ class NotificationServiceTests(unittest.TestCase): session.commit() enqueue.assert_not_called() + def test_cancellation_is_limited_to_locally_controllable_delivery_states(self) -> None: + with self.Session() as session: + notification = create_notification( + session, + tenant_id="tenant-1", + payload=self._inbox_payload(recipient_id="user-1", subject="Cancellation boundary"), + ) + notification.status = "sent" + session.flush() + + with self.assertRaisesRegex( + NotificationError, + "cannot be cancelled from status sent", + ): + update_notification( + session, + tenant_id="tenant-1", + notification_id=notification.id, + payload=NotificationUpdateRequest(status="cancelled"), + ) + + notification.status = "queued" + cancelled = update_notification( + session, + tenant_id="tenant-1", + notification_id=notification.id, + payload=NotificationUpdateRequest(status="cancelled"), + ) + self.assertEqual("cancelled", cancelled.status) + self.assertIsNotNone(cancelled.cancelled_at) + def test_action_url_accepts_only_application_relative_paths(self) -> None: payload = self._inbox_payload(recipient_id="user-1", subject="Safe action") safe = payload.model_copy(update={"action_url": "/calendar?event=event-1#details"}) diff --git a/webui/scripts/test-notification-page-structure.mjs b/webui/scripts/test-notification-page-structure.mjs index 8048888..24f0ea8 100644 --- a/webui/scripts/test-notification-page-structure.mjs +++ b/webui/scripts/test-notification-page-structure.mjs @@ -8,11 +8,14 @@ const page = readFileSync(pagePath, "utf8"); const styles = readFileSync(stylesPath, "utf8"); assert.match(page, /SelectionList,[\s\S]*SelectionListItem,[\s\S]*from "@govoplan\/core-webui"/); -assert.match(page, //); +assert.match(page, //); assert.match(page, /([]); const [selectedId, setSelectedId] = useState(""); @@ -36,12 +59,28 @@ export default function NotificationCenterPage({ settings, auth }: { settings: A const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); + const [confirmingAction, setConfirmingAction] = useState<"cancel" | "dispatch" | null>(null); const canRead = hasScope(auth, "notifications:notification:read"); const canWrite = hasScope(auth, "notifications:notification:write"); const canDispatch = hasScope(auth, "notifications:delivery:dispatch"); const selected = useMemo(() => notifications.find((item) => item.id === selectedId) || notifications[0] || null, [notifications, selectedId]); const unreadCount = notifications.filter((item) => !item.read_at && !["cancelled", "skipped"].includes(item.status)).length; + const commonSelectionReason = busy + ? NOTIFICATIONS_I18N.actionActive + : !selected + ? NOTIFICATIONS_I18N.selectionRequired + : !canWrite + ? NOTIFICATIONS_I18N.writePermissionRequired + : undefined; + const markReadDisabledReason = commonSelectionReason ?? (selected?.read_at ? NOTIFICATIONS_I18N.alreadyRead : undefined); + const acknowledgeDisabledReason = commonSelectionReason ?? (selected?.acknowledged_at ? NOTIFICATIONS_I18N.alreadyAcknowledged : undefined); + const cancelDisabledReason = commonSelectionReason ?? (selected && !cancellableStatuses.has(selected.status) ? NOTIFICATIONS_I18N.notCancellable : undefined); + const dispatchDisabledReason = busy + ? NOTIFICATIONS_I18N.actionActive + : !canDispatch + ? NOTIFICATIONS_I18N.dispatchPermissionRequired + : undefined; useEffect(() => { if (!canRead) { @@ -68,43 +107,65 @@ export default function NotificationCenterPage({ settings, auth }: { settings: A } } - async function markSelected(status: "read" | "acknowledged" | "cancelled") { - if (!selected || !canWrite) return; + async function markSelected(status: "read" | "acknowledged" | "cancelled"): Promise { + if (!selected || !canWrite) return false; setBusy(true); setError(""); try { const next = await updateNotification(settings, selected.id, { status }); setNotifications((items) => items.map((item) => item.id === next.id ? next : item)); notifyNotificationsChanged(); + return true; } catch (err) { setError(errorMessage(err)); + return false; } finally { setBusy(false); } } - async function runDelivery() { - if (!canDispatch) return; + async function runDelivery(): Promise { + if (!canDispatch) return false; setBusy(true); setError(""); try { await deliverPendingNotifications(settings, 50); await load(); notifyNotificationsChanged(); + return true; } catch (err) { setError(errorMessage(err)); + return false; } finally { setBusy(false); } } + async function confirmAction() { + const succeeded = confirmingAction === "cancel" + ? await markSelected("cancelled") + : confirmingAction === "dispatch" + ? await runDelivery() + : false; + if (succeeded) setConfirmingAction(null); + } + if (!canRead) { return (
-
- -

Notifications

-

You do not have permission to read notifications in this tenant.

+
+
); @@ -117,24 +178,30 @@ export default function NotificationCenterPage({ settings, auth }: { settings: A
- Notifications + i18n:govoplan-notifications.notifications {unreadCount > 0 ? {unreadCount} : null}
-
({ id: status, label: status }))} + options={statusFilters.map((status) => ({ id: status, label: statusLabel(status) }))} value={statusFilter} onChange={setStatusFilter} - ariaLabel="Notification status" + ariaLabel="i18n:govoplan-notifications.notification_status" width="fill" />
- {loading ?
Loading notifications
: null} - {!loading && notifications.length === 0 ?
No notifications in this view.
: null} + {loading ?
i18n:govoplan-notifications.loading_notifications
: null} + {!loading && notifications.length === 0 ?
i18n:govoplan-notifications.no_notifications
: null} {notifications.length > 0 ? ( - + {notifications.map((notification) => (
- {selected?.subject || selected?.event_kind || "Notification center"} + {selected?.subject || selected?.event_kind || "i18n:govoplan-notifications.surface.center"}
- - - + - {canDispatch ? ( - - ) : null}
{error ? {error} : null} + {selected && !canWrite ? ( + + ) : null} + {selected ? : (
-

Notifications

-

Select a notification to inspect delivery state, source context, and content.

+

i18n:govoplan-notifications.notifications

+

i18n:govoplan-notifications.select_notification_help

)} + setConfirmingAction(null)} + onConfirm={() => void confirmAction()} + /> + setConfirmingAction(null)} + onConfirm={() => void confirmAction()} + /> ); } @@ -206,32 +306,35 @@ function NotificationDetails({ notification }: { notification: NotificationMessa {formatDate(notification.created_at)}

{notification.subject || notification.event_kind}

- {notification.body_text ?

{notification.body_text}

:

No message body was provided.

} + {notification.body_text ?

{notification.body_text}

:

i18n:govoplan-notifications.no_message_body

} {actionUrl ? ( - Open related item + i18n:govoplan-notifications.open_related_item ) : null}
-

Source and delivery

+

i18n:govoplan-notifications.source_and_delivery

-
Source
{notification.source_module} / {notification.source_resource_type}
-
Resource
{notification.source_resource_id || "None"}
-
Recipient
{notification.recipient_label || notification.recipient || notification.recipient_id || "None"}
-
Priority
{notification.priority}
-
Queued
{formatDate(notification.queued_at)}
-
Sent
{formatDate(notification.sent_at)}
-
Read
{formatDate(notification.read_at)}
-
Attempts
{notification.attempt_count}
+
i18n:govoplan-notifications.source
{notification.source_module} / {notification.source_resource_type}
+
i18n:govoplan-notifications.resource
{notification.source_resource_id || "i18n:govoplan-notifications.none"}
+
i18n:govoplan-notifications.recipient
{notification.recipient_label || notification.recipient || notification.recipient_id || "i18n:govoplan-notifications.none"}
+
i18n:govoplan-notifications.priority
{notification.priority}
+
i18n:govoplan-notifications.queued
{formatDate(notification.queued_at)}
+
i18n:govoplan-notifications.sent
{formatDate(notification.sent_at)}
+
i18n:govoplan-notifications.read
{formatDate(notification.read_at)}
+
i18n:govoplan-notifications.attempts
{notification.attempt_count}
{notification.last_error ?

{notification.last_error}

: null}
-

Delivery attempts

- {notification.attempts.length === 0 ?

No delivery attempt has been recorded yet.

: null} +
+

i18n:govoplan-notifications.delivery_attempts

+ +
+ {notification.attempts.length === 0 ?

i18n:govoplan-notifications.no_delivery_attempt

: null} {notification.attempts.map((attempt) => (
{attempt.provider || attempt.channel} @@ -246,11 +349,15 @@ function NotificationDetails({ notification }: { notification: NotificationMessa } function formatStatus(value: string): string { - return value.replace(/_/g, " "); + return statusLabel(value); +} + +function statusLabel(value: string): string { + return `i18n:govoplan-notifications.status.${value.replace(/[^a-z0-9]+/gi, "_").toLowerCase()}`; } function formatDate(value?: string | null): string { - if (!value) return "Not set"; + if (!value) return "i18n:govoplan-notifications.not_set"; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return new Intl.DateTimeFormat(undefined, { @@ -260,7 +367,7 @@ function formatDate(value?: string | null): string { } function errorMessage(error: unknown): string { - return error instanceof Error ? error.message : "Request failed"; + return error instanceof Error ? error.message : "i18n:govoplan-notifications.request_failed"; } function notifyNotificationsChanged(): void { diff --git a/webui/src/features/notifications/NotificationSettingsPanel.tsx b/webui/src/features/notifications/NotificationSettingsPanel.tsx index 67fcb07..0a73157 100644 --- a/webui/src/features/notifications/NotificationSettingsPanel.tsx +++ b/webui/src/features/notifications/NotificationSettingsPanel.tsx @@ -1,24 +1,35 @@ import { useEffect, useMemo, useState } from "react"; import { Mail, Save } from "lucide-react"; import { + ActionBlockerHint, Button, Card, DismissibleAlert, + DocumentationHelpLink, FormField, ReferenceMultiSelect, ToggleSwitch, + hasScope, + i18nMessage, isViewSurfaceVisible, moduleViewSurfaceId, platformModuleReferenceProvider, useEffectiveView, usePlatformLanguage, usePlatformModules, + useUnsavedDraftGuard, useViewSurfaces, type ApiSettings, type AuthInfo, type ReferenceOption } from "@govoplan/core-webui"; import { getNotificationPreferences, updateNotificationPreferences, type NotificationPreferences } from "../../api/notifications"; +import { + NOTIFICATIONS_BLOCKER_LABELS, + NOTIFICATIONS_DELIVERY_DOCUMENTATION, + NOTIFICATIONS_DOCUMENTATION, + NOTIFICATIONS_I18N +} from "./interfacePatterns"; type Draft = Pick & { muted_source_modules: string[]; @@ -42,6 +53,8 @@ export default function NotificationSettingsPanel({ settings, auth }: { settings const [saving, setSaving] = useState(false); const [message, setMessage] = useState(""); const [messageTone, setMessageTone] = useState<"success" | "warning">("success"); + const canWrite = hasScope(auth, "notifications:notification:write"); + const mailAvailable = modules.some((module) => module.id === "mail"); const dirty = useMemo(() => { if (!loaded) return false; @@ -64,8 +77,8 @@ export default function NotificationSettingsPanel({ settings, auth }: { settings label: translateText(module.label), description: [ module.id, - `version ${module.version}`, - visible ? null : "Hidden by the active view" + i18nMessage("i18n:govoplan-notifications.version_value", { value0: module.version }), + visible ? null : translateText("i18n:govoplan-notifications.hidden_by_view") ].filter(Boolean).join(" · "), kind: "module", availability: visible ? "available" : "unavailable", @@ -84,6 +97,22 @@ export default function NotificationSettingsPanel({ settings, auth }: { settings [moduleOptions, settings] ); + function resetDraft() { + if (!loaded) return; + setDraft({ + show_unread_badge: loaded.show_unread_badge, + email_enabled: loaded.email_enabled, + email_digest_enabled: loaded.email_digest_enabled, + muted_source_modules: loaded.muted_source_modules + }); + } + + useUnsavedDraftGuard({ + dirty, + onSave: savePreferences, + onDiscard: resetDraft + }); + useEffect(() => { void loadPreferences(); }, [auth.user.id, settings.accessToken, settings.apiBaseUrl, settings.apiKey]); @@ -102,13 +131,14 @@ export default function NotificationSettingsPanel({ settings, auth }: { settings }); } catch (error) { setMessageTone("warning"); - setMessage(error instanceof Error ? error.message : "Loading notification preferences failed"); + setMessage(error instanceof Error ? error.message : translateText("i18n:govoplan-notifications.preferences_load_failed")); } finally { setLoading(false); } } - async function savePreferences() { + async function savePreferences(): Promise { + if (!canWrite) return false; setSaving(true); setMessage(""); try { @@ -127,65 +157,112 @@ export default function NotificationSettingsPanel({ settings, auth }: { settings }); window.dispatchEvent(new CustomEvent("govoplan:notifications-changed")); setMessageTone("success"); - setMessage("Notification preferences saved."); + setMessage("i18n:govoplan-notifications.preferences_saved"); + return true; } catch (error) { setMessageTone("warning"); - setMessage(error instanceof Error ? error.message : "Saving notification preferences failed"); + setMessage(error instanceof Error ? error.message : translateText("i18n:govoplan-notifications.preferences_save_failed")); + return false; } finally { setSaving(false); } } + const saveDisabledReason = loading + ? NOTIFICATIONS_I18N.loading + : saving + ? NOTIFICATIONS_I18N.saving + : !canWrite + ? NOTIFICATIONS_I18N.writePermissionRequired + : !dirty + ? NOTIFICATIONS_I18N.noChanges + : undefined; + const preferenceControlsDisabled = loading || saving || !canWrite; + const emailToggleDisabled = preferenceControlsDisabled || (!mailAvailable && !draft.email_enabled); + const digestToggleDisabled = preferenceControlsDisabled || !mailAvailable || !draft.email_enabled; + return (
- +
+ +
+ {!canWrite ? ( + + ) : null} +
setDraft((current) => ({ ...current, show_unread_badge: value }))} /> - + setDraft((current) => ({ ...current, muted_source_modules })) } provider={moduleProvider} - aria-label="Muted source modules" - placeholder="Add a source module" - emptyText="No visible modules match." - disabled={loading} + aria-label="i18n:govoplan-notifications.muted_sources" + placeholder="i18n:govoplan-notifications.add_source_module" + emptyText="i18n:govoplan-notifications.no_visible_modules" + disabled={preferenceControlsDisabled} />
-
{message ? {message} : null}
- +
+
- Email notifications + i18n:govoplan-notifications.email_notifications
+ {!mailAvailable ? ( + + ) : null} setDraft((current) => ({ ...current, email_enabled: value, email_digest_enabled: value ? current.email_digest_enabled : false }))} /> setDraft((current) => ({ ...current, email_digest_enabled: value }))} />
diff --git a/webui/src/features/notifications/NotificationSummaryWidget.tsx b/webui/src/features/notifications/NotificationSummaryWidget.tsx index 4792938..e51b987 100644 --- a/webui/src/features/notifications/NotificationSummaryWidget.tsx +++ b/webui/src/features/notifications/NotificationSummaryWidget.tsx @@ -1,13 +1,16 @@ import { Link } from "react-router"; import { DismissibleAlert, + DocumentationHelpLink, LoadingFrame, MetricCard, adminErrorMessage, + i18nMessage, type ApiSettings, type DashboardWidgetConfiguration, useSharedNotificationSummary } from "@govoplan/core-webui"; +import { NOTIFICATIONS_DOCUMENTATION } from "./interfacePatterns"; export default function NotificationSummaryWidget({ settings, @@ -31,7 +34,7 @@ export default function NotificationSummaryWidget({ const error = state.error ? adminErrorMessage(state.error) : ""; return ( - + {error && ( {error} @@ -39,35 +42,36 @@ export default function NotificationSummaryWidget({ )}
{showDeliveryState && ( )} {showDeliveryState && ( )}
- {showCenterLink && ( -
+
+ + {showCenterLink && ( - Open notification center + i18n:govoplan-notifications.open_center -
- )} + )} +
); } diff --git a/webui/src/features/notifications/interfacePatterns.ts b/webui/src/features/notifications/interfacePatterns.ts new file mode 100644 index 0000000..1f52937 --- /dev/null +++ b/webui/src/features/notifications/interfacePatterns.ts @@ -0,0 +1,32 @@ +import type { DocumentationHelpReference } from "@govoplan/core-webui"; + +export const NOTIFICATIONS_DOCUMENTATION = { + topicId: "notifications.center-and-preferences", + documentationType: "user" +} satisfies DocumentationHelpReference; + +export const NOTIFICATIONS_DELIVERY_DOCUMENTATION = { + topicId: "notifications.delivery-operations", + documentationType: "admin" +} satisfies DocumentationHelpReference; + +export const NOTIFICATIONS_I18N = { + loading: "i18n:govoplan-notifications.reason.loading", + saving: "i18n:govoplan-notifications.reason.saving", + actionActive: "i18n:govoplan-notifications.reason.action_active", + selectionRequired: "i18n:govoplan-notifications.reason.selection_required", + readPermissionRequired: "i18n:govoplan-notifications.reason.read_permission_required", + writePermissionRequired: "i18n:govoplan-notifications.reason.write_permission_required", + dispatchPermissionRequired: "i18n:govoplan-notifications.reason.dispatch_permission_required", + alreadyRead: "i18n:govoplan-notifications.reason.already_read", + alreadyAcknowledged: "i18n:govoplan-notifications.reason.already_acknowledged", + notCancellable: "i18n:govoplan-notifications.reason.not_cancellable", + noChanges: "i18n:govoplan-notifications.reason.no_changes", + mailUnavailable: "i18n:govoplan-notifications.reason.mail_unavailable" +} as const; + +export const NOTIFICATIONS_BLOCKER_LABELS = { + requiredAction: "i18n:govoplan-notifications.blocker.required_action", + actor: "i18n:govoplan-notifications.blocker.actor", + target: "i18n:govoplan-notifications.blocker.target" +} as const; diff --git a/webui/src/i18n/generatedTranslations.ts b/webui/src/i18n/generatedTranslations.ts index b2d1925..d7f994a 100644 --- a/webui/src/i18n/generatedTranslations.ts +++ b/webui/src/i18n/generatedTranslations.ts @@ -1,4 +1,224 @@ -export const generatedTranslations = { - en: {}, - de: {} +import type { PlatformTranslations } from "@govoplan/core-webui"; + +export const generatedTranslations: PlatformTranslations = { + en: { + "i18n:govoplan-notifications.surface.center": "Notification center", + "i18n:govoplan-notifications.surface.inbox": "Notification inbox", + "i18n:govoplan-notifications.surface.detail": "Notification details", + "i18n:govoplan-notifications.surface.delivery": "Notification delivery evidence", + "i18n:govoplan-notifications.surface.mark_read": "Mark notification as read", + "i18n:govoplan-notifications.surface.acknowledge": "Acknowledge notification", + "i18n:govoplan-notifications.surface.cancel": "Cancel notification delivery", + "i18n:govoplan-notifications.surface.dispatch": "Dispatch pending notifications", + "i18n:govoplan-notifications.surface.widget_summary": "Notification summary widget", + "i18n:govoplan-notifications.surface.preferences": "Notification preferences", + "i18n:govoplan-notifications.widget_description": "Unread notifications and delivery state.", + "i18n:govoplan-notifications.communication": "Communication", + "i18n:govoplan-notifications.show_delivery_state": "Show delivery state", + "i18n:govoplan-notifications.show_delivery_state_help": "Include pending and failed delivery counts.", + "i18n:govoplan-notifications.reason.loading": "Notifications are loading.", + "i18n:govoplan-notifications.reason.saving": "Notification preferences are being saved.", + "i18n:govoplan-notifications.reason.action_active": "A notification action is already running.", + "i18n:govoplan-notifications.reason.selection_required": "Select a notification first.", + "i18n:govoplan-notifications.reason.read_permission_required": "Notification-read permission is required.", + "i18n:govoplan-notifications.reason.write_permission_required": "Notification-write permission is required.", + "i18n:govoplan-notifications.reason.dispatch_permission_required": "Notification-dispatch permission is required.", + "i18n:govoplan-notifications.reason.already_read": "This notification is already marked as read.", + "i18n:govoplan-notifications.reason.already_acknowledged": "This notification is already acknowledged.", + "i18n:govoplan-notifications.reason.not_cancellable": "Only pending, queued, paused, or failed delivery can be cancelled safely.", + "i18n:govoplan-notifications.reason.no_changes": "No preference changes need to be saved.", + "i18n:govoplan-notifications.reason.mail_unavailable": "The optional Mail module is not enabled. In-product notifications remain available.", + "i18n:govoplan-notifications.blocker.required_action": "Required action", + "i18n:govoplan-notifications.blocker.actor": "Who can fix it", + "i18n:govoplan-notifications.blocker.target": "Where to go", + "i18n:govoplan-notifications.read_blocked_summary": "The notification center is unavailable.", + "i18n:govoplan-notifications.actions_read_only_summary": "Notification actions are read-only.", + "i18n:govoplan-notifications.preferences_read_only_summary": "Notification preferences are read-only.", + "i18n:govoplan-notifications.permission_action": "Ask a tenant administrator to grant the required Notifications permission.", + "i18n:govoplan-notifications.permission_actor": "Tenant administrator or access manager", + "i18n:govoplan-notifications.permission_target": "Administration > Access > Roles", + "i18n:govoplan-notifications.mail_unavailable_summary": "Email delivery is unavailable.", + "i18n:govoplan-notifications.mail_unavailable_action": "Enable and configure Mail before turning on production email notifications.", + "i18n:govoplan-notifications.mail_unavailable_actor": "System or tenant administrator", + "i18n:govoplan-notifications.mail_unavailable_target": "Administration > Modules and Mail settings", + "i18n:govoplan-notifications.refresh": "Refresh notifications", + "i18n:govoplan-notifications.notifications": "Notifications", + "i18n:govoplan-notifications.notification_status": "Notification status", + "i18n:govoplan-notifications.loading_notifications": "Loading notifications", + "i18n:govoplan-notifications.no_notifications": "No notifications in this view.", + "i18n:govoplan-notifications.mark_read": "Mark read", + "i18n:govoplan-notifications.acknowledge": "Acknowledge", + "i18n:govoplan-notifications.cancel_delivery": "Cancel delivery", + "i18n:govoplan-notifications.dispatch_pending": "Dispatch pending", + "i18n:govoplan-notifications.cancel_delivery_title": "Cancel notification delivery", + "i18n:govoplan-notifications.cancel_delivery_message": "Cancel delivery of {value0}. This stops eligible local delivery work but cannot recall provider-accepted messages.", + "i18n:govoplan-notifications.dispatch_pending_title": "Dispatch pending notifications", + "i18n:govoplan-notifications.dispatch_pending_message": "Attempt delivery for up to 50 eligible notifications in this tenant. Provider-accepted outcomes will not be repeated automatically.", + "i18n:govoplan-notifications.selected_notification": "the selected notification", + "i18n:govoplan-notifications.delivery_attempts": "Delivery attempts", + "i18n:govoplan-notifications.select_notification_help": "Select a notification to inspect delivery state, source context, and content.", + "i18n:govoplan-notifications.no_message_body": "No message body was provided.", + "i18n:govoplan-notifications.open_related_item": "Open related item", + "i18n:govoplan-notifications.source_and_delivery": "Source and delivery", + "i18n:govoplan-notifications.source": "Source", + "i18n:govoplan-notifications.resource": "Resource", + "i18n:govoplan-notifications.recipient": "Recipient", + "i18n:govoplan-notifications.priority": "Priority", + "i18n:govoplan-notifications.queued": "Queued", + "i18n:govoplan-notifications.sent": "Sent", + "i18n:govoplan-notifications.read": "Read", + "i18n:govoplan-notifications.attempts": "Attempts", + "i18n:govoplan-notifications.none": "None", + "i18n:govoplan-notifications.no_delivery_attempt": "No delivery attempt has been recorded yet.", + "i18n:govoplan-notifications.not_set": "Not set", + "i18n:govoplan-notifications.request_failed": "Request failed", + "i18n:govoplan-notifications.preferences_saved": "Notification preferences saved.", + "i18n:govoplan-notifications.preferences_load_failed": "Loading notification preferences failed.", + "i18n:govoplan-notifications.preferences_save_failed": "Saving notification preferences failed.", + "i18n:govoplan-notifications.save_preferences": "Save preferences", + "i18n:govoplan-notifications.saving": "Saving", + "i18n:govoplan-notifications.version_value": "version {value0}", + "i18n:govoplan-notifications.hidden_by_view": "Hidden by the active view", + "i18n:govoplan-notifications.unread_badge": "Unread badge", + "i18n:govoplan-notifications.unread_badge_help": "Show unread notification counts in the top bar.", + "i18n:govoplan-notifications.muted_sources": "Muted source modules", + "i18n:govoplan-notifications.muted_sources_help": "Choose active modules. Saved modules hidden by the current view remain visible here and are not discarded.", + "i18n:govoplan-notifications.add_source_module": "Add a source module", + "i18n:govoplan-notifications.no_visible_modules": "No visible modules match.", + "i18n:govoplan-notifications.delivery": "Delivery", + "i18n:govoplan-notifications.email_notifications": "Email notifications", + "i18n:govoplan-notifications.email_notifications_help": "Deliver production email through the optional Mail module. In-product notifications continue to work when Mail is unavailable.", + "i18n:govoplan-notifications.email_digest": "Email digest", + "i18n:govoplan-notifications.email_digest_help": "Batch eligible notifications into summary messages when delivery rules support it.", + "i18n:govoplan-notifications.loading_summary": "Loading notification summary", + "i18n:govoplan-notifications.unread": "Unread", + "i18n:govoplan-notifications.pending": "Pending", + "i18n:govoplan-notifications.failed": "Failed", + "i18n:govoplan-notifications.value_total": "{value0} total", + "i18n:govoplan-notifications.awaiting_delivery": "Awaiting delivery", + "i18n:govoplan-notifications.delivery_failures": "Delivery failures", + "i18n:govoplan-notifications.open_center": "Open notification center", + "i18n:govoplan-notifications.status.all": "All", + "i18n:govoplan-notifications.status.pending": "Pending", + "i18n:govoplan-notifications.status.queued": "Queued", + "i18n:govoplan-notifications.status.sending": "Sending", + "i18n:govoplan-notifications.status.accepted": "Accepted", + "i18n:govoplan-notifications.status.paused": "Paused", + "i18n:govoplan-notifications.status.sent": "Sent", + "i18n:govoplan-notifications.status.failed": "Failed", + "i18n:govoplan-notifications.status.skipped": "Skipped", + "i18n:govoplan-notifications.status.cancelled": "Cancelled", + "i18n:govoplan-notifications.status.read": "Read", + "i18n:govoplan-notifications.status.acknowledged": "Acknowledged" + }, + de: { + "i18n:govoplan-notifications.surface.center": "Benachrichtigungszentrale", + "i18n:govoplan-notifications.surface.inbox": "Benachrichtigungseingang", + "i18n:govoplan-notifications.surface.detail": "Benachrichtigungsdetails", + "i18n:govoplan-notifications.surface.delivery": "Zustellnachweise für Benachrichtigungen", + "i18n:govoplan-notifications.surface.mark_read": "Benachrichtigung als gelesen markieren", + "i18n:govoplan-notifications.surface.acknowledge": "Benachrichtigung bestätigen", + "i18n:govoplan-notifications.surface.cancel": "Zustellung der Benachrichtigung abbrechen", + "i18n:govoplan-notifications.surface.dispatch": "Ausstehende Benachrichtigungen zustellen", + "i18n:govoplan-notifications.surface.widget_summary": "Übersicht der Benachrichtigungen", + "i18n:govoplan-notifications.surface.preferences": "Benachrichtigungseinstellungen", + "i18n:govoplan-notifications.widget_description": "Ungelesene Benachrichtigungen und Zustellstatus.", + "i18n:govoplan-notifications.communication": "Kommunikation", + "i18n:govoplan-notifications.show_delivery_state": "Zustellstatus anzeigen", + "i18n:govoplan-notifications.show_delivery_state_help": "Ausstehende und fehlgeschlagene Zustellungen einbeziehen.", + "i18n:govoplan-notifications.reason.loading": "Benachrichtigungen werden geladen.", + "i18n:govoplan-notifications.reason.saving": "Benachrichtigungseinstellungen werden gespeichert.", + "i18n:govoplan-notifications.reason.action_active": "Eine Benachrichtigungsaktion wird bereits ausgeführt.", + "i18n:govoplan-notifications.reason.selection_required": "Wählen Sie zuerst eine Benachrichtigung aus.", + "i18n:govoplan-notifications.reason.read_permission_required": "Die Berechtigung zum Lesen von Benachrichtigungen ist erforderlich.", + "i18n:govoplan-notifications.reason.write_permission_required": "Die Berechtigung zum Bearbeiten von Benachrichtigungen ist erforderlich.", + "i18n:govoplan-notifications.reason.dispatch_permission_required": "Die Berechtigung zum Zustellen von Benachrichtigungen ist erforderlich.", + "i18n:govoplan-notifications.reason.already_read": "Diese Benachrichtigung ist bereits als gelesen markiert.", + "i18n:govoplan-notifications.reason.already_acknowledged": "Diese Benachrichtigung wurde bereits bestätigt.", + "i18n:govoplan-notifications.reason.not_cancellable": "Nur ausstehende, eingereihte, pausierte oder fehlgeschlagene Zustellungen können sicher abgebrochen werden.", + "i18n:govoplan-notifications.reason.no_changes": "Es müssen keine geänderten Einstellungen gespeichert werden.", + "i18n:govoplan-notifications.reason.mail_unavailable": "Das optionale Mail-Modul ist nicht aktiviert. Benachrichtigungen in GovOPlaN bleiben verfügbar.", + "i18n:govoplan-notifications.blocker.required_action": "Erforderliche Aktion", + "i18n:govoplan-notifications.blocker.actor": "Zuständige Stelle", + "i18n:govoplan-notifications.blocker.target": "Ziel", + "i18n:govoplan-notifications.read_blocked_summary": "Die Benachrichtigungszentrale ist nicht verfügbar.", + "i18n:govoplan-notifications.actions_read_only_summary": "Benachrichtigungsaktionen sind schreibgeschützt.", + "i18n:govoplan-notifications.preferences_read_only_summary": "Benachrichtigungseinstellungen sind schreibgeschützt.", + "i18n:govoplan-notifications.permission_action": "Bitten Sie eine mandantenverwaltende Person, die erforderliche Benachrichtigungsberechtigung zu erteilen.", + "i18n:govoplan-notifications.permission_actor": "Mandantenadministration oder Zugriffsverwaltung", + "i18n:govoplan-notifications.permission_target": "Administration > Zugriff > Rollen", + "i18n:govoplan-notifications.mail_unavailable_summary": "E-Mail-Zustellung ist nicht verfügbar.", + "i18n:govoplan-notifications.mail_unavailable_action": "Aktivieren und konfigurieren Sie Mail, bevor produktive E-Mail-Benachrichtigungen eingeschaltet werden.", + "i18n:govoplan-notifications.mail_unavailable_actor": "System- oder Mandantenadministration", + "i18n:govoplan-notifications.mail_unavailable_target": "Administration > Module und Mail-Einstellungen", + "i18n:govoplan-notifications.refresh": "Benachrichtigungen aktualisieren", + "i18n:govoplan-notifications.notifications": "Benachrichtigungen", + "i18n:govoplan-notifications.notification_status": "Benachrichtigungsstatus", + "i18n:govoplan-notifications.loading_notifications": "Benachrichtigungen werden geladen", + "i18n:govoplan-notifications.no_notifications": "In dieser Ansicht gibt es keine Benachrichtigungen.", + "i18n:govoplan-notifications.mark_read": "Als gelesen markieren", + "i18n:govoplan-notifications.acknowledge": "Bestätigen", + "i18n:govoplan-notifications.cancel_delivery": "Zustellung abbrechen", + "i18n:govoplan-notifications.dispatch_pending": "Ausstehende zustellen", + "i18n:govoplan-notifications.cancel_delivery_title": "Zustellung der Benachrichtigung abbrechen", + "i18n:govoplan-notifications.cancel_delivery_message": "Zustellung von {value0} abbrechen. Dies stoppt geeignete lokale Zustellarbeit, kann aber von einem Anbieter angenommene Nachrichten nicht zurückrufen.", + "i18n:govoplan-notifications.dispatch_pending_title": "Ausstehende Benachrichtigungen zustellen", + "i18n:govoplan-notifications.dispatch_pending_message": "Versuchen, bis zu 50 geeignete Benachrichtigungen in diesem Mandanten zuzustellen. Von einem Anbieter angenommene Ergebnisse werden nicht automatisch wiederholt.", + "i18n:govoplan-notifications.selected_notification": "die ausgewählte Benachrichtigung", + "i18n:govoplan-notifications.delivery_attempts": "Zustellversuche", + "i18n:govoplan-notifications.select_notification_help": "Wählen Sie eine Benachrichtigung aus, um Zustellstatus, Quellkontext und Inhalt zu prüfen.", + "i18n:govoplan-notifications.no_message_body": "Es wurde kein Nachrichtentext angegeben.", + "i18n:govoplan-notifications.open_related_item": "Zugehöriges Element öffnen", + "i18n:govoplan-notifications.source_and_delivery": "Quelle und Zustellung", + "i18n:govoplan-notifications.source": "Quelle", + "i18n:govoplan-notifications.resource": "Ressource", + "i18n:govoplan-notifications.recipient": "Empfänger", + "i18n:govoplan-notifications.priority": "Priorität", + "i18n:govoplan-notifications.queued": "Eingereiht", + "i18n:govoplan-notifications.sent": "Gesendet", + "i18n:govoplan-notifications.read": "Gelesen", + "i18n:govoplan-notifications.attempts": "Versuche", + "i18n:govoplan-notifications.none": "Keine Angabe", + "i18n:govoplan-notifications.no_delivery_attempt": "Es wurde noch kein Zustellversuch aufgezeichnet.", + "i18n:govoplan-notifications.not_set": "Nicht gesetzt", + "i18n:govoplan-notifications.request_failed": "Anfrage fehlgeschlagen", + "i18n:govoplan-notifications.preferences_saved": "Benachrichtigungseinstellungen gespeichert.", + "i18n:govoplan-notifications.preferences_load_failed": "Benachrichtigungseinstellungen konnten nicht geladen werden.", + "i18n:govoplan-notifications.preferences_save_failed": "Benachrichtigungseinstellungen konnten nicht gespeichert werden.", + "i18n:govoplan-notifications.save_preferences": "Einstellungen speichern", + "i18n:govoplan-notifications.saving": "Speichern", + "i18n:govoplan-notifications.version_value": "Version {value0}", + "i18n:govoplan-notifications.hidden_by_view": "Durch die aktive Ansicht ausgeblendet", + "i18n:govoplan-notifications.unread_badge": "Ungelesen-Markierung", + "i18n:govoplan-notifications.unread_badge_help": "Anzahl ungelesener Benachrichtigungen in der Titelleiste anzeigen.", + "i18n:govoplan-notifications.muted_sources": "Stummgeschaltete Quellmodule", + "i18n:govoplan-notifications.muted_sources_help": "Wählen Sie aktive Module. Gespeicherte, durch die aktuelle Ansicht ausgeblendete Module bleiben hier sichtbar und werden nicht verworfen.", + "i18n:govoplan-notifications.add_source_module": "Quellmodul hinzufügen", + "i18n:govoplan-notifications.no_visible_modules": "Keine sichtbaren Module passen.", + "i18n:govoplan-notifications.delivery": "Zustellung", + "i18n:govoplan-notifications.email_notifications": "E-Mail-Benachrichtigungen", + "i18n:govoplan-notifications.email_notifications_help": "Produktive E-Mails über das optionale Mail-Modul zustellen. Benachrichtigungen in GovOPlaN funktionieren weiter, wenn Mail nicht verfügbar ist.", + "i18n:govoplan-notifications.email_digest": "E-Mail-Zusammenfassung", + "i18n:govoplan-notifications.email_digest_help": "Geeignete Benachrichtigungen zu Zusammenfassungen bündeln, wenn die Zustellregeln dies unterstützen.", + "i18n:govoplan-notifications.loading_summary": "Benachrichtigungsübersicht wird geladen", + "i18n:govoplan-notifications.unread": "Ungelesen", + "i18n:govoplan-notifications.pending": "Ausstehend", + "i18n:govoplan-notifications.failed": "Fehlgeschlagen", + "i18n:govoplan-notifications.value_total": "{value0} insgesamt", + "i18n:govoplan-notifications.awaiting_delivery": "Wartet auf Zustellung", + "i18n:govoplan-notifications.delivery_failures": "Zustellfehler", + "i18n:govoplan-notifications.open_center": "Benachrichtigungszentrale öffnen", + "i18n:govoplan-notifications.status.all": "Alle", + "i18n:govoplan-notifications.status.pending": "Ausstehend", + "i18n:govoplan-notifications.status.queued": "Eingereiht", + "i18n:govoplan-notifications.status.sending": "Wird gesendet", + "i18n:govoplan-notifications.status.accepted": "Angenommen", + "i18n:govoplan-notifications.status.paused": "Pausiert", + "i18n:govoplan-notifications.status.sent": "Gesendet", + "i18n:govoplan-notifications.status.failed": "Fehlgeschlagen", + "i18n:govoplan-notifications.status.skipped": "Übersprungen", + "i18n:govoplan-notifications.status.cancelled": "Abgebrochen", + "i18n:govoplan-notifications.status.read": "Gelesen", + "i18n:govoplan-notifications.status.acknowledged": "Bestätigt" + } }; diff --git a/webui/src/module.ts b/webui/src/module.ts index a5b4fdb..eafc5be 100644 --- a/webui/src/module.ts +++ b/webui/src/module.ts @@ -18,10 +18,10 @@ const notificationDashboardWidgets: DashboardWidgetsUiCapability = { { id: "notifications.summary", surfaceId: "notifications.widget.summary", - title: "Notifications", - description: "Unread notifications and delivery state.", + title: "i18n:govoplan-notifications.surface.center", + description: "i18n:govoplan-notifications.widget_description", moduleId: "notifications", - category: "Communication", + category: "i18n:govoplan-notifications.communication", order: 60, defaultSize: "medium", supportedSizes: ["medium", "wide"], @@ -33,8 +33,8 @@ const notificationDashboardWidgets: DashboardWidgetsUiCapability = { configurationFields: [ { id: "showDeliveryState", - label: "Show delivery state", - description: "Include pending and failed delivery counts.", + label: "i18n:govoplan-notifications.show_delivery_state", + description: "i18n:govoplan-notifications.show_delivery_state_help", kind: "boolean" } ], @@ -59,7 +59,7 @@ const notificationSettingsSections: SettingsSectionsUiCapability = { { id: "notifications", surfaceId: "notifications.settings.preferences", - label: "Notifications", + label: "i18n:govoplan-notifications.surface.center", group: "ui", order: 40, anyOf: notificationRead, @@ -70,29 +70,36 @@ const notificationSettingsSections: SettingsSectionsUiCapability = { export const notificationsModule: PlatformWebModule = { id: "notifications", - label: "Notifications", + label: "i18n:govoplan-notifications.surface.center", version: "1.0.0", dependencies: [], optionalDependencies: ["mail", "tasks", "portal", "workflow", "calendar", "scheduling"], translations: generatedTranslations, viewSurfaces: [ + { id: "notifications.page.inbox", moduleId: "notifications", kind: "section", label: "i18n:govoplan-notifications.surface.inbox", parentId: "notifications.route.notifications", order: 20 }, + { id: "notifications.page.detail", moduleId: "notifications", kind: "section", label: "i18n:govoplan-notifications.surface.detail", parentId: "notifications.route.notifications", order: 30 }, + { id: "notifications.page.delivery", moduleId: "notifications", kind: "section", label: "i18n:govoplan-notifications.surface.delivery", parentId: "notifications.page.detail", order: 40 }, + { id: "notifications.action.mark-read", moduleId: "notifications", kind: "action", label: "i18n:govoplan-notifications.surface.mark_read", parentId: "notifications.page.detail", order: 50 }, + { id: "notifications.action.acknowledge", moduleId: "notifications", kind: "action", label: "i18n:govoplan-notifications.surface.acknowledge", parentId: "notifications.page.detail", order: 60 }, + { id: "notifications.action.cancel", moduleId: "notifications", kind: "action", label: "i18n:govoplan-notifications.surface.cancel", parentId: "notifications.page.delivery", order: 70 }, + { id: "notifications.action.dispatch", moduleId: "notifications", kind: "action", label: "i18n:govoplan-notifications.surface.dispatch", parentId: "notifications.page.delivery", order: 80 }, { id: "notifications.widget.summary", moduleId: "notifications", kind: "section", - label: "Notification summary widget", - order: 30 + label: "i18n:govoplan-notifications.surface.widget_summary", + order: 90 }, { id: "notifications.settings.preferences", moduleId: "notifications", kind: "section", - label: "Notification preferences", - order: 40 + label: "i18n:govoplan-notifications.surface.preferences", + order: 100 } ], routes: [ - { path: "/notifications", anyOf: notificationRead, order: 59, render: ({ settings, auth }) => createElement(NotificationCenterPage, { settings, auth }) } + { path: "/notifications", anyOf: notificationRead, order: 59, surfaceId: "notifications.route.notifications", render: ({ settings, auth }) => createElement(NotificationCenterPage, { settings, auth }) } ], uiCapabilities: { "settings.sections": notificationSettingsSections, diff --git a/webui/src/styles/notifications.css b/webui/src/styles/notifications.css index 62ccc6a..2a15494 100644 --- a/webui/src/styles/notifications.css +++ b/webui/src/styles/notifications.css @@ -15,6 +15,8 @@ .notifications-widget-actions { display: flex; + align-items: center; + gap: 8px; justify-content: flex-end; margin-top: 14px; } @@ -74,6 +76,11 @@ .notifications-status-filter { width: calc(100% - 16px); margin: 8px; + overflow-x: auto; +} + +.notifications-status-filter .segmented-control-option { + flex: 0 0 auto; } .notifications-list { @@ -158,6 +165,10 @@ margin: 8px 12px 0; } +.notifications-workspace > .action-blocker-hint { + margin: 8px 12px 0; +} + .notifications-detail { min-height: 0; overflow: auto; @@ -208,6 +219,16 @@ align-items: start; } +.notifications-settings-documentation, +.notifications-settings-panel > .action-blocker-hint { + grid-column: 1 / -1; +} + +.notifications-settings-documentation { + display: flex; + justify-content: flex-end; +} + .notifications-settings-inline-title { display: flex; align-items: center; @@ -223,6 +244,17 @@ font-size: 15px; } +.notifications-section-heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.notifications-section-heading h2 { + margin: 0; +} + .notifications-properties dl { display: grid; grid-template-columns: repeat(2, minmax(180px, 1fr)); @@ -285,6 +317,12 @@ text-align: center; } +.notifications-permission-state { + max-width: 860px; + margin: 0 auto; + padding: 32px 20px; +} + .notifications-empty-state h1 { margin: 0; color: var(--text-strong);