@voyantjs/ui 0.20.0 → 0.21.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.
@@ -21,7 +21,15 @@ export type TemplateAuthoringSnippet = {
21
21
  type ContractTemplateAuthoringHelpProps = {
22
22
  variableGroups: TemplateAuthoringVariableGroup[];
23
23
  snippets?: TemplateAuthoringSnippet[];
24
+ /**
25
+ * @deprecated Variables now expose a copy-to-clipboard button instead of
26
+ * an inline insert. Kept for prop-shape compatibility with older callers.
27
+ */
24
28
  onInsertVariable?: (variable: TemplateAuthoringVariable) => void;
29
+ /**
30
+ * @deprecated Snippets now expose a copy-to-clipboard button instead of
31
+ * an inline insert. Kept for prop-shape compatibility with older callers.
32
+ */
25
33
  onInsertSnippet?: (snippet: TemplateAuthoringSnippet) => void;
26
34
  className?: string;
27
35
  title?: string;
@@ -34,11 +42,12 @@ type ContractTemplateAuthoringHelpProps = {
34
42
  searchPlaceholder?: string;
35
43
  noVariables?: string;
36
44
  example?: string;
37
- insert?: string;
45
+ copy?: string;
46
+ copied?: string;
38
47
  liquidUsage?: string;
39
48
  noLiquidSnippets?: string;
40
49
  };
41
50
  };
42
- export declare function ContractTemplateAuthoringHelp({ variableGroups, snippets, onInsertVariable, onInsertSnippet, className, title, description, messages, }: ContractTemplateAuthoringHelpProps): import("react/jsx-runtime").JSX.Element;
51
+ export declare function ContractTemplateAuthoringHelp({ variableGroups, snippets, onInsertVariable: _deprecatedOnInsertVariable, onInsertSnippet: _deprecatedOnInsertSnippet, className, title, description, messages, }: ContractTemplateAuthoringHelpProps): import("react/jsx-runtime").JSX.Element;
43
52
  export {};
44
53
  //# sourceMappingURL=contract-template-authoring-help.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"contract-template-authoring-help.d.ts","sourceRoot":"","sources":["../../src/components/contract-template-authoring-help.tsx"],"names":[],"mappings":"AAYA,MAAM,MAAM,yBAAyB,GAAG;IACtC,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,8BAA8B,GAAG;IAC3C,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,yBAAyB,EAAE,CAAA;CACvC,CAAA;AAED,MAAM,MAAM,wBAAwB,GAAG;IACrC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,KAAK,kCAAkC,GAAG;IACxC,cAAc,EAAE,8BAA8B,EAAE,CAAA;IAChD,QAAQ,CAAC,EAAE,wBAAwB,EAAE,CAAA;IACrC,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,yBAAyB,KAAK,IAAI,CAAA;IAChE,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,wBAAwB,KAAK,IAAI,CAAA;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE;QACT,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;AAMD,wBAAgB,6BAA6B,CAAC,EAC5C,cAAc,EACd,QAAa,EACb,gBAAgB,EAChB,eAAe,EACf,SAAS,EACT,KAA4B,EAC5B,WAAwH,EACxH,QAAQ,GACT,EAAE,kCAAkC,2CAiLpC"}
1
+ {"version":3,"file":"contract-template-authoring-help.d.ts","sourceRoot":"","sources":["../../src/components/contract-template-authoring-help.tsx"],"names":[],"mappings":"AAYA,MAAM,MAAM,yBAAyB,GAAG;IACtC,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,8BAA8B,GAAG;IAC3C,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,yBAAyB,EAAE,CAAA;CACvC,CAAA;AAED,MAAM,MAAM,wBAAwB,GAAG;IACrC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,KAAK,kCAAkC,GAAG;IACxC,cAAc,EAAE,8BAA8B,EAAE,CAAA;IAChD,QAAQ,CAAC,EAAE,wBAAwB,EAAE,CAAA;IACrC;;;OAGG;IACH,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,yBAAyB,KAAK,IAAI,CAAA;IAChE;;;OAGG;IACH,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,wBAAwB,KAAK,IAAI,CAAA;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE;QACT,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,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAC1B,CAAA;CACF,CAAA;AA+CD,wBAAgB,6BAA6B,CAAC,EAC5C,cAAc,EACd,QAAa,EACb,gBAAgB,EAAE,2BAA2B,EAC7C,eAAe,EAAE,0BAA0B,EAC3C,SAAS,EACT,KAA4B,EAC5B,WAAwH,EACxH,QAAQ,GACT,EAAE,kCAAkC,2CAsMpC"}
@@ -1,6 +1,6 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { SearchIcon, SparklesIcon } from "lucide-react";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { CheckIcon, CopyIcon, SearchIcon, SparklesIcon } from "lucide-react";
4
4
  import * as React from "react";
5
5
  import { cn } from "../lib/utils";
6
6
  import { Badge } from "./badge";
@@ -8,12 +8,53 @@ import { Button } from "./button";
8
8
  import { Input } from "./input";
9
9
  import { ScrollArea } from "./scroll-area";
10
10
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs";
11
+ function useCopyToClipboard(timeoutMs = 1200) {
12
+ const [copiedId, setCopiedId] = React.useState(null);
13
+ const timeoutRef = React.useRef(null);
14
+ React.useEffect(() => {
15
+ return () => {
16
+ if (timeoutRef.current)
17
+ clearTimeout(timeoutRef.current);
18
+ };
19
+ }, []);
20
+ const copy = React.useCallback(async (id, text) => {
21
+ try {
22
+ if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
23
+ await navigator.clipboard.writeText(text);
24
+ }
25
+ else if (typeof document !== "undefined") {
26
+ // Fallback for browsers without the async clipboard API.
27
+ const textarea = document.createElement("textarea");
28
+ textarea.value = text;
29
+ textarea.setAttribute("readonly", "");
30
+ textarea.style.position = "fixed";
31
+ textarea.style.opacity = "0";
32
+ document.body.appendChild(textarea);
33
+ textarea.select();
34
+ document.execCommand("copy");
35
+ document.body.removeChild(textarea);
36
+ }
37
+ setCopiedId(id);
38
+ if (timeoutRef.current)
39
+ clearTimeout(timeoutRef.current);
40
+ timeoutRef.current = setTimeout(() => setCopiedId(null), timeoutMs);
41
+ }
42
+ catch {
43
+ // Silent — copy failures are surfaced by the lack of the
44
+ // confirmation state. Better than spamming a toast we don't own.
45
+ }
46
+ }, [timeoutMs]);
47
+ return { copiedId, copy };
48
+ }
11
49
  function matchesSearch(haystack, query) {
12
50
  return haystack.toLowerCase().includes(query);
13
51
  }
