@proteos/ui 0.41.1 → 0.42.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/dist/index.cjs +15 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.js +15 -8
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/draft-message-composer/draft-message-composer.test.tsx +9 -2
- package/src/components/draft-message-composer/draft-message-composer.tsx +10 -0
- package/src/components/message-composer/message-composer.tsx +16 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proteos/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.0",
|
|
4
4
|
"license": "FSL-1.1-Apache-2.0",
|
|
5
5
|
"description": "Proteos React component library (React Aria Components + Tailwind v4).",
|
|
6
6
|
"repository": {
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"tailwind-merge": "^3.5.0",
|
|
56
56
|
"tailwind-variants": "^3.2.2",
|
|
57
57
|
"tailwindcss": "^4.2.4",
|
|
58
|
-
"@proteos/sdk": "0.
|
|
58
|
+
"@proteos/sdk": "0.42.0"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@biomejs/biome": "^2.4.13",
|
|
@@ -151,14 +151,20 @@ describe('DraftMessageComposer', () => {
|
|
|
151
151
|
expect(screen.getByTestId('outcome')).toHaveTextContent('sent')
|
|
152
152
|
})
|
|
153
153
|
|
|
154
|
-
it('a failed delivery is terminal
|
|
154
|
+
it('a failed delivery is terminal, fires onFailed only and no outcome', async () => {
|
|
155
155
|
const client = mockClient({
|
|
156
156
|
sendDraft: vi.fn().mockResolvedValue(draftMessage({ status: 'failed', error: 'smtp 550' })),
|
|
157
157
|
})
|
|
158
158
|
const onSent = vi.fn()
|
|
159
|
+
const onFailed = vi.fn()
|
|
159
160
|
render(
|
|
160
161
|
<>
|
|
161
|
-
<DraftMessageComposer
|
|
162
|
+
<DraftMessageComposer
|
|
163
|
+
messageId="draft-1"
|
|
164
|
+
client={client}
|
|
165
|
+
onSent={onSent}
|
|
166
|
+
onFailed={onFailed}
|
|
167
|
+
/>
|
|
162
168
|
<OutcomeProbe messageId="draft-1" />
|
|
163
169
|
</>,
|
|
164
170
|
)
|
|
@@ -166,6 +172,7 @@ describe('DraftMessageComposer', () => {
|
|
|
166
172
|
await userEvent.click(screen.getByRole('button', { name: 'Send' }))
|
|
167
173
|
expect(await screen.findByText(/Sending failed — smtp 550/)).toBeInTheDocument()
|
|
168
174
|
expect(onSent).not.toHaveBeenCalled()
|
|
175
|
+
expect(onFailed).toHaveBeenCalledTimes(1)
|
|
169
176
|
expect(screen.getByTestId('outcome')).toHaveTextContent('none')
|
|
170
177
|
})
|
|
171
178
|
|
|
@@ -67,6 +67,8 @@ export interface DraftMessageComposerProps {
|
|
|
67
67
|
onSent?: (message: Message) => void
|
|
68
68
|
/** Fires once the draft was declined. */
|
|
69
69
|
onRejected?: (message: Message) => void
|
|
70
|
+
/** Fires when the human approved the send but delivery failed. */
|
|
71
|
+
onFailed?: (message: Message) => void
|
|
70
72
|
/**
|
|
71
73
|
* Default true. When false the internal Send / Save draft / Reject row is
|
|
72
74
|
* hidden — the composer becomes a pure preview/edit surface driven by
|
|
@@ -74,6 +76,9 @@ export interface DraftMessageComposerProps {
|
|
|
74
76
|
*/
|
|
75
77
|
showActions?: boolean
|
|
76
78
|
className?: string
|
|
79
|
+
/** Composes onto the body textarea (MessageComposer.bodyClassName) — review
|
|
80
|
+
* surfaces raise the growth cap so a whole email fits. */
|
|
81
|
+
bodyClassName?: string
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
/** Maps stored recipient snapshots back to editable send targets (contact
|
|
@@ -108,8 +113,10 @@ export function DraftMessageComposer({
|
|
|
108
113
|
client,
|
|
109
114
|
onSent,
|
|
110
115
|
onRejected,
|
|
116
|
+
onFailed,
|
|
111
117
|
showActions = true,
|
|
112
118
|
className,
|
|
119
|
+
bodyClassName,
|
|
113
120
|
}: DraftMessageComposerProps) {
|
|
114
121
|
const [phase, setPhase] = useState<Phase>('loading')
|
|
115
122
|
const [busy, setBusy] = useState<Busy>('idle')
|
|
@@ -266,6 +273,7 @@ export function DraftMessageComposer({
|
|
|
266
273
|
// published — the timeline's failed-retry pattern owns what's next.
|
|
267
274
|
setTerminalKind('failed')
|
|
268
275
|
setPhase('terminal')
|
|
276
|
+
onFailed?.(result)
|
|
269
277
|
return
|
|
270
278
|
}
|
|
271
279
|
setTerminalKind('sent')
|
|
@@ -288,6 +296,7 @@ export function DraftMessageComposer({
|
|
|
288
296
|
client,
|
|
289
297
|
messageId,
|
|
290
298
|
onSent,
|
|
299
|
+
onFailed,
|
|
291
300
|
areRecipientsEditable,
|
|
292
301
|
conversation,
|
|
293
302
|
to,
|
|
@@ -406,6 +415,7 @@ export function DraftMessageComposer({
|
|
|
406
415
|
value={value}
|
|
407
416
|
onValueChange={setValue}
|
|
408
417
|
placeholder="Draft body…"
|
|
418
|
+
{...(bodyClassName !== undefined ? { bodyClassName } : {})}
|
|
409
419
|
{...(isEmail && !isSubjectEditable ? { isSubjectReadOnly: true } : {})}
|
|
410
420
|
onSend={async () => send()}
|
|
411
421
|
{...(areRecipientsEditable && message.connection_id
|
|
@@ -135,6 +135,9 @@ export interface MessageComposerProps {
|
|
|
135
135
|
showSendButton?: boolean
|
|
136
136
|
/** Composes onto the outer box chrome (`messageComposerStyles`). */
|
|
137
137
|
className?: string
|
|
138
|
+
/** Composes onto the body textarea — review surfaces override the default
|
|
139
|
+
* `max-h-48` growth cap (e.g. `max-h-[55dvh]`) so a whole email fits. */
|
|
140
|
+
bodyClassName?: string
|
|
138
141
|
}
|
|
139
142
|
|
|
140
143
|
export function MessageComposer({
|
|
@@ -163,6 +166,7 @@ export function MessageComposer({
|
|
|
163
166
|
hasUploadingAttachments,
|
|
164
167
|
showSendButton,
|
|
165
168
|
className,
|
|
169
|
+
bodyClassName,
|
|
166
170
|
}: MessageComposerProps) {
|
|
167
171
|
const [isSending, setIsSending] = useState(false)
|
|
168
172
|
const [sendError, setSendError] = useState<string | null>(null)
|
|
@@ -172,6 +176,13 @@ export function MessageComposer({
|
|
|
172
176
|
const [showCcBcc, setShowCcBcc] = useState(false)
|
|
173
177
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
|
174
178
|
|
|
179
|
+
// Size-to-content on every text change — including a seeded value (a loaded
|
|
180
|
+
// draft body), which never fires onChange.
|
|
181
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: value.text is the trigger — the height reads the DOM, not the value.
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (textareaRef.current) autoGrow(textareaRef.current)
|
|
184
|
+
}, [value.text])
|
|
185
|
+
|
|
175
186
|
// Controlled-if-provided, per group: a provided prop wins over internal
|
|
176
187
|
// state; setters mirror into internal state only when uncontrolled.
|
|
177
188
|
const to = toProp ?? internalTo
|
|
@@ -402,14 +413,12 @@ export function MessageComposer({
|
|
|
402
413
|
value={value.text}
|
|
403
414
|
placeholder={placeholder}
|
|
404
415
|
rows={1}
|
|
405
|
-
onChange={(event) => {
|
|
406
|
-
onValueChange({ ...value, text: event.target.value })
|
|
407
|
-
autoGrow(event.currentTarget)
|
|
408
|
-
}}
|
|
416
|
+
onChange={(event) => onValueChange({ ...value, text: event.target.value })}
|
|
409
417
|
onKeyDown={onKeyDown}
|
|
410
418
|
className={cn(
|
|
411
419
|
'w-full resize-none bg-transparent outline-none',
|
|
412
420
|
'min-h-[40px] max-h-48 text-[14px] leading-[1.5] text-ink placeholder:text-ink-4',
|
|
421
|
+
bodyClassName,
|
|
413
422
|
)}
|
|
414
423
|
data-conversations-composer
|
|
415
424
|
/>
|
|
@@ -474,7 +483,9 @@ export function MessageComposer({
|
|
|
474
483
|
)
|
|
475
484
|
}
|
|
476
485
|
|
|
486
|
+
// Sizes the textarea to its content; the CSS max-h on the element clips the
|
|
487
|
+
// growth (and scrolls) so the cap stays a style concern, overridable per host.
|
|
477
488
|
function autoGrow(node: HTMLTextAreaElement) {
|
|
478
489
|
node.style.height = 'auto'
|
|
479
|
-
node.style.height = `${
|
|
490
|
+
node.style.height = `${node.scrollHeight}px`
|
|
480
491
|
}
|