Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
492ab7711a |
@@ -250,7 +250,10 @@ manifest = ModuleManifest(
|
|||||||
id="notifications.center-and-preferences",
|
id="notifications.center-and-preferences",
|
||||||
title="Use the notification center",
|
title="Use the notification center",
|
||||||
summary="The title-bar badge and notification center collect durable notices that require attention outside an immediate request.",
|
summary="The title-bar badge and notification center collect durable notices that require attention outside an immediate request.",
|
||||||
body="Open the notification center to read, acknowledge, or follow notifications from enabled modules. The status dropdown uses the same checkbox filter as tables: select multiple states to include any of them, select all to remove the restriction, or deselect all to show no notifications. Status filtering happens before the latest 200 matching notifications are loaded; this list is not a complete archive. Changing the filter does not change delivery or read state. Reload refreshes the current selection without a cached response. Personal source-muting preferences remove matching entries from the personal list and badge counts even when a producer addressed the actor through an account, membership, or identity identifier; tenant-administrator evidence views remain complete. Preferences also control eligible delivery channels and categories. Disabling an optional external channel does not remove an unmuted in-product notification unless the originating module's retention policy does so.",
|
body="Documentation books sit immediately beside the visible heading or contextual label for "
|
||||||
|
"Notifications, notification preferences and delivery details, not among operational action "
|
||||||
|
"buttons. Field help remains beside its label. "
|
||||||
|
"Open the notification center to read, acknowledge, or follow notifications from enabled modules. The status dropdown uses the same checkbox filter as tables: select multiple states to include any of them, select all to remove the restriction, or deselect all to show no notifications. Status filtering happens before the latest 200 matching notifications are loaded; this list is not a complete archive. Changing the filter does not change delivery or read state. Reload refreshes the current selection without a cached response. Personal source-muting preferences remove matching entries from the personal list and badge counts even when a producer addressed the actor through an account, membership, or identity identifier; tenant-administrator evidence views remain complete. Preferences also control eligible delivery channels and categories. Disabling an optional external channel does not remove an unmuted in-product notification unless the originating module's retention policy does so.",
|
||||||
documentation_types=("user",),
|
documentation_types=("user",),
|
||||||
audience=("user",),
|
audience=("user",),
|
||||||
conditions=(DocumentationCondition(required_scopes=(READ_SCOPE,)),),
|
conditions=(DocumentationCondition(required_scopes=(READ_SCOPE,)),),
|
||||||
@@ -280,6 +283,10 @@ manifest = ModuleManifest(
|
|||||||
"die außerhalb einer unmittelbaren Anfrage Aufmerksamkeit erfordern."
|
"die außerhalb einer unmittelbaren Anfrage Aufmerksamkeit erfordern."
|
||||||
),
|
),
|
||||||
"body": (
|
"body": (
|
||||||
|
"Dokumentationsbücher stehen unmittelbar neben der sichtbaren Überschrift oder "
|
||||||
|
"Kontextbezeichnung für Benachrichtigungen, Benachrichtigungseinstellungen und "
|
||||||
|
"Zustelldetails, nicht zwischen ausführbaren Aktionsschaltflächen. Feldhilfe bleibt neben der "
|
||||||
|
"Feldbezeichnung. "
|
||||||
"Die Benachrichtigungszentrale zeigt Hinweise aktivierter Module zum Lesen, Bestätigen oder "
|
"Die Benachrichtigungszentrale zeigt Hinweise aktivierter Module zum Lesen, Bestätigen oder "
|
||||||
"Weiterverfolgen. Der Statusfilter verwendet dieselben Kontrollkästchen wie Tabellen: mehrere "
|
"Weiterverfolgen. Der Statusfilter verwendet dieselben Kontrollkästchen wie Tabellen: mehrere "
|
||||||
"Zustände einschließen, mit Alle auswählen die Einschränkung aufheben oder mit Alle abwählen "
|
"Zustände einschließen, mit Alle auswählen die Einschränkung aufheben oder mit Alle abwählen "
|
||||||
@@ -315,7 +322,10 @@ manifest = ModuleManifest(
|
|||||||
id="notifications.delivery-operations",
|
id="notifications.delivery-operations",
|
||||||
title="Operate notification delivery",
|
title="Operate notification delivery",
|
||||||
summary="Notifications persists message intent and bounded per-channel attempts before workers dispatch optional delivery channels.",
|
summary="Notifications persists message intent and bounded per-channel attempts before workers dispatch optional delivery channels.",
|
||||||
body="Producing modules emit notifications through the dispatch capability and do not own delivery credentials. In-product delivery is the baseline. Production email delivery is available only through an enabled Mail capability; file delivery remains development-only. Operators can inspect pending and failed attempts and retry only outcomes that are safe to repeat. Tenant module entitlement is checked before enqueue and again before worker delivery; disabling Notifications preserves accepted messages and exposes an operator action instead of silently consuming them. Notification lists batch-load delivery attempts for the already tenant- and recipient-filtered page, avoiding one additional database query per message. Attempt order and full evidence are preserved. Attempts whose tenant or notification reference does not match the parent are never projected, including already-loaded relationships; inconsistent stored evidence requires an authorized operator investigation rather than broader visibility. This is read optimization, not dispatch, and it does not truncate an individual notification's history.",
|
body="Documentation books sit immediately beside the visible heading or contextual label for "
|
||||||
|
"Notifications, notification preferences and delivery details, not among operational action "
|
||||||
|
"buttons. Field help remains beside its label. "
|
||||||
|
"Producing modules emit notifications through the dispatch capability and do not own delivery credentials. In-product delivery is the baseline. Production email delivery is available only through an enabled Mail capability; file delivery remains development-only. Operators can inspect pending and failed attempts and retry only outcomes that are safe to repeat. Tenant module entitlement is checked before enqueue and again before worker delivery; disabling Notifications preserves accepted messages and exposes an operator action instead of silently consuming them. Notification lists batch-load delivery attempts for the already tenant- and recipient-filtered page, avoiding one additional database query per message. Attempt order and full evidence are preserved. Attempts whose tenant or notification reference does not match the parent are never projected, including already-loaded relationships; inconsistent stored evidence requires an authorized operator investigation rather than broader visibility. This is read optimization, not dispatch, and it does not truncate an individual notification's history.",
|
||||||
documentation_types=("admin",),
|
documentation_types=("admin",),
|
||||||
audience=("tenant_admin", "operator", "module_admin"),
|
audience=("tenant_admin", "operator", "module_admin"),
|
||||||
related_modules=("mail", "audit", "ops"),
|
related_modules=("mail", "audit", "ops"),
|
||||||
@@ -338,6 +348,10 @@ manifest = ModuleManifest(
|
|||||||
"Notifications speichert Nachrichtenabsicht und begrenzte kanalbezogene Versuche, bevor Worker optionale Zustellkanäle ausführen."
|
"Notifications speichert Nachrichtenabsicht und begrenzte kanalbezogene Versuche, bevor Worker optionale Zustellkanäle ausführen."
|
||||||
),
|
),
|
||||||
"body": (
|
"body": (
|
||||||
|
"Dokumentationsbücher stehen unmittelbar neben der sichtbaren Überschrift oder "
|
||||||
|
"Kontextbezeichnung für Benachrichtigungen, Benachrichtigungseinstellungen und "
|
||||||
|
"Zustelldetails, nicht zwischen ausführbaren Aktionsschaltflächen. Feldhilfe bleibt neben der "
|
||||||
|
"Feldbezeichnung. "
|
||||||
"Erzeugende Module übergeben Benachrichtigungen über die Dispatch-Fähigkeit und verwalten keine "
|
"Erzeugende Module übergeben Benachrichtigungen über die Dispatch-Fähigkeit und verwalten keine "
|
||||||
"Zugangsdaten für die Zustellung. Die interne Zustellung ist die Grundlage. Produktive "
|
"Zugangsdaten für die Zustellung. Die interne Zustellung ist die Grundlage. Produktive "
|
||||||
"E-Mail-Zustellung steht nur über eine aktivierte Mail-Fähigkeit bereit; Dateizustellung bleibt auf "
|
"E-Mail-Zustellung steht nur über eine aktivierte Mail-Fähigkeit bereit; Dateizustellung bleibt auf "
|
||||||
|
|||||||
@@ -101,7 +101,9 @@ class NotificationsInterfaceDocumentationContractTests(unittest.TestCase):
|
|||||||
"useUnsavedDraftGuard",
|
"useUnsavedDraftGuard",
|
||||||
):
|
):
|
||||||
self.assertIn(component, settings)
|
self.assertIn(component, settings)
|
||||||
self.assertIn("DocumentationHelpLink", widget)
|
contribution = (REPO_ROOT / "webui/src/module.ts").read_text(encoding="utf-8")
|
||||||
|
self.assertIn("documentation: NOTIFICATIONS_DOCUMENTATION", contribution)
|
||||||
|
self.assertNotIn("DocumentationHelpLink", widget)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const page = readFileSync(pagePath, "utf8");
|
|||||||
const styles = readFileSync(stylesPath, "utf8");
|
const styles = readFileSync(stylesPath, "utf8");
|
||||||
|
|
||||||
assert.match(page, /SelectionList,[\s\S]*SelectionListItem,[\s\S]*from "@govoplan\/core-webui"/);
|
assert.match(page, /SelectionList,[\s\S]*SelectionListItem,[\s\S]*from "@govoplan\/core-webui"/);
|
||||||
assert.match(page, /<SelectionList label="i18n:govoplan-notifications\.notifications" className="notifications-selection-list">/);
|
assert.match(page, /<SelectionList\b(?=[^>]*\bvariant="navigation")(?=[^>]*\blabel="i18n:govoplan-notifications\.notifications")(?=[^>]*\bclassName="notifications-selection-list")[^>]*>/);
|
||||||
assert.match(page, /<SelectionListItem[\s\S]*selected=\{selected\?\.id === notification\.id\}/);
|
assert.match(page, /<SelectionListItem[\s\S]*selected=\{selected\?\.id === notification\.id\}/);
|
||||||
assert.match(page, /className=\{`notifications-list-item \$\{notification\.read_at \? "is-read" : ""\}`\}/);
|
assert.match(page, /className=\{`notifications-list-item \$\{notification\.read_at \? "is-read" : ""\}`\}/);
|
||||||
assert.doesNotMatch(page, /<button[\s\S]{0,160}notifications-list-item/);
|
assert.doesNotMatch(page, /<button[\s\S]{0,160}notifications-list-item/);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
CountBadge,
|
CountBadge,
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
DocumentationHelpLink,
|
DocumentationHelpLink,
|
||||||
|
TextWithHelp,
|
||||||
MultiSelectFilter,
|
MultiSelectFilter,
|
||||||
SelectionList,
|
SelectionList,
|
||||||
SelectionListItem,
|
SelectionListItem,
|
||||||
@@ -288,11 +289,11 @@ export default function NotificationCenterPage({ settings, auth }: { settings: A
|
|||||||
scope="detail-pane"
|
scope="detail-pane"
|
||||||
variant="detail"
|
variant="detail"
|
||||||
className="notifications-topbar"
|
className="notifications-topbar"
|
||||||
contextActions={<div className="notifications-title-line">
|
title={<span className="notifications-title-line">
|
||||||
<Bell size={18} />
|
<Bell size={18} aria-hidden="true" />
|
||||||
<strong>{selected?.subject || selected?.event_kind || "i18n:govoplan-notifications.surface.center"}</strong>
|
<strong>{selected?.subject || selected?.event_kind || "i18n:govoplan-notifications.surface.center"}</strong>
|
||||||
</div>}
|
</span>}
|
||||||
helpAction={<DocumentationHelpLink reference={NOTIFICATIONS_DOCUMENTATION} />}
|
titleHelp={<DocumentationHelpLink reference={NOTIFICATIONS_DOCUMENTATION} />}
|
||||||
primaryActions={<div className="notifications-actions">
|
primaryActions={<div className="notifications-actions">
|
||||||
<Button onClick={() => void markSelected("read")} disabled={Boolean(markReadDisabledReason)} disabledReason={markReadDisabledReason}>
|
<Button onClick={() => void markSelected("read")} disabled={Boolean(markReadDisabledReason)} disabledReason={markReadDisabledReason}>
|
||||||
<Check size={16} /> i18n:govoplan-notifications.mark_read
|
<Check size={16} /> i18n:govoplan-notifications.mark_read
|
||||||
@@ -389,8 +390,9 @@ function NotificationDetails({ notification }: { notification: NotificationMessa
|
|||||||
|
|
||||||
<section className="notifications-attempts">
|
<section className="notifications-attempts">
|
||||||
<ActionToolbar surface="section-header" className="notifications-section-heading">
|
<ActionToolbar surface="section-header" className="notifications-section-heading">
|
||||||
|
<TextWithHelp as="div" help={<DocumentationHelpLink reference={NOTIFICATIONS_DELIVERY_DOCUMENTATION} />}>
|
||||||
<h2>i18n:govoplan-notifications.delivery_attempts</h2>
|
<h2>i18n:govoplan-notifications.delivery_attempts</h2>
|
||||||
<DocumentationHelpLink reference={NOTIFICATIONS_DELIVERY_DOCUMENTATION} />
|
</TextWithHelp>
|
||||||
</ActionToolbar>
|
</ActionToolbar>
|
||||||
{notification.attempts.length === 0 ? <p className="muted">i18n:govoplan-notifications.no_delivery_attempt</p> : null}
|
{notification.attempts.length === 0 ? <p className="muted">i18n:govoplan-notifications.no_delivery_attempt</p> : null}
|
||||||
{notification.attempts.map((attempt) => (
|
{notification.attempts.map((attempt) => (
|
||||||
|
|||||||
@@ -183,9 +183,6 @@ export default function NotificationSettingsPanel({ settings, auth }: { settings
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ContentGrid columns={2} collapseAt="workspace" className="notifications-settings-panel">
|
<ContentGrid columns={2} collapseAt="workspace" className="notifications-settings-panel">
|
||||||
<div className="notifications-settings-documentation">
|
|
||||||
<DocumentationHelpLink reference={NOTIFICATIONS_DOCUMENTATION} />
|
|
||||||
</div>
|
|
||||||
{!canWrite ? (
|
{!canWrite ? (
|
||||||
<ActionBlockerHint
|
<ActionBlockerHint
|
||||||
tone="info"
|
tone="info"
|
||||||
@@ -200,7 +197,7 @@ export default function NotificationSettingsPanel({ settings, auth }: { settings
|
|||||||
documentation={NOTIFICATIONS_DOCUMENTATION}
|
documentation={NOTIFICATIONS_DOCUMENTATION}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<Card title="i18n:govoplan-notifications.notifications">
|
<Card title="i18n:govoplan-notifications.notifications" titleHelp={<DocumentationHelpLink reference={NOTIFICATIONS_DOCUMENTATION} />}>
|
||||||
<FormGrid columns={1} collapseAt="standard" className="">
|
<FormGrid columns={1} collapseAt="standard" className="">
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
label="i18n:govoplan-notifications.unread_badge"
|
label="i18n:govoplan-notifications.unread_badge"
|
||||||
@@ -230,9 +227,9 @@ export default function NotificationSettingsPanel({ settings, auth }: { settings
|
|||||||
{message ? <DismissibleAlert tone={messageTone} resetKey={message} floating>{message}</DismissibleAlert> : null}
|
{message ? <DismissibleAlert tone={messageTone} resetKey={message} floating>{message}</DismissibleAlert> : null}
|
||||||
</FormGrid>
|
</FormGrid>
|
||||||
</Card>
|
</Card>
|
||||||
<Card title="i18n:govoplan-notifications.delivery">
|
<Card title="i18n:govoplan-notifications.delivery" titleHelp={<DocumentationHelpLink reference={NOTIFICATIONS_DELIVERY_DOCUMENTATION} />}>
|
||||||
<FormGrid columns={1} collapseAt="standard" className="">
|
<FormGrid columns={1} collapseAt="standard" className="">
|
||||||
<DocumentationHelpLink reference={NOTIFICATIONS_DELIVERY_DOCUMENTATION} />
|
|
||||||
<div className="notifications-settings-inline-title">
|
<div className="notifications-settings-inline-title">
|
||||||
<Mail size={16} />
|
<Mail size={16} />
|
||||||
<strong>i18n:govoplan-notifications.email_notifications</strong>
|
<strong>i18n:govoplan-notifications.email_notifications</strong>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { MetricGrid } from "@govoplan/core-webui";
|
|||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import {
|
import {
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
DocumentationHelpLink,
|
|
||||||
LoadingFrame,
|
LoadingFrame,
|
||||||
MetricCard,
|
MetricCard,
|
||||||
adminErrorMessage,
|
adminErrorMessage,
|
||||||
@@ -11,7 +10,6 @@ import {
|
|||||||
type DashboardWidgetConfiguration,
|
type DashboardWidgetConfiguration,
|
||||||
useSharedNotificationSummary
|
useSharedNotificationSummary
|
||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
import { NOTIFICATIONS_DOCUMENTATION } from "./interfacePatterns";
|
|
||||||
|
|
||||||
export default function NotificationSummaryWidget({
|
export default function NotificationSummaryWidget({
|
||||||
settings,
|
settings,
|
||||||
@@ -66,7 +64,6 @@ export default function NotificationSummaryWidget({
|
|||||||
)}
|
)}
|
||||||
</MetricGrid>
|
</MetricGrid>
|
||||||
<div className="notifications-widget-actions">
|
<div className="notifications-widget-actions">
|
||||||
<DocumentationHelpLink reference={NOTIFICATIONS_DOCUMENTATION} />
|
|
||||||
{showCenterLink && (
|
{showCenterLink && (
|
||||||
<Link className="btn btn-secondary" to="/notifications">
|
<Link className="btn btn-secondary" to="/notifications">
|
||||||
i18n:govoplan-notifications.open_center
|
i18n:govoplan-notifications.open_center
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
SettingsSectionsUiCapability
|
SettingsSectionsUiCapability
|
||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
import NotificationSummaryWidget from "./features/notifications/NotificationSummaryWidget";
|
import NotificationSummaryWidget from "./features/notifications/NotificationSummaryWidget";
|
||||||
|
import { NOTIFICATIONS_DOCUMENTATION } from "./features/notifications/interfacePatterns";
|
||||||
import { generatedTranslations } from "./i18n/generatedTranslations";
|
import { generatedTranslations } from "./i18n/generatedTranslations";
|
||||||
import "./styles/notifications.css";
|
import "./styles/notifications.css";
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ const notificationDashboardWidgets: DashboardWidgetsUiCapability = {
|
|||||||
id: "notifications.summary",
|
id: "notifications.summary",
|
||||||
surfaceId: "notifications.widget.summary",
|
surfaceId: "notifications.widget.summary",
|
||||||
title: "i18n:govoplan-notifications.surface.center",
|
title: "i18n:govoplan-notifications.surface.center",
|
||||||
|
documentation: NOTIFICATIONS_DOCUMENTATION,
|
||||||
description: "i18n:govoplan-notifications.widget_description",
|
description: "i18n:govoplan-notifications.widget_description",
|
||||||
moduleId: "notifications",
|
moduleId: "notifications",
|
||||||
category: "i18n:govoplan-notifications.communication",
|
category: "i18n:govoplan-notifications.communication",
|
||||||
|
|||||||
@@ -89,7 +89,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.notifications-title-line {
|
.notifications-title-line {
|
||||||
min-width: 160px;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notifications-title-line strong {
|
.notifications-title-line strong {
|
||||||
@@ -169,16 +169,10 @@
|
|||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notifications-settings-documentation,
|
|
||||||
.notifications-settings-panel > .action-blocker-hint {
|
.notifications-settings-panel > .action-blocker-hint {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notifications-settings-documentation {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notifications-settings-inline-title {
|
.notifications-settings-inline-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user