@handled-ai/design-system 0.20.26 → 0.20.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/button.d.ts +1 -1
- package/dist/components/conversation-panel.d.ts +2 -2
- package/dist/components/conversation-panel.js +39 -13
- package/dist/components/conversation-panel.js.map +1 -1
- package/dist/components/suggested-actions.d.ts +1 -0
- package/dist/components/suggested-actions.js +1 -1
- package/dist/components/suggested-actions.js.map +1 -1
- package/package.json +1 -1
- package/src/components/__tests__/conversation-panel.test.tsx +65 -0
- package/src/components/conversation-panel.tsx +38 -9
- package/src/components/suggested-actions.tsx +6 -3
|
@@ -126,7 +126,7 @@ export interface ConvMessage {
|
|
|
126
126
|
export type ConvStatus = "responded" | "awaiting" | "viewing" | "draft"
|
|
127
127
|
|
|
128
128
|
/**
|
|
129
|
-
* A
|
|
129
|
+
* A follow-up template offered in the in-composer "Templates" picker (WIT-970).
|
|
130
130
|
* Presentational: the consumer fills variables for THIS thread's recipient
|
|
131
131
|
* before passing it in, so `body` is composer-ready text. `tags` (e.g. "Reply")
|
|
132
132
|
* are shown as chips so reps can recognize the right template at a glance.
|
|
@@ -166,7 +166,7 @@ export interface ConversationThread {
|
|
|
166
166
|
/** Signature text appended to replies (plain text). */
|
|
167
167
|
signature?: string
|
|
168
168
|
/**
|
|
169
|
-
*
|
|
169
|
+
* Follow-up templates offered in the composer's "Templates" picker, already
|
|
170
170
|
* personalized for this thread's recipient (WIT-970). Omit/empty hides the
|
|
171
171
|
* picker.
|
|
172
172
|
*/
|
|
@@ -561,6 +561,14 @@ type PreviewState = {
|
|
|
561
561
|
|
|
562
562
|
const IDLE_PREVIEW: PreviewState = { loading: false, html: null, confirmationToken: null, error: null, local: false }
|
|
563
563
|
|
|
564
|
+
/**
|
|
565
|
+
* Matches an author-declared manual-fill placeholder ({{personalize}},
|
|
566
|
+
* {{personalization_recent_news}}, …). These have no resolver and must be
|
|
567
|
+
* replaced by the rep before sending; the composer blocks send while any remain.
|
|
568
|
+
* Non-global so repeated `.test()` calls during render are stateless.
|
|
569
|
+
*/
|
|
570
|
+
const MANUAL_FILL_TOKEN_RE = /\{\{\s*personaliz(?:e|ation)(?:_[a-z0-9]+)*\s*\}\}/i
|
|
571
|
+
|
|
564
572
|
function ReplyComposer({
|
|
565
573
|
thread,
|
|
566
574
|
me,
|
|
@@ -588,6 +596,10 @@ function ReplyComposer({
|
|
|
588
596
|
const [appliedTemplate, setAppliedTemplate] = React.useState<string | null>(null)
|
|
589
597
|
const [sig, setSig] = React.useState(true)
|
|
590
598
|
const replyTemplates = thread.replyTemplates ?? []
|
|
599
|
+
// Manual-fill placeholders ({{personalize…}}) are author-required: the rep
|
|
600
|
+
// must replace them before sending. While any remain in the body we block
|
|
601
|
+
// send/preview so the personalization is never sent as a literal token.
|
|
602
|
+
const hasManualFillToken = MANUAL_FILL_TOKEN_RE.test(body)
|
|
591
603
|
|
|
592
604
|
const applyTemplate = (template: ConversationReplyTemplate) => {
|
|
593
605
|
setBody(template.body)
|
|
@@ -719,7 +731,7 @@ function ReplyComposer({
|
|
|
719
731
|
onKeyDown={(e) => {
|
|
720
732
|
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
721
733
|
e.preventDefault()
|
|
722
|
-
void openPreview()
|
|
734
|
+
if (!hasManualFillToken) void openPreview()
|
|
723
735
|
}
|
|
724
736
|
}}
|
|
725
737
|
/>
|
|
@@ -736,11 +748,11 @@ function ReplyComposer({
|
|
|
736
748
|
<DropdownMenu>
|
|
737
749
|
<DropdownMenuTrigger asChild>
|
|
738
750
|
<Button type="button" variant="outline" size="sm" disabled={sending} data-slot="conv-reply-template-trigger">
|
|
739
|
-
<FileText size={14} />
|
|
751
|
+
<FileText size={14} /> Templates
|
|
740
752
|
</Button>
|
|
741
753
|
</DropdownMenuTrigger>
|
|
742
754
|
<DropdownMenuContent align="start" className="max-w-xs">
|
|
743
|
-
<DropdownMenuLabel>
|
|
755
|
+
<DropdownMenuLabel>Templates</DropdownMenuLabel>
|
|
744
756
|
<DropdownMenuSeparator />
|
|
745
757
|
{replyTemplates.map((template) => (
|
|
746
758
|
<DropdownMenuItem
|
|
@@ -773,16 +785,33 @@ function ReplyComposer({
|
|
|
773
785
|
</div>
|
|
774
786
|
) : null}
|
|
775
787
|
|
|
788
|
+
{hasManualFillToken ? (
|
|
789
|
+
<p data-slot="conv-reply-manual-fill" role="alert" className="text-destructive mt-2 text-[12px]">
|
|
790
|
+
Fill in the personalize placeholder before sending. Replace each {"{{personalize}}"} with a real, account-specific line.
|
|
791
|
+
</p>
|
|
792
|
+
) : null}
|
|
793
|
+
|
|
776
794
|
<div className="mt-2 flex flex-wrap items-center gap-2">
|
|
777
795
|
<RichTextToolbar />
|
|
778
796
|
<label className="text-muted-foreground ml-auto inline-flex cursor-pointer items-center gap-1.5 text-[12px]">
|
|
779
797
|
<Switch checked={sig} onCheckedChange={setSig} aria-label="Toggle signature" />
|
|
780
798
|
Signature
|
|
781
799
|
</label>
|
|
782
|
-
<Button
|
|
800
|
+
<Button
|
|
801
|
+
type="button"
|
|
802
|
+
variant="outline"
|
|
803
|
+
size="sm"
|
|
804
|
+
disabled={sending || hasManualFillToken}
|
|
805
|
+
onClick={() => void openPreview()}
|
|
806
|
+
>
|
|
783
807
|
<Eye size={14} /> Preview
|
|
784
808
|
</Button>
|
|
785
|
-
<Button
|
|
809
|
+
<Button
|
|
810
|
+
type="button"
|
|
811
|
+
size="sm"
|
|
812
|
+
disabled={sending || hasManualFillToken}
|
|
813
|
+
onClick={() => void openPreview()}
|
|
814
|
+
>
|
|
786
815
|
<Send size={14} /> Send
|
|
787
816
|
</Button>
|
|
788
817
|
</div>
|
|
@@ -795,7 +824,7 @@ function ReplyComposer({
|
|
|
795
824
|
</DialogTitle>
|
|
796
825
|
<DialogDescription>
|
|
797
826
|
{previewState.local
|
|
798
|
-
? "Local draft preview only
|
|
827
|
+
? "Local draft preview only. The server prepares the exact message on send."
|
|
799
828
|
: <>Stays on the {subject.replace(/^Re:\s*/i, "")} thread. Gmail keeps it threaded.</>}
|
|
800
829
|
</DialogDescription>
|
|
801
830
|
</DialogHeader>
|
|
@@ -855,7 +884,7 @@ function ReplyComposer({
|
|
|
855
884
|
<Button
|
|
856
885
|
type="button"
|
|
857
886
|
size="sm"
|
|
858
|
-
disabled={sending || previewState.loading}
|
|
887
|
+
disabled={sending || previewState.loading || hasManualFillToken}
|
|
859
888
|
onClick={handleSend}
|
|
860
889
|
>
|
|
861
890
|
<Send size={14} /> {sending ? "Sending..." : "Send now"}
|
|
@@ -35,6 +35,7 @@ import { AccountContactsPopover, BrandIcon } from "./account-contacts-popover"
|
|
|
35
35
|
|
|
36
36
|
export interface SuggestedActionsIconMap {
|
|
37
37
|
gmail?: string
|
|
38
|
+
outlook?: string
|
|
38
39
|
slack?: string
|
|
39
40
|
zendesk?: string
|
|
40
41
|
salesforce?: string
|
|
@@ -43,9 +44,11 @@ export interface SuggestedActionsIconMap {
|
|
|
43
44
|
function getActionTypeIcon(type: string, className?: string, iconMap?: SuggestedActionsIconMap) {
|
|
44
45
|
switch (type) {
|
|
45
46
|
case "email":
|
|
46
|
-
return iconMap?.
|
|
47
|
-
? <BrandIcon src={iconMap.
|
|
48
|
-
:
|
|
47
|
+
return iconMap?.outlook
|
|
48
|
+
? <BrandIcon src={iconMap.outlook} alt="Outlook" className={className} />
|
|
49
|
+
: iconMap?.gmail
|
|
50
|
+
? <BrandIcon src={iconMap.gmail} alt="Gmail" className={className} />
|
|
51
|
+
: <Mail className={className} />
|
|
49
52
|
case "slack":
|
|
50
53
|
return iconMap?.slack
|
|
51
54
|
? <BrandIcon src={iconMap.slack} alt="Slack" className={className} />
|