@startsimpli/integrations 0.2.22 → 0.2.23
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": "@startsimpli/integrations",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.23",
|
|
4
4
|
"description": "Provider-agnostic types, API clients, hooks, and UI primitives for email + calendar integrations across StartSimpli apps.",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -997,3 +997,113 @@ describe('ConnectionsPage — foundry mode is untouched by personal mode', () =>
|
|
|
997
997
|
expect(within(section).getByRole('button', { name: /^Save$/i })).toBeInTheDocument();
|
|
998
998
|
});
|
|
999
999
|
});
|
|
1000
|
+
|
|
1001
|
+
// --------------------------------------------------------------------------
|
|
1002
|
+
// the members' roster: a verb only where it still does something
|
|
1003
|
+
// (bd startsim-768w.22.25.19)
|
|
1004
|
+
// --------------------------------------------------------------------------
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* One row per status, because the whole defect was a button that never read
|
|
1008
|
+
* the status. `disabled` is the row AFTER a disconnect — key deleted, tokens
|
|
1009
|
+
* revoked, fetches stopped. `error` and `needs_reauth` still hold a stored
|
|
1010
|
+
* credential and a live poll leg, and they are the rows an admin offboarding
|
|
1011
|
+
* somebody most needs to cut, so the suppression has to be narrow enough to
|
|
1012
|
+
* leave them alone.
|
|
1013
|
+
*/
|
|
1014
|
+
const MEMBER_ROWS = [
|
|
1015
|
+
{
|
|
1016
|
+
id: 'm-live', owner: { id: 'u1', email: 'ana@acme.test', name: 'Ana' },
|
|
1017
|
+
connector_key: 'n8n', connector_name: 'n8n', name: 'Ana n8n',
|
|
1018
|
+
account_label: 'ana@acme.test', status: 'connected', health: 'ok',
|
|
1019
|
+
},
|
|
1020
|
+
{
|
|
1021
|
+
id: 'm-broken', owner: { id: 'u2', email: 'bo@acme.test', name: 'Bo' },
|
|
1022
|
+
connector_key: 'granola', connector_name: 'Granola', name: 'Bo Granola',
|
|
1023
|
+
account_label: 'bo@acme.test', status: 'error', health: 'failing',
|
|
1024
|
+
check_error: 'Vendor said 401',
|
|
1025
|
+
},
|
|
1026
|
+
{
|
|
1027
|
+
id: 'm-stale', owner: { id: 'u3', email: 'cy@acme.test', name: 'Cy' },
|
|
1028
|
+
connector_key: 'granola', connector_name: 'Granola', name: 'Cy Granola',
|
|
1029
|
+
account_label: 'cy@acme.test', status: 'needs_reauth', health: 'expired',
|
|
1030
|
+
},
|
|
1031
|
+
{
|
|
1032
|
+
id: 'm-gone', owner: { id: 'u4', email: 'di@acme.test', name: 'Di' },
|
|
1033
|
+
connector_key: 'n8n', connector_name: 'n8n', name: 'Di n8n',
|
|
1034
|
+
account_label: 'di@acme.test', status: 'disabled', health: 'disconnected',
|
|
1035
|
+
},
|
|
1036
|
+
];
|
|
1037
|
+
|
|
1038
|
+
function rosterApi(overrides: Partial<ConnectionsApi> = {}): ConnectionsApi {
|
|
1039
|
+
return stubApi({
|
|
1040
|
+
memberConnections: vi.fn().mockResolvedValue({
|
|
1041
|
+
results: MEMBER_ROWS,
|
|
1042
|
+
permissions: { view: true, manage: true, issue_tokens: false },
|
|
1043
|
+
}),
|
|
1044
|
+
disconnectMemberConnection: vi.fn().mockResolvedValue(undefined),
|
|
1045
|
+
...overrides,
|
|
1046
|
+
} as Partial<ConnectionsApi>);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/** The card element for one member row, by the id the roster stamps on it. */
|
|
1050
|
+
async function memberCard(id: string): Promise<HTMLElement> {
|
|
1051
|
+
return waitFor(() => {
|
|
1052
|
+
const el = document.querySelector<HTMLElement>(`div[data-member-connection="${id}"]`);
|
|
1053
|
+
if (!el) throw new Error(`no member row rendered for ${id}`);
|
|
1054
|
+
return el;
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
describe('MemberConnectionRoster — Disconnect is not offered twice', () => {
|
|
1059
|
+
it('does NOT offer Disconnect on a row that is already disconnected', async () => {
|
|
1060
|
+
render(<ConnectionsPage api={rosterApi()} foundrySlug="acme" />);
|
|
1061
|
+
const gone = await memberCard('m-gone');
|
|
1062
|
+
expect(within(gone).queryByRole('button', { name: /Disconnect/i })).toBeNull();
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
it('says the account is gone and that only the member can bring it back', async () => {
|
|
1066
|
+
// The row STAYS. Who used to hold a pipe into this foundry is the answer an
|
|
1067
|
+
// offboarding review is looking for; hiding it would lose that.
|
|
1068
|
+
render(<ConnectionsPage api={rosterApi()} foundrySlug="acme" />);
|
|
1069
|
+
const gone = await memberCard('m-gone');
|
|
1070
|
+
const note = gone.querySelector('[data-member-connection-disconnected]');
|
|
1071
|
+
expect(note).not.toBeNull();
|
|
1072
|
+
expect(note!.textContent).toMatch(/Disconnected\./);
|
|
1073
|
+
expect(note!.textContent).toMatch(/di@acme\.test has to connect with n8n again/);
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
it('never claims a deleted key "cannot be undone"', async () => {
|
|
1077
|
+
// The exact copy the bead objects to, over a key that is already gone.
|
|
1078
|
+
render(<ConnectionsPage api={rosterApi()} foundrySlug="acme" />);
|
|
1079
|
+
const gone = await memberCard('m-gone');
|
|
1080
|
+
expect(within(gone).queryByText(/cannot be undone/i)).toBeNull();
|
|
1081
|
+
expect(within(gone).queryByText(/stored key is deleted and their/i)).toBeNull();
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
it('KEEPS Disconnect on a failing row, whose key is still stored', async () => {
|
|
1085
|
+
render(<ConnectionsPage api={rosterApi()} foundrySlug="acme" />);
|
|
1086
|
+
const broken = await memberCard('m-broken');
|
|
1087
|
+
expect(within(broken).getByRole('button', { name: /^Disconnect$/i }))
|
|
1088
|
+
.toBeInTheDocument();
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
it('KEEPS Disconnect on a needs_reauth row, whose poll leg is still live', async () => {
|
|
1092
|
+
render(<ConnectionsPage api={rosterApi()} foundrySlug="acme" />);
|
|
1093
|
+
const stale = await memberCard('m-stale');
|
|
1094
|
+
expect(within(stale).getByRole('button', { name: /^Disconnect$/i }))
|
|
1095
|
+
.toBeInTheDocument();
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
it('still cuts a connected row, naming the person before it does', async () => {
|
|
1099
|
+
const api = rosterApi();
|
|
1100
|
+
render(<ConnectionsPage api={api} foundrySlug="acme" />);
|
|
1101
|
+
const live = await memberCard('m-live');
|
|
1102
|
+
await userEvent.click(within(live).getByRole('button', { name: /^Disconnect$/i }));
|
|
1103
|
+
const prompt = within(live).getByText(/cannot be undone/i);
|
|
1104
|
+
expect(prompt.textContent).toContain('ana@acme.test');
|
|
1105
|
+
await userEvent.click(within(live).getByRole('button', { name: /Yes, disconnect/i }));
|
|
1106
|
+
await waitFor(() =>
|
|
1107
|
+
expect(api.disconnectMemberConnection).toHaveBeenCalledWith('acme', 'm-live'));
|
|
1108
|
+
});
|
|
1109
|
+
});
|
|
@@ -108,6 +108,19 @@ function MemberConnectionCard({
|
|
|
108
108
|
const [error, setError] = React.useState('');
|
|
109
109
|
const who = row.owner?.email || row.owner?.name || 'Unknown member';
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* ALREADY CUT (bd startsim-768w.22.25.19). `disabled` is the status a row
|
|
113
|
+
* carries AFTER a disconnect: the key is deleted, the tokens are revoked and
|
|
114
|
+
* the scheduled fetches are stopped. There is nothing left for this verb to
|
|
115
|
+
* do, so offering it — and then confirming with "this cannot be undone" over
|
|
116
|
+
* a key that is already gone — describes an action the server would refuse.
|
|
117
|
+
*
|
|
118
|
+
* ONLY `disabled`. `error` and `needs_reauth` still hold a stored credential
|
|
119
|
+
* and a live poll leg; they are exactly the rows an admin offboarding someone
|
|
120
|
+
* most needs to cut, so the test for this asserts they keep the button.
|
|
121
|
+
*/
|
|
122
|
+
const alreadyDisconnected = row.status === 'disabled';
|
|
123
|
+
|
|
111
124
|
async function cut() {
|
|
112
125
|
if (!disconnect) return;
|
|
113
126
|
setBusy(true);
|
|
@@ -143,8 +156,18 @@ function MemberConnectionCard({
|
|
|
143
156
|
anything the member submitted. An admin deciding whether to cut a
|
|
144
157
|
pipe is entitled to know it is already broken. */}
|
|
145
158
|
{row.check_error ? <p className={cn.error}>{row.check_error}</p> : null}
|
|
159
|
+
{/* THE ROW STAYS. Who USED to have a pipe into this foundry is the
|
|
160
|
+
answer an offboarding review is looking for, so a disconnected
|
|
161
|
+
account is not hidden — it is labelled, and its verb is gone. */}
|
|
162
|
+
{alreadyDisconnected ? (
|
|
163
|
+
<p className={cn.muted} data-member-connection-disconnected>
|
|
164
|
+
Disconnected. This account’s key was deleted and its scheduled
|
|
165
|
+
fetches stopped. Nothing here can bring it back —{' '}
|
|
166
|
+
{who} has to connect with {row.connector_name} again themselves.
|
|
167
|
+
</p>
|
|
168
|
+
) : null}
|
|
146
169
|
</div>
|
|
147
|
-
{canManage && disconnect ? (
|
|
170
|
+
{canManage && disconnect && !alreadyDisconnected ? (
|
|
148
171
|
<button
|
|
149
172
|
type="button"
|
|
150
173
|
className={cn.button}
|
|
@@ -155,7 +178,11 @@ function MemberConnectionCard({
|
|
|
155
178
|
</button>
|
|
156
179
|
) : null}
|
|
157
180
|
</div>
|
|
158
|
-
{
|
|
181
|
+
{/* `alreadyDisconnected` again, not because the button can open this today
|
|
182
|
+
but because a reload can flip the row to `disabled` under an open
|
|
183
|
+
prompt — and the one thing this prompt must never do is warn that an
|
|
184
|
+
action "cannot be undone" about a key that is already gone. */}
|
|
185
|
+
{confirming && !alreadyDisconnected ? (
|
|
159
186
|
<div className="mt-3 space-y-2">
|
|
160
187
|
{/* NAMES THE PERSON, because the cost of this button falls on them and
|
|
161
188
|
not on the admin pressing it. A generic "are you sure?" over a list
|