@tangle-network/agent-app 0.42.2 → 0.42.4
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/dist/InvitationsPanel-ZKAUYZYU.js +8 -0
- package/dist/InviteAcceptPage-P3QJC3C6.js +7 -0
- package/dist/{MembersPanel-4CGUQRPF.js → MembersPanel-FOVHWUQ3.js} +2 -2
- package/dist/MembersPanel-FOVHWUQ3.js.map +1 -0
- package/dist/{access-ChAHG4Tp.d.ts → access-BZ81Btt5.d.ts} +1 -1
- package/dist/chunk-F7D7YCUL.js +206 -0
- package/dist/chunk-F7D7YCUL.js.map +1 -0
- package/dist/{chunk-WOVCWPFF.js → chunk-FJUOBNIY.js} +1 -1
- package/dist/{chunk-3G334FVA.js → chunk-GNL3MG5J.js} +4 -3
- package/dist/chunk-GNL3MG5J.js.map +1 -0
- package/dist/chunk-SWUVTGMR.js +262 -0
- package/dist/chunk-SWUVTGMR.js.map +1 -0
- package/dist/{chunk-535V6BH6.js → chunk-VCPZ3HTN.js} +44 -4
- package/dist/chunk-VCPZ3HTN.js.map +1 -0
- package/dist/chunk-WEBBJBDH.js +58 -0
- package/dist/chunk-WEBBJBDH.js.map +1 -0
- package/dist/{chunk-MY75I367.js → chunk-WYBP3AKG.js} +18 -9
- package/dist/chunk-WYBP3AKG.js.map +1 -0
- package/dist/design-canvas-react/index.js +1 -1
- package/dist/design-canvas-react/lazy.js +1 -1
- package/dist/index.js +1 -1
- package/dist/invitations-CZ5G2tIn.d.ts +56 -0
- package/dist/invitations-schema-DaMO4hZE.d.ts +280 -0
- package/dist/{roles-CLtYKHHl.d.ts → roles-BC1n4t37.d.ts} +1 -1
- package/dist/sandbox/index.js +1 -1
- package/dist/teams/drizzle.d.ts +4 -3
- package/dist/teams/drizzle.js +31 -1
- package/dist/teams/drizzle.js.map +1 -1
- package/dist/teams/index.d.ts +2 -1
- package/dist/teams/index.js +16 -0
- package/dist/teams/invitations-api.d.ts +169 -0
- package/dist/teams/invitations-api.js +338 -0
- package/dist/teams/invitations-api.js.map +1 -0
- package/dist/teams/members-api.d.ts +2 -2
- package/dist/teams/members-api.js +5 -255
- package/dist/teams/members-api.js.map +1 -1
- package/dist/teams-react/index.d.ts +66 -7
- package/dist/teams-react/index.js +6 -2
- package/dist/teams-react/lazy.d.ts +6 -4
- package/dist/teams-react/lazy.js +6 -2
- package/dist/teams-react/lazy.js.map +1 -1
- package/package.json +8 -3
- package/dist/InviteAcceptPage-EUOGZO2X.js +0 -7
- package/dist/chunk-3G334FVA.js.map +0 -1
- package/dist/chunk-535V6BH6.js.map +0 -1
- package/dist/chunk-MY75I367.js.map +0 -1
- /package/dist/{InviteAcceptPage-EUOGZO2X.js.map → InvitationsPanel-ZKAUYZYU.js.map} +0 -0
- /package/dist/{MembersPanel-4CGUQRPF.js.map → InviteAcceptPage-P3QJC3C6.js.map} +0 -0
- /package/dist/{chunk-WOVCWPFF.js.map → chunk-FJUOBNIY.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as drizzle_orm_sqlite_core from 'drizzle-orm/sqlite-core';
|
|
2
2
|
import { AnySQLiteTable, AnySQLiteColumn, BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core';
|
|
3
|
-
import { O as OrganizationRole, f as WorkspaceRole } from './roles-
|
|
3
|
+
import { O as OrganizationRole, f as WorkspaceRole } from './roles-BC1n4t37.js';
|
|
4
4
|
|
|
5
5
|
/** A product table referenced by FK — only the `id` column is touched. */
|
|
6
6
|
type TeamParentTable = AnySQLiteTable & {
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hasWorkspaceRole
|
|
3
|
+
} from "./chunk-63CE7FEZ.js";
|
|
4
|
+
|
|
5
|
+
// src/teams-react/components/InvitationsPanel.tsx
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
var ASSIGNABLE = [
|
|
9
|
+
{ value: "viewer", label: "Viewer" },
|
|
10
|
+
{ value: "editor", label: "Editor" },
|
|
11
|
+
{ value: "admin", label: "Admin" }
|
|
12
|
+
];
|
|
13
|
+
function InvitationsPanel({
|
|
14
|
+
invitations,
|
|
15
|
+
currentRole,
|
|
16
|
+
onInvite,
|
|
17
|
+
onResend,
|
|
18
|
+
onRevoke,
|
|
19
|
+
onCopy,
|
|
20
|
+
onNotice
|
|
21
|
+
}) {
|
|
22
|
+
const [inviteEmail, setInviteEmail] = useState("");
|
|
23
|
+
const [inviteRole, setInviteRole] = useState("editor");
|
|
24
|
+
const [inviting, setInviting] = useState(false);
|
|
25
|
+
const [busyId, setBusyId] = useState(null);
|
|
26
|
+
const canManage = hasWorkspaceRole(currentRole, "admin");
|
|
27
|
+
function notify(kind, message) {
|
|
28
|
+
onNotice?.({ kind, message });
|
|
29
|
+
}
|
|
30
|
+
async function submitInvite() {
|
|
31
|
+
const email = inviteEmail.trim();
|
|
32
|
+
if (!email || inviting) return;
|
|
33
|
+
setInviting(true);
|
|
34
|
+
try {
|
|
35
|
+
await onInvite({ email, role: inviteRole });
|
|
36
|
+
notify("success", `Invitation sent to ${email}`);
|
|
37
|
+
setInviteEmail("");
|
|
38
|
+
} catch (err) {
|
|
39
|
+
notify("error", err instanceof Error ? err.message : "Failed to invite");
|
|
40
|
+
} finally {
|
|
41
|
+
setInviting(false);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function copyLink(inviteUrl) {
|
|
45
|
+
try {
|
|
46
|
+
if (onCopy) await onCopy({ inviteUrl });
|
|
47
|
+
else await navigator.clipboard.writeText(inviteUrl);
|
|
48
|
+
notify("success", "Invite link copied");
|
|
49
|
+
} catch (err) {
|
|
50
|
+
notify("error", err instanceof Error ? err.message : "Failed to copy link");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function resend(invitationId) {
|
|
54
|
+
setBusyId(invitationId);
|
|
55
|
+
try {
|
|
56
|
+
await onResend({ invitationId });
|
|
57
|
+
notify("success", "Invitation resent");
|
|
58
|
+
} catch (err) {
|
|
59
|
+
notify("error", err instanceof Error ? err.message : "Failed to resend invitation");
|
|
60
|
+
} finally {
|
|
61
|
+
setBusyId(null);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function revoke(invitationId) {
|
|
65
|
+
setBusyId(invitationId);
|
|
66
|
+
try {
|
|
67
|
+
await onRevoke({ invitationId });
|
|
68
|
+
notify("success", "Invitation revoked");
|
|
69
|
+
} catch (err) {
|
|
70
|
+
notify("error", err instanceof Error ? err.message : "Failed to revoke invitation");
|
|
71
|
+
} finally {
|
|
72
|
+
setBusyId(null);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return /* @__PURE__ */ jsxs("section", { className: "flex flex-col gap-4", children: [
|
|
76
|
+
canManage && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
|
|
77
|
+
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-[var(--text-muted)]", children: "Invite by email" }),
|
|
78
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
79
|
+
/* @__PURE__ */ jsx(
|
|
80
|
+
"input",
|
|
81
|
+
{
|
|
82
|
+
type: "email",
|
|
83
|
+
placeholder: "colleague@example.com",
|
|
84
|
+
value: inviteEmail,
|
|
85
|
+
"aria-label": "Invite email address",
|
|
86
|
+
onChange: (event) => setInviteEmail(event.target.value),
|
|
87
|
+
onKeyDown: (event) => {
|
|
88
|
+
if (event.key === "Enter") void submitInvite();
|
|
89
|
+
},
|
|
90
|
+
className: "flex-1 rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-3 py-1.5 text-sm text-[var(--text-primary)] placeholder:text-[var(--text-muted)]"
|
|
91
|
+
}
|
|
92
|
+
),
|
|
93
|
+
/* @__PURE__ */ jsx(
|
|
94
|
+
"select",
|
|
95
|
+
{
|
|
96
|
+
value: inviteRole,
|
|
97
|
+
"aria-label": "Invite role",
|
|
98
|
+
onChange: (event) => setInviteRole(event.target.value),
|
|
99
|
+
className: "rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-2 py-1.5 text-xs text-[var(--text-secondary)]",
|
|
100
|
+
children: ASSIGNABLE.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.label }, option.value))
|
|
101
|
+
}
|
|
102
|
+
),
|
|
103
|
+
/* @__PURE__ */ jsx(
|
|
104
|
+
"button",
|
|
105
|
+
{
|
|
106
|
+
type: "button",
|
|
107
|
+
onClick: () => void submitInvite(),
|
|
108
|
+
disabled: inviting || !inviteEmail.trim(),
|
|
109
|
+
className: "rounded bg-[var(--brand-primary)] px-3 py-1.5 text-sm text-white disabled:opacity-50",
|
|
110
|
+
children: inviting ? "Inviting\u2026" : "Invite"
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
] })
|
|
114
|
+
] }),
|
|
115
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 border-t border-[var(--border-default)] pt-3", children: [
|
|
116
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
117
|
+
/* @__PURE__ */ jsx("h4", { className: "text-xs font-semibold text-[var(--text-primary)]", children: "Invitation history" }),
|
|
118
|
+
/* @__PURE__ */ jsxs("span", { className: "text-xs text-[var(--text-muted)]", children: [
|
|
119
|
+
invitations.length,
|
|
120
|
+
" total"
|
|
121
|
+
] })
|
|
122
|
+
] }),
|
|
123
|
+
invitations.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-[var(--text-muted)]", children: "No invitations yet." }) : invitations.map((invitation) => /* @__PURE__ */ jsx(
|
|
124
|
+
InvitationRow,
|
|
125
|
+
{
|
|
126
|
+
invitation,
|
|
127
|
+
canManage,
|
|
128
|
+
busy: busyId === invitation.id,
|
|
129
|
+
onCopy: () => void copyLink(invitation.inviteUrl),
|
|
130
|
+
onResend: () => void resend(invitation.id),
|
|
131
|
+
onRevoke: () => void revoke(invitation.id)
|
|
132
|
+
},
|
|
133
|
+
invitation.id
|
|
134
|
+
))
|
|
135
|
+
] })
|
|
136
|
+
] });
|
|
137
|
+
}
|
|
138
|
+
function InvitationRow({ invitation, canManage, busy, onCopy, onResend, onRevoke }) {
|
|
139
|
+
const isPending = invitation.status === "pending";
|
|
140
|
+
const emailFailed = invitation.emailStatus === "failed";
|
|
141
|
+
const expiry = new Date(invitation.expiresAt).toLocaleDateString("en-US", {
|
|
142
|
+
month: "short",
|
|
143
|
+
day: "numeric",
|
|
144
|
+
year: "numeric"
|
|
145
|
+
});
|
|
146
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-[var(--border-default)] p-3", children: [
|
|
147
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3", children: [
|
|
148
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
149
|
+
/* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium text-[var(--text-primary)]", children: invitation.email }),
|
|
150
|
+
/* @__PURE__ */ jsxs("p", { className: "text-xs capitalize text-[var(--text-muted)]", children: [
|
|
151
|
+
invitation.permissions,
|
|
152
|
+
" \xB7 expires ",
|
|
153
|
+
expiry
|
|
154
|
+
] }),
|
|
155
|
+
emailFailed && /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-[var(--text-danger)]", children: "Email was not sent \u2014 copy the link to share it." })
|
|
156
|
+
] }),
|
|
157
|
+
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [
|
|
158
|
+
/* @__PURE__ */ jsx("span", { className: "rounded border border-[var(--border-default)] px-2 py-0.5 text-[10px] uppercase text-[var(--text-secondary)]", children: invitation.status }),
|
|
159
|
+
/* @__PURE__ */ jsx(
|
|
160
|
+
"span",
|
|
161
|
+
{
|
|
162
|
+
className: `rounded border border-[var(--border-default)] px-2 py-0.5 text-[10px] uppercase ${emailFailed ? "text-[var(--text-danger)]" : "text-[var(--text-muted)]"}`,
|
|
163
|
+
children: invitation.emailStatus.replace("_", " ")
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
] })
|
|
167
|
+
] }),
|
|
168
|
+
canManage && isPending && /* @__PURE__ */ jsxs("div", { className: "mt-3 flex items-center justify-end gap-2", children: [
|
|
169
|
+
/* @__PURE__ */ jsx(
|
|
170
|
+
"button",
|
|
171
|
+
{
|
|
172
|
+
type: "button",
|
|
173
|
+
onClick: onCopy,
|
|
174
|
+
disabled: busy,
|
|
175
|
+
className: "rounded border border-[var(--border-default)] px-2 py-1 text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)] disabled:opacity-50",
|
|
176
|
+
children: "Copy link"
|
|
177
|
+
}
|
|
178
|
+
),
|
|
179
|
+
/* @__PURE__ */ jsx(
|
|
180
|
+
"button",
|
|
181
|
+
{
|
|
182
|
+
type: "button",
|
|
183
|
+
onClick: onResend,
|
|
184
|
+
disabled: busy,
|
|
185
|
+
className: "rounded border border-[var(--border-default)] px-2 py-1 text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)] disabled:opacity-50",
|
|
186
|
+
children: busy ? "Working\u2026" : "Resend"
|
|
187
|
+
}
|
|
188
|
+
),
|
|
189
|
+
/* @__PURE__ */ jsx(
|
|
190
|
+
"button",
|
|
191
|
+
{
|
|
192
|
+
type: "button",
|
|
193
|
+
onClick: onRevoke,
|
|
194
|
+
disabled: busy,
|
|
195
|
+
className: "rounded px-2 py-1 text-xs text-[var(--text-danger)] hover:bg-[var(--border-default)] disabled:opacity-50",
|
|
196
|
+
children: "Revoke"
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
] })
|
|
200
|
+
] });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export {
|
|
204
|
+
InvitationsPanel
|
|
205
|
+
};
|
|
206
|
+
//# sourceMappingURL=chunk-F7D7YCUL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/teams-react/components/InvitationsPanel.tsx"],"sourcesContent":["/**\n * Email-invitation panel: invite by email + role, and a history of every\n * invitation (pending / accepted / expired / revoked) with its email-delivery\n * status. Pending rows expose copy-link / resend / revoke. Fully callback-driven\n * — the host supplies the data and the async `onInvite` / `onResend` / `onRevoke`\n * callbacks (backed by `./teams/invitations-api`), so this imports no app router,\n * fetch client, or toast. Styled with the shipped Tangle Quiet tokens (`var(--*)`).\n *\n * Mount alongside a list-only `MembersPanel` (`showInviteForm={false}`): the\n * members panel shows accepted members, this owns the invite flow + pending list.\n */\n\nimport { useState } from 'react'\nimport type { WorkspaceRole } from '../../teams/roles'\nimport { hasWorkspaceRole } from '../../teams/roles'\nimport type { InvitationView, InvitationsPanelProps } from '../contracts'\n\nconst ASSIGNABLE: { value: WorkspaceRole; label: string }[] = [\n { value: 'viewer', label: 'Viewer' },\n { value: 'editor', label: 'Editor' },\n { value: 'admin', label: 'Admin' },\n]\n\nexport function InvitationsPanel({\n invitations,\n currentRole,\n onInvite,\n onResend,\n onRevoke,\n onCopy,\n onNotice,\n}: InvitationsPanelProps) {\n const [inviteEmail, setInviteEmail] = useState('')\n const [inviteRole, setInviteRole] = useState<WorkspaceRole>('editor')\n const [inviting, setInviting] = useState(false)\n const [busyId, setBusyId] = useState<string | null>(null)\n\n const canManage = hasWorkspaceRole(currentRole, 'admin')\n\n function notify(kind: 'success' | 'error', message: string) {\n onNotice?.({ kind, message })\n }\n\n async function submitInvite() {\n const email = inviteEmail.trim()\n if (!email || inviting) return\n setInviting(true)\n try {\n await onInvite({ email, role: inviteRole })\n notify('success', `Invitation sent to ${email}`)\n setInviteEmail('')\n } catch (err) {\n notify('error', err instanceof Error ? err.message : 'Failed to invite')\n } finally {\n setInviting(false)\n }\n }\n\n async function copyLink(inviteUrl: string) {\n try {\n if (onCopy) await onCopy({ inviteUrl })\n else await navigator.clipboard.writeText(inviteUrl)\n notify('success', 'Invite link copied')\n } catch (err) {\n notify('error', err instanceof Error ? err.message : 'Failed to copy link')\n }\n }\n\n async function resend(invitationId: string) {\n setBusyId(invitationId)\n try {\n await onResend({ invitationId })\n notify('success', 'Invitation resent')\n } catch (err) {\n notify('error', err instanceof Error ? err.message : 'Failed to resend invitation')\n } finally {\n setBusyId(null)\n }\n }\n\n async function revoke(invitationId: string) {\n setBusyId(invitationId)\n try {\n await onRevoke({ invitationId })\n notify('success', 'Invitation revoked')\n } catch (err) {\n notify('error', err instanceof Error ? err.message : 'Failed to revoke invitation')\n } finally {\n setBusyId(null)\n }\n }\n\n return (\n <section className=\"flex flex-col gap-4\">\n {canManage && (\n <div className=\"flex flex-col gap-1.5\">\n <label className=\"text-xs font-medium text-[var(--text-muted)]\">Invite by email</label>\n <div className=\"flex gap-2\">\n <input\n type=\"email\"\n placeholder=\"colleague@example.com\"\n value={inviteEmail}\n aria-label=\"Invite email address\"\n onChange={(event) => setInviteEmail(event.target.value)}\n onKeyDown={(event) => { if (event.key === 'Enter') void submitInvite() }}\n className=\"flex-1 rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-3 py-1.5 text-sm text-[var(--text-primary)] placeholder:text-[var(--text-muted)]\"\n />\n <select\n value={inviteRole}\n aria-label=\"Invite role\"\n onChange={(event) => setInviteRole(event.target.value as WorkspaceRole)}\n className=\"rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-2 py-1.5 text-xs text-[var(--text-secondary)]\"\n >\n {ASSIGNABLE.map((option) => (\n <option key={option.value} value={option.value}>{option.label}</option>\n ))}\n </select>\n <button\n type=\"button\"\n onClick={() => void submitInvite()}\n disabled={inviting || !inviteEmail.trim()}\n className=\"rounded bg-[var(--brand-primary)] px-3 py-1.5 text-sm text-white disabled:opacity-50\"\n >\n {inviting ? 'Inviting…' : 'Invite'}\n </button>\n </div>\n </div>\n )}\n\n <div className=\"flex flex-col gap-2 border-t border-[var(--border-default)] pt-3\">\n <div className=\"flex items-center justify-between\">\n <h4 className=\"text-xs font-semibold text-[var(--text-primary)]\">Invitation history</h4>\n <span className=\"text-xs text-[var(--text-muted)]\">{invitations.length} total</span>\n </div>\n {invitations.length === 0 ? (\n <p className=\"text-sm text-[var(--text-muted)]\">No invitations yet.</p>\n ) : (\n invitations.map((invitation) => (\n <InvitationRow\n key={invitation.id}\n invitation={invitation}\n canManage={canManage}\n busy={busyId === invitation.id}\n onCopy={() => void copyLink(invitation.inviteUrl)}\n onResend={() => void resend(invitation.id)}\n onRevoke={() => void revoke(invitation.id)}\n />\n ))\n )}\n </div>\n </section>\n )\n}\n\ninterface InvitationRowProps {\n invitation: InvitationView\n canManage: boolean\n busy: boolean\n onCopy(): void\n onResend(): void\n onRevoke(): void\n}\n\nfunction InvitationRow({ invitation, canManage, busy, onCopy, onResend, onRevoke }: InvitationRowProps) {\n const isPending = invitation.status === 'pending'\n const emailFailed = invitation.emailStatus === 'failed'\n const expiry = new Date(invitation.expiresAt).toLocaleDateString('en-US', {\n month: 'short',\n day: 'numeric',\n year: 'numeric',\n })\n\n return (\n <div className=\"rounded-lg border border-[var(--border-default)] p-3\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-[var(--text-primary)]\">{invitation.email}</p>\n <p className=\"text-xs capitalize text-[var(--text-muted)]\">\n {invitation.permissions} · expires {expiry}\n </p>\n {emailFailed && (\n <p className=\"mt-1 text-xs text-[var(--text-danger)]\">Email was not sent — copy the link to share it.</p>\n )}\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n <span className=\"rounded border border-[var(--border-default)] px-2 py-0.5 text-[10px] uppercase text-[var(--text-secondary)]\">\n {invitation.status}\n </span>\n <span\n className={`rounded border border-[var(--border-default)] px-2 py-0.5 text-[10px] uppercase ${\n emailFailed ? 'text-[var(--text-danger)]' : 'text-[var(--text-muted)]'\n }`}\n >\n {invitation.emailStatus.replace('_', ' ')}\n </span>\n </div>\n </div>\n {canManage && isPending && (\n <div className=\"mt-3 flex items-center justify-end gap-2\">\n <button\n type=\"button\"\n onClick={onCopy}\n disabled={busy}\n className=\"rounded border border-[var(--border-default)] px-2 py-1 text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)] disabled:opacity-50\"\n >\n Copy link\n </button>\n <button\n type=\"button\"\n onClick={onResend}\n disabled={busy}\n className=\"rounded border border-[var(--border-default)] px-2 py-1 text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)] disabled:opacity-50\"\n >\n {busy ? 'Working…' : 'Resend'}\n </button>\n <button\n type=\"button\"\n onClick={onRevoke}\n disabled={busy}\n className=\"rounded px-2 py-1 text-xs text-[var(--text-danger)] hover:bg-[var(--border-default)] disabled:opacity-50\"\n >\n Revoke\n </button>\n </div>\n )}\n </div>\n )\n}\n"],"mappings":"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","names":[]}
|
|
@@ -16,7 +16,8 @@ function MembersPanel({
|
|
|
16
16
|
onInvite,
|
|
17
17
|
onChangeRole,
|
|
18
18
|
onRemove,
|
|
19
|
-
onNotice
|
|
19
|
+
onNotice,
|
|
20
|
+
showInviteForm = true
|
|
20
21
|
}) {
|
|
21
22
|
const [inviteEmail, setInviteEmail] = useState("");
|
|
22
23
|
const [inviteRole, setInviteRole] = useState("editor");
|
|
@@ -73,7 +74,7 @@ function MembersPanel({
|
|
|
73
74
|
)),
|
|
74
75
|
members.length === 0 && /* @__PURE__ */ jsx("p", { className: "text-sm text-[var(--text-muted)]", children: "No team members yet." })
|
|
75
76
|
] }),
|
|
76
|
-
canManage && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5 border-t border-[var(--border-default)] pt-3", children: [
|
|
77
|
+
canManage && showInviteForm && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5 border-t border-[var(--border-default)] pt-3", children: [
|
|
77
78
|
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-[var(--text-muted)]", children: "Invite member" }),
|
|
78
79
|
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
79
80
|
/* @__PURE__ */ jsx(
|
|
@@ -159,4 +160,4 @@ function MemberRow({ member, canManage, onChangeRole, onRemove }) {
|
|
|
159
160
|
export {
|
|
160
161
|
MembersPanel
|
|
161
162
|
};
|
|
162
|
-
//# sourceMappingURL=chunk-
|
|
163
|
+
//# sourceMappingURL=chunk-GNL3MG5J.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/teams-react/components/MembersPanel.tsx"],"sourcesContent":["/**\n * Workspace members panel: list members, invite by email + role, change a\n * member's role, remove a member. Fully callback-driven — the host supplies the\n * data and the async `onInvite`/`onChangeRole`/`onRemove` callbacks (backed by\n * `./teams/members-api`), so this imports no app router, fetch client, or toast.\n * Styled with the shipped Tangle Quiet tokens (`var(--*)`).\n *\n * Role gating mirrors the API: only admins/owners see role selects and the\n * remove control; inherited org owners and explicit owners are not editable\n * here (org-level concern). The invite role select offers `admin` only to\n * admins/owners.\n */\n\nimport { useState } from 'react'\nimport type { WorkspaceRole } from '../../teams/roles'\nimport { hasWorkspaceRole } from '../../teams/roles'\nimport type { MemberView, MembersPanelProps } from '../contracts'\n\nconst ASSIGNABLE: { value: WorkspaceRole; label: string }[] = [\n { value: 'viewer', label: 'Viewer' },\n { value: 'editor', label: 'Editor' },\n { value: 'admin', label: 'Admin' },\n]\n\nexport function MembersPanel({\n members,\n currentRole,\n onInvite,\n onChangeRole,\n onRemove,\n onNotice,\n showInviteForm = true,\n}: MembersPanelProps) {\n const [inviteEmail, setInviteEmail] = useState('')\n const [inviteRole, setInviteRole] = useState<WorkspaceRole>('editor')\n const [inviting, setInviting] = useState(false)\n\n const canManage = hasWorkspaceRole(currentRole, 'admin')\n\n function notify(kind: 'success' | 'error', message: string) {\n onNotice?.({ kind, message })\n }\n\n async function submitInvite() {\n const email = inviteEmail.trim()\n if (!email || inviting) return\n setInviting(true)\n try {\n const result = await onInvite({ email, role: inviteRole })\n if (result && 'inviteUrl' in result && result.inviteUrl) {\n notify('success', `Invite link ready for ${email}`)\n } else {\n notify('success', `Invited ${email}`)\n }\n setInviteEmail('')\n } catch (err) {\n notify('error', err instanceof Error ? err.message : 'Failed to invite')\n } finally {\n setInviting(false)\n }\n }\n\n async function changeRole(memberId: string, role: WorkspaceRole) {\n try {\n await onChangeRole({ memberId, role })\n notify('success', 'Role updated')\n } catch (err) {\n notify('error', err instanceof Error ? err.message : 'Failed to update role')\n }\n }\n\n async function remove(memberId: string) {\n try {\n await onRemove({ memberId })\n notify('success', 'Member removed')\n } catch (err) {\n notify('error', err instanceof Error ? err.message : 'Failed to remove member')\n }\n }\n\n return (\n <section className=\"flex flex-col gap-4\">\n <div className=\"flex flex-col gap-2\">\n {members.map((member) => (\n <MemberRow\n key={member.id}\n member={member}\n canManage={canManage}\n onChangeRole={changeRole}\n onRemove={remove}\n />\n ))}\n {members.length === 0 && (\n <p className=\"text-sm text-[var(--text-muted)]\">No team members yet.</p>\n )}\n </div>\n\n {canManage && showInviteForm && (\n <div className=\"flex flex-col gap-1.5 border-t border-[var(--border-default)] pt-3\">\n <label className=\"text-xs font-medium text-[var(--text-muted)]\">Invite member</label>\n <div className=\"flex gap-2\">\n <input\n type=\"email\"\n placeholder=\"Email address\"\n value={inviteEmail}\n aria-label=\"Invite email address\"\n onChange={(event) => setInviteEmail(event.target.value)}\n onKeyDown={(event) => { if (event.key === 'Enter') void submitInvite() }}\n className=\"flex-1 rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-3 py-1.5 text-sm text-[var(--text-primary)] placeholder:text-[var(--text-muted)]\"\n />\n <select\n value={inviteRole}\n aria-label=\"Invite role\"\n onChange={(event) => setInviteRole(event.target.value as WorkspaceRole)}\n className=\"rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-2 py-1.5 text-xs text-[var(--text-secondary)]\"\n >\n {ASSIGNABLE.map((option) => (\n <option key={option.value} value={option.value}>{option.label}</option>\n ))}\n </select>\n <button\n type=\"button\"\n onClick={() => void submitInvite()}\n disabled={inviting || !inviteEmail.trim()}\n className=\"rounded bg-[var(--brand-primary)] px-3 py-1.5 text-sm text-white disabled:opacity-50\"\n >\n {inviting ? 'Inviting…' : 'Invite'}\n </button>\n </div>\n </div>\n )}\n </section>\n )\n}\n\ninterface MemberRowProps {\n member: MemberView\n canManage: boolean\n onChangeRole(memberId: string, role: WorkspaceRole): void\n onRemove(memberId: string): void\n}\n\nfunction MemberRow({ member, canManage, onChangeRole, onRemove }: MemberRowProps) {\n const pending = member.acceptedAt == null\n const label = member.name ?? member.email ?? 'Unknown'\n const initial = (member.name?.[0] ?? member.email?.[0] ?? '?').toUpperCase()\n const isOwner = member.role === 'owner'\n const editable = canManage && !isOwner && !member.inherited\n\n return (\n <div className=\"flex items-center justify-between border-b border-[var(--border-default)] py-2 last:border-0\">\n <div className=\"flex items-center gap-3\">\n <div className=\"flex h-8 w-8 items-center justify-center rounded-full bg-[var(--bg-input)] text-xs font-bold text-[var(--text-secondary)]\">\n {initial}\n </div>\n <div>\n <p className=\"flex items-center gap-2 text-sm font-medium text-[var(--text-primary)]\">\n {label}\n {pending && (\n <span className=\"rounded border border-[var(--border-default)] px-1.5 py-0.5 text-[10px] uppercase text-[var(--text-muted)]\">\n Pending\n </span>\n )}\n </p>\n {member.name && member.email && (\n <p className=\"text-xs text-[var(--text-muted)]\">{member.email}</p>\n )}\n </div>\n </div>\n\n <div className=\"flex items-center gap-2\">\n {isOwner ? (\n <span className=\"rounded border border-[var(--border-default)] px-2 py-0.5 text-xs text-[var(--text-secondary)]\">\n {member.inherited ? 'Org Admin' : 'Owner'}\n </span>\n ) : editable ? (\n <>\n <select\n value={member.role}\n aria-label={`Role for ${label}`}\n onChange={(event) => onChangeRole(member.id, event.target.value as WorkspaceRole)}\n className=\"rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-2 py-1 text-xs text-[var(--text-secondary)]\"\n >\n {ASSIGNABLE.map((option) => (\n <option key={option.value} value={option.value}>{option.label}</option>\n ))}\n </select>\n <button\n type=\"button\"\n aria-label={`Remove ${label}`}\n onClick={() => onRemove(member.id)}\n className=\"rounded px-2 py-1 text-xs text-[var(--text-danger)] hover:bg-[var(--border-default)]\"\n >\n Remove\n </button>\n </>\n ) : (\n <span className=\"rounded border border-[var(--border-default)] px-2 py-0.5 text-xs capitalize text-[var(--text-secondary)]\">\n {member.role}\n </span>\n )}\n </div>\n </div>\n )\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import {
|
|
2
|
+
generateInviteToken
|
|
3
|
+
} from "./chunk-3SVAA3MA.js";
|
|
4
|
+
import {
|
|
5
|
+
canManageWorkspaceMemberRole,
|
|
6
|
+
hasWorkspaceRole,
|
|
7
|
+
isAssignableWorkspaceRole,
|
|
8
|
+
organizationRoleGrantsWorkspaceOwner
|
|
9
|
+
} from "./chunk-63CE7FEZ.js";
|
|
10
|
+
|
|
11
|
+
// src/teams/members-api.ts
|
|
12
|
+
import { and, eq, isNull } from "drizzle-orm";
|
|
13
|
+
var SeatLimitError = class extends Error {
|
|
14
|
+
status;
|
|
15
|
+
capability;
|
|
16
|
+
requiredPlan;
|
|
17
|
+
constructor(message, opts = {}) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "SeatLimitError";
|
|
20
|
+
this.status = opts.status ?? 402;
|
|
21
|
+
this.capability = opts.capability;
|
|
22
|
+
this.requiredPlan = opts.requiredPlan;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
function createMembersApi(opts) {
|
|
26
|
+
const { db, tables, userTable, workspaceTable, access, enforceSeat, memberSyncSeam } = opts;
|
|
27
|
+
const { organizations, organizationMembers, workspaceMembers } = tables;
|
|
28
|
+
const users = userTable;
|
|
29
|
+
const workspaces = workspaceTable;
|
|
30
|
+
function fireSync(op) {
|
|
31
|
+
try {
|
|
32
|
+
Promise.resolve(op()).catch(() => {
|
|
33
|
+
});
|
|
34
|
+
} catch {
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function listMembers(input) {
|
|
38
|
+
const accessRow = await access.getWorkspaceAccess(input.workspaceId, input.actor.id);
|
|
39
|
+
if (!accessRow) return Response.json({ error: "Workspace not found" }, { status: 404 });
|
|
40
|
+
const organizationId = accessRow.workspace.organizationId;
|
|
41
|
+
const [projectMembers, orgAdmins] = await Promise.all([
|
|
42
|
+
db.select({
|
|
43
|
+
id: workspaceMembers.id,
|
|
44
|
+
organizationMemberId: workspaceMembers.organizationMemberId,
|
|
45
|
+
userId: workspaceMembers.userId,
|
|
46
|
+
role: workspaceMembers.role,
|
|
47
|
+
invitedAt: workspaceMembers.invitedAt,
|
|
48
|
+
acceptedAt: workspaceMembers.acceptedAt,
|
|
49
|
+
inviteEmail: workspaceMembers.inviteEmail,
|
|
50
|
+
userName: users.name,
|
|
51
|
+
userEmail: users.email
|
|
52
|
+
}).from(workspaceMembers).leftJoin(users, eq(users.id, workspaceMembers.userId)).where(eq(workspaceMembers.workspaceId, input.workspaceId)),
|
|
53
|
+
db.select({
|
|
54
|
+
id: organizationMembers.id,
|
|
55
|
+
userId: organizationMembers.userId,
|
|
56
|
+
orgRole: organizationMembers.role,
|
|
57
|
+
userName: users.name,
|
|
58
|
+
userEmail: users.email,
|
|
59
|
+
createdAt: organizationMembers.createdAt
|
|
60
|
+
}).from(organizationMembers).innerJoin(users, eq(users.id, organizationMembers.userId)).where(eq(organizationMembers.organizationId, organizationId))
|
|
61
|
+
]);
|
|
62
|
+
const explicitOrgMemberIds = new Set(
|
|
63
|
+
projectMembers.map((m) => m.organizationMemberId).filter(Boolean)
|
|
64
|
+
);
|
|
65
|
+
const members = [
|
|
66
|
+
...orgAdmins.filter((m) => organizationRoleGrantsWorkspaceOwner(m.orgRole) && !explicitOrgMemberIds.has(m.id)).map((m) => ({
|
|
67
|
+
id: `org:${m.id}`,
|
|
68
|
+
userId: m.userId,
|
|
69
|
+
organizationMemberId: m.id,
|
|
70
|
+
role: "owner",
|
|
71
|
+
name: m.userName,
|
|
72
|
+
email: m.userEmail,
|
|
73
|
+
invitedAt: m.createdAt,
|
|
74
|
+
acceptedAt: m.createdAt,
|
|
75
|
+
inherited: true
|
|
76
|
+
})),
|
|
77
|
+
...projectMembers.map((m) => ({
|
|
78
|
+
id: m.id,
|
|
79
|
+
userId: m.userId,
|
|
80
|
+
organizationMemberId: m.organizationMemberId,
|
|
81
|
+
role: m.role,
|
|
82
|
+
name: m.userName,
|
|
83
|
+
email: m.userEmail ?? m.inviteEmail,
|
|
84
|
+
invitedAt: m.invitedAt,
|
|
85
|
+
acceptedAt: m.acceptedAt,
|
|
86
|
+
inherited: false
|
|
87
|
+
}))
|
|
88
|
+
];
|
|
89
|
+
return Response.json({ members, currentRole: accessRow.role, organizationId });
|
|
90
|
+
}
|
|
91
|
+
async function inviteMember(input) {
|
|
92
|
+
if (!input.email) return Response.json({ error: "Missing email" }, { status: 400 });
|
|
93
|
+
const requestedRole = input.role ?? "editor";
|
|
94
|
+
if (!isAssignableWorkspaceRole(requestedRole)) {
|
|
95
|
+
return Response.json({ error: "Invalid role. Must be viewer, editor, or admin." }, { status: 400 });
|
|
96
|
+
}
|
|
97
|
+
const assignRole = requestedRole;
|
|
98
|
+
const accessRow = await access.getWorkspaceAccess(input.workspaceId, input.actor.id, "admin");
|
|
99
|
+
if (!accessRow) return Response.json({ error: "Workspace not found" }, { status: 404 });
|
|
100
|
+
if (!hasWorkspaceRole(accessRow.role, assignRole)) {
|
|
101
|
+
return Response.json({ error: "Cannot assign a role higher than your own" }, { status: 403 });
|
|
102
|
+
}
|
|
103
|
+
const organizationId = accessRow.workspace.organizationId;
|
|
104
|
+
const normalizedEmail = input.email.toLowerCase().trim();
|
|
105
|
+
const invitee = await getUserByEmail(normalizedEmail);
|
|
106
|
+
const existingOrgMember = invitee ? await getOrganizationMember(organizationId, invitee.id) : null;
|
|
107
|
+
if (organizationRoleGrantsWorkspaceOwner(existingOrgMember?.role)) {
|
|
108
|
+
return Response.json({ error: "Organization admins already have project access" }, { status: 409 });
|
|
109
|
+
}
|
|
110
|
+
const existingInvite = await findExistingProjectInvite(input.workspaceId, normalizedEmail, invitee?.id);
|
|
111
|
+
if (existingInvite) {
|
|
112
|
+
return Response.json({ error: "This collaborator is already invited to this project" }, { status: 409 });
|
|
113
|
+
}
|
|
114
|
+
if (enforceSeat && !existingOrgMember && !await hasPendingOrgInvite(organizationId, normalizedEmail)) {
|
|
115
|
+
try {
|
|
116
|
+
await enforceSeat({ actorId: input.actor.id, organizationId });
|
|
117
|
+
} catch (err) {
|
|
118
|
+
if (err instanceof SeatLimitError) {
|
|
119
|
+
return Response.json(
|
|
120
|
+
{ error: err.message, capability: err.capability, requiredPlan: err.requiredPlan },
|
|
121
|
+
{ status: err.status }
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const token = generateInviteToken();
|
|
128
|
+
const [member] = await db.insert(workspaceMembers).values({
|
|
129
|
+
workspaceId: input.workspaceId,
|
|
130
|
+
organizationMemberId: existingOrgMember?.id ?? null,
|
|
131
|
+
userId: invitee?.id ?? null,
|
|
132
|
+
role: assignRole,
|
|
133
|
+
invitedBy: input.actor.id,
|
|
134
|
+
inviteEmail: normalizedEmail,
|
|
135
|
+
inviteToken: token
|
|
136
|
+
}).returning();
|
|
137
|
+
return Response.json({ member, inviteToken: token });
|
|
138
|
+
}
|
|
139
|
+
async function updateMemberRole(input) {
|
|
140
|
+
if (!input.memberId || !input.role) {
|
|
141
|
+
return Response.json({ error: "Missing memberId or role" }, { status: 400 });
|
|
142
|
+
}
|
|
143
|
+
if (input.memberId.startsWith("org:")) {
|
|
144
|
+
return Response.json({ error: "Organization owner/admin project access is managed at the organization level" }, { status: 403 });
|
|
145
|
+
}
|
|
146
|
+
if (!isAssignableWorkspaceRole(input.role)) {
|
|
147
|
+
return Response.json({ error: "Invalid role" }, { status: 400 });
|
|
148
|
+
}
|
|
149
|
+
const accessRow = await access.getWorkspaceAccess(input.workspaceId, input.actor.id, "admin");
|
|
150
|
+
if (!accessRow) return Response.json({ error: "Workspace not found" }, { status: 404 });
|
|
151
|
+
if (!hasWorkspaceRole(accessRow.role, input.role)) {
|
|
152
|
+
return Response.json({ error: "Cannot assign a role higher than your own" }, { status: 403 });
|
|
153
|
+
}
|
|
154
|
+
const [target] = await db.select({ id: workspaceMembers.id, role: workspaceMembers.role, userId: workspaceMembers.userId }).from(workspaceMembers).where(and(eq(workspaceMembers.id, input.memberId), eq(workspaceMembers.workspaceId, input.workspaceId))).limit(1);
|
|
155
|
+
if (!target) return Response.json({ error: "Member not found" }, { status: 404 });
|
|
156
|
+
if (target.userId === input.actor.id) return Response.json({ error: "Cannot change your own role" }, { status: 403 });
|
|
157
|
+
if (!canManageWorkspaceMemberRole(accessRow.role, target.role)) {
|
|
158
|
+
return Response.json({ error: "Cannot modify a member with equal or higher role" }, { status: 403 });
|
|
159
|
+
}
|
|
160
|
+
await db.update(workspaceMembers).set({ role: input.role }).where(eq(workspaceMembers.id, input.memberId));
|
|
161
|
+
if (target.userId && memberSyncSeam?.role) {
|
|
162
|
+
const userId = target.userId;
|
|
163
|
+
const nextRole = input.role;
|
|
164
|
+
fireSync(() => memberSyncSeam.role({ workspaceId: input.workspaceId, userId, role: nextRole }));
|
|
165
|
+
}
|
|
166
|
+
return Response.json({ success: true });
|
|
167
|
+
}
|
|
168
|
+
async function removeMember(input) {
|
|
169
|
+
if (!input.memberId) return Response.json({ error: "Missing memberId" }, { status: 400 });
|
|
170
|
+
if (input.memberId.startsWith("org:")) {
|
|
171
|
+
return Response.json({ error: "Organization owner/admin project access is managed at the organization level" }, { status: 403 });
|
|
172
|
+
}
|
|
173
|
+
const accessRow = await access.getWorkspaceAccess(input.workspaceId, input.actor.id, "admin");
|
|
174
|
+
if (!accessRow) return Response.json({ error: "Workspace not found" }, { status: 404 });
|
|
175
|
+
const [target] = await db.select({ id: workspaceMembers.id, role: workspaceMembers.role, userId: workspaceMembers.userId }).from(workspaceMembers).where(and(eq(workspaceMembers.id, input.memberId), eq(workspaceMembers.workspaceId, input.workspaceId))).limit(1);
|
|
176
|
+
if (!target) return Response.json({ error: "Member not found" }, { status: 404 });
|
|
177
|
+
if (target.userId === input.actor.id) return Response.json({ error: "Cannot remove yourself from this project" }, { status: 403 });
|
|
178
|
+
if (!canManageWorkspaceMemberRole(accessRow.role, target.role)) {
|
|
179
|
+
return Response.json({ error: "Cannot remove a member with equal or higher role" }, { status: 403 });
|
|
180
|
+
}
|
|
181
|
+
await db.delete(workspaceMembers).where(eq(workspaceMembers.id, input.memberId));
|
|
182
|
+
if (target.userId && memberSyncSeam?.remove) {
|
|
183
|
+
const userId = target.userId;
|
|
184
|
+
fireSync(() => memberSyncSeam.remove({ workspaceId: input.workspaceId, userId }));
|
|
185
|
+
}
|
|
186
|
+
return Response.json({ success: true });
|
|
187
|
+
}
|
|
188
|
+
async function acceptInvite(input) {
|
|
189
|
+
if (!input.token || typeof input.token !== "string") {
|
|
190
|
+
return Response.json({ error: "Missing invite token" }, { status: 400 });
|
|
191
|
+
}
|
|
192
|
+
const [invite] = await db.select().from(workspaceMembers).where(eq(workspaceMembers.inviteToken, input.token)).limit(1);
|
|
193
|
+
if (!invite) return Response.json({ error: "Invalid or expired invite" }, { status: 404 });
|
|
194
|
+
if (invite.acceptedAt) return Response.json({ error: "Invite already accepted" }, { status: 409 });
|
|
195
|
+
if (invite.inviteEmail && invite.inviteEmail.toLowerCase() !== input.actor.email?.toLowerCase()) {
|
|
196
|
+
return Response.json(
|
|
197
|
+
{ error: `This invite was sent to ${invite.inviteEmail}. Please sign in with that email.` },
|
|
198
|
+
{ status: 403 }
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
const [workspace] = await db.select({ id: workspaces.id, organizationId: workspaces.organizationId }).from(workspaces).where(eq(workspaces.id, invite.workspaceId)).limit(1);
|
|
202
|
+
if (!workspace) return Response.json({ error: "Project not found" }, { status: 404 });
|
|
203
|
+
const organizationId = workspace.organizationId;
|
|
204
|
+
const updated = await db.update(workspaceMembers).set({
|
|
205
|
+
userId: input.actor.id,
|
|
206
|
+
acceptedAt: /* @__PURE__ */ new Date(),
|
|
207
|
+
inviteToken: null
|
|
208
|
+
}).where(and(
|
|
209
|
+
eq(workspaceMembers.inviteToken, input.token),
|
|
210
|
+
isNull(workspaceMembers.acceptedAt)
|
|
211
|
+
)).returning({ id: workspaceMembers.id });
|
|
212
|
+
if (updated.length === 0) return Response.json({ error: "Invite already accepted" }, { status: 409 });
|
|
213
|
+
const [orgMember] = await db.insert(organizationMembers).values({
|
|
214
|
+
organizationId,
|
|
215
|
+
userId: input.actor.id,
|
|
216
|
+
role: "member"
|
|
217
|
+
}).onConflictDoUpdate({
|
|
218
|
+
target: [organizationMembers.organizationId, organizationMembers.userId],
|
|
219
|
+
set: { updatedAt: /* @__PURE__ */ new Date() }
|
|
220
|
+
}).returning();
|
|
221
|
+
await db.update(workspaceMembers).set({ organizationMemberId: invite.organizationMemberId ?? orgMember.id }).where(eq(workspaceMembers.id, updated[0].id));
|
|
222
|
+
if (memberSyncSeam?.add) {
|
|
223
|
+
const role = invite.role;
|
|
224
|
+
fireSync(() => memberSyncSeam.add({ workspaceId: invite.workspaceId, userId: input.actor.id, role }));
|
|
225
|
+
}
|
|
226
|
+
return Response.json({
|
|
227
|
+
success: true,
|
|
228
|
+
workspaceId: invite.workspaceId,
|
|
229
|
+
role: invite.role
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
async function getUserByEmail(email) {
|
|
233
|
+
const [user] = await db.select({ id: users.id, email: users.email }).from(users).where(eq(users.email, email)).limit(1);
|
|
234
|
+
return user ?? null;
|
|
235
|
+
}
|
|
236
|
+
async function getOrganizationMember(organizationId, userId) {
|
|
237
|
+
const [member] = await db.select().from(organizationMembers).where(and(eq(organizationMembers.organizationId, organizationId), eq(organizationMembers.userId, userId))).limit(1);
|
|
238
|
+
return member ?? null;
|
|
239
|
+
}
|
|
240
|
+
async function findExistingProjectInvite(workspaceId, email, userId) {
|
|
241
|
+
const byEmail = await db.select({ id: workspaceMembers.id }).from(workspaceMembers).where(and(eq(workspaceMembers.workspaceId, workspaceId), eq(workspaceMembers.inviteEmail, email))).limit(1);
|
|
242
|
+
if (byEmail[0]) return byEmail[0];
|
|
243
|
+
if (!userId) return null;
|
|
244
|
+
const byUser = await db.select({ id: workspaceMembers.id }).from(workspaceMembers).where(and(eq(workspaceMembers.workspaceId, workspaceId), eq(workspaceMembers.userId, userId))).limit(1);
|
|
245
|
+
return byUser[0] ?? null;
|
|
246
|
+
}
|
|
247
|
+
async function hasPendingOrgInvite(organizationId, email) {
|
|
248
|
+
const [row] = await db.select({ id: workspaceMembers.id }).from(workspaceMembers).innerJoin(workspaces, eq(workspaces.id, workspaceMembers.workspaceId)).where(and(
|
|
249
|
+
eq(workspaces.organizationId, organizationId),
|
|
250
|
+
eq(workspaceMembers.inviteEmail, email),
|
|
251
|
+
isNull(workspaceMembers.acceptedAt)
|
|
252
|
+
)).limit(1);
|
|
253
|
+
return Boolean(row);
|
|
254
|
+
}
|
|
255
|
+
return { listMembers, inviteMember, updateMemberRole, removeMember, acceptInvite };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export {
|
|
259
|
+
SeatLimitError,
|
|
260
|
+
createMembersApi
|
|
261
|
+
};
|
|
262
|
+
//# sourceMappingURL=chunk-SWUVTGMR.js.map
|