14
- export function ContractTemplateAuthoringHelp({ variableGroups, snippets = [], onInsertVariable, onInsertSnippet, className, title = "Template variables", description = "Templates render with Liquid. Use output tags for variables and control tags for loops and conditionals.", messages, }) {
52
+ export function ContractTemplateAuthoringHelp({ variableGroups, snippets = [], onInsertVariable: _deprecatedOnInsertVariable, onInsertSnippet: _deprecatedOnInsertSnippet, className, title = "Template variables", description = "Templates render with Liquid. Use output tags for variables and control tags for loops and conditionals.", messages, }) {
15
53
  const [search, setSearch] = React.useState("");
16
54
  const normalizedQuery = search.trim().toLowerCase();
55
+ const { copiedId, copy } = useCopyToClipboard();
56
+ const copyLabel = messages?.copy ?? "Copy";
57
+ const copiedLabel = messages?.copied ?? "Copied";
17
58
  const filteredGroups = React.useMemo(() => {
18
59
  if (!normalizedQuery) {
19
60
  return variableGroups;
@@ -33,6 +74,6 @@ export function ContractTemplateAuthoringHelp({ variableGroups, snippets = [], o
33
74
  }
34
75
  return snippets.filter((snippet) => matchesSearch([snippet.label, snippet.description, snippet.code].filter(Boolean).join(" "), normalizedQuery));
35
76
  }, [normalizedQuery, snippets]);
36
- return (_jsxs("div", { className: cn("rounded-md border bg-muted/20", className), children: [_jsxs("div", { className: "flex flex-col gap-1 border-b px-4 py-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(SparklesIcon, { className: "h-4 w-4 text-muted-foreground" }), _jsx("h3", { className: "text-sm font-medium", children: title })] }), _jsx("p", { className: "text-xs text-muted-foreground", children: description })] }), _jsxs(Tabs, { defaultValue: "variables", className: "gap-3 p-4", children: [_jsxs(TabsList, { className: "w-full", children: [_jsx(TabsTrigger, { value: "variables", children: messages?.tabs?.variables ?? "Variables" }), _jsx(TabsTrigger, { value: "liquid", children: messages?.tabs?.liquid ?? "Liquid" })] }), _jsxs("div", { className: "relative", children: [_jsx(SearchIcon, { className: "pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { value: search, onChange: (event) => setSearch(event.target.value), placeholder: messages?.searchPlaceholder ?? "Search variables or snippets...", className: "pl-9" })] }), _jsx(TabsContent, { value: "variables", children: _jsx(ScrollArea, { className: "h-80", children: _jsx("div", { className: "space-y-4 pr-3", children: filteredGroups.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: messages?.noVariables ?? "No variables match this search." })) : (filteredGroups.map((group) => (_jsxs("section", { className: "space-y-2", children: [_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("h4", { className: "text-sm font-medium", children: group.label }), _jsx(Badge, { variant: "outline", children: group.variables.length })] }), group.description ? (_jsx("p", { className: "text-xs text-muted-foreground", children: group.description })) : null] }), _jsx("div", { className: "space-y-2", children: group.variables.map((variable) => (_jsx("div", { className: "rounded-md border bg-background p-3 shadow-xs", children: _jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("p", { className: "text-sm font-medium", children: variable.label }), _jsx(Badge, { variant: "outline", className: "font-normal", children: variable.type })] }), _jsx("code", { className: "rounded bg-muted px-1.5 py-0.5 font-mono text-xs", children: `{{ ${variable.key} }}` }), variable.description ? (_jsx("p", { className: "text-xs text-muted-foreground", children: variable.description })) : null, _jsxs("p", { className: "text-xs text-muted-foreground", children: [messages?.example ?? "Example", ":", " ", _jsx("span", { className: "font-mono text-foreground", children: variable.example })] })] }), onInsertVariable ? (_jsx(Button, { type: "button", size: "sm", variant: "outline", onClick: () => onInsertVariable(variable), children: messages?.insert ?? "Insert" })) : null] }) }, variable.key))) })] }, group.id)))) }) }) }), _jsx(TabsContent, { value: "liquid", children: _jsx(ScrollArea, { className: "h-80", children: _jsxs("div", { className: "space-y-3 pr-3", children: [_jsx("div", { className: "rounded-md border bg-background p-3 text-xs text-muted-foreground", children: messages?.liquidUsage ??
37
- "Use {{ ... }} for output and {% ... %} for control flow." }), filteredSnippets.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: messages?.noLiquidSnippets ?? "No Liquid snippets match this search." })) : (filteredSnippets.map((snippet) => (_jsxs("div", { className: "rounded-md border bg-background p-3 shadow-xs", children: [_jsxs("div", { className: "mb-2 flex items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium", children: snippet.label }), _jsx("p", { className: "text-xs text-muted-foreground", children: snippet.description })] }), onInsertSnippet ? (_jsx(Button, { type: "button", size: "sm", variant: "outline", onClick: () => onInsertSnippet(snippet), children: messages?.insert ?? "Insert" })) : null] }), _jsx("pre", { className: "whitespace-pre-wrap rounded-md bg-muted p-3 font-mono text-xs text-foreground", children: snippet.code })] }, snippet.id))))] }) }) })] })] }));
77
+ return (_jsxs("div", { className: cn("rounded-md border bg-muted/20", className), children: [_jsxs("div", { className: "flex flex-col gap-1 border-b px-4 py-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(SparklesIcon, { className: "h-4 w-4 text-muted-foreground" }), _jsx("h3", { className: "text-sm font-medium", children: title })] }), _jsx("p", { className: "text-xs text-muted-foreground", children: description })] }), _jsxs(Tabs, { defaultValue: "variables", className: "gap-3 p-4", children: [_jsxs(TabsList, { className: "w-full", children: [_jsx(TabsTrigger, { value: "variables", children: messages?.tabs?.variables ?? "Variables" }), _jsx(TabsTrigger, { value: "liquid", children: messages?.tabs?.liquid ?? "Liquid" })] }), _jsxs("div", { className: "relative", children: [_jsx(SearchIcon, { className: "pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { value: search, onChange: (event) => setSearch(event.target.value), placeholder: messages?.searchPlaceholder ?? "Search variables or snippets...", className: "pl-9" })] }), _jsx(TabsContent, { value: "variables", children: _jsx(ScrollArea, { className: "h-80", children: _jsx("div", { className: "space-y-4 pr-3", children: filteredGroups.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: messages?.noVariables ?? "No variables match this search." })) : (filteredGroups.map((group) => (_jsxs("section", { className: "space-y-2", children: [_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("h4", { className: "text-sm font-medium", children: group.label }), _jsx(Badge, { variant: "outline", children: group.variables.length })] }), group.description ? (_jsx("p", { className: "text-xs text-muted-foreground", children: group.description })) : null] }), _jsx("div", { className: "space-y-2", children: group.variables.map((variable) => (_jsx("div", { className: "rounded-md border bg-background p-3 shadow-xs", children: _jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("p", { className: "text-sm font-medium", children: variable.label }), _jsx(Badge, { variant: "outline", className: "font-normal", children: variable.type })] }), _jsx("code", { className: "rounded bg-muted px-1.5 py-0.5 font-mono text-xs", children: `{{ ${variable.key} }}` }), variable.description ? (_jsx("p", { className: "text-xs text-muted-foreground", children: variable.description })) : null, _jsxs("p", { className: "text-xs text-muted-foreground", children: [messages?.example ?? "Example", ":", " ", _jsx("span", { className: "font-mono text-foreground", children: variable.example })] })] }), _jsx(Button, { type: "button", size: "sm", variant: "outline", "aria-label": `${copyLabel} ${variable.key}`, onClick: () => copy(`var:${variable.key}`, `{{ ${variable.key} }}`), children: copiedId === `var:${variable.key}` ? (_jsxs(_Fragment, { children: [_jsx(CheckIcon, { className: "mr-1.5 h-3.5 w-3.5" }), copiedLabel] })) : (_jsxs(_Fragment, { children: [_jsx(CopyIcon, { className: "mr-1.5 h-3.5 w-3.5" }), copyLabel] })) })] }) }, variable.key))) })] }, group.id)))) }) }) }), _jsx(TabsContent, { value: "liquid", children: _jsx(ScrollArea, { className: "h-80", children: _jsxs("div", { className: "space-y-3 pr-3", children: [_jsx("div", { className: "rounded-md border bg-background p-3 text-xs text-muted-foreground", children: messages?.liquidUsage ??
78
+ "Use {{ ... }} for output and {% ... %} for control flow." }), filteredSnippets.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: messages?.noLiquidSnippets ?? "No Liquid snippets match this search." })) : (filteredSnippets.map((snippet) => (_jsxs("div", { className: "rounded-md border bg-background p-3 shadow-xs", children: [_jsxs("div", { className: "mb-2 flex items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium", children: snippet.label }), _jsx("p", { className: "text-xs text-muted-foreground", children: snippet.description })] }), _jsx(Button, { type: "button", size: "sm", variant: "outline", "aria-label": `${copyLabel} ${snippet.label}`, onClick: () => copy(`snippet:${snippet.id}`, snippet.code), children: copiedId === `snippet:${snippet.id}` ? (_jsxs(_Fragment, { children: [_jsx(CheckIcon, { className: "mr-1.5 h-3.5 w-3.5" }), copiedLabel] })) : (_jsxs(_Fragment, { children: [_jsx(CopyIcon, { className: "mr-1.5 h-3.5 w-3.5" }), copyLabel] })) })] }), _jsx("pre", { className: "whitespace-pre-wrap rounded-md bg-muted p-3 font-mono text-xs text-foreground", children: snippet.code })] }, snippet.id))))] }) }) })] })] }));
38
79
  }
@@ -1 +1 @@
1
- {"version":3,"file":"country-combobox.d.ts","sourceRoot":"","sources":["../../src/components/country-combobox.tsx"],"names":[],"mappings":"AAqBA,MAAM,MAAM,oBAAoB,GAAG;IACjC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IAChC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACvC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,CAAA;AAED,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,QAAQ,EACR,WAAiC,EACjC,SAAiC,EACjC,QAAQ,GACT,EAAE,oBAAoB,2CA+DtB"}
1
+ {"version":3,"file":"country-combobox.d.ts","sourceRoot":"","sources":["../../src/components/country-combobox.tsx"],"names":[],"mappings":"AAqBA,MAAM,MAAM,oBAAoB,GAAG;IACjC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IAChC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACvC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,CAAA;AAED,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,QAAQ,EACR,WAAiC,EACjC,SAAiC,EACjC,QAAQ,GACT,EAAE,oBAAoB,2CAsEtB"}
@@ -21,10 +21,15 @@ export function CountryCombobox({ value, onChange, placeholder = "Search countri
21
21
  setInputValue(selectedLabel);
22
22
  }, [selectedLabel]);
23
23
  const itemCodes = React.useMemo(() => COUNTRY_LIST.map((c) => c.code), []);
