@voyantjs/notifications-ui 0.28.3
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 +21 -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 +24 -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 +65 -0
- package/dist/i18n/en.d.ts +3 -0
- package/dist/i18n/en.d.ts.map +1 -0
- package/dist/i18n/en.js +138 -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 +139 -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 +138 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/package.json +93 -0
- package/src/styles.css +2 -0
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# @voyantjs/notifications-ui
|
|
2
|
+
|
|
3
|
+
React components for Voyant notifications: reminder sequence editor (stages + channels), notification settings, and preview.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @voyantjs/notifications-ui @voyantjs/notifications-react @voyantjs/ui @tanstack/react-query react react-dom
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import "@voyantjs/notifications-ui/styles.css"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Components
|
|
16
|
+
|
|
17
|
+
- `<StageList />` — reminder rule's ordered stages with reorder + delete.
|
|
18
|
+
- `<StageEditorDialog />` — create / edit a stage (anchor, window, cadence).
|
|
19
|
+
- `<StageChannelEditorDialog />` — create / edit a channel under a stage.
|
|
20
|
+
- `<NotificationSettingsForm />` — quiet hours, blackout dates, weekend skip, recipient rate limit, suppression window.
|
|
21
|
+
- `<RemindersPreviewList />` — what would fire on a given date with reasoning.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification-settings-form.d.ts","sourceRoot":"","sources":["../../src/components/notification-settings-form.tsx"],"names":[],"mappings":"AA0DA,wBAAgB,wBAAwB,4CAgNvC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useNotificationSettings, useNotificationSettingsMutation, } from "@voyantjs/notifications-react";
|
|
4
|
+
import { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Input, } from "@voyantjs/ui/components";
|
|
5
|
+
import { DatePicker } from "@voyantjs/ui/components/date-picker";
|
|
6
|
+
import { Field, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSet, FieldTitle, } from "@voyantjs/ui/components/field";
|
|
7
|
+
import { Switch } from "@voyantjs/ui/components/switch";
|
|
8
|
+
import { Loader2, Plus, Trash2 } from "lucide-react";
|
|
9
|
+
import { useEffect, useState } from "react";
|
|
10
|
+
import { useNotificationsUiMessagesOrDefault } from "../i18n/index.js";
|
|
11
|
+
import { TimezoneCombobox } from "./timezone-combobox.js";
|
|
12
|
+
let blackoutSeq = 0;
|
|
13
|
+
const nextBlackoutKey = () => `bo-${++blackoutSeq}`;
|
|
14
|
+
const emptyForm = {
|
|
15
|
+
quietHoursStart: "",
|
|
16
|
+
quietHoursEnd: "",
|
|
17
|
+
quietHoursTz: "UTC",
|
|
18
|
+
blackoutDates: [],
|
|
19
|
+
skipWeekends: false,
|
|
20
|
+
recipientRateLimitPerDay: "",
|
|
21
|
+
suppressionWindowHours: 24,
|
|
22
|
+
};
|
|
23
|
+
export function NotificationSettingsForm() {
|
|
24
|
+
const messages = useNotificationsUiMessagesOrDefault();
|
|
25
|
+
const { data: settings, isLoading } = useNotificationSettings();
|
|
26
|
+
const mutation = useNotificationSettingsMutation();
|
|
27
|
+
const [form, setForm] = useState(emptyForm);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!settings)
|
|
30
|
+
return;
|
|
31
|
+
setForm({
|
|
32
|
+
quietHoursStart: settings.quietHoursLocal?.start ?? "",
|
|
33
|
+
quietHoursEnd: settings.quietHoursLocal?.end ?? "",
|
|
34
|
+
quietHoursTz: settings.quietHoursLocal?.tz ?? "UTC",
|
|
35
|
+
blackoutDates: (settings.blackoutDates ?? []).map((date) => ({
|
|
36
|
+
rowKey: nextBlackoutKey(),
|
|
37
|
+
date,
|
|
38
|
+
})),
|
|
39
|
+
skipWeekends: settings.skipWeekends,
|
|
40
|
+
recipientRateLimitPerDay: settings.recipientRateLimitPerDay?.toString() ?? "",
|
|
41
|
+
suppressionWindowHours: settings.suppressionWindowHours,
|
|
42
|
+
});
|
|
43
|
+
}, [settings]);
|
|
44
|
+
const setField = (key, value) => setForm((prev) => ({ ...prev, [key]: value }));
|
|
45
|
+
const addBlackout = () => setField("blackoutDates", [...form.blackoutDates, { rowKey: nextBlackoutKey(), date: null }]);
|
|
46
|
+
const removeBlackout = (rowKey) => setField("blackoutDates", form.blackoutDates.filter((row) => row.rowKey !== rowKey));
|
|
47
|
+
const updateBlackout = (rowKey, date) => setField("blackoutDates", form.blackoutDates.map((row) => (row.rowKey === rowKey ? { ...row, date } : row)));
|
|
48
|
+
const handleSubmit = async () => {
|
|
49
|
+
const dates = form.blackoutDates
|
|
50
|
+
.map((row) => row.date)
|
|
51
|
+
.filter((d) => Boolean(d && /^\d{4}-\d{2}-\d{2}$/.test(d)));
|
|
52
|
+
const quiet = form.quietHoursStart && form.quietHoursEnd
|
|
53
|
+
? { start: form.quietHoursStart, end: form.quietHoursEnd, tz: form.quietHoursTz || "UTC" }
|
|
54
|
+
: null;
|
|
55
|
+
await mutation.mutateAsync({
|
|
56
|
+
quietHoursLocal: quiet,
|
|
57
|
+
blackoutDates: dates.length > 0 ? dates : null,
|
|
58
|
+
skipWeekends: form.skipWeekends,
|
|
59
|
+
recipientRateLimitPerDay: form.recipientRateLimitPerDay
|
|
60
|
+
? Number(form.recipientRateLimitPerDay)
|
|
61
|
+
: null,
|
|
62
|
+
suppressionWindowHours: form.suppressionWindowHours,
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
return (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: messages.settings.heading }), _jsx(CardDescription, { children: messages.settings.description })] }), _jsxs(CardContent, { children: [isLoading && _jsx("p", { className: "text-sm text-muted-foreground", children: messages.common.loading }), _jsxs(FieldGroup, { children: [_jsxs(FieldSet, { children: [_jsx(FieldLegend, { children: messages.settings.sections.quietHours }), _jsx(FieldDescription, { children: messages.settings.sections.quietHoursDesc }), _jsxs(FieldGroup, { className: "gap-4", children: [_jsxs(Field, { orientation: "horizontal", children: [_jsx(Switch, { id: "skipWeekends", checked: form.skipWeekends, onCheckedChange: (value) => setField("skipWeekends", value) }), _jsxs(FieldLabel, { htmlFor: "skipWeekends", className: "!w-auto !flex-row", children: [_jsx(FieldTitle, { children: messages.settings.fields.skipWeekends }), _jsx(FieldDescription, { children: messages.settings.fields.skipWeekendsDesc })] })] }), _jsxs("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-3", children: [_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "quietHoursStart", children: messages.settings.fields.quietHoursStart }), _jsx(Input, { id: "quietHoursStart", type: "time", value: form.quietHoursStart, onChange: (e) => setField("quietHoursStart", e.target.value) })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "quietHoursEnd", children: messages.settings.fields.quietHoursEnd }), _jsx(Input, { id: "quietHoursEnd", type: "time", value: form.quietHoursEnd, onChange: (e) => setField("quietHoursEnd", e.target.value) })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { children: messages.settings.fields.quietHoursTz }), _jsx(TimezoneCombobox, { value: form.quietHoursTz, onChange: (value) => setField("quietHoursTz", value ?? "UTC"), placeholder: messages.settings.placeholders.tz })] })] })] })] }), _jsxs(FieldSet, { children: [_jsx(FieldLegend, { children: messages.settings.sections.blackouts }), _jsx(FieldDescription, { children: messages.settings.sections.blackoutsDesc }), _jsxs("div", { className: "space-y-2", children: [form.blackoutDates.length === 0 && (_jsx("p", { className: "text-sm text-muted-foreground", children: messages.settings.placeholders.noBlackouts })), form.blackoutDates.map((row) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex-1", children: _jsx(DatePicker, { value: row.date, onChange: (value) => updateBlackout(row.rowKey, value) }) }), _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeBlackout(row.rowKey), "aria-label": messages.settings.actions.removeBlackoutDate, children: _jsx(Trash2, { className: "size-4" }) })] }, row.rowKey))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: addBlackout, children: [_jsx(Plus, { className: "size-4" }), " ", messages.settings.actions.addBlackoutDate] })] })] }), _jsxs(FieldSet, { children: [_jsx(FieldLegend, { children: messages.settings.sections.rateLimits }), _jsx(FieldDescription, { children: messages.settings.sections.rateLimitsDesc }), _jsxs("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-2", children: [_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "recipientRateLimitPerDay", children: messages.settings.fields.recipientRateLimitPerDay }), _jsx(Input, { id: "recipientRateLimitPerDay", type: "number", min: 1, value: form.recipientRateLimitPerDay, onChange: (e) => setField("recipientRateLimitPerDay", e.target.value), placeholder: messages.common.optionalPlaceholder }), _jsx(FieldDescription, { children: messages.settings.helpers.recipientRateLimitPerDay })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "suppressionWindowHours", children: messages.settings.fields.suppressionWindowHours }), _jsx(Input, { id: "suppressionWindowHours", type: "number", min: 0, value: form.suppressionWindowHours, onChange: (e) => setField("suppressionWindowHours", Number(e.target.value)) }), _jsx(FieldDescription, { children: messages.settings.helpers.suppressionWindowHours })] })] })] })] })] }), _jsx(CardFooter, { className: "justify-end", children: _jsxs(Button, { onClick: handleSubmit, disabled: mutation.isPending, children: [mutation.isPending ? _jsx(Loader2, { className: "size-4 animate-spin" }) : null, messages.common.save] }) })] }));
|
|
66
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface RemindersPreviewListProps {
|
|
2
|
+
ruleId?: string;
|
|
3
|
+
targetId?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function RemindersPreviewList({ ruleId, targetId }: RemindersPreviewListProps): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
//# sourceMappingURL=reminders-preview-list.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reminders-preview-list.d.ts","sourceRoot":"","sources":["../../src/components/reminders-preview-list.tsx"],"names":[],"mappings":"AAuBA,MAAM,WAAW,yBAAyB;IACxC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,wBAAgB,oBAAoB,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,yBAAyB,2CAyDnF"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useRemindersPreview } from "@voyantjs/notifications-react";
|
|
4
|
+
import { Card, CardContent } from "@voyantjs/ui/components";
|
|
5
|
+
import { DatePicker } from "@voyantjs/ui/components/date-picker";
|
|
6
|
+
import { Field, FieldLabel } from "@voyantjs/ui/components/field";
|
|
7
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@voyantjs/ui/components/table";
|
|
8
|
+
import { Loader2 } from "lucide-react";
|
|
9
|
+
import { useState } from "react";
|
|
10
|
+
import { useNotificationsUiMessagesOrDefault } from "../i18n/index.js";
|
|
11
|
+
function todayIso() {
|
|
12
|
+
return new Date().toISOString().slice(0, 10);
|
|
13
|
+
}
|
|
14
|
+
export function RemindersPreviewList({ ruleId, targetId }) {
|
|
15
|
+
const messages = useNotificationsUiMessagesOrDefault();
|
|
16
|
+
const [date, setDate] = useState(todayIso());
|
|
17
|
+
const { data, isFetching } = useRemindersPreview({ date, ruleId, targetId });
|
|
18
|
+
return (_jsx(Card, { children: _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs(Field, { className: "max-w-xs", children: [_jsx(FieldLabel, { htmlFor: "preview-date", children: messages.preview.dateLabel }), _jsx(DatePicker, { value: date, onChange: (next) => setDate(next ?? todayIso()), clearable: false })] }), isFetching ? (_jsx(Loader2, { className: "size-4 animate-spin text-muted-foreground", "aria-hidden": true })) : null] }), data && data.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: messages.preview.empty })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: messages.preview.columns.rule }), _jsx(TableHead, { children: messages.preview.columns.stage }), _jsx(TableHead, { children: messages.preview.columns.target }), _jsx(TableHead, { children: messages.preview.columns.anchor }), _jsx(TableHead, { children: messages.preview.columns.scheduledAt }), _jsx(TableHead, { children: messages.preview.columns.reasoning })] }) }), _jsx(TableBody, { children: data?.map((row) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-medium", children: row.ruleName }), _jsxs(TableCell, { children: ["#", row.stageOrderIndex, " ", row.stageName ?? ""] }), _jsx(TableCell, { className: "font-mono text-xs", children: row.targetId }), _jsx(TableCell, { children: row.anchor }), _jsx(TableCell, { className: "font-mono text-xs", children: new Date(row.scheduledAt).toLocaleString() }), _jsx(TableCell, { className: "text-xs text-muted-foreground", children: row.reasoning })] }, `${row.ruleId}:${row.targetId}:${row.stageId}`))) })] }))] }) }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ReminderStageChannelRecord } from "@voyantjs/notifications-react";
|
|
2
|
+
export interface StageChannelEditorDialogProps {
|
|
3
|
+
reminderRuleId: string;
|
|
4
|
+
stageId: string;
|
|
5
|
+
channel: ReminderStageChannelRecord | null;
|
|
6
|
+
defaultOrderIndex?: number;
|
|
7
|
+
open: boolean;
|
|
8
|
+
onOpenChange: (open: boolean) => void;
|
|
9
|
+
}
|
|
10
|
+
export declare function StageChannelEditorDialog({ reminderRuleId, stageId, channel, defaultOrderIndex, open, onOpenChange, }: StageChannelEditorDialogProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
//# sourceMappingURL=stage-channel-editor-dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stage-channel-editor-dialog.d.ts","sourceRoot":"","sources":["../../src/components/stage-channel-editor-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,0BAA0B,EAEhC,MAAM,+BAA+B,CAAA;AAwDtC,MAAM,WAAW,6BAA6B;IAC5C,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,0BAA0B,GAAG,IAAI,CAAA;IAC1C,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;CACtC;AAED,wBAAgB,wBAAwB,CAAC,EACvC,cAAc,EACd,OAAO,EACP,OAAO,EACP,iBAAqB,EACrB,IAAI,EACJ,YAAY,GACb,EAAE,6BAA6B,2CA+J/B"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useReminderStageChannelMutation, } from "@voyantjs/notifications-react";
|
|
4
|
+
import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components";
|
|
5
|
+
import { Field, FieldDescription, FieldGroup, FieldLabel } from "@voyantjs/ui/components/field";
|
|
6
|
+
import { Loader2 } from "lucide-react";
|
|
7
|
+
import { useEffect, useState } from "react";
|
|
8
|
+
import { useNotificationsUiMessagesOrDefault } from "../i18n/index.js";
|
|
9
|
+
import { TemplatePicker } from "./template-picker.js";
|
|
10
|
+
function fromRecord(channel, orderIndex) {
|
|
11
|
+
if (!channel) {
|
|
12
|
+
return {
|
|
13
|
+
orderIndex,
|
|
14
|
+
channel: "email",
|
|
15
|
+
provider: "automatic",
|
|
16
|
+
templateId: null,
|
|
17
|
+
recipientKind: "primary",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const provider = channel.provider === "resend" || channel.provider === "twilio" ? channel.provider : "automatic";
|
|
21
|
+
return {
|
|
22
|
+
orderIndex: channel.orderIndex,
|
|
23
|
+
channel: channel.channel,
|
|
24
|
+
provider,
|
|
25
|
+
templateId: channel.templateId ?? null,
|
|
26
|
+
recipientKind: channel.recipientKind,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function StageChannelEditorDialog({ reminderRuleId, stageId, channel, defaultOrderIndex = 0, open, onOpenChange, }) {
|
|
30
|
+
const messages = useNotificationsUiMessagesOrDefault();
|
|
31
|
+
const { create, update } = useReminderStageChannelMutation(reminderRuleId, stageId);
|
|
32
|
+
const [form, setForm] = useState(() => fromRecord(channel, defaultOrderIndex));
|
|
33
|
+
const isEdit = Boolean(channel);
|
|
34
|
+
const isPending = create.isPending || update.isPending;
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (open)
|
|
37
|
+
setForm(fromRecord(channel, defaultOrderIndex));
|
|
38
|
+
}, [open, channel, defaultOrderIndex]);
|
|
39
|
+
const setField = (key, value) => setForm((prev) => ({ ...prev, [key]: value }));
|
|
40
|
+
const handleSubmit = async () => {
|
|
41
|
+
const input = {
|
|
42
|
+
orderIndex: form.orderIndex,
|
|
43
|
+
channel: form.channel,
|
|
44
|
+
provider: form.provider === "automatic" ? null : form.provider,
|
|
45
|
+
templateId: form.templateId,
|
|
46
|
+
templateSlug: null,
|
|
47
|
+
recipientKind: form.recipientKind,
|
|
48
|
+
recipientRole: null,
|
|
49
|
+
};
|
|
50
|
+
if (isEdit && channel) {
|
|
51
|
+
await update.mutateAsync({ channelId: channel.id, input });
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
await create.mutateAsync(input);
|
|
55
|
+
}
|
|
56
|
+
onOpenChange(false);
|
|
57
|
+
};
|
|
58
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEdit ? messages.channel.titles.edit : messages.channel.titles.create }) }), _jsx(DialogBody, { children: _jsxs(FieldGroup, { children: [_jsxs("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-[1fr_8rem]", children: [_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "channel-channel", children: messages.channel.fields.channel }), _jsxs(Select, { value: form.channel, onValueChange: (v) => {
|
|
59
|
+
if (!v)
|
|
60
|
+
return;
|
|
61
|
+
const next = v;
|
|
62
|
+
setForm((prev) => ({
|
|
63
|
+
...prev,
|
|
64
|
+
channel: next,
|
|
65
|
+
// Picked template no longer matches the new channel — clear it.
|
|
66
|
+
templateId: null,
|
|
67
|
+
}));
|
|
68
|
+
}, children: [_jsx(SelectTrigger, { id: "channel-channel", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "email", children: messages.channel.channels.email }), _jsx(SelectItem, { value: "sms", children: messages.channel.channels.sms })] })] })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "channel-order", children: messages.channel.fields.orderIndex }), _jsx(Input, { id: "channel-order", type: "number", value: form.orderIndex, onChange: (e) => setField("orderIndex", Number(e.target.value)) })] })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { children: messages.channel.fields.template }), _jsx(TemplatePicker, { value: form.templateId, onChange: (value) => setField("templateId", value), channel: form.channel, placeholder: messages.channel.placeholders.template }), _jsx(FieldDescription, { children: "Filtered by the channel above. Resolved at send time so editing the template doesn't need a rule update." })] }), _jsxs("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-2", children: [_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "channel-recipient-kind", children: messages.channel.fields.recipientKind }), _jsxs(Select, { value: form.recipientKind, onValueChange: (v) => {
|
|
69
|
+
if (!v)
|
|
70
|
+
return;
|
|
71
|
+
setField("recipientKind", v);
|
|
72
|
+
}, children: [_jsx(SelectTrigger, { id: "channel-recipient-kind", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "primary", children: messages.channel.recipientKinds.primary }), _jsx(SelectItem, { value: "cc", children: messages.channel.recipientKinds.cc }), _jsx(SelectItem, { value: "bcc", children: messages.channel.recipientKinds.bcc })] })] })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "channel-provider", children: messages.channel.fields.provider }), _jsxs(Select, { value: form.provider, onValueChange: (v) => {
|
|
73
|
+
if (!v)
|
|
74
|
+
return;
|
|
75
|
+
setField("provider", v);
|
|
76
|
+
}, children: [_jsx(SelectTrigger, { id: "channel-provider", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "automatic", children: messages.channel.providers.automatic }), _jsx(SelectItem, { value: "resend", children: messages.channel.providers.resend }), _jsx(SelectItem, { value: "twilio", children: messages.channel.providers.twilio })] })] }), _jsx(FieldDescription, { children: "Use Automatic to fall back to the deployment default for this channel." })] })] })] }) }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: isPending, children: messages.common.cancel }), _jsxs(Button, { onClick: handleSubmit, disabled: isPending, children: [isPending ? _jsx(Loader2, { className: "size-4 animate-spin" }) : null, isEdit ? messages.common.save : messages.common.create] })] })] }) }));
|
|
77
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface StageChannelListProps {
|
|
2
|
+
reminderRuleId: string;
|
|
3
|
+
stageId: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function StageChannelList({ reminderRuleId, stageId }: StageChannelListProps): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
//# sourceMappingURL=stage-channel-list.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stage-channel-list.d.ts","sourceRoot":"","sources":["../../src/components/stage-channel-list.tsx"],"names":[],"mappings":"AAcA,MAAM,WAAW,qBAAqB;IACpC,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,wBAAgB,gBAAgB,CAAC,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,qBAAqB,2CAyElF"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useReminderStageChannelMutation, useReminderStageChannels, } from "@voyantjs/notifications-react";
|
|
4
|
+
import { Badge, Button, Card, CardContent, CardHeader, CardTitle } from "@voyantjs/ui/components";
|
|
5
|
+
import { Pencil, Plus, Trash2 } from "lucide-react";
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { useNotificationsUiMessagesOrDefault } from "../i18n/index.js";
|
|
8
|
+
import { StageChannelEditorDialog } from "./stage-channel-editor-dialog.js";
|
|
9
|
+
export function StageChannelList({ reminderRuleId, stageId }) {
|
|
10
|
+
const messages = useNotificationsUiMessagesOrDefault();
|
|
11
|
+
const { data: channels, isLoading } = useReminderStageChannels(reminderRuleId, stageId);
|
|
12
|
+
const { remove } = useReminderStageChannelMutation(reminderRuleId, stageId);
|
|
13
|
+
const [editing, setEditing] = useState(null);
|
|
14
|
+
const [creating, setCreating] = useState(false);
|
|
15
|
+
return (_jsxs(Card, { className: "border-dashed", children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between space-y-0 pb-2", children: [_jsx(CardTitle, { className: "text-sm font-medium", children: messages.channel.listHeading }), _jsxs(Button, { size: "sm", variant: "outline", onClick: () => setCreating(true), children: [_jsx(Plus, { className: "size-4" }), " ", messages.channel.addChannel] })] }), _jsxs(CardContent, { className: "space-y-2", children: [isLoading && _jsx("p", { className: "text-sm text-muted-foreground", children: messages.common.loading }), channels && channels.length === 0 && (_jsx("p", { className: "text-sm text-muted-foreground", children: messages.channel.listEmpty })), channels?.map((channel) => (_jsxs("div", { className: "flex items-center justify-between rounded-md border px-3 py-2 text-sm", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Badge, { variant: "outline", children: messages.channel.channels[channel.channel] }), _jsx(Badge, { variant: "secondary", children: messages.channel.recipientKinds[channel.recipientKind] })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { size: "icon", variant: "ghost", onClick: () => setEditing(channel), children: _jsx(Pencil, { className: "size-4" }) }), _jsx(Button, { size: "icon", variant: "ghost", onClick: () => {
|
|
16
|
+
if (window.confirm(messages.channel.deleteConfirm)) {
|
|
17
|
+
void remove.mutateAsync(channel.id);
|
|
18
|
+
}
|
|
19
|
+
}, children: _jsx(Trash2, { className: "size-4" }) })] })] }, channel.id)))] }), creating && (_jsx(StageChannelEditorDialog, { reminderRuleId: reminderRuleId, stageId: stageId, channel: null, defaultOrderIndex: channels?.length ?? 0, open: creating, onOpenChange: setCreating })), editing && (_jsx(StageChannelEditorDialog, { reminderRuleId: reminderRuleId, stageId: stageId, channel: editing, open: Boolean(editing), onOpenChange: (open) => !open && setEditing(null) }))] }));
|
|
20
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ReminderRuleStageRecord } from "@voyantjs/notifications-react";
|
|
2
|
+
export interface StageEditorDialogProps {
|
|
3
|
+
reminderRuleId: string;
|
|
4
|
+
stage: ReminderRuleStageRecord | null;
|
|
5
|
+
defaultOrderIndex?: number;
|
|
6
|
+
open: boolean;
|
|
7
|
+
onOpenChange: (open: boolean) => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function StageEditorDialog({ reminderRuleId, stage, defaultOrderIndex, open, onOpenChange, }: StageEditorDialogProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
//# sourceMappingURL=stage-editor-dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stage-editor-dialog.d.ts","sourceRoot":"","sources":["../../src/components/stage-editor-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,uBAAuB,EAE7B,MAAM,+BAA+B,CAAA;AAoGtC,MAAM,WAAW,sBAAsB;IACrC,cAAc,EAAE,MAAM,CAAA;IACtB,KAAK,EAAE,uBAAuB,GAAG,IAAI,CAAA;IACrC,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;CACtC;AAED,wBAAgB,iBAAiB,CAAC,EAChC,cAAc,EACd,KAAK,EACL,iBAAqB,EACrB,IAAI,EACJ,YAAY,GACb,EAAE,sBAAsB,2CAuTxB"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useReminderRuleStageMutation, } from "@voyantjs/notifications-react";
|
|
4
|
+
import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components";
|
|
5
|
+
import { Field, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSet, FieldTitle, } from "@voyantjs/ui/components/field";
|
|
6
|
+
import { Switch } from "@voyantjs/ui/components/switch";
|
|
7
|
+
import { Loader2, Plus, Trash2 } from "lucide-react";
|
|
8
|
+
import { useEffect, useState } from "react";
|
|
9
|
+
import { useNotificationsUiMessagesOrDefault } from "../i18n/index.js";
|
|
10
|
+
let intervalRowSeq = 0;
|
|
11
|
+
const nextIntervalRowKey = () => `iv-${++intervalRowSeq}`;
|
|
12
|
+
const ANCHORS = [
|
|
13
|
+
"due_date",
|
|
14
|
+
"booking_created_at",
|
|
15
|
+
"departure_date",
|
|
16
|
+
"invoice_issued_at",
|
|
17
|
+
"last_send_at",
|
|
18
|
+
];
|
|
19
|
+
const CADENCES = ["once", "every_n_days", "escalating"];
|
|
20
|
+
function fromRecord(stage, orderIndex) {
|
|
21
|
+
if (!stage) {
|
|
22
|
+
return {
|
|
23
|
+
name: "",
|
|
24
|
+
orderIndex,
|
|
25
|
+
anchor: "due_date",
|
|
26
|
+
windowStartDays: -7,
|
|
27
|
+
windowEndDays: 0,
|
|
28
|
+
cadenceKind: "once",
|
|
29
|
+
cadenceEveryDays: null,
|
|
30
|
+
cadenceIntervals: [],
|
|
31
|
+
maxSendsInStage: null,
|
|
32
|
+
respectQuietHours: true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
name: stage.name ?? "",
|
|
37
|
+
orderIndex: stage.orderIndex,
|
|
38
|
+
anchor: stage.anchor,
|
|
39
|
+
windowStartDays: stage.windowStartDays,
|
|
40
|
+
windowEndDays: stage.windowEndDays,
|
|
41
|
+
cadenceKind: stage.cadenceKind,
|
|
42
|
+
cadenceEveryDays: stage.cadenceEveryDays,
|
|
43
|
+
cadenceIntervals: stage.cadenceIntervals?.map((i) => ({
|
|
44
|
+
rowKey: nextIntervalRowKey(),
|
|
45
|
+
whenDaysUntilDueGT: i.whenDaysUntilDueGT ?? null,
|
|
46
|
+
repeatEveryDays: i.repeatEveryDays,
|
|
47
|
+
})) ?? [],
|
|
48
|
+
maxSendsInStage: stage.maxSendsInStage,
|
|
49
|
+
respectQuietHours: stage.respectQuietHours,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function StageEditorDialog({ reminderRuleId, stage, defaultOrderIndex = 0, open, onOpenChange, }) {
|
|
53
|
+
const messages = useNotificationsUiMessagesOrDefault();
|
|
54
|
+
const { create, update } = useReminderRuleStageMutation(reminderRuleId);
|
|
55
|
+
const [form, setForm] = useState(() => fromRecord(stage, defaultOrderIndex));
|
|
56
|
+
const isEdit = Boolean(stage);
|
|
57
|
+
const isPending = create.isPending || update.isPending;
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (open)
|
|
60
|
+
setForm(fromRecord(stage, defaultOrderIndex));
|
|
61
|
+
}, [open, stage, defaultOrderIndex]);
|
|
62
|
+
const setField = (key, value) => setForm((prev) => ({ ...prev, [key]: value }));
|
|
63
|
+
const addInterval = () => setField("cadenceIntervals", [
|
|
64
|
+
...form.cadenceIntervals,
|
|
65
|
+
{ rowKey: nextIntervalRowKey(), whenDaysUntilDueGT: null, repeatEveryDays: 7 },
|
|
66
|
+
]);
|
|
67
|
+
const removeInterval = (rowKey) => setField("cadenceIntervals", form.cadenceIntervals.filter((row) => row.rowKey !== rowKey));
|
|
68
|
+
const handleSubmit = async () => {
|
|
69
|
+
const input = {
|
|
70
|
+
name: form.name || null,
|
|
71
|
+
orderIndex: form.orderIndex,
|
|
72
|
+
anchor: form.anchor,
|
|
73
|
+
windowStartDays: form.windowStartDays,
|
|
74
|
+
windowEndDays: form.windowEndDays,
|
|
75
|
+
cadenceKind: form.cadenceKind,
|
|
76
|
+
cadenceEveryDays: form.cadenceKind === "every_n_days" ? form.cadenceEveryDays : null,
|
|
77
|
+
cadenceIntervals: form.cadenceKind === "escalating"
|
|
78
|
+
? form.cadenceIntervals.map((row) => ({
|
|
79
|
+
whenDaysUntilDueGT: row.whenDaysUntilDueGT,
|
|
80
|
+
repeatEveryDays: row.repeatEveryDays,
|
|
81
|
+
}))
|
|
82
|
+
: null,
|
|
83
|
+
maxSendsInStage: form.maxSendsInStage,
|
|
84
|
+
respectQuietHours: form.respectQuietHours,
|
|
85
|
+
};
|
|
86
|
+
if (isEdit && stage) {
|
|
87
|
+
await update.mutateAsync({ stageId: stage.id, input });
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
await create.mutateAsync(input);
|
|
91
|
+
}
|
|
92
|
+
onOpenChange(false);
|
|
93
|
+
};
|
|
94
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-2xl", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEdit ? messages.stage.titles.edit : messages.stage.titles.create }) }), _jsx(DialogBody, { children: _jsxs(FieldGroup, { children: [_jsxs("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-[1fr_8rem]", children: [_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "stage-name", children: messages.stage.fields.name }), _jsx(Input, { id: "stage-name", value: form.name, onChange: (e) => setField("name", e.target.value), placeholder: messages.stage.placeholders.name })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "stage-order", children: messages.stage.fields.orderIndex }), _jsx(Input, { id: "stage-order", type: "number", value: form.orderIndex, onChange: (e) => setField("orderIndex", Number(e.target.value)) })] })] }), _jsxs(FieldSet, { children: [_jsx(FieldLegend, { variant: "label", children: messages.stage.fields.anchor }), _jsx(FieldDescription, { children: "When the eligibility window opens, relative to the chosen anchor." }), _jsxs(FieldGroup, { className: "gap-4", children: [_jsx(Field, { children: _jsxs(Select, { value: form.anchor, onValueChange: (v) => setField("anchor", v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: ANCHORS.map((a) => (_jsx(SelectItem, { value: a, children: messages.stage.anchors[a] }, a))) })] }) }), _jsxs("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-2", children: [_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "window-start", children: messages.stage.fields.windowStartDays }), _jsx(Input, { id: "window-start", type: "number", value: form.windowStartDays, onChange: (e) => setField("windowStartDays", Number(e.target.value)) })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "window-end", children: messages.stage.fields.windowEndDays }), _jsx(Input, { id: "window-end", type: "number", value: form.windowEndDays, onChange: (e) => setField("windowEndDays", Number(e.target.value)) })] })] })] })] }), _jsxs(FieldSet, { children: [_jsx(FieldLegend, { variant: "label", children: messages.stage.fields.cadenceKind }), _jsx(FieldDescription, { children: "How often this stage may fire while inside the window." }), _jsxs(FieldGroup, { className: "gap-4", children: [_jsx(Field, { children: _jsxs(Select, { value: form.cadenceKind, onValueChange: (v) => setField("cadenceKind", v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: CADENCES.map((c) => (_jsx(SelectItem, { value: c, children: messages.stage.cadences[c] }, c))) })] }) }), form.cadenceKind === "every_n_days" && (_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "cadence-every", children: messages.stage.fields.cadenceEveryDays }), _jsx(Input, { id: "cadence-every", type: "number", min: 1, value: form.cadenceEveryDays ?? "", onChange: (e) => setField("cadenceEveryDays", e.target.value ? Number(e.target.value) : null) })] })), form.cadenceKind === "escalating" && (_jsxs(Field, { children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx(FieldLabel, { children: messages.stage.fields.cadenceIntervals }), _jsxs(Button, { type: "button", size: "sm", variant: "outline", onClick: addInterval, children: [_jsx(Plus, { className: "size-4" }), messages.stage.intervalRow.addInterval] })] }), form.cadenceIntervals.length === 0 ? (_jsx(FieldDescription, { children: "Add buckets keyed on days-until-due to scale cadence as the deadline approaches." })) : null, _jsx("div", { className: "space-y-2", children: form.cadenceIntervals.map((interval) => (_jsxs("div", { className: "grid grid-cols-[1fr_1fr_auto] items-end gap-2", children: [_jsxs(Field, { children: [_jsx(FieldLabel, { className: "text-xs", children: messages.stage.intervalRow.whenDaysUntilDueGT }), _jsx(Input, { type: "number", value: interval.whenDaysUntilDueGT ?? "", onChange: (e) => setField("cadenceIntervals", form.cadenceIntervals.map((row) => row.rowKey === interval.rowKey
|
|
95
|
+
? {
|
|
96
|
+
...row,
|
|
97
|
+
whenDaysUntilDueGT: e.target.value
|
|
98
|
+
? Number(e.target.value)
|
|
99
|
+
: null,
|
|
100
|
+
}
|
|
101
|
+
: row)) })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { className: "text-xs", children: messages.stage.intervalRow.repeatEveryDays }), _jsx(Input, { type: "number", min: 1, value: interval.repeatEveryDays, onChange: (e) => setField("cadenceIntervals", form.cadenceIntervals.map((row) => row.rowKey === interval.rowKey
|
|
102
|
+
? { ...row, repeatEveryDays: Number(e.target.value) }
|
|
103
|
+
: row)) })] }), _jsx(Button, { type: "button", size: "icon", variant: "ghost", onClick: () => removeInterval(interval.rowKey), "aria-label": messages.stage.intervalRow.removeInterval, children: _jsx(Trash2, { className: "size-4" }) })] }, interval.rowKey))) })] }))] })] }), _jsxs(FieldSet, { children: [_jsx(FieldLegend, { variant: "label", children: "Stop conditions" }), _jsxs(FieldGroup, { className: "gap-4", children: [_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "max-sends", children: messages.stage.fields.maxSendsInStage }), _jsx(Input, { id: "max-sends", type: "number", min: 1, value: form.maxSendsInStage ?? "", onChange: (e) => setField("maxSendsInStage", e.target.value ? Number(e.target.value) : null), placeholder: messages.common.optionalPlaceholder }), _jsx(FieldDescription, { children: "Leave blank for no limit. When reached, the next stage takes over." })] }), _jsxs(Field, { orientation: "horizontal", children: [_jsx(Switch, { id: "respect-quiet-hours", checked: form.respectQuietHours, onCheckedChange: (v) => setField("respectQuietHours", Boolean(v)) }), _jsxs(FieldLabel, { htmlFor: "respect-quiet-hours", className: "!w-auto !flex-row", children: [_jsx(FieldTitle, { children: messages.stage.fields.respectQuietHours }), _jsx(FieldDescription, { children: "Defer fires that would land inside the tenant's quiet-hours window." })] })] })] })] })] }) }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: isPending, children: messages.common.cancel }), _jsxs(Button, { onClick: handleSubmit, disabled: isPending, children: [isPending ? _jsx(Loader2, { className: "size-4 animate-spin" }) : null, isEdit ? messages.common.save : messages.common.create] })] })] }) }));
|
|
104
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stage-list.d.ts","sourceRoot":"","sources":["../../src/components/stage-list.tsx"],"names":[],"mappings":"AAeA,MAAM,WAAW,cAAc;IAC7B,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,wBAAgB,SAAS,CAAC,EAAE,cAAc,EAAE,EAAE,cAAc,2CAqH3D"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useReminderRuleStageMutation, useReminderRuleStages, } from "@voyantjs/notifications-react";
|
|
4
|
+
import { Badge, Button, Card, CardContent, CardHeader, CardTitle } from "@voyantjs/ui/components";
|
|
5
|
+
import { ArrowDown, ArrowUp, Pencil, Plus, Trash2 } from "lucide-react";
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { useNotificationsUiMessagesOrDefault } from "../i18n/index.js";
|
|
8
|
+
import { StageChannelList } from "./stage-channel-list.js";
|
|
9
|
+
import { StageEditorDialog } from "./stage-editor-dialog.js";
|
|
10
|
+
export function StageList({ reminderRuleId }) {
|
|
11
|
+
const messages = useNotificationsUiMessagesOrDefault();
|
|
12
|
+
const { data: stages, isLoading } = useReminderRuleStages(reminderRuleId);
|
|
13
|
+
const { remove, reorder } = useReminderRuleStageMutation(reminderRuleId);
|
|
14
|
+
const [editing, setEditing] = useState(null);
|
|
15
|
+
const [creating, setCreating] = useState(false);
|
|
16
|
+
const move = async (index, direction) => {
|
|
17
|
+
if (!stages)
|
|
18
|
+
return;
|
|
19
|
+
const target = index + direction;
|
|
20
|
+
if (target < 0 || target >= stages.length)
|
|
21
|
+
return;
|
|
22
|
+
const next = [...stages];
|
|
23
|
+
const [item] = next.splice(index, 1);
|
|
24
|
+
if (!item)
|
|
25
|
+
return;
|
|
26
|
+
next.splice(target, 0, item);
|
|
27
|
+
await reorder.mutateAsync({ stageIds: next.map((s) => s.id) });
|
|
28
|
+
};
|
|
29
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("h3", { className: "text-base font-semibold", children: messages.stage.listHeading }), _jsxs(Button, { size: "sm", onClick: () => setCreating(true), children: [_jsx(Plus, { className: "size-4" }), " ", messages.stage.addStage] })] }), isLoading && _jsx("p", { className: "text-sm text-muted-foreground", children: messages.common.loading }), stages && stages.length === 0 && (_jsx("p", { className: "text-sm text-muted-foreground", children: messages.stage.listEmpty })), stages?.map((stage, index) => (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between space-y-0 pb-3", children: [_jsxs(CardTitle, { className: "flex items-center gap-2 text-sm font-medium", children: [_jsxs(Badge, { variant: "outline", children: ["#", stage.orderIndex] }), stage.name ?? messages.stage.placeholders.name, _jsx(Badge, { variant: "secondary", children: messages.stage.anchors[stage.anchor] }), _jsx(Badge, { children: messages.stage.cadences[stage.cadenceKind] })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { size: "icon", variant: "ghost", onClick: () => move(index, -1), disabled: index === 0, children: _jsx(ArrowUp, { className: "size-4" }) }), _jsx(Button, { size: "icon", variant: "ghost", onClick: () => move(index, 1), disabled: index === stages.length - 1, children: _jsx(ArrowDown, { className: "size-4" }) }), _jsx(Button, { size: "icon", variant: "ghost", onClick: () => setEditing(stage), children: _jsx(Pencil, { className: "size-4" }) }), _jsx(Button, { size: "icon", variant: "ghost", onClick: () => {
|
|
30
|
+
if (window.confirm(messages.stage.deleteConfirm)) {
|
|
31
|
+
void remove.mutateAsync(stage.id);
|
|
32
|
+
}
|
|
33
|
+
}, children: _jsx(Trash2, { className: "size-4" }) })] })] }), _jsxs(CardContent, { className: "space-y-3 text-sm", children: [_jsxs("div", { className: "flex flex-wrap gap-x-6 gap-y-1 text-muted-foreground", children: [_jsxs("span", { children: [messages.stage.fields.windowStartDays, ": ", stage.windowStartDays] }), _jsxs("span", { children: [messages.stage.fields.windowEndDays, ": ", stage.windowEndDays] }), stage.cadenceKind === "every_n_days" && (_jsxs("span", { children: [messages.stage.fields.cadenceEveryDays, ": ", stage.cadenceEveryDays] })), stage.maxSendsInStage != null && (_jsxs("span", { children: [messages.stage.fields.maxSendsInStage, ": ", stage.maxSendsInStage] }))] }), _jsx(StageChannelList, { reminderRuleId: reminderRuleId, stageId: stage.id })] })] }, stage.id))), creating && (_jsx(StageEditorDialog, { reminderRuleId: reminderRuleId, stage: null, defaultOrderIndex: stages?.length ?? 0, open: creating, onOpenChange: setCreating })), editing && (_jsx(StageEditorDialog, { reminderRuleId: reminderRuleId, stage: editing, open: Boolean(editing), onOpenChange: (open) => !open && setEditing(null) }))] }));
|
|
34
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type NotificationTemplateRecord } from "@voyantjs/notifications-react";
|
|
2
|
+
export interface TemplatePickerProps {
|
|
3
|
+
/** Currently selected template id (or null when nothing is picked). */
|
|
4
|
+
value: string | null;
|
|
5
|
+
onChange: (value: string | null) => void;
|
|
6
|
+
/** Restrict results to templates registered for this channel. */
|
|
7
|
+
channel?: NotificationTemplateRecord["channel"];
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
emptyText?: string;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Async-friendly template picker. Searches active templates by name / slug
|
|
14
|
+
* filtered by the chosen channel and resolves to the template id. The
|
|
15
|
+
* currently selected template is fetched separately so its label keeps
|
|
16
|
+
* rendering even after the search list filters it out.
|
|
17
|
+
*/
|
|
18
|
+
export declare function TemplatePicker({ value, onChange, channel, placeholder, emptyText, disabled, }: TemplatePickerProps): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
//# sourceMappingURL=template-picker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template-picker.d.ts","sourceRoot":"","sources":["../../src/components/template-picker.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,0BAA0B,EAGhC,MAAM,+BAA+B,CAAA;AAItC,MAAM,WAAW,mBAAmB;IAClC,uEAAuE;IACvE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACxC,iEAAiE;IACjE,OAAO,CAAC,EAAE,0BAA0B,CAAC,SAAS,CAAC,CAAA;IAC/C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,QAAQ,EACR,OAAO,EACP,WAAiC,EACjC,SAAiC,EACjC,QAAQ,GACT,EAAE,mBAAmB,2CA6BrB"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useNotificationTemplate, useNotificationTemplates, } from "@voyantjs/notifications-react";
|
|
4
|
+
import { AsyncCombobox } from "@voyantjs/ui/components/async-combobox";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
/**
|
|
7
|
+
* Async-friendly template picker. Searches active templates by name / slug
|
|
8
|
+
* filtered by the chosen channel and resolves to the template id. The
|
|
9
|
+
* currently selected template is fetched separately so its label keeps
|
|
10
|
+
* rendering even after the search list filters it out.
|
|
11
|
+
*/
|
|
12
|
+
export function TemplatePicker({ value, onChange, channel, placeholder = "Search templates…", emptyText = "No templates found.", disabled, }) {
|
|
13
|
+
const [search, setSearch] = React.useState("");
|
|
14
|
+
const { data } = useNotificationTemplates({
|
|
15
|
+
channel,
|
|
16
|
+
status: "active",
|
|
17
|
+
search: search || undefined,
|
|
18
|
+
limit: 20,
|
|
19
|
+
offset: 0,
|
|
20
|
+
});
|
|
21
|
+
const { data: selected } = useNotificationTemplate(value ?? "", { enabled: Boolean(value) });
|
|
22
|
+
const items = data?.data ?? [];
|
|
23
|
+
return (_jsx(AsyncCombobox, { value: value, onChange: onChange, items: items, selectedItem: selected ?? null, getKey: (template) => template.id, getLabel: (template) => template.name, getSecondary: (template) => template.slug, onSearchChange: setSearch, placeholder: placeholder, emptyText: emptyText, disabled: disabled, clearable: true }));
|
|
24
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface TimezoneComboboxProps {
|
|
2
|
+
value: string | null | undefined;
|
|
3
|
+
onChange: (value: string | null) => void;
|
|
4
|
+
placeholder?: string;
|
|
5
|
+
emptyText?: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function TimezoneCombobox({ value, onChange, placeholder, emptyText, disabled, }: TimezoneComboboxProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
//# sourceMappingURL=timezone-combobox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timezone-combobox.d.ts","sourceRoot":"","sources":["../../src/components/timezone-combobox.tsx"],"names":[],"mappings":"AA6DA,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IAChC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACxC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,QAAQ,EACR,WAAiC,EACjC,SAAiC,EACjC,QAAQ,GACT,EAAE,qBAAqB,2CAwCvB"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyantjs/ui/components/combobox";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
const TIMEZONES = (() => {
|
|
6
|
+
const intl = Intl;
|
|
7
|
+
if (typeof intl.supportedValuesOf === "function") {
|
|
8
|
+
try {
|
|
9
|
+
return [...intl.supportedValuesOf("timeZone")].sort();
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
// fall through
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return [
|
|
16
|
+
"UTC",
|
|
17
|
+
"Africa/Cairo",
|
|
18
|
+
"Africa/Johannesburg",
|
|
19
|
+
"Africa/Lagos",
|
|
20
|
+
"America/Chicago",
|
|
21
|
+
"America/Denver",
|
|
22
|
+
"America/Los_Angeles",
|
|
23
|
+
"America/Mexico_City",
|
|
24
|
+
"America/New_York",
|
|
25
|
+
"America/Sao_Paulo",
|
|
26
|
+
"Asia/Bangkok",
|
|
27
|
+
"Asia/Dubai",
|
|
28
|
+
"Asia/Hong_Kong",
|
|
29
|
+
"Asia/Jakarta",
|
|
30
|
+
"Asia/Kolkata",
|
|
31
|
+
"Asia/Manila",
|
|
32
|
+
"Asia/Seoul",
|
|
33
|
+
"Asia/Shanghai",
|
|
34
|
+
"Asia/Singapore",
|
|
35
|
+
"Asia/Tokyo",
|
|
36
|
+
"Australia/Sydney",
|
|
37
|
+
"Europe/Amsterdam",
|
|
38
|
+
"Europe/Berlin",
|
|
39
|
+
"Europe/Bucharest",
|
|
40
|
+
"Europe/Istanbul",
|
|
41
|
+
"Europe/Lisbon",
|
|
42
|
+
"Europe/London",
|
|
43
|
+
"Europe/Madrid",
|
|
44
|
+
"Europe/Moscow",
|
|
45
|
+
"Europe/Paris",
|
|
46
|
+
"Europe/Rome",
|
|
47
|
+
"Europe/Vienna",
|
|
48
|
+
"Pacific/Auckland",
|
|
49
|
+
];
|
|
50
|
+
})();
|
|
51
|
+
export function TimezoneCombobox({ value, onChange, placeholder = "Search timezones…", emptyText = "No timezones found.", disabled, }) {
|
|
52
|
+
const [inputValue, setInputValue] = React.useState(value ?? "");
|
|
53
|
+
React.useEffect(() => {
|
|
54
|
+
setInputValue(value ?? "");
|
|
55
|
+
}, [value]);
|
|
56
|
+
return (_jsxs(Combobox, { items: TIMEZONES, value: value ?? null, inputValue: inputValue, autoHighlight: true, disabled: disabled, itemToStringLabel: (item) => item, itemToStringValue: (item) => item, onInputValueChange: (next) => {
|
|
57
|
+
setInputValue(next);
|
|
58
|
+
if (!next)
|
|
59
|
+
onChange(null);
|
|
60
|
+
}, onValueChange: (next) => {
|
|
61
|
+
const tz = next ?? null;
|
|
62
|
+
onChange(tz);
|
|
63
|
+
setInputValue(tz ?? "");
|
|
64
|
+
}, children: [_jsx(ComboboxInput, { placeholder: placeholder, showClear: Boolean(value) }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: emptyText }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (tz) => (_jsx(ComboboxItem, { value: tz, children: tz }, tz)) }) })] })] }));
|
|
65
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/i18n/en.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAA;AAE5D,eAAO,MAAM,iBAAiB,EAAE,uBA0I/B,CAAA"}
|