@remit/ui 0.0.130 → 0.0.132
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/package.json +1 -1
- package/src/components/compose-action-bar.stories.tsx +30 -6
- package/src/components/compose-action-bar.tsx +29 -11
- package/src/components/outbox-row.render.test.ts +15 -0
- package/src/components/outbox-row.stories.tsx +8 -0
- package/src/components/outbox-row.tsx +14 -9
- package/src/components/outbox-status-badge.tsx +12 -1
- package/src/index.ts +1 -1
package/package.json
CHANGED
|
@@ -17,7 +17,7 @@ const meta: Meta<typeof ComposeActionBar> = {
|
|
|
17
17
|
onSend: fn(),
|
|
18
18
|
onBlocked: fn(),
|
|
19
19
|
onDiscard: fn(),
|
|
20
|
-
|
|
20
|
+
save: { status: "idle" },
|
|
21
21
|
},
|
|
22
22
|
};
|
|
23
23
|
export default meta;
|
|
@@ -26,11 +26,35 @@ type Story = StoryObj<typeof ComposeActionBar>;
|
|
|
26
26
|
|
|
27
27
|
export const Ready: Story = {};
|
|
28
28
|
|
|
29
|
-
export const Saving: Story = { args: {
|
|
29
|
+
export const Saving: Story = { args: { save: { status: "saving" } } };
|
|
30
30
|
|
|
31
|
-
export const Saved: Story = { args: {
|
|
31
|
+
export const Saved: Story = { args: { save: { status: "saved" } } };
|
|
32
32
|
|
|
33
|
-
export const SaveFailed: Story = { args: {
|
|
33
|
+
export const SaveFailed: Story = { args: { save: { status: "error" } } };
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Nothing has been written to the server yet and nothing will be until the
|
|
37
|
+
* draft has a To address to be created against. Silence here was the worst of
|
|
38
|
+
* both: the text was not being kept, and the composer looked exactly like one
|
|
39
|
+
* that had nothing to keep. The sentence names To rather than "a recipient",
|
|
40
|
+
* which a message addressed only in Cc already has.
|
|
41
|
+
*/
|
|
42
|
+
export const NotSavedYet: Story = {
|
|
43
|
+
name: "Unsaved — the draft has no To address yet",
|
|
44
|
+
args: {
|
|
45
|
+
send: { status: "blocked", reason: "Add a To address before sending." },
|
|
46
|
+
save: {
|
|
47
|
+
status: "unsaved",
|
|
48
|
+
reason: "Not saved — add a To address to keep this draft.",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
play: async ({ canvasElement }) => {
|
|
52
|
+
const canvas = within(canvasElement);
|
|
53
|
+
await expect(canvas.getByRole("status")).toHaveTextContent(
|
|
54
|
+
"Not saved — add a To address to keep this draft.",
|
|
55
|
+
);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
34
58
|
|
|
35
59
|
export const Sending: Story = {
|
|
36
60
|
name: "Sending — also while the pending draft is written",
|
|
@@ -45,7 +69,7 @@ export const Sending: Story = {
|
|
|
45
69
|
export const NoRecipient: Story = {
|
|
46
70
|
name: "Blocked — nobody to send to",
|
|
47
71
|
args: {
|
|
48
|
-
send: { status: "blocked", reason: "Add
|
|
72
|
+
send: { status: "blocked", reason: "Add a To address before sending." },
|
|
49
73
|
},
|
|
50
74
|
render: (args) => {
|
|
51
75
|
const [reason, setReason] = useState<string>();
|
|
@@ -74,7 +98,7 @@ export const NoRecipient: Story = {
|
|
|
74
98
|
const canvas = within(canvasElement);
|
|
75
99
|
await userEvent.click(canvas.getByRole("button", { name: "Send" }));
|
|
76
100
|
await expect(canvas.getByTestId("compose-unavailable")).toHaveTextContent(
|
|
77
|
-
"Add
|
|
101
|
+
"Add a To address before sending.",
|
|
78
102
|
);
|
|
79
103
|
await expect(args.onSend).not.toHaveBeenCalled();
|
|
80
104
|
},
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import { Loader2, Send, Trash2 } from "lucide-react";
|
|
2
2
|
import { Button } from "./button.js";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* What the draft is doing, and — when it is not being saved — the sentence that
|
|
6
|
+
* says why. Built the same way as `ComposeSendState` and for the same reason:
|
|
7
|
+
* a composer holding text it is not persisting must never be able to say so
|
|
8
|
+
* without saying what is missing. The bare "idle" this replaces rendered
|
|
9
|
+
* nothing at all, so a message that could not be saved yet looked identical to
|
|
10
|
+
* one that had nothing to save.
|
|
11
|
+
*/
|
|
12
|
+
export type ComposeSaveState =
|
|
13
|
+
| { status: "idle" }
|
|
14
|
+
| { status: "saving" }
|
|
15
|
+
| { status: "saved" }
|
|
16
|
+
| { status: "error" }
|
|
17
|
+
| { status: "unsaved"; reason: string };
|
|
5
18
|
|
|
6
19
|
/**
|
|
7
20
|
* Whether Send can act, and when it cannot, the sentence that says why.
|
|
@@ -22,20 +35,25 @@ export interface ComposeActionBarProps {
|
|
|
22
35
|
/** Called with the reason when Send is pressed while it cannot act. */
|
|
23
36
|
onBlocked: (reason: string) => void;
|
|
24
37
|
onDiscard: () => void;
|
|
25
|
-
|
|
38
|
+
save?: ComposeSaveState;
|
|
26
39
|
}
|
|
27
40
|
|
|
28
|
-
const
|
|
29
|
-
if (status === "saving") {
|
|
41
|
+
const SaveStateIndicator = ({ save }: { save: ComposeSaveState }) => {
|
|
42
|
+
if (save.status === "saving") {
|
|
30
43
|
return (
|
|
31
|
-
<
|
|
44
|
+
<output className="animate-pulse text-xs text-fg-muted">Saving...</output>
|
|
32
45
|
);
|
|
33
46
|
}
|
|
34
|
-
if (status === "saved") {
|
|
35
|
-
return <
|
|
47
|
+
if (save.status === "saved") {
|
|
48
|
+
return <output className="text-xs text-fg-muted">Draft saved</output>;
|
|
49
|
+
}
|
|
50
|
+
if (save.status === "error") {
|
|
51
|
+
return <output className="text-xs text-danger">Save failed</output>;
|
|
36
52
|
}
|
|
37
|
-
if (status === "
|
|
38
|
-
return
|
|
53
|
+
if (save.status === "unsaved") {
|
|
54
|
+
return (
|
|
55
|
+
<output className="truncate text-xs text-warning">{save.reason}</output>
|
|
56
|
+
);
|
|
39
57
|
}
|
|
40
58
|
return null;
|
|
41
59
|
};
|
|
@@ -51,7 +69,7 @@ export function ComposeActionBar({
|
|
|
51
69
|
onSend,
|
|
52
70
|
onBlocked,
|
|
53
71
|
onDiscard,
|
|
54
|
-
|
|
72
|
+
save = { status: "idle" },
|
|
55
73
|
}: ComposeActionBarProps) {
|
|
56
74
|
const sending = send.status === "sending";
|
|
57
75
|
const blockedReason = send.status === "blocked" ? send.reason : undefined;
|
|
@@ -84,7 +102,7 @@ export function ComposeActionBar({
|
|
|
84
102
|
>
|
|
85
103
|
Send
|
|
86
104
|
</Button>
|
|
87
|
-
<
|
|
105
|
+
<SaveStateIndicator save={save} />
|
|
88
106
|
</div>
|
|
89
107
|
<Button
|
|
90
108
|
variant="ghost"
|
|
@@ -46,6 +46,21 @@ describe("OutboxRow", () => {
|
|
|
46
46
|
assert.doesNotMatch(html, /disabled=""/);
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
it("offers only delete for an unfiled row and names the reason", () => {
|
|
50
|
+
const html = renderToString(
|
|
51
|
+
createElement(OutboxRow, {
|
|
52
|
+
...baseProps,
|
|
53
|
+
status: "unfiled",
|
|
54
|
+
error: "this account has no Sent folder",
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
57
|
+
assert.match(html, /Sent, not filed/);
|
|
58
|
+
assert.match(html, /this account has no Sent folder/);
|
|
59
|
+
assert.match(html, /aria-label="Delete message"/);
|
|
60
|
+
assert.doesNotMatch(html, /aria-label="Retry sending"/);
|
|
61
|
+
assert.doesNotMatch(html, /aria-label="Edit as draft"/);
|
|
62
|
+
});
|
|
63
|
+
|
|
49
64
|
it("omits retry for a blocked row but keeps edit and delete", () => {
|
|
50
65
|
const html = renderToString(
|
|
51
66
|
createElement(OutboxRow, {
|
|
@@ -24,6 +24,14 @@ export const Sending: Story = { args: { status: "sending" } };
|
|
|
24
24
|
|
|
25
25
|
export const Sent: Story = { args: { status: "sent" } };
|
|
26
26
|
|
|
27
|
+
export const Unfiled: Story = {
|
|
28
|
+
args: {
|
|
29
|
+
status: "unfiled",
|
|
30
|
+
error:
|
|
31
|
+
"Sent, but not filed: this account has no Sent folder. Create one named Sent and later messages will be filed there.",
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
27
35
|
export const Failed: Story = {
|
|
28
36
|
args: {
|
|
29
37
|
status: "failed",
|
|
@@ -11,7 +11,7 @@ export interface OutboxRowProps {
|
|
|
11
11
|
/** Pre-formatted timestamp shown at the row's trailing edge. */
|
|
12
12
|
time: string;
|
|
13
13
|
status: OutboxStatus;
|
|
14
|
-
/** Surfaced after the status label for failed
|
|
14
|
+
/** Surfaced after the status label for failed, blocked and unfiled rows. */
|
|
15
15
|
error?: string;
|
|
16
16
|
selected?: boolean;
|
|
17
17
|
onSelect: () => void;
|
|
@@ -26,7 +26,7 @@ export interface OutboxRowProps {
|
|
|
26
26
|
const tintForStatus = (status: OutboxStatus, selected?: boolean): string => {
|
|
27
27
|
if (selected) return "bg-accent-2-soft";
|
|
28
28
|
if (status === "failed") return "bg-danger-soft";
|
|
29
|
-
if (status === "blocked") return "bg-warning/10";
|
|
29
|
+
if (status === "blocked" || status === "unfiled") return "bg-warning/10";
|
|
30
30
|
return "";
|
|
31
31
|
};
|
|
32
32
|
|
|
@@ -49,7 +49,8 @@ export function OutboxRow({
|
|
|
49
49
|
onDelete,
|
|
50
50
|
deleting,
|
|
51
51
|
}: OutboxRowProps) {
|
|
52
|
-
const showActions =
|
|
52
|
+
const showActions =
|
|
53
|
+
status === "failed" || status === "blocked" || status === "unfiled";
|
|
53
54
|
|
|
54
55
|
return (
|
|
55
56
|
<div
|
|
@@ -96,12 +97,16 @@ export function OutboxRow({
|
|
|
96
97
|
} as const,
|
|
97
98
|
]
|
|
98
99
|
: []),
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
...(status === "unfiled"
|
|
101
|
+
? []
|
|
102
|
+
: [
|
|
103
|
+
{
|
|
104
|
+
label: "Edit as draft",
|
|
105
|
+
iconOnly: true,
|
|
106
|
+
icon: <Send className="size-3.5" />,
|
|
107
|
+
onClick: onEdit,
|
|
108
|
+
} as const,
|
|
109
|
+
]),
|
|
105
110
|
]}
|
|
106
111
|
destructive={{
|
|
107
112
|
label: "Delete message",
|
|
@@ -8,7 +8,13 @@ import {
|
|
|
8
8
|
import { cn } from "../lib/cn.js";
|
|
9
9
|
|
|
10
10
|
/** The non-draft outbox lifecycle states (drafts live in a separate view). */
|
|
11
|
-
export type OutboxStatus =
|
|
11
|
+
export type OutboxStatus =
|
|
12
|
+
| "queued"
|
|
13
|
+
| "sending"
|
|
14
|
+
| "sent"
|
|
15
|
+
| "unfiled"
|
|
16
|
+
| "failed"
|
|
17
|
+
| "blocked";
|
|
12
18
|
|
|
13
19
|
interface StatusConfig {
|
|
14
20
|
icon: typeof Clock;
|
|
@@ -26,6 +32,11 @@ export const outboxStatusConfig: Record<OutboxStatus, StatusConfig> = {
|
|
|
26
32
|
spin: true,
|
|
27
33
|
},
|
|
28
34
|
sent: { icon: CheckCircle, label: "Sent", className: "text-positive" },
|
|
35
|
+
unfiled: {
|
|
36
|
+
icon: AlertTriangle,
|
|
37
|
+
label: "Sent, not filed",
|
|
38
|
+
className: "text-warning",
|
|
39
|
+
},
|
|
29
40
|
failed: { icon: AlertCircle, label: "Failed", className: "text-danger" },
|
|
30
41
|
blocked: { icon: AlertTriangle, label: "Blocked", className: "text-warning" },
|
|
31
42
|
};
|
package/src/index.ts
CHANGED
|
@@ -166,7 +166,7 @@ export { Checkbox, type CheckboxProps } from "./components/checkbox.js";
|
|
|
166
166
|
export {
|
|
167
167
|
ComposeActionBar,
|
|
168
168
|
type ComposeActionBarProps,
|
|
169
|
-
type
|
|
169
|
+
type ComposeSaveState,
|
|
170
170
|
type ComposeSendState,
|
|
171
171
|
} from "./components/compose-action-bar.js";
|
|
172
172
|
export {
|