24
- return (_jsxs(Combobox, { items: itemCodes, value: normalized, inputValue: inputValue, autoHighlight: true, disabled: disabled, itemToStringValue: (code) => {
25
- const match = COUNTRY_BY_CODE.get(code);
26
- return match ? `${match.name} (${match.code})` : code;
27
- }, onInputValueChange: (next) => {
24
+ // base-ui filters and displays via `itemToStringLabel`; the
25
+ // `itemToStringValue` prop is for form submission only. We want
26
+ // searches like "rom" to match Romania, so the label string includes
27
+ // the country name.
28
+ const itemToStringLabel = React.useCallback((code) => {
29
+ const match = COUNTRY_BY_CODE.get(code);
30
+ return match ? `${match.name} (${match.code})` : code;
31
+ }, []);
32
+ return (_jsxs(Combobox, { items: itemCodes, value: normalized, inputValue: inputValue, autoHighlight: true, disabled: disabled, itemToStringLabel: itemToStringLabel, itemToStringValue: (code) => code, onInputValueChange: (next) => {
28
33
  setInputValue(next);
29
34
  if (!next)
30
35
  onChange(null);
@@ -21,6 +21,7 @@ import { NotificationTemplateAuthoringHelp } from "./notification-template-autho
21
21
  import { NotificationTemplateDetailPage } from "./notification-template-detail-page";
22
22
  import { NotificationTemplatesPage } from "./notification-templates-page";
23
23
  import { OverviewMetric } from "./overview-metric";
24
+ import { RadioGroup, RadioGroupItem } from "./radio-group";
24
25
  import { RichTextEditor } from "./rich-text-editor";
25
26
  import { ScrollArea, ScrollBar } from "./scroll-area";
26
27
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";
@@ -82,5 +83,5 @@ declare const SidebarMenuSubButton: {
82
83
  ({ asChild, children, ...props }: Record<string, unknown> & AsChildProps): import("react/jsx-runtime").JSX.Element;
83
84
  displayName: string;
84
85
  };
85
- export { Avatar, AvatarFallback, AvatarImage, Badge, Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, Collapsible, CollapsibleContent, CollapsibleTrigger, ConfirmActionButton, ContractTemplateAuthoringHelp, cn, Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Input, InputOTP, InputOTPGroup, InputOTPSlot, Label, NotificationDeliveriesPage, NotificationDeliveryDetailDialog, NotificationReminderRulesPage, NotificationReminderRunsPage, NotificationTemplateAuthoringHelp, NotificationTemplateDetailPage, NotificationTemplatesPage, OverviewMetric, RichTextEditor, ScrollArea, ScrollBar, Select, SelectContent, SelectItem, SelectionActionBar, SelectTrigger, SelectValue, Sheet, SheetBody, SheetContent, SheetFooter, SheetHeader, SheetTitle, Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarHeader, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarRail, SidebarTrigger, Switch, Textarea, Toaster, useSidebar, };
86
+ export { Avatar, AvatarFallback, AvatarImage, Badge, Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, Collapsible, CollapsibleContent, CollapsibleTrigger, ConfirmActionButton, ContractTemplateAuthoringHelp, cn, Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Input, InputOTP, InputOTPGroup, InputOTPSlot, Label, NotificationDeliveriesPage, NotificationDeliveryDetailDialog, NotificationReminderRulesPage, NotificationReminderRunsPage, NotificationTemplateAuthoringHelp, NotificationTemplateDetailPage, NotificationTemplatesPage, OverviewMetric, RadioGroup, RadioGroupItem, RichTextEditor, ScrollArea, ScrollBar, Select, SelectContent, SelectItem, SelectionActionBar, SelectTrigger, SelectValue, Sheet, SheetBody, SheetContent, SheetFooter, SheetHeader, SheetTitle, Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarHeader, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarRail, SidebarTrigger, Switch, Textarea, Toaster, useSidebar, };
86
87
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.tsx"],"names":[],"mappings":"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"}
@@ -22,6 +22,7 @@ import { NotificationTemplateAuthoringHelp } from "./notification-template-autho
22
22
  import { NotificationTemplateDetailPage } from "./notification-template-detail-page";
23
23
  import { NotificationTemplatesPage } from "./notification-templates-page";
24
24
  import { OverviewMetric } from "./overview-metric";
25
+ import { RadioGroup, RadioGroupItem } from "./radio-group";
25
26
  import { RichTextEditor } from "./rich-text-editor";
26
27
  import { ScrollArea, ScrollBar } from "./scroll-area";
27
28
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";
@@ -82,4 +83,4 @@ const DropdownMenuTrigger = withAsChild(LocalDropdownMenuTrigger, "DropdownMenuT
82
83
  const DropdownMenuItem = withAsChild(LocalDropdownMenuItem, "DropdownMenuItem");
83
84
  const SidebarMenuButton = withAsChild(LocalSidebarMenuButton, "SidebarMenuButton");
84
85
  const SidebarMenuSubButton = withAsChild(LocalSidebarMenuSubButton, "SidebarMenuSubButton");
85
- export { Avatar, AvatarFallback, AvatarImage, Badge, Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, Collapsible, CollapsibleContent, CollapsibleTrigger, ConfirmActionButton, ContractTemplateAuthoringHelp, cn, Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Input, InputOTP, InputOTPGroup, InputOTPSlot, Label, NotificationDeliveriesPage, NotificationDeliveryDetailDialog, NotificationReminderRulesPage, NotificationReminderRunsPage, NotificationTemplateAuthoringHelp, NotificationTemplateDetailPage, NotificationTemplatesPage, OverviewMetric, RichTextEditor, ScrollArea, ScrollBar, Select, SelectContent, SelectItem, SelectionActionBar, SelectTrigger, SelectValue, Sheet, SheetBody, SheetContent, SheetFooter, SheetHeader, SheetTitle, Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarHeader, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarRail, SidebarTrigger, Switch, Textarea, Toaster, useSidebar, };
86
+ export { Avatar, AvatarFallback, AvatarImage, Badge, Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, Collapsible, CollapsibleContent, CollapsibleTrigger, ConfirmActionButton, ContractTemplateAuthoringHelp, cn, Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Input, InputOTP, InputOTPGroup, InputOTPSlot, Label, NotificationDeliveriesPage, NotificationDeliveryDetailDialog, NotificationReminderRulesPage, NotificationReminderRunsPage, NotificationTemplateAuthoringHelp, NotificationTemplateDetailPage, NotificationTemplatesPage, OverviewMetric, RadioGroup, RadioGroupItem, RichTextEditor, ScrollArea, ScrollBar, Select, SelectContent, SelectItem, SelectionActionBar, SelectTrigger, SelectValue, Sheet, SheetBody, SheetContent, SheetFooter, SheetHeader, SheetTitle, Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarHeader, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarRail, SidebarTrigger, Switch, Textarea, Toaster, useSidebar, };
@@ -1 +1 @@
1
- {"version":3,"file":"notification-deliveries-page.d.ts","sourceRoot":"","sources":["../../src/components/notification-deliveries-page.tsx"],"names":[],"mappings":"AAYA,wBAAgB,0BAA0B,4CA2GzC"}
1
+ {"version":3,"file":"notification-deliveries-page.d.ts","sourceRoot":"","sources":["../../src/components/notification-deliveries-page.tsx"],"names":[],"mappings":"AAuBA,wBAAgB,0BAA0B,4CAiLzC"}
@@ -1,22 +1,85 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useNotificationDeliveries, } from "@voyantjs/notifications-react";
4
- import { Loader2 } from "lucide-react";
3
+ import { useNotificationDeliveries, useNotificationDeliveryMutation, } from "@voyantjs/notifications-react";
4
+ import { Loader2, RotateCcw, Search } from "lucide-react";
5
5
  import { useState } from "react";
6
6
  import { Badge } from "./badge";
7
+ import { Button } from "./button";
8
+ import { Dialog, DialogBody, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "./index";
7
9
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";
8
10
  export function NotificationDeliveriesPage() {
9
11
  const [channel, setChannel] = useState("all");
10
12
  const [status, setStatus] = useState("all");
13
+ const [selectedDelivery, setSelectedDelivery] = useState(null);
14
+ const deliveryMutation = useNotificationDeliveryMutation();
15
+ const selectedDeliveryId = selectedDelivery?.id;
11
16
  const { data, isPending } = useNotificationDeliveries({
12
17
  channel: channel === "all" ? undefined : channel,
13
18
  status: status === "all" ? undefined : status,
14
19
  limit: 50,
15
20
  offset: 0,
16
21
  });
17
- 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: "Deliveries" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Review notification delivery attempts, rendered payloads, and provider-level outcomes." })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs(Select, { value: channel, onValueChange: (value) => setChannel(value ?? "all"), children: [_jsx(SelectTrigger, { className: "w-[140px]", children: _jsx(SelectValue, { placeholder: "Channel" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "All channels" }), _jsx(SelectItem, { value: "email", children: "Email" }), _jsx(SelectItem, { value: "sms", children: "SMS" })] })] }), _jsxs(Select, { value: status, onValueChange: (value) => setStatus(value ?? "all"), children: [_jsx(SelectTrigger, { className: "w-[140px]", children: _jsx(SelectValue, { placeholder: "Status" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "All statuses" }), _jsx(SelectItem, { value: "pending", children: "Pending" }), _jsx(SelectItem, { value: "sent", children: "Sent" }), _jsx(SelectItem, { value: "failed", children: "Failed" }), _jsx(SelectItem, { value: "cancelled", children: "Cancelled" })] })] })] }), 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: "No deliveries yet." }) })) : 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: "To" }), _jsx("th", { className: "px-4 py-3", children: "Template" }), _jsx("th", { className: "px-4 py-3", children: "Channel" }), _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("tbody", { children: 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 font-mono text-xs", children: delivery.templateSlug ?? "direct" }), _jsx("td", { className: "px-4 py-3", children: _jsx(Badge, { variant: "outline", children: delivery.channel }) }), _jsx("td", { className: "px-4 py-3", children: delivery.provider }), _jsx("td", { className: "px-4 py-3", children: _jsx(Badge, { variant: delivery.status === "sent"
18
- ? "default"
19
- : delivery.status === "failed"
20
- ? "destructive"
21
- : "secondary", children: delivery.status }) }), _jsx("td", { className: "px-4 py-3", children: new Date(delivery.createdAt).toLocaleString() })] }, delivery.id))) })] }) })) : null] }));
22
+ 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: "Deliveries" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Review notification delivery attempts, rendered payloads, and provider-level outcomes." })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs(Select, { value: channel, onValueChange: (value) => setChannel(value ?? "all"), children: [_jsx(SelectTrigger, { className: "w-[140px]", children: _jsx(SelectValue, { placeholder: "Channel" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "All channels" }), _jsx(SelectItem, { value: "email", children: "Email" }), _jsx(SelectItem, { value: "sms", children: "SMS" })] })] }), _jsxs(Select, { value: status, onValueChange: (value) => setStatus(value ?? "all"), children: [_jsx(SelectTrigger, { className: "w-[140px]", children: _jsx(SelectValue, { placeholder: "Status" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "All statuses" }), _jsx(SelectItem, { value: "pending", children: "Pending" }), _jsx(SelectItem, { value: "sent", children: "Sent" }), _jsx(SelectItem, { value: "failed", children: "Failed" }), _jsx(SelectItem, { value: "cancelled", children: "Cancelled" })] })] })] }), 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: "No deliveries yet." }) })) : 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: "To" }), _jsx("th", { className: "px-4 py-3", children: "Template" }), _jsx("th", { className: "px-4 py-3", children: "Channel" }), _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: "Logs" })] }) }), _jsx("tbody", { children: 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 font-mono text-xs", children: delivery.templateSlug ?? "direct" }), _jsx("td", { className: "px-4 py-3", children: _jsx(Badge, { variant: "outline", children: delivery.channel }) }), _jsx("td", { className: "px-4 py-3", children: delivery.provider }), _jsxs("td", { className: "px-4 py-3", children: [_jsx(Badge, { variant: delivery.status === "sent"
23
+ ? "default"
24
+ : delivery.status === "failed"
25
+ ? "destructive"
26
+ : "secondary", children: delivery.status }), delivery.status === "failed" && delivery.errorMessage ? (_jsx("div", { className: "mt-1 max-w-[280px] truncate text-destructive text-xs", children: delivery.errorMessage })) : null] }), _jsx("td", { className: "px-4 py-3", children: new Date(delivery.createdAt).toLocaleString() }), _jsx("td", { className: "px-4 py-3 text-right", children: _jsxs("div", { className: "flex justify-end gap-1", children: [delivery.status === "failed" ? (_jsxs(Button, { type: "button", variant: "ghost", size: "sm", disabled: deliveryMutation.resend.isPending, onClick: () => {
27
+ deliveryMutation.resend.mutate(delivery.id, {
28
+ onError(error) {
29
+ window.alert(error instanceof Error
30
+ ? error.message
31
+ : "Notification resend failed");
32
+ },
33
+ });
34
+ }, children: [deliveryMutation.resend.isPending ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(RotateCcw, { className: "mr-2 h-4 w-4" })), "Resend"] })) : null, _jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setSelectedDelivery(delivery), children: [_jsx(Search, { className: "mr-2 h-4 w-4" }), "Details"] })] }) })] }, delivery.id))) })] }) })) : null, _jsx(DeliveryDetailsDialog, { delivery: selectedDelivery, open: Boolean(selectedDelivery), onOpenChange: (open) => {
35
+ if (!open)
36
+ setSelectedDelivery(null);
37
+ }, onResend: selectedDelivery?.status === "failed" && selectedDeliveryId
38
+ ? () => {
39
+ deliveryMutation.resend.mutate(selectedDeliveryId, {
40
+ onError(error) {
41
+ window.alert(error instanceof Error ? error.message : "Notification resend failed");
42
+ },
43
+ });
44
+ }
45
+ : undefined, isResending: deliveryMutation.resend.isPending })] }));
46
+ }
47
+ function DeliveryDetailsDialog({ delivery, open, onOpenChange, onResend, isResending = false, }) {
48
+ if (!delivery)
49
+ return null;
50
+ const failureLog = readRecord(delivery.metadata?.failureLog);
51
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "xl", children: [_jsx(DialogHeader, { children: _jsxs("div", { className: "flex items-start justify-between gap-4 pr-8", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(DialogTitle, { children: "Delivery details" }), _jsx(DialogDescription, { children: "Provider response, failure log, rendered content, and payload for this notification." })] }), onResend ? (_jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: isResending, onClick: onResend, children: [isResending ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(RotateCcw, { className: "mr-2 h-4 w-4" })), "Resend"] })) : null] }) }), _jsxs(DialogBody, { className: "space-y-5", children: [_jsxs("section", { className: "grid gap-3 sm:grid-cols-2", children: [_jsx(Detail, { label: "Delivery ID", value: delivery.id, mono: true }), _jsx(Detail, { label: "Status", value: delivery.status }), _jsx(Detail, { label: "Provider", value: delivery.provider }), _jsx(Detail, { label: "Provider message ID", value: delivery.providerMessageId ?? "—", mono: true }), _jsx(Detail, { label: "Template", value: delivery.templateSlug ?? "direct", mono: true }), _jsx(Detail, { label: "Channel", value: delivery.channel }), _jsx(Detail, { label: "Created", value: formatDateTime(delivery.createdAt) }), _jsx(Detail, { label: "Failed", value: formatDateTime(delivery.failedAt) }), _jsx(Detail, { label: "Sent", value: formatDateTime(delivery.sentAt) }), _jsx(Detail, { label: "Scheduled", value: formatDateTime(delivery.scheduledFor) })] }), delivery.errorMessage ? (_jsx(LogSection, { title: "Error message", tone: "destructive", children: delivery.errorMessage })) : null, failureLog ? (_jsx(JsonSection, { title: "Failure log", value: failureLog })) : delivery.status === "failed" ? (_jsx(LogSection, { title: "Failure log", children: "No structured failure log was captured." })) : null, _jsxs("section", { className: "grid gap-3 sm:grid-cols-2", children: [_jsx(Detail, { label: "To", value: delivery.toAddress }), _jsx(Detail, { label: "From", value: delivery.fromAddress ?? "—" }), _jsx(Detail, { label: "Subject", value: delivery.subject ?? "—" }), _jsx(Detail, { label: "Target", value: formatTarget(delivery), mono: true })] }), _jsx(JsonSection, { title: "Payload data", value: delivery.payloadData }), _jsx(JsonSection, { title: "Metadata", value: delivery.metadata }), _jsx(BodySection, { title: "Text body", value: delivery.textBody }), _jsx(BodySection, { title: "HTML body", value: delivery.htmlBody })] })] }) }));
52
+ }
53
+ function Detail({ label, value, mono = false }) {
54
+ return (_jsxs("div", { className: "rounded-md border bg-muted/20 p-3", children: [_jsx("div", { className: "text-muted-foreground text-xs uppercase tracking-wide", children: label }), _jsx("div", { className: `mt-1 break-words text-sm ${mono ? "font-mono" : ""}`, children: value || "—" })] }));
55
+ }
56
+ function LogSection({ title, children, tone, }) {
57
+ return (_jsxs("section", { className: "space-y-2", children: [_jsx("h2", { className: "font-medium text-sm", children: title }), _jsx("pre", { className: `max-h-56 overflow-auto rounded-md border p-3 text-xs ${tone === "destructive" ? "border-destructive/30 bg-destructive/10" : "bg-muted/30"}`, children: children })] }));
58
+ }
59
+ function JsonSection({ title, value }) {
60
+ if (!value)
61
+ return null;
62
+ return _jsx(LogSection, { title: title, children: JSON.stringify(value, null, 2) });
63
+ }
64
+ function BodySection({ title, value }) {
65
+ if (!value)
66
+ return null;
67
+ return _jsx(LogSection, { title: title, children: value });
68
+ }
69
+ function readRecord(value) {
70
+ return value && typeof value === "object" && !Array.isArray(value)
71
+ ? value
72
+ : null;
73
+ }
74
+ function formatDateTime(value) {
75
+ return value ? new Date(value).toLocaleString() : "—";
76
+ }
77
+ function formatTarget(delivery) {
78
+ const targetId = delivery.bookingId ??
79
+ delivery.invoiceId ??
80
+ delivery.paymentSessionId ??
81
+ delivery.personId ??
82
+ delivery.organizationId ??
83
+ delivery.targetId;
84
+ return targetId ? `${delivery.targetType}:${targetId}` : delivery.targetType;
22
85
  }
