@remit/web-client 0.0.78 → 0.0.80

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/web-client",
3
- "version": "0.0.78",
3
+ "version": "0.0.80",
4
4
  "type": "module",
5
5
  "description": "Remit web client, published as composable primitives — the app shell, auth shells, and runtime config. A distributor imports what it composes and bundles it.",
6
6
  "exports": {
@@ -0,0 +1,134 @@
1
+ import type { RemitImapLabelResponse } from "@remit/api-http-client/types.gen.ts";
2
+ import type { Meta, StoryObj } from "@storybook/react-vite";
3
+ import { LabelsList } from "./LabelsList";
4
+
5
+ const meta: Meta<typeof LabelsList> = {
6
+ title: "Flows/Settings Labels/LabelsList",
7
+ component: LabelsList,
8
+ parameters: { layout: "padded" },
9
+ decorators: [
10
+ (Story) => (
11
+ <div className="mx-auto max-w-md">
12
+ <Story />
13
+ </div>
14
+ ),
15
+ ],
16
+ args: {
17
+ onRename: () => undefined,
18
+ onRecolor: () => undefined,
19
+ onDelete: () => undefined,
20
+ },
21
+ };
22
+ export default meta;
23
+
24
+ type Story = StoryObj<typeof LabelsList>;
25
+
26
+ const makeLabel = (
27
+ overrides: Partial<RemitImapLabelResponse>,
28
+ ): RemitImapLabelResponse => ({
29
+ labelId: "lbl-1",
30
+ accountConfigId: "acc-1",
31
+ name: "Receipts",
32
+ color: "Blue",
33
+ createdAt: 0,
34
+ updatedAt: 0,
35
+ filterCount: 0,
36
+ ...overrides,
37
+ });
38
+
39
+ const fewLabels: RemitImapLabelResponse[] = [
40
+ makeLabel({
41
+ labelId: "lbl-1",
42
+ name: "Receipts",
43
+ color: "Blue",
44
+ filterCount: 0,
45
+ }),
46
+ makeLabel({
47
+ labelId: "lbl-2",
48
+ name: "Travel",
49
+ color: "Green",
50
+ filterCount: 1,
51
+ }),
52
+ makeLabel({ labelId: "lbl-3", name: "Urgent", color: "Red", filterCount: 3 }),
53
+ ];
54
+
55
+ const manyLabels: RemitImapLabelResponse[] = [
56
+ ...fewLabels,
57
+ makeLabel({
58
+ labelId: "lbl-4",
59
+ name: "Newsletters",
60
+ color: "Yellow",
61
+ filterCount: 2,
62
+ }),
63
+ makeLabel({
64
+ labelId: "lbl-5",
65
+ name: "Work",
66
+ color: "Purple",
67
+ filterCount: 0,
68
+ }),
69
+ makeLabel({
70
+ labelId: "lbl-6",
71
+ name: "Family",
72
+ color: "Teal",
73
+ filterCount: 0,
74
+ }),
75
+ makeLabel({
76
+ labelId: "lbl-7",
77
+ name: "Finance",
78
+ color: "Orange",
79
+ filterCount: 4,
80
+ }),
81
+ makeLabel({
82
+ labelId: "lbl-8",
83
+ name: "Recruiting",
84
+ color: "Gray",
85
+ filterCount: 1,
86
+ }),
87
+ makeLabel({
88
+ labelId: "lbl-9",
89
+ name: "Quarterly compliance filings that need a second look",
90
+ color: "Default",
91
+ filterCount: 1,
92
+ }),
93
+ ];
94
+
95
+ /** No labels yet — the empty-state copy points at the create form below. */
96
+ export const Empty: Story = {
97
+ args: { labels: [] },
98
+ };
99
+
100
+ /** A few labels, each showing its filter-usage line. */
101
+ export const Few: Story = {
102
+ args: { labels: fewLabels },
103
+ };
104
+
105
+ /** The same few labels, on the dark theme. */
106
+ export const FewDark: Story = {
107
+ name: "Few (dark)",
108
+ parameters: { theme: "dark" },
109
+ args: { labels: fewLabels },
110
+ };
111
+
112
+ /** Many labels, including one with a long name that truncates in its chip. */
113
+ export const Many: Story = {
114
+ args: { labels: manyLabels },
115
+ };
116
+
117
+ /**
118
+ * Renaming inline: clicking a label's chip swaps it for a focused text input.
119
+ * Driven here rather than passed as a prop — `editingId` is internal state.
120
+ */
121
+ export const Renaming: Story = {
122
+ args: { labels: fewLabels },
123
+ play: async ({ canvasElement }) => {
124
+ const renameButton = canvasElement.querySelector<HTMLButtonElement>(
125
+ 'button[aria-label="Rename label Receipts"]',
126
+ );
127
+ renameButton?.click();
128
+ },
129
+ };
130
+
131
+ /** A delete in flight — the row's delete button disables. */
132
+ export const Deleting: Story = {
133
+ args: { labels: fewLabels, deletingLabelId: "lbl-2" },
134
+ };
@@ -0,0 +1,83 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { ConfirmDialog } from "@/components/ui/ConfirmDialog";
3
+ import { deleteLabelConfirmCopy } from "@/lib/organize/label-delete-copy";
4
+
5
+ /**
6
+ * The cascade-aware label delete confirmation (issue #26, #335) — the generic
7
+ * `ConfirmDialog` driven by `deleteLabelConfirmCopy`'s computed title and
8
+ * description, exactly as `AccountLabels` in the labels settings route wires
9
+ * it. No dedicated component exists for this; reusing `ConfirmDialog` is the
10
+ * approved surface, not a placeholder for one.
11
+ */
12
+ const meta: Meta<typeof ConfirmDialog> = {
13
+ title: "Flows/Settings Labels/Delete Confirmation",
14
+ component: ConfirmDialog,
15
+ parameters: { layout: "centered" },
16
+ };
17
+ export default meta;
18
+
19
+ type Story = StoryObj<typeof ConfirmDialog>;
20
+
21
+ /** Nothing references the label — no blast-radius line under the title. */
22
+ export const Unused: Story = {
23
+ args: {
24
+ isOpen: true,
25
+ ...deleteLabelConfirmCopy("Receipts", 0),
26
+ confirmLabel: "Delete label",
27
+ destructive: true,
28
+ onConfirm: () => undefined,
29
+ onCancel: () => undefined,
30
+ },
31
+ };
32
+
33
+ /** Exactly one filter applies the label — singular wording. */
34
+ export const OneFilter: Story = {
35
+ args: {
36
+ isOpen: true,
37
+ ...deleteLabelConfirmCopy("Receipts", 1),
38
+ confirmLabel: "Delete label",
39
+ destructive: true,
40
+ onConfirm: () => undefined,
41
+ onCancel: () => undefined,
42
+ },
43
+ };
44
+
45
+ /** Several filters apply the label — the cascade warning names the count. */
46
+ export const SeveralFilters: Story = {
47
+ args: {
48
+ isOpen: true,
49
+ ...deleteLabelConfirmCopy("Receipts", 5),
50
+ confirmLabel: "Delete label",
51
+ destructive: true,
52
+ onConfirm: () => undefined,
53
+ onCancel: () => undefined,
54
+ },
55
+ };
56
+
57
+ /** Several filters, on the dark theme. */
58
+ export const SeveralFiltersDark: Story = {
59
+ name: "Several Filters (dark)",
60
+ parameters: { theme: "dark" },
61
+ args: {
62
+ isOpen: true,
63
+ ...deleteLabelConfirmCopy("Receipts", 5),
64
+ confirmLabel: "Delete label",
65
+ destructive: true,
66
+ onConfirm: () => undefined,
67
+ onCancel: () => undefined,
68
+ },
69
+ };
70
+
71
+ /** The delete is in flight — confirm disables rather than allowing a second
72
+ * concurrent delete. */
73
+ export const Busy: Story = {
74
+ args: {
75
+ isOpen: true,
76
+ ...deleteLabelConfirmCopy("Receipts", 5),
77
+ confirmLabel: "Delete label",
78
+ destructive: true,
79
+ isBusy: true,
80
+ onConfirm: () => undefined,
81
+ onCancel: () => undefined,
82
+ },
83
+ };
@@ -12,6 +12,7 @@ const make = (
12
12
  hasStars: boolean,
13
13
  ): RemitImapThreadMessageResponse => ({
14
14
  senderTrust: "unknown",
15
+ category: "uncategorized",
15
16
  threadId: "t1",
16
17
  threadMessageId: `tm-${messageId}`,
17
18
  messageId,
@@ -77,6 +77,7 @@ describe("patchDescribeMessage", () => {
77
77
  cc: [],
78
78
  bcc: [],
79
79
  replyTo: [],
80
+ category: "uncategorized",
80
81
  senderTrust: "unknown",
81
82
  },
82
83
  flags: [],
@@ -24,6 +24,7 @@ function threadResponse(
24
24
  fromEmail: "sender@example.com",
25
25
  subject: "Subject",
26
26
  snippet: "Snippet",
27
+ category: "uncategorized",
27
28
  sentDate: 1767225600,
28
29
  isRead: false,
29
30
  isDeleted: false,
@@ -16,6 +16,7 @@ function threadMessage(
16
16
  fromEmail: "sender@example.com",
17
17
  subject: "Subject",
18
18
  snippet: "Snippet",
19
+ category: "uncategorized",
19
20
  sentDate: 1767225600,
20
21
  isRead: false,
21
22
  isDeleted: false,
@@ -10,7 +10,10 @@ describe("toDisplayCategory", () => {
10
10
  assert.strictEqual(toDisplayCategory("uncategorized"), "uncategorized");
11
11
  });
12
12
 
13
- test("treats a row with no category as unclassified", () => {
13
+ test("treats an absent category as unclassified", () => {
14
+ // No caller can reach this today — both response fields are required —
15
+ // but an absent category must resolve to `uncategorized`, never to
16
+ // `personal`, wherever one arrives from.
14
17
  assert.strictEqual(toDisplayCategory(undefined), "uncategorized");
15
18
  });
16
19
 
@@ -11,8 +11,10 @@ import type { ThreadCategory } from "@remit/ui";
11
11
  * decided was personal, so a classification gap read as a large personal inbox
12
12
  * rather than as missing work (issue #45).
13
13
  *
14
- * Pre-migration rows that read `undefined` carry no category either, so they
15
- * map to `uncategorized` the same way.
14
+ * Both response fields that feed this are required, so no caller reaches the
15
+ * `undefined` branch. The parameter stays tolerant anyway: this is the one place
16
+ * an absent category is turned into a value, and the answer has to be
17
+ * `uncategorized` rather than a crash or a `personal` default.
16
18
  */
17
19
  export function toDisplayCategory(
18
20
  category: RemitImapMessageCategory | undefined,
@@ -71,6 +71,7 @@ export const makeThreadMessage = (
71
71
  fromEmail: "alice@example.com",
72
72
  sentDate: 1_767_225_600_000,
73
73
  snippet: "",
74
+ category: "uncategorized",
74
75
  isRead: false,
75
76
  hasAttachment: false,
76
77
  star: "none",