@burdenoff/microfe-planmagnet 2026.1002.3 → 2026.1008.1
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/planmagnet/pages/TeamInviteMemberPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TeamInviteMemberPagePage.js +142 -82
- package/dist/planmagnet/pages/TeamInviteMemberPagePage.js.map +1 -1
- package/dist/planmagnet/team/PendingInvitations.d.ts +27 -0
- package/dist/planmagnet/team/PendingInvitations.d.ts.map +1 -0
- package/dist/planmagnet/team/PendingInvitations.js +220 -0
- package/dist/planmagnet/team/PendingInvitations.js.map +1 -0
- package/dist/planmagnet/team/invitationEmail.d.ts +35 -0
- package/dist/planmagnet/team/invitationEmail.d.ts.map +1 -0
- package/dist/planmagnet/team/invitationEmail.js +73 -0
- package/dist/planmagnet/team/invitationEmail.js.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamInviteMemberPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/TeamInviteMemberPagePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"TeamInviteMemberPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/TeamInviteMemberPagePage.tsx"],"names":[],"mappings":"AA6EA,wBAAgB,wBAAwB,4CAiRvC"}
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import { usePlanMagnet as e, usePlanMagnetNavigate as
|
|
2
|
-
import {
|
|
1
|
+
import { usePlanMagnet as e, usePlanMagnetNavigate as ee } from "../providers/PlanMagnetProvider.js";
|
|
2
|
+
import { useTr as te } from "../../shared/hooks/useTr.js";
|
|
3
|
+
import { throwOnError as t } from "../utils/mutationResult.js";
|
|
4
|
+
import { refusalMessage as n } from "../product/refusal.js";
|
|
3
5
|
import { isValidInviteEmail as r } from "../utils/email.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
6
|
+
import { invitationEmailReason as i, invitationEmailSent as a } from "../team/invitationEmail.js";
|
|
7
|
+
import { PENDING_INVITATIONS_QUERY as o, PendingInvitations as s, RESEND_INVITATION as ne } from "../team/PendingInvitations.js";
|
|
8
|
+
import { useMemo as c, useState as l } from "react";
|
|
9
|
+
import { RefreshCw as u, Send as d, UserPlus as f, X as re } from "lucide-react";
|
|
10
|
+
import { gql as p } from "@apollo/client";
|
|
11
|
+
import { useMutation as m, useQuery as h } from "@apollo/client/react";
|
|
12
|
+
import { Fragment as g, jsx as _, jsxs as v } from "react/jsx-runtime";
|
|
13
|
+
import { Button as y, Card as b, CardContent as x, CardHeader as S, CardTitle as C, Input as w } from "@burdenoff/fe-libs/ui";
|
|
10
14
|
//#region src/planmagnet/pages/TeamInviteMemberPagePage.tsx
|
|
11
|
-
var
|
|
15
|
+
var T = p`
|
|
12
16
|
query PlanmagnetInviteRoles($scopeId: String!) {
|
|
13
17
|
roles(
|
|
14
18
|
scopeId: $scopeId
|
|
@@ -20,151 +24,207 @@ var b = c`
|
|
|
20
24
|
description
|
|
21
25
|
}
|
|
22
26
|
}
|
|
23
|
-
`,
|
|
27
|
+
`, E = p`
|
|
24
28
|
mutation PlanmagnetInviteMember($input: CreateInvitationInput!) {
|
|
25
29
|
createInvitation(input: $input) {
|
|
26
30
|
id
|
|
27
31
|
email
|
|
28
32
|
status
|
|
29
33
|
expiresAt
|
|
34
|
+
emailStatus
|
|
35
|
+
emailLastAttemptAt
|
|
36
|
+
emailErrorCode
|
|
30
37
|
}
|
|
31
38
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
`, D = {
|
|
40
|
+
refetchQueries: [o],
|
|
41
|
+
awaitRefetchQueries: !0
|
|
42
|
+
};
|
|
43
|
+
function O() {
|
|
44
|
+
let o = te(), { workspaceId: p, currentUser: O } = e(), k = ee(), A = h(T, {
|
|
45
|
+
variables: { scopeId: p },
|
|
46
|
+
skip: !p
|
|
47
|
+
}), [ie, j] = m(E, D), [M, N] = m(ne, D), [P, F] = l(""), [I, L] = l(!1), [R, z] = l(""), [B, V] = l(null), [H, U] = l(null), W = H?.workspaceId === (p ?? null) ? H : null, [G, K] = l(null), q = c(() => A.data?.roles ?? [], [A.data]), J = c(() => new Map(q.map((e) => [e.id, e.name])), [q]), Y = q.find((e) => e.id === R) ?? null, X = r(P), Z = X && !!Y && !j.loading && !!p, Q = o("planmagnet.invite.pending.forbidden", "Your access level does not allow managing invitations in this workspace."), ae = async () => {
|
|
48
|
+
if (!(!Z || !Y)) {
|
|
49
|
+
V(null), U(null), K(null);
|
|
50
|
+
try {
|
|
51
|
+
let e = (await t(ie({ variables: { input: {
|
|
52
|
+
email: P.trim(),
|
|
53
|
+
roleIds: [Y.id],
|
|
54
|
+
workspaceId: p
|
|
55
|
+
} } }))).data?.createInvitation;
|
|
56
|
+
e && U({
|
|
57
|
+
...e,
|
|
58
|
+
workspaceId: p ?? null
|
|
59
|
+
}), F(""), L(!1);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
V(n(e, o("planmagnet.invite.errors.create", "The invitation could not be created."), Q));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}, oe = async () => {
|
|
65
|
+
if (W) {
|
|
66
|
+
K(null);
|
|
67
|
+
try {
|
|
68
|
+
let e = (await t(M({ variables: { id: W.id } }))).data?.resendInvitation;
|
|
69
|
+
e && U({
|
|
70
|
+
...W,
|
|
71
|
+
emailStatus: e.emailStatus,
|
|
72
|
+
emailLastAttemptAt: e.emailLastAttemptAt,
|
|
73
|
+
emailErrorCode: e.emailErrorCode
|
|
74
|
+
});
|
|
75
|
+
} catch (e) {
|
|
76
|
+
K(n(e, o("planmagnet.invite.errors.resend", "The invitation email could not be resent."), Q));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}, $ = W ? i(W.emailErrorCode) : null;
|
|
80
|
+
return /* @__PURE__ */ v("div", {
|
|
39
81
|
className: "max-w-3xl space-y-6 p-6",
|
|
40
82
|
children: [
|
|
41
|
-
/* @__PURE__ */
|
|
83
|
+
/* @__PURE__ */ v("div", {
|
|
42
84
|
className: "flex items-center justify-between",
|
|
43
|
-
children: [/* @__PURE__ */
|
|
85
|
+
children: [/* @__PURE__ */ v("div", { children: [/* @__PURE__ */ _("h1", {
|
|
44
86
|
className: "text-2xl font-semibold",
|
|
45
|
-
children: "Invite Team Member"
|
|
46
|
-
}), /* @__PURE__ */
|
|
87
|
+
children: o("planmagnet.invite.title", "Invite Team Member")
|
|
88
|
+
}), /* @__PURE__ */ _("p", {
|
|
47
89
|
className: "text-sm text-muted-foreground",
|
|
48
|
-
children: "Invite someone to this workspace with a PlanMagnet access level."
|
|
49
|
-
})] }), /* @__PURE__ */
|
|
90
|
+
children: o("planmagnet.invite.subtitle", "Invite someone to this workspace with a PlanMagnet access level.")
|
|
91
|
+
})] }), /* @__PURE__ */ v(y, {
|
|
50
92
|
variant: "outline",
|
|
51
93
|
size: "sm",
|
|
52
|
-
onClick: () =>
|
|
53
|
-
children: [/* @__PURE__ */
|
|
94
|
+
onClick: () => k("/team/team-page"),
|
|
95
|
+
children: [/* @__PURE__ */ _(re, { className: "mr-1 h-4 w-4" }), o("planmagnet.invite.back", "Back to team")]
|
|
54
96
|
})]
|
|
55
97
|
}),
|
|
56
|
-
|
|
98
|
+
W && a(W) && /* @__PURE__ */ _("div", {
|
|
57
99
|
role: "status",
|
|
58
100
|
className: "rounded-md border border-border bg-muted p-3 text-sm text-status-success-text",
|
|
59
101
|
"data-testid": "invite-sent",
|
|
102
|
+
children: o("planmagnet.invite.sent", "Invitation sent to {{email}}. They get this access when they accept it.", { email: W.email })
|
|
103
|
+
}),
|
|
104
|
+
W && !a(W) && $ && /* @__PURE__ */ v("div", {
|
|
105
|
+
role: "status",
|
|
106
|
+
className: "space-y-2 rounded-md border border-border bg-muted p-3 text-sm",
|
|
107
|
+
"data-testid": "invite-email-not-sent",
|
|
60
108
|
children: [
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
109
|
+
/* @__PURE__ */ _("p", {
|
|
110
|
+
className: "font-medium text-status-warning-text",
|
|
111
|
+
children: o("planmagnet.invite.emailNotSent", "Invitation created for {{email}}, but the email was not sent.", { email: W.email })
|
|
112
|
+
}),
|
|
113
|
+
/* @__PURE__ */ _("p", {
|
|
114
|
+
className: "text-muted-foreground",
|
|
115
|
+
"data-testid": "invite-email-not-sent-reason",
|
|
116
|
+
children: o($.key, $.fallback)
|
|
117
|
+
}),
|
|
118
|
+
/* @__PURE__ */ v(y, {
|
|
119
|
+
size: "sm",
|
|
120
|
+
variant: "outline",
|
|
121
|
+
onClick: () => void oe(),
|
|
122
|
+
disabled: N.loading,
|
|
123
|
+
"data-testid": "invite-resend",
|
|
124
|
+
children: [/* @__PURE__ */ _(u, { className: "mr-1 h-4 w-4" }), N.loading ? o("planmagnet.invite.resending", "Resending…") : o("planmagnet.invite.resend", "Resend email")]
|
|
125
|
+
}),
|
|
126
|
+
G && /* @__PURE__ */ _("p", {
|
|
127
|
+
role: "alert",
|
|
128
|
+
className: "text-destructive",
|
|
129
|
+
"data-testid": "invite-resend-error",
|
|
130
|
+
children: G
|
|
131
|
+
})
|
|
64
132
|
]
|
|
65
133
|
}),
|
|
66
|
-
|
|
134
|
+
B && /* @__PURE__ */ _("p", {
|
|
67
135
|
role: "alert",
|
|
68
136
|
className: "text-sm text-destructive",
|
|
69
137
|
"data-testid": "invite-error",
|
|
70
|
-
children:
|
|
138
|
+
children: B
|
|
71
139
|
}),
|
|
72
|
-
/* @__PURE__ */
|
|
140
|
+
/* @__PURE__ */ v(b, { children: [/* @__PURE__ */ _(S, { children: /* @__PURE__ */ v(C, {
|
|
73
141
|
className: "flex items-center gap-2",
|
|
74
|
-
children: [/* @__PURE__ */ f
|
|
75
|
-
}) }), /* @__PURE__ */
|
|
142
|
+
children: [/* @__PURE__ */ _(f, { className: "h-4 w-4" }), o("planmagnet.invite.details", "Invitation details")]
|
|
143
|
+
}) }), /* @__PURE__ */ v(x, {
|
|
76
144
|
className: "space-y-4",
|
|
77
145
|
children: [
|
|
78
|
-
/* @__PURE__ */
|
|
79
|
-
/* @__PURE__ */
|
|
146
|
+
/* @__PURE__ */ v("div", { children: [
|
|
147
|
+
/* @__PURE__ */ _("label", {
|
|
80
148
|
htmlFor: "invite-email",
|
|
81
149
|
className: "text-sm font-medium",
|
|
82
|
-
children: "Email address"
|
|
150
|
+
children: o("planmagnet.invite.emailLabel", "Email address")
|
|
83
151
|
}),
|
|
84
|
-
/* @__PURE__ */
|
|
152
|
+
/* @__PURE__ */ _(w, {
|
|
85
153
|
id: "invite-email",
|
|
86
154
|
type: "email",
|
|
87
|
-
value:
|
|
155
|
+
value: P,
|
|
88
156
|
onChange: (e) => {
|
|
89
|
-
|
|
157
|
+
F(e.target.value), U(null), K(null);
|
|
90
158
|
},
|
|
91
|
-
onBlur: () =>
|
|
92
|
-
"aria-invalid":
|
|
159
|
+
onBlur: () => L(!0),
|
|
160
|
+
"aria-invalid": I && !X,
|
|
93
161
|
"aria-describedby": "invite-email-help",
|
|
94
162
|
className: "mt-1",
|
|
95
163
|
placeholder: "colleague@company.com"
|
|
96
164
|
}),
|
|
97
|
-
|
|
165
|
+
I && P.trim() !== "" && !X ? /* @__PURE__ */ _("p", {
|
|
98
166
|
id: "invite-email-help",
|
|
99
167
|
role: "alert",
|
|
100
168
|
className: "mt-1 text-xs text-destructive",
|
|
101
|
-
children: "Enter a full email address, like name@company.com."
|
|
102
|
-
}) : /* @__PURE__ */
|
|
169
|
+
children: o("planmagnet.invite.emailInvalid", "Enter a full email address, like name@company.com.")
|
|
170
|
+
}) : /* @__PURE__ */ _("p", {
|
|
103
171
|
id: "invite-email-help",
|
|
104
172
|
className: "mt-1 text-xs text-muted-foreground",
|
|
105
|
-
children: "
|
|
173
|
+
children: o("planmagnet.invite.emailHelp", "An invitation email with a link to accept is sent when possible. Pending invitations can be resent below.")
|
|
106
174
|
})
|
|
107
175
|
] }),
|
|
108
|
-
/* @__PURE__ */
|
|
176
|
+
/* @__PURE__ */ v("div", { children: [/* @__PURE__ */ _("label", {
|
|
109
177
|
htmlFor: "invite-role",
|
|
110
178
|
className: "text-sm font-medium",
|
|
111
|
-
children: "Access level"
|
|
112
|
-
}),
|
|
179
|
+
children: o("planmagnet.invite.roleLabel", "Access level")
|
|
180
|
+
}), A.loading && !A.data ? /* @__PURE__ */ _("p", {
|
|
113
181
|
className: "mt-1 text-sm text-muted-foreground",
|
|
114
|
-
children: "Loading access levels…"
|
|
115
|
-
}) :
|
|
182
|
+
children: o("planmagnet.invite.rolesLoading", "Loading access levels…")
|
|
183
|
+
}) : A.error ? /* @__PURE__ */ _("p", {
|
|
116
184
|
role: "alert",
|
|
117
185
|
className: "mt-1 text-sm text-destructive",
|
|
118
|
-
children:
|
|
119
|
-
}) :
|
|
186
|
+
children: o("planmagnet.invite.rolesFailed", "Access levels could not be loaded: {{reason}}", { reason: A.error.message })
|
|
187
|
+
}) : q.length === 0 ? /* @__PURE__ */ _("p", {
|
|
120
188
|
className: "mt-1 text-sm text-muted-foreground",
|
|
121
|
-
children: "No PlanMagnet access levels are set up for this workspace yet."
|
|
122
|
-
}) : /* @__PURE__ */
|
|
189
|
+
children: o("planmagnet.invite.rolesNone", "No PlanMagnet access levels are set up for this workspace yet.")
|
|
190
|
+
}) : /* @__PURE__ */ v(g, { children: [/* @__PURE__ */ v("select", {
|
|
123
191
|
id: "invite-role",
|
|
124
192
|
className: "mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm",
|
|
125
|
-
value:
|
|
126
|
-
onChange: (e) =>
|
|
193
|
+
value: R,
|
|
194
|
+
onChange: (e) => z(e.target.value),
|
|
127
195
|
"data-testid": "invite-role",
|
|
128
|
-
children: [/* @__PURE__ */
|
|
196
|
+
children: [/* @__PURE__ */ _("option", {
|
|
129
197
|
value: "",
|
|
130
|
-
children: "Choose an access level"
|
|
131
|
-
}),
|
|
198
|
+
children: o("planmagnet.invite.rolePlaceholder", "Choose an access level")
|
|
199
|
+
}), q.map((e) => /* @__PURE__ */ _("option", {
|
|
132
200
|
value: e.id,
|
|
133
201
|
children: e.name
|
|
134
202
|
}, e.id))]
|
|
135
|
-
}),
|
|
203
|
+
}), Y?.description && /* @__PURE__ */ _("p", {
|
|
136
204
|
className: "mt-1 text-xs text-muted-foreground",
|
|
137
|
-
children:
|
|
205
|
+
children: Y.description
|
|
138
206
|
})] })] }),
|
|
139
|
-
/* @__PURE__ */
|
|
207
|
+
/* @__PURE__ */ _("div", {
|
|
140
208
|
className: "flex justify-end",
|
|
141
|
-
children: /* @__PURE__ */
|
|
209
|
+
children: /* @__PURE__ */ v(y, {
|
|
142
210
|
size: "sm",
|
|
143
|
-
onClick:
|
|
144
|
-
|
|
145
|
-
N(null), F(null);
|
|
146
|
-
try {
|
|
147
|
-
F((await n(w({ variables: { input: {
|
|
148
|
-
email: E.trim(),
|
|
149
|
-
roleIds: [L.id],
|
|
150
|
-
workspaceId: c
|
|
151
|
-
} } }))).data?.createInvitation.email ?? E.trim()), D(""), k(!1);
|
|
152
|
-
} catch (e) {
|
|
153
|
-
N(e instanceof Error ? e.message : "The invitation could not be sent.");
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
},
|
|
157
|
-
disabled: !z,
|
|
211
|
+
onClick: ae,
|
|
212
|
+
disabled: !Z,
|
|
158
213
|
"data-testid": "invite-send",
|
|
159
|
-
children: [/* @__PURE__ */
|
|
214
|
+
children: [/* @__PURE__ */ _(d, { className: "mr-1 h-4 w-4" }), j.loading ? o("planmagnet.invite.sending", "Sending…") : o("planmagnet.invite.send", "Send invite")]
|
|
160
215
|
})
|
|
161
216
|
})
|
|
162
217
|
]
|
|
163
|
-
})] })
|
|
218
|
+
})] }),
|
|
219
|
+
/* @__PURE__ */ _(s, {
|
|
220
|
+
workspaceId: p,
|
|
221
|
+
currentUserId: O?.id,
|
|
222
|
+
roleNames: J
|
|
223
|
+
}, p ?? "no-workspace")
|
|
164
224
|
]
|
|
165
225
|
});
|
|
166
226
|
}
|
|
167
227
|
//#endregion
|
|
168
|
-
export {
|
|
228
|
+
export { O as TeamInviteMemberPagePage };
|
|
169
229
|
|
|
170
230
|
//# sourceMappingURL=TeamInviteMemberPagePage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamInviteMemberPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/TeamInviteMemberPagePage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { Button, Card, CardContent, CardHeader, CardTitle, Input } from '@burdenoff/fe-libs/ui';\nimport { Send, UserPlus, X } from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { throwOnError } from '../utils/mutationResult';\nimport { isValidInviteEmail } from '../utils/email';\n\n/**\n * Invite a member (H12, BOFF-7833). The previous page offered job titles (developer, designer, …) in its\n * \"Role\" picker plus invented Department and Team pickers, none of which any backend accepts, and its Send\n * button was disabled on the belief that no invite mutation existed. The platform's createInvitation does\n * exist, validates the email and the role ids, and assigns the roles when the invitee accepts. The access\n * levels offered here are PlanMagnet's own RBAC roles for this workspace, as the platform returns them.\n */\n\nconst INVITE_ROLES = gql`\n query PlanmagnetInviteRoles($scopeId: String!) {\n roles(\n scopeId: $scopeId\n filter: { product: \"planmagnet\", includePlatformRoles: false }\n pagination: { limit: 50 }\n ) {\n id\n name\n description\n }\n }\n`;\n\nconst CREATE_INVITATION = gql`\n mutation PlanmagnetInviteMember($input: CreateInvitationInput!) {\n createInvitation(input: $input) {\n id\n email\n status\n expiresAt\n }\n }\n`;\n\ninterface InviteRole {\n id: string;\n name: string;\n description: string | null;\n}\n\nexport function TeamInviteMemberPagePage() {\n const { workspaceId } = usePlanMagnet();\n const navigate = usePlanMagnetNavigate();\n\n const roles = useQuery<{ roles: InviteRole[] }>(INVITE_ROLES, {\n variables: { scopeId: workspaceId },\n skip: !workspaceId,\n });\n const [invite, inviteState] = useMutation<{\n createInvitation: { id: string; email: string; status: string; expiresAt: string | null };\n }>(CREATE_INVITATION);\n\n const [email, setEmail] = useState('');\n const [emailTouched, setEmailTouched] = useState(false);\n const [roleId, setRoleId] = useState('');\n const [error, setError] = useState<string | null>(null);\n const [sentTo, setSentTo] = useState<string | null>(null);\n\n const available = roles.data?.roles ?? [];\n const chosen = available.find((r) => r.id === roleId) ?? null;\n const emailOk = isValidInviteEmail(email);\n const canSend = emailOk && !!chosen && !inviteState.loading && !!workspaceId;\n\n const send = async () => {\n if (!canSend || !chosen) return;\n setError(null);\n setSentTo(null);\n try {\n const result = await throwOnError(\n invite({ variables: { input: { email: email.trim(), roleIds: [chosen.id], workspaceId } } })\n );\n setSentTo(result.data?.createInvitation.email ?? email.trim());\n setEmail('');\n setEmailTouched(false);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The invitation could not be sent.');\n }\n };\n\n return (\n <div className=\"max-w-3xl space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold\">Invite Team Member</h1>\n <p className=\"text-sm text-muted-foreground\">\n Invite someone to this workspace with a PlanMagnet access level.\n </p>\n </div>\n <Button variant=\"outline\" size=\"sm\" onClick={() => navigate('/team/team-page')}>\n <X className=\"mr-1 h-4 w-4\" />\n Back to team\n </Button>\n </div>\n\n {sentTo && (\n <div\n role=\"status\"\n className=\"rounded-md border border-border bg-muted p-3 text-sm text-status-success-text\"\n data-testid=\"invite-sent\"\n >\n Invitation sent to {sentTo}. They get this access when they accept it.\n </div>\n )}\n {error && (\n <p role=\"alert\" className=\"text-sm text-destructive\" data-testid=\"invite-error\">\n {error}\n </p>\n )}\n\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <UserPlus className=\"h-4 w-4\" />\n Invitation details\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div>\n <label htmlFor=\"invite-email\" className=\"text-sm font-medium\">\n Email address\n </label>\n <Input\n id=\"invite-email\"\n type=\"email\"\n value={email}\n onChange={(e) => {\n setEmail(e.target.value);\n setSentTo(null);\n }}\n onBlur={() => setEmailTouched(true)}\n aria-invalid={emailTouched && !emailOk}\n aria-describedby=\"invite-email-help\"\n className=\"mt-1\"\n placeholder=\"colleague@company.com\"\n />\n {emailTouched && email.trim() !== '' && !emailOk ? (\n <p id=\"invite-email-help\" role=\"alert\" className=\"mt-1 text-xs text-destructive\">\n Enter a full email address, like name@company.com.\n </p>\n ) : (\n <p id=\"invite-email-help\" className=\"mt-1 text-xs text-muted-foreground\">\n They receive an email with a link to accept.\n </p>\n )}\n </div>\n\n <div>\n <label htmlFor=\"invite-role\" className=\"text-sm font-medium\">\n Access level\n </label>\n {roles.loading && !roles.data ? (\n <p className=\"mt-1 text-sm text-muted-foreground\">Loading access levels…</p>\n ) : roles.error ? (\n <p role=\"alert\" className=\"mt-1 text-sm text-destructive\">\n Access levels could not be loaded: {roles.error.message}\n </p>\n ) : available.length === 0 ? (\n <p className=\"mt-1 text-sm text-muted-foreground\">\n No PlanMagnet access levels are set up for this workspace yet.\n </p>\n ) : (\n <>\n <select\n id=\"invite-role\"\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm\"\n value={roleId}\n onChange={(e) => setRoleId(e.target.value)}\n data-testid=\"invite-role\"\n >\n <option value=\"\">Choose an access level</option>\n {available.map((r) => (\n <option key={r.id} value={r.id}>\n {r.name}\n </option>\n ))}\n </select>\n {chosen?.description && (\n <p className=\"mt-1 text-xs text-muted-foreground\">{chosen.description}</p>\n )}\n </>\n )}\n </div>\n\n <div className=\"flex justify-end\">\n <Button size=\"sm\" onClick={send} disabled={!canSend} data-testid=\"invite-send\">\n <Send className=\"mr-1 h-4 w-4\" />\n {inviteState.loading ? 'Sending…' : 'Send invite'}\n </Button>\n </div>\n </CardContent>\n </Card>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TeamInviteMemberPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/TeamInviteMemberPagePage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { Button, Card, CardContent, CardHeader, CardTitle, Input } from '@burdenoff/fe-libs/ui';\nimport { RefreshCw, Send, UserPlus, X } from 'lucide-react';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { throwOnError } from '../utils/mutationResult';\nimport { isValidInviteEmail } from '../utils/email';\nimport { refusalMessage } from '../product/refusal';\nimport {\n PENDING_INVITATIONS_QUERY,\n PendingInvitations,\n RESEND_INVITATION,\n type InvitationEmailResult,\n} from '../team/PendingInvitations';\nimport {\n invitationEmailReason,\n invitationEmailSent,\n type InvitationEmailFields,\n} from '../team/invitationEmail';\n\n/**\n * Invite a member (H12, BOFF-7833). The previous page offered job titles (developer, designer, …) in its\n * \"Role\" picker plus invented Department and Team pickers, none of which any backend accepts, and its Send\n * button was disabled on the belief that no invite mutation existed. The platform's createInvitation does\n * exist, validates the email and the role ids, and assigns the roles when the invitee accepts. The access\n * levels offered here are PlanMagnet's own RBAC roles for this workspace, as the platform returns them.\n *\n * BOFF-8866: creating an invitation does not mean its email went out. The service records the email outcome on\n * the invitation (emailStatus / emailErrorCode); the page says \"sent\" only for SENT, otherwise that the invitation\n * exists but the email was not sent, why, and offers Resend. The pending invitations are listed below the form.\n */\n\nconst INVITE_ROLES = gql`\n query PlanmagnetInviteRoles($scopeId: String!) {\n roles(\n scopeId: $scopeId\n filter: { product: \"planmagnet\", includePlatformRoles: false }\n pagination: { limit: 50 }\n ) {\n id\n name\n description\n }\n }\n`;\n\nconst CREATE_INVITATION = gql`\n mutation PlanmagnetInviteMember($input: CreateInvitationInput!) {\n createInvitation(input: $input) {\n id\n email\n status\n expiresAt\n emailStatus\n emailLastAttemptAt\n emailErrorCode\n }\n }\n`;\n\ninterface InviteRole {\n id: string;\n name: string;\n description: string | null;\n}\n\ninterface CreatedInvitation extends InvitationEmailFields {\n id: string;\n email: string;\n /** The workspace it was created in; its Resend is only valid there (the write is scoped by header). */\n workspaceId: string | null;\n}\n\nconst WRITE_OPTIONS = { refetchQueries: [PENDING_INVITATIONS_QUERY], awaitRefetchQueries: true };\n\nexport function TeamInviteMemberPagePage() {\n const tr = useTr();\n const { workspaceId, currentUser } = usePlanMagnet();\n const navigate = usePlanMagnetNavigate();\n\n const roles = useQuery<{ roles: InviteRole[] }>(INVITE_ROLES, {\n variables: { scopeId: workspaceId },\n skip: !workspaceId,\n });\n const [invite, inviteState] = useMutation<{\n createInvitation: InvitationEmailResult & { email: string };\n }>(CREATE_INVITATION, WRITE_OPTIONS);\n const [resend, resendState] = useMutation<{ resendInvitation: InvitationEmailResult }>(\n RESEND_INVITATION,\n WRITE_OPTIONS\n );\n\n const [email, setEmail] = useState('');\n const [emailTouched, setEmailTouched] = useState(false);\n const [roleId, setRoleId] = useState('');\n const [error, setError] = useState<string | null>(null);\n const [createdState, setCreated] = useState<CreatedInvitation | null>(null);\n const created = createdState?.workspaceId === (workspaceId ?? null) ? createdState : null;\n const [resendError, setResendError] = useState<string | null>(null);\n\n const available = useMemo(() => roles.data?.roles ?? [], [roles.data]);\n const roleNames = useMemo(() => new Map(available.map((r) => [r.id, r.name])), [available]);\n const chosen = available.find((r) => r.id === roleId) ?? null;\n const emailOk = isValidInviteEmail(email);\n const canSend = emailOk && !!chosen && !inviteState.loading && !!workspaceId;\n const forbidden = tr(\n 'planmagnet.invite.pending.forbidden',\n 'Your access level does not allow managing invitations in this workspace.'\n );\n\n const send = async () => {\n if (!canSend || !chosen) return;\n setError(null);\n setCreated(null);\n setResendError(null);\n try {\n const result = await throwOnError(\n invite({ variables: { input: { email: email.trim(), roleIds: [chosen.id], workspaceId } } })\n );\n const invitation = result.data?.createInvitation;\n if (invitation) setCreated({ ...invitation, workspaceId: workspaceId ?? null });\n setEmail('');\n setEmailTouched(false);\n } catch (e) {\n setError(\n refusalMessage(\n e,\n tr('planmagnet.invite.errors.create', 'The invitation could not be created.'),\n forbidden\n )\n );\n }\n };\n\n const resendCreated = async () => {\n if (!created) return;\n setResendError(null);\n try {\n const result = await throwOnError(resend({ variables: { id: created.id } }));\n const updated = result.data?.resendInvitation;\n if (updated) {\n setCreated({\n ...created,\n emailStatus: updated.emailStatus,\n emailLastAttemptAt: updated.emailLastAttemptAt,\n emailErrorCode: updated.emailErrorCode,\n });\n }\n } catch (e) {\n setResendError(\n refusalMessage(\n e,\n tr('planmagnet.invite.errors.resend', 'The invitation email could not be resent.'),\n forbidden\n )\n );\n }\n };\n\n const notSentReason = created ? invitationEmailReason(created.emailErrorCode) : null;\n\n return (\n <div className=\"max-w-3xl space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n {tr('planmagnet.invite.title', 'Invite Team Member')}\n </h1>\n <p className=\"text-sm text-muted-foreground\">\n {tr(\n 'planmagnet.invite.subtitle',\n 'Invite someone to this workspace with a PlanMagnet access level.'\n )}\n </p>\n </div>\n <Button variant=\"outline\" size=\"sm\" onClick={() => navigate('/team/team-page')}>\n <X className=\"mr-1 h-4 w-4\" />\n {tr('planmagnet.invite.back', 'Back to team')}\n </Button>\n </div>\n\n {created && invitationEmailSent(created) && (\n <div\n role=\"status\"\n className=\"rounded-md border border-border bg-muted p-3 text-sm text-status-success-text\"\n data-testid=\"invite-sent\"\n >\n {tr(\n 'planmagnet.invite.sent',\n 'Invitation sent to {{email}}. They get this access when they accept it.',\n { email: created.email }\n )}\n </div>\n )}\n {created && !invitationEmailSent(created) && notSentReason && (\n <div\n role=\"status\"\n className=\"space-y-2 rounded-md border border-border bg-muted p-3 text-sm\"\n data-testid=\"invite-email-not-sent\"\n >\n <p className=\"font-medium text-status-warning-text\">\n {tr(\n 'planmagnet.invite.emailNotSent',\n 'Invitation created for {{email}}, but the email was not sent.',\n { email: created.email }\n )}\n </p>\n <p className=\"text-muted-foreground\" data-testid=\"invite-email-not-sent-reason\">\n {tr(notSentReason.key, notSentReason.fallback)}\n </p>\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => void resendCreated()}\n disabled={resendState.loading}\n data-testid=\"invite-resend\"\n >\n <RefreshCw className=\"mr-1 h-4 w-4\" />\n {resendState.loading\n ? tr('planmagnet.invite.resending', 'Resending…')\n : tr('planmagnet.invite.resend', 'Resend email')}\n </Button>\n {resendError && (\n <p role=\"alert\" className=\"text-destructive\" data-testid=\"invite-resend-error\">\n {resendError}\n </p>\n )}\n </div>\n )}\n {error && (\n <p role=\"alert\" className=\"text-sm text-destructive\" data-testid=\"invite-error\">\n {error}\n </p>\n )}\n\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <UserPlus className=\"h-4 w-4\" />\n {tr('planmagnet.invite.details', 'Invitation details')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div>\n <label htmlFor=\"invite-email\" className=\"text-sm font-medium\">\n {tr('planmagnet.invite.emailLabel', 'Email address')}\n </label>\n <Input\n id=\"invite-email\"\n type=\"email\"\n value={email}\n onChange={(e) => {\n setEmail(e.target.value);\n setCreated(null);\n setResendError(null);\n }}\n onBlur={() => setEmailTouched(true)}\n aria-invalid={emailTouched && !emailOk}\n aria-describedby=\"invite-email-help\"\n className=\"mt-1\"\n placeholder=\"colleague@company.com\"\n />\n {emailTouched && email.trim() !== '' && !emailOk ? (\n <p id=\"invite-email-help\" role=\"alert\" className=\"mt-1 text-xs text-destructive\">\n {tr(\n 'planmagnet.invite.emailInvalid',\n 'Enter a full email address, like name@company.com.'\n )}\n </p>\n ) : (\n <p id=\"invite-email-help\" className=\"mt-1 text-xs text-muted-foreground\">\n {tr(\n 'planmagnet.invite.emailHelp',\n 'An invitation email with a link to accept is sent when possible. Pending invitations can be resent below.'\n )}\n </p>\n )}\n </div>\n\n <div>\n <label htmlFor=\"invite-role\" className=\"text-sm font-medium\">\n {tr('planmagnet.invite.roleLabel', 'Access level')}\n </label>\n {roles.loading && !roles.data ? (\n <p className=\"mt-1 text-sm text-muted-foreground\">\n {tr('planmagnet.invite.rolesLoading', 'Loading access levels…')}\n </p>\n ) : roles.error ? (\n <p role=\"alert\" className=\"mt-1 text-sm text-destructive\">\n {tr(\n 'planmagnet.invite.rolesFailed',\n 'Access levels could not be loaded: {{reason}}',\n {\n reason: roles.error.message,\n }\n )}\n </p>\n ) : available.length === 0 ? (\n <p className=\"mt-1 text-sm text-muted-foreground\">\n {tr(\n 'planmagnet.invite.rolesNone',\n 'No PlanMagnet access levels are set up for this workspace yet.'\n )}\n </p>\n ) : (\n <>\n <select\n id=\"invite-role\"\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm\"\n value={roleId}\n onChange={(e) => setRoleId(e.target.value)}\n data-testid=\"invite-role\"\n >\n <option value=\"\">\n {tr('planmagnet.invite.rolePlaceholder', 'Choose an access level')}\n </option>\n {available.map((r) => (\n <option key={r.id} value={r.id}>\n {r.name}\n </option>\n ))}\n </select>\n {chosen?.description && (\n <p className=\"mt-1 text-xs text-muted-foreground\">{chosen.description}</p>\n )}\n </>\n )}\n </div>\n\n <div className=\"flex justify-end\">\n <Button size=\"sm\" onClick={send} disabled={!canSend} data-testid=\"invite-send\">\n <Send className=\"mr-1 h-4 w-4\" />\n {inviteState.loading\n ? tr('planmagnet.invite.sending', 'Sending…')\n : tr('planmagnet.invite.send', 'Send invite')}\n </Button>\n </div>\n </CardContent>\n </Card>\n\n <PendingInvitations\n key={workspaceId ?? 'no-workspace'}\n workspaceId={workspaceId}\n currentUserId={currentUser?.id}\n roleNames={roleNames}\n />\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { InvitationEmailFields } from './invitationEmail';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The workspace's pending invitations on the Invite page (BOFF-8866): who was invited, with which access, whether
|
|
5
|
+
* the invitation email actually went out (and when it was last tried), when the invitation expires, and Resend /
|
|
6
|
+
* Cancel. `workspaceInvitations` and the two writes read the workspace from the gateway's x-workspace-id header.
|
|
7
|
+
* A write's outcome is shown above the list, so a row that leaves the list on the refresh does not take its message
|
|
8
|
+
* with it.
|
|
9
|
+
*/
|
|
10
|
+
/** Operation name of the list, for `refetchQueries` of every write that changes it. */
|
|
11
|
+
export declare const PENDING_INVITATIONS_QUERY = "PlanmagnetPendingInvitations";
|
|
12
|
+
/** Sends the invitation email again (and renews the invitation's expiry); answers with the new email outcome. */
|
|
13
|
+
export declare const RESEND_INVITATION: import('graphql').DocumentNode;
|
|
14
|
+
export interface InvitationEmailResult extends InvitationEmailFields {
|
|
15
|
+
id: string;
|
|
16
|
+
status: string;
|
|
17
|
+
expiresAt: string | null;
|
|
18
|
+
}
|
|
19
|
+
export declare function PendingInvitations({ workspaceId, currentUserId, roleNames, }: {
|
|
20
|
+
/** The workspace the list is for. The query is scoped by a request header, so callers key this component on it. */
|
|
21
|
+
workspaceId?: string | null;
|
|
22
|
+
/** Cancelling is the inviter's own action; other people's invitations are listed without it. */
|
|
23
|
+
currentUserId?: string | null;
|
|
24
|
+
/** PlanMagnet access-level names by role id, as the page already loaded them. */
|
|
25
|
+
roleNames: ReadonlyMap<string, string>;
|
|
26
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
27
|
+
//# sourceMappingURL=PendingInvitations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PendingInvitations.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/team/PendingInvitations.tsx"],"names":[],"mappings":"AAWA,OAAO,EAOL,KAAK,qBAAqB,EAC3B,MAAM,mBAAmB,CAAC;AAE3B;;;;;;GAMG;AAEH,uFAAuF;AACvF,eAAO,MAAM,yBAAyB,iCAAiC,CAAC;AAwBxE,iHAAiH;AACjH,eAAO,MAAM,iBAAiB,gCAW7B,CAAC;AAWF,MAAM,WAAW,qBAAsB,SAAQ,qBAAqB;IAClE,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAyBD,wBAAgB,kBAAkB,CAAC,EACjC,WAAW,EACX,aAAa,EACb,SAAS,GACV,EAAE;IACD,mHAAmH;IACnH,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,gGAAgG;IAChG,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,iFAAiF;IACjF,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACxC,2CAyRA"}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { useTr as e } from "../../shared/hooks/useTr.js";
|
|
2
|
+
import { isForbidden as t } from "../utils/graphqlErrors.js";
|
|
3
|
+
import { throwOnError as n } from "../utils/mutationResult.js";
|
|
4
|
+
import { ConfirmButton as r } from "../controls/ConfirmButton.js";
|
|
5
|
+
import { refusalMessage as i } from "../product/refusal.js";
|
|
6
|
+
import { invitationEmailBadgeVariant as a, invitationEmailReason as o, invitationEmailSent as s, invitationEmailStatusLabel as c, invitationRoleNames as l, parseInvitationDate as u } from "./invitationEmail.js";
|
|
7
|
+
import { useState as d } from "react";
|
|
8
|
+
import { Mail as f, RefreshCw as p, X as m } from "lucide-react";
|
|
9
|
+
import { gql as h } from "@apollo/client";
|
|
10
|
+
import { useMutation as g, useQuery as _ } from "@apollo/client/react";
|
|
11
|
+
import { jsx as v, jsxs as y } from "react/jsx-runtime";
|
|
12
|
+
import { Badge as b, Button as x, Card as S, CardContent as C, CardHeader as w, CardTitle as T } from "@burdenoff/fe-libs/ui";
|
|
13
|
+
import { clearGraphqlFetchResponseCache as E } from "@burdenoff/fe-libs/shared/graphql";
|
|
14
|
+
//#region src/planmagnet/team/PendingInvitations.tsx
|
|
15
|
+
var D = "PlanmagnetPendingInvitations", O = 50, k = h`
|
|
16
|
+
query PlanmagnetPendingInvitations($pagination: PaginationInput) {
|
|
17
|
+
workspaceInvitations(status: PENDING, pagination: $pagination) {
|
|
18
|
+
items {
|
|
19
|
+
id
|
|
20
|
+
email
|
|
21
|
+
status
|
|
22
|
+
roleIds
|
|
23
|
+
invitedById
|
|
24
|
+
expiresAt
|
|
25
|
+
emailStatus
|
|
26
|
+
emailLastAttemptAt
|
|
27
|
+
emailErrorCode
|
|
28
|
+
}
|
|
29
|
+
total
|
|
30
|
+
hasMore
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
`, A = h`
|
|
34
|
+
mutation PlanmagnetResendInvitation($id: ID!) {
|
|
35
|
+
resendInvitation(id: $id) {
|
|
36
|
+
id
|
|
37
|
+
status
|
|
38
|
+
expiresAt
|
|
39
|
+
emailStatus
|
|
40
|
+
emailLastAttemptAt
|
|
41
|
+
emailErrorCode
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
`, j = h`
|
|
45
|
+
mutation PlanmagnetCancelInvitation($id: ID!) {
|
|
46
|
+
cancelInvitation(id: $id) {
|
|
47
|
+
id
|
|
48
|
+
status
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
`, M = {
|
|
52
|
+
refetchQueries: [D],
|
|
53
|
+
awaitRefetchQueries: !0
|
|
54
|
+
}, N = {
|
|
55
|
+
success: "text-status-success-text",
|
|
56
|
+
warning: "text-status-warning-text",
|
|
57
|
+
error: "text-destructive"
|
|
58
|
+
}, P = (e) => u(e)?.toLocaleString() ?? null;
|
|
59
|
+
function F({ workspaceId: h, currentUserId: D, roleNames: F }) {
|
|
60
|
+
let I = e(), { data: L, loading: R, error: z, refetch: B } = _(k, {
|
|
61
|
+
variables: { pagination: { limit: O } },
|
|
62
|
+
skip: !h,
|
|
63
|
+
fetchPolicy: "network-only"
|
|
64
|
+
}), [V] = g(A, M), [H] = g(j, M), [U, W] = d(null), [G, K] = d(null), [q] = d(() => Date.now()), J = I("planmagnet.invite.pending.forbidden", "Your access level does not allow managing invitations in this workspace."), Y = () => {
|
|
65
|
+
E(), B();
|
|
66
|
+
}, X = async (e) => {
|
|
67
|
+
W(e.id), K(null);
|
|
68
|
+
try {
|
|
69
|
+
let t = (await n(V({ variables: { id: e.id } }))).data?.resendInvitation;
|
|
70
|
+
if (s(t)) K({
|
|
71
|
+
tone: "success",
|
|
72
|
+
text: I("planmagnet.invite.pending.resent", "Invitation email sent again to {{email}}.", { email: e.email })
|
|
73
|
+
});
|
|
74
|
+
else {
|
|
75
|
+
let n = o(t?.emailErrorCode);
|
|
76
|
+
K({
|
|
77
|
+
tone: "warning",
|
|
78
|
+
text: I("planmagnet.invite.pending.resendNotSent", "The invitation to {{email}} is still pending, but the email was not sent. {{reason}}", {
|
|
79
|
+
email: e.email,
|
|
80
|
+
reason: I(n.key, n.fallback)
|
|
81
|
+
})
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
} catch (t) {
|
|
85
|
+
K({
|
|
86
|
+
tone: "error",
|
|
87
|
+
text: I("planmagnet.invite.pending.resendFailed", "The invitation to {{email}} could not be resent: {{reason}}", {
|
|
88
|
+
email: e.email,
|
|
89
|
+
reason: i(t, I("planmagnet.invite.pending.resendFailedFallback", "Try again."), J)
|
|
90
|
+
})
|
|
91
|
+
}), Y();
|
|
92
|
+
} finally {
|
|
93
|
+
W(null);
|
|
94
|
+
}
|
|
95
|
+
}, Z = async (e) => {
|
|
96
|
+
W(e.id), K(null);
|
|
97
|
+
try {
|
|
98
|
+
await n(H({ variables: { id: e.id } })), K({
|
|
99
|
+
tone: "success",
|
|
100
|
+
text: I("planmagnet.invite.pending.cancelled", "The invitation to {{email}} was cancelled.", { email: e.email })
|
|
101
|
+
});
|
|
102
|
+
} catch (t) {
|
|
103
|
+
K({
|
|
104
|
+
tone: "error",
|
|
105
|
+
text: I("planmagnet.invite.pending.cancelFailed", "The invitation to {{email}} could not be cancelled: {{reason}}", {
|
|
106
|
+
email: e.email,
|
|
107
|
+
reason: i(t, I("planmagnet.invite.pending.cancelFailedFallback", "Try again."), J)
|
|
108
|
+
})
|
|
109
|
+
}), Y();
|
|
110
|
+
} finally {
|
|
111
|
+
W(null);
|
|
112
|
+
}
|
|
113
|
+
}, Q = L?.workspaceInvitations, $ = Q?.items ?? [], ee = (e) => {
|
|
114
|
+
let { names: t, unknown: n } = l(e, F), r = [...t];
|
|
115
|
+
return n > 0 && r.push(I("planmagnet.invite.pending.otherRoles", "{{count}} other access level(s)", { count: n })), r.length > 0 ? r.join(", ") : I("planmagnet.invite.pending.noRoles", "No access level");
|
|
116
|
+
};
|
|
117
|
+
return /* @__PURE__ */ y(S, {
|
|
118
|
+
"data-testid": "pending-invitations",
|
|
119
|
+
children: [/* @__PURE__ */ v(w, { children: /* @__PURE__ */ y(T, {
|
|
120
|
+
className: "flex items-center gap-2",
|
|
121
|
+
children: [/* @__PURE__ */ v(f, { className: "h-4 w-4" }), I("planmagnet.invite.pending.title", "Pending invitations")]
|
|
122
|
+
}) }), /* @__PURE__ */ y(C, {
|
|
123
|
+
className: "space-y-3",
|
|
124
|
+
children: [
|
|
125
|
+
G && /* @__PURE__ */ v("p", {
|
|
126
|
+
role: G.tone === "error" ? "alert" : "status",
|
|
127
|
+
className: `text-sm ${N[G.tone]}`,
|
|
128
|
+
"data-testid": "pending-invitations-notice",
|
|
129
|
+
children: G.text
|
|
130
|
+
}),
|
|
131
|
+
R && !L ? /* @__PURE__ */ v("p", {
|
|
132
|
+
className: "text-sm text-muted-foreground",
|
|
133
|
+
children: I("planmagnet.invite.pending.loading", "Loading pending invitations…")
|
|
134
|
+
}) : z && !L ? /* @__PURE__ */ v("p", {
|
|
135
|
+
role: "alert",
|
|
136
|
+
className: "text-sm text-destructive",
|
|
137
|
+
children: t(z) ? J : I("planmagnet.invite.pending.loadFailed", "Pending invitations could not be loaded: {{reason}}", { reason: z.message })
|
|
138
|
+
}) : $.length === 0 ? /* @__PURE__ */ v("p", {
|
|
139
|
+
className: "text-sm text-muted-foreground",
|
|
140
|
+
children: I("planmagnet.invite.pending.empty", "No invitations are waiting to be accepted.")
|
|
141
|
+
}) : /* @__PURE__ */ v("ul", {
|
|
142
|
+
className: "divide-y divide-border",
|
|
143
|
+
"data-testid": "pending-invitations-list",
|
|
144
|
+
children: $.map((e) => {
|
|
145
|
+
let t = s(e), n = c(e.emailStatus), i = o(e.emailErrorCode), l = P(e.emailLastAttemptAt), d = u(e.expiresAt), f = !!d && d.getTime() <= q, h = U === e.id;
|
|
146
|
+
return /* @__PURE__ */ y("li", {
|
|
147
|
+
className: "flex flex-col gap-2 py-3 sm:flex-row sm:items-start sm:justify-between",
|
|
148
|
+
"data-testid": "pending-invitation",
|
|
149
|
+
children: [/* @__PURE__ */ y("div", {
|
|
150
|
+
className: "min-w-0 space-y-1",
|
|
151
|
+
children: [
|
|
152
|
+
/* @__PURE__ */ v("p", {
|
|
153
|
+
className: "truncate text-sm font-medium",
|
|
154
|
+
children: e.email
|
|
155
|
+
}),
|
|
156
|
+
/* @__PURE__ */ v("p", {
|
|
157
|
+
className: "text-xs text-muted-foreground",
|
|
158
|
+
children: ee(e.roleIds)
|
|
159
|
+
}),
|
|
160
|
+
/* @__PURE__ */ y("div", {
|
|
161
|
+
className: "flex flex-wrap items-center gap-2 text-xs text-muted-foreground",
|
|
162
|
+
children: [
|
|
163
|
+
/* @__PURE__ */ v(b, {
|
|
164
|
+
variant: a(e.emailStatus),
|
|
165
|
+
"data-testid": "pending-invitation-email-status",
|
|
166
|
+
children: I(n.key, n.fallback)
|
|
167
|
+
}),
|
|
168
|
+
l && /* @__PURE__ */ v("span", { children: I("planmagnet.invite.pending.lastAttempt", "Last tried {{when}}", { when: l }) }),
|
|
169
|
+
d && /* @__PURE__ */ v("span", {
|
|
170
|
+
className: f ? "text-status-warning-text" : void 0,
|
|
171
|
+
children: f ? I("planmagnet.invite.pending.expired", "Expired {{when}}", { when: d.toLocaleString() }) : I("planmagnet.invite.pending.expires", "Expires {{when}}", { when: d.toLocaleString() })
|
|
172
|
+
})
|
|
173
|
+
]
|
|
174
|
+
}),
|
|
175
|
+
!t && e.emailStatus && /* @__PURE__ */ v("p", {
|
|
176
|
+
className: "text-xs text-status-warning-text",
|
|
177
|
+
children: I(i.key, i.fallback)
|
|
178
|
+
})
|
|
179
|
+
]
|
|
180
|
+
}), /* @__PURE__ */ y("div", {
|
|
181
|
+
className: "flex shrink-0 gap-2",
|
|
182
|
+
children: [/* @__PURE__ */ y(x, {
|
|
183
|
+
size: "sm",
|
|
184
|
+
variant: "outline",
|
|
185
|
+
disabled: U !== null,
|
|
186
|
+
onClick: () => void X(e),
|
|
187
|
+
"data-testid": "pending-invitation-resend",
|
|
188
|
+
children: [/* @__PURE__ */ v(p, { className: "mr-1 h-4 w-4" }), h ? I("planmagnet.invite.pending.working", "Working…") : I("planmagnet.invite.pending.resend", "Resend")]
|
|
189
|
+
}), e.invitedById === D && /* @__PURE__ */ v(r, {
|
|
190
|
+
label: I("planmagnet.invite.pending.cancel", "Cancel"),
|
|
191
|
+
variant: "outline",
|
|
192
|
+
icon: /* @__PURE__ */ v(m, { className: "mr-1 h-4 w-4" }),
|
|
193
|
+
testId: "pending-invitation-cancel",
|
|
194
|
+
title: I("planmagnet.invite.pending.confirmCancel.title", "Cancel the invitation to {{email}}?", { email: e.email }),
|
|
195
|
+
description: I("planmagnet.invite.pending.confirmCancel.description", "Its link stops working and they will not join this workspace from it. You can invite them again later."),
|
|
196
|
+
confirmLabel: I("planmagnet.invite.pending.confirmCancel.confirm", "Cancel invitation"),
|
|
197
|
+
cancelLabel: I("planmagnet.invite.pending.confirmCancel.keep", "Keep it"),
|
|
198
|
+
destructive: !0,
|
|
199
|
+
disabled: U !== null,
|
|
200
|
+
onConfirm: () => void Z(e)
|
|
201
|
+
})]
|
|
202
|
+
})]
|
|
203
|
+
}, e.id);
|
|
204
|
+
})
|
|
205
|
+
}),
|
|
206
|
+
Q?.hasMore && /* @__PURE__ */ v("p", {
|
|
207
|
+
className: "text-xs text-muted-foreground",
|
|
208
|
+
children: I("planmagnet.invite.pending.truncated", "Showing the first {{shown}} of {{total}} pending invitations.", {
|
|
209
|
+
shown: $.length,
|
|
210
|
+
total: Q.total
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
]
|
|
214
|
+
})]
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
//#endregion
|
|
218
|
+
export { D as PENDING_INVITATIONS_QUERY, F as PendingInvitations, A as RESEND_INVITATION };
|
|
219
|
+
|
|
220
|
+
//# sourceMappingURL=PendingInvitations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PendingInvitations.js","names":[],"sources":["../../../src/planmagnet/team/PendingInvitations.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { clearGraphqlFetchResponseCache } from '@burdenoff/fe-libs/shared/graphql';\nimport { Badge, Button, Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { Mail, RefreshCw, X } from 'lucide-react';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { ConfirmButton } from '../controls/ConfirmButton';\nimport { refusalMessage } from '../product/refusal';\nimport { isForbidden } from '../utils/graphqlErrors';\nimport { throwOnError } from '../utils/mutationResult';\nimport {\n invitationEmailBadgeVariant,\n invitationEmailReason,\n invitationEmailSent,\n invitationEmailStatusLabel,\n invitationRoleNames,\n parseInvitationDate,\n type InvitationEmailFields,\n} from './invitationEmail';\n\n/**\n * The workspace's pending invitations on the Invite page (BOFF-8866): who was invited, with which access, whether\n * the invitation email actually went out (and when it was last tried), when the invitation expires, and Resend /\n * Cancel. `workspaceInvitations` and the two writes read the workspace from the gateway's x-workspace-id header.\n * A write's outcome is shown above the list, so a row that leaves the list on the refresh does not take its message\n * with it.\n */\n\n/** Operation name of the list, for `refetchQueries` of every write that changes it. */\nexport const PENDING_INVITATIONS_QUERY = 'PlanmagnetPendingInvitations';\n\nconst PAGE_LIMIT = 50;\n\nconst PENDING_INVITATIONS = gql`\n query PlanmagnetPendingInvitations($pagination: PaginationInput) {\n workspaceInvitations(status: PENDING, pagination: $pagination) {\n items {\n id\n email\n status\n roleIds\n invitedById\n expiresAt\n emailStatus\n emailLastAttemptAt\n emailErrorCode\n }\n total\n hasMore\n }\n }\n`;\n\n/** Sends the invitation email again (and renews the invitation's expiry); answers with the new email outcome. */\nexport const RESEND_INVITATION = gql`\n mutation PlanmagnetResendInvitation($id: ID!) {\n resendInvitation(id: $id) {\n id\n status\n expiresAt\n emailStatus\n emailLastAttemptAt\n emailErrorCode\n }\n }\n`;\n\nconst CANCEL_INVITATION = gql`\n mutation PlanmagnetCancelInvitation($id: ID!) {\n cancelInvitation(id: $id) {\n id\n status\n }\n }\n`;\n\nexport interface InvitationEmailResult extends InvitationEmailFields {\n id: string;\n status: string;\n expiresAt: string | null;\n}\n\ninterface PendingInvitation extends InvitationEmailResult {\n email: string;\n roleIds: string[];\n invitedById: string | null;\n}\n\ninterface PendingInvitationsData {\n workspaceInvitations: { items: PendingInvitation[]; total: number; hasMore: boolean };\n}\n\nconst WRITE_OPTIONS = { refetchQueries: [PENDING_INVITATIONS_QUERY], awaitRefetchQueries: true };\n\ntype Notice = { tone: 'success' | 'warning' | 'error'; text: string };\n\nconst NOTICE_CLASS: Record<Notice['tone'], string> = {\n success: 'text-status-success-text',\n warning: 'text-status-warning-text',\n error: 'text-destructive',\n};\n\nconst formatDate = (value: string | null | undefined): string | null =>\n parseInvitationDate(value)?.toLocaleString() ?? null;\n\nexport function PendingInvitations({\n workspaceId,\n currentUserId,\n roleNames,\n}: {\n /** The workspace the list is for. The query is scoped by a request header, so callers key this component on it. */\n workspaceId?: string | null;\n /** Cancelling is the inviter's own action; other people's invitations are listed without it. */\n currentUserId?: string | null;\n /** PlanMagnet access-level names by role id, as the page already loaded them. */\n roleNames: ReadonlyMap<string, string>;\n}) {\n const tr = useTr();\n const { data, loading, error, refetch } = useQuery<PendingInvitationsData>(PENDING_INVITATIONS, {\n variables: { pagination: { limit: PAGE_LIMIT } },\n skip: !workspaceId,\n // Header-scoped (x-workspace-id), so its cache key is the same in every workspace: never render a cached\n // result from another workspace's visit, always wait for this workspace's answer.\n fetchPolicy: 'network-only',\n });\n const [resend] = useMutation<{ resendInvitation: InvitationEmailResult }>(\n RESEND_INVITATION,\n WRITE_OPTIONS\n );\n const [cancel] = useMutation(CANCEL_INVITATION, WRITE_OPTIONS);\n const [busyId, setBusyId] = useState<string | null>(null);\n const [notice, setNotice] = useState<Notice | null>(null);\n // The clock the expiry badge is judged against, read once per page visit (render must stay pure).\n const [now] = useState(() => Date.now());\n\n const forbidden = tr(\n 'planmagnet.invite.pending.forbidden',\n 'Your access level does not allow managing invitations in this workspace.'\n );\n\n // A refused write (e.g. the invitation was accepted or cancelled elsewhere) leaves the 15 s fetch replay in place;\n // drop it so the re-read shows the invitation as it is now.\n const reloadAfterRefusal = () => {\n clearGraphqlFetchResponseCache();\n void refetch();\n };\n\n const onResend = async (invitation: PendingInvitation) => {\n setBusyId(invitation.id);\n setNotice(null);\n try {\n const result = await throwOnError(resend({ variables: { id: invitation.id } }));\n const updated = result.data?.resendInvitation;\n if (invitationEmailSent(updated)) {\n setNotice({\n tone: 'success',\n text: tr(\n 'planmagnet.invite.pending.resent',\n 'Invitation email sent again to {{email}}.',\n {\n email: invitation.email,\n }\n ),\n });\n } else {\n const reason = invitationEmailReason(updated?.emailErrorCode);\n setNotice({\n tone: 'warning',\n text: tr(\n 'planmagnet.invite.pending.resendNotSent',\n 'The invitation to {{email}} is still pending, but the email was not sent. {{reason}}',\n { email: invitation.email, reason: tr(reason.key, reason.fallback) }\n ),\n });\n }\n } catch (e) {\n setNotice({\n tone: 'error',\n text: tr(\n 'planmagnet.invite.pending.resendFailed',\n 'The invitation to {{email}} could not be resent: {{reason}}',\n {\n email: invitation.email,\n reason: refusalMessage(\n e,\n tr('planmagnet.invite.pending.resendFailedFallback', 'Try again.'),\n forbidden\n ),\n }\n ),\n });\n reloadAfterRefusal();\n } finally {\n setBusyId(null);\n }\n };\n\n const onCancel = async (invitation: PendingInvitation) => {\n setBusyId(invitation.id);\n setNotice(null);\n try {\n await throwOnError(cancel({ variables: { id: invitation.id } }));\n setNotice({\n tone: 'success',\n text: tr(\n 'planmagnet.invite.pending.cancelled',\n 'The invitation to {{email}} was cancelled.',\n {\n email: invitation.email,\n }\n ),\n });\n } catch (e) {\n setNotice({\n tone: 'error',\n text: tr(\n 'planmagnet.invite.pending.cancelFailed',\n 'The invitation to {{email}} could not be cancelled: {{reason}}',\n {\n email: invitation.email,\n reason: refusalMessage(\n e,\n tr('planmagnet.invite.pending.cancelFailedFallback', 'Try again.'),\n forbidden\n ),\n }\n ),\n });\n reloadAfterRefusal();\n } finally {\n setBusyId(null);\n }\n };\n\n const list = data?.workspaceInvitations;\n const items = list?.items ?? [];\n\n const rolesText = (roleIds: string[]): string => {\n const { names, unknown } = invitationRoleNames(roleIds, roleNames);\n const parts = [...names];\n if (unknown > 0) {\n parts.push(\n tr('planmagnet.invite.pending.otherRoles', '{{count}} other access level(s)', {\n count: unknown,\n })\n );\n }\n return parts.length > 0\n ? parts.join(', ')\n : tr('planmagnet.invite.pending.noRoles', 'No access level');\n };\n\n return (\n <Card data-testid=\"pending-invitations\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <Mail className=\"h-4 w-4\" />\n {tr('planmagnet.invite.pending.title', 'Pending invitations')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {notice && (\n <p\n role={notice.tone === 'error' ? 'alert' : 'status'}\n className={`text-sm ${NOTICE_CLASS[notice.tone]}`}\n data-testid=\"pending-invitations-notice\"\n >\n {notice.text}\n </p>\n )}\n\n {loading && !data ? (\n <p className=\"text-sm text-muted-foreground\">\n {tr('planmagnet.invite.pending.loading', 'Loading pending invitations…')}\n </p>\n ) : error && !data ? (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {isForbidden(error)\n ? forbidden\n : tr(\n 'planmagnet.invite.pending.loadFailed',\n 'Pending invitations could not be loaded: {{reason}}',\n { reason: error.message }\n )}\n </p>\n ) : items.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n {tr('planmagnet.invite.pending.empty', 'No invitations are waiting to be accepted.')}\n </p>\n ) : (\n <ul className=\"divide-y divide-border\" data-testid=\"pending-invitations-list\">\n {items.map((invitation) => {\n const sent = invitationEmailSent(invitation);\n const status = invitationEmailStatusLabel(invitation.emailStatus);\n const reason = invitationEmailReason(invitation.emailErrorCode);\n const attempted = formatDate(invitation.emailLastAttemptAt);\n const expires = parseInvitationDate(invitation.expiresAt);\n const expired = !!expires && expires.getTime() <= now;\n const busy = busyId === invitation.id;\n return (\n <li\n key={invitation.id}\n className=\"flex flex-col gap-2 py-3 sm:flex-row sm:items-start sm:justify-between\"\n data-testid=\"pending-invitation\"\n >\n <div className=\"min-w-0 space-y-1\">\n <p className=\"truncate text-sm font-medium\">{invitation.email}</p>\n <p className=\"text-xs text-muted-foreground\">{rolesText(invitation.roleIds)}</p>\n <div className=\"flex flex-wrap items-center gap-2 text-xs text-muted-foreground\">\n <Badge\n variant={invitationEmailBadgeVariant(invitation.emailStatus)}\n data-testid=\"pending-invitation-email-status\"\n >\n {tr(status.key, status.fallback)}\n </Badge>\n {attempted && (\n <span>\n {tr('planmagnet.invite.pending.lastAttempt', 'Last tried {{when}}', {\n when: attempted,\n })}\n </span>\n )}\n {expires && (\n <span className={expired ? 'text-status-warning-text' : undefined}>\n {expired\n ? tr('planmagnet.invite.pending.expired', 'Expired {{when}}', {\n when: expires.toLocaleString(),\n })\n : tr('planmagnet.invite.pending.expires', 'Expires {{when}}', {\n when: expires.toLocaleString(),\n })}\n </span>\n )}\n </div>\n {!sent && invitation.emailStatus && (\n <p className=\"text-xs text-status-warning-text\">\n {tr(reason.key, reason.fallback)}\n </p>\n )}\n </div>\n <div className=\"flex shrink-0 gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={busyId !== null}\n onClick={() => void onResend(invitation)}\n data-testid=\"pending-invitation-resend\"\n >\n <RefreshCw className=\"mr-1 h-4 w-4\" />\n {busy\n ? tr('planmagnet.invite.pending.working', 'Working…')\n : tr('planmagnet.invite.pending.resend', 'Resend')}\n </Button>\n {invitation.invitedById === currentUserId && (\n <ConfirmButton\n label={tr('planmagnet.invite.pending.cancel', 'Cancel')}\n variant=\"outline\"\n icon={<X className=\"mr-1 h-4 w-4\" />}\n testId=\"pending-invitation-cancel\"\n title={tr(\n 'planmagnet.invite.pending.confirmCancel.title',\n 'Cancel the invitation to {{email}}?',\n { email: invitation.email }\n )}\n description={tr(\n 'planmagnet.invite.pending.confirmCancel.description',\n 'Its link stops working and they will not join this workspace from it. You can invite them again later.'\n )}\n confirmLabel={tr(\n 'planmagnet.invite.pending.confirmCancel.confirm',\n 'Cancel invitation'\n )}\n cancelLabel={tr('planmagnet.invite.pending.confirmCancel.keep', 'Keep it')}\n destructive\n disabled={busyId !== null}\n onConfirm={() => void onCancel(invitation)}\n />\n )}\n </div>\n </li>\n );\n })}\n </ul>\n )}\n\n {list?.hasMore && (\n <p className=\"text-xs text-muted-foreground\">\n {tr(\n 'planmagnet.invite.pending.truncated',\n 'Showing the first {{shown}} of {{total}} pending invitations.',\n { shown: items.length, total: list.total }\n )}\n </p>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invitation email outcome (BOFF-8866). The workspace service records what happened to the most recent invitation
|
|
3
|
+
* email on the invitation itself: `emailStatus` (SENT / FAILED / SKIPPED, null before any attempt was recorded),
|
|
4
|
+
* `emailLastAttemptAt`, and `emailErrorCode`, a machine code for a FAILED or SKIPPED outcome. Creating an
|
|
5
|
+
* invitation used to be reported as "Invitation sent" even when no email went out; these helpers turn the recorded
|
|
6
|
+
* outcome into what the invite page says. Each message is an i18n key with its English fallback, for `useTr`.
|
|
7
|
+
*/
|
|
8
|
+
export type InvitationEmailStatus = 'SENT' | 'FAILED' | 'SKIPPED';
|
|
9
|
+
export interface InvitationEmailFields {
|
|
10
|
+
emailStatus?: InvitationEmailStatus | null;
|
|
11
|
+
emailLastAttemptAt?: string | null;
|
|
12
|
+
emailErrorCode?: string | null;
|
|
13
|
+
}
|
|
14
|
+
export interface TrMessage {
|
|
15
|
+
key: string;
|
|
16
|
+
fallback: string;
|
|
17
|
+
}
|
|
18
|
+
/** Only a recorded SENT counts as sent: FAILED, SKIPPED and a missing outcome all mean no email is known to have left. */
|
|
19
|
+
export declare function invitationEmailSent(invitation: InvitationEmailFields | null | undefined): boolean;
|
|
20
|
+
/** Why the email was not sent, worded for the inviter, from the service's machine code. */
|
|
21
|
+
export declare function invitationEmailReason(code: string | null | undefined): TrMessage;
|
|
22
|
+
/** The short label for a pending invitation's email column. */
|
|
23
|
+
export declare function invitationEmailStatusLabel(status: InvitationEmailStatus | null | undefined): TrMessage;
|
|
24
|
+
export declare function invitationEmailBadgeVariant(status: InvitationEmailStatus | null | undefined): 'secondary' | 'destructive' | 'outline';
|
|
25
|
+
/**
|
|
26
|
+
* A DateTime from the gateway as a Date. The scalar is an ISO string, but a timestamp that reaches the client as
|
|
27
|
+
* epoch milliseconds in a string is read too, so a serializer change cannot turn a date into "Invalid Date".
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseInvitationDate(value: string | null | undefined): Date | null;
|
|
30
|
+
/** The names of an invitation's roles that this page knows; ids it cannot name are counted, never shown raw. */
|
|
31
|
+
export declare function invitationRoleNames(roleIds: readonly string[], known: ReadonlyMap<string, string>): {
|
|
32
|
+
names: string[];
|
|
33
|
+
unknown: number;
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=invitationEmail.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invitationEmail.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/team/invitationEmail.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAElE,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAAC;IAC3C,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,0HAA0H;AAC1H,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,qBAAqB,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAEjG;AAED,2FAA2F;AAC3F,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,SAAS,CA8BhF;AAED,+DAA+D;AAC/D,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,qBAAqB,GAAG,IAAI,GAAG,SAAS,GAC/C,SAAS,CAWX;AAED,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,qBAAqB,GAAG,IAAI,GAAG,SAAS,GAC/C,WAAW,GAAG,aAAa,GAAG,SAAS,CAIzC;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,GAAG,IAAI,CAIjF;AAED,gHAAgH;AAChH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GACjC;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAStC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
//#region src/planmagnet/team/invitationEmail.ts
|
|
2
|
+
function e(e) {
|
|
3
|
+
return e?.emailStatus === "SENT";
|
|
4
|
+
}
|
|
5
|
+
function t(e) {
|
|
6
|
+
switch (e) {
|
|
7
|
+
case "RECIPIENT_UNSUBSCRIBED": return {
|
|
8
|
+
key: "planmagnet.invite.email.reason.unsubscribed",
|
|
9
|
+
fallback: "The recipient has opted out of emails."
|
|
10
|
+
};
|
|
11
|
+
case "EMAIL_NOT_CONFIGURED": return {
|
|
12
|
+
key: "planmagnet.invite.email.reason.notConfigured",
|
|
13
|
+
fallback: "Invitation emails are not set up in this environment."
|
|
14
|
+
};
|
|
15
|
+
case "RATE_LIMITED": return {
|
|
16
|
+
key: "planmagnet.invite.email.reason.rateLimited",
|
|
17
|
+
fallback: "Too many emails were sent at once. Resend it in a few minutes."
|
|
18
|
+
};
|
|
19
|
+
case null:
|
|
20
|
+
case void 0:
|
|
21
|
+
case "": return {
|
|
22
|
+
key: "planmagnet.invite.email.reason.unknown",
|
|
23
|
+
fallback: "No email delivery was recorded for it."
|
|
24
|
+
};
|
|
25
|
+
default: return {
|
|
26
|
+
key: "planmagnet.invite.email.reason.deliveryFailed",
|
|
27
|
+
fallback: "The email service could not send it. Try resending it."
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function n(e) {
|
|
32
|
+
switch (e) {
|
|
33
|
+
case "SENT": return {
|
|
34
|
+
key: "planmagnet.invite.email.status.sent",
|
|
35
|
+
fallback: "Email sent"
|
|
36
|
+
};
|
|
37
|
+
case "FAILED": return {
|
|
38
|
+
key: "planmagnet.invite.email.status.failed",
|
|
39
|
+
fallback: "Email not sent"
|
|
40
|
+
};
|
|
41
|
+
case "SKIPPED": return {
|
|
42
|
+
key: "planmagnet.invite.email.status.skipped",
|
|
43
|
+
fallback: "Email not sent"
|
|
44
|
+
};
|
|
45
|
+
default: return {
|
|
46
|
+
key: "planmagnet.invite.email.status.unknown",
|
|
47
|
+
fallback: "Email not recorded"
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function r(e) {
|
|
52
|
+
return e === "SENT" ? "secondary" : e === "FAILED" || e === "SKIPPED" ? "destructive" : "outline";
|
|
53
|
+
}
|
|
54
|
+
function i(e) {
|
|
55
|
+
if (!e) return null;
|
|
56
|
+
let t = /^\d+$/.test(e) ? new Date(Number(e)) : new Date(e);
|
|
57
|
+
return Number.isNaN(t.getTime()) ? null : t;
|
|
58
|
+
}
|
|
59
|
+
function a(e, t) {
|
|
60
|
+
let n = [], r = 0;
|
|
61
|
+
for (let i of e) {
|
|
62
|
+
let e = t.get(i);
|
|
63
|
+
e ? n.push(e) : r++;
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
names: n,
|
|
67
|
+
unknown: r
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
export { r as invitationEmailBadgeVariant, t as invitationEmailReason, e as invitationEmailSent, n as invitationEmailStatusLabel, a as invitationRoleNames, i as parseInvitationDate };
|
|
72
|
+
|
|
73
|
+
//# sourceMappingURL=invitationEmail.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invitationEmail.js","names":[],"sources":["../../../src/planmagnet/team/invitationEmail.ts"],"sourcesContent":["/**\n * Invitation email outcome (BOFF-8866). The workspace service records what happened to the most recent invitation\n * email on the invitation itself: `emailStatus` (SENT / FAILED / SKIPPED, null before any attempt was recorded),\n * `emailLastAttemptAt`, and `emailErrorCode`, a machine code for a FAILED or SKIPPED outcome. Creating an\n * invitation used to be reported as \"Invitation sent\" even when no email went out; these helpers turn the recorded\n * outcome into what the invite page says. Each message is an i18n key with its English fallback, for `useTr`.\n */\n\nexport type InvitationEmailStatus = 'SENT' | 'FAILED' | 'SKIPPED';\n\nexport interface InvitationEmailFields {\n emailStatus?: InvitationEmailStatus | null;\n emailLastAttemptAt?: string | null;\n emailErrorCode?: string | null;\n}\n\nexport interface TrMessage {\n key: string;\n fallback: string;\n}\n\n/** Only a recorded SENT counts as sent: FAILED, SKIPPED and a missing outcome all mean no email is known to have left. */\nexport function invitationEmailSent(invitation: InvitationEmailFields | null | undefined): boolean {\n return invitation?.emailStatus === 'SENT';\n}\n\n/** Why the email was not sent, worded for the inviter, from the service's machine code. */\nexport function invitationEmailReason(code: string | null | undefined): TrMessage {\n switch (code) {\n case 'RECIPIENT_UNSUBSCRIBED':\n return {\n key: 'planmagnet.invite.email.reason.unsubscribed',\n fallback: 'The recipient has opted out of emails.',\n };\n case 'EMAIL_NOT_CONFIGURED':\n return {\n key: 'planmagnet.invite.email.reason.notConfigured',\n fallback: 'Invitation emails are not set up in this environment.',\n };\n case 'RATE_LIMITED':\n return {\n key: 'planmagnet.invite.email.reason.rateLimited',\n fallback: 'Too many emails were sent at once. Resend it in a few minutes.',\n };\n case null:\n case undefined:\n case '':\n return {\n key: 'planmagnet.invite.email.reason.unknown',\n fallback: 'No email delivery was recorded for it.',\n };\n default:\n return {\n key: 'planmagnet.invite.email.reason.deliveryFailed',\n fallback: 'The email service could not send it. Try resending it.',\n };\n }\n}\n\n/** The short label for a pending invitation's email column. */\nexport function invitationEmailStatusLabel(\n status: InvitationEmailStatus | null | undefined\n): TrMessage {\n switch (status) {\n case 'SENT':\n return { key: 'planmagnet.invite.email.status.sent', fallback: 'Email sent' };\n case 'FAILED':\n return { key: 'planmagnet.invite.email.status.failed', fallback: 'Email not sent' };\n case 'SKIPPED':\n return { key: 'planmagnet.invite.email.status.skipped', fallback: 'Email not sent' };\n default:\n return { key: 'planmagnet.invite.email.status.unknown', fallback: 'Email not recorded' };\n }\n}\n\nexport function invitationEmailBadgeVariant(\n status: InvitationEmailStatus | null | undefined\n): 'secondary' | 'destructive' | 'outline' {\n if (status === 'SENT') return 'secondary';\n if (status === 'FAILED' || status === 'SKIPPED') return 'destructive';\n return 'outline';\n}\n\n/**\n * A DateTime from the gateway as a Date. The scalar is an ISO string, but a timestamp that reaches the client as\n * epoch milliseconds in a string is read too, so a serializer change cannot turn a date into \"Invalid Date\".\n */\nexport function parseInvitationDate(value: string | null | undefined): Date | null {\n if (!value) return null;\n const date = /^\\d+$/.test(value) ? new Date(Number(value)) : new Date(value);\n return Number.isNaN(date.getTime()) ? null : date;\n}\n\n/** The names of an invitation's roles that this page knows; ids it cannot name are counted, never shown raw. */\nexport function invitationRoleNames(\n roleIds: readonly string[],\n known: ReadonlyMap<string, string>\n): { names: string[]; unknown: number } {\n const names: string[] = [];\n let unknown = 0;\n for (const id of roleIds) {\n const name = known.get(id);\n if (name) names.push(name);\n else unknown++;\n }\n return { names, unknown };\n}\n"],"mappings":";AAsBA,SAAgB,EAAoB,GAA+D;AACjG,QAAO,GAAY,gBAAgB;;AAIrC,SAAgB,EAAsB,GAA4C;AAChF,SAAQ,GAAR;EACE,KAAK,yBACH,QAAO;GACL,KAAK;GACL,UAAU;GACX;EACH,KAAK,uBACH,QAAO;GACL,KAAK;GACL,UAAU;GACX;EACH,KAAK,eACH,QAAO;GACL,KAAK;GACL,UAAU;GACX;EACH,KAAK;EACL,KAAK,KAAA;EACL,KAAK,GACH,QAAO;GACL,KAAK;GACL,UAAU;GACX;EACH,QACE,QAAO;GACL,KAAK;GACL,UAAU;GACX;;;AAKP,SAAgB,EACd,GACW;AACX,SAAQ,GAAR;EACE,KAAK,OACH,QAAO;GAAE,KAAK;GAAuC,UAAU;GAAc;EAC/E,KAAK,SACH,QAAO;GAAE,KAAK;GAAyC,UAAU;GAAkB;EACrF,KAAK,UACH,QAAO;GAAE,KAAK;GAA0C,UAAU;GAAkB;EACtF,QACE,QAAO;GAAE,KAAK;GAA0C,UAAU;GAAsB;;;AAI9F,SAAgB,EACd,GACyC;AAGzC,QAFI,MAAW,SAAe,cAC1B,MAAW,YAAY,MAAW,YAAkB,gBACjD;;AAOT,SAAgB,EAAoB,GAA+C;AACjF,KAAI,CAAC,EAAO,QAAO;CACnB,IAAM,IAAO,QAAQ,KAAK,EAAM,GAAG,IAAI,KAAK,OAAO,EAAM,CAAC,GAAG,IAAI,KAAK,EAAM;AAC5E,QAAO,OAAO,MAAM,EAAK,SAAS,CAAC,GAAG,OAAO;;AAI/C,SAAgB,EACd,GACA,GACsC;CACtC,IAAM,IAAkB,EAAE,EACtB,IAAU;AACd,MAAK,IAAM,KAAM,GAAS;EACxB,IAAM,IAAO,EAAM,IAAI,EAAG;AAC1B,EAAI,IAAM,EAAM,KAAK,EAAK,GACrB;;AAEP,QAAO;EAAE;EAAO;EAAS"}
|