@@ -1 +1 @@
1
- {"version":3,"file":"notification-reminder-rule-dialog.d.ts","sourceRoot":"","sources":["../../src/components/notification-reminder-rule-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,8BAA8B,EAGpC,MAAM,+BAA+B,CAAA;AA6BtC,KAAK,mCAAmC,GAAG;IACzC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,IAAI,CAAC,EAAE,8BAA8B,CAAA;IACrC,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB,CAAA;AAED,wBAAgB,8BAA8B,CAAC,EAC7C,IAAI,EACJ,YAAY,EACZ,IAAI,EACJ,SAAS,GACV,EAAE,mCAAmC,2CA8NrC"}
1
+ {"version":3,"file":"notification-reminder-rule-dialog.d.ts","sourceRoot":"","sources":["../../src/components/notification-reminder-rule-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,8BAA8B,EAGpC,MAAM,+BAA+B,CAAA;AAgDtC,KAAK,mCAAmC,GAAG;IACzC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,IAAI,CAAC,EAAE,8BAA8B,CAAA;IACrC,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB,CAAA;AAED,wBAAgB,8BAA8B,CAAC,EAC7C,IAAI,EACJ,YAAY,EACZ,IAAI,EACJ,SAAS,GACV,EAAE,mCAAmC,2CAiNrC"}
@@ -13,16 +13,32 @@ import { Label } from "./label";
13
13
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";
14
14
  const reminderRuleFormSchema = z.object({
15
15
  name: z.string().min(1, "Name is required"),
16
- slug: z
17
- .string()
18
- .min(1, "Slug is required")
19
- .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Must be kebab-case"),
20
16
  status: z.enum(["draft", "active", "archived"]).default("draft"),
21
- targetType: z.enum(["booking_payment_schedule", "invoice"]),
17
+ targetType: z.enum([
18
+ "booking_confirmed",
19
+ "booking_payment_schedule",
20
+ "payment_complete",
21
+ "booking_cancelled_non_payment",
22
+ ]),
22
23
  channel: z.enum(["email", "sms"]),
23
24
  templateId: z.string().min(1, "Template is required"),
24
25
  relativeDaysFromDueDate: z.number().int().min(-365).max(365),
25
26
  });
27
+ const reminderTargetOptions = [
28
+ { value: "booking_confirmed", label: "Booking confirmed" },
29
+ { value: "payment_complete", label: "Payment complete" },
30
+ { value: "booking_cancelled_non_payment", label: "Booking cancelled (non-payment)" },
31
+ { value: "booking_payment_schedule", label: "Booking payment schedule" },
32
+ ];
33
+ const dueDateTargetTypes = new Set(["booking_payment_schedule"]);
34
+ function slugifyReminderRule(value) {
35
+ const slug = value
36
+ .trim()
37
+ .toLowerCase()
38
+ .replace(/[^a-z0-9]+/g, "-")
39
+ .replace(/^-+|-+$/g, "");
40
+ return slug || "notification-rule";
41
+ }
26
42
  export function NotificationReminderRuleDialog({ open, onOpenChange, rule, onSuccess, }) {
27
43
  const isEditing = Boolean(rule);
28
44
  const { create, update } = useNotificationReminderRuleMutation();
@@ -30,7 +46,6 @@ export function NotificationReminderRuleDialog({ open, onOpenChange, rule, onSuc
30
46
  resolver: zodResolver(reminderRuleFormSchema),
31
47
  defaultValues: {
32
48
  name: "",
33
- slug: "",
34
49
  status: "draft",
35
50
  targetType: "booking_payment_schedule",
36
51
  channel: "email",
@@ -39,6 +54,8 @@ export function NotificationReminderRuleDialog({ open, onOpenChange, rule, onSuc
39
54
  },
40
55
  });
41
56
  const channel = form.watch("channel");
57
+ const targetType = form.watch("targetType");
58
+ const usesDueDateTiming = dueDateTargetTypes.has(targetType);
42
59
  const { data: templates } = useNotificationTemplates({
43
60
  channel,
44
61
  status: "active",
@@ -54,9 +71,8 @@ export function NotificationReminderRuleDialog({ open, onOpenChange, rule, onSuc
54
71
  : "");
55
72
  form.reset({
56
73
  name: rule.name,
57
- slug: rule.slug,
58
74
  status: rule.status,
59
- targetType: rule.targetType,
75
+ targetType: rule.targetType === "invoice" ? "booking_payment_schedule" : rule.targetType,
60
76
  channel: rule.channel,
61
77
  templateId: resolvedTemplateId,
62
78
  relativeDaysFromDueDate: rule.relativeDaysFromDueDate,
@@ -70,14 +86,17 @@ export function NotificationReminderRuleDialog({ open, onOpenChange, rule, onSuc
70
86
  const onSubmit = async (values) => {
71
87
  const payload = {
72
88
  name: values.name,
73
- slug: values.slug,
89
+ slug: rule?.slug ??
90
+ `${slugifyReminderRule(values.targetType)}-${slugifyReminderRule(values.name)}`,
74
91
  status: values.status,
75
92
  targetType: values.targetType,
76
93
  channel: values.channel,
77
94
  provider: null,
78
95
  templateId: values.templateId,
79
96
  templateSlug: null,
80
- relativeDaysFromDueDate: values.relativeDaysFromDueDate,
97
+ relativeDaysFromDueDate: dueDateTargetTypes.has(values.targetType)
98
+ ? values.relativeDaysFromDueDate
99
+ : 0,
81
100
  isSystem: rule?.isSystem ?? false,
82
101
  metadata: rule?.metadata ?? null,
83
102
  };
@@ -90,32 +109,19 @@ export function NotificationReminderRuleDialog({ open, onOpenChange, rule, onSuc
90
109
  onSuccess();
91
110
  };
92
111
  const isPending = create.isPending || update.isPending;
93
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing ? "Edit Reminder Rule" : "New Reminder Rule" }) }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), children: [_jsxs(DialogBody, { className: "grid gap-4", children: [_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Name" }), _jsx(Input, { ...form.register("name"), placeholder: "Invoice due in 3 days" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Slug" }), _jsx(Input, { ...form.register("slug"), placeholder: "invoice-due-minus-3" })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Target" }), _jsxs(Select, { items: [
94
- { label: "Booking payment schedule", value: "booking_payment_schedule" },
95
- { label: "Invoice", value: "invoice" },
96
- ], value: form.watch("targetType"), onValueChange: (value) => {
112
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing ? "Edit Reminder Rule" : "New Reminder Rule" }) }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), children: [_jsxs(DialogBody, { className: "grid gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Name" }), _jsx(Input, { ...form.register("name"), placeholder: "Payment due in 3 days" })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Target" }), _jsxs(Select, { value: form.watch("targetType"), onValueChange: (value) => {
97
113
  if (!value)
98
114
  return;
99
115
  form.setValue("targetType", value);
100
- }, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "booking_payment_schedule", children: "Booking payment schedule" }), _jsx(SelectItem, { value: "invoice", children: "Invoice" })] })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Status" }), _jsxs(Select, { items: [
101
- { label: "Draft", value: "draft" },
102
- { label: "Active", value: "active" },
103
- { label: "Archived", value: "archived" },
104
- ], value: form.watch("status"), onValueChange: (value) => {
116
+ }, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: reminderTargetOptions.map((option) => (_jsx(SelectItem, { value: option.value, children: option.label }, option.value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Status" }), _jsxs(Select, { value: form.watch("status"), onValueChange: (value) => {
105
117
  if (!value)
106
118
  return;
107
119
  form.setValue("status", value);
108
- }, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "draft", children: "Draft" }), _jsx(SelectItem, { value: "active", children: "Active" }), _jsx(SelectItem, { value: "archived", children: "Archived" })] })] })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Channel" }), _jsxs(Select, { items: [
109
- { label: "Email", value: "email" },
110
- { label: "SMS", value: "sms" },
111
- ], value: form.watch("channel"), onValueChange: (value) => {
120
+ }, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "draft", children: "Draft" }), _jsx(SelectItem, { value: "active", children: "Active" }), _jsx(SelectItem, { value: "archived", children: "Archived" })] })] })] })] }), _jsxs("div", { className: usesDueDateTiming ? "grid grid-cols-2 gap-4" : "grid gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Channel" }), _jsxs(Select, { value: form.watch("channel"), onValueChange: (value) => {
112
121
  if (!value)
113
122
  return;
114
123
  form.setValue("channel", value);
115
- }, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "email", children: "Email" }), _jsx(SelectItem, { value: "sms", children: "SMS" })] })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Offset days" }), _jsx(Input, { type: "number", value: form.watch("relativeDaysFromDueDate"), onChange: (event) => form.setValue("relativeDaysFromDueDate", Number.parseInt(event.target.value || "0", 10)) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Template" }), _jsxs(Select, { items: (templates?.data ?? []).map((template) => ({
116
- label: `${template.name} (${template.slug})`,
117
- value: template.id,
118
- })), value: form.watch("templateId"), onValueChange: (value) => {
124
+ }, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "email", children: "Email" }), _jsx(SelectItem, { value: "sms", children: "SMS" })] })] })] }), usesDueDateTiming ? (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Send timing" }), _jsx(Input, { type: "number", value: form.watch("relativeDaysFromDueDate"), onChange: (event) => form.setValue("relativeDaysFromDueDate", Number.parseInt(event.target.value || "0", 10)) }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Days from due date: -3 sends 3 days before, 0 on the due date, 3 after." })] })) : null] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Template" }), _jsxs(Select, { value: form.watch("templateId"), onValueChange: (value) => {
119
125
  if (!value)
