@remit/ui 0.0.54 → 0.0.55
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/filter-sheet.stories.tsx +2 -6
- package/src/components/message-list-pane.render.test.ts +31 -0
- package/src/components/message-list-pane.stories.tsx +30 -0
- package/src/components/message-list-state.stories.tsx +3 -3
- package/src/filter-presets.test.ts +16 -11
- package/src/filter-presets.ts +5 -20
- package/src/index.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
2
|
import { useState } from "react";
|
|
3
|
-
import { UNCLASSIFIED_CATEGORY } from "../filter-presets.js";
|
|
4
3
|
import {
|
|
5
4
|
FilterSheet,
|
|
6
5
|
type FilterSheetCategory,
|
|
@@ -11,7 +10,7 @@ import {
|
|
|
11
10
|
const CATEGORIES: FilterSheetCategory[] = [
|
|
12
11
|
{ id: "all", label: "All", tone: "neutral" },
|
|
13
12
|
{ id: "personal", label: "Personal", tone: "positive" },
|
|
14
|
-
|
|
13
|
+
{ id: "uncategorized", label: "Unclassified", tone: "neutral" },
|
|
15
14
|
{ id: "newsletters", label: "Newsletters", tone: "accent" },
|
|
16
15
|
{ id: "marketing", label: "Marketing", tone: "warning" },
|
|
17
16
|
{ id: "automated", label: "Automated", tone: "neutral" },
|
|
@@ -136,10 +135,7 @@ export const CollapsedWithActiveFilters: Story = {
|
|
|
136
135
|
*/
|
|
137
136
|
export const CollapsedWithUnclassified: Story = {
|
|
138
137
|
render: () => (
|
|
139
|
-
<ControlledShell
|
|
140
|
-
initialExpanded={false}
|
|
141
|
-
initialCategory={UNCLASSIFIED_CATEGORY.id}
|
|
142
|
-
/>
|
|
138
|
+
<ControlledShell initialExpanded={false} initialCategory="uncategorized" />
|
|
143
139
|
),
|
|
144
140
|
};
|
|
145
141
|
|
|
@@ -121,6 +121,37 @@ describe("MessageListPane", () => {
|
|
|
121
121
|
assert.match(html, /Report a problem/);
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
+
it("forwards the filter to the empty state so a narrowed list says so (#306)", () => {
|
|
125
|
+
const html = renderToString(
|
|
126
|
+
createElement(MessageListPane, {
|
|
127
|
+
...baseProps,
|
|
128
|
+
isDesktop: true,
|
|
129
|
+
listState: "empty",
|
|
130
|
+
listFilter: {
|
|
131
|
+
label: "Personal",
|
|
132
|
+
reach: "whole-folder",
|
|
133
|
+
onClear: () => undefined,
|
|
134
|
+
},
|
|
135
|
+
listScopeLabel: "Inbox",
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
assert.match(html, /No Personal mail in Inbox/);
|
|
139
|
+
assert.match(html, /Every message in this folder was checked\./);
|
|
140
|
+
assert.doesNotMatch(html, /No messages in this mailbox/);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("keeps the plain empty copy when nothing narrows the list", () => {
|
|
144
|
+
const html = renderToString(
|
|
145
|
+
createElement(MessageListPane, {
|
|
146
|
+
...baseProps,
|
|
147
|
+
isDesktop: true,
|
|
148
|
+
listState: "empty",
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
assert.match(html, /No messages in this mailbox/);
|
|
152
|
+
assert.doesNotMatch(html, /Every message in this folder was checked\./);
|
|
153
|
+
});
|
|
154
|
+
|
|
124
155
|
it("renders the selectionBar slot instead of the pane header when provided", () => {
|
|
125
156
|
const html = renderToString(
|
|
126
157
|
createElement(MessageListPane, {
|
|
@@ -258,6 +258,36 @@ export const NarrowExternalSelectionBar: Story = {
|
|
|
258
258
|
decorators: [narrowFrame],
|
|
259
259
|
};
|
|
260
260
|
|
|
261
|
+
/** An empty mailbox, unfiltered: one plain line and no completeness claim. */
|
|
262
|
+
export const EmptyState: Story = {
|
|
263
|
+
args: {
|
|
264
|
+
isDesktop: true,
|
|
265
|
+
flatList: true,
|
|
266
|
+
listState: "empty",
|
|
267
|
+
},
|
|
268
|
+
decorators: [desktopFrame],
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* The same pane under a category filter — the composition the inbox ships
|
|
273
|
+
* (#306). The pane forwards the filter and the scope to the empty state, so a
|
|
274
|
+
* narrowed list states what was read instead of reading as an empty mailbox.
|
|
275
|
+
*/
|
|
276
|
+
export const FilteredEmptyState: Story = {
|
|
277
|
+
args: {
|
|
278
|
+
isDesktop: true,
|
|
279
|
+
flatList: true,
|
|
280
|
+
listState: "empty",
|
|
281
|
+
listFilter: {
|
|
282
|
+
label: "Personal",
|
|
283
|
+
reach: "whole-folder",
|
|
284
|
+
onClear: () => undefined,
|
|
285
|
+
},
|
|
286
|
+
listScopeLabel: "Inbox",
|
|
287
|
+
},
|
|
288
|
+
decorators: [desktopFrame],
|
|
289
|
+
};
|
|
290
|
+
|
|
261
291
|
/** Fail-loud error state — the specific failure detail is surfaced under the
|
|
262
292
|
* headline (not a bare "something went wrong"), with a way back (Retry) and a
|
|
263
293
|
* place for the failure to go (Report a problem). */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Decorator, Meta, StoryObj } from "@storybook/react";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
|
-
import { inboxFilterConfig
|
|
3
|
+
import { inboxFilterConfig } from "../filter-presets.js";
|
|
4
4
|
import type { ThreadRowData } from "./app-shell-types.js";
|
|
5
5
|
import { FilterSheet } from "./filter-sheet.js";
|
|
6
6
|
import {
|
|
@@ -89,7 +89,7 @@ function FilteredShell({
|
|
|
89
89
|
return (
|
|
90
90
|
<div className="flex h-full flex-col">
|
|
91
91
|
<FilterSheet
|
|
92
|
-
categories={
|
|
92
|
+
categories={preset.categories}
|
|
93
93
|
filters={preset.filters}
|
|
94
94
|
selectedCategory={category}
|
|
95
95
|
activeFilters={new Set<string>()}
|
|
@@ -220,7 +220,7 @@ export const FilteredEmptyUnclassified: Story = {
|
|
|
220
220
|
scopeLabel: "Inbox",
|
|
221
221
|
},
|
|
222
222
|
render: (args) => (
|
|
223
|
-
<FilteredShell category=
|
|
223
|
+
<FilteredShell category="uncategorized">
|
|
224
224
|
<MessageListEmpty {...args} />
|
|
225
225
|
</FilteredShell>
|
|
226
226
|
),
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
type FilterAccount,
|
|
6
6
|
flaggedFilterConfig,
|
|
7
7
|
inboxFilterConfig,
|
|
8
|
-
UNCLASSIFIED_CATEGORY,
|
|
9
8
|
} from "./filter-presets.js";
|
|
10
9
|
|
|
11
10
|
const accounts: FilterAccount[] = [
|
|
@@ -23,6 +22,7 @@ describe("briefFilterConfig", () => {
|
|
|
23
22
|
[
|
|
24
23
|
"all",
|
|
25
24
|
"personal",
|
|
25
|
+
"uncategorized",
|
|
26
26
|
"transactional",
|
|
27
27
|
"newsletter",
|
|
28
28
|
"marketing",
|
|
@@ -100,26 +100,31 @@ describe("flaggedFilterConfig", () => {
|
|
|
100
100
|
});
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
-
describe("
|
|
104
|
-
it("is
|
|
103
|
+
describe("the Unclassified category", () => {
|
|
104
|
+
it("is offered by every shipped preset", () => {
|
|
105
105
|
for (const preset of [
|
|
106
106
|
briefFilterConfig(),
|
|
107
107
|
inboxFilterConfig(),
|
|
108
108
|
flaggedFilterConfig(),
|
|
109
109
|
]) {
|
|
110
110
|
assert.equal(
|
|
111
|
-
preset.categories.some((c) => c.id ===
|
|
112
|
-
|
|
111
|
+
preset.categories.some((c) => c.id === "uncategorized"),
|
|
112
|
+
true,
|
|
113
113
|
);
|
|
114
114
|
}
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
+
it("follows Personal, the order the brief already uses", () => {
|
|
118
|
+
const ids = inboxFilterConfig().categories.map((c) => c.id);
|
|
119
|
+
assert.equal(ids.indexOf("uncategorized"), ids.indexOf("personal") + 1);
|
|
120
|
+
});
|
|
121
|
+
|
|
117
122
|
it("carries its own label and tone, never personal's (#45)", () => {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
);
|
|
121
|
-
assert.equal(
|
|
122
|
-
assert.notEqual(
|
|
123
|
-
assert.notEqual(
|
|
123
|
+
const categories = briefFilterConfig().categories;
|
|
124
|
+
const unclassified = categories.find((c) => c.id === "uncategorized");
|
|
125
|
+
const personal = categories.find((c) => c.id === "personal");
|
|
126
|
+
assert.equal(unclassified?.label, "Unclassified");
|
|
127
|
+
assert.notEqual(unclassified?.label, personal?.label);
|
|
128
|
+
assert.notEqual(unclassified?.tone, personal?.tone);
|
|
124
129
|
});
|
|
125
130
|
});
|
package/src/filter-presets.ts
CHANGED
|
@@ -27,35 +27,20 @@ export interface FilterPreset {
|
|
|
27
27
|
sources?: FilterSheetSource[];
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
/**
|
|
31
|
-
* The Unclassified chip D6 requires, built and storied but not offered yet.
|
|
32
|
-
*
|
|
33
|
-
* This module is a runtime dependency of three live surfaces that filter over
|
|
34
|
-
* the loaded window, so a chip in the list below is a chip a user can click
|
|
35
|
-
* today. Offering it before #306 makes the predicate a server-side `where`
|
|
36
|
-
* would add a fresh instance of the bug this epic exists to fix: a category
|
|
37
|
-
* whose mail sits below the newest page reads as a category with no mail.
|
|
38
|
-
*
|
|
39
|
-
* #306 splices this entry into `MESSAGE_CATEGORIES` after `Personal` — matching
|
|
40
|
-
* `briefCategories`' order — and deletes this constant.
|
|
41
|
-
*/
|
|
42
|
-
export const UNCLASSIFIED_CATEGORY: FilterSheetCategory = {
|
|
43
|
-
id: "uncategorized",
|
|
44
|
-
label: "Unclassified",
|
|
45
|
-
tone: "neutral",
|
|
46
|
-
};
|
|
47
|
-
|
|
48
30
|
/**
|
|
49
31
|
* Content-type categories, mirroring the `MessageCategory` enum
|
|
50
32
|
* (@remit/remit-imap) by value. The leading "all" clears the category. Per
|
|
51
33
|
* message, not per mailbox — so they apply in the brief and an inbox alike.
|
|
52
34
|
*
|
|
53
|
-
* `uncategorized` is
|
|
54
|
-
* (issue #45)
|
|
35
|
+
* `uncategorized` is a category of its own here, never folded into `personal`
|
|
36
|
+
* (issue #45): mail the classifier has not reached is a pending state with a
|
|
37
|
+
* name, and it carries its own label and tone so the two can never read alike.
|
|
38
|
+
* It sits after `Personal` to match `briefCategories`' order.
|
|
55
39
|
*/
|
|
56
40
|
const MESSAGE_CATEGORIES: FilterSheetCategory[] = [
|
|
57
41
|
{ id: "all", label: "All", tone: "neutral" },
|
|
58
42
|
{ id: "personal", label: "Personal", tone: "accent" },
|
|
43
|
+
{ id: "uncategorized", label: "Unclassified", tone: "neutral" },
|
|
59
44
|
{ id: "transactional", label: "Transactional", tone: "positive" },
|
|
60
45
|
{ id: "newsletter", label: "Newsletter", tone: "neutral" },
|
|
61
46
|
{ id: "marketing", label: "Marketing", tone: "warning" },
|
package/src/index.ts
CHANGED
|
@@ -249,6 +249,7 @@ export {
|
|
|
249
249
|
} from "./components/message-header.js";
|
|
250
250
|
export { MessageListPane } from "./components/message-list-pane.js";
|
|
251
251
|
export {
|
|
252
|
+
type FilterReach,
|
|
252
253
|
type ListState,
|
|
253
254
|
MessageListEmpty,
|
|
254
255
|
type MessageListEmptyProps,
|
|
@@ -545,7 +546,6 @@ export {
|
|
|
545
546
|
type FilterPreset,
|
|
546
547
|
flaggedFilterConfig,
|
|
547
548
|
inboxFilterConfig,
|
|
548
|
-
UNCLASSIFIED_CATEGORY,
|
|
549
549
|
} from "./filter-presets.js";
|
|
550
550
|
export {
|
|
551
551
|
buildCidResolver,
|