@meetreeve/module-team 0.0.0-stage → 0.3.0
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/README.md +89 -2
- package/dist/client/index.d.mts +141 -0
- package/dist/client/index.d.ts +141 -0
- package/dist/client/index.js +100 -0
- package/dist/client/index.mjs +74 -0
- package/dist/config/index.d.mts +159 -0
- package/dist/config/index.d.ts +159 -0
- package/dist/config/index.js +770 -0
- package/dist/config/index.mjs +744 -0
- package/dist/index.d.mts +141 -0
- package/dist/index.d.ts +141 -0
- package/dist/index.js +102 -0
- package/dist/index.mjs +74 -0
- package/package.json +70 -4
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/config/index.ts
|
|
22
|
+
var config_exports = {};
|
|
23
|
+
__export(config_exports, {
|
|
24
|
+
TeamMembersPanel: () => TeamMembersPanel
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(config_exports);
|
|
27
|
+
|
|
28
|
+
// src/config/team-members-panel.tsx
|
|
29
|
+
var import_react2 = require("react");
|
|
30
|
+
|
|
31
|
+
// src/config/modal.tsx
|
|
32
|
+
var import_react = require("react");
|
|
33
|
+
var import_react_dom = require("react-dom");
|
|
34
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
35
|
+
function Modal({
|
|
36
|
+
open,
|
|
37
|
+
onClose,
|
|
38
|
+
titleId,
|
|
39
|
+
descriptionId,
|
|
40
|
+
dismissible = true,
|
|
41
|
+
children
|
|
42
|
+
}) {
|
|
43
|
+
const panelRef = (0, import_react.useRef)(null);
|
|
44
|
+
const closeRef = (0, import_react.useRef)({ onClose, dismissible });
|
|
45
|
+
(0, import_react.useEffect)(() => {
|
|
46
|
+
closeRef.current = { onClose, dismissible };
|
|
47
|
+
});
|
|
48
|
+
(0, import_react.useEffect)(() => {
|
|
49
|
+
if (!open) return;
|
|
50
|
+
const previouslyFocused = document.activeElement;
|
|
51
|
+
const first = panelRef.current?.querySelector(
|
|
52
|
+
"input, select, textarea, button:not([disabled])"
|
|
53
|
+
);
|
|
54
|
+
first?.focus();
|
|
55
|
+
const onKeyDown = (e) => {
|
|
56
|
+
if (e.key === "Escape" && closeRef.current.dismissible) closeRef.current.onClose();
|
|
57
|
+
};
|
|
58
|
+
document.addEventListener("keydown", onKeyDown);
|
|
59
|
+
return () => {
|
|
60
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
61
|
+
previouslyFocused?.focus?.();
|
|
62
|
+
};
|
|
63
|
+
}, [open]);
|
|
64
|
+
if (!open || typeof document === "undefined") return null;
|
|
65
|
+
return (0, import_react_dom.createPortal)(
|
|
66
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
67
|
+
"div",
|
|
68
|
+
{
|
|
69
|
+
className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4",
|
|
70
|
+
onMouseDown: (e) => {
|
|
71
|
+
if (e.target === e.currentTarget && dismissible) onClose();
|
|
72
|
+
},
|
|
73
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
74
|
+
"div",
|
|
75
|
+
{
|
|
76
|
+
ref: panelRef,
|
|
77
|
+
role: "dialog",
|
|
78
|
+
"aria-modal": "true",
|
|
79
|
+
"aria-labelledby": titleId,
|
|
80
|
+
"aria-describedby": descriptionId,
|
|
81
|
+
className: "w-full max-w-md rounded-lg border border-border bg-background p-6 text-foreground shadow-lg",
|
|
82
|
+
children
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
),
|
|
87
|
+
document.body
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/config/role-rules.ts
|
|
92
|
+
var MEMBER_ROLE_RANK = {
|
|
93
|
+
viewer: 5,
|
|
94
|
+
member: 10,
|
|
95
|
+
billing_manager: 15,
|
|
96
|
+
admin: 20,
|
|
97
|
+
owner: 30
|
|
98
|
+
};
|
|
99
|
+
function memberRoleRank(role) {
|
|
100
|
+
return Object.prototype.hasOwnProperty.call(MEMBER_ROLE_RANK, role) ? MEMBER_ROLE_RANK[role] : -1;
|
|
101
|
+
}
|
|
102
|
+
function canChangeMemberRole(callerRole, targetRole, requestedRole) {
|
|
103
|
+
if (callerRole !== "owner" && callerRole !== "admin") return false;
|
|
104
|
+
if (memberRoleRank(requestedRole) < 0) return false;
|
|
105
|
+
if (requestedRole === "owner" && callerRole !== "owner") return false;
|
|
106
|
+
const caller = memberRoleRank(callerRole);
|
|
107
|
+
const target = memberRoleRank(targetRole);
|
|
108
|
+
const requested = memberRoleRank(requestedRole);
|
|
109
|
+
return callerRole === "owner" ? caller >= target && caller >= requested : caller > target && caller > requested;
|
|
110
|
+
}
|
|
111
|
+
function grantableRoles(callerRole, targetRole, offered) {
|
|
112
|
+
return offered.filter((role) => role !== targetRole && canChangeMemberRole(callerRole, targetRole, role));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/config/team-members-panel.tsx
|
|
116
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
117
|
+
var DEFAULT_INVITE_ROLES = ["admin", "member", "viewer"];
|
|
118
|
+
var ROLE_LABELS = {
|
|
119
|
+
owner: "Owner",
|
|
120
|
+
admin: "Admin",
|
|
121
|
+
billing_manager: "Billing",
|
|
122
|
+
member: "Member",
|
|
123
|
+
viewer: "Viewer"
|
|
124
|
+
};
|
|
125
|
+
var INVITE_ROLE_OPTIONS = {
|
|
126
|
+
admin: {
|
|
127
|
+
label: "Admin \u2014 Can manage the team",
|
|
128
|
+
description: "Can manage team members, billing, and all settings"
|
|
129
|
+
},
|
|
130
|
+
member: {
|
|
131
|
+
label: "Member \u2014 Full agent access",
|
|
132
|
+
description: "Can use all agents and connectors, but can't manage team"
|
|
133
|
+
},
|
|
134
|
+
viewer: {
|
|
135
|
+
label: "Viewer \u2014 Read-only dashboards",
|
|
136
|
+
description: "Read-only access to dashboards and reports"
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
function roleLabel(role) {
|
|
140
|
+
return ROLE_LABELS[role] ?? role;
|
|
141
|
+
}
|
|
142
|
+
function nameFromEmail(email) {
|
|
143
|
+
if (!email) return "Team Member";
|
|
144
|
+
return email.split("@")[0].replace(/[._-]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
145
|
+
}
|
|
146
|
+
function formatDate(iso) {
|
|
147
|
+
const d = new Date(iso);
|
|
148
|
+
return Number.isNaN(d.getTime()) ? "\u2014" : d.toLocaleDateString();
|
|
149
|
+
}
|
|
150
|
+
function formatAgo(iso) {
|
|
151
|
+
const d = new Date(iso);
|
|
152
|
+
if (Number.isNaN(d.getTime())) return "\u2014";
|
|
153
|
+
const diff = Date.now() - d.getTime();
|
|
154
|
+
const mins = Math.floor(diff / 6e4);
|
|
155
|
+
const hours = Math.floor(diff / 36e5);
|
|
156
|
+
const days = Math.floor(diff / 864e5);
|
|
157
|
+
if (mins < 1) return "just now";
|
|
158
|
+
if (mins < 60) return `${mins}m ago`;
|
|
159
|
+
if (hours < 24) return `${hours}h ago`;
|
|
160
|
+
if (days < 7) return `${days}d ago`;
|
|
161
|
+
return d.toLocaleDateString();
|
|
162
|
+
}
|
|
163
|
+
function errorInfo(err, fallback) {
|
|
164
|
+
const e = typeof err === "object" && err !== null ? err : {};
|
|
165
|
+
const raw = e.body?.detail;
|
|
166
|
+
const nested = typeof raw === "object" && raw !== null ? raw.message : void 0;
|
|
167
|
+
const detail = typeof raw === "string" ? raw : typeof nested === "string" ? nested : "";
|
|
168
|
+
const message = typeof e.message === "string" ? e.message : "";
|
|
169
|
+
const status = typeof e.status === "number" ? e.status : Number(/^(?:API error )?(\d{3})\b/.exec(message)?.[1]) || null;
|
|
170
|
+
return { status, message: detail || message || fallback };
|
|
171
|
+
}
|
|
172
|
+
function serverDetail(err) {
|
|
173
|
+
const body = typeof err === "object" && err !== null ? err.body : null;
|
|
174
|
+
const detail = typeof body === "object" && body !== null ? body.detail : null;
|
|
175
|
+
return typeof detail === "string" && detail.trim() !== "" ? detail : null;
|
|
176
|
+
}
|
|
177
|
+
function plural(n, one, many) {
|
|
178
|
+
return `${n} ${n === 1 ? one : many}`;
|
|
179
|
+
}
|
|
180
|
+
var BUTTON = "inline-flex items-center justify-center rounded-md px-3 py-1.5 text-sm font-medium disabled:pointer-events-none disabled:opacity-50";
|
|
181
|
+
var BUTTON_PRIMARY = `${BUTTON} bg-primary text-primary-foreground hover:bg-primary/90`;
|
|
182
|
+
var BUTTON_OUTLINE = `${BUTTON} border border-border bg-background hover:bg-muted`;
|
|
183
|
+
var BUTTON_DESTRUCTIVE = `${BUTTON} bg-destructive text-white hover:bg-destructive/90`;
|
|
184
|
+
var BUTTON_GHOST = `${BUTTON} text-muted-foreground hover:bg-muted hover:text-foreground`;
|
|
185
|
+
var CONTROL = "rounded-md border border-border bg-background text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50";
|
|
186
|
+
var FIELD = `${CONTROL} w-full px-3 py-2`;
|
|
187
|
+
var BADGE = "inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium";
|
|
188
|
+
function roleBadgeClass(role) {
|
|
189
|
+
return role === "owner" || role === "admin" ? `${BADGE} border-primary/30 bg-primary/15 text-foreground` : `${BADGE} border-border bg-muted text-muted-foreground`;
|
|
190
|
+
}
|
|
191
|
+
function TeamMembersPanel(props) {
|
|
192
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(TeamMembersPanelBody, { ...props }, props.orgId);
|
|
193
|
+
}
|
|
194
|
+
function TeamMembersPanelBody({
|
|
195
|
+
client,
|
|
196
|
+
orgId,
|
|
197
|
+
inviteRoles = DEFAULT_INVITE_ROLES,
|
|
198
|
+
currentUserEmail,
|
|
199
|
+
renderMemberActions,
|
|
200
|
+
renderInvitationActions,
|
|
201
|
+
loadSpaceOptions,
|
|
202
|
+
onNotice
|
|
203
|
+
}) {
|
|
204
|
+
const ids = (0, import_react2.useId)();
|
|
205
|
+
const roleOptions = DEFAULT_INVITE_ROLES.filter((r) => inviteRoles.includes(r));
|
|
206
|
+
const defaultInviteRole = roleOptions.includes("member") ? "member" : roleOptions[0] ?? "member";
|
|
207
|
+
const [state, setState] = (0, import_react2.useState)({ status: "loading" });
|
|
208
|
+
const [notice, setNotice] = (0, import_react2.useState)(null);
|
|
209
|
+
const [busy, setBusy] = (0, import_react2.useState)({});
|
|
210
|
+
const [inviteOpen, setInviteOpen] = (0, import_react2.useState)(false);
|
|
211
|
+
const [inviteEmail, setInviteEmail] = (0, import_react2.useState)("");
|
|
212
|
+
const [inviteRole, setInviteRole] = (0, import_react2.useState)(defaultInviteRole);
|
|
213
|
+
const [inviteSubmitting, setInviteSubmitting] = (0, import_react2.useState)(false);
|
|
214
|
+
const [memberToRemove, setMemberToRemove] = (0, import_react2.useState)(null);
|
|
215
|
+
const [spaceOptions, setSpaceOptions] = (0, import_react2.useState)({ status: "loading" });
|
|
216
|
+
const [inviteSpaceIds, setInviteSpaceIds] = (0, import_react2.useState)([]);
|
|
217
|
+
const [inviteError, setInviteError] = (0, import_react2.useState)(null);
|
|
218
|
+
const mounted = (0, import_react2.useRef)(true);
|
|
219
|
+
const generation = (0, import_react2.useRef)(0);
|
|
220
|
+
const notify = (0, import_react2.useCallback)(
|
|
221
|
+
(next) => {
|
|
222
|
+
if (!mounted.current) return;
|
|
223
|
+
setNotice(next);
|
|
224
|
+
onNotice?.(next);
|
|
225
|
+
},
|
|
226
|
+
[onNotice]
|
|
227
|
+
);
|
|
228
|
+
const load = (0, import_react2.useCallback)(async () => {
|
|
229
|
+
const mine = ++generation.current;
|
|
230
|
+
try {
|
|
231
|
+
const [members, invitations] = await Promise.all([
|
|
232
|
+
client.listMembers(orgId),
|
|
233
|
+
client.listInvitations(orgId)
|
|
234
|
+
]);
|
|
235
|
+
if (!mounted.current || mine !== generation.current) return;
|
|
236
|
+
setState({
|
|
237
|
+
status: "ready",
|
|
238
|
+
members,
|
|
239
|
+
invitations: invitations.filter((inv) => inv.status === "pending")
|
|
240
|
+
});
|
|
241
|
+
} catch (err) {
|
|
242
|
+
if (!mounted.current || mine !== generation.current) return;
|
|
243
|
+
setState({ status: "error", message: errorInfo(err, "Failed to load team data").message });
|
|
244
|
+
}
|
|
245
|
+
}, [client, orgId]);
|
|
246
|
+
(0, import_react2.useEffect)(() => {
|
|
247
|
+
mounted.current = true;
|
|
248
|
+
void load();
|
|
249
|
+
return () => {
|
|
250
|
+
mounted.current = false;
|
|
251
|
+
};
|
|
252
|
+
}, [load]);
|
|
253
|
+
(0, import_react2.useEffect)(() => {
|
|
254
|
+
if (!inviteOpen || !loadSpaceOptions) return;
|
|
255
|
+
let live = true;
|
|
256
|
+
setSpaceOptions({ status: "loading" });
|
|
257
|
+
loadSpaceOptions().then(
|
|
258
|
+
(options) => {
|
|
259
|
+
if (live && mounted.current) setSpaceOptions({ status: "ready", options });
|
|
260
|
+
},
|
|
261
|
+
() => {
|
|
262
|
+
if (live && mounted.current) setSpaceOptions({ status: "error" });
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
return () => {
|
|
266
|
+
live = false;
|
|
267
|
+
};
|
|
268
|
+
}, [inviteOpen, loadSpaceOptions]);
|
|
269
|
+
const setBusyFor = (key, value) => {
|
|
270
|
+
if (mounted.current) setBusy((b) => ({ ...b, [key]: value }));
|
|
271
|
+
};
|
|
272
|
+
const updateReady = (fn) => {
|
|
273
|
+
if (!mounted.current) return;
|
|
274
|
+
setState((s) => s.status === "ready" ? fn(s) : s);
|
|
275
|
+
};
|
|
276
|
+
const closeInvite = () => {
|
|
277
|
+
setInviteSpaceIds([]);
|
|
278
|
+
setInviteError(null);
|
|
279
|
+
setInviteOpen(false);
|
|
280
|
+
};
|
|
281
|
+
const offered = spaceOptions.status === "ready" ? spaceOptions.options : null;
|
|
282
|
+
const pickRequired = offered !== null && offered.length > 1;
|
|
283
|
+
const pickedSpaces = (offered ?? []).filter((o) => o.can_manage && inviteSpaceIds.includes(o.id)).map((o) => ({ space_id: o.id, role: "member" }));
|
|
284
|
+
const inviteSpacesChanged = offered !== null && pickRequired && inviteSpaceIds.some((id) => !offered.some((o) => o.id === id && o.can_manage));
|
|
285
|
+
const inviteSpacesBlocked = loadSpaceOptions !== void 0 && (offered === null || inviteSpacesChanged || pickRequired && pickedSpaces.length === 0);
|
|
286
|
+
const handleInvite = async () => {
|
|
287
|
+
const email = inviteEmail.trim();
|
|
288
|
+
if (!email || inviteSubmitting || inviteSpacesBlocked) return;
|
|
289
|
+
setInviteSubmitting(true);
|
|
290
|
+
setInviteError(null);
|
|
291
|
+
try {
|
|
292
|
+
if (loadSpaceOptions) {
|
|
293
|
+
await client.inviteMember(orgId, email, inviteRole, pickRequired ? pickedSpaces : []);
|
|
294
|
+
} else {
|
|
295
|
+
await client.inviteMember(orgId, email, inviteRole);
|
|
296
|
+
}
|
|
297
|
+
if (!mounted.current) return;
|
|
298
|
+
notify({ kind: "success", message: `Invitation sent to ${email}` });
|
|
299
|
+
setInviteEmail("");
|
|
300
|
+
setInviteRole(defaultInviteRole);
|
|
301
|
+
closeInvite();
|
|
302
|
+
await load();
|
|
303
|
+
} catch (err) {
|
|
304
|
+
const { status, message } = errorInfo(err, "Failed to send invitation");
|
|
305
|
+
const shown = status === 409 ? "A pending invitation already exists for this email" : message;
|
|
306
|
+
if (mounted.current) setInviteError(shown);
|
|
307
|
+
notify({ kind: "error", message: shown });
|
|
308
|
+
} finally {
|
|
309
|
+
if (mounted.current) setInviteSubmitting(false);
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
const handleRevoke = async (inv) => {
|
|
313
|
+
const key = `inv:${inv.id}`;
|
|
314
|
+
setBusyFor(key, true);
|
|
315
|
+
try {
|
|
316
|
+
await client.revokeInvitation(orgId, inv.id);
|
|
317
|
+
notify({ kind: "success", message: "Invitation revoked" });
|
|
318
|
+
updateReady((s) => ({ ...s, invitations: s.invitations.filter((i) => i.id !== inv.id) }));
|
|
319
|
+
} catch {
|
|
320
|
+
notify({ kind: "error", message: "Failed to revoke invitation" });
|
|
321
|
+
} finally {
|
|
322
|
+
setBusyFor(key, false);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
const handleResend = async (inv) => {
|
|
326
|
+
const key = `inv:${inv.id}`;
|
|
327
|
+
setBusyFor(key, true);
|
|
328
|
+
try {
|
|
329
|
+
await client.resendInvitation(orgId, inv.id);
|
|
330
|
+
notify({ kind: "success", message: "Invitation resent \u2014 7-day expiry reset" });
|
|
331
|
+
if (mounted.current) await load();
|
|
332
|
+
} catch {
|
|
333
|
+
notify({ kind: "error", message: "Failed to resend invitation" });
|
|
334
|
+
} finally {
|
|
335
|
+
setBusyFor(key, false);
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
const handleRoleChange = async (member, role) => {
|
|
339
|
+
if (role === member.role) return;
|
|
340
|
+
const key = `member:${member.id}`;
|
|
341
|
+
setBusyFor(key, true);
|
|
342
|
+
try {
|
|
343
|
+
await client.updateMemberRole(orgId, member.id, role);
|
|
344
|
+
notify({ kind: "success", message: `Role updated to ${roleLabel(role)}` });
|
|
345
|
+
updateReady((s) => ({
|
|
346
|
+
...s,
|
|
347
|
+
members: s.members.map((m) => m.id === member.id ? { ...m, role } : m)
|
|
348
|
+
}));
|
|
349
|
+
} catch (err) {
|
|
350
|
+
notify({ kind: "error", message: serverDetail(err) ?? "Failed to update role" });
|
|
351
|
+
} finally {
|
|
352
|
+
setBusyFor(key, false);
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
const handleRemove = async () => {
|
|
356
|
+
const member = memberToRemove;
|
|
357
|
+
if (!member) return;
|
|
358
|
+
const key = `member:${member.id}`;
|
|
359
|
+
setBusyFor(key, true);
|
|
360
|
+
try {
|
|
361
|
+
await client.removeMember(orgId, member.id);
|
|
362
|
+
notify({ kind: "success", message: "Member removed from the team" });
|
|
363
|
+
updateReady((s) => ({ ...s, members: s.members.filter((m) => m.id !== member.id) }));
|
|
364
|
+
if (mounted.current) setMemberToRemove(null);
|
|
365
|
+
} catch (err) {
|
|
366
|
+
const { message } = errorInfo(err, "Failed to remove member");
|
|
367
|
+
notify({
|
|
368
|
+
kind: "error",
|
|
369
|
+
message: /last owner/i.test(message) ? "Can't remove the last owner" : message
|
|
370
|
+
});
|
|
371
|
+
} finally {
|
|
372
|
+
setBusyFor(key, false);
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
const ready = state.status === "ready" ? state : null;
|
|
376
|
+
const me = currentUserEmail?.toLowerCase() ?? null;
|
|
377
|
+
const callerRole = me !== null && ready?.members.find((m) => m.email?.toLowerCase() === me)?.role || null;
|
|
378
|
+
const inviteTitleId = `${ids}-invite-title`;
|
|
379
|
+
const inviteDescId = `${ids}-invite-desc`;
|
|
380
|
+
const removeTitleId = `${ids}-remove-title`;
|
|
381
|
+
const removeDescId = `${ids}-remove-desc`;
|
|
382
|
+
const removeBusy = memberToRemove ? Boolean(busy[`member:${memberToRemove.id}`]) : false;
|
|
383
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "space-y-6", "data-testid": "team-members-panel", "data-org-id": orgId, children: [
|
|
384
|
+
notice ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
385
|
+
"p",
|
|
386
|
+
{
|
|
387
|
+
role: notice.kind === "error" ? "alert" : "status",
|
|
388
|
+
"data-testid": "team-notice",
|
|
389
|
+
className: `rounded-md border px-3 py-2 text-sm ${notice.kind === "error" ? "border-destructive/30 bg-destructive/10 text-destructive" : "border-border bg-muted text-foreground"}`,
|
|
390
|
+
children: notice.message
|
|
391
|
+
}
|
|
392
|
+
) : null,
|
|
393
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
394
|
+
"section",
|
|
395
|
+
{
|
|
396
|
+
"aria-labelledby": `${ids}-members-title`,
|
|
397
|
+
className: "rounded-lg border border-border bg-card text-card-foreground",
|
|
398
|
+
children: [
|
|
399
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("header", { className: "flex flex-wrap items-center justify-between gap-3 border-b border-border p-4", children: [
|
|
400
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
|
|
401
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("h3", { id: `${ids}-members-title`, className: "text-base font-semibold", children: [
|
|
402
|
+
"Team Members",
|
|
403
|
+
ready ? ` (${ready.members.length})` : ""
|
|
404
|
+
] }),
|
|
405
|
+
ready ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { "data-testid": "team-seat-count", className: "text-xs text-muted-foreground", children: [
|
|
406
|
+
plural(ready.members.length, "seat", "seats"),
|
|
407
|
+
" in use",
|
|
408
|
+
ready.invitations.length > 0 ? ` \xB7 ${plural(ready.invitations.length, "invitation", "invitations")} pending` : ""
|
|
409
|
+
] }) : null
|
|
410
|
+
] }),
|
|
411
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
412
|
+
"button",
|
|
413
|
+
{
|
|
414
|
+
type: "button",
|
|
415
|
+
className: BUTTON_PRIMARY,
|
|
416
|
+
disabled: roleOptions.length === 0,
|
|
417
|
+
onClick: () => setInviteOpen(true),
|
|
418
|
+
children: "Invite Member"
|
|
419
|
+
}
|
|
420
|
+
)
|
|
421
|
+
] }),
|
|
422
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "p-4", children: [
|
|
423
|
+
state.status === "loading" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { "data-testid": "team-loading", className: "py-6 text-center text-sm text-muted-foreground", children: "Loading team\u2026" }) : null,
|
|
424
|
+
state.status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { role: "alert", "data-testid": "team-load-error", className: "space-y-3 py-4 text-center", children: [
|
|
425
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { className: "text-sm text-destructive", children: [
|
|
426
|
+
"Couldn't load this team: ",
|
|
427
|
+
state.message
|
|
428
|
+
] }),
|
|
429
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
430
|
+
"button",
|
|
431
|
+
{
|
|
432
|
+
type: "button",
|
|
433
|
+
className: BUTTON_OUTLINE,
|
|
434
|
+
onClick: () => {
|
|
435
|
+
setState({ status: "loading" });
|
|
436
|
+
void load();
|
|
437
|
+
},
|
|
438
|
+
children: "Retry"
|
|
439
|
+
}
|
|
440
|
+
)
|
|
441
|
+
] }) : null,
|
|
442
|
+
ready && ready.members.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "py-6 text-center text-sm text-muted-foreground", children: "No members yet \u2014 invite someone to get started." }) : null,
|
|
443
|
+
ready && ready.members.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ul", { className: "divide-y divide-border", children: ready.members.map((member) => {
|
|
444
|
+
const name = nameFromEmail(member.email);
|
|
445
|
+
const isMe = me !== null && member.email?.toLowerCase() === me;
|
|
446
|
+
const isOwner = member.role === "owner";
|
|
447
|
+
const rowBusy = Boolean(busy[`member:${member.id}`]);
|
|
448
|
+
const grantable = grantableRoles(callerRole, member.role, roleOptions);
|
|
449
|
+
const pickerRoles = roleOptions.includes(member.role) ? roleOptions.filter((r) => r === member.role || grantable.includes(r)) : [member.role, ...grantable];
|
|
450
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
451
|
+
"li",
|
|
452
|
+
{
|
|
453
|
+
"data-testid": `member-row-${member.id}`,
|
|
454
|
+
className: "flex flex-wrap items-center justify-between gap-3 py-3",
|
|
455
|
+
children: [
|
|
456
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex min-w-0 items-center gap-3", children: [
|
|
457
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
458
|
+
"span",
|
|
459
|
+
{
|
|
460
|
+
"aria-hidden": "true",
|
|
461
|
+
className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary/15 text-xs font-medium text-foreground",
|
|
462
|
+
children: name.slice(0, 2).toUpperCase()
|
|
463
|
+
}
|
|
464
|
+
),
|
|
465
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "min-w-0", children: [
|
|
466
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { className: "flex items-center gap-2 text-sm font-medium", children: [
|
|
467
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "truncate", children: name }),
|
|
468
|
+
isMe ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "rounded-full bg-primary/15 px-1.5 py-0.5 text-[10px] text-foreground", children: "you" }) : null
|
|
469
|
+
] }),
|
|
470
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { className: "truncate text-xs text-muted-foreground", children: [
|
|
471
|
+
member.email ?? "No email on record",
|
|
472
|
+
" \xB7 joined ",
|
|
473
|
+
formatDate(member.created_at)
|
|
474
|
+
] })
|
|
475
|
+
] })
|
|
476
|
+
] }),
|
|
477
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex shrink-0 flex-wrap items-center gap-2", children: [
|
|
478
|
+
isOwner ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: roleBadgeClass(member.role), children: roleLabel(member.role) }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
479
|
+
grantable.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: roleBadgeClass(member.role), children: roleLabel(member.role) }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
480
|
+
"select",
|
|
481
|
+
{
|
|
482
|
+
"aria-label": `Role for ${name}`,
|
|
483
|
+
className: `${CONTROL} px-2 py-1`,
|
|
484
|
+
value: member.role,
|
|
485
|
+
disabled: rowBusy,
|
|
486
|
+
onChange: (e) => void handleRoleChange(member, e.target.value),
|
|
487
|
+
children: pickerRoles.map((role) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: role, children: roleLabel(role) }, role))
|
|
488
|
+
}
|
|
489
|
+
),
|
|
490
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
491
|
+
"button",
|
|
492
|
+
{
|
|
493
|
+
type: "button",
|
|
494
|
+
className: BUTTON_GHOST,
|
|
495
|
+
disabled: rowBusy,
|
|
496
|
+
onClick: () => setMemberToRemove(member),
|
|
497
|
+
children: "Remove"
|
|
498
|
+
}
|
|
499
|
+
)
|
|
500
|
+
] }),
|
|
501
|
+
renderMemberActions?.(member)
|
|
502
|
+
] })
|
|
503
|
+
]
|
|
504
|
+
},
|
|
505
|
+
member.id
|
|
506
|
+
);
|
|
507
|
+
}) }) : null
|
|
508
|
+
] })
|
|
509
|
+
]
|
|
510
|
+
}
|
|
511
|
+
),
|
|
512
|
+
ready && ready.invitations.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
513
|
+
"section",
|
|
514
|
+
{
|
|
515
|
+
"aria-labelledby": `${ids}-invites-title`,
|
|
516
|
+
className: "rounded-lg border border-border bg-card text-card-foreground",
|
|
517
|
+
children: [
|
|
518
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("header", { className: "border-b border-border p-4", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("h3", { id: `${ids}-invites-title`, className: "text-base font-semibold", children: [
|
|
519
|
+
"Pending Invitations (",
|
|
520
|
+
ready.invitations.length,
|
|
521
|
+
")"
|
|
522
|
+
] }) }),
|
|
523
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ul", { className: "divide-y divide-border p-4", children: ready.invitations.map((inv) => {
|
|
524
|
+
const invBusy = Boolean(busy[`inv:${inv.id}`]);
|
|
525
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
526
|
+
"li",
|
|
527
|
+
{
|
|
528
|
+
"data-testid": `invitation-row-${inv.id}`,
|
|
529
|
+
className: "flex flex-wrap items-center justify-between gap-3 py-3",
|
|
530
|
+
children: [
|
|
531
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "min-w-0", children: [
|
|
532
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "truncate text-sm font-medium", children: inv.email }),
|
|
533
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { className: "text-xs text-muted-foreground", children: [
|
|
534
|
+
"invited ",
|
|
535
|
+
formatAgo(inv.created_at),
|
|
536
|
+
" by ",
|
|
537
|
+
inv.invited_by_email ?? "a team member",
|
|
538
|
+
" \xB7 expires ",
|
|
539
|
+
formatDate(inv.expires_at)
|
|
540
|
+
] }),
|
|
541
|
+
inv.spaces && inv.spaces.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { className: "truncate text-xs text-muted-foreground", children: [
|
|
542
|
+
"Spaces: ",
|
|
543
|
+
inv.spaces.map((sp) => sp.name).join(", ")
|
|
544
|
+
] }) : null
|
|
545
|
+
] }),
|
|
546
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex shrink-0 items-center gap-2", children: [
|
|
547
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: roleBadgeClass(inv.role), children: roleLabel(inv.role) }),
|
|
548
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
549
|
+
"button",
|
|
550
|
+
{
|
|
551
|
+
type: "button",
|
|
552
|
+
className: BUTTON_GHOST,
|
|
553
|
+
disabled: invBusy,
|
|
554
|
+
"aria-label": `Resend invitation to ${inv.email}`,
|
|
555
|
+
onClick: () => void handleResend(inv),
|
|
556
|
+
children: "Resend"
|
|
557
|
+
}
|
|
558
|
+
),
|
|
559
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
560
|
+
"button",
|
|
561
|
+
{
|
|
562
|
+
type: "button",
|
|
563
|
+
className: BUTTON_GHOST,
|
|
564
|
+
disabled: invBusy,
|
|
565
|
+
"aria-label": `Revoke invitation to ${inv.email}`,
|
|
566
|
+
onClick: () => void handleRevoke(inv),
|
|
567
|
+
children: "Revoke"
|
|
568
|
+
}
|
|
569
|
+
),
|
|
570
|
+
renderInvitationActions?.(inv, { reload: load })
|
|
571
|
+
] })
|
|
572
|
+
]
|
|
573
|
+
},
|
|
574
|
+
inv.id
|
|
575
|
+
);
|
|
576
|
+
}) })
|
|
577
|
+
]
|
|
578
|
+
}
|
|
579
|
+
) : null,
|
|
580
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
581
|
+
Modal,
|
|
582
|
+
{
|
|
583
|
+
open: inviteOpen,
|
|
584
|
+
onClose: closeInvite,
|
|
585
|
+
titleId: inviteTitleId,
|
|
586
|
+
descriptionId: inviteDescId,
|
|
587
|
+
dismissible: !inviteSubmitting,
|
|
588
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
589
|
+
"form",
|
|
590
|
+
{
|
|
591
|
+
className: "space-y-4",
|
|
592
|
+
onSubmit: (e) => {
|
|
593
|
+
e.preventDefault();
|
|
594
|
+
void handleInvite();
|
|
595
|
+
},
|
|
596
|
+
children: [
|
|
597
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "space-y-1", children: [
|
|
598
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { id: inviteTitleId, className: "text-lg font-semibold", children: "Invite a Team Member" }),
|
|
599
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { id: inviteDescId, className: "text-sm text-muted-foreground", children: "Send an invitation to join your team. They'll receive an email with a magic link." })
|
|
600
|
+
] }),
|
|
601
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "space-y-2", children: [
|
|
602
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { htmlFor: `${ids}-invite-email`, className: "text-sm font-medium", children: "Email Address" }),
|
|
603
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
604
|
+
"input",
|
|
605
|
+
{
|
|
606
|
+
id: `${ids}-invite-email`,
|
|
607
|
+
type: "email",
|
|
608
|
+
placeholder: "teammate@company.com",
|
|
609
|
+
className: FIELD,
|
|
610
|
+
value: inviteEmail,
|
|
611
|
+
disabled: inviteSubmitting,
|
|
612
|
+
onChange: (e) => setInviteEmail(e.target.value)
|
|
613
|
+
}
|
|
614
|
+
)
|
|
615
|
+
] }),
|
|
616
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "space-y-2", children: [
|
|
617
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { htmlFor: `${ids}-invite-role`, className: "text-sm font-medium", children: "Role" }),
|
|
618
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
619
|
+
"select",
|
|
620
|
+
{
|
|
621
|
+
id: `${ids}-invite-role`,
|
|
622
|
+
className: FIELD,
|
|
623
|
+
value: inviteRole,
|
|
624
|
+
disabled: inviteSubmitting,
|
|
625
|
+
onChange: (e) => setInviteRole(e.target.value),
|
|
626
|
+
children: roleOptions.map((role) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: role, children: INVITE_ROLE_OPTIONS[role].label }, role))
|
|
627
|
+
}
|
|
628
|
+
),
|
|
629
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "text-xs text-muted-foreground", children: INVITE_ROLE_OPTIONS[inviteRole]?.description })
|
|
630
|
+
] }),
|
|
631
|
+
inviteError ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { role: "alert", className: "text-sm text-destructive", children: inviteError }) : null,
|
|
632
|
+
loadSpaceOptions ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
633
|
+
InviteSpacesPicker,
|
|
634
|
+
{
|
|
635
|
+
state: spaceOptions,
|
|
636
|
+
selected: inviteSpaceIds,
|
|
637
|
+
selectionChanged: inviteSpacesChanged,
|
|
638
|
+
disabled: inviteSubmitting,
|
|
639
|
+
onToggle: (id, checked) => setInviteSpaceIds(
|
|
640
|
+
(prev) => checked ? [...prev.filter((x) => x !== id), id] : prev.filter((x) => x !== id)
|
|
641
|
+
)
|
|
642
|
+
}
|
|
643
|
+
) : null,
|
|
644
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex justify-end gap-2", children: [
|
|
645
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
646
|
+
"button",
|
|
647
|
+
{
|
|
648
|
+
type: "button",
|
|
649
|
+
className: BUTTON_OUTLINE,
|
|
650
|
+
disabled: inviteSubmitting,
|
|
651
|
+
onClick: closeInvite,
|
|
652
|
+
children: "Cancel"
|
|
653
|
+
}
|
|
654
|
+
),
|
|
655
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
656
|
+
"button",
|
|
657
|
+
{
|
|
658
|
+
type: "submit",
|
|
659
|
+
className: BUTTON_PRIMARY,
|
|
660
|
+
disabled: !inviteEmail.trim() || inviteSubmitting || inviteSpacesBlocked,
|
|
661
|
+
children: inviteSubmitting ? "Sending\u2026" : "Send Invitation"
|
|
662
|
+
}
|
|
663
|
+
)
|
|
664
|
+
] })
|
|
665
|
+
]
|
|
666
|
+
}
|
|
667
|
+
)
|
|
668
|
+
}
|
|
669
|
+
),
|
|
670
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
671
|
+
Modal,
|
|
672
|
+
{
|
|
673
|
+
open: memberToRemove !== null,
|
|
674
|
+
onClose: () => setMemberToRemove(null),
|
|
675
|
+
titleId: removeTitleId,
|
|
676
|
+
descriptionId: removeDescId,
|
|
677
|
+
dismissible: !removeBusy,
|
|
678
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "space-y-4", children: [
|
|
679
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "space-y-1", children: [
|
|
680
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { id: removeTitleId, className: "text-lg font-semibold", children: "Remove Team Member" }),
|
|
681
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { id: removeDescId, className: "text-sm text-muted-foreground", children: [
|
|
682
|
+
"Are you sure you want to remove",
|
|
683
|
+
" ",
|
|
684
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-medium text-foreground", children: memberToRemove ? nameFromEmail(memberToRemove.email) : "" }),
|
|
685
|
+
" ",
|
|
686
|
+
"from the team? They will lose access to all team resources."
|
|
687
|
+
] })
|
|
688
|
+
] }),
|
|
689
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex justify-end gap-2", children: [
|
|
690
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
691
|
+
"button",
|
|
692
|
+
{
|
|
693
|
+
type: "button",
|
|
694
|
+
className: BUTTON_OUTLINE,
|
|
695
|
+
disabled: removeBusy,
|
|
696
|
+
onClick: () => setMemberToRemove(null),
|
|
697
|
+
children: "Cancel"
|
|
698
|
+
}
|
|
699
|
+
),
|
|
700
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
701
|
+
"button",
|
|
702
|
+
{
|
|
703
|
+
type: "button",
|
|
704
|
+
className: BUTTON_DESTRUCTIVE,
|
|
705
|
+
disabled: removeBusy,
|
|
706
|
+
onClick: () => void handleRemove(),
|
|
707
|
+
children: "Remove Member"
|
|
708
|
+
}
|
|
709
|
+
)
|
|
710
|
+
] })
|
|
711
|
+
] })
|
|
712
|
+
}
|
|
713
|
+
)
|
|
714
|
+
] });
|
|
715
|
+
}
|
|
716
|
+
function InviteSpacesPicker({
|
|
717
|
+
state,
|
|
718
|
+
selected,
|
|
719
|
+
selectionChanged,
|
|
720
|
+
disabled,
|
|
721
|
+
onToggle
|
|
722
|
+
}) {
|
|
723
|
+
const ids = (0, import_react2.useId)();
|
|
724
|
+
if (state.status === "loading") {
|
|
725
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "text-sm text-muted-foreground", children: "Loading spaces\u2026" });
|
|
726
|
+
}
|
|
727
|
+
if (state.status === "error") {
|
|
728
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "text-sm text-destructive", children: selected.length > 0 ? "Couldn\u2019t load spaces, so the checked ones can\u2019t be sent. Cancel and reopen to retry." : "Couldn\u2019t load spaces, so the invite can\u2019t be sent yet. Cancel and reopen to retry." });
|
|
729
|
+
}
|
|
730
|
+
const { options } = state;
|
|
731
|
+
if (options.length === 1) {
|
|
732
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { className: "text-sm text-muted-foreground", children: [
|
|
733
|
+
"They\u2019ll get access to ",
|
|
734
|
+
options[0].name,
|
|
735
|
+
"."
|
|
736
|
+
] });
|
|
737
|
+
}
|
|
738
|
+
if (options.length === 0) return null;
|
|
739
|
+
const picked = options.some((o) => o.can_manage && selected.includes(o.id));
|
|
740
|
+
const manageable = options.some((o) => o.can_manage);
|
|
741
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("fieldset", { className: "space-y-2", disabled, "aria-labelledby": `${ids}-legend`, children: [
|
|
742
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("legend", { id: `${ids}-legend`, className: "text-sm font-medium", children: "Spaces" }),
|
|
743
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "text-xs text-muted-foreground", children: "The spaces they\u2019ll join." }),
|
|
744
|
+
selectionChanged ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "text-sm text-destructive", children: "A checked space changed or is no longer manageable. Cancel and reopen to review the assignment before sending." }) : !manageable ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "text-sm text-destructive", children: "You can\u2019t add people to any of these spaces. Ask an org owner or admin to send this invite." }) : !picked ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "text-sm text-muted-foreground", children: "Pick at least one space." }) : null,
|
|
745
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ul", { className: "max-h-48 divide-y divide-border overflow-y-auto rounded-md border border-border", children: options.map((o) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
746
|
+
"label",
|
|
747
|
+
{
|
|
748
|
+
className: `flex items-center gap-3 px-3 py-2 text-sm ${o.can_manage ? "cursor-pointer" : "cursor-not-allowed text-muted-foreground"}`,
|
|
749
|
+
children: [
|
|
750
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
751
|
+
"input",
|
|
752
|
+
{
|
|
753
|
+
type: "checkbox",
|
|
754
|
+
className: "accent-foreground",
|
|
755
|
+
checked: o.can_manage && selected.includes(o.id),
|
|
756
|
+
disabled: !o.can_manage,
|
|
757
|
+
onChange: (e) => onToggle(o.id, e.target.checked)
|
|
758
|
+
}
|
|
759
|
+
),
|
|
760
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: o.name }),
|
|
761
|
+
o.can_manage ? null : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "ml-auto text-xs", children: "You can't manage this space" })
|
|
762
|
+
]
|
|
763
|
+
}
|
|
764
|
+
) }, o.id)) })
|
|
765
|
+
] });
|
|
766
|
+
}
|
|
767
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
768
|
+
0 && (module.exports = {
|
|
769
|
+
TeamMembersPanel
|
|
770
|
+
});
|