@remit/web-client 0.0.259 → 0.0.261
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.
|
|
3
|
+
"version": "0.0.261",
|
|
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,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #1014: when compose cannot resolve the sending account, the `<select>`
|
|
3
|
+
* had no option carrying `value=""`, so the browser fell back to showing the
|
|
4
|
+
* first account's address even though nothing was actually selected. The
|
|
5
|
+
* unresolved state now gets its own disabled placeholder option, so no real
|
|
6
|
+
* identity is shown while nothing is chosen.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { afterEach, describe, it } from "node:test";
|
|
11
|
+
import { configOperationsGetConfigQueryKey } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
12
|
+
import { createElement } from "react";
|
|
13
|
+
import { createDomHarness, type DomHarness } from "../../test-support/dom";
|
|
14
|
+
import { makeAccount, makeConfig } from "../../test-support/fixtures";
|
|
15
|
+
import { FromSelector } from "./FromSelector";
|
|
16
|
+
|
|
17
|
+
let harness: DomHarness | undefined;
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
harness?.close();
|
|
21
|
+
harness = undefined;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const ACCOUNTS = [
|
|
25
|
+
makeAccount({ accountId: "acc-1", email: "alice@example.com" }),
|
|
26
|
+
makeAccount({ accountId: "acc-2", email: "bob@example.com" }),
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const mount = (selectedAccountId: string | undefined): DomHarness => {
|
|
30
|
+
harness = createDomHarness();
|
|
31
|
+
harness.queryClient.setQueryData(
|
|
32
|
+
configOperationsGetConfigQueryKey(),
|
|
33
|
+
makeConfig(ACCOUNTS),
|
|
34
|
+
);
|
|
35
|
+
harness.renderApp(
|
|
36
|
+
createElement(FromSelector, {
|
|
37
|
+
selectedAccountId,
|
|
38
|
+
onSelect: () => undefined,
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
return harness;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const select = (dom: DomHarness): HTMLSelectElement => {
|
|
45
|
+
const found = dom.query<HTMLSelectElement>("#from-account-selector");
|
|
46
|
+
if (!found) throw new Error("the from-account select is not mounted");
|
|
47
|
+
return found;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
describe("FromSelector with an unresolved account (#1014)", () => {
|
|
51
|
+
it("shows a disabled placeholder instead of the first account's address", () => {
|
|
52
|
+
const dom = mount(undefined);
|
|
53
|
+
const node = select(dom);
|
|
54
|
+
|
|
55
|
+
assert.equal(node.value, "", "the select should sit on the placeholder");
|
|
56
|
+
|
|
57
|
+
const placeholder =
|
|
58
|
+
node.querySelector<HTMLOptionElement>('option[value=""]');
|
|
59
|
+
assert.ok(placeholder, "no placeholder option was rendered");
|
|
60
|
+
assert.equal(
|
|
61
|
+
placeholder.disabled,
|
|
62
|
+
true,
|
|
63
|
+
"the placeholder option should not be a pickable account",
|
|
64
|
+
);
|
|
65
|
+
assert.notEqual(
|
|
66
|
+
placeholder.textContent,
|
|
67
|
+
ACCOUNTS[0]?.email,
|
|
68
|
+
"the placeholder must not read as the first account's address",
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("selects the resolved account and shows its address", () => {
|
|
73
|
+
const dom = mount("acc-2");
|
|
74
|
+
const node = select(dom);
|
|
75
|
+
|
|
76
|
+
assert.equal(node.value, "acc-2");
|
|
77
|
+
assert.equal(
|
|
78
|
+
node.selectedOptions[0]?.textContent,
|
|
79
|
+
"bob@example.com",
|
|
80
|
+
"the selected option should show the resolved account's address",
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("shows the placeholder when selectedAccountId names an account no longer in config", () => {
|
|
85
|
+
const dom = mount("acc-missing");
|
|
86
|
+
const node = select(dom);
|
|
87
|
+
|
|
88
|
+
assert.equal(node.value, "", "the select should sit on the placeholder");
|
|
89
|
+
|
|
90
|
+
const placeholder =
|
|
91
|
+
node.querySelector<HTMLOptionElement>('option[value=""]');
|
|
92
|
+
assert.ok(placeholder, "no placeholder option was rendered");
|
|
93
|
+
assert.notEqual(
|
|
94
|
+
node.selectedOptions[0]?.textContent,
|
|
95
|
+
ACCOUNTS[0]?.email,
|
|
96
|
+
"an unresolved selection must not read as the first account's address",
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { configOperationsGetConfigQueryKey } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
3
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
4
|
+
import { makeAccount, makeConfig } from "../../test-support/fixtures";
|
|
5
|
+
import { FromSelector } from "./FromSelector";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The compose From line (#1014). With more than one configured account it is
|
|
9
|
+
* a real picker; the unresolved state — no account chosen because the source
|
|
10
|
+
* mailbox could not be matched to one — gets a disabled placeholder option
|
|
11
|
+
* rather than silently falling back to the first account's address.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const ACCOUNTS = [
|
|
15
|
+
makeAccount({ accountId: "acc-1", email: "alice@example.com" }),
|
|
16
|
+
makeAccount({ accountId: "acc-2", email: "bob@example.com" }),
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function seededClient(): QueryClient {
|
|
20
|
+
const client = new QueryClient({
|
|
21
|
+
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
|
22
|
+
});
|
|
23
|
+
client.setQueryData(
|
|
24
|
+
configOperationsGetConfigQueryKey(),
|
|
25
|
+
makeConfig(ACCOUNTS),
|
|
26
|
+
);
|
|
27
|
+
return client;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const meta: Meta<typeof FromSelector> = {
|
|
31
|
+
title: "Components/FromSelector",
|
|
32
|
+
component: FromSelector,
|
|
33
|
+
parameters: { layout: "padded" },
|
|
34
|
+
decorators: [
|
|
35
|
+
(Story) => (
|
|
36
|
+
<QueryClientProvider client={seededClient()}>
|
|
37
|
+
<Story />
|
|
38
|
+
</QueryClientProvider>
|
|
39
|
+
),
|
|
40
|
+
],
|
|
41
|
+
args: {
|
|
42
|
+
onSelect: () => undefined,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
export default meta;
|
|
46
|
+
|
|
47
|
+
type Story = StoryObj<typeof FromSelector>;
|
|
48
|
+
|
|
49
|
+
/** A resolved account, picked from the configured accounts. */
|
|
50
|
+
export const Resolved: Story = {
|
|
51
|
+
args: { selectedAccountId: "acc-2" },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* No account resolved — the source mailbox could not be matched to a
|
|
56
|
+
* configured account. The select shows a disabled placeholder instead of
|
|
57
|
+
* falling back to the first account's address.
|
|
58
|
+
*/
|
|
59
|
+
export const Unresolved: Story = {
|
|
60
|
+
args: { selectedAccountId: undefined },
|
|
61
|
+
};
|
|
@@ -39,6 +39,8 @@ export const FromSelector = ({
|
|
|
39
39
|
);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
const resolved = accounts.some((a) => a.accountId === selectedAccountId);
|
|
43
|
+
|
|
42
44
|
return (
|
|
43
45
|
<div className="flex items-start gap-2">
|
|
44
46
|
<label
|
|
@@ -49,13 +51,18 @@ export const FromSelector = ({
|
|
|
49
51
|
</label>
|
|
50
52
|
<select
|
|
51
53
|
id="from-account-selector"
|
|
52
|
-
value={selectedAccountId
|
|
54
|
+
value={resolved ? selectedAccountId : ""}
|
|
53
55
|
onChange={(e) => {
|
|
54
56
|
const account = accounts.find((a) => a.accountId === e.target.value);
|
|
55
57
|
if (account) onSelect(account);
|
|
56
58
|
}}
|
|
57
59
|
className="flex-1 px-2 py-1.5 border rounded-md bg-canvas text-sm"
|
|
58
60
|
>
|
|
61
|
+
{!resolved && (
|
|
62
|
+
<option value="" disabled>
|
|
63
|
+
Choose an account
|
|
64
|
+
</option>
|
|
65
|
+
)}
|
|
59
66
|
{accounts.map((account) => (
|
|
60
67
|
<option key={account.accountId} value={account.accountId}>
|
|
61
68
|
{account.email}
|
|
@@ -482,9 +482,13 @@ function AccountsSettings() {
|
|
|
482
482
|
connector={isOAuthAccount ? "Microsoft 365" : "IMAP"}
|
|
483
483
|
syncLabel={deriveSyncLabel(account)}
|
|
484
484
|
state={deriveState(account)}
|
|
485
|
+
// What the mail server said, when it said anything. Not every
|
|
486
|
+
// refusal that fences an account is one the Reconnect button
|
|
487
|
+
// clears — "SmtpClientAuthentication is disabled" is a tenant
|
|
488
|
+
// setting — so the stored reason outranks the generic prompt.
|
|
485
489
|
errorDetail={
|
|
486
490
|
isReauth
|
|
487
|
-
? "Re-authentication required"
|
|
491
|
+
? (account.lastError ?? "Re-authentication required")
|
|
488
492
|
: isMissingPassword
|
|
489
493
|
? "Imported from a config file, which carries no password. Enter one to start syncing."
|
|
490
494
|
: account.lastError
|