@voyantjs/notifications-react 0.106.0 → 0.108.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/dist/admin/index.d.ts +80 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +102 -0
- package/dist/admin/notification-deliveries-host.d.ts +7 -0
- package/dist/admin/notification-deliveries-host.d.ts.map +1 -0
- package/dist/admin/notification-deliveries-host.js +92 -0
- package/dist/admin/notification-delivery-detail-dialog.d.ts +8 -0
- package/dist/admin/notification-delivery-detail-dialog.d.ts.map +1 -0
- package/dist/admin/notification-delivery-detail-dialog.js +30 -0
- package/dist/admin/notification-reminder-rule-detail-host.d.ts +12 -0
- package/dist/admin/notification-reminder-rule-detail-host.d.ts.map +1 -0
- package/dist/admin/notification-reminder-rule-detail-host.js +23 -0
- package/dist/admin/notification-reminder-rule-dialog.d.ts +10 -0
- package/dist/admin/notification-reminder-rule-dialog.d.ts.map +1 -0
- package/dist/admin/notification-reminder-rule-dialog.js +122 -0
- package/dist/admin/notification-reminder-rules-host.d.ts +8 -0
- package/dist/admin/notification-reminder-rules-host.d.ts.map +1 -0
- package/dist/admin/notification-reminder-rules-host.js +57 -0
- package/dist/admin/notification-reminder-runs-host.d.ts +7 -0
- package/dist/admin/notification-reminder-runs-host.d.ts.map +1 -0
- package/dist/admin/notification-reminder-runs-host.js +28 -0
- package/dist/admin/notification-settings-host.d.ts +7 -0
- package/dist/admin/notification-settings-host.d.ts.map +1 -0
- package/dist/admin/notification-settings-host.js +11 -0
- package/dist/admin/notification-template-authoring-help.d.ts +25 -0
- package/dist/admin/notification-template-authoring-help.d.ts.map +1 -0
- package/dist/admin/notification-template-authoring-help.js +8 -0
- package/dist/admin/notification-template-detail-host.d.ts +11 -0
- package/dist/admin/notification-template-detail-host.d.ts.map +1 -0
- package/dist/admin/notification-template-detail-host.js +159 -0
- package/dist/admin/notification-template-dialog.d.ts +10 -0
- package/dist/admin/notification-template-dialog.d.ts.map +1 -0
- package/dist/admin/notification-template-dialog.js +364 -0
- package/dist/admin/notification-templates-host.d.ts +9 -0
- package/dist/admin/notification-templates-host.d.ts.map +1 -0
- package/dist/admin/notification-templates-host.js +52 -0
- package/dist/admin/notifications-admin-shared.d.ts +14 -0
- package/dist/admin/notifications-admin-shared.d.ts.map +1 -0
- package/dist/admin/notifications-admin-shared.js +17 -0
- package/dist/admin/pages/notification-reminder-rule-detail-page.d.ts +8 -0
- package/dist/admin/pages/notification-reminder-rule-detail-page.d.ts.map +1 -0
- package/dist/admin/pages/notification-reminder-rule-detail-page.js +10 -0
- package/dist/admin/pages/notification-template-detail-page.d.ts +7 -0
- package/dist/admin/pages/notification-template-detail-page.d.ts.map +1 -0
- package/dist/admin/pages/notification-template-detail-page.js +9 -0
- package/dist/admin/reminders-preview-host.d.ts +7 -0
- package/dist/admin/reminders-preview-host.d.ts.map +1 -0
- package/dist/admin/reminders-preview-host.js +13 -0
- package/dist/components/notification-settings-form.d.ts +2 -0
- package/dist/components/notification-settings-form.d.ts.map +1 -0
- package/dist/components/notification-settings-form.js +66 -0
- package/dist/components/reminders-preview-list.d.ts +6 -0
- package/dist/components/reminders-preview-list.d.ts.map +1 -0
- package/dist/components/reminders-preview-list.js +19 -0
- package/dist/components/stage-channel-editor-dialog.d.ts +11 -0
- package/dist/components/stage-channel-editor-dialog.d.ts.map +1 -0
- package/dist/components/stage-channel-editor-dialog.js +77 -0
- package/dist/components/stage-channel-list.d.ts +6 -0
- package/dist/components/stage-channel-list.d.ts.map +1 -0
- package/dist/components/stage-channel-list.js +20 -0
- package/dist/components/stage-editor-dialog.d.ts +10 -0
- package/dist/components/stage-editor-dialog.d.ts.map +1 -0
- package/dist/components/stage-editor-dialog.js +104 -0
- package/dist/components/stage-list.d.ts +5 -0
- package/dist/components/stage-list.d.ts.map +1 -0
- package/dist/components/stage-list.js +34 -0
- package/dist/components/template-picker.d.ts +19 -0
- package/dist/components/template-picker.d.ts.map +1 -0
- package/dist/components/template-picker.js +26 -0
- package/dist/components/timezone-combobox.d.ts +9 -0
- package/dist/components/timezone-combobox.d.ts.map +1 -0
- package/dist/components/timezone-combobox.js +67 -0
- package/dist/i18n/en.d.ts +3 -0
- package/dist/i18n/en.d.ts.map +1 -0
- package/dist/i18n/en.js +385 -0
- package/dist/i18n/index.d.ts +5 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +3 -0
- package/dist/i18n/messages.d.ts +386 -0
- package/dist/i18n/messages.d.ts.map +1 -0
- package/dist/i18n/messages.js +1 -0
- package/dist/i18n/provider.d.ts +26 -0
- package/dist/i18n/provider.d.ts.map +1 -0
- package/dist/i18n/provider.js +44 -0
- package/dist/i18n/ro.d.ts +3 -0
- package/dist/i18n/ro.d.ts.map +1 -0
- package/dist/i18n/ro.js +385 -0
- package/dist/ui.d.ts +9 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +8 -0
- package/package.json +71 -10
- package/src/styles.css +2 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useAdminHref, useAdminNavigate } from "@voyantjs/admin";
|
|
4
|
+
import { Badge, Button, buttonVariants, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components";
|
|
5
|
+
import { Layers, Loader2, Pencil, Plus, Search } from "lucide-react";
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { useNotificationsUiMessagesOrDefault } from "../i18n/index.js";
|
|
8
|
+
import { useNotificationReminderRules, } from "../index.js";
|
|
9
|
+
import { NotificationReminderRuleDialog } from "./notification-reminder-rule-dialog.js";
|
|
10
|
+
import { DestinationLink } from "./notifications-admin-shared.js";
|
|
11
|
+
const reminderTargetKeys = [
|
|
12
|
+
"booking_confirmed",
|
|
13
|
+
"booking_payment_schedule",
|
|
14
|
+
"payment_complete",
|
|
15
|
+
"booking_cancelled_non_payment",
|
|
16
|
+
];
|
|
17
|
+
function getReminderTargetLabel(targets, targetType) {
|
|
18
|
+
return targets[targetType] ?? targetType;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Packaged admin host for the reminder rules list page (packaged-admin RFC
|
|
22
|
+
* Phase 3). Zero-prop: list/filter state stays component-local and the
|
|
23
|
+
* per-rule "Manage stages" link resolves through the
|
|
24
|
+
* `notificationReminderRule.detail` semantic destination.
|
|
25
|
+
*/
|
|
26
|
+
export function NotificationReminderRulesHost() {
|
|
27
|
+
const messages = useNotificationsUiMessagesOrDefault();
|
|
28
|
+
const t = messages.admin.reminderRulesPage;
|
|
29
|
+
const common = messages.admin.common;
|
|
30
|
+
const resolveHref = useAdminHref();
|
|
31
|
+
const navigateTo = useAdminNavigate();
|
|
32
|
+
const [search, setSearch] = useState("");
|
|
33
|
+
const [channel, setChannel] = useState("all");
|
|
34
|
+
const [status, setStatus] = useState("all");
|
|
35
|
+
const [targetType, setTargetType] = useState("all");
|
|
36
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
37
|
+
const [editing, setEditing] = useState();
|
|
38
|
+
const { data, isPending, refetch } = useNotificationReminderRules({
|
|
39
|
+
search,
|
|
40
|
+
channel: channel === "all" ? undefined : channel,
|
|
41
|
+
status: status === "all" ? undefined : status,
|
|
42
|
+
targetType: targetType === "all" ? undefined : targetType,
|
|
43
|
+
});
|
|
44
|
+
return (_jsxs("div", { className: "flex flex-col gap-6 p-6", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: t.title }), _jsx("p", { className: "text-sm text-muted-foreground", children: t.description })] }), _jsxs(Button, { onClick: () => {
|
|
45
|
+
setEditing(undefined);
|
|
46
|
+
setDialogOpen(true);
|
|
47
|
+
}, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), t.newRule] })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("div", { className: "relative max-w-sm flex-1", children: [_jsx(Search, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { placeholder: t.searchPlaceholder, value: search, onChange: (event) => setSearch(event.target.value), className: "pl-9" })] }), _jsxs(Select, { value: targetType, onValueChange: (value) => setTargetType(value ?? "all"), children: [_jsx(SelectTrigger, { className: "w-[190px]", children: _jsx(SelectValue, { placeholder: t.targetFilterPlaceholder }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: t.allTargets }), reminderTargetKeys.map((value) => (_jsx(SelectItem, { value: value, children: t.targets[value] }, value)))] })] }), _jsxs(Select, { value: channel, onValueChange: (value) => setChannel(value ?? "all"), children: [_jsx(SelectTrigger, { className: "w-[140px]", children: _jsx(SelectValue, { placeholder: common.channelFilterPlaceholder }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: common.allChannels }), _jsx(SelectItem, { value: "email", children: common.channelEmail }), _jsx(SelectItem, { value: "sms", children: common.channelSms })] })] }), _jsxs(Select, { value: status, onValueChange: (value) => setStatus(value ?? "all"), children: [_jsx(SelectTrigger, { className: "w-[140px]", children: _jsx(SelectValue, { placeholder: common.statusFilterPlaceholder }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: common.allStatuses }), _jsx(SelectItem, { value: "draft", children: common.statusDraft }), _jsx(SelectItem, { value: "active", children: common.statusActive }), _jsx(SelectItem, { value: "archived", children: common.statusArchived })] })] })] }), isPending ? (_jsx("div", { className: "flex items-center justify-center py-12", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground" }) })) : null, !isPending && (!data?.data || data.data.length === 0) ? (_jsx("div", { className: "rounded-md border border-dashed p-8 text-center", children: _jsx("p", { className: "text-sm text-muted-foreground", children: t.empty }) })) : null, !isPending && data?.data && data.data.length > 0 ? (_jsx("div", { className: "rounded-md border", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/40 text-left text-xs uppercase tracking-wide text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "px-4 py-3", children: "Rule" }), _jsx("th", { className: "px-4 py-3", children: "Target" }), _jsx("th", { className: "px-4 py-3", children: "Channel" }), _jsx("th", { className: "px-4 py-3", children: "Status" }), _jsx("th", { className: "px-4 py-3 text-right", children: "Actions" })] }) }), _jsx("tbody", { children: data.data.map((rule) => (_jsxs("tr", { className: "border-t", children: [_jsx("td", { className: "px-4 py-3", children: _jsx("div", { className: "font-medium", children: rule.name }) }), _jsx("td", { className: "px-4 py-3", children: getReminderTargetLabel(t.targets, rule.targetType) }), _jsx("td", { className: "px-4 py-3", children: _jsx(Badge, { variant: "outline", children: rule.channel }) }), _jsx("td", { className: "px-4 py-3", children: _jsx(Badge, { variant: rule.status === "active" ? "default" : "secondary", children: rule.status }) }), _jsx("td", { className: "px-4 py-3 text-right", children: _jsxs("div", { className: "flex items-center justify-end gap-1", children: [_jsxs(DestinationLink, { href: resolveHref("notificationReminderRule.detail", {
|
|
48
|
+
ruleId: rule.id,
|
|
49
|
+
}), onNavigate: () => navigateTo("notificationReminderRule.detail", { ruleId: rule.id }), className: buttonVariants({ variant: "ghost", size: "sm" }), children: [_jsx(Layers, { className: "mr-2 h-4 w-4" }), t.manageStages] }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => {
|
|
50
|
+
setEditing(rule);
|
|
51
|
+
setDialogOpen(true);
|
|
52
|
+
}, children: _jsx(Pencil, { className: "h-4 w-4" }) })] }) })] }, rule.id))) })] }) })) : null, _jsx(NotificationReminderRuleDialog, { open: dialogOpen, onOpenChange: setDialogOpen, rule: editing, onSuccess: () => {
|
|
53
|
+
setDialogOpen(false);
|
|
54
|
+
setEditing(undefined);
|
|
55
|
+
void refetch();
|
|
56
|
+
} })] }));
|
|
57
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Packaged admin host for the reminder runs page (packaged-admin RFC
|
|
3
|
+
* Phase 3). Zero-prop, read-only: filter state stays component-local and
|
|
4
|
+
* there is no cross-route navigation.
|
|
5
|
+
*/
|
|
6
|
+
export declare function NotificationReminderRunsHost(): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
//# sourceMappingURL=notification-reminder-runs-host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification-reminder-runs-host.d.ts","sourceRoot":"","sources":["../../src/admin/notification-reminder-runs-host.tsx"],"names":[],"mappings":"AAeA;;;;GAIG;AACH,wBAAgB,4BAA4B,4CA8F3C"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Badge, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components";
|
|
4
|
+
import { Loader2 } from "lucide-react";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { useNotificationsUiMessagesOrDefault } from "../i18n/index.js";
|
|
7
|
+
import { useNotificationReminderRuns } from "../index.js";
|
|
8
|
+
/**
|
|
9
|
+
* Packaged admin host for the reminder runs page (packaged-admin RFC
|
|
10
|
+
* Phase 3). Zero-prop, read-only: filter state stays component-local and
|
|
11
|
+
* there is no cross-route navigation.
|
|
12
|
+
*/
|
|
13
|
+
export function NotificationReminderRunsHost() {
|
|
14
|
+
const messages = useNotificationsUiMessagesOrDefault();
|
|
15
|
+
const t = messages.admin.reminderRunsPage;
|
|
16
|
+
const common = messages.admin.common;
|
|
17
|
+
const [status, setStatus] = useState("all");
|
|
18
|
+
const { data, isPending } = useNotificationReminderRuns({
|
|
19
|
+
status: status === "all" ? undefined : status,
|
|
20
|
+
limit: 50,
|
|
21
|
+
offset: 0,
|
|
22
|
+
});
|
|
23
|
+
return (_jsxs("div", { className: "flex flex-col gap-6 p-6", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: t.title }), _jsx("p", { className: "text-sm text-muted-foreground", children: t.description })] }), _jsx("div", { className: "flex items-center gap-3", children: _jsxs(Select, { value: status, onValueChange: (value) => setStatus(value ?? "all"), children: [_jsx(SelectTrigger, { className: "w-[160px]", children: _jsx(SelectValue, { placeholder: common.statusFilterPlaceholder }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: common.allStatuses }), _jsx(SelectItem, { value: "queued", children: common.statusQueued }), _jsx(SelectItem, { value: "processing", children: common.statusProcessing }), _jsx(SelectItem, { value: "sent", children: common.statusSent }), _jsx(SelectItem, { value: "skipped", children: common.statusSkipped }), _jsx(SelectItem, { value: "failed", children: common.statusFailed })] })] }) }), isPending ? (_jsx("div", { className: "flex items-center justify-center py-12", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground" }) })) : null, !isPending && (!data?.data || data.data.length === 0) ? (_jsx("div", { className: "rounded-md border border-dashed p-8 text-center", children: _jsx("p", { className: "text-sm text-muted-foreground", children: t.empty }) })) : null, !isPending && data?.data && data.data.length > 0 ? (_jsx("div", { className: "rounded-md border", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/40 text-left text-xs uppercase tracking-wide text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "px-4 py-3", children: "Rule" }), _jsx("th", { className: "px-4 py-3", children: "Target" }), _jsx("th", { className: "px-4 py-3", children: "Recipient" }), _jsx("th", { className: "px-4 py-3", children: "Status" }), _jsx("th", { className: "px-4 py-3", children: "Processed" })] }) }), _jsx("tbody", { children: data.data.map((run) => (_jsxs("tr", { className: "border-t", children: [_jsxs("td", { className: "px-4 py-3", children: [_jsx("div", { className: "font-medium", children: run.reminderRule.name }), _jsx("div", { className: "font-mono text-xs text-muted-foreground", children: run.reminderRule.slug })] }), _jsxs("td", { className: "px-4 py-3", children: [_jsx("div", { children: run.targetType }), _jsx("div", { className: "font-mono text-xs text-muted-foreground", children: run.targetId })] }), _jsx("td", { className: "px-4 py-3", children: run.recipient ?? "—" }), _jsx("td", { className: "px-4 py-3", children: _jsx(Badge, { variant: run.status === "sent"
|
|
24
|
+
? "default"
|
|
25
|
+
: run.status === "failed"
|
|
26
|
+
? "destructive"
|
|
27
|
+
: "secondary", children: run.status }) }), _jsx("td", { className: "px-4 py-3", children: new Date(run.processedAt).toLocaleString() })] }, run.id))) })] }) })) : null] }));
|
|
28
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Packaged admin host for the tenant-wide notification settings page
|
|
3
|
+
* (packaged-admin RFC Phase 3). Zero-prop: the settings form owns its data
|
|
4
|
+
* wiring through `@voyantjs/notifications-react`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function NotificationSettingsHost(): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
//# sourceMappingURL=notification-settings-host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification-settings-host.d.ts","sourceRoot":"","sources":["../../src/admin/notification-settings-host.tsx"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,wBAAgB,wBAAwB,4CAYvC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { NotificationSettingsForm } from "../components/notification-settings-form.js";
|
|
4
|
+
/**
|
|
5
|
+
* Packaged admin host for the tenant-wide notification settings page
|
|
6
|
+
* (packaged-admin RFC Phase 3). Zero-prop: the settings form owns its data
|
|
7
|
+
* wiring through `@voyantjs/notifications-react`.
|
|
8
|
+
*/
|
|
9
|
+
export function NotificationSettingsHost() {
|
|
10
|
+
return (_jsxs("div", { className: "container mx-auto space-y-6 p-6", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-semibold", children: "Notification settings" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Tenant-wide quiet hours, blackout dates, recipient rate limits, and suppression window." })] }), _jsx(NotificationSettingsForm, {})] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { NotificationLiquidSnippet, NotificationTemplateVariableCategory, NotificationTemplateVariableDefinition } from "@voyantjs/notifications";
|
|
2
|
+
type NotificationTemplateAuthoringHelpProps = {
|
|
3
|
+
variableGroups: NotificationTemplateVariableCategory[];
|
|
4
|
+
snippets?: NotificationLiquidSnippet[];
|
|
5
|
+
onInsertVariable?: (variable: NotificationTemplateVariableDefinition) => void;
|
|
6
|
+
onInsertSnippet?: (snippet: NotificationLiquidSnippet) => void;
|
|
7
|
+
className?: string;
|
|
8
|
+
messages?: {
|
|
9
|
+
title?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
tabs?: {
|
|
12
|
+
variables?: string;
|
|
13
|
+
liquid?: string;
|
|
14
|
+
};
|
|
15
|
+
searchPlaceholder?: string;
|
|
16
|
+
noVariables?: string;
|
|
17
|
+
example?: string;
|
|
18
|
+
insert?: string;
|
|
19
|
+
liquidUsage?: string;
|
|
20
|
+
noLiquidSnippets?: string;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
export declare function NotificationTemplateAuthoringHelp({ variableGroups, snippets, onInsertVariable, onInsertSnippet, className, messages, }: NotificationTemplateAuthoringHelpProps): import("react/jsx-runtime").JSX.Element;
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=notification-template-authoring-help.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification-template-authoring-help.d.ts","sourceRoot":"","sources":["../../src/admin/notification-template-authoring-help.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,yBAAyB,EACzB,oCAAoC,EACpC,sCAAsC,EACvC,MAAM,yBAAyB,CAAA;AAWhC,KAAK,sCAAsC,GAAG;IAC5C,cAAc,EAAE,oCAAoC,EAAE,CAAA;IACtD,QAAQ,CAAC,EAAE,yBAAyB,EAAE,CAAA;IACtC,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,sCAAsC,KAAK,IAAI,CAAA;IAC7E,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,yBAAyB,KAAK,IAAI,CAAA;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE;QACT,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,IAAI,CAAC,EAAE;YACL,SAAS,CAAC,EAAE,MAAM,CAAA;YAClB,MAAM,CAAC,EAAE,MAAM,CAAA;SAChB,CAAA;QACD,iBAAiB,CAAC,EAAE,MAAM,CAAA;QAC1B,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAC1B,CAAA;CACF,CAAA;AAED,wBAAgB,iCAAiC,CAAC,EAChD,cAAc,EACd,QAAa,EACb,gBAAgB,EAChB,eAAe,EACf,SAAS,EACT,QAAQ,GACT,EAAE,sCAAsC,2CAiBxC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { ContractTemplateAuthoringHelp, } from "@voyantjs/ui/components/contract-template-authoring-help";
|
|
4
|
+
import { useNotificationsUiMessagesOrDefault } from "../i18n/index.js";
|
|
5
|
+
export function NotificationTemplateAuthoringHelp({ variableGroups, snippets = [], onInsertVariable, onInsertSnippet, className, messages, }) {
|
|
6
|
+
const defaults = useNotificationsUiMessagesOrDefault().admin.authoringHelp;
|
|
7
|
+
return (_jsx(ContractTemplateAuthoringHelp, { className: className, title: messages?.title ?? defaults.title, description: messages?.description ?? defaults.description, messages: messages, variableGroups: variableGroups, snippets: snippets, onInsertVariable: onInsertVariable, onInsertSnippet: onInsertSnippet }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface NotificationTemplateDetailHostProps {
|
|
2
|
+
id: string;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Packaged admin host for the notification template detail page
|
|
6
|
+
* (packaged-admin RFC Phase 3). Takes the template id as a prop — the host
|
|
7
|
+
* route file binds `Route.useParams()` onto it. Back links resolve through
|
|
8
|
+
* the `notificationTemplate.list` semantic destination.
|
|
9
|
+
*/
|
|
10
|
+
export declare function NotificationTemplateDetailHost({ id }: NotificationTemplateDetailHostProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
//# sourceMappingURL=notification-template-detail-host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification-template-detail-host.d.ts","sourceRoot":"","sources":["../../src/admin/notification-template-detail-host.tsx"],"names":[],"mappings":"AAgGA,MAAM,WAAW,mCAAmC;IAClD,EAAE,EAAE,MAAM,CAAA;CACX;AAED;;;;;GAKG;AACH,wBAAgB,8BAA8B,CAAC,EAAE,EAAE,EAAE,EAAE,mCAAmC,2CAyVzF"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useAdminHref, useAdminNavigate } from "@voyantjs/admin";
|
|
4
|
+
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, Label, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, } from "@voyantjs/ui/components";
|
|
5
|
+
import { ArrowLeft, Loader2, Pencil } from "lucide-react";
|
|
6
|
+
import { lazy, Suspense, useMemo, useState } from "react";
|
|
7
|
+
import { toast } from "sonner";
|
|
8
|
+
import { useNotificationsUiMessagesOrDefault } from "../i18n/index.js";
|
|
9
|
+
import { useNotificationDeliveries, useNotificationTemplate, useNotificationTemplateAuthoring, useNotificationTemplateTools, } from "../index.js";
|
|
10
|
+
import { NotificationDeliveryDetailDialog } from "./notification-delivery-detail-dialog.js";
|
|
11
|
+
import { DestinationLink } from "./notifications-admin-shared.js";
|
|
12
|
+
// Lazy-load: the template dialog pulls the rich-text editor (tiptap +
|
|
13
|
+
// prosemirror). Keeping it out of the detail-page chunk means those modules
|
|
14
|
+
// only download when the user opens the edit dialog.
|
|
15
|
+
const NotificationTemplateDialog = lazy(() => import("./notification-template-dialog.js").then((m) => ({
|
|
16
|
+
default: m.NotificationTemplateDialog,
|
|
17
|
+
})));
|
|
18
|
+
function parsePath(path) {
|
|
19
|
+
return path
|
|
20
|
+
.replace(/\[(\d+)\]/g, ".$1")
|
|
21
|
+
.split(".")
|
|
22
|
+
.filter(Boolean);
|
|
23
|
+
}
|
|
24
|
+
function setDeepValue(target, path, value) {
|
|
25
|
+
const segments = parsePath(path);
|
|
26
|
+
let current = target;
|
|
27
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
28
|
+
const segment = segments[index];
|
|
29
|
+
const isLast = index === segments.length - 1;
|
|
30
|
+
const nextSegment = segments[index + 1];
|
|
31
|
+
const nextIsIndex = nextSegment ? /^\d+$/.test(nextSegment) : false;
|
|
32
|
+
if (Array.isArray(current)) {
|
|
33
|
+
const arrayIndex = Number(segment);
|
|
34
|
+
if (Number.isNaN(arrayIndex))
|
|
35
|
+
return;
|
|
36
|
+
if (isLast) {
|
|
37
|
+
current[arrayIndex] = value;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (current[arrayIndex] == null) {
|
|
41
|
+
current[arrayIndex] = nextIsIndex ? [] : {};
|
|
42
|
+
}
|
|
43
|
+
current = current[arrayIndex];
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (typeof current !== "object" || current == null)
|
|
47
|
+
return;
|
|
48
|
+
const record = current;
|
|
49
|
+
if (isLast) {
|
|
50
|
+
record[segment] = value;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (record[segment] == null) {
|
|
54
|
+
record[segment] = nextIsIndex ? [] : {};
|
|
55
|
+
}
|
|
56
|
+
current = record[segment];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function buildSamplePayload(variableGroups) {
|
|
60
|
+
const sample = {};
|
|
61
|
+
for (const group of variableGroups) {
|
|
62
|
+
for (const variable of group.variables) {
|
|
63
|
+
setDeepValue(sample, variable.key, variable.example);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return sample;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Packaged admin host for the notification template detail page
|
|
70
|
+
* (packaged-admin RFC Phase 3). Takes the template id as a prop — the host
|
|
71
|
+
* route file binds `Route.useParams()` onto it. Back links resolve through
|
|
72
|
+
* the `notificationTemplate.list` semantic destination.
|
|
73
|
+
*/
|
|
74
|
+
export function NotificationTemplateDetailHost({ id }) {
|
|
75
|
+
const messages = useNotificationsUiMessagesOrDefault();
|
|
76
|
+
const t = messages.admin.templateDetail;
|
|
77
|
+
const common = messages.admin.common;
|
|
78
|
+
const [editOpen, setEditOpen] = useState(false);
|
|
79
|
+
const [previewDataInput, setPreviewDataInput] = useState("");
|
|
80
|
+
const [selectedDeliveryId, setSelectedDeliveryId] = useState(null);
|
|
81
|
+
const resolveHref = useAdminHref();
|
|
82
|
+
const navigateTo = useAdminNavigate();
|
|
83
|
+
const { data: template, isPending, error, refetch } = useNotificationTemplate(id);
|
|
84
|
+
const { variableCatalog } = useNotificationTemplateAuthoring();
|
|
85
|
+
const variableGroups = useMemo(() => variableCatalog.map((group) => ({
|
|
86
|
+
...group,
|
|
87
|
+
variables: group.variables.map((variable) => ({
|
|
88
|
+
...variable,
|
|
89
|
+
example: String(variable.example),
|
|
90
|
+
})),
|
|
91
|
+
})), [variableCatalog]);
|
|
92
|
+
const defaultPreviewData = useMemo(() => JSON.stringify(buildSamplePayload(variableGroups), null, 2), [variableGroups]);
|
|
93
|
+
const { preview } = useNotificationTemplateTools();
|
|
94
|
+
const deliveries = useNotificationDeliveries({
|
|
95
|
+
templateSlug: template?.slug,
|
|
96
|
+
limit: 20,
|
|
97
|
+
offset: 0,
|
|
98
|
+
enabled: Boolean(template?.slug),
|
|
99
|
+
});
|
|
100
|
+
const parsePreviewData = () => {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = previewDataInput.trim() ? JSON.parse(previewDataInput) : {};
|
|
103
|
+
if (typeof parsed !== "object" || parsed == null || Array.isArray(parsed)) {
|
|
104
|
+
throw new Error(common.previewDataNotObject);
|
|
105
|
+
}
|
|
106
|
+
return parsed;
|
|
107
|
+
}
|
|
108
|
+
catch (previewError) {
|
|
109
|
+
throw new Error(previewError instanceof Error ? previewError.message : common.previewInvalidJson);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const handlePreview = async () => {
|
|
113
|
+
if (!template)
|
|
114
|
+
return;
|
|
115
|
+
try {
|
|
116
|
+
const data = parsePreviewData();
|
|
117
|
+
await preview.mutateAsync({
|
|
118
|
+
channel: template.channel,
|
|
119
|
+
provider: null,
|
|
120
|
+
fromAddress: template.fromAddress,
|
|
121
|
+
subjectTemplate: template.subjectTemplate,
|
|
122
|
+
htmlTemplate: template.htmlTemplate,
|
|
123
|
+
textTemplate: template.textTemplate,
|
|
124
|
+
data,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
catch (previewError) {
|
|
128
|
+
toast.error(previewError instanceof Error ? previewError.message : common.previewFailed);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
if (isPending) {
|
|
132
|
+
return (_jsx("div", { className: "flex items-center justify-center py-16", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground" }) }));
|
|
133
|
+
}
|
|
134
|
+
if (error || !template) {
|
|
135
|
+
return (_jsxs("div", { className: "flex flex-col gap-4 p-6", children: [_jsxs(DestinationLink, { href: resolveHref("notificationTemplate.list", {}), onNavigate: () => navigateTo("notificationTemplate.list", {}), className: "inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground", children: [_jsx(ArrowLeft, { className: "h-4 w-4" }), t.backToTemplates] }), _jsx("div", { className: "rounded-md border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive", children: error instanceof Error ? error.message : t.notFound })] }));
|
|
136
|
+
}
|
|
137
|
+
const renderedPreview = preview.data;
|
|
138
|
+
return (_jsxs("div", { className: "flex flex-col gap-6 p-6", children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsxs(DestinationLink, { href: resolveHref("notificationTemplate.list", {}), onNavigate: () => navigateTo("notificationTemplate.list", {}), className: "inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground", children: [_jsx(ArrowLeft, { className: "h-4 w-4" }), t.backToTemplates] }), _jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: template.name }), _jsx("p", { className: "font-mono text-xs text-muted-foreground", children: template.slug })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx(Badge, { variant: "outline", children: template.channel }), _jsx(Badge, { variant: template.status === "active" ? "default" : "secondary", children: template.status })] })] }), _jsxs(Button, { onClick: () => setEditOpen(true), children: [_jsx(Pencil, { className: "mr-2 h-4 w-4" }), t.editTemplate] })] }), _jsxs("div", { className: "grid gap-4 md:grid-cols-2 xl:grid-cols-4", children: [_jsx(MetaCard, { label: t.metaChannel, value: template.channel }), _jsx(MetaCard, { label: t.metaFrom, value: template.fromAddress ?? common.defaultSender }), _jsx(MetaCard, { label: t.metaUpdated, value: new Date(template.updatedAt).toLocaleString() })] }), _jsxs(Tabs, { defaultValue: "overview", children: [_jsxs(TabsList, { className: "w-full", children: [_jsx(TabsTrigger, { value: "overview", children: t.tabOverview }), _jsx(TabsTrigger, { value: "preview", children: t.tabPreview }), _jsx(TabsTrigger, { value: "deliveries", children: t.recentDeliveries })] }), _jsx(TabsContent, { value: "overview", className: "mt-4 space-y-4", children: _jsxs("div", { className: "grid gap-4 lg:grid-cols-2", children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: t.messageStructureTitle }) }), _jsxs(CardContent, { className: "space-y-3 text-sm", children: [_jsx(KeyValue, { label: t.subjectLabel, value: template.subjectTemplate ?? "—" }), _jsx(KeyValue, { label: t.textFallbackLabel, value: template.textTemplate ?? "—" }), _jsx(KeyValue, { label: t.descriptionLabel, value: template.metadata ? JSON.stringify(template.metadata) : "—" })] })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: t.htmlBodyTitle }) }), _jsx(CardContent, { children: template.htmlTemplate ? (_jsx("div", { className: "prose prose-sm max-w-none rounded-md border bg-background px-4 py-4 dark:prose-invert",
|
|
139
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: Notification template HTML is rendered for preview.
|
|
140
|
+
dangerouslySetInnerHTML: { __html: template.htmlTemplate } })) : (_jsx("div", { className: "rounded-md border bg-muted/20 px-4 py-3 text-sm text-muted-foreground", children: t.noHtmlConfigured })) })] })] }) }), _jsx(TabsContent, { value: "preview", className: "mt-4", children: _jsxs("div", { className: "grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]", children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: t.sampleDataTitle }) }), _jsxs(CardContent, { className: "space-y-3", children: [_jsx(Label, { children: t.customJsonLabel }), _jsx(Textarea, { value: previewDataInput || defaultPreviewData, onChange: (event) => setPreviewDataInput(event.target.value), rows: 16, className: "font-mono text-xs" }), _jsxs(Button, { type: "button", variant: "outline", onClick: handlePreview, disabled: preview.isPending, children: [preview.isPending ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : null, t.renderPreview] })] })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: t.renderedOutputTitle }) }), _jsxs(CardContent, { className: "space-y-3", children: [_jsx(KeyValue, { label: t.subjectLabel, value: renderedPreview?.subject ?? t.notRenderedYet }), template.channel === "email" ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: t.htmlBodyTitle }), renderedPreview?.html ? (_jsx("div", { className: "prose prose-sm max-w-none rounded-md border bg-background px-4 py-4 dark:prose-invert",
|
|
141
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: Rendered preview HTML is generated server-side for preview.
|
|
142
|
+
dangerouslySetInnerHTML: { __html: renderedPreview.html } })) : (_jsx("div", { className: "rounded-md border bg-muted/20 px-4 py-3 text-sm text-muted-foreground", children: t.noRenderedHtml }))] }), _jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: t.textFallbackLabel }), _jsx("pre", { className: "whitespace-pre-wrap rounded-md border bg-muted/20 px-3 py-3 text-xs", children: renderedPreview?.text ?? t.noRenderedText })] })] })) : (_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: t.smsBodyLabel }), _jsx("pre", { className: "whitespace-pre-wrap rounded-md border bg-muted/20 px-3 py-3 text-xs", children: renderedPreview?.text ?? t.noRenderedText })] }))] })] })] }) }), _jsx(TabsContent, { value: "deliveries", className: "mt-4", children: _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: t.recentDeliveries }) }), _jsx(CardContent, { children: deliveries.isPending ? (_jsx("div", { className: "flex items-center justify-center py-12", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground" }) })) : deliveries.data?.data && deliveries.data.data.length > 0 ? (_jsx("div", { className: "rounded-md border", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/40 text-left text-xs uppercase tracking-wide text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "px-4 py-3", children: "Recipient" }), _jsx("th", { className: "px-4 py-3", children: "Provider" }), _jsx("th", { className: "px-4 py-3", children: "Status" }), _jsx("th", { className: "px-4 py-3", children: "Created" }), _jsx("th", { className: "px-4 py-3 text-right", children: "View" })] }) }), _jsx("tbody", { children: deliveries.data.data.map((delivery) => (_jsxs("tr", { className: "border-t", children: [_jsxs("td", { className: "px-4 py-3", children: [_jsx("div", { children: delivery.toAddress }), delivery.subject ? (_jsx("div", { className: "text-xs text-muted-foreground", children: delivery.subject })) : null] }), _jsx("td", { className: "px-4 py-3", children: delivery.provider }), _jsx("td", { className: "px-4 py-3", children: _jsx(Badge, { variant: delivery.status === "sent"
|
|
143
|
+
? "default"
|
|
144
|
+
: delivery.status === "failed"
|
|
145
|
+
? "destructive"
|
|
146
|
+
: "secondary", children: delivery.status }) }), _jsx("td", { className: "px-4 py-3", children: new Date(delivery.createdAt).toLocaleString() }), _jsx("td", { className: "px-4 py-3 text-right", children: _jsx(Button, { variant: "ghost", size: "sm", onClick: () => setSelectedDeliveryId(delivery.id), children: t.inspect }) })] }, delivery.id))) })] }) })) : (_jsx("div", { className: "rounded-md border border-dashed px-4 py-8 text-center text-sm text-muted-foreground", children: t.noDeliveriesForTemplate })) })] }) })] }), _jsx(Suspense, { fallback: null, children: _jsx(NotificationTemplateDialog, { open: editOpen, onOpenChange: setEditOpen, template: template, onSuccess: () => {
|
|
147
|
+
setEditOpen(false);
|
|
148
|
+
void refetch();
|
|
149
|
+
} }) }), _jsx(NotificationDeliveryDetailDialog, { deliveryId: selectedDeliveryId, open: Boolean(selectedDeliveryId), onOpenChange: (open) => {
|
|
150
|
+
if (!open)
|
|
151
|
+
setSelectedDeliveryId(null);
|
|
152
|
+
} })] }));
|
|
153
|
+
}
|
|
154
|
+
function MetaCard({ label, value }) {
|
|
155
|
+
return (_jsxs("div", { className: "rounded-md border p-4", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: label }), _jsx("div", { className: "mt-2 text-sm", children: value })] }));
|
|
156
|
+
}
|
|
157
|
+
function KeyValue({ label, value }) {
|
|
158
|
+
return (_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: label }), _jsx("div", { className: "break-words text-sm", children: value })] }));
|
|
159
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type NotificationTemplateRecord } from "../index.js";
|
|
2
|
+
type NotificationTemplateDialogProps = {
|
|
3
|
+
open: boolean;
|
|
4
|
+
onOpenChange: (open: boolean) => void;
|
|
5
|
+
template?: NotificationTemplateRecord;
|
|
6
|
+
onSuccess: () => void;
|
|
7
|
+
};
|
|
8
|
+
export declare function NotificationTemplateDialog(props: NotificationTemplateDialogProps): import("react/jsx-runtime").JSX.Element | null;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=notification-template-dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification-template-dialog.d.ts","sourceRoot":"","sources":["../../src/admin/notification-template-dialog.tsx"],"names":[],"mappings":"AAkCA,OAAO,EACL,KAAK,0BAA0B,EAIhC,MAAM,aAAa,CAAA;AAoDpB,KAAK,+BAA+B,GAAG;IACrC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,QAAQ,CAAC,EAAE,0BAA0B,CAAA;IACrC,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB,CAAA;AA2GD,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,+BAA+B,kDAMhF"}
|