120
126
  return;
121
127
  form.setValue("templateId", value);
@@ -1 +1 @@
1
- {"version":3,"file":"notification-reminder-rules-page.d.ts","sourceRoot":"","sources":["../../src/components/notification-reminder-rules-page.tsx"],"names":[],"mappings":"AAgBA,wBAAgB,6BAA6B,4CA0J5C"}
1
+ {"version":3,"file":"notification-reminder-rules-page.d.ts","sourceRoot":"","sources":["../../src/components/notification-reminder-rules-page.tsx"],"names":[],"mappings":"AAyCA,wBAAgB,6BAA6B,4CA+J5C"}
@@ -8,6 +8,27 @@ import { Button } from "./button";
8
8
  import { Input } from "./input";
9
9
  import { NotificationReminderRuleDialog } from "./notification-reminder-rule-dialog";
10
10
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";
11
+ const reminderTargetLabels = {
12
+ booking_confirmed: "Booking confirmed",
13
+ booking_payment_schedule: "Booking payment schedule",
14
+ payment_complete: "Payment complete",
15
+ booking_cancelled_non_payment: "Booking cancelled (non-payment)",
16
+ };
17
+ const dueDateTargetTypes = new Set([
18
+ "booking_payment_schedule",
19
+ ]);
20
+ function getReminderTargetLabel(targetType) {
21
+ if (targetType === "invoice")
22
+ return "Invoice";
23
+ return reminderTargetLabels[targetType] ?? targetType;
24
+ }
25
+ function formatReminderTiming(targetType, days) {
26
+ if (!dueDateTargetTypes.has(targetType))
27
+ return "Event";
28
+ if (days === 0)
29
+ return "Due date";
30
+ return days < 0 ? `${Math.abs(days)} days before` : `${days} days after`;
31
+ }
11
32
  export function NotificationReminderRulesPage() {
12
33
  const [search, setSearch] = useState("");
13
34
  const [channel, setChannel] = useState("all");
@@ -21,10 +42,10 @@ export function NotificationReminderRulesPage() {
21
42
  status: status === "all" ? undefined : status,
22
43
  targetType: targetType === "all" ? undefined : targetType,
23
44
  });
24
- 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: "Reminder Rules" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Schedule invoice and payment reminders against templates and channels." })] }), _jsxs(Button, { onClick: () => {
45
+ 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: "Reminder Rules" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Schedule booking payment reminders and event notifications against templates and channels." })] }), _jsxs(Button, { onClick: () => {
25
46
  setEditing(undefined);
26
47
  setDialogOpen(true);
