@echothink-ui/inbox 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/components/AgentDraftReviewPanel.d.ts +18 -0
- package/dist/components/ApprovalToSendPanel.d.ts +11 -0
- package/dist/components/AttachmentList.d.ts +8 -0
- package/dist/components/AttachmentPreview.d.ts +7 -0
- package/dist/components/EmailAutomationRulePanel.d.ts +8 -0
- package/dist/components/InboxShell.d.ts +10 -0
- package/dist/components/LabelManager.d.ts +9 -0
- package/dist/components/MailboxFolderList.d.ts +8 -0
- package/dist/components/MessageComposer.d.ts +13 -0
- package/dist/components/MessageFilterBar.d.ts +13 -0
- package/dist/components/MessageList.d.ts +8 -0
- package/dist/components/MessagePreview.d.ts +9 -0
- package/dist/components/MessageSearch.d.ts +11 -0
- package/dist/components/MessageThread.d.ts +22 -0
- package/dist/components/PriorityInboxView.d.ts +14 -0
- package/dist/components/RecipientPicker.d.ts +10 -0
- package/dist/components/ThreadSummaryPanel.d.ts +9 -0
- package/dist/index.cjs +1699 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +2155 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +1645 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +75 -0
- package/dist/utils.d.ts +5 -0
- package/package.json +44 -0
- package/src/components/AgentDraftReviewPanel.tsx +128 -0
- package/src/components/ApprovalToSendPanel.tsx +118 -0
- package/src/components/AttachmentList.tsx +85 -0
- package/src/components/AttachmentPreview.tsx +207 -0
- package/src/components/EmailAutomationRulePanel.tsx +100 -0
- package/src/components/InboxShell.tsx +62 -0
- package/src/components/LabelManager.tsx +147 -0
- package/src/components/MailboxFolderList.tsx +66 -0
- package/src/components/MessageComposer.tsx +160 -0
- package/src/components/MessageFilterBar.test.tsx +34 -0
- package/src/components/MessageFilterBar.tsx +69 -0
- package/src/components/MessageList.tsx +101 -0
- package/src/components/MessagePreview.test.tsx +48 -0
- package/src/components/MessagePreview.tsx +84 -0
- package/src/components/MessageSearch.tsx +96 -0
- package/src/components/MessageThread.tsx +173 -0
- package/src/components/PriorityInboxView.tsx +74 -0
- package/src/components/RecipientPicker.tsx +181 -0
- package/src/components/ThreadSummaryPanel.tsx +107 -0
- package/src/index.test.tsx +276 -0
- package/src/index.tsx +60 -0
- package/src/styles.css +2523 -0
- package/src/types.ts +85 -0
- package/src/utils.ts +33 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Badge, Button, Panel, Textarea } from "@echothink-ui/core";
|
|
3
|
+
import type { MessageDraft } from "../types";
|
|
4
|
+
|
|
5
|
+
export interface ApprovalToSendPanelProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
6
|
+
draft?: MessageDraft;
|
|
7
|
+
recipients?: string[];
|
|
8
|
+
riskLevel?: "low" | "medium" | "high" | "critical";
|
|
9
|
+
policyRef?: React.ReactNode;
|
|
10
|
+
onApprove?: (reason?: string) => void;
|
|
11
|
+
onReject?: (reason?: string) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ApprovalToSendPanel({
|
|
15
|
+
draft,
|
|
16
|
+
recipients = [],
|
|
17
|
+
riskLevel = "low",
|
|
18
|
+
policyRef,
|
|
19
|
+
onApprove,
|
|
20
|
+
onReject,
|
|
21
|
+
className,
|
|
22
|
+
...props
|
|
23
|
+
}: ApprovalToSendPanelProps) {
|
|
24
|
+
const [reason, setReason] = React.useState("");
|
|
25
|
+
const toRecipients = recipients.length ? recipients : (draft?.to ?? []);
|
|
26
|
+
const ccRecipients = draft?.cc ?? [];
|
|
27
|
+
const bccRecipients = draft?.bcc ?? [];
|
|
28
|
+
const subject = draft?.subject?.trim() || "No subject";
|
|
29
|
+
const body = draft?.body?.trim() || "No message body provided.";
|
|
30
|
+
const riskSeverity =
|
|
31
|
+
riskLevel === "critical" || riskLevel === "high"
|
|
32
|
+
? "danger"
|
|
33
|
+
: riskLevel === "medium"
|
|
34
|
+
? "warning"
|
|
35
|
+
: "info";
|
|
36
|
+
const riskLabel = `${riskLevel.charAt(0).toUpperCase()}${riskLevel.slice(1)} risk`;
|
|
37
|
+
const recipientCount =
|
|
38
|
+
recipients.length || toRecipients.length + ccRecipients.length + bccRecipients.length;
|
|
39
|
+
const recipientKind = recipients.length ? "external recipient" : "recipient";
|
|
40
|
+
const recipientSummary =
|
|
41
|
+
recipientCount === 1 ? `1 ${recipientKind}` : `${recipientCount || "No"} ${recipientKind}s`;
|
|
42
|
+
|
|
43
|
+
const formatRecipients = (values: string[]) =>
|
|
44
|
+
values.length ? values.join(", ") : "Not specified";
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Panel
|
|
48
|
+
{...props}
|
|
49
|
+
className={`eth-inbox-send-approval eth-inbox-send-approval--${riskLevel} ${className ?? ""}`}
|
|
50
|
+
title="Approval required"
|
|
51
|
+
subtitle="Review this outbound message before it is sent."
|
|
52
|
+
data-eth-component="ApprovalToSendPanel"
|
|
53
|
+
>
|
|
54
|
+
<div className="eth-inbox-send-approval__status">
|
|
55
|
+
<div className="eth-inbox-send-approval__summary">
|
|
56
|
+
<Badge severity={riskSeverity}>{riskLabel}</Badge>
|
|
57
|
+
{policyRef ? <span className="eth-inbox-send-approval__policy">{policyRef}</span> : null}
|
|
58
|
+
</div>
|
|
59
|
+
<span className="eth-inbox-send-approval__state">Awaiting approval</span>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<section
|
|
63
|
+
className="eth-inbox-send-approval__message"
|
|
64
|
+
aria-label="Outbound message for approval"
|
|
65
|
+
>
|
|
66
|
+
<header className="eth-inbox-send-approval__message-header">
|
|
67
|
+
<div>
|
|
68
|
+
<h4>Outbound message</h4>
|
|
69
|
+
<p>{recipientSummary}</p>
|
|
70
|
+
</div>
|
|
71
|
+
</header>
|
|
72
|
+
<dl className="eth-inbox-send-approval__meta">
|
|
73
|
+
<div>
|
|
74
|
+
<dt>To</dt>
|
|
75
|
+
<dd>{formatRecipients(toRecipients)}</dd>
|
|
76
|
+
</div>
|
|
77
|
+
{ccRecipients.length ? (
|
|
78
|
+
<div>
|
|
79
|
+
<dt>Cc</dt>
|
|
80
|
+
<dd>{formatRecipients(ccRecipients)}</dd>
|
|
81
|
+
</div>
|
|
82
|
+
) : null}
|
|
83
|
+
{bccRecipients.length ? (
|
|
84
|
+
<div>
|
|
85
|
+
<dt>Bcc</dt>
|
|
86
|
+
<dd>{formatRecipients(bccRecipients)}</dd>
|
|
87
|
+
</div>
|
|
88
|
+
) : null}
|
|
89
|
+
<div>
|
|
90
|
+
<dt>Subject</dt>
|
|
91
|
+
<dd>{subject}</dd>
|
|
92
|
+
</div>
|
|
93
|
+
</dl>
|
|
94
|
+
<div className="eth-inbox-send-approval__message-body">
|
|
95
|
+
<span>Message body</span>
|
|
96
|
+
<p>{body}</p>
|
|
97
|
+
</div>
|
|
98
|
+
</section>
|
|
99
|
+
|
|
100
|
+
<div className="eth-inbox-send-approval__decision">
|
|
101
|
+
<Textarea
|
|
102
|
+
value={reason}
|
|
103
|
+
labelText="Decision note"
|
|
104
|
+
helperText="Optional for approval. Include the policy reason when rejecting."
|
|
105
|
+
placeholder="Add an approval or rejection reason"
|
|
106
|
+
onChange={(event) => setReason(event.currentTarget.value)}
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div className="eth-inbox-send-approval__actions">
|
|
111
|
+
<Button intent="danger" onClick={() => onReject?.(reason || undefined)}>
|
|
112
|
+
Reject
|
|
113
|
+
</Button>
|
|
114
|
+
<Button onClick={() => onApprove?.(reason || undefined)}>Approve send</Button>
|
|
115
|
+
</div>
|
|
116
|
+
</Panel>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { IconButton } from "@echothink-ui/core";
|
|
3
|
+
import { DocumentIcon, DownloadIcon, ViewIcon } from "@echothink-ui/icons";
|
|
4
|
+
import type { Attachment } from "../types";
|
|
5
|
+
import { formatBytes } from "../utils";
|
|
6
|
+
|
|
7
|
+
export interface AttachmentListProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
8
|
+
attachments?: Attachment[];
|
|
9
|
+
onPreview?: (attachment: Attachment) => void;
|
|
10
|
+
onDownload?: (attachment: Attachment) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function AttachmentList({
|
|
14
|
+
attachments = [],
|
|
15
|
+
onPreview,
|
|
16
|
+
onDownload,
|
|
17
|
+
className,
|
|
18
|
+
role,
|
|
19
|
+
"aria-label": ariaLabel,
|
|
20
|
+
...props
|
|
21
|
+
}: AttachmentListProps) {
|
|
22
|
+
if (!attachments.length) return null;
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
{...props}
|
|
26
|
+
className={["eth-inbox-attachments", className].filter(Boolean).join(" ")}
|
|
27
|
+
data-eth-component="AttachmentList"
|
|
28
|
+
role={role ?? "list"}
|
|
29
|
+
aria-label={ariaLabel ?? "Message attachments"}
|
|
30
|
+
>
|
|
31
|
+
{attachments.map((attachment) => {
|
|
32
|
+
const typeLabel = attachmentTypeLabel(attachment);
|
|
33
|
+
const hasActions = Boolean(onPreview || onDownload);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div key={attachment.id} className="eth-inbox-attachments__tile" role="listitem">
|
|
37
|
+
<span className="eth-inbox-attachments__icon" aria-hidden="true">
|
|
38
|
+
<DocumentIcon size={18} />
|
|
39
|
+
</span>
|
|
40
|
+
<div className="eth-inbox-attachments__content">
|
|
41
|
+
<strong title={attachment.name}>{attachment.name}</strong>
|
|
42
|
+
<span className="eth-inbox-attachments__meta">
|
|
43
|
+
<span className="eth-inbox-attachments__type">{typeLabel}</span>
|
|
44
|
+
<span>{attachment.mimeType}</span>
|
|
45
|
+
<span>{formatBytes(attachment.size)}</span>
|
|
46
|
+
</span>
|
|
47
|
+
</div>
|
|
48
|
+
{hasActions ? (
|
|
49
|
+
<div
|
|
50
|
+
className="eth-inbox-attachments__actions"
|
|
51
|
+
aria-label={`Actions for ${attachment.name}`}
|
|
52
|
+
>
|
|
53
|
+
{onPreview ? (
|
|
54
|
+
<IconButton
|
|
55
|
+
intent="ghost"
|
|
56
|
+
density="compact"
|
|
57
|
+
label={`Preview ${attachment.name}`}
|
|
58
|
+
icon={<ViewIcon size={16} />}
|
|
59
|
+
onClick={() => onPreview(attachment)}
|
|
60
|
+
/>
|
|
61
|
+
) : null}
|
|
62
|
+
{onDownload ? (
|
|
63
|
+
<IconButton
|
|
64
|
+
intent="ghost"
|
|
65
|
+
density="compact"
|
|
66
|
+
label={`Download ${attachment.name}`}
|
|
67
|
+
icon={<DownloadIcon size={16} />}
|
|
68
|
+
onClick={() => onDownload(attachment)}
|
|
69
|
+
/>
|
|
70
|
+
) : null}
|
|
71
|
+
</div>
|
|
72
|
+
) : null}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
})}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function attachmentTypeLabel(attachment: Attachment) {
|
|
81
|
+
const extension = attachment.name.split(".").pop();
|
|
82
|
+
if (extension && extension !== attachment.name) return extension.slice(0, 4).toUpperCase();
|
|
83
|
+
const subtype = attachment.mimeType.split("/").pop();
|
|
84
|
+
return subtype ? subtype.slice(0, 4).toUpperCase() : "FILE";
|
|
85
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { Attachment } from "../types";
|
|
3
|
+
import { formatBytes } from "../utils";
|
|
4
|
+
|
|
5
|
+
export interface AttachmentPreviewProps extends React.HTMLAttributes<HTMLElement> {
|
|
6
|
+
attachment?: Attachment;
|
|
7
|
+
children?: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function AttachmentPreview({
|
|
11
|
+
attachment,
|
|
12
|
+
children,
|
|
13
|
+
className,
|
|
14
|
+
...props
|
|
15
|
+
}: AttachmentPreviewProps) {
|
|
16
|
+
const kind = getPreviewKind(attachment);
|
|
17
|
+
const previewSource = attachment?.previewUrl ?? attachment?.src;
|
|
18
|
+
const hasCustomPreview = children !== undefined && children !== null;
|
|
19
|
+
const label = attachment ? `Attachment preview for ${attachment.name}` : "Attachment preview";
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<section
|
|
23
|
+
{...props}
|
|
24
|
+
aria-label={props["aria-label"] ?? label}
|
|
25
|
+
className={`eth-inbox-attachment-preview ${className ?? ""}`}
|
|
26
|
+
data-eth-component="AttachmentPreview"
|
|
27
|
+
>
|
|
28
|
+
{attachment ? (
|
|
29
|
+
<header className="eth-inbox-attachment-preview__header">
|
|
30
|
+
<div className="eth-inbox-attachment-preview__file">
|
|
31
|
+
<span
|
|
32
|
+
className="eth-inbox-attachment-preview__glyph"
|
|
33
|
+
data-kind={kind}
|
|
34
|
+
aria-hidden="true"
|
|
35
|
+
>
|
|
36
|
+
{kindToken(kind)}
|
|
37
|
+
</span>
|
|
38
|
+
<div className="eth-inbox-attachment-preview__summary">
|
|
39
|
+
<strong title={attachment.name}>{attachment.name}</strong>
|
|
40
|
+
<span>
|
|
41
|
+
{attachment.mimeType} - {formatBytes(attachment.size)}
|
|
42
|
+
</span>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
<span className="eth-inbox-attachment-preview__state">{previewStateLabel(kind)}</span>
|
|
46
|
+
</header>
|
|
47
|
+
) : null}
|
|
48
|
+
<div
|
|
49
|
+
className={`eth-inbox-attachment-preview__body eth-inbox-attachment-preview__body--${kind}`}
|
|
50
|
+
>
|
|
51
|
+
{hasCustomPreview ? children : renderDefaultPreview(attachment, kind, previewSource)}
|
|
52
|
+
</div>
|
|
53
|
+
</section>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type AttachmentPreviewKind =
|
|
58
|
+
| "empty"
|
|
59
|
+
| "pdf"
|
|
60
|
+
| "image"
|
|
61
|
+
| "text"
|
|
62
|
+
| "spreadsheet"
|
|
63
|
+
| "archive"
|
|
64
|
+
| "file";
|
|
65
|
+
|
|
66
|
+
function getPreviewKind(attachment?: Attachment): AttachmentPreviewKind {
|
|
67
|
+
if (!attachment) return "empty";
|
|
68
|
+
const mimeType = attachment.mimeType.toLowerCase();
|
|
69
|
+
const name = attachment.name.toLowerCase();
|
|
70
|
+
|
|
71
|
+
if (mimeType === "application/pdf" || name.endsWith(".pdf")) return "pdf";
|
|
72
|
+
if (mimeType.startsWith("image/")) return "image";
|
|
73
|
+
if (
|
|
74
|
+
mimeType.includes("spreadsheet") ||
|
|
75
|
+
mimeType.includes("csv") ||
|
|
76
|
+
name.endsWith(".csv") ||
|
|
77
|
+
name.endsWith(".xls") ||
|
|
78
|
+
name.endsWith(".xlsx")
|
|
79
|
+
) {
|
|
80
|
+
return "spreadsheet";
|
|
81
|
+
}
|
|
82
|
+
if (
|
|
83
|
+
mimeType.startsWith("text/") ||
|
|
84
|
+
["application/json", "application/xml", "application/yaml"].includes(mimeType) ||
|
|
85
|
+
name.endsWith(".md")
|
|
86
|
+
) {
|
|
87
|
+
return "text";
|
|
88
|
+
}
|
|
89
|
+
if (
|
|
90
|
+
mimeType.includes("zip") ||
|
|
91
|
+
mimeType.includes("compressed") ||
|
|
92
|
+
name.endsWith(".zip") ||
|
|
93
|
+
name.endsWith(".gz")
|
|
94
|
+
) {
|
|
95
|
+
return "archive";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return "file";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function kindToken(kind: AttachmentPreviewKind) {
|
|
102
|
+
if (kind === "pdf") return "PDF";
|
|
103
|
+
if (kind === "image") return "IMG";
|
|
104
|
+
if (kind === "text") return "TXT";
|
|
105
|
+
if (kind === "spreadsheet") return "CSV";
|
|
106
|
+
if (kind === "archive") return "ZIP";
|
|
107
|
+
return "FILE";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function previewStateLabel(kind: AttachmentPreviewKind) {
|
|
111
|
+
if (kind === "archive" || kind === "file") return "Preview unavailable";
|
|
112
|
+
return "Inline preview";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function renderDefaultPreview(
|
|
116
|
+
attachment: Attachment | undefined,
|
|
117
|
+
kind: AttachmentPreviewKind,
|
|
118
|
+
previewSource?: string
|
|
119
|
+
) {
|
|
120
|
+
if (!attachment) {
|
|
121
|
+
return (
|
|
122
|
+
<div className="eth-inbox-attachment-preview__empty">
|
|
123
|
+
<strong>No attachment selected</strong>
|
|
124
|
+
<span>PDF, image, text, and spreadsheet files can be previewed inline.</span>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (kind === "image") {
|
|
130
|
+
return previewSource ? (
|
|
131
|
+
<img
|
|
132
|
+
className="eth-inbox-attachment-preview__image"
|
|
133
|
+
src={previewSource}
|
|
134
|
+
alt={`${attachment.name} preview`}
|
|
135
|
+
/>
|
|
136
|
+
) : (
|
|
137
|
+
<div
|
|
138
|
+
className="eth-inbox-attachment-preview__image-frame"
|
|
139
|
+
role="img"
|
|
140
|
+
aria-label={`${attachment.name} image preview`}
|
|
141
|
+
>
|
|
142
|
+
<span />
|
|
143
|
+
<span />
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (kind === "text") {
|
|
149
|
+
return (
|
|
150
|
+
<div
|
|
151
|
+
className="eth-inbox-attachment-preview__text"
|
|
152
|
+
role="img"
|
|
153
|
+
aria-label={`${attachment.name} text preview`}
|
|
154
|
+
>
|
|
155
|
+
{Array.from({ length: 9 }, (_, index) => (
|
|
156
|
+
<span key={index} />
|
|
157
|
+
))}
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (kind === "spreadsheet") {
|
|
163
|
+
return (
|
|
164
|
+
<div
|
|
165
|
+
className="eth-inbox-attachment-preview__sheet"
|
|
166
|
+
role="img"
|
|
167
|
+
aria-label={`${attachment.name} spreadsheet preview`}
|
|
168
|
+
>
|
|
169
|
+
{Array.from({ length: 20 }, (_, index) => (
|
|
170
|
+
<span key={index} />
|
|
171
|
+
))}
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (kind === "pdf") {
|
|
177
|
+
return (
|
|
178
|
+
<div
|
|
179
|
+
className="eth-inbox-attachment-preview__document"
|
|
180
|
+
role="img"
|
|
181
|
+
aria-label={`${attachment.name} PDF preview`}
|
|
182
|
+
>
|
|
183
|
+
<div className="eth-inbox-attachment-preview__document-bar">
|
|
184
|
+
<span>{attachment.mimeType}</span>
|
|
185
|
+
<span>Page 1</span>
|
|
186
|
+
</div>
|
|
187
|
+
<div className="eth-inbox-attachment-preview__page">
|
|
188
|
+
<strong>{attachment.name.replace(/\.[^.]+$/, "") || attachment.name}</strong>
|
|
189
|
+
<span />
|
|
190
|
+
<span />
|
|
191
|
+
<span />
|
|
192
|
+
<span />
|
|
193
|
+
<span />
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<div className="eth-inbox-attachment-preview__empty">
|
|
201
|
+
<strong>Preview unavailable</strong>
|
|
202
|
+
<span>
|
|
203
|
+
{attachment.mimeType} files are kept as attachments for download or external review.
|
|
204
|
+
</span>
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Badge, Button, Panel, Toggle } from "@echothink-ui/core";
|
|
3
|
+
import { EditIcon } from "@echothink-ui/icons";
|
|
4
|
+
import type { AutomationRule } from "../types";
|
|
5
|
+
|
|
6
|
+
export interface EmailAutomationRulePanelProps extends Omit<
|
|
7
|
+
React.HTMLAttributes<HTMLDivElement>,
|
|
8
|
+
"onToggle"
|
|
9
|
+
> {
|
|
10
|
+
rules?: AutomationRule[];
|
|
11
|
+
onToggle?: (id: string, enabled: boolean) => void;
|
|
12
|
+
onEdit?: (id: string) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function EmailAutomationRulePanel({
|
|
16
|
+
rules = [],
|
|
17
|
+
onToggle,
|
|
18
|
+
onEdit,
|
|
19
|
+
className,
|
|
20
|
+
...props
|
|
21
|
+
}: EmailAutomationRulePanelProps) {
|
|
22
|
+
const enabledCount = rules.reduce((count, rule) => count + (rule.enabled ? 1 : 0), 0);
|
|
23
|
+
const ruleCountLabel = `${rules.length} automation ${rules.length === 1 ? "rule" : "rules"}`;
|
|
24
|
+
const subtitle = rules.length
|
|
25
|
+
? `${enabledCount} of ${ruleCountLabel} enabled`
|
|
26
|
+
: "Create routing, drafting, and mailbox automation rules.";
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Panel
|
|
30
|
+
{...props}
|
|
31
|
+
className={`eth-inbox-rules ${className ?? ""}`}
|
|
32
|
+
title="Automation rules"
|
|
33
|
+
subtitle={subtitle}
|
|
34
|
+
data-eth-component="EmailAutomationRulePanel"
|
|
35
|
+
>
|
|
36
|
+
{rules.length ? (
|
|
37
|
+
<div className="eth-inbox-rules__list" role="list">
|
|
38
|
+
{rules.map((rule) => {
|
|
39
|
+
const headingId = `eth-inbox-rule-${rule.id.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<article
|
|
43
|
+
key={rule.id}
|
|
44
|
+
aria-labelledby={headingId}
|
|
45
|
+
className={`eth-inbox-rules__item ${
|
|
46
|
+
rule.enabled ? "eth-inbox-rules__item--enabled" : "eth-inbox-rules__item--paused"
|
|
47
|
+
}`}
|
|
48
|
+
role="listitem"
|
|
49
|
+
>
|
|
50
|
+
<div className="eth-inbox-rules__content">
|
|
51
|
+
<header className="eth-inbox-rules__item-header">
|
|
52
|
+
<h4 id={headingId}>{rule.name}</h4>
|
|
53
|
+
<Badge severity={rule.enabled ? "success" : "neutral"}>
|
|
54
|
+
{rule.enabled ? "Enabled" : "Paused"}
|
|
55
|
+
</Badge>
|
|
56
|
+
</header>
|
|
57
|
+
<dl className="eth-inbox-rules__definition">
|
|
58
|
+
<div>
|
|
59
|
+
<dt>Trigger</dt>
|
|
60
|
+
<dd>{rule.conditions}</dd>
|
|
61
|
+
</div>
|
|
62
|
+
<div>
|
|
63
|
+
<dt>Action</dt>
|
|
64
|
+
<dd>{rule.actions}</dd>
|
|
65
|
+
</div>
|
|
66
|
+
</dl>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div className="eth-inbox-rules__controls">
|
|
70
|
+
<Toggle
|
|
71
|
+
checked={rule.enabled}
|
|
72
|
+
className="eth-inbox-rules__toggle"
|
|
73
|
+
disabled={!onToggle}
|
|
74
|
+
hideLabel
|
|
75
|
+
label={`Automation status for ${rule.name}`}
|
|
76
|
+
offLabel="Paused"
|
|
77
|
+
onChange={(event) => onToggle?.(rule.id, event.currentTarget.checked)}
|
|
78
|
+
onLabel="Enabled"
|
|
79
|
+
/>
|
|
80
|
+
<Button
|
|
81
|
+
aria-label={`Edit ${rule.name}`}
|
|
82
|
+
density="compact"
|
|
83
|
+
disabled={!onEdit}
|
|
84
|
+
icon={<EditIcon size={16} />}
|
|
85
|
+
intent="secondary"
|
|
86
|
+
onClick={() => onEdit?.(rule.id)}
|
|
87
|
+
>
|
|
88
|
+
Edit
|
|
89
|
+
</Button>
|
|
90
|
+
</div>
|
|
91
|
+
</article>
|
|
92
|
+
);
|
|
93
|
+
})}
|
|
94
|
+
</div>
|
|
95
|
+
) : (
|
|
96
|
+
<p className="eth-inbox-rules__empty">No automation rules configured.</p>
|
|
97
|
+
)}
|
|
98
|
+
</Panel>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { SurfaceComponentProps } from "@echothink-ui/core";
|
|
3
|
+
|
|
4
|
+
export interface InboxShellProps extends SurfaceComponentProps {
|
|
5
|
+
folders?: React.ReactNode;
|
|
6
|
+
list?: React.ReactNode;
|
|
7
|
+
thread?: React.ReactNode;
|
|
8
|
+
inspector?: React.ReactNode;
|
|
9
|
+
toolbar?: React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function InboxShell({
|
|
13
|
+
folders,
|
|
14
|
+
list,
|
|
15
|
+
thread,
|
|
16
|
+
inspector,
|
|
17
|
+
toolbar,
|
|
18
|
+
className,
|
|
19
|
+
title,
|
|
20
|
+
description,
|
|
21
|
+
...props
|
|
22
|
+
}: InboxShellProps) {
|
|
23
|
+
const hasInspector = Boolean(inspector);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<section
|
|
27
|
+
{...props}
|
|
28
|
+
className={`eth-inbox-shell ${className ?? ""}`}
|
|
29
|
+
data-eth-component="InboxShell"
|
|
30
|
+
>
|
|
31
|
+
{(title || description || toolbar) && (
|
|
32
|
+
<header className="eth-inbox-shell__header">
|
|
33
|
+
<div>
|
|
34
|
+
{title ? <h2>{title}</h2> : null}
|
|
35
|
+
{description ? <p>{description}</p> : null}
|
|
36
|
+
</div>
|
|
37
|
+
{toolbar ? <div className="eth-inbox-shell__toolbar">{toolbar}</div> : null}
|
|
38
|
+
</header>
|
|
39
|
+
)}
|
|
40
|
+
<div
|
|
41
|
+
className={`eth-inbox-shell__layout ${
|
|
42
|
+
hasInspector ? "eth-inbox-shell__layout--has-inspector" : ""
|
|
43
|
+
}`}
|
|
44
|
+
>
|
|
45
|
+
<aside className="eth-inbox-shell__folders" aria-label="Mailbox folders">
|
|
46
|
+
{folders}
|
|
47
|
+
</aside>
|
|
48
|
+
<aside className="eth-inbox-shell__list" aria-label="Message list">
|
|
49
|
+
{list}
|
|
50
|
+
</aside>
|
|
51
|
+
<main className="eth-inbox-shell__thread" aria-label="Selected conversation">
|
|
52
|
+
{thread}
|
|
53
|
+
</main>
|
|
54
|
+
{inspector ? (
|
|
55
|
+
<aside className="eth-inbox-shell__inspector" aria-label="Conversation inspector">
|
|
56
|
+
{inspector}
|
|
57
|
+
</aside>
|
|
58
|
+
) : null}
|
|
59
|
+
</div>
|
|
60
|
+
</section>
|
|
61
|
+
);
|
|
62
|
+
}
|