27
- }, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "New Rule"] })] }), _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: "Search rules...", 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: "Target" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "All targets" }), _jsx(SelectItem, { value: "booking_payment_schedule", children: "Booking payment schedule" }), _jsx(SelectItem, { value: "invoice", children: "Invoice" })] })] }), _jsxs(Select, { value: channel, onValueChange: (value) => setChannel(value ?? "all"), children: [_jsx(SelectTrigger, { className: "w-[140px]", children: _jsx(SelectValue, { placeholder: "Channel" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "All channels" }), _jsx(SelectItem, { value: "email", children: "Email" }), _jsx(SelectItem, { value: "sms", children: "SMS" })] })] }), _jsxs(Select, { value: status, onValueChange: (value) => setStatus(value ?? "all"), children: [_jsx(SelectTrigger, { className: "w-[140px]", children: _jsx(SelectValue, { placeholder: "Status" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "All statuses" }), _jsx(SelectItem, { value: "draft", children: "Draft" }), _jsx(SelectItem, { value: "active", children: "Active" }), _jsx(SelectItem, { value: "archived", children: "Archived" })] })] })] }), 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: "No reminder rules yet." }) })) : 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: "Offset" }), _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: [_jsxs("td", { className: "px-4 py-3", children: [_jsx("div", { className: "font-medium", children: rule.name }), _jsx("div", { className: "font-mono text-xs text-muted-foreground", children: rule.slug })] }), _jsx("td", { className: "px-4 py-3", children: rule.targetType }), _jsx("td", { className: "px-4 py-3", children: _jsx(Badge, { variant: "outline", children: rule.channel }) }), _jsxs("td", { className: "px-4 py-3", children: [rule.relativeDaysFromDueDate, " days"] }), _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: _jsx(Button, { variant: "ghost", size: "sm", onClick: () => {
48
+ }, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "New Rule"] })] }), _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: "Search rules...", 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: "Target" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "All targets" }), Object.entries(reminderTargetLabels).map(([value, label]) => (_jsx(SelectItem, { value: value, children: label }, value)))] })] }), _jsxs(Select, { value: channel, onValueChange: (value) => setChannel(value ?? "all"), children: [_jsx(SelectTrigger, { className: "w-[140px]", children: _jsx(SelectValue, { placeholder: "Channel" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "All channels" }), _jsx(SelectItem, { value: "email", children: "Email" }), _jsx(SelectItem, { value: "sms", children: "SMS" })] })] }), _jsxs(Select, { value: status, onValueChange: (value) => setStatus(value ?? "all"), children: [_jsx(SelectTrigger, { className: "w-[140px]", children: _jsx(SelectValue, { placeholder: "Status" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "All statuses" }), _jsx(SelectItem, { value: "draft", children: "Draft" }), _jsx(SelectItem, { value: "active", children: "Active" }), _jsx(SelectItem, { value: "archived", children: "Archived" })] })] })] }), 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: "No reminder rules yet." }) })) : 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: "Timing" }), _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(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: formatReminderTiming(rule.targetType, rule.relativeDaysFromDueDate) }), _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: _jsx(Button, { variant: "ghost", size: "sm", onClick: () => {
28
49
  setEditing(rule);
29
50
  setDialogOpen(true);
30
51
  }, children: _jsx(Pencil, { className: "h-4 w-4" }) }) })] }, rule.id))) })] }) })) : null, _jsx(NotificationReminderRuleDialog, { open: dialogOpen, onOpenChange: setDialogOpen, rule: editing, onSuccess: () => {
@@ -5,6 +5,6 @@ type NotificationTemplateDialogProps = {
5
5
  template?: NotificationTemplateRecord;
6
6
  onSuccess: () => void;
7
7
  };
8
- export declare function NotificationTemplateDialog({ open, onOpenChange, template, onSuccess, }: NotificationTemplateDialogProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare function NotificationTemplateDialog(props: NotificationTemplateDialogProps): import("react/jsx-runtime").JSX.Element | null;
9
9
  export {};
10
10
  //# sourceMappingURL=notification-template-dialog.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"notification-template-dialog.d.ts","sourceRoot":"","sources":["../../src/components/notification-template-dialog.tsx"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,0BAA0B,EAIhC,MAAM,+BAA+B,CAAA;AAkDtC,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;AAuED,wBAAgB,0BAA0B,CAAC,EACzC,IAAI,EACJ,YAAY,EACZ,QAAQ,EACR,SAAS,GACV,EAAE,+BAA+B,2CAghBjC"}
1
+ {"version":3,"file":"notification-template-dialog.d.ts","sourceRoot":"","sources":["../../src/components/notification-template-dialog.tsx"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,0BAA0B,EAIhC,MAAM,+BAA+B,CAAA;AA+DtC,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"}
@@ -2,12 +2,13 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useNotificationTemplateAuthoring, useNotificationTemplateMutation, useNotificationTemplateTools, } from "@voyantjs/notifications-react";
4
4
  import { Loader2 } from "lucide-react";
5
- import { useEffect, useMemo, useState } from "react";
5
+ import { useEffect, useMemo, useRef, useState } from "react";
6
6
  import { useForm } from "react-hook-form";
7
7
  import { toast } from "sonner";
8
8
  import { z } from "zod/v4";
9
9
  import { zodResolver } from "../lib/zod-resolver";
10
10
  import { Button } from "./button";
11
+ import { Checkbox } from "./checkbox";
11
12
  import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "./index";
12
13
  import { Input } from "./input";
13
14
  import { Label } from "./label";
@@ -15,7 +16,6 @@ import { NotificationTemplateAuthoringHelp } from "./notification-template-autho
15
16
  import { RichTextEditor } from "./rich-text-editor";
16
17
  import { insertPlainText, insertVariableToken } from "./rich-text-variable-extension";
17
18
  import { ScrollArea } from "./scroll-area";
18
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";
19
19
  import { Switch } from "./switch";
20
20
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs";
21
21
  import { Textarea } from "./textarea";
@@ -28,6 +28,13 @@ const STATUS_ITEMS = [
28
28
  { label: "Active", value: "active" },
29
29
  { label: "Archived", value: "archived" },
30
30
  ];
31
+ const ATTACHMENT_ITEMS = [
32
+ { label: "Contract", value: "contract" },
33
+ { label: "Invoice", value: "invoice" },
34
+ { label: "Brochure", value: "brochure" },
35
+ ];
36
+ const nativeSelectClassName = "h-9 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-input/30";
37
+ const templateAttachmentSchema = z.enum(["contract", "invoice", "brochure"]);
31
38
  const templateFormSchema = z.object({
32
39
  name: z.string().min(1, "Name is required"),
33
40
  slug: z
@@ -40,6 +47,7 @@ const templateFormSchema = z.object({
40
47
  htmlTemplate: z.string().optional(),
41
48
  textTemplate: z.string().optional(),
42
49
  fromAddress: z.string().optional(),
50
+ attachments: z.array(templateAttachmentSchema).default([]),
43
51
  active: z.boolean(),
44
52
  });
45
53
  function parsePath(path) {
@@ -100,10 +108,44 @@ function appendTemplateValue(current, addition) {
100
108
  function variableReference(key) {
101
109
  return `{{ ${key} }}`;
102
110
  }
103
- export function NotificationTemplateDialog({ open, onOpenChange, template, onSuccess, }) {
111
+ function getMetadataRecord(value) {
112
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
113
+ return null;
114
+ }
115
+ return value;
116
+ }
117
+ function readTemplateAttachments(metadata) {
118
+ const record = getMetadataRecord(metadata);
119
+ const value = record?.attachments;
120
+ if (!Array.isArray(value)) {
121
+ return [];
122
+ }
123
+ const allowed = new Set(ATTACHMENT_ITEMS.map((item) => item.value));
124
+ return ATTACHMENT_ITEMS.map((item) => item.value).filter((attachment) => allowed.has(attachment) && value.includes(attachment));
125
+ }
126
+ function buildTemplateMetadata(metadata, attachments) {
127
+ const current = getMetadataRecord(metadata);
128
+ const next = current ? { ...current } : {};
129
+ if (attachments.length > 0) {
130
+ next.attachments = [...attachments];
131
+ }
132
+ else {
133
+ delete next.attachments;
134
+ }
135
+ return Object.keys(next).length > 0 ? next : null;
136
+ }
137
+ export function NotificationTemplateDialog(props) {
138
+ if (!props.open) {
139
+ return null;
140
+ }
141
+ return _jsx(NotificationTemplateDialogInner, { ...props });
142
+ }
143
+ function NotificationTemplateDialogInner({ open, onOpenChange, template, onSuccess, }) {
104
144
  const isEditing = Boolean(template);
105
145
  const { create, update } = useNotificationTemplateMutation();
106
146
  const { preview, testSend } = useNotificationTemplateTools();
147
+ const previewResetRef = useRef(preview.reset);
148
+ const testSendResetRef = useRef(testSend.reset);
107
149
  const { variableCatalog, liquidSnippets } = useNotificationTemplateAuthoring();
108
150
  const [editorInstance, setEditorInstance] = useState(null);
109
151
  const [insertionTarget, setInsertionTarget] = useState("body");
@@ -128,10 +170,14 @@ export function NotificationTemplateDialog({ open, onOpenChange, template, onSuc
128
170
  htmlTemplate: "",
129
171
  textTemplate: "",
130
172
  fromAddress: "",
173
+ attachments: [],
131
174
  active: true,
132
175
  },
133
176
  });
134
177
  const channel = form.watch("channel");
178
+ const attachments = form.watch("attachments") ?? [];
179
+ previewResetRef.current = preview.reset;
180
+ testSendResetRef.current = testSend.reset;
135
181
  useEffect(() => {
136
182
  if (open && template) {
137
183
  form.reset({
@@ -143,6 +189,7 @@ export function NotificationTemplateDialog({ open, onOpenChange, template, onSuc
143
189
  htmlTemplate: template.htmlTemplate ?? "",
144
190
  textTemplate: template.textTemplate ?? "",
145
191
  fromAddress: template.fromAddress ?? "",
192
+ attachments: template.channel === "email" ? readTemplateAttachments(template.metadata) : [],
146
193
  active: template.status === "active",
147
194
  });
148
195
  return;
@@ -154,16 +201,28 @@ export function NotificationTemplateDialog({ open, onOpenChange, template, onSuc
154
201
  useEffect(() => {
155
202
  if (!open)
156
203
  return;
157
- setInsertionTarget((current) => channel === "sms" ? "text" : current === "text" ? "body" : current);
204
+ setInsertionTarget((current) => {
205
+ const next = channel === "sms" ? "text" : current === "text" ? "body" : current;
206
+ return next === current ? current : next;
207
+ });
158
208
  }, [channel, open]);
209
+ useEffect(() => {
210
+ if (!open || channel === "email" || (form.getValues("attachments") ?? []).length === 0)
211
+ return;
212
+ form.setValue("attachments", [], {
213
+ shouldDirty: true,
214
+ shouldTouch: true,
215
+ shouldValidate: true,
216
+ });
217
+ }, [channel, form, open]);
159
218
  useEffect(() => {
160
219
  if (!open)
161
220
  return;
162
221
  setPreviewDataInput(defaultPreviewData);
163
222
  setTestRecipient("");
164
- preview.reset();
165
- testSend.reset();
166
- }, [defaultPreviewData, open, preview, testSend]);
223
+ previewResetRef.current();
224
+ testSendResetRef.current();
225
+ }, [defaultPreviewData, open]);
167
226
  const onSubmit = async (values) => {
168
227
  const payload = {
169
228
  name: values.name,
@@ -173,10 +232,12 @@ export function NotificationTemplateDialog({ open, onOpenChange, template, onSuc
173
232
  status: values.active ? (values.status === "archived" ? "active" : values.status) : "draft",
174
233
  subjectTemplate: values.channel === "email" ? values.subjectTemplate || null : null,
175
234
  htmlTemplate: values.channel === "email" ? values.htmlTemplate || null : null,
176
- textTemplate: values.textTemplate || null,
235
+ textTemplate: values.channel === "sms" ? values.textTemplate || null : null,
177
236
  fromAddress: values.channel === "email" ? values.fromAddress || null : null,
178
237
  isSystem: template?.isSystem ?? false,
179
- metadata: template?.metadata ?? null,
238
+ metadata: values.channel === "email"
239
+ ? buildTemplateMetadata(template?.metadata, values.attachments)
240
+ : buildTemplateMetadata(template?.metadata, []),
180
241
  };
181
242
  if (isEditing && template) {
182
243
  await update.mutateAsync({ id: template.id, input: payload });
@@ -229,7 +290,7 @@ export function NotificationTemplateDialog({ open, onOpenChange, template, onSuc
229
290
  fromAddress: channel === "email" ? form.getValues("fromAddress") || null : null,
230
291
  subjectTemplate: channel === "email" ? form.getValues("subjectTemplate") || null : null,
231
292
  htmlTemplate: channel === "email" ? form.getValues("htmlTemplate") || null : null,
232
- textTemplate: form.getValues("textTemplate") || null,
293
+ textTemplate: channel === "sms" ? form.getValues("textTemplate") || null : null,
233
294
  data,
234
295
  });
235
296
  }
@@ -251,7 +312,7 @@ export function NotificationTemplateDialog({ open, onOpenChange, template, onSuc
251
312
  from: channel === "email" ? form.getValues("fromAddress") || null : null,
252
313
  subject: channel === "email" ? form.getValues("subjectTemplate") || null : null,
253
314
  html: channel === "email" ? form.getValues("htmlTemplate") || null : null,
254
- text: form.getValues("textTemplate") || null,
315
+ text: channel === "sms" ? form.getValues("textTemplate") || null : null,
255
316
  data,
256
317
  targetType: "other",
257
318
  });
@@ -261,36 +322,45 @@ export function NotificationTemplateDialog({ open, onOpenChange, template, onSuc
261
322
  toast.error(error instanceof Error ? error.message : "Test send failed");
262
323
  }
263
324
  };
264
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsx(DialogContent, { size: "xl", className: "h-[calc(100vh-2rem)]", children: _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), className: "grid min-h-0 flex-1 grid-rows-[auto_minmax(0,1fr)_auto] overflow-hidden", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing ? "Edit Notification Template" : "New Notification Template" }) }), _jsx(ScrollArea, { className: "min-h-0 flex-1", children: _jsxs("div", { className: "grid gap-4 py-4 pr-4", children: [_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Name" }), _jsx(Input, { ...form.register("name"), placeholder: "Booking confirmation" }), form.formState.errors.name ? (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.name.message })) : null] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Slug" }), _jsx(Input, { ...form.register("slug"), placeholder: "booking-confirmation" }), form.formState.errors.slug ? (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.slug.message })) : null] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Channel" }), _jsxs(Select, { items: CHANNEL_ITEMS, value: form.watch("channel"), onValueChange: (value) => {
265
- if (!value)
325
+ const setAttachmentSelected = (attachment, checked) => {
326
+ const current = form.getValues("attachments") ?? [];
327
+ const next = checked
328
+ ? [...current, attachment].filter((value, index, values) => values.indexOf(value) === index)
329
+ : current.filter((value) => value !== attachment);
330
+ form.setValue("attachments", next, {
331
+ shouldDirty: true,
332
+ shouldTouch: true,
333
+ shouldValidate: true,
334
+ });
335
+ };
336
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsx(DialogContent, { size: "xl", className: "h-[calc(100vh-2rem)]", children: _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), className: "grid min-h-0 flex-1 grid-rows-[auto_minmax(0,1fr)_auto] overflow-hidden", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing ? "Edit Notification Template" : "New Notification Template" }) }), _jsx(ScrollArea, { className: "min-h-0 flex-1", children: _jsxs("div", { className: "grid gap-4 py-4 pr-4", children: [_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Name" }), _jsx(Input, { ...form.register("name"), placeholder: "Booking confirmation" }), form.formState.errors.name ? (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.name.message })) : null] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Slug" }), _jsx(Input, { ...form.register("slug"), placeholder: "booking-confirmation" }), form.formState.errors.slug ? (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.slug.message })) : null] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Channel" }), _jsx("select", { className: nativeSelectClassName, value: form.watch("channel"), onChange: (event) => {
337
+ const nextChannel = event.target.value;
338
+ if (form.getValues("channel") === nextChannel)
266
339
  return;
267
- form.setValue("channel", value);
268
- }, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: CHANNEL_ITEMS.map((item) => (_jsx(SelectItem, { value: item.value, children: item.label }, item.value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Status" }), _jsxs(Select, { items: STATUS_ITEMS, value: form.watch("status"), onValueChange: (value) => {
269
- if (!value)
340
+ form.setValue("channel", nextChannel, {
341
+ shouldDirty: true,
342
+ shouldTouch: true,
343
+ shouldValidate: true,
344
+ });
345
+ }, children: CHANNEL_ITEMS.map((item) => (_jsx("option", { value: item.value, children: item.label }, item.value))) })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Status" }), _jsx("select", { className: nativeSelectClassName, value: form.watch("status"), onChange: (event) => {
346
+ const nextStatus = event.target.value;
347
+ if (form.getValues("status") === nextStatus)
270
348
  return;
271
- form.setValue("status", value);
272
- }, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: STATUS_ITEMS.map((item) => (_jsx(SelectItem, { value: item.value, children: item.label }, item.value))) })] })] })] }), channel === "email" ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "From address" }), _jsx(Input, { ...form.register("fromAddress"), placeholder: "reservations@example.com" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Subject" }), _jsx(Input, { ...form.register("subjectTemplate"), placeholder: "Your booking {{ booking.reference }}" })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "HTML body" }), _jsx(RichTextEditor, { value: form.watch("htmlTemplate") ?? "", onChange: (value) => form.setValue("htmlTemplate", value, {
349
+ form.setValue("status", nextStatus, {
350
+ shouldDirty: true,
351
+ shouldTouch: true,
352
+ shouldValidate: true,
353
+ });
354
+ }, children: STATUS_ITEMS.map((item) => (_jsx("option", { value: item.value, children: item.label }, item.value))) })] })] }), channel === "email" ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Attachments" }), _jsx("div", { className: "flex flex-wrap gap-3", children: ATTACHMENT_ITEMS.map((item) => (_jsxs("div", { className: "flex h-9 items-center gap-2 rounded-md border px-3 text-sm", children: [_jsx(Checkbox, { id: `notification-template-attachment-${item.value}`, checked: attachments.includes(item.value), onCheckedChange: (checked) => setAttachmentSelected(item.value, checked === true) }), _jsx(Label, { htmlFor: `notification-template-attachment-${item.value}`, className: "cursor-pointer text-sm font-normal", children: item.label })] }, item.value))) })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "From address" }), _jsx(Input, { ...form.register("fromAddress"), placeholder: "reservations@example.com" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Subject" }), _jsx(Input, { ...form.register("subjectTemplate"), placeholder: "Your booking {{ booking.reference }}" })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "HTML body" }), _jsx(RichTextEditor, { value: form.watch("htmlTemplate") ?? "", onChange: (value) => form.setValue("htmlTemplate", value, {
273
355
  shouldDirty: true,
274
356
  shouldTouch: true,
275
357
  shouldValidate: true,
276
- }), placeholder: "Compose the email body using Liquid variables...", enableVariables: true, onEditorReady: setEditorInstance })] })] })) : null, _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: channel === "sms" ? "SMS body" : "Plain-text fallback" }), _jsx(Textarea, { ...form.register("textTemplate"), placeholder: channel === "sms"
277
- ? 'Hi {{ traveler.firstName | default: "traveler" }}, your booking is confirmed.'
278
- : "Optional plain-text version for email clients.", rows: 6, className: "font-mono text-xs" })] }), _jsxs(Tabs, { defaultValue: "authoring", children: [_jsxs(TabsList, { className: "w-full", children: [_jsx(TabsTrigger, { value: "authoring", children: "Authoring" }), _jsx(TabsTrigger, { value: "preview", children: "Preview & Test" })] }), _jsxs(TabsContent, { value: "authoring", className: "mt-4 space-y-4", children: [_jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-[180px_1fr] sm:items-center", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: "Insert into" }), _jsxs(Select, { items: [
279
- ...(channel === "email"
280
- ? [
281
- { label: "Subject", value: "subject" },
282
- { label: "HTML body", value: "body" },
283
- ]
284
- : []),
285
- {
286
- label: channel === "sms" ? "SMS body" : "Plain-text fallback",
287
- value: "text",
288
- },
289
- ], value: insertionTarget, onValueChange: (value) => {
290
- if (!value)
358
+ }), placeholder: "Compose the email body using Liquid variables...", enableVariables: true, onEditorReady: setEditorInstance })] })] })) : null, channel === "sms" ? (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "SMS body" }), _jsx(Textarea, { ...form.register("textTemplate"), placeholder: 'Hi {{ traveler.firstName | default: "traveler" }}, your booking is confirmed.', rows: 6, className: "font-mono text-xs" })] })) : null, _jsxs(Tabs, { defaultValue: "authoring", children: [_jsxs(TabsList, { className: "w-full", children: [_jsx(TabsTrigger, { value: "authoring", children: "Authoring" }), _jsx(TabsTrigger, { value: "preview", children: "Preview & Test" })] }), _jsxs(TabsContent, { value: "authoring", className: "mt-4 space-y-4", children: [_jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-[180px_1fr] sm:items-center", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: "Insert into" }), _jsxs("select", { className: nativeSelectClassName, value: insertionTarget, onChange: (event) => {
359
+ const nextTarget = event.target.value;
360
+ if (nextTarget === insertionTarget)
291
361
  return;
292
- setInsertionTarget(value);
293
- }, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [channel === "email" ? (_jsx(SelectItem, { value: "subject", children: "Subject" })) : null, channel === "email" ? (_jsx(SelectItem, { value: "body", children: "HTML body" })) : null, _jsx(SelectItem, { value: "text", children: channel === "sms" ? "SMS body" : "Plain-text fallback" })] })] })] }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Variables insert as Liquid tags in text fields and as inline chips in the rich-text HTML body." })] }), _jsx(NotificationTemplateAuthoringHelp, { variableGroups: variableGroups, snippets: liquidSnippets, onInsertVariable: (variable) => insertIntoTarget(variable.key, "variable"), onInsertSnippet: (snippet) => insertIntoTarget(snippet.code, "snippet") })] }), _jsx(TabsContent, { value: "preview", className: "mt-4 space-y-4", children: _jsxs("div", { className: "grid gap-4 lg:grid-cols-[minmax(0,1fr)_320px]", children: [_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Preview data (JSON)" }), _jsx(Textarea, { value: previewDataInput, onChange: (event) => setPreviewDataInput(event.target.value), rows: 14, className: "font-mono text-xs", placeholder: '{"booking":{"reference":"BKG-2026-00125"}}' }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Use sample JSON to preview Liquid rendering and send a safe test message." })] }), _jsx("div", { className: "flex gap-2", children: _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, "Refresh Preview"] }) }), _jsxs("div", { className: "space-y-3 rounded-md border p-4", children: [_jsx("div", { className: "text-sm font-medium", children: "Rendered preview" }), channel === "email" ? (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: "Subject" }), _jsx("div", { className: "rounded-md border bg-muted/20 px-3 py-2 text-sm", children: preview.data?.subject || "No subject rendered yet." })] }), _jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: "HTML body" }), _jsx("div", { className: "rounded-md border bg-background", children: preview.data?.html ? (_jsx("div", { className: "prose prose-sm max-w-none px-3 py-3 dark:prose-invert",
362
+ setInsertionTarget(nextTarget);
363
+ }, children: [channel === "email" ? _jsx("option", { value: "subject", children: "Subject" }) : null, channel === "email" ? _jsx("option", { value: "body", children: "HTML body" }) : null, channel === "sms" ? _jsx("option", { value: "text", children: "SMS body" }) : null] })] }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Variables insert as Liquid tags in text fields and as inline chips in the rich-text HTML body." })] }), _jsx(NotificationTemplateAuthoringHelp, { variableGroups: variableGroups, snippets: liquidSnippets, onInsertVariable: (variable) => insertIntoTarget(variable.key, "variable"), onInsertSnippet: (snippet) => insertIntoTarget(snippet.code, "snippet") })] }), _jsx(TabsContent, { value: "preview", className: "mt-4 space-y-4", children: _jsxs("div", { className: "grid gap-4 lg:grid-cols-[minmax(0,1fr)_320px]", children: [_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Preview data (JSON)" }), _jsx(Textarea, { value: previewDataInput, onChange: (event) => setPreviewDataInput(event.target.value), rows: 14, className: "font-mono text-xs", placeholder: '{"booking":{"reference":"BKG-2026-00125"}}' }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Use sample JSON to preview Liquid rendering and send a safe test message." })] }), _jsx("div", { className: "flex gap-2", children: _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, "Refresh Preview"] }) }), _jsxs("div", { className: "space-y-3 rounded-md border p-4", children: [_jsx("div", { className: "text-sm font-medium", children: "Rendered preview" }), channel === "email" ? (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: "Subject" }), _jsx("div", { className: "rounded-md border bg-muted/20 px-3 py-2 text-sm", children: preview.data?.subject || "No subject rendered yet." })] }), _jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: "HTML body" }), _jsx("div", { className: "rounded-md border bg-background", children: preview.data?.html ? (_jsx("div", { className: "prose prose-sm max-w-none px-3 py-3 dark:prose-invert",
294
364
  // biome-ignore lint/security/noDangerouslySetInnerHtml: Preview HTML is generated server-side for template preview.
295
- dangerouslySetInnerHTML: { __html: preview.data.html } })) : (_jsx("div", { className: "px-3 py-3 text-sm text-muted-foreground", children: "No HTML content rendered yet." })) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: "Plain-text fallback" }), _jsx("pre", { className: "whitespace-pre-wrap rounded-md border bg-muted/20 px-3 py-3 text-xs", children: preview.data?.text || "No plain-text content rendered yet." })] })] })) : (_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: "SMS body" }), _jsx("pre", { className: "whitespace-pre-wrap rounded-md border bg-muted/20 px-3 py-3 text-xs", children: preview.data?.text || "No SMS content rendered yet." })] }))] })] }), _jsxs("div", { className: "space-y-4 rounded-md border p-4", children: [_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-sm font-medium", children: "Test send" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Sends the current unsaved content through the configured provider path." })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: channel === "email" ? "Recipient email" : "Recipient phone" }), _jsx(Input, { value: testRecipient, onChange: (event) => setTestRecipient(event.target.value), placeholder: channel === "email" ? "qa@example.com" : "+40 721 111 222" })] }), _jsxs("div", { className: "space-y-1 text-xs text-muted-foreground", children: [_jsx("div", { children: "Provider is selected automatically by the app runtime." }), channel === "email" ? (_jsxs("div", { children: ["From: ", form.watch("fromAddress") || "Default sender"] })) : null] }), _jsxs(Button, { type: "button", className: "w-full", onClick: handleTestSend, disabled: testSend.isPending, children: [testSend.isPending ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : null, "Send Test ", channel === "email" ? "Email" : "SMS"] }), testSend.data ? (_jsxs("div", { className: "rounded-md border border-emerald-500/30 bg-emerald-500/10 px-3 py-2 text-xs text-emerald-200", children: ["Delivery queued with status ", _jsx("strong", { children: testSend.data.status }), testSend.data.provider ? ` via ${testSend.data.provider}` : "", "."] })) : null] })] }) })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Switch, { checked: form.watch("active"), onCheckedChange: (checked) => form.setValue("active", checked) }), _jsx(Label, { className: "cursor-pointer", children: "Mark template active after saving" })] })] }) }), _jsxs(DialogFooter, { className: "mt-0", children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: "Cancel" }), _jsxs(Button, { type: "submit", disabled: isPending, children: [isPending ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : null, isEditing ? "Save Changes" : "Create Template"] })] })] }) }) }));
365
+ dangerouslySetInnerHTML: { __html: preview.data.html } })) : (_jsx("div", { className: "px-3 py-3 text-sm text-muted-foreground", children: "No HTML content rendered yet." })) })] })] })) : (_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: "SMS body" }), _jsx("pre", { className: "whitespace-pre-wrap rounded-md border bg-muted/20 px-3 py-3 text-xs", children: preview.data?.text || "No SMS content rendered yet." })] }))] })] }), _jsxs("div", { className: "space-y-4 rounded-md border p-4", children: [_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-sm font-medium", children: "Test send" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Sends the current unsaved content through the configured provider path." })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: channel === "email" ? "Recipient email" : "Recipient phone" }), _jsx(Input, { value: testRecipient, onChange: (event) => setTestRecipient(event.target.value), placeholder: channel === "email" ? "qa@example.com" : "+40 721 111 222" })] }), _jsxs("div", { className: "space-y-1 text-xs text-muted-foreground", children: [_jsx("div", { children: "Provider is selected automatically by the app runtime." }), channel === "email" ? (_jsxs("div", { children: ["From: ", form.watch("fromAddress") || "Default sender"] })) : null] }), _jsxs(Button, { type: "button", className: "w-full", onClick: handleTestSend, disabled: testSend.isPending, children: [testSend.isPending ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : null, "Send Test ", channel === "email" ? "Email" : "SMS"] }), testSend.data ? (_jsxs("div", { className: "rounded-md border border-emerald-500/30 bg-emerald-500/10 px-3 py-2 text-xs text-emerald-200", children: ["Delivery queued with status ", _jsx("strong", { children: testSend.data.status }), testSend.data.provider ? ` via ${testSend.data.provider}` : "", "."] })) : null] })] }) })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Switch, { checked: form.watch("active"), onCheckedChange: (checked) => form.setValue("active", checked) }), _jsx(Label, { className: "cursor-pointer", children: "Mark template active after saving" })] })] }) }), _jsxs(DialogFooter, { className: "mt-0", children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: "Cancel" }), _jsxs(Button, { type: "submit", disabled: isPending, children: [isPending ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : null, isEditing ? "Save Changes" : "Create Template"] })] })] }) }) }));
296
366
  }
@@ -1 +1 @@
1
- {"version":3,"file":"rich-text-variable-extension.d.ts","sourceRoot":"","sources":["../../src/components/rich-text-variable-extension.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,EAA8B,IAAI,EAAa,MAAM,cAAc,CAAA;AAK1E,eAAO,MAAM,gBAAgB,gBA+H3B,CAAA;AAEF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,WAS9D;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAK3D"}
1
+ {"version":3,"file":"rich-text-variable-extension.d.ts","sourceRoot":"","sources":["../../src/components/rich-text-variable-extension.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,EAA8B,IAAI,EAAa,MAAM,cAAc,CAAA;AAU1E,eAAO,MAAM,gBAAgB,gBA8H3B,CAAA;AAEF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,WAS9D;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAK3D"}
@@ -1,6 +1,11 @@
1
1
  import { InputRule, mergeAttributes, Node, PasteRule } from "@tiptap/core";
2
2
  const variableInputRegex = /(?:^|\s)(\{\{\s*([^}]+)\s*\}\})$/;
3
- const variablePasteRegex = /(?:^|\s)(\{\{\s*([^}]+)\s*\}\})/g;
3
+ // Paste accepts `{{ ... }}` regardless of what precedes it. The
4
+ // live-typing rule above keeps requiring whitespace so we don't gobble
5
+ // braces mid-word, but pasted content commonly has the variable
6
+ // tucked between punctuation (`Document ({{ customer.document.type }}):`,
7
+ // `["{{ foo }}"]`, etc.) and we still want those tokenised.
8
+ const variablePasteRegex = /\{\{\s*([^}]+?)\s*\}\}/g;
4
9
  export const RichTextVariable = Node.create({
5
10
  name: "variable",
6
11
  group: "inline",
@@ -88,11 +93,10 @@ export const RichTextVariable = Node.create({
88
93
  new PasteRule({
89
94
  find: variablePasteRegex,
90
95
  handler: ({ state, range, match }) => {
91
- if (!match[2])
96
+ if (!match[1])
92
97
  return;
93
- const variableContent = match[2].trim();
94
- const addedPosition = match[0].startsWith(" ") ? 1 : 0;
95
- state.tr.replaceWith(range.from + addedPosition, range.to, this.type.create({
98
+ const variableContent = match[1].trim();
99
+ state.tr.replaceWith(range.from, range.to, this.type.create({
96
100
  content: variableContent,
97
101
  }));
98
102
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyantjs/ui",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -35,10 +35,10 @@
35
35
  "tw-animate-css": "^1.3.5",
36
36
  "vaul": "^1.1.2",
37
37
  "zod": "^4.3.6",
38
- "@voyantjs/i18n": "0.20.0",
39
- "@voyantjs/notifications": "0.20.0",
40
- "@voyantjs/notifications-react": "0.20.0",
41
- "@voyantjs/utils": "0.20.0"
38
+ "@voyantjs/notifications": "0.21.0",
39
+ "@voyantjs/notifications-react": "0.21.0",
40
+ "@voyantjs/i18n": "0.21.0",
41
+ "@voyantjs/utils": "0.21.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@tailwindcss/postcss": "^4.1.11",